@gotgenes/pi-permission-system 5.1.2 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [5.2.0](https://github.com/gotgenes/pi-permission-system/compare/v5.1.2...v5.2.0) (2026-05-05)
9
+
10
+
11
+ ### Features
12
+
13
+ * add SUBAGENT_PARENT_SESSION_ENV_CANDIDATES, iterate in resolver ([#96](https://github.com/gotgenes/pi-permission-system/issues/96)) ([ac6831d](https://github.com/gotgenes/pi-permission-system/commit/ac6831d3418db0aee5d5ed4757d5833730a6e130))
14
+ * broaden SUBAGENT_ENV_HINT_KEYS for nicobailon + HazAT extensions ([#96](https://github.com/gotgenes/pi-permission-system/issues/96)) ([8adafdb](https://github.com/gotgenes/pi-permission-system/commit/8adafdb45af66cf99b000b3ad011bee5a2c90476))
15
+
16
+
17
+ ### Documentation
18
+
19
+ * plan broaden subagent env hint keys ([#96](https://github.com/gotgenes/pi-permission-system/issues/96)) ([9fa97b7](https://github.com/gotgenes/pi-permission-system/commit/9fa97b7385e5b9203a35c80d15f980ac4501f788))
20
+ * update target-architecture subagent detection for [#96](https://github.com/gotgenes/pi-permission-system/issues/96) ([64cce35](https://github.com/gotgenes/pi-permission-system/commit/64cce3569c09cede690858af41d2f35611a8705f))
21
+
8
22
  ## [5.1.2](https://github.com/gotgenes/pi-permission-system/compare/v5.1.1...v5.1.2) (2026-05-05)
9
23
 
10
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "5.1.2",
3
+ "version": "5.2.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "files": [
@@ -18,6 +18,7 @@ import {
18
18
  PERMISSION_FORWARDING_POLL_INTERVAL_MS,
19
19
  PERMISSION_FORWARDING_TIMEOUT_MS,
20
20
  resolvePermissionForwardingTargetSessionId,
21
+ SUBAGENT_PARENT_SESSION_ENV_CANDIDATES,
21
22
  } from "../permission-forwarding";
22
23
  import { isSubagentExecutionContext } from "../subagent-context";
23
24
 
@@ -110,7 +111,10 @@ export async function waitForForwardedPermissionApproval(
110
111
  if (!targetSessionId) {
111
112
  logPermissionForwardingError(
112
113
  deps.logger,
113
- "Permission forwarding target session could not be resolved from subagent runtime metadata (expected PI_AGENT_ROUTER_PARENT_SESSION_ID)",
114
+ `Permission forwarding target session could not be resolved. ` +
115
+ `Checked env vars: ${SUBAGENT_PARENT_SESSION_ENV_CANDIDATES.join(", ")}. ` +
116
+ `If you are using nicobailon/pi-subagents or HazAT/pi-interactive-subagents, ` +
117
+ `parent-session forwarding is not yet supported for those extensions (see issue #98).`,
114
118
  );
115
119
  return { approved: false, state: "denied" };
116
120
  }
@@ -5,12 +5,30 @@ import type { PermissionDecisionState } from "./permission-dialog";
5
5
  export const PERMISSION_FORWARDING_POLL_INTERVAL_MS = 250;
6
6
  export const PERMISSION_FORWARDING_TIMEOUT_MS = 10 * 60 * 1000;
7
7
  export const SUBAGENT_ENV_HINT_KEYS = [
8
+ // pi-agent-router (original)
8
9
  "PI_IS_SUBAGENT",
9
10
  "PI_SUBAGENT_SESSION_ID",
10
11
  "PI_AGENT_ROUTER_SUBAGENT",
12
+ // nicobailon/pi-subagents
13
+ "PI_SUBAGENT_CHILD",
14
+ "PI_SUBAGENT_RUN_ID",
15
+ "PI_SUBAGENT_CHILD_AGENT",
16
+ "PI_SUBAGENT_DEPTH",
17
+ // HazAT/pi-interactive-subagents
18
+ "PI_SUBAGENT_NAME",
19
+ "PI_SUBAGENT_ID",
20
+ "PI_SUBAGENT_SESSION",
21
+ "PI_SUBAGENT_ACTIVITY_FILE",
11
22
  ] as const;
23
+ /** Ordered list of env var names to check for the parent session ID. First match wins. */
24
+ export const SUBAGENT_PARENT_SESSION_ENV_CANDIDATES: readonly string[] = [
25
+ // pi-agent-router (original)
26
+ "PI_AGENT_ROUTER_PARENT_SESSION_ID",
27
+ ] as const;
28
+
29
+ /** @deprecated Use SUBAGENT_PARENT_SESSION_ENV_CANDIDATES */
12
30
  export const SUBAGENT_PARENT_SESSION_ENV_KEY =
13
- "PI_AGENT_ROUTER_PARENT_SESSION_ID";
31
+ SUBAGENT_PARENT_SESSION_ENV_CANDIDATES[0];
14
32
 
15
33
  const SESSION_FORWARDING_ROOT_DIRECTORY_NAME = "sessions";
16
34
  const SESSION_FORWARDING_REQUESTS_DIRECTORY_NAME = "requests";
@@ -106,9 +124,12 @@ export function resolvePermissionForwardingTargetSessionId(options: {
106
124
  return null;
107
125
  }
108
126
 
109
- return normalizePermissionForwardingSessionId(
110
- options.env?.[SUBAGENT_PARENT_SESSION_ENV_KEY],
111
- );
127
+ const env = options.env ?? process.env;
128
+ for (const key of SUBAGENT_PARENT_SESSION_ENV_CANDIDATES) {
129
+ const resolved = normalizePermissionForwardingSessionId(env[key]);
130
+ if (resolved) return resolved;
131
+ }
132
+ return null;
112
133
  }
113
134
 
114
135
  export function isForwardedPermissionRequestForSession(
@@ -0,0 +1,143 @@
1
+ import { afterEach, describe, expect, test, vi } from "vitest";
2
+ import {
3
+ resolvePermissionForwardingTargetSessionId,
4
+ SUBAGENT_PARENT_SESSION_ENV_CANDIDATES,
5
+ SUBAGENT_PARENT_SESSION_ENV_KEY,
6
+ } from "../src/permission-forwarding";
7
+
8
+ afterEach(() => {
9
+ vi.unstubAllEnvs();
10
+ });
11
+
12
+ describe("SUBAGENT_PARENT_SESSION_ENV_CANDIDATES", () => {
13
+ test("is an array containing PI_AGENT_ROUTER_PARENT_SESSION_ID", () => {
14
+ expect(Array.isArray(SUBAGENT_PARENT_SESSION_ENV_CANDIDATES)).toBe(true);
15
+ expect(SUBAGENT_PARENT_SESSION_ENV_CANDIDATES).toContain(
16
+ "PI_AGENT_ROUTER_PARENT_SESSION_ID",
17
+ );
18
+ });
19
+
20
+ test("deprecated SUBAGENT_PARENT_SESSION_ENV_KEY equals the first candidate", () => {
21
+ expect(SUBAGENT_PARENT_SESSION_ENV_KEY).toBe(
22
+ SUBAGENT_PARENT_SESSION_ENV_CANDIDATES[0],
23
+ );
24
+ });
25
+ });
26
+
27
+ describe("resolvePermissionForwardingTargetSessionId", () => {
28
+ test("hasUI=true returns the current session ID (UI host owns forwarding)", () => {
29
+ expect(
30
+ resolvePermissionForwardingTargetSessionId({
31
+ hasUI: true,
32
+ isSubagent: false,
33
+ currentSessionId: "parent-session-abc",
34
+ env: {},
35
+ }),
36
+ ).toBe("parent-session-abc");
37
+ });
38
+
39
+ test("hasUI=true with isSubagent=true still returns current session ID", () => {
40
+ expect(
41
+ resolvePermissionForwardingTargetSessionId({
42
+ hasUI: true,
43
+ isSubagent: true,
44
+ currentSessionId: "session-xyz",
45
+ env: { PI_AGENT_ROUTER_PARENT_SESSION_ID: "other" },
46
+ }),
47
+ ).toBe("session-xyz");
48
+ });
49
+
50
+ test("hasUI=false, isSubagent=false returns null", () => {
51
+ expect(
52
+ resolvePermissionForwardingTargetSessionId({
53
+ hasUI: false,
54
+ isSubagent: false,
55
+ currentSessionId: "session-xyz",
56
+ env: { PI_AGENT_ROUTER_PARENT_SESSION_ID: "parent-session-abc" },
57
+ }),
58
+ ).toBeNull();
59
+ });
60
+
61
+ test("isSubagent=true, no candidates set returns null", () => {
62
+ expect(
63
+ resolvePermissionForwardingTargetSessionId({
64
+ hasUI: false,
65
+ isSubagent: true,
66
+ currentSessionId: "session-xyz",
67
+ env: {},
68
+ }),
69
+ ).toBeNull();
70
+ });
71
+
72
+ test("isSubagent=true, PI_AGENT_ROUTER_PARENT_SESSION_ID set returns its value", () => {
73
+ expect(
74
+ resolvePermissionForwardingTargetSessionId({
75
+ hasUI: false,
76
+ isSubagent: true,
77
+ currentSessionId: "session-xyz",
78
+ env: { PI_AGENT_ROUTER_PARENT_SESSION_ID: "parent-session-abc" },
79
+ }),
80
+ ).toBe("parent-session-abc");
81
+ });
82
+
83
+ test("isSubagent=true, first candidate absent but second set returns second value", () => {
84
+ // Inject a second candidate at test-time to validate the iteration logic
85
+ // without waiting for a real extension to adopt the convention.
86
+ const originalCandidates = [...SUBAGENT_PARENT_SESSION_ENV_CANDIDATES];
87
+ // Mutate the array via index-assignment through a cast so we can test
88
+ // multi-candidate iteration without changing the exported constant type.
89
+ // This is test-only; production code never mutates the array.
90
+ (SUBAGENT_PARENT_SESSION_ENV_CANDIDATES as unknown as string[]).push(
91
+ "PI_SUBAGENT_PARENT_SESSION_ID_TEST_ONLY",
92
+ );
93
+
94
+ try {
95
+ expect(
96
+ resolvePermissionForwardingTargetSessionId({
97
+ hasUI: false,
98
+ isSubagent: true,
99
+ currentSessionId: "session-xyz",
100
+ env: {
101
+ PI_SUBAGENT_PARENT_SESSION_ID_TEST_ONLY: "parent-from-second",
102
+ },
103
+ }),
104
+ ).toBe("parent-from-second");
105
+ } finally {
106
+ // Restore original array contents.
107
+ (SUBAGENT_PARENT_SESSION_ENV_CANDIDATES as unknown as string[]).length =
108
+ originalCandidates.length;
109
+ }
110
+ });
111
+
112
+ test("isSubagent=true, candidate value is empty string returns null", () => {
113
+ expect(
114
+ resolvePermissionForwardingTargetSessionId({
115
+ hasUI: false,
116
+ isSubagent: true,
117
+ currentSessionId: "session-xyz",
118
+ env: { PI_AGENT_ROUTER_PARENT_SESSION_ID: "" },
119
+ }),
120
+ ).toBeNull();
121
+ });
122
+
123
+ test("isSubagent=true, candidate value is 'unknown' returns null", () => {
124
+ expect(
125
+ resolvePermissionForwardingTargetSessionId({
126
+ hasUI: false,
127
+ isSubagent: true,
128
+ currentSessionId: "session-xyz",
129
+ env: { PI_AGENT_ROUTER_PARENT_SESSION_ID: "unknown" },
130
+ }),
131
+ ).toBeNull();
132
+ });
133
+
134
+ test("env defaults to process.env when omitted", () => {
135
+ vi.stubEnv("PI_AGENT_ROUTER_PARENT_SESSION_ID", "env-session-abc");
136
+ expect(
137
+ resolvePermissionForwardingTargetSessionId({
138
+ hasUI: false,
139
+ isSubagent: true,
140
+ }),
141
+ ).toBe("env-session-abc");
142
+ });
143
+ });
@@ -61,11 +61,86 @@ describe("isSubagentExecutionContext — env hint detection", () => {
61
61
  ).toBe(true);
62
62
  });
63
63
 
64
- test("covers all three declared SUBAGENT_ENV_HINT_KEYS", () => {
64
+ // nicobailon/pi-subagents keys
65
+ test("returns true when PI_SUBAGENT_CHILD is set", () => {
66
+ vi.stubEnv("PI_SUBAGENT_CHILD", "1");
67
+ expect(
68
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
69
+ ).toBe(true);
70
+ });
71
+
72
+ test("returns true when PI_SUBAGENT_RUN_ID is set", () => {
73
+ vi.stubEnv("PI_SUBAGENT_RUN_ID", "run-abc");
74
+ expect(
75
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
76
+ ).toBe(true);
77
+ });
78
+
79
+ test("returns true when PI_SUBAGENT_CHILD_AGENT is set", () => {
80
+ vi.stubEnv("PI_SUBAGENT_CHILD_AGENT", "worker");
81
+ expect(
82
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
83
+ ).toBe(true);
84
+ });
85
+
86
+ test("returns true when PI_SUBAGENT_DEPTH is set", () => {
87
+ vi.stubEnv("PI_SUBAGENT_DEPTH", "1");
88
+ expect(
89
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
90
+ ).toBe(true);
91
+ });
92
+
93
+ test("returns true when PI_SUBAGENT_DEPTH is zero (depth-0 is still a subagent context)", () => {
94
+ vi.stubEnv("PI_SUBAGENT_DEPTH", "0");
95
+ expect(
96
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
97
+ ).toBe(true);
98
+ });
99
+
100
+ // HazAT/pi-interactive-subagents keys
101
+ test("returns true when PI_SUBAGENT_NAME is set", () => {
102
+ vi.stubEnv("PI_SUBAGENT_NAME", "my-agent");
103
+ expect(
104
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
105
+ ).toBe(true);
106
+ });
107
+
108
+ test("returns true when PI_SUBAGENT_ID is set", () => {
109
+ vi.stubEnv("PI_SUBAGENT_ID", "id-xyz");
110
+ expect(
111
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
112
+ ).toBe(true);
113
+ });
114
+
115
+ test("returns true when PI_SUBAGENT_SESSION is set", () => {
116
+ vi.stubEnv("PI_SUBAGENT_SESSION", "session-xyz");
117
+ expect(
118
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
119
+ ).toBe(true);
120
+ });
121
+
122
+ test("returns true when PI_SUBAGENT_ACTIVITY_FILE is set", () => {
123
+ vi.stubEnv("PI_SUBAGENT_ACTIVITY_FILE", "/tmp/activity.json");
124
+ expect(
125
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
126
+ ).toBe(true);
127
+ });
128
+
129
+ test("covers all declared SUBAGENT_ENV_HINT_KEYS", () => {
65
130
  // Verify the keys we test match what the module declares.
66
131
  expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_IS_SUBAGENT");
67
132
  expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_SESSION_ID");
68
133
  expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_AGENT_ROUTER_SUBAGENT");
134
+ // nicobailon/pi-subagents
135
+ expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_CHILD");
136
+ expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_RUN_ID");
137
+ expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_CHILD_AGENT");
138
+ expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_DEPTH");
139
+ // HazAT/pi-interactive-subagents
140
+ expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_NAME");
141
+ expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_ID");
142
+ expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_SESSION");
143
+ expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_ACTIVITY_FILE");
69
144
  });
70
145
 
71
146
  test("returns false when env hint value is empty string", () => {