javi-forge 1.38.2 → 1.38.3

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.
@@ -177,6 +177,97 @@ function lex(command, powershell = false) {
177
177
  if (!commands.at(-1).length) commands.pop();
178
178
  return { commands, separators };
179
179
  }
180
+ function hasUnquotedHereDocOperator(line) {
181
+ let quote = "", escaped = false;
182
+ for (let index = 0; index < line.length; index++) {
183
+ const char = line[index];
184
+ if (escaped) { escaped = false; continue; }
185
+ if (quote) {
186
+ if (char === "\\" && quote === '"') escaped = true;
187
+ else if (char === quote) quote = "";
188
+ continue;
189
+ }
190
+ if (char === "\\") { escaped = true; continue; }
191
+ if (char === "'" || char === '"') { quote = char; continue; }
192
+ if (char === "#" && (index === 0 || /\s/.test(line[index - 1]))) break;
193
+ if (char === "<" && line[index + 1] === "<" && line[index + 2] !== "<") return true;
194
+ }
195
+ return false;
196
+ }
197
+ const SHELL_HEADER_WHITESPACE = new Set([" ", "\t"]);
198
+ const STATIC_FILE_TARGET_CHARS = new Set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._/-");
199
+ const HEREDOC_DELIMITER_START_CHARS = new Set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_");
200
+ const HEREDOC_DELIMITER_CHARS = new Set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_");
201
+ function isShellHeaderWhitespace(char) {
202
+ return SHELL_HEADER_WHITESPACE.has(char);
203
+ }
204
+ function skipShellWhitespace(line, index) {
205
+ while (isShellHeaderWhitespace(line[index])) index++;
206
+ return index;
207
+ }
208
+ function parseStaticFileTarget(line, index) {
209
+ const start = index;
210
+ while (index < line.length && !isShellHeaderWhitespace(line[index])) {
211
+ if (!STATIC_FILE_TARGET_CHARS.has(line[index])) return null;
212
+ index++;
213
+ }
214
+ return index === start ? null : { target: line.slice(start, index), index };
215
+ }
216
+ function parseQuotedHereDocDelimiter(line, index) {
217
+ const quote = line[index];
218
+ if (quote !== "'" && quote !== '"') return null;
219
+ const start = ++index;
220
+ while (index < line.length && line[index] !== quote) {
221
+ const char = line[index];
222
+ if (!(index === start ? HEREDOC_DELIMITER_START_CHARS : HEREDOC_DELIMITER_CHARS).has(char)) return null;
223
+ index++;
224
+ }
225
+ return index === start || line[index] !== quote ? null : { delimiter: line.slice(start, index), index: index + 1 };
226
+ }
227
+ function parseInertCatHereDocHeader(line) {
228
+ let index = skipShellWhitespace(line, 0);
229
+ if (line.slice(index, index + 3) !== "cat" || !(isShellHeaderWhitespace(line[index + 3]) || line[index + 3] === ">")) return null;
230
+ index = skipShellWhitespace(line, index + 3);
231
+ let target, delimiter;
232
+ if (line[index] === ">") {
233
+ const parsedTarget = parseStaticFileTarget(line, skipShellWhitespace(line, index + 1));
234
+ if (!parsedTarget) return null;
235
+ target = parsedTarget.target;
236
+ index = skipShellWhitespace(line, parsedTarget.index);
237
+ if (line[index] !== "<" || line[index + 1] !== "<" || line[index + 2] === "-") return null;
238
+ const parsedDelimiter = parseQuotedHereDocDelimiter(line, skipShellWhitespace(line, index + 2));
239
+ if (!parsedDelimiter) return null;
240
+ delimiter = parsedDelimiter.delimiter;
241
+ index = parsedDelimiter.index;
242
+ } else if (line[index] === "<" && line[index + 1] === "<" && line[index + 2] !== "-") {
243
+ const parsedDelimiter = parseQuotedHereDocDelimiter(line, skipShellWhitespace(line, index + 2));
244
+ if (!parsedDelimiter) return null;
245
+ delimiter = parsedDelimiter.delimiter;
246
+ index = skipShellWhitespace(line, parsedDelimiter.index);
247
+ if (line[index] !== ">") return null;
248
+ const parsedTarget = parseStaticFileTarget(line, skipShellWhitespace(line, index + 1));
249
+ if (!parsedTarget) return null;
250
+ target = parsedTarget.target;
251
+ index = parsedTarget.index;
252
+ } else return null;
253
+ return skipShellWhitespace(line, index) === line.length ? { target, delimiter } : null;
254
+ }
255
+ function opaqueInertCatHereDoc(command, cwd, config, projectRoot) {
256
+ // A continuation changes the lexical boundary which identifies a heredoc
257
+ // header. This bounded parser cannot prove it is body-only, so fail closed.
258
+ if (command.includes("\\" + "\n") || command.includes("\\" + "\r\n")) return null;
259
+ const lines = command.split(/\r?\n/);
260
+ if (lines.at(-1) === "") lines.pop();
261
+ const parsed = parseInertCatHereDocHeader(lines[0] ?? "");
262
+ if (parsed && lines.length >= 2) {
263
+ const terminator = lines.findIndex((line, index) => index > 0 && line === parsed.delimiter);
264
+ if (terminator === lines.length - 1) {
265
+ const target = lexicalizePolicyPath(parsed.target, { base: cwd, projectRoot });
266
+ return evaluateFile("Write", target, config, projectRoot).allowed ? lines[0] : null;
267
+ }
268
+ }
269
+ return lines.some(hasUnquotedHereDocOperator) ? null : command;
270
+ }
180
271
  // The pre-redesign boolean helpers (parseEnvSplit/hasChmodRecursive/hasBase64Decode
