Skip to content

permissions: the gate marks every call costly, so the surface cannot tell rm -rf from ls #953

Closed

Description

@santoshkumarradha

What happened

The engine's permission gate marks every call StakesCostly, deliberately and alike — rm -rf *, git push --force, and ls:

// THE STAKES ARE `costly` AND NOT `irreversible`, deliberately and for every
// call alike. This gate does not know what a command will do — that judgement
// is internal/approval's, and it is expressed as WHETHER TO ASK rather than as
// how much is at stake …

internal/session/consent.go (consentAsk). Nothing in internal/approval ever produces StakesIrreversible; only test fixtures reach that value.

So the surface cannot tell a grave call from an ordinary one, and every drawing that would like to — where the pointer opens, whether always is offered, whether a clock may run — has to treat them the same. The owner ruled on 2026-09-11 that a permission's pointer opens on allow once for an ordinary call and on deny for a grave one; the second half is unreachable until the engine says which is which, so #933 ships deny-first for all of them.

Replication

Deterministic (no model). In internal/session, raise the gate's own question for a destructive command and read its stakes:

q := agent.consentAsk(1, ai.ToolCall{Name: "bash", Arguments: `{"command":"rm -rf *"}`}, decision, true, "run it?")
// TODAY: q.Stakes == session.StakesCostly, and q.Options still carries the widening `always`.

Acceptance

  • internal/approval's judgement — the shapes it will never stop asking about — is what says a call is irreversible; the gate passes it through to Question.Stakes. No hardcoded command list in the surface, and no string matching on the command in internal/session (the repository's generic-and-meta law).
  • rm -rf *, git push --force, a drop table, a delete of a path outside the workspace: Stakes == StakesIrreversible, always not offered, no clock (Question.Check already refuses a clock on irreversible).
  • ls, go test, a read: StakesCostly, unchanged.
  • The manual (internal/manual/chat/permissions.md) says what enter does on each, in the asker's words ("what happens if I just press enter?").
  • End to end: the tmux suite raises a real gate for a destructive command and asserts the pointer is on deny, and one for an ordinary command asserting allow once — the second half of the owner's ruling, which Questions: one frame, one chooser, one key grammar — and the amber on the marks alone #933 could not ship.

Notes

Comes out of #830 (question views). #933 restores deny-first for every permission so the safe half is never wrong while this is open.


Drafted with CodeAF · reviewed and owned by the author

Activity

  1. santoshkumarradha commented on Sep 11, 2026

    @santoshkumarradha
    MemberAuthor

    Implementation pointer — the mechanism already exists, so this is a pass-through, not a new judgement.

    internal/approval/floor.go has AlwaysAsks(tool string, args json.RawMessage) bool. It is true for exactly the grave shapes: bash.go's critical table (criticalHit) and ActsInThePersonsName. Its doc comment already states the reason in the words this issue needs — these are the shapes that hold under a blanket allow, so a memo cannot swallow them.

    That is the same set as "irreversible", and it is why always must not be offered on them: a memo provably cannot cover one, so drawing the option is a lie the gate will refuse to honour.

    So the change is:

    1. internal/session/consent.go, consentAsk: Stakes = StakesIrreversible when approval.AlwaysAsks(call.Name, call.Arguments), StakesCostly otherwise. Replace the paragraph of the comment that says the stakes are the same for every call alike — do not leave it beside the new behaviour.
    2. Same place: drop the widening always option when the stakes are irreversible. One condition, from the same predicate — not a second list.
    3. internal/tui3: the pointer already reads questionHandsOnly(q) alone (Questions: one frame, one chooser, one key grammar — and the amber on the marks alone #933, deny-first for all). With stakes now graded, the ruling's other half becomes reachable: pointer on allow once for StakesCostly, on deny for StakesIrreversible. TestTheGateGradesNoCallAsIrreversibleAndMarksDenyTheSafeAnswer is the tripwire Questions: one frame, one chooser, one key grammar — and the amber on the marks alone #933 left behind and it asserts today's behaviour — it is now the thing to REPLACE, deliberately and in the same commit, with a test that asserts the graded split. That is the one existing assertion this issue is allowed to change; nothing else.

    NO string matching on the command in internal/session, and no command list anywhere outside internal/approval/bash.go — the generic-and-meta law. If a shape is missing, it is added to that table with its own test, never to a second table elsewhere.

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

    bugSomething the code does that it should not

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions