@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,27 @@
1
+ /**
2
+ * The environment values the server's `agent_open` (and every other agent frame that launches an adapter) overlays — the workspace's
3
+ * `env` and the agent's `agentEnv`, where API keys and tokens are configured — are secrets for stderr purposes (spec 2026-10-11 §13,
4
+ * Task 23's sentinel gate): an adapter that prints its environment would otherwise leave the key in keeper-acp's `stderr.log`, the
5
+ * `err` records and the exit tail, or — for the daemon's short-lived connections (`connectAgent`: verify, native session listing,
6
+ * logins) — in daemon.log. Not redacted, because they are no credential and blanking them would garble every line they appear in
7
+ * (stack traces print paths): the overlay's `PATH` (the daemon computes it from `pathPrepend`), values shorter than
8
+ * {@link ENV_SECRET_MIN_LENGTH} ("1", "debug"), absolute paths, booleans and plain numbers.
9
+ */
10
+ export const ENV_SECRET_MIN_LENGTH = 8;
11
+ /** A value that is plainly not a secret (see the file comment). */
12
+ export function isPlainEnvValue(value) {
13
+ return value.length < ENV_SECRET_MIN_LENGTH
14
+ || /^(?:\/|~\/|[A-Za-z]:[\\/])/.test(value)
15
+ || /^(?:true|false|yes|no|on|off)$/i.test(value)
16
+ || /^[-+]?\d+(?:\.\d+)?$/.test(value);
17
+ }
18
+ export function overlayEnvSecrets(overlays) {
19
+ const values = new Set();
20
+ for (const overlay of [overlays.env, overlays.agentEnv]) {
21
+ for (const [key, value] of Object.entries(overlay ?? {})) {
22
+ if (key !== 'PATH' && typeof value === 'string' && !isPlainEnvValue(value))
23
+ values.add(value);
24
+ }
25
+ }
26
+ return [...values];
27
+ }
@@ -0,0 +1,285 @@
1
+ /**
2
+ * The `initialize` handshake, as data: the params this client sends and what it reads out of the
3
+ * agent's reply.
4
+ *
5
+ * ## Why this is its own module
6
+ *
7
+ * Two readers must agree on every byte of this: `connectAgent` (the live SDK path) and the
8
+ * deterministic ACP processor (`processor/`, spec 2026-10-11 §5.4), which parses the same reply off
9
+ * the keeper's raw log inside `step`. `step`'s import graph may not reach anything that spawns a
10
+ * process, reads the clock or draws a random id (`processor/guard.test.ts`), and `acpClient.ts` does
11
+ * all three — so the pure half lives here and `acpClient.ts` re-exports every name it used to own.
12
+ * One implementation rather than two kept equal by a test: the equivalence test in
13
+ * `processor/initialize.test.ts` then guards the processor's *wiring* (the right line, the right
14
+ * result), not a second copy of these rules.
15
+ *
16
+ * The Qoder runtime profile in `_meta` is deliberately not read here: it belongs to the connection's
17
+ * runtime diagnostics, which the processor takes over in Task 17b/17c.
18
+ */
19
+ import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk';
20
+ import { airCapabilityMeta, asSdkClientCapabilities, AIR_ASYNC_TASKS_CAPABILITY, AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, DRAFT_SUBAGENTS_CAPABILITY, } from './draftSubagents.js';
21
+ import { metaTerminalAuthArgs, terminalLoginSpecOf } from './terminalLoginSpec.js';
22
+ /** The `initialize` params this client sends — the same object on the live path and from the processor. */
23
+ export function initializeRequestParams() {
24
+ return {
25
+ protocolVersion: PROTOCOL_VERSION,
26
+ // Three things are declared here, and they are not interchangeable — see
27
+ // `draftSubagents.ts` for the tier each one sits in.
28
+ //
29
+ // 1. `subagents: {}` is the **canonical** draft field from ACP RFD #1992. Both official
30
+ // adapters gate native subagent sessions on it, and both answer with
31
+ // `agentCapabilities.sessionCapabilities.subagents` (measured against each).
32
+ //
33
+ // 2. The AIR list is sent as well, for two separate reasons. It is the *only* channel for
34
+ // `asyncTasks` — nothing canonical exists for background work — and it is the fallback
35
+ // for `nativeSubagentSessions`, because a released SDK sitting between us and an agent
36
+ // can strip a draft field it has no type for (upstream says so in #1017's compatibility
37
+ // note). Declaring both is not belt-and-braces: they cover different failure modes.
38
+ //
39
+ // 3. `subagent-transcript: true` stays. It is claude-agent-acp's older vendor key
40
+ // (`dist/acp-agent.js:155-158`), and it still governs the **fallback** path taken when
41
+ // native subagent sessions are not negotiated — an older adapter, or a stripped
42
+ // capability. Without it that path strips every subagent's text and thinking
43
+ // (`stripSubagentTextAndThinking`, `:3926`), leaving nothing but tool-call wreckage.
44
+ // Adapters that have never heard of any of these keys are required by the spec to
45
+ // ignore them.
46
+ clientCapabilities: asSdkClientCapabilities({
47
+ [DRAFT_SUBAGENTS_CAPABILITY]: {},
48
+ // `ClientSessionCapabilities.configOptions` (`types.gen.d.ts:4306`), part of the schema
49
+ // proper — unlike `subagents` above, this survives the pinned SDK's `zInitializeRequest`
50
+ // stripping because it's a known field, not a draft one. `{}` for `boolean` tells the
51
+ // agent it may send `type: "boolean"` entries in `SessionConfigOption` and that this
52
+ // client will send `session/set_config_option` with a boolean `value`
53
+ // (`SessionConfigOptionsCapabilities`, `types.gen.d.ts:4333-4353`); without it an agent
54
+ // with a boolean toggle downgrades it to a two-value select.
55
+ session: { configOptions: { boolean: {} } },
56
+ // `ClientCapabilities.elicitation` (`types.gen.d.ts:4479`) is a known field of the schema
57
+ // proper, not a draft one, so it survives `zInitializeRequest` stripping as-is. `{}` for
58
+ // `url` (`ElicitationUrlCapabilities`, `:4616`) means the client can put a URL in front of
59
+ // the user; this is the precondition for the whole device-code login shape to exist at all
60
+ // — Codex's own `getCodexAuthMethods` gates its `chat-gpt-device-code` auth method on
61
+ // `clientCapabilities?.elicitation?.url != null`. Without declaring it, Codex never offers
62
+ // that method and `elicitation/create` with `mode: 'url'` never arrives to answer (see
63
+ // `permission.ts`'s `handleCreateElicitation`).
64
+ //
65
+ // `form` (`ElicitationFormCapabilities`, `types.gen.d.ts:4455`) is what keeps Claude's
66
+ // AskUserQuestion alive: without it the adapter puts the tool in `disallowedTools`
67
+ // (`claude-agent-acp/dist/acp-agent.js:5959`), so the model never has it at all — the
68
+ // symptom is prose questions, not an unrendered card. This is the trigger for the whole
69
+ // elicitation-standardization plan: everything through Task 17 built the machinery to
70
+ // answer `elicitation/create` once it arrives; this line is what makes it arrive at all.
71
+ elicitation: { url: {}, form: {} },
72
+ _meta: {
73
+ // The convention Zed introduced and the ACP adapters adopted: an agent may gate its terminal
74
+ // sign-in methods on this key — claude-agent-acp advertises `claude-ai-login` / `console-login`
75
+ // (each with a `_meta["terminal-auth"]` naming the pipe-friendly invocation) only when the
76
+ // client declares it; pi-acp always returns its method but attaches the same block only then.
77
+ // We DO host those invocations — `terminalLogin.ts` runs them with pipes, sniffs the OAuth URL
78
+ // out of the output and surfaces it as a login link (spec 2026-09-24 D6/D7/D9) — so declaring
79
+ // this is a statement of fact, not an aspiration. Same-machine only: a hosted login's
80
+ // localhost OAuth callback must reach the workstation's own listener, which a browser opened
81
+ // on another machine cannot do; the standard `authenticate` methods (device-code style) are
82
+ // unaffected and never needed this key.
83
+ 'terminal-auth': true,
84
+ 'subagent-transcript': true,
85
+ ...airCapabilityMeta(AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, AIR_ASYNC_TASKS_CAPABILITY),
86
+ },
87
+ }),
88
+ };
89
+ }
90
+ /**
91
+ * Whether a JSON-RPC error code is one a frame may carry: an integer inside the Kotlin twin's `Int`
92
+ * range. See `acpClient.ts` `jsonRpcErrorCode` for why a malformed code is reported as no code at
93
+ * all. Split out so the processor, which sees the peer's `{code}` as plain JSON rather than as the
94
+ * SDK's `RequestError`, applies the identical rule.
95
+ */
96
+ export function jsonRpcErrorCodeOf(code) {
97
+ if (typeof code !== 'number' || !Number.isInteger(code))
98
+ return undefined;
99
+ return code >= -2_147_483_648 && code <= 2_147_483_647 ? code : undefined;
100
+ }
101
+ /**
102
+ * Read an `initialize` result exactly the way `connectAgent` always has.
103
+ *
104
+ * The protocol version is checked **first** and nothing else is read when it is wrong — Global
105
+ * Constraint 1 is v1-only and a reply for another version is not ours to interpret. Malformed input
106
+ * throws where it always threw (a non-array `authMethods`, a non-object result); callers that must
107
+ * not throw (the processor) catch it themselves, so the live path keeps its exact behaviour.
108
+ */
109
+ export function parseInitializeResult(initResponse) {
110
+ if (initResponse.protocolVersion !== PROTOCOL_VERSION) {
111
+ return { protocolSupported: false, protocolVersion: initResponse.protocolVersion };
112
+ }
113
+ const sessionCapabilities = initResponse.agentCapabilities?.sessionCapabilities;
114
+ const capabilities = {
115
+ // Capability markers are empty objects (`{ _meta? }`); supplying `{}` means "supported", and
116
+ // omitting the field (or explicit `null`) means "not supported". A truthiness check reads `{}`
117
+ // correctly by accident but reads `null`/`undefined` correctly too, which hides the real bug:
118
+ // this must stay an explicit `!= null`, never `Boolean(...)` or a bare truthy test.
119
+ list: sessionCapabilities?.list != null,
120
+ delete: sessionCapabilities?.delete != null,
121
+ resume: sessionCapabilities?.resume != null,
122
+ loadSession: initResponse.agentCapabilities?.loadSession === true,
123
+ logout: initResponse.agentCapabilities?.auth?.logout != null,
124
+ /**
125
+ * ACP `PromptCapabilities.image` (`types.gen.d.ts:1601`): "Agent supports
126
+ * [`ContentBlock::Image`]". Every field of that type is optional, so an agent that says
127
+ * nothing supports text only -- `=== true` rather than `!== false`.
128
+ *
129
+ * Verified against codex-acp 1.7.0, which declares `{embeddedContext: true, image: true}`.
130
+ * `audio` is deliberately not read: no agent we target declares it, and an unread capability
131
+ * is a field nobody can act on.
132
+ */
133
+ promptImage: initResponse.agentCapabilities?.promptCapabilities?.image === true,
134
+ mcpHttp: initResponse.agentCapabilities?.mcpCapabilities?.http === true,
135
+ };
136
+ // Honour the advertised capability, not what the agent happens to route: some adapters (e.g.
137
+ // codex-acp routing `session/fork`) implement more than they declare in `agentCapabilities`.
138
+ // Reading `capabilities` above off `initResponse` — never probing the agent for what it accepts
139
+ // — is what keeps that distinction intact.
140
+ const advertisedAuthMethods = initResponse.authMethods ?? [];
141
+ const authMethods = advertisedAuthMethods
142
+ .map(toAuthMethodSnapshot)
143
+ .filter((method) => method !== null);
144
+ const terminalLogins = [];
145
+ for (const method of advertisedAuthMethods) {
146
+ const spec = terminalLoginSpecOf(method);
147
+ if (spec && typeof method.id === 'string') {
148
+ // A duplicate id keeps its first position and takes the later spec — exactly `Map.set`.
149
+ const existing = terminalLogins.findIndex(([id]) => id === method.id);
150
+ if (existing >= 0)
151
+ terminalLogins[existing] = [method.id, spec];
152
+ else
153
+ terminalLogins.push([method.id, spec]);
154
+ }
155
+ }
156
+ return {
157
+ protocolSupported: true,
158
+ capabilities,
159
+ authMethods,
160
+ droppedAuthMethods: advertisedAuthMethods.length - authMethods.length,
161
+ terminalLogins,
162
+ ...(initResponse.agentCapabilities?._meta?.['axom/accountRateLimits']?.read === true ? { supportsAccountLimits: true, accountLimitsVersion: initResponse.agentCapabilities?._meta?.['axom/accountRateLimits']?.version ?? 1 } : {}),
163
+ supportsSteering: steeringAdvertised(initResponse._meta),
164
+ steeringTimings: steeringTimings(initResponse._meta),
165
+ agentInfo: initResponse.agentInfo ?? undefined,
166
+ };
167
+ }
168
+ /**
169
+ * Whether an `InitializeResponse._meta` advertises the `_session/steering` extension.
170
+ *
171
+ * ## `_session/steering` is not ACP
172
+ *
173
+ * It is absent from the schema this client is pinned to (`grep steer
174
+ * node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.d.ts` returns nothing). It is an
175
+ * extension `claude-agent-acp` implements: the method constant lives at its `acp-agent.js@0.76.0:164`
176
+ * and its own comment calls it a *"Custom (extension) request method ... Named `_session/steering`
177
+ * per the agreed ACP steering wire protocol"*. The leading underscore is ACP's extension namespace.
178
+ *
179
+ * ## Why the top-level `_meta` specifically
180
+ *
181
+ * The adapter advertises it at `InitializeResponse._meta.steering.supported`, a **sibling of
182
+ * `agentCapabilities`** (`acp-agent.js@0.76.0:944-950`), not a key inside it. `_meta` is typed
183
+ * `{[key: string]: unknown} | null` (`types.gen.d.ts:1498-1500`), so every hop down has to be
184
+ * narrowed rather than indexed through a cast.
185
+ *
186
+ * Strictly `=== true`. An agent that sends `steering: {}` (the shape ACP uses for its *own*
187
+ * capability markers) has not said `supported`, and this extension does not define that spelling —
188
+ * reading it as a yes would be this client inventing a wire contract.
189
+ */
190
+ export function steeringAdvertised(meta) {
191
+ if (typeof meta !== 'object' || meta === null)
192
+ return false;
193
+ const steering = meta.steering;
194
+ if (typeof steering !== 'object' || steering === null)
195
+ return false;
196
+ return steering.supported === true;
197
+ }
198
+ /**
199
+ * The delivery timings the `_session/steering` extension defines — when the agent hands an injected
200
+ * message to the model. `next` is the only lossless one: `now` aborts the running generation and
201
+ * discards whatever tool calls were still outstanding.
202
+ */
203
+ export const STEER_TIMINGS = ['now', 'next', 'later'];
204
+ /**
205
+ * Which delivery timings an `InitializeResponse._meta` says the agent accepts —
206
+ * `_meta.steering.timings`, the sibling of the `supported` flag {@link steeringAdvertised} reads,
207
+ * and read from the **top-level** `_meta` for exactly the same reason.
208
+ *
209
+ * ## Empty is the answer, not the absence of one
210
+ *
211
+ * An agent that declares no timings has told us we may not name one. That is not "assume the
212
+ * default" — the extension ignores an unrecognised `priority` **silently** (the measurement behind
213
+ * ruling N4: sending `next` to today's adapters lands as `now`, which throws away the user's
214
+ * in-flight work). So an empty result must reach the caller as "do not specify a timing at all",
215
+ * and today — before the upstream PR that makes adapters broadcast this — every agent in the wild
216
+ * produces exactly that. Steady state, not a bug.
217
+ *
218
+ * ## Unknown values are dropped, never rejected
219
+ *
220
+ * A timing this build has never heard of is skipped and the known ones beside it survive. This
221
+ * client is deployed on user machines and can be older than both the agent it launches and the
222
+ * server it reports to; throwing (or discarding the whole array) on a value from a future version
223
+ * of the extension would take steering away from an agent that does support it. Non-strings are
224
+ * dropped the same way: `_meta` is typed `{[key: string]: unknown}`, so nothing here is guaranteed.
225
+ *
226
+ * Duplicates collapse — the wire shape is a JSON array but the fact is a set.
227
+ */
228
+ export function steeringTimings(meta) {
229
+ if (typeof meta !== 'object' || meta === null)
230
+ return [];
231
+ const steering = meta.steering;
232
+ if (typeof steering !== 'object' || steering === null)
233
+ return [];
234
+ const declared = steering.timings;
235
+ if (!Array.isArray(declared))
236
+ return [];
237
+ const known = [];
238
+ for (const entry of declared) {
239
+ if (typeof entry !== 'string')
240
+ continue;
241
+ if (!STEER_TIMINGS.includes(entry))
242
+ continue;
243
+ if (known.includes(entry))
244
+ continue;
245
+ known.push(entry);
246
+ }
247
+ return known;
248
+ }
249
+ /**
250
+ * One `initialize` auth method, or `null` when the agent did not advertise a usable one.
251
+ *
252
+ * `initResponse` is another process's JSON that arrived over a pipe; `AuthMethod` is what this
253
+ * client *asked* for, not what it was handed. Both variants of that union make `id` and `name`
254
+ * required (`@agentclientprotocol/sdk` `types.gen.d.ts:2241-2246` for `AuthMethodTerminal`,
255
+ * `:2281-2286` for `AuthMethodAgent`), so an agent that omits them is off-spec -- but nothing
256
+ * between its stdout and here checks, and the snapshot then carries `undefined` in two positions
257
+ * its own type declares non-optional. That lie does not stay local: `JSON.stringify` erases an
258
+ * `undefined` value entirely, so the object passes every `typeof x.id` check in this process and
259
+ * still reaches a peer with the keys missing.
260
+ *
261
+ * `agoragentic-acp` advertises exactly `{type: 'terminal', description: '…'}`. Observing it once
262
+ * put a malformed `AuthMethodInfo` in the roster, which rides in every subsequent
263
+ * `management_hello`, which the server then cannot decode -- and a hello the server cannot decode
264
+ * closes the socket with nothing said, so the workstation retried into a permanent OFFLINE.
265
+ *
266
+ * Dropping rather than repairing: `id` is the value `authenticate` is called with, so inventing
267
+ * one would trade a missing sign-in method for a broken one.
268
+ */
269
+ export function toAuthMethodSnapshot(method) {
270
+ if (typeof method.id !== 'string' || typeof method.name !== 'string')
271
+ return null;
272
+ // Terminal by shape (spec 2026-09-24 D6): a top-level `type: "terminal"`, or a `_meta
273
+ // ["terminal-auth"]` object from an agent that advertises its terminal login only there.
274
+ const terminal = terminalLoginSpecOf(method) !== undefined;
275
+ const args = 'args' in method && Array.isArray(method.args) ? method.args : terminal ? metaTerminalAuthArgs(method) : undefined;
276
+ return {
277
+ id: method.id,
278
+ name: method.name,
279
+ ...(terminal ? { type: 'terminal' } : {}),
280
+ ...(method.description != null ? { description: method.description } : {}),
281
+ // `args` only exists on a terminal method — top-level, or under `_meta["terminal-auth"]` when
282
+ // that is the only place the agent put it. Never the `_meta` command: see `terminalLogins`.
283
+ ...(args ? { args } : {}),
284
+ };
285
+ }
@@ -16,13 +16,13 @@
16
16
  * built.
