@platless/axom-cli 0.3.13 → 0.6.36

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 (320) hide show
  1. package/README.md +18 -5
  2. package/dist/axom-patch/cli/README.md +119 -9
  3. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  4. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  6. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  7. package/dist/axom-patch/cli/account-limits/generate.py +123 -0
  8. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +81 -0
  9. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  10. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  11. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  12. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  13. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  14. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  15. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  16. package/dist/axom-patch/cli/nightly-task.md +194 -0
  17. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  18. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
  19. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  20. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  21. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  22. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  23. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
  24. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
  25. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -5
  26. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -7
  27. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  28. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  29. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  30. package/dist/axom-patch/cli/runtime.json +39 -15
  31. package/dist/axom-patch/cli/zcode-account-limits.json +30 -0
  32. package/dist/axom-patch/cli/zcode-models.json +37 -0
  33. package/dist/axom-patch/cli/zcode-subagent-subwork.json +72 -0
  34. package/dist/cli.js +4 -9
  35. package/dist/commands/knowledge.js +47 -0
  36. package/dist/commands/update.js +65 -3
  37. package/dist/commands/workstation.js +73 -7
  38. package/dist/install/claude/commands.js +13 -13
  39. package/dist/keepers/acp/keeper.mjs +3083 -0
  40. package/dist/keepers/acp/manifest.json +9 -0
  41. package/dist/keepers/terminal/keeper.mjs +8469 -0
  42. package/dist/keepers/terminal/manifest.json +9 -0
  43. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  44. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  45. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  46. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  47. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  48. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  52. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  53. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  54. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  55. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  56. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  57. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  58. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  59. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  60. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  61. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  62. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  63. package/dist/mcp.js +1 -1
  64. package/dist/native/darwin-arm64/axom-quic +0 -0
  65. package/dist/test/keeperKit.js +67 -0
  66. package/dist/workstation/acp/acpAgents.js +2192 -636
  67. package/dist/workstation/acp/acpClient.js +84 -495
  68. package/dist/workstation/acp/adoption.js +10 -396
  69. package/dist/workstation/acp/binaryInstall.js +100 -27
  70. package/dist/workstation/acp/builds/gc.js +245 -0
  71. package/dist/workstation/acp/builds/install/binary.js +28 -0
  72. package/dist/workstation/acp/builds/install/npm.js +324 -0
  73. package/dist/workstation/acp/builds/install/uv.js +254 -0
  74. package/dist/workstation/acp/builds/migrate.js +195 -0
  75. package/dist/workstation/acp/builds/model.js +128 -0
  76. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  77. package/dist/workstation/acp/builds/store.js +352 -0
  78. package/dist/workstation/acp/commandTableProbe.js +68 -0
  79. package/dist/workstation/acp/configOptions.js +103 -1
  80. package/dist/workstation/acp/envSecrets.js +27 -0
  81. package/dist/workstation/acp/initializeResponse.js +290 -0
  82. package/dist/workstation/acp/installErrors.js +12 -2
  83. package/dist/workstation/acp/installProbe.js +1 -1
  84. package/dist/workstation/acp/managedPatches.js +239 -159
  85. package/dist/workstation/acp/managedRuntime.js +314 -0
  86. package/dist/workstation/acp/outbox.js +330 -0
  87. package/dist/workstation/acp/patchAssets.js +60 -3
  88. package/dist/workstation/acp/permission.js +42 -10
  89. package/dist/workstation/acp/processor/checkpoint.js +78 -0
  90. package/dist/workstation/acp/processor/commands.js +35 -0
  91. package/dist/workstation/acp/processor/control.js +282 -0
  92. package/dist/workstation/acp/processor/emit.js +26 -0
  93. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  94. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  95. package/dist/workstation/acp/processor/origins.js +48 -0
  96. package/dist/workstation/acp/processor/output.js +67 -0
  97. package/dist/workstation/acp/processor/projection.js +42 -0
  98. package/dist/workstation/acp/processor/record.js +128 -0
  99. package/dist/workstation/acp/processor/runtime.js +105 -0
  100. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  101. package/dist/workstation/acp/processor/session.js +496 -0
  102. package/dist/workstation/acp/processor/state.js +67 -0
  103. package/dist/workstation/acp/processor/step.js +442 -0
  104. package/dist/workstation/acp/processor/test/fixtureLog.js +58 -0
  105. package/dist/workstation/acp/processor/test/harness.js +120 -0
  106. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  107. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  108. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  109. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  110. package/dist/workstation/acp/processor/transport/local.js +269 -0
  111. package/dist/workstation/acp/processor/transport/types.js +78 -0
  112. package/dist/workstation/acp/processor/updates.js +390 -0
  113. package/dist/workstation/acp/processorSession.js +1348 -0
  114. package/dist/workstation/acp/registry/catalog.js +189 -41
  115. package/dist/workstation/acp/registry/launch.js +76 -119
  116. package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
  117. package/dist/workstation/acp/steering.js +58 -0
  118. package/dist/workstation/acp/switching.js +111 -0
  119. package/dist/workstation/acp/terminalLogin.js +16 -42
  120. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  121. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  122. package/dist/workstation/acp/test/daemonLife.js +106 -0
  123. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  124. package/dist/workstation/acp/test/keeperWorld.js +295 -0
  125. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  126. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  127. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  128. package/dist/workstation/acp/test/raw/sources.js +135 -0
  129. package/dist/workstation/acp/test/realAdapter.js +331 -0
  130. package/dist/workstation/acp/test/scriptAgent.js +214 -0
  131. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  132. package/dist/workstation/acp/timedCommand.js +4 -2
  133. package/dist/workstation/acp/uvxInstall.js +72 -122
  134. package/dist/workstation/agentFlow.js +103 -0
  135. package/dist/workstation/agentSessions.js +1289 -544
  136. package/dist/workstation/channels/agent.js +7 -5
  137. package/dist/workstation/channels/computer.js +69 -21
  138. package/dist/workstation/channels/computerExecs.js +200 -0
  139. package/dist/workstation/channels/management.js +172 -8
  140. package/dist/workstation/channels/url.js +1 -1
  141. package/dist/workstation/config.js +16 -4
  142. package/dist/workstation/connection/encryptedChannel.js +4 -1
  143. package/dist/workstation/control/bindingStore.js +200 -0
  144. package/dist/workstation/control/commands.js +124 -0
  145. package/dist/workstation/control/daemonIdentity.js +53 -0
  146. package/dist/workstation/control/enrollment.js +106 -0
  147. package/dist/workstation/control/inspect.js +71 -0
  148. package/dist/workstation/control/locks.js +63 -0
  149. package/dist/workstation/control/protocol.js +40 -0
  150. package/dist/workstation/control/restartGuard.js +46 -0
  151. package/dist/workstation/control/runtimeClient.js +97 -0
  152. package/dist/workstation/control/runtimeHost.js +148 -0
  153. package/dist/workstation/control/serverIdentity.js +35 -0
  154. package/dist/workstation/daemon.js +393 -12
  155. package/dist/workstation/daemonctl.js +103 -14
  156. package/dist/workstation/desktopControl/audio.js +99 -0
  157. package/dist/workstation/desktopControl/capabilities.js +92 -0
  158. package/dist/workstation/desktopControl/captureModes.js +9 -0
  159. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  160. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  161. package/dist/workstation/desktopControl/helperProcess.js +353 -0
  162. package/dist/workstation/desktopControl/lobby.js +129 -0
  163. package/dist/workstation/desktopControl/macProvider.js +155 -0
  164. package/dist/workstation/desktopControl/media.js +331 -0
  165. package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
  166. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  167. package/dist/workstation/desktopControl/overview.js +58 -0
  168. package/dist/workstation/desktopControl/protocol.js +302 -0
  169. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  170. package/dist/workstation/desktopControl/rateController.js +226 -0
  171. package/dist/workstation/desktopControl/supervisor.js +1681 -0
  172. package/dist/workstation/desktopControl/transport.js +27 -0
  173. package/dist/workstation/desktopControl/wallpaper.js +78 -0
  174. package/dist/workstation/exec/attach.js +126 -0
  175. package/dist/workstation/exec/command.js +40 -0
  176. package/dist/workstation/exec/durable.js +517 -0
  177. package/dist/workstation/exec/fallback.js +106 -0
  178. package/dist/workstation/exec/policy.js +837 -0
  179. package/dist/workstation/exec/shaping.js +211 -0
  180. package/dist/workstation/exec.js +3 -112
  181. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  182. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  183. package/dist/workstation/files/client.js +455 -0
  184. package/dist/workstation/files/credentials.js +142 -0
  185. package/dist/workstation/files/errors.js +28 -0
  186. package/dist/workstation/files/fence.js +350 -0
  187. package/dist/workstation/files/find/fileIndex.js +153 -0
  188. package/dist/workstation/files/find/fuzzy.js +155 -0
  189. package/dist/workstation/files/host.js +122 -0
  190. package/dist/workstation/files/hostProtocol.js +2 -0
  191. package/dist/workstation/files/media/cache.js +620 -0
  192. package/dist/workstation/files/media/encoding.js +308 -0
  193. package/dist/workstation/files/media/estimator.js +100 -0
  194. package/dist/workstation/files/media/media.js +325 -0
  195. package/dist/workstation/files/media/packedAudio.js +23 -0
  196. package/dist/workstation/files/media/scheduler.js +551 -0
  197. package/dist/workstation/files/media/types.js +5 -0
  198. package/dist/workstation/files/scope.js +45 -0
  199. package/dist/workstation/files/structured.js +284 -0
  200. package/dist/workstation/{fileOps.js → files/text.js} +152 -39
  201. package/dist/workstation/files/uploads.js +351 -0
  202. package/dist/workstation/journal.js +65 -236
  203. package/dist/workstation/keepers/acp/client.js +101 -0
  204. package/dist/workstation/keepers/acp/envelope.js +62 -0
  205. package/dist/workstation/keepers/acp/host.js +419 -0
  206. package/dist/workstation/keepers/acp/main.js +18 -0
  207. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  208. package/dist/workstation/keepers/acp/params.js +137 -0
  209. package/dist/workstation/keepers/acp/protocol.js +66 -0
  210. package/dist/workstation/keepers/acp/recover.js +120 -0
  211. package/dist/workstation/keepers/acp/session.js +584 -0
  212. package/dist/workstation/keepers/acp/stderr.js +154 -0
  213. package/dist/workstation/keepers/kit/activity.js +103 -0
  214. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  215. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  216. package/dist/workstation/keepers/kit/bundle.js +124 -0
  217. package/dist/workstation/keepers/kit/client.js +432 -0
  218. package/dist/workstation/keepers/kit/identity.js +21 -0
  219. package/dist/workstation/keepers/kit/ipc.js +112 -0
  220. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  221. package/dist/workstation/keepers/kit/launcher.js +289 -0
  222. package/dist/workstation/keepers/kit/lock.js +123 -0
  223. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  224. package/dist/workstation/keepers/kit/paths.js +76 -0
  225. package/dist/workstation/keepers/kit/protocol.js +43 -0
  226. package/dist/workstation/keepers/kit/server.js +518 -0
  227. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  228. package/dist/workstation/keepers/legacy.js +296 -0
  229. package/dist/workstation/keepers/policy.js +576 -0
  230. package/dist/workstation/keepers/stop.js +34 -0
  231. package/dist/workstation/keepers/terminal/client.js +213 -0
  232. package/dist/workstation/keepers/terminal/exec.js +981 -0
  233. package/dist/workstation/keepers/terminal/generations.js +129 -0
  234. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  235. package/dist/workstation/keepers/terminal/main.js +43 -0
  236. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  237. package/dist/workstation/keepers/terminal/params.js +27 -0
  238. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  239. package/dist/workstation/keepers/terminal/pty.js +330 -0
  240. package/dist/workstation/managedInstall.js +57 -0
  241. package/dist/workstation/protocol/accountLimits.js +47 -0
  242. package/dist/workstation/protocol/agent.js +50 -19
  243. package/dist/workstation/protocol/common.js +30 -0
  244. package/dist/workstation/protocol/computer.js +15 -4
  245. package/dist/workstation/protocol/management.js +141 -4
  246. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  247. package/dist/workstation/rd/artifact.js +119 -0
  248. package/dist/workstation/rd/control.js +119 -0
  249. package/dist/workstation/rd/display.js +29 -0
  250. package/dist/workstation/rd/provider.js +222 -0
  251. package/dist/workstation/rd/sessions.js +391 -0
  252. package/dist/workstation/rd/state.js +45 -0
  253. package/dist/workstation/rd/update.js +171 -0
  254. package/dist/workstation/rd/upkeep.js +38 -0
  255. package/dist/workstation/register.js +27 -37
  256. package/dist/workstation/service/installer.js +104 -13
  257. package/dist/workstation/service/launchd.js +1 -0
  258. package/dist/workstation/storageMetrics.js +118 -0
  259. package/dist/workstation/systemMetrics.js +154 -93
  260. package/dist/workstation/toolchain.js +94 -0
  261. package/dist/workstation/tools/browser.js +28 -4
  262. package/dist/workstation/tools/client.js +9 -1
  263. package/dist/workstation/tools/info.js +50 -0
  264. package/dist/workstation/tools/keeperProcess.js +15 -5
  265. package/dist/workstation/tools/router.js +55 -0
  266. package/dist/workstation/tools/terminalRouter.js +222 -0
  267. package/dist/workstation/tools/validation.js +37 -1
  268. package/dist/workstation/update/artifact.js +2 -1
  269. package/dist/workstation/update/coordinator.js +3 -0
  270. package/dist/workstation/update/globalInstall.js +116 -0
  271. package/dist/workstation/update/npmInstaller.js +49 -23
  272. package/dist/workstation/update/ptyPreflight.js +21 -0
  273. package/dist/workstation/update/rdRollback.js +145 -0
  274. package/dist/workstation/update/runUpdate.js +3 -2
  275. package/dist/workstation/update/taskRegistry.js +105 -11
  276. package/dist/workstation/update/watchdog.js +16790 -334
  277. package/package.json +11 -8
  278. package/skills/axom/SKILL.md +11 -11
  279. package/skills/axom-definition/SKILL.md +3 -3
  280. package/skills/axom-platform/SKILL.md +3 -3
  281. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  282. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -49
  283. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  284. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  285. package/dist/commands/guide.js +0 -38
  286. package/dist/workstation/acp/acpSession.js +0 -1475
  287. package/dist/workstation/acp/contract.js +0 -499
  288. package/dist/workstation/acp/delegationMcp.js +0 -24
  289. package/dist/workstation/acp/draftInterceptor.js +0 -163
  290. package/dist/workstation/acp/qoderStartup.js +0 -45
  291. package/dist/workstation/acp/sessionScope.js +0 -19
  292. package/dist/workstation/filePaths.js +0 -174
  293. package/dist/workstation/fileWorker.js +0 -81
  294. package/dist/workstation/fileWorkerClient.js +0 -121
  295. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -106
  296. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -262
  297. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  298. package/dist/workstation/qoderAdapter/contract.js +0 -78
  299. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -30
  300. package/dist/workstation/qoderAdapter/history.js +0 -87
  301. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  302. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  303. package/dist/workstation/qoderAdapter/main.js +0 -93
  304. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  305. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  306. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  307. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  308. package/dist/workstation/qoderAdapter/profile.js +0 -93
  309. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -49
  310. package/dist/workstation/qoderAdapter/session.js +0 -319
  311. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  312. package/dist/workstation/qoderAdapter/subwork.js +0 -77
  313. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  314. package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
  315. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  316. package/dist/workstation/qoderAdapter/usage.js +0 -37
  317. package/dist/workstation/tools/core.js +0 -50
  318. package/dist/workstation/tools/files.js +0 -525
  319. package/dist/workstation/tools/terminal.js +0 -214
  320. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
@@ -0,0 +1,284 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { base64, CHUNK_BYTES, identifier, integer, keys, string } from '../tools/validation.js';
4
+ import { openMediaSource, pinned, resolveAbsolute } from './fence.js';
5
+ import { configuredRoots, filesystemScope, inside, READ_ONLY } from './scope.js';
6
+ import { MAX_UPLOAD_BYTES, TEMP_PREFIX, UPLOAD_TTL_MS, UploadStore } from './uploads.js';
7
+ import { FileIndexCache } from './find/fileIndex.js';
8
+ import { rankPaths } from './find/fuzzy.js';
9
+ export const LIST_MAX_ENTRIES = 2000;
10
+ export const LIST_MAX_VISITED = 10000;
11
+ export const FIND_DEFAULT_LIMIT = 50;
12
+ export const FIND_MAX_LIMIT = 200;
13
+ /**
14
+ * The Files tab's file semantics (`files.*` tool requests): absolute paths in the `configured-roots` scope, or in the
15
+ * `read-only` scope when a request says `readOnly: true`. Errors are thrown with the messages callers see today
16
+ * (`tool_result.error`). Tools never compose a shell command.
17
+ */
18
+ export class StructuredFiles {
19
+ scope;
20
+ uploads;
21
+ // Home and its ancestors walk instead of asking git: a dotfiles repository at `~` (often ignoring
22
+ // `*`) would answer for the whole home with just its tracked dotfiles.
23
+ indexes = new FileIndexCache(name => name.startsWith(TEMP_PREFIX), Date.now, scope => { const relative = path.relative(scope, this.home); return !relative.startsWith('..') && !path.isAbsolute(relative); });
24
+ home;
25
+ roots;
26
+ downloads;
27
+ constructor(scope, ttl, stateDir) {
28
+ this.scope = scope;
29
+ this.home = scope.home;
30
+ this.roots = [...scope.roots];
31
+ this.downloads = path.join(scope.home, 'Downloads');
32
+ this.uploads = new UploadStore(scope.roots, ttl, stateDir);
33
+ }
34
+ static async create(home, roots, ttl = UPLOAD_TTL_MS, stateDir, filesystem = false) {
35
+ const scope = filesystem ? await filesystemScope(home) : await configuredRoots(home, roots);
36
+ if (stateDir)
37
+ await UploadStore.prepareStateDir(stateDir);
38
+ const files = new StructuredFiles(scope, ttl, stateDir);
39
+ await files.uploads.recover((raw, missingLeaf, mutation) => files.resolve(raw, missingLeaf, mutation));
40
+ return files;
41
+ }
42
+ /** Uploads in flight. */
43
+ get activeUploads() { return this.uploads.size; }
44
+ scopeFor(readOnly) { return readOnly ? READ_ONLY : this.scope; }
45
+ /** Validates an absolute path in this instance's configured-roots scope (or the read-only scope). */
46
+ resolve(raw, missingLeaf = false, mutation = false, readOnly = false, followLeaf = true) {
47
+ return resolveAbsolute(this.scopeFor(readOnly), raw, missingLeaf, mutation, followLeaf);
48
+ }
49
+ /** The descriptor stays pinned after cwd is restored; media children inherit it, never re-open the path. */
50
+ openMediaSource(raw, readOnly = false) {
51
+ return openMediaSource(this.scopeFor(readOnly), raw);
52
+ }
53
+ async dispatch(method, p) {
54
+ if (p.readOnly !== undefined && typeof p.readOnly !== 'boolean')
55
+ throw new Error('Invalid readOnly');
56
+ if (p.readOnly !== undefined && !['files.list', 'files.find', 'files.read'].includes(method))
57
+ throw new Error('Read-only file access cannot modify files');
58
+ await this.expire();
59
+ if (method === 'files.find')
60
+ return this.find(p);
61
+ // Any write may add, move or remove a path a cached search list still holds. Invalidate before and again after
62
+ // the mutation: a search building its list concurrently (find takes no pin) then never caches a pre-mutation list.
63
+ if (!['files.list', 'files.read'].includes(method)) {
64
+ this.indexes.invalidate();
65
+ try {
66
+ return await this.locate(method, p);
67
+ }
68
+ finally {
69
+ this.indexes.invalidate();
70
+ }
71
+ }
72
+ return this.locate(method, p);
73
+ }
74
+ async locate(method, p) {
75
+ let parent;
76
+ if (method === 'files.upload.chunk' || method === 'files.upload.finish' || method === 'files.upload.cancel') {
77
+ const upload = this.uploads.get(identifier(p, 'uploadId'));
78
+ if (!upload && method === 'files.upload.cancel') {
79
+ keys(p, ['uploadId']);
80
+ return { ok: true };
81
+ }
82
+ if (!upload)
83
+ throw new Error('Upload not found or expired');
84
+ parent = path.dirname(upload.target);
85
+ }
86
+ else {
87
+ const target = await this.resolve(string(p, 'path', 4096, method === 'files.list' ? this.home : undefined), method === 'files.mkdir' || method === 'files.upload.begin', !['files.list', 'files.read'].includes(method), p.readOnly === true, !['files.rename', 'files.delete'].includes(method));
88
+ parent = method === 'files.list' ? target : path.dirname(target);
89
+ }
90
+ return pinned(parent, async (pin) => {
91
+ if (method.startsWith('files.upload.') && method !== 'files.upload.begin') {
92
+ // Re-read: the upload may have finished, been cancelled or expired while this call waited for the pin.
93
+ const upload = this.uploads.get(identifier(p, 'uploadId'));
94
+ if (!upload && method === 'files.upload.cancel') {
95
+ keys(p, ['uploadId']);
96
+ return { ok: true };
97
+ }
98
+ if (!upload)
99
+ throw new Error('Upload not found or expired');
100
+ const stat = await fs.stat('.');
101
+ if (stat.dev !== upload.parent.dev || stat.ino !== upload.parent.ino) {
102
+ if (method === 'files.upload.cancel') {
103
+ await this.uploads.abandon(upload);
104
+ return { ok: true };
105
+ }
106
+ throw new Error('Upload parent changed');
107
+ }
108
+ }
109
+ return this.perform(method, p, pin);
110
+ });
111
+ }
112
+ async perform(method, p, pin) {
113
+ switch (method) {
114
+ case 'files.list': {
115
+ keys(p, ['path', 'readOnly']);
116
+ const target = await this.resolve(string(p, 'path', 4096, this.home), false, false, p.readOnly === true);
117
+ const displayedPath = this.scope.kind === 'filesystem' ? string(p, 'path', 4096, this.home) : target;
118
+ const listing = await fs.opendir(pin.leaf(target));
119
+ const entries = [];
120
+ let visited = 0;
121
+ try {
122
+ for await (const entry of listing) {
123
+ if (++visited > LIST_MAX_VISITED || entries.length >= LIST_MAX_ENTRIES)
124
+ throw new Error('Directory entry limit exceeded');
125
+ if (entry.name.startsWith(TEMP_PREFIX) || entry.isSymbolicLink())
126
+ continue;
127
+ const entryPath = path.join(target, entry.name);
128
+ let stat;
129
+ try {
130
+ await this.resolve(entryPath, false, false, p.readOnly === true);
131
+ stat = await fs.lstat(pin.leaf(entryPath));
132
+ }
133
+ catch {
134
+ continue;
135
+ }
136
+ if (!stat.isFile() && !stat.isDirectory())
137
+ continue;
138
+ entries.push({ name: entry.name, path: path.join(displayedPath, entry.name), directory: stat.isDirectory(), size: stat.size, modifiedAt: stat.mtimeMs, createdAt: stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.ctimeMs > 0 ? stat.ctimeMs : stat.mtimeMs,
139
+ ...(this.scope.kind === 'filesystem' ? { writable: await fs.access(entryPath, constants.W_OK).then(() => true, () => false) } : {}) });
140
+ }
141
+ }
142
+ finally {
143
+ await listing.close().catch(() => { });
144
+ }
145
+ entries.sort((a, b) => Number(b.directory) - Number(a.directory) || String(a.name).localeCompare(String(b.name)));
146
+ const parent = path.dirname(displayedPath);
147
+ return { path: displayedPath, parent: p.readOnly === true || this.scope.kind === 'filesystem' ? (parent === displayedPath ? null : parent) : this.roots.includes(target) || !this.roots.some(root => inside(parent, root)) ? null : parent, entries, roots: this.roots };
148
+ }
149
+ case 'files.read': {
150
+ keys(p, ['path', 'offset', 'length', 'readOnly']);
151
+ const offset = integer(p, 'offset', 0, Number.MAX_SAFE_INTEGER, 0);
152
+ const length = integer(p, 'length', 1, CHUNK_BYTES, CHUNK_BYTES);
153
+ const target = await this.resolve(string(p, 'path'), false, false, p.readOnly === true);
154
+ const handle = await pin.regularHandle(target);
155
+ try {
156
+ const stat = await handle.stat();
157
+ const buffer = Buffer.alloc(length);
158
+ const { bytesRead } = await handle.read(buffer, 0, length, offset);
159
+ return { data: buffer.subarray(0, bytesRead).toString('base64'), size: stat.size, eof: offset + bytesRead >= stat.size, name: path.basename(target),
160
+ ...(this.scope.kind === 'filesystem' ? { writable: await fs.access(pin.leaf(target), constants.W_OK).then(() => true, () => false) } : {}) };
161
+ }
162
+ finally {
163
+ await handle.close();
164
+ }
165
+ }
166
+ case 'files.mkdir': {
167
+ keys(p, ['path']);
168
+ const target = await this.resolve(string(p, 'path'), true, true);
169
+ await fs.mkdir(pin.leaf(target));
170
+ return { ok: true };
171
+ }
172
+ case 'files.rename': {
173
+ keys(p, ['path', 'name']);
174
+ const target = await this.resolve(string(p, 'path'), false, true, false, false);
175
+ const name = string(p, 'name', 255);
176
+ if (name === '.' || name === '..' || /[/\\]/.test(name) || name.startsWith(TEMP_PREFIX))
177
+ throw new Error('Invalid new name');
178
+ const destination = await this.resolve(path.join(path.dirname(target), name), true, true);
179
+ // Node rename() overwrites files. Hard-link commits are exclusive for files; for directories,
180
+ // reserve the destination with mkdir and replace only that empty reservation atomically.
181
+ const stat = await fs.lstat(pin.leaf(target));
182
+ if (stat.isFile()) {
183
+ await fs.link(pin.leaf(target), pin.leaf(destination));
184
+ await fs.unlink(pin.leaf(target));
185
+ }
186
+ else if (stat.isDirectory()) {
187
+ await fs.mkdir(pin.leaf(destination), { mode: stat.mode });
188
+ try {
189
+ await fs.rename(pin.leaf(target), pin.leaf(destination));
190
+ }
191
+ catch (error) {
192
+ await fs.rmdir(pin.leaf(destination)).catch(() => { });
193
+ throw error;
194
+ }
195
+ }
196
+ else
197
+ throw new Error('Not a regular file or directory');
198
+ return { ok: true };
199
+ }
200
+ case 'files.delete': {
201
+ keys(p, ['path']);
202
+ const target = await this.resolve(string(p, 'path'), false, true, false, false);
203
+ const stat = await fs.lstat(pin.leaf(target));
204
+ if (stat.isDirectory())
205
+ await fs.rmdir(pin.leaf(target));
206
+ else if (stat.isFile())
207
+ await fs.unlink(pin.leaf(target));
208
+ else
209
+ throw new Error('Not a regular file or directory');
210
+ return { ok: true };
211
+ }
212
+ case 'files.upload.begin': {
213
+ keys(p, ['path', 'size', 'overwrite']);
214
+ const size = integer(p, 'size', 0, MAX_UPLOAD_BYTES);
215
+ if (p.overwrite !== undefined && typeof p.overwrite !== 'boolean')
216
+ throw new Error('Invalid overwrite');
217
+ return this.uploads.begin(pin, () => this.resolve(string(p, 'path'), true, true), size, p.overwrite === true, this.scope.kind === 'filesystem', string(p, 'path'));
218
+ }
219
+ case 'files.upload.chunk': {
220
+ keys(p, ['uploadId', 'offset', 'data']);
221
+ const upload = this.uploads.use(identifier(p, 'uploadId'));
222
+ const offset = integer(p, 'offset', 0, MAX_UPLOAD_BYTES);
223
+ const bytes = base64(p.data);
224
+ return this.uploads.chunk(upload, offset, bytes);
225
+ }
226
+ case 'files.upload.finish': {
227
+ keys(p, ['uploadId']);
228
+ const upload = this.uploads.use(identifier(p, 'uploadId'));
229
+ return this.uploads.finish(pin, upload, (raw, missingLeaf, mutation) => this.resolve(raw, missingLeaf, mutation));
230
+ }
231
+ case 'files.upload.cancel': {
232
+ keys(p, ['uploadId']);
233
+ return this.uploads.cancel(identifier(p, 'uploadId'));
234
+ }
235
+ default: throw new Error('Unsupported file tool');
236
+ }
237
+ }
238
+ /**
239
+ * Fuzzy file-name search below one directory (`path`, default home). Ranking runs over the cached
240
+ * relative-path list; only the best candidates are re-validated against the same root/symlink fence
241
+ * as `files.list` before they are returned, so a path git still remembers but the disk lost, or one
242
+ * that became a symbolic link, never reaches the client. Uses no cwd: absolute paths throughout, so it
243
+ * never enters (or waits for) a `pinned()` section.
244
+ */
245
+ async find(p) {
246
+ keys(p, ['path', 'query', 'limit', 'readOnly']);
247
+ const readOnly = p.readOnly === true;
248
+ const query = string(p, 'query', 256).trim();
249
+ if (!query)
250
+ throw new Error('Invalid query');
251
+ const limit = integer(p, 'limit', 1, FIND_MAX_LIMIT, FIND_DEFAULT_LIMIT);
252
+ const scope = await this.resolve(string(p, 'path', 4096, this.home), false, false, readOnly);
253
+ const displayedScope = this.scope.kind === 'filesystem' ? string(p, 'path', 4096, this.home) : scope;
254
+ if (!(await fs.stat(scope)).isDirectory())
255
+ throw new Error('Search scope is not a directory');
256
+ const index = await this.indexes.get(scope);
257
+ const ranked = rankPaths(index.paths, query);
258
+ const entries = [];
259
+ let checked = 0;
260
+ // Bounded: a stale list full of vanished paths must not turn one search into a disk scan.
261
+ for (const hit of ranked.slice(0, limit * 4)) {
262
+ if (entries.length >= limit)
263
+ break;
264
+ checked++;
265
+ const relative = index.paths[hit.index];
266
+ const target = path.join(scope, relative);
267
+ let stat;
268
+ try {
269
+ await this.resolve(target, false, false, readOnly);
270
+ stat = await fs.lstat(target);
271
+ }
272
+ catch {
273
+ continue;
274
+ }
275
+ if (!stat.isFile() && !stat.isDirectory())
276
+ continue;
277
+ entries.push({ name: path.basename(relative), path: path.join(displayedScope, relative), directory: stat.isDirectory(), match: relative, positions: hit.positions });
278
+ }
279
+ // Candidates the fence dropped are not matches; the unchecked tail is counted as is.
280
+ return { path: displayedScope, entries, total: entries.length + ranked.length - checked, truncated: index.truncated, source: index.source };
281
+ }
282
+ async expire() { await this.uploads.expire(); }
283
+ async dispose() { await this.uploads.dispose(); }
284
+ }
@@ -1,28 +1,28 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { constants } from 'node:fs';
3
- import { link, lstat, mkdir, open, readdir, rename, stat, unlink } from 'node:fs/promises';
3
+ import { link, lstat, mkdir, open, readdir, realpath, rename, stat, unlink } from 'node:fs/promises';
4
4
  import { basename, dirname, join } from 'node:path';
