@brunobracaioli/aibrain 0.2.0-beta.1 → 0.3.0-beta.13
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/README.md +67 -4
- package/dist/assets/claude/.claude-plugin/plugin.json +49 -1
- package/dist/assets/claude/hooks/context-compaction.ts +6062 -734
- package/dist/assets/claude/hooks/provider-window.js +263 -0
- package/dist/assets/claude/hooks/state-fitting.d.mts +47 -0
- package/dist/assets/claude/hooks/state-fitting.mjs +324 -0
- package/dist/assets/codex-context-compaction.mjs +2690 -266
- package/dist/assets/context-selection-fitting.mjs +194 -0
- package/dist/assets/context-state-fitting.mjs +324 -0
- package/dist/assets/context-telemetry-reporter.mjs +305 -0
- package/dist/assets/jev-paid-pilot-terminal.mjs +591 -125
- package/dist/claude-checkpoint.mjs +1338 -0
- package/dist/claude-observation.mjs +1336 -0
- package/dist/cli.mjs +250 -20
- package/dist/codex-compatibility.mjs +467 -0
- package/dist/codex-diagnostics.mjs +426 -0
- package/dist/codex-permissions.mjs +590 -0
- package/dist/codex-protocol-fingerprint.json +131 -0
- package/dist/context-telemetry.mjs +257 -0
- package/dist/launch.mjs +341 -22
- package/dist/mcp.mjs +329 -7
- package/dist/monitor.mjs +11 -0
- package/dist/package.json +1 -1
- package/dist/platform.mjs +33 -3
- package/dist/state.mjs +107 -4
- package/dist/telemetry-report.mjs +133 -0
- package/package.json +1 -1
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { lstat } from 'node:fs/promises';
|
|
5
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
6
|
+
|
|
7
|
+
/** @typedef {Record<string, unknown>} JsonObject */
|
|
8
|
+
/** @typedef {JsonObject & { version: number, writableRoots: string[], profile?: string }} PermissionEnvelope */
|
|
9
|
+
/** @typedef {JsonObject & { additionalRoots: string[], nativeArgs: string[], effectiveCwd: string, profile?: string|undefined, server: JsonObject }} LaunchPlan */
|
|
10
|
+
/** @typedef {import('node:child_process').ChildProcess} OwnedChild */
|
|
11
|
+
/** @typedef {{ id?: string|number, result?: JsonObject, error?: unknown }} JsonRpcResponse */
|
|
12
|
+
|
|
13
|
+
export const CODEX_LAUNCH_ENVELOPE = 'AIBRAIN_CODEX_LAUNCH_ENVELOPE';
|
|
14
|
+
export const CODEX_APP_SERVER_PROFILE = 'CODEX_APP_SERVER_PROFILE';
|
|
15
|
+
export const CODEX_MONITOR_CWD = 'CODEX_MONITOR_CWD';
|
|
16
|
+
|
|
17
|
+
const ENVELOPE_VERSION = 1;
|
|
18
|
+
const MAX_ROOTS = 32;
|
|
19
|
+
const MAX_PATH_BYTES = 4096;
|
|
20
|
+
const MAX_ENVELOPE_BYTES = 16 * 1024;
|
|
21
|
+
const PREFLIGHT_TIMEOUT_MS = 5_000;
|
|
22
|
+
const PREFLIGHT_FRAME_BYTES = 2 * 1024 * 1024;
|
|
23
|
+
const PROFILE_PATTERN = /^[A-Za-z0-9._-]{1,64}$/u;
|
|
24
|
+
const SANDBOX_MODES = new Set(['read-only', 'workspace-write', 'danger-full-access']);
|
|
25
|
+
const APPROVAL_POLICIES = new Set(['untrusted', 'on-request', 'never']);
|
|
26
|
+
const REVIEWERS = new Set(['user', 'auto_review', 'guardian_subagent']);
|
|
27
|
+
const CONFIG_KEYS = new Set([
|
|
28
|
+
'sandbox_mode',
|
|
29
|
+
'approval_policy',
|
|
30
|
+
'approvals_reviewer',
|
|
31
|
+
'apps._default.approvals_reviewer',
|
|
32
|
+
'sandbox_workspace_write.network_access',
|
|
33
|
+
]);
|
|
34
|
+
const ENVELOPE_KEYS = new Set([
|
|
35
|
+
'version',
|
|
36
|
+
'writableRoots',
|
|
37
|
+
'profile',
|
|
38
|
+
'sandboxMode',
|
|
39
|
+
'approvalPolicy',
|
|
40
|
+
'approvalsReviewer',
|
|
41
|
+
'appsDefaultApprovalsReviewer',
|
|
42
|
+
'networkAccess',
|
|
43
|
+
]);
|
|
44
|
+
const VALUE_OPTIONS = new Set([
|
|
45
|
+
'--add-dir',
|
|
46
|
+
'--sandbox',
|
|
47
|
+
'-s',
|
|
48
|
+
'--ask-for-approval',
|
|
49
|
+
'-a',
|
|
50
|
+
'--profile',
|
|
51
|
+
'-p',
|
|
52
|
+
'-c',
|
|
53
|
+
'--config',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/** @param {string} code @returns {never} */
|
|
57
|
+
function fail(code) {
|
|
58
|
+
throw new Error(`codex_permissions_${code}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @param {string} argument */
|
|
62
|
+
function optionName(argument) {
|
|
63
|
+
return argument.split('=', 1)[0];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @param {string} value @param {string} key */
|
|
67
|
+
function parseBoolean(value, key) {
|
|
68
|
+
if (value === 'true') return true;
|
|
69
|
+
if (value === 'false') return false;
|
|
70
|
+
fail(`config_value_invalid:${key}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @param {string} value @param {string} key */
|
|
74
|
+
function parseTomlString(value, key) {
|
|
75
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(value);
|
|
78
|
+
if (typeof parsed === 'string') return parsed;
|
|
79
|
+
} catch {
|
|
80
|
+
// Fall through to the bounded error below.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (/^[A-Za-z0-9._-]+$/u.test(value)) return value;
|
|
84
|
+
fail(`config_value_invalid:${key}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** @param {string} key @param {string} value */
|
|
88
|
+
function assertValue(key, value) {
|
|
89
|
+
if (key === 'sandbox_mode') {
|
|
90
|
+
const parsed = parseTomlString(value, key);
|
|
91
|
+
if (!SANDBOX_MODES.has(parsed)) fail('sandbox_invalid');
|
|
92
|
+
return parsed;
|
|
93
|
+
}
|
|
94
|
+
if (key === 'approval_policy') {
|
|
95
|
+
const parsed = parseTomlString(value, key);
|
|
96
|
+
if (!APPROVAL_POLICIES.has(parsed)) fail('approval_invalid');
|
|
97
|
+
return parsed;
|
|
98
|
+
}
|
|
99
|
+
if (key === 'approvals_reviewer' || key === 'apps._default.approvals_reviewer') {
|
|
100
|
+
const parsed = parseTomlString(value, key);
|
|
101
|
+
if (!REVIEWERS.has(parsed)) fail('reviewer_invalid');
|
|
102
|
+
return parsed;
|
|
103
|
+
}
|
|
104
|
+
if (key === 'sandbox_workspace_write.network_access') return parseBoolean(value, key);
|
|
105
|
+
fail(`config_key_unsupported:${key}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @param {JsonObject} target @param {string} key @param {string} value */
|
|
109
|
+
function setPermission(target, key, value) {
|
|
110
|
+
const parsed = assertValue(key, value);
|
|
111
|
+
const field =
|
|
112
|
+
key === 'sandbox_mode'
|
|
113
|
+
? 'sandboxMode'
|
|
114
|
+
: key === 'approval_policy'
|
|
115
|
+
? 'approvalPolicy'
|
|
116
|
+
: key === 'approvals_reviewer'
|
|
117
|
+
? 'approvalsReviewer'
|
|
118
|
+
: key === 'apps._default.approvals_reviewer'
|
|
119
|
+
? 'appsDefaultApprovalsReviewer'
|
|
120
|
+
: 'networkAccess';
|
|
121
|
+
if (target[field] !== undefined && target[field] !== parsed) fail(`permission_conflict:${key}`);
|
|
122
|
+
target[field] = parsed;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** @param {string[]} args @param {number} index @param {string} argument */
|
|
126
|
+
function consumeValue(args, index, argument) {
|
|
127
|
+
const value = args[index + 1];
|
|
128
|
+
if (value === undefined || value === '' || value === '--') fail(`value_required:${argument}`);
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** @param {string} raw @param {JsonObject} target */
|
|
133
|
+
function parseConfigArgument(raw, target) {
|
|
134
|
+
const equals = raw.indexOf('=');
|
|
135
|
+
if (equals <= 0) fail('config_value_required');
|
|
136
|
+
const key = raw.slice(0, equals);
|
|
137
|
+
const value = raw.slice(equals + 1);
|
|
138
|
+
if (key === 'sandbox_workspace_write.writable_roots') fail('writable_roots_override_unsupported');
|
|
139
|
+
if (!CONFIG_KEYS.has(key)) return false;
|
|
140
|
+
setPermission(target, key, value);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** @param {string} value @param {string} cwd */
|
|
145
|
+
async function normalizeAdditionalRoot(value, cwd) {
|
|
146
|
+
if (value.length === 0 || value.includes('\0')) fail('root_invalid');
|
|
147
|
+
const path = resolve(cwd, value);
|
|
148
|
+
if (Buffer.byteLength(path, 'utf8') > MAX_PATH_BYTES) fail('root_too_long');
|
|
149
|
+
const info = await lstat(path).catch(() => null);
|
|
150
|
+
if (info === null) fail('root_missing');
|
|
151
|
+
if (info.isSymbolicLink()) fail('root_symlink_rejected');
|
|
152
|
+
if (!info.isDirectory()) fail('root_not_directory');
|
|
153
|
+
return path;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** @param {string[]} values */
|
|
157
|
+
function deduplicate(values) {
|
|
158
|
+
return [...new Set(values)];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Parse native arguments while preserving all ordinary terminal arguments.
|
|
163
|
+
* @param {string[]} args
|
|
164
|
+
* @param {string} cwd
|
|
165
|
+
*/
|
|
166
|
+
export async function parseCodexLaunchArgs(args, cwd) {
|
|
167
|
+
let effectiveCwd = cwd;
|
|
168
|
+
let cwdValue;
|
|
169
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
170
|
+
const argument = args[index];
|
|
171
|
+
if (argument === '--') break;
|
|
172
|
+
const name = optionName(argument);
|
|
173
|
+
if (name !== '--cd' && name !== '-C') {
|
|
174
|
+
if (!argument.includes('=') && VALUE_OPTIONS.has(name)) {
|
|
175
|
+
if (index + 1 >= args.length || args[index + 1] === '--')
|
|
176
|
+
fail(`value_required:${argument}`);
|
|
177
|
+
index += 1;
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const value = argument.includes('=')
|
|
182
|
+
? argument.slice(argument.indexOf('=') + 1)
|
|
183
|
+
: consumeValue(args, index, argument);
|
|
184
|
+
if (!argument.includes('=')) index += 1;
|
|
185
|
+
const nextCwd = resolve(cwd, value);
|
|
186
|
+
if (cwdValue !== undefined && nextCwd !== cwdValue) fail('cwd_conflict');
|
|
187
|
+
cwdValue = nextCwd;
|
|
188
|
+
}
|
|
189
|
+
if (cwdValue !== undefined) {
|
|
190
|
+
const info = await lstat(cwdValue).catch(() => null);
|
|
191
|
+
if (info === null || info.isSymbolicLink() || !info.isDirectory()) fail('cwd_invalid');
|
|
192
|
+
effectiveCwd = cwdValue;
|
|
193
|
+
}
|
|
194
|
+
/** @type {JsonObject} */
|
|
195
|
+
const server = {};
|
|
196
|
+
const additionalRoots = [];
|
|
197
|
+
const nativeArgs = [];
|
|
198
|
+
let profile;
|
|
199
|
+
let afterTerminator = false;
|
|
200
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
201
|
+
const argument = args[index];
|
|
202
|
+
if (afterTerminator) {
|
|
203
|
+
nativeArgs.push(argument);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (argument === '--') {
|
|
207
|
+
afterTerminator = true;
|
|
208
|
+
nativeArgs.push(argument);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const name = optionName(argument);
|
|
212
|
+
if (name === '--add-dir' || name === '--add-dir=') {
|
|
213
|
+
const value = argument.startsWith('--add-dir=')
|
|
214
|
+
? argument.slice('--add-dir='.length)
|
|
215
|
+
: consumeValue(args, index, argument);
|
|
216
|
+
if (!argument.startsWith('--add-dir=')) index += 1;
|
|
217
|
+
additionalRoots.push(await normalizeAdditionalRoot(value, effectiveCwd));
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (name === '--sandbox' || name === '-s') {
|
|
221
|
+
const value = argument.includes('=')
|
|
222
|
+
? argument.slice(argument.indexOf('=') + 1)
|
|
223
|
+
: consumeValue(args, index, argument);
|
|
224
|
+
if (!argument.includes('=')) index += 1;
|
|
225
|
+
if (!SANDBOX_MODES.has(value)) fail('sandbox_invalid');
|
|
226
|
+
setPermission(server, 'sandbox_mode', JSON.stringify(value));
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (name === '--ask-for-approval' || name === '-a') {
|
|
230
|
+
const value = argument.includes('=')
|
|
231
|
+
? argument.slice(argument.indexOf('=') + 1)
|
|
232
|
+
: consumeValue(args, index, argument);
|
|
233
|
+
if (!argument.includes('=')) index += 1;
|
|
234
|
+
if (!APPROVAL_POLICIES.has(value)) fail('approval_invalid');
|
|
235
|
+
setPermission(server, 'approval_policy', JSON.stringify(value));
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (name === '--profile' || name === '-p') {
|
|
239
|
+
const value = argument.includes('=')
|
|
240
|
+
? argument.slice(argument.indexOf('=') + 1)
|
|
241
|
+
: consumeValue(args, index, argument);
|
|
242
|
+
if (!argument.includes('=')) index += 1;
|
|
243
|
+
if (!PROFILE_PATTERN.test(value)) fail('profile_invalid');
|
|
244
|
+
if (profile !== undefined && profile !== value) fail('profile_conflict');
|
|
245
|
+
profile = value;
|
|
246
|
+
nativeArgs.push(argument);
|
|
247
|
+
if (!argument.includes('=')) nativeArgs.push(value);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (name === '--cd' || name === '-C') {
|
|
251
|
+
const value = argument.includes('=')
|
|
252
|
+
? argument.slice(argument.indexOf('=') + 1)
|
|
253
|
+
: consumeValue(args, index, argument);
|
|
254
|
+
if (!argument.includes('=')) index += 1;
|
|
255
|
+
nativeArgs.push(argument);
|
|
256
|
+
if (!argument.includes('=')) nativeArgs.push(value);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (name === '-c' || name === '--config') {
|
|
260
|
+
const raw = argument.includes('=')
|
|
261
|
+
? argument.slice(argument.indexOf('=') + 1)
|
|
262
|
+
: consumeValue(args, index, argument);
|
|
263
|
+
if (!argument.includes('=')) index += 1;
|
|
264
|
+
if (parseConfigArgument(raw, server)) continue;
|
|
265
|
+
nativeArgs.push(argument);
|
|
266
|
+
if (!argument.includes('=')) nativeArgs.push(raw);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (argument.startsWith('--config=')) {
|
|
270
|
+
if (parseConfigArgument(argument.slice('--config='.length), server)) continue;
|
|
271
|
+
}
|
|
272
|
+
nativeArgs.push(argument);
|
|
273
|
+
}
|
|
274
|
+
const roots = deduplicate(additionalRoots);
|
|
275
|
+
if (roots.length > MAX_ROOTS) fail('too_many_roots');
|
|
276
|
+
return {
|
|
277
|
+
nativeArgs,
|
|
278
|
+
effectiveCwd,
|
|
279
|
+
profile,
|
|
280
|
+
additionalRoots: roots,
|
|
281
|
+
server,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** @param {string|undefined} profile */
|
|
286
|
+
function validateProfile(profile) {
|
|
287
|
+
if (profile !== undefined && !PROFILE_PATTERN.test(profile)) fail('profile_invalid');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** @param {unknown} value @returns {PermissionEnvelope} */
|
|
291
|
+
function validateEnvelope(value) {
|
|
292
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) fail('envelope_invalid');
|
|
293
|
+
const record = /** @type {JsonObject} */ (value);
|
|
294
|
+
if (Object.keys(record).some((key) => !ENVELOPE_KEYS.has(key))) fail('envelope_invalid');
|
|
295
|
+
if (record.version !== ENVELOPE_VERSION || !Array.isArray(record.writableRoots))
|
|
296
|
+
fail('envelope_invalid');
|
|
297
|
+
const writableRoots = /** @type {unknown[]} */ (record.writableRoots);
|
|
298
|
+
if (
|
|
299
|
+
writableRoots.length > MAX_ROOTS ||
|
|
300
|
+
writableRoots.some(
|
|
301
|
+
(path) =>
|
|
302
|
+
typeof path !== 'string' ||
|
|
303
|
+
path.includes('\0') ||
|
|
304
|
+
!isAbsolute(path) ||
|
|
305
|
+
Buffer.byteLength(path, 'utf8') > MAX_PATH_BYTES,
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
fail('envelope_invalid');
|
|
309
|
+
if (record.profile !== undefined && typeof record.profile !== 'string') fail('envelope_invalid');
|
|
310
|
+
validateProfile(record.profile === undefined ? undefined : String(record.profile));
|
|
311
|
+
const settings = /** @type {Record<string, unknown>} */ (record);
|
|
312
|
+
for (const [key, expected] of /** @type {[string, Set<string>][]} */ ([
|
|
313
|
+
['sandboxMode', SANDBOX_MODES],
|
|
314
|
+
['approvalPolicy', APPROVAL_POLICIES],
|
|
315
|
+
['approvalsReviewer', REVIEWERS],
|
|
316
|
+
['appsDefaultApprovalsReviewer', REVIEWERS],
|
|
317
|
+
])) {
|
|
318
|
+
if (
|
|
319
|
+
settings[key] !== undefined &&
|
|
320
|
+
(typeof settings[key] !== 'string' || !expected.has(settings[key]))
|
|
321
|
+
)
|
|
322
|
+
fail('envelope_invalid');
|
|
323
|
+
}
|
|
324
|
+
if (settings.networkAccess !== undefined && typeof settings.networkAccess !== 'boolean')
|
|
325
|
+
fail('envelope_invalid');
|
|
326
|
+
return /** @type {PermissionEnvelope} */ (
|
|
327
|
+
Object.freeze({
|
|
328
|
+
...record,
|
|
329
|
+
writableRoots: [...deduplicate(writableRoots.filter((path) => typeof path === 'string'))],
|
|
330
|
+
})
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** @param {string|undefined} raw */
|
|
335
|
+
export function parseCodexLaunchEnvelope(raw) {
|
|
336
|
+
if (raw === undefined || raw === '') return null;
|
|
337
|
+
if (Buffer.byteLength(raw, 'utf8') > MAX_ENVELOPE_BYTES) fail('envelope_too_large');
|
|
338
|
+
try {
|
|
339
|
+
return validateEnvelope(JSON.parse(raw));
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (error instanceof Error && error.message.startsWith('codex_permissions_')) throw error;
|
|
342
|
+
fail('envelope_invalid');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** @param {string} value */
|
|
347
|
+
function tomlString(value) {
|
|
348
|
+
return JSON.stringify(value);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** @param {string[]} values */
|
|
352
|
+
function tomlArray(values) {
|
|
353
|
+
return `[${values.map(tomlString).join(',')}]`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** @param {PermissionEnvelope} envelope */
|
|
357
|
+
export function ownedAppServerConfigArgs(envelope) {
|
|
358
|
+
const checked = validateEnvelope(envelope);
|
|
359
|
+
const args = [];
|
|
360
|
+
if (checked.sandboxMode !== undefined)
|
|
361
|
+
args.push('-c', `sandbox_mode=${tomlString(String(checked.sandboxMode))}`);
|
|
362
|
+
if (checked.approvalPolicy !== undefined)
|
|
363
|
+
args.push('-c', `approval_policy=${tomlString(String(checked.approvalPolicy))}`);
|
|
364
|
+
if (checked.approvalsReviewer !== undefined)
|
|
365
|
+
args.push('-c', `approvals_reviewer=${tomlString(String(checked.approvalsReviewer))}`);
|
|
366
|
+
if (checked.appsDefaultApprovalsReviewer !== undefined)
|
|
367
|
+
args.push(
|
|
368
|
+
'-c',
|
|
369
|
+
`apps._default.approvals_reviewer=${tomlString(String(checked.appsDefaultApprovalsReviewer))}`,
|
|
370
|
+
);
|
|
371
|
+
if (checked.networkAccess !== undefined)
|
|
372
|
+
args.push(
|
|
373
|
+
'-c',
|
|
374
|
+
`sandbox_workspace_write.network_access=${checked.networkAccess ? 'true' : 'false'}`,
|
|
375
|
+
);
|
|
376
|
+
if (checked.writableRoots.length > 0)
|
|
377
|
+
args.push('-c', `sandbox_workspace_write.writable_roots=${tomlArray(checked.writableRoots)}`);
|
|
378
|
+
return args;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** @param {NodeJS.ProcessEnv} environment */
|
|
382
|
+
function childEnvironment(environment) {
|
|
383
|
+
const copy = { ...environment };
|
|
384
|
+
delete copy[CODEX_LAUNCH_ENVELOPE];
|
|
385
|
+
delete copy[CODEX_APP_SERVER_PROFILE];
|
|
386
|
+
delete copy.JEV_API_KEY;
|
|
387
|
+
delete copy.TYPESAFE_API_KEY;
|
|
388
|
+
return copy;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** @param {OwnedChild} child @returns {Promise<void>} */
|
|
392
|
+
function stopChild(child) {
|
|
393
|
+
/** @param {NodeJS.Signals} signal */
|
|
394
|
+
const signalGroup = (signal) => {
|
|
395
|
+
try {
|
|
396
|
+
if (process.platform !== 'win32' && child.pid) process.kill(-child.pid, signal);
|
|
397
|
+
else child.kill(signal);
|
|
398
|
+
} catch {
|
|
399
|
+
// The owned process may already have exited; the group still gets a bounded attempt.
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
signalGroup('SIGTERM');
|
|
403
|
+
return new Promise((resolvePromise) => {
|
|
404
|
+
let settled = false;
|
|
405
|
+
const finish = () => {
|
|
406
|
+
if (settled) return;
|
|
407
|
+
settled = true;
|
|
408
|
+
clearTimeout(timer);
|
|
409
|
+
signalGroup('SIGKILL');
|
|
410
|
+
resolvePromise();
|
|
411
|
+
};
|
|
412
|
+
const timer = setTimeout(finish, 500);
|
|
413
|
+
child.once('exit', finish);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** @param {OwnedChild} child @param {string|number} requestId @returns {Promise<JsonRpcResponse>} */
|
|
418
|
+
function waitForResponse(child, requestId) {
|
|
419
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
420
|
+
let settled = false;
|
|
421
|
+
let buffer = Buffer.alloc(0);
|
|
422
|
+
/** @param {Error|null} error @param {JsonRpcResponse} [value] */
|
|
423
|
+
const finish = (error, value = {}) => {
|
|
424
|
+
if (settled) return;
|
|
425
|
+
settled = true;
|
|
426
|
+
clearTimeout(timer);
|
|
427
|
+
child.stdout?.removeListener('data', onData);
|
|
428
|
+
child.stdout?.removeListener('error', onError);
|
|
429
|
+
child.removeListener('exit', onExit);
|
|
430
|
+
child.removeListener('error', onChildError);
|
|
431
|
+
child.stdin?.removeListener('error', onStdinError);
|
|
432
|
+
if (error) rejectPromise(error);
|
|
433
|
+
else resolvePromise(value);
|
|
434
|
+
};
|
|
435
|
+
const onError = () => finish(new Error('codex_permissions_preflight_failed'));
|
|
436
|
+
const onChildError = () => finish(new Error('codex_permissions_preflight_failed'));
|
|
437
|
+
const onStdinError = () => finish(new Error('codex_permissions_preflight_failed'));
|
|
438
|
+
/** @param {number|null} code */
|
|
439
|
+
const onExit = (code) => {
|
|
440
|
+
if (code !== null) finish(new Error('codex_permissions_preflight_failed'));
|
|
441
|
+
};
|
|
442
|
+
/** @param {Buffer|string} chunk */
|
|
443
|
+
const onData = (chunk) => {
|
|
444
|
+
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
|
|
445
|
+
if (buffer.length > PREFLIGHT_FRAME_BYTES) {
|
|
446
|
+
finish(new Error('codex_permissions_preflight_frame_limit'));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
while (true) {
|
|
450
|
+
const newline = buffer.indexOf(10);
|
|
451
|
+
if (newline < 0) return;
|
|
452
|
+
const line = buffer.subarray(0, newline);
|
|
453
|
+
buffer = buffer.subarray(newline + 1);
|
|
454
|
+
let message;
|
|
455
|
+
try {
|
|
456
|
+
message = JSON.parse(line.toString('utf8'));
|
|
457
|
+
} catch {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (message?.id === requestId) {
|
|
461
|
+
finish(null, message);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const timer = setTimeout(
|
|
467
|
+
() => finish(new Error('codex_permissions_preflight_timeout')),
|
|
468
|
+
PREFLIGHT_TIMEOUT_MS,
|
|
469
|
+
);
|
|
470
|
+
child.stdout?.on('data', onData);
|
|
471
|
+
child.stdout?.once('error', onError);
|
|
472
|
+
child.once('exit', onExit);
|
|
473
|
+
child.once('error', onChildError);
|
|
474
|
+
child.stdin?.once('error', onStdinError);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** @param {OwnedChild} child @param {number} id @param {string} method @param {JsonObject} params */
|
|
479
|
+
async function request(child, id, method, params) {
|
|
480
|
+
const stdin = /** @type {import('node:stream').Writable} */ (child.stdin);
|
|
481
|
+
if (stdin.destroyed || child.exitCode !== null) fail('preflight_failed');
|
|
482
|
+
const responsePromise = waitForResponse(child, id);
|
|
483
|
+
try {
|
|
484
|
+
stdin.write(`${JSON.stringify({ id, method, params })}\n`);
|
|
485
|
+
} catch {
|
|
486
|
+
await stopChild(child);
|
|
487
|
+
await responsePromise.catch(() => undefined);
|
|
488
|
+
fail('preflight_failed');
|
|
489
|
+
}
|
|
490
|
+
const message = await responsePromise;
|
|
491
|
+
if (
|
|
492
|
+
message.error !== undefined ||
|
|
493
|
+
message.result === undefined ||
|
|
494
|
+
message.result === null ||
|
|
495
|
+
typeof message.result !== 'object' ||
|
|
496
|
+
Array.isArray(message.result)
|
|
497
|
+
)
|
|
498
|
+
fail('preflight_response_invalid');
|
|
499
|
+
return message.result;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** @param {string} executable @param {string} cwd @param {string|undefined} profile @param {NodeJS.ProcessEnv} environment */
|
|
503
|
+
export async function readEffectiveWritableRoots(executable, cwd, profile, environment) {
|
|
504
|
+
validateProfile(profile);
|
|
505
|
+
const args = [];
|
|
506
|
+
if (profile !== undefined) args.push('--profile', profile);
|
|
507
|
+
args.push('app-server', '--listen', 'stdio://');
|
|
508
|
+
const child = spawn(executable, args, {
|
|
509
|
+
cwd,
|
|
510
|
+
env: childEnvironment(environment),
|
|
511
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
512
|
+
detached: process.platform !== 'win32',
|
|
513
|
+
});
|
|
514
|
+
let cancelled = false;
|
|
515
|
+
const cancel = () => {
|
|
516
|
+
cancelled = true;
|
|
517
|
+
void stopChild(child);
|
|
518
|
+
};
|
|
519
|
+
process.once('SIGINT', cancel);
|
|
520
|
+
process.once('SIGTERM', cancel);
|
|
521
|
+
process.once('SIGHUP', cancel);
|
|
522
|
+
try {
|
|
523
|
+
await request(child, 0, 'initialize', {
|
|
524
|
+
clientInfo: { name: 'aibrain-preflight', version: '1' },
|
|
525
|
+
capabilities: { experimentalApi: true },
|
|
526
|
+
});
|
|
527
|
+
const stdin = /** @type {import('node:stream').Writable} */ (child.stdin);
|
|
528
|
+
if (cancelled) fail('preflight_cancelled');
|
|
529
|
+
try {
|
|
530
|
+
stdin.write(`${JSON.stringify({ method: 'initialized' })}\n`);
|
|
531
|
+
} catch {
|
|
532
|
+
fail('preflight_failed');
|
|
533
|
+
}
|
|
534
|
+
const result = await request(child, 1, 'config/read', { cwd, includeLayers: true });
|
|
535
|
+
const rawConfig = result.config;
|
|
536
|
+
if (
|
|
537
|
+
rawConfig === undefined ||
|
|
538
|
+
rawConfig === null ||
|
|
539
|
+
typeof rawConfig !== 'object' ||
|
|
540
|
+
Array.isArray(rawConfig)
|
|
541
|
+
)
|
|
542
|
+
fail('preflight_roots_unavailable');
|
|
543
|
+
const config = /** @type {JsonObject} */ (rawConfig);
|
|
544
|
+
const rawSandbox = config.sandbox_workspace_write;
|
|
545
|
+
if (cancelled) fail('preflight_cancelled');
|
|
546
|
+
if (rawSandbox === undefined || rawSandbox === null) return [];
|
|
547
|
+
if (typeof rawSandbox !== 'object' || Array.isArray(rawSandbox))
|
|
548
|
+
fail('preflight_roots_unavailable');
|
|
549
|
+
const sandbox = /** @type {JsonObject} */ (rawSandbox);
|
|
550
|
+
const roots = sandbox.writable_roots;
|
|
551
|
+
if (cancelled) fail('preflight_cancelled');
|
|
552
|
+
if (roots === undefined || roots === null) return [];
|
|
553
|
+
if (
|
|
554
|
+
!Array.isArray(roots) ||
|
|
555
|
+
roots.some((path) => typeof path !== 'string' || !isAbsolute(path))
|
|
556
|
+
)
|
|
557
|
+
fail('preflight_roots_unavailable');
|
|
558
|
+
if (cancelled) fail('preflight_cancelled');
|
|
559
|
+
return deduplicate(roots);
|
|
560
|
+
} finally {
|
|
561
|
+
process.removeListener('SIGINT', cancel);
|
|
562
|
+
process.removeListener('SIGTERM', cancel);
|
|
563
|
+
process.removeListener('SIGHUP', cancel);
|
|
564
|
+
await stopChild(child);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** @param {LaunchPlan} plan @param {string[]} configuredRoots */
|
|
569
|
+
export function makeCodexLaunchEnvelope(plan, configuredRoots) {
|
|
570
|
+
const roots = deduplicate([...configuredRoots, ...plan.additionalRoots]);
|
|
571
|
+
if (roots.length > MAX_ROOTS) fail('too_many_roots');
|
|
572
|
+
const envelope = {
|
|
573
|
+
version: ENVELOPE_VERSION,
|
|
574
|
+
writableRoots: roots,
|
|
575
|
+
profile: plan.profile,
|
|
576
|
+
...plan.server,
|
|
577
|
+
};
|
|
578
|
+
const checked = validateEnvelope(envelope);
|
|
579
|
+
if (Buffer.byteLength(JSON.stringify(checked), 'utf8') > MAX_ENVELOPE_BYTES)
|
|
580
|
+
fail('envelope_too_large');
|
|
581
|
+
return checked;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export const codexPermissionInternals = {
|
|
585
|
+
MAX_ROOTS,
|
|
586
|
+
MAX_PATH_BYTES,
|
|
587
|
+
MAX_ENVELOPE_BYTES,
|
|
588
|
+
CONFIG_KEYS,
|
|
589
|
+
validateEnvelope,
|
|
590
|
+
};
|