feat: gate built-in tools by profile features

This commit is contained in:
2026-06-09 21:05:38 +09:00
parent 41133e0cd5
commit f0f6cc92d8
19 changed files with 833 additions and 109 deletions
+121
View File
@@ -215,6 +215,127 @@ async fn wait_for_status(handle: &PodHandle, status: PodStatus) {
// ---------------------------------------------------------------------------
fn request_tool_names(request: &Request) -> Vec<String> {
let mut names = request
.tools
.iter()
.map(|tool| tool.name.clone())
.collect::<Vec<_>>();
names.sort();
names
}
async fn wait_for_captured_request(client: &MockClient) -> Request {
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
loop {
let requests = client.captured_requests();
if let Some(request) = requests.into_iter().next() {
return request;
}
assert!(
tokio::time::Instant::now() < deadline,
"timed out waiting for captured LLM request"
);
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
}
#[tokio::test]
async fn feature_flags_default_to_core_tool_surface_only() {
let client = MockClient::new(simple_text_events());
let client_for_assert = client.clone();
let pod = make_pod(client).await;
let handle = spawn_controller(pod).await;
handle.send(Method::run_text("Hello")).await.unwrap();
wait_for_status(&handle, PodStatus::Idle).await;
let request = wait_for_captured_request(&client_for_assert).await;
let names = request_tool_names(&request);
assert_eq!(names, vec!["Bash", "Edit", "Glob", "Grep", "Read", "Write"]);
assert!(!names.iter().any(|name| name == "TaskCreate"));
assert!(!names.iter().any(|name| name == "WebSearch"));
assert!(!names.iter().any(|name| name == "SpawnPod"));
}
#[tokio::test]
async fn enabled_task_and_web_features_register_their_tools() {
let manifest = r#"
[pod]
name = "feature-test-pod"
pwd = "./"
[model]
scheme = "anthropic"
model_id = "test-model"
[worker]
max_tokens = 100
[feature.task]
enabled = true
[feature.web]
enabled = true
[web]
enabled = false
[[scope.allow]]
target = "./"
permission = "write"
"#;
let client = MockClient::new(simple_text_events());
let client_for_assert = client.clone();
let pod = make_pod_with_pwd_and_manifest(client, manifest).await.0;
let handle = spawn_controller(pod).await;
handle.send(Method::run_text("Hello")).await.unwrap();
wait_for_status(&handle, PodStatus::Idle).await;
let request = wait_for_captured_request(&client_for_assert).await;
let names = request_tool_names(&request);
assert!(names.iter().any(|name| name == "TaskCreate"));
assert!(names.iter().any(|name| name == "TaskUpdate"));
assert!(names.iter().any(|name| name == "WebSearch"));
assert!(names.iter().any(|name| name == "WebFetch"));
assert!(!names.iter().any(|name| name == "SpawnPod"));
assert!(!names.iter().any(|name| name == "MemoryRead"));
}
#[tokio::test]
async fn pod_management_feature_requires_delegation_scope() {
let manifest = r#"
[pod]
name = "pod-management-feature-test"
pwd = "./"
[model]
scheme = "anthropic"
model_id = "test-model"
[worker]
max_tokens = 100
[feature.pod_management]
enabled = true
[[scope.allow]]
target = "./"
permission = "write"
"#;
let client = MockClient::new(simple_text_events());
let pod = make_pod_with_pwd_and_manifest(client, manifest).await.0;
let tmp = tempfile::tempdir().unwrap();
let result = PodController::spawn(pod, tmp.path()).await;
assert!(result.is_err());
let message = result.err().unwrap().to_string();
assert!(
message.contains("[feature.pod_management].enabled = true requires non-empty"),
"unexpected error: {message}"
);
}
#[tokio::test]
async fn run_end_returns_to_idle_without_busy_status() {
let client = MockClient::new(simple_text_events());