5
- import { ComputerFileError, computerFileError, resolveComputerDirCreatePath, resolveComputerDirPath, resolveComputerFilePath } from './filePaths.js';
5
+ import { canonicalRoot, FileError, pinned, resolvePinnedDir, resolvePinnedDirCreate, resolvePinnedLeaf, resolveRelativeDir, resolveRelativeDirCreate, resolveRelativeFile, throwIfAborted, toFileError, withinRoot, } from './fence.js';
6
+ import { assertNotCredentialPath } from './credentials.js';
7
+ import { assertScopeAllows } from './scope.js';
8
+ /** Text file semantics of the platform agent tools (`file_*_request`): request-root scope only. */
6
9
  export const MAX_READ_BYTES = 8 * 1024 * 1024;
7
10
  export const MAX_PAGE_BYTES = 256 * 1024;
8
11
  export const MAX_WRITE_BYTES = 1024 * 1024;
9
- // Transport-free primitives. Untrusted channel requests must use fileWorkerClient, which pins the
10
- // parent directory before invoking these with resolvePinnedLeafPath; pathname-only resolution here
11
- // is retained for unit tests and trusted callers, not as a security boundary against rename races.
12
+ // Transport-free engines (formerly fileOps.ts). Untrusted requests must go through the scope entry points at the
13
+ // bottom of this file (or the legacy fork-per-request worker), which pin the parent directory before invoking these
14
+ // with a pinned leaf resolver; pathname-only resolution here is retained for unit tests and trusted callers, not as a
15
+ // security boundary against rename races.
12
16
  const hash = (bytes) => createHash('sha256').update(bytes).digest('hex');
