javi-forge 1.35.0 → 1.36.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.
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Agent adapter registry (agent-agnostic slice 2). One descriptor per host
3
+ * (`claude`, `codex`) capturing the per-agent facts the SkillGuard installer and
4
+ * doctor need: config paths, the protected managed set, the project-root source,
5
+ * the settings-schema validator (SHARED — both hosts use the identical hooks
6
+ * schema), the managed marker, the deny protocol, and the trust model.
7
+ *
8
+ * This descriptor is additive: Codex is routed through it, while Claude keeps its
9
+ * existing, byte-identical runtime path (the descriptor only DESCRIBES Claude; it
10
+ * does not change how the Claude command executes). The CLI dispatch decides the
11
+ * valid `hooks <sub> <agent>` target set through `isAgentId` (backed by the
12
+ * `AGENT_ADAPTERS` keys) — this registry is the single source of agent truth, and
13
+ * a new agent added here cannot be silently dropped: the dispatch's command-loader
14
+ * map is typed `Record<AgentId, …>`, so omitting a loader is a compile error.
15
+ */
16
+ import path from "node:path";
17
+ import { ASSET_NAME } from "./__fixtures__/claude-hook-ownership.js";
18
+ import { validateSettingsShape } from "./claude-hook-settings.js";
19
+ import { codexConfigPaths, codexTrustGrantCommand, hasCodexTrustEntry, } from "./codex-hook-manager.js";
20
+ const CLAUDE_MANAGED_SET = [
21
+ ".claude/settings.json",
22
+ ".claude/settings.local.json",
23
+ ".claude/CLAUDE.md",
24
+ "CLAUDE.md",
25
+ ".javi-forge/ci.yaml",
26
+ ".claude/hooks/",
27
+ ".claude/agents/",
28
+ ".claude/skills/",
29
+ ];
30
+ const CODEX_MANAGED_SET = [
31
+ ".codex/hooks.json",
32
+ ".claude/settings.json",
33
+ ".claude/settings.local.json",
34
+ ".claude/CLAUDE.md",
35
+ "CLAUDE.md",
36
+ ".javi-forge/ci.yaml",
37
+ ".claude/hooks/",
38
+ ".claude/agents/",
39
+ ".claude/skills/",
40
+ ];
41
+ export const claudeAdapter = {
42
+ id: "claude",
43
+ configPaths(projectDir) {
44
+ return {
45
+ hooksFile: path.join(projectDir, ".claude", "hooks", ASSET_NAME),
46
+ settingsFile: path.join(projectDir, ".claude", "settings.json"),
47
+ };
48
+ },
49
+ managedSet: CLAUDE_MANAGED_SET,
50
+ projectDir: { envVar: "CLAUDE_PROJECT_DIR" },
51
+ settingsSchema: validateSettingsShape,
52
+ marker: "// javi-forge-managed: claude-pretooluse v1",
53
+ emitDeny: "exit2+stderr",
54
+ trust: null,
55
+ };
56
+ export const codexAdapter = {
57
+ id: "codex",
58
+ configPaths(homeDir) {
59
+ const paths = codexConfigPaths(homeDir);
60
+ return { hooksFile: paths.hooksFile, settingsFile: paths.configFile };
61
+ },
62
+ managedSet: CODEX_MANAGED_SET,
63
+ projectDir: { envVar: null },
64
+ settingsSchema: validateSettingsShape,
65
+ marker: "// javi-forge-managed: codex-pretooluse v1",
66
+ emitDeny: "exit2+stderr",
67
+ trust: {
68
+ detect(configText, hooksFile) {
69
+ return hasCodexTrustEntry(configText, hooksFile)
70
+ ? "trusted"
71
+ : "untrusted";
72
+ },
73
+ grantCommand: codexTrustGrantCommand,
74
+ },
75
+ };
76
+ export const AGENT_ADAPTERS = {
77
+ claude: claudeAdapter,
78
+ codex: codexAdapter,
79
+ };
80
+ /**
81
+ * Whether a raw CLI token names a known agent adapter. Backed by the
82
+ * `AGENT_ADAPTERS` registry so there is ONE source deciding valid targets: adding
83
+ * an adapter to the registry automatically widens the accepted CLI set.
84
+ */
85
+ export function isAgentId(value) {
86
+ return typeof value === "string" && Object.hasOwn(AGENT_ADAPTERS, value);
87
+ }
88
+ //# sourceMappingURL=agent-adapter.js.map
@@ -14,7 +14,7 @@ import os from "node:os";
14
14
  import path from "node:path";
