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

280 lines
15 KiB
Markdown

---
issue: 96
issue_title: "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`:
```typescript
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:
```typescript
/** 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:
```typescript
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:
```typescript
/** @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:
```typescript
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).