@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Luxor Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,221 @@
1
- # Temporary Holding Version
1
+ # @tenkicloud/agentbox-provider
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Run [AgentBox](https://agent-box.sh) coding agents in [Tenki](https://tenki.cloud) Firecracker
4
+ microVMs — a community provider plugin, published as its own package and registered with
5
+ `agentbox plugin add`.
6
+
7
+ ## What Tenki is
8
+
9
+ [Tenki](https://tenki.cloud) provisions Firecracker microVMs on demand, driven by the official
10
+ [`@tenkicloud/sandbox`](https://www.npmjs.com/package/@tenkicloud/sandbox) TypeScript SDK — a
11
+ ConnectRPC control plane plus a per-session data plane for command execution and file transfer.
12
+ Each AgentBox box becomes one Tenki session: a full VM with its own kernel, not a shared container.
13
+
14
+ ## What you get
15
+
16
+ | Feature | What it means |
17
+ | ------------------------- | ---------------------------------------------------------------------------------------------------------- |
18
+ | **A VM per box** | Every box is its own Firecracker microVM — real kernel isolation, not a shared container |
19
+ | **Your project, ready** | The workspace arrives as a git clone on a per-box branch, carrying your uncommitted and untracked changes |
20
+ | **Agents preinstalled** | `claude`, `codex` and `opencode` are in the image; `agentbox tenki claude` drops you straight into one |
21
+ | **Your setup travels** | Settings, MCP servers and plugins come from your host; credentials are seeded per box, never baked in |
22
+ | **Interactive terminal** | Attach over SSH into a tmux session — detach and come back, and resizing your window works |
23
+ | **Public preview URLs** | Every exposed port gets an HTTPS URL with no tunnel or token to manage; signed URLs for private ports |
24
+ | **Files in and out** | `agentbox cp` moves files between your machine and the box, in either direction |
25
+ | **Docker inside the box** | `docker build` and `docker run` work in the box, so containerised test suites and compose stacks run there |
26
+ | **Free pause and resume** | Pausing costs nothing and keeps running processes; resuming picks up where you left off |
27
+ | **Checkpoints** | Snapshot a warm box and start new ones from it, dependencies and all |
28
+ | **Long sessions** | A box you are actively using has its deadline extended for you |
29
+ | **Seconds to a new box** | A one-time `agentbox prepare` bakes the base image, so every box after that boots ready |
30
+
31
+ ## Requirements
32
+
33
+ - Node.js 20 or newer
34
+ - The `agentbox` CLI (0.27.1 or newer — it must support provider SDK v2)
35
+ - An OpenSSH client (`ssh` and `ssh-keygen`) on the host, used for interactive attach
36
+ - A Tenki account with a workspace and an auth token
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ npm i -g @tenkicloud/agentbox-provider
42
+ agentbox plugin add @tenkicloud/agentbox-provider
43
+ agentbox plugin list # -> tenki … (SDK v2)
44
+ ```
45
+
46
+ AgentBox does not know about `tenki` until `plugin add` records it. A plugin runs in-process with
47
+ full host and credential access, so `plugin add` is the trust boundary — see
48
+ [Security](#security-considerations).
49
+
50
+ ## Authentication
51
+
52
+ On a terminal, the first `agentbox create --provider tenki` prompts for a token and saves it. To set
53
+ it up ahead of time, expose it as `TENKI_AUTH_TOKEN`:
54
+
55
+ ```bash
56
+ export TENKI_AUTH_TOKEN=... # or add TENKI_AUTH_TOKEN=… to ~/.agentbox/secrets.env
57
+ agentbox doctor # shows the `tenki:` group
58
+ ```
59
+
60
+ `TENKI_API_TOKEN` is accepted as an alias. Credentials are read from the environment first, then
61
+ from `~/.agentbox/secrets.env` (mode `0600`); project `.env` files are never harvested. The token is
62
+ never logged and never included in the published package.
63
+
64
+ ## Basic usage
65
+
66
+ One-time: bake the AgentBox base image in your Tenki workspace, then create a box. `prepare` boots a
67
+ throwaway builder from Tenki's `sandbox` base, installs tmux, the AgentBox runtime and the coding
68
+ agents, snapshots it, and pins the snapshot id. Every box then boots from that snapshot.
69
+
70
+ ```bash
71
+ agentbox prepare --provider tenki # bakes the base image; pins the snapshot id locally
72
+ agentbox create --provider tenki
73
+ agentbox tenki claude # provider-prefix sugar also works
74
+ ```
75
+
76
+ Pin it project-wide with `box.provider: tenki` in `agentbox.yaml`, or per box with
77
+ `--provider tenki`.
78
+
79
+ ## Resource configuration
80
+
81
+ Because AgentBox does not add config keys for plugin providers, Tenki reads its settings from the
82
+ generic keys plus its own environment variables:
83
+
84
+ | Setting | Where | Notes |
85
+ | ------------------ | ----------------------------- | ---------------------------------------------------------- |
86
+ | VM size | `--size` / `box.size` | `cpu-memory[-disk]` in GB, e.g. `4-8-20` |
87
+ | Default checkpoint | `box.defaultCheckpoint` | Generic key; no per-provider variant |
88
+ | Base image | `AGENTBOX_TENKI_BASE_IMAGE` | Tenki base image `prepare` layers onto (default `sandbox`) |
89
+ | Workspace | `AGENTBOX_TENKI_WORKSPACE_ID` | Defaults to the token's own scope |
90
+ | Session lifetime | `AGENTBOX_TENKI_TIMEOUT_MS` | Seeds the host keepalive loop |
91
+
92
+ The baked base snapshot id lives in `~/.agentbox/tenki-prepared.json`, managed by this plugin.
93
+
94
+ ## Agent configuration and credentials
95
+
96
+ `prepare` bakes your host's **static** agent config into the base snapshot — settings, MCP servers,
97
+ plugin registries, and `/workspace` pre-trust — so boxes start with your own setup rather than a
98
+ blank one. It is best-effort: an agent whose config can't be read is skipped with a warning rather
99
+ than failing the bake.
100
+
101
+ One host-side wrinkle is handled for you: codex leaves a live Unix socket at `~/.codex/ipc/ipc.sock`
102
+ whenever it has run, and the upstream staging helper's `rsync -a` cannot recreate a socket (it exits
103
+ 23). `prepare` stages codex through a sanitized shadow of that directory, so its config bakes whether
104
+ or not codex is running.
105
+
106
+ Credentials are handled separately, and deliberately not baked. AgentBox seeds them per box at
107
+ create time into `~/.agentbox-creds/<agent>/`, which the base image pivots into place with symlinks
108
+ (`~/.claude/.credentials.json`, `~/.codex/auth.json`, `~/.local/share/opencode/auth.json`). That
109
+ keeps auth tokens out of an image that may be long-lived or shared, and lets a refreshed token
110
+ propagate without re-baking.
111
+
112
+ That seeding only has something to push when AgentBox has host-side credential backups
113
+ (`~/.agentbox/<agent>-credentials.json`), which are captured from boxes you have already used. With
114
+ no backup yet, the symlinks are dangling by design: you log into the agent once inside the box, the
115
+ login lands in `~/.agentbox-creds/`, and AgentBox captures it from there for later boxes.
116
+
117
+ The base image also creates a `vscode` account as an alias of the box user (same uid, same home).
118
+ The credential seed shipped in the current provider SDK extracts with an in-shell `sudo -u vscode`,
119
+ so without that account the seed fails with `sudo: unknown user vscode` and boxes start logged out.
120
+ The alias keeps seeded files owned by the user the agents actually run as.
121
+
122
+ ## Preview URLs
123
+
124
+ Every exposed port gets a public HTTPS URL through Tenki's WebProxy — no tunnel and no token
125
+ needed. `agentbox open` and the `expose:` entries in `agentbox.yaml` work as they do on any cloud
126
+ provider. Signed URLs are available for ports you don't want publicly reachable.
127
+
128
+ ## Checkpoint support
129
+
130
+ Tenki snapshots are **id-addressed**: the platform returns an opaque id rather than a name you
131
+ choose. The plugin stores that id in AgentBox's cloud-checkpoint manifest, so
132
+ `agentbox checkpoint create` / `list` / restore behave the same as on Vercel and E2B. Snapshots are
133
+ taken live — the source box keeps running.
134
+
135
+ ## Troubleshooting
136
+
137
+ - **`unknown provider "tenki"`** — the plugin isn't registered. Run `agentbox plugin add
138
+ @tenkicloud/agentbox-provider` and confirm with `agentbox plugin list`.
139
+ - **`plugin targets provider SDK vN`** — your `agentbox` CLI is too old. Update to 0.27.1 or newer.
140
+ - **Credentials not found** — check `agentbox doctor`. The token must be in the environment or in
141
+ `~/.agentbox/secrets.env`, not a project `.env`.
142
+ - **No base image** — run `agentbox prepare --provider tenki`. Boxes boot from a snapshot baked in
143
+ your own Tenki workspace, so this is required once per workspace. If that snapshot is later deleted
144
+ (or the token is pointed at another workspace), the next `prepare` notices and re-bakes.
145
+ - **Box expired mid-session** — raise `AGENTBOX_TENKI_TIMEOUT_MS`; the host keepalive extends a
146
+ working session, but it starts from the lifetime the box was created with.
147
+ - **Attach fails to open a shell** — attach needs `ssh` and `ssh-keygen` on the host. Per-box key
148
+ material lives in `~/.agentbox/boxes/<session-id>/ssh/`; the certificate is re-minted on every
149
+ attach, so deleting that directory is safe and simply regenerates it.
150
+ - **Attach as a different user** — set `AGENTBOX_TENKI_SSH_USER` if a custom base image does not run
151
+ as `tenki`.
152
+
153
+ ## Security considerations
154
+
155
+ - An AgentBox provider plugin runs **in-process with the CLI**, with full host and credential
156
+ access. AgentBox does not sandbox plugin code — provisioning infrastructure and handling secrets
157
+ is the job. `agentbox plugin add` is the consent boundary; only add plugins you trust.
158
+ - The Tenki token is read from the environment or `~/.agentbox/secrets.env` (`0600`), never logged,
159
+ and never bundled into the published artifact.
160
+ - Each box is an isolated microVM, so an agent cannot reach your host filesystem. Host-side
161
+ operations that need your credentials (`git push` in particular) are brokered by the AgentBox host
162
+ relay rather than by handing keys to the box.
163
+ - **The microVM is the boundary, not the in-box user.** Inside a box the agent has passwordless
164
+ `sudo` and Docker access, which is root-equivalent by design — in-box Docker requires it.
165
+ - Interactive attach trusts the Tenki gateway's TLS rather than an SSH host key (the "host" is a
166
+ session id, not a stable endpoint). If you override `TENKI_BASE_URL` or `TENKI_GATEWAY_ADDRESS`,
167
+ point them only at endpoints you trust.
168
+ - Preview URLs are public: anyone holding one can reach the port, with no token. Use the signed
169
+ (expiring) variant for anything that should not stay durably reachable.
170
+
171
+ The full trust model, and how to report a vulnerability privately, are in
172
+ [SECURITY.md](./SECURITY.md).
173
+
174
+ ## Uninstallation
175
+
176
+ ```bash
177
+ agentbox plugin remove tenki # unregister (leaves the npm package installed)
178
+ npm uninstall -g @tenkicloud/agentbox-provider
179
+ ```
180
+
181
+ Destroy any remaining boxes with `agentbox destroy <name>` first — removing the plugin leaves
182
+ AgentBox unable to manage them. Base images and snapshots in your Tenki workspace are not deleted;
183
+ remove those through Tenki.
184
+
185
+ ## Development
186
+
187
+ ```bash
188
+ npm install
189
+ npm run build
190
+ npm test
191
+ npm run validate:package # packs, installs in isolation, asserts the plugin contract
192
+
193
+ agentbox plugin add . # register this working copy
194
+ agentbox doctor
195
+ ```
196
+
197
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full gate set, the test conventions, and what to
198
+ know before running `prepare` against a real (billable) workspace. Release notes are in
199
+ [CHANGELOG.md](./CHANGELOG.md).
200
+
201
+ ## Releasing
202
+
203
+ Releases run from the **Publish** workflow, and the version in `package.json` is the single source
204
+ of truth for both npm and the git tag:
205
+
206
+ 1. Bump `version` in `package.json` on `main`, and add the matching section to
207
+ [CHANGELOG.md](./CHANGELOG.md). Both CI and the publish workflow fail if the declared version has
208
+ no changelog entry (`npm run check:changelog`).
209
+ 2. Run the workflow with `dry_run` left on. It runs every CI gate plus `npm publish --dry-run`,
210
+ and changes nothing.
211
+ 3. Run it again with `dry_run` off. It publishes to npm, tags the commit `v<version>`, and cuts a
212
+ GitHub release with generated notes.
213
+
214
+ Publishing is idempotent — an already-published version is skipped — so a re-run after a partial
215
+ failure is safe. Authentication is npm trusted publishing (OIDC), so there is no token to rotate.
216
+
217
+ ## License
218
+
219
+ MIT — see [LICENSE](./LICENSE). Maintained by Luxor Labs. AgentBox itself is a separate project by
220
+ Marco D'Alia; this plugin builds on its public
221
+ [`@madarco/agentbox-provider-sdk`](https://www.npmjs.com/package/@madarco/agentbox-provider-sdk).
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+
3
+ // src/attach-helper.ts
4
+ var import_sandbox = require("@tenkicloud/sandbox");
5
+
6
+ // src/env-loader.ts
7
+ var import_node_fs = require("fs");
8
+ var import_node_os = require("os");
9
+ var import_node_path = require("path");
10
+ var TENKI_KEYS = [
11
+ "TENKI_AUTH_TOKEN",
12
+ "TENKI_API_TOKEN",
13
+ "TENKI_BASE_URL",
14
+ "TENKI_GATEWAY_ADDRESS"
15
+ ];
16
+ var loaded = false;
17
+ function ensureTenkiEnvLoaded() {
18
+ if (loaded) return;
19
+ loaded = true;
20
+ importTenkiFromFile((0, import_node_path.resolve)((0, import_node_os.homedir)(), ".agentbox", "secrets.env"), TENKI_KEYS);
21
+ if (!process.env.TENKI_AUTH_TOKEN && process.env.TENKI_API_TOKEN) {
22
+ process.env.TENKI_AUTH_TOKEN = process.env.TENKI_API_TOKEN;
23
+ }
24
+ }
25
+ function importTenkiFromFile(path, keys) {
26
+ if (!(0, import_node_fs.existsSync)(path)) return;
27
+ let body;
28
+ try {
29
+ body = (0, import_node_fs.readFileSync)(path, "utf8");
30
+ } catch {
31
+ return;
32
+ }
33
+ const parsed = parseEnvFile(body);
34
+ for (const key of keys) {
35
+ if (process.env[key] !== void 0) continue;
36
+ const value = parsed[key];
37
+ if (typeof value === "string") {
38
+ process.env[key] = value;
39
+ }
40
+ }
41
+ }
42
+ function parseEnvFile(body) {
43
+ const out = {};
44
+ for (const rawLine of body.split(/\r?\n/)) {
45
+ const line = rawLine.trim();
46
+ if (line.length === 0 || line.startsWith("#")) continue;
47
+ const stripped = line.startsWith("export ") ? line.slice("export ".length) : line;
48
+ const eq = stripped.indexOf("=");
49
+ if (eq <= 0) continue;
50
+ const key = stripped.slice(0, eq).trim();
51
+ let value = stripped.slice(eq + 1).trim();
52
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
53
+ value = value.slice(1, -1);
54
+ }
55
+ out[key] = value;
56
+ }
57
+ return out;
58
+ }
59
+
60
+ // src/attach-helper.ts
61
+ function parseArgs(argv) {
62
+ let sessionId;
63
+ let mode;
64
+ for (let i = 0; i < argv.length; i++) {
65
+ const a = argv[i];
66
+ if (a === "--session-id") {
67
+ sessionId = argv[i + 1];
68
+ i++;
69
+ } else if (a === "--proxy") {
70
+ mode = "proxy";
71
+ } else if (a === "--detached") {
72
+ mode = "detached";
73
+ }
74
+ }
75
+ if (!sessionId) {
76
+ process.stderr.write("attach-helper: --session-id is required\n");
77
+ process.exit(2);
78
+ }
79
+ if (!mode) {
80
+ process.stderr.write("attach-helper: one of --proxy or --detached is required\n");
81
+ process.exit(2);
82
+ }
83
+ return { sessionId, mode };
84
+ }
85
+ function buildClient() {
86
+ const authToken = process.env.TENKI_AUTH_TOKEN;
87
+ if (!authToken) {
88
+ process.stderr.write("attach-helper: TENKI_AUTH_TOKEN env is required\n");
89
+ process.exit(2);
90
+ }
91
+ const opts = { authToken };
92
+ if (process.env.TENKI_BASE_URL) opts.baseUrl = process.env.TENKI_BASE_URL;
93
+ if (process.env.TENKI_GATEWAY_ADDRESS) opts.gatewayAddress = process.env.TENKI_GATEWAY_ADDRESS;
94
+ return new import_sandbox.TenkiSandbox(opts);
95
+ }
96
+ async function main() {
97
+ const { sessionId, mode } = parseArgs(process.argv.slice(2));
98
+ ensureTenkiEnvLoaded();
99
+ const client = buildClient();
100
+ let session;
101
+ try {
102
+ session = await client.get(sessionId);
103
+ } catch (err) {
104
+ process.stderr.write(
105
+ `attach-helper: could not resolve session ${sessionId}: ${err instanceof Error ? err.message : String(err)}
106
+ `
107
+ );
108
+ process.exit(1);
109
+ }
110
+ if (session.state !== "RUNNING") {
111
+ try {
112
+ await session.resume();
113
+ await session.waitReady(6e4);
114
+ } catch {
115
+ }
116
+ }
117
+ if (mode === "detached") {
118
+ const inner = process.env.AGENTBOX_TENKI_INNER_CMD;
119
+ if (!inner) {
120
+ process.stderr.write("attach-helper: AGENTBOX_TENKI_INNER_CMD env is required\n");
121
+ process.exit(2);
122
+ }
123
+ try {
124
+ const r = await session.run(["bash", "-c", inner], { cwd: "/workspace" });
125
+ process.exit(r.exitCode ?? 0);
126
+ } catch (err) {
127
+ process.stderr.write(
128
+ `attach-helper: detached pre-start failed: ${err instanceof Error ? err.message : String(err)}
129
+ `
130
+ );
131
+ process.exit(1);
132
+ }
133
+ }
134
+ let conn;
135
+ try {
136
+ conn = await session.ssh();
137
+ } catch (err) {
138
+ process.stderr.write(
139
+ `attach-helper: could not open the SSH transport: ${err instanceof Error ? err.message : String(err)}
140
+ `
141
+ );
142
+ process.exit(1);
143
+ }
144
+ process.stdin.on("data", (chunk) => {
145
+ conn.write(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)).catch(() => {
146
+ });
147
+ });
148
+ process.stdin.on("end", () => {
149
+ try {
150
+ conn.close();
151
+ } catch {
152
+ }
153
+ });
154
+ let exitCode = 0;
155
+ try {
156
+ for (; ; ) {
157
+ const data = await conn.read();
158
+ if (data === null) break;
159
+ process.stdout.write(data);
160
+ }
161
+ } catch (err) {
162
+ process.stderr.write(
163
+ `attach-helper: SSH transport read failed: ${err instanceof Error ? err.message : String(err)}
164
+ `
165
+ );
166
+ exitCode = 1;
167
+ } finally {
168
+ try {
169
+ conn.close();
170
+ } catch {
171
+ }
172
+ }
173
+ process.exit(exitCode);
174
+ }
175
+ main().catch((err) => {
176
+ process.stderr.write(
177
+ `attach-helper: unhandled error: ${err instanceof Error ? err.message : String(err)}
178
+ `
179
+ );
180
+ process.exit(1);
181
+ });
182
+ //# sourceMappingURL=attach-helper.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/attach-helper.ts","../src/env-loader.ts"],"sourcesContent":["/**\n * Standalone helper spawned by `buildTenkiAttach`. Two modes, no interactive\n * shell handling of its own:\n *\n * --proxy An `ssh` ProxyCommand. Bridges this process's stdin/stdout to\n * the session's SSH transport (`session.ssh()`), so the host's\n * real `ssh` client speaks the SSH protocol end to end and owns\n * the PTY, auth, and window-resize handling.\n *\n * --detached Pre-start. Runs AGENTBOX_TENKI_INNER_CMD once over the\n * non-interactive data plane and exits. In detached mode the\n * inner command only CREATES the tmux session, so there is\n * nothing to attach to and opening a channel would hang the\n * host's `runDetached` await forever.\n *\n * Why the proxy mode is byte-shuffling and nothing more: `SSHConnection` wraps a\n * WebSocket carrying the RAW SSH wire protocol. It is a transport, not a shell —\n * an earlier version of this helper wrote shell commands into it and got back\n * only the gateway's version banner.\n *\n * Argv: `node attach-helper.cjs (--proxy|--detached) --session-id <id>`\n * Env:\n * TENKI_AUTH_TOKEN Tenki credentials (threaded in by build-attach).\n * AGENTBOX_TENKI_INNER_CMD Inner bash command (--detached only).\n * TENKI_BASE_URL,\n * TENKI_GATEWAY_ADDRESS Optional control-plane overrides.\n */\n\nimport { TenkiSandbox } from '@tenkicloud/sandbox';\nimport { ensureTenkiEnvLoaded } from './env-loader.js';\n\ninterface ParsedArgs {\n sessionId: string;\n mode: 'proxy' | 'detached';\n}\n\nfunction parseArgs(argv: string[]): ParsedArgs {\n let sessionId: string | undefined;\n let mode: 'proxy' | 'detached' | undefined;\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i];\n if (a === '--session-id') {\n sessionId = argv[i + 1];\n i++;\n } else if (a === '--proxy') {\n mode = 'proxy';\n } else if (a === '--detached') {\n mode = 'detached';\n }\n }\n if (!sessionId) {\n process.stderr.write('attach-helper: --session-id is required\\n');\n process.exit(2);\n }\n if (!mode) {\n process.stderr.write('attach-helper: one of --proxy or --detached is required\\n');\n process.exit(2);\n }\n return { sessionId, mode };\n}\n\nfunction buildClient(): TenkiSandbox {\n const authToken = process.env.TENKI_AUTH_TOKEN;\n if (!authToken) {\n process.stderr.write('attach-helper: TENKI_AUTH_TOKEN env is required\\n');\n process.exit(2);\n }\n const opts: ConstructorParameters<typeof TenkiSandbox>[0] = { authToken };\n if (process.env.TENKI_BASE_URL) opts.baseUrl = process.env.TENKI_BASE_URL;\n if (process.env.TENKI_GATEWAY_ADDRESS) opts.gatewayAddress = process.env.TENKI_GATEWAY_ADDRESS;\n return new TenkiSandbox(opts);\n}\n\nasync function main(): Promise<void> {\n const { sessionId, mode } = parseArgs(process.argv.slice(2));\n ensureTenkiEnvLoaded();\n\n const client = buildClient();\n let session;\n try {\n session = await client.get(sessionId);\n } catch (err) {\n process.stderr.write(\n `attach-helper: could not resolve session ${sessionId}: ${err instanceof Error ? err.message : String(err)}\\n`,\n );\n process.exit(1);\n }\n\n // Defensive: an attach right after resume should already be RUNNING, but wake\n // a paused box rather than failing the connection.\n if (session.state !== 'RUNNING') {\n try {\n await session.resume();\n await session.waitReady(60_000);\n } catch {\n // fall through — the op below surfaces a clear transport error\n }\n }\n\n if (mode === 'detached') {\n const inner = process.env.AGENTBOX_TENKI_INNER_CMD;\n if (!inner) {\n process.stderr.write('attach-helper: AGENTBOX_TENKI_INNER_CMD env is required\\n');\n process.exit(2);\n }\n try {\n const r = await session.run(['bash', '-c', inner], { cwd: '/workspace' });\n process.exit(r.exitCode ?? 0);\n } catch (err) {\n process.stderr.write(\n `attach-helper: detached pre-start failed: ${err instanceof Error ? err.message : String(err)}\\n`,\n );\n process.exit(1);\n }\n }\n\n // --proxy: pure transport. stdin -> gateway, gateway -> stdout. `ssh` on the\n // other side of these pipes does the handshake and runs the remote command.\n let conn;\n try {\n conn = await session.ssh();\n } catch (err) {\n process.stderr.write(\n `attach-helper: could not open the SSH transport: ${err instanceof Error ? err.message : String(err)}\\n`,\n );\n process.exit(1);\n }\n\n process.stdin.on('data', (chunk: Buffer) => {\n conn.write(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)).catch(() => {\n // transport gone; the read loop below exits and we follow it out\n });\n });\n // ssh closing its end of the pipe means the session is over.\n process.stdin.on('end', () => {\n try {\n conn.close();\n } catch {\n // already closed\n }\n });\n\n let exitCode = 0;\n try {\n for (;;) {\n const data = await conn.read();\n if (data === null) break; // transport closed\n process.stdout.write(data);\n }\n } catch (err) {\n process.stderr.write(\n `attach-helper: SSH transport read failed: ${err instanceof Error ? err.message : String(err)}\\n`,\n );\n exitCode = 1;\n } finally {\n try {\n conn.close();\n } catch {\n // ignore\n }\n }\n process.exit(exitCode);\n}\n\nmain().catch((err) => {\n process.stderr.write(\n `attach-helper: unhandled error: ${err instanceof Error ? err.message : String(err)}\\n`,\n );\n process.exit(1);\n});\n","/**\n * Tenki env auto-loader. The `@tenkicloud/sandbox` SDK reads `TENKI_AUTH_TOKEN`\n * from `process.env` (and we honor optional `TENKI_BASE_URL` /\n * `TENKI_GATEWAY_ADDRESS` overrides for non-default / self-hosted control\n * planes). We seed those from `~/.agentbox/secrets.env` — the store AgentBox's\n * built-in cloud providers use — so the SDK Just Works after a one-time setup.\n *\n * Lookup order (first wins; process.env is never overwritten):\n * 1. `process.env` (already set in the shell).\n * 2. `~/.agentbox/secrets.env` — written by the credential prompt.\n *\n * `TENKI_API_TOKEN` is accepted as an alias for `TENKI_AUTH_TOKEN`.\n *\n * Project-level `.env` / `.env.local` are intentionally NOT consulted: those\n * files belong to the app code being developed. Put host credentials in\n * `~/.agentbox/secrets.env` (or the shell env).\n *\n * Only TENKI-prefixed keys are imported; the rest of the file is left alone.\n * Idempotent and side-effect-free after the first call.\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { resolve } from 'node:path';\n\nconst TENKI_KEYS = [\n 'TENKI_AUTH_TOKEN',\n 'TENKI_API_TOKEN',\n 'TENKI_BASE_URL',\n 'TENKI_GATEWAY_ADDRESS',\n] as const;\n\nlet loaded = false;\n\nexport function ensureTenkiEnvLoaded(): void {\n if (loaded) return;\n loaded = true;\n importTenkiFromFile(resolve(homedir(), '.agentbox', 'secrets.env'), TENKI_KEYS);\n // `TENKI_AUTH_TOKEN` is the name the Tenki SDK reads and the one we write.\n // `TENKI_API_TOKEN` is accepted as an alias because it's the more common\n // spelling to reach for; normalize it once here so nothing downstream has to\n // know about two names.\n if (!process.env.TENKI_AUTH_TOKEN && process.env.TENKI_API_TOKEN) {\n process.env.TENKI_AUTH_TOKEN = process.env.TENKI_API_TOKEN;\n }\n}\n\n/**\n * Force a re-read of `~/.agentbox/secrets.env`. Used by the interactive\n * credential flow after it persists the token, so the same process can pick it\n * up without a restart.\n */\nexport function reloadTenkiEnv(): void {\n loaded = false;\n ensureTenkiEnvLoaded();\n}\n\nfunction importTenkiFromFile(path: string, keys: readonly string[]): void {\n if (!existsSync(path)) return;\n let body: string;\n try {\n body = readFileSync(path, 'utf8');\n } catch {\n return;\n }\n const parsed = parseEnvFile(body);\n for (const key of keys) {\n if (process.env[key] !== undefined) continue;\n const value = parsed[key];\n if (typeof value === 'string') {\n process.env[key] = value;\n }\n }\n}\n\n/**\n * Minimal `.env` parser: handles `KEY=value`, `KEY=\"value\"`, `KEY='value'`,\n * `export KEY=value`, blank lines, and `#` comments. No variable interpolation\n * — predictable over feature-complete (matches the daytona / vercel / e2b\n * loaders).\n */\nexport function parseEnvFile(body: string): Record<string, string> {\n const out: Record<string, string> = {};\n for (const rawLine of body.split(/\\r?\\n/)) {\n const line = rawLine.trim();\n if (line.length === 0 || line.startsWith('#')) continue;\n const stripped = line.startsWith('export ') ? line.slice('export '.length) : line;\n const eq = stripped.indexOf('=');\n if (eq <= 0) continue;\n const key = stripped.slice(0, eq).trim();\n let value = stripped.slice(eq + 1).trim();\n if (\n value.length >= 2 &&\n ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\")))\n ) {\n value = value.slice(1, -1);\n }\n out[key] = value;\n }\n return out;\n}\n"],"mappings":";;;AA4BA,qBAA6B;;;ACP7B,qBAAyC;AACzC,qBAAwB;AACxB,uBAAwB;AAExB,IAAM,aAAa;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAI,SAAS;AAEN,SAAS,uBAA6B;AAC3C,MAAI,OAAQ;AACZ,WAAS;AACT,0BAAoB,8BAAQ,wBAAQ,GAAG,aAAa,aAAa,GAAG,UAAU;AAK9E,MAAI,CAAC,QAAQ,IAAI,oBAAoB,QAAQ,IAAI,iBAAiB;AAChE,YAAQ,IAAI,mBAAmB,QAAQ,IAAI;AAAA,EAC7C;AACF;AAYA,SAAS,oBAAoB,MAAc,MAA+B;AACxE,MAAI,KAAC,2BAAW,IAAI,EAAG;AACvB,MAAI;AACJ,MAAI;AACF,eAAO,6BAAa,MAAM,MAAM;AAAA,EAClC,QAAQ;AACN;AAAA,EACF;AACA,QAAM,SAAS,aAAa,IAAI;AAChC,aAAW,OAAO,MAAM;AACtB,QAAI,QAAQ,IAAI,GAAG,MAAM,OAAW;AACpC,UAAM,QAAQ,OAAO,GAAG;AACxB,QAAI,OAAO,UAAU,UAAU;AAC7B,cAAQ,IAAI,GAAG,IAAI;AAAA,IACrB;AAAA,EACF;AACF;AAQO,SAAS,aAAa,MAAsC;AACjE,QAAM,MAA8B,CAAC;AACrC,aAAW,WAAW,KAAK,MAAM,OAAO,GAAG;AACzC,UAAM,OAAO,QAAQ,KAAK;AAC1B,QAAI,KAAK,WAAW,KAAK,KAAK,WAAW,GAAG,EAAG;AAC/C,UAAM,WAAW,KAAK,WAAW,SAAS,IAAI,KAAK,MAAM,UAAU,MAAM,IAAI;AAC7E,UAAM,KAAK,SAAS,QAAQ,GAAG;AAC/B,QAAI,MAAM,EAAG;AACb,UAAM,MAAM,SAAS,MAAM,GAAG,EAAE,EAAE,KAAK;AACvC,QAAI,QAAQ,SAAS,MAAM,KAAK,CAAC,EAAE,KAAK;AACxC,QACE,MAAM,UAAU,MACd,MAAM,WAAW,GAAG,KAAK,MAAM,SAAS,GAAG,KAC1C,MAAM,WAAW,GAAG,KAAK,MAAM,SAAS,GAAG,IAC9C;AACA,cAAQ,MAAM,MAAM,GAAG,EAAE;AAAA,IAC3B;AACA,QAAI,GAAG,IAAI;AAAA,EACb;AACA,SAAO;AACT;;;ADjEA,SAAS,UAAU,MAA4B;AAC7C,MAAI;AACJ,MAAI;AACJ,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,IAAI,KAAK,CAAC;AAChB,QAAI,MAAM,gBAAgB;AACxB,kBAAY,KAAK,IAAI,CAAC;AACtB;AAAA,IACF,WAAW,MAAM,WAAW;AAC1B,aAAO;AAAA,IACT,WAAW,MAAM,cAAc;AAC7B,aAAO;AAAA,IACT;AAAA,EACF;AACA,MAAI,CAAC,WAAW;AACd,YAAQ,OAAO,MAAM,2CAA2C;AAChE,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,MAAI,CAAC,MAAM;AACT,YAAQ,OAAO,MAAM,2DAA2D;AAChF,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,SAAO,EAAE,WAAW,KAAK;AAC3B;AAEA,SAAS,cAA4B;AACnC,QAAM,YAAY,QAAQ,IAAI;AAC9B,MAAI,CAAC,WAAW;AACd,YAAQ,OAAO,MAAM,mDAAmD;AACxE,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,QAAM,OAAsD,EAAE,UAAU;AACxE,MAAI,QAAQ,IAAI,eAAgB,MAAK,UAAU,QAAQ,IAAI;AAC3D,MAAI,QAAQ,IAAI,sBAAuB,MAAK,iBAAiB,QAAQ,IAAI;AACzE,SAAO,IAAI,4BAAa,IAAI;AAC9B;AAEA,eAAe,OAAsB;AACnC,QAAM,EAAE,WAAW,KAAK,IAAI,UAAU,QAAQ,KAAK,MAAM,CAAC,CAAC;AAC3D,uBAAqB;AAErB,QAAM,SAAS,YAAY;AAC3B,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,OAAO,IAAI,SAAS;AAAA,EACtC,SAAS,KAAK;AACZ,YAAQ,OAAO;AAAA,MACb,4CAA4C,SAAS,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA;AAAA,IAC5G;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AAIA,MAAI,QAAQ,UAAU,WAAW;AAC/B,QAAI;AACF,YAAM,QAAQ,OAAO;AACrB,YAAM,QAAQ,UAAU,GAAM;AAAA,IAChC,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,SAAS,YAAY;AACvB,UAAM,QAAQ,QAAQ,IAAI;AAC1B,QAAI,CAAC,OAAO;AACV,cAAQ,OAAO,MAAM,2DAA2D;AAChF,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,QAAI;AACF,YAAM,IAAI,MAAM,QAAQ,IAAI,CAAC,QAAQ,MAAM,KAAK,GAAG,EAAE,KAAK,aAAa,CAAC;AACxE,cAAQ,KAAK,EAAE,YAAY,CAAC;AAAA,IAC9B,SAAS,KAAK;AACZ,cAAQ,OAAO;AAAA,QACb,6CAA6C,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA;AAAA,MAC/F;AACA,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF;AAIA,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,QAAQ,IAAI;AAAA,EAC3B,SAAS,KAAK;AACZ,YAAQ,OAAO;AAAA,MACb,oDAAoD,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA;AAAA,IACtG;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,UAAQ,MAAM,GAAG,QAAQ,CAAC,UAAkB;AAC1C,SAAK,MAAM,IAAI,WAAW,MAAM,QAAQ,MAAM,YAAY,MAAM,UAAU,CAAC,EAAE,MAAM,MAAM;AAAA,IAEzF,CAAC;AAAA,EACH,CAAC;AAED,UAAQ,MAAM,GAAG,OAAO,MAAM;AAC5B,QAAI;AACF,WAAK,MAAM;AAAA,IACb,QAAQ;AAAA,IAER;AAAA,EACF,CAAC;AAED,MAAI,WAAW;AACf,MAAI;AACF,eAAS;AACP,YAAM,OAAO,MAAM,KAAK,KAAK;AAC7B,UAAI,SAAS,KAAM;AACnB,cAAQ,OAAO,MAAM,IAAI;AAAA,IAC3B;AAAA,EACF,SAAS,KAAK;AACZ,YAAQ,OAAO;AAAA,MACb,6CAA6C,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA;AAAA,IAC/F;AACA,eAAW;AAAA,EACb,UAAE;AACA,QAAI;AACF,WAAK,MAAM;AAAA,IACb,QAAQ;AAAA,IAER;AAAA,EACF;AACA,UAAQ,KAAK,QAAQ;AACvB;AAEA,KAAK,EAAE,MAAM,CAAC,QAAQ;AACpB,UAAQ,OAAO;AAAA,IACb,mCAAmC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA;AAAA,EACrF;AACA,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}