You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
decisions approve --yes runs headless, so an agent with a shell can approve its own proposal #25
decisions approve is kept off the MCP surface on purpose (mcp=False, "A person approves; no agent tool stands in for it"). But an agent with a shell, the common setup for coding agents, can run the CLI directly:
It runs headless (meta.headless: true), and --approver falls back to $USER, so the record shows the human as approver of a run the agent approved for itself.
Evidence that agents try
In a spike, an agent was told in its system prompt that only the operator approves. Nemotron 3 Ultra still tried to approve its own proposal in 2 of 3 runs, once 10 times in a row, guessing operations approve, approve and playbooks run. It did not find decisions approve only by chance; the harness blocked the rest.
Expected
The approval should not depend on the agent following instructions. Options:
refuse --yes when headless unless an explicit approval token or interactive confirmation is present
require a typed confirmation on a TTY (the decision id or its diff hash) before running
record how the approval arrived (TTY, token, policy) so cloudfall why can tell a human approval from an unattended one
Not filed as a vulnerability: the documented agent surface is MCP, which already excludes the command. This hardens the CLI path.
Triage: implement, for the next release candidate. Waits on your decision below.
Confirmed on main (d84a2ae): decisions approve is mcp=False (sdk/src/cloudfall/app.py), but from a shell cloudfall decisions approve <id> --yes runs with no further check, and --approver falls back to $USER, so the record names the human for an approval the agent gave. Nothing in the record says how the approval arrived.
The plan, once the design is settled:
Gate --yes in decisions_approve on how the command was started, per the option chosen below
Record how the approval arrived in the decision record (approval.via), and have cloudfall why print it
CLI tests for each path (TTY confirmed, TTY mistyped, headless refused or recorded)
Design. This changes a flag's behaviour and the decision record's schema, so it goes through the design gate. Compatibility: anyone who approves from a script or CI today breaks under option 1; the new approval.via field is additive, and records written before it read as via: unknown. No decisions log exists yet, so nothing settled is departed from.
A caveat for every option: an agent with a shell can drive a pseudo-terminal, so a TTY check is a speed bump, not a boundary. The real boundary is a secret or an OS user the agent can't reach, which is a larger feature. I'll link the PR here.
@romamo Decision needed: how should cloudfall decisions approve --yes behave when no person is at a terminal?
Refuse it headless, and on a terminal ask the person to type the decision id (recommended): an agent's plain shell call fails with a clear message, and every approval is recorded as typed at a terminal; scripted or CI approvals stop working until a token or policy feature exists
Keep it working headless, but record how it arrived (terminal or headless) and show that in cloudfall why: nothing breaks, but an agent can still approve its own proposal; it is only caught afterwards
Require an approval token headless (a file only the operator's account can read), and a typed confirmation on a terminal: the strongest, and keeps unattended approvals possible, but needs a separate OS user for the agent and a new setup step
Reply here with a number or your own answer; shipmill takes this up on the tick after your reply.
Decision by @romamo, relayed by Claude Code: 1. Refuse --yes headless; on a terminal the person types the decision id; record how the approval arrived (approval.via) and show it in cloudfall why.
On hold: waits on romamo/treaty#424, ctx.attest + requires_person. Treaty's prompts all have an agent-answerable flag by design, so the person-only confirmation is being added to Treaty first; Cloudfall then bumps from treaty rc29.
Triage: still implement as decided above (option 1), now waiting on a treaty release rather than a treaty decision.
romamo/treaty#424 closed: ctx.attest and requires_person merged in romamo/treaty#425 as f10143b (2026-10-08 16:50 UTC). But cloudfall pins treaty==1.0.0rc29 from PyPI, and treaty's newest release, 1.0.0rc37, was cut on 2026-10-07, before that merge. A treaty release run started at 16:50 UTC; the two before it failed.
On hold: waits on a treaty release that contains romamo/treaty#425 (rc38 or later). Labelled blocked until it's on PyPI; then the plan is a treaty bump from rc29 plus the --yes gate, approval.via and cloudfall why output from the plan above, in one PR. The bump from rc29 crosses eight treaty releases, so it may bring changes of its own.
Labelled release-blocker and put in the 0.6.0 milestone: the redesigned cloudfall.dev says the gate lives in Cloudfall and an agent can't go around it, and this issue is exactly an agent going around it. Stable 0.6.0 and new rcs wait for the fix; other merges continue.
Triage: unblocked; implementing option 1 as decided above.
treaty 1.0.0rc38 is on PyPI (released 2026-10-08 17:44 UTC) and contains romamo/treaty#425 (f10143b, ctx.attest and requires_person). The plan, in one PR:
Bump treaty from 1.0.0rc29 to 1.0.0rc38, and fix whatever the eight releases in between change
decisions approve: refuse --yes when headless; on a terminal the person types the decision id, through treaty's person-only confirmation
Record how the approval arrived (approval.via) in the decision record, and show it in cloudfall why; records written before it read as via: unknown
CLI tests for each path: terminal confirmed, terminal mistyped, headless refused
Draft PR: #37. It bumps treaty to rc38, makes decisions approve require a person to type the decision id at a terminal (headless exits 4 PERSON_REQUIRED), and records approval.via, shown by cloudfall why.
Trade-off: treaty reserves --yes on a person-only command, so the preview moves to decisions show. The bump also changes the stream output of dashboard serve and operator run. Both wait on the decision asked on #37.
operator approve has the same hole and is served over MCP: filed as #35.
Activity
shipmill-romamo commentedon Oct 8, 2026
Triage: implement, for the next release candidate. Waits on your decision below.
Confirmed on
main(d84a2ae):decisions approveismcp=False(sdk/src/cloudfall/app.py), but from a shellcloudfall decisions approve <id> --yesruns with no further check, and--approverfalls back to$USER, so the record names the human for an approval the agent gave. Nothing in the record says how the approval arrived.The plan, once the design is settled:
--yesindecisions_approveon how the command was started, per the option chosen belowapproval.via), and havecloudfall whyprint itDesign. This changes a flag's behaviour and the decision record's schema, so it goes through the design gate. Compatibility: anyone who approves from a script or CI today breaks under option 1; the new
approval.viafield is additive, and records written before it read asvia: unknown. No decisions log exists yet, so nothing settled is departed from.A caveat for every option: an agent with a shell can drive a pseudo-terminal, so a TTY check is a speed bump, not a boundary. The real boundary is a secret or an OS user the agent can't reach, which is a larger feature. I'll link the PR here.
shipmill-romamo commentedon Oct 8, 2026
@romamo Decision needed: how should
cloudfall decisions approve --yesbehave when no person is at a terminal?terminalorheadless) and show that incloudfall why: nothing breaks, but an agent can still approve its own proposal; it is only caught afterwardsReply here with a number or your own answer; shipmill takes this up on the tick after your reply.
romamo commentedon Oct 8, 2026
Decision by @romamo, relayed by Claude Code: 1. Refuse
--yesheadless; on a terminal the person types the decision id; record how the approval arrived (approval.via) and show it incloudfall why.On hold: waits on romamo/treaty#424, ctx.attest + requires_person. Treaty's prompts all have an agent-answerable flag by design, so the person-only confirmation is being added to Treaty first; Cloudfall then bumps from treaty rc29.
shipmill-romamo commentedon Oct 8, 2026
Triage: still implement as decided above (option 1), now waiting on a treaty release rather than a treaty decision.
romamo/treaty#424 closed:
ctx.attestandrequires_personmerged in romamo/treaty#425 asf10143b(2026-10-08 16:50 UTC). But cloudfall pinstreaty==1.0.0rc29from PyPI, and treaty's newest release, 1.0.0rc37, was cut on 2026-10-07, before that merge. A treaty release run started at 16:50 UTC; the two before it failed.On hold: waits on a treaty release that contains romamo/treaty#425 (rc38 or later). Labelled
blockeduntil it's on PyPI; then the plan is a treaty bump from rc29 plus the--yesgate,approval.viaandcloudfall whyoutput from the plan above, in one PR. The bump from rc29 crosses eight treaty releases, so it may bring changes of its own.shipmill-romamo commentedon Oct 8, 2026
Labelled
release-blockerand put in the 0.6.0 milestone: the redesigned cloudfall.dev says the gate lives in Cloudfall and an agent can't go around it, and this issue is exactly an agent going around it. Stable 0.6.0 and new rcs wait for the fix; other merges continue.shipmill-romamo commentedon Oct 8, 2026
Triage: unblocked; implementing option 1 as decided above.
treaty 1.0.0rc38 is on PyPI (released 2026-10-08 17:44 UTC) and contains romamo/treaty#425 (
f10143b,ctx.attestandrequires_person). The plan, in one PR:treatyfrom 1.0.0rc29 to 1.0.0rc38, and fix whatever the eight releases in between changedecisions approve: refuse--yeswhen headless; on a terminal the person types the decision id, through treaty's person-only confirmationapproval.via) in the decision record, and show it incloudfall why; records written before it read asvia: unknownI'll link the PR here.
shipmill-romamo commentedon Oct 8, 2026
Draft PR: #37. It bumps treaty to rc38, makes
decisions approverequire a person to type the decision id at a terminal (headless exits 4PERSON_REQUIRED), and recordsapproval.via, shown bycloudfall why.Trade-off: treaty reserves
--yeson a person-only command, so the preview moves todecisions show. The bump also changes the stream output ofdashboard serveandoperator run. Both wait on the decision asked on #37.operator approvehas the same hole and is served over MCP: filed as #35.shipmill-romamo commentedon Oct 9, 2026
Released in v0.6.0rc7.