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,798 @@
1
+ 'use strict';
2
+
3
+ // WP-2 (docs/remote-session-hub-bridge-2026-10-30.html §5 R-4/R-5/R-8/R-9, the Rev-2 card R-11, §6.2,
4
+ // §8 WP-2): the ACCESS DECISION for a hub-bridge op — may this caller session run this tier on THIS
5
+ // machine, and if not, what does the hub tell the agent and the user?
6
+ //
7
+ // R-4 grants, on TWO axes (docs/hub-bridge-approval-scopes-2026-11-01.html §5 D-1..D-5, §6.1).
8
+ // A grant is keyed (machineId, callerSessionId, tier) and covers only its own tier (`exec`
9
+ // implies `read`). SCOPE is one caller session, or every session of that machine written
10
+ // `callerSessionId: '*'` (ALL_SESSIONS). DURATION is a TTL in memory (default 3600 s, hard max
11
+ // 28800 s — a restart revokes it) or `always: true`, persisted in the trust file's `grants`
12
+ // array so it survives a restart. Neither axis ever writes machine trust or the mesh (D-2).
13
+ // Minted only by a user principal (`POST /api/hub-bridge/grants`, lib/api/hub-bridge.js). To
14
+ // keep it testable the clock is INJECTED (`deps.now`), never read directly.
15
+ // R-5 requested, never ambient. A refusal at trust `ask` records ONE pending row per
16
+ // (machineId, callerSessionId, tier) — a second call while it is open adds no row and
17
+ // re-broadcasts nothing — and answers `hub_grant_required` with that row's id.
18
+ // R-11 trust policy per machine, set on the machine that does the work. `getDataDir()/hub-bridge-trust.json`
19
+ // (mode 0600, atomic tmp+rename) holds `{ v:1, default:'ask', machines:{ [instanceId]: 'ask'|'always'|'never' },
20
+ // grants:[ {machineId, callerSessionId, tier, at} ] }`. `always` is an implicit grant of every
21
+ // tier to every session of that member (audited, no TTL); `never` refuses with
22
+ // `hub_bridge_denied` and creates NO pending row; `ask` is the R-4 flow. `grants` holds the
23
+ // machine-scoped and durable grants of D-1/D-4 and is written ONLY by grant/revoke (a trust
24
+ // PUT carries it through untouched). The file is re-read per decision (mtime+size cached), so a
25
+ // change applies to the next call.
26
+ // R-9 audited. `argsDigest` is sha256(JSON(args)) and the RAW args are never written.
27
+ //
28
+ // Everything is injectable (`configure`) and the process singleton is wired once at boot in server.js.
29
+
30
+ const crypto = require('node:crypto');
31
+ const fs = require('node:fs');
32
+ const path = require('node:path');
33
+
34
+ const { getDataDir } = require('./data-dir');
35
+
36
+ const TRUST_FILE = 'hub-bridge-trust.json';
37
+ const AUDIT_FILE = 'hub-bridge-audit.jsonl';
38
+ const TRUST_VALUES = Object.freeze(['ask', 'always', 'never']);
39
+ // The known tiers, ordered for display only. Coverage is NOT by rank: R-4 says `exec` implies `read`
40
+ // and nothing else — a `browser` grant gives no shell, and an `exec` grant drives no browser
41
+ // (IMPLIES below). "always covers every tier" (R-11) is the trust check in decide(), not a grant.
42
+ const TIER_RANK = Object.freeze({ none: 0, read: 1, exec: 2, browser: 3 });
43
+ const IMPLIES = Object.freeze({ read: ['read'], exec: ['exec', 'read'], browser: ['browser'] });
44
+ const DEFAULT_TTL_SEC = 3600;
45
+ const MAX_TTL_SEC = 28800;
46
+ const SUMMARY_MAX = 120;
47
+ const AUDIT_TAIL_BYTES = 256 * 1024;
48
+ const AUDIT_DEFAULT_LIMIT = 20;
49
+ const AUDIT_MAX_LIMIT = 200;
50
+ const TRUST_FILE_MODE = 0o600;
51
+ // D-3: the machine-scope sentinel. A grant (or a request) whose callerSessionId is ALL_SESSIONS
52
+ // stands for "every session of that machine" on the wire, in memory and on disk. Real caller session
53
+ // ids are UUIDs, so the sentinel can never collide with one — and decide() REFUSES a caller that
54
+ // claims to be '*' or empty, so a box can never impersonate "all sessions".
55
+ const ALL_SESSIONS = '*';
56
+
57
+ let _deps = null;
58
+ // pairKey -> Map<tier, expiresAtMs>. Grants are per-process (a restart revokes everything, R-4).
59
+ const _grants = new Map();
60
+ // pendingId -> row. At most one row per (machineId, callerSessionId, tier) — see PENDING_INDEX.
61
+ const _pending = new Map();
62
+ // `${machineId}\u0000${callerSessionId}\u0000${tier}` -> pendingId. This is what makes R-5's
63
+ // "one row per ask" true without scanning.
64
+ const _pendingIndex = new Map();
65
+ // mtime+size cache for the trust file (R-11: re-read per decision, cheap when unchanged).
66
+ let _trustCache = { file: '', mtimeMs: -1, size: -1, value: null };
67
+ // Every trust-file write (trust PUTs AND the durable grants of D-4) runs through this one chain, so
68
+ // two concurrent writers cannot both read the same "current" and lose one of the two rows.
69
+ let _trustWriteChain = Promise.resolve();
70
+
71
+ const DEFAULT_DEPS = {
72
+ now: () => Date.now(),
73
+ randomUUID: () => crypto.randomUUID(),
74
+ // (frame) => void — the hub browser fan-out (hub-bridge-pending). Absent in tests that only care
75
+ // about the decision.
76
+ broadcast: null,
77
+ dataDir: '',
78
+ };
79
+
80
+ function configure(deps = {}) {
81
+ _deps = { ...DEFAULT_DEPS, ...(deps || {}) };
82
+ return _deps;
83
+ }
84
+
85
+ function _d() {
86
+ return _deps || DEFAULT_DEPS;
87
+ }
88
+
89
+ function _dataDir() {
90
+ const injected = _d().dataDir;
91
+ try {
92
+ return path.resolve(String(injected || getDataDir()));
93
+ } catch {
94
+ // expected: an unresolvable data dir still yields a usable absolute path for the default.
95
+ return path.resolve(getDataDir());
96
+ }
97
+ }
98
+
99
+ /** The trust file's absolute path (R-11). */
100
+ function trustPath() {
101
+ return path.join(_dataDir(), TRUST_FILE);
102
+ }
103
+
104
+ /** The audit log's absolute path (R-9). */
105
+ function auditPath() {
106
+ return path.join(_dataDir(), AUDIT_FILE);
107
+ }
108
+
109
+ /** Ranks for the two tiers an op may declare, in ascending order (exported for the tests). */
110
+ function tierRank(tier) {
111
+ return Object.prototype.hasOwnProperty.call(TIER_RANK, String(tier)) ? TIER_RANK[String(tier)] : -1;
112
+ }
113
+
114
+ function isValidTier(tier) {
115
+ return tierRank(tier) >= 0;
116
+ }
117
+
118
+ /** Does a grant of `grantTier` satisfy a request for `tier`? (R-4: only exec implies read.) */
119
+ function covers(grantTier, tier) {
120
+ const implied = IMPLIES[String(grantTier)];
121
+ return Array.isArray(implied) && implied.includes(String(tier));
122
+ }
123
+
124
+ /** A refusal/validation failure as this module speaks it: the code rides the error, never the message. */
125
+ function _namedError(code, message) {
126
+ return Object.assign(new Error(message), { code });
127
+ }
128
+
129
+ /**
130
+ * Fan one browser frame out (R-5's `hub-bridge-pending`, and its `-cleared` twin). Best-effort: a
131
+ * dead browser socket must never fail the decision that triggered the frame.
132
+ */
133
+ function _broadcast(frame) {
134
+ const broadcast = _d().broadcast;
135
+ if (typeof broadcast !== 'function') return;
136
+ try {
137
+ broadcast(frame);
138
+ } catch {
139
+ // expected: the fan-out is best-effort — a dead browser socket must never fail a decision.
140
+ }
141
+ }
142
+
143
+ /**
144
+ * R-5/R-13: tell every browser which pending rows just went away, so a toolbar banner can hide
145
+ * without a re-read. Fires ONCE per removal batch and NEVER when nothing was removed.
146
+ * @param {string[]} ids the removed pending row ids
147
+ * @returns {boolean} whether a frame was broadcast
148
+ */
149
+ function _broadcastCleared(ids) {
150
+ const list = (Array.isArray(ids) ? ids : []).map((id) => String(id || '')).filter(Boolean);
151
+ if (!list.length) return false;
152
+ _broadcast({ type: 'hub-bridge-pending-cleared', ids: list });
153
+ return true;
154
+ }
155
+
156
+ function _pairKey(machineId, callerSessionId) {
157
+ return `${machineId}\u0000${callerSessionId}`;
158
+ }
159
+
160
+ function _pendingKey(machineId, callerSessionId, tier) {
161
+ return `${machineId}\u0000${callerSessionId}\u0000${tier}`;
162
+ }
163
+
164
+ // ---------------------------------------------------------------------------------------------
165
+ // R-11 — the trust file
166
+ // ---------------------------------------------------------------------------------------------
167
+
168
+ /** The one shape a persisted grant row may take, or null when the row is not one (D-4). */
169
+ function _sanitizeGrantRow(row) {
170
+ if (!row || typeof row !== 'object') return null;
171
+ const machineId = String(row.machineId || '');
172
+ const callerSessionId = String(row.callerSessionId || '');
173
+ const tier = String(row.tier || '');
174
+ if (!machineId || !callerSessionId) return null;
175
+ if (!isValidTier(tier) || tier === 'none') return null;
176
+ return { machineId, callerSessionId, tier, at: Number(row.at) || 0 };
177
+ }
178
+
179
+ /** D-4's `grants`: keep the valid rows, drop exact (machineId, callerSessionId, tier) duplicates. */
180
+ function _sanitizeGrants(input) {
181
+ const rows = Array.isArray(input) ? input : [];
182
+ const out = [];
183
+ const seen = new Set();
184
+ for (const raw of rows) {
185
+ const row = _sanitizeGrantRow(raw);
186
+ if (!row) continue;
187
+ const key = `${row.machineId}\u0000${row.callerSessionId}\u0000${row.tier}`;
188
+ if (seen.has(key)) continue;
189
+ seen.add(key);
190
+ out.push(row);
191
+ }
192
+ return out;
193
+ }
194
+
195
+ /** Coerce anything read off disk (or off the wire) into the R-11 shape; unknown values collapse to ask. */
196
+ function sanitizeTrust(input) {
197
+ const raw = input && typeof input === 'object' ? input : {};
198
+ const out = { v: 1, default: 'ask', machines: {}, grants: [] };
199
+ if (TRUST_VALUES.includes(raw.default)) out.default = raw.default;
200
+ const machines = raw.machines && typeof raw.machines === 'object' ? raw.machines : {};
201
+ for (const [id, value] of Object.entries(machines)) {
202
+ if (!id) continue;
203
+ if (TRUST_VALUES.includes(value)) out.machines[String(id)] = value;
204
+ }
205
+ out.grants = _sanitizeGrants(raw.grants);
206
+ return out;
207
+ }
208
+
209
+ /**
210
+ * The trust policy in force right now. Re-read per call, but only when the file's mtime/size changed
211
+ * (R-11: "the policy is read when each request is handled"). A missing or unreadable file is the
212
+ * documented default (`{v:1, default:'ask', machines:{}}`) — never a crash, never a silent 'always'.
213
+ */
214
+ function readTrust() {
215
+ const file = trustPath();
216
+ let stat = null;
217
+ try {
218
+ stat = fs.statSync(file);
219
+ } catch {
220
+ stat = null; // expected: no trust file yet — the default policy applies
221
+ }
222
+ if (!stat) {
223
+ _trustCache = { file, mtimeMs: -1, size: -1, value: sanitizeTrust(null) };
224
+ return _trustCache.value;
225
+ }
226
+ if (_trustCache.file === file && _trustCache.mtimeMs === stat.mtimeMs && _trustCache.size === stat.size) {
227
+ return _trustCache.value;
228
+ }
229
+ let text = '';
230
+ try {
231
+ text = fs.readFileSync(file, 'utf8');
232
+ } catch {
233
+ text = ''; // REPORT via the default below: an unreadable trust file reads as 'ask' (fail to the ask flow, not to ambient trust)
234
+ }
235
+ let parsed = null;
236
+ try {
237
+ parsed = JSON.parse(text);
238
+ } catch {
239
+ parsed = null; // expected: a half-written or hand-broken file collapses to the default policy
240
+ }
241
+ const value = sanitizeTrust(parsed);
242
+ _trustCache = { file, mtimeMs: stat.mtimeMs, size: stat.size, value };
243
+ return value;
244
+ }
245
+
246
+ /** The trust value for one machine: its own row, else the file default (R-11). */
247
+ function trustFor(machineId) {
248
+ const trust = readTrust();
249
+ const id = String(machineId || '');
250
+ const value = Object.prototype.hasOwnProperty.call(trust.machines, id) ? trust.machines[id] : trust.default;
251
+ return TRUST_VALUES.includes(value) ? value : 'ask';
252
+ }
253
+
254
+ /**
255
+ * Merge-and-write the trust policy atomically (tmp + rename, mode 0600). `patch.machines` is merged
256
+ * per id; a `null`/`''` value for an id REMOVES that id's row (back to the default). Values are
257
+ * validated against ask|always|never and a bad one rejects `trust_value_invalid` (the route answers
258
+ * 400). `patch.grants` is IGNORED: the durable grants of D-4 ride through untouched, so a trust PUT
259
+ * can neither add nor drop one (only grantGrants/revokeGrants write grants). Async: a write never
260
+ * runs on the hub's main loop (a loaded disk must not freeze the UI).
261
+ */
262
+ async function writeTrust(patch = {}) {
263
+ const raw = patch && typeof patch === 'object' ? patch : {};
264
+ if (raw.default !== undefined && !TRUST_VALUES.includes(raw.default)) {
265
+ throw _namedError('trust_value_invalid', `default must be one of ${TRUST_VALUES.join('|')}`);
266
+ }
267
+ const incoming = raw.machines && typeof raw.machines === 'object' ? raw.machines : {};
268
+ for (const [id, value] of Object.entries(incoming)) {
269
+ if (!id || value === null || value === '') continue;
270
+ if (!TRUST_VALUES.includes(value)) {
271
+ throw _namedError('trust_value_invalid', `machines['${id}'] must be one of ${TRUST_VALUES.join('|')}`);
272
+ }
273
+ }
274
+ const next = await _chainTrustWrite(async () => {
275
+ const current = readTrust();
276
+ const machines = { ...current.machines };
277
+ for (const [id, value] of Object.entries(incoming)) {
278
+ if (!id) continue;
279
+ if (value === null || value === '') {
280
+ delete machines[id];
281
+ continue;
282
+ }
283
+ machines[id] = value;
284
+ }
285
+ const value = sanitizeTrust({
286
+ v: 1,
287
+ default: raw.default !== undefined ? raw.default : current.default,
288
+ machines,
289
+ // D-4: a trust PUT can never add or drop a grant — the durable rows ride through untouched.
290
+ grants: current.grants,
291
+ });
292
+ await _writeTrustFile(value);
293
+ return value;
294
+ });
295
+ _clearPendingTrusted();
296
+ return next;
297
+ }
298
+
299
+ /**
300
+ * Run one trust-file write at a time. The chain NEVER rejects (a failed write must not poison the
301
+ * queue for the writes behind it); the caller still sees its own rejection through the returned
302
+ * promise. Both writeTrust and the durable grants of D-4 go through here.
303
+ * @template T
304
+ * @param {() => Promise<T>} run
305
+ * @returns {Promise<T>}
306
+ */
307
+ function _chainTrustWrite(run) {
308
+ const result = _trustWriteChain.then(run);
309
+ _trustWriteChain = result.then(() => {}, () => {});
310
+ return result;
311
+ }
312
+
313
+ /**
314
+ * Write the whole trust object atomically (tmp + rename, mode 0600) and become the cache's truth.
315
+ * Shared by writeTrust and the `always` grant writes, so both leave the exact same file on disk.
316
+ * @param {{v:number, default:string, machines:object, grants:Array}} next
317
+ */
318
+ async function _writeTrustFile(next) {
319
+ const file = trustPath();
320
+ await fs.promises.mkdir(path.dirname(file), { recursive: true });
321
+ // G-5's spirit (no partial file ever visible): write a sibling tmp then rename over the target.
322
+ const tmp = `${file}.tmp-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
323
+ await fs.promises.writeFile(tmp, `${JSON.stringify(next, null, 2)}\n`, { mode: TRUST_FILE_MODE });
324
+ await fs.promises.rename(tmp, file);
325
+ // The rename may keep the same mtime tick; trust the value we just wrote rather than the stat cache.
326
+ _trustCache = { file, mtimeMs: -1, size: -1, value: next };
327
+ return next;
328
+ }
329
+
330
+ /**
331
+ * A trust write that makes a machine 'always' has answered its open asks (the [Always allow] button, or
332
+ * the card select / page switch): drop them and broadcast ONE cleared frame, so no banner keeps asking.
333
+ */
334
+ function _clearPendingTrusted() {
335
+ const cleared = [];
336
+ for (const [id, row] of [..._pending.entries()]) {
337
+ if (trustFor(row.machineId) !== 'always') continue;
338
+ _pending.delete(id);
339
+ _pendingIndex.delete(_pendingKey(row.machineId, row.callerSessionId, row.tier));
340
+ cleared.push(id);
341
+ }
342
+ _broadcastCleared(cleared);
343
+ return cleared.length > 0;
344
+ }
345
+
346
+ // ---------------------------------------------------------------------------------------------
347
+ // R-4 — grants (D-1: one session or the whole machine × a TTL or `always`)
348
+ // ---------------------------------------------------------------------------------------------
349
+
350
+ /** D-1: the scope a callerSessionId names — '*' is every session of the machine. */
351
+ function _scopeOf(callerSessionId) {
352
+ return callerSessionId === ALL_SESSIONS ? 'machine' : 'session';
353
+ }
354
+
355
+ /** D-1's wire/route shape of one grant row. */
356
+ function _grantRow({ machineId, callerSessionId, tier, always, ttlSec, expiresAt }) {
357
+ return {
358
+ machineId,
359
+ callerSessionId,
360
+ tier,
361
+ scope: _scopeOf(callerSessionId),
362
+ always: always === true,
363
+ ttlSec: always === true ? null : ttlSec,
364
+ expiresAt: always === true ? null : expiresAt,
365
+ };
366
+ }
367
+
368
+ /** Does a durable (D-4) row for this exact pair cover `tier`? The trust file is the source of truth. */
369
+ function _persistedGrant(machineId, callerSessionId, tier) {
370
+ return readTrust().grants.some((row) => row.machineId === machineId
371
+ && row.callerSessionId === callerSessionId
372
+ && covers(row.tier, tier));
373
+ }
374
+
375
+ function _clampTtlSec(value) {
376
+ const n = Number(value);
377
+ if (!Number.isFinite(n) || n <= 0) return DEFAULT_TTL_SEC;
378
+ return Math.min(MAX_TTL_SEC, Math.max(1, Math.trunc(n)));
379
+ }
380
+
381
+ /** The live grant covering `tier` for this pair, or null. Expired rows are dropped as they are seen. */
382
+ function _activeGrant(pairKey, tier) {
383
+ const byTier = _grants.get(pairKey);
384
+ if (!byTier) return null;
385
+ const nowMs = Number(_d().now()) || 0;
386
+ let found = null;
387
+ for (const [grantTier, expiresAt] of [...byTier.entries()]) {
388
+ if (!(Number(expiresAt) > nowMs)) {
389
+ byTier.delete(grantTier);
390
+ continue;
391
+ }
392
+ if (covers(grantTier, tier)) found = { tier: grantTier, expiresAt };
393
+ }
394
+ if (!byTier.size) _grants.delete(pairKey);
395
+ return found;
396
+ }
397
+
398
+ /**
399
+ * Mint (or refresh) a grant on both axes (D-1). `callerSessionId` is one session — or ALL_SESSIONS
400
+ * for every session of the machine.
401
+ * - `always !== true`: an in-memory TTL grant as before. `ttlSec` defaults to 3600 and is capped at
402
+ * 28800 (R-4); a restart drops it.
403
+ * - `always === true`: a durable row `{machineId, callerSessionId, tier, at}` appended to the trust
404
+ * file (D-4) through the same atomic write as writeTrust, idempotent per triple; `ttlSec` is
405
+ * ignored. It never touches `machines`, never calls the mesh, and survives a restart.
406
+ * Both clear the pending rows the grant answers (D-5) and broadcast ONE cleared frame.
407
+ * @returns {Promise<{machineId:string, callerSessionId:string, tier:string, scope:'session'|'machine',
408
+ * always:boolean, ttlSec:number|null, expiresAt:number|null}>}
409
+ */
410
+ async function grantGrants({ machineId, callerSessionId, tier, ttlSec, always } = {}) {
411
+ const id = String(machineId || '');
412
+ const session = String(callerSessionId || '');
413
+ const wanted = String(tier || '');
414
+ if (!id || !session) throw _namedError('grant_invalid', 'machineId and callerSessionId are required');
415
+ if (!isValidTier(wanted) || wanted === 'none') {
416
+ throw _namedError('grant_invalid', `tier must be one of ${Object.keys(TIER_RANK).filter((t) => t !== 'none').join('|')}`);
417
+ }
418
+ if (always === true) {
419
+ const at = Number(_d().now()) || 0;
420
+ await _chainTrustWrite(async () => {
421
+ const current = readTrust();
422
+ const rows = current.grants.slice();
423
+ const known = rows.some((row) => row.machineId === id && row.callerSessionId === session && row.tier === wanted);
424
+ if (!known) rows.push({ machineId: id, callerSessionId: session, tier: wanted, at });
425
+ return _writeTrustFile(sanitizeTrust({
426
+ v: 1,
427
+ default: current.default,
428
+ machines: current.machines,
429
+ grants: rows,
430
+ }));
431
+ });
432
+ _clearPending(id, session, wanted);
433
+ return _grantRow({ machineId: id, callerSessionId: session, tier: wanted, always: true });
434
+ }
435
+ const ttl = _clampTtlSec(ttlSec);
436
+ const expiresAt = (Number(_d().now()) || 0) + ttl * 1000;
437
+ const pairKey = _pairKey(id, session);
438
+ const byTier = _grants.get(pairKey) || new Map();
439
+ byTier.set(wanted, expiresAt);
440
+ _grants.set(pairKey, byTier);
441
+ _clearPending(id, session, wanted);
442
+ return _grantRow({ machineId: id, callerSessionId: session, tier: wanted, always: false, ttlSec: ttl, expiresAt });
443
+ }
444
+
445
+ /**
446
+ * Drop every pending row this grant answers (D-5). A machine-scoped grant (callerSessionId ===
447
+ * ALL_SESSIONS) answers every session of that machine; otherwise the match is the strict pair.
448
+ */
449
+ function _clearPending(machineId, callerSessionId, grantedTier) {
450
+ const everySession = callerSessionId === ALL_SESSIONS;
451
+ const cleared = [];
452
+ for (const [id, row] of [..._pending.entries()]) {
453
+ if (row.machineId !== machineId) continue;
454
+ if (!everySession && row.callerSessionId !== callerSessionId) continue;
455
+ if (!covers(grantedTier, row.tier)) continue;
456
+ _pending.delete(id);
457
+ _pendingIndex.delete(_pendingKey(row.machineId, row.callerSessionId, row.tier));
458
+ cleared.push(id);
459
+ }
460
+ // R-13: one frame per removal batch, and none at all when the grant answered no open ask.
461
+ _broadcastCleared(cleared);
462
+ return cleared.length > 0;
463
+ }
464
+
465
+ /**
466
+ * Revoke grants for a pair. With no `tier`, every tier is dropped; with one, that tier and every
467
+ * grant that covers it is dropped (revoking `read` also drops `exec`, matching how a grant is honoured).
468
+ * Both the in-memory TTL rows and the durable rows of D-4 go — the trust file is rewritten ONLY when
469
+ * a durable row was actually removed, so revoking an hour grant never touches the disk.
470
+ * @returns {Promise<number>} how many grant rows were removed (both kinds together)
471
+ */
472
+ async function revokeGrants({ machineId, callerSessionId, tier } = {}) {
473
+ const id = String(machineId || '');
474
+ const session = String(callerSessionId || '');
475
+ const wanted = String(tier || '');
476
+ const pairKey = _pairKey(id, session);
477
+ const byTier = _grants.get(pairKey);
478
+ let removed = 0;
479
+ if (byTier) {
480
+ for (const grantTier of [...byTier.keys()]) {
481
+ if (!wanted || covers(grantTier, wanted)) {
482
+ byTier.delete(grantTier);
483
+ removed += 1;
484
+ }
485
+ }
486
+ if (!byTier.size) _grants.delete(pairKey);
487
+ }
488
+ const persisted = await _chainTrustWrite(async () => {
489
+ const current = readTrust();
490
+ const kept = [];
491
+ let dropped = 0;
492
+ for (const row of current.grants) {
493
+ if (row.machineId === id && row.callerSessionId === session && (!wanted || covers(row.tier, wanted))) {
494
+ dropped += 1;
495
+ continue;
496
+ }
497
+ kept.push(row);
498
+ }
499
+ if (!dropped) return 0;
500
+ await _writeTrustFile(sanitizeTrust({
501
+ v: 1,
502
+ default: current.default,
503
+ machines: current.machines,
504
+ grants: kept,
505
+ }));
506
+ return dropped;
507
+ });
508
+ return removed + persisted;
509
+ }
510
+
511
+ /**
512
+ * Every live grant: the in-memory TTL rows as before, plus the durable rows of D-4 (`always:true`,
513
+ * `ttlSec`/`expiresAt` null, read straight off the trust file). Sort (D-1's display order): machine,
514
+ * then session rows before the '*' machine rows, then callerSessionId, then tier rank, then `always`
515
+ * last inside a triple.
516
+ */
517
+ function listGrants() {
518
+ const nowMs = Number(_d().now()) || 0;
519
+ const rows = [];
520
+ for (const byTier of _grants.values()) {
521
+ for (const [tier, expiresAt] of [...byTier.entries()]) {
522
+ if (!(Number(expiresAt) > nowMs)) byTier.delete(tier);
523
+ }
524
+ }
525
+ for (const [pairKey, byTier] of [..._grants.entries()]) {
526
+ if (!byTier.size) {
527
+ _grants.delete(pairKey);
528
+ continue;
529
+ }
530
+ const [machineId, callerSessionId] = pairKey.split('\u0000');
531
+ for (const [tier, expiresAt] of byTier.entries()) {
532
+ rows.push({
533
+ machineId,
534
+ callerSessionId,
535
+ tier,
536
+ scope: _scopeOf(callerSessionId),
537
+ always: false,
538
+ ttlSec: Math.max(0, Math.round((expiresAt - nowMs) / 1000)),
539
+ expiresAt,
540
+ });
541
+ }
542
+ }
543
+ for (const row of readTrust().grants) {
544
+ rows.push(_grantRow({ ...row, always: true }));
545
+ }
546
+ rows.sort((a, b) => (a.machineId.localeCompare(b.machineId)
547
+ || (a.scope === b.scope ? 0 : a.scope === 'session' ? -1 : 1)
548
+ || a.callerSessionId.localeCompare(b.callerSessionId)
549
+ || tierRank(a.tier) - tierRank(b.tier)
550
+ || (a.always === b.always ? 0 : a.always ? 1 : -1)));
551
+ return rows;
552
+ }
553
+
554
+ // ---------------------------------------------------------------------------------------------
555
+ // R-5 — pending asks
556
+ // ---------------------------------------------------------------------------------------------
557
+
558
+ /** The one-line human summary an ask carries: the first 120 chars of the command or path. */
559
+ function summarizeArgs(args) {
560
+ const raw = args && typeof args === 'object' ? args : {};
561
+ const text = String(raw.command || raw.path || raw.ref || '').replace(/\s+/g, ' ').trim();
562
+ return text.slice(0, SUMMARY_MAX);
563
+ }
564
+
565
+ /**
566
+ * Create or REUSE the single pending row for (machineId, callerSessionId, tier) and broadcast it
567
+ * ONLY when it is new (R-5: an agent polling in a loop must not spam the toolbar).
568
+ */
569
+ function _requestPending({ machineId, callerSessionId, tier, op, machineName, summary }) {
570
+ const key = _pendingKey(machineId, callerSessionId, tier);
571
+ const existingId = _pendingIndex.get(key);
572
+ if (existingId && _pending.has(existingId)) return _pending.get(existingId);
573
+ const row = {
574
+ id: _d().randomUUID(),
575
+ machineId,
576
+ machineName: String(machineName || ''),
577
+ callerSessionId,
578
+ tier,
579
+ op: String(op || ''),
580
+ summary: String(summary || '').slice(0, SUMMARY_MAX),
581
+ at: Number(_d().now()) || 0,
582
+ };
583
+ _pending.set(row.id, row);
584
+ _pendingIndex.set(key, row.id);
585
+ _broadcast({ type: 'hub-bridge-pending', pending: row });
586
+ return row;
587
+ }
588
+
589
+ function listPending({ machineId } = {}) {
590
+ const rows = [..._pending.values()];
591
+ const id = String(machineId || '');
592
+ return (id ? rows.filter((row) => row.machineId === id) : rows).map((row) => ({ ...row }));
593
+ }
594
+
595
+ /** Clear one pending ask (the toolbar's Deny). Returns whether a row was actually removed. */
596
+ function denyPending(id) {
597
+ const key = String(id || '');
598
+ const row = _pending.get(key);
599
+ if (!row) return false;
600
+ _pending.delete(key);
601
+ _pendingIndex.delete(_pendingKey(row.machineId, row.callerSessionId, row.tier));
602
+ // R-13: the toolbar hides this row's banner on the frame, not on a re-read.
603
+ _broadcastCleared([key]);
604
+ return true;
605
+ }
606
+
607
+ // ---------------------------------------------------------------------------------------------
608
+ // R-9 — audit
609
+ // ---------------------------------------------------------------------------------------------
610
+
611
+ /** sha256(JSON(args)) — the ONLY form of the args that ever reaches disk (R-9). */
612
+ function digestArgs(args) {
613
+ let text;
614
+ try {
615
+ text = JSON.stringify(args === undefined ? null : args);
616
+ } catch {
617
+ text = '"[unserialisable]"';
618
+ }
619
+ return crypto.createHash('sha256').update(String(text), 'utf8').digest('hex');
620
+ }
621
+
622
+ // Appends are chained so lines land in call order without a sync write on the main loop.
623
+ let _auditChain = Promise.resolve();
624
+
625
+ /** Resolves once every audit line queued so far is on disk (recentAudit and the tests await it). */
626
+ function flushAudit() {
627
+ return _auditChain;
628
+ }
629
+
630
+ /**
631
+ * Queue one JSONL audit line and return the row. `entry.args` is digested here and never written;
632
+ * every other field is copied verbatim. Best-effort: a failed write must never fail the op it
633
+ * describes.
634
+ */
635
+ function audit(entry = {}) {
636
+ const row = {
637
+ at: Number(entry.at) || Number(_d().now()) || 0,
638
+ machineId: String(entry.machineId || ''),
639
+ callerSessionId: String(entry.callerSessionId || ''),
640
+ op: String(entry.op || ''),
641
+ tier: String(entry.tier || 'none'),
642
+ via: String(entry.via || 'none'),
643
+ argsDigest: digestArgs(entry.args),
644
+ ok: entry.ok === true,
645
+ code: String(entry.code || ''),
646
+ ms: Math.max(0, Number(entry.ms) || 0),
647
+ };
648
+ const file = auditPath();
649
+ const line = `${JSON.stringify(row)}\n`;
650
+ _auditChain = _auditChain
651
+ .then(() => fs.promises.mkdir(path.dirname(file), { recursive: true }))
652
+ .then(() => fs.promises.appendFile(file, line))
653
+ .catch(() => {
654
+ // expected: the audit sink is best-effort (R-9) — a full disk must not fail a granted op.
655
+ });
656
+ return row;
657
+ }
658
+
659
+ /**
660
+ * The newest audit rows for one machine, newest first, from at most the last 256 KiB of the file.
661
+ * A line that straddles the read window (or is torn) is dropped rather than surfacing as garbage.
662
+ */
663
+ async function recentAudit({ machineId, limit = AUDIT_DEFAULT_LIMIT } = {}) {
664
+ await flushAudit();
665
+ const file = auditPath();
666
+ const cap = Math.min(AUDIT_MAX_LIMIT, Math.max(1, Math.trunc(Number(limit) || AUDIT_DEFAULT_LIMIT)));
667
+ let handle = null;
668
+ let text = '';
669
+ let startedMidFile = false;
670
+ try {
671
+ handle = await fs.promises.open(file, 'r');
672
+ const stat = await handle.stat();
673
+ const start = Math.max(0, stat.size - AUDIT_TAIL_BYTES);
674
+ startedMidFile = start > 0;
675
+ const length = stat.size - start;
676
+ if (length <= 0) return [];
677
+ const buffer = Buffer.alloc(length);
678
+ await handle.read(buffer, 0, length, start);
679
+ text = buffer.toString('utf8');
680
+ } catch {
681
+ // REPORT via []: no audit file yet (or an unreadable one) is an empty history, not an error.
682
+ return [];
683
+ } finally {
684
+ if (handle) {
685
+ try { await handle.close(); } catch { /* expected: already closed */ }
686
+ }
687
+ }
688
+ const lines = text.split('\n');
689
+ if (startedMidFile) lines.shift(); // the first line may be a fragment of a row above the window
690
+ const wanted = String(machineId || '');
691
+ const rows = [];
692
+ for (const line of lines) {
693
+ if (!line.trim()) continue;
694
+ let parsed = null;
695
+ try {
696
+ parsed = JSON.parse(line);
697
+ } catch {
698
+ continue; // expected: a torn trailing line (another writer mid-append) is skipped
699
+ }
700
+ if (wanted && String(parsed.machineId || '') !== wanted) continue;
701
+ rows.push(parsed);
702
+ }
703
+ rows.reverse(); // newest first
704
+ return rows.slice(0, cap);
705
+ }
706
+
707
+ // ---------------------------------------------------------------------------------------------
708
+ // The decision
709
+ // ---------------------------------------------------------------------------------------------
710
+
711
+ /**
712
+ * R-4/R-11/D-3/D-5: decide one request. Order is fixed (D-5): an invalid tier or a caller that claims
713
+ * to be "all sessions" is refused outright; then machine trust; then the four grant lookups (hour
714
+ * session, hour '*', durable session, durable '*'); then the ask. Every grant hit reports via 'grant'.
715
+ * Still SYNCHRONOUS — it rides the op's request path (the trust file's cached sync read is why).
716
+ *
717
+ * @param {{machineId:string, callerSessionId:string, tier:string, op?:string, machineName?:string,
718
+ * args?:object, summary?:string}} request
719
+ * @returns {{allow:true, via:'trust'|'grant'|'none'} |
720
+ * {allow:false, code:'hub_bridge_denied'} |
721
+ * {allow:false, code:'hub_grant_required', pendingId:string, tier:string}}
722
+ */
723
+ function decide(request = {}) {
724
+ const machineId = String(request.machineId || '');
725
+ const callerSessionId = String(request.callerSessionId || '');
726
+ const tier = String(request.tier || 'none');
727
+ if (!isValidTier(tier)) return { allow: false, code: 'hub_bridge_denied' };
728
+ if (tier === 'none') return { allow: true, via: 'none' };
729
+ // D-3: '*' is the machine-scope SENTINEL, never a caller. A box claiming it (or claiming nothing)
730
+ // is refused before any trust/grant read, and creates NO pending row — it can never impersonate
731
+ // "all sessions" and thereby answer somebody else's ask.
732
+ if (!callerSessionId || callerSessionId === ALL_SESSIONS) return { allow: false, code: 'hub_bridge_denied' };
733
+
734
+ const trust = trustFor(machineId);
735
+ if (trust === 'never') return { allow: false, code: 'hub_bridge_denied' };
736
+ if (trust === 'always') return { allow: true, via: 'trust' };
737
+
738
+ // D-5: this session's own hour grant, then the machine's hour grant, then the two durable kinds.
739
+ if (_activeGrant(_pairKey(machineId, callerSessionId), tier)) return { allow: true, via: 'grant' };
740
+ if (_activeGrant(_pairKey(machineId, ALL_SESSIONS), tier)) return { allow: true, via: 'grant' };
741
+ if (_persistedGrant(machineId, callerSessionId, tier)) return { allow: true, via: 'grant' };
742
+ if (_persistedGrant(machineId, ALL_SESSIONS, tier)) return { allow: true, via: 'grant' };
743
+
744
+ const pending = _requestPending({
745
+ machineId,
746
+ callerSessionId,
747
+ tier,
748
+ op: request.op,
749
+ machineName: request.machineName,
750
+ summary: request.summary !== undefined ? request.summary : summarizeArgs(request.args),
751
+ });
752
+ return { allow: false, code: 'hub_grant_required', pendingId: pending.id, tier };
753
+ }
754
+
755
+ function _resetForTests() {
756
+ _deps = null;
757
+ _grants.clear();
758
+ _pending.clear();
759
+ _pendingIndex.clear();
760
+ _trustCache = { file: '', mtimeMs: -1, size: -1, value: null };
761
+ _trustWriteChain = Promise.resolve();
762
+ _auditChain = Promise.resolve();
763
+ }
764
+
765
+ module.exports = {
766
+ TRUST_FILE,
767
+ AUDIT_FILE,
768
+ TRUST_VALUES,
769
+ ALL_SESSIONS,
770
+ TIER_RANK,
771
+ DEFAULT_TTL_SEC,
772
+ MAX_TTL_SEC,
773
+ SUMMARY_MAX,
774
+ AUDIT_TAIL_BYTES,
775
+ AUDIT_DEFAULT_LIMIT,
776
+ configure,
777
+ tierRank,
778
+ isValidTier,
779
+ covers,
780
+ trustPath,
781
+ auditPath,
782
+ sanitizeTrust,
783
+ readTrust,
784
+ trustFor,
785
+ writeTrust,
786
+ grantGrants,
787
+ revokeGrants,
788
+ listGrants,
789
+ summarizeArgs,
790
+ listPending,
791
+ denyPending,
792
+ digestArgs,
793
+ audit,
794
+ flushAudit,
795
+ recentAudit,
796
+ decide,
797
+ _resetForTests,
798
+ };