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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -3
- package/config.example.json +23 -1
- package/lib/codex/binary.js +287 -0
- package/lib/codex/delivery-finalizer.js +97 -0
- package/lib/codex/diagnostics.js +483 -0
- package/lib/codex/event-normalizer.js +50 -0
- package/lib/codex/host-identity.js +103 -0
- package/lib/codex/model-catalog.js +359 -0
- package/lib/codex/retention-scheduler.js +59 -0
- package/lib/codex/runtime-controller.js +1292 -0
- package/lib/codex/runtime-profile.js +888 -0
- package/lib/codex/spawn-context.js +87 -0
- package/lib/db/auto-resume.js +77 -9
- package/lib/db/codex-reconciliation.js +490 -0
- package/lib/db/codex-retention.js +139 -0
- package/lib/db/sessions.js +102 -1
- package/lib/db.js +3418 -19
- package/lib/error/classify.js +140 -0
- package/lib/handlers/abort.js +143 -5
- package/lib/handlers/autosteer.js +256 -1
- package/lib/handlers/codex-reconciliation.js +230 -0
- package/lib/handlers/config-callback.js +324 -89
- package/lib/handlers/config-ui.js +401 -39
- package/lib/handlers/dispatcher.js +71 -8
- package/lib/handlers/edit-correction.js +11 -1
- package/lib/handlers/edit-redelivery.js +230 -4
- package/lib/handlers/gate-inbound.js +7 -0
- package/lib/handlers/replay-disposition.js +133 -11
- package/lib/handlers/slash-commands.js +443 -37
- package/lib/ipc/client.js +22 -12
- package/lib/ipc/runtime-directory.js +208 -0
- package/lib/ipc/server.js +18 -12
- package/lib/model-costs.js +4 -3
- package/lib/pm-interface.js +33 -0
- package/lib/prompt.js +15 -9
- package/lib/rewind/rewind.js +21 -1
- package/lib/runtime-config.js +605 -0
- package/lib/sdk/build-options.js +4 -1
- package/lib/sdk/callbacks.js +46 -3
- package/migrations/015-agent-runtime-sessions.sql +50 -0
- package/migrations/016-agent-turn-attempts.sql +211 -0
- package/migrations/017-config-runtime-changes.sql +46 -0
- package/ops/README.md +57 -0
- package/package.json +2 -2
- package/polygram.js +697 -48
- package/scripts/doctor.js +37 -8
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createHash } = require('node:crypto');
|
|
4
|
+
const {
|
|
5
|
+
chmodSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
realpathSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} = require('node:fs');
|
|
13
|
+
const os = require('node:os');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const { isDeepStrictEqual } = require('node:util');
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
CodexAppServerClient,
|
|
19
|
+
codexProtocolSchema,
|
|
20
|
+
} = require('@shumkov/orchestra');
|
|
21
|
+
const {
|
|
22
|
+
CODEX_BINARY_SHA256,
|
|
23
|
+
CODEX_CLI_PINNED_VERSION,
|
|
24
|
+
} = require('./binary');
|
|
25
|
+
|
|
26
|
+
const CODEX_PERMISSION_PROFILE_ID = 'polygram-session';
|
|
27
|
+
const CONTROLLED_PATH = '/usr/bin:/bin';
|
|
28
|
+
const MAX_PAGES = 16;
|
|
29
|
+
const MAX_PROFILES = 1_000;
|
|
30
|
+
const MAX_STRING_BYTES = 64 * 1024;
|
|
31
|
+
const SHA256_RE = /^[a-f0-9]{64}$/;
|
|
32
|
+
|
|
33
|
+
class CodexRuntimeProfileError extends Error {
|
|
34
|
+
constructor(message, code, action, options) {
|
|
35
|
+
super(message, options);
|
|
36
|
+
this.name = 'CodexRuntimeProfileError';
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.action = action;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function deepFreeze(value) {
|
|
43
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
47
|
+
return Object.freeze(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function canonical(value) {
|
|
51
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
52
|
+
if (value && typeof value === 'object') {
|
|
53
|
+
return Object.fromEntries(
|
|
54
|
+
Object.keys(value).sort().map((key) => [key, canonical(value[key])]),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function digest(value) {
|
|
61
|
+
const input = typeof value === 'string'
|
|
62
|
+
? value
|
|
63
|
+
: JSON.stringify(canonical(value));
|
|
64
|
+
return createHash('sha256').update(input).digest('hex');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fail(message, code, action, cause) {
|
|
68
|
+
return new CodexRuntimeProfileError(
|
|
69
|
+
message,
|
|
70
|
+
code,
|
|
71
|
+
action,
|
|
72
|
+
cause ? { cause } : undefined,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function requiredString(value, label) {
|
|
77
|
+
if (
|
|
78
|
+
typeof value !== 'string'
|
|
79
|
+
|| value.length === 0
|
|
80
|
+
|| Buffer.byteLength(value) > MAX_STRING_BYTES
|
|
81
|
+
|| /[\u0000-\u001f\u007f]/u.test(value)
|
|
82
|
+
) {
|
|
83
|
+
throw fail(
|
|
84
|
+
`Codex ${label} must be a bounded string without control characters.`,
|
|
85
|
+
'CODEX_RUNTIME_PROFILE_INVALID',
|
|
86
|
+
'Correct the Codex runtime configuration before enabling it.',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function requiredSha256(value, label) {
|
|
93
|
+
if (!SHA256_RE.test(value)) {
|
|
94
|
+
throw fail(
|
|
95
|
+
`Codex ${label} must be a lowercase SHA-256.`,
|
|
96
|
+
'CODEX_RUNTIME_POLICY_DRIFT',
|
|
97
|
+
'Rerun Codex runtime characterization with the pinned client.',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isWithin(root, candidate) {
|
|
104
|
+
const relative = path.relative(root, candidate);
|
|
105
|
+
return relative === ''
|
|
106
|
+
|| (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function canonicalTemporaryRoots() {
|
|
110
|
+
const roots = new Set();
|
|
111
|
+
for (const candidate of [
|
|
112
|
+
os.tmpdir(),
|
|
113
|
+
'/tmp',
|
|
114
|
+
'/private/tmp',
|
|
115
|
+
'/var/tmp',
|
|
116
|
+
'/private/var/folders',
|
|
117
|
+
]) {
|
|
118
|
+
try {
|
|
119
|
+
roots.add(realpathSync(candidate));
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
return [...roots];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function assertOwnedMode(target, {
|
|
126
|
+
code,
|
|
127
|
+
kind,
|
|
128
|
+
expectedMode,
|
|
129
|
+
directory,
|
|
130
|
+
allowRoot = false,
|
|
131
|
+
}) {
|
|
132
|
+
let stat;
|
|
133
|
+
try {
|
|
134
|
+
stat = lstatSync(target);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
throw fail(
|
|
137
|
+
`Codex ${kind} could not be inspected.`,
|
|
138
|
+
code,
|
|
139
|
+
`Provision a canonical owner-only ${kind}.`,
|
|
140
|
+
error,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const expectedType = directory ? stat.isDirectory() : stat.isFile();
|
|
144
|
+
const uid = process.getuid?.();
|
|
145
|
+
if (
|
|
146
|
+
stat.isSymbolicLink()
|
|
147
|
+
|| !expectedType
|
|
148
|
+
|| (uid !== undefined && stat.uid !== uid && !(allowRoot && stat.uid === 0))
|
|
149
|
+
|| (stat.mode & 0o777) !== expectedMode
|
|
150
|
+
|| (!directory && stat.nlink !== 1)
|
|
151
|
+
) {
|
|
152
|
+
throw fail(
|
|
153
|
+
`Codex ${kind} ownership, type, or mode is unsafe.`,
|
|
154
|
+
code,
|
|
155
|
+
`Provision a canonical owner-only ${kind} with mode ${
|
|
156
|
+
expectedMode.toString(8).padStart(4, '0')
|
|
157
|
+
}.`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return stat;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function assertSafeParentChain(target, kind) {
|
|
164
|
+
const root = path.parse(target).root;
|
|
165
|
+
let component = root;
|
|
166
|
+
for (const part of target.slice(root.length).split('/').filter(Boolean)) {
|
|
167
|
+
component = path.join(component, part);
|
|
168
|
+
const stat = lstatSync(component);
|
|
169
|
+
const uid = process.getuid?.();
|
|
170
|
+
if (
|
|
171
|
+
stat.isSymbolicLink()
|
|
172
|
+
|| (uid !== undefined && ![0, uid].includes(stat.uid))
|
|
173
|
+
|| (stat.mode & 0o022) !== 0
|
|
174
|
+
) {
|
|
175
|
+
throw fail(
|
|
176
|
+
`Codex ${kind} path ownership or mode is unsafe.`,
|
|
177
|
+
'CODEX_HOME_UNSAFE',
|
|
178
|
+
`Move the Codex ${kind} under an owner-controlled path.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function canonicalDirectory(target, label) {
|
|
185
|
+
requiredString(target, label);
|
|
186
|
+
if (!path.isAbsolute(target)) {
|
|
187
|
+
throw fail(
|
|
188
|
+
`Codex ${label} must be absolute.`,
|
|
189
|
+
'CODEX_RUNTIME_PROFILE_INVALID',
|
|
190
|
+
`Configure a canonical absolute ${label}.`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
let canonicalPath;
|
|
194
|
+
try {
|
|
195
|
+
canonicalPath = realpathSync(target);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
throw fail(
|
|
198
|
+
`Codex ${label} does not exist.`,
|
|
199
|
+
'CODEX_RUNTIME_PROFILE_INVALID',
|
|
200
|
+
`Provision the ${label} before enabling Codex.`,
|
|
201
|
+
error,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (canonicalPath !== target || !lstatSync(target).isDirectory()) {
|
|
205
|
+
throw fail(
|
|
206
|
+
`Codex ${label} must be a canonical directory.`,
|
|
207
|
+
'CODEX_RUNTIME_PROFILE_INVALID',
|
|
208
|
+
`Remove aliases from the configured ${label}.`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return canonicalPath;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function ensurePrivateDirectory(target, code, kind) {
|
|
215
|
+
if (!existsSync(target)) {
|
|
216
|
+
try {
|
|
217
|
+
mkdirSync(target, { mode: 0o700 });
|
|
218
|
+
chmodSync(target, 0o700);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
throw fail(
|
|
221
|
+
`Codex ${kind} could not be provisioned.`,
|
|
222
|
+
code,
|
|
223
|
+
`Create the ${kind} as an owner-only mode-0700 directory.`,
|
|
224
|
+
error,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
let canonicalPath;
|
|
229
|
+
try {
|
|
230
|
+
canonicalPath = realpathSync(target);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
throw fail(
|
|
233
|
+
`Codex ${kind} could not be resolved.`,
|
|
234
|
+
code,
|
|
235
|
+
`Create the ${kind} as an owner-only mode-0700 directory.`,
|
|
236
|
+
error,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (canonicalPath !== target) {
|
|
240
|
+
throw fail(
|
|
241
|
+
`Codex ${kind} must not be a path alias.`,
|
|
242
|
+
code,
|
|
243
|
+
`Replace the ${kind} with a canonical mode-0700 directory.`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
assertOwnedMode(target, {
|
|
247
|
+
code,
|
|
248
|
+
kind,
|
|
249
|
+
expectedMode: 0o700,
|
|
250
|
+
directory: true,
|
|
251
|
+
});
|
|
252
|
+
return canonicalPath;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function tomlString(value) {
|
|
256
|
+
requiredString(value, 'TOML value');
|
|
257
|
+
return JSON.stringify(value);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function tomlKey(value) {
|
|
261
|
+
return JSON.stringify(requiredString(value, 'TOML key'));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function buildOwnedConfig({
|
|
265
|
+
codexHome,
|
|
266
|
+
workspace,
|
|
267
|
+
daemonSecretRoots,
|
|
268
|
+
commandHome,
|
|
269
|
+
commandTmp,
|
|
270
|
+
}) {
|
|
271
|
+
const filesystem = {
|
|
272
|
+
':minimal': 'read',
|
|
273
|
+
[codexHome]: 'deny',
|
|
274
|
+
};
|
|
275
|
+
for (const root of [...daemonSecretRoots].sort()) {
|
|
276
|
+
filesystem[root] = 'deny';
|
|
277
|
+
}
|
|
278
|
+
filesystem[':workspace_roots'] = { '.': 'write' };
|
|
279
|
+
return {
|
|
280
|
+
cli_auth_credentials_store: 'file',
|
|
281
|
+
default_permissions: CODEX_PERMISSION_PROFILE_ID,
|
|
282
|
+
approval_policy: 'never',
|
|
283
|
+
approvals_reviewer: 'user',
|
|
284
|
+
web_search: 'disabled',
|
|
285
|
+
allow_login_shell: false,
|
|
286
|
+
shell_environment_policy: {
|
|
287
|
+
inherit: 'none',
|
|
288
|
+
ignore_default_excludes: false,
|
|
289
|
+
set: {
|
|
290
|
+
HOME: commandHome,
|
|
291
|
+
TMPDIR: commandTmp,
|
|
292
|
+
PATH: CONTROLLED_PATH,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
permissions: {
|
|
296
|
+
[CODEX_PERMISSION_PROFILE_ID]: {
|
|
297
|
+
filesystem,
|
|
298
|
+
network: { enabled: false },
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
projects: {
|
|
302
|
+
[workspace]: { trust_level: 'untrusted' },
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function renderOwnedConfig(config, {
|
|
308
|
+
codexHome,
|
|
309
|
+
workspace,
|
|
310
|
+
daemonSecretRoots,
|
|
311
|
+
}) {
|
|
312
|
+
const command = config.shell_environment_policy.set;
|
|
313
|
+
const filesystem = config.permissions[
|
|
314
|
+
CODEX_PERMISSION_PROFILE_ID
|
|
315
|
+
].filesystem;
|
|
316
|
+
const lines = [
|
|
317
|
+
'cli_auth_credentials_store = "file"',
|
|
318
|
+
`default_permissions = ${tomlString(CODEX_PERMISSION_PROFILE_ID)}`,
|
|
319
|
+
'approval_policy = "never"',
|
|
320
|
+
'approvals_reviewer = "user"',
|
|
321
|
+
'web_search = "disabled"',
|
|
322
|
+
'allow_login_shell = false',
|
|
323
|
+
'',
|
|
324
|
+
'[shell_environment_policy]',
|
|
325
|
+
'inherit = "none"',
|
|
326
|
+
'ignore_default_excludes = false',
|
|
327
|
+
'',
|
|
328
|
+
'[shell_environment_policy.set]',
|
|
329
|
+
`HOME = ${tomlString(command.HOME)}`,
|
|
330
|
+
`TMPDIR = ${tomlString(command.TMPDIR)}`,
|
|
331
|
+
`PATH = ${tomlString(command.PATH)}`,
|
|
332
|
+
'',
|
|
333
|
+
`[permissions.${tomlKey(CODEX_PERMISSION_PROFILE_ID)}.filesystem]`,
|
|
334
|
+
`${tomlKey(':minimal')} = "read"`,
|
|
335
|
+
`${tomlKey(codexHome)} = "deny"`,
|
|
336
|
+
];
|
|
337
|
+
for (const root of [...daemonSecretRoots].sort()) {
|
|
338
|
+
lines.push(`${tomlKey(root)} = "deny"`);
|
|
339
|
+
}
|
|
340
|
+
lines.push(
|
|
341
|
+
`${tomlKey(':workspace_roots')} = { "." = "write" }`,
|
|
342
|
+
'',
|
|
343
|
+
`[permissions.${tomlKey(CODEX_PERMISSION_PROFILE_ID)}.network]`,
|
|
344
|
+
'enabled = false',
|
|
345
|
+
'',
|
|
346
|
+
`[projects.${tomlKey(workspace)}]`,
|
|
347
|
+
'trust_level = "untrusted"',
|
|
348
|
+
'',
|
|
349
|
+
);
|
|
350
|
+
if (
|
|
351
|
+
filesystem[':minimal'] !== 'read'
|
|
352
|
+
|| filesystem[codexHome] !== 'deny'
|
|
353
|
+
|| !isDeepStrictEqual(filesystem[':workspace_roots'], { '.': 'write' })
|
|
354
|
+
) {
|
|
355
|
+
throw new Error('owned config rendering invariant failed');
|
|
356
|
+
}
|
|
357
|
+
return lines.join('\n');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function buildControlledEnv(codexHome, processEnv) {
|
|
361
|
+
const home = requiredString(processEnv?.HOME, 'app-server HOME');
|
|
362
|
+
const tmpdir = requiredString(processEnv?.TMPDIR, 'app-server TMPDIR');
|
|
363
|
+
if (!path.isAbsolute(home) || !path.isAbsolute(tmpdir)) {
|
|
364
|
+
throw fail(
|
|
365
|
+
'Codex app-server HOME and TMPDIR must be absolute.',
|
|
366
|
+
'CODEX_RUNTIME_PROFILE_INVALID',
|
|
367
|
+
'Configure explicit absolute service HOME and TMPDIR values.',
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return Object.fromEntries(Object.entries({
|
|
371
|
+
HOME: home,
|
|
372
|
+
PATH: CONTROLLED_PATH,
|
|
373
|
+
TMPDIR: tmpdir,
|
|
374
|
+
LANG: processEnv?.LANG,
|
|
375
|
+
LC_ALL: processEnv?.LC_ALL,
|
|
376
|
+
CODEX_HOME: codexHome,
|
|
377
|
+
}).filter(([, value]) => value !== undefined));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function validateBinaryReceipt(receipt, schema) {
|
|
381
|
+
if (
|
|
382
|
+
!receipt
|
|
383
|
+
|| typeof receipt !== 'object'
|
|
384
|
+
|| !Object.isFrozen(receipt)
|
|
385
|
+
|| !path.isAbsolute(receipt.path ?? '')
|
|
386
|
+
|| receipt.version !== CODEX_CLI_PINNED_VERSION
|
|
387
|
+
|| receipt.sha256 !== CODEX_BINARY_SHA256
|
|
388
|
+
|| schema?.cliVersion !== CODEX_CLI_PINNED_VERSION
|
|
389
|
+
|| schema?.binarySha256 !== CODEX_BINARY_SHA256
|
|
390
|
+
|| !SHA256_RE.test(schema?.generatedProtocolV2CanonicalSha256)
|
|
391
|
+
) {
|
|
392
|
+
throw fail(
|
|
393
|
+
'Codex binary receipt does not match the installed protocol pin.',
|
|
394
|
+
'CODEX_RUNTIME_PIN_MISMATCH',
|
|
395
|
+
'Resolve the pinned binary again with the exact installed Orchestra build.',
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function assertRootSeparation(codexHome, workspace, daemonSecretRoots) {
|
|
401
|
+
if (isWithin(codexHome, workspace) || isWithin(workspace, codexHome)) {
|
|
402
|
+
throw fail(
|
|
403
|
+
'Codex workspace and credential home overlap.',
|
|
404
|
+
'CODEX_RUNTIME_ROOT_OVERLAP',
|
|
405
|
+
'Use separate canonical workspace and credential roots.',
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
for (const root of daemonSecretRoots) {
|
|
409
|
+
if (
|
|
410
|
+
isWithin(root, workspace)
|
|
411
|
+
|| isWithin(workspace, root)
|
|
412
|
+
|| isWithin(root, codexHome)
|
|
413
|
+
|| isWithin(codexHome, root)
|
|
414
|
+
) {
|
|
415
|
+
throw fail(
|
|
416
|
+
'Codex daemon-secret root overlaps the workspace or credential home.',
|
|
417
|
+
'CODEX_RUNTIME_ROOT_OVERLAP',
|
|
418
|
+
'Move daemon secrets outside the Codex workspace and credential home.',
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function provisionRuntimeFiles(options, temporaryRoots) {
|
|
425
|
+
const workspace = canonicalDirectory(options.workspace, 'workspace');
|
|
426
|
+
const daemonSecretRoots = [...new Set(
|
|
427
|
+
(options.daemonSecretRoots ?? []).map(
|
|
428
|
+
(root) => canonicalDirectory(root, 'daemon-secret root'),
|
|
429
|
+
),
|
|
430
|
+
)].sort();
|
|
431
|
+
if (daemonSecretRoots.length === 0) {
|
|
432
|
+
throw fail(
|
|
433
|
+
'Codex requires at least one explicit daemon-secret root.',
|
|
434
|
+
'CODEX_RUNTIME_PROFILE_INVALID',
|
|
435
|
+
'Configure every daemon-secret root before enabling Codex.',
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const configuredHome = requiredString(options.codexHome, 'credential home');
|
|
440
|
+
if (!path.isAbsolute(configuredHome) || path.normalize(configuredHome) !== configuredHome) {
|
|
441
|
+
throw fail(
|
|
442
|
+
'Codex credential home must be a normalized absolute path.',
|
|
443
|
+
'CODEX_HOME_UNSAFE',
|
|
444
|
+
'Configure a canonical non-temporary mode-0700 credential home.',
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
if (temporaryRoots.some((root) => isWithin(root, configuredHome))) {
|
|
448
|
+
throw fail(
|
|
449
|
+
'Codex credential home cannot be inside a temporary root.',
|
|
450
|
+
'CODEX_HOME_UNSAFE',
|
|
451
|
+
'Move CODEX_HOME to a persistent owner-only deployment directory.',
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
if (!existsSync(configuredHome)) {
|
|
455
|
+
const parent = path.dirname(configuredHome);
|
|
456
|
+
if (realpathSync(parent) !== parent) {
|
|
457
|
+
throw fail(
|
|
458
|
+
'Codex credential-home parent must be canonical.',
|
|
459
|
+
'CODEX_HOME_UNSAFE',
|
|
460
|
+
'Provision CODEX_HOME under an owner-controlled canonical parent.',
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
assertSafeParentChain(parent, 'credential home');
|
|
464
|
+
}
|
|
465
|
+
const codexHome = ensurePrivateDirectory(
|
|
466
|
+
configuredHome,
|
|
467
|
+
'CODEX_HOME_UNSAFE',
|
|
468
|
+
'credential home',
|
|
469
|
+
);
|
|
470
|
+
assertSafeParentChain(codexHome, 'credential home');
|
|
471
|
+
assertRootSeparation(codexHome, workspace, daemonSecretRoots);
|
|
472
|
+
|
|
473
|
+
const commandHome = ensurePrivateDirectory(
|
|
474
|
+
path.join(workspace, '.codex-command-home'),
|
|
475
|
+
'CODEX_COMMAND_DIR_UNSAFE',
|
|
476
|
+
'command HOME',
|
|
477
|
+
);
|
|
478
|
+
const commandTmp = ensurePrivateDirectory(
|
|
479
|
+
path.join(workspace, '.codex-command-tmp'),
|
|
480
|
+
'CODEX_COMMAND_DIR_UNSAFE',
|
|
481
|
+
'command TMPDIR',
|
|
482
|
+
);
|
|
483
|
+
const ownedConfig = buildOwnedConfig({
|
|
484
|
+
codexHome,
|
|
485
|
+
workspace,
|
|
486
|
+
daemonSecretRoots,
|
|
487
|
+
commandHome,
|
|
488
|
+
commandTmp,
|
|
489
|
+
});
|
|
490
|
+
const rawConfig = renderOwnedConfig(ownedConfig, {
|
|
491
|
+
codexHome,
|
|
492
|
+
workspace,
|
|
493
|
+
daemonSecretRoots,
|
|
494
|
+
});
|
|
495
|
+
const configPath = path.join(codexHome, 'config.toml');
|
|
496
|
+
if (existsSync(configPath)) {
|
|
497
|
+
assertOwnedMode(configPath, {
|
|
498
|
+
code: 'CODEX_OWNED_CONFIG_DRIFT',
|
|
499
|
+
kind: 'owned config.toml',
|
|
500
|
+
expectedMode: 0o600,
|
|
501
|
+
directory: false,
|
|
502
|
+
});
|
|
503
|
+
if (readFileSync(configPath, 'utf8') !== rawConfig) {
|
|
504
|
+
throw fail(
|
|
505
|
+
'Codex owned config.toml differs from the native-beta policy.',
|
|
506
|
+
'CODEX_OWNED_CONFIG_DRIFT',
|
|
507
|
+
'Restore the exact owned config; it is never overwritten automatically.',
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
} else {
|
|
511
|
+
try {
|
|
512
|
+
writeFileSync(configPath, rawConfig, { flag: 'wx', mode: 0o600 });
|
|
513
|
+
chmodSync(configPath, 0o600);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
throw fail(
|
|
516
|
+
'Codex owned config.toml could not be provisioned.',
|
|
517
|
+
'CODEX_OWNED_CONFIG_DRIFT',
|
|
518
|
+
'Provision the exact owner-only config before enabling Codex.',
|
|
519
|
+
error,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const authPath = path.join(codexHome, 'auth.json');
|
|
525
|
+
if (existsSync(authPath)) {
|
|
526
|
+
assertOwnedMode(authPath, {
|
|
527
|
+
code: 'CODEX_HOME_UNSAFE',
|
|
528
|
+
kind: 'auth.json',
|
|
529
|
+
expectedMode: 0o600,
|
|
530
|
+
directory: false,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
codexHome,
|
|
536
|
+
workspace,
|
|
537
|
+
daemonSecretRoots,
|
|
538
|
+
commandHome,
|
|
539
|
+
commandTmp,
|
|
540
|
+
ownedConfig,
|
|
541
|
+
rawConfig,
|
|
542
|
+
ownedConfigSha256: digest(rawConfig),
|
|
543
|
+
env: buildControlledEnv(codexHome, options.processEnv ?? process.env),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function expectedFilesystem(materialized) {
|
|
548
|
+
return {
|
|
549
|
+
glob_scan_max_depth: null,
|
|
550
|
+
...materialized.ownedConfig.permissions[
|
|
551
|
+
CODEX_PERMISSION_PROFILE_ID
|
|
552
|
+
].filesystem,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function validateProjectedConfig(config, materialized) {
|
|
557
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
558
|
+
throw new Error('effective config projection is missing');
|
|
559
|
+
}
|
|
560
|
+
const allowedKeys = [
|
|
561
|
+
'sha256',
|
|
562
|
+
'model',
|
|
563
|
+
'modelProvider',
|
|
564
|
+
'defaultPermissions',
|
|
565
|
+
'approvalPolicy',
|
|
566
|
+
'approvalsReviewer',
|
|
567
|
+
'webSearch',
|
|
568
|
+
'allowLoginShell',
|
|
569
|
+
'shellEnvironmentInherit',
|
|
570
|
+
'permissionProfiles',
|
|
571
|
+
'mcpServers',
|
|
572
|
+
'plugins',
|
|
573
|
+
'modelProviders',
|
|
574
|
+
].sort();
|
|
575
|
+
if (
|
|
576
|
+
JSON.stringify(Object.keys(config).sort()) !== JSON.stringify(allowedKeys)
|
|
577
|
+
|| !SHA256_RE.test(config.sha256)
|
|
578
|
+
|| (config.model !== null && typeof config.model !== 'string')
|
|
579
|
+
|| config.modelProvider !== 'openai'
|
|
580
|
+
|| config.defaultPermissions !== CODEX_PERMISSION_PROFILE_ID
|
|
581
|
+
|| config.approvalPolicy !== 'never'
|
|
582
|
+
|| config.approvalsReviewer !== 'user'
|
|
583
|
+
|| config.webSearch !== 'disabled'
|
|
584
|
+
|| config.allowLoginShell !== false
|
|
585
|
+
|| config.shellEnvironmentInherit !== 'none'
|
|
586
|
+
|| !isDeepStrictEqual(config.mcpServers, { count: 0, keySha256: [] })
|
|
587
|
+
|| !isDeepStrictEqual(config.plugins, { count: 0, keySha256: [] })
|
|
588
|
+
|| !isDeepStrictEqual(config.modelProviders, {
|
|
589
|
+
count: 1,
|
|
590
|
+
keySha256: [digest('openai')],
|
|
591
|
+
})
|
|
592
|
+
|| !Array.isArray(config.permissionProfiles)
|
|
593
|
+
|| config.permissionProfiles.length !== 1
|
|
594
|
+
) {
|
|
595
|
+
throw new Error('effective config projection is outside owned policy');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const profile = config.permissionProfiles[0];
|
|
599
|
+
const filesystem = expectedFilesystem(materialized);
|
|
600
|
+
const rules = Object.entries(filesystem)
|
|
601
|
+
.filter(([, access]) => typeof access === 'string')
|
|
602
|
+
.map(([root, access]) => ({ rootSha256: digest(root), access }))
|
|
603
|
+
.sort((left, right) => left.rootSha256.localeCompare(right.rootSha256));
|
|
604
|
+
if (
|
|
605
|
+
profile?.id !== CODEX_PERMISSION_PROFILE_ID
|
|
606
|
+
|| profile.extends !== null
|
|
607
|
+
|| profile.networkEnabled !== false
|
|
608
|
+
|| profile.filesystemSha256 !== digest(filesystem)
|
|
609
|
+
|| !isDeepStrictEqual(profile.filesystem, rules)
|
|
610
|
+
) {
|
|
611
|
+
throw new Error('permission profile projection is outside owned policy');
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function validateProjectedLayers(layers, materialized) {
|
|
616
|
+
if (!Array.isArray(layers) || ![1, 2].includes(layers.length)) {
|
|
617
|
+
throw new Error('config layers are outside the owned layer set');
|
|
618
|
+
}
|
|
619
|
+
let userCount = 0;
|
|
620
|
+
let systemCount = 0;
|
|
621
|
+
for (const layer of layers) {
|
|
622
|
+
if (
|
|
623
|
+
!layer
|
|
624
|
+
|| typeof layer !== 'object'
|
|
625
|
+
|| Array.isArray(layer)
|
|
626
|
+
|| JSON.stringify(Object.keys(layer).sort())
|
|
627
|
+
!== JSON.stringify(['configSha256', 'disabled', 'type', 'version'])
|
|
628
|
+
|| typeof layer.version !== 'string'
|
|
629
|
+
|| layer.version.length === 0
|
|
630
|
+
|| layer.disabled !== false
|
|
631
|
+
|| !SHA256_RE.test(layer.configSha256)
|
|
632
|
+
) {
|
|
633
|
+
throw new Error('config layer projection is malformed');
|
|
634
|
+
}
|
|
635
|
+
if (layer.type === 'user') {
|
|
636
|
+
userCount += 1;
|
|
637
|
+
if (layer.configSha256 !== digest(materialized.ownedConfig)) {
|
|
638
|
+
throw new Error('owned user layer hash changed');
|
|
639
|
+
}
|
|
640
|
+
} else if (layer.type === 'system') {
|
|
641
|
+
systemCount += 1;
|
|
642
|
+
if (layer.configSha256 !== digest({})) {
|
|
643
|
+
throw new Error('system layer is not empty');
|
|
644
|
+
}
|
|
645
|
+
} else {
|
|
646
|
+
throw new Error('unexpected config layer is active');
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (userCount !== 1 || systemCount > 1) {
|
|
650
|
+
throw new Error('owned user layer is not unique');
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function validateRequirements(requirements) {
|
|
655
|
+
if (
|
|
656
|
+
!requirements
|
|
657
|
+
|| typeof requirements !== 'object'
|
|
658
|
+
|| Array.isArray(requirements)
|
|
659
|
+
|| Object.keys(requirements).length !== 1
|
|
660
|
+
|| requirements.requirements !== null
|
|
661
|
+
) {
|
|
662
|
+
throw new Error(
|
|
663
|
+
'managed requirements cannot be validated from the public projection',
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function collectPermissionProfiles(client, workspace) {
|
|
669
|
+
const profiles = [];
|
|
670
|
+
const ids = new Set();
|
|
671
|
+
const cursors = new Set();
|
|
672
|
+
let cursor;
|
|
673
|
+
for (let page = 0; page < MAX_PAGES; page += 1) {
|
|
674
|
+
const params = cursor === undefined
|
|
675
|
+
? { cwd: workspace }
|
|
676
|
+
: { cwd: workspace, cursor };
|
|
677
|
+
const result = await client.request('permissionProfile/list', params);
|
|
678
|
+
if (
|
|
679
|
+
!result
|
|
680
|
+
|| !Array.isArray(result.data)
|
|
681
|
+
|| result.data.length > 100
|
|
682
|
+
) {
|
|
683
|
+
throw new Error('permission profile page is malformed');
|
|
684
|
+
}
|
|
685
|
+
for (const profile of result.data) {
|
|
686
|
+
requiredString(profile?.id, 'permission profile ID');
|
|
687
|
+
if (
|
|
688
|
+
ids.has(profile.id)
|
|
689
|
+
|| typeof profile.allowed !== 'boolean'
|
|
690
|
+
|| (
|
|
691
|
+
profile.descriptionSha256 !== null
|
|
692
|
+
&& !SHA256_RE.test(profile.descriptionSha256)
|
|
693
|
+
)
|
|
694
|
+
) {
|
|
695
|
+
throw new Error('permission profile catalog is malformed');
|
|
696
|
+
}
|
|
697
|
+
ids.add(profile.id);
|
|
698
|
+
profiles.push({
|
|
699
|
+
id: profile.id,
|
|
700
|
+
allowed: profile.allowed,
|
|
701
|
+
descriptionSha256: profile.descriptionSha256,
|
|
702
|
+
});
|
|
703
|
+
if (profiles.length > MAX_PROFILES) {
|
|
704
|
+
throw new Error('permission profile catalog exceeded its bound');
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (result.nextCursor == null) break;
|
|
708
|
+
requiredString(result.nextCursor, 'permission profile cursor');
|
|
709
|
+
if (cursors.has(result.nextCursor)) {
|
|
710
|
+
throw new Error('permission profile cursor repeated');
|
|
711
|
+
}
|
|
712
|
+
cursors.add(result.nextCursor);
|
|
713
|
+
cursor = result.nextCursor;
|
|
714
|
+
if (page === MAX_PAGES - 1) {
|
|
715
|
+
throw new Error('permission profile pagination exceeded its bound');
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const owned = profiles.filter(
|
|
719
|
+
({ id }) => id === CODEX_PERMISSION_PROFILE_ID,
|
|
720
|
+
);
|
|
721
|
+
if (owned.length !== 1 || owned[0].allowed !== true) {
|
|
722
|
+
throw new Error('owned permission profile is not uniquely allowed');
|
|
723
|
+
}
|
|
724
|
+
return profiles.sort((left, right) => left.id.localeCompare(right.id));
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
async function characterizeRuntime(materialized, {
|
|
728
|
+
binaryReceipt,
|
|
729
|
+
clientFactory,
|
|
730
|
+
}) {
|
|
731
|
+
let latchedFault = null;
|
|
732
|
+
const client = clientFactory({
|
|
733
|
+
binary: binaryReceipt.path,
|
|
734
|
+
cwd: materialized.workspace,
|
|
735
|
+
codexHome: materialized.codexHome,
|
|
736
|
+
env: materialized.env,
|
|
737
|
+
expectedConfigSha256: materialized.ownedConfigSha256,
|
|
738
|
+
onNotification: async () => {
|
|
739
|
+
throw fail(
|
|
740
|
+
'Codex runtime characterization received a state notification.',
|
|
741
|
+
'CODEX_RUNTIME_POLICY_DRIFT',
|
|
742
|
+
'Disable Codex and inspect the pinned app-server protocol drift.',
|
|
743
|
+
);
|
|
744
|
+
},
|
|
745
|
+
onFault: (outcome) => {
|
|
746
|
+
outcome?.assertActive?.();
|
|
747
|
+
latchedFault = outcome;
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
if (
|
|
751
|
+
!client
|
|
752
|
+
|| typeof client.start !== 'function'
|
|
753
|
+
|| typeof client.request !== 'function'
|
|
754
|
+
|| typeof client.close !== 'function'
|
|
755
|
+
|| typeof client.waitForFault !== 'function'
|
|
756
|
+
) {
|
|
757
|
+
throw new TypeError('Codex runtime clientFactory returned an invalid client');
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
let result;
|
|
761
|
+
let failure = null;
|
|
762
|
+
try {
|
|
763
|
+
await client.start();
|
|
764
|
+
const configRead = await client.request('config/read', {
|
|
765
|
+
cwd: materialized.workspace,
|
|
766
|
+
includeLayers: true,
|
|
767
|
+
});
|
|
768
|
+
const requirements = await client.request('configRequirements/read');
|
|
769
|
+
const permissionProfiles = await collectPermissionProfiles(
|
|
770
|
+
client,
|
|
771
|
+
materialized.workspace,
|
|
772
|
+
);
|
|
773
|
+
validateProjectedConfig(configRead?.config, materialized);
|
|
774
|
+
validateProjectedLayers(configRead?.layers, materialized);
|
|
775
|
+
requiredSha256(configRead?.originsSha256, 'origins projection');
|
|
776
|
+
validateRequirements(requirements);
|
|
777
|
+
result = {
|
|
778
|
+
config: configRead.config,
|
|
779
|
+
layers: configRead.layers,
|
|
780
|
+
originsSha256: configRead.originsSha256,
|
|
781
|
+
requirements,
|
|
782
|
+
permissionProfiles,
|
|
783
|
+
};
|
|
784
|
+
} catch (error) {
|
|
785
|
+
failure = error;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
try {
|
|
789
|
+
await client.close();
|
|
790
|
+
} catch (error) {
|
|
791
|
+
throw fail(
|
|
792
|
+
'Codex runtime characterization cleanup failed.',
|
|
793
|
+
'CODEX_RUNTIME_CLIENT_CLOSE_FAILED',
|
|
794
|
+
'Disable Codex and inspect the pinned app-server child cleanup.',
|
|
795
|
+
error,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
let terminalFault;
|
|
800
|
+
try {
|
|
801
|
+
terminalFault = await client.waitForFault();
|
|
802
|
+
} catch (error) {
|
|
803
|
+
throw fail(
|
|
804
|
+
'Codex runtime client fault handoff failed.',
|
|
805
|
+
'CODEX_RUNTIME_CLIENT_FAULT',
|
|
806
|
+
'Disable Codex and inspect the pinned app-server fault handoff.',
|
|
807
|
+
error,
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
if (latchedFault || terminalFault) {
|
|
811
|
+
throw fail(
|
|
812
|
+
'Codex runtime client faulted during policy characterization.',
|
|
813
|
+
'CODEX_RUNTIME_CLIENT_FAULT',
|
|
814
|
+
'Disable Codex and inspect the pinned app-server fault.',
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
if (failure) {
|
|
818
|
+
if (failure instanceof CodexRuntimeProfileError) throw failure;
|
|
819
|
+
throw fail(
|
|
820
|
+
'Codex runtime policy characterization failed closed.',
|
|
821
|
+
'CODEX_RUNTIME_POLICY_DRIFT',
|
|
822
|
+
'Restore the exact owned policy and rerun characterization.',
|
|
823
|
+
failure,
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function createCodexRuntimeProfileBuilder({
|
|
830
|
+
temporaryRoots = canonicalTemporaryRoots(),
|
|
831
|
+
clientFactory = (options) => new CodexAppServerClient(options),
|
|
832
|
+
protocolSchema = codexProtocolSchema,
|
|
833
|
+
} = {}) {
|
|
834
|
+
if (
|
|
835
|
+
!Array.isArray(temporaryRoots)
|
|
836
|
+
|| typeof clientFactory !== 'function'
|
|
837
|
+
) {
|
|
838
|
+
throw new TypeError('Codex runtime profile builder options are invalid');
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return Object.freeze({
|
|
842
|
+
async prepare(options = {}) {
|
|
843
|
+
validateBinaryReceipt(options.binaryReceipt, protocolSchema);
|
|
844
|
+
const materialized = provisionRuntimeFiles(options, temporaryRoots);
|
|
845
|
+
const characterized = await characterizeRuntime(materialized, {
|
|
846
|
+
binaryReceipt: options.binaryReceipt,
|
|
847
|
+
clientFactory,
|
|
848
|
+
});
|
|
849
|
+
const model = requiredString(options.model, 'model');
|
|
850
|
+
const effort = requiredString(options.effort, 'reasoning effort');
|
|
851
|
+
return deepFreeze({
|
|
852
|
+
runtime: 'codex',
|
|
853
|
+
binary: options.binaryReceipt.path,
|
|
854
|
+
binarySha256: options.binaryReceipt.sha256,
|
|
855
|
+
cliVersion: options.binaryReceipt.version,
|
|
856
|
+
protocolSchemaSha256:
|
|
857
|
+
protocolSchema.generatedProtocolV2CanonicalSha256,
|
|
858
|
+
codexHome: materialized.codexHome,
|
|
859
|
+
cwd: materialized.workspace,
|
|
860
|
+
env: materialized.env,
|
|
861
|
+
allowlistedEnvironmentFingerprint: digest(materialized.env),
|
|
862
|
+
ownedConfigSha256: materialized.ownedConfigSha256,
|
|
863
|
+
expectedConfigSha256: characterized.config.sha256,
|
|
864
|
+
expectedConfig: characterized.config,
|
|
865
|
+
expectedLayers: characterized.layers,
|
|
866
|
+
expectedOriginsSha256: characterized.originsSha256,
|
|
867
|
+
expectedRequirements: characterized.requirements.requirements,
|
|
868
|
+
expectedPermissionProfiles: characterized.permissionProfiles,
|
|
869
|
+
permissionProfileId: CODEX_PERMISSION_PROFILE_ID,
|
|
870
|
+
model,
|
|
871
|
+
effort,
|
|
872
|
+
});
|
|
873
|
+
},
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const defaultBuilder = createCodexRuntimeProfileBuilder();
|
|
878
|
+
|
|
879
|
+
async function prepareCodexRuntimeProfile(options) {
|
|
880
|
+
return defaultBuilder.prepare(options);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
module.exports = {
|
|
884
|
+
CODEX_PERMISSION_PROFILE_ID,
|
|
885
|
+
CodexRuntimeProfileError,
|
|
886
|
+
createCodexRuntimeProfileBuilder,
|
|
887
|
+
prepareCodexRuntimeProfile,
|
|
888
|
+
};
|