javi-forge 1.39.0 → 1.39.1

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.
@@ -792,12 +792,63 @@ function bashSubstitutions(command) {
792
792
  }
793
793
  return bodies;
794
794
  }
795
+ // This deliberately recognizes only a direct, quoted Python stdin heredoc on
796
+ // the first command line. It removes the opaque body before shell inspection;
797
+ // it does not parse Python or permit its execution.
798
+ function pythonQuotedHeredoc(command) {
799
+ const newline = command.indexOf("\n");
800
+ const header = newline < 0 ? command : command.slice(0, newline);
801
+ const starts = [0];
802
+ let quote = "", escaped = false, heredocs = 0;
803
+ for (let index = 0; index < header.length; index++) {
804
+ const char = header[index];
805
+ if (escaped) { escaped = false; continue; }
806
+ if (char === "\\" && quote !== "'") { escaped = true; continue; }
807
+ if (quote) { if (char === quote) quote = ""; continue; }
808
+ if (char === "'" || char === '"' || char === "`") { quote = char; continue; }
809
+ if (char === "<" && header[index + 1] === "<") { heredocs++; index++; }
810
+ if (char === ";" || char === "|" || char === "&") starts.push(index + 1);
811
+ }
812
+ const literalPath = String.raw`(?:[A-Za-z0-9_./~-]+|'[A-Za-z0-9_./~ -]+'|"[A-Za-z0-9_./~ -]+")`;
813
+ const redirection = String.raw`(?:[0-9]*>>?|[0-9]*<)[ \t]*${literalPath}[ \t]*`;
814
+ const python = String.raw`[ \t]*(?:python3?|/(?:[A-Za-z0-9_.-]+/)*python3?)(?:[ \t]+-)?[ \t]*`;
815
+ const pattern = new RegExp(String.raw`^(${python}(?:${redirection})*)<<(-?)[ \t]*(['"])([A-Za-z_][A-Za-z0-9_]*)\3([ \t]*(?:${redirection})*)([;|&].*)?$`);
816
+ for (const start of starts) {
817
+ const match = pattern.exec(header.slice(start));
818
+ if (!match) continue;
819
+ // Multiple documents and tab-stripping need a shell grammar this guard
820
+ // intentionally does not implement, so retain fail-closed behavior.
821
+ if (newline < 0 || heredocs !== 1 || match[2]) fail("unlexable-command");
822
+ if (match[6]?.trimStart().startsWith("|")) fail("unlexable-command");
823
+ let offset = newline + 1;
824
+ while (offset <= command.length) {
825
+ const end = command.indexOf("\n", offset);
826
+ const lineEnd = end < 0 ? command.length : end;
827
+ if (command.slice(offset, lineEnd) === match[4]) {
828
+ const outerHeader = header.slice(0, start) + match[1] + match[5] + (match[6] ?? "");
829
+ return { command: `${outerHeader}\n${command.slice(lineEnd + 1)}`, unsupported: true };
830
+ }
831
+ if (end < 0) break;
832
+ offset = end + 1;
833
+ }
834
+ fail("unlexable-command");
835
+ }
836
+ return { command, unsupported: false };
837
+ }
795
838
  function evaluateBash(command, cwd, config = AGENT_CONFIGS.claude, projectRoot = PROJECT_ROOT, depth = 0) {
796
839
  if (depth > 4) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
840
+ let unsupported = false;
841
+ try { ({ command, unsupported } = pythonQuotedHeredoc(command)); } catch { return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; }
797
842
  const headerOnlyCommand = opaqueInertCatHereDoc(command, cwd, config, projectRoot);
798
843
  if (headerOnlyCommand === null) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
799
844
  command = headerOnlyCommand;
800
- try { for (const body of bashSubstitutions(command)) { const nested = evaluateBash(body, cwd, config, projectRoot, depth + 1); if (!nested.allowed) return nested; } } catch { return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; }
845
+ try {
846
+ for (const body of bashSubstitutions(command)) {
847
+ const nested = evaluateBash(body, cwd, config, projectRoot, depth + 1);
848
+ if (nested.ruleId === "shell.unsupported-interpreter") unsupported = true;
849
+ else if (!nested.allowed) return nested;
850
+ }
851
+ } catch { return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; }
801
852
  if (/^\s*:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:\s*$/.test(command)) return { allowed: false, ruleId: "shell.destructive-root" };
802
853
  let parsed;
803
854
  try { parsed = lex(command); } catch { return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; }
@@ -834,10 +885,16 @@ function evaluateBash(command, cwd, config = AGENT_CONFIGS.claude, projectRoot =
834
885
  if (/^(?:powershell|pwsh)(?:\.exe)?$/i.test(executable) && tokens.some((token) => /^-(?:enc|encodedcommand)$/i.test(token))) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
835
886
  if (/^(?:bash|sh|zsh|dash|ksh)$/.test(executable)) {
836
887
  const flag = tokens.findIndex((token) => /^-[^-]*c[^-]*$/.test(token));
837
- if (flag >= 0) { const body = tokens[flag + 1]; if (!body || /\$(?!\()/.test(body)) return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; const nested = evaluateBash(body, cwd, config, projectRoot, depth + 1); if (!nested.allowed) return nested; }
888
+ if (flag >= 0) {
889
+ const body = tokens[flag + 1];
890
+ if (!body || /\$(?!\()/.test(body)) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
891
+ const nested = evaluateBash(body, cwd, config, projectRoot, depth + 1);
892
+ if (nested.ruleId === "shell.unsupported-interpreter") unsupported = true;
893
+ else if (!nested.allowed) return nested;
894
+ }
838
895
  }
839
896
  }
840
- return { allowed: true };
897
+ return unsupported ? { allowed: false, ruleId: "shell.unsupported-interpreter" } : { allowed: true };
841
898
  }
842
899
  function evaluatePowerShell(command, cwd, config = AGENT_CONFIGS.claude, projectRoot = PROJECT_ROOT) {
843
900
  const parsed = lex(command, true);
@@ -939,6 +996,7 @@ function diagnostic(error) {
939
996
  }
940
997
  function denialDiagnostic(toolName, decision) {
941
998
  const tool = SUPPORTED_TOOLS.includes(toolName) ? toolName : "supported tool";
999
+ if (decision.ruleId === "shell.unsupported-interpreter") return "javi-forge PreToolUse denied Bash [shell.unsupported-interpreter]: quoted Python heredoc execution is unsupported";
942
1000
  if (decision.ambiguity) return `javi-forge PreToolUse denied ${tool} [${decision.ruleId}]: ${decision.ambiguity.utility} ${decision.ambiguity.profile} ${decision.ambiguity.sink} semantics denied as ambiguous`;
943
1001
  return `javi-forge PreToolUse denied ${tool} [${decision.ruleId}]: global guard policy denied the invocation`;
944
1002
  }
@@ -4,7 +4,7 @@
4
4
  "name": "javi-forge-skillguard-pre-tool-use.mjs",
5
5
  "version": 1,
6
6
  "policyVersion": 2,
7
- "sha256": "6edfbb31ce0551b27e38ae1ffd1daf9cc4bea54f2c86687c5124132af5b8c0af",
7
+ "sha256": "507a57a15f1b967103bb1eefe701a74813d5a9603fa7bcc15579a215b03621e3",
8
8
  "historical": [
9
9
  "78be7e6613c012280b7ad17886462ba166b63ebd031e34565d757b3a0796d7cc",
10
10
  "5dc2a5c31131f4ac7d8657c78b950de52776aad6eaefe78ea0d764a9963c4425",
@@ -12,7 +12,8 @@
12
12
  "3581862f0567cce75a58b693c9ade80d39ee7d58add11537a34a8461c47c1ed4",
13
13
  "54a270f28b068450b79547a88ec6f2d4854514392fd5f38ed1d6174ea093d7aa",
14
14
  "9a565cec31d9e091e3fb9420b86685f824733bc1ebe479f086b2b955aba6ef3e",
15
- "59fc4224975ad64cfc85bab50ec60d9bd4948070e9d41f42ab43e6d8231c19a1"
15
+ "59fc4224975ad64cfc85bab50ec60d9bd4948070e9d41f42ab43e6d8231c19a1",
16
+ "6edfbb31ce0551b27e38ae1ffd1daf9cc4bea54f2c86687c5124132af5b8c0af"
16
17
  ]
17
18
  },
18
19
  "settingsEntries": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javi-forge",
3
- "version": "1.39.0",
3
+ "version": "1.39.1",
4
4
  "description": "Project scaffolding and AI-ready CI bootstrap",
5
5
  "type": "module",
6
6
  "bin": {