@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
@@ -1,11 +1,16 @@
1
- /** Automatic adapter patches, owned by Axom, never written into npm's shared cache. */
2
- import { createHash, randomUUID } from 'node:crypto';
3
- import { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
1
+ /**
2
+ * Automatic adapter patches, owned by Axom, applied only at **install** time to a copy of the
3
+ * daemon-owned pristine tree (spec §6.2/§3.3). The pristine tree is never patched in place, and
4
+ * nothing here ever runs at launch except {@link verifyPatchedFiles}, the tamper check.
5
+ */
6
+ import { createHash } from 'node:crypto';
7
+ import { constants } from 'node:fs';
8
+ import { cp, lstat, mkdir, open, realpath, writeFile } from 'node:fs/promises';
4
9
  import path from 'node:path';
5
- import { installNpxPackage, npmCacheDir, npmPackageName, npmPackageVersion } from './installProbe.js';
6
- import { packagePatchMatch, packagePatches } from './patchAssets.js';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { assertSafePackageName, inside } from './builds/model.js';
12
+ import { packagePatchMatch, packagePatches, patchAssetRoot, validatePackagePatch, validatePatchPath } from './patchAssets.js';
7
13
  const sha = (content) => createHash('sha256').update(content).digest('hex');
8
- const pending = new Map();
9
14
  const warned = new Set();
10
15
  function warnOnce(key, message) {
11
16
  if (warned.has(key))
@@ -13,31 +18,150 @@ function warnOnce(key, message) {
13
18
  warned.add(key);
14
19
  console.warn(message);
15
20
  }
16
- function inside(root, relative) {
21
+ function within(root, relative) {
22
+ validatePatchPath(relative);
17
23
  const target = path.resolve(root, relative);
18
24
  if (!target.startsWith(path.resolve(root) + path.sep))
19
25
  throw new Error('Axom patch path escapes its package');
20
26
  return target;
21
27
  }
28
+ /**
29
+ * Check every component, including the supplied root and its ancestors, without following links.
30
+ * The one links crossed are root-owned system symlinks (`/var -> /private/var` on macOS): stock
31
+ * macOS hands out per-user tmp dirs behind one, and refusing it would reject every tmp-scoped
32
+ * tree. A root-owned link to a root-owned, non-group-writable directory is not plantable by the
33
+ * running user; every user-owned symlink is still refused.
34
+ */
35
+ async function noSymlinks(filename, missing = false) {
36
+ const absolute = path.resolve(filename);
37
+ let current = path.parse(absolute).root;
38
+ const systemSymlink = async (uid) => {
39
+ if (uid !== 0 || process.getuid?.() === 0)
40
+ return false;
41
+ try {
42
+ const target = await realpath(current);
43
+ const stat = await lstat(target);
44
+ return stat.isDirectory() && stat.uid === 0 && (stat.mode & 0o022) === 0;
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ };
50
+ for (const part of absolute.slice(current.length).split(path.sep).filter(Boolean)) {
51
+ current = path.join(current, part);
52
+ try {
53
+ const info = await lstat(current);
54
+ if (info.isSymbolicLink() && !(await systemSymlink(info.uid)))
55
+ throw new Error(`Axom patch symlink is forbidden: ${current}`);
56
+ }
57
+ catch (error) {
58
+ if (missing && error.code === 'ENOENT')
59
+ return;
60
+ throw error;
61
+ }
62
+ }
63
+ }
64
+ async function readOwnedFile(filename) {
65
+ await noSymlinks(filename);
66
+ const handle = await open(filename, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
67
+ try {
68
+ if (!(await handle.stat()).isFile())
69
+ throw new Error(`Axom patch requires a regular file: ${filename}`);
70
+ return await handle.readFile();
71
+ }
72
+ finally {
73
+ await handle.close();
74
+ }
75
+ }
76
+ async function makeOwnedDirectories(directory) {
77
+ await noSymlinks(directory, true);
78
+ const parent = path.dirname(directory);
79
+ if (parent !== directory) {
80
+ try {
81
+ await lstat(directory);
82
+ }
83
+ catch (error) {
84
+ if (error.code !== 'ENOENT')
85
+ throw error;
86
+ await makeOwnedDirectories(parent);
87
+ try {
88
+ await mkdir(directory);
89
+ }
90
+ catch (error) {
91
+ if (error.code !== 'EEXIST')
92
+ throw error;
93
+ }
94
+ }
95
+ }
96
+ await noSymlinks(directory);
97
+ if (!(await lstat(directory)).isDirectory())
98
+ throw new Error(`Axom patch parent is not a directory: ${directory}`);
99
+ }
22
100
  /** A matching version and exact original bytes; already-patched or changed originals are rejected. */
23
- export async function applyPackagePatches(packageDir, recipes) {
101
+ export async function applyPackagePatches(nodeModulesDir, recipes, assetRoot = patchAssetRoot) {
24
102
  const output = {};
103
+ // Validate the complete declaration and asset closure before any writes.
104
+ expectedPatchedFiles(recipes);
105
+ const assets = new Map();
25
106
  for (const recipe of recipes) {
26
- const metadata = JSON.parse(await readFile(path.join(packageDir, 'package.json'), 'utf8'));
107
+ const packageDir = inside(nodeModulesDir, recipe.package);
108
+ const metadata = JSON.parse((await readOwnedFile(path.join(packageDir, 'package.json'))).toString('utf8'));
27
109
  if (metadata.name !== recipe.package || !packagePatchMatch(recipe, metadata.version)) {
28
110
  throw new Error(`Axom patch '${recipe.id}' does not support ${metadata.name}@${metadata.version}`);
29
111
  }
112
+ for (const edit of recipe.edits)
113
+ await noSymlinks(within(packageDir, edit.file));
114
+ for (const asset of recipe.assets ?? []) {
115
+ const filename = within(packageDir, asset.target);
116
+ await noSymlinks(filename, true);
117
+ try {
118
+ await lstat(filename);
119
+ throw new Error(`Axom patch asset collision: target already exists: ${asset.target}`);
120
+ }
121
+ catch (error) {
122
+ if (error.code !== 'ENOENT')
123
+ throw error;
124
+ }
125
+ const bytes = await readOwnedFile(within(assetRoot instanceof URL ? fileURLToPath(assetRoot) : assetRoot, asset.source));
126
+ if (sha(bytes) !== asset.sha256)
127
+ throw new Error(`Axom patch asset hash mismatch: ${asset.source}`);
128
+ assets.set(`${recipe.package}/${asset.target}`, bytes);
129
+ }
130
+ }
131
+ for (const recipe of recipes) {
132
+ const packageDir = inside(nodeModulesDir, recipe.package);
30
133
  for (const edit of recipe.edits) {
31
- const filename = inside(packageDir, edit.file);
32
- const original = await readFile(filename, 'utf8');
134
+ const filename = within(packageDir, edit.file);
135
+ const original = (await readOwnedFile(filename)).toString('utf8');
33
136
  if (sha(original) !== edit.before_sha256 || !edit.before || original.split(edit.before).length !== 2) {
34
137
  throw new Error(`Axom patch '${recipe.id}': original bytes changed in ${edit.file}`);
35
138
  }
36
139
  const patched = original.replace(edit.before, edit.after);
37
140
  if (sha(patched) !== edit.after_sha256)
38
141
  throw new Error(`Axom patch '${recipe.id}': invalid expected output`);
39
- await writeFile(filename, patched, 'utf8');
40
- output[edit.file] = sha(patched);
142
+ await noSymlinks(filename);
143
+ const handle = await open(filename, constants.O_WRONLY | constants.O_NOFOLLOW);
144
+ try {
145
+ await handle.truncate(0);
146
+ await handle.writeFile(patched, 'utf8');
147
+ }
148
+ finally {
149
+ await handle.close();
150
+ }
151
+ output[`${recipe.package}/${edit.file}`] = sha(patched);
152
+ }
153
+ for (const asset of recipe.assets ?? []) {
154
+ const filename = within(packageDir, asset.target);
155
+ await makeOwnedDirectories(path.dirname(filename));
156
+ await noSymlinks(filename, true);
157
+ const handle = await open(filename, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o644);
158
+ try {
159
+ await handle.writeFile(assets.get(`${recipe.package}/${asset.target}`));
160
+ }
161
+ finally {
162
+ await handle.close();
163
+ }
164
+ output[`${recipe.package}/${asset.target}`] = asset.sha256;
41
165
  }
42
166
  }
43
167
  return output;
@@ -49,7 +173,7 @@ async function originalsFit(packageDir, recipe) {
49
173
  let content = files.get(edit.file);
50
174
  if (content === undefined) {
51
175
  try {
52
- content = await readFile(inside(packageDir, edit.file), 'utf8');
176
+ content = (await readOwnedFile(within(packageDir, edit.file))).toString('utf8');
53
177
  }
54
178
  catch {
55
179
  return false;
@@ -61,168 +185,124 @@ async function originalsFit(packageDir, recipe) {
61
185
  }
62
186
  return true;
63
187
  }
64
- async function cachedModules(name, version, probe) {
65
- const root = path.join(npmCacheDir(probe), '_npx');
66
- let entries;
67
- try {
68
- entries = await readdir(root, { withFileTypes: true });
69
- }
70
- catch {
71
- return undefined;
72
- }
73
- for (const entry of entries) {
74
- if (!entry.isDirectory())
75
- continue;
76
- const modules = path.join(root, entry.name, 'node_modules');
77
- try {
78
- const metadata = JSON.parse(await readFile(inside(modules, name + '/package.json'), 'utf8'));
79
- if (metadata.name === name && metadata.version === version)
80
- return modules;
81
- }
82
- catch { /* Interrupted/other version of npm's cache. */ }
83
- }
84
- return undefined;
85
- }
86
- /** Called by Install/Update and every real launch, including adapters installed before this CLI. */
87
- export async function preparePatchedLaunch(agent, launch, adapterRoot, probe, options = {}) {
88
- const distribution = agent.distribution.npx;
89
- if (!distribution)
90
- return launch;
91
- const name = npmPackageName(distribution.package);
92
- const version = npmPackageVersion(distribution.package);
93
- const candidates = (options.recipes ?? packagePatches).filter((patch) => patch.package === name);
188
+ /**
189
+ * The recipes this CLI applies to `name@version`, read against the **pristine** package at
190
+ * `pristinePackageDir`: exact-version recipes always (their bytes are a promise — a mismatch fails the
191
+ * patch step loudly), range recipes only when the pristine bytes fit (otherwise upstream runs, with a
192
+ * warning). An empty answer means the build is the pristine tree itself.
193
+ */
194
+ export async function selectPackagePatches(name, version, pristinePackageDir, recipes = packagePatches, dependencies = {}) {
195
+ const candidates = recipes.filter((patch) => patch.package === name || Object.hasOwn(dependencies, patch.package));
94
196
  const matched = candidates.flatMap((recipe) => {
95
- const match = version ? packagePatchMatch(recipe, version) : undefined;
197
+ const match = packagePatchMatch(recipe, recipe.package === name ? version : dependencies[recipe.package]);
96
198
  return match ? [{ recipe, match }] : [];
97
199
  });
98
- if (matched.length && (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i.test(name) || !version || !/^[0-9a-z.+-]+$/i.test(version))) {
200
+ if (matched.length && (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i.test(name) || !/^[0-9a-z.+-]+$/i.test(version))) {
99
201
  throw new Error('Invalid package identity for Axom adapter patch');
100
202
  }
101
- // A range only says a recipe may fit; the pristine package's bytes decide.
102
- const fitting = new Set();
103
- if (matched.some(({ match }) => match === 'range')) {
104
- const modules = await pristineModules();
105
- for (const { recipe, match } of matched) {
106
- if (match !== 'range')
107
- continue;
108
- if (await originalsFit(inside(modules, name), recipe))
109
- fitting.add(recipe);
110
- else
111
- warnOnce(`${recipe.id}@${distribution.package}`, `[axom-patch] ${recipe.id} does not fit ${distribution.package}; using the upstream package. Review the patch inventory.`);
112
- }
113
- }
114
- const recipes = matched.filter(({ recipe, match }) => match === 'exact' || fitting.has(recipe)).map(({ recipe }) => recipe);
115
- if (!recipes.length) {
116
- if (candidates.length && !matched.length) {
117
- warnOnce(distribution.package, `[axom-patch] No verified patch for ${distribution.package}; using the upstream package. Review the patch inventory.`);
203
+ const selected = [];
204
+ const nodeModulesDir = path.resolve(pristinePackageDir, ...name.split('/').map(() => '..'));
205
+ for (const { recipe, match } of matched) {
206
+ assertSafePackageName(recipe.package);
207
+ const packageDir = recipe.package === name ? pristinePackageDir : inside(nodeModulesDir, recipe.package);
208
+ if (match === 'exact') {
209
+ selected.push(recipe);
210
+ continue;
118
211
  }
119
- return launch;
212
+ if (await originalsFit(packageDir, recipe))
213
+ selected.push(recipe);
214
+ else
215
+ warnOnce(`${recipe.id}@${name}@${version}`, `[axom-patch] ${recipe.id} does not fit ${name}@${version}; using the upstream package. Review the patch inventory.`);
120
216
  }
121
- const digest = sha(JSON.stringify(recipes));
122
- const target = path.join(adapterRoot, 'patched-npm', sha(name).slice(0, 16), version, digest);
123
- const existing = pending.get(target);
124
- if (existing) {
125
- await existing;
126
- return makeLaunch();
217
+ if (!selected.length && candidates.length && !matched.length) {
218
+ warnOnce(`${name}@${version}`, `[axom-patch] No verified patch for ${name}@${version}; using the upstream package. Review the patch inventory.`);
127
219
  }
128
- const preparation = prepare();
129
- pending.set(target, preparation);
130
- try {
131
- await preparation;
132
- return await makeLaunch();
133
- }
134
- finally {
135
- pending.delete(target);
220
+ return selected;
221
+ }
222
+ /** The identity of a recipe set: any recipe change is a new digest, hence a new build. */
223
+ export function patchDigest(recipes) {
224
+ return sha(JSON.stringify(recipes));
225
+ }
226
+ /**
227
+ * Copy the pristine `node_modules` (reflink where the filesystem supports it — `COPYFILE_FICLONE`
228
+ * falls back to an ordinary copy by itself; relative symlinks such as `.bin/*` are kept relative so
229
+ * the copy never points back into pristine) into `stagedRoot`, patch the copy, and write a receipt.
230
+ * Returns the sha256 of every patched file, qualified by its package name under node_modules.
231
+ */
232
+ export async function createPatchedTree(pristineRoot, stagedRoot, name, version, recipes, assetRoot = patchAssetRoot) {
233
+ const original = path.resolve(pristineRoot, 'node_modules');
234
+ const copy = path.resolve(stagedRoot, 'node_modules');
235
+ if (original === copy || original.startsWith(copy + path.sep) || copy.startsWith(original + path.sep)) {
236
+ throw new Error('Axom patch staged tree must not overlap pristine');
136
237
  }
137
- async function prepare() {
138
- const packageDir = inside(path.join(target, 'node_modules'), name);
139
- const receiptFile = path.join(target, 'axom-patches.json');
140
- let receipt;
238
+ await noSymlinks(original);
239
+ const receipt = path.join(stagedRoot, 'axom-patches.json');
240
+ for (const destination of [copy, receipt]) {
241
+ await noSymlinks(destination, true);
141
242
  try {
142
- receipt = JSON.parse(await readFile(receiptFile, 'utf8'));
243
+ await lstat(destination);
244
+ throw new Error(`Axom patch staged target already exists: ${destination}`);
143
245
  }
144
246
  catch (error) {
145
247
  if (error.code !== 'ENOENT')
146
248
  throw error;
147
249
  }
148
- if (receipt) {
149
- if (receipt.digest !== digest)
150
- throw new Error('Axom adapter patch receipt does not match this CLI');
151
- // Derive expected files from the shipped recipe, never from a writable receipt alone.
152
- const expected = Object.fromEntries(recipes.flatMap((recipe) => recipe.edits.map((edit) => [edit.file, edit.after_sha256])));
153
- if (JSON.stringify(receipt.files) !== JSON.stringify(expected))
154
- throw new Error('Incomplete Axom adapter patch receipt');
155
- for (const [file, hash] of Object.entries(expected)) {
156
- if (sha(await readFile(inside(packageDir, file))) !== hash)
157
- throw new Error(`Axom patched adapter was modified: ${file}`);
158
- }
250
+ }
251
+ await cp(path.join(pristineRoot, 'node_modules'), path.join(stagedRoot, 'node_modules'), {
252
+ recursive: true, verbatimSymlinks: true, mode: constants.COPYFILE_FICLONE,
253
+ });
254
+ const files = await applyPackagePatches(path.join(stagedRoot, 'node_modules'), recipes, assetRoot);
255
+ const expected = expectedPatchedFiles(recipes);
256
+ if (JSON.stringify(sortKeys(files)) !== JSON.stringify(sortKeys(expected)))
257
+ throw new Error('Incomplete Axom adapter patch');
258
+ await noSymlinks(receipt, true);
259
+ await writeFile(receipt, JSON.stringify({
260
+ patchedFilesBase: 'node_modules', digest: patchDigest(recipes), package: name, version, patches: recipes.map((recipe) => recipe.id), files,
261
+ }) + '\n', { flag: 'wx' });
262
+ console.info(`[axom-patch] Applied ${recipes.map((recipe) => recipe.id).join(', ')} to ${name}@${version}`);
263
+ return files;
264
+ }
265
+ /** The files a recipe set rewrites and their expected sha256, derived from the shipped recipes. */
266
+ export function expectedPatchedFiles(recipes) {
267
+ const files = {};
268
+ const assetTargets = new Set();
269
+ const editTargets = new Set();
270
+ for (const recipe of recipes) {
271
+ assertSafePackageName(recipe.package);
272
+ validatePackagePatch(recipe);
273
+ for (const edit of recipe.edits) {
274
+ const key = `${recipe.package}/${edit.file}`;
275
+ editTargets.add(key);
276
+ files[key] = edit.after_sha256;
159
277
  }
160
- else {
161
- try {
162
- await stat(target);
163
- throw new Error('Incomplete Axom patched adapter directory; refusing to use it');
164
- }
165
- catch (error) {
166
- if (error.code !== 'ENOENT')
167
- throw error;
168
- }
169
- const modules = await pristineModules();
170
- await mkdir(path.dirname(target), { recursive: true });
171
- const temporary = target + '.staging-' + randomUUID();
172
- try {
173
- // Dependencies and .bin are part of the adapter environment; clone them as well.
174
- await cp(modules, path.join(temporary, 'node_modules'), { recursive: true, dereference: true });
175
- const files = await applyPackagePatches(inside(path.join(temporary, 'node_modules'), name), recipes);
176
- await writeFile(path.join(temporary, 'axom-patches.json'), JSON.stringify({
177
- digest, package: name, version, patches: recipes.map((recipe) => recipe.id), files,
178
- }) + '\n');
179
- try {
180
- await rename(temporary, target);
181
- }
182
- catch (error) {
183
- // A second daemon may have published the same immutable copy while we built ours.
184
- if (!['EEXIST', 'ENOTEMPTY'].includes(error.code ?? ''))
185
- throw error;
186
- return prepare();
187
- }
188
- console.info(`[axom-patch] Applied ${recipes.map((recipe) => recipe.id).join(', ')} to ${name}@${version}`);
189
- }
190
- finally {
191
- await rm(temporary, { recursive: true, force: true });
278
+ }
279
+ for (const recipe of recipes)
280
+ for (const asset of recipe.assets ?? []) {
281
+ const key = `${recipe.package}/${asset.target}`;
282
+ if ([...assetTargets, ...editTargets].some(other => other === key || other.startsWith(key + '/') || key.startsWith(other + '/'))) {
283
+ throw new Error(`Axom patch asset collision: ${key}`);
192
284
  }
285
+ assetTargets.add(key);
286
+ files[key] = asset.sha256;
193
287
  }
194
- }
195
- async function pristineModules() {
196
- let modules = await cachedModules(name, version, probe);
197
- if (!modules) {
198
- const installed = await installNpxPackage(distribution.package, {
199
- env: probe, timeoutMs: options.timeoutMs, npxCommand: options.npxCommand,
200
- });
201
- if (!installed.ok)
202
- throw new Error(installed.error);
203
- modules = await cachedModules(name, version, probe);
204
- }
205
- if (!modules)
206
- throw new Error(`Installed package ${distribution.package} is absent from npm's cache`);
207
- return modules;
208
- }
209
- async function makeLaunch() {
210
- const packageDir = inside(path.join(target, 'node_modules'), name);
211
- const metadata = JSON.parse(await readFile(path.join(packageDir, 'package.json'), 'utf8'));
212
- if (metadata.name !== name || metadata.version !== version)
213
- throw new Error('Patched adapter package identity changed');
214
- const selectedBins = [...new Set(recipes.map(recipe => recipe.bin).filter((bin) => bin !== undefined))];
215
- if (selectedBins.length > 1)
216
- throw new Error('Axom patches disagree about the adapter executable');
217
- const binaries = selectedBins.length ? [metadata.bin?.[selectedBins[0]]] :
218
- typeof metadata.bin === 'string' ? [metadata.bin] : Object.values(metadata.bin ?? {});
219
- if (binaries.length !== 1 || typeof binaries[0] !== 'string')
220
- throw new Error('Patched adapter has no unambiguous executable');
221
- const entry = inside(packageDir, binaries[0]);
222
- if (!(await stat(entry)).isFile())
223
- throw new Error('Patched adapter executable is missing');
224
- const env = { ...launch.env };
225
- env.PATH = path.join(target, 'node_modules', '.bin') + path.delimiter + (env.PATH ?? probe.path ?? process.env.PATH ?? '');
226
- return { command: process.execPath, args: [entry, ...(distribution.args ?? [])], env };
288
+ return files;
289
+ }
290
+ function sortKeys(record) {
291
+ return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
292
+ }
293
+ /** Tamper check against an explicit base: node_modules for qualified keys, adapter package for legacy keys. */
294
+ export async function verifyPatchedFiles(packageDir, files) {
295
+ for (const [file, hash] of Object.entries(files)) {
296
+ let content;
297
+ try {
298
+ content = await readOwnedFile(within(packageDir, file));
299
+ }
300
+ catch (error) {
301
+ if (error.code === 'ENOENT')
302
+ throw new Error(`Axom patched adapter is missing ${file}`);
303
+ throw new Error(`Axom patched adapter was modified: ${file} (${error.message})`);
304
+ }
305
+ if (sha(content) !== hash)
306
+ throw new Error(`Axom patched adapter was modified: ${file}`);
227
307
  }
228
308
  }