@renseiai/agentfactory 0.8.21 → 0.8.23
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/src/config/repository-config.d.ts +3 -3
- package/dist/src/orchestrator/index.d.ts +1 -0
- package/dist/src/orchestrator/index.d.ts.map +1 -1
- package/dist/src/orchestrator/index.js +2 -0
- package/dist/src/orchestrator/null-issue-tracker-client.d.ts +34 -0
- package/dist/src/orchestrator/null-issue-tracker-client.d.ts.map +1 -0
- package/dist/src/orchestrator/null-issue-tracker-client.js +72 -0
- package/dist/src/orchestrator/orchestrator.d.ts +19 -0
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.js +134 -15
- package/dist/src/orchestrator/state-types.d.ts +3 -0
- package/dist/src/orchestrator/state-types.d.ts.map +1 -1
- package/dist/src/providers/codex-app-server-provider.d.ts +87 -0
- package/dist/src/providers/codex-app-server-provider.d.ts.map +1 -1
- package/dist/src/providers/codex-app-server-provider.integration.test.d.ts +14 -0
- package/dist/src/providers/codex-app-server-provider.integration.test.d.ts.map +1 -0
- package/dist/src/providers/codex-app-server-provider.integration.test.js +909 -0
- package/dist/src/providers/codex-app-server-provider.js +339 -52
- package/dist/src/providers/codex-app-server-provider.test.js +838 -10
- package/dist/src/providers/codex-provider.d.ts +2 -0
- package/dist/src/providers/codex-provider.d.ts.map +1 -1
- package/dist/src/providers/codex-provider.js +36 -6
- package/dist/src/providers/codex-provider.test.js +12 -3
- package/dist/src/providers/types.d.ts +17 -0
- package/dist/src/providers/types.d.ts.map +1 -1
- package/dist/src/workflow/workflow-types.d.ts +5 -5
- package/package.json +2 -2
|
@@ -23,14 +23,56 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { spawn } from 'child_process';
|
|
25
25
|
import { createInterface } from 'readline';
|
|
26
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
27
|
+
import { join } from 'path';
|
|
28
|
+
import { homedir } from 'os';
|
|
26
29
|
import { classifyTool } from '../tools/tool-category.js';
|
|
27
30
|
import { evaluateCommandApproval, evaluateFileChangeApproval, } from './codex-approval-bridge.js';
|
|
31
|
+
function isServerRequest(msg) {
|
|
32
|
+
return 'id' in msg && 'method' in msg;
|
|
33
|
+
}
|
|
28
34
|
function isResponse(msg) {
|
|
29
|
-
return 'id' in msg &&
|
|
35
|
+
return 'id' in msg && !('method' in msg);
|
|
30
36
|
}
|
|
31
37
|
function isNotification(msg) {
|
|
32
38
|
return 'method' in msg && !('id' in msg);
|
|
33
39
|
}
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Codex model mapping (SUP-1749)
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
export const CODEX_MODEL_MAP = {
|
|
44
|
+
'opus': 'gpt-5-codex',
|
|
45
|
+
'sonnet': 'gpt-5.2-codex',
|
|
46
|
+
'haiku': 'gpt-5.3-codex',
|
|
47
|
+
};
|
|
48
|
+
export const CODEX_DEFAULT_MODEL = 'gpt-5-codex';
|
|
49
|
+
export function resolveCodexModel(config) {
|
|
50
|
+
if (config.model)
|
|
51
|
+
return config.model;
|
|
52
|
+
const tier = config.env.CODEX_MODEL_TIER;
|
|
53
|
+
if (tier && CODEX_MODEL_MAP[tier])
|
|
54
|
+
return CODEX_MODEL_MAP[tier];
|
|
55
|
+
if (config.env.CODEX_MODEL)
|
|
56
|
+
return config.env.CODEX_MODEL;
|
|
57
|
+
return CODEX_DEFAULT_MODEL;
|
|
58
|
+
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Codex pricing and cost calculation (SUP-1750)
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
/** Codex pricing per 1M tokens (USD). Update when pricing changes. */
|
|
63
|
+
export const CODEX_PRICING = {
|
|
64
|
+
'gpt-5-codex': { input: 2.00, cachedInput: 0.50, output: 8.00 },
|
|
65
|
+
'gpt-5.2-codex': { input: 1.00, cachedInput: 0.25, output: 4.00 },
|
|
66
|
+
'gpt-5.3-codex': { input: 0.50, cachedInput: 0.125, output: 2.00 },
|
|
67
|
+
};
|
|
68
|
+
export const CODEX_DEFAULT_PRICING = CODEX_PRICING['gpt-5-codex'];
|
|
69
|
+
export function calculateCostUsd(inputTokens, cachedInputTokens, outputTokens, model) {
|
|
70
|
+
const pricing = (model && CODEX_PRICING[model]) || CODEX_DEFAULT_PRICING;
|
|
71
|
+
const freshInputTokens = Math.max(0, inputTokens - cachedInputTokens);
|
|
72
|
+
return ((freshInputTokens / 1_000_000) * pricing.input +
|
|
73
|
+
(cachedInputTokens / 1_000_000) * pricing.cachedInput +
|
|
74
|
+
(outputTokens / 1_000_000) * pricing.output);
|
|
75
|
+
}
|
|
34
76
|
/**
|
|
35
77
|
* Manages a single long-lived `codex app-server` process.
|
|
36
78
|
*
|
|
@@ -60,19 +102,109 @@ export class AppServerProcessManager {
|
|
|
60
102
|
this.cwd = options.cwd;
|
|
61
103
|
this.env = options.env || {};
|
|
62
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Get the PID file path for tracking the app-server process.
|
|
107
|
+
* Used for orphan detection on startup.
|
|
108
|
+
*/
|
|
109
|
+
static getPidFilePath() {
|
|
110
|
+
const dir = join(homedir(), '.agentfactory');
|
|
111
|
+
if (!existsSync(dir)) {
|
|
112
|
+
mkdirSync(dir, { recursive: true });
|
|
113
|
+
}
|
|
114
|
+
return join(dir, 'codex-app-server.pid');
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Kill any orphaned app-server process from a prior fleet run.
|
|
118
|
+
* Called before starting a new process to prevent resource leaks.
|
|
119
|
+
*/
|
|
120
|
+
static killOrphanedProcess() {
|
|
121
|
+
const pidFile = AppServerProcessManager.getPidFilePath();
|
|
122
|
+
if (!existsSync(pidFile))
|
|
123
|
+
return;
|
|
124
|
+
try {
|
|
125
|
+
const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
126
|
+
if (isNaN(pid)) {
|
|
127
|
+
unlinkSync(pidFile);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Check if the process is alive
|
|
131
|
+
try {
|
|
132
|
+
process.kill(pid, 0); // signal 0 = check existence
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Process is dead, just clean up the PID file
|
|
136
|
+
unlinkSync(pidFile);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Process is alive — kill it
|
|
140
|
+
console.error(`[CodexAppServer] Killing orphaned app-server process (PID ${pid})`);
|
|
141
|
+
try {
|
|
142
|
+
process.kill(pid, 'SIGTERM');
|
|
143
|
+
// Give it a moment, then force kill
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
try {
|
|
146
|
+
process.kill(pid, 0); // still alive?
|
|
147
|
+
process.kill(pid, 'SIGKILL');
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Already dead
|
|
151
|
+
}
|
|
152
|
+
}, 2000);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Kill failed, process may have already exited
|
|
156
|
+
}
|
|
157
|
+
unlinkSync(pidFile);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// PID file read/delete failed — ignore
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Write the current app-server PID to the PID file.
|
|
165
|
+
*/
|
|
166
|
+
writePidFile() {
|
|
167
|
+
if (!this.process?.pid)
|
|
168
|
+
return;
|
|
169
|
+
try {
|
|
170
|
+
writeFileSync(AppServerProcessManager.getPidFilePath(), String(this.process.pid));
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Best effort
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Remove the PID file on shutdown.
|
|
178
|
+
*/
|
|
179
|
+
static removePidFile() {
|
|
180
|
+
try {
|
|
181
|
+
const pidFile = AppServerProcessManager.getPidFilePath();
|
|
182
|
+
if (existsSync(pidFile)) {
|
|
183
|
+
unlinkSync(pidFile);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Best effort
|
|
188
|
+
}
|
|
189
|
+
}
|
|
63
190
|
/**
|
|
64
191
|
* Start the app-server process and complete the initialization handshake.
|
|
65
192
|
* Idempotent — returns immediately if already initialized.
|
|
193
|
+
* Kills any orphaned app-server from a prior fleet run before starting.
|
|
66
194
|
*/
|
|
67
195
|
async start() {
|
|
68
196
|
if (this.initialized && this.process && !this.process.killed) {
|
|
69
197
|
return;
|
|
70
198
|
}
|
|
199
|
+
// Kill orphaned app-server from prior fleet run
|
|
200
|
+
AppServerProcessManager.killOrphanedProcess();
|
|
71
201
|
this.process = spawn(this.codexBin, ['app-server'], {
|
|
72
202
|
cwd: this.cwd,
|
|
73
203
|
env: { ...process.env, ...this.env },
|
|
74
204
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
75
205
|
});
|
|
206
|
+
// Track PID for orphan detection on next startup
|
|
207
|
+
this.writePidFile();
|
|
76
208
|
this.readline = createInterface({ input: this.process.stdout });
|
|
77
209
|
// Parse incoming JSONL messages
|
|
78
210
|
this.readline.on('line', (line) => {
|
|
@@ -87,7 +219,12 @@ export class AppServerProcessManager {
|
|
|
87
219
|
// Non-JSON output — ignore
|
|
88
220
|
return;
|
|
89
221
|
}
|
|
90
|
-
if (
|
|
222
|
+
if (isServerRequest(msg)) {
|
|
223
|
+
// Server requests have both `id` and `method` — Codex expects a response.
|
|
224
|
+
// Approval requests (commandExecution/requestApproval, etc.) come as server requests.
|
|
225
|
+
this.handleServerRequest(msg);
|
|
226
|
+
}
|
|
227
|
+
else if (isResponse(msg)) {
|
|
91
228
|
this.handleResponse(msg);
|
|
92
229
|
}
|
|
93
230
|
else if (isNotification(msg)) {
|
|
@@ -106,6 +243,16 @@ export class AppServerProcessManager {
|
|
|
106
243
|
});
|
|
107
244
|
// Perform initialization handshake
|
|
108
245
|
await this.initialize();
|
|
246
|
+
// Discover available models (best effort — older servers may not support model/list)
|
|
247
|
+
try {
|
|
248
|
+
const models = await this.listModels();
|
|
249
|
+
if (models.length > 0) {
|
|
250
|
+
console.error(`[CodexAppServer] Available models: ${models.map(m => m.id).join(', ')}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
console.error('[CodexAppServer] model/list not supported by this server version');
|
|
255
|
+
}
|
|
109
256
|
}
|
|
110
257
|
/**
|
|
111
258
|
* JSON-RPC 2.0 initialization handshake:
|
|
@@ -174,6 +321,44 @@ export class AppServerProcessManager {
|
|
|
174
321
|
pending.resolve(response.result);
|
|
175
322
|
}
|
|
176
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Handle an incoming JSON-RPC server request (has both `id` and `method`).
|
|
326
|
+
* Codex sends approval requests as server requests that expect a response.
|
|
327
|
+
* Route to the thread listener (as a notification-like object) and store
|
|
328
|
+
* the request ID so the handle can respond.
|
|
329
|
+
*/
|
|
330
|
+
handleServerRequest(request) {
|
|
331
|
+
const threadId = request.params?.threadId;
|
|
332
|
+
console.error(`[CodexAppServer] Server request: ${request.method} (id=${request.id}, thread=${threadId ?? 'none'})`);
|
|
333
|
+
// Wrap as a notification-compatible object for the thread listener,
|
|
334
|
+
// but include the id so the handle can respond.
|
|
335
|
+
const notificationLike = {
|
|
336
|
+
method: request.method,
|
|
337
|
+
params: { ...request.params, _serverRequestId: request.id },
|
|
338
|
+
};
|
|
339
|
+
if (threadId) {
|
|
340
|
+
const listener = this.threadListeners.get(threadId);
|
|
341
|
+
if (listener) {
|
|
342
|
+
listener(notificationLike);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// No thread listener — auto-accept to avoid hanging
|
|
347
|
+
console.error(`[CodexAppServer] No thread listener for ${request.method} — auto-accepting`);
|
|
348
|
+
this.respondToServerRequest(request.id, { decision: 'acceptForSession' });
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Send a JSON-RPC response to a server request.
|
|
352
|
+
*/
|
|
353
|
+
respondToServerRequest(requestId, result) {
|
|
354
|
+
if (!this.process?.stdin?.writable) {
|
|
355
|
+
console.error('[CodexAppServer] Cannot respond to server request: stdin not writable');
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
console.error(`[CodexAppServer] Responding to server request ${requestId}: ${JSON.stringify(result)}`);
|
|
359
|
+
const response = JSON.stringify({ jsonrpc: '2.0', id: requestId, result });
|
|
360
|
+
this.process.stdin.write(response + '\n');
|
|
361
|
+
}
|
|
177
362
|
/**
|
|
178
363
|
* Handle an incoming JSON-RPC notification.
|
|
179
364
|
* Routes to the appropriate thread listener based on threadId in params.
|
|
@@ -236,8 +421,8 @@ export class AppServerProcessManager {
|
|
|
236
421
|
}
|
|
237
422
|
try {
|
|
238
423
|
await this.request('config/batchWrite', {
|
|
239
|
-
|
|
240
|
-
{
|
|
424
|
+
edits: [
|
|
425
|
+
{ keyPath: 'mcpServers', mergeStrategy: 'replace', value: mcpServers },
|
|
241
426
|
],
|
|
242
427
|
});
|
|
243
428
|
this.mcpConfigured = true;
|
|
@@ -264,6 +449,13 @@ export class AppServerProcessManager {
|
|
|
264
449
|
return [];
|
|
265
450
|
}
|
|
266
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Discover available models from the app-server via model/list.
|
|
454
|
+
*/
|
|
455
|
+
async listModels() {
|
|
456
|
+
const result = await this.request('model/list', {});
|
|
457
|
+
return result?.models ?? [];
|
|
458
|
+
}
|
|
267
459
|
/**
|
|
268
460
|
* Get the PID of the app-server process.
|
|
269
461
|
*/
|
|
@@ -281,6 +473,8 @@ export class AppServerProcessManager {
|
|
|
281
473
|
}
|
|
282
474
|
async performShutdown() {
|
|
283
475
|
this.initialized = false;
|
|
476
|
+
// Remove PID file before killing process
|
|
477
|
+
AppServerProcessManager.removePidFile();
|
|
284
478
|
// Clear all pending requests
|
|
285
479
|
this.rejectAllPending(new Error('App server shutting down'));
|
|
286
480
|
// Kill the process
|
|
@@ -359,6 +553,7 @@ export function mapAppServerNotification(notification, state) {
|
|
|
359
553
|
if (turn?.usage) {
|
|
360
554
|
state.totalInputTokens += turn.usage.input_tokens ?? 0;
|
|
361
555
|
state.totalOutputTokens += turn.usage.output_tokens ?? 0;
|
|
556
|
+
state.totalCachedInputTokens += turn.usage.cached_input_tokens ?? 0;
|
|
362
557
|
}
|
|
363
558
|
if (turnStatus === 'completed') {
|
|
364
559
|
return [{
|
|
@@ -367,6 +562,8 @@ export function mapAppServerNotification(notification, state) {
|
|
|
367
562
|
cost: {
|
|
368
563
|
inputTokens: state.totalInputTokens || undefined,
|
|
369
564
|
outputTokens: state.totalOutputTokens || undefined,
|
|
565
|
+
cachedInputTokens: state.totalCachedInputTokens || undefined,
|
|
566
|
+
totalCostUsd: calculateCostUsd(state.totalInputTokens, state.totalCachedInputTokens, state.totalOutputTokens, state.model ?? undefined),
|
|
370
567
|
numTurns: state.turnCount || undefined,
|
|
371
568
|
},
|
|
372
569
|
raw: notification,
|
|
@@ -381,6 +578,8 @@ export function mapAppServerNotification(notification, state) {
|
|
|
381
578
|
cost: {
|
|
382
579
|
inputTokens: state.totalInputTokens || undefined,
|
|
383
580
|
outputTokens: state.totalOutputTokens || undefined,
|
|
581
|
+
cachedInputTokens: state.totalCachedInputTokens || undefined,
|
|
582
|
+
totalCostUsd: calculateCostUsd(state.totalInputTokens, state.totalCachedInputTokens, state.totalOutputTokens, state.model ?? undefined),
|
|
384
583
|
numTurns: state.turnCount || undefined,
|
|
385
584
|
},
|
|
386
585
|
raw: notification,
|
|
@@ -408,7 +607,7 @@ export function mapAppServerNotification(notification, state) {
|
|
|
408
607
|
return mapAppServerItemEvent(method, params);
|
|
409
608
|
// --- Item deltas (streaming) ---
|
|
410
609
|
case 'item/agentMessage/delta': {
|
|
411
|
-
const text = params.text;
|
|
610
|
+
const text = (params.delta ?? params.text);
|
|
412
611
|
if (text) {
|
|
413
612
|
return [{
|
|
414
613
|
type: 'assistant_text',
|
|
@@ -435,7 +634,7 @@ export function mapAppServerNotification(notification, state) {
|
|
|
435
634
|
return [{
|
|
436
635
|
type: 'system',
|
|
437
636
|
subtype: 'command_progress',
|
|
438
|
-
message: (params.delta ?? params.output) ?? '',
|
|
637
|
+
message: stripAnsi((params.delta ?? params.output) ?? ''),
|
|
439
638
|
raw: notification,
|
|
440
639
|
}];
|
|
441
640
|
// --- Turn diff/plan ---
|
|
@@ -462,6 +661,16 @@ export function mapAppServerNotification(notification, state) {
|
|
|
462
661
|
}];
|
|
463
662
|
}
|
|
464
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* Strip ANSI escape codes from text.
|
|
666
|
+
* Codex shell commands produce raw terminal output with color codes,
|
|
667
|
+
* cursor movement, etc. that pollute logs and activity tracking.
|
|
668
|
+
*/
|
|
669
|
+
// eslint-disable-next-line no-control-regex
|
|
670
|
+
const ANSI_PATTERN = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07|\x1b[()][AB012]|\x1b\[[\d;]*m/g;
|
|
671
|
+
function stripAnsi(text) {
|
|
672
|
+
return text.replace(ANSI_PATTERN, '');
|
|
673
|
+
}
|
|
465
674
|
/**
|
|
466
675
|
* Map item/started and item/completed notifications to AgentEvents.
|
|
467
676
|
* Exported for unit testing.
|
|
@@ -507,7 +716,7 @@ export function mapAppServerItemEvent(method, params) {
|
|
|
507
716
|
type: 'tool_result',
|
|
508
717
|
toolName: 'shell',
|
|
509
718
|
toolUseId: item.id,
|
|
510
|
-
content: item.text ?? '',
|
|
719
|
+
content: stripAnsi(item.text ?? ''),
|
|
511
720
|
isError: item.status === 'failed' || (item.exitCode !== undefined && item.exitCode !== 0),
|
|
512
721
|
raw: { method, params },
|
|
513
722
|
}];
|
|
@@ -607,20 +816,75 @@ export function normalizeMcpToolName(server, tool) {
|
|
|
607
816
|
// Resolve approval policy from AgentSpawnConfig
|
|
608
817
|
// ---------------------------------------------------------------------------
|
|
609
818
|
function resolveApprovalPolicy(config) {
|
|
610
|
-
// SUP-1747: Use '
|
|
819
|
+
// SUP-1747: Use 'on-request' for autonomous agents so all tool executions
|
|
611
820
|
// flow through the approval bridge for safety evaluation. The bridge
|
|
612
821
|
// auto-approves safe commands and declines destructive patterns.
|
|
822
|
+
// Codex v0.117+ uses kebab-case: 'on-request' | 'untrusted' | 'on-failure' | 'never'
|
|
613
823
|
if (config.autonomous)
|
|
614
|
-
return '
|
|
615
|
-
return '
|
|
824
|
+
return 'on-request';
|
|
825
|
+
return 'untrusted';
|
|
616
826
|
}
|
|
617
|
-
|
|
827
|
+
/**
|
|
828
|
+
* Map AgentSpawnConfig sandbox settings to Codex App Server sandbox policy.
|
|
829
|
+
*
|
|
830
|
+
* Codex sandbox levels vs Claude sandbox:
|
|
831
|
+
* | Feature | Claude | Codex |
|
|
832
|
+
* |-----------------------|-------------------------|--------------------------------|
|
|
833
|
+
* | File write control | Per-file glob patterns | Workspace root only |
|
|
834
|
+
* | Network access | Per-domain allow-lists | All-or-nothing per level |
|
|
835
|
+
* | Tool-level permissions| Per-tool allow/deny | Not supported (approval policy)|
|
|
836
|
+
* | Custom writable paths | Multiple glob patterns | Single writableRoots array |
|
|
837
|
+
* | Process isolation | macOS sandbox-exec | Docker/firewall container |
|
|
838
|
+
*
|
|
839
|
+
* Key limitation: Codex cannot restrict writes to specific subdirectories within
|
|
840
|
+
* the workspace or allow network access to specific domains. The mapping is intent-based:
|
|
841
|
+
* "safe browsing/analysis" → readOnly
|
|
842
|
+
* "normal development" → workspaceWrite
|
|
843
|
+
* "install/deploy/admin" → dangerFullAccess
|
|
844
|
+
*/
|
|
845
|
+
/**
|
|
846
|
+
* Resolve sandbox policy as an object for turn/start (supports writableRoots).
|
|
847
|
+
* Codex v0.117+ turn/start accepts: { type: 'workspaceWrite', writableRoots: [...] }
|
|
848
|
+
*
|
|
849
|
+
* Network access is enabled by default for agents because they need to run
|
|
850
|
+
* commands like `gh`, `curl`, `pnpm install`, etc. The sandbox still restricts
|
|
851
|
+
* file writes to the workspace root.
|
|
852
|
+
*/
|
|
853
|
+
export function resolveSandboxPolicy(config) {
|
|
854
|
+
if (config.sandboxLevel) {
|
|
855
|
+
switch (config.sandboxLevel) {
|
|
856
|
+
case 'read-only':
|
|
857
|
+
return { type: 'readOnly', networkAccess: true };
|
|
858
|
+
case 'workspace-write':
|
|
859
|
+
return { type: 'workspaceWrite', writableRoots: [config.cwd], networkAccess: true };
|
|
860
|
+
case 'full-access':
|
|
861
|
+
return { type: 'dangerFullAccess' };
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
// Fallback: boolean sandboxEnabled → workspaceWrite with network
|
|
618
865
|
if (!config.sandboxEnabled)
|
|
619
866
|
return undefined;
|
|
620
|
-
return {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
867
|
+
return { type: 'workspaceWrite', writableRoots: [config.cwd], networkAccess: true };
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Resolve sandbox mode as a simple string for thread/start.
|
|
871
|
+
* Codex v0.117+ thread/start accepts: 'read-only' | 'workspace-write' | 'danger-full-access'
|
|
872
|
+
*/
|
|
873
|
+
export function resolveSandboxMode(config) {
|
|
874
|
+
if (config.sandboxLevel) {
|
|
875
|
+
switch (config.sandboxLevel) {
|
|
876
|
+
case 'read-only':
|
|
877
|
+
return 'read-only';
|
|
878
|
+
case 'workspace-write':
|
|
879
|
+
return 'workspace-write';
|
|
880
|
+
case 'full-access':
|
|
881
|
+
return 'danger-full-access';
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
// Fallback: boolean sandboxEnabled → workspace-write
|
|
885
|
+
if (!config.sandboxEnabled)
|
|
886
|
+
return undefined;
|
|
887
|
+
return 'workspace-write';
|
|
624
888
|
}
|
|
625
889
|
// ---------------------------------------------------------------------------
|
|
626
890
|
// Base Instructions Builder (SUP-1746)
|
|
@@ -663,8 +927,10 @@ class AppServerAgentHandle {
|
|
|
663
927
|
resumeThreadId;
|
|
664
928
|
mapperState = {
|
|
665
929
|
sessionId: null,
|
|
930
|
+
model: null,
|
|
666
931
|
totalInputTokens: 0,
|
|
667
932
|
totalOutputTokens: 0,
|
|
933
|
+
totalCachedInputTokens: 0,
|
|
668
934
|
turnCount: 0,
|
|
669
935
|
};
|
|
670
936
|
activeTurnId = null;
|
|
@@ -673,6 +939,8 @@ class AppServerAgentHandle {
|
|
|
673
939
|
streamEnded = false;
|
|
674
940
|
/** True while we're waiting for a possible injected turn between turns */
|
|
675
941
|
awaitingInjection = false;
|
|
942
|
+
/** Accumulated assistant text for the result message (completion comment) */
|
|
943
|
+
accumulatedText = '';
|
|
676
944
|
constructor(processManager, config, resumeThreadId) {
|
|
677
945
|
this.processManager = processManager;
|
|
678
946
|
this.config = config;
|
|
@@ -725,7 +993,7 @@ class AppServerAgentHandle {
|
|
|
725
993
|
}
|
|
726
994
|
await this.processManager.request('turn/steer', {
|
|
727
995
|
threadId: this.sessionId,
|
|
728
|
-
|
|
996
|
+
expectedTurnId: this.activeTurnId,
|
|
729
997
|
input: [{ type: 'text', text }],
|
|
730
998
|
});
|
|
731
999
|
}
|
|
@@ -738,9 +1006,10 @@ class AppServerAgentHandle {
|
|
|
738
1006
|
*
|
|
739
1007
|
* Returns a system event if the request was declined, for observability.
|
|
740
1008
|
*/
|
|
741
|
-
|
|
1009
|
+
handleApprovalRequest(notification) {
|
|
742
1010
|
const params = notification.params ?? {};
|
|
743
|
-
|
|
1011
|
+
// Server requests pass _serverRequestId; fall back to requestId for backwards compat
|
|
1012
|
+
const serverRequestId = params._serverRequestId;
|
|
744
1013
|
const command = params.command;
|
|
745
1014
|
const filePath = params.filePath;
|
|
746
1015
|
let decision;
|
|
@@ -756,13 +1025,14 @@ class AppServerAgentHandle {
|
|
|
756
1025
|
// Unknown approval request — accept by default
|
|
757
1026
|
decision = { action: 'acceptForSession' };
|
|
758
1027
|
}
|
|
759
|
-
// Respond to the
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1028
|
+
// Respond to the server request with the approval decision.
|
|
1029
|
+
// Codex sends approval requests as JSON-RPC server requests (with `id`),
|
|
1030
|
+
// expecting a JSON-RPC response matching that id.
|
|
1031
|
+
if (serverRequestId != null) {
|
|
1032
|
+
this.processManager.respondToServerRequest(serverRequestId, {
|
|
1033
|
+
decision: decision.action,
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
766
1036
|
// Emit system event for declined approvals (observability)
|
|
767
1037
|
if (decision.action === 'decline') {
|
|
768
1038
|
const target = command ?? filePath ?? 'unknown';
|
|
@@ -789,9 +1059,7 @@ class AppServerAgentHandle {
|
|
|
789
1059
|
cwd: this.config.cwd,
|
|
790
1060
|
approvalPolicy: resolveApprovalPolicy(this.config),
|
|
791
1061
|
};
|
|
792
|
-
|
|
793
|
-
turnParams.maxTurns = this.config.maxTurns;
|
|
794
|
-
}
|
|
1062
|
+
turnParams.model = resolveCodexModel(this.config);
|
|
795
1063
|
const sandboxPolicy = resolveSandboxPolicy(this.config);
|
|
796
1064
|
if (sandboxPolicy) {
|
|
797
1065
|
turnParams.sandboxPolicy = sandboxPolicy;
|
|
@@ -818,7 +1086,7 @@ class AppServerAgentHandle {
|
|
|
818
1086
|
// Resume existing thread
|
|
819
1087
|
const result = await this.processManager.request('thread/resume', {
|
|
820
1088
|
threadId: this.resumeThreadId,
|
|
821
|
-
personality: '
|
|
1089
|
+
personality: 'pragmatic',
|
|
822
1090
|
});
|
|
823
1091
|
threadId = result?.thread?.id ?? this.resumeThreadId;
|
|
824
1092
|
}
|
|
@@ -829,15 +1097,17 @@ class AppServerAgentHandle {
|
|
|
829
1097
|
approvalPolicy: resolveApprovalPolicy(this.config),
|
|
830
1098
|
serviceName: 'agentfactory',
|
|
831
1099
|
};
|
|
832
|
-
// SUP-1746: Pass persistent system instructions via `
|
|
1100
|
+
// SUP-1746: Pass persistent system instructions via `baseInstructions` on thread/start.
|
|
833
1101
|
// Separates safety rules and project context from per-turn task input.
|
|
834
1102
|
const instructions = buildBaseInstructions(this.config);
|
|
835
1103
|
if (instructions) {
|
|
836
|
-
threadParams.
|
|
1104
|
+
threadParams.baseInstructions = instructions;
|
|
837
1105
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1106
|
+
threadParams.model = resolveCodexModel(this.config);
|
|
1107
|
+
// thread/start uses simple string sandbox mode (not object like turn/start)
|
|
1108
|
+
const sandboxMode = resolveSandboxMode(this.config);
|
|
1109
|
+
if (sandboxMode) {
|
|
1110
|
+
threadParams.sandbox = sandboxMode;
|
|
841
1111
|
}
|
|
842
1112
|
const result = await this.processManager.request('thread/start', threadParams);
|
|
843
1113
|
threadId = result?.thread?.id ?? '';
|
|
@@ -852,6 +1122,7 @@ class AppServerAgentHandle {
|
|
|
852
1122
|
}
|
|
853
1123
|
this.sessionId = threadId;
|
|
854
1124
|
this.mapperState.sessionId = threadId;
|
|
1125
|
+
this.mapperState.model = resolveCodexModel(this.config);
|
|
855
1126
|
// Subscribe to thread notifications
|
|
856
1127
|
this.processManager.subscribeThread(threadId, (notification) => {
|
|
857
1128
|
this.notificationQueue.push(notification);
|
|
@@ -873,9 +1144,7 @@ class AppServerAgentHandle {
|
|
|
873
1144
|
cwd: this.config.cwd,
|
|
874
1145
|
approvalPolicy: resolveApprovalPolicy(this.config),
|
|
875
1146
|
};
|
|
876
|
-
|
|
877
|
-
turnParams.maxTurns = this.config.maxTurns;
|
|
878
|
-
}
|
|
1147
|
+
turnParams.model = resolveCodexModel(this.config);
|
|
879
1148
|
const sandboxPolicy = resolveSandboxPolicy(this.config);
|
|
880
1149
|
if (sandboxPolicy) {
|
|
881
1150
|
turnParams.sandboxPolicy = sandboxPolicy;
|
|
@@ -905,9 +1174,11 @@ class AppServerAgentHandle {
|
|
|
905
1174
|
while (this.notificationQueue.length > 0) {
|
|
906
1175
|
const notification = this.notificationQueue.shift();
|
|
907
1176
|
// SUP-1747: Intercept approval requests before other processing.
|
|
908
|
-
//
|
|
909
|
-
|
|
910
|
-
|
|
1177
|
+
// Codex sends approvals as server requests with methods like:
|
|
1178
|
+
// item/commandExecution/requestApproval, item/fileChange/requestApproval,
|
|
1179
|
+
// item/permissions/requestApproval, applyPatchApproval, execCommandApproval
|
|
1180
|
+
if (notification.method.includes('pproval') || notification.method.includes('requestApproval')) {
|
|
1181
|
+
const deniedEvent = this.handleApprovalRequest(notification);
|
|
911
1182
|
if (deniedEvent) {
|
|
912
1183
|
yield deniedEvent;
|
|
913
1184
|
}
|
|
@@ -928,18 +1199,31 @@ class AppServerAgentHandle {
|
|
|
928
1199
|
}
|
|
929
1200
|
const events = mapAppServerNotification(notification, this.mapperState);
|
|
930
1201
|
for (const event of events) {
|
|
931
|
-
// Intercept turn/completed result events
|
|
932
|
-
//
|
|
933
|
-
//
|
|
1202
|
+
// Intercept turn/completed result events.
|
|
1203
|
+
// In autonomous mode (fleet), emit the result directly to end the session.
|
|
1204
|
+
// In interactive mode, convert to system event to keep the stream alive
|
|
1205
|
+
// for potential message injection.
|
|
1206
|
+
// Accumulate assistant text for the result message / completion comment
|
|
1207
|
+
if (event.type === 'assistant_text' && event.text) {
|
|
1208
|
+
this.accumulatedText += event.text;
|
|
1209
|
+
}
|
|
934
1210
|
if (event.type === 'result') {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1211
|
+
if (this.config.autonomous) {
|
|
1212
|
+
// Autonomous: emit result with accumulated text and end stream
|
|
1213
|
+
yield { ...event, message: this.accumulatedText.trim() || undefined };
|
|
1214
|
+
this.streamEnded = true;
|
|
1215
|
+
}
|
|
1216
|
+
else {
|
|
1217
|
+
// Interactive: keep stream alive for injection
|
|
1218
|
+
lastTurnSuccess = event.success;
|
|
1219
|
+
lastTurnErrors = event.errors;
|
|
1220
|
+
yield {
|
|
1221
|
+
type: 'system',
|
|
1222
|
+
subtype: 'turn_result',
|
|
1223
|
+
message: `Turn ${event.success ? 'succeeded' : 'failed'}${event.errors?.length ? ': ' + event.errors[0] : ''}`,
|
|
1224
|
+
raw: event.raw,
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
943
1227
|
}
|
|
944
1228
|
else {
|
|
945
1229
|
yield event;
|
|
@@ -955,14 +1239,17 @@ class AppServerAgentHandle {
|
|
|
955
1239
|
catch {
|
|
956
1240
|
// Best effort
|
|
957
1241
|
}
|
|
958
|
-
// Emit the final result event when the stream ends
|
|
1242
|
+
// Emit the final result event when the stream ends (interactive mode / stop)
|
|
959
1243
|
yield {
|
|
960
1244
|
type: 'result',
|
|
961
1245
|
success: lastTurnSuccess,
|
|
1246
|
+
message: this.accumulatedText.trim() || undefined,
|
|
962
1247
|
errors: lastTurnErrors,
|
|
963
1248
|
cost: {
|
|
964
1249
|
inputTokens: this.mapperState.totalInputTokens || undefined,
|
|
965
1250
|
outputTokens: this.mapperState.totalOutputTokens || undefined,
|
|
1251
|
+
cachedInputTokens: this.mapperState.totalCachedInputTokens || undefined,
|
|
1252
|
+
totalCostUsd: calculateCostUsd(this.mapperState.totalInputTokens, this.mapperState.totalCachedInputTokens, this.mapperState.totalOutputTokens, this.mapperState.model ?? undefined),
|
|
966
1253
|
numTurns: this.mapperState.turnCount || undefined,
|
|
967
1254
|
},
|
|
968
1255
|
raw: null,
|