@webpieces/ai-hook-rules 0.4.710 → 0.4.712
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/package.json +2 -2
- package/src/adapters/detect-ai.d.ts +5 -4
- package/src/adapters/detect-ai.js +5 -4
- package/src/adapters/detect-ai.js.map +1 -1
- package/src/adapters/hook-core.js +9 -3
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/codex-guard-presence.d.ts +86 -0
- package/src/bin/codex-guard-presence.js +150 -0
- package/src/bin/codex-guard-presence.js.map +1 -0
- package/src/bin/codex-trust.d.ts +99 -0
- package/src/bin/codex-trust.js +199 -0
- package/src/bin/codex-trust.js.map +1 -0
- package/src/bin/hook-registration.d.ts +200 -43
- package/src/bin/hook-registration.js +218 -89
- package/src/bin/hook-registration.js.map +1 -1
- package/src/bin/l0-allowlist.d.ts +0 -30
- package/src/bin/l0-allowlist.js +9 -67
- package/src/bin/l0-allowlist.js.map +1 -1
- package/src/bin/l0-decide.d.ts +43 -0
- package/src/bin/l0-decide.js +90 -0
- package/src/bin/l0-decide.js.map +1 -0
- package/src/bin/l0-ignored-tools.d.ts +38 -0
- package/src/bin/l0-ignored-tools.js +44 -0
- package/src/bin/l0-ignored-tools.js.map +1 -0
- package/src/bin/setup-config.d.ts +19 -0
- package/src/bin/setup-config.js +370 -0
- package/src/bin/setup-config.js.map +1 -0
- package/src/bin/setup.d.ts +56 -21
- package/src/bin/setup.js +125 -370
- package/src/bin/setup.js.map +1 -1
- package/src/bin/shim-audit-log.js +10 -1
- package/src/bin/shim-audit-log.js.map +1 -1
- package/src/bin/shim-deny-reason.js +4 -4
- package/src/bin/shim-deny-reason.js.map +1 -1
- package/src/bin/shim.d.ts +2 -0
- package/src/bin/shim.js +21 -1
- package/src/bin/shim.js.map +1 -1
- package/src/bin/upgrade-shim.d.ts +1 -1
- package/src/bin/upgrade-shim.js +12 -8
- package/src/bin/upgrade-shim.js.map +1 -1
- package/src/core/agent-event.d.ts +16 -0
- package/src/core/agent-event.js +17 -1
- package/src/core/agent-event.js.map +1 -1
- package/src/core/ai-type-context.d.ts +36 -0
- package/src/core/ai-type-context.js +45 -0
- package/src/core/ai-type-context.js.map +1 -0
- package/src/core/decision-log.js +9 -0
- package/src/core/decision-log.js.map +1 -1
- package/src/core/l0-matrix.js +14 -6
- package/src/core/l0-matrix.js.map +1 -1
- package/src/core/l0-tooling-doc.d.ts +3 -2
- package/src/core/l0-tooling-doc.js +22 -10
- package/src/core/l0-tooling-doc.js.map +1 -1
- package/src/core/rejection-log.js +5 -1
- package/src/core/rejection-log.js.map +1 -1
- package/src/index.d.ts +1 -1
- package/src/index.js +4 -1
- package/src/index.js.map +1 -1
- package/templates/ai-hook.sh +15 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CODEX HOOK TRUST — read it, report it, NEVER write it.
|
|
3
|
+
*
|
|
4
|
+
* ─── What Codex actually does ─────────────────────────────────────────────────────────────────────
|
|
5
|
+
* Codex trusts a hook entry TOFU. On first sight of a new or changed entry it PROMPTS
|
|
6
|
+
* (`Hooks need review / 1 hook is new or changed`) and, if the human accepts, records a hash in
|
|
7
|
+
* `~/.codex/config.toml`:
|
|
8
|
+
*
|
|
9
|
+
* [hooks.state."<abs path to .codex/hooks.json>:pre_tool_use:<group>:<index>"]
|
|
10
|
+
* trusted_hash = "sha256:…"
|
|
11
|
+
*
|
|
12
|
+
* ─── Why this only ever REPORTS ───────────────────────────────────────────────────────────────────
|
|
13
|
+
* The hash is NOT reproducible from outside Codex. Sixteen encodings were tried against a hooks.json we
|
|
14
|
+
* wrote ourselves and none of them produced the recorded value. So an installer that tried to write one
|
|
15
|
+
* would be guessing at a security decision on a human's behalf, and would either be ignored or — worse —
|
|
16
|
+
* appear to succeed. Forging it is not on the table even if the encoding were known: the prompt IS the
|
|
17
|
+
* mechanism, and the whole point of TOFU is that a person saw the command once.
|
|
18
|
+
*
|
|
19
|
+
* The consequence the installer must therefore SAY OUT LOUD: the prompt's third option is
|
|
20
|
+
* `Continue without trusting (hooks won't run)`. That is one keystroke to a fully unguarded session,
|
|
21
|
+
* with no later warning of any kind. Install-time verification cannot see that choice — it happens
|
|
22
|
+
* afterwards, in another process — which is exactly why guard-presence attestation exists as a separate
|
|
23
|
+
* mechanism (see ./codex-guard-presence.ts) rather than being folded into this check.
|
|
24
|
+
*/
|
|
25
|
+
/** What one repo's Codex trust looks like right now. Data-only → a class, per CLAUDE.md. */
|
|
26
|
+
export declare class CodexTrustStatus {
|
|
27
|
+
/** `~/.codex/config.toml`, named even when it does not exist so the report can point at it. */
|
|
28
|
+
readonly configPath: string;
|
|
29
|
+
readonly configExists: boolean;
|
|
30
|
+
/** `[projects."<root>"] trust_level = "trusted"`. */
|
|
31
|
+
readonly projectTrusted: boolean;
|
|
32
|
+
/** The repo's `.codex/hooks.json`, absolute — the key every `hooks.state` entry is prefixed by. */
|
|
33
|
+
readonly hooksPath: string;
|
|
34
|
+
/** How many webpieces-managed PreToolUse entries that file registers. 0 ⇒ Codex is not armed. */
|
|
35
|
+
readonly registeredEntries: number;
|
|
36
|
+
/** How many of them `~/.codex/config.toml` records a `trusted_hash` for. */
|
|
37
|
+
readonly trustedEntries: number;
|
|
38
|
+
constructor(
|
|
39
|
+
/** `~/.codex/config.toml`, named even when it does not exist so the report can point at it. */
|
|
40
|
+
configPath: string, configExists: boolean,
|
|
41
|
+
/** `[projects."<root>"] trust_level = "trusted"`. */
|
|
42
|
+
projectTrusted: boolean,
|
|
43
|
+
/** The repo's `.codex/hooks.json`, absolute — the key every `hooks.state` entry is prefixed by. */
|
|
44
|
+
hooksPath: string,
|
|
45
|
+
/** How many webpieces-managed PreToolUse entries that file registers. 0 ⇒ Codex is not armed. */
|
|
46
|
+
registeredEntries: number,
|
|
47
|
+
/** How many of them `~/.codex/config.toml` records a `trusted_hash` for. */
|
|
48
|
+
trustedEntries: number);
|
|
49
|
+
/** True when Codex is armed here AND every entry it would run is trusted. */
|
|
50
|
+
fullyTrusted(): boolean;
|
|
51
|
+
/** True when there is nothing to say — this repo has not armed Codex at all. */
|
|
52
|
+
notArmed(): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The report, as lines. It states what IS, then the one action that changes it — and the action is
|
|
55
|
+
* always the human's, because nothing here can be repaired by a tool.
|
|
56
|
+
*/
|
|
57
|
+
lines(): readonly string[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Reads Codex's trust state. Never writes, never throws — an unreadable or absent config reports as
|
|
61
|
+
* "not trusted", which is the true and safe answer.
|
|
62
|
+
*/
|
|
63
|
+
export declare class CodexTrustProbe {
|
|
64
|
+
/** `~/.codex/config.toml`. `homeDir` is injectable so a unit test never reads the real one. */
|
|
65
|
+
configPath(homeDir?: string): string;
|
|
66
|
+
read(projectRoot: string, homeDir?: string): CodexTrustStatus;
|
|
67
|
+
/**
|
|
68
|
+
* How many webpieces-managed PreToolUse commands `.codex/hooks.json` registers.
|
|
69
|
+
*
|
|
70
|
+
* It counts what WE own, not every hook in the file: a consumer's own unrelated hook is none of this
|
|
71
|
+
* check's business, and counting it would make the report say "1 of 3 trusted" about hooks webpieces
|
|
72
|
+
* neither wrote nor can advise on.
|
|
73
|
+
*/
|
|
74
|
+
private registeredEntryCount;
|
|
75
|
+
private readOrEmpty;
|
|
76
|
+
/**
|
|
77
|
+
* `[projects."<root>"]` carrying `trust_level = "trusted"`.
|
|
78
|
+
*
|
|
79
|
+
* A LINE SCAN, not a TOML parser, and deliberately: this package must load on a tree too broken to
|
|
80
|
+
* build a DI container, so it takes no dependency it does not need, and the two shapes it reads are
|
|
81
|
+
* both written by Codex itself in one fixed form. It over-reports "not trusted" for anything it
|
|
82
|
+
* cannot recognise, which is the safe direction — the consequence is one advisory line too many,
|
|
83
|
+
* never a session reported as guarded when it is not.
|
|
84
|
+
*/
|
|
85
|
+
private projectTrusted;
|
|
86
|
+
/**
|
|
87
|
+
* How many `hooks.state."<hooksPath>:pre_tool_use:<group>:<index>"` sections carry a `trusted_hash`.
|
|
88
|
+
*
|
|
89
|
+
* The count is compared against the number of entries the file registers rather than matched
|
|
90
|
+
* one-for-one to a specific group/index: the group and index are Codex's own numbering of a file it
|
|
91
|
+
* re-reads on every launch, and an installer that assumed a numbering would report a false alarm the
|
|
92
|
+
* first time Codex renumbered.
|
|
93
|
+
*/
|
|
94
|
+
private trustedEntryCount;
|
|
95
|
+
/** The body lines of one TOML section, or [] when the file carries no such header. */
|
|
96
|
+
private sectionBody;
|
|
97
|
+
/** The `<name>` of a `[<name>]` header line, or null for anything else. */
|
|
98
|
+
private headerOf;
|
|
99
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CodexTrustProbe = exports.CodexTrustStatus = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
const os_1 = require("os");
|
|
8
|
+
const to_error_1 = require("../core/to-error");
|
|
9
|
+
const hook_registration_1 = require("./hook-registration");
|
|
10
|
+
/**
|
|
11
|
+
* CODEX HOOK TRUST — read it, report it, NEVER write it.
|
|
12
|
+
*
|
|
13
|
+
* ─── What Codex actually does ─────────────────────────────────────────────────────────────────────
|
|
14
|
+
* Codex trusts a hook entry TOFU. On first sight of a new or changed entry it PROMPTS
|
|
15
|
+
* (`Hooks need review / 1 hook is new or changed`) and, if the human accepts, records a hash in
|
|
16
|
+
* `~/.codex/config.toml`:
|
|
17
|
+
*
|
|
18
|
+
* [hooks.state."<abs path to .codex/hooks.json>:pre_tool_use:<group>:<index>"]
|
|
19
|
+
* trusted_hash = "sha256:…"
|
|
20
|
+
*
|
|
21
|
+
* ─── Why this only ever REPORTS ───────────────────────────────────────────────────────────────────
|
|
22
|
+
* The hash is NOT reproducible from outside Codex. Sixteen encodings were tried against a hooks.json we
|
|
23
|
+
* wrote ourselves and none of them produced the recorded value. So an installer that tried to write one
|
|
24
|
+
* would be guessing at a security decision on a human's behalf, and would either be ignored or — worse —
|
|
25
|
+
* appear to succeed. Forging it is not on the table even if the encoding were known: the prompt IS the
|
|
26
|
+
* mechanism, and the whole point of TOFU is that a person saw the command once.
|
|
27
|
+
*
|
|
28
|
+
* The consequence the installer must therefore SAY OUT LOUD: the prompt's third option is
|
|
29
|
+
* `Continue without trusting (hooks won't run)`. That is one keystroke to a fully unguarded session,
|
|
30
|
+
* with no later warning of any kind. Install-time verification cannot see that choice — it happens
|
|
31
|
+
* afterwards, in another process — which is exactly why guard-presence attestation exists as a separate
|
|
32
|
+
* mechanism (see ./codex-guard-presence.ts) rather than being folded into this check.
|
|
33
|
+
*/
|
|
34
|
+
/** What one repo's Codex trust looks like right now. Data-only → a class, per CLAUDE.md. */
|
|
35
|
+
class CodexTrustStatus {
|
|
36
|
+
configPath;
|
|
37
|
+
configExists;
|
|
38
|
+
projectTrusted;
|
|
39
|
+
hooksPath;
|
|
40
|
+
registeredEntries;
|
|
41
|
+
trustedEntries;
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
43
|
+
constructor(
|
|
44
|
+
/** `~/.codex/config.toml`, named even when it does not exist so the report can point at it. */
|
|
45
|
+
configPath, configExists,
|
|
46
|
+
/** `[projects."<root>"] trust_level = "trusted"`. */
|
|
47
|
+
projectTrusted,
|
|
48
|
+
/** The repo's `.codex/hooks.json`, absolute — the key every `hooks.state` entry is prefixed by. */
|
|
49
|
+
hooksPath,
|
|
50
|
+
/** How many webpieces-managed PreToolUse entries that file registers. 0 ⇒ Codex is not armed. */
|
|
51
|
+
registeredEntries,
|
|
52
|
+
/** How many of them `~/.codex/config.toml` records a `trusted_hash` for. */
|
|
53
|
+
trustedEntries) {
|
|
54
|
+
this.configPath = configPath;
|
|
55
|
+
this.configExists = configExists;
|
|
56
|
+
this.projectTrusted = projectTrusted;
|
|
57
|
+
this.hooksPath = hooksPath;
|
|
58
|
+
this.registeredEntries = registeredEntries;
|
|
59
|
+
this.trustedEntries = trustedEntries;
|
|
60
|
+
}
|
|
61
|
+
/** True when Codex is armed here AND every entry it would run is trusted. */
|
|
62
|
+
fullyTrusted() {
|
|
63
|
+
return this.registeredEntries > 0 && this.trustedEntries >= this.registeredEntries && this.projectTrusted;
|
|
64
|
+
}
|
|
65
|
+
/** True when there is nothing to say — this repo has not armed Codex at all. */
|
|
66
|
+
notArmed() {
|
|
67
|
+
return this.registeredEntries === 0;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The report, as lines. It states what IS, then the one action that changes it — and the action is
|
|
71
|
+
* always the human's, because nothing here can be repaired by a tool.
|
|
72
|
+
*/
|
|
73
|
+
lines() {
|
|
74
|
+
if (this.notArmed())
|
|
75
|
+
return [];
|
|
76
|
+
if (this.fullyTrusted()) {
|
|
77
|
+
return [` ✅ Codex trusts all ${String(this.registeredEntries)} webpieces hook(s) in ${this.hooksPath}`];
|
|
78
|
+
}
|
|
79
|
+
const out = [];
|
|
80
|
+
out.push(` ⚠️ Codex has NOT yet trusted the webpieces hooks in ${this.hooksPath}`);
|
|
81
|
+
if (!this.configExists) {
|
|
82
|
+
out.push(` ${this.configPath} does not exist yet — Codex has never run for this user.`);
|
|
83
|
+
}
|
|
84
|
+
else if (!this.projectTrusted) {
|
|
85
|
+
out.push(' this project is not marked trusted in ~/.codex/config.toml.');
|
|
86
|
+
}
|
|
87
|
+
if (this.trustedEntries < this.registeredEntries) {
|
|
88
|
+
out.push(` ${String(this.trustedEntries)} of ${String(this.registeredEntries)} hook entries carry a trusted_hash.`);
|
|
89
|
+
}
|
|
90
|
+
out.push(' Fix: run `codex` in this repo and choose "Trust all" when it asks about hooks.');
|
|
91
|
+
out.push(' Until you do, its third option ("Continue without trusting") leaves the session UNGUARDED —');
|
|
92
|
+
out.push(' which is what the guard-presence check on the build exists to catch.');
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.CodexTrustStatus = CodexTrustStatus;
|
|
97
|
+
/**
|
|
98
|
+
* Reads Codex's trust state. Never writes, never throws — an unreadable or absent config reports as
|
|
99
|
+
* "not trusted", which is the true and safe answer.
|
|
100
|
+
*/
|
|
101
|
+
class CodexTrustProbe {
|
|
102
|
+
/** `~/.codex/config.toml`. `homeDir` is injectable so a unit test never reads the real one. */
|
|
103
|
+
configPath(homeDir = (0, os_1.homedir)()) {
|
|
104
|
+
return path.join(homeDir, '.codex', 'config.toml');
|
|
105
|
+
}
|
|
106
|
+
read(projectRoot, homeDir = (0, os_1.homedir)()) {
|
|
107
|
+
const configPath = this.configPath(homeDir);
|
|
108
|
+
const hooksPath = path.join(projectRoot, ...hook_registration_1.CODEX_REGISTRATION.settingsFiles[0].split('/'));
|
|
109
|
+
const registered = this.registeredEntryCount(hooksPath);
|
|
110
|
+
const raw = this.readOrEmpty(configPath);
|
|
111
|
+
return new CodexTrustStatus(configPath, raw !== null, this.projectTrusted(raw ?? '', projectRoot), hooksPath, registered, this.trustedEntryCount(raw ?? '', hooksPath));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* How many webpieces-managed PreToolUse commands `.codex/hooks.json` registers.
|
|
115
|
+
*
|
|
116
|
+
* It counts what WE own, not every hook in the file: a consumer's own unrelated hook is none of this
|
|
117
|
+
* check's business, and counting it would make the report say "1 of 3 trusted" about hooks webpieces
|
|
118
|
+
* neither wrote nor can advise on.
|
|
119
|
+
*/
|
|
120
|
+
registeredEntryCount(hooksPath) {
|
|
121
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
122
|
+
try {
|
|
123
|
+
if (!fs.existsSync(hooksPath))
|
|
124
|
+
return 0;
|
|
125
|
+
const entries = (0, hook_registration_1.readSettings)(hooksPath).hooks?.PreToolUse ?? [];
|
|
126
|
+
return entries.reduce((total, entry) => total + entry.hooks.filter((h) => (0, hook_registration_1.isManagedCommand)(h.command)).length, 0);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const error = (0, to_error_1.toError)(err);
|
|
130
|
+
void error; // an unreadable/invalid hooks.json is "not armed"; the drift check is what reports that
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
readOrEmpty(configPath) {
|
|
135
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
136
|
+
try {
|
|
137
|
+
return fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : null;
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
const error = (0, to_error_1.toError)(err);
|
|
141
|
+
void error; // unreadable → treated as absent, which reports as "not trusted"
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* `[projects."<root>"]` carrying `trust_level = "trusted"`.
|
|
147
|
+
*
|
|
148
|
+
* A LINE SCAN, not a TOML parser, and deliberately: this package must load on a tree too broken to
|
|
149
|
+
* build a DI container, so it takes no dependency it does not need, and the two shapes it reads are
|
|
150
|
+
* both written by Codex itself in one fixed form. It over-reports "not trusted" for anything it
|
|
151
|
+
* cannot recognise, which is the safe direction — the consequence is one advisory line too many,
|
|
152
|
+
* never a session reported as guarded when it is not.
|
|
153
|
+
*/
|
|
154
|
+
projectTrusted(raw, projectRoot) {
|
|
155
|
+
return this.sectionBody(raw, `projects."${projectRoot}"`)
|
|
156
|
+
.some((line) => /^\s*trust_level\s*=\s*"trusted"\s*$/.test(line));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* How many `hooks.state."<hooksPath>:pre_tool_use:<group>:<index>"` sections carry a `trusted_hash`.
|
|
160
|
+
*
|
|
161
|
+
* The count is compared against the number of entries the file registers rather than matched
|
|
162
|
+
* one-for-one to a specific group/index: the group and index are Codex's own numbering of a file it
|
|
163
|
+
* re-reads on every launch, and an installer that assumed a numbering would report a false alarm the
|
|
164
|
+
* first time Codex renumbered.
|
|
165
|
+
*/
|
|
166
|
+
trustedEntryCount(raw, hooksPath) {
|
|
167
|
+
const prefix = `hooks.state."${hooksPath}:pre_tool_use:`;
|
|
168
|
+
let trusted = 0;
|
|
169
|
+
let inEntry = false;
|
|
170
|
+
for (const line of raw.split('\n')) {
|
|
171
|
+
const header = this.headerOf(line);
|
|
172
|
+
if (header !== null)
|
|
173
|
+
inEntry = header.startsWith(prefix);
|
|
174
|
+
else if (inEntry && /^\s*trusted_hash\s*=\s*"/.test(line))
|
|
175
|
+
trusted += 1;
|
|
176
|
+
}
|
|
177
|
+
return trusted;
|
|
178
|
+
}
|
|
179
|
+
/** The body lines of one TOML section, or [] when the file carries no such header. */
|
|
180
|
+
sectionBody(raw, wanted) {
|
|
181
|
+
const body = [];
|
|
182
|
+
let inside = false;
|
|
183
|
+
for (const line of raw.split('\n')) {
|
|
184
|
+
const header = this.headerOf(line);
|
|
185
|
+
if (header !== null)
|
|
186
|
+
inside = header === wanted;
|
|
187
|
+
else if (inside)
|
|
188
|
+
body.push(line);
|
|
189
|
+
}
|
|
190
|
+
return body;
|
|
191
|
+
}
|
|
192
|
+
/** The `<name>` of a `[<name>]` header line, or null for anything else. */
|
|
193
|
+
headerOf(line) {
|
|
194
|
+
const match = /^\s*\[([^\]]+)\]\s*$/.exec(line);
|
|
195
|
+
return match === null ? null : match[1];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
exports.CodexTrustProbe = CodexTrustProbe;
|
|
199
|
+
//# sourceMappingURL=codex-trust.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codex-trust.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/codex-trust.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { homedir } from 'os';\n\nimport { toError } from '../core/to-error';\nimport { CODEX_REGISTRATION, HookCommand, HookEntry, isManagedCommand, readSettings } from './hook-registration';\n\n/**\n * CODEX HOOK TRUST — read it, report it, NEVER write it.\n *\n * ─── What Codex actually does ─────────────────────────────────────────────────────────────────────\n * Codex trusts a hook entry TOFU. On first sight of a new or changed entry it PROMPTS\n * (`Hooks need review / 1 hook is new or changed`) and, if the human accepts, records a hash in\n * `~/.codex/config.toml`:\n *\n * [hooks.state.\"<abs path to .codex/hooks.json>:pre_tool_use:<group>:<index>\"]\n * trusted_hash = \"sha256:…\"\n *\n * ─── Why this only ever REPORTS ───────────────────────────────────────────────────────────────────\n * The hash is NOT reproducible from outside Codex. Sixteen encodings were tried against a hooks.json we\n * wrote ourselves and none of them produced the recorded value. So an installer that tried to write one\n * would be guessing at a security decision on a human's behalf, and would either be ignored or — worse —\n * appear to succeed. Forging it is not on the table even if the encoding were known: the prompt IS the\n * mechanism, and the whole point of TOFU is that a person saw the command once.\n *\n * The consequence the installer must therefore SAY OUT LOUD: the prompt's third option is\n * `Continue without trusting (hooks won't run)`. That is one keystroke to a fully unguarded session,\n * with no later warning of any kind. Install-time verification cannot see that choice — it happens\n * afterwards, in another process — which is exactly why guard-presence attestation exists as a separate\n * mechanism (see ./codex-guard-presence.ts) rather than being folded into this check.\n */\n\n/** What one repo's Codex trust looks like right now. Data-only → a class, per CLAUDE.md. */\nexport class CodexTrustStatus {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n /** `~/.codex/config.toml`, named even when it does not exist so the report can point at it. */\n readonly configPath: string,\n readonly configExists: boolean,\n /** `[projects.\"<root>\"] trust_level = \"trusted\"`. */\n readonly projectTrusted: boolean,\n /** The repo's `.codex/hooks.json`, absolute — the key every `hooks.state` entry is prefixed by. */\n readonly hooksPath: string,\n /** How many webpieces-managed PreToolUse entries that file registers. 0 ⇒ Codex is not armed. */\n readonly registeredEntries: number,\n /** How many of them `~/.codex/config.toml` records a `trusted_hash` for. */\n readonly trustedEntries: number,\n ) {}\n\n /** True when Codex is armed here AND every entry it would run is trusted. */\n fullyTrusted(): boolean {\n return this.registeredEntries > 0 && this.trustedEntries >= this.registeredEntries && this.projectTrusted;\n }\n\n /** True when there is nothing to say — this repo has not armed Codex at all. */\n notArmed(): boolean {\n return this.registeredEntries === 0;\n }\n\n /**\n * The report, as lines. It states what IS, then the one action that changes it — and the action is\n * always the human's, because nothing here can be repaired by a tool.\n */\n lines(): readonly string[] {\n if (this.notArmed()) return [];\n if (this.fullyTrusted()) {\n return [` ✅ Codex trusts all ${String(this.registeredEntries)} webpieces hook(s) in ${this.hooksPath}`];\n }\n const out: string[] = [];\n out.push(` ⚠️ Codex has NOT yet trusted the webpieces hooks in ${this.hooksPath}`);\n if (!this.configExists) {\n out.push(` ${this.configPath} does not exist yet — Codex has never run for this user.`);\n } else if (!this.projectTrusted) {\n out.push(' this project is not marked trusted in ~/.codex/config.toml.');\n }\n if (this.trustedEntries < this.registeredEntries) {\n out.push(` ${String(this.trustedEntries)} of ${String(this.registeredEntries)} hook entries carry a trusted_hash.`);\n }\n out.push(' Fix: run `codex` in this repo and choose \"Trust all\" when it asks about hooks.');\n out.push(' Until you do, its third option (\"Continue without trusting\") leaves the session UNGUARDED —');\n out.push(' which is what the guard-presence check on the build exists to catch.');\n return out;\n }\n}\n\n/**\n * Reads Codex's trust state. Never writes, never throws — an unreadable or absent config reports as\n * \"not trusted\", which is the true and safe answer.\n */\nexport class CodexTrustProbe {\n /** `~/.codex/config.toml`. `homeDir` is injectable so a unit test never reads the real one. */\n configPath(homeDir: string = homedir()): string {\n return path.join(homeDir, '.codex', 'config.toml');\n }\n\n read(projectRoot: string, homeDir: string = homedir()): CodexTrustStatus {\n const configPath = this.configPath(homeDir);\n const hooksPath = path.join(projectRoot, ...CODEX_REGISTRATION.settingsFiles[0].split('/'));\n const registered = this.registeredEntryCount(hooksPath);\n const raw = this.readOrEmpty(configPath);\n return new CodexTrustStatus(\n configPath, raw !== null, this.projectTrusted(raw ?? '', projectRoot),\n hooksPath, registered, this.trustedEntryCount(raw ?? '', hooksPath),\n );\n }\n\n /**\n * How many webpieces-managed PreToolUse commands `.codex/hooks.json` registers.\n *\n * It counts what WE own, not every hook in the file: a consumer's own unrelated hook is none of this\n * check's business, and counting it would make the report say \"1 of 3 trusted\" about hooks webpieces\n * neither wrote nor can advise on.\n */\n private registeredEntryCount(hooksPath: string): number {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (!fs.existsSync(hooksPath)) return 0;\n const entries: readonly HookEntry[] = readSettings(hooksPath).hooks?.PreToolUse ?? [];\n return entries.reduce(\n (total: number, entry: HookEntry): number =>\n total + entry.hooks.filter((h: HookCommand): boolean => isManagedCommand(h.command)).length,\n 0,\n );\n } catch (err: unknown) {\n const error = toError(err);\n void error; // an unreadable/invalid hooks.json is \"not armed\"; the drift check is what reports that\n return 0;\n }\n }\n\n private readOrEmpty(configPath: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : null;\n } catch (err: unknown) {\n const error = toError(err);\n void error; // unreadable → treated as absent, which reports as \"not trusted\"\n return null;\n }\n }\n\n /**\n * `[projects.\"<root>\"]` carrying `trust_level = \"trusted\"`.\n *\n * A LINE SCAN, not a TOML parser, and deliberately: this package must load on a tree too broken to\n * build a DI container, so it takes no dependency it does not need, and the two shapes it reads are\n * both written by Codex itself in one fixed form. It over-reports \"not trusted\" for anything it\n * cannot recognise, which is the safe direction — the consequence is one advisory line too many,\n * never a session reported as guarded when it is not.\n */\n private projectTrusted(raw: string, projectRoot: string): boolean {\n return this.sectionBody(raw, `projects.\"${projectRoot}\"`)\n .some((line: string): boolean => /^\\s*trust_level\\s*=\\s*\"trusted\"\\s*$/.test(line));\n }\n\n /**\n * How many `hooks.state.\"<hooksPath>:pre_tool_use:<group>:<index>\"` sections carry a `trusted_hash`.\n *\n * The count is compared against the number of entries the file registers rather than matched\n * one-for-one to a specific group/index: the group and index are Codex's own numbering of a file it\n * re-reads on every launch, and an installer that assumed a numbering would report a false alarm the\n * first time Codex renumbered.\n */\n private trustedEntryCount(raw: string, hooksPath: string): number {\n const prefix = `hooks.state.\"${hooksPath}:pre_tool_use:`;\n let trusted = 0;\n let inEntry = false;\n for (const line of raw.split('\\n')) {\n const header = this.headerOf(line);\n if (header !== null) inEntry = header.startsWith(prefix);\n else if (inEntry && /^\\s*trusted_hash\\s*=\\s*\"/.test(line)) trusted += 1;\n }\n return trusted;\n }\n\n /** The body lines of one TOML section, or [] when the file carries no such header. */\n private sectionBody(raw: string, wanted: string): readonly string[] {\n const body: string[] = [];\n let inside = false;\n for (const line of raw.split('\\n')) {\n const header = this.headerOf(line);\n if (header !== null) inside = header === wanted;\n else if (inside) body.push(line);\n }\n return body;\n }\n\n /** The `<name>` of a `[<name>]` header line, or null for anything else. */\n private headerOf(line: string): string | null {\n const match = /^\\s*\\[([^\\]]+)\\]\\s*$/.exec(line);\n return match === null ? null : match[1];\n }\n}\n"]}
|