@wix/pathgrade 1.0.20 → 1.0.21
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 +4 -2
- package/dist/agents/codex-app-server/agent.d.ts +0 -9
- package/dist/agents/codex-app-server/agent.js +73 -57
- package/dist/agents/codex-app-server/mcp-approval-correlator.d.ts +57 -4
- package/dist/agents/codex-app-server/mcp-approval-correlator.js +66 -9
- package/dist/agents/codex-app-server/protocol/ClientRequest.js +1 -1
- package/dist/agents/codex-app-server/protocol/DynamicToolCallParams.js +1 -1
- package/dist/agents/codex-app-server/protocol/GrantedPermissionProfile.js +1 -1
- package/dist/agents/codex-app-server/protocol/McpElicitationRequestParams.d.ts +5 -0
- package/dist/agents/codex-app-server/protocol/McpElicitationRequestParams.js +1 -1
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalParams.d.ts +3 -0
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalParams.js +1 -1
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalResponse.d.ts +1 -1
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalResponse.js +1 -1
- package/dist/agents/codex-app-server/protocol/SandboxMode.js +1 -1
- package/dist/agents/codex-app-server/protocol/ServerRequest.js +2 -2
- package/dist/agents/codex-app-server/protocol/ThreadStartParams.d.ts +14 -18
- package/dist/agents/codex-app-server/protocol/ThreadStartParams.js +3 -3
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputAnswer.js +1 -1
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputOption.js +1 -1
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputParams.d.ts +3 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputParams.js +1 -1
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputQuestion.js +1 -1
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputResponse.js +1 -1
- package/dist/agents/codex-app-server/protocol/TurnCompletedNotification.js +1 -1
- package/dist/agents/codex-app-server/protocol/index.js +1 -1
- package/dist/providers/mcp-config.d.ts +2 -0
- package/dist/providers/mcp-config.js +5 -2
- package/dist/providers/workspace.d.ts +2 -1
- package/dist/providers/workspace.js +2 -1
- package/dist/sdk/judge-prompt-builder.js +13 -1
- package/dist/sdk/managed-session.js +1 -0
- package/dist/sdk/mcp-mock-approvals.js +2 -2
- package/dist/types.d.ts +3 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -350,8 +350,10 @@ Read-only metadata is explicit: add `annotations.readOnlyHint: true` only when
|
|
|
350
350
|
the operation being modeled is read-only. Unannotated and explicitly mutating
|
|
351
351
|
mock tools retain the runtime's normal approval behavior.
|
|
352
352
|
|
|
353
|
-
To evaluate
|
|
354
|
-
|
|
353
|
+
To evaluate through the receipt-backed mock host with Claude or Codex
|
|
354
|
+
app-server, pass an ordered `mcpMockApprovalRules` array. Use an empty array for
|
|
355
|
+
read-only-only mocks. Mutation scenarios require explicit rules; the first
|
|
356
|
+
matching rule wins:
|
|
355
357
|
|
|
356
358
|
```ts
|
|
357
359
|
const agent = await createAgent({
|
|
@@ -2,13 +2,6 @@ import { AgentCommandRunner, AgentSession, AgentSessionOptions, BaseAgent, Envir
|
|
|
2
2
|
import { type AppServerSessionHandle } from './transport.js';
|
|
3
3
|
import { type LifecycleClock } from './item-lifecycle.js';
|
|
4
4
|
type SandboxMode = 'workspace-write' | 'danger-full-access';
|
|
5
|
-
export interface PermissionGrantLogEntry {
|
|
6
|
-
type: 'permissions_granted';
|
|
7
|
-
turnNumber: number;
|
|
8
|
-
requested: unknown;
|
|
9
|
-
scope: 'turn';
|
|
10
|
-
strictAutoReview: false;
|
|
11
|
-
}
|
|
12
5
|
export interface CodexAppServerAgentDeps {
|
|
13
6
|
/**
|
|
14
7
|
* Inject a transport factory for tests. Default: spawn `codex app-server`
|
|
@@ -24,8 +17,6 @@ export interface CodexAppServerAgentDeps {
|
|
|
24
17
|
}) => Promise<AppServerSessionHandle>;
|
|
25
18
|
/** Sandbox mode for `thread/start`. Default: 'workspace-write'. */
|
|
26
19
|
sandboxMode?: SandboxMode;
|
|
27
|
-
/** Observer for per-grant audit entries (§7 of design decisions). */
|
|
28
|
-
onPermissionGrant?: (entry: PermissionGrantLogEntry) => void;
|
|
29
20
|
/** Injectable clocks keep wall timestamps and monotonic durations independently testable. */
|
|
30
21
|
clock?: LifecycleClock;
|
|
31
22
|
/** Inject Codex-managed ChatGPT access sessions for deterministic tests. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseAgent, getRuntimeEnv, getWorkspacePath, } from '../../types.js';
|
|
2
|
-
import { mountMcpForCodexAppServer } from '../../providers/mcp-runtime-mounting.js';
|
|
2
|
+
import { assertUnsupportedLiveMcpSafetyRuntime, mountMcpForCodexAppServer, } from '../../providers/mcp-runtime-mounting.js';
|
|
3
3
|
import { assertMcpSecretReferencesReady } from '../../providers/mcp-config.js';
|
|
4
4
|
import { enrichSkillEvents, } from '../../tool-events.js';
|
|
5
5
|
import { collectSensitiveEnvValues, sanitizePersistenceValue, } from '../../tool-event-results.js';
|
|
@@ -7,12 +7,11 @@ import { requireAskBusForLiveBatches } from '../../sdk/ask-bus/bus.js';
|
|
|
7
7
|
import { attachTurnResultSensitiveValues } from '../../sdk/turn-result-secrets.js';
|
|
8
8
|
import { attachToolEventSensitiveValues } from '../../sdk/tool-event-secrets.js';
|
|
9
9
|
import { toAskUserToolEvent } from '../../sdk/ask-bus/projection.js';
|
|
10
|
-
import { decideMcpToolCall } from '../../sdk/mcp-safety.js';
|
|
11
10
|
import { spawnAppServerTransport, } from './transport.js';
|
|
12
11
|
import { normalizeUpstreamQuestion, toWireAnswerMap, } from './wire-translators.js';
|
|
13
12
|
import { extractTurnCompletionFailure } from './turn-completion.js';
|
|
14
13
|
import { resolveCodexModel } from '../codex-model.js';
|
|
15
|
-
import { CodexMcpApprovalCorrelator,
|
|
14
|
+
import { buildLiveMcpProtocolErrorEvent, buildPolicyDeniedMcpToolEvent, CodexMcpApprovalCorrelator, consumeMatchingMcpDenial, evaluateLiveMcpApproval, hasScriptedApprovalPolicy, isMcpToolCallApprovalRequest, queuePolicyDeniedMcpToolCall, } from './mcp-approval-correlator.js';
|
|
16
15
|
import { projectItemIntoTurn } from './item-projection.js';
|
|
17
16
|
import { CodexItemLifecycle, } from './item-lifecycle.js';
|
|
18
17
|
import { createCodexManagedAuth } from './managed-auth.js';
|
|
@@ -45,6 +44,26 @@ function failTurn(turn, message) {
|
|
|
45
44
|
turn.failureMessage = message;
|
|
46
45
|
turn.signalFailure?.(message);
|
|
47
46
|
}
|
|
47
|
+
function recordFromUnknown(value) {
|
|
48
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
49
|
+
? value
|
|
50
|
+
: {};
|
|
51
|
+
}
|
|
52
|
+
function projectCompletedItem(item, timing, params, turn, correlator, sensitiveValues) {
|
|
53
|
+
if (correlator?.completed(params) === false)
|
|
54
|
+
return;
|
|
55
|
+
const mcpItem = item.type === 'mcpToolCall'
|
|
56
|
+
? item
|
|
57
|
+
: undefined;
|
|
58
|
+
const pendingDenial = mcpItem?.status === 'failed'
|
|
59
|
+
? consumeMatchingMcpDenial(turn, mcpItem.server, mcpItem.tool, recordFromUnknown(mcpItem.arguments))
|
|
60
|
+
: undefined;
|
|
61
|
+
if (!pendingDenial || !mcpItem) {
|
|
62
|
+
projectItemIntoTurn(item, turn, sensitiveValues, timing);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
turn.nonAskToolEvents.push(buildPolicyDeniedMcpToolEvent(turn.turnNumber, pendingDenial, mcpItem));
|
|
66
|
+
}
|
|
48
67
|
function extractTurnCompletionIdentity(params) {
|
|
49
68
|
if (!params || typeof params !== 'object' || Array.isArray(params))
|
|
50
69
|
return {};
|
|
@@ -74,6 +93,12 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
74
93
|
const sensitiveValues = collectSensitiveEnvValues(runtimeEnv);
|
|
75
94
|
const model = resolveCodexModel(options?.model);
|
|
76
95
|
const sandboxMode = this.deps.sandboxMode ?? 'workspace-write';
|
|
96
|
+
await assertUnsupportedLiveMcpSafetyRuntime({
|
|
97
|
+
runtimeName: 'Codex app-server',
|
|
98
|
+
workspacePath,
|
|
99
|
+
mcpConfigPath: options?.mcpConfigPath,
|
|
100
|
+
mcpSafety: options?.mcpSafety,
|
|
101
|
+
});
|
|
77
102
|
let handle = null;
|
|
78
103
|
let threadId = null;
|
|
79
104
|
let turnCounter = 0;
|
|
@@ -106,7 +131,6 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
106
131
|
transport,
|
|
107
132
|
askBus,
|
|
108
133
|
activeTurn: () => activeTurn,
|
|
109
|
-
onPermissionGrant: this.deps.onPermissionGrant,
|
|
110
134
|
mcpSafety: options?.mcpSafety,
|
|
111
135
|
scriptedHost,
|
|
112
136
|
correlator,
|
|
@@ -150,7 +174,7 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
150
174
|
codexUserAgent = initialized.userAgent.slice(0, 200);
|
|
151
175
|
}
|
|
152
176
|
// Upstream ClientNotification = { method: "initialized" }: send it
|
|
153
|
-
// before any thread/start so the handshake matches the v0.
|
|
177
|
+
// before any thread/start so the handshake matches the v0.149
|
|
154
178
|
// contract and is forward-compatible with servers that enforce it.
|
|
155
179
|
transport.sendNotification('initialized', null);
|
|
156
180
|
await managedAuth.login(transport);
|
|
@@ -170,6 +194,7 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
170
194
|
turnNumber: turnCounter,
|
|
171
195
|
askBatchIds: [],
|
|
172
196
|
nonAskToolEvents: [],
|
|
197
|
+
pendingMcpDenials: [],
|
|
173
198
|
assistantMessageParts: [],
|
|
174
199
|
turnFailed: false,
|
|
175
200
|
pendingTurnCompletions: [],
|
|
@@ -191,7 +216,13 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
191
216
|
mcpConfigPath: options.mcpConfigPath,
|
|
192
217
|
})
|
|
193
218
|
: undefined);
|
|
194
|
-
const resp = await t.sendRequest('thread/start', buildThreadStartParams({
|
|
219
|
+
const resp = await t.sendRequest('thread/start', buildThreadStartParams({
|
|
220
|
+
cwd: workspacePath,
|
|
221
|
+
model,
|
|
222
|
+
sandboxMode,
|
|
223
|
+
mcpConfig,
|
|
224
|
+
enableMcpElicitations: !!scriptedHost || shouldEnableMcpElicitations(options),
|
|
225
|
+
}));
|
|
195
226
|
if (scriptedHost && !hasScriptedApprovalPolicy(resp)) {
|
|
196
227
|
throw new Error(`Codex app-server scripted MCP approval policy drift (userAgent=${codexUserAgent})`);
|
|
197
228
|
}
|
|
@@ -213,9 +244,7 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
213
244
|
},
|
|
214
245
|
onCompleted: (item, timing, params) => {
|
|
215
246
|
try {
|
|
216
|
-
|
|
217
|
-
projectItemIntoTurn(item, turn, sensitiveValues, timing);
|
|
218
|
-
}
|
|
247
|
+
projectCompletedItem(item, timing, params, turn, correlator, sensitiveValues);
|
|
219
248
|
}
|
|
220
249
|
catch (error) {
|
|
221
250
|
failTurn(turn, error instanceof Error ? error.message : String(error));
|
|
@@ -404,41 +433,25 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
404
433
|
};
|
|
405
434
|
}
|
|
406
435
|
dispatchServerRequest(req, ctx) {
|
|
407
|
-
const { transport, askBus, activeTurn,
|
|
436
|
+
const { transport, askBus, activeTurn, mcpSafety, scriptedHost, correlator } = ctx;
|
|
408
437
|
switch (req.method) {
|
|
409
438
|
case 'item/tool/requestUserInput':
|
|
410
439
|
void handleRequestUserInput(req, { transport, askBus, activeTurn });
|
|
411
440
|
return;
|
|
412
441
|
case 'item/permissions/requestApproval': {
|
|
413
|
-
|
|
414
|
-
transport.sendResponse(req.id, { permissions: {}, scope: 'turn', strictAutoReview: false });
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
const params = (req.params ?? {});
|
|
418
|
-
transport.sendResponse(req.id, {
|
|
419
|
-
permissions: params.permissions ?? {},
|
|
420
|
-
scope: 'turn',
|
|
421
|
-
strictAutoReview: false,
|
|
422
|
-
});
|
|
423
|
-
const turn = activeTurn();
|
|
424
|
-
if (onPermissionGrant) {
|
|
425
|
-
onPermissionGrant({
|
|
426
|
-
type: 'permissions_granted',
|
|
427
|
-
turnNumber: turn?.turnNumber ?? 0,
|
|
428
|
-
requested: params.permissions ?? {},
|
|
429
|
-
scope: 'turn',
|
|
430
|
-
strictAutoReview: false,
|
|
431
|
-
});
|
|
432
|
-
}
|
|
442
|
+
transport.sendErrorResponse(req.id, -32000, 'pathgrade: permission grants are not authorized by the MCP-only approval policy');
|
|
433
443
|
return;
|
|
434
444
|
}
|
|
435
445
|
case 'item/commandExecution/requestApproval':
|
|
436
446
|
case 'item/fileChange/requestApproval':
|
|
447
|
+
transport.sendResponse(req.id, { decision: 'decline' });
|
|
448
|
+
return;
|
|
437
449
|
case 'applyPatchApproval':
|
|
438
450
|
case 'execCommandApproval':
|
|
439
|
-
transport.sendResponse(req.id,
|
|
440
|
-
|
|
441
|
-
|
|
451
|
+
transport.sendResponse(req.id, { decision: { denied: {
|
|
452
|
+
rejection: 'Pathgrade does not authorize legacy command or patch approvals.',
|
|
453
|
+
} },
|
|
454
|
+
});
|
|
442
455
|
return;
|
|
443
456
|
case 'item/tool/call':
|
|
444
457
|
transport.sendResponse(req.id, { status: 'declined' });
|
|
@@ -456,25 +469,17 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
456
469
|
return;
|
|
457
470
|
}
|
|
458
471
|
if (isMcpToolCallApprovalRequest(req.params)) {
|
|
459
|
-
const
|
|
460
|
-
if (
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
_meta: {
|
|
468
|
-
pathgrade_policy_denial: {
|
|
469
|
-
reason: decision.reason,
|
|
470
|
-
message: decision.message,
|
|
471
|
-
},
|
|
472
|
-
},
|
|
473
|
-
});
|
|
474
|
-
return;
|
|
472
|
+
const evaluation = evaluateLiveMcpApproval(mcpSafety, req.params);
|
|
473
|
+
if (evaluation.denial) {
|
|
474
|
+
queuePolicyDeniedMcpToolCall(activeTurn(), evaluation.denial.request, evaluation.denial.decision, req.params);
|
|
475
|
+
}
|
|
476
|
+
if (evaluation.protocolError) {
|
|
477
|
+
const turn = activeTurn();
|
|
478
|
+
if (turn) {
|
|
479
|
+
turn.nonAskToolEvents.push(buildLiveMcpProtocolErrorEvent(turn.turnNumber, evaluation.protocolError, req.params));
|
|
475
480
|
}
|
|
476
481
|
}
|
|
477
|
-
transport.sendResponse(req.id,
|
|
482
|
+
transport.sendResponse(req.id, evaluation.response);
|
|
478
483
|
}
|
|
479
484
|
else {
|
|
480
485
|
transport.sendResponse(req.id, { action: 'decline', content: null, _meta: null });
|
|
@@ -528,21 +533,32 @@ async function handleRequestUserInput(req, ctx) {
|
|
|
528
533
|
function buildThreadStartParams(opts) {
|
|
529
534
|
return {
|
|
530
535
|
cwd: opts.cwd,
|
|
531
|
-
approvalPolicy: opts.
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
+
approvalPolicy: opts.enableMcpElicitations
|
|
537
|
+
? {
|
|
538
|
+
granular: {
|
|
539
|
+
sandbox_approval: false,
|
|
540
|
+
rules: false,
|
|
541
|
+
skill_approval: false,
|
|
542
|
+
request_permissions: false,
|
|
543
|
+
mcp_elicitations: true,
|
|
544
|
+
},
|
|
545
|
+
}
|
|
546
|
+
: 'never',
|
|
547
|
+
...(opts.enableMcpElicitations ? { approvalsReviewer: 'user' } : {}),
|
|
536
548
|
sandbox: opts.sandboxMode,
|
|
537
549
|
ephemeral: true,
|
|
538
|
-
experimentalRawEvents: false,
|
|
539
|
-
persistExtendedHistory: false,
|
|
540
550
|
model: opts.model,
|
|
541
551
|
...(opts.mcpConfig ? { config: opts.mcpConfig } : {}),
|
|
542
552
|
};
|
|
543
553
|
}
|
|
554
|
+
function shouldEnableMcpElicitations(options) {
|
|
555
|
+
return options?.mcpConfigOrigin === 'generated_mock';
|
|
556
|
+
}
|
|
544
557
|
function assembleTurnResult(args) {
|
|
545
558
|
const { askBus, activeTurn, exitCode, message, pid, signal, sensitiveValues = [] } = args;
|
|
559
|
+
for (const pending of activeTurn.pendingMcpDenials.splice(0)) {
|
|
560
|
+
activeTurn.nonAskToolEvents.push(buildPolicyDeniedMcpToolEvent(activeTurn.turnNumber, pending));
|
|
561
|
+
}
|
|
546
562
|
const askBatchIds = new Set(activeTurn.askBatchIds);
|
|
547
563
|
const askEvents = askBus
|
|
548
564
|
.snapshot()
|
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import type { ToolEvent } from '../../tool-events.js';
|
|
2
2
|
import type { ScriptedMcpMockHost } from '../../providers/scripted-mcp-mock-host.js';
|
|
3
|
-
import { type McpToolPolicyDecision } from '../../sdk/mcp-safety.js';
|
|
3
|
+
import { type McpSafetyOptions, type McpToolPolicyDecision } from '../../sdk/mcp-safety.js';
|
|
4
|
+
export interface PendingMcpDenial {
|
|
5
|
+
request: {
|
|
6
|
+
serverName: string;
|
|
7
|
+
toolName: string;
|
|
8
|
+
arguments: Record<string, unknown>;
|
|
9
|
+
};
|
|
10
|
+
decision: Extract<McpToolPolicyDecision, {
|
|
11
|
+
action: 'deny';
|
|
12
|
+
}>;
|
|
13
|
+
rawParams: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface CodexMcpTerminalItem {
|
|
16
|
+
type: 'mcpToolCall';
|
|
17
|
+
id: string;
|
|
18
|
+
server: string;
|
|
19
|
+
tool: string;
|
|
20
|
+
status?: string;
|
|
21
|
+
arguments?: unknown;
|
|
22
|
+
result?: unknown;
|
|
23
|
+
error?: {
|
|
24
|
+
message?: string;
|
|
25
|
+
} | null;
|
|
26
|
+
durationMs?: number | null;
|
|
27
|
+
}
|
|
4
28
|
export interface CorrelatedApprovalResponse {
|
|
5
29
|
action: 'accept' | 'decline';
|
|
6
30
|
events: ToolEvent[];
|
|
@@ -15,9 +39,30 @@ export declare function extractMcpToolApprovalRequest(params: unknown): {
|
|
|
15
39
|
toolName: string;
|
|
16
40
|
arguments: Record<string, unknown>;
|
|
17
41
|
} | undefined;
|
|
18
|
-
export declare function
|
|
19
|
-
|
|
20
|
-
|
|
42
|
+
export declare function evaluateLiveMcpApproval(safety: McpSafetyOptions | undefined, params: unknown): {
|
|
43
|
+
response: {
|
|
44
|
+
action: 'accept' | 'decline';
|
|
45
|
+
content: Record<string, never> | null;
|
|
46
|
+
_meta: {
|
|
47
|
+
pathgrade_policy_denial: {
|
|
48
|
+
reason: string;
|
|
49
|
+
message: string;
|
|
50
|
+
};
|
|
51
|
+
} | null;
|
|
52
|
+
};
|
|
53
|
+
denial?: {
|
|
54
|
+
request: NonNullable<ReturnType<typeof extractMcpToolApprovalRequest>>;
|
|
55
|
+
decision: Extract<McpToolPolicyDecision, {
|
|
56
|
+
action: 'deny';
|
|
57
|
+
}>;
|
|
58
|
+
};
|
|
59
|
+
protocolError?: {
|
|
60
|
+
reason: string;
|
|
61
|
+
message: string;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
export declare function queuePolicyDeniedMcpToolCall(turn: {
|
|
65
|
+
pendingMcpDenials: PendingMcpDenial[];
|
|
21
66
|
} | null, request: {
|
|
22
67
|
serverName: string;
|
|
23
68
|
toolName: string;
|
|
@@ -25,6 +70,14 @@ export declare function recordPolicyDeniedMcpToolCall(turn: {
|
|
|
25
70
|
}, decision: Extract<McpToolPolicyDecision, {
|
|
26
71
|
action: 'deny';
|
|
27
72
|
}>, rawParams: unknown): void;
|
|
73
|
+
export declare function buildPolicyDeniedMcpToolEvent(turnNumber: number, pending: PendingMcpDenial, terminal?: CodexMcpTerminalItem): ToolEvent;
|
|
74
|
+
export declare function consumeMatchingMcpDenial(turn: {
|
|
75
|
+
pendingMcpDenials: PendingMcpDenial[];
|
|
76
|
+
}, serverName: string, toolName: string, args: Record<string, unknown>): PendingMcpDenial | undefined;
|
|
77
|
+
export declare function buildLiveMcpProtocolErrorEvent(turnNumber: number, error: {
|
|
78
|
+
reason: string;
|
|
79
|
+
message: string;
|
|
80
|
+
}, rawParams: unknown): ToolEvent;
|
|
28
81
|
export declare class CodexMcpApprovalCorrelator {
|
|
29
82
|
private readonly host;
|
|
30
83
|
private readonly getTurnNumber;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { redactMcpSecrets, } from '../../sdk/mcp-safety.js';
|
|
1
|
+
import { decideMcpToolCall, redactMcpSecrets, } from '../../sdk/mcp-safety.js';
|
|
2
2
|
import { canonicalizeJson } from '../../core/canonical-json.js';
|
|
3
3
|
import { buildScriptedMcpApprovalEvent, buildScriptedMcpDeniedCallEvent, } from '../../sdk/scripted-mcp-events.js';
|
|
4
4
|
function isRecord(value) {
|
|
@@ -30,18 +30,75 @@ export function extractMcpToolApprovalRequest(params) {
|
|
|
30
30
|
return undefined;
|
|
31
31
|
return { serverName, toolName, arguments: isRecord(meta.tool_params) ? meta.tool_params : {} };
|
|
32
32
|
}
|
|
33
|
-
export function
|
|
33
|
+
export function evaluateLiveMcpApproval(safety, params) {
|
|
34
|
+
const request = extractMcpToolApprovalRequest(params);
|
|
35
|
+
if (!request) {
|
|
36
|
+
const protocolError = {
|
|
37
|
+
reason: 'unrecognized_mcp_tool_name',
|
|
38
|
+
message: 'Live MCP tool identity could not be established.',
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
response: {
|
|
42
|
+
action: 'decline', content: null,
|
|
43
|
+
_meta: { pathgrade_policy_denial: protocolError },
|
|
44
|
+
},
|
|
45
|
+
protocolError,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const decision = decideMcpToolCall(safety, request);
|
|
49
|
+
if (decision.action === 'allow') {
|
|
50
|
+
return { response: { action: 'accept', content: {}, _meta: null } };
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
response: { action: 'decline', content: null, _meta: {
|
|
54
|
+
pathgrade_policy_denial: { reason: decision.reason, message: decision.message },
|
|
55
|
+
} },
|
|
56
|
+
denial: { request, decision },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function queuePolicyDeniedMcpToolCall(turn, request, decision, rawParams) {
|
|
34
60
|
if (!turn)
|
|
35
61
|
return;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
62
|
+
turn.pendingMcpDenials.push({ request, decision, rawParams });
|
|
63
|
+
}
|
|
64
|
+
export function buildPolicyDeniedMcpToolEvent(turnNumber, pending, terminal) {
|
|
65
|
+
const args = redactMcpSecrets(pending.request.arguments);
|
|
66
|
+
const providerToolName = `${pending.request.serverName}.${pending.request.toolName}`;
|
|
67
|
+
return {
|
|
68
|
+
action: 'mcp_tool_call', provider: 'codex', providerToolName,
|
|
69
|
+
...(terminal ? { toolUseId: terminal.id } : {}),
|
|
70
|
+
turnNumber, status: 'error',
|
|
71
|
+
mcp: { serverName: pending.request.serverName, toolName: pending.request.toolName,
|
|
72
|
+
invocation: 'not_invoked', outcome: 'policy_denied' },
|
|
73
|
+
arguments: {
|
|
74
|
+
...args,
|
|
75
|
+
server: pending.request.serverName,
|
|
76
|
+
tool: pending.request.toolName,
|
|
77
|
+
status: 'policy_denied',
|
|
78
|
+
policyResult: { action: 'deny', reason: pending.decision.reason, message: pending.decision.message },
|
|
79
|
+
},
|
|
42
80
|
summary: `MCP tool ${providerToolName} policy_denied`, confidence: 'high',
|
|
81
|
+
rawSnippet: JSON.stringify(redactMcpSecrets(terminal ? { approval: pending.rawParams, terminal } : pending.rawParams)),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function consumeMatchingMcpDenial(turn, serverName, toolName, args) {
|
|
85
|
+
const argsKey = canonicalizeJson(args);
|
|
86
|
+
const index = turn.pendingMcpDenials.findIndex((pending) => pending.request.serverName === serverName
|
|
87
|
+
&& pending.request.toolName === toolName
|
|
88
|
+
&& canonicalizeJson(pending.request.arguments) === argsKey);
|
|
89
|
+
if (index < 0)
|
|
90
|
+
return undefined;
|
|
91
|
+
return turn.pendingMcpDenials.splice(index, 1)[0];
|
|
92
|
+
}
|
|
93
|
+
export function buildLiveMcpProtocolErrorEvent(turnNumber, error, rawParams) {
|
|
94
|
+
return {
|
|
95
|
+
action: 'mcp_approval', provider: 'codex', providerToolName: 'mcp.protocol_error',
|
|
96
|
+
turnNumber, status: 'error',
|
|
97
|
+
arguments: { decision: 'deny', outcome: 'protocol_error', reason: error.reason,
|
|
98
|
+
message: error.message, decisionSource: 'live_policy' },
|
|
99
|
+
summary: 'MCP approval protocol error', confidence: 'high',
|
|
43
100
|
rawSnippet: JSON.stringify(redactMcpSecrets(rawParams)),
|
|
44
|
-
}
|
|
101
|
+
};
|
|
45
102
|
}
|
|
46
103
|
export class CodexMcpApprovalCorrelator {
|
|
47
104
|
host;
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
// `codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts`
|
|
4
4
|
// (top-level, NOT `typescript/v2/`). This module enumerates the subset of
|
|
5
5
|
// method names the pathgrade Codex app-server driver actually sends. Refresh
|
|
6
|
-
//
|
|
6
|
+
// Refreshed against openai/codex@rust-v0.149.0-alpha.4.3.
|
|
7
7
|
export {};
|
|
@@ -14,6 +14,11 @@ export type McpElicitationRequestParams = {
|
|
|
14
14
|
_meta: JsonValue | null;
|
|
15
15
|
message: string;
|
|
16
16
|
requestedSchema: McpElicitationSchema;
|
|
17
|
+
} | {
|
|
18
|
+
mode: 'openai/form';
|
|
19
|
+
_meta: JsonValue | null;
|
|
20
|
+
message: string;
|
|
21
|
+
requestedSchema: JsonValue;
|
|
17
22
|
} | {
|
|
18
23
|
mode: 'url';
|
|
19
24
|
_meta: JsonValue | null;
|
|
@@ -4,6 +4,9 @@ export type PermissionsRequestApprovalParams = {
|
|
|
4
4
|
threadId: string;
|
|
5
5
|
turnId: string;
|
|
6
6
|
itemId: string;
|
|
7
|
+
environmentId: string | null;
|
|
8
|
+
/** Unix timestamp (in milliseconds) when this approval request started. */
|
|
9
|
+
startedAtMs: number;
|
|
7
10
|
cwd: AbsolutePathBuf;
|
|
8
11
|
reason: string | null;
|
|
9
12
|
permissions: RequestPermissionProfile;
|
|
@@ -4,7 +4,7 @@ import type { GrantedPermissionProfile } from './GrantedPermissionProfile.js';
|
|
|
4
4
|
* turn only; `'thread'` persists until the thread ends. Upstream union is
|
|
5
5
|
* represented as a string literal set here.
|
|
6
6
|
*/
|
|
7
|
-
export type PermissionGrantScope = 'turn' | '
|
|
7
|
+
export type PermissionGrantScope = 'turn' | 'session';
|
|
8
8
|
export type PermissionsRequestApprovalResponse = {
|
|
9
9
|
permissions: GrantedPermissionProfile;
|
|
10
10
|
scope: PermissionGrantScope;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Vendored from openai/codex@rust-v0.
|
|
1
|
+
// Vendored from openai/codex@rust-v0.149.0-alpha.4.3
|
|
2
2
|
// Source: codex-rs/app-server-protocol/schema/typescript/v2/PermissionsRequestApprovalResponse.ts
|
|
3
3
|
// GENERATED CODE in upstream; do not modify locally either.
|
|
4
4
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Pathgrade-local composition; not a single upstream file.
|
|
2
|
-
// Enumerates the 9 server-request variants the driver
|
|
3
|
-
//
|
|
2
|
+
// Enumerates the 9 server-request variants the driver supports from
|
|
3
|
+
// rust-v0.149.0-alpha.4.3. Upstream ships the discriminated union at
|
|
4
4
|
// `codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts`
|
|
5
5
|
// (top-level, NOT `typescript/v2/`); the per-variant params files this module
|
|
6
6
|
// imports live under the `v2/` subdirectory.
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import type { SandboxMode } from './SandboxMode.js';
|
|
2
2
|
export type Personality = unknown;
|
|
3
|
-
export type ServiceTier = unknown;
|
|
4
3
|
export type JsonValue = unknown;
|
|
5
|
-
export type ApprovalsReviewer =
|
|
6
|
-
export type AskForApproval =
|
|
7
|
-
|
|
4
|
+
export type ApprovalsReviewer = 'user' | 'auto_review' | 'guardian_subagent';
|
|
5
|
+
export type AskForApproval = 'untrusted' | 'on-request' | {
|
|
6
|
+
granular: {
|
|
7
|
+
sandbox_approval: boolean;
|
|
8
|
+
rules: boolean;
|
|
9
|
+
skill_approval: boolean;
|
|
10
|
+
request_permissions: boolean;
|
|
11
|
+
mcp_elicitations: boolean;
|
|
12
|
+
};
|
|
13
|
+
} | 'never';
|
|
8
14
|
export type ThreadStartSource = unknown;
|
|
15
|
+
export type ThreadSource = unknown;
|
|
9
16
|
export type ThreadStartParams = {
|
|
10
17
|
model?: string | null;
|
|
11
18
|
modelProvider?: string | null;
|
|
12
|
-
serviceTier?:
|
|
19
|
+
serviceTier?: string | null | null;
|
|
13
20
|
cwd?: string | null;
|
|
14
21
|
approvalPolicy?: AskForApproval | null;
|
|
15
22
|
/**
|
|
@@ -18,11 +25,6 @@ export type ThreadStartParams = {
|
|
|
18
25
|
*/
|
|
19
26
|
approvalsReviewer?: ApprovalsReviewer | null;
|
|
20
27
|
sandbox?: SandboxMode | null;
|
|
21
|
-
/**
|
|
22
|
-
* Full permissions override for this thread. Cannot be combined with
|
|
23
|
-
* `sandbox`.
|
|
24
|
-
*/
|
|
25
|
-
permissionProfile?: PermissionProfile | null;
|
|
26
28
|
config?: {
|
|
27
29
|
[key: string]: JsonValue | undefined;
|
|
28
30
|
} | null;
|
|
@@ -33,13 +35,7 @@ export type ThreadStartParams = {
|
|
|
33
35
|
ephemeral?: boolean | null;
|
|
34
36
|
sessionStartSource?: ThreadStartSource | null;
|
|
35
37
|
/**
|
|
36
|
-
*
|
|
37
|
-
* This is for internal use only (e.g. Codex Cloud).
|
|
38
|
-
*/
|
|
39
|
-
experimentalRawEvents: boolean;
|
|
40
|
-
/**
|
|
41
|
-
* If true, persist additional rollout EventMsg variants required to
|
|
42
|
-
* reconstruct a richer thread history on resume/fork/read.
|
|
38
|
+
* Optional client-supplied analytics source classification for this thread.
|
|
43
39
|
*/
|
|
44
|
-
|
|
40
|
+
threadSource?: ThreadSource | null;
|
|
45
41
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// Vendored from openai/codex@rust-v0.
|
|
1
|
+
// Vendored from openai/codex@rust-v0.149.0-alpha.4.3
|
|
2
2
|
// Source: codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts
|
|
3
3
|
// GENERATED CODE in upstream; do not modify locally either.
|
|
4
4
|
//
|
|
5
|
-
// NOTE: upstream imports additional shared types (Personality,
|
|
6
|
-
//
|
|
5
|
+
// NOTE: upstream imports additional shared types (Personality, JsonValue,
|
|
6
|
+
// ThreadSource, ThreadStartSource) that are not
|
|
7
7
|
// required by the pathgrade Codex driver. They are modelled here as opaque aliases so the
|
|
8
8
|
// vendored surface compiles standalone. Refresh policy: when a new upstream version lands,
|
|
9
9
|
// replace each alias with the vendored definition if the driver starts consuming it.
|
|
@@ -7,4 +7,7 @@ export type ToolRequestUserInputParams = {
|
|
|
7
7
|
turnId: string;
|
|
8
8
|
itemId: string;
|
|
9
9
|
questions: Array<ToolRequestUserInputQuestion>;
|
|
10
|
+
isBlocking: boolean;
|
|
11
|
+
/** @deprecated Use `isBlocking` to decide whether the request should block. */
|
|
12
|
+
autoResolutionMs: number | null;
|
|
10
13
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Curated re-exports for the pathgrade Codex app-server driver and fixture
|
|
2
2
|
// tests. Every re-exported symbol is either a curated upstream shape from
|
|
3
|
-
// openai/codex@rust-v0.
|
|
3
|
+
// openai/codex@rust-v0.149.0-alpha.4.3 (with an upstream-citation header in its file) or
|
|
4
4
|
// a pathgrade-local composition (ClientRequest, ServerRequest, Op) whose header
|
|
5
5
|
// spells out the composition rationale.
|
|
6
6
|
//
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MockMcpServerDescriptor } from '../core/mcp-mock.types.js';
|
|
2
|
+
import type { McpConfigOrigin } from '../types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Stdio-server shape pathgrade writes to `.pathgrade-mcp.json`. Lines up
|
|
4
5
|
* directly with the SDK's `McpStdioServerConfig` (`sdk.d.ts:1005`) — the
|
|
@@ -38,5 +39,6 @@ export declare function assertMcpSecretReferencesReady(opts: {
|
|
|
38
39
|
}): Promise<void>;
|
|
39
40
|
export interface McpConfigResult {
|
|
40
41
|
mcpConfigPath: string | undefined;
|
|
42
|
+
mcpConfigOrigin: McpConfigOrigin | undefined;
|
|
41
43
|
}
|
|
42
44
|
export declare function stageMcpConfig(workspacePath: string, mcp: McpDeclaration | undefined): Promise<McpConfigResult>;
|
|
@@ -130,7 +130,7 @@ async function resolveMockServerScript(workspacePath) {
|
|
|
130
130
|
}
|
|
131
131
|
export async function stageMcpConfig(workspacePath, mcp) {
|
|
132
132
|
if (!mcp)
|
|
133
|
-
return { mcpConfigPath: undefined };
|
|
133
|
+
return { mcpConfigPath: undefined, mcpConfigOrigin: undefined };
|
|
134
134
|
const mcpConfigPath = MCP_CONFIG_FILENAME;
|
|
135
135
|
if ('configFile' in mcp) {
|
|
136
136
|
const mcpSrc = path.resolve(mcp.configFile);
|
|
@@ -161,5 +161,8 @@ export async function stageMcpConfig(workspacePath, mcp) {
|
|
|
161
161
|
}
|
|
162
162
|
await fs.writeJson(path.join(workspacePath, mcpConfigPath), { mcpServers }, { spaces: 2 });
|
|
163
163
|
}
|
|
164
|
-
return {
|
|
164
|
+
return {
|
|
165
|
+
mcpConfigPath,
|
|
166
|
+
mcpConfigOrigin: 'mock' in mcp ? 'generated_mock' : 'caller_config',
|
|
167
|
+
};
|
|
165
168
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type SandboxConfig } from './sandbox.js';
|
|
2
|
-
import type { CommandResult } from '../types.js';
|
|
2
|
+
import type { CommandResult, McpConfigOrigin } from '../types.js';
|
|
3
3
|
export type { McpDeclaration } from './mcp-config.js';
|
|
4
4
|
export interface Workspace {
|
|
5
5
|
readonly path: string;
|
|
6
6
|
readonly mcpConfigPath: string | undefined;
|
|
7
|
+
readonly mcpConfigOrigin: McpConfigOrigin | undefined;
|
|
7
8
|
readonly env: Record<string, string>;
|
|
8
9
|
readonly setupCommands: string[];
|
|
9
10
|
readonly sensitiveValues?: readonly string[];
|
|
@@ -56,11 +56,12 @@ export async function prepareWorkspace(spec) {
|
|
|
56
56
|
await copyPathsFromHostHome(creds.copyFromHome, homePath);
|
|
57
57
|
await linkPathsFromHostHome(creds.linkFromHome ?? [], homePath);
|
|
58
58
|
await stageSensitiveHomeFiles(creds.sensitiveHomeFiles ?? [], homePath);
|
|
59
|
-
const { mcpConfigPath } = await stageMcpConfig(workspacePath, mcp);
|
|
59
|
+
const { mcpConfigPath, mcpConfigOrigin } = await stageMcpConfig(workspacePath, mcp);
|
|
60
60
|
let disposed = false;
|
|
61
61
|
return {
|
|
62
62
|
path: workspacePath,
|
|
63
63
|
mcpConfigPath,
|
|
64
|
+
mcpConfigOrigin,
|
|
64
65
|
env: sandboxEnv,
|
|
65
66
|
setupCommands: creds.setupCommands,
|
|
66
67
|
sensitiveValues: [...new Set([
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { sanitizePersistenceValue } from '../tool-event-results.js';
|
|
2
|
+
import { getToolEventSensitiveValues } from './tool-event-secrets.js';
|
|
1
3
|
export function buildJudgePrompt(scorer, ctx, input) {
|
|
2
4
|
const sections = [];
|
|
3
5
|
sections.push(`## Session Transcript\n${ctx.transcript}`);
|
|
@@ -23,7 +25,17 @@ function formatToolEvents(ctx) {
|
|
|
23
25
|
return ctx.toolEvents
|
|
24
26
|
.map((event) => {
|
|
25
27
|
const turn = event.turnNumber ? `turn ${event.turnNumber}` : 'instruction';
|
|
26
|
-
|
|
28
|
+
const sensitiveValues = getToolEventSensitiveValues(event);
|
|
29
|
+
const details = [
|
|
30
|
+
event.status ? ` status: ${event.status}` : undefined,
|
|
31
|
+
event.mcp ? ` mcp: ${JSON.stringify(event.mcp)}` : undefined,
|
|
32
|
+
event.arguments ? ` arguments: ${JSON.stringify(sanitizePersistenceValue(event.arguments, sensitiveValues))}` : undefined,
|
|
33
|
+
event.result ? ` result: ${JSON.stringify(sanitizePersistenceValue(event.result, sensitiveValues))}` : undefined,
|
|
34
|
+
].filter((line) => line !== undefined);
|
|
35
|
+
return [
|
|
36
|
+
`- ${turn}: ${event.action} via ${event.providerToolName} (${event.provider})`,
|
|
37
|
+
...details,
|
|
38
|
+
].join('\n');
|
|
27
39
|
})
|
|
28
40
|
.join('\n');
|
|
29
41
|
}
|
|
@@ -22,6 +22,7 @@ export function createManagedSession(deps) {
|
|
|
22
22
|
?? createAskBus({ askUserTimeoutMs: deps.askUserTimeoutMs ?? 30_000 });
|
|
23
23
|
const sessionOptions = {
|
|
24
24
|
...(ws.mcpConfigPath ? { mcpConfigPath: ws.mcpConfigPath } : {}),
|
|
25
|
+
...(ws.mcpConfigOrigin ? { mcpConfigOrigin: ws.mcpConfigOrigin } : {}),
|
|
25
26
|
...(model ? { model } : {}),
|
|
26
27
|
...(conversationWindow !== undefined ? { conversationWindow } : {}),
|
|
27
28
|
...(runtimePolicies.length > 0 ? { runtimePolicies } : {}),
|
|
@@ -171,8 +171,8 @@ function assertClaudeNamesUnambiguous(names) {
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
export function compileMcpMockApprovalSession(opts) {
|
|
174
|
-
if (!Array.isArray(opts.rules)
|
|
175
|
-
throw new Error('mcpMockApprovalRules must be
|
|
174
|
+
if (!Array.isArray(opts.rules)) {
|
|
175
|
+
throw new Error('mcpMockApprovalRules must be an array');
|
|
176
176
|
}
|
|
177
177
|
const descriptors = (Array.isArray(opts.mcpMock) ? opts.mcpMock : [opts.mcpMock])
|
|
178
178
|
.map((descriptor, index) => cloneDescriptor(descriptor, index));
|
package/dist/types.d.ts
CHANGED
|
@@ -352,8 +352,11 @@ export interface AgentSession {
|
|
|
352
352
|
export declare function getWorkspacePath(handle: EnvironmentHandle): string;
|
|
353
353
|
export declare function getRuntimeHandle(handle: EnvironmentHandle): string;
|
|
354
354
|
export declare function getRuntimeEnv(handle: EnvironmentHandle): Record<string, string>;
|
|
355
|
+
export type McpConfigOrigin = 'generated_mock' | 'caller_config';
|
|
355
356
|
export interface AgentSessionOptions {
|
|
356
357
|
mcpConfigPath?: string;
|
|
358
|
+
/** Authority provenance for the staged MCP configuration. */
|
|
359
|
+
mcpConfigOrigin?: McpConfigOrigin;
|
|
357
360
|
model?: string;
|
|
358
361
|
conversationWindow?: import('./sdk/types.js').ConversationWindowConfig | false;
|
|
359
362
|
runtimePolicies?: RuntimePolicyDescriptor[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/pathgrade",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
4
4
|
"packageManager": "yarn@4.12.0",
|
|
5
5
|
"description": "Evaluate whether AI agents discover and use your skills correctly",
|
|
6
6
|
"exports": {
|
|
@@ -144,5 +144,5 @@
|
|
|
144
144
|
"typescript": "^5.9.3",
|
|
145
145
|
"zod": "4.3.6"
|
|
146
146
|
},
|
|
147
|
-
"falconPackageHash": "
|
|
147
|
+
"falconPackageHash": "20beff94d0506c770c82c813225aca2ab4c0b4785199afb176f72286"
|
|
148
148
|
}
|