@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.
Files changed (73) hide show
  1. package/dist/bin/agent-serve.js +8 -1
  2. package/dist/internal/advertise-tools.d.ts.map +1 -1
  3. package/dist/internal/advertise-tools.js +4 -2
  4. package/dist/internal/cli-ax.js +1 -1
  5. package/dist/internal/cli-deploy.d.ts +3 -2
  6. package/dist/internal/cli-deploy.d.ts.map +1 -1
  7. package/dist/internal/cli-deploy.js +84 -9
  8. package/dist/internal/cli-docs.js +11 -0
  9. package/dist/internal/cloud-merge.d.ts +3 -1
  10. package/dist/internal/cloud-merge.d.ts.map +1 -1
  11. package/dist/internal/cloud-merge.js +10 -2
  12. package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
  13. package/dist/internal/cursor/account-mcp.js +8 -4
  14. package/dist/internal/cursor/backend-client.d.ts +12 -0
  15. package/dist/internal/cursor/backend-client.d.ts.map +1 -1
  16. package/dist/internal/cursor/backend-client.js +27 -1
  17. package/dist/internal/deploy-client.d.ts +42 -1
  18. package/dist/internal/deploy-client.d.ts.map +1 -1
  19. package/dist/internal/deploy-client.js +70 -2
  20. package/dist/internal/discovery.d.ts.map +1 -1
  21. package/dist/internal/discovery.js +87 -11
  22. package/dist/internal/docs-site.d.ts +13 -2
  23. package/dist/internal/docs-site.d.ts.map +1 -1
  24. package/dist/internal/docs-site.js +76 -13
  25. package/dist/internal/grokbot/runner.d.ts +61 -0
  26. package/dist/internal/grokbot/runner.d.ts.map +1 -0
  27. package/dist/internal/grokbot/runner.js +278 -0
  28. package/dist/internal/mcp-endpoint.js +6 -3
  29. package/dist/internal/mcp-host.d.ts +14 -1
  30. package/dist/internal/mcp-host.d.ts.map +1 -1
  31. package/dist/internal/mcp-host.js +41 -2
  32. package/dist/internal/runtime-dispatch-runner.d.ts +30 -0
  33. package/dist/internal/runtime-dispatch-runner.d.ts.map +1 -0
  34. package/dist/internal/runtime-dispatch-runner.js +60 -0
  35. package/dist/internal/sdk-runner.d.ts.map +1 -1
  36. package/dist/internal/sdk-runner.js +7 -0
  37. package/dist/internal/server.d.ts.map +1 -1
  38. package/dist/internal/server.js +24 -1
  39. package/dist/internal/session-engine.d.ts +9 -2
  40. package/dist/internal/session-engine.d.ts.map +1 -1
  41. package/dist/internal/session-engine.js +121 -46
  42. package/dist/internal/tool-policy.d.ts +33 -1
  43. package/dist/internal/tool-policy.d.ts.map +1 -1
  44. package/dist/internal/tool-policy.js +53 -0
  45. package/dist/playground/assets/{index-Bqn91tW4.js → index-BmiIjFlM.js} +45 -45
  46. package/dist/playground/assets/index-DQGZnAI0.css +1 -0
  47. package/dist/playground/index.html +2 -2
  48. package/dist/types.d.ts +86 -2
  49. package/dist/types.d.ts.map +1 -1
  50. package/package.json +1 -1
  51. package/skills/debug/SKILL.md +1 -2
  52. package/skills/framework-map/SKILL.md +1 -2
  53. package/src/bin/agent-serve.ts +9 -1
  54. package/src/internal/advertise-tools.ts +7 -0
  55. package/src/internal/cli-ax.ts +1 -1
  56. package/src/internal/cli-deploy.ts +123 -15
  57. package/src/internal/cli-docs.ts +11 -0
  58. package/src/internal/cloud-merge.ts +12 -2
  59. package/src/internal/cursor/account-mcp.ts +15 -1
  60. package/src/internal/cursor/backend-client.ts +37 -0
  61. package/src/internal/deploy-client.ts +122 -2
  62. package/src/internal/discovery.ts +133 -13
  63. package/src/internal/docs-site.ts +83 -13
  64. package/src/internal/grokbot/runner.ts +361 -0
  65. package/src/internal/mcp-endpoint.ts +6 -1
  66. package/src/internal/mcp-host.ts +48 -0
  67. package/src/internal/runtime-dispatch-runner.ts +63 -0
  68. package/src/internal/sdk-runner.ts +9 -0
  69. package/src/internal/server.ts +32 -4
  70. package/src/internal/session-engine.ts +126 -21
  71. package/src/internal/tool-policy.ts +80 -1
  72. package/src/types.ts +84 -2
  73. 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 SDK cannot enforce the built-in tool allowlist on
1374
- // cloud agents yet, and a restriction that silently does not apply is
1375
- // worse than a refused turn. Cloud-runtime agents already fail
1376
- // discovery; this guards hybrid agents' per-send cloud sessions.
1377
- if (runtime === "cloud" && agent.allowedTools !== undefined) {
1378
- throw new Error(`Agent "${agent.name}" declares a built-in tool allowlist (tools), which cannot be enforced on cloud turns yet. Refusing the cloud session rather than running it unrestricted.`);
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 MCP config. A turn
1383
- // that silently runs without its per-session tools or identity is worse
1384
- // than a refused one. Cloud-runtime agents already fail discovery; this
1385
- // guards hybrid agents' per-send cloud sessions.
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 === "cloud" && perSessionConnection !== undefined) {
1388
- throw new Error(`Agent "${agent.name}" connection "${perSessionConnection.name}" uses advertiseTools/auth, which only run on local turns. Refusing the cloud session rather than running it without the connection's tools or identity.`);
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 })), (runtime === "cloud" && !isCursorHostedFilesAvailable()
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 })), { execute: (args, context) => __awaiter(this, void 0, void 0, function* () {
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
- dryRun: args.record.dryRun === true,
1782
- effect: args.tool.definition.effect,
1783
- dryRunResult: args.tool.definition.dryRunResult,
1784
- input: validation.value,
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
- dryRun: (record === null || record === void 0 ? void 0 : record.dryRun) === true,
2014
- effect: tool.definition.effect,
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 === "answer") {
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
- var _a;
2107
- // Read off the in-flight turn's context rather than the session store:
2108
- // the bridge already refuses a call with no active turn, so the record is
2109
- // in memory. That keeps this free for the sessions that are not dry runs,
2110
- // which is every session today.
2111
- const record = (_a = this.activeToolContexts.get(args.sessionId)) === null || _a === void 0 ? void 0 : _a.record;
2112
- const decision = decideToolCall({
2113
- dryRun: (record === null || record === void 0 ? void 0 : record.dryRun) === true,
2114
- effect: undefined,
2115
- });
2116
- if (decision.kind === "run") {
2117
- return { answered: false };
2118
- }
2119
- this.logger(`[session] bridged tool ${args.toolName} answered by host (${args.sessionId})`);
2120
- return { answered: true, result: normalizeToolResult(decision.result) };
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;AAElD,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,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,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,YAAY,CAWjE"}
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
  }