ruvnet-brain 4.5.11 → 4.5.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.
@@ -1,16 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ const assertModelRoutingText = async text => (await import('./model-routing-defence.mjs')).assertModelRoutingText(text);
2
3
  // A controlled prompt boundary using native print/SDK controls, not the native terminal UI.
3
4
  import path from 'node:path';
4
5
  import fs from 'node:fs';
5
6
  import os from 'node:os';
6
7
  import crypto from 'node:crypto';
7
8
  import { spawn, execFileSync } from 'node:child_process';
8
- import { createInterface } from 'node:readline/promises';
9
+ import { createManagedTerminal } from './managed-terminal-input.mjs';
9
10
  import { pathToFileURL } from 'node:url';
10
11
  import { StringDecoder } from 'node:string_decoder';
11
12
  import { decideNativeTurn, appendGatewayReceipt } from './model-routing-gateway.mjs';
12
13
  import { subscriptionEnvironment, assertSubscriptionAuth, validateDispatchDecision } from './model-router-dispatch.mjs';
13
14
  import { validateClaudeTerminalSettings } from './model-terminal-launchers.mjs';
15
+ import { runManagedPrompt } from './model-managed-prompt.mjs';
14
16
 
15
17
  const REFUSED = 'Controlled Claude turn refused; no fallback.';
16
18
  const uuid = value => /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i.test(value || '');
@@ -91,11 +93,11 @@ export function retireControlledClaudeChild(child, { graceMs = 200, killMs = 200
91
93
  });
92
94
  }
93
95
 
