@huynle/hub 0.6.1 → 0.7.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 (355) hide show
  1. package/CHANGELOG.md +870 -0
  2. package/Dockerfile +33 -0
  3. package/README.md +1007 -66
  4. package/package.json +47 -2
  5. package/packages/claude-code/package.json +12 -0
  6. package/packages/claude-code/src/bridge.ts +79 -0
  7. package/packages/claude-code/src/daemon.ts +339 -0
  8. package/packages/claude-code/src/hook-input.ts +44 -0
  9. package/packages/claude-code/src/ids.ts +27 -0
  10. package/packages/claude-code/src/index.ts +18 -0
  11. package/packages/claude-code/src/launcher.ts +265 -0
  12. package/packages/claude-code/src/main.ts +127 -0
  13. package/packages/claude-code/src/mcp.ts +109 -0
  14. package/packages/claude-code/src/paths.ts +34 -0
  15. package/packages/claude-code/src/plugin.ts +99 -0
  16. package/packages/claude-code/src/references.ts +51 -0
  17. package/packages/claude-code/src/sessions.ts +600 -0
  18. package/packages/claude-code/src/tools.ts +66 -0
  19. package/packages/claude-code/src/wire.ts +71 -0
  20. package/packages/claude-code/src/wrapper.ts +156 -0
  21. package/packages/cli/dist/hub-cli.js +21647 -0
  22. package/packages/cli/package.json +11 -0
  23. package/packages/cli/src/acting.ts +70 -0
  24. package/packages/cli/src/args.ts +91 -0
  25. package/packages/cli/src/bootstrap/adapters/archive-stream.ts +276 -0
  26. package/packages/cli/src/bootstrap/adapters/claude/README.md +63 -0
  27. package/packages/cli/src/bootstrap/adapters/claude/adapter.ts +250 -0
  28. package/packages/cli/src/bootstrap/adapters/claude/index.ts +42 -0
  29. package/packages/cli/src/bootstrap/adapters/claude/node.ts +99 -0
  30. package/packages/cli/src/bootstrap/adapters/claude/openpgp.ts +221 -0
  31. package/packages/cli/src/bootstrap/adapters/claude/ports.ts +44 -0
  32. package/packages/cli/src/bootstrap/adapters/claude/source.ts +160 -0
  33. package/packages/cli/src/bootstrap/adapters/claude/verify.ts +165 -0
  34. package/packages/cli/src/bootstrap/adapters/codex/archive.ts +103 -0
  35. package/packages/cli/src/bootstrap/adapters/codex/index.ts +60 -0
  36. package/packages/cli/src/bootstrap/adapters/codex/plan.ts +129 -0
  37. package/packages/cli/src/bootstrap/adapters/codex/sigstore.ts +298 -0
  38. package/packages/cli/src/bootstrap/adapters/codex/source.ts +102 -0
  39. package/packages/cli/src/bootstrap/adapters/codex/verify.ts +298 -0
  40. package/packages/cli/src/bootstrap/adapters/inflate.ts +433 -0
  41. package/packages/cli/src/bootstrap/adapters/node-services/control.ts +392 -0
  42. package/packages/cli/src/bootstrap/adapters/node-services/index.ts +23 -0
  43. package/packages/cli/src/bootstrap/adapters/node-services/launch.ts +223 -0
  44. package/packages/cli/src/bootstrap/adapters/node-services/plan.ts +222 -0
  45. package/packages/cli/src/bootstrap/adapters/node-services/render.ts +183 -0
  46. package/packages/cli/src/bootstrap/adapters/node-services/validate.ts +197 -0
  47. package/packages/cli/src/bootstrap/adapters/opencode/README.md +204 -0
  48. package/packages/cli/src/bootstrap/adapters/opencode/apply.ts +205 -0
  49. package/packages/cli/src/bootstrap/adapters/opencode/archive.ts +91 -0
  50. package/packages/cli/src/bootstrap/adapters/opencode/config.ts +212 -0
  51. package/packages/cli/src/bootstrap/adapters/opencode/index.ts +81 -0
  52. package/packages/cli/src/bootstrap/adapters/opencode/plan.ts +266 -0
  53. package/packages/cli/src/bootstrap/adapters/opencode/verify.ts +208 -0
  54. package/packages/cli/src/bootstrap/adapters/readiness.ts +19 -0
  55. package/packages/cli/src/bootstrap/ansible/index.ts +2 -0
  56. package/packages/cli/src/bootstrap/ansible/render.ts +279 -0
  57. package/packages/cli/src/bootstrap/contracts/actions.ts +470 -0
  58. package/packages/cli/src/bootstrap/contracts/artifact.ts +323 -0
  59. package/packages/cli/src/bootstrap/contracts/canonical.ts +54 -0
  60. package/packages/cli/src/bootstrap/contracts/credentials.ts +108 -0
  61. package/packages/cli/src/bootstrap/contracts/discovery.ts +215 -0
  62. package/packages/cli/src/bootstrap/contracts/envelope.ts +143 -0
  63. package/packages/cli/src/bootstrap/contracts/exec.ts +74 -0
  64. package/packages/cli/src/bootstrap/contracts/harness.ts +202 -0
  65. package/packages/cli/src/bootstrap/contracts/index.ts +28 -0
  66. package/packages/cli/src/bootstrap/contracts/journal.ts +198 -0
  67. package/packages/cli/src/bootstrap/contracts/paths.ts +91 -0
  68. package/packages/cli/src/bootstrap/contracts/plan.ts +155 -0
  69. package/packages/cli/src/bootstrap/contracts/platform.ts +62 -0
  70. package/packages/cli/src/bootstrap/contracts/preview.ts +190 -0
  71. package/packages/cli/src/bootstrap/contracts/profile.ts +105 -0
  72. package/packages/cli/src/bootstrap/contracts/secrets.ts +52 -0
  73. package/packages/cli/src/bootstrap/contracts/validate.ts +212 -0
  74. package/packages/cli/src/bootstrap/discovery/discover.ts +146 -0
  75. package/packages/cli/src/bootstrap/discovery/executables.ts +134 -0
  76. package/packages/cli/src/bootstrap/discovery/host.ts +96 -0
  77. package/packages/cli/src/bootstrap/discovery/index.ts +6 -0
  78. package/packages/cli/src/bootstrap/discovery/node-probe.ts +97 -0
  79. package/packages/cli/src/bootstrap/discovery/probe.ts +41 -0
  80. package/packages/cli/src/bootstrap/discovery/util.ts +13 -0
  81. package/packages/cli/src/bootstrap/enrollment/enroll.ts +226 -0
  82. package/packages/cli/src/bootstrap/enrollment/index.ts +7 -0
  83. package/packages/cli/src/bootstrap/enrollment/policy.ts +54 -0
  84. package/packages/cli/src/bootstrap/enrollment/result.ts +63 -0
  85. package/packages/cli/src/bootstrap/enrollment/status.ts +119 -0
  86. package/packages/cli/src/bootstrap/enrollment/store.ts +173 -0
  87. package/packages/cli/src/bootstrap/preview/index.ts +5 -0
  88. package/packages/cli/src/bootstrap/preview/links.ts +31 -0
  89. package/packages/cli/src/bootstrap/preview/plan.ts +242 -0
  90. package/packages/cli/src/bootstrap/preview/preview.ts +104 -0
  91. package/packages/cli/src/bootstrap/preview/render.ts +30 -0
  92. package/packages/cli/src/bootstrap/profiles/audit.ts +87 -0
  93. package/packages/cli/src/bootstrap/profiles/catalog.ts +49 -0
  94. package/packages/cli/src/bootstrap/profiles/desired.ts +96 -0
  95. package/packages/cli/src/bootstrap/profiles/drift.ts +286 -0
  96. package/packages/cli/src/bootstrap/profiles/history.ts +263 -0
  97. package/packages/cli/src/bootstrap/profiles/index.ts +11 -0
  98. package/packages/cli/src/bootstrap/profiles/upgrade.ts +276 -0
  99. package/packages/cli/src/bootstrap/profiles/versions.ts +50 -0
  100. package/packages/cli/src/bootstrap/release/README.md +115 -0
  101. package/packages/cli/src/bootstrap/release/assets.ts +39 -0
  102. package/packages/cli/src/bootstrap/release/cli.ts +332 -0
  103. package/packages/cli/src/bootstrap/release/harness-pins.json +94 -0
  104. package/packages/cli/src/bootstrap/release/manifest.ts +118 -0
  105. package/packages/cli/src/bootstrap/release/pins.ts +192 -0
  106. package/packages/cli/src/bootstrap/release/text-assets.d.ts +9 -0
  107. package/packages/cli/src/bootstrap/release/trust-root.json +13 -0
  108. package/packages/cli/src/bootstrap/release/trust-root.ts +39 -0
  109. package/packages/cli/src/bootstrap/release/verify.ts +88 -0
  110. package/packages/cli/src/bootstrap/transaction/builtin.ts +312 -0
  111. package/packages/cli/src/bootstrap/transaction/engine.ts +994 -0
  112. package/packages/cli/src/bootstrap/transaction/executor.ts +112 -0
  113. package/packages/cli/src/bootstrap/transaction/fs.ts +194 -0
  114. package/packages/cli/src/bootstrap/transaction/index.ts +12 -0
  115. package/packages/cli/src/bootstrap/transaction/json-merge.ts +403 -0
  116. package/packages/cli/src/bootstrap/transaction/lock.ts +132 -0
  117. package/packages/cli/src/bootstrap/transaction/resolve.ts +48 -0
  118. package/packages/cli/src/bootstrap/transaction/result.ts +60 -0
  119. package/packages/cli/src/bootstrap/transaction/state.ts +88 -0
  120. package/packages/cli/src/bootstrap/transaction/store.ts +189 -0
  121. package/packages/cli/src/bootstrap/transaction/stream.ts +254 -0
  122. package/packages/cli/src/cli.ts +814 -0
  123. package/packages/cli/src/credentials.ts +75 -0
  124. package/packages/cli/src/decisions.ts +106 -0
  125. package/packages/cli/src/format.ts +185 -0
  126. package/packages/cli/src/grants.ts +105 -0
  127. package/packages/cli/src/http.ts +86 -0
  128. package/packages/cli/src/launches.ts +157 -0
  129. package/packages/cli/src/login.ts +160 -0
  130. package/packages/cli/src/main.ts +63 -0
  131. package/packages/cli/src/mcp.ts +879 -0
  132. package/packages/cli/src/messaging.ts +108 -0
  133. package/packages/cli/src/node.ts +279 -0
  134. package/packages/cli/src/openshell.ts +166 -0
  135. package/packages/cli/src/setup/commands.ts +809 -0
  136. package/packages/cli/src/setup/contributors.ts +171 -0
  137. package/packages/cli/src/setup/dispatch.ts +74 -0
  138. package/packages/cli/src/setup/launch-driver.ts +109 -0
  139. package/packages/cli/src/setup/runtime.ts +456 -0
  140. package/packages/cli/src/setup/spoke-probe.ts +120 -0
  141. package/packages/cli/src/setup/state.ts +244 -0
  142. package/packages/cli/src/setup/user-bin.ts +39 -0
  143. package/packages/cli/src/spoke-services.ts +109 -0
  144. package/packages/cli/src/watch.ts +28 -0
  145. package/packages/codex/package.json +12 -0
  146. package/packages/codex/src/app-server.ts +44 -0
  147. package/packages/codex/src/ids.ts +28 -0
  148. package/packages/codex/src/index.ts +11 -0
  149. package/packages/codex/src/jsonrpc.ts +168 -0
  150. package/packages/codex/src/main.ts +123 -0
  151. package/packages/codex/src/paths.ts +65 -0
  152. package/packages/codex/src/sessions.ts +578 -0
  153. package/packages/codex/src/spoke.ts +252 -0
  154. package/packages/codex/src/stdio.ts +101 -0
  155. package/packages/codex/src/websocket.ts +206 -0
  156. package/packages/codex/src/wrapper.ts +104 -0
  157. package/packages/node/package.json +12 -0
  158. package/packages/node/src/config.ts +184 -0
  159. package/packages/node/src/connection.ts +283 -0
  160. package/packages/node/src/daemon.ts +281 -0
  161. package/packages/node/src/git.ts +107 -0
  162. package/packages/node/src/harnesses.ts +95 -0
  163. package/packages/node/src/host.ts +319 -0
  164. package/packages/node/src/index.ts +17 -0
  165. package/packages/node/src/main.ts +11 -0
  166. package/packages/node/src/openshell.ts +73 -0
  167. package/packages/node/src/run.ts +106 -0
  168. package/packages/preset/package.json +12 -0
  169. package/packages/preset/presets/aider.json +8 -0
  170. package/packages/preset/presets/goose.json +8 -0
  171. package/packages/preset/src/index.ts +2 -0
  172. package/packages/preset/src/main.ts +195 -0
  173. package/packages/preset/src/presets.ts +87 -0
  174. package/packages/preset/src/spoke.ts +251 -0
  175. package/packages/preset/src/tmux.ts +160 -0
  176. package/packages/protocol/package.json +12 -0
  177. package/packages/protocol/src/harness-events.ts +207 -0
  178. package/packages/protocol/src/index.ts +1215 -0
  179. package/packages/protocol/src/launch.ts +172 -0
  180. package/packages/protocol/src/node.ts +409 -0
  181. package/packages/protocol/src/preset.ts +144 -0
  182. package/packages/protocol/src/webhook.ts +62 -0
  183. package/packages/protocol/src/wire.ts +54 -0
  184. package/packages/runtime-openshell/image/Dockerfile +36 -0
  185. package/packages/runtime-openshell/image/README.md +78 -0
  186. package/packages/runtime-openshell/image/build.ts +203 -0
  187. package/packages/runtime-openshell/image/entry.sh +163 -0
  188. package/packages/runtime-openshell/image/image.ts +119 -0
  189. package/packages/runtime-openshell/image/start.sh +24 -0
  190. package/packages/runtime-openshell/package.json +12 -0
  191. package/packages/runtime-openshell/src/cli.ts +584 -0
  192. package/packages/runtime-openshell/src/index.ts +6 -0
  193. package/packages/runtime-openshell/src/policy.ts +155 -0
  194. package/packages/runtime-openshell/src/profiles.ts +181 -0
  195. package/packages/runtime-openshell/src/runtime.ts +475 -0
  196. package/packages/runtime-sdk/package.json +12 -0
  197. package/packages/runtime-sdk/src/index.ts +87 -0
  198. package/packages/sdk/package.json +12 -0
  199. package/packages/sdk/src/address.ts +25 -0
  200. package/packages/sdk/src/client.ts +553 -0
  201. package/packages/sdk/src/envelope.ts +108 -0
  202. package/packages/sdk/src/errors.ts +107 -0
  203. package/packages/sdk/src/index.ts +10 -0
  204. package/packages/sdk/src/sse.ts +70 -0
  205. package/packages/sdk/src/types.ts +517 -0
  206. package/packages/sdk/src/webhook.ts +102 -0
  207. package/packages/spoke-sdk/package.json +12 -0
  208. package/packages/spoke-sdk/src/activity.ts +3 -0
  209. package/packages/spoke-sdk/src/connection.ts +295 -0
  210. package/packages/spoke-sdk/src/delivery.ts +119 -0
  211. package/packages/spoke-sdk/src/enroll.ts +101 -0
  212. package/packages/spoke-sdk/src/harness-hello.ts +79 -0
  213. package/packages/spoke-sdk/src/harness-spoke.ts +263 -0
  214. package/packages/spoke-sdk/src/index.ts +12 -0
  215. package/packages/spoke-sdk/src/prompt-refs.ts +154 -0
  216. package/packages/spoke-sdk/src/prompt-source.ts +8 -0
  217. package/packages/spoke-sdk/src/session-ids.ts +49 -0
  218. package/packages/spoke-sdk/src/session-tokens.ts +78 -0
  219. package/scripts/hub-claude-code.service +34 -0
  220. package/scripts/hub-codex-spoke.service +34 -0
  221. package/scripts/hub-node.service +35 -0
  222. package/scripts/io.github.huynle.hub-claude-code.plist +53 -0
  223. package/scripts/io.github.huynle.hub-codex-spoke.plist +53 -0
  224. package/scripts/io.github.huynle.hub-node.plist +52 -0
  225. package/src/central/actions.ts +233 -0
  226. package/src/central/agent/acting-routes.ts +133 -0
  227. package/src/central/agent/acting-switch.ts +93 -0
  228. package/src/central/agent/acting.ts +599 -0
  229. package/src/central/agent/actor.ts +193 -0
  230. package/src/central/agent/app-tokens.ts +152 -0
  231. package/src/central/agent/archive-inactive.ts +66 -0
  232. package/src/central/agent/audit.ts +185 -0
  233. package/src/central/agent/decisions.ts +530 -0
  234. package/src/central/agent/delivery.ts +678 -0
  235. package/src/central/agent/directory-routes.ts +328 -0
  236. package/src/central/agent/directory.ts +473 -0
  237. package/src/central/agent/envelope.ts +51 -0
  238. package/src/central/agent/errors.ts +74 -0
  239. package/src/central/agent/escalation.ts +219 -0
  240. package/src/central/agent/event-cursor.ts +49 -0
  241. package/src/central/agent/event-writers.ts +181 -0
  242. package/src/central/agent/events-routes.ts +148 -0
  243. package/src/central/agent/events.ts +277 -0
  244. package/src/central/agent/grant-requests.ts +221 -0
  245. package/src/central/agent/grant-routes.ts +70 -0
  246. package/src/central/agent/grants.ts +235 -0
  247. package/src/central/agent/launch-engine.ts +1025 -0
  248. package/src/central/agent/launch-routes.ts +252 -0
  249. package/src/central/agent/launch-spec.ts +289 -0
  250. package/src/central/agent/launch-store.ts +264 -0
  251. package/src/central/agent/launch-tokens.ts +206 -0
  252. package/src/central/agent/message-limits.ts +123 -0
  253. package/src/central/agent/message-routes.ts +385 -0
  254. package/src/central/agent/messages.ts +442 -0
  255. package/src/central/agent/names.ts +86 -0
  256. package/src/central/agent/placement.ts +419 -0
  257. package/src/central/agent/policy.ts +378 -0
  258. package/src/central/agent/read-routes.ts +385 -0
  259. package/src/central/agent/routes.ts +281 -0
  260. package/src/central/agent/session-tokens.ts +227 -0
  261. package/src/central/agent/versioned.ts +26 -0
  262. package/src/central/agent/watch.ts +112 -0
  263. package/src/central/agent/webhook-routes.ts +86 -0
  264. package/src/central/agent/webhooks.ts +493 -0
  265. package/src/central/agent-data.ts +177 -0
  266. package/src/central/auth.ts +350 -121
  267. package/src/central/backend/file-state.ts +240 -0
  268. package/src/central/backend/index.ts +116 -0
  269. package/src/central/backend/memory-bus.ts +93 -0
  270. package/src/central/backend/memory-catalog.ts +90 -0
  271. package/src/central/backend/memory-counters.ts +102 -0
  272. package/src/central/backend/memory-events.ts +45 -0
  273. package/src/central/backend/memory-kv.ts +38 -0
  274. package/src/central/backend/memory-state.ts +77 -0
  275. package/src/central/backend/postgres-migrate.ts +100 -0
  276. package/src/central/backend/postgres-state.ts +197 -0
  277. package/src/central/backend/types.ts +246 -0
  278. package/src/central/backend/valkey-bus.ts +425 -0
  279. package/src/central/backend/valkey-catalog.ts +190 -0
  280. package/src/central/backend/valkey-client.ts +36 -0
  281. package/src/central/backend/valkey-counters.ts +38 -0
  282. package/src/central/backend/valkey-events.ts +168 -0
  283. package/src/central/backend/valkey-kv.ts +54 -0
  284. package/src/central/backend/valkey-locks.ts +92 -0
  285. package/src/central/cluster/directory.ts +141 -0
  286. package/src/central/cluster/feed.ts +84 -0
  287. package/src/central/cluster/leader.ts +232 -0
  288. package/src/central/cluster/live.ts +151 -0
  289. package/src/central/cluster/recommend.ts +59 -0
  290. package/src/central/cluster/rpc.ts +150 -0
  291. package/src/central/cluster/view.ts +232 -0
  292. package/src/central/config.ts +197 -3
  293. package/src/central/done.ts +96 -0
  294. package/src/central/events-log.ts +100 -0
  295. package/src/central/http.ts +1149 -170
  296. package/src/central/import.ts +318 -0
  297. package/src/central/main.ts +211 -12
  298. package/src/central/mcp-http.ts +116 -0
  299. package/src/central/nodes.ts +314 -0
  300. package/src/central/oidc.ts +34 -14
  301. package/src/central/push-target.ts +118 -18
  302. package/src/central/push.ts +256 -82
  303. package/src/central/registry.ts +902 -92
  304. package/src/central/say-stream.ts +128 -0
  305. package/src/central/session-token-push.ts +233 -0
  306. package/src/central/speech/openai.ts +21 -14
  307. package/src/central/speech/openrouter.ts +27 -10
  308. package/src/central/speech/provider.ts +54 -0
  309. package/src/central/speech/service.ts +22 -7
  310. package/src/central/spoke-ws.ts +522 -44
  311. package/src/central/store-shared.ts +518 -0
  312. package/src/central/store.ts +625 -189
  313. package/src/central/token-keys.ts +165 -0
  314. package/src/config.ts +118 -7
  315. package/src/index.ts +195 -43
  316. package/src/protocol.ts +3 -650
  317. package/src/rpc.ts +27 -2
  318. package/src/spoke/client.ts +3 -272
  319. package/src/spoke/conversation.ts +16 -12
  320. package/src/spoke/deliver.ts +75 -0
  321. package/src/spoke/enroll.ts +2 -79
  322. package/src/spoke/env-inject.ts +160 -0
  323. package/src/spoke/hub-rpc.ts +115 -0
  324. package/src/spoke/hub-tool.ts +409 -0
  325. package/src/spoke/launch-source.ts +22 -0
  326. package/src/spoke/launchers.ts +174 -0
  327. package/src/spoke/locations.ts +3 -2
  328. package/src/spoke/node-roots.ts +61 -0
  329. package/src/spoke/prompt-refs.ts +116 -0
  330. package/src/spoke/session-tokens.ts +2 -0
  331. package/src/spoke/spoke.ts +465 -28
  332. package/src/spoke/taskboard.ts +243 -0
  333. package/src/spoke/timeline.ts +184 -6
  334. package/src/spoke/tracker.ts +79 -4
  335. package/src/status.ts +203 -10
  336. package/src/tui-commands.ts +13 -6
  337. package/src/tui-messaging.ts +93 -0
  338. package/src/tui.ts +15 -0
  339. package/web/dist/apple-touch-icon.png +0 -0
  340. package/web/dist/assets/highlight-y4rJE6Tt.js +5 -0
  341. package/web/dist/assets/index-DWnMhXk9.js +20 -0
  342. package/web/dist/assets/index-DmUXEnw9.css +1 -0
  343. package/web/dist/assets/ort-wasm-simd-threaded-CxTQ5xH-.wasm +0 -0
  344. package/web/dist/assets/silero-worker-C8-8vLVS.js +11 -0
  345. package/web/dist/assets/silero_vad-Dt9-bTDn.onnx +0 -0
  346. package/web/dist/favicon.ico +0 -0
  347. package/web/dist/icon-192.png +0 -0
  348. package/web/dist/icon-512.png +0 -0
  349. package/web/dist/icon-maskable-512.png +0 -0
  350. package/web/dist/icon.svg +5 -5
  351. package/web/dist/index.html +17 -4
  352. package/web/dist/manifest.webmanifest +2 -2
  353. package/web/dist/sw.js +79 -15
  354. package/web/dist/assets/index-DClBvfLL.css +0 -1
  355. package/web/dist/assets/index-Djo_yF0a.js +0 -12
