Skip to content

feat(mcp): respond to / resolve Operator Queue items over MCP (#1104, #1101 follow-up)#1125

Merged
vybe merged 2 commits into
devfrom
feature/1104-operator-queue-respond-mcp
Jun 17, 2026
Merged

feat(mcp): respond to / resolve Operator Queue items over MCP (#1104, #1101 follow-up)#1125
vybe merged 2 commits into
devfrom
feature/1104-operator-queue-respond-mcp

Conversation

@dolho

@dolho dolho commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

#1101 shipped the read half of the operator-queue MCP surface (list_operator_queue, get_operator_queue_item) and explicitly deferred respond/resolve to a follow-up. This is that follow-up: agents and external Claude Code clients can now resolve Operating Room items — answer questions, approve/deny approval requests — closing the raise → observe → respond round-trip and finishing Invariant #13 (MCP = third surface in sync) for OPS-001.

Changes

  • tools/operator_queue.ts — new respond_to_operator_queue tool. Resolves the item's agent_name via get first (so the gate has a target + surfaces 404 cleanly), applies the same checkAgentAccess gate as the read tools, then proxies POST /api/operator-queue/{id}/respond. Backend 400 on a non-pending item → structured { error }, not a throw.
  • client.tsrespondToOperatorQueueItem(id, {response, response_text}).
  • server.ts — registration comment (auto-registered via addAllTools).
  • operator_queue.test.ts — 4 tests.
  • architecture.md — operator-queue row: read-only → read + respond (2 → 3 tools).

Access control (the crux)

The backend resolves an agent-scoped key to its owner and does NOT apply agent_permissions (architecture §5) — so agent-to-agent gating lives in the MCP layer. The write tool resolves agent_name, then runs the identical checkAgentAccess the read tools use: an agent-scoped key may resolve items for {self} ∪ permitted only. On denial the write is never attempted (pinned by a test).

Design decisions (per the issue)

  • respond only in v1; cancel deferred (wider blast radius).
  • Approvals are agent-respondable for permitted agents (no type-gating) — thin proxy, reuse the existing access gate. The backend already enforces ownership/accessibility and pending-state; type-restricting which items an agent may resolve can be a later refinement if needed.

Acceptance criteria

  • respond_to_operator_queue proxies POST /{id}/respond with response (required) + optional response_text.
  • Access control honored for writes — reuses checkAgentAccess ({self} ∪ permitted).
  • Resolves agent_name before gating.
  • State-machine errors (400 on non-pending) surfaced as structured { error }.
  • MCP tool count + architecture.md row updated.
  • No new backend endpoints — thin TS proxy.
  • cancel — intentionally deferred.

Verification

  • npx tsc --noEmit clean.
  • 20/20 mcp-server tests (4 new): deny-on-non-permitted and never writes; self-resolve proxies the exact body; permitted non-self resolve; backend 400 → structured error.
  • Tool (respond_to_operator_queue) + client method (respondToOperatorQueueItem) compiled into the running container's dist and auto-registered.

Related to #1104

🤖 Generated with Claude Code

…ver MCP (#1104)

#1101 shipped the read half of the operator-queue MCP surface and deferred
the write/respond half. This is that follow-up: agents and external Claude
Code clients can now close the loop — answer questions, approve/deny approval
requests — on items they can already read, completing raise → observe →
respond and finishing Invariant #13 for OPS-001.

- New `respond_to_operator_queue` tool (`tools/operator_queue.ts`): resolves
  the item's agent_name via get first, applies the SAME MCP-layer
  checkAgentAccess gate as the read tools ({self} ∪ permitted — the backend
  resolves an agent key to its owner and does NOT apply agent_permissions),
  then proxies POST /api/operator-queue/{id}/respond. Backend 400 on a
  non-pending item is surfaced as a structured { error }, not a throw.
- `respondToOperatorQueueItem` client method (`client.ts`).
- 4 unit tests: deny-on-non-permitted (and never writes), self-resolve
  proxies the body, permitted non-self resolve, 400→structured error.
- architecture.md operator-queue row: read-only → read + respond (2→3 tools).

v1 scope: `respond` only; `cancel` deferred (wider blast radius). No backend
changes — thin TS proxy over the existing REST route.

Verified: tsc clean; 20/20 mcp-server tests; tool + client method compiled
into the running container's dist and auto-registered.

Related to #1104

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dolho

dolho commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

🔎 Code review (high-effort, recall-biased) — no blocking bugs

Faithful mirror of the verified #1101 read-tool pattern; clean. No correctness bugs survived verification.

Verified

  • Body shaperesponse_text: undefined is dropped by JSON.stringify, so the backend OperatorResponse{response, response_text=None} is satisfied; response is min(1) matching the required backend field.
  • Defense-in-depth holds for user/system scopecheckAgentAccess returns allowed for non-agent scopes, but the backend respond_to_queue_item independently runs _assert_agent_accessible + 404/pending checks. The MCP gate is purely the agent→agent layer (the backend can't apply agent_permissions), exactly as designed — no authz gap.
  • Deny-before-write pinned by a test (responded === false); permitted/self/400-paths all covered (20/20 tests, tsc clean, live-registered in the running container's dist).
  • Info-leak parity — get-then-gate reveals an item's existence on denial, but identical to the existing get_operator_queue_item; not introduced here.

Non-blocking notes

  1. Micro-efficiency — the pre-fetch getOperatorQueueItem is only needed to give the agent-scope gate a target; for user/system keys it's redundant with the backend's own 404/accessibility checks. Could if (scope === 'agent')-guard it, but it also yields a clean 404 and mirrors the read tool — fine to leave.
  2. DRY — the get + checkAgentAccess preamble is now duplicated between get_operator_queue_item and respond_to_operator_queue (~10 lines). A shared resolveAndGate(client, ctx, id) helper would dedupe if a third write tool (cancel) lands later. Not worth it for two callers.

✅ Ready to merge.

Reviewed with the code-review skill.

@github-actions

Copy link
Copy Markdown

⚠️ Nightly unit-suite check skipped — merge conflict against dev.

Resolve by running git merge dev locally and pushing the result. The next nightly run will re-test once the conflict is gone.

# Conflicts:
#	docs/memory/architecture.md

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated: respond-to-operator-queue over MCP (#1104/#1101 follow-up). Resolved architecture.md conflict (kept dev's editorial trims + the operator_queue.ts (3) respond entry). base=dev, all checks green incl. schema-parity + verify-non-root.

@vybe vybe merged commit 6d81e14 into dev Jun 17, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants