Skip to content

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.

mjs
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.

mjs
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.

EventApplication action
thread.startedSave thread_id for follow-up tasks
item.started with command_executionIndicate that a command is running
item.completed with agent_messageShow the completed message
turn.completedMark the task complete and store token usage
turn.failed or errorShow 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.

mjs
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.

mjs
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.