Skip to content

Require a person at a terminal for operator approve - #42

Merged
shipmill-romamo[bot] merged 1 commit into
mainfrom
feat/35-operator-approve-requires-person
Oct 9, 2026
Merged

shipmill-romamo[bot] merged 1 commit into
mainfrom
feat/35-operator-approve-requires-person

Conversation

@shipmill-romamo

Copy link
Copy Markdown
Contributor

Summary

cloudfall operator approve <id> ran a proposal's playbook and its verify step on any shell call, so an agent with a shell could approve its own proposal. It now follows decisions approve (#37): the command is requires_person=True and calls ctx.attest, so the person types the proposal id at a terminal before the playbook or the verify step runs. No flag answers it, --yes and a raw payload's "yes": true included. Off a terminal it exits 4 with PERSON_REQUIRED; a mistyped id exits 4 with ATTESTATION_MISMATCH; either way the proposal stays proposed. operator show is the preview.

A person's approval on the receipt records approval.via: terminal, reusing #37's ApprovalChannel; the field is optional in operator-proposal.schema.json. Docs: docs/operator-guide.md (Reviewing and approving), sdk/README.md, and a CHANGELOG Security entry beside #25's.

Stacked on #38. #38's branch predates #37, so this branch also carries a replay of #37's commit (05a7348, already on main as 327ef40); this PR's own change is 22b7de2. On main alone the change breaks cloudfall mcp serve --project (MCP_BIND_UNKNOWN: "McpServe bind fixed 'gateway_url', which no served command tool has."), because requires_person implies mcp=False; #38 removes those gateway flags. The decorator keeps #38's explicit mcp=False next to requires_person=True.

Test notes

  • uv sync, uv run ruff check ., uv run mypy, uv run cloudfall config validate --project config/examples, uv lock --check: pass
  • uv run pytest: 416 passed
  • New tests in sdk/tests/test_operator.py: off a terminal with and without --yes, --raw-payload with "yes": true, stdin-only and stdout-only terminals all exit PERSON_REQUIRED; a mistyped id gives ATTESTATION_MISMATCH; each refusal leaves the proposal proposed with no approval or outcome. The typed id runs the playbook and records via: terminal (the example hosts do not resolve, so the engine step fails fast with ENGINE_STEP_FAILED). The gateway flags still parse and still name missing material. Older receipts read as via: unknown; a new approval cannot be unknown
  • Not exercised: a real interactive terminal session; the tests use treaty's simulated terminal streams, as Require a person at a terminal to approve a decision #37's do

Decisions for you

  • Behaviour change: operator approve refuses any approval made off a terminal, so scripted or CI approvals stop working. Autonomous runs licensed by an OperatorPolicy are unaffected
  • Public API change: cloudfall.operator.approve requires a keyword-only via: ApprovalChannel and rejects UNKNOWN (operator_approval_channel_unknown). ApprovalRecord gains via; a human approval without one, or a policy approval with one, is rejected
  • Stored state: the proposal schema gains optional approval.via (terminal or unknown); only a person's approval carries it
  • Check order: the gateway-material check runs before the person is asked to type, so off a terminal a missing --gateway-* flag reports PRECONDITION, not PERSON_REQUIRED
  • Unchanged on purpose: operator receipts still record no approver name (no --approver, unlike decisions approve); the guide's "Who approved" line and "Through an agent" section are Take operator approve off the MCP server #38's
  • Not verified: a real interactive terminal session

Part of #35

🤖 Generated with Claude Code

operator approve ran a proposal's playbook and its verify step on any
shell call, so an agent with a shell could approve its own proposal. It
now follows decisions approve (#37): requires_person=True, and ctx.attest
asks the person to type the proposal id before anything runs. No flag
answers it, --yes and a raw payload's "yes" included; off a terminal it
exits 4 with PERSON_REQUIRED, a mistyped id with ATTESTATION_MISMATCH,
and the proposal stays proposed. operator show is the preview.

A person's approval on the receipt records approval.via (terminal; a
human approval written before reads as unknown; a policy's has none),
reusing ApprovalChannel from the decision record. The field is optional
in operator-proposal.schema.json.

requires_person implies mcp=False, so on its own this breaks
`mcp serve --project`: its bound gateway_url no longer names a served
tool (MCP_BIND_UNKNOWN). It lands after #38, which takes the gateway
flags off mcp serve.

Part of #35

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shipmill-romamo
shipmill-romamo Bot force-pushed the fix/35-operator-approve-off-mcp branch from a9022a7 to f668590 Compare October 9, 2026 08:17
@shipmill-romamo
shipmill-romamo Bot force-pushed the feat/35-operator-approve-requires-person branch from 22b7de2 to d213d13 Compare October 9, 2026 08:17
Base automatically changed from fix/35-operator-approve-off-mcp to main October 9, 2026 08:21
@shipmill-romamo
shipmill-romamo Bot marked this pull request as ready for review October 9, 2026 08:21
@shipmill-romamo
shipmill-romamo Bot merged commit 20b33c7 into main Oct 9, 2026
1 check passed
@shipmill-romamo
shipmill-romamo Bot deleted the feat/35-operator-approve-requires-person branch October 9, 2026 08:21
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.

0 participants