Skip to content

Subagent parent deny inheritance over-constrains delegated agents with explicit permissions #26700

@nabilfreeman

Description

@nabilfreeman

Description

Regression introduced by the fix for #26514 / #26597.

PR #26597 correctly closed a Plan Mode security bypass: a read-only parent agent could spawn a subagent that retained edit/write permissions. However, the implementation now copies all parent agent deny rules into the child subagent session. For deny-by-default multi-agent configurations, this creates a new failure mode: a deliberately restricted primary agent can no longer delegate to a deliberately more-capable subagent, because the primary agent's deny rules are appended after the subagent's allow rules and override them.

This is not about restoring the Plan Mode bypass. The issue is that the new inheritance behavior treats every parent deny as a descendant-wide permission ceiling. That is valid for Plan Mode read-only restrictions, but it breaks controller/coordinator patterns where the primary agent is intentionally narrow and delegates execution to a subagent that has its own explicit allowlist.

Our configuration style is deny-by-default:

  • Start every agent with wildcard deny: "*": { "*": "deny" }
  • Add explicit per-tool allow rules for that agent
  • Give the primary/controller only task access to selected subagents
  • Give the execution subagent read/grep/glob/list/task/bash as needed
  • Give the execution subagent permission to delegate to a worker sub-sub-agent

This worked in 1.14.41. In 1.14.46, the executor subagent inherits the controller's deny rules, so it loses its own tools and cannot continue the delegation flow.

Relevant code path:

Observed effect:

  • read, grep, glob, list, task, todowrite, webfetch disappear from the subagent tool inventory
  • bash may remain visible, but every command is denied at runtime because inherited bash * -> deny wins over the subagent's bash * -> allow
  • The subagent reports that it only has bash, cannot use task, cannot read files, and cannot delegate further

Plugins

None required. This is reproducible with configuration only.

OpenCode version

Regresses in 1.14.46.

Known-good comparison: 1.14.41.

Steps to reproduce

Use a generic three-agent setup: controller -> executor -> worker. The names are arbitrary; the important part is that the primary/controller is intentionally restricted and the executor is explicitly whitelisted.

Minimal permission profile:

{
  "default_agent": "controller",
  "agent": {
    "controller": {
      "mode": "primary",
      "permission": {
        "*": { "*": "deny" },
        "read": { "*": "deny" },
        "bash": {
          "*": "deny",
          "git *": "allow",
          "npm *": "allow"
        },
        "task": {
          "*": "deny",
          "executor": "allow"
        },
        "edit": { "*": "deny" },
        "write": { "*": "deny" }
      },
      "prompt": "You are a restricted controller. Delegate implementation to executor."
    },
    "executor": {
      "mode": "subagent",
      "permission": {
        "*": { "*": "deny" },
        "read": { "*": "allow" },
        "grep": { "*": "allow" },
        "glob": { "*": "allow" },
        "list": { "*": "allow" },
        "bash": { "*": "allow" },
        "task": {
          "*": "deny",
          "worker": "allow"
        },
        "edit": { "*": "deny" },
        "write": { "*": "deny" }
      },
      "prompt": "You are an executor. Read files and delegate file edits to worker."
    },
    "worker": {
      "mode": "subagent",
      "permission": {
        "*": { "*": "deny" },
        "read": { "*": "allow" },
        "edit": { "*": "allow" },
        "write": { "*": "allow" },
        "bash": { "*": "allow" },
        "task": { "*": "deny" }
      },
      "prompt": "You are a terminal worker. Modify files when asked."
    }
  }
}

Then ask controller to delegate a simple task to executor, where executor must read a file and delegate a small edit to worker.

Expected behavior:

  1. controller can only spawn executor.
  2. executor receives its configured tools: read, grep, glob, list, bash, and task to worker.
  3. executor cannot edit directly, but can delegate to worker.
  4. worker receives its own configured edit/write tools.

Actual behavior in 1.14.46:

  1. executor inherits the controller's deny rules.
  2. The inherited * * -> deny, read * -> deny, bash * -> deny, and task * -> deny rules are evaluated after executor's own allows.
  3. Because last matching rule wins, executor's explicit allowlist is overridden.
  4. executor cannot read, cannot use task, and cannot run even allowlisted bash commands like git status or npm test.
  5. The delegated workflow stops before the worker can be spawned.

This is the effective rule conflict for executor after #26597:

executor permission: read * -> allow
inherited controller deny: read * -> deny
last match wins: read is denied

executor permission: task worker -> allow
inherited controller deny: task * -> deny
last match wins: task is denied

executor permission: bash * -> allow
inherited controller deny: bash * -> deny
last match wins: bash is denied

A possible fix direction would be to distinguish parent restrictions that are intended to bind descendants (for example Plan Mode read-only ceilings) from parent-local restrictions that only describe what the parent agent itself may do. Another option would be an explicit config flag for permission inheritance strategy. The current behavior makes any deny-by-default primary agent over-constrain its subagents.

Screenshot and/or share link

N/A. This is a config/runtime permission inheritance issue.

Operating System

Observed on macOS, but the regression is in the permission derivation code path and should be platform-independent.

Terminal

Observed through an OpenCode agent session. Terminal is not material to the bug.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions