@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,168 @@
1
+ // JSON-RPC as Codex's app-server speaks it (codex app-server, P7-T0 spike v0hsdqhm): one JSON object per message,
2
+ // without a "jsonrpc" member. Requests both ways: the client calls methods (thread/start, turn/start …) and the
3
+ // server asks its clients (approvals), each side numbering its own ids. Notifications carry the thread's events.
4
+ // The channel is any transport of whole text messages: lines over a child's stdio (stdio.ts) or WebSocket text frames
5
+ // over the shared daemon's Unix socket (websocket.ts).
6
+
7
+ /** A transport of whole text messages. */
8
+ export interface Channel {
9
+ send(text: string): void
10
+ close(): void
11
+ onMessage(cb: (text: string) => void): void
12
+ /** Called once, when the channel closed (either side). */
13
+ onClose(cb: (reason: string) => void): void
14
+ }
15
+
16
+ export type RpcID = number | string
17
+
18
+ export class RpcError extends Error {
19
+ constructor(
20
+ message: string,
21
+ readonly code: number,
22
+ readonly method?: string,
23
+ ) {
24
+ super(message)
25
+ this.name = "RpcError"
26
+ }
27
+ }
28
+
29
+ /** JSON-RPC's code for a request that could not be sent or answered (this side's own failures). */
30
+ export const LOCAL_ERROR = -32000
31
+
32
+ export interface PeerHandlers {
33
+ onNotification?(method: string, params: unknown): void
34
+ onRequest?(id: RpcID, method: string, params: unknown): void
35
+ }
36
+
37
+ const DEFAULT_TIMEOUT_MS = 60_000
38
+
39
+ interface Pending {
40
+ readonly method: string
41
+ resolve(value: unknown): void
42
+ reject(error: Error): void
43
+ readonly timer: ReturnType<typeof setTimeout>
44
+ }
45
+
46
+ const isObj = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v)
47
+ const isID = (v: unknown): v is RpcID => typeof v === "number" || typeof v === "string"
48
+
49
+ export class JsonRpcPeer {
50
+ readonly closed: Promise<string>
51
+ #closedReason: string | undefined
52
+ #next = 0
53
+ readonly #pending = new Map<RpcID, Pending>()
54
+ readonly #channel: Channel
55
+ readonly #handlers: PeerHandlers
56
+
57
+ constructor(channel: Channel, handlers: PeerHandlers = {}) {
58
+ this.#channel = channel
59
+ this.#handlers = handlers
60
+ let done!: (reason: string) => void
61
+ this.closed = new Promise((resolve) => (done = resolve))
62
+ channel.onMessage((text) => this.#receive(text))
63
+ channel.onClose((reason) => {
64
+ if (this.#closedReason !== undefined) return
65
+ this.#closedReason = reason || "closed"
66
+ for (const [id, call] of this.#pending) {
67
+ clearTimeout(call.timer)
68
+ call.reject(new RpcError(`The app-server connection closed before it answered ${call.method} (${this.#closedReason})`, LOCAL_ERROR, call.method))
69
+ this.#pending.delete(id)
70
+ }
71
+ done(this.#closedReason)
72
+ })
73
+ }
74
+
75
+ get isClosed(): boolean {
76
+ return this.#closedReason !== undefined
77
+ }
78
+
79
+ request(method: string, params?: unknown, options: { readonly timeoutMs?: number } = {}): Promise<any> {
80
+ if (this.#closedReason !== undefined) return Promise.reject(new RpcError(`The app-server connection is closed (${this.#closedReason})`, LOCAL_ERROR, method))
81
+ const id = ++this.#next
82
+ return new Promise((resolve, reject) => {
83
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
84
+ const timer = setTimeout(() => {
85
+ this.#pending.delete(id)
86
+ reject(new RpcError(`${method}: the app-server did not answer within ${timeoutMs} ms`, LOCAL_ERROR, method))
87
+ }, timeoutMs)
88
+ this.#pending.set(id, { method, resolve, reject, timer })
89
+ this.#send(params === undefined ? { id, method } : { id, method, params })
90
+ })
91
+ }
92
+
93
+ notify(method: string, params?: unknown): void {
94
+ this.#send(params === undefined ? { method } : { method, params })
95
+ }
96
+
97
+ /** Answers a server request. */
98
+ respond(id: RpcID, result: unknown): void {
99
+ this.#send({ id, result })
100
+ }
101
+
102
+ respondError(id: RpcID, code: number, message: string): void {
103
+ this.#send({ id, error: { code, message } })
104
+ }
105
+
106
+ close(): void {
107
+ this.#channel.close()
108
+ }
109
+
110
+ #send(message: object): void {
111
+ if (this.#closedReason !== undefined) return
112
+ try {
113
+ this.#channel.send(JSON.stringify(message))
114
+ } catch {
115
+ // The channel reports its own close; a request then fails there.
116
+ }
117
+ }
118
+
119
+ #receive(text: string): void {
120
+ let message: unknown
121
+ try {
122
+ message = JSON.parse(text)
123
+ } catch {
124
+ return
125
+ }
126
+ if (!isObj(message)) return
127
+ if (typeof message.method === "string") {
128
+ if (isID(message.id)) this.#handlers.onRequest?.(message.id, message.method, message.params)
129
+ else this.#handlers.onNotification?.(message.method, message.params)
130
+ return
131
+ }
132
+ if (!isID(message.id)) return
133
+ const call = this.#pending.get(message.id)
134
+ if (!call) return
135
+ this.#pending.delete(message.id)
136
+ clearTimeout(call.timer)
137
+ if (isObj(message.error)) {
138
+ const code = typeof message.error.code === "number" ? message.error.code : LOCAL_ERROR
139
+ call.reject(new RpcError(`${call.method}: ${typeof message.error.message === "string" ? message.error.message : "error"}`, code, call.method))
140
+ } else call.resolve(message.result)
141
+ }
142
+ }
143
+
144
+ /** Two connected in-memory channels (tests, and in-process fakes). */
145
+ export function memoryChannelPair(): [Channel, Channel] {
146
+ type End = { listeners: ((t: string) => void)[]; closers: ((r: string) => void)[]; closed: boolean }
147
+ const ends: [End, End] = [
148
+ { listeners: [], closers: [], closed: false },
149
+ { listeners: [], closers: [], closed: false },
150
+ ]
151
+ const close = (reason: string) => {
152
+ for (const end of ends) {
153
+ if (end.closed) continue
154
+ end.closed = true
155
+ for (const cb of end.closers) cb(reason)
156
+ }
157
+ }
158
+ const channel = (self: End, other: End): Channel => ({
159
+ send(text) {
160
+ if (self.closed) throw new Error("channel closed")
161
+ for (const cb of other.listeners) cb(text)
162
+ },
163
+ close: () => close("closed"),
164
+ onMessage: (cb) => void self.listeners.push(cb),
165
+ onClose: (cb) => void self.closers.push(cb),
166
+ })
167
+ return [channel(ends[0], ends[1]), channel(ends[1], ends[0])]
168
+ }
@@ -0,0 +1,123 @@
1
+ // `hub codex-spoke serve`: the per-user Codex spoke service until SIGINT/SIGTERM (main.ts aborts io.signal), started
2
+ // in the background by `hub codex` and never typed by hand in normal use. It needs an enrolled machine (the machine
3
+ // token in the spoke's config.json, read-only), keeps a pid file in <hub state dir>/codex/spoke.pid (one service per
4
+ // user: a second one refuses while the first runs) and attaches to the shared Codex daemon's socket when present.
5
+ //
6
+ // Settings (environment): HUB_CODEX_BIN (default: the Codex `hubctl setup` installed, else `codex` on PATH), HUB_CODEX_DAEMON_SOCKET (another socket, or
7
+ // "off"), HUB_CODEX_CONFIG (Codex config overrides for the app-server the spoke starts, one key=value per line,
8
+ // passed as -c; Codex's files are never written).
9
+ import { spawnSync } from "node:child_process"
10
+ import { openSync, closeSync, readFileSync, rmSync, writeSync } from "node:fs"
11
+ import { configPath } from "../../../src/config.ts"
12
+ import { readSpokeCredentials } from "../../node/src/config.ts"
13
+ import { codexBinary } from "../../node/src/harnesses.ts"
14
+ import pkg from "../package.json" with { type: "json" }
15
+ import { codexStateDir, ensurePrivateDir, daemonSocketPath, pidFile, readServicePid } from "./paths.ts"
16
+ import { startCodexSpoke } from "./spoke.ts"
17
+
18
+ export const VERSION: string = pkg.version
19
+
20
+ export interface CodexMainIo {
21
+ readonly env: Readonly<Record<string, string | undefined>>
22
+ readonly home: string
23
+ stdout(s: string): void
24
+ stderr(s: string): void
25
+ readonly signal?: AbortSignal
26
+ }
27
+
28
+ export const CODEX_SPOKE_USAGE = "Usage: hub codex-spoke serve | install [--dry-run] | uninstall [--dry-run] | status"
29
+
30
+ /** `codex --version` → "0.160.0", or "" when it cannot be told. */
31
+ export function codexVersion(bin: string, env: CodexMainIo["env"]): string {
32
+ try {
33
+ const r = spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 5000, env: env as NodeJS.ProcessEnv })
34
+ return /\d+\.\d+\.\d+[\w.+-]*/.exec(r.stdout ?? "")?.[0]?.slice(0, 100) ?? ""
35
+ } catch {
36
+ return ""
37
+ }
38
+ }
39
+
40
+ /** HUB_CODEX_CONFIG's lines as `-c key=value` arguments. */
41
+ export function configOverrides(value: string | undefined): string[] {
42
+ return (value ?? "")
43
+ .split("\n")
44
+ .map((line) => line.trim())
45
+ .filter((line) => line !== "" && line.includes("="))
46
+ .flatMap((line) => ["-c", line])
47
+ }
48
+
49
+ /** Claims the pid file for this process; false when another live service holds it. */
50
+ function claimPidFile(file: string, env: CodexMainIo["env"], home: string): boolean {
51
+ for (let attempt = 0; attempt < 2; attempt++) {
52
+ try {
53
+ const fd = openSync(file, "wx", 0o600)
54
+ try {
55
+ writeSync(fd, `${process.pid}\n`)
56
+ } finally {
57
+ closeSync(fd)
58
+ }
59
+ return true
60
+ } catch (cause) {
61
+ if ((cause as NodeJS.ErrnoException).code !== "EEXIST") throw cause
62
+ if (readServicePid(env, home) !== undefined) return false
63
+ rmSync(file, { force: true }) // a stale file: its process is gone
64
+ }
65
+ }
66
+ return false
67
+ }
68
+
69
+ function releasePidFile(file: string): void {
70
+ try {
71
+ if (readFileSync(file, "utf8").trim() === String(process.pid)) rmSync(file, { force: true })
72
+ } catch {}
73
+ }
74
+
75
+ async function serve(io: CodexMainIo): Promise<number> {
76
+ const credentials = readSpokeCredentials(io.env, io.home)
77
+ if (!credentials) {
78
+ io.stderr(`hub codex-spoke serve: this machine is not enrolled with a hub (no machine token in ${configPath(io.env, io.home)})\n`)
79
+ return 1
80
+ }
81
+ const log = (line: string) => io.stderr(`[hub codex-spoke] ${new Date().toISOString()} ${line}\n`)
82
+ ensurePrivateDir(codexStateDir(io.env, io.home))
83
+ const file = pidFile(io.env, io.home)
84
+ if (!claimPidFile(file, io.env, io.home)) {
85
+ io.stderr(`hub codex-spoke serve: already running (pid ${readServicePid(io.env, io.home) ?? "?"})\n`)
86
+ return 1
87
+ }
88
+ try {
89
+ const codexBin = codexBinary(io.env, io.home)
90
+ const env: Record<string, string | undefined> = { ...io.env }
91
+ delete env.BUN_BE_BUN
92
+ const daemonSocket = daemonSocketPath(io.env, io.home)
93
+ const running = startCodexSpoke({
94
+ server: credentials.server,
95
+ token: credentials.token,
96
+ harnessVersion: codexVersion(codexBin, env),
97
+ pluginVersion: VERSION,
98
+ directory: io.home,
99
+ codexBin,
100
+ codexArgs: configOverrides(io.env.HUB_CODEX_CONFIG),
101
+ env,
102
+ ...(daemonSocket ? { daemonSocket } : {}),
103
+ log,
104
+ })
105
+ log(daemonSocket ? `serving; the Codex daemon socket is ${daemonSocket}` : "serving; not attaching to a Codex daemon")
106
+ await new Promise<void>((resolve) => {
107
+ if (!io.signal) return
108
+ if (io.signal.aborted) return resolve()
109
+ io.signal.addEventListener("abort", () => resolve(), { once: true })
110
+ })
111
+ await running.stop()
112
+ return 0
113
+ } finally {
114
+ releasePidFile(file)
115
+ }
116
+ }
117
+
118
+ export async function codexMain(argv: readonly string[], io: CodexMainIo): Promise<number> {
119
+ const [command] = argv
120
+ if (command === "serve") return serve(io)
121
+ io.stderr(`${CODEX_SPOKE_USAGE}\n`)
122
+ return 2
123
+ }
@@ -0,0 +1,65 @@
1
+ // Where the Codex spoke looks and keeps its files. It only reads Codex's home (CODEX_HOME, default ~/.codex) to find
2
+ // the shared daemon's socket; its own files live in <hub state dir>/codex (the hub state dir is $HUB_STATE_DIR, else
3
+ // ~/.local/state/hub), a private (0700) directory holding the service's pid file (spoke.pid) and its log.
4
+ // $HUB_CODEX_DAEMON_SOCKET names another daemon socket, or "off" for none.
5
+ import { chmodSync, lstatSync, mkdirSync, readFileSync } from "node:fs"
6
+ import { homedir } from "node:os"
7
+ import path from "node:path"
8
+ import { hubEnv, type Env } from "../../../src/env.ts"
9
+ import { APP_DIR, stateBase } from "../../../src/migrate.ts"
10
+
11
+ export function codexHome(env: Env = process.env, home: string = homedir()): string {
12
+ return env.CODEX_HOME || path.join(home, ".codex")
13
+ }
14
+
15
+ /** The shared daemon's control socket, or undefined when attaching to it is turned off. */
16
+ export function daemonSocketPath(env: Env = process.env, home: string = homedir()): string | undefined {
17
+ const override = env.HUB_CODEX_DAEMON_SOCKET
18
+ if (override === "off") return undefined
19
+ return override || path.join(codexHome(env, home), "app-server-control", "app-server-control.sock")
20
+ }
21
+
22
+ export function codexStateDir(env: Env = process.env, home: string = homedir()): string {
23
+ return path.join(hubEnv(env).HUB_STATE_DIR || path.join(stateBase(home), APP_DIR), "codex")
24
+ }
25
+
26
+ export const pidFile = (env: Env = process.env, home: string = homedir()) => path.join(codexStateDir(env, home), "spoke.pid")
27
+
28
+ /** A usable process ID: an integer above 1 (0 and negative IDs signal process groups or every process). */
29
+ export const isPid = (n: unknown): n is number => typeof n === "number" && Number.isInteger(n) && n > 1
30
+
31
+ export function isAlive(pid: number): boolean {
32
+ if (!isPid(pid)) return false
33
+ try {
34
+ process.kill(pid, 0)
35
+ return true
36
+ } catch (cause) {
37
+ return (cause as NodeJS.ErrnoException).code === "EPERM"
38
+ }
39
+ }
40
+
41
+ /** The running service's pid from its pid file, or undefined when there is none or that process is gone. */
42
+ export function readServicePid(env: Env = process.env, home: string = homedir()): number | undefined {
43
+ let text: string
44
+ try {
45
+ text = readFileSync(pidFile(env, home), "utf8").trim()
46
+ } catch {
47
+ return undefined
48
+ }
49
+ const pid = /^\d+$/.test(text) ? Number(text) : NaN
50
+ return isPid(pid) && isAlive(pid) ? pid : undefined
51
+ }
52
+
53
+ /**
54
+ * Makes `dir` (and its parents) and checks it is a real directory of this user that nobody else can reach: a
55
+ * symlink or someone else's directory is refused, group or other permissions are removed (0700).
56
+ */
57
+ export function ensurePrivateDir(dir: string): void {
58
+ mkdirSync(dir, { recursive: true, mode: 0o700 })
59
+ const stat = lstatSync(dir)
60
+ if (stat.isSymbolicLink()) throw new Error(`${dir} is a symlink; the Codex spoke needs a real directory there`)
61
+ if (!stat.isDirectory()) throw new Error(`${dir} is not a directory`)
62
+ const uid = process.getuid?.()
63
+ if (uid !== undefined && stat.uid !== uid) throw new Error(`${dir} belongs to another user`)
64
+ if ((stat.mode & 0o777) !== 0o700) chmodSync(dir, 0o700)
65
+ }