@kbediako/codex-orchestrator 0.1.3 → 0.1.4

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 (28) hide show
  1. package/README.md +6 -1
  2. package/dist/bin/codex-orchestrator.js +38 -0
  3. package/dist/orchestrator/src/cli/config/delegationConfig.js +485 -0
  4. package/dist/orchestrator/src/cli/control/confirmations.js +262 -0
  5. package/dist/orchestrator/src/cli/control/controlServer.js +1476 -0
  6. package/dist/orchestrator/src/cli/control/controlState.js +46 -0
  7. package/dist/orchestrator/src/cli/control/controlWatcher.js +222 -0
  8. package/dist/orchestrator/src/cli/control/delegationTokens.js +62 -0
  9. package/dist/orchestrator/src/cli/control/questions.js +106 -0
  10. package/dist/orchestrator/src/cli/delegationServer.js +1368 -0
  11. package/dist/orchestrator/src/cli/events/runEventStream.js +246 -0
  12. package/dist/orchestrator/src/cli/exec/context.js +4 -1
  13. package/dist/orchestrator/src/cli/exec/stageRunner.js +30 -5
  14. package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +377 -147
  15. package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +3 -5
  16. package/dist/orchestrator/src/cli/orchestrator.js +217 -40
  17. package/dist/orchestrator/src/cli/rlmRunner.js +26 -3
  18. package/dist/orchestrator/src/cli/run/manifestPersister.js +33 -3
  19. package/dist/orchestrator/src/cli/run/runPaths.js +14 -0
  20. package/dist/orchestrator/src/cli/services/commandRunner.js +1 -1
  21. package/dist/orchestrator/src/cli/utils/devtools.js +33 -2
  22. package/dist/orchestrator/src/persistence/ExperienceStore.js +113 -46
  23. package/dist/orchestrator/src/persistence/PersistenceCoordinator.js +8 -8
  24. package/dist/orchestrator/src/persistence/TaskStateStore.js +2 -1
  25. package/dist/orchestrator/src/persistence/lockFile.js +26 -1
  26. package/dist/orchestrator/src/sync/CloudSyncWorker.js +17 -4
  27. package/dist/packages/orchestrator/src/telemetry/otel-exporter.js +21 -0
  28. package/package.json +3 -1
@@ -0,0 +1,1368 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { realpathSync } from 'node:fs';
4
+ import { chmod, readFile } from 'node:fs/promises';
5
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path';
6
+ import process from 'node:process';
7
+ import { loadDelegationConfigFiles, computeEffectiveDelegationConfig, parseDelegationConfigOverride, splitDelegationConfigOverrides } from './config/delegationConfig.js';
8
+ import { logger } from '../logger.js';
9
+ import { writeJsonAtomic } from './utils/fs.js';
10
+ const PROTOCOL_VERSION = '2024-11-05';
11
+ const QUESTION_POLL_INTERVAL_MS = 500;
12
+ const MAX_QUESTION_POLL_WAIT_MS = 10_000;
13
+ const DEFAULT_SPAWN_TIMEOUT_MS = 5 * 60 * 1000;
14
+ const DEFAULT_GH_TIMEOUT_MS = 60_000;
15
+ const DEFAULT_DELEGATION_TOKEN_RETRY_MS = 2000;
16
+ const DEFAULT_DELEGATION_TOKEN_RETRY_INTERVAL_MS = 200;
17
+ const DEFAULT_CONTROL_ENDPOINT_TIMEOUT_MS = 15_000;
18
+ const MAX_MCP_MESSAGE_BYTES = 1024 * 1024;
19
+ const MAX_MCP_HEADER_BYTES = 16 * 1024;
20
+ const MCP_HEADER_DELIMITER = '\r\n\r\n';
21
+ const MCP_HEADER_DELIMITER_BYTES = MCP_HEADER_DELIMITER.length;
22
+ const MCP_HEADER_DELIMITER_BUFFER = Buffer.from(MCP_HEADER_DELIMITER, 'utf8');
23
+ const MAX_MCP_BUFFER_BYTES = (MAX_MCP_MESSAGE_BYTES + MAX_MCP_HEADER_BYTES + MCP_HEADER_DELIMITER_BYTES) * 2;
24
+ const DELEGATION_TOKEN_HEADER = 'x-codex-delegation-token';
25
+ const DELEGATION_RUN_HEADER = 'x-codex-delegation-run-id';
26
+ const DELEGATION_TOKEN_FILE = 'delegation_token.json';
27
+ const CSRF_HEADER = 'x-csrf-token';
28
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
29
+ const CONFIG_OVERRIDE_ENV_KEYS = ['CODEX_CONFIG_OVERRIDES', 'CODEX_MCP_CONFIG_OVERRIDES'];
30
+ const CONFIRMATION_ERROR_CODES = new Set([
31
+ 'confirmation_required',
32
+ 'missing_confirm_nonce',
33
+ 'confirmation_invalid',
34
+ 'confirmation_scope_mismatch',
35
+ 'confirmation_request_not_found',
36
+ 'confirmation_not_approved',
37
+ 'confirmation_expired',
38
+ 'nonce_already_consumed'
39
+ ]);
40
+ const TOOL_PROFILE_ENTRY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
41
+ export async function startDelegationServer(options) {
42
+ const repoRoot = resolve(options.repoRoot);
43
+ const configFiles = await loadDelegationConfigFiles({ repoRoot });
44
+ const envOverrides = collectConfigOverridesFromEnv();
45
+ const overrideLayers = buildConfigOverrideLayers([...envOverrides, ...(options.configOverrides ?? [])]);
46
+ const layers = [configFiles.global, configFiles.repo, ...overrideLayers].filter(Boolean);
47
+ const effectiveConfig = computeEffectiveDelegationConfig({ repoRoot, layers });
48
+ const mode = options.mode ?? effectiveConfig.delegate.mode ?? 'full';
49
+ const allowNested = effectiveConfig.delegate.allowNested ?? false;
50
+ const githubEnabled = effectiveConfig.github.enabled;
51
+ const allowedGithubOps = new Set(effectiveConfig.github.operations);
52
+ const allowedRoots = effectiveConfig.paths.allowedRoots;
53
+ const allowedHosts = effectiveConfig.ui.allowedBindHosts;
54
+ const toolProfile = effectiveConfig.delegate.toolProfile;
55
+ const tools = buildToolList({ mode, githubEnabled, allowedGithubOps });
56
+ const handler = async (request) => {
57
+ switch (request.method) {
58
+ case 'initialize':
59
+ return {
60
+ protocolVersion: PROTOCOL_VERSION,
61
+ serverInfo: { name: 'codex-delegation', version: '0.1.0' },
62
+ capabilities: { tools: {} }
63
+ };
64
+ case 'tools/list':
65
+ return { tools };
66
+ case 'tools/call':
67
+ return await handleToolCall(request, {
68
+ repoRoot,
69
+ mode,
70
+ allowNested,
71
+ githubEnabled,
72
+ allowedGithubOps,
73
+ allowedRoots,
74
+ allowedHosts,
75
+ toolProfile,
76
+ expiryFallback: effectiveConfig.delegate.expiryFallback
77
+ });
78
+ default:
79
+ throw new Error(`Unsupported method: ${request.method}`);
80
+ }
81
+ };
82
+ await runJsonRpcServer(handler);
83
+ }
84
+ function buildToolList(options) {
85
+ const tools = [];
86
+ const includeFull = options.mode !== 'question_only';
87
+ if (includeFull) {
88
+ tools.push(toolDefinition('delegate.spawn', 'Spawn a delegated run', {
89
+ type: 'object',
90
+ properties: {
91
+ task_id: { type: 'string' },
92
+ pipeline: { type: 'string' },
93
+ repo: { type: 'string' },
94
+ parent_run_id: { type: 'string' },
95
+ parent_manifest_path: { type: 'string' },
96
+ env: { type: 'object', additionalProperties: { type: 'string' } },
97
+ delegate_mode: { type: 'string', enum: ['full', 'question_only'] }
98
+ },
99
+ required: ['pipeline', 'repo']
100
+ }));
101
+ tools.push(toolDefinition('delegate.pause', 'Pause or resume a run', {
102
+ type: 'object',
103
+ properties: {
104
+ manifest_path: { type: 'string' },
105
+ paused: { type: 'boolean' }
106
+ },
107
+ required: ['manifest_path', 'paused']
108
+ }));
109
+ tools.push(toolDefinition('delegate.cancel', 'Cancel a run (confirmation required)', {
110
+ type: 'object',
111
+ properties: {
112
+ manifest_path: { type: 'string' }
113
+ },
114
+ required: ['manifest_path']
115
+ }));
116
+ }
117
+ tools.push(toolDefinition('delegate.status', 'Fetch run status', {
118
+ type: 'object',
119
+ properties: {
120
+ manifest_path: { type: 'string' }
121
+ },
122
+ required: ['manifest_path']
123
+ }));
124
+ tools.push(toolDefinition('delegate.question.enqueue', 'Enqueue a question to the parent run', {
125
+ type: 'object',
126
+ properties: {
127
+ parent_manifest_path: { type: 'string' },
128
+ parent_run_id: { type: 'string' },
129
+ parent_task_id: { type: 'string' },
130
+ from_manifest_path: { type: 'string' },
131
+ prompt: { type: 'string' },
132
+ urgency: { type: 'string', enum: ['low', 'med', 'high'] },
133
+ expires_in_ms: { type: 'number' },
134
+ auto_pause: { type: 'boolean' }
135
+ },
136
+ required: ['parent_manifest_path', 'prompt']
137
+ }));
138
+ tools.push(toolDefinition('delegate.question.poll', 'Poll for a question answer', {
139
+ type: 'object',
140
+ properties: {
141
+ parent_manifest_path: { type: 'string' },
142
+ question_id: { type: 'string' },
143
+ wait_ms: { type: 'number' }
144
+ },
145
+ required: ['parent_manifest_path', 'question_id']
146
+ }));
147
+ if (options.githubEnabled) {
148
+ if (options.allowedGithubOps.has('open_pr')) {
149
+ tools.push(toolDefinition('github.open_pr', 'Open a pull request', {
150
+ type: 'object',
151
+ properties: {
152
+ repo: { type: 'string' },
153
+ title: { type: 'string' },
154
+ body: { type: 'string' },
155
+ base: { type: 'string' },
156
+ head: { type: 'string' },
157
+ draft: { type: 'boolean' }
158
+ },
159
+ required: ['title']
160
+ }));
161
+ }
162
+ if (options.allowedGithubOps.has('comment')) {
163
+ tools.push(toolDefinition('github.comment', 'Create a PR/issue comment', {
164
+ type: 'object',
165
+ properties: {
166
+ repo: { type: 'string' },
167
+ issue_number: { type: 'number' },
168
+ body: { type: 'string' }
169
+ },
170
+ required: ['issue_number', 'body']
171
+ }));
172
+ }
173
+ if (options.allowedGithubOps.has('review')) {
174
+ tools.push(toolDefinition('github.review', 'Submit a PR review', {
175
+ type: 'object',
176
+ properties: {
177
+ repo: { type: 'string' },
178
+ pull_number: { type: 'number' },
179
+ event: { type: 'string', enum: ['APPROVE', 'REQUEST_CHANGES', 'COMMENT'] },
180
+ body: { type: 'string' }
181
+ },
182
+ required: ['pull_number', 'event']
183
+ }));
184
+ }
185
+ if (options.allowedGithubOps.has('get_checks')) {
186
+ tools.push(toolDefinition('github.get_checks', 'Fetch PR checks', {
187
+ type: 'object',
188
+ properties: {
189
+ repo: { type: 'string' },
190
+ pull_number: { type: 'number' }
191
+ },
192
+ required: ['pull_number']
193
+ }));
194
+ }
195
+ if (options.allowedGithubOps.has('merge')) {
196
+ tools.push(toolDefinition('github.merge', 'Merge a PR', {
197
+ type: 'object',
198
+ properties: {
199
+ manifest_path: { type: 'string' },
200
+ repo: { type: 'string' },
201
+ pull_number: { type: 'number' },
202
+ method: { type: 'string', enum: ['merge', 'squash', 'rebase'] },
203
+ delete_branch: { type: 'boolean' }
204
+ },
205
+ required: ['pull_number']
206
+ }));
207
+ }
208
+ }
209
+ return tools;
210
+ }
211
+ function toolDefinition(name, description, inputSchema) {
212
+ return { name, description, inputSchema };
213
+ }
214
+ async function handleToolCall(request, context) {
215
+ const params = asRecord(request.params);
216
+ const toolName = readStringValue(params, 'name');
217
+ if (!toolName) {
218
+ throw new Error('Invalid tool call: missing name');
219
+ }
220
+ const input = asRecord(params.arguments);
221
+ if (context.mode === 'question_only' && isRestrictedTool(toolName)) {
222
+ await reportSecurityViolation('delegate_mode_violation', `Tool ${toolName} blocked in question_only mode.`, toolName, context.allowedHosts);
223
+ throw new Error('delegate_mode_forbidden');
224
+ }
225
+ if (containsSecret(input, 'confirm_nonce') || containsSecret(input, 'confirmNonce')) {
226
+ await reportSecurityViolation('confirm_nonce_present', 'Model supplied confirm_nonce.', toolName, context.allowedHosts);
227
+ throw new Error('confirm_nonce must be injected by the runner');
228
+ }
229
+ if (containsSecret(input, 'delegation_token') || containsSecret(input, 'delegationToken')) {
230
+ await reportSecurityViolation('delegation_token_present', 'Model supplied delegation_token.', toolName, context.allowedHosts);
231
+ throw new Error('delegation_token must be injected by the runner');
232
+ }
233
+ switch (toolName) {
234
+ case 'delegate.status':
235
+ return wrapResult(await handleDelegateStatus(input, context.allowedRoots, context.allowedHosts));
236
+ case 'delegate.pause':
237
+ return wrapResult(await handleDelegatePause(input, context.allowedRoots, context.allowedHosts));
238
+ case 'delegate.cancel':
239
+ return wrapResult(await handleDelegateCancel(input, request, context.allowedRoots, context.allowedHosts));
240
+ case 'delegate.spawn':
241
+ return wrapResult(await handleDelegateSpawn(input, context.repoRoot, context.allowNested, context.allowedRoots, context.allowedHosts, context.toolProfile));
242
+ case 'delegate.question.enqueue':
243
+ return wrapResult(await handleQuestionEnqueue(input, request, context.allowedRoots, context.allowedHosts, context.expiryFallback));
244
+ case 'delegate.question.poll':
245
+ return wrapResult(await handleQuestionPoll(input, request, context.allowedRoots, context.allowedHosts, context.expiryFallback));
246
+ case 'github.open_pr':
247
+ case 'github.comment':
248
+ case 'github.review':
249
+ case 'github.get_checks':
250
+ case 'github.merge':
251
+ return wrapResult(await handleGithubCall(toolName, input, request, context));
252
+ default:
253
+ throw new Error(`Unknown tool: ${toolName}`);
254
+ }
255
+ }
256
+ function wrapResult(payload) {
257
+ return {
258
+ content: [
259
+ {
260
+ type: 'text',
261
+ text: typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2)
262
+ }
263
+ ],
264
+ isError: false
265
+ };
266
+ }
267
+ async function handleDelegateStatus(input, allowedRoots, allowedHosts) {
268
+ const manifestPath = resolveManifestPath(readStringValue(input, 'manifest_path', 'manifestPath'), allowedRoots);
269
+ const raw = await readFile(manifestPath, 'utf8');
270
+ const manifest = JSON.parse(raw);
271
+ const eventsPath = resolve(dirname(manifestPath), 'events.jsonl');
272
+ await assertControlEndpoint(manifestPath, allowedHosts);
273
+ return {
274
+ run_id: manifest.run_id,
275
+ task_id: manifest.task_id,
276
+ status: manifest.status,
277
+ status_detail: manifest.status_detail ?? null,
278
+ manifest_path: manifestPath,
279
+ events_path: eventsPath,
280
+ log_path: manifest.log_path ?? null
281
+ };
282
+ }
283
+ async function handleDelegatePause(input, allowedRoots, allowedHosts) {
284
+ const manifestPath = resolveManifestPath(readStringValue(input, 'manifest_path', 'manifestPath'), allowedRoots);
285
+ const paused = readBooleanValue(input, 'paused') ?? false;
286
+ return await callControlEndpoint(manifestPath, '/control/action', {
287
+ action: paused ? 'pause' : 'resume',
288
+ requested_by: 'delegate'
289
+ }, undefined, { allowedHosts });
290
+ }
291
+ async function handleDelegateCancel(input, request, allowedRoots, allowedHosts) {
292
+ const manifestPath = resolveManifestPath(readStringValue(input, 'manifest_path', 'manifestPath'), allowedRoots);
293
+ const privateNonce = request.codex_private?.confirm_nonce;
294
+ if (!privateNonce) {
295
+ return await callControlEndpoint(manifestPath, '/confirmations/create', {
296
+ action: 'cancel',
297
+ tool: 'delegate.cancel',
298
+ params: { manifest_path: manifestPath }
299
+ }, undefined, { allowedHosts });
300
+ }
301
+ try {
302
+ return await callControlEndpoint(manifestPath, '/control/action', {
303
+ action: 'cancel',
304
+ requested_by: 'delegate',
305
+ confirm_nonce: String(privateNonce),
306
+ tool: 'delegate.cancel',
307
+ params: { manifest_path: manifestPath }
308
+ }, undefined, { allowedHosts });
309
+ }
310
+ catch (error) {
311
+ if (!isConfirmationError(error)) {
312
+ throw error;
313
+ }
314
+ return await callControlEndpoint(manifestPath, '/confirmations/create', {
315
+ action: 'cancel',
316
+ tool: 'delegate.cancel',
317
+ params: { manifest_path: manifestPath }
318
+ }, undefined, { allowedHosts });
319
+ }
320
+ }
321
+ async function handleDelegateSpawn(input, repoRoot, allowNested, allowedRoots, allowedHosts, toolProfile) {
322
+ const pipeline = requireString(readStringValue(input, 'pipeline'), 'pipeline');
323
+ const repo = readStringValue(input, 'repo') ?? repoRoot ?? process.cwd();
324
+ const resolvedRepo = resolve(repo);
325
+ if (!isPathWithinRoots(resolvedRepo, allowedRoots)) {
326
+ throw new Error('repo_not_permitted');
327
+ }
328
+ const taskId = readStringValue(input, 'task_id', 'taskId');
329
+ const args = ['start', pipeline, '--format', 'json', '--no-interactive'];
330
+ if (taskId) {
331
+ args.push('--task', taskId);
332
+ }
333
+ const parentRunId = readStringValue(input, 'parent_run_id', 'parentRunId') ?? process.env.CODEX_ORCHESTRATOR_RUN_ID;
334
+ if (parentRunId) {
335
+ args.push('--parent-run', parentRunId);
336
+ }
337
+ const requestedMode = readStringValue(input, 'delegate_mode', 'delegateMode') ?? 'question_only';
338
+ const childMode = allowNested && requestedMode === 'full' ? 'full' : 'question_only';
339
+ const envOverrides = readStringMap(input, 'env');
340
+ const delegationToken = randomBytes(32).toString('hex');
341
+ const parentManifestPath = resolveParentManifestPath(input, allowedRoots);
342
+ const mcpOverrides = buildDelegateMcpOverrides(toolProfile);
343
+ const childEnv = {
344
+ ...process.env,
345
+ ...(envOverrides ?? {}),
346
+ CODEX_DELEGATE_MODE: childMode,
347
+ ...(parentManifestPath ? { CODEX_DELEGATION_PARENT_MANIFEST_PATH: parentManifestPath } : {}),
348
+ ...(mcpOverrides.length > 0 ? { CODEX_MCP_CONFIG_OVERRIDES: mcpOverrides.join(';') } : {})
349
+ };
350
+ const child = spawn('codex-orchestrator', args, { cwd: resolvedRepo, env: childEnv });
351
+ const output = await collectOutput(child, DEFAULT_SPAWN_TIMEOUT_MS);
352
+ const parsedRecord = parseSpawnOutput(output.stdout);
353
+ const manifestPath = readStringValue(parsedRecord, 'manifest');
354
+ if (!manifestPath) {
355
+ return { status: 'spawn_failed', stdout: output.stdout.trim(), stderr: output.stderr.trim() };
356
+ }
357
+ const runId = readStringValue(parsedRecord, 'run_id', 'runId');
358
+ const logPath = readStringValue(parsedRecord, 'log_path', 'logPath');
359
+ const resolvedManifestPath = resolveSpawnManifestPath(manifestPath, resolvedRepo, allowedRoots);
360
+ if (!resolvedManifestPath) {
361
+ return { status: 'spawn_failed', stdout: output.stdout.trim(), stderr: output.stderr.trim() };
362
+ }
363
+ const eventsPath = `${dirname(resolvedManifestPath)}/events.jsonl`;
364
+ await persistDelegationToken(resolvedManifestPath, delegationToken, {
365
+ parentRunId: parentRunId ?? null,
366
+ childRunId: runId ?? null
367
+ });
368
+ if (parentManifestPath && parentRunId && runId) {
369
+ try {
370
+ await callControlEndpoint(parentManifestPath, '/delegation/register', {
371
+ token: delegationToken,
372
+ parent_run_id: parentRunId,
373
+ child_run_id: runId
374
+ }, undefined, { allowedHosts });
375
+ }
376
+ catch (error) {
377
+ logger.warn(`Failed to register delegation token: ${error?.message ?? error}`);
378
+ }
379
+ }
380
+ return {
381
+ run_id: runId,
382
+ manifest_path: resolvedManifestPath,
383
+ log_path: logPath,
384
+ events_path: eventsPath
385
+ };
386
+ }
387
+ async function handleQuestionEnqueue(input, request, allowedRoots, allowedHosts, expiryFallback) {
388
+ const parentManifestPath = resolveParentManifestPath(input, allowedRoots);
389
+ if (!parentManifestPath) {
390
+ throw new Error('parent_manifest_path is required');
391
+ }
392
+ const delegationToken = await resolveDelegationToken(request, allowedRoots, {
393
+ retryMs: DEFAULT_DELEGATION_TOKEN_RETRY_MS,
394
+ intervalMs: DEFAULT_DELEGATION_TOKEN_RETRY_INTERVAL_MS
395
+ });
396
+ const childRunId = process.env.CODEX_ORCHESTRATOR_RUN_ID ?? readStringValue(input, 'from_run_id', 'fromRunId') ?? '';
397
+ if (!delegationToken) {
398
+ throw new Error('delegation_token missing');
399
+ }
400
+ const autoPause = readBooleanValue(input, 'auto_pause', 'autoPause') ?? true;
401
+ const manifestFromEnv = process.env.CODEX_ORCHESTRATOR_MANIFEST_PATH;
402
+ const manifestFromInput = readStringValue(input, 'from_manifest_path', 'fromManifestPath');
403
+ const childManifestPath = manifestFromEnv ?? manifestFromInput;
404
+ const result = await callControlEndpointWithRetry(parentManifestPath, '/questions/enqueue', {
405
+ parent_run_id: readStringValue(input, 'parent_run_id', 'parentRunId') ?? '',
406
+ parent_task_id: readStringValue(input, 'parent_task_id', 'parentTaskId') ?? null,
407
+ from_run_id: childRunId,
408
+ from_manifest_path: childManifestPath ?? null,
409
+ prompt: requireString(readStringValue(input, 'prompt'), 'prompt'),
410
+ urgency: readStringValue(input, 'urgency') ?? 'med',
411
+ expires_in_ms: readNumberValue(input, 'expires_in_ms', 'expiresInMs'),
412
+ auto_pause: autoPause,
413
+ expiry_fallback: expiryFallback
414
+ }, {
415
+ [DELEGATION_TOKEN_HEADER]: delegationToken,
416
+ [DELEGATION_RUN_HEADER]: childRunId
417
+ }, {
418
+ allowedHosts,
419
+ allowedRoots,
420
+ retryMs: DEFAULT_DELEGATION_TOKEN_RETRY_MS,
421
+ retryIntervalMs: DEFAULT_DELEGATION_TOKEN_RETRY_INTERVAL_MS
422
+ });
423
+ if (autoPause && manifestFromEnv) {
424
+ const resolvedManifest = resolveRunManifestPath(manifestFromEnv, allowedRoots, 'manifest_path');
425
+ await callControlEndpoint(resolvedManifest, '/control/action', {
426
+ action: 'pause',
427
+ requested_by: 'delegate',
428
+ reason: 'awaiting_question_answer'
429
+ }, undefined, { allowedHosts, allowedRoots });
430
+ }
431
+ return {
432
+ ...result,
433
+ fallback_action: expiryFallback
434
+ };
435
+ }
436
+ async function handleQuestionPoll(input, request, allowedRoots, allowedHosts, expiryFallback) {
437
+ const parentManifestPath = resolveParentManifestPath(input, allowedRoots);
438
+ if (!parentManifestPath) {
439
+ throw new Error('parent_manifest_path is required');
440
+ }
441
+ const delegationToken = await resolveDelegationToken(request, allowedRoots, {
442
+ retryMs: DEFAULT_DELEGATION_TOKEN_RETRY_MS,
443
+ intervalMs: DEFAULT_DELEGATION_TOKEN_RETRY_INTERVAL_MS
444
+ });
445
+ const childRunId = process.env.CODEX_ORCHESTRATOR_RUN_ID ?? readStringValue(input, 'from_run_id', 'fromRunId') ?? '';
446
+ if (!delegationToken) {
447
+ throw new Error('delegation_token missing');
448
+ }
449
+ const questionId = requireString(readStringValue(input, 'question_id', 'questionId'), 'question_id');
450
+ const requestedWaitMs = readNumberValue(input, 'wait_ms', 'waitMs') ?? 0;
451
+ const waitMs = clampQuestionPollWaitMs(requestedWaitMs);
452
+ const deadline = Date.now() + waitMs;
453
+ const maxIterations = waitMs > 0 ? Math.max(1, Math.ceil(waitMs / QUESTION_POLL_INTERVAL_MS)) : 1;
454
+ for (let iteration = 0; iteration < maxIterations; iteration += 1) {
455
+ const remainingMs = waitMs > 0 ? Math.max(0, deadline - Date.now()) : null;
456
+ const timeoutMs = remainingMs === null ? undefined : Math.max(1, Math.min(DEFAULT_CONTROL_ENDPOINT_TIMEOUT_MS, remainingMs));
457
+ const retryMs = remainingMs === null ? DEFAULT_DELEGATION_TOKEN_RETRY_MS : Math.min(DEFAULT_DELEGATION_TOKEN_RETRY_MS, remainingMs);
458
+ const record = await callControlEndpointWithRetry(parentManifestPath, `/questions/${questionId}`, null, {
459
+ [DELEGATION_TOKEN_HEADER]: delegationToken,
460
+ [DELEGATION_RUN_HEADER]: childRunId
461
+ }, {
462
+ allowedHosts,
463
+ allowedRoots,
464
+ retryMs,
465
+ retryIntervalMs: DEFAULT_DELEGATION_TOKEN_RETRY_INTERVAL_MS,
466
+ ...(timeoutMs !== undefined ? { timeoutMs } : {})
467
+ });
468
+ const status = readStringValue(record, 'status');
469
+ if (status !== 'queued' || waitMs <= 0 || Date.now() >= deadline) {
470
+ const expiresAt = readStringValue(record, 'expires_at', 'expiresAt');
471
+ if (status === 'expired') {
472
+ await applyQuestionFallback(expiryFallback, allowedHosts, allowedRoots);
473
+ }
474
+ return {
475
+ ...record,
476
+ expired_at: status === 'expired' ? expiresAt ?? null : null,
477
+ fallback_action: status === 'expired' ? expiryFallback : null
478
+ };
479
+ }
480
+ await delay(QUESTION_POLL_INTERVAL_MS);
481
+ }
482
+ const remainingMs = waitMs > 0 ? Math.max(0, deadline - Date.now()) : null;
483
+ const timeoutMs = remainingMs === null ? undefined : Math.max(1, Math.min(DEFAULT_CONTROL_ENDPOINT_TIMEOUT_MS, remainingMs));
484
+ const record = await callControlEndpoint(parentManifestPath, `/questions/${questionId}`, null, {
485
+ [DELEGATION_TOKEN_HEADER]: delegationToken,
486
+ [DELEGATION_RUN_HEADER]: childRunId
487
+ }, {
488
+ allowedHosts,
489
+ ...(timeoutMs !== undefined ? { timeoutMs } : {})
490
+ });
491
+ return {
492
+ ...record,
493
+ expired_at: null,
494
+ fallback_action: null
495
+ };
496
+ }
497
+ async function handleGithubCall(toolName, input, request, context) {
498
+ const op = toolName.replace('github.', '');
499
+ if (!context.githubEnabled || !context.allowedGithubOps.has(op)) {
500
+ throw new Error('github_operation_disallowed');
501
+ }
502
+ if (toolName === 'github.merge') {
503
+ const privateNonce = request.codex_private?.confirm_nonce;
504
+ const manifestPath = resolveManifestPath(readStringValue(input, 'manifest_path', 'manifestPath') ?? process.env.CODEX_ORCHESTRATOR_MANIFEST_PATH, context.allowedRoots);
505
+ if (!privateNonce) {
506
+ return await callControlEndpoint(manifestPath, '/confirmations/create', {
507
+ action: 'merge',
508
+ tool: toolName,
509
+ params: { ...input, manifest_path: manifestPath }
510
+ }, undefined, { allowedHosts: context.allowedHosts });
511
+ }
512
+ try {
513
+ await callControlEndpoint(manifestPath, '/confirmations/validate', {
514
+ confirm_nonce: String(privateNonce),
515
+ tool: toolName,
516
+ params: { ...input, manifest_path: manifestPath }
517
+ }, undefined, { allowedHosts: context.allowedHosts });
518
+ }
519
+ catch (error) {
520
+ if (!isConfirmationError(error)) {
521
+ throw error;
522
+ }
523
+ return await callControlEndpoint(manifestPath, '/confirmations/create', {
524
+ action: 'merge',
525
+ tool: toolName,
526
+ params: { ...input, manifest_path: manifestPath }
527
+ }, undefined, { allowedHosts: context.allowedHosts });
528
+ }
529
+ }
530
+ switch (toolName) {
531
+ case 'github.open_pr':
532
+ return await runGh([
533
+ 'pr',
534
+ 'create',
535
+ ...(readStringValue(input, 'title') ? ['--title', readStringValue(input, 'title')] : []),
536
+ ...(readStringValue(input, 'body') ? ['--body', readStringValue(input, 'body')] : ['--body', '']),
537
+ ...(readStringValue(input, 'base') ? ['--base', readStringValue(input, 'base')] : []),
538
+ ...(readStringValue(input, 'head') ? ['--head', readStringValue(input, 'head')] : []),
539
+ ...(readBooleanValue(input, 'draft') ? ['--draft'] : []),
540
+ ...(readStringValue(input, 'repo') ? ['--repo', readStringValue(input, 'repo')] : [])
541
+ ]);
542
+ case 'github.comment':
543
+ return await runGh([
544
+ 'issue',
545
+ 'comment',
546
+ String(requireNumber(readNumberValue(input, 'issue_number', 'issueNumber'), 'issue_number')),
547
+ ...(readStringValue(input, 'body') ? ['--body', readStringValue(input, 'body')] : []),
548
+ ...(readStringValue(input, 'repo') ? ['--repo', readStringValue(input, 'repo')] : [])
549
+ ]);
550
+ case 'github.review':
551
+ return await runGh([
552
+ 'pr',
553
+ 'review',
554
+ String(requireNumber(readNumberValue(input, 'pull_number', 'pullNumber'), 'pull_number')),
555
+ ...(readStringValue(input, 'event') === 'APPROVE' ? ['--approve'] : []),
556
+ ...(readStringValue(input, 'event') === 'REQUEST_CHANGES' ? ['--request-changes'] : []),
557
+ ...(readStringValue(input, 'event') === 'COMMENT' ? ['--comment'] : []),
558
+ ...(readStringValue(input, 'body') ? ['--body', readStringValue(input, 'body')] : []),
559
+ ...(readStringValue(input, 'repo') ? ['--repo', readStringValue(input, 'repo')] : [])
560
+ ]);
561
+ case 'github.get_checks': {
562
+ const pullNumber = requireNumber(readNumberValue(input, 'pull_number', 'pullNumber'), 'pull_number');
563
+ const result = await runGh([
564
+ 'pr',
565
+ 'view',
566
+ String(pullNumber),
567
+ ...(readStringValue(input, 'repo') ? ['--repo', readStringValue(input, 'repo')] : []),
568
+ '--json',
569
+ 'statusCheckRollup'
570
+ ]);
571
+ return safeJsonParse(result.stdout) ?? result;
572
+ }
573
+ case 'github.merge': {
574
+ const mergeNumber = requireNumber(readNumberValue(input, 'pull_number', 'pullNumber'), 'pull_number');
575
+ return await runGh([
576
+ 'pr',
577
+ 'merge',
578
+ String(mergeNumber),
579
+ ...(readStringValue(input, 'method') === 'squash'
580
+ ? ['--squash']
581
+ : readStringValue(input, 'method') === 'rebase'
582
+ ? ['--rebase']
583
+ : ['--merge']),
584
+ ...(readBooleanValue(input, 'delete_branch', 'deleteBranch') ? ['--delete-branch'] : []),
585
+ ...(readStringValue(input, 'repo') ? ['--repo', readStringValue(input, 'repo')] : [])
586
+ ]);
587
+ }
588
+ default:
589
+ throw new Error(`Unsupported GitHub tool: ${toolName}`);
590
+ }
591
+ }
592
+ export async function callControlEndpointWithRetry(manifestPath, endpoint, payload, extraHeaders = {}, options = {}) {
593
+ const retryMs = options.retryMs ?? 0;
594
+ const retryIntervalMs = options.retryIntervalMs ?? DEFAULT_DELEGATION_TOKEN_RETRY_INTERVAL_MS;
595
+ const deadline = Date.now() + retryMs;
596
+ let attempt = 0;
597
+ while (attempt === 0 || Date.now() < deadline) {
598
+ try {
599
+ return await callControlEndpoint(manifestPath, endpoint, payload, extraHeaders, options);
600
+ }
601
+ catch (error) {
602
+ if (!shouldRetryControlError(error) || Date.now() >= deadline) {
603
+ throw error;
604
+ }
605
+ attempt += 1;
606
+ await delay(retryIntervalMs * Math.min(4, attempt));
607
+ }
608
+ }
609
+ throw new Error('control endpoint retry exhausted');
610
+ }
611
+ async function callControlEndpoint(manifestPath, endpoint, payload, extraHeaders = {}, options = {}) {
612
+ const { baseUrl, token } = await loadControlEndpoint(manifestPath, options);
613
+ const url = new URL(endpoint, baseUrl);
614
+ const timeoutMs = options.timeoutMs ?? DEFAULT_CONTROL_ENDPOINT_TIMEOUT_MS;
615
+ const controller = new AbortController();
616
+ const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
617
+ let res;
618
+ try {
619
+ res = await fetch(url.toString(), {
620
+ method: payload ? 'POST' : 'GET',
621
+ headers: {
622
+ 'Content-Type': 'application/json',
623
+ Authorization: `Bearer ${token}`,
624
+ [CSRF_HEADER]: token,
625
+ ...extraHeaders
626
+ },
627
+ body: payload ? JSON.stringify(payload) : undefined,
628
+ signal: controller.signal
629
+ });
630
+ }
631
+ catch (error) {
632
+ if (error?.name === 'AbortError') {
633
+ throw new Error('control endpoint request timeout');
634
+ }
635
+ throw error;
636
+ }
637
+ finally {
638
+ if (timer) {
639
+ clearTimeout(timer);
640
+ }
641
+ }
642
+ if (!res.ok) {
643
+ const raw = await res.text();
644
+ let errorCode = null;
645
+ let message = raw;
646
+ try {
647
+ const parsed = JSON.parse(raw);
648
+ if (parsed && typeof parsed === 'object' && typeof parsed.error === 'string') {
649
+ errorCode = parsed.error;
650
+ message = parsed.error;
651
+ }
652
+ }
653
+ catch {
654
+ // ignore parse errors
655
+ }
656
+ const error = new Error(`control endpoint error: ${res.status} ${message}`);
657
+ error.code = errorCode ?? undefined;
658
+ throw error;
659
+ }
660
+ return (await res.json());
661
+ }
662
+ function shouldRetryControlError(error) {
663
+ const code = error?.code;
664
+ if (code === 'delegation_token_invalid') {
665
+ return true;
666
+ }
667
+ const message = error?.message ?? '';
668
+ return message.includes('delegation_token_invalid');
669
+ }
670
+ function isConfirmationError(error) {
671
+ const code = error?.code;
672
+ return !!(code && CONFIRMATION_ERROR_CODES.has(code));
673
+ }
674
+ export async function loadControlEndpoint(manifestPath, options = {}) {
675
+ const resolvedManifest = resolveRunManifestPath(manifestPath, options.allowedRoots, 'manifest_path');
676
+ const runDir = dirname(resolvedManifest);
677
+ const endpointPath = resolve(runDir, 'control_endpoint.json');
678
+ const raw = await readFile(endpointPath, 'utf8');
679
+ const endpointInfo = JSON.parse(raw);
680
+ const baseUrl = validateControlBaseUrl(endpointInfo.base_url, options.allowedHosts);
681
+ const tokenPath = resolveControlTokenPath(endpointInfo.token_path, runDir);
682
+ const token = await readControlToken(tokenPath);
683
+ return { baseUrl, token };
684
+ }
685
+ async function assertControlEndpoint(manifestPath, allowedHosts) {
686
+ await loadControlEndpoint(manifestPath, { allowedHosts });
687
+ }
688
+ function validateControlBaseUrl(raw, allowedHosts) {
689
+ if (typeof raw !== 'string' || raw.trim().length === 0) {
690
+ throw new Error('control base_url missing');
691
+ }
692
+ let parsed;
693
+ try {
694
+ parsed = new URL(raw);
695
+ }
696
+ catch {
697
+ throw new Error('control base_url invalid');
698
+ }
699
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
700
+ throw new Error('control base_url invalid');
701
+ }
702
+ if (parsed.username || parsed.password) {
703
+ throw new Error('control base_url invalid');
704
+ }
705
+ const allowed = normalizeAllowedHosts(allowedHosts);
706
+ if (allowed.size > 0 && !allowed.has(parsed.hostname.toLowerCase())) {
707
+ throw new Error('control base_url not permitted');
708
+ }
709
+ return parsed;
710
+ }
711
+ function normalizeAllowedHosts(allowedHosts) {
712
+ const values = allowedHosts && allowedHosts.length > 0 ? allowedHosts : Array.from(LOOPBACK_HOSTS);
713
+ return new Set(values.map((entry) => entry.toLowerCase()));
714
+ }
715
+ function resolveControlTokenPath(tokenPath, runDir) {
716
+ const fallback = resolve(runDir, 'control_auth.json');
717
+ const raw = typeof tokenPath === 'string' ? tokenPath.trim() : '';
718
+ const resolved = raw ? resolve(runDir, raw) : fallback;
719
+ if (!isPathWithinRoots(resolved, [runDir])) {
720
+ throw new Error('control auth path invalid');
721
+ }
722
+ return resolved;
723
+ }
724
+ async function readControlToken(tokenPath) {
725
+ const tokenRaw = await readFile(tokenPath, 'utf8');
726
+ const parsedToken = safeJsonParse(tokenRaw);
727
+ const tokenValue = parsedToken && typeof parsedToken === 'object' && !Array.isArray(parsedToken)
728
+ ? parsedToken.token
729
+ : null;
730
+ const token = typeof tokenValue === 'string' && tokenValue.trim().length > 0
731
+ ? tokenValue.trim()
732
+ : tokenRaw.trim();
733
+ if (!token) {
734
+ throw new Error('control auth token missing');
735
+ }
736
+ return token;
737
+ }
738
+ async function runGh(args, timeoutMs = DEFAULT_GH_TIMEOUT_MS) {
739
+ return new Promise((resolvePromise, reject) => {
740
+ const child = spawn('gh', args, { stdio: ['ignore', 'pipe', 'pipe'] });
741
+ let stdout = '';
742
+ let stderr = '';
743
+ let settled = false;
744
+ const timer = setTimeout(() => {
745
+ if (settled) {
746
+ return;
747
+ }
748
+ settled = true;
749
+ child.kill('SIGTERM');
750
+ setTimeout(() => child.kill('SIGKILL'), 5000);
751
+ reject(new Error('gh command timed out'));
752
+ }, timeoutMs);
753
+ child.stdout?.on('data', (chunk) => {
754
+ stdout += chunk.toString();
755
+ });
756
+ child.stderr?.on('data', (chunk) => {
757
+ stderr += chunk.toString();
758
+ });
759
+ child.on('error', (error) => {
760
+ if (settled) {
761
+ return;
762
+ }
763
+ settled = true;
764
+ clearTimeout(timer);
765
+ reject(error);
766
+ });
767
+ child.on('exit', (code) => {
768
+ if (settled) {
769
+ return;
770
+ }
771
+ settled = true;
772
+ clearTimeout(timer);
773
+ if (code === 0) {
774
+ resolvePromise({ stdout, stderr });
775
+ }
776
+ else {
777
+ reject(new Error(stderr.trim() || `gh exited with code ${code}`));
778
+ }
779
+ });
780
+ });
781
+ }
782
+ async function runJsonRpcServer(handler, options = {}) {
783
+ let buffer = Buffer.alloc(0);
784
+ let expectedLength = null;
785
+ let processing = Promise.resolve();
786
+ let halted = false;
787
+ const input = options.stdin ?? process.stdin;
788
+ const output = options.stdout ?? process.stdout;
789
+ const handleProtocolViolation = (message) => {
790
+ if (halted) {
791
+ return;
792
+ }
793
+ halted = true;
794
+ logger.warn(message);
795
+ process.exitCode = 1;
796
+ buffer = Buffer.alloc(0);
797
+ expectedLength = null;
798
+ if (typeof input.pause === 'function') {
799
+ input.pause();
800
+ }
801
+ };
802
+ input.on('data', (chunk) => {
803
+ if (halted) {
804
+ return;
805
+ }
806
+ buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
807
+ if (buffer.length > MAX_MCP_BUFFER_BYTES) {
808
+ handleProtocolViolation(`Rejecting MCP buffer larger than ${MAX_MCP_BUFFER_BYTES} bytes`);
809
+ return;
810
+ }
811
+ processing = processing
812
+ .then(() => processBuffer())
813
+ .catch((error) => {
814
+ logger.error(`Failed to process MCP buffer: ${error?.message ?? error}`);
815
+ });
816
+ });
817
+ async function processBuffer() {
818
+ while (buffer.length > 0) {
819
+ if (halted) {
820
+ return;
821
+ }
822
+ if (expectedLength !== null) {
823
+ if (buffer.length < expectedLength) {
824
+ return;
825
+ }
826
+ const body = buffer.slice(0, expectedLength);
827
+ buffer = buffer.slice(expectedLength);
828
+ expectedLength = null;
829
+ await handleMessage(body.toString('utf8'));
830
+ continue;
831
+ }
832
+ const headerEnd = buffer.indexOf('\r\n\r\n');
833
+ if (headerEnd === -1) {
834
+ if (buffer.length > MAX_MCP_HEADER_BYTES) {
835
+ const overflow = buffer.slice(MAX_MCP_HEADER_BYTES);
836
+ const allowedPrefix = MCP_HEADER_DELIMITER_BUFFER.subarray(0, overflow.length);
837
+ if (overflow.length > MCP_HEADER_DELIMITER_BYTES || !overflow.equals(allowedPrefix)) {
838
+ handleProtocolViolation(`Rejecting MCP header larger than ${MAX_MCP_HEADER_BYTES} bytes`);
839
+ }
840
+ }
841
+ return;
842
+ }
843
+ if (headerEnd > MAX_MCP_HEADER_BYTES) {
844
+ handleProtocolViolation(`Rejecting MCP header larger than ${MAX_MCP_HEADER_BYTES} bytes`);
845
+ return;
846
+ }
847
+ const header = buffer.slice(0, headerEnd).toString('utf8');
848
+ const parsed = parseContentLengthHeader(header);
849
+ if (parsed.error) {
850
+ handleProtocolViolation(parsed.error);
851
+ return;
852
+ }
853
+ if (parsed.length === null) {
854
+ handleProtocolViolation('Missing Content-Length header in MCP message');
855
+ return;
856
+ }
857
+ const length = parsed.length;
858
+ if (!Number.isFinite(length) || length < 0) {
859
+ handleProtocolViolation('Invalid Content-Length for MCP payload');
860
+ return;
861
+ }
862
+ if (length > MAX_MCP_MESSAGE_BYTES) {
863
+ handleProtocolViolation(`Rejecting MCP payload (${length} bytes) larger than ${MAX_MCP_MESSAGE_BYTES}`);
864
+ return;
865
+ }
866
+ expectedLength = length;
867
+ buffer = buffer.slice(headerEnd + 4);
868
+ }
869
+ }
870
+ async function handleMessage(raw) {
871
+ let request;
872
+ try {
873
+ request = JSON.parse(raw);
874
+ }
875
+ catch (error) {
876
+ logger.error(`Failed to parse MCP message: ${error?.message ?? error}`);
877
+ return;
878
+ }
879
+ if (typeof request.method !== 'string') {
880
+ return;
881
+ }
882
+ const id = request.id ?? null;
883
+ try {
884
+ const result = await handler(request);
885
+ if (id !== null && typeof id !== 'undefined') {
886
+ sendResponse({ jsonrpc: '2.0', id, result }, output);
887
+ }
888
+ }
889
+ catch (error) {
890
+ if (id !== null && typeof id !== 'undefined') {
891
+ sendResponse({
892
+ jsonrpc: '2.0',
893
+ id,
894
+ error: { code: -32603, message: error?.message ?? String(error) }
895
+ }, output);
896
+ }
897
+ }
898
+ }
899
+ }
900
+ function parseContentLengthHeader(header) {
901
+ const lines = header.split(/\r?\n/);
902
+ let contentLength = null;
903
+ for (const line of lines) {
904
+ const separator = line.indexOf(':');
905
+ if (separator === -1) {
906
+ continue;
907
+ }
908
+ const name = line.slice(0, separator).trim().toLowerCase();
909
+ if (name !== 'content-length') {
910
+ continue;
911
+ }
912
+ if (contentLength !== null) {
913
+ return { length: null, error: 'Multiple Content-Length headers in MCP message' };
914
+ }
915
+ const value = line.slice(separator + 1).trim();
916
+ if (!/^\d+$/.test(value)) {
917
+ return { length: null, error: 'Invalid Content-Length header in MCP message' };
918
+ }
919
+ contentLength = Number(value);
920
+ }
921
+ return { length: contentLength };
922
+ }
923
+ function sendResponse(response, output = process.stdout) {
924
+ const payload = Buffer.from(JSON.stringify(response), 'utf8');
925
+ const header = Buffer.from(`Content-Length: ${payload.length}\r\n\r\n`, 'utf8');
926
+ output.write(Buffer.concat([header, payload]));
927
+ }
928
+ function safeJsonParse(text) {
929
+ try {
930
+ return JSON.parse(text);
931
+ }
932
+ catch {
933
+ return null;
934
+ }
935
+ }
936
+ function parseSpawnOutput(stdout) {
937
+ const trimmed = stdout.trim();
938
+ if (!trimmed) {
939
+ return {};
940
+ }
941
+ const direct = safeJsonParse(trimmed);
942
+ if (direct && typeof direct === 'object' && !Array.isArray(direct)) {
943
+ return direct;
944
+ }
945
+ const lines = trimmed.split(/\r?\n/);
946
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
947
+ if (!lines[i].trim().startsWith('{')) {
948
+ continue;
949
+ }
950
+ for (let j = lines.length - 1; j >= i; j -= 1) {
951
+ if (!lines[j].includes('}')) {
952
+ continue;
953
+ }
954
+ const candidate = lines.slice(i, j + 1).join('\n');
955
+ const parsed = safeJsonParse(candidate);
956
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
957
+ return parsed;
958
+ }
959
+ }
960
+ }
961
+ return {};
962
+ }
963
+ export const __test__ = {
964
+ runJsonRpcServer,
965
+ handleToolCall,
966
+ parseContentLengthHeader,
967
+ parseSpawnOutput,
968
+ handleDelegateSpawn,
969
+ MAX_MCP_MESSAGE_BYTES,
970
+ MAX_MCP_HEADER_BYTES,
971
+ MAX_QUESTION_POLL_WAIT_MS,
972
+ QUESTION_POLL_INTERVAL_MS,
973
+ clampQuestionPollWaitMs
974
+ };
975
+ function clampQuestionPollWaitMs(value) {
976
+ if (!Number.isFinite(value) || value <= 0) {
977
+ return 0;
978
+ }
979
+ return Math.min(value, MAX_QUESTION_POLL_WAIT_MS);
980
+ }
981
+ function delay(ms) {
982
+ return new Promise((resolve) => setTimeout(resolve, ms));
983
+ }
984
+ async function collectOutput(child, timeoutMs) {
985
+ let stdout = '';
986
+ let stderr = '';
987
+ let settled = false;
988
+ let rejectPromise = null;
989
+ const timer = setTimeout(() => {
990
+ if (settled) {
991
+ return;
992
+ }
993
+ settled = true;
994
+ child.kill('SIGTERM');
995
+ setTimeout(() => child.kill('SIGKILL'), 5000);
996
+ rejectPromise?.(new Error('delegate.spawn timed out'));
997
+ }, timeoutMs);
998
+ child.stdout?.on('data', (chunk) => {
999
+ stdout += chunk.toString();
1000
+ });
1001
+ child.stderr?.on('data', (chunk) => {
1002
+ stderr += chunk.toString();
1003
+ });
1004
+ await new Promise((resolvePromise, reject) => {
1005
+ rejectPromise = reject;
1006
+ child.once('error', (error) => {
1007
+ if (settled) {
1008
+ return;
1009
+ }
1010
+ settled = true;
1011
+ clearTimeout(timer);
1012
+ reject(error);
1013
+ });
1014
+ child.once('exit', (code, signal) => {
1015
+ if (settled) {
1016
+ return;
1017
+ }
1018
+ settled = true;
1019
+ clearTimeout(timer);
1020
+ if (code === 0 && !signal) {
1021
+ resolvePromise();
1022
+ return;
1023
+ }
1024
+ reject(new Error(`delegate.spawn exited with code ${code ?? 'null'} (${signal ?? 'no signal'}): ${stderr.trim()}`));
1025
+ });
1026
+ });
1027
+ return { stdout, stderr };
1028
+ }
1029
+ function asRecord(value) {
1030
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
1031
+ return {};
1032
+ }
1033
+ return value;
1034
+ }
1035
+ function readStringValue(record, ...keys) {
1036
+ for (const key of keys) {
1037
+ const value = record[key];
1038
+ if (typeof value === 'string' && value.trim().length > 0) {
1039
+ return value.trim();
1040
+ }
1041
+ }
1042
+ return undefined;
1043
+ }
1044
+ function readNumberValue(record, ...keys) {
1045
+ for (const key of keys) {
1046
+ const value = record[key];
1047
+ if (typeof value === 'number' && Number.isFinite(value)) {
1048
+ return value;
1049
+ }
1050
+ if (typeof value === 'string' && value.trim().length > 0) {
1051
+ const parsed = Number(value);
1052
+ if (Number.isFinite(parsed)) {
1053
+ return parsed;
1054
+ }
1055
+ }
1056
+ }
1057
+ return undefined;
1058
+ }
1059
+ function readBooleanValue(record, ...keys) {
1060
+ for (const key of keys) {
1061
+ const value = record[key];
1062
+ if (typeof value === 'boolean') {
1063
+ return value;
1064
+ }
1065
+ }
1066
+ return undefined;
1067
+ }
1068
+ function readStringMap(record, key) {
1069
+ const raw = record[key];
1070
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
1071
+ return undefined;
1072
+ }
1073
+ const entries = {};
1074
+ for (const [entryKey, entryValue] of Object.entries(raw)) {
1075
+ if (typeof entryValue === 'string') {
1076
+ entries[entryKey] = entryValue;
1077
+ }
1078
+ }
1079
+ return Object.keys(entries).length > 0 ? entries : undefined;
1080
+ }
1081
+ function requireString(value, field) {
1082
+ if (!value) {
1083
+ throw new Error(`${field} is required`);
1084
+ }
1085
+ return value;
1086
+ }
1087
+ function requireNumber(value, field) {
1088
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
1089
+ throw new Error(`${field} is required`);
1090
+ }
1091
+ return value;
1092
+ }
1093
+ function isRestrictedTool(toolName) {
1094
+ return toolName === 'delegate.spawn' || toolName === 'delegate.pause' || toolName === 'delegate.cancel';
1095
+ }
1096
+ function containsSecret(record, key) {
1097
+ return Object.prototype.hasOwnProperty.call(record, key);
1098
+ }
1099
+ async function reportSecurityViolation(kind, summary, toolName, allowedHosts) {
1100
+ const manifestPath = process.env.CODEX_ORCHESTRATOR_MANIFEST_PATH;
1101
+ if (!manifestPath) {
1102
+ return;
1103
+ }
1104
+ try {
1105
+ await callControlEndpoint(resolve(manifestPath), '/security/violation', {
1106
+ kind,
1107
+ summary: toolName ? `${summary} Tool=${toolName}` : summary,
1108
+ severity: 'high'
1109
+ }, undefined, { allowedHosts });
1110
+ }
1111
+ catch {
1112
+ // ignore
1113
+ }
1114
+ }
1115
+ export async function resolveDelegationToken(request, allowedRoots, options = {}) {
1116
+ const privateToken = request.codex_private?.delegation_token;
1117
+ if (privateToken) {
1118
+ return String(privateToken);
1119
+ }
1120
+ const tokenPath = resolveDelegationTokenPath(allowedRoots);
1121
+ if (!tokenPath) {
1122
+ return null;
1123
+ }
1124
+ const retryMs = options.retryMs ?? 0;
1125
+ const intervalMs = options.intervalMs ?? DEFAULT_DELEGATION_TOKEN_RETRY_INTERVAL_MS;
1126
+ const deadline = Date.now() + retryMs;
1127
+ let token = await readDelegationTokenFile(tokenPath);
1128
+ while (!token && Date.now() < deadline) {
1129
+ await delay(intervalMs);
1130
+ token = await readDelegationTokenFile(tokenPath);
1131
+ }
1132
+ return token;
1133
+ }
1134
+ function resolveDelegationTokenPath(allowedRoots) {
1135
+ const explicit = process.env.CODEX_DELEGATION_TOKEN_PATH?.trim();
1136
+ const manifestPath = process.env.CODEX_ORCHESTRATOR_MANIFEST_PATH?.trim();
1137
+ let runDir = null;
1138
+ if (manifestPath) {
1139
+ try {
1140
+ const resolvedManifest = resolveRunManifestPath(manifestPath, allowedRoots, 'manifest_path');
1141
+ runDir = dirname(resolvedManifest);
1142
+ }
1143
+ catch {
1144
+ return null;
1145
+ }
1146
+ }
1147
+ if (explicit) {
1148
+ if (!runDir && !isAbsolute(explicit)) {
1149
+ return null;
1150
+ }
1151
+ const resolvedToken = runDir && !isAbsolute(explicit) ? resolve(runDir, explicit) : resolve(explicit);
1152
+ if (runDir) {
1153
+ if (!isPathWithinRoots(resolvedToken, [runDir])) {
1154
+ return null;
1155
+ }
1156
+ }
1157
+ else if (allowedRoots && allowedRoots.length > 0 && !isPathWithinRoots(resolvedToken, allowedRoots)) {
1158
+ return null;
1159
+ }
1160
+ return resolvedToken;
1161
+ }
1162
+ if (runDir) {
1163
+ return resolve(runDir, DELEGATION_TOKEN_FILE);
1164
+ }
1165
+ return null;
1166
+ }
1167
+ async function readDelegationTokenFile(tokenPath) {
1168
+ try {
1169
+ const raw = await readFile(tokenPath, 'utf8');
1170
+ const parsed = safeJsonParse(raw);
1171
+ const tokenValue = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
1172
+ ? parsed.token
1173
+ : null;
1174
+ const token = typeof tokenValue === 'string' && tokenValue.trim().length > 0 ? tokenValue.trim() : raw.trim();
1175
+ return token || null;
1176
+ }
1177
+ catch {
1178
+ return null;
1179
+ }
1180
+ }
1181
+ function buildDelegateMcpOverrides(toolProfile) {
1182
+ const overrides = ['mcp_servers.delegation.enabled=true'];
1183
+ for (const entry of toolProfile) {
1184
+ const sanitized = sanitizeToolProfileEntry(entry);
1185
+ if (!sanitized) {
1186
+ continue;
1187
+ }
1188
+ overrides.push(`mcp_servers.${sanitized}.enabled=true`);
1189
+ }
1190
+ return dedupeOverrides(overrides);
1191
+ }
1192
+ function sanitizeToolProfileEntry(entry) {
1193
+ const trimmed = entry.trim();
1194
+ if (!trimmed) {
1195
+ return null;
1196
+ }
1197
+ if (!TOOL_PROFILE_ENTRY_PATTERN.test(trimmed)) {
1198
+ return null;
1199
+ }
1200
+ return trimmed;
1201
+ }
1202
+ function dedupeOverrides(overrides) {
1203
+ return Array.from(new Set(overrides.filter((override) => override.trim().length > 0)));
1204
+ }
1205
+ function collectConfigOverridesFromEnv(env = process.env) {
1206
+ const overrides = [];
1207
+ for (const key of CONFIG_OVERRIDE_ENV_KEYS) {
1208
+ const raw = env[key];
1209
+ if (!raw) {
1210
+ continue;
1211
+ }
1212
+ for (const value of splitDelegationConfigOverrides(raw)) {
1213
+ overrides.push({ source: 'env', value });
1214
+ }
1215
+ }
1216
+ return overrides;
1217
+ }
1218
+ function buildConfigOverrideLayers(overrides) {
1219
+ const layers = [];
1220
+ for (const override of overrides) {
1221
+ try {
1222
+ const parsed = parseDelegationConfigOverride(override.value, override.source);
1223
+ if (parsed) {
1224
+ layers.push(parsed);
1225
+ }
1226
+ }
1227
+ catch (error) {
1228
+ logger.warn(`Invalid delegation config override (${override.source}): ${error?.message ?? String(error)}`);
1229
+ }
1230
+ }
1231
+ return layers;
1232
+ }
1233
+ function resolveParentManifestPath(input, allowedRoots) {
1234
+ const envPath = process.env.CODEX_DELEGATION_PARENT_MANIFEST_PATH?.trim();
1235
+ const rawPath = envPath ?? readStringValue(input, 'parent_manifest_path', 'parentManifestPath');
1236
+ if (!rawPath) {
1237
+ return null;
1238
+ }
1239
+ return resolveRunManifestPath(rawPath, allowedRoots, 'parent_manifest_path');
1240
+ }
1241
+ function resolveManifestPath(value, allowedRoots) {
1242
+ const raw = requireString(value, 'manifest_path');
1243
+ return resolveRunManifestPath(raw, allowedRoots, 'manifest_path');
1244
+ }
1245
+ export function resolveRunManifestPath(rawPath, allowedRoots, label = 'manifest_path') {
1246
+ const resolved = resolve(rawPath);
1247
+ assertRunManifestPath(resolved, label);
1248
+ if (allowedRoots && !isPathWithinRoots(resolved, allowedRoots)) {
1249
+ throw new Error(`${label} not permitted`);
1250
+ }
1251
+ return resolved;
1252
+ }
1253
+ function assertRunManifestPath(pathname, label) {
1254
+ if (basename(pathname) !== 'manifest.json') {
1255
+ throw new Error(`${label} invalid`);
1256
+ }
1257
+ const runDir = dirname(pathname);
1258
+ const cliDir = dirname(runDir);
1259
+ if (basename(cliDir) !== 'cli') {
1260
+ throw new Error(`${label} invalid`);
1261
+ }
1262
+ const taskDir = dirname(cliDir);
1263
+ const runsDir = dirname(taskDir);
1264
+ if (basename(runsDir) !== '.runs') {
1265
+ throw new Error(`${label} invalid`);
1266
+ }
1267
+ if (!basename(runDir) || !basename(taskDir)) {
1268
+ throw new Error(`${label} invalid`);
1269
+ }
1270
+ }
1271
+ function isPathWithinRoots(pathname, roots) {
1272
+ const resolved = normalizePath(realpathSafe(pathname));
1273
+ return roots.some((root) => {
1274
+ const resolvedRoot = normalizePath(realpathSafe(root));
1275
+ if (resolvedRoot === resolved) {
1276
+ return true;
1277
+ }
1278
+ const relativePath = relative(resolvedRoot, resolved);
1279
+ if (!relativePath) {
1280
+ return true;
1281
+ }
1282
+ if (isAbsolute(relativePath)) {
1283
+ return false;
1284
+ }
1285
+ return !relativePath.startsWith(`..${sep}`) && relativePath !== '..';
1286
+ });
1287
+ }
1288
+ function realpathSafe(pathname) {
1289
+ try {
1290
+ return realpathSync(pathname);
1291
+ }
1292
+ catch {
1293
+ return resolve(pathname);
1294
+ }
1295
+ }
1296
+ function normalizePath(pathname) {
1297
+ return process.platform === 'win32' ? pathname.toLowerCase() : pathname;
1298
+ }
1299
+ function resolveSpawnManifestPath(manifestPath, repoRoot, allowedRoots) {
1300
+ if (!manifestPath) {
1301
+ return null;
1302
+ }
1303
+ const resolved = isAbsolute(manifestPath) ? manifestPath : resolve(repoRoot, manifestPath);
1304
+ try {
1305
+ assertRunManifestPath(resolved, 'manifest_path');
1306
+ if (allowedRoots && !isPathWithinRoots(resolved, allowedRoots)) {
1307
+ return null;
1308
+ }
1309
+ return resolved;
1310
+ }
1311
+ catch {
1312
+ return null;
1313
+ }
1314
+ }
1315
+ async function persistDelegationToken(manifestPath, token, info) {
1316
+ const tokenPath = resolve(dirname(manifestPath), DELEGATION_TOKEN_FILE);
1317
+ try {
1318
+ await writeJsonAtomic(tokenPath, {
1319
+ token,
1320
+ parent_run_id: info.parentRunId,
1321
+ child_run_id: info.childRunId,
1322
+ created_at: new Date().toISOString()
1323
+ });
1324
+ await chmod(tokenPath, 0o600).catch(() => undefined);
1325
+ }
1326
+ catch (error) {
1327
+ logger.warn(`Failed to persist delegation token: ${error?.message ?? error}`);
1328
+ }
1329
+ }
1330
+ async function isRunAwaitingQuestion(manifestPath, allowedRoots) {
1331
+ try {
1332
+ const resolvedManifest = resolveRunManifestPath(manifestPath, allowedRoots, 'manifest_path');
1333
+ const controlPath = resolve(dirname(resolvedManifest), 'control.json');
1334
+ const raw = await readFile(controlPath, 'utf8');
1335
+ const snapshot = safeJsonParse(raw);
1336
+ const latest = snapshot && snapshot.latest_action && typeof snapshot.latest_action === 'object'
1337
+ ? snapshot.latest_action
1338
+ : null;
1339
+ if (!latest) {
1340
+ return false;
1341
+ }
1342
+ return latest.action === 'pause' && latest.reason === 'awaiting_question_answer';
1343
+ }
1344
+ catch {
1345
+ return false;
1346
+ }
1347
+ }
1348
+ export async function applyQuestionFallback(fallback, allowedHosts, allowedRoots) {
1349
+ const manifestPath = process.env.CODEX_ORCHESTRATOR_MANIFEST_PATH;
1350
+ if (!manifestPath) {
1351
+ return;
1352
+ }
1353
+ const shouldResolve = await isRunAwaitingQuestion(manifestPath, allowedRoots);
1354
+ if (!shouldResolve) {
1355
+ return;
1356
+ }
1357
+ const action = fallback === 'pause' ? 'pause' : fallback === 'resume' ? 'resume' : 'fail';
1358
+ try {
1359
+ await callControlEndpoint(resolveRunManifestPath(manifestPath, allowedRoots, 'manifest_path'), '/control/action', {
1360
+ action,
1361
+ requested_by: 'delegate',
1362
+ reason: 'question_expired'
1363
+ }, undefined, { allowedHosts, allowedRoots });
1364
+ }
1365
+ catch {
1366
+ // ignore
1367
+ }
1368
+ }