seekrit
Docs/OpenAI Agents API

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.

note

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.typeWho runs the codeCan you resolve at startup?Give it
"none"Nobody — the harness calls remote MCP tools directlyNoNothing. Credentials stay in your app or behind your MCP server
"openai_hosted"OpenAI's sandboxNo process of yoursAn OpenAI vault placeholder, or a seekrit placeholder and a proxy you run
"self_hosted"You, running codex exec-serverYesseekrit 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.

tip

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 only api.agents.read, api.agents.write and api.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:

HostFor
api.openai.comRegistering the environment
codex-cloud-environments.chatgpt.com (WSS)Receiving commands, returning results
api.seekrit.devseekrit 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_*, and OPENAI_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_TOKEN there 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/resolve with 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:

  1. 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.
  2. 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.
  3. The proxy bounds the operation, not just the credential. methods and paths on the route mean a legitimate placeholder still cannot reach DELETE /v1/customers. See bounding what the agent may do.
caution

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 shapeWhere the credential livesNotes
Function tool answered by your appYour application serverResolve with the SDKs — per request, per tenant if you need it
Remote MCP, connection_origin: "service"Inline transport.authorization, or an OpenAI vaultEncrypted and omitted from the returned session resource
Remote MCP you wroteYour MCP server, resolving on its own sideThe 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_vars is 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 is env with extra steps.
  • A stdio MCP in an OpenAI-hosted environment forces network.access open. The disabled and restricted policies 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 .env it 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 youDo
Rotate a valueseekrit-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 sessionRevoke the proxy ticket through its private /session/revoke control endpoint, or let a short TTL expire
Retire an agentseekrit token revoke the executor's token; delete any environment template that carried env values
Suspect exposureRotate at the provider, not just in seekrit — see rotation

Checklist

  • Application key scoped to api.agents.read, api.agents.write, and api.responses.write, kept in the application environment
  • CODEX_API_KEY stored 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.dev allowed through egress, --cache on long runs
  • OpenAI-hosted: no provider key and no SEEKRIT_TOKEN in environment.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/paths on 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