@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.
- package/LICENSE +21 -0
- package/README.md +220 -2
- package/dist/attach-helper.cjs +181 -0
- package/dist/attach-helper.cjs.map +1 -0
- package/dist/index.d.ts +333 -0
- package/dist/index.js +1634 -0
- package/dist/index.js.map +1 -0
- package/package.json +78 -4
package/dist/index.d.ts
ADDED
|
@@ -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 };
|