Skip to content

operator approve is an MCP tool, so an agent can approve its own proposal without a shell #35

Closed

Description

@shipmill-romamo

What happens

decisions approve is kept off the MCP surface (mcp=False, "A person approves; no agent tool stands in for it"). operator approve is not: on a project, cloudfall mcp serve serves it as a tool, because operator.approve is in the project command set in sdk/src/cloudfall/agent_server.py (line 311), and the command in sdk/src/cloudfall/app.py (operator_approve, around line 2534) has no mcp=False and no person check.

So an agent connected over MCP to a project can approve an operator proposal and run it, with no shell needed. This is the same hole as #25, on the surface the docs call the agent's.

Expected

An agent can't approve its own proposal over MCP. At least: operator approve off the MCP surface, as decisions approve is. Whether it also gets the person-only confirmation from #25 (ctx.attest) belongs with #25's design.

Found while implementing #25.

Activity

  1. shipmill-romamo commented on Oct 8, 2026

    @shipmill-romamo
    ContributorAuthor

    Triage: implement the MCP part now; the person check waits on #37.

    Confirmed on main (9cb777c):

    • operator approve (sdk/src/cloudfall/app.py, operator_approve) has no mcp=False, and operator.approve is in PROJECT_COMMANDS (sdk/src/cloudfall/agent_server.py), so cloudfall mcp serve on a project serves it as a tool
    • It runs the proposal's playbook and verifies it with no person check, so an agent connected over MCP can approve and run its own proposal
    • decisions approve already sets the rule: mcp=False, "A person approves; no agent tool stands in for it"

    Design. This removes a tool from the MCP surface, a contract change, so it went through the design gate. It follows the rule decisions approve already states, so there is no new choice to make:

    • operator approve gets mcp=False with the same comment as decisions approve, and operator.approve leaves PROJECT_COMMANDS
    • A test checks it is not in the MCP tool list, beside the decisions approve one
    • Docs that list operator_approve as a tool, or tell an agent to call it, are updated; the CHANGELOG says it under Security
    • Compatibility: an MCP client that called operator_approve gets "unknown tool". That is the point of the fix; a person approves with cloudfall operator approve from a shell

    The PR says "Part of #35". On hold for the rest: the person-only confirmation (requires_person and ctx.attest, as in #25) waits on #37, which brings treaty rc38 and is waiting on decision A there. Once #37 merges, operator approve gets the same treatment in a follow-up PR.

    Labelled release-blocker and put in the 0.6.0 milestone, for the reason #25 was: the site says an agent can't go around the gate, and this is a way around it on the agent's own surface. I'll link the PR here.

  2. added this to the 0.6.0 milestone on Oct 8, 2026
  3. shipmill-romamo commented on Oct 8, 2026

    @shipmill-romamo
    ContributorAuthor

    PR: #38 (Part of #35). It takes operator approve off the MCP surface (mcp=False, out of PROJECT_COMMANDS), adds a test that the tool is gone and a call to it gets UNKNOWN_TOOL, and rewrites the operator guide, sdk/README.md and the server's MCP instructions to say a person approves from a shell.

    Trade-off: the four --gateway-* flags of cloudfall mcp serve fed only that tool and are removed with it, which breaks a server started with them. That waits on the decision asked on #38. The person-only confirmation still waits on #37, as above.

  4. shipmill-romamo commented on Oct 8, 2026

    @shipmill-romamo
    ContributorAuthor

    Triage: the person check is unblocked. #37 merged (964d889), so main now has treaty rc38 with requires_person and ctx.attest, and decisions approve uses them.

    Plan for the rest of this issue, in its own PR, separate from #38:

  5. shipmill-romamo commented on Oct 8, 2026

    @shipmill-romamo
    ContributorAuthor

    PR: #42 (draft, Part of #35, stacked on #38). It declares operator approve requires_person=True, so the person types the proposal id at a terminal and no flag answers that prompt. Off a terminal it exits 4 with PERSON_REQUIRED, and a mistyped id exits 4 with ATTESTATION_MISMATCH. Either way the playbook and the verify step don't run. A person's approval records approval.via: terminal. 416 tests pass on the stack.

    It has to land after #38. treaty turns requires_person into mcp=False, so on main alone cloudfall mcp serve --project would still bind the --gateway-* flags to a tool that's gone, and it would refuse to start with MCP_BIND_UNKNOWN. #38 removes those flags, and that still waits on the question asked there.

    Trade-offs, also listed in #42:

    • Scripted or CI runs of operator approve stop working. Runs licensed by an OperatorPolicy are unaffected
    • cloudfall.operator.approve takes a keyword-only via, and the proposal schema gains an optional approval.via
    • When --gateway-* material is missing, the command reports PRECONDITION before it asks the person to type the id

    #38's branch predates #37, so #42's diff also shows #37's files until #38 is rebased onto main.

  6. shipmill-romamo commented on Oct 9, 2026

    @shipmill-romamo
    ContributorAuthor

    Closed: #38 and #42 both landed; see the next comment.

  7. shipmill-romamo commented on Oct 9, 2026

    @shipmill-romamo
    ContributorAuthor

    Both parts have landed on main, so I'm closing this:

    The review of #42 found a small schema gap, filed as #46: the schema accepts approval.via on an autonomous approval. It doesn't reopen this hole.

  8. shipmill-romamo commented on Oct 9, 2026

    @shipmill-romamo
    ContributorAuthor

    Shipped in 0.6.0rc7 (#38 and #42): uv tool install cloudfall==0.6.0rc7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    release-blockerHolds the release lanes the policy names until closed

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions