@gotgenes/pi-permission-system 16.2.1 → 17.1.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/README.md +2 -1
- package/package.json +1 -1
- package/src/access-intent/access-intent.ts +8 -10
- package/src/access-intent/access-path.ts +39 -16
- package/src/access-intent/bash/bash-path-resolver.ts +531 -0
- package/src/access-intent/bash/program.ts +21 -14
- package/src/access-intent/bash/token-classification.ts +1 -1
- package/src/canonicalize-path.ts +11 -5
- package/src/forwarded-permissions/permission-forwarder.ts +11 -1
- package/src/forwarding-manager.ts +7 -1
- package/src/handlers/before-agent-start.ts +1 -0
- package/src/handlers/gates/bash-path-extractor.ts +7 -5
- package/src/handlers/gates/bash-path.ts +13 -13
- package/src/handlers/gates/external-directory.ts +14 -16
- package/src/handlers/gates/path.ts +12 -7
- package/src/handlers/gates/skill-read.ts +7 -1
- package/src/handlers/gates/tool-call-gate-pipeline.ts +21 -4
- package/src/handlers/gates/tool.ts +4 -2
- package/src/index.ts +12 -1
- package/src/input-normalizer.ts +9 -4
- package/src/path-normalizer.ts +70 -0
- package/src/path-utils.ts +39 -28
- package/src/permission-manager.ts +21 -7
- package/src/permission-session.ts +35 -2
- package/src/prompting-gateway.ts +3 -0
- package/src/rule.ts +8 -6
- package/src/skill-prompt-sanitizer.ts +15 -4
- package/src/subagent-context.ts +17 -9
- package/test/access-intent/access-path.test.ts +124 -15
- package/test/access-intent/bash/program.test.ts +178 -80
- package/test/bash-external-directory.test.ts +15 -1
- package/test/canonicalize-path.test.ts +34 -8
- package/test/forwarding-manager.test.ts +7 -1
- package/test/handlers/external-directory-symlink-acceptance.test.ts +26 -4
- package/test/handlers/gates/bash-command-metamorphic.test.ts +5 -1
- package/test/handlers/gates/bash-external-directory.test.ts +5 -2
- package/test/handlers/gates/bash-path.test.ts +14 -9
- package/test/handlers/gates/external-directory-policy.test.ts +28 -18
- package/test/handlers/gates/external-directory.test.ts +9 -1
- package/test/handlers/gates/path.test.ts +90 -20
- package/test/handlers/gates/skill-read.test.ts +22 -13
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +2 -1
- package/test/handlers/gates/tool.test.ts +13 -1
- package/test/helpers/gate-fixtures.ts +11 -2
- package/test/helpers/session-fixtures.ts +3 -0
- package/test/input-normalizer.test.ts +104 -39
- package/test/path-normalizer.test.ts +123 -0
- package/test/path-utils.test.ts +130 -51
- package/test/permission-forwarder.test.ts +1 -0
- package/test/permission-manager-unified.test.ts +40 -0
- package/test/permission-resolver.test.ts +14 -32
- package/test/permission-session.test.ts +41 -0
- package/test/pi-infrastructure-read.test.ts +27 -4
- package/test/prompting-gateway.test.ts +1 -0
- package/test/rule.test.ts +88 -42
- package/test/session-rules.test.ts +21 -4
- package/test/skill-prompt-sanitizer.test.ts +69 -14
- package/test/subagent-context.test.ts +77 -31
- package/test/synthesize.test.ts +13 -11
- package/src/access-intent/bash/cwd-projection.ts +0 -498
package/src/path-utils.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
join,
|
|
3
|
-
normalize,
|
|
4
|
-
posix as posixPath,
|
|
5
|
-
relative,
|
|
6
|
-
resolve,
|
|
7
|
-
win32 as winPath,
|
|
8
|
-
} from "node:path";
|
|
1
|
+
import { join, posix as posixPath, win32 as winPath } from "node:path";
|
|
9
2
|
|
|
10
3
|
import { canonicalizePath } from "./canonicalize-path";
|
|
11
4
|
import { expandHomePath } from "./expand-home";
|
|
@@ -16,6 +9,7 @@ import { wildcardMatch } from "./wildcard-matcher";
|
|
|
16
9
|
export function normalizePathForComparison(
|
|
17
10
|
pathValue: string,
|
|
18
11
|
cwd: string,
|
|
12
|
+
platform: NodeJS.Platform,
|
|
19
13
|
): string {
|
|
20
14
|
const trimmed = pathValue.trim().replace(/^['"]|['"]$/g, "");
|
|
21
15
|
if (!trimmed) {
|
|
@@ -25,9 +19,10 @@ export function normalizePathForComparison(
|
|
|
25
19
|
let normalizedPath = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
|
26
20
|
normalizedPath = expandHomePath(normalizedPath);
|
|
27
21
|
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
22
|
+
const impl = platform === "win32" ? winPath : posixPath;
|
|
23
|
+
const absolutePath = impl.resolve(cwd, normalizedPath);
|
|
24
|
+
const normalizedAbsolutePath = impl.normalize(absolutePath);
|
|
25
|
+
return platform === "win32"
|
|
31
26
|
? normalizedAbsolutePath.toLowerCase()
|
|
32
27
|
: normalizedAbsolutePath;
|
|
33
28
|
}
|
|
@@ -38,13 +33,13 @@ export function normalizePathForComparison(
|
|
|
38
33
|
* Containment is decided with Node's platform-native `path.relative` rather
|
|
39
34
|
* than a hand-rolled prefix check: on `win32` the comparison folds case (and
|
|
40
35
|
* tolerates either separator), matching the case-insensitive filesystem.
|
|
41
|
-
* `platform`
|
|
42
|
-
*
|
|
36
|
+
* `platform` is injected from the composition root so Windows behavior is
|
|
37
|
+
* testable on a POSIX CI.
|
|
43
38
|
*/
|
|
44
39
|
export function isPathWithinDirectory(
|
|
45
40
|
pathValue: string,
|
|
46
41
|
directory: string,
|
|
47
|
-
platform: NodeJS.Platform
|
|
42
|
+
platform: NodeJS.Platform,
|
|
48
43
|
): boolean {
|
|
49
44
|
if (!pathValue || !directory) {
|
|
50
45
|
return false;
|
|
@@ -101,46 +96,56 @@ export function normalizePathPolicyLiteral(pathValue: string): string {
|
|
|
101
96
|
*/
|
|
102
97
|
export function getPathPolicyValues(
|
|
103
98
|
pathValue: string,
|
|
104
|
-
options: PathPolicyValueOptions
|
|
99
|
+
options: PathPolicyValueOptions,
|
|
100
|
+
platform: NodeJS.Platform,
|
|
105
101
|
): string[] {
|
|
106
102
|
const literal = normalizePathPolicyLiteral(pathValue);
|
|
107
103
|
if (!literal) return [];
|
|
108
104
|
if (literal === "*") return ["*"];
|
|
109
105
|
|
|
110
106
|
return [
|
|
111
|
-
...new Set([
|
|
107
|
+
...new Set([
|
|
108
|
+
...getAbsolutePathPolicyValues(pathValue, options, platform),
|
|
109
|
+
literal,
|
|
110
|
+
]),
|
|
112
111
|
];
|
|
113
112
|
}
|
|
114
113
|
|
|
115
114
|
function getAbsolutePathPolicyValues(
|
|
116
115
|
pathValue: string,
|
|
117
116
|
options: PathPolicyValueOptions,
|
|
117
|
+
platform: NodeJS.Platform,
|
|
118
118
|
): string[] {
|
|
119
119
|
const resolveBase = options.resolveBase ?? options.cwd;
|
|
120
120
|
if (!resolveBase) return [];
|
|
121
121
|
|
|
122
|
-
const absolute = normalizePathForComparison(pathValue, resolveBase);
|
|
122
|
+
const absolute = normalizePathForComparison(pathValue, resolveBase, platform);
|
|
123
123
|
if (!absolute) return [];
|
|
124
124
|
|
|
125
|
-
return [
|
|
125
|
+
return [
|
|
126
|
+
absolute,
|
|
127
|
+
...getCwdRelativePathPolicyValues(absolute, options.cwd, platform),
|
|
128
|
+
];
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
function getCwdRelativePathPolicyValues(
|
|
129
132
|
absolute: string,
|
|
130
133
|
cwd: string | undefined,
|
|
134
|
+
platform: NodeJS.Platform,
|
|
131
135
|
): string[] {
|
|
132
136
|
if (!cwd) return [];
|
|
133
137
|
|
|
134
|
-
const normalizedCwd = normalizePathForComparison(cwd, cwd);
|
|
138
|
+
const normalizedCwd = normalizePathForComparison(cwd, cwd, platform);
|
|
135
139
|
if (!normalizedCwd) return [];
|
|
136
140
|
if (
|
|
137
141
|
absolute !== normalizedCwd &&
|
|
138
|
-
!isPathWithinDirectory(absolute, normalizedCwd)
|
|
142
|
+
!isPathWithinDirectory(absolute, normalizedCwd, platform)
|
|
139
143
|
) {
|
|
140
144
|
return [];
|
|
141
145
|
}
|
|
142
146
|
|
|
143
|
-
const
|
|
147
|
+
const impl = platform === "win32" ? winPath : posixPath;
|
|
148
|
+
const relativeValue = impl.relative(normalizedCwd, absolute);
|
|
144
149
|
return relativeValue ? [relativeValue] : [];
|
|
145
150
|
}
|
|
146
151
|
|
|
@@ -253,26 +258,32 @@ export function getToolInputPath(
|
|
|
253
258
|
export function canonicalNormalizePathForComparison(
|
|
254
259
|
pathValue: string,
|
|
255
260
|
cwd: string,
|
|
261
|
+
platform: NodeJS.Platform,
|
|
256
262
|
): string {
|
|
257
|
-
const lexical = normalizePathForComparison(pathValue, cwd);
|
|
263
|
+
const lexical = normalizePathForComparison(pathValue, cwd, platform);
|
|
258
264
|
if (!lexical) return "";
|
|
259
|
-
const canonical = canonicalizePath(lexical);
|
|
260
|
-
return
|
|
265
|
+
const canonical = canonicalizePath(lexical, platform);
|
|
266
|
+
return platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
261
267
|
}
|
|
262
268
|
|
|
263
269
|
export function isPathOutsideWorkingDirectory(
|
|
264
270
|
pathValue: string,
|
|
265
271
|
cwd: string,
|
|
272
|
+
platform: NodeJS.Platform,
|
|
266
273
|
): boolean {
|
|
267
|
-
const normalizedCwd = canonicalNormalizePathForComparison(cwd, cwd);
|
|
268
|
-
const normalizedPath = canonicalNormalizePathForComparison(
|
|
274
|
+
const normalizedCwd = canonicalNormalizePathForComparison(cwd, cwd, platform);
|
|
275
|
+
const normalizedPath = canonicalNormalizePathForComparison(
|
|
276
|
+
pathValue,
|
|
277
|
+
cwd,
|
|
278
|
+
platform,
|
|
279
|
+
);
|
|
269
280
|
if (!normalizedCwd || !normalizedPath) {
|
|
270
281
|
return false;
|
|
271
282
|
}
|
|
272
283
|
if (isSafeSystemPath(normalizedPath)) {
|
|
273
284
|
return false;
|
|
274
285
|
}
|
|
275
|
-
return !isPathWithinDirectory(normalizedPath, normalizedCwd);
|
|
286
|
+
return !isPathWithinDirectory(normalizedPath, normalizedCwd, platform);
|
|
276
287
|
}
|
|
277
288
|
|
|
278
289
|
function containsGlobChars(value: string): boolean {
|
|
@@ -299,7 +310,7 @@ export function isPiInfrastructureRead(
|
|
|
299
310
|
normalizedPath: string,
|
|
300
311
|
infrastructureDirs: readonly string[],
|
|
301
312
|
cwd: string,
|
|
302
|
-
platform: NodeJS.Platform
|
|
313
|
+
platform: NodeJS.Platform,
|
|
303
314
|
): boolean {
|
|
304
315
|
if (!READ_ONLY_PATH_BEARING_TOOLS.has(toolName)) {
|
|
305
316
|
return false;
|
|
@@ -85,10 +85,17 @@ export interface PermissionManagerOptions extends PolicyLoaderOptions {
|
|
|
85
85
|
* from this value and supports {@link PermissionManager.configureForCwd}.
|
|
86
86
|
*/
|
|
87
87
|
agentDir?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Host platform, injected from the composition root, that decides whether
|
|
90
|
+
* path-surface rule matching folds case (and separators) on Windows.
|
|
91
|
+
* Defaults to a POSIX flavor; production always supplies the real platform.
|
|
92
|
+
*/
|
|
93
|
+
platform?: NodeJS.Platform;
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
export class PermissionManager implements ScopedPermissionManager {
|
|
91
97
|
private readonly agentDir: string | undefined;
|
|
98
|
+
private readonly platform: NodeJS.Platform;
|
|
92
99
|
private currentCwd: string | undefined;
|
|
93
100
|
private loader: PolicyLoader;
|
|
94
101
|
private readonly resolvedPermissionsCache = new Map<
|
|
@@ -98,6 +105,7 @@ export class PermissionManager implements ScopedPermissionManager {
|
|
|
98
105
|
|
|
99
106
|
constructor(options: PermissionManagerOptions = {}) {
|
|
100
107
|
this.agentDir = options.agentDir;
|
|
108
|
+
this.platform = options.platform ?? "linux";
|
|
101
109
|
this.loader =
|
|
102
110
|
options.policyLoader ??
|
|
103
111
|
new FilePolicyLoader(
|
|
@@ -216,23 +224,25 @@ export class PermissionManager implements ScopedPermissionManager {
|
|
|
216
224
|
|
|
217
225
|
// Special surfaces (external_directory): evaluate directly by surface name.
|
|
218
226
|
if (SPECIAL_PERMISSION_KEYS.has(normalizedToolName)) {
|
|
219
|
-
return evaluate(normalizedToolName, "*", composedRules)
|
|
227
|
+
return evaluate(normalizedToolName, "*", composedRules, this.platform)
|
|
228
|
+
.action;
|
|
220
229
|
}
|
|
221
230
|
|
|
222
231
|
// Bash, MCP, skill: evaluate with "*" value — the per-surface catch-all
|
|
223
232
|
// (or universal default) handles this correctly.
|
|
224
233
|
if (normalizedToolName === "bash") {
|
|
225
|
-
return evaluate("bash", "*", composedRules).action;
|
|
234
|
+
return evaluate("bash", "*", composedRules, this.platform).action;
|
|
226
235
|
}
|
|
227
236
|
if (normalizedToolName === "mcp") {
|
|
228
|
-
return evaluate("mcp", "*", composedRules).action;
|
|
237
|
+
return evaluate("mcp", "*", composedRules, this.platform).action;
|
|
229
238
|
}
|
|
230
239
|
if (normalizedToolName === "skill") {
|
|
231
|
-
return evaluate("skill", "*", composedRules).action;
|
|
240
|
+
return evaluate("skill", "*", composedRules, this.platform).action;
|
|
232
241
|
}
|
|
233
242
|
|
|
234
243
|
// Tool-name surfaces (read, write, etc. and extension tools).
|
|
235
|
-
return evaluate(normalizedToolName, "*", composedRules)
|
|
244
|
+
return evaluate(normalizedToolName, "*", composedRules, this.platform)
|
|
245
|
+
.action;
|
|
236
246
|
}
|
|
237
247
|
|
|
238
248
|
/**
|
|
@@ -260,6 +270,7 @@ export class PermissionManager implements ScopedPermissionManager {
|
|
|
260
270
|
intent.surface,
|
|
261
271
|
intent.surface,
|
|
262
272
|
fullRules,
|
|
273
|
+
this.platform,
|
|
263
274
|
);
|
|
264
275
|
}
|
|
265
276
|
|
|
@@ -269,6 +280,7 @@ export class PermissionManager implements ScopedPermissionManager {
|
|
|
269
280
|
toolName,
|
|
270
281
|
intent.input,
|
|
271
282
|
this.loader.getConfiguredMcpServerNames(),
|
|
283
|
+
this.platform,
|
|
272
284
|
this.currentCwd,
|
|
273
285
|
);
|
|
274
286
|
return buildCheckResult(
|
|
@@ -278,6 +290,7 @@ export class PermissionManager implements ScopedPermissionManager {
|
|
|
278
290
|
toolName,
|
|
279
291
|
intent.surface,
|
|
280
292
|
fullRules,
|
|
293
|
+
this.platform,
|
|
281
294
|
);
|
|
282
295
|
}
|
|
283
296
|
}
|
|
@@ -296,10 +309,11 @@ function buildCheckResult(
|
|
|
296
309
|
normalizedToolName: string,
|
|
297
310
|
toolName: string,
|
|
298
311
|
fullRules: Ruleset,
|
|
312
|
+
platform: NodeJS.Platform,
|
|
299
313
|
): PermissionCheckResult {
|
|
300
314
|
const { rule, value } = PATH_SURFACES.has(surface)
|
|
301
|
-
? evaluateAnyValue(surface, values, fullRules)
|
|
302
|
-
: evaluateFirst(surface, values, fullRules);
|
|
315
|
+
? evaluateAnyValue(surface, values, fullRules, platform)
|
|
316
|
+
: evaluateFirst(surface, values, fullRules, platform);
|
|
303
317
|
|
|
304
318
|
// For MCP, replace the normalizer's fallback target with the actual
|
|
305
319
|
// matched candidate value so PermissionCheckResult.target is accurate.
|
|
@@ -9,6 +9,7 @@ import type { PermissionSystemExtensionConfig } from "./extension-config";
|
|
|
9
9
|
import type { ExtensionPaths } from "./extension-paths";
|
|
10
10
|
import type { ForwardingController } from "./forwarding-manager";
|
|
11
11
|
import type { ToolCallGateInputs } from "./handlers/gates/tool-call-gate-pipeline";
|
|
12
|
+
import { PathNormalizer } from "./path-normalizer";
|
|
12
13
|
import type { ScopedPermissionManager } from "./permission-manager";
|
|
13
14
|
import type { PromptingGatewayLifecycle } from "./prompting-gateway";
|
|
14
15
|
|
|
@@ -37,6 +38,7 @@ export class PermissionSession implements ToolCallGateInputs {
|
|
|
37
38
|
private context: ExtensionContext | null = null;
|
|
38
39
|
private skillEntries: SkillPromptEntry[] = [];
|
|
39
40
|
private knownAgentName: string | null = null;
|
|
41
|
+
private pathNormalizer: PathNormalizer;
|
|
40
42
|
|
|
41
43
|
constructor(
|
|
42
44
|
private readonly paths: ExtensionPaths,
|
|
@@ -45,13 +47,28 @@ export class PermissionSession implements ToolCallGateInputs {
|
|
|
45
47
|
private readonly sessionRules: SessionRules,
|
|
46
48
|
private readonly configStore: SessionConfigStore,
|
|
47
49
|
private readonly gateway: PromptingGatewayLifecycle,
|
|
48
|
-
|
|
50
|
+
private readonly platform: NodeJS.Platform,
|
|
51
|
+
) {
|
|
52
|
+
// Placeholder until the first activate(ctx) binds the real cwd; every gate
|
|
53
|
+
// evaluate runs after activate (handleToolCall activates first), so this
|
|
54
|
+
// empty-cwd value is never read.
|
|
55
|
+
this.pathNormalizer = new PathNormalizer(platform, "");
|
|
56
|
+
}
|
|
49
57
|
|
|
50
58
|
// ── Context lifecycle ──────────────────────────────────────────────────
|
|
51
59
|
|
|
52
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Store the current extension context, rebuild the path normalizer for its
|
|
62
|
+
* cwd, start forwarding, and activate the gateway.
|
|
63
|
+
*
|
|
64
|
+
* The normalizer is (re)built here rather than only at `resetForNewSession`
|
|
65
|
+
* so it always tracks the active context's cwd — `ctx.cwd` is stable within a
|
|
66
|
+
* session, so this is a no-op rebuild in production, but it closes the
|
|
67
|
+
* fail-open gap if a tool call ever arrives before `session_start`.
|
|
68
|
+
*/
|
|
53
69
|
activate(ctx: ExtensionContext): void {
|
|
54
70
|
this.context = ctx;
|
|
71
|
+
this.pathNormalizer = new PathNormalizer(this.platform, ctx.cwd);
|
|
55
72
|
this.forwarding.start(ctx);
|
|
56
73
|
this.gateway.activate(ctx);
|
|
57
74
|
}
|
|
@@ -68,6 +85,11 @@ export class PermissionSession implements ToolCallGateInputs {
|
|
|
68
85
|
return this.context;
|
|
69
86
|
}
|
|
70
87
|
|
|
88
|
+
/** The host platform injected at the composition root. */
|
|
89
|
+
getPlatform(): NodeJS.Platform {
|
|
90
|
+
return this.platform;
|
|
91
|
+
}
|
|
92
|
+
|
|
71
93
|
// ── UI notifications ────────────────────────────────────────────────────
|
|
72
94
|
|
|
73
95
|
/** Surface a warning message to the user via the active UI context, if any. */
|
|
@@ -186,4 +208,15 @@ export class PermissionSession implements ToolCallGateInputs {
|
|
|
186
208
|
getToolPreviewLimits(): ToolPreviewFormatterOptions {
|
|
187
209
|
return resolveToolPreviewLimits(this.config);
|
|
188
210
|
}
|
|
211
|
+
|
|
212
|
+
// ── Path normalization ────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The session's {@link PathNormalizer}, carrying the host platform and the
|
|
216
|
+
* session cwd. Rebuilt on every `resetForNewSession` so a session switch
|
|
217
|
+
* rebinds the cwd.
|
|
218
|
+
*/
|
|
219
|
+
getPathNormalizer(): PathNormalizer {
|
|
220
|
+
return this.pathNormalizer;
|
|
221
|
+
}
|
|
189
222
|
}
|
package/src/prompting-gateway.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface PromptingGatewayDeps {
|
|
|
23
23
|
config: ConfigReader;
|
|
24
24
|
/** Static path used to detect a forwarding subagent context. */
|
|
25
25
|
subagentSessionsDir: string;
|
|
26
|
+
/** Host platform, injected from the composition root, for subagent-context path detection. */
|
|
27
|
+
platform: NodeJS.Platform;
|
|
26
28
|
/** Process-global registry used to detect a registered child session. */
|
|
27
29
|
registry?: SubagentSessionRegistry;
|
|
28
30
|
/** Resolves the permission decision: direct UI dialog or forwarded to parent. */
|
|
@@ -82,6 +84,7 @@ export class PromptingGateway
|
|
|
82
84
|
isSubagent: isSubagentExecutionContext(
|
|
83
85
|
this.context,
|
|
84
86
|
this.deps.subagentSessionsDir,
|
|
87
|
+
this.deps.platform,
|
|
85
88
|
this.deps.registry,
|
|
86
89
|
),
|
|
87
90
|
});
|
package/src/rule.ts
CHANGED
|
@@ -54,8 +54,8 @@ export function evaluate(
|
|
|
54
54
|
surface: string,
|
|
55
55
|
pattern: string,
|
|
56
56
|
rules: Ruleset,
|
|
57
|
+
platform: NodeJS.Platform,
|
|
57
58
|
defaultAction?: PermissionState,
|
|
58
|
-
platform: NodeJS.Platform = process.platform,
|
|
59
59
|
): Rule {
|
|
60
60
|
const rule = rules.findLast((r) =>
|
|
61
61
|
ruleMatches(r, surface, pattern, platform),
|
|
@@ -124,10 +124,11 @@ export function evaluateMostRestrictive(
|
|
|
124
124
|
surface: string,
|
|
125
125
|
values: string[],
|
|
126
126
|
rules: Ruleset,
|
|
127
|
+
platform: NodeJS.Platform,
|
|
127
128
|
): { rule: Rule; value: string } | null {
|
|
128
129
|
let worst: { rule: Rule; value: string } | null = null;
|
|
129
130
|
for (const value of values) {
|
|
130
|
-
const rule = evaluate(surface, value, rules);
|
|
131
|
+
const rule = evaluate(surface, value, rules, platform);
|
|
131
132
|
if (rule.action === "deny") return { rule, value };
|
|
132
133
|
if (rule.action === "ask" && worst?.rule.action !== "ask") {
|
|
133
134
|
worst = { rule, value };
|
|
@@ -140,9 +141,10 @@ export function evaluateFirst(
|
|
|
140
141
|
surface: string,
|
|
141
142
|
values: string[],
|
|
142
143
|
rules: Ruleset,
|
|
144
|
+
platform: NodeJS.Platform,
|
|
143
145
|
): { rule: Rule; value: string } {
|
|
144
146
|
for (const value of values) {
|
|
145
|
-
const rule = evaluate(surface, value, rules);
|
|
147
|
+
const rule = evaluate(surface, value, rules, platform);
|
|
146
148
|
if (rule.layer !== "default") {
|
|
147
149
|
return { rule, value };
|
|
148
150
|
}
|
|
@@ -150,7 +152,7 @@ export function evaluateFirst(
|
|
|
150
152
|
// All candidates matched only the synthesized default — use the first.
|
|
151
153
|
const fallbackValue = values[0] ?? "*";
|
|
152
154
|
return {
|
|
153
|
-
rule: evaluate(surface, fallbackValue, rules),
|
|
155
|
+
rule: evaluate(surface, fallbackValue, rules, platform),
|
|
154
156
|
value: fallbackValue,
|
|
155
157
|
};
|
|
156
158
|
}
|
|
@@ -167,7 +169,7 @@ export function evaluateAnyValue(
|
|
|
167
169
|
surface: string,
|
|
168
170
|
values: string[],
|
|
169
171
|
rules: Ruleset,
|
|
170
|
-
platform: NodeJS.Platform
|
|
172
|
+
platform: NodeJS.Platform,
|
|
171
173
|
): { rule: Rule; value: string } {
|
|
172
174
|
const fallbackValue = values[0] ?? "*";
|
|
173
175
|
const rule = rules.findLast((r) =>
|
|
@@ -182,7 +184,7 @@ export function evaluateAnyValue(
|
|
|
182
184
|
};
|
|
183
185
|
}
|
|
184
186
|
return {
|
|
185
|
-
rule: evaluate(surface, fallbackValue, rules),
|
|
187
|
+
rule: evaluate(surface, fallbackValue, rules, platform),
|
|
186
188
|
value: fallbackValue,
|
|
187
189
|
};
|
|
188
190
|
}
|
|
@@ -152,14 +152,23 @@ function createResolvedSkillEntry(
|
|
|
152
152
|
entry: ParsedSkillPromptEntry,
|
|
153
153
|
state: PermissionState,
|
|
154
154
|
cwd: string,
|
|
155
|
+
platform: NodeJS.Platform,
|
|
155
156
|
): SkillPromptEntry {
|
|
156
157
|
return {
|
|
157
158
|
name: entry.name,
|
|
158
159
|
description: entry.description,
|
|
159
160
|
location: entry.location,
|
|
160
161
|
state,
|
|
161
|
-
normalizedLocation: normalizePathForComparison(
|
|
162
|
-
|
|
162
|
+
normalizedLocation: normalizePathForComparison(
|
|
163
|
+
entry.location,
|
|
164
|
+
cwd,
|
|
165
|
+
platform,
|
|
166
|
+
),
|
|
167
|
+
normalizedBaseDir: normalizePathForComparison(
|
|
168
|
+
dirname(entry.location),
|
|
169
|
+
cwd,
|
|
170
|
+
platform,
|
|
171
|
+
),
|
|
163
172
|
};
|
|
164
173
|
}
|
|
165
174
|
|
|
@@ -190,6 +199,7 @@ export function resolveSkillPromptEntries(
|
|
|
190
199
|
permissionManager: SkillPermissionChecker,
|
|
191
200
|
agentName: string | null,
|
|
192
201
|
cwd: string,
|
|
202
|
+
platform: NodeJS.Platform,
|
|
193
203
|
): { prompt: string; entries: SkillPromptEntry[] } {
|
|
194
204
|
const sections = parseAllSkillPromptSections(prompt);
|
|
195
205
|
if (sections.length === 0) {
|
|
@@ -209,7 +219,7 @@ export function resolveSkillPromptEntries(
|
|
|
209
219
|
agentName,
|
|
210
220
|
permissionCache,
|
|
211
221
|
);
|
|
212
|
-
return createResolvedSkillEntry(entry, state, cwd);
|
|
222
|
+
return createResolvedSkillEntry(entry, state, cwd, platform);
|
|
213
223
|
});
|
|
214
224
|
|
|
215
225
|
const visibleSectionEntries = resolvedEntries.filter(
|
|
@@ -257,6 +267,7 @@ export function resolveSkillPromptEntries(
|
|
|
257
267
|
export function findSkillPathMatch(
|
|
258
268
|
normalizedPath: string,
|
|
259
269
|
entries: readonly SkillPromptEntry[],
|
|
270
|
+
platform: NodeJS.Platform,
|
|
260
271
|
): SkillPromptEntry | null {
|
|
261
272
|
if (!normalizedPath || entries.length === 0) {
|
|
262
273
|
return null;
|
|
@@ -275,7 +286,7 @@ export function findSkillPathMatch(
|
|
|
275
286
|
for (const entry of entries) {
|
|
276
287
|
if (
|
|
277
288
|
!entry.normalizedBaseDir ||
|
|
278
|
-
!isPathWithinDirectory(normalizedPath, entry.normalizedBaseDir)
|
|
289
|
+
!isPathWithinDirectory(normalizedPath, entry.normalizedBaseDir, platform)
|
|
279
290
|
) {
|
|
280
291
|
continue;
|
|
281
292
|
}
|
package/src/subagent-context.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { posix as posixPath, win32 as winPath } from "node:path";
|
|
2
2
|
|
|
3
3
|
import { SUBAGENT_ENV_HINT_KEYS } from "./permission-forwarding";
|
|
4
4
|
import type { SubagentSessionRegistry } from "./subagent-registry";
|
|
@@ -15,16 +15,19 @@ export interface SubagentDetectionContext {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function normalizeFilesystemPath(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
export function normalizeFilesystemPath(
|
|
19
|
+
pathValue: string,
|
|
20
|
+
platform: NodeJS.Platform,
|
|
21
|
+
): string {
|
|
22
|
+
const impl = platform === "win32" ? winPath : posixPath;
|
|
23
|
+
const normalizedPath = impl.normalize(pathValue);
|
|
24
|
+
return platform === "win32" ? normalizedPath.toLowerCase() : normalizedPath;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
function isPathWithinDirectoryForSubagent(
|
|
26
28
|
pathValue: string,
|
|
27
29
|
directory: string,
|
|
30
|
+
platform: NodeJS.Platform,
|
|
28
31
|
): boolean {
|
|
29
32
|
if (!pathValue || !directory) {
|
|
30
33
|
return false;
|
|
@@ -34,7 +37,7 @@ function isPathWithinDirectoryForSubagent(
|
|
|
34
37
|
return true;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
const sep =
|
|
40
|
+
const sep = platform === "win32" ? "\\" : "/";
|
|
38
41
|
const prefix = directory.endsWith(sep) ? directory : `${directory}${sep}`;
|
|
39
42
|
return pathValue.startsWith(prefix);
|
|
40
43
|
}
|
|
@@ -68,6 +71,7 @@ export function isRegisteredSubagentChild(
|
|
|
68
71
|
export function isSubagentExecutionContext(
|
|
69
72
|
ctx: SubagentDetectionContext,
|
|
70
73
|
subagentSessionsDir: string,
|
|
74
|
+
platform: NodeJS.Platform,
|
|
71
75
|
registry?: SubagentSessionRegistry,
|
|
72
76
|
): boolean {
|
|
73
77
|
// 1. Explicit registry — in-process subagent extensions register by child
|
|
@@ -95,10 +99,14 @@ export function isSubagentExecutionContext(
|
|
|
95
99
|
return false;
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
const normalizedSessionDir = normalizeFilesystemPath(sessionDir);
|
|
99
|
-
const normalizedSubagentRoot = normalizeFilesystemPath(
|
|
102
|
+
const normalizedSessionDir = normalizeFilesystemPath(sessionDir, platform);
|
|
103
|
+
const normalizedSubagentRoot = normalizeFilesystemPath(
|
|
104
|
+
subagentSessionsDir,
|
|
105
|
+
platform,
|
|
106
|
+
);
|
|
100
107
|
return isPathWithinDirectoryForSubagent(
|
|
101
108
|
normalizedSessionDir,
|
|
102
109
|
normalizedSubagentRoot,
|
|
110
|
+
platform,
|
|
103
111
|
);
|
|
104
112
|
}
|