API mode
CODEXBOX_API_MODE=1 runs codexbox as a long-lived FastAPI server on :8080 (change it with CODEXBOX_API_MODE_PORT). It serves agent runs, workspace file operations and an OpenAI-compatible chat endpoint.
Required:
CODEXBOX_AVAILABLE_MODELS=<csv>, for examplegpt-5.1-codex,gpt-5.1-codex-mini. API mode refuses to start without it./openai/v1/modelsneeds a real list, and Codex has no fixed model name to fall back to. OpenAI’s servers decide the list, and it changes without notice.
Setup
# docker-compose.yml
services:
codexbox:
image: psyb0t/codexbox:latest
ports:
- "127.0.0.1:8080:8080"
environment:
- CODEXBOX_API_MODE=1
- CODEXBOX_API_MODE_TOKEN=your-secret
- CODEXBOX_AVAILABLE_MODELS=gpt-5.1-codex,gpt-5.1-codex-mini
- OPENAI_API_KEY=sk-...
volumes:
- ~/.codex:/home/aicode/.codex
- ~/workspaces:/workspace
Mount ~/.codex to use a subscription login instead of OPENAI_API_KEY.
For a local server in the current directory, the wrapper does the mounts for you. Forward the container settings with CODEXBOX_ENV_:
CODEXBOX_ENV_CODEXBOX_API_MODE=1 \
CODEXBOX_ENV_CODEXBOX_AVAILABLE_MODELS=your-model-id \
CODEXBOX_ENV_CODEXBOX_API_MODE_TOKEN=your-api-token \
codexbox
The wrapper uses host networking, so the server listens on port 8080 of the host.
Endpoints
| Method | Path | What it does |
|---|---|---|
GET | /healthz | Liveness, with no token required |
GET | /status | Runs in flight and busy workspaces |
POST | /run | Synchronous agent run, returning {runId, workspace, exitCode, text, ...}. With "async": true it returns a runId at once. |
GET | /run/result?runId=<id> | Poll an async run |
DELETE | /run/{run_id} | Kill a run in flight |
GET | /files | List the workspace root ({entries: [{name, type, size?}, ...]}) |
GET | /files/{path} | List a subdirectory, or stream a file’s bytes |
PUT | /files/{path} | Upload. The raw request body becomes the file; parent directories are created. |
DELETE | /files/{path} | Delete a file. Directories are refused with 400. There is no undo. |
POST | /openai/v1/chat/completions | OpenAI-compatible chat, streaming and non-streaming |
GET | /openai/v1/models | The CODEXBOX_AVAILABLE_MODELS list |
POST | /mcp/ | MCP over streamable HTTP, only when CODEXBOX_MCP_MODE=1. See MCP mode. |
Every /files path resolves against the workspace root with traversal checking: a .. that escapes the root returns 400. The same bearer token protects them as the rest of the API.
# upload a file
curl -sS -X PUT \
--oauth2-bearer "$CODEXBOX_API_MODE_TOKEN" \
--data-binary @local.txt \
http://localhost:8080/files/notes/hello.txt
# download it again
curl -sS \
--oauth2-bearer "$CODEXBOX_API_MODE_TOKEN" \
http://localhost:8080/files/notes/hello.txt
# list the directory
curl -sS \
--oauth2-bearer "$CODEXBOX_API_MODE_TOKEN" \
http://localhost:8080/files/notes | jq
# delete it
curl -sS -X DELETE \
--oauth2-bearer "$CODEXBOX_API_MODE_TOKEN" \
http://localhost:8080/files/notes/hello.txt
Runs serialize per workspace. A second run against a busy workspace gets 409.
Running a prompt
The POST /run body takes prompt (required), workspace, model, systemPrompt, appendSystemPrompt, jsonSchema, eventMode, outputFormat, noContinue, resume, timeoutSeconds, thinking, noTools, toolsAllowlist, includeRaw, async and fireAndForget.
curl -sS http://localhost:8080/run \
--oauth2-bearer "$CODEXBOX_API_MODE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"prompt": "say HELLO", "workspace": "/workspace"}'
For a long run, send "async": true and poll until status is no longer running:
run_id=$(
curl -sS http://localhost:8080/run \
--oauth2-bearer "$CODEXBOX_API_MODE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"prompt": "run the full test suite and summarize failures", "async": true}' |
jq -r .runId
)
curl -sS "http://localhost:8080/run/result?runId=$run_id" \
--oauth2-bearer "$CODEXBOX_API_MODE_TOKEN" | jq
# cancel it
curl -sS -X DELETE "http://localhost:8080/run/$run_id" \
--oauth2-bearer "$CODEXBOX_API_MODE_TOKEN"
Events
"eventMode": "full" returns every Codex exec --json record. Each one is wrapped as {sequence, attempt, backend, eventType, event}, and the nested event is untouched, so reasoning items, command execution updates, file changes, MCP activity, web activity, todo updates and turn usage all come through. "eventMode": "none" returns only the final result. The default, "auto", keeps the older behavior of turning events on for jsonSchema requests; "outputFormat": "json-verbose" is the compatibility alias for that.
"outputFormat": "text" and "outputFormat": "json" are still accepted but change nothing in the /run response. New callers should use eventMode.
Structured output
jsonSchema maps to Codex’s native --output-schema, so Codex itself enforces the schema. The shared validator and its retry stay as a fallback if the returned text still fails validation. jsonSchema works with either event mode.
curl -sS http://localhost:8080/run \
--oauth2-bearer "$CODEXBOX_API_MODE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"prompt": "extract the version and license from README.md",
"workspace": "/workspace",
"jsonSchema": {
"type": "object",
"properties": {
"version": {"type": "string"},
"license": {"type": "string"}
},
"required": ["version", "license"]
}
}' | jq .json
Prompts, tools and sessions
systemPrompt, appendSystemPrompt, noTools, toolsAllowlist, resume and noContinue map onto Codex in ways that differ from the other aicodebox agents. Codex adapter has the mapping. In short: toolsAllowlist is ignored with a warning, and a call with neither resume nor noContinue continues the workspace’s pinned Codex session.
OpenAI-compatible endpoint
POST /openai/v1/chat/completions sits on the same port behind the same token. Point an OpenAI SDK’s base URL at http://host:8080/openai/v1:
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8080/openai/v1",
api_key="your-secret",
)
resp = client.chat.completions.create(
model="gpt-5.1-codex",
messages=[{"role": "user", "content": "say HELLO"}],
)
print(resp.choices[0].message.content)
- Streaming:
"stream": truestreams plain chat as it arrives. Withtools,tool_choiceor a schema, the full answer is computed first and then sent as one SSE stream.stream_options.include_aicodebox_eventsadds Codex’s native records as namedaicodebox.nativeSSE events; standard chunks are unchanged. - Tools: OpenAI-style
toolsandtool_choice, executed by the client. Codex runs its own tools internally, so the endpoint asks Codex to answer with a{"tool_calls":[...]}object and turns that into OpenAItool_calls. It is stateless: send the full history on every round trip, as with the OpenAI tool loop. response_format:{"type":"text"}(the default),{"type":"json_object"}, or{"type":"json_schema","json_schema":{...}}, which goes through the same--output-schemapath asjsonSchema. It combines withtools; only the final answer that is not a tool call is checked against the schema.- Headers for what the OpenAI format has no field for:
x-aicodebox-workspace,x-aicodebox-continue,x-aicodebox-resume,x-aicodebox-append-system-prompt,x-aicodebox-json-schema(used whenresponse_formatis not set),x-aicodebox-no-tools,x-aicodebox-tools-allowlist,x-aicodebox-timeout-secondsandx-aicodebox-extra-args.x-claude-workspace,x-claude-continueandx-claude-append-system-promptare accepted as aliases.
API mode settings
| Variable | Default | What it does |
|---|---|---|
CODEXBOX_API_MODE | 0 | Run the HTTP API server in the foreground |
CODEXBOX_API_MODE_PORT | 8080 | Port the server binds |
CODEXBOX_API_MODE_TOKEN | empty | Bearer token for every route except /healthz and /mcp/, which has its own. Empty means no auth. |
CODEXBOX_AVAILABLE_MODELS | none | Required. Comma-separated list served at /openai/v1/models |
Every CODEXBOX_* name is an alias for the AICODEBOX_* name the base reads. If both are set, AICODEBOX_* wins.
With CODEXBOX_API_MODE_TOKEN empty, anyone who can reach the port can run Codex and read, write and delete workspace files. Set the token, and bind to loopback or put an authenticating proxy in front before exposing the port.
With MCP mode
API mode is the one foreground mode where MCP costs nothing extra. It is mounted at /mcp/ on this same port instead of running a second server. See MCP mode.