javi-forge 1.40.0 → 1.41.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/assets/claude-hooks/javi-forge-skillguard-pre-tool-use.mjs +26 -14
- package/assets/claude-hooks/manifest.json +5 -2
- package/assets/grok-hooks/manifest.json +9 -0
- package/dist/cli/dispatch/hooks.js +2 -1
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +12 -3
- package/dist/commands/grok-hooks.d.ts +15 -0
- package/dist/commands/grok-hooks.js +45 -0
- package/dist/lib/agent-adapter.d.ts +2 -1
- package/dist/lib/agent-adapter.js +32 -0
- package/dist/lib/grok-hook-manager.d.ts +88 -0
- package/dist/lib/grok-hook-manager.js +327 -0
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
export const MANAGED_MARKER = "// javi-forge-managed: claude-pretooluse v1";
|
|
9
9
|
export const INPUT_LIMIT_BYTES = 1_048_576;
|
|
10
10
|
export const SUPPORTED_TOOLS = Object.freeze(["Bash", "PowerShell", "Read", "Write", "Edit"]);
|
|
11
|
+
export function mapGrokToolName(toolName, platform = process.platform) { return toolName === "run_terminal_command" ? (platform === "win32" ? "PowerShell" : "Bash") : toolName === "read_file" ? "Read" : toolName === "search_replace" ? "Edit" : toolName; }
|
|
11
12
|
export const POLICY_REGISTRY = Object.freeze({ schemaVersion: 1, policyVersion: 2, diagnosticsMaxBytes: 240 });
|
|
12
13
|
export function resolvePlatformSupport(platform = process.platform) { return platform === "linux" || platform === "win32" ? { supported: true } : { supported: false, reason: "unsupported-platform" }; }
|
|
13
14
|
const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
@@ -15,6 +16,7 @@ const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)),
|
|
|
15
16
|
// never to an event field or a command token. This is intentionally an exact,
|
|
16
17
|
// two-file allowlist rather than a whole-home or whole-.codex policy.
|
|
17
18
|
const HOST_HOME = os.homedir();
|
|
19
|
+
const GROK_HOME_ROOT = process.env.GROK_HOME ? path.resolve(process.env.GROK_HOME) : path.join(HOST_HOME, ".grok");
|
|
18
20
|
// Per-agent adapter config (S0 core-extraction): every agent-specific input the guard needs
|
|
19
21
|
// (the isManaged protected-path set, the project-dir source, the managed marker) is resolved by
|
|
20
22
|
// the --agent selector instead of a baked-in literal. The pure evaluate*/utility engine stays
|
|
@@ -23,8 +25,10 @@ const HOST_HOME = os.homedir();
|
|
|
23
25
|
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"]) });
|
|
24
26
|
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"]) });
|
|
25
27
|
const OPENCODE_MANAGED_SET = Object.freeze({ exact: Object.freeze(["opencode.json", "opencode.jsonc", ".opencode/opencode.json", ".opencode/opencode.jsonc", ".opencode/AGENTS.md", "AGENTS.md", ".javi-forge/ci.yaml"]), prefixes: Object.freeze([".opencode/plugins/", ".opencode/agents/", ".opencode/skills/", ".agents/skills/", ".claude/hooks/", ".claude/agents/", ".claude/skills/"]), caseFoldExact: Object.freeze(["agents.md", ".opencode/agents.md", "claude.md", ".claude/claude.md"]) });
|
|
28
|
+
const GROK_MANAGED_SET = Object.freeze({ exact: Object.freeze([".grok/config.toml", "AGENTS.md", ".claude/settings.json", ".claude/settings.local.json", ".claude/CLAUDE.md", "CLAUDE.md", ".javi-forge/ci.yaml"]), prefixes: Object.freeze([".grok/hooks/", ".claude/hooks/", ".claude/agents/", ".claude/skills/"]), caseFoldExact: Object.freeze(["agents.md", "claude.md", ".claude/claude.md"]) });
|
|
26
29
|
const CODEX_GLOBAL_MANAGED_PATHS = Object.freeze([".codex/hooks.json", ".codex/config.toml"]);
|
|
27
|
-
|
|
30
|
+
const GROK_GLOBAL_MANAGED_PATHS = Object.freeze([path.join(GROK_HOME_ROOT, "hooks", "javi-forge-skillguard-pre-tool-use.json"), path.join(GROK_HOME_ROOT, "hooks", "javi-forge-skillguard-pre-tool-use.mjs")]);
|
|
31
|
+
export const AGENT_CONFIGS = Object.freeze({ claude: Object.freeze({ id: "claude", managedSet: CLAUDE_MANAGED_SET, globalManagedPaths: Object.freeze([]), projectDir: Object.freeze({ envVar: "CLAUDE_PROJECT_DIR", fallback: "asset-root" }), marker: MANAGED_MARKER }), codex: Object.freeze({ id: "codex", managedSet: CODEX_MANAGED_SET, globalManagedPaths: CODEX_GLOBAL_MANAGED_PATHS, projectDir: Object.freeze({ envVar: null, fallback: "cwd" }), marker: "// javi-forge-managed: codex-pretooluse v1" }), opencode: Object.freeze({ id: "opencode", managedSet: OPENCODE_MANAGED_SET, globalManagedPaths: Object.freeze([]), projectDir: Object.freeze({ envVar: null, fallback: "cwd" }), marker: "// javi-forge-managed: opencode-skillguard v1" }), grok: Object.freeze({ id: "grok", managedSet: GROK_MANAGED_SET, globalManagedPaths: GROK_GLOBAL_MANAGED_PATHS, projectDir: Object.freeze({ envVar: null, fallback: "cwd" }), marker: "javi-forge-managed: grok-pretooluse v1" }) });
|
|
28
32
|
// Fail-closed agent selector: a missing/unknown --agent means we cannot know what to protect, so refuse.
|
|
29
33
|
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; }
|
|
30
34
|
// Project root per agent: the env var when set (Claude = CLAUDE_PROJECT_DIR); otherwise the per-agent
|
|
@@ -87,7 +91,7 @@ export function lexicalizePolicyPath(input, options = {}) {
|
|
|
87
91
|
let expanded = input;
|
|
88
92
|
if (options.base) {
|
|
89
93
|
expanded = expanded.replace(/^\$\{CLAUDE_PROJECT_DIR\}|^\$CLAUDE_PROJECT_DIR/, options.projectRoot ?? PROJECT_ROOT);
|
|
90
|
-
expanded = expanded.replace(/^~(?=[\\/]|$)
|
|
94
|
+
expanded = expanded.replace(/^~(?=[\\/]|$)|^(?:\$HOME|\$\{HOME\})(?=[\\/]|$)/, os.homedir());
|
|
91
95
|
if (!POSIX_ABSOLUTE.test(expanded) && !WINDOWS_DRIVE.test(expanded) && !WINDOWS_UNC.test(expanded)) {
|
|
92
96
|
expanded = path.resolve(options.base, expanded);
|
|
93
97
|
}
|
|
@@ -142,7 +146,7 @@ export function isSensitivePolicyKey(key, platform = process.platform) {
|
|
|
142
146
|
return platform === "win32" ? basename.toLowerCase() === "serviceaccountkey.json" : basename === "serviceAccountKey.json";
|
|
143
147
|
}
|
|
144
148
|
function isManaged(key, managedSet = CLAUDE_MANAGED_SET, projectRoot = PROJECT_ROOT, globalManagedPaths = []) {
|
|
145
|
-
if (globalManagedPaths.some((
|
|
149
|
+
if (globalManagedPaths.some((targetPath) => key === canonicalizePolicyPath(path.isAbsolute(targetPath) ? targetPath : path.join(HOST_HOME, targetPath)))) return true;
|
|
146
150
|
const project = canonicalizePolicyPath(projectRoot);
|
|
147
151
|
if (!key.startsWith(`${project}/`) && key !== project) return false;
|
|
148
152
|
const relative = key.slice(project.length + 1);
|
|
@@ -952,30 +956,36 @@ export function parseApplyPatchPaths(command) {
|
|
|
952
956
|
return paths;
|
|
953
957
|
}
|
|
954
958
|
export function evaluateEvent(input, config = AGENT_CONFIGS.claude) {
|
|
955
|
-
if (!isObject(input) || input.hook_event_name !== "PreToolUse"
|
|
956
|
-
const
|
|
957
|
-
|
|
959
|
+
if (!isObject(input) || input.hook_event_name !== "PreToolUse") fail("invalid-event");
|
|
960
|
+
const rawToolName = typeof input.tool_name === "string" ? input.tool_name : input.toolName;
|
|
961
|
+
const toolName = config.id === "grok" && typeof rawToolName === "string" ? mapGrokToolName(rawToolName) : rawToolName;
|
|
962
|
+
const toolInput = isObject(input.tool_input) ? input.tool_input : input.toolInput;
|
|
963
|
+
if (typeof toolName !== "string" || !isObject(toolInput)) fail("invalid-event");
|
|
964
|
+
if (input.toolInputTruncated === true || input.tool_input_truncated === true) return { allowed: false, ruleId: "tool-input-truncated" };
|
|
965
|
+
const applyPatch = toolName === "apply_patch";
|
|
966
|
+
if (!applyPatch && !SUPPORTED_TOOLS.includes(toolName)) fail("invalid-event");
|
|
958
967
|
const cwd = typeof input.cwd === "string" && isAbsolutePolicyPath(input.cwd) ? input.cwd : PROJECT_ROOT;
|
|
959
968
|
const projectRoot = resolveProjectRoot(config, cwd);
|
|
960
969
|
if (applyPatch) {
|
|
961
|
-
if (typeof
|
|
970
|
+
if (typeof toolInput.command !== "string") fail("invalid-event");
|
|
962
971
|
// apply_patch is a WRITE tool (not "Read"), so the managed-config rule fires. Each header
|
|
963
972
|
// path is made absolute LEXICALLY (no realpath) vs the envelope cwd and handed to evaluateFile
|
|
964
973
|
// exactly like Write/Edit's raw file_path, so policyPathKeys owns the dual lexical+realpath
|
|
965
974
|
// canonicalization. Realpath-ing here first would collapse a symlinked managed dir onto its
|
|
966
975
|
// target and drop the protective lexical key -> managed-config write bypass.
|
|
967
|
-
for (const target of parseApplyPatchPaths(
|
|
976
|
+
for (const target of parseApplyPatchPaths(toolInput.command)) {
|
|
968
977
|
const decision = evaluateFile("apply_patch", lexicalizePolicyPath(target, { base: cwd, projectRoot }), config, projectRoot);
|
|
969
978
|
if (!decision.allowed) return decision;
|
|
970
979
|
}
|
|
971
980
|
return { allowed: true };
|
|
972
981
|
}
|
|
973
|
-
if (
|
|
974
|
-
if (typeof
|
|
975
|
-
return
|
|
982
|
+
if (toolName === "Bash" || toolName === "PowerShell") {
|
|
983
|
+
if (typeof toolInput.command !== "string") fail("invalid-event");
|
|
984
|
+
return toolName === "Bash" ? evaluateBash(toolInput.command, cwd, config, projectRoot) : evaluatePowerShell(toolInput.command, cwd, config, projectRoot);
|
|
976
985
|
}
|
|
977
|
-
|
|
978
|
-
|
|
986
|
+
const filePath = typeof toolInput.file_path === "string" ? toolInput.file_path : typeof toolInput.filePath === "string" ? toolInput.filePath : config.id === "grok" && typeof toolInput.path === "string" ? lexicalizePolicyPath(toolInput.path, { base: cwd, projectRoot }) : undefined;
|
|
987
|
+
if (typeof filePath !== "string" || !isAbsolutePolicyPath(filePath)) fail("invalid-event");
|
|
988
|
+
return evaluateFile(toolName, filePath, config, projectRoot);
|
|
979
989
|
}
|
|
980
990
|
export function parseAndEvaluateInput(input) {
|
|
981
991
|
if (!Buffer.isBuffer(input) || input.length === 0) fail("invalid-json");
|
|
@@ -1072,7 +1082,9 @@ export async function main() {
|
|
|
1072
1082
|
}
|
|
1073
1083
|
if (fault === "evaluator-throw") throw new Error("internal-error");
|
|
1074
1084
|
const decision = evaluateEvent(parsed, config);
|
|
1075
|
-
|
|
1085
|
+
const rawToolName = typeof parsed.tool_name === "string" ? parsed.tool_name : parsed.toolName;
|
|
1086
|
+
const toolName = config.id === "grok" && typeof rawToolName === "string" ? mapGrokToolName(rawToolName) : rawToolName;
|
|
1087
|
+
if (!decision.allowed) denyAndExit(denialDiagnostic(toolName, decision));
|
|
1076
1088
|
process.exitCode = 0;
|
|
1077
1089
|
} catch (error) {
|
|
1078
1090
|
denyAndExit(diagnostic(error));
|
|
@@ -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": "044f6816d18a2894532fde52a145f10347f617cbb361fc10075d04f885327d22",
|
|
8
8
|
"historical": [
|
|
9
9
|
"78be7e6613c012280b7ad17886462ba166b63ebd031e34565d757b3a0796d7cc",
|
|
10
10
|
"5dc2a5c31131f4ac7d8657c78b950de52776aad6eaefe78ea0d764a9963c4425",
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
"59fc4224975ad64cfc85bab50ec60d9bd4948070e9d41f42ab43e6d8231c19a1",
|
|
16
16
|
"6edfbb31ce0551b27e38ae1ffd1daf9cc4bea54f2c86687c5124132af5b8c0af",
|
|
17
17
|
"507a57a15f1b967103bb1eefe701a74813d5a9603fa7bcc15579a215b03621e3",
|
|
18
|
-
"f55ece1aea76251ec005f2b731fa44a58b6245fef8150b58486ae062167a2377"
|
|
18
|
+
"f55ece1aea76251ec005f2b731fa44a58b6245fef8150b58486ae062167a2377",
|
|
19
|
+
"6fe1ef55363e6e3f38253ca94b17ec50c942352c00d8431cd2ff2f6858c094b6",
|
|
20
|
+
"b79ee8dc3df525810b07215927590366ed2669255666fdc817e9637cf0e10cf2",
|
|
21
|
+
"a352b5b6c29fcdeecf2a014ab0e271cab9373119460134d65134f16a19222e47"
|
|
19
22
|
]
|
|
20
23
|
},
|
|
21
24
|
"settingsEntries": {
|
|
@@ -24,6 +24,7 @@ const AGENT_COMMAND_LOADERS = {
|
|
|
24
24
|
claude: async () => (await import("../../commands/claude-hooks.js")).runClaudeHookCommand,
|
|
25
25
|
codex: async () => (await import("../../commands/codex-hooks.js")).runCodexHookCommand,
|
|
26
26
|
opencode: async () => (await import("../../commands/opencode-hooks.js")).runOpenCodeHookCommand,
|
|
27
|
+
grok: async () => (await import("../../commands/grok-hooks.js")).runGrokHookCommand,
|
|
27
28
|
};
|
|
28
29
|
export async function handleHooks(cli) {
|
|
29
30
|
if (cli.flags.help === true) {
|
|
@@ -46,7 +47,7 @@ export async function handleHooks(cli) {
|
|
|
46
47
|
// Validity is decided by the agent registry (isAgentId), the single source
|
|
47
48
|
// of truth; the loader map is exhaustive over AgentId by type.
|
|
48
49
|
if (!isAgentId(agent)) {
|
|
49
|
-
console.error(`Usage: javi-forge hooks ${sub} <claude|codex|opencode>`);
|
|
50
|
+
console.error(`Usage: javi-forge hooks ${sub} <claude|codex|opencode|grok>`);
|
|
50
51
|
process.exit(1);
|
|
51
52
|
}
|
|
52
53
|
const run = await AGENT_COMMAND_LOADERS[agent]();
|
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|codex|opencode> [--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 install opencode Install the managed OpenCode global plugin (~/.config/opencode/plugins/)\n doctor opencode Report OpenCode plugin and policy file currency (informational)\n repair opencode Repair the OpenCode plugin pair; --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 doctor opencode is informational and inspects installed files only; it does\n not prove OpenCode discovered, loaded, or executed the plugin.\n Linux: install/repair claude need the acl package (getfacl) to
|
|
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|opencode|grok> [--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 install opencode Install the managed OpenCode global plugin (~/.config/opencode/plugins/)\n doctor opencode Report OpenCode plugin and policy file currency (informational)\n repair opencode Repair the OpenCode plugin pair; --force overwrites edits\n install grok Install the Grok Build global PreToolUse hook (~/.grok/hooks/)\n doctor grok Report Grok hook registration and policy file currency (informational)\n repair grok Repair the Grok hook pair; --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 doctor opencode is informational and inspects installed files only; it does\n not prove OpenCode discovered, loaded, or executed the plugin.\n doctor grok is informational and inspects the global hook registration and\n adjacent policy runtime only; it does not prove Grok loaded or executed them.\n Linux: install/repair claude and grok need the acl package (getfacl) to\n prove the parent chain \u2014 apt install acl / apk add acl / dnf install acl.\n 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 $ javi-forge hooks install opencode\n $ javi-forge hooks doctor opencode\n $ javi-forge hooks repair opencode --force\n $ javi-forge hooks install grok\n $ javi-forge hooks doctor grok\n $ javi-forge hooks repair grok --force\n";
|
|
23
23
|
export declare const FLAGS_SCHEMA: {
|
|
24
24
|
readonly help: {
|
|
25
25
|
readonly type: "boolean";
|
package/dist/cli/help.js
CHANGED
|
@@ -167,7 +167,7 @@ export const CI_HELP_TEXT = `
|
|
|
167
167
|
export const HOOKS_HELP_TEXT = `
|
|
168
168
|
Usage
|
|
169
169
|
$ javi-forge hooks run <pre-commit|pre-push>
|
|
170
|
-
$ javi-forge hooks <install|doctor|repair> <claude|codex|opencode> [--force]
|
|
170
|
+
$ javi-forge hooks <install|doctor|repair> <claude|codex|opencode|grok> [--force]
|
|
171
171
|
|
|
172
172
|
Run the sections enabled under hooks: in .javi-forge/ci.yaml, in a fixed
|
|
173
173
|
cheap→expensive order, fail-fast. With no hooks: config the default is the
|
|
@@ -185,6 +185,9 @@ export const HOOKS_HELP_TEXT = `
|
|
|
185
185
|
install opencode Install the managed OpenCode global plugin (~/.config/opencode/plugins/)
|
|
186
186
|
doctor opencode Report OpenCode plugin and policy file currency (informational)
|
|
187
187
|
repair opencode Repair the OpenCode plugin pair; --force overwrites edits
|
|
188
|
+
install grok Install the Grok Build global PreToolUse hook (~/.grok/hooks/)
|
|
189
|
+
doctor grok Report Grok hook registration and policy file currency (informational)
|
|
190
|
+
repair grok Repair the Grok hook pair; --force overwrites edits
|
|
188
191
|
|
|
189
192
|
Notes
|
|
190
193
|
A blocking section failure exits non-zero and blocks the commit/push.
|
|
@@ -198,8 +201,11 @@ export const HOOKS_HELP_TEXT = `
|
|
|
198
201
|
--force only to overwrite an edited managed asset.
|
|
199
202
|
doctor opencode is informational and inspects installed files only; it does
|
|
200
203
|
not prove OpenCode discovered, loaded, or executed the plugin.
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
doctor grok is informational and inspects the global hook registration and
|
|
205
|
+
adjacent policy runtime only; it does not prove Grok loaded or executed them.
|
|
206
|
+
Linux: install/repair claude and grok need the acl package (getfacl) to
|
|
207
|
+
prove the parent chain — apt install acl / apk add acl / dnf install acl.
|
|
208
|
+
Without it
|
|
203
209
|
they refuse fail-closed; an already-installed guard keeps firing, and
|
|
204
210
|
doctor claude reports the acl capability as its own row.
|
|
205
211
|
Claude Code spawns the guard with node from ITS path, so node must resolve
|
|
@@ -217,6 +223,9 @@ export const HOOKS_HELP_TEXT = `
|
|
|
217
223
|
$ javi-forge hooks install opencode
|
|
218
224
|
$ javi-forge hooks doctor opencode
|
|
219
225
|
$ javi-forge hooks repair opencode --force
|
|
226
|
+
$ javi-forge hooks install grok
|
|
227
|
+
$ javi-forge hooks doctor grok
|
|
228
|
+
$ javi-forge hooks repair grok --force
|
|
220
229
|
`;
|
|
221
230
|
export const FLAGS_SCHEMA = {
|
|
222
231
|
// `--help` is handled manually (autoHelp is disabled at the entrypoint so
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Console renderer for the Grok Build global SkillGuard hook manager. */
|
|
2
|
+
import { doctorGrokSkillGuard, installGrokSkillGuard, repairGrokSkillGuard } from "../lib/grok-hook-manager.js";
|
|
3
|
+
export type GrokHookSub = "install" | "doctor" | "repair";
|
|
4
|
+
export interface GrokHookCmdDeps {
|
|
5
|
+
install?: typeof installGrokSkillGuard;
|
|
6
|
+
doctor?: typeof doctorGrokSkillGuard;
|
|
7
|
+
repair?: typeof repairGrokSkillGuard;
|
|
8
|
+
homeDir?: string;
|
|
9
|
+
log?: (msg: string) => void;
|
|
10
|
+
logError?: (msg: string) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function runGrokHookCommand(sub: GrokHookSub, _cwd: string, opts: {
|
|
13
|
+
force?: boolean;
|
|
14
|
+
}, deps?: GrokHookCmdDeps): Promise<number>;
|
|
15
|
+
//# sourceMappingURL=grok-hooks.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Console renderer for the Grok Build global SkillGuard hook manager. */
|
|
2
|
+
import { doctorGrokSkillGuard, installGrokSkillGuard, repairGrokSkillGuard, } from "../lib/grok-hook-manager.js";
|
|
3
|
+
function renderMutation(verb, result, log, logError) {
|
|
4
|
+
if (!result.ok) {
|
|
5
|
+
logError(`${verb} grok: refused`);
|
|
6
|
+
for (const error of result.errors)
|
|
7
|
+
logError(` ${error}`);
|
|
8
|
+
for (const backup of result.backups)
|
|
9
|
+
logError(` backup: ${backup}`);
|
|
10
|
+
return 1;
|
|
11
|
+
}
|
|
12
|
+
log(`${verb} grok: ok`);
|
|
13
|
+
if (result.changed.length === 0)
|
|
14
|
+
log("changed: nothing (already up to date)");
|
|
15
|
+
else
|
|
16
|
+
for (const changed of result.changed)
|
|
17
|
+
log(`changed: ${changed}`);
|
|
18
|
+
for (const backup of result.backups)
|
|
19
|
+
log(`backup: ${backup}`);
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
function renderDoctor(report, log) {
|
|
23
|
+
log(`doctor grok: ${report.healthy ? "healthy" : "unhealthy"}`);
|
|
24
|
+
log(` hook: ${report.hook.state}`);
|
|
25
|
+
log(` policy: ${report.policy.state}`);
|
|
26
|
+
log(" runtime: not verified (installed files only)");
|
|
27
|
+
for (const remediation of report.remediation)
|
|
28
|
+
log(` remediation: ${remediation}`);
|
|
29
|
+
// This is intentionally informational: local files cannot prove that Grok
|
|
30
|
+
// discovered, loaded, or invoked a plugin in a real session.
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
export async function runGrokHookCommand(sub, _cwd, opts, deps = {}) {
|
|
34
|
+
const log = deps.log ?? ((message) => console.log(message));
|
|
35
|
+
const logError = deps.logError ?? ((message) => console.error(message));
|
|
36
|
+
const home = deps.homeDir;
|
|
37
|
+
if (sub === "install")
|
|
38
|
+
return renderMutation("install", await (deps.install ?? installGrokSkillGuard)(home), log, logError);
|
|
39
|
+
if (sub === "repair")
|
|
40
|
+
return renderMutation("repair", await (deps.repair ?? repairGrokSkillGuard)(home, {
|
|
41
|
+
force: opts.force === true,
|
|
42
|
+
}), log, logError);
|
|
43
|
+
return renderDoctor(await (deps.doctor ?? doctorGrokSkillGuard)(home), log);
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=grok-hooks.js.map
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* map is typed `Record<AgentId, …>`, so omitting a loader is a compile error.
|
|
15
15
|
*/
|
|
16
16
|
import { validateSettingsShape } from "./claude-hook-settings.js";
|
|
17
|
-
export type AgentId = "claude" | "codex" | "opencode";
|
|
17
|
+
export type AgentId = "claude" | "codex" | "opencode" | "grok";
|
|
18
18
|
export type TrustState = "trusted" | "untrusted" | "unknown";
|
|
19
19
|
export interface TrustDescriptor {
|
|
20
20
|
/** Detect whether the installed hook is TRUSTED from the host config text. */
|
|
@@ -47,6 +47,7 @@ export interface AgentAdapter {
|
|
|
47
47
|
export declare const claudeAdapter: AgentAdapter;
|
|
48
48
|
export declare const codexAdapter: AgentAdapter;
|
|
49
49
|
export declare const opencodeAdapter: AgentAdapter;
|
|
50
|
+
export declare const grokAdapter: AgentAdapter;
|
|
50
51
|
export declare const AGENT_ADAPTERS: Record<AgentId, AgentAdapter>;
|
|
51
52
|
/**
|
|
52
53
|
* Whether a raw CLI token names a known agent adapter. Backed by the
|
|
@@ -52,6 +52,21 @@ const OPENCODE_MANAGED_SET = [
|
|
|
52
52
|
".claude/agents/",
|
|
53
53
|
".claude/skills/",
|
|
54
54
|
];
|
|
55
|
+
const GROK_MANAGED_SET = [
|
|
56
|
+
".grok/hooks/javi-forge-skillguard-pre-tool-use.json",
|
|
57
|
+
".grok/hooks/javi-forge-skillguard-pre-tool-use.mjs",
|
|
58
|
+
".grok/config.toml",
|
|
59
|
+
"AGENTS.md",
|
|
60
|
+
".claude/settings.json",
|
|
61
|
+
".claude/settings.local.json",
|
|
62
|
+
".claude/CLAUDE.md",
|
|
63
|
+
"CLAUDE.md",
|
|
64
|
+
".javi-forge/ci.yaml",
|
|
65
|
+
".grok/hooks/",
|
|
66
|
+
".claude/hooks/",
|
|
67
|
+
".claude/agents/",
|
|
68
|
+
".claude/skills/",
|
|
69
|
+
];
|
|
55
70
|
export const claudeAdapter = {
|
|
56
71
|
id: "claude",
|
|
57
72
|
configPaths(projectDir) {
|
|
@@ -103,10 +118,27 @@ export const opencodeAdapter = {
|
|
|
103
118
|
emitDeny: "throw-error",
|
|
104
119
|
trust: null,
|
|
105
120
|
};
|
|
121
|
+
export const grokAdapter = {
|
|
122
|
+
id: "grok",
|
|
123
|
+
configPaths(homeDir) {
|
|
124
|
+
const hooksDir = path.join(homeDir, ".grok", "hooks");
|
|
125
|
+
return {
|
|
126
|
+
hooksFile: path.join(hooksDir, "javi-forge-skillguard-pre-tool-use.json"),
|
|
127
|
+
settingsFile: path.join(hooksDir, "javi-forge-skillguard-pre-tool-use.mjs"),
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
managedSet: GROK_MANAGED_SET,
|
|
131
|
+
projectDir: { envVar: null },
|
|
132
|
+
settingsSchema: null,
|
|
133
|
+
marker: "javi-forge-managed: grok-pretooluse v1",
|
|
134
|
+
emitDeny: "exit2+stderr",
|
|
135
|
+
trust: null,
|
|
136
|
+
};
|
|
106
137
|
export const AGENT_ADAPTERS = {
|
|
107
138
|
claude: claudeAdapter,
|
|
108
139
|
codex: codexAdapter,
|
|
109
140
|
opencode: opencodeAdapter,
|
|
141
|
+
grok: grokAdapter,
|
|
110
142
|
};
|
|
111
143
|
/**
|
|
112
144
|
* Whether a raw CLI token names a known agent adapter. Backed by the
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok Build SkillGuard hook ownership manager. Grok reads trusted global hook
|
|
3
|
+
* definitions from ~/.grok/hooks, so installation is a two-file transaction:
|
|
4
|
+
* the PreToolUse JSON registration and the adjacent policy runtime. Doctor
|
|
5
|
+
* deliberately reports installed bytes/registration only, not runtime loading.
|
|
6
|
+
*/
|
|
7
|
+
import type { AssetManifestEntry } from "./claude-hook-manager.js";
|
|
8
|
+
import type { ClaudeHookComponentState } from "./claude-hook-settings.js";
|
|
9
|
+
import { type PlatformSecureFs } from "./secure-fs-transaction.js";
|
|
10
|
+
export declare const GROK_HOOK_NAME = "javi-forge-skillguard-pre-tool-use.json";
|
|
11
|
+
export declare const GROK_POLICY_NAME = "javi-forge-skillguard-pre-tool-use.mjs";
|
|
12
|
+
export declare const GROK_HOOK_MARKER = "javi-forge-managed: grok-pretooluse v1";
|
|
13
|
+
export declare const GROK_POLICY_MARKER = "// javi-forge-managed: claude-pretooluse v1";
|
|
14
|
+
export declare const GROK_HOOK_VERSION = 1;
|
|
15
|
+
export declare const GROK_TOOL_MATCHER = "run_terminal_command|read_file|search_replace";
|
|
16
|
+
export declare const GROK_ASSETS_DIR: string;
|
|
17
|
+
export declare const SHIPPED_GROK_POLICY: string;
|
|
18
|
+
export interface GrokHookManifestEntry {
|
|
19
|
+
name: string;
|
|
20
|
+
version: number;
|
|
21
|
+
matcher: string;
|
|
22
|
+
historicalVersions: number[];
|
|
23
|
+
}
|
|
24
|
+
export interface GrokManifest {
|
|
25
|
+
hook: GrokHookManifestEntry;
|
|
26
|
+
policy: AssetManifestEntry;
|
|
27
|
+
}
|
|
28
|
+
export interface GrokConfigPaths {
|
|
29
|
+
grokDir: string;
|
|
30
|
+
hooksDir: string;
|
|
31
|
+
hookFile: string;
|
|
32
|
+
policyFile: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function resolveGrokHomeRoot(baseDir: string, env?: NodeJS.ProcessEnv): string;
|
|
35
|
+
export declare function grokConfigPaths(baseDir: string, env?: NodeJS.ProcessEnv): GrokConfigPaths;
|
|
36
|
+
/** Generate the complete standalone Grok hook registration for an adjacent asset. */
|
|
37
|
+
export declare function expectedGrokHookJson(policyFile: string): string;
|
|
38
|
+
export interface GrokAssetClassification {
|
|
39
|
+
state: ClaudeHookComponentState;
|
|
40
|
+
sha256?: string;
|
|
41
|
+
version?: number;
|
|
42
|
+
detail?: string;
|
|
43
|
+
}
|
|
44
|
+
/** Classify an installed policy runtime with the packaged manifest identity. */
|
|
45
|
+
export declare function classifyGrokAsset(target: string, marker: string, manifest: AssetManifestEntry): Promise<GrokAssetClassification>;
|
|
46
|
+
/**
|
|
47
|
+
* Classify a generated hook registration semantically. JSON whitespace does not
|
|
48
|
+
* matter; the command is compared against the supplied baseDir's adjacent asset.
|
|
49
|
+
*/
|
|
50
|
+
export declare function classifyGrokHookJson(target: string, policyFile: string, manifest: GrokHookManifestEntry): Promise<GrokAssetClassification>;
|
|
51
|
+
export interface GrokHookDoctorReport {
|
|
52
|
+
healthy: boolean;
|
|
53
|
+
hook: GrokAssetClassification;
|
|
54
|
+
policy: GrokAssetClassification;
|
|
55
|
+
remediation: string[];
|
|
56
|
+
/** Installed files and registration only; no Grok execution is observed. */
|
|
57
|
+
runtimeEvidence: "not-implemented";
|
|
58
|
+
}
|
|
59
|
+
export interface GrokDoctorOptions {
|
|
60
|
+
manifest?: GrokManifest;
|
|
61
|
+
policySource?: string;
|
|
62
|
+
env?: NodeJS.ProcessEnv;
|
|
63
|
+
}
|
|
64
|
+
export declare function doctorGrokSkillGuard(baseDir?: string, options?: GrokDoctorOptions): Promise<GrokHookDoctorReport>;
|
|
65
|
+
interface GrokMutationSuccess {
|
|
66
|
+
ok: boolean;
|
|
67
|
+
changed: string[];
|
|
68
|
+
backups: string[];
|
|
69
|
+
errors: string[];
|
|
70
|
+
warnings: string[];
|
|
71
|
+
report: GrokHookDoctorReport;
|
|
72
|
+
}
|
|
73
|
+
export type GrokHookMutationResult = GrokMutationSuccess;
|
|
74
|
+
export interface GrokHookRunDeps extends GrokDoctorOptions {
|
|
75
|
+
secureFs?: PlatformSecureFs | null;
|
|
76
|
+
clock?: () => Date;
|
|
77
|
+
nonce?: () => string;
|
|
78
|
+
doctor?: typeof doctorGrokSkillGuard;
|
|
79
|
+
}
|
|
80
|
+
export declare function _runGrok(baseDir: string, mode: "install" | "repair", options: {
|
|
81
|
+
force?: boolean;
|
|
82
|
+
}, deps?: GrokHookRunDeps): Promise<GrokHookMutationResult>;
|
|
83
|
+
export declare function installGrokSkillGuard(baseDir?: string, deps?: GrokHookRunDeps): Promise<GrokHookMutationResult>;
|
|
84
|
+
export declare function repairGrokSkillGuard(baseDir?: string, options?: {
|
|
85
|
+
force?: boolean;
|
|
86
|
+
}, deps?: GrokHookRunDeps): Promise<GrokHookMutationResult>;
|
|
87
|
+
export {};
|
|
88
|
+
//# sourceMappingURL=grok-hook-manager.d.ts.map
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok Build SkillGuard hook ownership manager. Grok reads trusted global hook
|
|
3
|
+
* definitions from ~/.grok/hooks, so installation is a two-file transaction:
|
|
4
|
+
* the PreToolUse JSON registration and the adjacent policy runtime. Doctor
|
|
5
|
+
* deliberately reports installed bytes/registration only, not runtime loading.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
8
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { CLAUDE_HOOK_ASSETS_DIR, FORGE_ROOT } from "../constants.js";
|
|
12
|
+
import { safeReadFile } from "./safe-read.js";
|
|
13
|
+
import { selectSecureFs } from "./secure-fs-posix.js";
|
|
14
|
+
import { runTransaction, } from "./secure-fs-transaction.js";
|
|
15
|
+
const MAX_BYTES = 1024 * 1024;
|
|
16
|
+
const READ_OPTS = {
|
|
17
|
+
maxBytes: MAX_BYTES,
|
|
18
|
+
hardRejectOverBytes: MAX_BYTES,
|
|
19
|
+
maxLineLength: Number.POSITIVE_INFINITY,
|
|
20
|
+
};
|
|
21
|
+
export const GROK_HOOK_NAME = "javi-forge-skillguard-pre-tool-use.json";
|
|
22
|
+
export const GROK_POLICY_NAME = "javi-forge-skillguard-pre-tool-use.mjs";
|
|
23
|
+
export const GROK_HOOK_MARKER = "javi-forge-managed: grok-pretooluse v1";
|
|
24
|
+
export const GROK_POLICY_MARKER = "// javi-forge-managed: claude-pretooluse v1";
|
|
25
|
+
export const GROK_HOOK_VERSION = 1;
|
|
26
|
+
export const GROK_TOOL_MATCHER = "run_terminal_command|read_file|search_replace";
|
|
27
|
+
export const GROK_ASSETS_DIR = path.join(FORGE_ROOT, "assets", "grok-hooks");
|
|
28
|
+
export const SHIPPED_GROK_POLICY = path.join(CLAUDE_HOOK_ASSETS_DIR, GROK_POLICY_NAME);
|
|
29
|
+
export function resolveGrokHomeRoot(baseDir, env = process.env) {
|
|
30
|
+
const override = env.GROK_HOME;
|
|
31
|
+
return override && override.length > 0
|
|
32
|
+
? path.resolve(override)
|
|
33
|
+
: path.join(baseDir, ".grok");
|
|
34
|
+
}
|
|
35
|
+
export function grokConfigPaths(baseDir, env = process.env) {
|
|
36
|
+
const grokDir = resolveGrokHomeRoot(baseDir, env);
|
|
37
|
+
const hooksDir = path.join(grokDir, "hooks");
|
|
38
|
+
return {
|
|
39
|
+
grokDir,
|
|
40
|
+
hooksDir,
|
|
41
|
+
hookFile: path.join(hooksDir, GROK_HOOK_NAME),
|
|
42
|
+
policyFile: path.join(hooksDir, GROK_POLICY_NAME),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Generate the complete standalone Grok hook registration for an adjacent asset. */
|
|
46
|
+
export function expectedGrokHookJson(policyFile) {
|
|
47
|
+
const definition = {
|
|
48
|
+
"javi-forge-managed": GROK_HOOK_MARKER,
|
|
49
|
+
version: GROK_HOOK_VERSION,
|
|
50
|
+
hooks: {
|
|
51
|
+
PreToolUse: [
|
|
52
|
+
{
|
|
53
|
+
matcher: GROK_TOOL_MATCHER,
|
|
54
|
+
hooks: [
|
|
55
|
+
{
|
|
56
|
+
type: "command",
|
|
57
|
+
command: `${JSON.stringify(process.execPath)} ${JSON.stringify(policyFile)} --agent=grok`,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
return `${JSON.stringify(definition, null, 2)}\n`;
|
|
65
|
+
}
|
|
66
|
+
async function fileKind(target) {
|
|
67
|
+
try {
|
|
68
|
+
const stat = await lstat(target);
|
|
69
|
+
if (stat.isSymbolicLink())
|
|
70
|
+
return { kind: "symlink" };
|
|
71
|
+
return stat.isFile() ? { kind: "file" } : { kind: "non-regular" };
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const code = error.code;
|
|
75
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
76
|
+
return { kind: "absent" };
|
|
77
|
+
return { kind: "error", detail: code ?? String(error) };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function sha256(content) {
|
|
81
|
+
return createHash("sha256")
|
|
82
|
+
.update(Buffer.from(content, "utf8"))
|
|
83
|
+
.digest("hex");
|
|
84
|
+
}
|
|
85
|
+
/** Classify an installed policy runtime with the packaged manifest identity. */
|
|
86
|
+
export async function classifyGrokAsset(target, marker, manifest) {
|
|
87
|
+
const stat = await fileKind(target);
|
|
88
|
+
if (stat.kind === "absent")
|
|
89
|
+
return { state: "absent" };
|
|
90
|
+
if (stat.kind === "symlink")
|
|
91
|
+
return { state: "symlink" };
|
|
92
|
+
if (stat.kind !== "file")
|
|
93
|
+
return { state: "non-regular", detail: stat.detail };
|
|
94
|
+
const read = await safeReadFile(target, READ_OPTS);
|
|
95
|
+
if (!read.ok) {
|
|
96
|
+
if (read.reason === "not-found")
|
|
97
|
+
return { state: "absent" };
|
|
98
|
+
if (read.reason === "binary" || read.reason === "too-large")
|
|
99
|
+
return { state: "foreign", detail: read.reason };
|
|
100
|
+
return { state: "non-regular", detail: read.reason };
|
|
101
|
+
}
|
|
102
|
+
if (!read.content.startsWith(`${marker}\n`))
|
|
103
|
+
return { state: "foreign" };
|
|
104
|
+
const observed = sha256(read.content);
|
|
105
|
+
if (observed === manifest.sha256)
|
|
106
|
+
return {
|
|
107
|
+
state: "managed-current",
|
|
108
|
+
sha256: observed,
|
|
109
|
+
version: manifest.version,
|
|
110
|
+
};
|
|
111
|
+
if (manifest.historical.includes(observed))
|
|
112
|
+
return {
|
|
113
|
+
state: "released-outdated",
|
|
114
|
+
sha256: observed,
|
|
115
|
+
version: manifest.version,
|
|
116
|
+
};
|
|
117
|
+
return { state: "edited-managed", sha256: observed };
|
|
118
|
+
}
|
|
119
|
+
function isObject(value) {
|
|
120
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Classify a generated hook registration semantically. JSON whitespace does not
|
|
124
|
+
* matter; the command is compared against the supplied baseDir's adjacent asset.
|
|
125
|
+
*/
|
|
126
|
+
export async function classifyGrokHookJson(target, policyFile, manifest) {
|
|
127
|
+
const stat = await fileKind(target);
|
|
128
|
+
if (stat.kind === "absent")
|
|
129
|
+
return { state: "absent" };
|
|
130
|
+
if (stat.kind === "symlink")
|
|
131
|
+
return { state: "symlink" };
|
|
132
|
+
if (stat.kind !== "file")
|
|
133
|
+
return { state: "non-regular", detail: stat.detail };
|
|
134
|
+
const read = await safeReadFile(target, READ_OPTS);
|
|
135
|
+
if (!read.ok) {
|
|
136
|
+
if (read.reason === "not-found")
|
|
137
|
+
return { state: "absent" };
|
|
138
|
+
if (read.reason === "binary" || read.reason === "too-large")
|
|
139
|
+
return { state: "foreign", detail: read.reason };
|
|
140
|
+
return { state: "non-regular", detail: read.reason };
|
|
141
|
+
}
|
|
142
|
+
let parsed;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(read.content);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return { state: "malformed" };
|
|
148
|
+
}
|
|
149
|
+
if (!isObject(parsed))
|
|
150
|
+
return { state: "malformed" };
|
|
151
|
+
if (parsed["javi-forge-managed"] !== GROK_HOOK_MARKER)
|
|
152
|
+
return { state: "foreign" };
|
|
153
|
+
if (typeof parsed.version !== "number")
|
|
154
|
+
return { state: "edited-managed" };
|
|
155
|
+
if (manifest.historicalVersions.includes(parsed.version))
|
|
156
|
+
return { state: "released-outdated", version: parsed.version };
|
|
157
|
+
const expected = JSON.parse(expectedGrokHookJson(policyFile));
|
|
158
|
+
if (parsed.version === manifest.version &&
|
|
159
|
+
JSON.stringify(parsed) === JSON.stringify(expected))
|
|
160
|
+
return { state: "managed-current", version: parsed.version };
|
|
161
|
+
return { state: "edited-managed", version: parsed.version };
|
|
162
|
+
}
|
|
163
|
+
async function readManifest() {
|
|
164
|
+
const [hookRead, policyRead] = await Promise.all([
|
|
165
|
+
safeReadFile(path.join(GROK_ASSETS_DIR, "manifest.json"), READ_OPTS),
|
|
166
|
+
safeReadFile(path.join(CLAUDE_HOOK_ASSETS_DIR, "manifest.json"), READ_OPTS),
|
|
167
|
+
]);
|
|
168
|
+
if (!hookRead.ok)
|
|
169
|
+
throw new Error(`unreadable grok hook manifest: ${hookRead.reason}`);
|
|
170
|
+
if (!policyRead.ok)
|
|
171
|
+
throw new Error(`unreadable claude-hooks manifest: ${policyRead.reason}`);
|
|
172
|
+
const hookManifest = JSON.parse(hookRead.content);
|
|
173
|
+
const policyManifest = JSON.parse(policyRead.content);
|
|
174
|
+
return { hook: hookManifest.hook, policy: policyManifest.asset };
|
|
175
|
+
}
|
|
176
|
+
export async function doctorGrokSkillGuard(baseDir = os.homedir(), options = {}) {
|
|
177
|
+
const manifest = options.manifest ?? (await readManifest());
|
|
178
|
+
const paths = grokConfigPaths(baseDir, options.env);
|
|
179
|
+
const [hook, policy] = await Promise.all([
|
|
180
|
+
classifyGrokHookJson(paths.hookFile, paths.policyFile, manifest.hook),
|
|
181
|
+
classifyGrokAsset(paths.policyFile, GROK_POLICY_MARKER, manifest.policy),
|
|
182
|
+
]);
|
|
183
|
+
const remediation = [];
|
|
184
|
+
for (const [name, asset] of [
|
|
185
|
+
["hook registration", hook],
|
|
186
|
+
["policy runtime", policy],
|
|
187
|
+
]) {
|
|
188
|
+
if (asset.state === "absent" || asset.state === "released-outdated") {
|
|
189
|
+
remediation.push(`install the Grok ${name} with: javi-forge hooks install grok`);
|
|
190
|
+
}
|
|
191
|
+
else if (asset.state === "edited-managed") {
|
|
192
|
+
remediation.push(`repair the edited Grok ${name} with: javi-forge hooks repair grok --force`);
|
|
193
|
+
}
|
|
194
|
+
else if (asset.state !== "managed-current") {
|
|
195
|
+
remediation.push(`manually review the Grok ${name} (${asset.state}); it will not be overwritten`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
healthy: hook.state === "managed-current" && policy.state === "managed-current",
|
|
200
|
+
hook,
|
|
201
|
+
policy,
|
|
202
|
+
remediation: [...new Set(remediation)],
|
|
203
|
+
runtimeEvidence: "not-implemented",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function writePlan(state, force, name) {
|
|
207
|
+
if (state === "absent" || state === "released-outdated")
|
|
208
|
+
return { write: true };
|
|
209
|
+
if (state === "managed-current")
|
|
210
|
+
return { write: false };
|
|
211
|
+
if (state === "edited-managed" && force)
|
|
212
|
+
return { write: true };
|
|
213
|
+
if (state === "edited-managed")
|
|
214
|
+
return {
|
|
215
|
+
write: false,
|
|
216
|
+
refusal: `refuse edited ${name}; rerun repair with --force`,
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
write: false,
|
|
220
|
+
refusal: `refuse ${name} in state ${state} — manual review`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
export async function _runGrok(baseDir, mode, options, deps = {}) {
|
|
224
|
+
const manifest = deps.manifest ?? (await readManifest());
|
|
225
|
+
const paths = grokConfigPaths(baseDir, deps.env);
|
|
226
|
+
const doctorFn = deps.doctor ?? doctorGrokSkillGuard;
|
|
227
|
+
const doctor = () => doctorFn(baseDir, {
|
|
228
|
+
env: deps.env,
|
|
229
|
+
manifest,
|
|
230
|
+
policySource: deps.policySource,
|
|
231
|
+
});
|
|
232
|
+
const force = mode === "repair" && options.force === true;
|
|
233
|
+
const [hook, policy] = await Promise.all([
|
|
234
|
+
classifyGrokHookJson(paths.hookFile, paths.policyFile, manifest.hook),
|
|
235
|
+
classifyGrokAsset(paths.policyFile, GROK_POLICY_MARKER, manifest.policy),
|
|
236
|
+
]);
|
|
237
|
+
const hookPlan = writePlan(hook.state, force, "Grok hook registration");
|
|
238
|
+
const policyPlan = writePlan(policy.state, force, "Grok policy runtime");
|
|
239
|
+
if (hookPlan.refusal || policyPlan.refusal) {
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
changed: [],
|
|
243
|
+
backups: [],
|
|
244
|
+
errors: [hookPlan.refusal ?? policyPlan.refusal ?? "refused"],
|
|
245
|
+
warnings: [],
|
|
246
|
+
report: await doctor(),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (!hookPlan.write && !policyPlan.write) {
|
|
250
|
+
return {
|
|
251
|
+
ok: true,
|
|
252
|
+
changed: [],
|
|
253
|
+
backups: [],
|
|
254
|
+
errors: [],
|
|
255
|
+
warnings: [],
|
|
256
|
+
report: await doctor(),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const secureFs = deps.secureFs !== undefined
|
|
260
|
+
? deps.secureFs
|
|
261
|
+
: selectSecureFs(process.platform);
|
|
262
|
+
if (!secureFs) {
|
|
263
|
+
return {
|
|
264
|
+
ok: false,
|
|
265
|
+
changed: [],
|
|
266
|
+
backups: [],
|
|
267
|
+
errors: ["windows-secure-object-unavailable"],
|
|
268
|
+
warnings: [],
|
|
269
|
+
report: await doctor(),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const policySource = deps.policySource ?? SHIPPED_GROK_POLICY;
|
|
273
|
+
const policyBytes = policyPlan.write ? await readFile(policySource) : null;
|
|
274
|
+
if (policyBytes &&
|
|
275
|
+
createHash("sha256").update(policyBytes).digest("hex") !==
|
|
276
|
+
manifest.policy.sha256) {
|
|
277
|
+
return {
|
|
278
|
+
ok: false,
|
|
279
|
+
changed: [],
|
|
280
|
+
backups: [],
|
|
281
|
+
errors: ["refuse packaged Grok policy asset with manifest hash mismatch"],
|
|
282
|
+
warnings: [],
|
|
283
|
+
report: await doctor(),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const hookBytes = hookPlan.write
|
|
287
|
+
? Buffer.from(expectedGrokHookJson(paths.policyFile), "utf8")
|
|
288
|
+
: null;
|
|
289
|
+
const components = [
|
|
290
|
+
{
|
|
291
|
+
path: paths.hookFile,
|
|
292
|
+
desired: hookBytes,
|
|
293
|
+
capturePrior: hookPlan.write && hook.state !== "absent",
|
|
294
|
+
forceBackup: force && hook.state === "edited-managed",
|
|
295
|
+
wasAbsent: hook.state === "absent",
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
path: paths.policyFile,
|
|
299
|
+
desired: policyBytes,
|
|
300
|
+
capturePrior: policyPlan.write && policy.state !== "absent",
|
|
301
|
+
forceBackup: force && policy.state === "edited-managed",
|
|
302
|
+
wasAbsent: policy.state === "absent",
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
const tx = await runTransaction({
|
|
306
|
+
secureFs,
|
|
307
|
+
clock: deps.clock ?? (() => new Date()),
|
|
308
|
+
nonce: deps.nonce ?? (() => randomBytes(4).toString("hex")),
|
|
309
|
+
projectDir: path.dirname(paths.grokDir),
|
|
310
|
+
layout: { containers: [paths.grokDir, paths.hooksDir], components },
|
|
311
|
+
});
|
|
312
|
+
return {
|
|
313
|
+
ok: tx.ok,
|
|
314
|
+
changed: tx.committed,
|
|
315
|
+
backups: tx.backups,
|
|
316
|
+
errors: tx.errors,
|
|
317
|
+
warnings: [],
|
|
318
|
+
report: await doctor(),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
export function installGrokSkillGuard(baseDir, deps = {}) {
|
|
322
|
+
return _runGrok(baseDir ?? os.homedir(), "install", {}, deps);
|
|
323
|
+
}
|
|
324
|
+
export function repairGrokSkillGuard(baseDir, options, deps = {}) {
|
|
325
|
+
return _runGrok(baseDir ?? os.homedir(), "repair", options ?? {}, deps);
|
|
326
|
+
}
|
|
327
|
+
//# sourceMappingURL=grok-hook-manager.js.map
|