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.
@@ -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
+ };