@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/dist/launch.mjs CHANGED
@@ -1,11 +1,24 @@
1
1
  // @ts-check
2
2
 
3
- import { access, chmod, lstat, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { access, chmod, lstat, mkdir, mkdtemp, open, rm, writeFile } from 'node:fs/promises';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { constants } from 'node:fs';
6
6
  import { isAbsolute, join, resolve, sep } from 'node:path';
7
7
  import { assertTestedNativeVersion, nativeVersion, resolveNative } from './platform.mjs';
8
+ import { codexCompatibilityError, resolveCodexCompatibility } from './codex-compatibility.mjs';
9
+ import {
10
+ claudeCheckpointConfigBinding,
11
+ claudeObservationConfigBinding,
12
+ telemetryConfigBinding,
13
+ } from './state.mjs';
8
14
  import { codexPolicyJson, makePolicy } from './policy.mjs';
15
+ import {
16
+ CODEX_LAUNCH_ENVELOPE,
17
+ CODEX_MONITOR_CWD,
18
+ makeCodexLaunchEnvelope,
19
+ parseCodexLaunchArgs,
20
+ readEffectiveWritableRoots,
21
+ } from './codex-permissions.mjs';
9
22
 
10
23
  const MCP_ALIAS = 'aibrain_context';
11
24
  const PLUGIN_NAME = 'ai-brain-context-compaction';
@@ -17,12 +30,16 @@ const CLAUDE_HEADLESS_FLAGS = new Set([
17
30
  '--input-format',
18
31
  '--output-style',
19
32
  ]);
20
- const RESERVED_CLAUDE_FLAGS = new Set([
21
- '--settings',
22
- '--mcp-config',
23
- '--plugin-dir',
24
- '--allowedTools',
25
- ]);
33
+ const RESERVED_CLAUDE_FLAGS = new Set(['--mcp-config', '--plugin-dir', '--allowedTools']);
34
+ const MAX_CLAUDE_SETTINGS_BYTES = 64 * 1024;
35
+ const MAX_CLAUDE_SETTINGS_DEPTH = 12;
36
+ const MAX_CLAUDE_SETTINGS_NODES = 512;
37
+ const MAX_CLAUDE_SETTINGS_STRING_LENGTH = 16 * 1024;
38
+ const MAX_CLAUDE_PERMISSION_LIST_ITEMS = 256;
39
+ const MAX_CLAUDE_PERMISSION_STRING_LENGTH = 4096;
40
+ const CLAUDE_SETTINGS_SECTIONS = new Set(['sandbox', 'permissions']);
41
+ const CLAUDE_PERMISSION_LISTS = new Set(['allow', 'ask', 'deny', 'additionalDirectories']);
42
+ const CLAUDE_SETTINGS_FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
26
43
 
27
44
  /**
28
45
  * @typedef {object} LaunchConfig
@@ -33,6 +50,8 @@ const RESERVED_CLAUDE_FLAGS = new Set([
33
50
  * @property {string|null} codexVersion
34
51
  * @property {number} schemaVersion
35
52
  * @property {import('./policy.mjs').ContextPolicy} [context]
53
+ * @property {{ enabled: boolean, disclosedAt?: string | null }} [telemetry]
54
+ * @property {{ shadowEnabled: boolean, diagnosticsEnabled: boolean, disclosedAt?: string | null }} [claudeObservation]
36
55
  */
37
56
 
38
57
  /** @param {LaunchConfig} config */
@@ -105,6 +124,8 @@ function childEnvironment(root) {
105
124
  delete environment.AIBRAIN_TOKEN_FILE;
106
125
  delete environment.CODEX_CONTEXT_POLICY_JSON;
107
126
  delete environment.CODEX_CONTEXT_COMPACT_AT;
127
+ delete environment[CODEX_LAUNCH_ENVELOPE];
128
+ delete environment[CODEX_MONITOR_CWD];
108
129
  environment.AIBRAIN_STATE_DIR = resolve(root);
109
130
  return environment;
110
131
  }
@@ -112,6 +133,7 @@ function childEnvironment(root) {
112
133
  /** @param {string[]} args */
113
134
  function assertClaudeArgs(args) {
114
135
  for (const argument of args) {
136
+ if (argument === '--') break;
115
137
  const option = argument.split('=', 1)[0];
116
138
  if (CLAUDE_HEADLESS_FLAGS.has(option) || /^-p(?:=|[^-])/u.test(argument))
117
139
  throw new Error('claude_headless_mode_unsupported');
@@ -119,6 +141,180 @@ function assertClaudeArgs(args) {
119
141
  }
120
142
  }
121
143
 
144
+ /** @param {unknown} value @returns {value is Record<string, unknown>} */
145
+ function plainObject(value) {
146
+ return (
147
+ value !== null &&
148
+ typeof value === 'object' &&
149
+ !Array.isArray(value) &&
150
+ Object.getPrototypeOf(value) === Object.prototype
151
+ );
152
+ }
153
+
154
+ /** @param {unknown} value */
155
+ function validateClaudeSettingsValue(value, depth = 0, state = { nodes: 0 }) {
156
+ state.nodes += 1;
157
+ if (state.nodes > MAX_CLAUDE_SETTINGS_NODES || depth > MAX_CLAUDE_SETTINGS_DEPTH)
158
+ throw new Error('claude_settings_structure_invalid');
159
+ if (typeof value === 'string') {
160
+ if (value.length > MAX_CLAUDE_SETTINGS_STRING_LENGTH)
161
+ throw new Error('claude_settings_string_too_long');
162
+ return;
163
+ }
164
+ if (typeof value === 'number') {
165
+ if (!Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER)
166
+ throw new Error('claude_settings_value_invalid');
167
+ return;
168
+ }
169
+ if (value === null || typeof value === 'boolean') return;
170
+ if (Array.isArray(value)) {
171
+ if (value.length > MAX_CLAUDE_SETTINGS_NODES)
172
+ throw new Error('claude_settings_structure_invalid');
173
+ for (const item of value) validateClaudeSettingsValue(item, depth + 1, state);
174
+ return;
175
+ }
176
+ if (!plainObject(value)) throw new Error('claude_settings_value_invalid');
177
+ for (const [key, item] of Object.entries(value)) {
178
+ if (CLAUDE_SETTINGS_FORBIDDEN_KEYS.has(key)) throw new Error('claude_settings_prototype_key');
179
+ if (key.length > 256) throw new Error('claude_settings_key_too_long');
180
+ validateClaudeSettingsValue(item, depth + 1, state);
181
+ }
182
+ }
183
+
184
+ /** @param {unknown} value */
185
+ function validateClaudeSettingsFragment(value) {
186
+ if (!plainObject(value)) throw new Error('claude_settings_root_invalid');
187
+ validateClaudeSettingsValue(value);
188
+ for (const key of Object.keys(value)) {
189
+ if (!CLAUDE_SETTINGS_SECTIONS.has(key)) throw new Error('claude_settings_section_unsupported');
190
+ const section = value[key];
191
+ if (!plainObject(section)) throw new Error('claude_settings_section_invalid');
192
+ }
193
+ const permissions = value.permissions;
194
+ if (permissions !== undefined) {
195
+ const permissionObject = /** @type {Record<string, unknown>} */ (permissions);
196
+ for (const key of CLAUDE_PERMISSION_LISTS) {
197
+ const list = permissionObject[key];
198
+ if (list === undefined) continue;
199
+ if (
200
+ !Array.isArray(list) ||
201
+ list.length > MAX_CLAUDE_PERMISSION_LIST_ITEMS ||
202
+ list.some(
203
+ (item) => typeof item !== 'string' || item.length > MAX_CLAUDE_PERMISSION_STRING_LENGTH,
204
+ )
205
+ )
206
+ throw new Error('claude_settings_permission_list_invalid');
207
+ }
208
+ }
209
+ return /** @type {{sandbox?: Record<string, unknown>, permissions?: Record<string, unknown>}} */ (
210
+ value
211
+ );
212
+ }
213
+
214
+ /** @param {string} text */
215
+ function parseClaudeSettingsText(text) {
216
+ const bytes = Buffer.byteLength(text, 'utf8');
217
+ if (bytes > MAX_CLAUDE_SETTINGS_BYTES) throw new Error('claude_settings_too_large');
218
+ let value;
219
+ try {
220
+ value = JSON.parse(text);
221
+ } catch {
222
+ throw new Error('claude_settings_invalid_json');
223
+ }
224
+ return validateClaudeSettingsFragment(value);
225
+ }
226
+
227
+ /** @param {string} path */
228
+ async function readClaudeSettingsFile(path) {
229
+ let handle;
230
+ try {
231
+ const link = await lstat(path);
232
+ if (link.isSymbolicLink() || !link.isFile()) throw new Error('claude_settings_file_invalid');
233
+ handle = await open(
234
+ path,
235
+ constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0),
236
+ );
237
+ const entry = await handle.stat();
238
+ if (!entry.isFile()) throw new Error('claude_settings_file_invalid');
239
+ if (entry.size > MAX_CLAUDE_SETTINGS_BYTES) throw new Error('claude_settings_too_large');
240
+ const buffer = Buffer.alloc(MAX_CLAUDE_SETTINGS_BYTES + 1);
241
+ let offset = 0;
242
+ while (offset < buffer.length) {
243
+ const result = await handle.read(buffer, offset, buffer.length - offset, null);
244
+ if (result.bytesRead === 0) break;
245
+ offset += result.bytesRead;
246
+ }
247
+ if (offset > MAX_CLAUDE_SETTINGS_BYTES) throw new Error('claude_settings_too_large');
248
+ let text;
249
+ try {
250
+ text = new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(0, offset));
251
+ } catch {
252
+ throw new Error('claude_settings_invalid_utf8');
253
+ }
254
+ return parseClaudeSettingsText(text);
255
+ } catch (error) {
256
+ if (error instanceof Error && error.message.startsWith('claude_settings_')) throw error;
257
+ throw new Error('claude_settings_file_invalid');
258
+ } finally {
259
+ await handle?.close().catch(() => undefined);
260
+ }
261
+ }
262
+
263
+ /** @param {string} value @param {string} cwd */
264
+ async function readClaudeSettings(value, cwd) {
265
+ const first = value.trimStart()[0];
266
+ const explicitPath = isAbsolute(value) || value.startsWith('./') || value.startsWith('../');
267
+ if (Buffer.byteLength(value, 'utf8') > MAX_CLAUDE_SETTINGS_BYTES)
268
+ throw new Error('claude_settings_too_large');
269
+ try {
270
+ return validateClaudeSettingsFragment(JSON.parse(value));
271
+ } catch (error) {
272
+ if (error instanceof Error && error.message.startsWith('claude_settings_')) throw error;
273
+ if (!explicitPath && (first === '{' || first === '[' || first === '"' || first === '-'))
274
+ throw new Error('claude_settings_invalid_json');
275
+ }
276
+ return readClaudeSettingsFile(resolve(cwd, value));
277
+ }
278
+
279
+ /**
280
+ * @param {string[]} args
281
+ * @param {string} cwd
282
+ * @returns {Promise<{args: string[], settings?: {sandbox?: Record<string, unknown>, permissions?: Record<string, unknown>}}>}
283
+ */
284
+ async function parseClaudeLaunchArgs(args, cwd) {
285
+ const forwarded = [];
286
+ let settingsValue;
287
+ let terminated = false;
288
+ for (let index = 0; index < args.length; index += 1) {
289
+ const argument = args[index];
290
+ if (terminated) {
291
+ forwarded.push(argument);
292
+ continue;
293
+ }
294
+ if (argument === '--') {
295
+ terminated = true;
296
+ forwarded.push(argument);
297
+ continue;
298
+ }
299
+ if (argument === '--settings' || argument.startsWith('--settings=')) {
300
+ if (settingsValue !== undefined) throw new Error('claude_settings_duplicate');
301
+ const value =
302
+ argument === '--settings' ? args[++index] : argument.slice('--settings='.length);
303
+ if (value === undefined || value === '' || value.startsWith('-'))
304
+ throw new Error('claude_settings_value_required');
305
+ settingsValue = value;
306
+ continue;
307
+ }
308
+ forwarded.push(argument);
309
+ }
310
+ return {
311
+ args: forwarded,
312
+ ...(settingsValue === undefined
313
+ ? {}
314
+ : { settings: await readClaudeSettings(settingsValue, cwd) }),
315
+ };
316
+ }
317
+
122
318
  /** @param {string[]} args */
123
319
  function assertCodexArgs(args) {
124
320
  for (const argument of args) {
@@ -135,11 +331,35 @@ async function assertClaudeAssets(packageRoot) {
135
331
  join(pluginRoot, '.claude-plugin', 'plugin.json'),
136
332
  join(pluginRoot, 'hooks', 'hooks.json'),
137
333
  join(pluginRoot, 'hooks', 'context-compaction.ts'),
334
+ join(pluginRoot, 'hooks', 'provider-window.js'),
335
+ join(pluginRoot, 'hooks', 'state-fitting.mjs'),
336
+ join(pluginRoot, 'hooks', 'state-fitting.d.mts'),
138
337
  ];
139
338
  await Promise.all(required.map((path) => access(path, constants.R_OK)));
140
339
  return pluginRoot;
141
340
  }
142
341
 
342
+ /** @param {string} packageRoot */
343
+ async function assertTelemetryHelper(packageRoot) {
344
+ const helperPath = join(packageRoot, 'telemetry-report.mjs');
345
+ await access(helperPath, constants.R_OK);
346
+ return helperPath;
347
+ }
348
+
349
+ /** @param {string} packageRoot */
350
+ async function assertClaudeObservationHelper(packageRoot) {
351
+ const helperPath = join(packageRoot, 'claude-observation.mjs');
352
+ await access(helperPath, constants.R_OK);
353
+ return helperPath;
354
+ }
355
+
356
+ /** @param {string} packageRoot */
357
+ async function assertClaudeCheckpointHelper(packageRoot) {
358
+ const helperPath = join(packageRoot, 'claude-checkpoint.mjs');
359
+ await access(helperPath, constants.R_OK);
360
+ return helperPath;
361
+ }
362
+
143
363
  /** @param {string} root @param {string} prefix */
144
364
  async function launchDirectory(root, prefix) {
145
365
  const parent = await privateDirectory(join(root, 'runtime'));
@@ -183,12 +403,31 @@ function waitForChild(child, directory) {
183
403
  /** @param {LaunchConfig} config @param {string} root @param {string[]} args */
184
404
  export async function launchClaude(config, root, args) {
185
405
  const validated = assertConfig(config);
186
- assertClaudeArgs(args);
406
+ const launchArgs = await parseClaudeLaunchArgs(args, process.cwd());
407
+ assertClaudeArgs(launchArgs.args);
187
408
  const executable = resolveNative('claude');
188
409
  if (!executable) throw new Error('claude_not_found');
189
410
  const currentVersion = nativeVersion(executable);
190
411
  assertTestedNativeVersion('claude', currentVersion);
191
412
  const pluginRoot = await assertClaudeAssets(validated.packageRoot);
413
+ const telemetryActive =
414
+ validated.telemetry?.enabled === true && validated.telemetry?.disclosedAt != null;
415
+ const telemetryHelperPath = telemetryActive
416
+ ? await assertTelemetryHelper(validated.packageRoot)
417
+ : undefined;
418
+ const telemetryConfigDigest = telemetryActive ? telemetryConfigBinding(validated) : undefined;
419
+ const observationActive =
420
+ validated.claudeObservation?.shadowEnabled === true ||
421
+ validated.claudeObservation?.diagnosticsEnabled === true;
422
+ const observationHelperPath = observationActive
423
+ ? await assertClaudeObservationHelper(validated.packageRoot)
424
+ : undefined;
425
+ const observationConfigDigest = observationActive
426
+ ? claudeObservationConfigBinding(validated)
427
+ : undefined;
428
+ const checkpointHelperPath = await assertClaudeCheckpointHelper(validated.packageRoot);
429
+ const checkpointConfigDigest = claudeCheckpointConfigBinding(validated);
430
+ const checkpointStateDir = await privateDirectory(join(root, 'claude', 'checkpoints'));
192
431
  const directory = await launchDirectory(root, 'claude');
193
432
  const settingsPath = join(directory, 'settings.json');
194
433
  const mcpPath = join(directory, 'mcp.json');
@@ -213,21 +452,63 @@ export async function launchClaude(config, root, args) {
213
452
  }),
214
453
  enabled: true,
215
454
  disclosed: true,
455
+ compactionStrategy: 'native_summary_notes',
216
456
  preservationMode: 'upstream_compatible',
217
457
  diagnosticMode: false,
458
+ telemetryEnabled: validated.telemetry?.enabled === true,
459
+ telemetryDisclosed:
460
+ validated.telemetry?.enabled === true && validated.telemetry?.disclosedAt != null,
461
+ telemetryClientVersion: currentVersion.match(/\b\d+\.\d+\.\d+\b/u)?.[0] ?? '',
462
+ ...(telemetryActive
463
+ ? {
464
+ telemetryNodePath: process.execPath,
465
+ telemetryHelperPath,
466
+ telemetryStateDir: resolve(root),
467
+ telemetryConfigDigest,
468
+ }
469
+ : {}),
470
+ ...(observationActive
471
+ ? {
472
+ observationNodePath: process.execPath,
473
+ observationHelperPath,
474
+ observationStateDir: resolve(root),
475
+ observationConfigDigest,
476
+ observationShadowEnabled: validated.claudeObservation?.shadowEnabled === true,
477
+ observationDiagnosticsEnabled: validated.claudeObservation?.diagnosticsEnabled === true,
478
+ }
479
+ : {}),
480
+ checkpointNodePath: process.execPath,
481
+ checkpointHelperPath,
482
+ checkpointStateDir,
483
+ checkpointConfigDigest,
484
+ checkpointBinding: checkpointConfigDigest,
218
485
  };
219
486
  if (!legacy && context.goal !== undefined) options.goal = context.goal;
487
+ const requiredAllow = [
488
+ `mcp__${MCP_ALIAS}__brain_context_status`,
489
+ `mcp__${MCP_ALIAS}__brain_context_compact`,
490
+ `mcp__${MCP_ALIAS}__brain_context_select`,
491
+ `mcp__${MCP_ALIAS}__brain_context_budget`,
492
+ ...(options.telemetryEnabled && options.telemetryDisclosed
493
+ ? [`mcp__${MCP_ALIAS}__brain_context_report`]
494
+ : []),
495
+ ];
496
+ const userPermissions = launchArgs.settings?.permissions ?? {};
497
+ const userAllow = Array.isArray(userPermissions.allow) ? userPermissions.allow : [];
498
+ /** @type {string[]} */
499
+ const mergedAllow = [];
500
+ for (const rule of [...userAllow, ...requiredAllow]) {
501
+ if (!mergedAllow.includes(rule)) mergedAllow.push(rule);
502
+ }
220
503
  const settings = {
504
+ ...(launchArgs.settings?.sandbox === undefined ? {} : { sandbox: launchArgs.settings.sandbox }),
221
505
  pluginConfigs: {
222
506
  [PLUGIN_NAME]: { options },
223
507
  [PLUGIN_INLINE_NAME]: { options: { ...options } },
224
508
  },
225
509
  permissions: {
226
- allow: [
227
- `mcp__${MCP_ALIAS}__brain_context_status`,
228
- `mcp__${MCP_ALIAS}__brain_context_compact`,
229
- `mcp__${MCP_ALIAS}__brain_context_select`,
230
- ],
510
+ ...userPermissions,
511
+ allow: mergedAllow,
231
512
  },
232
513
  };
233
514
  const mcp = {
@@ -259,10 +540,10 @@ export async function launchClaude(config, root, args) {
259
540
  '--mcp-config',
260
541
  mcpPath,
261
542
  '--allowedTools',
262
- `mcp__${MCP_ALIAS}__brain_context_status,mcp__${MCP_ALIAS}__brain_context_compact,mcp__${MCP_ALIAS}__brain_context_select`,
543
+ requiredAllow.join(','),
263
544
  '--plugin-dir',
264
545
  pluginRoot,
265
- ...args,
546
+ ...launchArgs.args,
266
547
  ],
267
548
  { cwd: process.cwd(), env: environment, stdio: 'inherit' },
268
549
  );
@@ -280,34 +561,72 @@ export async function launchCodex(config, root, args) {
280
561
  const executable = resolveNative('codex');
281
562
  const node = resolveNative('node');
282
563
  if (!executable || !node) throw new Error('codex_or_node_not_found');
283
- const currentVersion = nativeVersion(executable);
284
- assertTestedNativeVersion('codex', currentVersion);
285
- const currentSemver = currentVersion.match(/\b\d+\.\d+\.\d+\b/u)?.[0];
286
- if (currentSemver === undefined) throw new Error('codex_version_unverified');
564
+ const compatibility = await resolveCodexCompatibility(executable);
565
+ if (compatibility.status === 'rejected') throw new Error(codexCompatibilityError(compatibility));
566
+ if (compatibility.version === null) throw new Error('codex_version_unverified');
567
+ if (compatibility.status === 'protocol_match')
568
+ process.stderr.write(
569
+ `aibrain: Codex ${compatibility.version} matched the local protocol contract; native behavioral qualification is not established.\n`,
570
+ );
571
+ const launchPlan = await parseCodexLaunchArgs(args, process.cwd());
287
572
  const packageRoot = resolve(validated.packageRoot);
288
573
  const terminal = join(packageRoot, 'assets', 'jev-paid-pilot-terminal.mjs');
289
574
  const monitor = join(packageRoot, 'monitor.mjs');
290
- await Promise.all([access(terminal, constants.R_OK), access(monitor, constants.X_OK)]);
575
+ const diagnosticModule = join(packageRoot, 'codex-diagnostics.mjs');
576
+ await Promise.all([
577
+ access(terminal, constants.R_OK),
578
+ access(monitor, constants.X_OK),
579
+ access(diagnosticModule, constants.R_OK),
580
+ ]);
291
581
  const checkpoints = await privateDirectory(join(root, 'codex', 'checkpoints'));
292
582
  const notes = await privateDirectory(join(root, 'codex', 'notes'));
293
583
  const directory = await launchDirectory(root, 'codex');
294
584
  const environment = childEnvironment(root);
585
+ try {
586
+ if (
587
+ launchPlan.additionalRoots.length > 0 ||
588
+ Object.keys(launchPlan.server).length > 0 ||
589
+ launchPlan.profile !== undefined
590
+ ) {
591
+ const configuredRoots =
592
+ launchPlan.additionalRoots.length > 0
593
+ ? await readEffectiveWritableRoots(
594
+ executable,
595
+ launchPlan.effectiveCwd,
596
+ launchPlan.profile,
597
+ environment,
598
+ )
599
+ : [];
600
+ const envelope = makeCodexLaunchEnvelope(launchPlan, configuredRoots);
601
+ environment[CODEX_LAUNCH_ENVELOPE] = JSON.stringify(envelope);
602
+ }
603
+ } catch (error) {
604
+ await rm(directory, { recursive: true, force: true });
605
+ throw error;
606
+ }
295
607
  environment.AI_BRAIN_MCP_SERVER = MCP_ALIAS;
296
608
  environment.CODEX_BIN = executable;
297
609
  environment.CODEX_MONITOR_COMMAND = monitor;
610
+ environment[CODEX_MONITOR_CWD] = launchPlan.effectiveCwd;
298
611
  environment.CODEX_CONTEXT_RECOVERY_ENABLED = '1';
299
612
  environment.CODEX_CONTEXT_RECOVERY_DISCLOSED = '1';
300
613
  environment.CODEX_CONTEXT_CHECKPOINT_DIR = checkpoints;
301
614
  environment.CODEX_CONTEXT_RECOVERY_DIR = notes;
615
+ environment.CODEX_CONTEXT_DIAGNOSTICS_MODULE = diagnosticModule;
616
+ environment.CODEX_CONTEXT_DIAGNOSTICS_DIR = resolve(root);
617
+ environment.CODEX_CONTEXT_DIAGNOSTICS_ENABLED = '1';
302
618
  environment.AIBRAIN_MCP_URL = validated.endpoint;
303
619
  environment.AIBRAIN_MCP_TOKEN = validated.token;
304
- environment.CODEX_CLIENT_VERSION = currentSemver;
620
+ environment.CODEX_CLIENT_VERSION = compatibility.version;
621
+ environment.CODEX_CONTEXT_TELEMETRY_ENABLED = validated.telemetry?.enabled === true ? '1' : '0';
622
+ environment.CODEX_CONTEXT_TELEMETRY_DISCLOSED =
623
+ validated.telemetry?.enabled === true && validated.telemetry?.disclosedAt != null ? '1' : '0';
305
624
  if (validated.schemaVersion === 2 && validated.context !== undefined) {
306
625
  environment.CODEX_CONTEXT_POLICY_JSON = codexPolicyJson(validated.context);
307
626
  } else {
308
627
  delete environment.CODEX_CONTEXT_POLICY_JSON;
309
628
  }
310
- const child = spawn(node, [terminal, '--', ...args], {
629
+ const child = spawn(node, [terminal, '--', ...launchPlan.nativeArgs], {
311
630
  cwd: process.cwd(),
312
631
  env: environment,
313
632
  stdio: 'inherit',