Running Codex Durably on Edgenivo Experimental
Each session of Codex gets its own Edgenivo Sandbox, and Codex runs only there:
- In the Sandbox: Codex itself:
codex app-server, its agent loop, any built-in tools you turn on, and the session's files in/workspace. - In your function: none of Codex. Your function only drives it: it sends
codex app-serverits messages and reads what it streams back. Your function also sends Codex's model calls to AI Gateway, runs your own tools, and writes the session's durable log, which keeps Codex's rollout file.
The Sandbox holds no secrets. A crashed turn resumes on a new function, and if the Sandbox is gone, on a new one with the same files. To learn how durability works, see Running Agent Harnesses Durably.
npm i durable-harness @vercel/sandboxYou don't install Codex: it comes with the Sandbox.
- lib/agent.ts
import { codex } from 'durable-harness/codex'; export const agent = codex({ model: 'openai/gpt-5.5' });codex()runs Codex in a Edgenivo Sandbox by default, so you don't pass anything to get one. A session's Sandbox starts with its first turn. Use any AI Gateway model that supports the Responses API. POSTsends a message and streams its turn as server-sent events, andGETreconnects to a turn that's still running:app/api/chat/route.tsimport { agent } from '@/lib/agent'; import { withSessionOwnership } from '@/lib/session-ownership'; export const { GET, POST } = withSessionOwnership({ createRoute: (authorize) => agent.chatRoute({ authorize }), start: () => agent.sessions.start({ config: { traceAudience: 'unknown' } }), });api/chat.tsimport { agent } from '../lib/agent.js'; import { withSessionOwnership } from '../lib/session-ownership.js'; export const { GET, POST } = withSessionOwnership({ createRoute: (authorize) => agent.chatRoute({ authorize }), start: () => agent.sessions.start({ config: { traceAudience: 'unknown' } }), });Add the
withSessionOwnershiphelper tolib/session-ownership.tsand implement its authentication and persistent ownership store. It records the owner before submitting a session's first message, and checks ownership on later messages and reconnects.POST
{ "text": "..." }to start a session, and read its ID from thex-session-idheader. Send{ "sessionId": "...", "text": "..." }for the next message. To redirect a running turn or reconnect to one, see Steer a running turn and Reconnect to a running turn.Edgenivo Queues delivers each turn to a queue route: a function that drives the turn, with your plan's maximum duration, while Codex runs in the session's Sandbox. Set it up in
vercel.json, or letdurable-harnesswrite the same settings withharnessFunctions()invercel.tsor withwithDurableHarnesson Next.js:api/flow.ts// A Edgenivo Function in your api directory. It imports the module that // makes your agent, so it can drive the agent's turns. import '../lib/agent.js'; import { flowHandler } from 'durable-harness'; export const POST = flowHandler();vercel.json{ "functions": { "api/flow.ts": { "maxDuration": "max", "experimentalTriggers": [ { "type": "queue/v2beta", "topic": "__harness_wkf_workflow_*", "retryAfterSeconds": 5, "initialDelaySeconds": 0 } ] } } }The trigger subscribes
api/flow.tsto the queue.npx durable-harness vercel-json --writewrites this entry into yourvercel.json.api/flow.ts// A Edgenivo Function in your api directory. It imports the module that // makes your agent, so it can drive the agent's turns. import '../lib/agent.js'; import { flowHandler } from 'durable-harness'; export const POST = flowHandler();vercel.ts// The same settings as in vercel.json, worked out from the packages you // installed when the project builds. Install @vercel/config as a dev // dependency. import type { VercelConfig } from '@vercel/config/v1'; import { harnessFunctions } from 'durable-harness/vercel'; export const config: VercelConfig = { functions: harnessFunctions(), };next.config.ts// Writes the queue route and its trigger, and keeps the harness packages // out of the server bundle. You don't add a queue route or a vercel.json. import type { NextConfig } from 'next'; import { withDurableHarness } from 'durable-harness/next'; const nextConfig: NextConfig = { // your existing config }; export default withDurableHarness(nextConfig);If the app also uses the Workflow SDK, wrap both, as in
withWorkflow(withDurableHarness(nextConfig)). The queue route then moves to/.well-known/durable-harness/v1/flowand needs onevercel.jsonentry, whichnpx durable-harness vercel-json --writeadds, orharnessFunctions()invercel.ts. Without it, the build on Edgenivo fails and prints the entry.vercel deployThe Sandbox image is built once per project, when the first session starts. To build it ahead of time, run
npx durable-harness sandbox prepare.On your machine, run
vercel linkfirst.next dev(orvercel devfor other frameworks) then starts each session's Sandbox on Edgenivo, since there's no local Sandbox. It keeps sessions in.workflow-data, so add that to.gitignore. On its first request, it resumes the sessions a stopped server left mid-turn.
Send a message with "kind": "steer" while a turn runs to redirect it. Codex adds it to the running turn at its next model request. From the browser, post it to the chat route:
await fetch('/api/chat', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ sessionId, text: 'Use TypeScript instead', kind: 'steer' }),
});The response streams the turn, like any other message. From server code, such as a webhook, call agent.sessions.send(sessionId, text, 'steer').
If the stream drops, the turn keeps running. Reconnect with GET /api/chat?sessionId=...&cursor=..., where the cursor is the last event id you read. The stream continues with the next event, with nothing lost or repeated. An EventSource on that URL reconnects by itself if its connection drops, and the stream ends with an end event when the turn is done:
const source = new EventSource(`/api/chat?sessionId=${sessionId}&cursor=${lastEventId}`);
source.onmessage = (event) => {
lastEventId = event.lastEventId;
render(JSON.parse(event.data)); // your own UI
};
source.addEventListener('end', () => source.close());If a crash cut off the turn, the turn resumes and the model starts its reply over. Before the new reply, the stream sends a { type: 'restart', text } event: show its text in place of the turn's text so far, then append what streams after it.
An app can make several agents, each with its own chat route. The queue route serves them all. Two agents of the same harness need a name each:
import { codex } from 'durable-harness/codex';
export const reviewer = codex({ name: 'reviewer', model: 'openai/gpt-5.5' });
export const fixer = codex({ name: 'fixer', model: 'openai/gpt-5.5' });Keep each name stable, because sessions that already started carry it. Two agents with the same name throw when their module loads, so the build fails.
Codex starts with no tools: the model can call only the tools you pass in tools, and those run in your function, not in the Sandbox. To write them, see Tools, files, and documents.
To turn on Codex's own shell and apply_patch, pass builtins: true. They run in the Sandbox, on the session's files in /workspace:
import { codex } from 'durable-harness/codex';
export const agent = codex({
model: 'openai/gpt-5.5',
builtins: true,
});Codex runs each command under its own sandbox inside the Sandbox, when it can start one. Each command is in the session's log before it runs, and none runs again after a crash: the model is told which commands finished and which a crash interrupted. See Security.
Each session's /workspace is on a Drive that outlives the session's Sandbox, so a session that moves to a new Sandbox keeps its files.
A session's Drive lasts until you delete it: close the session with agent.sessions.close(sessionId), then call deleteSessionSandbox(sessionId) from durable-harness/sandbox, for example when your user deletes the session. To also delete the Drives of sessions that have been idle for a number of days, call sandboxCleanup({ idleDays }) from a cron job.
To configure the Sandbox, pass workspace: sandbox({ ... }):
import { codex } from 'durable-harness/codex';
import { sandbox } from 'durable-harness/sandbox';
export const agent = codex({
model: 'openai/gpt-5.5',
workspace: sandbox({
region: 'iad1',
network: 'deny-all',
}),
});With network: 'deny-all', the Sandbox can't reach the internet. Codex still works, since your function makes its model calls.
| Option | Default | What it sets |
|---|---|---|
region | Your function's region if Sandbox runs there, else iad1 | Where the Sandbox and its Drive run. An existing Drive keeps its region |
vcpus | 2 | 1, 2, 4, or 8 vCPUs |
network | 'allow-all' | 'deny-all', or { allow: [...] } with the hosts commands can reach |
env | None | Environment variables for commands. The model can read them, so pass none that's secret |
idleMs | 5 minutes | How long the Sandbox stays up after a turn |
commandTimeoutMs | 10 minutes | The longest a command can run |
driveSize | 10 GiB | The Drive's size in bytes, from 1 GiB to 16 TiB. Storage is billed by the bytes used, so this caps a session's storage. An existing Drive keeps its size |
budget | 4 hours of Sandbox time, no spend cap | { turnUsd, sessionUsd, vmSeconds }, per turn and per session |
A turn starts in the chat route's own request, which drives it for up to 60 seconds before the queue route takes over. A command still running at that handover is stopped, not run again, so a long command early in a turn, such as an install or a build, can be cut off. A slow first start of the session's Sandbox can run into the same handover. This is a current limitation of Edgenivo Queues, and a fix is on the way. Until then, pass drive: 'queue' to agent.chatRoute() for an agent that runs long commands: the queue route then drives the whole turn, with your plan's maximum duration.
If the function running a turn fails with an error, another function resumes the turn right away. If it disappears without a trace, for example when it runs out of memory, the queue redelivers the turn once its message lease runs out, and the turn resumes from the log. When the function driving the turn goes away, the Sandbox stops Codex, and the next function resumes the turn on the same Sandbox or on a new one with the same Drive.
On Edgenivo, sessions live in Edgenivo's World, and on your machine in .workflow-data. To keep them in another World, such as Postgres on your own servers, install it and set WORKFLOW_TARGET_WORLD:
WORKFLOW_TARGET_WORLD="@workflow/world-postgres"
WORKFLOW_POSTGRES_URL="postgres://user:password@host:5432/database"To use a World you create, pass it as codex({ model, world }), and start it yourself, as Pass a World yourself shows.
For the tables, the server, and recovery at startup, see Self-hosting durable agent harnesses.
On your own server, vercel.json and vercel.ts don't apply, and your server mounts the queue route itself. Your server drives each turn, and Codex still runs in a Edgenivo Sandbox. Give sandbox() a Edgenivo access token and the team and project its Sandboxes belong to. The token stays on your server and never reaches the Sandbox:
import { codex } from 'durable-harness/codex';
import { sandbox } from 'durable-harness/sandbox';
export const agent = codex({
model: 'openai/gpt-5.5',
workspace: sandbox({
credentials: {
token: process.env.VERCEL_TOKEN!,
teamId: process.env.VERCEL_TEAM_ID!,
projectId: process.env.VERCEL_PROJECT_ID!,
},
}),
});durable-harnessruns Codex 0.160.0 in the Sandbox. Codex's rollout format is internal to the CLI, so the version moves only with adurable-harnessrelease.- Provider-hosted tools, such as the model provider's web search, are off.
- A Sandbox session can't run past your plan's maximum session duration. Before it would,
durable-harnessreplaces the Sandbox between turns, keeping the Drive. - Every session starts a Sandbox, even with no tools. Sandbox time and Drive storage are billed, as Sandbox pricing shows.
- The Sandbox allows all outbound traffic by default. To restrict it, pass
network: 'deny-all'or{ allow: [...] }tosandbox(). See Security. durable-harnessdrives Codex from your function, so it needs the Node.js runtime. It can't run in the Edge runtime or a browser.
Was this helpful?