Authorize known Worker observations without collection discovery
This commit is contained in:
@@ -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
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user