Files
bot-bottle/docs/prds/prd-new-nonblocking-supervise.md
T
didericis 034f774529
test / integration (pull_request) Successful in 10s
tracker-policy-pr / check-pr (pull_request) Successful in 11s
test / coverage (pull_request) Successful in 39s
test / unit (pull_request) Successful in 1m30s
prd-number / assign-numbers (push) Failing after 10s
test / integration (push) Successful in 7s
test / unit (push) Successful in 30s
lint / lint (push) Successful in 42s
test / coverage (push) Successful in 35s
Update Quality Badges / update-badges (push) Successful in 34s
feat(supervise): non-blocking MCP — pending carries proposal id + check-proposal poll tool
Closes #412.

The supervise MCP server blocked the agent's tool call polling for the
operator's decision, and on timeout returned `status: pending` with no
proposal id and no way to poll a specific proposal — so the only way to
learn a late decision was to re-propose (a duplicate).

- `handle_tools_call` pending timeout now returns the `proposal_id` and
  points the agent at `check-proposal`.
- New `check-proposal` MCP tool: non-blocking status lookup by proposal id
  (pending | approved | modified | rejected | unknown). Reuses the queue's
  FileNotFoundError semantics; archives a decided proposal exactly like the
  synchronous path, so a pending proposal stays visible to the operator
  until it's both decided and polled.
- `TOOL_CHECK_PROPOSAL` constant, re-exported from supervise; kept out of
  TOOLS since it never becomes a Proposal.tool.

Enforcement is unchanged — the tools only propose policy; the egress proxy
and git-gate still enforce — so returning early opens no hole. Follow-ups
(git-gate reject-requeue, backpressure, notifications, web console) are in
the PRD.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBCHap11yGAKuKfsehNPaD
2026-07-18 17:06:04 -04:00

5.4 KiB

PRD prd-new: Non-blocking supervise (async approval + proposal polling)

  • Status: Draft
  • Author: didericis
  • Created: 2026-07-18
  • Issue: #412

Summary

The per-bottle supervise MCP server (bot_bottle/supervise_server.py) answers tools/call synchronously: it queues the agent's proposal and blocks the tool call polling for the operator's decision. On timeout it returns status: pending and leaves the proposal queued — but it hands the agent no proposal id and offers no way to poll a specific pending proposal, so the only way to learn the outcome is to re-propose (a duplicate).

This PRD makes the MCP flow non-blocking and pollable, so an approval can happen out-of-band (a human taking minutes-to-hours in a review console) without holding an HTTP request open or wedging the agent:

  1. Include the proposal_id in the pending response.
  2. Add a check-proposal MCP tool: a non-blocking status lookup by proposal id.
  3. Keep the short synchronous grace window for the common "operator is right there" fast path.

Problem

handle_tools_call_sv.wait_for_response(...) blocks up to SUPERVISE_RESPONSE_TIMEOUT_SECONDS (default 30s). Two problems follow:

  • Human latency ≠ tool-call latency. A real review — rendered diff, RBAC routing to an approver, someone tapping approve on their phone — is minutes-to-hours. Holding the MCP request open that long is fragile (proxy/keepalive timeouts, the mitmproxy egress hop, and the agent harness's own tool-call timeout, which a long block can trip and stall the whole turn).
  • No resume path. The pending fallback already exists, but without a proposal id and a poll tool the agent can't reconnect to that specific decision — it re-proposes, duplicating the queue entry.

This is also the precondition for the planned web-console human-review flow (RBAC, audit retention, mobile) — see issue #412.

Safety note: the MCP tools only propose policy changes; enforcement stays at the egress proxy and the git-gate. Returning early on pending therefore opens no hole — the agent still cannot egress or push anything unapproved.

Goals / success criteria

  • A pending MCP response carries the proposal_id.
  • An agent can call check-proposal(proposal_id) and get the current state (pending | approved | modified | rejected) without blocking and without creating a new proposal.
  • The synchronous fast path (operator approves within the grace window) is unchanged: the first tools/call still returns the decision directly.
  • No change to enforcement, attribution (source-IP → bottle), or the operator-side queue/response schema.

Non-goals

  • The git-gate pre-receive path (it is synchronous by nature and cannot poll — its async variant is reject-fast + re-push; tracked as a follow-up).
  • Backpressure / in-flight-proposal caps.
  • MCP server→client notifications (event-driven resume).
  • Any web-console UI (this PRD is the protocol groundwork it needs).

Design

pending response carries the id

handle_tools_call's timeout branch formats the pending text with the proposal.id and a pointer to check-proposal, so the agent knows what to poll.

check-proposal tool

A new read-only MCP tool (TOOL_CHECK_PROPOSAL = "check-proposal"), attributed to the calling bottle by source IP exactly like the proposal tools. Input: { "proposal_id": string }. Behavior:

  1. read_response(slug, id)
    • found: archive the proposal (same terminal step the synchronous path takes) and return the decision via format_response_text; isError iff rejected.
  2. not foundread_proposal(slug, id)
    • found: still queued → return status: pending.
    • not found: unknown id, or already resolved-and-archived (e.g. a second poll) → return status: unknown, isError: true.

Both lookups already raise FileNotFoundError when absent (queue_store.py), so the handler needs no new store methods. check- proposal is the only path (besides the synchronous response) that archives, so a proposal that times out to pending stays visible to the operator until it is decided and then polled.

Grace window

Left at the existing 30s default (SUPERVISE_RESPONSE_TIMEOUT_SECONDS), which doubles as the instant-approve fast path. Tuning it down is an operator setting, not a code change; noted for the console rollout.

Implementation chunks

  1. (this PR) TOOL_CHECK_PROPOSAL constant; check-proposal tool definition + handle_check_proposal; dispatch wiring; proposal_id in the pending text; unit tests. Files: bot_bottle/supervise_types.py, bot_bottle/supervise.py (re-export), bot_bottle/supervise_server.py, tests/unit/test_supervise_server.py.
  2. (follow-up) git-gate pre-receive reject-fast + re-push.
  3. (follow-up) per-bottle in-flight-proposal backpressure cap.
  4. (follow-up) MCP notifications for event-driven resume; web-console review flow (RBAC, audit retention) on top.

Open questions

  • Should a resolved-but-unpolled proposal auto-archive after some TTL, or only on poll? (Leaning: only on poll, so a decision is never lost to a reaper before the agent sees it.)
  • Does the agent harness need an explicit "you have a pending proposal" nudge, or is returning pending from the original call enough? (Deferred to the notifications chunk.)