fix(tool): deny edit to subagents dispatched by an edit-restricted caller#1132
fix(tool): deny edit to subagents dispatched by an edit-restricted caller#1132Astro-Han wants to merge 1 commit into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR enforces caller agent permissions on dispatched subagents. When a subtask invokes a subagent, the dispatcher now captures the real caller agent, evaluates whether that caller is denied the ChangesSubagent edit permission inheritance from caller
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request addresses a privilege escalation vulnerability (#26597) by forwarding an edit-restricted parent agent's edit deny rules into the subagent session, appending them to the end of the permissions list to ensure they take precedence. Unit tests have been added to verify this behavior. The review feedback points out a critical security concern where catching all errors when fetching the parent agent and falling back to undefined creates a fail-open risk; it is recommended to let these failures propagate to ensure a fail-closed security boundary.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| // session. All PawWork edit-class tools (edit/write/apply_patch) share the "edit" | ||
| // permission key, so this one filter covers them. ctx.agent names the dispatching | ||
| // (parent) agent; sessions don't store their agent, so resolve it here. | ||
| const parentAgent = yield* agent.get(ctx.agent).pipe(Effect.catchCause(() => Effect.succeed(undefined))) |
There was a problem hiding this comment.
Silently swallowing all errors/defects via Effect.catchCause and falling back to undefined is a fail-open security risk. If fetching the parent agent fails due to a transient system error or database issue, the subagent session will be created without the parent agent's edit restrictions, potentially leading to privilege escalation.
Since agent.get returns undefined at runtime if the agent is not found (rather than failing), we do not need to catch errors for the "agent not found" case. We should let any actual failures propagate (fail-closed) to ensure security boundaries are enforced.
const parentAgent = yield* agent.get(ctx.agent)e04c0fb to
2857711
Compare
…ller
An agent's file-edit restriction lives on its agent ruleset, but a dispatched
subagent could still edit — a privilege escalation. The task tool builds the
subagent's tool set, and SessionPrompt.prompt rebuilds the child
session.permission from that tools map, so the tools map is the layer that
actually binds the subagent (session.permission set at sessions.create is
overwritten before the child runs).
When the caller (the agent dispatching the task) can't edit, disable the edit
tool for the subagent in that tools map. The caller is resolved as
ctx.extra.callerAgent ?? ctx.agent: on a normal LLM dispatch ctx.agent is the
caller, but on a subtask command SessionPrompt.handleSubtask runs the agent
tool as the child, so it now threads the real caller through ctx.extra
(PawWork sessions don't store their agent). Effective edit is evaluated over
the caller's agent ruleset *and* this session's permission via
Permission.evaluate, so both an agent-level deny and a per-prompt/session deny
are honored, and a wildcard restricted agent (permission: { "*": "deny" }) is
caught as well as an explicit { edit: "deny" }. All edit-class tools
(edit/write/apply_patch) ask under the single "edit" key, so one check covers
them.
2857711 to
7d7a595
Compare
What
An agent's file-edit restriction lives on its agent ruleset, but a dispatched subagent could still edit — a privilege escalation. The task tool builds the subagent's tool set, and
SessionPrompt.promptrebuilds the childsession.permissionfrom that tools map, so the tools map is the layer that actually binds the subagent (thesession.permissionset atsessions.createis overwritten before the child runs).When the caller (the agent dispatching the task) can't edit, this disables the
edittool for the subagent in that tools map.Why
Semantic port of anomalyco/opencode#26597 (+#27201), adapted to PawWork's permission model.
Key adaptations (and the review findings fixed across rounds)
sessions.create. An earlier revision set the deny on the created session;SessionPrompt.promptrebuildssession.permissionfrominput.tools, so that was overwritten in the real (non-stubbed) prompt path. The fix injectsedit: falseinto the same tools map that already carries theagent/enter-worktree/todowritenested-denies — the proven enforcement layer. (Codex P1)Permission.evaluate, not an exact-key filter. Evaluation folds in wildcard"*": "deny"rules (the common read-only-agent shape), not just an explicit{ edit: "deny" }. (Codex P1)Permission.evaluate("edit", "*", callerAgent.permission, session.permission), so a per-prompt/sessioneditdeny binds the subagent too — not only an agent-definition deny. (Codex round-2 P1)ctx.agentis the caller, butSessionPrompt.handleSubtaskruns the agent tool as the child, soctx.agentthere is the subtask's own agent.handleSubtasknow threads the real caller throughctx.extra.callerAgent, and the tool resolvesctx.extra.callerAgent ?? ctx.agent(PawWork sessions don't store their agent; upstream readsparent.agent). (Codex round-2 P1)"edit"check covers all edit-class tools —edit,write,apply_patchall ask under the single"edit"key.Boundary
packages/opencode/src/tool/agent.ts— resolve the caller, detect its edit-deny (agent + session ruleset), injectedit: falseinto the subagent tools mappackages/opencode/src/session/prompt.ts—handleSubtaskpassescallerAgentso the subtask path honors the dispatcher's restriction (boundary relaxation authorized for a fully-correct fix)packages/opencode/test/tool/agent.test.ts— five regression testsVerification
bun test test/tool/agent.test.ts— 21 pass / 1 skip. Tests assert the prompt input's tools map (the layer the real prompt rebuilds from):editis disabled when the caller denies edit explicitly, via wildcard, or via session permission, and when the subtask caller (ctx.extra.callerAgent) denies edit even thoughctx.agentcould; left untouched when the caller (build) can edit. Red→green confirmed (all four deny tests fail without the injection; the allow test stays green).bun test(full opencode suite) — 3629 pass / 0 fail.bun run typecheck— clean.Scope / severity
Privilege escalation, but only reachable via a non-default config: a primary agent that denies
editwhile still allowingtask. Default agents don't hit it. Labeled P2 on that basis — happy to bump.Summary by CodeRabbit
Release Notes
Bug Fixes
Tests