@cursor/july 0.1.87 → 0.1.89
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/bin/agent-serve.js +8 -1
- package/dist/internal/advertise-tools.d.ts.map +1 -1
- package/dist/internal/advertise-tools.js +4 -2
- package/dist/internal/cli-ax.js +1 -1
- package/dist/internal/cli-deploy.d.ts +3 -2
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +84 -9
- package/dist/internal/cli-docs.js +11 -0
- package/dist/internal/cloud-merge.d.ts +3 -1
- package/dist/internal/cloud-merge.d.ts.map +1 -1
- package/dist/internal/cloud-merge.js +10 -2
- package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
- package/dist/internal/cursor/account-mcp.js +8 -4
- package/dist/internal/cursor/backend-client.d.ts +12 -0
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +27 -1
- package/dist/internal/deploy-client.d.ts +42 -1
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +70 -2
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +87 -11
- package/dist/internal/docs-site.d.ts +13 -2
- package/dist/internal/docs-site.d.ts.map +1 -1
- package/dist/internal/docs-site.js +76 -13
- package/dist/internal/grokbot/runner.d.ts +61 -0
- package/dist/internal/grokbot/runner.d.ts.map +1 -0
- package/dist/internal/grokbot/runner.js +278 -0
- package/dist/internal/mcp-endpoint.js +6 -3
- package/dist/internal/mcp-host.d.ts +14 -1
- package/dist/internal/mcp-host.d.ts.map +1 -1
- package/dist/internal/mcp-host.js +41 -2
- package/dist/internal/runtime-dispatch-runner.d.ts +30 -0
- package/dist/internal/runtime-dispatch-runner.d.ts.map +1 -0
- package/dist/internal/runtime-dispatch-runner.js +60 -0
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +7 -0
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +24 -1
- package/dist/internal/session-engine.d.ts +9 -2
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +121 -46
- package/dist/internal/tool-policy.d.ts +33 -1
- package/dist/internal/tool-policy.d.ts.map +1 -1
- package/dist/internal/tool-policy.js +53 -0
- package/dist/playground/assets/{index-Bqn91tW4.js → index-BmiIjFlM.js} +45 -45
- package/dist/playground/assets/index-DQGZnAI0.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/types.d.ts +86 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/debug/SKILL.md +1 -2
- package/skills/framework-map/SKILL.md +1 -2
- package/src/bin/agent-serve.ts +9 -1
- package/src/internal/advertise-tools.ts +7 -0
- package/src/internal/cli-ax.ts +1 -1
- package/src/internal/cli-deploy.ts +123 -15
- package/src/internal/cli-docs.ts +11 -0
- package/src/internal/cloud-merge.ts +12 -2
- package/src/internal/cursor/account-mcp.ts +15 -1
- package/src/internal/cursor/backend-client.ts +37 -0
- package/src/internal/deploy-client.ts +122 -2
- package/src/internal/discovery.ts +133 -13
- package/src/internal/docs-site.ts +83 -13
- package/src/internal/grokbot/runner.ts +361 -0
- package/src/internal/mcp-endpoint.ts +6 -1
- package/src/internal/mcp-host.ts +48 -0
- package/src/internal/runtime-dispatch-runner.ts +63 -0
- package/src/internal/sdk-runner.ts +9 -0
- package/src/internal/server.ts +32 -4
- package/src/internal/session-engine.ts +126 -21
- package/src/internal/tool-policy.ts +80 -1
- package/src/types.ts +84 -2
- package/dist/playground/assets/index-C0_5hOsf.css +0 -1
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
} from "../files-backends/cursor-hosted.js";
|
|
36
36
|
import {
|
|
37
37
|
type AgentProject,
|
|
38
|
+
type AgentRuntime,
|
|
38
39
|
type ApprovalDecision,
|
|
39
40
|
type ArtifactListFilter,
|
|
40
41
|
type ArtifactRecord,
|
|
@@ -54,6 +55,7 @@ import {
|
|
|
54
55
|
isHostOauthConnectionTransport,
|
|
55
56
|
isSymbolicConnectionTransport,
|
|
56
57
|
type JsonValue,
|
|
58
|
+
type McpToolAnnotations,
|
|
57
59
|
type PendingApproval,
|
|
58
60
|
type ReceiveInput,
|
|
59
61
|
type ResolvedAgent,
|
|
@@ -1916,28 +1918,29 @@ export class SessionEngine {
|
|
|
1916
1918
|
agentRuntime: agent.runtime,
|
|
1917
1919
|
cloudOverride: record.cloudOverride,
|
|
1918
1920
|
});
|
|
1919
|
-
// Fail closed: the
|
|
1920
|
-
//
|
|
1921
|
-
//
|
|
1922
|
-
//
|
|
1923
|
-
if (runtime
|
|
1921
|
+
// Fail closed: only the local harness can enforce the built-in tool
|
|
1922
|
+
// allowlist, and a restriction that silently does not apply is worse
|
|
1923
|
+
// than a refused turn. Non-local agents already fail discovery; this
|
|
1924
|
+
// guards hybrid agents' per-send cloud sessions.
|
|
1925
|
+
if (runtime !== "local" && agent.allowedTools !== undefined) {
|
|
1924
1926
|
throw new Error(
|
|
1925
|
-
`Agent "${agent.name}" declares a built-in tool allowlist (tools), which cannot be enforced on
|
|
1927
|
+
`Agent "${agent.name}" declares a built-in tool allowlist (tools), which cannot be enforced on ${runtime} turns. Refusing the session rather than running it unrestricted.`
|
|
1926
1928
|
);
|
|
1927
1929
|
}
|
|
1928
1930
|
// Same fail-closed posture for per-session connection features:
|
|
1929
1931
|
// advertised tools are server tools (in-process, local turns only) and
|
|
1930
|
-
// per-session auth headers cannot reach a cloud VM's
|
|
1931
|
-
// that silently runs without its
|
|
1932
|
-
// than a refused one.
|
|
1933
|
-
// guards hybrid agents' per-send
|
|
1932
|
+
// per-session auth headers cannot reach a cloud VM's (or the Grok Bot
|
|
1933
|
+
// harness's) MCP config. A turn that silently runs without its
|
|
1934
|
+
// per-session tools or identity is worse than a refused one. Non-local
|
|
1935
|
+
// agents already fail discovery; this guards hybrid agents' per-send
|
|
1936
|
+
// cloud sessions.
|
|
1934
1937
|
const perSessionConnection = agent.connections.find(
|
|
1935
1938
|
(connection) =>
|
|
1936
1939
|
connection.advertiseTools === true || connection.auth !== undefined
|
|
1937
1940
|
);
|
|
1938
|
-
if (runtime
|
|
1941
|
+
if (runtime !== "local" && perSessionConnection !== undefined) {
|
|
1939
1942
|
throw new Error(
|
|
1940
|
-
`Agent "${agent.name}" connection "${perSessionConnection.name}" uses advertiseTools/auth, which only run on local turns. Refusing the
|
|
1943
|
+
`Agent "${agent.name}" connection "${perSessionConnection.name}" uses advertiseTools/auth, which only run on local turns. Refusing the session rather than running it without the connection's tools or identity.`
|
|
1941
1944
|
);
|
|
1942
1945
|
}
|
|
1943
1946
|
const mergedCloud =
|
|
@@ -1986,11 +1989,13 @@ export class SessionEngine {
|
|
|
1986
1989
|
? `\n\n${buildCloudMemorySection(this.project.name)}`
|
|
1987
1990
|
: "";
|
|
1988
1991
|
turnPrompt = `${preamble}${memorySection}\n\n${turnPrompt}`;
|
|
1989
|
-
} else if (instructionsInvisibleToHarness) {
|
|
1992
|
+
} else if (runtime === "local" && instructionsInvisibleToHarness) {
|
|
1990
1993
|
// Local workspace whose AGENTS.md the harness will not load. With a
|
|
1991
1994
|
// borrowed harness cwd, agent tool scripts live only in the durable
|
|
1992
1995
|
// scaffold dir — embed bodies like cloud. Otherwise the catalog can
|
|
1993
|
-
// reference on-disk paths under the harness cwd.
|
|
1996
|
+
// reference on-disk paths under the harness cwd. Grokbot turns are
|
|
1997
|
+
// excluded: their instructions ride the request's `instructions`
|
|
1998
|
+
// field into the Sand system prompt instead.
|
|
1994
1999
|
const includeScripts = this.hasBorrowedWorkspace(record);
|
|
1995
2000
|
const preamble = buildAgentsMdContent(agent, { includeScripts });
|
|
1996
2001
|
turnPrompt = `${preamble}\n\n${turnPrompt}`;
|
|
@@ -2012,7 +2017,17 @@ export class SessionEngine {
|
|
|
2012
2017
|
...(images !== undefined && images.length > 0 ? { images } : {}),
|
|
2013
2018
|
...(agent.model === undefined ? {} : { model: agent.model }),
|
|
2014
2019
|
runtime,
|
|
2020
|
+
agentName: agent.name,
|
|
2015
2021
|
...(mergedCloud === undefined ? {} : { cloud: mergedCloud }),
|
|
2022
|
+
// The hosted Grok Bot harness reads no workspace AGENTS.md: grokbot
|
|
2023
|
+
// turns carry the composed instructions into the hosted session,
|
|
2024
|
+
// which surfaces them in the Sand system prompt. The box never sees
|
|
2025
|
+
// the local session workspace, so agent-tool script bodies are
|
|
2026
|
+
// always embedded — same rule as the cloud first-prompt preamble.
|
|
2027
|
+
instructions:
|
|
2028
|
+
runtime === "grokbot"
|
|
2029
|
+
? buildAgentsMdContent(agent, { includeScripts: true })
|
|
2030
|
+
: undefined,
|
|
2016
2031
|
...(runtime === "cloud" && !isCursorHostedFilesAvailable()
|
|
2017
2032
|
? { agentServeAgent: this.project.name }
|
|
2018
2033
|
: {}),
|
|
@@ -2135,10 +2150,15 @@ export class SessionEngine {
|
|
|
2135
2150
|
* being omitted or failing the turn closed.
|
|
2136
2151
|
*/
|
|
2137
2152
|
private buildMcpServers(
|
|
2138
|
-
runtime:
|
|
2153
|
+
runtime: AgentRuntime,
|
|
2139
2154
|
sessionId?: string,
|
|
2140
2155
|
sessionAuth?: ReadonlyMap<string, Record<string, string>>
|
|
2141
2156
|
): Record<string, ConnectionTransport> {
|
|
2157
|
+
// The Grok Bot harness manages its own MCP surface; authored
|
|
2158
|
+
// connections are refused at discovery, so there is nothing to mount.
|
|
2159
|
+
if (runtime === "grokbot") {
|
|
2160
|
+
return {};
|
|
2161
|
+
}
|
|
2142
2162
|
// Advertised connections surface as named server tools instead of an
|
|
2143
2163
|
// attached MCP server (each tool appears exactly once), so they are
|
|
2144
2164
|
// excluded here alongside hostOnly.
|
|
@@ -2326,6 +2346,12 @@ export class SessionEngine {
|
|
|
2326
2346
|
...(tool.outputSchema === undefined
|
|
2327
2347
|
? {}
|
|
2328
2348
|
: { outputSchema: tool.outputSchema }),
|
|
2349
|
+
// Same pinned-SDK carry for `SDKCustomTool.annotations`: an advertised
|
|
2350
|
+
// MCP passthrough's server-declared hints (bounded at listing time)
|
|
2351
|
+
// ride into the harness listing once the dependency bumps.
|
|
2352
|
+
...(tool.annotations === undefined
|
|
2353
|
+
? {}
|
|
2354
|
+
: { annotations: tool.annotations }),
|
|
2329
2355
|
execute: async (args, context) => {
|
|
2330
2356
|
return this.executeServerTool({
|
|
2331
2357
|
tool,
|
|
@@ -2455,6 +2481,14 @@ export class SessionEngine {
|
|
|
2455
2481
|
const decision = decideToolCall({
|
|
2456
2482
|
dryRun: args.record.dryRun === true,
|
|
2457
2483
|
effect: args.tool.definition.effect,
|
|
2484
|
+
...(args.tool.advertised === true
|
|
2485
|
+
? {
|
|
2486
|
+
mcpTool: {
|
|
2487
|
+
name: args.tool.name,
|
|
2488
|
+
annotations: args.tool.annotations,
|
|
2489
|
+
},
|
|
2490
|
+
}
|
|
2491
|
+
: {}),
|
|
2458
2492
|
dryRunResult: args.tool.definition.dryRunResult,
|
|
2459
2493
|
input: validation.value,
|
|
2460
2494
|
});
|
|
@@ -2465,6 +2499,17 @@ export class SessionEngine {
|
|
|
2465
2499
|
);
|
|
2466
2500
|
return normalizeToolResult(decision.result);
|
|
2467
2501
|
}
|
|
2502
|
+
if (decision.kind === "refuse") {
|
|
2503
|
+
this.logger(
|
|
2504
|
+
`[session] tool ${args.tool.name} refused, effect unclassified (${toolCallId})`
|
|
2505
|
+
);
|
|
2506
|
+
// The coverage signal: a run reports how much of itself was runnable.
|
|
2507
|
+
args.emit({
|
|
2508
|
+
type: "action.effect_unclassified",
|
|
2509
|
+
data: { callId: toolCallId, toolName: args.tool.name },
|
|
2510
|
+
});
|
|
2511
|
+
return toolErrorResult(decision.message);
|
|
2512
|
+
}
|
|
2468
2513
|
const needsGate = await evaluateNeedsApproval(
|
|
2469
2514
|
args.tool.definition.needsApproval,
|
|
2470
2515
|
validation.value
|
|
@@ -2753,6 +2798,9 @@ export class SessionEngine {
|
|
|
2753
2798
|
const decision = decideToolCall({
|
|
2754
2799
|
dryRun: record?.dryRun === true,
|
|
2755
2800
|
effect: tool.definition.effect,
|
|
2801
|
+
...(tool.advertised === true
|
|
2802
|
+
? { mcpTool: { name: tool.name, annotations: tool.annotations } }
|
|
2803
|
+
: {}),
|
|
2756
2804
|
dryRunResult: tool.definition.dryRunResult,
|
|
2757
2805
|
input: validation.value,
|
|
2758
2806
|
});
|
|
@@ -2773,7 +2821,17 @@ export class SessionEngine {
|
|
|
2773
2821
|
}).catch(() => {});
|
|
2774
2822
|
};
|
|
2775
2823
|
try {
|
|
2776
|
-
if (decision.kind === "
|
|
2824
|
+
if (decision.kind === "refuse") {
|
|
2825
|
+
this.logger(
|
|
2826
|
+
`[session] tool ${toolName} refused, effect unclassified (direct ${callId})`
|
|
2827
|
+
);
|
|
2828
|
+
emit?.({
|
|
2829
|
+
type: "action.effect_unclassified",
|
|
2830
|
+
data: { callId, toolName },
|
|
2831
|
+
});
|
|
2832
|
+
result = normalizeToolResult(toolErrorResult(decision.message));
|
|
2833
|
+
isError = true;
|
|
2834
|
+
} else if (decision.kind === "answer") {
|
|
2777
2835
|
this.logger(
|
|
2778
2836
|
`[session] tool ${toolName} answered by host (direct ${callId})`
|
|
2779
2837
|
);
|
|
@@ -2871,28 +2929,75 @@ export class SessionEngine {
|
|
|
2871
2929
|
* bridged reads too; resolving effects from a server's own listing is the
|
|
2872
2930
|
* next unit's work.
|
|
2873
2931
|
*/
|
|
2874
|
-
bridgedCallAnswer(args: {
|
|
2932
|
+
async bridgedCallAnswer(args: {
|
|
2875
2933
|
sessionId: string;
|
|
2934
|
+
connectionName: string;
|
|
2876
2935
|
toolName: string;
|
|
2877
|
-
}):
|
|
2936
|
+
}): Promise<
|
|
2937
|
+
{ answered: true; result: SDKCustomToolResult } | { answered: false }
|
|
2938
|
+
> {
|
|
2878
2939
|
// Read off the in-flight turn's context rather than the session store:
|
|
2879
2940
|
// the bridge already refuses a call with no active turn, so the record is
|
|
2880
2941
|
// in memory. That keeps this free for the sessions that are not dry runs,
|
|
2881
|
-
// which is every session today
|
|
2882
|
-
|
|
2942
|
+
// which is every session today — the listing below is only paid inside a
|
|
2943
|
+
// dry run.
|
|
2944
|
+
const context = this.activeToolContexts.get(args.sessionId);
|
|
2945
|
+
if (context?.record.dryRun !== true) {
|
|
2946
|
+
return { answered: false };
|
|
2947
|
+
}
|
|
2948
|
+
// A bridged tool carries no `defineTool`, so its effect comes from the
|
|
2949
|
+
// server's own listing annotations; a tool the listing cannot classify
|
|
2950
|
+
// refuses rather than being stubbed. A failed listing classifies nothing,
|
|
2951
|
+
// which lands on the same refusal — never on a run.
|
|
2952
|
+
const annotations = await this.bridgedToolAnnotations(
|
|
2953
|
+
args.connectionName,
|
|
2954
|
+
args.toolName
|
|
2955
|
+
);
|
|
2883
2956
|
const decision = decideToolCall({
|
|
2884
|
-
dryRun:
|
|
2957
|
+
dryRun: true,
|
|
2885
2958
|
effect: undefined,
|
|
2959
|
+
mcpTool: { name: args.toolName, annotations },
|
|
2886
2960
|
});
|
|
2887
2961
|
if (decision.kind === "run") {
|
|
2888
2962
|
return { answered: false };
|
|
2889
2963
|
}
|
|
2964
|
+
if (decision.kind === "refuse") {
|
|
2965
|
+
this.logger(
|
|
2966
|
+
`[session] bridged tool ${args.toolName} refused, effect unclassified (${args.sessionId})`
|
|
2967
|
+
);
|
|
2968
|
+
context.emit({
|
|
2969
|
+
type: "action.effect_unclassified",
|
|
2970
|
+
data: {
|
|
2971
|
+
callId: newToolCallId(args.toolName),
|
|
2972
|
+
toolName: args.toolName,
|
|
2973
|
+
connection: args.connectionName,
|
|
2974
|
+
},
|
|
2975
|
+
});
|
|
2976
|
+
return { answered: true, result: toolErrorResult(decision.message) };
|
|
2977
|
+
}
|
|
2890
2978
|
this.logger(
|
|
2891
2979
|
`[session] bridged tool ${args.toolName} answered by host (${args.sessionId})`
|
|
2892
2980
|
);
|
|
2893
2981
|
return { answered: true, result: normalizeToolResult(decision.result) };
|
|
2894
2982
|
}
|
|
2895
2983
|
|
|
2984
|
+
/**
|
|
2985
|
+
* The bounded annotations a connection's listing declares for one tool, or
|
|
2986
|
+
* undefined when the tool is unlisted, unannotated, or the listing fails —
|
|
2987
|
+
* all of which the policy reads as unclassified, the refusing state.
|
|
2988
|
+
*/
|
|
2989
|
+
private async bridgedToolAnnotations(
|
|
2990
|
+
connectionName: string,
|
|
2991
|
+
toolName: string
|
|
2992
|
+
): Promise<McpToolAnnotations | undefined> {
|
|
2993
|
+
try {
|
|
2994
|
+
const listed = await this.host.mcp.listTools(connectionName);
|
|
2995
|
+
return listed.find((tool) => tool.name === toolName)?.annotations;
|
|
2996
|
+
} catch {
|
|
2997
|
+
return undefined;
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
|
|
2896
3001
|
// ==========================================================================
|
|
2897
3002
|
// Events: append, dispatch to channel handlers + hooks
|
|
2898
3003
|
// ==========================================================================
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type {
|
|
13
|
+
McpToolAnnotations,
|
|
13
14
|
ToolEffect,
|
|
14
15
|
ToolEffectDeclaration,
|
|
15
16
|
ToolExecuteResult,
|
|
@@ -20,7 +21,14 @@ export type ToolDecision =
|
|
|
20
21
|
/** Run the tool body. */
|
|
21
22
|
| { kind: "run" }
|
|
22
23
|
/** Do not run it; return this to the model instead. */
|
|
23
|
-
| { kind: "answer"; result: ToolExecuteResult }
|
|
24
|
+
| { kind: "answer"; result: ToolExecuteResult }
|
|
25
|
+
/**
|
|
26
|
+
* Do not run it; fail the call with this message. Only ever produced in a
|
|
27
|
+
* dry run, for an MCP tool nothing classifies: stubbing it would let the
|
|
28
|
+
* run reach a confident conclusion from no data, and running it could leak
|
|
29
|
+
* a real write — refusing is the one failure that is loud.
|
|
30
|
+
*/
|
|
31
|
+
| { kind: "refuse"; message: string };
|
|
24
32
|
|
|
25
33
|
/** What a policy decision needs to know about the call. */
|
|
26
34
|
export interface ToolCallFacts {
|
|
@@ -28,6 +36,14 @@ export interface ToolCallFacts {
|
|
|
28
36
|
dryRun: boolean;
|
|
29
37
|
/** The tool's declaration, if it made one. */
|
|
30
38
|
effect: ToolEffectDeclaration<never> | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Present when the call targets a tool nobody authored — an advertised MCP
|
|
41
|
+
* passthrough or a bridged MCP call. Consulted only when {@link effect} is
|
|
42
|
+
* undeclared: the server's own annotations classify the call, and a tool
|
|
43
|
+
* they cannot classify refuses in a dry run instead of being stubbed as a
|
|
44
|
+
* write the way an undeclared authored tool is.
|
|
45
|
+
*/
|
|
46
|
+
mcpTool?: { name: string; annotations?: McpToolAnnotations };
|
|
31
47
|
/** What a stubbed write should answer with, if the tool supplied one. */
|
|
32
48
|
dryRunResult?: ToolExecuteResult | ((input: never) => ToolExecuteResult);
|
|
33
49
|
/**
|
|
@@ -64,11 +80,74 @@ export function resolveEffect(
|
|
|
64
80
|
return declared === "read" ? "read" : "write";
|
|
65
81
|
}
|
|
66
82
|
|
|
83
|
+
/**
|
|
84
|
+
* The effect an MCP server's own annotations declare, fail-closed.
|
|
85
|
+
*
|
|
86
|
+
* `readOnlyHint: true` without `destructiveHint: true` is a read; either
|
|
87
|
+
* negative signal is a write; anything else — absent annotations, or
|
|
88
|
+
* annotations that carry no effect signal (a bare `title`) — is `undefined`,
|
|
89
|
+
* unclassified. Mirrors `@anysphere/mcp-core/mcp-tool-annotations`
|
|
90
|
+
* (`classifyMcpToolAnnotationRisk`), which this package cannot import (npm
|
|
91
|
+
* publish, workspace-only dependency); keep the two in sync.
|
|
92
|
+
*/
|
|
93
|
+
export function classifyAnnotationsEffect(
|
|
94
|
+
annotations: McpToolAnnotations | undefined
|
|
95
|
+
): ToolEffect | undefined {
|
|
96
|
+
if (annotations === undefined) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
if (
|
|
100
|
+
annotations.destructiveHint !== true &&
|
|
101
|
+
annotations.readOnlyHint === true
|
|
102
|
+
) {
|
|
103
|
+
return "read";
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
annotations.readOnlyHint === false ||
|
|
107
|
+
annotations.destructiveHint === true
|
|
108
|
+
) {
|
|
109
|
+
return "write";
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* What a dry run answers when nothing classifies an MCP tool. Deliberately
|
|
116
|
+
* reads as a strict API rather than disclosing the session's posture (see
|
|
117
|
+
* {@link ACKNOWLEDGED}), and names what is missing so the fix — the server
|
|
118
|
+
* annotating, or a server-side classification entry — is one step away.
|
|
119
|
+
*/
|
|
120
|
+
function unclassifiedRefusal(toolName: string): string {
|
|
121
|
+
return (
|
|
122
|
+
`Tool "${toolName}" is not available in this session: its MCP server ` +
|
|
123
|
+
`does not declare whether it reads or writes (readOnlyHint/` +
|
|
124
|
+
`destructiveHint), and no server-side classification exists for it, so ` +
|
|
125
|
+
`this session cannot tell whether calling it would change anything ` +
|
|
126
|
+
`outside the session. Use a tool that declares its effect.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
67
130
|
/** Decide what to do with one tool call. Pure; no I/O, no session state. */
|
|
68
131
|
export function decideToolCall(facts: ToolCallFacts): ToolDecision {
|
|
69
132
|
if (!facts.dryRun) {
|
|
70
133
|
return { kind: "run" };
|
|
71
134
|
}
|
|
135
|
+
if (facts.effect === undefined && facts.mcpTool !== undefined) {
|
|
136
|
+
const effect = classifyAnnotationsEffect(facts.mcpTool.annotations);
|
|
137
|
+
if (effect === undefined) {
|
|
138
|
+
return {
|
|
139
|
+
kind: "refuse",
|
|
140
|
+
message: unclassifiedRefusal(facts.mcpTool.name),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (effect === "read") {
|
|
144
|
+
return { kind: "run" };
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
kind: "answer",
|
|
148
|
+
result: dryRunAnswer(facts.dryRunResult, facts.input),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
72
151
|
if (resolveEffect(facts.effect, facts.input) === "read") {
|
|
73
152
|
return { kind: "run" };
|
|
74
153
|
}
|
package/src/types.ts
CHANGED
|
@@ -95,9 +95,14 @@ export const DEFAULT_MODEL: ModelSetting = {
|
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
97
|
* Where turns execute. `"local"` (default) uses the Cursor SDK local
|
|
98
|
-
* harness on this machine; `"cloud"` runs on Cursor cloud agents
|
|
98
|
+
* harness on this machine; `"cloud"` runs on Cursor cloud agents;
|
|
99
|
+
* `"grokbot"` runs on Cursor's hosted Grok Bot (Sand) harness with the
|
|
100
|
+
* account's own computer (box) — the SDK is a thin client over the
|
|
101
|
+
* `/v0/grokbot` session API, and the user-visible reply is whatever the
|
|
102
|
+
* agent delivers through its SendToUser tool. The hosted harness picks
|
|
103
|
+
* the model; {@link AgentConfig.model} does not apply to grokbot turns.
|
|
99
104
|
*/
|
|
100
|
-
export type AgentRuntime = "local" | "cloud";
|
|
105
|
+
export type AgentRuntime = "local" | "cloud" | "grokbot";
|
|
101
106
|
|
|
102
107
|
/**
|
|
103
108
|
* Cloud-only options forwarded to the Cursor SDK's `cloud` field on
|
|
@@ -473,11 +478,39 @@ export interface HostMcpRegistry {
|
|
|
473
478
|
): Promise<HostMcpCallResult>;
|
|
474
479
|
}
|
|
475
480
|
|
|
481
|
+
/**
|
|
482
|
+
* MCP spec tool annotations (`Tool.annotations`, 2025-03-26 revision).
|
|
483
|
+
*
|
|
484
|
+
* A closed type on purpose: these are exactly the fields listing-time
|
|
485
|
+
* bounding keeps, so the type matches what a reader can ever see — and the
|
|
486
|
+
* hints are behavioral, so a misspelled or wrong-typed key must not
|
|
487
|
+
* masquerade as a declaration.
|
|
488
|
+
*/
|
|
489
|
+
export interface McpToolAnnotations {
|
|
490
|
+
/** Human-readable display name for the tool, length-capped at listing. */
|
|
491
|
+
title?: string;
|
|
492
|
+
/** The tool does not modify its environment. */
|
|
493
|
+
readOnlyHint?: boolean;
|
|
494
|
+
/** The tool may perform destructive updates (meaningful when not read-only). */
|
|
495
|
+
destructiveHint?: boolean;
|
|
496
|
+
/** Repeated calls with the same arguments have no additional effect. */
|
|
497
|
+
idempotentHint?: boolean;
|
|
498
|
+
/** The tool may interact with an open world of external entities. */
|
|
499
|
+
openWorldHint?: boolean;
|
|
500
|
+
}
|
|
501
|
+
|
|
476
502
|
export interface HostMcpToolInfo {
|
|
477
503
|
name: string;
|
|
478
504
|
description?: string;
|
|
479
505
|
inputSchema?: JsonObject;
|
|
480
506
|
outputSchema?: JsonObject;
|
|
507
|
+
/**
|
|
508
|
+
* MCP spec tool annotations, bounded at listing time to the
|
|
509
|
+
* {@link McpToolAnnotations} fields. Untrusted server hints — consumers
|
|
510
|
+
* that classify on them must fail closed when absent, and absent is
|
|
511
|
+
* exactly what a server that declares nothing produces.
|
|
512
|
+
*/
|
|
513
|
+
annotations?: McpToolAnnotations;
|
|
481
514
|
}
|
|
482
515
|
|
|
483
516
|
export interface HostMcpCallResult {
|
|
@@ -1406,6 +1439,17 @@ export type SessionEventPayload =
|
|
|
1406
1439
|
parentCallId?: string;
|
|
1407
1440
|
};
|
|
1408
1441
|
}
|
|
1442
|
+
| {
|
|
1443
|
+
/**
|
|
1444
|
+
* A dry-run call was refused because nothing classifies the tool's
|
|
1445
|
+
* effect: the MCP server sent no readOnlyHint/destructiveHint and no
|
|
1446
|
+
* server-side classification exists for it. The per-run count of these
|
|
1447
|
+
* is the coverage signal — how much of a backtest was actually
|
|
1448
|
+
* runnable, reported rather than assumed.
|
|
1449
|
+
*/
|
|
1450
|
+
type: "action.effect_unclassified";
|
|
1451
|
+
data: { callId: string; toolName: string; connection?: string };
|
|
1452
|
+
}
|
|
1409
1453
|
| {
|
|
1410
1454
|
type: "subagent.called";
|
|
1411
1455
|
data: { callId: string; name?: string; description?: string };
|
|
@@ -2579,6 +2623,22 @@ export interface DiscoveredTool {
|
|
|
2579
2623
|
* declares nothing, which a dry run treats as a write.
|
|
2580
2624
|
*/
|
|
2581
2625
|
effect?: ToolEffect | "dynamic";
|
|
2626
|
+
/**
|
|
2627
|
+
* MCP spec tool annotations, present only on advertised MCP passthroughs
|
|
2628
|
+
* whose server declared them (see {@link HostMcpToolInfo.annotations}).
|
|
2629
|
+
* Authored tools declare {@link effect} instead. Untrusted hints,
|
|
2630
|
+
* propagated so the harness listing carries them and so effect
|
|
2631
|
+
* classification can read them; absent is the fail-closed state.
|
|
2632
|
+
*/
|
|
2633
|
+
annotations?: McpToolAnnotations;
|
|
2634
|
+
/**
|
|
2635
|
+
* True for a 1:1 MCP passthrough synthesized from an advertised
|
|
2636
|
+
* connection's listing. The policy treats these differently from authored
|
|
2637
|
+
* tools when nothing declares an effect: an undeclared authored tool is
|
|
2638
|
+
* stubbed as a write in a dry run, an unclassifiable advertised tool is
|
|
2639
|
+
* refused — see `decideToolCall`.
|
|
2640
|
+
*/
|
|
2641
|
+
advertised?: true;
|
|
2582
2642
|
/**
|
|
2583
2643
|
* Agent tools: script body materialized under
|
|
2584
2644
|
* `.agent-serve/tools/<name>.sh`.
|
|
@@ -2840,8 +2900,22 @@ export interface RunnerTurnRequest {
|
|
|
2840
2900
|
images?: Array<{ data: string; mimeType: string }>;
|
|
2841
2901
|
model?: ModelSetting;
|
|
2842
2902
|
runtime: AgentRuntime;
|
|
2903
|
+
/**
|
|
2904
|
+
* The agent's display name. Grokbot turns address the caller's hosted
|
|
2905
|
+
* Grok Bot agent of this name: every SDK session of one agent forwards
|
|
2906
|
+
* into the same hosted agent and conversation.
|
|
2907
|
+
*/
|
|
2908
|
+
agentName?: string;
|
|
2843
2909
|
/** Cloud options when {@link runtime} is `"cloud"`. */
|
|
2844
2910
|
cloud?: AgentCloudOptions;
|
|
2911
|
+
/**
|
|
2912
|
+
* Composed instructions ({@link ResolvedAgent.instructions}) for runtimes
|
|
2913
|
+
* whose harness cannot read the workspace `AGENTS.md` — grokbot turns
|
|
2914
|
+
* carry them into the hosted session so they reach the Sand system
|
|
2915
|
+
* prompt. Local turns read the materialized workspace instead and leave
|
|
2916
|
+
* this unset.
|
|
2917
|
+
*/
|
|
2918
|
+
instructions?: string;
|
|
2845
2919
|
/**
|
|
2846
2920
|
* defineAgent directory slug for personal-key cloud creates. Injected
|
|
2847
2921
|
* by the engine — not part of authored {@link AgentCloudOptions}.
|
|
@@ -2962,6 +3036,14 @@ export interface ServeOptions {
|
|
|
2962
3036
|
* SSO/Okta proxy in front of the host).
|
|
2963
3037
|
*/
|
|
2964
3038
|
allowAnonymousCursorGithub?: boolean;
|
|
3039
|
+
/**
|
|
3040
|
+
* Allow `--allow-anonymous` together with a `runtime: "grokbot"` mount.
|
|
3041
|
+
* Off by default: grokbot turns execute on the signed-in account's hosted
|
|
3042
|
+
* Grok Bot computer, so an anonymous caller could drive Shell and other
|
|
3043
|
+
* box tools there. Set it only when the network boundary already
|
|
3044
|
+
* authenticates callers (e.g. an SSO/Okta proxy in front of the host).
|
|
3045
|
+
*/
|
|
3046
|
+
allowAnonymousGrokbot?: boolean;
|
|
2965
3047
|
/**
|
|
2966
3048
|
* Route `githubChannel({ cursorAccount })` API calls through the Cursor
|
|
2967
3049
|
* backend's GitHub forwarder (`/v0/github-proxy`) instead of minting raw
|