Skip to content

decisionForOutcome

decisionForOutcome(outcome): ToolApprovalReply

Defined in: packages/agent/src/modules/acp/acpPermissions.ts:304

What an ACP permission outcome means — both to the gate and to the archive, which are two different questions with two different answers.

The safety polarity, unchanged: only an explicit allow runs anything

Section titled “The safety polarity, unchanged: only an explicit allow runs anything”

A cancel, an option id we do not recognise and an outcome variant that postdates this code all refuse the call, exactly as they always have. That polarity is the whole safety property: a client that answers with something unexpected must not be able to run a command, and a future ACP outcome must fail toward refusing rather than toward executing. Nothing below changes it, and nothing below may: approve is the only arm of ToolApprovalReply that runs a tool, and the runner turns every other arm into the same refusal.

And the part that is NOT the polarity: who the archive says refused

Section titled “And the part that is NOT the polarity: who the archive says refused”

[[EXT-110]] made the /debug-dump archive’s human-answer field readonly, written only by recordHumanAnswer from the reply a surface hands back. So what this function returns is what the archive believes a person did, and for three of the cases below a bare { type: 'reject' } said a human considered this command and refused it. That is the strongest statement the archive makes — the branch where the machine deferred and somebody took responsibility — and on those three it was false.

The messages here already distinguished the cases in prose while the TYPE collapsed them. Each now leaves as the arm that says only what is known:

  • cancelled → a teardown. The prompt ended with nobody’s answer on it, which is what teardown already means, so no third value is invented for it. The tempting one would be named for agency — the user cancelled — and the wire cannot support that claim: this outcome arrives both when a person presses stop and when the client shuts down or drops the connection, and nothing on the protocol tells those apart. Note this is the mirror of the reasoning that made teardown a value of its own rather than a reuse of no-human: there the reused value would have asserted LESS than the evidence, here a new one would assert MORE. The rule is the same both times — say exactly what is known and no more.
  • Any other outcome variant → unreadable. The wire type’s third arm is a custom or future outcome string, so this branch is NOT the cancel branch and must not borrow its answer: a variant we have never seen may well be an answer, and calling it a teardown would say the prompt was never answered when it may have been.
  • selected with an option id we do not offer → unreadable. A client that answered, in a vocabulary this build does not have. It is not a teardown — the prompt WAS answered — and it is not a refusal, because which of four options was chosen is exactly what we cannot say.
  • reject-once / reject-always → a refusal, and these two alone. The fixed constraint this function exists to hold: nothing records a human refusal unless the client returned a refusal option this code recognises. A future editor of this switch that needs a new reject row should be sure it can name the option the person pressed.

The next surface’s author decides this again, which is why the argument lives here rather than only in the ticket that made it.

The whole of this surface’s human-answer decisions

Section titled “The whole of this surface’s human-answer decisions”

This function is the only one. The two ACP apps (acpAgentApp.ts, acpAgentAppV1.ts) each wire exactly one setToolApprovalCallback, and both hand the client’s outcome straight to this function — there is no second place, and no V1 twin of it. Neither app wires setAttackHaltCallback at all, so an attack verdict on an ACP session reaches §6.2’s no-surface path and is archived as no-human, which is the true statement: on this surface the banner is never put in front of anybody.

allow-always maps to the always scope, which persists to the project allow-list — the same thing the terminal menu’s always approve does. [[EXT-107]] — reject-always is now its mirror and maps to always too, persisting to the project deny-list. One menu item cannot mean two lifetimes across three surfaces: the same answer given in the TUI, at the readline prompt and in an editor over ACP has to be the same refusal, or a user who learns the control on one surface has learned something untrue about the others. Both labels say “remember” so the user is told which pair of choices is written down.

The two unreadable arms and the teardown carry no scope and cannot, which is the property that stops a reply nobody can read from leaving a standing allow or deny rule behind it.

RequestPermissionOutcome

ToolApprovalReply