@guilz-dev/belay 0.7.0 → 0.8.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.
Files changed (75) hide show
  1. package/README.md +19 -19
  2. package/dist/adapters/shared/gate-runtime.js +23 -5
  3. package/dist/bundle/claude-runtime.mjs +4909 -3504
  4. package/dist/bundle/codex-runtime.mjs +5050 -3645
  5. package/dist/bundle/cursor-runtime.mjs +5050 -3645
  6. package/dist/commands/doctor.js +7 -0
  7. package/dist/commands/dogfood.js +1 -1
  8. package/dist/commands/explain.js +2 -2
  9. package/dist/commands/harvest.d.ts +0 -1
  10. package/dist/commands/harvest.js +1 -4
  11. package/dist/commands/metrics.js +6 -0
  12. package/dist/commands/quality.js +1 -3
  13. package/dist/conformance/guarantee-table.js +133 -11
  14. package/dist/conformance/types.d.ts +1 -0
  15. package/dist/core/audit-metrics.d.ts +1 -0
  16. package/dist/core/audit-metrics.js +12 -4
  17. package/dist/core/audit-types.d.ts +3 -0
  18. package/dist/core/capability/index.d.ts +1 -1
  19. package/dist/core/capability/policy-engine.d.ts +31 -1
  20. package/dist/core/capability/policy-engine.js +186 -0
  21. package/dist/core/capability/request.d.ts +9 -0
  22. package/dist/core/capability/resolver.d.ts +0 -1
  23. package/dist/core/capability/resolver.js +0 -1
  24. package/dist/core/config.js +0 -2
  25. package/dist/core/effect-ir/audit.js +1 -0
  26. package/dist/core/effect-ir/build.js +5 -1
  27. package/dist/core/effect-ir/index.d.ts +6 -1
  28. package/dist/core/effect-ir/index.js +2 -0
  29. package/dist/core/effect-ir/normalize.js +9 -3
  30. package/dist/core/effect-ir/policy.d.ts +2 -1
  31. package/dist/core/effect-ir/policy.js +93 -6
  32. package/dist/core/effect-ir/shell-build.d.ts +43 -0
  33. package/dist/core/effect-ir/shell-build.js +77 -0
  34. package/dist/core/effect-ir/shell-lower.d.ts +14 -0
  35. package/dist/core/effect-ir/shell-lower.js +1509 -0
  36. package/dist/core/effect-ir/types.d.ts +9 -2
  37. package/dist/core/gate-engine.js +63 -23
  38. package/dist/core/git-resource-identity.d.ts +26 -0
  39. package/dist/core/git-resource-identity.js +284 -0
  40. package/dist/core/harvest.d.ts +3 -7
  41. package/dist/core/harvest.js +4 -27
  42. package/dist/core/index.d.ts +2 -0
  43. package/dist/core/index.js +1 -0
  44. package/dist/core/path-utils.js +42 -23
  45. package/dist/core/shell-substitution.d.ts +1 -0
  46. package/dist/core/shell-substitution.js +61 -0
  47. package/dist/core/standing-allow.d.ts +1 -2
  48. package/dist/core/standing-allow.js +3 -20
  49. package/dist/core/types.d.ts +4 -1
  50. package/dist/core/verdict/adapter.js +15 -15
  51. package/dist/core/verdict/containment.js +4 -0
  52. package/dist/core/verdict/egress-classify.d.ts +12 -0
  53. package/dist/core/verdict/egress-classify.js +746 -0
  54. package/dist/core/verdict/git-classifier.d.ts +12 -0
  55. package/dist/core/verdict/git-classifier.js +320 -0
  56. package/dist/core/verdict/launcher-resolve.js +77 -17
  57. package/dist/core/verdict/parser.d.ts +2 -0
  58. package/dist/core/verdict/parser.js +95 -1
  59. package/dist/core/verdict/types.d.ts +2 -3
  60. package/dist/core/verdict/verdict.js +103 -952
  61. package/dist/corpus/must-allow-commands.d.ts +2 -1
  62. package/dist/corpus/must-allow-commands.js +2 -1
  63. package/dist/corpus/runtime-match.d.ts +2 -1
  64. package/dist/corpus/runtime-match.js +2 -1
  65. package/dist/corpus/types.d.ts +2 -2
  66. package/dist/templates.js +7 -2
  67. package/dist/version.d.ts +1 -1
  68. package/dist/version.js +1 -1
  69. package/package.json +2 -2
  70. package/dist/core/verdict/overrides.d.ts +0 -7
  71. package/dist/core/verdict/overrides.js +0 -37
  72. package/dist/core/verdict/shell-policy.d.ts +0 -26
  73. package/dist/core/verdict/shell-policy.js +0 -40
  74. package/dist/corpus/standing-allow-catalog.generated.d.ts +0 -13
  75. package/dist/corpus/standing-allow-catalog.generated.js +0 -99
