Run Codex as an API

OpenAI's Codex CLI, headless in the sandbox.

Run Codex on a sandbox with your repositories, packages and credentials. Send a prompt over HTTP, follow the transcript, and send the next prompt to the same conversation. Fountain manages the machine between turns.

To use a chat interface, open Conversations. To call it from your own code, follow the quickstart, then use the agent definition below.

Summary

Provider openai
Multi-provider No
Transport ACP, through the pinned codex-acp adapter
Skills root /home/sprite/.codex/skills
skills.sh agent codex
System prompt ~/.codex/AGENTS.md
Credential An OpenAI API key

Why you would choose this one

You want an OpenAI model to do the work. Or you compare two runtimes on the same task.

Set it up

apiVersion: fountain.dev/v1
kind: Agent
metadata:
name: reviewer
spec:
runtime: codex
model: openai/gpt-6-astra

The model must carry the openai/ prefix.

Add your OpenAI key at /account/inference-credentials in the app.

ChatGPT subscriptions

A credential set can name a linked ChatGPT subscription, and codex then runs on that plan with no OpenAI key. This is in development. It is on for every account on the hosted platform since 2026-09-21, with parts not tested on the real service, and off on your own instance unless the operator forces the chatgpt_subscriptions flag. See feature status and Run Codex on a ChatGPT subscription, which also says what a setup script must not do with CODEX_HOME.

Call it over HTTP

After you apply the agent definition, set FOUNTAIN_AGENT_ID to the returned agent id, FOUNTAIN_BASE_URL to your instance URL, and FOUNTAIN_API_KEY to your Fountain account key. The account key is separate from the model credential.

curl -sS -X POST "$FOUNTAIN_BASE_URL/api/conversations" \
-H "Authorization: Bearer $FOUNTAIN_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"agent_id\": \"$FOUNTAIN_AGENT_ID\", \"prompt\": \"Which operating system and working directory are you in? Answer in one sentence.\"}"

Use the returned conversation id to follow events and send another prompt. A self-hosted instance uses the same request at its own base URL.

Verify

Check the turn's model_selection in GET /api/conversations/:id/turns. The same fields appear in the model stream stage:

  • requested_model: the ID sent to the runtime.
  • effective_model: the runtime's selected ID, or null on selection failure.
  • source: runtime for a returned model field, or selection_ack when the runtime accepted the setter without returning a model field.
  • status and error: whether selection succeeded and the failure message.

Selection evidence is separate from the agent's saved model. To verify actual execution, inspect Codex's session JSONL turn_context.payload.model. An assistant's answer about its model is not execution evidence.

An explicit model that the runtime rejects stops the turn before inference. Fountain does not substitute another model. An unavailable ID in the runtime catalog does not, by itself, prove that your provider account lacks access. Check the bundled Codex version, its refreshed model/list response, and the provider response with the same credentials.

Fountain checks the pinned adapter version before opening a new connection, including on a persistent sandbox. An existing connection keeps its process until it closes. If an old runtime rejects the model, the failed connection closes; retrying opens the updated adapter against the same session and disk. The sandbox must allow registry access through its configured network path.

A saved agent model change takes effect on the next user turn, including on an existing ACP connection. The conversation, session, transcript and worktree remain in place. A change during a running turn applies to the next turn.

The sandbox codex builds for itself

Codex applies a sandbox policy of its own inside the Fountain sandbox. The pinned codex-acp adapter sends that policy with each session. By default a command can write to the workspace, to the environment's repositories and to the temporary directories, and it cannot reach the network.

What the adapter sends Default
The sandbox workspaceWrite
writableRoots each repository's mount_path, and its .git
networkAccess false
excludeSlashTmp false, so /tmp is writable
excludeTmpdirEnvVar false, so $TMPDIR is writable
The approval policy on-request
The approvals reviewer auto_review

Fountain sends each repository's mount_path, and the .git inside it, to the adapter as ACP additionalDirectories entries, and the adapter adds them to writableRoots. The .git has to be named on its own because codex keeps .git read-only inside every writable root, so a branch or a commit fails with the clone alone. The agent can therefore commit to a clone Fountain made for it, and cut a worktree from it, even though the clone lives outside the workspace. That includes .git/hooks and .git/config, which the Fountain sandbox, and not codex's, contains.

Inside that sandbox, other writes and all network calls are still refused. A write outside the workspace, the repositories and the temporary directories fails. And a network call fails, so git fetch, a package install and a curl the agent runs itself all fail.