@@ -0,0 +1,227 @@
1
+ // Hub-minted session tokens (design §1.3): a bearer credential for one session of one machine, which the hub
2
+ // pushes to the spoke reporting the session (sessionTokens op, see ../session-token-push.ts) and the spoke hands
3
+ // to the session's shells.
4
+ //
5
+ // Format: `hst1.<b64url(payload JSON)>.<b64url(HMAC-SHA256(secret of kid, "hst1." + payload part))>`, where the
6
+ // payload is {typ, uid, mid, sid, exp (ms since the epoch), kid, ue (user epoch), me (machine epoch), aud (the
7
+ // hub's public URL)}. One algorithm, one prefix; anything else is not a session token.
8
+ //
9
+ // typ "session" is the shell token (HUB_SESSION_TOKEN; any code in the session's shells can read it): its actor
10
+ // is via "shell" and can never act. typ "act" (phase 3, decision #1) is the act token, pushed only to spokes
11
+ // announcing "actTokens" (actTokens op), which never put it in any environment; its actor is via "act", the only
12
+ // session actor act verbs may admit (canActAsSession). The signature covers typ, so one cannot be turned into the other.
13
+ //
14
+ // A token is accepted while all of this holds: signature (constant-time) under a known kid, audience, expiry,
15
+ // the user's and machine's current token epochs, the machine exists, is not revoked and still belongs to uid,
16
+ // the user exists (the resolver refuses disabled users), and that machine still reports the session for uid.
17
+ // Tokens and secrets are never logged and never appear in this object's JSON or inspect output.
18
+ import { createHmac, timingSafeEqual } from "node:crypto"
19
+ import { SESSION_TOKEN_MAX_CHARS } from "../../protocol"
20
+ import type { Store } from "../store"
21
+ import type { TokenKeys } from "../token-keys"
22
+ import type { BearerVerifier, SessionActor, VerifyResult } from "./actor"
23
+
24
+ export const SESSION_TOKEN_PREFIX = "hst1"
25
+ /** How long a session token is valid. */
26
+ export const SESSION_TOKEN_LIFETIME_MS = 24 * 60 * 60 * 1000
27
+
28
+ /** What the verifier needs to know about live sessions (implemented by the registry). */
29
+ export interface SessionLiveness {
30
+ /** Tenant (user ID) of a reported session (Registry.sessionTenant). */
31
+ sessionTenant(sessionID: string): string | undefined
32
+ /** Whether a connected process of the machine reports the session (for the machine's tenant). */
33
+ machineReports(machineID: string, sessionID: string): boolean
34
+ }
35
+
36
+ export interface SessionTokensOptions {
37
+ readonly keys: TokenKeys
38
+ readonly store: Store
39
+ readonly sessions: SessionLiveness
40
+ /** The hub's public URL: the tokens' audience. */
41
+ readonly publicURL: string
42
+ readonly now?: () => number
43
+ readonly lifetimeMs?: number
44
+ }
45
+
46
+ export interface MintedSessionToken {
47
+ readonly token: string
48
+ /** Expiry, ms since the epoch. */
49
+ readonly exp: number
50
+ /** The machine's stamp when minted (see SessionTokens.stamp). */
51
+ readonly stamp: string
52
+ }
53
+
54
+ /** Token types: "session" for shells, "act" for act verbs (the hub tool). */
55
+ export type SessionTokenType = "session" | "act"
56
+
57
+ interface Claims {
58
+ readonly typ: SessionTokenType
59
+ readonly uid: string
60
+ readonly mid: string
61
+ readonly sid: string
62
+ readonly exp: number
63
+ readonly kid: string
64
+ readonly ue: number
65
+ readonly me: number
66
+ readonly aud: string
67
+ }
68
+
69
+ const CLAIM_KEYS = ["typ", "uid", "mid", "sid", "exp", "kid", "ue", "me", "aud"].sort().join()
70
+ const B64URL = /^[A-Za-z0-9_-]+$/
71
+ /** HMAC-SHA256 output length. */
72
+ const SIG_BYTES = 32
73
+
74
+ const text = (v: unknown): v is string => typeof v === "string" && v.length > 0
75
+ const count = (v: unknown): v is number => typeof v === "number" && Number.isSafeInteger(v) && v >= 0
76
+
77
+ /** Canonical base64url only (no padding, no other alphabet, no non-zero trailing bits). */
78
+ function decode(part: string): Buffer | undefined {
79
+ if (!B64URL.test(part)) return undefined
80
+ const bytes = Buffer.from(part, "base64url")
81
+ return bytes.toString("base64url") === part ? bytes : undefined
82
+ }
83
+
84
+ function parseClaims(bytes: Buffer): Claims | undefined {
85
+ let raw: unknown
86
+ try {
87
+ raw = JSON.parse(bytes.toString("utf8"))
88
+ } catch {
89
+ return undefined
90
+ }
91
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return undefined
92
+ const c = raw as Record<string, unknown>
93
+ if (Object.keys(c).sort().join() !== CLAIM_KEYS) return undefined
94
+ if ((c.typ !== "session" && c.typ !== "act") || !text(c.uid) || !text(c.mid) || !text(c.sid) || !text(c.kid) || !text(c.aud)) return undefined
95
+ if (!count(c.exp) || !count(c.ue) || !count(c.me)) return undefined
96
+ return c as unknown as Claims
97
+ }
98
+
99
+ const sign = (secret: Buffer, payload: string): Buffer => createHmac("sha256", secret).update(`${SESSION_TOKEN_PREFIX}.${payload}`).digest()
100
+
101
+ export class SessionTokens {
102
+ readonly lifetimeMs: number
103
+ readonly verifier: BearerVerifier
104
+ readonly #keys: TokenKeys
105
+ readonly #store: Store
106
+ readonly #sessions: SessionLiveness
107
+ readonly #aud: string
108
+ readonly #now: () => number
109
+
110
+ constructor(options: SessionTokensOptions) {
111
+ this.#keys = options.keys
112
+ this.#store = options.store
113
+ this.#sessions = options.sessions
114
+ this.#aud = options.publicURL
115
+ this.#now = options.now ?? Date.now
116
+ this.lifetimeMs = options.lifetimeMs ?? SESSION_TOKEN_LIFETIME_MS
117
+ this.verifier = { kind: "session", verify: (token) => this.verifyFresh(token) }
118
+ }
119
+
120
+ /** The owner of an enrolled, unrevoked machine whose user exists and is enabled. */
121
+ #owner(machineID: string): string | undefined {
122
+ const machine = this.#store.machines().find((m) => m.id === machineID)
123
+ if (!machine || machine.revoked) return undefined
124
+ const user = this.#store.user(machine.ownerID)
125
+ return user && !user.disabled ? user.id : undefined
126
+ }
127
+
128
+ /**
129
+ * What a machine's tokens depend on besides the session: owner, active kid, user and machine epochs. Tokens
130
+ * minted under another stamp are stale (resend them); undefined when the machine cannot get tokens.
131
+ */
132
+ stamp(machineID: string): string | undefined {
133
+ const uid = this.#owner(machineID)
134
+ if (!uid) return undefined
135
+ return JSON.stringify([uid, this.#keys.active().kid, this.#store.userEpoch(uid), this.#store.machineEpoch(machineID)])
136
+ }
137
+
138
+ /**
139
+ * A token for a session of the machine's owner (undefined when the machine is unknown, revoked or its owner
140
+ * disabled, or the session belongs to another tenant or to nobody).
141
+ */
142
+ mint(machineID: string, sessionID: string): MintedSessionToken | undefined {
143
+ return this.#mint("session", machineID, sessionID)
144
+ }
145
+
146
+ /** An act token (typ "act") for the session, under the same rules as mint. Only for the actTokens op. */
147
+ mintAct(machineID: string, sessionID: string): MintedSessionToken | undefined {
148
+ return this.#mint("act", machineID, sessionID)
149
+ }
150
+
151
+ #mint(typ: SessionTokenType, machineID: string, sessionID: string): MintedSessionToken | undefined {
152
+ const uid = this.#owner(machineID)
153
+ if (!uid || this.#sessions.sessionTenant(sessionID) !== uid) return undefined
154
+ const { kid, secret } = this.#keys.active()
155
+ const exp = this.#now() + this.lifetimeMs
156
+ const claims: Claims = {
157
+ typ,
158
+ uid,
159
+ mid: machineID,
160
+ sid: sessionID,
161
+ exp,
162
+ kid,
163
+ ue: this.#store.userEpoch(uid),
164
+ me: this.#store.machineEpoch(machineID),
165
+ aud: this.#aud,
166
+ }
167
+ const payload = Buffer.from(JSON.stringify(claims)).toString("base64url")
168
+ const stamp = JSON.stringify([uid, kid, claims.ue, claims.me])
169
+ return { token: `${SESSION_TOKEN_PREFIX}.${payload}.${sign(secret, payload).toString("base64url")}`, exp, stamp }
170
+ }
171
+
172
+ /** The session actor of a valid token, "expired" for a genuine token past its expiry, else undefined (from memory). */
173
+ verify(token: string): VerifyResult {
174
+ const claims = this.#genuine(token)
175
+ return claims && claims !== "expired" ? this.#current(claims) : claims
176
+ }
177
+
178
+ /**
179
+ * verify, after reading the token's user and machine again (a shared store's security re-check: a revocation,
180
+ * epoch bump or disabled user on another pod applies at once). The bearer verifier uses this.
181
+ */
182
+ async verifyFresh(token: string): Promise<VerifyResult> {
183
+ const claims = this.#genuine(token)
184
+ if (!claims || claims === "expired") return claims
185
+ await this.#store.recheck({ users: [claims.uid], machines: [claims.mid] })
186
+ return this.#current(claims)
187
+ }
188
+
189
+ /** The claims of a token signed with a known key for this hub, "expired" past its expiry, else undefined. */
190
+ #genuine(token: string): Claims | "expired" | undefined {
191
+ if (typeof token !== "string" || token.length > SESSION_TOKEN_MAX_CHARS) return undefined
192
+ const parts = token.split(".")
193
+ if (parts.length !== 3 || parts[0] !== SESSION_TOKEN_PREFIX) return undefined
194
+ const [, payload, sigPart] = parts as [string, string, string]
195
+ const body = decode(payload)
196
+ const sig = decode(sigPart)
197
+ if (!body || !sig || sig.length !== SIG_BYTES) return undefined
198
+ const claims = parseClaims(body)
199
+ if (!claims) return undefined
200
+ const secret = this.#keys.key(claims.kid)
201
+ if (!secret) return undefined
202
+ const expected = sign(secret, payload)
203
+ if (expected.length !== sig.length || !timingSafeEqual(expected, sig)) return undefined
204
+ if (claims.aud !== this.#aud) return undefined
205
+ if (this.#now() >= claims.exp) return "expired"
206
+ return claims
207
+ }
208
+
209
+ /** The session actor while the token's epochs, machine, user and session are all still current. */
210
+ #current(claims: Claims): VerifyResult {
211
+ if (claims.ue !== this.#store.userEpoch(claims.uid) || claims.me !== this.#store.machineEpoch(claims.mid)) return undefined
212
+ const machine = this.#store.machines().find((m) => m.id === claims.mid)
213
+ if (!machine || machine.revoked || machine.ownerID !== claims.uid) return undefined
214
+ if (!this.#store.user(claims.uid)) return undefined
215
+ if (this.#sessions.sessionTenant(claims.sid) !== claims.uid || !this.#sessions.machineReports(claims.mid, claims.sid)) return undefined
216
+ const actor: SessionActor = { kind: "session", userID: claims.uid, machineID: claims.mid, sessionID: claims.sid, via: claims.typ === "act" ? "act" : "shell" }
217
+ return actor
218
+ }
219
+
220
+ toJSON(): { audience: string; lifetimeMs: number } {
221
+ return { audience: this.#aud, lifetimeMs: this.lifetimeMs }
222
+ }
223
+
224
+ [Symbol.for("nodejs.util.inspect.custom")](): string {
225
+ return `SessionTokens(${this.#aud})`
226
+ }
227
+ }
@@ -0,0 +1,26 @@
1
+ // Read-modify-write over a StateStore key with optimistic versions: retried when another writer got there first.
2
+ import type { StateStore } from "../backend/types"
3
+
4
+ /** Attempts before a contended update gives up (each one re-reads the key). */
5
+ const ATTEMPTS = 16
6
+
7
+ /**
8
+ * Applies `change` to the key's current value (undefined: missing) and writes the result with ifVersion.
9
+ * `change` returning undefined leaves the key alone. Resolves with the written value (or undefined).
10
+ */
11
+ export async function updateVersioned<T>(
12
+ state: StateStore,
13
+ userID: string,
14
+ collection: string,
15
+ key: string,
16
+ change: (current: T | undefined) => T | undefined,
17
+ ): Promise<T | undefined> {
18
+ for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
19
+ const current = await state.get<T>(userID, collection, key)
20
+ const next = change(current?.value)
21
+ if (next === undefined) return undefined
22
+ const result = await state.put(userID, collection, key, next, { ifVersion: current?.version ?? 0 })
23
+ if (result.ok) return next
24
+ }
25
+ throw new Error(`state update of ${collection}/${key} kept conflicting`)
26
+ }
@@ -0,0 +1,112 @@
1
+ // GET /api/agent/watch (design §4; lifecycle design "Events"): a live SSE view of the canonical agent event journal
2
+ // (events.ts), for orchestrator loops. The same events, visibility and filters as GET /api/agent/events, read with
3
+ // the same read() (events-routes.ts), so the two never define events differently. Not the console's /api/events.
4
+ //
5
+ // ?blocked=1 blocker events only (blocker.*)
6
+ // ?session=<addr> only that session's tree (an address resolved for the caller; several for a @@role)
7
+ // ?cursor=… or Last-Event-ID: resume after a cursor this stream (or GET /api/agent/events) gave; a bad one is
8
+ // 400 invalid_cursor before the stream starts
9
+ //
10
+ // Frames (each `data:` line one JSON object; `id:` lines carry the resumable cursor):
11
+ // {type: "ready", at} first, once subscribed
12
+ // {id: "evt_…", at, type, subject, data} a journal event (no free text)
13
+ // {type: "reset", at, refresh: [...]} the cursor was no longer covered: re-read those
14
+ // event: ping every heartbeat while idle (default 20 s), with the cursor
15
+ // When the hub drains (shutting down), the stream ends with `retry: 2000` (reconnect, to another pod, after it).
16
+ import type { AgentActor } from "./actor"
17
+ import type { AgentEvent } from "./events"
18
+ import { read, RESET_REFRESH, type EventsPage, type EventsRouteDeps } from "./events-routes"
19
+
20
+ export const WATCH_HEARTBEAT_MS = 20_000
21
+ /** Draining: the `retry:` (ms) an ending watch stream sends. */
22
+ export const WATCH_RETRY_MS = 2_000
23
+ const RETRY = `retry: ${WATCH_RETRY_MS}\n\n`
24
+ const SSE_HEADERS = { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" }
25
+ const PAGE = 200
26
+
27
+ export interface WatchOptions {
28
+ readonly events: EventsRouteDeps
29
+ readonly userID: string
30
+ readonly actor: AgentActor
31
+ readonly signal: AbortSignal
32
+ /** Resume after this cursor (none: from now). */
33
+ readonly cursor?: string
34
+ /** Only these event types (exact, or `blocker.*`). */
35
+ readonly types?: readonly string[]
36
+ /** Only events of these sessions or of trees rooted at them. */
37
+ readonly sessions?: ReadonlySet<string>
38
+ readonly heartbeatMs?: number
39
+ readonly now?: () => number
40
+ /** Aborted when the hub drains: the stream ends with a `retry:` hint. */
41
+ readonly draining?: AbortSignal
42
+ }
43
+
44
+ /** The watch stream; throws invalid_cursor (before streaming) for a cursor this hub did not give this user. */
45
+ export async function watchStream(options: WatchOptions): Promise<Response> {
46
+ if (options.draining?.aborted) return new Response(RETRY, { headers: SSE_HEADERS })
47
+ const { userID, actor } = options
48
+ const now = options.now ?? Date.now
49
+ const heartbeatMs = options.heartbeatMs ?? WATCH_HEARTBEAT_MS
50
+ const filters = { limit: PAGE, types: options.types ?? [], ...(options.sessions ? { sessions: options.sessions } : {}) }
51
+ const first = await read(options.events, userID, actor, { ...filters, ...(options.cursor ? { cursor: options.cursor } : {}), wait: 0 })
52
+ const encoder = new TextEncoder()
53
+ const stop = new AbortController()
54
+
55
+ const stream = new ReadableStream<Uint8Array>({
56
+ async start(controller) {
57
+ let open = true
58
+ const close = () => {
59
+ if (!open) return
60
+ open = false
61
+ stop.abort()
62
+ try {
63
+ controller.close()
64
+ } catch {}
65
+ }
66
+ const write = (chunk: string) => {
67
+ if (!open) return
68
+ try {
69
+ controller.enqueue(encoder.encode(chunk))
70
+ } catch {
71
+ close()
72
+ }
73
+ }
74
+ const frame = (data: object, id?: string) => write(`${id ? `id: ${id}\n` : ""}data: ${JSON.stringify(data)}\n\n`)
75
+ const emit = (page: EventsPage) => {
76
+ if (page.reset) return frame({ type: "reset", at: now(), refresh: page.refresh ?? RESET_REFRESH }, page.cursor)
77
+ if (page.events.length === 0) return write(`event: ping\nid: ${page.cursor}\ndata: 0\n\n`)
78
+ page.events.forEach((event: AgentEvent, i) => frame(event, i === page.events.length - 1 ? page.cursor : undefined))
79
+ }
80
+ options.signal.addEventListener("abort", () => close(), { once: true })
81
+ options.draining?.addEventListener(
82
+ "abort",
83
+ () => {
84
+ write(RETRY)
85
+ close()
86
+ },
87
+ { once: true },
88
+ )
89
+ frame({ type: "ready", at: now() }, options.cursor && !first.reset ? options.cursor : first.cursor)
90
+ if (first.reset || first.events.length) emit(first)
91
+ let cursor = first.cursor
92
+ while (open && !stop.signal.aborted) {
93
+ let page: EventsPage
94
+ try {
95
+ page = await read(options.events, userID, actor, { ...filters, cursor, wait: heartbeatMs / 1000 }, stop.signal)
96
+ } catch (cause) {
97
+ console.error(`[hub] agent watch read failed: ${(cause as Error).message}`)
98
+ write(RETRY)
99
+ return close()
100
+ }
101
+ if (stop.signal.aborted) break
102
+ emit(page)
103
+ cursor = page.cursor
104
+ }
105
+ close()
106
+ },
107
+ cancel() {
108
+ stop.abort()
109
+ },
110
+ })
111
+ return new Response(stream, { headers: SSE_HEADERS })
112
+ }
@@ -0,0 +1,86 @@
1
+ // The webhook API on /api/agent (P4-T10; decision znbg8xa5 #8), over Webhooks (webhooks.ts):
2
+ //
3
+ // POST /api/agent/webhooks {url, events?, label?} → 201 {webhook, secret} (the secret only here)
4
+ // GET /api/agent/webhooks → {webhooks}
5
+ // GET /api/agent/webhooks/:id → {webhook, deliveries} (recent deliveries, oldest first)
6
+ // DELETE /api/agent/webhooks/:id → 204 (its pending deliveries are dropped)
7
+ //
8
+ // Who: users (all of their registrations, their own and their app tokens'); app tokens with sessions:launch (the
9
+ // scope of the only events so far, launch.*), which see and delete only the registrations they made and get only
10
+ // the events of launches they started. Sessions cannot register webhooks. Another user's registration, or (apps)
11
+ // one the token did not make, is 404 not_found.
12
+ import type { Store } from "../store"
13
+ import type { AgentActor } from "./actor"
14
+ import { AgentError } from "./errors"
15
+ import type { AgentRoute } from "./routes"
16
+ import { webhookEventScopes, type WebhookOwner, type Webhooks } from "./webhooks"
17
+
18
+ // A literal: routes.ts imports this module.
19
+ const PATH = "/api/agent/webhooks"
20
+ const NO_STORE = { "cache-control": "no-store" }
21
+ /** Users; apps with sessions:launch. */
22
+ export const WEBHOOK_SCOPE = "events:subscribe"
23
+ const ALLOW = { scope: WEBHOOK_SCOPE } as const
24
+
25
+ export type RouteWebhooks = Pick<Webhooks, "register" | "list" | "get" | "remove" | "deliveries">
26
+
27
+ /** The registrations an actor manages: an app token its own, a user all (undefined). */
28
+ const ownerOf = (actor: AgentActor): WebhookOwner | undefined => (actor.kind === "app" ? `app:${actor.tokenID}` : undefined)
29
+
30
+ const notFound = (id: string) => new AgentError("not_found", `No webhook ${id}`)
31
+
32
+ const requireEventScopes = (actor: AgentActor, events: unknown) => {
33
+ if (actor.kind !== "app") return
34
+ const patterns = events === undefined ? ["launch.*"] : Array.isArray(events) && events.every((e) => typeof e === "string") ? events : []
35
+ const missing = webhookEventScopes(patterns).filter((scope) => !actor.scopes.includes(scope))
36
+ if (missing.length) throw new AgentError("forbidden", `Webhook events need scope ${missing.join(", ")}`)
37
+ }
38
+
39
+ async function bodyOf(request: Request): Promise<Record<string, unknown>> {
40
+ const raw = JSON.parse(await request.text()) as unknown
41
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new AgentError("invalid", "Body must be a JSON object")
42
+ return raw as Record<string, unknown>
43
+ }
44
+
45
+ export function webhookRoutes(webhooks: RouteWebhooks, store?: Pick<Store, "audit">): AgentRoute[] {
46
+ return [
47
+ {
48
+ method: "POST",
49
+ path: PATH,
50
+ allow: ALLOW,
51
+ async handle({ request, actor, user }) {
52
+ const input = await bodyOf(request)
53
+ requireEventScopes(actor, input.events)
54
+ const result = await webhooks.register(user.id, ownerOf(actor) ?? "user", { url: input.url, events: input.events, label: input.label })
55
+ await store?.audit({ actor: user.id, action: "webhook.create", target: result.webhook.id, detail: new URL(result.webhook.url).host })
56
+ return Response.json(result, { status: 201, headers: NO_STORE })
57
+ },
58
+ },
59
+ {
60
+ method: "GET",
61
+ path: PATH,
62
+ allow: ALLOW,
63
+ handle: async ({ actor, user }) => Response.json({ webhooks: await webhooks.list(user.id, ownerOf(actor)) }, { headers: NO_STORE }),
64
+ },
65
+ {
66
+ method: "GET",
67
+ path: `${PATH}/:id`,
68
+ allow: ALLOW,
69
+ async handle({ params, actor, user }) {
70
+ const webhook = await webhooks.get(user.id, params.id!, ownerOf(actor))
71
+ if (!webhook) throw notFound(params.id!)
72
+ return Response.json({ webhook, deliveries: await webhooks.deliveries(user.id, webhook.id) }, { headers: NO_STORE })
73
+ },
74
+ },
75
+ {
76
+ method: "DELETE",
77
+ path: `${PATH}/:id`,
78
+ allow: ALLOW,
79
+ async handle({ params, actor, user }) {
80
+ if (!(await webhooks.remove(user.id, params.id!, ownerOf(actor)))) throw notFound(params.id!)
81
+ await store?.audit({ actor: user.id, action: "webhook.delete", target: params.id! })
82
+ return new Response(null, { status: 204, headers: NO_STORE })
83
+ },
84
+ },
85
+ ]
86
+ }