17
17
  *
18
18
  * Three shapes, matching the three things a failed install can mean (see `acpClient.ts`'s
19
- * `isMissingPackageVersion`/`stderrShowsMissingPackageVersion` — the one predicate every ETARGET
19
+ * `stderrShowsMissingPackageVersion` — the one predicate every ETARGET
20
20
  * check in this codebase reuses rather than re-deriving):
21
21
  *
22
22
  * 1. An ordinary failure with no ETARGET in sight: a network hiccup, a full disk, a missing archive
23
23
  * tool, a timeout. {@link friendlyOrdinaryInstallError}.
24
24
  * 2. The registry pins a version that is not published upstream, and the unpinned retry
25
- * (`unpinnedFallbackFor`, `registry/launch.ts`) that tries to work around it fails too.
25
+ * (`AcpAgents.installPristineWithFallback`) that tries to work around it fails too.
26
26
  * {@link friendlyPinnedVersionUnavailableAndFallbackFailed}.
27
27
  * 3. The same missing pin, but the unpinned retry succeeds — not a failure at all, so it produces no
28
28
  * message here. It produces a {@link PinnedVersionUnavailable} fact instead, which
@@ -87,3 +87,13 @@ export function friendlyPinnedVersionUnavailableAndFallbackFailed(agentId, pinne
87
87
  return (`The registry pins '${agentId}' to version ${pinned}, which is not published. Installing the ` +
88
88
  `latest available version instead failed too: ${shortDetail(fallbackDetail)}.${reassurance(existingVersion)}`);
89
89
  }