@@ -0,0 +1,1509 @@
1
+ import { lstatSync, realpathSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { inspectGitResourceIdentity } from '../git-resource-identity.js';
4
+ import { isFdDuplication, isRedirectOperator, tokenizeShell } from '../shell-tokenizer.js';
5
+ import { decodeEgressEffects } from '../verdict/egress-classify.js';
6
+ import { decodeGitEffects } from '../verdict/git-classifier.js';
7
+ import { resolveLauncherRecipe } from '../verdict/launcher-resolve.js';
8
+ import { extractRecursiveScript, parseSegment, redactCommand, segmentOpacity, splitStructuralShellSegments, structuralSubstitutionInners, } from '../verdict/parser.js';
9
+ import { classifyPackageAcquisitionSpec, innerRecipeFromPeel, peelPackageExecArgv, resolveLocalBin, } from './package-exec.js';
10
+ import { buildShellEffectPlan, } from './shell-build.js';
11
+ const MAX_LOWER_DEPTH = 8;
12
+ const ENV_PREFIX_PATTERN = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
13
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '0:0:0:0:0:0:0:1']);
14
+ const SECRET_PATH_PATTERN = /(?:^|[/\\])(?:\.env(?:\..*)?|credentials?|secrets?|id_rsa)(?:$|[/\\])/i;
15
+ const CONTROL_PLANE_PATH_PATTERN = /^(?:\/etc(?:\/|$)|\/var\/run(?:\/|$)|.*(?:^|[/\\])\.(?:git|ssh|cursor|claude)(?:[/\\]|$))/;
16
+ /**
17
+ * Parse and lower a general shell command into the canonical Task 2 model.
18
+ * This function only produces effects; it does not evaluate policy or return a
19
+ * permission/verdict.
20
+ */
21
+ export function lowerShellEffectPlan(params) {
22
+ const context = { ...params, depth: 0 };
23
+ const segments = lowerTopLevelSegments(params.command, context);
24
+ return buildShellEffectPlan({
25
+ inputFingerprint: params.inputFingerprint,
26
+ segments,
27
+ signals: pipeToShell(params.command) ? ['pipe_to_shell'] : [],
28
+ });
29
+ }
30
+ function lowerTopLevelSegments(command, context) {
31
+ const commands = splitStructuralShellSegments(command);
32
+ if (commands.length === 0 && command.trim()) {
33
+ commands.push(command.trim());
34
+ }
35
+ const pipedToShell = pipeToShell(command);
36
+ const lowered = [];
37
+ let cwd = context.cwd;
38
+ let cwdKnown = true;
39
+ let inferredEnv = { ...context.env };
40
+ for (const segment of commands) {
41
+ let result = lowerSegment(segment, {
42
+ ...context,
43
+ cwd,
44
+ env: inferredEnv,
45
+ command: segment,
46
+ depth: context.depth,
47
+ inputFingerprint: context.inputFingerprint,
48
+ ...(pipedToShell && isShellHead(parseSegment(segment).head)
49
+ ? { pipeToShellSegment: true }
50
+ : {}),
51
+ });
52
+ if (!cwdKnown) {
53
+ const requiresCwd = result.requirements.some(requiresKnownCwd);
54
+ const requirements = [...result.requirements];
55
+ if (requiresCwd) {
56
+ requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, result.commandRedacted, ['shell.cwd_unknown']));
57
+ }
58
+ result = {
59
+ ...result,
60
+ requirements,
61
+ ...(requiresCwd
62
+ ? {
63
+ completeness: 'partial',
64
+ opacity: result.opacity === 'unparseable' ? 'unparseable' : 'opaque',
65
+ }
66
+ : {}),
67
+ signals: [...new Set([...result.signals, 'shell.cwd_unknown'])],
68
+ };
69
+ }
70
+ lowered.push(result);
71
+ if (!inferredEnv.DATABASE_URL && startsLocalPostgresService(segment)) {
72
+ inferredEnv = {
73
+ ...inferredEnv,
74
+ DATABASE_URL: 'postgresql://127.0.0.1:5432/local',
75
+ };
76
+ }
77
+ const nextCwd = resolveCdTransition(segment, cwd);
78
+ if (nextCwd) {
79
+ cwd = nextCwd.cwd;
80
+ cwdKnown = nextCwd.known;
81
+ }
82
+ }
83
+ if (commands.length > 1 &&
84
+ /[\r\n]/.test(command) &&
85
+ lowered.some((segment) => segment.requirements.some((entry) => entry.action === 'network.connect'))) {
86
+ lowered.push(shellSegment('[multiline network boundary]', 'shell-boundary', [
87
+ requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, '[multiline network boundary]', ['shell.multiline_network_boundary']),
88
+ ], 'opaque', new Set(['shell.multiline_network_boundary'])));
89
+ }
90
+ return lowered;
91
+ }
92
+ function startsLocalPostgresService(command) {
93
+ const tokens = tokenizeShell(command);
94
+ return (path.basename(tokens[0] ?? '') === 'docker' &&
95
+ tokens[1] === 'compose' &&
96
+ ['up', 'start', 'restart'].includes(tokens[2] ?? '') &&
97
+ tokens.includes('postgres'));
98
+ }
99
+ function resolveCdTransition(command, currentCwd) {
100
+ const tokens = tokenizeShell(command);
101
+ if (path.basename(tokens[0] ?? '') !== 'cd') {
102
+ return null;
103
+ }
104
+ const target = tokens[1] ?? '~';
105
+ if (!target || target === '-' || target.includes('$') || target.includes('`')) {
106
+ return { cwd: currentCwd, known: false };
107
+ }
108
+ return { cwd: resolvePathOperand(target, currentCwd), known: true };
109
+ }
110
+ function requiresKnownCwd(requirementValue) {
111
+ if (requirementValue.action === 'fs.write' ||
112
+ requirementValue.action === 'git.ref.write' ||
113
+ requirementValue.action === 'control_plane.write') {
114
+ return true;
115
+ }
116
+ return (requirementValue.action === 'process.exec' &&
117
+ requirementValue.resource.kind === 'executable' &&
118
+ requirementValue.resource.operation === 'spawn');
119
+ }
120
+ function lowerSegment(command, context) {
121
+ const commandRedacted = redactCommand(command);
122
+ const rawTokens = tokenizeShell(command);
123
+ const environment = extractEnvironment(rawTokens, context.env);
124
+ const env = environment.env;
125
+ const parsed = parseSegment(command);
126
+ const tokens = stripRedirects(environment.commandTokens ?? parsed.tokens).map((token) => expandKnownVariables(token, env));
127
+ const head = path.basename(tokens[0] ?? parsed.head);
128
+ let opacity = segmentOpacity(command);
129
+ const signals = new Set();
130
+ const requirements = [];
131
+ addRedirectEffects(requirements, rawTokens, env, context, commandRedacted);
132
+ addSubstitutionEffects(requirements, command, context, commandRedacted, signals);
133
+ if (environment.malformed) {
134
+ requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
135
+ 'shell.env_wrapper_incomplete',
136
+ ]));
137
+ opacity = 'opaque';
138
+ }
139
+ if (path.basename((environment.commandTokens ?? rawTokens)[0] ?? '') === 'xargs') {
140
+ requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
141
+ 'shell.xargs_stdin_dynamic',
142
+ ]));
143
+ signals.add('shell.xargs_stdin_dynamic');
144
+ opacity = 'opaque';
145
+ }
146
+ if (context.depth >= MAX_LOWER_DEPTH) {
147
+ requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
148
+ 'shell.lower_depth_exceeded',
149
+ ]));
150
+ return shellSegment(commandRedacted, head, requirements, 'opaque', signals);
151
+ }
152
+ const packageExec = peelPackageExecArgv(tokens);
153
+ if (packageExec) {
154
+ requirements.push(...decodePackageExec(packageExec, context.cwd, context.repoRoot, commandRedacted));
155
+ for (const signal of packageExec.signals) {
156
+ signals.add(signal);
157
+ }
158
+ if (packageExec.opaque) {
159
+ opacity = 'opaque';
160
+ }
161
+ const innerRecipe = packageExecInnerIsMetadata(packageExec)
162
+ ? null
163
+ : innerRecipeFromPeel(packageExec);
164
+ if (innerRecipe) {
165
+ const nested = lowerTopLevelSegments(innerRecipe, {
166
+ ...context,
167
+ command: innerRecipe,
168
+ env,
169
+ depth: context.depth + 1,
170
+ });
171
+ for (const nestedSegment of nested) {
172
+ requirements.push(...nestedSegment.requirements.map((entry) => withInnerProvenance(entry, innerRecipe, packageExec.launcher, commandRedacted)));
173
+ for (const signal of nestedSegment.signals) {
174
+ signals.add(signal);
175
+ }
176
+ if (nestedSegment.completeness === 'partial') {
177
+ opacity = nestedSegment.opacity === 'unparseable' ? 'unparseable' : 'recursive';
178
+ }
179
+ }
180
+ }
181
+ return shellSegment(commandRedacted, head, requirements, opacity, signals);
182
+ }
183
+ const recursiveScript = extractRecursiveScript(tokens);
184
+ if (recursiveScript) {
185
+ requirements.push(processRequirement(head || 'sh', 'spawn', commandRedacted, ['shell.recursive_wrapper']));
186
+ const nested = lowerTopLevelSegments(recursiveScript, {
187
+ ...context,
188
+ command: recursiveScript,
189
+ env,
190
+ depth: context.depth + 1,
191
+ });
192
+ for (const nestedSegment of nested) {
193
+ requirements.push(...nestedSegment.requirements.map((entry) => withInnerProvenance(entry, recursiveScript, head, commandRedacted)));
194
+ for (const signal of nestedSegment.signals) {
195
+ signals.add(signal);
196
+ }
197
+ }
198
+ signals.add('shell.recursive_wrapper');
199
+ return shellSegment(commandRedacted, head, requirements, 'recursive', signals);
200
+ }
201
+ const launcher = resolveLauncherRecipe({
202
+ tokens,
203
+ cwd: context.cwd,
204
+ repoRoot: context.repoRoot,
205
+ depth: context.depth,
206
+ });
207
+ if (launcher) {
208
+ signals.add(`launcher.${launcher.reason}`);
209
+ requirements.push(requirement('process.exec', 'process.exec', { kind: 'executable', command: head, operation: 'spawn' }, commandRedacted, ['launcher.invoke']));
210
+ for (const recipe of launcher.recipes) {
211
+ const nested = lowerTopLevelSegments(recipe, {
212
+ ...context,
213
+ command: recipe,
214
+ env,
215
+ depth: context.depth + 1,
216
+ });
217
+ for (const nestedSegment of nested) {
218
+ requirements.push(...nestedSegment.requirements.map((entry) => withInnerProvenance(entry, recipe, head, commandRedacted)));
219
+ for (const signal of nestedSegment.signals) {
220
+ signals.add(signal);
221
+ }
222
+ if (nestedSegment.completeness === 'partial') {
223
+ opacity = nestedSegment.opacity === 'unparseable' ? 'unparseable' : 'recursive';
224
+ }
225
+ }
226
+ }
227
+ if (launcher.opaque || launcher.recipes.length === 0) {
228
+ requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
229
+ `launcher.${launcher.reason}`,
230
+ ]));
231
+ opacity = 'opaque';
232
+ }
233
+ return shellSegment(commandRedacted, head, requirements, opacity, signals);
234
+ }
235
+ const egress = decodeEgressEffects({
236
+ tokens,
237
+ cwd: context.cwd,
238
+ segment: commandRedacted,
239
+ });
240
+ if (egress) {
241
+ requirements.push(...egress);
242
+ for (const signal of egress.flatMap((entry) => entry.evidence.signals)) {
243
+ signals.add(signal);
244
+ }
245
+ }
246
+ else {
247
+ const git = decodeGitEffects({
248
+ tokens,
249
+ cwd: context.cwd,
250
+ repoRoot: context.repoRoot,
251
+ segment: commandRedacted,
252
+ });
253
+ if (git) {
254
+ requirements.push(...git);
255
+ for (const signal of git.flatMap((entry) => entry.evidence.signals)) {
256
+ signals.add(signal);
257
+ }
258
+ }
259
+ else {
260
+ requirements.push(...decodeProcessOrFilesystem({
261
+ tokens,
262
+ head,
263
+ env,
264
+ cwd: context.cwd,
265
+ repoRoot: context.repoRoot,
266
+ segment: commandRedacted,
267
+ }));
268
+ }
269
+ }
270
+ const environmentSignals = effectChangingEnvironmentSignals(head, env, environment.changedNames);
271
+ if (environmentSignals.length > 0) {
272
+ requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, environmentSignals));
273
+ opacity = 'opaque';
274
+ for (const signal of environmentSignals) {
275
+ signals.add(signal);
276
+ }
277
+ }
278
+ if (context.pipeToShellSegment) {
279
+ signals.add('pipe_to_shell');
280
+ ensureRequirement(requirements, requirement('process.exec', 'process.exec', { kind: 'executable', command: head || 'sh', operation: 'spawn' }, commandRedacted, ['pipe_to_shell']));
281
+ ensureRequirement(requirements, requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
282
+ 'pipe_to_shell',
283
+ ]));
284
+ opacity = 'opaque';
285
+ }
286
+ if (opacity === 'unparseable' || opacity === 'opaque') {
287
+ ensureRequirement(requirements, requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
288
+ `shell.${opacity}`,
289
+ ]));
290
+ }
291
+ return shellSegment(commandRedacted, head, requirements, opacity, signals);
292
+ }
293
+ function decodePackageExec(peel, cwd, repoRoot, segment) {
294
+ if (peel.opaque) {
295
+ return [
296
+ requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, segment, [
297
+ peel.reason,
298
+ ...peel.signals,
299
+ ]),
300
+ ];
301
+ }
302
+ if (peel.reason === 'npx_wrapper_readonly') {
303
+ return [];
304
+ }
305
+ const binHead = peel.innerTokens[0] ?? '';
306
+ const metadataOnly = packageExecInnerIsMetadata(peel);
307
+ const local = !peel.forceAcquire ? resolveLocalBin(binHead, cwd, repoRoot) : null;
308
+ if (local) {
309
+ return metadataOnly
310
+ ? [
311
+ requirement('process.exec', 'process.exec', { kind: 'executable', command: local.path, operation: 'inspect' }, segment, ['package_exec.delegate', 'package_exec.local_bin_resolved']),
312
+ ]
313
+ : [];
314
+ }
315
+ const networkResources = peel.acquisitionSpecs
316
+ .map((spec) => classifyPackageAcquisitionSpec(spec))
317
+ .filter((source) => source.kind !== 'local')
318
+ .map((source) => source.kind === 'registry'
319
+ ? {
320
+ kind: 'network',
321
+ host: 'registry.npmjs.org',
322
+ protocol: 'registry',
323
+ mode: 'read',
324
+ payload: 'none',
325
+ }
326
+ : {
327
+ kind: 'network',
328
+ host: source.host,
329
+ ...(source.port ? { port: source.port } : {}),
330
+ protocol: source.protocol,
331
+ mode: 'read',
332
+ payload: 'none',
333
+ });
334
+ if (networkResources.length === 0) {
335
+ networkResources.push({
336
+ kind: 'network',
337
+ host: 'registry.npmjs.org',
338
+ protocol: 'registry',
339
+ mode: 'read',
340
+ payload: 'none',
341
+ });
342
+ }
343
+ return [
344
+ ...networkResources.map((resource) => requirement('network.acquire', 'network.connect', resource, segment, [
345
+ 'package_acquire_possible',
346
+ ])),
347
+ requirement('fs.write', 'fs.write', { kind: 'package-cache', manager: peel.launcher === 'pnpm' ? 'pnpm' : 'npm' }, segment, ['package_cache_write']),
348
+ ...(metadataOnly
349
+ ? [
350
+ requirement('process.exec', 'process.exec', { kind: 'executable', command: binHead || peel.launcher, operation: 'inspect' }, segment, ['package_exec.delegate']),
351
+ ]
352
+ : []),
353
+ ];
354
+ }
355
+ function packageExecInnerIsMetadata(peel) {
356
+ const args = peel.innerTokens.slice(1);
357
+ return (args.length > 0 && args.every((arg) => ['--help', '--version', '-V', '-h', '-v'].includes(arg)));
358
+ }
359
+ function decodeProcessOrFilesystem(params) {
360
+ const { tokens, head, env, cwd, repoRoot, segment } = params;
361
+ const args = tokens.slice(1);
362
+ if (head === 'lsof') {
363
+ return validLsof(args)
364
+ ? [processRequirement(head, 'inspect', segment, ['process.inspect.lsof'])]
365
+ : unsupportedProcess(head, segment, 'process.lsof_grammar_incomplete');
366
+ }
367
+ if (head === 'ps') {
368
+ return validPs(args)
369
+ ? [processRequirement(head, 'inspect', segment, ['process.inspect.ps'])]
370
+ : unsupportedProcess(head, segment, 'process.ps_grammar_incomplete');
371
+ }
372
+ if (head === 'test' || head === '[') {
373
+ return validFilesystemTest(head, args)
374
+ ? [processRequirement(head, 'inspect', segment, ['process.inspect.filesystem_test'])]
375
+ : unsupportedProcess(head, segment, 'process.filesystem_test_grammar_incomplete');
376
+ }
377
+ if (head === 'docker') {
378
+ if (args[0] === 'info') {
379
+ return validDockerInfo(args.slice(1))
380
+ ? [processRequirement(head, 'inspect', segment, ['process.inspect.docker_info'])]
381
+ : unsupportedProcess(head, segment, 'process.docker_info_grammar_incomplete');
382
+ }
383
+ if (args[0] === 'compose' &&
384
+ ['up', 'down', 'start', 'stop', 'restart', 'create', 'rm'].includes(args[1] ?? '')) {
385
+ return [processRequirement(head, 'spawn', segment, ['service.local_mutation'])];
386
+ }
387
+ if (args[0] === 'push' ||
388
+ (args[0] === 'build' && args.includes('--push')) ||
389
+ (args[0] === 'buildx' &&
390
+ args[1] === 'build' &&
391
+ (args.includes('--push') ||
392
+ args.some((arg) => arg.startsWith('--output=type=registry') || arg.startsWith('-o=type=registry'))))) {
393
+ return [
394
+ processRequirement(head, 'spawn', segment, ['tier0_external', 'docker.remote_mutation']),
395
+ requirement('network.connect', 'network.connect', {
396
+ kind: 'network',
397
+ host: 'registry',
398
+ protocol: 'container-registry',
399
+ mode: 'mutate',
400
+ payload: 'present',
401
+ }, segment, ['tier0_external', 'docker.remote_mutation']),
402
+ ];
403
+ }
404
+ if (args[0] === 'run' ||
405
+ args[0] === 'create' ||
406
+ args[0] === 'pull' ||
407
+ args[0] === 'build' ||
408
+ (args[0] === 'buildx' && args[1] === 'build')) {
409
+ return [
410
+ processRequirement(head, 'spawn', segment, ['process.docker_spawn']),
411
+ requirement('network.acquire', 'network.connect', {
412
+ kind: 'network',
413
+ host: 'unknown',
414
+ protocol: 'container-registry',
415
+ mode: 'ambiguous',
416
+ payload: 'none',
417
+ }, segment, ['docker.image_acquisition_possible']),
418
+ ];
419
+ }
420
+ return [processRequirement(head, 'spawn', segment, ['process.docker_spawn'])];
421
+ }
422
+ if (isShellHead(head)) {
423
+ const syntax = bashSyntaxTarget(args);
424
+ if (syntax) {
425
+ return [
426
+ processRequirement(head, 'inspect', segment, ['process.inspect.shell_syntax']),
427
+ requirement('fs.read', 'fs.read', { kind: 'path', path: path.resolve(cwd, syntax) }, segment, ['shell.syntax_source_read']),
428
+ ];
429
+ }
430
+ return unsupportedProcess(head, segment, 'process.shell_grammar_incomplete');
431
+ }
432
+ if (head === 'prisma') {
433
+ return decodePrisma(args, env, repoRoot, segment);
434
+ }
435
+ if ((head === 'npm' || head === 'pnpm') && args.length === 1 && args[0] === '--version') {
436
+ return [processRequirement(head, 'inspect', segment, ['process.inspect.package_manager'])];
437
+ }
438
+ if (head === 'belay') {
439
+ return decodeBelay(args, repoRoot, segment);
440
+ }
441
+ if (head === 'tsc') {
442
+ return decodeTsc(args, cwd, segment);
443
+ }
444
+ if (head === 'go') {
445
+ return decodeGo(args, segment);
446
+ }
447
+ if (head === 'rsync') {
448
+ return decodeRsync(args, cwd, segment);
449
+ }
450
+ if (head === 'sed') {
451
+ return decodeSed(args, cwd, segment);
452
+ }
453
+ if (head === 'node') {
454
+ return decodeNode(args, cwd, segment);
455
+ }
456
+ if (head === 'vite' || head === 'vite-node') {
457
+ return [processRequirement(head, 'spawn', segment, ['process.local_dev_spawn'])];
458
+ }
459
+ if (((head === 'npm' || head === 'pnpm') && args[0] === 'publish') ||
460
+ (head === 'terraform' && args[0] === 'apply')) {
461
+ return [
462
+ processRequirement(head, 'spawn', segment, ['tier0_external']),
463
+ requirement('network.connect', 'network.connect', {
464
+ kind: 'network',
465
+ host: 'control-plane',
466
+ protocol: head,
467
+ mode: 'mutate',
468
+ payload: 'present',
469
+ }, segment, ['tier0_external']),
470
+ ];
471
+ }
472
+ if (head === 'cd') {
473
+ return [];
474
+ }
475
+ if (head === 'cp' || head === 'mv') {
476
+ return decodeCopyMove(head, args, cwd, segment);
477
+ }
478
+ if (head === 'rm') {
479
+ return decodeRm(args, cwd, repoRoot, segment);
480
+ }
481
+ if (head === 'vitest' || head === 'eslint' || head === 'biome' || head === 'belay') {
482
+ return [processRequirement(head, 'spawn', segment, ['process.known_local_spawn'])];
483
+ }
484
+ if (head === 'pwd' || head === 'which' || head === 'whoami') {
485
+ return [processRequirement(head, 'inspect', segment, ['process.pure_inspection'])];
486
+ }
487
+ const readOperands = filesystemReadOperands(head, args);
488
+ if (readOperands !== null) {
489
+ const lowered = [processRequirement(head, 'inspect', segment, ['process.filesystem_inspect'])];
490
+ for (const operand of readOperands) {
491
+ const resolved = resolvePathOperand(operand, cwd);
492
+ lowered.push(requirement('fs.read', 'fs.read', { kind: 'path', path: resolved }, segment, [
493
+ 'filesystem.read',
494
+ ]));
495
+ addSecretRead(lowered, resolved, segment);
496
+ }
497
+ return lowered;
498
+ }
499
+ const writeOperands = filesystemWriteOperands(head, args);
500
+ if (writeOperands !== null) {
501
+ const lowered = [processRequirement(head, 'spawn', segment, ['process.filesystem_mutation'])];
502
+ for (const operand of writeOperands) {
503
+ addWriteEffects(lowered, resolvePathOperand(operand, cwd), segment, ['filesystem.write']);
504
+ }
505
+ return lowered;
506
+ }
507
+ if (head === 'printf' || head === 'echo' || head === 'true' || head === 'false' || head === ':') {
508
+ return [];
509
+ }
510
+ if (!head) {
511
+ return [
512
+ requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, segment, [
513
+ 'shell.segment_empty',
514
+ ]),
515
+ ];
516
+ }
517
+ return unsupportedProcess(head, segment, 'process.grammar_unknown');
518
+ }
519
+ function decodeBelay(args, repoRoot, segment) {
520
+ const [section, operation, key] = args;
521
+ const judgeCommand = section === 'judge' &&
522
+ operation !== undefined &&
523
+ ['consent', 'list', 'status', 'test', 'use'].includes(operation);
524
+ const configRead = section === 'config' &&
525
+ (operation === undefined ||
526
+ operation === 'list' ||
527
+ (operation === 'get' && key?.startsWith('judge.')));
528
+ const configJudgeMutation = section === 'config' &&
529
+ ((['set', 'unset'].includes(operation ?? '') && key?.startsWith('judge.')) ||
530
+ (operation === 'credential' && key === 'mode'));
531
+ if (judgeCommand || configRead || configJudgeMutation) {
532
+ return [
533
+ processRequirement('belay', 'inspect', segment, [
534
+ 'belay_control_plane_command',
535
+ configJudgeMutation ? 'belay.config_judge_mutation' : 'belay.config_read',
536
+ ]),
537
+ ];
538
+ }
539
+ if (section === 'config' && ['set', 'unset', 'credential'].includes(operation ?? '')) {
540
+ return [
541
+ requirement('control_plane.write', 'control_plane.write', { kind: 'path', path: path.join(repoRoot, '.belay-control-plane') }, segment, ['belay.config_non_judge_mutation']),
542
+ ];
543
+ }
544
+ return [processRequirement('belay', 'spawn', segment, ['process.known_local_spawn'])];
545
+ }
546
+ function decodeTsc(args, cwd, segment) {
547
+ const inspect = args.length > 0 && args.every((arg) => ['--help', '--version', '-h', '-v'].includes(arg));
548
+ const requirements = [
549
+ processRequirement('tsc', inspect ? 'inspect' : 'spawn', segment, [
550
+ inspect ? 'process.inspect.tsc_metadata' : 'process.known_local_spawn',
551
+ ]),
552
+ ];
553
+ for (let index = 0; index < args.length; index += 1) {
554
+ const arg = args[index] ?? '';
555
+ const inline = arg.match(/^--(?:outDir|outFile)=(.+)$/)?.[1];
556
+ const separate = (arg === '--outDir' || arg === '--outFile') && args[index + 1] ? args[index + 1] : null;
557
+ const output = inline ?? separate;
558
+ if (!output) {
559
+ continue;
560
+ }
561
+ addWriteEffects(requirements, resolvePathOperand(output, cwd), segment, ['tsc.output']);
562
+ if (separate) {
563
+ index += 1;
564
+ }
565
+ }
566
+ return requirements;
567
+ }
568
+ function decodeCopyMove(head, args, cwd, segment) {
569
+ const requirements = [processRequirement(head, 'spawn', segment, ['process.filesystem_mutation'])];
570
+ const operands = [];
571
+ let targetDirectory = null;
572
+ let recursive = false;
573
+ let incomplete = false;
574
+ let optionsEnded = false;
575
+ const neutralFlags = new Set([
576
+ '-f',
577
+ '-i',
578
+ '-n',
579
+ '-p',
580
+ '-v',
581
+ '--force',
582
+ '--interactive',
583
+ '--no-clobber',
584
+ ]);
585
+ for (let index = 0; index < args.length; index += 1) {
586
+ const arg = args[index] ?? '';
587
+ if (!optionsEnded && arg === '--') {
588
+ optionsEnded = true;
589
+ continue;
590
+ }
591
+ if (!optionsEnded && (arg === '-r' || arg === '-R' || arg === '--recursive')) {
592
+ recursive = true;
593
+ continue;
594
+ }
595
+ if (!optionsEnded && (arg === '-t' || arg === '--target-directory')) {
596
+ const value = args[index + 1];
597
+ if (!value || value.startsWith('-')) {
598
+ incomplete = true;
599
+ }
600
+ else {
601
+ targetDirectory = value;
602
+ index += 1;
603
+ }
604
+ continue;
605
+ }
606
+ if (!optionsEnded && arg.startsWith('--target-directory=')) {
607
+ targetDirectory = arg.slice('--target-directory='.length) || null;
608
+ incomplete ||= !targetDirectory;
609
+ continue;
610
+ }
611
+ if (!optionsEnded && arg.startsWith('-')) {
612
+ if (!neutralFlags.has(arg)) {
613
+ incomplete = true;
614
+ }
615
+ continue;
616
+ }
617
+ operands.push(arg);
618
+ }
619
+ const sources = targetDirectory ? operands : operands.slice(0, -1);
620
+ const destination = targetDirectory ?? (operands.length >= 2 ? (operands.at(-1) ?? null) : null);
621
+ if (sources.length === 0 || !destination) {
622
+ incomplete = true;
623
+ }
624
+ for (const source of sources) {
625
+ const resolved = resolvePathOperand(source, cwd);
626
+ requirements.push(requirement('fs.read', 'fs.read', { kind: 'path', path: resolved }, segment, [
627
+ `${head}.source_read`,
628
+ ]));
629
+ addSecretRead(requirements, resolved, segment);
630
+ if (head === 'mv') {
631
+ addWriteEffects(requirements, resolved, segment, ['mv.source_remove']);
632
+ }
633
+ }
634
+ if (destination) {
635
+ addWriteEffects(requirements, resolvePathOperand(destination, cwd), segment, [
636
+ `${head}.destination_write`,
637
+ ]);
638
+ }
639
+ if (recursive || incomplete) {
640
+ requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, segment, [
641
+ recursive ? `${head}.recursive_source` : `${head}.grammar_incomplete`,
642
+ ]));
643
+ }
644
+ return requirements;
645
+ }
646
+ function decodeRm(args, cwd, repoRoot, segment) {
647
+ const requirements = [processRequirement('rm', 'spawn', segment, ['process.filesystem_mutation'])];
648
+ const recursive = args.some((arg) => arg === '-r' || arg === '-R' || arg === '--recursive' || /^-[^-]*[rR]/.test(arg));
649
+ const operands = args.filter((arg) => arg && !arg.startsWith('-'));
650
+ if (operands.length === 0) {
651
+ return [
652
+ ...requirements,
653
+ requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, segment, [
654
+ 'rm.operand_missing',
655
+ ]),
656
+ ];
657
+ }
658
+ const identity = inspectGitResourceIdentity(repoRoot);
659
+ for (const operand of operands) {
660
+ const resolved = resolvePathOperand(operand, cwd);
661
+ addWriteEffects(requirements, resolved, segment, ['filesystem.write', 'rm.remove']);
662
+ const finalOperandIsSymlink = isSymbolicLink(resolved);
663
+ const canonicalResolved = canonicalRmOperand(resolved, finalOperandIsSymlink);
664
+ const targetIdentity = recursive && !finalOperandIsSymlink
665
+ ? inspectGitResourceIdentity(resolved)
666
+ : { status: 'absent' };
667
+ const sameIdentityTargetRoot = identity.status === 'resolved' &&
668
+ targetIdentity.status === 'resolved' &&
669
+ identity.identity.commonDir === targetIdentity.identity.commonDir &&
670
+ pathContains(canonicalResolved, targetIdentity.identity.repositoryRoot);
671
+ if (recursive &&
672
+ identity.status === 'resolved' &&
673
+ (sameIdentityTargetRoot ||
674
+ [
675
+ repoRoot,
676
+ identity.identity.repositoryRoot,
677
+ identity.identity.gitDir,
678
+ identity.identity.commonDir,
679
+ identity.identity.gitEntryPath,
680
+ ].some((metadataPath) => pathContains(canonicalResolved, metadataPath)))) {
681
+ const gitEntryPath = targetIdentity.status === 'resolved'
682
+ ? targetIdentity.identity.gitEntryPath
683
+ : identity.identity.gitEntryPath;
684
+ requirements.push(requirement('control_plane.write', 'control_plane.write', { kind: 'path', path: gitEntryPath }, segment, ['rm.recursive_git_boundary', 'tier1_catastrophic']));
685
+ }
686
+ }
687
+ return requirements;
688
+ }
689
+ function canonicalRmOperand(targetPath, finalOperandIsSymlink) {
690
+ try {
691
+ if (finalOperandIsSymlink) {
692
+ return path.join(realpathSync.native(path.dirname(targetPath)), path.basename(targetPath));
693
+ }
694
+ return realpathSync.native(targetPath);
695
+ }
696
+ catch {
697
+ return path.resolve(targetPath);
698
+ }
699
+ }
700
+ function isSymbolicLink(targetPath) {
701
+ try {
702
+ return lstatSync(targetPath).isSymbolicLink();
703
+ }
704
+ catch {
705
+ return false;
706
+ }
707
+ }
708
+ function pathContains(ancestor, candidate) {
709
+ const relative = path.relative(path.resolve(ancestor), path.resolve(candidate));
710
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
711
+ }
712
+ function decodeGo(args, segment) {
713
+ if (['test', 'list', 'vet'].includes(args[0] ?? '')) {
714
+ return [processRequirement('go', 'spawn', segment, ['process.go_local_spawn'])];
715
+ }
716
+ if (args[0] === 'install' || (args[0] === 'mod' && args[1] === 'download')) {
717
+ return [
718
+ processRequirement('go', 'spawn', segment, ['process.go_acquire']),
719
+ requirement('network.acquire', 'network.connect', {
720
+ kind: 'network',
721
+ host: 'unknown',
722
+ protocol: 'go-module',
723
+ mode: 'ambiguous',
724
+ payload: 'none',
725
+ }, segment, ['go.network_acquisition_possible']),
726
+ ];
727
+ }
728
+ return unsupportedProcess('go', segment, 'process.go_grammar_incomplete');
729
+ }
730
+ function decodeRsync(args, cwd, segment) {
731
+ const destructive = args.some((arg) => arg === '--delete' || arg === '--del' || arg.startsWith('--delete-'));
732
+ if (destructive) {
733
+ return [
734
+ processRequirement('rsync', 'spawn', segment, ['rsync_destructive']),
735
+ requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, segment, [
736
+ 'rsync_destructive',
737
+ ]),
738
+ ];
739
+ }
740
+ const allowedFlags = new Set([
741
+ '-a',
742
+ '-r',
743
+ '-v',
744
+ '-z',
745
+ '--archive',
746
+ '--delay-updates',
747
+ '--recursive',
748
+ '--verbose',
749
+ ]);
750
+ if (args.some((arg) => arg.startsWith('-') && !allowedFlags.has(arg))) {
751
+ return unsupportedProcess('rsync', segment, 'process.rsync_grammar_incomplete');
752
+ }
753
+ const operands = args.filter((arg) => !arg.startsWith('-'));
754
+ if (operands.length < 2) {
755
+ return unsupportedProcess('rsync', segment, 'process.rsync_operands_incomplete');
756
+ }
757
+ if (operands.some(isRemoteRsyncOperand)) {
758
+ return [
759
+ processRequirement('rsync', 'spawn', segment, ['rsync.remote']),
760
+ requirement('network.connect', 'network.connect', {
761
+ kind: 'network',
762
+ host: 'remote',
763
+ protocol: 'rsync',
764
+ mode: 'mutate',
765
+ payload: 'present',
766
+ }, segment, ['rsync.remote']),
767
+ ];
768
+ }
769
+ const destination = operands.at(-1) ?? '';
770
+ return [
771
+ processRequirement('rsync', 'spawn', segment, ['process.rsync_local']),
772
+ ...operands
773
+ .slice(0, -1)
774
+ .map((operand) => requirement('fs.read', 'fs.read', { kind: 'path', path: resolvePathOperand(operand, cwd) }, segment, ['rsync.local_source'])),
775
+ requirement('fs.write', 'fs.write', { kind: 'path', path: resolvePathOperand(destination, cwd) }, segment, ['rsync.local_destination']),
776
+ ];
777
+ }
778
+ function isRemoteRsyncOperand(operand) {
779
+ return (/^rsync:\/\//i.test(operand) ||
780
+ /^[^/:\s]+::/.test(operand) ||
781
+ /^(?:[^@/:\s]+@)?[^/:\s]+:/.test(operand));
782
+ }
783
+ function decodeSed(args, cwd, segment) {
784
+ let inline = false;
785
+ let scriptConsumed = false;
786
+ const files = [];
787
+ for (let index = 0; index < args.length; index += 1) {
788
+ const arg = args[index] ?? '';
789
+ if (arg === '-n' || arg === '--quiet' || arg === '--silent') {
790
+ continue;
791
+ }
792
+ if (arg === '-i' || arg === '--in-place' || arg.startsWith('-i')) {
793
+ inline = true;
794
+ continue;
795
+ }
796
+ if (arg === '-e' || arg === '--expression') {
797
+ if (!args[index + 1]) {
798
+ return unsupportedProcess('sed', segment, 'process.sed_expression_missing');
799
+ }
800
+ scriptConsumed = true;
801
+ index += 1;
802
+ continue;
803
+ }
804
+ if (arg.startsWith('--expression=')) {
805
+ scriptConsumed = true;
806
+ continue;
807
+ }
808
+ if (arg.startsWith('-')) {
809
+ return unsupportedProcess('sed', segment, 'process.sed_grammar_incomplete');
810
+ }
811
+ if (!scriptConsumed) {
812
+ scriptConsumed = true;
813
+ continue;
814
+ }
815
+ files.push(arg);
816
+ }
817
+ const operation = inline ? 'spawn' : 'inspect';
818
+ const lowered = [processRequirement('sed', operation, segment, ['process.sed'])];
819
+ for (const file of files) {
820
+ lowered.push(requirement(inline ? 'fs.write' : 'fs.read', inline ? 'fs.write' : 'fs.read', { kind: 'path', path: resolvePathOperand(file, cwd) }, segment, [inline ? 'sed.in_place_write' : 'sed.file_read']));
821
+ }
822
+ return lowered;
823
+ }
824
+ function decodeNode(args, cwd, segment) {
825
+ if (args.length > 0 && args.every((arg) => ['--help', '--version', '-h', '-v'].includes(arg))) {
826
+ return [processRequirement('node', 'inspect', segment, ['process.inspect.node_metadata'])];
827
+ }
828
+ if ((args[0] === '--check' || args[0] === '-c') && args.length === 2 && args[1]) {
829
+ return [
830
+ processRequirement('node', 'inspect', segment, ['process.inspect.node_syntax']),
831
+ requirement('fs.read', 'fs.read', { kind: 'path', path: resolvePathOperand(args[1], cwd) }, segment, ['node.syntax_source_read']),
832
+ ];
833
+ }
834
+ return unsupportedProcess('node', segment, 'process.node_grammar_incomplete');
835
+ }
836
+ function decodePrisma(args, env, repoRoot, segment) {
837
+ const processEffect = processRequirement('prisma', 'spawn', segment, ['process.prisma']);
838
+ if (args[0] === 'generate') {
839
+ return [
840
+ processEffect,
841
+ requirement('fs.write', 'fs.write', { kind: 'path', path: repoRoot }, segment, [
842
+ 'prisma.generate.repo_local',
843
+ ]),
844
+ ];
845
+ }
846
+ const databaseMutation = args[0] === 'migrate' || (args[0] === 'db' && args[1] === 'seed') || args[0] === 'seed';
847
+ if (!databaseMutation) {
848
+ return [
849
+ processEffect,
850
+ requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, segment, [
851
+ 'prisma.grammar_incomplete',
852
+ ]),
853
+ ];
854
+ }
855
+ const endpoint = databaseEndpoint(env.DATABASE_URL);
856
+ if (!endpoint) {
857
+ return [
858
+ processEffect,
859
+ requirement('network.connect', 'network.connect', {
860
+ kind: 'network',
861
+ host: 'unknown',
862
+ protocol: 'database',
863
+ mode: 'ambiguous',
864
+ payload: 'present',
865
+ }, segment, ['prisma.database_endpoint_unknown']),
866
+ requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, segment, [
867
+ 'prisma.database_endpoint_unknown',
868
+ ]),
869
+ ];
870
+ }
871
+ const network = requirement('network.connect', 'network.connect', {
872
+ kind: 'network',
873
+ ...endpoint,
874
+ mode: 'mutate',
875
+ payload: 'present',
876
+ }, segment, [LOOPBACK_HOSTS.has(endpoint.host) ? 'prisma.database_local' : 'prisma.database_remote']);
877
+ if (LOOPBACK_HOSTS.has(endpoint.host)) {
878
+ return [processEffect, network];
879
+ }
880
+ return [
881
+ processEffect,
882
+ network,
883
+ requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, segment, [
884
+ 'prisma.remote_database_mutation',
885
+ ]),
886
+ ];
887
+ }
888
+ function addRedirectEffects(requirements, tokens, env, context, segment) {
889
+ for (let index = 0; index < tokens.length; index += 1) {
890
+ const operator = tokens[index] ?? '';
891
+ if (!isRedirectOperator(operator) || isFdDuplication(operator)) {
892
+ continue;
893
+ }
894
+ const rawTarget = tokens[index + 1];
895
+ if (!rawTarget) {
896
+ requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, segment, [
897
+ 'shell.redirect_target_missing',
898
+ ]));
899
+ continue;
900
+ }
901
+ if (operator.includes('<<')) {
902
+ requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, segment, [
903
+ 'shell.heredoc_opaque',
904
+ ]));
905
+ index += 1;
906
+ continue;
907
+ }
908
+ const target = expandKnownVariables(rawTarget, env);
909
+ const resolved = resolvePathOperand(target, context.cwd);
910
+ if (resolved === '/dev/null') {
911
+ index += 1;
912
+ continue;
913
+ }
914
+ if (operator.includes('<')) {
915
+ requirements.push(requirement('fs.read', 'fs.read', { kind: 'path', path: resolved }, segment, [
916
+ 'shell.input_redirect',
917
+ ]));
918
+ addSecretRead(requirements, resolved, segment);
919
+ }
920
+ if (operator.includes('>')) {
921
+ addWriteEffects(requirements, resolved, segment, ['shell.output_redirect']);
922
+ }
923
+ index += 1;
924
+ }
925
+ }
926
+ function addSubstitutionEffects(requirements, command, context, segment, signals) {
927
+ const inners = structuralSubstitutionInners(command);
928
+ for (const inner of inners) {
929
+ signals.add('command_substitution');
930
+ if (context.depth >= MAX_LOWER_DEPTH) {
931
+ requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, segment, [
932
+ 'shell.substitution_depth_exceeded',
933
+ ]));
934
+ continue;
935
+ }
936
+ const nestedSegments = lowerTopLevelSegments(inner, {
937
+ ...context,
938
+ command: inner,
939
+ depth: context.depth + 1,
940
+ });
941
+ for (const nested of nestedSegments) {
942
+ requirements.push(...nested.requirements.map((entry) => {
943
+ const outerProvenance = {
944
+ segment,
945
+ innerCommand: inner,
946
+ };
947
+ const provenance = {
948
+ ...entry.provenance,
949
+ segment,
950
+ innerCommand: entry.provenance.innerCommand ?? inner,
951
+ };
952
+ return withProvenances(entry, provenance, outerProvenance);
953
+ }));
954
+ }
955
+ }
956
+ }
957
+ function extractEnvironment(tokens, inherited) {
958
+ const env = { ...inherited };
959
+ const changedNames = new Set();
960
+ if (tokens[0] === 'env') {
961
+ let index = 1;
962
+ let malformed = false;
963
+ while (index < tokens.length) {
964
+ const option = tokens[index] ?? '';
965
+ if (option === '-u' || option === '--unset') {
966
+ const name = tokens[index + 1] ?? '';
967
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
968
+ malformed = true;
969
+ index += 1;
970
+ break;
971
+ }
972
+ delete env[name];
973
+ changedNames.add(name);
974
+ index += 2;
975
+ continue;
976
+ }
977
+ if (option.startsWith('--unset=')) {
978
+ const name = option.slice('--unset='.length);
979
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
980
+ malformed = true;
981
+ index += 1;
982
+ break;
983
+ }
984
+ delete env[name];
985
+ changedNames.add(name);
986
+ index += 1;
987
+ continue;
988
+ }
989
+ if (option === '-i' || option === '--ignore-environment') {
990
+ for (const name of Object.keys(env)) {
991
+ delete env[name];
992
+ }
993
+ changedNames.add('*');
994
+ index += 1;
995
+ continue;
996
+ }
997
+ if (option === '--') {
998
+ index += 1;
999
+ break;
1000
+ }
1001
+ if (option.startsWith('-')) {
1002
+ malformed = true;
1003
+ index += 1;
1004
+ break;
1005
+ }
1006
+ break;
1007
+ }
1008
+ while (index < tokens.length) {
1009
+ const token = tokens[index] ?? '';
1010
+ const match = ENV_PREFIX_PATTERN.exec(token);
1011
+ if (!match) {
1012
+ break;
1013
+ }
1014
+ const [, name, value] = match;
1015
+ if (name) {
1016
+ env[name] = expandKnownVariables(value ?? '', env);
1017
+ changedNames.add(name);
1018
+ }
1019
+ index += 1;
1020
+ }
1021
+ const commandTokens = tokens.slice(index);
1022
+ return {
1023
+ env,
1024
+ commandTokens,
1025
+ malformed: malformed || commandTokens.length === 0,
1026
+ changedNames,
1027
+ };
1028
+ }
1029
+ for (const token of tokens) {
1030
+ const match = ENV_PREFIX_PATTERN.exec(token);
1031
+ if (!match) {
1032
+ break;
1033
+ }
1034
+ const [, name, value] = match;
1035
+ if (name) {
1036
+ env[name] = expandKnownVariables(value ?? '', env);
1037
+ changedNames.add(name);
1038
+ }
1039
+ }
1040
+ return { env, malformed: false, changedNames };
1041
+ }
1042
+ function expandKnownVariables(token, env) {
1043
+ return token.replace(/\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g, (raw, a, b) => {
1044
+ const value = env[a ?? b];
1045
+ return value === undefined ? raw : value;
1046
+ });
1047
+ }
1048
+ function stripRedirects(tokens) {
1049
+ const stripped = [];
1050
+ for (let index = 0; index < tokens.length; index += 1) {
1051
+ const token = tokens[index] ?? '';
1052
+ if (isFdDuplication(token)) {
1053
+ continue;
1054
+ }
1055
+ if (!isRedirectOperator(token)) {
1056
+ stripped.push(token);
1057
+ continue;
1058
+ }
1059
+ if (token.includes('>') || token.includes('<')) {
1060
+ const inline = token.replace(/^\d*(?:>>?|<<?|<>|>\|)/, '');
1061
+ if (!inline) {
1062
+ index += 1;
1063
+ }
1064
+ }
1065
+ }
1066
+ return stripped;
1067
+ }
1068
+ function shellSegment(commandRedacted, segmentHead, requirements, opacity, signals) {
1069
+ const normalizedRequirements = requirements.flatMap((entry) => {
1070
+ const dynamicSignal = dynamicResourceSignal(entry.resource);
1071
+ if (dynamicSignal) {
1072
+ return [
1073
+ { ...entry, resource: { kind: 'unknown' } },
1074
+ requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, entry.provenance.segment ?? commandRedacted, [...entry.evidence.signals, dynamicSignal]),
1075
+ ];
1076
+ }
1077
+ return [entry];
1078
+ });
1079
+ const partial = normalizedRequirements.some((entry) => entry.tag === 'indeterminate' || entry.action === 'indeterminate');
1080
+ return {
1081
+ commandRedacted,
1082
+ segmentHead,
1083
+ requirements: normalizedRequirements,
1084
+ completeness: partial ? 'partial' : 'complete',
1085
+ opacity,
1086
+ signals: [...signals].sort(),
1087
+ };
1088
+ }
1089
+ const DYNAMIC_SHELL_VALUE_PATTERN = /(?:\$\(|`|\$(?:\d+|[@*#?$!-]|\{[^}]*\}|[A-Za-z_][A-Za-z0-9_]*))/;
1090
+ const SHELL_GLOB_PATTERN = /[*?[]/;
1091
+ function effectChangingEnvironmentSignals(head, env, changedNames) {
1092
+ if (head === 'curl' &&
1093
+ (env.CURL_HOME ||
1094
+ changedNames.has('CURL_HOME') ||
1095
+ changedNames.has('HOME') ||
1096
+ changedNames.has('XDG_CONFIG_HOME'))) {
1097
+ return ['egress.curl.environment_config_override'];
1098
+ }
1099
+ if (head === 'wget' && (env.WGETRC || changedNames.has('WGETRC') || changedNames.has('HOME'))) {
1100
+ return ['egress.wget.environment_config_override'];
1101
+ }
1102
+ if (head !== 'git') {
1103
+ return [];
1104
+ }
1105
+ const hazardousNames = new Set([
1106
+ 'GIT_ASKPASS',
1107
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
1108
+ 'GIT_CEILING_DIRECTORIES',
1109
+ 'GIT_COMMON_DIR',
1110
+ 'GIT_CONFIG',
1111
+ 'GIT_CONFIG_GLOBAL',
1112
+ 'GIT_CONFIG_SYSTEM',
1113
+ 'GIT_DIR',
1114
+ 'GIT_DISCOVERY_ACROSS_FILESYSTEM',
1115
+ 'GIT_EDITOR',
1116
+ 'GIT_EXEC_PATH',
1117
+ 'GIT_EXTERNAL_DIFF',
1118
+ 'GIT_INDEX_FILE',
1119
+ 'GIT_NAMESPACE',
1120
+ 'GIT_OBJECT_DIRECTORY',
1121
+ 'GIT_PAGER',
1122
+ 'GIT_PROXY_COMMAND',
1123
+ 'GIT_QUARANTINE_PATH',
1124
+ 'GIT_SEQUENCE_EDITOR',
1125
+ 'GIT_SHALLOW_FILE',
1126
+ 'GIT_SSH',
1127
+ 'GIT_SSH_COMMAND',
1128
+ 'GIT_TEMPLATE_DIR',
1129
+ 'GIT_WORK_TREE',
1130
+ 'SSH_ASKPASS',
1131
+ ]);
1132
+ const overridden = changedNames.has('HOME') ||
1133
+ changedNames.has('XDG_CONFIG_HOME') ||
1134
+ Object.entries(env).some(([name, value]) => {
1135
+ if (!value) {
1136
+ return false;
1137
+ }
1138
+ if (hazardousNames.has(name) ||
1139
+ /^GIT_CONFIG_(?:KEY|VALUE)_\d+$/.test(name) ||
1140
+ /^GIT_TRACE/.test(name)) {
1141
+ return true;
1142
+ }
1143
+ return name === 'GIT_CONFIG_COUNT' && value !== '0';
1144
+ });
1145
+ return overridden ? ['git.environment_execution_or_config_override'] : [];
1146
+ }
1147
+ function dynamicResourceSignal(resource) {
1148
+ const expanded = (value) => Boolean(value && DYNAMIC_SHELL_VALUE_PATTERN.test(value));
1149
+ const pathLike = (value, allowSemanticWildcard = false) => Boolean(value && (expanded(value) || (!allowSemanticWildcard && SHELL_GLOB_PATTERN.test(value))));
1150
+ switch (resource.kind) {
1151
+ case 'path':
1152
+ return pathLike(resource.path) ? 'shell.path_unresolved' : null;
1153
+ case 'network':
1154
+ return expanded(resource.host) || expanded(resource.protocol)
1155
+ ? 'shell.network_resource_unresolved'
1156
+ : null;
1157
+ case 'git-ref':
1158
+ return pathLike(resource.repoPath) || pathLike(resource.ref, resource.ref.endsWith('/*'))
1159
+ ? 'shell.git_resource_unresolved'
1160
+ : null;
1161
+ case 'executable':
1162
+ return expanded(resource.command) ? 'shell.executable_unresolved' : null;
1163
+ default:
1164
+ return null;
1165
+ }
1166
+ }
1167
+ function processRequirement(command, operation, segment, signals) {
1168
+ return requirement('process.exec', 'process.exec', { kind: 'executable', command, operation }, segment, signals);
1169
+ }
1170
+ function unsupportedProcess(command, segment, signal) {
1171
+ return [
1172
+ processRequirement(command, 'spawn', segment, [signal]),
1173
+ requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, segment, [signal]),
1174
+ ];
1175
+ }
1176
+ function addWriteEffects(requirements, resolved, segment, signals) {
1177
+ requirements.push(requirement('fs.write', 'fs.write', { kind: 'path', path: resolved }, segment, signals));
1178
+ if (CONTROL_PLANE_PATH_PATTERN.test(resolved)) {
1179
+ requirements.push(requirement('control_plane.write', 'control_plane.write', { kind: 'path', path: resolved }, segment, [...signals, 'protected_path']));
1180
+ }
1181
+ }
1182
+ function addSecretRead(requirements, resolved, segment) {
1183
+ if (SECRET_PATH_PATTERN.test(resolved)) {
1184
+ requirements.push(requirement('secret.read', 'secret.read', { kind: 'path', path: resolved }, segment, [
1185
+ 'secret_path_read',
1186
+ ]));
1187
+ }
1188
+ }
1189
+ function filesystemReadOperands(head, args) {
1190
+ switch (head) {
1191
+ case 'ls': {
1192
+ const operands = args.filter((arg) => arg && !arg.startsWith('-'));
1193
+ return operands.length > 0 ? operands : ['.'];
1194
+ }
1195
+ case 'find':
1196
+ return findReadOperands(args);
1197
+ case 'cat':
1198
+ case 'head':
1199
+ case 'tail':
1200
+ case 'less':
1201
+ case 'stat':
1202
+ case 'file':
1203
+ case 'du':
1204
+ case 'wc':
1205
+ return args.filter((arg) => arg && !arg.startsWith('-'));
1206
+ case 'grep':
1207
+ case 'rg':
1208
+ return args.slice(1).filter((arg) => arg && !arg.startsWith('-'));
1209
+ case 'jq':
1210
+ return args.slice(1).filter((arg) => arg && !arg.startsWith('-'));
1211
+ default:
1212
+ return null;
1213
+ }
1214
+ }
1215
+ function findReadOperands(args) {
1216
+ const mutatingPrimaries = new Set([
1217
+ '-delete',
1218
+ '-exec',
1219
+ '-execdir',
1220
+ '-fls',
1221
+ '-fprint',
1222
+ '-fprintf',
1223
+ '-ok',
1224
+ '-okdir',
1225
+ ]);
1226
+ if (args.some((arg) => mutatingPrimaries.has(arg) ||
1227
+ [...mutatingPrimaries].some((primary) => arg.startsWith(`${primary}=`)))) {
1228
+ return null;
1229
+ }
1230
+ const paths = [];
1231
+ for (const arg of args) {
1232
+ if (arg.startsWith('-') || arg === '!' || arg === '(') {
1233
+ break;
1234
+ }
1235
+ paths.push(arg);
1236
+ }
1237
+ return paths.length > 0 ? paths : ['.'];
1238
+ }
1239
+ function filesystemWriteOperands(head, args) {
1240
+ const positional = args.filter((arg) => arg && !arg.startsWith('-'));
1241
+ switch (head) {
1242
+ case 'touch':
1243
+ case 'mkdir':
1244
+ case 'rm':
1245
+ case 'truncate':
1246
+ case 'chmod':
1247
+ return positional;
1248
+ case 'cp':
1249
+ case 'mv':
1250
+ return positional.length > 0 ? [positional[positional.length - 1] ?? ''] : [];
1251
+ case 'tee':
1252
+ return positional;
1253
+ default:
1254
+ return null;
1255
+ }
1256
+ }
1257
+ function validLsof(args) {
1258
+ const flagsWithoutValues = new Set(['-a', '-b', '-l', '-n', '-P', '-R', '-t', '-V']);
1259
+ const flagsWithValues = new Set(['-c', '-d', '-F', '-g', '-p', '-T', '-u', '+d', '+D']);
1260
+ for (let index = 0; index < args.length; index += 1) {
1261
+ const arg = args[index] ?? '';
1262
+ if (flagsWithoutValues.has(arg) || arg === '-i') {
1263
+ continue;
1264
+ }
1265
+ if (flagsWithValues.has(arg)) {
1266
+ if (!args[index + 1] || isOptionToken(args[index + 1] ?? '')) {
1267
+ return false;
1268
+ }
1269
+ index += 1;
1270
+ continue;
1271
+ }
1272
+ if ((/^-[ablnPRtV]+$/.test(arg) &&
1273
+ [...arg.slice(1)].every((flag) => 'ablnPRtV'.includes(flag))) ||
1274
+ /^-i(?:TCP|UDP)?(?::\d+(?:-\d+)?)?$/.test(arg) ||
1275
+ /^-s(?:TCP|UDP):[A-Za-z]+$/.test(arg) ||
1276
+ /^-(?:c|d|F|g|p|T|u).+$/.test(arg) ||
1277
+ /^\+(?:d|D).+$/.test(arg)) {
1278
+ continue;
1279
+ }
1280
+ return false;
1281
+ }
1282
+ return true;
1283
+ }
1284
+ function validPs(args) {
1285
+ const bareForms = new Set(['aux', 'ax', 'x', 'u']);
1286
+ const flagsWithoutValues = new Set(['-A', '-a', '-d', '-e', '-f', '-j', '-l', '-T', '-x']);
1287
+ const flagsWithValues = new Set([
1288
+ '--format',
1289
+ '--group',
1290
+ '--pid',
1291
+ '--ppid',
1292
+ '--sort',
1293
+ '--tty',
1294
+ '--user',
1295
+ ]);
1296
+ for (let index = 0; index < args.length; index += 1) {
1297
+ const arg = args[index] ?? '';
1298
+ if (bareForms.has(arg) || flagsWithoutValues.has(arg)) {
1299
+ continue;
1300
+ }
1301
+ if (flagsWithValues.has(arg)) {
1302
+ if (!args[index + 1] || isOptionToken(args[index + 1] ?? '')) {
1303
+ return false;
1304
+ }
1305
+ index += 1;
1306
+ continue;
1307
+ }
1308
+ if (/^(?:--format|--group|--pid|--ppid|--sort|--tty|--user)=.+$/.test(arg)) {
1309
+ continue;
1310
+ }
1311
+ if (/^-[^-]/.test(arg)) {
1312
+ const noValue = new Set(['A', 'a', 'd', 'e', 'f', 'j', 'l', 'T', 'x']);
1313
+ const withValue = new Set(['G', 'g', 'N', 'o', 'p', 't', 'U', 'u']);
1314
+ const characters = arg.slice(1);
1315
+ let valid = true;
1316
+ for (let optionIndex = 0; optionIndex < characters.length; optionIndex += 1) {
1317
+ const option = characters[optionIndex] ?? '';
1318
+ if (noValue.has(option)) {
1319
+ continue;
1320
+ }
1321
+ if (withValue.has(option)) {
1322
+ if (optionIndex === characters.length - 1) {
1323
+ if (!args[index + 1] || isOptionToken(args[index + 1] ?? '')) {
1324
+ return false;
1325
+ }
1326
+ index += 1;
1327
+ }
1328
+ break;
1329
+ }
1330
+ valid = false;
1331
+ break;
1332
+ }
1333
+ if (valid) {
1334
+ continue;
1335
+ }
1336
+ }
1337
+ return false;
1338
+ }
1339
+ return true;
1340
+ }
1341
+ function validFilesystemTest(head, args) {
1342
+ const operands = head === '[' && args.at(-1) === ']' ? args.slice(0, -1) : args;
1343
+ if (head === '[' && args.at(-1) !== ']') {
1344
+ return false;
1345
+ }
1346
+ return (operands.length === 2 &&
1347
+ new Set(['-b', '-c', '-d', '-e', '-f', '-h', '-L', '-r', '-s', '-w', '-x']).has(operands[0] ?? '') &&
1348
+ Boolean(operands[1]));
1349
+ }
1350
+ function validDockerInfo(args) {
1351
+ for (let index = 0; index < args.length; index += 1) {
1352
+ const arg = args[index] ?? '';
1353
+ if (arg === '--help') {
1354
+ continue;
1355
+ }
1356
+ if (arg === '--format' || arg === '-f') {
1357
+ if (!args[index + 1] || isOptionToken(args[index + 1] ?? '')) {
1358
+ return false;
1359
+ }
1360
+ index += 1;
1361
+ continue;
1362
+ }
1363
+ if (arg.startsWith('--format=')) {
1364
+ if (arg.slice('--format='.length).length === 0) {
1365
+ return false;
1366
+ }
1367
+ continue;
1368
+ }
1369
+ return false;
1370
+ }
1371
+ return true;
1372
+ }
1373
+ function bashSyntaxTarget(args) {
1374
+ let syntaxOnly = false;
1375
+ let script = null;
1376
+ const shortFlags = new Set('abefhkmnptuvxBCEHPT'.split(''));
1377
+ const longFlags = new Set([
1378
+ '--debugger',
1379
+ '--dump-po-strings',
1380
+ '--dump-strings',
1381
+ '--help',
1382
+ '--login',
1383
+ '--noediting',
1384
+ '--noprofile',
1385
+ '--norc',
1386
+ '--posix',
1387
+ '--restricted',
1388
+ '--verbose',
1389
+ '--version',
1390
+ ]);
1391
+ for (let index = 0; index < args.length; index += 1) {
1392
+ const arg = args[index] ?? '';
1393
+ if (arg === '-c' || arg === '-lc') {
1394
+ return null;
1395
+ }
1396
+ if (arg === '-O' || arg === '+O' || arg === '--rcfile' || arg === '--init-file') {
1397
+ if (!args[index + 1] || isOptionToken(args[index + 1] ?? '')) {
1398
+ return null;
1399
+ }
1400
+ index += 1;
1401
+ continue;
1402
+ }
1403
+ if ((arg.startsWith('-O') && arg.length > 2) ||
1404
+ (arg.startsWith('+O') && arg.length > 2) ||
1405
+ (arg.startsWith('--rcfile=') && arg.length > '--rcfile='.length) ||
1406
+ (arg.startsWith('--init-file=') && arg.length > '--init-file='.length)) {
1407
+ continue;
1408
+ }
1409
+ if (longFlags.has(arg)) {
1410
+ continue;
1411
+ }
1412
+ if (/^[+-][A-Za-z]+$/.test(arg)) {
1413
+ const options = arg.slice(1);
1414
+ if ([...options].some((option) => !shortFlags.has(option))) {
1415
+ return null;
1416
+ }
1417
+ syntaxOnly ||= options.includes('n');
1418
+ continue;
1419
+ }
1420
+ if (arg === '--') {
1421
+ script = args[index + 1] ?? null;
1422
+ break;
1423
+ }
1424
+ if (arg.startsWith('-') || arg.startsWith('+')) {
1425
+ return null;
1426
+ }
1427
+ script = arg;
1428
+ break;
1429
+ }
1430
+ return syntaxOnly ? script : null;
1431
+ }
1432
+ function isOptionToken(value) {
1433
+ return value.startsWith('-') || value.startsWith('+');
1434
+ }
1435
+ function databaseEndpoint(raw) {
1436
+ if (!raw || raw.includes('$')) {
1437
+ return null;
1438
+ }
1439
+ try {
1440
+ const url = new URL(raw);
1441
+ if (!url.hostname || !url.protocol) {
1442
+ return null;
1443
+ }
1444
+ return {
1445
+ host: url.hostname.toLowerCase(),
1446
+ protocol: url.protocol.slice(0, -1),
1447
+ ...(url.port ? { port: Number(url.port) } : {}),
1448
+ };
1449
+ }
1450
+ catch {
1451
+ return null;
1452
+ }
1453
+ }
1454
+ function resolvePathOperand(operand, cwd) {
1455
+ if (operand === '~') {
1456
+ return process.env.HOME ?? operand;
1457
+ }
1458
+ if (operand.startsWith('~/')) {
1459
+ return path.join(process.env.HOME ?? '~', operand.slice(2));
1460
+ }
1461
+ return path.resolve(cwd, operand);
1462
+ }
1463
+ function isShellHead(head) {
1464
+ return head === 'bash' || head === 'sh' || head === 'zsh' || head === 'dash' || head === 'fish';
1465
+ }
1466
+ function pipeToShell(command) {
1467
+ return /(?:^|[|;&]\s*)(?:bash|sh|zsh|dash|fish)(?:\s|$)/.test(command) && /\|/.test(command);
1468
+ }
1469
+ function withInnerProvenance(requirementValue, innerCommand, launcher, outerSegment) {
1470
+ const provenance = {
1471
+ ...requirementValue.provenance,
1472
+ segment: outerSegment,
1473
+ innerCommand: requirementValue.provenance.innerCommand ?? innerCommand,
1474
+ ...(launcher === 'npm' || launcher === 'pnpm' ? { launcher, phase: 'delegate' } : {}),
1475
+ };
1476
+ const launcherProvenance = {
1477
+ segment: outerSegment,
1478
+ innerCommand,
1479
+ ...(launcher === 'npm' || launcher === 'pnpm' ? { launcher, phase: 'delegate' } : {}),
1480
+ };
1481
+ return withProvenances(requirementValue, provenance, launcherProvenance);
1482
+ }
1483
+ function withProvenances(requirementValue, provenance, contributing) {
1484
+ const provenances = [
1485
+ ...(requirementValue.provenances ?? [requirementValue.provenance]),
1486
+ contributing,
1487
+ provenance,
1488
+ ].filter((candidate, index, all) => all.findIndex((entry) => JSON.stringify(entry) === JSON.stringify(candidate)) === index);
1489
+ return { ...requirementValue, provenance, provenances };
1490
+ }
1491
+ function ensureRequirement(requirements, candidate) {
1492
+ if (!requirements.some((entry) => entry.action === candidate.action &&
1493
+ JSON.stringify(entry.resource) === JSON.stringify(candidate.resource))) {
1494
+ requirements.push(candidate);
1495
+ }
1496
+ }
1497
+ function requirement(tag, action, resource, segment, signals) {
1498
+ return {
1499
+ tag,
1500
+ action,
1501
+ resource,
1502
+ evidence: {
1503
+ level: tag === 'indeterminate' ? 'indeterminate' : 'certain',
1504
+ signals: [...new Set(signals)].sort(),
1505
+ basis: ['shell_semantic_lowering'],
1506
+ },
1507
+ provenance: { segment },
1508
+ };
1509
+ }