javi-forge 1.35.1 → 1.37.0
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 +5 -0
- package/assets/claude-hooks/javi-forge-skillguard-pre-tool-use.mjs +97 -29
- package/assets/claude-hooks/manifest.json +1 -1
- package/ci-local/README.md +11 -6
- package/ci-local/ci-local.ps1 +34 -0
- package/ci-local/ci-local.sh +19 -1
- package/ci-local/install.ps1 +23 -0
- package/ci-local/install.sh +18 -1
- package/dist/cli/dispatch/hooks.d.ts +9 -2
- package/dist/cli/dispatch/hooks.js +22 -8
- package/dist/cli/dispatch/simple-renderers.js +7 -0
- package/dist/commands/claude-hooks.js +2 -0
- package/dist/commands/codex-hooks.d.ts +26 -0
- package/dist/commands/codex-hooks.js +106 -0
- package/dist/commands/init.d.ts +9 -1
- package/dist/commands/init.js +10 -6
- package/dist/lib/__fixtures__/claude-hook-ownership.d.ts +6 -0
- package/dist/lib/__fixtures__/claude-hook-ownership.js +7 -1
- package/dist/lib/agent-adapter.d.ts +56 -0
- package/dist/lib/agent-adapter.js +88 -0
- package/dist/lib/claude-hook-manager.d.ts +17 -1
- package/dist/lib/claude-hook-manager.js +20 -4
- package/dist/lib/claude-hook-settings.d.ts +3 -3
- package/dist/lib/claude-hook-settings.js +3 -3
- package/dist/lib/codex-hook-manager.d.ts +178 -0
- package/dist/lib/codex-hook-manager.js +546 -0
- package/dist/lib/platform-support.d.ts +19 -0
- package/dist/lib/platform-support.js +22 -0
- package/dist/lib/secure-fs-transaction.d.ts +25 -5
- package/dist/lib/secure-fs-transaction.js +44 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -372,6 +372,11 @@ npx javi-forge doctor
|
|
|
372
372
|
- **Project Manifest** — Reads `.javi-forge/manifest.json`
|
|
373
373
|
- **Installed Modules** — engram, obsidian-brain, memory-simple, ghagga
|
|
374
374
|
|
|
375
|
+
|
|
376
|
+
## macOS CI-Local support
|
|
377
|
+
|
|
378
|
+
macOS is deprecated and unsupported for new CI-Local install/startup. Pin a supported release or migrate your workflow. Existing installed guards are not removed in 1.x; Darwin code removal is planned separately for 2.0.
|
|
379
|
+
|
|
375
380
|
## Requirements
|
|
376
381
|
|
|
377
382
|
- **Node.js** >= 22 (required by ink 7; previous versions ran on >= 18)
|
|
@@ -10,6 +10,22 @@ export const INPUT_LIMIT_BYTES = 1_048_576;
|
|
|
10
10
|
export const SUPPORTED_TOOLS = Object.freeze(["Bash", "PowerShell", "Read", "Write", "Edit"]);
|
|
11
11
|
export const POLICY_REGISTRY = Object.freeze({ schemaVersion: 1, policyVersion: 1, diagnosticsMaxBytes: 240 });
|
|
12
12
|
const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
13
|
+
// Per-agent adapter config (S0 core-extraction): every agent-specific input the guard needs
|
|
14
|
+
// (the isManaged protected-path set, the project-dir source, the managed marker) is resolved by
|
|
15
|
+
// the --agent selector instead of a baked-in literal. The pure evaluate*/utility engine stays
|
|
16
|
+
// agent-independent. Claude's entry holds today's EXACT values so its observable behavior is
|
|
17
|
+
// byte-identical; codex is defined so the map is agent-generic but wired end-to-end only in a later slice.
|
|
18
|
+
const CLAUDE_MANAGED_SET = Object.freeze({ exact: Object.freeze([".claude/settings.json", ".claude/settings.local.json", ".claude/CLAUDE.md", "CLAUDE.md", ".javi-forge/ci.yaml"]), prefixes: Object.freeze([".claude/hooks/", ".claude/agents/", ".claude/skills/"]), caseFoldExact: Object.freeze(["claude.md", ".claude/claude.md"]) });
|
|
19
|
+
const CODEX_MANAGED_SET = Object.freeze({ exact: Object.freeze([".codex/hooks.json", ".claude/settings.json", ".claude/settings.local.json", ".claude/CLAUDE.md", "CLAUDE.md", ".javi-forge/ci.yaml"]), prefixes: Object.freeze([".claude/hooks/", ".claude/agents/", ".claude/skills/"]), caseFoldExact: Object.freeze(["claude.md", ".claude/claude.md"]) });
|
|
20
|
+
export const AGENT_CONFIGS = Object.freeze({ claude: Object.freeze({ id: "claude", managedSet: CLAUDE_MANAGED_SET, projectDir: Object.freeze({ envVar: "CLAUDE_PROJECT_DIR", fallback: "asset-root" }), marker: MANAGED_MARKER }), codex: Object.freeze({ id: "codex", managedSet: CODEX_MANAGED_SET, projectDir: Object.freeze({ envVar: null, fallback: "cwd" }), marker: "// javi-forge-managed: codex-pretooluse v1" }) });
|
|
21
|
+
// Fail-closed agent selector: a missing/unknown --agent means we cannot know what to protect, so refuse.
|
|
22
|
+
function resolveAgentConfig(argv) { const arg = argv.find((value) => typeof value === "string" && value.startsWith("--agent=")); const id = arg === undefined ? undefined : arg.slice("--agent=".length); const config = id === undefined ? undefined : AGENT_CONFIGS[id]; if (!config) fail("invalid-config"); return config; }
|
|
23
|
+
// Project root per agent: the env var when set (Claude = CLAUDE_PROJECT_DIR); otherwise the per-agent
|
|
24
|
+
// fallback decides. "asset-root" anchors to the asset-relative PROJECT_ROOT so Claude is byte-identical
|
|
25
|
+
// to the pre-extraction guard whether the env var is set OR unset (closing the cwd-divergence gap);
|
|
26
|
+
// "cwd" uses the envelope cwd (codex, whose user-global asset has no asset-relative project root and no
|
|
27
|
+
// env var). Fail-safe: an unknown/missing fallback anchors to the stricter PROJECT_ROOT, never looser.
|
|
28
|
+
function resolveProjectRoot(config, cwd) { const envVar = config.projectDir.envVar; if (envVar) { const value = process.env[envVar]; if (typeof value === "string" && value.length > 0) return value; } if (config.projectDir.fallback === "cwd") return cwd ?? PROJECT_ROOT; return PROJECT_ROOT; }
|
|
13
29
|
const POSIX_ABSOLUTE = /^\//;
|
|
14
30
|
const WINDOWS_DRIVE = /^[a-zA-Z]:[\\/]/;
|
|
15
31
|
const WINDOWS_UNC = /^\\\\[^\\]+\\[^\\]+/;
|
|
@@ -59,9 +75,8 @@ function nativeRealpath(input) {
|
|
|
59
75
|
fail("path-resolution-failed");
|
|
60
76
|
}
|
|
61
77
|
}
|
|
62
|
-
export function
|
|
78
|
+
export function lexicalizePolicyPath(input, options = {}) {
|
|
63
79
|
if (typeof input !== "string" || input.includes("\0")) fail("invalid-event");
|
|
64
|
-
const platform = options.platform ?? process.platform;
|
|
65
80
|
let expanded = input;
|
|
66
81
|
if (options.base) {
|
|
67
82
|
expanded = expanded.replace(/^\$\{CLAUDE_PROJECT_DIR\}|^\$CLAUDE_PROJECT_DIR/, options.projectRoot ?? PROJECT_ROOT);
|
|
@@ -70,6 +85,11 @@ export function canonicalizePolicyPath(input, options = {}) {
|
|
|
70
85
|
expanded = path.resolve(options.base, expanded);
|
|
71
86
|
}
|
|
72
87
|
}
|
|
88
|
+
return expanded;
|
|
89
|
+
}
|
|
90
|
+
export function canonicalizePolicyPath(input, options = {}) {
|
|
91
|
+
const platform = options.platform ?? process.platform;
|
|
92
|
+
let expanded = lexicalizePolicyPath(input, options);
|
|
73
93
|
// Strip Windows device aliases (\??\, \\?\, \\.\) BEFORE native realpath:
|
|
74
94
|
// on a real win32 host nativeRealpath resolves an unstripped \??\ against the
|
|
75
95
|
// current drive (D:\??\C:\...), so the alias must be canonicalized first or a
|
|
@@ -114,21 +134,21 @@ export function isSensitivePolicyKey(key, platform = process.platform) {
|
|
|
114
134
|
if (SENSITIVE_DIRECTORY_SUFFIXES.some((directory) => key.endsWith(directory) || key.includes(`${directory}/`))) return true;
|
|
115
135
|
return platform === "win32" || platform === "darwin" ? basename.toLowerCase() === "serviceaccountkey.json" : basename === "serviceAccountKey.json";
|
|
116
136
|
}
|
|
117
|
-
function isManaged(key) {
|
|
118
|
-
const project = canonicalizePolicyPath(
|
|
137
|
+
function isManaged(key, managedSet = CLAUDE_MANAGED_SET, projectRoot = PROJECT_ROOT) {
|
|
138
|
+
const project = canonicalizePolicyPath(projectRoot);
|
|
119
139
|
if (!key.startsWith(`${project}/`) && key !== project) return false;
|
|
120
140
|
const relative = key.slice(project.length + 1);
|
|
121
141
|
// On case-insensitive platforms lexicalNormalize folds the key to lowercase,
|
|
122
142
|
// so the mixed-case CLAUDE.md literals must be matched case-insensitively too
|
|
123
143
|
// (the other literals are already lowercase). Otherwise CLAUDE.md and
|
|
124
144
|
// .claude/CLAUDE.md lose managed-config protection on macOS/Windows.
|
|
125
|
-
const foldedClaudeMd = (process.platform === "win32" || process.platform === "darwin") && (relative
|
|
126
|
-
return foldedClaudeMd ||
|
|
145
|
+
const foldedClaudeMd = (process.platform === "win32" || process.platform === "darwin") && managedSet.caseFoldExact.includes(relative);
|
|
146
|
+
return foldedClaudeMd || managedSet.exact.includes(relative) || managedSet.prefixes.some((prefix) => relative.startsWith(prefix));
|
|
127
147
|
}
|
|
128
|
-
function evaluateFile(toolName, filePath) {
|
|
148
|
+
function evaluateFile(toolName, filePath, config = AGENT_CONFIGS.claude, projectRoot = PROJECT_ROOT) {
|
|
129
149
|
const keys = policyPathKeys(filePath);
|
|
130
150
|
if (keys.some((key) => isSensitivePolicyKey(key))) return { allowed: false, ruleId: "path.sensitive" };
|
|
131
|
-
if (toolName !== "Read" && keys.some(isManaged)) return { allowed: false, ruleId: "path.managed-config" };
|
|
151
|
+
if (toolName !== "Read" && keys.some((key) => isManaged(key, config.managedSet, projectRoot))) return { allowed: false, ruleId: "path.managed-config" };
|
|
132
152
|
return { allowed: true };
|
|
133
153
|
}
|
|
134
154
|
function lex(command, powershell = false) {
|
|
@@ -628,21 +648,21 @@ function reduceWrappers(input, powershell = false) {
|
|
|
628
648
|
}
|
|
629
649
|
return { tokens };
|
|
630
650
|
}
|
|
631
|
-
function hasSensitiveLiteral(tokens, cwd) {
|
|
651
|
+
function hasSensitiveLiteral(tokens, cwd, projectRoot = PROJECT_ROOT) {
|
|
632
652
|
return tokens.some((token) => {
|
|
633
653
|
if ((token.startsWith("-") && !/^-(?:LiteralPath|Path):/i.test(token)) || !/[\\/.~$]/.test(token)) return false;
|
|
634
654
|
try {
|
|
635
|
-
return isSensitivePolicyKey(canonicalizePolicyPath(token.replace(/^(?:-LiteralPath:|-Path:)/i, "").replace(/[;,]$/, ""), { base: cwd, projectRoot
|
|
655
|
+
return isSensitivePolicyKey(canonicalizePolicyPath(token.replace(/^(?:-LiteralPath:|-Path:)/i, "").replace(/[;,]$/, ""), { base: cwd, projectRoot }));
|
|
636
656
|
} catch {
|
|
637
657
|
return false;
|
|
638
658
|
}
|
|
639
659
|
});
|
|
640
660
|
}
|
|
641
|
-
function hasManagedLiteral(tokens, cwd) {
|
|
661
|
+
function hasManagedLiteral(tokens, cwd, config = AGENT_CONFIGS.claude, projectRoot = PROJECT_ROOT) {
|
|
642
662
|
return tokens.some((token) => {
|
|
643
663
|
if ((token.startsWith("-") && !/^-(?:LiteralPath|Path):/i.test(token)) || !/[\\/.]/.test(token)) return false;
|
|
644
664
|
try {
|
|
645
|
-
return isManaged(canonicalizePolicyPath(token.replace(/^(?:-LiteralPath:|-Path:)/i, "").replace(/[;,]$/, ""), { base: cwd, projectRoot
|
|
665
|
+
return isManaged(canonicalizePolicyPath(token.replace(/^(?:-LiteralPath:|-Path:)/i, "").replace(/[;,]$/, ""), { base: cwd, projectRoot }), config.managedSet, projectRoot);
|
|
646
666
|
} catch {
|
|
647
667
|
return false;
|
|
648
668
|
}
|
|
@@ -681,9 +701,9 @@ function bashSubstitutions(command) {
|
|
|
681
701
|
}
|
|
682
702
|
return bodies;
|
|
683
703
|
}
|
|
684
|
-
function evaluateBash(command, cwd, depth = 0) {
|
|
704
|
+
function evaluateBash(command, cwd, config = AGENT_CONFIGS.claude, projectRoot = PROJECT_ROOT, depth = 0) {
|
|
685
705
|
if (depth > 4) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
|
|
686
|
-
try { for (const body of bashSubstitutions(command)) { const nested = evaluateBash(body, cwd, depth + 1); if (!nested.allowed) return nested; } } catch { return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; }
|
|
706
|
+
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" }; }
|
|
687
707
|
if (/^\s*:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:\s*$/.test(command)) return { allowed: false, ruleId: "shell.destructive-root" };
|
|
688
708
|
let parsed;
|
|
689
709
|
try { parsed = lex(command); } catch { return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; }
|
|
@@ -712,20 +732,20 @@ function evaluateBash(command, cwd, depth = 0) {
|
|
|
712
732
|
if (decision) return decision;
|
|
713
733
|
} else if (/^(?:curl|wget)$/.test(executable) && shellSink) return { allowed: false, ruleId: "shell.pipe-to-shell" };
|
|
714
734
|
}
|
|
715
|
-
if (["cat", "less", "more", "head", "tail", "bat", "grep", "rg", "sed", "awk", "source", ".", "cp", "install"].includes(executable) && hasSensitiveLiteral(tokens.slice(1), cwd)) return { allowed: false, ruleId: "shell.sensitive-read" };
|
|
716
|
-
if (tokens.some((token) => token === "<") && hasSensitiveLiteral(tokens, cwd)) return { allowed: false, ruleId: "shell.sensitive-read" };
|
|
735
|
+
if (["cat", "less", "more", "head", "tail", "bat", "grep", "rg", "sed", "awk", "source", ".", "cp", "install"].includes(executable) && hasSensitiveLiteral(tokens.slice(1), cwd, projectRoot)) return { allowed: false, ruleId: "shell.sensitive-read" };
|
|
736
|
+
if (tokens.some((token) => token === "<") && hasSensitiveLiteral(tokens, cwd, projectRoot)) return { allowed: false, ruleId: "shell.sensitive-read" };
|
|
717
737
|
if (executable === "git" && tokens[1]?.toLowerCase() === "push" && tokens.some((token) => ["-f", "--force", "--force-with-lease"].includes(token.toLowerCase()))) return { allowed: false, ruleId: "shell.force-push" };
|
|
718
|
-
if (["rm", "mv", "cp", "install", "truncate", "touch", "chmod", "chown", "tee"].includes(executable) && hasManagedLiteral(tokens.slice(1), cwd)) return { allowed: false, ruleId: "shell.managed-config-tamper" };
|
|
719
|
-
if ((/^(?:sed|perl)$/.test(executable) && tokens.some((token) => token.startsWith("-i")) && hasManagedLiteral(tokens, cwd)) || (tokens.includes(">") && hasManagedLiteral(tokens, cwd))) return { allowed: false, ruleId: "shell.managed-config-tamper" };
|
|
738
|
+
if (["rm", "mv", "cp", "install", "truncate", "touch", "chmod", "chown", "tee"].includes(executable) && hasManagedLiteral(tokens.slice(1), cwd, config, projectRoot)) return { allowed: false, ruleId: "shell.managed-config-tamper" };
|
|
739
|
+
if ((/^(?:sed|perl)$/.test(executable) && tokens.some((token) => token.startsWith("-i")) && hasManagedLiteral(tokens, cwd, config, projectRoot)) || (tokens.includes(">") && hasManagedLiteral(tokens, cwd, config, projectRoot))) return { allowed: false, ruleId: "shell.managed-config-tamper" };
|
|
720
740
|
if (/^(?:powershell|pwsh)(?:\.exe)?$/i.test(executable) && tokens.some((token) => /^-(?:enc|encodedcommand)$/i.test(token))) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
|
|
721
741
|
if (/^(?:bash|sh|zsh|dash|ksh)$/.test(executable)) {
|
|
722
742
|
const flag = tokens.findIndex((token) => /^-[^-]*c[^-]*$/.test(token));
|
|
723
|
-
if (flag >= 0) { const body = tokens[flag + 1]; if (!body || /\$(?!\()/.test(body)) return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; const nested = evaluateBash(body, cwd, depth + 1); if (!nested.allowed) return nested; }
|
|
743
|
+
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; }
|
|
724
744
|
}
|
|
725
745
|
}
|
|
726
746
|
return { allowed: true };
|
|
727
747
|
}
|
|
728
|
-
function evaluatePowerShell(command, cwd) {
|
|
748
|
+
function evaluatePowerShell(command, cwd, config = AGENT_CONFIGS.claude, projectRoot = PROJECT_ROOT) {
|
|
729
749
|
const parsed = lex(command, true);
|
|
730
750
|
for (let index = 0; index < parsed.commands.length; index++) {
|
|
731
751
|
const reduced = reduceWrappers(parsed.commands[index], true);
|
|
@@ -734,23 +754,69 @@ function evaluatePowerShell(command, cwd) {
|
|
|
734
754
|
const executable = (tokens[0] ?? "").toLowerCase();
|
|
735
755
|
if ((["remove-item", "rm", "del", "erase", "rmdir", "rd"].includes(executable) && tokens.some((token) => /^-(?:r|recurse)$/i.test(token)) && tokens.some((token) => /^-(?:fo|force)$/i.test(token)) && tokens.some((token) => /^(?:[a-z]:\\?|[/~]|\$HOME)$/i.test(token))) || ["format-volume", "clear-disk", "initialize-disk"].includes(executable)) return { allowed: false, ruleId: "powershell.destructive-root" };
|
|
736
756
|
if (parsed.separators[index] === "|" && /^(?:invoke-webrequest|iwr|curl|wget|invoke-restmethod|irm)$/.test(executable) && /^(?:invoke-expression|iex)$/.test(((reduceWrappers(parsed.commands[index + 1] ?? [], true).tokens ?? [])[0] ?? "").toLowerCase())) return { allowed: false, ruleId: "powershell.pipe-to-shell" };
|
|
737
|
-
if (["get-content", "gc", "cat", "type", "select-string", "copy-item", "cp", "copy"].includes(executable) && hasSensitiveLiteral(tokens.slice(1), cwd)) return { allowed: false, ruleId: "powershell.sensitive-read" };
|
|
757
|
+
if (["get-content", "gc", "cat", "type", "select-string", "copy-item", "cp", "copy"].includes(executable) && hasSensitiveLiteral(tokens.slice(1), cwd, projectRoot)) return { allowed: false, ruleId: "powershell.sensitive-read" };
|
|
738
758
|
if (executable === "git" && tokens[1]?.toLowerCase() === "push" && tokens.some((token) => ["-f", "--force", "--force-with-lease"].includes(token.toLowerCase()))) return { allowed: false, ruleId: "powershell.force-push" };
|
|
739
|
-
if (["set-content", "add-content", "out-file", "clear-content", "remove-item", "move-item", "copy-item", "rename-item", "new-item"].includes(executable) && hasManagedLiteral(tokens.slice(1), cwd)) return { allowed: false, ruleId: "powershell.managed-config-tamper" };
|
|
740
|
-
if (tokens.includes(">") && hasManagedLiteral(tokens, cwd)) return { allowed: false, ruleId: "powershell.managed-config-tamper" };
|
|
759
|
+
if (["set-content", "add-content", "out-file", "clear-content", "remove-item", "move-item", "copy-item", "rename-item", "new-item"].includes(executable) && hasManagedLiteral(tokens.slice(1), cwd, config, projectRoot)) return { allowed: false, ruleId: "powershell.managed-config-tamper" };
|
|
760
|
+
if (tokens.includes(">") && hasManagedLiteral(tokens, cwd, config, projectRoot)) return { allowed: false, ruleId: "powershell.managed-config-tamper" };
|
|
741
761
|
if (/^(?:powershell|pwsh)(?:\.exe)?$/i.test(executable) && tokens.some((token) => /^-(?:enc|encodedcommand)$/i.test(token))) return { allowed: false, ruleId: "powershell.obfuscated-interpreter" };
|
|
742
762
|
}
|
|
743
763
|
return { allowed: true };
|
|
744
764
|
}
|
|
745
|
-
|
|
746
|
-
|
|
765
|
+
// Codex file-write shim (S1, security-critical): Codex delivers file writes as
|
|
766
|
+
// tool_name:"apply_patch" with the target path(s) inside the tool_input.command patch
|
|
767
|
+
// text and NO file_path field, so evaluateFile's managed-config protection cannot fire
|
|
768
|
+
// without first parsing the header paths out of the patch. This is a WRITE-class surface:
|
|
769
|
+
// a parse miss = a managed-config file-write bypass, so every unresolved shape fails
|
|
770
|
+
// closed (throw -> main() catch -> denyAndExit, exit 2). Grammar VERIFIED against a REAL
|
|
771
|
+
// captured codex-cli 0.147.0 envelope (2026-08-18): first line is `*** Begin Patch`,
|
|
772
|
+
// closed by `*** End Patch`, with `*** Add File:` / `*** Update File:` / `*** Delete File:`
|
|
773
|
+
// headers AND `*** Move to:` for a rename target -- the last one CONFIRMED emitted (a
|
|
774
|
+
// rename can retarget a benign source onto a managed path, so its destination is checked too).
|
|
775
|
+
export function parseApplyPatchPaths(command) {
|
|
776
|
+
if (typeof command !== "string" || command.includes("\0")) fail("invalid-event");
|
|
777
|
+
const lines = command.split(/\r?\n/);
|
|
778
|
+
if ((lines[0] ?? "").trim() !== "*** Begin Patch") fail("invalid-event"); // header-less patch -> cannot prove safe
|
|
779
|
+
let seenEnd = false;
|
|
780
|
+
const paths = [];
|
|
781
|
+
for (let index = 1; index < lines.length; index++) {
|
|
782
|
+
const line = lines[index];
|
|
783
|
+
if (line.trim() === "*** End Patch") { seenEnd = true; break; }
|
|
784
|
+
const file = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/.exec(line);
|
|
785
|
+
if (file) { paths.push(file[1].trim()); continue; }
|
|
786
|
+
const move = /^\*\*\* Move to: (.+)$/.exec(line); // rename target (verified present)
|
|
787
|
+
if (move) { paths.push(move[1].trim()); continue; }
|
|
788
|
+
// body lines (+/-/space/@@/index) carry no header path and are ignored
|
|
789
|
+
}
|
|
790
|
+
if (!seenEnd) fail("invalid-event"); // truncated patch could hide a managed write
|
|
791
|
+
if (paths.length === 0) fail("invalid-event"); // zero extractable paths -> fail closed
|
|
792
|
+
for (const target of paths) if (target.length === 0 || target.includes("\0")) fail("invalid-event");
|
|
793
|
+
return paths;
|
|
794
|
+
}
|
|
795
|
+
export function evaluateEvent(input, config = AGENT_CONFIGS.claude) {
|
|
796
|
+
if (!isObject(input) || input.hook_event_name !== "PreToolUse" || !isObject(input.tool_input)) fail("invalid-event");
|
|
797
|
+
const applyPatch = input.tool_name === "apply_patch";
|
|
798
|
+
if (!applyPatch && !SUPPORTED_TOOLS.includes(input.tool_name)) fail("invalid-event");
|
|
799
|
+
const cwd = typeof input.cwd === "string" && isAbsolutePolicyPath(input.cwd) ? input.cwd : PROJECT_ROOT;
|
|
800
|
+
const projectRoot = resolveProjectRoot(config, cwd);
|
|
801
|
+
if (applyPatch) {
|
|
802
|
+
if (typeof input.tool_input.command !== "string") fail("invalid-event");
|
|
803
|
+
// apply_patch is a WRITE tool (not "Read"), so the managed-config rule fires. Each header
|
|
804
|
+
// path is made absolute LEXICALLY (no realpath) vs the envelope cwd and handed to evaluateFile
|
|
805
|
+
// exactly like Write/Edit's raw file_path, so policyPathKeys owns the dual lexical+realpath
|
|
806
|
+
// canonicalization. Realpath-ing here first would collapse a symlinked managed dir onto its
|
|
807
|
+
// target and drop the protective lexical key -> managed-config write bypass.
|
|
808
|
+
for (const target of parseApplyPatchPaths(input.tool_input.command)) {
|
|
809
|
+
const decision = evaluateFile("apply_patch", lexicalizePolicyPath(target, { base: cwd, projectRoot }), config, projectRoot);
|
|
810
|
+
if (!decision.allowed) return decision;
|
|
811
|
+
}
|
|
812
|
+
return { allowed: true };
|
|
813
|
+
}
|
|
747
814
|
if (input.tool_name === "Bash" || input.tool_name === "PowerShell") {
|
|
748
815
|
if (typeof input.tool_input.command !== "string") fail("invalid-event");
|
|
749
|
-
|
|
750
|
-
return input.tool_name === "Bash" ? evaluateBash(input.tool_input.command, cwd) : evaluatePowerShell(input.tool_input.command, cwd);
|
|
816
|
+
return input.tool_name === "Bash" ? evaluateBash(input.tool_input.command, cwd, config, projectRoot) : evaluatePowerShell(input.tool_input.command, cwd, config, projectRoot);
|
|
751
817
|
}
|
|
752
818
|
if (typeof input.tool_input.file_path !== "string" || !isAbsolutePolicyPath(input.tool_input.file_path)) fail("invalid-event");
|
|
753
|
-
return evaluateFile(input.tool_name, input.tool_input.file_path);
|
|
819
|
+
return evaluateFile(input.tool_name, input.tool_input.file_path, config, projectRoot);
|
|
754
820
|
}
|
|
755
821
|
export function parseAndEvaluateInput(input) {
|
|
756
822
|
if (!Buffer.isBuffer(input) || input.length === 0) fail("invalid-json");
|
|
@@ -769,6 +835,7 @@ function diagnostic(error) {
|
|
|
769
835
|
const messages = {
|
|
770
836
|
"invalid-json": "input is not a valid JSON object",
|
|
771
837
|
"invalid-event": "input does not match the supported event schema",
|
|
838
|
+
"invalid-config": "missing or unknown --agent selector (expected --agent=<id>)",
|
|
772
839
|
"oversized-input": "stdin exceeds 1048576 bytes",
|
|
773
840
|
"missing-policy": "embedded policy registry is unavailable",
|
|
774
841
|
"internal-error": "policy evaluation could not complete",
|
|
@@ -830,6 +897,7 @@ export function readBoundedStdin(stream = process.stdin) {
|
|
|
830
897
|
}
|
|
831
898
|
export async function main() {
|
|
832
899
|
try {
|
|
900
|
+
const config = resolveAgentConfig(process.argv);
|
|
833
901
|
const fault = process.argv.find((arg) => arg.startsWith("--javi-forge-test-fault="))?.split("=")[1];
|
|
834
902
|
if (fault === "missing-policy") throw new Error("missing-policy");
|
|
835
903
|
if (POLICY_REGISTRY.schemaVersion !== 1 || POLICY_REGISTRY.policyVersion !== 1) throw new Error("missing-policy");
|
|
@@ -841,7 +909,7 @@ export async function main() {
|
|
|
841
909
|
throw new Error("invalid-json");
|
|
842
910
|
}
|
|
843
911
|
if (fault === "evaluator-throw") throw new Error("internal-error");
|
|
844
|
-
const decision = evaluateEvent(parsed);
|
|
912
|
+
const decision = evaluateEvent(parsed, config);
|
|
845
913
|
if (!decision.allowed) denyAndExit(denialDiagnostic(parsed.tool_name, decision));
|
|
846
914
|
process.exitCode = 0;
|
|
847
915
|
} catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"schemaVersion":1,"asset":{"name":"javi-forge-skillguard-pre-tool-use.mjs","version":1,"policyVersion":1,"sha256":"
|
|
1
|
+
{"schemaVersion":1,"asset":{"name":"javi-forge-skillguard-pre-tool-use.mjs","version":1,"policyVersion":1,"sha256":"9a565cec31d9e091e3fb9420b86685f824733bc1ebe479f086b2b955aba6ef3e","historical":["78be7e6613c012280b7ad17886462ba166b63ebd031e34565d757b3a0796d7cc","5dc2a5c31131f4ac7d8657c78b950de52776aad6eaefe78ea0d764a9963c4425","0c9aa8fa26b389f4892782f83104f0792c2b71ebca39c18c02706e2185e22b40","3581862f0567cce75a58b693c9ade80d39ee7d58add11537a34a8461c47c1ed4","54a270f28b068450b79547a88ec6f2d4854514392fd5f38ed1d6174ea093d7aa"]},"settingsEntries":{"current":{"version":1,"canonicalSha256":"b1341803cd076091edfcb473494514df1a23bd33372374ca1de6b75258e52e06"},"historical":[{"version":1,"canonicalSha256":"038c59a91bf8967f6908afed74c465f1e7030254e11e4f8738975d6d708424d4"}]},"installerHelpers":{"windowsSecureObject":{"name":"javi-forge-windows-secure-object.ps1","sha256":"2289ef6ac6b039ec74dc3ea0894413e243ff9bea963f04008a356b3838f9b8dd"}}}
|
package/ci-local/README.md
CHANGED
|
@@ -34,21 +34,26 @@ cp -r lib /path/to/new-project/
|
|
|
34
34
|
|
|
35
35
|
# En el nuevo proyecto
|
|
36
36
|
cd /path/to/new-project
|
|
37
|
-
./.ci-local/install.sh # Linux/
|
|
37
|
+
./.ci-local/install.sh # Linux/WSL
|
|
38
38
|
# o
|
|
39
39
|
.\.ci-local\install.ps1 # Windows (PowerShell 7+)
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
> **Importante:** CI-Local depende de `lib/common.sh` (Linux/
|
|
42
|
+
> **Importante:** CI-Local depende de `lib/common.sh` (Linux/WSL) y/o `lib/common.psm1` (Windows). Copiá ambos junto con el directorio `ci-local/`. macOS está deprecated y se rechaza para instalaciones o arranques nuevos.
|
|
43
43
|
|
|
44
44
|
El installer (ambas variantes) falla con mensaje claro si `javi-forge` no está en el PATH.
|
|
45
45
|
|
|
46
|
+
|
|
47
|
+
## macOS deprecation
|
|
48
|
+
|
|
49
|
+
macOS is deprecated and unsupported for new CI-Local install/startup. Pin a supported release or migrate your workflow. Existing installed guards are not removed in 1.x; Darwin code removal is planned separately for 2.0.
|
|
50
|
+
|
|
46
51
|
### Soporte cross-platform
|
|
47
52
|
|
|
48
53
|
| Plataforma | Installer | Runner | Hooks | Requisitos |
|
|
49
54
|
|---|---|---|---|---|
|
|
50
55
|
| Linux | `install.sh` | `ci-local.sh` | bash | bash, perl, docker (opcional) |
|
|
51
|
-
| macOS |
|
|
56
|
+
| macOS | Unsupported: pin a supported release or migrate | Refused before startup | Existing installed guards retained | No new install or startup |
|
|
52
57
|
| WSL | `install.sh` | `ci-local.sh` | bash | bash, perl, docker (opcional) |
|
|
53
58
|
| Windows nativo | `install.ps1` | `ci-local.ps1` | bash | PowerShell 7+, Git for Windows (MSYS2 bash), docker (opcional) |
|
|
54
59
|
|
|
@@ -117,7 +122,7 @@ Si tu modelo de amenaza requiere defensa contra adversarios motivados, usá
|
|
|
117
122
|
.\.ci-local\ci-local.ps1 shell # Shell en entorno CI
|
|
118
123
|
.\.ci-local\ci-local.ps1 detect # Ver stack detectado
|
|
119
124
|
|
|
120
|
-
# Linux/
|
|
125
|
+
# Linux/WSL
|
|
121
126
|
./.ci-local/ci-local.sh quick
|
|
122
127
|
./.ci-local/ci-local.sh full
|
|
123
128
|
./.ci-local/ci-local.sh shell
|
|
@@ -164,9 +169,9 @@ Developer workflow:
|
|
|
164
169
|
```
|
|
165
170
|
.ci-local/
|
|
166
171
|
├── ci-local.ps1 # Script principal (Windows)
|
|
167
|
-
├── ci-local.sh # Script principal (Linux/
|
|
172
|
+
├── ci-local.sh # Script principal (Linux/WSL)
|
|
168
173
|
├── install.ps1 # Instalador Windows
|
|
169
|
-
├── install.sh # Instalador Linux/
|
|
174
|
+
├── install.sh # Instalador Linux/WSL
|
|
170
175
|
├── semgrep.yml # Reglas de seguridad
|
|
171
176
|
├── README.md # Esta guía
|
|
172
177
|
├── hooks/
|
package/ci-local/ci-local.ps1
CHANGED
|
@@ -21,6 +21,30 @@ param(
|
|
|
21
21
|
[string]$Mode = 'full'
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
+
function Invoke-CiLocalMain {
|
|
25
|
+
param(
|
|
26
|
+
[Parameter(Mandatory)][string]$Platform,
|
|
27
|
+
[ref]$ExitCode
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if ($Platform -eq 'Darwin') {
|
|
31
|
+
Write-Host 'macOS is deprecated and unsupported for new CI-Local install/startup. Pin a supported release or migrate. Existing installed guards are not removed; Darwin code removal is planned separately for 2.0.'
|
|
32
|
+
if ($PSBoundParameters.ContainsKey('ExitCode')) {
|
|
33
|
+
$ExitCode.Value = 1
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
return 1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Invoke-CiLocalStartupBody -Platform $Platform
|
|
40
|
+
if ($PSBoundParameters.ContainsKey('ExitCode')) {
|
|
41
|
+
$ExitCode.Value = 0
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Invoke-CiLocalStartupBody {
|
|
46
|
+
param([string]$Platform)
|
|
47
|
+
|
|
24
48
|
# See install.ps1 for the rationale behind 7.2 minimum (ResolveLinkTarget).
|
|
25
49
|
if ($PSVersionTable.PSEdition -ne 'Core' -or $PSVersionTable.PSVersion -lt [Version]'7.2') {
|
|
26
50
|
Write-Host 'ERROR: ci-local.ps1 requires PowerShell 7.2+ (pwsh).' -ForegroundColor Red
|
|
@@ -441,3 +465,13 @@ Write-Host ''
|
|
|
441
465
|
Write-Host 'CI Local completed successfully!' -ForegroundColor Green
|
|
442
466
|
Write-Host ' Safe to push - CI should pass.' -ForegroundColor Green
|
|
443
467
|
Write-Host ''
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if ($MyInvocation.InvocationName -ne '.') {
|
|
471
|
+
$platform = if ($IsMacOS) { 'Darwin' } else { 'Windows' }
|
|
472
|
+
$exitCode = 0
|
|
473
|
+
& ${function:Invoke-CiLocalMain} -Platform $platform -ExitCode ([ref]$exitCode)
|
|
474
|
+
if ($exitCode -ne 0) {
|
|
475
|
+
exit $exitCode
|
|
476
|
+
}
|
|
477
|
+
}
|
package/ci-local/ci-local.sh
CHANGED
|
@@ -11,7 +11,20 @@
|
|
|
11
11
|
# ./ci-local.sh detect # Mostrar stack detectado
|
|
12
12
|
# =============================================================================
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
function ci_local_main() {
|
|
15
|
+
local platform="${1:?platform required}"
|
|
16
|
+
shift
|
|
17
|
+
|
|
18
|
+
if [ "$platform" = "Darwin" ]; then
|
|
19
|
+
printf '%s\n' 'macOS is deprecated and unsupported for new CI-Local install/startup. Pin a supported release or migrate. Existing installed guards are not removed; Darwin code removal is planned separately for 2.0.'
|
|
20
|
+
return 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
ci_local_startup_body "$@"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ci_local_startup_body() {
|
|
27
|
+
set -e
|
|
15
28
|
|
|
16
29
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
17
30
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
@@ -390,3 +403,8 @@ esac
|
|
|
390
403
|
|
|
391
404
|
echo -e "\n${GREEN}CI Local completed successfully!${NC}"
|
|
392
405
|
echo -e "${GREEN} Safe to push - CI should pass.${NC}\n"
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
409
|
+
ci_local_main "$(/usr/bin/uname -s)" "$@"
|
|
410
|
+
fi
|
package/ci-local/install.ps1
CHANGED
|
@@ -14,6 +14,20 @@
|
|
|
14
14
|
[CmdletBinding()]
|
|
15
15
|
param()
|
|
16
16
|
|
|
17
|
+
function Invoke-CiLocalMain {
|
|
18
|
+
param([Parameter(Mandatory)][string]$Platform)
|
|
19
|
+
|
|
20
|
+
if ($Platform -eq 'Darwin') {
|
|
21
|
+
Write-Host 'macOS is deprecated and unsupported for new CI-Local install/startup. Pin a supported release or migrate. Existing installed guards are not removed; Darwin code removal is planned separately for 2.0.'
|
|
22
|
+
return 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Invoke-CiLocalStartupBody -Platform $Platform
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Invoke-CiLocalStartupBody {
|
|
29
|
+
param([string]$Platform)
|
|
30
|
+
|
|
17
31
|
# Defense-in-depth: #Requires is parsed by some hosts AFTER the script body.
|
|
18
32
|
# 7.2 is the minimum because FileSystemInfo.ResolveLinkTarget($true) is .NET 6+,
|
|
19
33
|
# which shipped with pwsh 7.2. Earlier pwsh 7.x runs on .NET 5 and the method
|
|
@@ -365,3 +379,12 @@ try {
|
|
|
365
379
|
} finally {
|
|
366
380
|
Pop-Location
|
|
367
381
|
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if ($MyInvocation.InvocationName -ne '.') {
|
|
385
|
+
$platform = if ($IsMacOS) { 'Darwin' } else { 'Windows' }
|
|
386
|
+
$exitCode = & ${function:Invoke-CiLocalMain} -Platform $platform
|
|
387
|
+
if ($exitCode -is [int] -and $exitCode -ne 0) {
|
|
388
|
+
exit $exitCode
|
|
389
|
+
}
|
|
390
|
+
}
|
package/ci-local/install.sh
CHANGED
|
@@ -3,7 +3,19 @@
|
|
|
3
3
|
# CI-LOCAL: Installation Script
|
|
4
4
|
# =============================================================================
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
function ci_local_main() {
|
|
7
|
+
local platform="${1:?platform required}"
|
|
8
|
+
|
|
9
|
+
if [ "$platform" = "Darwin" ]; then
|
|
10
|
+
printf '%s\n' 'macOS is deprecated and unsupported for new CI-Local install/startup. Pin a supported release or migrate. Existing installed guards are not removed; Darwin code removal is planned separately for 2.0.'
|
|
11
|
+
return 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
ci_local_startup_body "$platform"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ci_local_startup_body() {
|
|
18
|
+
set -e
|
|
7
19
|
|
|
8
20
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
21
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
@@ -193,3 +205,8 @@ echo -e " - pre-commit: AI check + lint + security"
|
|
|
193
205
|
echo -e " - commit-msg: Block AI attribution"
|
|
194
206
|
echo -e " - pre-push: CI simulation in Docker"
|
|
195
207
|
echo -e ""
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
211
|
+
ci_local_main "$(/usr/bin/uname -s)"
|
|
212
|
+
fi
|
|
@@ -6,9 +6,16 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Subcommands:
|
|
8
8
|
* - `hooks run <pre-commit|pre-push>` → runHook, exits with its code.
|
|
9
|
-
* - `hooks <install|doctor|repair> claude [--force]` →
|
|
10
|
-
* exits with its code (
|
|
9
|
+
* - `hooks <install|doctor|repair> <claude|codex> [--force]` → the matching
|
|
10
|
+
* agent command, exits with its code (unknown/missing agent → usage + exit 1).
|
|
11
11
|
* Any other subcommand or a missing name → usage + exit 1.
|
|
12
|
+
*
|
|
13
|
+
* The valid agent target set is decided by `isAgentId` (backed by the
|
|
14
|
+
* `AGENT_ADAPTERS` registry — the single source of agent truth). Each id maps to
|
|
15
|
+
* a lazy loader for its console command runner; the map is typed
|
|
16
|
+
* `Record<AgentId, …>`, so a new registry agent cannot be silently dropped
|
|
17
|
+
* (omitting its loader is a compile error). Lazy imports keep cold-start minimal
|
|
18
|
+
* on the commit/push hot path.
|
|
12
19
|
*/
|
|
13
20
|
import type { CLI } from "./types.js";
|
|
14
21
|
export declare function handleHooks(cli: CLI): Promise<void>;
|
|
@@ -6,11 +6,24 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Subcommands:
|
|
8
8
|
* - `hooks run <pre-commit|pre-push>` → runHook, exits with its code.
|
|
9
|
-
* - `hooks <install|doctor|repair> claude [--force]` →
|
|
10
|
-
* exits with its code (
|
|
9
|
+
* - `hooks <install|doctor|repair> <claude|codex> [--force]` → the matching
|
|
10
|
+
* agent command, exits with its code (unknown/missing agent → usage + exit 1).
|
|
11
11
|
* Any other subcommand or a missing name → usage + exit 1.
|
|
12
|
+
*
|
|
13
|
+
* The valid agent target set is decided by `isAgentId` (backed by the
|
|
14
|
+
* `AGENT_ADAPTERS` registry — the single source of agent truth). Each id maps to
|
|
15
|
+
* a lazy loader for its console command runner; the map is typed
|
|
16
|
+
* `Record<AgentId, …>`, so a new registry agent cannot be silently dropped
|
|
17
|
+
* (omitting its loader is a compile error). Lazy imports keep cold-start minimal
|
|
18
|
+
* on the commit/push hot path.
|
|
12
19
|
*/
|
|
20
|
+
import { isAgentId } from "../../lib/agent-adapter.js";
|
|
13
21
|
import { HOOKS_HELP_TEXT } from "../help.js";
|
|
22
|
+
/** Agent registry: id → lazy loader for its console command runner. */
|
|
23
|
+
const AGENT_COMMAND_LOADERS = {
|
|
24
|
+
claude: async () => (await import("../../commands/claude-hooks.js")).runClaudeHookCommand,
|
|
25
|
+
codex: async () => (await import("../../commands/codex-hooks.js")).runCodexHookCommand,
|
|
26
|
+
};
|
|
14
27
|
export async function handleHooks(cli) {
|
|
15
28
|
if (cli.flags.help === true) {
|
|
16
29
|
console.log(HOOKS_HELP_TEXT);
|
|
@@ -28,14 +41,15 @@ export async function handleHooks(cli) {
|
|
|
28
41
|
}
|
|
29
42
|
const sub = cli.input[1];
|
|
30
43
|
if (sub === "install" || sub === "doctor" || sub === "repair") {
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
const agent = cli.input[2];
|
|
45
|
+
// Validity is decided by the agent registry (isAgentId), the single source
|
|
46
|
+
// of truth; the loader map is exhaustive over AgentId by type.
|
|
47
|
+
if (!isAgentId(agent)) {
|
|
48
|
+
console.error(`Usage: javi-forge hooks ${sub} <claude|codex>`);
|
|
33
49
|
process.exit(1);
|
|
34
50
|
}
|
|
35
|
-
const
|
|
36
|
-
process.exit(await
|
|
37
|
-
force: cli.flags.force === true,
|
|
38
|
-
}));
|
|
51
|
+
const run = await AGENT_COMMAND_LOADERS[agent]();
|
|
52
|
+
process.exit(await run(sub, process.cwd(), { force: cli.flags.force === true }));
|
|
39
53
|
}
|
|
40
54
|
// No subcommand → show usage (exit 0). An unknown subcommand is a typo →
|
|
41
55
|
// show usage but exit 1 rather than run nothing silently.
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { render } from "ink";
|
|
11
11
|
import React from "react";
|
|
12
|
+
import { resolvePlatformSupport } from "../../lib/platform-support.js";
|
|
12
13
|
import AnalyzeUI from "../../ui/AnalyzeUI.js";
|
|
13
14
|
import App from "../../ui/App.js";
|
|
14
15
|
import { CIProvider as CIContextProvider } from "../../ui/CIContext.js";
|
|
@@ -49,6 +50,12 @@ export function handlePlugin(cli, ctx) {
|
|
|
49
50
|
React.createElement(Plugin, { action: action, target: target, dryRun: cli.flags.dryRun, codex: cli.flags.codex, force: cli.flags.force })), { stdin: ctx.inkStdin });
|
|
50
51
|
}
|
|
51
52
|
export function handleInitDefault(cli, ctx) {
|
|
53
|
+
const platformSupport = resolvePlatformSupport(process.platform);
|
|
54
|
+
if (platformSupport) {
|
|
55
|
+
console.error(`${platformSupport.refusalCode}: ${platformSupport.guidance}`);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
52
59
|
const presetStack = VALID_STACKS.includes(cli.flags.stack)
|
|
53
60
|
? cli.flags.stack
|
|
54
61
|
: undefined;
|
|
@@ -59,6 +59,8 @@ function renderMutation(verb, result, log, logError) {
|
|
|
59
59
|
}
|
|
60
60
|
function renderDoctor(report, log) {
|
|
61
61
|
log(`doctor claude: ${report.healthy ? "healthy" : "unhealthy"}`);
|
|
62
|
+
if (report.platformSupport)
|
|
63
|
+
log(` platform-support: ${report.platformSupport.state} — ${report.platformSupport.guidance}`);
|
|
62
64
|
log(` settings: ${report.settings.state} — ${report.settings.detail}`);
|
|
63
65
|
log(` asset: ${report.asset.state} — ${report.asset.detail}`);
|
|
64
66
|
log(` node: ${report.node.version ?? "unavailable"} (min-satisfied: ${report.node.satisfiesMinimum})`);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `javi-forge hooks <install|doctor|repair> codex` — console-only renderer for
|
|
3
|
+
* the Codex PreToolUse guard library (agent-agnostic slice 2). It wires the
|
|
4
|
+
* install/doctor/repair lib fns to human output + exit codes; it adds NO new
|
|
5
|
+
* security logic and never touches `runTransaction`/secure-fs directly.
|
|
6
|
+
*
|
|
7
|
+
* Doctor exit code follows the effective-execution verdict (runnable → 0,
|
|
8
|
+
* blocked → 1, inconclusive → 2), independent of any component health — the same
|
|
9
|
+
* honest-execution contract as the Claude renderer. The UNTRUSTED state is a
|
|
10
|
+
* `blocked` verdict (an untrusted Codex hook is silently skipped), so a fresh
|
|
11
|
+
* install correctly reports blocked until the user grants trust.
|
|
12
|
+
*/
|
|
13
|
+
import { doctorCodexPreToolUse, installCodexPreToolUse, repairCodexPreToolUse } from "../lib/codex-hook-manager.js";
|
|
14
|
+
export type CodexHookSub = "install" | "doctor" | "repair";
|
|
15
|
+
export interface CodexHookCmdDeps {
|
|
16
|
+
install?: typeof installCodexPreToolUse;
|
|
17
|
+
doctor?: typeof doctorCodexPreToolUse;
|
|
18
|
+
repair?: typeof repairCodexPreToolUse;
|
|
19
|
+
homeDir?: string;
|
|
20
|
+
log?: (msg: string) => void;
|
|
21
|
+
logError?: (msg: string) => void;
|
|
22
|
+
}
|
|
23
|
+
export declare function runCodexHookCommand(sub: CodexHookSub, _cwd: string, opts: {
|
|
24
|
+
force?: boolean;
|
|
25
|
+
}, deps?: CodexHookCmdDeps): Promise<number>;
|
|
26
|
+
//# sourceMappingURL=codex-hooks.d.ts.map
|