@guilz-dev/belay 0.6.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 (183) hide show
  1. package/README.md +39 -20
  2. package/dist/adapters/claude/runtime-entry.js +54 -25
  3. package/dist/adapters/shared/gate-runtime.d.ts +7 -0
  4. package/dist/adapters/shared/gate-runtime.js +443 -225
  5. package/dist/bundle/claude-runtime.mjs +13027 -7271
  6. package/dist/bundle/codex-runtime.mjs +12700 -6977
  7. package/dist/bundle/cursor-runtime.mjs +12695 -6977
  8. package/dist/cli.js +37 -1
  9. package/dist/commands/approve.js +34 -10
  10. package/dist/commands/doctor.js +58 -0
  11. package/dist/commands/dogfood.js +1 -1
  12. package/dist/commands/explain.js +2 -2
  13. package/dist/commands/harvest.d.ts +0 -1
  14. package/dist/commands/harvest.js +1 -4
  15. package/dist/commands/metrics.js +6 -0
  16. package/dist/commands/quality.js +1 -3
  17. package/dist/commands/recovery-checkpoints.d.ts +164 -0
  18. package/dist/commands/recovery-checkpoints.js +347 -0
  19. package/dist/commands/revoke.js +19 -7
  20. package/dist/commands/status.js +7 -0
  21. package/dist/config-io.js +49 -18
  22. package/dist/conformance/guarantee-table.js +137 -9
  23. package/dist/conformance/types.d.ts +2 -0
  24. package/dist/core/approval-replay.d.ts +1 -1
  25. package/dist/core/approval-replay.js +4 -1
  26. package/dist/core/approval-service.d.ts +6 -1
  27. package/dist/core/approval-service.js +86 -27
  28. package/dist/core/approval.d.ts +2 -0
  29. package/dist/core/approval.js +6 -3
  30. package/dist/core/audit-metrics.d.ts +1 -0
  31. package/dist/core/audit-metrics.js +12 -4
  32. package/dist/core/audit-types.d.ts +16 -0
  33. package/dist/core/capability/approval-state-mutation.d.ts +19 -0
  34. package/dist/core/capability/approval-state-mutation.js +76 -3
  35. package/dist/core/capability/approval-v3.d.ts +18 -1
  36. package/dist/core/capability/approval-v3.js +63 -23
  37. package/dist/core/capability/attestation.d.ts +3 -0
  38. package/dist/core/capability/attestation.js +7 -0
  39. package/dist/core/capability/boundary-driver-container.d.ts +12 -0
  40. package/dist/core/capability/boundary-driver-container.js +101 -34
  41. package/dist/core/capability/boundary-driver.js +1 -0
  42. package/dist/core/capability/boundary-grant-materialize.d.ts +1 -0
  43. package/dist/core/capability/boundary-grant-materialize.js +15 -0
  44. package/dist/core/capability/boundary-run.d.ts +25 -0
  45. package/dist/core/capability/boundary-run.js +37 -9
  46. package/dist/core/capability/boundary-session.d.ts +4 -0
  47. package/dist/core/capability/boundary-session.js +9 -0
  48. package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
  49. package/dist/core/capability/boundary-workspace-mount.js +68 -0
  50. package/dist/core/capability/gate-policy-shadow.js +23 -1
  51. package/dist/core/capability/grant-consumption.d.ts +4 -0
  52. package/dist/core/capability/grant-consumption.js +11 -0
  53. package/dist/core/capability/grant-lease.d.ts +36 -1
  54. package/dist/core/capability/grant-lease.js +213 -16
  55. package/dist/core/capability/grant-match.js +3 -0
  56. package/dist/core/capability/index.d.ts +2 -2
  57. package/dist/core/capability/index.js +1 -1
  58. package/dist/core/capability/policy-engine.d.ts +38 -1
  59. package/dist/core/capability/policy-engine.js +388 -15
  60. package/dist/core/capability/request.d.ts +12 -0
  61. package/dist/core/capability/resolver.d.ts +0 -1
  62. package/dist/core/capability/resolver.js +0 -1
  63. package/dist/core/config.d.ts +20 -0
  64. package/dist/core/config.js +62 -2
  65. package/dist/core/effect-ir/audit.d.ts +9 -0
  66. package/dist/core/effect-ir/audit.js +87 -0
  67. package/dist/core/effect-ir/build.d.ts +31 -0
  68. package/dist/core/effect-ir/build.js +281 -0
  69. package/dist/core/effect-ir/index.d.ts +13 -0
  70. package/dist/core/effect-ir/index.js +9 -0
  71. package/dist/core/effect-ir/normalize.d.ts +4 -0
  72. package/dist/core/effect-ir/normalize.js +110 -0
  73. package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
  74. package/dist/core/effect-ir/package-exec-eval.js +152 -0
  75. package/dist/core/effect-ir/package-exec.d.ts +29 -0
  76. package/dist/core/effect-ir/package-exec.js +292 -0
  77. package/dist/core/effect-ir/policy.d.ts +14 -0
  78. package/dist/core/effect-ir/policy.js +164 -0
  79. package/dist/core/effect-ir/shell-build.d.ts +43 -0
  80. package/dist/core/effect-ir/shell-build.js +77 -0
  81. package/dist/core/effect-ir/shell-lower.d.ts +14 -0
  82. package/dist/core/effect-ir/shell-lower.js +1509 -0
  83. package/dist/core/effect-ir/types.d.ts +62 -0
  84. package/dist/core/effect-ir/types.js +1 -0
  85. package/dist/core/egress/allowlist.d.ts +1 -0
  86. package/dist/core/egress/allowlist.js +8 -0
  87. package/dist/core/egress-approval.js +45 -51
  88. package/dist/core/gate-contract.d.ts +2 -0
  89. package/dist/core/gate-contract.js +2 -0
  90. package/dist/core/gate-engine.js +82 -25
  91. package/dist/core/git-resource-identity.d.ts +26 -0
  92. package/dist/core/git-resource-identity.js +284 -0
  93. package/dist/core/harvest.d.ts +3 -7
  94. package/dist/core/harvest.js +4 -27
  95. package/dist/core/index.d.ts +5 -1
  96. package/dist/core/index.js +3 -1
  97. package/dist/core/judge-cloud-consent.js +29 -15
  98. package/dist/core/network-endpoint.d.ts +10 -0
  99. package/dist/core/network-endpoint.js +51 -0
  100. package/dist/core/path-utils.js +42 -23
  101. package/dist/core/process-runner.d.ts +10 -0
  102. package/dist/core/process-runner.js +124 -0
  103. package/dist/core/recover-advice.js +2 -1
  104. package/dist/core/recovery/artifact-store.d.ts +29 -0
  105. package/dist/core/recovery/artifact-store.js +336 -0
  106. package/dist/core/recovery/checkpoint.d.ts +25 -0
  107. package/dist/core/recovery/checkpoint.js +260 -0
  108. package/dist/core/recovery/index.d.ts +3 -1
  109. package/dist/core/recovery/index.js +2 -0
  110. package/dist/core/recovery/operator-guidance.d.ts +7 -0
  111. package/dist/core/recovery/operator-guidance.js +39 -0
  112. package/dist/core/recovery/reconcile.d.ts +3 -0
  113. package/dist/core/recovery/reconcile.js +48 -0
  114. package/dist/core/recovery/resource-identity.d.ts +4 -0
  115. package/dist/core/recovery/resource-identity.js +34 -0
  116. package/dist/core/recovery/restore.d.ts +13 -0
  117. package/dist/core/recovery/restore.js +114 -0
  118. package/dist/core/recovery/snapshot-node.d.ts +27 -0
  119. package/dist/core/recovery/snapshot-node.js +261 -0
  120. package/dist/core/recovery/types.d.ts +85 -1
  121. package/dist/core/shell-substitution.d.ts +1 -0
  122. package/dist/core/shell-substitution.js +61 -0
  123. package/dist/core/standing-allow.d.ts +1 -2
  124. package/dist/core/standing-allow.js +3 -20
  125. package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
  126. package/dist/core/transactional/apply-observed-changes.js +275 -0
  127. package/dist/core/transactional/backend-selector.d.ts +8 -0
  128. package/dist/core/transactional/backend-selector.js +118 -0
  129. package/dist/core/transactional/backend.d.ts +55 -0
  130. package/dist/core/transactional/backend.js +1 -0
  131. package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
  132. package/dist/core/transactional/file-checkpoint-backend.js +278 -0
  133. package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
  134. package/dist/core/transactional/file-checkpoint-git.js +217 -0
  135. package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
  136. package/dist/core/transactional/file-checkpoint-staging.js +54 -0
  137. package/dist/core/transactional/file-clone.d.ts +15 -0
  138. package/dist/core/transactional/file-clone.js +139 -0
  139. package/dist/core/transactional/file-tree-path.d.ts +7 -0
  140. package/dist/core/transactional/file-tree-path.js +50 -0
  141. package/dist/core/transactional/file-tree.d.ts +41 -0
  142. package/dist/core/transactional/file-tree.js +186 -0
  143. package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
  144. package/dist/core/transactional/git-worktree-backend.js +50 -0
  145. package/dist/core/transactional/git-worktree.d.ts +9 -11
  146. package/dist/core/transactional/git-worktree.js +24 -119
  147. package/dist/core/transactional/index.d.ts +11 -0
  148. package/dist/core/transactional/index.js +10 -0
  149. package/dist/core/transactional/reasons.js +1 -0
  150. package/dist/core/transactional/runner.js +157 -25
  151. package/dist/core/transactional/snapshot-node.d.ts +26 -0
  152. package/dist/core/transactional/snapshot-node.js +87 -0
  153. package/dist/core/transactional/types.d.ts +17 -1
  154. package/dist/core/types.d.ts +13 -1
  155. package/dist/core/verdict/adapter.js +23 -15
  156. package/dist/core/verdict/containment.js +4 -0
  157. package/dist/core/verdict/egress-classify.d.ts +12 -0
  158. package/dist/core/verdict/egress-classify.js +746 -0
  159. package/dist/core/verdict/git-classifier.d.ts +12 -0
  160. package/dist/core/verdict/git-classifier.js +320 -0
  161. package/dist/core/verdict/launcher-resolve.js +79 -37
  162. package/dist/core/verdict/parser.d.ts +2 -0
  163. package/dist/core/verdict/parser.js +95 -1
  164. package/dist/core/verdict/types.d.ts +6 -2
  165. package/dist/core/verdict/verdict.js +103 -935
  166. package/dist/corpus/must-allow-commands.d.ts +2 -1
  167. package/dist/corpus/must-allow-commands.js +2 -1
  168. package/dist/corpus/runtime-match.d.ts +2 -1
  169. package/dist/corpus/runtime-match.js +2 -1
  170. package/dist/corpus/types.d.ts +2 -2
  171. package/dist/templates.js +7 -2
  172. package/dist/types.d.ts +11 -0
  173. package/dist/version.d.ts +1 -1
  174. package/dist/version.js +1 -1
  175. package/package.json +2 -2
  176. package/skills/belay/SKILL.md +8 -3
  177. package/skills/belay/belay-approve.md +10 -7
  178. package/dist/core/verdict/overrides.d.ts +0 -7
  179. package/dist/core/verdict/overrides.js +0 -37
  180. package/dist/core/verdict/shell-policy.d.ts +0 -25
  181. package/dist/core/verdict/shell-policy.js +0 -40
  182. package/dist/corpus/standing-allow-catalog.generated.d.ts +0 -13
  183. 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) {