@gotgenes/pi-permission-system 7.1.4 → 7.3.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 +26 -0
- package/package.json +2 -2
- package/src/active-agent.ts +1 -1
- package/src/bash-arity.ts +1 -0
- package/src/config-modal.ts +2 -0
- package/src/forwarded-permissions/io.ts +4 -2
- package/src/forwarded-permissions/polling.ts +22 -9
- package/src/forwarding-manager.ts +3 -1
- package/src/handlers/before-agent-start.ts +7 -6
- package/src/handlers/gates/bash-path-extractor.ts +3 -5
- package/src/handlers/gates/bash-path.ts +1 -1
- package/src/handlers/gates/runner.ts +3 -0
- package/src/handlers/lifecycle.ts +9 -8
- package/src/handlers/permission-gate-handler.ts +12 -7
- package/src/index.ts +19 -1
- package/src/logging.ts +3 -0
- package/src/node-modules-discovery.ts +1 -1
- package/src/normalize.ts +1 -0
- package/src/permission-event-rpc.ts +2 -0
- package/src/permission-forwarding.ts +15 -0
- package/src/permission-manager.ts +7 -6
- package/src/permission-merge.ts +4 -2
- package/src/permission-prompter.ts +7 -0
- package/src/permission-prompts.ts +1 -1
- package/src/policy-loader.ts +5 -5
- package/src/service.ts +37 -1
- package/src/skill-prompt-sanitizer.ts +3 -3
- package/src/subagent-context.ts +14 -1
- package/src/subagent-registry.ts +60 -0
- package/src/tool-registry.ts +1 -1
- package/src/yolo-mode.ts +2 -1
- package/test/config-modal.test.ts +6 -8
- package/test/forwarding-manager.test.ts +1 -0
- package/test/handlers/before-agent-start.test.ts +1 -1
- package/test/handlers/external-directory-integration.test.ts +1 -1
- package/test/handlers/gates/skill-read.test.ts +8 -10
- package/test/handlers/gates/tool.test.ts +1 -1
- package/test/handlers/input-events.test.ts +1 -1
- package/test/handlers/input.test.ts +1 -1
- package/test/handlers/tool-call-events.test.ts +1 -1
- package/test/handlers/tool-call.test.ts +1 -1
- package/test/permission-event-rpc.test.ts +1 -0
- package/test/permission-events.test.ts +2 -0
- package/test/permission-forwarding.test.ts +98 -0
- package/test/permission-manager-unified.test.ts +4 -2
- package/test/permission-session.test.ts +2 -2
- package/test/permission-system.test.ts +8 -8
- package/test/service.test.ts +100 -6
- package/test/subagent-context.test.ts +65 -0
- package/test/subagent-registry.test.ts +94 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,32 @@ 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
|
+
## [7.3.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v7.2.0...pi-permission-system-v7.3.0) (2026-05-25)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** add SubagentSessionRegistry class ([a0ef16b](https://github.com/gotgenes/pi-packages/commit/a0ef16b8302f95b30cc11cb121441dbd164c276c))
|
|
14
|
+
* **pi-permission-system:** detect in-process subagents via session registry ([c90b824](https://github.com/gotgenes/pi-packages/commit/c90b824b4515a1d5ca259348ae0b60c7d70f29d4))
|
|
15
|
+
* **pi-permission-system:** expose registry and getToolPermission on PermissionsService ([984d2bb](https://github.com/gotgenes/pi-packages/commit/984d2bbb76f08cea91b5c0117eb356ae576ad6be))
|
|
16
|
+
* **pi-permission-system:** resolve forwarding target from subagent registry ([5eb15af](https://github.com/gotgenes/pi-packages/commit/5eb15afe680bfd36627c2c21165b59a0ea5e227c))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Documentation
|
|
20
|
+
|
|
21
|
+
* **pi-permission-system:** document subagent session registry API ([93c5c3e](https://github.com/gotgenes/pi-packages/commit/93c5c3e72b2b757a99eba17d1c6885ea49271403))
|
|
22
|
+
* **pi-permission-system:** update architecture for subagent registry ([7b32e6a](https://github.com/gotgenes/pi-packages/commit/7b32e6a247e789b927e5cb3f19a367db0c110353))
|
|
23
|
+
* plan subagent session registry and tool-level permission query ([#221](https://github.com/gotgenes/pi-packages/issues/221)) ([a11d91a](https://github.com/gotgenes/pi-packages/commit/a11d91aa1e13e846030deb0af37444c44eeda7c8))
|
|
24
|
+
* **retro:** add planning stage notes for issue [#221](https://github.com/gotgenes/pi-packages/issues/221) ([cf434c2](https://github.com/gotgenes/pi-packages/commit/cf434c2f9711f26290a4635aea519f1f56e98cc7))
|
|
25
|
+
* **retro:** add TDD stage notes for issue [#221](https://github.com/gotgenes/pi-packages/issues/221) ([e050898](https://github.com/gotgenes/pi-packages/commit/e05089840ee6bbb07cbeab5c55367e2dcd304866))
|
|
26
|
+
|
|
27
|
+
## [7.2.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v7.1.4...pi-permission-system-v7.2.0) (2026-05-24)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Features
|
|
31
|
+
|
|
32
|
+
* add eslint config with type-aware rules and import enforcement ([4fb3cc6](https://github.com/gotgenes/pi-packages/commit/4fb3cc678da10d350b85c464318476ba9ae99dca))
|
|
33
|
+
|
|
8
34
|
## [7.1.4](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v7.1.3...pi-permission-system-v7.1.4) (2026-05-23)
|
|
9
35
|
|
|
10
36
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotgenes/pi-permission-system",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.3.0",
|
|
4
4
|
"description": "Permission enforcement extension for the Pi coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -77,6 +77,6 @@
|
|
|
77
77
|
"test": "vitest run",
|
|
78
78
|
"test:watch": "vitest",
|
|
79
79
|
"lint:md": "rumdl check *.md docs/**/*.md",
|
|
80
|
-
"lint": "biome check . && pnpm run lint:md"
|
|
80
|
+
"lint": "biome check . && eslint . && pnpm run lint:md"
|
|
81
81
|
}
|
|
82
82
|
}
|
package/src/active-agent.ts
CHANGED
package/src/bash-arity.ts
CHANGED
|
@@ -175,6 +175,7 @@ export function prefix(tokens: string[]): string[] {
|
|
|
175
175
|
.map((t) => t.toLowerCase())
|
|
176
176
|
.join(" ");
|
|
177
177
|
const arity = ARITY[key];
|
|
178
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- ARITY record type hides that a key may be absent at runtime
|
|
178
179
|
if (arity !== undefined) {
|
|
179
180
|
return tokens.slice(0, Math.min(arity, tokens.length));
|
|
180
181
|
}
|
package/src/config-modal.ts
CHANGED
|
@@ -61,6 +61,7 @@ function toOnOff(value: boolean): string {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
function formatRulesSummary(rules: Ruleset): string {
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- origin may be absent despite its type
|
|
64
65
|
const configRules = rules.filter((r) => r.layer === "config" && r.origin);
|
|
65
66
|
if (configRules.length === 0) return "";
|
|
66
67
|
const formatted = configRules
|
|
@@ -171,6 +172,7 @@ async function openSettingsModal(
|
|
|
171
172
|
margin: 1,
|
|
172
173
|
};
|
|
173
174
|
|
|
175
|
+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- ctx.ui.custom<void> is valid; rule does not allow void in generic fn call type args
|
|
174
176
|
await ctx.ui.custom<void>(
|
|
175
177
|
(_tui, _theme, _keybindings, done) => {
|
|
176
178
|
let current = controller.getConfig();
|
|
@@ -9,13 +9,13 @@ import {
|
|
|
9
9
|
writeFileSync,
|
|
10
10
|
} from "node:fs";
|
|
11
11
|
|
|
12
|
-
import { isPermissionDecisionState } from "
|
|
12
|
+
import { isPermissionDecisionState } from "#src/permission-dialog";
|
|
13
13
|
import {
|
|
14
14
|
createPermissionForwardingLocation,
|
|
15
15
|
type ForwardedPermissionRequest,
|
|
16
16
|
type ForwardedPermissionResponse,
|
|
17
17
|
type PermissionForwardingLocation,
|
|
18
|
-
} from "
|
|
18
|
+
} from "#src/permission-forwarding";
|
|
19
19
|
|
|
20
20
|
type LogFn = (event: string, details: Record<string, unknown>) => void;
|
|
21
21
|
|
|
@@ -262,6 +262,7 @@ export function readForwardedPermissionRequest(
|
|
|
262
262
|
const raw = readFileSync(filePath, "utf-8");
|
|
263
263
|
const parsed = JSON.parse(raw) as Partial<ForwardedPermissionRequest>;
|
|
264
264
|
if (
|
|
265
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- JSON.parse can return null for the string "null"
|
|
265
266
|
!parsed ||
|
|
266
267
|
typeof parsed.id !== "string" ||
|
|
267
268
|
typeof parsed.createdAt !== "number" ||
|
|
@@ -303,6 +304,7 @@ export function readForwardedPermissionResponse(
|
|
|
303
304
|
const raw = readFileSync(filePath, "utf-8");
|
|
304
305
|
const parsed = JSON.parse(raw) as Partial<ForwardedPermissionResponse>;
|
|
305
306
|
if (
|
|
307
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- JSON.parse can return null for the string "null"
|
|
306
308
|
!parsed ||
|
|
307
309
|
typeof parsed.approved !== "boolean" ||
|
|
308
310
|
!isPermissionDecisionState(parsed.state) ||
|
|
@@ -5,12 +5,12 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
5
5
|
import {
|
|
6
6
|
getActiveAgentName,
|
|
7
7
|
getActiveAgentNameFromSystemPrompt,
|
|
8
|
-
} from "
|
|
9
|
-
import { toRecord } from "
|
|
8
|
+
} from "#src/active-agent";
|
|
9
|
+
import { toRecord } from "#src/common";
|
|
10
10
|
import type {
|
|
11
11
|
PermissionPromptDecision,
|
|
12
12
|
RequestPermissionOptions,
|
|
13
|
-
} from "
|
|
13
|
+
} from "#src/permission-dialog";
|
|
14
14
|
import {
|
|
15
15
|
type ForwardedPermissionRequest,
|
|
16
16
|
type ForwardedPermissionResponse,
|
|
@@ -19,8 +19,9 @@ import {
|
|
|
19
19
|
PERMISSION_FORWARDING_TIMEOUT_MS,
|
|
20
20
|
resolvePermissionForwardingTargetSessionId,
|
|
21
21
|
SUBAGENT_PARENT_SESSION_ENV_CANDIDATES,
|
|
22
|
-
} from "
|
|
23
|
-
import { isSubagentExecutionContext } from "
|
|
22
|
+
} from "#src/permission-forwarding";
|
|
23
|
+
import { isSubagentExecutionContext } from "#src/subagent-context";
|
|
24
|
+
import type { SubagentSessionRegistry } from "#src/subagent-registry";
|
|
24
25
|
|
|
25
26
|
import {
|
|
26
27
|
cleanupPermissionForwardingLocationIfEmpty,
|
|
@@ -40,6 +41,8 @@ import {
|
|
|
40
41
|
export interface PermissionForwardingDeps {
|
|
41
42
|
forwardingDir: string;
|
|
42
43
|
subagentSessionsDir: string;
|
|
44
|
+
/** In-process subagent session registry for detection and forwarding target resolution. */
|
|
45
|
+
registry?: SubagentSessionRegistry;
|
|
43
46
|
logger: ForwardedPermissionLogger;
|
|
44
47
|
writeReviewLog: (event: string, details: Record<string, unknown>) => void;
|
|
45
48
|
requestPermissionDecisionFromUi: (
|
|
@@ -69,6 +72,7 @@ function getContextSystemPrompt(ctx: ExtensionContext): string | undefined {
|
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
try {
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- getSystemPrompt is a Pi SDK accessor returning any
|
|
72
76
|
const systemPrompt = getSystemPrompt.call(ctx);
|
|
73
77
|
return typeof systemPrompt === "string" ? systemPrompt : undefined;
|
|
74
78
|
} catch (error) {
|
|
@@ -101,11 +105,18 @@ export async function waitForForwardedPermissionApproval(
|
|
|
101
105
|
deps: PermissionForwardingDeps,
|
|
102
106
|
): Promise<PermissionPromptDecision> {
|
|
103
107
|
const requesterSessionId = getSessionId(ctx);
|
|
108
|
+
const sessionDir = ctx.sessionManager.getSessionDir();
|
|
104
109
|
const targetSessionId = resolvePermissionForwardingTargetSessionId({
|
|
105
110
|
hasUI: ctx.hasUI,
|
|
106
|
-
isSubagent: isSubagentExecutionContext(
|
|
111
|
+
isSubagent: isSubagentExecutionContext(
|
|
112
|
+
ctx,
|
|
113
|
+
deps.subagentSessionsDir,
|
|
114
|
+
deps.registry,
|
|
115
|
+
),
|
|
107
116
|
currentSessionId: requesterSessionId,
|
|
108
117
|
env: process.env,
|
|
118
|
+
sessionDir,
|
|
119
|
+
registry: deps.registry,
|
|
109
120
|
});
|
|
110
121
|
|
|
111
122
|
if (!targetSessionId) {
|
|
@@ -135,8 +146,8 @@ export async function waitForForwardedPermissionApproval(
|
|
|
135
146
|
|
|
136
147
|
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${process.pid}`;
|
|
137
148
|
const requesterAgentName =
|
|
138
|
-
getActiveAgentName(ctx)
|
|
139
|
-
getActiveAgentNameFromSystemPrompt(getContextSystemPrompt(ctx))
|
|
149
|
+
getActiveAgentName(ctx) ??
|
|
150
|
+
getActiveAgentNameFromSystemPrompt(getContextSystemPrompt(ctx)) ??
|
|
140
151
|
"unknown";
|
|
141
152
|
const request: ForwardedPermissionRequest = {
|
|
142
153
|
id: requestId,
|
|
@@ -359,7 +370,9 @@ export async function confirmPermission(
|
|
|
359
370
|
);
|
|
360
371
|
}
|
|
361
372
|
|
|
362
|
-
if (
|
|
373
|
+
if (
|
|
374
|
+
!isSubagentExecutionContext(ctx, deps.subagentSessionsDir, deps.registry)
|
|
375
|
+
) {
|
|
363
376
|
return { approved: false, state: "denied" };
|
|
364
377
|
}
|
|
365
378
|
|
|
@@ -4,6 +4,7 @@ import type { PermissionForwardingDeps } from "./forwarded-permissions/polling";
|
|
|
4
4
|
import { processForwardedPermissionRequests } from "./forwarded-permissions/polling";
|
|
5
5
|
import { PERMISSION_FORWARDING_POLL_INTERVAL_MS } from "./permission-forwarding";
|
|
6
6
|
import { isSubagentExecutionContext } from "./subagent-context";
|
|
7
|
+
import type { SubagentSessionRegistry } from "./subagent-registry";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Narrow interface for the forwarding lifecycle used by `PermissionSession`.
|
|
@@ -30,6 +31,7 @@ export class ForwardingManager {
|
|
|
30
31
|
constructor(
|
|
31
32
|
private readonly subagentSessionsDir: string,
|
|
32
33
|
private readonly forwardingDeps: PermissionForwardingDeps,
|
|
34
|
+
private readonly registry?: SubagentSessionRegistry,
|
|
33
35
|
) {}
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -41,7 +43,7 @@ export class ForwardingManager {
|
|
|
41
43
|
start(ctx: ExtensionContext): void {
|
|
42
44
|
if (
|
|
43
45
|
!ctx.hasUI ||
|
|
44
|
-
isSubagentExecutionContext(ctx, this.subagentSessionsDir)
|
|
46
|
+
isSubagentExecutionContext(ctx, this.subagentSessionsDir, this.registry)
|
|
45
47
|
) {
|
|
46
48
|
this.stop();
|
|
47
49
|
return;
|
|
@@ -6,12 +6,12 @@ import type {
|
|
|
6
6
|
import {
|
|
7
7
|
createActiveToolsCacheKey,
|
|
8
8
|
createBeforeAgentStartPromptStateKey,
|
|
9
|
-
} from "
|
|
10
|
-
import type { PermissionSession } from "
|
|
11
|
-
import { resolveSkillPromptEntries } from "
|
|
12
|
-
import { sanitizeAvailableToolsSection } from "
|
|
13
|
-
import { getToolNameFromValue, type ToolRegistry } from "
|
|
14
|
-
import type { PermissionState } from "
|
|
9
|
+
} from "#src/before-agent-start-cache";
|
|
10
|
+
import type { PermissionSession } from "#src/permission-session";
|
|
11
|
+
import { resolveSkillPromptEntries } from "#src/skill-prompt-sanitizer";
|
|
12
|
+
import { sanitizeAvailableToolsSection } from "#src/system-prompt-sanitizer";
|
|
13
|
+
import { getToolNameFromValue, type ToolRegistry } from "#src/tool-registry";
|
|
14
|
+
import type { PermissionState } from "#src/types";
|
|
15
15
|
|
|
16
16
|
/** Minimal subset of BeforeAgentStartEvent used by this handler. */
|
|
17
17
|
interface BeforeAgentStartPayload {
|
|
@@ -45,6 +45,7 @@ export class AgentPrepHandler {
|
|
|
45
45
|
private readonly toolRegistry: ToolRegistry,
|
|
46
46
|
) {}
|
|
47
47
|
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
48
49
|
async handle(
|
|
49
50
|
event: BeforeAgentStartPayload,
|
|
50
51
|
ctx: ExtensionContext,
|
|
@@ -40,13 +40,11 @@ async function initParser(): Promise<TSParser> {
|
|
|
40
40
|
const bashWasm = req.resolve("tree-sitter-bash/tree-sitter-bash.wasm");
|
|
41
41
|
const bash = await Language.load(bashWasm);
|
|
42
42
|
parser.setLanguage(bash);
|
|
43
|
-
return parser
|
|
43
|
+
return parser;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function getParser(): Promise<TSParser> {
|
|
47
|
-
|
|
48
|
-
parserPromise = initParser();
|
|
49
|
-
}
|
|
47
|
+
parserPromise ??= initParser();
|
|
50
48
|
return parserPromise;
|
|
51
49
|
}
|
|
52
50
|
|
|
@@ -83,7 +81,7 @@ function resolveNodeText(node: TSNode): string {
|
|
|
83
81
|
case "raw_string": {
|
|
84
82
|
// Strip surrounding single quotes: 'content' → content
|
|
85
83
|
const t = node.text;
|
|
86
|
-
if (t.length >= 2 && t
|
|
84
|
+
if (t.length >= 2 && t.startsWith("'") && t.endsWith("'")) {
|
|
87
85
|
return t.slice(1, -1);
|
|
88
86
|
}
|
|
89
87
|
return t;
|
|
@@ -73,7 +73,7 @@ export async function describeBashPathGate(
|
|
|
73
73
|
worstToken = token;
|
|
74
74
|
break; // Short-circuit on deny.
|
|
75
75
|
}
|
|
76
|
-
if (check.state === "ask" &&
|
|
76
|
+
if (check.state === "ask" && worstCheck?.state !== "ask") {
|
|
77
77
|
worstCheck = check;
|
|
78
78
|
worstToken = token;
|
|
79
79
|
}
|
|
@@ -61,6 +61,7 @@ export async function runGateCheck(
|
|
|
61
61
|
value: descriptor.decision.value,
|
|
62
62
|
result: "allow",
|
|
63
63
|
resolution: "session_approved",
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- ?? null normalises undefined to null for the log record
|
|
64
65
|
origin: check.origin ?? null,
|
|
65
66
|
agentName: agentName ?? null,
|
|
66
67
|
matchedPattern: check.matchedPattern ?? null,
|
|
@@ -107,6 +108,7 @@ export async function runGateCheck(
|
|
|
107
108
|
autoApproved = decision.autoApproved === true;
|
|
108
109
|
return decision;
|
|
109
110
|
},
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method -- logger methods are plain functions; no this-binding issue
|
|
110
112
|
writeLog: deps.writeReviewLog,
|
|
111
113
|
logContext: { ...descriptor.logContext, agentName },
|
|
112
114
|
messages,
|
|
@@ -128,6 +130,7 @@ export async function runGateCheck(
|
|
|
128
130
|
canConfirm,
|
|
129
131
|
autoApproved,
|
|
130
132
|
),
|
|
133
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- ?? null normalises undefined to null for the log record
|
|
131
134
|
origin: check.origin ?? null,
|
|
132
135
|
agentName: agentName ?? null,
|
|
133
136
|
matchedPattern: check.matchedPattern ?? null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
-
import type { PermissionSession } from "
|
|
4
|
-
import { PERMISSION_SYSTEM_STATUS_KEY } from "
|
|
3
|
+
import type { PermissionSession } from "#src/permission-session";
|
|
4
|
+
import { PERMISSION_SYSTEM_STATUS_KEY } from "#src/status";
|
|
5
5
|
|
|
6
6
|
/** Minimal subset of SessionStartEvent used by this handler. */
|
|
7
7
|
interface SessionStartPayload {
|
|
@@ -26,7 +26,7 @@ export class SessionLifecycleHandler {
|
|
|
26
26
|
private readonly cleanupRpc: () => void,
|
|
27
27
|
) {}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
handleSessionStart(
|
|
30
30
|
event: SessionStartPayload,
|
|
31
31
|
ctx: ExtensionContext,
|
|
32
32
|
): Promise<void> {
|
|
@@ -48,13 +48,12 @@ export class SessionLifecycleHandler {
|
|
|
48
48
|
cwd: ctx.cwd,
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
|
+
return Promise.resolve();
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
event: ResourcesDiscoverPayload,
|
|
55
|
-
): Promise<void> {
|
|
54
|
+
handleResourcesDiscover(event: ResourcesDiscoverPayload): Promise<void> {
|
|
56
55
|
if (event.reason !== "reload") {
|
|
57
|
-
return;
|
|
56
|
+
return Promise.resolve();
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
const { session } = this;
|
|
@@ -64,9 +63,10 @@ export class SessionLifecycleHandler {
|
|
|
64
63
|
reason: event.reason,
|
|
65
64
|
cwd: session.getRuntimeContext()?.cwd ?? null,
|
|
66
65
|
});
|
|
66
|
+
return Promise.resolve();
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
handleSessionShutdown(): Promise<void> {
|
|
70
70
|
const { session } = this;
|
|
71
71
|
const ctx = session.getRuntimeContext();
|
|
72
72
|
if (ctx) {
|
|
@@ -74,5 +74,6 @@ export class SessionLifecycleHandler {
|
|
|
74
74
|
}
|
|
75
75
|
session.shutdown();
|
|
76
76
|
this.cleanupRpc();
|
|
77
|
+
return Promise.resolve();
|
|
77
78
|
}
|
|
78
79
|
}
|
|
@@ -3,24 +3,24 @@ import type {
|
|
|
3
3
|
InputEventResult,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
5
|
|
|
6
|
-
import { toRecord } from "
|
|
6
|
+
import { toRecord } from "#src/common";
|
|
7
7
|
import {
|
|
8
8
|
emitDecisionEvent,
|
|
9
9
|
type PermissionEventBus,
|
|
10
|
-
} from "
|
|
11
|
-
import { applyPermissionGate } from "
|
|
12
|
-
import type { PromptPermissionDetails } from "
|
|
10
|
+
} from "#src/permission-events";
|
|
11
|
+
import { applyPermissionGate } from "#src/permission-gate";
|
|
12
|
+
import type { PromptPermissionDetails } from "#src/permission-prompter";
|
|
13
13
|
import {
|
|
14
14
|
formatMissingToolNameReason,
|
|
15
15
|
formatSkillAskPrompt,
|
|
16
16
|
formatUnknownToolReason,
|
|
17
|
-
} from "
|
|
18
|
-
import type { PermissionSession } from "
|
|
17
|
+
} from "#src/permission-prompts";
|
|
18
|
+
import type { PermissionSession } from "#src/permission-session";
|
|
19
19
|
import {
|
|
20
20
|
checkRequestedToolRegistration,
|
|
21
21
|
getToolNameFromValue,
|
|
22
22
|
type ToolRegistry,
|
|
23
|
-
} from "
|
|
23
|
+
} from "#src/tool-registry";
|
|
24
24
|
import { describeBashExternalDirectoryGate } from "./gates/bash-external-directory";
|
|
25
25
|
import { describeBashPathGate } from "./gates/bash-path";
|
|
26
26
|
import type { GateRunnerDeps } from "./gates/descriptor";
|
|
@@ -104,6 +104,7 @@ export class PermissionGateHandler {
|
|
|
104
104
|
session.prompt(ctx, details);
|
|
105
105
|
const emitDecision: GateRunnerDeps["emitDecision"] = (e) =>
|
|
106
106
|
emitDecisionEvent(this.events, e);
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method -- logger.review is a plain function closure; no this-binding issue
|
|
107
108
|
const writeReviewLog = session.logger.review;
|
|
108
109
|
const checkPermission: GateRunnerDeps["checkPermission"] = (
|
|
109
110
|
surface,
|
|
@@ -305,6 +306,7 @@ export class PermissionGateHandler {
|
|
|
305
306
|
skillInputAutoApproved = decision.autoApproved === true;
|
|
306
307
|
return decision;
|
|
307
308
|
},
|
|
309
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method -- logger.review is a plain function closure; no this-binding issue
|
|
308
310
|
writeLog: session.logger.review,
|
|
309
311
|
logContext: {
|
|
310
312
|
source: "skill_input",
|
|
@@ -324,6 +326,7 @@ export class PermissionGateHandler {
|
|
|
324
326
|
surface: "skill",
|
|
325
327
|
value: skillName,
|
|
326
328
|
result: skillInputGate.action === "allow" ? "allow" : "deny",
|
|
329
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-condition -- defensive fallback; TypeScript narrows check.state before the ternary's else branch */
|
|
327
330
|
resolution:
|
|
328
331
|
check.state === "allow"
|
|
329
332
|
? "policy_allow"
|
|
@@ -336,6 +339,8 @@ export class PermissionGateHandler {
|
|
|
336
339
|
: skillInputCanConfirm
|
|
337
340
|
? "user_denied"
|
|
338
341
|
: "confirmation_unavailable",
|
|
342
|
+
/* eslint-enable @typescript-eslint/no-unnecessary-condition */
|
|
343
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- ?? null normalises undefined to null for the log record
|
|
339
344
|
origin: check.origin ?? null,
|
|
340
345
|
agentName: agentName ?? null,
|
|
341
346
|
matchedPattern: check.matchedPattern ?? null,
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "./service";
|
|
28
28
|
import { createSessionLogger } from "./session-logger";
|
|
29
29
|
import { isSubagentExecutionContext } from "./subagent-context";
|
|
30
|
+
import { SubagentSessionRegistry } from "./subagent-registry";
|
|
30
31
|
import {
|
|
31
32
|
canResolveAskPermissionRequest,
|
|
32
33
|
shouldAutoApprovePermissionState,
|
|
@@ -34,18 +35,21 @@ import {
|
|
|
34
35
|
|
|
35
36
|
export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
36
37
|
const runtime = createExtensionRuntime();
|
|
38
|
+
const subagentRegistry = new SubagentSessionRegistry();
|
|
37
39
|
|
|
38
40
|
const prompter = new PermissionPrompter({
|
|
39
41
|
getConfig: () => runtime.config,
|
|
40
42
|
writeReviewLog: runtime.writeReviewLog.bind(runtime),
|
|
41
43
|
subagentSessionsDir: runtime.subagentSessionsDir,
|
|
42
44
|
forwardingDir: runtime.forwardingDir,
|
|
45
|
+
registry: subagentRegistry,
|
|
43
46
|
requestPermissionDecisionFromUi,
|
|
44
47
|
});
|
|
45
48
|
|
|
46
49
|
const forwardingDeps: PermissionForwardingDeps = {
|
|
47
50
|
forwardingDir: runtime.forwardingDir,
|
|
48
51
|
subagentSessionsDir: runtime.subagentSessionsDir,
|
|
52
|
+
registry: subagentRegistry,
|
|
49
53
|
logger: {
|
|
50
54
|
writeReviewLog: runtime.writeReviewLog.bind(runtime),
|
|
51
55
|
writeDebugLog: runtime.writeDebugLog.bind(runtime),
|
|
@@ -61,7 +65,11 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
61
65
|
const session = new PermissionSession(
|
|
62
66
|
runtime,
|
|
63
67
|
createSessionLogger(runtime),
|
|
64
|
-
new ForwardingManager(
|
|
68
|
+
new ForwardingManager(
|
|
69
|
+
runtime.subagentSessionsDir,
|
|
70
|
+
forwardingDeps,
|
|
71
|
+
subagentRegistry,
|
|
72
|
+
),
|
|
65
73
|
{
|
|
66
74
|
refreshExtensionConfig: (ctx) => refreshExtensionConfig(runtime, ctx),
|
|
67
75
|
logResolvedConfigPaths: () => logResolvedConfigPaths(runtime),
|
|
@@ -73,6 +81,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
73
81
|
isSubagent: isSubagentExecutionContext(
|
|
74
82
|
ctx,
|
|
75
83
|
runtime.subagentSessionsDir,
|
|
84
|
+
subagentRegistry,
|
|
76
85
|
),
|
|
77
86
|
}),
|
|
78
87
|
promptPermission: (ctx, details) => prompter.prompt(ctx, details),
|
|
@@ -108,6 +117,15 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
108
117
|
sessionRules,
|
|
109
118
|
);
|
|
110
119
|
},
|
|
120
|
+
registerSubagentSession(sessionKey, info) {
|
|
121
|
+
subagentRegistry.register(sessionKey, info);
|
|
122
|
+
},
|
|
123
|
+
unregisterSubagentSession(sessionKey) {
|
|
124
|
+
subagentRegistry.unregister(sessionKey);
|
|
125
|
+
},
|
|
126
|
+
getToolPermission(toolName, agentName) {
|
|
127
|
+
return runtime.permissionManager.getToolPermission(toolName, agentName);
|
|
128
|
+
},
|
|
111
129
|
};
|
|
112
130
|
publishPermissionsService(permissionsService);
|
|
113
131
|
|
package/src/logging.ts
CHANGED
|
@@ -21,12 +21,15 @@ export function safeJsonStringify(value: unknown): string | undefined {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (typeof currentValue === "object" && currentValue !== null) {
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- JSON.stringify replacer receives any; currentValue is narrowed to object here
|
|
24
25
|
if (seen.has(currentValue)) {
|
|
25
26
|
return "[Circular]";
|
|
26
27
|
}
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- same as above
|
|
27
29
|
seen.add(currentValue);
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return -- JSON.stringify replacer must return any
|
|
30
33
|
return currentValue;
|
|
31
34
|
});
|
|
32
35
|
}
|
|
@@ -40,7 +40,7 @@ function discoverGlobalNodeModulesViaSubprocess(): string | null {
|
|
|
40
40
|
timeout: 5000,
|
|
41
41
|
stdio: ["ignore", "pipe", "ignore"],
|
|
42
42
|
});
|
|
43
|
-
const root = result.stdout
|
|
43
|
+
const root = result.stdout.trim();
|
|
44
44
|
if (result.status === 0 && root && existsSync(root)) {
|
|
45
45
|
return root;
|
|
46
46
|
}
|
package/src/normalize.ts
CHANGED
|
@@ -20,6 +20,7 @@ export function normalizeFlatConfig(permission: FlatPermissionConfig): Ruleset {
|
|
|
20
20
|
if (isPermissionState(value)) {
|
|
21
21
|
rules.push({ surface, pattern: "*", action: value, origin: "builtin" });
|
|
22
22
|
}
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive null check; value type does not include null but runtime JSON may
|
|
23
24
|
} else if (typeof value === "object" && value !== null) {
|
|
24
25
|
for (const [pattern, action] of Object.entries(value)) {
|
|
25
26
|
if (isPermissionState(action)) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-deprecated -- this module implements the deprecated event-bus RPC channel; references to its own deprecated symbols are intentional */
|
|
1
2
|
/**
|
|
2
3
|
* Permission event bus RPC handlers.
|
|
3
4
|
*
|
|
@@ -112,6 +113,7 @@ function handleCheckRpc(
|
|
|
112
113
|
const data: PermissionsCheckReplyData = {
|
|
113
114
|
result: result.state,
|
|
114
115
|
matchedPattern: result.matchedPattern ?? null,
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- ?? null normalises undefined to null for the reply record
|
|
115
117
|
origin: result.origin ?? null,
|
|
116
118
|
};
|
|
117
119
|
events.emit(replyChannel, successReply(data));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
|
|
3
3
|
import type { PermissionDecisionState } from "./permission-dialog";
|
|
4
|
+
import type { SubagentSessionRegistry } from "./subagent-registry";
|
|
4
5
|
|
|
5
6
|
export const PERMISSION_FORWARDING_POLL_INTERVAL_MS = 250;
|
|
6
7
|
export const PERMISSION_FORWARDING_TIMEOUT_MS = 10 * 60 * 1000;
|
|
@@ -118,6 +119,10 @@ export function resolvePermissionForwardingTargetSessionId(options: {
|
|
|
118
119
|
isSubagent: boolean;
|
|
119
120
|
currentSessionId?: string | null;
|
|
120
121
|
env?: NodeJS.ProcessEnv;
|
|
122
|
+
/** Session directory key for registry lookup. */
|
|
123
|
+
sessionDir?: string;
|
|
124
|
+
/** In-process subagent session registry (checked before env vars). */
|
|
125
|
+
registry?: SubagentSessionRegistry;
|
|
121
126
|
}): string | null {
|
|
122
127
|
if (options.hasUI) {
|
|
123
128
|
return normalizePermissionForwardingSessionId(options.currentSessionId);
|
|
@@ -127,6 +132,16 @@ export function resolvePermissionForwardingTargetSessionId(options: {
|
|
|
127
132
|
return null;
|
|
128
133
|
}
|
|
129
134
|
|
|
135
|
+
// 1. Registry — in-process subagents register parentSessionId explicitly.
|
|
136
|
+
if (options.registry && options.sessionDir) {
|
|
137
|
+
const entry = options.registry.get(options.sessionDir);
|
|
138
|
+
const resolved = normalizePermissionForwardingSessionId(
|
|
139
|
+
entry?.parentSessionId,
|
|
140
|
+
);
|
|
141
|
+
if (resolved) return resolved;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 2. Env vars — process-based subagent extensions.
|
|
130
145
|
const env = options.env ?? process.env;
|
|
131
146
|
for (const key of SUBAGENT_PARENT_SESSION_ENV_CANDIDATES) {
|
|
132
147
|
const resolved = normalizePermissionForwardingSessionId(env[key]);
|
|
@@ -78,7 +78,7 @@ export class PermissionManager {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
private resolvePermissions(agentName?: string): ResolvedPermissions {
|
|
81
|
-
const cacheKey = agentName
|
|
81
|
+
const cacheKey = agentName ?? "__global__";
|
|
82
82
|
const stamp = this.loader.getCacheStamp(agentName);
|
|
83
83
|
const cached = this.resolvedPermissionsCache.get(cacheKey);
|
|
84
84
|
if (cached?.stamp === stamp) {
|
|
@@ -107,17 +107,19 @@ export class PermissionManager {
|
|
|
107
107
|
|
|
108
108
|
for (const [surface, value] of Object.entries(scope.permission)) {
|
|
109
109
|
const baseVal = mergedPermission[surface];
|
|
110
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-condition -- defensive null/type checks; config values may differ at runtime */
|
|
110
111
|
const bothObjects =
|
|
111
112
|
typeof baseVal === "object" &&
|
|
112
113
|
baseVal !== null &&
|
|
113
114
|
typeof value === "object" &&
|
|
114
115
|
value !== null;
|
|
116
|
+
/* eslint-enable @typescript-eslint/no-unnecessary-condition */
|
|
115
117
|
|
|
116
118
|
if (bothObjects) {
|
|
117
119
|
// Shallow-merge: each incoming pattern is attributed to this scope;
|
|
118
120
|
// existing patterns from lower scopes keep their earlier origin.
|
|
119
121
|
if (!origins.has(surface)) origins.set(surface, new Map());
|
|
120
|
-
for (const pattern of Object.keys(value
|
|
122
|
+
for (const pattern of Object.keys(value)) {
|
|
121
123
|
origins.get(surface)!.set(pattern, scopeName);
|
|
122
124
|
}
|
|
123
125
|
} else {
|
|
@@ -125,10 +127,9 @@ export class PermissionManager {
|
|
|
125
127
|
const surfaceOrigins = new Map<string, RuleOrigin>();
|
|
126
128
|
if (typeof value === "string") {
|
|
127
129
|
surfaceOrigins.set("*", scopeName);
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive null check
|
|
128
131
|
} else if (typeof value === "object" && value !== null) {
|
|
129
|
-
for (const pattern of Object.keys(
|
|
130
|
-
value as Record<string, unknown>,
|
|
131
|
-
)) {
|
|
132
|
+
for (const pattern of Object.keys(value)) {
|
|
132
133
|
surfaceOrigins.set(pattern, scopeName);
|
|
133
134
|
}
|
|
134
135
|
}
|
|
@@ -146,7 +147,7 @@ export class PermissionManager {
|
|
|
146
147
|
// The "*" key feeds synthesizeDefaults() only — it is NOT included as a
|
|
147
148
|
// config rule so that extension tools fall through to source:"default".
|
|
148
149
|
const universalFallback = isPermissionState(mergedPermission["*"])
|
|
149
|
-
?
|
|
150
|
+
? mergedPermission["*"]
|
|
150
151
|
: DEFAULT_UNIVERSAL_FALLBACK;
|
|
151
152
|
// Track which scope contributed the universal fallback.
|
|
152
153
|
const universalFallbackOrigin: RuleOrigin =
|
package/src/permission-merge.ts
CHANGED
|
@@ -12,15 +12,17 @@ export function mergeFlatPermissions(
|
|
|
12
12
|
const merged: FlatPermissionConfig = { ...base };
|
|
13
13
|
for (const [key, value] of Object.entries(override)) {
|
|
14
14
|
const baseVal = merged[key];
|
|
15
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-condition -- defensive null/type checks; config values may differ at runtime */
|
|
15
16
|
if (
|
|
16
17
|
typeof baseVal === "object" &&
|
|
17
18
|
baseVal !== null &&
|
|
18
19
|
typeof value === "object" &&
|
|
19
20
|
value !== null
|
|
20
21
|
) {
|
|
22
|
+
/* eslint-enable @typescript-eslint/no-unnecessary-condition */
|
|
21
23
|
merged[key] = {
|
|
22
|
-
...
|
|
23
|
-
...
|
|
24
|
+
...baseVal,
|
|
25
|
+
...value,
|
|
24
26
|
};
|
|
25
27
|
} else {
|
|
26
28
|
merged[key] = value;
|