@platless/axom-cli 0.1.0 → 0.5.9

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 (281) hide show
  1. package/README.md +1 -1
  2. package/dist/authScopes.js +19 -0
  3. package/dist/axom-patch/cli/README.md +132 -0
  4. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  6. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  7. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  8. package/dist/axom-patch/cli/account-limits/generate.py +120 -0
  9. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
  10. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  11. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  12. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  13. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  14. package/dist/axom-patch/cli/nightly-task.md +152 -0
  15. package/dist/axom-patch/cli/pi-steering.json +37 -0
  16. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  17. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
  18. package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
  19. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  20. package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
  21. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
  22. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  23. package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
  24. package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
  25. package/dist/axom-patch/cli/qoder-steering.json +52 -0
  26. package/dist/axom-patch/cli/qoder-steering.md +30 -0
  27. package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
  28. package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
  29. package/dist/axom-patch/cli/runtime.json +63 -0
  30. package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
  31. package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
  32. package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
  33. package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
  34. package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
  35. package/dist/axom-patch/cli/zcode-models.json +38 -0
  36. package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
  37. package/dist/cli.js +36 -1
  38. package/dist/commands/auth.js +4 -13
  39. package/dist/commands/execute.js +4 -4
  40. package/dist/commands/record.js +6 -6
  41. package/dist/commands/struct.js +4 -4
  42. package/dist/commands/workstation.js +2 -2
  43. package/dist/credentials.js +2 -2
  44. package/dist/install/claude/commands.js +13 -13
  45. package/dist/keepers/acp/keeper.mjs +3083 -0
  46. package/dist/keepers/acp/manifest.json +9 -0
  47. package/dist/keepers/terminal/keeper.mjs +8469 -0
  48. package/dist/keepers/terminal/manifest.json +9 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  52. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  53. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  54. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  55. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  56. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  57. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  58. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  59. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  60. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  61. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  62. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  63. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  64. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  65. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  66. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  67. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  68. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  69. package/dist/mcp.js +32 -14
  70. package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
  71. package/dist/runtime-deps/qoder-sdk/package.json +8 -0
  72. package/dist/test/keeperKit.js +67 -0
  73. package/dist/workstation/acp/acpAgents.js +2514 -681
  74. package/dist/workstation/acp/acpClient.js +95 -318
  75. package/dist/workstation/acp/activityBusy.js +43 -0
  76. package/dist/workstation/acp/activityReporting.js +54 -0
  77. package/dist/workstation/acp/adoption.js +28 -356
  78. package/dist/workstation/acp/authUrlSniffer.js +36 -0
  79. package/dist/workstation/acp/binaryInstall.js +100 -27
  80. package/dist/workstation/acp/builds/gc.js +245 -0
  81. package/dist/workstation/acp/builds/install/binary.js +28 -0
  82. package/dist/workstation/acp/builds/install/npm.js +235 -0
  83. package/dist/workstation/acp/builds/install/uv.js +254 -0
  84. package/dist/workstation/acp/builds/migrate.js +195 -0
  85. package/dist/workstation/acp/builds/model.js +128 -0
  86. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  87. package/dist/workstation/acp/builds/store.js +352 -0
  88. package/dist/workstation/acp/commandTableProbe.js +68 -0
  89. package/dist/workstation/acp/configOptions.js +103 -1
  90. package/dist/workstation/acp/delegationMcp.js +24 -0
  91. package/dist/workstation/acp/envSecrets.js +27 -0
  92. package/dist/workstation/acp/initializeResponse.js +285 -0
  93. package/dist/workstation/acp/installErrors.js +12 -2
  94. package/dist/workstation/acp/installProbe.js +1 -1
  95. package/dist/workstation/acp/managedPatches.js +144 -0
  96. package/dist/workstation/acp/outbox.js +330 -0
  97. package/dist/workstation/acp/patchAssets.js +80 -0
  98. package/dist/workstation/acp/permission.js +335 -37
  99. package/dist/workstation/acp/processor/checkpoint.js +87 -0
  100. package/dist/workstation/acp/processor/commands.js +35 -0
  101. package/dist/workstation/acp/processor/control.js +355 -0
  102. package/dist/workstation/acp/processor/emit.js +26 -0
  103. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  104. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  105. package/dist/workstation/acp/processor/origins.js +48 -0
  106. package/dist/workstation/acp/processor/output.js +67 -0
  107. package/dist/workstation/acp/processor/projection.js +42 -0
  108. package/dist/workstation/acp/processor/record.js +128 -0
  109. package/dist/workstation/acp/processor/runtime.js +181 -0
  110. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  111. package/dist/workstation/acp/processor/session.js +525 -0
  112. package/dist/workstation/acp/processor/sideChannel.js +80 -0
  113. package/dist/workstation/acp/processor/state.js +74 -0
  114. package/dist/workstation/acp/processor/step.js +459 -0
  115. package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
  116. package/dist/workstation/acp/processor/test/harness.js +120 -0
  117. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  118. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  119. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  120. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  121. package/dist/workstation/acp/processor/transport/local.js +269 -0
  122. package/dist/workstation/acp/processor/transport/types.js +78 -0
  123. package/dist/workstation/acp/processor/updates.js +364 -0
  124. package/dist/workstation/acp/processorSession.js +1512 -0
  125. package/dist/workstation/acp/qoderStartup.js +45 -0
  126. package/dist/workstation/acp/registry/catalog.js +385 -4
  127. package/dist/workstation/acp/registry/detect.js +55 -0
  128. package/dist/workstation/acp/registry/launch.js +61 -119
  129. package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
  130. package/dist/workstation/acp/steering.js +58 -0
  131. package/dist/workstation/acp/switching.js +114 -0
  132. package/dist/workstation/acp/terminalLogin.js +171 -0
  133. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  134. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  135. package/dist/workstation/acp/test/daemonLife.js +106 -0
  136. package/dist/workstation/acp/test/fakeAgent.js +185 -9
  137. package/dist/workstation/acp/test/keeperWorld.js +267 -0
  138. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  139. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  140. package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
  141. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  142. package/dist/workstation/acp/test/raw/sources.js +249 -0
  143. package/dist/workstation/acp/test/realAdapter.js +377 -0
  144. package/dist/workstation/acp/test/scriptAgent.js +206 -0
  145. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  146. package/dist/workstation/acp/timedCommand.js +4 -2
  147. package/dist/workstation/acp/uvxInstall.js +72 -122
  148. package/dist/workstation/acp/validate.js +33 -5
  149. package/dist/workstation/agentFlow.js +103 -0
  150. package/dist/workstation/agentSessions.js +1686 -348
  151. package/dist/workstation/channels/agent.js +14 -6
  152. package/dist/workstation/channels/computer.js +203 -7
  153. package/dist/workstation/channels/computerExecs.js +200 -0
  154. package/dist/workstation/channels/management.js +86 -6
  155. package/dist/workstation/config.js +3 -1
  156. package/dist/workstation/connection/encryptedChannel.js +88 -21
  157. package/dist/workstation/daemon.js +309 -6
  158. package/dist/workstation/daemonctl.js +28 -2
  159. package/dist/workstation/desktop/binding.js +36 -0
  160. package/dist/workstation/desktop/bridge.js +212 -0
  161. package/dist/workstation/desktop/flow-control.js +52 -0
  162. package/dist/workstation/desktop/focused-text.js +130 -0
  163. package/dist/workstation/desktop/protocol.js +35 -0
  164. package/dist/workstation/exec/attach.js +126 -0
  165. package/dist/workstation/exec/command.js +40 -0
  166. package/dist/workstation/exec/durable.js +517 -0
  167. package/dist/workstation/exec/fallback.js +106 -0
  168. package/dist/workstation/exec/policy.js +837 -0
  169. package/dist/workstation/exec/shaping.js +211 -0
  170. package/dist/workstation/exec.js +3 -112
  171. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  172. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  173. package/dist/workstation/files/client.js +455 -0
  174. package/dist/workstation/files/errors.js +28 -0
  175. package/dist/workstation/files/fence.js +350 -0
  176. package/dist/workstation/files/find/fileIndex.js +153 -0
  177. package/dist/workstation/files/find/fuzzy.js +155 -0
  178. package/dist/workstation/files/host.js +122 -0
  179. package/dist/workstation/files/hostProtocol.js +2 -0
  180. package/dist/workstation/files/media/cache.js +620 -0
  181. package/dist/workstation/files/media/encoding.js +308 -0
  182. package/dist/workstation/files/media/estimator.js +100 -0
  183. package/dist/workstation/files/media/media.js +325 -0
  184. package/dist/workstation/files/media/packedAudio.js +23 -0
  185. package/dist/workstation/files/media/scheduler.js +551 -0
  186. package/dist/workstation/files/media/types.js +5 -0
  187. package/dist/workstation/files/scope.js +45 -0
  188. package/dist/workstation/files/structured.js +284 -0
  189. package/dist/workstation/files/text.js +356 -0
  190. package/dist/workstation/files/uploads.js +351 -0
  191. package/dist/workstation/journal.js +65 -236
  192. package/dist/workstation/keepers/acp/client.js +101 -0
  193. package/dist/workstation/keepers/acp/envelope.js +62 -0
  194. package/dist/workstation/keepers/acp/host.js +419 -0
  195. package/dist/workstation/keepers/acp/main.js +18 -0
  196. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  197. package/dist/workstation/keepers/acp/params.js +137 -0
  198. package/dist/workstation/keepers/acp/protocol.js +66 -0
  199. package/dist/workstation/keepers/acp/recover.js +120 -0
  200. package/dist/workstation/keepers/acp/session.js +584 -0
  201. package/dist/workstation/keepers/acp/stderr.js +154 -0
  202. package/dist/workstation/keepers/kit/activity.js +103 -0
  203. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  204. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  205. package/dist/workstation/keepers/kit/bundle.js +120 -0
  206. package/dist/workstation/keepers/kit/client.js +432 -0
  207. package/dist/workstation/keepers/kit/identity.js +21 -0
  208. package/dist/workstation/keepers/kit/ipc.js +112 -0
  209. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  210. package/dist/workstation/keepers/kit/launcher.js +249 -0
  211. package/dist/workstation/keepers/kit/lock.js +123 -0
  212. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  213. package/dist/workstation/keepers/kit/paths.js +75 -0
  214. package/dist/workstation/keepers/kit/protocol.js +43 -0
  215. package/dist/workstation/keepers/kit/server.js +518 -0
  216. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  217. package/dist/workstation/keepers/legacy.js +296 -0
  218. package/dist/workstation/keepers/policy.js +576 -0
  219. package/dist/workstation/keepers/stop.js +34 -0
  220. package/dist/workstation/keepers/terminal/client.js +213 -0
  221. package/dist/workstation/keepers/terminal/exec.js +981 -0
  222. package/dist/workstation/keepers/terminal/generations.js +129 -0
  223. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  224. package/dist/workstation/keepers/terminal/main.js +43 -0
  225. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  226. package/dist/workstation/keepers/terminal/params.js +27 -0
  227. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  228. package/dist/workstation/keepers/terminal/pty.js +330 -0
  229. package/dist/workstation/oauth/deviceAuthorization.js +21 -12
  230. package/dist/workstation/protocol/accountLimits.js +47 -0
  231. package/dist/workstation/protocol/agent.js +201 -8
  232. package/dist/workstation/protocol/common.js +36 -0
  233. package/dist/workstation/protocol/computer.js +107 -7
  234. package/dist/workstation/protocol/management.js +50 -2
  235. package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
  236. package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
  237. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
  238. package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
  239. package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
  240. package/dist/workstation/qoderAdapter/contract.js +78 -0
  241. package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
  242. package/dist/workstation/qoderAdapter/history.js +85 -0
  243. package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
  244. package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
  245. package/dist/workstation/qoderAdapter/main.js +95 -0
  246. package/dist/workstation/qoderAdapter/mapper.js +117 -0
  247. package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
  248. package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
  249. package/dist/workstation/qoderAdapter/permissions.js +61 -0
  250. package/dist/workstation/qoderAdapter/profile.js +93 -0
  251. package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
  252. package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
  253. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
  254. package/dist/workstation/qoderAdapter/session.js +319 -0
  255. package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
  256. package/dist/workstation/qoderAdapter/subwork.js +85 -0
  257. package/dist/workstation/qoderAdapter/taskControl.js +38 -0
  258. package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
  259. package/dist/workstation/qoderAdapter/types.js +1 -0
  260. package/dist/workstation/qoderAdapter/usage.js +37 -0
  261. package/dist/workstation/register.js +4 -0
  262. package/dist/workstation/systemMetrics.js +110 -1
  263. package/dist/workstation/tools/browser.js +394 -0
  264. package/dist/workstation/tools/client.js +277 -0
  265. package/dist/workstation/tools/info.js +50 -0
  266. package/dist/workstation/tools/keeperProcess.js +124 -0
  267. package/dist/workstation/tools/router.js +55 -0
  268. package/dist/workstation/tools/terminalRouter.js +222 -0
  269. package/dist/workstation/tools/validation.js +86 -0
  270. package/dist/workstation/update/coordinator.js +32 -0
  271. package/dist/workstation/update/npmInstaller.js +2 -0
  272. package/dist/workstation/update/ptyPreflight.js +45 -0
  273. package/dist/workstation/update/taskRegistry.js +141 -23
  274. package/dist/workstation/update/watchdog.js +66 -0
  275. package/package.json +20 -7
  276. package/skills/axom/SKILL.md +55 -17
  277. package/skills/axom-definition/SKILL.md +32 -32
  278. package/skills/axom-platform/SKILL.md +39 -39
  279. package/dist/workstation/acp/acpSession.js +0 -1047
  280. package/dist/workstation/acp/contract.js +0 -494
  281. package/dist/workstation/acp/draftInterceptor.js +0 -154
