Skip to content

fix(subagent): children inherit the parent's permission policy and thinking level - #172

Closed
jasonkneen wants to merge 1 commit into
stepfun-ai:mainfrom
jasonkneen:fix/harness-subagent-inherit-settings
Closed

jasonkneen wants to merge 1 commit into
stepfun-ai:mainfrom
jasonkneen:fix/harness-subagent-inherit-settings

Conversation

@jasonkneen

@jasonkneen jasonkneen commented Sep 23, 2026

Copy link
Copy Markdown

Problem

Invariant: a subagent child inherits model, provider, reasoning effort and permission policy unless something explicitly overrides them, and is never more permissive than the parent.

  • Thinking dropped (packages/coding-agent/src/features/subagent/rpc-adapter.ts:192): if (input.thinkingLevel && !input.agent.model) args.push("--thinking", ...). Any agent file that set model: lost the parent's thinking level, even when it declared none.
  • Permission policy not propagated (rpc-adapter.ts:170-181, buildSubagentChildEnv): the child only saw ...process.env, so it re-resolved its own policy from env and settings (step/permissions.ts:211-260). A preset the parent picked at runtime (/permissions, Shift+Tab cycle), CLI flags (--approval-mode, --non-interactive-approval, --tool-override), and trusted-project policy never reached it. A parent in Ask or Read Only could spawn a child that resolved to Bypass, for example through an inherited STEP_PERMISSION_PRESET=bypass / STEP_AUTOPILOT=1 or the defaulted fallback.

Fix

  • buildSubagentChildArgs (a new pure function pulled out of createSubagentRpcSession) now:
    • passes --thinking <parent> unless the model pattern declares its own level with a :<level> suffix. That suffix is the only way an agent file can declare thinking today, and an explicit --thinking would override it in the child. A model without reasoning support clamps the level (sdk.ts:274) and does not error.
    • passes the parent's policy as the existing CLI flags --approval-mode, --non-interactive-approval and --tool-override tool=mode. These flags outrank every STEP_* env var and the persisted preset in the child's resolver. Auto-resume has no flag, so buildSubagentChildEnv pins STEP_AUTO_RESUME=0|1 and clears any inherited STEP_AUTOPILOT.
  • resolveStepChildPermissionPolicy(state, parentHasUI) in step/permissions.ts works out what the child gets:
    • strict and confirm pass through unchanged. The rpc child's confirm dialogs are auto-cancelled (rpc-adapter.ts onUiRequest), so a child confirm blocks the call and never allows it.
    • A headless parent passes on the policy it actually enforces. When unattended approval is refused or unconfigured, auto drops to confirm/deny, the same downgrade StepPermissionController.effectiveState applies.
    • Defaulted parent (nothing selected a policy): the child keeps the parent's mode but always gets --non-interactive-approval deny. It is never handed an explicit unattended allow. For an interactive defaulted parent, the child runs auto inside rpc (whose UI channel is present), so it behaves like the parent and like today. This keeps subagent writes working on a fresh install. For a headless defaulted parent, the child gets confirm/deny.
  • The parent's live state reaches the subagent tool over the existing extension event bus (pi.events). The Step extension answers step:permission-state-request with permissions.getState(), and executeSubagent reads it when each child spawns, so a preset switched mid-session applies to the next child. With no Step extension loaded, no flags are sent, which matches the old behaviour.
  • Keep-alive lanes: a live child records the policy it was spawned with (permissionKey). If a reply arrives after the parent's policy has changed, the idle child is replaced and its transcript is resumed under the current policy, so it cannot keep running with a policy looser than the parent's.
  • Provider: when the model is inherited from ctx.model, it is already qualified as ${provider}/${id} (execute.ts). A new wiring test now covers this.

Test

New file: packages/coding-agent/test/subagent-inherit-settings.test.ts. The policy cases build the child's argv and env, then feed them through the child's own parseArgs and resolveInitialStepPermissionState. The inherited env is deliberately hostile (STEP_PERMISSION_PRESET=bypass, STEP_AUTOPILOT=1).

  • parent read-only → child strict/read-only
  • parent ask → child confirm/deny (write_file decides confirm, which gets auto-cancelled)
  • parent explicit bypass → child bypass; autopilot → autopilot
  • defaulted interactive parent → child auto + deny
  • headless defaulted parent, or explicit auto+deny → child confirm
  • tool overrides are forwarded
  • agent model without a thinking suffix → parent's --thinking; model: x:low → no --thinking; llama3:8b is not treated as a declaration
  • wiring: the subagent tool hands the runner the live policy, the qualified model and the thinking level, and a mid-session preset switch reaches the next child
  • keep-alive: same policy reuses the child; a tightened policy replaces it (fake rpc child, same pattern as subagent-child-env.test.ts)

