@luxorai/agentbox-provider 0.0.0-stage → 0.1.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.
@@ -0,0 +1,348 @@
1
+ import { CloudBackend, CredStatusSummary, Provider, BoxRecord, AttachKind, BuildAttachOptions, AttachSpec, ProviderModule } from '@madarco/agentbox-provider-sdk';
2
+ export { SDK_API_VERSION } from '@madarco/agentbox-provider-sdk';
3
+
4
+ /**
5
+ * Tenki `CloudBackend` — maps the provider-neutral cloud primitives onto the
6
+ * `@tenkicloud/sandbox` SDK (Firecracker microVMs + pause/resume snapshots).
7
+ * Composed into a full `Provider` by the provider SDK's `createCloudProvider`.
8
+ *
9
+ * Platform shape this backend is built around:
10
+ * - Boxes boot from a snapshot id: the base snapshot baked by `agentbox
11
+ * prepare --provider tenki`, or a checkpoint snapshot (`req.snapshot`),
12
+ * which wins. `backend.provision` gates on `ensureTenkiBaseImage()` (mirrors
13
+ * the e2b/hetzner/vercel pattern: `prepare` sidesteps the gate so a cold
14
+ * install can bootstrap).
15
+ * - The SDK's high-level `Session` carries both the control-plane handle and
16
+ * a per-session data plane (`run`, `readFile`/`writeFile`/`fs.*`). We
17
+ * resolve a fresh `Session` per op via `client.get(sessionId)` — the CLI is
18
+ * a short-lived process per command, matching e2b's `Sandbox.connect`.
19
+ * - `CloudHandle.sandboxId` IS the Tenki session id.
20
+ * - Comms are ConnectRPC + a websocket data plane (no SSH for exec/files);
21
+ * interactive attach uses `session.ssh()`, wired by `buildTenkiAttach`.
22
+ * - Preview URLs come from `session.exposePort(port)` — public HTTPS, no
23
+ * header token (same shape as e2b/vercel).
24
+ * - `session.pause()` / `session.resume()` give free pause/resume;
25
+ * `createSnapshotAndWait` is the reusable, id-addressed checkpoint
26
+ * primitive (the provider overrides `checkpoint` in index.ts to store the
27
+ * snapshot id, matching vercel/e2b).
28
+ */
29
+
30
+ declare const tenkiBackend: CloudBackend;
31
+
32
+ /**
33
+ * Tenki env auto-loader. The `@tenkicloud/sandbox` SDK reads `TENKI_AUTH_TOKEN`
34
+ * from `process.env` (and we honor optional `TENKI_BASE_URL` /
35
+ * `TENKI_GATEWAY_ADDRESS` overrides for non-default / self-hosted control
36
+ * planes). We seed those from `~/.agentbox/secrets.env` — the store AgentBox's
37
+ * built-in cloud providers use — so the SDK Just Works after a one-time setup.
38
+ *
39
+ * Lookup order (first wins; process.env is never overwritten):
40
+ * 1. `process.env` (already set in the shell).
41
+ * 2. `~/.agentbox/secrets.env` — written by the credential prompt.
42
+ *
43
+ * `TENKI_API_TOKEN` is accepted as an alias for `TENKI_AUTH_TOKEN`.
44
+ *
45
+ * Project-level `.env` / `.env.local` are intentionally NOT consulted: those
46
+ * files belong to the app code being developed. Put host credentials in
47
+ * `~/.agentbox/secrets.env` (or the shell env).
48
+ *
49
+ * Only TENKI-prefixed keys are imported; the rest of the file is left alone.
50
+ * Idempotent and side-effect-free after the first call.
51
+ */
52
+ declare function ensureTenkiEnvLoaded(): void;
53
+ /**
54
+ * Force a re-read of `~/.agentbox/secrets.env`. Used by the interactive
55
+ * credential flow after it persists the token, so the same process can pick it
56
+ * up without a restart.
57
+ */
58
+ declare function reloadTenkiEnv(): void;
59
+
60
+ /**
61
+ * Atomic writer for the managed keys this provider owns in
62
+ * `~/.agentbox/secrets.env` — the same store AgentBox's built-in cloud providers
63
+ * use, so a Tenki token sits alongside the rest and `agentbox doctor` finds it.
64
+ *
65
+ * The provider SDK does not re-export AgentBox's internal secrets writer, so
66
+ * this is a local implementation of the same contract: strip any prior values
67
+ * for the keys we manage, append the new ones, and write through a temp file +
68
+ * rename at mode 0600 so a concurrent reader never sees a half-written file and
69
+ * the token is never briefly world-readable.
70
+ */
71
+
72
+ /**
73
+ * Outcome of a non-interactive credential write. `status` reflects the store
74
+ * *after* the attempt, so a caller can report `configured` without a second read.
75
+ * Mirrors the shape AgentBox's `providerModule.setCredentials` returns.
76
+ */
77
+ interface CredSetResult {
78
+ ok: boolean;
79
+ /** One-line failure reason when `ok` is false (e.g. a rejected token). */
80
+ error?: string;
81
+ status: CredStatusSummary;
82
+ }
83
+
84
+ /**
85
+ * Interactive Tenki credential setup. Single mode — paste a workspace auth
86
+ * token (`tk_…`) from the Tenki dashboard. Persists to
87
+ * `~/.agentbox/secrets.env` (the canonical store, matching daytona / hetzner /
88
+ * vercel / e2b).
89
+ *
90
+ * Non-interactive callers (no TTY): silent no-op, so scripted/CI runs surface
91
+ * the SDK's own "not configured" error instead of hanging on a prompt.
92
+ */
93
+
94
+ interface EnsureTenkiCredentialsOptions {
95
+ /** Re-prompt even when valid credentials are already present. */
96
+ force?: boolean;
97
+ }
98
+ declare function ensureTenkiCredentials(opts?: EnsureTenkiCredentialsOptions): Promise<void>;
99
+ /**
100
+ * Non-interactive credential write — the headless path an API driver uses,
101
+ * bypassing the TTY-gated `ensureTenkiCredentials` prompts. Extracts the `token`
102
+ * field, persists it, and returns a normalized result without echoing the secret.
103
+ */
104
+ declare function setTenkiCredentials(fields: Record<string, string>): CredSetResult;
105
+ declare function secretsPath(): string;
106
+ interface TenkiCredStatus {
107
+ auth: 'token' | 'none';
108
+ token?: string;
109
+ source: 'env' | 'secrets.env' | 'none';
110
+ }
111
+ declare function readTenkiCredStatus(): TenkiCredStatus;
112
+ declare function maskKey(value: string): string;
113
+
114
+ /**
115
+ * `agentbox prepare --provider tenki` — bake the AgentBox base image, then pin it
116
+ * as a snapshot so per-box `create` boots ready in seconds.
117
+ *
118
+ * ## Shape: builder sandbox, not the template API
119
+ *
120
+ * Boot a throwaway session from Tenki's plain `sandbox` base, push the runtime in
121
+ * over the data plane, run the install with `session.run`, snapshot it, and
122
+ * record the snapshot id. This is the same shape the built-in Vercel and E2B
123
+ * providers use, and it sidesteps every constraint the template API imposes:
124
+ *
125
+ * - **Real file upload.** A template build context is Git-only, so a template
126
+ * cannot receive host files at all — it would have to reinstall the runtime
127
+ * from npm and hope the published version matches the CLI. Over the data
128
+ * plane we ship the *running CLI's own* `ctl.cjs` and shims via the provider
129
+ * SDK's `resolveSharedRuntimeAsset`, which is the intended version-locking
130
+ * mechanism.
131
+ * - **Bash.** Template steps run under `sh -lc` (dash), where `pipefail` is a
132
+ * fatal bashism. `session.run(['bash', '-c', …])` — what the backend already
133
+ * uses for every exec — gives us bash 5.2.
134
+ * - **Per-step error attribution.** Each install step is its own exec with a
135
+ * name, so a failure says which step broke instead of failing somewhere
136
+ * inside one opaque script.
137
+ * - **No template-API restrictions**: no required `setup_script`, no
138
+ * single-default-run-step projection, no Git-context rule.
139
+ *
140
+ * Boxes then boot with `create({ snapshotId })` — the same path the
141
+ * checkpoint-restore flow already exercises.
142
+ */
143
+
144
+ /**
145
+ * A provider-neutral runtime asset, taken from the *running CLI* and pushed in
146
+ * over the data plane.
147
+ */
148
+ interface PrepareAsset {
149
+ /** Basename in the CLI's `runtime/_shared/`. */
150
+ shared: string;
151
+ /** Absolute destination inside the box. */
152
+ dest: string;
153
+ /** Octal mode applied on install. */
154
+ mode: string;
155
+ }
156
+ /**
157
+ * `shared` names must exist in the SDK's `SHARED_RUNTIME_ASSETS`; never vendor
158
+ * copies of these — resolving them from the CLI is what keeps the baked
159
+ * `agentbox-ctl` version-locked to the host driving the box.
160
+ *
161
+ * The `gh` / `git` / `ntn` / `linear` entries are deliberately *shims*: they land
162
+ * in `/usr/local/bin`, which precedes `/usr/bin` on PATH, so an agent running
163
+ * `git push` reaches the host relay (which holds the credentials) rather than the
164
+ * real binary. That indirection is why a box needs no credentials of its own.
165
+ */
166
+ declare const PREPARE_ASSETS: readonly PrepareAsset[];
167
+ /** Staging path an asset is uploaded to before being installed into place. */
168
+ declare function stagedAssetPath(shared: string): string;
169
+ /** One install step, run as its own exec so a failure names the step. */
170
+ interface PrepareStep {
171
+ name: string;
172
+ command: string;
173
+ /** Run as root (`privileged`). Unset means the box user. */
174
+ root?: boolean;
175
+ /** Per-step attempt timeout; the backend default (5 min) is too short for npm. */
176
+ timeoutMs?: number;
177
+ }
178
+ /**
179
+ * The install plan, as discrete steps. Pure, so the whole recipe is assertable
180
+ * offline — a mistake here otherwise only surfaces minutes into a live bake.
181
+ *
182
+ * `bash -c` is guaranteed (the backend's exec always uses it), so `pipefail` and
183
+ * heredocs are safe here, unlike in a template build step.
184
+ */
185
+ declare function buildInstallPlan(): PrepareStep[];
186
+ interface PrepareTenkiOptions {
187
+ name?: string;
188
+ hostWorkspace?: string;
189
+ /** Rebake even when a base snapshot is already recorded. */
190
+ force?: boolean;
191
+ /** Tenki base-image id the builder boots from (default: the workspace default). */
192
+ baseImage?: string;
193
+ /** Tenki workspace to build in (defaults to the token's own scope). */
194
+ workspaceId?: string;
195
+ cpuCount?: number;
196
+ memoryMB?: number;
197
+ diskSizeGB?: number;
198
+ onLog?: (line: string) => void;
199
+ }
200
+ interface PrepareTenkiResult {
201
+ /** The Tenki snapshot id recorded as the base. */
202
+ snapshotName?: string;
203
+ }
204
+ /**
205
+ * Build a throwaway HOME containing a *shadow* of `~/.codex`: real directories,
206
+ * but every regular file replaced by a symlink to the original.
207
+ *
208
+ * Why: `stageCodexStaticForUpload` runs `rsync -aL` over `~/.codex`, and `-a`
209
+ * implies `-D` (recreate devices and specials). Codex leaves a live Unix socket
210
+ * at `~/.codex/ipc/ipc.sock` whenever it has run, which rsync cannot recreate —
211
+ * it exits 23 ("partial transfer due to error") and the whole staging throws. The
212
+ * upstream exclude list doesn't filter specials, so the built-in vercel and e2b
213
+ * providers fail their entire `prepare` on such a host.
214
+ *
215
+ * The shadow skips anything that isn't a directory or regular file, so the socket
216
+ * simply isn't there to trip over. Symlinks (rather than copies) mean no user data
217
+ * is duplicated on disk — `rsync -L` reads through them to the originals. We also
218
+ * omit `auth.json`: the helper excludes it from the tarball anyway, and this way
219
+ * the token never lands in a temp directory at all.
220
+ *
221
+ * Returns null when there is no `~/.codex` to shadow — the caller then stages
222
+ * normally and the helper reports "nothing to stage".
223
+ */
224
+ declare function shadowCodexHome(hostHome: string): {
225
+ home: string;
226
+ cleanup: () => void;
227
+ } | null;
228
+ declare function prepareTenki(opts?: PrepareTenkiOptions): Promise<PrepareTenkiResult>;
229
+ /** Provider-level binding used by the CLI's `prepare` command. */
230
+ declare const prepareTenkiProvider: NonNullable<Provider['prepare']>;
231
+
232
+ /**
233
+ * Persisted record of what `agentbox prepare --provider tenki` has resolved.
234
+ * Lives at `~/.agentbox/tenki-prepared.json` so the auto-prepare gate
235
+ * (`ensureTenkiBaseImage()`) and `backend.provision` can resolve the base
236
+ * image every box boots from.
237
+ *
238
+ * Tenki boots a session from a snapshot id.
239
+ * The "base" recorded here is the ID of the snapshot carrying
240
+ * the AgentBox runtime (agentbox-ctl, the agents, tmux), captured by
241
+ * `prepare` from a throwaway builder sandbox. Per-box `create` then boots from
242
+ * it in seconds.
243
+ *
244
+ * Schema versioned so future shape changes can migrate. Only `schema: 2` is
245
+ * accepted; an older file is refused by `readPreparedState` and simply re-baked.
246
+ */
247
+ declare const SCHEMA: 2;
248
+ interface PreparedTenkiBase {
249
+ /** Tenki snapshot id the AgentBox runtime is baked into. `create({ snapshotId })` boots from this. */
250
+ snapshotId: string;
251
+ /** Human-readable snapshot label (informational; ids are what we boot from). */
252
+ snapshotName?: string;
253
+ /** CLI version that produced this base (informational). */
254
+ cliVersion?: string;
255
+ /** Git short SHA of the CLI build (informational). */
256
+ cliCommit?: string;
257
+ /** ISO timestamp of prepare completion. */
258
+ createdAt: string;
259
+ }
260
+ interface PreparedTenkiState {
261
+ schema: typeof SCHEMA;
262
+ /** The shared base image ref. Absent until first `agentbox prepare`. */
263
+ base?: PreparedTenkiBase;
264
+ }
265
+ declare function preparedStatePath(): string;
266
+ declare function readPreparedState(): PreparedTenkiState;
267
+ declare function writePreparedState(state: PreparedTenkiState): void;
268
+ /** Update one field of the state without forcing callers to read/merge/write. */
269
+ declare function updatePreparedState(mutate: (s: PreparedTenkiState) => void): void;
270
+ /**
271
+ * CURRENT build-context fingerprint for the tenki base. The base is an opaque
272
+ * Tenki snapshot id, so there is no reproducible host-side fingerprint to diff
273
+ * against. Returning `undefined` makes the cross-provider freshness nudge
274
+ * degrade to "can't tell, don't nag" — the same contract the other providers use
275
+ * when assets can't be resolved.
276
+ */
277
+ declare function currentTenkiBaseFingerprintLive(): Promise<string | undefined>;
278
+ /**
279
+ * First-use gate. If no base image is recorded, throw an actionable error
280
+ * pointing at `agentbox prepare --provider tenki`. Called by
281
+ * `backend.provision` (so `create` / `claude` trip it but `prepare` itself
282
+ * does not — same shape as the hetzner / vercel / e2b gates).
283
+ */
284
+ declare function ensureTenkiBaseImage(): void;
285
+
286
+ /**
287
+ * `buildTenkiAttach` — the Tenki provider's override of `Provider.buildAttach`.
288
+ *
289
+ * ## Why this shells out to real OpenSSH
290
+ *
291
+ * `session.ssh()` is NOT a shell channel: `SSHConnection` is a thin wrapper over
292
+ * a WebSocket carrying the **raw SSH wire protocol**, exposing only
293
+ * `read()/write()/close()`. Writing shell commands into it just gets you the
294
+ * gateway's version banner (`SSH-2.0-Tenki-edge-gateway`) — reaching a shell
295
+ * means performing the SSH handshake: key exchange, publickey auth, channel
296
+ * open, PTY request. The data plane is no help either: `session.run` has no
297
+ * tty/pty option, so there is no interactive path through it.
298
+ *
299
+ * So we let the host's `ssh` do the protocol and use our helper purely as the
300
+ * transport:
301
+ *
302
+ * ssh -o ProxyCommand='node attach-helper.cjs --proxy --session-id <id>' \
303
+ * -i <key> -o CertificateFile=<key>-cert.pub -t <user>@<id> <inner cmd>
304
+ *
305
+ * Auth is an ephemeral ed25519 key plus a short-lived OpenSSH certificate from
306
+ * `issueSandboxSSHCert` — which the SDK documents for exactly this
307
+ * (`<key>-cert.pub` / `-o CertificateFile=`).
308
+ *
309
+ * Beyond correctness this gets window-size propagation for free: OpenSSH
310
+ * forwards SIGWINCH, so tmux tracks terminal resizes. The previous
311
+ * bridge-the-bytes-ourselves design could not, since `SSHConnection` has no
312
+ * resize op.
313
+ *
314
+ * Detached pre-start keeps its own path (see `--detached` in the helper): it only
315
+ * needs to CREATE the tmux session, which is a plain non-interactive exec.
316
+ */
317
+
318
+ declare function buildTenkiAttach(box: BoxRecord, kind: AttachKind, opts?: BuildAttachOptions): Promise<AttachSpec>;
319
+
320
+ /**
321
+ * The Tenki sandbox provider. A thin `CloudBackend` over the
322
+ * `@tenkicloud/sandbox` SDK, composed via the provider SDK's
323
+ * `createCloudProvider` for everything provider-agnostic (workspace seeding,
324
+ * ctl launch, state, relay polling).
325
+ *
326
+ * Three capabilities are overridden on top of the cloud scaffold:
327
+ * - `prepare` — bake the AgentBox base snapshot in the Tenki workspace
328
+ * (`agentbox prepare --provider tenki`).
329
+ * - `buildAttach` — the host's own `ssh` client, pointed at the session
330
+ * through a `ProxyCommand` that bridges stdio to
331
+ * `session.ssh()`. Requires OpenSSH on the host.
332
+ * - `checkpoint` — store the Tenki snapshot id in the manifest so restore
333
+ * boots from it (`createSnapshotAndWait` returns an id-addressed snapshot).
334
+ *
335
+ * `launchDockerd: true` — in-box Docker works on Tenki: verified on a live
336
+ * microVM with cgroup2 and native overlay2 (not the vfs fallback), brought up by
337
+ * the shared `agentbox-dockerd-start` launcher. Containers need no nested
338
+ * virtualization, so the absence of /dev/kvm doesn't matter. The daemon is
339
+ * started by the same idempotent bootstrap kick used on create and resume.
340
+ */
341
+
342
+ /** The AgentBox provider name. `agentbox create --provider tenki` matches on this exactly. */
343
+ declare const PROVIDER_NAME = "tenki";
344
+ declare const tenkiProvider: Provider;
345
+ /** Uniform surface the CLI provider loader resolves this package through. */
346
+ declare const providerModule: ProviderModule;
347
+
348
+ export { type EnsureTenkiCredentialsOptions, PREPARE_ASSETS, PROVIDER_NAME, type PrepareAsset, type PrepareStep, type PrepareTenkiOptions, type PrepareTenkiResult, type PreparedTenkiBase, type PreparedTenkiState, type TenkiCredStatus, buildInstallPlan, buildTenkiAttach, currentTenkiBaseFingerprintLive, ensureTenkiBaseImage, ensureTenkiCredentials, ensureTenkiEnvLoaded, maskKey, prepareTenki, prepareTenkiProvider, preparedStatePath, providerModule, readPreparedState, readTenkiCredStatus, reloadTenkiEnv, secretsPath, setTenkiCredentials, shadowCodexHome, stagedAssetPath, tenkiBackend, tenkiProvider, updatePreparedState, writePreparedState };