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 {
|
|
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) {
|
|
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": "
|
|
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": {
|