@wix/pathgrade 0.32.0 → 0.34.0
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/claude/denied-mcp-event-store.d.ts +8 -0
- package/dist/agents/claude/denied-mcp-event-store.js +17 -0
- package/dist/agents/claude/mcp-tool-name.d.ts +12 -0
- package/dist/agents/claude/mcp-tool-name.js +22 -0
- package/dist/agents/claude/sdk-message-projector.d.ts +5 -0
- package/dist/agents/claude/sdk-message-projector.js +31 -3
- package/dist/agents/claude/sdk-options.d.ts +4 -2
- package/dist/agents/claude/sdk-options.js +2 -0
- package/dist/agents/claude/tool-permission-bridge.d.ts +18 -0
- package/dist/agents/claude/tool-permission-bridge.js +86 -0
- package/dist/agents/claude.d.ts +1 -1
- package/dist/agents/claude.js +43 -5
- package/dist/agents/codex-app-server/agent.js +162 -3
- package/dist/agents/codex.js +4 -0
- package/dist/agents/cursor.js +12 -13
- package/dist/providers/mcp-config.d.ts +24 -16
- package/dist/providers/mcp-config.js +128 -20
- package/dist/providers/mcp-runtime-mounting.d.ts +46 -0
- package/dist/providers/mcp-runtime-mounting.js +155 -0
- package/dist/providers/sandbox.d.ts +1 -1
- package/dist/providers/sandbox.js +2 -1
- package/dist/providers/workspace.d.ts +1 -1
- package/dist/providers/workspace.js +2 -2
- package/dist/sdk/agent.js +7 -4
- package/dist/sdk/index.d.ts +6 -1
- package/dist/sdk/index.js +2 -0
- package/dist/sdk/managed-session.d.ts +3 -0
- package/dist/sdk/managed-session.js +2 -0
- package/dist/sdk/mcp-evidence.d.ts +29 -0
- package/dist/sdk/mcp-evidence.js +96 -0
- package/dist/sdk/mcp-safety.d.ts +30 -0
- package/dist/sdk/mcp-safety.js +61 -0
- package/dist/sdk/types.d.ts +12 -2
- package/dist/sdk/types.js +2 -2
- package/dist/tool-events.d.ts +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/utils/timeout.js +11 -10
- package/package.json +3 -2
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ToolEvent } from '../tool-events.js';
|
|
2
|
+
export interface McpToolCallEvidence {
|
|
3
|
+
serverName: string;
|
|
4
|
+
toolName: string;
|
|
5
|
+
status: string;
|
|
6
|
+
arguments: Record<string, unknown>;
|
|
7
|
+
event: ToolEvent;
|
|
8
|
+
}
|
|
9
|
+
export interface ExpectedMcpToolCall {
|
|
10
|
+
serverName?: string;
|
|
11
|
+
toolName?: string;
|
|
12
|
+
status?: string;
|
|
13
|
+
argumentsContaining?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface McpStartupStatusEvidence {
|
|
16
|
+
serverName: string;
|
|
17
|
+
status: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
event: ToolEvent;
|
|
20
|
+
}
|
|
21
|
+
export interface ExpectedMcpStartupStatus {
|
|
22
|
+
serverName?: string;
|
|
23
|
+
status?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function getMcpToolCall(event: ToolEvent): McpToolCallEvidence | undefined;
|
|
26
|
+
export declare function isMcpToolCall(event: ToolEvent, expected?: ExpectedMcpToolCall): boolean;
|
|
27
|
+
export declare function findMcpToolCalls(events: readonly ToolEvent[], expected?: ExpectedMcpToolCall): McpToolCallEvidence[];
|
|
28
|
+
export declare function getMcpStartupStatus(event: ToolEvent): McpStartupStatusEvidence | undefined;
|
|
29
|
+
export declare function isMcpStartupStatus(event: ToolEvent, expected?: ExpectedMcpStartupStatus): boolean;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export function getMcpToolCall(event) {
|
|
2
|
+
if (event.action !== 'mcp_tool_call')
|
|
3
|
+
return undefined;
|
|
4
|
+
const args = event.arguments ?? {};
|
|
5
|
+
const serverName = typeof args.server === 'string' ? args.server : undefined;
|
|
6
|
+
const toolName = typeof args.tool === 'string' ? args.tool : undefined;
|
|
7
|
+
const status = typeof args.status === 'string' ? args.status : undefined;
|
|
8
|
+
if (!serverName || !toolName || !status)
|
|
9
|
+
return undefined;
|
|
10
|
+
return {
|
|
11
|
+
serverName,
|
|
12
|
+
toolName,
|
|
13
|
+
status,
|
|
14
|
+
arguments: args,
|
|
15
|
+
event,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function isMcpToolCall(event, expected = {}) {
|
|
19
|
+
const call = getMcpToolCall(event);
|
|
20
|
+
if (!call)
|
|
21
|
+
return false;
|
|
22
|
+
if (expected.serverName !== undefined && call.serverName !== expected.serverName)
|
|
23
|
+
return false;
|
|
24
|
+
if (expected.toolName !== undefined && call.toolName !== expected.toolName)
|
|
25
|
+
return false;
|
|
26
|
+
if (expected.status !== undefined && call.status !== expected.status)
|
|
27
|
+
return false;
|
|
28
|
+
if (expected.argumentsContaining && !containsArguments(call.arguments, expected.argumentsContaining)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
export function findMcpToolCalls(events, expected = {}) {
|
|
34
|
+
return events
|
|
35
|
+
.map((event) => getMcpToolCall(event))
|
|
36
|
+
.filter((call) => call !== undefined && matchesMcpToolCall(call, expected));
|
|
37
|
+
}
|
|
38
|
+
export function getMcpStartupStatus(event) {
|
|
39
|
+
if (event.providerToolName !== 'mcpServer/startupStatus/updated')
|
|
40
|
+
return undefined;
|
|
41
|
+
const args = event.arguments ?? {};
|
|
42
|
+
const serverName = typeof args.name === 'string' ? args.name : undefined;
|
|
43
|
+
const status = typeof args.status === 'string' ? args.status : undefined;
|
|
44
|
+
if (!serverName || !status)
|
|
45
|
+
return undefined;
|
|
46
|
+
const error = typeof args.error === 'string' ? args.error : undefined;
|
|
47
|
+
return {
|
|
48
|
+
serverName,
|
|
49
|
+
status,
|
|
50
|
+
...(error ? { error } : {}),
|
|
51
|
+
event,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function isMcpStartupStatus(event, expected = {}) {
|
|
55
|
+
const startup = getMcpStartupStatus(event);
|
|
56
|
+
if (!startup)
|
|
57
|
+
return false;
|
|
58
|
+
if (expected.serverName !== undefined && startup.serverName !== expected.serverName)
|
|
59
|
+
return false;
|
|
60
|
+
if (expected.status !== undefined && startup.status !== expected.status)
|
|
61
|
+
return false;
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
function matchesMcpToolCall(call, expected) {
|
|
65
|
+
if (expected.serverName !== undefined && call.serverName !== expected.serverName)
|
|
66
|
+
return false;
|
|
67
|
+
if (expected.toolName !== undefined && call.toolName !== expected.toolName)
|
|
68
|
+
return false;
|
|
69
|
+
if (expected.status !== undefined && call.status !== expected.status)
|
|
70
|
+
return false;
|
|
71
|
+
if (expected.argumentsContaining && !containsArguments(call.arguments, expected.argumentsContaining)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
function containsArguments(actual, expected) {
|
|
77
|
+
return Object.entries(expected).every(([key, expectedValue]) => valuesEqual(actual[key], expectedValue));
|
|
78
|
+
}
|
|
79
|
+
function valuesEqual(actual, expected) {
|
|
80
|
+
if (Object.is(actual, expected))
|
|
81
|
+
return true;
|
|
82
|
+
if (Array.isArray(actual) || Array.isArray(expected)) {
|
|
83
|
+
if (!Array.isArray(actual) || !Array.isArray(expected))
|
|
84
|
+
return false;
|
|
85
|
+
if (actual.length !== expected.length)
|
|
86
|
+
return false;
|
|
87
|
+
return actual.every((value, index) => valuesEqual(value, expected[index]));
|
|
88
|
+
}
|
|
89
|
+
if (isRecord(actual) && isRecord(expected)) {
|
|
90
|
+
return Object.entries(expected).every(([key, value]) => valuesEqual(actual[key], value));
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
function isRecord(value) {
|
|
95
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
96
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type McpRunMode = 'mock' | 'live-readonly' | 'live-sandbox' | 'live';
|
|
2
|
+
export interface McpToolPolicyRule {
|
|
3
|
+
serverName?: string;
|
|
4
|
+
toolName?: string;
|
|
5
|
+
readonly?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface McpToolPolicy {
|
|
8
|
+
allow?: McpToolPolicyRule[];
|
|
9
|
+
deny?: McpToolPolicyRule[];
|
|
10
|
+
}
|
|
11
|
+
export interface McpSafetyOptions {
|
|
12
|
+
runMode?: McpRunMode;
|
|
13
|
+
liveOptIn?: boolean;
|
|
14
|
+
mcpToolPolicy?: McpToolPolicy;
|
|
15
|
+
}
|
|
16
|
+
export interface McpToolCallRequest {
|
|
17
|
+
serverName: string;
|
|
18
|
+
toolName: string;
|
|
19
|
+
arguments?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
export type McpPolicyDenialReason = 'missing_live_opt_in' | 'missing_mcp_tool_policy' | 'denylisted' | 'not_allowlisted' | 'not_marked_readonly' | 'unrecognized_mcp_tool_name';
|
|
22
|
+
export type McpToolPolicyDecision = {
|
|
23
|
+
action: 'allow';
|
|
24
|
+
} | {
|
|
25
|
+
action: 'deny';
|
|
26
|
+
reason: McpPolicyDenialReason;
|
|
27
|
+
message: string;
|
|
28
|
+
};
|
|
29
|
+
export declare function decideMcpToolCall(options: McpSafetyOptions | undefined, request: McpToolCallRequest): McpToolPolicyDecision;
|
|
30
|
+
export declare function redactMcpSecrets<T>(value: T): T;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const SECRET_KEY_PATTERN = /(^|[_-])(api[_-]?key|token|secret|password|authorization|auth|bearer)([_-]|$)|authorization/i;
|
|
2
|
+
export function decideMcpToolCall(options, request) {
|
|
3
|
+
const runMode = options?.runMode ?? 'mock';
|
|
4
|
+
if (runMode === 'mock')
|
|
5
|
+
return { action: 'allow' };
|
|
6
|
+
if (!options?.liveOptIn) {
|
|
7
|
+
return deny('missing_live_opt_in', 'Live MCP tool calls require explicit mcpSafety.liveOptIn.');
|
|
8
|
+
}
|
|
9
|
+
const policy = options.mcpToolPolicy;
|
|
10
|
+
if (runMode === 'live-readonly' && !policy) {
|
|
11
|
+
return deny('missing_mcp_tool_policy', 'live-readonly MCP runs require an explicit mcpToolPolicy.');
|
|
12
|
+
}
|
|
13
|
+
const denyRule = policy?.deny?.find((rule) => ruleMatches(rule, request));
|
|
14
|
+
if (denyRule) {
|
|
15
|
+
return deny('denylisted', `MCP tool ${request.serverName}.${request.toolName} is denied by policy.`);
|
|
16
|
+
}
|
|
17
|
+
const allowRules = policy?.allow ?? [];
|
|
18
|
+
if (allowRules.length > 0) {
|
|
19
|
+
const allowRule = allowRules.find((rule) => ruleMatches(rule, request));
|
|
20
|
+
if (!allowRule) {
|
|
21
|
+
return deny('not_allowlisted', `MCP tool ${request.serverName}.${request.toolName} is not allowlisted.`);
|
|
22
|
+
}
|
|
23
|
+
if (runMode === 'live-readonly' && allowRule.readonly !== true) {
|
|
24
|
+
return deny('not_marked_readonly', `MCP tool ${request.serverName}.${request.toolName} is not marked readonly.`);
|
|
25
|
+
}
|
|
26
|
+
return { action: 'allow' };
|
|
27
|
+
}
|
|
28
|
+
if (runMode === 'live-readonly') {
|
|
29
|
+
return deny('not_allowlisted', `MCP tool ${request.serverName}.${request.toolName} is not allowlisted.`);
|
|
30
|
+
}
|
|
31
|
+
return { action: 'allow' };
|
|
32
|
+
}
|
|
33
|
+
export function redactMcpSecrets(value) {
|
|
34
|
+
return redactValue(value, '');
|
|
35
|
+
}
|
|
36
|
+
function ruleMatches(rule, request) {
|
|
37
|
+
if (rule.serverName !== undefined && rule.serverName !== request.serverName)
|
|
38
|
+
return false;
|
|
39
|
+
if (rule.toolName !== undefined && rule.toolName !== request.toolName)
|
|
40
|
+
return false;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
function deny(reason, message) {
|
|
44
|
+
return { action: 'deny', reason, message };
|
|
45
|
+
}
|
|
46
|
+
function redactValue(value, key) {
|
|
47
|
+
if (SECRET_KEY_PATTERN.test(key))
|
|
48
|
+
return '<redacted>';
|
|
49
|
+
if (Array.isArray(value))
|
|
50
|
+
return value.map((entry) => redactValue(entry, key));
|
|
51
|
+
if (isRecord(value)) {
|
|
52
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
|
|
53
|
+
entryKey,
|
|
54
|
+
redactValue(entryValue, entryKey),
|
|
55
|
+
]));
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
function isRecord(value) {
|
|
60
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
61
|
+
}
|
package/dist/sdk/types.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { MockMcpServerDescriptor } from '../core/mcp-mock.types.js';
|
|
|
4
4
|
import type { TrialResult } from '../types.js';
|
|
5
5
|
import type { DiagnosticsReport } from '../reporters/diagnostics.js';
|
|
6
6
|
import type { LLMPort } from '../utils/llm-types.js';
|
|
7
|
+
import type { McpSafetyOptions } from './mcp-safety.js';
|
|
7
8
|
export type AgentName = 'claude' | 'codex' | 'cursor';
|
|
8
9
|
export interface AgentOptions {
|
|
9
10
|
agent?: AgentName;
|
|
@@ -14,6 +15,7 @@ export interface AgentOptions {
|
|
|
14
15
|
copyFromHome?: string[];
|
|
15
16
|
env?: Record<string, string>;
|
|
16
17
|
mcpMock?: MockMcpServerDescriptor | MockMcpServerDescriptor[];
|
|
18
|
+
mcpConfigFile?: string;
|
|
17
19
|
/** Configure the conversation window for transcript-based agents. Set false to disable. */
|
|
18
20
|
conversationWindow?: ConversationWindowConfig | false;
|
|
19
21
|
/** Copy workspace to a persistent location before cleanup. true = ./pathgrade-debug/{test-name}/, string = custom path. */
|
|
@@ -31,7 +33,14 @@ export interface AgentOptions {
|
|
|
31
33
|
* in v1 (documented but not enforced).
|
|
32
34
|
*/
|
|
33
35
|
transport?: AgentTransport;
|
|
36
|
+
/**
|
|
37
|
+
* Live MCP Server Safety policy. `live-readonly` requires explicit
|
|
38
|
+
* `liveOptIn` plus an inspectable allow/deny policy before tools are
|
|
39
|
+
* approved by supported live harnesses.
|
|
40
|
+
*/
|
|
41
|
+
mcpSafety?: McpSafetyOptions;
|
|
34
42
|
}
|
|
43
|
+
export type { McpRunMode, McpSafetyOptions, McpToolPolicy, McpToolPolicyRule, } from './mcp-safety.js';
|
|
35
44
|
export interface ConversationWindowConfig {
|
|
36
45
|
/** Number of recent messages to keep verbatim. Default: 4 */
|
|
37
46
|
windowSize?: number;
|
|
@@ -350,8 +359,9 @@ export interface AgentCapabilities {
|
|
|
350
359
|
}
|
|
351
360
|
/**
|
|
352
361
|
* Codex-specific transport. Claude and Cursor ignore this.
|
|
353
|
-
* `app-server` unlocks
|
|
354
|
-
* `
|
|
362
|
+
* `app-server` unlocks MCP Runtime Mounting and
|
|
363
|
+
* `interactiveQuestionTransport: 'reliable'` for Codex; `exec` keeps the
|
|
364
|
+
* `'noninteractive'` channel and has no MCP Runtime Mounting support.
|
|
355
365
|
*/
|
|
356
366
|
export type AgentTransport = 'exec' | 'app-server';
|
|
357
367
|
export declare function getAgentCapabilities(agent: AgentName, transport?: AgentTransport): AgentCapabilities;
|
package/dist/sdk/types.js
CHANGED
|
@@ -5,8 +5,8 @@ const BASE_CAPABILITIES = {
|
|
|
5
5
|
};
|
|
6
6
|
export function getAgentCapabilities(agent, transport) {
|
|
7
7
|
const base = BASE_CAPABILITIES[agent];
|
|
8
|
-
if (agent === 'codex' && transport === 'app-server') {
|
|
9
|
-
return { ...base, interactiveQuestionTransport: 'reliable' };
|
|
8
|
+
if (agent === 'codex' && (transport ?? 'app-server') === 'app-server') {
|
|
9
|
+
return { ...base, mcp: true, interactiveQuestionTransport: 'reliable' };
|
|
10
10
|
}
|
|
11
11
|
return base;
|
|
12
12
|
}
|
package/dist/tool-events.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ToolAction = 'run_shell' | 'read_file' | 'write_file' | 'edit_file' | 'search_code' | 'list_files' | 'ask_user' | 'web_fetch' | 'use_skill' | 'update_todos' | 'unknown';
|
|
1
|
+
export type ToolAction = 'run_shell' | 'read_file' | 'write_file' | 'edit_file' | 'search_code' | 'list_files' | 'ask_user' | 'web_fetch' | 'use_skill' | 'update_todos' | 'mcp_tool_call' | 'unknown';
|
|
2
2
|
export interface ToolEvent {
|
|
3
3
|
action: ToolAction;
|
|
4
4
|
provider: 'claude' | 'codex' | 'cursor';
|
package/dist/types.d.ts
CHANGED
|
@@ -364,6 +364,12 @@ export interface AgentSessionOptions {
|
|
|
364
364
|
* only `lifecycle: 'post-hoc'` no-op when absent.
|
|
365
365
|
*/
|
|
366
366
|
askBus?: import('./sdk/ask-bus/types.js').AskBus;
|
|
367
|
+
/** Public live MCP safety policy threaded from createAgent(). */
|
|
368
|
+
mcpSafety?: import('./sdk/mcp-safety.js').McpSafetyOptions;
|
|
369
|
+
/** Per-turn timeout/cancellation signal owned by the managed session. */
|
|
370
|
+
abortSignal?: AbortSignal;
|
|
371
|
+
/** Supplies the current timeout/cancellation signal for reused sessions. */
|
|
372
|
+
getAbortSignal?: () => AbortSignal | undefined;
|
|
367
373
|
}
|
|
368
374
|
export declare abstract class BaseAgent {
|
|
369
375
|
createSession(runtime: EnvironmentHandle, runCommand: AgentCommandRunner, options?: AgentSessionOptions): Promise<AgentSession>;
|
package/dist/utils/timeout.js
CHANGED
|
@@ -8,24 +8,25 @@
|
|
|
8
8
|
export function withAbortTimeout(run, timeoutMs, label) {
|
|
9
9
|
return new Promise((resolve, reject) => {
|
|
10
10
|
const controller = new AbortController();
|
|
11
|
-
let
|
|
11
|
+
let settled = false;
|
|
12
12
|
const timer = setTimeout(() => {
|
|
13
|
-
|
|
13
|
+
if (settled)
|
|
14
|
+
return;
|
|
15
|
+
settled = true;
|
|
14
16
|
controller.abort();
|
|
17
|
+
reject(new Error(`${label} timed out after ${timeoutMs / 1000}s`));
|
|
15
18
|
}, timeoutMs);
|
|
16
19
|
run(controller.signal).then((val) => {
|
|
17
|
-
|
|
18
|
-
if (timedOut || controller.signal.aborted) {
|
|
19
|
-
reject(new Error(`${label} timed out after ${timeoutMs / 1000}s`));
|
|
20
|
+
if (settled)
|
|
20
21
|
return;
|
|
21
|
-
|
|
22
|
+
settled = true;
|
|
23
|
+
clearTimeout(timer);
|
|
22
24
|
resolve(val);
|
|
23
25
|
}, (err) => {
|
|
24
|
-
|
|
25
|
-
if (timedOut || controller.signal.aborted) {
|
|
26
|
-
reject(new Error(`${label} timed out after ${timeoutMs / 1000}s`));
|
|
26
|
+
if (settled)
|
|
27
27
|
return;
|
|
28
|
-
|
|
28
|
+
settled = true;
|
|
29
|
+
clearTimeout(timer);
|
|
29
30
|
reject(err);
|
|
30
31
|
});
|
|
31
32
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/pathgrade",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Evaluate whether AI agents discover and use your skills correctly",
|
|
5
5
|
"main": "./dist/sdk/index.js",
|
|
6
6
|
"types": "./dist/sdk/index.d.ts",
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
91
|
"@anthropic-ai/claude-agent-sdk": "0.2.85",
|
|
92
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
92
93
|
"@types/node": "^25.3.1",
|
|
93
94
|
"fs-extra": "^11.3.3",
|
|
94
95
|
"jiti": "^2.6.1",
|
|
@@ -96,5 +97,5 @@
|
|
|
96
97
|
"typescript": "^5.9.3",
|
|
97
98
|
"zod": "4.3.6"
|
|
98
99
|
},
|
|
99
|
-
"falconPackageHash": "
|
|
100
|
+
"falconPackageHash": "77ac8633dc5bff7dea81e34b2bad4228699443f0788ce2eb56a2bc36"
|
|
100
101
|
}
|