polygram 0.38.7 → 0.38.9
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/.claude-plugin/plugin.json +1 -1
- package/config.example.json +1 -0
- package/docs/2026-08-16-003-codex-protected-hook-artifact-runbook.md +323 -0
- package/lib/codex/hook-artifacts.js +1033 -0
- package/lib/codex/hook-bundle.js +296 -0
- package/lib/codex/hook-command.js +192 -0
- package/lib/codex/hook-runtime.js +533 -0
- package/lib/codex/hook-source-policy.js +440 -0
- package/lib/codex/hooks/hook-observer.bundle.js +185 -0
- package/lib/codex/hooks/observer-capture.js +61 -0
- package/lib/codex/hooks/observer-entry.js +74 -0
- package/lib/codex/runtime-controller.js +52 -2
- package/lib/codex/runtime-profile.js +215 -31
- package/package.json +8 -2
- package/scripts/build-codex-hook-bundle.js +74 -0
- package/scripts/codex-hook-artifacts.js +136 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createHash, randomUUID } = require('node:crypto');
|
|
4
|
+
const {
|
|
5
|
+
closeSync,
|
|
6
|
+
constants,
|
|
7
|
+
existsSync,
|
|
8
|
+
fsyncSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
openSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
renameSync,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} = require('node:fs');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
attestHookArtifactVersion,
|
|
20
|
+
} = require('./hook-artifacts');
|
|
21
|
+
const { renderHookCommand } = require('./hook-command');
|
|
22
|
+
|
|
23
|
+
const HOOK_EVENTS = Object.freeze([
|
|
24
|
+
Object.freeze({ configName: 'SessionStart', event: 'sessionStart' }),
|
|
25
|
+
Object.freeze({ configName: 'UserPromptSubmit', event: 'userPromptSubmit' }),
|
|
26
|
+
Object.freeze({ configName: 'Stop', event: 'stop' }),
|
|
27
|
+
]);
|
|
28
|
+
const CAPTURE_DIRECTORY = '.polygram-codex-hook-observations';
|
|
29
|
+
const LOCK_NAME = '.polygram-hooks.lock';
|
|
30
|
+
const SHA256_RE = /^[a-f0-9]{64}$/;
|
|
31
|
+
const CURRENT_HASH_RE = /^sha256:[a-f0-9]{64}$/;
|
|
32
|
+
|
|
33
|
+
class CodexHookRuntimeError extends Error {
|
|
34
|
+
constructor(message, code, action, options) {
|
|
35
|
+
super(message, options);
|
|
36
|
+
this.name = 'CodexHookRuntimeError';
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.action = action;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fail(message, code, action, cause) {
|
|
43
|
+
return new CodexHookRuntimeError(
|
|
44
|
+
message,
|
|
45
|
+
code,
|
|
46
|
+
action,
|
|
47
|
+
cause ? { cause } : undefined,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function digest(value) {
|
|
52
|
+
return createHash('sha256').update(value).digest('hex');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function tomlKey(value) {
|
|
56
|
+
return JSON.stringify(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hookToken(event) {
|
|
60
|
+
return event.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeHookOptions(value) {
|
|
64
|
+
if (value == null) return null;
|
|
65
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
66
|
+
throw fail(
|
|
67
|
+
'Codex hook deployment configuration must be an object.',
|
|
68
|
+
'CODEX_HOOK_CONFIG_INVALID',
|
|
69
|
+
'Configure the complete protected hook deployment or omit it.',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const keys = [
|
|
73
|
+
'artifactRoot',
|
|
74
|
+
'enabled',
|
|
75
|
+
'operatorUid',
|
|
76
|
+
'runtimeId',
|
|
77
|
+
'runtimeRoot',
|
|
78
|
+
'runtimeSha256',
|
|
79
|
+
'serviceUid',
|
|
80
|
+
'version',
|
|
81
|
+
];
|
|
82
|
+
if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(keys)) {
|
|
83
|
+
throw fail(
|
|
84
|
+
'Codex hook deployment configuration has an unsupported shape.',
|
|
85
|
+
'CODEX_HOOK_CONFIG_INVALID',
|
|
86
|
+
'Configure exactly the documented protected hook deployment fields.',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
for (const key of ['artifactRoot', 'runtimeRoot']) {
|
|
90
|
+
if (typeof value[key] !== 'string' || !path.isAbsolute(value[key])) {
|
|
91
|
+
throw fail(
|
|
92
|
+
`Codex hook ${key} must be absolute.`,
|
|
93
|
+
'CODEX_HOOK_CONFIG_INVALID',
|
|
94
|
+
'Use canonical absolute protected-tree paths.',
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const key of ['runtimeId', 'version']) {
|
|
99
|
+
if (
|
|
100
|
+
typeof value[key] !== 'string'
|
|
101
|
+
|| value[key].length === 0
|
|
102
|
+
|| value[key].length > 128
|
|
103
|
+
|| /[\u0000-\u001f\u007f/\\]/u.test(value[key])
|
|
104
|
+
) {
|
|
105
|
+
throw fail(
|
|
106
|
+
`Codex hook ${key} is invalid.`,
|
|
107
|
+
'CODEX_HOOK_CONFIG_INVALID',
|
|
108
|
+
'Use a bounded immutable runtime and artifact identity.',
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (
|
|
113
|
+
typeof value.enabled !== 'boolean'
|
|
114
|
+
|| !Number.isSafeInteger(value.operatorUid)
|
|
115
|
+
|| value.operatorUid < 0
|
|
116
|
+
|| !Number.isSafeInteger(value.serviceUid)
|
|
117
|
+
|| value.serviceUid < 0
|
|
118
|
+
|| !SHA256_RE.test(value.runtimeSha256)
|
|
119
|
+
) {
|
|
120
|
+
throw fail(
|
|
121
|
+
'Codex hook deployment identity or runtime digest is invalid.',
|
|
122
|
+
'CODEX_HOOK_CONFIG_INVALID',
|
|
123
|
+
'Use the measured runtime receipt and numeric host account identities.',
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return Object.freeze({ ...value });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function renderHooksJson(commands) {
|
|
130
|
+
return `${JSON.stringify({
|
|
131
|
+
hooks: Object.fromEntries(HOOK_EVENTS.map(({ configName, event }) => [
|
|
132
|
+
configName,
|
|
133
|
+
[{ hooks: [{ type: 'command', command: commands[event].command }] }],
|
|
134
|
+
])),
|
|
135
|
+
}, null, 2)}\n`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function createHookMaterial({ codexHome, workspace, hookOptions }) {
|
|
139
|
+
const options = normalizeHookOptions(hookOptions);
|
|
140
|
+
if (options == null) return null;
|
|
141
|
+
const receipt = attestHookArtifactVersion({
|
|
142
|
+
artifactRoot: options.artifactRoot,
|
|
143
|
+
version: options.version,
|
|
144
|
+
runtimeRoot: options.runtimeRoot,
|
|
145
|
+
runtimeId: options.runtimeId,
|
|
146
|
+
runtimeSha256: options.runtimeSha256,
|
|
147
|
+
operatorUid: options.operatorUid,
|
|
148
|
+
serviceUid: options.serviceUid,
|
|
149
|
+
});
|
|
150
|
+
const bundle = receipt.artifacts.find((entry) => (
|
|
151
|
+
entry.kind === 'protected-artifact'
|
|
152
|
+
&& path.basename(entry.path) === 'hook-observer.js'
|
|
153
|
+
));
|
|
154
|
+
if (!bundle) {
|
|
155
|
+
throw fail(
|
|
156
|
+
'The protected hook receipt does not contain the observer bundle.',
|
|
157
|
+
'CODEX_HOOK_ARTIFACT_MISMATCH',
|
|
158
|
+
'Reinstall the exact Polygram hook artifact version.',
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
const commands = {};
|
|
162
|
+
for (const { configName, event } of HOOK_EVENTS) {
|
|
163
|
+
commands[event] = renderHookCommand({
|
|
164
|
+
runtime: { path: receipt.runtime.path, kind: 'protected-runtime' },
|
|
165
|
+
artifacts: [{ path: bundle.path, kind: 'protected-artifact' }],
|
|
166
|
+
argv: [
|
|
167
|
+
{ kind: 'literal', value: configName },
|
|
168
|
+
{ kind: 'literal', value: CAPTURE_DIRECTORY },
|
|
169
|
+
],
|
|
170
|
+
}, receipt);
|
|
171
|
+
}
|
|
172
|
+
const hooksPath = path.join(codexHome, 'hooks.json');
|
|
173
|
+
const entries = HOOK_EVENTS.map(({ event }, ordinal) => Object.freeze({
|
|
174
|
+
ordinal,
|
|
175
|
+
configKey: `${hooksPath}:${hookToken(event)}:0:0`,
|
|
176
|
+
sourcePath: hooksPath,
|
|
177
|
+
event,
|
|
178
|
+
handlerType: 'command',
|
|
179
|
+
source: 'user',
|
|
180
|
+
isManaged: false,
|
|
181
|
+
displayOrder: ordinal,
|
|
182
|
+
timeoutSec: 600,
|
|
183
|
+
commandSha256: commands[event].sha256,
|
|
184
|
+
}));
|
|
185
|
+
const hooksJson = renderHooksJson(commands);
|
|
186
|
+
const hookManifest = Object.freeze({
|
|
187
|
+
ownedCwd: workspace,
|
|
188
|
+
entries: Object.freeze(entries),
|
|
189
|
+
});
|
|
190
|
+
return Object.freeze({
|
|
191
|
+
hookManifest,
|
|
192
|
+
hooksJson,
|
|
193
|
+
hooksPath,
|
|
194
|
+
options,
|
|
195
|
+
receipt,
|
|
196
|
+
hookArtifactsSha256: digest(JSON.stringify({
|
|
197
|
+
closureSha256: receipt.closureSha256,
|
|
198
|
+
hooksSha256: digest(hooksJson),
|
|
199
|
+
})),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function withTrustState(baseOwnedConfig, manifest, projected) {
|
|
204
|
+
if (
|
|
205
|
+
!Array.isArray(projected)
|
|
206
|
+
|| projected.length !== manifest.entries.length
|
|
207
|
+
|| projected.some((entry, index) => (
|
|
208
|
+
entry?.ordinal !== index
|
|
209
|
+
|| entry.trustStatus !== 'untrusted'
|
|
210
|
+
|| entry.enabled !== true
|
|
211
|
+
|| !CURRENT_HASH_RE.test(entry.currentHash ?? '')
|
|
212
|
+
))
|
|
213
|
+
) {
|
|
214
|
+
throw fail(
|
|
215
|
+
'Codex hook discovery did not return the exact untrusted manifest.',
|
|
216
|
+
'CODEX_HOOK_TRUST_UNVERIFIED',
|
|
217
|
+
'Restore the exact hooks file and reprovision the owned Codex home.',
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
...baseOwnedConfig,
|
|
222
|
+
hooks: {
|
|
223
|
+
state: Object.fromEntries(manifest.entries.map((entry, index) => [
|
|
224
|
+
entry.configKey,
|
|
225
|
+
{ enabled: true, trusted_hash: projected[index].currentHash },
|
|
226
|
+
])),
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function renderTrustTables(ownedConfig) {
|
|
232
|
+
const state = ownedConfig.hooks?.state;
|
|
233
|
+
if (!state) return '';
|
|
234
|
+
const lines = [];
|
|
235
|
+
for (const key of Object.keys(state).sort()) {
|
|
236
|
+
const entry = state[key];
|
|
237
|
+
if (entry?.enabled !== true || !CURRENT_HASH_RE.test(entry.trusted_hash ?? '')) {
|
|
238
|
+
throw fail(
|
|
239
|
+
'Codex hook trust state is malformed.',
|
|
240
|
+
'CODEX_HOOK_TRUST_UNVERIFIED',
|
|
241
|
+
'Reprovision the exact owned Codex configuration.',
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
lines.push(
|
|
245
|
+
`[hooks.state.${tomlKey(key)}]`,
|
|
246
|
+
'enabled = true',
|
|
247
|
+
`trusted_hash = ${JSON.stringify(entry.trusted_hash)}`,
|
|
248
|
+
'',
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return lines.join('\n');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function extractTrustedState(configText, baseConfigText, manifest) {
|
|
255
|
+
if (!configText.startsWith(baseConfigText)) return null;
|
|
256
|
+
const suffix = configText.slice(baseConfigText.length);
|
|
257
|
+
const hashes = [];
|
|
258
|
+
for (const entry of manifest.entries) {
|
|
259
|
+
const prefix = [
|
|
260
|
+
`[hooks.state.${tomlKey(entry.configKey)}]`,
|
|
261
|
+
'enabled = true',
|
|
262
|
+
'trusted_hash = "',
|
|
263
|
+
].join('\n');
|
|
264
|
+
const start = suffix.indexOf(prefix);
|
|
265
|
+
if (start < 0) return null;
|
|
266
|
+
const valueStart = start + prefix.length;
|
|
267
|
+
const value = suffix.slice(valueStart, valueStart + 71);
|
|
268
|
+
if (!CURRENT_HASH_RE.test(value) || suffix[valueStart + 71] !== '"') {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
hashes.push(value);
|
|
272
|
+
}
|
|
273
|
+
const projected = hashes.map((currentHash, ordinal) => ({
|
|
274
|
+
ordinal,
|
|
275
|
+
currentHash,
|
|
276
|
+
trustStatus: 'untrusted',
|
|
277
|
+
enabled: true,
|
|
278
|
+
}));
|
|
279
|
+
return projected;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function writeAtomic(target, contents) {
|
|
283
|
+
const directory = path.dirname(target);
|
|
284
|
+
const temporary = path.join(
|
|
285
|
+
directory,
|
|
286
|
+
`.polygram-${path.basename(target)}-${process.pid}-${randomUUID()}.tmp`,
|
|
287
|
+
);
|
|
288
|
+
let fd = null;
|
|
289
|
+
try {
|
|
290
|
+
fd = openSync(
|
|
291
|
+
temporary,
|
|
292
|
+
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
|
|
293
|
+
0o600,
|
|
294
|
+
);
|
|
295
|
+
writeFileSync(fd, contents);
|
|
296
|
+
fsyncSync(fd);
|
|
297
|
+
closeSync(fd);
|
|
298
|
+
fd = null;
|
|
299
|
+
renameSync(temporary, target);
|
|
300
|
+
const dirFd = openSync(directory, constants.O_RDONLY);
|
|
301
|
+
try {
|
|
302
|
+
fsyncSync(dirFd);
|
|
303
|
+
} finally {
|
|
304
|
+
closeSync(dirFd);
|
|
305
|
+
}
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (fd !== null) closeSync(fd);
|
|
308
|
+
try { unlinkSync(temporary); } catch {}
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function removeAtomic(target) {
|
|
314
|
+
if (!existsSync(target)) return;
|
|
315
|
+
unlinkSync(target);
|
|
316
|
+
const dirFd = openSync(path.dirname(target), constants.O_RDONLY);
|
|
317
|
+
try {
|
|
318
|
+
fsyncSync(dirFd);
|
|
319
|
+
} finally {
|
|
320
|
+
closeSync(dirFd);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function acquireHomeLock(codexHome) {
|
|
325
|
+
const lockPath = path.join(codexHome, LOCK_NAME);
|
|
326
|
+
const token = `${process.pid}:${randomUUID()}`;
|
|
327
|
+
let fd;
|
|
328
|
+
try {
|
|
329
|
+
fd = openSync(
|
|
330
|
+
lockPath,
|
|
331
|
+
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
|
|
332
|
+
0o600,
|
|
333
|
+
);
|
|
334
|
+
writeFileSync(fd, `${token}\n`);
|
|
335
|
+
fsyncSync(fd);
|
|
336
|
+
closeSync(fd);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (fd !== undefined) try { closeSync(fd); } catch {}
|
|
339
|
+
throw fail(
|
|
340
|
+
'Codex hook provisioning is already locked.',
|
|
341
|
+
'CODEX_HOOK_PROVISION_LOCKED',
|
|
342
|
+
`Confirm no Polygram process is provisioning this home, then remove ${LOCK_NAME}.`,
|
|
343
|
+
error,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return () => {
|
|
347
|
+
let observed;
|
|
348
|
+
try {
|
|
349
|
+
observed = readFileSync(lockPath, 'utf8');
|
|
350
|
+
} catch (error) {
|
|
351
|
+
throw fail(
|
|
352
|
+
'Codex hook provisioning lock disappeared.',
|
|
353
|
+
'CODEX_HOOK_PROVISION_LOCKED',
|
|
354
|
+
'Keep Codex admission closed and inspect the owned home.',
|
|
355
|
+
error,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (observed !== `${token}\n`) {
|
|
359
|
+
throw fail(
|
|
360
|
+
'Codex hook provisioning lock ownership changed.',
|
|
361
|
+
'CODEX_HOOK_PROVISION_LOCKED',
|
|
362
|
+
'Keep Codex admission closed and inspect the owned home.',
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
removeAtomic(lockPath);
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function assertHooksFile(material) {
|
|
370
|
+
let stat;
|
|
371
|
+
try {
|
|
372
|
+
stat = lstatSync(material.hooksPath);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
throw fail(
|
|
375
|
+
'Codex hooks.json is missing.',
|
|
376
|
+
'CODEX_OWNED_HOOKS_DRIFT',
|
|
377
|
+
'Reprovision the exact owned hook manifest.',
|
|
378
|
+
error,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const uid = process.getuid?.();
|
|
382
|
+
if (
|
|
383
|
+
!stat.isFile()
|
|
384
|
+
|| stat.isSymbolicLink()
|
|
385
|
+
|| stat.nlink !== 1
|
|
386
|
+
|| (uid !== undefined && stat.uid !== uid)
|
|
387
|
+
|| (stat.mode & 0o777) !== 0o600
|
|
388
|
+
|| readFileSync(material.hooksPath, 'utf8') !== material.hooksJson
|
|
389
|
+
) {
|
|
390
|
+
throw fail(
|
|
391
|
+
'Codex hooks.json differs from the owned manifest.',
|
|
392
|
+
'CODEX_OWNED_HOOKS_DRIFT',
|
|
393
|
+
'Reprovision the exact owner-only hook manifest.',
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function prepareHookRuntime({
|
|
399
|
+
hookOptions,
|
|
400
|
+
codexHome,
|
|
401
|
+
workspace,
|
|
402
|
+
baseOwnedConfig,
|
|
403
|
+
baseRawConfig,
|
|
404
|
+
renderOwnedConfig,
|
|
405
|
+
verifyDiscovery,
|
|
406
|
+
}) {
|
|
407
|
+
const options = normalizeHookOptions(hookOptions);
|
|
408
|
+
const configPath = path.join(codexHome, 'config.toml');
|
|
409
|
+
if (options == null) {
|
|
410
|
+
return {
|
|
411
|
+
ownedConfig: baseOwnedConfig,
|
|
412
|
+
rawConfig: baseRawConfig,
|
|
413
|
+
hookManifest: null,
|
|
414
|
+
hookArtifactsSha256: null,
|
|
415
|
+
hookMaterial: null,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
const material = createHookMaterial({ codexHome, workspace, hookOptions: options });
|
|
419
|
+
const release = acquireHomeLock(codexHome);
|
|
420
|
+
try {
|
|
421
|
+
const configText = existsSync(configPath)
|
|
422
|
+
? readFileSync(configPath, 'utf8')
|
|
423
|
+
: null;
|
|
424
|
+
const hooksText = existsSync(material.hooksPath)
|
|
425
|
+
? readFileSync(material.hooksPath, 'utf8')
|
|
426
|
+
: null;
|
|
427
|
+
const extracted = configText == null
|
|
428
|
+
? null
|
|
429
|
+
: extractTrustedState(configText, baseRawConfig, material.hookManifest);
|
|
430
|
+
const trustedConfig = extracted == null
|
|
431
|
+
? null
|
|
432
|
+
: withTrustState(baseOwnedConfig, material.hookManifest, extracted);
|
|
433
|
+
const trustedRaw = trustedConfig == null
|
|
434
|
+
? null
|
|
435
|
+
: renderOwnedConfig(trustedConfig);
|
|
436
|
+
const state = configText == null && hooksText == null
|
|
437
|
+
? 'absent'
|
|
438
|
+
: configText === baseRawConfig && hooksText == null
|
|
439
|
+
? 'hooks-off'
|
|
440
|
+
: configText === baseRawConfig && hooksText === material.hooksJson
|
|
441
|
+
? 'discovery'
|
|
442
|
+
: trustedRaw === configText && hooksText === material.hooksJson
|
|
443
|
+
? 'trusted'
|
|
444
|
+
: 'drift';
|
|
445
|
+
if (state === 'drift') {
|
|
446
|
+
throw fail(
|
|
447
|
+
'Codex hook files are outside the owned state machine.',
|
|
448
|
+
'CODEX_OWNED_HOOKS_DRIFT',
|
|
449
|
+
'Back up the owned home, remove config.toml and hooks.json, then reprovision.',
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
if (!options.enabled) {
|
|
453
|
+
if (state === 'trusted') writeAtomic(configPath, baseRawConfig);
|
|
454
|
+
if (state === 'trusted' || state === 'discovery') {
|
|
455
|
+
removeAtomic(material.hooksPath);
|
|
456
|
+
}
|
|
457
|
+
if (state === 'absent') writeAtomic(configPath, baseRawConfig);
|
|
458
|
+
return {
|
|
459
|
+
ownedConfig: baseOwnedConfig,
|
|
460
|
+
rawConfig: baseRawConfig,
|
|
461
|
+
hookManifest: null,
|
|
462
|
+
hookArtifactsSha256: null,
|
|
463
|
+
hookMaterial: null,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (state === 'absent') writeAtomic(configPath, baseRawConfig);
|
|
468
|
+
if (state === 'absent' || state === 'hooks-off') {
|
|
469
|
+
writeAtomic(material.hooksPath, material.hooksJson);
|
|
470
|
+
}
|
|
471
|
+
if (state !== 'trusted') {
|
|
472
|
+
assertHooksFile(material);
|
|
473
|
+
const projected = await verifyDiscovery(
|
|
474
|
+
material.hookManifest,
|
|
475
|
+
digest(baseRawConfig),
|
|
476
|
+
);
|
|
477
|
+
const nextOwnedConfig = withTrustState(
|
|
478
|
+
baseOwnedConfig,
|
|
479
|
+
material.hookManifest,
|
|
480
|
+
projected,
|
|
481
|
+
);
|
|
482
|
+
const nextRawConfig = renderOwnedConfig(nextOwnedConfig);
|
|
483
|
+
writeAtomic(configPath, nextRawConfig);
|
|
484
|
+
assertHooksFile(material);
|
|
485
|
+
return {
|
|
486
|
+
ownedConfig: nextOwnedConfig,
|
|
487
|
+
rawConfig: nextRawConfig,
|
|
488
|
+
hookManifest: material.hookManifest,
|
|
489
|
+
hookArtifactsSha256: material.hookArtifactsSha256,
|
|
490
|
+
hookMaterial: material,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
assertHooksFile(material);
|
|
494
|
+
return {
|
|
495
|
+
ownedConfig: trustedConfig,
|
|
496
|
+
rawConfig: trustedRaw,
|
|
497
|
+
hookManifest: material.hookManifest,
|
|
498
|
+
hookArtifactsSha256: material.hookArtifactsSha256,
|
|
499
|
+
hookMaterial: material,
|
|
500
|
+
};
|
|
501
|
+
} finally {
|
|
502
|
+
release();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function reattestHookRuntime({ hookOptions, codexHome, workspace, expected }) {
|
|
507
|
+
const material = createHookMaterial({ codexHome, workspace, hookOptions });
|
|
508
|
+
assertHooksFile(material);
|
|
509
|
+
if (
|
|
510
|
+
material.hookArtifactsSha256 !== expected.hookArtifactsSha256
|
|
511
|
+
|| JSON.stringify(material.hookManifest) !== JSON.stringify(expected.hookManifest)
|
|
512
|
+
) {
|
|
513
|
+
throw fail(
|
|
514
|
+
'Codex hook material changed after preflight.',
|
|
515
|
+
'CODEX_OWNED_HOOKS_DRIFT',
|
|
516
|
+
'Close admission and reprovision the exact hook deployment.',
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
return material;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
module.exports = {
|
|
523
|
+
CAPTURE_DIRECTORY,
|
|
524
|
+
CodexHookRuntimeError,
|
|
525
|
+
HOOK_EVENTS,
|
|
526
|
+
createHookMaterial,
|
|
527
|
+
normalizeHookOptions,
|
|
528
|
+
prepareHookRuntime,
|
|
529
|
+
reattestHookRuntime,
|
|
530
|
+
renderHooksJson,
|
|
531
|
+
renderTrustTables,
|
|
532
|
+
withTrustState,
|
|
533
|
+
};
|