@@ -0,0 +1,254 @@
1
+ /**
2
+ * uvx-distributed adapters as daemon-owned, per-version builds (spec 2026-10-11 §6.2, Task 15b):
3
+ * `uv tool install` with `UV_TOOL_DIR=<build>/tools` and `UV_TOOL_BIN_DIR=<build>/bin`, so every
4
+ * version lives in its own `uv/<pkg>/<version>/` and nothing is written to the user's `uv tool dir`
5
+ * or `~/.local/bin`. Interpreters uv has to download go to the daemon-owned `uv/.python/`.
6
+ *
7
+ * ## Installed in place, published by a marker (ruling)
8
+ *
9
+ * A uv tool environment embeds its absolute location (entry shebang, bin symlink, receipt — measured,
10
+ * see `uvxInstall.ts`), so it cannot be staged and renamed like npm and binary trees. Instead, under
11
+ * the agent lock **and** a per-tree lock:
12
+ *
13
+ * 1. a tree with a valid `.axom-uv-build.json` is complete and is reused as is (no `uv` at all);
14
+ * 2. otherwise anything at that path is the remains of a killed install — moved to `.trash/`;
15
+ * 3. `uv tool install` runs straight into the final directory; any failure moves it to `.trash/`;
16
+ * 4. the entry is resolved from that build's own `uv tool list --show-paths` (same env);
17
+ * 5. the marker is written **last** (temp + fsync + rename + directory fsync).
18
+ *
19
+ * A daemon killed anywhere before step 5 leaves a tree without a marker: never recorded, never
20
+ * launched, replaced by the next Install. Killed after step 5 but before the `builds.json` commit, the
21
+ * next Install adopts the tree without running `uv` (the same "complete but unrecorded" rule as npm).
22
+ *
23
+ * Launch never runs `uv`: `Build.launch` names the tool's own executable inside the build, which runs
24
+ * with the absolute venv interpreter its shebang names.
25
+ */
26
+ import { existsSync, realpathSync, statSync } from 'node:fs';
27
+ import { mkdir, readFile, rm } from 'node:fs/promises';
28
+ import path from 'node:path';
29
+ import { assertSafeVersion, buildIdFor, inside, normalizeUvPackageName, UV_PYTHON_BIN_DIR, UV_PYTHON_DIR, uvBuildDir } from '../model.js';
30
+ import { writeFileDurably } from '../store.js';
31
+ import { findUvTool, listUvTools, parseUvxSpec, UV_PROBE_TIMEOUT_MS, uvToolEntrypointFrom } from '../../uvxInstall.js';
32
+ /** Written last into a uv build; its presence (and agreement with the path) is what "complete" means. */
33
+ export const UV_BUILD_MARKER = '.axom-uv-build.json';
34
+ /**
35
+ * The env every `uv` call for the build at `buildDir` runs with: its own tool and bin directories, and
36
+ * the daemon-owned interpreter directories. Laid over the daemon's environment, so a user's own uv
37
+ * configuration (an index mirror, `UV_CACHE_DIR`) still applies — only *where tools go* is ours.
38
+ */
39
+ export function uvBuildEnv(root, buildDir) {
40
+ return {
41
+ UV_TOOL_DIR: path.join(buildDir, 'tools'),
42
+ UV_TOOL_BIN_DIR: path.join(buildDir, 'bin'),
43
+ UV_PYTHON_INSTALL_DIR: inside(root, UV_PYTHON_DIR),
44
+ UV_PYTHON_BIN_DIR: inside(root, UV_PYTHON_BIN_DIR),
45
+ };
46
+ }
47
+ export async function readMarker(buildDir, name, version) {
48
+ let text;
49
+ try {
50
+ text = await readFile(path.join(buildDir, UV_BUILD_MARKER), 'utf8');
51
+ }
52
+ catch (error) {
53
+ if (error.code === 'ENOENT')
54
+ return { state: 'absent' };
55
+ return { state: 'unreadable', reason: errorText(error) };
56
+ }
57
+ let parsed;
58
+ try {
59
+ parsed = JSON.parse(text);
60
+ }
61
+ catch (error) {
62
+ return { state: 'unreadable', reason: `marker is not JSON (${errorText(error)})` };
63
+ }
64
+ if (parsed.schema !== 1 || parsed.package !== name || parsed.version !== version || typeof parsed.entry !== 'string') {
65
+ return { state: 'mismatch', reason: `marker names ${String(parsed.package)} ${String(parsed.version)}` };
66
+ }
67
+ try {
68
+ if (!statSync(inside(buildDir, parsed.entry)).isFile())
69
+ return { state: 'mismatch', reason: `entry ${parsed.entry} is not a file` };
70
+ }
71
+ catch (error) {
72
+ if (error.code === 'ENOENT')
73
+ return { state: 'mismatch', reason: `entry ${parsed.entry} is missing` };
74
+ return { state: 'unreadable', reason: errorText(error) };
75
+ }
76
+ return { state: 'valid', marker: parsed };
77
+ }
78
+ const errorText = (error) => (error instanceof Error ? error.message : String(error));
79
+ /** Whether any agent's record in `builds.json` names `dir` — such a tree is never thrown away by an install. */
80
+ async function referenced(store, dir) {
81
+ const document = await store.read();
82
+ return Object.values(document.agents).some((agent) => Object.values(agent.builds ?? {}).some((build) => build.dir === dir));
83
+ }
84
+ /**
85
+ * uv's own words for "no interpreter that fits, and none it could download" — only these trigger the
86
+ * fallback to an interpreter the user's own uv already knows; any other failure is that failure.
87
+ */
88
+ const INTERPRETER_UNAVAILABLE = [
89
+ /No interpreter found/i,
90
+ /No (?:suitable |compatible )?Python (?:installation|interpreter|version)s? (?:found|available)/i,
91
+ /Failed to (?:download|install|fetch) (?:managed )?(?:cpython|python|pypy)/i,
92
+ /python-build-standalone/i,
93
+ /does not satisfy Python\s*[<>=!~]/i,
94
+ ];
95
+ export function interpreterUnavailable(error) {
96
+ return !!error && INTERPRETER_UNAVAILABLE.some((pattern) => pattern.test(error));
97
+ }
98
+ /** The Python request uv's message names (`>=3.12` in "No interpreter found for Python >=3.12 in …"), if any. */
99
+ export function requestedPython(error) {
100
+ const named = error?.match(/No interpreter found for (?:Python )?(\S+(?: \S+)?) in /i)?.[1]
101
+ ?? error?.match(/does not satisfy Python\s*([<>=!~]=?\s*[0-9][0-9.*]*)/i)?.[1];
102
+ return named?.replace(/\s+/g, '');
103
+ }
104
+ /**
105
+ * `uv tool install` into a build, with one fallback (ruling 15c): the first attempt runs with the build's
106
+ * env (`UV_PYTHON_INSTALL_DIR` under the store), which hides the user's uv-managed interpreters. When it
107
+ * fails because no suitable interpreter could be found or downloaded (an offline first install, say),
108
+ * `uv python find [<request>]` is run with the user's own, unmodified environment — read-only discovery,
109
+ * nothing is written to the user's uv directories — and, if it names one, the install is retried with
110
+ * `--python <that path>`. `reset` clears the failed attempt's leftovers before the retry.
111
+ */
112
+ async function uvToolInstall(options) {
113
+ const { runUv, requirement, env, timeoutMs } = options;
114
+ const install = (python) => runUv(['tool', 'install', ...options.extraArgs, ...python, requirement], timeoutMs, env);
115
+ const first = await install([]);
116
+ if (first.ok || !interpreterUnavailable(first.error))
117
+ return first;
118
+ const request = requestedPython(first.error);
119
+ // No env argument: the user's default uv environment, read-only.
120
+ const found = await runUv(['python', 'find', ...(request ? [request] : [])], UV_PROBE_TIMEOUT_MS);
121
+ const python = found.ok ? found.stdout.trim().split('\n').pop()?.trim() : undefined;
122
+ if (!python || !path.isAbsolute(python))
123
+ return first;
124
+ console.info(`[acp-builds] uv found no interpreter for ${requirement} of its own; retrying with the user's ${python}`);
125
+ await options.reset();
126
+ return { ...await install(['--python', python]), python };
127
+ }
128
+ /** `absolute` (an executable uv reported) as a posix path relative to `buildDir`, following symlinks; refuses anything outside. */
129
+ function entryWithin(buildDir, absolute) {
130
+ const base = realpathSync(buildDir);
131
+ const target = realpathSync(absolute);
132
+ const relative = path.relative(base, target);
133
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
134
+ throw new Error(`uv reported an executable outside its build directory: ${absolute}`);
135
+ }
136
+ return relative.split(path.sep).join('/');
137
+ }
138
+ /**
139
+ * The uv build for registry specifier `spec` (`pkg==1.2.3`, `pkg@1.2.3`, or unpinned `pkg`), installed
140
+ * if it is not complete on disk yet. Not committed — the caller does that under the agent lock it holds.
141
+ */
142
+ export async function installUvBuild(options) {
143
+ const { store, runUv } = options;
144
+ const spec = parseUvxSpec(options.spec);
145
+ const name = normalizeUvPackageName(spec.name);
146
+ let version = spec.version;
147
+ if (version === undefined) {
148
+ const resolved = await resolveUnpinnedVersion(store, spec.name + spec.extras, name, runUv, options.timeoutMs);
149
+ if (!resolved.ok)
150
+ return resolved;
151
+ version = resolved.version;
152
+ }
153
+ assertSafeVersion(version);
154
+ const dir = uvBuildDir(name, version);
155
+ const buildDir = inside(store.root, dir);
156
+ return store.withTreeLock(dir, async () => {
157
+ const found = await readMarker(buildDir, name, version);
158
+ if (found.state === 'unreadable') {
159
+ return { ok: false, error: `The uv build at ${dir} cannot be checked (${found.reason}); it was left untouched.` };
160
+ }
161
+ let marker = found.state === 'valid' ? found.marker : undefined;
162
+ const downloaded = marker === undefined;
163
+ if (!marker) {
164
+ if (existsSync(buildDir)) {
165
+ // Without a valid marker this is a killed install's remains — unless a record names it, in which
166
+ // case it was a working build and something else is wrong: never delete it on a guess.
167
+ if (await referenced(store, dir)) {
168
+ return {
169
+ ok: false,
170
+ error: `The uv build at ${dir} is recorded as installed but its marker is ${found.state === 'mismatch' ? `wrong (${found.reason})` : 'missing'}; it was left untouched.`,
171
+ };
172
+ }
173
+ await store.discard(buildDir);
174
+ }
175
+ try {
176
+ await mkdir(buildDir, { recursive: true });
177
+ const env = uvBuildEnv(store.root, buildDir);
178
+ const requirement = `${spec.name}${spec.extras}==${version}`;
179
+ // `--compile-bytecode`: the interpreter then never writes `__pycache__` into the build while it
180
+ // runs, so a build stays byte-identical for as long as it exists.
181
+ const ran = await uvToolInstall({
182
+ runUv, requirement, extraArgs: ['--compile-bytecode'], env, timeoutMs: options.timeoutMs,
183
+ reset: async () => { await store.discard(buildDir); await mkdir(buildDir, { recursive: true }); },
184
+ });
185
+ if (!ran.ok) {
186
+ await store.discard(buildDir);
187
+ return { ok: false, error: ran.error ?? `uv tool install ${requirement} failed`, ...(ran.code === 'ENOENT' ? { uvMissing: true } : {}) };
188
+ }
189
+ const tools = await listUvTools(runUv, UV_PROBE_TIMEOUT_MS, env);
190
+ const installed = findUvTool(tools, name);
191
+ if (installed?.version !== version) {
192
+ throw new Error(`uv installed ${name} ${installed ? `v${installed.version}` : 'nothing'} when ${requirement} was asked for`);
193
+ }
194
+ const picked = uvToolEntrypointFrom(name, tools);
195
+ if (!picked.ok) {
196
+ throw new Error(picked.reason === 'ambiguous'
197
+ ? `${name} ${version} declares several executables and none is named like the package: ${picked.candidates.join(', ')}`
198
+ : `${name} ${version} installed no executable`);
199
+ }
200
+ marker = {
201
+ schema: 1, package: name, version, entry: entryWithin(buildDir, picked.path),
202
+ ...(ran.python ? { python: ran.python } : {}),
203
+ };
204
+ await writeFileDurably(path.join(buildDir, UV_BUILD_MARKER), `${JSON.stringify(marker, null, 2)}\n`);
205
+ }
206
+ catch (error) {
207
+ await store.discard(buildDir).catch(() => undefined);
208
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
209
+ }
210
+ }
211
+ const build = {
212
+ buildId: buildIdFor(version),
213
+ agentId: options.agentId,
214
+ kind: 'uv',
215
+ version,
216
+ package: name,
217
+ dir,
218
+ launch: { runtime: 'native', entry: marker.entry, args: [...options.args], env: { ...options.env } },
219
+ ...(typeof marker.python === 'string' ? { pythonFrom: marker.python } : {}),
220
+ registryVersion: options.registryVersion,
221
+ source: options.source,
222
+ installedBy: options.installedBy,
223
+ installedAt: (options.now ?? new Date()).toISOString(),
224
+ };
225
+ return { ok: true, build, downloaded };
226
+ });
227
+ }
228
+ /**
229
+ * An unpinned specifier names no build directory until uv has resolved it, and a uv tree cannot be
230
+ * moved afterwards. So the version is resolved by a throwaway install into staging (its tree is
231
+ * discarded), and the build is then installed pinned to exactly that version (from uv's cache).
232
+ */
233
+ async function resolveUnpinnedVersion(store, requirement, name, runUv, timeoutMs) {
234
+ const staged = await store.stagingDir();
235
+ try {
236
+ const env = uvBuildEnv(store.root, staged);
237
+ const ran = await uvToolInstall({
238
+ runUv, requirement, extraArgs: [], env, timeoutMs,
239
+ reset: async () => {
240
+ for (const sub of ['tools', 'bin'])
241
+ await rm(path.join(staged, sub), { recursive: true, force: true });
242
+ },
243
+ });
244
+ if (!ran.ok)
245
+ return { ok: false, error: ran.error ?? `uv tool install ${requirement} failed`, ...(ran.code === 'ENOENT' ? { uvMissing: true } : {}) };
246
+ const version = findUvTool(await listUvTools(runUv, UV_PROBE_TIMEOUT_MS, env), name)?.version;
247
+ if (!version)
248
+ return { ok: false, error: `uv reported success but ${requirement} is not in the installed tools` };
249
+ return { ok: true, version };
250
+ }
251
+ finally {
252
+ await store.discard(staged);
253
+ }
254
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Moving what an older CLI left on this machine into the build store (spec 2026-10-11 §6.2 "迁移").
3
+ *
4
+ * Before builds, npx agents ran out of npm's own `~/.npm/_npx/<hash>/node_modules` and binary agents
5
+ * out of `acp-adapters/<agentId>/<version>/`. Neither has a `builds.json` record, so without this every
6
+ * agent a user already had would read as not installed. Per agent, under its lock, once:
7
+ *
8
+ * - **npm**: the version the old launch path would have used (the *effective* version: registry, then
9
+ * catalog, then runtime.json override — `requestedVersionOf` in `acpAgents.ts`) is looked up in
10
+ * `_npx`. Found complete → copied into `npm/<sha16(name)>/<version>/pristine` (staging → rename) and,
11
+ * patched or not, made current. Not found → the first open runs one Install (`AcpAgents.open`).
12
+ * `_npx` itself is never modified or deleted: it is npm's, not ours.
13
+ * - **binary**: every `acp-adapters/<agentId>/<version>/` is renamed in place to `bin/<agentId>/<version>/`
14
+ * (same filesystem, so the same inodes: a running binary keeps running), a symlink is left at the old
15
+ * path for whatever a running adapter still reaches by path, and it is recorded; the version the old
16
+ * launch path picked (the pinned one if present, else the highest) becomes current. The links are
17
+ * never followed by the store itself (a symlink is not a legacy version) and are left for GC.
18
+ * - **uv**: nothing to move — the user's uv tool dir is not Axom's. The first open installs a build.
19
+ */
20
+ import { cp, lstat, mkdir, readdir, readFile, readlink, realpath, rename, rm, stat, symlink } from 'node:fs/promises';
21
+ import { constants, existsSync } from 'node:fs';
22
+ import { randomUUID } from 'node:crypto';
23
+ import path from 'node:path';
24
+ import { inside, npmPristineDir, assertSafePackageName, assertSafeVersion } from './model.js';
25
+ import { publishStaged } from './store.js';
26
+ /** Directory names under the `acp-adapters` root that are the build store's own, never a legacy agent. */
27
+ export const RESERVED_ROOT_NAMES = new Set(['npm', 'uv', 'bin', 'patched-npm', 'builds.json']);
28
+ async function manifestOf(packageDir) {
29
+ try {
30
+ return JSON.parse(await readFile(path.join(packageDir, 'package.json'), 'utf8'));
31
+ }
32
+ catch {
33
+ return undefined;
34
+ }
35
+ }
36
+ /**
37
+ * The `node_modules` of an `_npx/<hash>` entry holding exactly `name@version`, complete: the package's
38
+ * own `package.json` names it at that version, and npm finished reifying the tree (npm 7+ writes the
39
+ * hidden lockfile `node_modules/.package-lock.json` last). A half-written entry is not a copy we take —
40
+ * the first open installs instead, which only costs a download. Entries are tried in name order, so
41
+ * the answer does not depend on directory enumeration order.
42
+ */
43
+ export async function findNpxCopy(npmCache, name, version) {
44
+ assertSafePackageName(name);
45
+ const npxDir = path.join(npmCache, '_npx');
46
+ let entries;
47
+ try {
48
+ entries = (await readdir(npxDir)).sort();
49
+ }
50
+ catch {
51
+ return undefined;
52
+ }
53
+ for (const entry of entries) {
54
+ const nodeModules = path.join(npxDir, entry, 'node_modules');
55
+ const manifest = await manifestOf(path.join(nodeModules, ...name.split('/')));
56
+ if (manifest?.name !== name || manifest.version !== version)
57
+ continue;
58
+ if (!existsSync(path.join(nodeModules, '.package-lock.json')))
59
+ continue;
60
+ return nodeModules;
61
+ }
62
+ return undefined;
63
+ }
64
+ const within = (root, target) => target === root || target.startsWith(root + path.sep);
65
+ /**
66
+ * Copy the `node_modules` tree at `source` to `destination` so that it no longer depends on `source`:
67
+ * relative symlinks that stay inside the tree (npm's `.bin/*` → `../pkg/…`) are kept verbatim (they
68
+ * resolve the same way in the copy); a symlink that leaves the tree, or an absolute one, is replaced by
69
+ * a copy of what it points at (dereferenced). Files are cloned where the filesystem can (reflink).
70
+ */
71
+ export async function copyTreeSelfContained(source, destination) {
72
+ const root = await realpath(source);
73
+ await cp(root, destination, { recursive: true, verbatimSymlinks: true, mode: constants.COPYFILE_FICLONE, errorOnExist: true, force: false });
74
+ const links = (await readdir(root, { recursive: true, withFileTypes: true })).filter((entry) => entry.isSymbolicLink());
75
+ for (const link of links) {
76
+ const sourceLink = path.join(link.parentPath, link.name);
77
+ const relative = path.relative(root, sourceLink);
78
+ const copied = path.join(destination, relative);
79
+ const target = await readlink(sourceLink);
80
+ const resolved = path.resolve(path.dirname(sourceLink), target);
81
+ if (!path.isAbsolute(target) && within(root, resolved))
82
+ continue;
83
+ await rm(copied, { force: true });
84
+ if (within(root, resolved)) {
85
+ // Absolute but inside the tree: the same link, relative, so it points into the copy.
86
+ await symlink(path.relative(path.dirname(sourceLink), resolved), copied);
87
+ continue;
88
+ }
89
+ let real;
90
+ try {
91
+ real = await realpath(resolved);
92
+ }
93
+ catch {
94
+ // Dangling outside the tree: npm would not have run it either. Keep the link as it was.
95
+ await symlink(target, copied);
96
+ continue;
97
+ }
98
+ await cp(real, copied, { recursive: true, dereference: true, mode: constants.COPYFILE_FICLONE });
99
+ }
100
+ }
101
+ /**
102
+ * `npm/<sha16(name)>/<version>/pristine` made from an `_npx` copy (staging → rename). Already there →
103
+ * nothing copied. The caller holds the agent lock.
104
+ */
105
+ export async function adoptNpxCopy(options) {
106
+ const { store, name, version } = options;
107
+ assertSafeVersion(version);
108
+ const final = inside(store.root, npmPristineDir(name, version));
109
+ const installed = await manifestOf(inside(path.join(final, 'node_modules'), name));
110
+ if (installed?.name === name && installed.version === version)
111
+ return { copied: false };
112
+ const staged = await store.stagingDir();
113
+ try {
114
+ await copyTreeSelfContained(options.nodeModules, path.join(staged, 'node_modules'));
115
+ const copied = await manifestOf(inside(path.join(staged, 'node_modules'), name));
116
+ if (copied?.name !== name || copied.version !== version)
117
+ throw new Error(`npx copy of ${name}@${version} changed while it was copied`);
118
+ const { published } = await publishStaged(staged, final);
119
+ return { copied: published };
120
+ }
121
+ finally {
122
+ await store.discard(staged);
123
+ }
124
+ }
125
+ /** The legacy (pre-build) binary version directories of `agentId`: `acp-adapters/<agentId>/<version>/`. */
126
+ export async function legacyBinaryVersions(root, agentId) {
127
+ if (RESERVED_ROOT_NAMES.has(agentId) || agentId.startsWith('.'))
128
+ return [];
129
+ let entries;
130
+ try {
131
+ entries = await readdir(path.join(root, agentId), { withFileTypes: true });
132
+ }
133
+ catch {
134
+ return [];
135
+ }
136
+ return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.')).map((entry) => entry.name);
137
+ }
138
+ /**
139
+ * Rename `acp-adapters/<agentId>/<version>` to `bin/<agentId>/<version>`. `rename(2)` keeps every inode,
140
+ * so a process running from the old path keeps its executable and every file it already opened; and
141
+ * the old path is left as a symlink to the new one, so a running adapter that later opens or spawns a
142
+ * sibling **by path** (Antigravity ships its server and a helper side by side) still finds it.
143
+ * Refuses (returns false) across filesystems and when the destination already exists; never copies.
144
+ */
145
+ export async function moveLegacyBinary(root, agentId, version, destination) {
146
+ const from = path.join(root, agentId, version);
147
+ if (!(await lstat(from)).isDirectory())
148
+ return false;
149
+ if (existsSync(destination))
150
+ return false;
151
+ await mkdir(path.dirname(destination), { recursive: true });
152
+ if ((await stat(from)).dev !== (await stat(path.dirname(destination))).dev) {
153
+ console.warn(`[acp-builds] ${from} is on another filesystem than ${destination}; left where it is.`);
154
+ return false;
155
+ }
156
+ // The link is made first, under a hidden temporary name beside the old path (dangling until the move),
157
+ // so the old path is missing only between the two renames below — never across a symlink creation
158
+ // that could fail or be slow. It cannot be closed entirely: the old path *is* the directory, and
159
+ // replacing a directory with a link in one step needs an exchange rename (`renameat2(RENAME_EXCHANGE)`
160
+ // / `renamex_np(RENAME_SWAP)`) that Node does not expose.
161
+ const link = path.join(path.dirname(from), `.${version}.link-${randomUUID()}`);
162
+ let linked = true;
163
+ try {
164
+ // A junction on Windows (no privilege needed; it wants an absolute target).
165
+ if (process.platform === 'win32')
166
+ await symlink(destination, link, 'junction');
167
+ else
168
+ await symlink(path.relative(path.dirname(from), destination), link, 'dir');
169
+ }
170
+ catch (error) {
171
+ linked = false;
172
+ console.warn(`[acp-builds] could not prepare a link at ${from} for adapters still running from it: ${String(error)}`);
173
+ }
174
+ try {
175
+ await rename(from, destination);
176
+ }
177
+ catch (error) {
178
+ if (linked)
179
+ await rm(link, { force: true }).catch(() => undefined);
180
+ const code = error.code;
181
+ if (code === 'EXDEV' || code === 'EEXIST' || code === 'ENOTEMPTY')
182
+ return false;
183
+ throw error;
184
+ }
185
+ if (linked) {
186
+ try {
187
+ await rename(link, from);
188
+ }
189
+ catch (error) {
190
+ await rm(link, { force: true }).catch(() => undefined);
191
+ console.warn(`[acp-builds] could not leave a link at ${from} for adapters still running from it: ${String(error)}`);
192
+ }
193
+ }
194
+ return true;
195
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The adapter **build** model (spec 2026-10-11 workstation-keepers §6.1/§6.2).
3
+ *
4
+ * A build is one immutable, daemon-owned install of one adapter version — the unit "which version
5
+ * does a new session use" and "which version is this running process" are answered in. Each agent
6
+ * has at most one `current` build in `<configDir>/acp-adapters/builds.json`; only Install/Update (and
7
+ * a CLI-certified pin or patch-set change, marked `currentSetBy: 'axom'`) move it. A registry refresh
8
+ * never does: it only makes the settings card offer Update.
9
+ *
10
+ * ## Layout (all under `<configDir>/acp-adapters/`, all staged then renamed into place)
11
+ *
12
+ * ```
13
+ * builds.json
14
+ * npm/<sha16(name)>/<version>/pristine/node_modules/… # npm install --prefix
15
+ * npm/<sha16(name)>/<version>/patched/<digest>/node_modules/… # copied from pristine (reflink first) + patched
16
+ * uv/<pkg>/<version>/{tools,bin}/ + .axom-uv-build.json # UV_TOOL_DIR + UV_TOOL_BIN_DIR (see below)
17
+ * uv/.python/ uv/.python-bin/ # UV_PYTHON_INSTALL_DIR / UV_PYTHON_BIN_DIR
18
+ * bin/<agentId>/<version>/… # binary archives
19
+ * .staging/<uuid>/ .trash/<uuid>/
20
+ * ```
21
+ *
22
+ * A directory under `npm/` or `bin/` only ever appears by `rename(2)` of a complete staging tree,
23
+ * so its existence means "complete". It is still not *installed* until a record for it is committed
24
+ * to `builds.json`: an install killed between the rename and the commit leaves an unreferenced, complete
25
+ * tree that the next Install reuses instead of downloading again.
26
+ *
27
+ * **uv is the one exception to "appears only by rename"** (ruling, Task 15b): a uv tool environment
28
+ * embeds its own absolute path (the entry script's shebang names `<UV_TOOL_DIR>/<pkg>/bin/python`, the
29
+ * `UV_TOOL_BIN_DIR` entry is an absolute symlink, `uv-receipt.toml` records both), so a tree renamed
30
+ * after `uv tool install` no longer runs. uv installs straight into `uv/<pkg>/<version>/`, under a
31
+ * per-tree lock, and the tree only counts as complete once `.axom-uv-build.json` — written last, by
32
+ * temp + fsync + rename — names its entry. A tree without that marker (a killed install) is moved to
33
+ * `.trash/` by the next Install of that version and installed again; it is never recorded, never launched.
34
+ */
35
+ import { createHash } from 'node:crypto';
36
+ import path from 'node:path';
37
+ export const BUILDS_FILE = 'builds.json';
38
+ export const STAGING_DIR = '.staging';
39
+ export const TRASH_DIR = '.trash';
40
+ export class AdapterNotInstalledError extends Error {
41
+ agentId;
42
+ constructor(agentId, detail) {
43
+ super(`Agent '${agentId}' has no installed adapter on this machine${detail ? ` (${detail})` : ''}. ` +
44
+ 'Install it from the workstation\'s external agents settings.');
45
+ this.name = 'AdapterNotInstalledError';
46
+ this.agentId = agentId;
47
+ }
48
+ }
49
+ /**
50
+ * An open had to install the agent's adapter and the install is still running when the open's budget
51
+ * (`OPEN_INSTALL_BUDGET_MS`) ran out. The install goes on under the agent lock; retrying the open waits
52
+ * for it. The message is what the reader sees as the open's error.
53
+ */
54
+ export class AdapterStillInstallingError extends Error {
55
+ agentId;
56
+ version;
57
+ constructor(agentId, agentName, version) {
58
+ super(`${agentName} adapter ${version} is still installing; try again shortly.`);
59
+ this.name = 'AdapterStillInstallingError';
60
+ this.agentId = agentId;
61
+ this.version = version;
62
+ }
63
+ }
64
+ const sha256 = (value) => createHash('sha256').update(value).digest('hex');
65
+ /** A version string safe to use as one path segment. Registry/npm data must never escape the layout. */
66
+ export function assertSafeVersion(version) {
67
+ if (!/^[0-9A-Za-z][0-9A-Za-z.+_-]{0,127}$/.test(version) || version.includes('..')) {
68
+ throw new Error(`Refusing unsafe adapter version '${version}'`);
69
+ }
70
+ return version;
71
+ }
72
+ /** An agent id safe to use as one path segment. */
73
+ export function assertSafeAgentId(agentId) {
74
+ if (!/^[0-9A-Za-z][0-9A-Za-z._-]{0,127}$/.test(agentId) || agentId.includes('..')) {
75
+ throw new Error(`Refusing unsafe agent id '${agentId}'`);
76
+ }
77
+ return agentId;
78
+ }
79
+ export function assertSafePackageName(name) {
80
+ if (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i.test(name))
81
+ throw new Error(`Refusing unsafe npm package name '${name}'`);
82
+ return name;
83
+ }
84
+ export function buildIdFor(version, patchDigest) {
85
+ return patchDigest ? `${version}+axom.${patchDigest.slice(0, 8)}` : version;
86
+ }
87
+ export function labelFor(build) {
88
+ return { agentId: build.agentId, buildId: build.buildId, version: build.version, kind: build.kind };
89
+ }
90
+ const posix = (...parts) => parts.join('/');
91
+ /** `npm/<sha16(name)>/<version>` — the version directory both npm kinds live under. */
92
+ export function npmVersionDir(name, version) {
93
+ return posix('npm', sha256(assertSafePackageName(name)).slice(0, 16), assertSafeVersion(version));
94
+ }
95
+ export function npmPristineDir(name, version) {
96
+ return posix(npmVersionDir(name, version), 'pristine');
97
+ }
98
+ export function npmPatchedDir(name, version, digest) {
99
+ if (!/^[0-9a-f]{64}$/.test(digest))
100
+ throw new Error('Invalid patch digest');
101
+ return posix(npmVersionDir(name, version), 'patched', digest);
102
+ }
103
+ export function binaryBuildDir(agentId, version) {
104
+ return posix('bin', assertSafeAgentId(agentId), assertSafeVersion(version));
105
+ }
106
+ /** A Python package name normalised the PEP 503 way (`Fast_Agent.ACP` → `fast-agent-acp`), safe as one path segment. */
107
+ export function normalizeUvPackageName(name) {
108
+ const normalized = name.toLowerCase().replace(/[-_.]+/g, '-');
109
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/.test(normalized))
110
+ throw new Error(`Refusing unsafe Python package name '${name}'`);
111
+ return normalized;
112
+ }
113
+ /** `uv/<pkg>/<version>` — one uv build; holds `tools/` (`UV_TOOL_DIR`) and `bin/` (`UV_TOOL_BIN_DIR`). */
114
+ export function uvBuildDir(name, version) {
115
+ return posix('uv', normalizeUvPackageName(name), assertSafeVersion(version));
116
+ }
117
+ /** Interpreters uv downloads for builds (`UV_PYTHON_INSTALL_DIR`): daemon-owned, never the user's `~/.local/share/uv/python`. */
118
+ export const UV_PYTHON_DIR = 'uv/.python';
119
+ /** Where uv may link interpreter executables (`UV_PYTHON_BIN_DIR`) — never the user's `~/.local/bin`. */
120
+ export const UV_PYTHON_BIN_DIR = 'uv/.python-bin';
121
+ /** `relative` resolved under `root`, refusing anything that escapes it. */
122
+ export function inside(root, relative) {
123
+ const base = path.resolve(root);
124
+ const target = path.resolve(base, ...relative.split('/'));
125
+ if (target !== base && !target.startsWith(base + path.sep))
126
+ throw new Error('Adapter build path escapes its directory');
127
+ return target;
128
+ }