create-walle 0.10.2 → 0.10.3

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 (488) hide show
  1. package/README.md +3 -1
  2. package/lib/release-schema.json +2 -2
  3. package/lib/runtime-closure-policy.json +8 -7
  4. package/package.json +1 -1
  5. package/template/coding-task-manager/api-prompts.js +45 -7
  6. package/template/coding-task-manager/bin/claude-hook.js +55 -1
  7. package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
  8. package/template/coding-task-manager/bin/land.js +2069 -0
  9. package/template/coding-task-manager/bin/memory-census.js +4 -2
  10. package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
  11. package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
  12. package/template/coding-task-manager/db.js +156 -32
  13. package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
  14. package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
  15. package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
  16. package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
  17. package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
  18. package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
  19. package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
  20. package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
  21. package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
  22. package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
  23. package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
  24. package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
  25. package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
  26. package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
  27. package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
  28. package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
  29. package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
  30. package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
  31. package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
  32. package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
  33. package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
  34. package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
  35. package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
  36. package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
  37. package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
  38. package/template/coding-task-manager/lib/agent-definitions.js +19 -0
  39. package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
  40. package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
  41. package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
  42. package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
  43. package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
  44. package/template/coding-task-manager/lib/agent-presets.js +84 -1
  45. package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
  46. package/template/coding-task-manager/lib/agent-version-service.js +74 -11
  47. package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
  48. package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
  49. package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
  50. package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
  51. package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
  52. package/template/coding-task-manager/lib/api/fleet.js +118 -0
  53. package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
  54. package/template/coding-task-manager/lib/api/ingress.js +9 -1
  55. package/template/coding-task-manager/lib/api/landing.js +403 -0
  56. package/template/coding-task-manager/lib/api/machines.js +691 -2
  57. package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
  58. package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
  59. package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
  60. package/template/coding-task-manager/lib/api/session-attach.js +623 -0
  61. package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
  62. package/template/coding-task-manager/lib/api/sessions.js +77 -51
  63. package/template/coding-task-manager/lib/api/setup-network.js +544 -16
  64. package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
  65. package/template/coding-task-manager/lib/api/setup.js +829 -10
  66. package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
  67. package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
  68. package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
  69. package/template/coding-task-manager/lib/auth-rules.js +292 -21
  70. package/template/coding-task-manager/lib/background-stop.js +369 -0
  71. package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
  72. package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
  73. package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
  74. package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
  75. package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
  76. package/template/coding-task-manager/lib/build-info.js +4 -2
  77. package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
  78. package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
  79. package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
  80. package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
  81. package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
  82. package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
  83. package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
  84. package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
  85. package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
  86. package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
  87. package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
  88. package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
  89. package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
  90. package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
  91. package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
  92. package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
  93. package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
  94. package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
  95. package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
  96. package/template/coding-task-manager/lib/conversation-sync.js +355 -0
  97. package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
  98. package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
  99. package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
  100. package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
  101. package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
  102. package/template/coding-task-manager/lib/db-domains.js +1 -0
  103. package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
  104. package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
  105. package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
  106. package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
  107. package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
  108. package/template/coding-task-manager/lib/feature-manifest.js +4 -0
  109. package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
  110. package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
  111. package/template/coding-task-manager/lib/fleet-hub.js +762 -0
  112. package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
  113. package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
  114. package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
  115. package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
  116. package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
  117. package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
  118. package/template/coding-task-manager/lib/fleet-update.js +798 -0
  119. package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
  120. package/template/coding-task-manager/lib/fs-watcher.js +58 -42
  121. package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
  122. package/template/coding-task-manager/lib/headless-term-service.js +84 -8
  123. package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
  124. package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
  125. package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
  126. package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
  127. package/template/coding-task-manager/lib/host-handoff.js +16 -19
  128. package/template/coding-task-manager/lib/host-stats.js +150 -3
  129. package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
  130. package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
  131. package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
  132. package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
  133. package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
  134. package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
  135. package/template/coding-task-manager/lib/hub-bridge.js +398 -0
  136. package/template/coding-task-manager/lib/idle-notify.js +491 -0
  137. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
  138. package/template/coding-task-manager/lib/instance-manifest.js +5 -1
  139. package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
  140. package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
  141. package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
  142. package/template/coding-task-manager/lib/landing/config.js +536 -0
  143. package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
  144. package/template/coding-task-manager/lib/landing/gate.js +360 -0
  145. package/template/coding-task-manager/lib/landing/git.js +344 -0
  146. package/template/coding-task-manager/lib/landing/lease.js +247 -0
  147. package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
  148. package/template/coding-task-manager/lib/landing/proof.js +223 -0
  149. package/template/coding-task-manager/lib/landing/queue.js +998 -0
  150. package/template/coding-task-manager/lib/landing/radar.js +489 -0
  151. package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
  152. package/template/coding-task-manager/lib/landing/state.js +263 -0
  153. package/template/coding-task-manager/lib/landing/stop.js +49 -0
  154. package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
  155. package/template/coding-task-manager/lib/landing/sync.js +456 -0
  156. package/template/coding-task-manager/lib/landing/train.js +885 -0
  157. package/template/coding-task-manager/lib/landing/window.js +57 -0
  158. package/template/coding-task-manager/lib/launch-environment.js +121 -0
  159. package/template/coding-task-manager/lib/launch-model-override.js +36 -10
  160. package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
  161. package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
  162. package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
  163. package/template/coding-task-manager/lib/lead-model-options.js +160 -3
  164. package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
  165. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
  166. package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
  167. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
  168. package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
  169. package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
  170. package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
  171. package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
  172. package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
  173. package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
  174. package/template/coding-task-manager/lib/machine-registry.js +750 -5
  175. package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
  176. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
  177. package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
  178. package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
  179. package/template/coding-task-manager/lib/memory-join.js +337 -0
  180. package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
  181. package/template/coding-task-manager/lib/memory-sync.js +565 -0
  182. package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
  183. package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
  184. package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
  185. package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
  186. package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
  187. package/template/coding-task-manager/lib/model-availability.js +727 -0
  188. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
  189. package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
  190. package/template/coding-task-manager/lib/model-id-rule.js +19 -0
  191. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  192. package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
  193. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  194. package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
  195. package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
  196. package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
  197. package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
  198. package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
  199. package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
  200. package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
  201. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  202. package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
  203. package/template/coding-task-manager/lib/native-history-control.js +6 -4
  204. package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
  205. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
  206. package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
  207. package/template/coding-task-manager/lib/opencode-cli.js +480 -0
  208. package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
  209. package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
  210. package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
  211. package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
  212. package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
  213. package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
  214. package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
  215. package/template/coding-task-manager/lib/perf-summary.js +7 -0
  216. package/template/coding-task-manager/lib/perf-tracker.js +63 -2
  217. package/template/coding-task-manager/lib/project-config.js +7 -0
  218. package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
  219. package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
  220. package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
  221. package/template/coding-task-manager/lib/read-pool-client.js +89 -2
  222. package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
  223. package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
  224. package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
  225. package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
  226. package/template/coding-task-manager/lib/relay-client.js +57 -3
  227. package/template/coding-task-manager/lib/relink-audit.js +34 -5
  228. package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
  229. package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
  230. package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
  231. package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
  232. package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
  233. package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
  234. package/template/coding-task-manager/lib/restart-guard.js +18 -6
  235. package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
  236. package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
  237. package/template/coding-task-manager/lib/resume-admission.js +17 -3
  238. package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
  239. package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
  240. package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
  241. package/template/coding-task-manager/lib/runtime-contract.js +3 -1
  242. package/template/coding-task-manager/lib/schema-indexes.js +1 -0
  243. package/template/coding-task-manager/lib/session-context-locator.js +742 -0
  244. package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
  245. package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
  246. package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
  247. package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
  248. package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
  249. package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
  250. package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
  251. package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
  252. package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
  253. package/template/coding-task-manager/lib/session-create/response.js +7 -7
  254. package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
  255. package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
  256. package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
  257. package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
  258. package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
  259. package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
  260. package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
  261. package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
  262. package/template/coding-task-manager/lib/session-empty.js +13 -10
  263. package/template/coding-task-manager/lib/session-host-manager.js +60 -0
  264. package/template/coding-task-manager/lib/session-jobs.js +3 -1
  265. package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
  266. package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
  267. package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
  268. package/template/coding-task-manager/lib/session-park.js +738 -0
  269. package/template/coding-task-manager/lib/session-path-grant.js +13 -4
  270. package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
  271. package/template/coding-task-manager/lib/session-transport.js +17 -0
  272. package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
  273. package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
  274. package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
  275. package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
  276. package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
  277. package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
  278. package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
  279. package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
  280. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
  281. package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
  282. package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
  283. package/template/coding-task-manager/lib/submission-ack.js +90 -2
  284. package/template/coding-task-manager/lib/systemd-scope.js +115 -0
  285. package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
  286. package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
  287. package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
  288. package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
  289. package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
  290. package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
  291. package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
  292. package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
  293. package/template/coding-task-manager/lib/transcript-source.js +36 -4
  294. package/template/coding-task-manager/lib/transcript-store.js +9 -0
  295. package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
  296. package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
  297. package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
  298. package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
  299. package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
  300. package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
  301. package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
  302. package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
  303. package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
  304. package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
  305. package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
  306. package/template/coding-task-manager/lib/walle/turn.js +1 -1
  307. package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
  308. package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
  309. package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
  310. package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
  311. package/template/coding-task-manager/lib/walle-row-history.js +495 -0
  312. package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
  313. package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
  314. package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
  315. package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
  316. package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
  317. package/template/coding-task-manager/lib/ws/misc.js +1 -1
  318. package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
  319. package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
  320. package/template/coding-task-manager/oauth-proxy.js +112 -115
  321. package/template/coding-task-manager/package-lock.json +10 -94
  322. package/template/coding-task-manager/package.json +4 -3
  323. package/template/coding-task-manager/protocol/message-table.json +10 -0
  324. package/template/coding-task-manager/protocol/messages.js +10 -0
  325. package/template/coding-task-manager/protocol/ws-schema.json +20 -0
  326. package/template/coding-task-manager/protocol/ws-schema.md +2 -0
  327. package/template/coding-task-manager/public/css/app.css +170 -0
  328. package/template/coding-task-manager/public/css/walle-session.css +4 -0
  329. package/template/coding-task-manager/public/index.html +473 -243
  330. package/template/coding-task-manager/public/js/agent-command.js +86 -0
  331. package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
  332. package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
  333. package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
  334. package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
  335. package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
  336. package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
  337. package/template/coding-task-manager/public/js/background-work.js +196 -27
  338. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
  339. package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
  340. package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
  341. package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
  342. package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
  343. package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
  344. package/template/coding-task-manager/public/js/document-review-links.js +9 -1
  345. package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
  346. package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
  347. package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
  348. package/template/coding-task-manager/public/js/message-renderer.js +4 -3
  349. package/template/coding-task-manager/public/js/model-picker.js +3 -1
  350. package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
  351. package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
  352. package/template/coding-task-manager/public/js/pasted-content.js +236 -0
  353. package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
  354. package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
  355. package/template/coding-task-manager/public/js/prompts.js +11 -10
  356. package/template/coding-task-manager/public/js/protocol.js +10 -0
  357. package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
  358. package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
  359. package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
  360. package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
  361. package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
  362. package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
  363. package/template/coding-task-manager/public/js/stream-view.js +6 -5
  364. package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
  365. package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
  366. package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
  367. package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
  368. package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
  369. package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
  370. package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
  371. package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
  372. package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
  373. package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
  374. package/template/coding-task-manager/public/js/walle.js +6 -0
  375. package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
  376. package/template/coding-task-manager/public/m/app.css +140 -0
  377. package/template/coding-task-manager/public/m/app.js +507 -68
  378. package/template/coding-task-manager/public/m/index.html +17 -0
  379. package/template/coding-task-manager/public/m/machines.js +838 -4
  380. package/template/coding-task-manager/public/m/private-api.js +31 -1
  381. package/template/coding-task-manager/public/usage.html +1 -0
  382. package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
  383. package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
  384. package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
  385. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
  386. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
  387. package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
  388. package/template/coding-task-manager/server.js +1950 -11679
  389. package/template/coding-task-manager/session-integrity.js +64 -4
  390. package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
  391. package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
  392. package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
  393. package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
  394. package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
  395. package/template/coding-task-manager/workers/session-host-process.js +5 -5
  396. package/template/coding-task-manager/workers/watch-worker.js +105 -0
  397. package/template/coding-task-manager/workers/worktree-worker.js +168 -33
  398. package/template/ctm-relay/lib/protocol.js +14 -1
  399. package/template/package-lock.json +2 -2
  400. package/template/package.json +1 -1
  401. package/template/shared/backup-retention.js +82 -0
  402. package/template/shared/conversation-admission.js +2 -0
  403. package/template/shared/conversation-provenance.js +3 -7
  404. package/template/shared/conversation-storage-v2-contract.js +5 -0
  405. package/template/shared/linked-worktree.js +51 -0
  406. package/template/shared/macos-fda-target.js +57 -0
  407. package/template/shared/orphan-parents.js +55 -0
  408. package/template/shared/release-schema.json +2 -2
  409. package/template/shared/storage/contract.js +3 -0
  410. package/template/wall-e/adapters/imessage.js +12 -0
  411. package/template/wall-e/agent.js +71 -4
  412. package/template/wall-e/api-walle.js +21 -1
  413. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
  414. package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
  415. package/template/wall-e/brain/brain-join.js +859 -0
  416. package/template/wall-e/brain/brain-sync-membership.js +20 -0
  417. package/template/wall-e/brain/brain-sync-store.js +131 -77
  418. package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
  419. package/template/wall-e/brain/brain-sync-v2.js +578 -0
  420. package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
  421. package/template/wall-e/brain/deterministic-id.js +51 -0
  422. package/template/wall-e/brain/memories.js +2 -2
  423. package/template/wall-e/brain/migrations.js +40 -6
  424. package/template/wall-e/brain/model-identity-migration.js +1 -1
  425. package/template/wall-e/brain/owner-call-whitelist.js +23 -7
  426. package/template/wall-e/brain/schema.js +45 -85
  427. package/template/wall-e/brain.js +171 -35
  428. package/template/wall-e/chat.js +3 -3
  429. package/template/wall-e/coding/agent-shell-env.js +7 -2
  430. package/template/wall-e/coding/prompt-bundle.js +66 -21
  431. package/template/wall-e/config/main-loop-exemptions.json +2 -2
  432. package/template/wall-e/http/admin-api.js +37 -0
  433. package/template/wall-e/http/gws-api.js +4 -0
  434. package/template/wall-e/http/helper-rules-api.js +7 -1
  435. package/template/wall-e/http/model-admin.js +8 -1
  436. package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
  437. package/template/wall-e/http/model-proxy-providers.js +174 -1
  438. package/template/wall-e/http/model-route-health.js +157 -0
  439. package/template/wall-e/http/sync-api.js +278 -464
  440. package/template/wall-e/jobs/job-runner.js +28 -1
  441. package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
  442. package/template/wall-e/lib/ctm-data-dir.js +26 -0
  443. package/template/wall-e/lib/derivation-home.js +147 -0
  444. package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
  445. package/template/wall-e/lib/helper-rules/grade.js +51 -8
  446. package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
  447. package/template/wall-e/lib/helper-rules/store.js +4 -2
  448. package/template/wall-e/lib/landing-mcp-tools.js +170 -0
  449. package/template/wall-e/lib/mcp-client-config.js +133 -14
  450. package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
  451. package/template/wall-e/lib/mcp-integration.js +58 -13
  452. package/template/wall-e/lib/parent-death.js +7 -1
  453. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
  454. package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
  455. package/template/wall-e/llm/delegate-vendor-default.js +12 -5
  456. package/template/wall-e/llm/model-roles.js +1 -1
  457. package/template/wall-e/llm/native-delegate-selection.js +56 -8
  458. package/template/wall-e/llm/worker-delegate-args.js +72 -7
  459. package/template/wall-e/llm/worker-mcp-routing.js +66 -17
  460. package/template/wall-e/loops/brain-custom-writes.js +19 -0
  461. package/template/wall-e/loops/ingest.js +17 -1
  462. package/template/wall-e/loops/think.js +19 -4
  463. package/template/wall-e/mcp-contract.js +222 -6
  464. package/template/wall-e/mcp-protocol-contract.js +40 -5
  465. package/template/wall-e/mcp-server.js +128 -29
  466. package/template/wall-e/mcp-worker-schemas.js +5 -5
  467. package/template/wall-e/memory/ctm-context-client.js +85 -0
  468. package/template/wall-e/memory/ctm-session-context.js +9 -3
  469. package/template/wall-e/memory/hub-bridge-client.js +388 -0
  470. package/template/wall-e/memory/replica-origin.js +56 -0
  471. package/template/wall-e/memory/session-pool.js +135 -0
  472. package/template/wall-e/runtime/live-capabilities.js +8 -3
  473. package/template/wall-e/server.js +4 -2
  474. package/template/wall-e/shared/release-schema.json +2 -2
  475. package/template/wall-e/shared/sqlite-file-set.js +39 -0
  476. package/template/wall-e/shared/storage/contract.js +3 -0
  477. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  478. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
  479. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
  480. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
  481. package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
  482. package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
  483. package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
  484. package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
  485. package/template/wall-e/tools/chrome-automation.js +6 -0
  486. package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
  487. package/template/wall-e/workers/brain-owner-worker.js +18 -4
  488. package/template/wall-e/workers/mcp-gateway-process.js +5 -5
@@ -28,8 +28,17 @@
28
28
  // the /api/setup/* rules), and a route with no rule is 403 auth_rule_missing by design — so the
29
29
  // rule rows and these registrations must land together.
30
30
 
31
+ const os = require('os');
31
32
  const { registerApiRoutes } = require('../api-router');
32
33
  const machinePairing = require('../machine-pairing');
34
+ // WP-8 (docs/remote-agent-skills-bundles-2026-10-25.html §5 D-12/D-13, §6.5): the hub end of the
35
+ // one-skill exchange — import a box skill into this hub, retire the box copy, and per-machine plugin
36
+ // exclusions. Policy is in the lib; this module owns the dial and the request body.
37
+ const skillTransfer = require('../agent-bundle/skill-transfer');
38
+ // WP-1 (docs/remote-box-model-access-2026-10-26.html §6.1, §5.3 D-2/D-3/D-9): the box's own
39
+ // reachability answer — "which of the routes configured on THIS machine can this machine reach".
40
+ // A leaf module (http/https only), so it adds no cycle to this one.
41
+ const modelAvailability = require('../model-availability');
33
42
 
34
43
  // §6.3: the identity probe is a reachability check, not a data fetch — it must not hold a request
35
44
  // open on a box that is merely slow to answer.
@@ -48,8 +57,43 @@ function configure({
48
57
  sendJsonResponse,
49
58
  ready = null,
50
59
  fetchImpl = null,
60
+ // WP-8: the hub's own data dir (import backups + the exclusion file) and the hub user's home (the
61
+ // installed skill root). Both injected, so a route test never reads the real hub home/data dir.
62
+ dataDir = null,
63
+ hubHomeDir = null,
64
+ hubEnv = null,
65
+ now = null,
66
+ // WP-1 (§6.1): the availability handler's three seams. All optional — production reads the
67
+ // loopback Wall-E client, the coding-agent-auth probe and the real clock.
68
+ walleClient = null,
69
+ probe = null,
70
+ probeLoginState = null,
71
+ delegateDefaults = null,
51
72
  } = {}) {
52
- _deps = { db, registry, pool, readJsonRequestBody, sendJsonResponse, ready, fetchImpl };
73
+ _deps = {
74
+ db, registry, pool, readJsonRequestBody, sendJsonResponse, ready, fetchImpl, dataDir, hubHomeDir, hubEnv, now,
75
+ walleClient, probe, probeLoginState, delegateDefaults,
76
+ };
77
+ }
78
+
79
+ /** The hub data dir (the exclusion store + import backups live under it). */
80
+ function _dataDir() {
81
+ if (_deps && _deps.dataDir) return _deps.dataDir;
82
+ return require('../data-dir').getDataDir();
83
+ }
84
+
85
+ /** The hub user's home and env — the two inputs the installed skill root is resolved from. */
86
+ function _hubHomeDir() {
87
+ return (_deps && _deps.hubHomeDir) || os.homedir();
88
+ }
89
+
90
+ function _hubEnv() {
91
+ return (_deps && _deps.hubEnv) || process.env;
92
+ }
93
+
94
+ /** An injected clock, so a test controls the retire/backup directory stamps. */
95
+ function _now() {
96
+ return typeof (_deps && _deps.now) === 'function' ? Number(_deps.now()) || 0 : Date.now();
53
97
  }
54
98
 
55
99
  /**
@@ -202,7 +246,22 @@ async function _postToTarget(target, pathname, body, headers) {
202
246
 
203
247
  async function handleMachinesList(req, res) {
204
248
  const registry = _deps && _deps.registry;
205
- const { machines, hubNetworkSuspect } = registry.listMachines({ db: _deps && _deps.db });
249
+ // WP-8 (§6.5): the row's `skills` block also carries this machine's plugin exclusions. The provider
250
+ // is injected here — the registry module never reads a data dir — and it reads the one hub-side file
251
+ // (cached in memory by lib/agent-bundle/skill-transfer.js).
252
+ try {
253
+ await skillTransfer.loadExcludePlugins(_dataDir());
254
+ } catch {
255
+ /* expected: an unreadable exclusion file is "nothing excluded", never a failed list */
256
+ }
257
+ const excludePluginsFor = (instanceId) => {
258
+ try {
259
+ return skillTransfer.excludePluginsFor(_dataDir(), instanceId);
260
+ } catch {
261
+ return []; // expected: an unreadable exclusion file is "nothing excluded", never a failed list
262
+ }
263
+ };
264
+ const { machines, hubNetworkSuspect } = registry.listMachines({ db: _deps && _deps.db, excludePluginsFor });
206
265
  // WP-1 (D-4) / P-8: which credential this hub will actually dial THIS row with — computed per row
207
266
  // from the row's instanceId, because a pool hub sends the pool secret only to pool members and the
208
267
  // env bearer to a legacy box that never joined (see peerCredentialState). The token itself is
