@webpieces/ai-hook-rules 0.4.734 → 0.4.736
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/bin/codex-trust.d.ts +8 -2
- package/src/bin/codex-trust.js +10 -4
- package/src/bin/codex-trust.js.map +1 -1
- package/src/index.d.ts +0 -1
- package/src/index.js +1 -8
- package/src/index.js.map +1 -1
- package/src/bin/codex-guard-presence.d.ts +0 -112
- package/src/bin/codex-guard-presence.js +0 -190
- package/src/bin/codex-guard-presence.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/ai-hook-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.736",
|
|
4
4
|
"description": "Pluggable write-time validation framework for AI coding agents (@webpieces/ai-hook-rules). Claude Code PreToolUse + openclaw before_tool_call adapters share one rule engine.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"directory": "packages/tooling/ai-hook-rules"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@webpieces/rules-config": "0.4.
|
|
28
|
+
"@webpieces/rules-config": "0.4.736",
|
|
29
29
|
"inversify": "7.10.4",
|
|
30
30
|
"reflect-metadata": "0.2.2"
|
|
31
31
|
},
|
package/src/bin/codex-trust.d.ts
CHANGED
|
@@ -19,8 +19,14 @@
|
|
|
19
19
|
* The consequence the installer must therefore SAY OUT LOUD: the prompt's third option is
|
|
20
20
|
* `Continue without trusting (hooks won't run)`. That is one keystroke to a fully unguarded session,
|
|
21
21
|
* with no later warning of any kind. Install-time verification cannot see that choice — it happens
|
|
22
|
-
* afterwards, in another process —
|
|
23
|
-
*
|
|
22
|
+
* afterwards, in another process — so this check reports the trust state it CAN read, names the file it
|
|
23
|
+
* read it from, and stops there.
|
|
24
|
+
*
|
|
25
|
+
* Declining the prompt is a HUMAN's decision about their own machine, and it is deliberately left
|
|
26
|
+
* standing. Tooling that tried to detect and refuse it afterwards would be voiding a setting a person
|
|
27
|
+
* chose on purpose, and it has nothing to catch besides: the guards constrain the AGENT, while the
|
|
28
|
+
* prompt is answered by the HUMAN, who the agent cannot impersonate. So there is no second mechanism
|
|
29
|
+
* downstream of this one — this report is the whole of what webpieces knows about Codex trust.
|
|
24
30
|
*/
|
|
25
31
|
/** What one repo's Codex trust looks like right now. Data-only → a class, per CLAUDE.md. */
|
|
26
32
|
export declare class CodexTrustStatus {
|
package/src/bin/codex-trust.js
CHANGED
|
@@ -28,8 +28,14 @@ const hook_registration_1 = require("./hook-registration");
|
|
|
28
28
|
* The consequence the installer must therefore SAY OUT LOUD: the prompt's third option is
|
|
29
29
|
* `Continue without trusting (hooks won't run)`. That is one keystroke to a fully unguarded session,
|
|
30
30
|
* with no later warning of any kind. Install-time verification cannot see that choice — it happens
|
|
31
|
-
* afterwards, in another process —
|
|
32
|
-
*
|
|
31
|
+
* afterwards, in another process — so this check reports the trust state it CAN read, names the file it
|
|
32
|
+
* read it from, and stops there.
|
|
33
|
+
*
|
|
34
|
+
* Declining the prompt is a HUMAN's decision about their own machine, and it is deliberately left
|
|
35
|
+
* standing. Tooling that tried to detect and refuse it afterwards would be voiding a setting a person
|
|
36
|
+
* chose on purpose, and it has nothing to catch besides: the guards constrain the AGENT, while the
|
|
37
|
+
* prompt is answered by the HUMAN, who the agent cannot impersonate. So there is no second mechanism
|
|
38
|
+
* downstream of this one — this report is the whole of what webpieces knows about Codex trust.
|
|
33
39
|
*/
|
|
34
40
|
/** What one repo's Codex trust looks like right now. Data-only → a class, per CLAUDE.md. */
|
|
35
41
|
class CodexTrustStatus {
|
|
@@ -88,8 +94,8 @@ class CodexTrustStatus {
|
|
|
88
94
|
out.push(` ${String(this.trustedEntries)} of ${String(this.registeredEntries)} hook entries carry a trusted_hash.`);
|
|
89
95
|
}
|
|
90
96
|
out.push(' Fix: run `codex` in this repo and choose "Trust all" when it asks about hooks.');
|
|
91
|
-
out.push('
|
|
92
|
-
out.push('
|
|
97
|
+
out.push(' Its third option ("Continue without trusting") leaves the session UNGUARDED for its');
|
|
98
|
+
out.push(' whole life, and nothing later in the run reports that — this message is the only notice.');
|
|
93
99
|
return out;
|
|
94
100
|
}
|
|
95
101
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codex-trust.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/codex-trust.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAC7B,2BAA6B;AAE7B,+CAA2C;AAC3C,2DAAiH;AAEjH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,4FAA4F;AAC5F,MAAa,gBAAgB;IAIZ;IACA;IAEA;IAEA;IAEA;IAEA;IAZb,yDAAyD;IACzD;IACI,+FAA+F;IACtF,UAAkB,EAClB,YAAqB;IAC9B,qDAAqD;IAC5C,cAAuB;IAChC,mGAAmG;IAC1F,SAAiB;IAC1B,iGAAiG;IACxF,iBAAyB;IAClC,4EAA4E;IACnE,cAAsB;QATtB,eAAU,GAAV,UAAU,CAAQ;QAClB,iBAAY,GAAZ,YAAY,CAAS;QAErB,mBAAc,GAAd,cAAc,CAAS;QAEvB,cAAS,GAAT,SAAS,CAAQ;QAEjB,sBAAiB,GAAjB,iBAAiB,CAAQ;QAEzB,mBAAc,GAAd,cAAc,CAAQ;IAChC,CAAC;IAEJ,6EAA6E;IAC7E,YAAY;QACR,OAAO,IAAI,CAAC,iBAAiB,GAAG,CAAC,IAAI,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC,iBAAiB,IAAI,IAAI,CAAC,cAAc,CAAC;IAC9G,CAAC;IAED,gFAAgF;IAChF,QAAQ;QACJ,OAAO,IAAI,CAAC,iBAAiB,KAAK,CAAC,CAAC;IACxC,CAAC;IAED;;;OAGG;IACH,KAAK;QACD,IAAI,IAAI,CAAC,QAAQ,EAAE;YAAE,OAAO,EAAE,CAAC;QAC/B,IAAI,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC;YACtB,OAAO,CAAC,wBAAwB,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,yBAAyB,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QAC7G,CAAC;QACD,MAAM,GAAG,GAAa,EAAE,CAAC;QACzB,GAAG,CAAC,IAAI,CAAC,0DAA0D,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QACrF,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;YACrB,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,UAAU,0DAA0D,CAAC,CAAC;QAChG,CAAC;aAAM,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;YAC9B,GAAG,CAAC,IAAI,CAAC,kEAAkE,CAAC,CAAC;QACjF,CAAC;QACD,IAAI,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAC/C,GAAG,CAAC,IAAI,CAAC,QAAQ,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,OAAO,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,qCAAqC,CAAC,CAAC;QAC5H,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,qFAAqF,CAAC,CAAC;QAChG,GAAG,CAAC,IAAI,CAAC,kGAAkG,CAAC,CAAC;QAC7G,GAAG,CAAC,IAAI,CAAC,2EAA2E,CAAC,CAAC;QACtF,OAAO,GAAG,CAAC;IACf,CAAC;CACJ;AAlDD,4CAkDC;AAED;;;GAGG;AACH,MAAa,eAAe;IACxB,+FAA+F;IAC/F,UAAU,CAAC,UAAkB,IAAA,YAAO,GAAE;QAClC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC;IACvD,CAAC;IAED,IAAI,CAAC,WAAmB,EAAE,UAAkB,IAAA,YAAO,GAAE;QACjD,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAC5C,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,sCAAkB,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QAC5F,MAAM,UAAU,GAAG,IAAI,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAAC;QACxD,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;QACzC,OAAO,IAAI,gBAAgB,CACvB,UAAU,EAAE,GAAG,KAAK,IAAI,EAAE,IAAI,CAAC,cAAc,CAAC,GAAG,IAAI,EAAE,EAAE,WAAW,CAAC,EACrE,SAAS,EAAE,UAAU,EAAE,IAAI,CAAC,iBAAiB,CAAC,GAAG,IAAI,EAAE,EAAE,SAAS,CAAC,CACtE,CAAC;IACN,CAAC;IAED;;;;;;OAMG;IACK,oBAAoB,CAAC,SAAiB;QAC1C,8DAA8D;QAC9D,IAAI,CAAC;YACD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;gBAAE,OAAO,CAAC,CAAC;YACxC,MAAM,OAAO,GAAyB,IAAA,gCAAY,EAAC,SAAS,CAAC,CAAC,KAAK,EAAE,UAAU,IAAI,EAAE,CAAC;YACtF,OAAO,OAAO,CAAC,MAAM,CACjB,CAAC,KAAa,EAAE,KAAgB,EAAU,EAAE,CACxC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAc,EAAW,EAAE,CAAC,IAAA,oCAAgB,EAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAC/F,CAAC,CACJ,CAAC;QACN,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC,CAAC,wFAAwF;YACpG,OAAO,CAAC,CAAC;QACb,CAAC;IACL,CAAC;IAEO,WAAW,CAAC,UAAkB;QAClC,8DAA8D;QAC9D,IAAI,CAAC;YACD,OAAO,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAClF,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC,CAAC,iEAAiE;YAC7E,OAAO,IAAI,CAAC;QAChB,CAAC;IACL,CAAC;IAED;;;;;;;;OAQG;IACK,cAAc,CAAC,GAAW,EAAE,WAAmB;QACnD,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,aAAa,WAAW,GAAG,CAAC;aACpD,IAAI,CAAC,CAAC,IAAY,EAAW,EAAE,CAAC,qCAAqC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3F,CAAC;IAED;;;;;;;OAOG;IACK,iBAAiB,CAAC,GAAW,EAAE,SAAiB;QACpD,MAAM,MAAM,GAAG,gBAAgB,SAAS,gBAAgB,CAAC;QACzD,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YACnC,IAAI,MAAM,KAAK,IAAI;gBAAE,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;iBACpD,IAAI,OAAO,IAAI,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAC,CAAC;QAC5E,CAAC;QACD,OAAO,OAAO,CAAC;IACnB,CAAC;IAED,sFAAsF;IAC9E,WAAW,CAAC,GAAW,EAAE,MAAc;QAC3C,MAAM,IAAI,GAAa,EAAE,CAAC;QAC1B,IAAI,MAAM,GAAG,KAAK,CAAC;QACnB,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YACnC,IAAI,MAAM,KAAK,IAAI;gBAAE,MAAM,GAAG,MAAM,KAAK,MAAM,CAAC;iBAC3C,IAAI,MAAM;gBAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrC,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,2EAA2E;IACnE,QAAQ,CAAC,IAAY;QACzB,MAAM,KAAK,GAAG,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChD,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC5C,CAAC;CACJ;AAvGD,0CAuGC","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"]}
|
|
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 — so this check reports the trust state it CAN read, names the file it\n * read it from, and stops there.\n *\n * Declining the prompt is a HUMAN's decision about their own machine, and it is deliberately left\n * standing. Tooling that tried to detect and refuse it afterwards would be voiding a setting a person\n * chose on purpose, and it has nothing to catch besides: the guards constrain the AGENT, while the\n * prompt is answered by the HUMAN, who the agent cannot impersonate. So there is no second mechanism\n * downstream of this one — this report is the whole of what webpieces knows about Codex trust.\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(' Its third option (\"Continue without trusting\") leaves the session UNGUARDED for its');\n out.push(' whole life, and nothing later in the run reports that — this message is the only notice.');\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"]}
|
package/src/index.d.ts
CHANGED
|
@@ -26,4 +26,3 @@ export { run } from './core/runner';
|
|
|
26
26
|
export { stripTsNoise } from './core/strip-ts-noise';
|
|
27
27
|
export { parseDirectives, DirectiveIndex, createIsLineDisabled } from './core/disable-directives';
|
|
28
28
|
export { formatReport, ReportSubject, WRITE_SUBJECT, READ_SUBJECT, BASH_SUBJECT } from './core/report';
|
|
29
|
-
export { CodexGuardPresence, CodexSessionDetector, GuardPresenceVerdict } from './bin/codex-guard-presence';
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WRITE_SUBJECT = exports.ReportSubject = exports.formatReport = exports.createIsLineDisabled = exports.DirectiveIndex = exports.parseDirectives = exports.stripTsNoise = exports.run = exports.FeatureBranchGuardRule = exports.NoJsFilesRule = exports.RedirectHowToMergeMainRule = exports.PrMergeGuardRule = exports.MergeInProgressGuardRule = exports.PrCreationOrPushGuardRule = exports.BranchCreationGuardRule = exports.NoProcessExitOutsideMainRule = exports.NoSymbolDiTokensRule = exports.ThrowCauseRequiredRule = exports.CatchErrorPatternRule = exports.NoUnmanagedExceptionsRule = exports.RequireReturnTypeRule = exports.NoDestructureRule = exports.ValidateTsInSrcRule = exports.MaxFileLinesRule = exports.NoImplicitAnyRule = exports.NoAnyUnknownRule = exports.CustomRuleAdapter = exports.EmptyRuleConfig = exports.BashRuleBase = exports.FileRuleBase = exports.EditRuleBase = exports.DisableEscape = exports.FixHint = exports.detectAiType = exports.AI_TYPE_TOKEN_SH = exports.AI_TYPE_SH = exports.FileOperation = exports.AgentHookEvent = exports.AI_TYPE_UNKNOWN = exports.AI_TYPES = exports.ResolvedRuleConfig = exports.ResolvedConfig = exports.BlockedResult = exports.RuleGroup = exports.BashContext = exports.FileContext = exports.EditContext = exports.NormalizedToolInput = exports.NormalizedEdit = exports.Violation = void 0;
|
|
4
|
-
exports.
|
|
4
|
+
exports.BASH_SUBJECT = exports.READ_SUBJECT = void 0;
|
|
5
5
|
// Pluggable write-time validation framework for AI coding agents
|
|
6
6
|
var types_1 = require("./core/types");
|
|
7
7
|
Object.defineProperty(exports, "Violation", { enumerable: true, get: function () { return types_1.Violation; } });
|
|
@@ -91,11 +91,4 @@ Object.defineProperty(exports, "ReportSubject", { enumerable: true, get: functio
|
|
|
91
91
|
Object.defineProperty(exports, "WRITE_SUBJECT", { enumerable: true, get: function () { return report_1.WRITE_SUBJECT; } });
|
|
92
92
|
Object.defineProperty(exports, "READ_SUBJECT", { enumerable: true, get: function () { return report_1.READ_SUBJECT; } });
|
|
93
93
|
Object.defineProperty(exports, "BASH_SUBJECT", { enumerable: true, get: function () { return report_1.BASH_SUBJECT; } });
|
|
94
|
-
// Guard-presence attestation. Exported because the GATE that consumes it lives in @webpieces/pr-gate
|
|
95
|
-
// (BuildAffected.runBuildGate) — the check and its refusal belong here, beside the L0 shim stream that
|
|
96
|
-
// is their evidence, and only the throw belongs over there.
|
|
97
|
-
var codex_guard_presence_1 = require("./bin/codex-guard-presence");
|
|
98
|
-
Object.defineProperty(exports, "CodexGuardPresence", { enumerable: true, get: function () { return codex_guard_presence_1.CodexGuardPresence; } });
|
|
99
|
-
Object.defineProperty(exports, "CodexSessionDetector", { enumerable: true, get: function () { return codex_guard_presence_1.CodexSessionDetector; } });
|
|
100
|
-
Object.defineProperty(exports, "GuardPresenceVerdict", { enumerable: true, get: function () { return codex_guard_presence_1.GuardPresenceVerdict; } });
|
|
101
94
|
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/tooling/ai-hook-rules/src/index.ts"],"names":[],"mappings":";;;;AAAA,iEAAiE;AACjE,sCAOsB;AALlB,kGAAA,SAAS,OAAA;AAAE,uGAAA,cAAc,OAAA;AAAE,4GAAA,mBAAmB,OAAA;AAC9C,oGAAA,WAAW,OAAA;AAAE,oGAAA,WAAW,OAAA;AAAE,oGAAA,WAAW,OAAA;AAErC,kGAAA,SAAS,OAAA;AAAE,sGAAA,aAAa,OAAA;AACxB,uGAAA,cAAc,OAAA;AAAE,2GAAA,kBAAkB,OAAA;AAGtC,oGAAoG;AACpG,oGAAoG;AACpG,yDAAyD;AACzD,kDAAsH;AAArG,uGAAA,QAAQ,OAAA;AAAE,8GAAA,eAAe,OAAA;AAAkB,6GAAA,cAAc,OAAA;AAAE,4GAAA,aAAa,OAAA;AACzF,kDAAkF;AAAzE,uGAAA,UAAU,OAAA;AAAE,6GAAA,gBAAgB,OAAA;AAAE,yGAAA,YAAY,OAAA;AAEnD,iGAAiG;AACjG,yFAAyF;AACzF,gEAAgE;AAChE,4CAAyD;AAAhD,mGAAA,OAAO,OAAA;AAAE,yGAAA,aAAa,OAAA;AAE/B,gGAAgG;AAChG,8CAA6F;AAApF,yGAAA,YAAY,OAAA;AAAE,yGAAA,YAAY,OAAA;AAAE,yGAAA,YAAY,OAAA;AAAE,4GAAA,eAAe,OAAA;AAClE,kEAA+D;AAAtD,wHAAA,iBAAiB,OAAA;AAE1B,+FAA+F;AAC/F,8DAA+D;AAAtD,kHAAA,gBAAgB,OAAA;AACzB,gEAAiE;AAAxD,oHAAA,iBAAiB,OAAA;AAC1B,8DAA+D;AAAtD,kHAAA,gBAAgB,OAAA;AACzB,sEAAsE;AAA7D,yHAAA,mBAAmB,OAAA;AAC5B,8DAAgE;AAAvD,mHAAA,iBAAiB,OAAA;AAC1B,wEAAyE;AAAhE,4HAAA,qBAAqB,OAAA;AAC9B,gFAAiF;AAAxE,oIAAA,yBAAyB,OAAA;AAClC,wEAAyE;AAAhE,4HAAA,qBAAqB,OAAA;AAC9B,0EAA2E;AAAlE,8HAAA,sBAAsB,OAAA;AAC/B,wEAAwE;AAA/D,2HAAA,oBAAoB,OAAA;AAC7B,0FAAyF;AAAhF,4IAAA,4BAA4B,OAAA;AACrC,4EAA6E;AAApE,gIAAA,uBAAuB,OAAA;AAChC,oFAAmF;AAA1E,sIAAA,yBAAyB,OAAA;AAClC,gFAAgF;AAAvE,mIAAA,wBAAwB,OAAA;AACjC,8DAA+D;AAAtD,kHAAA,gBAAgB,OAAA;AACzB,sFAAqF;AAA5E,wIAAA,0BAA0B,OAAA;AACnC,wDAAyD;AAAhD,4GAAA,aAAa,OAAA;AACtB,0EAA2E;AAAlE,8HAAA,sBAAsB,OAAA;AAE/B,wCAAoC;AAA3B,6FAAA,GAAG,OAAA;AACZ,wDAAqD;AAA5C,8GAAA,YAAY,OAAA;AACrB,gEAAkG;AAAzF,qHAAA,eAAe,OAAA;AAAE,oHAAA,cAAc,OAAA;AAAE,0HAAA,oBAAoB,OAAA;AAC9D,wCAAuG;AAA9F,sGAAA,YAAY,OAAA;AAAE,uGAAA,aAAa,OAAA;AAAE,uGAAA,aAAa,OAAA;AAAE,sGAAA,YAAY,OAAA;AAAE,sGAAA,YAAY,OAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/tooling/ai-hook-rules/src/index.ts"],"names":[],"mappings":";;;;AAAA,iEAAiE;AACjE,sCAOsB;AALlB,kGAAA,SAAS,OAAA;AAAE,uGAAA,cAAc,OAAA;AAAE,4GAAA,mBAAmB,OAAA;AAC9C,oGAAA,WAAW,OAAA;AAAE,oGAAA,WAAW,OAAA;AAAE,oGAAA,WAAW,OAAA;AAErC,kGAAA,SAAS,OAAA;AAAE,sGAAA,aAAa,OAAA;AACxB,uGAAA,cAAc,OAAA;AAAE,2GAAA,kBAAkB,OAAA;AAGtC,oGAAoG;AACpG,oGAAoG;AACpG,yDAAyD;AACzD,kDAAsH;AAArG,uGAAA,QAAQ,OAAA;AAAE,8GAAA,eAAe,OAAA;AAAkB,6GAAA,cAAc,OAAA;AAAE,4GAAA,aAAa,OAAA;AACzF,kDAAkF;AAAzE,uGAAA,UAAU,OAAA;AAAE,6GAAA,gBAAgB,OAAA;AAAE,yGAAA,YAAY,OAAA;AAEnD,iGAAiG;AACjG,yFAAyF;AACzF,gEAAgE;AAChE,4CAAyD;AAAhD,mGAAA,OAAO,OAAA;AAAE,yGAAA,aAAa,OAAA;AAE/B,gGAAgG;AAChG,8CAA6F;AAApF,yGAAA,YAAY,OAAA;AAAE,yGAAA,YAAY,OAAA;AAAE,yGAAA,YAAY,OAAA;AAAE,4GAAA,eAAe,OAAA;AAClE,kEAA+D;AAAtD,wHAAA,iBAAiB,OAAA;AAE1B,+FAA+F;AAC/F,8DAA+D;AAAtD,kHAAA,gBAAgB,OAAA;AACzB,gEAAiE;AAAxD,oHAAA,iBAAiB,OAAA;AAC1B,8DAA+D;AAAtD,kHAAA,gBAAgB,OAAA;AACzB,sEAAsE;AAA7D,yHAAA,mBAAmB,OAAA;AAC5B,8DAAgE;AAAvD,mHAAA,iBAAiB,OAAA;AAC1B,wEAAyE;AAAhE,4HAAA,qBAAqB,OAAA;AAC9B,gFAAiF;AAAxE,oIAAA,yBAAyB,OAAA;AAClC,wEAAyE;AAAhE,4HAAA,qBAAqB,OAAA;AAC9B,0EAA2E;AAAlE,8HAAA,sBAAsB,OAAA;AAC/B,wEAAwE;AAA/D,2HAAA,oBAAoB,OAAA;AAC7B,0FAAyF;AAAhF,4IAAA,4BAA4B,OAAA;AACrC,4EAA6E;AAApE,gIAAA,uBAAuB,OAAA;AAChC,oFAAmF;AAA1E,sIAAA,yBAAyB,OAAA;AAClC,gFAAgF;AAAvE,mIAAA,wBAAwB,OAAA;AACjC,8DAA+D;AAAtD,kHAAA,gBAAgB,OAAA;AACzB,sFAAqF;AAA5E,wIAAA,0BAA0B,OAAA;AACnC,wDAAyD;AAAhD,4GAAA,aAAa,OAAA;AACtB,0EAA2E;AAAlE,8HAAA,sBAAsB,OAAA;AAE/B,wCAAoC;AAA3B,6FAAA,GAAG,OAAA;AACZ,wDAAqD;AAA5C,8GAAA,YAAY,OAAA;AACrB,gEAAkG;AAAzF,qHAAA,eAAe,OAAA;AAAE,oHAAA,cAAc,OAAA;AAAE,0HAAA,oBAAoB,OAAA;AAC9D,wCAAuG;AAA9F,sGAAA,YAAY,OAAA;AAAE,uGAAA,aAAa,OAAA;AAAE,uGAAA,aAAa,OAAA;AAAE,sGAAA,YAAY,OAAA;AAAE,sGAAA,YAAY,OAAA","sourcesContent":["// Pluggable write-time validation framework for AI coding agents\nexport {\n ToolKind, RuleScope, RuleOptions, IsLineDisabled,\n Violation, NormalizedEdit, NormalizedToolInput,\n EditContext, FileContext, BashContext,\n Rule, PlainRule,\n RuleGroup, BlockedResult,\n ResolvedConfig, ResolvedRuleConfig,\n} from './core/types';\n\n// The ONE normalized hook event every harness's payload is morphed into, and the discriminator that\n// decides which harness produced it. `AgentHookEvent` sits beside the types above because it is the\n// shape the adapters produce and the hook body consumes.\nexport { AiType, AI_TYPES, AI_TYPE_UNKNOWN, AgentEventKind, AgentHookEvent, FileOperation } from './core/agent-event';\nexport { AI_TYPE_SH, AI_TYPE_TOKEN_SH, detectAiType } from './adapters/detect-ai';\n\n// Structured fix guidance shown in blocked reports (violation + mainMessage + options + escape).\n// `Option` is NOT re-exported here — it has ONE home, `@webpieces/rules-config`, because\n// `RuleFailError` carries the same class. Import it from there.\nexport { FixHint, DisableEscape } from './core/fix-hint';\n\n// Scope-specific rule bases (each extends the shared AbstractRule from @webpieces/rules-config)\nexport { EditRuleBase, FileRuleBase, BashRuleBase, EmptyRuleConfig } from './core/rule-base';\nexport { CustomRuleAdapter } from './core/custom-rule-adapter';\n\n// Built-in rule classes — each constructed with its typed *Config from @webpieces/rules-config\nexport { NoAnyUnknownRule } from './core/rules/no-any-unknown';\nexport { NoImplicitAnyRule } from './core/rules/no-implicit-any';\nexport { MaxFileLinesRule } from './core/rules/max-file-lines';\nexport { ValidateTsInSrcRule } from './core/rules/validate-ts-in-src';\nexport { NoDestructureRule } from './core/rules/no-destructure';\nexport { RequireReturnTypeRule } from './core/rules/require-return-type';\nexport { NoUnmanagedExceptionsRule } from './core/rules/no-unmanaged-exceptions';\nexport { CatchErrorPatternRule } from './core/rules/catch-error-pattern';\nexport { ThrowCauseRequiredRule } from './core/rules/throw-cause-required';\nexport { NoSymbolDiTokensRule } from './core/rules/no-symbol-di-tokens';\nexport { NoProcessExitOutsideMainRule } from './core/rules/no-process-exit-outside-main';\nexport { BranchCreationGuardRule } from './core/rules/branch-creation-guard';\nexport { PrCreationOrPushGuardRule } from './core/rules/pr-creation-or-push-guard';\nexport { MergeInProgressGuardRule } from './core/rules/merge-in-progress-guard';\nexport { PrMergeGuardRule } from './core/rules/pr-merge-guard';\nexport { RedirectHowToMergeMainRule } from './core/rules/redirect-how-to-merge-main';\nexport { NoJsFilesRule } from './core/rules/no-js-files';\nexport { FeatureBranchGuardRule } from './core/rules/feature-branch-guard';\n\nexport { run } from './core/runner';\nexport { stripTsNoise } from './core/strip-ts-noise';\nexport { parseDirectives, DirectiveIndex, createIsLineDisabled } from './core/disable-directives';\nexport { formatReport, ReportSubject, WRITE_SUBJECT, READ_SUBJECT, BASH_SUBJECT } from './core/report';\n\n"]}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { Option } from '@webpieces/rules-config';
|
|
2
|
-
/**
|
|
3
|
-
* GUARD-PRESENCE ATTESTATION — "did the guards actually RUN in this Codex session?"
|
|
4
|
-
*
|
|
5
|
-
* ─── The hole this closes ─────────────────────────────────────────────────────────────────────────
|
|
6
|
-
* Codex prompts before running a hook it has not trusted, and the prompt's third option is
|
|
7
|
-
* `Continue without trusting (hooks won't run)`. One keystroke, no later warning, and the session is
|
|
8
|
-
* fully unguarded for its whole life. Nothing at INSTALL time can see that choice — it is made
|
|
9
|
-
* afterwards, in another process — so `codex-trust.ts` reading `~/.codex/config.toml` is necessary and
|
|
10
|
-
* not sufficient.
|
|
11
|
-
*
|
|
12
|
-
* The same check catches a second, quieter failure with no relation to trust: a matcher that names a
|
|
13
|
-
* tool Codex does not emit. That was the live state of every `.codex/hooks.json` a desktop sync had
|
|
14
|
-
* written — `Write|Edit|MultiEdit` against a harness whose file tool is `apply_patch` — and it produced
|
|
15
|
-
* exactly the same symptom, which is no symptom at all.
|
|
16
|
-
*
|
|
17
|
-
* ─── Why the L0 shim log is the evidence ──────────────────────────────────────────────────────────
|
|
18
|
-
* The L0 shim writes one row per tool call, on EVERY path including the healthy one, before anything
|
|
19
|
-
* else can fail. So a session that produced ZERO rows did not have a guard run — that is the whole
|
|
20
|
-
* inference, and it holds no matter WHY (untrusted, wrong matcher, deleted file, unresolvable path).
|
|
21
|
-
*
|
|
22
|
-
* It is deliberately a count of rows and not a check of any row's content: what is being attested is
|
|
23
|
-
* that the hook EXECUTED, not what it decided.
|
|
24
|
-
*
|
|
25
|
-
* ─── Where it is WIRED ────────────────────────────────────────────────────────────────────────────
|
|
26
|
-
* `BuildAffected.runBuildGate` (@webpieces/pr-gate) calls it before resolving the build command, so ALL
|
|
27
|
-
* THREE build entry points are covered by one call: `wp-build`, stage ② (`wp-review-upsert-pr`) and
|
|
28
|
-
* stage ③ (`wp-finish-upsert-pr`). Attesting at the build is the right place because the build is the
|
|
29
|
-
* moment a session's work is about to be claimed as verified — an unguarded session that never builds
|
|
30
|
-
* has produced nothing to trust.
|
|
31
|
-
*
|
|
32
|
-
* It BLOCKS, and blocking is the whole point. This check was deliberately shipped detached for one
|
|
33
|
-
* release rather than half-wired, because a check that detects and never refuses reads as coverage
|
|
34
|
-
* nobody actually has.
|
|
35
|
-
*/
|
|
36
|
-
/**
|
|
37
|
-
* Is THIS process running inside a Codex session?
|
|
38
|
-
*
|
|
39
|
-
* MEASURED (codex-cli 0.151.0), and the negative half matters as much as the positive: there is NO
|
|
40
|
-
* `CODEX_SESSION_ID`. Reaching for one is the obvious thing to do and it does not exist, so the
|
|
41
|
-
* fingerprints are the three that DO: `CODEX_MANAGED_BY_NPM`, `CODEX_MANAGED_PACKAGE_ROOT`, and a
|
|
42
|
-
* `/.codex/tmp/arg0/` entry on `PATH`.
|
|
43
|
-
*
|
|
44
|
-
* Any one of them is enough. They come from different install shapes, and requiring all three would
|
|
45
|
-
* silently answer "not Codex" — which for a check that BLOCKS on absence of evidence is the dangerous
|
|
46
|
-
* direction: it would turn an unguarded session into an unchecked one.
|
|
47
|
-
*/
|
|
48
|
-
export declare class CodexSessionDetector {
|
|
49
|
-
/** The env keys that identify a Codex-managed process. */
|
|
50
|
-
static readonly ENV_KEYS: readonly string[];
|
|
51
|
-
/** The PATH segment Codex injects for its arg0 shims. */
|
|
52
|
-
static readonly PATH_MARKER = "/.codex/tmp/arg0/";
|
|
53
|
-
isCodexSession(env?: NodeJS.ProcessEnv): boolean;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* The answer, with the sentence a human or an agent reads. Data-only → a class, per CLAUDE.md.
|
|
57
|
-
*
|
|
58
|
-
* `cures` are `Option`s rather than "Fix:" lines inside `reason`, because the ONE top-level handler
|
|
59
|
-
* owns the rendering of a cure list — a hand-numbered list in a string literal is an automatic review
|
|
60
|
-
* reject, and it also means the two cures could not be re-ordered or counted by anything but a human
|
|
61
|
-
* reading prose. Empty on both green paths: there is nothing to fix.
|
|
62
|
-
*/
|
|
63
|
-
export declare class GuardPresenceVerdict {
|
|
64
|
-
/** True ⇒ the caller may proceed. False ⇒ BLOCK: a Codex session ran with no guard rows. */
|
|
65
|
-
readonly ok: boolean;
|
|
66
|
-
/** Why, in one line — always populated, including on the green paths. */
|
|
67
|
-
readonly reason: string;
|
|
68
|
-
/** How many L0 shim rows this tree has for the session under attestation. */
|
|
69
|
-
readonly rows: number;
|
|
70
|
-
/** What to DO about it, for the caller to hand to RuleFailError. Empty when `ok`. */
|
|
71
|
-
readonly cures: readonly Option[];
|
|
72
|
-
constructor(
|
|
73
|
-
/** True ⇒ the caller may proceed. False ⇒ BLOCK: a Codex session ran with no guard rows. */
|
|
74
|
-
ok: boolean,
|
|
75
|
-
/** Why, in one line — always populated, including on the green paths. */
|
|
76
|
-
reason: string,
|
|
77
|
-
/** How many L0 shim rows this tree has for the session under attestation. */
|
|
78
|
-
rows: number,
|
|
79
|
-
/** What to DO about it, for the caller to hand to RuleFailError. Empty when `ok`. */
|
|
80
|
-
cures?: readonly Option[]);
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* The check itself: in a Codex session, REFUSE when the tree has no L0 shim rows at all.
|
|
84
|
-
*
|
|
85
|
-
* Outside a Codex session it is a no-op that says so, which is what makes it safe to call
|
|
86
|
-
* unconditionally from a shared build path.
|
|
87
|
-
*/
|
|
88
|
-
export declare class CodexGuardPresence {
|
|
89
|
-
private readonly detector;
|
|
90
|
-
constructor(detector: CodexSessionDetector);
|
|
91
|
-
/**
|
|
92
|
-
* `root` is the tree whose `.webpieces` logs are the evidence — the same root every other webpieces
|
|
93
|
-
* writer resolves, so a worktree is attested by its own rows rather than the primary clone's.
|
|
94
|
-
*/
|
|
95
|
-
check(root: string, env?: NodeJS.ProcessEnv): GuardPresenceVerdict;
|
|
96
|
-
/**
|
|
97
|
-
* BOTH cures, always both, in likelihood order.
|
|
98
|
-
*
|
|
99
|
-
* Two unrelated failures produce this one symptom, and they have DIFFERENT fixes — an agent handed
|
|
100
|
-
* only the likelier one will run it, see nothing change, and conclude the check is broken. So the
|
|
101
|
-
* list is fixed rather than guessed at: nothing observable at build time distinguishes "the human
|
|
102
|
-
* declined the trust prompt" from "the matcher names a tool Codex never emits".
|
|
103
|
-
*/
|
|
104
|
-
static readonly CURES: readonly Option[];
|
|
105
|
-
/**
|
|
106
|
-
* The refusal's EVIDENCE — what was measured and where. The cures are Options (above); this is only
|
|
107
|
-
* the finding, so the top-level handler renders the two halves in its own house style.
|
|
108
|
-
*/
|
|
109
|
-
private refusal;
|
|
110
|
-
/** How many `.log` lines the L0 shim stream holds for this tree. Never throws; 0 on any failure. */
|
|
111
|
-
private shimRowCount;
|
|
112
|
-
}
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var CodexSessionDetector_1, CodexGuardPresence_1;
|
|
3
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.CodexGuardPresence = exports.GuardPresenceVerdict = exports.CodexSessionDetector = void 0;
|
|
5
|
-
const tslib_1 = require("tslib");
|
|
6
|
-
const fs = tslib_1.__importStar(require("fs"));
|
|
7
|
-
const path = tslib_1.__importStar(require("path"));
|
|
8
|
-
const inversify_1 = require("inversify");
|
|
9
|
-
const rules_config_1 = require("@webpieces/rules-config");
|
|
10
|
-
const log_streams_1 = require("../core/log-streams");
|
|
11
|
-
const to_error_1 = require("../core/to-error");
|
|
12
|
-
/**
|
|
13
|
-
* GUARD-PRESENCE ATTESTATION — "did the guards actually RUN in this Codex session?"
|
|
14
|
-
*
|
|
15
|
-
* ─── The hole this closes ─────────────────────────────────────────────────────────────────────────
|
|
16
|
-
* Codex prompts before running a hook it has not trusted, and the prompt's third option is
|
|
17
|
-
* `Continue without trusting (hooks won't run)`. One keystroke, no later warning, and the session is
|
|
18
|
-
* fully unguarded for its whole life. Nothing at INSTALL time can see that choice — it is made
|
|
19
|
-
* afterwards, in another process — so `codex-trust.ts` reading `~/.codex/config.toml` is necessary and
|
|
20
|
-
* not sufficient.
|
|
21
|
-
*
|
|
22
|
-
* The same check catches a second, quieter failure with no relation to trust: a matcher that names a
|
|
23
|
-
* tool Codex does not emit. That was the live state of every `.codex/hooks.json` a desktop sync had
|
|
24
|
-
* written — `Write|Edit|MultiEdit` against a harness whose file tool is `apply_patch` — and it produced
|
|
25
|
-
* exactly the same symptom, which is no symptom at all.
|
|
26
|
-
*
|
|
27
|
-
* ─── Why the L0 shim log is the evidence ──────────────────────────────────────────────────────────
|
|
28
|
-
* The L0 shim writes one row per tool call, on EVERY path including the healthy one, before anything
|
|
29
|
-
* else can fail. So a session that produced ZERO rows did not have a guard run — that is the whole
|
|
30
|
-
* inference, and it holds no matter WHY (untrusted, wrong matcher, deleted file, unresolvable path).
|
|
31
|
-
*
|
|
32
|
-
* It is deliberately a count of rows and not a check of any row's content: what is being attested is
|
|
33
|
-
* that the hook EXECUTED, not what it decided.
|
|
34
|
-
*
|
|
35
|
-
* ─── Where it is WIRED ────────────────────────────────────────────────────────────────────────────
|
|
36
|
-
* `BuildAffected.runBuildGate` (@webpieces/pr-gate) calls it before resolving the build command, so ALL
|
|
37
|
-
* THREE build entry points are covered by one call: `wp-build`, stage ② (`wp-review-upsert-pr`) and
|
|
38
|
-
* stage ③ (`wp-finish-upsert-pr`). Attesting at the build is the right place because the build is the
|
|
39
|
-
* moment a session's work is about to be claimed as verified — an unguarded session that never builds
|
|
40
|
-
* has produced nothing to trust.
|
|
41
|
-
*
|
|
42
|
-
* It BLOCKS, and blocking is the whole point. This check was deliberately shipped detached for one
|
|
43
|
-
* release rather than half-wired, because a check that detects and never refuses reads as coverage
|
|
44
|
-
* nobody actually has.
|
|
45
|
-
*/
|
|
46
|
-
/**
|
|
47
|
-
* Is THIS process running inside a Codex session?
|
|
48
|
-
*
|
|
49
|
-
* MEASURED (codex-cli 0.151.0), and the negative half matters as much as the positive: there is NO
|
|
50
|
-
* `CODEX_SESSION_ID`. Reaching for one is the obvious thing to do and it does not exist, so the
|
|
51
|
-
* fingerprints are the three that DO: `CODEX_MANAGED_BY_NPM`, `CODEX_MANAGED_PACKAGE_ROOT`, and a
|
|
52
|
-
* `/.codex/tmp/arg0/` entry on `PATH`.
|
|
53
|
-
*
|
|
54
|
-
* Any one of them is enough. They come from different install shapes, and requiring all three would
|
|
55
|
-
* silently answer "not Codex" — which for a check that BLOCKS on absence of evidence is the dangerous
|
|
56
|
-
* direction: it would turn an unguarded session into an unchecked one.
|
|
57
|
-
*/
|
|
58
|
-
let CodexSessionDetector = class CodexSessionDetector {
|
|
59
|
-
static { CodexSessionDetector_1 = this; }
|
|
60
|
-
/** The env keys that identify a Codex-managed process. */
|
|
61
|
-
static ENV_KEYS = ['CODEX_MANAGED_BY_NPM', 'CODEX_MANAGED_PACKAGE_ROOT'];
|
|
62
|
-
/** The PATH segment Codex injects for its arg0 shims. */
|
|
63
|
-
static PATH_MARKER = '/.codex/tmp/arg0/';
|
|
64
|
-
isCodexSession(env = process.env) {
|
|
65
|
-
for (const key of CodexSessionDetector_1.ENV_KEYS) {
|
|
66
|
-
const value = env[key];
|
|
67
|
-
if (value !== undefined && value !== '')
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
const search = env['PATH'] ?? '';
|
|
71
|
-
return search.includes(CodexSessionDetector_1.PATH_MARKER);
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
exports.CodexSessionDetector = CodexSessionDetector;
|
|
75
|
-
exports.CodexSessionDetector = CodexSessionDetector = CodexSessionDetector_1 = tslib_1.__decorate([
|
|
76
|
-
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
|
|
77
|
-
], CodexSessionDetector);
|
|
78
|
-
/**
|
|
79
|
-
* The answer, with the sentence a human or an agent reads. Data-only → a class, per CLAUDE.md.
|
|
80
|
-
*
|
|
81
|
-
* `cures` are `Option`s rather than "Fix:" lines inside `reason`, because the ONE top-level handler
|
|
82
|
-
* owns the rendering of a cure list — a hand-numbered list in a string literal is an automatic review
|
|
83
|
-
* reject, and it also means the two cures could not be re-ordered or counted by anything but a human
|
|
84
|
-
* reading prose. Empty on both green paths: there is nothing to fix.
|
|
85
|
-
*/
|
|
86
|
-
class GuardPresenceVerdict {
|
|
87
|
-
ok;
|
|
88
|
-
reason;
|
|
89
|
-
rows;
|
|
90
|
-
cures;
|
|
91
|
-
constructor(
|
|
92
|
-
/** True ⇒ the caller may proceed. False ⇒ BLOCK: a Codex session ran with no guard rows. */
|
|
93
|
-
ok,
|
|
94
|
-
/** Why, in one line — always populated, including on the green paths. */
|
|
95
|
-
reason,
|
|
96
|
-
/** How many L0 shim rows this tree has for the session under attestation. */
|
|
97
|
-
rows,
|
|
98
|
-
/** What to DO about it, for the caller to hand to RuleFailError. Empty when `ok`. */
|
|
99
|
-
cures = []) {
|
|
100
|
-
this.ok = ok;
|
|
101
|
-
this.reason = reason;
|
|
102
|
-
this.rows = rows;
|
|
103
|
-
this.cures = cures;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
exports.GuardPresenceVerdict = GuardPresenceVerdict;
|
|
107
|
-
/**
|
|
108
|
-
* The check itself: in a Codex session, REFUSE when the tree has no L0 shim rows at all.
|
|
109
|
-
*
|
|
110
|
-
* Outside a Codex session it is a no-op that says so, which is what makes it safe to call
|
|
111
|
-
* unconditionally from a shared build path.
|
|
112
|
-
*/
|
|
113
|
-
let CodexGuardPresence = class CodexGuardPresence {
|
|
114
|
-
static { CodexGuardPresence_1 = this; }
|
|
115
|
-
detector;
|
|
116
|
-
// Injected BY TYPE — no Symbol token, per CLAUDE.md's DI convention. The detector is a separate
|
|
117
|
-
// class because "is this Codex?" is a question other callers ask without wanting the attestation.
|
|
118
|
-
constructor(detector) {
|
|
119
|
-
this.detector = detector;
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* `root` is the tree whose `.webpieces` logs are the evidence — the same root every other webpieces
|
|
123
|
-
* writer resolves, so a worktree is attested by its own rows rather than the primary clone's.
|
|
124
|
-
*/
|
|
125
|
-
check(root, env = process.env) {
|
|
126
|
-
if (!this.detector.isCodexSession(env)) {
|
|
127
|
-
return new GuardPresenceVerdict(true, 'not a Codex session — guard presence is not attested here', 0);
|
|
128
|
-
}
|
|
129
|
-
const rows = this.shimRowCount(root);
|
|
130
|
-
if (rows > 0) {
|
|
131
|
-
return new GuardPresenceVerdict(true, `Codex session with ${String(rows)} L0 guard row(s) — the guards ran`, rows);
|
|
132
|
-
}
|
|
133
|
-
return new GuardPresenceVerdict(false, this.refusal(root), 0, CodexGuardPresence_1.CURES);
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* BOTH cures, always both, in likelihood order.
|
|
137
|
-
*
|
|
138
|
-
* Two unrelated failures produce this one symptom, and they have DIFFERENT fixes — an agent handed
|
|
139
|
-
* only the likelier one will run it, see nothing change, and conclude the check is broken. So the
|
|
140
|
-
* list is fixed rather than guessed at: nothing observable at build time distinguishes "the human
|
|
141
|
-
* declined the trust prompt" from "the matcher names a tool Codex never emits".
|
|
142
|
-
*/
|
|
143
|
-
static CURES = [
|
|
144
|
-
new rules_config_1.Option('You answered "Continue without trusting (hooks won\'t run)" at Codex\'s hook prompt.\n'
|
|
145
|
-
+ 'Restart `codex` in this repo and choose "Trust all".', true),
|
|
146
|
-
new rules_config_1.Option('.codex/hooks.json registers a matcher Codex never emits, or a shim path that does not\n'
|
|
147
|
-
+ 'resolve. Run EXACTLY: \'pnpm exec wp-install-ai-hooks --target=project\''),
|
|
148
|
-
];
|
|
149
|
-
/**
|
|
150
|
-
* The refusal's EVIDENCE — what was measured and where. The cures are Options (above); this is only
|
|
151
|
-
* the finding, so the top-level handler renders the two halves in its own house style.
|
|
152
|
-
*/
|
|
153
|
-
refusal(root) {
|
|
154
|
-
return [
|
|
155
|
-
'This is a Codex session and NOT ONE guard has run in this tree.',
|
|
156
|
-
'',
|
|
157
|
-
` no rows in ${path.join(rules_config_1.dotWebpieces.logs(root), log_streams_1.L0_SHIM_STREAM)}`,
|
|
158
|
-
' → the L0 shim writes one row per tool call on EVERY path, including the healthy one, so',
|
|
159
|
-
' zero rows means the PreToolUse hook never executed. Every tool call so far was unguarded.',
|
|
160
|
-
].join('\n');
|
|
161
|
-
}
|
|
162
|
-
/** How many `.log` lines the L0 shim stream holds for this tree. Never throws; 0 on any failure. */
|
|
163
|
-
shimRowCount(root) {
|
|
164
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
165
|
-
try {
|
|
166
|
-
const streamDir = path.join(rules_config_1.dotWebpieces.logs(root), log_streams_1.L0_SHIM_STREAM);
|
|
167
|
-
if (!fs.existsSync(streamDir))
|
|
168
|
-
return 0;
|
|
169
|
-
let rows = 0;
|
|
170
|
-
for (const name of fs.readdirSync(streamDir)) {
|
|
171
|
-
if (!name.endsWith('.log'))
|
|
172
|
-
continue;
|
|
173
|
-
const body = fs.readFileSync(path.join(streamDir, name), 'utf8');
|
|
174
|
-
rows += body.split('\n').filter((line) => line.trim() !== '').length;
|
|
175
|
-
}
|
|
176
|
-
return rows;
|
|
177
|
-
}
|
|
178
|
-
catch (err) {
|
|
179
|
-
const error = (0, to_error_1.toError)(err);
|
|
180
|
-
void error; // an unreadable log dir is ZERO rows — the direction that refuses rather than waves through
|
|
181
|
-
return 0;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
exports.CodexGuardPresence = CodexGuardPresence;
|
|
186
|
-
exports.CodexGuardPresence = CodexGuardPresence = CodexGuardPresence_1 = tslib_1.__decorate([
|
|
187
|
-
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
188
|
-
tslib_1.__metadata("design:paramtypes", [CodexSessionDetector])
|
|
189
|
-
], CodexGuardPresence);
|
|
190
|
-
//# sourceMappingURL=codex-guard-presence.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"codex-guard-presence.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/codex-guard-presence.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { dotWebpieces, Option } from '@webpieces/rules-config';\n\nimport { L0_SHIM_STREAM } from '../core/log-streams';\nimport { toError } from '../core/to-error';\n\n/**\n * GUARD-PRESENCE ATTESTATION — \"did the guards actually RUN in this Codex session?\"\n *\n * ─── The hole this closes ─────────────────────────────────────────────────────────────────────────\n * Codex prompts before running a hook it has not trusted, and the prompt's third option is\n * `Continue without trusting (hooks won't run)`. One keystroke, no later warning, and the session is\n * fully unguarded for its whole life. Nothing at INSTALL time can see that choice — it is made\n * afterwards, in another process — so `codex-trust.ts` reading `~/.codex/config.toml` is necessary and\n * not sufficient.\n *\n * The same check catches a second, quieter failure with no relation to trust: a matcher that names a\n * tool Codex does not emit. That was the live state of every `.codex/hooks.json` a desktop sync had\n * written — `Write|Edit|MultiEdit` against a harness whose file tool is `apply_patch` — and it produced\n * exactly the same symptom, which is no symptom at all.\n *\n * ─── Why the L0 shim log is the evidence ──────────────────────────────────────────────────────────\n * The L0 shim writes one row per tool call, on EVERY path including the healthy one, before anything\n * else can fail. So a session that produced ZERO rows did not have a guard run — that is the whole\n * inference, and it holds no matter WHY (untrusted, wrong matcher, deleted file, unresolvable path).\n *\n * It is deliberately a count of rows and not a check of any row's content: what is being attested is\n * that the hook EXECUTED, not what it decided.\n *\n * ─── Where it is WIRED ────────────────────────────────────────────────────────────────────────────\n * `BuildAffected.runBuildGate` (@webpieces/pr-gate) calls it before resolving the build command, so ALL\n * THREE build entry points are covered by one call: `wp-build`, stage ② (`wp-review-upsert-pr`) and\n * stage ③ (`wp-finish-upsert-pr`). Attesting at the build is the right place because the build is the\n * moment a session's work is about to be claimed as verified — an unguarded session that never builds\n * has produced nothing to trust.\n *\n * It BLOCKS, and blocking is the whole point. This check was deliberately shipped detached for one\n * release rather than half-wired, because a check that detects and never refuses reads as coverage\n * nobody actually has.\n */\n\n/**\n * Is THIS process running inside a Codex session?\n *\n * MEASURED (codex-cli 0.151.0), and the negative half matters as much as the positive: there is NO\n * `CODEX_SESSION_ID`. Reaching for one is the obvious thing to do and it does not exist, so the\n * fingerprints are the three that DO: `CODEX_MANAGED_BY_NPM`, `CODEX_MANAGED_PACKAGE_ROOT`, and a\n * `/.codex/tmp/arg0/` entry on `PATH`.\n *\n * Any one of them is enough. They come from different install shapes, and requiring all three would\n * silently answer \"not Codex\" — which for a check that BLOCKS on absence of evidence is the dangerous\n * direction: it would turn an unguarded session into an unchecked one.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CodexSessionDetector {\n /** The env keys that identify a Codex-managed process. */\n static readonly ENV_KEYS: readonly string[] = ['CODEX_MANAGED_BY_NPM', 'CODEX_MANAGED_PACKAGE_ROOT'];\n\n /** The PATH segment Codex injects for its arg0 shims. */\n static readonly PATH_MARKER = '/.codex/tmp/arg0/';\n\n isCodexSession(env: NodeJS.ProcessEnv = process.env): boolean {\n for (const key of CodexSessionDetector.ENV_KEYS) {\n const value = env[key];\n if (value !== undefined && value !== '') return true;\n }\n const search = env['PATH'] ?? '';\n return search.includes(CodexSessionDetector.PATH_MARKER);\n }\n}\n\n/**\n * The answer, with the sentence a human or an agent reads. Data-only → a class, per CLAUDE.md.\n *\n * `cures` are `Option`s rather than \"Fix:\" lines inside `reason`, because the ONE top-level handler\n * owns the rendering of a cure list — a hand-numbered list in a string literal is an automatic review\n * reject, and it also means the two cures could not be re-ordered or counted by anything but a human\n * reading prose. Empty on both green paths: there is nothing to fix.\n */\nexport class GuardPresenceVerdict {\n constructor(\n /** True ⇒ the caller may proceed. False ⇒ BLOCK: a Codex session ran with no guard rows. */\n readonly ok: boolean,\n /** Why, in one line — always populated, including on the green paths. */\n readonly reason: string,\n /** How many L0 shim rows this tree has for the session under attestation. */\n readonly rows: number,\n /** What to DO about it, for the caller to hand to RuleFailError. Empty when `ok`. */\n readonly cures: readonly Option[] = [],\n ) {}\n}\n\n/**\n * The check itself: in a Codex session, REFUSE when the tree has no L0 shim rows at all.\n *\n * Outside a Codex session it is a no-op that says so, which is what makes it safe to call\n * unconditionally from a shared build path.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CodexGuardPresence {\n // Injected BY TYPE — no Symbol token, per CLAUDE.md's DI convention. The detector is a separate\n // class because \"is this Codex?\" is a question other callers ask without wanting the attestation.\n constructor(private readonly detector: CodexSessionDetector) {}\n\n /**\n * `root` is the tree whose `.webpieces` logs are the evidence — the same root every other webpieces\n * writer resolves, so a worktree is attested by its own rows rather than the primary clone's.\n */\n check(root: string, env: NodeJS.ProcessEnv = process.env): GuardPresenceVerdict {\n if (!this.detector.isCodexSession(env)) {\n return new GuardPresenceVerdict(true, 'not a Codex session — guard presence is not attested here', 0);\n }\n const rows = this.shimRowCount(root);\n if (rows > 0) {\n return new GuardPresenceVerdict(true, `Codex session with ${String(rows)} L0 guard row(s) — the guards ran`, rows);\n }\n return new GuardPresenceVerdict(false, this.refusal(root), 0, CodexGuardPresence.CURES);\n }\n\n /**\n * BOTH cures, always both, in likelihood order.\n *\n * Two unrelated failures produce this one symptom, and they have DIFFERENT fixes — an agent handed\n * only the likelier one will run it, see nothing change, and conclude the check is broken. So the\n * list is fixed rather than guessed at: nothing observable at build time distinguishes \"the human\n * declined the trust prompt\" from \"the matcher names a tool Codex never emits\".\n */\n static readonly CURES: readonly Option[] = [\n new Option(\n 'You answered \"Continue without trusting (hooks won\\'t run)\" at Codex\\'s hook prompt.\\n'\n + 'Restart `codex` in this repo and choose \"Trust all\".',\n true),\n new Option(\n '.codex/hooks.json registers a matcher Codex never emits, or a shim path that does not\\n'\n + 'resolve. Run EXACTLY: \\'pnpm exec wp-install-ai-hooks --target=project\\''),\n ];\n\n /**\n * The refusal's EVIDENCE — what was measured and where. The cures are Options (above); this is only\n * the finding, so the top-level handler renders the two halves in its own house style.\n */\n private refusal(root: string): string {\n return [\n 'This is a Codex session and NOT ONE guard has run in this tree.',\n '',\n ` no rows in ${path.join(dotWebpieces.logs(root), L0_SHIM_STREAM)}`,\n ' → the L0 shim writes one row per tool call on EVERY path, including the healthy one, so',\n ' zero rows means the PreToolUse hook never executed. Every tool call so far was unguarded.',\n ].join('\\n');\n }\n\n /** How many `.log` lines the L0 shim stream holds for this tree. Never throws; 0 on any failure. */\n private shimRowCount(root: string): number {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const streamDir = path.join(dotWebpieces.logs(root), L0_SHIM_STREAM);\n if (!fs.existsSync(streamDir)) return 0;\n let rows = 0;\n for (const name of fs.readdirSync(streamDir)) {\n if (!name.endsWith('.log')) continue;\n const body = fs.readFileSync(path.join(streamDir, name), 'utf8');\n rows += body.split('\\n').filter((line: string): boolean => line.trim() !== '').length;\n }\n return rows;\n } catch (err: unknown) {\n const error = toError(err);\n void error; // an unreadable log dir is ZERO rows — the direction that refuses rather than waves through\n return 0;\n }\n }\n}\n"]}
|