@cursor/july 0.1.33 → 0.1.34

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 (164) hide show
  1. package/README.md +1 -1
  2. package/dist/artifacts.d.ts.map +1 -1
  3. package/dist/artifacts.js +0 -5
  4. package/dist/bin/agent-serve.d.ts +1 -0
  5. package/dist/bin/agent-serve.d.ts.map +1 -1
  6. package/dist/bin/agent-serve.js +32 -2
  7. package/dist/channels/github/types.d.ts +10 -7
  8. package/dist/channels/github/types.d.ts.map +1 -1
  9. package/dist/docs/404.html +2 -2
  10. package/dist/docs/ab.html +3 -3
  11. package/dist/docs/assets/{app.CgZqLyWL.js → app.FPupl4SP.js} +1 -1
  12. package/dist/docs/assets/chunks/@localSearchIndexroot.WoYunhnT.js +1 -0
  13. package/dist/docs/assets/chunks/{VPLocalSearchBox.C4KigoQn.js → VPLocalSearchBox.Cd182Cu0.js} +1 -1
  14. package/dist/docs/assets/chunks/{theme.C_Ty4yT7.js → theme.BEM3Okcd.js} +2 -2
  15. package/dist/docs/assets/{example-agents_weather-agent.md.Dmrcphhl.js → example-agents_weather-agent.md.BADkPqxQ.js} +6 -4
  16. package/dist/docs/assets/example-agents_weather-agent.md.BADkPqxQ.lean.js +1 -0
  17. package/dist/docs/assets/guides_cloud-runtime.md.CDJGvVC4.js +9 -0
  18. package/dist/docs/assets/guides_cloud-runtime.md.CDJGvVC4.lean.js +1 -0
  19. package/dist/docs/building-with-agents.html +3 -3
  20. package/dist/docs/concepts.html +3 -3
  21. package/dist/docs/deployment.html +3 -3
  22. package/dist/docs/evals.html +3 -3
  23. package/dist/docs/example-agents/approval-buddy.html +3 -3
  24. package/dist/docs/example-agents/benny.html +3 -3
  25. package/dist/docs/example-agents/bugbot.html +3 -3
  26. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  27. package/dist/docs/example-agents/codeowners-review.html +3 -3
  28. package/dist/docs/example-agents/concierge.html +3 -3
  29. package/dist/docs/example-agents/fsd.html +3 -3
  30. package/dist/docs/example-agents/index.html +3 -3
  31. package/dist/docs/example-agents/knowledge-base.html +3 -3
  32. package/dist/docs/example-agents/oncall.html +3 -3
  33. package/dist/docs/example-agents/security-reviewer.html +3 -3
  34. package/dist/docs/example-agents/slack-agent.html +3 -3
  35. package/dist/docs/example-agents/weather-agent.html +10 -8
  36. package/dist/docs/guides/agent-to-agent.html +3 -3
  37. package/dist/docs/guides/cloud-runtime.html +5 -5
  38. package/dist/docs/guides/github.html +3 -3
  39. package/dist/docs/guides/human-in-the-loop.html +3 -3
  40. package/dist/docs/guides/mcp-oauth.html +3 -3
  41. package/dist/docs/guides/slack.html +3 -3
  42. package/dist/docs/guides/webhooks.html +3 -3
  43. package/dist/docs/hashmap.json +1 -1
  44. package/dist/docs/hillclimbing.html +3 -3
  45. package/dist/docs/index.html +3 -3
  46. package/dist/docs/quickstart.html +3 -3
  47. package/dist/docs/reference/agent-config.html +3 -3
  48. package/dist/docs/reference/channels.html +3 -3
  49. package/dist/docs/reference/cli.html +3 -3
  50. package/dist/docs/reference/connections.html +3 -3
  51. package/dist/docs/reference/hooks.html +3 -3
  52. package/dist/docs/reference/http-api.html +3 -3
  53. package/dist/docs/reference/instructions.html +3 -3
  54. package/dist/docs/reference/playground.html +3 -3
  55. package/dist/docs/reference/project-layout.html +3 -3
  56. package/dist/docs/reference/prompt.html +3 -3
  57. package/dist/docs/reference/schedules.html +3 -3
  58. package/dist/docs/reference/sessions.html +3 -3
  59. package/dist/docs/reference/skills.html +3 -3
  60. package/dist/docs/reference/subagents.html +3 -3
  61. package/dist/docs/reference/tools.html +3 -3
  62. package/dist/docs/scaffolding-agents.html +3 -3
  63. package/dist/docs/storage.html +3 -3
  64. package/dist/docs/troubleshooting.html +3 -3
  65. package/dist/files-backends/agent-store-presigned-url.d.ts +3 -11
  66. package/dist/files.d.ts +20 -0
  67. package/dist/files.d.ts.map +1 -1
  68. package/dist/files.js +20 -0
  69. package/dist/index.d.ts +2 -2
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +2 -2
  72. package/dist/internal/artifacts-store.d.ts +14 -6
  73. package/dist/internal/artifacts-store.d.ts.map +1 -1
  74. package/dist/internal/artifacts-store.js +160 -16
  75. package/dist/internal/cli-convert-automation.d.ts +39 -0
  76. package/dist/internal/cli-convert-automation.d.ts.map +1 -0
  77. package/dist/internal/cli-convert-automation.js +280 -0
  78. package/dist/internal/cli-deploy.d.ts +2 -0
  79. package/dist/internal/cli-deploy.d.ts.map +1 -1
  80. package/dist/internal/cli-deploy.js +1 -1
  81. package/dist/internal/cli-mcp-oauth.d.ts +23 -0
  82. package/dist/internal/cli-mcp-oauth.d.ts.map +1 -1
  83. package/dist/internal/cli-mcp-oauth.js +57 -33
  84. package/dist/internal/convert-automation/convert-workflow.d.ts +10 -0
  85. package/dist/internal/convert-automation/convert-workflow.d.ts.map +1 -0
  86. package/dist/internal/convert-automation/convert-workflow.js +998 -0
  87. package/dist/internal/convert-automation/index.d.ts +3 -0
  88. package/dist/internal/convert-automation/index.d.ts.map +1 -0
  89. package/dist/internal/convert-automation/index.js +1 -0
  90. package/dist/internal/convert-automation/slug.d.ts +11 -0
  91. package/dist/internal/convert-automation/slug.d.ts.map +1 -0
  92. package/dist/internal/convert-automation/slug.js +36 -0
  93. package/dist/internal/convert-automation/types.d.ts +191 -0
  94. package/dist/internal/convert-automation/types.d.ts.map +1 -0
  95. package/dist/internal/convert-automation/types.js +13 -0
  96. package/dist/internal/deploy-client.d.ts +31 -0
  97. package/dist/internal/deploy-client.d.ts.map +1 -1
  98. package/dist/internal/deploy-client.js +48 -0
  99. package/dist/internal/discovery.d.ts.map +1 -1
  100. package/dist/internal/discovery.js +3 -32
  101. package/dist/internal/init-project.d.ts +1 -0
  102. package/dist/internal/init-project.d.ts.map +1 -1
  103. package/dist/internal/init-project.js +1 -1
  104. package/dist/internal/mcp-endpoint.d.ts +6 -0
  105. package/dist/internal/mcp-endpoint.d.ts.map +1 -1
  106. package/dist/internal/mcp-endpoint.js +168 -0
  107. package/dist/internal/resolved-connections.d.ts +15 -0
  108. package/dist/internal/resolved-connections.d.ts.map +1 -1
  109. package/dist/internal/resolved-connections.js +12 -5
  110. package/dist/internal/server.d.ts.map +1 -1
  111. package/dist/internal/server.js +75 -7
  112. package/dist/internal/session-engine.d.ts +56 -3
  113. package/dist/internal/session-engine.d.ts.map +1 -1
  114. package/dist/internal/session-engine.js +252 -83
  115. package/dist/internal/storage-coordinator.d.ts +0 -15
  116. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  117. package/dist/internal/storage-coordinator.js +0 -52
  118. package/dist/internal/workspace.d.ts +10 -5
  119. package/dist/internal/workspace.d.ts.map +1 -1
  120. package/dist/internal/workspace.js +13 -0
  121. package/dist/memory.d.ts +22 -6
  122. package/dist/memory.d.ts.map +1 -1
  123. package/dist/memory.js +100 -8
  124. package/dist/playground/assets/{index-CVlCAcUx.js → index-CDDWw0YX.js} +46 -46
  125. package/dist/playground/assets/index-MVuNTd8v.css +1 -0
  126. package/dist/playground/index.html +2 -2
  127. package/dist/storage.d.ts +0 -4
  128. package/dist/storage.d.ts.map +1 -1
  129. package/dist/storage.js +0 -4
  130. package/dist/types.d.ts +18 -0
  131. package/dist/types.d.ts.map +1 -1
  132. package/docs/example-agents/weather-agent.md +29 -11
  133. package/docs/guides/cloud-runtime.md +6 -4
  134. package/package.json +3 -1
  135. package/src/artifacts.ts +0 -6
  136. package/src/bin/agent-serve.ts +36 -2
  137. package/src/channels/github/types.ts +10 -7
  138. package/src/files.ts +27 -0
  139. package/src/index.ts +6 -1
  140. package/src/internal/artifacts-store.ts +185 -22
  141. package/src/internal/cli-convert-automation.ts +352 -0
  142. package/src/internal/cli-deploy.ts +1 -1
  143. package/src/internal/cli-mcp-oauth.ts +69 -31
  144. package/src/internal/convert-automation/convert-workflow.ts +1422 -0
  145. package/src/internal/convert-automation/index.ts +10 -0
  146. package/src/internal/convert-automation/slug.ts +38 -0
  147. package/src/internal/convert-automation/types.ts +199 -0
  148. package/src/internal/deploy-client.ts +78 -0
  149. package/src/internal/discovery.ts +3 -37
  150. package/src/internal/init-project.ts +1 -1
  151. package/src/internal/mcp-endpoint.ts +243 -0
  152. package/src/internal/resolved-connections.ts +32 -6
  153. package/src/internal/server.ts +99 -1
  154. package/src/internal/session-engine.ts +329 -99
  155. package/src/internal/storage-coordinator.ts +0 -62
  156. package/src/internal/workspace.ts +19 -5
  157. package/src/memory.ts +126 -10
  158. package/src/storage.ts +0 -6
  159. package/src/types.ts +15 -0
  160. package/dist/docs/assets/chunks/@localSearchIndexroot.B1uHGxBZ.js +0 -1
  161. package/dist/docs/assets/example-agents_weather-agent.md.Dmrcphhl.lean.js +0 -1
  162. package/dist/docs/assets/guides_cloud-runtime.md.BZ2GA7Es.js +0 -9
  163. package/dist/docs/assets/guides_cloud-runtime.md.BZ2GA7Es.lean.js +0 -1
  164. package/dist/playground/assets/index-BRhBbnd4.css +0 -1
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { mkdir, rm, writeFile } from "node:fs/promises";
11
11
  import { dirname, join } from "node:path";
12
- import type { SDKCustomTool } from "@cursor/sdk";
12
+ import type { SDKCustomTool, SDKCustomToolResult } from "@cursor/sdk";
13
13
  import {
14
14
  type ABDefinition,
15
15
  mergeABInstructions,
@@ -22,6 +22,7 @@ import {
22
22
  SlackNudgeStore,
23
23
  } from "../channels/slack/nudge-store.js";
24
24
  import { resolveDedicatedSlackHostBinding } from "../channels/slack/slack-channel.js";
25
+ import { isCursorHostedFilesAvailable } from "../files-backends/cursor-hosted.js";
25
26
  import {
26
27
  type AgentProject,
27
28
  type ApprovalDecision,
@@ -111,6 +112,7 @@ import {
111
112
  import { TurnGovernor } from "./turn-governor.js";
112
113
  import {
113
114
  buildAgentsMdContent,
115
+ buildCloudMemorySection,
114
116
  isNestedInGitRepo,
115
117
  materializeWorkspace,
116
118
  writeWorkspaceFiles,
@@ -237,6 +239,12 @@ export interface EngineResolvedConnection {
237
239
  accountServersFilter?: readonly string[];
238
240
  }
239
241
 
242
+ interface ActiveToolContext {
243
+ record: SessionRecord;
244
+ turnId: string;
245
+ emit: (payload: SessionEventPayload) => void;
246
+ }
247
+
240
248
  export interface EngineSendOptions extends SendMessageOptions {
241
249
  mode?: "chat" | "task";
242
250
  }
@@ -286,6 +294,8 @@ export class SessionEngine {
286
294
  /** Sessions with a turn being admitted (before the run promise exists). */
287
295
  private readonly pendingTurns = new Set<string>();
288
296
  private readonly activeTurns = new Map<string, Promise<TurnOutcome>>();
297
+ /** Context used by the cloud turn's HTTP MCP callbacks. */
298
+ private readonly activeToolContexts = new Map<string, ActiveToolContext>();
289
299
  /** Aborts the in-flight turn per session so a follow-up can preempt it. */
290
300
  private readonly turnAborts = new Map<string, AbortController>();
291
301
  /** Serializes turn admission per session (latest follow-up wins). */
@@ -318,6 +328,8 @@ export class SessionEngine {
318
328
  private readonly artifactsStore: ArtifactsStore;
319
329
  /** Serve-time resolved connections (see {@link setResolvedConnections}). */
320
330
  private resolvedConnections: EngineResolvedConnection[] = [];
331
+ /** Direct server-tool HTTP MCP transport exposed only to cloud turns. */
332
+ private cloudServerToolsConnection: ConnectionTransport | undefined;
321
333
  private readonly getCloudUsage: (
322
334
  sdkAgentId: string
323
335
  ) => Promise<CloudAgentUsage | undefined>;
@@ -369,7 +381,7 @@ export class SessionEngine {
369
381
  });
370
382
  this.artifactsStore = new ArtifactsStore({
371
383
  stateRoot: options.stateRoot,
372
- storage: this.storage,
384
+ agentName: options.project.name,
373
385
  config: options.project.artifacts,
374
386
  });
375
387
  this.abCollector = new ABCollector(
@@ -427,6 +439,10 @@ export class SessionEngine {
427
439
  }
428
440
  }
429
441
 
442
+ setCloudServerToolsConnection(connection: ConnectionTransport): void {
443
+ this.cloudServerToolsConnection = connection;
444
+ }
445
+
430
446
  /** Shared host services (MCP / GitHub / Slack / reminders / evals / kv / Slack nudges). */
431
447
  get host(): HostContext {
432
448
  return {
@@ -1317,7 +1333,12 @@ export class SessionEngine {
1317
1333
  },
1318
1334
  signal
1319
1335
  );
1320
- outcome = await this.runner.runTurn(request);
1336
+ this.activeToolContexts.set(sessionId, { record, turnId, emit });
1337
+ try {
1338
+ outcome = await this.runner.runTurn(request);
1339
+ } finally {
1340
+ this.activeToolContexts.delete(sessionId);
1341
+ }
1321
1342
  } catch (error) {
1322
1343
  this.approvals.cancelSession(sessionId, "deny");
1323
1344
  outcome = { status: "error", errorMessage: describeError(error) };
@@ -1441,9 +1462,13 @@ export class SessionEngine {
1441
1462
  if (isFirstTurn) {
1442
1463
  if (runtime === "cloud") {
1443
1464
  // Cloud VMs have no materialized workspace: ship the full context,
1444
- // including agent tool script bodies.
1465
+ // including agent tool script bodies. On hosted deployments the
1466
+ // shared memory journal is on the store mount — say where.
1445
1467
  const preamble = buildAgentsMdContent(agent, { includeScripts: true });
1446
- turnPrompt = `${preamble}\n\n${turnPrompt}`;
1468
+ const memorySection = isCursorHostedFilesAvailable()
1469
+ ? `\n\n${buildCloudMemorySection(this.project.name)}`
1470
+ : "";
1471
+ turnPrompt = `${preamble}${memorySection}\n\n${turnPrompt}`;
1447
1472
  } else if (instructionsInvisibleToHarness) {
1448
1473
  // Local workspace whose AGENTS.md the harness will not load. With a
1449
1474
  // borrowed harness cwd, agent tool scripts live only in the durable
@@ -1469,7 +1494,7 @@ export class SessionEngine {
1469
1494
  ...(mergedCloud === undefined ? {} : { cloud: mergedCloud }),
1470
1495
  customTools:
1471
1496
  runtime === "local" ? this.buildCustomTools(record, turnId, emit) : {},
1472
- mcpServers: this.buildMcpServers(runtime),
1497
+ mcpServers: this.buildMcpServers(runtime, record.sessionId),
1473
1498
  subagents: buildRunnerSubagents(agent),
1474
1499
  onUpdate,
1475
1500
  signal,
@@ -1500,23 +1525,107 @@ export class SessionEngine {
1500
1525
  }
1501
1526
 
1502
1527
  /**
1503
- * Authored + resolved MCP servers for a turn; skips `hostOnly`, and skips
1504
- * host-OAuth connections on cloud turns (their tokens live on this host).
1528
+ * Connection names a cloud turn can only reach through the host connection
1529
+ * bridge on the tool MCP endpoint: filtered account connections without a
1530
+ * public bridge URL, and host-OAuth connections whose tokens live on this
1531
+ * host. `hostOnly` names are never bridged — that flag means the model
1532
+ * must not see the connection at all, on any runtime.
1533
+ */
1534
+ cloudBridgeableConnectionNames(): string[] {
1535
+ const hostOnlyNames = new Set(
1536
+ this.project.agent.connections
1537
+ .filter((connection) => connection.hostOnly === true)
1538
+ .map((connection) => connection.name)
1539
+ );
1540
+ const names: string[] = [];
1541
+ for (const connection of this.project.agent.connections) {
1542
+ if (
1543
+ !hostOnlyNames.has(connection.name) &&
1544
+ isHostOauthConnectionTransport(connection.transport)
1545
+ ) {
1546
+ names.push(connection.name);
1547
+ }
1548
+ }
1549
+ for (const resolved of this.resolvedConnections) {
1550
+ if (
1551
+ !hostOnlyNames.has(resolved.name) &&
1552
+ resolved.accountServersFilter !== undefined &&
1553
+ resolved.cloudUrl === undefined
1554
+ ) {
1555
+ names.push(resolved.name);
1556
+ }
1557
+ }
1558
+ return names;
1559
+ }
1560
+
1561
+ /**
1562
+ * True when cloud turns reach `name` through the host connection bridge —
1563
+ * the gate the tool MCP endpoint applies before forwarding a bridge
1564
+ * request to `host.mcp`, so `hostOnly` connections (and arbitrary names)
1565
+ * stay unreachable from a VM.
1566
+ */
1567
+ isCloudBridgedConnection(name: string): boolean {
1568
+ if (this.cloudServerToolsConnection === undefined) {
1569
+ return false;
1570
+ }
1571
+ return this.cloudBridgeableConnectionNames().includes(name);
1572
+ }
1573
+
1574
+ /**
1575
+ * Session-bound transport for one host-bridged connection: the tool MCP
1576
+ * endpoint with the connection named, so `tools/list` / `tools/call`
1577
+ * forward to the host-side client (`host.mcp`) where the credential —
1578
+ * account bridge secret or host OAuth token — actually lives.
1579
+ */
1580
+ private cloudBridgeTransport(
1581
+ sessionId: string,
1582
+ connectionName: string
1583
+ ): ConnectionTransport | undefined {
1584
+ const connection = this.cloudServerToolsConnection;
1585
+ if (connection === undefined || !("url" in connection)) {
1586
+ return undefined;
1587
+ }
1588
+ const url = new URL(connection.url);
1589
+ url.searchParams.set("agentkitSessionId", sessionId);
1590
+ url.searchParams.set("connection", connectionName);
1591
+ return { ...connection, url: url.toString() };
1592
+ }
1593
+
1594
+ /**
1595
+ * Authored + resolved MCP servers for a turn. Skips `hostOnly`. On cloud
1596
+ * turns, connections whose credential lives on this host — filtered
1597
+ * account connections without a public bridge URL, host-OAuth servers —
1598
+ * mount as host connection bridges over the tool MCP endpoint instead of
1599
+ * being omitted or failing the turn closed.
1505
1600
  */
1506
1601
  private buildMcpServers(
1507
- runtime: "local" | "cloud"
1602
+ runtime: "local" | "cloud",
1603
+ sessionId?: string
1508
1604
  ): Record<string, ConnectionTransport> {
1509
1605
  const hostOnlyNames = new Set(
1510
1606
  this.project.agent.connections
1511
1607
  .filter((connection) => connection.hostOnly === true)
1512
1608
  .map((connection) => connection.name)
1513
1609
  );
1610
+ const bridgedNames = new Set(
1611
+ runtime === "cloud" &&
1612
+ sessionId !== undefined &&
1613
+ this.cloudServerToolsConnection !== undefined &&
1614
+ "url" in this.cloudServerToolsConnection
1615
+ ? this.cloudBridgeableConnectionNames()
1616
+ : []
1617
+ );
1514
1618
  assertCloudCanHonorAccountServersFilters({
1515
1619
  runtime,
1516
1620
  resolvedConnections: this.resolvedConnections.filter(
1517
1621
  (resolved) => !hostOnlyNames.has(resolved.name)
1518
1622
  ),
1623
+ bridgedNames,
1519
1624
  });
1625
+ const bridgeTransport = (name: string): ConnectionTransport | undefined =>
1626
+ sessionId === undefined || !bridgedNames.has(name)
1627
+ ? undefined
1628
+ : this.cloudBridgeTransport(sessionId, name);
1520
1629
  const servers: Record<string, ConnectionTransport> = {};
1521
1630
  for (const connection of this.project.agent.connections) {
1522
1631
  if (hostOnlyNames.has(connection.name)) {
@@ -1527,12 +1636,17 @@ export class SessionEngine {
1527
1636
  }
1528
1637
  // A cloud VM cannot use a credential that lives on this host, and the
1529
1638
  // cloud API rejects the `oauth` key outright ("Unrecognized key(s) in
1530
- // object: 'oauth'"), which fails the whole turn. Omit it there; host
1531
- // tools (`ctx.host.mcp`) still reach the server over the host client.
1639
+ // object: 'oauth'"), which fails the whole turn. Mount the host
1640
+ // connection bridge instead when serve has a cloud tools URL; omit
1641
+ // otherwise (host tools via `ctx.host.mcp` still reach the server).
1532
1642
  if (
1533
1643
  runtime === "cloud" &&
1534
1644
  isHostOauthConnectionTransport(connection.transport)
1535
1645
  ) {
1646
+ const bridged = bridgeTransport(connection.name);
1647
+ if (bridged !== undefined) {
1648
+ servers[connection.name] = bridged;
1649
+ }
1536
1650
  continue;
1537
1651
  }
1538
1652
  servers[connection.name] = connection.transport;
@@ -1543,6 +1657,13 @@ export class SessionEngine {
1543
1657
  }
1544
1658
  const url = runtime === "cloud" ? resolved.cloudUrl : resolved.localUrl;
1545
1659
  if (url === undefined) {
1660
+ // Filtered account connections bridge through the host so the
1661
+ // `servers` filter is honored; unfiltered ones keep their documented
1662
+ // fallback to the account's native connectors on the VM.
1663
+ const bridged = bridgeTransport(resolved.name);
1664
+ if (bridged !== undefined) {
1665
+ servers[resolved.name] = bridged;
1666
+ }
1546
1667
  continue;
1547
1668
  }
1548
1669
  servers[resolved.name] = {
@@ -1550,6 +1671,27 @@ export class SessionEngine {
1550
1671
  headers: resolved.headers,
1551
1672
  };
1552
1673
  }
1674
+ if (
1675
+ runtime === "cloud" &&
1676
+ sessionId !== undefined &&
1677
+ this.cloudServerToolsConnection !== undefined &&
1678
+ this.project.agent.tools.some((tool) => tool.execution === "server")
1679
+ ) {
1680
+ if (servers["agentsdk-tools"] !== undefined) {
1681
+ throw new Error(
1682
+ 'MCP connection name "agentsdk-tools" is reserved for authored server tools on cloud turns.'
1683
+ );
1684
+ }
1685
+ const connection = this.cloudServerToolsConnection;
1686
+ if ("url" in connection) {
1687
+ const url = new URL(connection.url);
1688
+ url.searchParams.set("agentkitSessionId", sessionId);
1689
+ servers["agentsdk-tools"] = {
1690
+ ...connection,
1691
+ url: url.toString(),
1692
+ };
1693
+ }
1694
+ }
1553
1695
  return servers;
1554
1696
  }
1555
1697
 
@@ -1571,96 +1713,144 @@ export class SessionEngine {
1571
1713
  description: tool.description,
1572
1714
  inputSchema: tool.inputSchema as SDKCustomTool["inputSchema"],
1573
1715
  execute: async (args, context) => {
1574
- const validation = validateToolInput(
1575
- tool.definition.inputSchema,
1576
- args
1577
- );
1578
- if (!validation.ok) {
1579
- return {
1580
- content: [
1581
- {
1582
- type: "text",
1583
- text: `Invalid input for tool "${tool.name}": ${validation.message}`,
1584
- },
1585
- ],
1586
- isError: true,
1587
- };
1588
- }
1589
- const toolCallId = context.toolCallId ?? newToolCallId(tool.name);
1590
- const toolCtx: ToolContext = {
1591
- toolCallId,
1592
- session: this.sessionInfo(record),
1593
- workspaceDir: this.harnessCwd(record),
1594
- stateRoot: this.stateRoot,
1595
- host: this.hostForSession(record.sessionId),
1596
- artifacts: this.artifactsApi({
1597
- sessionId: record.sessionId,
1598
- turnId,
1599
- }),
1600
- send: (channelId, message, sendOptions = {}) =>
1601
- this.send(channelId, message, {
1602
- ...sendOptions,
1603
- auth:
1604
- sendOptions.auth !== undefined
1605
- ? sendOptions.auth
1606
- : record.auth,
1607
- }),
1608
- getSession: (channelId, sessionId) =>
1609
- this.getChannelSession(channelId, sessionId),
1610
- };
1611
- try {
1612
- const needsGate = await evaluateNeedsApproval(
1613
- tool.definition.needsApproval,
1614
- validation.value
1615
- );
1616
- if (needsGate) {
1617
- const decision = await this.parkForApproval({
1618
- sessionId: record.sessionId,
1619
- turnId,
1620
- callId: toolCallId,
1621
- toolName: tool.name,
1622
- args: validation.value,
1623
- emit,
1624
- });
1625
- if (decision === "deny") {
1626
- return {
1627
- content: [
1628
- {
1629
- type: "text",
1630
- text: `Tool "${tool.name}" was denied by a human approver.`,
1631
- },
1632
- ],
1633
- isError: true,
1634
- };
1635
- }
1636
- }
1637
- const started = Date.now();
1638
- this.logger(`[session] tool ${tool.name} start`);
1639
- const result = await execute(validation.value as never, toolCtx);
1640
- this.logger(
1641
- `[session] tool ${tool.name} done (${Date.now() - started}ms)`
1642
- );
1643
- return normalizeToolResult(result);
1644
- } catch (error) {
1645
- this.logger(
1646
- `[session] tool ${tool.name} failed: ${describeError(error)}`
1647
- );
1648
- return {
1649
- content: [
1650
- {
1651
- type: "text",
1652
- text: `Tool "${tool.name}" failed: ${describeError(error)}`,
1653
- },
1654
- ],
1655
- isError: true,
1656
- };
1657
- }
1716
+ return this.executeServerTool({
1717
+ tool,
1718
+ input: args,
1719
+ toolCallId: context.toolCallId,
1720
+ record,
1721
+ turnId,
1722
+ emit,
1723
+ });
1658
1724
  },
1659
1725
  };
1660
1726
  }
1661
1727
  return tools;
1662
1728
  }
1663
1729
 
1730
+ /**
1731
+ * True while `sessionId` has a turn in flight. The host connection bridge
1732
+ * applies this before forwarding to `host.mcp` — the same window
1733
+ * {@link callActiveServerTool} enforces for authored server tools — so an
1734
+ * endpoint credential that outlives a turn (e.g. in a VM's MCP config)
1735
+ * cannot reach host-credentialed connectors between turns.
1736
+ */
1737
+ hasActiveTurn(sessionId: string): boolean {
1738
+ return this.activeToolContexts.has(sessionId);
1739
+ }
1740
+
1741
+ async callActiveServerTool(
1742
+ sessionId: string,
1743
+ toolName: string,
1744
+ input: unknown
1745
+ ): Promise<SDKCustomToolResult> {
1746
+ const active = this.activeToolContexts.get(sessionId);
1747
+ if (active === undefined) {
1748
+ return toolErrorResult(
1749
+ `Session ${sessionId} has no active turn for tool "${toolName}".`
1750
+ );
1751
+ }
1752
+ const tool = this.project.agent.tools.find(
1753
+ (candidate) =>
1754
+ candidate.name === toolName && candidate.execution === "server"
1755
+ );
1756
+ if (tool === undefined) {
1757
+ return toolErrorResult(`Unknown server tool "${toolName}".`);
1758
+ }
1759
+ const toolCallId = newToolCallId(toolName);
1760
+ this.logger(
1761
+ `[session] cloud HTTP MCP tool ${toolName} dispatch (${sessionId}, ${toolCallId})`
1762
+ );
1763
+ const result = await this.executeServerTool({
1764
+ tool,
1765
+ input,
1766
+ toolCallId,
1767
+ record: active.record,
1768
+ turnId: active.turnId,
1769
+ emit: active.emit,
1770
+ });
1771
+ this.logger(
1772
+ `[session] cloud HTTP MCP tool ${toolName} complete (${sessionId}, ${toolCallId}, isError=${isErrorEnvelope(result)})`
1773
+ );
1774
+ return result;
1775
+ }
1776
+
1777
+ private async executeServerTool(args: {
1778
+ tool: ResolvedAgent["tools"][number];
1779
+ input: unknown;
1780
+ toolCallId?: string;
1781
+ record: SessionRecord;
1782
+ turnId: string;
1783
+ emit: (payload: SessionEventPayload) => void;
1784
+ }): Promise<SDKCustomToolResult> {
1785
+ const execute = args.tool.definition.execute;
1786
+ if (args.tool.execution !== "server" || typeof execute !== "function") {
1787
+ return toolErrorResult(`Tool "${args.tool.name}" is not a server tool.`);
1788
+ }
1789
+ const validation = validateToolInput(
1790
+ args.tool.definition.inputSchema,
1791
+ args.input
1792
+ );
1793
+ if (!validation.ok) {
1794
+ return toolErrorResult(
1795
+ `Invalid input for tool "${args.tool.name}": ${validation.message}`
1796
+ );
1797
+ }
1798
+ const toolCallId = args.toolCallId ?? newToolCallId(args.tool.name);
1799
+ const toolCtx: ToolContext = {
1800
+ toolCallId,
1801
+ session: this.sessionInfo(args.record),
1802
+ workspaceDir: this.harnessCwd(args.record),
1803
+ stateRoot: this.stateRoot,
1804
+ host: this.hostForSession(args.record.sessionId),
1805
+ artifacts: this.artifactsApi({
1806
+ sessionId: args.record.sessionId,
1807
+ turnId: args.turnId,
1808
+ }),
1809
+ send: (channelId, message, sendOptions = {}) =>
1810
+ this.send(channelId, message, {
1811
+ ...sendOptions,
1812
+ auth: sendOptions.auth ?? args.record.auth,
1813
+ }),
1814
+ getSession: (channelId, sessionId) =>
1815
+ this.getChannelSession(channelId, sessionId),
1816
+ };
1817
+ try {
1818
+ const needsGate = await evaluateNeedsApproval(
1819
+ args.tool.definition.needsApproval,
1820
+ validation.value
1821
+ );
1822
+ if (needsGate) {
1823
+ const decision = await this.parkForApproval({
1824
+ sessionId: args.record.sessionId,
1825
+ turnId: args.turnId,
1826
+ callId: toolCallId,
1827
+ toolName: args.tool.name,
1828
+ args: validation.value,
1829
+ emit: args.emit,
1830
+ });
1831
+ if (decision === "deny") {
1832
+ return toolErrorResult(
1833
+ `Tool "${args.tool.name}" was denied by a human approver.`
1834
+ );
1835
+ }
1836
+ }
1837
+ const started = Date.now();
1838
+ this.logger(`[session] tool ${args.tool.name} start`);
1839
+ const result = await execute(validation.value as never, toolCtx);
1840
+ this.logger(
1841
+ `[session] tool ${args.tool.name} done (${Date.now() - started}ms)`
1842
+ );
1843
+ return normalizeToolResult(result);
1844
+ } catch (error) {
1845
+ this.logger(
1846
+ `[session] tool ${args.tool.name} failed: ${describeError(error)}`
1847
+ );
1848
+ return toolErrorResult(
1849
+ `Tool "${args.tool.name}" failed: ${describeError(error)}`
1850
+ );
1851
+ }
1852
+ }
1853
+
1664
1854
  /**
1665
1855
  * Park a server-tool call until a human approves or denies. Emits
1666
1856
  * `action.approval_requested` / `action.approval_resolved` on the
@@ -2166,15 +2356,48 @@ export class SessionEngine {
2166
2356
  if (options?.includeAll === true) {
2167
2357
  return records;
2168
2358
  }
2359
+ const owned = await this.ownedSessionIds(caller);
2360
+ return records.filter(
2361
+ (record) => record.sessionId !== undefined && owned.has(record.sessionId)
2362
+ );
2363
+ }
2364
+
2365
+ /**
2366
+ * One artifact's file/blob payload for `GET /v1/artifacts/:id/content`.
2367
+ * Same visibility rule as {@link listArtifacts}. Undefined when the
2368
+ * artifact is missing, invisible to the caller, or carries no payload.
2369
+ */
2370
+ async getArtifactContent(
2371
+ caller: AuthContext | null,
2372
+ id: string,
2373
+ options?: { includeAll?: boolean }
2374
+ ): Promise<{ record: ArtifactRecord; body: Uint8Array } | undefined> {
2375
+ const record = await this.artifactsStore.get(id);
2376
+ if (record === undefined || record.content === undefined) {
2377
+ return undefined;
2378
+ }
2379
+ if (options?.includeAll !== true) {
2380
+ if (record.sessionId === undefined) {
2381
+ return undefined;
2382
+ }
2383
+ const owned = await this.ownedSessionIds(caller);
2384
+ if (!owned.has(record.sessionId)) {
2385
+ return undefined;
2386
+ }
2387
+ }
2388
+ const body = await this.artifactsStore.getContent(id);
2389
+ return body === undefined ? undefined : { record, body };
2390
+ }
2391
+
2392
+ private async ownedSessionIds(
2393
+ caller: AuthContext | null
2394
+ ): Promise<Set<string>> {
2169
2395
  const sessions = await this.sessions.list();
2170
- const owned = new Set(
2396
+ return new Set(
2171
2397
  sessions
2172
2398
  .filter((record) => samePrincipal(record.auth, caller))
2173
2399
  .map((record) => record.sessionId)
2174
2400
  );
2175
- return records.filter(
2176
- (record) => record.sessionId !== undefined && owned.has(record.sessionId)
2177
- );
2178
2401
  }
2179
2402
 
2180
2403
  /**
@@ -2998,6 +3221,13 @@ function describeError(error: unknown): string {
2998
3221
  return error instanceof Error ? error.message : String(error);
2999
3222
  }
3000
3223
 
3224
+ function toolErrorResult(message: string): SDKCustomToolResult {
3225
+ return {
3226
+ content: [{ type: "text", text: message }],
3227
+ isError: true,
3228
+ };
3229
+ }
3230
+
3001
3231
  async function defaultGetCloudUsage(
3002
3232
  sdkAgentId: string
3003
3233
  ): Promise<CloudAgentUsage | undefined> {
@@ -458,68 +458,6 @@ export class StorageCoordinator {
458
458
  await this.definition.delete?.(key, this.context("policy"));
459
459
  }
460
460
 
461
- // ==========================================================================
462
- // Artifacts (`ctx.artifacts`)
463
- // ==========================================================================
464
-
465
- /**
466
- * Whether the sink can serve awaited artifact rows: `get` (upsert
467
- * read-back), `list` (listing + eviction), and `delete` (`max` cap
468
- * eviction) are all required — without `delete` the retention cap
469
- * could never evict, so rows fall back to the filesystem store.
470
- */
471
- get supportsArtifacts(): boolean {
472
- return (
473
- this.definition.get !== undefined &&
474
- this.definition.list !== undefined &&
475
- this.definition.delete !== undefined
476
- );
477
- }
478
-
479
- /** Awaited artifact-row read. Errors propagate (read-after-write). */
480
- async artifactGet(id: string): Promise<JsonValue | undefined> {
481
- const get = this.definition.get;
482
- if (get === undefined) {
483
- throw new Error("storage get is not configured");
484
- }
485
- const value = await get(
486
- storageKeys.artifact(this.agentName, id),
487
- this.context("restore")
488
- );
489
- return value ?? undefined;
490
- }
491
-
492
- /** Awaited artifact-row write (straight to the sink, like author KV). */
493
- async artifactPut(id: string, value: JsonValue): Promise<void> {
494
- await this.definition.put(
495
- storageKeys.artifact(this.agentName, id),
496
- value,
497
- this.context("policy")
498
- );
499
- }
500
-
501
- /** Awaited artifact-row delete. Errors propagate (cap eviction). */
502
- async artifactDelete(id: string): Promise<void> {
503
- const del = this.definition.delete;
504
- if (del === undefined) {
505
- throw new Error("storage delete is not configured");
506
- }
507
- await del(storageKeys.artifact(this.agentName, id), this.context("policy"));
508
- }
509
-
510
- /** All artifact-row values for this agent. Errors propagate. */
511
- async artifactList(): Promise<JsonValue[]> {
512
- const list = this.definition.list;
513
- if (list === undefined) {
514
- throw new Error("storage list is not configured");
515
- }
516
- const entries = await list(
517
- storageKeys.artifactPrefix(this.agentName),
518
- this.context("restore")
519
- );
520
- return Array.isArray(entries) ? entries.map((entry) => entry.value) : [];
521
- }
522
-
523
461
  // ==========================================================================
524
462
  // Evals (dedicated `evals` table)
525
463
  // ==========================================================================
@@ -19,6 +19,7 @@ import {
19
19
  writeFile,
20
20
  } from "node:fs/promises";
21
21
  import { dirname, join, parse, relative, resolve, sep } from "node:path";
22
+ import { agentStoreKeys, DEPLOYMENT_STORE_MOUNT_PATH } from "../files.js";
22
23
  import { MEMORY_DIR_NAME } from "../memory.js";
23
24
  import type {
24
25
  DiscoveredSkill,
@@ -38,15 +39,28 @@ export interface MaterializeWorkspaceOptions {
38
39
  seed: boolean;
39
40
  /**
40
41
  * Agent state root. `<stateRoot>/memory/` (the durable cross-session
41
- * directory the memory hook journals into) is created and symlinked into
42
- * the workspace as `memory` so the agent reads it with plain file tools.
43
- *
44
- * TODO(agent-store): replace with the mounted AgentStore path so cloud
45
- * VMs see the same directory.
42
+ * directory the memory hook journals into on hosting a local mirror of
43
+ * the Agent Store journal) is created and symlinked into the workspace as
44
+ * `memory` so the agent reads it with plain file tools. Cloud VMs have no
45
+ * materialized workspace; they read the store mount directly (see
46
+ * {@link buildCloudMemorySection}).
46
47
  */
47
48
  stateRoot: string;
48
49
  }
49
50
 
51
+ /**
52
+ * First-turn preamble section for cloud-runtime sessions on hosted
53
+ * deployments: cloud VMs read the memory journal from the store mount.
54
+ */
55
+ export function buildCloudMemorySection(agentName: string): string {
56
+ const dir = `${DEPLOYMENT_STORE_MOUNT_PATH}/${agentStoreKeys.memoryDir(agentName)}`;
57
+ return [
58
+ "## Shared session memory",
59
+ "",
60
+ `The cross-session memory journal (the \`memory/\` directory your instructions may reference) lives at \`${dir}\` on this machine — read \`${dir}/journal.jsonl\` with your normal file tools.`,
61
+ ].join("\n");
62
+ }
63
+
50
64
  /** Relative path of an agent-side tool script inside the session workspace. */
51
65
  export function agentToolScriptPath(toolName: string): string {
52
66
  return `.agent-serve/tools/${toolName}.sh`;