@blogic-cz/agent-tools 1.6.4 → 1.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -670,7 +670,9 @@ Python JSON parsing, dictionary iteration, and printing are not themselves evide
670
670
 
671
671
  The bounded Python subprocess proof supports one immutable literal argv binding followed by one `subprocess.run` call. A literal `cwd` is replayed as a quoted, success-gated directory change before the nested argv, so relative reader operands and named directories receive the ordinary path policies. Supported directories are literal relative or absolute paths, including quoted spaces. Empty, dynamic, Unicode, escaped, tilde and other unsupported directory forms refuse rather than dropping the context. Cwd proof recursively unwraps only supported command, RTK proxy and environment wrappers. Environment directory/split-string options and unproved wrapper forms refuse. Only supported passive text, reader, metadata, literal Bun script and blocked CLI roles receive nested replay; shell executors, inline interpreter programs, unproved sed programs, find execution actions and unknown command roles refuse. Nested blocked CLI, environment, sensitive-path and custom-command policies remain active. Outer shell redirects retain their original shell directory. Arbitrary multi-call Python and `check_output` programs remain unsupported; direct Git metadata commands provide index/tree inspection.
672
672
 
673
- Supported runtime script/tool operands form an argument boundary: a later tool `-c` configuration flag is data. Genuine eval and loader flags, Bun exec/repl and unproved startup-option arity still refuse. Literal Herdr prompts can compose with the existing literal agent-get and pane-read forms and passive consumers. One literal `--sql` operand on the supported `bun run db-tool sql/query` shape can contain SQL braces; substitutions, duplicate SQL operands and unknown wrapper options receive no exception. SQL output pipelines require closed passive consumers, so shell, xargs and unknown executors refuse. Numeric `$?` can appear only in passive echo/printf data operands, with passive pipeline consumers. Explicitly approved environment names can appear in quoted equality or blankness tests, including the quoted empty default `${NAME:-}`. This does not add default-approved names or permit shell evaluators. One sed print program with numeric or escaped-regex addresses/ranges and known `-n`/`-E` flags is passive; execution/read/write callbacks, extra programs and script files receive no print proof. Local file bindings use that same closed print proof and never a generic sed-word fallback. Ordinary path and configured command policies still apply to all these forms.
673
+ Supported runtime script/tool operands form an argument boundary: a later tool `-c` configuration flag is data. Genuine eval and loader flags, Bun exec/repl and unproved startup-option arity still refuse. Literal Herdr prompts can compose with literal agent-get, pane-read and agent-read forms and passive consumers. Both read forms require a literal identifier followed by `--source recent-unwrapped --lines N`, with positive integer `N`. A separate `sleep N` command accepts one literal nonnegative integer or decimal duration; delay pipelines, options and extra operands receive no composition proof. Delay redirects receive the ordinary sensitive-path checks. The wrapper sleep-polling guard still applies. One literal `--sql` operand on the supported `bun run db-tool sql/query` shape can contain SQL braces; substitutions, duplicate SQL operands and unknown wrapper options receive no exception. SQL output pipelines require closed passive consumers, so shell, xargs and unknown executors refuse. Numeric `$?` can appear only in passive echo/printf data operands, with passive pipeline consumers. Explicitly approved environment names can appear in quoted equality or blankness tests, including the quoted empty default `${NAME:-}`. This does not add default-approved names or permit shell evaluators. One sed print program with numeric or escaped-regex addresses/ranges and known `-n`/`-E` flags is passive; execution/read/write callbacks, extra programs and script files receive no print proof. Local file bindings use that same closed print proof and never a generic sed-word fallback. Ordinary path and configured command policies still apply to all these forms.
674
+
675
+ Literal shell bodies receive recursive shared policy replay under exact `sh -c`/`bash -c` and `zsh -f -c` invocations, using only these bare executable names or their `/bin/` and `/usr/bin/` paths and known RTK/command wrappers. Both protected paths and blocked CLIs remain enforced inside the body, along with environment and custom-command policies. A conventional shell-family name or numeric version in an unproved executing operand/control position refuses; proved literal text, reader and file-metadata operands remain data. Only the listed sh/bash/zsh identities receive body replay; rbash/rksh/ksh93 and versioned names refuse, and arbitrary renamed executables are outside this identity proof. Replay permits only existing closed body roles/materializations, at most eight shell levels and 64 KiB per invocation. Unproved options, environment prefixes, active exec/nohup/sudo wrappers, positional/script/stdin forms, executable binders, control flow and outer directory changes around a wrapper refuse. A body-internal proved cd affects body readers; outer redirects retain the outer shell directory. This is a textual body proof, not verification of native binaries or all host startup code. In particular, zsh -f still sources the global zshenv file. For unsupported executable inventories, use separate literal metadata requests and literal candidate invocations; no program name or --version flag receives a special exception.
674
676
 
