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_subscriptionsflag. See feature status and Run Codex on a ChatGPT subscription, which also says what a setup script must not do withCODEX_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, ornullon selection failure.source:runtimefor a returned model field, orselection_ackwhen the runtime accepted the setter without returning a model field.statusanderror: 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
neveras well as the sandbox todangerFullAccess. Codex then runs its shell commands and file edits without asking. Fountain applies the agent's permission policy only when a runtime sendssession/request_permission, and it cannot hold or deny an operation that sends no request. So a policy ofaskorauto_denyno longer stops those commands and edits. Give full access only to agents whose policy you would set toauto_allow. The reverse does not hold: a policy ofauto_allowdoes 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'sallowed_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.