Address runtime API review findings

This commit is contained in:
2026-09-18 02:11:20 +09:00
parent d837684a33
commit fbdb6ccc22
4 changed files with 402 additions and 194 deletions
+55 -19
View File
@@ -638,28 +638,13 @@ pub struct RemainingRuntimeRoute {
pub const REMAINING_RUNTIME_ROUTES: &[RemainingRuntimeRoute] = &[ pub const REMAINING_RUNTIME_ROUTES: &[RemainingRuntimeRoute] = &[
RemainingRuntimeRoute { RemainingRuntimeRoute {
method: "POST", method: "POST",
path: "/v1/bootstrap", path: "/v1/workspace-runtime-verification/challenge",
reason: "bootstrap lifecycle", reason: "Workspace verification handshake",
},
RemainingRuntimeRoute {
method: "GET",
path: "/v1/protocol/ws",
reason: "WebSocket protocol transport",
},
RemainingRuntimeRoute {
method: "GET",
path: "/v1/workers/{worker_id}/protocol/ws",
reason: "WebSocket protocol transport",
}, },
RemainingRuntimeRoute { RemainingRuntimeRoute {
method: "POST", method: "POST",
path: "/v1/workers/{worker_id}/attachments", path: "/v1/workspace-runtime-verification/acknowledgement",
reason: "binary attachment transport", reason: "Workspace verification handshake",
},
RemainingRuntimeRoute {
method: "DELETE",
path: "/v1/workers/{worker_id}/attachments/{artifact_id}",
reason: "binary attachment transport",
}, },
RemainingRuntimeRoute { RemainingRuntimeRoute {
method: "GET", method: "GET",
@@ -691,6 +676,56 @@ pub const REMAINING_RUNTIME_ROUTES: &[RemainingRuntimeRoute] = &[
path: "/v1/working-directories", path: "/v1/working-directories",
reason: "Workdir transport", reason: "Workdir transport",
}, },
RemainingRuntimeRoute {
method: "POST",
path: "/v1/working-directories/repository-access",
reason: "Workdir transport",
},
RemainingRuntimeRoute {
method: "POST",
path: "/v1/repositories/ssh/probe",
reason: "Repository SSH transport",
},
RemainingRuntimeRoute {
method: "POST",
path: "/v1/repository-refs/observe",
reason: "Repository observation transport",
},
RemainingRuntimeRoute {
method: "POST",
path: "/v1/working-directories/{working_directory_id}/sessions",
reason: "Workdir session transport",
},
RemainingRuntimeRoute {
method: "POST",
path: "/v1/workdir-sessions/{session_id}/operations",
reason: "Workdir session transport",
},
RemainingRuntimeRoute {
method: "DELETE",
path: "/v1/workdir-sessions/{session_id}",
reason: "Workdir session transport",
},
RemainingRuntimeRoute {
method: "GET",
path: "/v1/working-directories/{working_directory_id}",
reason: "Workdir transport",
},
RemainingRuntimeRoute {
method: "DELETE",
path: "/v1/working-directories/{working_directory_id}",
reason: "Workdir transport",
},
RemainingRuntimeRoute {
method: "POST",
path: "/v1/workers/{worker_id}/attachments",
reason: "Binary attachment transport",
},
RemainingRuntimeRoute {
method: "DELETE",
path: "/v1/workers/{worker_id}/attachments/{artifact_id}",
reason: "Binary attachment transport",
},
]; ];
#[cfg(test)] #[cfg(test)]
@@ -866,6 +901,7 @@ mod tests {
#[test] #[test]
fn remaining_route_inventory_does_not_overlap_contract() { fn remaining_route_inventory_does_not_overlap_contract() {
assert_eq!(REMAINING_RUNTIME_ROUTES.len(), 18);
for remaining in REMAINING_RUNTIME_ROUTES { for remaining in REMAINING_RUNTIME_ROUTES {
assert!(!RuntimeApiMetadata::OPERATIONS.iter().any(|operation| { assert!(!RuntimeApiMetadata::OPERATIONS.iter().any(|operation| {
format!("{:?}", operation.method).eq_ignore_ascii_case(remaining.method) format!("{:?}", operation.method).eq_ignore_ascii_case(remaining.method)
+32
View File
@@ -348,11 +348,13 @@ struct RuntimeHttpConfigBundleAvailabilityQuery {
digest: String, digest: String,
} }
#[allow(dead_code)]
#[derive(Clone, Debug, Default, Deserialize)] #[derive(Clone, Debug, Default, Deserialize)]
struct RuntimeHttpWorkersQuery { struct RuntimeHttpWorkersQuery {
status: Option<RuntimeHttpWorkerStatusFilter>, status: Option<RuntimeHttpWorkerStatusFilter>,
} }
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, Deserialize)] #[derive(Clone, Copy, Debug, Deserialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
enum RuntimeHttpWorkerStatusFilter { enum RuntimeHttpWorkerStatusFilter {
@@ -646,6 +648,7 @@ async fn post_workspace_verification_acknowledgement(
})) }))
} }
#[allow(dead_code)]
async fn get_runtime_ping( async fn get_runtime_ping(
State(state): State<RuntimeHttpState>, State(state): State<RuntimeHttpState>,
Extension(auth): Extension<RuntimeAuthContext>, Extension(auth): Extension<RuntimeAuthContext>,
@@ -688,6 +691,7 @@ async fn get_runtime_ping(
})) }))
} }
#[allow(dead_code)]
async fn get_runtime( async fn get_runtime(
State(state): State<RuntimeHttpState>, State(state): State<RuntimeHttpState>,
) -> RestResult<RuntimeHttpSummaryResponse> { ) -> RestResult<RuntimeHttpSummaryResponse> {
@@ -767,6 +771,7 @@ async fn check_config_bundle(
})) }))
} }
#[allow(dead_code)]
async fn list_workers( async fn list_workers(
State(state): State<RuntimeHttpState>, State(state): State<RuntimeHttpState>,
auth: Option<Extension<RuntimeAuthContext>>, auth: Option<Extension<RuntimeAuthContext>>,
@@ -1107,6 +1112,7 @@ async fn cleanup_working_directory(
})) }))
} }
#[allow(dead_code)]
async fn get_worker( async fn get_worker(
State(state): State<RuntimeHttpState>, State(state): State<RuntimeHttpState>,
auth: Option<Extension<RuntimeAuthContext>>, auth: Option<Extension<RuntimeAuthContext>>,
@@ -1121,6 +1127,7 @@ async fn get_worker(
Ok(Json(RuntimeHttpWorkerResponse { worker })) Ok(Json(RuntimeHttpWorkerResponse { worker }))
} }
#[allow(dead_code)]
async fn delete_worker( async fn delete_worker(
State(state): State<RuntimeHttpState>, State(state): State<RuntimeHttpState>,
auth: Option<Extension<RuntimeAuthContext>>, auth: Option<Extension<RuntimeAuthContext>>,
@@ -1135,6 +1142,7 @@ async fn delete_worker(
Ok(Json(RuntimeHttpWorkerDeleteResponse { worker })) Ok(Json(RuntimeHttpWorkerDeleteResponse { worker }))
} }
#[allow(dead_code)]
async fn create_worker( async fn create_worker(
State(state): State<RuntimeHttpState>, State(state): State<RuntimeHttpState>,
auth: Option<Extension<RuntimeAuthContext>>, auth: Option<Extension<RuntimeAuthContext>>,
@@ -1149,6 +1157,7 @@ async fn create_worker(
Ok(Json(RuntimeHttpWorkerResponse { worker })) Ok(Json(RuntimeHttpWorkerResponse { worker }))
} }
#[allow(dead_code)]
async fn replace_worker_workspace_api( async fn replace_worker_workspace_api(
State(state): State<RuntimeHttpState>, State(state): State<RuntimeHttpState>,
auth: Option<Extension<RuntimeAuthContext>>, auth: Option<Extension<RuntimeAuthContext>>,
@@ -1171,6 +1180,7 @@ async fn replace_worker_workspace_api(
Ok(Json(RuntimeHttpWorkerResponse { worker })) Ok(Json(RuntimeHttpWorkerResponse { worker }))
} }
#[allow(dead_code)]
async fn restore_worker( async fn restore_worker(
State(state): State<RuntimeHttpState>, State(state): State<RuntimeHttpState>,
auth: Option<Extension<RuntimeAuthContext>>, auth: Option<Extension<RuntimeAuthContext>>,
@@ -1734,6 +1744,7 @@ fn protocol_error_event(message: impl Into<String>) -> protocol::Event {
} }
} }
#[allow(dead_code)]
async fn worker_retention_inventory( async fn worker_retention_inventory(
State(state): State<RuntimeHttpState>, State(state): State<RuntimeHttpState>,
auth: Option<Extension<RuntimeAuthContext>>, auth: Option<Extension<RuntimeAuthContext>>,
@@ -1754,6 +1765,7 @@ async fn worker_retention_inventory(
.map_err(RuntimeHttpRestError::runtime) .map_err(RuntimeHttpRestError::runtime)
} }
#[allow(dead_code)]
async fn execute_worker_retention( async fn execute_worker_retention(
State(state): State<RuntimeHttpState>, State(state): State<RuntimeHttpState>,
auth: Option<Extension<RuntimeAuthContext>>, auth: Option<Extension<RuntimeAuthContext>>,
@@ -1789,6 +1801,7 @@ async fn execute_worker_retention(
.map_err(RuntimeHttpRestError::runtime) .map_err(RuntimeHttpRestError::runtime)
} }
#[allow(dead_code)]
async fn send_worker_input( async fn send_worker_input(
State(state): State<RuntimeHttpState>, State(state): State<RuntimeHttpState>,
auth: Option<Extension<RuntimeAuthContext>>, auth: Option<Extension<RuntimeAuthContext>>,
@@ -1805,6 +1818,7 @@ async fn send_worker_input(
Ok(Json(RuntimeHttpWorkerInputResponse { ack })) Ok(Json(RuntimeHttpWorkerInputResponse { ack }))
} }
#[allow(dead_code)]
async fn worker_completions( async fn worker_completions(
State(state): State<RuntimeHttpState>, State(state): State<RuntimeHttpState>,
auth: Option<Extension<RuntimeAuthContext>>, auth: Option<Extension<RuntimeAuthContext>>,
@@ -1941,6 +1955,7 @@ async fn delete_worker_uploaded_file(
})) }))
} }
#[allow(dead_code)]
async fn stop_worker( async fn stop_worker(
State(state): State<RuntimeHttpState>, State(state): State<RuntimeHttpState>,
auth: Option<Extension<RuntimeAuthContext>>, auth: Option<Extension<RuntimeAuthContext>>,
@@ -1959,6 +1974,7 @@ async fn stop_worker(
Ok(Json(RuntimeHttpWorkerLifecycleResponse { ack })) Ok(Json(RuntimeHttpWorkerLifecycleResponse { ack }))
} }
#[allow(dead_code)]
async fn cancel_worker( async fn cancel_worker(
State(state): State<RuntimeHttpState>, State(state): State<RuntimeHttpState>,
auth: Option<Extension<RuntimeAuthContext>>, auth: Option<Extension<RuntimeAuthContext>>,
@@ -3252,6 +3268,22 @@ mod tests {
let token = "local-token"; let token = "local-token";
let app = runtime_http_router(runtime.clone(), token.to_string()); let app = runtime_http_router(runtime.clone(), token.to_string());
let malformed_response = app
.clone()
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/v1/workers")
.header(header::AUTHORIZATION, format!("Bearer {token}"))
.body(Body::from("{"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(malformed_response.status(), StatusCode::BAD_REQUEST);
let malformed: RuntimeHttpErrorResponse = read_json(malformed_response).await;
assert_eq!(malformed.error.code, "invalid_json");
let response = authed_json_request( let response = authed_json_request(
app.clone(), app.clone(),
Method::POST, Method::POST,
@@ -50,7 +50,17 @@ async fn runtime_api_wire_compatibility(
mut request: axum::extract::Request, mut request: axum::extract::Request,
next: Next, next: Next,
) -> Response { ) -> Response {
if !matches!(*request.method(), Method::GET | Method::HEAD) { let is_json_request = !matches!(*request.method(), Method::GET | Method::HEAD);
let is_query_request = request.method() == Method::GET;
if request.uri().path() == "/v1/ping" && !request.headers().contains_key("x-yoi-workspace-id") {
return RuntimeHttpRestError::new(
StatusCode::FORBIDDEN,
"runtime_ping_workspace_scope_required",
"Runtime ping requires an authenticated Workspace scope",
)
.into_response();
}
if is_json_request {
if !request.headers().contains_key(header::CONTENT_TYPE) { if !request.headers().contains_key(header::CONTENT_TYPE) {
request.headers_mut().insert( request.headers_mut().insert(
header::CONTENT_TYPE, header::CONTENT_TYPE,
@@ -73,11 +83,28 @@ async fn runtime_api_wire_compatibility(
request = axum::extract::Request::from_parts(parts, Body::from(body)); request = axum::extract::Request::from_parts(parts, Body::from(body));
} }
let response = next.run(request).await; let response = next.run(request).await;
if response.status() == StatusCode::UNPROCESSABLE_ENTITY { let is_json_error = response
.headers()
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.starts_with("application/json"));
if matches!(
response.status(),
StatusCode::BAD_REQUEST | StatusCode::UNPROCESSABLE_ENTITY
) && !is_json_error
{
return RuntimeHttpRestError::new( return RuntimeHttpRestError::new(
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
"invalid_json", if is_query_request {
"request body must be valid JSON", "invalid_query"
} else {
"invalid_json"
},
if is_query_request {
"request query parameters are invalid"
} else {
"request body must be valid JSON"
},
) )
.into_response(); .into_response();
} }
@@ -125,79 +152,117 @@ impl runtime_api::RuntimeApi for RuntimeManagementApi {
&self, &self,
workspace_id: String, workspace_id: String,
) -> Result<runtime_api::RuntimePingResponse, runtime_api::RuntimeApiError> { ) -> Result<runtime_api::RuntimePingResponse, runtime_api::RuntimeApiError> {
let mut headers = HeaderMap::new(); let Extension(auth) = required_auth()?;
headers.insert( if workspace_id != auth.workspace_id {
"x-yoi-workspace-id", return Err(runtime_api::RuntimeApiError::new(
workspace_id.parse().map_err(|error| { StatusCode::FORBIDDEN.as_u16(),
"runtime_ping_workspace_scope_mismatch",
"Runtime ping Workspace scope does not match the authenticated capability",
));
}
let runtime_id = self
.state
.workspace_auth
.as_ref()
.map(|auth| auth.signer.runtime_id().trim())
.filter(|runtime_id| !runtime_id.is_empty())
.ok_or_else(|| {
runtime_api::RuntimeApiError::new( runtime_api::RuntimeApiError::new(
StatusCode::BAD_REQUEST.as_u16(), StatusCode::SERVICE_UNAVAILABLE.as_u16(),
"invalid_workspace_header", "runtime_ping_identity_unavailable",
format!("invalid Workspace ping header: {error}"), "Runtime ping identity is not configured",
) )
})?, })?;
); Ok(runtime_api::RuntimePingResponse {
let Json(value) = get_runtime_ping(State(self.state.clone()), required_auth()?, headers) runtime_id: runtime_id.to_string(),
.await protocol_version: RUNTIME_HTTP_PROTOCOL_VERSION,
.map_err(api_error)?; })
response(value)
} }
async fn runtime_summary( async fn runtime_summary(
&self, &self,
) -> Result<runtime_api::RuntimeSummaryResponse, runtime_api::RuntimeApiError> { ) -> Result<runtime_api::RuntimeSummaryResponse, runtime_api::RuntimeApiError> {
let Json(value) = get_runtime(State(self.state.clone())) let runtime = self
.await .state
.runtime
.summary()
.map_err(RuntimeHttpRestError::runtime)
.map_err(api_error)?; .map_err(api_error)?;
response(value) let runtime = response(runtime)?;
Ok(runtime_api::RuntimeSummaryResponse { runtime })
} }
async fn list_workers( async fn list_workers(
&self, &self,
query: runtime_api::WorkerListQuery, query: runtime_api::WorkerListQuery,
) -> Result<runtime_api::WorkersResponse, runtime_api::RuntimeApiError> { ) -> Result<runtime_api::WorkersResponse, runtime_api::RuntimeApiError> {
let Json(value) = list_workers( let scope =
State(self.state.clone()), auth_workspace_scope(&self.state, auth_extension().as_ref()).map_err(api_error)?;
auth_extension(), let workers = match (query.status, scope.as_ref()) {
Ok(Query(request(query)?)), (Some(runtime_api::WorkerStatusFilter::Stopped), Some(scope)) => {
) self.state.runtime.list_stopped_workers_scoped(scope)
.await }
(Some(runtime_api::WorkerStatusFilter::Stopped), None) => {
self.state.runtime.list_stopped_workers()
}
(None, Some(scope)) => self.state.runtime.list_workers_scoped(scope),
(None, None) => self.state.runtime.list_workers(),
}
.map_err(RuntimeHttpRestError::runtime)
.map_err(api_error)?; .map_err(api_error)?;
response(value) let workers = response(workers)?;
Ok(runtime_api::WorkersResponse { workers })
} }
async fn get_worker( async fn get_worker(
&self, &self,
worker_id: String, worker_id: String,
) -> Result<runtime_api::WorkerResponse, runtime_api::RuntimeApiError> { ) -> Result<runtime_api::WorkerResponse, runtime_api::RuntimeApiError> {
let Json(value) = get_worker(State(self.state.clone()), auth_extension(), Path(worker_id)) let worker_ref = worker_ref_for(&self.state.runtime, worker_id).map_err(api_error)?;
.await let worker = match auth_workspace_scope(&self.state, auth_extension().as_ref())
.map_err(api_error)?; .map_err(api_error)?
response(value) {
Some(scope) => self.state.runtime.worker_detail_scoped(&scope, &worker_ref),
None => self.state.runtime.worker_detail(&worker_ref),
}
.map_err(RuntimeHttpRestError::runtime)
.map_err(api_error)?;
let worker = response(worker)?;
Ok(runtime_api::WorkerResponse { worker })
} }
async fn create_worker( async fn create_worker(
&self, &self,
value: runtime_api::CreateWorkerRequest, value: runtime_api::CreateWorkerRequest,
) -> Result<runtime_api::WorkerResponse, runtime_api::RuntimeApiError> { ) -> Result<runtime_api::WorkerResponse, runtime_api::RuntimeApiError> {
let Json(value) = create_worker( let request: CreateWorkerRequest = request(value)?;
State(self.state.clone()), let worker = match auth_workspace_scope(&self.state, auth_extension().as_ref())
auth_extension(), .map_err(api_error)?
Ok(Json(request(value)?)), {
) Some(scope) => self.state.runtime.create_worker_scoped(&scope, request),
.await None => self.state.runtime.create_worker(request),
}
.map_err(RuntimeHttpRestError::runtime)
.map_err(api_error)?; .map_err(api_error)?;
response(value) let worker = response(worker)?;
Ok(runtime_api::WorkerResponse { worker })
} }
async fn delete_worker( async fn delete_worker(
&self, &self,
worker_id: String, worker_id: String,
) -> Result<runtime_api::WorkerDeleteResponse, runtime_api::RuntimeApiError> { ) -> Result<runtime_api::WorkerDeleteResponse, runtime_api::RuntimeApiError> {
let Json(value) = let worker_ref = worker_ref_for(&self.state.runtime, worker_id).map_err(api_error)?;
delete_worker(State(self.state.clone()), auth_extension(), Path(worker_id)) let worker = match auth_workspace_scope(&self.state, auth_extension().as_ref())
.await .map_err(api_error)?
.map_err(api_error)?; {
response(value) Some(scope) => self.state.runtime.delete_worker_scoped(&scope, &worker_ref),
None => self.state.runtime.delete_worker(&worker_ref),
}
.map_err(RuntimeHttpRestError::runtime)
.map_err(api_error)?;
let worker = response(worker)?;
Ok(runtime_api::WorkerDeleteResponse { worker })
} }
async fn send_worker_input( async fn send_worker_input(
@@ -205,15 +270,21 @@ impl runtime_api::RuntimeApi for RuntimeManagementApi {
worker_id: String, worker_id: String,
input: runtime_api::WorkerInput, input: runtime_api::WorkerInput,
) -> Result<runtime_api::WorkerInputResponse, runtime_api::RuntimeApiError> { ) -> Result<runtime_api::WorkerInputResponse, runtime_api::RuntimeApiError> {
let Json(value) = send_worker_input( let worker_ref = worker_ref_for(&self.state.runtime, worker_id).map_err(api_error)?;
State(self.state.clone()), let input: WorkerInput = request(input)?;
auth_extension(), let ack = match auth_workspace_scope(&self.state, auth_extension().as_ref())
Path(worker_id), .map_err(api_error)?
Ok(Json(request(input)?)), {
) Some(scope) => self
.await .state
.runtime
.send_input_scoped(&scope, &worker_ref, input),
None => self.state.runtime.send_input(&worker_ref, input),
}
.map_err(RuntimeHttpRestError::runtime)
.map_err(api_error)?; .map_err(api_error)?;
response(value) let ack = response(ack)?;
Ok(runtime_api::WorkerInputResponse { ack })
} }
async fn stop_worker( async fn stop_worker(
@@ -221,23 +292,20 @@ impl runtime_api::RuntimeApi for RuntimeManagementApi {
worker_id: String, worker_id: String,
value: runtime_api::WorkerLifecycleRequest, value: runtime_api::WorkerLifecycleRequest,
) -> Result<runtime_api::WorkerLifecycleResponse, runtime_api::RuntimeApiError> { ) -> Result<runtime_api::WorkerLifecycleResponse, runtime_api::RuntimeApiError> {
let body = serde_json::to_vec(&request::<_, RuntimeHttpWorkerLifecycleRequest>(value)?) let worker_ref = worker_ref_for(&self.state.runtime, worker_id).map_err(api_error)?;
.map_err(|error| { let ack = match auth_workspace_scope(&self.state, auth_extension().as_ref())
runtime_api::RuntimeApiError::new( .map_err(api_error)?
500, {
"runtime_request_conversion_failed", Some(scope) => self
error.to_string(), .state
) .runtime
})?; .stop_worker_scoped(&scope, &worker_ref, value.reason),
let Json(value) = stop_worker( None => self.state.runtime.stop_worker(&worker_ref, value.reason),
State(self.state.clone()), }
auth_extension(), .map_err(RuntimeHttpRestError::runtime)
Path(worker_id),
Bytes::from(body),
)
.await
.map_err(api_error)?; .map_err(api_error)?;
response(value) let ack = response(ack)?;
Ok(runtime_api::WorkerLifecycleResponse { ack })
} }
async fn cancel_worker( async fn cancel_worker(
@@ -245,23 +313,21 @@ impl runtime_api::RuntimeApi for RuntimeManagementApi {
worker_id: String, worker_id: String,
value: runtime_api::WorkerLifecycleRequest, value: runtime_api::WorkerLifecycleRequest,
) -> Result<runtime_api::WorkerLifecycleResponse, runtime_api::RuntimeApiError> { ) -> Result<runtime_api::WorkerLifecycleResponse, runtime_api::RuntimeApiError> {
let body = serde_json::to_vec(&request::<_, RuntimeHttpWorkerLifecycleRequest>(value)?) let worker_ref = worker_ref_for(&self.state.runtime, worker_id).map_err(api_error)?;
.map_err(|error| { let ack = match auth_workspace_scope(&self.state, auth_extension().as_ref())
runtime_api::RuntimeApiError::new( .map_err(api_error)?
500, {
"runtime_request_conversion_failed", Some(scope) => {
error.to_string(), self.state
) .runtime
})?; .cancel_worker_scoped(&scope, &worker_ref, value.reason)
let Json(value) = cancel_worker( }
State(self.state.clone()), None => self.state.runtime.cancel_worker(&worker_ref, value.reason),
auth_extension(), }
Path(worker_id), .map_err(RuntimeHttpRestError::runtime)
Bytes::from(body),
)
.await
.map_err(api_error)?; .map_err(api_error)?;
response(value) let ack = response(ack)?;
Ok(runtime_api::WorkerLifecycleResponse { ack })
} }
async fn restore_worker( async fn restore_worker(
@@ -269,11 +335,20 @@ impl runtime_api::RuntimeApi for RuntimeManagementApi {
worker_id: String, worker_id: String,
_request: runtime_api::EmptyObjectRequest, _request: runtime_api::EmptyObjectRequest,
) -> Result<runtime_api::WorkerResponse, runtime_api::RuntimeApiError> { ) -> Result<runtime_api::WorkerResponse, runtime_api::RuntimeApiError> {
let Json(value) = let worker_ref = worker_ref_for(&self.state.runtime, worker_id).map_err(api_error)?;
restore_worker(State(self.state.clone()), auth_extension(), Path(worker_id)) let worker = match auth_workspace_scope(&self.state, auth_extension().as_ref())
.await .map_err(api_error)?
.map_err(api_error)?; {
response(value) Some(scope) => self
.state
.runtime
.restore_worker_scoped(&scope, &worker_ref),
None => self.state.runtime.restore_worker(&worker_ref),
}
.map_err(RuntimeHttpRestError::runtime)
.map_err(api_error)?;
let worker = response(worker)?;
Ok(runtime_api::WorkerResponse { worker })
} }
async fn replace_worker_workspace_api( async fn replace_worker_workspace_api(
@@ -281,15 +356,25 @@ impl runtime_api::RuntimeApi for RuntimeManagementApi {
worker_id: String, worker_id: String,
value: runtime_api::WorkerWorkspaceApiRequest, value: runtime_api::WorkerWorkspaceApiRequest,
) -> Result<runtime_api::WorkerResponse, runtime_api::RuntimeApiError> { ) -> Result<runtime_api::WorkerResponse, runtime_api::RuntimeApiError> {
let Json(value) = replace_worker_workspace_api( let workspace_api: WorkspaceApiRef = request(value.workspace_api)?;
State(self.state.clone()), let worker_ref = worker_ref_for(&self.state.runtime, worker_id).map_err(api_error)?;
auth_extension(), let worker = match auth_workspace_scope(&self.state, auth_extension().as_ref())
Path(worker_id), .map_err(api_error)?
Ok(Json(request(value)?)), {
) Some(scope) => self.state.runtime.replace_worker_workspace_api_scoped(
.await &scope,
&worker_ref,
workspace_api,
),
None => self
.state
.runtime
.replace_worker_workspace_api(&worker_ref, workspace_api),
}
.map_err(RuntimeHttpRestError::runtime)
.map_err(api_error)?; .map_err(api_error)?;
response(value) let worker = response(worker)?;
Ok(runtime_api::WorkerResponse { worker })
} }
async fn complete_worker_arguments( async fn complete_worker_arguments(
@@ -297,29 +382,53 @@ impl runtime_api::RuntimeApi for RuntimeManagementApi {
worker_id: String, worker_id: String,
value: runtime_api::CompletionRequest, value: runtime_api::CompletionRequest,
) -> Result<runtime_api::CompletionResponse, runtime_api::RuntimeApiError> { ) -> Result<runtime_api::CompletionResponse, runtime_api::RuntimeApiError> {
let Json(value) = worker_completions( let worker_ref = worker_ref_for(&self.state.runtime, worker_id).map_err(api_error)?;
State(self.state.clone()), let kind = value.kind;
auth_extension(), let prefix = value.prefix;
Path(worker_id), let entries = match auth_workspace_scope(&self.state, auth_extension().as_ref())
Ok(Json(request(value)?)), .map_err(api_error)?
) {
.await Some(scope) => {
self.state
.runtime
.worker_completions_scoped(&scope, &worker_ref, kind, &prefix)
}
None => self
.state
.runtime
.worker_completions(&worker_ref, kind, &prefix),
}
.map_err(RuntimeHttpRestError::runtime)
.map_err(api_error)?; .map_err(api_error)?;
response(value) let entries = response(entries)?;
Ok(runtime_api::CompletionResponse {
kind,
prefix,
entries,
})
} }
async fn retention_inventory( async fn retention_inventory(
&self, &self,
worker_id: String, worker_id: String,
) -> Result<runtime_api::WorkerRetentionInventory, runtime_api::RuntimeApiError> { ) -> Result<runtime_api::WorkerRetentionInventory, runtime_api::RuntimeApiError> {
let Json(value) = worker_retention_inventory( let scope = auth_workspace_scope(&self.state, auth_extension().as_ref())
State(self.state.clone()), .map_err(api_error)?
auth_extension(), .ok_or_else(|| {
Path(worker_id), runtime_api::RuntimeApiError::new(
) StatusCode::FORBIDDEN.as_u16(),
.await "workspace_scope_required",
.map_err(api_error)?; "Worker retention inventory requires workspace-scoped authorization",
response(value) )
})?;
let worker_ref = worker_ref_for(&self.state.runtime, worker_id).map_err(api_error)?;
let inventory = self
.state
.runtime
.worker_retention_inventory(&scope.workspace_id, &worker_ref)
.map_err(RuntimeHttpRestError::runtime)
.map_err(api_error)?;
response(inventory)
} }
async fn execute_retention( async fn execute_retention(
@@ -327,14 +436,37 @@ impl runtime_api::RuntimeApi for RuntimeManagementApi {
worker_id: String, worker_id: String,
value: runtime_api::WorkerRetentionExecutionRequest, value: runtime_api::WorkerRetentionExecutionRequest,
) -> Result<runtime_api::WorkerRetentionExecutionResult, runtime_api::RuntimeApiError> { ) -> Result<runtime_api::WorkerRetentionExecutionResult, runtime_api::RuntimeApiError> {
let Json(value) = execute_worker_retention( let scope = auth_workspace_scope(&self.state, auth_extension().as_ref())
State(self.state.clone()), .map_err(api_error)?
auth_extension(), .ok_or_else(|| {
Path(worker_id), runtime_api::RuntimeApiError::new(
Ok(Json(request(value)?)), StatusCode::FORBIDDEN.as_u16(),
) "workspace_scope_required",
.await "Worker retention execution requires workspace-scoped authorization",
.map_err(api_error)?; )
response(value) })?;
let worker_ref = worker_ref_for(&self.state.runtime, worker_id).map_err(api_error)?;
let request: WorkerRetentionExecutionRequest = request(value)?;
if request.workspace_id != scope.workspace_id {
return Err(runtime_api::RuntimeApiError::new(
StatusCode::FORBIDDEN.as_u16(),
"workspace_scope_mismatch",
"Worker retention request workspace does not match the authenticated workspace scope",
));
}
if request.worker_id != worker_ref.worker_id {
return Err(runtime_api::RuntimeApiError::new(
StatusCode::BAD_REQUEST.as_u16(),
"worker_id_mismatch",
"Worker retention request worker_id does not match route worker_id",
));
}
let result = self
.state
.runtime
.execute_worker_retention(&request)
.map_err(RuntimeHttpRestError::runtime)
.map_err(api_error)?;
response(result)
} }
} }
+62 -54
View File
@@ -46,10 +46,7 @@ use worker_runtime::http_server::{
RUNTIME_PING_PERMISSION, RUNTIME_WORKSPACE_SCOPE_HEADER, RUNTIME_PING_PERMISSION, RUNTIME_WORKSPACE_SCOPE_HEADER,
RuntimeHttpConfigBundleAvailabilityResponse, RuntimeHttpConfigBundleSyncRequest, RuntimeHttpConfigBundleAvailabilityResponse, RuntimeHttpConfigBundleSyncRequest,
RuntimeHttpErrorResponse, RuntimeHttpPingResponse, RuntimeHttpRepositoryAccessResponse, RuntimeHttpErrorResponse, RuntimeHttpPingResponse, RuntimeHttpRepositoryAccessResponse,
RuntimeHttpSummaryResponse, RuntimeHttpUploadedFileDeleteResponse, RuntimeHttpUploadedFileDeleteResponse, RuntimeHttpUploadedFileResponse,
RuntimeHttpUploadedFileResponse, RuntimeHttpWorkerCompletionsResponse,
RuntimeHttpWorkerDeleteResponse, RuntimeHttpWorkerLifecycleResponse, RuntimeHttpWorkerResponse,
RuntimeHttpWorkerWorkspaceApiRequest, RuntimeHttpWorkersResponse,
RuntimeHttpWorkingDirectoriesResponse, RuntimeHttpWorkingDirectoryResponse, RuntimeHttpWorkingDirectoriesResponse, RuntimeHttpWorkingDirectoryResponse,
RuntimeHttpWorkspacePromptProjectionRequest, RuntimeHttpWorkspacePromptProjectionResponse, RuntimeHttpWorkspacePromptProjectionRequest, RuntimeHttpWorkspacePromptProjectionResponse,
}; };
@@ -3981,7 +3978,7 @@ impl RemoteWorkerRuntime {
fn lifecycle_result_from_response( fn lifecycle_result_from_response(
&self, &self,
worker_id: &str, worker_id: &str,
response: RuntimeHttpWorkerLifecycleResponse, response: worker_runtime::catalog::WorkerLifecycleAck,
) -> WorkerLifecycleResult { ) -> WorkerLifecycleResult {
WorkerLifecycleResult { WorkerLifecycleResult {
state: WorkerOperationState::Accepted, state: WorkerOperationState::Accepted,
@@ -3991,7 +3988,7 @@ impl RemoteWorkerRuntime {
DiagnosticSeverity::Info, DiagnosticSeverity::Info,
format!( format!(
"Remote Runtime acknowledged lifecycle operation for '{worker_id}' with status {}", "Remote Runtime acknowledged lifecycle operation for '{worker_id}' with status {}",
embedded_worker_status_label(response.ack.status) embedded_worker_status_label(response.status)
), ),
)], )],
} }
@@ -4010,25 +4007,27 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
MAX_REMOTE_RUNTIME_RESPONSE_BYTES, MAX_REMOTE_RUNTIME_RESPONSE_BYTES,
|client| async move { client.runtime_summary().await }, |client| async move { client.runtime_summary().await },
) )
.and_then(|value| runtime_contract_convert::<_, RuntimeHttpSummaryResponse>(value)) .and_then(|value| {
{ runtime_contract_convert::<_, worker_runtime::management::RuntimeSummary>(
value.runtime,
)
}) {
Ok(response) => RuntimeSummary { Ok(response) => RuntimeSummary {
runtime_id: self.runtime_id.clone(), runtime_id: self.runtime_id.clone(),
label: response label: response
.runtime
.display_name .display_name
.unwrap_or_else(|| self.display_name.clone()), .unwrap_or_else(|| self.display_name.clone()),
kind: "remote_worker_runtime".to_string(), kind: "remote_worker_runtime".to_string(),
status: embedded_runtime_status_label(response.runtime.status).to_string(), status: embedded_runtime_status_label(response.status).to_string(),
source: RuntimeSourceSummary::remote_http(), source: RuntimeSourceSummary::remote_http(),
host_ids: if limit == 0 { host_ids: if limit == 0 {
Vec::new() Vec::new()
} else { } else {
vec![self.host_id.clone()] vec![self.host_id.clone()]
}, },
worker_creation_available: response.runtime.worker_creation_available, worker_creation_available: response.worker_creation_available,
os: response.runtime.os, os: response.os,
arch: response.runtime.arch, arch: response.arch,
diagnostics: Vec::new(), diagnostics: Vec::new(),
}, },
Err(diagnostic) => RuntimeSummary { Err(diagnostic) => RuntimeSummary {
@@ -4162,11 +4161,13 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
.await .await
}, },
) )
.and_then(|value| runtime_contract_convert::<_, RuntimeHttpWorkersResponse>(value)) .and_then(|value| {
{ runtime_contract_convert::<_, Vec<worker_runtime::catalog::WorkerSummary>>(
value.workers,
)
}) {
Ok(response) => RuntimeList::new( Ok(response) => RuntimeList::new(
response response
.workers
.into_iter() .into_iter()
.take(limit) .take(limit)
.map(|worker| self.map_worker_summary(worker)) .map(|worker| self.map_worker_summary(worker))
@@ -4193,11 +4194,13 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
.await .await
}, },
) )
.and_then(|value| runtime_contract_convert::<_, RuntimeHttpWorkersResponse>(value)) .and_then(|value| {
{ runtime_contract_convert::<_, Vec<worker_runtime::catalog::WorkerSummary>>(
value.workers,
)
}) {
Ok(response) => RuntimeList::new( Ok(response) => RuntimeList::new(
response response
.workers
.into_iter() .into_iter()
.take(limit) .take(limit)
.map(|worker| self.map_worker_summary(worker)) .map(|worker| self.map_worker_summary(worker))
@@ -4216,10 +4219,11 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
MAX_REMOTE_RUNTIME_RESPONSE_BYTES, MAX_REMOTE_RUNTIME_RESPONSE_BYTES,
move |client| async move { client.get_worker(worker_id_owned).await }, move |client| async move { client.get_worker(worker_id_owned).await },
) )
.and_then(|value| runtime_contract_convert::<_, RuntimeHttpWorkerResponse>(value)) .and_then(|value| {
{ runtime_contract_convert::<_, worker_runtime::catalog::WorkerDetail>(value.worker)
}) {
Ok(response) => WorkerLookupResult { Ok(response) => WorkerLookupResult {
worker: Some(self.map_worker_detail(response.worker)), worker: Some(self.map_worker_detail(response)),
diagnostics: Vec::new(), diagnostics: Vec::new(),
}, },
Err(diagnostic) if diagnostic.code == "worker_not_found" => WorkerLookupResult { Err(diagnostic) if diagnostic.code == "worker_not_found" => WorkerLookupResult {
@@ -4245,11 +4249,12 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
.await .await
}, },
) )
.and_then(|value| runtime_contract_convert::<_, RuntimeHttpWorkerResponse>(value)) .and_then(|value| {
{ runtime_contract_convert::<_, worker_runtime::catalog::WorkerDetail>(value.worker)
}) {
Ok(response) => WorkerRestoreResult { Ok(response) => WorkerRestoreResult {
state: WorkerOperationState::Accepted, state: WorkerOperationState::Accepted,
worker: Some(self.map_worker_detail(response.worker)), worker: Some(self.map_worker_detail(response)),
diagnostics: Vec::new(), diagnostics: Vec::new(),
}, },
Err(diagnostic) => WorkerRestoreResult { Err(diagnostic) => WorkerRestoreResult {
@@ -4265,10 +4270,8 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
worker_id: &str, worker_id: &str,
workspace_api: WorkspaceApiRef, workspace_api: WorkspaceApiRef,
) -> WorkerWorkspaceApiResult { ) -> WorkerWorkspaceApiResult {
let request = match runtime_contract_convert(RuntimeHttpWorkerWorkspaceApiRequest { let request = match runtime_contract_convert(workspace_api) {
workspace_api, Ok(workspace_api) => runtime_api::WorkerWorkspaceApiRequest { workspace_api },
}) {
Ok(request) => request,
Err(diagnostic) => { Err(diagnostic) => {
return WorkerWorkspaceApiResult { return WorkerWorkspaceApiResult {
state: WorkerOperationState::Rejected, state: WorkerOperationState::Rejected,
@@ -4288,11 +4291,12 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
.await .await
}, },
) )
.and_then(|value| runtime_contract_convert::<_, RuntimeHttpWorkerResponse>(value)) .and_then(|value| {
{ runtime_contract_convert::<_, worker_runtime::catalog::WorkerDetail>(value.worker)
}) {
Ok(response) => WorkerWorkspaceApiResult { Ok(response) => WorkerWorkspaceApiResult {
state: WorkerOperationState::Accepted, state: WorkerOperationState::Accepted,
worker: Some(self.map_worker_detail(response.worker)), worker: Some(self.map_worker_detail(response)),
diagnostics: Vec::new(), diagnostics: Vec::new(),
}, },
Err(diagnostic) => WorkerWorkspaceApiResult { Err(diagnostic) => WorkerWorkspaceApiResult {
@@ -4506,11 +4510,12 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
MAX_REMOTE_RUNTIME_RESPONSE_BYTES, MAX_REMOTE_RUNTIME_RESPONSE_BYTES,
move |client| async move { client.create_worker(create).await }, move |client| async move { client.create_worker(create).await },
) )
.and_then(|value| runtime_contract_convert::<_, RuntimeHttpWorkerResponse>(value)) .and_then(|value| {
{ runtime_contract_convert::<_, worker_runtime::catalog::WorkerDetail>(value.worker)
}) {
Ok(response) => WorkerSpawnResult { Ok(response) => WorkerSpawnResult {
state: WorkerOperationState::Accepted, state: WorkerOperationState::Accepted,
worker: Some(self.map_worker_detail(response.worker)), worker: Some(self.map_worker_detail(response)),
acceptance_evidence: vec![WorkerSpawnAcceptanceEvidence { acceptance_evidence: vec![WorkerSpawnAcceptanceEvidence {
kind: "remote_runtime_worker_created".to_string(), kind: "remote_runtime_worker_created".to_string(),
detail: "worker-runtime REST create endpoint accepted the Worker".to_string(), detail: "worker-runtime REST create endpoint accepted the Worker".to_string(),
@@ -4589,7 +4594,9 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
move |client| async move { client.stop_worker(worker_id_owned, body).await }, move |client| async move { client.stop_worker(worker_id_owned, body).await },
) )
.and_then(|value| { .and_then(|value| {
runtime_contract_convert::<_, RuntimeHttpWorkerLifecycleResponse>(value) runtime_contract_convert::<_, worker_runtime::catalog::WorkerLifecycleAck>(
value.ack,
)
}) { }) {
Ok(response) => self.lifecycle_result_from_response(worker_id, response), Ok(response) => self.lifecycle_result_from_response(worker_id, response),
Err(diagnostic) => remote_lifecycle_rejected(&self.runtime_id, worker_id, diagnostic), Err(diagnostic) => remote_lifecycle_rejected(&self.runtime_id, worker_id, diagnostic),
@@ -4612,7 +4619,9 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
move |client| async move { client.cancel_worker(worker_id_owned, body).await }, move |client| async move { client.cancel_worker(worker_id_owned, body).await },
) )
.and_then(|value| { .and_then(|value| {
runtime_contract_convert::<_, RuntimeHttpWorkerLifecycleResponse>(value) runtime_contract_convert::<_, worker_runtime::catalog::WorkerLifecycleAck>(
value.ack,
)
}) { }) {
Ok(response) => self.lifecycle_result_from_response(worker_id, response), Ok(response) => self.lifecycle_result_from_response(worker_id, response),
Err(diagnostic) => remote_lifecycle_rejected(&self.runtime_id, worker_id, diagnostic), Err(diagnostic) => remote_lifecycle_rejected(&self.runtime_id, worker_id, diagnostic),
@@ -4627,15 +4636,18 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
MAX_REMOTE_RUNTIME_RESPONSE_BYTES, MAX_REMOTE_RUNTIME_RESPONSE_BYTES,
move |client| async move { client.delete_worker(worker_id_owned).await }, move |client| async move { client.delete_worker(worker_id_owned).await },
) )
.and_then(|value| runtime_contract_convert::<_, RuntimeHttpWorkerDeleteResponse>(value)) .and_then(|value| {
{ runtime_contract_convert::<_, worker_runtime::management::WorkerDeleteResult>(
value.worker,
)
}) {
Ok(response) => WorkerDeleteResult { Ok(response) => WorkerDeleteResult {
state: WorkerOperationState::Accepted, state: WorkerOperationState::Accepted,
worker: RuntimeWorkerRef::new( worker: RuntimeWorkerRef::new(
self.runtime_id.clone(), self.runtime_id.clone(),
response.worker.worker_id.to_string(), response.worker_id.to_string(),
), ),
deleted: response.worker.deleted, deleted: response.deleted,
diagnostics: Vec::new(), diagnostics: Vec::new(),
}, },
Err(diagnostic) => WorkerDeleteResult { Err(diagnostic) => WorkerDeleteResult {
@@ -4799,19 +4811,15 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
let failure_kind = request.kind; let failure_kind = request.kind;
let failure_prefix = request.prefix.clone(); let failure_prefix = request.prefix.clone();
let worker_id_owned = worker_id.to_string(); let worker_id_owned = worker_id.to_string();
match self match self.run_runtime_api(
.run_runtime_api( self.request_timeout,
self.request_timeout, MAX_REMOTE_RUNTIME_RESPONSE_BYTES,
MAX_REMOTE_RUNTIME_RESPONSE_BYTES, move |client| async move {
move |client| async move { client
client .complete_worker_arguments(worker_id_owned, request)
.complete_worker_arguments(worker_id_owned, request) .await
.await },
}, ) {
)
.and_then(|value| {
runtime_contract_convert::<_, RuntimeHttpWorkerCompletionsResponse>(value)
}) {
Ok(response) => WorkerCompletionsResult { Ok(response) => WorkerCompletionsResult {
worker: RuntimeWorkerRef::new(self.runtime_id.clone(), worker_id.to_string()), worker: RuntimeWorkerRef::new(self.runtime_id.clone(), worker_id.to_string()),
kind: response.kind, kind: response.kind,