@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.
- package/.dz-manifest.json +283 -103
- package/LICENSE +21 -0
- package/README.md +73 -5
- package/dist/agents-policy.d.ts +67 -0
- package/dist/agents-policy.d.ts.map +1 -0
- package/dist/agents-policy.js +258 -0
- package/dist/agents-policy.js.map +1 -0
- package/dist/codex-hooks-assets.d.ts +47 -0
- package/dist/codex-hooks-assets.d.ts.map +1 -0
- package/dist/codex-hooks-assets.js +287 -0
- package/dist/codex-hooks-assets.js.map +1 -0
- package/dist/codex-hooks-verify.d.ts +74 -0
- package/dist/codex-hooks-verify.d.ts.map +1 -0
- package/dist/codex-hooks-verify.js +140 -0
- package/dist/codex-hooks-verify.js.map +1 -0
- package/dist/codex-hooks.d.ts +258 -0
- package/dist/codex-hooks.d.ts.map +1 -0
- package/dist/codex-hooks.js +391 -0
- package/dist/codex-hooks.js.map +1 -0
- package/dist/discrimination-gate.d.ts +88 -15
- package/dist/discrimination-gate.d.ts.map +1 -1
- package/dist/discrimination-gate.js +343 -51
- package/dist/discrimination-gate.js.map +1 -1
- package/dist/feature-adr-checkpoints.d.ts +22 -0
- package/dist/feature-adr-checkpoints.d.ts.map +1 -1
- package/dist/feature-adr-checkpoints.js +42 -0
- package/dist/feature-adr-checkpoints.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +196 -5
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +538 -54
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard.d.ts +13 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +25 -1
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +17 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -4
- package/dist/index.js.map +1 -1
- package/dist/loop-blobs.generated.js +2 -2
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/managed-hooks.d.ts +76 -0
- package/dist/managed-hooks.d.ts.map +1 -0
- package/dist/managed-hooks.js +89 -0
- package/dist/managed-hooks.js.map +1 -0
- package/dist/mutation-gate.d.ts +14 -0
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +25 -2
- package/dist/mutation-gate.js.map +1 -1
- package/dist/operations.d.ts +153 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +560 -24
- package/dist/operations.js.map +1 -1
- package/dist/parity.d.ts +38 -1
- package/dist/parity.d.ts.map +1 -1
- package/dist/parity.js +78 -5
- package/dist/parity.js.map +1 -1
- package/dist/recall-usage.d.ts +53 -0
- package/dist/recall-usage.d.ts.map +1 -1
- package/dist/recall-usage.js +125 -2
- package/dist/recall-usage.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +14 -26
- package/dist/setup.js.map +1 -1
- package/dist/shell-veto-policy.d.ts +53 -0
- package/dist/shell-veto-policy.d.ts.map +1 -0
- package/dist/shell-veto-policy.js +103 -0
- package/dist/shell-veto-policy.js.map +1 -0
- package/dist/skills.d.ts +86 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +116 -1
- package/dist/skills.js.map +1 -1
- package/dist/targets.d.ts +75 -0
- package/dist/targets.d.ts.map +1 -1
- package/dist/targets.js +160 -0
- package/dist/targets.js.map +1 -1
- package/package.json +20 -19
- package/sbom.json +552 -102
- package/src/agents-policy.ts +338 -0
- package/src/codex-hooks-assets.ts +291 -0
- package/src/codex-hooks-verify.ts +184 -0
- package/src/codex-hooks.ts +571 -0
- package/src/discrimination-gate.ts +456 -58
- package/src/feature-adr-checkpoints.ts +38 -0
- package/src/feature-adr-routing.ts +642 -75
- package/src/guard.ts +36 -1
- package/src/index.ts +118 -2
- package/src/loop-blobs.generated.ts +2 -2
- package/src/managed-hooks.ts +129 -0
- package/src/mutation-gate.ts +24 -2
- package/src/operations.ts +719 -28
- package/src/parity.ts +120 -6
- package/src/recall-usage.ts +184 -1
- package/src/setup.ts +26 -27
- package/src/shell-veto-policy.ts +119 -0
- package/src/skills.ts +174 -1
- 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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"}
|