Boot a disposable Tenki Sandbox microVM with OpenCode and a Git checkout already inside it, start the agent's headless server, and expose it on a public HTTPS URL — a coding agent, and its web UI, running on a machine you can throw away. No install step: enableOpenCode bakes the CLI into the guest.
import { TenkiSandbox } from "@tenkicloud/sandbox";
import { randomBytes } from "node:crypto";
const REPO = "https://github.com/sindresorhus/yocto-queue";
const PORT = 4096;
const PASSWORD = randomBytes(24).toString("hex"); // this URL is public; see the note below
const tenki = new TenkiSandbox({ authToken: process.env.TENKI_AUTH_TOKEN });
// enableOpenCode bakes the CLI into the guest — nothing to npm install.
// openCodeProvider.apiKey lands in the guest as OPENCODE_API_KEY.
// No `await using` here: it would terminate the sandbox at the end of this scope and the
// URL below would 404 before you could open it. idleTimeoutMinutes caps it instead.
const sandbox = await tenki.createAndWait({
cpuCores: 2,
memoryMb: 4096,
enableOpenCode: true,
openCodeProvider: { apiKey: process.env.OPENAI_API_KEY },
allowInbound: true,
cloneRepoUrl: REPO,
idleTimeoutMinutes: 30,
workspaceId: process.env.TENKI_WORKSPACE_ID,
});
// --hostname 0.0.0.0 is load-bearing: the default 127.0.0.1 is unreachable from the gateway.
// setsid + redirected stdio keeps the server running after this exec() returns.
await sandbox.exec("sh", {
args: ["-c", `cd repo && setsid opencode serve --port ${PORT} --hostname 0.0.0.0 >/tmp/opencode.log 2>&1 </dev/null & sleep 5`],
env: { OPENCODE_SERVER_PASSWORD: PASSWORD },
});
const { previewUrl } = await sandbox.exposePort(PORT);
// Auth is HTTP Basic — any username, the password above. A Bearer token is rejected.
const auth = { Authorization: "Basic " + Buffer.from(`opencode:${PASSWORD}`).toString("base64") };
const project = await (await fetch(`${previewUrl}/project/current`, { headers: auth })).json();
console.log(`${project.worktree} (${project.vcs}) is live at ${previewUrl}/app`);
console.log(`user: anything, password: ${PASSWORD}
Ctrl-C to terminate the sandbox.`);
process.on("SIGINT", async () => {
await sandbox[Symbol.asyncDispose]();
process.exit(0);
});
setInterval(() => {}, 1 << 30); // hold the event loop open; an unsettled await would exit 13The script stays in the foreground so the URL keeps working. Open the printed /app URL in a browser and you are looking at OpenCode's own UI, driving a repo inside the microVM; the same URL serves the JSON API — /session, /agent, /project/current — so a script can drive it just as easily. Ctrl-C terminates the sandbox and the URL with it.
npm install
export TENKI_AUTH_TOKEN=... # from `tenki login` (~/.config/tenki/config.yaml)
export TENKI_WORKSPACE_ID=...
export OPENAI_API_KEY=sk-... # or any provider OpenCode supports, via openCodeProvider
node run.mjs # -> /home/tenki/repo (git) is live at https://....sb.tenki.sh/app
# stays up until you Ctrl-CVerify the Tenki half without a model key — this is what CI runs:
node verify.mjs # boot → serve → exposePort → assert 401 unauthed, then 200 + the checkout- This example deliberately does not use
await using. It would terminate the sandbox at the end of the script's scope, and the URL it just printed would404before you could open it — the whole point here is a machine that outlives the script. So it disposes onSIGINTinstead, and setsidleTimeoutMinutes: 30so a forgotten sandbox still reaps itself. Holding the process open needs a ref'd handle (setInterval); an unsettled top-levelawaitmakes Node exit 13 withDetected unsettled top-level await. - Set
OPENCODE_SERVER_PASSWORD, always. Without it the server logsOPENCODE_SERVER_PASSWORD is not set; server is unsecuredand answers every caller — andexposePorthas just put it on the public internet, so that is an open coding agent with a shell. With it set, unauthenticated requests get401. Auth is HTTP Basic (any username, that password); aBearertoken is rejected.verify.mjsasserts the401precisely so this cannot regress unnoticed. - Port 7681 is spoken for. Tenki's own
ttydconsole listens there, andexposePort(7681)fails with[invalid_argument] port 7681 cannot be exposed as a preview. Pick any other port for your server. --hostname 0.0.0.0is required.opencode servedefaults to127.0.0.1, which the gateway cannot reach, so the preview URL would just hang.enableOpenCode: trueputsopencode(1.17.20 at the time of writing) at/usr/local/bin/opencodebefore the sandbox reports ready — nonpm install, no custom image.openCodeProvider.apiKeyand.baseUrlarrive in the guest asOPENCODE_API_KEYandOPENCODE_PROVIDER_BASE_URL.cloneRepoUrlchecks out to./repo, so the server is started withcd repo— that is what makes/project/currentreportworktree: /home/tenki/repowithvcs: gitinstead of an empty directory.- For the same agent driven as a one-shot CLI instead of a server,
opencode run "<task>"works too — the shape used by claude-code-sandbox and codex-sandbox.