@cursor/july 0.1.103 → 0.1.104

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 (186) hide show
  1. package/dist/channels/change-monitors/change-monitors-channel.d.ts +5 -0
  2. package/dist/channels/change-monitors/change-monitors-channel.d.ts.map +1 -0
  3. package/dist/channels/change-monitors/change-monitors-channel.js +41 -0
  4. package/dist/channels/change-monitors/index.d.ts +4 -0
  5. package/dist/channels/change-monitors/index.d.ts.map +1 -0
  6. package/dist/channels/change-monitors/index.js +2 -0
  7. package/dist/channels/change-monitors/types.d.ts +39 -0
  8. package/dist/channels/change-monitors/types.d.ts.map +1 -0
  9. package/dist/channels/change-monitors/types.js +1 -0
  10. package/dist/channels/github/defaults.d.ts.map +1 -1
  11. package/dist/channels/github/defaults.js +4 -0
  12. package/dist/channels/github/progress.d.ts +22 -0
  13. package/dist/channels/github/progress.d.ts.map +1 -1
  14. package/dist/channels/github/progress.js +32 -5
  15. package/dist/channels/issues/index.d.ts +4 -0
  16. package/dist/channels/issues/index.d.ts.map +1 -0
  17. package/dist/channels/issues/index.js +2 -0
  18. package/dist/channels/issues/issues-channel.d.ts +5 -0
  19. package/dist/channels/issues/issues-channel.d.ts.map +1 -0
  20. package/dist/channels/issues/issues-channel.js +37 -0
  21. package/dist/channels/issues/types.d.ts +33 -0
  22. package/dist/channels/issues/types.d.ts.map +1 -0
  23. package/dist/channels/issues/types.js +1 -0
  24. package/dist/docs/404.html +2 -2
  25. package/dist/docs/ab.html +4 -4
  26. package/dist/docs/assets/{app.DW9pXkpL.js → app.DbMWUyGb.js} +1 -1
  27. package/dist/docs/assets/chunks/@localSearchIndexroot.Bqsulb9K.js +1 -0
  28. package/dist/docs/assets/chunks/{VPLocalSearchBox.CdEIUwDg.js → VPLocalSearchBox.DcNeTh9o.js} +1 -1
  29. package/dist/docs/assets/chunks/{theme.YZMHgPgg.js → theme.DMjhY3gl.js} +2 -2
  30. package/dist/docs/assets/{guides_github.md.DKYiT9do.js → guides_github.md.c0gdGJ-D.js} +1 -1
  31. package/dist/docs/assets/{guides_github.md.DKYiT9do.lean.js → guides_github.md.c0gdGJ-D.lean.js} +1 -1
  32. package/dist/docs/assets/{reference_channels.md.Dz8Cnudp.js → reference_channels.md.DBGcaB7m.js} +2 -2
  33. package/dist/docs/assets/{reference_channels.md.Dz8Cnudp.lean.js → reference_channels.md.DBGcaB7m.lean.js} +1 -1
  34. package/dist/docs/building-with-agents.html +4 -4
  35. package/dist/docs/concepts.html +4 -4
  36. package/dist/docs/deployment.html +4 -4
  37. package/dist/docs/design/filesystem.md +376 -0
  38. package/dist/docs/evals.html +4 -4
  39. package/dist/docs/guides/agent-to-agent.html +4 -4
  40. package/dist/docs/guides/cloud-agents.html +4 -4
  41. package/dist/docs/guides/convert-automation.html +4 -4
  42. package/dist/docs/guides/github.html +6 -6
  43. package/dist/docs/guides/github.md +5 -0
  44. package/dist/docs/guides/human-in-the-loop.html +4 -4
  45. package/dist/docs/guides/mcp-oauth.html +4 -4
  46. package/dist/docs/guides/opentelemetry.html +4 -4
  47. package/dist/docs/guides/slack.html +4 -4
  48. package/dist/docs/guides/webhooks.html +4 -4
  49. package/dist/docs/hashmap.json +1 -1
  50. package/dist/docs/hillclimbing.html +4 -4
  51. package/dist/docs/index.html +4 -4
  52. package/dist/docs/llms-full.txt +406 -0
  53. package/dist/docs/llms.txt +1 -0
  54. package/dist/docs/quickstart.html +4 -4
  55. package/dist/docs/reference/agent-config.html +4 -4
  56. package/dist/docs/reference/artifacts.html +4 -4
  57. package/dist/docs/reference/channels.html +6 -6
  58. package/dist/docs/reference/channels.md +20 -0
  59. package/dist/docs/reference/cli.html +4 -4
  60. package/dist/docs/reference/connections.html +4 -4
  61. package/dist/docs/reference/extensions.html +4 -4
  62. package/dist/docs/reference/hooks.html +4 -4
  63. package/dist/docs/reference/http-api.html +4 -4
  64. package/dist/docs/reference/instructions.html +4 -4
  65. package/dist/docs/reference/playground.html +4 -4
  66. package/dist/docs/reference/project-layout.html +4 -4
  67. package/dist/docs/reference/prompt.html +4 -4
  68. package/dist/docs/reference/result.html +4 -4
  69. package/dist/docs/reference/schedules.html +4 -4
  70. package/dist/docs/reference/sessions.html +4 -4
  71. package/dist/docs/reference/skills.html +4 -4
  72. package/dist/docs/reference/subagents.html +4 -4
  73. package/dist/docs/reference/tools.html +4 -4
  74. package/dist/docs/scaffolding-agents.html +4 -4
  75. package/dist/docs/skills/ab.html +4 -4
  76. package/dist/docs/skills/create-agent.html +4 -4
  77. package/dist/docs/skills/debug.html +4 -4
  78. package/dist/docs/skills/evals.html +4 -4
  79. package/dist/docs/skills/framework-map.html +4 -4
  80. package/dist/docs/skills/github.html +4 -4
  81. package/dist/docs/skills/hillclimb.html +4 -4
  82. package/dist/docs/skills/index.html +4 -4
  83. package/dist/docs/skills/mcp-auth.html +4 -4
  84. package/dist/docs/skills/otel.html +4 -4
  85. package/dist/docs/skills/setup-slack.html +4 -4
  86. package/dist/docs/storage.html +4 -4
  87. package/dist/docs/templates/agentic-owners.html +4 -4
  88. package/dist/docs/templates/agents-md.html +4 -4
  89. package/dist/docs/templates/code-wiki.html +4 -4
  90. package/dist/docs/templates/demo.html +4 -4
  91. package/dist/docs/templates/pr-autofixer.html +4 -4
  92. package/dist/docs/templates/security-help.html +4 -4
  93. package/dist/docs/templates/security-reviewer.html +4 -4
  94. package/dist/docs/templates/triage.html +4 -4
  95. package/dist/docs/troubleshooting.html +4 -4
  96. package/dist/filesystem.d.ts +440 -0
  97. package/dist/filesystem.d.ts.map +1 -0
  98. package/dist/filesystem.js +1694 -0
  99. package/dist/index.d.ts +2 -0
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +1 -0
  102. package/dist/internal/continuation-channel.d.ts +4 -2
  103. package/dist/internal/continuation-channel.d.ts.map +1 -1
  104. package/dist/internal/continuation-channel.js +21 -12
  105. package/dist/internal/continuation-identity.d.ts +13 -5
  106. package/dist/internal/continuation-identity.d.ts.map +1 -1
  107. package/dist/internal/continuation-identity.js +74 -35
  108. package/dist/internal/discovery/agent.d.ts.map +1 -1
  109. package/dist/internal/discovery/agent.js +80 -8
  110. package/dist/internal/discovery/info.d.ts.map +1 -1
  111. package/dist/internal/discovery/info.js +3 -0
  112. package/dist/internal/factory-resource-channel.d.ts +33 -0
  113. package/dist/internal/factory-resource-channel.d.ts.map +1 -0
  114. package/dist/internal/factory-resource-channel.js +77 -0
  115. package/dist/internal/filesystem/errors.d.ts +64 -0
  116. package/dist/internal/filesystem/errors.d.ts.map +1 -0
  117. package/dist/internal/filesystem/errors.js +175 -0
  118. package/dist/internal/filesystem/format.d.ts +28 -0
  119. package/dist/internal/filesystem/format.d.ts.map +1 -0
  120. package/dist/internal/filesystem/format.js +213 -0
  121. package/dist/internal/filesystem/merge.d.ts +34 -0
  122. package/dist/internal/filesystem/merge.d.ts.map +1 -0
  123. package/dist/internal/filesystem/merge.js +196 -0
  124. package/dist/internal/filesystem/scratch.d.ts +59 -0
  125. package/dist/internal/filesystem/scratch.d.ts.map +1 -0
  126. package/dist/internal/filesystem/scratch.js +212 -0
  127. package/dist/internal/filesystem/sdk-schemas.d.ts +29 -0
  128. package/dist/internal/filesystem/sdk-schemas.d.ts.map +1 -0
  129. package/dist/internal/filesystem/sdk-schemas.js +153 -0
  130. package/dist/internal/filesystem/tools.d.ts +42 -0
  131. package/dist/internal/filesystem/tools.d.ts.map +1 -0
  132. package/dist/internal/filesystem/tools.js +618 -0
  133. package/dist/internal/filesystem/walk.d.ts +74 -0
  134. package/dist/internal/filesystem/walk.d.ts.map +1 -0
  135. package/dist/internal/filesystem/walk.js +339 -0
  136. package/dist/internal/guest-network.d.ts +5 -2
  137. package/dist/internal/guest-network.d.ts.map +1 -1
  138. package/dist/internal/guest-network.js +7 -2
  139. package/dist/internal/hosted-delivery.d.ts.map +1 -1
  140. package/dist/internal/hosted-delivery.js +12 -2
  141. package/dist/internal/sdk-runner.d.ts +1 -1
  142. package/dist/internal/sdk-runner.d.ts.map +1 -1
  143. package/dist/internal/sdk-runner.js +3 -1
  144. package/dist/internal/session-engine.d.ts +22 -0
  145. package/dist/internal/session-engine.d.ts.map +1 -1
  146. package/dist/internal/session-engine.js +362 -40
  147. package/dist/internal/testing/factory-resource-fixtures.d.ts +48 -0
  148. package/dist/internal/testing/factory-resource-fixtures.d.ts.map +1 -0
  149. package/dist/internal/testing/factory-resource-fixtures.js +41 -0
  150. package/dist/playground/assets/{index-59KvgREd.js → index-8YiozU89.js} +38 -38
  151. package/dist/playground/index.html +1 -1
  152. package/dist/types.d.ts +51 -3
  153. package/dist/types.d.ts.map +1 -1
  154. package/docs/design/filesystem.md +376 -0
  155. package/docs/guides/github.md +5 -0
  156. package/docs/reference/channels.md +20 -0
  157. package/package.json +16 -2
  158. package/src/channels/change-monitors/change-monitors-channel.ts +62 -0
  159. package/src/channels/change-monitors/index.ts +8 -0
  160. package/src/channels/change-monitors/types.ts +44 -0
  161. package/src/channels/github/defaults.ts +4 -0
  162. package/src/channels/github/progress.ts +76 -10
  163. package/src/channels/issues/index.ts +9 -0
  164. package/src/channels/issues/issues-channel.ts +51 -0
  165. package/src/channels/issues/types.ts +41 -0
  166. package/src/filesystem.ts +2074 -0
  167. package/src/index.ts +28 -0
  168. package/src/internal/continuation-channel.ts +29 -12
  169. package/src/internal/continuation-identity.ts +122 -37
  170. package/src/internal/discovery/agent.ts +87 -1
  171. package/src/internal/discovery/info.ts +3 -0
  172. package/src/internal/factory-resource-channel.ts +122 -0
  173. package/src/internal/filesystem/errors.ts +220 -0
  174. package/src/internal/filesystem/format.ts +276 -0
  175. package/src/internal/filesystem/merge.ts +195 -0
  176. package/src/internal/filesystem/scratch.ts +255 -0
  177. package/src/internal/filesystem/sdk-schemas.ts +204 -0
  178. package/src/internal/filesystem/tools.ts +821 -0
  179. package/src/internal/filesystem/walk.ts +369 -0
  180. package/src/internal/guest-network.ts +8 -2
  181. package/src/internal/hosted-delivery.ts +18 -1
  182. package/src/internal/sdk-runner.ts +4 -0
  183. package/src/internal/session-engine.ts +439 -29
  184. package/src/internal/testing/factory-resource-fixtures.ts +85 -0
  185. package/src/types.ts +49 -3
  186. package/dist/docs/assets/chunks/@localSearchIndexroot.nrl1ifBq.js +0 -1
