@gotgenes/pi-permission-system 7.1.4 → 7.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 +7 -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 +8 -7
- 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/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-manager.ts +7 -6
- package/src/permission-merge.ts +4 -2
- package/src/permission-prompter.ts +3 -0
- package/src/permission-prompts.ts +1 -1
- package/src/policy-loader.ts +5 -5
- package/src/service.ts +1 -0
- package/src/skill-prompt-sanitizer.ts +3 -3
- package/src/tool-registry.ts +1 -1
- package/src/yolo-mode.ts +2 -1
- package/test/config-modal.test.ts +6 -8
- 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 +1 -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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ 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.2.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v7.1.4...pi-permission-system-v7.2.0) (2026-05-24)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add eslint config with type-aware rules and import enforcement ([4fb3cc6](https://github.com/gotgenes/pi-packages/commit/4fb3cc678da10d350b85c464318476ba9ae99dca))
|
|
14
|
+
|
|
8
15
|
## [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
16
|
|
|
10
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotgenes/pi-permission-system",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.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,8 @@ 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
24
|
|
|
25
25
|
import {
|
|
26
26
|
cleanupPermissionForwardingLocationIfEmpty,
|
|
@@ -69,6 +69,7 @@ function getContextSystemPrompt(ctx: ExtensionContext): string | undefined {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
try {
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- getSystemPrompt is a Pi SDK accessor returning any
|
|
72
73
|
const systemPrompt = getSystemPrompt.call(ctx);
|
|
73
74
|
return typeof systemPrompt === "string" ? systemPrompt : undefined;
|
|
74
75
|
} catch (error) {
|
|
@@ -135,8 +136,8 @@ export async function waitForForwardedPermissionApproval(
|
|
|
135
136
|
|
|
136
137
|
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${process.pid}`;
|
|
137
138
|
const requesterAgentName =
|
|
138
|
-
getActiveAgentName(ctx)
|
|
139
|
-
getActiveAgentNameFromSystemPrompt(getContextSystemPrompt(ctx))
|
|
139
|
+
getActiveAgentName(ctx) ??
|
|
140
|
+
getActiveAgentNameFromSystemPrompt(getContextSystemPrompt(ctx)) ??
|
|
140
141
|
"unknown";
|
|
141
142
|
const request: ForwardedPermissionRequest = {
|
|
142
143
|
id: requestId,
|
|
@@ -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/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));
|
|
@@ -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;
|
|
@@ -148,6 +148,7 @@ export class PermissionPrompter implements PermissionPrompterApi {
|
|
|
148
148
|
private buildForwardingDeps(): PermissionForwardingDeps {
|
|
149
149
|
const { deps } = this;
|
|
150
150
|
const logger: ForwardedPermissionLogger = {
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method -- logger methods are plain function closures; no this-binding issue
|
|
151
152
|
writeReviewLog: deps.writeReviewLog,
|
|
152
153
|
writeDebugLog: () => undefined,
|
|
153
154
|
};
|
|
@@ -155,7 +156,9 @@ export class PermissionPrompter implements PermissionPrompterApi {
|
|
|
155
156
|
forwardingDir: deps.forwardingDir,
|
|
156
157
|
subagentSessionsDir: deps.subagentSessionsDir,
|
|
157
158
|
logger,
|
|
159
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method -- logger methods are plain function closures; no this-binding issue
|
|
158
160
|
writeReviewLog: deps.writeReviewLog,
|
|
161
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method -- same as above
|
|
159
162
|
requestPermissionDecisionFromUi: deps.requestPermissionDecisionFromUi,
|
|
160
163
|
shouldAutoApprove: () => false,
|
|
161
164
|
};
|
|
@@ -38,7 +38,7 @@ export function formatAskPrompt(
|
|
|
38
38
|
const patternInfo = result.matchedPattern
|
|
39
39
|
? ` (matched '${result.matchedPattern}')`
|
|
40
40
|
: "";
|
|
41
|
-
return `${subject} requested bash command '${result.command
|
|
41
|
+
return `${subject} requested bash command '${result.command ?? ""}'${patternInfo}. Allow this command?`;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
if ((result.source === "mcp" || result.toolName === "mcp") && result.target) {
|
package/src/policy-loader.ts
CHANGED
|
@@ -169,12 +169,12 @@ export class FilePolicyLoader implements PolicyLoader {
|
|
|
169
169
|
|
|
170
170
|
constructor(options: PolicyLoaderOptions = {}) {
|
|
171
171
|
this.globalConfigPath =
|
|
172
|
-
options.globalConfigPath
|
|
173
|
-
this.agentsDir = options.agentsDir
|
|
174
|
-
this.projectGlobalConfigPath = options.projectGlobalConfigPath
|
|
175
|
-
this.projectAgentsDir = options.projectAgentsDir
|
|
172
|
+
options.globalConfigPath ?? defaultGlobalConfigPath();
|
|
173
|
+
this.agentsDir = options.agentsDir ?? defaultAgentsDir();
|
|
174
|
+
this.projectGlobalConfigPath = options.projectGlobalConfigPath ?? null;
|
|
175
|
+
this.projectAgentsDir = options.projectAgentsDir ?? null;
|
|
176
176
|
this.globalMcpConfigPath =
|
|
177
|
-
options.globalMcpConfigPath
|
|
177
|
+
options.globalMcpConfigPath ?? defaultGlobalMcpConfigPath();
|
|
178
178
|
this.configuredMcpServerNamesOverride = options.mcpServerNames
|
|
179
179
|
? [
|
|
180
180
|
...new Set(
|
package/src/service.ts
CHANGED
|
@@ -71,5 +71,6 @@ export function getPermissionsService(): PermissionsService | undefined {
|
|
|
71
71
|
* extension is torn down.
|
|
72
72
|
*/
|
|
73
73
|
export function unpublishPermissionsService(): void {
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- Symbol-keyed global property; Map.delete() is not applicable
|
|
74
75
|
delete (globalThis as Record<symbol, unknown>)[SERVICE_KEY];
|
|
75
76
|
}
|
|
@@ -70,9 +70,9 @@ function parseSkillEntries(sectionBody: string): ParsedSkillPromptEntry[] {
|
|
|
70
70
|
|
|
71
71
|
for (const match of sectionBody.matchAll(skillBlockRegex)) {
|
|
72
72
|
const block = match[1];
|
|
73
|
-
const nameMatch =
|
|
74
|
-
const descriptionMatch =
|
|
75
|
-
const locationMatch =
|
|
73
|
+
const nameMatch = SKILL_NAME_REGEX.exec(block);
|
|
74
|
+
const descriptionMatch = SKILL_DESCRIPTION_REGEX.exec(block);
|
|
75
|
+
const locationMatch = SKILL_LOCATION_REGEX.exec(block);
|
|
76
76
|
|
|
77
77
|
if (!nameMatch || !descriptionMatch || !locationMatch) {
|
|
78
78
|
continue;
|
package/src/tool-registry.ts
CHANGED
|
@@ -35,7 +35,7 @@ function buildReverseAliases(
|
|
|
35
35
|
const reverse = new Map<string, string[]>();
|
|
36
36
|
|
|
37
37
|
for (const [alias, canonical] of Object.entries(aliases)) {
|
|
38
|
-
const existing = reverse.get(canonical)
|
|
38
|
+
const existing = reverse.get(canonical) ?? [];
|
|
39
39
|
if (!existing.includes(alias)) {
|
|
40
40
|
existing.push(alias);
|
|
41
41
|
}
|
package/src/yolo-mode.ts
CHANGED
|
@@ -10,7 +10,8 @@ export interface AskPermissionResolutionOptions {
|
|
|
10
10
|
export function isYoloModeEnabled(
|
|
11
11
|
config: PermissionSystemExtensionConfig,
|
|
12
12
|
): boolean {
|
|
13
|
-
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion -- typed as boolean but may be undefined at runtime (untyped callers); Boolean() guards against that
|
|
14
|
+
return Boolean(config.yoloMode);
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export function shouldAutoApprovePermissionState(
|
|
@@ -68,7 +68,7 @@ function createCommandContext(hasUI: boolean): {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
function lastNotification(notifications: Notification[]): Notification {
|
|
71
|
-
return notifications[notifications.length - 1]
|
|
71
|
+
return notifications[notifications.length - 1];
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
test("permission-system command completions expose top-level config actions", () => {
|
|
@@ -101,7 +101,7 @@ test("permission-system command completions expose top-level config actions", ()
|
|
|
101
101
|
definition = nextDefinition;
|
|
102
102
|
},
|
|
103
103
|
} as never,
|
|
104
|
-
controller
|
|
104
|
+
controller,
|
|
105
105
|
);
|
|
106
106
|
|
|
107
107
|
expect(definition!.getArgumentCompletions).toBeTypeOf("function");
|
|
@@ -172,13 +172,11 @@ test("permission-system command handlers manage config summary, persistence, and
|
|
|
172
172
|
definition = nextDefinition;
|
|
173
173
|
},
|
|
174
174
|
} as never,
|
|
175
|
-
controller
|
|
175
|
+
controller,
|
|
176
176
|
);
|
|
177
177
|
|
|
178
178
|
expect(registeredName).toBe("permission-system");
|
|
179
|
-
expect(definition!.description
|
|
180
|
-
"Configure pi-permission-system",
|
|
181
|
-
);
|
|
179
|
+
expect(definition!.description).toContain("Configure pi-permission-system");
|
|
182
180
|
|
|
183
181
|
const infoCtx = createCommandContext(true);
|
|
184
182
|
await definition!.handler("show", infoCtx.ctx);
|
|
@@ -267,7 +265,7 @@ test("show output includes rule origins when getComposedRules is provided", asyn
|
|
|
267
265
|
definition = nextDef;
|
|
268
266
|
},
|
|
269
267
|
} as never,
|
|
270
|
-
controller
|
|
268
|
+
controller,
|
|
271
269
|
);
|
|
272
270
|
|
|
273
271
|
const ctx = createCommandContext(true);
|
|
@@ -300,7 +298,7 @@ test("show output omits rule summary when getComposedRules is not provided", asy
|
|
|
300
298
|
definition = nextDef;
|
|
301
299
|
},
|
|
302
300
|
} as never,
|
|
303
|
-
controller
|
|
301
|
+
controller,
|
|
304
302
|
);
|
|
305
303
|
|
|
306
304
|
const ctx = createCommandContext(true);
|
|
@@ -52,7 +52,7 @@ function makeSession(
|
|
|
52
52
|
activate: vi.fn(),
|
|
53
53
|
refreshConfig: vi.fn(),
|
|
54
54
|
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
55
|
-
getToolPermission: vi.fn().mockReturnValue("allow"
|
|
55
|
+
getToolPermission: vi.fn().mockReturnValue("allow"),
|
|
56
56
|
checkPermission: vi.fn().mockReturnValue({ state: "allow" }),
|
|
57
57
|
shouldUpdateActiveTools: vi.fn().mockReturnValue(true),
|
|
58
58
|
commitActiveToolsCacheKey: vi.fn(),
|
|
@@ -110,7 +110,7 @@ function makeSession(
|
|
|
110
110
|
activate: vi.fn(),
|
|
111
111
|
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
112
112
|
checkPermission: makeCheckPermission("deny"),
|
|
113
|
-
getToolPermission: vi.fn().mockReturnValue("allow"
|
|
113
|
+
getToolPermission: vi.fn().mockReturnValue("allow"),
|
|
114
114
|
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
115
115
|
approveSessionRule: vi.fn(),
|
|
116
116
|
getActiveSkillEntries: vi.fn().mockReturnValue([]),
|
|
@@ -74,7 +74,7 @@ describe("describeSkillReadGate", () => {
|
|
|
74
74
|
makeSkillEntry({ state: "ask" }),
|
|
75
75
|
]);
|
|
76
76
|
expect(result).not.toBeNull();
|
|
77
|
-
const desc = result
|
|
77
|
+
const desc = result!;
|
|
78
78
|
expect(desc.preResolved).toEqual({ state: "ask" });
|
|
79
79
|
});
|
|
80
80
|
|
|
@@ -83,7 +83,7 @@ describe("describeSkillReadGate", () => {
|
|
|
83
83
|
makeSkillEntry({ state: "allow" }),
|
|
84
84
|
]);
|
|
85
85
|
expect(result).not.toBeNull();
|
|
86
|
-
const desc = result
|
|
86
|
+
const desc = result!;
|
|
87
87
|
expect(desc.preResolved).toEqual({ state: "allow" });
|
|
88
88
|
});
|
|
89
89
|
|
|
@@ -92,14 +92,14 @@ describe("describeSkillReadGate", () => {
|
|
|
92
92
|
makeSkillEntry({ state: "deny" }),
|
|
93
93
|
]);
|
|
94
94
|
expect(result).not.toBeNull();
|
|
95
|
-
const desc = result
|
|
95
|
+
const desc = result!;
|
|
96
96
|
expect(desc.preResolved).toEqual({ state: "deny" });
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
it("decision surface is 'skill' and decision value is the skill name", () => {
|
|
100
100
|
const result = describeSkillReadGate(makeTcc(), () => [
|
|
101
101
|
makeSkillEntry({ name: "my-skill" }),
|
|
102
|
-
])
|
|
102
|
+
])!;
|
|
103
103
|
expect(result.decision.surface).toBe("skill");
|
|
104
104
|
expect(result.decision.value).toBe("my-skill");
|
|
105
105
|
});
|
|
@@ -107,7 +107,7 @@ describe("describeSkillReadGate", () => {
|
|
|
107
107
|
it("denialContext contains the skill name and read path", () => {
|
|
108
108
|
const result = describeSkillReadGate(makeTcc(), () => [
|
|
109
109
|
makeSkillEntry({ name: "librarian" }),
|
|
110
|
-
])
|
|
110
|
+
])!;
|
|
111
111
|
expect(result.denialContext).toEqual({
|
|
112
112
|
kind: "skill_read",
|
|
113
113
|
skillName: "librarian",
|
|
@@ -120,7 +120,7 @@ describe("describeSkillReadGate", () => {
|
|
|
120
120
|
const result = describeSkillReadGate(
|
|
121
121
|
makeTcc({ agentName: "test-agent", toolCallId: "tc-42" }),
|
|
122
122
|
() => [makeSkillEntry({ name: "my-skill" })],
|
|
123
|
-
)
|
|
123
|
+
)!;
|
|
124
124
|
expect(result.promptDetails).toMatchObject({
|
|
125
125
|
source: "skill_read",
|
|
126
126
|
agentName: "test-agent",
|
|
@@ -135,7 +135,7 @@ describe("describeSkillReadGate", () => {
|
|
|
135
135
|
const result = describeSkillReadGate(
|
|
136
136
|
makeTcc({ agentName: "agent-1" }),
|
|
137
137
|
() => [makeSkillEntry({ name: "librarian" })],
|
|
138
|
-
)
|
|
138
|
+
)!;
|
|
139
139
|
expect(result.logContext).toMatchObject({
|
|
140
140
|
source: "skill_read",
|
|
141
141
|
skillName: "librarian",
|
|
@@ -144,9 +144,7 @@ describe("describeSkillReadGate", () => {
|
|
|
144
144
|
});
|
|
145
145
|
|
|
146
146
|
it("surface is 'skill' on the descriptor", () => {
|
|
147
|
-
const result = describeSkillReadGate(makeTcc(), () => [
|
|
148
|
-
makeSkillEntry(),
|
|
149
|
-
]) as GateDescriptor;
|
|
147
|
+
const result = describeSkillReadGate(makeTcc(), () => [makeSkillEntry()])!;
|
|
150
148
|
expect(result.surface).toBe("skill");
|
|
151
149
|
});
|
|
152
150
|
});
|
|
@@ -93,7 +93,7 @@ describe("describeToolGate", () => {
|
|
|
93
93
|
it("populates denialContext with agent name when provided", () => {
|
|
94
94
|
const check = makeCheckResult("ask", { toolName: "read" });
|
|
95
95
|
const desc = describeToolGate(makeTcc({ agentName: "my-agent" }), check);
|
|
96
|
-
expect(desc.denialContext
|
|
96
|
+
expect(desc.denialContext.agentName).toBe("my-agent");
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
it("populates denialContext with input for tool context", () => {
|
|
@@ -54,7 +54,7 @@ function makeSession(
|
|
|
54
54
|
origin: "global",
|
|
55
55
|
matchedPattern: "*",
|
|
56
56
|
}),
|
|
57
|
-
getToolPermission: vi.fn().mockReturnValue("allow"
|
|
57
|
+
getToolPermission: vi.fn().mockReturnValue("allow"),
|
|
58
58
|
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
59
59
|
approveSessionRule: vi.fn(),
|
|
60
60
|
canPrompt: vi.fn().mockReturnValue(true),
|
|
@@ -42,7 +42,7 @@ function makeSession(
|
|
|
42
42
|
activate: vi.fn(),
|
|
43
43
|
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
44
44
|
checkPermission: vi.fn().mockReturnValue({ state: "allow" }),
|
|
45
|
-
getToolPermission: vi.fn().mockReturnValue("allow"
|
|
45
|
+
getToolPermission: vi.fn().mockReturnValue("allow"),
|
|
46
46
|
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
47
47
|
approveSessionRule: vi.fn(),
|
|
48
48
|
canPrompt: vi.fn().mockReturnValue(true),
|
|
@@ -75,7 +75,7 @@ function makeSession(
|
|
|
75
75
|
activate: vi.fn(),
|
|
76
76
|
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
77
77
|
checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
|
|
78
|
-
getToolPermission: vi.fn().mockReturnValue("allow"
|
|
78
|
+
getToolPermission: vi.fn().mockReturnValue("allow"),
|
|
79
79
|
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
80
80
|
approveSessionRule: vi.fn(),
|
|
81
81
|
getActiveSkillEntries: vi.fn().mockReturnValue([]),
|
|
@@ -66,7 +66,7 @@ function makeSession(
|
|
|
66
66
|
activate: vi.fn(),
|
|
67
67
|
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
68
68
|
checkPermission: vi.fn().mockReturnValue(makePermissionResult("allow")),
|
|
69
|
-
getToolPermission: vi.fn().mockReturnValue("allow"
|
|
69
|
+
getToolPermission: vi.fn().mockReturnValue("allow"),
|
|
70
70
|
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
71
71
|
approveSessionRule: vi.fn(),
|
|
72
72
|
getActiveSkillEntries: vi.fn().mockReturnValue([]),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-deprecated -- tests the deprecated RPC channel implementation */
|
|
1
2
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { tmpdir } from "node:os";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
@@ -164,6 +165,7 @@ describe("type shapes (PermissionsRpcReply)", () => {
|
|
|
164
165
|
error: "no_ui",
|
|
165
166
|
};
|
|
166
167
|
expect(reply.success).toBe(false);
|
|
168
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- narrowing on discriminated union
|
|
167
169
|
if (!reply.success) {
|
|
168
170
|
expect(reply.error).toBe("no_ui");
|
|
169
171
|
}
|
|
@@ -24,6 +24,7 @@ describe("SUBAGENT_PARENT_SESSION_ENV_CANDIDATES", () => {
|
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
test("deprecated SUBAGENT_PARENT_SESSION_ENV_KEY equals the first candidate", () => {
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- test verifying the deprecated alias
|
|
27
28
|
expect(SUBAGENT_PARENT_SESSION_ENV_KEY).toBe(
|
|
28
29
|
SUBAGENT_PARENT_SESSION_ENV_CANDIDATES[0],
|
|
29
30
|
);
|
|
@@ -685,10 +685,12 @@ function createInMemoryPolicyLoader(
|
|
|
685
685
|
): PolicyLoader {
|
|
686
686
|
const issues: string[] = [];
|
|
687
687
|
return {
|
|
688
|
-
loadGlobalConfig: () => scopes.global ?? {},
|
|
689
|
-
loadProjectConfig: () => scopes.project ?? {},
|
|
688
|
+
loadGlobalConfig: () => scopes.global ?? ({} as const),
|
|
689
|
+
loadProjectConfig: () => scopes.project ?? ({} as const),
|
|
690
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- || is intentional: handles both falsy name and missing key
|
|
690
691
|
loadAgentConfig: (name?: string) => (name && scopes.agent?.[name]) || {},
|
|
691
692
|
loadProjectAgentConfig: (name?: string) =>
|
|
693
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- || is intentional: handles both falsy name and missing key
|
|
692
694
|
(name && scopes.projectAgent?.[name]) || {},
|
|
693
695
|
getConfiguredMcpServerNames: () => mcpServerNames,
|
|
694
696
|
getCacheStamp: () => "in-memory",
|
|
@@ -122,7 +122,7 @@ function makePermissionManager(
|
|
|
122
122
|
toolName: "read",
|
|
123
123
|
source: "tool",
|
|
124
124
|
origin: "builtin",
|
|
125
|
-
}
|
|
125
|
+
}),
|
|
126
126
|
getToolPermission: vi.fn().mockReturnValue("allow"),
|
|
127
127
|
getConfigIssues: vi.fn().mockReturnValue([]),
|
|
128
128
|
getPolicyCacheStamp: vi.fn().mockReturnValue("stamp-1"),
|
|
@@ -260,7 +260,7 @@ describe("PermissionSession", () => {
|
|
|
260
260
|
toolName: "bash",
|
|
261
261
|
source: "bash",
|
|
262
262
|
origin: "global",
|
|
263
|
-
}
|
|
263
|
+
}),
|
|
264
264
|
});
|
|
265
265
|
mockCreatePermissionManagerForCwd.mockReturnValue(pm2);
|
|
266
266
|
const { session } = createSession();
|
|
@@ -112,6 +112,7 @@ type ExtensionHarnessOptions = {
|
|
|
112
112
|
|
|
113
113
|
const INHERITED_SUBAGENT_ENV_KEYS = [
|
|
114
114
|
...SUBAGENT_ENV_HINT_KEYS,
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- test uses deprecated alias intentionally
|
|
115
116
|
SUBAGENT_PARENT_SESSION_ENV_KEY,
|
|
116
117
|
] as const;
|
|
117
118
|
|
|
@@ -121,6 +122,7 @@ async function withIsolatedSubagentEnv<T>(
|
|
|
121
122
|
const originalValues = new Map<string, string | undefined>();
|
|
122
123
|
for (const key of INHERITED_SUBAGENT_ENV_KEYS) {
|
|
123
124
|
originalValues.set(key, process.env[key]);
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- process.env cleanup requires dynamic delete
|
|
124
126
|
delete process.env[key];
|
|
125
127
|
}
|
|
126
128
|
|
|
@@ -129,6 +131,7 @@ async function withIsolatedSubagentEnv<T>(
|
|
|
129
131
|
} finally {
|
|
130
132
|
for (const [key, value] of originalValues.entries()) {
|
|
131
133
|
if (value === undefined) {
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- process.env cleanup requires dynamic delete
|
|
132
135
|
delete process.env[key];
|
|
133
136
|
} else {
|
|
134
137
|
process.env[key] = value;
|
|
@@ -143,7 +146,7 @@ function createToolCallHarness(
|
|
|
143
146
|
options: ExtensionHarnessOptions = {},
|
|
144
147
|
): ExtensionHarness {
|
|
145
148
|
const baseDir = mkdtempSync(join(tmpdir(), "pi-permission-system-runtime-"));
|
|
146
|
-
const cwd = options.cwd
|
|
149
|
+
const cwd = options.cwd ?? baseDir;
|
|
147
150
|
const prompts: string[] = [];
|
|
148
151
|
const handlers: Record<string, MockHandler> = {};
|
|
149
152
|
const originalAgentDir = process.env.PI_CODING_AGENT_DIR;
|
|
@@ -188,10 +191,7 @@ function createToolCallHarness(
|
|
|
188
191
|
prompts,
|
|
189
192
|
cleanup: async (): Promise<void> => {
|
|
190
193
|
await Promise.resolve(
|
|
191
|
-
handlers.session_shutdown
|
|
192
|
-
{},
|
|
193
|
-
createMockContext(cwd, prompts, options),
|
|
194
|
-
),
|
|
194
|
+
handlers.session_shutdown({}, createMockContext(cwd, prompts, options)),
|
|
195
195
|
);
|
|
196
196
|
rmSync(baseDir, { recursive: true, force: true });
|
|
197
197
|
},
|
|
@@ -236,7 +236,7 @@ async function runToolCall(
|
|
|
236
236
|
handler(event, createMockContext(harness.cwd, harness.prompts, options)),
|
|
237
237
|
),
|
|
238
238
|
);
|
|
239
|
-
return
|
|
239
|
+
return result ?? {};
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
test("Yolo mode only auto-approves ask-state permissions", () => {
|
|
@@ -1515,7 +1515,7 @@ test("REGRESSION: resolveSkillPromptEntries sanitizes every available_skills blo
|
|
|
1515
1515
|
|
|
1516
1516
|
expect(result.prompt).not.toContain("denied-skill");
|
|
1517
1517
|
expect(result.prompt).toContain("visible-skill");
|
|
1518
|
-
expect((result.prompt.match(/<available_skills>/g)
|
|
1518
|
+
expect((result.prompt.match(/<available_skills>/g) ?? []).length).toBe(1);
|
|
1519
1519
|
expect(result.entries.map((entry) => entry.name)).toEqual([
|
|
1520
1520
|
"visible-skill",
|
|
1521
1521
|
]);
|
|
@@ -2371,7 +2371,7 @@ test("session approval: session_shutdown clears session approvals", async () =>
|
|
|
2371
2371
|
hasUI: true,
|
|
2372
2372
|
selectResponse: "Yes",
|
|
2373
2373
|
});
|
|
2374
|
-
await Promise.resolve(harness.handlers.session_shutdown
|
|
2374
|
+
await Promise.resolve(harness.handlers.session_shutdown({}, shutdownCtx));
|
|
2375
2375
|
|
|
2376
2376
|
// Access same path again — should prompt because cache was cleared
|
|
2377
2377
|
const result = await runToolCall(
|