OpenAI Agents API
The Agents API moves the loop to OpenAI. Sessions, context management, tool discovery, parallel tool calls and subagent orchestration run in the Codex harness; you submit tasks and read events. What you still choose is where the code runs — nowhere, in an OpenAI-hosted sandbox, or on your own machines.
That choice determines which processes can read a credential. The application server, sandbox executor, MCP connections, and any credential proxy have different access.
This is the hosted runtime, not the library. If your own process runs the
loop with pip install openai-agents, you want the OpenAI Agents
SDK page instead — there, wrapping the
process is the whole integration. The two share a name, an API key, and almost
nothing else about where secrets end up.
Three placements, three answers
environment.type | Who runs the code | Can you resolve at startup? | Give it |
|---|---|---|---|
"none" | Nobody — the harness calls remote MCP tools directly | No | Nothing. Credentials stay in your app or behind your MCP server |
"openai_hosted" | OpenAI's sandbox | No process of yours | An OpenAI vault placeholder, or a seekrit placeholder and a proxy you run |
"self_hosted" | You, running codex exec-server | Yes | seekrit run in front of the executor, or a proxy beside it |
An OpenAI-hosted environment takes an env map of string values. OpenAI's
security guidance
says agent-generated code can read injected secrets. A real value in env can
be printed, sent over the network, or written to an artifact.
The short version. Your application server gets secrets the normal way
(seekrit run -- node server.js). A self-hosted environment gets them the
sandbox way (resolve at executor start, or broker with the proxy). An
OpenAI-hosted environment can use OpenAI's
vault credentials
or {{seekrit:NAME}} with a proxy you run. Both keep the real value out of
sandbox code; the latter also keeps it out of OpenAI's vault.
1. The application server
Your side of the Agents API is an ordinary process: it calls
client.beta.agents.sessions.create(...), streams events, and answers function
tool calls. So it is the ordinary integration — the three
commands and a wrapper:
seekrit run -- node server.js # or: python app.py, uv run …
Keep the application and executor credentials in separate seekrit environments.
Each seekrit run invocation resolves its entire bound environment:
seekrit secrets set OPENAI_API_KEY sk-… --app storefront --env app-server
seekrit secrets set CODEX_API_KEY sk-… --app storefront --env agent-exec # self-hosted only
OPENAI_API_KEY— your application key. Mint it with onlyapi.agents.read,api.agents.writeandapi.responses.write; a key that can also read files, fine-tunes and org settings is a bigger key than the session loop needs.CODEX_API_KEY— the restricted executor key, which authorizes connecting an environment and nothing else. It is a deliberately small credential, and it is still a credential: it goes in seekrit and is injected where the executor runs, not baked into the image.
Keep them apart. The executor runs model-authored commands, so it must never
receive the application key. Create separate service tokens bound to each
seekrit environment and start the application with the app-server token.
2. Self-hosted environments
environment.type: "self_hosted" gives you back the thing every other page in
these docs assumes: a process you start. You run the executor inside your own
compute and it dials out to OpenAI:
# Both values come from the session you just created:
# session.environment.remote_url and session.environment.id.
seekrit run --app storefront --env agent-exec -- \
codex exec-server \
--remote "$REMOTE_URL" \
--environment-id "$ENVIRONMENT_ID"
seekrit run resolves the agent-exec environment, decrypts locally, and
execs the executor with the values set. Keep only CODEX_API_KEY and other
values the executor must read in that environment. The launcher does not send
them to OpenAI, but model-authored commands can still print or transmit them.
In a container, use
seekrit-run, which is a static binary with no Node
dependency.
Then remember what the executor is. It runs the shell commands the model
asks for, so anything in its environment is readable by those commands —
os.environ, printenv, a stack trace, or a file the agent writes. Injection
here is exactly as strong as injection into
any other sandbox: it bounds which secrets are reachable, not what the agent
does with them.
So put the values one process away:
# 1. Configure seekrit-proxy.toml with a /acme route to api.acme.com,
# allowing only ACME_API_KEY and the methods and paths this agent needs.
# Start it separately, with its own seekrit token.
SEEKRIT_TOKEN=skt_… seekrit-proxy --config seekrit-proxy.toml &
# 2. The executor still needs CODEX_API_KEY, but receives placeholders for
# third-party APIs. Its seekrit token resolves only the agent-exec environment.
ACME_API_BASE=http://127.0.0.1:8080/acme \
ACME_API_KEY='{{seekrit:ACME_API_KEY}}' \
seekrit run --app storefront --env agent-exec -- \
codex exec-server --remote "$REMOTE_URL" --environment-id "$ENVIRONMENT_ID"
seekrit-proxy substitutes the real value on the way
out, toward the hosts, methods and paths you allowlisted, default-deny. For
this third-party key, the agent's environment holds only
{{seekrit:ACME_API_KEY}}. The restricted executor key remains readable by
agent-generated code, as OpenAI documents.
Run the proxy where the agent cannot reach its token — a sidecar container or a
separate OS user — or the agent can call /v1/resolve itself and policy becomes
decoration. That, and the rest of the reasoning, is the
sandboxes page; a self-hosted Agents API environment is
a sandbox with an OpenAI-shaped control plane on top.
Let the executor out
The executor connects outbound only, and needs to reach:
| Host | For |
|---|---|
api.openai.com | Registering the environment |
codex-cloud-environments.chatgpt.com (WSS) | Receiving commands, returning results |
api.seekrit.dev | seekrit run / seekrit-proxy resolving ciphertext |
If your egress rules are default-deny, api.seekrit.dev is the one people
forget, and it fails quietly: seekrit run
degrades gracefully and starts the
executor anyway, so a blocked resolve looks like an agent whose tools mysteriously
have no credentials. Use --cache on long-running executors so a transient
failure falls back to the last encrypted response rather than starting bare.
Forward mode, for commands you did not write
A base URL only helps for SDKs that read one. An agent that runs
curl https://api.internal/… inside the executor's environment doesn't. For
that, run the proxy in forward mode
and set HTTPS_PROXY plus the CA bundle in the executor's environment — every
outbound request is then matched against the same allowlist, whoever made it.
The honest caveat is in that section: HTTPS_PROXY is not confinement on its
own. Pair it with a network namespace or firewall rule that makes the proxy the
only route out.
3. OpenAI-hosted environments
environment.type: "openai_hosted" is the one that changes the rules. OpenAI
provisions the sandbox; your application configures it through the API. The
environment takes packages, setup_commands, files, env, and a network
policy. OpenAI also offers vault credentials:
their network proxy substitutes a secret for an approved HTTPS host while
agent-generated code sees a placeholder. That keeps the value out of the
sandbox, but stores it in OpenAI's vault. If the value must remain in seekrit
and on infrastructure you control, use the external proxy below.
You can sync a seekrit environment to OpenAI Vault to provision and rotate those credentials. This grants seekrit's sync engine decryption access to that environment and copies the real values into OpenAI's vault. Existing sandbox sessions do not pick up credential updates; create a new session after rotation. The proxy flow keeps the real value outside OpenAI.
A real value in env is readable by agent-generated code
The env map takes strings that become environment variables inside the
sandbox. Agent-generated code can read them. Configuration can also be saved
and reused through an environment_template_id. A real credential here may
appear in command output or files the agent writes.
Two practical notes before the alternative:
- Reserved names are rejected —
PATH,CODEX_*, andOPENAI_API_KEY. So the obvious move (hand the sandbox an OpenAI key so its code can call the API) isn't available anyway. Name the variable something else and you are back to putting a live provider key where the model can read it. - A
SEEKRIT_TOKENthere is worse, not better. It is one credential instead of five, but it decrypts the whole environment it is bound to, and the agent can call/v1/resolvewith it directly. If you do it anyway — for a sandbox doing genuine work against genuinely low-value secrets — bind it to an environment created for that purpose and nothing else, and revoke it when the session ends.
The shape that works: a placeholder and a proxy you run
Point the sandbox at a seekrit-proxy on your own infrastructure and give it a
placeholder. Restrict its sandbox egress to the proxy's hostname. This is an
illustrative session configuration; first provision a TLS ingress, a proxy route
and a ticket as described below:
session = client.beta.agents.sessions.create(
agent={
"model": "gpt-6-astra",
"instructions": "Use the internal API through the configured base URL.",
},
environment={
"type": "openai_hosted",
"network": {"access": "restricted", "allowed_domains": ["broker.acme.com"]},
"env": {
"ACME_API_BASE": "https://broker.acme.com/acme",
"ACME_API_KEY": "{{seekrit:ACME_API_KEY}}",
"SEEKRIT_TICKET": ticket, # bearer ticket; see below
},
},
input="Reconcile yesterday's orders and write a summary to /workspace/report.md.",
stream=True,
)
Three properties, in the order they matter:
- The sandbox holds no upstream API key. It holds a placeholder and a short-lived proxy ticket. The ticket is still a bearer credential that agent-generated code can read.
- Sandbox network access is restricted.
network.access: "restricted"allows the listed hosts. Keep that list narrow; an allowed endpoint can still receive data the agent sends it, and service-origin MCP connections are a separate route outside the sandbox's network policy. - The proxy bounds the operation, not just the credential.
methodsandpathson the route mean a legitimate placeholder still cannot reachDELETE /v1/customers. See bounding what the agent may do.
allowed_domains takes bare hostnames — no ports, protocols, paths or
wildcards. Your proxy therefore has to answer on standard HTTPS at a real
hostname, behind a TLS terminator; broker.acme.com:8080 is not expressible.
Redirect destinations and other hosts the sandbox needs require their own
entries. Add only the ones the workload actually needs.
Give the session its own ticket
A proxy reachable from the internet needs to know which caller it is serving.
Your application server creates the session, so it can mint a scoped,
short-lived ticket and pass it in env:
curl -s localhost:9090/session \
-H "x-seekrit-control-token: $SEEKRIT_PROXY_CONTROL_TOKEN" \
-H 'content-type: application/json' \
-d '{"agent":"orders-bot","scopes":["ACME_API_KEY"],"ttl":"30m"}'
# → {"ticket":"skp_…","agent":"orders-bot","expires_in_seconds":1800,"header":"x-seekrit-ticket"}
Setting SEEKRIT_TICKET does not add an HTTP header automatically. The
client code or agent command must present it with the placeholder:
curl "$ACME_API_BASE/orders" \
-H "Authorization: Bearer $ACME_API_KEY" \
-H "x-seekrit-ticket: $SEEKRIT_TICKET"
The proxy strips x-seekrit-ticket before forwarding. Scopes can only narrow:
the effective set is the ticket's intersection with policy, and an expired or
unknown presented ticket is refused. But a request with no ticket can use
the proxy's default identity. Configure your HTTPS ingress to reject requests
without x-seekrit-ticket before they reach the proxy, and keep the proxy's
control listener private. The proxy must still validate the ticket itself.
The ticket is readable inside the sandbox, so treat it as a short-lived bearer
credential and avoid logging it. Renew it deliberately if a session outlives
its TTL. The full mechanism is in several agents behind one
proxy.
For a fleet, publish the rules from the dashboard instead of shipping a file per proxy: agent access policy signs the bundle in the browser, and the proxy verifies it against a thumbprint pinned in its own TOML. The API stores bytes it cannot forge, which keeps "where may this agent send a credential" out of reach of anything that isn't you.
Don't install a resolver in setup_commands
setup_commands runs before the agent starts, which makes it tempting:
curl … | sh the launcher, then seekrit-run your entrypoint inside the
sandbox. It resolves the rotation problem and nothing else — the values still
end up in an environment the model's code reads, and now the token is in there
too. The reason it works on Fly.io Sprites is
that you decide what runs there. Here you don't.
Use setup_commands for what it is good at — packages, project layout,
seeding /workspace from files — and leave credentials to the proxy.
4. environment.type: "none"
No environment is needed when an agent uses only remote MCP servers or function calls answered by your application.
Then the question becomes where its tools keep their credentials:
| Tool shape | Where the credential lives | Notes |
|---|---|---|
| Function tool answered by your app | Your application server | Resolve with the SDKs — per request, per tenant if you need it |
Remote MCP, connection_origin: "service" | Inline transport.authorization, or an OpenAI vault | Encrypted and omitted from the returned session resource |
| Remote MCP you wrote | Your MCP server, resolving on its own side | The agent authenticates to you; the upstream key never enters the picture |
The middle row is the one to think about. Inline and vault credentials are
handled carefully by OpenAI — vault values are never returned, and inline
authorization/headers are encrypted and stripped from the session object —
but both mean a live third-party credential is held at OpenAI on your behalf. If
that server is yours and you want to keep its upstream key outside OpenAI, give
the harness a credential that only authenticates to your MCP server. Your MCP
server resolves the upstream key from seekrit at the moment of use.
MCP servers: which plane goes where
seekrit ships two MCP servers, split along the zero-knowledge line, and they map onto the Agents API's connection origins almost too neatly.
The hosted metadata server can connect at connection_origin: "service".
mcp.seekrit.dev provisions applications and environments, lists secret
names, and reads the audit trail. It registers no tool that returns a stored
secret value or creates a decrypting service token, which a test enforces.
Its metadata actions can still change or delete configuration, so authenticate
it and use allowed_tools to narrow the agent's actions:
{
"type": "mcp",
"server_label": "seekrit",
"transport": {
"type": "http",
"server_url": "https://mcp.seekrit.dev/mcp",
"authorization": "Basic <base64(clientId:clientSecret)>"
},
"connection_origin": "service",
"allowed_tools": ["whoami", "list_apps", "list_envs"],
"required": true
}
Add creation tools only when the agent needs them. See the MCP server page for authentication and the current tool list.
The local crypto server does not belong in a sandbox. @seekrit/mcp is the
half that decrypts: get_secret, set_secret, export_env. Running it as a
stdio MCP inside an OpenAI-hosted environment hands the model a
value-returning tool and the credential behind it. In a self-hosted
environment, it is your call and occasionally the right one — a trusted internal
agent whose whole job is managing configuration — but it is the opposite of the
split the two servers exist to make.
Three more things about MCP transports that cost people an afternoon:
transport.env_varsis readable by the environment. It names variables the stdio server reads from the sandbox's environment, which means anything else in that sandbox can read them too. OpenAI documents this plainly. It isenvwith extra steps.- A stdio MCP in an OpenAI-hosted environment forces
network.accessopen. Thedisabledandrestrictedpolicies are not supported for those connections — so the "lock egress to my proxy" shape and stdio MCPs are mutually exclusive on the hosted sandbox. If you need both, self-host the environment. - Environment-origin HTTP MCPs cannot use vaults. OpenAI's docs send you to inline credentials or a trusted proxy for those; the proxy is the one that does not put a live credential anywhere the agent can read it.
What leaves the sandbox
Two exits, both worth a minute:
- Artifacts and files. The point of a sandbox environment is that the agent
produces something you retrieve. A
.envit wrote, a debug dump, a config file with an expanded value — those come out with the report. A placeholder coming out is a non-event; a key coming out is an incident. - Session events. Tool arguments and command output flow back through the harness and into whatever you log. If a credential is in the environment, agent-generated code can print it into a transcript. See OpenAI's retention controls for what that means on their side; the fix on yours is for there to be nothing to log.
Subagents multiply whatever you decided
Subagents are a first-class feature — enabled: true, max_concurrent_subagents
— and each one inherits the environment's credential posture. If the answer was
"a placeholder and a ticket", four subagents are four callers presenting the same
ticket, and the scope you set is the scope all of them have. If the answer was
"a key in env", you now have four processes that can read it.
Nothing extra to configure; it just raises the cost of getting the earlier decision wrong.
Rotation and teardown
An Agents API session survives between turns, and a connected sandbox can stay alive. Plan how each credential or ticket will refresh or expire.
| When you | Do |
|---|---|
| Rotate a value | seekrit-proxy resolves at startup by default; restart it or enable [secrets] refresh_interval (automatic in server policy mode). Restart self-hosted executors that received an injected value |
| End a session | Revoke the proxy ticket through its private /session/revoke control endpoint, or let a short TTL expire |
| Retire an agent | seekrit token revoke the executor's token; delete any environment template that carried env values |
| Suspect exposure | Rotate at the provider, not just in seekrit — see rotation |
Checklist
- Application key scoped to
api.agents.read,api.agents.write, andapi.responses.write, kept in the application environment -
CODEX_API_KEYstored in seekrit, injected where the executor runs, never in an image - Self-hosted: executor has only its restricted connection key and necessary placeholders; proxy holds third-party values separately
- Self-hosted:
api.seekrit.devallowed through egress,--cacheon long runs - OpenAI-hosted: no provider key and no
SEEKRIT_TOKENinenvironment.env - OpenAI-hosted: choose OpenAI vault credentials or a seekrit proxy you run; set
network.access: "restricted"to the required hostnames - Own proxy: HTTPS ingress rejects missing tickets; control listener stays private
- Own proxy: per-session ticket with a short TTL, minted by your app, explicitly sent as
x-seekrit-ticket -
methods/pathson every proxy route, so a valid credential still can't reach a destructive operation - MCP: metadata plane at
connection_origin: "service"; crypto plane nowhere near a hosted sandbox - Artifacts reviewed before they leave, on the assumption the agent wrote a config file
See also
- OpenAI Agents SDK — the in-process library, where wrapping is the whole integration
- AI frameworks — the three-command setup and the four levels, in any framework's idiom
- Agent sandboxes — the shape a self-hosted environment is a special case of
- Credential broker — placeholders, tickets, and the allowlist
- Agent access policy — signed policy bundles for a fleet of proxies
- MCP servers — the metadata plane the harness can hold, and the crypto plane it cannot
seekrit-run— the static launcher, for executors in containers