181
272
  // and their isLongPrefix/splitEnvString internals) were replaced by the semantic
182
273
  // state machines below; only ENV_ESCAPES survives, shared with splitEnvSemantics.
@@ -703,6 +794,9 @@ function bashSubstitutions(command) {
703
794
  }
704
795
  function evaluateBash(command, cwd, config = AGENT_CONFIGS.claude, projectRoot = PROJECT_ROOT, depth = 0) {
705
796
  if (depth > 4) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
797
+ const headerOnlyCommand = opaqueInertCatHereDoc(command, cwd, config, projectRoot);
798
+ if (headerOnlyCommand === null) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
799
+ command = headerOnlyCommand;
706
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" }; }
707
801
  if (/^\s*:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:\s*$/.test(command)) return { allowed: false, ruleId: "shell.destructive-root" };
708
802
  let parsed;
@@ -4,14 +4,15 @@
4
4
  "name": "javi-forge-skillguard-pre-tool-use.mjs",
5
5
  "version": 1,
6
6
  "policyVersion": 2,
7
- "sha256": "59fc4224975ad64cfc85bab50ec60d9bd4948070e9d41f42ab43e6d8231c19a1",
7
+ "sha256": "6edfbb31ce0551b27e38ae1ffd1daf9cc4bea54f2c86687c5124132af5b8c0af",
8
8
  "historical": [
9
9
  "78be7e6613c012280b7ad17886462ba166b63ebd031e34565d757b3a0796d7cc",
10
10
  "5dc2a5c31131f4ac7d8657c78b950de52776aad6eaefe78ea0d764a9963c4425",
11
11
  "0c9aa8fa26b389f4892782f83104f0792c2b71ebca39c18c02706e2185e22b40",
12
12
  "3581862f0567cce75a58b693c9ade80d39ee7d58add11537a34a8461c47c1ed4",
13
13
  "54a270f28b068450b79547a88ec6f2d4854514392fd5f38ed1d6174ea093d7aa",
14
- "9a565cec31d9e091e3fb9420b86685f824733bc1ebe479f086b2b955aba6ef3e"
14
+ "9a565cec31d9e091e3fb9420b86685f824733bc1ebe479f086b2b955aba6ef3e",
15
+ "59fc4224975ad64cfc85bab50ec60d9bd4948070e9d41f42ab43e6d8231c19a1"
15
16
  ]
16
17
  },
17
18
  "settingsEntries": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javi-forge",
3
- "version": "1.38.2",
3
+ "version": "1.38.3",
4
4
  "description": "Project scaffolding and AI-ready CI bootstrap",
5
5
  "type": "module",
6
6
  "bin": {