@wix/pathgrade 0.33.0 → 0.35.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 +13 -11
- 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.js +20 -3
- package/dist/agents/codex-app-server/agent.js +51 -5
- package/dist/agents/codex-app-server/transport.js +3 -1
- package/dist/agents/codex.js +1 -1
- package/dist/agents/cursor.js +7 -1
- package/dist/providers/mcp-runtime-mounting.d.ts +10 -0
- package/dist/providers/mcp-runtime-mounting.js +39 -0
- package/dist/providers/sandbox.js +2 -1
- package/dist/sdk/mcp-safety.d.ts +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolEvent } from '../../tool-events.js';
|
|
2
|
+
export interface ClaudeDeniedMcpEventStore {
|
|
3
|
+
record(toolUseId: string, event: ToolEvent): void;
|
|
4
|
+
get(toolUseId: string | undefined): ToolEvent | undefined;
|
|
5
|
+
all(): ToolEvent[];
|
|
6
|
+
clear(): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function createClaudeDeniedMcpEventStore(): ClaudeDeniedMcpEventStore;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function createClaudeDeniedMcpEventStore() {
|
|
2
|
+
const byToolUseId = new Map();
|
|
3
|
+
return {
|
|
4
|
+
record(toolUseId, event) {
|
|
5
|
+
byToolUseId.set(toolUseId, event);
|
|
6
|
+
},
|
|
7
|
+
get(toolUseId) {
|
|
8
|
+
return toolUseId ? byToolUseId.get(toolUseId) : undefined;
|
|
9
|
+
},
|
|
10
|
+
all() {
|
|
11
|
+
return [...byToolUseId.values()];
|
|
12
|
+
},
|
|
13
|
+
clear() {
|
|
14
|
+
byToolUseId.clear();
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { McpPolicyDenialReason } from '../../sdk/mcp-safety.js';
|
|
2
|
+
export type ClaudeSdkMcpToolNameParseResult = {
|
|
3
|
+
kind: 'non_mcp';
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'mcp';
|
|
6
|
+
server: string;
|
|
7
|
+
tool: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'unrecognized';
|
|
10
|
+
reason: Extract<McpPolicyDenialReason, 'unrecognized_mcp_tool_name'>;
|
|
11
|
+
};
|
|
12
|
+
export declare function parseClaudeSdkMcpToolName(providerToolName: string, configuredServerNames: readonly string[]): ClaudeSdkMcpToolNameParseResult;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const MCP_TOOL_PREFIX = 'mcp__';
|
|
2
|
+
export function parseClaudeSdkMcpToolName(providerToolName, configuredServerNames) {
|
|
3
|
+
if (!providerToolName.startsWith(MCP_TOOL_PREFIX))
|
|
4
|
+
return { kind: 'non_mcp' };
|
|
5
|
+
const body = providerToolName.slice(MCP_TOOL_PREFIX.length);
|
|
6
|
+
const matches = configuredServerNames
|
|
7
|
+
.filter((serverName) => {
|
|
8
|
+
if (serverName.length === 0)
|
|
9
|
+
return false;
|
|
10
|
+
const toolStart = `${serverName}__`;
|
|
11
|
+
return body.startsWith(toolStart) && body.length > toolStart.length;
|
|
12
|
+
})
|
|
13
|
+
.sort((a, b) => b.length - a.length);
|
|
14
|
+
if (matches.length !== 1) {
|
|
15
|
+
return { kind: 'unrecognized', reason: 'unrecognized_mcp_tool_name' };
|
|
16
|
+
}
|
|
17
|
+
const server = matches[0];
|
|
18
|
+
const tool = body.slice(server.length + 2);
|
|
19
|
+
if (!tool)
|
|
20
|
+
return { kind: 'unrecognized', reason: 'unrecognized_mcp_tool_name' };
|
|
21
|
+
return { kind: 'mcp', server, tool };
|
|
22
|
+
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
19
19
|
import type { AgentTurnResult } from '../../types.js';
|
|
20
20
|
import type { AskUserAnswerStore } from './ask-user-answer-store.js';
|
|
21
|
+
import type { ClaudeDeniedMcpEventStore } from './denied-mcp-event-store.js';
|
|
21
22
|
export interface ProjectTurnInput {
|
|
22
23
|
/** Buffered typed-message stream from one `query()` call. */
|
|
23
24
|
messages: SDKMessage[];
|
|
@@ -34,6 +35,10 @@ export interface ProjectTurnInput {
|
|
|
34
35
|
* `answerSource: 'unknown'` it stamped before the bridge existed.
|
|
35
36
|
*/
|
|
36
37
|
answerStore?: AskUserAnswerStore;
|
|
38
|
+
/** Staged MCP server names from the Claude SDK mount boundary. */
|
|
39
|
+
mcpServerNames?: readonly string[];
|
|
40
|
+
/** Policy-denied MCP events recorded by the per-turn permission bridge. */
|
|
41
|
+
deniedMcpEvents?: ClaudeDeniedMcpEventStore;
|
|
37
42
|
}
|
|
38
43
|
export interface ProjectedTurn {
|
|
39
44
|
result: AgentTurnResult;
|
|
@@ -22,6 +22,7 @@ const SDK_ERROR_SUBTYPES = [
|
|
|
22
22
|
'error_max_structured_output_retries',
|
|
23
23
|
];
|
|
24
24
|
import { TOOL_NAME_MAP, buildSummary, enrichSkillEvents } from '../../tool-events.js';
|
|
25
|
+
import { parseClaudeSdkMcpToolName } from './mcp-tool-name.js';
|
|
25
26
|
export function projectSdkMessages(input) {
|
|
26
27
|
let sessionId;
|
|
27
28
|
let initSkills;
|
|
@@ -59,7 +60,9 @@ export function projectSdkMessages(input) {
|
|
|
59
60
|
continue;
|
|
60
61
|
}
|
|
61
62
|
if (block.type === 'tool_use') {
|
|
62
|
-
|
|
63
|
+
const event = buildToolEvent(block, input.turnNumber, input.answerStore, input.mcpServerNames ?? [], input.deniedMcpEvents);
|
|
64
|
+
if (event)
|
|
65
|
+
toolEvents.push(event);
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
break;
|
|
@@ -109,7 +112,10 @@ export function projectSdkMessages(input) {
|
|
|
109
112
|
const trimmedResult = resultText.trim();
|
|
110
113
|
const visible = isError ? '' : (trimmedAssistant || trimmedResult);
|
|
111
114
|
const rawOutput = resultText || assistantText;
|
|
112
|
-
const enriched = enrichSkillEvents(
|
|
115
|
+
const enriched = enrichSkillEvents([
|
|
116
|
+
...toolEvents,
|
|
117
|
+
...(input.deniedMcpEvents?.all() ?? []),
|
|
118
|
+
]);
|
|
113
119
|
const finalToolEvents = prependSlashCommandSkillEvent(enriched, input.firstMessage, initSkills);
|
|
114
120
|
const traceOutput = input.messages.map((m) => JSON.stringify(m)).join('\n');
|
|
115
121
|
const result = {
|
|
@@ -161,12 +167,14 @@ function prependSlashCommandSkillEvent(events, firstMessage, initSkills) {
|
|
|
161
167
|
...events,
|
|
162
168
|
];
|
|
163
169
|
}
|
|
164
|
-
function buildToolEvent(block, turnNumber, answerStore) {
|
|
170
|
+
function buildToolEvent(block, turnNumber, answerStore, mcpServerNames, deniedMcpEvents) {
|
|
165
171
|
const providerToolName = String(block.name || 'unknown');
|
|
166
172
|
const rawInput = block.input ?? undefined;
|
|
167
173
|
const toolUseId = typeof block.id === 'string' ? block.id : undefined;
|
|
168
|
-
const mcpTool = parseClaudeSdkMcpToolName(providerToolName);
|
|
169
|
-
if (mcpTool) {
|
|
174
|
+
const mcpTool = parseClaudeSdkMcpToolName(providerToolName, mcpServerNames);
|
|
175
|
+
if (mcpTool.kind === 'mcp') {
|
|
176
|
+
if (deniedMcpEvents?.get(toolUseId))
|
|
177
|
+
return undefined;
|
|
170
178
|
const normalizedProviderToolName = `${mcpTool.server}.${mcpTool.tool}`;
|
|
171
179
|
const args = {
|
|
172
180
|
...(rawInput ?? {}),
|
|
@@ -202,12 +210,6 @@ function buildToolEvent(block, turnNumber, answerStore) {
|
|
|
202
210
|
rawSnippet,
|
|
203
211
|
};
|
|
204
212
|
}
|
|
205
|
-
function parseClaudeSdkMcpToolName(providerToolName) {
|
|
206
|
-
const match = providerToolName.match(/^mcp__([^_].*?)__(.+)$/);
|
|
207
|
-
if (!match)
|
|
208
|
-
return undefined;
|
|
209
|
-
return { server: match[1], tool: match[2] };
|
|
210
|
-
}
|
|
211
213
|
/**
|
|
212
214
|
* Boundary with the ask-user bridge.
|
|
213
215
|
*
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CanUseTool } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import type { AskBus } from '../../sdk/ask-bus/types.js';
|
|
3
|
+
import { type McpSafetyOptions } from '../../sdk/mcp-safety.js';
|
|
4
|
+
import type { AskUserAnswerStore } from './ask-user-answer-store.js';
|
|
5
|
+
import type { ClaudeDeniedMcpEventStore } from './denied-mcp-event-store.js';
|
|
6
|
+
export interface ClaudeToolPermissionBridgeDeps {
|
|
7
|
+
askBus: AskBus;
|
|
8
|
+
getTurnNumber: () => number;
|
|
9
|
+
answerStore: AskUserAnswerStore;
|
|
10
|
+
mcpServerNames: readonly string[];
|
|
11
|
+
mcpSafety?: McpSafetyOptions;
|
|
12
|
+
deniedMcpEvents?: ClaudeDeniedMcpEventStore;
|
|
13
|
+
}
|
|
14
|
+
export type ClaudeToolPermissionBridge = CanUseTool & {
|
|
15
|
+
lastError(): Error | null;
|
|
16
|
+
clearLastError(): void;
|
|
17
|
+
};
|
|
18
|
+
export declare function createClaudeToolPermissionBridge(deps: ClaudeToolPermissionBridgeDeps): ClaudeToolPermissionBridge;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { decideMcpToolCall, redactMcpSecrets, } from '../../sdk/mcp-safety.js';
|
|
2
|
+
import { createAskUserBridge, } from './ask-user-bridge.js';
|
|
3
|
+
import { parseClaudeSdkMcpToolName } from './mcp-tool-name.js';
|
|
4
|
+
export function createClaudeToolPermissionBridge(deps) {
|
|
5
|
+
const askUserBridge = createAskUserBridge({
|
|
6
|
+
askBus: deps.askBus,
|
|
7
|
+
getTurnNumber: deps.getTurnNumber,
|
|
8
|
+
answerStore: deps.answerStore,
|
|
9
|
+
});
|
|
10
|
+
const canUseTool = async (toolName, input, options) => {
|
|
11
|
+
if (toolName === 'AskUserQuestion') {
|
|
12
|
+
return askUserBridge(toolName, input, options);
|
|
13
|
+
}
|
|
14
|
+
const runMode = deps.mcpSafety?.runMode ?? 'mock';
|
|
15
|
+
const parsed = parseClaudeSdkMcpToolName(toolName, deps.mcpServerNames);
|
|
16
|
+
if (parsed.kind === 'non_mcp') {
|
|
17
|
+
return allow(input);
|
|
18
|
+
}
|
|
19
|
+
if (runMode === 'mock') {
|
|
20
|
+
return allow(input);
|
|
21
|
+
}
|
|
22
|
+
if (parsed.kind === 'unrecognized') {
|
|
23
|
+
return {
|
|
24
|
+
behavior: 'deny',
|
|
25
|
+
message: `unrecognized Claude SDK MCP tool name: ${toolName}`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const decision = decideMcpToolCall(deps.mcpSafety, {
|
|
29
|
+
serverName: parsed.server,
|
|
30
|
+
toolName: parsed.tool,
|
|
31
|
+
arguments: asRecord(input),
|
|
32
|
+
});
|
|
33
|
+
if (decision.action === 'allow')
|
|
34
|
+
return allow(input);
|
|
35
|
+
recordDeniedMcpEvent({
|
|
36
|
+
store: deps.deniedMcpEvents,
|
|
37
|
+
toolUseId: options.toolUseID,
|
|
38
|
+
serverName: parsed.server,
|
|
39
|
+
toolName: parsed.tool,
|
|
40
|
+
input: asRecord(input),
|
|
41
|
+
decision,
|
|
42
|
+
});
|
|
43
|
+
return { behavior: 'deny', message: decision.message };
|
|
44
|
+
};
|
|
45
|
+
const bridge = canUseTool;
|
|
46
|
+
bridge.lastError = () => askUserBridge.lastError();
|
|
47
|
+
bridge.clearLastError = () => askUserBridge.clearLastError();
|
|
48
|
+
return bridge;
|
|
49
|
+
}
|
|
50
|
+
function allow(input) {
|
|
51
|
+
return { behavior: 'allow', updatedInput: asRecord(input) };
|
|
52
|
+
}
|
|
53
|
+
function asRecord(value) {
|
|
54
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
55
|
+
? value
|
|
56
|
+
: {};
|
|
57
|
+
}
|
|
58
|
+
function recordDeniedMcpEvent(opts) {
|
|
59
|
+
if (!opts.store)
|
|
60
|
+
return;
|
|
61
|
+
const providerToolName = `${opts.serverName}.${opts.toolName}`;
|
|
62
|
+
const args = redactMcpSecrets(opts.input);
|
|
63
|
+
const event = {
|
|
64
|
+
action: 'mcp_tool_call',
|
|
65
|
+
provider: 'claude',
|
|
66
|
+
providerToolName,
|
|
67
|
+
arguments: {
|
|
68
|
+
...args,
|
|
69
|
+
server: opts.serverName,
|
|
70
|
+
tool: opts.toolName,
|
|
71
|
+
status: 'policy_denied',
|
|
72
|
+
policyResult: {
|
|
73
|
+
action: 'deny',
|
|
74
|
+
reason: opts.decision.reason,
|
|
75
|
+
message: opts.decision.message,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
summary: `MCP tool ${providerToolName} policy_denied`,
|
|
79
|
+
confidence: 'high',
|
|
80
|
+
rawSnippet: JSON.stringify(redactMcpSecrets({
|
|
81
|
+
toolUseID: opts.toolUseId,
|
|
82
|
+
input: opts.input,
|
|
83
|
+
})).slice(0, 200),
|
|
84
|
+
};
|
|
85
|
+
opts.store.record(opts.toolUseId, event);
|
|
86
|
+
}
|
package/dist/agents/claude.js
CHANGED
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
import { query as sdkQuery, } from '@anthropic-ai/claude-agent-sdk';
|
|
25
25
|
import { BaseAgent, getRuntimeEnv, getWorkspacePath, } from '../types.js';
|
|
26
26
|
import { createSandboxedClaudeSpawn } from '../providers/sandboxed-claude-spawn.js';
|
|
27
|
-
import { assertStdioMcpServersStartForClaudeSdk, mountMcpForClaudeSdk, } from '../providers/mcp-runtime-mounting.js';
|
|
27
|
+
import { assertClaudeLiveMcpSafetyPreflight, assertStdioMcpServersStartForClaudeSdk, mountMcpForClaudeSdk, } from '../providers/mcp-runtime-mounting.js';
|
|
28
28
|
import { buildClaudeSdkOptions, resolveClaudeCodeExecutable, } from './claude/sdk-options.js';
|
|
29
29
|
import { projectSdkMessages } from './claude/sdk-message-projector.js';
|
|
30
|
-
import { createAskUserBridge } from './claude/ask-user-bridge.js';
|
|
31
30
|
import { createAskUserAnswerStore } from './claude/ask-user-answer-store.js';
|
|
31
|
+
import { createClaudeToolPermissionBridge } from './claude/tool-permission-bridge.js';
|
|
32
|
+
import { createClaudeDeniedMcpEventStore } from './claude/denied-mcp-event-store.js';
|
|
32
33
|
import { requireAskBusForLiveBatches } from '../sdk/ask-bus/bus.js';
|
|
33
34
|
function createLinkedAbortController(signal) {
|
|
34
35
|
const controller = new AbortController();
|
|
@@ -76,6 +77,10 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
76
77
|
mcpConfigPath: sessionOptions?.mcpConfigPath,
|
|
77
78
|
runtimeEnv: getRuntimeEnv(runtime),
|
|
78
79
|
};
|
|
80
|
+
await assertClaudeLiveMcpSafetyPreflight({
|
|
81
|
+
...mcpMountOptions,
|
|
82
|
+
mcpSafety: sessionOptions?.mcpSafety,
|
|
83
|
+
});
|
|
79
84
|
await assertStdioMcpServersStartForClaudeSdk(mcpMountOptions);
|
|
80
85
|
const mcpServers = await mountMcpForClaudeSdk(mcpMountOptions);
|
|
81
86
|
let priorSessionId;
|
|
@@ -87,14 +92,24 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
87
92
|
// turn 2 cannot read a stale answer from turn 1 even on toolUseID
|
|
88
93
|
// collisions.
|
|
89
94
|
let answerStore = createAskUserAnswerStore();
|
|
90
|
-
|
|
95
|
+
let deniedMcpEvents = createClaudeDeniedMcpEventStore();
|
|
96
|
+
const bridge = createClaudeToolPermissionBridge({
|
|
91
97
|
askBus,
|
|
92
98
|
getTurnNumber: () => turnNumber,
|
|
93
99
|
answerStore: { record: (id, e) => answerStore.record(id, e), get: (id) => answerStore.get(id) },
|
|
100
|
+
mcpServerNames: mcpServers ? Object.keys(mcpServers) : [],
|
|
101
|
+
mcpSafety: sessionOptions?.mcpSafety,
|
|
102
|
+
deniedMcpEvents: {
|
|
103
|
+
record: (id, event) => deniedMcpEvents.record(id, event),
|
|
104
|
+
get: (id) => deniedMcpEvents.get(id),
|
|
105
|
+
all: () => deniedMcpEvents.all(),
|
|
106
|
+
clear: () => deniedMcpEvents.clear(),
|
|
107
|
+
},
|
|
94
108
|
});
|
|
95
109
|
const runTurn = async (message) => {
|
|
96
110
|
turnNumber += 1;
|
|
97
111
|
answerStore = createAskUserAnswerStore();
|
|
112
|
+
deniedMcpEvents = createClaudeDeniedMcpEventStore();
|
|
98
113
|
// Clear any ask-bus rejection captured on a prior turn so a
|
|
99
114
|
// stale error never causes a spurious result on this turn.
|
|
100
115
|
bridge.clearLastError();
|
|
@@ -128,6 +143,8 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
128
143
|
turnNumber,
|
|
129
144
|
firstMessage: projectorFirstMessage,
|
|
130
145
|
answerStore,
|
|
146
|
+
mcpServerNames: mcpServers ? Object.keys(mcpServers) : [],
|
|
147
|
+
deniedMcpEvents,
|
|
131
148
|
});
|
|
132
149
|
// Capture the SDK-reported session id BEFORE checking for a bus
|
|
133
150
|
// rejection so the next turn's `Options.resume` points at this
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { BaseAgent, getRuntimeEnv, getWorkspacePath, } from '../../types.js';
|
|
2
2
|
import { mountMcpForCodexAppServer } from '../../providers/mcp-runtime-mounting.js';
|
|
3
3
|
import { assertMcpSecretReferencesReady } from '../../providers/mcp-config.js';
|
|
4
|
+
import { buildSummary, enrichSkillEvents, extractSkillNameFromPath, inferCodexExecAction, } from '../../tool-events.js';
|
|
4
5
|
import { requireAskBusForLiveBatches, } from '../../sdk/ask-bus/bus.js';
|
|
5
6
|
import { toAskUserToolEvent } from '../../sdk/ask-bus/projection.js';
|
|
6
7
|
import { decideMcpToolCall, redactMcpSecrets, } from '../../sdk/mcp-safety.js';
|
|
7
8
|
import { spawnAppServerTransport, } from './transport.js';
|
|
8
9
|
import { normalizeUpstreamQuestion, toWireAnswerMap, } from './wire-translators.js';
|
|
9
|
-
const DEFAULT_MODEL = 'gpt-5.
|
|
10
|
+
const DEFAULT_MODEL = 'gpt-5.4';
|
|
10
11
|
const TURN_COMPLETED_METHOD = 'turn/completed';
|
|
11
12
|
function recordFromUnknown(value) {
|
|
12
13
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
@@ -40,6 +41,27 @@ function extractMcpToolApprovalRequest(params) {
|
|
|
40
41
|
function parseToolNameFromApprovalMessage(message) {
|
|
41
42
|
return message.match(/tool\s+"([^"]+)"/i)?.[1];
|
|
42
43
|
}
|
|
44
|
+
function extractCommandActionSkillName(action) {
|
|
45
|
+
if (typeof action.path === 'string') {
|
|
46
|
+
const direct = extractSkillNameFromPath(action.path);
|
|
47
|
+
if (direct)
|
|
48
|
+
return direct;
|
|
49
|
+
const embedded = extractSkillNameFromText(action.path);
|
|
50
|
+
if (embedded)
|
|
51
|
+
return embedded;
|
|
52
|
+
}
|
|
53
|
+
return typeof action.command === 'string' ? extractSkillNameFromText(action.command) : undefined;
|
|
54
|
+
}
|
|
55
|
+
function extractSkillNameFromText(value) {
|
|
56
|
+
if (!value)
|
|
57
|
+
return undefined;
|
|
58
|
+
return value.match(/(?:^|[/\s"'])\.(?:agents|claude)\/skills\/([^/\s"']+)\/SKILL\.md(?:$|[\s"'])/)?.[1];
|
|
59
|
+
}
|
|
60
|
+
function extractSkillPathFromText(value) {
|
|
61
|
+
if (!value)
|
|
62
|
+
return undefined;
|
|
63
|
+
return value.match(/(?:^|[\s"'])(?<path>(?:\/|\.{1,2}\/)?[^\s"']*(?:\.agents|\.claude)\/skills\/[^/\s"']+\/SKILL\.md)(?:$|[\s"'])/)?.groups?.path;
|
|
64
|
+
}
|
|
43
65
|
function projectItemIntoTurn(item, turn) {
|
|
44
66
|
if (item.type === 'agentMessage') {
|
|
45
67
|
const msg = item;
|
|
@@ -50,16 +72,40 @@ function projectItemIntoTurn(item, turn) {
|
|
|
50
72
|
}
|
|
51
73
|
if (item.type === 'commandExecution') {
|
|
52
74
|
const cmd = item;
|
|
75
|
+
const action = inferCodexExecAction(cmd.command);
|
|
76
|
+
const skillPath = extractSkillPathFromText(cmd.command);
|
|
77
|
+
const args = {
|
|
78
|
+
command: cmd.command,
|
|
79
|
+
...(skillPath ? { path: skillPath } : {}),
|
|
80
|
+
};
|
|
53
81
|
turn.nonAskToolEvents.push({
|
|
54
|
-
action
|
|
82
|
+
action,
|
|
55
83
|
provider: 'codex',
|
|
56
84
|
providerToolName: 'commandExecution',
|
|
57
85
|
turnNumber: turn.turnNumber,
|
|
58
|
-
arguments:
|
|
59
|
-
summary:
|
|
86
|
+
arguments: args,
|
|
87
|
+
summary: buildSummary(action, 'commandExecution', args),
|
|
60
88
|
confidence: 'high',
|
|
61
89
|
rawSnippet: JSON.stringify(cmd),
|
|
62
90
|
});
|
|
91
|
+
const recordedSkills = new Set();
|
|
92
|
+
for (const action of cmd.commandActions ?? []) {
|
|
93
|
+
const skillName = extractCommandActionSkillName(action) ?? extractSkillNameFromText(cmd.command);
|
|
94
|
+
if (!skillName || recordedSkills.has(skillName))
|
|
95
|
+
continue;
|
|
96
|
+
recordedSkills.add(skillName);
|
|
97
|
+
turn.nonAskToolEvents.push({
|
|
98
|
+
action: 'use_skill',
|
|
99
|
+
provider: 'codex',
|
|
100
|
+
providerToolName: `commandExecution.commandActions.${action.type ?? 'unknown'}`,
|
|
101
|
+
turnNumber: turn.turnNumber,
|
|
102
|
+
arguments: { path: action.path, name: action.name },
|
|
103
|
+
summary: `use_skill ${skillName}`,
|
|
104
|
+
confidence: 'high',
|
|
105
|
+
rawSnippet: JSON.stringify(action),
|
|
106
|
+
skillName,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
63
109
|
return;
|
|
64
110
|
}
|
|
65
111
|
if (item.type === 'fileChange') {
|
|
@@ -515,7 +561,7 @@ function assembleTurnResult(args) {
|
|
|
515
561
|
.snapshot()
|
|
516
562
|
.filter((s) => askBatchIds.has(s.batchId))
|
|
517
563
|
.map((s) => toAskUserToolEvent(s));
|
|
518
|
-
const toolEvents = [...askEvents, ...activeTurn.nonAskToolEvents];
|
|
564
|
+
const toolEvents = enrichSkillEvents([...askEvents, ...activeTurn.nonAskToolEvents]);
|
|
519
565
|
const rawOutput = exitCode === 0
|
|
520
566
|
? message
|
|
521
567
|
: [
|
|
@@ -142,9 +142,11 @@ function quoteCodexConfigString(value) {
|
|
|
142
142
|
}
|
|
143
143
|
export function buildAppServerSpawnArgs(args = [], env = process.env) {
|
|
144
144
|
const baseUrl = env.OPENAI_BASE_URL?.trim();
|
|
145
|
+
const requestUserInputConfig = ['-c', 'features.default_mode_request_user_input=true'];
|
|
145
146
|
if (!baseUrl)
|
|
146
|
-
return [...args, 'app-server'];
|
|
147
|
+
return [...requestUserInputConfig, ...args, 'app-server'];
|
|
147
148
|
return [
|
|
149
|
+
...requestUserInputConfig,
|
|
148
150
|
'-c', `model_provider=${quoteCodexConfigString(CODEX_PROXY_PROVIDER_ID)}`,
|
|
149
151
|
'-c', `model_providers.${CODEX_PROXY_PROVIDER_ID}.name=${quoteCodexConfigString('PathGrade OpenAI Proxy')}`,
|
|
150
152
|
'-c', `model_providers.${CODEX_PROXY_PROVIDER_ID}.base_url=${quoteCodexConfigString(baseUrl)}`,
|
package/dist/agents/codex.js
CHANGED
|
@@ -26,7 +26,7 @@ export class CodexAgent extends TranscriptAgent {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
const DEFAULT_CODEX_MODEL = 'gpt-5.
|
|
29
|
+
const DEFAULT_CODEX_MODEL = 'gpt-5.4';
|
|
30
30
|
const CODEX_PROXY_PROVIDER_ID = 'pathgrade_openai_proxy';
|
|
31
31
|
function buildCodexExecCommand(promptPath, model = DEFAULT_CODEX_MODEL) {
|
|
32
32
|
const quotedPromptPath = JSON.stringify(promptPath);
|
package/dist/agents/cursor.js
CHANGED
|
@@ -2,7 +2,7 @@ import { BaseAgent, getWorkspacePath } from '../types.js';
|
|
|
2
2
|
import { TOOL_NAME_MAP, buildSummary, enrichSkillEvents } from '../tool-events.js';
|
|
3
3
|
import { getVisibleAssistantMessage } from '../sdk/visible-turn.js';
|
|
4
4
|
import { prependRuntimePolicies } from '../sdk/runtime-policy.js';
|
|
5
|
-
import { mountMcpForCursor } from '../providers/mcp-runtime-mounting.js';
|
|
5
|
+
import { assertUnsupportedLiveMcpSafetyRuntime, mountMcpForCursor, } from '../providers/mcp-runtime-mounting.js';
|
|
6
6
|
/**
|
|
7
7
|
* Cursor tool_call discriminants observed in the discovery spike. Probed in
|
|
8
8
|
* order — first present key wins. Explicit probing (vs. `Object.keys(…)[0]`)
|
|
@@ -170,6 +170,12 @@ export class CursorAgent extends BaseAgent {
|
|
|
170
170
|
return getVisibleAssistantMessage(result);
|
|
171
171
|
}
|
|
172
172
|
async runTurn(instruction, workspacePath, runCommand, sessionId, options) {
|
|
173
|
+
await assertUnsupportedLiveMcpSafetyRuntime({
|
|
174
|
+
runtimeName: 'Cursor',
|
|
175
|
+
workspacePath,
|
|
176
|
+
mcpConfigPath: options?.mcpConfigPath,
|
|
177
|
+
mcpSafety: options?.mcpSafety,
|
|
178
|
+
});
|
|
173
179
|
// Runtime policies are injected into the first-turn prompt only.
|
|
174
180
|
// On resumed turns (sessionId set) the policy was already delivered,
|
|
175
181
|
// so we skip re-prepending. The Claude SDK driver does not prepend
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { McpStdioEntry } from './mcp-config.js';
|
|
2
|
+
import type { McpSafetyOptions } from '../sdk/mcp-safety.js';
|
|
2
3
|
export type ClaudeSdkMcpServerEntry = McpStdioEntry | {
|
|
3
4
|
type: 'http';
|
|
4
5
|
url: string;
|
|
@@ -25,9 +26,18 @@ interface WorkspaceMcpRuntimeMountingOptions extends McpRuntimeMountingOptions {
|
|
|
25
26
|
workspacePath: string;
|
|
26
27
|
runtimeEnv?: Record<string, string>;
|
|
27
28
|
}
|
|
29
|
+
interface ClaudeLiveMcpSafetyPreflightOptions extends WorkspaceMcpRuntimeMountingOptions {
|
|
30
|
+
mcpSafety?: McpSafetyOptions;
|
|
31
|
+
}
|
|
32
|
+
interface UnsupportedLiveMcpSafetyRuntimeOptions extends WorkspaceMcpRuntimeMountingOptions {
|
|
33
|
+
runtimeName: string;
|
|
34
|
+
mcpSafety?: McpSafetyOptions;
|
|
35
|
+
}
|
|
28
36
|
export interface CursorMcpRuntimeMount {
|
|
29
37
|
approveMcps: boolean;
|
|
30
38
|
}
|
|
39
|
+
export declare function assertClaudeLiveMcpSafetyPreflight(options: ClaudeLiveMcpSafetyPreflightOptions): Promise<void>;
|
|
40
|
+
export declare function assertUnsupportedLiveMcpSafetyRuntime(options: UnsupportedLiveMcpSafetyRuntimeOptions): Promise<void>;
|
|
31
41
|
export declare function assertStdioMcpServersStartForClaudeSdk(options: WorkspaceMcpRuntimeMountingOptions): Promise<void>;
|
|
32
42
|
export declare function mountMcpForClaudeSdk(options: WorkspaceMcpRuntimeMountingOptions): Promise<McpServersObject | undefined>;
|
|
33
43
|
export declare function mountMcpForCodexAppServer(options: WorkspaceMcpRuntimeMountingOptions): Promise<CodexAppServerMcpConfig | undefined>;
|
|
@@ -33,6 +33,23 @@ function toCodexAppServerMcpServerEntry(entry) {
|
|
|
33
33
|
tool_timeout_sec: entry.tool_timeout_sec,
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
+
function isLiveMcpSafetyMode(options) {
|
|
37
|
+
const runMode = options?.runMode ?? 'mock';
|
|
38
|
+
return runMode === 'live-readonly' || runMode === 'live-sandbox' || runMode === 'live';
|
|
39
|
+
}
|
|
40
|
+
function describeClaudeIncompatibleFields(serverName, entry) {
|
|
41
|
+
if (isStdioEntry(entry))
|
|
42
|
+
return [];
|
|
43
|
+
const fields = [];
|
|
44
|
+
if (entry.env_http_headers !== undefined)
|
|
45
|
+
fields.push('env_http_headers');
|
|
46
|
+
if (entry.bearer_token_env_var !== undefined)
|
|
47
|
+
fields.push('bearer_token_env_var');
|
|
48
|
+
if (entry.headers !== undefined && entry.http_headers !== undefined) {
|
|
49
|
+
fields.push('headers and http_headers');
|
|
50
|
+
}
|
|
51
|
+
return fields.map((field) => `${serverName}: ${field}`);
|
|
52
|
+
}
|
|
36
53
|
function formatMcpStartupError(serverName, error, stderr) {
|
|
37
54
|
const message = error instanceof Error ? error.message : String(error);
|
|
38
55
|
const stderrSuffix = stderr.trim() ? `\nstderr:\n${stderr.trim()}` : '';
|
|
@@ -67,6 +84,28 @@ async function assertStdioMcpServerStarts(workspacePath, serverName, entry, runt
|
|
|
67
84
|
await transport.close().catch(() => undefined);
|
|
68
85
|
}
|
|
69
86
|
}
|
|
87
|
+
export async function assertClaudeLiveMcpSafetyPreflight(options) {
|
|
88
|
+
if (!options.mcpConfigPath || !isLiveMcpSafetyMode(options.mcpSafety))
|
|
89
|
+
return;
|
|
90
|
+
const mcpServers = await readStagedMcpServers(options.workspacePath, options.mcpConfigPath);
|
|
91
|
+
if (!mcpServers)
|
|
92
|
+
return;
|
|
93
|
+
if (options.mcpSafety?.liveOptIn !== true) {
|
|
94
|
+
throw new Error('Claude live MCP safety requires mcpSafety.liveOptIn: true before MCP runtime mounting.');
|
|
95
|
+
}
|
|
96
|
+
const unsupported = Object.entries(mcpServers).flatMap(([serverName, entry]) => describeClaudeIncompatibleFields(serverName, entry));
|
|
97
|
+
if (unsupported.length > 0) {
|
|
98
|
+
throw new Error(`Claude live MCP safety does not support runtime-incompatible MCP config fields: ${unsupported.join('; ')}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export async function assertUnsupportedLiveMcpSafetyRuntime(options) {
|
|
102
|
+
if (!options.mcpConfigPath || !isLiveMcpSafetyMode(options.mcpSafety))
|
|
103
|
+
return;
|
|
104
|
+
const mcpServers = await readStagedMcpServers(options.workspacePath, options.mcpConfigPath);
|
|
105
|
+
if (!mcpServers)
|
|
106
|
+
return;
|
|
107
|
+
throw new Error(`${options.runtimeName} live MCP safety enforcement is not supported. Use mock mode or a runtime that enforces mcpSafety before live MCP mounting.`);
|
|
108
|
+
}
|
|
70
109
|
export async function assertStdioMcpServersStartForClaudeSdk(options) {
|
|
71
110
|
const mcpServers = await readStagedMcpServers(options.workspacePath, options.mcpConfigPath);
|
|
72
111
|
if (!mcpServers)
|
|
@@ -29,7 +29,8 @@ export async function createSandbox(spec) {
|
|
|
29
29
|
const skillName = path.basename(skillPath);
|
|
30
30
|
const skillFilter = (src) => {
|
|
31
31
|
const rel = path.relative(skillPath, src);
|
|
32
|
-
|
|
32
|
+
const [topLevelDir] = rel.split(path.sep);
|
|
33
|
+
if (topLevelDir === 'test' || topLevelDir === 'tests') {
|
|
33
34
|
return false;
|
|
34
35
|
}
|
|
35
36
|
return copyFilter(src);
|
package/dist/sdk/mcp-safety.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface McpToolCallRequest {
|
|
|
18
18
|
toolName: string;
|
|
19
19
|
arguments?: Record<string, unknown>;
|
|
20
20
|
}
|
|
21
|
-
export type McpPolicyDenialReason = 'missing_live_opt_in' | 'missing_mcp_tool_policy' | 'denylisted' | 'not_allowlisted' | 'not_marked_readonly';
|
|
21
|
+
export type McpPolicyDenialReason = 'missing_live_opt_in' | 'missing_mcp_tool_policy' | 'denylisted' | 'not_allowlisted' | 'not_marked_readonly' | 'unrecognized_mcp_tool_name';
|
|
22
22
|
export type McpToolPolicyDecision = {
|
|
23
23
|
action: 'allow';
|
|
24
24
|
} | {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/pathgrade",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.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",
|
|
@@ -80,7 +80,6 @@
|
|
|
80
80
|
"vitest": "^4.0.0"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
|
-
"@modelcontextprotocol/sdk": "1.29.0",
|
|
84
83
|
"@types/fs-extra": "^11.0.4",
|
|
85
84
|
"@types/picomatch": "^4.0.2",
|
|
86
85
|
"@vitest/coverage-v8": "^4.0.18",
|
|
@@ -90,6 +89,7 @@
|
|
|
90
89
|
},
|
|
91
90
|
"dependencies": {
|
|
92
91
|
"@anthropic-ai/claude-agent-sdk": "0.2.85",
|
|
92
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
93
93
|
"@types/node": "^25.3.1",
|
|
94
94
|
"fs-extra": "^11.3.3",
|
|
95
95
|
"jiti": "^2.6.1",
|
|
@@ -97,5 +97,5 @@
|
|
|
97
97
|
"typescript": "^5.9.3",
|
|
98
98
|
"zod": "4.3.6"
|
|
99
99
|
},
|
|
100
|
-
"falconPackageHash": "
|
|
100
|
+
"falconPackageHash": "7d7c65385d2ddc29c1b518b76ca8c6c57fd24241c74c182d1fd6fb56"
|
|
101
101
|
}
|