@webpieces/ai-hook-rules 0.4.718 → 0.4.720

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/ai-hook-rules",
3
- "version": "0.4.718",
3
+ "version": "0.4.720",
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.718",
28
+ "@webpieces/rules-config": "0.4.720",
29
29
  "inversify": "7.10.4",
30
30
  "reflect-metadata": "0.2.2"
31
31
  },
@@ -1,3 +1,4 @@
1
+ import { Option } from '@webpieces/rules-config';
1
2
  /**
2
3
  * GUARD-PRESENCE ATTESTATION — "did the guards actually RUN in this Codex session?"
3
4
  *
@@ -21,11 +22,16 @@
21
22
  * It is deliberately a count of rows and not a check of any row's content: what is being attested is
22
23
  * that the hook EXECUTED, not what it decided.
23
24
  *
24
- * ─── Scope, stated honestly ───────────────────────────────────────────────────────────────────────
25
- * This is a REUSABLE CHECK, not yet a gate. `check()` returns a verdict and a message; wiring it into
26
- * `wp-build` and the PR gate's build stage is a follow-up, because the build path lives in a different
27
- * published package (`@webpieces/pr-gate`) and a half-wired gate one that detects but never blocks
28
- * would be worse than none: it would read as coverage nobody has.
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.
29
35
  */
30
36
  /**
31
37
  * Is THIS process running inside a Codex session?
@@ -46,7 +52,14 @@ export declare class CodexSessionDetector {
46
52
  static readonly PATH_MARKER = "/.codex/tmp/arg0/";
47
53
  isCodexSession(env?: NodeJS.ProcessEnv): boolean;
48
54
  }
49
- /** The answer, with the sentence a human or an agent reads. Data-only → a class, per CLAUDE.md. */
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
+ */
50
63
  export declare class GuardPresenceVerdict {
51
64
  /** True ⇒ the caller may proceed. False ⇒ BLOCK: a Codex session ran with no guard rows. */
52
65
  readonly ok: boolean;
@@ -54,13 +67,17 @@ export declare class GuardPresenceVerdict {
54
67
  readonly reason: string;
55
68
  /** How many L0 shim rows this tree has for the session under attestation. */
56
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[];
57
72
  constructor(
58
73
  /** True ⇒ the caller may proceed. False ⇒ BLOCK: a Codex session ran with no guard rows. */
59
74
  ok: boolean,
60
75
  /** Why, in one line — always populated, including on the green paths. */
61
76
  reason: string,
62
77
  /** How many L0 shim rows this tree has for the session under attestation. */
63
- rows: number);
78
+ rows: number,
79
+ /** What to DO about it, for the caller to hand to RuleFailError. Empty when `ok`. */
80
+ cures?: readonly Option[]);
64
81
  }
65
82
  /**
66
83
  * The check itself: in a Codex session, REFUSE when the tree has no L0 shim rows at all.
@@ -70,15 +87,24 @@ export declare class GuardPresenceVerdict {
70
87
  */
71
88
  export declare class CodexGuardPresence {
72
89
  private readonly detector;
90
+ constructor(detector: CodexSessionDetector);
73
91
  /**
74
92
  * `root` is the tree whose `.webpieces` logs are the evidence — the same root every other webpieces
75
93
  * writer resolves, so a worktree is attested by its own rows rather than the primary clone's.
76
94
  */
77
95
  check(root: string, env?: NodeJS.ProcessEnv): GuardPresenceVerdict;
78
96
  /**
79
- * The refusal text. It names the two causes that produce this identical symptom, because they have
80
- * DIFFERENT cures and an agent handed only one of them will run it, see nothing change, and conclude
81
- * the check is broken.
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.
82
108
  */
83
109
  private refusal;
84
110
  /** How many `.log` lines the L0 shim stream holds for this tree. Never throws; 0 on any failure. */
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
+ var CodexSessionDetector_1, CodexGuardPresence_1;
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
4
  exports.CodexGuardPresence = exports.GuardPresenceVerdict = exports.CodexSessionDetector = void 0;
4
5
  const tslib_1 = require("tslib");
5
6
  const fs = tslib_1.__importStar(require("fs"));
6
7
  const path = tslib_1.__importStar(require("path"));
8
+ const inversify_1 = require("inversify");
7
9
  const rules_config_1 = require("@webpieces/rules-config");
8
10
  const log_streams_1 = require("../core/log-streams");
9
11
  const to_error_1 = require("../core/to-error");
@@ -30,11 +32,16 @@ const to_error_1 = require("../core/to-error");
30
32
  * It is deliberately a count of rows and not a check of any row's content: what is being attested is
31
33
  * that the hook EXECUTED, not what it decided.
32
34
  *
33
- * ─── Scope, stated honestly ───────────────────────────────────────────────────────────────────────
34
- * This is a REUSABLE CHECK, not yet a gate. `check()` returns a verdict and a message; wiring it into
35
- * `wp-build` and the PR gate's build stage is a follow-up, because the build path lives in a different
36
- * published package (`@webpieces/pr-gate`) and a half-wired gate one that detects but never blocks
37
- * would be worse than none: it would read as coverage nobody has.
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.
38
45
  */
39
46
  /**
40
47
  * Is THIS process running inside a Codex session?
@@ -48,37 +55,52 @@ const to_error_1 = require("../core/to-error");
48
55
  * silently answer "not Codex" — which for a check that BLOCKS on absence of evidence is the dangerous
49
56
  * direction: it would turn an unguarded session into an unchecked one.
50
57
  */
51
- class CodexSessionDetector {
58
+ let CodexSessionDetector = class CodexSessionDetector {
59
+ static { CodexSessionDetector_1 = this; }
52
60
  /** The env keys that identify a Codex-managed process. */
53
61
  static ENV_KEYS = ['CODEX_MANAGED_BY_NPM', 'CODEX_MANAGED_PACKAGE_ROOT'];
54
62
  /** The PATH segment Codex injects for its arg0 shims. */
55
63
  static PATH_MARKER = '/.codex/tmp/arg0/';
56
64
  isCodexSession(env = process.env) {
57
- for (const key of CodexSessionDetector.ENV_KEYS) {
65
+ for (const key of CodexSessionDetector_1.ENV_KEYS) {
58
66
  const value = env[key];
59
67
  if (value !== undefined && value !== '')
60
68
  return true;
61
69
  }
62
70
  const search = env['PATH'] ?? '';
63
- return search.includes(CodexSessionDetector.PATH_MARKER);
71
+ return search.includes(CodexSessionDetector_1.PATH_MARKER);
64
72
  }
65
- }
73
+ };
66
74
  exports.CodexSessionDetector = CodexSessionDetector;
67
- /** The answer, with the sentence a human or an agent reads. Data-only a class, per CLAUDE.md. */
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
+ */
68
86
  class GuardPresenceVerdict {
69
87
  ok;
70
88
  reason;
71
89
  rows;
90
+ cures;
72
91
  constructor(
73
92
  /** True ⇒ the caller may proceed. False ⇒ BLOCK: a Codex session ran with no guard rows. */
74
93
  ok,
75
94
  /** Why, in one line — always populated, including on the green paths. */
76
95
  reason,
77
96
  /** How many L0 shim rows this tree has for the session under attestation. */
78
- rows) {
97
+ rows,
98
+ /** What to DO about it, for the caller to hand to RuleFailError. Empty when `ok`. */
99
+ cures = []) {
79
100
  this.ok = ok;
80
101
  this.reason = reason;
81
102
  this.rows = rows;
103
+ this.cures = cures;
82
104
  }
83
105
  }
84
106
  exports.GuardPresenceVerdict = GuardPresenceVerdict;
@@ -88,8 +110,14 @@ exports.GuardPresenceVerdict = GuardPresenceVerdict;
88
110
  * Outside a Codex session it is a no-op that says so, which is what makes it safe to call
89
111
  * unconditionally from a shared build path.
90
112
  */
91
- class CodexGuardPresence {
92
- detector = new CodexSessionDetector();
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
+ }
93
121
  /**
94
122
  * `root` is the tree whose `.webpieces` logs are the evidence — the same root every other webpieces
95
123
  * writer resolves, so a worktree is attested by its own rows rather than the primary clone's.
@@ -102,25 +130,33 @@ class CodexGuardPresence {
102
130
  if (rows > 0) {
103
131
  return new GuardPresenceVerdict(true, `Codex session with ${String(rows)} L0 guard row(s) — the guards ran`, rows);
104
132
  }
105
- return new GuardPresenceVerdict(false, this.refusal(root), 0);
133
+ return new GuardPresenceVerdict(false, this.refusal(root), 0, CodexGuardPresence_1.CURES);
106
134
  }
107
135
  /**
108
- * The refusal text. It names the two causes that produce this identical symptom, because they have
109
- * DIFFERENT cures and an agent handed only one of them will run it, see nothing change, and conclude
110
- * the check is broken.
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.
111
152
  */
112
153
  refusal(root) {
113
154
  return [
114
- ' webpieces: this is a Codex session and NOT ONE guard has run in this tree.',
155
+ 'This is a Codex session and NOT ONE guard has run in this tree.',
115
156
  '',
116
157
  ` no rows in ${path.join(rules_config_1.dotWebpieces.logs(root), log_streams_1.L0_SHIM_STREAM)}`,
117
158
  ' → the L0 shim writes one row per tool call on EVERY path, including the healthy one, so',
118
159
  ' zero rows means the PreToolUse hook never executed. Every tool call so far was unguarded.',
119
- '',
120
- ' Cause 1 (most likely): you answered "Continue without trusting (hooks won\'t run)" at Codex\'s',
121
- ' hook prompt. Fix: restart `codex` in this repo and choose "Trust all".',
122
- ' Cause 2: .codex/hooks.json registers a matcher Codex never emits, or a shim path that does',
123
- ' not resolve. Fix: run EXACTLY: \'pnpm exec wp-install-ai-hooks --target=project\'',
124
160
  ].join('\n');
125
161
  }
126
162
  /** How many `.log` lines the L0 shim stream holds for this tree. Never throws; 0 on any failure. */
@@ -145,6 +181,10 @@ class CodexGuardPresence {
145
181
  return 0;
146
182
  }
147
183
  }
148
- }
184
+ };
149
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);
150
190
  //# sourceMappingURL=codex-guard-presence.js.map
