javi-forge 1.38.2 → 1.38.4
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": "
|
|
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/dist/cli/help.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export declare const CI_HELP_TEXT = "\n Usage\n $ javi-forge ci [subcommand]
|
|
|
19
19
|
* Per-command help for `hooks`, shown by `javi-forge hooks --help` (or when
|
|
20
20
|
* `hooks` is given an unknown subcommand). Whitespace is significant.
|
|
21
21
|
*/
|
|
22
|
-
export declare const HOOKS_HELP_TEXT = "\n Usage\n $ javi-forge hooks run <pre-commit|pre-push>\n $ javi-forge hooks <install|doctor|repair> claude [--force]\n\n Run the sections enabled under hooks: in .javi-forge/ci.yaml, in a fixed\n cheap\u2192expensive order, fail-fast. With no hooks: config the default is the\n quick native CI gate (setup + lint + compile + gates \u2014 no tests, no coverage).\n\n Subcommands\n run pre-commit Run the composed pre-commit sections\n run pre-push Run the composed pre-push sections\n install claude Install the managed Claude PreToolUse guard (.claude/)\n doctor claude Report Claude PreToolUse guard health (informational)\n repair claude Repair the managed guard; --force overwrites edited assets\n\n Notes\n A blocking section failure exits non-zero and blocks the commit/push.\n A broken .javi-forge/ci.yaml exits 1 (fail-closed \u2014 never skips a gate).\n To skip: git commit --no-verify (pre-push: git push --no-verify)\n doctor claude is informational (always exits 0); install/repair exit 0 on\n success, non-zero on refusal/failure. Use repair claude --force to overwrite\n a locally edited managed asset.\n Linux: install/repair claude need the acl package (getfacl) to prove the\n parent chain \u2014 apt install acl / apk add acl / dnf install acl. Without it\n they refuse fail-closed; an already-installed guard keeps firing, and\n doctor claude reports the acl capability as its own row.\n Claude Code spawns the guard with node from ITS path, so node must resolve\n there, not only inside javi-forge.\n\n Examples\n $ javi-forge hooks run pre-commit\n $ javi-forge hooks run pre-push\n $ javi-forge hooks install claude\n $ javi-forge hooks doctor claude\n $ javi-forge hooks repair claude --force\n";
|
|
22
|
+
export declare const HOOKS_HELP_TEXT = "\n Usage\n $ javi-forge hooks run <pre-commit|pre-push>\n $ javi-forge hooks <install|doctor|repair> <claude|codex> [--force]\n\n Run the sections enabled under hooks: in .javi-forge/ci.yaml, in a fixed\n cheap\u2192expensive order, fail-fast. With no hooks: config the default is the\n quick native CI gate (setup + lint + compile + gates \u2014 no tests, no coverage).\n\n Subcommands\n run pre-commit Run the composed pre-commit sections\n run pre-push Run the composed pre-push sections\n install claude Install the managed Claude PreToolUse guard (.claude/)\n doctor claude Report Claude PreToolUse guard health (informational)\n repair claude Repair the managed guard; --force overwrites edited assets\n install codex Install the managed Codex PreToolUse guard (~/.codex/)\n doctor codex Report Codex hook execution readiness and trust boundary\n repair codex Repair the managed Codex guard; --force overwrites edits\n\n Notes\n A blocking section failure exits non-zero and blocks the commit/push.\n A broken .javi-forge/ci.yaml exits 1 (fail-closed \u2014 never skips a gate).\n To skip: git commit --no-verify (pre-push: git push --no-verify)\n doctor claude is informational (always exits 0); install/repair exit 0 on\n success, non-zero on refusal/failure. Use repair claude --force to overwrite\n a locally edited managed asset.\n doctor codex exits 0 when runnable, 1 when blocked, and 2 when inconclusive;\n it does not prove provider trust or runtime execution. Use repair codex\n --force only to overwrite an edited managed asset.\n Linux: install/repair claude need the acl package (getfacl) to prove the\n parent chain \u2014 apt install acl / apk add acl / dnf install acl. Without it\n they refuse fail-closed; an already-installed guard keeps firing, and\n doctor claude reports the acl capability as its own row.\n Claude Code spawns the guard with node from ITS path, so node must resolve\n there, not only inside javi-forge.\n\n Examples\n $ javi-forge hooks run pre-commit\n $ javi-forge hooks run pre-push\n $ javi-forge hooks install claude\n $ javi-forge hooks doctor claude\n $ javi-forge hooks repair claude --force\n $ javi-forge hooks install codex\n $ javi-forge hooks doctor codex\n $ javi-forge hooks repair codex --force\n";
|
|
23
23
|
export declare const FLAGS_SCHEMA: {
|
|
24
24
|
readonly help: {
|
|
25
25
|
readonly type: "boolean";
|
package/dist/cli/help.js
CHANGED
|
@@ -166,7 +166,7 @@ export const CI_HELP_TEXT = `
|
|
|
166
166
|
export const HOOKS_HELP_TEXT = `
|
|
167
167
|
Usage
|
|
168
168
|
$ javi-forge hooks run <pre-commit|pre-push>
|
|
169
|
-
$ javi-forge hooks <install|doctor|repair> claude [--force]
|
|
169
|
+
$ javi-forge hooks <install|doctor|repair> <claude|codex> [--force]
|
|
170
170
|
|
|
171
171
|
Run the sections enabled under hooks: in .javi-forge/ci.yaml, in a fixed
|
|
172
172
|
cheap→expensive order, fail-fast. With no hooks: config the default is the
|
|
@@ -178,6 +178,9 @@ export const HOOKS_HELP_TEXT = `
|
|
|
178
178
|
install claude Install the managed Claude PreToolUse guard (.claude/)
|
|
179
179
|
doctor claude Report Claude PreToolUse guard health (informational)
|
|
180
180
|
repair claude Repair the managed guard; --force overwrites edited assets
|
|
181
|
+
install codex Install the managed Codex PreToolUse guard (~/.codex/)
|
|
182
|
+
doctor codex Report Codex hook execution readiness and trust boundary
|
|
183
|
+
repair codex Repair the managed Codex guard; --force overwrites edits
|
|
181
184
|
|
|
182
185
|
Notes
|
|
183
186
|
A blocking section failure exits non-zero and blocks the commit/push.
|
|
@@ -186,6 +189,9 @@ export const HOOKS_HELP_TEXT = `
|
|
|
186
189
|
doctor claude is informational (always exits 0); install/repair exit 0 on
|
|
187
190
|
success, non-zero on refusal/failure. Use repair claude --force to overwrite
|
|
188
191
|
a locally edited managed asset.
|
|
192
|
+
doctor codex exits 0 when runnable, 1 when blocked, and 2 when inconclusive;
|
|
193
|
+
it does not prove provider trust or runtime execution. Use repair codex
|
|
194
|
+
--force only to overwrite an edited managed asset.
|
|
189
195
|
Linux: install/repair claude need the acl package (getfacl) to prove the
|
|
190
196
|
parent chain — apt install acl / apk add acl / dnf install acl. Without it
|
|
191
197
|
they refuse fail-closed; an already-installed guard keeps firing, and
|
|
@@ -199,6 +205,9 @@ export const HOOKS_HELP_TEXT = `
|
|
|
199
205
|
$ javi-forge hooks install claude
|
|
200
206
|
$ javi-forge hooks doctor claude
|
|
201
207
|
$ javi-forge hooks repair claude --force
|
|
208
|
+
$ javi-forge hooks install codex
|
|
209
|
+
$ javi-forge hooks doctor codex
|
|
210
|
+
$ javi-forge hooks repair codex --force
|
|
202
211
|
`;
|
|
203
212
|
export const FLAGS_SCHEMA = {
|
|
204
213
|
// `--help` is handled manually (autoHelp is disabled at the entrypoint so
|
|
@@ -254,9 +254,10 @@ function buildCodexHooksContainer(assetPath) {
|
|
|
254
254
|
};
|
|
255
255
|
}
|
|
256
256
|
/**
|
|
257
|
-
* Merge our managed group into an existing container:
|
|
258
|
-
*
|
|
259
|
-
*
|
|
257
|
+
* Merge our managed group into an existing container: remove only prior managed
|
|
258
|
+
* handlers (ours, by command regex), preserve foreign handlers in their groups,
|
|
259
|
+
* and append one fresh canonical managed group. A fresh install (no container)
|
|
260
|
+
* yields the clean container.
|
|
260
261
|
*/
|
|
261
262
|
function mergeCodexHooks(existing, assetPath) {
|
|
262
263
|
if (!isPlainObject(existing))
|
|
@@ -266,14 +267,24 @@ function mergeCodexHooks(existing, assetPath) {
|
|
|
266
267
|
container.hooks = {};
|
|
267
268
|
const hooks = container.hooks;
|
|
268
269
|
const groups = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
|
|
269
|
-
const kept =
|
|
270
|
-
|
|
271
|
-
|
|
270
|
+
const kept = [];
|
|
271
|
+
for (const group of groups) {
|
|
272
|
+
if (!isPlainObject(group) || !Array.isArray(group.hooks)) {
|
|
273
|
+
kept.push(group);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const foreign = group.hooks.filter((h) => !(isPlainObject(h) &&
|
|
272
277
|
h.type === "command" &&
|
|
273
278
|
typeof h.command === "string" &&
|
|
274
|
-
CODEX_CMD_RE.test(h.command));
|
|
275
|
-
|
|
276
|
-
|
|
279
|
+
CODEX_CMD_RE.test(h.command)));
|
|
280
|
+
if (foreign.length === 0)
|
|
281
|
+
continue;
|
|
282
|
+
if (foreign.length === group.hooks.length) {
|
|
283
|
+
kept.push(group);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
kept.push({ ...group, hooks: foreign });
|
|
287
|
+
}
|
|
277
288
|
const fresh = buildCodexHooksContainer(assetPath).hooks;
|
|
278
289
|
hooks.PreToolUse = [...kept, ...fresh.PreToolUse];
|
|
279
290
|
return container;
|
|
@@ -372,7 +383,9 @@ export async function doctorCodexPreToolUse(homeDir, options = {}) {
|
|
|
372
383
|
? "inconclusive"
|
|
373
384
|
: "runnable";
|
|
374
385
|
const remediation = [];
|
|
375
|
-
if (hooksJson.state === "absent" ||
|
|
386
|
+
if (hooksJson.state === "absent" ||
|
|
387
|
+
hooksJson.state === "released-outdated" ||
|
|
388
|
+
asset.state !== "managed-current") {
|
|
376
389
|
remediation.push("install the codex guard with: javi-forge hooks install codex");
|
|
377
390
|
}
|
|
378
391
|
if (!trusted)
|