fix(exec): rebuild command authorization on the Tree-sitter command planner#84172
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 15, 2026, 1:50 PM ET / 17:50 UTC. Summary PR surface: Source +1105, Tests -43, Other +4. Total +1066 across 51 files. Reproducibility: yes. by source inspection: current main still has ask-derived exec approval decisions and no Review metrics: 2 noteworthy metrics.
Stored data model Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Mantis proof suggestion Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land this as the canonical planner only after exec/gateway owners accept the stricter durable-trust semantics and resolve the overlapping planner branch. Do we have a high-confidence way to reproduce the issue? Yes by source inspection: current main still has ask-derived exec approval decisions and no Is this the best way to solve the issue? Yes with owner sign-off: using one parsed command model is the clean owner-boundary fix for keeping allowlist, persistence, and rendering consistent, but the stricter durable-trust behavior is a maintainer policy decision. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against a0b16f37e835. Label changesLabel justifications:
Evidence reviewedPR surface: Source +1105, Tests -43, Other +4. Total +1066 across 51 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
There was a problem hiding this comment.
Pull request overview
Introduces a new “candidate planner” boundary for command authorization so allowlisting/approval can reason about inner commands within POSIX shell wrappers (when payloads are static), while preserving legacy/Windows paths and existing “curl | sh” semantics. It also updates the UI approval modal to honor allowedDecisions from approval requests.
Changes:
- Add
exec-authorization-planto produce authorization groups/candidates (including shell-wrapper transport metadata and trust modes) and route non-Windows allowlist evaluation through it. - Adjust allowlist evaluation to return “planned” segments where appropriate (e.g., inner candidates for static wrappers, wrapper collapse for safe-bin rewrite), with expanded test coverage.
- Update exec approval UI parsing/rendering to carry
allowedDecisionsthrough and hide disallowed actions.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| ui/src/ui/views/exec-approval.ts | Hide disallowed approval actions based on allowedDecisions. |
| ui/src/ui/views/exec-approval.test.ts | Add UI coverage ensuring disallowed actions are hidden. |
| ui/src/ui/controllers/exec-approval.ts | Parse/normalize allowedDecisions from approval events. |
| ui/src/ui/controllers/exec-approval.test.ts | Add controller tests for preserving allowedDecisions. |
| src/node-host/invoke-system-run.test.ts | Add integration tests for planner-driven wrapper behavior and rewrites. |
| src/node-host/invoke-system-run-allowlist.ts | Prefer planned segments from allowlist evaluation when available. |
| src/infra/exec-authorization-plan.ts | New planner producing authorization groups/candidates (incl. wrapper unwrapping rules). |
| src/infra/exec-authorization-plan.test.ts | Unit tests for planner behavior (chains, pipelines, wrapper unwrapping/fallback). |
| src/infra/exec-authorization-allowlist.test.ts | New tests for candidate-based allowlist semantics (wrappers, curl |
| src/infra/exec-approvals-analysis.test.ts | Update expectations to reflect planned segment reporting. |
| src/infra/exec-approvals-allowlist.ts | Route non-Windows allowlist checks through planner; refine segment/trust handling. |
| src/infra/exec-approvals-allow-always.test.ts | Prevent persisting path-scoped wrapper payloads as reusable script trust. |
| src/agents/bash-tools.exec.approval-id.test.ts | Normalize allowlist patterns to real paths in tests. |
Comments suppressed due to low confidence (1)
ui/src/ui/views/exec-approval.ts:132
allowedDecisionsis now used to hide actions, but the modal cancel handler still always triggershandleExecApprovalDecision("deny")on Escape/backdrop cancel. If a request omits "deny" fromallowedDecisions(e.g. allow-once-only UIs), the user can still send a disallowed decision via cancel, which the gateway will reject. UpdatehandleCancel(and/or the modal’s cancelability) to only submit "deny" when it’s included inallowedDecisions, otherwise treat cancel as a no-op (or keep the modal open).
const allowedDecisions = request.allowedDecisions ?? ["allow-once", "allow-always", "deny"];
const title = isPlugin
? (active.pluginTitle ?? t("execApproval.pluginApprovalNeeded"))
: t("execApproval.execApprovalNeeded");
const titleId = "exec-approval-title";
const descriptionId = "exec-approval-description";
const handleCancel = () => {
if (!state.execApprovalBusy) {
void state.handleExecApprovalDecision("deny");
}
ce260c9 to
c7cbaba
Compare
|
ClawSweeper PR egg 🔥 Warming up: real-behavior proof passed; findings, security review, or rank-up moves are still in progress. Hatch commandComment Hatchability rules:
What is this egg doing here?
|
1817a26 to
27f001f
Compare
27f001f to
fe89c09
Compare
fe89c09 to
e45a6d2
Compare
e45a6d2 to
22304c8
Compare
|
Land-ready proof for ce23811 What changed since the last review:
Verification:
Known proof note:
|
Summary
This PR rebuilds POSIX command authorization on the Tree-sitter command model. The ad hoc shell splitter that previously decided allowlist matches and allow-always persistence is replaced by a candidate planner (
planShellAuthorization) built on the Tree-sitter-backedexplainShellCommand, so authorization, persistence, and shell rewriting all read one parsed view of the command instead of three approximations of it.src/infra/exec-approvals-analysis.tsshrinks from ~1300 lines to a thin re-export shim, with parsing now owned byexec-authorization-plan.ts/exec-authorization-render.tsBecause the planner can now prove what a command does (or admit it cannot), approval trust gets safer as a consequence:
unavailableDecisions) travel from the requesting host through the gateway protocol, and the gateway derives the renderedallowedDecisionssnapshot so approval surfaces only offer decisions that can actually be honoredOne parsed command model
flowchart LR CMD["raw shell command"] --> TS["explainShellCommand<br/>Tree-sitter parse"] TS --> PLAN["planShellAuthorization<br/>candidates + transports + trust modes"] PLAN --> AL["allowlist evaluation"] PLAN --> PERSIST["allow-always persistence<br/>coverage proof"] PLAN --> REW["enforced command rewrite"] OLD["old: ad hoc splitter<br/>re-derived per consumer"] -. replaced .-> PLANBefore this PR, allowlist matching, persistence, and shell rewriting each re-derived command structure through the string splitter in
exec-approvals-analysis.ts, and they could disagree. Now the plan is computed once and carried into every consumer; Windows keeps its dedicated analyzer (windows-shell-command.ts, moved out of the old monolith) because cmd/PowerShell parsing genuinely differs.How an approval flows
sequenceDiagram participant H as Agent host (gateway/node exec) participant P as Authorization planner participant G as Gateway server participant U as Approval surfaces (UI, push, channels) H->>P: planShellAuthorization(command) P-->>H: candidates + trust modes + coverage H->>H: resolveAllowAlwaysPersistenceDecision H->>G: exec.approval.request (command, ask, unavailableDecisions) G->>G: derive allowedDecisions from ask policy<br/>minus unavailable optional decisions G->>U: snapshot with allowedDecisions U->>G: resolve(decision) G->>G: reject decision not in allowedDecisions G-->>H: decision H->>H: persistAllowAlwaysDecision (patterns / one-shot)The requester is the only side that knows whether persistence is safe (it has the prepared argv, transport wrappers, platform, and coverage analysis), so it can mark optional approval decisions unavailable with the new
unavailableDecisionsrequest field. Today that list can only containallow-always;allow-onceanddenyremain server-owned baseline decisions. The gateway normalizes the request, derives theallowedDecisionssnapshot for renderers, and re-validates resolution server-side, so a client that ignores the narrowing still cannot push through a disallowedallow-always.How allow-always persistence is decided
flowchart TD A["allow-always chosen"] --> B{"Plan has hard blockers?<br/>runtime payload, prompt-only, unplanned"} B -- yes --> OS["one-shot<br/>nothing persisted"] B -- no --> C{"Reusable pattern coverage<br/>complete for every candidate?"} C -- yes --> PAT["persist executable patterns"] C -- no --> OS PAT --> R["future runs auto-allowed via allowlist"]On the gateway host, allowlisted commands whose execution plan cannot be safely rewritten (
requiresAllowlistPlanApproval) are downgraded to one-shot approval. Pattern-based durable trust deliberately does not bypass that gate, because enforcement cannot pin the resolved executables for an unenforceable plan.Decisions and rationale
planShellAuthorization()uses the Tree-sitter-backedexplainShellCommand()path, and the plan is carried into allowlist evaluation, allow-always persistence, and shell rewrite. The old POSIX splitter/render fallback is no longer an alternate source of candidates.unavailableDecisionsis additive protocol: optional field, enum-validated, and currently typed to the only optional exec approval decision (allow-always). Old or native clients that never send it keep the ask-derived behavior. The gateway still stores and publishes computedallowedDecisionsfor UI/rendering and resolve validation..ssh/~/.sshthrough basic read commands are approval-required hard-coded control policy.&is preserved as shell topology; the planner models it as a background relationship instead of failing closed.denystill wins when deny-mode policy is active.Behavior changes
sh -c 'git status'can now be authorized by the innergitcandidate when the payload is static and safely parsed.sh -c '$CMD', command substitution, line continuations, and path-scoped wrapper payloads stay approval-gated instead of becoming reusable inner approvals.curl URL | shbehavior is preserved: both sides must be allowed for auto-run; allow-always persistscurlonly; persistedcurlalone does not authorize the shell side on rerun.ask=alwaysapproval requests no longer offer an unusable Always Allow action.Protocol and storage
unavailableDecisionsis an optional gateway protocol request field; no version bump required. ComputedallowedDecisionsremains in stored/request snapshots for approval renderers.Known follow-up
Node
system.runkeeps the shipped behavior of auto-running allowlist-satisfied POSIX shell payloads even when the authorization plan cannot rebuild an enforced command (src/node-host/invoke-system-run-allowlist.ts,resolveSystemRunExecArgv). The gateway host is now stricter for the same case (requiresAllowlistPlanApproval). Extending the plan-unavailable approval gate to the node daemon is deliberate follow-up work, not part of this PR, to keep the diff bounded; node behavior is unchanged from shipped releases.Verification
Behavior addressed: plan-based command authorization with provable allow-always persistence and decision narrowing across all approval surfaces.
Real environment tested: local macOS focused suites on current head (
824c85b), including the finalunavailableDecisionsprotocol change.Exact steps or command run after this patch:
Evidence after fix: targeted Vitest passed 4 shards covering protocol validation, approval policy helpers, gateway approval handlers, and node/gateway host approval behavior;
pnpm tsgo:prodexited 0;oxfmt --checkexited 0 on the touched TypeScript files;git diff --checkexited 0; autoreview reported no accepted/actionable findings after verifyingallowedDecisionswas PR-local and unshipped.Observed result after fix: unpersistable commands offer Allow Once / Deny only; allow-always on a coverage-complete command persists executable patterns; unenforceable allowlisted commands stay one-shot; requesters disable only optional exec decisions through
unavailableDecisions, while gateway snapshots still expose computedallowedDecisions.What was not tested: Windows-native end-to-end exec; full GitHub CI for current head.