@cursor/july 0.1.103 → 0.1.105

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 (264) 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/deployments/deployments-channel.d.ts.map +1 -1
  11. package/dist/channels/deployments/deployments-channel.js +1 -0
  12. package/dist/channels/deployments/types.d.ts +2 -0
  13. package/dist/channels/deployments/types.d.ts.map +1 -1
  14. package/dist/channels/github/defaults.d.ts.map +1 -1
  15. package/dist/channels/github/defaults.js +4 -0
  16. package/dist/channels/github/progress.d.ts +22 -0
  17. package/dist/channels/github/progress.d.ts.map +1 -1
  18. package/dist/channels/github/progress.js +32 -5
  19. package/dist/channels/issues/index.d.ts +4 -0
  20. package/dist/channels/issues/index.d.ts.map +1 -0
  21. package/dist/channels/issues/index.js +2 -0
  22. package/dist/channels/issues/issues-channel.d.ts +5 -0
  23. package/dist/channels/issues/issues-channel.d.ts.map +1 -0
  24. package/dist/channels/issues/issues-channel.js +37 -0
  25. package/dist/channels/issues/types.d.ts +33 -0
  26. package/dist/channels/issues/types.d.ts.map +1 -0
  27. package/dist/channels/issues/types.js +1 -0
  28. package/dist/docs/404.html +2 -2
  29. package/dist/docs/ab.html +4 -4
  30. package/dist/docs/assets/{app.DW9pXkpL.js → app.BVwaffdK.js} +1 -1
  31. package/dist/docs/assets/chunks/@localSearchIndexroot.CJxMklAy.js +1 -0
  32. package/dist/docs/assets/chunks/{VPLocalSearchBox.CdEIUwDg.js → VPLocalSearchBox.WuWTZ1x8.js} +1 -1
  33. package/dist/docs/assets/chunks/{theme.YZMHgPgg.js → theme.BYdI_6sF.js} +2 -2
  34. package/dist/docs/assets/{guides_github.md.DKYiT9do.js → guides_github.md.c0gdGJ-D.js} +1 -1
  35. package/dist/docs/assets/{guides_github.md.DKYiT9do.lean.js → guides_github.md.c0gdGJ-D.lean.js} +1 -1
  36. package/dist/docs/assets/guides_grokbot-agents.md.DVqdhBKb.js +9 -0
  37. package/dist/docs/assets/guides_grokbot-agents.md.DVqdhBKb.lean.js +1 -0
  38. package/dist/docs/assets/index.md.BW_6tOgR.js +5 -0
  39. package/dist/docs/assets/index.md.BW_6tOgR.lean.js +1 -0
  40. package/dist/docs/assets/{reference_channels.md.Dz8Cnudp.js → reference_channels.md.DBGcaB7m.js} +2 -2
  41. package/dist/docs/assets/{reference_channels.md.Dz8Cnudp.lean.js → reference_channels.md.DBGcaB7m.lean.js} +1 -1
  42. package/dist/docs/assets/{reference_cli.md.CJNc_xYK.js → reference_cli.md.C6Lo8SEd.js} +2 -1
  43. package/dist/docs/assets/{reference_extensions.md.DykHW3qL.js → reference_extensions.md.CGmMLblt.js} +1 -1
  44. package/dist/docs/assets/templates_grokbot-agents.md.c1s6Flp9.js +3 -0
  45. package/dist/docs/assets/templates_grokbot-agents.md.c1s6Flp9.lean.js +1 -0
  46. package/dist/docs/building-with-agents.html +4 -4
  47. package/dist/docs/concepts.html +4 -4
  48. package/dist/docs/deployment.html +4 -4
  49. package/dist/docs/design/filesystem.md +376 -0
  50. package/dist/docs/evals.html +4 -4
  51. package/dist/docs/guides/agent-to-agent.html +4 -4
  52. package/dist/docs/guides/cloud-agents.html +5 -5
  53. package/dist/docs/guides/convert-automation.html +4 -4
  54. package/dist/docs/guides/github.html +6 -6
  55. package/dist/docs/guides/github.md +5 -0
  56. package/dist/docs/guides/grokbot-agents.html +35 -0
  57. package/dist/docs/guides/grokbot-agents.md +88 -0
  58. package/dist/docs/guides/human-in-the-loop.html +4 -4
  59. package/dist/docs/guides/mcp-oauth.html +4 -4
  60. package/dist/docs/guides/opentelemetry.html +5 -5
  61. package/dist/docs/guides/slack.html +4 -4
  62. package/dist/docs/guides/webhooks.html +4 -4
  63. package/dist/docs/hashmap.json +1 -1
  64. package/dist/docs/hillclimbing.html +4 -4
  65. package/dist/docs/index.html +6 -6
  66. package/dist/docs/index.md +6 -1
  67. package/dist/docs/llms-full.txt +562 -2
  68. package/dist/docs/llms.txt +3 -0
  69. package/dist/docs/quickstart.html +4 -4
  70. package/dist/docs/reference/agent-config.html +4 -4
  71. package/dist/docs/reference/artifacts.html +4 -4
  72. package/dist/docs/reference/channels.html +6 -6
  73. package/dist/docs/reference/channels.md +20 -0
  74. package/dist/docs/reference/cli.html +7 -6
  75. package/dist/docs/reference/cli.md +2 -1
  76. package/dist/docs/reference/connections.html +4 -4
  77. package/dist/docs/reference/extensions.html +6 -6
  78. package/dist/docs/reference/extensions.md +2 -0
  79. package/dist/docs/reference/hooks.html +4 -4
  80. package/dist/docs/reference/http-api.html +4 -4
  81. package/dist/docs/reference/instructions.html +4 -4
  82. package/dist/docs/reference/playground.html +4 -4
  83. package/dist/docs/reference/project-layout.html +4 -4
  84. package/dist/docs/reference/prompt.html +4 -4
  85. package/dist/docs/reference/result.html +4 -4
  86. package/dist/docs/reference/schedules.html +4 -4
  87. package/dist/docs/reference/sessions.html +4 -4
  88. package/dist/docs/reference/skills.html +4 -4
  89. package/dist/docs/reference/subagents.html +4 -4
  90. package/dist/docs/reference/tools.html +4 -4
  91. package/dist/docs/scaffolding-agents.html +4 -4
  92. package/dist/docs/skills/ab.html +4 -4
  93. package/dist/docs/skills/create-agent.html +4 -4
  94. package/dist/docs/skills/debug.html +4 -4
  95. package/dist/docs/skills/evals.html +4 -4
  96. package/dist/docs/skills/framework-map.html +4 -4
  97. package/dist/docs/skills/github.html +4 -4
  98. package/dist/docs/skills/hillclimb.html +4 -4
  99. package/dist/docs/skills/index.html +4 -4
  100. package/dist/docs/skills/mcp-auth.html +4 -4
  101. package/dist/docs/skills/otel.html +4 -4
  102. package/dist/docs/skills/setup-slack.html +4 -4
  103. package/dist/docs/storage.html +4 -4
  104. package/dist/docs/templates/agentic-owners.html +4 -4
  105. package/dist/docs/templates/agents-md.html +4 -4
  106. package/dist/docs/templates/code-wiki.html +5 -5
  107. package/dist/docs/templates/demo.html +5 -5
  108. package/dist/docs/templates/grokbot-agents.html +29 -0
  109. package/dist/docs/templates/grokbot-agents.md +48 -0
  110. package/dist/docs/templates/pr-autofixer.html +4 -4
  111. package/dist/docs/templates/security-help.html +4 -4
  112. package/dist/docs/templates/security-reviewer.html +4 -4
  113. package/dist/docs/templates/triage.html +4 -4
  114. package/dist/docs/troubleshooting.html +4 -4
  115. package/dist/files.d.ts +7 -2
  116. package/dist/files.d.ts.map +1 -1
  117. package/dist/files.js +7 -2
  118. package/dist/filesystem.d.ts +440 -0
  119. package/dist/filesystem.d.ts.map +1 -0
  120. package/dist/filesystem.js +1694 -0
  121. package/dist/index.d.ts +2 -0
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +1 -0
  124. package/dist/internal/continuation-channel.d.ts +4 -2
  125. package/dist/internal/continuation-channel.d.ts.map +1 -1
  126. package/dist/internal/continuation-channel.js +21 -12
  127. package/dist/internal/continuation-identity.d.ts +13 -5
  128. package/dist/internal/continuation-identity.d.ts.map +1 -1
  129. package/dist/internal/continuation-identity.js +74 -35
  130. package/dist/internal/cursor-agent-template.d.ts +1 -1
  131. package/dist/internal/cursor-agent-template.d.ts.map +1 -1
  132. package/dist/internal/cursor-agent-template.js +1 -0
  133. package/dist/internal/discovery/agent.d.ts.map +1 -1
  134. package/dist/internal/discovery/agent.js +80 -8
  135. package/dist/internal/discovery/info.d.ts.map +1 -1
  136. package/dist/internal/discovery/info.js +3 -0
  137. package/dist/internal/factory-resource-channel.d.ts +33 -0
  138. package/dist/internal/factory-resource-channel.d.ts.map +1 -0
  139. package/dist/internal/factory-resource-channel.js +77 -0
  140. package/dist/internal/filesystem/errors.d.ts +64 -0
  141. package/dist/internal/filesystem/errors.d.ts.map +1 -0
  142. package/dist/internal/filesystem/errors.js +175 -0
  143. package/dist/internal/filesystem/format.d.ts +28 -0
  144. package/dist/internal/filesystem/format.d.ts.map +1 -0
  145. package/dist/internal/filesystem/format.js +213 -0
  146. package/dist/internal/filesystem/merge.d.ts +34 -0
  147. package/dist/internal/filesystem/merge.d.ts.map +1 -0
  148. package/dist/internal/filesystem/merge.js +196 -0
  149. package/dist/internal/filesystem/scratch.d.ts +59 -0
  150. package/dist/internal/filesystem/scratch.d.ts.map +1 -0
  151. package/dist/internal/filesystem/scratch.js +212 -0
  152. package/dist/internal/filesystem/sdk-schemas.d.ts +29 -0
  153. package/dist/internal/filesystem/sdk-schemas.d.ts.map +1 -0
  154. package/dist/internal/filesystem/sdk-schemas.js +153 -0
  155. package/dist/internal/filesystem/tools.d.ts +42 -0
  156. package/dist/internal/filesystem/tools.d.ts.map +1 -0
  157. package/dist/internal/filesystem/tools.js +618 -0
  158. package/dist/internal/filesystem/walk.d.ts +74 -0
  159. package/dist/internal/filesystem/walk.d.ts.map +1 -0
  160. package/dist/internal/filesystem/walk.js +339 -0
  161. package/dist/internal/framework-file-storage.d.ts +3 -1
  162. package/dist/internal/framework-file-storage.d.ts.map +1 -1
  163. package/dist/internal/framework-file-storage.js +3 -1
  164. package/dist/internal/guest-network.d.ts +5 -2
  165. package/dist/internal/guest-network.d.ts.map +1 -1
  166. package/dist/internal/guest-network.js +7 -2
  167. package/dist/internal/hosted-delivery.d.ts +7 -0
  168. package/dist/internal/hosted-delivery.d.ts.map +1 -1
  169. package/dist/internal/hosted-delivery.js +41 -3
  170. package/dist/internal/hosted-managed-automation.d.ts.map +1 -1
  171. package/dist/internal/hosted-managed-automation.js +27 -2
  172. package/dist/internal/init-project.d.ts.map +1 -1
  173. package/dist/internal/init-project.js +4 -1
  174. package/dist/internal/sdk-runner.d.ts +13 -1
  175. package/dist/internal/sdk-runner.d.ts.map +1 -1
  176. package/dist/internal/sdk-runner.js +21 -1
  177. package/dist/internal/server.d.ts.map +1 -1
  178. package/dist/internal/server.js +11 -3
  179. package/dist/internal/session-engine.d.ts +30 -0
  180. package/dist/internal/session-engine.d.ts.map +1 -1
  181. package/dist/internal/session-engine.js +484 -82
  182. package/dist/internal/store-mirror.d.ts +2 -2
  183. package/dist/internal/store-mirror.js +2 -2
  184. package/dist/internal/tenant-slack/relay.d.ts +2 -0
  185. package/dist/internal/tenant-slack/relay.d.ts.map +1 -1
  186. package/dist/internal/tenant-slack/relay.js +18 -8
  187. package/dist/internal/testing/factory-resource-fixtures.d.ts +48 -0
  188. package/dist/internal/testing/factory-resource-fixtures.d.ts.map +1 -0
  189. package/dist/internal/testing/factory-resource-fixtures.js +41 -0
  190. package/dist/internal/workspace-sync.d.ts +31 -6
  191. package/dist/internal/workspace-sync.d.ts.map +1 -1
  192. package/dist/internal/workspace-sync.js +67 -12
  193. package/dist/internal/workspace.d.ts +9 -0
  194. package/dist/internal/workspace.d.ts.map +1 -1
  195. package/dist/internal/workspace.js +16 -0
  196. package/dist/playground/assets/{index-59KvgREd.js → index-BaQzjaNK.js} +40 -40
  197. package/dist/playground/assets/index-CceqnLTX.css +1 -0
  198. package/dist/playground/index.html +2 -2
  199. package/dist/types.d.ts +60 -11
  200. package/dist/types.d.ts.map +1 -1
  201. package/docs/README.md +6 -1
  202. package/docs/design/filesystem.md +376 -0
  203. package/docs/guides/github.md +5 -0
  204. package/docs/guides/grokbot-agents.md +93 -0
  205. package/docs/reference/channels.md +20 -0
  206. package/docs/reference/cli.md +2 -1
  207. package/docs/reference/extensions.md +2 -0
  208. package/docs/templates/grokbot-agents.md +53 -0
  209. package/package.json +25 -2
  210. package/src/channels/change-monitors/change-monitors-channel.ts +62 -0
  211. package/src/channels/change-monitors/index.ts +8 -0
  212. package/src/channels/change-monitors/types.ts +44 -0
  213. package/src/channels/deployments/deployments-channel.ts +1 -0
  214. package/src/channels/deployments/types.ts +2 -0
  215. package/src/channels/github/defaults.ts +4 -0
  216. package/src/channels/github/progress.ts +76 -10
  217. package/src/channels/issues/index.ts +9 -0
  218. package/src/channels/issues/issues-channel.ts +51 -0
  219. package/src/channels/issues/types.ts +41 -0
  220. package/src/files.ts +9 -2
  221. package/src/filesystem.ts +2074 -0
  222. package/src/index.ts +28 -0
  223. package/src/internal/continuation-channel.ts +29 -12
  224. package/src/internal/continuation-identity.ts +122 -37
  225. package/src/internal/cursor-agent-template.ts +1 -0
  226. package/src/internal/discovery/agent.ts +87 -1
  227. package/src/internal/discovery/info.ts +3 -0
  228. package/src/internal/factory-resource-channel.ts +122 -0
  229. package/src/internal/filesystem/errors.ts +220 -0
  230. package/src/internal/filesystem/format.ts +276 -0
  231. package/src/internal/filesystem/merge.ts +195 -0
  232. package/src/internal/filesystem/scratch.ts +255 -0
  233. package/src/internal/filesystem/sdk-schemas.ts +204 -0
  234. package/src/internal/filesystem/tools.ts +821 -0
  235. package/src/internal/filesystem/walk.ts +369 -0
  236. package/src/internal/framework-file-storage.ts +3 -1
  237. package/src/internal/guest-network.ts +8 -2
  238. package/src/internal/hosted-delivery.ts +71 -2
  239. package/src/internal/hosted-managed-automation.ts +49 -5
  240. package/src/internal/init-project.ts +4 -1
  241. package/src/internal/sdk-runner.ts +23 -0
  242. package/src/internal/server.ts +16 -0
  243. package/src/internal/session-engine.ts +595 -73
  244. package/src/internal/store-mirror.ts +2 -2
  245. package/src/internal/tenant-slack/relay.ts +28 -4
  246. package/src/internal/testing/factory-resource-fixtures.ts +85 -0
  247. package/src/internal/workspace-sync.ts +84 -12
  248. package/src/internal/workspace.ts +20 -0
  249. package/src/types.ts +58 -11
  250. package/templates/grokbot-agents/README.md +26 -0
  251. package/templates/grokbot-agents/agent/agent.ts +6 -0
  252. package/templates/grokbot-agents/agent/extensions/grokbot.ts +5 -0
  253. package/templates/grokbot-agents/agent/instructions.md +7 -0
  254. package/templates/grokbot-agents/evals/evals.config.ts +5 -0
  255. package/templates/grokbot-agents/evals/grokbot-agents.eval.ts +18 -0
  256. package/templates/grokbot-agents/init.json +18 -0
  257. package/templates/grokbot-agents/package.json +17 -0
  258. package/templates/grokbot-agents/tsconfig.json +12 -0
  259. package/dist/docs/assets/chunks/@localSearchIndexroot.nrl1ifBq.js +0 -1
  260. package/dist/docs/assets/index.md.D75GWhPl.js +0 -5
  261. package/dist/docs/assets/index.md.D75GWhPl.lean.js +0 -1
  262. package/dist/playground/assets/index-MbMxXg-y.css +0 -1
  263. /package/dist/docs/assets/{reference_cli.md.CJNc_xYK.lean.js → reference_cli.md.C6Lo8SEd.lean.js} +0 -0
  264. /package/dist/docs/assets/{reference_extensions.md.DykHW3qL.lean.js → reference_extensions.md.CGmMLblt.lean.js} +0 -0
