diff --git a/docs/diagrams/06-mq-protocol.puml b/docs/diagrams/06-mq-protocol.puml index a1ecd1cd..5d188813 100644 --- a/docs/diagrams/06-mq-protocol.puml +++ b/docs/diagrams/06-mq-protocol.puml @@ -61,6 +61,7 @@ package "Inbound Messages (Client → Bridge)" as InPkg #FFF3E0 { + target_node: str = "" + initial_message: str = "" + skill: str = "" + + user_id: str = "" } class CloseWorkstreamMessage { diff --git a/docs/diagrams/11-console-data-flow.puml b/docs/diagrams/11-console-data-flow.puml index 26895b40..06d13901 100644 --- a/docs/diagrams/11-console-data-flow.puml +++ b/docs/diagrams/11-console-data-flow.puml @@ -154,7 +154,7 @@ activate Server #FFECB3 Server -> CC : _pick_best_node() or\nget_node_detail(node_id) CC --> Server : node validated -Server -> Server : Build CreateWorkstreamMessage\n{target_node:"nodeA", name:"new-task"} +Server -> Server : Build CreateWorkstreamMessage\n{target_node:"nodeA", name:"new-task",\nuser_id: from auth_result} Server -> Redis : RPUSH turnstone:inbound:nodeA\n(directed queue) Server --> Browser : {status:"ok", correlation_id:"abc",\ntarget_node:"nodeA"} @@ -163,7 +163,8 @@ deactivate Server note right of Redis Bridge on Node-A picks up the message from its directed queue, - POSTs to /v1/api/workstreams/new, + POSTs to /v1/api/workstreams/new + (forwarding user_id in payload), registers ownership, publishes ws_created to cluster channel. end note diff --git a/docs/security.md b/docs/security.md index 2d4956a1..19f6f93c 100644 --- a/docs/security.md +++ b/docs/security.md @@ -499,6 +499,17 @@ Service tokens use 1-hour expiry with automatic refresh via httpx event hooks to ensure rotated tokens are picked up on SSE reconnects. +### User identity in MQ-dispatched workstreams + +When the console creates a workstream via MQ (the normal path), the +authenticated user's `user_id` is embedded in the +`CreateWorkstreamMessage`. The bridge forwards this `user_id` in the +HTTP payload when calling the server's `POST /v1/api/workstreams/new`. +The server accepts a `user_id` from the request body **only when the +caller is a trusted service** — identified by `token_source` matching +`bridge`, `console-proxy`, or `console`. Regular API callers cannot +override `user_id`; the server always uses their JWT identity. + Note that the channel gateway uses a distinct JWT audience (`turnstone-channel`) from the server (`turnstone-server`) and console (`turnstone-console`). A server-scoped JWT cannot authenticate to the