@@ -37,6 +37,17 @@ import {
37
37
  isCursorHostedFilesAvailable,
38
38
  isCursorUserStoreFilesAvailable,
39
39
  } from "../files-backends/cursor-hosted.js";
40
+ import {
41
+ type FileSystem,
42
+ isFileSystemToolAlias,
43
+ isMutatingFileSystemTool,
44
+ localFileSystem,
45
+ resolveFileSystemTools,
46
+ resolveHostHarnessToolOptions,
47
+ tempFs,
48
+ unionFs,
49
+ writeWorkspaceFilesToRoot,
50
+ } from "../filesystem.js";
40
51
  import { storageKeysForArchitecture } from "../storage.js";
41
52
  import {
42
53
  type AgentProject,
@@ -69,6 +80,7 @@ import {
69
80
  type PendingAuth,
70
81
  type ReceiveInput,
71
82
  type ResolvedAgent,
83
+ type RootContext,
72
84
  type RunnerSubagent,
73
85
  type RunnerTurnRequest,
74
86
  type SendMessageFn,
@@ -124,6 +136,17 @@ import { describeError } from "./describe-error.js";
124
136
  import { isDevMode } from "./dev-mode.js";
125
137
  import { type EventLogMemoryStats, EventLogStore } from "./event-log.js";
126
138
  import { TurnEventMapper } from "./event-mapper.js";
139
+ import {
140
+ applyWebScratchUpdate,
141
+ ingestHostAgentToolsScratch,
142
+ remapScratchPaths,
143
+ } from "./filesystem/scratch.js";
144
+ import {
145
+ fileSystemTool,
146
+ hostFileToolsFromAuthored,
147
+ rootFileToolsFromAuthored,
148
+ synthesizeFileSystemTools,
149
+ } from "./filesystem/tools.js";
127
150
  import { selectFrameworkStorageDefinition } from "./framework-storage-selection.js";
128
151
  import { createHostFilesApi } from "./host-files.js";
129
152
  import { createHostKvApi } from "./host-kv.js";
@@ -184,7 +207,6 @@ import {
184
207
  isNestedInGitRepo,
185
208
  materializeWorkspace,
186
209
  SERVER_TOOLS_MCP_SERVER_NAME,
187
- writeWorkspaceFiles,
188
210
  } from "./workspace.js";
189
211
  import { syncWorkspaceToStore } from "./workspace-sync.js";
190
212
 
@@ -510,6 +532,15 @@ export class SessionEngine {
510
532
  private readonly cloudCostBackfillDone = new Set<string>();
511
533
  /** Deduped in-flight cloud cost backfills, keyed by session id. */
512
534
  private readonly cloudCostBackfill = new Map<string, Promise<void>>();
535
+ /** Per-session FileSystem handle cache (not a tree snapshot). */
536
+ private readonly sessionRoots = new Map<string, FileSystem>();
537
+ /** In-flight factory resolves, so the first turn and a racing callTool share one handle. */
538
+ private readonly sessionRootInflight = new Map<string, Promise<FileSystem>>();
539
+ /** Per-turn `unionFs({ "/": authored, "/tmp": tempFs() })`. Dropped at end of turn. */
540
+ private readonly turnRoots = new Map<
541
+ string,
542
+ { root: FileSystem; cwd: string }
543
+ >();
513
544
 
514
545
  constructor(options: SessionEngineOptions) {
515
546
  this.project = options.project;
@@ -1793,6 +1824,261 @@ export class SessionEngine {
1793
1824
  return this.interrupt(existing.sessionId);
1794
1825
  }
1795
1826
 
1827
+ private hasRootOverride(): boolean {
1828
+ return this.project.agent.root !== undefined;
1829
+ }
1830
+
1831
+ /**
1832
+ * FileSystem for this turn or `callTool`. Authored `root` (static or
1833
+ * factory) is cached per session when `cache` is set; unscoped
1834
+ * `callTool` passes the invocation id and does not cache, so factories
1835
+ * cannot share one tree across callers. A static handle is used as
1836
+ * authored — every session sees that same object. Host-default agents
1837
+ * get `localFileSystem` against the harness cwd — same path verbs as
1838
+ * the Cursor SDK natives, always through the synthesized tools.
1839
+ */
1840
+ private async resolveFileSystem(options: {
1841
+ sessionId: string;
1842
+ continuationKey?: string;
1843
+ cache: boolean;
1844
+ cwd: string;
1845
+ }): Promise<FileSystem> {
1846
+ const authored = this.project.agent.root;
1847
+ if (authored === undefined) {
1848
+ return localFileSystem({ cwd: options.cwd });
1849
+ }
1850
+ const authoredRoot = await this.resolveAuthoredFileSystem(options);
1851
+ if (options.cache) {
1852
+ const existing = this.turnRoots.get(options.sessionId);
1853
+ if (existing !== undefined) {
1854
+ return existing.root;
1855
+ }
1856
+ }
1857
+ const root = unionFs({
1858
+ "/": authoredRoot,
1859
+ "/tmp": tempFs(),
1860
+ });
1861
+ if (options.cache && this.isTurnRunning(options.sessionId)) {
1862
+ this.turnRoots.set(options.sessionId, { root, cwd: options.cwd });
1863
+ }
1864
+ return root;
1865
+ }
1866
+
1867
+ private async resolveAuthoredFileSystem(options: {
1868
+ sessionId: string;
1869
+ continuationKey?: string;
1870
+ cache: boolean;
1871
+ }): Promise<FileSystem> {
1872
+ const authored = this.project.agent.root;
1873
+ if (authored === undefined) {
1874
+ throw new Error("resolveAuthoredFileSystem requires an authored root");
1875
+ }
1876
+ if (options.cache) {
1877
+ const cached = this.sessionRoots.get(options.sessionId);
1878
+ if (cached !== undefined) {
1879
+ return cached;
1880
+ }
1881
+ const inflight = this.sessionRootInflight.get(options.sessionId);
1882
+ if (inflight !== undefined) {
1883
+ return inflight;
1884
+ }
1885
+ }
1886
+ const resolve = async (): Promise<FileSystem> => {
1887
+ if (typeof authored !== "function") {
1888
+ return authored;
1889
+ }
1890
+ const host = this.hostForSession(
1891
+ options.cache ? options.sessionId : undefined
1892
+ );
1893
+ const ctx: RootContext = {
1894
+ sessionId: options.sessionId,
1895
+ agentName: this.project.agent.name,
1896
+ ...(options.continuationKey === undefined
1897
+ ? {}
1898
+ : { continuationKey: options.continuationKey }),
1899
+ host: { files: host.files, reminders: host.reminders },
1900
+ };
1901
+ return await authored(ctx);
1902
+ };
1903
+ if (!options.cache) {
1904
+ return resolve();
1905
+ }
1906
+ const pending = resolve();
1907
+ this.sessionRootInflight.set(options.sessionId, pending);
1908
+ try {
1909
+ const resolved = await pending;
1910
+ this.sessionRoots.set(options.sessionId, resolved);
1911
+ return resolved;
1912
+ } finally {
1913
+ this.sessionRootInflight.delete(options.sessionId);
1914
+ }
1915
+ }
1916
+
1917
+ private dropTurnFileSystem(sessionId: string): void {
1918
+ this.turnRoots.delete(sessionId);
1919
+ }
1920
+
1921
+ private async prepareScratchToolInput(options: {
1922
+ sessionId?: string;
1923
+ input: Record<string, unknown>;
1924
+ }): Promise<Record<string, unknown>> {
1925
+ if (options.sessionId === undefined) {
1926
+ return options.input;
1927
+ }
1928
+ const turn = this.turnRoots.get(options.sessionId);
1929
+ if (turn === undefined) {
1930
+ return options.input;
1931
+ }
1932
+ await ingestHostAgentToolsScratch({
1933
+ cwd: turn.cwd,
1934
+ root: turn.root,
1935
+ });
1936
+ return remapScratchPaths(options.input, turn.cwd);
1937
+ }
1938
+
1939
+ private withTurnScratchTools(
1940
+ tools: DiscoveredTool[],
1941
+ sessionId: string
1942
+ ): DiscoveredTool[] {
1943
+ const turn = this.turnRoots.get(sessionId);
1944
+ if (turn === undefined) {
1945
+ return tools;
1946
+ }
1947
+ return tools.map((tool) => {
1948
+ const execute = tool.definition.execute;
1949
+ if (typeof execute !== "function") {
1950
+ return tool;
1951
+ }
1952
+ return {
1953
+ ...tool,
1954
+ definition: {
1955
+ ...tool.definition,
1956
+ execute: async (input, ctx) => {
1957
+ const remapped = await this.prepareScratchToolInput({
1958
+ sessionId,
1959
+ input:
1960
+ input !== null &&
1961
+ typeof input === "object" &&
1962
+ !Array.isArray(input)
1963
+ ? (input as Record<string, unknown>)
1964
+ : {},
1965
+ });
1966
+ return execute(remapped as never, ctx);
1967
+ },
1968
+ },
1969
+ };
1970
+ });
1971
+ }
1972
+
1973
+ private async nativeFileSystemOutcome(options: {
1974
+ toolName: string;
1975
+ input: unknown;
1976
+ callId: string;
1977
+ sessionId?: string;
1978
+ continuationKey?: string;
1979
+ cwd: string;
1980
+ dryRun?: boolean;
1981
+ }): Promise<ToolCallOutcome | undefined> {
1982
+ const { toolName, input, callId } = options;
1983
+ if (!isFileSystemToolAlias(toolName)) {
1984
+ return undefined;
1985
+ }
1986
+ // Same policy the authored callTool path asks: a dry-run write is
1987
+ // answered by the host. Native edit/delete used to return before
1988
+ // decideToolCall and persist through the FileSystem.
1989
+ const decision = decideToolCall({
1990
+ dryRun: options.dryRun === true,
1991
+ effect: isMutatingFileSystemTool(toolName) ? "write" : "read",
1992
+ input,
1993
+ });
1994
+ if (decision.kind === "answer") {
1995
+ return {
1996
+ callId,
1997
+ toolName,
1998
+ isError: false,
1999
+ result: decision.result,
2000
+ durationMs: 0,
2001
+ sessionId: options.sessionId,
2002
+ };
2003
+ }
2004
+ const root = await this.resolveFileSystem({
2005
+ sessionId: options.sessionId ?? callId,
2006
+ continuationKey: options.continuationKey,
2007
+ cache: options.sessionId !== undefined,
2008
+ cwd: options.cwd,
2009
+ });
2010
+ const rawArgs =
2011
+ input !== null && typeof input === "object" && !Array.isArray(input)
2012
+ ? (input as Record<string, unknown>)
2013
+ : {};
2014
+ const args = await this.prepareScratchToolInput({
2015
+ sessionId: options.sessionId,
2016
+ input: rawArgs,
2017
+ });
2018
+ const tool = fileSystemTool(root, toolName, args);
2019
+ const execute = tool?.definition.execute;
2020
+ if (tool === undefined || typeof execute !== "function") {
2021
+ // A resolved root owns this native name. Falling through would let
2022
+ // an authored lookalike run, or surface UnknownToolError after we
2023
+ // already advertised the FileSystem tool.
2024
+ return {
2025
+ callId,
2026
+ toolName,
2027
+ isError: true,
2028
+ result: {
2029
+ status: "error",
2030
+ error: { message: `FileSystem cannot handle ${toolName}` },
2031
+ },
2032
+ durationMs: 0,
2033
+ sessionId: options.sessionId,
2034
+ errorMessage: `FileSystem cannot handle ${toolName}`,
2035
+ };
2036
+ }
2037
+ const validation = validateToolInput(tool.definition.inputSchema, args);
2038
+ if (!validation.ok) {
2039
+ return {
2040
+ callId,
2041
+ toolName,
2042
+ isError: true,
2043
+ result: {
2044
+ status: "error",
2045
+ error: { message: validation.message },
2046
+ },
2047
+ durationMs: 0,
2048
+ sessionId: options.sessionId,
2049
+ errorMessage: validation.message,
2050
+ };
2051
+ }
2052
+ try {
2053
+ const value = await execute(
2054
+ validation.value as never,
2055
+ {
2056
+ toolCallId: callId,
2057
+ workspaceDir: options.cwd,
2058
+ } as never
2059
+ );
2060
+ return {
2061
+ callId,
2062
+ toolName,
2063
+ isError: false,
2064
+ result: normalizeToolResult(value),
2065
+ durationMs: 0,
2066
+ sessionId: options.sessionId,
2067
+ };
2068
+ } catch (error) {
2069
+ const message = error instanceof Error ? error.message : String(error);
2070
+ return {
2071
+ callId,
2072
+ toolName,
2073
+ isError: true,
2074
+ result: { status: "error", error: { message } },
2075
+ durationMs: 0,
2076
+ sessionId: options.sessionId,
2077
+ errorMessage: message,
2078
+ };
2079
+ }
2080
+ }
2081
+
1796
2082
  /**
1797
2083
  * Local harness cwd for a session: per-send override, else the shared
1798
2084
  * `agent.local.workspaceDir`, else `<agent.local.cwd>/<sessionId>`, else
@@ -1840,19 +2126,22 @@ export class SessionEngine {
1840
2126
  if (
1841
2127
  workspaceDir === undefined ||
1842
2128
  agent.runtime !== "local" ||
2129
+ this.hasRootOverride() ||
1843
2130
  this.runner.prewarm === undefined
1844
2131
  ) {
1845
2132
  return;
1846
2133
  }
1847
2134
  const startedAt = Date.now();
1848
2135
  try {
2136
+ const harness = resolveHostHarnessToolOptions(agent.allowedTools);
1849
2137
  const outcome = await this.runner.prewarm({
1850
2138
  workspaceDir,
1851
2139
  runnerStateDir: join(this.stateRoot, "runner"),
1852
2140
  sandbox: agent.local?.sandbox === true,
1853
- ...(agent.allowedTools === undefined
2141
+ ...(harness.tools === undefined ? {} : { tools: harness.tools }),
2142
+ ...(harness.disallowedTools === undefined
1854
2143
  ? {}
1855
- : { tools: agent.allowedTools }),
2144
+ : { disallowedTools: harness.disallowedTools }),
1856
2145
  // No session yet, so auth'd connections are omitted rather than
1857
2146
  // warmed without an identity; the per-send flag still has to match
1858
2147
  // the turns' requests or the warmed executor is one no turn asks for.
@@ -1942,22 +2231,24 @@ export class SessionEngine {
1942
2231
  // cwd is overridden (e.g. a PR worktree). A/B variant instruction
1943
2232
  // overlays are applied via agentForSession.
1944
2233
  const agent = this.agentForSession(record);
1945
- await materializeWorkspace({
1946
- agent,
1947
- workspaceDir,
1948
- seed: isFirstTurn,
1949
- stateRoot: this.stateRoot,
1950
- });
1951
- this.materializedWorkspaces.add(sessionId);
1952
- // Must precede the carrier: the harness resolves rules, skills,
1953
- // `mcp.json` and hooks from its git root, so a workspace nested in an
1954
- // outer repo would load that repo's hooks instead of the ones written
1955
- // just below. Only for directories this session owns a borrowed
1956
- // checkout or worktree keeps its own root.
1957
- if (!this.hasBorrowedWorkspace(record)) {
1958
- await ensureOwnGitRoot(harnessCwd);
2234
+ if (!this.hasRootOverride()) {
2235
+ await materializeWorkspace({
2236
+ agent,
2237
+ workspaceDir,
2238
+ seed: isFirstTurn,
2239
+ stateRoot: this.stateRoot,
2240
+ });
2241
+ this.materializedWorkspaces.add(sessionId);
2242
+ // Must precede the carrier: the harness resolves rules, skills,
2243
+ // `mcp.json` and hooks from its git root, so a workspace nested in an
2244
+ // outer repo would load that repo's hooks instead of the ones written
2245
+ // just below. Only for directories this session owns — a borrowed
2246
+ // checkout or worktree keeps its own root.
2247
+ if (!this.hasBorrowedWorkspace(record)) {
2248
+ await ensureOwnGitRoot(harnessCwd);
2249
+ }
2250
+ await this.materializeNudgeCarrier(record, harnessCwd);
1959
2251
  }
1960
- await this.materializeNudgeCarrier(record, harnessCwd);
1961
2252
  // Baseline before the agent can run, so a follow-up that arrives and
1962
2253
  // drains before the first tool boundary still counts as an increase.
1963
2254
  await this.baselineDeliveredNudges(record);
@@ -1967,7 +2258,13 @@ export class SessionEngine {
1967
2258
  pendingFiles !== undefined &&
1968
2259
  Object.keys(pendingFiles).length > 0
1969
2260
  ) {
1970
- await writeWorkspaceFiles(harnessCwd, pendingFiles);
2261
+ const root = await this.resolveFileSystem({
2262
+ sessionId,
2263
+ continuationKey: record.continuationKey ?? undefined,
2264
+ cache: true,
2265
+ cwd: harnessCwd,
2266
+ });
2267
+ await writeWorkspaceFilesToRoot(root, pendingFiles);
1971
2268
  }
1972
2269
  if (
1973
2270
  (pendingFiles !== undefined &&
@@ -2004,6 +2301,14 @@ export class SessionEngine {
2004
2301
  instructionsInvisibleToHarness,
2005
2302
  emit,
2006
2303
  async (update) => {
2304
+ const turn = this.turnRoots.get(sessionId);
2305
+ if (turn !== undefined) {
2306
+ update = await applyWebScratchUpdate({
2307
+ cwd: turn.cwd,
2308
+ root: turn.root,
2309
+ update,
2310
+ });
2311
+ }
2007
2312
  mapper.handleUpdate(update);
2008
2313
  // Yield so the HTTP NDJSON writer can flush tool start/result
2009
2314
  // events while sibling parallel tools are still executing.
@@ -2039,6 +2344,7 @@ export class SessionEngine {
2039
2344
  outcome = { status: "error", errorMessage: describeError(error) };
2040
2345
  } finally {
2041
2346
  permit.release();
2347
+ this.dropTurnFileSystem(sessionId);
2042
2348
  this.approvals.cancelSession(sessionId, "deny");
2043
2349
  this.auths.cancelSession(sessionId, "deny");
2044
2350
  await this.clearPendingApprovals(sessionId);
@@ -2291,6 +2597,17 @@ export class SessionEngine {
2291
2597
  // allowlist, and a restriction that silently does not apply is worse
2292
2598
  // than a refused turn. Non-local agents already fail discovery; this
2293
2599
  // guards hybrid agents' per-send cloud sessions.
2600
+ //
2601
+ // Factory roots omit `allowedTools` at discovery (the live handle is
2602
+ // resolved per session). The allowlist guard below therefore never
2603
+ // fires for `defineAgent({ root: (ctx) => … })` with omitted `tools`,
2604
+ // and a per-send `cloud` attach would skip the virtual-root allowlist
2605
+ // and run unrestricted on a VM. Any authored root refuses non-local turns.
2606
+ if (runtime !== "local" && this.hasRootOverride()) {
2607
+ throw new Error(
2608
+ `Agent "${agent.name}" declares root, which cannot be served on ${runtime} turns. Refusing the session rather than running it against the host VM.`
2609
+ );
2610
+ }
2294
2611
  if (runtime !== "local" && agent.allowedTools !== undefined) {
2295
2612
  throw new Error(
2296
2613
  `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.`
@@ -2319,6 +2636,48 @@ export class SessionEngine {
2319
2636
  override: record.cloudOverride,
2320
2637
  })
2321
2638
  : undefined;
2639
+ const authoredRoot = this.hasRootOverride();
2640
+ const root =
2641
+ runtime === "local"
2642
+ ? await this.resolveFileSystem({
2643
+ sessionId: record.sessionId,
2644
+ continuationKey: record.continuationKey ?? undefined,
2645
+ cache: true,
2646
+ cwd: this.harnessCwd(record),
2647
+ })
2648
+ : undefined;
2649
+ const harnessTools =
2650
+ runtime === "local" && root !== undefined
2651
+ ? authoredRoot
2652
+ ? {
2653
+ tools: resolveFileSystemTools({
2654
+ root,
2655
+ tools: agent.allowedTools,
2656
+ }),
2657
+ }
2658
+ : resolveHostHarnessToolOptions(agent.allowedTools)
2659
+ : undefined;
2660
+ const fileTools =
2661
+ root === undefined
2662
+ ? []
2663
+ : synthesizeFileSystemTools(
2664
+ root,
2665
+ authoredRoot
2666
+ ? rootFileToolsFromAuthored(agent.allowedTools)
2667
+ : hostFileToolsFromAuthored(agent.allowedTools)
2668
+ );
2669
+ // Host-default keeps native SemanticSearch (the host index).
2670
+ // `localFileSystem.search` is empty, so synthesizing would
2671
+ // replace a working index with an empty custom tool.
2672
+ const synthesized =
2673
+ root === undefined
2674
+ ? []
2675
+ : this.withTurnScratchTools(
2676
+ authoredRoot
2677
+ ? fileTools
2678
+ : fileTools.filter((tool) => tool.name !== "SemanticSearch"),
2679
+ record.sessionId
2680
+ );
2322
2681
  let turnPrompt = prompt;
2323
2682
  if (isFirstTurn) {
2324
2683
  const contextBlock = buildSessionContextBlock({
@@ -2374,6 +2733,18 @@ export class SessionEngine {
2374
2733
  sections += `\n\n${buildCloudMemorySection(this.project.name)}`;
2375
2734
  }
2376
2735
  turnPrompt = `${preamble}${sections}\n\n${turnPrompt}`;
2736
+ } else if (runtime === "local" && authoredRoot) {
2737
+ // Virtual roots skip materialize, so AGENTS.md never lands on disk.
2738
+ // Ship the same preamble the borrowed-cwd local path uses.
2739
+ const preamble = buildAgentsMdContent(
2740
+ { ...agent, tools: [...agent.tools, ...synthesized] },
2741
+ { includeScripts: true }
2742
+ );
2743
+ const workspaceInfo = `<user_info>\nWorkspace Path: /\n</user_info>`;
2744
+ turnPrompt =
2745
+ preamble === ""
2746
+ ? `${workspaceInfo}\n\n${turnPrompt}`
2747
+ : `${workspaceInfo}\n\n${preamble}\n\n${turnPrompt}`;
2377
2748
  } else if (runtime === "local" && instructionsInvisibleToHarness) {
2378
2749
  // Local workspace whose AGENTS.md the harness will not load. With a
2379
2750
  // borrowed harness cwd, agent tool scripts live only in the durable
@@ -2392,9 +2763,18 @@ export class SessionEngine {
2392
2763
  ? {}
2393
2764
  : { sdkAgentId: record.sdkAgentId }),
2394
2765
  workspaceDir: this.harnessCwd(record),
2395
- sandbox: this.project.agent.local?.sandbox === true,
2396
- ...(runtime === "local" && agent.allowedTools !== undefined
2397
- ? { tools: agent.allowedTools }
2766
+ sandbox: authoredRoot
2767
+ ? false
2768
+ : this.project.agent.local?.sandbox === true,
2769
+ ...(runtime === "local" && harnessTools !== undefined
2770
+ ? {
2771
+ ...(harnessTools.tools === undefined
2772
+ ? {}
2773
+ : { tools: harnessTools.tools }),
2774
+ ...(harnessTools.disallowedTools === undefined
2775
+ ? {}
2776
+ : { disallowedTools: harnessTools.disallowedTools }),
2777
+ }
2398
2778
  : {}),
2399
2779
  runnerStateDir: join(this.stateRoot, "runner"),
2400
2780
  prompt: turnPrompt,
@@ -2418,7 +2798,13 @@ export class SessionEngine {
2418
2798
  : {}),
2419
2799
  customTools:
2420
2800
  runtime === "local"
2421
- ? await this.buildCustomTools(record, agent, turnId, emit)
2801
+ ? await this.buildCustomTools(
2802
+ record,
2803
+ agent,
2804
+ turnId,
2805
+ emit,
2806
+ synthesized
2807
+ )
2422
2808
  : {},
2423
2809
  mcpServers: this.buildMcpServers(
2424
2810
  runtime,
@@ -2712,7 +3098,8 @@ export class SessionEngine {
2712
3098
  record: SessionRecord,
2713
3099
  agent: ResolvedAgent,
2714
3100
  turnId: string,
2715
- emit: (payload: SessionEventPayload) => void
3101
+ emit: (payload: SessionEventPayload) => void,
3102
+ extraTools: DiscoveredTool[] = []
2716
3103
  ): Promise<Record<string, SDKCustomTool>> {
2717
3104
  const tools: Record<string, SDKCustomTool> = {};
2718
3105
  const register = (tool: ResolvedAgent["tools"][number]): void => {
@@ -2742,7 +3129,7 @@ export class SessionEngine {
2742
3129
  },
2743
3130
  };
2744
3131
  };
2745
- for (const tool of agent.tools) {
3132
+ for (const tool of [...agent.tools, ...extraTools]) {
2746
3133
  if (tool.execution !== "server") {
2747
3134
  continue;
2748
3135
  }
@@ -3682,6 +4069,27 @@ export class SessionEngine {
3682
4069
  const session = identity.session;
3683
4070
  let workspaceDir = identity.workspaceDir;
3684
4071
  let scratchDir = identity.scratchDir;
4072
+ if (
4073
+ isFileSystemToolAlias(toolName) &&
4074
+ identity.busy &&
4075
+ record !== undefined &&
4076
+ isMutatingFileSystemTool(toolName)
4077
+ ) {
4078
+ throw new SessionBusyError(record.sessionId);
4079
+ }
4080
+ const native = await this.nativeFileSystemOutcome({
4081
+ toolName,
4082
+ input,
4083
+ callId,
4084
+ sessionId: record?.sessionId,
4085
+ continuationKey:
4086
+ record?.continuationKey ?? session.continuationKey ?? undefined,
4087
+ cwd: workspaceDir,
4088
+ dryRun: record?.dryRun === true,
4089
+ });
4090
+ if (native !== undefined) {
4091
+ return native;
4092
+ }
3685
4093
  let tool = this.project.agent.tools.find((t) => t.name === toolName);
3686
4094
  if (
3687
4095
  tool === undefined &&
@@ -3783,7 +4191,10 @@ export class SessionEngine {
3783
4191
  record !== undefined &&
3784
4192
  identity.busy &&
3785
4193
  this.materializedWorkspaces.has(record.sessionId);
3786
- if (record === undefined || workspaceUnavailable || !turnMaterialized) {
4194
+ if (
4195
+ !this.hasRootOverride() &&
4196
+ (record === undefined || workspaceUnavailable || !turnMaterialized)
4197
+ ) {
3787
4198
  await materializeWorkspace({
3788
4199
  agent: this.project.agent,
3789
4200
  workspaceDir:
@@ -4745,9 +5156,8 @@ export class SessionEngine {
4745
5156
  }
4746
5157
  const record = await this.sessions.get(sessionId);
4747
5158
  const sdkAgentId =
4748
- (record?.sdkAgentId !== undefined && record.sdkAgentId.startsWith("bc-")
4749
- ? record.sdkAgentId
4750
- : undefined) ?? cloudSdkAgentIdFromEvents(events);
5159
+ (record?.sdkAgentId?.startsWith("bc-") ? record.sdkAgentId : undefined) ??
5160
+ cloudSdkAgentIdFromEvents(events);
4751
5161
  if (sdkAgentId === undefined) {
4752
5162
  return;
4753
5163
  }