@huynle/hub 0.6.0 → 0.7.1

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 (219) hide show
  1. package/CHANGELOG.md +800 -0
  2. package/Dockerfile +30 -0
  3. package/README.md +870 -66
  4. package/package.json +45 -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 +155 -0
  21. package/packages/cli/dist/hub-cli.js +10162 -0
  22. package/packages/codex/package.json +12 -0
  23. package/packages/codex/src/app-server.ts +44 -0
  24. package/packages/codex/src/ids.ts +28 -0
  25. package/packages/codex/src/index.ts +11 -0
  26. package/packages/codex/src/jsonrpc.ts +168 -0
  27. package/packages/codex/src/main.ts +122 -0
  28. package/packages/codex/src/paths.ts +65 -0
  29. package/packages/codex/src/sessions.ts +578 -0
  30. package/packages/codex/src/spoke.ts +249 -0
  31. package/packages/codex/src/stdio.ts +101 -0
  32. package/packages/codex/src/websocket.ts +206 -0
  33. package/packages/codex/src/wrapper.ts +100 -0
  34. package/packages/node/package.json +12 -0
  35. package/packages/node/src/config.ts +184 -0
  36. package/packages/node/src/connection.ts +283 -0
  37. package/packages/node/src/daemon.ts +281 -0
  38. package/packages/node/src/git.ts +107 -0
  39. package/packages/node/src/harnesses.ts +60 -0
  40. package/packages/node/src/host.ts +319 -0
  41. package/packages/node/src/index.ts +17 -0
  42. package/packages/node/src/main.ts +11 -0
  43. package/packages/node/src/openshell.ts +73 -0
  44. package/packages/node/src/run.ts +106 -0
  45. package/packages/preset/package.json +12 -0
  46. package/packages/preset/presets/aider.json +8 -0
  47. package/packages/preset/presets/goose.json +8 -0
  48. package/packages/preset/src/index.ts +2 -0
  49. package/packages/preset/src/main.ts +195 -0
  50. package/packages/preset/src/presets.ts +87 -0
  51. package/packages/preset/src/spoke.ts +251 -0
  52. package/packages/preset/src/tmux.ts +160 -0
  53. package/packages/protocol/package.json +12 -0
  54. package/packages/protocol/src/harness-events.ts +207 -0
  55. package/packages/protocol/src/index.ts +1215 -0
  56. package/packages/protocol/src/launch.ts +172 -0
  57. package/packages/protocol/src/node.ts +409 -0
  58. package/packages/protocol/src/preset.ts +144 -0
  59. package/packages/protocol/src/webhook.ts +62 -0
  60. package/packages/protocol/src/wire.ts +54 -0
  61. package/packages/runtime-openshell/image/Dockerfile +36 -0
  62. package/packages/runtime-openshell/image/README.md +78 -0
  63. package/packages/runtime-openshell/image/build.ts +203 -0
  64. package/packages/runtime-openshell/image/entry.sh +163 -0
  65. package/packages/runtime-openshell/image/image.ts +119 -0
  66. package/packages/runtime-openshell/image/start.sh +24 -0
  67. package/packages/runtime-openshell/package.json +12 -0
  68. package/packages/runtime-openshell/src/cli.ts +584 -0
  69. package/packages/runtime-openshell/src/index.ts +6 -0
  70. package/packages/runtime-openshell/src/policy.ts +155 -0
  71. package/packages/runtime-openshell/src/profiles.ts +181 -0
  72. package/packages/runtime-openshell/src/runtime.ts +475 -0
  73. package/packages/runtime-sdk/package.json +12 -0
  74. package/packages/runtime-sdk/src/index.ts +87 -0
  75. package/packages/sdk/package.json +12 -0
  76. package/packages/sdk/src/address.ts +25 -0
  77. package/packages/sdk/src/client.ts +502 -0
  78. package/packages/sdk/src/envelope.ts +108 -0
  79. package/packages/sdk/src/errors.ts +106 -0
  80. package/packages/sdk/src/index.ts +10 -0
  81. package/packages/sdk/src/sse.ts +67 -0
  82. package/packages/sdk/src/types.ts +458 -0
  83. package/packages/sdk/src/webhook.ts +102 -0
  84. package/packages/spoke-sdk/package.json +12 -0
  85. package/packages/spoke-sdk/src/activity.ts +3 -0
  86. package/packages/spoke-sdk/src/connection.ts +295 -0
  87. package/packages/spoke-sdk/src/delivery.ts +119 -0
  88. package/packages/spoke-sdk/src/enroll.ts +79 -0
  89. package/packages/spoke-sdk/src/harness-hello.ts +79 -0
  90. package/packages/spoke-sdk/src/harness-spoke.ts +263 -0
  91. package/packages/spoke-sdk/src/index.ts +12 -0
  92. package/packages/spoke-sdk/src/prompt-refs.ts +154 -0
  93. package/packages/spoke-sdk/src/prompt-source.ts +8 -0
  94. package/packages/spoke-sdk/src/session-ids.ts +49 -0
  95. package/packages/spoke-sdk/src/session-tokens.ts +78 -0
  96. package/scripts/hub-claude-code.service +34 -0
  97. package/scripts/hub-codex-spoke.service +34 -0
  98. package/scripts/hub-node.service +35 -0
  99. package/scripts/io.github.huynle.hub-claude-code.plist +53 -0
  100. package/scripts/io.github.huynle.hub-codex-spoke.plist +53 -0
  101. package/scripts/io.github.huynle.hub-node.plist +52 -0
  102. package/src/central/actions.ts +233 -0
  103. package/src/central/agent/acting-routes.ts +133 -0
  104. package/src/central/agent/acting-switch.ts +93 -0
  105. package/src/central/agent/acting.ts +599 -0
  106. package/src/central/agent/actor.ts +193 -0
  107. package/src/central/agent/app-tokens.ts +142 -0
  108. package/src/central/agent/audit.ts +185 -0
  109. package/src/central/agent/decisions.ts +530 -0
  110. package/src/central/agent/delivery.ts +678 -0
  111. package/src/central/agent/directory-routes.ts +185 -0
  112. package/src/central/agent/directory.ts +473 -0
  113. package/src/central/agent/envelope.ts +51 -0
  114. package/src/central/agent/errors.ts +72 -0
  115. package/src/central/agent/escalation.ts +219 -0
  116. package/src/central/agent/grant-requests.ts +221 -0
  117. package/src/central/agent/grant-routes.ts +70 -0
  118. package/src/central/agent/grants.ts +235 -0
  119. package/src/central/agent/launch-engine.ts +1025 -0
  120. package/src/central/agent/launch-routes.ts +252 -0
  121. package/src/central/agent/launch-spec.ts +289 -0
  122. package/src/central/agent/launch-store.ts +264 -0
  123. package/src/central/agent/launch-tokens.ts +206 -0
  124. package/src/central/agent/message-limits.ts +123 -0
  125. package/src/central/agent/message-routes.ts +385 -0
  126. package/src/central/agent/messages.ts +442 -0
  127. package/src/central/agent/names.ts +86 -0
  128. package/src/central/agent/placement.ts +419 -0
  129. package/src/central/agent/policy.ts +378 -0
  130. package/src/central/agent/read-routes.ts +389 -0
  131. package/src/central/agent/routes.ts +278 -0
  132. package/src/central/agent/session-tokens.ts +227 -0
  133. package/src/central/agent/versioned.ts +26 -0
  134. package/src/central/agent/watch.ts +221 -0
  135. package/src/central/agent/webhook-routes.ts +78 -0
  136. package/src/central/agent/webhooks.ts +449 -0
  137. package/src/central/agent-data.ts +177 -0
  138. package/src/central/auth.ts +224 -121
  139. package/src/central/backend/file-state.ts +240 -0
  140. package/src/central/backend/index.ts +113 -0
  141. package/src/central/backend/memory-bus.ts +93 -0
  142. package/src/central/backend/memory-catalog.ts +90 -0
  143. package/src/central/backend/memory-counters.ts +102 -0
  144. package/src/central/backend/memory-events.ts +45 -0
  145. package/src/central/backend/memory-kv.ts +38 -0
  146. package/src/central/backend/memory-state.ts +77 -0
  147. package/src/central/backend/postgres-migrate.ts +100 -0
  148. package/src/central/backend/postgres-state.ts +197 -0
  149. package/src/central/backend/types.ts +244 -0
  150. package/src/central/backend/valkey-bus.ts +425 -0
  151. package/src/central/backend/valkey-catalog.ts +190 -0
  152. package/src/central/backend/valkey-client.ts +36 -0
  153. package/src/central/backend/valkey-counters.ts +38 -0
  154. package/src/central/backend/valkey-events.ts +168 -0
  155. package/src/central/backend/valkey-kv.ts +54 -0
  156. package/src/central/backend/valkey-locks.ts +92 -0
  157. package/src/central/cluster/directory.ts +141 -0
  158. package/src/central/cluster/feed.ts +84 -0
  159. package/src/central/cluster/leader.ts +232 -0
  160. package/src/central/cluster/live.ts +151 -0
  161. package/src/central/cluster/recommend.ts +59 -0
  162. package/src/central/cluster/rpc.ts +150 -0
  163. package/src/central/cluster/view.ts +232 -0
  164. package/src/central/config.ts +197 -3
  165. package/src/central/done.ts +96 -0
  166. package/src/central/events-log.ts +100 -0
  167. package/src/central/http.ts +1067 -171
  168. package/src/central/import.ts +318 -0
  169. package/src/central/main.ts +211 -12
  170. package/src/central/nodes.ts +314 -0
  171. package/src/central/oidc.ts +34 -14
  172. package/src/central/push-target.ts +118 -18
  173. package/src/central/push.ts +256 -82
  174. package/src/central/registry.ts +902 -92
  175. package/src/central/say-stream.ts +128 -0
  176. package/src/central/session-token-push.ts +233 -0
  177. package/src/central/speech/openai.ts +21 -14
  178. package/src/central/speech/openrouter.ts +27 -10
  179. package/src/central/speech/provider.ts +54 -0
  180. package/src/central/speech/service.ts +22 -7
  181. package/src/central/spoke-ws.ts +522 -44
  182. package/src/central/store-shared.ts +518 -0
  183. package/src/central/store.ts +572 -189
  184. package/src/central/token-keys.ts +165 -0
  185. package/src/config.ts +49 -1
  186. package/src/index.ts +165 -12
  187. package/src/protocol.ts +3 -650
  188. package/src/rpc.ts +27 -2
  189. package/src/spoke/client.ts +3 -272
  190. package/src/spoke/conversation.ts +16 -12
  191. package/src/spoke/deliver.ts +75 -0
  192. package/src/spoke/enroll.ts +2 -79
  193. package/src/spoke/env-inject.ts +160 -0
  194. package/src/spoke/hub-rpc.ts +115 -0
  195. package/src/spoke/hub-tool.ts +409 -0
  196. package/src/spoke/launch-source.ts +22 -0
  197. package/src/spoke/launchers.ts +68 -0
  198. package/src/spoke/locations.ts +3 -2
  199. package/src/spoke/node-roots.ts +61 -0
  200. package/src/spoke/prompt-refs.ts +116 -0
  201. package/src/spoke/session-tokens.ts +2 -0
  202. package/src/spoke/spoke.ts +465 -28
  203. package/src/spoke/taskboard.ts +243 -0
  204. package/src/spoke/timeline.ts +184 -6
  205. package/src/spoke/tracker.ts +79 -4
  206. package/src/status.ts +203 -10
  207. package/src/tui-commands.ts +13 -6
  208. package/src/tui-messaging.ts +93 -0
  209. package/src/tui.ts +15 -0
  210. package/web/dist/assets/highlight-y4rJE6Tt.js +5 -0
  211. package/web/dist/assets/index-D9zMbxSg.css +1 -0
  212. package/web/dist/assets/index-db1z4duA.js +20 -0
  213. package/web/dist/assets/ort-wasm-simd-threaded-CxTQ5xH-.wasm +0 -0
  214. package/web/dist/assets/silero-worker-C8-8vLVS.js +11 -0
  215. package/web/dist/assets/silero_vad-Dt9-bTDn.onnx +0 -0
  216. package/web/dist/index.html +9 -2
  217. package/web/dist/sw.js +79 -15
  218. package/web/dist/assets/index-DClBvfLL.css +0 -1
  219. package/web/dist/assets/index-Djo_yF0a.js +0 -12
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env bash
2
+ # hub-sandbox-entry: runs the harness of a hub launch inside an OpenShell sandbox (packages/runtime-openshell/image).
3
+ # Started detached by hub-sandbox-start (the runtime execs that; the sandbox has no main command, so it stays Ready
4
+ # and exec keeps working after the harness ends). In order:
5
+ # 1. waits until the runtime's start op has uploaded .hub/config.json (hub URL, launch token, launch ID: the
6
+ # plugin's spoke reads it; this script never does) and touched .hub/ready; a .hub/stop first ends the wait;
7
+ # 2. adds .hub/env.json (a JSON object of extra variables; never over the ones below or OpenShell's CA variables);
8
+ # 3. sets the offline switches and XDG dirs under the sandbox home, writes the global OpenCode config loading the
9
+ # baked hub plugin, and runs `opencode serve --service` on loopback in /sandbox/work;
10
+ # 4. sends the server one request so it loads its instance (and with it the plugin, which connects to the hub);
11
+ # 5. on .hub/stop ("graceful": SIGTERM, SIGKILL after HUB_STOP_GRACE seconds; "kill": SIGKILL) or the harness's own
12
+ # end, writes its exit code to .hub/exited and exits with it.
13
+ # Same script in the image (/usr/local) and the bootstrap fallback (/sandbox/.hub/opt): the harness and plugin are
14
+ # found next to it (<root>/bin/opencode, <root>/lib/hub/node_modules/@huynle/hub). Bash 3.2 compatible (tests run it
15
+ # on macOS too). Environment overrides, for tests: HUB_SANDBOX_HOME, HUB_HARNESS_PORT, HUB_STOP_GRACE,
16
+ # HUB_POLL_INTERVAL.
17
+ set -u
18
+
19
+ here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
20
+ root=$(cd "$here/.." && pwd)
21
+ home="${HUB_SANDBOX_HOME:-/sandbox}"
22
+ hub="$home/.hub"
23
+ opencode="$root/bin/opencode"
24
+ plugin="$root/lib/hub/node_modules/@huynle/hub"
25
+ port="${HUB_HARNESS_PORT:-4096}"
26
+ grace="${HUB_STOP_GRACE:-10}"
27
+ poll="${HUB_POLL_INTERVAL:-0.5}"
28
+
29
+ say() { echo "[hub-sandbox] $*" >&2; }
30
+
31
+ mkdir -p "$hub"
32
+ echo "$$" >"$hub/entry.pid"
33
+ rm -f "$hub/exited"
34
+
35
+ # 1. Wait for the start op.
36
+ say "waiting for $hub/config.json and $hub/ready"
37
+ until [ -f "$hub/config.json" ] && [ -e "$hub/ready" ]; do
38
+ if [ -e "$hub/stop" ]; then
39
+ say "stopped before the harness started"
40
+ rm -f "$hub/entry.pid"
41
+ exit 0
42
+ fi
43
+ sleep "$poll"
44
+ done
45
+
46
+ # 2. Extra environment: NUL-separated NAME=value pairs from the harness's own bun (no jq in the fallback image).
47
+ protected() {
48
+ case "$1" in
49
+ OPENCODE_DISABLE_* | XDG_* | HOME | PATH | HUB_LAUNCH_CONFIG | HUB_SANDBOX_HOME | BUN_* | LD_*) return 0 ;;
50
+ NODE_EXTRA_CA_CERTS | SSL_CERT_FILE | SSL_CERT_DIR | CURL_CA_BUNDLE | REQUESTS_CA_BUNDLE | GIT_SSL_CAINFO | DENO_CERT) return 0 ;;
51
+ esac
52
+ return 1
53
+ }
54
+ if [ -f "$hub/env.json" ]; then
55
+ pairs=$(mktemp "${TMPDIR:-/tmp}/hub-env.XXXXXX") || exit 2
56
+ # shellcheck disable=SC2016
57
+ if ! HUB_ENV_JSON="$hub/env.json" BUN_BE_BUN=1 "$opencode" -e '
58
+ // An uncaught error in `bun -e` can end with status 0: fail explicitly.
59
+ let env
60
+ try {
61
+ env = JSON.parse(require("node:fs").readFileSync(process.env.HUB_ENV_JSON, "utf8"))
62
+ } catch {
63
+ process.exit(2)
64
+ }
65
+ if (typeof env !== "object" || env === null || Array.isArray(env)) process.exit(2)
66
+ let out = ""
67
+ for (const [k, v] of Object.entries(env)) {
68
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k) || !["string", "number", "boolean"].includes(typeof v)) continue
69
+ const value = String(v)
70
+ if (!value.includes("\0")) out += `${k}=${value}\0`
71
+ }
72
+ process.stdout.write(out)
73
+ ' >"$pairs" 2>/dev/null </dev/null; then
74
+ say "invalid $hub/env.json: expected a JSON object"
75
+ rm -f "$pairs" "$hub/entry.pid"
76
+ echo 2 >"$hub/exited"
77
+ exit 2
78
+ fi
79
+ while IFS= read -r -d '' pair; do
80
+ name="${pair%%=*}"
81
+ if protected "$name"; then
82
+ say "ignoring $name from env.json"
83
+ continue
84
+ fi
85
+ export "${pair?}"
86
+ done <"$pairs"
87
+ rm -f "$pairs"
88
+ fi
89
+
90
+ # 3. The harness's environment and config.
91
+ export OPENCODE_DISABLE_MODELS_FETCH=1 OPENCODE_DISABLE_AUTOUPDATE=1 OPENCODE_DISABLE_PROJECT_CONFIG=1
92
+ export XDG_CONFIG_HOME="$home/.xdg/config" XDG_DATA_HOME="$home/.xdg/data" XDG_STATE_HOME="$home/.xdg/state" XDG_CACHE_HOME="$home/.xdg/cache"
93
+ export HUB_LAUNCH_CONFIG="$hub/config.json"
94
+ unset BUN_BE_BUN
95
+ if [ -z "${OPENCODE_PASSWORD:-}" ]; then
96
+ OPENCODE_PASSWORD=$(od -An -N24 -tx1 /dev/urandom | tr -d ' \n')
97
+ export OPENCODE_PASSWORD
98
+ fi
99
+ mkdir -p "$XDG_CONFIG_HOME/opencode" "$XDG_DATA_HOME" "$XDG_STATE_HOME" "$XDG_CACHE_HOME"
100
+ json_path=$(printf '%s' "$plugin" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')
101
+ printf '{ "plugins": [{ "package": "%s", "options": {} }] }\n' "$json_path" >"$XDG_CONFIG_HOME/opencode/opencode.jsonc"
102
+
103
+ workdir="$home"
104
+ [ -d "$home/work" ] && workdir="$home/work"
105
+ cd "$workdir" || exit 1
106
+ say "starting opencode serve --service on 127.0.0.1:$port in $workdir"
107
+ "$opencode" serve --service --hostname 127.0.0.1 --port "$port" &
108
+ harness=$!
109
+ echo "$harness" >"$hub/harness.pid"
110
+
111
+ # 4. One authorized request loads the server's instance and the plugin (a --service server may keep its own password
112
+ # in $XDG_CONFIG_HOME/opencode/service.json).
113
+ # shellcheck disable=SC2016
114
+ HUB_KICK_URL="http://127.0.0.1:$port/api/agent" BUN_BE_BUN=1 "$opencode" -e '
115
+ const fs = require("node:fs")
116
+ const password = () => {
117
+ try {
118
+ return JSON.parse(fs.readFileSync(`${process.env.XDG_CONFIG_HOME}/opencode/service.json`, "utf8")).password ?? process.env.OPENCODE_PASSWORD
119
+ } catch {
120
+ return process.env.OPENCODE_PASSWORD
121
+ }
122
+ }
123
+ for (const end = Date.now() + 60_000; Date.now() < end; await Bun.sleep(500)) {
124
+ const ok = await fetch(process.env.HUB_KICK_URL, { headers: { authorization: `Basic ${btoa(`opencode:${password()}`)}` } }).then((r) => r.ok, () => false)
125
+ if (ok) {
126
+ console.error("[hub-sandbox] the harness answered; its plugin is loading")
127
+ process.exit(0)
128
+ }
129
+ }
130
+ console.error("[hub-sandbox] the harness did not answer within 60 s")
131
+ ' </dev/null &
132
+ kick=$!
133
+
134
+ # 5. Supervise.
135
+ stop_harness() {
136
+ if [ "$1" = kill ]; then
137
+ say "stop (kill): SIGKILL"
138
+ kill -KILL "$harness" 2>/dev/null
139
+ return
140
+ fi
141
+ say "stop ($1): SIGTERM, SIGKILL after ${grace}s"
142
+ kill -TERM "$harness" 2>/dev/null
143
+ local started=$SECONDS
144
+ while kill -0 "$harness" 2>/dev/null && [ $((SECONDS - started)) -lt "$grace" ]; do sleep "$poll"; done
145
+ kill -0 "$harness" 2>/dev/null && kill -KILL "$harness" 2>/dev/null
146
+ }
147
+ trap 'stop_harness graceful' TERM INT
148
+
149
+ while kill -0 "$harness" 2>/dev/null; do
150
+ if [ -e "$hub/stop" ]; then
151
+ mode=$(head -n 1 "$hub/stop" 2>/dev/null | tr -d '[:space:]')
152
+ stop_harness "${mode:-graceful}"
153
+ break
154
+ fi
155
+ sleep "$poll"
156
+ done
157
+ wait "$harness"
158
+ code=$?
159
+ kill "$kick" 2>/dev/null
160
+ echo "$code" >"$hub/exited"
161
+ rm -f "$hub/harness.pid" "$hub/entry.pid"
162
+ say "the harness exited with $code"
163
+ exit "$code"
@@ -0,0 +1,119 @@
1
+ // The hub sandbox image and its upload fallback (P5 spike report gut64rwq §7–§8): naming, layout and the commands
2
+ // build.ts and the openshell runtime run. Pure: nothing here touches Docker, the network or a sandbox.
3
+ //
4
+ // Layout (the same under both roots, so one entry script serves both):
5
+ // <root>/bin/opencode the Linux OpenCode build from npm @opencode/cli-linux-<arch>
6
+ // <root>/bin/hub-sandbox-start starts the entry script detached (setsid nohup), returns at once
7
+ // <root>/bin/hub-sandbox-entry waits for .hub/config.json + .hub/ready, runs `opencode serve
8
+ // --service`, stops it on .hub/stop
9
+ // <root>/lib/hub/node_modules/@huynle/hub the hub plugin (this repository's package), with its dependencies
10
+ // Roots: IMAGE_ROOT in the hub-sandbox image; BOOTSTRAP_ROOT when the runtime uploads the bootstrap tarball into a
11
+ // sandbox on the gateway's default image (/usr is read-only there; only /sandbox and /tmp are writable).
12
+
13
+ /** The OpenCode release the image pins (the version the repository's live tests run against). */
14
+ export const OPENCODE_VERSION = "2.1.4"
15
+ /** The image repository name; a registry prefix makes it pullable (the vm driver pulls over HTTPS only). */
16
+ export const IMAGE_NAME = "hub-sandbox"
17
+ /** Where the image installs the harness, scripts and plugin. */
18
+ export const IMAGE_ROOT = "/usr/local"
19
+ /** Where the bootstrap fallback unpacks the same layout inside a sandbox. */
20
+ export const BOOTSTRAP_ROOT = "/sandbox/.hub/opt"
21
+ /** The sandbox's writable working directory: uploads, downloads and the bootstrap live under it. */
22
+ export const SANDBOX_HOME = "/sandbox"
23
+ export const START_SCRIPT = "hub-sandbox-start"
24
+ export const ENTRY_SCRIPT = "hub-sandbox-entry"
25
+ export const PLUGIN_SUBPATH = "lib/hub/node_modules/@huynle/hub"
26
+
27
+ export type LinuxArch = "arm64" | "x64"
28
+
29
+ const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/
30
+
31
+ /** The Linux build matching a host architecture (process.arch or uname -m); throws for others. */
32
+ export function linuxArch(arch: string): LinuxArch {
33
+ if (arch === "arm64" || arch === "aarch64") return "arm64"
34
+ if (arch === "x64" || arch === "amd64" || arch === "x86_64") return "x64"
35
+ throw new Error(`no Linux OpenCode build for architecture ${arch} (arm64 and x64 only)`)
36
+ }
37
+
38
+ export function dockerPlatform(arch: LinuxArch): string {
39
+ return arch === "arm64" ? "linux/arm64" : "linux/amd64"
40
+ }
41
+
42
+ function pinned(version: string): string {
43
+ if (!VERSION.test(version)) throw new Error(`the OpenCode version must be an exact version, not ${JSON.stringify(version)}`)
44
+ return version
45
+ }
46
+
47
+ /** The npm package of the Linux OpenCode build: @opencode/cli-linux-<arch>@<version>. */
48
+ export function opencodePackage(arch: LinuxArch, version: string = OPENCODE_VERSION): string {
49
+ return `@opencode/cli-linux-${arch}@${pinned(version)}`
50
+ }
51
+
52
+ /** The registry tarball of that package (it holds package/bin/opencode). */
53
+ export function opencodeTarballURL(arch: LinuxArch, version: string = OPENCODE_VERSION): string {
54
+ return `https://registry.npmjs.org/@opencode/cli-linux-${arch}/-/cli-linux-${arch}-${pinned(version)}.tgz`
55
+ }
56
+
57
+ /** The registry metadata of that version (dist.integrity, dist.tarball). */
58
+ export function opencodeMetadataURL(arch: LinuxArch, version: string = OPENCODE_VERSION): string {
59
+ return `https://registry.npmjs.org/@opencode/cli-linux-${arch}/${pinned(version)}`
60
+ }
61
+
62
+ /** <name>:<hub version>-opencode<OpenCode version>. */
63
+ export function imageTag(hubVersion: string, opencodeVersion: string = OPENCODE_VERSION, name: string = IMAGE_NAME): string {
64
+ return `${name}:${hubVersion}-opencode${pinned(opencodeVersion)}`
65
+ }
66
+
67
+ export function bootstrapTarballName(hubVersion: string, arch: LinuxArch, opencodeVersion: string = OPENCODE_VERSION): string {
68
+ return `${IMAGE_NAME}-bootstrap-${hubVersion}-opencode${pinned(opencodeVersion)}-linux-${arch}.tgz`
69
+ }
70
+
71
+ /** The harness binary under a root; the sandbox policy must name it (policy.ts HARNESS_BINARY for the image). */
72
+ export function harnessBinary(root: string = IMAGE_ROOT): string {
73
+ return `${root}/bin/opencode`
74
+ }
75
+
76
+ export function pluginDir(root: string = IMAGE_ROOT): string {
77
+ return `${root}/${PLUGIN_SUBPATH}`
78
+ }
79
+
80
+ /** The argv the runtime execs (after start uploaded .hub/config.json and touched .hub/ready, or before). */
81
+ export function startCommand(root: string = IMAGE_ROOT): string[] {
82
+ return [`${root}/bin/${START_SCRIPT}`]
83
+ }
84
+
85
+ /** Unpacks an uploaded bootstrap tarball (a path under /sandbox) into the bootstrap root, then removes it. */
86
+ export function bootstrapCommands(uploaded: string, root: string = BOOTSTRAP_ROOT): string[][] {
87
+ const parts = uploaded.split("/")
88
+ if (!uploaded.startsWith(`${SANDBOX_HOME}/`) || parts.includes("..") || parts.includes(".")) throw new Error(`the bootstrap tarball must be uploaded under ${SANDBOX_HOME}, not ${uploaded}`)
89
+ return [
90
+ ["mkdir", "-p", root],
91
+ ["tar", "-xzf", uploaded, "-C", root],
92
+ ["rm", "-f", uploaded],
93
+ ]
94
+ }
95
+
96
+ export interface DockerBuildOptions {
97
+ readonly tag: string
98
+ readonly arch: LinuxArch
99
+ /** The staged build context (build.ts stage). */
100
+ readonly context: string
101
+ readonly hubVersion: string
102
+ readonly opencodeVersion?: string
103
+ }
104
+
105
+ /** `docker build` arguments for one platform. */
106
+ export function dockerBuildArgs(o: DockerBuildOptions): string[] {
107
+ return [
108
+ "build",
109
+ "--platform",
110
+ dockerPlatform(o.arch),
111
+ "--tag",
112
+ o.tag,
113
+ "--build-arg",
114
+ `HUB_VERSION=${o.hubVersion}`,
115
+ "--build-arg",
116
+ `OPENCODE_VERSION=${pinned(o.opencodeVersion ?? OPENCODE_VERSION)}`,
117
+ o.context,
118
+ ]
119
+ }
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bash
2
+ # hub-sandbox-start: starts hub-sandbox-entry detached and returns at once (the runtime execs it; spike report
3
+ # gut64rwq §6: a process started by exec survives the exec when it has its own session and no open stdio). Logs go to
4
+ # <sandbox home>/.hub/harness.log. Starting again while the entry runs does nothing.
5
+ set -u
6
+
7
+ here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
8
+ hub="${HUB_SANDBOX_HOME:-/sandbox}/.hub"
9
+ log="$hub/harness.log"
10
+ mkdir -p "$hub"
11
+
12
+ if [ -f "$hub/entry.pid" ] && kill -0 "$(cat "$hub/entry.pid" 2>/dev/null)" 2>/dev/null; then
13
+ echo "hub-sandbox: the harness is already running (pid $(cat "$hub/entry.pid"))"
14
+ exit 0
15
+ fi
16
+
17
+ if command -v setsid >/dev/null 2>&1; then
18
+ setsid nohup "$here/hub-sandbox-entry" >>"$log" 2>&1 </dev/null &
19
+ else
20
+ nohup "$here/hub-sandbox-entry" >>"$log" 2>&1 </dev/null &
21
+ fi
22
+ # The entry rewrites this with its own pid; written here too so a start right after sees it.
23
+ echo "$!" >"$hub/entry.pid"
24
+ echo "hub-sandbox: started (pid $!, log $log)"
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@huynle/hub-runtime-openshell",
3
+ "version": "0.7.1",
4
+ "private": true,
5
+ "description": "The `openshell` runtime of `hub node`: launches in NVIDIA OpenShell sandboxes, driven through the openshell CLI (internal; shipped inside @huynle/hub, not published on its own).",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": "./src/index.ts",
10
+ "./package.json": "./package.json"
11
+ }
12
+ }