94
- export function controlledClaudeArguments(decision, sessionId, resume = false) {
96
+ export function controlledClaudeArguments(decision, sessionId, resume = false, responseSchema) {
95
97
  if (decision?.harness !== 'claude-code' || decision.provider !== 'anthropic' || decision.subscriptionCovered !== true ||
96
98
  !/^claude-[a-z0-9][a-z0-9.-]*$/.test(decision.model || '') ||
97
99
  !['low', 'medium', 'high', 'xhigh', 'max'].includes(decision.effort) || !uuid(sessionId)) throw new Error(REFUSED);
98
- return ['--print', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose',
100
+ return [...(responseSchema ? ['--json-schema', JSON.stringify(responseSchema)] : []), '--print', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose',
99
101
  '--model', decision.model, '--effort', decision.effort, '--permission-mode', 'manual', '--permission-prompts', 'host',
100
102
  '--permission-prompt-tool', 'stdio',
101
103
  '--disallowedTools', 'Agent,Task',
@@ -103,14 +105,20 @@ export function controlledClaudeArguments(decision, sessionId, resume = false) {
103
105
  }
104
106
 
105
107
  /** One native process per routed turn. Permission requests require an explicit host answer. */
106
- export async function runControlledClaudeTurn({ binary, prompt, sessionId = crypto.randomUUID(), resume = false,
108
+ export async function runControlledClaudeTurn({ binary, prompt, decisionPrompt = prompt, sessionId = crypto.randomUUID(), resume = false,
107
109
  cwd = process.cwd(), env = process.env, decide = decideNativeTurn, verifyDecision = validateDispatchDecision,
108
110
  checkSettings = validateClaudeTerminalSettings, checkAuth = assertSubscriptionAuth, spawnNative = spawn,
109
111
  probe = execFileSync, approve = async () => false, output = () => {}, receipt = appendGatewayReceipt,
110
- checkModules = assertClaudeModuleBoundary, signal, timeoutMs = 900000, handshakeMs = 10000 } = {}) {
112
+ checkModules = assertClaudeModuleBoundary, responseSchema, validateStructuredOutput, signal, timeoutMs = 900000, handshakeMs = 10000 } = {}) {
111
113
  if (!path.isAbsolute(binary || '') || typeof prompt !== 'string' || !prompt.trim() || prompt.length > 200000 ||
112
114
  /^\s*\//.test(prompt) || !Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 900000 ||
113
115
  !Number.isSafeInteger(handshakeMs) || handshakeMs <= 0 || handshakeMs > 30000 || signal?.aborted) throw new Error(REFUSED);
116
+ if (responseSchema && (responseSchema.type !== 'object' || typeof validateStructuredOutput !== 'function'
117
+ || Buffer.byteLength(JSON.stringify(responseSchema)) > 65536)) throw new Error(REFUSED);
118
+ const limit = performance.now() + timeoutMs;
119
+ const expired = () => signal?.aborted || performance.now() >= limit;
120
+ await assertModelRoutingText(prompt);
121
+ if (expired()) throw new Error(REFUSED);
114
122
  // Extra body / effort env overrides can bypass the native CLI's requested allocation.
115
123
  if (Object.keys(env).some(key => allocationEnv.test(key))) throw new Error(REFUSED);
116
124
  const clean = subscriptionEnvironment(env);
@@ -118,17 +126,20 @@ export async function runControlledClaudeTurn({ binary, prompt, sessionId = cryp
118
126
  if (fs.existsSync(managed)) throw new Error('Managed Claude settings require separate module-boundary qualification.');
119
127
  checkSettings({ env: clean, cwd, home: env.HOME || os.homedir() });
120
128
  checkAuth('claude-code', { env: clean, probe: (_name, args, options) => probe(binary, args, options) });
121
- const decision = await decide(prompt, 'claude-code', { env: clean });
129
+ const decision = await decide(decisionPrompt, 'claude-code', { env: clean });
122
130
  verifyDecision(decision);
123
- const args = controlledClaudeArguments(decision, sessionId, resume);
124
- if (signal?.aborted) throw new Error(REFUSED);
131
+ if (expired()) throw new Error(REFUSED);
132
+ const args = controlledClaudeArguments(decision, sessionId, resume, responseSchema);
133
+ if (expired()) throw new Error(REFUSED);
125
134
  const child = spawnNative(binary, args, { cwd, env: clean, shell: false, stdio: ['pipe', 'pipe', 'pipe'] });
126
135
  return new Promise((resolve, reject) => {
127
136
  let done = false, phase = 'initialize', buffer = '', bytes = 0, result, observed = false, permissions = false;
137
+ const assistantText = [];
138
+ let structuredAnswer;
128
139
  const decoder = new StringDecoder('utf8');
129
140
  const ids = { initialize: crypto.randomUUID(), before: crypto.randomUUID(), after: crypto.randomUUID() };
130
141
  let handshake;
131
- const timer = setTimeout(() => fail(), timeoutMs);
142
+ const timer = setTimeout(() => fail(), Math.max(1, limit - performance.now()));
132
143
  const abort = () => fail();
133
144
  signal?.addEventListener('abort', abort, { once: true });
134
145
  const clear = () => { clearTimeout(timer); clearTimeout(handshake); signal?.removeEventListener('abort', abort); };
@@ -137,7 +148,7 @@ export async function runControlledClaudeTurn({ binary, prompt, sessionId = cryp
137
148
  done = true; clear();
138
149
  void retireControlledClaudeChild(child).then(() => reject(new Error(REFUSED)));
139
150
  };
140
- const send = message => { if (!done) child.stdin.write(JSON.stringify(message) + '\n'); };
151
+ const send = message => { if (done) return; if (expired()) return fail(); child.stdin.write(JSON.stringify(message) + '\n'); };
141
152
  const control = which => {
142
153
  phase = which; clearTimeout(handshake); handshake = setTimeout(fail, handshakeMs);
143
154
  send({ type: 'control_request', request_id: ids[which], request: { subtype: which === 'initialize' ? 'initialize' : 'get_settings' } });
@@ -146,6 +157,7 @@ export async function runControlledClaudeTurn({ binary, prompt, sessionId = cryp
146
157
  Array.isArray(value.sources) && !value.sources.some(source => source.source === 'policySettings' && Object.keys(source.settings || {}).length);
147
158
  const onMessage = async message => {
148
159
  if (done) return;
160
+ if (expired()) return fail();
149
161
  if (message.type === 'control_response') {
150
162
  const response = message.response;
151
163
  if (response?.request_id !== ids[phase] || response.subtype !== 'success') return fail();
@@ -168,7 +180,17 @@ export async function runControlledClaudeTurn({ binary, prompt, sessionId = cryp
168
180
  if (['Agent', 'Task'].includes(message.request.tool_name)) return fail();
169
181
  permissions = true;
170
182
  let allowed = false;
171
- try { allowed = await approve(message.request) === true; } catch { allowed = false; }
183
+ try {
184
+ if (message.request.tool_name === 'StructuredOutput') {
185
+ const value = message.request.input;
186
+ const serialized = JSON.stringify(value);
187
+ if (responseSchema && value && typeof value === 'object' && !Array.isArray(value)
188
+ && Buffer.byteLength(serialized) <= 1024 * 1024 && validateStructuredOutput(value) === true) {
189
+ await assertModelRoutingText(serialized);
190
+ allowed = !expired();
191
+ }
192
+ } else allowed = await approve(message.request) === true;
193
+ } catch { allowed = false; }
172
194
  permissions = false;
173
195
  send({ type: 'control_response', response: { subtype: 'success', request_id: message.request_id,
174
196
  response: allowed ? { behavior: 'allow', updatedInput: message.request.input } :
@@ -177,13 +199,21 @@ export async function runControlledClaudeTurn({ binary, prompt, sessionId = cryp
177
199
  }
178
200
  if (message.session_id && message.session_id !== sessionId) return fail();
179
201
  if (message.type === 'assistant' && !message.parent_tool_use_id) {
180
- if (phase !== 'turn' || message.message?.model !== decision.model) return fail();
202
+ if (phase !== 'turn' || message.session_id !== sessionId || message.message?.model !== decision.model) return fail();
181
203
  observed = true;
182
- for (const block of message.message.content || []) if (block.type === 'text') output(cleanText(block.text));
204
+ for (const block of message.message.content || []) if (block.type === 'text') assistantText.push(cleanText(block.text));
183
205
  }
184
206
  if (message.type === 'result') {
185
207
  if (phase !== 'turn' || permissions || !observed || message.session_id !== sessionId || message.subtype !== 'success' ||
186
- message.is_error || message.permission_denials?.length || message.errors?.length) return fail();
208
+ message.is_error !== false || message.permission_denials?.length || message.errors?.length) return fail();
209
+ if (responseSchema) {
210
+ try {
211
+ const value = message.structured_output;
212
+ if (!value || typeof value !== 'object' || Array.isArray(value) || validateStructuredOutput(value) !== true) return fail();
213
+ structuredAnswer = JSON.stringify(value);
214
+ if (Buffer.byteLength(structuredAnswer) > 1024 * 1024) return fail();
215
+ } catch { return fail(); }
216
+ } else if (typeof message.result !== 'string' || !message.result.trim()) return fail();
187
217
  result = message; control('after');
188
218
  }
189
219
  };
@@ -199,14 +229,21 @@ export async function runControlledClaudeTurn({ binary, prompt, sessionId = cryp
199
229
  });
200
230
  child.stderr.on('data', () => {}); // Native diagnostics may contain credentials or prompt text.
201
231
  child.stdin.on('error', fail); child.once('error', fail);
202
- child.once('close', code => {
232
+ child.once('close', async code => {
203
233
  if (done) return;
204
234
  if (code !== 0 || phase !== 'exit' || !result || buffer.trim() || decoder.end()) return fail();
205
235
  try {
236
+ await assertModelRoutingText(assistantText.join('\n'));
237
+ await assertModelRoutingText(responseSchema ? structuredAnswer : result.result);
238
+ if (done || expired()) return fail();
239
+ for (const text of assistantText) { if (expired()) return fail(); output(text); }
240
+ if (expired()) return fail();
206
241
  receipt({ ts: new Date().toISOString(), harness: 'claude-code', status: 'completed', model: decision.model,
207
- effort: decision.effort, taskClass: decision.taskClass, modelObserved: true,
242
+ effort: decision.effort, taskClass: decision.taskClass, modelObserved: true, outputFormat: responseSchema ? 'json-schema' : 'text',
243
+ nativeSchemaRetries: responseSchema ? 'not-observed' : undefined,
208
244
  evidence: 'assistant model observed; get_settings applied effort matched before and after turn; per-request effort not exposed' }, { env: clean });
209
- done = true; clear(); resolve({ sessionId, decision, modelObserved: true, effortSettingsObserved: true, perRequestEffortObserved: false });
245
+ done = true; clear(); resolve({ sessionId, decision, finalAnswer: responseSchema ? structuredAnswer : result.result, structuredOutput: Boolean(responseSchema),
246
+ nativeSchemaRetries: responseSchema ? 'not-observed' : undefined, modelObserved: true, effortSettingsObserved: true, perRequestEffortObserved: false });
210
247
  } catch { fail(); }
211
248
  });
212
249
  control('initialize');
@@ -215,7 +252,8 @@ export async function runControlledClaudeTurn({ binary, prompt, sessionId = cryp
215
252
 
216
253
  /** Native tool approvals are presented by this host; --print supplies no terminal dialogs. */
217
254
  export async function launchControlledClaudeTerminal({ binary, args = [], input = process.stdin, output = process.stdout,
218
- diagnostics = process.stderr, env = process.env, cwd = process.cwd(), runTurn = runControlledClaudeTurn } = {}) {
255
+ diagnostics = process.stderr, env = process.env, cwd = process.cwd(), signalSource = process, runTurn = runControlledClaudeTurn,
256
+ managedPrompt = runManagedPrompt } = {}) {
219
257
  let sessionId, resume = false, initialPrompt, ownerBypass = false;
220
258
  const remaining = [...args];
221
259
  const invalid = () => new Error('Controlled Claude accepts only --resume <session UUID>, --permission-mode bypassPermissions, and a literal initial prompt.');
@@ -230,29 +268,36 @@ export async function launchControlledClaudeTerminal({ binary, args = [], input
230
268
  if (remaining.some(arg => typeof arg !== 'string' || arg.startsWith('-'))) throw invalid();
231
269
  if (remaining.length) initialPrompt = remaining.join(' ');
232
270
  if (!input.isTTY || !output.isTTY) throw new Error('Controlled Claude requires a person at a terminal for prompts and approvals.');
233
- const terminal = createInterface({ input, output });
271
+ const terminal = createManagedTerminal({ input, output });
234
272
  const controller = new AbortController();
235
273
  const cancel = () => controller.abort();
236
274
  terminal.on('SIGINT', cancel);
275
+ for (const name of ['SIGINT', 'SIGTERM', 'SIGHUP']) signalSource.on(name, cancel);
237
276
  diagnostics.write('Controlled Claude: one reviewed native allocation per prompt; tool approvals are answered here. Native Agent/Task workers are disabled; use the managed dispatcher for independent child work. /exit closes.\n');
238
277
  if (ownerBypass) diagnostics.write('Owner permission bypass is active: native tool requests are approved by this host automatically. Agent/Task remain refused; routing and subscription guards remain active.\n');
239
278
  try {
240
279
  while (!controller.signal.aborted) {
241
- const prompt = initialPrompt ?? await terminal.question('Claude> '); initialPrompt = undefined;
280
+ const prompt = initialPrompt ?? await terminal.question('Claude> ', { signal: controller.signal }); initialPrompt = undefined;
242
281
  if (prompt.trim() === '/exit') break;
243
282
  if (!prompt.trim()) continue;
244
- const turn = await runTurn({ binary, prompt, sessionId, resume, cwd, env, signal: controller.signal,
283
+ const deadline = Date.now() + 900000;
284
+ const turn = await managedPrompt({ binary, originalPrompt: prompt, harness: 'claude-code', projectRoot: cwd, deadline,
285
+ nativeContext: { sessionId, resume }, permissions: { apiBilling: false, write: ownerBypass },
286
+ primaryTurn: runTurn, cwd, env, signal: controller.signal,
245
287
  output: text => output.write(text + '\n'), approve: async request => {
246
288
  if (['Agent', 'Task'].includes(request.tool_name)) return false;
247
289
  const details = cleanText(JSON.stringify({ tool: request.tool_name, input: request.input, reason: request.decision_reason }));
248
290
  output.write(`Native permission request (untrusted tool text):\n${details}\n`);
249
291
  if (ownerBypass) return true;
250
- return (await terminal.question('Approve this tool request? Type yes: ')).trim() === 'yes';
292
+ return (await terminal.question('Approve this tool request? Type yes: ', { signal: controller.signal, approval: true })).trim() === 'yes';
251
293
  } });
252
294
  sessionId = turn.sessionId; resume = true;
295
+ const actual = turn.modelObserved === true ? turn.decision : undefined;
296
+ const reviewer = turn.managedWorkflow?.executions?.find(item => item.workerId === 'independent-review');
297
+ if (actual?.model && actual?.effort) diagnostics.write(`Completed by ${cleanText(actual.model)} · applied ${cleanText(actual.effort)}${reviewer ? `; review ${cleanText(reviewer.observedModel)} · ${cleanText(reviewer.observedEffort)}` : ''}\n`);
253
298
  diagnostics.write(`Native session: ${sessionId} (resume with --resume ${sessionId})\n`);
254
299
  }
255
- } finally { terminal.close(); }
300
+ } finally { controller.abort(); terminal.close(); for (const name of ['SIGINT', 'SIGTERM', 'SIGHUP']) signalSource.removeListener(name, cancel); }
256
301
  }
257
302
 
258
303
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
@@ -0,0 +1,113 @@
1
+ // Controlled native exec/resume frontend. This is deliberately not the native Codex TUI.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { performance } from 'node:perf_hooks';
6
+ import { createManagedTerminal } from './managed-terminal-input.mjs';
7
+ import { runManagedPrompt } from './model-managed-prompt.mjs';
8
+ import { decideNativeTurn, appendGatewayReceipt } from './model-routing-gateway.mjs';
9
+ import { validateDispatchDecision, subscriptionEnvironment } from './model-router-dispatch.mjs';
10
+ import { classifyTerminalArguments } from './model-terminal-gateway.mjs';
11
+ import { executeCodexWorkflowWorker } from './model-routing-execution-adapters.mjs';
12
+
13
+ const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-8][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
14
+ const BYPASS = '--dangerously-bypass-approvals-and-sandbox';
15
+ const invalid = () => new Error('Controlled Codex accepts a literal prompt, resume <UUID>, -C/--cd <directory>, and explicit owner permission bypass. Unsupported flags are refused.');
16
+
17
+ export function parseCodexManagedArguments(args = [], cwd = process.cwd()) {
18
+ if (!Array.isArray(args) || args.some(value => typeof value !== 'string')) throw invalid();
19
+ if (classifyTerminalArguments(args) === 'admin') return { administrative: true, args: [...args], cwd };
20
+ let sessionId, ownerBypass = false, literal = false;
21
+ const prompts = [];
22
+ for (let index = 0; index < args.length; index++) {
23
+ const arg = args[index];
24
+ if (literal) { prompts.push(arg); continue; }
25
+ if (arg === '--') { literal = true; continue; }
26
+ if (arg === BYPASS && !ownerBypass) { ownerBypass = true; continue; }
27
+ if (['-C', '--cd'].includes(arg)) {
28
+ const directory = args[++index]; if (!directory || directory.startsWith('-')) throw invalid();
29
+ cwd = fs.realpathSync(path.resolve(cwd, directory)); if (!fs.statSync(cwd).isDirectory()) throw invalid(); continue;
30
+ }
31
+ if (arg === 'resume' && !prompts.length && !sessionId) {
32
+ sessionId = args[++index]; if (!UUID.test(sessionId || '')) throw invalid(); continue;
33
+ }
34
+ if (arg.startsWith('-') || ['fork', 'agents'].includes(arg) && !prompts.length) throw invalid();
35
+ prompts.push(arg);
36
+ }
37
+ return { administrative: false, sessionId, ownerBypass, cwd: fs.realpathSync(cwd), initialPrompt: prompts.length ? prompts.join(' ') : undefined };
38
+ }
39
+
40
+ /** Allocation is selected for every ordinary parent turn; native rollout evidence is authoritative. */
41
+ export async function runCodexManagedPrimaryTurn({ binary, prompt, decisionPrompt = prompt, sessionId, threadId, cwd = process.cwd(),
42
+ env = process.env, readOnly = true, signal, timeoutMs = 900000, decide = decideNativeTurn,
43
+ verifyDecision = validateDispatchDecision, executeNative = executeCodexWorkflowWorker, receipt = appendGatewayReceipt,
44
+ output = () => {}, monotonic = () => performance.now() } = {}) {
45
+ if (!path.isAbsolute(binary || '') || typeof prompt !== 'string' || !prompt.trim() || prompt.length > 200000 ||
46
+ /^\s*\//.test(prompt) || !Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 900000 || signal?.aborted) throw invalid();
47
+ const id = sessionId ?? threadId;
48
+ if (id !== undefined && !UUID.test(id)) throw invalid();
49
+ const limit = monotonic() + timeoutMs;
50
+ const live = () => { if (signal?.aborted || monotonic() >= limit) throw new Error('Controlled Codex cancelled or absolute deadline exceeded'); };
51
+ const clean = subscriptionEnvironment(env);
52
+ const decision = await decide(decisionPrompt, 'codex', { env: clean });
53
+ live(); verifyDecision(decision); live();
54
+ const result = await executeNative({ binary, decision, prompt, cwd, readOnly, signal, timeoutMs: Math.max(1, limit - monotonic()),
55
+ env: { ...clean, RNB_TERMINAL_LAUNCH_ACTIVE: '1' }, sessionId: id });
56
+ live();
57
+ if (result?.completed !== true || result.modelObserved !== true || result.effortSettingsObserved !== true ||
58
+ result.model !== decision.model || result.effort !== decision.effort || !UUID.test(result.sessionId || '') ||
59
+ id !== undefined && result.sessionId !== id) throw new Error('Controlled Codex native model, effort or parent session unproven');
60
+ verifyDecision(decision); live();
61
+ receipt({ ts: new Date().toISOString(), harness: 'codex', status: 'completed', model: result.model, effort: result.effort,
62
+ taskClass: decision.taskClass, modelObserved: true, serviceMode: 'standard',
63
+ evidence: 'native turn_context model/effort and exact parent session observed; recursive native agents disabled' }, { env: clean });
64
+ if (result.answer) output(result.answer);
65
+ return { ...result, decision };
66
+ }
67
+
68
+ async function administrative(binary, args, env, cwd, signalSource, spawnNative) {
69
+ const child = spawnNative(binary, args, { cwd, env: { ...subscriptionEnvironment(env), RNB_TERMINAL_LAUNCH_ACTIVE: '1' }, shell: false, stdio: 'inherit' });
70
+ const handlers = new Map(['SIGINT', 'SIGTERM', 'SIGHUP'].map(signal => [signal, () => { try { child.kill(signal); } catch {} }]));
71
+ handlers.forEach((handler, signal) => signalSource.on(signal, handler));
72
+ try {
73
+ return await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', (code, signal) => resolve({ code, signal })); });
74
+ } finally { handlers.forEach((handler, signal) => signalSource.removeListener(signal, handler)); }
75
+ }
76
+
77
+ export async function launchCodexManagedTerminal({ binary, args = [], input = process.stdin, output = process.stdout,
78
+ diagnostics = process.stderr, cwd = process.cwd(), env = process.env, signalSource = process,
79
+ primaryTurn = runCodexManagedPrimaryTurn, managedPrompt = runManagedPrompt, spawnNative = spawn } = {}) {
80
+ if (env.RNB_TERMINAL_LAUNCH_ACTIVE) throw new Error('Controlled Codex recursive terminal invocation refused');
81
+ if (!path.isAbsolute(binary || '')) throw new Error('Absolute native Codex binary required');
82
+ const parsed = parseCodexManagedArguments(args, cwd);
83
+ if (parsed.administrative) return administrative(binary, args, env, parsed.cwd, signalSource, spawnNative);
84
+ if (!input.isTTY || !output.isTTY) throw new Error('Controlled Codex requires a person at a terminal');
85
+ let sessionId = parsed.sessionId, resume = Boolean(sessionId), initialPrompt = parsed.initialPrompt;
86
+ const terminal = createManagedTerminal({ input, output }), controller = new AbortController();
87
+ const cancel = () => controller.abort();
88
+ terminal.on('SIGINT', cancel);
89
+ for (const name of ['SIGINT', 'SIGTERM', 'SIGHUP']) signalSource.on(name, cancel);
90
+ diagnostics.write('Controlled Codex: native exec/resume with automatic managed workflows; native TUI and recursive agents unavailable. /exit closes.\n');
91
+ if (parsed.ownerBypass) diagnostics.write('Explicit owner bypass authorizes guarded workspace writes; unrestricted native sandbox bypass is not forwarded.\n');
92
+ try {
93
+ while (!controller.signal.aborted) {
94
+ const prompt = initialPrompt ?? await terminal.question('Codex> ', { signal: controller.signal }); initialPrompt = undefined;
95
+ if (prompt.trim() === '/exit') break;
96
+ if (!prompt.trim()) continue;
97
+ const turn = await managedPrompt({ binary, originalPrompt: prompt, harness: 'codex', projectRoot: parsed.cwd,
98
+ nativeContext: { sessionId, resume }, permissions: { apiBilling: false, write: parsed.ownerBypass },
99
+ primaryTurn, readOnly: !parsed.ownerBypass, cwd: parsed.cwd, env, signal: controller.signal,
100
+ output: text => output.write(String(text).replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, '') + '\n') });
101
+ if (!UUID.test(turn?.sessionId || '')) throw new Error('Controlled Codex parent session unproven');
102
+ sessionId = turn.sessionId; resume = true;
103
+ const reviewer = turn.managedWorkflow?.executions?.find(item => item.workerId === 'independent-review');
104
+ const safe = value => String(value).replace(/[\x00-\x1f\x7f-\x9f]/g, '');
105
+ if (turn.modelObserved === true && turn.model && turn.effort) diagnostics.write(`Completed by ${safe(turn.model)} · ${safe(turn.effort)}${reviewer ? `; review ${safe(reviewer.observedModel)} · ${safe(reviewer.observedEffort)}` : ''}\n`);
106
+ diagnostics.write(`Native session: ${sessionId} (resume with resume ${sessionId})\n`);
107
+ }
108
+ return { code: 0, signal: null, sessionId };
109
+ } finally { controller.abort(); terminal.close(); for (const name of ['SIGINT', 'SIGTERM', 'SIGHUP']) signalSource.removeListener(name, cancel); }
110
+ }
111
+
112
+ // Canonical launcher API name; the controlled implementation is shared.
113
+ export const launchManagedCodexTerminal = launchCodexManagedTerminal;