@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,160 @@
1
+ // Session identity for shells (P1-T6, Brain decision 31swaq28): every shell an agent runs through the shell tool
2
+ // gets HUB_URL and HUB_SESSION_TOKEN for its own session, so `hub` there acts as that session; the hub bin dir
3
+ // (where the spoke writes the `hub`/`hubctl` launchers, P1-T10) goes first on PATH for every shell.
4
+ //
5
+ // OpenCode's `shell.create.before` hook has a mutable env but no sessionID; the tool `execute.before` hook has the
6
+ // sessionID and the command. In the shell tool the two run back to back (execute.before, then Shell.create runs
7
+ // create.before, then the tool's own env tweaks and the permission check), so a shell is matched to the most recent
8
+ // pending shell tool call with the same command. Not used: the OPENCODE_SESSION_ID in the create.before env — the
9
+ // shell tool sets it only after our hook, so at that point it is the server process's inherited one (of the
10
+ // session that started this OpenCode, if any), which would hand out the wrong identity.
11
+ //
12
+ // Fail closed: the same command pending from two different sessions gives neither a token; so does a command that
13
+ // another plugin rewrote in a way we cannot see. Known gap: user `!` commands (session.shell) have no tool call, so
14
+ // they get only the stripped env and PATH, no token.
15
+ import { homedir } from "node:os"
16
+ import path from "node:path"
17
+ import { defaultStateDir } from "./localhub"
18
+
19
+ type Env = Record<string, string | undefined>
20
+
21
+ /** What the tool `execute.before`/`execute.after` hooks report (the fields used here). */
22
+ export interface ToolCall {
23
+ readonly tool: string
24
+ readonly sessionID: string
25
+ readonly id: string
26
+ readonly input: unknown
27
+ }
28
+
29
+ /** The shell `create.before` invocation (the fields used here). */
30
+ export interface ShellInvocation {
31
+ readonly command: string
32
+ env: Env
33
+ }
34
+
35
+ /** A session's identity for its shells. */
36
+ export interface Identity {
37
+ readonly token: string
38
+ readonly url?: string
39
+ }
40
+
41
+ /** The OpenCode tool names that run a command in a shell. */
42
+ const SHELL_TOOLS = new Set(["shell", "bash"])
43
+ const HUB_VARS = ["HUB_URL", "HUB_SESSION_TOKEN"] as const
44
+ const DEFAULT_PATH = ["/usr/bin", "/bin", "/usr/sbin", "/sbin"]
45
+
46
+ interface Pending {
47
+ readonly call: ToolCall
48
+ readonly at: number
49
+ }
50
+
51
+ const commandOf = (call: ToolCall): string | undefined => {
52
+ const input = call.input as { command?: unknown } | null | undefined
53
+ return input && typeof input === "object" && typeof input.command === "string" ? input.command : undefined
54
+ }
55
+
56
+ /** Pairs shells with the shell tool calls that create them (see the file comment). */
57
+ export class ShellCorrelator {
58
+ readonly #pending: Pending[] = []
59
+ readonly #ttlMs: number
60
+ readonly #max: number
61
+ readonly #now: () => number
62
+
63
+ constructor(options: { ttlMs?: number; max?: number; now?: () => number } = {}) {
64
+ this.#ttlMs = options.ttlMs ?? 30_000
65
+ this.#max = options.max ?? 256
66
+ this.#now = options.now ?? Date.now
67
+ }
68
+
69
+ /** Remembers a shell tool call (other tools are ignored). */
70
+ record(call: ToolCall): void {
71
+ if (!SHELL_TOOLS.has(call.tool) || typeof call.sessionID !== "string" || commandOf(call) === undefined) return
72
+ this.#prune()
73
+ this.#pending.push({ call, at: this.#now() })
74
+ if (this.#pending.length > this.#max) this.#pending.splice(0, this.#pending.length - this.#max)
75
+ }
76
+
77
+ /** Forgets a tool call once it finished (its shell, if any, was created long before). */
78
+ done(callID: string): void {
79
+ for (let i = this.#pending.length - 1; i >= 0; i--) if (this.#pending[i]!.call.id === callID) this.#pending.splice(i, 1)
80
+ }
81
+
82
+ /**
83
+ * The session of the pending shell tool call with this command, consuming it; undefined when none or when calls
84
+ * from different sessions are pending with it (then all of them are dropped). The command is read now, so input
85
+ * rewrites by hooks that ran after ours count.
86
+ */
87
+ match(command: string): string | undefined {
88
+ this.#prune()
89
+ const matches = this.#pending.filter((p) => commandOf(p.call) === command)
90
+ if (matches.length === 0) return undefined
91
+ const sessions = new Set(matches.map((p) => p.call.sessionID))
92
+ if (sessions.size > 1) {
93
+ for (const p of matches) this.#pending.splice(this.#pending.indexOf(p), 1)
94
+ return undefined
95
+ }
96
+ this.#pending.splice(this.#pending.indexOf(matches[0]!), 1)
97
+ return matches[0]!.call.sessionID
98
+ }
99
+
100
+ get size(): number {
101
+ this.#prune()
102
+ return this.#pending.length
103
+ }
104
+
105
+ #prune(): void {
106
+ const oldest = this.#now() - this.#ttlMs
107
+ while (this.#pending.length > 0 && this.#pending[0]!.at <= oldest) this.#pending.shift()
108
+ }
109
+ }
110
+
111
+ /** Deletes HUB_URL and HUB_SESSION_TOKEN (an identity inherited from an outer session must never leak). */
112
+ export function stripInheritedHubEnv(env: Env): void {
113
+ for (const name of HUB_VARS) delete env[name]
114
+ }
115
+
116
+ /** Strips inherited HUB_*, sets the session's identity when it has one, and puts `binDir` first on PATH once. */
117
+ export function applyHubEnv(env: Env, identity: Identity | undefined, binDir: string): void {
118
+ stripInheritedHubEnv(env)
119
+ if (identity) {
120
+ if (identity.url) env.HUB_URL = identity.url
121
+ env.HUB_SESSION_TOKEN = identity.token
122
+ }
123
+ const rest = env.PATH === undefined ? DEFAULT_PATH : env.PATH.split(path.delimiter).filter((dir) => dir !== binDir)
124
+ env.PATH = [binDir, ...rest].join(path.delimiter)
125
+ }
126
+
127
+ /** Where the `hub`/`hubctl` launchers live: bin under the hub state dir ($HUB_STATE_DIR or ~/.local/state/hub). */
128
+ export function hubBinDir(env: Env = process.env, home: string = homedir()): string {
129
+ return path.join(defaultStateDir(env, home), "bin")
130
+ }
131
+
132
+ /** The hook handlers: tool execute.before/after and shell create.before. They never throw. */
133
+ export function envInjector(options: { correlator: ShellCorrelator; binDir: string; identity: (sessionID: string) => Identity | undefined }) {
134
+ const { correlator, binDir, identity } = options
135
+ return {
136
+ toolBefore(call: ToolCall): void {
137
+ try {
138
+ correlator.record(call)
139
+ } catch (cause) {
140
+ console.error("[hub] shell env: could not record a tool call", cause)
141
+ }
142
+ },
143
+ toolAfter(call: ToolCall): void {
144
+ try {
145
+ correlator.done(call.id)
146
+ } catch {
147
+ // nothing to forget
148
+ }
149
+ },
150
+ shellBefore(invocation: ShellInvocation): void {
151
+ try {
152
+ if (!invocation.env || typeof invocation.env !== "object") return
153
+ const sessionID = typeof invocation.command === "string" ? correlator.match(invocation.command) : undefined
154
+ applyHubEnv(invocation.env, sessionID ? identity(sessionID) : undefined, binDir)
155
+ } catch (cause) {
156
+ console.error("[hub] shell env: could not prepare a shell", cause)
157
+ }
158
+ },
159
+ }
160
+ }
@@ -0,0 +1,115 @@
1
+ // The TUI's /who and /msg, server side (RPC hubWho and hubMsg, registered in src/index.ts): the hub's agent API as
2
+ // the TUI's CURRENT session, through the shared SDK with that session's own token (Spoke.shellIdentity), the same
3
+ // identity the hub tool uses (src/spoke/hub-tool.ts). The token is only sent to the hub; it never appears in a
4
+ // result or an error (redacted should the hub echo it). Labels are sanitised here, so the TUI shows them as is.
5
+ import { HubClient, HubError, HubTransportError, sanitizeLabel, type AgentMessage, type DirectoryEntry } from "../../packages/sdk/src/index.ts"
6
+ import type { HubToolSpoke } from "./hub-tool"
7
+
8
+ /** The RPC error a failure answers with (src/rpc.ts lists them). */
9
+ export type HubRpcErrorCode = "invalid" | "not_connected" | "unsupported" | "unreachable" | "server_error"
10
+
11
+ export class HubRpcError extends Error {
12
+ constructor(
13
+ readonly code: HubRpcErrorCode,
14
+ message: string,
15
+ ) {
16
+ super(message)
17
+ }
18
+ }
19
+
20
+ /** One session in /who (labels sanitised; roles by bare name). */
21
+ export interface WhoRow {
22
+ readonly sessionID: string
23
+ readonly name?: string
24
+ readonly project?: string
25
+ readonly state?: string
26
+ readonly online: boolean
27
+ readonly roles: string[]
28
+ }
29
+
30
+ export interface MsgResult {
31
+ readonly id: string
32
+ /** Who it reached: @name, a session ID, or "the user". */
33
+ readonly to: string
34
+ readonly delivery?: string
35
+ }
36
+
37
+ /** An RPC call waits at most this long for the hub. */
38
+ const TIMEOUT_MS = 15_000
39
+
40
+ type Input = Record<string, unknown>
41
+
42
+ const inputOf = (value: unknown): Input => (value && typeof value === "object" ? (value as Input) : {})
43
+ const optStr = (input: Input, key: string) => (typeof input[key] === "string" && (input[key] as string).trim() ? (input[key] as string).trim() : undefined)
44
+
45
+ /** /msg: sends `text` word for word to `to` as session `sessionID`. */
46
+ export async function hubMsg(spoke: HubToolSpoke, value: unknown): Promise<MsgResult> {
47
+ const input = inputOf(value)
48
+ const sessionID = optStr(input, "sessionID")
49
+ if (!sessionID) throw new HubRpcError("invalid", "Open a session first: /msg sends as the current session.")
50
+ const to = optStr(input, "to")
51
+ if (!to) throw new HubRpcError("invalid", "Who should get it? Usage: /msg @name text")
52
+ const text = input.text
53
+ if (typeof text !== "string" || !text.trim()) throw new HubRpcError("invalid", "What should the message say? Usage: /msg @name text")
54
+ return withClient(spoke, sessionID, "messaging", async (client, signal) => {
55
+ const { message, delivery } = await client.msg(to, text, { signal })
56
+ return { id: message.id, to: recipientText(message), ...(delivery ? { delivery } : {}) }
57
+ })
58
+ }
59
+
60
+ /** /who: the named and role-holding sessions (optionally one project or one role), as session `sessionID`. */
61
+ export async function hubWho(spoke: HubToolSpoke, value: unknown): Promise<{ sessions: WhoRow[] }> {
62
+ const input = inputOf(value)
63
+ const sessionID = optStr(input, "sessionID")
64
+ if (!sessionID) throw new HubRpcError("invalid", "Open a session first: /who asks the hub as the current session.")
65
+ const project = optStr(input, "project")
66
+ const role = optStr(input, "role")
67
+ return withClient(spoke, sessionID, undefined, async (client, signal) => {
68
+ const { sessions } = await client.who({ ...(project ? { project } : {}), ...(role ? { role } : {}), signal })
69
+ return { sessions: sessions.map(whoRow) }
70
+ })
71
+ }
72
+
73
+ /** Runs `action` with a client holding the session's token; every failure becomes a HubRpcError without the token. */
74
+ async function withClient<T>(
75
+ spoke: HubToolSpoke,
76
+ sessionID: string,
77
+ feature: string | undefined,
78
+ action: (client: HubClient, signal: AbortSignal) => Promise<T>,
79
+ ): Promise<T> {
80
+ const identity = spoke.shellIdentity(sessionID)
81
+ if (!identity) throw new HubRpcError("not_connected", "This session has no hub token yet: share it with the hub (/hub) and wait until it is connected.")
82
+ if (!identity.url) throw new HubRpcError("not_connected", "The hub's URL is not known yet; try again shortly.")
83
+ if (feature && !spoke.hubSupports(feature)) throw new HubRpcError("unsupported", `The hub this machine is connected to does not support ${feature}; it needs a newer hub.`)
84
+ const scrub = (text: string) => (identity.token ? text.split(identity.token).join("[redacted]") : text)
85
+ try {
86
+ return await action(new HubClient({ url: identity.url, token: identity.token }), AbortSignal.timeout(TIMEOUT_MS))
87
+ } catch (cause) {
88
+ if (cause instanceof HubError) {
89
+ throw new HubRpcError("server_error", scrub(`${cause.message} (${cause.code})${cause.hint ? `. Hint: ${cause.hint}` : ""}`))
90
+ }
91
+ if (cause instanceof HubTransportError) throw new HubRpcError("unreachable", scrub(cause.message))
92
+ throw new HubRpcError("server_error", scrub(cause instanceof Error ? cause.message : String(cause)))
93
+ }
94
+ }
95
+
96
+ function whoRow(entry: DirectoryEntry): WhoRow {
97
+ const name = entry.name ? sanitizeLabel(entry.name) : ""
98
+ const project = entry.project ? sanitizeLabel(entry.project) : ""
99
+ const state = entry.state ? sanitizeLabel(entry.state) : ""
100
+ return {
101
+ sessionID: sanitizeLabel(entry.sessionID),
102
+ ...(name ? { name } : {}),
103
+ ...(project ? { project } : {}),
104
+ ...(state ? { state } : {}),
105
+ online: entry.online === true,
106
+ roles: (entry.roles ?? []).map((r) => sanitizeLabel(r.role)).filter(Boolean),
107
+ }
108
+ }
109
+
110
+ function recipientText(message: AgentMessage): string {
111
+ const r = message.recipient
112
+ if (r?.kind === "session") return r.name ? `@${sanitizeLabel(r.name)}` : sanitizeLabel(r.sessionID)
113
+ if (r?.kind === "user") return "the user"
114
+ return sanitizeLabel(message.to)
115
+ }