Authorize known Worker observations without collection discovery

This commit is contained in:
2026-10-30 00:37:56 +09:00
parent 74ddd568eb
commit 51f0c7e019
19 changed files with 406 additions and 88 deletions
+86 -1
View File
@@ -2045,7 +2045,7 @@ where
crate::feature::builtin::objective::composition::compose(
&mut feature_registry,
feature_config.objective.enabled,
workspace_client,
workspace_client.clone(),
objective_namespace.map(|route| (route, wip_permissions.clone())),
)?;
@@ -2066,6 +2066,15 @@ where
) {
observation_providers.push(provider);
}
// A scoped Backend provider belongs to the Worker Feature's composition,
// including ordinary spawn and restore. Runtime's optional local peer
// provider is not its enablement or authority: every actual capture still
// requires current operation use and the target's Backend control grant.
if worker_enabled {
observation_providers.push(Arc::new(
crate::feature::builtin::worker_observation::WorkspaceClientWorkerObservationProvider::new(workspace_client),
));
}
if let Some(provider) = host_worker_observation_provider {
observation_providers.push(provider);
}
@@ -4141,6 +4150,82 @@ mod tests {
}
}
#[tokio::test]
async fn scoped_observation_is_composed_only_with_runtime_worker_service_without_startup_access()
{
for enabled in [false, true] {
let root = TempDir::new().unwrap();
let mut manifest = manifest::WorkerManifest::from_toml(
"[worker]\nname = 'observation-wiring'\n[model]\nscheme = 'anthropic'\nmodel_id = 'test'\n[engine]\n[scope]\n"
).unwrap();
manifest.feature.worker.enabled = enabled;
manifest.feature.sub_worker.enabled = false;
manifest.feature.workdir_catalog.enabled = false;
let model = ScriptedClient::new(vec![]);
let engine =
Engine::<_, agen::state::Mutable, crate::SessionHistoryMetadata>::new_annotated(
model,
);
let store = session_store::CombinedStore::new(
session_store::FsStore::new(root.path().join("sessions")).unwrap(),
session_store::FsWorkerStore::new(root.path().join("workers")).unwrap(),
);
let operations = Arc::new(std::sync::Mutex::new(Vec::new()));
let host = Arc::new(DriveWorkspaceHost {
available: true,
workspace: Some("workspace-1"),
granted: false,
operations: operations.clone(),
});
let mut worker = Worker::new(
manifest,
engine,
store,
crate::worker::WorkerWorkspaceContext::with_client(
Some(crate::worker::WorkspaceId::new("workspace-1").unwrap()),
host,
),
crate::worker::WorkerFilesystemAuthority::None,
manifest::Scope::empty(),
)
.await
.unwrap();
worker.set_system_prompt_template(
crate::SystemPromptTemplate::parse(
"default",
crate::PromptCatalogSource::builtins_only(),
)
.unwrap(),
);
let prepared = WorkerController::initialize(worker, root.path(), None)
.await
.unwrap();
let Event::Snapshot { greeting, .. } =
crate::ipc::protocol_session::subscribe_worker_protocol_session(
&prepared.handle,
)
.snapshot_event
else {
panic!("initial committed greeting");
};
for name in [
"ViewSessionOverview",
"SearchSessionEntries",
"ReadSessionEntry",
] {
assert_eq!(
greeting.tools.iter().any(|tool| tool == name),
enabled,
"Worker service configuration must determine observation publication, not client availability alone"
);
}
assert!(
operations.lock().unwrap().is_empty(),
"provider composition may not authorize or read protected resources"
);
}
}
#[tokio::test]
async fn drive_controller_registers_without_authorization_and_checks_each_access_in_both_modes()
{
@@ -403,46 +403,26 @@ impl WorkerControlService for WorkspaceWorkerControlService {
"Runtime Worker control is not enabled for this Worker".to_string(),
));
}
self.authorize_runtime_operation(&format!("worker.{permission}"))?;
let operation = if permission == "notify" {
"worker.messaging".to_owned()
} else {
format!("worker.{permission}")
};
self.authorize_runtime_operation(&operation)?;
let response = self.client.execute_server_operation(
crate::worker::WorkspaceServerOperation::WorkerControlList,
crate::worker::WorkspaceServerOperation::WorkerControlAuthorize {
runtime_id: runtime_id.clone(),
worker_id: worker_id.clone(),
permission: permission.into(),
},
)?;
if !response.is_success() {
return Err(WorkspaceClientError::Request(format!(
"Workspace control request returned {}: {}",
response.status, response.body
)));
}
let body: serde_json::Value =
serde_json::from_str(&response.body).map_err(|error| {
WorkspaceClientError::Request(format!(
"invalid Workspace control response: {error}"
))
})?;
let granted = body
.get("items")
.and_then(serde_json::Value::as_array)
.into_iter()
.flatten()
.any(|item| {
item.get("subject")
.and_then(|value| value.get("runtime_id"))
== Some(&serde_json::Value::String(runtime_id.clone()))
&& item.get("subject").and_then(|value| value.get("worker_id"))
== Some(&serde_json::Value::String(worker_id.clone()))
&& item
.get("permissions")
.and_then(serde_json::Value::as_array)
.is_some_and(|permissions| {
permissions.iter().any(|candidate| candidate == permission)
})
});
if granted {
if response.is_success() {
Ok(())
} else {
Err(WorkspaceClientError::Request(
"unknown Worker or permission not granted".to_string(),
))
Err(WorkspaceClientError::Request(format!(
"Worker target permission check returned {}: {}",
response.status, response.body
)))
}
}
}
+15
View File
@@ -762,6 +762,11 @@ pub enum WorkspaceServerOperation {
result_id: String,
},
WorkerControlList,
WorkerControlAuthorize {
runtime_id: String,
worker_id: String,
permission: String,
},
WorkerControlSpawn(server_api::CreateWorkspaceWorkerRequest),
WorkerControlInput {
runtime_id: String,
@@ -959,6 +964,16 @@ fn workspace_server_operation_request(
WorkspaceServerOperation::WorkerControlList => Ok(WorkspaceRequest::get(format!(
"{base}/worker-control/workers"
))),
WorkspaceServerOperation::WorkerControlAuthorize {
runtime_id,
worker_id,
permission,
} => Ok(WorkspaceRequest::get(format!(
"{base}/worker-control/workers/{}/{}/permissions/{}",
encode_workspace_path_segment(&runtime_id),
encode_workspace_path_segment(&worker_id),
encode_workspace_path_segment(&permission)
))),
WorkspaceServerOperation::WorkerControlSpawn(request) => {
workspace_server_json_request(format!("{base}/worker-control/workers"), &request)
}