13
- function throwIfAborted(signal) {
14
- if (signal?.aborted)
15
- throw new ComputerFileError('workstation_offline', 'File operation was cancelled');
16
- }
17
17
  async function snapshotFile(file, signal) {
18
18
  let handle;
19
19
  try {
20
20
  handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
21
21
  const before = await handle.stat({ bigint: true });
22
22
  if (!before.isFile())
23
- throw new ComputerFileError('not_regular_file', 'Target is not a regular file');
23
+ throw new FileError('not_regular_file', 'Target is not a regular file');
24
24
  if (before.size > BigInt(MAX_READ_BYTES))
25
- throw new ComputerFileError('too_large', 'File exceeds 8 MiB');
25
+ throw new FileError('too_large', 'File exceeds 8 MiB');
26
26
  const parts = [];
27
27
  let total = 0;
28
28
  while (true) {
@@ -33,25 +33,25 @@ async function snapshotFile(file, signal) {
33
33
  break;
34
34
  total += bytesRead;
35
35
  if (total > MAX_READ_BYTES)
36
- throw new ComputerFileError('too_large', 'File exceeds 8 MiB');
36
+ throw new FileError('too_large', 'File exceeds 8 MiB');
37
37
  parts.push(chunk.subarray(0, bytesRead));
38
38
  }
39
39
  const after = await handle.stat({ bigint: true });
40
40
  if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs || BigInt(total) !== after.size) {
41
- throw new ComputerFileError('conflict', 'File changed during read');
41
+ throw new FileError('conflict', 'File changed during read');
42
42
  }
43
43
  return Buffer.concat(parts, total);
44
44
  }
45
45
  catch (error) {
46
- throw computerFileError(error);
46
+ throw toFileError(error);
47
47
  }
48
48
  finally {
49
49
  await handle?.close();
50
50
  }
51
51
  }
