@wix/pathgrade 0.31.0 → 0.33.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/sdk-message-projector.js +26 -0
- package/dist/agents/claude/sdk-options.d.ts +4 -2
- package/dist/agents/claude/sdk-options.js +2 -0
- package/dist/agents/claude.d.ts +1 -1
- package/dist/agents/claude.js +24 -3
- package/dist/agents/codex-app-server/agent.js +162 -3
- package/dist/agents/codex.js +4 -0
- package/dist/agents/cursor.js +6 -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 +36 -0
- package/dist/providers/mcp-runtime-mounting.js +116 -0
- package/dist/providers/sandbox.d.ts +1 -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 +7 -2
- 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 +15 -2
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { McpStdioEntry } from './mcp-config.js';
|
|
2
|
+
export type ClaudeSdkMcpServerEntry = McpStdioEntry | {
|
|
3
|
+
type: 'http';
|
|
4
|
+
url: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
};
|
|
7
|
+
/** Object form of MCP server entries the SDK driver passes to `Options.mcpServers`. */
|
|
8
|
+
export type McpServersObject = Record<string, ClaudeSdkMcpServerEntry>;
|
|
9
|
+
export type CodexMcpServerEntry = McpStdioEntry | {
|
|
10
|
+
url: string;
|
|
11
|
+
http_headers?: Record<string, string>;
|
|
12
|
+
env_http_headers?: Record<string, string>;
|
|
13
|
+
bearer_token_env_var?: string;
|
|
14
|
+
startup_timeout_sec?: number;
|
|
15
|
+
tool_timeout_sec?: number;
|
|
16
|
+
};
|
|
17
|
+
export interface CodexAppServerMcpConfig extends Record<string, unknown> {
|
|
18
|
+
mcp_servers: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
export interface McpRuntimeMountingOptions {
|
|
21
|
+
workspacePath?: string;
|
|
22
|
+
mcpConfigPath?: string;
|
|
23
|
+
}
|
|
24
|
+
interface WorkspaceMcpRuntimeMountingOptions extends McpRuntimeMountingOptions {
|
|
25
|
+
workspacePath: string;
|
|
26
|
+
runtimeEnv?: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
export interface CursorMcpRuntimeMount {
|
|
29
|
+
approveMcps: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function assertStdioMcpServersStartForClaudeSdk(options: WorkspaceMcpRuntimeMountingOptions): Promise<void>;
|
|
32
|
+
export declare function mountMcpForClaudeSdk(options: WorkspaceMcpRuntimeMountingOptions): Promise<McpServersObject | undefined>;
|
|
33
|
+
export declare function mountMcpForCodexAppServer(options: WorkspaceMcpRuntimeMountingOptions): Promise<CodexAppServerMcpConfig | undefined>;
|
|
34
|
+
export declare function mountMcpForCursor(options: WorkspaceMcpRuntimeMountingOptions): Promise<CursorMcpRuntimeMount>;
|
|
35
|
+
export declare function assertMcpRuntimeMountingSupportedForCodexExec(options: McpRuntimeMountingOptions): Promise<void>;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
4
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
5
|
+
import { readStagedMcpServers } from './mcp-config.js';
|
|
6
|
+
function isStdioEntry(entry) {
|
|
7
|
+
return typeof entry.command === 'string';
|
|
8
|
+
}
|
|
9
|
+
function headersForRemote(entry) {
|
|
10
|
+
return entry.headers ?? entry.http_headers;
|
|
11
|
+
}
|
|
12
|
+
function compact(entry) {
|
|
13
|
+
return Object.fromEntries(Object.entries(entry).filter(([, value]) => value !== undefined));
|
|
14
|
+
}
|
|
15
|
+
function toClaudeSdkMcpServerEntry(entry) {
|
|
16
|
+
if (isStdioEntry(entry))
|
|
17
|
+
return entry;
|
|
18
|
+
return compact({
|
|
19
|
+
type: 'http',
|
|
20
|
+
url: entry.url,
|
|
21
|
+
headers: headersForRemote(entry),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function toCodexAppServerMcpServerEntry(entry) {
|
|
25
|
+
if (isStdioEntry(entry))
|
|
26
|
+
return entry;
|
|
27
|
+
return compact({
|
|
28
|
+
url: entry.url,
|
|
29
|
+
http_headers: headersForRemote(entry),
|
|
30
|
+
env_http_headers: entry.env_http_headers,
|
|
31
|
+
bearer_token_env_var: entry.bearer_token_env_var,
|
|
32
|
+
startup_timeout_sec: entry.startup_timeout_sec,
|
|
33
|
+
tool_timeout_sec: entry.tool_timeout_sec,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function formatMcpStartupError(serverName, error, stderr) {
|
|
37
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
38
|
+
const stderrSuffix = stderr.trim() ? `\nstderr:\n${stderr.trim()}` : '';
|
|
39
|
+
return new Error(`MCP server ${serverName} failed to start: ${message}${stderrSuffix}`);
|
|
40
|
+
}
|
|
41
|
+
async function assertStdioMcpServerStarts(workspacePath, serverName, entry, runtimeEnv) {
|
|
42
|
+
const timeoutMs = Math.max(1, entry.startup_timeout_sec ?? 15) * 1000;
|
|
43
|
+
const transport = new StdioClientTransport({
|
|
44
|
+
command: entry.command,
|
|
45
|
+
args: entry.args,
|
|
46
|
+
env: {
|
|
47
|
+
...(runtimeEnv ?? {}),
|
|
48
|
+
...(entry.env ?? {}),
|
|
49
|
+
},
|
|
50
|
+
cwd: workspacePath,
|
|
51
|
+
stderr: 'pipe',
|
|
52
|
+
});
|
|
53
|
+
let stderr = '';
|
|
54
|
+
transport.stderr?.on('data', (chunk) => {
|
|
55
|
+
stderr += chunk.toString();
|
|
56
|
+
});
|
|
57
|
+
const client = new Client({ name: 'pathgrade-stdio-mcp-preflight', version: '0.5.0' }, { capabilities: {} });
|
|
58
|
+
try {
|
|
59
|
+
await client.connect(transport, { timeout: timeoutMs });
|
|
60
|
+
await client.listTools(undefined, { timeout: timeoutMs });
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
throw formatMcpStartupError(serverName, error, stderr);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
await client.close().catch(() => undefined);
|
|
67
|
+
await transport.close().catch(() => undefined);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export async function assertStdioMcpServersStartForClaudeSdk(options) {
|
|
71
|
+
const mcpServers = await readStagedMcpServers(options.workspacePath, options.mcpConfigPath);
|
|
72
|
+
if (!mcpServers)
|
|
73
|
+
return;
|
|
74
|
+
for (const [name, entry] of Object.entries(mcpServers)) {
|
|
75
|
+
if (!isStdioEntry(entry))
|
|
76
|
+
continue;
|
|
77
|
+
await assertStdioMcpServerStarts(options.workspacePath, name, entry, options.runtimeEnv);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export async function mountMcpForClaudeSdk(options) {
|
|
81
|
+
const mcpServers = await readStagedMcpServers(options.workspacePath, options.mcpConfigPath);
|
|
82
|
+
if (!mcpServers)
|
|
83
|
+
return undefined;
|
|
84
|
+
return Object.fromEntries(Object.entries(mcpServers).map(([name, entry]) => [
|
|
85
|
+
name,
|
|
86
|
+
toClaudeSdkMcpServerEntry(entry),
|
|
87
|
+
]));
|
|
88
|
+
}
|
|
89
|
+
export async function mountMcpForCodexAppServer(options) {
|
|
90
|
+
const mcpServers = await readStagedMcpServers(options.workspacePath, options.mcpConfigPath);
|
|
91
|
+
if (!mcpServers)
|
|
92
|
+
return undefined;
|
|
93
|
+
return {
|
|
94
|
+
mcp_servers: Object.fromEntries(Object.entries(mcpServers).map(([name, entry]) => [
|
|
95
|
+
name,
|
|
96
|
+
toCodexAppServerMcpServerEntry(entry),
|
|
97
|
+
])),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export async function mountMcpForCursor(options) {
|
|
101
|
+
if (!options.mcpConfigPath)
|
|
102
|
+
return { approveMcps: false };
|
|
103
|
+
await readStagedMcpServers(options.workspacePath, options.mcpConfigPath);
|
|
104
|
+
const srcMcp = path.join(options.workspacePath, options.mcpConfigPath);
|
|
105
|
+
if (!(await fs.pathExists(srcMcp)))
|
|
106
|
+
return { approveMcps: false };
|
|
107
|
+
const cursorDir = path.join(options.workspacePath, '.cursor');
|
|
108
|
+
await fs.ensureDir(cursorDir);
|
|
109
|
+
await fs.copy(srcMcp, path.join(cursorDir, 'mcp.json'), { overwrite: true });
|
|
110
|
+
return { approveMcps: true };
|
|
111
|
+
}
|
|
112
|
+
export async function assertMcpRuntimeMountingSupportedForCodexExec(options) {
|
|
113
|
+
if (!options.mcpConfigPath)
|
|
114
|
+
return;
|
|
115
|
+
throw new Error('Codex exec does not support MCP Runtime Mounting. Use the codex app-server transport for fixtures that stage MCP config.');
|
|
116
|
+
}
|
|
@@ -4,7 +4,7 @@ export interface SandboxConfig {
|
|
|
4
4
|
skillDir?: string;
|
|
5
5
|
copyFromHome?: string[];
|
|
6
6
|
env?: Record<string, string>;
|
|
7
|
-
mcp?: import('./mcp-config.js').
|
|
7
|
+
mcp?: import('./mcp-config.js').McpDeclaration;
|
|
8
8
|
/**
|
|
9
9
|
* Glob patterns to ignore when copying workspace and skill directories.
|
|
10
10
|
* Replaces the default ignore list entirely. Pass `[]` to disable filtering.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type SandboxConfig } from './sandbox.js';
|
|
2
2
|
import type { CommandResult } from '../types.js';
|
|
3
|
-
export type {
|
|
3
|
+
export type { McpDeclaration } from './mcp-config.js';
|
|
4
4
|
export interface Workspace {
|
|
5
5
|
readonly path: string;
|
|
6
6
|
readonly mcpConfigPath: string | undefined;
|
|
@@ -2,7 +2,7 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { createSandbox } from './sandbox.js';
|
|
5
|
-
import {
|
|
5
|
+
import { stageMcpConfig } from './mcp-config.js';
|
|
6
6
|
import { sandboxExec } from './sandbox-exec.js';
|
|
7
7
|
import { resolveCredentials } from './credentials.js';
|
|
8
8
|
async function copyPathsFromHostHome(pathsToCopy, sandboxHomePath) {
|
|
@@ -38,7 +38,7 @@ export async function prepareWorkspace(spec) {
|
|
|
38
38
|
Object.assign(sandboxEnv, creds.env);
|
|
39
39
|
await copyPathsFromHostHome(creds.copyFromHome, homePath);
|
|
40
40
|
await linkPathsFromHostHome(creds.linkFromHome ?? [], homePath);
|
|
41
|
-
const { mcpConfigPath } = await
|
|
41
|
+
const { mcpConfigPath } = await stageMcpConfig(workspacePath, mcp);
|
|
42
42
|
let disposed = false;
|
|
43
43
|
return {
|
|
44
44
|
path: workspacePath,
|
package/dist/sdk/agent.js
CHANGED
|
@@ -39,7 +39,8 @@ class AgentImpl {
|
|
|
39
39
|
lastConversationResult = null;
|
|
40
40
|
verbose;
|
|
41
41
|
transport;
|
|
42
|
-
|
|
42
|
+
mcpSafety;
|
|
43
|
+
constructor(ws, agentName, llm, timeoutSetting, conversationWindow, modelOpt, debugOpt, debugName, debugBaseDir, verbose, transport, mcpSafety) {
|
|
43
44
|
this.ws = ws;
|
|
44
45
|
this.agentName = agentName;
|
|
45
46
|
this.llm = llm;
|
|
@@ -51,6 +52,7 @@ class AgentImpl {
|
|
|
51
52
|
this.debugBaseDir = debugBaseDir;
|
|
52
53
|
this.verbose = verbose;
|
|
53
54
|
this.transport = transport;
|
|
55
|
+
this.mcpSafety = mcpSafety;
|
|
54
56
|
}
|
|
55
57
|
get messages() {
|
|
56
58
|
return this._messages;
|
|
@@ -79,6 +81,7 @@ class AgentImpl {
|
|
|
79
81
|
llm: this.llm,
|
|
80
82
|
...(askUserTimeoutMs !== undefined ? { askUserTimeoutMs } : {}),
|
|
81
83
|
...(this.transport !== undefined ? { transport: this.transport } : {}),
|
|
84
|
+
...(this.mcpSafety !== undefined ? { mcpSafety: this.mcpSafety } : {}),
|
|
82
85
|
});
|
|
83
86
|
}
|
|
84
87
|
resolveTimeoutSec(mode, maxTurns) {
|
|
@@ -345,11 +348,11 @@ export async function createAgent(opts) {
|
|
|
345
348
|
const timeoutSetting = opts.timeout ?? 300;
|
|
346
349
|
// Capture test context now, while vitest state is available
|
|
347
350
|
const testCtx = opts.debug ? resolveTestContext() : { name: '', dir: '' };
|
|
348
|
-
const { timeout: _, mcpMock, agent: __, debug: ___, model: ____, transport: _____, ...rest } = opts;
|
|
351
|
+
const { timeout: _, mcpMock, mcpConfigFile, agent: __, debug: ___, model: ____, transport: _____, mcpSafety: ______, ...rest } = opts;
|
|
349
352
|
const workspace = await prepareWorkspace({
|
|
350
353
|
...rest,
|
|
351
354
|
agent: agentName,
|
|
352
|
-
mcp: mcpMock ? { mock: mcpMock } : undefined,
|
|
355
|
+
mcp: mcpConfigFile ? { configFile: mcpConfigFile } : mcpMock ? { mock: mcpMock } : undefined,
|
|
353
356
|
});
|
|
354
357
|
// Create agent LLM once, using the fully-resolved sandbox env (includes
|
|
355
358
|
// keychain OAuth tokens, API keys, safe host vars).
|
|
@@ -363,7 +366,7 @@ export async function createAgent(opts) {
|
|
|
363
366
|
sink: verboseSinkOverride ?? undefined,
|
|
364
367
|
testName: testCtx.name || undefined,
|
|
365
368
|
});
|
|
366
|
-
const agent = new AgentImpl(workspace, agentName, llm, timeoutSetting, opts.conversationWindow, opts.model, opts.debug, debugName, debugBaseDir, verbose, transport);
|
|
369
|
+
const agent = new AgentImpl(workspace, agentName, llm, timeoutSetting, opts.conversationWindow, opts.model, opts.debug, debugName, debugBaseDir, verbose, transport, opts.mcpSafety);
|
|
367
370
|
lifecycle.trackAgent(agent);
|
|
368
371
|
return agent;
|
|
369
372
|
}
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { createAgent } from './agent.js';
|
|
|
2
2
|
export { resolveAgentName, resolveCodexTransport, InvalidTransportEnvError, } from './agent-resolution.js';
|
|
3
3
|
export { AgentCrashError } from './agent-crash.js';
|
|
4
4
|
export { check, score, judge, toolUsage } from './scorers.js';
|
|
5
|
+
export { getMcpToolCall, isMcpToolCall, findMcpToolCalls, getMcpStartupStatus, isMcpStartupStatus, } from './mcp-evidence.js';
|
|
6
|
+
export { decideMcpToolCall, redactMcpSecrets, } from './mcp-safety.js';
|
|
5
7
|
export { evaluate, EvalScorerError } from './evaluate.js';
|
|
6
8
|
export { RUN_SNAPSHOT_VERSION, buildRunSnapshot, loadRunSnapshot, SnapshotParseError, SnapshotVersionError, WorkspaceMissingError, } from './snapshots.js';
|
|
7
9
|
export { createPersona } from './persona.js';
|
|
@@ -16,14 +18,17 @@ export { toAskUserToolEvent } from './ask-bus/projection.js';
|
|
|
16
18
|
export type { AskUserToolEvent, AskUserToolEventArguments, AskUserToolEventQuestionArgument, } from './ask-bus/projection.js';
|
|
17
19
|
export { buildAskBatchLogEntries } from './agent-result-log.js';
|
|
18
20
|
export { getAgentCapabilities } from './types.js';
|
|
19
|
-
export type { AgentTransport, AgentCapabilities, AgentName } from './types.js';
|
|
21
|
+
export type { AgentTransport, AgentCapabilities, AgentName, McpRunMode, McpSafetyOptions, McpToolPolicy, McpToolPolicyRule, } from './types.js';
|
|
20
22
|
export type { AskBus, AskBatch, AskQuestion, AskOption, AskAnswer, AskResolution, AskBatchSnapshot, AskAnswerSnapshot, AskResolutionSnapshot, AskHandle, AskHandler, AskSource, AskLifecycle, AskAnswerSource, Unsubscribe as AskBusUnsubscribe, } from './ask-bus/types.js';
|
|
21
|
-
export type { Agent, AgentOptions, Message, Scorer, CheckScorer, ScoreScorer, JudgeScorer, ToolUsageScorer, ScorerContext, EvalResult, ScorerResultEntry, ScorerStatus, ChatSession, ConversationResult, ConverseOptions, Reaction, TextReaction, AskUserReaction, AskUserQuestion, AskUserOption, ReactionPreviewEntry, TextReactionPreviewEntry, AskUserReactionPreviewEntry, ReactionPreviewResult, ReactionPreviewTurn, Persona, PersonaConfig, ConversationWindowConfig, PathgradePluginOptions, PathgradeMeta, TurnTiming, TokenUsage, EvaluateOptions, ReactionPreviewStatus, } from './types.js';
|
|
23
|
+
export type { Agent, AgentOptions, Message, Scorer, CheckScorer, ScoreScorer, JudgeScorer, ToolUsageScorer, ScorerContext, EvalResult, ScorerResultEntry, ScorerStatus, ChatSession, ConversationResult, ConverseOptions, UntilPredicate, UntilContext, Reaction, TextReaction, AskUserReaction, AskUserQuestion, AskUserOption, ReactionPreviewEntry, TextReactionPreviewEntry, AskUserReactionPreviewEntry, ReactionPreviewResult, ReactionPreviewTurn, StepScorer, Persona, PersonaConfig, ConversationWindowConfig, TurnDetail, ReactionFiredEntry, PathgradePluginOptions, PathgradeMeta, TurnTiming, TokenUsage, EvaluateOptions, ReactionPreviewStatus, ScoreResult, JudgeInput, CodeJudgeToolName, ToolExpectation, SessionArtifactMatchOptions, SessionArtifactContent, SessionArtifacts, RecordedEvalResult, PathgradeTestMeta, } from './types.js';
|
|
22
24
|
export type { ConversationWindow, ConversationWindowOptions } from './conversation-window.js';
|
|
23
25
|
export type { JudgePipelineOptions } from './judge-pipeline.js';
|
|
24
26
|
export type { RunScorerOptions } from './run-scorer.js';
|
|
25
27
|
export type { OnScorerErrorMode } from './evaluate.js';
|
|
26
28
|
export type { RunSnapshot } from './snapshots.js';
|
|
29
|
+
export type { ExpectedMcpStartupStatus, ExpectedMcpToolCall, McpStartupStatusEvidence, McpToolCallEvidence, } from './mcp-evidence.js';
|
|
30
|
+
export type { McpPolicyDenialReason, McpToolCallRequest, McpToolPolicyDecision, } from './mcp-safety.js';
|
|
31
|
+
export type { ToolEvent } from '../tool-events.js';
|
|
27
32
|
export type { LLMPort, EvalRuntime } from './eval-runtime.js';
|
|
28
33
|
export { createLLMClient, ProviderNotSupportedError } from '../utils/llm.js';
|
|
29
34
|
export type { CreateLLMClientOptions, LLMProviderAdapter, TokenUsage as LLMTokenUsage } from '../utils/llm.js';
|
package/dist/sdk/index.js
CHANGED
|
@@ -3,6 +3,8 @@ export { createAgent } from './agent.js';
|
|
|
3
3
|
export { resolveAgentName, resolveCodexTransport, InvalidTransportEnvError, } from './agent-resolution.js';
|
|
4
4
|
export { AgentCrashError } from './agent-crash.js';
|
|
5
5
|
export { check, score, judge, toolUsage } from './scorers.js';
|
|
6
|
+
export { getMcpToolCall, isMcpToolCall, findMcpToolCalls, getMcpStartupStatus, isMcpStartupStatus, } from './mcp-evidence.js';
|
|
7
|
+
export { decideMcpToolCall, redactMcpSecrets, } from './mcp-safety.js';
|
|
6
8
|
export { evaluate, EvalScorerError } from './evaluate.js';
|
|
7
9
|
export { RUN_SNAPSHOT_VERSION, buildRunSnapshot, loadRunSnapshot, SnapshotParseError, SnapshotVersionError, WorkspaceMissingError, } from './snapshots.js';
|
|
8
10
|
export { createPersona } from './persona.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentTurnResult, LogEntry } from '../types.js';
|
|
2
2
|
import type { Workspace } from '../providers/workspace.js';
|
|
3
3
|
import type { AgentName, AgentTransport, Message } from './types.js';
|
|
4
|
+
import type { McpSafetyOptions } from './mcp-safety.js';
|
|
4
5
|
import type { LLMPort } from '../utils/llm-types.js';
|
|
5
6
|
import type { AskBus } from './ask-bus/types.js';
|
|
6
7
|
export interface ManagedSessionDeps {
|
|
@@ -28,6 +29,8 @@ export interface ManagedSessionDeps {
|
|
|
28
29
|
* driver capabilities fall back to the default (`exec` semantics).
|
|
29
30
|
*/
|
|
30
31
|
transport?: AgentTransport;
|
|
32
|
+
/** Live MCP Server Safety policy to enforce in supported harnesses. */
|
|
33
|
+
mcpSafety?: McpSafetyOptions;
|
|
31
34
|
}
|
|
32
35
|
export interface ManagedSession {
|
|
33
36
|
/** Full lifecycle: log start/result, push messages, check exit code. */
|
|
@@ -23,6 +23,8 @@ export function createManagedSession(deps) {
|
|
|
23
23
|
...(llm ? { llm } : {}),
|
|
24
24
|
askBus,
|
|
25
25
|
...(transport !== undefined ? { transport } : {}),
|
|
26
|
+
...(deps.mcpSafety !== undefined ? { mcpSafety: deps.mcpSafety } : {}),
|
|
27
|
+
getAbortSignal: () => currentSignal,
|
|
26
28
|
};
|
|
27
29
|
let session = null;
|
|
28
30
|
let setupDone = false;
|
|
@@ -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';
|
|
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
|
});
|