A refusal is not always the end. Because the approval policy is on-request, codex can ask to run a refused command outside the sandbox, or ask for network or file-system access. Codex's automatic reviewer judges that request first, and it can approve or deny it without asking Fountain. A request that reaches Fountain arrives as session/request_permission, and the agent's permission policy answers it. An approval can let that one command run, or grant the access for the turn or the session. The reviewer and the policy can also deny the request, and codex does not ask every time. Try this route before full access, which removes both the sandbox and the approvals.

~/.codex/config.toml does not widen it. The adapter sends an explicit per-session policy, and that policy wins over the file, in the same way the CODEX_CONFIG overlay wins over a model_provider written into it.

Give codex full access

Full access is for an agent that needs the network, or writes outside its workspace, on every turn and without asking. Set INITIAL_AGENT_MODE to agent-full-access in the environment's env_vars. The adapter reads it from the process environment when it opens the session.

apiVersion: fountain.dev/v1
kind: Environment
metadata:
name: codex-full-access
spec:
env_vars:
INITIAL_AGENT_MODE: agent-full-access

Six things follow from that.

  • It also turns off codex's own approvals. The mode sets the adapter's approval policy to never as well as the sandbox to dangerFullAccess. Codex then runs its shell commands and file edits without asking. Fountain applies the agent's permission policy only when a runtime sends session/request_permission, and it cannot hold or deny an operation that sends no request. So a policy of ask or auto_deny no longer stops those commands and edits. Give full access only to agents whose policy you would set to auto_allow. The reverse does not hold: a policy of auto_allow does not remove the sandbox. It approves only the requests that codex sends to Fountain.
  • It is all or nothing. The value names a mode, not a list. The environment's repositories are already writable without it, but there is no way to add network access on its own, or a writable directory that is not a repository. The adapter has no per-session network setting short of full access. #1684 tracks the rest.
  • It does not widen Fountain's own egress. An environment with networking_type: limited, and the credential broker where it is on, still decide which hosts a request reaches. Full access lets codex attempt the call. The network policy decides whether it lands, and a host that is not allowed still gets a 403 that names it.
  • Every conversation on that environment gets it. Give the agents that need full access an environment of their own. A launch can then name it with environment_id, within the agent's allowed_environment_ids, rather than widening the environment everything else shares.
  • The value is scrubbed from the logs. It is longer than the 8-byte redaction floor, so an agent that prints its environment shows INITIAL_AGENT_MODE=[REDACTED]. That is the scrubber working, and not a variable that failed to arrive. Read Where a secret comes from.
  • It is codex only. claude, gemini and opencode do not express a sandbox this way. The variable reaches them and means nothing to them.

Limits

The CLI takes the bare model id, so Fountain removes the openai/ prefix before it calls the CLI. You never see that in normal use. It matters when you read a spawn command in the logs.

On a deployment with the egress broker on, Fountain moves the conversation onto a model provider of its own. The provider is the same endpoint with the WebSocket transport off. Codex's WebSocket dialer cannot use the broker's https-scheme proxy. It waits out the full connect timeout before it falls back to HTTP, which was about 300 seconds on every turn. Fountain carries across OPENAI_BASE_URL, the OpenAI-Organization and OpenAI-Project header mappings, and standalone web search. A conversation with no OPENAI_API_KEY keeps the built-in provider, which reads ~/.codex/auth.json. A conversation on a ChatGPT subscription gets the same treatment. Its provider points at the Codex backend and reads the subscription's sign-in.

Fountain also sets MODEL_PROVIDER to that provider. The ACP adapter passes this variable to Codex when it resumes a conversation. Without it, a resumed conversation went back to the built-in provider and to the WebSocket wait.

Fountain reads only the CODEX_CONFIG overlay and MODEL_PROVIDER when it does this. A model_provider that your setup script writes into ~/.codex/config.toml is not read, and the overlay wins over the file. An agent that reached a gateway that way now reaches the endpoint above instead. To keep your own provider, name it in CODEX_CONFIG or in MODEL_PROVIDER. Fountain leaves both alone.

The sandbox images do not pin the Codex CLI. The field that turns the transport off is supports_websockets, which Codex 0.153.3 accepts. A later Codex that ignores the field brings the 300-second wait back, and nothing in Fountain reports it. The symptom is the gap between the model stream stage and the first agent output. See openai/codex#13103.