@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,517 @@
1
+ // The shapes of the hub's /api/agent answers (src/central/agent/*). Fields are those the hub sends today; clients
2
+ // should ignore unknown ones, and later hubs may add fields.
3
+
4
+ /** The caller, as whoami and status (`you`) report it. */
5
+ export type Actor =
6
+ | { readonly kind: "user"; readonly userID: string; readonly deviceID: string; readonly via: "cookie" | "device-token" }
7
+ | {
8
+ readonly kind: "session"
9
+ readonly userID: string
10
+ readonly machineID: string
11
+ readonly sessionID: string
12
+ /** Which of the session's tokens: its shell token, or the act token only the hub tool has (missing: shell). */
13
+ readonly via?: "shell" | "act"
14
+ }
15
+ | { readonly kind: "app"; readonly userID: string; readonly tokenID: string; readonly scopes: readonly string[]; readonly label?: string }
16
+
17
+ export interface UserView {
18
+ readonly id: string
19
+ readonly name?: string
20
+ readonly email?: string
21
+ readonly role?: string
22
+ }
23
+
24
+ export interface Whoami {
25
+ readonly actor: Actor
26
+ readonly user: UserView
27
+ }
28
+
29
+ export interface Blocker {
30
+ readonly kind: string
31
+ readonly id?: string
32
+ readonly summary: string
33
+ readonly sessionID?: string
34
+ }
35
+
36
+ /** One session in status and sessions answers. */
37
+ export interface SessionView {
38
+ readonly id: string
39
+ readonly name?: string
40
+ readonly roles?: readonly string[]
41
+ readonly title?: string
42
+ readonly state: string
43
+ readonly harness?: string
44
+ readonly parentID?: string
45
+ readonly busySince?: number
46
+ readonly now?: string
47
+ readonly blockers?: readonly Blocker[]
48
+ readonly children?: number
49
+ readonly labels?: Readonly<Record<string, string>>
50
+ /** In sessions answers. */
51
+ readonly machineID?: string
52
+ readonly project?: string
53
+ }
54
+
55
+ export interface AttentionItem {
56
+ readonly kind: string
57
+ readonly sessionID: string
58
+ readonly name?: string
59
+ readonly id?: string
60
+ readonly summary: string
61
+ }
62
+
63
+ /** GET /api/agent/status: machines → projects → sessions, and what needs attention. */
64
+ export interface Status {
65
+ readonly machines: readonly {
66
+ readonly id: string
67
+ readonly name?: string
68
+ readonly online: boolean
69
+ readonly harnesses?: readonly string[]
70
+ readonly projects: readonly { readonly root: string; readonly name: string; readonly sessions: readonly SessionView[] }[]
71
+ }[]
72
+ readonly attention: readonly AttentionItem[]
73
+ readonly you?: Actor
74
+ }
75
+
76
+ /** GET /api/agent/sessions/:addr: one session in detail. */
77
+ export interface SessionDetail extends SessionView {
78
+ readonly online?: boolean
79
+ readonly directory?: string
80
+ readonly lastSeen?: number
81
+ readonly summary?: {
82
+ readonly branch?: string
83
+ readonly context?: { readonly used: number; readonly limit?: number; readonly percent?: number; readonly model?: string }
84
+ readonly changes?: { readonly additions: number; readonly deletions: number; readonly totalFiles: number }
85
+ readonly mcp?: readonly { readonly name: string; readonly status: string }[]
86
+ }
87
+ readonly unsupported?: readonly { readonly feature?: string; readonly harness?: string }[]
88
+ readonly failed?: readonly { readonly feature?: string }[]
89
+ }
90
+
91
+ /** One timeline row (newest first). */
92
+ export interface LogRow {
93
+ readonly kind: string
94
+ readonly text?: string
95
+ readonly name?: string
96
+ readonly state?: string
97
+ readonly title?: string
98
+ readonly at?: number
99
+ }
100
+
101
+ export interface RoleView {
102
+ readonly role: string
103
+ readonly project?: string
104
+ readonly generation?: number
105
+ readonly exclusive?: boolean
106
+ }
107
+
108
+ /** One session in directory answers (who). */
109
+ export interface DirectoryEntry {
110
+ readonly sessionID: string
111
+ readonly name?: string
112
+ readonly aliases?: readonly string[]
113
+ readonly machineID?: string
114
+ readonly parentID?: string
115
+ readonly project?: string
116
+ readonly title?: string
117
+ readonly state?: string
118
+ readonly online: boolean
119
+ readonly lastSeen?: number
120
+ readonly archived?: boolean
121
+ readonly roles: readonly RoleView[]
122
+ }
123
+
124
+ /** One compact event of GET /api/agent/events (no free text). Unknown types are valid: ignore them. */
125
+ export interface AgentEvent {
126
+ readonly id: string
127
+ readonly at: number
128
+ readonly type: string
129
+ readonly subject: Readonly<Record<string, string>>
130
+ readonly data: Readonly<Record<string, string | number | boolean>>
131
+ }
132
+
133
+ /**
134
+ * A page of GET /api/agent/events: pass `cursor` to the next call. `more`: records remain unread. `reset`: the
135
+ * cursor was no longer covered (expired, trimmed, hub restarted); re-read the `refresh` snapshots, then go on.
136
+ */
137
+ export interface EventsPage {
138
+ readonly events: AgentEvent[]
139
+ readonly cursor: string
140
+ readonly more: boolean
141
+ readonly reset: boolean
142
+ readonly refresh?: string[]
143
+ }
144
+
145
+ export interface Archived {
146
+ readonly sessionID: string
147
+ readonly archived: boolean
148
+ readonly changed: boolean
149
+ }
150
+
151
+ export interface Pinned {
152
+ readonly sessionID: string
153
+ readonly pinned: boolean
154
+ readonly changed: boolean
155
+ }
156
+
157
+ export type ArchiveSkipReason = "active" | "pinned" | "open" | "archived" | "not_found"
158
+ export interface ArchiveInactivePreview { readonly candidates: readonly string[]; readonly previewToken: string; readonly expiresAt: number }
159
+ export interface ArchiveInactiveResult { readonly changed: readonly string[]; readonly skipped: readonly { readonly sessionID: string; readonly reason: ArchiveSkipReason }[] }
160
+
161
+ export interface Named {
162
+ readonly sessionID: string
163
+ readonly name: string
164
+ /** The previous name, kept as an alias for 24 h. */
165
+ readonly alias?: string
166
+ }
167
+
168
+ export interface RoleClaim extends RoleView {
169
+ readonly sessionID: string
170
+ }
171
+
172
+ export interface RoleRelease {
173
+ readonly sessionID: string
174
+ readonly role: string
175
+ readonly project?: string
176
+ readonly released?: boolean
177
+ }
178
+
179
+ export interface AppTokenView {
180
+ readonly id: string
181
+ readonly label?: string
182
+ readonly scopes: readonly string[]
183
+ readonly created?: number
184
+ }
185
+
186
+ /** POST /api/agent/app-tokens: the token is shown once. */
187
+ export interface CreatedAppToken extends AppTokenView {
188
+ readonly token: string
189
+ }
190
+
191
+ /** Who sends or receives a message: a session, the user (`@me`), or an app token. */
192
+ export type Party =
193
+ | { readonly kind: "session"; readonly sessionID: string; readonly name?: string; readonly harness?: string; readonly machine?: string }
194
+ | { readonly kind: "user" }
195
+ | { readonly kind: "app"; readonly tokenID: string; readonly label?: string }
196
+
197
+ export type MessageKind = "msg" | "ask" | "reply" | "decline"
198
+ /** `sent` for msg/reply/decline; asks are open until answered, declined or expired. */
199
+ export type MessageState = "sent" | "open" | "answered" | "declined" | "expired"
200
+ /** How a message reached its recipient session. */
201
+ export type MessageDelivery = "steer" | "turn" | "queued" | "retained" | "pending"
202
+
203
+ /** A stored agent message. */
204
+ export interface AgentMessage {
205
+ readonly id: string
206
+ readonly kind: MessageKind
207
+ /** The authenticated sender. */
208
+ readonly from: Party
209
+ /** The address as the sender wrote it. */
210
+ readonly to: string
211
+ /** The party it reached. */
212
+ readonly recipient: Party
213
+ /** The role holder generation it reached (role addresses). */
214
+ readonly generation?: number
215
+ readonly text: string
216
+ readonly replyTo?: string
217
+ readonly state: MessageState
218
+ readonly at: number
219
+ readonly hop: number
220
+ readonly expiresAt?: number
221
+ readonly delivery?: MessageDelivery
222
+ readonly deliveredAt?: number
223
+ /** The reply or decline that closed an ask. */
224
+ readonly answer?: { readonly id: string; readonly at: number }
225
+ }
226
+
227
+ /** POST /api/agent/msg and /ask: the stored message and how it was delivered (none: stored only). */
228
+ export interface Sent {
229
+ readonly message: AgentMessage
230
+ readonly delivery?: MessageDelivery
231
+ }
232
+
233
+ /** POST /api/agent/msg or /ask with all: one per session. */
234
+ export interface SentAll {
235
+ readonly sent: readonly Sent[]
236
+ }
237
+
238
+ /** POST /api/agent/ask (after any wait) and GET /api/agent/messages/:id: the message and, once closed, its answer. */
239
+ export interface Asked extends Sent {
240
+ readonly answer?: AgentMessage
241
+ /** An ask to @me: the Decision it opened. */
242
+ readonly decision?: Decision
243
+ }
244
+
245
+ /** One choice a Decision offers; onTimeout may pick only a safe one. */
246
+ export interface DecisionOption {
247
+ readonly id: string
248
+ readonly label: string
249
+ readonly safe?: boolean
250
+ }
251
+
252
+ export type DecisionUrgency = "low" | "normal" | "high"
253
+ /** confirm: any of the user's devices; tap: only the signed-in console. */
254
+ export type DecisionTier = "confirm" | "tap"
255
+ /** closed: ended without an answer because what it asked about went away (answered elsewhere, ended). */
256
+ export type DecisionState = "open" | "answered" | "expired" | "closed"
257
+
258
+ /** What an ask to @me opens (design §12.1); only the user answers it. */
259
+ export interface Decision {
260
+ readonly id: string
261
+ /** The ask (to @me) carrying it. */
262
+ readonly messageID: string
263
+ readonly from: Party
264
+ /** Session IDs it concerns. */
265
+ readonly about: readonly string[]
266
+ readonly sessionID?: string
267
+ readonly question: string
268
+ readonly options: readonly DecisionOption[]
269
+ readonly recommendation?: { readonly option: string; readonly why?: string }
270
+ readonly urgency: DecisionUrgency
271
+ readonly tier: DecisionTier
272
+ readonly deadline?: number
273
+ readonly onTimeout?: string
274
+ readonly state: DecisionState
275
+ readonly at: number
276
+ readonly expiresAt: number
277
+ readonly escalatedAt?: number
278
+ readonly answer?: { readonly option?: string; readonly text?: string; readonly answeredBy: "user" | "timeout"; readonly at: number; readonly deviceID?: string }
279
+ /** Why it closed without an answer (state closed). */
280
+ readonly closed?: { readonly at: number; readonly reason: string }
281
+ }
282
+
283
+ /** POST /api/decisions/:id/answer. */
284
+ export interface DecisionAnswered {
285
+ readonly decision: Decision
286
+ /** The user's reply to the asker. */
287
+ readonly reply?: AgentMessage
288
+ }
289
+
290
+ /** POST /api/agent/reply and /decline: the answer and the message it answered. */
291
+ export interface Replied extends Sent {
292
+ readonly original: AgentMessage
293
+ }
294
+
295
+ /** GET /api/agent/inbox. */
296
+ export interface Inbox {
297
+ /** Open asks to the caller, newest first. */
298
+ readonly asks: readonly AgentMessage[]
299
+ /** The caller's own open asks. */
300
+ readonly waiting: readonly AgentMessage[]
301
+ /** Recent messages to the caller. */
302
+ readonly messages: readonly AgentMessage[]
303
+ /** Session IDs the caller muted. */
304
+ readonly muted: readonly string[]
305
+ }
306
+
307
+ /** One server-sent event of GET /api/agent/watch (data is the raw text, JSON for hub events). */
308
+ export interface WatchEvent {
309
+ readonly event: string
310
+ readonly data: string
311
+ /** The stream's cursor at this event (GET /api/agent/watch?cursor= resumes after it). */
312
+ readonly id?: string
313
+ }
314
+
315
+ // Acting (POST /api/agent/sessions/:addr/{approve,deny,answer,prompt,stop}; src/central/agent/acting.ts).
316
+
317
+ /** OpenCode's reply to a permission request. */
318
+ export type PermissionDecision = "once" | "always" | "reject"
319
+
320
+ /** How an agent's prompt enters a busy session (the spoke picks when unset). */
321
+ export type PromptDelivery = "queue" | "steer"
322
+
323
+ /** An answer to a form (question): field key → value. */
324
+ export type FormAnswer = Readonly<Record<string, string | number | boolean | readonly string[]>>
325
+
326
+ /** Why an act verb needs the user (the needs_user envelope's `evaluation`). */
327
+ export interface Evaluation {
328
+ readonly verdict: "outside"
329
+ readonly reason: string
330
+ /** Set by the hub: confirm (any of the user's devices) or tap (the signed-in console). */
331
+ readonly tier: DecisionTier
332
+ /** The usable grants it was evaluated against. */
333
+ readonly grants: readonly string[]
334
+ }
335
+
336
+ export interface Approved {
337
+ readonly sessionID: string
338
+ readonly requestID: string
339
+ readonly decision: PermissionDecision
340
+ /** The grant it acted under (agents). */
341
+ readonly grant?: string
342
+ }
343
+
344
+ export interface Denied {
345
+ readonly sessionID: string
346
+ readonly requestID: string
347
+ readonly grant?: string
348
+ /** With a why: whether the harness got the reason. */
349
+ readonly reasonSent?: boolean
350
+ }
351
+
352
+ export interface FormAnswered {
353
+ readonly sessionID: string
354
+ readonly formID: string
355
+ readonly grant?: string
356
+ }
357
+
358
+ export interface Stopped {
359
+ readonly sessionID: string
360
+ readonly grant?: string
361
+ /** The spoke's answer. */
362
+ readonly result: unknown
363
+ }
364
+
365
+ /** An agent's prompt (delivered under its provenance), or the user's (steered into the tree, or a new turn). */
366
+ export type Prompted =
367
+ | { readonly sessionID: string; readonly messageID: string; readonly delivered: string; readonly inboxID?: string; readonly grant: string }
368
+ | { readonly sessionID: string; readonly mode: "steer"; readonly inboxID: string }
369
+ | { readonly sessionID: string; readonly mode: "turn" }
370
+
371
+ // Grants (/api/agent/grants; src/central/agent/grants.ts): created, edited and revoked only by the user.
372
+
373
+ export type GrantVerb = "approve" | "deny" | "answer" | "prompt" | "stop" | "roles" | "launch"
374
+ export type GrantState = "active" | "expired" | "revoked"
375
+
376
+ /** What the user grants (create and update bodies). */
377
+ export interface GrantSpec {
378
+ /** A session (ses_… or @name, pinned at creation), a reserved role (@@orchestrator…), or app:<token id>. */
379
+ readonly holder: string
380
+ readonly scope: { readonly projects: readonly string[] }
381
+ readonly allow: readonly GrantVerb[]
382
+ readonly permissions?: { readonly allow?: readonly string[]; readonly never?: readonly string[] }
383
+ readonly allowAlways?: boolean
384
+ /** Seconds, or 30m, 8h, 2d (1 minute to 30 days). */
385
+ readonly expires?: number | string
386
+ readonly maxActionsPerHour?: number
387
+ }
388
+
389
+ export interface Grant {
390
+ readonly id: string
391
+ readonly holder: string
392
+ readonly holderSessionID?: string
393
+ readonly scope: { readonly projects: readonly string[] }
394
+ readonly allow: readonly GrantVerb[]
395
+ readonly permissions: { readonly allow: readonly string[]; readonly never: readonly string[] }
396
+ readonly allowAlways: boolean
397
+ readonly maxActionsPerHour: number
398
+ readonly created: number
399
+ readonly expiresAt: number
400
+ readonly updated?: number
401
+ readonly revokedAt?: number
402
+ readonly state: GrantState
403
+ }
404
+
405
+ /** POST /api/agent/grants/requests: what an agent asks the user for (the holder defaults to the requester). */
406
+ export type GrantRequestSpec = Omit<GrantSpec, "holder"> & {
407
+ /** Only the requester itself (ses_…, its @name, app:<own token id>) or a reserved @@role the session holds. */
408
+ readonly holder?: string
409
+ }
410
+
411
+ /** POST /api/agent/grants/requests: the ask to @me and the tap Decision it opened (approve creates the grant). */
412
+ export interface GrantRequested {
413
+ readonly message: AgentMessage
414
+ readonly decision: Decision
415
+ }
416
+
417
+ // Launches (design §6.1; packages/protocol/src/launch.ts is the wire source of truth, kept in step by the tests).
418
+
419
+ export type LaunchState = "queued" | "placing" | "provisioning" | "running" | "ended"
420
+ export type LaunchOutcome = "idle" | "needs_user" | "failed" | "timeout" | "stopped" | "lost"
421
+ export type WorkspaceMode = "worktree" | "checkout" | "clone"
422
+ export type RuntimeKind = "host" | "openshell"
423
+ export type PlacementMode = "require" | "prefer"
424
+ export type LaunchEndWhen = "idle" | "asked" | "never"
425
+ export type CleanupPolicy = "keep" | "remove"
426
+
427
+ /** POST /api/agent/launches: what to start, where, and when it ends. Unset fields take the hub's defaults. */
428
+ export interface LaunchSpec {
429
+ /** Idempotency key: the same key and body return the same launch (another body: invalid). */
430
+ readonly key?: string
431
+ /** The session's @name (without the @). */
432
+ readonly name?: string
433
+ readonly roles?: readonly string[]
434
+ /** Default opencode. */
435
+ readonly harness?: string
436
+ readonly agent?: string
437
+ /** "provider/model". */
438
+ readonly model?: string
439
+ readonly workspace: {
440
+ /** Absolute path on the node: the repository (worktree, checkout) or where the clone goes. */
441
+ readonly repo: string
442
+ /** Default worktree (`<repo>/.worktrees/<branch>`; needs branch). */
443
+ readonly mode?: WorkspaceMode
444
+ readonly branch?: string
445
+ readonly base?: string
446
+ /** https or ssh; required for clone. */
447
+ readonly remote?: string
448
+ }
449
+ readonly runtime?: { readonly kind: RuntimeKind; readonly profile?: string }
450
+ readonly placement?: { readonly labels?: Readonly<Record<string, string>>; readonly machine?: string; readonly mode?: PlacementMode }
451
+ readonly prompt?: string
452
+ /** Free-form; never interpreted by the hub (find launches and sessions by them). */
453
+ readonly labels?: Readonly<Record<string, string>>
454
+ readonly end?: { readonly when?: LaunchEndWhen; readonly timeout?: string; readonly cleanup?: CleanupPolicy }
455
+ }
456
+
457
+ export interface LaunchStep {
458
+ readonly state: LaunchState
459
+ readonly reason: string
460
+ readonly at: number
461
+ readonly outcome?: LaunchOutcome
462
+ }
463
+
464
+ export interface LaunchArtifacts {
465
+ readonly branch?: string
466
+ readonly base?: string
467
+ readonly head?: string
468
+ readonly commits?: number
469
+ readonly dirty?: boolean
470
+ }
471
+
472
+ export interface WebhookView {
473
+ readonly id: string
474
+ readonly url: string
475
+ readonly events: readonly string[]
476
+ readonly by: string
477
+ readonly label?: string
478
+ readonly created: number
479
+ }
480
+
481
+ export interface WebhookDelivery {
482
+ readonly id: string
483
+ readonly event: string
484
+ readonly type: string
485
+ readonly state: "pending" | "delivered" | "failed"
486
+ readonly attempts: number
487
+ readonly created: number
488
+ readonly nextAt?: number
489
+ readonly last?: { readonly at: number; readonly status?: number; readonly error?: string }
490
+ }
491
+
492
+ /** A launch as the hub reports it (the spec with its defaults filled in). */
493
+ export interface LaunchView {
494
+ readonly id: string
495
+ readonly state: LaunchState
496
+ readonly reason: string
497
+ readonly spec: LaunchSpec
498
+ /** ms epoch */
499
+ readonly created: number
500
+ /** When it entered its state. */
501
+ readonly updated: number
502
+ /** Who launched it: `user`, `app:<token id>` or a session ID. */
503
+ readonly by?: string
504
+ readonly machineID?: string
505
+ readonly directory?: string
506
+ readonly sessionID?: string
507
+ readonly name?: string
508
+ readonly outcome?: LaunchOutcome
509
+ readonly artifacts?: LaunchArtifacts
510
+ readonly history: readonly LaunchStep[]
511
+ }
512
+
513
+ /** POST /api/agent/launches: the launch, and whether it is new (false: the same key again). */
514
+ export interface LaunchCreated {
515
+ readonly launch: LaunchView
516
+ readonly created: boolean
517
+ }
@@ -0,0 +1,102 @@
1
+ // Webhook verification for receivers (design §6.1; decision znbg8xa5 #8). The hub signs every webhook POST with
2
+ // Hub-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
3
+ // (format: packages/protocol/src/webhook.ts). verifyWebhook checks it against the receiver's secret(s) in constant
4
+ // time and refuses timestamps outside the tolerance (replay). Verify the raw body bytes as received, before parsing.
5
+ import { createHmac, timingSafeEqual } from "node:crypto"
6
+
7
+ // The format, as in packages/protocol/src/webhook.ts (the SDK imports nothing outside its package; the tests check
8
+ // the two agree).
9
+ export const WEBHOOK_SIGNATURE_HEADER = "Hub-Signature"
10
+ export const WEBHOOK_EVENT_HEADER = "Hub-Event"
11
+ export const WEBHOOK_DELIVERY_HEADER = "Hub-Delivery"
12
+ /** Largest gap, either way, between a signature's timestamp and the receiver's clock. */
13
+ export const WEBHOOK_TOLERANCE_SECONDS = 300
14
+ const SCHEME = "v1"
15
+ const ALGORITHM = "sha256"
16
+ const HEADER_MAX = 2048
17
+ const HEX_SHA256 = /^[0-9a-f]{64}$/
18
+
19
+ /** A Hub-Signature header's timestamp (unix seconds) and v1 signatures, or undefined when it is malformed. */
20
+ function parseSignature(value: string): { t: number; v1: string[] } | undefined {
21
+ if (value.length > HEADER_MAX) return undefined
22
+ let t: number | undefined
23
+ const v1: string[] = []
24
+ for (const part of value.split(",")) {
25
+ const eq = part.indexOf("=")
26
+ if (eq < 0) return undefined
27
+ const key = part.slice(0, eq).trim()
28
+ const v = part.slice(eq + 1).trim()
29
+ if (key === "t") {
30
+ if (t !== undefined || !/^\d{1,12}$/.test(v)) return undefined
31
+ t = Number(v)
32
+ } else if (key === SCHEME) {
33
+ if (!HEX_SHA256.test(v)) return undefined
34
+ v1.push(v)
35
+ }
36
+ }
37
+ return t !== undefined && v1.length > 0 ? { t, v1 } : undefined
38
+ }
39
+
40
+ /** Request headers: a Headers, or a plain object such as node's IncomingMessage.headers (any key case). */
41
+ export type WebhookHeaders = Headers | Readonly<Record<string, string | readonly string[] | undefined>>
42
+
43
+ export interface VerifyWebhookInput {
44
+ /** The raw request body, exactly as received. */
45
+ readonly body: string | Uint8Array
46
+ /** The Hub-Signature header value (or give `headers`). */
47
+ readonly signature?: string | null
48
+ /** The request's headers: Hub-Signature, and Hub-Event / Hub-Delivery for the answer. */
49
+ readonly headers?: WebhookHeaders
50
+ /** The registration's secret; several while one rotates (any may match). */
51
+ readonly secret: string | readonly string[]
52
+ /** The receiver's clock in ms (default Date.now()). */
53
+ readonly now?: number
54
+ /** Seconds a timestamp may be off either way (default WEBHOOK_TOLERANCE_SECONDS). */
55
+ readonly tolerance?: number
56
+ }
57
+
58
+ export type WebhookVerification =
59
+ | { readonly ok: true; readonly timestamp: number; readonly event?: string; readonly delivery?: string }
60
+ | { readonly ok: false; readonly reason: "missing" | "malformed" | "stale" | "mismatch" }
61
+
62
+ function header(headers: WebhookHeaders | undefined, name: string): string | undefined {
63
+ if (!headers) return undefined
64
+ if (typeof (headers as Headers).get === "function") return (headers as Headers).get(name) ?? undefined
65
+ const wanted = name.toLowerCase()
66
+ for (const [key, value] of Object.entries(headers as Record<string, string | readonly string[] | undefined>)) {
67
+ if (key.toLowerCase() !== wanted || value === undefined) continue
68
+ return typeof value === "string" ? value : value[0]
69
+ }
70
+ return undefined
71
+ }
72
+
73
+ const text = (body: string | Uint8Array) => (typeof body === "string" ? body : new TextDecoder().decode(body))
74
+
75
+ const hmac = (secret: string, t: number, body: string): Buffer => createHmac(ALGORITHM, secret).update(`${t}.${body}`).digest()
76
+
77
+ /** Whether a Hub-Signature matches the body under one of the secrets, and is fresh. */
78
+ export function verifyWebhook(input: VerifyWebhookInput): WebhookVerification {
79
+ const raw = input.signature ?? header(input.headers, WEBHOOK_SIGNATURE_HEADER)
80
+ if (raw === undefined || raw === null || raw === "") return { ok: false, reason: "missing" }
81
+ const parsed = parseSignature(raw)
82
+ if (!parsed) return { ok: false, reason: "malformed" }
83
+ if (Math.abs((input.now ?? Date.now()) / 1000 - parsed.t) > (input.tolerance ?? WEBHOOK_TOLERANCE_SECONDS)) return { ok: false, reason: "stale" }
84
+ const body = text(input.body)
85
+ const secrets = (typeof input.secret === "string" ? [input.secret] : input.secret).filter((s) => s.length > 0)
86
+ const given = parsed.v1.map((hex) => Buffer.from(hex, "hex"))
87
+ // Every comparison runs (no early exit), each in constant time.
88
+ let match = false
89
+ for (const secret of secrets) {
90
+ const expected = hmac(secret, parsed.t, body)
91
+ for (const signature of given) if (signature.length === expected.length && timingSafeEqual(signature, expected)) match = true
92
+ }
93
+ if (!match) return { ok: false, reason: "mismatch" }
94
+ const event = header(input.headers, WEBHOOK_EVENT_HEADER)
95
+ const delivery = header(input.headers, WEBHOOK_DELIVERY_HEADER)
96
+ return { ok: true, timestamp: parsed.t, ...(event !== undefined ? { event } : {}), ...(delivery !== undefined ? { delivery } : {}) }
97
+ }
98
+
99
+ /** The Hub-Signature header the hub would send for `body` (for testing receivers); `t` in unix seconds, default now. */
100
+ export function signWebhook(body: string | Uint8Array, secret: string, t: number = Math.floor(Date.now() / 1000)): string {
101
+ return `t=${t},${SCHEME}=${hmac(secret, t, text(body)).toString("hex")}`
102
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@huynle/hub-spoke-sdk",
3
+ "version": "0.7.2",
4
+ "private": true,
5
+ "description": "Harness-neutral spoke building blocks: hub connection, enrollment, tokens, idempotent deliver, prompt references, HarnessSpoke (internal; shipped inside @huynle/hub, not published on its own).",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": "./src/index.ts",
10
+ "./package.json": "./package.json"
11
+ }
12
+ }
@@ -0,0 +1,3 @@
1
+ // Activity compaction (tool inputs cut to short summaries, no outputs) is part of the wire format and lives in the
2
+ // protocol package; the spoke SDK re-exports it so a spoke needs one import.
3
+ export { compactActivity, compactStatus, jobFromInfo, summarizeToolInput } from "../../protocol/src/index.ts"