Files
my-pi/pi-permission-system/docs/plans/archive/0096-broaden-subagent-env-hint-keys.md
T

15 KiB

issue, issue_title
issue issue_title
96 Subagent permission forwarding broken for all major pi-subagent extensions

Broaden subagent env hint keys for major pi-subagent extensions

Problem Statement

Permission forwarding relies on SUBAGENT_ENV_HINT_KEYS to detect whether the current process is running as a subagent. Those three keys (PI_IS_SUBAGENT, PI_SUBAGENT_SESSION_ID, PI_AGENT_ROUTER_SUBAGENT) are not set by any of the three major pi-subagent extensions. As a result, isSubagentExecutionContext() returns false in child sessions spawned by nicobailon/pi-subagents or HazAT/pi-interactive-subagents, and resolvePermissionForwardingTargetSessionId() returns null because PI_AGENT_ROUTER_PARENT_SESSION_ID is also never set by those extensions. Any ask-state permission in such a headless child session silently denies instead of forwarding the dialog to the parent.

The tintinweb extension runs subagents fully in-process — no env vars are ever set, so detection there cannot rely on env vars at all. That case is deferred to #29 (event bus RPC).

Goals

  • Broaden SUBAGENT_ENV_HINT_KEYS to include the env vars that nicobailon/pi-subagents and HazAT/pi-interactive-subagents actually set in child processes.
  • Add a SUBAGENT_PARENT_SESSION_ENV_CANDIDATES list covering known parent-session env vars so parent-session resolution succeeds for each extension where that information is available.
  • Emit a structured debug/review log entry when parent-session resolution fails so users get an actionable message instead of a silent denial.
  • Add tests covering detection and parent-session resolution for each extension's env var pattern.
  • Document which extensions are now covered and which remain deferred (tintinweb in-process case → #29).

Non-Goals

  • Fixing the tintinweb in-process subagent case (no child process → no env vars; tracked in #29).
  • Proposing or enforcing a shared upstream convention across extensions (tracked in #98).
  • Changing the file-based forwarding protocol or polling logic.
  • Modifying how yolo-mode short-circuits the ask path.

Background

Permission surfaces involved

This issue touches the forwarding path that sits above all permission surfaces: when a permission resolves to ask in a headless subagent context the extension must forward the dialog to the parent session rather than blocking or silently denying. No surface-level rule evaluation changes — only the subagent detection and parent-session resolution steps.

Relevant modules

src/permission-forwarding.ts
Declares SUBAGENT_ENV_HINT_KEYS, SUBAGENT_PARENT_SESSION_ENV_KEY, and resolvePermissionForwardingTargetSessionId(). These are the primary targets of this fix.
src/subagent-context.ts
isSubagentExecutionContext() iterates SUBAGENT_ENV_HINT_KEYS and falls back to session-dir path comparison. The path-based fallback works only when the session directory happens to be nested under subagentSessionsDir, which is path-layout-dependent and fragile for external extensions.
src/forwarded-permissions/polling.ts
waitForForwardedPermissionApproval() calls both isSubagentExecutionContext() and resolvePermissionForwardingTargetSessionId(). When either returns a falsy result the function logs a forwarding error and returns { approved: false, state: "denied" } — the silent denial.
tests/subagent-context.test.ts
Existing tests cover the three original SUBAGENT_ENV_HINT_KEYS and the session-dir path fallback. New tests for the additional keys go in the same file.

Extension env var inventory

Extension Child-process env vars Parent-session env var
nicobailon/pi-subagents PI_SUBAGENT_CHILD, PI_SUBAGENT_RUN_ID, PI_SUBAGENT_CHILD_AGENT, PI_SUBAGENT_DEPTH none set (no mechanism today)
tintinweb/pi-subagents none (in-process, deferred) n/a
HazAT/pi-interactive-subagents PI_SUBAGENT_NAME, PI_SUBAGENT_ID, PI_SUBAGENT_SESSION, PI_SUBAGENT_ACTIVITY_FILE none set (no mechanism today)

Neither extension currently sets a parent-session env var. Parent-session resolution for these extensions will fail at the SUBAGENT_PARENT_SESSION_ENV_CANDIDATES lookup. The fix improves the error log to surface this explicitly so users can track progress against #98.

Design Overview

1. Broaden env hint keys

Add the known child-indicator vars from each extension to SUBAGENT_ENV_HINT_KEYS in src/permission-forwarding.ts:

export const SUBAGENT_ENV_HINT_KEYS = [
  // pi-agent-router (original)
  "PI_IS_SUBAGENT",
  "PI_SUBAGENT_SESSION_ID",
  "PI_AGENT_ROUTER_SUBAGENT",
  // nicobailon/pi-subagents
  "PI_SUBAGENT_CHILD",
  "PI_SUBAGENT_RUN_ID",
  "PI_SUBAGENT_CHILD_AGENT",
  "PI_SUBAGENT_DEPTH",
  // HazAT/pi-interactive-subagents
  "PI_SUBAGENT_NAME",
  "PI_SUBAGENT_ID",
  "PI_SUBAGENT_SESSION",
  "PI_SUBAGENT_ACTIVITY_FILE",
] as const;

This makes isSubagentExecutionContext() return true for child processes from both extensions without changing the function's signature or logic.

2. Add parent-session env var candidates

The existing SUBAGENT_PARENT_SESSION_ENV_KEY is a single string ("PI_AGENT_ROUTER_PARENT_SESSION_ID"). Replace it with an ordered array of candidates so resolvePermissionForwardingTargetSessionId() can try each in turn:

/** Ordered list of env var names to check for the parent session ID. */
export const SUBAGENT_PARENT_SESSION_ENV_CANDIDATES: readonly string[] = [
  // pi-agent-router (original)
  "PI_AGENT_ROUTER_PARENT_SESSION_ID",
] as const;

Neither nicobailon nor HazAT currently sets a parent-session env var, so only the original key appears now. The array design lets a future step (or a #98 adoption) add more candidates without changing call sites.

resolvePermissionForwardingTargetSessionId() is updated to iterate the candidates:

export function resolvePermissionForwardingTargetSessionId(options: {
  hasUI: boolean;
  isSubagent: boolean;
  currentSessionId?: string | null;
  env?: NodeJS.ProcessEnv;
}): string | null {
  if (options.hasUI) {
    return normalizePermissionForwardingSessionId(options.currentSessionId);
  }
  if (!options.isSubagent) {
    return null;
  }
  for (const key of SUBAGENT_PARENT_SESSION_ENV_CANDIDATES) {
    const resolved = normalizePermissionForwardingSessionId(options.env?.[key]);
    if (resolved) return resolved;
  }
  return null;
}

SUBAGENT_PARENT_SESSION_ENV_KEY is kept as a deprecated re-export alias for one release so external callers are not broken:

/** @deprecated Use SUBAGENT_PARENT_SESSION_ENV_CANDIDATES */
export const SUBAGENT_PARENT_SESSION_ENV_KEY =
  SUBAGENT_PARENT_SESSION_ENV_CANDIDATES[0];

3. Improve the failure log message

In waitForForwardedPermissionApproval() in src/forwarded-permissions/polling.ts, the existing error message names only PI_AGENT_ROUTER_PARENT_SESSION_ID. Update it to list all candidates and mention the open tracking issue:

logPermissionForwardingError(
  deps.logger,
  `Permission forwarding target session could not be resolved. ` +
    `Checked env vars: ${SUBAGENT_PARENT_SESSION_ENV_CANDIDATES.join(", ")}. ` +
    `If you are using nicobailon/pi-subagents or HazAT/pi-interactive-subagents, ` +
    `parent-session forwarding is not yet supported for those extensions (see issue #98).`,
);

Edge cases

  • A PI_SUBAGENT_DEPTH=0 value is a non-empty string and will correctly trigger detection; depth-0 is still a subagent context.
  • PI_SUBAGENT_ACTIVITY_FILE is a file path string; any non-empty value marks the child as a subagent.
  • The session-dir path-based fallback in isSubagentExecutionContext() is unchanged and remains as a secondary guard.
  • Adding keys increases the surface area of "what counts as a subagent"; this is intentional and aligned with least-privilege (forward/ask rather than silently allow in a falsely-non-subagent context).

Merge precedence impact

No config-level policy change. The forwarding path sits above rule evaluation; this fix only changes when the extension decides to attempt forwarding rather than what decision it makes.

Module-Level Changes

src/permission-forwarding.ts
  • Replace SUBAGENT_ENV_HINT_KEYS tuple with the expanded list.
  • Add SUBAGENT_PARENT_SESSION_ENV_CANDIDATES array.
  • Keep SUBAGENT_PARENT_SESSION_ENV_KEY as a deprecated alias.
  • Update resolvePermissionForwardingTargetSessionId() to iterate candidates.
src/forwarded-permissions/polling.ts
  • Update the "could not resolve" error log message to name all candidates and reference #98.
tests/subagent-context.test.ts
  • Add detection tests for each new env hint key (nicobailon group and HazAT group).
  • Add a test asserting SUBAGENT_ENV_HINT_KEYS contains every key from both groups.
tests/permission-forwarding.test.ts (new file)
  • Test resolvePermissionForwardingTargetSessionId():
  • hasUI=true returns current session ID.
  • isSubagent=false returns null.
  • isSubagent=true, none of the candidates set → returns null.
  • isSubagent=true, first candidate (PI_AGENT_ROUTER_PARENT_SESSION_ID) set → returns its value.
  • isSubagent=true, first candidate absent but a hypothetical second set → returns second's value (future-proofing).
  • Test SUBAGENT_PARENT_SESSION_ENV_KEY is still exported and equals the first candidate.
docs/architecture/target-architecture.md
  • Update the subagent-detection section to name the three extensions and their env var sets.
  • Note the tintinweb in-process case as deferred to #29.

TDD Order

Step 1 — tests: new env hint key detection

File: tests/subagent-context.test.ts

Add test cases that isSubagentExecutionContext() returns true for each newly added key: PI_SUBAGENT_CHILD, PI_SUBAGENT_RUN_ID, PI_SUBAGENT_CHILD_AGENT, PI_SUBAGENT_DEPTH, PI_SUBAGENT_NAME, PI_SUBAGENT_ID, PI_SUBAGENT_SESSION, PI_SUBAGENT_ACTIVITY_FILE.

Add a "covers all declared SUBAGENT_ENV_HINT_KEYS" guard test that reads the exported array and asserts each key has an individual test.

These tests are red until Step 2.

Commit: test: cover nicobailon + HazAT subagent env hint keys (#96)

Step 2 — feat: broaden SUBAGENT_ENV_HINT_KEYS

File: src/permission-forwarding.ts

Expand SUBAGENT_ENV_HINT_KEYS with the eight new keys. No other logic changes.

Step 1 tests turn green.

Commit: feat: broaden SUBAGENT_ENV_HINT_KEYS for nicobailon + HazAT extensions (#96)

Step 3 — tests: SUBAGENT_PARENT_SESSION_ENV_CANDIDATES and updated resolver

File: tests/permission-forwarding.test.ts (new)

Cover:

  • SUBAGENT_PARENT_SESSION_ENV_CANDIDATES is an array containing "PI_AGENT_ROUTER_PARENT_SESSION_ID".
  • SUBAGENT_PARENT_SESSION_ENV_KEY equals SUBAGENT_PARENT_SESSION_ENV_CANDIDATES[0] (deprecated alias still present).
  • resolvePermissionForwardingTargetSessionId with hasUI=true, isSubagent=false, isSubagent=true+none set, isSubagent=true+first candidate set.

These tests are red until Step 4.

Commit: test: cover SUBAGENT_PARENT_SESSION_ENV_CANDIDATES and resolver (#96)

Step 4 — feat: add SUBAGENT_PARENT_SESSION_ENV_CANDIDATES, iterate in resolver

File: src/permission-forwarding.ts

  • Add SUBAGENT_PARENT_SESSION_ENV_CANDIDATES.
  • Keep SUBAGENT_PARENT_SESSION_ENV_KEY as a deprecated alias.
  • Update resolvePermissionForwardingTargetSessionId() to iterate the candidates array.

File: src/forwarded-permissions/polling.ts

  • Update the forwarding-failure log message to list all candidates and reference #98.

Step 3 tests turn green.

Commit: feat: add SUBAGENT_PARENT_SESSION_ENV_CANDIDATES, iterate in resolver (#96)

Step 5 — docs: update target architecture

File: docs/architecture/target-architecture.md

Document the three extensions, their env vars, and the tintinweb in-process deferral.

Commit: docs: update target-architecture subagent detection for #96

Risks and Mitigations

Risk Mitigation
New env hint keys over-match — a non-subagent process happens to have one of these vars set and is wrongly treated as headless PI_SUBAGENT_DEPTH=0 and similar are specific to these extensions; over-match risk is low. The consequence of a false positive is that the extension tries to forward rather than silently allow, which still prompts the user — not a silent bypass.
Could this silently weaken a permission? No. Broadening detection makes more sessions attempt forwarding rather than silently denying. The only failure mode is a forwarding attempt that cannot resolve a parent session, which already emits a denial — not an allow.
SUBAGENT_PARENT_SESSION_ENV_KEY removal breaks external callers Kept as a deprecated alias for at least one release.
Parent-session resolution still fails for nicobailon and HazAT (no parent-session env var) The improved error message makes this explicit and points to #98. The silent-denial behavior is unchanged for this specific sub-case until #98 lands.
PI_SUBAGENT_SESSION from HazAT is the child's session ID, not the parent's It is added to SUBAGENT_ENV_HINT_KEYS (detection only), not to SUBAGENT_PARENT_SESSION_ENV_CANDIDATES (resolution). No confusion possible.

Open Questions

  • #98 adoption: Once nicobailon and HazAT adopt a shared parent-session env var, add it to SUBAGENT_PARENT_SESSION_ENV_CANDIDATES. Plan is intentionally array-shaped to make that a one-line change.
  • PI_SUBAGENT_DEPTH=0: Depth-0 could mean "top-level orchestrator in a subagent run". If that case should be excluded from subagent detection, a depth check could be added — deferred until there is a concrete user report.
  • tintinweb in-process: No env var approach can fix this; deferred to #29 (event bus RPC).