@huynle/hub 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/Dockerfile +3 -0
  3. package/README.md +148 -11
  4. package/package.json +3 -1
  5. package/packages/claude-code/package.json +1 -1
  6. package/packages/claude-code/src/main.ts +4 -4
  7. package/packages/claude-code/src/wrapper.ts +3 -2
  8. package/packages/cli/dist/hub-cli.js +19334 -7849
  9. package/packages/cli/package.json +11 -0
  10. package/packages/cli/src/acting.ts +70 -0
  11. package/packages/cli/src/args.ts +91 -0
  12. package/packages/cli/src/bootstrap/adapters/archive-stream.ts +276 -0
  13. package/packages/cli/src/bootstrap/adapters/claude/README.md +63 -0
  14. package/packages/cli/src/bootstrap/adapters/claude/adapter.ts +250 -0
  15. package/packages/cli/src/bootstrap/adapters/claude/index.ts +42 -0
  16. package/packages/cli/src/bootstrap/adapters/claude/node.ts +99 -0
  17. package/packages/cli/src/bootstrap/adapters/claude/openpgp.ts +221 -0
  18. package/packages/cli/src/bootstrap/adapters/claude/ports.ts +44 -0
  19. package/packages/cli/src/bootstrap/adapters/claude/source.ts +160 -0
  20. package/packages/cli/src/bootstrap/adapters/claude/verify.ts +165 -0
  21. package/packages/cli/src/bootstrap/adapters/codex/archive.ts +103 -0
  22. package/packages/cli/src/bootstrap/adapters/codex/index.ts +60 -0
  23. package/packages/cli/src/bootstrap/adapters/codex/plan.ts +129 -0
  24. package/packages/cli/src/bootstrap/adapters/codex/sigstore.ts +298 -0
  25. package/packages/cli/src/bootstrap/adapters/codex/source.ts +102 -0
  26. package/packages/cli/src/bootstrap/adapters/codex/verify.ts +298 -0
  27. package/packages/cli/src/bootstrap/adapters/inflate.ts +433 -0
  28. package/packages/cli/src/bootstrap/adapters/node-services/control.ts +392 -0
  29. package/packages/cli/src/bootstrap/adapters/node-services/index.ts +23 -0
  30. package/packages/cli/src/bootstrap/adapters/node-services/launch.ts +223 -0
  31. package/packages/cli/src/bootstrap/adapters/node-services/plan.ts +222 -0
  32. package/packages/cli/src/bootstrap/adapters/node-services/render.ts +183 -0
  33. package/packages/cli/src/bootstrap/adapters/node-services/validate.ts +197 -0
  34. package/packages/cli/src/bootstrap/adapters/opencode/README.md +204 -0
  35. package/packages/cli/src/bootstrap/adapters/opencode/apply.ts +205 -0
  36. package/packages/cli/src/bootstrap/adapters/opencode/archive.ts +91 -0
  37. package/packages/cli/src/bootstrap/adapters/opencode/config.ts +212 -0
  38. package/packages/cli/src/bootstrap/adapters/opencode/index.ts +81 -0
  39. package/packages/cli/src/bootstrap/adapters/opencode/plan.ts +266 -0
  40. package/packages/cli/src/bootstrap/adapters/opencode/verify.ts +208 -0
  41. package/packages/cli/src/bootstrap/adapters/readiness.ts +19 -0
  42. package/packages/cli/src/bootstrap/ansible/index.ts +2 -0
  43. package/packages/cli/src/bootstrap/ansible/render.ts +279 -0
  44. package/packages/cli/src/bootstrap/contracts/actions.ts +470 -0
  45. package/packages/cli/src/bootstrap/contracts/artifact.ts +323 -0
  46. package/packages/cli/src/bootstrap/contracts/canonical.ts +54 -0
  47. package/packages/cli/src/bootstrap/contracts/credentials.ts +108 -0
  48. package/packages/cli/src/bootstrap/contracts/discovery.ts +215 -0
  49. package/packages/cli/src/bootstrap/contracts/envelope.ts +143 -0
  50. package/packages/cli/src/bootstrap/contracts/exec.ts +74 -0
  51. package/packages/cli/src/bootstrap/contracts/harness.ts +202 -0
  52. package/packages/cli/src/bootstrap/contracts/index.ts +28 -0
  53. package/packages/cli/src/bootstrap/contracts/journal.ts +198 -0
  54. package/packages/cli/src/bootstrap/contracts/paths.ts +91 -0
  55. package/packages/cli/src/bootstrap/contracts/plan.ts +155 -0
  56. package/packages/cli/src/bootstrap/contracts/platform.ts +62 -0
  57. package/packages/cli/src/bootstrap/contracts/preview.ts +190 -0
  58. package/packages/cli/src/bootstrap/contracts/profile.ts +105 -0
  59. package/packages/cli/src/bootstrap/contracts/secrets.ts +52 -0
  60. package/packages/cli/src/bootstrap/contracts/validate.ts +212 -0
  61. package/packages/cli/src/bootstrap/discovery/discover.ts +146 -0
  62. package/packages/cli/src/bootstrap/discovery/executables.ts +134 -0
  63. package/packages/cli/src/bootstrap/discovery/host.ts +96 -0
  64. package/packages/cli/src/bootstrap/discovery/index.ts +6 -0
  65. package/packages/cli/src/bootstrap/discovery/node-probe.ts +97 -0
  66. package/packages/cli/src/bootstrap/discovery/probe.ts +41 -0
  67. package/packages/cli/src/bootstrap/discovery/util.ts +13 -0
  68. package/packages/cli/src/bootstrap/enrollment/enroll.ts +226 -0
  69. package/packages/cli/src/bootstrap/enrollment/index.ts +7 -0
  70. package/packages/cli/src/bootstrap/enrollment/policy.ts +54 -0
  71. package/packages/cli/src/bootstrap/enrollment/result.ts +63 -0
  72. package/packages/cli/src/bootstrap/enrollment/status.ts +119 -0
  73. package/packages/cli/src/bootstrap/enrollment/store.ts +173 -0
  74. package/packages/cli/src/bootstrap/preview/index.ts +5 -0
  75. package/packages/cli/src/bootstrap/preview/links.ts +31 -0
  76. package/packages/cli/src/bootstrap/preview/plan.ts +242 -0
  77. package/packages/cli/src/bootstrap/preview/preview.ts +104 -0
  78. package/packages/cli/src/bootstrap/preview/render.ts +30 -0
  79. package/packages/cli/src/bootstrap/profiles/audit.ts +87 -0
  80. package/packages/cli/src/bootstrap/profiles/catalog.ts +49 -0
  81. package/packages/cli/src/bootstrap/profiles/desired.ts +96 -0
  82. package/packages/cli/src/bootstrap/profiles/drift.ts +286 -0
  83. package/packages/cli/src/bootstrap/profiles/history.ts +263 -0
  84. package/packages/cli/src/bootstrap/profiles/index.ts +11 -0
  85. package/packages/cli/src/bootstrap/profiles/upgrade.ts +276 -0
  86. package/packages/cli/src/bootstrap/profiles/versions.ts +50 -0
  87. package/packages/cli/src/bootstrap/release/README.md +115 -0
  88. package/packages/cli/src/bootstrap/release/assets.ts +39 -0
  89. package/packages/cli/src/bootstrap/release/cli.ts +332 -0
  90. package/packages/cli/src/bootstrap/release/harness-pins.json +94 -0
  91. package/packages/cli/src/bootstrap/release/manifest.ts +118 -0
  92. package/packages/cli/src/bootstrap/release/pins.ts +192 -0
  93. package/packages/cli/src/bootstrap/release/text-assets.d.ts +9 -0
  94. package/packages/cli/src/bootstrap/release/trust-root.json +13 -0
  95. package/packages/cli/src/bootstrap/release/trust-root.ts +39 -0
  96. package/packages/cli/src/bootstrap/release/verify.ts +88 -0
  97. package/packages/cli/src/bootstrap/transaction/builtin.ts +312 -0
  98. package/packages/cli/src/bootstrap/transaction/engine.ts +994 -0
  99. package/packages/cli/src/bootstrap/transaction/executor.ts +112 -0
  100. package/packages/cli/src/bootstrap/transaction/fs.ts +194 -0
  101. package/packages/cli/src/bootstrap/transaction/index.ts +12 -0
  102. package/packages/cli/src/bootstrap/transaction/json-merge.ts +403 -0
  103. package/packages/cli/src/bootstrap/transaction/lock.ts +132 -0
  104. package/packages/cli/src/bootstrap/transaction/resolve.ts +48 -0
  105. package/packages/cli/src/bootstrap/transaction/result.ts +60 -0
  106. package/packages/cli/src/bootstrap/transaction/state.ts +88 -0
  107. package/packages/cli/src/bootstrap/transaction/store.ts +189 -0
  108. package/packages/cli/src/bootstrap/transaction/stream.ts +254 -0
  109. package/packages/cli/src/cli.ts +814 -0
  110. package/packages/cli/src/credentials.ts +75 -0
  111. package/packages/cli/src/decisions.ts +106 -0
  112. package/packages/cli/src/format.ts +185 -0
  113. package/packages/cli/src/grants.ts +105 -0
  114. package/packages/cli/src/http.ts +86 -0
  115. package/packages/cli/src/launches.ts +157 -0
  116. package/packages/cli/src/login.ts +160 -0
  117. package/packages/cli/src/main.ts +63 -0
  118. package/packages/cli/src/mcp.ts +879 -0
  119. package/packages/cli/src/messaging.ts +108 -0
  120. package/packages/cli/src/node.ts +279 -0
  121. package/packages/cli/src/openshell.ts +166 -0
  122. package/packages/cli/src/setup/commands.ts +809 -0
  123. package/packages/cli/src/setup/contributors.ts +171 -0
  124. package/packages/cli/src/setup/dispatch.ts +74 -0
  125. package/packages/cli/src/setup/launch-driver.ts +109 -0
  126. package/packages/cli/src/setup/runtime.ts +456 -0
  127. package/packages/cli/src/setup/spoke-probe.ts +120 -0
  128. package/packages/cli/src/setup/state.ts +244 -0
  129. package/packages/cli/src/setup/user-bin.ts +39 -0
  130. package/packages/cli/src/spoke-services.ts +109 -0
  131. package/packages/cli/src/watch.ts +28 -0
  132. package/packages/codex/package.json +1 -1
  133. package/packages/codex/src/main.ts +3 -2
  134. package/packages/codex/src/spoke.ts +4 -1
  135. package/packages/codex/src/wrapper.ts +7 -3
  136. package/packages/node/package.json +1 -1
  137. package/packages/node/src/harnesses.ts +39 -4
  138. package/packages/node/src/run.ts +1 -1
  139. package/packages/preset/package.json +1 -1
  140. package/packages/protocol/package.json +1 -1
  141. package/packages/protocol/src/harness-events.ts +2 -2
  142. package/packages/protocol/src/index.ts +70 -7
  143. package/packages/runtime-openshell/package.json +1 -1
  144. package/packages/runtime-sdk/package.json +1 -1
  145. package/packages/sdk/package.json +1 -1
  146. package/packages/sdk/src/client.ts +55 -4
  147. package/packages/sdk/src/errors.ts +1 -0
  148. package/packages/sdk/src/sse.ts +4 -1
  149. package/packages/sdk/src/types.ts +59 -0
  150. package/packages/spoke-sdk/package.json +1 -1
  151. package/packages/spoke-sdk/src/enroll.ts +26 -4
  152. package/src/central/agent/app-tokens.ts +27 -17
  153. package/src/central/agent/archive-inactive.ts +66 -0
  154. package/src/central/agent/directory-routes.ts +155 -12
  155. package/src/central/agent/directory.ts +1 -1
  156. package/src/central/agent/errors.ts +2 -0
  157. package/src/central/agent/event-cursor.ts +49 -0
  158. package/src/central/agent/event-writers.ts +181 -0
  159. package/src/central/agent/events-routes.ts +148 -0
  160. package/src/central/agent/events.ts +277 -0
  161. package/src/central/agent/names.ts +1 -1
  162. package/src/central/agent/read-routes.ts +32 -36
  163. package/src/central/agent/routes.ts +14 -11
  164. package/src/central/agent/watch.ts +65 -174
  165. package/src/central/agent/webhook-routes.ts +10 -2
  166. package/src/central/agent/webhooks.ts +49 -5
  167. package/src/central/auth.ts +127 -1
  168. package/src/central/backend/index.ts +3 -0
  169. package/src/central/backend/types.ts +2 -0
  170. package/src/central/http.ts +116 -32
  171. package/src/central/mcp-http.ts +116 -0
  172. package/src/central/push.ts +14 -1
  173. package/src/central/registry.ts +34 -5
  174. package/src/central/store.ts +60 -0
  175. package/src/config.ts +70 -7
  176. package/src/index.ts +90 -91
  177. package/src/spoke/launchers.ts +109 -3
  178. package/src/spoke/spoke.ts +8 -4
  179. package/src/spoke/timeline.ts +59 -3
  180. package/src/spoke/tracker.ts +59 -6
  181. package/src/status.ts +97 -6
  182. package/web/dist/apple-touch-icon.png +0 -0
  183. package/web/dist/assets/index-Dm8OmkaO.css +1 -0
  184. package/web/dist/assets/index-FUJbj_Du.js +20 -0
  185. package/web/dist/favicon.ico +0 -0
  186. package/web/dist/icon-192.png +0 -0
  187. package/web/dist/icon-512.png +0 -0
  188. package/web/dist/icon-maskable-512.png +0 -0
  189. package/web/dist/icon.svg +5 -5
  190. package/web/dist/index.html +10 -4
  191. package/web/dist/manifest.webmanifest +2 -2
  192. package/web/dist/sw.js +4 -1
  193. package/web/dist/assets/index-D9zMbxSg.css +0 -1
  194. package/web/dist/assets/index-db1z4duA.js +0 -20
package/CHANGELOG.md CHANGED
@@ -2,6 +2,93 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.3
6
+
7
+ Session errors are visible: a quota, auth or rate-limit error in an OpenCode session now shows up in the hub.
8
+
9
+ - **Retries with OpenCode 2.2**: OpenCode 2.2 no longer sends `session.status`; the plugin translates
10
+ `session.retry.scheduled` into a `session.status` retry with `next` (time of the next try), `errorType` and
11
+ `errorStatus`, so older hubs keep working. `session.execution.failed` errors go out as `{message, type, status}` (no
12
+ provider response body). A pending retry and the last failure are re-sent after a reconnect.
13
+ - **Console**: an alert banner in the session header ("Quota exceeded (HTTP 429): …"), error and retry rows in the
14
+ transcript (with a live countdown), a "!" badge in session lists; a failed session is listed under Needs you until
15
+ it runs again or is archived.
16
+ - **Where a session runs**: the session header names machine · project › folder · git branch (a line under the
17
+ header on phones).
18
+ - **Notifications**: new `providerError` ("Model provider error", on by default) for quota, billing, auth, no-route
19
+ and unknown provider errors in any session of the tree, once per session per run. Devices with it off still get
20
+ `failed`. A root's and a subagent's notifications no longer replace each other on the phone.
21
+
22
+ ## 0.7.2
23
+
24
+ First release with signed standalone `hubctl` binaries (`hubctl-<target>`, `hubctl-manifest.json` signed by `hub-release-1`, `HUBCTL_SHA256SUMS`) and pinned OpenCode 2.2.0 for `hubctl setup`.
25
+
26
+ - **`hub login` with a device code**: without `--token`, `hub login [--url URL] [--scope S]... [--name N]` prints an
27
+ approval link and code; you approve it in the console as yourself (`/approve`: "CLI login for <name>", every
28
+ scope as a checkbox, the requested ones ticked) and get an app token with the scopes you chose (default
29
+ `sessions:read`, `messages:send`, `messages:read`; `--scope sessions:launch` or ticking it for launching, which
30
+ also needs a grant with verb `launch`, as the output says). Stored like `--token` (owner-only, now written
31
+ atomically); the URL defaults to this machine's enrollment. `--token` and a piped token still work. New hub routes:
32
+ `POST /cli/login`, `/cli/login/poll`, `GET /api/cli-logins/:code`, `POST …/approve|deny` (devices only; single-use,
33
+ 10-minute codes; audited `cli-login.approve|deny`, never the token).
34
+ - On an enrolled machine without a user login, `hub launch`, `whoami` and the other user commands now say that the
35
+ machine enrollment (`mch_…`) is not a user login and to run `hub login`; a forbidden `hub launch` with an app token
36
+ says it needs a grant with verb `launch`.
37
+ - **`hub` on PATH for plugin-only installs**: the OpenCode plugin links `~/.local/bin/hub` to its launcher (only when
38
+ nothing of yours is there and no other `hub` is on PATH; the setup's `hub` → `hubctl` link is kept), with one hint
39
+ when `~/.local/bin` is not on PATH.
40
+
41
+ - **hubctl bootstrap**: `hubctl machine enroll|status` and `hubctl setup discover|preview|apply|verify|drift|
42
+ rollback|recover|ansible` (also as `hub …`; the program name grants nothing). A standalone hubctl enrolls the
43
+ machine with the device code, previews a deterministic typed plan (writes nothing; `--out` keeps it for apply),
44
+ and applies exactly that plan after rediscovering and re-planning: pinned OpenCode, Claude Code and Codex installs
45
+ from the signed release manifest (`--manifest`), lossless config edits, hub's user services (loaded only with
46
+ `--confirm-activation`), an optional bounded verification launch (`--verify-launch`), all journaled and undone
47
+ with `setup rollback`. Readiness comes from real probes (`setup verify`, exit 15 when not ready). `--json` prints
48
+ one `hub.bootstrap.result` envelope; exits 10–15 are new (docs/bootstrap-contracts.md §3.11).
49
+ - **hubctl bootstrap, install is not readiness (contract v1.2)**: `setup apply` on a fresh host no longer rolls
50
+ back because you have not logged in to a provider or started the harness yet (which needs the installed harness).
51
+ Readiness probes are classified: a broken install (missing or wrong binary, version mismatch, container shim, a
52
+ binary that does not run) still rolls back; a harness that only waits on you is committed as "installed, not
53
+ ready", with each probe's status and the next steps (`--json`: `data.harnesses`). `--require-ready` keeps the
54
+ strict behaviour (exit 15, rolled back); `setup verify` stays the strict gate; `--verify-launch` keeps its
55
+ harness strict. A rollback now also removes the root directories the apply created (e.g. `~/.local/bin`), and a
56
+ directory hub created that something else wrote into is kept as a note, not a conflict. The Ansible role passes
57
+ `--require-ready` only with `hub_bootstrap_apply_require_ready` and gates readiness at its verify step.
58
+ - **hubctl bootstrap, HBS-12 acceptance fixes**: `machine enroll` turns sharing on (`mode: "always"`) when
59
+ `config.json` has no mode, so the OpenCode spoke of a hubctl-enrolled machine connects (an explicit `"off"` is kept
60
+ and reported); a rerun on a converged host plans only verification (the installed hubctl is compared by digest, not
61
+ path); a hub-installed OpenCode, Claude Code or Codex is linked into `~/.local/bin` when no other one is on PATH
62
+ (with a hint when `~/.local/bin` is not on PATH); the OpenCode probes retry once when a cold OpenCode service has
63
+ not finished activating its plugins (HTTP 503); and the plugin no longer loads `@opencode/plugin`'s runtime, so it
64
+ activates faster on a cold start. Units for a standalone hubctl no longer set `BUN_BE_BUN=1` (it made the compiled
65
+ binary act as the bun CLI and crash-loop), and `--confirm-activation` now checks that each activated service stays
66
+ running: a failed or crash-looping service fails the apply with `service_failed` (exit 15), is unloaded and rolled
67
+ back, and the message points at `journalctl --user -u <unit>` or the agent's log.
68
+ - **Hub API**: `GET /spoke/presence` (machine token only, read-only) lists which harness spokes of the caller's own
69
+ machine are connected (harness, evidence, features), for `hubctl setup verify`.
70
+ - `hub node install`, `hub claude-code install` and `hub codex-spoke install` render their units from templates
71
+ embedded in the program (so a standalone hubctl can install them); a standalone hubctl's units start the binary
72
+ itself. The hints for an unenrolled machine now say `hubctl machine enroll`.
73
+ - The `hub`/`hubctl` launchers run `~/.local/bin/hubctl` when it is installed. `hub claude`, the Claude Code daemon
74
+ and the node run the Claude Code `hubctl setup` installed (HUB_CLAUDE_BIN, else hub's `claude-code.json`
75
+ `binary`, else PATH); `hub codex`, the Codex spoke and its unit the hub-installed Codex (HUB_CODEX_BIN, else
76
+ `~/.local/share/hub/harnesses/codex/current/bin/codex`, else PATH). The node's host runtime now offers `codex`
77
+ when the Codex CLI is found.
78
+
79
+ - **Archive through MCP**: `archive` and `unarchive` (and the narrow agent API
80
+ `PUT /api/agent/sessions/:addr/archive`) change the Hub's archived state without deleting a harness session.
81
+ App tokens use the new explicit `sessions:manage` scope, selectable in Settings → Agents. Existing tokens do
82
+ not gain the scope; operations are idempotent and preserve unrelated view preferences.
83
+
84
+ - Add App token management to Settings → Agents: create scoped tokens, copy the one-time bearer secret and grant-holder ID, list existing tokens and revoke them.
85
+
86
+ - **MCP over HTTP**: the hub serves `hub mcp`'s tools at `POST /mcp` (MCP Streamable HTTP, stateless JSON
87
+ responses) for MCP clients and gateways. Auth is `Authorization: Bearer <token>` (an app token with the tools'
88
+ scopes; device, session and act tokens too); a missing or bad token answers `401` with `WWW-Authenticate: Bearer`.
89
+ The tools run as that token through the agent API in process, so scopes, grants, tenancy and the audit apply.
90
+ Packages now ship `packages/cli/src` (the image copies it too).
91
+
5
92
  ## 0.7.1
6
93
 
7
94
  - **Release**: the npm package is published again. 0.7.0's publish step had no Bun for the CLI bundle, so 0.7.0 is
package/Dockerfile CHANGED
@@ -63,6 +63,9 @@ COPY packages/protocol/src ./packages/protocol/src
63
63
  # The SDK's pure envelope and address helpers (src/central/agent/envelope.ts and names.ts re-export them).
64
64
  COPY packages/sdk/package.json ./packages/sdk/
65
65
  COPY packages/sdk/src ./packages/sdk/src
66
+ # The CLI's MCP tool table (POST /mcp serves it; src/central/mcp-http.ts imports packages/cli/src/mcp.ts).
67
+ COPY packages/cli/package.json ./packages/cli/
68
+ COPY packages/cli/src ./packages/cli/src
66
69
  COPY --from=web /build/web/dist ./web/dist
67
70
 
68
71
  ENV HUB_HOST=0.0.0.0 \
package/README.md CHANGED
@@ -133,6 +133,75 @@ Optional: put the voice agent on a fast model in `~/.config/hub/config.json`:
133
133
  `"voiceModel": "anthropic/claude-haiku-4-5"` (any `provider/model` you have). Subagents keep the
134
134
  session's model.
135
135
 
136
+ ## Quick start (hubctl, before any harness)
137
+
138
+ `hubctl` is a standalone, signed program (Bun and the CLI compiled into one file; no bun, node or harness needed)
139
+ that bootstraps a machine into Hub: it enrolls the machine, then previews and applies a typed plan that installs
140
+ pinned harnesses, edits their configs losslessly and writes hub's user services. `hub` and `hubctl` are the same
141
+ program; the name grants nothing (only the machine credential and your consent flags do). Nothing is run through a
142
+ shell, and the hub never gets a way to run commands here.
143
+
144
+ ```sh
145
+ # 1. Download hubctl for your platform (darwin-arm64, darwin-x64, linux-x64, linux-arm64) and the signed manifest
146
+ # from the release, then install the binary yourself (the first apply installs the release's copy as
147
+ # ~/.local/bin/hubctl, with `hub` linked to it):
148
+ curl -fsSLo hubctl https://github.com/huynle/hub/releases/download/v<version>/hubctl-darwin-arm64 && chmod +x hubctl
149
+ curl -fsSLO https://github.com/huynle/hub/releases/download/v<version>/hubctl-manifest.json
150
+
151
+ # 2. Enroll this machine (device code: approve it on the hub; Ctrl-C cancels). One identity for every spoke here.
152
+ ./hubctl machine enroll --server https://hub.example.com
153
+ ./hubctl machine status # verified | revoked (exit 9) | offline (exit 7) | insecure (plain-http hub)
154
+
155
+ # 3. Preview: discover the host and print the exact plan and any manual edits. It writes nothing.
156
+ ./hubctl setup preview --profile session-only --manifest hubctl-manifest.json --out preview.json
157
+
158
+ # 4. Apply exactly that plan (rediscovered and re-planned; refused when the host changed since, exit 12). A harness
159
+ # that is installed and runs but still waits on you (provider login, spoke not started) is committed as
160
+ # "installed, not ready" with the next steps; --require-ready fails (exit 15) and rolls back instead.
161
+ ./hubctl setup apply --preview preview.json --manifest hubctl-manifest.json
162
+
163
+ # 5. Do the next steps it printed (e.g. `opencode auth login`, start OpenCode), then check readiness with real
164
+ # probes (version, provider login, spoke connected; exit 15 when not ready), and undo whenever you like.
165
+ hubctl setup verify
166
+ hubctl setup rollback # or --to <txID>; files you changed since are kept and reported
167
+ ```
168
+
169
+ - Profiles: `session-only` (OpenCode), `launch-node` (+ `hub node` as a user service; give
170
+ `--workspace-root <dir>`), `full-harness` (OpenCode, Claude Code, Codex and their spokes), or a profile file
171
+ (`--profile <file>` or `-` for stdin).
172
+ - Consent is explicit and never a default: `--confirm-activation` (load the user services; give it at preview and
173
+ again at apply), `--escalate` (system locations), `--allow-downgrade`.
174
+ - `--enrollment separate` leaves enrollment to `hubctl machine enroll` (the default is inline when you pass
175
+ `--server` to preview). A config behind a symlink (dotfiles) is never written through: the preview prints the
176
+ exact edit and the file it belongs in.
177
+ - `hubctl setup drift` reports hub's managed fields (in sync, missing, outdated, changed by you);
178
+ `hubctl setup recover [--resume]` finishes or rolls back an interrupted apply.
179
+ - With `--json`, every command prints one `hub.bootstrap.result` envelope; exit codes are listed in
180
+ [docs/bootstrap-contracts.md](docs/bootstrap-contracts.md#311-output-envelope-and-exit-codes) (10 conflict,
181
+ 11 artifact rejected, 12 preview invalid, 13 locked, 14 recovery required, 15 not ready).
182
+ - Many machines: the optional Ansible role in [contrib/ansible](contrib/ansible/roles/hub_bootstrap/README.md)
183
+ runs the same preview and apply (`hubctl setup ansible` renders its vars; enrollment stays a separate step).
184
+
185
+ ### Start a launch from a terminal
186
+
187
+ On a machine set up with `hubctl setup` (or the OpenCode plugin), `hub` is on your PATH: `~/.local/bin/hub` links to
188
+ hubctl, or (plugin only) to the plugin's launcher; if `~/.local/bin` is not on your PATH, both say so once. A machine
189
+ enrollment identifies the **machine**, never you, so launching from your own shell needs a user login first:
190
+
191
+ ```sh
192
+ hub login --scope sessions:launch # prints a link and a code; approve it in the console as yourself
193
+ # (the page lists the scopes; tick "sessions:launch" if you did not ask)
194
+ hub launch --repo ~/projects/app --branch feat/x --prompt "Fix the flaky test"
195
+ ```
196
+
197
+ `hub login` without `--token` is a device code (Ctrl-C cancels); its URL defaults to `HUB_URL`, your last login, else
198
+ this machine's enrollment. You get an app token with the scopes you approved (default `sessions:read`,
199
+ `messages:send`, `messages:read`), stored owner-only in `~/.config/hub/cli.json`, and revocable in Settings → Agents.
200
+ An app token launches only under a **grant** with verb `launch` covering the repo: create it in Settings → Agents, or
201
+ ask for it from the terminal (`hub grant request --project ~/projects/app --allow launch --why "…"`) and approve it.
202
+ Without a user login, `hub launch` on an enrolled machine says so (`this machine is enrolled (mch_…), but launching
203
+ needs a user login: run hub login`). `hub login --token <app token>` (or a token piped on stdin) still works.
204
+
136
205
  ## Running the hub
137
206
 
138
207
  The hub serves the web app, pairs devices, enrolls machines, and does push and (for Voice) speech. Pick one:
@@ -198,7 +267,8 @@ foreground with `hub.json`; `scripts/io.github.huynle.hub.plist` is a launchd te
198
267
  paired device or a signed-in user; spokes need machine tokens; nothing trusts the client IP. For a public
199
268
  hostname, consider [sign-in with Google](#multi-user-sign-in-with-google) or an authenticating proxy in
200
269
  front of the web app, and leave `/spoke/*`, `/auth/*`, `/healthz`, `/.well-known/hub`, the manifest and
201
- icons reachable for spokes, sign-in, app discovery and the PWA.
270
+ icons reachable for spokes, sign-in, app discovery and the PWA (and `/mcp`, bearer-authenticated, if MCP clients
271
+ or gateways use [MCP over HTTP](#over-http-mcp)).
202
272
 
203
273
  ## Configuration
204
274
 
@@ -244,6 +314,32 @@ conversation mode and utterances (403 `not_exposed` for sessions not shared with
244
314
 
245
315
  ## Agent identity (agent API)
246
316
 
317
+ **Organization.** `PUT /api/agent/sessions/:addr/archive {archived: true|false}` archives or unarchives one
318
+ session in Hub views without deleting it in its harness. Users always may; app tokens need `sessions:manage`.
319
+ The operation is idempotent, returns `{sessionID, archived, changed}`, and preserves unrelated view preferences.
320
+ MCP exposes the same operations as `archive` and `unarchive`. Naming (`POST /api/agent/name`) also takes an app
321
+ token with `sessions:manage`; role claims and releases take one with `sessions:manage` **and** a grant allowing
322
+ `roles` whose projects cover the session's directory (the session must be online), under the kill switch and hourly
323
+ cap, with every outcome in the agent audit (verbs `roles` and `roles.release`). Reserved roles stay yours to assign.
324
+ Apps and users name the session (`session`); foreign and unknown sessions are both `404 not_found`. MCP: `name`,
325
+ `role_claim` and `role_release`. Each change is one `session.named`, `role.claimed` or `role.released` event.
326
+
327
+ **Events.** `GET /api/agent/events?cursor=&wait=&limit=&type=&session=` reads a bounded per-user journal (24 h or
328
+ 10,000 events) of compact events: `session.added/removed/changed`, `blocker.opened/closed`, `machine.changed`,
329
+ `message.changed`, `launch.changed`, `session.archived`, `session.named`, `role.claimed`, `role.released`. Each is `{id, at, type, subject, data}` with IDs and states
330
+ only: never prompts, message bodies, titles, blocker summaries, launch reasons or labels. Without a cursor the answer
331
+ is empty with a cursor at the latest event; pass the returned `cursor` back to get what came after. `wait` (0–60 s)
332
+ holds the request while nothing matches; `limit` is 1–200 (default 50); `type` (exact, or `launch.*`) and `session`
333
+ may repeat and still advance the cursor over what they skip. At most 1,000 events are inspected per request; `more`
334
+ means some remain. A cursor the journal no longer covers (expired, trimmed, hub restarted) answers
335
+ `{events: [], cursor, more: false, reset: true, refresh: ["status", "sessions", "inbox", "decisions", "launches"]}`:
336
+ re-read those, then continue with the new cursor. `GET /api/agent/watch` streams the same events live over SSE
337
+ (`{type: "ready"}` first, `{type: "reset", refresh}` when its cursor is no longer covered, `event: ping` while
338
+ idle), with the same visibility and filters (`blocked=1` is `type=blocker.*`). A malformed, forged or another user's cursor is
339
+ `400 invalid_cursor`. Unknown event types are valid; ignore them. App tokens need `events:subscribe`, and see each
340
+ family only with its scope: session and machine events with `sessions:read`, archive, name and role events with `sessions:manage`,
341
+ messages they are a party to with `messages:read`, launches they started with `sessions:launch`. MCP: `events`.
342
+
247
343
  Agents, scripts and apps talk to the hub through `/api/agent/*`, a small API with its own credentials, separate
248
344
  from the console's routes. Every request resolves to exactly one **actor**:
249
345
 
@@ -251,7 +347,7 @@ from the console's routes. Every request resolves to exactly one **actor**:
251
347
  |---|---|---|
252
348
  | user | the device cookie (browsers) or the same secret as `Authorization: Bearer <device token>` (apps, CLIs; [linked with a code](#multi-user-sign-in-with-google)) | you, on one of your devices |
253
349
  | session | a **session token** `hst1.…` the hub mints for each session a spoke reports | that session's shells (`HUB_SESSION_TOKEN`) |
254
- | app | an **app token** `hat_…` you create (scopes `sessions:read`, `messages:send`, `messages:read`, `sessions:act`) | a script or tool outside a session (`hub login`) |
350
+ | app | an **app token** `hat_…` you create (scopes `sessions:read`, `sessions:manage`, `messages:send`, `messages:read`, `sessions:act`, `sessions:launch`, `events:subscribe`) | a script or tool outside a session (`hub login`) |
255
351
 
256
352
  **Session tokens in shells.** The hub mints a 24 h token for every session (subagents included) its spokes report
257
353
  for you and pushes it to the spoke, which keeps it in memory only. Every shell an agent starts through OpenCode's
@@ -296,10 +392,11 @@ cookie and a hub bearer token (400).
296
392
  | `GET /api/agent/sessions?project=&state=&machine=&label=k=v` | ✓ | ✓ | ✓ | the same sessions as a flat list |
297
393
  | `GET /api/agent/sessions/:addr` | ✓ | ✓ | ✓ | one session with full blockers and the spoke's summary (`unsupported` when its harness lacks it) |
298
394
  | `GET /api/agent/sessions/:addr/log?n=20` | ✓ | ✓ | ✓ | newest timeline rows (1–200); `422 unsupported {feature, harness}` when the spoke has no timeline, `503 offline` |
299
- | `GET /api/agent/watch?blocked=1&session=<addr>` | ✓ | ✓ | ✓ | SSE of compact events (`ready`, `state`, `blocker.opened/closed`, `session.added/removed`, `message`; no text) |
395
+ | `GET /api/agent/watch?blocked=1&session=<addr>&cursor=` | ✓ | ✓ | ✓ (+ each family's scope) | SSE of the same compact events as `/api/agent/events` (see **Events**), after `ready`; each frame's `id:` is a cursor to resume with (`?cursor=` or `Last-Event-ID`); `reset` when it is no longer covered |
396
+ | `GET /api/agent/events?cursor=&wait=&limit=&type=&session=` | ✓ | ✓ | `events:subscribe` (+ each family's scope) | compact events after a resumable cursor (see **Events** above) |
300
397
  | `GET /api/agent/who?project=&role=` | ✓ | ✓ | ✓ | named, role-holding and online sessions |
301
- | `POST /api/agent/name {name, session?}` | ✓ | own session | – | names a session (`409 name_taken`) |
302
- | `POST /api/agent/roles/claim {role, exclusive?, project?, session?}` · `…/roles/release` | ✓ | own session | – | claims (with a new `generation`) or releases a role |
398
+ | `POST /api/agent/name {name, session?}` | ✓ | own session | `sessions:manage` | names a session (`409 name_taken`) |
399
+ | `POST /api/agent/roles/claim {role, exclusive?, project?, session?}` · `…/roles/release` | ✓ | own session | `sessions:manage` + `roles` grant | claims (with a new `generation`) or releases a role; never a reserved role for an app |
303
400
  | `POST /api/agent/msg {to, text, wake?, urgent?, all?}` | ✓ | ✓ | `messages:send` | tells a session (`all`: every holder of a role); never yourself |
304
401
  | `POST /api/agent/ask {to, text, expiresIn?, wait?}` | ✓ | ✓ | `messages:send` | an ask, open until answered, declined or expired; `wait` (≤ 60 s) returns the `answer` |
305
402
  | `POST /api/agent/ask {to: "@me", text, options?, recommendation?, urgency?, tier?, deadline?, onTimeout?, about?, wait?}` | – | ✓ | `messages:send` | opens a **Decision** for the user (below): `{message, decision, answer?}` |
@@ -396,7 +493,8 @@ sessions or apps; see [Decisions and `@me`](#agent-identity-agent-api) for the r
396
493
  - The model itself: the plugin's `hub` tool (verbs `msg`, `ask`, `reply`, `decline`, `inbox`, `who`, `status`,
397
494
  `whoami`, and the [act verbs](#agent-actions-and-grants)), as the calling session ([Spoke](#spoke-opencode-plugin)).
398
495
  - You, in the OpenCode TUI: `/msg @name text` (as the session on screen) and `/who`.
399
- - Other agents and tools: [`hub mcp`](#hub-mcp-mcp-server), as the app token you logged in with.
496
+ - Other agents and tools: [`hub mcp`](#hub-mcp-mcp-server), as the app token you logged in with, or the hub's
497
+ [`POST /mcp`](#over-http-mcp) as the bearer token sent.
400
498
  - Scripts: `hub login` with an app token that has the `messages:send` (and `messages:read`) scopes.
401
499
 
402
500
  **Limits.** Per 10 minutes: 20 messages per sender→recipient pair and 300 per user (`rate_limited`, exit 6, with
@@ -448,7 +546,7 @@ where the call is visible and audited.
448
546
  reserved roles), or `app:<token id>` (one of your app tokens).
449
547
  - **scope.projects**: 1–20 absolute project directories; a session is in scope when its directory is one of them or
450
548
  inside one (on a path boundary, after resolving `.`/`..`; worktrees inside the project count).
451
- - **allow**: the verbs (`approve`, `deny`, `answer`, `prompt`, `stop`; `roles` is accepted for role management).
549
+ - **allow**: the verbs (`approve`, `deny`, `answer`, `prompt`, `stop`; `roles` lets an app token with `sessions:manage` claim and release roles of sessions in scope).
452
550
  `deny`, `answer`, `prompt` and `stop` need only the verb; `approve` also needs the request to pass the patterns.
453
551
  - **permissions**: patterns `action[:glob]` in OpenCode's wildcard syntax (`*` any run, `?` one character, a trailing
454
552
  ` *` optional; `bash` is OpenCode's `shell`; the action alone means every resource). A request is allowed only
@@ -579,6 +677,12 @@ for agents and scripts. The OpenCode plugin writes both launchers into the hub b
579
677
  session `hub` just works. A launcher runs the CLI bundle shipped in this package (`packages/cli/dist/hub-cli.js`)
580
678
  with the OpenCode binary itself (`BUN_BE_BUN=1 exec <opencode> hub-cli.js "$@"`), so neither bun nor node has to
581
679
  be installed; without Bun it falls back to `node`. The launchers are rewritten only when their content changes.
680
+ When a standalone hubctl is installed as `~/.local/bin/hubctl` (what `hubctl setup` installs), the launchers run it
681
+ instead, so shells, services and MCP clients all use that one signed program. For your own terminals the plugin also
682
+ links `~/.local/bin/hub` to its `hub` launcher, with the setup's rule for `~/.local/bin`: only when nothing is there or
683
+ it is hub's own link, never when another `hub` is on PATH (then it prints where hub's is), and only from the default
684
+ state dir (not with `HUB_STATE_DIR`). `hubctl setup`'s `hub` → `hubctl` link is kept; `~/.local/bin/hubctl` is left to
685
+ `hubctl setup`. When `~/.local/bin` is not on PATH it prints one hint.
582
686
 
583
687
  **Name clash:** GitHub's old `hub` CLI (github/hub) uses the same name. In hub sessions ours comes first on PATH;
584
688
  elsewhere use `hubctl`, or call the bundle directly (`bun …/packages/cli/dist/hub-cli.js`).
@@ -628,7 +732,8 @@ hub codex-spoke install|uninstall [--dry-run] · hub codex-spoke status the Co
628
732
  hub preset list · hub preset check <name|file>
629
733
  hub preset run <name|file> [--socket <name> | --current] [--cwd <dir>] [--resume <id>] [--no-hub] [-- args…]
630
734
  hub mcp an MCP server on stdio (see below)
631
- hub login [--url URL] [--token APP_TOKEN] · hub logout
735
+ hub login [--url URL] [--scope S]... [--name N] device code: approve it in the console, choose the scopes
736
+ hub login [--url URL] --token APP_TOKEN|- · hub logout
632
737
  hub help · hub --version
633
738
  ```
634
739
 
@@ -742,7 +847,12 @@ OpenCode (Claude Desktop, Cursor, other agents). It speaks JSON-RPC 2.0, one mes
742
847
  transport; protocol revisions `2025-06-18`, `2025-03-26` and `2024-11-05`), and answers `initialize`, `ping`,
743
848
  `tools/list` and `tools/call`; notifications (`notifications/initialized`, `notifications/cancelled`, which aborts
744
849
  the call) get no answer, other methods answer `-32601`. Its tools are the CLI's verbs: `whoami`, `status`,
745
- `sessions`, `show`, `log`, `who`, `msg`, `ask`, `reply`, `decline`, `inbox` and the act verbs `approve`, `deny`,
850
+ `sessions`, `show`, `log`, `who`, `archive`, `unarchive`, `pin`, `unpin` (`addr`), `archive_inactive`
851
+ (`preview`, `inactiveFor`, `limit`, then `previewToken`, `confirm`), `msg`, `ask`, `reply`, `decline`, `inbox`, `message_show` (`id`, `wait`),
852
+ `mute` and `unmute` (`addr`; agent-message mutes, not console notifications), `decisions` (`state`) and
853
+ `decision_show` (`id`) (reading the Decisions you asked; no tool answers one, only the user does), and
854
+ `webhook_create` (`url`, `events`, `label`), `webhooks`, `webhook_show` (`id`) and `webhook_delete` (`id`). Webhooks
855
+ carry the same canonical compact events as `events`; their signing secret appears only in `webhook_create`. The act verbs are `approve`, `deny`,
746
856
  `answer` (`answer` object or `cancel`), `prompt` (`delivery`) and `stop`, and the launch tools `launch` (`repo`
747
857
  required and absolute; `mode`, `branch`, `base`, `remote`, `key`, `name`, `roles`, `harness`, `agent`, `model`,
748
858
  `runtime`, `profile`, `machine`, `nodeLabels` and `labels` objects, `prefer`, `prompt`, `end`, `timeout`,
@@ -750,7 +860,8 @@ required and absolute; `mode`, `branch`, `base`, `remote`, `key`, `name`, `roles
750
860
  (`to`, `text`, `addr`, `id`, `requestID`, `formID`, `why`, `n`, `wake`, `urgent`, `all`, `project`, `role`, …). The act
751
861
  tools need an app token with scope `sessions:act` and a grant, the launch tools scope `sessions:launch` and a
752
862
  launch grant; `needs_user` results include the evaluation. `ask` takes `wait` of at
753
- most 60 s (one long poll; a longer wait is cut to 60) and `expires` in seconds or as `2h`. Answers are the CLI's
863
+ most 60 s (one long poll; a longer wait is cut to 60) and `expires` in seconds or as `2h`; `ask @me` reports the
864
+ Decision it opened. Answers are the CLI's
754
865
  short text; messages and answers from other sessions come in the hub's envelope and are marked as not from the
755
866
  user. Hub errors are tool results with `isError: true` and the hub's `code: message` (plus candidates and hint),
756
867
  never protocol errors. Only protocol frames go to stdout; the server ends when stdin closes (after answering the
@@ -766,9 +877,35 @@ calls in flight) or on SIGINT/SIGTERM.
766
877
  set, else the `hub login` file. MCP clients start servers without a hub session's shell environment, so in practice
767
878
  it acts as the **app token** you logged in with (needs `sessions:read`, `messages:send` and `messages:read`), not
768
879
  as an OpenCode session: messages are sent from the app, and replies to them land in its inbox (the `inbox` tool),
769
- not in a session. Naming and roles stay out (app tokens cannot use them). Inside OpenCode, agents use the plugin's
880
+ not in a session. App tokens can archive, unarchive and name sessions with `sessions:manage`, and claim or release roles with it plus a `roles` grant. Inside OpenCode, agents use the plugin's
770
881
  `hub` tool instead, which acts as the calling session.
771
882
 
883
+ #### Over HTTP (/mcp)
884
+
885
+ The hub serves the same tools at **`POST /mcp`** ([Streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http)),
886
+ for MCP clients and gateways that take a remote server URL. It is stateless: one JSON-RPC message per POST, answered
887
+ as `application/json` (no SSE stream, no `Mcp-Session-Id`); notifications and responses get `202` with no body;
888
+ batches answer `-32600`; `GET`/`DELETE` answer `405` (`Allow: POST`). `initialize` also returns short
889
+ `instructions`. Requests need `Content-Type: application/json`, an `Accept` that allows `application/json` (or
890
+ none), a body of at most 1 MiB (else `413`) and, when sent, a supported `MCP-Protocol-Version` (else `400`). A
891
+ browser `Origin` other than the hub's own is refused (`403`); requests without one (servers, gateways) are fine.
892
+
893
+ **Auth.** `Authorization: Bearer <token>`: any credential the agent API takes, in practice an **app token**
894
+ (Settings → Agents, or `POST /api/agent/app-tokens`) with the scopes the tools need: `sessions:read` (reading), `sessions:manage` (archive/unarchive, name; roles also need a `roles` grant), `events:subscribe` (events),
895
+ `messages:send` and `messages:read` (messaging), `sessions:act` plus a grant (act tools), `sessions:launch` plus a
896
+ launch grant (launch tools). Device, session and act tokens work too. No or a bad token answers `401` with
897
+ `WWW-Authenticate: Bearer realm="hub"` (no OAuth discovery). The tools call `/api/agent` as that token, inside the
898
+ hub process: scopes, grants, tenancy, kill switches and the agent audit apply as on the HTTP API, and a scope the
899
+ token lacks is an `isError` tool result. Cookies never authenticate `/mcp`.
900
+
901
+ Add it to an MCP client or gateway as a remote (HTTP) server with bearer auth, one token per user:
902
+
903
+ ```json
904
+ { "mcpServers": { "hub": { "type": "http", "url": "https://hub.example.com/mcp", "headers": { "Authorization": "Bearer hat_…" } } } }
905
+ ```
906
+
907
+ (an MCP gateway: URL `https://<hub>/mcp`, auth kind bearer, per-user credential: each user pastes their own app token).
908
+
772
909
  ## Spoke (OpenCode plugin)
773
910
 
774
911
  The plugin is a thin client ("spoke") of the hub. Every OpenCode process that loads
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huynle/hub",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Hub: a harness-agnostic central hub your AI agent harnesses connect to (OpenCode first). Follow, steer and stop sessions from your phone or browser, with notifications, optional hands-free Voice and a self-hostable server.",
5
5
  "license": "MIT",
6
6
  "author": "Huy Le",
@@ -49,6 +49,8 @@
49
49
  "packages/protocol/package.json",
50
50
  "packages/sdk/src",
51
51
  "packages/sdk/package.json",
52
+ "packages/cli/src",
53
+ "packages/cli/package.json",
52
54
  "packages/cli/dist/hub-cli.js",
53
55
  "packages/runtime-sdk/src",
54
56
  "packages/runtime-sdk/package.json",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huynle/hub-claude-code",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "private": true,
5
5
  "description": "The Claude Code hooks spoke: per-user daemon, hook bridge, plugin dir, `hub claude` wrapper and daemon-backed MCP server (internal; shipped inside @huynle/hub, not published on its own).",
6
6
  "license": "MIT",
@@ -9,7 +9,7 @@ import { spawnSync } from "node:child_process"
9
9
  import { realpathSync } from "node:fs"
10
10
  import { configPath } from "../../../src/config.ts"
11
11
  import { nodeConfigPath, readNodeConfig, readSpokeCredentials } from "../../node/src/config.ts"
12
- import { findExecutable } from "../../node/src/harnesses.ts"
12
+ import { claudeBinary, findExecutable } from "../../node/src/harnesses.ts"
13
13
  import { runHook } from "./bridge.ts"
14
14
  import { startDaemon, type DaemonLaunchOptions } from "./daemon.ts"
15
15
  import { runMcpPipe } from "./mcp.ts"
@@ -42,12 +42,12 @@ export function claudeVersion(bin: string, env: ClaudeCodeIo["env"]): string {
42
42
 
43
43
  /**
44
44
  * How the daemon starts Claude Code for hub launches, or undefined when it does not: the Claude Code CLI must be
45
- * found (HUB_CLAUDE_BIN, else `claude` on PATH) and node.json must not withhold claude-code (harnesses). Launch
45
+ * found (claudeBinary: HUB_CLAUDE_BIN, hub's claude-code.json, else `claude` on PATH) and node.json must not withhold claude-code (harnesses). Launch
46
46
  * directories must be under node.json's workspaceRoots (read again at each create); launched sessions load the hub
47
47
  * plugin `hub claude` loads (written when the first launch needs it).
48
48
  */
49
49
  export function daemonLaunchOptions(io: { readonly env: ClaudeCodeIo["env"]; readonly home: string; readonly program: Program }): DaemonLaunchOptions | undefined {
50
- const bin = findExecutable(io.env.HUB_CLAUDE_BIN || "claude", io.env)
50
+ const bin = findExecutable(claudeBinary(io.env, io.home), io.env)
51
51
  if (!bin) return undefined
52
52
  const file = nodeConfigPath(io.env, io.home)
53
53
  if (readNodeConfig(file).config.harnesses?.["claude-code"] === false) return undefined
@@ -83,7 +83,7 @@ async function daemon(io: ClaudeCodeIo): Promise<number> {
83
83
  socketPath: socketPath(io.env, io.home),
84
84
  server: credentials.server,
85
85
  token: credentials.token,
86
- harnessVersion: claudeVersion(io.env.HUB_CLAUDE_BIN || "claude", io.env),
86
+ harnessVersion: claudeVersion(claudeBinary(io.env, io.home), io.env),
87
87
  pluginVersion: VERSION,
88
88
  directory: io.home,
89
89
  log,
@@ -7,7 +7,7 @@
7
7
  // asyncRewake Stop hook would hold the run until its timeout). On a machine that is not enrolled (no machine token) or when the daemon
8
8
  // cannot be started, Claude Code runs without the hub, and the command says why.
9
9
  //
10
- // Settings (environment): HUB_CLAUDE_BIN (default `claude` on PATH), HUB_CLAUDE_PERMISSION_WAIT (seconds a
10
+ // Settings (environment): HUB_CLAUDE_BIN (default: hub's claude-code.json `binary`, else `claude` on PATH), HUB_CLAUDE_PERMISSION_WAIT (seconds a
11
11
  // permission request waits for the hub, default 120; 0: report only), HUB_CLAUDE_REWAKE_TIMEOUT (seconds the
12
12
  // asyncRewake Stop hook may wait for a message, default 3600).
13
13
  import { spawn } from "node:child_process"
@@ -16,6 +16,7 @@ import { closeSync, openSync } from "node:fs"
16
16
  import path from "node:path"
17
17
  import { configPath } from "../../../src/config.ts"
18
18
  import { readSpokeCredentials } from "../../node/src/config.ts"
19
+ import { claudeBinary } from "../../node/src/harnesses.ts"
19
20
  import pkg from "../package.json" with { type: "json" }
20
21
  import { writePlugin } from "./plugin.ts"
21
22
  import { claudeStateDir, ensurePrivateDir, socketPath } from "./paths.ts"
@@ -149,7 +150,7 @@ export async function runClaude(args: readonly string[], io: ClaudeIo): Promise<
149
150
  if (printWithoutStreamingInput(args)) env.HUB_CLAUDE_REWAKE = "off"
150
151
  await ask(socket, { v: WIRE_VERSION, kind: "launch", launch, pid: process.pid }, { timeoutMs: ASK_MS })
151
152
  }
152
- const code = await spawnClaude(io.env.HUB_CLAUDE_BIN || "claude", plugin ? ["--plugin-dir", plugin, ...args] : [...args], env, io.stderr)
153
+ const code = await spawnClaude(claudeBinary(io.env, io.home), plugin ? ["--plugin-dir", plugin, ...args] : [...args], env, io.stderr)
153
154
  if (plugin) await ask(socket, { v: WIRE_VERSION, kind: "exit", launch }, { timeoutMs: ASK_MS })
154
155
  return code
155
156
  }