@@ -211,6 +270,15 @@ async function handleMachinesList(req, res) {
211
270
  // WP-3: the box's coding-CLI sign-in truth the probe sweep last read. Guarded by typeof so a test
212
271
  // registry that only carries listMachines still serves rows (they just read 'unknown').
213
272
  const agentAuthFor = registry && typeof registry.agentAuthFor === 'function' ? registry.agentAuthFor : null;
273
+ // WP-13b (D-15): the box's own available-agents presence (`{ dsh: { available } }`), the read the
274
+ // probe sweep keeps. Guarded the same way: a test registry that only carries listMachines serves
275
+ // rows whose presence is null ("unknown"), never a fabricated verdict.
276
+ const agentAvailabilityFor = registry && typeof registry.agentAvailabilityFor === 'function' ? registry.agentAvailabilityFor : null;
277
+ // WP-0 (docs/fleet-auto-update-pooled-machines-2026-10-26.html §6, S-3): the hub's own fleet
278
+ // target — its local refs/heads/main (D-1: local main IS the fleet target). Memoized in
279
+ // lib/fleet-update.js, so a list of N boxes costs one `git` read every 15 s, and a hub without a
280
+ // readable main answers '' (never an error page for an informational field).
281
+ const fleetTarget = await require('../fleet-update').readHubTarget();
214
282
  _send(res, 200, {
215
283
  machines: machines.map((machine) => ({
216
284
  instanceId: machine.instanceId,
@@ -220,6 +288,9 @@ async function handleMachinesList(req, res) {
220
288
  enabled: machine.enabled,
221
289
  health: machine.health,
222
290
  healthDetail: machine.healthDetail,
291
+ // WP-3 rev 3 (docs/off-lan-hub-to-box-access-2026-10-31.html §13.6 D-17): why an unreachable
292
+ // box cannot be dialled from here ('no_away_path' / 'away_path_down'), or null.
293
+ awayReason: machine.awayReason || null,
223
294
  lastSeenAt: machine.lastSeenAt,
224
295
  pickerState: machine.pickerState,
225
296
  visible: machine.visible,
@@ -238,11 +309,38 @@ async function handleMachinesList(req, res) {
238
309
  os: machine.os || '',
239
310
  arch: machine.arch || '',
240
311
  hostname: machine.hostname || '',
312
+ // WP-0 (S-3): the build the box published in its own manifest (null = this hub has never read
313
+ // one). `build.sha` is the version truth the Machines card compares against `fleetTarget`.
314
+ build: machine.build || null,
315
+ // WP-2 (docs/fleet-auto-update-pooled-machines-2026-10-26.html §8 WP-2, S-3): this box's last
316
+ // fleet decision — `{ target, reason, behind, inFlight, lastVerdict }`, or null before the
317
+ // reconciler's first pass (never a fabricated "current"). A pure read of the last plan + the
318
+ // cached hub state: no I/O and no git, because this is evaluated once per row per list.
319
+ fleet: require('../fleet-hub').fleetRowFor(machine.instanceId),
241
320
  peerCredential: peerCredentialFor(process.env, { instanceId: machine.instanceId }),
242
321
  agents: (agentAuthFor ? agentAuthFor.call(registry, machine.instanceId) : null)
243
322
  || { claude: 'unknown', codex: 'unknown', checkedAt: 0 },
323
+ // WP-13b (D-15): whether THIS box has the DeepSeek Harness, as the hub last read it from the
324
+ // box's own available-agents answer. `{ dsh: true }` = the box has it, `{ dsh: false }` = it
325
+ // said it does not, `{ dsh: null }` = this hub has no verdict (P3 "Unknown != Dead"): the
326
+ // New Session card disables the dsh card only on an explicit false.
327
+ agentPresence: (() => {
328
+ const verdict = agentAvailabilityFor ? agentAvailabilityFor.call(registry, machine.instanceId) : null;
329
+ const available = verdict && verdict.dsh ? verdict.dsh.available : undefined;
330
+ return { dsh: typeof available === 'boolean' ? available : null };
331
+ })(),
332
+ // WP-3 (remote agent skills §6.5): the last bundle push to this box — sentTreeHash/pushError —
333
+ // plus the WP-7 classification of its own skills and the WP-8 plugin exclusions.
334
+ skills: machine.skills || null,
335
+ // WP-1 (remote box model access §6.2): the box's OWN model-availability block, read on the
336
+ // existing probe tick — null = never read, or a box older than WP-1 (404). Exposed exactly
337
+ // like `skills`: the registry owns the read, this module only serves the row.
338
+ models: machine.models || null,
244
339
  })),
245
340
  hubNetworkSuspect,
341
+ // WP-0 (S-3): the hub's own fleet target (local refs/heads/main), '' when this hub has no
342
+ // readable main. Top level because it is one fact about the hub, not about any one box.
343
+ fleetTarget,
246
344
  });
247
345
  }
248
346
 
@@ -441,12 +539,593 @@ async function handleMachineRemove(req, res, url, params) {
441
539
  return _send(res, 200, { ok: true, removed: Number(removed) || 0, rotated, unreachable });
442
540
  }
443
541
 
542
+ /* ------------------------------------------------------------------------------------------------
543
+ * WP-8 (§6.5): the hub end of the one-skill exchange, and per-machine plugin exclusions.
544
+ * ---------------------------------------------------------------------------------------------- */
545
+
546
+ /** One machine row by instance id, in the same shape lib/machine-registry.js's view reads. */
547
+ function _machineRow(instanceId) {
548
+ const rows = (_deps && _deps.db && _deps.db.listMachineRows()) || [];
549
+ const wanted = String(instanceId || '');
550
+ return rows.find((row) => String((row && (row.instanceId || row.instance_id)) || '') === wanted) || null;
551
+ }
552
+
553
+ /** P-4 dial order for a row: the roster `urls`, else the single pinned `url`. */
554
+ function _dialUrlsFor(row) {
555
+ const list = (Array.isArray(row && row.urls) ? row.urls : [])
556
+ .map((url) => String(url || '').trim().replace(/\/+$/, ''))
557
+ .filter(Boolean);
558
+ if (list.length) return list;
559
+ const pinned = String((row && row.url) || '').trim().replace(/\/+$/, '');
560
+ return pinned ? [pinned] : [];
561
+ }
562
+
563
+ /**
564
+ * One request to a box, in the P-4 order: try every advertised URL and stop at the first one that
565
+ * ANSWERS (a transport failure moves on; an HTTP answer — even a refusal — ends the attempt, because a
566
+ * second URL that answers differently is a different machine). The credential is THIS hub's, per
567
+ * target (`_peerHeadersFor`), and no answer is ever followed to a second address.
568
+ *
569
+ * @returns {Promise<{ok: boolean, status: number, body: object|null}>}
570
+ */
571
+ async function _callBox(instanceId, pathname, { method = 'GET', body = null } = {}) {
572
+ const row = _machineRow(instanceId);
573
+ const headers = _peerHeadersFor(instanceId);
574
+ for (const base of _dialUrlsFor(row)) {
575
+ const res = await _fetchJson(`${base}${pathname}`, { method, body, headers });
576
+ if (res.status) return res;
577
+ }
578
+ return { ok: false, status: 0, body: null };
579
+ }
580
+
581
+ /**
582
+ * This hub's credential state for a box — `missing` means there is nothing to dial WITH, which is a
583
+ * 409 the user can act on, not a 502 "the box is down".
584
+ */
585
+ function _credentialMissing(instanceId) {
586
+ try {
587
+ return require('../remote-machine-create').peerCredentialState(process.env, { instanceId }) === 'missing';
588
+ } catch {
589
+ return false; // expected: a credential-probe fault is not proof the credential is absent
590
+ }
591
+ }
592
+
593
+ /** Every WP-8 failure path: the transfer's own typed status, else a 500 in the module's shape. */
594
+ function _skillFail(res, error) {
595
+ if (error instanceof skillTransfer.AgentSkillError) {
596
+ const payload = { ok: false, error: error.code };
597
+ if (error.detail) payload.detail = error.detail;
598
+ return _send(res, error.status, payload);
599
+ }
600
+ if (_deps && _deps.errorSink && typeof _deps.errorSink.report === 'function') {
601
+ _deps.errorSink.report('machines:skills', error);
602
+ }
603
+ return _send(res, 500, { ok: false, error: 'internal_error' });
604
+ }
605
+
606
+ /** §6.5 step 7: an import/retire moved the hub's own tree, so both caches are now stale. */
607
+ function _invalidateSkillsCaches(instanceId) {
608
+ const registry = _deps && _deps.registry;
609
+ if (!registry) return;
610
+ if (typeof registry.invalidateHubItemCache === 'function') registry.invalidateHubItemCache();
611
+ if (typeof registry.markAgentSkillsStale === 'function') registry.markAgentSkillsStale(instanceId);
612
+ }
613
+
614
+ /** The transports the transfer lib calls — built here so the lib owns policy and this module owns the socket. */
615
+ function _boxTransports(instanceId) {
616
+ return {
617
+ fetchExport: ({ agent, name }) => _callBox(
618
+ instanceId,
619
+ `/api/agent-skills/export?agent=${encodeURIComponent(agent)}&name=${encodeURIComponent(name)}`,
620
+ { method: 'GET' },
621
+ ),
622
+ retire: ({ agent, name, expectHash }) => _callBox(instanceId, '/api/agent-skills/retire', {
623
+ method: 'POST',
624
+ body: { agent, name, expectHash },
625
+ }),
626
+ };
627
+ }
628
+
629
+ /**
630
+ * WP-8 §6.5: `POST /api/machines/:instanceId/skills/import { agent, name, expectBoxHash }`.
631
+ *
632
+ * The bytes come off the box, are verified against the hash the user reviewed (AC-8c), and are written
633
+ * into the hub's own skill root with a backup first (AC-8b, P-5) — and only then is the box asked to
634
+ * retire its shadowing copy (D-13). A retire failure never undoes the import; it is reported.
635
+ */
636
+ async function handleMachineSkillsImport(req, res, url, params) {
637
+ const instanceId = decodeURIComponent((params && (params['1'] || params.id)) || '');
638
+ if (!_machineRow(instanceId)) return _send(res, 404, { ok: false, error: 'machine_not_found' });
639
+ if (_credentialMissing(instanceId)) return _send(res, 409, { ok: false, error: 'agent_skill_no_credential' });
640
+ let body = {};
641
+ try {
642
+ body = await _deps.readJsonRequestBody(req);
643
+ } catch {
644
+ return _send(res, 400, { ok: false, error: 'invalid_json' });
645
+ }
646
+ const fields = body && !Array.isArray(body) ? body : {};
647
+ try {
648
+ const result = await skillTransfer.importBoxSkillToHub({
649
+ agent: fields.agent,
650
+ name: fields.name,
651
+ expectBoxHash: fields.expectBoxHash,
652
+ dataDir: _dataDir(),
653
+ hubHomeDir: _hubHomeDir(),
654
+ hubEnv: _hubEnv(),
655
+ now: _now(),
656
+ ..._boxTransports(instanceId),
657
+ });
658
+ _invalidateSkillsCaches(instanceId);
659
+ return _send(res, 200, { ok: true, ...result });
660
+ } catch (error) {
661
+ return _skillFail(res, error);
662
+ }
663
+ }
664
+
665
+ /**
666
+ * WP-8 §6.5: `POST /api/machines/:instanceId/skills/retire { agent, name, expectBoxHash }` — "use the
667
+ * laptop's copy" (or a redundant `same` copy): the box's folder moves aside and NOTHING is written here.
668
+ */
669
+ async function handleMachineSkillsRetire(req, res, url, params) {
670
+ const instanceId = decodeURIComponent((params && (params['1'] || params.id)) || '');
671
+ if (!_machineRow(instanceId)) return _send(res, 404, { ok: false, error: 'machine_not_found' });
672
+ if (_credentialMissing(instanceId)) return _send(res, 409, { ok: false, error: 'agent_skill_no_credential' });
673
+ let body = {};
674
+ try {
675
+ body = await _deps.readJsonRequestBody(req);
676
+ } catch {
677
+ return _send(res, 400, { ok: false, error: 'invalid_json' });
678
+ }
679
+ const fields = body && !Array.isArray(body) ? body : {};
680
+ try {
681
+ const result = await skillTransfer.retireBoxSkillFromHub({
682
+ agent: fields.agent,
683
+ name: fields.name,
684
+ expectBoxHash: fields.expectBoxHash,
685
+ retire: _boxTransports(instanceId).retire,
686
+ });
687
+ _invalidateSkillsCaches(instanceId);
688
+ return _send(res, 200, { ok: true, ...result });
689
+ } catch (error) {
690
+ return _skillFail(res, error);
691
+ }
692
+ }
693
+
694
+ /**
695
+ * WP-9 (§9 S-9, AC-9b): `GET /api/machines/:instanceId/skills/preview?agent=&name=` — the Review
696
+ * dialog's DIFF SOURCE. The browser cannot call the box's own `GET /api/agent-skills/export`
697
+ * (peer-bearer auth), so this route relays one read: the box's copy through the SAME fetch +
698
+ * verification the import leg runs, and the hub's own copy (or null) beside it.
699
+ *
700
+ * ORDER, and why it differs from the two write legs: the machine is resolved, then the REF is
701
+ * validated (400), and only then is the credential asked for. A malformed `agent`/`name` never dials
702
+ * anything, so its answer must not depend on whether this hub happens to hold a credential for that
703
+ * box — the write legs read the credential first only because their body is the transfer itself.
704
+ */
705
+ async function handleMachineSkillsPreview(req, res, url, params) {
706
+ const instanceId = decodeURIComponent((params && (params['1'] || params.id)) || '');
707
+ const search = (url && typeof url.searchParams === 'object' && url.searchParams) ? url.searchParams : null;
708
+ const agent = String((search && search.get('agent')) || '').trim();
709
+ const name = String((search && search.get('name')) || '').trim();
710
+ if (!_machineRow(instanceId)) return _send(res, 404, { ok: false, error: 'machine_not_found' });
711
+ try {
712
+ skillTransfer.validateSkillRef(agent, name);
713
+ } catch {
714
+ // expected: a bad agent/name is a 400 — the module's own code is reused so the two agree
715
+ return _send(res, 400, { ok: false, error: 'agent_skill_invalid' });
716
+ }
717
+ if (_credentialMissing(instanceId)) return _send(res, 409, { ok: false, error: 'agent_skill_no_credential' });
718
+ try {
719
+ const preview = await skillTransfer.previewBoxSkill({
720
+ agent,
721
+ name,
722
+ hubHomeDir: _hubHomeDir(),
723
+ hubEnv: _hubEnv(),
724
+ fetchExport: _boxTransports(instanceId).fetchExport,
725
+ });
726
+ return _send(res, 200, { ok: true, ...preview });
727
+ } catch (error) {
728
+ return _skillFail(res, error);
729
+ }
730
+ }
731
+
732
+ /**
733
+ * WP-8 §6.5: `PUT /api/machines/:instanceId/skills/exclude-plugins { plugins: [ids] }` — replace one
734
+ * machine's plugin exclusion list (one hub-side JSON file; a DB column is out of scope for this WP).
735
+ */
736
+ async function handleMachineSkillsExcludePlugins(req, res, url, params) {
737
+ const instanceId = decodeURIComponent((params && (params['1'] || params.id)) || '');
738
+ if (!_machineRow(instanceId)) return _send(res, 404, { ok: false, error: 'machine_not_found' });
739
+ let body = {};
740
+ try {
741
+ body = await _deps.readJsonRequestBody(req);
742
+ } catch {
743
+ return _send(res, 400, { ok: false, error: 'invalid_json' });
744
+ }
745
+ const plugins = body && !Array.isArray(body) ? body.plugins : undefined;
746
+ try {
747
+ const stored = await skillTransfer.setExcludePlugins(_dataDir(), instanceId, plugins);
748
+ const registry = _deps && _deps.registry;
749
+ if (registry && typeof registry.noteRegistryChanged === 'function') registry.noteRegistryChanged();
750
+ return _send(res, 200, { ok: true, excludePlugins: stored });
751
+ } catch (error) {
752
+ return _skillFail(res, error);
753
+ }
754
+ }
755
+
756
+ // ---------------------------------------------------------------------------------------------
757
+ // WP-1 (docs/remote-box-model-access-2026-10-26.html §6.1, §5.1 R-2/R-3, §5.3 D-2/D-3/D-9):
758
+ // `GET /api/models/availability` — THIS machine's answer about its OWN route rows.
759
+ //
760
+ // The hub never infers a box's models from its own rows (R-2); it asks the box, and the box answers
761
+ // from its own Wall-E table with an unauthenticated reachability probe (R-3). Auth is the
762
+ // `admin` + peerControl READ row in lib/auth-rules.js — the caller is the paired hub's peer bearer.
763
+ // ---------------------------------------------------------------------------------------------
764
+
765
+ /** The box's own coding-CLI sign-in truth — the SAME source `GET /api/providers/login-state` serves. */
766
+ async function _nativeLoginState() {
767
+ if (_deps && typeof _deps.probeLoginState === 'function') return _deps.probeLoginState();
768
+ try {
769
+ const accounts = await require('../coding-agent-auth').probeAll();
770
+ const signedIn = (entry) => !!(entry && entry.signedIn === true);
771
+ return {
772
+ claude: { signedIn: signedIn(accounts && accounts['claude-code']) },
773
+ codex: { signedIn: signedIn(accounts && accounts.codex) },
774
+ };
775
+ } catch {
776
+ // expected: a CLI that cannot be probed is "we cannot say signed in" — never a 500 on a read
777
+ return { claude: { signedIn: false }, codex: { signedIn: false } };
778
+ }
779
+ }
780
+
781
+ /** The box's own `default:claude` / `default:codex` picks, from its own Wall-E over loopback. */
782
+ async function _delegateDefaults() {
783
+ if (_deps && typeof _deps.delegateDefaults === 'function') return _deps.delegateDefaults();
784
+ try {
785
+ const response = await require('../walle-client').requestJson('/api/wall-e/internal/delegate-defaults');
786
+ const body = response && response.json;
787
+ return body && Array.isArray(body.defaults) ? body.defaults : [];
788
+ } catch {
789
+ // expected: Wall-E unreachable — the route rows are still a useful answer, with no defaults
790
+ return [];
791
+ }
792
+ }
793
+
794
+ async function handleModelsAvailability(req, res, url) {
795
+ const refresh = !!(url && url.searchParams && url.searchParams.get('refresh') === '1');
796
+ const callerKey = String((req && req.ctmAuth && req.ctmAuth.peerInstanceId) || 'local');
797
+ try {
798
+ const body = await modelAvailability.buildAvailability({
799
+ walleClient: (_deps && _deps.walleClient) || require('../walle-client'),
800
+ loginState: await _nativeLoginState(),
801
+ delegatePrefs: await _delegateDefaults(),
802
+ probe: _deps && _deps.probe,
803
+ refresh,
804
+ callerKey,
805
+ instanceId: _instanceId(),
806
+ now: _deps && typeof _deps.now === 'function' ? _deps.now : undefined,
807
+ });
808
+ return _send(res, 200, body);
809
+ } catch (error) {
810
+ // HANDLE: a constant reply, like the rest of this module's failures. The only errors that reach
811
+ // here are the box-local Wall-E read and the probe, and neither message may be echoed back —
812
+ // `availability_failed` is the whole answer (the hub's probe tick records its own verdict).
813
+ void error;
814
+ return _send(res, 500, { ok: false, code: 'availability_failed' });
815
+ }
816
+ }
817
+
818
+ // ---------------------------------------------------------------------------------------------
819
+ // WP-4 (§6.4): `POST /api/models/remove` — the BOX end of "Remove from <box>", and
820
+ // `POST /api/machines/:instanceId/models/remove` — the HUB end that dials it.
821
+ //
822
+ // The direction is the point: the hub never writes a box's route rows (R-2), so removal is asked of
823
+ // the box that owns them and only the box's own Wall-E deletes. Both halves validate with the SAME
824
+ // `modelAvailability.normalizeRouteIds`, so a malformed body is one 400 with one message wherever it
825
+ // is caught; the ids are the only thing that crosses the wire in either direction.
826
+ // ---------------------------------------------------------------------------------------------
827
+
828
+ /** Read the request body, or null when it is not parseable JSON. Shared by the box and hub legs. */
829
+ async function _readObjectBody(req) {
830
+ let body = null;
831
+ try {
832
+ body = await _deps.readJsonRequestBody(req);
833
+ } catch {
834
+ return null; // expected: an unreadable body is the caller's 400, never a 500
835
+ }
836
+ return body && typeof body === 'object' && !Array.isArray(body) ? body : {};
837
+ }
838
+
839
+ /**
840
+ * The box's own 400 for a bad `routeIds` — the same shape on both legs, so a client parses one code.
841
+ * The message is a fixed sentence about the SHAPE only; it never echoes the caller's value.
842
+ */
843
+ function _invalidRouteIds(res, message) {
844
+ return _send(res, 400, { ok: false, code: 'invalid_request', message: String(message || 'routeIds is invalid') });
845
+ }
846
+
847
+ /** §6.4: the BOX end — remove ids from THIS machine's own Wall-E rows. */
848
+ async function handleModelsRemove(req, res) {
849
+ const fields = await _readObjectBody(req);
850
+ if (fields === null) return _invalidRouteIds(res, 'the request body must be JSON');
851
+ const normalized = modelAvailability.normalizeRouteIds(fields.routeIds);
852
+ if (!normalized.ok) return _invalidRouteIds(res, normalized.message);
853
+ try {
854
+ const result = await modelAvailability.removeRoutes({
855
+ walleClient: (_deps && _deps.walleClient) || require('../walle-client'),
856
+ routeIds: normalized.ids,
857
+ });
858
+ return _send(res, 200, { ok: true, removed: result.removed, missing: result.missing });
859
+ } catch (error) {
860
+ if (error && error.code === 'walle_unavailable') return _send(res, 502, { ok: false, code: 'walle_unavailable' });
861
+ // REPORT + HANDLE: a constant reply, like the rest of this module's failures — the message of an
862
+ // unclassified fault is never echoed back (it could only describe this box's own storage).
863
+ if (_deps && _deps.errorSink && typeof _deps.errorSink.report === 'function') _deps.errorSink.report('machines:models-remove', error);
864
+ return _send(res, 500, { ok: false, code: 'remove_failed' });
865
+ }
866
+ }
867
+
868
+ /**
869
+ * The hub's named refusal — the same body every machine forward writes, naming the machine so the
870
+ * card can say WHICH box failed. `status` is 503 for a box that never answered (the dial failed) and
871
+ * 502 for one that answered nothing usable (the module's own spelling for that refusal, as in the
872
+ * pair route above).
873
+ */
874
+ function _machineUnreachable(res, instanceId, machineName, status = 503) {
875
+ const label = String(machineName || instanceId || 'the machine');
876
+ return _send(res, status, {
877
+ ok: false, code: 'machine_unreachable', error: `Machine ${label} is unreachable`,
878
+ machine: String(instanceId || ''), machineName: String(machineName || ''),
879
+ });
880
+ }
881
+
882
+ /**
883
+ * §6.4: the HUB end — validate here (400), then dial the box ONCE with this hub's own credential for
884
+ * that machine (409 when there is none to dial with, exactly like the skills legs) and relay the
885
+ * box's status + body VERBATIM, so the client parses the box's answer as it parses a local one. A box
886
+ * that does not answer is the named 503 `machine_unreachable`.
887
+ *
888
+ * A successful 2xx answer that names `removed` ids also forgets those ids in the hub's cached models
889
+ * block for that machine (lib/machine-registry.js) — the cache is a read of the box's rows and would
890
+ * otherwise keep showing a row the box just deleted until its next 60 s tick.
891
+ */
892
+ async function handleMachineModelsRemove(req, res, url, params) {
893
+ const instanceId = decodeURIComponent((params && (params['1'] || params.id)) || '');
894
+ const row = _machineRow(instanceId);
895
+ if (!row) return _send(res, 404, { ok: false, error: 'machine_not_found' });
896
+ if (_credentialMissing(instanceId)) return _send(res, 409, { ok: false, error: 'agent_skill_no_credential' });
897
+ const fields = await _readObjectBody(req);
898
+ if (fields === null) return _invalidRouteIds(res, 'the request body must be JSON');
899
+ const normalized = modelAvailability.normalizeRouteIds(fields.routeIds);
900
+ if (!normalized.ok) return _invalidRouteIds(res, normalized.message);
901
+ const answer = await _callBox(instanceId, '/api/models/remove', { method: 'POST', body: { routeIds: normalized.ids } });
902
+ if (!answer || !answer.status) return _machineUnreachable(res, instanceId, row.displayName);
903
+ const body = answer.body && typeof answer.body === 'object' ? answer.body : null;
904
+ // An answer with no JSON object is not an answer this client can parse — the same refusal, one
905
+ // status up, never a 2xx carrying nothing.
906
+ if (!body) return _machineUnreachable(res, instanceId, row.displayName, 502);
907
+ if (answer.status >= 200 && answer.status < 300 && Array.isArray(body.removed) && body.removed.length) {
908
+ // REPORT-free by design: a cache that cannot be updated is not a failure of the box's own
909
+ // answer, which is relayed below regardless.
910
+ try {
911
+ require('../machine-registry').forgetMachineModelRoutes(instanceId, body.removed);
912
+ } catch { /* expected: an unwired/legacy registry — the box's answer still stands */ }
913
+ }
914
+ return _send(res, answer.status, body);
915
+ }
916
+
917
+ // ---------------------------------------------------------------------------------------------
918
+ // WP-5 (§5.1 R-5/R-6, §6.4): "Use on <box>" — copy ONE hub route (with its credential) onto a box,
919
+ // on an explicit request, and the BOX end that performs the write.
920
+ //
921
+ // HUB GET /api/machines/:instanceId/models/copyable -> the hub's rows this box lacks
922
+ // HUB POST /api/machines/:instanceId/models/copy -> read the row (key included) over loopback,
923
+ // hand it to the box, relay the box's answer
924
+ // BOX POST /api/models/import -> validate the row, write it to this box's
925
+ // own Wall-E
926
+ //
927
+ // R-5/R-6: per-route and user-initiated, never a bulk export and never a sync. The key exists only
928
+ // inside the hub's in-memory request to the box (`_dialBoxImport`), is never logged and never
929
+ // returned, and both answers carry `{ ok, routeId }` and nothing else.
930
+ // ---------------------------------------------------------------------------------------------
931
+
932
+ /** This process's loopback Wall-E client — the injected seam in tests, the real one in production. */
933
+ function _walle() {
934
+ return (_deps && _deps.walleClient) || require('../walle-client');
935
+ }
936
+
937
+ /** The hub's cached §6.2 block for one box, or null when it has never been read (pre-WP-1 box). */
938
+ function _modelsBlockFor(instanceId) {
939
+ const registry = _deps && _deps.registry;
940
+ if (!registry || typeof registry.modelsFor !== 'function') return null;
941
+ return registry.modelsFor(instanceId) || null;
942
+ }
943
+
944
+ /** The box's own route rows as its last availability read saw them — never null, [] when unknown. */
945
+ function _boxRouteRows(instanceId) {
946
+ const block = _modelsBlockFor(instanceId);
947
+ return block && Array.isArray(block.routes) ? block.routes : [];
948
+ }
949
+
950
+ /** §6.4: does the box's last read say it cannot reach this origin? An unprobed origin is NOT a no. */
951
+ function _originUnreachableOn(instanceId, origin) {
952
+ if (!origin) return false;
953
+ return _boxRouteRows(instanceId).some(
954
+ (row) => row && row.origin === origin && (row.reach === 'unreachable' || row.reach === 'no_server'),
955
+ );
956
+ }
957
+
958
+ /** The row's default model string, when the hub's provider list carries one — else ''. */
959
+ function _routeModelOf(row) {
960
+ for (const field of ['model', 'default_model']) {
961
+ if (row && typeof row[field] === 'string' && row[field].trim()) return row[field].trim();
962
+ }
963
+ return '';
964
+ }
965
+
966
+ /** §6.4: the HUB's "what could this box use" list. A read: no step-up ceremony, no local write. */
967
+ async function handleMachineModelsCopyable(req, res, url, params) {
968
+ const instanceId = decodeURIComponent((params && (params['1'] || params.id)) || '');
969
+ const row = _machineRow(instanceId);
970
+ if (!row) return _send(res, 404, { ok: false, error: 'machine_not_found' });
971
+ if (_credentialMissing(instanceId)) return _send(res, 409, { ok: false, error: 'agent_skill_no_credential' });
972
+ // No block: the hub has never read this box's routes (a pre-WP-1 box, or its first tick has not
973
+ // run). It cannot say which of its rows the box already has, so it offers NOTHING rather than a
974
+ // list it cannot qualify — the card shows "not read yet" and no copy button (§6.4).
975
+ if (!_modelsBlockFor(instanceId)) return _send(res, 200, { ok: true, routes: [] });
976
+ let hubRows = null;
977
+ try {
978
+ hubRows = await modelAvailability.readOwnProviderRows({ walleClient: _walle() });
979
+ } catch (error) {
980
+ // HANDLE: the hub cannot read ITS OWN route table — a constant reply, like this module's other
981
+ // unclassified failures (the row's key is not involved in this read at all).
982
+ if (_deps && _deps.errorSink && typeof _deps.errorSink.report === 'function') _deps.errorSink.report('machines:models-copyable', error);
983
+ return _send(res, 502, { ok: false, code: 'walle_unavailable' });
984
+ }
985
+ const boxRoutes = _boxRouteRows(instanceId);
986
+ const onBox = new Set(boxRoutes.map((entry) => entry && entry.id).filter(Boolean));
987
+ const unreachable = new Set(
988
+ boxRoutes
989
+ .filter((entry) => entry && (entry.reach === 'unreachable' || entry.reach === 'no_server'))
990
+ .map((entry) => entry.origin)
991
+ .filter(Boolean),
992
+ );
993
+ const routes = [];
994
+ for (const hubRow of hubRows) {
995
+ const id = String((hubRow && hubRow.id) == null ? '' : hubRow.id);
996
+ if (!id || onBox.has(id)) continue;
997
+ if (hubRow.enabled === 0 || hubRow.enabled === false) continue;
998
+ // The box's raw upsert carries no auth method (it always lands as the default api_key), so a
999
+ // route that signs in any other way would arrive as a DIFFERENT route. Not offered, not copied.
1000
+ if (!_copyableAuthMethod(hubRow.auth_method)) continue;
1001
+ const origin = modelAvailability.rowOrigin(hubRow);
1002
+ // "the box says that ORIGIN is out of reach" — an origin the box has not probed yet is KEPT
1003
+ // (D-9: one probe per origin per 10 min, so "no verdict" is the normal state of a fresh origin).
1004
+ if (origin && unreachable.has(origin)) continue;
1005
+ routes.push({
1006
+ id,
1007
+ type: String((hubRow.type == null) ? '' : hubRow.type),
1008
+ name: String((hubRow.name == null) ? '' : hubRow.name),
1009
+ origin: origin || null,
1010
+ model: _routeModelOf(hubRow),
1011
+ });
1012
+ }
1013
+ return _send(res, 200, { ok: true, routes });
1014
+ }
1015
+
1016
+ /** §6.4/R-5: the ONE credential-carrying read — scoped so the reference dies with the dial. */
1017
+ /** A route a box can recreate: no auth method recorded, or the default `api_key` (case-insensitive). */
1018
+ function _copyableAuthMethod(value) {
1019
+ const method = String(value == null ? '' : value).trim().toLowerCase();
1020
+ return method === '' || method === 'api_key';
1021
+ }
1022
+
1023
+ async function _dialBoxImport(instanceId, routeId) {
1024
+ const exported = await modelAvailability.exportOwnRoute({ walleClient: _walle(), routeId });
1025
+ if (!exported) return { ok: false, status: 0, body: null, missing: true };
1026
+ if (!_copyableAuthMethod(exported.row && exported.row.auth_method)) {
1027
+ return { ok: false, status: 0, body: null, missing: false, unsupported: true };
1028
+ }
1029
+ const answer = await _callBox(instanceId, '/api/models/import', { method: 'POST', body: { row: exported.row } });
1030
+ return { ...(answer || { ok: false, status: 0, body: null }), missing: false };
1031
+ }
1032
+
1033
+ /**
1034
+ * §6.4: the HUB end of "Use on <box>". The order is the remove leg's — 404 machine → 409 credential
1035
+ * → validate the body (400) — then the three refusals that belong to THIS act: no such hub route
1036
+ * (404 route_not_found), a stale client (`origin_changed`), and a box whose own last read says the
1037
+ * route's origin is out of reach (`route_unreachable_on_machine`, dialled nothing at all — the whole
1038
+ * point of checking the cache first is that the user learns the route would be dead on that box
1039
+ * before a credential is moved to it).
1040
+ */
1041
+ async function handleMachineModelsCopy(req, res, url, params) {
1042
+ const instanceId = decodeURIComponent((params && (params['1'] || params.id)) || '');
1043
+ const row = _machineRow(instanceId);
1044
+ if (!row) return _send(res, 404, { ok: false, error: 'machine_not_found' });
1045
+ if (_credentialMissing(instanceId)) return _send(res, 409, { ok: false, error: 'agent_skill_no_credential' });
1046
+ const fields = await _readObjectBody(req);
1047
+ if (fields === null) return _send(res, 400, { ok: false, code: 'invalid_request', message: 'the request body must be JSON' });
1048
+ const normalized = modelAvailability.normalizeCopyRequest(fields);
1049
+ if (!normalized.ok) return _send(res, 400, { ok: false, code: 'invalid_request', message: normalized.message });
1050
+ let hubRows = null;
1051
+ try {
1052
+ hubRows = await modelAvailability.readOwnProviderRows({ walleClient: _walle() });
1053
+ } catch (error) {
1054
+ if (_deps && _deps.errorSink && typeof _deps.errorSink.report === 'function') _deps.errorSink.report('machines:models-copy', error);
1055
+ return _send(res, 502, { ok: false, code: 'walle_unavailable' });
1056
+ }
1057
+ const hubRow = hubRows.find((entry) => String((entry && entry.id) == null ? '' : entry.id) === normalized.routeId) || null;
1058
+ if (!hubRow) return _send(res, 404, { ok: false, code: 'route_not_found' });
1059
+ const origin = modelAvailability.rowOrigin(hubRow);
1060
+ if (String(origin || '') !== normalized.expectOrigin) return _send(res, 409, { ok: false, code: 'origin_changed' });
1061
+ if (_originUnreachableOn(instanceId, origin)) return _send(res, 409, { ok: false, code: 'route_unreachable_on_machine' });
1062
+ const answer = await _dialBoxImport(instanceId, normalized.routeId);
1063
+ if (answer.missing) return _send(res, 404, { ok: false, code: 'route_not_found' });
1064
+ if (answer.unsupported) return _send(res, 409, { ok: false, code: 'auth_method_not_supported' });
1065
+ if (!answer.status) return _machineUnreachable(res, instanceId, row.displayName);
1066
+ const body = answer.body && typeof answer.body === 'object' ? answer.body : null;
1067
+ // An answer with no JSON object is not an answer this client can parse — the same refusal, one
1068
+ // status up, never a 2xx carrying nothing.
1069
+ if (!body) return _machineUnreachable(res, instanceId, row.displayName, 502);
1070
+ if (answer.status >= 200 && answer.status < 300) {
1071
+ // REPORT-free by design (as in the remove leg): a cache that cannot be updated is not a failure
1072
+ // of the box's own answer. The box now has one MORE row, so the stamp is zeroed — the block
1073
+ // stays readable until the next tick re-reads the box's truth.
1074
+ try {
1075
+ require('../machine-registry').markMachineModelsStale(instanceId);
1076
+ } catch { /* expected: an unwired/legacy registry — the box's answer still stands */ }
1077
+ }
1078
+ return _send(res, answer.status, body);
1079
+ }
1080
+
1081
+ /** §6.4: the BOX end — validate the row here, then write it to THIS machine's own Wall-E. */
1082
+ async function handleModelsImport(req, res) {
1083
+ const fields = await _readObjectBody(req);
1084
+ if (fields === null) return _send(res, 400, { ok: false, code: 'invalid_request', message: 'the request body must be JSON' });
1085
+ const normalized = modelAvailability.normalizeImportRow(fields.row);
1086
+ if (!normalized.ok) return _send(res, 400, { ok: false, code: 'invalid_request', message: normalized.message });
1087
+ try {
1088
+ const result = await modelAvailability.importRoute({ walleClient: _walle(), row: normalized.row });
1089
+ return _send(res, 200, { ok: true, routeId: result.routeId });
1090
+ } catch (error) {
1091
+ if (error && error.code === 'walle_unavailable') return _send(res, 502, { ok: false, code: 'walle_unavailable' });
1092
+ // REPORT + HANDLE: a constant reply, like the rest of this module's failures — the message of an
1093
+ // unclassified fault is never echoed back, and the row (which carries a key) is never part of it.
1094
+ if (_deps && _deps.errorSink && typeof _deps.errorSink.report === 'function') _deps.errorSink.report('machines:models-import', error);
1095
+ return _send(res, 500, { ok: false, code: 'import_failed' });
1096
+ }
1097
+ }
1098
+
444
1099
  function register() {
445
1100
  registerApiRoutes('machines', [
446
1101
  { method: 'GET', path: '/api/machines', handler: (req, res) => handleMachinesList(req, res) },
447
1102
  { method: 'POST', path: '/api/machines', handler: (req, res) => handleMachinesCreate(req, res) },
448
1103
  { method: 'POST', path: '/api/machines/pair', handler: (req, res) => handleMachinesPair(req, res) },
449
1104
  { method: 'POST', regex: /^\/api\/machines\/([^/]+)\/remove$/, handler: (req, res, url, params) => handleMachineRemove(req, res, url, params) },
1105
+ // WP-8 (§6.5): the one-skill exchange. The regexes are byte-identical to lib/auth-rules.js's rows
1106
+ // on purpose — routing and auth must agree on what "this route" is, or a malformed id would be
1107
+ // answered by the auth wall with a different code than the handler's own 404.
1108
+ { method: 'POST', regex: /^\/api\/machines\/([^/]+)\/skills\/import$/, handler: (req, res, url, params) => handleMachineSkillsImport(req, res, url, params) },
1109
+ { method: 'POST', regex: /^\/api\/machines\/([^/]+)\/skills\/retire$/, handler: (req, res, url, params) => handleMachineSkillsRetire(req, res, url, params) },
1110
+ // WP-9 (§9 S-9): the diff source — a READ, so it is the one skills row that is neither a step-up
1111
+ // nor a mutation (lib/auth-rules.js carries the matching row, byte-identical regex).
1112
+ { method: 'GET', regex: /^\/api\/machines\/([^/]+)\/skills\/preview$/, handler: (req, res, url, params) => handleMachineSkillsPreview(req, res, url, params) },
1113
+ { method: 'PUT', regex: /^\/api\/machines\/([^/]+)\/skills\/exclude-plugins$/, handler: (req, res, url, params) => handleMachineSkillsExcludePlugins(req, res, url, params) },
1114
+ // WP-1 (§6.1): the BOX side of the model-availability read — registered beside the machine-self
1115
+ // and agent-skills peer reads because it is the same kind of surface (a route the paired hub
1116
+ // dials with the peer bearer, lib/auth-rules.js carrying the matching peerControl row).
1117
+ { method: 'GET', path: '/api/models/availability', handler: (req, res, url) => handleModelsAvailability(req, res, url) },
1118
+ // WP-4 (§6.4): the BOX end of "Remove from <box>" — this machine's own rows, removed through its
1119
+ // own Wall-E. Auth is the peer-control mutation row in lib/auth-rules.js (the paired hub's bearer).
1120
+ { method: 'POST', path: '/api/models/remove', handler: (req, res, url) => handleModelsRemove(req, res, url) },
1121
+ // WP-5 (§6.4, R-5/R-6): the BOX end of "Use on <box>" — this machine writes ONE route the hub
1122
+ // exported into its own Wall-E. Auth is the peer-control mutation row in lib/auth-rules.js.
1123
+ { method: 'POST', path: '/api/models/import', handler: (req, res, url) => handleModelsImport(req, res, url) },
1124
+ // §6.4: the HUB ends — dial the box above with this hub's credential for it. Same regex spelling
1125
+ // as the skills rows: routing and auth must agree on what "this route" is.
1126
+ { method: 'POST', regex: /^\/api\/machines\/([^/]+)\/models\/remove$/, handler: (req, res, url, params) => handleMachineModelsRemove(req, res, url, params) },
1127
+ { method: 'GET', regex: /^\/api\/machines\/([^/]+)\/models\/copyable$/, handler: (req, res, url, params) => handleMachineModelsCopyable(req, res, url, params) },
1128
+ { method: 'POST', regex: /^\/api\/machines\/([^/]+)\/models\/copy$/, handler: (req, res, url, params) => handleMachineModelsCopy(req, res, url, params) },
450
1129
  ]);
451
1130
  }
452
1131
 
@@ -458,4 +1137,14 @@ module.exports = {
458
1137
  handleMachinesCreate,
459
1138
  handleMachinesPair,
460
1139
  handleMachineRemove,
1140
+ handleMachineSkillsImport,
1141
+ handleMachineSkillsRetire,
1142
+ handleMachineSkillsPreview,
1143
+ handleMachineSkillsExcludePlugins,
1144
+ handleModelsAvailability,
1145
+ handleModelsRemove,
1146
+ handleMachineModelsRemove,
1147
+ handleModelsImport,
1148
+ handleMachineModelsCopyable,
1149
+ handleMachineModelsCopy,
461
1150
  };