polygram 0.22.0 → 0.23.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 (47) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +5 -3
  3. package/config.example.json +23 -1
  4. package/lib/codex/binary.js +287 -0
  5. package/lib/codex/delivery-finalizer.js +97 -0
  6. package/lib/codex/diagnostics.js +483 -0
  7. package/lib/codex/event-normalizer.js +50 -0
  8. package/lib/codex/host-identity.js +103 -0
  9. package/lib/codex/model-catalog.js +359 -0
  10. package/lib/codex/retention-scheduler.js +59 -0
  11. package/lib/codex/runtime-controller.js +1292 -0
  12. package/lib/codex/runtime-profile.js +888 -0
  13. package/lib/codex/spawn-context.js +87 -0
  14. package/lib/db/auto-resume.js +77 -9
  15. package/lib/db/codex-reconciliation.js +490 -0
  16. package/lib/db/codex-retention.js +139 -0
  17. package/lib/db/sessions.js +102 -1
  18. package/lib/db.js +3418 -19
  19. package/lib/error/classify.js +140 -0
  20. package/lib/handlers/abort.js +143 -5
  21. package/lib/handlers/autosteer.js +256 -1
  22. package/lib/handlers/codex-reconciliation.js +230 -0
  23. package/lib/handlers/config-callback.js +324 -89
  24. package/lib/handlers/config-ui.js +401 -39
  25. package/lib/handlers/dispatcher.js +71 -8
  26. package/lib/handlers/edit-correction.js +11 -1
  27. package/lib/handlers/edit-redelivery.js +230 -4
  28. package/lib/handlers/gate-inbound.js +7 -0
  29. package/lib/handlers/replay-disposition.js +133 -11
  30. package/lib/handlers/slash-commands.js +443 -37
  31. package/lib/ipc/client.js +22 -12
  32. package/lib/ipc/runtime-directory.js +208 -0
  33. package/lib/ipc/server.js +18 -12
  34. package/lib/model-costs.js +4 -3
  35. package/lib/pm-interface.js +33 -0
  36. package/lib/prompt.js +15 -9
  37. package/lib/rewind/rewind.js +21 -1
  38. package/lib/runtime-config.js +605 -0
  39. package/lib/sdk/build-options.js +4 -1
  40. package/lib/sdk/callbacks.js +46 -3
  41. package/migrations/015-agent-runtime-sessions.sql +50 -0
  42. package/migrations/016-agent-turn-attempts.sql +211 -0
  43. package/migrations/017-config-runtime-changes.sql +46 -0
  44. package/ops/README.md +57 -0
  45. package/package.json +2 -2
  46. package/polygram.js +697 -48
  47. package/scripts/doctor.js +37 -8
