@gotgenes/pi-permission-system 17.0.0 → 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 +7 -0
- package/package.json +1 -1
- package/src/access-intent/access-path.ts +5 -5
- 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/external-directory.ts +14 -15
- package/src/handlers/gates/path.ts +3 -2
- 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 +73 -24
- package/test/access-intent/bash/program.test.ts +131 -65
- 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 -1
- package/test/handlers/gates/bash-path.test.ts +6 -1
- package/test/handlers/gates/external-directory-policy.test.ts +26 -8
- package/test/handlers/gates/external-directory.test.ts +9 -1
- package/test/handlers/gates/path.test.ts +53 -14
- 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 +10 -0
- 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 +12 -3
- 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 -500
|
@@ -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
|
}
|
|
@@ -35,30 +35,37 @@ describe("AccessPath.forPath", () => {
|
|
|
35
35
|
realpathSync.mockImplementation((p: string) =>
|
|
36
36
|
p.startsWith("/tmp") ? `/private${p}` : p,
|
|
37
37
|
);
|
|
38
|
-
expect(
|
|
39
|
-
"/tmp/x",
|
|
40
|
-
|
|
41
|
-
]);
|
|
38
|
+
expect(
|
|
39
|
+
AccessPath.forPath("/tmp/x", { cwd, platform: "linux" }).matchValues(),
|
|
40
|
+
).toEqual(["/tmp/x", "/private/tmp/x"]);
|
|
42
41
|
});
|
|
43
42
|
|
|
44
43
|
test("deduplicates when the canonical form equals the lexical form", () => {
|
|
45
|
-
expect(
|
|
46
|
-
"/etc/hosts",
|
|
47
|
-
|
|
44
|
+
expect(
|
|
45
|
+
AccessPath.forPath("/etc/hosts", {
|
|
46
|
+
cwd,
|
|
47
|
+
platform: "linux",
|
|
48
|
+
}).matchValues(),
|
|
49
|
+
).toEqual(["/etc/hosts"]);
|
|
48
50
|
});
|
|
49
51
|
|
|
50
52
|
test("keeps the relative aliases for an in-cwd token without duplicating", () => {
|
|
51
|
-
expect(
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
expect(
|
|
54
|
+
AccessPath.forPath("src/foo.ts", {
|
|
55
|
+
cwd,
|
|
56
|
+
platform: "linux",
|
|
57
|
+
}).matchValues(),
|
|
58
|
+
).toEqual(["/projects/my-app/src/foo.ts", "src/foo.ts"]);
|
|
55
59
|
});
|
|
56
60
|
|
|
57
61
|
test("includes only the lexical aliases when canonical is empty", () => {
|
|
58
62
|
// Force canonicalizePath to return the original (no-op symlink resolution
|
|
59
63
|
// effectively means canonical === lexical, handled by dedup).
|
|
60
64
|
expect(
|
|
61
|
-
AccessPath.forPath("/etc/hosts", {
|
|
65
|
+
AccessPath.forPath("/etc/hosts", {
|
|
66
|
+
cwd,
|
|
67
|
+
platform: "linux",
|
|
68
|
+
}).matchValues(),
|
|
62
69
|
).not.toHaveLength(0);
|
|
63
70
|
});
|
|
64
71
|
|
|
@@ -68,6 +75,7 @@ describe("AccessPath.forPath", () => {
|
|
|
68
75
|
AccessPath.forPath("foo.ts", {
|
|
69
76
|
cwd,
|
|
70
77
|
resolveBase: "/projects/my-app/sub",
|
|
78
|
+
platform: "linux",
|
|
71
79
|
}).matchValues(),
|
|
72
80
|
).toEqual(["/projects/my-app/sub/foo.ts", "sub/foo.ts", "foo.ts"]);
|
|
73
81
|
});
|
|
@@ -80,6 +88,7 @@ describe("AccessPath.forPath", () => {
|
|
|
80
88
|
AccessPath.forPath("foo.ts", {
|
|
81
89
|
cwd,
|
|
82
90
|
resolveBase: "/projects/my-app/sub",
|
|
91
|
+
platform: "linux",
|
|
83
92
|
}).matchValues(),
|
|
84
93
|
).toEqual([
|
|
85
94
|
"/projects/my-app/sub/foo.ts",
|
|
@@ -90,24 +99,59 @@ describe("AccessPath.forPath", () => {
|
|
|
90
99
|
});
|
|
91
100
|
});
|
|
92
101
|
|
|
102
|
+
describe("platform option", () => {
|
|
103
|
+
test("win32: builds lexical/match/boundary values with win32 rules", () => {
|
|
104
|
+
const ap = AccessPath.forPath("src\\foo.ts", {
|
|
105
|
+
cwd: "C:\\Projects\\App",
|
|
106
|
+
platform: "win32",
|
|
107
|
+
});
|
|
108
|
+
expect(ap.value()).toBe("c:\\projects\\app\\src\\foo.ts");
|
|
109
|
+
expect(ap.boundaryValue()).toBe("c:\\projects\\app\\src\\foo.ts");
|
|
110
|
+
expect(ap.matchValues()).toEqual([
|
|
111
|
+
"c:\\projects\\app\\src\\foo.ts",
|
|
112
|
+
"src\\foo.ts",
|
|
113
|
+
]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("win32: lowercases the symlink-resolved boundary value", () => {
|
|
117
|
+
realpathSync.mockImplementation((p: string) =>
|
|
118
|
+
p === "c:\\projects\\app\\link" ? "C:\\Real\\App" : p,
|
|
119
|
+
);
|
|
120
|
+
expect(
|
|
121
|
+
AccessPath.forPath("link", {
|
|
122
|
+
cwd: "C:\\Projects\\App",
|
|
123
|
+
platform: "win32",
|
|
124
|
+
}).boundaryValue(),
|
|
125
|
+
).toBe("c:\\real\\app");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
93
129
|
describe("boundaryValue()", () => {
|
|
94
130
|
test("returns the canonical (symlink-resolved) form", () => {
|
|
95
131
|
realpathSync.mockImplementation((p: string) =>
|
|
96
132
|
p.startsWith("/tmp") ? `/private${p}` : p,
|
|
97
133
|
);
|
|
98
|
-
expect(
|
|
99
|
-
"/
|
|
100
|
-
|
|
134
|
+
expect(
|
|
135
|
+
AccessPath.forPath("/tmp/x", {
|
|
136
|
+
cwd,
|
|
137
|
+
platform: "linux",
|
|
138
|
+
}).boundaryValue(),
|
|
139
|
+
).toBe("/private/tmp/x");
|
|
101
140
|
});
|
|
102
141
|
|
|
103
142
|
test("returns the lexical form when path has no symlinks", () => {
|
|
104
|
-
expect(
|
|
105
|
-
"/etc/hosts",
|
|
106
|
-
|
|
143
|
+
expect(
|
|
144
|
+
AccessPath.forPath("/etc/hosts", {
|
|
145
|
+
cwd,
|
|
146
|
+
platform: "linux",
|
|
147
|
+
}).boundaryValue(),
|
|
148
|
+
).toBe("/etc/hosts");
|
|
107
149
|
});
|
|
108
150
|
|
|
109
151
|
test("returns empty string for empty input", () => {
|
|
110
|
-
expect(
|
|
152
|
+
expect(
|
|
153
|
+
AccessPath.forPath("", { cwd, platform: "linux" }).boundaryValue(),
|
|
154
|
+
).toBe("");
|
|
111
155
|
});
|
|
112
156
|
});
|
|
113
157
|
|
|
@@ -117,14 +161,16 @@ describe("AccessPath.forPath", () => {
|
|
|
117
161
|
p.startsWith("/tmp") ? `/private${p}` : p,
|
|
118
162
|
);
|
|
119
163
|
// Even when the path resolves to a different canonical, value() stays lexical.
|
|
120
|
-
expect(
|
|
164
|
+
expect(
|
|
165
|
+
AccessPath.forPath("/tmp/x", { cwd, platform: "linux" }).value(),
|
|
166
|
+
).toBe("/tmp/x");
|
|
121
167
|
});
|
|
122
168
|
|
|
123
169
|
test("normalizes the path against cwd", () => {
|
|
124
170
|
// A relative path becomes an absolute lexical value.
|
|
125
|
-
expect(
|
|
126
|
-
"
|
|
127
|
-
);
|
|
171
|
+
expect(
|
|
172
|
+
AccessPath.forPath("src/foo.ts", { cwd, platform: "linux" }).value(),
|
|
173
|
+
).toBe("/projects/my-app/src/foo.ts");
|
|
128
174
|
});
|
|
129
175
|
|
|
130
176
|
test("normalizes a relative path against an explicit resolveBase", () => {
|
|
@@ -132,12 +178,15 @@ describe("AccessPath.forPath", () => {
|
|
|
132
178
|
AccessPath.forPath("foo.ts", {
|
|
133
179
|
cwd,
|
|
134
180
|
resolveBase: "/projects/my-app/sub",
|
|
181
|
+
platform: "linux",
|
|
135
182
|
}).value(),
|
|
136
183
|
).toBe("/projects/my-app/sub/foo.ts");
|
|
137
184
|
});
|
|
138
185
|
|
|
139
186
|
test("returns empty string for empty input", () => {
|
|
140
|
-
expect(AccessPath.forPath("", { cwd }).value()).toBe(
|
|
187
|
+
expect(AccessPath.forPath("", { cwd, platform: "linux" }).value()).toBe(
|
|
188
|
+
"",
|
|
189
|
+
);
|
|
141
190
|
});
|
|
142
191
|
});
|
|
143
192
|
});
|