fix(cli): keep ask and plan modes read-only under broad permission rules - #13124
Conversation
6b1bd9d to
727bf37
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 727bf37bb2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Incremental review of commit Files Reviewed (3 files)
Previous Review Summary (commit 727bf37)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 727bf37)Status: No Issues Found | Recommendation: Merge lgtm — the Files Reviewed (3 files)
Reviewed by kimi-k3 · Input: 209.1K · Output: 10.5K · Cached: 413.2K Review guidance: REVIEW.md from base branch |
Permission rules are positional, so the global config merged after the
read-only guards and any catch-all shadowed them. Ask kept its edit deny
and simply routed around it through the shell.
Reapply the guard after the user layer for ask and plan: the catch-all
deny plus the read-only allowlist, the user's rules re-expanded onto safe
permissions by exact name, then the bash, MCP and mutating-tool ceilings.
Config rules can no longer widen bash, task, write, notebook_edit,
notebook_execute, agent_manager, repo_clone or interactive_terminal for
these modes, whatever pattern they use. `agent.<name>.permission` remains
the way to opt a single mode in.
A blanket guard deny stays an absolute ceiling, but one aimed at a single
target is only a default: naming that exact target still lifts it, so
plan keeps honoring `task: { general: "allow" }` while no wildcard
reaches it.
Guarding the agent is not enough on its own, since plan may delegate and
a subagent builds its ruleset from the same config, so KiloTask.inherited
carries the same set into delegated sessions.
Fixes #12053
727bf37 to
3f9b0b1
Compare
Issue
Fixes #12053
Context
The reporter had never edited a permission and had no catch-all rule. Their whole relevant config is
bash: { "*": "ask", "*bevy_app*": null, "ls *": "allow" }— the shape the settings UI writes.That one rule is enough. Permission rules are positional (
Permission.evaluateis afindLast), andpatchAgentsmerged the global config after the read-only guards, so"*": "ask"shadowed Ask's entirereadOnlyBashallowlist. Ask never lost itseditdeny — it didn't need it. Every shell command became a prompt instead of a refusal, the user approved one, and Ask edited the codebase.Their config verbatim through the real
Agent.Service:mainecho 'pwned' > src/main.rsaskdenyaskpython - <<'PY' … PYaskdenyaskrm -rf srcaskdenyaskls -laallowallowallowOn
main, Ask and Code are identical: "read-only mode" had degraded to "mode that asks first."Two automatic paths make this worse, which is why the fix goes past
bash: "Always allow" persists<command> *rules into the global config (tool/shell.ts→config.updateGlobal), so approvingpython - *once in Code silently grants it to Ask forever; and "Allow everything" writes a bare{"*": {"*": "allow"}}, which reaches every tool.Implementation
baseline()reapplies the guard after theuserlayer for ask and plan: the catch-all deny plus the read-only allowlist, the user's rules re-expanded onto safe permissions by exact name, then the bash, MCP and guarded-deny ceilings. Config can no longer widenbash,task,write,notebook_edit,notebook_execute,agent_manager,repo_cloneorinteractive_terminalfor these modes.Worth reviewer attention:
python - *is untrustworthy precisely because "Always allow" writes it automatically. The boundary is provenance: a top-level rule never widens a guarded tool, andagent.<name>.permissionis the only opt-in.read/grep/plan_exitand Ask cannot read a file. Every other test still passed in that state, hence the explicit "keep their safe tools" cases.*deny is an absolute ceiling; one aimed at a single target — plan'stask: { general: "deny" }— is a default that an exact-named user rule may lift, preserving upstream's per-subagent opt-in (test/agent/agent.test.ts). No wildcard qualifies.explore, which builds its ruleset from the same config.KiloTask.inheritednow derives its mutation set from the exportedguardedlist, so the two cannot drift.Behavior change: two existing tests moved from the global form to the per-agent form, because the global form is the vulnerability.
Out of scope, verified rather than assumed:
Permission.resolvepromotesask→allowunder a saved approval, so the MCPaskis not a runtime floor (needshardRuleset); andorchestratorhas the same bug class, tracked separately as it is deprecated.Screenshots / Video
Ask
Before
Plan
After
How to Test
Manual/local verification
Executed by the agent against the real
Agent.Service(no mocks): the table above, from the issue's verbatim config; plus Plan →exploreunder a catch-all, whereagent_manager,repo_cloneandwritewentallowbefore anddenyafter. Code mode's ruleset is identical before and after.331 pass / 0 fail across
test/agent/agent.test.ts,agent-permission-overrides,ask-agent-permissions,plan-mode-subagent-bypass,permission-task,permission/next,tool/task,agent-skill-permissions,agent-routing.bun turbo typecheckclean (29/29).check-opencode-annotations.ts --worktree: no shared upstream files touched.Reviewer test steps
permissionblock into~/.config/kilo/kilo.jsonc, with noagent.*overrides.mainyou get a prompt and it succeeds; here it is denied outright.ls -la.agent.ask.permission.bash: { "cargo search *": "allow" }and confirm that one command works again and Plan does not inherit it.Blocked checks and substitute verification
session-export/e2efails in CI, but also fails onmain(run 31814004052) — pre-existing.session-prompt-permission-refreshfails locally both here and withmain'sagent/index.tsrestored, including standalone — pre-existing.Checklist
Get in Touch