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