Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Give an agent a sandboxed place to run the code it writes.
| --- | --- |
| [MCP server](examples/mcp-tenki-sandbox/) | Sandbox tools for Claude, Cursor, or any MCP client |
| [Claude Code](examples/claude-code-sandbox/) | Headless coding agent on a real repo checkout |
| [Codex](examples/codex-sandbox/) | OpenAI's coding agent, headless, on a real repo checkout |
| [Composio](examples/composio-tenki/) | Tenki tools in a Composio agent |
| [Covalent](examples/covalent-tenki/) | Each workflow task in its own microVM |
| [AgentBox](examples/agentbox-tenki/) | Coding-agent boxes as Firecracker microVMs |
Expand Down
68 changes: 68 additions & 0 deletions examples/codex-sandbox/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Codex on Tenki (headless, on a real repo)

Run OpenAI Codex non-interactively against a real Git checkout inside a disposable [Tenki Sandbox](https://tenki.cloud/products/sandbox) microVM, then read back the diff the agent produced and throw the machine away.

## The code (`run.mjs`)

```js
import { TenkiSandbox, stdoutText } from "@tenkicloud/sandbox";

const REPO = "https://github.com/sindresorhus/yocto-queue";
const TASK = "Add a toArray() method to the Queue class in index.js that returns the queued values as an array, oldest first. Declare it in index.d.ts and add a test for it in test.js.";

const tenki = new TenkiSandbox({ authToken: process.env.TENKI_AUTH_TOKEN });

// cloneRepoUrl checks the repo out to ./repo before createAndWait resolves.
await using sandbox = await tenki.createAndWait({
cpuCores: 2,
memoryMb: 4096,
cloneRepoUrl: REPO,
workspaceId: process.env.TENKI_WORKSPACE_ID,
});

// Outbound is on by default, so no allowOutbound is needed to reach the npm registry.
await sandbox.exec("npm", { args: ["i", "-g", "@openai/codex"] });

const decoder = new TextDecoder();
await sandbox.exec("codex", {
// Codex sandboxes itself with bubblewrap. Inside a microVM that layer is redundant, and
// bypassing it drops both the nesting and the "could not find bubblewrap" warning.
args: ["exec", "--dangerously-bypass-approvals-and-sandbox", TASK],
cwd: "repo", // relative paths resolve under the workdir, /home/tenki
timeoutMs: 10 * 60_000,
env: { OPENAI_API_KEY: process.env.OPENAI_API_KEY }, // scoped to this one process
onOutput: ({ data }) => process.stdout.write(decoder.decode(data)), // data is a Uint8Array
});

// sandbox.git.* runs at the workdir, and the checkout is one level down — so use `git -C`.
console.log(stdoutText(await sandbox.exec("git", { args: ["-C", "repo", "diff"] })));
```

The last line is the point: a unified diff against a real upstream checkout, produced by an agent that had free run of a machine you are about to delete.

## Run it

```bash
npm install
export TENKI_AUTH_TOKEN=... # from `tenki login` (~/.config/tenki/config.yaml)
export TENKI_WORKSPACE_ID=...
export OPENAI_API_KEY=sk-... # the agent turn
node run.mjs # streams the agent's turn, then prints the diff
```

`run.mjs` uses top-level `await using`, which needs Node 24+.

Verify the Tenki half without a model key — this is what CI runs:

```bash
node verify.mjs # create + clone → install the CLI → check its version and flags → edit → git diff
```

## Notes

- **Codex brings its own sandbox, and nesting it inside a microVM buys you nothing.** Under the default `--sandbox workspace-write` it looks for bubblewrap, warns `could not find bubblewrap on PATH`, and falls back to a bundled copy. `--dangerously-bypass-approvals-and-sandbox` reports `sandbox: danger-full-access` and skips the layer — the right call *here* precisely because the blast radius is one disposable VM.
- **`sandbox.git.*` runs at the sandbox workdir (`/home/tenki`), but `cloneRepoUrl` checks out one level down into `./repo`** — so `sandbox.git.diff()` fails with "not a git repository". Read the checkout with `sandbox.exec("git", { args: ["-C", "repo", "diff"] })`, or clone at the workdir root if you want the helpers.
- `codex exec` runs fine outside a Git repo, so `--skip-git-repo-check` is not needed here; the checkout just gives it something to diff. Pass `-C, --cd <DIR>` if you would rather point Codex at a directory than set `cwd`.
- The default image already ships Node 24, npm 11, and git 2.43, and outbound network is on for a bare `create`. `npm i -g @openai/codex` finishes in about seven seconds — no `allowOutbound`, no custom image, no baked-in CLI.
- `exec(command, { args })` runs a bare binary and its args with no shell splitting, so the whole task prompt goes through as a single argument. `ExecOptions.env` is scoped to that one process — the model key never lands in the repo or the image. A key whose value is `undefined` arrives as an *empty string* rather than unset, so filter before adding more vars.
- The same shape with Anthropic's CLI is in [claude-code-sandbox](../claude-code-sandbox/); for the OpenAI Agents SDK as a code-interpreter backend, see [openai-agents-sdk](../openai-agents-sdk/).
9 changes: 9 additions & 0 deletions examples/codex-sandbox/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "codex-sandbox",
"private": true,
"type": "module",
"description": "Cookbook example — run OpenAI Codex headless against a real repo in a disposable Tenki sandbox.",
"dependencies": {
"@tenkicloud/sandbox": "^0.4.0"
}
}
33 changes: 33 additions & 0 deletions examples/codex-sandbox/run.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// OpenAI Codex, headless, on a real repo in a disposable Tenki sandbox:
// clone -> install the CLI -> let the agent edit the checkout -> read back the diff.
import { TenkiSandbox, stdoutText } from "@tenkicloud/sandbox";

const REPO = "https://github.com/sindresorhus/yocto-queue";
const TASK = "Add a toArray() method to the Queue class in index.js that returns the queued values as an array, oldest first. Declare it in index.d.ts and add a test for it in test.js.";

const tenki = new TenkiSandbox({ authToken: process.env.TENKI_AUTH_TOKEN });

// cloneRepoUrl checks the repo out to ./repo before createAndWait resolves.
await using sandbox = await tenki.createAndWait({
cpuCores: 2,
memoryMb: 4096,
cloneRepoUrl: REPO,
workspaceId: process.env.TENKI_WORKSPACE_ID,
});

// Outbound is on by default, so no allowOutbound is needed to reach the npm registry.
await sandbox.exec("npm", { args: ["i", "-g", "@openai/codex"] });

const decoder = new TextDecoder();
await sandbox.exec("codex", {
// Codex sandboxes itself with bubblewrap. Inside a microVM that layer is redundant, and
// bypassing it drops both the nesting and the "could not find bubblewrap" warning.
args: ["exec", "--dangerously-bypass-approvals-and-sandbox", TASK],
cwd: "repo", // relative paths resolve under the workdir, /home/tenki
timeoutMs: 10 * 60_000,
env: { OPENAI_API_KEY: process.env.OPENAI_API_KEY }, // scoped to this one process
onOutput: ({ data }) => process.stdout.write(decoder.decode(data)), // data is a Uint8Array
});

// sandbox.git.* runs at the workdir, and the checkout is one level down — so use `git -C`.
console.log(stdoutText(await sandbox.exec("git", { args: ["-C", "repo", "diff"] })));
67 changes: 67 additions & 0 deletions examples/codex-sandbox/verify.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Proves the Tenki-facing half of this example without a model key: boot a sandbox with
* the repo cloned, install the Codex CLI, check its version and run.mjs's flags, edit a
* file in the checkout, assert the diff round-trips. (The agent turn needs a model key;
* CONTRIBUTING says CI verifies the backend, not the model.)
* Token/workspace from env (CI) or ~/.config/tenki/config.yaml (local `tenki login`).
*/
import { TenkiSandbox, stdoutText } from "@tenkicloud/sandbox";
import { readFileSync } from "node:fs";
import { homedir } from "node:os";

const cfg = (key) => {
try {
const c = readFileSync(`${homedir()}/.config/tenki/config.yaml`, "utf8");
return (c.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1] ?? "").trim();
} catch {
return "";
}
};

const authToken = process.env.TENKI_AUTH_TOKEN || process.env.TENKI_API_KEY || cfg("auth_token");
const workspaceId = process.env.TENKI_WORKSPACE_ID || cfg("current_workspace_id");
if (!authToken) {
console.error("No token. Set TENKI_AUTH_TOKEN, or run `tenki login`.");
process.exit(1);
}

const tenki = new TenkiSandbox({ authToken });
let sandbox;
try {
const cloneRepoUrl = "https://github.com/sindresorhus/yocto-queue";
sandbox = await tenki.createAndWait({ cpuCores: 2, memoryMb: 4096, cloneRepoUrl, workspaceId });

const pkg = JSON.parse(stdoutText(await sandbox.exec("cat", { args: ["repo/package.json"] })));
if (pkg.name !== "yocto-queue") throw new Error(`clone landed wrong: repo/package.json is ${pkg.name}`);

const install = await sandbox.exec("npm", { args: ["i", "-g", "@openai/codex"], timeoutMs: 300_000 });
if (install.exitCode !== 0) throw new Error(`npm i -g @openai/codex exited ${install.exitCode}`);

const version = stdoutText(await sandbox.exec("codex", { args: ["--version"] })).trim();
if (!/^codex-cli \d+\.\d+\.\d+$/.test(version)) throw new Error(`codex --version said ${JSON.stringify(version)}`);

// npm always installs the latest CLI, so check run.mjs's flags still exist in it.
const help = stdoutText(await sandbox.exec("codex", { args: ["exec", "--help"] }));
const missing = ["--dangerously-bypass-approvals-and-sandbox", "-C, --cd"].filter((f) => !help.includes(f));
if (missing.length) throw new Error(`codex exec --help no longer lists ${missing.join(", ")}`);

// Stand in for the agent's edit, then read it back the way run.mjs reads the agent's.
await sandbox.exec("sh", { args: ["-c", "printf '\\nexport const verified = true;\\n' >> repo/index.js"] });
const diff = stdoutText(await sandbox.exec("git", { args: ["-C", "repo", "diff"] }));
if (!diff.includes("--- a/index.js") || !diff.includes("+export const verified = true;")) {
throw new Error(`diff did not round-trip: ${JSON.stringify(diff.slice(0, 200))}`);
}

console.log(`✓ codex-sandbox: create + clone → npm i -g @openai/codex → ${version} → edit → git diff → dispose`);
} catch (e) {
console.error("✗ " + (e?.message ?? e));
process.exitCode = 1;
} finally {
if (sandbox) {
try {
await sandbox[Symbol.asyncDispose]();
} catch {
/* self-reaps via idle/lifetime caps */
}
}
}
Loading