@wix/pathgrade 1.0.17 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/dist/agents/claude/sdk-message-projector.js +12 -4
- package/dist/agents/claude/tool-results.d.ts +1 -2
- package/dist/agents/claude/tool-results.js +9 -40
- package/dist/agents/claude.js +6 -4
- package/dist/agents/codex-app-server/agent.d.ts +5 -0
- package/dist/agents/codex-app-server/agent.js +145 -179
- package/dist/agents/codex-app-server/item-lifecycle.d.ts +30 -0
- package/dist/agents/codex-app-server/item-lifecycle.js +95 -0
- package/dist/agents/codex-app-server/item-projection.d.ts +62 -0
- package/dist/agents/codex-app-server/item-projection.js +135 -0
- package/dist/agents/codex-app-server/managed-auth.d.ts +12 -0
- package/dist/agents/codex-app-server/managed-auth.js +53 -0
- package/dist/agents/codex-app-server/transport.d.ts +2 -0
- package/dist/agents/codex-app-server/transport.js +30 -3
- package/dist/agents/opencode/host-safety.d.ts +3 -0
- package/dist/agents/opencode/host-safety.js +30 -0
- package/dist/agents/opencode.d.ts +2 -4
- package/dist/agents/opencode.js +47 -38
- package/dist/openai-oauth/chatgpt-oauth-llm.d.ts +25 -0
- package/dist/openai-oauth/chatgpt-oauth-llm.js +398 -0
- package/dist/openai-oauth/codex-auth-broker.d.ts +32 -0
- package/dist/openai-oauth/codex-auth-broker.js +110 -0
- package/dist/openai-oauth/index.d.ts +2 -0
- package/dist/openai-oauth/index.js +1 -0
- package/dist/providers/credentials.d.ts +4 -1
- package/dist/providers/credentials.js +4 -3
- package/dist/providers/sandbox.d.ts +2 -0
- package/dist/providers/scripted-mcp-mock-host.js +6 -3
- package/dist/providers/workspace.d.ts +1 -0
- package/dist/providers/workspace.js +9 -1
- package/dist/sdk/agent-result-log.js +4 -2
- package/dist/sdk/agent.js +7 -0
- package/dist/sdk/judge-tools.js +14 -3
- package/dist/sdk/managed-session.d.ts +2 -0
- package/dist/sdk/managed-session.js +22 -5
- package/dist/sdk/mcp-safety.js +2 -18
- package/dist/sdk/snapshots.d.ts +1 -0
- package/dist/sdk/snapshots.js +3 -2
- package/dist/sdk/tool-event-log.js +5 -2
- package/dist/sdk/tool-event-secrets.d.ts +4 -0
- package/dist/sdk/tool-event-secrets.js +14 -0
- package/dist/sdk/turn-result-secrets.d.ts +5 -0
- package/dist/sdk/turn-result-secrets.js +12 -0
- package/dist/tool-event-results.d.ts +10 -0
- package/dist/tool-event-results.js +171 -0
- package/dist/types.d.ts +2 -0
- package/dist/utils/llm.js +11 -0
- package/docs/OPENAI_OAUTH_JUDGE.md +91 -0
- package/package.json +13 -2
|
@@ -1,133 +1,22 @@
|
|
|
1
1
|
import { BaseAgent, getRuntimeEnv, getWorkspacePath, } from '../../types.js';
|
|
2
2
|
import { mountMcpForCodexAppServer } from '../../providers/mcp-runtime-mounting.js';
|
|
3
3
|
import { assertMcpSecretReferencesReady } from '../../providers/mcp-config.js';
|
|
4
|
-
import {
|
|
4
|
+
import { enrichSkillEvents, } from '../../tool-events.js';
|
|
5
|
+
import { collectSensitiveEnvValues, sanitizePersistenceValue, } from '../../tool-event-results.js';
|
|
5
6
|
import { requireAskBusForLiveBatches } from '../../sdk/ask-bus/bus.js';
|
|
7
|
+
import { attachTurnResultSensitiveValues } from '../../sdk/turn-result-secrets.js';
|
|
8
|
+
import { attachToolEventSensitiveValues } from '../../sdk/tool-event-secrets.js';
|
|
6
9
|
import { toAskUserToolEvent } from '../../sdk/ask-bus/projection.js';
|
|
7
10
|
import { decideMcpToolCall } from '../../sdk/mcp-safety.js';
|
|
8
|
-
import { attachOriginalMcpInput } from '../../sdk/mcp-event-input.js';
|
|
9
11
|
import { spawnAppServerTransport, } from './transport.js';
|
|
10
12
|
import { normalizeUpstreamQuestion, toWireAnswerMap, } from './wire-translators.js';
|
|
11
13
|
import { extractTurnCompletionFailure } from './turn-completion.js';
|
|
12
14
|
import { resolveCodexModel } from '../codex-model.js';
|
|
13
15
|
import { CodexMcpApprovalCorrelator, extractMcpToolApprovalRequest, hasScriptedApprovalPolicy, isMcpToolCallApprovalRequest, recordPolicyDeniedMcpToolCall } from './mcp-approval-correlator.js';
|
|
16
|
+
import { projectItemIntoTurn } from './item-projection.js';
|
|
17
|
+
import { CodexItemLifecycle, } from './item-lifecycle.js';
|
|
18
|
+
import { createCodexManagedAuth } from './managed-auth.js';
|
|
14
19
|
const TURN_COMPLETED_METHOD = 'turn/completed';
|
|
15
|
-
function recordFromUnknown(value) {
|
|
16
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
19
|
-
return {};
|
|
20
|
-
}
|
|
21
|
-
function extractCommandActionSkillName(action) {
|
|
22
|
-
if (typeof action.path === 'string') {
|
|
23
|
-
const direct = extractSkillNameFromPath(action.path);
|
|
24
|
-
if (direct)
|
|
25
|
-
return direct;
|
|
26
|
-
const embedded = extractSkillNameFromText(action.path);
|
|
27
|
-
if (embedded)
|
|
28
|
-
return embedded;
|
|
29
|
-
}
|
|
30
|
-
return typeof action.command === 'string' ? extractSkillNameFromText(action.command) : undefined;
|
|
31
|
-
}
|
|
32
|
-
function extractSkillNameFromText(value) {
|
|
33
|
-
if (!value)
|
|
34
|
-
return undefined;
|
|
35
|
-
return value.match(/(?:^|[/\s"'])\.(?:agents|claude)\/skills\/([^/\s"']+)\/SKILL\.md(?:$|[\s"'])/)?.[1];
|
|
36
|
-
}
|
|
37
|
-
function extractSkillPathFromText(value) {
|
|
38
|
-
if (!value)
|
|
39
|
-
return undefined;
|
|
40
|
-
return value.match(/(?:^|[\s"'])(?<path>(?:\/|\.{1,2}\/)?[^\s"']*(?:\.agents|\.claude)\/skills\/[^/\s"']+\/SKILL\.md)(?:$|[\s"'])/)?.groups?.path;
|
|
41
|
-
}
|
|
42
|
-
function projectItemIntoTurn(item, turn) {
|
|
43
|
-
if (item.type === 'agentMessage') {
|
|
44
|
-
const msg = item;
|
|
45
|
-
if (msg.text) {
|
|
46
|
-
turn.assistantMessageParts.push(msg.text);
|
|
47
|
-
}
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (item.type === 'commandExecution') {
|
|
51
|
-
const cmd = item;
|
|
52
|
-
const action = inferCodexExecAction(cmd.command);
|
|
53
|
-
const skillPath = extractSkillPathFromText(cmd.command);
|
|
54
|
-
const args = {
|
|
55
|
-
command: cmd.command,
|
|
56
|
-
...(skillPath ? { path: skillPath } : {}),
|
|
57
|
-
};
|
|
58
|
-
turn.nonAskToolEvents.push({
|
|
59
|
-
action,
|
|
60
|
-
provider: 'codex',
|
|
61
|
-
providerToolName: 'commandExecution',
|
|
62
|
-
turnNumber: turn.turnNumber,
|
|
63
|
-
arguments: args,
|
|
64
|
-
summary: buildSummary(action, 'commandExecution', args),
|
|
65
|
-
confidence: 'high',
|
|
66
|
-
rawSnippet: JSON.stringify(cmd),
|
|
67
|
-
});
|
|
68
|
-
const recordedSkills = new Set();
|
|
69
|
-
for (const action of cmd.commandActions ?? []) {
|
|
70
|
-
const skillName = extractCommandActionSkillName(action) ?? extractSkillNameFromText(cmd.command);
|
|
71
|
-
if (!skillName || recordedSkills.has(skillName))
|
|
72
|
-
continue;
|
|
73
|
-
recordedSkills.add(skillName);
|
|
74
|
-
turn.nonAskToolEvents.push({
|
|
75
|
-
action: 'use_skill',
|
|
76
|
-
provider: 'codex',
|
|
77
|
-
providerToolName: `commandExecution.commandActions.${action.type ?? 'unknown'}`,
|
|
78
|
-
turnNumber: turn.turnNumber,
|
|
79
|
-
arguments: { path: action.path, name: action.name },
|
|
80
|
-
summary: `use_skill ${skillName}`,
|
|
81
|
-
confidence: 'high',
|
|
82
|
-
rawSnippet: JSON.stringify(action),
|
|
83
|
-
skillName,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
if (item.type === 'fileChange') {
|
|
89
|
-
const fc = item;
|
|
90
|
-
for (const change of fc.changes ?? []) {
|
|
91
|
-
turn.nonAskToolEvents.push({
|
|
92
|
-
action: 'edit_file',
|
|
93
|
-
provider: 'codex',
|
|
94
|
-
providerToolName: 'fileChange',
|
|
95
|
-
turnNumber: turn.turnNumber,
|
|
96
|
-
arguments: { file_path: change.path },
|
|
97
|
-
summary: `edit_file: ${change.path}`,
|
|
98
|
-
confidence: 'high',
|
|
99
|
-
rawSnippet: JSON.stringify(change),
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (item.type === 'mcpToolCall') {
|
|
105
|
-
const call = item;
|
|
106
|
-
if (turn.nonAskToolEvents.some((event) => event.action === 'mcp_tool_call'
|
|
107
|
-
&& event.toolUseId === call.id && event.mcp?.invocation === 'not_invoked'))
|
|
108
|
-
return;
|
|
109
|
-
const args = recordFromUnknown(call.arguments);
|
|
110
|
-
const providerToolName = `${call.server}.${call.tool}`;
|
|
111
|
-
turn.nonAskToolEvents.push(attachOriginalMcpInput({
|
|
112
|
-
action: 'mcp_tool_call',
|
|
113
|
-
provider: 'codex',
|
|
114
|
-
providerToolName,
|
|
115
|
-
toolUseId: call.id,
|
|
116
|
-
turnNumber: turn.turnNumber,
|
|
117
|
-
arguments: {
|
|
118
|
-
...args,
|
|
119
|
-
server: call.server,
|
|
120
|
-
tool: call.tool,
|
|
121
|
-
status: call.status ?? 'unknown',
|
|
122
|
-
},
|
|
123
|
-
summary: `MCP tool ${providerToolName} ${call.status ?? 'unknown'}`,
|
|
124
|
-
confidence: 'high',
|
|
125
|
-
rawSnippet: JSON.stringify(call),
|
|
126
|
-
...(call.result !== undefined ? { result: { content: JSON.stringify(call.result) } } : {}),
|
|
127
|
-
}, args));
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
20
|
function projectMcpStartupStatusIntoTurn(params, turn) {
|
|
132
21
|
const name = params.name ?? 'unknown';
|
|
133
22
|
const status = params.status ?? 'unknown';
|
|
@@ -156,6 +45,18 @@ function failTurn(turn, message) {
|
|
|
156
45
|
turn.failureMessage = message;
|
|
157
46
|
turn.signalFailure?.(message);
|
|
158
47
|
}
|
|
48
|
+
function extractTurnCompletionIdentity(params) {
|
|
49
|
+
if (!params || typeof params !== 'object' || Array.isArray(params))
|
|
50
|
+
return {};
|
|
51
|
+
const record = params;
|
|
52
|
+
const turnId = typeof record.turn?.id === 'string'
|
|
53
|
+
? record.turn.id
|
|
54
|
+
: typeof record.turnId === 'string' ? record.turnId : undefined;
|
|
55
|
+
return {
|
|
56
|
+
...(typeof record.threadId === 'string' ? { threadId: record.threadId } : {}),
|
|
57
|
+
...(turnId ? { turnId } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
159
60
|
function isLiveMcpSafetyMode(options) {
|
|
160
61
|
const runMode = options?.runMode ?? 'mock';
|
|
161
62
|
return runMode === 'live-readonly' || runMode === 'live-sandbox' || runMode === 'live';
|
|
@@ -170,6 +71,7 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
170
71
|
const askBus = requireAskBusForLiveBatches(options, 'CodexAppServerAgent');
|
|
171
72
|
const workspacePath = getWorkspacePath(runtime);
|
|
172
73
|
const runtimeEnv = getRuntimeEnv(runtime);
|
|
74
|
+
const sensitiveValues = collectSensitiveEnvValues(runtimeEnv);
|
|
173
75
|
const model = resolveCodexModel(options?.model);
|
|
174
76
|
const sandboxMode = this.deps.sandboxMode ?? 'workspace-write';
|
|
175
77
|
let handle = null;
|
|
@@ -179,6 +81,12 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
179
81
|
let activeTurn = null;
|
|
180
82
|
let disposed = false;
|
|
181
83
|
let codexUserAgent = 'unknown';
|
|
84
|
+
const managedAuth = createCodexManagedAuth({
|
|
85
|
+
enabled: !runtimeEnv.OPENAI_API_KEY && !runtimeEnv.OPENAI_BASE_URL
|
|
86
|
+
&& (this.deps.authBroker !== undefined || this.deps.createTransport === undefined),
|
|
87
|
+
...(this.deps.authBroker ? { broker: this.deps.authBroker } : {}),
|
|
88
|
+
...(runtimeEnv.CODEX_HOME ? { codexHome: runtimeEnv.CODEX_HOME } : {}),
|
|
89
|
+
});
|
|
182
90
|
const scriptedHost = options?.scriptedMcpHost;
|
|
183
91
|
const correlator = scriptedHost
|
|
184
92
|
? new CodexMcpApprovalCorrelator(scriptedHost, () => turnCounter, () => threadId, () => options?.getRemainingMs?.() ?? Number.POSITIVE_INFINITY)
|
|
@@ -191,8 +99,9 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
191
99
|
const factory = this.deps.createTransport
|
|
192
100
|
?? (async (ctx) => spawnAppServerTransport({ cwd: ctx.workspacePath, env: ctx.env,
|
|
193
101
|
args: scriptedHost ? ['--enable', 'tool_call_mcp_elicitation'] : [] }));
|
|
194
|
-
|
|
195
|
-
|
|
102
|
+
const spawnedHandle = await factory({ workspacePath, env: runtimeEnv });
|
|
103
|
+
handle = spawnedHandle;
|
|
104
|
+
const transport = spawnedHandle.transport;
|
|
196
105
|
transport.onServerRequest((req) => this.dispatchServerRequest(req, {
|
|
197
106
|
transport,
|
|
198
107
|
askBus,
|
|
@@ -201,13 +110,15 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
201
110
|
mcpSafety: options?.mcpSafety,
|
|
202
111
|
scriptedHost,
|
|
203
112
|
correlator,
|
|
113
|
+
managedAuth,
|
|
204
114
|
}));
|
|
205
115
|
transport.onClose((info) => {
|
|
206
116
|
closeInfo = info;
|
|
207
117
|
});
|
|
208
118
|
transport.onNotification((n) => {
|
|
209
119
|
if (process.env.PATHGRADE_CODEX_DEBUG) {
|
|
210
|
-
|
|
120
|
+
const debugParams = sanitizePersistenceValue(n.params, sensitiveValues);
|
|
121
|
+
console.error(`[codex app-server] notification method=${n.method} params=${JSON.stringify(debugParams).slice(0, 300)}`);
|
|
211
122
|
}
|
|
212
123
|
if (n.method === 'mcpServer/startupStatus/updated') {
|
|
213
124
|
const turn = activeTurn;
|
|
@@ -221,37 +132,36 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
221
132
|
return;
|
|
222
133
|
const params = n.params;
|
|
223
134
|
if (n.method === 'item/started') {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
catch (error) {
|
|
228
|
-
failTurn(turn, error instanceof Error ? error.message : String(error));
|
|
229
|
-
}
|
|
135
|
+
if (params)
|
|
136
|
+
turn.itemLifecycle?.receiveStarted(params);
|
|
230
137
|
return;
|
|
231
138
|
}
|
|
232
139
|
if (n.method !== 'item/completed')
|
|
233
140
|
return;
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
if (correlator?.completed(params) !== false)
|
|
238
|
-
projectItemIntoTurn(params.item, turn);
|
|
239
|
-
}
|
|
240
|
-
catch (error) {
|
|
241
|
-
failTurn(turn, error instanceof Error ? error.message : String(error));
|
|
242
|
-
}
|
|
141
|
+
if (params)
|
|
142
|
+
turn.itemLifecycle?.receiveCompleted(params);
|
|
243
143
|
});
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
144
|
+
const initialize = async () => {
|
|
145
|
+
const initialized = await transport.sendRequest('initialize', {
|
|
146
|
+
clientInfo: { name: 'pathgrade', version: '0.5.0', title: null },
|
|
147
|
+
capabilities: { experimentalApi: true, optOutNotificationMethods: null },
|
|
148
|
+
});
|
|
149
|
+
if (typeof initialized.userAgent === 'string') {
|
|
150
|
+
codexUserAgent = initialized.userAgent.slice(0, 200);
|
|
151
|
+
}
|
|
152
|
+
// Upstream ClientNotification = { method: "initialized" }: send it
|
|
153
|
+
// before any thread/start so the handshake matches the v0.124
|
|
154
|
+
// contract and is forward-compatible with servers that enforce it.
|
|
155
|
+
transport.sendNotification('initialized', null);
|
|
156
|
+
await managedAuth.login(transport);
|
|
157
|
+
return transport;
|
|
158
|
+
};
|
|
159
|
+
return initialize().catch(async (error) => {
|
|
160
|
+
if (handle === spawnedHandle)
|
|
161
|
+
handle = null;
|
|
162
|
+
await spawnedHandle.close().catch(() => undefined);
|
|
163
|
+
throw error;
|
|
247
164
|
});
|
|
248
|
-
if (typeof initialized.userAgent === 'string')
|
|
249
|
-
codexUserAgent = initialized.userAgent.slice(0, 200);
|
|
250
|
-
// Upstream ClientNotification = { method: "initialized" }: send it
|
|
251
|
-
// before any thread/start so the handshake matches the v0.124
|
|
252
|
-
// contract and is forward-compatible with servers that enforce it.
|
|
253
|
-
transport.sendNotification('initialized', null);
|
|
254
|
-
return transport;
|
|
255
165
|
};
|
|
256
166
|
const runTurn = async (message) => {
|
|
257
167
|
const t = await ensureTransport();
|
|
@@ -262,6 +172,7 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
262
172
|
nonAskToolEvents: [],
|
|
263
173
|
assistantMessageParts: [],
|
|
264
174
|
turnFailed: false,
|
|
175
|
+
pendingTurnCompletions: [],
|
|
265
176
|
};
|
|
266
177
|
activeTurn = turn;
|
|
267
178
|
correlator?.beginTurn();
|
|
@@ -286,6 +197,31 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
286
197
|
}
|
|
287
198
|
threadId = resp.thread.id;
|
|
288
199
|
}
|
|
200
|
+
const authoritativeThreadId = threadId;
|
|
201
|
+
turn.authoritativeThreadId = authoritativeThreadId;
|
|
202
|
+
turn.itemLifecycle = new CodexItemLifecycle({
|
|
203
|
+
threadId: authoritativeThreadId,
|
|
204
|
+
clock: this.deps.clock,
|
|
205
|
+
onFailure: (message) => failTurn(turn, message),
|
|
206
|
+
onStarted: (params) => {
|
|
207
|
+
try {
|
|
208
|
+
correlator?.started(params);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
failTurn(turn, error instanceof Error ? error.message : String(error));
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
onCompleted: (item, timing, params) => {
|
|
215
|
+
try {
|
|
216
|
+
if (correlator?.completed(params) !== false) {
|
|
217
|
+
projectItemIntoTurn(item, turn, sensitiveValues, timing);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
failTurn(turn, error instanceof Error ? error.message : String(error));
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
});
|
|
289
225
|
// Wait for TurnCompleted OR subprocess crash OR dispatcher failure.
|
|
290
226
|
const turnCompletion = new Promise((resolve, reject) => {
|
|
291
227
|
let settled = false;
|
|
@@ -293,15 +229,30 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
293
229
|
if (settled)
|
|
294
230
|
return;
|
|
295
231
|
if (n.method === TURN_COMPLETED_METHOD) {
|
|
296
|
-
|
|
297
|
-
off();
|
|
298
|
-
closeOff();
|
|
299
|
-
const failure = extractTurnCompletionFailure(n.params);
|
|
300
|
-
if (failure)
|
|
301
|
-
Object.assign(turn, { turnFailed: true, failureMessage: failure });
|
|
302
|
-
resolve();
|
|
232
|
+
turn.acceptTurnCompletion?.(n.params);
|
|
303
233
|
}
|
|
304
234
|
});
|
|
235
|
+
const acceptTurnCompletion = (params) => {
|
|
236
|
+
if (settled)
|
|
237
|
+
return;
|
|
238
|
+
const identity = extractTurnCompletionIdentity(params);
|
|
239
|
+
if (identity.threadId && identity.threadId !== turn.authoritativeThreadId)
|
|
240
|
+
return;
|
|
241
|
+
if (identity.turnId && !turn.authoritativeTurnId) {
|
|
242
|
+
turn.pendingTurnCompletions.push(params);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (identity.turnId && identity.turnId !== turn.authoritativeTurnId)
|
|
246
|
+
return;
|
|
247
|
+
settled = true;
|
|
248
|
+
off();
|
|
249
|
+
closeOff();
|
|
250
|
+
const failure = extractTurnCompletionFailure(params);
|
|
251
|
+
if (failure)
|
|
252
|
+
Object.assign(turn, { turnFailed: true, failureMessage: failure });
|
|
253
|
+
resolve();
|
|
254
|
+
};
|
|
255
|
+
turn.acceptTurnCompletion = acceptTurnCompletion;
|
|
305
256
|
const closeOff = t.onClose((info) => {
|
|
306
257
|
if (settled)
|
|
307
258
|
return;
|
|
@@ -339,9 +290,25 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
339
290
|
}
|
|
340
291
|
}
|
|
341
292
|
});
|
|
342
|
-
const
|
|
293
|
+
const authoritativeTurn = t.sendRequest('turn/start', {
|
|
343
294
|
threadId,
|
|
344
295
|
input: [{ type: 'text', text: message, text_elements: [] }],
|
|
296
|
+
})
|
|
297
|
+
.then((started) => {
|
|
298
|
+
const authoritativeTurnId = started.turn?.id ?? started.turnId;
|
|
299
|
+
if (!authoritativeTurnId) {
|
|
300
|
+
scriptedHost?.failProtocol('turn/start did not return an authoritative turn id');
|
|
301
|
+
turn.signalFailure?.('turn/start did not return an authoritative turn id');
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
turn.itemLifecycle?.setAuthoritativeTurn(authoritativeTurnId);
|
|
305
|
+
turn.authoritativeTurnId = authoritativeTurnId;
|
|
306
|
+
if (correlator?.setAuthoritativeTurn(authoritativeThreadId, authoritativeTurnId) === false) {
|
|
307
|
+
turn.signalFailure?.('MCP lifecycle did not match the authoritative turn');
|
|
308
|
+
}
|
|
309
|
+
for (const pending of turn.pendingTurnCompletions.splice(0)) {
|
|
310
|
+
turn.acceptTurnCompletion?.(pending);
|
|
311
|
+
}
|
|
345
312
|
})
|
|
346
313
|
.catch((err) => {
|
|
347
314
|
// A JSON-RPC error on turn/start means the server will
|
|
@@ -355,18 +322,6 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
355
322
|
: 'turn/start failed';
|
|
356
323
|
turn.signalFailure?.(msg);
|
|
357
324
|
});
|
|
358
|
-
if (scriptedHost) {
|
|
359
|
-
const started = await startTurnResp;
|
|
360
|
-
if (!started?.turn?.id || !threadId) {
|
|
361
|
-
scriptedHost.failProtocol('turn/start did not return an authoritative turn id');
|
|
362
|
-
turn.signalFailure?.('turn/start did not return an authoritative turn id');
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
if (correlator?.setAuthoritativeTurn(threadId, started.turn.id) === false) {
|
|
366
|
-
turn.signalFailure?.('MCP lifecycle did not match the authoritative turn');
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
325
|
try {
|
|
371
326
|
await turnCompletion;
|
|
372
327
|
}
|
|
@@ -379,16 +334,25 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
379
334
|
message: info.message ?? 'app-server exited',
|
|
380
335
|
pid: info.pid,
|
|
381
336
|
signal: info.signal,
|
|
337
|
+
sensitiveValues,
|
|
382
338
|
});
|
|
383
339
|
}
|
|
384
|
-
if (!scriptedHost)
|
|
385
|
-
void startTurnResp;
|
|
386
340
|
if (turn.turnFailed) {
|
|
387
341
|
return assembleTurnResult({
|
|
388
342
|
askBus,
|
|
389
343
|
activeTurn: turn,
|
|
390
344
|
exitCode: 1,
|
|
391
345
|
message: turn.failureMessage ?? 'turn failed',
|
|
346
|
+
sensitiveValues,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
// Ensure pre-response lifecycle notifications have been correlated
|
|
350
|
+
// and projected before exposing the successful turn result.
|
|
351
|
+
await authoritativeTurn;
|
|
352
|
+
if (turn.turnFailed) {
|
|
353
|
+
return assembleTurnResult({
|
|
354
|
+
askBus, activeTurn: turn, exitCode: 1,
|
|
355
|
+
message: turn.failureMessage ?? 'turn failed', sensitiveValues,
|
|
392
356
|
});
|
|
393
357
|
}
|
|
394
358
|
return assembleTurnResult({
|
|
@@ -396,6 +360,7 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
396
360
|
activeTurn: turn,
|
|
397
361
|
exitCode: 0,
|
|
398
362
|
message: turn.assistantMessageParts.join('\n\n'),
|
|
363
|
+
sensitiveValues,
|
|
399
364
|
});
|
|
400
365
|
}
|
|
401
366
|
finally {
|
|
@@ -516,11 +481,11 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
516
481
|
}
|
|
517
482
|
return;
|
|
518
483
|
case 'account/chatgptAuthTokens/refresh': {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
484
|
+
ctx.managedAuth.respondToRefresh(req.id, transport, (message) => {
|
|
485
|
+
const turn = activeTurn();
|
|
486
|
+
if (turn)
|
|
487
|
+
failTurn(turn, message);
|
|
488
|
+
});
|
|
524
489
|
return;
|
|
525
490
|
}
|
|
526
491
|
default:
|
|
@@ -577,14 +542,15 @@ function buildThreadStartParams(opts) {
|
|
|
577
542
|
};
|
|
578
543
|
}
|
|
579
544
|
function assembleTurnResult(args) {
|
|
580
|
-
const { askBus, activeTurn, exitCode, message, pid, signal } = args;
|
|
545
|
+
const { askBus, activeTurn, exitCode, message, pid, signal, sensitiveValues = [] } = args;
|
|
581
546
|
const askBatchIds = new Set(activeTurn.askBatchIds);
|
|
582
547
|
const askEvents = askBus
|
|
583
548
|
.snapshot()
|
|
584
549
|
.filter((s) => askBatchIds.has(s.batchId))
|
|
585
550
|
.map((s) => toAskUserToolEvent(s));
|
|
586
|
-
const toolEvents = enrichSkillEvents([...askEvents, ...activeTurn.nonAskToolEvents])
|
|
587
|
-
|
|
551
|
+
const toolEvents = enrichSkillEvents([...askEvents, ...activeTurn.nonAskToolEvents])
|
|
552
|
+
.map((event) => attachToolEventSensitiveValues(event, sensitiveValues));
|
|
553
|
+
const rawOutput = sanitizePersistenceValue(exitCode === 0
|
|
588
554
|
? message
|
|
589
555
|
: [
|
|
590
556
|
message,
|
|
@@ -593,8 +559,8 @@ function assembleTurnResult(args) {
|
|
|
593
559
|
`exitCode=${exitCode}`,
|
|
594
560
|
]
|
|
595
561
|
.filter(Boolean)
|
|
596
|
-
.join(' ');
|
|
597
|
-
return {
|
|
562
|
+
.join(' '), sensitiveValues);
|
|
563
|
+
return attachTurnResultSensitiveValues({
|
|
598
564
|
rawOutput,
|
|
599
565
|
assistantMessage: exitCode === 0 ? message : '',
|
|
600
566
|
visibleAssistantMessage: exitCode === 0 ? message : '',
|
|
@@ -611,5 +577,5 @@ function assembleTurnResult(args) {
|
|
|
611
577
|
},
|
|
612
578
|
}
|
|
613
579
|
: {}),
|
|
614
|
-
};
|
|
580
|
+
}, sensitiveValues);
|
|
615
581
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CodexItem, ItemTiming } from './item-projection.js';
|
|
2
|
+
export interface ItemLifecycleParams {
|
|
3
|
+
item?: CodexItem;
|
|
4
|
+
threadId?: string;
|
|
5
|
+
turnId?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface LifecycleClock {
|
|
8
|
+
wallNow(): number;
|
|
9
|
+
monotonicNow(): number;
|
|
10
|
+
}
|
|
11
|
+
export declare class CodexItemLifecycle {
|
|
12
|
+
private readonly options;
|
|
13
|
+
private turnId?;
|
|
14
|
+
private readonly pending;
|
|
15
|
+
private readonly started;
|
|
16
|
+
private readonly completed;
|
|
17
|
+
constructor(options: {
|
|
18
|
+
threadId: string;
|
|
19
|
+
clock?: LifecycleClock;
|
|
20
|
+
onStarted?: (params: ItemLifecycleParams) => void;
|
|
21
|
+
onCompleted: (item: CodexItem, timing: ItemTiming, params: ItemLifecycleParams) => void;
|
|
22
|
+
onFailure: (message: string) => void;
|
|
23
|
+
});
|
|
24
|
+
receiveStarted(params: ItemLifecycleParams): void;
|
|
25
|
+
receiveCompleted(params: ItemLifecycleParams): void;
|
|
26
|
+
setAuthoritativeTurn(turnId: string): boolean;
|
|
27
|
+
private receive;
|
|
28
|
+
private validateEnvelope;
|
|
29
|
+
private process;
|
|
30
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { canonicalizeJson } from '../../core/canonical-json.js';
|
|
3
|
+
export class CodexItemLifecycle {
|
|
4
|
+
options;
|
|
5
|
+
turnId;
|
|
6
|
+
pending = [];
|
|
7
|
+
started = new Map();
|
|
8
|
+
completed = new Map();
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
}
|
|
12
|
+
receiveStarted(params) {
|
|
13
|
+
this.receive('started', params);
|
|
14
|
+
}
|
|
15
|
+
receiveCompleted(params) {
|
|
16
|
+
this.receive('completed', params);
|
|
17
|
+
}
|
|
18
|
+
setAuthoritativeTurn(turnId) {
|
|
19
|
+
if (!turnId) {
|
|
20
|
+
this.options.onFailure('turn/start did not return an authoritative turn id');
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (this.turnId && this.turnId !== turnId) {
|
|
24
|
+
this.options.onFailure('turn/start returned conflicting authoritative turn ids');
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
this.turnId = turnId;
|
|
28
|
+
for (const entry of this.pending.splice(0))
|
|
29
|
+
this.process(entry.phase, entry.receipt);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
receive(phase, params) {
|
|
33
|
+
const clock = this.options.clock ?? DEFAULT_CLOCK;
|
|
34
|
+
const receipt = { params, wallMs: clock.wallNow(), monotonicMs: clock.monotonicNow() };
|
|
35
|
+
if (!this.validateEnvelope(params))
|
|
36
|
+
return;
|
|
37
|
+
if (!this.turnId) {
|
|
38
|
+
this.pending.push({ phase, receipt });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
this.process(phase, receipt);
|
|
42
|
+
}
|
|
43
|
+
validateEnvelope(params) {
|
|
44
|
+
if (!params.item || typeof params.item.id !== 'string' || !params.item.id
|
|
45
|
+
|| typeof params.threadId !== 'string' || !params.threadId
|
|
46
|
+
|| typeof params.turnId !== 'string' || !params.turnId) {
|
|
47
|
+
this.options.onFailure('Codex item lifecycle notification is missing threadId, turnId, or item.id');
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
// Notifications for an earlier thread are stale, not failures of this turn.
|
|
51
|
+
return params.threadId === this.options.threadId;
|
|
52
|
+
}
|
|
53
|
+
process(phase, receipt) {
|
|
54
|
+
const { params } = receipt;
|
|
55
|
+
if (params.turnId !== this.turnId || !params.item || typeof params.item.id !== 'string')
|
|
56
|
+
return;
|
|
57
|
+
const itemId = params.item.id;
|
|
58
|
+
const fingerprint = canonicalizeJson(params.item);
|
|
59
|
+
if (phase === 'started') {
|
|
60
|
+
const prior = this.started.get(itemId);
|
|
61
|
+
if (prior) {
|
|
62
|
+
if (prior.fingerprint !== fingerprint) {
|
|
63
|
+
this.options.onFailure(`Conflicting item/started notification for item ${itemId}`);
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (this.completed.has(itemId)) {
|
|
68
|
+
this.options.onFailure(`item/started arrived after item/completed for item ${itemId}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.started.set(itemId, { ...receipt, fingerprint });
|
|
72
|
+
this.options.onStarted?.(params);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const priorTerminal = this.completed.get(itemId);
|
|
76
|
+
if (priorTerminal !== undefined) {
|
|
77
|
+
if (priorTerminal !== fingerprint) {
|
|
78
|
+
this.options.onFailure(`Conflicting item/completed notification for item ${itemId}`);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
this.completed.set(itemId, fingerprint);
|
|
83
|
+
const start = this.started.get(itemId);
|
|
84
|
+
this.started.delete(itemId);
|
|
85
|
+
this.options.onCompleted(params.item, {
|
|
86
|
+
...(start ? { startedAtWallMs: start.wallMs } : {}),
|
|
87
|
+
completedAtWallMs: receipt.wallMs,
|
|
88
|
+
...(start ? { observedDurationMs: Math.max(0, receipt.monotonicMs - start.monotonicMs) } : {}),
|
|
89
|
+
}, params);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const DEFAULT_CLOCK = {
|
|
93
|
+
wallNow: () => Date.now(),
|
|
94
|
+
monotonicNow: () => performance.now(),
|
|
95
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type ToolEvent } from '../../tool-events.js';
|
|
2
|
+
export interface CodexTurnProjection {
|
|
3
|
+
turnNumber: number;
|
|
4
|
+
nonAskToolEvents: ToolEvent[];
|
|
5
|
+
assistantMessageParts: string[];
|
|
6
|
+
}
|
|
7
|
+
interface CodexCommandExecutionAction {
|
|
8
|
+
type?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
path?: string;
|
|
11
|
+
command?: string;
|
|
12
|
+
}
|
|
13
|
+
interface CodexCommandExecutionItem {
|
|
14
|
+
type: 'commandExecution';
|
|
15
|
+
id: string;
|
|
16
|
+
command: string;
|
|
17
|
+
status?: 'inProgress' | 'completed' | 'failed' | 'declined';
|
|
18
|
+
cwd?: string;
|
|
19
|
+
commandActions?: CodexCommandExecutionAction[];
|
|
20
|
+
aggregatedOutput?: string | null;
|
|
21
|
+
exitCode?: number | null;
|
|
22
|
+
durationMs?: number | null;
|
|
23
|
+
}
|
|
24
|
+
interface CodexMcpToolCallItem {
|
|
25
|
+
type: 'mcpToolCall';
|
|
26
|
+
id: string;
|
|
27
|
+
server: string;
|
|
28
|
+
tool: string;
|
|
29
|
+
status?: string;
|
|
30
|
+
arguments?: unknown;
|
|
31
|
+
result?: unknown;
|
|
32
|
+
error?: {
|
|
33
|
+
message?: string;
|
|
34
|
+
} | null;
|
|
35
|
+
durationMs?: number | null;
|
|
36
|
+
}
|
|
37
|
+
export type CodexItem = {
|
|
38
|
+
type: 'agentMessage';
|
|
39
|
+
id: string;
|
|
40
|
+
text: string;
|
|
41
|
+
phase?: string;
|
|
42
|
+
} | CodexCommandExecutionItem | {
|
|
43
|
+
type: 'fileChange';
|
|
44
|
+
id: string;
|
|
45
|
+
changes: Array<{
|
|
46
|
+
path: string;
|
|
47
|
+
kind?: unknown;
|
|
48
|
+
diff?: string;
|
|
49
|
+
}>;
|
|
50
|
+
} | CodexMcpToolCallItem | {
|
|
51
|
+
type: string;
|
|
52
|
+
id?: string;
|
|
53
|
+
};
|
|
54
|
+
export interface ItemTiming {
|
|
55
|
+
startedAtWallMs?: number;
|
|
56
|
+
completedAtWallMs?: number;
|
|
57
|
+
observedDurationMs?: number;
|
|
58
|
+
}
|
|
59
|
+
export declare function projectItemIntoTurn(item: CodexItem, turn: CodexTurnProjection, sensitiveValues: readonly string[], timing?: ItemTiming): void;
|
|
60
|
+
/** Provider duration is authoritative; observed monotonic duration is the fallback. */
|
|
61
|
+
export declare function projectToolTiming(timing: ItemTiming, providerDurationMs: number | undefined): Pick<ToolEvent, 'startedAt' | 'completedAt' | 'durationMs'>;
|
|
62
|
+
export {};
|