vibe-coding-master 0.8.13 → 0.9.1
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/README.md +7 -3
- package/dist/backend/adapters/claude-adapter.js +2 -1
- package/dist/backend/cli/install-vcm-harness.js +66 -16
- package/dist/backend/gateway/gateway-service.js +5 -3
- package/dist/backend/runtime/terminal-submit.js +1 -0
- package/dist/backend/server.js +14 -4
- package/dist/backend/services/app-settings-service.js +3 -2
- package/dist/backend/services/auto-memory-service.js +4 -3
- package/dist/backend/services/claude-hook-service.js +6 -0
- package/dist/backend/services/claude-transcript-service.js +2 -13
- package/dist/backend/services/claude-version-check.js +87 -0
- package/dist/backend/services/gate-review-service.js +46 -8
- package/dist/backend/services/harness-service.js +41 -20
- package/dist/backend/services/message-service.js +30 -6
- package/dist/backend/services/project-service.js +8 -2
- package/dist/backend/services/runtime-recovery-service.js +36 -9
- package/dist/backend/services/session-service.js +10 -19
- package/dist/backend/services/task-close-service.js +1 -0
- package/dist/backend/services/terminal-process-exit-service.js +4 -1
- package/dist/backend/services/workflow-control-service.js +55 -11
- package/dist/backend/templates/harness/architect-worker-frontmatter.js +33 -0
- package/dist/backend/templates/harness/check-scaffold-ledger.js +480 -511
- package/dist/backend/templates/harness/gate-review.js +676 -643
- package/dist/backend/templates/harness/node-tool-runtime.js +102 -0
- package/dist/backend/templates/harness/project-owned-history.js +21 -0
- package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +49 -55
- package/dist/backend/templates/harness/restart-architect-skill.js +49 -58
- package/dist/backend/templates/harness/vcm-ask-user-skill.js +57 -59
- package/dist/backend/templates/harness/vcm-task-state-skill.js +66 -72
- package/dist/shared/types/app-settings.js +31 -0
- package/dist-frontend/assets/index-Bs4g_1yF.css +32 -0
- package/dist-frontend/assets/{index-DqANomt1.js → index-CjM9OqZh.js} +41 -41
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/harness-tools/check-durable-docs +332 -298
- package/scripts/harness-tools/generate-module-index +1047 -512
- package/scripts/harness-tools/generate-public-surface +1231 -1069
- package/scripts/harness-tools/package.json +3 -0
- package/scripts/harness-tools/run-long-check +608 -531
- package/scripts/harness-tools/vcm-artifact +181 -113
- package/scripts/harness-tools/vcm-bash-guard +839 -605
- package/scripts/harness-tools/vcm-subagent-guard +67 -63
- package/scripts/harness-tools/watch-job +313 -238
- package/dist-frontend/assets/index-xgiQeUD5.css +0 -32
|
@@ -1,605 +1,839 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
""
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// VCM PreToolUse guard for supervised tools inside VCM workflow-role sessions.
|
|
4
|
+
//
|
|
5
|
+
// Reads the Claude Code PreToolUse hook payload on stdin. It enforces supervised
|
|
6
|
+
// Bash execution and managed-artifact writes for VCM workflow roles. Tool roles
|
|
7
|
+
// are outside the workflow and bypass this guard.
|
|
8
|
+
//
|
|
9
|
+
// Quoted payloads of `sh -c` / `bash -lc` style invocations are executable
|
|
10
|
+
// shell code, so they are scanned recursively. `.ai/tools/run-long-check` is the
|
|
11
|
+
// only sanctioned detached worker; the command it runs must still stay in the
|
|
12
|
+
// supervised foreground process group.
|
|
13
|
+
|
|
14
|
+
const fs = require("node:fs");
|
|
15
|
+
|
|
16
|
+
const WORKFLOW_ROLES = new Set(["project-manager", "architect", "coder", "tester", "reviewer"]);
|
|
17
|
+
|
|
18
|
+
const SKILL_HINT = "Use the vcm-long-running-validation skill instead: "
|
|
19
|
+
+ "`.ai/tools/run-long-check --timeout <duration> -- <command>` then "
|
|
20
|
+
+ "`.ai/tools/watch-job <job-id>` in the same turn, repeating watch-job "
|
|
21
|
+
+ "until it reports a terminal result.";
|
|
22
|
+
const ARTIFACT_HINT = "VCM-managed Markdown cannot be written directly. Write a candidate outside "
|
|
23
|
+
+ ".ai/vcm, then run .ai/tools/vcm-artifact <kind> --file <candidate> "
|
|
24
|
+
+ "--mode <draft|final>.";
|
|
25
|
+
const MAX_NESTED_SHELL_DEPTH = 3;
|
|
26
|
+
const QUOTED = /'([^']*)'|"([^"]*)"/g;
|
|
27
|
+
const SHELL_DASH_C = /(?:^|[\s;&|(])(?:sh|bash|zsh|dash|ksh)\s+(?:-\w+\s+)*-\w*c\w*(?:\s|$)/;
|
|
28
|
+
const PROTECTED_TOOL_INVOCATION = new RegExp(
|
|
29
|
+
"(?:^|[;&|()\\n`])\\s*"
|
|
30
|
+
+ "(?:(?:[A-Za-z_][A-Za-z0-9_]*=[^\\s;&|()]+)\\s+)*"
|
|
31
|
+
+ "(?:[^\\s;&|()]*/)?(?:run-long-check|watch-job)(?=\\s|$)"
|
|
32
|
+
);
|
|
33
|
+
const HEREDOC_START = /<<(?!<)(-?)\s*(?:(['"])([^'"]+)\2|([^\s;&|()<>]+))/g;
|
|
34
|
+
const EXECUTABLE_HEREDOC_RECEIVER = new RegExp(
|
|
35
|
+
"(?:^|[;&|()])\\s*"
|
|
36
|
+
+ "(?:(?:env\\s+)?(?:[A-Za-z_][A-Za-z0-9_]*=[^\\s;&|()]+\\s+)*)"
|
|
37
|
+
+ "(?:[^\\s;&|()]*/)?(?<receiver>sh|bash|zsh|dash|ksh|python\\d*(?:\\.\\d+)?|node)(?=\\s|$)"
|
|
38
|
+
);
|
|
39
|
+
const SHELL_HEREDOC_RECEIVERS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
|
|
40
|
+
const SHELL_CONTROL_OPERATOR = /(?:\|&?|&&|\|\||;|\n|\(|\)|`)/;
|
|
41
|
+
const RUN_LONG_CHECK_SHELL_STRING = new RegExp(
|
|
42
|
+
"(?:^|[;&|()\\n])\\s*"
|
|
43
|
+
+ "(?:[^\\s;&|()]*/)?run-long-check\\b.*?\\s--\\s+"
|
|
44
|
+
+ "(?:env(?:\\s+[A-Za-z_][A-Za-z0-9_]*=[^\\s]+)*\\s+)?"
|
|
45
|
+
+ "(?:[^\\s;&|()]*/)?(?:sh|bash|zsh|dash|ksh)\\s+"
|
|
46
|
+
+ "(?:-\\w+\\s+)*-\\w*c\\w*(?:\\s|$)"
|
|
47
|
+
);
|
|
48
|
+
const COMMAND_SUBSTITUTION = /\$\(([^()]*)\)|`([^`]*)`/g;
|
|
49
|
+
const MANAGED_ARTIFACT_PATH = new RegExp(
|
|
50
|
+
"(?:^|[\\s/\\\\])\\.ai[/\\\\]vcm[/\\\\](?:"
|
|
51
|
+
+ "handoffs[/\\\\].*\\.md|"
|
|
52
|
+
+ "gate-reviews[/\\\\]requests[/\\\\].*\\.report\\.md|"
|
|
53
|
+
+ "memory-review[/\\\\].*\\.md|"
|
|
54
|
+
+ "coder-workers[/\\\\]reports[/\\\\].*\\.md|"
|
|
55
|
+
+ "harness-feedback[/\\\\]pending[/\\\\].*\\.md"
|
|
56
|
+
+ ")"
|
|
57
|
+
);
|
|
58
|
+
const SHELL_SEPARATOR_TOKEN = /^[;&|()]+$/;
|
|
59
|
+
const SHELL_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
60
|
+
const PYTHON_PATH_MUTATION = /\bPath\s*\(\s*(['"])(?<path>[^'"]+)\1\s*\)\s*\.\s*(?:write_text|write_bytes|unlink|rmdir|touch|rename|replace)\s*\(/g;
|
|
61
|
+
const PYTHON_OPEN = /\bopen\s*\(\s*(['"])(?<path>[^'"]+)\1\s*,\s*(['"])(?<mode>[^'"]+)\3/g;
|
|
62
|
+
const NODE_PATH_MUTATION = new RegExp(
|
|
63
|
+
"\\b(?:writeFile|writeFileSync|appendFile|appendFileSync|unlink|unlinkSync|"
|
|
64
|
+
+ "rm|rmSync|rmdir|rmdirSync|createWriteStream)\\s*\\(\\s*"
|
|
65
|
+
+ "(['\"])(?<path>[^'\"]+)\\1",
|
|
66
|
+
"g"
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
function stripQuoted(command) {
|
|
70
|
+
return command.replace(QUOTED, " ");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function quotedSegments(command) {
|
|
74
|
+
return [...command.matchAll(QUOTED)].map((match) => match[1] ?? match[2] ?? "");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function doubleQuotedCommandSubstitutions(command) {
|
|
78
|
+
const substitutions = [];
|
|
79
|
+
for (const match of command.matchAll(QUOTED)) {
|
|
80
|
+
const double = match[2];
|
|
81
|
+
if (match[1] !== undefined || !double) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
for (const substitution of double.matchAll(COMMAND_SUBSTITUTION)) {
|
|
85
|
+
substitutions.push(substitution[1] ?? substitution[2] ?? "");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return substitutions;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function stripEscapedCharacters(command) {
|
|
92
|
+
let cleaned = "";
|
|
93
|
+
let index = 0;
|
|
94
|
+
while (index < command.length) {
|
|
95
|
+
if (command[index] === "\\" && index + 1 < command.length) {
|
|
96
|
+
cleaned += " ";
|
|
97
|
+
index += 2;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
cleaned += command[index];
|
|
101
|
+
index += 1;
|
|
102
|
+
}
|
|
103
|
+
return cleaned;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function heredocReceiverLanguage(line) {
|
|
107
|
+
const match = EXECUTABLE_HEREDOC_RECEIVER.exec(line);
|
|
108
|
+
if (!match) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const receiver = match.groups.receiver;
|
|
112
|
+
if (SHELL_HEREDOC_RECEIVERS.has(receiver)) {
|
|
113
|
+
return "shell";
|
|
114
|
+
}
|
|
115
|
+
return receiver.startsWith("python") ? "python" : "node";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function splitLinesKeepEnds(text) {
|
|
119
|
+
return text.match(/[^\r\n]*(?:\r\n|\r|\n)|[^\r\n]+$/g) ?? [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function endsWithNewline(text) {
|
|
123
|
+
return text.endsWith("\n") || text.endsWith("\r");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Returns shell-only text, all executable text, and typed interpreter bodies.
|
|
127
|
+
function parseHeredocContent(command) {
|
|
128
|
+
const pending = [];
|
|
129
|
+
let shellExecutable = "";
|
|
130
|
+
let allExecutable = "";
|
|
131
|
+
const interpreterSections = [];
|
|
132
|
+
for (const line of splitLinesKeepEnds(command)) {
|
|
133
|
+
if (pending.length > 0) {
|
|
134
|
+
const { delimiter, stripTabs, language, body } = pending[0];
|
|
135
|
+
let candidate = line.replace(/[\r\n]+$/, "");
|
|
136
|
+
if (stripTabs) {
|
|
137
|
+
candidate = candidate.replace(/^\t+/, "");
|
|
138
|
+
}
|
|
139
|
+
if (candidate === delimiter) {
|
|
140
|
+
pending.shift();
|
|
141
|
+
} else if (language) {
|
|
142
|
+
const contentLine = stripTabs ? line.replace(/^\t+/, "") : line;
|
|
143
|
+
allExecutable += contentLine;
|
|
144
|
+
if (language === "shell") {
|
|
145
|
+
shellExecutable += contentLine;
|
|
146
|
+
} else {
|
|
147
|
+
body.push(contentLine);
|
|
148
|
+
if (endsWithNewline(contentLine)) {
|
|
149
|
+
shellExecutable += "\n";
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} else if (endsWithNewline(line)) {
|
|
153
|
+
shellExecutable += "\n";
|
|
154
|
+
allExecutable += "\n";
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
shellExecutable += line;
|
|
160
|
+
allExecutable += line;
|
|
161
|
+
const language = heredocReceiverLanguage(line);
|
|
162
|
+
for (const match of line.matchAll(HEREDOC_START)) {
|
|
163
|
+
const body = [];
|
|
164
|
+
pending.push({ delimiter: match[3] ?? match[4], stripTabs: Boolean(match[1]), language, body });
|
|
165
|
+
if (language === "python" || language === "node") {
|
|
166
|
+
interpreterSections.push({ language, body });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
shellCommand: shellExecutable,
|
|
172
|
+
executable: allExecutable,
|
|
173
|
+
interpreterSections: interpreterSections.map(({ language, body }) => ({ language, source: body.join("") }))
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Removes data heredocs while preserving all executable interpreter bodies.
|
|
178
|
+
function stripHeredocBodies(command) {
|
|
179
|
+
return parseHeredocContent(command).executable;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Mirrors Python shlex in posix mode with punctuation_chars=";&|()<>" and
|
|
183
|
+
// whitespace_split: quotes are removed, runs of punctuation become tokens, and
|
|
184
|
+
// an unclosed quote or trailing escape makes the command unparsable.
|
|
185
|
+
function shellTokens(text) {
|
|
186
|
+
const whitespace = " \t\r\n";
|
|
187
|
+
const punctuation = ";&|()<>";
|
|
188
|
+
const tokens = [];
|
|
189
|
+
let current = "";
|
|
190
|
+
let inWord = false;
|
|
191
|
+
let index = 0;
|
|
192
|
+
const finishWord = () => {
|
|
193
|
+
if (inWord) {
|
|
194
|
+
tokens.push(current);
|
|
195
|
+
current = "";
|
|
196
|
+
inWord = false;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
while (index < text.length) {
|
|
200
|
+
const character = text[index];
|
|
201
|
+
if (whitespace.includes(character)) {
|
|
202
|
+
finishWord();
|
|
203
|
+
index += 1;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (punctuation.includes(character)) {
|
|
207
|
+
finishWord();
|
|
208
|
+
let run = "";
|
|
209
|
+
while (index < text.length && punctuation.includes(text[index])) {
|
|
210
|
+
run += text[index];
|
|
211
|
+
index += 1;
|
|
212
|
+
}
|
|
213
|
+
tokens.push(run);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (character === "\\") {
|
|
217
|
+
if (index + 1 >= text.length) {
|
|
218
|
+
throw new Error("No escaped character");
|
|
219
|
+
}
|
|
220
|
+
current += text[index + 1];
|
|
221
|
+
inWord = true;
|
|
222
|
+
index += 2;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (character === "'") {
|
|
226
|
+
const end = text.indexOf("'", index + 1);
|
|
227
|
+
if (end < 0) {
|
|
228
|
+
throw new Error("No closing quotation");
|
|
229
|
+
}
|
|
230
|
+
current += text.slice(index + 1, end);
|
|
231
|
+
inWord = true;
|
|
232
|
+
index = end + 1;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (character === "\"") {
|
|
236
|
+
index += 1;
|
|
237
|
+
let closed = false;
|
|
238
|
+
while (index < text.length) {
|
|
239
|
+
const next = text[index];
|
|
240
|
+
if (next === "\"") {
|
|
241
|
+
closed = true;
|
|
242
|
+
index += 1;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
if (next === "\\" && index + 1 < text.length && (text[index + 1] === "\"" || text[index + 1] === "\\")) {
|
|
246
|
+
current += text[index + 1];
|
|
247
|
+
index += 2;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
current += next;
|
|
251
|
+
index += 1;
|
|
252
|
+
}
|
|
253
|
+
if (!closed) {
|
|
254
|
+
throw new Error("No closing quotation");
|
|
255
|
+
}
|
|
256
|
+
inWord = true;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
current += character;
|
|
260
|
+
inWord = true;
|
|
261
|
+
index += 1;
|
|
262
|
+
}
|
|
263
|
+
finishWord();
|
|
264
|
+
return tokens;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function shellSegments(command) {
|
|
268
|
+
const { shellCommand } = parseHeredocContent(command);
|
|
269
|
+
const normalized = shellCommand.replaceAll("\\\n", " ").replaceAll("\n", " ; ");
|
|
270
|
+
let tokens;
|
|
271
|
+
try {
|
|
272
|
+
tokens = shellTokens(normalized);
|
|
273
|
+
} catch {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const segments = [];
|
|
278
|
+
let current = [];
|
|
279
|
+
for (const token of tokens) {
|
|
280
|
+
if (SHELL_SEPARATOR_TOKEN.test(token)) {
|
|
281
|
+
if (current.length > 0) {
|
|
282
|
+
segments.push(current);
|
|
283
|
+
current = [];
|
|
284
|
+
}
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
current.push(token);
|
|
288
|
+
}
|
|
289
|
+
if (current.length > 0) {
|
|
290
|
+
segments.push(current);
|
|
291
|
+
}
|
|
292
|
+
return segments;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isManagedArtifactPath(value) {
|
|
296
|
+
return MANAGED_ARTIFACT_PATH.test(value);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function baseName(token) {
|
|
300
|
+
return token.split("/").pop();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function commandIndex(segment) {
|
|
304
|
+
let index = 0;
|
|
305
|
+
while (index < segment.length && SHELL_ASSIGNMENT.test(segment[index])) {
|
|
306
|
+
index += 1;
|
|
307
|
+
}
|
|
308
|
+
if (index < segment.length && baseName(segment[index]) === "env") {
|
|
309
|
+
index += 1;
|
|
310
|
+
while (index < segment.length && (segment[index].startsWith("-") || SHELL_ASSIGNMENT.test(segment[index]))) {
|
|
311
|
+
index += 1;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return index < segment.length ? index : null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function redirectionDetails(segment) {
|
|
318
|
+
const targets = [];
|
|
319
|
+
const consumed = new Set();
|
|
320
|
+
segment.forEach((token, index) => {
|
|
321
|
+
if (!token.includes(">") || ![...token].every((character) => "<>&|".includes(character))) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
consumed.add(index);
|
|
325
|
+
if (index > 0 && /^\d+$/.test(segment[index - 1])) {
|
|
326
|
+
consumed.add(index - 1);
|
|
327
|
+
}
|
|
328
|
+
if (index + 1 >= segment.length) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
consumed.add(index + 1);
|
|
332
|
+
const target = segment[index + 1];
|
|
333
|
+
if ((token === ">&" || token === "<&") && (/^\d+$/.test(target) || target === "-")) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
targets.push(target);
|
|
337
|
+
});
|
|
338
|
+
return { targets, consumed };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function commandOperands(segment, start, consumed) {
|
|
342
|
+
const operands = [];
|
|
343
|
+
let optionsDone = false;
|
|
344
|
+
for (let index = start; index < segment.length; index += 1) {
|
|
345
|
+
if (consumed.has(index)) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const token = segment[index];
|
|
349
|
+
if (token === "--") {
|
|
350
|
+
optionsDone = true;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (!optionsDone && token.startsWith("-")) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
operands.push(token);
|
|
357
|
+
}
|
|
358
|
+
return operands;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function explicitInterpreterMutation(commandNames, source) {
|
|
362
|
+
if ([...commandNames].some((name) => /^python\d*(?:\.\d+)?$/.test(name))) {
|
|
363
|
+
for (const match of source.matchAll(PYTHON_PATH_MUTATION)) {
|
|
364
|
+
if (isManagedArtifactPath(match.groups.path)) {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const match of source.matchAll(PYTHON_OPEN)) {
|
|
369
|
+
if (isManagedArtifactPath(match.groups.path) && [..."wax+"].some((flag) => match.groups.mode.includes(flag))) {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (commandNames.has("node")) {
|
|
375
|
+
return [...source.matchAll(NODE_PATH_MUTATION)].some((match) => isManagedArtifactPath(match.groups.path));
|
|
376
|
+
}
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function mutatesManagedArtifact(command) {
|
|
381
|
+
const executableCommand = stripHeredocBodies(command);
|
|
382
|
+
const segments = shellSegments(command);
|
|
383
|
+
const commandNames = new Set();
|
|
384
|
+
for (const segment of segments) {
|
|
385
|
+
const index = commandIndex(segment);
|
|
386
|
+
if (index !== null) {
|
|
387
|
+
commandNames.add(baseName(segment[index]));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (explicitInterpreterMutation(commandNames, executableCommand)) {
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
for (const segment of segments) {
|
|
395
|
+
const { targets, consumed } = redirectionDetails(segment);
|
|
396
|
+
if (targets.some(isManagedArtifactPath)) {
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const executableIndex = commandIndex(segment);
|
|
401
|
+
if (executableIndex === null) {
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const commandName = baseName(segment[executableIndex]);
|
|
405
|
+
const argumentStart = executableIndex + 1;
|
|
406
|
+
const args = segment.slice(argumentStart);
|
|
407
|
+
const operands = commandOperands(segment, argumentStart, consumed);
|
|
408
|
+
|
|
409
|
+
let targetDirectory = null;
|
|
410
|
+
args.forEach((argument, index) => {
|
|
411
|
+
if ((argument === "-t" || argument === "--target-directory") && index + 1 < args.length) {
|
|
412
|
+
targetDirectory = args[index + 1];
|
|
413
|
+
} else if (argument.startsWith("--target-directory=")) {
|
|
414
|
+
targetDirectory = argument.split("=").slice(1).join("=");
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
if (commandName === "cp") {
|
|
419
|
+
const destination = targetDirectory || (operands.length >= 2 ? operands[operands.length - 1] : null);
|
|
420
|
+
if (destination && isManagedArtifactPath(destination)) {
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
} else if (commandName === "mv") {
|
|
424
|
+
if (targetDirectory && isManagedArtifactPath(targetDirectory)) {
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
if (operands.some(isManagedArtifactPath)) {
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
} else if (commandName === "rm" || commandName === "tee") {
|
|
431
|
+
if (operands.some(isManagedArtifactPath)) {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
} else if (commandName === "sed" || commandName === "perl") {
|
|
435
|
+
const inPlace = args.some((argument) => argument.startsWith("-") && argument.slice(1).includes("i"));
|
|
436
|
+
if (inPlace && args.some(isManagedArtifactPath)) {
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function unquotedAmpersand(commandWithoutQuotes) {
|
|
445
|
+
const cleaned = commandWithoutQuotes
|
|
446
|
+
.replace(/\d*>&\d*/g, " ")
|
|
447
|
+
.replace(/<&\d*/g, " ")
|
|
448
|
+
.replaceAll("&&", " ");
|
|
449
|
+
return /&\s*(?:$|[;\n)])/.test(cleaned) || /\s&\s/.test(cleaned);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function protectedToolInvoked(commandWithoutQuotes) {
|
|
453
|
+
return PROTECTED_TOOL_INVOCATION.test(commandWithoutQuotes);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function shellBackgroundReasons(command) {
|
|
457
|
+
const stripped = stripEscapedCharacters(stripQuoted(command));
|
|
458
|
+
const reasons = [];
|
|
459
|
+
if (/(?:^|[\s;&|(])(?:nohup|setsid)(?:\s|$)/.test(stripped)) {
|
|
460
|
+
reasons.push("nohup/setsid detach is forbidden");
|
|
461
|
+
}
|
|
462
|
+
if (/(?:^|[\s;&|(])disown(?:\s|$)/.test(stripped)) {
|
|
463
|
+
reasons.push("disown is forbidden");
|
|
464
|
+
}
|
|
465
|
+
if (unquotedAmpersand(stripped)) {
|
|
466
|
+
reasons.push("'&' background execution is forbidden");
|
|
467
|
+
}
|
|
468
|
+
return reasons;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Masks Python comments (and optionally string contents) with spaces while
|
|
472
|
+
// preserving offsets, so call and import scanning only sees executable code.
|
|
473
|
+
function maskPython(source, maskStrings) {
|
|
474
|
+
let output = "";
|
|
475
|
+
let index = 0;
|
|
476
|
+
while (index < source.length) {
|
|
477
|
+
const character = source[index];
|
|
478
|
+
if (character === "#") {
|
|
479
|
+
while (index < source.length && source[index] !== "\n") {
|
|
480
|
+
output += " ";
|
|
481
|
+
index += 1;
|
|
482
|
+
}
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (character === "'" || character === "\"") {
|
|
486
|
+
const triple = source.startsWith(character.repeat(3), index);
|
|
487
|
+
const delimiter = triple ? character.repeat(3) : character;
|
|
488
|
+
output += maskStrings ? " ".repeat(delimiter.length) : delimiter;
|
|
489
|
+
index += delimiter.length;
|
|
490
|
+
while (index < source.length) {
|
|
491
|
+
if (source[index] === "\\" && index + 1 < source.length) {
|
|
492
|
+
output += maskStrings ? " " : source.slice(index, index + 2);
|
|
493
|
+
index += 2;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (source.startsWith(delimiter, index)) {
|
|
497
|
+
output += maskStrings ? " ".repeat(delimiter.length) : delimiter;
|
|
498
|
+
index += delimiter.length;
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
if (!triple && source[index] === "\n") {
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
output += maskStrings && source[index] !== "\n" ? " " : source[index];
|
|
505
|
+
index += 1;
|
|
506
|
+
}
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
output += character;
|
|
510
|
+
index += 1;
|
|
511
|
+
}
|
|
512
|
+
return output;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function pythonImportAliases(code) {
|
|
516
|
+
const aliases = new Map();
|
|
517
|
+
for (const statement of code.split(/[;\n]/)) {
|
|
518
|
+
const fromImport = /^\s*from\s+([A-Za-z_][\w.]*)\s+import\s+(.+)$/.exec(statement);
|
|
519
|
+
if (fromImport) {
|
|
520
|
+
for (const item of fromImport[2].replace(/[()]/g, " ").split(",")) {
|
|
521
|
+
const parts = item.trim().split(/\s+as\s+/);
|
|
522
|
+
if (parts[0]) {
|
|
523
|
+
aliases.set((parts[1] || parts[0]).trim(), `${fromImport[1]}.${parts[0].trim()}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const plainImport = /^\s*import\s+(.+)$/.exec(statement);
|
|
529
|
+
if (plainImport) {
|
|
530
|
+
for (const item of plainImport[1].split(",")) {
|
|
531
|
+
const parts = item.trim().split(/\s+as\s+/);
|
|
532
|
+
const name = parts[0].trim();
|
|
533
|
+
if (name) {
|
|
534
|
+
aliases.set(parts[1] ? parts[1].trim() : name.split(".")[0], name);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return aliases;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Returns the value of a plain Python string literal starting at offset when it
|
|
543
|
+
// is the whole argument, mirroring an ast.Constant first argument.
|
|
544
|
+
function pythonLiteralArgument(source, offset) {
|
|
545
|
+
const match = /^\s*([rRuU]?)('''|"""|'|")/.exec(source.slice(offset));
|
|
546
|
+
if (!match) {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
const delimiter = match[2];
|
|
550
|
+
let index = offset + match[0].length;
|
|
551
|
+
let value = "";
|
|
552
|
+
while (index < source.length) {
|
|
553
|
+
if (source[index] === "\\" && index + 1 < source.length) {
|
|
554
|
+
value += source.slice(index, index + 2);
|
|
555
|
+
index += 2;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (source.startsWith(delimiter, index)) {
|
|
559
|
+
index += delimiter.length;
|
|
560
|
+
return /^\s*[,)]/.test(source.slice(index)) ? value : null;
|
|
561
|
+
}
|
|
562
|
+
value += source[index];
|
|
563
|
+
index += 1;
|
|
564
|
+
}
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function callArgumentSpan(executable, openParen) {
|
|
569
|
+
let depth = 0;
|
|
570
|
+
for (let index = openParen; index < executable.length; index += 1) {
|
|
571
|
+
if (executable[index] === "(") {
|
|
572
|
+
depth += 1;
|
|
573
|
+
} else if (executable[index] === ")") {
|
|
574
|
+
depth -= 1;
|
|
575
|
+
if (depth === 0) {
|
|
576
|
+
return executable.slice(openParen + 1, index);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return executable.slice(openParen + 1);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function scanPythonBackgrounding(source) {
|
|
584
|
+
const withoutComments = maskPython(source, false);
|
|
585
|
+
const executable = maskPython(source, true);
|
|
586
|
+
const aliases = pythonImportAliases(withoutComments);
|
|
587
|
+
const reasons = [];
|
|
588
|
+
const asynchronousCalls = new Set([
|
|
589
|
+
"subprocess.Popen",
|
|
590
|
+
"os.fork",
|
|
591
|
+
"os.popen",
|
|
592
|
+
"asyncio.create_subprocess_exec",
|
|
593
|
+
"asyncio.create_subprocess_shell"
|
|
594
|
+
]);
|
|
595
|
+
const shellCalls = new Set(["os.system", "subprocess.getoutput", "subprocess.getstatusoutput"]);
|
|
596
|
+
const foregroundCalls = new Set(["subprocess.run", "subprocess.call", "subprocess.check_call", "subprocess.check_output"]);
|
|
597
|
+
const callPattern = /(?<![\w.])([A-Za-z_]\w*(?:\s*\.\s*[A-Za-z_]\w*)*)\s*\(/g;
|
|
598
|
+
for (const match of executable.matchAll(callPattern)) {
|
|
599
|
+
if (/\b(?:def|class)\s+$/.test(executable.slice(0, match.index))) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const parts = match[1].replace(/\s+/g, "").split(".");
|
|
603
|
+
const resolved = aliases.has(parts[0]) ? [...aliases.get(parts[0]).split("."), ...parts.slice(1)] : parts;
|
|
604
|
+
const name = resolved.join(".");
|
|
605
|
+
if (asynchronousCalls.has(name) || name.startsWith("os.spawn")) {
|
|
606
|
+
reasons.push("Python asynchronous process creation is forbidden");
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const openParen = match.index + match[0].length - 1;
|
|
610
|
+
const command = pythonLiteralArgument(source, openParen + 1);
|
|
611
|
+
if (shellCalls.has(name) && command && shellBackgroundReasons(command).length > 0) {
|
|
612
|
+
reasons.push("Python shell background execution is forbidden");
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (foregroundCalls.has(name)) {
|
|
616
|
+
const usesShell = /(?:^|[,\s])shell\s*=\s*True\b/.test(callArgumentSpan(executable, openParen));
|
|
617
|
+
if (usesShell && command && shellBackgroundReasons(command).length > 0) {
|
|
618
|
+
reasons.push("Python shell background execution is forbidden");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return reasons;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function maskJavascript(source, maskStrings) {
|
|
626
|
+
let output = "";
|
|
627
|
+
let state = "code";
|
|
628
|
+
let quote = "";
|
|
629
|
+
let index = 0;
|
|
630
|
+
while (index < source.length) {
|
|
631
|
+
const character = source[index];
|
|
632
|
+
const following = index + 1 < source.length ? source[index + 1] : "";
|
|
633
|
+
if (state === "code") {
|
|
634
|
+
if (character === "/" && following === "/") {
|
|
635
|
+
output += " ";
|
|
636
|
+
state = "line-comment";
|
|
637
|
+
index += 2;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (character === "/" && following === "*") {
|
|
641
|
+
output += " ";
|
|
642
|
+
state = "block-comment";
|
|
643
|
+
index += 2;
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
if (character === "'" || character === "\"" || character === "`") {
|
|
647
|
+
quote = character;
|
|
648
|
+
state = "string";
|
|
649
|
+
output += maskStrings ? " " : character;
|
|
650
|
+
index += 1;
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
output += character;
|
|
654
|
+
index += 1;
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (state === "line-comment") {
|
|
658
|
+
output += character === "\n" ? "\n" : " ";
|
|
659
|
+
if (character === "\n") {
|
|
660
|
+
state = "code";
|
|
661
|
+
}
|
|
662
|
+
index += 1;
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (state === "block-comment") {
|
|
666
|
+
if (character === "*" && following === "/") {
|
|
667
|
+
output += " ";
|
|
668
|
+
state = "code";
|
|
669
|
+
index += 2;
|
|
670
|
+
} else {
|
|
671
|
+
output += character === "\n" ? "\n" : " ";
|
|
672
|
+
index += 1;
|
|
673
|
+
}
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (character === "\\" && following) {
|
|
677
|
+
output += maskStrings ? " " : character + following;
|
|
678
|
+
index += 2;
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
output += maskStrings && character !== "\n" ? " " : character;
|
|
682
|
+
if (character === quote) {
|
|
683
|
+
state = "code";
|
|
684
|
+
}
|
|
685
|
+
index += 1;
|
|
686
|
+
}
|
|
687
|
+
return output;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function escapeRegExp(value) {
|
|
691
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function scanNodeBackgrounding(source) {
|
|
695
|
+
const withoutComments = maskJavascript(source, false);
|
|
696
|
+
const executable = maskJavascript(source, true);
|
|
697
|
+
const moduleAliases = new Set(["child_process"]);
|
|
698
|
+
const functionAliases = new Set();
|
|
699
|
+
const modulePattern = "(?:node:)?child_process";
|
|
700
|
+
for (const match of withoutComments.matchAll(new RegExp(
|
|
701
|
+
`\\b(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*require\\(\\s*['"]${modulePattern}['"]\\s*\\)`,
|
|
702
|
+
"g"
|
|
703
|
+
))) {
|
|
704
|
+
moduleAliases.add(match[1]);
|
|
705
|
+
}
|
|
706
|
+
for (const match of withoutComments.matchAll(new RegExp(
|
|
707
|
+
`\\bimport\\s+\\*\\s+as\\s+([A-Za-z_$][\\w$]*)\\s+from\\s+['"]${modulePattern}['"]`,
|
|
708
|
+
"g"
|
|
709
|
+
))) {
|
|
710
|
+
moduleAliases.add(match[1]);
|
|
711
|
+
}
|
|
712
|
+
const destructuredPatterns = [
|
|
713
|
+
`\\b(?:const|let|var)\\s*\\{([^}]+)\\}\\s*=\\s*require\\(\\s*['"]${modulePattern}['"]\\s*\\)`,
|
|
714
|
+
`\\bimport\\s*\\{([^}]+)\\}\\s*from\\s*['"]${modulePattern}['"]`
|
|
715
|
+
];
|
|
716
|
+
const asynchronous = new Set(["spawn", "exec", "execFile", "fork"]);
|
|
717
|
+
for (const pattern of destructuredPatterns) {
|
|
718
|
+
for (const match of withoutComments.matchAll(new RegExp(pattern, "g"))) {
|
|
719
|
+
for (const item of match[1].split(",")) {
|
|
720
|
+
const parts = item.trim().split(/\s+(?:as)\s+|\s*:\s*/);
|
|
721
|
+
if (parts.length > 0 && asynchronous.has(parts[0])) {
|
|
722
|
+
functionAliases.add(parts[parts.length - 1]);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
for (const alias of moduleAliases) {
|
|
728
|
+
if (new RegExp(`\\b${escapeRegExp(alias)}\\s*\\.\\s*(?:spawn|exec|execFile|fork)\\s*\\(`).test(executable)) {
|
|
729
|
+
return ["Node asynchronous process creation is forbidden"];
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
for (const alias of functionAliases) {
|
|
733
|
+
if (new RegExp(`\\b${escapeRegExp(alias)}\\s*\\(`).test(executable)) {
|
|
734
|
+
return ["Node asynchronous process creation is forbidden"];
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (new RegExp(`\\brequire\\(\\s*['"]${modulePattern}['"]\\s*\\)\\s*\\.\\s*(?:spawn|exec|execFile|fork)\\s*\\(`).test(withoutComments)) {
|
|
738
|
+
return ["Node asynchronous process creation is forbidden"];
|
|
739
|
+
}
|
|
740
|
+
return [];
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function scanShellCommand(command, depth = 0) {
|
|
744
|
+
const { shellCommand, interpreterSections } = parseHeredocContent(command);
|
|
745
|
+
const reasons = shellBackgroundReasons(shellCommand);
|
|
746
|
+
const stripped = stripEscapedCharacters(stripQuoted(shellCommand));
|
|
747
|
+
if (protectedToolInvoked(stripped)) {
|
|
748
|
+
if (SHELL_CONTROL_OPERATOR.test(stripped)) {
|
|
749
|
+
reasons.push("run-long-check and watch-job must be standalone Bash commands");
|
|
750
|
+
}
|
|
751
|
+
if (RUN_LONG_CHECK_SHELL_STRING.test(stripped)) {
|
|
752
|
+
reasons.push("run-long-check must receive a direct executable or script, not a shell command string");
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// `sh -c '...'` quoted payloads are shell code, not plain strings.
|
|
757
|
+
if (depth < MAX_NESTED_SHELL_DEPTH && SHELL_DASH_C.test(stripped)) {
|
|
758
|
+
for (const segment of quotedSegments(shellCommand)) {
|
|
759
|
+
if (protectedToolInvoked(stripEscapedCharacters(stripQuoted(segment)))) {
|
|
760
|
+
reasons.push("run-long-check and watch-job must not be invoked through a shell command string");
|
|
761
|
+
}
|
|
762
|
+
reasons.push(...scanShellCommand(segment, depth + 1));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (depth < MAX_NESTED_SHELL_DEPTH) {
|
|
766
|
+
for (const substitution of doubleQuotedCommandSubstitutions(shellCommand)) {
|
|
767
|
+
if (protectedToolInvoked(stripEscapedCharacters(stripQuoted(substitution)))) {
|
|
768
|
+
reasons.push("run-long-check and watch-job must not be invoked through command substitution");
|
|
769
|
+
}
|
|
770
|
+
reasons.push(...scanShellCommand(substitution, depth + 1));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
for (const { language, source } of interpreterSections) {
|
|
774
|
+
if (language === "python") {
|
|
775
|
+
reasons.push(...scanPythonBackgrounding(source));
|
|
776
|
+
} else if (language === "node") {
|
|
777
|
+
reasons.push(...scanNodeBackgrounding(source));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return reasons;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function guardReasons(toolInput) {
|
|
784
|
+
const reasons = [];
|
|
785
|
+
if (toolInput.run_in_background) {
|
|
786
|
+
reasons.push("Bash run_in_background is forbidden");
|
|
787
|
+
}
|
|
788
|
+
const command = typeof toolInput.command === "string" ? toolInput.command : "";
|
|
789
|
+
reasons.push(...scanShellCommand(command));
|
|
790
|
+
if (mutatesManagedArtifact(command)) {
|
|
791
|
+
reasons.push("direct Bash writes to VCM-managed Markdown are forbidden");
|
|
792
|
+
}
|
|
793
|
+
return [...new Set(reasons)];
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function asObject(value) {
|
|
797
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function main() {
|
|
801
|
+
const raw = fs.readFileSync(0, "utf8");
|
|
802
|
+
if (!WORKFLOW_ROLES.has((process.env.VCM_ROLE || "").trim())) {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
let payload;
|
|
807
|
+
try {
|
|
808
|
+
payload = raw.trim() ? JSON.parse(raw) : {};
|
|
809
|
+
} catch {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
payload = asObject(payload);
|
|
813
|
+
let reasons;
|
|
814
|
+
if (payload.tool_name === "Bash") {
|
|
815
|
+
reasons = guardReasons(asObject(payload.tool_input));
|
|
816
|
+
} else if (payload.tool_name === "Write" || payload.tool_name === "Edit") {
|
|
817
|
+
const toolInput = asObject(payload.tool_input);
|
|
818
|
+
const filePath = toolInput.file_path || toolInput.path;
|
|
819
|
+
reasons = typeof filePath === "string" && MANAGED_ARTIFACT_PATH.test(filePath)
|
|
820
|
+
? ["direct Write/Edit of VCM-managed Markdown is forbidden"]
|
|
821
|
+
: [];
|
|
822
|
+
} else {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (reasons.length === 0) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
process.stdout.write(`${JSON.stringify({
|
|
830
|
+
hookSpecificOutput: {
|
|
831
|
+
hookEventName: "PreToolUse",
|
|
832
|
+
permissionDecision: "deny",
|
|
833
|
+
permissionDecisionReason: `VCM denied this tool call (${reasons.join("; ")}). `
|
|
834
|
+
+ (reasons.some((reason) => reason.includes("Markdown")) ? ARTIFACT_HINT : SKILL_HINT)
|
|
835
|
+
}
|
|
836
|
+
})}\n`);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
main();
|