@@ -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";
@@ -179,14 +202,19 @@ import { resolveAgentV2Hosting } from "./v2-hosting.js";
179
202
  import {
180
203
  buildAgentsMdContent,
181
204
  buildCloudMemorySection,
205
+ buildCloudSessionWorkspaceSection,
182
206
  buildCloudWorkspaceSection,
183
207
  ensureOwnGitRoot,
184
208
  isNestedInGitRepo,
185
209
  materializeWorkspace,
186
210
  SERVER_TOOLS_MCP_SERVER_NAME,
187
- writeWorkspaceFiles,
188
211
  } from "./workspace.js";
189
- import { syncWorkspaceToStore } from "./workspace-sync.js";
212
+ import {
213
+ syncSessionWorkspaceFiles,
214
+ syncWorkspaceToStore,
215
+ WorkspaceFileRefusal,
216
+ wipeSessionWorkspace,
217
+ } from "./workspace-sync.js";
190
218
 
191
219
  export const HTTP_CHANNEL_ID = "http";
192
220
  export { SCHEDULE_CHANNEL_PREFIX } from "./prompt-context.js";
@@ -200,6 +228,7 @@ interface CloudStoreTarget {
200
228
  mountPath: string;
201
229
  skillsPrefix: string;
202
230
  workspacePrefix: string;
231
+ sessionWorkspacePrefix: (sessionId: string) => string;
203
232
  label: string;
204
233
  }
