Close runtime contract acceptance gaps

This commit is contained in:
2026-09-18 02:31:37 +09:00
parent fbdb6ccc22
commit f967c1b392
3 changed files with 189 additions and 4 deletions
+11 -1
View File
@@ -716,6 +716,16 @@ pub const REMAINING_RUNTIME_ROUTES: &[RemainingRuntimeRoute] = &[
path: "/v1/working-directories/{working_directory_id}",
reason: "Workdir transport",
},
RemainingRuntimeRoute {
method: "GET",
path: "/v1/protocol/ws",
reason: "WebSocket protocol transport (ws-server feature)",
},
RemainingRuntimeRoute {
method: "GET",
path: "/v1/workers/{worker_id}/protocol/ws",
reason: "WebSocket protocol transport (ws-server feature)",
},
RemainingRuntimeRoute {
method: "POST",
path: "/v1/workers/{worker_id}/attachments",
@@ -901,7 +911,7 @@ mod tests {
#[test]
fn remaining_route_inventory_does_not_overlap_contract() {
assert_eq!(REMAINING_RUNTIME_ROUTES.len(), 18);
assert_eq!(REMAINING_RUNTIME_ROUTES.len(), 20);
for remaining in REMAINING_RUNTIME_ROUTES {
assert!(!RuntimeApiMetadata::OPERATIONS.iter().any(|operation| {
format!("{:?}", operation.method).eq_ignore_ascii_case(remaining.method)
+175
View File
@@ -2460,6 +2460,11 @@ fn status_for_runtime_error(error: &RuntimeError) -> StatusCode {
{
StatusCode::NOT_FOUND
}
RuntimeError::InvalidRequest(message)
if message.contains("already created with a different fingerprint") =>
{
StatusCode::CONFLICT
}
RuntimeError::RuntimeStopped
| RuntimeError::RuntimeStoreAlreadyOpen { .. }
| RuntimeError::WorkerExecutionUnavailable { .. }
@@ -2498,6 +2503,11 @@ fn code_for_runtime_error(error: &RuntimeError) -> String {
RuntimeError::WorkerExecutionRejected { .. } => "worker_execution_rejected".to_string(),
RuntimeError::WorkspaceOwnerMismatch { .. } => "workspace_owner_mismatch".to_string(),
RuntimeError::LimitTooLarge { .. } => "limit_too_large".to_string(),
RuntimeError::InvalidRequest(message)
if message.contains("already created with a different fingerprint") =>
{
"worker_create_conflict".to_string()
}
RuntimeError::InvalidRequest(_) => "invalid_request".to_string(),
RuntimeError::WorkingDirectory(diagnostic) => diagnostic.code.clone(),
RuntimeError::InvalidInitialInputKind { .. } => "invalid_initial_input_kind".to_string(),
@@ -2552,6 +2562,29 @@ mod tests {
use manifest::{Scope, SharedScope};
use sha2::Digest as _;
use tower::ServiceExt;
#[derive(Clone)]
struct TestRuntimeAuthorizer(String);
impl runtime_api::client_support::RequestAuthorizer for TestRuntimeAuthorizer {
fn authorize(
&self,
_request: runtime_api::client_support::AuthorizerRequest<'_>,
) -> Result<
runtime_api::client_support::framework::header::HeaderMap,
runtime_api::client_support::AuthorizationError,
> {
let mut headers = runtime_api::client_support::framework::header::HeaderMap::new();
headers.insert(
runtime_api::client_support::framework::header::AUTHORIZATION,
format!("Bearer {}", self.0)
.parse()
.map_err(|_| runtime_api::client_support::AuthorizationError::new())?,
);
Ok(headers)
}
}
use workdir::{
GrepOutputMode, GrepRequest, LocalWorkdirSession, StatRequest, Workdir, WorkdirPath,
WorkdirSessionCapabilities,
@@ -2589,6 +2622,38 @@ mod tests {
}
}
#[test]
fn remaining_route_inventory_covers_every_handwritten_router_path() {
let inventoried = runtime_api::REMAINING_RUNTIME_ROUTES
.iter()
.map(|route| route.path)
.collect::<std::collections::BTreeSet<_>>();
for path in [
WORKSPACE_VERIFICATION_CHALLENGE_PATH,
WORKSPACE_VERIFICATION_ACK_PATH,
"/v1/config-bundles",
"/v1/config-bundles/{bundle_id}/availability",
"/v1/workspace-prompt-projections",
"/v1/working-directories",
"/v1/working-directories/repository-access",
SSH_HOST_KEY_PROBE_PATH,
"/v1/repository-refs/observe",
"/v1/working-directories/{working_directory_id}/sessions",
"/v1/workdir-sessions/{session_id}/operations",
"/v1/workdir-sessions/{session_id}",
"/v1/working-directories/{working_directory_id}",
"/v1/workers/{worker_id}/attachments",
"/v1/workers/{worker_id}/attachments/{artifact_id}",
"/v1/protocol/ws",
"/v1/workers/{worker_id}/protocol/ws",
] {
assert!(
inventoried.contains(path),
"missing handwritten route inventory for {path}"
);
}
}
#[tokio::test]
async fn workspace_signed_verification_requires_exact_request_and_acknowledges_response() {
let runtime = Runtime::new_memory();
@@ -3284,6 +3349,116 @@ mod tests {
let malformed: RuntimeHttpErrorResponse = read_json(malformed_response).await;
assert_eq!(malformed.error.code, "invalid_json");
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let server_app = app.clone();
let server = tokio::spawn(async move {
axum::serve(listener, server_app).await.unwrap();
});
let client = runtime_api::RuntimeApiClient::builder(&format!("http://{address}"))
.unwrap()
.authorizer(TestRuntimeAuthorizer(token.to_string()))
.build()
.unwrap();
let generated_request: runtime_api::CreateWorkerRequest = serde_json::from_value(
serde_json::to_value(task_request("generated-roundtrip")).unwrap(),
)
.unwrap();
let generated_worker_id = generated_request.worker_id.to_string();
let mut conflicting_request = generated_request.clone();
conflicting_request.create_fingerprint = "different-fingerprint".to_string();
let generated = client.create_worker(generated_request).await.unwrap();
assert_eq!(generated.worker.worker_id.to_string(), generated_worker_id);
let generated_input: runtime_api::WorkerInput = serde_json::from_value(
serde_json::to_value(WorkerInput::user("generated client input")).unwrap(),
)
.unwrap();
client
.send_worker_input(generated_worker_id.clone(), generated_input)
.await
.unwrap();
client
.stop_worker(
generated_worker_id.clone(),
runtime_api::WorkerLifecycleRequest::default(),
)
.await
.unwrap();
client
.restore_worker(
generated_worker_id.clone(),
runtime_api::EmptyObjectRequest::default(),
)
.await
.unwrap();
let conflict = client.create_worker(conflicting_request).await.unwrap_err();
assert!(
matches!(
&conflict,
runtime_api::client_support::ClientError::Public { status, .. }
if *status == StatusCode::CONFLICT
),
"{conflict:?}"
);
client
.stop_worker(
generated_worker_id.clone(),
runtime_api::WorkerLifecycleRequest::default(),
)
.await
.unwrap();
client
.delete_worker(generated_worker_id.clone())
.await
.unwrap();
let missing = client
.get_worker(generated_worker_id.clone())
.await
.unwrap_err();
assert!(matches!(
missing,
runtime_api::client_support::ClientError::Public { status, .. }
if status == StatusCode::NOT_FOUND
));
let unauthorized = runtime_api::RuntimeApiClient::builder(&format!("http://{address}"))
.unwrap()
.build()
.unwrap()
.list_workers(runtime_api::WorkerListQuery::default())
.await
.unwrap_err();
assert!(matches!(
unauthorized,
runtime_api::client_support::ClientError::Public { status, .. }
if status == StatusCode::UNAUTHORIZED
));
let forbidden_response = app
.clone()
.oneshot(
Request::builder()
.method(Method::GET)
.uri("/v1/ping")
.header(header::AUTHORIZATION, format!("Bearer {token}"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(forbidden_response.status(), StatusCode::FORBIDDEN);
let bounded_client = runtime_api::RuntimeApiClient::builder(&format!("http://{address}"))
.unwrap()
.authorizer(TestRuntimeAuthorizer(token.to_string()))
.response_body_limit(1)
.build()
.unwrap();
assert!(matches!(
bounded_client.runtime_summary().await.unwrap_err(),
runtime_api::client_support::ClientError::Failure(
runtime_api::client_support::ClientFailure::ResponseTooLarge { .. }
)
));
server.abort();
let response = authed_json_request(
app.clone(),
Method::POST,
@@ -449,9 +449,9 @@ impl runtime_api::RuntimeApi for RuntimeManagementApi {
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",
StatusCode::NOT_FOUND.as_u16(),
"worker_not_found",
"worker does not belong to the authenticated Workspace",
));
}
if request.worker_id != worker_ref.worker_id {