From 779ced82b17c6dbea3f02c32ef1fdf0557dad298 Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 22 Aug 2026 19:43:18 +0900 Subject: [PATCH] fix: allow workspace clients through ticket REST operations --- crates/workspace-server/src/server.rs | 94 ++++++++++++++++----------- 1 file changed, 56 insertions(+), 38 deletions(-) diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index ff981554..117fb040 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -3842,7 +3842,7 @@ fn reject_unguarded_ticket_completion(operation: &TicketBackendOperation) -> Res Ok(()) } -async fn execute_worker_ticket_rest_operation( +async fn execute_ticket_rest_operation( api: &WorkspaceApi, workspace_id: &str, headers: HeaderMap, @@ -3863,7 +3863,10 @@ async fn execute_worker_ticket_rest_operation( let operation_kind = ticket_mutation_operation_kind(&operation); let is_mutation = operation_kind != "read"; let target = ticket_mutation_target(&operation).cloned(); - let source = authenticate_worker_mutation_source(api, workspace_id, &headers)?; + // Human clients are authorized by the Workspace route boundary. Runtime-forwarded Worker + // calls carry source headers; when either header is present, the complete pair is required + // and authenticated before source attribution is attached. + let source = optional_worker_mutation_source(api, workspace_id, &headers)?; reject_unguarded_ticket_completion(&operation)?; validate_ticket_repository_operation(api, &operation)?; let before = target.as_ref().and_then(|id| backend.show(id.clone()).ok()); @@ -3871,9 +3874,12 @@ async fn execute_worker_ticket_rest_operation( .as_ref() .map(|ticket| ticket.meta.workflow_state.as_str().to_string()) .unwrap_or_else(|| ticket_operation_initial_state(&operation)); - bind_worker_ticket_operation_source(&source, &mut operation); - let source_context = worker_ticket_source_context(api, workspace_id, &source, before.as_ref()); - backend = backend.with_event_attributes(source_context.attributes(operation_kind)); + if let Some(source) = source.as_ref() { + bind_worker_ticket_operation_source(source, &mut operation); + let source_context = + worker_ticket_source_context(api, workspace_id, source, before.as_ref()); + backend = backend.with_event_attributes(source_context.attributes(operation_kind)); + } let result = execute_ticket_backend_operation(&backend, operation).map_err(Error::from)?; if is_mutation @@ -3886,7 +3892,7 @@ async fn execute_worker_ticket_rest_operation( &ticket.meta.id, &previous_state, ticket.meta.workflow_state.as_str(), - Some(source), + source, ); } Ok(result) @@ -3899,7 +3905,7 @@ async fn execute_worker_ticket_test_operation( headers: HeaderMap, Json(operation): Json, ) -> ApiResult> { - execute_worker_ticket_rest_operation(&api, &path.workspace_id, headers, operation) + execute_ticket_rest_operation(&api, &path.workspace_id, headers, operation) .await .map(Json) } @@ -3935,7 +3941,7 @@ async fn scoped_default_intake_ready_body( headers: HeaderMap, Json(request): Json, ) -> ApiResult> { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &path.workspace_id, headers, @@ -3972,7 +3978,7 @@ async fn scoped_list_ticket_summaries( ticket::TicketListQuery::states(selected) } }; - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &path.workspace_id, headers, @@ -3990,7 +3996,7 @@ async fn scoped_get_ticket_record( AxumPath((workspace_id, id)): AxumPath<(String, String)>, headers: HeaderMap, ) -> ApiResult> { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &workspace_id, headers, @@ -4020,7 +4026,7 @@ async fn scoped_create_ticket_record( "Ticket creation must start in planning; use guarded workflow operations for later states", )); } - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &path.workspace_id, headers, @@ -4039,7 +4045,7 @@ async fn scoped_edit_ticket_record_item( headers: HeaderMap, Json(edit): Json, ) -> ApiResult> { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &workspace_id, headers, @@ -4060,7 +4066,7 @@ async fn scoped_ticket_dependency_check( AxumPath((workspace_id, id)): AxumPath<(String, String)>, headers: HeaderMap, ) -> ApiResult> { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &workspace_id, headers, @@ -4081,7 +4087,7 @@ async fn scoped_add_ticket_thread_event( headers: HeaderMap, Json(event): Json, ) -> ApiResult { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &workspace_id, headers, @@ -4100,7 +4106,7 @@ async fn scoped_add_ticket_state_change( headers: HeaderMap, Json(change): Json, ) -> ApiResult { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &workspace_id, headers, @@ -4119,7 +4125,7 @@ async fn scoped_add_ticket_intake_summary( headers: HeaderMap, Json(summary): Json, ) -> ApiResult { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &workspace_id, headers, @@ -4147,7 +4153,7 @@ async fn scoped_set_ticket_state_field( headers: HeaderMap, Json(change): Json, ) -> ApiResult { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &workspace_id, headers, @@ -4167,7 +4173,7 @@ async fn scoped_set_ticket_workflow_state( headers: HeaderMap, Json(change): Json, ) -> ApiResult { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &workspace_id, headers, @@ -4186,7 +4192,7 @@ async fn scoped_mark_ticket_ready( headers: HeaderMap, Json(request): Json, ) -> ApiResult> { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &workspace_id, headers, @@ -4212,7 +4218,7 @@ async fn scoped_queue_ticket_record( AxumPath((workspace_id, id)): AxumPath<(String, String)>, headers: HeaderMap, ) -> ApiResult { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &workspace_id, headers, @@ -4975,7 +4981,7 @@ async fn scoped_close_ticket_record( headers: HeaderMap, Json(resolution): Json, ) -> ApiResult { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &workspace_id, headers, @@ -4994,7 +5000,7 @@ async fn scoped_record_ticket_relation( headers: HeaderMap, Json(relation): Json, ) -> ApiResult> { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &workspace_id, headers, @@ -5022,7 +5028,7 @@ async fn scoped_remove_ticket_relation( headers: HeaderMap, Json(relation): Json, ) -> ApiResult> { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &workspace_id, headers, @@ -5051,7 +5057,7 @@ async fn scoped_query_ticket_relations( headers: HeaderMap, Json(query): Json, ) -> ApiResult>> { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &path.workspace_id, headers, @@ -5072,7 +5078,7 @@ async fn scoped_ticket_relation_view( AxumPath((workspace_id, id)): AxumPath<(String, String)>, headers: HeaderMap, ) -> ApiResult> { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &workspace_id, headers, @@ -5093,7 +5099,7 @@ async fn scoped_record_ticket_orchestration_plan( headers: HeaderMap, Json(record): Json, ) -> ApiResult> { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &workspace_id, headers, @@ -5121,7 +5127,7 @@ async fn scoped_query_ticket_orchestration_plans( headers: HeaderMap, Json(query): Json, ) -> ApiResult>> { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &path.workspace_id, headers, @@ -5142,7 +5148,7 @@ async fn scoped_ticket_doctor( AxumPath(path): AxumPath, headers: HeaderMap, ) -> ApiResult> { - let result = execute_worker_ticket_rest_operation( + let result = execute_ticket_rest_operation( &api, &path.workspace_id, headers, @@ -15390,14 +15396,9 @@ mod tests { }, ]; for operation in operations { - execute_worker_ticket_rest_operation( - &api, - TEST_WORKSPACE_ID, - source_headers(), - operation, - ) - .await - .unwrap(); + execute_ticket_rest_operation(&api, TEST_WORKSPACE_ID, source_headers(), operation) + .await + .unwrap(); } let inputs = execution.take_inputs(); @@ -18165,7 +18166,8 @@ mod tests { } #[tokio::test] - async fn ticket_rest_search_requires_worker_source_identity_and_rpc_route_is_removed() { + async fn ticket_rest_search_allows_workspace_product_clients_and_rejects_invalid_worker_source() + { let dir = tempfile::tempdir().unwrap(); let api = test_api(dir.path()).await; let app = build_router(api); @@ -18183,7 +18185,23 @@ mod tests { ) .await .unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!(response.status(), StatusCode::OK); + + let response = app + .clone() + .oneshot( + Request::builder() + .method("POST") + .uri(format!("/api/w/{TEST_WORKSPACE_ID}/tickets")) + .header("content-type", "application/json") + .body(Body::from( + serde_json::to_vec(&ticket::NewTicket::new("CLI Ticket")).unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); let response = app .clone()