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
@@ -0,0 +1,724 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * WP-8 (docs/remote-agent-skills-bundles-2026-10-25.html §5 D-11/D-12/D-13, §6.2, §6.5, the WP-8 row
5
+ * AC-8a..AC-8d, P-5 "never overwrite the other side silently"): the ONE-SKILL transfers, and the
6
+ * per-machine plugin exclusion list.
7
+ *
8
+ * WP-7 answers "what does the box have that the hub does not?" and classifies it; this module is what
9
+ * happens when a human acts on that answer. Two directions, one file, because they are one exchange:
10
+ *
11
+ * BOX `exportBoxSkill({ homeDir, env, agent, name })` the bytes of ONE item folder, hashed with
12
+ * build.js's own walk so the hash IS a hub item hash. `retireBoxSkill({ …, expectHash })`
13
+ * MOVES that folder under `<data dir>/agent-bundles/retired/<agent>/<name>-<ts>/` — D-13's
14
+ * "moves, never deletes" — after re-hashing it and refusing a copy that changed since review.
15
+ * HUB `importBoxSkillToHub({ … })` fetches that export, verifies every byte, and writes the folder
16
+ * into the hub's OWN skill root via stage-inside-root + rename (D-12), backing up any copy that
17
+ * was already there FIRST (P-5), then asks the box to retire its shadowing copy.
18
+ *
19
+ * The two rules that shape every line here:
20
+ * P-5 the hub NEVER writes over a folder it did not just verify a backup of, and the box NEVER
21
+ * deletes. A failure anywhere leaves both sides exactly as they were.
22
+ * Identity is a NAME, held to the store's own id shape. A name that could escape a root (a slash, a
23
+ * `..`, a NUL) is refused before a path is ever built from it, on both sides.
24
+ *
25
+ * Everything filesystem-touching takes an injectable `homeDir` / `env` / `dataDir` / `now`, so a test
26
+ * never reads this machine's real `~/.claude`, `~/.agents` or CTM data dir, and never sleeps.
27
+ */
28
+
29
+ const crypto = require('crypto');
30
+ const fsp = require('fs/promises');
31
+ const os = require('os');
32
+ const path = require('path');
33
+
34
+ const { atomicWriteFile } = require('../../atomic-write');
35
+ const {
36
+ computeTreeHash, itemHashOfDir, walkItem, listClaudeSkillItems, listCodexSkillItems,
37
+ } = require('./build');
38
+ const inventory = require('./inventory');
39
+ const store = require('./store');
40
+ const codexInstall = require('./codex-install');
41
+ const installer = require('../coding-cli-skill-installer');
42
+
43
+ const { MANAGED_SKILLS } = installer;
44
+ const { readMarker } = installer._internals;
45
+
46
+ /** store.js's ID_RE — a name that is also a directory name, and nothing else. */
47
+ const NAME_RE = /^[A-Za-z0-9._-]{1,128}$/;
48
+ // WP-12 (AC-12c): `dsh` (the DeepSeek Harness) is a transfer target like Claude/Codex; its hub root is
49
+ // $DSH_HOME/skills (default ~/.dsh/skills) and its box roots are $DSH_HOME/skills plus ~/.agents/skills.
50
+ const AGENTS = Object.freeze(['claude', 'codex', 'dsh']);
51
+ /** §6.2: the whole export must fit in one response body. */
52
+ const MAX_EXPORT_BYTES = 25 * 1024 * 1024;
53
+
54
+ /**
55
+ * WP-9 (§9 S-9, AC-9b): the DIFF SOURCE. A browser cannot call the box's own
56
+ * `GET /api/agent-skills/export` (peer-bearer auth), so the hub relays one preview per side.
57
+ *
58
+ * The two caps are different questions. TEXT is per FILE: past 256 KiB a file is reported as binary
59
+ * (the dialog never renders a wall of diff lines), and the SAME rule covers a file that is not valid
60
+ * UTF-8 — `text: null, binary: true` is one answer for "not lines we can diff". TOTAL is per
61
+ * RESPONSE: the hub refuses (413) rather than streaming megabytes through one JSON body.
62
+ */
63
+ const MAX_PREVIEW_TEXT_BYTES = 256 * 1024;
64
+ const MAX_PREVIEW_BYTES = 2 * 1024 * 1024;
65
+ /** codex-install.js's marker `source` (an ON-DISK value, restated as data — see inventory.js). */
66
+ const AGENT_BUNDLE_SOURCE = 'agent-bundle';
67
+
68
+ /** The per-machine plugin exclusion store (§6.5). A DB column is out of scope for this WP: see below. */
69
+ const EXCLUDE_FILE = 'agent-bundle-exclude-plugins.json';
70
+ const EXCLUDE_SCHEMA = 1;
71
+ const MAX_EXCLUDE_ENTRIES = 200;
72
+ const MAX_EXCLUDE_ID_CHARS = 200;
73
+
74
+ /**
75
+ * One typed failure, carrying the wire status. Both route modules map it by `status`/`code` alone, so
76
+ * the box's answer and the hub's relay of it are the same shape for the same fault.
77
+ */
78
+ class AgentSkillError extends Error {
79
+ constructor(code, status, detail) {
80
+ super(code);
81
+ this.name = 'AgentSkillError';
82
+ this.code = code;
83
+ this.status = Number(status) || 500;
84
+ if (detail != null) this.detail = String(detail).slice(0, 200);
85
+ }
86
+ }
87
+
88
+ function sha256Hex(buffer) {
89
+ return crypto.createHash('sha256').update(buffer).digest('hex');
90
+ }
91
+
92
+ function _safeDetail(value) {
93
+ const text = value instanceof Error
94
+ ? String(value.message || value.name || 'error')
95
+ : String(value == null ? '' : value);
96
+ // Defensive: a transport message is caller-supplied text and the only secret on this path is the
97
+ // peer bearer, so redact the shape a bearer takes before it can escape into a response.
98
+ return text.replace(/bearer\s+[^\s,;]+/gi, 'bearer <redacted>').replace(/[\r\n\t]+/g, ' ').trim().slice(0, 200);
99
+ }
100
+
101
+ function _resolvedHome(homeDir) {
102
+ return String(homeDir == null ? '' : homeDir).replace(/\/+$/, '') || '/';
103
+ }
104
+
105
+ function _string(value, max = 1000) {
106
+ return String(value == null ? '' : value).trim().slice(0, max);
107
+ }
108
+
109
+ /** `20261026T120000000Z` — sortable, path-safe, and derived from an INJECTED clock. */
110
+ function _compactTs(ms) {
111
+ return new Date(Number(ms) || Date.now()).toISOString().replace(/[-:.]/g, '');
112
+ }
113
+
114
+ async function _statOrNull(target) {
115
+ try {
116
+ return await fsp.stat(target);
117
+ } catch (error) {
118
+ if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) return null;
119
+ throw error; // RETHROW: a real stat fault is the caller's to REPORT
120
+ }
121
+ }
122
+
123
+ async function _realOrPlain(target) {
124
+ try {
125
+ return await fsp.realpath(target);
126
+ } catch (error) {
127
+ if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) return target;
128
+ throw error; // RETHROW: a real resolution fault is the caller's to REPORT
129
+ }
130
+ }
131
+
132
+ /**
133
+ * The two ref fields, validated. `agent` is one of exactly two CLIs and `name` is a directory name
134
+ * (never a path): everything downstream builds a path out of `name`, so this is the one gate.
135
+ */
136
+ function validateSkillRef(agent, name) {
137
+ const resolvedAgent = _string(agent, 40);
138
+ const resolvedName = String(name == null ? '' : name).trim();
139
+ if (!AGENTS.includes(resolvedAgent) || !NAME_RE.test(resolvedName)) {
140
+ throw new AgentSkillError('agent_skill_invalid', 422);
141
+ }
142
+ // NAME_RE alone still admits the two directory self-names, and every path below is built by joining
143
+ // this name onto a root — so `.`/`..` would escape the root rather than name an item (`readdir` never
144
+ // returns either, so no real item is refused by this).
145
+ if (resolvedName === '.' || resolvedName === '..') throw new AgentSkillError('agent_skill_invalid', 422);
146
+ return { agent: resolvedAgent, name: resolvedName };
147
+ }
148
+
149
+ /* ------------------------------------------------------------------------------------------------
150
+ * BOX side — locate / export / retire
151
+ * ---------------------------------------------------------------------------------------------- */
152
+
153
+ /**
154
+ * The item folder for `(agent, name)` on THIS machine, or null. Deliberately the SAME enumeration
155
+ * `readBoxInventory` runs (build.js's listers, in build.js's order), so "the same skill" means the
156
+ * same folder for the inventory, the export and the retire — including Codex's first-root-wins rule.
157
+ */
158
+ async function _locateBoxItem(agent, name, homeDir, env) {
159
+ const resolvedHome = _resolvedHome(homeDir);
160
+ const items = agent === 'claude'
161
+ ? await listClaudeSkillItems(inventory.claudeConfigDir(resolvedHome, env))
162
+ : agent === 'dsh'
163
+ // WP-12: dsh's box roots are <home>/.agents/skills AND $DSH_HOME/skills (default ~/.dsh/skills) --
164
+ // the same pair the codex lister walks, with $DSH_HOME standing in for $CODEX_HOME, so a copy
165
+ // under either dsh root is exportable.
166
+ ? await listCodexSkillItems(resolvedHome, { ...env, CODEX_HOME: env.DSH_HOME || path.join(resolvedHome, '.dsh') })
167
+ : await listCodexSkillItems(resolvedHome, env);
168
+ return items.find((item) => item.name === name) || null;
169
+ }
170
+
171
+ /**
172
+ * §6.2 `GET /api/agent-skills/export`: the bytes of ONE box-owned skill.
173
+ *
174
+ * `managed: 'ctm-managed'` is a 404 (`agent_skill_not_found`) rather than a 409: this release's own
175
+ * `delegate` copy is simply not an item that can be proposed for import, and saying "not found" keeps
176
+ * the hub from ever rendering it as one.
177
+ *
178
+ * @returns {Promise<{agent: string, name: string, itemHash: string, files: Array<object>}>}
179
+ */
180
+ async function exportBoxSkill({ homeDir = os.homedir(), env = process.env, agent, name } = {}) {
181
+ const ref = validateSkillRef(agent, name);
182
+ const item = await _locateBoxItem(ref.agent, ref.name, homeDir, env);
183
+ if (!item) throw new AgentSkillError('agent_skill_not_found', 404);
184
+ if (await inventory.managedStateOf(item) === 'ctm-managed') {
185
+ throw new AgentSkillError('agent_skill_not_found', 404);
186
+ }
187
+ const realRoot = await _realOrPlain(item.rootDir);
188
+ const walked = await walkItem(realRoot);
189
+ if (walked.error) throw new AgentSkillError('agent_skill_not_found', 404);
190
+ const files = walked.files;
191
+ const total = files.reduce((sum, file) => sum + file.size, 0);
192
+ if (total > MAX_EXPORT_BYTES) throw new AgentSkillError('agent_skill_too_large', 413);
193
+
194
+ const out = [];
195
+ for (const file of files) {
196
+ const buffer = await fsp.readFile(file.abs);
197
+ out.push({
198
+ path: file.rel,
199
+ mode: file.mode,
200
+ size: buffer.length,
201
+ sha256: sha256Hex(buffer),
202
+ contentBase64: buffer.toString('base64'),
203
+ });
204
+ }
205
+ // The item hash is the canonical entry list build.js hashes — computed from the SAME bytes this
206
+ // response carries, so the hub's recomputation can only disagree if a byte changed in flight.
207
+ return { agent: ref.agent, name: ref.name, itemHash: computeTreeHash(out.map(_entryOf)), files: out };
208
+ }
209
+
210
+ /** The `{path, mode, sha256, size}` projection computeTreeHash folds (one shape, one place). */
211
+ function _entryOf(file) {
212
+ return { path: file.path, mode: file.mode, sha256: file.sha256, size: file.size };
213
+ }
214
+
215
+ /** rename, or (across filesystems) copy-then-remove. NEVER a plain delete of the source. */
216
+ async function _moveDir(src, dest) {
217
+ try {
218
+ await fsp.rename(src, dest);
219
+ return;
220
+ } catch (error) {
221
+ if (!error || error.code !== 'EXDEV') throw error; // RETHROW: only EXDEV has a fallback
222
+ }
223
+ await fsp.cp(src, dest, { recursive: true, force: true, verbatimSymlinks: true });
224
+ await fsp.rm(src, { recursive: true, force: true });
225
+ }
226
+
227
+ /**
228
+ * §6.2/§6.5 `POST /api/agent-skills/retire` — D-13's "moves, never deletes".
229
+ *
230
+ * The copy is re-hashed NOW and must still be the one the user reviewed (`expectHash`); a managed
231
+ * (`ctm-managed`) copy is refused outright. The folder is then MOVED under
232
+ * `<data dir>/agent-bundles/retired/<agent>/<name>-<compactTs>/` — never removed — so the box user can
233
+ * always recover it by hand.
234
+ *
235
+ * A folder carrying an `agent-bundle` marker (a WP-6 Codex install the box then edited) ALSO drops its
236
+ * root-record entry in `<homeDir>/.agents/skills`: without that, the hub's next push would read the
237
+ * missing target as a USER DELETION (`removed`) and never re-install the bundle copy, which is exactly
238
+ * the shadowing D-13 exists to end.
239
+ *
240
+ * @returns {Promise<{retiredTo: string}>}
241
+ */
242
+ async function retireBoxSkill({
243
+ homeDir = os.homedir(), env = process.env, dataDir = null, agent, name, expectHash = null, now = Date.now,
244
+ } = {}) {
245
+ const ref = validateSkillRef(agent, name);
246
+ if (!dataDir) throw new Error('retireBoxSkill: a data dir is required');
247
+ const item = await _locateBoxItem(ref.agent, ref.name, homeDir, env);
248
+ if (!item) throw new AgentSkillError('agent_skill_not_found', 404);
249
+ if (await inventory.managedStateOf(item) === 'ctm-managed') {
250
+ throw new AgentSkillError('agent_skill_managed', 409);
251
+ }
252
+ const itemHash = await itemHashOfDir(item.rootDir);
253
+ if (itemHash === null) throw new AgentSkillError('agent_skill_not_found', 404);
254
+ const expected = String(expectHash == null ? '' : expectHash).trim();
255
+ if (!expected || itemHash !== expected) {
256
+ throw new AgentSkillError('agent_skill_changed_since_review', 409);
257
+ }
258
+ // Read the marker BEFORE the move: after it, the folder is at the retired path.
259
+ const marker = await readMarker(item.rootDir, ref.name);
260
+ const retiredTo = path.join(store.storeRoot(dataDir), 'retired', ref.agent, `${ref.name}-${_compactTs(now)}`);
261
+ await fsp.mkdir(path.dirname(retiredTo), { recursive: true });
262
+ await _moveDir(item.rootDir, retiredTo);
263
+ if (marker && marker.source === AGENT_BUNDLE_SOURCE) {
264
+ await codexInstall.dropRecordEntry(codexInstall.skillsRoot(_resolvedHome(homeDir)), ref.name);
265
+ }
266
+ return { retiredTo };
267
+ }
268
+
269
+ /* ------------------------------------------------------------------------------------------------
270
+ * HUB side — the hub's own skill root, the export verification, the backup + stage + rename write
271
+ * ---------------------------------------------------------------------------------------------- */
272
+
273
+ /** The hub's skill root for one agent: `<CLAUDE_CONFIG_DIR|~/.claude>/skills`, `~/.agents/skills`, or
274
+ * (WP-12) `$DSH_HOME/skills` (default `~/.dsh/skills`) for dsh. */
275
+ async function resolveHubSkillsRoot(agent, homeDir = os.homedir(), env = process.env) {
276
+ const resolvedHome = _resolvedHome(homeDir);
277
+ const plain = agent === 'claude'
278
+ ? path.join(inventory.claudeConfigDir(resolvedHome, env), 'skills')
279
+ : agent === 'dsh'
280
+ ? path.join(env.DSH_HOME ? _resolvedHome(env.DSH_HOME) : path.join(resolvedHome, '.dsh'), 'skills')
281
+ : path.join(resolvedHome, '.agents', 'skills');
282
+ // AC-8a: a Dropbox/`~/.claude`-symlinked root is the observed real shape, so the write target is the
283
+ // RESOLVED directory — writing through the symlink would still work, but the backup and the replace
284
+ // would then straddle two paths for one folder.
285
+ return _realOrPlain(plain);
286
+ }
287
+
288
+ /**
289
+ * §6.2/§6.5 step 2: every file's `sha256` is recomputed from its decoded bytes, and the item hash is
290
+ * recomputed from those bytes — an export that disagrees with ITSELF (`agent_skill_export_corrupt`,
291
+ * 502) is a different fault from an export that disagrees with what the user reviewed
292
+ * (`agent_skill_changed_since_review`, 409, AC-8c).
293
+ *
294
+ * @returns {Array<{path: string, mode: number, sha256: string, size: number, contentBase64: string}>}
295
+ */
296
+ function verifyExport(body, agent, name) {
297
+ const corrupt = () => new AgentSkillError('agent_skill_export_corrupt', 502);
298
+ if (!body || typeof body !== 'object' || Array.isArray(body)) throw corrupt();
299
+ if (String(body.agent) !== agent || String(body.name) !== name) throw corrupt();
300
+ if (!Array.isArray(body.files) || typeof body.itemHash !== 'string') throw corrupt();
301
+ const files = [];
302
+ for (const raw of body.files) {
303
+ if (!raw || typeof raw !== 'object') throw corrupt();
304
+ const rel = String(raw.path == null ? '' : raw.path);
305
+ if (!rel || rel.startsWith('/') || rel.includes('\\') || rel.split('/').some((part) => !part || part === '.' || part === '..')) {
306
+ throw corrupt();
307
+ }
308
+ const sha = String(raw.sha256 == null ? '' : raw.sha256);
309
+ if (!/^[0-9a-f]{64}$/.test(sha)) throw corrupt();
310
+ const mode = Number(raw.mode);
311
+ if (mode !== 0o444 && mode !== 0o555) throw corrupt();
312
+ const size = Number(raw.size);
313
+ if (!Number.isInteger(size) || size < 0) throw corrupt();
314
+ const contentBase64 = String(raw.contentBase64 == null ? '' : raw.contentBase64);
315
+ const buffer = Buffer.from(contentBase64, 'base64');
316
+ if (buffer.length !== size || sha256Hex(buffer) !== sha) throw corrupt();
317
+ files.push({ path: rel, mode, sha256: sha, size, contentBase64 });
318
+ }
319
+ if (_itemHashOfFiles(files) !== body.itemHash) throw corrupt();
320
+ return files;
321
+ }
322
+
323
+ /** The item hash of a verified file list — build.js's canonical list, byte for byte. */
324
+ function _itemHashOfFiles(files) {
325
+ return computeTreeHash(files.map(_entryOf));
326
+ }
327
+
328
+ /* ------------------------------------------------------------------------------------------------
329
+ * WP-9 PREVIEW (§9 S-9, AC-9b) — the read-only diff source both sides of the Review dialog use
330
+ * ---------------------------------------------------------------------------------------------- */
331
+
332
+ /**
333
+ * One preview entry from the bytes on disk. `text` is the file's lines only when the bytes are valid
334
+ * UTF-8 AND at most MAX_PREVIEW_TEXT_BYTES; everything else is `{text: null, binary: true}` with the
335
+ * real size, so the dialog can say "binary file, <size>" without ever guessing.
336
+ *
337
+ * The validity test is the round trip plus the encoded form's NUL scan: a decoder that substituted
338
+ * U+FFFD lost information (the file is not UTF-8 text), and a NUL is the classic payload signature.
339
+ */
340
+ function _previewEntryOf(filePath, buffer) {
341
+ const size = buffer.length;
342
+ const binary = size > MAX_PREVIEW_TEXT_BYTES || buffer.includes(0);
343
+ let text = null;
344
+ if (!binary) {
345
+ const decoded = buffer.toString('utf8');
346
+ text = (decoded.includes('�') || Buffer.byteLength(decoded, 'utf8') !== size) ? null : decoded;
347
+ }
348
+ return { path: filePath, text: text === null ? null : text, binary: text === null, size };
349
+ }
350
+
351
+ /**
352
+ * The HUB's own copy of `(agent, name)` — the folder `_writeIntoHubRoot` writes to, read with async
353
+ * fs. `null` when the hub holds no such folder (the "new on <machine>" case: there is nothing to diff
354
+ * against). An unreadable folder is also `null`: a preview is a read, never a failure the user acts on.
355
+ *
356
+ * @returns {Promise<{files: Array<{path: string, text: string|null, binary: boolean, size: number}>}|null>}
357
+ */
358
+ async function readHubSkillFiles({ agent, name, hubHomeDir = os.homedir(), hubEnv = process.env } = {}) {
359
+ const ref = validateSkillRef(agent, name);
360
+ const root = await resolveHubSkillsRoot(ref.agent, hubHomeDir, hubEnv);
361
+ const dir = path.join(root, ref.name);
362
+ const stat = await _statOrNull(dir);
363
+ if (!stat || !stat.isDirectory()) return null;
364
+ const walked = await walkItem(await _realOrPlain(dir));
365
+ if (walked.error) return null;
366
+ const files = [];
367
+ for (const file of walked.files) {
368
+ let buffer;
369
+ try {
370
+ buffer = await fsp.readFile(file.abs);
371
+ } catch {
372
+ // HANDLE: one unreadable file is reported as a binary entry — the preview still shows the rest.
373
+ files.push({ path: file.rel, text: null, binary: true, size: Number(file.size) || 0 });
374
+ continue;
375
+ }
376
+ files.push(_previewEntryOf(file.rel, buffer));
377
+ }
378
+ return { files };
379
+ }
380
+
381
+ /**
382
+ * WP-9 (§9 S-9, AC-9b/AC-9c): `GET /api/machines/:id/skills/preview?agent=&name=`.
383
+ *
384
+ * The box's half rides the SAME fetch + verification as an import (validateSkillRef, `verifyExport`,
385
+ * the item hash recomputed from the bytes) — so a preview can never show a folder the import would
386
+ * then refuse. The hub's half is its own copy, or null. NOTHING is written on either side, and no
387
+ * backup/retire leg runs: this is the read the Review dialog renders diffs from.
388
+ *
389
+ * @returns {Promise<{agent: string, name: string, boxHash: string,
390
+ * box: {files: Array<object>}, hub: {files: Array<object>}|null}>}
391
+ */
392
+ async function previewBoxSkill({ agent, name, hubHomeDir = os.homedir(), hubEnv = process.env, fetchExport } = {}) {
393
+ const ref = validateSkillRef(agent, name);
394
+ if (typeof fetchExport !== 'function') throw new AgentSkillError('agent_skill_box_unreachable', 502);
395
+ let response;
396
+ try {
397
+ response = await fetchExport({ agent: ref.agent, name: ref.name });
398
+ } catch (error) {
399
+ throw new AgentSkillError('agent_skill_box_unreachable', 502, _safeDetail(error));
400
+ }
401
+ const status = Number(response && response.status);
402
+ if (!Number.isFinite(status) || status === 0) throw new AgentSkillError('agent_skill_box_unreachable', 502, 'the box did not answer');
403
+ if (status === 404) throw new AgentSkillError('agent_skill_not_found', 404);
404
+ if (status !== 200 || !response.body) {
405
+ throw new AgentSkillError('agent_skill_box_unreachable', 502, `the box answered ${status}`);
406
+ }
407
+
408
+ // Same verification the import runs, so "what the dialog showed" and "what an import would write"
409
+ // are the same bytes (and the same hash the Import button sends back as `expectBoxHash`).
410
+ const files = verifyExport(response.body, ref.agent, ref.name);
411
+ const boxHash = String(response.body.itemHash);
412
+ const box = {
413
+ files: files.map((file) => _previewEntryOf(file.path, Buffer.from(file.contentBase64, 'base64'))),
414
+ };
415
+ const hub = await readHubSkillFiles({
416
+ agent: ref.agent, name: ref.name, hubHomeDir, hubEnv,
417
+ });
418
+
419
+ const totalBytes = box.files.reduce((sum, file) => sum + file.size, 0)
420
+ + (hub ? hub.files.reduce((sum, file) => sum + file.size, 0) : 0);
421
+ if (totalBytes > MAX_PREVIEW_BYTES) throw new AgentSkillError('agent_skill_preview_too_large', 413);
422
+
423
+ return { agent: ref.agent, name: ref.name, boxHash, box, hub };
424
+ }
425
+
426
+ /** Recursive copy that preserves the exec bit and reproduces neither the tree's nor the source's
427
+ * read-only dir modes (the hub's skill root is user-editable, P-5's whole point). */
428
+ async function _copyDirTree(srcDir, destDir) {
429
+ await fsp.mkdir(destDir, { recursive: true });
430
+ await fsp.chmod(destDir, 0o755);
431
+ for (const dirent of await fsp.readdir(srcDir, { withFileTypes: true })) {
432
+ const src = path.join(srcDir, dirent.name);
433
+ const dest = path.join(destDir, dirent.name);
434
+ if (dirent.isDirectory()) {
435
+ await _copyDirTree(src, dest);
436
+ continue;
437
+ }
438
+ if (!dirent.isFile()) continue; // symlinks are never followed out of the folder
439
+ const stat = await fsp.lstat(src);
440
+ await fsp.copyFile(src, dest);
441
+ await fsp.chmod(dest, (stat.mode & 0o111) !== 0 ? 0o755 : 0o644);
442
+ }
443
+ }
444
+
445
+ /** Write the verified files into `destDir` (staging): files 0644, exec bit preserved as 0755, dirs
446
+ * 0755 — the user edits these copies, so no 0444 can survive the import. */
447
+ async function _writeFiles(destDir, files) {
448
+ await fsp.mkdir(destDir, { recursive: true });
449
+ await fsp.chmod(destDir, 0o755);
450
+ const dirs = new Set([destDir]);
451
+ for (const file of files) {
452
+ const target = path.join(destDir, ...file.path.split('/'));
453
+ const dir = path.dirname(target);
454
+ await fsp.mkdir(dir, { recursive: true });
455
+ dirs.add(dir);
456
+ await fsp.writeFile(target, Buffer.from(file.contentBase64, 'base64'));
457
+ await fsp.chmod(target, (file.mode & 0o111) !== 0 ? 0o755 : 0o644);
458
+ }
459
+ for (const dir of dirs) await fsp.chmod(dir, 0o755);
460
+ }
461
+
462
+ /**
463
+ * D-12: back up any existing hub copy FIRST, then stage inside the root and rename.
464
+ *
465
+ * ORDER IS LOAD-BEARING (P-5): the backup is written and VERIFIED (same item hash as the folder it
466
+ * came from) before a single byte of the live copy is touched, so the only way to lose the old copy is
467
+ * to fail while the new one is already in place — and even then the replaced folder is still on disk
468
+ * until the swap is complete.
469
+ *
470
+ * @returns {Promise<{importedTo: string, backupDir: string|null}>}
471
+ */
472
+ async function _writeIntoHubRoot({ agent, name, files, dataDir, hubHomeDir, hubEnv, now }) {
473
+ const root = await resolveHubSkillsRoot(agent, hubHomeDir, hubEnv);
474
+ await fsp.mkdir(root, { recursive: true });
475
+ const targetDir = path.join(root, name);
476
+ const existing = await _statOrNull(targetDir);
477
+
478
+ let backupDir = null;
479
+ if (existing && existing.isDirectory()) {
480
+ backupDir = path.join(String(dataDir), 'skill-import-backups', agent, `${name}-${_compactTs(now)}`);
481
+ await fsp.mkdir(path.dirname(backupDir), { recursive: true });
482
+ await _copyDirTree(targetDir, backupDir);
483
+ const [beforeHash, backupHash] = await Promise.all([itemHashOfDir(targetDir), itemHashOfDir(backupDir)]);
484
+ if (beforeHash === null || beforeHash !== backupHash) {
485
+ throw new AgentSkillError('agent_skill_backup_failed', 500);
486
+ }
487
+ }
488
+
489
+ const rand = crypto.randomBytes(6).toString('hex');
490
+ const staging = path.join(root, `.${name}.ctm-import-${rand}`);
491
+ const replaced = path.join(root, `.${name}.ctm-replaced-${rand}`);
492
+ try {
493
+ await _writeFiles(staging, files);
494
+ let moved = false;
495
+ if (existing) {
496
+ await fsp.rename(targetDir, replaced);
497
+ moved = true;
498
+ }
499
+ try {
500
+ await fsp.rename(staging, targetDir);
501
+ } catch (error) {
502
+ // RETHROW: the swap failed, so put the original back before the staging dir is cleaned up.
503
+ if (moved) await fsp.rename(replaced, targetDir);
504
+ throw error;
505
+ }
506
+ if (moved) await fsp.rm(replaced, { recursive: true, force: true });
507
+ } catch (error) {
508
+ await fsp.rm(staging, { recursive: true, force: true });
509
+ throw error; // RETHROW: the caller maps it; the original folder is still in place
510
+ }
511
+ return { importedTo: targetDir, backupDir };
512
+ }
513
+
514
+ /**
515
+ * §6.5 `POST /api/machines/:instanceId/skills/import`.
516
+ *
517
+ * `fetchExport` / `retire` are the caller's transport (lib/api/machines.js builds them from the peer
518
+ * bearer + the row's dial list), so this module owns the policy and the route owns the socket. A
519
+ * retire failure NEVER undoes the import (the bytes are already the hub's), so the result carries
520
+ * `retired: false` plus the code instead of throwing.
521
+ *
522
+ * @returns {Promise<{importedTo: string, backupDir: string|null, retired: boolean,
523
+ * retiredTo: string|null, retireError: string|null}>}
524
+ */
525
+ async function importBoxSkillToHub({
526
+ agent, name, expectBoxHash = null, dataDir,
527
+ hubHomeDir = os.homedir(), hubEnv = process.env,
528
+ fetchExport, retire = null, now = Date.now,
529
+ } = {}) {
530
+ const ref = validateSkillRef(agent, name);
531
+ if (!dataDir) throw new Error('importBoxSkillToHub: a data dir is required');
532
+ if (Object.hasOwn(MANAGED_SKILLS, ref.name)) {
533
+ throw new AgentSkillError('agent_skill_managed', 409);
534
+ }
535
+
536
+ let response;
537
+ try {
538
+ response = await fetchExport({ agent: ref.agent, name: ref.name });
539
+ } catch (error) {
540
+ throw new AgentSkillError('agent_skill_box_unreachable', 502, _safeDetail(error));
541
+ }
542
+ const status = Number(response && response.status);
543
+ if (!Number.isFinite(status) || status === 0) {
544
+ throw new AgentSkillError('agent_skill_box_unreachable', 502, 'the box did not answer');
545
+ }
546
+ if (status === 404) throw new AgentSkillError('agent_skill_not_found', 404);
547
+ if (status !== 200 || !response.body) {
548
+ throw new AgentSkillError('agent_skill_box_unreachable', 502, `the box answered ${status}`);
549
+ }
550
+
551
+ const files = verifyExport(response.body, ref.agent, ref.name);
552
+ const boxHash = String(response.body.itemHash);
553
+ const expected = String(expectBoxHash == null ? '' : expectBoxHash).trim();
554
+ if (boxHash !== expected) throw new AgentSkillError('agent_skill_changed_since_review', 409);
555
+
556
+ const written = await _writeIntoHubRoot({
557
+ agent: ref.agent, name: ref.name, files, dataDir, hubHomeDir, hubEnv, now,
558
+ });
559
+
560
+ let retired = false;
561
+ let retiredTo = null;
562
+ let retireError = null;
563
+ if (typeof retire === 'function') {
564
+ try {
565
+ const answer = await retire({ agent: ref.agent, name: ref.name, expectHash: boxHash });
566
+ const retireStatus = Number(answer && answer.status);
567
+ if (retireStatus === 200 && answer.body && typeof answer.body.retiredTo === 'string') {
568
+ retired = true;
569
+ retiredTo = answer.body.retiredTo;
570
+ } else {
571
+ retireError = _string(answer && answer.body && answer.body.code, 80)
572
+ || (retireStatus ? `agent_skill_box_http_${retireStatus}` : 'agent_skill_box_unreachable');
573
+ }
574
+ } catch (error) {
575
+ // REPORT: the import already succeeded; a dead box on the retire leg is a partial outcome the
576
+ // caller surfaces, never a rollback of bytes that are already correct.
577
+ retireError = 'agent_skill_box_unreachable';
578
+ }
579
+ } else {
580
+ retireError = 'agent_skill_retire_skipped';
581
+ }
582
+
583
+ return { importedTo: written.importedTo, backupDir: written.backupDir, retired, retiredTo, retireError };
584
+ }
585
+
586
+ /**
587
+ * §6.5 `POST /api/machines/:instanceId/skills/retire` — "use the laptop's copy", or a redundant `same`
588
+ * copy: the box's folder moves aside, and NOTHING is written on the hub. Same error mapping as the
589
+ * import leg, minus the write.
590
+ *
591
+ * @returns {Promise<{retiredTo: string}>}
592
+ */
593
+ async function retireBoxSkillFromHub({ agent, name, expectBoxHash = null, retire } = {}) {
594
+ const ref = validateSkillRef(agent, name);
595
+ if (typeof retire !== 'function') throw new AgentSkillError('agent_skill_box_unreachable', 502);
596
+ let response;
597
+ try {
598
+ response = await retire({ agent: ref.agent, name: ref.name, expectHash: expectBoxHash });
599
+ } catch (error) {
600
+ throw new AgentSkillError('agent_skill_box_unreachable', 502, _safeDetail(error));
601
+ }
602
+ const status = Number(response && response.status);
603
+ const code = _string(response && response.body && response.body.code, 80);
604
+ if (!Number.isFinite(status) || status === 0) throw new AgentSkillError('agent_skill_box_unreachable', 502);
605
+ if (status === 200 && response.body && typeof response.body.retiredTo === 'string') {
606
+ return { retiredTo: response.body.retiredTo };
607
+ }
608
+ if (status === 404) throw new AgentSkillError('agent_skill_not_found', 404);
609
+ if (status === 409) throw new AgentSkillError(code || 'agent_skill_changed_since_review', 409);
610
+ if (status === 422) throw new AgentSkillError(code || 'agent_skill_invalid', 422);
611
+ throw new AgentSkillError('agent_skill_box_unreachable', 502, `the box answered ${status}`);
612
+ }
613
+
614
+ /* ------------------------------------------------------------------------------------------------
615
+ * Per-machine plugin exclusions (§6.5)
616
+ *
617
+ * ONE hub-side JSON file, `<hub data dir>/agent-bundle-exclude-plugins.json`, shaped
618
+ * `{ schema: 1, machines: { <instanceId>: [<plugin id>…] } }`. A DB column is deliberately OUT of
619
+ * scope for this WP (it would mean a migration plus a db.js writer for a list that is read on the
620
+ * create path and written from one settings row); the file is atomic and the read below is cached in
621
+ * memory, re-read on write.
622
+ * ---------------------------------------------------------------------------------------------- */
623
+
624
+ let _excludeCache = null; // { dataDir, machines }
625
+
626
+ function _excludePath(dataDir) {
627
+ return path.join(String(dataDir == null ? '' : dataDir), EXCLUDE_FILE);
628
+ }
629
+
630
+ /**
631
+ * Load the store for `dataDir` into the in-memory cache (async — no sync fs on the main thread). A
632
+ * missing/garbled file is an empty list. Callers that need the list (the Machines list handler, the
633
+ * push path) await this first; `excludePluginsFor` then reads the cache synchronously.
634
+ */
635
+ async function loadExcludePlugins(dataDir) {
636
+ const dir = String(dataDir == null ? '' : dataDir);
637
+ if (_excludeCache && _excludeCache.dataDir === dir) return _excludeCache.machines;
638
+ let machines = {};
639
+ try {
640
+ const parsed = JSON.parse(await fsp.readFile(path.join(dir, EXCLUDE_FILE), 'utf8'));
641
+ if (parsed && typeof parsed === 'object' && parsed.machines && typeof parsed.machines === 'object'
642
+ && !Array.isArray(parsed.machines)) {
643
+ machines = parsed.machines;
644
+ }
645
+ } catch {
646
+ /* expected: no file yet, or a torn copy — both mean "nothing excluded", never a create failure */
647
+ }
648
+ _excludeCache = { dataDir: dir, machines };
649
+ return machines;
650
+ }
651
+
652
+ /** The plugin ids excluded for ONE machine, or `[]`, from the cache `loadExcludePlugins` filled
653
+ * (a store never loaded for this dir reads as nothing excluded). */
654
+ function excludePluginsFor(dataDir, instanceId) {
655
+ const id = _string(instanceId, 160);
656
+ if (!id) return [];
657
+ const dir = String(dataDir == null ? '' : dataDir);
658
+ const machines = _excludeCache && _excludeCache.dataDir === dir ? _excludeCache.machines : {};
659
+ const list = machines[id];
660
+ return Array.isArray(list) ? list.slice() : [];
661
+ }
662
+
663
+ /**
664
+ * §6.5 `PUT /api/machines/:instanceId/skills/exclude-plugins`: replace one machine's list. Ids are
665
+ * strings of at most 200 chars, deduped and sorted; at most 200 of them (the plugin count a hub can
666
+ * plausibly carry, and the bound that keeps the file small).
667
+ *
668
+ * @returns {Promise<string[]>} the stored list
669
+ */
670
+ async function setExcludePlugins(dataDir, instanceId, plugins) {
671
+ const id = _string(instanceId, 160);
672
+ if (!id) throw new AgentSkillError('agent_skill_invalid', 422);
673
+ if (!Array.isArray(plugins)) throw new AgentSkillError('exclude_plugins_invalid', 422);
674
+ const unique = new Set();
675
+ for (const value of plugins) {
676
+ if (typeof value !== 'string') throw new AgentSkillError('exclude_plugins_invalid', 422);
677
+ const pluginId = value.trim();
678
+ if (!pluginId || pluginId.length > MAX_EXCLUDE_ID_CHARS) throw new AgentSkillError('exclude_plugins_invalid', 422);
679
+ unique.add(pluginId);
680
+ }
681
+ if (unique.size > MAX_EXCLUDE_ENTRIES) throw new AgentSkillError('exclude_plugins_invalid', 422);
682
+ const list = [...unique].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
683
+ const dir = String(dataDir == null ? '' : dataDir);
684
+ const machines = { ...(await loadExcludePlugins(dir)), [id]: list };
685
+ const next = { schema: EXCLUDE_SCHEMA, machines };
686
+ await fsp.mkdir(dir, { recursive: true });
687
+ await atomicWriteFile(_excludePath(dir), JSON.stringify(next, null, 2) + '\n');
688
+ // Re-read on write: the cache IS the file's parsed content, so a write refreshes it in place.
689
+ _excludeCache = { dataDir: dir, machines };
690
+ return list;
691
+ }
692
+
693
+ /**
694
+ * Test-only: drop the in-memory cache so each test observes its own file. Module state, like every
695
+ * other cache in this tree.
696
+ */
697
+ function _resetExcludePluginsCacheForTests() {
698
+ _excludeCache = null;
699
+ }
700
+
701
+ module.exports = {
702
+ AgentSkillError,
703
+ // box
704
+ validateSkillRef,
705
+ exportBoxSkill,
706
+ retireBoxSkill,
707
+ // hub
708
+ resolveHubSkillsRoot,
709
+ verifyExport,
710
+ importBoxSkillToHub,
711
+ retireBoxSkillFromHub,
712
+ // WP-9: the review dialog's diff source.
713
+ readHubSkillFiles,
714
+ previewBoxSkill,
715
+ // per-machine plugin exclusions
716
+ loadExcludePlugins,
717
+ excludePluginsFor,
718
+ setExcludePlugins,
719
+ _resetExcludePluginsCacheForTests,
720
+ EXCLUDE_FILE,
721
+ MAX_EXPORT_BYTES,
722
+ MAX_PREVIEW_BYTES,
723
+ MAX_PREVIEW_TEXT_BYTES,
724
+ };