@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
@@ -2,6 +2,8 @@
2
2
  * Egress tool classification (SPEC v2.1.3 R33–R34).
3
3
  * destructive → ask | read → allow at egress layer | ambiguous → require_approval via PolicyEngine
4
4
  */
5
+ import path from 'node:path';
6
+ import { parseNetworkEndpoint } from '../network-endpoint.js';
5
7
  const EGRESS_TOOL_HEADS = new Set([
6
8
  'aws',
7
9
  'curl',
@@ -16,6 +18,38 @@ const EGRESS_TOOL_HEADS = new Set([
16
18
  const READ_VERB_PATTERN = /\b(ls|list|describe|get|view|logs|status|top|head|explain)\b/;
17
19
  const CURL_DATA_FLAGS = new Set(['-d', '-F', '-T', '--post-data', '--post-file', '--upload-file']);
18
20
  const CURL_DATA_PREFIXES = ['--data', '--form', '--upload-file', '--post-'];
21
+ const CURL_EFFECT_NEUTRAL_FLAGS = new Set([
22
+ '-f',
23
+ '-L',
24
+ '-s',
25
+ '-S',
26
+ '--fail',
27
+ '--fail-with-body',
28
+ '--location',
29
+ '--show-error',
30
+ '--silent',
31
+ ]);
32
+ const SECRET_VALUE_PATTERN = /(?:\$\{?[A-Za-z_][A-Za-z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASS|KEY|CREDENTIAL)[A-Za-z0-9_]*\}?|(?:^|[/\\])\.env(?:\.|$)|credentials?|secrets?|id_rsa|\.pem\b)/i;
33
+ const GH_READ_COMMANDS = new Set([
34
+ 'api',
35
+ 'auth status',
36
+ 'issue list',
37
+ 'issue status',
38
+ 'issue view',
39
+ 'pr checks',
40
+ 'pr diff',
41
+ 'pr list',
42
+ 'pr status',
43
+ 'pr view',
44
+ 'release list',
45
+ 'release view',
46
+ 'repo list',
47
+ 'repo view',
48
+ 'run list',
49
+ 'run view',
50
+ 'workflow list',
51
+ 'workflow view',
52
+ ]);
19
53
  const KUBECTL_DESTRUCTIVE = new Set([
20
54
  'apply',
21
55
  'cordon',
@@ -62,6 +96,718 @@ export function classifyEgressTool(head, tokens) {
62
96
  }
63
97
  return 'ambiguous';
64
98
  }
99
+ /**
100
+ * Decode curl/wget/gh grammar into typed effects. This API deliberately has no
101
+ * permission or verdict output; uncertainty is represented by indeterminate
102
+ * requirements alongside every effect that was still recoverable.
103
+ */
104
+ export function decodeEgressEffects(params) {
105
+ const head = path.basename(params.tokens[0] ?? '');
106
+ if (head !== 'curl' && head !== 'wget' && head !== 'gh') {
107
+ return null;
108
+ }
109
+ const decoded = head === 'gh' ? decodeGhGrammar(params.tokens) : decodeCurlWgetGrammar(head, params.tokens);
110
+ const provenance = { segment: params.segment };
111
+ const requirements = [];
112
+ for (const file of decoded.files) {
113
+ const resolved = path.resolve(params.cwd, expandHome(file));
114
+ requirements.push(requirement('fs.read', 'fs.read', { kind: 'path', path: resolved }, params.segment, [
115
+ 'egress.explicit_file_read',
116
+ ]));
117
+ if (decoded.secretFiles.includes(file) ||
118
+ (!decoded.nonSecretFiles.includes(file) && isSecretValue(file))) {
119
+ requirements.push(requirement('secret.read', 'secret.read', { kind: 'path', path: resolved }, params.segment, ['egress.explicit_secret_payload']));
120
+ }
121
+ }
122
+ for (const file of decoded.outputFiles) {
123
+ if (file === '-') {
124
+ continue;
125
+ }
126
+ const resolved = path.resolve(params.cwd, expandHome(file));
127
+ if (resolved === '/dev/null') {
128
+ continue;
129
+ }
130
+ requirements.push(requirement('fs.write', 'fs.write', { kind: 'path', path: resolved }, params.segment, [
131
+ 'egress.output_write',
132
+ ]));
133
+ if (/^(?:\/etc(?:\/|$)|\/var\/run(?:\/|$)|.*(?:^|[/\\])\.(?:git|ssh|cursor|claude)(?:[/\\]|$))/.test(resolved)) {
134
+ requirements.push(requirement('control_plane.write', 'control_plane.write', { kind: 'path', path: resolved }, params.segment, ['egress.output_write', 'protected_path']));
135
+ }
136
+ }
137
+ if (decoded.secretRead) {
138
+ requirements.push(requirement('secret.read', 'secret.read', { kind: 'unknown' }, params.segment, [
139
+ 'egress.ambient_secret_read',
140
+ ]));
141
+ }
142
+ if (decoded.ambientWrite) {
143
+ requirements.push(requirement('fs.write', 'fs.write', { kind: 'unknown' }, params.segment, [
144
+ 'egress.ambient_write',
145
+ ]));
146
+ }
147
+ for (const command of decoded.processSpawns ?? []) {
148
+ requirements.push(requirement('process.exec', 'process.exec', { kind: 'executable', command, operation: 'spawn' }, params.segment, ['egress.external_process_spawn']));
149
+ }
150
+ if (decoded.transfers.some((transfer) => transfer.payload === 'secret') &&
151
+ !requirements.some((entry) => entry.tag === 'secret.read')) {
152
+ requirements.push(requirement('secret.read', 'secret.read', { kind: 'unknown' }, params.segment, [
153
+ 'egress.explicit_secret_payload',
154
+ ]));
155
+ }
156
+ for (const transfer of decoded.transfers) {
157
+ requirements.push({
158
+ ...requirement('network.connect', 'network.connect', {
159
+ kind: 'network',
160
+ ...transfer.endpoint,
161
+ mode: transfer.mode,
162
+ payload: transfer.payload,
163
+ }, params.segment, decoded.signals),
164
+ provenance,
165
+ });
166
+ }
167
+ if (!decoded.complete || decoded.transfers.length === 0) {
168
+ requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, params.segment, [
169
+ ...decoded.signals,
170
+ decoded.transfers.length > 0 ? 'egress.grammar_incomplete' : 'egress.endpoint_unknown',
171
+ ]));
172
+ }
173
+ return requirements;
174
+ }
175
+ const CURL_OPTIONS_WITH_SEPARATE_VALUES = new Set([
176
+ '-A',
177
+ '-D',
178
+ '-F',
179
+ '-H',
180
+ '-K',
181
+ '-T',
182
+ '-X',
183
+ '-b',
184
+ '-c',
185
+ '-d',
186
+ '-e',
187
+ '-o',
188
+ '-u',
189
+ '-w',
190
+ '--cacert',
191
+ '--cert',
192
+ '--config',
193
+ '--connect-timeout',
194
+ '--connect-to',
195
+ '--cookie',
196
+ '--cookie-jar',
197
+ '--data',
198
+ '--data-ascii',
199
+ '--data-binary',
200
+ '--data-raw',
201
+ '--dump-header',
202
+ '--form',
203
+ '--header',
204
+ '--interface',
205
+ '--key',
206
+ '--limit-rate',
207
+ '--max-time',
208
+ '--output',
209
+ '--output-dir',
210
+ '--proxy',
211
+ '--referer',
212
+ '--request',
213
+ '--resolve',
214
+ '--retry',
215
+ '--retry-delay',
216
+ '--upload-file',
217
+ '--url',
218
+ '--user',
219
+ '--user-agent',
220
+ '--write-out',
221
+ ]);
222
+ function decodeCurlWgetGrammar(head, tokens) {
223
+ if (head !== 'curl' || !tokens.includes('--next')) {
224
+ return decodeSingleCurlWgetGrammar(head, tokens);
225
+ }
226
+ const scopes = [[]];
227
+ const args = tokens.slice(1);
228
+ for (let index = 0; index < args.length; index += 1) {
229
+ const token = args[index] ?? '';
230
+ if (token === '--next') {
231
+ scopes.push([]);
232
+ }
233
+ else {
234
+ scopes.at(-1)?.push(token);
235
+ if (CURL_OPTIONS_WITH_SEPARATE_VALUES.has(token) && args[index + 1] !== undefined) {
236
+ scopes.at(-1)?.push(args[index + 1] ?? '');
237
+ index += 1;
238
+ }
239
+ }
240
+ }
241
+ const decodedScopes = scopes.map((scope) => decodeSingleCurlWgetGrammar(head, [head, ...scope]));
242
+ const mode = mergeTransferModes(decodedScopes.flatMap((scope) => scope.transfers));
243
+ const payload = decodedScopes.reduce((current, scope) => mergePayload(current, scope.payload), 'none');
244
+ return {
245
+ transfers: decodedScopes.flatMap((scope) => scope.transfers),
246
+ mode,
247
+ payload,
248
+ files: [...new Set(decodedScopes.flatMap((scope) => scope.files))],
249
+ secretFiles: [...new Set(decodedScopes.flatMap((scope) => scope.secretFiles))],
250
+ nonSecretFiles: [...new Set(decodedScopes.flatMap((scope) => scope.nonSecretFiles))],
251
+ outputFiles: [...new Set(decodedScopes.flatMap((scope) => scope.outputFiles))],
252
+ complete: scopes.every((scope) => scope.length > 0) &&
253
+ decodedScopes.every((scope) => scope.complete && scope.transfers.length > 0),
254
+ signals: [
255
+ 'egress.curl',
256
+ 'egress.curl.transfer_scopes',
257
+ `egress.mode.${mode}`,
258
+ `egress.payload.${payload}`,
259
+ ],
260
+ };
261
+ }
262
+ function decodeSingleCurlWgetGrammar(head, tokens) {
263
+ let mode = 'read';
264
+ let payload = 'none';
265
+ let complete = true;
266
+ const files = [];
267
+ const secretFiles = [];
268
+ const nonSecretFiles = [];
269
+ const outputFiles = [];
270
+ const directoryEligibleOutputs = new Set();
271
+ let remoteNameOutput = false;
272
+ let explicitOutput = false;
273
+ let outputDirectory = null;
274
+ const args = tokens.slice(1);
275
+ const endpoints = [];
276
+ const endpointOutputNames = [];
277
+ const methodFlags = head === 'curl' ? ['-X', '--request'] : ['--method'];
278
+ const bodyDataFlags = head === 'curl'
279
+ ? ['-d', '--data', '--data-ascii', '--data-binary', '--data-raw']
280
+ : ['--body-data', '--post-data'];
281
+ const bodyFileFlags = head === 'curl' ? ['-T', '--upload-file'] : ['--body-file', '--post-file'];
282
+ const formFlags = head === 'curl' ? ['-F', '--form'] : [];
283
+ const valueOnlyFlags = head === 'curl'
284
+ ? [
285
+ '-A',
286
+ '-D',
287
+ '-H',
288
+ '-b',
289
+ '-c',
290
+ '-e',
291
+ '-o',
292
+ '-u',
293
+ '-w',
294
+ '--cacert',
295
+ '--cert',
296
+ '--connect-timeout',
297
+ '--connect-to',
298
+ '--cookie',
299
+ '--cookie-jar',
300
+ '--dump-header',
301
+ '--header',
302
+ '--interface',
303
+ '--key',
304
+ '--limit-rate',
305
+ '--max-time',
306
+ '--output',
307
+ '--output-dir',
308
+ '--proxy',
309
+ '--referer',
310
+ '--resolve',
311
+ '--retry',
312
+ '--retry-delay',
313
+ '--user',
314
+ '--user-agent',
315
+ '--write-out',
316
+ ]
317
+ : [
318
+ '-O',
319
+ '-P',
320
+ '-a',
321
+ '-o',
322
+ '--append-output',
323
+ '--bind-address',
324
+ '--ca-certificate',
325
+ '--certificate',
326
+ '--directory-prefix',
327
+ '--header',
328
+ '--output-document',
329
+ '--output-file',
330
+ '--password',
331
+ '--private-key',
332
+ '--referer',
333
+ '--timeout',
334
+ '--tries',
335
+ '--user',
336
+ '--user-agent',
337
+ '--wait',
338
+ ];
339
+ for (let index = 0; index < args.length; index += 1) {
340
+ const token = args[index] ?? '';
341
+ const next = args[index + 1] ?? '';
342
+ if (head === 'curl' &&
343
+ (CURL_EFFECT_NEUTRAL_FLAGS.has(token) ||
344
+ (/^-[fLsS]+$/.test(token) &&
345
+ [...token.slice(1)].every((flag) => CURL_EFFECT_NEUTRAL_FLAGS.has(`-${flag}`))))) {
346
+ continue;
347
+ }
348
+ if (head === 'curl' && (token === '-I' || token === '--head')) {
349
+ continue;
350
+ }
351
+ if (head === 'curl' && (token === '-O' || token === '--remote-name')) {
352
+ remoteNameOutput = true;
353
+ explicitOutput = true;
354
+ continue;
355
+ }
356
+ const directoryFlags = head === 'curl' ? ['--output-dir'] : ['-P', '--directory-prefix'];
357
+ if (directoryFlags.includes(token)) {
358
+ if (!next || next.startsWith('-')) {
359
+ complete = false;
360
+ }
361
+ else {
362
+ outputDirectory = next;
363
+ index += 1;
364
+ }
365
+ continue;
366
+ }
367
+ const directoryValue = optionValue(token, next, directoryFlags);
368
+ if (directoryValue) {
369
+ outputDirectory = directoryValue.value;
370
+ continue;
371
+ }
372
+ const method = optionValue(token, next, methodFlags);
373
+ if (method) {
374
+ if (!/^[A-Za-z]+$/.test(method.value) || method.value.includes('$')) {
375
+ mode = 'ambiguous';
376
+ complete = false;
377
+ }
378
+ else {
379
+ const normalized = method.value.toUpperCase();
380
+ mode = normalized === 'GET' || normalized === 'HEAD' ? 'read' : 'mutate';
381
+ }
382
+ if (method.fromNext) {
383
+ index += 1;
384
+ }
385
+ continue;
386
+ }
387
+ if (matchesAnyOption(token, methodFlags)) {
388
+ mode = 'ambiguous';
389
+ complete = false;
390
+ continue;
391
+ }
392
+ const bodyData = optionValue(token, next, bodyDataFlags);
393
+ if (bodyData) {
394
+ mode = mode === 'ambiguous' ? 'ambiguous' : 'mutate';
395
+ const dataFile = bodyData.value.startsWith('@') ? bodyData.value.slice(1) : null;
396
+ if (dataFile) {
397
+ files.push(dataFile);
398
+ }
399
+ payload = mergePayload(payload, isSecretValue(bodyData) ? 'secret' : 'present');
400
+ if (bodyData.fromNext) {
401
+ index += 1;
402
+ }
403
+ continue;
404
+ }
405
+ if (matchesAnyOption(token, bodyDataFlags)) {
406
+ mode = 'ambiguous';
407
+ complete = false;
408
+ continue;
409
+ }
410
+ const bodyFile = optionValue(token, next, bodyFileFlags);
411
+ if (bodyFile) {
412
+ mode = mode === 'ambiguous' ? 'ambiguous' : 'mutate';
413
+ payload = mergePayload(payload, isSecretValue(bodyFile) ? 'secret' : 'present');
414
+ if (bodyFile.value !== '-') {
415
+ files.push(bodyFile.value.replace(/^@/, ''));
416
+ }
417
+ if (bodyFile.fromNext) {
418
+ index += 1;
419
+ }
420
+ continue;
421
+ }
422
+ if (matchesAnyOption(token, bodyFileFlags)) {
423
+ mode = 'ambiguous';
424
+ complete = false;
425
+ continue;
426
+ }
427
+ const form = optionValue(token, next, formFlags);
428
+ if (form) {
429
+ mode = mode === 'ambiguous' ? 'ambiguous' : 'mutate';
430
+ payload = mergePayload(payload, isSecretValue(form) ? 'secret' : 'present');
431
+ const formFile = uploadFileOperand(form);
432
+ if (formFile) {
433
+ files.push(formFile);
434
+ }
435
+ if (form.fromNext) {
436
+ index += 1;
437
+ }
438
+ continue;
439
+ }
440
+ if (matchesAnyOption(token, formFlags)) {
441
+ mode = 'ambiguous';
442
+ complete = false;
443
+ continue;
444
+ }
445
+ const configValue = optionValue(token, next, ['-K', '--config']);
446
+ if (configValue) {
447
+ files.push(configValue.value);
448
+ complete = false;
449
+ if (configValue.fromNext) {
450
+ index += 1;
451
+ }
452
+ continue;
453
+ }
454
+ if (matchesAnyOption(token, ['-K', '--config'])) {
455
+ complete = false;
456
+ continue;
457
+ }
458
+ const outputFlags = head === 'curl'
459
+ ? ['-o', '--output', '-c', '--cookie-jar', '-D', '--dump-header']
460
+ : ['-O', '--output-document'];
461
+ const outputValue = optionValue(token, next, outputFlags);
462
+ if (outputValue) {
463
+ outputFiles.push(outputValue.value);
464
+ if (head === 'curl' && matchesAnyOption(token, ['-o', '--output'])) {
465
+ directoryEligibleOutputs.add(outputValue.value);
466
+ }
467
+ explicitOutput = true;
468
+ if (outputValue.fromNext) {
469
+ index += 1;
470
+ }
471
+ continue;
472
+ }
473
+ if (matchesAnyOption(token, outputFlags)) {
474
+ complete = false;
475
+ continue;
476
+ }
477
+ const credentialFlags = head === 'curl'
478
+ ? ['--key', '--cert', '--cacert']
479
+ : ['--private-key', '--certificate', '--ca-certificate'];
480
+ const credential = optionValue(token, next, credentialFlags);
481
+ if (credential) {
482
+ files.push(credential.value);
483
+ if (token === '--key' ||
484
+ token.startsWith('--key=') ||
485
+ token === '--cert' ||
486
+ token.startsWith('--cert=') ||
487
+ token === '--private-key' ||
488
+ token.startsWith('--private-key=') ||
489
+ token === '--certificate' ||
490
+ token.startsWith('--certificate=')) {
491
+ secretFiles.push(credential.value);
492
+ }
493
+ else {
494
+ nonSecretFiles.push(credential.value);
495
+ }
496
+ if (credential.fromNext) {
497
+ index += 1;
498
+ }
499
+ continue;
500
+ }
501
+ if (matchesAnyOption(token, credentialFlags)) {
502
+ complete = false;
503
+ continue;
504
+ }
505
+ const cookie = head === 'curl' ? optionValue(token, next, ['-b', '--cookie']) : null;
506
+ if (cookie) {
507
+ if (!cookie.value.includes('=')) {
508
+ files.push(cookie.value);
509
+ secretFiles.push(cookie.value);
510
+ payload = 'secret';
511
+ }
512
+ if (cookie.fromNext) {
513
+ index += 1;
514
+ }
515
+ continue;
516
+ }
517
+ const header = optionValue(token, next, ['-H', '--header']);
518
+ if (header) {
519
+ mode = mode === 'ambiguous' ? 'ambiguous' : 'mutate';
520
+ if (header.value.startsWith('@')) {
521
+ const headerFile = header.value.slice(1);
522
+ files.push(headerFile);
523
+ payload = mergePayload(payload, isSecretValue(headerFile) ? 'secret' : 'present');
524
+ }
525
+ else if (/^\s*(?:authorization|proxy-authorization|x-api-key|api-key|x-auth-token|x-token|token)\s*:\s*.+/i.test(header.value)) {
526
+ payload = 'secret';
527
+ }
528
+ else {
529
+ payload = mergePayload(payload, isExplicitSecretInterpolation(header.value) ? 'secret' : 'present');
530
+ }
531
+ if (header.fromNext) {
532
+ index += 1;
533
+ }
534
+ continue;
535
+ }
536
+ const urlValue = head === 'curl' ? optionValue(token, next, ['--url']) : null;
537
+ if (urlValue) {
538
+ const endpoint = parseNetworkEndpoint(urlValue.value);
539
+ if (endpoint) {
540
+ endpoints.push(endpoint);
541
+ endpointOutputNames.push(remoteOutputName(urlValue.value));
542
+ if (isSecretValue(urlValue.value)) {
543
+ payload = 'secret';
544
+ }
545
+ }
546
+ else {
547
+ complete = false;
548
+ }
549
+ if (urlValue.fromNext) {
550
+ index += 1;
551
+ }
552
+ continue;
553
+ }
554
+ const skippedValue = optionValue(token, next, valueOnlyFlags);
555
+ if (skippedValue) {
556
+ if (isExplicitSecretInterpolation(skippedValue.value)) {
557
+ mode = mode === 'ambiguous' ? 'ambiguous' : 'mutate';
558
+ payload = 'secret';
559
+ }
560
+ if (skippedValue.fromNext) {
561
+ index += 1;
562
+ }
563
+ continue;
564
+ }
565
+ if (matchesAnyOption(token, valueOnlyFlags)) {
566
+ complete = false;
567
+ continue;
568
+ }
569
+ if (isExplicitSecretInterpolation(token)) {
570
+ mode = mode === 'ambiguous' ? 'ambiguous' : 'mutate';
571
+ payload = 'secret';
572
+ }
573
+ const parsed = parseNetworkEndpoint(token);
574
+ if (parsed) {
575
+ endpoints.push(parsed);
576
+ endpointOutputNames.push(remoteOutputName(token));
577
+ if (isSecretValue(token)) {
578
+ payload = 'secret';
579
+ }
580
+ continue;
581
+ }
582
+ if (token.startsWith('-') && token !== '-') {
583
+ complete = false;
584
+ }
585
+ }
586
+ if (head === 'wget' && !explicitOutput) {
587
+ outputFiles.push(...endpointOutputNames.map((name) => outputDirectory ? path.join(outputDirectory, name) : name));
588
+ }
589
+ else if (head === 'curl' && remoteNameOutput) {
590
+ outputFiles.push(...endpointOutputNames.map((name) => outputDirectory ? path.join(outputDirectory, name) : name));
591
+ }
592
+ return {
593
+ transfers: endpoints.map((endpoint) => ({ endpoint, mode, payload })),
594
+ mode,
595
+ payload,
596
+ files: [...new Set(files)],
597
+ secretFiles: [...new Set(secretFiles)],
598
+ nonSecretFiles: [...new Set(nonSecretFiles)],
599
+ outputFiles: [
600
+ ...new Set(outputFiles.map((file) => outputDirectory && directoryEligibleOutputs.has(file) && !path.isAbsolute(file)
601
+ ? path.join(outputDirectory, file)
602
+ : file)),
603
+ ],
604
+ complete,
605
+ signals: [`egress.${head}`, `egress.mode.${mode}`, `egress.payload.${payload}`],
606
+ };
607
+ }
608
+ function remoteOutputName(spec) {
609
+ let pathname = '';
610
+ try {
611
+ pathname = new URL(spec).pathname;
612
+ }
613
+ catch {
614
+ pathname = spec.split(/[?#]/, 1)[0] ?? '';
615
+ }
616
+ const name = path.posix.basename(pathname);
617
+ return name && name !== '/' ? name : 'index.html';
618
+ }
619
+ function decodeGhGrammar(tokens) {
620
+ const args = tokens.slice(1);
621
+ const hostnameIndex = args.findIndex((token) => token === '--hostname' || token.startsWith('--hostname='));
622
+ const hostnameToken = hostnameIndex >= 0 ? (args[hostnameIndex] ?? '') : '';
623
+ const hostnameMissing = hostnameToken === '--hostname' &&
624
+ (!args[hostnameIndex + 1] || (args[hostnameIndex + 1] ?? '').startsWith('-'));
625
+ const host = hostnameMissing
626
+ ? null
627
+ : hostnameToken === '--hostname'
628
+ ? (args[hostnameIndex + 1] ?? null)
629
+ : hostnameToken.startsWith('--hostname=')
630
+ ? hostnameToken.slice('--hostname='.length) || null
631
+ : 'api.github.com';
632
+ const endpoint = host ? parseNetworkEndpoint(`https://${host}`) : null;
633
+ const commandTokens = args.filter((token, index) => {
634
+ if (index === hostnameIndex ||
635
+ (hostnameToken === '--hostname' && index === hostnameIndex + 1)) {
636
+ return false;
637
+ }
638
+ return !token.startsWith('-');
639
+ });
640
+ const command = commandTokens[0] === 'api'
641
+ ? 'api'
642
+ : `${commandTokens[0] ?? ''} ${commandTokens[1] ?? ''}`.trim();
643
+ let mode = GH_READ_COMMANDS.has(command) ? 'read' : 'ambiguous';
644
+ let payload = 'none';
645
+ let complete = mode !== 'ambiguous' && !hostnameMissing && Boolean(endpoint);
646
+ const files = [];
647
+ let secretRead = false;
648
+ let ambientWrite = false;
649
+ const processSpawns = [];
650
+ for (let index = 0; index < args.length; index += 1) {
651
+ const token = args[index] ?? '';
652
+ const next = args[index + 1] ?? '';
653
+ if (token === '--show-token' && command === 'auth status') {
654
+ secretRead = true;
655
+ continue;
656
+ }
657
+ if (token === '--web' && command !== 'api') {
658
+ processSpawns.push('browser');
659
+ continue;
660
+ }
661
+ const cache = optionValue(token, next, ['--cache']);
662
+ if (cache) {
663
+ ambientWrite = true;
664
+ if (cache.fromNext) {
665
+ index += 1;
666
+ }
667
+ continue;
668
+ }
669
+ if (matchesAnyOption(token, ['--cache'])) {
670
+ complete = false;
671
+ continue;
672
+ }
673
+ const header = optionValue(token, next, ['-H', '--header']);
674
+ if (header) {
675
+ payload =
676
+ /^\s*(?:authorization|proxy-authorization|x-api-key|api-key|x-auth-token|x-token|token)\s*:\s*.+/i.test(header.value) || isExplicitSecretInterpolation(header.value)
677
+ ? 'secret'
678
+ : mergePayload(payload, 'present');
679
+ if (header.fromNext) {
680
+ index += 1;
681
+ }
682
+ continue;
683
+ }
684
+ if (matchesAnyOption(token, ['-H', '--header'])) {
685
+ complete = false;
686
+ continue;
687
+ }
688
+ const method = optionValue(token, next, ['-X', '--method']);
689
+ if (method) {
690
+ if (!method.value || method.value.includes('$')) {
691
+ mode = 'ambiguous';
692
+ complete = false;
693
+ }
694
+ else {
695
+ const normalized = method.value.toUpperCase();
696
+ mode = normalized === 'GET' || normalized === 'HEAD' ? 'read' : 'mutate';
697
+ }
698
+ if (method.fromNext) {
699
+ index += 1;
700
+ }
701
+ continue;
702
+ }
703
+ if (matchesAnyOption(token, ['-X', '--method'])) {
704
+ mode = 'ambiguous';
705
+ complete = false;
706
+ continue;
707
+ }
708
+ const body = optionValue(token, next, ['-f', '-F', '--field', '--raw-field', '--input']);
709
+ if (body) {
710
+ mode = 'mutate';
711
+ payload = isSecretValue(body.value) ? 'secret' : 'present';
712
+ if (token === '--input' || token.startsWith('--input=')) {
713
+ files.push(body.value);
714
+ }
715
+ if (body.fromNext) {
716
+ index += 1;
717
+ }
718
+ continue;
719
+ }
720
+ if (matchesAnyOption(token, ['-f', '-F', '--field', '--raw-field', '--input'])) {
721
+ mode = 'ambiguous';
722
+ complete = false;
723
+ }
724
+ }
725
+ return {
726
+ transfers: endpoint ? [{ endpoint, mode, payload }] : [],
727
+ mode,
728
+ payload,
729
+ files,
730
+ secretFiles: [],
731
+ nonSecretFiles: [],
732
+ outputFiles: [],
733
+ secretRead,
734
+ ambientWrite,
735
+ processSpawns,
736
+ complete,
737
+ signals: ['egress.gh', `egress.mode.${mode}`, `egress.payload.${payload}`],
738
+ };
739
+ }
740
+ function optionValue(token, next, flags) {
741
+ if (flags.includes(token)) {
742
+ return next ? { value: next, fromNext: true } : null;
743
+ }
744
+ for (const flag of flags) {
745
+ if (token.startsWith(`${flag}=`)) {
746
+ return { value: token.slice(flag.length + 1), fromNext: false };
747
+ }
748
+ if (flag.length === 2 && token.startsWith(flag) && token.length > 2) {
749
+ return { value: token.slice(flag.length), fromNext: false };
750
+ }
751
+ }
752
+ return null;
753
+ }
754
+ function matchesAnyOption(token, flags) {
755
+ return flags.some((flag) => token === flag ||
756
+ token.startsWith(`${flag}=`) ||
757
+ (flag.length === 2 && token.startsWith(flag) && token.length > 2));
758
+ }
759
+ function mergePayload(current, next) {
760
+ if (current === 'secret' || next === 'secret') {
761
+ return 'secret';
762
+ }
763
+ return current === 'present' || next === 'present' ? 'present' : 'none';
764
+ }
765
+ function mergeTransferModes(transfers) {
766
+ const modes = new Set(transfers.map((transfer) => transfer.mode));
767
+ if (modes.size === 1) {
768
+ return transfers[0]?.mode ?? 'ambiguous';
769
+ }
770
+ return 'ambiguous';
771
+ }
772
+ function uploadFileOperand(value) {
773
+ const formFile = /(?:^|=)[@<](.+)$/.exec(value.value);
774
+ if (formFile?.[1]) {
775
+ return formFile[1];
776
+ }
777
+ return value.value.startsWith('@') || value.value.startsWith('<') ? value.value.slice(1) : null;
778
+ }
779
+ function isSecretValue(value) {
780
+ const raw = typeof value === 'string' ? value : value.value;
781
+ return SECRET_VALUE_PATTERN.test(raw.replace(/^@/, ''));
782
+ }
783
+ function isExplicitSecretInterpolation(token) {
784
+ return ((/\b(?:authorization|token|secret|password|credential)\b/i.test(token) &&
785
+ /\$(?:\{|[A-Za-z_])/.test(token)) ||
786
+ /\bBearer\s+\$/i.test(token) ||
787
+ (token.includes('$') && isSecretValue(token)));
788
+ }
789
+ function expandHome(value) {
790
+ if (value === '~') {
791
+ return process.env.HOME ?? value;
792
+ }
793
+ if (value.startsWith('~/')) {
794
+ return path.join(process.env.HOME ?? '~', value.slice(2));
795
+ }
796
+ return value;
797
+ }
798
+ function requirement(tag, action, resource, segment, signals) {
799
+ return {
800
+ tag,
801
+ action,
802
+ resource,
803
+ evidence: {
804
+ level: tag === 'indeterminate' ? 'indeterminate' : 'certain',
805
+ signals: [...new Set(signals)].sort(),
806
+ basis: ['egress_grammar'],
807
+ },
808
+ provenance: { segment },
809
+ };
810
+ }
65
811
  function classifyCurlWget(tokens) {
66
812
  const args = tokens.slice(1);
67
813
  for (let index = 0; index < args.length; index += 1) {