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_KEYSto include the env vars that nicobailon/pi-subagents and HazAT/pi-interactive-subagents actually set in child processes. - Add a
SUBAGENT_PARENT_SESSION_ENV_CANDIDATESlist 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-modeshort-circuits theaskpath.
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, andresolvePermissionForwardingTargetSessionId(). These are the primary targets of this fix. src/subagent-context.tsisSubagentExecutionContext()iteratesSUBAGENT_ENV_HINT_KEYSand falls back to session-dir path comparison. The path-based fallback works only when the session directory happens to be nested undersubagentSessionsDir, which is path-layout-dependent and fragile for external extensions.src/forwarded-permissions/polling.tswaitForForwardedPermissionApproval()calls bothisSubagentExecutionContext()andresolvePermissionForwardingTargetSessionId(). 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_KEYSand 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=0value is a non-empty string and will correctly trigger detection; depth-0 is still a subagent context. PI_SUBAGENT_ACTIVITY_FILEis 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_KEYStuple with the expanded list.
- Replace
- Add
SUBAGENT_PARENT_SESSION_ENV_CANDIDATESarray.
- Add
- Keep
SUBAGENT_PARENT_SESSION_ENV_KEYas a deprecated alias.
- Keep
- Update
resolvePermissionForwardingTargetSessionId()to iterate candidates.
- Update
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_KEYScontains every key from both groups.
- Add a test asserting
tests/permission-forwarding.test.ts(new file)- Test
resolvePermissionForwardingTargetSessionId():
- Test
- 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_KEYis 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_CANDIDATESis an array containing"PI_AGENT_ROUTER_PARENT_SESSION_ID".SUBAGENT_PARENT_SESSION_ENV_KEYequalsSUBAGENT_PARENT_SESSION_ENV_CANDIDATES[0](deprecated alias still present).resolvePermissionForwardingTargetSessionIdwith 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_KEYas 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).