@wix/pathgrade 1.0.14 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +28 -0
  2. package/dist/adapters/jest/results.js +1 -1
  3. package/dist/adapters/node-test/index.js +1 -1
  4. package/dist/agents/claude/sdk-message-projector.js +3 -2
  5. package/dist/agents/claude/tool-permission-bridge.d.ts +4 -0
  6. package/dist/agents/claude/tool-permission-bridge.js +68 -1
  7. package/dist/agents/claude.d.ts +2 -0
  8. package/dist/agents/claude.js +55 -10
  9. package/dist/agents/codex-app-server/agent.js +94 -76
  10. package/dist/agents/codex-app-server/mcp-approval-correlator.d.ts +55 -0
  11. package/dist/agents/codex-app-server/mcp-approval-correlator.js +299 -0
  12. package/dist/core/canonical-json.d.ts +2 -0
  13. package/dist/core/canonical-json.js +51 -0
  14. package/dist/core/generated-mcp-protocol.d.ts +19 -0
  15. package/dist/core/generated-mcp-protocol.js +26 -0
  16. package/dist/core/mcp-mock.d.ts +1 -1
  17. package/dist/core/mcp-mock.js +24 -0
  18. package/dist/core/mcp-mock.types.d.ts +6 -0
  19. package/dist/core/mcp-schema-profile.d.ts +10 -0
  20. package/dist/core/mcp-schema-profile.js +91 -0
  21. package/dist/mcp-mock-server.js +29 -13
  22. package/dist/providers/mcp-config.js +4 -2
  23. package/dist/providers/scripted-mcp-mock-host.d.ts +39 -0
  24. package/dist/providers/scripted-mcp-mock-host.js +368 -0
  25. package/dist/reporters/cli.js +4 -3
  26. package/dist/reporters/github-comment.js +1 -1
  27. package/dist/reporters/report-summary.js +1 -0
  28. package/dist/reporting/core.js +19 -5
  29. package/dist/reporting/types.d.ts +2 -1
  30. package/dist/runners/model-builders.js +1 -1
  31. package/dist/runners/model-validation.js +4 -1
  32. package/dist/runners/model.d.ts +1 -1
  33. package/dist/runners/report-projection.js +2 -2
  34. package/dist/runners/vitest-adapter.js +1 -1
  35. package/dist/sdk/agent.js +64 -19
  36. package/dist/sdk/diagnostics.d.ts +1 -0
  37. package/dist/sdk/diagnostics.js +6 -3
  38. package/dist/sdk/index.d.ts +5 -4
  39. package/dist/sdk/index.js +2 -2
  40. package/dist/sdk/lifecycle.js +3 -3
  41. package/dist/sdk/managed-session.d.ts +3 -0
  42. package/dist/sdk/managed-session.js +71 -27
  43. package/dist/sdk/mcp-event-input.d.ts +3 -0
  44. package/dist/sdk/mcp-event-input.js +8 -0
  45. package/dist/sdk/mcp-evidence.d.ts +39 -0
  46. package/dist/sdk/mcp-evidence.js +71 -12
  47. package/dist/sdk/mcp-mock-approvals.d.ts +40 -0
  48. package/dist/sdk/mcp-mock-approvals.js +235 -0
  49. package/dist/sdk/scripted-mcp-events.d.ts +24 -0
  50. package/dist/sdk/scripted-mcp-events.js +30 -0
  51. package/dist/sdk/types.d.ts +6 -2
  52. package/dist/tool-events.d.ts +16 -1
  53. package/dist/types.d.ts +3 -1
  54. package/dist/viewer.html +4 -4
  55. package/package.json +3 -2
@@ -4,11 +4,13 @@ import { assertMcpSecretReferencesReady } from '../../providers/mcp-config.js';
4
4
  import { buildSummary, enrichSkillEvents, extractSkillNameFromPath, inferCodexExecAction, } from '../../tool-events.js';
5
5
  import { requireAskBusForLiveBatches } from '../../sdk/ask-bus/bus.js';
6
6
  import { toAskUserToolEvent } from '../../sdk/ask-bus/projection.js';
7
- import { decideMcpToolCall, redactMcpSecrets, } from '../../sdk/mcp-safety.js';
7
+ import { decideMcpToolCall } from '../../sdk/mcp-safety.js';
8
+ import { attachOriginalMcpInput } from '../../sdk/mcp-event-input.js';
8
9
  import { spawnAppServerTransport, } from './transport.js';
9
10
  import { normalizeUpstreamQuestion, toWireAnswerMap, } from './wire-translators.js';
10
11
  import { extractTurnCompletionFailure } from './turn-completion.js';
11
12
  import { resolveCodexModel } from '../codex-model.js';
13
+ import { CodexMcpApprovalCorrelator, extractMcpToolApprovalRequest, hasScriptedApprovalPolicy, isMcpToolCallApprovalRequest, recordPolicyDeniedMcpToolCall } from './mcp-approval-correlator.js';
12
14
  const TURN_COMPLETED_METHOD = 'turn/completed';
13
15
  function recordFromUnknown(value) {
14
16
  if (value && typeof value === 'object' && !Array.isArray(value)) {
@@ -16,32 +18,6 @@ function recordFromUnknown(value) {
16
18
  }
17
19
  return {};
18
20
  }
19
- function isMcpToolCallApprovalRequest(params) {
20
- const meta = recordFromUnknown(recordFromUnknown(params)._meta);
21
- return meta.codex_approval_kind === 'mcp_tool_call';
22
- }
23
- function extractMcpToolApprovalRequest(params) {
24
- if (!isMcpToolCallApprovalRequest(params))
25
- return undefined;
26
- const record = recordFromUnknown(params);
27
- const meta = recordFromUnknown(record._meta);
28
- const serverName = typeof record.serverName === 'string' ? record.serverName : undefined;
29
- const toolName = typeof meta.toolName === 'string' ? meta.toolName
30
- : typeof meta.tool_name === 'string' ? meta.tool_name
31
- : typeof meta.name === 'string' ? meta.name
32
- : typeof record.message === 'string' ? parseToolNameFromApprovalMessage(record.message)
33
- : undefined;
34
- if (!serverName || !toolName)
35
- return undefined;
36
- return {
37
- serverName,
38
- toolName,
39
- arguments: recordFromUnknown(meta.tool_params),
40
- };
41
- }
42
- function parseToolNameFromApprovalMessage(message) {
43
- return message.match(/tool\s+"([^"]+)"/i)?.[1];
44
- }
45
21
  function extractCommandActionSkillName(action) {
46
22
  if (typeof action.path === 'string') {
47
23
  const direct = extractSkillNameFromPath(action.path);
@@ -127,12 +103,16 @@ function projectItemIntoTurn(item, turn) {
127
103
  }
128
104
  if (item.type === 'mcpToolCall') {
129
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;
130
109
  const args = recordFromUnknown(call.arguments);
131
110
  const providerToolName = `${call.server}.${call.tool}`;
132
- turn.nonAskToolEvents.push({
111
+ turn.nonAskToolEvents.push(attachOriginalMcpInput({
133
112
  action: 'mcp_tool_call',
134
113
  provider: 'codex',
135
114
  providerToolName,
115
+ toolUseId: call.id,
136
116
  turnNumber: turn.turnNumber,
137
117
  arguments: {
138
118
  ...args,
@@ -143,7 +123,8 @@ function projectItemIntoTurn(item, turn) {
143
123
  summary: `MCP tool ${providerToolName} ${call.status ?? 'unknown'}`,
144
124
  confidence: 'high',
145
125
  rawSnippet: JSON.stringify(call),
146
- });
126
+ ...(call.result !== undefined ? { result: { content: JSON.stringify(call.result) } } : {}),
127
+ }, args));
147
128
  return;
148
129
  }
149
130
  }
@@ -167,36 +148,13 @@ function projectMcpStartupStatusIntoTurn(params, turn) {
167
148
  });
168
149
  if (status === 'failed') {
169
150
  const message = `MCP server ${name} failed to start${error ? `: ${error}` : ''}`;
170
- turn.turnFailed = true;
171
- turn.failureMessage = message;
172
- turn.signalFailure?.(message);
151
+ failTurn(turn, message);
173
152
  }
174
153
  }
175
- function recordPolicyDeniedMcpToolCall(turn, request, decision, rawParams) {
176
- if (!turn)
177
- return;
178
- const args = redactMcpSecrets(request.arguments);
179
- const providerToolName = `${request.serverName}.${request.toolName}`;
180
- turn.nonAskToolEvents.push({
181
- action: 'mcp_tool_call',
182
- provider: 'codex',
183
- providerToolName,
184
- turnNumber: turn.turnNumber,
185
- arguments: {
186
- ...args,
187
- server: request.serverName,
188
- tool: request.toolName,
189
- status: 'policy_denied',
190
- policyResult: {
191
- action: 'deny',
192
- reason: decision.reason,
193
- message: decision.message,
194
- },
195
- },
196
- summary: `MCP tool ${providerToolName} policy_denied`,
197
- confidence: 'high',
198
- rawSnippet: JSON.stringify(redactMcpSecrets(rawParams)),
199
- });
154
+ function failTurn(turn, message) {
155
+ turn.turnFailed = true;
156
+ turn.failureMessage = message;
157
+ turn.signalFailure?.(message);
200
158
  }
201
159
  function isLiveMcpSafetyMode(options) {
202
160
  const runMode = options?.runMode ?? 'mock';
@@ -220,13 +178,19 @@ export class CodexAppServerAgent extends BaseAgent {
220
178
  let closeInfo = null;
221
179
  let activeTurn = null;
222
180
  let disposed = false;
181
+ let codexUserAgent = 'unknown';
182
+ const scriptedHost = options?.scriptedMcpHost;
183
+ const correlator = scriptedHost
184
+ ? new CodexMcpApprovalCorrelator(scriptedHost, () => turnCounter, () => threadId, () => options?.getRemainingMs?.() ?? Number.POSITIVE_INFINITY)
185
+ : undefined;
223
186
  const ensureTransport = async () => {
224
187
  if (disposed)
225
188
  throw new Error('CodexAppServerAgent session disposed');
226
189
  if (handle)
227
190
  return handle.transport;
228
191
  const factory = this.deps.createTransport
229
- ?? (async (ctx) => spawnAppServerTransport({ cwd: ctx.workspacePath, env: ctx.env }));
192
+ ?? (async (ctx) => spawnAppServerTransport({ cwd: ctx.workspacePath, env: ctx.env,
193
+ args: scriptedHost ? ['--enable', 'tool_call_mcp_elicitation'] : [] }));
230
194
  handle = await factory({ workspacePath, env: runtimeEnv });
231
195
  const transport = handle.transport;
232
196
  transport.onServerRequest((req) => this.dispatchServerRequest(req, {
@@ -235,6 +199,8 @@ export class CodexAppServerAgent extends BaseAgent {
235
199
  activeTurn: () => activeTurn,
236
200
  onPermissionGrant: this.deps.onPermissionGrant,
237
201
  mcpSafety: options?.mcpSafety,
202
+ scriptedHost,
203
+ correlator,
238
204
  }));
239
205
  transport.onClose((info) => {
240
206
  closeInfo = info;
@@ -250,20 +216,37 @@ export class CodexAppServerAgent extends BaseAgent {
250
216
  }
251
217
  return;
252
218
  }
253
- if (n.method !== 'item/completed')
254
- return;
255
219
  const turn = activeTurn;
256
220
  if (!turn)
257
221
  return;
258
222
  const params = n.params;
223
+ if (n.method === 'item/started') {
224
+ try {
225
+ correlator?.started(n.params);
226
+ }
227
+ catch (error) {
228
+ failTurn(turn, error instanceof Error ? error.message : String(error));
229
+ }
230
+ return;
231
+ }
232
+ if (n.method !== 'item/completed')
233
+ return;
259
234
  if (!params?.item)
260
235
  return;
261
- projectItemIntoTurn(params.item, turn);
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
+ }
262
243
  });
263
- await transport.sendRequest('initialize', {
244
+ const initialized = await transport.sendRequest('initialize', {
264
245
  clientInfo: { name: 'pathgrade', version: '0.5.0', title: null },
265
246
  capabilities: { experimentalApi: true, optOutNotificationMethods: null },
266
247
  });
248
+ if (typeof initialized.userAgent === 'string')
249
+ codexUserAgent = initialized.userAgent.slice(0, 200);
267
250
  // Upstream ClientNotification = { method: "initialized" }: send it
268
251
  // before any thread/start so the handshake matches the v0.124
269
252
  // contract and is forward-compatible with servers that enforce it.
@@ -281,6 +264,7 @@ export class CodexAppServerAgent extends BaseAgent {
281
264
  turnFailed: false,
282
265
  };
283
266
  activeTurn = turn;
267
+ correlator?.beginTurn();
284
268
  try {
285
269
  if (threadId === null) {
286
270
  if (options?.mcpConfigPath && isLiveMcpSafetyMode(options.mcpSafety)) {
@@ -290,13 +274,16 @@ export class CodexAppServerAgent extends BaseAgent {
290
274
  env: runtimeEnv,
291
275
  });
292
276
  }
293
- const mcpConfig = options?.mcpConfigPath
277
+ const mcpConfig = scriptedHost?.codexConfig ?? (options?.mcpConfigPath
294
278
  ? await mountMcpForCodexAppServer({
295
279
  workspacePath,
296
280
  mcpConfigPath: options.mcpConfigPath,
297
281
  })
298
- : undefined;
299
- const resp = await t.sendRequest('thread/start', buildThreadStartParams({ cwd: workspacePath, model, sandboxMode, mcpConfig }));
282
+ : undefined);
283
+ const resp = await t.sendRequest('thread/start', buildThreadStartParams({ cwd: workspacePath, model, sandboxMode, mcpConfig, scripted: !!scriptedHost }));
284
+ if (scriptedHost && !hasScriptedApprovalPolicy(resp)) {
285
+ throw new Error(`Codex app-server scripted MCP approval policy drift (userAgent=${codexUserAgent})`);
286
+ }
300
287
  threadId = resp.thread.id;
301
288
  }
302
289
  // Wait for TurnCompleted OR subprocess crash OR dispatcher failure.
@@ -352,8 +339,7 @@ export class CodexAppServerAgent extends BaseAgent {
352
339
  }
353
340
  }
354
341
  });
355
- const startTurnResp = t
356
- .sendRequest('turn/start', {
342
+ const startTurnResp = t.sendRequest('turn/start', {
357
343
  threadId,
358
344
  input: [{ type: 'text', text: message, text_elements: [] }],
359
345
  })
@@ -369,6 +355,18 @@ export class CodexAppServerAgent extends BaseAgent {
369
355
  : 'turn/start failed';
370
356
  turn.signalFailure?.(msg);
371
357
  });
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
+ }
372
370
  try {
373
371
  await turnCompletion;
374
372
  }
@@ -383,7 +381,8 @@ export class CodexAppServerAgent extends BaseAgent {
383
381
  signal: info.signal,
384
382
  });
385
383
  }
386
- void startTurnResp;
384
+ if (!scriptedHost)
385
+ void startTurnResp;
387
386
  if (turn.turnFailed) {
388
387
  return assembleTurnResult({
389
388
  askBus,
@@ -401,6 +400,7 @@ export class CodexAppServerAgent extends BaseAgent {
401
400
  }
402
401
  finally {
403
402
  activeTurn = null;
403
+ correlator?.endTurn();
404
404
  }
405
405
  };
406
406
  const dispose = async () => {
@@ -439,12 +439,16 @@ export class CodexAppServerAgent extends BaseAgent {
439
439
  };
440
440
  }
441
441
  dispatchServerRequest(req, ctx) {
442
- const { transport, askBus, activeTurn, onPermissionGrant, mcpSafety } = ctx;
442
+ const { transport, askBus, activeTurn, onPermissionGrant, mcpSafety, scriptedHost, correlator } = ctx;
443
443
  switch (req.method) {
444
444
  case 'item/tool/requestUserInput':
445
445
  void handleRequestUserInput(req, { transport, askBus, activeTurn });
446
446
  return;
447
447
  case 'item/permissions/requestApproval': {
448
+ if (scriptedHost) {
449
+ transport.sendResponse(req.id, { permissions: {}, scope: 'turn', strictAutoReview: false });
450
+ return;
451
+ }
448
452
  const params = (req.params ?? {});
449
453
  transport.sendResponse(req.id, {
450
454
  permissions: params.permissions ?? {},
@@ -467,12 +471,25 @@ export class CodexAppServerAgent extends BaseAgent {
467
471
  case 'item/fileChange/requestApproval':
468
472
  case 'applyPatchApproval':
469
473
  case 'execCommandApproval':
470
- transport.sendResponse(req.id, { decision: 'approved', scope: 'turn' });
474
+ transport.sendResponse(req.id, scriptedHost
475
+ ? { decision: 'denied' }
476
+ : { decision: 'approved', scope: 'turn' });
471
477
  return;
472
478
  case 'item/tool/call':
473
479
  transport.sendResponse(req.id, { status: 'declined' });
474
480
  return;
475
481
  case 'mcpServer/elicitation/request':
482
+ if (scriptedHost && correlator) {
483
+ void correlator.decide(req.id, req.params).then((correlated) => {
484
+ activeTurn()?.nonAskToolEvents.push(...correlated.events);
485
+ transport.sendResponse(req.id, {
486
+ action: correlated.action,
487
+ content: correlated.action === 'accept' ? {} : null,
488
+ _meta: null,
489
+ });
490
+ });
491
+ return;
492
+ }
476
493
  if (isMcpToolCallApprovalRequest(req.params)) {
477
494
  const toolRequest = extractMcpToolApprovalRequest(req.params);
478
495
  if (toolRequest) {
@@ -502,11 +519,8 @@ export class CodexAppServerAgent extends BaseAgent {
502
519
  const message = 'codex app-server requires OPENAI_API_KEY for pathgrade and honors OPENAI_BASE_URL when set; ChatGPT/cached auth unsupported under transport=app-server';
503
520
  transport.sendErrorResponse(req.id, -32001, message);
504
521
  const turn = activeTurn();
505
- if (turn) {
506
- turn.turnFailed = true;
507
- turn.failureMessage = message;
508
- turn.signalFailure?.(message);
509
- }
522
+ if (turn)
523
+ failTurn(turn, message);
510
524
  return;
511
525
  }
512
526
  default:
@@ -549,7 +563,11 @@ async function handleRequestUserInput(req, ctx) {
549
563
  function buildThreadStartParams(opts) {
550
564
  return {
551
565
  cwd: opts.cwd,
552
- approvalPolicy: 'never',
566
+ approvalPolicy: opts.scripted ? { granular: {
567
+ sandbox_approval: false, rules: false, skill_approval: false,
568
+ request_permissions: false, mcp_elicitations: true,
569
+ } } : 'never',
570
+ ...(opts.scripted ? { approvalsReviewer: 'user' } : {}),
553
571
  sandbox: opts.sandboxMode,
554
572
  ephemeral: true,
555
573
  experimentalRawEvents: false,
@@ -0,0 +1,55 @@
1
+ import type { ToolEvent } from '../../tool-events.js';
2
+ import type { ScriptedMcpMockHost } from '../../providers/scripted-mcp-mock-host.js';
3
+ import { type McpToolPolicyDecision } from '../../sdk/mcp-safety.js';
4
+ export interface CorrelatedApprovalResponse {
5
+ action: 'accept' | 'decline';
6
+ events: ToolEvent[];
7
+ }
8
+ export declare function isMcpToolCallApprovalRequest(params: unknown): boolean;
9
+ export declare function hasScriptedApprovalPolicy(response: {
10
+ approvalPolicy?: unknown;
11
+ approvalsReviewer?: unknown;
12
+ }): boolean;
13
+ export declare function extractMcpToolApprovalRequest(params: unknown): {
14
+ serverName: string;
15
+ toolName: string;
16
+ arguments: Record<string, unknown>;
17
+ } | undefined;
18
+ export declare function recordPolicyDeniedMcpToolCall(turn: {
19
+ turnNumber: number;
20
+ nonAskToolEvents: ToolEvent[];
21
+ } | null, request: {
22
+ serverName: string;
23
+ toolName: string;
24
+ arguments: Record<string, unknown>;
25
+ }, decision: Extract<McpToolPolicyDecision, {
26
+ action: 'deny';
27
+ }>, rawParams: unknown): void;
28
+ export declare class CodexMcpApprovalCorrelator {
29
+ private readonly host;
30
+ private readonly getTurnNumber;
31
+ private readonly getThreadId;
32
+ private readonly getRemainingMs;
33
+ private live;
34
+ private terminal;
35
+ private decisions;
36
+ private active;
37
+ private authoritativeTurn?;
38
+ private authorityPromise;
39
+ private resolveAuthority?;
40
+ private waitingRequests;
41
+ private poisoned;
42
+ constructor(host: ScriptedMcpMockHost, getTurnNumber: () => number, getThreadId: () => string | null, getRemainingMs?: () => number);
43
+ started(params: unknown): void;
44
+ completed(params: unknown): boolean;
45
+ decide(requestId: string | number, rawParams: unknown): Promise<CorrelatedApprovalResponse>;
46
+ beginTurn(): void;
47
+ setAuthoritativeTurn(threadId: string, turnId: string): boolean;
48
+ endTurn(): void;
49
+ private protocolEvent;
50
+ private waitForAuthority;
51
+ private parseLifecycleRecord;
52
+ private validateLifecycleTurn;
53
+ private failLifecycle;
54
+ private poison;
55
+ }