Codex
Image updates
Ready-made agent images are updated every morning to include the current CLI version. To use a fixed image version, pin a template version.
Codex is an OpenAI agent that works with files, runs commands, and uses tools to complete tasks. AgentBox runs it in a separate sandbox: your application supplies instructions and data, receives events, and retrieves the result. The ready-made codex template already includes the agent CLI.
Connect
Install the AgentBox SDK and set AGENTBOX_API_KEY in your application's environment. Codex needs a separate OPENAI_API_KEY with access to the example model, gpt-5.6-luna. When creating the sandbox, the example maps this key to CODEX_API_KEY, which codex exec reads. You can select another model available to your OpenAI project.
The examples use the AgentBox proxy at socks5h://sandbox-proxy.agentbox.ru:65180 through ALL_PROXY inside the sandbox. This is a process-wide setting, rather than a proxy restricted to model requests. Do not put the OpenAI key in a template or input file.
Prepare a sales report
This example uploads three orders and asks the agent to write report.md. Paid revenue is RUB 120,000 online plus RUB 80,000 in retail. RUB 45,000 in cancelled orders must be reported separately. Replace these rows with a CRM export while keeping the column names and calculation rules.
import { Sandbox } from "@abox-dev/sdk";
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) throw new Error("OPENAI_API_KEY is required");
const proxy =
process.env.SANDBOX_PROXY_URL ?? "socks5h://sandbox-proxy.agentbox.ru:65180";
const sandbox = await Sandbox.create("codex", {
timeoutMs: 600_000,
envs: {
CODEX_API_KEY: apiKey,
ALL_PROXY: proxy,
NO_PROXY: "localhost,127.0.0.1",
},
});
try {
await sandbox.files.makeDir("/home/user/work");
await sandbox.files.write(
"/home/user/work/sales.csv",
"channel,status,amount_rub\nOnline,paid,120000\nRetail,paid,80000\nOnline,cancelled,45000\n",
);
await sandbox.commands.run(
"codex exec --ephemeral --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --model gpt-5.6-luna 'Read sales.csv. Calculate paid revenue by channel and total; exclude cancelled orders. Write report.md: a short sales briefing with a table, the cancelled amount separately, and one next action.'",
{
cwd: "/home/user/work",
timeoutMs: 300_000,
onStderr: (chunk) => process.stderr.write(chunk),
},
);
console.log(
"report.md\n" + (await sandbox.files.read("/home/user/work/report.md")),
);
} finally {
await sandbox.kill();
}codex exec handles a task without a terminal interface. --skip-git-repo-check allows processing exports outside a Git repository. --ephemeral suits a one-off report because it does not persist conversation history. Read the output through the SDK before deleting the sandbox.
The example allows commands without approval (--dangerously-bypass-approvals-and-sandbox). The AgentBox sandbox provides the isolation boundary. The agent can change sandbox files and access the network. Supply only the data and credentials needed for this task.
Return structured data to your application
Use JSON Schema when the result feeds a report card or another process step. This example returns currency, paid revenue, cancelled amount, and a suggested action. --output-last-message saves the final answer separately from CLI progress output.
import { Sandbox } from "@abox-dev/sdk";
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) throw new Error("OPENAI_API_KEY is required");
const proxy =
process.env.SANDBOX_PROXY_URL ?? "socks5h://sandbox-proxy.agentbox.ru:65180";
const sandbox = await Sandbox.create("codex", {
timeoutMs: 600_000,
envs: {
CODEX_API_KEY: apiKey,
ALL_PROXY: proxy,
NO_PROXY: "localhost,127.0.0.1",
},
});
try {
await sandbox.files.makeDir("/home/user/work");
await sandbox.files.write(
"/home/user/work/sales.csv",
"channel,status,amount_rub\nOnline,paid,120000\nRetail,paid,80000\nOnline,cancelled,45000\n",
);
await sandbox.files.write(
"/home/user/work/schema.json",
'{"type": "object", "properties": {"currency": {"type": "string", "enum": ["RUB"]}, "paid_revenue": {"type": "integer"}, "cancelled_amount": {"type": "integer"}, "recommendation": {"type": "string"}}, "required": ["currency", "paid_revenue", "cancelled_amount", "recommendation"], "additionalProperties": false}',
);
await sandbox.commands.run(
"codex exec --ephemeral --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --model gpt-5.6-luna --output-schema schema.json --output-last-message summary.json 'Read sales.csv. Calculate paid revenue and cancelled amount separately. Return a sales summary matching the schema, with one concise recommendation.'",
{ cwd: "/home/user/work", timeoutMs: 300_000 },
);
const summary = JSON.parse(
await sandbox.files.read("/home/user/work/summary.json"),
);
console.log(JSON.stringify(summary));
} finally {
await sandbox.kill();
}These inputs should produce paid_revenue: 200000 and cancelled_amount: 45000. The JSON schema defines the result's fields and data types.
Stream events and ask a follow-up
Show progress while the report is being prepared. --json emits JSONL events. The handler below buffers unfinished lines between callbacks: a network chunk may contain part of an event or several events at once.
| Event | Application action |
|---|---|
thread.started | Save thread_id for follow-up tasks |
item.started with command_execution | Indicate that a command is running |
item.completed with agent_message | Show the completed message |
turn.completed | Mark the task complete and store token usage |
turn.failed or error | Show the failure. Do not present a partial report as finished |
The example prepares a report, then asks for two next actions for the sales manager in the same sandbox and conversation. It uses codex exec resume with the saved ID, without --ephemeral.
import { Sandbox } from "@abox-dev/sdk";
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) throw new Error("OPENAI_API_KEY is required");
const proxy =
process.env.SANDBOX_PROXY_URL ?? "socks5h://sandbox-proxy.agentbox.ru:65180";
const sandbox = await Sandbox.create("codex", {
timeoutMs: 600_000,
envs: {
CODEX_API_KEY: apiKey,
ALL_PROXY: proxy,
NO_PROXY: "localhost,127.0.0.1",
},
});
try {
await sandbox.files.makeDir("/home/user/work");
await sandbox.files.write(
"/home/user/work/sales.csv",
"channel,status,amount_rub\nOnline,paid,120000\nRetail,paid,80000\nOnline,cancelled,45000\n",
);
let pending = "";
let threadId;
let failure;
let completed = false;
function event(message) {
if (message.type === "thread.started") threadId = message.thread_id;
if (
message.type === "item.completed" &&
message.item.type === "agent_message"
) {
console.log(message.item.text);
}
if (
message.type === "item.started" &&
message.item.type === "command_execution"
) {
console.log("Command:", message.item.command);
}
if (message.type === "turn.completed") {
completed = true;
console.log("Usage:", message.usage);
}
if (message.type === "turn.failed" || message.type === "error") {
failure = new Error(
message.error?.message ?? message.message ?? "Codex failed",
);
}
}
function onStdout(chunk) {
pending += chunk;
let newline;
while ((newline = pending.indexOf("\n")) !== -1) {
const line = pending.slice(0, newline);
pending = pending.slice(newline + 1);
if (line.trim()) event(JSON.parse(line));
}
}
async function execute(command) {
completed = false;
await sandbox.commands.run(command, {
cwd: "/home/user/work",
timeoutMs: 300_000,
onStdout,
onStderr: (chunk) => process.stderr.write(chunk),
});
if (pending.trim()) event(JSON.parse(pending));
pending = "";
if (failure) throw failure;
if (!completed) throw new Error("Codex ended without turn.completed");
}
await execute(
"codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --model gpt-5.6-luna --json 'Read sales.csv. Write report.md with paid revenue by channel, total, and cancelled amount separately.'",
);
if (!threadId) throw new Error("Codex did not return a thread ID");
await execute(
"codex exec resume --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --json " +
"'" +
threadId.replaceAll("'", "'\"'\"'") +
"' " +
"'Add two concrete next actions for the sales manager to report.md. Keep the original figures.'",
);
console.log(
"report.md\n" + (await sandbox.files.read("/home/user/work/report.md")),
);
} finally {
await sandbox.kill();
}Check command completion independently of events. Output on stdout or a successful process start does not establish success: wait for turn.completed, handle nonzero exit codes, then return the file.
Build a conversation with App Server
codex app-server accepts application commands on stdin and sends responses and events on stdout. This example starts the server in a ready-made sandbox, uploads a sales CSV, and displays the response as it is generated.
import { Readable } from "node:stream";
import { Sandbox } from "@abox-dev/sdk";
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) throw new Error("OPENAI_API_KEY is required");
const sandbox = await Sandbox.create("codex", {
timeoutMs: 600_000,
envs: {
ALL_PROXY:
process.env.SANDBOX_PROXY_URL ??
"socks5h://sandbox-proxy.agentbox.ru:65180",
NO_PROXY: "localhost,127.0.0.1",
},
});
try {
await sandbox.files.write(
"/home/user/sales.csv",
"channel,paid_revenue_rub\nOnline,120000\nRetail,80000\n",
);
const messages = new Readable({ objectMode: true, read() {} });
let buffer = "";
const server = await sandbox.commands.run("codex app-server", {
background: true,
stdin: true,
timeoutMs: 300_000,
onStdout(chunk) {
buffer += chunk;
let end;
while ((end = buffer.indexOf("\n")) !== -1) {
const line = buffer.slice(0, end);
buffer = buffer.slice(end + 1);
try {
if (line.trim()) messages.push(JSON.parse(line));
} catch (error) {
messages.destroy(error);
}
}
},
onStderr: (chunk) => process.stderr.write(chunk),
});
server.wait().then(
() => messages.push(null),
(error) => messages.destroy(error),
);
const incoming = messages[Symbol.asyncIterator]();
let status;
async function receive() {
const { value: message, done } = await incoming.next();
if (done) throw new Error("App Server closed before completing the task");
if (message.error) throw new Error(message.error.message);
if (message.method === "item/agentMessage/delta")
process.stdout.write(message.params.delta);
if (message.method === "turn/completed") {
status = message.params.turn.status;
if (status !== "completed")
throw new Error(message.params.turn.error?.message ?? status);
}
return message;
}
let nextId = 0;
async function call(method, params) {
const id = ++nextId;
await server.sendStdin(JSON.stringify({ id, method, params }) + "\n");
while (true) {
const message = await receive();
if (message.id === id) return message.result;
}
}
await call("initialize", {
clientInfo: { name: "sales-app", version: "1.0.0" },
});
await server.sendStdin(JSON.stringify({ method: "initialized" }) + "\n");
await call("account/login/start", { type: "apiKey", apiKey });
const { thread } = await call("thread/start", {
model: "gpt-5.6-luna",
cwd: "/home/user",
approvalPolicy: "never",
sandbox: "danger-full-access",
});
await call("turn/start", {
threadId: thread.id,
input: [
{
type: "text",
text: "Read sales.csv. Give a short sales briefing with revenue by channel and the total in RUB.",
},
],
});
while (!status) await receive();
console.log("\nStatus:", status);
} finally {
await sandbox.kill();
}call matches responses by id while continuing to process notifications. The handler assembles complete JSON lines from stdout chunks, displays item/agentMessage/delta, and checks the status in turn/completed. account/login/start sends the API key through stdin rather than command arguments.
For another question, send a new turn/start with the same thread.id while keeping the process and sandbox alive. This example stops after the first response and deletes the sandbox. See the App Server protocol for other methods.
Timeouts and preserving work
Sandbox lifetime and command timeout are separate limits. These examples allow a ten-minute sandbox lifetime and five minutes for one task. If a command times out, stop it and decide which intermediate files to keep. The finally block deletes the sandbox even on failure.
To continue later, keep the sandbox ID and thread_id, then pause the sandbox. Deleting it also removes the report and Codex history unless your application has saved them elsewhere.