feat: migrate workspace worker and companion domains to ServerApi

This commit is contained in:
2026-09-23 10:00:56 +09:00
parent ad12d77ad4
commit 89024d3b01
28 changed files with 7471 additions and 819 deletions
@@ -19,10 +19,7 @@ use crate::feature::{
ServiceDeclaration, ServiceId, ToolContribution, ToolDeclaration,
};
use crate::spawn::registry::{SpawnedWorkerRegistry, SubWorkerStopSummary};
use crate::worker::{
WorkspaceClient, WorkspaceClientError, WorkspaceRequest, WorkspaceRequestMethod,
WorkspaceResponse,
};
use crate::worker::{WorkspaceClient, WorkspaceClientError, WorkspaceResponse};
const FEATURE_ID: &str = "worker";
const FEATURE_NAME: &str = "Worker";
@@ -76,16 +73,22 @@ pub trait WorkerControlService: Send + Sync {
&self,
request: WorkerLifecycleSpawnRequest,
) -> Result<WorkspaceResponse, WorkspaceClientError>;
async fn restore_worker(
&self,
runtime_id: String,
worker_id: String,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
let _ = (runtime_id, worker_id);
Err(WorkspaceClientError::Unavailable(
"Runtime Worker restore is unavailable".to_string(),
))
}
fn remove_runtime_worker(
&self,
runtime_id: &str,
worker_id: &str,
reason: &str,
) -> Result<WorkspaceResponse, WorkspaceClientError>;
async fn execute_runtime(
&self,
request: WorkspaceRequest,
) -> Result<WorkspaceResponse, WorkspaceClientError>;
async fn ensure_permission(
&self,
subject: &super::worker_observation::WorkerObservationSubjectRef,
@@ -144,11 +147,9 @@ impl WorkerControlService for WorkspaceWorkerControlService {
async fn list_workers(&self) -> Result<WorkspaceResponse, WorkspaceClientError> {
let response = if self.runtime_worker_control {
self.execute_runtime(WorkspaceRequest::get(format!(
"/api/w/{}/worker-control/workers",
self.workspace_id
)))
.await?
self.client.execute_server_operation(
crate::worker::WorkspaceServerOperation::WorkerControlList,
)?
} else {
WorkspaceResponse {
status: 200,
@@ -210,19 +211,17 @@ impl WorkerControlService for WorkspaceWorkerControlService {
WorkerControlInputKind::User => "user",
WorkerControlInputKind::Notify => "notify",
};
self.execute_runtime(WorkspaceRequest::json(
WorkspaceRequestMethod::Post,
format!(
"/api/w/{}/worker-control/workers/{runtime_id}/{worker_id}/input",
self.workspace_id
),
serde_json::json!({
"kind": kind,
"content": content,
})
.to_string(),
))
.await
self.client.execute_server_operation(
crate::worker::WorkspaceServerOperation::WorkerControlInput {
runtime_id,
worker_id,
request: server_api::RuntimeWorkerInputRequest {
kind: Some(kind.to_string()),
content,
segments: None,
},
},
)
}
WorkerControlSubject::SubWorker { name } => {
if kind != WorkerControlInputKind::User {
@@ -273,19 +272,29 @@ impl WorkerControlService for WorkspaceWorkerControlService {
runtime_id,
worker_id,
} => {
let action = match kind {
WorkerControlStopKind::Cancel => "cancel",
WorkerControlStopKind::Stop => "stop",
let operation = match kind {
WorkerControlStopKind::Cancel => {
crate::worker::WorkspaceServerOperation::WorkerControlCancel {
runtime_id,
worker_id,
request: server_api::RuntimeWorkerLifecycleRequest {
reason,
ticket_assignment: None,
},
}
}
WorkerControlStopKind::Stop => {
crate::worker::WorkspaceServerOperation::WorkerControlStop {
runtime_id,
worker_id,
request: server_api::RuntimeWorkerLifecycleRequest {
reason,
ticket_assignment: None,
},
}
}
};
self.execute_runtime(WorkspaceRequest::json(
WorkspaceRequestMethod::Post,
format!(
"/api/w/{}/worker-control/workers/{runtime_id}/{worker_id}/{action}",
self.workspace_id
),
serde_json::json!({ "reason": reason }).to_string(),
))
.await
self.client.execute_server_operation(operation)
}
WorkerControlSubject::SubWorker { name } => {
if kind != WorkerControlStopKind::Stop {
@@ -329,12 +338,29 @@ impl WorkerControlService for WorkspaceWorkerControlService {
) -> Result<WorkspaceResponse, WorkspaceClientError> {
WorkspaceWorkerLifecycleService {
client: self.client.clone(),
workspace_id: self.workspace_id.clone(),
}
.spawn(request)
.await
}
async fn restore_worker(
&self,
runtime_id: String,
worker_id: String,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
if !self.runtime_worker_control {
return Err(WorkspaceClientError::Unavailable(
"Runtime Worker control is not enabled for this Worker".to_string(),
));
}
self.client.execute_server_operation(
crate::worker::WorkspaceServerOperation::WorkerControlRestore {
runtime_id,
worker_id,
},
)
}
fn remove_runtime_worker(
&self,
runtime_id: &str,
@@ -350,18 +376,6 @@ impl WorkerControlService for WorkspaceWorkerControlService {
.execute_worker_remove(runtime_id, worker_id, reason)
}
async fn execute_runtime(
&self,
request: WorkspaceRequest,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
if !self.runtime_worker_control {
return Err(WorkspaceClientError::Request(
"Runtime Worker control is not enabled for this Worker".to_string(),
));
}
self.client.execute(request)
}
async fn ensure_permission(
&self,
subject: &super::worker_observation::WorkerObservationSubjectRef,
@@ -391,10 +405,9 @@ impl WorkerControlService for WorkspaceWorkerControlService {
"Runtime Worker control is not enabled for this Worker".to_string(),
));
}
let response = self.client.execute(WorkspaceRequest::get(format!(
"/api/w/{}/worker-control/workers",
self.workspace_id
)))?;
let response = self.client.execute_server_operation(
crate::worker::WorkspaceServerOperation::WorkerControlList,
)?;
if !response.is_success() {
return Err(WorkspaceClientError::Request(format!(
"Workspace control request returned {}: {}",
@@ -459,7 +472,6 @@ pub struct WorkerLifecycleSpawnRequest {
struct WorkspaceWorkerLifecycleService {
client: Arc<dyn WorkspaceClient>,
workspace_id: String,
}
fn workspace_worker_create_request(
@@ -505,12 +517,9 @@ impl WorkerLifecycleService for WorkspaceWorkerLifecycleService {
request: WorkerLifecycleSpawnRequest,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
let body = workspace_worker_create_request(request)?;
self.client.execute(WorkspaceRequest::json(
WorkspaceRequestMethod::Post,
format!("/api/w/{}/worker-control/workers", self.workspace_id),
serde_json::to_string(&body)
.map_err(|error| WorkspaceClientError::Request(error.to_string()))?,
))
self.client.execute_server_operation(
crate::worker::WorkspaceServerOperation::WorkerControlSpawn(body),
)
}
}
@@ -639,20 +648,17 @@ impl FeatureModule for ManageWorkerFeature {
}
fn install(&self, context: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> {
let workspace_id = self
.client
self.client
.workspace_id()
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| {
FeatureInstallError::InvalidDescriptor(
"worker feature requires a Workspace id".to_string(),
)
})?
.to_string();
})?;
let lifecycle: Arc<dyn WorkerLifecycleService> =
Arc::new(WorkspaceWorkerLifecycleService {
client: self.client.clone(),
workspace_id: workspace_id.clone(),
});
context.services().provide(
ServiceDeclaration::new(
@@ -948,14 +954,7 @@ impl Tool for WorkspaceWorkerTool {
let input = parse::<WorkerTargetInput>(input_json, "WorkerRestore")?;
let (runtime_id, worker_id) = runtime_subject_ids(&input.subject, self.operation)?;
self.control
.execute_runtime(WorkspaceRequest::json(
WorkspaceRequestMethod::Post,
format!(
"/api/w/{}/worker-control/workers/{runtime_id}/{worker_id}/restore",
self.control.workspace_id()
),
"{}",
))
.restore_worker(runtime_id, worker_id)
.await
.map_err(control_tool_error)?
}
@@ -1191,7 +1190,7 @@ mod tests {
use std::sync::Mutex;
use super::*;
use crate::worker::{WorkspaceClientError, WorkspaceResponse};
use crate::worker::{WorkspaceClientError, WorkspaceRequest, WorkspaceResponse};
#[derive(Debug, Default)]
struct RecordingWorkspaceClient {
@@ -1361,15 +1360,6 @@ mod tests {
))
}
async fn execute_runtime(
&self,
_request: WorkspaceRequest,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
Err(WorkspaceClientError::Unavailable(
"Runtime Worker control is disabled in this test".to_string(),
))
}
async fn ensure_permission(
&self,
_subject: &crate::feature::builtin::WorkerObservationSubjectRef,
@@ -125,17 +125,16 @@ impl WorkerObservationProvider for WorkspaceClientWorkerObservationProvider {
async fn list_worker_sessions(
&self,
) -> Result<Vec<WorkerObservationSubject>, WorkerObservationError> {
let workspace_id = self.client.workspace_id().ok_or_else(|| {
let _workspace_id = self.client.workspace_id().ok_or_else(|| {
WorkerObservationError::Unavailable(
"Workspace observation requires a scoped Workspace client".to_string(),
)
})?;
let response = self
.client
.execute(crate::worker::WorkspaceRequest::get(format!(
"/api/w/{}/worker-observation/sessions",
workspace_id
)))
.execute_server_operation(
crate::worker::WorkspaceServerOperation::WorkerObservationSessions,
)
.map_err(workspace_client_error)?;
let body = workspace_response_body(response)?;
serde_json::from_str::<WorkspaceWorkerObservationListResponse>(&body)
@@ -147,20 +146,28 @@ impl WorkerObservationProvider for WorkspaceClientWorkerObservationProvider {
&self,
subject: &WorkerObservationSubjectRef,
) -> Result<WorkerSessionCapture, WorkerObservationError> {
let body = serde_json::to_string(subject)
.map_err(|error| WorkerObservationError::Unavailable(error.to_string()))?;
let workspace_id = self.client.workspace_id().ok_or_else(|| {
let request = match subject {
WorkerObservationSubjectRef::RuntimeWorker {
runtime_id,
worker_id,
} => server_api::WorkerObservationSubjectRef::RuntimeWorker {
runtime_id: runtime_id.clone(),
worker_id: worker_id.clone(),
},
WorkerObservationSubjectRef::SubWorker { name } => {
server_api::WorkerObservationSubjectRef::SubWorker { name: name.clone() }
}
};
let _workspace_id = self.client.workspace_id().ok_or_else(|| {
WorkerObservationError::Unavailable(
"Workspace observation requires a scoped Workspace client".to_string(),
)
})?;
let response = self
.client
.execute(crate::worker::WorkspaceRequest::json(
crate::worker::WorkspaceRequestMethod::Post,
format!("/api/w/{}/worker-observation/session", workspace_id),
body,
))
.execute_server_operation(
crate::worker::WorkspaceServerOperation::WorkerObservationCapture(request),
)
.map_err(workspace_client_error)?;
let body = workspace_response_body(response)?;
let response = serde_json::from_str::<WorkspaceWorkerObservationCaptureResponse>(&body)
+2 -2
View File
@@ -60,6 +60,6 @@ pub use worker::{
LocalWorkingDirectory, Worker, WorkerError, WorkerFilesystemAuthority, WorkerRunResult,
WorkerWorkspaceContext, WorkspaceClient, WorkspaceClientError, WorkspaceId, WorkspaceIdError,
WorkspacePromptCatalogResolution, WorkspaceRequest, WorkspaceRequestMethod, WorkspaceResponse,
WorkspaceWorkerDiscoveryRequest, apply_worker_manifest, marker_workspace_client,
unavailable_workspace_client,
WorkspaceServerOperation, WorkspaceWorkerDiscoveryRequest, apply_worker_manifest,
marker_workspace_client, unavailable_workspace_client,
};
+104
View File
@@ -544,6 +544,98 @@ pub struct WorkspaceWorkerDiscoveryRequest {
pub query: Option<String>,
}
#[derive(Debug, Clone)]
pub enum WorkspaceServerOperation {
WorkerControlList,
WorkerControlSpawn(server_api::CreateWorkspaceWorkerRequest),
WorkerControlInput {
runtime_id: String,
worker_id: String,
request: server_api::RuntimeWorkerInputRequest,
},
WorkerControlCancel {
runtime_id: String,
worker_id: String,
request: server_api::RuntimeWorkerLifecycleRequest,
},
WorkerControlStop {
runtime_id: String,
worker_id: String,
request: server_api::RuntimeWorkerLifecycleRequest,
},
WorkerControlRestore {
runtime_id: String,
worker_id: String,
},
WorkerObservationSessions,
WorkerObservationCapture(server_api::WorkerObservationSubjectRef),
}
fn workspace_server_json_request<T: serde::Serialize>(
path: String,
body: &T,
) -> Result<WorkspaceRequest, WorkspaceClientError> {
Ok(WorkspaceRequest::json(
WorkspaceRequestMethod::Post,
path,
serde_json::to_string(body)
.map_err(|error| WorkspaceClientError::Request(error.to_string()))?,
))
}
fn workspace_server_operation_request(
workspace_id: &str,
operation: WorkspaceServerOperation,
) -> Result<WorkspaceRequest, WorkspaceClientError> {
let base = format!("/api/w/{workspace_id}");
match operation {
WorkspaceServerOperation::WorkerControlList => Ok(WorkspaceRequest::get(format!(
"{base}/worker-control/workers"
))),
WorkspaceServerOperation::WorkerControlSpawn(request) => {
workspace_server_json_request(format!("{base}/worker-control/workers"), &request)
}
WorkspaceServerOperation::WorkerControlInput {
runtime_id,
worker_id,
request,
} => workspace_server_json_request(
format!("{base}/worker-control/workers/{runtime_id}/{worker_id}/input"),
&request,
),
WorkspaceServerOperation::WorkerControlCancel {
runtime_id,
worker_id,
request,
} => workspace_server_json_request(
format!("{base}/worker-control/workers/{runtime_id}/{worker_id}/cancel"),
&request,
),
WorkspaceServerOperation::WorkerControlStop {
runtime_id,
worker_id,
request,
} => workspace_server_json_request(
format!("{base}/worker-control/workers/{runtime_id}/{worker_id}/stop"),
&request,
),
WorkspaceServerOperation::WorkerControlRestore {
runtime_id,
worker_id,
} => Ok(WorkspaceRequest::json(
WorkspaceRequestMethod::Post,
format!("{base}/worker-control/workers/{runtime_id}/{worker_id}/restore"),
"".to_string(),
)),
WorkspaceServerOperation::WorkerObservationSessions => Ok(WorkspaceRequest::get(format!(
"{base}/worker-observation/sessions"
))),
WorkspaceServerOperation::WorkerObservationCapture(request) => {
workspace_server_json_request(format!("{base}/worker-observation/session"), &request)
}
}
}
/// Path-free Workspace operation authority injected by Runtime/host code.
///
/// Workers receive this trait object rather than a Backend URL. The concrete
@@ -556,6 +648,18 @@ pub trait WorkspaceClient: std::fmt::Debug + Send + Sync {
fn execute(&self, request: WorkspaceRequest)
-> Result<WorkspaceResponse, WorkspaceClientError>;
fn execute_server_operation(
&self,
operation: WorkspaceServerOperation,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
let workspace_id = self.workspace_id().ok_or_else(|| {
WorkspaceClientError::Unavailable(
"Workspace Server operation requires Workspace identity".to_string(),
)
})?;
self.execute(workspace_server_operation_request(workspace_id, operation)?)
}
/// Lists Workspace-visible Workers through dedicated Runtime-owned source
/// proof. Implementations must not fall back to generic Workspace request
/// authority.