@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
@@ -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 SDK cannot enforce the built-in tool allowlist on
1920
- // cloud agents yet, and a restriction that silently does not apply is
1921
- // worse than a refused turn. Cloud-runtime agents already fail
1922
- // discovery; this guards hybrid agents' per-send cloud sessions.
1923
- if (runtime === "cloud" && agent.allowedTools !== undefined) {
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 cloud turns yet. Refusing the cloud session rather than running it unrestricted.`
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 MCP config. A turn
1931
- // that silently runs without its per-session tools or identity is worse
1932
- // than a refused one. Cloud-runtime agents already fail discovery; this
1933
- // guards hybrid agents' per-send cloud sessions.
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 === "cloud" && perSessionConnection !== undefined) {
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 cloud session rather than running it without the connection's tools or identity.`
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: "local" | "cloud",
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 === "answer") {
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
- }): { answered: true; result: SDKCustomToolResult } | { answered: false } {
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
- const record = this.activeToolContexts.get(args.sessionId)?.record;
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: record?.dryRun === true,
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