@webpieces/ai-hook-rules 0.4.735 → 0.4.737

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.
@@ -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"]}