Codex
Обновление образов
Готовые образы агентов обновляются каждое утро и содержат актуальную версию CLI. Чтобы использовать фиксированную версию образа, закрепите версию шаблона.
Codex — агент OpenAI, который работает с файлами, выполняет команды и использует инструменты для решения задач. В AgentBox он запускается в отдельной песочнице: приложение передаёт задание и данные, получает события и забирает результат. Готовый шаблон codex уже содержит CLI агента.
Подключение
Установите SDK AgentBox и задайте AGENTBOX_API_KEY в окружении приложения. Для Codex нужен отдельный OPENAI_API_KEY с доступом к модели из примера, gpt-5.6-luna. При создании песочницы пример передаёт его в CODEX_API_KEY, которую читает codex exec. Модель можно заменить на доступную вашему проекту OpenAI.
Примеры подключают прокси AgentBox socks5h://sandbox-proxy.agentbox.ru:65180 через ALL_PROXY внутри песочницы. Это общая настройка процесса, а не отдельный прокси только для модели. Не записывайте ключ OpenAI в образ или входные файлы.
Отчёт по продажам
Пример загружает три заказа и просит агента подготовить report.md. В выручку входят только оплаченные заказы: 120 000 ₽ онлайн и 80 000 ₽ в рознице. Отменённые 45 000 ₽ должны быть показаны отдельно. Вместо этих строк можно передать выгрузку из CRM, сохранив названия столбцов и правила расчёта.
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 выполняет одно задание без терминального интерфейса. --skip-git-repo-check позволяет работать с выгрузкой без Git-репозитория. --ephemeral подходит для одноразового отчёта: история диалога не сохраняется. Файл читается через SDK до удаления песочницы.
Здесь агенту разрешено выполнять команды без подтверждений (--dangerously-bypass-approvals-and-sandbox): границей изоляции служит песочница AgentBox. Агент может изменять её файлы и обращаться в сеть. Выдавайте ему только данные и доступы этой задачи.
Структурированный результат для приложения
Если результат должен попасть в карточку отчёта или следующий шаг процесса, задайте JSON Schema. Следующий пример возвращает валюту, оплаченную выручку, сумму отмен и рекомендацию. --output-last-message сохраняет финальный ответ отдельно от служебного вывода CLI.
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();
}Для этих входных данных ожидаются paid_revenue: 200000 и cancelled_amount: 45000. JSON Schema задаёт поля и типы данных результата.
Поток событий и следующий вопрос
Когда отчёт занимает время, сотруднику полезно видеть ход работы. --json выдаёт поток JSONL. Обработчик ниже сохраняет незавершённую строку между вызовами: сетевой фрагмент может содержать часть события или несколько событий.
| Событие | Действие приложения |
|---|---|
thread.started | Сохранить thread_id для продолжения |
item.started с command_execution | Показать, что агент запустил команду |
item.completed с agent_message | Показать готовое сообщение |
turn.completed | Отметить завершение задания и сохранить расход токенов |
turn.failed или error | Показать ошибку. Не выдавать частичный отчёт за готовый |
Пример сначала собирает отчёт, затем в той же песочнице и той же сессии просит добавить два действия для руководителя продаж. Для этого используется codex exec resume с сохранённым ID. --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();
}Завершение команды проверяется отдельно от событий. Не считайте любой текст в stdout или успешный запуск процесса признаком готовности: дождитесь turn.completed, обработайте ненулевой exit code и только затем отдавайте файл.
Диалог в приложении через App Server
codex app-server принимает команды приложения через stdin и отправляет ответы и события через stdout. Пример запускает сервер в готовой песочнице, загружает CSV продаж и выводит ответ по мере генерации.
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 связывает ответ с запросом по id, продолжая принимать уведомления. Обработчик собирает целые JSON-строки из фрагментов stdout, выводит item/agentMessage/delta и проверяет статус в turn/completed. account/login/start передаёт API-ключ через stdin, а не в аргументах команды.
Для следующего вопроса отправьте ещё один turn/start с тем же thread.id, сохранив процесс и песочницу. Пример заканчивается после первого ответа и удаляет песочницу. Остальные методы описаны в протоколе App Server.
Таймауты и сохранение работы
Время жизни песочницы и таймаут команды — разные ограничения. В примерах песочница живёт до десяти минут, а одно задание ограничено пятью минутами. Если команда завершилась по таймауту, остановите её и решите, какие промежуточные файлы сохранить. Блок finally удаляет песочницу и при ошибке.
Для продолжения позже сохраняйте ID песочницы и thread_id, а песочницу ставьте на паузу. После удаления песочницы пропадут и отчёт, и история Codex, если приложение не сохранило их отдельно.