52
- export async function readComputerFile(request, signal, resolvePath = resolveComputerFilePath) {
52
+ export async function readTextFile(request, signal, resolvePath = resolveRelativeFile) {
53
53
  if (!Number.isSafeInteger(request.startLine) || request.startLine < 1 || !Number.isSafeInteger(request.maxLines) || request.maxLines < 1 || request.maxLines > 2000) {
54
- throw new ComputerFileError('invalid_argument', 'Invalid line range');
54
+ throw new FileError('invalid_argument', 'Invalid line range');
55
55
  }
56
56
  const file = await resolvePath(request.root, request.path, false);
57
57
  let bytes;
@@ -59,7 +59,7 @@ export async function readComputerFile(request, signal, resolvePath = resolveCom
59
59
  bytes = await snapshotFile(file, signal);
60
60
  }
61
61
  catch (error) {
62
- if (!(error instanceof ComputerFileError) || error.code !== 'conflict')
62
+ if (!(error instanceof FileError) || error.code !== 'conflict')
63
63
  throw error;
64
64
  bytes = await snapshotFile(file, signal);
65
65
  }
@@ -68,7 +68,7 @@ export async function readComputerFile(request, signal, resolvePath = resolveCom
68
68
  source = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
69
69
  }
70
70
  catch {
71
- throw new ComputerFileError('invalid_encoding', 'File is not valid UTF-8');
71
+ throw new FileError('invalid_encoding', 'File is not valid UTF-8');
72
72
  }
73
73
  const lines = [];
74
74
  for (let begin = 0; begin < source.length;) {
@@ -84,7 +84,7 @@ export async function readComputerFile(request, signal, resolvePath = resolveCom
84
84
  for (let index = first; index < lines.length && selected < request.maxLines; index++) {
85
85
  const lineBytes = Buffer.byteLength(lines[index], 'utf8');
86
86
  if (lineBytes > MAX_PAGE_BYTES)
87
- throw new ComputerFileError('line_too_long', 'A line exceeds 256 KiB');
87
+ throw new FileError('line_too_long', 'A line exceeds 256 KiB');
88
88
  if (pageBytes + lineBytes > MAX_PAGE_BYTES)
89
89
  break;
90
90
  content += lines[index];
@@ -129,17 +129,17 @@ function validUnicode(content) {
129
129
  }
130
130
  return true;
131
131
  }
132
- export async function writeComputerFile(request, signal, resolvePath = resolveComputerFilePath) {
132
+ export async function writeTextFile(request, signal, resolvePath = resolveRelativeFile) {
133
133
  if (request.content.includes('\0') || !validUnicode(request.content)) {
134
- throw new ComputerFileError('invalid_encoding', 'Content must be valid UTF-8 text without NUL');
134
+ throw new FileError('invalid_encoding', 'Content must be valid UTF-8 text without NUL');
135
135
  }
136
136
  const bytes = Buffer.from(request.content, 'utf8');
137
137
  if (bytes.length > MAX_WRITE_BYTES)
138
- throw new ComputerFileError('too_large', 'Content exceeds 1 MiB');
138
+ throw new FileError('too_large', 'Content exceeds 1 MiB');
139
139
  if (request.mode !== 'create' && request.mode !== 'replace')
140
- throw new ComputerFileError('invalid_argument', 'Invalid write mode');
140
+ throw new FileError('invalid_argument', 'Invalid write mode');
141
141
  if (request.mode === 'create' ? request.expectedSha256 !== undefined : !/^[0-9a-f]{64}$/.test(request.expectedSha256 ?? '')) {
142
- throw new ComputerFileError('invalid_argument', 'create forbids expected_sha256; replace requires lowercase SHA-256');
142
+ throw new FileError('invalid_argument', 'create forbids expected_sha256; replace requires lowercase SHA-256');
143
143
  }
144
144
  throwIfAborted(signal);
145
145
  const file = await resolvePath(request.root, request.path, request.mode === 'create');
@@ -150,12 +150,12 @@ export async function writeComputerFile(request, signal, resolvePath = resolveCo
150
150
  if (request.mode === 'replace') {
151
151
  const current = await snapshotFile(file, signal);
152
152
  if (hash(current) !== request.expectedSha256)
153
- throw new ComputerFileError('conflict', 'File changed since it was read');
153
+ throw new FileError('conflict', 'File changed since it was read');
154
154
  try {
155
155
  mode = (await stat(file)).mode & 0o777;
156
156
  }
157
157
  catch (error) {
158
- throw computerFileError(error);
158
+ throw toFileError(error);
159
159
  }
160
160
  }
161
161
  const parent = dirname(file);
@@ -178,18 +178,18 @@ export async function writeComputerFile(request, signal, resolvePath = resolveCo
178
178
  return { path: request.path, sizeBytes: bytes.length, sha256: hash(bytes), created: request.mode === 'create' };
179
179
  }
180
180
  catch (error) {
181
- throw computerFileError(error);
181
+ throw toFileError(error);
182
182
  }
183
183
  finally {
184
184
  await handle?.close();
185
185
  await unlink(temporary).catch(error => { if (error.code !== 'ENOENT')
186
- throw computerFileError(error); });
186
+ throw toFileError(error); });
187
187
  }
188
188
  });
189
189
  }
190
- export async function listComputerDir(request, signal, resolvePath = resolveComputerDirPath) {
190
+ export async function listTextDir(request, signal, resolvePath = resolveRelativeDir) {
191
191
  if (!Number.isSafeInteger(request.maxEntries) || request.maxEntries < 1 || request.maxEntries > 2000) {
192
- throw new ComputerFileError('invalid_argument', 'Invalid entry range');
192
+ throw new FileError('invalid_argument', 'Invalid entry range');
193
193
  }
194
194
  throwIfAborted(signal);
195
195
  const dir = await resolvePath(request.root, request.path);
@@ -220,7 +220,7 @@ export async function listComputerDir(request, signal, resolvePath = resolveComp
220
220
  }
221
221
  catch (error) {
222
222
  if (error.code !== 'ENOENT')
223
- throw computerFileError(error);
223
+ throw toFileError(error);
224
224
  continue;
225
225
  }
226
226
  }
@@ -232,7 +232,7 @@ export async function listComputerDir(request, signal, resolvePath = resolveComp
232
232
  catch (error) {
233
233
  // A file that vanished between readdir and lstat simply drops out of the snapshot.
234
234
  if (error.code !== 'ENOENT')
235
- throw computerFileError(error);
235
+ throw toFileError(error);
236
236
  continue;
237
237
  }
238
238
  }
@@ -241,16 +241,16 @@ export async function listComputerDir(request, signal, resolvePath = resolveComp
241
241
  const nextAfter = truncated ? entries[entries.length - 1]?.name : undefined;
242
242
  return { path: request.path, entries, truncated, ...(nextAfter !== undefined ? { nextAfter } : {}) };
243
243
  }
244
- export async function createComputerDir(request, signal, resolvePlan = resolveComputerDirCreatePath) {
244
+ export async function createTextDir(request, signal, resolvePlan = resolveRelativeDirCreate) {
245
245
  throwIfAborted(signal);
246
246
  const plan = await resolvePlan(request.root, request.path, request.parents);
247
247
  if (plan.missing.length === 0) {
248
248
  if (!request.parents)
249
- throw new ComputerFileError('conflict', 'Target already exists');
249
+ throw new FileError('conflict', 'Target already exists');
250
250
  return { path: request.path, created: false };
251
251
  }
252
252
  if (!request.parents && plan.missing.length > 1) {
253
- throw new ComputerFileError('not_found', 'Parent directory not found');
253
+ throw new FileError('not_found', 'Parent directory not found');
254
254
  }
255
255
  let cursor = plan.anchor;
256
256
  let created = false;
@@ -263,15 +263,128 @@ export async function createComputerDir(request, signal, resolvePlan = resolveCo
263
263
  }
264
264
  catch (error) {
265
265
  if (error.code !== 'EEXIST')
266
- throw computerFileError(error);
266
+ throw toFileError(error);
267
267
  if (!request.parents)
268
- throw new ComputerFileError('conflict', 'Target already exists');
268
+ throw new FileError('conflict', 'Target already exists');
269
269
  }
270
270
  // A component we just made (or found racing us) must be a real directory, never a symlink stand-in.
271
- const entry = await lstat(target).catch(error => { throw computerFileError(error); });
271
+ const entry = await lstat(target).catch(error => { throw toFileError(error); });
272
272
  if (entry.isSymbolicLink() || !entry.isDirectory())
273
- throw new ComputerFileError('conflict', 'Path is not a directory');
273
+ throw new FileError('conflict', 'Path is not a directory');
274
274
  cursor = target;
275
275
  }
276
276
  return { path: request.path, created };
277
277
  }
278
+ const targetLocks = new Map();
279
+ async function serialTarget(target, action) {
280
+ const previous = targetLocks.get(target) ?? Promise.resolve();
281
+ let release;
282
+ const gate = new Promise(resolve => { release = resolve; });
283
+ const queued = previous.then(() => gate);
284
+ targetLocks.set(target, queued);
285
+ await previous;
286
+ try {
287
+ return await action();
288
+ }
289
+ finally {
290
+ release();
291
+ if (targetLocks.get(target) === queued)
292
+ targetLocks.delete(target);
293
+ }
294
+ }
295
+ // The worker client checked for cancellation after pathname validation, when it was about to start the operation.
296
+ async function throwIfAbortedThen(signal, run) {
297
+ throwIfAborted(signal);
298
+ return run();
299
+ }
300
+ function requireRequestRoot(scope, operation) {
301
+ if (scope?.kind !== 'request-root')
302
+ throw new FileError('invalid_argument', 'Text file operations require a request root');
303
+ assertScopeAllows(scope, operation);
304
+ }
305
+ /**
306
+ * Runs `run` with the cwd pinned to `anchor`, which must still be inside the canonical root after chdir. Cancellation
307
+ * keeps the file worker's contract: a request cancelled once it is under way reports `outcome_unknown` for mutations
308
+ * and `workstation_offline` for reads.
309
+ */
310
+ async function anchored(operation, root, anchor, leaves, signal, hooks, run) {
311
+ const insideRoot = withinRoot(root);
312
+ try {
313
+ await hooks?.afterValidation?.();
314
+ return await pinned(anchor, async () => {
315
+ await hooks?.afterPinned?.();
316
+ throwIfAborted(signal);
317
+ // What each existing leaf resolves to, by the name stored on disk: a spelling a case-insensitive file system
318
+ // maps onto a store (`.git-credentialſ`) is judged by the store it opens, not by how it was written.
319
+ for (const leaf of leaves) {
320
+ let stored;
321
+ try {
322
+ stored = await realpath(leaf);
323
+ }
324
+ catch {
325
+ continue;
326
+ }
327
+ assertNotCredentialPath(stored);
328
+ }
329
+ return run();
330
+ }, {
331
+ // The directory the kernel now holds, and what the operation touches below it, are checked again: a directory
332
+ // swapped for a link after validation must not reach credential storage (credentials.ts).
333
+ verify: (pinnedReal, parent) => {
334
+ insideRoot(pinnedReal, parent);
335
+ assertNotCredentialPath(pinnedReal);
336
+ for (const leaf of leaves)
337
+ assertNotCredentialPath(join(pinnedReal, leaf));
338
+ },
339
+ signal,
340
+ });
341
+ }
342
+ catch (error) {
343
+ if (signal?.aborted)
344
+ throw new FileError(operation === 'write' || operation === 'mkdir' ? 'outcome_unknown' : 'workstation_offline', 'File operation was cancelled');
345
+ throw toFileError(error);
346
+ }
347
+ }
348
+ export async function readText(scope, request, signal, hooks) {
349
+ requireRequestRoot(scope, 'read');
350
+ const root = await canonicalRoot(scope.root);
351
+ const target = await resolveRelativeFile(root, request.path, false);
352
+ assertNotCredentialPath(target);
353
+ throwIfAborted(signal);
354
+ // The pinned read sees only the leaf; the result names the workspace-relative path it was asked for.
355
+ const data = await anchored('read', root, dirname(target), [basename(target)], signal, hooks, () => readTextFile({ ...request, root: '.', path: basename(target) }, signal, resolvePinnedLeaf));
356
+ return { ...data, path: request.path };
357
+ }
358
+ export async function writeText(scope, request, signal, hooks) {
359
+ requireRequestRoot(scope, 'write');
360
+ const root = await canonicalRoot(scope.root);
361
+ const target = await resolveRelativeFile(root, request.path, request.mode === 'create');
362
+ assertNotCredentialPath(target);
363
+ const data = await serialTarget(target, () => throwIfAbortedThen(signal, () => anchored('write', root, dirname(target), [basename(target)], signal, hooks, () => writeTextFile({ ...request, root: '.', path: basename(target) }, signal, resolvePinnedLeaf))));
364
+ return { ...data, path: request.path };
365
+ }
366
+ export async function listDir(scope, request, signal, hooks) {
367
+ requireRequestRoot(scope, 'list');
368
+ const root = await canonicalRoot(scope.root);
369
+ const target = await resolveRelativeDir(root, request.path);
370
+ assertNotCredentialPath(target);
371
+ throwIfAborted(signal);
372
+ // Listing the root pins the root itself; '' is the zero-component leaf.
373
+ const anchor = target === root ? root : dirname(target);
374
+ const leaf = target === root ? '' : basename(target);
375
+ const data = await anchored('list', root, anchor, leaf === '' ? [] : [leaf], signal, hooks, () => listTextDir({ ...request, root: '.', path: leaf }, signal, resolvePinnedDir));
376
+ return { ...data, path: request.path };
377
+ }
378
+ export async function makeDir(scope, request, signal, hooks) {
379
+ requireRequestRoot(scope, 'mkdir');
380
+ const root = await canonicalRoot(scope.root);
381
+ const plan = await resolveRelativeDirCreate(root, request.path, request.parents);
382
+ // Every directory this would create, not only the last one.
383
+ const created = plan.missing.map((_, index) => plan.missing.slice(0, index + 1).join('/'));
384
+ assertNotCredentialPath(plan.anchor);
385
+ for (const leaf of created)
386
+ assertNotCredentialPath(join(plan.anchor, leaf));
387
+ throwIfAborted(signal);
388
+ const data = await anchored('mkdir', root, plan.anchor, created, signal, hooks, () => createTextDir({ ...request, root: '.', path: plan.missing.join('/') }, signal, resolvePinnedDirCreate));
389
+ return { ...data, path: request.path };
390
+ }