nebula-ai-gateway 0.1.0

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.
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Bootstrap script template for first-cold-start of an nebula harness inside
3
+ * a Mantle Sandbox container. Returned as a string the init/deploy/upgrade
4
+ * commands feed to `provider-client.execInToolbox(id, { command })`.
5
+ *
6
+ * Two modes:
7
+ * - 'git': clones the monorepo + bun install. ~5-8 min cold start. Pins to
8
+ * any branch/SHA.
9
+ * - 'npm': `bun add -g nebula-ai-cli@<version>`. ~30-60 sec cold start.
10
+ * Only published versions.
11
+ *
12
+ * Design constraint: the Daytona toolbox `process/execute` endpoint caps each
13
+ * exec call at ~60s. Whatever install path runs blows that easily, so we
14
+ * detach the slow work into a background subshell via `nohup bash -c '...' &`
15
+ * and return exit 0 immediately. Progress is observable via two files:
16
+ *
17
+ * - `/tmp/nebula-bootstrap-progress.log` (tail-able, line-by-line stages)
18
+ * - `/tmp/nebula-bootstrap-done` (created only on full success, contains harness pid)
19
+ *
20
+ * The caller (`sandbox-provision.ts`) launches the bootstrap, polls for the
21
+ * `done` marker (then for `/bootstrap/pubkey` from the harness HTTP server).
22
+ *
23
+ * Robustness rules:
24
+ * - All variables shell-quote-escaped to defeat injection from operator
25
+ * address or sandbox id (validated upstream, defense-in-depth).
26
+ * - Git mode: always-clone fresh. Daytona occasionally re-uses post-delete
27
+ * volumes whose stale git credential helpers break re-fetch.
28
+ * - Npm mode: `bun add -g nebula-ai-cli@<exact-version>` is idempotent
29
+ * and overwrites. Same version twice = no-op. Different version = clean
30
+ * swap. Lower risk than git's stale-credential failure mode.
31
+ */
32
+
33
+ export type BootstrapMode = 'git' | 'npm'
34
+
35
+ export interface BuildBootstrapScriptOpts {
36
+ /** Sandbox UUID returned by provider's createSandbox. */
37
+ sandboxId: string
38
+ /** EIP-191 checksummed operator address. Stored in container env, used by `verifyChatSig`. */
39
+ operatorAddress: string
40
+ /**
41
+ * Bootstrap mode. Defaults to 'npm' (since v0.21.20) because it's ~10x
42
+ * faster. Callers in cli/src always pass `mode` explicitly via
43
+ * `resolveBootstrapMode`; this default is defense-in-depth.
44
+ */
45
+ mode?: BootstrapMode
46
+ /**
47
+ * Git mode: tag/branch/SHA to clone (e.g. 'v0.15.0', 'main', or commit SHA).
48
+ * Npm mode: ignored (use `packageVersion`).
49
+ */
50
+ ref: string
51
+ /**
52
+ * Npm mode: the exact published version to install (e.g. '0.21.15').
53
+ * Required when mode='npm'. Ignored in git mode.
54
+ */
55
+ packageVersion?: string
56
+ /**
57
+ * Public git URL of nebula. Defaults to the canonical hackathon repo.
58
+ * Override only when running against a fork / private mirror. (Git mode only.)
59
+ */
60
+ repoUrl?: string
61
+ /** Port the harness binds inside the container. Default 8080. */
62
+ port?: number
63
+ /**
64
+ * Extra `apt-get install` packages. Defaults to xvfb + git + ca-certificates
65
+ * + curl + unzip + psmisc. Caller can append.
66
+ */
67
+ extraAptPackages?: string[]
68
+ /**
69
+ * GitHub PAT for cloning private nebula repos. Embedded in clone URL as
70
+ * `https://x-access-token:TOKEN@github.com/...`. For public repos, leave
71
+ * unset. Token is base64-wrapped inside the inner script (which itself is
72
+ * base64-encoded into the outer command), and the inner script is written
73
+ * to /tmp on the container; ensure the container is single-tenant (Daytona
74
+ * containers are by-operator). Gets cleared from container env after clone.
75
+ * (Git mode only.)
76
+ */
77
+ githubToken?: string
78
+ }
79
+
80
+ export interface BuildBootstrapScriptResult {
81
+ /** Outer script: launches the inner subshell + returns exit 0. ~1s execution. */
82
+ script: string
83
+ /**
84
+ * Path the caller should poll via `execInToolbox(id, { command: cat <path> })`
85
+ * to detect bootstrap completion. Returns success line `nebula-gateway-pid=<N>`
86
+ * once everything is up; absent until then.
87
+ */
88
+ doneMarkerPath: string
89
+ /** Path the caller can tail to surface bootstrap progress. */
90
+ progressLogPath: string
91
+ }
92
+
93
+ /**
94
+ * Quote a string for safe inclusion in a single-quoted bash literal.
95
+ * Single-quoted strings forbid `'`; we escape it as `'\''`.
96
+ */
97
+ function shQuote(s: string): string {
98
+ return `'${s.replace(/'/g, "'\\''")}'`
99
+ }
100
+
101
+ const DEFAULT_APT_PACKAGES: readonly string[] = [
102
+ 'curl',
103
+ 'unzip',
104
+ 'ca-certificates',
105
+ 'git',
106
+ // psmisc provides `fuser` which the harness launch step uses to free port
107
+ // 8080 if Daytona's snapshot ships a default service squatting on it.
108
+ 'psmisc',
109
+ // xvfb retained as headed-browser fallback insurance; agent-browser's
110
+ // installed Chrome-for-Testing runs headless natively but xvfb is cheap
111
+ // (~5MB) and keeps the door open for visual debugging.
112
+ 'xvfb',
113
+ ] as const
114
+
115
+ const PROGRESS_LOG = '/tmp/nebula-bootstrap-progress.log'
116
+ const DONE_MARKER = '/tmp/nebula-bootstrap-done'
117
+ const FAIL_MARKER = '/tmp/nebula-bootstrap-failed'
118
+
119
+ /**
120
+ * Stage marker bodies emitted as `STAGE: <body>` lines into the progress log.
121
+ * Single source of truth for both the script generator (writes them) and the
122
+ * CLI poll loop (reads them via `extractBootstrapProgressLine` and routes to
123
+ * `BootstrapProgressBox`). Strings are prefixes — the apt/nebula/chrome rows
124
+ * append details after a space.
125
+ */
126
+ export const BOOTSTRAP_STAGE_MARKERS = {
127
+ aptUpdate: 'updating package index',
128
+ systemDeps: 'installing system deps',
129
+ bunInstall: 'installing bun runtime',
130
+ nebulaInstall: 'installing nebula',
131
+ browserDeps: 'installing chrome for browser tools',
132
+ harnessSpawn: 'starting harness daemon',
133
+ harnessReady: 'harness ready',
134
+ } as const
135
+
136
+ /**
137
+ * Shell literal (NOT a Node path). Where Bun symlinks third-party global bins
138
+ * after `bun add -g`. Don't pass to `path.join` — `$HOME` won't expand.
139
+ */
140
+ export const BUN_GLOBAL_BIN_SHELL = '$HOME/.bun/install/global/node_modules/.bin'
141
+
142
+ function buildPreambleLines(
143
+ opts: BuildBootstrapScriptOpts,
144
+ modeLabel: string,
145
+ aptList: string,
146
+ ): string[] {
147
+ return [
148
+ '#!/bin/bash',
149
+ 'set -uo pipefail',
150
+ `exec > ${PROGRESS_LOG} 2>&1`,
151
+ `echo "[$(date -u +%FT%TZ)] bootstrap-start (mode=${modeLabel})"`,
152
+ `echo " sandbox=${opts.sandboxId}"`,
153
+ 'retry() {',
154
+ ' local L=$1; shift',
155
+ ' local n',
156
+ ' for n in 1 2 3; do',
157
+ ' echo "[$L attempt $n/3]"',
158
+ ' "$@" && return 0',
159
+ ' [ $n -lt 3 ] && { echo "[$L failed, retry in $((n*5))s]"; sleep $((n*5)); }',
160
+ ' done',
161
+ ' return 1',
162
+ '}',
163
+ 'export DEBIAN_FRONTEND=noninteractive',
164
+ `echo "STAGE: ${BOOTSTRAP_STAGE_MARKERS.aptUpdate}"`,
165
+ `retry 'apt update' sudo -n apt-get update -qq || { echo "apt-update-failed" > ${FAIL_MARKER}; exit 11; }`,
166
+ `echo "STAGE: ${BOOTSTRAP_STAGE_MARKERS.systemDeps} (build-essential, curl, git, xvfb)"`,
167
+ `retry 'apt install' sudo -n apt-get install -y -qq ${aptList} || { echo "apt-install-failed" > ${FAIL_MARKER}; exit 12; }`,
168
+ 'install_bun() { curl -fsSL https://bun.sh/install | bash; }',
169
+ 'if ! command -v bun >/dev/null 2>&1; then',
170
+ ` echo "STAGE: ${BOOTSTRAP_STAGE_MARKERS.bunInstall}"`,
171
+ ` retry 'bun binary' install_bun || { echo "bun-install-failed" > ${FAIL_MARKER}; exit 13; }`,
172
+ 'fi',
173
+ 'export PATH="$HOME/.bun/bin:$PATH"',
174
+ ]
175
+ }
176
+
177
+ function buildLaunchLines(opts: BuildBootstrapScriptOpts, gatewayLaunchCmd: string): string[] {
178
+ const port = opts.port ?? 8080
179
+ return [
180
+ 'mkdir -p "$HOME/nebula-logs" "$HOME/workspace"',
181
+ '',
182
+ `export SANDBOX_ID=${shQuote(opts.sandboxId)}`,
183
+ `export NEBULA_OPERATOR_ADDRESS=${shQuote(opts.operatorAddress)}`,
184
+ `export HARNESS_PORT=${shQuote(String(port))}`,
185
+ "export HARNESS_HOST='0.0.0.0'",
186
+ '',
187
+ `fuser -k ${port}/tcp 2>/dev/null || true`,
188
+ 'sleep 2',
189
+ `echo "STAGE: ${BOOTSTRAP_STAGE_MARKERS.harnessSpawn}"`,
190
+ 'echo "[launch harness daemon]"',
191
+ 'HARNESS_PID=""',
192
+ 'HARNESS_OK=0',
193
+ 'for h_attempt in 1 2 3; do',
194
+ ' echo "[launch attempt $h_attempt/3]"',
195
+ ` fuser -k ${port}/tcp 2>/dev/null || true`,
196
+ ' sleep 1',
197
+ ` nohup ${gatewayLaunchCmd} > "$HOME/nebula-logs/nebula-gateway.log" 2>&1 &`,
198
+ ' HARNESS_PID=$!',
199
+ ' disown',
200
+ ' sleep 10',
201
+ ' if kill -0 "$HARNESS_PID" 2>/dev/null; then',
202
+ ' HARNESS_OK=1',
203
+ ' break',
204
+ ' fi',
205
+ ' echo "[harness died on attempt $h_attempt, log tail:]"',
206
+ ' tail -n 50 "$HOME/nebula-logs/nebula-gateway.log" 2>/dev/null',
207
+ ' if [ $h_attempt -lt 3 ]; then',
208
+ ' echo "[retrying in 5s]"',
209
+ ' sleep 5',
210
+ ' fi',
211
+ 'done',
212
+ 'if [ "$HARNESS_OK" -ne 1 ]; then',
213
+ ' echo "[all 3 harness launch attempts failed, full log dump:]"',
214
+ ' tail -n 200 "$HOME/nebula-logs/nebula-gateway.log" 2>/dev/null',
215
+ ` echo "harness-died-early" > ${FAIL_MARKER}`,
216
+ ' exit 18',
217
+ 'fi',
218
+ `echo "STAGE: ${BOOTSTRAP_STAGE_MARKERS.harnessReady}"`,
219
+ `echo "nebula-gateway-pid=$HARNESS_PID" > ${DONE_MARKER}`,
220
+ 'echo "[$(date -u +%FT%TZ)] bootstrap-done pid=$HARNESS_PID"',
221
+ '',
222
+ ]
223
+ }
224
+
225
+ function buildGitInnerScript(opts: BuildBootstrapScriptOpts, aptList: string): string {
226
+ const repoUrl = opts.repoUrl ?? 'https://github.com/rstfulzz/nebula.git'
227
+ const cloneUrl = opts.githubToken
228
+ ? repoUrl.replace(
229
+ 'https://github.com/',
230
+ `https://x-access-token:${opts.githubToken}@github.com/`,
231
+ )
232
+ : repoUrl
233
+ const preamble = buildPreambleLines(opts, 'git', aptList)
234
+ const installLines = [
235
+ `echo " ref=${opts.ref}"`,
236
+ `echo " repo=${repoUrl}"`,
237
+ 'NEBULA_DIR="$HOME/nebula"',
238
+ `echo "STAGE: ${BOOTSTRAP_STAGE_MARKERS.nebulaInstall} (git ${opts.ref})"`,
239
+ `git_clone_one() { rm -rf "$NEBULA_DIR"; git clone --depth 1 --branch ${shQuote(opts.ref)} ${shQuote(cloneUrl)} "$NEBULA_DIR"; }`,
240
+ `retry 'git clone' git_clone_one || { echo "git-clone-failed" > ${FAIL_MARKER}; exit 14; }`,
241
+ `cd "$NEBULA_DIR" && git remote set-url origin ${shQuote(repoUrl)}`,
242
+ `retry 'bun deps' bun install --frozen-lockfile || { echo "bun-install-failed" > ${FAIL_MARKER}; exit 17; }`,
243
+ '',
244
+ // Install Chrome-for-Testing for browser tools. `agent-browser install`
245
+ // pulls a Chromium build + Linux system libs (`--with-deps`). `doctor`
246
+ // exits 0 only when the install state is healthy, so re-runs are no-ops
247
+ // on container restarts that share a persisted volume.
248
+ //
249
+ // Invoked via `node_modules/.bin/agent-browser` directly (not `bunx`)
250
+ // because Daytona's `curl bun.sh/install` install path doesn't always
251
+ // ship a `bunx` symlink.
252
+ `echo "STAGE: ${BOOTSTRAP_STAGE_MARKERS.browserDeps}"`,
253
+ 'echo "[browser deps]"',
254
+ 'if node_modules/.bin/agent-browser doctor >/dev/null 2>&1; then',
255
+ ' echo "[browser deps] already installed, skipping"',
256
+ 'else',
257
+ ` retry 'browser deps' node_modules/.bin/agent-browser install --with-deps || { echo "browser-install-failed" > ${FAIL_MARKER}; exit 19; }`,
258
+ 'fi',
259
+ '',
260
+ ]
261
+ const launch = buildLaunchLines(opts, 'bun "$NEBULA_DIR/packages/gateway/bin/nebula-gateway"')
262
+ return [...preamble, ...installLines, ...launch].join('\n')
263
+ }
264
+
265
+ function buildNpmInnerScript(opts: BuildBootstrapScriptOpts, aptList: string): string {
266
+ if (!opts.packageVersion) {
267
+ throw new Error('buildBootstrapScript: packageVersion is required when mode=npm')
268
+ }
269
+ const preamble = buildPreambleLines(opts, 'npm', aptList)
270
+ const installLines = [
271
+ `echo " package=nebula-ai-cli@${opts.packageVersion}"`,
272
+ `echo "STAGE: ${BOOTSTRAP_STAGE_MARKERS.nebulaInstall} (${opts.packageVersion})"`,
273
+ // Install nebula from npm. `bun add -g <pkg>@<exact-version>` is idempotent
274
+ // and overwrites whatever is in the global store. Atomic on success; on
275
+ // failure the prior version remains (which may be empty on a fresh container).
276
+ `retry 'nebula install' bun add -g ${shQuote(`nebula-ai-cli@${opts.packageVersion}`)} || { echo "nebula-install-failed" > ${FAIL_MARKER}; exit 14; }`,
277
+ // Add Bun's global package binaries to PATH so nebula-gateway + agent-browser
278
+ // resolve. ~/.bun/bin only contains bun's own binary, NOT third-party global
279
+ // package bins (those live at ~/.bun/install/global/node_modules/.bin/).
280
+ `export PATH="${BUN_GLOBAL_BIN_SHELL}:$PATH"`,
281
+ '',
282
+ // Browser deps (Chrome-for-Testing + Linux libs) installed via the global
283
+ // agent-browser binary. `doctor` is the idempotent guard.
284
+ `echo "STAGE: ${BOOTSTRAP_STAGE_MARKERS.browserDeps}"`,
285
+ 'echo "[browser deps]"',
286
+ `if ${BUN_GLOBAL_BIN_SHELL}/agent-browser doctor >/dev/null 2>&1; then`,
287
+ ' echo "[browser deps] already installed, skipping"',
288
+ 'else',
289
+ ` retry 'browser deps' ${BUN_GLOBAL_BIN_SHELL}/agent-browser install --with-deps || { echo "browser-install-failed" > ${FAIL_MARKER}; exit 19; }`,
290
+ 'fi',
291
+ '',
292
+ ]
293
+ const launch = buildLaunchLines(opts, `${BUN_GLOBAL_BIN_SHELL}/nebula-gateway`)
294
+ return [...preamble, ...installLines, ...launch].join('\n')
295
+ }
296
+
297
+ export function buildBootstrapScript(opts: BuildBootstrapScriptOpts): BuildBootstrapScriptResult {
298
+ const mode: BootstrapMode = opts.mode ?? 'npm'
299
+ const aptPkgs = [...DEFAULT_APT_PACKAGES, ...(opts.extraAptPackages ?? [])]
300
+ const aptList = [...new Set(aptPkgs)].join(' ')
301
+ const inner =
302
+ mode === 'npm' ? buildNpmInnerScript(opts, aptList) : buildGitInnerScript(opts, aptList)
303
+
304
+ // Daytona's `process/execute` API does NOT run via a shell — it splits the
305
+ // command string argv-style. Heredocs / pipes / `>` redirects fail because
306
+ // they're passed as literal args to the first binary. To run our complex
307
+ // inner script we base64-encode it (yields only [A-Za-z0-9+/=], no shell
308
+ // metachars) and have `bash -c '...'` decode + write + launch. The single-
309
+ // quoted bash -c wrapper has no internal quotes to escape.
310
+ //
311
+ // Sequencing rules:
312
+ // - File-write steps chain with `&&` (must succeed in order).
313
+ // - `nohup ... &` sends the inner script to background. After `&` you
314
+ // CANNOT use `&&` (syntax error: `& && X`) so we use `;` to follow up
315
+ // with the success marker echo. The launching shell exits ~instantly.
316
+ const innerPath = '/tmp/nebula-bootstrap-inner.sh'
317
+ const innerB64 = Buffer.from(inner).toString('base64')
318
+ const fileWrites = [
319
+ `rm -f ${PROGRESS_LOG} ${DONE_MARKER} ${FAIL_MARKER}`,
320
+ `echo ${innerB64} | base64 -d > ${innerPath}`,
321
+ `chmod +x ${innerPath}`,
322
+ ].join(' && ')
323
+ const launchBody = `${fileWrites} && nohup bash ${innerPath} >/dev/null 2>&1 & echo bootstrap-launched`
324
+ const outerScript = `bash -c '${launchBody}'`
325
+
326
+ return {
327
+ script: outerScript,
328
+ doneMarkerPath: DONE_MARKER,
329
+ progressLogPath: PROGRESS_LOG,
330
+ }
331
+ }
332
+
333
+ export const BOOTSTRAP_DONE_MARKER = DONE_MARKER
334
+ export const BOOTSTRAP_FAIL_MARKER = FAIL_MARKER
335
+ export const BOOTSTRAP_PROGRESS_LOG = PROGRESS_LOG
336
+ export const BOOTSTRAP_SUCCESS_MARKER_PREFIX = 'nebula-gateway-pid='
337
+
338
+ /**
339
+ * The exact strings the inner subshell writes to FAIL_MARKER on each step
340
+ * failure. Kept in sync with the per-step `echo "X-failed"` calls inside
341
+ * `buildBootstrapScript`. Pollers compare via substring match (the marker
342
+ * file may also contain bash setlocale warnings).
343
+ */
344
+ export const BOOTSTRAP_FAIL_KEYWORDS = [
345
+ 'apt-update-failed',
346
+ 'apt-install-failed',
347
+ 'bun-install-failed',
348
+ 'git-clone-failed',
349
+ 'nebula-install-failed',
350
+ 'browser-install-failed',
351
+ 'harness-died-early',
352
+ ] as const