15
15
  import { CLAUDE_HOOK_ASSETS_DIR } from "../constants.js";
16
16
  import { ASSET_MANAGED_MARKER, ASSET_NAME, } from "./__fixtures__/claude-hook-ownership.js";
17
- import { buildManagedContainer, classifySettingsEntry, isPlainObject, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, planForceReplace, planLegacyCohortExcision, planManagedClaudeHookMerge, scanExecutionFlags, } from "./claude-hook-settings.js";
17
+ import { buildManagedContainer, classifySettingsEntry, isPlainObject, LEGACY_FILE_SHA256, MANAGED_AGENT_ARG, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, planForceReplace, planLegacyCohortExcision, planManagedClaudeHookMerge, scanExecutionFlags, } from "./claude-hook-settings.js";
18
18
  import { safeReadFile } from "./safe-read.js";
19
19
  import { ACL_DETAIL, probeAclCapability, selectSecureFs, } from "./secure-fs-posix.js";
20
20
  import { runTransaction, } from "./secure-fs-transaction.js";
@@ -174,8 +174,9 @@ function settingsSignals(value, classification, currentAssetSha) {
174
174
  const commandShapeExact = handler.type === "command" &&
175
175
  handler.command === "node" &&
176
176
  Array.isArray(args) &&
177
- args.length === 1 &&
177
+ args.length === 2 &&
178
178
  args[0] === MANAGED_ASSET_ARG &&
179
+ args[1] === MANAGED_AGENT_ARG &&
179
180
  handler.timeout === 30;
180
181
  let assetSettingsConsistent = false;
181
182
  if (typeof handler.statusMessage === "string" &&
@@ -8,8 +8,8 @@
8
8
  * removal/merge PLANNING only. Identity is always recomputed from observed
9
9
  * structure; a marker only claims ownership, it never proves it.
10
10
  */
11
- import { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX } from "./__fixtures__/claude-hook-ownership.js";
12
- export { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, };
11
+ import { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_AGENT_ARG, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX } from "./__fixtures__/claude-hook-ownership.js";
12
+ export { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_AGENT_ARG, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, };
13
13
  /** The nine independent component states (identical for asset and settings). */
14
14
  export type ClaudeHookComponentState = "absent" | "managed-current" | "released-outdated" | "exact-legacy" | "edited-managed" | "foreign" | "symlink" | "non-regular" | "malformed";
15
15
  /** A released settings-entry identity: version plus placeholder-normalized hash. */
@@ -149,7 +149,7 @@ export declare function planManagedClaudeHookMerge(parsed: unknown, currentAsset
149
149
  export interface ManagedHandler {
150
150
  type: "command";
151
151
  command: "node";
152
- args: [string];
152
+ args: [string, string];
153
153
  timeout: number;
154
154
  statusMessage: string;
155
155
  }
@@ -9,8 +9,8 @@
9
9
  * structure; a marker only claims ownership, it never proves it.
10
10
  */
11
11
  import { createHash } from "node:crypto";
12
- import { ASSET_SHA_PLACEHOLDER, LEGACY_COHORT, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, } from "./__fixtures__/claude-hook-ownership.js";
13
- export { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, };
12
+ import { ASSET_SHA_PLACEHOLDER, LEGACY_COHORT, LEGACY_FILE_SHA256, MANAGED_AGENT_ARG, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, } from "./__fixtures__/claude-hook-ownership.js";
13
+ export { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_AGENT_ARG, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, };
14
14
  // Shape helpers
15
15
  export function isPlainObject(value) {
16
16
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -443,7 +443,7 @@ export function buildManagedContainer(currentAssetSha) {
443
443
  {
444
444
  type: "command",
445
445
  command: "node",
446
- args: [MANAGED_ASSET_ARG],
446
+ args: [MANAGED_ASSET_ARG, MANAGED_AGENT_ARG],
447
447
  timeout: MANAGED_TIMEOUT,
448
448
  statusMessage: `${MANAGED_STATUS_PREFIX}${currentAssetSha}`,
449
449
  },
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Codex PreToolUse ownership manager (agent-agnostic slice 2). Installs the
3
+ * SAME shipped SkillGuard `.mjs` asset as a Codex `PreToolUse` hook by writing
4
+ * `~/.codex/hooks.json` + setting `[features] hooks = true` in
5
+ * `~/.codex/config.toml`, through the identical secure-fs transaction the Claude
6
+ * installer uses (no weaker path). It NEVER modifies the guard asset, the pure
7
+ * `evaluate*` engine, or Claude's observable behavior.
8
+ *
9
+ * TRUST (highest-risk surface — engram id 15743, codex-cli 0.147.0, verified
10
+ * live 2026-08-18): codex hooks are stable + default-ON, but each hook needs a
11
+ * `trusted_hash` recorded in `config.toml` under
12
+ * `[hooks.state."<abs-hook-path>:pre_tool_use:0:0"]`; an UNTRUSTED hook is
13
+ * SILENTLY SKIPPED unless `--dangerously-bypass-hook-trust`. There is NO
14
+ * `codex hooks trust` subcommand (confirmed: `codex --help` has no `hooks`
15
+ * command). So we DO NOT compute-and-write a trusted_hash we cannot prove
16
+ * reproducible (a wrong-but-present hash would leave the hook skipped while
17
+ * making doctor believe it is trusted — the exact fail-open theater this arc
18
+ * exists to kill). Instead: install writes the files + REPORTS the trust step,
19
+ * and the doctor DETECTS the missing trust entry and reports `blocked`
20
+ * (untrusted = NOT running).
21
+ *
22
+ * STALE-HASH INVALIDATION: the trust key path is STABLE across upgrades, so a
23
+ * rewrite of hooks.json (asset/command/timeout change) leaves the recorded
24
+ * `trusted_hash` stale — Codex silently skips the hook while the header
25
+ * persists (doctor would wrongly stay `trusted`). So whenever install/repair
26
+ * REWRITES the managed hooks.json, it REMOVES our `[hooks.state."<hooksFile>:*"]`
27
+ * table(s) in the same transactional config write (foreign rows untouched),
28
+ * reverting the doctor to `untrusted → blocked` until the user re-approves. An
29
+ * idempotent no-op install (unchanged hook content) never touches the table.
30
+ */
31
+ import { type AssetManifestEntry, type ExecutionReport, type NodeOnPathProbe } from "./claude-hook-manager.js";
32
+ import { type ClaudeHookComponentState } from "./claude-hook-settings.js";
33
+ import { type SpawnFn } from "./secure-fs-posix.js";
34
+ import { type PlatformSecureFs } from "./secure-fs-transaction.js";
35
+ export type { NodeOnPathProbe };
36
+ /** The shipped, in-package guard asset the Codex hook references by ABSOLUTE path. */
37
+ export declare const SHIPPED_CODEX_ASSET: string;
38
+ /** Codex manifest view — only the asset entry is needed for currency. */
39
+ export interface Manifest {
40
+ asset: AssetManifestEntry;
41
+ }
42
+ export interface CodexConfigPaths {
43
+ codexDir: string;
44
+ hooksFile: string;
45
+ configFile: string;
46
+ }
47
+ /** Resolve `~/.codex/{hooks.json,config.toml}` for a given home directory. */
48
+ export declare function codexConfigPaths(homeDir: string): CodexConfigPaths;
49
+ /** The exact `command` string the managed Codex hook runs (single-string form). */
50
+ export declare function expectedCodexCommand(assetPath: string): string;
51
+ /** The interactive step that establishes hook trust (there is no non-interactive subcommand). */
52
+ export declare function codexTrustGrantCommand(hooksFile: string): string;
53
+ /** Read the `[features] hooks` flag: "true" | "false" | "absent". */
54
+ export declare function parseFeaturesHooks(text: string): "true" | "false" | "absent";
55
+ /**
56
+ * True when `config.toml` records a trust table for THIS hook path, i.e. a
57
+ * `[hooks.state."<hooksFile>:pre_tool_use:0:0"]` header. Fail-closed: a trust
58
+ * entry for a different path does not count.
59
+ *
60
+ * NOTE (fail-open the arc kills): presence of the header is NOT proof the hook
61
+ * is still trusted — Codex records a `trusted_hash` under it, and a hook whose
62
+ * content was rewritten (e.g. an asset/command/timeout upgrade) has a STALE hash
63
+ * → Codex silently skips it and re-prompts. We cannot recompute Codex's hash to
64
+ * compare here, so instead the installer INVALIDATES this table whenever it
65
+ * rewrites the managed hooks.json (see `removeCodexTrustEntries`), reverting the
66
+ * doctor to `untrusted → blocked` until the user re-approves in codex.
67
+ */
68
+ export declare function hasCodexTrustEntry(text: string, hooksFile: string): boolean;
69
+ /**
70
+ * Remove every `[hooks.state."<hooksFile>:*"]` table (header + body lines) keyed
71
+ * on OUR managed hooks.json path, preserving all other content — including
72
+ * FOREIGN `hooks.state` rows for other hooks files. Used to invalidate a now-
73
+ * stale `trusted_hash` when the managed hooks.json content is rewritten: the
74
+ * trust-key path is stable across upgrades, so a rewritten hook keeps its old
75
+ * (now wrong) recorded hash and would be silently skipped by Codex while the
76
+ * header persisted. Dropping the table forces the doctor back to `untrusted`
77
+ * until the user re-approves the hook in codex.
78
+ */
79
+ export declare function removeCodexTrustEntries(text: string, hooksFile: string): string;
80
+ /**
81
+ * Ensure `[features] hooks = true`, preserving all other content and idempotent
82
+ * when already true. Only ever INSERTS a line or flips a `hooks = false` inside
83
+ * `[features]`, so it can never corrupt unrelated TOML.
84
+ */
85
+ export declare function mergeFeaturesHooksTrue(text: string): string;
86
+ export interface CodexHooksClassification {
87
+ state: ClaudeHookComponentState;
88
+ detail?: string;
89
+ }
90
+ /**
91
+ * Classify `hooks.json`. Reuses `validateSettingsShape` (the SAME settings-schema
92
+ * validator the Claude classifier uses — the Codex hooks.json schema is
93
+ * identical) and recognizes our managed handler by its exact command string.
94
+ * - malformed → not a valid hooks container
95
+ * - managed-current → our exact command present
96
+ * - released-outdated→ our guard present but at a stale asset path
97
+ * - foreign → other PreToolUse handlers, none of them ours
98
+ * - absent → no PreToolUse handlers at all (installable)
99
+ */
100
+ export declare function classifyCodexHooksJson(value: unknown, expectedCommand: string): CodexHooksClassification;
101
+ export type CodexTrustState = "trusted" | "untrusted";
102
+ export interface CodexHookDoctorReport {
103
+ healthy: boolean;
104
+ hooksJson: {
105
+ state: ClaudeHookComponentState;
106
+ detail?: string;
107
+ };
108
+ config: {
109
+ featuresHooks: "true" | "false" | "absent";
110
+ readable: boolean;
111
+ };
112
+ asset: {
113
+ state: ClaudeHookComponentState;
114
+ sha256?: string;
115
+ };
116
+ node: {
117
+ available: boolean;
118
+ version?: string;
119
+ satisfiesMinimum: boolean;
120
+ };
121
+ nodeOnPath: NodeOnPathProbe;
122
+ execution: ExecutionReport;
123
+ trust: {
124
+ state: CodexTrustState;
125
+ grantCommand: string;
126
+ };
127
+ remediation: string[];
128
+ }
129
+ export interface CodexDoctorOptions {
130
+ manifest?: Manifest;
131
+ assetPath?: string;
132
+ nodeVersion?: string;
133
+ nodeProbe?: () => Promise<NodeOnPathProbe>;
134
+ }
135
+ export declare function doctorCodexPreToolUse(homeDir?: string, options?: CodexDoctorOptions): Promise<CodexHookDoctorReport>;
136
+ export interface CodexHookMutationResult {
137
+ ok: boolean;
138
+ changed: string[];
139
+ backups: string[];
140
+ report: CodexHookDoctorReport;
141
+ errors: string[];
142
+ warnings: string[];
143
+ }
144
+ export interface CodexHookRunDeps {
145
+ secureFs?: PlatformSecureFs | null;
146
+ clock?: () => Date;
147
+ nonce?: () => string;
148
+ manifest?: Manifest;
149
+ platform?: NodeJS.Platform;
150
+ assetPath?: string;
151
+ nodeProbe?: () => Promise<NodeOnPathProbe>;
152
+ nodeSpawn?: SpawnFn;
153
+ }
154
+ export declare function _runCodex(homeDir: string, _mode: "install" | "repair", _options: {
155
+ force?: boolean;
156
+ }, deps: CodexHookRunDeps): Promise<CodexHookMutationResult>;
157
+ export declare function installCodexPreToolUse(homeDir?: string): Promise<CodexHookMutationResult>;
158
+ export declare function repairCodexPreToolUse(homeDir?: string, options?: {
159
+ force?: boolean;
160
+ }): Promise<CodexHookMutationResult>;
161
+ //# sourceMappingURL=codex-hook-manager.d.ts.map