Skip to content

fix(tool): deny edit to subagents dispatched by an edit-restricted caller#1132

Open
Astro-Han wants to merge 1 commit into
devfrom
claude/l4-26597-subagent-deny
Open

fix(tool): deny edit to subagents dispatched by an edit-restricted caller#1132
Astro-Han wants to merge 1 commit into
devfrom
claude/l4-26597-subagent-deny

Conversation

@Astro-Han
Copy link
Copy Markdown
Owner

@Astro-Han Astro-Han commented Jun 3, 2026

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.prompt rebuilds the child session.permission from that tools map, so the tools map is the layer that actually binds the subagent (the session.permission set at sessions.create is overwritten before the child runs).

When the caller (the agent dispatching the task) can't edit, this disables the edit tool 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)

  • Enforce via the tools map, not sessions.create. An earlier revision set the deny on the created session; SessionPrompt.prompt rebuilds session.permission from input.tools, so that was overwritten in the real (non-stubbed) prompt path. The fix injects edit: false into the same tools map that already carries the agent/enter-worktree/todowrite nested-denies — the proven enforcement layer. (Codex P1)
  • Detect via 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)
  • Fold in the caller's session permission, not just the agent ruleset. Effective edit is evaluated over Permission.evaluate("edit", "*", callerAgent.permission, session.permission), so a per-prompt/session edit deny binds the subagent too — not only an agent-definition deny. (Codex round-2 P1)
  • Resolve the real caller on the subtask path. On a normal LLM dispatch ctx.agent is the caller, but SessionPrompt.handleSubtask runs the agent tool as the child, so ctx.agent there is the subtask's own agent. handleSubtask now threads the real caller through ctx.extra.callerAgent, and the tool resolves ctx.extra.callerAgent ?? ctx.agent (PawWork sessions don't store their agent; upstream reads parent.agent). (Codex round-2 P1)
  • One "edit" check covers all edit-class toolsedit, write, apply_patch all ask under the single "edit" key.

Boundary

  • packages/opencode/src/tool/agent.ts — resolve the caller, detect its edit-deny (agent + session ruleset), inject edit: false into the subagent tools map
  • packages/opencode/src/session/prompt.tshandleSubtask passes callerAgent so 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 tests

Verification

  • 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): edit is disabled when the caller denies edit explicitly, via wildcard, or via session permission, and when the subtask caller (ctx.extra.callerAgent) denies edit even though ctx.agent could; 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 edit while still allowing task. Default agents don't hit it. Labeled P2 on that basis — happy to bump.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Edit permissions from a caller agent are now properly enforced when dispatching to subagents, restricting edit access in child agents when the caller's permissions deny it.
  • Tests

    • Added tests validating edit permission inheritance and enforcement in agent dispatch scenarios.

@Astro-Han Astro-Han added bug Something isn't working P2 Medium priority upstream Tracked upstream or vendor behavior harness Model harness, prompts, tool descriptions, and session mechanics labels Jun 3, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ae2cd2b5-2d10-47e3-9f9e-d4c1faa46f24

📥 Commits

Reviewing files that changed from the base of the PR and between 37de1a1 and 7d7a595.

📒 Files selected for processing (3)
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/tool/agent.ts
  • packages/opencode/test/tool/agent.test.ts

📝 Walkthrough

Walkthrough

This 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 edit permission, and restricts the subagent's tool access accordingly by conditionally injecting edit: false into the tool permission map.

Changes

Subagent edit permission inheritance from caller

Layer / File(s) Summary
Caller agent context propagation
packages/opencode/src/session/prompt.ts, packages/opencode/src/tool/agent.ts
Subtask execution in handleSubtask now passes callerAgent: lastUser.agent through dispatch extra context. Agent module imports Permission to enable permission evaluation during subagent setup.
Caller edit permission evaluation and subagent restriction
packages/opencode/src/tool/agent.ts
Dispatcher derives the effective caller agent (from ctx.extra.callerAgent or ctx.agent), evaluates whether the caller's edit permission is denied via Permission.evaluate("edit", "*", ...), and conditionally sets edit: false in the subagent's tool permission map when restriction applies.
Test coverage for permission inheritance
packages/opencode/test/tool/agent.test.ts
New test helper dispatchTools captures the tool permission map passed to SessionPrompt.prompt. Multiple it.live tests verify that subagent edit is disabled under all denial configurations (agent config, wildcard, session-level) and enabled when the caller can edit.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A rabbit hopped through agent trees,
And passed along permissions with ease,
If parent says "no edit," child agrees—
Permission inherited, logic to please!
Subtasks now know what they can be. 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main security fix: denying edit access to subagents when dispatched by an edit-restricted parent, which is the core change across all modified files.
Description check ✅ Passed The PR description comprehensively covers all required sections: What (privilege escalation fix), Why (semantic port), verification steps with concrete results, scope/severity, and adaptations explaining the design decisions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/l4-26597-subagent-deny

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested priority: P2 (includes non-doc, non-test paths outside the low-risk bucket).

P1/P0 are reserved for maintainer confirmation. Please relabel manually if this is a release blocker, security issue, data-loss risk, or updater/runtime failure.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/opencode/src/tool/agent.ts Outdated
// 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)))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

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)

@Astro-Han Astro-Han force-pushed the claude/l4-26597-subagent-deny branch from e04c0fb to 2857711 Compare June 3, 2026 10:22
@Astro-Han Astro-Han changed the title fix(tool): forward edit-restricted parent agent's deny into subagent sessions fix(tool): deny edit to subagents dispatched by an edit-restricted parent agent Jun 3, 2026
…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.
@Astro-Han Astro-Han force-pushed the claude/l4-26597-subagent-deny branch from 2857711 to 7d7a595 Compare June 3, 2026 10:46
@Astro-Han Astro-Han changed the title fix(tool): deny edit to subagents dispatched by an edit-restricted parent agent fix(tool): deny edit to subagents dispatched by an edit-restricted caller Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working harness Model harness, prompts, tool descriptions, and session mechanics P2 Medium priority upstream Tracked upstream or vendor behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant