@dzhechkov/harness-core 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/.dz-manifest.json +283 -103
  2. package/LICENSE +21 -0
  3. package/README.md +73 -5
  4. package/dist/agents-policy.d.ts +67 -0
  5. package/dist/agents-policy.d.ts.map +1 -0
  6. package/dist/agents-policy.js +258 -0
  7. package/dist/agents-policy.js.map +1 -0
  8. package/dist/codex-hooks-assets.d.ts +47 -0
  9. package/dist/codex-hooks-assets.d.ts.map +1 -0
  10. package/dist/codex-hooks-assets.js +287 -0
  11. package/dist/codex-hooks-assets.js.map +1 -0
  12. package/dist/codex-hooks-verify.d.ts +74 -0
  13. package/dist/codex-hooks-verify.d.ts.map +1 -0
  14. package/dist/codex-hooks-verify.js +140 -0
  15. package/dist/codex-hooks-verify.js.map +1 -0
  16. package/dist/codex-hooks.d.ts +258 -0
  17. package/dist/codex-hooks.d.ts.map +1 -0
  18. package/dist/codex-hooks.js +391 -0
  19. package/dist/codex-hooks.js.map +1 -0
  20. package/dist/discrimination-gate.d.ts +88 -15
  21. package/dist/discrimination-gate.d.ts.map +1 -1
  22. package/dist/discrimination-gate.js +343 -51
  23. package/dist/discrimination-gate.js.map +1 -1
  24. package/dist/feature-adr-checkpoints.d.ts +22 -0
  25. package/dist/feature-adr-checkpoints.d.ts.map +1 -1
  26. package/dist/feature-adr-checkpoints.js +42 -0
  27. package/dist/feature-adr-checkpoints.js.map +1 -1
  28. package/dist/feature-adr-routing.d.ts +196 -5
  29. package/dist/feature-adr-routing.d.ts.map +1 -1
  30. package/dist/feature-adr-routing.js +538 -54
  31. package/dist/feature-adr-routing.js.map +1 -1
  32. package/dist/guard.d.ts +13 -0
  33. package/dist/guard.d.ts.map +1 -1
  34. package/dist/guard.js +25 -1
  35. package/dist/guard.js.map +1 -1
  36. package/dist/index.d.ts +17 -7
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +23 -4
  39. package/dist/index.js.map +1 -1
  40. package/dist/loop-blobs.generated.js +2 -2
  41. package/dist/loop-blobs.generated.js.map +1 -1
  42. package/dist/managed-hooks.d.ts +76 -0
  43. package/dist/managed-hooks.d.ts.map +1 -0
  44. package/dist/managed-hooks.js +89 -0
  45. package/dist/managed-hooks.js.map +1 -0
  46. package/dist/mutation-gate.d.ts +14 -0
  47. package/dist/mutation-gate.d.ts.map +1 -1
  48. package/dist/mutation-gate.js +25 -2
  49. package/dist/mutation-gate.js.map +1 -1
  50. package/dist/operations.d.ts +153 -0
  51. package/dist/operations.d.ts.map +1 -1
  52. package/dist/operations.js +560 -24
  53. package/dist/operations.js.map +1 -1
  54. package/dist/parity.d.ts +38 -1
  55. package/dist/parity.d.ts.map +1 -1
  56. package/dist/parity.js +78 -5
  57. package/dist/parity.js.map +1 -1
  58. package/dist/recall-usage.d.ts +53 -0
  59. package/dist/recall-usage.d.ts.map +1 -1
  60. package/dist/recall-usage.js +125 -2
  61. package/dist/recall-usage.js.map +1 -1
  62. package/dist/setup.d.ts.map +1 -1
  63. package/dist/setup.js +14 -26
  64. package/dist/setup.js.map +1 -1
  65. package/dist/shell-veto-policy.d.ts +53 -0
  66. package/dist/shell-veto-policy.d.ts.map +1 -0
  67. package/dist/shell-veto-policy.js +103 -0
  68. package/dist/shell-veto-policy.js.map +1 -0
  69. package/dist/skills.d.ts +86 -1
  70. package/dist/skills.d.ts.map +1 -1
  71. package/dist/skills.js +116 -1
  72. package/dist/skills.js.map +1 -1
  73. package/dist/targets.d.ts +75 -0
  74. package/dist/targets.d.ts.map +1 -1
  75. package/dist/targets.js +160 -0
  76. package/dist/targets.js.map +1 -1
  77. package/package.json +20 -19
  78. package/sbom.json +552 -102
  79. package/src/agents-policy.ts +338 -0
  80. package/src/codex-hooks-assets.ts +291 -0
  81. package/src/codex-hooks-verify.ts +184 -0
  82. package/src/codex-hooks.ts +571 -0
  83. package/src/discrimination-gate.ts +456 -58
  84. package/src/feature-adr-checkpoints.ts +38 -0
  85. package/src/feature-adr-routing.ts +642 -75
  86. package/src/guard.ts +36 -1
  87. package/src/index.ts +118 -2
  88. package/src/loop-blobs.generated.ts +2 -2
  89. package/src/managed-hooks.ts +129 -0
  90. package/src/mutation-gate.ts +24 -2
  91. package/src/operations.ts +719 -28
  92. package/src/parity.ts +120 -6
  93. package/src/recall-usage.ts +184 -1
  94. package/src/setup.ts +26 -27
  95. package/src/shell-veto-policy.ts +119 -0
  96. package/src/skills.ts +174 -1
  97. package/src/targets.ts +189 -0
@@ -0,0 +1,287 @@
1
+ /**
2
+ * The emitted Codex helper bodies (`crossrt-2-codex-hooks`, ADR-003 / ADR-004).
3
+ *
4
+ * Generated-code-as-a-string, following the `generateAgentdbWriter` precedent (`setup.ts`): the
5
+ * helpers are THIN and the logic lives in the package, because a changed helper body changes
6
+ * codex's `currentHash` and disarms the entry until it is re-trusted (MEASURED — M0 spike §4).
7
+ * Every byte in here is therefore a liability; keep them boring.
8
+ *
9
+ * ## The contracts, side by side
10
+ *
11
+ * | | `dz-codex-veto.cjs` (PreToolUse) | `dz-codex-recall.cjs` (UserPromptSubmit) |
12
+ * |----------------|-----------------------------------------------------|------------------------------------------|
13
+ * | Polarity | mode-driven on POLICY (default `warn` ⇒ exit 0) | **never-block, always** |
14
+ * | Keys on | presence of `tool_input.command` (AM-8) | `payload.prompt` |
15
+ * | Activation | nearest `.dz` walking up from `payload.cwd` (AM-25) | same |
16
+ * | Our own failure| exit 0, silent, note to `helper-errors.jsonl` | exit 0, empty stdout, no write (AM-9) |
17
+ *
18
+ * `.git` is **not** an activation marker (AM-25): a user-global hook that treats "any git checkout"
19
+ * as opted-in reaches every repository on the machine, including trees nobody pointed at dz.
20
+ *
21
+ * The self-failure note goes to `$CODEX_HOME/dz-hooks/helper-errors.jsonl` (AM-33), never into a
22
+ * project — writing it into `<project>/.dz/` would CREATE a `.dz/` in a foreign repo, which is the
23
+ * exact thing the activation rule exists to prevent.
24
+ *
25
+ * A `UserPromptSubmit` hook that exits 2 **blocks the user's turn**, so the recall helper has no
26
+ * path to a non-zero exit at all.
27
+ *
28
+ * @packageDocumentation
29
+ */
30
+ import { DZ_HOOK_HELPER_VERSION } from './codex-hooks.js';
31
+ /** Shared prologue: payload read, project-root walk, never-throw error note. */
32
+ function preamble() {
33
+ return `#!/usr/bin/env node
34
+ // GENERATED by @dzhechkov/harness-core — dz-hook-helper-version: ${DZ_HOOK_HELPER_VERSION}
35
+ // Do not edit: dz rewrites this file, and any edit disarms the hook until it is re-trusted.
36
+ 'use strict';
37
+ const fs = require('node:fs');
38
+ const path = require('node:path');
39
+
40
+ const HELPER_VERSION = ${DZ_HOOK_HELPER_VERSION};
41
+ const CODEX_HOME = process.env.CODEX_HOME || path.join(require('node:os').homedir(), '.codex');
42
+ const ERROR_LOG = path.join(CODEX_HOME, 'dz-hooks', 'helper-errors.jsonl');
43
+
44
+ /**
45
+ * Append one line to the helper NOTES log. Never throws. Never writes into a project.
46
+ *
47
+ * Carries BOTH self-failures and policy WARNINGS. MEASURED (M9 live probe, 2026-08-19): codex
48
+ * surfaces a hook's stderr in the transcript ONLY when the hook exits 2 — an exit-0 hook's stderr is
49
+ * swallowed. So a warn that lived only on stderr would be unobservable, and "the guard is live but
50
+ * not blocking" (G-J) would be unprovable. This file is where it is observable.
51
+ */
52
+ function note(stage, err, extra) {
53
+ try {
54
+ fs.appendFileSync(
55
+ ERROR_LOG,
56
+ JSON.stringify(Object.assign(
57
+ { ts: new Date().toISOString(), helper: HELPER, helperVersion: HELPER_VERSION, stage, message: String((err && err.message) || err || '') },
58
+ extra || {},
59
+ )) + '\\n',
60
+ );
61
+ } catch (_) { /* the note is best-effort; it must never become the failure */ }
62
+ }
63
+
64
+ function readPayload() {
65
+ try {
66
+ const raw = fs.readFileSync(0, 'utf8');
67
+ const parsed = JSON.parse(raw);
68
+ return parsed && typeof parsed === 'object' ? parsed : null;
69
+ } catch (err) {
70
+ note('read-payload', err);
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Walk UP from cwd to the nearest directory containing a \`.dz\` DIRECTORY.
77
+ * \`.git\` is deliberately not a marker (AM-25). No root => the helper is inert.
78
+ */
79
+ function findProjectRoot(startDir) {
80
+ try {
81
+ let dir = path.resolve(startDir);
82
+ for (let i = 0; i < 64; i += 1) {
83
+ try {
84
+ if (fs.statSync(path.join(dir, '.dz')).isDirectory()) return dir;
85
+ } catch (_) { /* not here; keep walking */ }
86
+ const parent = path.dirname(dir);
87
+ if (parent === dir) return null;
88
+ dir = parent;
89
+ }
90
+ return null;
91
+ } catch (err) {
92
+ note('find-root', err);
93
+ return null;
94
+ }
95
+ }
96
+
97
+ function readProjectConfig(root) {
98
+ try {
99
+ return JSON.parse(fs.readFileSync(path.join(root, '.dz', 'config.json'), 'utf8'));
100
+ } catch (_) {
101
+ return null; // absent or unreadable config is the DEFAULT case, not an error
102
+ }
103
+ }
104
+
105
+ /** Load a built module from the project's own harness-core, or null. Never throws. */
106
+ function loadCore(root, fileName, predicate) {
107
+ const candidates = [
108
+ path.join(root, 'node_modules', '@dzhechkov', 'harness-core', 'dist', fileName),
109
+ path.join(root, 'packages', '@dzhechkov', 'harness-core', 'dist', fileName),
110
+ ];
111
+ for (const candidate of candidates) {
112
+ try {
113
+ if (!fs.existsSync(candidate)) continue;
114
+ const mod = require(candidate);
115
+ if (predicate(mod)) return mod;
116
+ } catch (err) {
117
+ note('load-core:' + fileName, err);
118
+ }
119
+ }
120
+ return null;
121
+ }
122
+ `;
123
+ }
124
+ /**
125
+ * The PreToolUse veto helper.
126
+ *
127
+ * Fail-OPEN on OUR failure, mode-driven on a POLICY hit. Exit 2 only when the project explicitly
128
+ * opted in — the shipped default warns and returns 0 (AM-24 / G-J).
129
+ */
130
+ export function generateCodexVetoHelper() {
131
+ return `${preamble().replace('const HELPER_VERSION', "const HELPER = 'dz-codex-veto';\nconst HELPER_VERSION")}
132
+ function main() {
133
+ const payload = readPayload();
134
+ if (payload === null) return 0;
135
+
136
+ // AM-8: key on the PRESENCE of a command, never on tool_name equality. MEASURED payload
137
+ // (M0 spike): { tool_name: 'Bash', tool_input: { command: "…" }, cwd: "…" }.
138
+ const input = payload.tool_input;
139
+ const command = input && typeof input === 'object' ? input.command : undefined;
140
+ if (typeof command !== 'string' || command === '') return 0;
141
+
142
+ const cwd = typeof payload.cwd === 'string' && payload.cwd !== '' ? payload.cwd : process.env.PWD || process.cwd();
143
+ const root = findProjectRoot(cwd);
144
+ if (root === null) return 0; // inert outside an opted-in dz project: no decision, no output, no write
145
+
146
+ const policy = loadCore(root, 'shell-veto-policy.js', (m) => typeof m.vetoShellCommand === 'function');
147
+ if (policy === null) return 0; // no logic available => allow (never block on OUR failure)
148
+
149
+ let hit = null;
150
+ let mode = 'warn';
151
+ try {
152
+ hit = policy.vetoShellCommand(command);
153
+ mode = policy.resolveVetoMode(readProjectConfig(root));
154
+ } catch (err) {
155
+ note('policy', err);
156
+ return 0;
157
+ }
158
+ if (hit === null || mode === 'off') return 0;
159
+
160
+ if (mode === 'block') {
161
+ process.stderr.write('DZ-VETO: ' + hit.rule + ' — ' + hit.reason + ' — command: ' + command + '\\n');
162
+ note('veto-block', null, { marker: 'DZ-VETO', rule: hit.rule, command: command, mode: mode });
163
+ return 2;
164
+ }
165
+ const warnLine = 'DZ-VETO-WARN: ' + hit.rule + ' — ' + hit.reason + ' — command: ' + command;
166
+ process.stderr.write(warnLine + '\\n');
167
+ // Codex swallows an exit-0 hook's stderr (MEASURED), so the warn is ALSO recorded where it can be
168
+ // read back — otherwise the shipped default would be live-but-unobservable.
169
+ note('veto-warn', null, { marker: 'DZ-VETO-WARN', rule: hit.rule, command: command, mode: mode });
170
+ return 0;
171
+ }
172
+
173
+ let code = 0;
174
+ try {
175
+ code = main();
176
+ } catch (err) {
177
+ note('main', err);
178
+ code = 0;
179
+ }
180
+ process.exit(code === 2 ? 2 : 0);
181
+ `;
182
+ }
183
+ /**
184
+ * The UserPromptSubmit recall helper.
185
+ *
186
+ * NEVER blocks and NEVER exits non-zero (AM-9). Silence is its correct output when nothing is
187
+ * relevant, which is exactly why the acceptance canary is a FORCED HIT plus a removed-hook twin
188
+ * (AM-4) — a dead hook and a correctly-silent one are indistinguishable from the outside.
189
+ */
190
+ export function generateCodexRecallHelper() {
191
+ return `${preamble().replace('const HELPER_VERSION', "const HELPER = 'dz-codex-recall';\nconst HELPER_VERSION")}
192
+ const net = require('node:net');
193
+ const DAEMON_TIMEOUT_MS = 2000;
194
+
195
+ /** Ask the project's embed daemon for candidates. Resolves undefined on ANY problem. */
196
+ function askDaemon(root, prompt) {
197
+ return new Promise((resolve) => {
198
+ const socket = process.env.DZ_EMBED_SOCKET || path.join(root, '.dz', 'embed.sock');
199
+ let exists = false;
200
+ try { exists = fs.existsSync(socket); } catch (_) { exists = false; }
201
+ if (!exists) return resolve(undefined);
202
+ let settled = false;
203
+ const done = (v) => {
204
+ if (settled) return;
205
+ settled = true;
206
+ try { sock.destroy(); } catch (_) { /* ignore */ }
207
+ resolve(v);
208
+ };
209
+ const sock = net.connect(socket);
210
+ const timer = setTimeout(() => done(undefined), DAEMON_TIMEOUT_MS);
211
+ if (typeof timer.unref === 'function') timer.unref();
212
+ let buf = '';
213
+ sock.on('connect', () => sock.write(JSON.stringify({ op: 'recall', prompt: prompt, limit: 8 }) + '\\n'));
214
+ sock.on('data', (chunk) => {
215
+ buf += chunk.toString('utf8');
216
+ const nl = buf.indexOf('\\n');
217
+ if (nl === -1) return;
218
+ clearTimeout(timer);
219
+ let msg = null;
220
+ try { msg = JSON.parse(buf.slice(0, nl)); } catch (_) { msg = null; }
221
+ done(msg && Array.isArray(msg.hits) ? msg.hits : undefined);
222
+ });
223
+ sock.on('error', () => { clearTimeout(timer); done(undefined); });
224
+ });
225
+ }
226
+
227
+ async function main() {
228
+ const payload = readPayload();
229
+ if (payload === null) return;
230
+ const prompt = typeof payload.prompt === 'string' ? payload.prompt : '';
231
+ if (prompt.trim() === '') return;
232
+
233
+ const cwd = typeof payload.cwd === 'string' && payload.cwd !== '' ? payload.cwd : process.env.PWD || process.cwd();
234
+ const root = findProjectRoot(cwd);
235
+ if (root === null) return; // inert outside an opted-in dz project
236
+
237
+ const policy = loadCore(root, 'recall-hook-policy.js', (m) => typeof m.selectHookHits === 'function');
238
+ if (policy === null) return;
239
+
240
+ const candidates = await askDaemon(root, prompt);
241
+ if (!candidates || candidates.length === 0) return; // daemon dead or nothing relevant: silence
242
+
243
+ let selection = null;
244
+ try {
245
+ selection = policy.selectHookHits(prompt, candidates);
246
+ } catch (err) {
247
+ note('select-hits', err);
248
+ return;
249
+ }
250
+ if (!selection || !Array.isArray(selection.hits) || selection.hits.length === 0) return;
251
+
252
+ let context = '';
253
+ try { context = policy.renderHookContext(selection); } catch (err) { note('render', err); return; }
254
+ if (context === '') return; // empty context => print NOTHING (an empty block is noise)
255
+
256
+ process.stdout.write(
257
+ JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: context } }) + '\\n',
258
+ );
259
+
260
+ // The usage row carries runtime: 'codex' (ADR-003 §3) through the SHARED chained appender in
261
+ // harness-core — ONE writer implementation, two callers (AM-6/AM-7). Written AFTER stdout so a
262
+ // logging failure can never cost the user their injection.
263
+ const usage = loadCore(root, 'recall-usage.js', (m) => typeof m.appendRecallUsage === 'function');
264
+ if (usage === null) return;
265
+ try {
266
+ usage.appendRecallUsage({
267
+ projectRoot: root,
268
+ runtime: 'codex',
269
+ query: prompt,
270
+ runId: typeof payload.session_id === 'string' ? payload.session_id : undefined,
271
+ hits: selection.hits.map((h) => ({ dzId: h.dzId, score: h.score })),
272
+ });
273
+ } catch (err) {
274
+ note('append-usage', err);
275
+ }
276
+ }
277
+
278
+ main()
279
+ .catch((err) => note('main', err))
280
+ .finally(() => process.exit(0)); // a UserPromptSubmit exit 2 BLOCKS the user's turn
281
+ `;
282
+ }
283
+ /** Both helper bodies, keyed by the file they are written to. */
284
+ export function generateCodexHelpers() {
285
+ return { veto: generateCodexVetoHelper(), recall: generateCodexRecallHelper() };
286
+ }
287
+ //# sourceMappingURL=codex-hooks-assets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-hooks-assets.js","sourceRoot":"","sources":["../src/codex-hooks-assets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,gFAAgF;AAChF,SAAS,QAAQ;IACf,OAAO;oEAC2D,sBAAsB;;;;;;yBAMjE,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkF9C,CAAC;AACF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB;IACrC,OAAO,GAAG,QAAQ,EAAE,CAAC,OAAO,CAAC,sBAAsB,EAAE,uDAAuD,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkD9G,CAAC;AACF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,yBAAyB;IACvC,OAAO,GAAG,QAAQ,EAAE,CAAC,OAAO,CAAC,sBAAsB,EAAE,yDAAyD,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0FhH,CAAC;AACF,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,oBAAoB;IAClC,OAAO,EAAE,IAAI,EAAE,uBAAuB,EAAE,EAAE,MAAM,EAAE,yBAAyB,EAAE,EAAE,CAAC;AAClF,CAAC"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The fail-closed veto-probe classifier (`crossrt-2-codex-hooks`, ADR-002).
3
+ *
4
+ * Turns the evidence of ONE live probe run into a two-axis verdict. Pure: it never reads a file,
5
+ * never runs a command, and never consults its own emitter's intent — the whole point is that
6
+ * "we wrote the hook" is not evidence that the hook fired.
7
+ *
8
+ * ## The two axes (AM-16)
9
+ *
10
+ * `verdict` answers *did OUR hook block the forbidden command?* and comes from the TRANSCRIPT plus
11
+ * the sentinel side effect. `trust` answers *is the entry armed in the runtime's own view?* and
12
+ * comes from `hooks/list`'s `trustStatus` (MEASURED available headlessly — M0 spike, probe 1).
13
+ *
14
+ * ## No path defaults to a pass
15
+ *
16
+ * Every branch states its own verdict; there is no `??` fallback and no `else` that guesses. The
17
+ * four ways a probe can LOOK like a pass without being one, each closed here:
18
+ *
19
+ * 1. **A bypassed run.** `--dangerously-bypass-hook-trust` makes an UNTRUSTED hook fire, so a block
20
+ * observed under it says nothing about the installed state (AM-1). Decided FIRST.
21
+ * 2. **Somebody else's block.** MEASURED (M0 §5): the runtime prefixes our stderr with its own
22
+ * `Command blocked by PreToolUse hook: `. That phrase WITHOUT `DZ-VETO:` means *a* hook blocked,
23
+ * not that ours did.
24
+ * 3. **A command that never ran.** "the model declined" and "the command was blocked" both show an
25
+ * absent sentinel; only the transcript separates them (AM-36).
26
+ * 4. **A stale transcript.** Without this run's nonce the text is evidence about some other run.
27
+ *
28
+ * @packageDocumentation
29
+ */
30
+ export type VetoVerdict = 'armed' | 'not-armed' | 'inconclusive';
31
+ export type VetoTrust = 'trusted' | 'trust-pending' | 'unknown';
32
+ /** The runtime's own `HookTrustStatus` vocabulary (v2 `HooksListResponse.json`, codex 0.147.0). */
33
+ export type CodexHookTrustStatus = 'managed' | 'untrusted' | 'trusted' | 'modified';
34
+ /** Our block marker. Must appear verbatim for a block to be attributed to dz. */
35
+ export declare const DZ_VETO_MARKER = "DZ-VETO:";
36
+ /** The default-mode (warn) marker — a hit that did NOT block (G-J). */
37
+ export declare const DZ_VETO_WARN_MARKER = "DZ-VETO-WARN:";
38
+ /** The runtime's generic block phrase. Present WITHOUT our marker ⇒ not our block. */
39
+ export declare const RUNTIME_BLOCK_PHRASE = "Command blocked by PreToolUse hook";
40
+ export interface VetoProbeEvidence {
41
+ /** stdout+stderr of the probe run, verbatim. */
42
+ readonly transcript: string;
43
+ /** Per-run nonce embedded in the probe command; guards against grading a stale transcript. */
44
+ readonly nonce: string;
45
+ /** Did the sentinel side effect land? `true` ⇒ the command RAN. */
46
+ readonly sentinelPresent: boolean;
47
+ /** Did the transcript show the model actually attempting the shell command? */
48
+ readonly shellAttempted: boolean;
49
+ /** Was `--dangerously-bypass-hook-trust` in play? `true` can never yield a pass. */
50
+ readonly bypassedTrust: boolean;
51
+ /** From `hooks/list`. Absent ⇒ trust could not be established, which is not the same as bad. */
52
+ readonly trustStatus?: CodexHookTrustStatus | undefined;
53
+ /** The codex version the manifest was written against. */
54
+ readonly recordedCodexVersion?: string | undefined;
55
+ /** The codex version observed now. A mismatch re-asserts stale facts, so it is inconclusive. */
56
+ readonly probedCodexVersion?: string | undefined;
57
+ }
58
+ export interface VetoProbeResult {
59
+ readonly verdict: VetoVerdict;
60
+ readonly trust: VetoTrust;
61
+ readonly reason: string;
62
+ }
63
+ /** Trust axis. Derived only from the runtime's own report, never from our emitter. */
64
+ export declare function classifyTrust(status: VetoProbeEvidence['trustStatus']): VetoTrust;
65
+ /** Classify one probe run. Exhaustive; no branch falls through to a pass. */
66
+ export declare function classifyVetoProbe(e: VetoProbeEvidence): VetoProbeResult;
67
+ /**
68
+ * Exit map (ADR-002 §5). `0` demands BOTH axes: an `armed` run whose entry is only `trust-pending`
69
+ * exits non-zero, because the next session may not fire it.
70
+ */
71
+ export declare function verifyExitCode(r: VetoProbeResult): 0 | 1 | 3;
72
+ /** True only for the one outcome that may print a success word (AM-17 / G-G). */
73
+ export declare function isReadyVerdict(r: VetoProbeResult): boolean;
74
+ //# sourceMappingURL=codex-hooks-verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-hooks-verify.d.ts","sourceRoot":"","sources":["../src/codex-hooks-verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,WAAW,GAAG,cAAc,CAAC;AACjE,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,eAAe,GAAG,SAAS,CAAC;AAEhE,mGAAmG;AACnG,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,UAAU,CAAC;AAEpF,iFAAiF;AACjF,eAAO,MAAM,cAAc,aAAa,CAAC;AACzC,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,sFAAsF;AACtF,eAAO,MAAM,oBAAoB,uCAAuC,CAAC;AAEzE,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,8FAA8F;IAC9F,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,mEAAmE;IACnE,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,+EAA+E;IAC/E,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,oFAAoF;IACpF,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,gGAAgG;IAChG,QAAQ,CAAC,WAAW,CAAC,EAAE,oBAAoB,GAAG,SAAS,CAAC;IACxD,0DAA0D;IAC1D,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnD,gGAAgG;IAChG,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClD;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,MAAM,EAAE,iBAAiB,CAAC,aAAa,CAAC,GAAG,SAAS,CAWjF;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,iBAAiB,GAAG,eAAe,CAqFvE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,eAAe,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAI5D;AAED,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,CAAC,EAAE,eAAe,GAAG,OAAO,CAE1D"}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * The fail-closed veto-probe classifier (`crossrt-2-codex-hooks`, ADR-002).
3
+ *
4
+ * Turns the evidence of ONE live probe run into a two-axis verdict. Pure: it never reads a file,
5
+ * never runs a command, and never consults its own emitter's intent — the whole point is that
6
+ * "we wrote the hook" is not evidence that the hook fired.
7
+ *
8
+ * ## The two axes (AM-16)
9
+ *
10
+ * `verdict` answers *did OUR hook block the forbidden command?* and comes from the TRANSCRIPT plus
11
+ * the sentinel side effect. `trust` answers *is the entry armed in the runtime's own view?* and
12
+ * comes from `hooks/list`'s `trustStatus` (MEASURED available headlessly — M0 spike, probe 1).
13
+ *
14
+ * ## No path defaults to a pass
15
+ *
16
+ * Every branch states its own verdict; there is no `??` fallback and no `else` that guesses. The
17
+ * four ways a probe can LOOK like a pass without being one, each closed here:
18
+ *
19
+ * 1. **A bypassed run.** `--dangerously-bypass-hook-trust` makes an UNTRUSTED hook fire, so a block
20
+ * observed under it says nothing about the installed state (AM-1). Decided FIRST.
21
+ * 2. **Somebody else's block.** MEASURED (M0 §5): the runtime prefixes our stderr with its own
22
+ * `Command blocked by PreToolUse hook: `. That phrase WITHOUT `DZ-VETO:` means *a* hook blocked,
23
+ * not that ours did.
24
+ * 3. **A command that never ran.** "the model declined" and "the command was blocked" both show an
25
+ * absent sentinel; only the transcript separates them (AM-36).
26
+ * 4. **A stale transcript.** Without this run's nonce the text is evidence about some other run.
27
+ *
28
+ * @packageDocumentation
29
+ */
30
+ /** Our block marker. Must appear verbatim for a block to be attributed to dz. */
31
+ export const DZ_VETO_MARKER = 'DZ-VETO:';
32
+ /** The default-mode (warn) marker — a hit that did NOT block (G-J). */
33
+ export const DZ_VETO_WARN_MARKER = 'DZ-VETO-WARN:';
34
+ /** The runtime's generic block phrase. Present WITHOUT our marker ⇒ not our block. */
35
+ export const RUNTIME_BLOCK_PHRASE = 'Command blocked by PreToolUse hook';
36
+ /** Trust axis. Derived only from the runtime's own report, never from our emitter. */
37
+ export function classifyTrust(status) {
38
+ switch (status) {
39
+ case 'trusted':
40
+ case 'managed':
41
+ return 'trusted';
42
+ case 'untrusted':
43
+ case 'modified':
44
+ return 'trust-pending';
45
+ default:
46
+ return 'unknown';
47
+ }
48
+ }
49
+ /** Classify one probe run. Exhaustive; no branch falls through to a pass. */
50
+ export function classifyVetoProbe(e) {
51
+ const transcript = typeof e.transcript === 'string' ? e.transcript : '';
52
+ // 1. A bypassed run is a DIAGNOSTIC, never a verdict (AM-1). Decided before any other evidence.
53
+ if (e.bypassedTrust === true) {
54
+ return {
55
+ verdict: 'inconclusive',
56
+ trust: 'unknown',
57
+ reason: 'run used --dangerously-bypass-hook-trust: it proves the helper body works, not that the installed entry is armed',
58
+ };
59
+ }
60
+ const trust = classifyTrust(e.trustStatus);
61
+ // 2. Stale facts are not facts (AM-10).
62
+ const recorded = e.recordedCodexVersion;
63
+ const probed = e.probedCodexVersion;
64
+ if (typeof recorded === 'string' && typeof probed === 'string' && recorded !== probed) {
65
+ return {
66
+ verdict: 'inconclusive',
67
+ trust,
68
+ reason: `codex version mismatch: manifest recorded ${recorded}, runtime reports ${probed} — re-probe before trusting the verdict`,
69
+ };
70
+ }
71
+ // 3. A stale or foreign transcript cannot grade THIS run.
72
+ if (typeof e.nonce !== 'string' || e.nonce === '' || !transcript.includes(e.nonce)) {
73
+ return {
74
+ verdict: 'inconclusive',
75
+ trust,
76
+ reason: "transcript does not carry this run's nonce — it is not evidence about this run",
77
+ };
78
+ }
79
+ // 4. "the command never ran" and "the command was blocked" both show an absent sentinel (AM-36).
80
+ if (e.shellAttempted !== true) {
81
+ return {
82
+ verdict: 'inconclusive',
83
+ trust,
84
+ reason: 'transcript shows no shell attempt: the model declined or rewrote the command — re-drive the probe',
85
+ };
86
+ }
87
+ const ourMarker = transcript.includes(DZ_VETO_MARKER);
88
+ // 5. Somebody ELSE's block is not ours (MEASURED: the runtime prefixes our stderr, M0 §5).
89
+ if (!ourMarker && transcript.includes(RUNTIME_BLOCK_PHRASE)) {
90
+ return {
91
+ verdict: 'inconclusive',
92
+ trust,
93
+ reason: `a PreToolUse hook blocked the command but the transcript carries no ${DZ_VETO_MARKER} marker — the block is not attributable to dz`,
94
+ };
95
+ }
96
+ if (ourMarker) {
97
+ if (e.sentinelPresent === true) {
98
+ return {
99
+ verdict: 'inconclusive',
100
+ trust,
101
+ reason: 'contradictory evidence: our block marker is present AND the sentinel side effect landed',
102
+ };
103
+ }
104
+ return {
105
+ verdict: 'armed',
106
+ trust,
107
+ reason: 'our marker present and the sentinel absent: the forbidden command was blocked by the dz hook',
108
+ };
109
+ }
110
+ // 6. No marker, and the command demonstrably ran.
111
+ if (e.sentinelPresent === true) {
112
+ return {
113
+ verdict: 'not-armed',
114
+ trust,
115
+ reason: 'the forbidden command ran to completion (sentinel present) with no dz block marker',
116
+ };
117
+ }
118
+ // 7. Attempted, no marker, no sentinel: something swallowed it. Missing evidence is not clean evidence.
119
+ return {
120
+ verdict: 'inconclusive',
121
+ trust,
122
+ reason: 'shell was attempted but neither a dz block marker nor the sentinel is present — evidence is missing, not clean',
123
+ };
124
+ }
125
+ /**
126
+ * Exit map (ADR-002 §5). `0` demands BOTH axes: an `armed` run whose entry is only `trust-pending`
127
+ * exits non-zero, because the next session may not fire it.
128
+ */
129
+ export function verifyExitCode(r) {
130
+ if (r.verdict === 'armed' && r.trust === 'trusted')
131
+ return 0;
132
+ if (r.verdict === 'inconclusive')
133
+ return 3;
134
+ return 1;
135
+ }
136
+ /** True only for the one outcome that may print a success word (AM-17 / G-G). */
137
+ export function isReadyVerdict(r) {
138
+ return r.verdict === 'armed' && r.trust === 'trusted';
139
+ }
140
+ //# sourceMappingURL=codex-hooks-verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-hooks-verify.js","sourceRoot":"","sources":["../src/codex-hooks-verify.ts"],"names":[],"mappings":"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"}