create-walle 0.10.1 → 0.10.2

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 (341) hide show
  1. package/README.md +4 -0
  2. package/lib/owner-write-safety.js +135 -0
  3. package/lib/public-runtime.js +13 -5
  4. package/lib/release-schema.json +3 -2
  5. package/lib/runtime-closure-policy.json +6 -4
  6. package/package.json +1 -1
  7. package/template/bin/deps-doctor.js +1 -1
  8. package/template/bin/node-bin.sh +11 -0
  9. package/template/coding-task-manager/api-prompts.js +10 -22
  10. package/template/coding-task-manager/bin/claude-hook.js +7 -3
  11. package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
  12. package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
  13. package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
  14. package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
  15. package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
  16. package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
  17. package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
  18. package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
  19. package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
  20. package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
  21. package/template/coding-task-manager/db.js +705 -286
  22. package/template/coding-task-manager/git-utils.js +44 -0
  23. package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
  24. package/template/coding-task-manager/lib/agent-presets.js +15 -0
  25. package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
  26. package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
  27. package/template/coding-task-manager/lib/api/host-stats.js +98 -0
  28. package/template/coding-task-manager/lib/api/ingress.js +35 -12
  29. package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
  30. package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
  31. package/template/coding-task-manager/lib/api/machines.js +461 -0
  32. package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
  33. package/template/coding-task-manager/lib/api/providers.js +53 -6
  34. package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
  35. package/template/coding-task-manager/lib/api/sessions.js +160 -8
  36. package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
  37. package/template/coding-task-manager/lib/api/worktrees.js +38 -37
  38. package/template/coding-task-manager/lib/approval-drift.js +16 -1
  39. package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
  40. package/template/coding-task-manager/lib/approval-scope.js +68 -4
  41. package/template/coding-task-manager/lib/auth-context.js +32 -2
  42. package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
  43. package/template/coding-task-manager/lib/auth-rules.js +186 -45
  44. package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
  45. package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
  46. package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
  47. package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
  48. package/template/coding-task-manager/lib/claude-session-title.js +6 -30
  49. package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
  50. package/template/coding-task-manager/lib/client-diag-handler.js +5 -0
  51. package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
  52. package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
  53. package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
  54. package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
  55. package/template/coding-task-manager/lib/codex-paths.js +2 -1
  56. package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
  57. package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
  58. package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
  59. package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
  60. package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
  61. package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
  62. package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
  63. package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
  64. package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
  65. package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
  66. package/template/coding-task-manager/lib/db-domains.js +21 -2
  67. package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
  68. package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
  69. package/template/coding-task-manager/lib/feature-manifest.js +1 -1
  70. package/template/coding-task-manager/lib/fs-watcher.js +3 -2
  71. package/template/coding-task-manager/lib/harness-history-transition.js +3 -3
  72. package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
  73. package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
  74. package/template/coding-task-manager/lib/headless-term-service.js +55 -17
  75. package/template/coding-task-manager/lib/host-control-dial.js +167 -0
  76. package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
  77. package/template/coding-task-manager/lib/host-handoff.js +51 -13
  78. package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
  79. package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
  80. package/template/coding-task-manager/lib/host-stats.js +328 -0
  81. package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
  82. package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
  83. package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
  84. package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
  85. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
  86. package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
  87. package/template/coding-task-manager/lib/instance-manifest.js +13 -1
  88. package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
  89. package/template/coding-task-manager/lib/launch-model-override.js +160 -1
  90. package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
  91. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
  92. package/template/coding-task-manager/lib/lead-model-switch.js +3 -2
  93. package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
  94. package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
  95. package/template/coding-task-manager/lib/login-tripwire.js +1 -0
  96. package/template/coding-task-manager/lib/machine-pairing.js +238 -0
  97. package/template/coding-task-manager/lib/machine-pool.js +409 -0
  98. package/template/coding-task-manager/lib/machine-registry.js +749 -0
  99. package/template/coding-task-manager/lib/machine-self.js +66 -0
  100. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +52 -4
  101. package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
  102. package/template/coding-task-manager/lib/media-journal.js +84 -27
  103. package/template/coding-task-manager/lib/media-store-options.js +43 -15
  104. package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
  105. package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
  106. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  107. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  108. package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
  109. package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
  110. package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
  111. package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
  112. package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
  113. package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
  114. package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
  115. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  116. package/template/coding-task-manager/lib/native-history-control.js +2 -2
  117. package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
  118. package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
  119. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
  120. package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
  121. package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
  122. package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
  123. package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
  124. package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
  125. package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
  126. package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
  127. package/template/coding-task-manager/lib/read-pool-client.js +5 -2
  128. package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
  129. package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
  130. package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
  131. package/template/coding-task-manager/lib/remote-command-context.js +12 -0
  132. package/template/coding-task-manager/lib/remote-command-runtime.js +13 -1
  133. package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
  134. package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
  135. package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
  136. package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
  137. package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
  138. package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
  139. package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
  140. package/template/coding-task-manager/lib/restart-preflight.js +9 -2
  141. package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
  142. package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
  143. package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
  144. package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
  145. package/template/coding-task-manager/lib/resume-cwd.js +13 -0
  146. package/template/coding-task-manager/lib/resume-from-store.js +12 -1
  147. package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
  148. package/template/coding-task-manager/lib/resume-preflight.js +21 -16
  149. package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
  150. package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
  151. package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
  152. package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
  153. package/template/coding-task-manager/lib/root-prompt-admission.js +33 -2
  154. package/template/coding-task-manager/lib/root-prompt-flow.js +167 -33
  155. package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
  156. package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
  157. package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
  158. package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
  159. package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
  160. package/template/coding-task-manager/lib/session-create/identity.js +20 -1
  161. package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
  162. package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
  163. package/template/coding-task-manager/lib/session-create/provider-process.js +34 -37
  164. package/template/coding-task-manager/lib/session-create/response.js +30 -12
  165. package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
  166. package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
  167. package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
  168. package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
  169. package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
  170. package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
  171. package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
  172. package/template/coding-task-manager/lib/session-host-manager.js +152 -206
  173. package/template/coding-task-manager/lib/session-jobs.js +17 -9
  174. package/template/coding-task-manager/lib/session-lifecycle.js +18 -0
  175. package/template/coding-task-manager/lib/session-model-action-preconditions.js +9 -0
  176. package/template/coding-task-manager/lib/session-resource-sampler.js +19 -9
  177. package/template/coding-task-manager/lib/session-standup.js +67 -4
  178. package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
  179. package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
  180. package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
  181. package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
  182. package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
  183. package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
  184. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
  185. package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
  186. package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
  187. package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
  188. package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
  189. package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
  190. package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
  191. package/template/coding-task-manager/lib/transcript-link.js +77 -0
  192. package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
  193. package/template/coding-task-manager/lib/transcript-source.js +8 -3
  194. package/template/coding-task-manager/lib/transcript-store.js +40 -0
  195. package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
  196. package/template/coding-task-manager/lib/typing-interaction-trace.js +92 -0
  197. package/template/coding-task-manager/lib/typing-worker-stages.js +60 -0
  198. package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
  199. package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
  200. package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
  201. package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
  202. package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
  203. package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
  204. package/template/coding-task-manager/lib/ws-router.js +68 -1
  205. package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
  206. package/template/coding-task-manager/oauth-proxy.js +99 -55
  207. package/template/coding-task-manager/package-lock.json +24 -0
  208. package/template/coding-task-manager/package.json +3 -0
  209. package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
  210. package/template/coding-task-manager/providers/codex-mcp.js +14 -5
  211. package/template/coding-task-manager/public/css/app.css +279 -1
  212. package/template/coding-task-manager/public/css/reviews.css +4 -0
  213. package/template/coding-task-manager/public/index.html +294 -223
  214. package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
  215. package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
  216. package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
  217. package/template/coding-task-manager/public/js/background-work.js +13 -7
  218. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
  219. package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
  220. package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
  221. package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
  222. package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
  223. package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
  224. package/template/coding-task-manager/public/js/history-scroller.js +187 -9
  225. package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
  226. package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
  227. package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
  228. package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -8
  229. package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
  230. package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
  231. package/template/coding-task-manager/public/js/reviews.js +7 -3
  232. package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
  233. package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
  234. package/template/coding-task-manager/public/js/row-join.js +4 -2
  235. package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
  236. package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
  237. package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
  238. package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
  239. package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
  240. package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
  241. package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
  242. package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
  243. package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
  244. package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
  245. package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
  246. package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
  247. package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
  248. package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -1
  249. package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
  250. package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
  251. package/template/coding-task-manager/public/js/walle-session.js +30 -7
  252. package/template/coding-task-manager/public/m/app.css +226 -8
  253. package/template/coding-task-manager/public/m/app.js +502 -9
  254. package/template/coding-task-manager/public/m/index.html +38 -1
  255. package/template/coding-task-manager/public/m/machines.js +1313 -0
  256. package/template/coding-task-manager/public/m/pool-failover.js +391 -0
  257. package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
  258. package/template/coding-task-manager/public/m/sw.js +5 -0
  259. package/template/coding-task-manager/queue-engine.js +13 -0
  260. package/template/coding-task-manager/server/approvals.js +5 -6
  261. package/template/coding-task-manager/server/boot.js +136 -17
  262. package/template/coding-task-manager/server.js +902 -239
  263. package/template/coding-task-manager/session-integrity.js +7 -9
  264. package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
  265. package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
  266. package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
  267. package/template/coding-task-manager/workers/session-host-process.js +14 -3
  268. package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
  269. package/template/coding-task-manager/workers/worktree-worker.js +34 -0
  270. package/template/package-lock.json +2 -2
  271. package/template/package.json +1 -1
  272. package/template/scripts/conversation-storage-v2-migration.js +78 -11
  273. package/template/shared/conversation-storage-v2-contract.js +42 -6
  274. package/template/shared/fixtures/worker-status-payloads.json +392 -0
  275. package/template/shared/git-remote-key.js +83 -0
  276. package/template/shared/host-identity.js +98 -2
  277. package/template/shared/owner-write-safety.js +135 -0
  278. package/template/shared/public-runtime.js +13 -5
  279. package/template/shared/release-schema.json +3 -2
  280. package/template/shared/report-citations.js +14 -0
  281. package/template/shared/sqlite-write-lock.js +361 -223
  282. package/template/shared/storage/contract.js +35 -3
  283. package/template/wall-e/agent.js +21 -0
  284. package/template/wall-e/api-walle.js +5 -0
  285. package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
  286. package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
  287. package/template/wall-e/brain/helper-rules-schema.js +129 -0
  288. package/template/wall-e/brain/migrations.js +51 -22
  289. package/template/wall-e/brain/model-deployment-facts.js +31 -1
  290. package/template/wall-e/brain/owner-call-whitelist.js +16 -0
  291. package/template/wall-e/brain/schema-ensures.js +22 -0
  292. package/template/wall-e/brain.js +1 -1
  293. package/template/wall-e/coding/journal-dispatcher.js +51 -0
  294. package/template/wall-e/coding/session-journal.js +28 -40
  295. package/template/wall-e/coding-orchestrator.js +23 -26
  296. package/template/wall-e/http/auth.js +18 -1
  297. package/template/wall-e/http/blog-views-api.js +318 -0
  298. package/template/wall-e/http/helper-rules-api.js +321 -0
  299. package/template/wall-e/http/model-proxy-providers.js +3 -1
  300. package/template/wall-e/http/telemetry-api.js +12 -0
  301. package/template/wall-e/lib/blog-view-policy.js +88 -0
  302. package/template/wall-e/lib/blog-view-store.js +169 -0
  303. package/template/wall-e/lib/client-config-lock.js +2 -1
  304. package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
  305. package/template/wall-e/lib/error-kinds.js +10 -1
  306. package/template/wall-e/lib/feature-manifest.js +1 -1
  307. package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
  308. package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
  309. package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
  310. package/template/wall-e/lib/helper-rules/grade.js +277 -0
  311. package/template/wall-e/lib/helper-rules/label.js +247 -0
  312. package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
  313. package/template/wall-e/lib/helper-rules/store.js +667 -0
  314. package/template/wall-e/lib/mcp-integration.js +5 -6
  315. package/template/wall-e/lib/runtime-worker-pool.js +1 -0
  316. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
  317. package/template/wall-e/lib/telemetry-db-schema.js +37 -0
  318. package/template/wall-e/lib/telemetry-process-client.js +76 -1
  319. package/template/wall-e/llm/default-fallback.js +4 -0
  320. package/template/wall-e/llm/model-catalog.js +13 -4
  321. package/template/wall-e/llm/model-roles.js +6 -0
  322. package/template/wall-e/llm/portkey-http.js +150 -0
  323. package/template/wall-e/llm/portkey-sync.js +15 -145
  324. package/template/wall-e/llm/worker-mcp-routing.js +61 -0
  325. package/template/wall-e/llm/worker-route.js +43 -1
  326. package/template/wall-e/loops/brain-custom-writes.js +45 -0
  327. package/template/wall-e/mcp-protocol-contract.js +41 -1
  328. package/template/wall-e/mcp-server.js +13 -2
  329. package/template/wall-e/mcp-worker-schemas.js +20 -2
  330. package/template/wall-e/memory/ctm-context-client.js +25 -5
  331. package/template/wall-e/shared/release-schema.json +3 -2
  332. package/template/wall-e/shared/sqlite-write-lock.js +361 -223
  333. package/template/wall-e/shared/storage/contract.js +35 -3
  334. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  335. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
  336. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
  337. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
  338. package/template/wall-e/tools/local-tools.js +6 -4
  339. package/template/wall-e/workers/brain-owner-worker.js +9 -0
  340. package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
  341. package/template/wall-e/workers/telemetry-worker.js +8 -0