675
677
  A separate bounded Node proof supports literal `package.json` inventory loops that print `bin` metadata or use the supported scripts filter. The complete invocation must preserve loader identity through known passive commands, closed literal writers, or proved inventories. Startup/environment mutations, unknown commands, loader changes, and other dynamic loading remain unproved and are refused.
676
678
 
@@ -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;AAwhFF,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,CA4TrF;AAQD,kEAAkE;AAClE,eAAO,MAAM,uBAAuB,UA93FD,SAAS,UAAU,UAAU,KAAK,IA83FM,CAAC;AAE5E,oDAAoD;AACpD,eAAO,MAAM,aAAa,YAh4FC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAg4FjB,CAAC;AAExD,yEAAyE;AACzE,eAAO,MAAM,aAAa,aAl4FE,MAAM,KAAK,OAk4FgB,CAAC;AAExD,yDAAyD;AACzD,eAAO,MAAM,aAAa,aAp4FE,MAAM,KAAK,OAo4FgB,CAAC;AAExD,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,YAt4FC,MAAM,KAAK,OAs4F0B,CAAC;AAE1E,iDAAiD;AACjD,eAAO,MAAM,iBAAiB,YAx4FC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAw4Ff,CAAC;AAEhE,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,YA14FC,MAAM,KAAK,OA04FsB,CAAC;AAElE,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,YA54FC,MAAM,KAAK,MAAM,GAAG,IA44Fa,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;AA8oFF,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,UAphGD,SAAS,UAAU,UAAU,KAAK,IAohGM,CAAC;AAE5E,oDAAoD;AACpD,eAAO,MAAM,aAAa,YAthGC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAshGjB,CAAC;AAExD,yEAAyE;AACzE,eAAO,MAAM,aAAa,aAxhGE,MAAM,KAAK,OAwhGgB,CAAC;AAExD,yDAAyD;AACzD,eAAO,MAAM,aAAa,aA1hGE,MAAM,KAAK,OA0hGgB,CAAC;AAExD,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,YA5hGC,MAAM,KAAK,OA4hG0B,CAAC;AAE1E,iDAAiD;AACjD,eAAO,MAAM,iBAAiB,YA9hGC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IA8hGf,CAAC;AAEhE,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,YAhiGC,MAAM,KAAK,OAgiGsB,CAAC;AAElE,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,YAliGC,MAAM,KAAK,MAAM,GAAG,IAkiGa,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "1.6.4",
3
+ "version": "1.6.6",
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",
@@ -510,26 +510,130 @@ function isLiteralLoopCommand(words: string[], marker: string, value: string): b
510
510
  );
511
511
  }
512
512
 
