@blogic-cz/agent-tools 1.6.11 → 1.6.13

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/credential-guard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAK5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAM/D,8CAA8C;AAC9C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,+CAA+C;AAC/C,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B,CAAC;AAQF,+CAA+C;AAC/C,MAAM,MAAM,eAAe,GAAG;IAC5B,uBAAuB,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IACxE,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC3E,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,sBAAsB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,iBAAiB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjF,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACjD,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CACxD,CAAC;AA2uFF,6CAA6C;AAC7C,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAErE;AAED,2CAA2C;AAC3C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEpE;AAED,2CAA2C;AAC3C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEpE;AAMD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,eAAe,CA4VrF;AAQD,kEAAkE;AAClE,eAAO,MAAM,uBAAuB,UAjnGD,SAAS,UAAU,UAAU,KAAK,IAinGM,CAAC;AAE5E,oDAAoD;AACpD,eAAO,MAAM,aAAa,YAnnGC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAmnGjB,CAAC;AAExD,yEAAyE;AACzE,eAAO,MAAM,aAAa,aArnGE,MAAM,KAAK,OAqnGgB,CAAC;AAExD,yDAAyD;AACzD,eAAO,MAAM,aAAa,aAvnGE,MAAM,KAAK,OAunGgB,CAAC;AAExD,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,YAznGC,MAAM,KAAK,OAynG0B,CAAC;AAE1E,iDAAiD;AACjD,eAAO,MAAM,iBAAiB,YA3nGC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IA2nGf,CAAC;AAEhE,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,YA7nGC,MAAM,KAAK,OA6nGsB,CAAC;AAElE,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,YA/nGC,MAAM,KAAK,MAAM,GAAG,IA+nGa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/credential-guard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAK5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAM/D,8CAA8C;AAC9C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,+CAA+C;AAC/C,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B,CAAC;AAQF,+CAA+C;AAC/C,MAAM,MAAM,eAAe,GAAG;IAC5B,uBAAuB,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IACxE,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC3E,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,sBAAsB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,iBAAiB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjF,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACjD,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CACxD,CAAC;AA66FF,6CAA6C;AAC7C,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAErE;AAED,2CAA2C;AAC3C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEpE;AAED,2CAA2C;AAC3C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEpE;AAMD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,eAAe,CA+XrF;AAQD,kEAAkE;AAClE,eAAO,MAAM,uBAAuB,UAt1GD,SAAS,UAAU,UAAU,KAAK,IAs1GM,CAAC;AAE5E,oDAAoD;AACpD,eAAO,MAAM,aAAa,YAx1GC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAw1GjB,CAAC;AAExD,yEAAyE;AACzE,eAAO,MAAM,aAAa,aA11GE,MAAM,KAAK,OA01GgB,CAAC;AAExD,yDAAyD;AACzD,eAAO,MAAM,aAAa,aA51GE,MAAM,KAAK,OA41GgB,CAAC;AAExD,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,YA91GC,MAAM,KAAK,OA81G0B,CAAC;AAE1E,iDAAiD;AACjD,eAAO,MAAM,iBAAiB,YAh2GC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAg2Gf,CAAC;AAEhE,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,YAl2GC,MAAM,KAAK,OAk2GsB,CAAC;AAElE,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,YAp2GC,MAAM,KAAK,MAAM,GAAG,IAo2Ga,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "1.6.11",
3
+ "version": "1.6.13",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure platform, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -226,11 +226,13 @@ function isSafeHerdrTabCreate(argv: string[]): boolean {
226
226
  function hasSafeLocalAssignmentCommands(
227
227
  command: string,
228
228
  assignments: Map<string, { value: string; end: number }>,
229
+ statusCaptures: Set<string>,
229
230
  ): boolean {
230
231
  if (!assignments.size) return true;
231
232
  const parsed = parseStaticShellCommands(command);
232
233
  if (!parsed || typeof parsed === "string") return false;
233
234
  return parsed.pipelines.flat().every((words) => {
235
+ if (words.length === 1 && statusCaptures.has(words[0] ?? "")) return true;
234
236
  const assignment = /^([A-Za-z_][A-Za-z0-9_]*)=([A-Za-z0-9_./-]+)$/.exec(words[0] ?? "");
235
237
  if (assignment) {
236
238
  return (
@@ -420,6 +422,7 @@ function literalBindingText(
420
422
  command: string,
421
423
  name: string,
422
424
  value: string,
425
+ allowBrackets = false,
423
426
  ): { text: string; syntax: string } | undefined {
424
427
  let text = "";
425
428
  let syntax = "";
@@ -451,7 +454,11 @@ function literalBindingText(
451
454
  syntax += quote ? " ".repeat(value.length) : value;
452
455
  i += match[0].length - 1;
453
456
  } else {
454
- if (char === "`" || (!quote && /[(){}*?[\]#]/.test(char))) return undefined;
457
+ if (
458
+ char === "`" ||
459
+ (!quote && (/[(){}*?#]/.test(char) || (!allowBrackets && (char === "[" || char === "]"))))
460
+ )
461
+ return undefined;
455
462
  text += char;
456
463
  syntax += quote ? " " : char;
457
464
  }
@@ -606,24 +613,45 @@ function literalShellBodies(command: string): string[] | undefined {
606
613
  return bodies;
607
614
  }
608
615
 
609
- function isProvedLiteralShellBody(body: string): boolean {
616
+ function isProvedLiteralShellBody(
617
+ body: string,
618
+ allowedNames: Set<string>,
619
+ isPathBlocked: (path: string) => boolean,
620
+ onProvedPredicate: () => void,
621
+ ): boolean {
610
622
  if (!body.trim()) return true;
611
623
  const materialized = materializeLiteralShell(body)?.command ?? body;
612
624
  const parsed = parseStaticShellCommands(materialized);
613
- if (!parsed || typeof parsed === "string" || !hasProvedNavigationSequence(materialized))
614
- return false;
625
+ if (!parsed || typeof parsed === "string") {
626
+ let proved = false;
627
+ if (
628
+ hasEnvironmentVariableExpansionRead(body, allowedNames, isPathBlocked, undefined, () => {
629
+ proved = true;
630
+ }) ||
631
+ !proved
632
+ )
633
+ return false;
634
+ onProvedPredicate();
635
+ return true;
636
+ }
637
+ if (!hasProvedNavigationSequence(materialized)) return false;
615
638
  if (isStaticHerdrPrompt(materialized)) return true;
616
639
  return parsed.pipelines.flat().every((words) => {
617
640
  const argv = unwrapProofCommand(words, "\0");
618
641
  if (!argv) return false;
619
642
  if (argv[0]?.split("/").at(-1) === "env") return false;
620
643
  const name = unwrapStaticCommand(argv)[0]?.split("/").at(-1) ?? "";
621
- if (isShellWord(name))
644
+ if (isShellWord(name)) {
645
+ const nested = literalShellBodies(
646
+ words.map((word) => "'" + word.replaceAll("'", "'\"'\"'") + "'").join(" "),
647
+ );
622
648
  return (
623
- literalShellBodies(
624
- words.map((word) => "'" + word.replaceAll("'", "'\"'\"'") + "'").join(" "),
625
- ) !== undefined
649
+ nested !== undefined &&
650
+ nested.every((inner) =>
651
+ isProvedLiteralShellBody(inner, allowedNames, isPathBlocked, onProvedPredicate),
652
+ )
626
653
  );
654
+ }
627
655
  return (
628
656
  isProvedCwdSubprocessRole(unwrapStaticCommand(argv)) ||
629
657
  (name === "bun" && isSafeLocalAssignmentCommand(argv) && !isUnsupportedInlineRuntime(argv)) ||
@@ -1890,6 +1918,37 @@ function isStaticNavigationCommand(argv: string[]): boolean {
1890
1918
  );
1891
1919
  }
1892
1920
 
1921
+ /** Literal wrappers and exact directory-operation roles; unknown consumers keep refusal. */
1922
+ function isProvedDirectoryCommand(words: string[]): boolean {
1923
+ const argv = unwrapStaticCommand(words);
1924
+ if (
1925
+ !words
1926
+ .slice(0, words.length - argv.length)
1927
+ .every((word) => ["rtk", "proxy", "command", "--"].includes(word))
1928
+ )
1929
+ return false;
1930
+ if (argv[0] === "cd") return isStaticNavigationCommand(argv);
1931
+ if (argv[0] !== "git") return false;
1932
+ let index = 1;
1933
+ while (argv[index] === "-C") {
1934
+ if (!isStaticNavigationCommand(["cd", argv[index + 1] ?? ""])) return false;
1935
+ index += 2;
1936
+ }
1937
+ const args = argv.slice(index);
1938
+ if (args[0] === "status")
1939
+ return args.length === 1 || (args.length === 2 && args[1] === "--short");
1940
+ if (args[0] === "fetch")
1941
+ return (
1942
+ args.length >= 2 &&
1943
+ args.length <= 3 &&
1944
+ args.slice(1).every((arg) => /^[A-Za-z0-9_][A-Za-z0-9_./-]*$/.test(arg))
1945
+ );
1946
+ return (
1947
+ (args.length === 2 && args[0] === "rev-parse" && args[1] === "HEAD") ||
1948
+ (args.length === 3 && args[0] === "rev-list" && args[1] === "--count" && args[2] === "HEAD")
1949
+ );
1950
+ }
1951
+
1893
1952
  /** Prove a single cd whose uses are gated on success, without simulating shell state. */
1894
1953
  function hasProvedNavigationSequence(command: string): boolean {
1895
1954
  const literal = literalBindingText(command, "", "");
@@ -2279,6 +2338,7 @@ function hasSensitiveFileRead(command: string, isPathBlocked: (path: string) =>
2279
2338
  if (parsed && parsed !== "brace-expansion") {
2280
2339
  let directory = "";
2281
2340
  const trackDirectory = hasProvedNavigationSequence(command);
2341
+ const safeDirectorySequence = parsed.pipelines.flat().every(isProvedDirectoryCommand);
2282
2342
  return parsed.pipelines.flat().some((words) => {
2283
2343
  const argv = unwrapStaticCommand(words);
2284
2344
  const inspectedArgv =
@@ -2291,19 +2351,36 @@ function hasSensitiveFileRead(command: string, isPathBlocked: (path: string) =>
2291
2351
  path.startsWith("/") ? posix.normalize(path) : posix.join(commandDirectory, path);
2292
2352
  const blockedOperand = (path: string) =>
2293
2353
  [path, inDirectory(path)].some((value) => isPathBlocked(value) || isSensitivePath(value));
2294
- const blockedDirectory = (path: string) => blockedOperand(path) || blockedOperand(path + "/");
2354
+ // Directory slots use path policies, not the broad credential-file name heuristic.
2355
+ const blockedDirectory = (path: string) =>
2356
+ [path, inDirectory(path)].some(
2357
+ (value) => isPathBlocked(value) || isPathBlocked(value + "/"),
2358
+ );
2295
2359
  if (name === "cd") {
2296
2360
  const path = inspected[inspected[1] === "--" ? 2 : 1];
2297
2361
  if (path === undefined) return false;
2298
2362
  if (blockedDirectory(path)) return true;
2363
+ // A successful cd alone proves neither its later readers nor pipeline consumers safe.
2364
+ if (
2365
+ (!trackDirectory || !safeDirectorySequence) &&
2366
+ (blockedOperand(path) || blockedOperand(path + "/"))
2367
+ )
2368
+ return true;
2299
2369
  if (trackDirectory && isStaticNavigationCommand(inspected)) directory = inDirectory(path);
2300
2370
  return false;
2301
2371
  }
2302
2372
  if (name === "git") {
2303
2373
  let index = 1;
2304
2374
  while (inspected[index] === "-C" && inspected[index + 1]) {
2305
- if (blockedDirectory(inspected[index + 1] ?? "")) return true;
2306
- commandDirectory = inDirectory(inspected[index + 1] ?? "");
2375
+ const path = inspected[index + 1] ?? "";
2376
+ if (blockedDirectory(path)) return true;
2377
+ // Git -C affects this command only; following consumers keep the shell directory.
2378
+ if (
2379
+ !isProvedDirectoryCommand(words) &&
2380
+ (blockedOperand(path) || blockedOperand(path + "/"))
2381
+ )
2382
+ return true;
2383
+ commandDirectory = inDirectory(path);
2307
2384
  index += 2;
2308
2385
  }
2309
2386
  // Git grep has the same pattern-versus-file roles as grep below.
@@ -2483,7 +2560,10 @@ function hasEnvironmentVariableExpansionRead(
2483
2560
  command: string,
2484
2561
  allowedNames: Set<string>,
2485
2562
  isPathBlocked: (path: string) => boolean,
2563
+ onProvedStatus?: (command: string) => void,
2564
+ onProvedPredicate?: () => void,
2486
2565
  ): boolean {
2566
+ const originalCommand = command;
2487
2567
  let heredoc = boundedHeredoc(command);
2488
2568
  while (heredoc) {
2489
2569
  // In an unquoted heredoc, # and quote characters are data, not shell syntax.
@@ -2492,10 +2572,18 @@ function hasEnvironmentVariableExpansionRead(
2492
2572
  heredoc = boundedHeredoc(command);
2493
2573
  }
2494
2574
  if (!command.includes("$") && !command.includes("`")) return false;
2575
+ let predicateSyntaxIntact = command === originalCommand;
2495
2576
  let invalidExpansion = false;
2496
2577
  const assignments = getLeadingLiteralAssignments(command);
2497
- const localValues = new Map<string, string>();
2498
2578
  let localIndex = 0;
2579
+ const directCaptures = new Map<string, string>();
2580
+ const statusReferences = new Map<string, { name: string; fallback: string }>();
2581
+ const marker = (prefix: string): string => {
2582
+ let value: string;
2583
+ do value = `${prefix}${localIndex++}END`;
2584
+ while (command.includes(value));
2585
+ return value;
2586
+ };
2499
2587
  let expansions = 0;
2500
2588
  let normalized = "";
2501
2589
  let quote: "'" | '"' | undefined;
@@ -2503,6 +2591,7 @@ function hasEnvironmentVariableExpansionRead(
2503
2591
  for (let i = 0; i < command.length; i++) {
2504
2592
  const char = command[i] ?? "";
2505
2593
  if (!quote && char === "#" && !wordStarted) {
2594
+ predicateSyntaxIntact = false;
2506
2595
  while (i + 1 < command.length && command[i + 1] !== "\n") i++;
2507
2596
  continue;
2508
2597
  }
@@ -2534,7 +2623,18 @@ function hasEnvironmentVariableExpansionRead(
2534
2623
  }
2535
2624
  if (command[i + 1] === "?") {
2536
2625
  expansions++;
2537
- normalized += "NUMERICSTATUSVALUE";
2626
+ const value = marker("NUMERICSTATUSVALUE");
2627
+ // Only an exact standalone assignment token captures in the parent shell.
2628
+ // Quoting/escaping the name can instead produce an executable command word.
2629
+ const capture = /(?:^|[;\n])[ \t]*([A-Za-z_][A-Za-z0-9_]*)=("?)$/.exec(command.slice(0, i));
2630
+ const ending = capture?.[2] === '"' ? '"' : "";
2631
+ if (
2632
+ capture &&
2633
+ command.slice(i + 2).startsWith(ending) &&
2634
+ /^[ \t]*(?:[;\n]|$)/.test(command.slice(i + 2 + ending.length))
2635
+ )
2636
+ directCaptures.set(value, capture[1] ?? "");
2637
+ normalized += value;
2538
2638
  i++;
2539
2639
  continue;
2540
2640
  }
@@ -2558,26 +2658,101 @@ function hasEnvironmentVariableExpansionRead(
2558
2658
  }
2559
2659
  const assignment = assignments.get(name);
2560
2660
  if (assignment && assignment.end >= i) invalidExpansion = true;
2561
- if (assignment && assignment.end < i) {
2562
- const marker = `LOCALVALUE${localIndex++}`;
2563
- localValues.set(marker, assignment.value);
2564
- normalized += marker;
2565
- } else if (allowedNames.has(name)) {
2566
- normalized += quote ? "ENVVALUE" : "ENVVALUEUNQUOTED";
2567
- } else {
2568
- normalized += quote ? "UNKNOWNVALUE" : "UNKNOWNVALUEUNQUOTED";
2569
- }
2661
+ const reference = marker("STATUSREFERENCE");
2662
+ const fallback =
2663
+ assignment && assignment.end < i
2664
+ ? assignment.value
2665
+ : allowedNames.has(name)
2666
+ ? quote
2667
+ ? "ENVVALUE"
2668
+ : "ENVVALUEUNQUOTED"
2669
+ : quote
2670
+ ? "UNKNOWNVALUE"
2671
+ : "UNKNOWNVALUEUNQUOTED";
2672
+ statusReferences.set(reference, { name, fallback });
2673
+ normalized += reference;
2570
2674
  i += variable[0].length - 1;
2571
2675
  }
2572
2676
  if (!expansions) return invalidExpansion;
2573
2677
  if (invalidExpansion) return true;
2574
2678
 
2575
- const localMaterialized = [...localValues].reduce(
2576
- (text, [marker, value]) => text.replaceAll(marker, () => value),
2577
- normalized,
2578
- );
2679
+ // Resolve every reference at its command position; a later capture cannot prove
2680
+ // an earlier read, and only proved passive intermediates preserve the binding.
2681
+ const initial = parseStaticShellCommands(normalized);
2682
+ if (!initial || typeof initial === "string") return true;
2683
+ const captures = new Map<string, string>();
2684
+ for (const words of initial.pipelines.flat()) {
2685
+ const capture = /^([A-Za-z_][A-Za-z0-9_]*)=(.+)$/.exec(words[0] ?? "");
2686
+ if (capture && directCaptures.get(capture[2] ?? "") === capture[1])
2687
+ captures.set(words[0] ?? "", capture[1] ?? "");
2688
+ }
2689
+ const statusCaptures = new Set<string>();
2690
+ const localStatuses = new Set<string>();
2691
+ const resolutions = new Map<string, string>();
2692
+ const capturedNames = new Set(captures.values());
2693
+ const bound = new Set<string>();
2694
+ if (captures.size) {
2695
+ const syntax = literalBindingText(normalized, "", "")?.syntax;
2696
+ if (syntax === undefined || /&&|\|\||&/.test(syntax.replace(/[<>]&[0-9-]+/g, ""))) return true;
2697
+ }
2698
+ for (const [index, pipeline] of initial.pipelines.entries()) {
2699
+ for (const words of pipeline) {
2700
+ for (const arg of words) {
2701
+ for (const [reference, { name, fallback }] of statusReferences) {
2702
+ if (!arg.includes(reference)) continue;
2703
+ if (bound.has(name)) {
2704
+ const value = marker("NUMERICSTATUSVALUE");
2705
+ localStatuses.add(value);
2706
+ resolutions.set(reference, value);
2707
+ } else {
2708
+ if (capturedNames.has(name)) return true;
2709
+ resolutions.set(reference, fallback);
2710
+ }
2711
+ }
2712
+ }
2713
+ const capture = captures.get(words[0] ?? "");
2714
+ if (capture) {
2715
+ if (
2716
+ pipeline.length !== 1 ||
2717
+ words.length !== 1 ||
2718
+ initial.redirects.some((redirect) => redirect.pipeline === index) ||
2719
+ // Inherited export attributes can make a numeric value select code or paths.
2720
+ /^(?:LD_|DYLD_|_RLD_|LDR_|GIT_CONFIG_(?:KEY|VALUE)_)/.test(capture) ||
2721
+ /^(?:PATH|IFS|CDPATH|BASH_ENV|ENV|FPATH|ZDOTDIR|SHELLOPTS|BASHOPTS|PROMPT_COMMAND|PS[0-4]|GLOBIGNORE|RANDOM|SECONDS|PIPESTATUS|UID|EUID|BASH_XTRACEFD|HOME|XDG_CONFIG_HOME|LIBPATH|SHLIB_PATH|NODE_OPTIONS|NODE_PATH|BUN_OPTIONS|BUN_PRELOAD|PYTHONPATH|PYTHONSTARTUP|PYTHONHOME|RUBYOPT|RUBYLIB|GEM_HOME|GEM_PATH|PERL5OPT|PERL5LIB|PERLLIB|JAVA_TOOL_OPTIONS|JDK_JAVA_OPTIONS|CLASSPATH|_JAVA_OPTIONS|JAVA_HOME|GIT_SSH_COMMAND|GIT_SSH|GIT_PROXY_COMMAND|GIT_EXEC_PATH|GIT_CONFIG|GIT_CONFIG_GLOBAL|GIT_CONFIG_SYSTEM|GIT_CONFIG_PARAMETERS|GIT_CONFIG_COUNT|GIT_ASKPASS|SSH_ASKPASS|GIT_EDITOR|GIT_SEQUENCE_EDITOR|GIT_PAGER|PAGER|EDITOR|VISUAL)$/.test(
2722
+ capture,
2723
+ )
2724
+ )
2725
+ return true;
2726
+ bound.add(capture);
2727
+ statusCaptures.add(words[0] ?? "");
2728
+ continue;
2729
+ }
2730
+ const argv = unwrapStaticCommand(words);
2731
+ const name = argv[0]?.split("/").at(-1) ?? "";
2732
+ if (
2733
+ captures.size &&
2734
+ /^(?:alias|unalias|readonly|declare|typeset|local|export|eval|source|\.|trap|set|read|unset|let)$/.test(
2735
+ name,
2736
+ )
2737
+ )
2738
+ return true;
2739
+ if (!isPassiveTextCommand(argv) && name !== "cat") bound.clear();
2740
+ }
2741
+ for (const redirect of initial.redirects.filter((value) => value.pipeline === index)) {
2742
+ if (
2743
+ [...statusReferences].some(
2744
+ ([reference, { name }]) => capturedNames.has(name) && redirect.target.includes(reference),
2745
+ )
2746
+ )
2747
+ return true;
2748
+ }
2749
+ }
2750
+ for (const [reference, { fallback }] of statusReferences) {
2751
+ normalized = normalized.replaceAll(reference, () => resolutions.get(reference) ?? fallback);
2752
+ }
2753
+ const localMaterialized = normalized;
2579
2754
  // Options must be checked after local literals become actual argv, never as placeholders.
2580
- if (!hasSafeLocalAssignmentCommands(localMaterialized, assignments)) return true;
2755
+ if (!hasSafeLocalAssignmentCommands(localMaterialized, assignments, statusCaptures)) return true;
2581
2756
  const parsed = parseStaticShellCommands(localMaterialized);
2582
2757
  if (!parsed || typeof parsed === "string") return true;
2583
2758
  for (const [index, pipeline] of parsed.pipelines.entries()) {
@@ -2586,6 +2761,7 @@ function hasEnvironmentVariableExpansionRead(
2586
2761
  if (parsed.redirects.some((redirect) => redirect.pipeline === index)) return true;
2587
2762
  if (
2588
2763
  pipeline.some((words) => {
2764
+ if (words.length === 1 && statusCaptures.has(words[0] ?? "")) return false;
2589
2765
  const argv = unwrapStaticCommand(words);
2590
2766
  if (!words.some((arg) => arg.includes("NUMERICSTATUSVALUE")))
2591
2767
  return !isPassiveTextCommand(argv);
@@ -2594,6 +2770,9 @@ function hasEnvironmentVariableExpansionRead(
2594
2770
  return (
2595
2771
  !["echo", "printf"].includes(name ?? "") ||
2596
2772
  !isPassiveTextCommand(argv) ||
2773
+ argv.some(
2774
+ (arg) => arg.startsWith("-") && [...localStatuses].some((value) => arg.includes(value)),
2775
+ ) ||
2597
2776
  words
2598
2777
  .slice(0, words.length - argv.length)
2599
2778
  .some((arg) => arg.includes("NUMERICSTATUSVALUE")) ||
@@ -2617,11 +2796,12 @@ function hasEnvironmentVariableExpansionRead(
2617
2796
  return true;
2618
2797
  }
2619
2798
  if (!localMaterialized.includes("ENVVALUE")) {
2620
- return (
2799
+ const blocked =
2621
2800
  hasSensitiveFileRead(localMaterialized, isPathBlocked) ||
2622
2801
  hasSensitivePathRedirect(localMaterialized, isPathBlocked) ||
2623
- hasEnvironmentRead(localMaterialized, allowedNames)
2624
- );
2802
+ hasEnvironmentRead(localMaterialized, allowedNames);
2803
+ if (!blocked && statusCaptures.size) onProvedStatus?.(localMaterialized);
2804
+ return blocked;
2625
2805
  }
2626
2806
 
2627
2807
  for (const [index, pipeline] of parsed.pipelines.entries()) {
@@ -2657,11 +2837,25 @@ function hasEnvironmentVariableExpansionRead(
2657
2837
  return true;
2658
2838
  }
2659
2839
  }
2660
- const materialized = [...localValues].reduce(
2661
- (text, [marker, value]) => text.replaceAll(marker, () => value),
2662
- normalized.replaceAll("ENVVALUE", "SAFEENVVALUE"),
2663
- );
2664
- return hasSensitiveFileRead(materialized, isPathBlocked);
2840
+ const materialized = normalized.replaceAll("ENVVALUE", "SAFEENVVALUE");
2841
+ const blocked = hasSensitiveFileRead(materialized, isPathBlocked);
2842
+ if (!blocked && statusCaptures.size) onProvedStatus?.(materialized);
2843
+ // This is a complete command proof, not merely a recursively safe shell body.
2844
+ if (
2845
+ !blocked &&
2846
+ parsed.pipelines.length === 1 &&
2847
+ parsed.pipelines[0]?.length === 1 &&
2848
+ !parsed.redirects.length
2849
+ ) {
2850
+ const argv = unwrapProofCommand(parsed.pipelines[0]?.[0] ?? [], "ENVVALUE");
2851
+ if (argv && isApprovedEnvironmentPredicate(argv) && predicateSyntaxIntact) {
2852
+ // Roles alone cannot prove original shell boundaries or discarded controls.
2853
+ // Brackets are enabled only for the completely proved bracket builtin.
2854
+ const binding = literalBindingText(normalized, "", "", argv[0] === "[");
2855
+ if (binding && !/[;&|<>\r\n]/.test(binding.syntax.trim())) onProvedPredicate?.();
2856
+ }
2857
+ }
2858
+ return blocked;
2665
2859
  }
2666
2860
 
2667
2861
  /** Quoted approved metadata can be compared or checked for blankness, never evaluated. */
@@ -2971,11 +3165,37 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
2971
3165
  const shellBodies = literalShellBodies(command);
2972
3166
  if (!shellBodies || (shellBodies.length && shellDepth >= 8))
2973
3167
  return "cannot prove a bounded literal shell invocation";
3168
+ const provedPredicateShellBodies: string[] = [];
2974
3169
  for (const body of shellBodies) {
2975
3170
  const reason = getDangerousBashReason(body, shellDepth + 1);
2976
3171
  if (reason || getBlockedCliTool(body, true, shellDepth + 1))
2977
3172
  return reason ?? "invokes a blocked CLI from a shell body";
2978
- if (!isProvedLiteralShellBody(body)) return "cannot prove a closed literal shell body";
3173
+ if (
3174
+ !isProvedLiteralShellBody(body, allowedEnvironmentVariables, isPathBlocked, () => {
3175
+ provedPredicateShellBodies.push(body);
3176
+ })
3177
+ )
3178
+ return "cannot prove a closed literal shell body";
3179
+ }
3180
+ if (provedPredicateShellBodies.length) {
3181
+ const context = parseStaticShellCommands(command);
3182
+ const binding = literalBindingText(command, "", "");
3183
+ const argv =
3184
+ context && typeof context !== "string"
3185
+ ? unwrapProofCommand(context.pipelines[0]?.[0] ?? [], "\0")
3186
+ : undefined;
3187
+ if (
3188
+ !context ||
3189
+ typeof context === "string" ||
3190
+ context.pipelines.length !== 1 ||
3191
+ context.pipelines[0]?.length !== 1 ||
3192
+ context.redirects.length !== 0 ||
3193
+ !binding ||
3194
+ /[;&|<>\r\n]/.test(binding.syntax.trim()) ||
3195
+ !argv ||
3196
+ !SHELL_IDENTITIES.has(argv[0] ?? "")
3197
+ )
3198
+ return "cannot prove a single literal shell predicate invocation";
2979
3199
  }
2980
3200
  const proof = materializeLiteralShell(command);
2981
3201
  if (!proof && /^\s*for\b/.test(command)) return "cannot prove a bounded literal shell loop";
@@ -2999,19 +3219,28 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
2999
3219
  hasSensitivePathRedirect(command, isPathBlocked)
3000
3220
  )
3001
3221
  return "blocked by the file-access policy; a sensitive path matched or this command form could not be verified";
3222
+ let environmentCommand = command;
3002
3223
  if (
3003
3224
  !isLiteralTextWrite(command) &&
3004
- hasEnvironmentVariableExpansionRead(command, allowedEnvironmentVariables, isPathBlocked)
3225
+ hasEnvironmentVariableExpansionRead(
3226
+ command,
3227
+ allowedEnvironmentVariables,
3228
+ isPathBlocked,
3229
+ (proved) => {
3230
+ environmentCommand = proved;
3231
+ },
3232
+ )
3005
3233
  ) {
3006
3234
  return "expands an unapproved or executable environment variable value";
3007
3235
  }
3008
3236
  if (
3009
3237
  hasEnvironmentSourceAccess(command, rawEnvironmentSource, allowedEnvironmentVariables) ||
3010
3238
  hasEnvironmentRead(
3011
- command,
3239
+ environmentCommand,
3012
3240
  allowedEnvironmentVariables,
3013
3241
  allowPackageInventory,
3014
3242
  shellBodies.filter((body) => {
3243
+ if (provedPredicateShellBodies.includes(body)) return true;
3015
3244
  const parsed = parseStaticShellCommands(body);
3016
3245
  return (
3017
3246
  parsed &&