Summary
OperatorResponse.response is a bare str; the router, the DB write and the agent write-back pass it through verbatim, and no layer checks that an approval item's recorded decision is one of the options the agent offered. #2370 shipped response: "approved" against options: ["Approve", "Deny"] for five months without a single 4xx — a server-side membership check would have turned the first tap into a named 422.
Context
The operator queue is the approval channel for irreversible agent actions (#1402 poison-park, ent#329 respond→resume, the TARGET_ARCHITECTURE v2 human-gate). Four producers exist today (desktop store, /m, the Workspace asks panel, the MCP tool) and the next one — a channel button, an ops agent — can re-ship the class unless the sink refuses. Defence in depth at the boundary (Product Quality Bar #6: a named validation error, never a silently broken state). Surfaced by the #2370 plan review.
Acceptance Criteria
Technical Notes
Summary
OperatorResponse.responseis a barestr; the router, the DB write and the agent write-back pass it through verbatim, and no layer checks that an approval item's recorded decision is one of the options the agent offered. #2370 shippedresponse: "approved"againstoptions: ["Approve", "Deny"]for five months without a single 4xx — a server-side membership check would have turned the first tap into a named 422.Context
The operator queue is the approval channel for irreversible agent actions (#1402 poison-park, ent#329 respond→resume, the TARGET_ARCHITECTURE v2 human-gate). Four producers exist today (desktop store,
/m, the Workspace asks panel, the MCP tool) and the next one — a channel button, an ops agent — can re-ship the class unless the sink refuses. Defence in depth at the boundary (Product Quality Bar #6: a named validation error, never a silently broken state). Surfaced by the #2370 plan review.Acceptance Criteria
type == 'approval'and real options (a non-empty list that is not the size-cap marker_OPTIONS_DROPPED_MARKER), aresponsenot inoptionsis rejected with a named 422 (e.g.response_not_an_offered_option) that lists the offered options; exact string match (options are agent-authored)acknowledgedare unaffectedPOST /api/operator-queue/{id}/respondand the Workspace asks answer path (src/backend/client_portal/asks/service.py) — a shared validator at the service/db chokepoint, not a copy per routerrespond_to_operator_queuesurfaces the 422 as a structured error (no silent success)Technical Notes
src/backend/models.py(OperatorResponse),src/backend/routers/operator_queue.py(respond),src/backend/db/operator_queue.py(optionsstored as JSON and loaded back in the row mapper),src/backend/services/operator_queue_service.py(_OPTIONS_DROPPED_MARKER),src/backend/client_portal/asks/service.py,src/mcp-server/src/tools/operator_queue.ts