Close runtime contract acceptance gaps
This commit is contained in:
@@ -716,6 +716,16 @@ pub const REMAINING_RUNTIME_ROUTES: &[RemainingRuntimeRoute] = &[
|
|||||||
path: "/v1/working-directories/{working_directory_id}",
|
path: "/v1/working-directories/{working_directory_id}",
|
||||||
reason: "Workdir transport",
|
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 {
|
RemainingRuntimeRoute {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/v1/workers/{worker_id}/attachments",
|
path: "/v1/workers/{worker_id}/attachments",
|
||||||
@@ -901,7 +911,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);
|
assert_eq!(REMAINING_RUNTIME_ROUTES.len(), 20);
|
||||||
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)
|
||||||
|
|||||||
@@ -2460,6 +2460,11 @@ fn status_for_runtime_error(error: &RuntimeError) -> StatusCode {
|
|||||||
{
|
{
|
||||||
StatusCode::NOT_FOUND
|
StatusCode::NOT_FOUND
|
||||||
}
|
}
|
||||||
|
RuntimeError::InvalidRequest(message)
|
||||||
|
if message.contains("already created with a different fingerprint") =>
|
||||||
|
{
|
||||||
|
StatusCode::CONFLICT
|
||||||
|
}
|
||||||
RuntimeError::RuntimeStopped
|
RuntimeError::RuntimeStopped
|
||||||
| RuntimeError::RuntimeStoreAlreadyOpen { .. }
|
| RuntimeError::RuntimeStoreAlreadyOpen { .. }
|
||||||
| RuntimeError::WorkerExecutionUnavailable { .. }
|
| RuntimeError::WorkerExecutionUnavailable { .. }
|
||||||
@@ -2498,6 +2503,11 @@ fn code_for_runtime_error(error: &RuntimeError) -> String {
|
|||||||
RuntimeError::WorkerExecutionRejected { .. } => "worker_execution_rejected".to_string(),
|
RuntimeError::WorkerExecutionRejected { .. } => "worker_execution_rejected".to_string(),
|
||||||
RuntimeError::WorkspaceOwnerMismatch { .. } => "workspace_owner_mismatch".to_string(),
|
RuntimeError::WorkspaceOwnerMismatch { .. } => "workspace_owner_mismatch".to_string(),
|
||||||
RuntimeError::LimitTooLarge { .. } => "limit_too_large".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::InvalidRequest(_) => "invalid_request".to_string(),
|
||||||
RuntimeError::WorkingDirectory(diagnostic) => diagnostic.code.clone(),
|
RuntimeError::WorkingDirectory(diagnostic) => diagnostic.code.clone(),
|
||||||
RuntimeError::InvalidInitialInputKind { .. } => "invalid_initial_input_kind".to_string(),
|
RuntimeError::InvalidInitialInputKind { .. } => "invalid_initial_input_kind".to_string(),
|
||||||
@@ -2552,6 +2562,29 @@ mod tests {
|
|||||||
use manifest::{Scope, SharedScope};
|
use manifest::{Scope, SharedScope};
|
||||||
use sha2::Digest as _;
|
use sha2::Digest as _;
|
||||||
use tower::ServiceExt;
|
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::{
|
use workdir::{
|
||||||
GrepOutputMode, GrepRequest, LocalWorkdirSession, StatRequest, Workdir, WorkdirPath,
|
GrepOutputMode, GrepRequest, LocalWorkdirSession, StatRequest, Workdir, WorkdirPath,
|
||||||
WorkdirSessionCapabilities,
|
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]
|
#[tokio::test]
|
||||||
async fn workspace_signed_verification_requires_exact_request_and_acknowledges_response() {
|
async fn workspace_signed_verification_requires_exact_request_and_acknowledges_response() {
|
||||||
let runtime = Runtime::new_memory();
|
let runtime = Runtime::new_memory();
|
||||||
@@ -3284,6 +3349,116 @@ mod tests {
|
|||||||
let malformed: RuntimeHttpErrorResponse = read_json(malformed_response).await;
|
let malformed: RuntimeHttpErrorResponse = read_json(malformed_response).await;
|
||||||
assert_eq!(malformed.error.code, "invalid_json");
|
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(
|
let response = authed_json_request(
|
||||||
app.clone(),
|
app.clone(),
|
||||||
Method::POST,
|
Method::POST,
|
||||||
|
|||||||
@@ -449,9 +449,9 @@ impl runtime_api::RuntimeApi for RuntimeManagementApi {
|
|||||||
let request: WorkerRetentionExecutionRequest = request(value)?;
|
let request: WorkerRetentionExecutionRequest = request(value)?;
|
||||||
if request.workspace_id != scope.workspace_id {
|
if request.workspace_id != scope.workspace_id {
|
||||||
return Err(runtime_api::RuntimeApiError::new(
|
return Err(runtime_api::RuntimeApiError::new(
|
||||||
StatusCode::FORBIDDEN.as_u16(),
|
StatusCode::NOT_FOUND.as_u16(),
|
||||||
"workspace_scope_mismatch",
|
"worker_not_found",
|
||||||
"Worker retention request workspace does not match the authenticated workspace scope",
|
"worker does not belong to the authenticated Workspace",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if request.worker_id != worker_ref.worker_id {
|
if request.worker_id != worker_ref.worker_id {
|
||||||
|
|||||||
Reference in New Issue
Block a user