@@ -0,0 +1,483 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ lstatSync,
5
+ readFileSync,
6
+ realpathSync,
7
+ } = require('node:fs');
8
+ const os = require('node:os');
9
+ const path = require('node:path');
10
+
11
+ const orchestra = require('@shumkov/orchestra');
12
+ const {
13
+ CODEX_BINARY_SHA256,
14
+ CODEX_CLI_PINNED_VERSION,
15
+ resolvePinnedCodexBinary,
16
+ } = require('./binary');
17
+ const { resolveCodexHostIdentity } = require('./host-identity');
18
+ const {
19
+ CODEX_PERMISSION_PROFILE_ID,
20
+ } = require('./runtime-profile');
21
+ const {
22
+ runtimeDirectory: resolveIpcRuntimeDirectory,
23
+ } = require('../ipc/runtime-directory');
24
+
25
+ const MAX_DETAIL_BYTES = 256;
26
+ const MAX_CONFIG_BYTES = 64 * 1024;
27
+ const SHA256_RE = /^[a-f0-9]{64}$/;
28
+ const HOST_ID_RE = /^host:[a-f0-9]{64}$/;
29
+ const BOOT_ID_RE = /^boot:[a-f0-9]{64}$/;
30
+ const REASON_CODE_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
31
+
32
+ function check(name, status, detail, extra) {
33
+ if (
34
+ typeof detail !== 'string'
35
+ || Buffer.byteLength(detail, 'utf8') > MAX_DETAIL_BYTES
36
+ || /[\u0000-\u001f\u007f]/u.test(detail)
37
+ ) {
38
+ throw new TypeError('Codex doctor detail must be a bounded single line');
39
+ }
40
+ return Object.freeze({
41
+ name,
42
+ status,
43
+ detail,
44
+ ...(extra === undefined ? {} : { extra: Object.freeze({ ...extra }) }),
45
+ });
46
+ }
47
+
48
+ function safeErrorCode(error) {
49
+ return typeof error?.code === 'string'
50
+ && /^(?:CODEX|IPC)_[A-Z0-9_]{1,96}$/.test(error.code)
51
+ ? error.code
52
+ : null;
53
+ }
54
+
55
+ function isWithin(root, candidate) {
56
+ const relative = path.relative(root, candidate);
57
+ return relative === ''
58
+ || (!relative.startsWith('..') && !path.isAbsolute(relative));
59
+ }
60
+
61
+ function defaultTemporaryRoots() {
62
+ const roots = new Set();
63
+ for (const candidate of [
64
+ os.tmpdir(),
65
+ '/tmp',
66
+ '/private/tmp',
67
+ '/var/tmp',
68
+ '/private/var/folders',
69
+ ]) {
70
+ try {
71
+ roots.add(realpathSync(candidate));
72
+ } catch {}
73
+ }
74
+ return [...roots];
75
+ }
76
+
77
+ function assertOwnedPath(target, {
78
+ directory,
79
+ mode,
80
+ temporaryRoots = [],
81
+ genericCodexHome = null,
82
+ }) {
83
+ if (
84
+ typeof target !== 'string'
85
+ || !path.isAbsolute(target)
86
+ || realpathSync(target) !== target
87
+ || temporaryRoots.some((root) => isWithin(root, target))
88
+ || (genericCodexHome && target === genericCodexHome)
89
+ ) {
90
+ throw new Error('unsafe Codex deployment path');
91
+ }
92
+ const stat = lstatSync(target);
93
+ if (
94
+ stat.isSymbolicLink()
95
+ || (directory ? !stat.isDirectory() : !stat.isFile())
96
+ || (stat.mode & 0o777) !== mode
97
+ || (
98
+ typeof process.getuid === 'function'
99
+ && stat.uid !== process.getuid()
100
+ )
101
+ || (!directory && stat.nlink !== 1)
102
+ ) {
103
+ throw new Error('unsafe Codex deployment ownership or mode');
104
+ }
105
+ }
106
+
107
+ function inspectCodexHome(codexHome, dependencies) {
108
+ assertOwnedPath(codexHome, {
109
+ directory: true,
110
+ mode: 0o700,
111
+ temporaryRoots: dependencies.temporaryRoots,
112
+ genericCodexHome: dependencies.genericCodexHome,
113
+ });
114
+ for (const file of ['config.toml', 'auth.json']) {
115
+ assertOwnedPath(path.join(codexHome, file), {
116
+ directory: false,
117
+ mode: 0o600,
118
+ });
119
+ }
120
+ }
121
+
122
+ function inspectCodexProfile(codexHome, profileId) {
123
+ const configPath = path.join(codexHome, 'config.toml');
124
+ const stat = lstatSync(configPath);
125
+ if (stat.size > MAX_CONFIG_BYTES) {
126
+ throw new Error('Codex owned config exceeded the inspection bound');
127
+ }
128
+ const raw = readFileSync(configPath, 'utf8');
129
+ const escapedProfile = profileId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
130
+ const required = [
131
+ new RegExp(`^default_permissions\\s*=\\s*"${escapedProfile}"\\s*$`, 'm'),
132
+ /^approval_policy\s*=\s*"never"\s*$/m,
133
+ /^web_search\s*=\s*"disabled"\s*$/m,
134
+ new RegExp(
135
+ `^\\[permissions\\."${escapedProfile}"\\.network\\]\\s*\\n`
136
+ + 'enabled\\s*=\\s*false\\s*$',
137
+ 'm',
138
+ ),
139
+ ];
140
+ if (!required.every((pattern) => pattern.test(raw))) {
141
+ throw new Error('Codex owned profile is unavailable');
142
+ }
143
+ }
144
+
145
+ function readLease(db) {
146
+ if (!db || typeof db.prepare !== 'function') return undefined;
147
+ return db.prepare(`
148
+ SELECT generation_id, status, quarantine_reason
149
+ FROM codex_daemon_lease
150
+ WHERE singleton = 1
151
+ `).get();
152
+ }
153
+
154
+ function leaseCheck(db) {
155
+ if (!db) {
156
+ return check(
157
+ 'codex-lease',
158
+ 'warn',
159
+ 'not checked because the database is unavailable',
160
+ { runtime: 'codex', scope: 'daemon', status: 'unknown' },
161
+ );
162
+ }
163
+ let lease;
164
+ try {
165
+ lease = readLease(db);
166
+ } catch (error) {
167
+ return check(
168
+ 'codex-lease',
169
+ 'warn',
170
+ 'lease table is unavailable; apply the Codex schema before enablement',
171
+ {
172
+ runtime: 'codex',
173
+ scope: 'daemon',
174
+ status: 'unknown',
175
+ code: safeErrorCode(error),
176
+ },
177
+ );
178
+ }
179
+ if (!lease || lease.status === 'clear') {
180
+ return check(
181
+ 'codex-lease',
182
+ 'ok',
183
+ 'daemon-wide native Codex lease is clear',
184
+ {
185
+ runtime: 'codex',
186
+ scope: 'daemon',
187
+ status: 'clear',
188
+ ownerPresent: false,
189
+ rebootRequired: false,
190
+ },
191
+ );
192
+ }
193
+ if (lease.status === 'active') {
194
+ return check(
195
+ 'codex-lease',
196
+ 'warn',
197
+ 'one daemon-wide native Codex generation is active',
198
+ {
199
+ runtime: 'codex',
200
+ scope: 'daemon',
201
+ status: 'active',
202
+ ownerPresent: typeof lease.generation_id === 'string',
203
+ rebootRequired: false,
204
+ },
205
+ );
206
+ }
207
+ if (lease.status === 'quarantined') {
208
+ const reasonCode = typeof lease.quarantine_reason === 'string'
209
+ && lease.quarantine_reason.length <= 96
210
+ && REASON_CODE_RE.test(lease.quarantine_reason)
211
+ ? lease.quarantine_reason
212
+ : null;
213
+ return check(
214
+ 'codex-lease',
215
+ 'fail',
216
+ 'daemon-wide Codex quarantine is active; same-host reboot required before release',
217
+ {
218
+ runtime: 'codex',
219
+ scope: 'daemon',
220
+ status: 'quarantined',
221
+ ownerPresent: typeof lease.generation_id === 'string',
222
+ rebootRequired: true,
223
+ reasonCode,
224
+ },
225
+ );
226
+ }
227
+ return check(
228
+ 'codex-lease',
229
+ 'fail',
230
+ 'daemon-wide Codex lease has an invalid state',
231
+ {
232
+ runtime: 'codex',
233
+ scope: 'daemon',
234
+ status: 'invalid',
235
+ rebootRequired: true,
236
+ },
237
+ );
238
+ }
239
+
240
+ function dependenciesWithDefaults(input = {}) {
241
+ let genericCodexHome = path.join(os.homedir(), '.codex');
242
+ try {
243
+ genericCodexHome = realpathSync(genericCodexHome);
244
+ } catch {}
245
+ return {
246
+ resolveBinary:
247
+ input.resolveBinary ?? resolvePinnedCodexBinary,
248
+ resolveHostIdentity:
249
+ input.resolveHostIdentity ?? resolveCodexHostIdentity,
250
+ protocolSchema:
251
+ input.protocolSchema ?? orchestra.codexProtocolSchema,
252
+ pinnedVersion:
253
+ input.pinnedVersion ?? CODEX_CLI_PINNED_VERSION,
254
+ pinnedBinarySha256:
255
+ input.pinnedBinarySha256 ?? CODEX_BINARY_SHA256,
256
+ profileId:
257
+ input.profileId ?? CODEX_PERMISSION_PROFILE_ID,
258
+ temporaryRoots:
259
+ input.temporaryRoots ?? defaultTemporaryRoots(),
260
+ ipcTemporaryRoots:
261
+ input.ipcTemporaryRoots ?? defaultTemporaryRoots(),
262
+ resolveIpcRuntimeDirectory:
263
+ input.resolveIpcRuntimeDirectory ?? resolveIpcRuntimeDirectory,
264
+ genericCodexHome:
265
+ input.genericCodexHome ?? genericCodexHome,
266
+ };
267
+ }
268
+
269
+ async function collectCodexDoctorChecks({
270
+ config,
271
+ db,
272
+ dependencies: dependencyOverrides,
273
+ } = {}) {
274
+ if (
275
+ !config?.codex
276
+ || typeof config.codex !== 'object'
277
+ || Array.isArray(config.codex)
278
+ ) {
279
+ return [check(
280
+ 'codex-runtime',
281
+ 'ok',
282
+ 'not configured',
283
+ { runtime: 'codex', configured: false },
284
+ )];
285
+ }
286
+
287
+ const dependencies = dependenciesWithDefaults(dependencyOverrides);
288
+ const checks = [];
289
+ try {
290
+ const receipt = await dependencies.resolveBinary({
291
+ binaryPath: config.codex.binary,
292
+ env: { POLYGRAM_CODEX_BIN: config.codex.binary },
293
+ });
294
+ if (
295
+ receipt?.version !== dependencies.pinnedVersion
296
+ || receipt?.sha256 !== dependencies.pinnedBinarySha256
297
+ ) {
298
+ throw new Error('pinned Codex binary receipt mismatch');
299
+ }
300
+ checks.push(check(
301
+ 'codex-binary',
302
+ 'ok',
303
+ `pinned runtime verified (${dependencies.pinnedVersion})`,
304
+ {
305
+ runtime: 'codex',
306
+ pinned: true,
307
+ version: dependencies.pinnedVersion,
308
+ },
309
+ ));
310
+ } catch (error) {
311
+ checks.push(check(
312
+ 'codex-binary',
313
+ 'fail',
314
+ 'pinned runtime verification failed',
315
+ {
316
+ runtime: 'codex',
317
+ pinned: false,
318
+ code: safeErrorCode(error),
319
+ },
320
+ ));
321
+ }
322
+
323
+ try {
324
+ inspectCodexHome(config.codex.home, dependencies);
325
+ checks.push(check(
326
+ 'codex-home',
327
+ 'ok',
328
+ 'dedicated persistent mode-0700 CODEX_HOME is provisioned',
329
+ {
330
+ runtime: 'codex',
331
+ dedicated: true,
332
+ credentialsPresent: true,
333
+ },
334
+ ));
335
+ } catch (error) {
336
+ checks.push(check(
337
+ 'codex-home',
338
+ 'fail',
339
+ 'dedicated CODEX_HOME is missing, temporary, aliased, or has unsafe permissions',
340
+ {
341
+ runtime: 'codex',
342
+ dedicated: false,
343
+ code: safeErrorCode(error),
344
+ },
345
+ ));
346
+ }
347
+
348
+ const schema = dependencies.protocolSchema;
349
+ if (
350
+ schema?.cliVersion === dependencies.pinnedVersion
351
+ && schema?.binarySha256 === dependencies.pinnedBinarySha256
352
+ && SHA256_RE.test(schema?.generatedProtocolV2CanonicalSha256)
353
+ ) {
354
+ checks.push(check(
355
+ 'codex-protocol',
356
+ 'ok',
357
+ 'installed protocol schema matches the pinned runtime',
358
+ {
359
+ runtime: 'codex',
360
+ version: dependencies.pinnedVersion,
361
+ schemaPinned: true,
362
+ },
363
+ ));
364
+ } else {
365
+ checks.push(check(
366
+ 'codex-protocol',
367
+ 'fail',
368
+ 'installed protocol schema does not match the pinned runtime',
369
+ { runtime: 'codex', schemaPinned: false },
370
+ ));
371
+ }
372
+
373
+ try {
374
+ inspectCodexProfile(config.codex.home, dependencies.profileId);
375
+ checks.push(check(
376
+ 'codex-profile',
377
+ 'ok',
378
+ 'owned no-approval, no-network permission profile is present',
379
+ {
380
+ runtime: 'codex',
381
+ profile: dependencies.profileId,
382
+ available: true,
383
+ },
384
+ ));
385
+ } catch (error) {
386
+ checks.push(check(
387
+ 'codex-profile',
388
+ 'fail',
389
+ 'owned Codex permission profile is missing or drifted',
390
+ {
391
+ runtime: 'codex',
392
+ profile: dependencies.profileId,
393
+ available: false,
394
+ code: safeErrorCode(error),
395
+ },
396
+ ));
397
+ }
398
+
399
+ try {
400
+ const ipcRuntimeDirectory = dependencies.resolveIpcRuntimeDirectory();
401
+ assertOwnedPath(ipcRuntimeDirectory, {
402
+ directory: true,
403
+ mode: 0o700,
404
+ temporaryRoots: dependencies.ipcTemporaryRoots,
405
+ });
406
+ checks.push(check(
407
+ 'codex-ipc-root',
408
+ 'ok',
409
+ 'owner-only daemon IPC runtime directory is protected from Codex',
410
+ {
411
+ runtime: 'codex',
412
+ protected: true,
413
+ },
414
+ ));
415
+ } catch (error) {
416
+ checks.push(check(
417
+ 'codex-ipc-root',
418
+ 'fail',
419
+ 'daemon IPC runtime directory is missing, temporary, aliased, or has unsafe permissions',
420
+ {
421
+ runtime: 'codex',
422
+ protected: false,
423
+ code: safeErrorCode(error),
424
+ },
425
+ ));
426
+ }
427
+
428
+ checks.push(check(
429
+ 'codex-model-catalog',
430
+ 'warn',
431
+ 'offline doctor does not start Codex; authenticated profile and model availability require preflight',
432
+ { runtime: 'codex', checked: false, requiresPreflight: true },
433
+ ));
434
+
435
+ try {
436
+ const identity = dependencies.resolveHostIdentity();
437
+ if (
438
+ !HOST_ID_RE.test(identity?.stableHostId)
439
+ || !BOOT_ID_RE.test(identity?.bootSessionId)
440
+ ) {
441
+ throw new Error('invalid Codex host identity projection');
442
+ }
443
+ checks.push(check(
444
+ 'codex-identity',
445
+ 'ok',
446
+ 'stable host and kernel boot-session identities are available',
447
+ {
448
+ runtime: 'codex',
449
+ stableHostAvailable: true,
450
+ bootSessionAvailable: true,
451
+ },
452
+ ));
453
+ } catch (error) {
454
+ checks.push(check(
455
+ 'codex-identity',
456
+ 'fail',
457
+ 'stable host or kernel boot-session identity is unavailable',
458
+ {
459
+ runtime: 'codex',
460
+ stableHostAvailable: false,
461
+ bootSessionAvailable: false,
462
+ code: safeErrorCode(error),
463
+ },
464
+ ));
465
+ }
466
+
467
+ checks.push(leaseCheck(db));
468
+ checks.push(check(
469
+ 'codex-native-background',
470
+ 'warn',
471
+ 'detached and background servers are unsupported in the native beta and may require a host reboot after hard loss',
472
+ {
473
+ runtime: 'codex',
474
+ detachedBackgroundSupported: false,
475
+ hardLossRebootRequired: true,
476
+ },
477
+ ));
478
+ return checks;
479
+ }
480
+
481
+ module.exports = {
482
+ collectCodexDoctorChecks,
483
+ };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ function isCodexProcess(entry) {
4
+ return entry?.runtime === 'codex' || entry?.backend === 'codex';
5
+ }
6
+
7
+ function isCodexResult(result) {
8
+ return (
9
+ typeof result?.generationId === 'string'
10
+ && result.generationId.length > 0
11
+ && typeof result?.providerTurnId === 'string'
12
+ && result.providerTurnId.length > 0
13
+ );
14
+ }
15
+
16
+ /**
17
+ * Orchestra already converts Codex deltas and authoritative item completions
18
+ * into cumulative snapshots. Polygram must forward each snapshot as-is: the
19
+ * Telegram streamer replaces its current body, so appending here would repeat
20
+ * prefixes on every event.
21
+ *
22
+ * Claude values deliberately bypass validation to preserve its existing
23
+ * callback contract exactly.
24
+ */
25
+ function normalizeStreamText(value, entry) {
26
+ if (!isCodexProcess(entry)) return value;
27
+ return typeof value === 'string' ? value : null;
28
+ }
29
+
30
+ /**
31
+ * CodexProcess returns its authoritative final item text in result.text. The
32
+ * generation and provider-turn IDs are the pinned public discriminator; Claude
33
+ * results continue through unchanged.
34
+ */
35
+ function normalizeFinalText(result) {
36
+ if (!isCodexResult(result)) return result?.text;
37
+ if (typeof result.text !== 'string') {
38
+ const error = new TypeError('Codex final result text must be a string');
39
+ error.code = 'CODEX_TEXT_EVENT_INVALID';
40
+ throw error;
41
+ }
42
+ return result.text;
43
+ }
44
+
45
+ module.exports = {
46
+ isCodexProcess,
47
+ isCodexResult,
48
+ normalizeStreamText,
49
+ normalizeFinalText,
50
+ };
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync: defaultExecFileSync } = require('node:child_process');
4
+ const { createHash } = require('node:crypto');
5
+
6
+ const MAC_HOST_RE = /"IOPlatformUUID"\s*=\s*"([0-9A-Fa-f-]{36})"/;
7
+ const MAC_BOOT_RE = /\{\s*sec\s*=\s*(\d+),\s*usec\s*=\s*(\d+)\s*\}/;
8
+
9
+ class CodexHostIdentityError extends Error {
10
+ constructor(message, code, options) {
11
+ super(message, options);
12
+ this.name = 'CodexHostIdentityError';
13
+ this.code = code;
14
+ }
15
+ }
16
+
17
+ function identityError(message, code, cause) {
18
+ return new CodexHostIdentityError(
19
+ message,
20
+ code,
21
+ cause ? { cause } : undefined,
22
+ );
23
+ }
24
+
25
+ function rendered(value) {
26
+ return Buffer.isBuffer(value) ? value.toString('utf8') : String(value);
27
+ }
28
+
29
+ function digest(domain, ...parts) {
30
+ const hash = createHash('sha256');
31
+ hash.update(domain);
32
+ for (const part of parts) {
33
+ hash.update('\0');
34
+ hash.update(part);
35
+ }
36
+ return hash.digest('hex');
37
+ }
38
+
39
+ function readDarwinIdentity(execFileSync) {
40
+ const hostOutput = rendered(execFileSync(
41
+ '/usr/sbin/ioreg',
42
+ ['-rd1', '-c', 'IOPlatformExpertDevice'],
43
+ { encoding: 'utf8', timeout: 5_000, maxBuffer: 256 * 1024 },
44
+ ));
45
+ const hostMatch = MAC_HOST_RE.exec(hostOutput);
46
+ if (!hostMatch) throw new Error('macOS platform UUID is unavailable');
47
+
48
+ const bootOutput = rendered(execFileSync(
49
+ '/usr/sbin/sysctl',
50
+ ['-n', 'kern.boottime'],
51
+ { encoding: 'utf8', timeout: 5_000, maxBuffer: 4 * 1024 },
52
+ ));
53
+ const bootMatch = MAC_BOOT_RE.exec(bootOutput);
54
+ if (!bootMatch) throw new Error('macOS kernel boot time is unavailable');
55
+
56
+ return {
57
+ stable: hostMatch[1].toLowerCase(),
58
+ boot: `${bootMatch[1]}.${bootMatch[2]}`,
59
+ };
60
+ }
61
+
62
+ function resolveCodexHostIdentity({
63
+ platform = process.platform,
64
+ execFileSync = defaultExecFileSync,
65
+ } = {}) {
66
+ if (platform !== 'darwin') {
67
+ throw identityError(
68
+ `Codex native macOS beta is unsupported on ${platform}`,
69
+ 'CODEX_HOST_PLATFORM_UNSUPPORTED',
70
+ );
71
+ }
72
+
73
+ let source;
74
+ try {
75
+ source = readDarwinIdentity(execFileSync);
76
+ } catch (cause) {
77
+ throw identityError(
78
+ 'Codex stable host or kernel boot identity could not be verified',
79
+ 'CODEX_HOST_IDENTITY_UNAVAILABLE',
80
+ cause,
81
+ );
82
+ }
83
+
84
+ const stableDigest = digest(
85
+ 'polygram/codex/stable-host/v1',
86
+ platform,
87
+ source.stable,
88
+ );
89
+ return Object.freeze({
90
+ stableHostId: `host:${stableDigest}`,
91
+ bootSessionId: `boot:${digest(
92
+ 'polygram/codex/boot-session/v1',
93
+ platform,
94
+ stableDigest,
95
+ source.boot,
96
+ )}`,
97
+ });
98
+ }
99
+
100
+ module.exports = {
101
+ CodexHostIdentityError,
102
+ resolveCodexHostIdentity,
103
+ };