90
+ /**
91
+ * A uv-distributed adapter, and `uv` itself is not on this workstation (the spawn failed `ENOENT`). Not a
92
+ * transient failure, so not "try again in a moment": the reader has to install uv first. Raised only by
93
+ * an Install that tried to run `uv` — scanning never checks for it.
94
+ */
95
+ export function friendlyUvMissingError(agentId, existingVersion) {
96
+ return (`Could not install '${agentId}': uv is not installed on this workstation, and this agent is distributed ` +
97
+ `as a Python tool installed with uv. Install uv (https://docs.astral.sh/uv/getting-started/installation/) ` +
98
+ `so the workstation's service can find it on its PATH, then install the agent again.${reassurance(existingVersion)}`);
99
+ }
@@ -312,7 +312,7 @@ export function installNpxPackage(spec, options = {}) {
312
312
  // 都没有时 `npxCacheVersion` 同样是 `undefined` —— 两个 `undefined` 相等,若不单独守住这一条,
313
313
  // 一个从未被任何 npx 调用装过的包名,只要没剥版本前先被查过缓存,就会在这里被判定成"已经装好",
314
314
  // 一次 npx 都不用起。registry 自己从不发不钉版本的 spec(见 `registry/launch.ts` 的说明),这条分支
315
- // 过去因此从没被真正走到过;`unpinnedFallbackFor` 的回退——`AcpAgents.installNpxWithFallback`——
315
+ // 过去因此从没被真正走到过;旧的 npx 安装期回退——
316
316
  // 是第一个会把不带版本的 spec 递进来的调用方,伪装成功比真的失败更糟:界面会说"已安装",磁盘上
317
317
  // 其实什么都没有。
318
318
  const pinnedVersion = npmPackageVersion(spec);
@@ -0,0 +1,144 @@
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, readFile, writeFile } from 'node:fs/promises';
9
+ import path from 'node:path';
10
+ import { inside } from './builds/model.js';
11
+ import { packagePatchMatch, packagePatches } from './patchAssets.js';
12
+ const sha = (content) => createHash('sha256').update(content).digest('hex');
13
+ const warned = new Set();
14
+ function warnOnce(key, message) {
15
+ if (warned.has(key))
16
+ return;
17
+ warned.add(key);
18
+ console.warn(message);
19
+ }
20
+ function within(root, relative) {
21
+ const target = path.resolve(root, relative);
22
+ if (!target.startsWith(path.resolve(root) + path.sep))
23
+ throw new Error('Axom patch path escapes its package');
24
+ return target;
25
+ }
26
+ /** A matching version and exact original bytes; already-patched or changed originals are rejected. */
27
+ export async function applyPackagePatches(packageDir, recipes) {
28
+ const output = {};
29
+ for (const recipe of recipes) {
30
+ const metadata = JSON.parse(await readFile(path.join(packageDir, 'package.json'), 'utf8'));
31
+ if (metadata.name !== recipe.package || !packagePatchMatch(recipe, metadata.version)) {
32
+ throw new Error(`Axom patch '${recipe.id}' does not support ${metadata.name}@${metadata.version}`);
33
+ }
34
+ for (const edit of recipe.edits) {
35
+ const filename = within(packageDir, edit.file);
36
+ const original = await readFile(filename, 'utf8');
37
+ if (sha(original) !== edit.before_sha256 || !edit.before || original.split(edit.before).length !== 2) {
38
+ throw new Error(`Axom patch '${recipe.id}': original bytes changed in ${edit.file}`);
39
+ }
40
+ const patched = original.replace(edit.before, edit.after);
41
+ if (sha(patched) !== edit.after_sha256)
42
+ throw new Error(`Axom patch '${recipe.id}': invalid expected output`);
43
+ await writeFile(filename, patched, 'utf8');
44
+ output[edit.file] = sha(patched);
45
+ }
46
+ }
47
+ return output;
48
+ }
49
+ /** Whether every edit of `recipe` finds the bytes it was written against, edits on one file applied in order. */
50
+ async function originalsFit(packageDir, recipe) {
51
+ const files = new Map();
52
+ for (const edit of recipe.edits) {
53
+ let content = files.get(edit.file);
54
+ if (content === undefined) {
55
+ try {
56
+ content = await readFile(within(packageDir, edit.file), 'utf8');
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ }
62
+ if (sha(content) !== edit.before_sha256 || !edit.before || content.split(edit.before).length !== 2)
63
+ return false;
64
+ files.set(edit.file, content.replace(edit.before, edit.after));
65
+ }
66
+ return true;
67
+ }
68
+ /**
69
+ * The recipes this CLI applies to `name@version`, read against the **pristine** package at
70
+ * `pristinePackageDir`: exact-version recipes always (their bytes are a promise — a mismatch fails the
71
+ * patch step loudly), range recipes only when the pristine bytes fit (otherwise upstream runs, with a
72
+ * warning). An empty answer means the build is the pristine tree itself.
73
+ */
74
+ export async function selectPackagePatches(name, version, pristinePackageDir, recipes = packagePatches) {
75
+ const candidates = recipes.filter((patch) => patch.package === name);
76
+ const matched = candidates.flatMap((recipe) => {
77
+ const match = packagePatchMatch(recipe, version);
78
+ return match ? [{ recipe, match }] : [];
79
+ });
80
+ if (matched.length && (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i.test(name) || !/^[0-9a-z.+-]+$/i.test(version))) {
81
+ throw new Error('Invalid package identity for Axom adapter patch');
82
+ }
83
+ const selected = [];
84
+ for (const { recipe, match } of matched) {
85
+ if (match === 'exact') {
86
+ selected.push(recipe);
87
+ continue;
88
+ }
89
+ if (await originalsFit(pristinePackageDir, recipe))
90
+ selected.push(recipe);
91
+ else
92
+ warnOnce(`${recipe.id}@${name}@${version}`, `[axom-patch] ${recipe.id} does not fit ${name}@${version}; using the upstream package. Review the patch inventory.`);
93
+ }
94
+ if (!selected.length && candidates.length && !matched.length) {
95
+ warnOnce(`${name}@${version}`, `[axom-patch] No verified patch for ${name}@${version}; using the upstream package. Review the patch inventory.`);
96
+ }
97
+ return selected;
98
+ }
99
+ /** The identity of a recipe set: any recipe change is a new digest, hence a new build. */
100
+ export function patchDigest(recipes) {
101
+ return sha(JSON.stringify(recipes));
102
+ }
103
+ /**
104
+ * Copy the pristine `node_modules` (reflink where the filesystem supports it — `COPYFILE_FICLONE`
105
+ * falls back to an ordinary copy by itself; relative symlinks such as `.bin/*` are kept relative so
106
+ * the copy never points back into pristine) into `stagedRoot`, patch the copy, and write a receipt.
107
+ * Returns the sha256 of every patched file, relative to the package directory.
108
+ */
109
+ export async function createPatchedTree(pristineRoot, stagedRoot, name, version, recipes) {
110
+ await cp(path.join(pristineRoot, 'node_modules'), path.join(stagedRoot, 'node_modules'), {
111
+ recursive: true, verbatimSymlinks: true, mode: constants.COPYFILE_FICLONE,
112
+ });
113
+ const packageDir = inside(path.join(stagedRoot, 'node_modules'), name);
114
+ const files = await applyPackagePatches(packageDir, recipes);
115
+ const expected = expectedPatchedFiles(recipes);
116
+ if (JSON.stringify(sortKeys(files)) !== JSON.stringify(sortKeys(expected)))
117
+ throw new Error('Incomplete Axom adapter patch');
118
+ await writeFile(path.join(stagedRoot, 'axom-patches.json'), JSON.stringify({
119
+ digest: patchDigest(recipes), package: name, version, patches: recipes.map((recipe) => recipe.id), files,
120
+ }) + '\n');
121
+ console.info(`[axom-patch] Applied ${recipes.map((recipe) => recipe.id).join(', ')} to ${name}@${version}`);
122
+ return files;
123
+ }
124
+ /** The files a recipe set rewrites and their expected sha256, derived from the shipped recipes. */
125
+ export function expectedPatchedFiles(recipes) {
126
+ return Object.fromEntries(recipes.flatMap((recipe) => recipe.edits.map((edit) => [edit.file, edit.after_sha256])));
127
+ }
128
+ function sortKeys(record) {
129
+ return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
130
+ }
131
+ /** The launch-time tamper check: every patched file still hashes to what was recorded at install. */
132
+ export async function verifyPatchedFiles(packageDir, files) {
133
+ for (const [file, hash] of Object.entries(files)) {
134
+ let content;
135
+ try {
136
+ content = await readFile(within(packageDir, file));
137
+ }
138
+ catch {
139
+ throw new Error(`Axom patched adapter is missing ${file}`);
140
+ }
141
+ if (sha(content) !== hash)
142
+ throw new Error(`Axom patched adapter was modified: ${file}`);
143
+ }
144
+ }