@@ -1 +1 @@
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 { dotWebpieces } 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 * ─── Scope, stated honestly ───────────────────────────────────────────────────────────────────────\n * This is a REUSABLE CHECK, not yet a gate. `check()` returns a verdict and a message; wiring it into\n * `wp-build` and the PR gate's build stage is a follow-up, because the build path lives in a different\n * published package (`@webpieces/pr-gate`) and a half-wired gate — one that detects but never blocks —\n * would be worse than none: it would read as coverage nobody 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 */\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/** The answer, with the sentence a human or an agent reads. Data-only → a class, per CLAUDE.md. */\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 ) {}\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 */\nexport class CodexGuardPresence {\n private readonly detector = new 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);\n }\n\n /**\n * The refusal text. It names the two causes that produce this identical symptom, because they have\n * DIFFERENT cures and an agent handed only one of them will run it, see nothing change, and conclude\n * the check is broken.\n */\n private refusal(root: string): string {\n return [\n '❌ webpieces: 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 '',\n ' Cause 1 (most likely): you answered \"Continue without trusting (hooks won\\'t run)\" at Codex\\'s',\n ' hook prompt. Fix: restart `codex` in this repo and choose \"Trust all\".',\n ' Cause 2: .codex/hooks.json registers a matcher Codex never emits, or a shim path that does',\n ' not resolve. Fix: run EXACTLY: \\'pnpm exec wp-install-ai-hooks --target=project\\'',\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"]}
1
+ {"version":3,"file":"codex-guard-presence.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/codex-guard-presence.ts"],"names":[],"mappings":";;;;;AAAA,+CAAyB;AACzB,mDAA6B;AAE7B,yCAA2D;AAE3D,0DAA+D;AAE/D,qDAAqD;AACrD,+CAA2C;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH;;;;;;;;;;;GAWG;AAEI,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;;IAC7B,0DAA0D;IAC1D,MAAM,CAAU,QAAQ,GAAsB,CAAC,sBAAsB,EAAE,4BAA4B,CAAC,CAAC;IAErG,yDAAyD;IACzD,MAAM,CAAU,WAAW,GAAG,mBAAmB,CAAC;IAElD,cAAc,CAAC,MAAyB,OAAO,CAAC,GAAG;QAC/C,KAAK,MAAM,GAAG,IAAI,sBAAoB,CAAC,QAAQ,EAAE,CAAC;YAC9C,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;YACvB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE;gBAAE,OAAO,IAAI,CAAC;QACzD,CAAC;QACD,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACjC,OAAO,MAAM,CAAC,QAAQ,CAAC,sBAAoB,CAAC,WAAW,CAAC,CAAC;IAC7D,CAAC;;AAdQ,oDAAoB;+BAApB,oBAAoB;IADhC,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,oBAAoB,CAehC;AAED;;;;;;;GAOG;AACH,MAAa,oBAAoB;IAGhB;IAEA;IAEA;IAEA;IARb;IACI,4FAA4F;IACnF,EAAW;IACpB,yEAAyE;IAChE,MAAc;IACvB,6EAA6E;IACpE,IAAY;IACrB,qFAAqF;IAC5E,QAA2B,EAAE;QAN7B,OAAE,GAAF,EAAE,CAAS;QAEX,WAAM,GAAN,MAAM,CAAQ;QAEd,SAAI,GAAJ,IAAI,CAAQ;QAEZ,UAAK,GAAL,KAAK,CAAwB;IACvC,CAAC;CACP;AAXD,oDAWC;AAED;;;;;GAKG;AAEI,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;;IAGE;IAF7B,gGAAgG;IAChG,kGAAkG;IAClG,YAA6B,QAA8B;QAA9B,aAAQ,GAAR,QAAQ,CAAsB;IAAG,CAAC;IAE/D;;;OAGG;IACH,KAAK,CAAC,IAAY,EAAE,MAAyB,OAAO,CAAC,GAAG;QACpD,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;YACrC,OAAO,IAAI,oBAAoB,CAAC,IAAI,EAAE,2DAA2D,EAAE,CAAC,CAAC,CAAC;QAC1G,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;YACX,OAAO,IAAI,oBAAoB,CAAC,IAAI,EAAE,sBAAsB,MAAM,CAAC,IAAI,CAAC,mCAAmC,EAAE,IAAI,CAAC,CAAC;QACvH,CAAC;QACD,OAAO,IAAI,oBAAoB,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,oBAAkB,CAAC,KAAK,CAAC,CAAC;IAC5F,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,CAAU,KAAK,GAAsB;QACvC,IAAI,qBAAM,CACN,wFAAwF;cACtF,sDAAsD,EACxD,IAAI,CAAC;QACT,IAAI,qBAAM,CACN,yFAAyF;cACvF,0EAA0E,CAAC;KACpF,CAAC;IAEF;;;OAGG;IACK,OAAO,CAAC,IAAY;QACxB,OAAO;YACH,iEAAiE;YACjE,EAAE;YACF,gBAAgB,IAAI,CAAC,IAAI,CAAC,2BAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,4BAAc,CAAC,EAAE;YACpE,6FAA6F;YAC7F,iGAAiG;SACpG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAED,oGAAoG;IAC5F,YAAY,CAAC,IAAY;QAC7B,8DAA8D;QAC9D,IAAI,CAAC;YACD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,2BAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,4BAAc,CAAC,CAAC;YACrE,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;gBAAE,OAAO,CAAC,CAAC;YACxC,IAAI,IAAI,GAAG,CAAC,CAAC;YACb,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC3C,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;oBAAE,SAAS;gBACrC,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;gBACjE,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC;YAC1F,CAAC;YACD,OAAO,IAAI,CAAC;QAChB,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC,CAAC,4FAA4F;YACxG,OAAO,CAAC,CAAC;QACb,CAAC;IACL,CAAC;;AAtEQ,gDAAkB;6BAAlB,kBAAkB;IAD9B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAIE,oBAAoB;GAHlD,kBAAkB,CAuE9B","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"]}
@@ -1,4 +1,4 @@
1
- import { AiType } from '../core/agent-event';
1
+ import { L0AllowEntry } from './l0-entry';
2
2
  export declare const WORKSPACE_MANIFEST = "pnpm-workspace.yaml";
3
3
  export declare const PACKAGE_MANIFEST = "package.json";
4
4
  export declare const MANIFEST_FILENAMES: ReadonlySet<string>;
@@ -42,75 +42,6 @@ export declare const ORIENT_ALLOW_JS: RegExp;
42
42
  export declare const ORIENT_CMD = "pwd";
43
43
  export declare const INSTALL_HOOKS_CMD = "pnpm exec wp-install-ai-hooks";
44
44
  export declare const INSTALL_HOOKS_TARGET_CMD = "pnpm wp-install-ai-hooks --target=project";
45
- /** One tool call as L0 judges it: the tool name, the Bash command (or ''), the file target (or ''). */
46
- export declare class L0Call {
47
- readonly toolName: string;
48
- readonly command: string;
49
- readonly filePath: string;
50
- constructor(toolName: string, command: string, filePath: string);
51
- }
52
- /**
53
- * One entry of THE L0 allowlist. Data-only → a class, per CLAUDE.md.
54
- *
55
- * `ere`/`js` are the twin regex BODIES for a Bash entry, or null for a tool-shaped entry (Read, the
56
- * webpieces.config.json target) that no regex can express. `sample` is a call this entry must accept —
57
- * it is what the matrix-coverage and cure-reachability tests drive isAllowed() with.
58
- *
59
- * `extraSamples` pins ADDITIONAL spellings the same entry must accept. A spelling that some deny
60
- * message prescribes belongs here, or nothing stops a later tightening of the pattern from making that
61
- * message's cure untypable again — which is the deadlock shape this whole module exists to prevent.
62
- *
63
- * `cure` is the ONE thing that is not uniform across the list, and it is not about L0 at all — see
64
- * L0_CURE_ALLOW_JS below. Every entry is judged identically while an L0 fault is up; `cure` decides
65
- * only whether the entry ALSO bypasses the downstream (L1) guards on a HEALTHY tree.
66
- */
67
- export declare class L0AllowEntry {
68
- readonly label: string;
69
- readonly kind: 'pass' | 'allow';
70
- /**
71
- * True when this entry REPAIRS the tooling (install, sync, shim restore). A cure has to run
72
- * before webpieces.config.json can even be loaded, so it bypasses everything, always. A
73
- * non-cure (read-only orientation) is allowed while a fault is up and is otherwise an ordinary
74
- * command the downstream guards still judge.
75
- */
76
- readonly cure: boolean;
77
- readonly ere: string | null;
78
- readonly js: string | null;
79
- /**
80
- * The ONE harness this entry exists for, or null for the entries every harness gets.
81
- *
82
- * A gated entry is unreachable from any other harness — it is spliced into its own union, which
83
- * both halves of L0 consult only after answering "which harness sent this call?". It exists
84
- * because the two harnesses do not have the same TOOLS: `Read` is entry 1 for Claude Code and
85
- * Codex has no such tool, so read parity at L0 can only be expressed per harness. Anything added
86
- * here later must satisfy the property stated on AI_TYPE_SH: the sh half's answer is an
87
- * approximation, so a gated entry may never grant more than the OTHER harness already has.
88
- */
89
- readonly aiType: AiType | null;
90
- readonly sample: L0Call;
91
- readonly extraSamples: readonly L0Call[];
92
- constructor(label: string, kind: 'pass' | 'allow',
93
- /**
94
- * True when this entry REPAIRS the tooling (install, sync, shim restore). A cure has to run
95
- * before webpieces.config.json can even be loaded, so it bypasses everything, always. A
96
- * non-cure (read-only orientation) is allowed while a fault is up and is otherwise an ordinary
97
- * command the downstream guards still judge.
98
- */
99
- cure: boolean, ere: string | null, js: string | null,
100
- /**
101
- * The ONE harness this entry exists for, or null for the entries every harness gets.
102
- *
103
- * A gated entry is unreachable from any other harness — it is spliced into its own union, which
104
- * both halves of L0 consult only after answering "which harness sent this call?". It exists
105
- * because the two harnesses do not have the same TOOLS: `Read` is entry 1 for Claude Code and
106
- * Codex has no such tool, so read parity at L0 can only be expressed per harness. Anything added
107
- * here later must satisfy the property stated on AI_TYPE_SH: the sh half's answer is an
108
- * approximation, so a gated entry may never grant more than the OTHER harness already has.
109
- */
110
- aiType: AiType | null, sample: L0Call, extraSamples?: readonly L0Call[]);
111
- /** Every call this entry pins: the canonical sample plus every extra spelling. */
112
- allSamples(): readonly L0Call[];
113
- }
114
45
  export declare const L0_ALLOWLIST: readonly L0AllowEntry[];
115
46
  export declare const L0_ALLOW_ERE: string;
116
47
  export declare const L0_ALLOW_ERE_SH: string;
@@ -1,7 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.L0_CURE_ALLOW_JS = exports.L0_ALLOW_JS = exports.L0_ALLOW_ERE_SH = exports.L0_ALLOW_ERE = exports.L0_ALLOWLIST = exports.L0AllowEntry = exports.L0Call = exports.INSTALL_HOOKS_TARGET_CMD = exports.INSTALL_HOOKS_CMD = exports.ORIENT_CMD = exports.ORIENT_ALLOW_JS = exports.ORIENT_ALLOW_ERE = exports.ADD_HOOK_PKG_CMD = exports.HOOK_PKG = exports.ADD_HOOK_PKG_ALLOW_JS = exports.ADD_HOOK_PKG_ALLOW_ERE = exports.INSTALL_HOOKS_ALLOW_JS = exports.INSTALL_HOOKS_ALLOW_ERE = exports.RESTORE_SHIM_CMD = exports.RESTORE_SHIM_ALLOW_JS = exports.RESTORE_SHIM_ALLOW_ERE = exports.UPGRADE_SHIM_CMD = exports.UPGRADE_SHIM_ALLOW_JS = exports.UPGRADE_SHIM_ALLOW_ERE = exports.CHECKOUT_MAIN_PULL_CMD = exports.CHECKOUT_MAIN_PULL_ALLOW_JS = exports.CHECKOUT_MAIN_PULL_ALLOW_ERE = exports.FETCH_ALLOW_JS = exports.FETCH_ALLOW_ERE = exports.RECOVERY_CMD = exports.RECOVERY_ALLOW_JS = exports.RECOVERY_ALLOW_ERE = exports.INSTALLER_ALLOW_JS = exports.INSTALLER_ALLOW_ERE = exports.CD_PREFIX_JS_SRC = exports.CD_PREFIX_ERE = exports.CAPTURE_TAIL_JS_SRC = exports.CAPTURE_TAIL_ERE = exports.MANIFEST_FILENAMES = exports.PACKAGE_MANIFEST = exports.WORKSPACE_MANIFEST = void 0;
3
+ exports.L0_CURE_ALLOW_JS = exports.L0_ALLOW_JS = exports.L0_ALLOW_ERE_SH = exports.L0_ALLOW_ERE = exports.L0_ALLOWLIST = exports.INSTALL_HOOKS_TARGET_CMD = exports.INSTALL_HOOKS_CMD = exports.ORIENT_CMD = exports.ORIENT_ALLOW_JS = exports.ORIENT_ALLOW_ERE = exports.ADD_HOOK_PKG_CMD = exports.HOOK_PKG = exports.ADD_HOOK_PKG_ALLOW_JS = exports.ADD_HOOK_PKG_ALLOW_ERE = exports.INSTALL_HOOKS_ALLOW_JS = exports.INSTALL_HOOKS_ALLOW_ERE = exports.RESTORE_SHIM_CMD = exports.RESTORE_SHIM_ALLOW_JS = exports.RESTORE_SHIM_ALLOW_ERE = exports.UPGRADE_SHIM_CMD = exports.UPGRADE_SHIM_ALLOW_JS = exports.UPGRADE_SHIM_ALLOW_ERE = exports.CHECKOUT_MAIN_PULL_CMD = exports.CHECKOUT_MAIN_PULL_ALLOW_JS = exports.CHECKOUT_MAIN_PULL_ALLOW_ERE = exports.FETCH_ALLOW_JS = exports.FETCH_ALLOW_ERE = exports.RECOVERY_CMD = exports.RECOVERY_ALLOW_JS = exports.RECOVERY_ALLOW_ERE = exports.INSTALLER_ALLOW_JS = exports.INSTALLER_ALLOW_ERE = exports.CD_PREFIX_JS_SRC = exports.CD_PREFIX_ERE = exports.CAPTURE_TAIL_JS_SRC = exports.CAPTURE_TAIL_ERE = exports.MANIFEST_FILENAMES = exports.PACKAGE_MANIFEST = exports.WORKSPACE_MANIFEST = void 0;
4
4
  const rules_config_1 = require("@webpieces/rules-config");
5
+ // The shape of ONE entry, in its own module so the LIST below is not crowded out by the types that
6
+ // describe it. `./shim` stars both, so consumers see no seam.
7
+ const l0_entry_1 = require("./l0-entry");
5
8
  // The harness-GATED half of L0, in its own module for the same size reason this one was split out of
6
9
  // ./shim. It owns the read-shaped vocabulary AND the union built from it; this file owns the entry
7
10
  // that puts it on the list, so the dependency runs one way and there is no cycle.
@@ -415,85 +418,13 @@ exports.INSTALL_HOOKS_TARGET_CMD = 'pnpm wp-install-ai-hooks --target=project';
415
418
  // L0_ALLOWLIST is that list as DATA — the one array isAllowed(), the rendered shim's grep and the
416
419
  // published matrix doc (webpieces.guard-matrix.md) all derive from, so the doc cannot describe an
417
420
  // allowlist the code does not have. Adding an entry here is the ONLY way to widen L0.
418
- // ---------------------------------------------------------------------------
419
- /** One tool call as L0 judges it: the tool name, the Bash command (or ''), the file target (or ''). */
420
- class L0Call {
421
- toolName;
422
- command;
423
- filePath;
424
- constructor(toolName, command, filePath) {
425
- this.toolName = toolName;
426
- this.command = command;
427
- this.filePath = filePath;
428
- }
429
- }
430
- exports.L0Call = L0Call;
431
- /**
432
- * One entry of THE L0 allowlist. Data-only → a class, per CLAUDE.md.
433
- *
434
- * `ere`/`js` are the twin regex BODIES for a Bash entry, or null for a tool-shaped entry (Read, the
435
- * webpieces.config.json target) that no regex can express. `sample` is a call this entry must accept —
436
- * it is what the matrix-coverage and cure-reachability tests drive isAllowed() with.
437
- *
438
- * `extraSamples` pins ADDITIONAL spellings the same entry must accept. A spelling that some deny
439
- * message prescribes belongs here, or nothing stops a later tightening of the pattern from making that
440
- * message's cure untypable again — which is the deadlock shape this whole module exists to prevent.
441
- *
442
- * `cure` is the ONE thing that is not uniform across the list, and it is not about L0 at all — see
443
- * L0_CURE_ALLOW_JS below. Every entry is judged identically while an L0 fault is up; `cure` decides
444
- * only whether the entry ALSO bypasses the downstream (L1) guards on a HEALTHY tree.
445
- */
446
- class L0AllowEntry {
447
- label;
448
- kind;
449
- cure;
450
- ere;
451
- js;
452
- aiType;
453
- sample;
454
- extraSamples;
455
- // eslint-disable-next-line @typescript-eslint/max-params
456
- constructor(label, kind,
457
- /**
458
- * True when this entry REPAIRS the tooling (install, sync, shim restore). A cure has to run
459
- * before webpieces.config.json can even be loaded, so it bypasses everything, always. A
460
- * non-cure (read-only orientation) is allowed while a fault is up and is otherwise an ordinary
461
- * command the downstream guards still judge.
462
- */
463
- cure, ere, js,
464
- /**
465
- * The ONE harness this entry exists for, or null for the entries every harness gets.
466
- *
467
- * A gated entry is unreachable from any other harness — it is spliced into its own union, which
468
- * both halves of L0 consult only after answering "which harness sent this call?". It exists
469
- * because the two harnesses do not have the same TOOLS: `Read` is entry 1 for Claude Code and
470
- * Codex has no such tool, so read parity at L0 can only be expressed per harness. Anything added
471
- * here later must satisfy the property stated on AI_TYPE_SH: the sh half's answer is an
472
- * approximation, so a gated entry may never grant more than the OTHER harness already has.
473
- */
474
- aiType, sample, extraSamples = []) {
475
- this.label = label;
476
- this.kind = kind;
477
- this.cure = cure;
478
- this.ere = ere;
479
- this.js = js;
480
- this.aiType = aiType;
481
- this.sample = sample;
482
- this.extraSamples = extraSamples;
483
- }
484
- /** Every call this entry pins: the canonical sample plus every extra spelling. */
485
- allSamples() {
486
- return [this.sample, ...this.extraSamples];
487
- }
488
- }
489
- exports.L0AllowEntry = L0AllowEntry;
490
421
  exports.L0_ALLOWLIST = [
491
- new L0AllowEntry('any Read', 'pass', false, null, null, null, new L0Call('Read', '', 'README.md')),
422
+ new l0_entry_1.L0AllowEntry('any Read', 'pass', false, null, null, l0_entry_1.EVERY_HARNESS, new l0_entry_1.L0Call('Read', '', 'README.md')),
492
423
  // TOOL-SHAPED, like the Read entry above and for the same reason: no regex can express "this tool
493
424
  // has nothing to judge". See L0_IGNORED_TOOLS for why the list is explicit and why `apply_patch`
494
425
  // (Codex's only WRITE) is deliberately not on it.
495
- new L0AllowEntry(`a Codex tool with nothing to judge: ${[...l0_ignored_tools_1.L0_IGNORED_TOOLS].join(', ')}`, 'pass', false, null, null, null, new L0Call('update_plan', '', ''), [new L0Call('view_image', '', ''), new L0Call('webrun', '', '')]),
496
- new L0AllowEntry(`a Write/Edit whose target is ${rules_config_1.CONFIG_FILENAME}`, 'pass', false, null, null, null, new L0Call('Edit', '', `/repo/${rules_config_1.CONFIG_FILENAME}`)),
426
+ new l0_entry_1.L0AllowEntry(`a Codex tool with nothing to judge: ${[...l0_ignored_tools_1.L0_IGNORED_TOOLS].join(', ')}`, 'pass', false, null, null, l0_entry_1.EVERY_HARNESS, new l0_entry_1.L0Call('update_plan', '', ''), [new l0_entry_1.L0Call('view_image', '', ''), new l0_entry_1.L0Call('webrun', '', '')]),
427
+ new l0_entry_1.L0AllowEntry(`a Write/Edit whose target is ${rules_config_1.CONFIG_FILENAME}`, 'pass', false, null, null, l0_entry_1.EVERY_HARNESS, new l0_entry_1.L0Call('Edit', '', `/repo/${rules_config_1.CONFIG_FILENAME}`)),
497
428
  // THE MANIFEST ESCAPE, and it is an escape L1 row 8 already PROMISED before it existed. Its report
498
429
  // says "STILL ALLOWED HERE: ... edits to pnpm-workspace.yaml / package.json / webpieces.config.json"
499
430
  // and its docblock calls that one of "two structurally independent escapes" — but only
@@ -510,35 +441,35 @@ exports.L0_ALLOWLIST = [
510
441
  // The runner carves the same two files out on its own edit path (an early `return null`, beside the
511
442
  // webpieces.config.json pass). Scoped by isRootManifest, so it admits the root of the MAIN tree and
512
443
  // of every worktree — and no other package.json in the repo.
513
- new L0AllowEntry(`a Write/Edit whose target is a tree ROOT's ${exports.WORKSPACE_MANIFEST} or ${exports.PACKAGE_MANIFEST} - the version pin`, 'pass', false, null, null, null, new L0Call('Edit', '', `/repo/${exports.WORKSPACE_MANIFEST}`), [new L0Call('Write', '', `/repo/${exports.PACKAGE_MANIFEST}`)]),
514
- new L0AllowEntry('pnpm|npm install', 'allow', true, INSTALLER_BODY_ERE, INSTALLER_BODY_JS, null, new L0Call('Bash', 'pnpm install', '')),
515
- new L0AllowEntry(`${exports.RECOVERY_CMD} - the cure for a CORRUPT node_modules`, 'allow', true, RECOVERY_BODY_ERE, RECOVERY_BODY_JS, null, new L0Call('Bash', exports.RECOVERY_CMD, '')),
444
+ new l0_entry_1.L0AllowEntry(`a Write/Edit whose target is a tree ROOT's ${exports.WORKSPACE_MANIFEST} or ${exports.PACKAGE_MANIFEST} - the version pin`, 'pass', false, null, null, l0_entry_1.EVERY_HARNESS, new l0_entry_1.L0Call('Edit', '', `/repo/${exports.WORKSPACE_MANIFEST}`), [new l0_entry_1.L0Call('Write', '', `/repo/${exports.PACKAGE_MANIFEST}`)]),
445
+ new l0_entry_1.L0AllowEntry('pnpm|npm install', 'allow', true, INSTALLER_BODY_ERE, INSTALLER_BODY_JS, l0_entry_1.EVERY_HARNESS, new l0_entry_1.L0Call('Bash', 'pnpm install', '')),
446
+ new l0_entry_1.L0AllowEntry(`${exports.RECOVERY_CMD} - the cure for a CORRUPT node_modules`, 'allow', true, RECOVERY_BODY_ERE, RECOVERY_BODY_JS, l0_entry_1.EVERY_HARNESS, new l0_entry_1.L0Call('Bash', exports.RECOVERY_CMD, '')),
516
447
  // webpieces-disable no-fetch -- prose naming the git sync subcommand in a doc label, not an HTTP call
517
- new L0AllowEntry('git fetch - a bare git pull and git merge are NOT on the list', 'allow', true, FETCH_BODY_ERE, FETCH_BODY_JS, null, new L0Call('Bash', 'git fetch', ''), [new L0Call('Bash', 'git fetch --prune origin main', '')]),
448
+ new l0_entry_1.L0AllowEntry('git fetch - a bare git pull and git merge are NOT on the list', 'allow', true, FETCH_BODY_ERE, FETCH_BODY_JS, l0_entry_1.EVERY_HARNESS, new l0_entry_1.L0Call('Bash', 'git fetch', ''), [new l0_entry_1.L0Call('Bash', 'git fetch --prune origin main', '')]),
518
449
  // The one fork-point-safe pull: it ends on MAIN, so no feature branch is merged into. A bare
519
450
  // `git pull origin main` is deliberately NOT here — see CHECKOUT_MAIN_PULL_BODY_ERE.
520
- new L0AllowEntry(exports.CHECKOUT_MAIN_PULL_CMD, 'allow', true, CHECKOUT_MAIN_PULL_BODY_ERE, CHECKOUT_MAIN_PULL_BODY_JS, null, new L0Call('Bash', exports.CHECKOUT_MAIN_PULL_CMD, '')),
521
- new L0AllowEntry(exports.UPGRADE_SHIM_CMD, 'allow', true, UPGRADE_SHIM_BODY_ERE, UPGRADE_SHIM_BODY_JS, null, new L0Call('Bash', exports.UPGRADE_SHIM_CMD, '')),
522
- new L0AllowEntry(exports.RESTORE_SHIM_CMD, 'allow', true, RESTORE_SHIM_BODY_ERE, RESTORE_SHIM_BODY_JS, null, new L0Call('Bash', exports.RESTORE_SHIM_CMD, '')),
451
+ new l0_entry_1.L0AllowEntry(exports.CHECKOUT_MAIN_PULL_CMD, 'allow', true, CHECKOUT_MAIN_PULL_BODY_ERE, CHECKOUT_MAIN_PULL_BODY_JS, l0_entry_1.EVERY_HARNESS, new l0_entry_1.L0Call('Bash', exports.CHECKOUT_MAIN_PULL_CMD, '')),
452
+ new l0_entry_1.L0AllowEntry(exports.UPGRADE_SHIM_CMD, 'allow', true, UPGRADE_SHIM_BODY_ERE, UPGRADE_SHIM_BODY_JS, l0_entry_1.EVERY_HARNESS, new l0_entry_1.L0Call('Bash', exports.UPGRADE_SHIM_CMD, '')),
453
+ new l0_entry_1.L0AllowEntry(exports.RESTORE_SHIM_CMD, 'allow', true, RESTORE_SHIM_BODY_ERE, RESTORE_SHIM_BODY_JS, l0_entry_1.EVERY_HARNESS, new l0_entry_1.L0Call('Bash', exports.RESTORE_SHIM_CMD, '')),
523
454
  // The cure for unknown keys in webpieces.config.json — see PRUNE_CONFIG_BODY_ERE for why a cure the
524
455
  // messages name must be reachable from inside the block those messages are read under.
525
- new L0AllowEntry(rules_config_1.PRUNE_UNKNOWN_COMMAND, 'allow', true, PRUNE_CONFIG_BODY_ERE, PRUNE_CONFIG_BODY_JS, null, new L0Call('Bash', rules_config_1.PRUNE_UNKNOWN_COMMAND, ''), [new L0Call('Bash', 'pnpm exec wp-prune-unknown-config', '')]),
526
- new L0AllowEntry(`${exports.INSTALL_HOOKS_CMD} (flags allowed, e.g. --target=project)`, 'allow', true, INSTALL_HOOKS_BODY_ERE, INSTALL_HOOKS_BODY_JS, null, new L0Call('Bash', exports.INSTALL_HOOKS_CMD, ''), [new L0Call('Bash', exports.INSTALL_HOOKS_TARGET_CMD, '')]),
456
+ new l0_entry_1.L0AllowEntry(rules_config_1.PRUNE_UNKNOWN_COMMAND, 'allow', true, PRUNE_CONFIG_BODY_ERE, PRUNE_CONFIG_BODY_JS, l0_entry_1.EVERY_HARNESS, new l0_entry_1.L0Call('Bash', rules_config_1.PRUNE_UNKNOWN_COMMAND, ''), [new l0_entry_1.L0Call('Bash', 'pnpm exec wp-prune-unknown-config', '')]),
457
+ new l0_entry_1.L0AllowEntry(`${exports.INSTALL_HOOKS_CMD} (flags allowed, e.g. --target=project)`, 'allow', true, INSTALL_HOOKS_BODY_ERE, INSTALL_HOOKS_BODY_JS, l0_entry_1.EVERY_HARNESS, new l0_entry_1.L0Call('Bash', exports.INSTALL_HOOKS_CMD, ''), [new l0_entry_1.L0Call('Bash', exports.INSTALL_HOOKS_TARGET_CMD, '')]),
527
458
  // The fault-U cure. `@<version>` is pinned as an extra sample because the deny INFERS a pin from the
528
459
  // repo's other @webpieces pins and prescribes the versioned spelling — the exact shape that must not
529
460
  // become untypable under a later tightening.
530
- new L0AllowEntry(`${exports.ADD_HOOK_PKG_CMD} (an @version and extra flags allowed)`, 'allow', true, ADD_HOOK_PKG_BODY_ERE, ADD_HOOK_PKG_BODY_JS, null, new L0Call('Bash', exports.ADD_HOOK_PKG_CMD, ''), [
531
- new L0Call('Bash', `${exports.ADD_HOOK_PKG_CMD}@0.4.574`, ''),
532
- new L0Call('Bash', `pnpm add -D -w ${exports.HOOK_PKG}@0.4.574`, ''),
533
- new L0Call('Bash', `npm add --save-dev ${exports.HOOK_PKG}`, ''),
461
+ new l0_entry_1.L0AllowEntry(`${exports.ADD_HOOK_PKG_CMD} (an @version and extra flags allowed)`, 'allow', true, ADD_HOOK_PKG_BODY_ERE, ADD_HOOK_PKG_BODY_JS, l0_entry_1.EVERY_HARNESS, new l0_entry_1.L0Call('Bash', exports.ADD_HOOK_PKG_CMD, ''), [
462
+ new l0_entry_1.L0Call('Bash', `${exports.ADD_HOOK_PKG_CMD}@0.4.574`, ''),
463
+ new l0_entry_1.L0Call('Bash', `pnpm add -D -w ${exports.HOOK_PKG}@0.4.574`, ''),
464
+ new l0_entry_1.L0Call('Bash', `npm add --save-dev ${exports.HOOK_PKG}`, ''),
534
465
  ]),
535
466
  // NOT a cure (`cure: false`): it repairs nothing, so it must not bypass the L1 guards on a healthy
536
467
  // tree — `git status` from a subdirectory still meets force-to-root. It is on the L0 list because
537
468
  // while a fault is UP you have to be able to see where you are standing.
538
- new L0AllowEntry('read-only orientation: pwd, git status/log/diff/show/branch/rev-parse, git worktree list', 'allow', false, ORIENT_BODY_ERE, ORIENT_BODY_JS, null, new L0Call('Bash', exports.ORIENT_CMD, ''), [
539
- new L0Call('Bash', 'git rev-parse --show-toplevel', ''),
540
- new L0Call('Bash', 'git worktree list', ''),
541
- new L0Call('Bash', 'git status', ''),
469
+ new l0_entry_1.L0AllowEntry('read-only orientation: pwd, git status/log/diff/show/branch/rev-parse, git worktree list', 'allow', false, ORIENT_BODY_ERE, ORIENT_BODY_JS, l0_entry_1.EVERY_HARNESS, new l0_entry_1.L0Call('Bash', exports.ORIENT_CMD, ''), [
470
+ new l0_entry_1.L0Call('Bash', 'git rev-parse --show-toplevel', ''),
471
+ new l0_entry_1.L0Call('Bash', 'git worktree list', ''),
472
+ new l0_entry_1.L0Call('Bash', 'git status', ''),
542
473
  ]),
543
474
  // CODEX ONLY — entry 1's twin for the harness that has no `Read` tool. See CODEX_READ_BODY_ERE for
544
475
  // the deadlock it removes, why it is exactly as wide as core/shell-read-parity.ts, and why it alone
@@ -549,18 +480,18 @@ exports.L0_ALLOWLIST = [
549
480
  // guards. And `pass`, not `allow`, so it mirrors entry 1 exactly: where the bin is running (fault
550
481
  // S) the read falls THROUGH to read-stale-guard, and where it is not (D/X/U/K) the sh half is
551
482
  // terminal by construction, which is the documented asymmetry entry 1 already has.
552
- new L0AllowEntry('CODEX ONLY - a read-shaped Bash command (the harness has no Read tool): '
553
- + `${[...shell_read_parity_1.READ_COMMANDS].join(', ')}, or sed -n '<range>p'`, 'pass', false, l0_codex_read_1.CODEX_READ_BODY_ERE, l0_codex_read_1.CODEX_READ_BODY_JS, 'codex', new L0Call('Bash', l0_codex_read_1.CODEX_READ_CMD, ''), [
554
- new L0Call('Bash', 'cat webpieces.config.json', ''),
555
- new L0Call('Bash', 'head -50 .webpieces/instruct-ai/webpieces.guard-matrix.md', ''),
556
- new L0Call('Bash', 'tail -n 20 package.json', ''),
557
- new L0Call('Bash', "cat '/Users/dean hiller/repo/package.json'", ''),
483
+ new l0_entry_1.L0AllowEntry('CODEX ONLY - a read-shaped Bash command (the harness has no Read tool): '
484
+ + `${[...shell_read_parity_1.READ_COMMANDS].join(', ')}, or sed -n '<range>p'`, 'pass', false, l0_codex_read_1.CODEX_READ_BODY_ERE, l0_codex_read_1.CODEX_READ_BODY_JS, 'codex', new l0_entry_1.L0Call('Bash', l0_codex_read_1.CODEX_READ_CMD, ''), [
485
+ new l0_entry_1.L0Call('Bash', 'cat webpieces.config.json', ''),
486
+ new l0_entry_1.L0Call('Bash', 'head -50 .webpieces/instruct-ai/webpieces.guard-matrix.md', ''),
487
+ new l0_entry_1.L0Call('Bash', 'tail -n 20 package.json', ''),
488
+ new l0_entry_1.L0Call('Bash', "cat '/Users/dean hiller/repo/package.json'", ''),
558
489
  ]),
559
490
  ];
560
491
  // The UNGATED bodies — every entry that is not scoped to one harness. A gated entry is spliced into its
561
492
  // own union below instead, which is what makes it unreachable from the harness it is not for.
562
- const L0_BODIES_ERE = exports.L0_ALLOWLIST.flatMap((e) => (e.ere === null || e.aiType !== null ? [] : [e.ere]));
563
- const L0_BODIES_JS = exports.L0_ALLOWLIST.flatMap((e) => (e.js === null || e.aiType !== null ? [] : [e.js]));
493
+ const L0_BODIES_ERE = exports.L0_ALLOWLIST.flatMap((e) => (e.ere === null || e.harness !== l0_entry_1.EVERY_HARNESS ? [] : [e.ere]));
494
+ const L0_BODIES_JS = exports.L0_ALLOWLIST.flatMap((e) => (e.js === null || e.harness !== l0_entry_1.EVERY_HARNESS ? [] : [e.js]));
564
495
  // The ONE Bash allowlist. Anchored and tailed exactly like each individual hatch, so it inherits every
565
496
  // security property: no shell operator can ride along, and only the optional leading `cd <path> &&` /
566
497
  // trailing `2>&1 | tail -N` are tolerated.
@@ -592,6 +523,6 @@ exports.L0_ALLOW_JS = new RegExp(CD_PREFIX_JS_ANCHORED + '(' + L0_BODIES_JS.join
592
523
  // A harness-GATED entry is excluded here too, and not merely because today's one is a non-cure: this
593
524
  // union is consulted by runner.ts where NO harness has been established either, so a gated body spliced
594
525
  // in would be judged with the gate missing — i.e. reachable from the harness it was written to exclude.
595
- const L0_CURE_BODIES_JS = exports.L0_ALLOWLIST.flatMap((e) => (e.js === null || !e.cure || e.aiType !== null ? [] : [e.js]));
526
+ const L0_CURE_BODIES_JS = exports.L0_ALLOWLIST.flatMap((e) => (e.js === null || !e.cure || e.harness !== l0_entry_1.EVERY_HARNESS ? [] : [e.js]));
596
527
  exports.L0_CURE_ALLOW_JS = new RegExp(CD_PREFIX_JS_ANCHORED + '(' + L0_CURE_BODIES_JS.join('|') + ')' + exports.CAPTURE_TAIL_JS_SRC);
597
528
  //# sourceMappingURL=l0-allowlist.js.map