@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
|
@@ -1370,22 +1370,23 @@ export class SessionEngine {
|
|
|
1370
1370
|
agentRuntime: agent.runtime,
|
|
1371
1371
|
cloudOverride: record.cloudOverride,
|
|
1372
1372
|
});
|
|
1373
|
-
// Fail closed: the
|
|
1374
|
-
//
|
|
1375
|
-
//
|
|
1376
|
-
//
|
|
1377
|
-
if (runtime
|
|
1378
|
-
throw new Error(`Agent "${agent.name}" declares a built-in tool allowlist (tools), which cannot be enforced on
|
|
1373
|
+
// Fail closed: only the local harness can enforce the built-in tool
|
|
1374
|
+
// allowlist, and a restriction that silently does not apply is worse
|
|
1375
|
+
// than a refused turn. Non-local agents already fail discovery; this
|
|
1376
|
+
// guards hybrid agents' per-send cloud sessions.
|
|
1377
|
+
if (runtime !== "local" && agent.allowedTools !== undefined) {
|
|
1378
|
+
throw new Error(`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.`);
|
|
1379
1379
|
}
|
|
1380
1380
|
// Same fail-closed posture for per-session connection features:
|
|
1381
1381
|
// advertised tools are server tools (in-process, local turns only) and
|
|
1382
|
-
// per-session auth headers cannot reach a cloud VM's
|
|
1383
|
-
// that silently runs without its
|
|
1384
|
-
// than a refused one.
|
|
1385
|
-
// guards hybrid agents' per-send
|
|
1382
|
+
// per-session auth headers cannot reach a cloud VM's (or the Grok Bot
|
|
1383
|
+
// harness's) MCP config. A turn that silently runs without its
|
|
1384
|
+
// per-session tools or identity is worse than a refused one. Non-local
|
|
1385
|
+
// agents already fail discovery; this guards hybrid agents' per-send
|
|
1386
|
+
// cloud sessions.
|
|
1386
1387
|
const perSessionConnection = agent.connections.find((connection) => connection.advertiseTools === true || connection.auth !== undefined);
|
|
1387
|
-
if (runtime
|
|
1388
|
-
throw new Error(`Agent "${agent.name}" connection "${perSessionConnection.name}" uses advertiseTools/auth, which only run on local turns. Refusing the
|
|
1388
|
+
if (runtime !== "local" && perSessionConnection !== undefined) {
|
|
1389
|
+
throw new Error(`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.`);
|
|
1389
1390
|
}
|
|
1390
1391
|
const mergedCloud = runtime === "cloud"
|
|
1391
1392
|
? mergeCloudOptions({
|
|
@@ -1432,21 +1433,31 @@ export class SessionEngine {
|
|
|
1432
1433
|
: "";
|
|
1433
1434
|
turnPrompt = `${preamble}${memorySection}\n\n${turnPrompt}`;
|
|
1434
1435
|
}
|
|
1435
|
-
else if (instructionsInvisibleToHarness) {
|
|
1436
|
+
else if (runtime === "local" && instructionsInvisibleToHarness) {
|
|
1436
1437
|
// Local workspace whose AGENTS.md the harness will not load. With a
|
|
1437
1438
|
// borrowed harness cwd, agent tool scripts live only in the durable
|
|
1438
1439
|
// scaffold dir — embed bodies like cloud. Otherwise the catalog can
|
|
1439
|
-
// reference on-disk paths under the harness cwd.
|
|
1440
|
+
// reference on-disk paths under the harness cwd. Grokbot turns are
|
|
1441
|
+
// excluded: their instructions ride the request's `instructions`
|
|
1442
|
+
// field into the Sand system prompt instead.
|
|
1440
1443
|
const includeScripts = this.hasBorrowedWorkspace(record);
|
|
1441
1444
|
const preamble = buildAgentsMdContent(agent, { includeScripts });
|
|
1442
1445
|
turnPrompt = `${preamble}\n\n${turnPrompt}`;
|
|
1443
1446
|
}
|
|
1444
1447
|
}
|
|
1445
|
-
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ sessionId: record.sessionId }, (record.sdkAgentId === undefined
|
|
1448
|
+
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ sessionId: record.sessionId }, (record.sdkAgentId === undefined
|
|
1446
1449
|
? {}
|
|
1447
1450
|
: { sdkAgentId: record.sdkAgentId })), { workspaceDir: this.harnessCwd(record), sandbox: ((_c = this.project.agent.local) === null || _c === void 0 ? void 0 : _c.sandbox) === true }), (runtime === "local" && agent.allowedTools !== undefined
|
|
1448
1451
|
? { tools: agent.allowedTools }
|
|
1449
|
-
: {})), { runnerStateDir: join(this.stateRoot, "runner"), prompt: turnPrompt, isFirstTurn }), (images !== undefined && images.length > 0 ? { images } : {})), (agent.model === undefined ? {} : { model: agent.model })), { runtime }), (mergedCloud === undefined ? {} : { cloud: mergedCloud })),
|
|
1452
|
+
: {})), { runnerStateDir: join(this.stateRoot, "runner"), prompt: turnPrompt, isFirstTurn }), (images !== undefined && images.length > 0 ? { images } : {})), (agent.model === undefined ? {} : { model: agent.model })), { runtime, agentName: agent.name }), (mergedCloud === undefined ? {} : { cloud: mergedCloud })), {
|
|
1453
|
+
// The hosted Grok Bot harness reads no workspace AGENTS.md: grokbot
|
|
1454
|
+
// turns carry the composed instructions into the hosted session,
|
|
1455
|
+
// which surfaces them in the Sand system prompt. The box never sees
|
|
1456
|
+
// the local session workspace, so agent-tool script bodies are
|
|
1457
|
+
// always embedded — same rule as the cloud first-prompt preamble.
|
|
1458
|
+
instructions: runtime === "grokbot"
|
|
1459
|
+
? buildAgentsMdContent(agent, { includeScripts: true })
|
|
1460
|
+
: undefined }), (runtime === "cloud" && !isCursorHostedFilesAvailable()
|
|
1450
1461
|
? { agentServeAgent: this.project.name }
|
|
1451
1462
|
: {})), { customTools: runtime === "local"
|
|
1452
1463
|
? yield this.buildCustomTools(record, agent, turnId, emit)
|
|
@@ -1537,6 +1548,11 @@ export class SessionEngine {
|
|
|
1537
1548
|
* being omitted or failing the turn closed.
|
|
1538
1549
|
*/
|
|
1539
1550
|
buildMcpServers(runtime, sessionId, sessionAuth) {
|
|
1551
|
+
// The Grok Bot harness manages its own MCP surface; authored
|
|
1552
|
+
// connections are refused at discovery, so there is nothing to mount.
|
|
1553
|
+
if (runtime === "grokbot") {
|
|
1554
|
+
return {};
|
|
1555
|
+
}
|
|
1540
1556
|
// Advertised connections surface as named server tools instead of an
|
|
1541
1557
|
// attached MCP server (each tool appears exactly once), so they are
|
|
1542
1558
|
// excluded here alongside hostOnly.
|
|
@@ -1677,9 +1693,11 @@ export class SessionEngine {
|
|
|
1677
1693
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1678
1694
|
const tools = {};
|
|
1679
1695
|
const register = (tool) => {
|
|
1680
|
-
tools[tool.name] = Object.assign(Object.assign({ description: tool.description, inputSchema: tool.inputSchema }, (tool.outputSchema === undefined
|
|
1696
|
+
tools[tool.name] = Object.assign(Object.assign(Object.assign({ description: tool.description, inputSchema: tool.inputSchema }, (tool.outputSchema === undefined
|
|
1681
1697
|
? {}
|
|
1682
|
-
: { outputSchema: tool.outputSchema })),
|
|
1698
|
+
: { outputSchema: tool.outputSchema })), (tool.annotations === undefined
|
|
1699
|
+
? {}
|
|
1700
|
+
: { annotations: tool.annotations })), { execute: (args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
1683
1701
|
return this.executeServerTool({
|
|
1684
1702
|
tool,
|
|
1685
1703
|
input: args,
|
|
@@ -1777,17 +1795,28 @@ export class SessionEngine {
|
|
|
1777
1795
|
try {
|
|
1778
1796
|
// Before the approval gate: a dry run is unattended, so parking a write
|
|
1779
1797
|
// for a human would hang the turn rather than answer it.
|
|
1780
|
-
const decision = decideToolCall({
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1798
|
+
const decision = decideToolCall(Object.assign(Object.assign({ dryRun: args.record.dryRun === true, effect: args.tool.definition.effect }, (args.tool.advertised === true
|
|
1799
|
+
? {
|
|
1800
|
+
mcpTool: {
|
|
1801
|
+
name: args.tool.name,
|
|
1802
|
+
annotations: args.tool.annotations,
|
|
1803
|
+
},
|
|
1804
|
+
}
|
|
1805
|
+
: {})), { dryRunResult: args.tool.definition.dryRunResult, input: validation.value }));
|
|
1786
1806
|
if (decision.kind === "answer") {
|
|
1787
1807
|
(_a = this.stubbedCallIds.get(args.record.sessionId)) === null || _a === void 0 ? void 0 : _a.add(toolCallId);
|
|
1788
1808
|
this.logger(`[session] tool ${args.tool.name} answered by host (${toolCallId})`);
|
|
1789
1809
|
return normalizeToolResult(decision.result);
|
|
1790
1810
|
}
|
|
1811
|
+
if (decision.kind === "refuse") {
|
|
1812
|
+
this.logger(`[session] tool ${args.tool.name} refused, effect unclassified (${toolCallId})`);
|
|
1813
|
+
// The coverage signal: a run reports how much of itself was runnable.
|
|
1814
|
+
args.emit({
|
|
1815
|
+
type: "action.effect_unclassified",
|
|
1816
|
+
data: { callId: toolCallId, toolName: args.tool.name },
|
|
1817
|
+
});
|
|
1818
|
+
return toolErrorResult(decision.message);
|
|
1819
|
+
}
|
|
1791
1820
|
const needsGate = yield evaluateNeedsApproval(args.tool.definition.needsApproval, validation.value);
|
|
1792
1821
|
if (needsGate) {
|
|
1793
1822
|
const decision = yield this.parkForApproval({
|
|
@@ -2009,12 +2038,9 @@ export class SessionEngine {
|
|
|
2009
2038
|
// A separate body from the model path, so it asks the same policy: a
|
|
2010
2039
|
// session-bound `POST /v1/tools/:name`, `call --session`, or
|
|
2011
2040
|
// `handle.callTool` performs a write just as readily.
|
|
2012
|
-
const decision = decideToolCall({
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
dryRunResult: tool.definition.dryRunResult,
|
|
2016
|
-
input: validation.value,
|
|
2017
|
-
});
|
|
2041
|
+
const decision = decideToolCall(Object.assign(Object.assign({ dryRun: (record === null || record === void 0 ? void 0 : record.dryRun) === true, effect: tool.definition.effect }, (tool.advertised === true
|
|
2042
|
+
? { mcpTool: { name: tool.name, annotations: tool.annotations } }
|
|
2043
|
+
: {})), { dryRunResult: tool.definition.dryRunResult, input: validation.value }));
|
|
2018
2044
|
const started = Date.now();
|
|
2019
2045
|
this.logger(`[session] tool ${toolName} start (direct ${callId})`);
|
|
2020
2046
|
let result;
|
|
@@ -2027,7 +2053,16 @@ export class SessionEngine {
|
|
|
2027
2053
|
void this.appendEvent(live.sessionId, Object.assign(Object.assign({}, payload), { turnId: callId })).catch(() => { });
|
|
2028
2054
|
};
|
|
2029
2055
|
try {
|
|
2030
|
-
if (decision.kind === "
|
|
2056
|
+
if (decision.kind === "refuse") {
|
|
2057
|
+
this.logger(`[session] tool ${toolName} refused, effect unclassified (direct ${callId})`);
|
|
2058
|
+
emit === null || emit === void 0 ? void 0 : emit({
|
|
2059
|
+
type: "action.effect_unclassified",
|
|
2060
|
+
data: { callId, toolName },
|
|
2061
|
+
});
|
|
2062
|
+
result = normalizeToolResult(toolErrorResult(decision.message));
|
|
2063
|
+
isError = true;
|
|
2064
|
+
}
|
|
2065
|
+
else if (decision.kind === "answer") {
|
|
2031
2066
|
this.logger(`[session] tool ${toolName} answered by host (direct ${callId})`);
|
|
2032
2067
|
result = normalizeToolResult(decision.result);
|
|
2033
2068
|
isError = false;
|
|
@@ -2103,21 +2138,61 @@ export class SessionEngine {
|
|
|
2103
2138
|
* next unit's work.
|
|
2104
2139
|
*/
|
|
2105
2140
|
bridgedCallAnswer(args) {
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2141
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2142
|
+
// Read off the in-flight turn's context rather than the session store:
|
|
2143
|
+
// the bridge already refuses a call with no active turn, so the record is
|
|
2144
|
+
// in memory. That keeps this free for the sessions that are not dry runs,
|
|
2145
|
+
// which is every session today — the listing below is only paid inside a
|
|
2146
|
+
// dry run.
|
|
2147
|
+
const context = this.activeToolContexts.get(args.sessionId);
|
|
2148
|
+
if ((context === null || context === void 0 ? void 0 : context.record.dryRun) !== true) {
|
|
2149
|
+
return { answered: false };
|
|
2150
|
+
}
|
|
2151
|
+
// A bridged tool carries no `defineTool`, so its effect comes from the
|
|
2152
|
+
// server's own listing annotations; a tool the listing cannot classify
|
|
2153
|
+
// refuses rather than being stubbed. A failed listing classifies nothing,
|
|
2154
|
+
// which lands on the same refusal — never on a run.
|
|
2155
|
+
const annotations = yield this.bridgedToolAnnotations(args.connectionName, args.toolName);
|
|
2156
|
+
const decision = decideToolCall({
|
|
2157
|
+
dryRun: true,
|
|
2158
|
+
effect: undefined,
|
|
2159
|
+
mcpTool: { name: args.toolName, annotations },
|
|
2160
|
+
});
|
|
2161
|
+
if (decision.kind === "run") {
|
|
2162
|
+
return { answered: false };
|
|
2163
|
+
}
|
|
2164
|
+
if (decision.kind === "refuse") {
|
|
2165
|
+
this.logger(`[session] bridged tool ${args.toolName} refused, effect unclassified (${args.sessionId})`);
|
|
2166
|
+
context.emit({
|
|
2167
|
+
type: "action.effect_unclassified",
|
|
2168
|
+
data: {
|
|
2169
|
+
callId: newToolCallId(args.toolName),
|
|
2170
|
+
toolName: args.toolName,
|
|
2171
|
+
connection: args.connectionName,
|
|
2172
|
+
},
|
|
2173
|
+
});
|
|
2174
|
+
return { answered: true, result: toolErrorResult(decision.message) };
|
|
2175
|
+
}
|
|
2176
|
+
this.logger(`[session] bridged tool ${args.toolName} answered by host (${args.sessionId})`);
|
|
2177
|
+
return { answered: true, result: normalizeToolResult(decision.result) };
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* The bounded annotations a connection's listing declares for one tool, or
|
|
2182
|
+
* undefined when the tool is unlisted, unannotated, or the listing fails —
|
|
2183
|
+
* all of which the policy reads as unclassified, the refusing state.
|
|
2184
|
+
*/
|
|
2185
|
+
bridgedToolAnnotations(connectionName, toolName) {
|
|
2186
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2187
|
+
var _a;
|
|
2188
|
+
try {
|
|
2189
|
+
const listed = yield this.host.mcp.listTools(connectionName);
|
|
2190
|
+
return (_a = listed.find((tool) => tool.name === toolName)) === null || _a === void 0 ? void 0 : _a.annotations;
|
|
2191
|
+
}
|
|
2192
|
+
catch (_b) {
|
|
2193
|
+
return undefined;
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2121
2196
|
}
|
|
2122
2197
|
// ==========================================================================
|
|
2123
2198
|
// Events: append, dispatch to channel handlers + hooks
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* decision lives here, in one pure function, and every path asks it rather
|
|
9
9
|
* than re-implementing it.
|
|
10
10
|
*/
|
|
11
|
-
import type { ToolEffect, ToolEffectDeclaration, ToolExecuteResult } from "../types.js";
|
|
11
|
+
import type { McpToolAnnotations, ToolEffect, ToolEffectDeclaration, ToolExecuteResult } from "../types.js";
|
|
12
12
|
/** What the host does with one tool call. */
|
|
13
13
|
export type ToolDecision =
|
|
14
14
|
/** Run the tool body. */
|
|
@@ -19,6 +19,16 @@ export type ToolDecision =
|
|
|
19
19
|
| {
|
|
20
20
|
kind: "answer";
|
|
21
21
|
result: ToolExecuteResult;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Do not run it; fail the call with this message. Only ever produced in a
|
|
25
|
+
* dry run, for an MCP tool nothing classifies: stubbing it would let the
|
|
26
|
+
* run reach a confident conclusion from no data, and running it could leak
|
|
27
|
+
* a real write — refusing is the one failure that is loud.
|
|
28
|
+
*/
|
|
29
|
+
| {
|
|
30
|
+
kind: "refuse";
|
|
31
|
+
message: string;
|
|
22
32
|
};
|
|
23
33
|
/** What a policy decision needs to know about the call. */
|
|
24
34
|
export interface ToolCallFacts {
|
|
@@ -26,6 +36,17 @@ export interface ToolCallFacts {
|
|
|
26
36
|
dryRun: boolean;
|
|
27
37
|
/** The tool's declaration, if it made one. */
|
|
28
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?: {
|
|
47
|
+
name: string;
|
|
48
|
+
annotations?: McpToolAnnotations;
|
|
49
|
+
};
|
|
29
50
|
/** What a stubbed write should answer with, if the tool supplied one. */
|
|
30
51
|
dryRunResult?: ToolExecuteResult | ((input: never) => ToolExecuteResult);
|
|
31
52
|
/**
|
|
@@ -45,6 +66,17 @@ export interface ToolCallFacts {
|
|
|
45
66
|
* rather than the one that leaks a real write.
|
|
46
67
|
*/
|
|
47
68
|
export declare function resolveEffect(effect: ToolEffectDeclaration<never> | undefined, input: unknown): ToolEffect;
|
|
69
|
+
/**
|
|
70
|
+
* The effect an MCP server's own annotations declare, fail-closed.
|
|
71
|
+
*
|
|
72
|
+
* `readOnlyHint: true` without `destructiveHint: true` is a read; either
|
|
73
|
+
* negative signal is a write; anything else — absent annotations, or
|
|
74
|
+
* annotations that carry no effect signal (a bare `title`) — is `undefined`,
|
|
75
|
+
* unclassified. Mirrors `@anysphere/mcp-core/mcp-tool-annotations`
|
|
76
|
+
* (`classifyMcpToolAnnotationRisk`), which this package cannot import (npm
|
|
77
|
+
* publish, workspace-only dependency); keep the two in sync.
|
|
78
|
+
*/
|
|
79
|
+
export declare function classifyAnnotationsEffect(annotations: McpToolAnnotations | undefined): ToolEffect | undefined;
|
|
48
80
|
/** Decide what to do with one tool call. Pure; no I/O, no session state. */
|
|
49
81
|
export declare function decideToolCall(facts: ToolCallFacts): ToolDecision;
|
|
50
82
|
//# sourceMappingURL=tool-policy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-policy.d.ts","sourceRoot":"","sources":["../../src/internal/tool-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,qBAAqB,EACrB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAErB,6CAA6C;AAC7C,MAAM,MAAM,YAAY;AACtB,yBAAyB;AACvB;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE;AACjB,uDAAuD;GACrD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"tool-policy.d.ts","sourceRoot":"","sources":["../../src/internal/tool-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,UAAU,EACV,qBAAqB,EACrB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAErB,6CAA6C;AAC7C,MAAM,MAAM,YAAY;AACtB,yBAAyB;AACvB;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE;AACjB,uDAAuD;GACrD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE;AAC/C;;;;;GAKG;GACD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAExC,2DAA2D;AAC3D,MAAM,WAAW,aAAa;IAC5B,0DAA0D;IAC1D,MAAM,EAAE,OAAO,CAAC;IAChB,8CAA8C;IAC9C,MAAM,EAAE,qBAAqB,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC;IACjD;;;;;;OAMG;IACH,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,kBAAkB,CAAA;KAAE,CAAC;IAC7D,yEAAyE;IACzE,YAAY,CAAC,EAAE,iBAAiB,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,iBAAiB,CAAC,CAAC;IACzE;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAUD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,qBAAqB,CAAC,KAAK,CAAC,GAAG,SAAS,EAChD,KAAK,EAAE,OAAO,GACb,UAAU,CAIZ;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,kBAAkB,GAAG,SAAS,GAC1C,UAAU,GAAG,SAAS,CAiBxB;AAkBD,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,YAAY,CA2BjE"}
|
|
@@ -27,11 +27,64 @@ export function resolveEffect(effect, input) {
|
|
|
27
27
|
const declared = typeof effect === "function" ? effect(input) : effect;
|
|
28
28
|
return declared === "read" ? "read" : "write";
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* The effect an MCP server's own annotations declare, fail-closed.
|
|
32
|
+
*
|
|
33
|
+
* `readOnlyHint: true` without `destructiveHint: true` is a read; either
|
|
34
|
+
* negative signal is a write; anything else — absent annotations, or
|
|
35
|
+
* annotations that carry no effect signal (a bare `title`) — is `undefined`,
|
|
36
|
+
* unclassified. Mirrors `@anysphere/mcp-core/mcp-tool-annotations`
|
|
37
|
+
* (`classifyMcpToolAnnotationRisk`), which this package cannot import (npm
|
|
38
|
+
* publish, workspace-only dependency); keep the two in sync.
|
|
39
|
+
*/
|
|
40
|
+
export function classifyAnnotationsEffect(annotations) {
|
|
41
|
+
if (annotations === undefined) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
if (annotations.destructiveHint !== true &&
|
|
45
|
+
annotations.readOnlyHint === true) {
|
|
46
|
+
return "read";
|
|
47
|
+
}
|
|
48
|
+
if (annotations.readOnlyHint === false ||
|
|
49
|
+
annotations.destructiveHint === true) {
|
|
50
|
+
return "write";
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* What a dry run answers when nothing classifies an MCP tool. Deliberately
|
|
56
|
+
* reads as a strict API rather than disclosing the session's posture (see
|
|
57
|
+
* {@link ACKNOWLEDGED}), and names what is missing so the fix — the server
|
|
58
|
+
* annotating, or a server-side classification entry — is one step away.
|
|
59
|
+
*/
|
|
60
|
+
function unclassifiedRefusal(toolName) {
|
|
61
|
+
return (`Tool "${toolName}" is not available in this session: its MCP server ` +
|
|
62
|
+
`does not declare whether it reads or writes (readOnlyHint/` +
|
|
63
|
+
`destructiveHint), and no server-side classification exists for it, so ` +
|
|
64
|
+
`this session cannot tell whether calling it would change anything ` +
|
|
65
|
+
`outside the session. Use a tool that declares its effect.`);
|
|
66
|
+
}
|
|
30
67
|
/** Decide what to do with one tool call. Pure; no I/O, no session state. */
|
|
31
68
|
export function decideToolCall(facts) {
|
|
32
69
|
if (!facts.dryRun) {
|
|
33
70
|
return { kind: "run" };
|
|
34
71
|
}
|
|
72
|
+
if (facts.effect === undefined && facts.mcpTool !== undefined) {
|
|
73
|
+
const effect = classifyAnnotationsEffect(facts.mcpTool.annotations);
|
|
74
|
+
if (effect === undefined) {
|
|
75
|
+
return {
|
|
76
|
+
kind: "refuse",
|
|
77
|
+
message: unclassifiedRefusal(facts.mcpTool.name),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (effect === "read") {
|
|
81
|
+
return { kind: "run" };
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
kind: "answer",
|
|
85
|
+
result: dryRunAnswer(facts.dryRunResult, facts.input),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
35
88
|
if (resolveEffect(facts.effect, facts.input) === "read") {
|
|
36
89
|
return { kind: "run" };
|
|
37
90
|
}
|