205
234
 
@@ -510,6 +539,15 @@ export class SessionEngine {
510
539
  private readonly cloudCostBackfillDone = new Set<string>();
511
540
  /** Deduped in-flight cloud cost backfills, keyed by session id. */
512
541
  private readonly cloudCostBackfill = new Map<string, Promise<void>>();
542
+ /** Per-session FileSystem handle cache (not a tree snapshot). */
543
+ private readonly sessionRoots = new Map<string, FileSystem>();
544
+ /** In-flight factory resolves, so the first turn and a racing callTool share one handle. */
545
+ private readonly sessionRootInflight = new Map<string, Promise<FileSystem>>();
546
+ /** Per-turn `unionFs({ "/": authored, "/tmp": tempFs() })`. Dropped at end of turn. */
547
+ private readonly turnRoots = new Map<
548
+ string,
549
+ { root: FileSystem; cwd: string }
550
+ >();
513
551
 
514
552
  constructor(options: SessionEngineOptions) {
515
553
  this.project = options.project;
@@ -1793,6 +1831,261 @@ export class SessionEngine {
1793
1831
  return this.interrupt(existing.sessionId);
1794
1832
  }
1795
1833
 
1834
+ private hasRootOverride(): boolean {
1835
+ return this.project.agent.root !== undefined;
1836
+ }
1837
+
1838
+ /**
1839
+ * FileSystem for this turn or `callTool`. Authored `root` (static or
1840
+ * factory) is cached per session when `cache` is set; unscoped
1841
+ * `callTool` passes the invocation id and does not cache, so factories
1842
+ * cannot share one tree across callers. A static handle is used as
1843
+ * authored — every session sees that same object. Host-default agents
1844
+ * get `localFileSystem` against the harness cwd — same path verbs as
1845
+ * the Cursor SDK natives, always through the synthesized tools.
1846
+ */
1847
+ private async resolveFileSystem(options: {
1848
+ sessionId: string;
1849
+ continuationKey?: string;
1850
+ cache: boolean;
1851
+ cwd: string;
1852
+ }): Promise<FileSystem> {
1853
+ const authored = this.project.agent.root;
1854
+ if (authored === undefined) {
1855
+ return localFileSystem({ cwd: options.cwd });
1856
+ }
1857
+ const authoredRoot = await this.resolveAuthoredFileSystem(options);
1858
+ if (options.cache) {
1859
+ const existing = this.turnRoots.get(options.sessionId);
1860
+ if (existing !== undefined) {
1861
+ return existing.root;
1862
+ }
1863
+ }
1864
+ const root = unionFs({
1865
+ "/": authoredRoot,
1866
+ "/tmp": tempFs(),
1867
+ });
1868
+ if (options.cache && this.isTurnRunning(options.sessionId)) {
1869
+ this.turnRoots.set(options.sessionId, { root, cwd: options.cwd });
1870
+ }
1871
+ return root;
1872
+ }
1873
+
1874
+ private async resolveAuthoredFileSystem(options: {
1875
+ sessionId: string;
1876
+ continuationKey?: string;
1877
+ cache: boolean;
1878
+ }): Promise<FileSystem> {
1879
+ const authored = this.project.agent.root;
1880
+ if (authored === undefined) {
1881
+ throw new Error("resolveAuthoredFileSystem requires an authored root");
1882
+ }
1883
+ if (options.cache) {
1884
+ const cached = this.sessionRoots.get(options.sessionId);
1885
+ if (cached !== undefined) {
1886
+ return cached;
1887
+ }
1888
+ const inflight = this.sessionRootInflight.get(options.sessionId);
1889
+ if (inflight !== undefined) {
1890
+ return inflight;
1891
+ }
1892
+ }
1893
+ const resolve = async (): Promise<FileSystem> => {
1894
+ if (typeof authored !== "function") {
1895
+ return authored;
1896
+ }
1897
+ const host = this.hostForSession(
1898
+ options.cache ? options.sessionId : undefined
1899
+ );
1900
+ const ctx: RootContext = {
1901
+ sessionId: options.sessionId,
1902
+ agentName: this.project.agent.name,
1903
+ ...(options.continuationKey === undefined
1904
+ ? {}
1905
+ : { continuationKey: options.continuationKey }),
1906
+ host: { files: host.files, reminders: host.reminders },
1907
+ };
1908
+ return await authored(ctx);
1909
+ };
1910
+ if (!options.cache) {
1911
+ return resolve();
1912
+ }
1913
+ const pending = resolve();
1914
+ this.sessionRootInflight.set(options.sessionId, pending);
1915
+ try {
1916
+ const resolved = await pending;
1917
+ this.sessionRoots.set(options.sessionId, resolved);
1918
+ return resolved;
1919
+ } finally {
1920
+ this.sessionRootInflight.delete(options.sessionId);
1921
+ }
1922
+ }
1923
+
1924
+ private dropTurnFileSystem(sessionId: string): void {
1925
+ this.turnRoots.delete(sessionId);
1926
+ }
1927
+
1928
+ private async prepareScratchToolInput(options: {
1929
+ sessionId?: string;
1930
+ input: Record<string, unknown>;
1931
+ }): Promise<Record<string, unknown>> {
1932
+ if (options.sessionId === undefined) {
1933
+ return options.input;
1934
+ }
1935
+ const turn = this.turnRoots.get(options.sessionId);
1936
+ if (turn === undefined) {
1937
+ return options.input;
1938
+ }
1939
+ await ingestHostAgentToolsScratch({
1940
+ cwd: turn.cwd,
1941
+ root: turn.root,
1942
+ });
1943
+ return remapScratchPaths(options.input, turn.cwd);
1944
+ }
1945
+
1946
+ private withTurnScratchTools(
1947
+ tools: DiscoveredTool[],
1948
+ sessionId: string
1949
+ ): DiscoveredTool[] {
1950
+ const turn = this.turnRoots.get(sessionId);
1951
+ if (turn === undefined) {
1952
+ return tools;
1953
+ }
1954
+ return tools.map((tool) => {
1955
+ const execute = tool.definition.execute;
1956
+ if (typeof execute !== "function") {
1957
+ return tool;
1958
+ }
1959
+ return {
1960
+ ...tool,
1961
+ definition: {
1962
+ ...tool.definition,
1963
+ execute: async (input, ctx) => {
1964
+ const remapped = await this.prepareScratchToolInput({
1965
+ sessionId,
1966
+ input:
1967
+ input !== null &&
1968
+ typeof input === "object" &&
1969
+ !Array.isArray(input)
1970
+ ? (input as Record<string, unknown>)
1971
+ : {},
1972
+ });
1973
+ return execute(remapped as never, ctx);
1974
+ },
1975
+ },
1976
+ };
1977
+ });
1978
+ }
1979
+
1980
+ private async nativeFileSystemOutcome(options: {
1981
+ toolName: string;
1982
+ input: unknown;
1983
+ callId: string;
1984
+ sessionId?: string;
1985
+ continuationKey?: string;
1986
+ cwd: string;
1987
+ dryRun?: boolean;
1988
+ }): Promise<ToolCallOutcome | undefined> {
1989
+ const { toolName, input, callId } = options;
1990
+ if (!isFileSystemToolAlias(toolName)) {
1991
+ return undefined;
1992
+ }
1993
+ // Same policy the authored callTool path asks: a dry-run write is
1994
+ // answered by the host. Native edit/delete used to return before
1995
+ // decideToolCall and persist through the FileSystem.
1996
+ const decision = decideToolCall({
1997
+ dryRun: options.dryRun === true,
1998
+ effect: isMutatingFileSystemTool(toolName) ? "write" : "read",
1999
+ input,
2000
+ });
2001
+ if (decision.kind === "answer") {
2002
+ return {
2003
+ callId,
2004
+ toolName,
2005
+ isError: false,
2006
+ result: decision.result,
2007
+ durationMs: 0,
2008
+ sessionId: options.sessionId,
2009
+ };
2010
+ }
2011
+ const root = await this.resolveFileSystem({
2012
+ sessionId: options.sessionId ?? callId,
2013
+ continuationKey: options.continuationKey,
2014
+ cache: options.sessionId !== undefined,
2015
+ cwd: options.cwd,
2016
+ });
2017
+ const rawArgs =
2018
+ input !== null && typeof input === "object" && !Array.isArray(input)
2019
+ ? (input as Record<string, unknown>)
2020
+ : {};
2021
+ const args = await this.prepareScratchToolInput({
2022
+ sessionId: options.sessionId,
2023
+ input: rawArgs,
2024
+ });
2025
+ const tool = fileSystemTool(root, toolName, args);
2026
+ const execute = tool?.definition.execute;
2027
+ if (tool === undefined || typeof execute !== "function") {
2028
+ // A resolved root owns this native name. Falling through would let
2029
+ // an authored lookalike run, or surface UnknownToolError after we
2030
+ // already advertised the FileSystem tool.
2031
+ return {
2032
+ callId,
2033
+ toolName,
2034
+ isError: true,
2035
+ result: {
2036
+ status: "error",
2037
+ error: { message: `FileSystem cannot handle ${toolName}` },
2038
+ },
2039
+ durationMs: 0,
2040
+ sessionId: options.sessionId,
2041
+ errorMessage: `FileSystem cannot handle ${toolName}`,
2042
+ };
2043
+ }
2044
+ const validation = validateToolInput(tool.definition.inputSchema, args);
2045
+ if (!validation.ok) {
2046
+ return {
2047
+ callId,
2048
+ toolName,
2049
+ isError: true,
2050
+ result: {
2051
+ status: "error",
2052
+ error: { message: validation.message },
2053
+ },
2054
+ durationMs: 0,
2055
+ sessionId: options.sessionId,
2056
+ errorMessage: validation.message,
2057
+ };
2058
+ }
2059
+ try {
2060
+ const value = await execute(
2061
+ validation.value as never,
2062
+ {
2063
+ toolCallId: callId,
2064
+ workspaceDir: options.cwd,
2065
+ } as never
2066
+ );
2067
+ return {
2068
+ callId,
2069
+ toolName,
2070
+ isError: false,
2071
+ result: normalizeToolResult(value),
2072
+ durationMs: 0,
2073
+ sessionId: options.sessionId,
2074
+ };
2075
+ } catch (error) {
2076
+ const message = error instanceof Error ? error.message : String(error);
2077
+ return {
2078
+ callId,
2079
+ toolName,
2080
+ isError: true,
2081
+ result: { status: "error", error: { message } },
2082
+ durationMs: 0,
2083
+ sessionId: options.sessionId,
2084
+ errorMessage: message,
2085
+ };
2086
+ }
2087
+ }
2088
+
1796
2089
  /**
1797
2090
  * Local harness cwd for a session: per-send override, else the shared
1798
2091
  * `agent.local.workspaceDir`, else `<agent.local.cwd>/<sessionId>`, else
@@ -1840,19 +2133,22 @@ export class SessionEngine {
1840
2133
  if (
1841
2134
  workspaceDir === undefined ||
1842
2135
  agent.runtime !== "local" ||
2136
+ this.hasRootOverride() ||
1843
2137
  this.runner.prewarm === undefined
1844
2138
  ) {
1845
2139
  return;
1846
2140
  }
1847
2141
  const startedAt = Date.now();
1848
2142
  try {
2143
+ const harness = resolveHostHarnessToolOptions(agent.allowedTools);
1849
2144
  const outcome = await this.runner.prewarm({
1850
2145
  workspaceDir,
1851
2146
  runnerStateDir: join(this.stateRoot, "runner"),
1852
2147
  sandbox: agent.local?.sandbox === true,
1853
- ...(agent.allowedTools === undefined
2148
+ ...(harness.tools === undefined ? {} : { tools: harness.tools }),
2149
+ ...(harness.disallowedTools === undefined
1854
2150
  ? {}
1855
- : { tools: agent.allowedTools }),
2151
+ : { disallowedTools: harness.disallowedTools }),
1856
2152
  // No session yet, so auth'd connections are omitted rather than
1857
2153
  // warmed without an identity; the per-send flag still has to match
1858
2154
  // the turns' requests or the warmed executor is one no turn asks for.
@@ -1942,41 +2238,53 @@ export class SessionEngine {
1942
2238
  // cwd is overridden (e.g. a PR worktree). A/B variant instruction
1943
2239
  // overlays are applied via agentForSession.
1944
2240
  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);
2241
+ if (!this.hasRootOverride()) {
2242
+ await materializeWorkspace({
2243
+ agent,
2244
+ workspaceDir,
2245
+ seed: isFirstTurn,
2246
+ stateRoot: this.stateRoot,
2247
+ });
2248
+ this.materializedWorkspaces.add(sessionId);
2249
+ // Must precede the carrier: the harness resolves rules, skills,
2250
+ // `mcp.json` and hooks from its git root, so a workspace nested in an
2251
+ // outer repo would load that repo's hooks instead of the ones written
2252
+ // just below. Only for directories this session owns — a borrowed
2253
+ // checkout or worktree keeps its own root.
2254
+ if (!this.hasBorrowedWorkspace(record)) {
2255
+ await ensureOwnGitRoot(harnessCwd);
2256
+ }
2257
+ await this.materializeNudgeCarrier(record, harnessCwd);
1959
2258
  }
1960
- await this.materializeNudgeCarrier(record, harnessCwd);
1961
2259
  // Baseline before the agent can run, so a follow-up that arrives and
1962
2260
  // drains before the first tool boundary still counts as an increase.
1963
2261
  await this.baselineDeliveredNudges(record);
1964
2262
  const pendingFiles = record.pendingWorkspaceFiles;
1965
2263
  const pendingImages = record.pendingImages;
1966
- if (
1967
- pendingFiles !== undefined &&
1968
- Object.keys(pendingFiles).length > 0
1969
- ) {
1970
- await writeWorkspaceFiles(harnessCwd, pendingFiles);
2264
+ const runtime = resolveSessionRuntime({
2265
+ agentRuntime: agent.runtime,
2266
+ cloudOverride: record.cloudOverride,
2267
+ });
2268
+ const hasPendingFiles =
2269
+ pendingFiles !== undefined && Object.keys(pendingFiles).length > 0;
2270
+ if (hasPendingFiles && runtime !== "cloud") {
2271
+ const root = await this.resolveFileSystem({
2272
+ sessionId,
2273
+ continuationKey: record.continuationKey ?? undefined,
2274
+ cache: true,
2275
+ cwd: harnessCwd,
2276
+ });
2277
+ await writeWorkspaceFilesToRoot(root, pendingFiles);
1971
2278
  }
1972
2279
  if (
1973
- (pendingFiles !== undefined &&
1974
- Object.keys(pendingFiles).length > 0) ||
2280
+ (hasPendingFiles && runtime !== "cloud") ||
1975
2281
  (pendingImages !== undefined && pendingImages.length > 0)
1976
2282
  ) {
1977
2283
  await this.updateRecord(sessionId, (current) => ({
1978
2284
  ...current,
1979
2285
  pendingWorkspaceFiles:
2286
+ hasPendingFiles &&
2287
+ runtime !== "cloud" &&
1980
2288
  current.pendingWorkspaceFiles === pendingFiles
1981
2289
  ? undefined
1982
2290
  : current.pendingWorkspaceFiles,
@@ -2004,6 +2312,14 @@ export class SessionEngine {
2004
2312
  instructionsInvisibleToHarness,
2005
2313
  emit,
2006
2314
  async (update) => {
2315
+ const turn = this.turnRoots.get(sessionId);
2316
+ if (turn !== undefined) {
2317
+ update = await applyWebScratchUpdate({
2318
+ cwd: turn.cwd,
2319
+ root: turn.root,
2320
+ update,
2321
+ });
2322
+ }
2007
2323
  mapper.handleUpdate(update);
2008
2324
  // Yield so the HTTP NDJSON writer can flush tool start/result
2009
2325
  // events while sibling parallel tools are still executing.
@@ -2039,6 +2355,7 @@ export class SessionEngine {
2039
2355
  outcome = { status: "error", errorMessage: describeError(error) };
2040
2356
  } finally {
2041
2357
  permit.release();
2358
+ this.dropTurnFileSystem(sessionId);
2042
2359
  this.approvals.cancelSession(sessionId, "deny");
2043
2360
  this.auths.cancelSession(sessionId, "deny");
2044
2361
  await this.clearPendingApprovals(sessionId);
@@ -2135,6 +2452,7 @@ export class SessionEngine {
2135
2452
  data: { message: outcome.errorMessage ?? "task failed" },
2136
2453
  }
2137
2454
  );
2455
+ await this.wipeCloudSessionWorkspace(sessionId);
2138
2456
  } else {
2139
2457
  await this.appendEvent(sessionId, {
2140
2458
  type: "session.waiting",
@@ -2189,6 +2507,68 @@ export class SessionEngine {
2189
2507
  );
2190
2508
  }
2191
2509
 
2510
+ /**
2511
+ * Replace this session's store prefix with the pending `workspaceFiles`.
2512
+ * Undefined when the write did not land; the prompt must not name the
2513
+ * mount, matching ensureCloudWorkspaceSynced.
2514
+ */
2515
+ private async syncCloudSessionWorkspaceFiles(
2516
+ store: CloudStoreTarget,
2517
+ sessionId: string,
2518
+ files: NonNullable<SessionRecord["pendingWorkspaceFiles"]>
2519
+ ): Promise<{ dir: string; relPaths: string[] } | undefined> {
2520
+ const workspacePrefix = store.sessionWorkspacePrefix(sessionId);
2521
+ try {
2522
+ const relPaths = await syncSessionWorkspaceFiles({
2523
+ sink: store.sink,
2524
+ files,
2525
+ workspacePrefix,
2526
+ });
2527
+ return {
2528
+ dir: `${store.mountPath}/${workspacePrefix}`,
2529
+ relPaths,
2530
+ };
2531
+ } catch (error) {
2532
+ this.logger(
2533
+ `[workspace] ${store.label} session files sync failed: ${error instanceof Error ? error.message : String(error)}`
2534
+ );
2535
+ if (error instanceof WorkspaceFileRefusal) {
2536
+ await this.clearPendingWorkspaceFiles(sessionId, files);
2537
+ }
2538
+ return undefined;
2539
+ }
2540
+ }
2541
+
2542
+ private async wipeCloudSessionWorkspace(sessionId: string): Promise<void> {
2543
+ const store = this.cloudStore();
2544
+ if (store === undefined) {
2545
+ return;
2546
+ }
2547
+ try {
2548
+ await wipeSessionWorkspace({
2549
+ sink: store.sink,
2550
+ workspacePrefix: store.sessionWorkspacePrefix(sessionId),
2551
+ });
2552
+ } catch (error) {
2553
+ this.logger(
2554
+ `[workspace] ${store.label} session files wipe failed: ${error instanceof Error ? error.message : String(error)}`
2555
+ );
2556
+ }
2557
+ }
2558
+
2559
+ private async clearPendingWorkspaceFiles(
2560
+ sessionId: string,
2561
+ pending: NonNullable<SessionRecord["pendingWorkspaceFiles"]>
2562
+ ): Promise<void> {
2563
+ await this.updateRecord(sessionId, (current) => ({
2564
+ ...current,
2565
+ pendingWorkspaceFiles:
2566
+ current.pendingWorkspaceFiles === pending
2567
+ ? undefined
2568
+ : current.pendingWorkspaceFiles,
2569
+ }));
2570
+ }
2571
+
2192
2572
  /**
2193
2573
  * With files authored, every first cloud turn re-copies the authored
2194
2574
  * bytes and the memo only dedupes concurrent first turns. The mount is
@@ -2241,6 +2621,8 @@ export class SessionEngine {
2241
2621
  mountPath: DEPLOYMENT_STORE_MOUNT_PATH,
2242
2622
  skillsPrefix: agentStoreKeys.skillsPrefix(),
2243
2623
  workspacePrefix: agentStoreKeys.workspacePrefix(),
2624
+ sessionWorkspacePrefix: (sessionId: string) =>
2625
+ agentStoreKeys.sessionWorkspacePrefix(sessionId),
2244
2626
  label: "deployment store",
2245
2627
  };
2246
2628
  if (v2Hosting.kind === "cursor-managed") {
@@ -2263,6 +2645,11 @@ export class SessionEngine {
2263
2645
  mountPath: USER_STORE_MOUNT_PATH,
2264
2646
  skillsPrefix: agentStoreKeys.userSkillsPrefix(this.project.name),
2265
2647
  workspacePrefix: agentStoreKeys.userWorkspacePrefix(this.project.name),
2648
+ sessionWorkspacePrefix: (sessionId: string) =>
2649
+ agentStoreKeys.userSessionWorkspacePrefix(
2650
+ this.project.name,
2651
+ sessionId
2652
+ ),
2266
2653
  label: "user store",
2267
2654
  };
2268
2655
  }
@@ -2291,6 +2678,17 @@ export class SessionEngine {
2291
2678
  // allowlist, and a restriction that silently does not apply is worse
2292
2679
  // than a refused turn. Non-local agents already fail discovery; this
2293
2680
  // guards hybrid agents' per-send cloud sessions.
2681
+ //
2682
+ // Factory roots omit `allowedTools` at discovery (the live handle is
2683
+ // resolved per session). The allowlist guard below therefore never
2684
+ // fires for `defineAgent({ root: (ctx) => … })` with omitted `tools`,
2685
+ // and a per-send `cloud` attach would skip the virtual-root allowlist
2686
+ // and run unrestricted on a VM. Any authored root refuses non-local turns.
2687
+ if (runtime !== "local" && this.hasRootOverride()) {
2688
+ throw new Error(
2689
+ `Agent "${agent.name}" declares root, which cannot be served on ${runtime} turns. Refusing the session rather than running it against the host VM.`
2690
+ );
2691
+ }
2294
2692
  if (runtime !== "local" && agent.allowedTools !== undefined) {
2295
2693
  throw new Error(
2296
2694
  `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 +2717,48 @@ export class SessionEngine {
2319
2717
  override: record.cloudOverride,
2320
2718
  })
2321
2719
  : undefined;
2720
+ const authoredRoot = this.hasRootOverride();
2721
+ const root =
2722
+ runtime === "local"
2723
+ ? await this.resolveFileSystem({
2724
+ sessionId: record.sessionId,
2725
+ continuationKey: record.continuationKey ?? undefined,
2726
+ cache: true,
2727
+ cwd: this.harnessCwd(record),
2728
+ })
2729
+ : undefined;
2730
+ const harnessTools =
2731
+ runtime === "local" && root !== undefined
2732
+ ? authoredRoot
2733
+ ? {
2734
+ tools: resolveFileSystemTools({
2735
+ root,
2736
+ tools: agent.allowedTools,
2737
+ }),
2738
+ }
2739
+ : resolveHostHarnessToolOptions(agent.allowedTools)
2740
+ : undefined;
2741
+ const fileTools =
2742
+ root === undefined
2743
+ ? []
2744
+ : synthesizeFileSystemTools(
2745
+ root,
2746
+ authoredRoot
2747
+ ? rootFileToolsFromAuthored(agent.allowedTools)
2748
+ : hostFileToolsFromAuthored(agent.allowedTools)
2749
+ );
2750
+ // Host-default keeps native SemanticSearch (the host index).
2751
+ // `localFileSystem.search` is empty, so synthesizing would
2752
+ // replace a working index with an empty custom tool.
2753
+ const synthesized =
2754
+ root === undefined
2755
+ ? []
2756
+ : this.withTurnScratchTools(
2757
+ authoredRoot
2758
+ ? fileTools
2759
+ : fileTools.filter((tool) => tool.name !== "SemanticSearch"),
2760
+ record.sessionId
2761
+ );
2322
2762
  let turnPrompt = prompt;
2323
2763
  if (isFirstTurn) {
2324
2764
  const contextBlock = buildSessionContextBlock({
@@ -2331,34 +2771,39 @@ export class SessionEngine {
2331
2771
  });
2332
2772
  turnPrompt = `${contextBlock}\n\n${prompt}`;
2333
2773
  }
2334
- if (isFirstTurn) {
2335
- if (runtime === "cloud") {
2336
- // Cloud VMs have no materialized workspace: ship the full context,
2337
- // including agent tool script bodies. Authored skills and workspace
2338
- // seeds land on the store mount (skills for native discovery, seeds
2339
- // for the model's file tools), and on hosted deployments the shared
2340
- // memory journal is on that mount too — say where. When the turn
2341
- // carries the session-bound server-tool MCP endpoint, the preamble
2342
- // catalogs server tools under that MCP server name *before* the
2343
- // agent-tool "write the script if missing" recipe, so a model told
2344
- // to "use <server tool>" calls MCP instead of inventing a .sh.
2345
- const preamble = buildAgentsMdContent(agent, {
2774
+ if (runtime === "cloud") {
2775
+ // Cloud VMs have no materialized workspace: ship the full context,
2776
+ // including agent tool script bodies. Authored skills and workspace
2777
+ // seeds land on the store mount (skills for native discovery, seeds
2778
+ // for the model's file tools), per-turn workspaceFiles land under
2779
+ // the session prefix, and on hosted deployments the shared memory
2780
+ // journal is on that mount too — say where. When the turn carries
2781
+ // the session-bound server-tool MCP endpoint, the preamble catalogs
2782
+ // server tools under that MCP server name *before* the agent-tool
2783
+ // "write the script if missing" recipe, so a model told to "use
2784
+ // <server tool>" calls MCP instead of inventing a .sh.
2785
+ const hosted = isCursorHostedFilesAvailable();
2786
+ const store =
2787
+ hosted || isCursorUserStoreFilesAvailable()
2788
+ ? this.cloudStore()
2789
+ : undefined;
2790
+ let preamble = "";
2791
+ const sections: string[] = [];
2792
+ const pending = record.pendingWorkspaceFiles;
2793
+ if (isFirstTurn) {
2794
+ preamble = buildAgentsMdContent(agent, {
2346
2795
  includeScripts: true,
2347
2796
  serverToolsMcpName:
2348
2797
  this.cloudServerToolsConnection === undefined
2349
2798
  ? undefined
2350
2799
  : SERVER_TOOLS_MCP_SERVER_NAME,
2351
2800
  });
2352
- const hosted = isCursorHostedFilesAvailable();
2353
- // Synced whenever a store is reachable, not just when skills or
2354
- // seeds exist: removing the last one has to delete its store copies
2355
- // too.
2356
- const store =
2357
- hosted || isCursorUserStoreFilesAvailable()
2358
- ? this.cloudStore()
2359
- : undefined;
2360
- let sections = "";
2361
- if (store !== undefined) {
2801
+ }
2802
+ if (store !== undefined) {
2803
+ if (isFirstTurn) {
2804
+ // Synced whenever a store is reachable, not just when skills or
2805
+ // seeds exist: removing the last one has to delete its store copies
2806
+ // too.
2362
2807
  const [, seedsSynced] = await Promise.all([
2363
2808
  this.ensureCloudSkillsSynced(store),
2364
2809
  this.ensureCloudWorkspaceSynced(store),
@@ -2366,25 +2811,63 @@ export class SessionEngine {
2366
2811
  // Only name the mount when the bytes landed; a failed sync must
2367
2812
  // not send the model looking for files that are not there.
2368
2813
  if (seedsSynced && agent.seedFiles.length > 0) {
2369
- const dir = `${store.mountPath}/${store.workspacePrefix}`;
2370
- sections += `\n\n${buildCloudWorkspaceSection(dir)}`;
2814
+ sections.push(
2815
+ buildCloudWorkspaceSection(
2816
+ `${store.mountPath}/${store.workspacePrefix}`
2817
+ )
2818
+ );
2371
2819
  }
2372
2820
  }
2373
- if (hosted) {
2374
- sections += `\n\n${buildCloudMemorySection(this.project.name)}`;
2821
+ if (pending !== undefined && Object.keys(pending).length > 0) {
2822
+ const synced = await this.syncCloudSessionWorkspaceFiles(
2823
+ store,
2824
+ record.sessionId,
2825
+ pending
2826
+ );
2827
+ if (synced !== undefined) {
2828
+ sections.push(
2829
+ buildCloudSessionWorkspaceSection(synced.dir, synced.relPaths)
2830
+ );
2831
+ await this.clearPendingWorkspaceFiles(record.sessionId, pending);
2832
+ }
2375
2833
  }
2376
- turnPrompt = `${preamble}${sections}\n\n${turnPrompt}`;
2377
- } else if (runtime === "local" && instructionsInvisibleToHarness) {
2378
- // Local workspace whose AGENTS.md the harness will not load. With a
2379
- // borrowed harness cwd, agent tool scripts live only in the durable
2380
- // scaffold dir — embed bodies like cloud. Otherwise the catalog can
2381
- // reference on-disk paths under the harness cwd. Grokbot turns are
2382
- // excluded: their instructions ride the request's `instructions`
2383
- // field into the Sand system prompt instead.
2384
- const includeScripts = this.hasBorrowedWorkspace(record);
2385
- const preamble = buildAgentsMdContent(agent, { includeScripts });
2386
- turnPrompt = `${preamble}\n\n${turnPrompt}`;
2834
+ } else if (pending !== undefined && Object.keys(pending).length > 0) {
2835
+ await this.clearPendingWorkspaceFiles(record.sessionId, pending);
2836
+ }
2837
+ if (isFirstTurn && hosted) {
2838
+ sections.push(buildCloudMemorySection(this.project.name));
2387
2839
  }
2840
+ if (preamble !== "" || sections.length > 0) {
2841
+ turnPrompt = [preamble, ...sections, turnPrompt]
2842
+ .filter((part) => part !== "")
2843
+ .join("\n\n");
2844
+ }
2845
+ } else if (isFirstTurn && runtime === "local" && authoredRoot) {
2846
+ // Virtual roots skip materialize, so AGENTS.md never lands on disk.
2847
+ // Ship the same preamble the borrowed-cwd local path uses.
2848
+ const preamble = buildAgentsMdContent(
2849
+ { ...agent, tools: [...agent.tools, ...synthesized] },
2850
+ { includeScripts: true }
2851
+ );
2852
+ const workspaceInfo = `<user_info>\nWorkspace Path: /\n</user_info>`;
2853
+ turnPrompt =
2854
+ preamble === ""
2855
+ ? `${workspaceInfo}\n\n${turnPrompt}`
2856
+ : `${workspaceInfo}\n\n${preamble}\n\n${turnPrompt}`;
2857
+ } else if (
2858
+ isFirstTurn &&
2859
+ runtime === "local" &&
2860
+ instructionsInvisibleToHarness
2861
+ ) {
2862
+ // Local workspace whose AGENTS.md the harness will not load. With a
2863
+ // borrowed harness cwd, agent tool scripts live only in the durable
2864
+ // scaffold dir — embed bodies like cloud. Otherwise the catalog can
2865
+ // reference on-disk paths under the harness cwd. Grokbot turns are
2866
+ // excluded: their instructions ride the request's `instructions`
2867
+ // field into the Sand system prompt instead.
2868
+ const includeScripts = this.hasBorrowedWorkspace(record);
2869
+ const preamble = buildAgentsMdContent(agent, { includeScripts });
2870
+ turnPrompt = `${preamble}\n\n${turnPrompt}`;
2388
2871
  }
2389
2872
  return {
2390
2873
  sessionId: record.sessionId,
@@ -2392,9 +2875,18 @@ export class SessionEngine {
2392
2875
  ? {}
2393
2876
  : { sdkAgentId: record.sdkAgentId }),
2394
2877
  workspaceDir: this.harnessCwd(record),
2395
- sandbox: this.project.agent.local?.sandbox === true,
2396
- ...(runtime === "local" && agent.allowedTools !== undefined
2397
- ? { tools: agent.allowedTools }
2878
+ sandbox: authoredRoot
2879
+ ? false
2880
+ : this.project.agent.local?.sandbox === true,
2881
+ ...(runtime === "local" && harnessTools !== undefined
2882
+ ? {
2883
+ ...(harnessTools.tools === undefined
2884
+ ? {}
2885
+ : { tools: harnessTools.tools }),
2886
+ ...(harnessTools.disallowedTools === undefined
2887
+ ? {}
2888
+ : { disallowedTools: harnessTools.disallowedTools }),
2889
+ }
2398
2890
  : {}),
2399
2891
  runnerStateDir: join(this.stateRoot, "runner"),
2400
2892
  prompt: turnPrompt,
@@ -2418,7 +2910,13 @@ export class SessionEngine {
2418
2910
  : {}),
2419
2911
  customTools:
2420
2912
  runtime === "local"
2421
- ? await this.buildCustomTools(record, agent, turnId, emit)
2913
+ ? await this.buildCustomTools(
2914
+ record,
2915
+ agent,
2916
+ turnId,
2917
+ emit,
2918
+ synthesized
2919
+ )
2422
2920
  : {},
2423
2921
  mcpServers: this.buildMcpServers(
2424
2922
  runtime,
@@ -2712,7 +3210,8 @@ export class SessionEngine {
2712
3210
  record: SessionRecord,
2713
3211
  agent: ResolvedAgent,
2714
3212
  turnId: string,
2715
- emit: (payload: SessionEventPayload) => void
3213
+ emit: (payload: SessionEventPayload) => void,
3214
+ extraTools: DiscoveredTool[] = []
2716
3215
  ): Promise<Record<string, SDKCustomTool>> {
2717
3216
  const tools: Record<string, SDKCustomTool> = {};
2718
3217
  const register = (tool: ResolvedAgent["tools"][number]): void => {
@@ -2742,7 +3241,7 @@ export class SessionEngine {
2742
3241
  },
2743
3242
  };
2744
3243
  };
2745
- for (const tool of agent.tools) {
3244
+ for (const tool of [...agent.tools, ...extraTools]) {
2746
3245
  if (tool.execution !== "server") {
2747
3246
  continue;
2748
3247
  }
@@ -3682,6 +4181,27 @@ export class SessionEngine {
3682
4181
  const session = identity.session;
3683
4182
  let workspaceDir = identity.workspaceDir;
3684
4183
  let scratchDir = identity.scratchDir;
4184
+ if (
4185
+ isFileSystemToolAlias(toolName) &&
4186
+ identity.busy &&
4187
+ record !== undefined &&
4188
+ isMutatingFileSystemTool(toolName)
4189
+ ) {
4190
+ throw new SessionBusyError(record.sessionId);
4191
+ }
4192
+ const native = await this.nativeFileSystemOutcome({
4193
+ toolName,
4194
+ input,
4195
+ callId,
4196
+ sessionId: record?.sessionId,
4197
+ continuationKey:
4198
+ record?.continuationKey ?? session.continuationKey ?? undefined,
4199
+ cwd: workspaceDir,
4200
+ dryRun: record?.dryRun === true,
4201
+ });
4202
+ if (native !== undefined) {
4203
+ return native;
4204
+ }
3685
4205
  let tool = this.project.agent.tools.find((t) => t.name === toolName);
3686
4206
  if (
3687
4207
  tool === undefined &&
@@ -3783,7 +4303,10 @@ export class SessionEngine {
3783
4303
  record !== undefined &&
3784
4304
  identity.busy &&
3785
4305
  this.materializedWorkspaces.has(record.sessionId);
3786
- if (record === undefined || workspaceUnavailable || !turnMaterialized) {
4306
+ if (
4307
+ !this.hasRootOverride() &&
4308
+ (record === undefined || workspaceUnavailable || !turnMaterialized)
4309
+ ) {
3787
4310
  await materializeWorkspace({
3788
4311
  agent: this.project.agent,
3789
4312
  workspaceDir:
@@ -4745,9 +5268,8 @@ export class SessionEngine {
4745
5268
  }
4746
5269
  const record = await this.sessions.get(sessionId);
4747
5270
  const sdkAgentId =
4748
- (record?.sdkAgentId !== undefined && record.sdkAgentId.startsWith("bc-")
4749
- ? record.sdkAgentId
4750
- : undefined) ?? cloudSdkAgentIdFromEvents(events);
5271
+ (record?.sdkAgentId?.startsWith("bc-") ? record.sdkAgentId : undefined) ??
5272
+ cloudSdkAgentIdFromEvents(events);
4751
5273
  if (sdkAgentId === undefined) {
4752
5274
  return;
4753
5275
  }