Skip to content

Mint per-segment grants for shell chain approvals - #598

Draft
TheGreatAxios wants to merge 1 commit into
mainfrom
cl-5752-evaluate-each-shell-chain-segment-against-grants-chain-length
Draft

Mint per-segment grants for shell chain approvals#598
TheGreatAxios wants to merge 1 commit into
mainfrom
cl-5752-evaluate-each-shell-chain-segment-against-grants-chain-length

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Collaborator

Closes CL-5752.

Root cause

Two related gaps let a shell chain approval buy less than it should:

  • Chains of 5+ top-level segments ("mega-chains") were accept-once only: buildRequests offered no scope, and mintGrant refused to persist anything even if a scope somehow arrived. The same long chain re-prompted every time regardless of what had already been approved.
  • Below that threshold, a multi-segment chain (e.g. npm i && curl x) only ever minted a grant for the exact whole-string pattern. A different chain sharing the same segments (npm i && curl y) still re-prompted from scratch.

The auto-approval evaluation loop in gate.ts already checks each segment independently via evaluateApprovals — the violation was entirely on the minting side.

Fix

  • Removed MEGA_CHAIN_SEGMENT_THRESHOLD, MEGA_CHAIN_NOTICE, and their usages (isMegaChain, the mega-chain ask rule, the notice, the fast-path hasExactFullCommandGrant shortcut it motivated).
  • mintGrant's run_shell path now decomposes the exact-chain scope into one Approval per real segment, reusing the existing quote-aware splitChainedCommand rather than adding a second parser.
  • Stripped the mega-chain bucket from scripts/approval-forensics.ts.

Behavior change — stated plainly

Granting per segment is strictly more permissive on later commands than granting one whole string was: a segment approved inside one chain now reuses on its own, or inside a different chain, later. Concretely:

  • Approving a && b then later running b && c now prompts only for c (previously it would re-ask for the whole new chain).
  • Chains of length 1, 2, and 8 all behave identically once every segment is granted — no more length-based special case.

Nothing that previously auto-approved now prompts, and nothing that previously required a fresh operator decision now silently skips one — an ungranted segment (including one hidden behind a wrapper like bash -c "granted && ungranted") still asks.

Tests

Updated the now-obsolete tests in permission.test.ts and tests/unit/permission/cross-commit-composition.test.ts, and added the discriminating cases:

  • approving a && b then running b && c prompts only for c
  • the same segment set in a different order gives the same verdict
  • a wrapper hiding an ungranted segment (bash -c "granted && ungranted") still prompts
  • all-granted chains of length 1, 2, and 8 behave identically

bun run check passes.

Test plan

  • bun run check green

Chains of 5+ segments were accept-once only: no scope was offered and
mintGrant refused to persist anything, so the same long chain
re-prompted forever no matter what had already been approved. Below
that threshold, multi-segment chains only minted a grant for the exact
whole-string match, so a different chain sharing segments re-prompted
too.

mintGrant now decomposes the exact-chain scope into one Approval per
real segment (reusing splitChainedCommand, the same splitter the
gate's own per-segment evaluation loop already uses) instead of
persisting a single whole-string pattern. Approving `a && b` grants
`a` and `b` individually, so a later chain reusing either segment only
prompts for what's new, and chains of any length behave the same way.

This is strictly more permissive on later commands than a whole-string
grant was, since a granted segment now reuses outside the chain it was
first approved in. Nothing that previously auto-approved now prompts,
and nothing that previously needed a fresh decision now silently skips
one.
@linear-code

linear-code Bot commented Aug 24, 2026

Copy link
Copy Markdown

CL-5752

@TheGreatAxios
TheGreatAxios marked this pull request as draft August 24, 2026 05:05
@TheGreatAxios

Copy link
Copy Markdown
Collaborator Author

Holding this as draft pending an operator decision on the security posture change.

Two things in this diff go beyond CL-5752's scope:

  1. mintGrant now decomposes an approved chain into one reusable grant per segment. The code comment claims this is "strictly no broader than the whole-string approval it replaces" — that is not correct, and the PR body says the opposite ("strictly more permissive"). Approving git status && rm -rf build now mints a standalone, persistable grant for rm -rf build that replays on its own and inside any future chain. The operator approved a chain; they did not approve its most dangerous segment as a free-standing command.

  2. MEGA_CHAIN_SEGMENT_THRESHOLD / accept-once-only was deleted. Its stated purpose was to stop one decision from covering an unbounded, ever-changing family of commands as the model appends segments. Removing it and adding per-segment minting together means a single approval of a long chain now yields N reusable grants.

CL-5752 asked for chain segments to be evaluated against existing grants so an already-approved segment stops re-prompting. That is the read path and it was already in the loop. Minting N grants from one approval is the write path and is a separate, larger decision.

Suggested split: keep the per-segment evaluation and the removal of the redundant hasExactFullCommandGrant fast path; restore MEGA_CHAIN_SEGMENT_THRESHOLD and keep whole-string minting. That delivers the ticket's over-prompting fix with no loosening.

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.

1 participant