Before the fix, 8 of 13 tests failed. Examples:

  • parent read-only -> child read-only: AssertionError: expected 'auto' to be 'strict'
  • parent ask -> ...: expected 'auto' to be 'confirm'
  • defaulted interactive parent ...: expected 'allow' to be 'deny'
  • agent file sets a model but no thinking ...: expected undefined to be 'high'

Risk / behaviour change

  • Children now use the parent's preset instead of re-resolving their own. A parent in Ask or Read Only therefore gets children that cannot write. That is the intended fix, but it is visible to users who relied on children silently running as Bypass.
  • Defaulted-parent choice: the child gets auto + deny, not the stricter confirm. confirm would block every subagent write on a fresh install. Switching to it is a one-line change in resolveStepChildPermissionPolicy.
  • When the policy changes, a keep-alive lane's next reply respawns the child and resumes its transcript from disk. onChildRespawn fires for this.
  • Not covered: workflow agents (features/workflow/agent-runner.ts) also call runStepSubagentProcess without a permission. They keep the old behaviour, with their own path ACL.

Merge note: interaction between #168 (session writer lock) and #172 (subagent inherit settings)

When both fix/harness-session-writer-lock and fix/harness-subagent-inherit-settings are merged, replacing an idle keep-alive subagent lane (on a parent permission change) resumes its transcript right after the old child is told to stop. The old child may still hold the session writer lock, so the replacement can fail with "session is already open". Whichever merges second should await the old child's exit before spawning the replacement.

https://claude.ai/code/session_01GUdnnHEaDThHUATSwXBpV9

…inking level

Invariant: a subagent child inherits model, provider, reasoning effort and
permission policy unless explicitly overridden, and is never more
permissive than the parent.

Cause: createSubagentRpcSession dropped --thinking whenever the agent file
set a model, and the permission policy reached the child only through the
inherited process.env. The child re-resolved its own policy from STEP_* env
and settings, so a preset the parent chose at runtime (/permissions, CLI
flags, trusted project) never reached it. That let an Ask or Read Only
parent spawn a Bypass child.

Fix: buildSubagentChildArgs passes the parent's thinking level unless the
model declares one with a :<level> suffix. It also passes the parent's
effective policy as --approval-mode, --non-interactive-approval and
--tool-override, which outrank env and persisted settings in the child;
STEP_AUTO_RESUME is pinned in the env. resolveStepChildPermissionPolicy
applies the parent's headless downgrade and never gives a defaulted parent's
child an explicit unattended allow (auto + deny). The subagent tool reads
the live state over the extension event bus when each child spawns. A
keep-alive child whose spawn-time policy no longer matches is replaced on
the next reply.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Critical keep-alive and policy-transition issues remain unresolved, along with cleanup and model-resolution concerns.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity

Open (2)
What changed in this PR

Updates Step subagents to inherit the parent’s model, thinking level, and permission policy.

Changes:

  • Propagates permission, thinking, model, and tool settings.
  • Adds live policy propagation and policy-aware keep-alive replacement.
  • Adds inheritance, wiring, and lifecycle tests.
File Summary
packages/​coding-agent/​test/​subagent-inherit-settings.test.ts Tests inheritance, wiring, and keep-alive behavior.
packages/​coding-agent/​src/​step/​permissions.ts Resolves child permission policies and state exchange.
packages/​coding-agent/​src/​features/​subagent/​rpc-adapter.ts Builds inherited child arguments and environment.
packages/​coding-agent/​src/​features/​subagent/​execute.ts Supplies live parent state to child runs.
packages/​coding-agent/​src/​features/​step.ts Registers permission-state event handling.
packages/​coding-agent/​src/​features/​step-subagent.ts Manages policy-aware child replacement.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +415 to +416
if (live.isTurnActive() || live.permissionKey === subagentPermissionKey(input.permission)) {
return live.runTurn(input);
// A live child keeps the policy it was spawned with. The parent's changed
// since (e.g. tightened via /permissions), so replace the idle child and
// resume its transcript under the current policy.
live.stop();
@ZouR-Ma ZouR-Ma closed this Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants