feat: migrate workspace worker and companion domains to ServerApi
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user