@@ -0,0 +1,693 @@
1
+ 'use strict';
2
+
3
+ // WP-2 (docs/remote-machine-hub-sessions-2026-10-22.html §5 D-6/D-10, §6.3, §9 S-1/S-2/S-26):
4
+ // hub-side remote create. A `create` frame that names a REGISTERED REMOTE machine is never
5
+ // prepared locally — it is filtered through the §6.3 allowlist and forwarded to that box over a
6
+ // short-lived WS client connection authenticated with the peer bearer (D-7; the ONLY credential
7
+ // this feature has — no new env var, no new secret).
8
+ //
9
+ // Three rules drive every decision here:
10
+ // D-10 the forwarded frame is ALLOWLISTED, never the raw client frame. `msg.env`,
11
+ // `agentRole`, `workerRow`, every PUBLIC_CREATE_INTERNAL_CONTROLS field, every
12
+ // underscore-prefixed field and every field nobody classified are stripped by
13
+ // construction: `filterRemoteCreateForward` copies ONLY `REMOTE_CREATE_FORWARD_ALLOWLIST`
14
+ // members, so a new client-side create field is dropped until someone classifies it
15
+ // (tests/create-forward-allowlist.test.js fails on the unclassified field by name).
16
+ // This is the hole S-2/S1-D9 names: `msg.env` is not in the local deny list and merges
17
+ // into the spawn env (lib/prepared.js, lib/resolve-launch-params.js), so forwarding a raw
18
+ // frame would let the hub author the BOX's process environment.
19
+ // D-6 the ORIGINAL `id` (the create idempotency key) is forwarded verbatim; `clientMessageId`
20
+ // is deliberately NOT part of the forwarded payload. CORRECTED 2026-10-23 (WP-4 of
21
+ // docs/remote-hub-followups-credential-projects-rows-idempotency-2026-10-23.html §5
22
+ // D-11..D-14): this module used to claim that the box's `existing?.ptyProcess` dedupe made a
23
+ // same-id retry idempotent. It did not — that branch ATTACHES (a snapshot, no `created`) and
24
+ // the caller then rolled the attempt back as `session_setup_incomplete`, or, when the first
25
+ // launch was still in flight, answered `coding_model_generation_superseded` or spawned a
26
+ // SECOND PTY for the one id. The box's create path now carries the Idempotency-Key rules
27
+ // itself (fingerprint D-11, re-answer `created` D-12, `create_in_progress` D-13), so a
28
+ // same-id retry IS idempotent on the box — but the guarantee lives THERE, not here, and the
29
+ // hub still only forwards and relays whatever the box answers.
30
+ // S-26 env resolution is box-side BY CONSTRUCTION: forwarding happens in
31
+ // lib/ws/terminal-attach.js's handleCreate, BEFORE the local plan is built, so
32
+ // lib/resolve-launch-params.js never runs on the hub for a remote create and the box
33
+ // injects its own ports/hosts into the child environment.
34
+ //
35
+ // The persistent per-machine link (refcounted, lazy, reconnect/backoff) is WP-3's
36
+ // lib/remote-machine-link.js and is deliberately NOT built here: this module opens ONE
37
+ // short-lived socket per create, waits for the box's reply, and closes it.
38
+ //
39
+ // Dependency note: the peer credential is built from env in the same shape
40
+ // lib/hosted-sync-worker.js's `_authHeaders` uses (that function is not exported, and this module
41
+ // must not edit hosted-sync-worker.js — see the dependency-cycle note in lib/machine-registry.js).
42
+ // Everything is injectable for tests.
43
+
44
+ // §6.3 normative allowlist — the ONLY fields a forwarded create may carry, in §6.3's order.
45
+ const REMOTE_CREATE_FORWARD_ALLOWLIST = Object.freeze([
46
+ 'id',
47
+ 'cmd',
48
+ 'args',
49
+ 'cwd',
50
+ 'createCwd',
51
+ 'label',
52
+ 'title',
53
+ // D-13/WP-5 (§6.3 amendment): a CTM-composed label (`Shell:`/`Folder:`/…) marks itself
54
+ // `titleIntent:'default'` so the BOX's resolver writes it at tier 3. Without this the hub strips
55
+ // the mark and a remote create promotes its own composed label to a tier-1 (user) name.
56
+ 'titleIntent',
57
+ 'cols',
58
+ 'rows',
59
+ 'terminalDimsMeasured',
60
+ 'model_id',
61
+ 'model_provider',
62
+ 'mcp_profile',
63
+ 'agentType',
64
+ 'chatSessionId',
65
+ ]);
66
+
67
+ const DEFAULT_TIMEOUT_MS = 15000;
68
+
69
+ // WP-1 (D-2): a missing peer credential is a CONFIGURATION error, not an unreachable box. The box
70
+ // may be perfectly reachable — the hub simply has nothing to authenticate with, and no retry can
71
+ // change that (env is read at process start). Every hub→box lane reports this one code, so an
72
+ // operator sees "set CTM_SYNC_PEER_TOKEN", never a phantom network fault.
73
+ const PEER_CREDENTIAL_MISSING = 'peer_credential_missing';
74
+
75
+ // P-3 (rev 2): the one-hop guard's header. Every member-to-member request carries it; a receiver
76
+ // that sees it and is asked to reach a THIRD machine refuses `machine_forward_loop`.
77
+ const X_CTM_FORWARDED_BY = 'x-ctm-forwarded-by';
78
+ const MACHINE_FORWARD_LOOP = 'machine_forward_loop';
79
+
80
+ // ctm session id -> { machineInstanceId, machineName, at } for every session this hub created
81
+ // ON a box (OQ-4: mirror sessions stay OUTSIDE the sessions Map, whose LIST/ATTACH-reachable
82
+ // `_ptyProcess` surface is ~16 sites > the ~15 worth merging for). WP-3/WP-6 read this map; it is
83
+ // deliberately in-memory (a restart re-derives nothing and the box remains the authority).
84
+ const _mirrorSessions = new Map();
85
+
86
+ let _injected = null;
87
+
88
+ /** Test/host seam: override any production dependency (db, registry, env, WebSocket, …). */
89
+ function configure(deps) {
90
+ _injected = deps && typeof deps === 'object' ? deps : null;
91
+ }
92
+
93
+ function _string(value, max = 1000) {
94
+ return String(value == null ? '' : value).trim().slice(0, max);
95
+ }
96
+
97
+ function _trimUrl(url) {
98
+ return _string(url, 1000).replace(/\/+$/, '');
99
+ }
100
+
101
+ // Lazy: a caller that only needs the pure filter (the unit test, the WS branch test) must never
102
+ // load db.js / the owner client.
103
+ function _productionDeps() {
104
+ return {
105
+ env: process.env,
106
+ // The same handle server.js hands lib/api/machines.js: the DB-owner mutation Proxy, so the
107
+ // registry read below is the one the routes use.
108
+ db: require('../db-owner-client'),
109
+ registry: require('./machine-registry'),
110
+ parseSyncPeers: require('./hosted-sync-worker').parseSyncPeers,
111
+ // WP-6 §8: the post-create catalog refresh (lazy: the catalog module requires THIS one).
112
+ refreshCatalog: (machineId) => {
113
+ try {
114
+ require('./remote-machine-catalog').fetchCatalog(machineId).catch(() => { /* expected: fetchCatalog resolves its own failures */ });
115
+ } catch { /* expected: a catalog fault must never fail a create */ }
116
+ },
117
+ };
118
+ }
119
+
120
+ function _deps(overrides) {
121
+ return Object.assign({}, _productionDeps(), _injected || {}, overrides || {});
122
+ }
123
+
124
+ /**
125
+ * The §6.3 forward: copy ONLY allowlisted own properties. Stripped ALWAYS (by construction, and
126
+ * asserted by the allowlist test): `env`, `agentRole`, `workerRow`, every
127
+ * PUBLIC_CREATE_INTERNAL_CONTROLS name, every underscore-prefixed name, and anything nobody
128
+ * classified. `machine` is a routing field of the hub's own protocol and never crosses.
129
+ */
130
+ function filterRemoteCreateForward(msg) {
131
+ const out = {};
132
+ if (!msg || typeof msg !== 'object') return out;
133
+ for (const key of REMOTE_CREATE_FORWARD_ALLOWLIST) {
134
+ if (key.startsWith('_')) continue; // defensive: no underscore name is on the list today
135
+ if (!Object.prototype.hasOwnProperty.call(msg, key)) continue;
136
+ out[key] = msg[key];
137
+ }
138
+ return out;
139
+ }
140
+
141
+ /**
142
+ * WP-3 (DGX Spark coding readiness §2): which coding agent — if any — a forwarded create is about.
143
+ * The create path picks the CLI from `msg.agentType || detectAgentType(cmd)` (server.js:34024, whose
144
+ * canonical values are exactly 'claude' | 'codex' — lib/agent-capabilities.js:19,45 and
145
+ * lib/create/resolve-launch-params.js:692, which also writes the matching bare `cmd` at :772/:824,
146
+ * the same shape public/js/reviews.js:858 sends). Only those two can be signed out, so a `null`
147
+ * (shell / gemini / walle / anything else) is never refused — the caller checks the sign-in verdict
148
+ * for that agent only. Pure and total: any other shape reads `null`.
149
+ */
150
+ function remoteCreateAgentKey(payload) {
151
+ const agentType = _string(payload && payload.agentType, 40).toLowerCase();
152
+ if (agentType === 'claude' || agentType === 'codex') return agentType;
153
+ const cmd = _string(payload && payload.cmd, 300);
154
+ if (!cmd) return null;
155
+ const base = cmd.split(/[\\/]/).pop().toLowerCase();
156
+ return base === 'claude' || base === 'codex' ? base : null;
157
+ }
158
+
159
+ /** The hub's own minted instance id, or '' when identity is not available yet. */
160
+ function _localInstanceId(deps) {
161
+ if (typeof deps.localInstanceId === 'string') return _string(deps.localInstanceId, 160);
162
+ try {
163
+ const identity = require('./instance-identity-access').getInstanceIdentity({ required: false });
164
+ return _string(identity && identity.instanceId, 160);
165
+ } catch {
166
+ // expected: identity is not ready yet (pre-DB-open) — the caller then treats an explicitly
167
+ // named machine as remote rather than silently running a foreign session on this host.
168
+ return '';
169
+ }
170
+ }
171
+
172
+ /**
173
+ * S-1: does this create name a REMOTE machine? `machine` absent, empty, or equal to this
174
+ * process's own minted instance id takes the UNCHANGED local path (S-2: the local path and the
175
+ * local deny list are untouched). When the local id cannot be resolved yet, an explicitly named
176
+ * machine is treated as remote — the honest failure (`machine_unreachable`) beats spawning a
177
+ * session on the wrong host.
178
+ */
179
+ function isRemoteMachineTarget(msg, deps = {}) {
180
+ const machine = _string(msg && msg.machine, 160);
181
+ if (!machine) return false;
182
+ const d = _deps(deps);
183
+ return machine !== _localInstanceId(d);
184
+ }
185
+
186
+ function _findMachine(machineId, deps) {
187
+ const registry = deps.registry;
188
+ if (!registry || typeof registry.listMachines !== 'function') return null;
189
+ const listed = registry.listMachines({ db: deps.db }) || {};
190
+ const machines = Array.isArray(listed.machines) ? listed.machines : [];
191
+ return machines.find((row) => _string(row.instanceId, 160) === machineId) || null;
192
+ }
193
+
194
+ function _parsePeerTokenMap(raw = '') {
195
+ const value = _string(raw, 10000);
196
+ const map = new Map();
197
+ if (!value) return map;
198
+ if (value.startsWith('{')) {
199
+ try {
200
+ const parsed = JSON.parse(value);
201
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
202
+ for (const [peerId, token] of Object.entries(parsed)) {
203
+ if (_string(peerId, 160) && _string(token, 1000)) map.set(_string(peerId, 160), _string(token, 1000));
204
+ }
205
+ return map;
206
+ }
207
+ } catch {
208
+ // expected: a malformed JSON map falls through to the `id=token` list form below
209
+ }
210
+ }
211
+ for (const part of value.split(',')) {
212
+ const [peerId, ...rest] = part.split('=');
213
+ if (_string(peerId, 160) && _string(rest.join('='), 1000)) {
214
+ map.set(_string(peerId, 160), _string(rest.join('='), 1000));
215
+ }
216
+ }
217
+ return map;
218
+ }
219
+
220
+ /** The legacy env bearer (D-8): CTM_SYNC_PEER_TOKEN, or this host's entry in CTM_SYNC_PEER_TOKENS. */
221
+ function _envPeerToken(env, peerId) {
222
+ const direct = _string(env.CTM_SYNC_PEER_TOKEN, 1000);
223
+ if (direct) return direct;
224
+ if (env.CTM_SYNC_PEER_TOKENS && peerId) return _string(_parsePeerTokenMap(env.CTM_SYNC_PEER_TOKENS).get(peerId), 1000);
225
+ return '';
226
+ }
227
+
228
+ /** This process's MINTED instance id (falling back to the env tag) — the P-3 hop header's value. */
229
+ function _ownForwardId(env, deps) {
230
+ return _localInstanceId(deps) || _string(env.CTM_INSTANCE_ID || env.CTM_INSTANCE_TAG || env.HOSTNAME || '', 160);
231
+ }
232
+
233
+ /**
234
+ * The hub's OWN peer credential (D-7 / D-8 rev 2 / P-8): `Authorization: Bearer <token>` +
235
+ * `X-CTM-Sync-Peer: <peer id>` + (rev 2, P-3) `X-CTM-Forwarded-By: <own instanceId>`.
236
+ *
237
+ * D-8 OUTBOUND: the shared pool token when this machine has a pool (lib/machine-pool.js), else
238
+ * the env CTM_SYNC_PEER_TOKEN(S) legacy fallback. The env shape and precedence are
239
+ * unchanged from WP-1.
240
+ * P-8 …but a pool secret is sent ONLY to a machine that is IN the pool. A legacy env-token box
241
+ * that never joined would 401 a pool bearer, so once this hub has a pool, a non-member
242
+ * target is dialled with the env bearer instead. The choice is per TARGET: pass
243
+ * `deps.instanceId`. Precedence is
244
+ * pool when hasPool() && isMember(instanceId) — never to a non-member: a box outside the
245
+ * pool cannot verify the secret, and lib/auth-context.js does not fall through on a
246
+ * `ctmpool1_` bearer it cannot verify, so sending it is both a refusal and a leak,
247
+ * env when an env token is configured,
248
+ * null otherwise.
249
+ * With NO `instanceId` the caller is not describing a target (a self-probe, a test), so the
250
+ * old pool-if-pool precedence is kept.
251
+ * P-3 EVERY result (pool or env) carries `X-CTM-Forwarded-By`. It is the one-hop guard's
252
+ * signal: a receiver that sees it and is asked to dial a THIRD machine refuses
253
+ * `machine_forward_loop`. The value is the sender's minted instance id (the same id the
254
+ * `machine` routing field uses), read through lib/instance-identity-access; it falls back to
255
+ * the env instance tag so a pre-boot dial still names the sender.
256
+ *
257
+ * Returns null when NO credential is configured (then the caller reports peer_credential_missing).
258
+ */
259
+ function buildPeerControlHeaders(env = process.env, deps = {}) {
260
+ const peerId = _string(env.CTM_INSTANCE_ID || env.CTM_INSTANCE_TAG || env.HOSTNAME || '', 160);
261
+ const forwardedBy = _ownForwardId(env, deps);
262
+ const envToken = _envPeerToken(env, peerId);
263
+ const poolToken = _poolToken();
264
+ // P-8: the per-target decision. A named non-member is dialled with the env bearer so a legacy
265
+ // box keeps answering; an unnamed target keeps the pool-if-pool behaviour this function had.
266
+ const target = _string(deps.instanceId, 160);
267
+ const poolApplies = !!poolToken && (!target || _isPoolMember(target));
268
+ if (poolApplies) {
269
+ return {
270
+ Authorization: `Bearer ${poolToken}`,
271
+ ...(forwardedBy ? { 'X-CTM-Sync-Peer': forwardedBy } : {}),
272
+ 'X-CTM-Forwarded-By': forwardedBy,
273
+ };
274
+ }
275
+ if (!envToken) return null;
276
+ return {
277
+ Authorization: `Bearer ${envToken}`,
278
+ ...(peerId ? { 'X-CTM-Sync-Peer': peerId } : {}),
279
+ 'X-CTM-Forwarded-By': forwardedBy,
280
+ };
281
+ }
282
+
283
+ /** P-8: is `instanceId` one of this pool's machines? Guarded so a leaf/test never loads the store. */
284
+ function _isPoolMember(instanceId) {
285
+ try {
286
+ const pool = require('./machine-pool').getMachinePool();
287
+ return !!(pool && typeof pool.isMember === 'function' && pool.isMember(instanceId));
288
+ } catch {
289
+ return false; // expected: machine-pool is a pure leaf; an unreadable store means "not a member"
290
+ }
291
+ }
292
+
293
+ /** The loaded pool's outbound bearer, or '' — lazy + guarded so a unit test never loads the store. */
294
+ function _poolToken() {
295
+ try {
296
+ const pool = require('./machine-pool').getMachinePool();
297
+ return pool && pool.hasPool() ? _string(pool.token(), 1000) : '';
298
+ } catch {
299
+ return ''; // expected: machine-pool is a pure leaf; a load failure must never break a dial
300
+ }
301
+ }
302
+
303
+ /**
304
+ * WP-1 (D-4) / D-8 rev 2 / P-8: the credential state, for surfaces that must SHOW the operator why
305
+ * a box cannot be reached. It answers per TARGET when given one: 'pool' when THIS target is dialled
306
+ * with the shared pool secret, 'env' when it is dialled with the legacy CTM_SYNC_PEER_TOKEN(S),
307
+ * else 'missing'. The token VALUE is never returned, logged or derived from here — only which
308
+ * credential class is in play. It is the same decision buildPeerControlHeaders makes, so a row's
309
+ * word always matches the bearer that will actually be sent.
310
+ */
311
+ function peerCredentialState(env = process.env, { instanceId } = {}) {
312
+ return buildPeerControlHeaders(env, { instanceId }) ? _credentialClassOf(env, instanceId) : 'missing';
313
+ }
314
+
315
+ /** P-8: which credential class buildPeerControlHeaders picks for `instanceId` — never the value. */
316
+ function _credentialClassOf(env, instanceId) {
317
+ const peerId = _string(env.CTM_INSTANCE_ID || env.CTM_INSTANCE_TAG || env.HOSTNAME || '', 160);
318
+ const target = _string(instanceId, 160);
319
+ const poolApplies = !!_poolToken() && (!target || _isPoolMember(target));
320
+ return poolApplies ? 'pool' : 'env';
321
+ }
322
+
323
+ /** P-3: the forwarded-by hop header on an inbound request, or '' (never throws on a bare ctx). */
324
+ function forwardedByOf(source) {
325
+ const headers = (source && (source.headers || (source.ws && source.ws._ctmRequest && source.ws._ctmRequest.headers))) || {};
326
+ return _string(headers[X_CTM_FORWARDED_BY] || '', 200);
327
+ }
328
+
329
+ /**
330
+ * WP-1 (D-2): the operator-facing copy for PEER_CREDENTIAL_MISSING. Names the env var and BOTH
331
+ * machines, because the credential must be the same on each side and is only read at process start.
332
+ */
333
+ function peerCredentialMissingMessage(machineName) {
334
+ return `Set the same CTM_SYNC_PEER_TOKEN on this machine and on ${machineName || 'the machine'}, then restart both`;
335
+ }
336
+
337
+ function _wsUrlFor(machineUrl) {
338
+ const base = _trimUrl(machineUrl);
339
+ if (!base) return '';
340
+ try {
341
+ const url = new URL(base);
342
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
343
+ return `${url.origin}/ws`;
344
+ } catch {
345
+ return '';
346
+ }
347
+ }
348
+
349
+ /**
350
+ * The shared DIAL prologue (WP-2 + WP-3 §6.4) — everything that must happen before a peer socket
351
+ * exists: the ws/wss URL, the peer bearer (D-7), and the WebSocket constructor. Both the one-shot
352
+ * create forward (sendRemoteCreate) and the persistent hub link (lib/remote-machine-link.js) start
353
+ * here so the two lanes can never drift on auth or URL shape.
354
+ *
355
+ * @returns {{ok: true, url: string, headers: object, WebSocketImpl: Function}|{ok: false, code: string, error: string}}
356
+ */
357
+ function preparePeerDial(machineUrl, deps = {}) {
358
+ const url = _wsUrlFor(machineUrl);
359
+ if (!url) return { ok: false, code: 'machine_unreachable', error: 'machine_url_invalid' };
360
+ // P-8: `deps.instanceId` is the machine this dial is FOR, when the caller knows it. The dial
361
+ // prologue is shared with the hub link lane, so the target must ride the deps rather than being
362
+ // re-derived here.
363
+ const headers = typeof deps.authHeaders === 'function'
364
+ ? deps.authHeaders()
365
+ : buildPeerControlHeaders(deps.env || process.env, { instanceId: deps.instanceId });
366
+ // D-2: the credential is mandatory on EVERY leg, so a missing one is named as a configuration
367
+ // error (with the machine's name in the copy when the caller knows it) instead of being folded
368
+ // into `machine_unreachable`, which sends the operator hunting for a network fault.
369
+ if (!headers) {
370
+ return {
371
+ ok: false,
372
+ code: PEER_CREDENTIAL_MISSING,
373
+ error: peerCredentialMissingMessage(_string(deps.machineName, 200)),
374
+ };
375
+ }
376
+ let WebSocketImpl = deps.WebSocket;
377
+ if (!WebSocketImpl) {
378
+ try {
379
+ WebSocketImpl = require('ws').WebSocket;
380
+ } catch (error) {
381
+ return { ok: false, code: 'machine_unreachable', error: `ws_unavailable:${error?.message || error}` };
382
+ }
383
+ }
384
+ return { ok: true, url, headers, WebSocketImpl };
385
+ }
386
+
387
+ /**
388
+ * The dial ORDER is load-bearing (D-6/§6.4 "HELLO = normal client hello"): the box's WS gate drops
389
+ * a terminal-protocol message — `create` included — that arrives before a compatible `client-info`.
390
+ * This is the one frame both lanes answer the box's `hello` with, so a browser-faithful handshake
391
+ * can never be re-implemented slightly differently on a second lane.
392
+ */
393
+ function peerClientInfoFrame(hello) {
394
+ const appVersion = (hello && hello.appVersion) || {};
395
+ return {
396
+ type: 'client-info',
397
+ terminalProtocol: Number((hello && hello.terminalProtocol) || appVersion.terminalProtocol || 0),
398
+ assetEpoch: String(appVersion.assetEpoch || ''),
399
+ surface: 'machine-hub',
400
+ };
401
+ }
402
+
403
+ /**
404
+ * One short-lived forward: connect, HELLO handshake, send the filtered create, wait for the
405
+ * box's `created` (or `error`) frame, close. Never throws — the caller turns a failure into the
406
+ * `machine_unreachable` (or box-error) frame.
407
+ *
408
+ * @returns {Promise<{ok: true, frame: object}|{ok: false, code: string, error: string}>}
409
+ */
410
+ function sendRemoteCreate({ machineUrl, payload, deps }) {
411
+ return new Promise((resolve) => {
412
+ const dial = preparePeerDial(machineUrl, deps);
413
+ if (!dial.ok) return resolve(dial);
414
+ const { url, headers, WebSocketImpl } = dial;
415
+ const timeoutMs = Number(deps.timeoutMs || DEFAULT_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS;
416
+ const setTimeoutFn = typeof deps.setTimeoutFn === 'function' ? deps.setTimeoutFn : setTimeout;
417
+ const clearTimeoutFn = typeof deps.clearTimeoutFn === 'function' ? deps.clearTimeoutFn : clearTimeout;
418
+
419
+ let socket = null;
420
+ let timer = null;
421
+ let settled = false;
422
+ let sent = false;
423
+ const finish = (result) => {
424
+ if (settled) return;
425
+ settled = true;
426
+ if (timer) clearTimeoutFn(timer);
427
+ try { if (socket) socket.close(); } catch { /* expected: close on an already-dead socket */ }
428
+ resolve(result);
429
+ };
430
+ // Only ever after client-info (see the dial-order note above): a create sent earlier is
431
+ // dropped by the box's protocol gate and the forward would hang until the timeout.
432
+ const sendCreate = () => {
433
+ if (sent || settled) return;
434
+ sent = true;
435
+ try {
436
+ socket.send(JSON.stringify({ type: 'create', ...payload }));
437
+ } catch (error) {
438
+ finish({ ok: false, code: 'machine_unreachable', error: `send_failed:${error?.message || error}` });
439
+ }
440
+ };
441
+
442
+ try {
443
+ socket = new WebSocketImpl(url, { headers });
444
+ } catch (error) {
445
+ return resolve({ ok: false, code: 'machine_unreachable', error: `connect_failed:${error?.message || error}` });
446
+ }
447
+
448
+ timer = setTimeoutFn(() => {
449
+ try { socket.terminate(); } catch { /* expected: the socket is already gone */ }
450
+ finish({ ok: false, code: 'machine_unreachable', error: 'timeout' });
451
+ }, timeoutMs);
452
+ if (timer && typeof timer.unref === 'function') timer.unref();
453
+
454
+ // No create on 'open': the box has not said hello yet, so its protocol gate is not satisfied.
455
+ socket.on('message', (raw) => {
456
+ let frame = null;
457
+ try {
458
+ frame = JSON.parse(raw.toString());
459
+ } catch {
460
+ return; // expected: a non-JSON frame (the box's binary/other lanes) is not the reply
461
+ }
462
+ if (!frame || typeof frame !== 'object') return;
463
+ if (frame.type === 'hello') {
464
+ // The box gates every terminal-protocol message (including 'create') behind a compatible
465
+ // client-info, so the hub identifies itself with the epoch the box just advertised — the
466
+ // same handshake a browser does. Both frames go out in this order on this one socket.
467
+ try {
468
+ socket.send(JSON.stringify(peerClientInfoFrame(frame)));
469
+ } catch { /* expected: the socket died between hello and client-info */ }
470
+ sendCreate();
471
+ return;
472
+ }
473
+ if (frame.type === 'created') return finish({ ok: true, frame });
474
+ if (frame.type === 'terminal-protocol-incompatible') {
475
+ // The box refused the frame on protocol grounds. Fail now with the reason instead of
476
+ // waiting out the timeout for a reply that will never come (this is the frame that made a
477
+ // mis-ordered dial look like an unreachable box).
478
+ return finish({
479
+ ok: false,
480
+ code: 'machine_unreachable',
481
+ error: `terminal_protocol_incompatible:${_string(frame.reason, 80) || _string(frame.blockedMessage, 60) || 'unknown'}`,
482
+ });
483
+ }
484
+ if (frame.type === 'auth-error') {
485
+ // The box's WS authorization verdict on a frame (server.js `_sendWsAuthFailure`), which
486
+ // NAMES the frame it refused in `messageType`. A verdict on the `create` means the session
487
+ // was never prepared — the box's own code (`rate_limited`, `insufficient_scope`, …) is the
488
+ // reply, so surface it rather than waiting out the timeout for a `created` that is not
489
+ // coming. A verdict on `client-info` is routine noise on this lane (the box applies ws
490
+ // build compatibility at server.js:30158, BEFORE `_authorizeWsMessage`, so it refuses the
491
+ // handshake frame for a peer-control identity and honours the create anyway) and must stay
492
+ // non-fatal — treating every auth-error as fatal would break every forward.
493
+ if (_string(frame.messageType, 40) === 'create') {
494
+ const code = _string(frame.code, 80) || 'machine_unreachable';
495
+ return finish({ ok: false, code, error: `box_refused_create:${code}` });
496
+ }
497
+ return;
498
+ }
499
+ if (frame.type === 'error') {
500
+ return finish({
501
+ ok: false,
502
+ code: _string(frame.code, 80) || 'machine_unreachable',
503
+ error: _string(frame.message || frame.error, 300) || 'box_error',
504
+ });
505
+ }
506
+ // Everything else (hello, lifecycle-pressure, session-activity, sessions, …) is either the
507
+ // handshake or the box's ambient traffic on this lane and is not a reply to the create.
508
+ });
509
+ socket.on('error', (error) => {
510
+ finish({ ok: false, code: 'machine_unreachable', error: _string(error?.message || error, 300) });
511
+ });
512
+ socket.on('close', () => {
513
+ finish({ ok: false, code: 'machine_unreachable', error: 'closed_before_reply' });
514
+ });
515
+ });
516
+ }
517
+
518
+ function _sendFrame(ctx, frame) {
519
+ try {
520
+ if (ctx && ctx.ws && ctx.ws.readyState === 1 && typeof ctx.send === 'function') {
521
+ ctx.send(JSON.stringify(frame));
522
+ }
523
+ } catch {
524
+ // expected: the requesting browser socket died while the box was answering — the session
525
+ // exists on the box and WP-6's catalog merge is what re-surfaces it.
526
+ }
527
+ }
528
+
529
+ function _fail(ctx, code, detail) {
530
+ _sendFrame(ctx, {
531
+ type: 'error',
532
+ code: code || 'machine_unreachable',
533
+ message: detail || 'The machine could not be reached.',
534
+ });
535
+ return { ok: false, code: code || 'machine_unreachable', error: detail || '' };
536
+ }
537
+
538
+ /**
539
+ * Forward an already-classified remote create (S-1). On success the box's own `created` frame is
540
+ * re-emitted to the requesting browser socket with `machine`/`machineName` added (AC-2a) and the
541
+ * mirror registration is recorded; on failure the caller's socket gets exactly one typed `error`
542
+ * frame and NOTHING is created locally (AC-2c).
543
+ */
544
+ async function forwardRemoteCreate(ctx, msg, deps = {}) {
545
+ const d = _deps(deps);
546
+ const machineId = _string(msg && msg.machine, 160);
547
+ // P-3 one-hop guard. This function is reached ONLY when the create names a machine that is not
548
+ // this instance (lib/ws/terminal-attach.js checks isRemoteMachineTarget first), so a request that
549
+ // already carries the hop header is by definition a request to forward a forwarded create — the
550
+ // A→B→C chain (and the A→B→A loop) P-3 forbids. Nothing is dialled and nothing is created.
551
+ const forwardedBy = forwardedByOf(ctx);
552
+ if (forwardedBy) {
553
+ return _fail(ctx, MACHINE_FORWARD_LOOP, `This create was already forwarded by ${forwardedBy}; a forwarded request is never forwarded again`);
554
+ }
555
+ const row = _findMachine(machineId, d);
556
+ if (!row) return _fail(ctx, 'machine_unreachable', `machine ${machineId} is not registered on this hub`);
557
+ if (!row.enabled) return _fail(ctx, 'machine_unreachable', `machine ${machineId} is disabled`);
558
+ if (!_string(row.url, 1000)) return _fail(ctx, 'machine_unreachable', `machine ${machineId} has no url`);
559
+ if (_string(row.health, 40) === 'unreachable') {
560
+ return _fail(ctx, 'machine_unreachable', `machine ${machineId} is unreachable`);
561
+ }
562
+
563
+ const payload = filterRemoteCreateForward(msg);
564
+ // WP-3: the box judges a create only by its manifest probe, so a box that is REACHABLE but has no
565
+ // signed-in Claude/Codex CLI would happily spawn a session that opens at a login prompt. Refuse at
566
+ // the hub instead, naming the box and the URL that fixes it. 'unknown' (never probed, unreachable
567
+ // probe, unreadable body) and 'signed_in' both proceed: only an explicit `signedIn:false` refuses.
568
+ const agentKey = remoteCreateAgentKey(payload);
569
+ const auth = agentKey && d.registry && typeof d.registry.agentAuthFor === 'function'
570
+ ? d.registry.agentAuthFor(row.instanceId)
571
+ : null;
572
+ if (auth && auth[agentKey] === 'signed_out') {
573
+ const label = agentKey === 'codex' ? 'Codex' : 'Claude Code';
574
+ return _fail(ctx, 'machine_agent_signed_out', `${label} is not signed in on ${_string(row.displayName, 200) || row.instanceId} — open ${row.url} → AI Providers → Coding Agents → Sign in`);
575
+ }
576
+ // The machine's DISPLAY name rides the dial deps so a credential-less refusal can name the box
577
+ // the operator has to configure (D-2), not just "the machine".
578
+ const result = await sendRemoteCreate({
579
+ machineUrl: row.url,
580
+ payload,
581
+ // P-8: the TARGET's instanceId rides the dial deps so the credential is chosen per machine —
582
+ // a legacy env-token box that never joined the pool is dialled with the env bearer, not with a
583
+ // pool secret it would 401.
584
+ deps: { ...d, machineName: _string(row.displayName, 200), instanceId: _string(row.instanceId, 160) },
585
+ });
586
+ if (!result.ok) return _fail(ctx, result.code, result.error);
587
+
588
+ const frame = result.frame || {};
589
+ const sessionId = _string(frame.id || payload.id, 200);
590
+ if (sessionId) recordMirrorSession(sessionId, row);
591
+ _sendFrame(ctx, {
592
+ ...frame,
593
+ machine: _string(row.instanceId, 160),
594
+ machineName: _string(row.displayName, 200),
595
+ });
596
+ // WP-6 §8: "right after a forwarded create" is a catalog refresh trigger — the new session must
597
+ // appear in the hub's merged list at once, not on the next ~15 s probe sweep. Fire-and-forget:
598
+ // the in-memory registration above already makes the relay work, so a catalog fault is a stale
599
+ // list, never a failed create.
600
+ if (typeof d.refreshCatalog === 'function') {
601
+ try { d.refreshCatalog(_string(row.instanceId, 160)); } catch { /* expected: a catalog fault never fails a create */ }
602
+ }
603
+ return { ok: true, sessionId, machineInstanceId: _string(row.instanceId, 160) };
604
+ }
605
+
606
+ /** Record (or refresh) the in-memory mirror registration for a session this hub created on a box. */
607
+ function recordMirrorSession(sessionId, machineRow) {
608
+ const id = _string(sessionId, 200);
609
+ if (!id) return null;
610
+ const entry = {
611
+ machineInstanceId: _string(machineRow && machineRow.instanceId, 160),
612
+ machineName: _string(machineRow && machineRow.displayName, 200),
613
+ at: Date.now(),
614
+ };
615
+ _mirrorSessions.set(id, entry);
616
+ return entry;
617
+ }
618
+
619
+ /**
620
+ * WP-6 (S-3/S-27): the SINGLE read seam every relaying caller uses — "which machine owns this ctm
621
+ * session id". It is the UNION over the in-memory registrations above, the live catalog
622
+ * (lib/remote-machine-catalog.js) and the at-rest sync mirror rows, so a hub RESTART restores
623
+ * relaying from the first catalog fetch (or from the sync mirror rows) with no user action — the
624
+ * in-memory map alone would forget every relay.
625
+ *
626
+ * The union lives in lib/remote-machine-catalog.js. The require is lazy and one-directional (this
627
+ * module -> the catalog; the catalog takes this module as an injected peer, wired by server.js), so
628
+ * the lib/** graph stays acyclic and boot order is never load-bearing. The raw-map read below stays
629
+ * O(1) for this module's own callers.
630
+ */
631
+ function getMirrorSessionMachine(ctmSessionId) {
632
+ try {
633
+ return require('./remote-machine-catalog').getMirrorSessionMachine(ctmSessionId) || null;
634
+ } catch {
635
+ // REPORT: a catalog fault degrades to the in-memory registrations, which is strictly less
636
+ // knowledge — never more.
637
+ return getLocalMirrorSession(ctmSessionId);
638
+ }
639
+ }
640
+
641
+ /**
642
+ * Drop a registration once the hub relayed the session's kill: the box is the authority from then
643
+ * on (the catalog re-reads it as exited), and without this every create this process ever relayed
644
+ * would stay pinned in the map for the life of the process.
645
+ */
646
+ function forgetMirrorSession(sessionId) {
647
+ return _mirrorSessions.delete(_string(sessionId, 200));
648
+ }
649
+
650
+ /** The in-memory registrations ONLY (no catalog, no sync mirror) — one map read. */
651
+ function getLocalMirrorSession(ctmSessionId) {
652
+ return _mirrorSessions.get(_string(ctmSessionId, 200)) || null;
653
+ }
654
+
655
+ /** WP-3/WP-6 read seam: every mirror registration this process holds. */
656
+ function listMirrorSessions() {
657
+ return [..._mirrorSessions.entries()].map(([sessionId, row]) => ({ sessionId, ...row }));
658
+ }
659
+
660
+ /** Test seam: the mirror map is module state and must never leak between tests. */
661
+ function _resetMirrorSessionsForTests() {
662
+ _mirrorSessions.clear();
663
+ }
664
+
665
+ module.exports = {
666
+ REMOTE_CREATE_FORWARD_ALLOWLIST,
667
+ DEFAULT_TIMEOUT_MS,
668
+ configure,
669
+ filterRemoteCreateForward,
670
+ remoteCreateAgentKey,
671
+ isRemoteMachineTarget,
672
+ forwardRemoteCreate,
673
+ sendRemoteCreate,
674
+ buildPeerControlHeaders,
675
+ // WP-1 (D-2/D-4): the missing-credential code, its operator copy, and the hub-local state.
676
+ PEER_CREDENTIAL_MISSING,
677
+ peerCredentialMissingMessage,
678
+ peerCredentialState,
679
+ // WP-2a (P-3): the hop header name, its refusal code, and the inbound-header reader both the
680
+ // WS create path and (via lib/remote-machine-proxy.js) the HTTP proxy path use.
681
+ X_CTM_FORWARDED_BY,
682
+ MACHINE_FORWARD_LOOP,
683
+ forwardedByOf,
684
+ // WP-3: the shared dial/handshake prologue both lanes use (lib/remote-machine-link.js).
685
+ preparePeerDial,
686
+ peerClientInfoFrame,
687
+ recordMirrorSession,
688
+ forgetMirrorSession,
689
+ getMirrorSessionMachine,
690
+ getLocalMirrorSession,
691
+ listMirrorSessions,
692
+ _resetMirrorSessionsForTests,
693
+ };