@wix/pathgrade 1.0.21 → 1.0.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/agents/codex-app-server/agent.js +68 -47
- package/dist/agents/codex-app-server/transport.d.ts +3 -1
- package/dist/agents/codex-app-server/transport.js +14 -1
- package/dist/agents/codex-app-server/turn-authority.d.ts +42 -0
- package/dist/agents/codex-app-server/turn-authority.js +96 -0
- package/dist/agents/opencode/runtime-policy.js +7 -4
- package/dist/tool-event-results.js +86 -17
- package/docs/OPENAI_OAUTH_JUDGE.md +3 -2
- package/package.json +3 -7
|
@@ -15,6 +15,7 @@ import { buildLiveMcpProtocolErrorEvent, buildPolicyDeniedMcpToolEvent, CodexMcp
|
|
|
15
15
|
import { projectItemIntoTurn } from './item-projection.js';
|
|
16
16
|
import { CodexItemLifecycle, } from './item-lifecycle.js';
|
|
17
17
|
import { createCodexManagedAuth } from './managed-auth.js';
|
|
18
|
+
import { clearTurnAuthorityDeadline, closeUncorrelatedTurnTransport, establishAuthoritativeTurn, isAbortError, isAuthoritativeTurnCompletion, rememberAuthoritativeTurnId, shouldRetireAuthoritativeTurnTransport, } from './turn-authority.js';
|
|
18
19
|
const TURN_COMPLETED_METHOD = 'turn/completed';
|
|
19
20
|
function projectMcpStartupStatusIntoTurn(params, turn) {
|
|
20
21
|
const name = params.name ?? 'unknown';
|
|
@@ -64,18 +65,6 @@ function projectCompletedItem(item, timing, params, turn, correlator, sensitiveV
|
|
|
64
65
|
}
|
|
65
66
|
turn.nonAskToolEvents.push(buildPolicyDeniedMcpToolEvent(turn.turnNumber, pendingDenial, mcpItem));
|
|
66
67
|
}
|
|
67
|
-
function extractTurnCompletionIdentity(params) {
|
|
68
|
-
if (!params || typeof params !== 'object' || Array.isArray(params))
|
|
69
|
-
return {};
|
|
70
|
-
const record = params;
|
|
71
|
-
const turnId = typeof record.turn?.id === 'string'
|
|
72
|
-
? record.turn.id
|
|
73
|
-
: typeof record.turnId === 'string' ? record.turnId : undefined;
|
|
74
|
-
return {
|
|
75
|
-
...(typeof record.threadId === 'string' ? { threadId: record.threadId } : {}),
|
|
76
|
-
...(turnId ? { turnId } : {}),
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
68
|
function isLiveMcpSafetyMode(options) {
|
|
80
69
|
const runMode = options?.runMode ?? 'mock';
|
|
81
70
|
return runMode === 'live-readonly' || runMode === 'live-sandbox' || runMode === 'live';
|
|
@@ -102,9 +91,11 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
102
91
|
let handle = null;
|
|
103
92
|
let threadId = null;
|
|
104
93
|
let turnCounter = 0;
|
|
94
|
+
const authoritativeTurnIds = new Set();
|
|
105
95
|
let closeInfo = null;
|
|
106
96
|
let activeTurn = null;
|
|
107
97
|
let disposed = false;
|
|
98
|
+
let sessionRetired = false;
|
|
108
99
|
let codexUserAgent = 'unknown';
|
|
109
100
|
const managedAuth = createCodexManagedAuth({
|
|
110
101
|
enabled: !runtimeEnv.OPENAI_API_KEY && !runtimeEnv.OPENAI_BASE_URL
|
|
@@ -137,7 +128,8 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
137
128
|
managedAuth,
|
|
138
129
|
}));
|
|
139
130
|
transport.onClose((info) => {
|
|
140
|
-
|
|
131
|
+
if (handle === spawnedHandle)
|
|
132
|
+
closeInfo = info;
|
|
141
133
|
});
|
|
142
134
|
transport.onNotification((n) => {
|
|
143
135
|
if (process.env.PATHGRADE_CODEX_DEBUG) {
|
|
@@ -173,9 +165,6 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
173
165
|
if (typeof initialized.userAgent === 'string') {
|
|
174
166
|
codexUserAgent = initialized.userAgent.slice(0, 200);
|
|
175
167
|
}
|
|
176
|
-
// Upstream ClientNotification = { method: "initialized" }: send it
|
|
177
|
-
// before any thread/start so the handshake matches the v0.149
|
|
178
|
-
// contract and is forward-compatible with servers that enforce it.
|
|
179
168
|
transport.sendNotification('initialized', null);
|
|
180
169
|
await managedAuth.login(transport);
|
|
181
170
|
return transport;
|
|
@@ -189,6 +178,7 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
189
178
|
};
|
|
190
179
|
const runTurn = async (message) => {
|
|
191
180
|
const t = await ensureTransport();
|
|
181
|
+
const turnHandle = handle;
|
|
192
182
|
turnCounter += 1;
|
|
193
183
|
const turn = {
|
|
194
184
|
turnNumber: turnCounter,
|
|
@@ -199,6 +189,8 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
199
189
|
turnFailed: false,
|
|
200
190
|
pendingTurnCompletions: [],
|
|
201
191
|
};
|
|
192
|
+
const turnStartController = new AbortController();
|
|
193
|
+
let turnStartCanReuseTransport = false;
|
|
202
194
|
activeTurn = turn;
|
|
203
195
|
correlator?.beginTurn();
|
|
204
196
|
try {
|
|
@@ -251,7 +243,18 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
251
243
|
}
|
|
252
244
|
},
|
|
253
245
|
});
|
|
254
|
-
|
|
246
|
+
let resolveAuthoritativeTurn;
|
|
247
|
+
const authoritativeTurnReady = new Promise((resolve) => {
|
|
248
|
+
resolveAuthoritativeTurn = resolve;
|
|
249
|
+
});
|
|
250
|
+
const markAuthoritativeTurn = (candidateTurnId) => establishAuthoritativeTurn({
|
|
251
|
+
turn,
|
|
252
|
+
candidateTurnId,
|
|
253
|
+
authoritativeThreadId,
|
|
254
|
+
authoritativeTurnIds,
|
|
255
|
+
correlator,
|
|
256
|
+
onReady: () => resolveAuthoritativeTurn?.(),
|
|
257
|
+
});
|
|
255
258
|
const turnCompletion = new Promise((resolve, reject) => {
|
|
256
259
|
let settled = false;
|
|
257
260
|
const off = t.onNotification((n) => {
|
|
@@ -264,14 +267,13 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
264
267
|
const acceptTurnCompletion = (params) => {
|
|
265
268
|
if (settled)
|
|
266
269
|
return;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
if (identity.turnId && identity.turnId !== turn.authoritativeTurnId)
|
|
270
|
+
if (!isAuthoritativeTurnCompletion({
|
|
271
|
+
params,
|
|
272
|
+
turn,
|
|
273
|
+
authoritativeTurnIds,
|
|
274
|
+
markAuthoritativeTurn,
|
|
275
|
+
getRemainingMs: () => options?.getRemainingMs?.() ?? Number.POSITIVE_INFINITY,
|
|
276
|
+
}))
|
|
275
277
|
return;
|
|
276
278
|
settled = true;
|
|
277
279
|
off();
|
|
@@ -295,13 +297,14 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
295
297
|
}));
|
|
296
298
|
});
|
|
297
299
|
turn.signalFailure = (msg) => {
|
|
300
|
+
turn.turnFailed = true;
|
|
301
|
+
turn.failureMessage = msg;
|
|
302
|
+
resolveAuthoritativeTurn?.();
|
|
298
303
|
if (settled)
|
|
299
304
|
return;
|
|
300
305
|
settled = true;
|
|
301
306
|
off();
|
|
302
307
|
closeOff();
|
|
303
|
-
turn.turnFailed = true;
|
|
304
|
-
turn.failureMessage = msg;
|
|
305
308
|
resolve();
|
|
306
309
|
};
|
|
307
310
|
if (turn.turnFailed) {
|
|
@@ -319,31 +322,31 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
319
322
|
}
|
|
320
323
|
}
|
|
321
324
|
});
|
|
322
|
-
|
|
325
|
+
void t.sendRequest('turn/start', {
|
|
323
326
|
threadId,
|
|
324
327
|
input: [{ type: 'text', text: message, text_elements: [] }],
|
|
325
|
-
})
|
|
328
|
+
}, { signal: turnStartController.signal })
|
|
326
329
|
.then((started) => {
|
|
327
|
-
const authoritativeTurnId = started
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
|
|
330
|
+
const authoritativeTurnId = started?.turn?.id ?? started?.turnId;
|
|
331
|
+
if (activeTurn !== turn) {
|
|
332
|
+
if (authoritativeTurnId)
|
|
333
|
+
rememberAuthoritativeTurnId(authoritativeTurnIds, authoritativeTurnId);
|
|
331
334
|
return;
|
|
332
335
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
turn.acceptTurnCompletion?.(pending);
|
|
336
|
+
if (!authoritativeTurnId) {
|
|
337
|
+
if (!turn.authoritativeTurnId) {
|
|
338
|
+
scriptedHost?.failProtocol('turn/start did not return an authoritative turn id');
|
|
339
|
+
turn.signalFailure?.('turn/start did not return an authoritative turn id');
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
340
342
|
}
|
|
343
|
+
turnStartCanReuseTransport = true;
|
|
344
|
+
markAuthoritativeTurn(authoritativeTurnId);
|
|
341
345
|
})
|
|
342
346
|
.catch((err) => {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
// waiting for an outer timeout.
|
|
347
|
+
if (isAbortError(err) || activeTurn !== turn)
|
|
348
|
+
return;
|
|
349
|
+
turnStartCanReuseTransport = true;
|
|
347
350
|
const msg = err instanceof Error
|
|
348
351
|
? err.message
|
|
349
352
|
: typeof err === 'string'
|
|
@@ -375,9 +378,7 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
375
378
|
sensitiveValues,
|
|
376
379
|
});
|
|
377
380
|
}
|
|
378
|
-
|
|
379
|
-
// and projected before exposing the successful turn result.
|
|
380
|
-
await authoritativeTurn;
|
|
381
|
+
await authoritativeTurnReady;
|
|
381
382
|
if (turn.turnFailed) {
|
|
382
383
|
return assembleTurnResult({
|
|
383
384
|
askBus, activeTurn: turn, exitCode: 1,
|
|
@@ -393,8 +394,25 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
393
394
|
});
|
|
394
395
|
}
|
|
395
396
|
finally {
|
|
397
|
+
turnStartController.abort();
|
|
398
|
+
clearTurnAuthorityDeadline(turn);
|
|
396
399
|
activeTurn = null;
|
|
397
400
|
correlator?.endTurn();
|
|
401
|
+
const retireTransport = shouldRetireAuthoritativeTurnTransport(authoritativeTurnIds);
|
|
402
|
+
await closeUncorrelatedTurnTransport({
|
|
403
|
+
turnStartCanReuseTransport,
|
|
404
|
+
authoritativeTurnId: turn.authoritativeTurnId,
|
|
405
|
+
retireTransport,
|
|
406
|
+
currentHandle: handle,
|
|
407
|
+
turnHandle,
|
|
408
|
+
reset: () => {
|
|
409
|
+
sessionRetired ||= retireTransport;
|
|
410
|
+
handle = null;
|
|
411
|
+
threadId = null;
|
|
412
|
+
authoritativeTurnIds.clear();
|
|
413
|
+
closeInfo = null;
|
|
414
|
+
},
|
|
415
|
+
});
|
|
398
416
|
}
|
|
399
417
|
};
|
|
400
418
|
const dispose = async () => {
|
|
@@ -419,6 +437,9 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
419
437
|
if (disposed) {
|
|
420
438
|
throw new Error('CodexAppServerAgent session disposed');
|
|
421
439
|
}
|
|
440
|
+
if (sessionRetired) {
|
|
441
|
+
throw new Error('CodexAppServerAgent session reached its safe turn-history limit; create a new session');
|
|
442
|
+
}
|
|
422
443
|
};
|
|
423
444
|
return {
|
|
424
445
|
start: async ({ message }) => {
|
|
@@ -15,7 +15,9 @@ export interface TransportCloseInfo {
|
|
|
15
15
|
readonly pid?: number;
|
|
16
16
|
}
|
|
17
17
|
export interface AppServerTransport {
|
|
18
|
-
sendRequest<T = unknown>(method: string, params: unknown
|
|
18
|
+
sendRequest<T = unknown>(method: string, params: unknown, options?: {
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
}): Promise<T>;
|
|
19
21
|
sendNotification(method: string, params: unknown): void;
|
|
20
22
|
sendResponse(id: number | string, result: unknown): void;
|
|
21
23
|
sendErrorResponse(id: number | string, code: number, message: string): void;
|
|
@@ -80,18 +80,31 @@ function createNdjsonTransportInternal(cfg) {
|
|
|
80
80
|
pendingRequests.clear();
|
|
81
81
|
};
|
|
82
82
|
return {
|
|
83
|
-
sendRequest(method, params) {
|
|
83
|
+
sendRequest(method, params, options) {
|
|
84
84
|
if (closed)
|
|
85
85
|
return Promise.reject(new Error('AppServerTransport closed'));
|
|
86
86
|
const id = nextId++;
|
|
87
87
|
return new Promise((resolve, reject) => {
|
|
88
|
+
const abort = () => {
|
|
89
|
+
if (!pendingRequests.delete(id))
|
|
90
|
+
return;
|
|
91
|
+
const error = new Error('AppServerTransport request aborted');
|
|
92
|
+
error.name = 'AbortError';
|
|
93
|
+
reject(error);
|
|
94
|
+
};
|
|
88
95
|
pendingRequests.set(id, (env) => {
|
|
96
|
+
options?.signal?.removeEventListener('abort', abort);
|
|
89
97
|
if (env.error) {
|
|
90
98
|
reject(new Error(`codex app-server '${method}' failed (${env.error.code}): ${env.error.message}`));
|
|
91
99
|
return;
|
|
92
100
|
}
|
|
93
101
|
resolve(env.result);
|
|
94
102
|
});
|
|
103
|
+
options?.signal?.addEventListener('abort', abort, { once: true });
|
|
104
|
+
if (options?.signal?.aborted) {
|
|
105
|
+
abort();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
95
108
|
writeLine({ jsonrpc: '2.0', id, method, params });
|
|
96
109
|
});
|
|
97
110
|
},
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CodexMcpApprovalCorrelator } from './mcp-approval-correlator.js';
|
|
2
|
+
import type { CodexItemLifecycle } from './item-lifecycle.js';
|
|
3
|
+
import type { AppServerSessionHandle } from './transport.js';
|
|
4
|
+
interface TurnAuthorityState {
|
|
5
|
+
authoritativeThreadId?: string;
|
|
6
|
+
authoritativeTurnId?: string;
|
|
7
|
+
pendingTurnCompletions: unknown[];
|
|
8
|
+
acceptTurnCompletion?: (params: unknown) => void;
|
|
9
|
+
itemLifecycle?: CodexItemLifecycle;
|
|
10
|
+
turnFailed: boolean;
|
|
11
|
+
failureMessage?: string;
|
|
12
|
+
signalFailure?: (message: string) => void;
|
|
13
|
+
authorityDeadlineTimer?: ReturnType<typeof setTimeout>;
|
|
14
|
+
}
|
|
15
|
+
export declare function isAbortError(error: unknown): boolean;
|
|
16
|
+
export declare function closeUncorrelatedTurnTransport(args: {
|
|
17
|
+
turnStartCanReuseTransport: boolean;
|
|
18
|
+
authoritativeTurnId?: string;
|
|
19
|
+
retireTransport: boolean;
|
|
20
|
+
currentHandle: AppServerSessionHandle | null;
|
|
21
|
+
turnHandle: AppServerSessionHandle | null;
|
|
22
|
+
reset(): void;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
export declare function rememberAuthoritativeTurnId(turnIds: Set<string>, turnId: string): void;
|
|
25
|
+
export declare function shouldRetireAuthoritativeTurnTransport(turnIds: ReadonlySet<string>): boolean;
|
|
26
|
+
export declare function establishAuthoritativeTurn(args: {
|
|
27
|
+
turn: TurnAuthorityState;
|
|
28
|
+
candidateTurnId: string;
|
|
29
|
+
authoritativeThreadId: string;
|
|
30
|
+
authoritativeTurnIds: Set<string>;
|
|
31
|
+
correlator?: CodexMcpApprovalCorrelator;
|
|
32
|
+
onReady(): void;
|
|
33
|
+
}): boolean;
|
|
34
|
+
export declare function isAuthoritativeTurnCompletion(args: {
|
|
35
|
+
params: unknown;
|
|
36
|
+
turn: TurnAuthorityState;
|
|
37
|
+
authoritativeTurnIds: ReadonlySet<string>;
|
|
38
|
+
markAuthoritativeTurn(turnId: string): boolean;
|
|
39
|
+
getRemainingMs(): number;
|
|
40
|
+
}): boolean;
|
|
41
|
+
export declare function clearTurnAuthorityDeadline(turn: TurnAuthorityState): void;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const MAX_RECENT_AUTHORITATIVE_TURN_IDS = 256;
|
|
2
|
+
export function isAbortError(error) {
|
|
3
|
+
return error instanceof Error && error.name === 'AbortError';
|
|
4
|
+
}
|
|
5
|
+
export async function closeUncorrelatedTurnTransport(args) {
|
|
6
|
+
const { turnStartCanReuseTransport, authoritativeTurnId, retireTransport, currentHandle, turnHandle, reset } = args;
|
|
7
|
+
if ((!retireTransport && (turnStartCanReuseTransport || authoritativeTurnId))
|
|
8
|
+
|| currentHandle !== turnHandle || !turnHandle)
|
|
9
|
+
return;
|
|
10
|
+
reset();
|
|
11
|
+
await turnHandle.close().catch(() => undefined);
|
|
12
|
+
}
|
|
13
|
+
export function rememberAuthoritativeTurnId(turnIds, turnId) {
|
|
14
|
+
turnIds.add(turnId);
|
|
15
|
+
}
|
|
16
|
+
export function shouldRetireAuthoritativeTurnTransport(turnIds) {
|
|
17
|
+
return turnIds.size >= MAX_RECENT_AUTHORITATIVE_TURN_IDS;
|
|
18
|
+
}
|
|
19
|
+
export function establishAuthoritativeTurn(args) {
|
|
20
|
+
const { turn, candidateTurnId, authoritativeThreadId, authoritativeTurnIds, correlator, onReady } = args;
|
|
21
|
+
if (turn.authoritativeTurnId) {
|
|
22
|
+
if (turn.authoritativeTurnId === candidateTurnId)
|
|
23
|
+
return true;
|
|
24
|
+
failTurn(turn, 'turn lifecycle reported conflicting authoritative turn ids');
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (turn.itemLifecycle?.setAuthoritativeTurn(candidateTurnId) === false)
|
|
28
|
+
return false;
|
|
29
|
+
turn.authoritativeTurnId = candidateTurnId;
|
|
30
|
+
clearTurnAuthorityDeadline(turn);
|
|
31
|
+
rememberAuthoritativeTurnId(authoritativeTurnIds, candidateTurnId);
|
|
32
|
+
if (correlator?.setAuthoritativeTurn(authoritativeThreadId, candidateTurnId) === false) {
|
|
33
|
+
failTurn(turn, 'MCP lifecycle did not match the authoritative turn');
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
onReady();
|
|
37
|
+
for (const pending of turn.pendingTurnCompletions.splice(0)) {
|
|
38
|
+
turn.acceptTurnCompletion?.(pending);
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
export function isAuthoritativeTurnCompletion(args) {
|
|
43
|
+
const { params, turn, authoritativeTurnIds, markAuthoritativeTurn, getRemainingMs } = args;
|
|
44
|
+
const identity = extractTurnCompletionIdentity(params);
|
|
45
|
+
if (identity.threadId && identity.threadId !== turn.authoritativeThreadId)
|
|
46
|
+
return false;
|
|
47
|
+
if (!identity.turnId && !turn.authoritativeTurnId) {
|
|
48
|
+
if (turn.pendingTurnCompletions.length === 0) {
|
|
49
|
+
const remainingMs = getRemainingMs();
|
|
50
|
+
if (Number.isFinite(remainingMs))
|
|
51
|
+
turn.authorityDeadlineTimer = setTimeout(() => {
|
|
52
|
+
if (!turn.authoritativeTurnId && !turn.turnFailed) {
|
|
53
|
+
failTurn(turn, 'turn/completed did not identify the authoritative turn');
|
|
54
|
+
}
|
|
55
|
+
}, Math.max(0, remainingMs));
|
|
56
|
+
}
|
|
57
|
+
turn.pendingTurnCompletions.push(params);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (identity.turnId && !turn.authoritativeTurnId) {
|
|
61
|
+
if (authoritativeTurnIds.has(identity.turnId))
|
|
62
|
+
return false;
|
|
63
|
+
if (identity.threadId === turn.authoritativeThreadId) {
|
|
64
|
+
if (!markAuthoritativeTurn(identity.turnId))
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
turn.pendingTurnCompletions.push(params);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return !identity.turnId || identity.turnId === turn.authoritativeTurnId;
|
|
73
|
+
}
|
|
74
|
+
function extractTurnCompletionIdentity(params) {
|
|
75
|
+
if (!params || typeof params !== 'object' || Array.isArray(params))
|
|
76
|
+
return {};
|
|
77
|
+
const record = params;
|
|
78
|
+
const turnId = typeof record.turn?.id === 'string'
|
|
79
|
+
? record.turn.id
|
|
80
|
+
: typeof record.turnId === 'string' ? record.turnId : undefined;
|
|
81
|
+
return {
|
|
82
|
+
...(typeof record.threadId === 'string' ? { threadId: record.threadId } : {}),
|
|
83
|
+
...(turnId ? { turnId } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function failTurn(turn, message) {
|
|
87
|
+
clearTurnAuthorityDeadline(turn);
|
|
88
|
+
turn.turnFailed = true;
|
|
89
|
+
turn.failureMessage = message;
|
|
90
|
+
turn.signalFailure?.(message);
|
|
91
|
+
}
|
|
92
|
+
export function clearTurnAuthorityDeadline(turn) {
|
|
93
|
+
if (turn.authorityDeadlineTimer)
|
|
94
|
+
clearTimeout(turn.authorityDeadlineTimer);
|
|
95
|
+
turn.authorityDeadlineTimer = undefined;
|
|
96
|
+
}
|
|
@@ -130,11 +130,14 @@ export class OpenCodeRuntimePolicy {
|
|
|
130
130
|
if (this.authWatcher)
|
|
131
131
|
throw new Error('OpenCode auth monitor is already active');
|
|
132
132
|
this.authMutationObserved = false;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (filename === null || filename.toString() === expectedName)
|
|
133
|
+
try {
|
|
134
|
+
this.authWatcher = watch(this.authPath, { persistent: false }, () => {
|
|
136
135
|
this.authMutationObserved = true;
|
|
137
|
-
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
throw poisonedAuthError();
|
|
140
|
+
}
|
|
138
141
|
this.authWatcher.on('error', () => {
|
|
139
142
|
this.authMutationObserved = true;
|
|
140
143
|
});
|
|
@@ -24,10 +24,24 @@ const SECRET_KEY_NAMES = [
|
|
|
24
24
|
'connectionstring',
|
|
25
25
|
'credentials',
|
|
26
26
|
'credential',
|
|
27
|
+
'svsession',
|
|
28
|
+
'smsession',
|
|
29
|
+
'wixsession',
|
|
30
|
+
'wixsession2',
|
|
27
31
|
];
|
|
32
|
+
const EXACT_ONLY_SECRET_KEY_NAMES = new Set([
|
|
33
|
+
'auth',
|
|
34
|
+
'token',
|
|
35
|
+
'svsession',
|
|
36
|
+
'smsession',
|
|
37
|
+
'wixsession',
|
|
38
|
+
'wixsession2',
|
|
39
|
+
]);
|
|
40
|
+
const NON_SECRET_ENVIRONMENT_KEY_NAMES = new Set(['tokencount', 'tokenizersparallelism', 'tokenusage']);
|
|
41
|
+
const BOUNDARY_ONLY_SENSITIVE_VALUE_MAX_LENGTH = 1;
|
|
28
42
|
export function collectSensitiveEnvValues(env) {
|
|
29
43
|
return [...new Set(Object.entries(env ?? {})
|
|
30
|
-
.filter(([key, value]) =>
|
|
44
|
+
.filter(([key, value]) => isSecretEnvironmentKey(key) && value.length > 0)
|
|
31
45
|
.map(([, value]) => value))]
|
|
32
46
|
.sort((a, b) => b.length - a.length);
|
|
33
47
|
}
|
|
@@ -37,11 +51,13 @@ export function collectSensitiveEnvValues(env) {
|
|
|
37
51
|
* is important for summaries, snippets, traces, and provider error text.
|
|
38
52
|
*/
|
|
39
53
|
export function sanitizePersistenceValue(source, explicitSensitiveValues = []) {
|
|
54
|
+
const explicitValues = [...new Set(explicitSensitiveValues.filter((value) => value.length > 0))]
|
|
55
|
+
.sort((a, b) => b.length - a.length);
|
|
40
56
|
const sensitiveValues = [...new Set([
|
|
41
|
-
...
|
|
57
|
+
...explicitValues,
|
|
42
58
|
...collectStructuredSensitiveValues(source),
|
|
43
59
|
])].sort((a, b) => b.length - a.length);
|
|
44
|
-
return sanitizeValue(source, '', sensitiveValues);
|
|
60
|
+
return sanitizeValue(source, '', sensitiveValues, new Set(explicitValues));
|
|
45
61
|
}
|
|
46
62
|
export function sanitizeToolEventResult(source, sensitiveValues) {
|
|
47
63
|
const result = {};
|
|
@@ -63,18 +79,27 @@ export function sanitizeToolEventResult(source, sensitiveValues) {
|
|
|
63
79
|
result.truncated = true;
|
|
64
80
|
return result;
|
|
65
81
|
}
|
|
66
|
-
function redactSensitiveValues(value, sensitiveValues) {
|
|
82
|
+
function redactSensitiveValues(value, sensitiveValues, explicitSensitiveValues) {
|
|
67
83
|
let redacted = value;
|
|
68
84
|
for (const secret of sensitiveValues) {
|
|
69
|
-
if (secret.length
|
|
85
|
+
if (secret.length === 0)
|
|
86
|
+
continue;
|
|
87
|
+
if (explicitSensitiveValues.has(secret) || secret.length > BOUNDARY_ONLY_SENSITIVE_VALUE_MAX_LENGTH) {
|
|
70
88
|
redacted = redacted.split(secret).join('<redacted>');
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const escaped = secret.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
92
|
+
const leftBoundary = /^[a-z0-9]/i.test(secret) ? '(^|[^a-z0-9])' : '()';
|
|
93
|
+
const rightBoundary = /[a-z0-9]$/i.test(secret) ? '(?=$|[^a-z0-9])' : '';
|
|
94
|
+
redacted = redacted.replace(new RegExp(`${leftBoundary}${escaped}${rightBoundary}`, 'g'), '$1<redacted>');
|
|
71
95
|
}
|
|
72
96
|
return redacted;
|
|
73
97
|
}
|
|
74
98
|
function collectStructuredSensitiveValues(source) {
|
|
75
99
|
const values = new Set();
|
|
76
100
|
const visit = (value, key) => {
|
|
77
|
-
if (typeof value === 'string' &&
|
|
101
|
+
if (typeof value === 'string' && !isNumericTokenTelemetry(key, value)
|
|
102
|
+
&& isSecretKey(key) && value.length > 0) {
|
|
78
103
|
values.add(value);
|
|
79
104
|
return;
|
|
80
105
|
}
|
|
@@ -91,26 +116,32 @@ function collectStructuredSensitiveValues(source) {
|
|
|
91
116
|
visit(source, '');
|
|
92
117
|
return [...values];
|
|
93
118
|
}
|
|
94
|
-
function sanitizeValue(value, key, sensitiveValues) {
|
|
95
|
-
if (isSecretKey(key))
|
|
119
|
+
function sanitizeValue(value, key, sensitiveValues, explicitSensitiveValues) {
|
|
120
|
+
if (!isNumericTokenTelemetry(key, value) && isSecretKey(key))
|
|
96
121
|
return '<redacted>';
|
|
97
|
-
if (typeof value === 'string')
|
|
98
|
-
return redactCredentialShapes(redactSensitiveValues(value, sensitiveValues));
|
|
99
|
-
|
|
100
|
-
|
|
122
|
+
if (typeof value === 'string') {
|
|
123
|
+
return redactCredentialShapes(redactSensitiveValues(value, sensitiveValues, explicitSensitiveValues), sensitiveValues, explicitSensitiveValues);
|
|
124
|
+
}
|
|
125
|
+
if (Array.isArray(value)) {
|
|
126
|
+
return value.map((entry) => sanitizeValue(entry, key, sensitiveValues, explicitSensitiveValues));
|
|
127
|
+
}
|
|
101
128
|
if (isRecord(value)) {
|
|
102
129
|
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
|
|
103
130
|
entryKey,
|
|
104
|
-
sanitizeValue(entryValue, entryKey, sensitiveValues),
|
|
131
|
+
sanitizeValue(entryValue, entryKey, sensitiveValues, explicitSensitiveValues),
|
|
105
132
|
]));
|
|
106
133
|
}
|
|
107
134
|
return value;
|
|
108
135
|
}
|
|
109
|
-
function redactCredentialShapes(value) {
|
|
136
|
+
function redactCredentialShapes(value, sensitiveValues, explicitSensitiveValues) {
|
|
110
137
|
let redacted = value;
|
|
111
138
|
const structured = parseStructuredJson(redacted);
|
|
112
139
|
if (structured !== undefined) {
|
|
113
|
-
|
|
140
|
+
const nestedSensitiveValues = [...new Set([
|
|
141
|
+
...sensitiveValues,
|
|
142
|
+
...collectStructuredSensitiveValues(structured),
|
|
143
|
+
])].sort((a, b) => b.length - a.length);
|
|
144
|
+
redacted = JSON.stringify(sanitizeValue(structured, '', nestedSensitiveValues, explicitSensitiveValues));
|
|
114
145
|
}
|
|
115
146
|
if (redacted.includes('PRIVATE KEY-----')) {
|
|
116
147
|
redacted = redacted.replace(/-----BEGIN [^-\n]*PRIVATE KEY-----[\s\S]*?-----END [^-\n]*PRIVATE KEY-----/g, '<redacted>');
|
|
@@ -128,12 +159,20 @@ function redactCredentialShapes(value) {
|
|
|
128
159
|
}
|
|
129
160
|
function isSecretKey(key) {
|
|
130
161
|
const normalized = key.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
162
|
+
if (normalized === 'tokens')
|
|
163
|
+
return true;
|
|
131
164
|
if (SECRET_KEY_NAMES.some((name) => normalized === name))
|
|
132
165
|
return true;
|
|
133
|
-
|
|
166
|
+
if (normalized !== 'tokens'
|
|
167
|
+
&& (normalized.endsWith('token') || /token(?:value|id|key|payload|secret)$/.test(normalized)))
|
|
168
|
+
return true;
|
|
169
|
+
if (/^(?:svsession|smsession|wixsession2?)(?:id|token|cookie|value)$/.test(normalized))
|
|
170
|
+
return true;
|
|
171
|
+
const compoundNames = SECRET_KEY_NAMES.filter((name) => !EXACT_ONLY_SECRET_KEY_NAMES.has(name));
|
|
134
172
|
if (compoundNames.some((name) => normalized.startsWith(name) || normalized.endsWith(name)))
|
|
135
173
|
return true;
|
|
136
|
-
const segments = key.
|
|
174
|
+
const segments = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
175
|
+
.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
137
176
|
for (let start = 0; start < segments.length; start++) {
|
|
138
177
|
let candidate = '';
|
|
139
178
|
for (let end = start; end < segments.length; end++) {
|
|
@@ -144,6 +183,36 @@ function isSecretKey(key) {
|
|
|
144
183
|
}
|
|
145
184
|
return false;
|
|
146
185
|
}
|
|
186
|
+
function isNumericTokenTelemetry(key, value) {
|
|
187
|
+
const normalized = key.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
188
|
+
if (normalized === 'tokencount') {
|
|
189
|
+
return (typeof value === 'number' && Number.isFinite(value))
|
|
190
|
+
|| (typeof value === 'string' && /^\d+$/.test(value));
|
|
191
|
+
}
|
|
192
|
+
if (!isRecord(value))
|
|
193
|
+
return false;
|
|
194
|
+
const entries = Object.entries(value);
|
|
195
|
+
if (normalized === 'tokenusage') {
|
|
196
|
+
return entries.length > 0 && entries.every(([entryKey, entryValue]) => (entryKey === 'inputTokens' || entryKey === 'outputTokens')
|
|
197
|
+
&& typeof entryValue === 'number' && Number.isFinite(entryValue));
|
|
198
|
+
}
|
|
199
|
+
if (normalized !== 'tokens' || entries.length === 0)
|
|
200
|
+
return false;
|
|
201
|
+
return entries.every(([entryKey, entryValue]) => {
|
|
202
|
+
if (entryKey === 'input' || entryKey === 'output' || entryKey === 'reasoning') {
|
|
203
|
+
return typeof entryValue === 'number' && Number.isFinite(entryValue);
|
|
204
|
+
}
|
|
205
|
+
return entryKey === 'cache' && isRecord(entryValue)
|
|
206
|
+
&& Object.entries(entryValue).every(([cacheKey, cacheValue]) => (cacheKey === 'write' || cacheKey === 'read')
|
|
207
|
+
&& typeof cacheValue === 'number' && Number.isFinite(cacheValue));
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function isSecretEnvironmentKey(key) {
|
|
211
|
+
const normalized = key.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
212
|
+
if (NON_SECRET_ENVIRONMENT_KEY_NAMES.has(normalized))
|
|
213
|
+
return false;
|
|
214
|
+
return isSecretKey(key);
|
|
215
|
+
}
|
|
147
216
|
function parseStructuredJson(value) {
|
|
148
217
|
const trimmed = value.trim();
|
|
149
218
|
if (!(trimmed.startsWith('{') && trimmed.endsWith('}'))
|
|
@@ -6,10 +6,11 @@ managed ChatGPT access session from Codex App Server.
|
|
|
6
6
|
|
|
7
7
|
## Install and authenticate
|
|
8
8
|
|
|
9
|
-
Install
|
|
9
|
+
Install Pathgrade and sign in once with Codex. Pathgrade installs its pinned
|
|
10
|
+
OAuth runtime dependency automatically:
|
|
10
11
|
|
|
11
12
|
```bash
|
|
12
|
-
yarn add -D @wix/pathgrade
|
|
13
|
+
yarn add -D @wix/pathgrade
|
|
13
14
|
codex login
|
|
14
15
|
```
|
|
15
16
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/pathgrade",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
4
4
|
"packageManager": "yarn@4.12.0",
|
|
5
5
|
"description": "Evaluate whether AI agents discover and use your skills correctly",
|
|
6
6
|
"exports": {
|
|
@@ -107,14 +107,10 @@
|
|
|
107
107
|
"node": ">=20.19.0"
|
|
108
108
|
},
|
|
109
109
|
"peerDependencies": {
|
|
110
|
-
"@openai-oauth/core": "2.0.0",
|
|
111
110
|
"jest": "^30.0.0",
|
|
112
111
|
"vitest": "^4.0.0"
|
|
113
112
|
},
|
|
114
113
|
"peerDependenciesMeta": {
|
|
115
|
-
"@openai-oauth/core": {
|
|
116
|
-
"optional": true
|
|
117
|
-
},
|
|
118
114
|
"jest": {
|
|
119
115
|
"optional": true
|
|
120
116
|
},
|
|
@@ -123,7 +119,6 @@
|
|
|
123
119
|
}
|
|
124
120
|
},
|
|
125
121
|
"devDependencies": {
|
|
126
|
-
"@openai-oauth/core": "2.0.0",
|
|
127
122
|
"@types/fs-extra": "^11.0.4",
|
|
128
123
|
"@types/jest": "^30.0.0",
|
|
129
124
|
"@types/picomatch": "^4.0.2",
|
|
@@ -135,6 +130,7 @@
|
|
|
135
130
|
"dependencies": {
|
|
136
131
|
"@anthropic-ai/claude-agent-sdk": "0.2.141",
|
|
137
132
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
133
|
+
"@openai-oauth/core": "2.0.0",
|
|
138
134
|
"@types/node": "25.6.0",
|
|
139
135
|
"ajv": "8.20.0",
|
|
140
136
|
"fs-extra": "11.3.3",
|
|
@@ -144,5 +140,5 @@
|
|
|
144
140
|
"typescript": "^5.9.3",
|
|
145
141
|
"zod": "4.3.6"
|
|
146
142
|
},
|
|
147
|
-
"falconPackageHash": "
|
|
143
|
+
"falconPackageHash": "0debc4519da71de54bc8ca58c57f1ddbf0899cc2b06cbf908371e1e8"
|
|
148
144
|
}
|