diff --git a/TASKS.md b/TASKS.md index 366be29..00e7890 100644 --- a/TASKS.md +++ b/TASKS.md @@ -2,18 +2,18 @@ ## Current -### `feat/error-structure-normalization` +### `feat/correlation-id-propagation` Status: in_progress DoD: -- runtime and API errors expose structured context where stable fields exist -- secret/runtime failure classes are mappable without parsing ad hoc message text -- opaque third-party details remain only where no stable structure exists +- admin-api, mcp-server, runtime, and adapters share a propagated correlation id +- request-scoped logs can be joined across layers without guessing from timestamps +- outbound adapter calls include correlation metadata where transport allows it ## Next -- `feat/correlation-id-propagation` +- `feat/runtime-rate-limiting-and-backpressure` ## Backlog diff --git a/apps/admin-api/src/error.rs b/apps/admin-api/src/error.rs index 23a9369..aa33ef9 100644 --- a/apps/admin-api/src/error.rs +++ b/apps/admin-api/src/error.rs @@ -69,20 +69,6 @@ impl ApiError { } } - pub fn not_found(message: impl Into) -> Self { - Self::NotFound { - message: message.into(), - context: None, - } - } - - pub fn conflict(message: impl Into) -> Self { - Self::Conflict { - message: message.into(), - context: None, - } - } - pub fn internal(message: impl Into) -> Self { Self::Internal { message: message.into(), diff --git a/apps/admin-api/src/service.rs b/apps/admin-api/src/service.rs index e7a8b64..eef5d4c 100644 --- a/apps/admin-api/src/service.rs +++ b/apps/admin-api/src/service.rs @@ -961,7 +961,12 @@ impl AdminService { .registry .get_auth_user_by_id(user_id) .await? - .ok_or_else(|| ApiError::not_found(format!("user {} was not found", user_id.as_str())))? + .ok_or_else(|| { + ApiError::not_found_with_context( + format!("user {} was not found", user_id.as_str()), + json!({ "user_id": user_id.as_str() }), + ) + })? .user; let memberships = self.registry.list_workspaces_for_user(user_id).await?; @@ -988,7 +993,10 @@ impl AdminService { .get_auth_user_by_id(user_id) .await? .ok_or_else(|| { - ApiError::not_found(format!("user {} was not found", user_id.as_str())) + ApiError::not_found_with_context( + format!("user {} was not found", user_id.as_str()), + json!({ "user_id": user_id.as_str() }), + ) })?; if !verify_password( @@ -1016,7 +1024,10 @@ impl AdminService { .get_workspace(workspace_id) .await? .ok_or_else(|| { - ApiError::not_found(format!("workspace {} was not found", workspace_id.as_str())) + ApiError::not_found_with_context( + format!("workspace {} was not found", workspace_id.as_str()), + json!({ "workspace_id": workspace_id.as_str() }), + ) }) } @@ -1071,11 +1082,17 @@ impl AdminService { .iter() .find(|membership| &membership.user.id == target_user_id) .ok_or_else(|| { - ApiError::not_found(format!( - "membership for user {} in workspace {} was not found", - target_user_id.as_str(), - workspace_id.as_str() - )) + ApiError::not_found_with_context( + format!( + "membership for user {} in workspace {} was not found", + target_user_id.as_str(), + workspace_id.as_str() + ), + json!({ + "workspace_id": workspace_id.as_str(), + "user_id": target_user_id.as_str(), + }), + ) })?; if !matches!( @@ -1122,11 +1139,17 @@ impl AdminService { .iter() .find(|membership| &membership.user.id == target_user_id) .ok_or_else(|| { - ApiError::not_found(format!( - "membership for user {} in workspace {} was not found", - target_user_id.as_str(), - workspace_id.as_str() - )) + ApiError::not_found_with_context( + format!( + "membership for user {} in workspace {} was not found", + target_user_id.as_str(), + workspace_id.as_str() + ), + json!({ + "workspace_id": workspace_id.as_str(), + "user_id": target_user_id.as_str(), + }), + ) })?; if !matches!( @@ -1826,10 +1849,10 @@ impl AdminService { .await? .is_some() { - return Err(ApiError::conflict(format!( - "operation with name {} already exists", - payload.name - ))); + return Err(ApiError::conflict_with_context( + format!("operation with name {} already exists", payload.name), + json!({ "name": payload.name }), + )); } let now = OffsetDateTime::now_utc(); @@ -1888,7 +1911,10 @@ impl AdminService { .get_operation_summary(workspace_id, operation_id) .await? .ok_or_else(|| { - ApiError::not_found(format!("operation {} was not found", operation_id.as_str())) + ApiError::not_found_with_context( + format!("operation {} was not found", operation_id.as_str()), + json!({ "operation_id": operation_id.as_str() }), + ) })?; let now = OffsetDateTime::now_utc(); let version = summary.current_draft_version + 1; @@ -2757,10 +2783,10 @@ impl AdminService { .await? .is_some() { - return Err(ApiError::conflict(format!( - "agent with slug {} already exists", - payload.slug - ))); + return Err(ApiError::conflict_with_context( + format!("agent with slug {} already exists", payload.slug), + json!({ "slug": payload.slug }), + )); } let now = OffsetDateTime::now_utc(); @@ -2816,7 +2842,10 @@ impl AdminService { .get_agent_summary(workspace_id, agent_id) .await? .ok_or_else(|| { - ApiError::not_found(format!("agent {} was not found", agent_id.as_str())) + ApiError::not_found_with_context( + format!("agent {} was not found", agent_id.as_str()), + json!({ "agent_id": agent_id.as_str() }), + ) })?; if payload.slug != existing.slug @@ -2825,10 +2854,10 @@ impl AdminService { .await? .is_some() { - return Err(ApiError::conflict(format!( - "agent with slug {} already exists", - payload.slug - ))); + return Err(ApiError::conflict_with_context( + format!("agent with slug {} already exists", payload.slug), + json!({ "slug": payload.slug }), + )); } let updated_at = OffsetDateTime::now_utc(); @@ -2861,7 +2890,10 @@ impl AdminService { .get_agent_summary(workspace_id, agent_id) .await? .ok_or_else(|| { - ApiError::not_found(format!("agent {} was not found", agent_id.as_str())) + ApiError::not_found_with_context( + format!("agent {} was not found", agent_id.as_str()), + json!({ "agent_id": agent_id.as_str() }), + ) })?; self.registry.delete_agent(workspace_id, agent_id).await?; @@ -4924,11 +4956,17 @@ fn ensure_stream_session_workspace( return Ok(()); } - Err(ApiError::not_found(format!( - "stream session {} was not found in workspace {}", - session.id.as_str(), - workspace_id.as_str() - ))) + Err(ApiError::not_found_with_context( + format!( + "stream session {} was not found in workspace {}", + session.id.as_str(), + workspace_id.as_str() + ), + json!({ + "session_id": session.id.as_str(), + "workspace_id": workspace_id.as_str(), + }), + )) } fn ensure_async_job_workspace( @@ -4939,11 +4977,17 @@ fn ensure_async_job_workspace( return Ok(()); } - Err(ApiError::not_found(format!( - "async job {} was not found in workspace {}", - job.id.as_str(), - workspace_id.as_str() - ))) + Err(ApiError::not_found_with_context( + format!( + "async job {} was not found in workspace {}", + job.id.as_str(), + workspace_id.as_str() + ), + json!({ + "job_id": job.id.as_str(), + "workspace_id": workspace_id.as_str(), + }), + )) } fn usage_window(period: UsagePeriod) -> Result<(UsagePeriod, String, UsageBucket), ApiError> {