513
- function isLiteralHerdrPaneRead(argv: string[]): boolean {
513
+ function isLiteralHerdrRead(argv: string[]): boolean {
514
514
  return (
515
- argv.length === 8 &&
516
- argv.slice(0, 3).join(" ") === "herdr pane read" &&
515
+ argv[0] === "herdr" &&
516
+ !argv[3]?.startsWith("-") &&
517
517
  /^[A-Za-z0-9_.:-]+$/.test(argv[3] ?? "") &&
518
- argv[4] === "--source" &&
519
- argv[5] === "recent-unwrapped" &&
520
- argv[6] === "--lines" &&
521
- /^[1-9]\d*$/.test(argv[7] ?? "")
518
+ ((argv.length === 4 && argv[1] === "agent" && argv[2] === "get") ||
519
+ (argv.length === 8 &&
520
+ ["pane", "agent"].includes(argv[1] ?? "") &&
521
+ argv[2] === "read" &&
522
+ argv[4] === "--source" &&
523
+ argv[5] === "recent-unwrapped" &&
524
+ argv[6] === "--lines" &&
525
+ /^[1-9]\d*$/.test(argv[7] ?? "")))
522
526
  );
523
527
  }
524
528
 
525
- function isLiteralHerdrRead(argv: string[]): boolean {
526
- return (
527
- ((argv.length === 4 && argv.slice(0, 3).join(" ") === "herdr agent get") ||
528
- isLiteralHerdrPaneRead(argv)) &&
529
- /^[A-Za-z0-9_.:-]+$/.test(argv[3] ?? "")
529
+ /** Conventional platform shell names and numeric versions; not arbitrary renamed executables. */
530
+ function isShellWord(word: string): boolean {
531
+ return /^(?:sh|bash|rbash|zsh|dash|ash|ksh|rksh|csh|tcsh|fish)(?:[.-]?[0-9]+(?:[.][0-9]+)*)?$/.test(
532
+ word.split("/").at(-1) ?? "",
530
533
  );
531
534
  }
532
535
 
536
+ /** Shell-named operands are data only under an existing non-executing argument role. */
537
+ function hasProvedShellDataOperands(argv: string[]): boolean {
538
+ const name = argv[0]?.split("/").at(-1) ?? "";
539
+ if (isPassiveTextCommand(argv) || isLiteralHerdrRead(argv) || isStaticNavigationCommand(argv))
540
+ return true;
541
+ if (["cat", "ls", "uniq", "cut"].includes(name)) return true;
542
+ if (["find", "wc", "sort"].includes(name)) return isProvedCwdSubprocessRole(argv);
543
+ return name === "test" && argv.length === 3 && argv[1] === "-e" && !/[$`]/.test(argv[2] ?? "");
544
+ }
545
+
546
+ const SHELL_IDENTITIES = new Set([
547
+ "sh",
548
+ "bash",
549
+ "zsh",
550
+ "/bin/sh",
551
+ "/usr/bin/sh",
552
+ "/bin/bash",
553
+ "/usr/bin/bash",
554
+ "/bin/zsh",
555
+ "/usr/bin/zsh",
556
+ ]);
557
+
558
+ /** Literal argv only. Undefined means a shell was found without a complete invocation proof. */
559
+ function literalShellBodies(command: string): string[] | undefined {
560
+ const parsed = parseStaticShellCommands(command);
561
+ const operands = parsed ?? parseStaticShellCommands(command, true);
562
+ if (!operands || typeof operands === "string") return [];
563
+ const commands = operands.pipelines.flat();
564
+ if (
565
+ commands.some((words) =>
566
+ ["exec", "nohup", "sudo"].includes(unwrapStaticCommand(words)[0]?.split("/").at(-1) ?? ""),
567
+ )
568
+ )
569
+ return undefined;
570
+ for (const words of commands) {
571
+ const argv = unwrapStaticCommand(words);
572
+ if (
573
+ !isShellWord(argv[0] ?? "") &&
574
+ argv.some(
575
+ (word, index) =>
576
+ index !== inlineProgramIndex(argv) &&
577
+ (isShellWord(word) ||
578
+ (/^--?[A-Za-z0-9-]+=/.test(word) && isShellWord(word.slice(word.indexOf("=") + 1)))),
579
+ ) &&
580
+ !hasProvedShellDataOperands(argv) &&
581
+ !isStaticHerdrPrompt(command)
582
+ )
583
+ return undefined;
584
+ }
585
+ const shells = commands.filter((words) => isShellWord(unwrapStaticCommand(words)[0] ?? ""));
586
+ if (!shells.length) return [];
587
+ if (
588
+ !parsed ||
589
+ typeof parsed === "string" ||
590
+ !literalBindingText(command, "", "") ||
591
+ new TextEncoder().encode(command).byteLength > 65_536 ||
592
+ commands.some((words) => unwrapStaticCommand(words)[0]?.split("/").at(-1) === "cd")
593
+ )
594
+ return undefined;
595
+ const bodies: string[] = [];
596
+ for (const words of shells) {
597
+ const argv = unwrapProofCommand(words, "\0");
598
+ if (!argv || !SHELL_IDENTITIES.has(argv[0] ?? "")) return undefined;
599
+ const name = argv[0]?.split("/").at(-1);
600
+ if (argv.length === 2 && ["--version", "--help"].includes(argv[1] ?? "")) continue;
601
+ const index = name === "zsh" && argv[1] === "-f" ? 2 : 1;
602
+ if ((name === "zsh" && index !== 2) || argv[index] !== "-c" || argv.length !== index + 2)
603
+ return undefined;
604
+ bodies.push(argv[index + 1] ?? "");
605
+ }
606
+ return bodies;
607
+ }
608
+
609
+ function isProvedLiteralShellBody(body: string): boolean {
610
+ if (!body.trim()) return true;
611
+ const materialized = materializeLiteralShell(body)?.command ?? body;
612
+ const parsed = parseStaticShellCommands(materialized);
613
+ if (!parsed || typeof parsed === "string" || !hasProvedNavigationSequence(materialized))
614
+ return false;
615
+ if (isStaticHerdrPrompt(materialized)) return true;
616
+ return parsed.pipelines.flat().every((words) => {
617
+ const argv = unwrapProofCommand(words, "\0");
618
+ if (!argv) return false;
619
+ if (argv[0]?.split("/").at(-1) === "env") return false;
620
+ const name = unwrapStaticCommand(argv)[0]?.split("/").at(-1) ?? "";
621
+ if (isShellWord(name))
622
+ return (
623
+ literalShellBodies(
624
+ words.map((word) => "'" + word.replaceAll("'", "'\"'\"'") + "'").join(" "),
625
+ ) !== undefined
626
+ );
627
+ return (
628
+ isProvedCwdSubprocessRole(unwrapStaticCommand(argv)) ||
629
+ (name === "bun" && isSafeLocalAssignmentCommand(argv) && !isUnsupportedInlineRuntime(argv)) ||
630
+ isStaticNavigationCommand(argv) ||
631
+ isLiteralHerdrRead(argv) ||
632
+ (argv.length === 1 && ["true", "false", "pwd"].includes(name))
633
+ );
634
+ });
635
+ }
636
+
533
637
  /** A complete stdin-only JSON projection, with no suffix, imports or executable input. */
534
638
  function isLiteralStdinJsonDisplay(argv: string[]): boolean {
535
639
  if (argv.length !== 3 || !["python", "python3"].includes(argv[0] ?? "") || argv[1] !== "-c")
@@ -1836,6 +1940,13 @@ function isStaticHerdrPrompt(command: string): boolean {
1836
1940
  );
1837
1941
  if (isPassiveTextCommand(argv)) return true;
1838
1942
  if (isLiteralHerdrRead(argv)) return index === 0;
1943
+ if (argv[0] === "sleep")
1944
+ return (
1945
+ index === 0 &&
1946
+ pipeline.length === 1 &&
1947
+ argv.length === 2 &&
1948
+ /^\d+(?:\.\d+)?$/.test(argv[1] ?? "")
1949
+ );
1839
1950
  if (isStaticNavigationCommand(argv))
1840
1951
  return index === 0 && (argv[0] !== "cd" || pipeline.length === 1);
1841
1952
  return false;
@@ -1858,6 +1969,7 @@ function hasEnvironmentRead(
1858
1969
  command: string,
1859
1970
  allowedNames: Set<string>,
1860
1971
  allowInventory = hasProvedPackageInventoryInvocation(command),
1972
+ replayedShellBodies: string[] = [],
1861
1973
  ): boolean {
1862
1974
  if (isStaticHerdrPrompt(command)) return false;
1863
1975
  if (isHerdrPrompt(command)) return true;
@@ -1926,7 +2038,13 @@ function hasEnvironmentRead(
1926
2038
  ) {
1927
2039
  return true;
1928
2040
  }
1929
- if (unwrapped.some((argv) => hasStaticEnvironmentRead(argv, allowedNames, allowInventory))) {
2041
+ if (
2042
+ unwrapped.some(
2043
+ (argv) =>
2044
+ !(SHELL_IDENTITIES.has(argv[0] ?? "") && replayedShellBodies.includes(argv.at(-1) ?? "")) &&
2045
+ hasStaticEnvironmentRead(argv, allowedNames, allowInventory),
2046
+ )
2047
+ ) {
1930
2048
  return true;
1931
2049
  }
1932
2050
 
@@ -2743,7 +2861,7 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
2743
2861
  return getDangerousBashReason(command) !== null;
2744
2862
  }
2745
2863
 
2746
- function getDangerousBashReason(command: string): string | null {
2864
+ function getDangerousBashReason(command: string, shellDepth = 0): string | null {
2747
2865
  const originalCommand = command;
2748
2866
  const allowPackageInventory = hasProvedPackageInventoryInvocation(originalCommand);
2749
2867
  const rawEnvironmentSource =
@@ -2757,20 +2875,30 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
2757
2875
  if (rawEnvironmentSource && hasEnvironmentMutationPrefix(originalCommand))
2758
2876
  return "blocked by the environment/execution policy after an unproved environment mutation";
2759
2877
  command = normalizeProgramHeredocs(command);
2878
+ const shellBodies = literalShellBodies(command);
2879
+ if (!shellBodies || (shellBodies.length && shellDepth >= 8))
2880
+ return "cannot prove a bounded literal shell invocation";
2881
+ for (const body of shellBodies) {
2882
+ const reason = getDangerousBashReason(body, shellDepth + 1);
2883
+ if (reason || getBlockedCliTool(body, true, shellDepth + 1))
2884
+ return reason ?? "invokes a blocked CLI from a shell body";
2885
+ if (!isProvedLiteralShellBody(body)) return "cannot prove a closed literal shell body";
2886
+ }
2760
2887
  const proof = materializeLiteralShell(command);
2761
2888
  if (!proof && /^\s*for\b/.test(command)) return "cannot prove a bounded literal shell loop";
2762
2889
  if (proof) {
2763
2890
  if (proof.paths.some((path) => hasSensitiveFileRead("cat " + path, isPathBlocked)))
2764
2891
  return "accesses a sensitive file path";
2765
2892
  command = proof.command;
2766
- if (getBlockedCliTool(command)) return "invokes a blocked CLI from a literal expansion";
2893
+ if (getBlockedCliTool(command, true, shellDepth))
2894
+ return "invokes a blocked CLI from a literal expansion";
2767
2895
  }
2768
2896
  for (const nested of [
2769
2897
  ...inlineSubprocessCommands(command),
2770
2898
  ...inlinePackageCommands(command, allowPackageInventory),
2771
2899
  ]) {
2772
- const reason = getDangerousBashReason(nested);
2773
- if (reason || getBlockedCliTool(nested))
2900
+ const reason = getDangerousBashReason(nested, shellDepth);
2901
+ if (reason || getBlockedCliTool(nested, true, shellDepth))
2774
2902
  return reason ?? "invokes a blocked CLI from a script";
2775
2903
  }
2776
2904
  if (
@@ -2786,7 +2914,21 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
2786
2914
  }
2787
2915
  if (
2788
2916
  hasEnvironmentSourceAccess(command, rawEnvironmentSource, allowedEnvironmentVariables) ||
2789
- hasEnvironmentRead(command, allowedEnvironmentVariables, allowPackageInventory)
2917
+ hasEnvironmentRead(
2918
+ command,
2919
+ allowedEnvironmentVariables,
2920
+ allowPackageInventory,
2921
+ shellBodies.filter((body) => {
2922
+ const parsed = parseStaticShellCommands(body);
2923
+ return (
2924
+ parsed &&
2925
+ typeof parsed !== "string" &&
2926
+ parsed.pipelines
2927
+ .flat()
2928
+ .every((words) => isPassiveTextCommand(unwrapStaticCommand(words)))
2929
+ );
2930
+ }),
2931
+ )
2790
2932
  ) {
2791
2933
  return "blocked by the environment/execution policy; permitted access or non-execution could not be established for this command form";
2792
2934
  }
@@ -2828,16 +2970,24 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
2828
2970
  function getBlockedCliTool(
2829
2971
  command: string,
2830
2972
  expand = true,
2973
+ shellDepth = 0,
2831
2974
  ): { name: string; wrapper: string } | null {
2832
2975
  command = normalizeProgramHeredocs(command);
2976
+ const shellBodies = literalShellBodies(command);
2977
+ if (shellBodies && shellDepth < 8) {
2978
+ for (const body of shellBodies) {
2979
+ const blocked = getBlockedCliTool(body, true, shellDepth + 1);
2980
+ if (blocked) return blocked;
2981
+ }
2982
+ }
2833
2983
  const proof = expand ? materializeLiteralShell(command) : undefined;
2834
2984
  if (proof) {
2835
- const originalBlocked = getBlockedCliTool(command, false);
2985
+ const originalBlocked = getBlockedCliTool(command, false, shellDepth);
2836
2986
  if (originalBlocked) return originalBlocked;
2837
2987
  command = proof.command;
2838
2988
  }
2839
2989
  for (const nested of inlineSubprocessCommands(command)) {
2840
- const blocked = getBlockedCliTool(nested);
2990
+ const blocked = getBlockedCliTool(nested, true, shellDepth);
2841
2991
  if (blocked) return blocked;
2842
2992
  }
2843
2993
  const staticCommands = parseStaticShellCommands(command);