Environment Spec Referencepublic
Last verified 23 Sep 2026
DigitalOcean Harness Runtime combines the functionality of a lightweight microVM, built-in tools like chromium and a coding sandbox needed by agents to do work. The product offers rich lifecycle APIs that persist conversational history and working state across sessions, with pause/resume/fork semantics so that developers can control costs and adapt workflows to the nonlinear quirks of agentic work. See What You Can Build for example use cases.
As an alternative to configuring your environment in the Control Panel, you can define an environment specification using YAML.
Field names are decoded strictly. An unknown or misspelled field returns a 400 naming the key, so nothing you write is silently ignored.
Example Environment Specification
The following spec shows a named Codex session with size and idle timeout, a persistent workspace, env and secrets, an egress allowlist, Action Gateway and custom MCP tools, inline skills, permission rules, and interactive mode.
name: my-first-session
agent: codex
size: mars-2vcpu-4gb
idle_timeout: 10m
persistent_workspace: true
env:
MODEL: gpt-5.4
secrets:
OPENAI_API_KEY: ${OPENAI_API_KEY}
egress:
- api.github.com
- pypi.org
- mcp.example.com # inline MCP hosts are NOT auto-allowed
tools:
# One entry per server. `- do.actions` (all tools) OR `- do.actions: [...]`
# (selection). Declaring both is a duplicate-server 400.
- do.actions: [exa_web_search, action_code, "toolbelt:read-only@1"]
- name: my-mcp
url: https://mcp.example.com
tools: [search, fetch]
auth:
header: Authorization
secret: MY_MCP_TOKEN
skills:
- name: pdf-processing
description: "Use when the task involves reading or editing PDF files."
instructions: |
# PDF Processing
Steps the agent should follow...
license: Apache-2.0
metadata:
author: platform-team
allowed_tools: [file.read, bash]
permissions:
default: ask
rules:
- tool: bash
action: ask
enforcement: strict # strict | best-effort
- tool: file.read
action: allow
# Server-qualified MCP targets; server must be declared in `tools`.
- tool: do.actions/exa_web_search
action: allow
- tool: my-mcp/create_issue
action: ask
mode: interactiveTop-Level Fields
The environment specification has the following top-level fields.
| Field | Type | Required | Description |
|---|---|---|---|
agent |
string | Yes | Adapter identifier. See agent. |
name |
string | No | Session name. Team-unique among live sessions, 1-64 characters of letters, digits, -, ., or _, starting and ending alphanumeric, and not in UUID form. Generated when not provided. |
description |
string | No | Free-form, up to 1024 characters. |
labels |
map | No | Free-form labels. |
image / entrypoint |
string / list | With agent: custom |
Digest-pinned container image and argv entrypoint for a bring-your-own container. |
config |
object | No | Provider-specific runtime config. Use only with agent: codex-agentapi. Forwarded as the OpenAI Agents API create-session body; DigitalOcean stores and echoes it without validating contents. |
repos |
list | No | GitHub OWNER/REPO entries, unique case-insensitively. The first entry is the session repo_hint. The full list reaches the guest as HARNESS_WORKSPACE_REPOS and as the platform workspace-repos skill. Nothing is cloned for the agent, and egress must still permit github.com. |
env |
map | No | Non-secret plaintext guest environment. See env. |
secrets |
map | No | Declared credentials. See secrets. |
size |
string | No | Machine size slug. When omitted, the service default applies (currently mars-2vcpu-4gb). Available sizes are mars-1vcpu-1gb, mars-2vcpu-2gb, mars-2vcpu-4gb, mars-4vcpu-8gb, and mars-16vcpu-32gb. Use GET /v2/agents/sessions/sandbox/sizes for the current list. See Sandbox Sizes. |
idle_timeout |
duration | No | Idle suspend for this session. Defaults to 15 minutes. Mutually exclusive with keep_warm. |
keep_warm |
boolean | No | Exempt this session from idle auto-pause. See keep_warm. |
egress |
string, list, or object | No | Outbound network policy request. See egress. |
tools |
list | No | MCP server attachments. See tools. |
permissions |
object | No | Tool-permission policy. See permissions. |
template |
string | No | Execution-environment template. When not provided, the value is derived from the adapter (for example codex to coding-codex). |
persistent_workspace |
boolean | No | Keep /workspace across idle suspend and resume. |
skills |
list | No | Inline agent skills. See skills. |
mode / serving |
string / object | No | Lifecycle mode. See mode and serving. |
Durations are strings of integer hour, minute, or second components (10m, 90s, 8h, 1h30m). Bare second counts such as 600 are not allowed.
agent
The agent field accepts the following adapter identifiers. For the architectural difference between them, see Agent Adapters.
| Value | Description |
|---|---|
claude-code |
Runs Claude Code under DigitalOcean supervision. |
opencode |
Runs OpenCode under DigitalOcean supervision. |
codex |
Runs Codex CLI under DigitalOcean supervision. |
codex-agentapi |
Externally orchestrated session. An external agent provider owns the agent loop and DigitalOcean supplies the sandbox. Use config to configure the session. See Agent Adapters and Run an OpenAI Agents API Session. See also Connect a DigitalOcean sandbox to an Agents API session. |
langgraph |
Runs a customer LangGraph application. See Run a LangGraph Agent. |
hermes |
Runs the Hermes gateway as a long-lived agent with an admin dashboard on loopback port 9119. See Run a Hermes Agent. |
custom |
Runs a container image you supply. Requires image and entrypoint. |
The deprecated alias codex-cli is still accepted and resolves to codex, with a warning.
config for codex-agentapi
Use config only with agent: codex-agentapi. DigitalOcean forwards it as the OpenAI Agents API create-session body, stores and echoes it, and does not validate its contents.
CLI and webhook-managed spec
Include the OpenAI create-session request in config. The CLI authenticates that request with OPENAI_API_KEY, fills ${ENV_ID} from the response, and injects the executor key as CODEX_API_KEY:
name: openai-codex-session
agent: codex-agentapi
config:
agent:
model: gpt-5.6-sol
instructions: Work from the files in /workspace.
environment:
type: self_hosted
workspace_directory: /workspace
egress:
- api.openai.com
- codex-cloud-environments.chatgpt.com
env:
CODEX_ENVIRONMENT_ID: ${ENV_ID}
secrets:
CODEX_API_KEY: ${OPENAI_EXECUTOR_API_KEY}Application-managed spec
When the application creates the OpenAI session first, omit config from the sandbox-only spec:
agent: codex-agentapi
egress:
- api.openai.com
- codex-cloud-environments.chatgpt.com
env:
CODEX_ENVIRONMENT_ID: ${ENV_ID}
secrets:
CODEX_API_KEY: ${OPENAI_EXECUTOR_API_KEY}Pass the OpenAI session ID in params.openai_session_id and supply ENV_ID and OPENAI_EXECUTOR_API_KEY through body.variables when calling client.agents.create_session.
Provider-specific fields
| Field | Description |
|---|---|
agent |
Set to codex-agentapi. Selects the DigitalOcean image that includes Codex and starts codex exec-server. |
config |
OpenAI create-session request for CLI and webhook-managed flows. Application-managed flows can omit it. |
config.agent |
OpenAI model and agent instructions. |
config.environment.type |
Set to self_hosted. |
config.environment.workspace_directory |
Set to /workspace. |
env.CODEX_ENVIRONMENT_ID |
OpenAI environment ID. The CLI fills ${ENV_ID} after creating the OpenAI session. Applications and webhook controllers supply it through the DigitalOcean create request. |
secrets.CODEX_API_KEY |
OpenAI environment key. Supply OPENAI_EXECUTOR_API_KEY, not the key used for other OpenAI API operations. |
egress |
The platform includes the OpenAI executor hosts for this adapter. Spec examples often list api.openai.com and codex-cloud-environments.chatgpt.com for clarity. Add destinations required by the agent’s tools. |
Use OPENAI_API_KEY for OpenAI API requests made by the application, controller, or CLI. Use a separate OPENAI_EXECUTOR_API_KEY environment key for the executor connection. Both keys must have the same owner, organization, and project. Place the executor key under secrets, not env, and do not log or save the resolved manifest. See Secrets and Configuration.
mode and serving
mode is the lifecycle discriminator.
| Value | Description |
|---|---|
interactive |
Default. One session to one sandbox. serving is not allowed. |
served |
Automatically scaled fleet. serving is required. |
When mode: served, the serving object takes the following fields.
| Field | Type | Required | Description |
|---|---|---|---|
min |
integer | Yes | Minimum number of sandboxes to keep running. |
max |
integer | Yes | Maximum number of sandboxes to scale to. |
target_concurrency |
integer | Yes | Sessions per sandbox to target before scaling out. |
scale_to_zero.idle |
duration | No | Idle period before the fleet scales to zero. |
keep_warm
keep_warm: true exempts a session from idle auto-pause entirely. It exists because idle_timeout cannot express “never”: omitting it means “use the team default” and 0 means the same, so no duration disables the timer.
Declaring both idle_timeout and keep_warm is rejected rather than resolved in favor of one, because the timeout would otherwise silently never elapse.
Omitting keep_warm leaves the choice to the adapter. Adapters whose value is an always-available interactive surface default to staying warm, such as hermes, whose dashboard is reachable through port forwarding only while the sandbox runs. Every other adapter defaults to pausing on idle. Setting keep_warm: false opts a hermes session back into pausing.
A warm session bills for its sandbox until you stop it explicitly, and a request to pause one returns a 409. The policy applies to manual pause as well as the automatic path.
agent: hermes
keep_warm: trueegress
egress requests the session’s outbound network policy. Omitting it leaves the session unrestricted. Setting an allowlist turns it on, and everything unnamed is then denied. The field takes exactly one of the following forms.
| Form | Example | Meaning |
|---|---|---|
| Omitted | no egress field |
Unrestricted. No platform defaults are added. |
| Scalar | egress: unrestricted |
The same, stated explicitly. Cannot be combined with an allowlist or a VPC. |
| List | egress: [pypi.org, api.github.com] |
Hostname allowlist. Put IP literals in the object form instead. |
| Object | the fields below | Allowlist and VPC path. |
The object form takes the following fields.
| Field | Type | Required | Description |
|---|---|---|---|
allow_hosts |
list | No | Hostnames the sandbox may reach. Naming any host turns the allowlist on. |
allow_ips |
list | No | Exact IPv4 or IPv6 literals, not CIDR ranges. Matches IP-addressed traffic only, so a hostname resolving to an allowed IP is still denied unless it is in allow_hosts. |
vpc_uuid |
string | No | Send egress through this VPC instead of public NAT. Your team must own it, and it must be in a supported region. Cannot change after the session is created. |
subnet_uuid |
string | No | Subnet within vpc_uuid. Rejected without vpc_uuid. |
When the allowlist is on, the platform merges in the hosts the session needs to function: platform hosts such as api.github.com, github.com, and registry.digitalocean.com, the hosts the adapter requires, and the configured model inference endpoints. Omitting egress adds none of these, because there is no allowlist to add to. An allowlist built only from allow_ips also does not receive them, and session creation returns a warning in that case.
The request is not the final policy. The platform intersects it with team and platform policy, which can narrow what you ask for but never widens it.
Inline MCP hosts are not reliably added to the allowlist. List them under egress as well. Tools reached through Action Gateway need no entry, because the gateway calls the provider rather than the sandbox doing so. For the wider model, see Network Egress.
env
Non-secret plaintext config merged into the guest environment. The guest environment is debug-readable, so use secrets for credentials.
| Rule | Behavior |
|---|---|
| Reserved platform keys | Rejected at parse, including TEAM_ID, SESSION_ID, SANDBOX_ID, and related sandbox control keys. |
| Credential-shaped key names | Keys such as API_KEY, TOKEN, and SECRET are rejected on durable config create. Declare them under secrets. |
| Credential-shaped values | Values carrying a known credential prefix are rejected on durable config create. |
secrets
Every credential the agent needs, declared in one map. The slot name is the guest environment variable. doctl expands ${VAR} placeholders client-side before upload, and an unexpanded ${VAR} or $VAR value is rejected.
secrets:
OPENAI_API_KEY: ${OPENAI_API_KEY}
DATASETS_ACCESS_KEY:
value: ${SPACES_ACCESS_KEY}
env: false
DATASETS_SECRET_KEY:
value: ${SPACES_SECRET_KEY}
env: falseA slot value takes one of the following forms.
| Form | Description |
|---|---|
| String | Write-only tenant secret. DigitalOcean extracts it at create time and never persists it in the stored manifest, returns it on GET, or captures it in an audit snapshot. |
| Object | Long form of the shorthand. See the fields below. |
The object form takes the following fields.
| Field | Type | Required | Description |
|---|---|---|---|
value |
string | Yes | The credential, usually a ${VAR} placeholder. |
env |
boolean | No | Defaults to true. Set false to keep the slot mount-only and out of the guest environment. Mount credentials must use env: false. |
url |
string | No | Binds the credential to one HTTPS destination, making the slot a scoped secret. The sandbox receives an authorization handle under the slot name instead of the credential, and the egress proxy attaches the real credential to requests for this destination. |
secrets:
PROVIDER_API_KEY:
value: ${PROVIDER_API_KEY}
url: https://api.example.comurl accepts https:// with one exact hostname and no port, query, fragment, or embedded credentials. Wildcards, IP literals, localhost, and metadata addresses are rejected, and a hostname matches only itself rather than its subdomains. A path is optional, is treated as a prefix, must start with /, must stay under 256 characters, and cannot contain ?, #, or %.
An environment can declare at most 16 scoped secrets. A slot cannot combine url with env: false, because the handle has to reach the guest environment. The bound host is added to the egress allowlist automatically.
Redemption depends on infrastructure support that is not yet enabled everywhere. Where it is missing, the sandbox still receives a handle rather than the credential, and requests to the bound host fail to authenticate. See Secrets and Configuration.
For guidance on choosing between env and secrets, see Secrets and Configuration.
permissions
The permission policy for the environment. It applies when the session starts and is checked against what the chosen adapter supports. If you omit permissions, the platform uses its built-in default. For native Harness Runtime actions, the last matching rule in the list wins, and if no rule matches, the action uses default.
A rule naming a tool the adapter does not declare is not silently ignored. Under the default strict enforcement a deny or ask rule on an undeclared tool fails session create, while an allow rule degrades to a warning. This matters most with codex, which declares only bash. Two parts of the taxonomy are currently declared by no adapter at all, so a rule naming them can only ever warn or block: git.* and web.fetch. Set enforcement: best-effort on a rule you want downgraded to a warning rather than treated as a create-time error.
| Field | Type | Required | Description |
|---|---|---|---|
default |
string | No | allow, ask, or deny. Defaults to ask. |
rules[].tool |
string | Yes | Harness taxonomy (bash, file.read, file.write, web.fetch, git.*, mcp, custom) or a server-qualified MCP target (<server>/<tool> or <server>/toolbelt:<name>[@<int>]). The server must be declared in tools. |
rules[].match |
map | No | Matcher map, for example command: "rm -rf *". Values must not contain $VAR expansions. |
rules[].action |
string | Yes | allow, ask, or deny. |
rules[].enforcement |
string | No | strict or best-effort. Defaults to strict for deny and best-effort otherwise. For gateway-routed MCP rules, enforcement is always strict. |
filesystem.mode |
string | No | read-only or workspace-write. |
filesystem.allow_write |
list | No | Extra writable paths. |
network.default / network.allow |
string / list | No | Narrows within egress, never widens it. |
Action Gateway evaluates its projected tool rules separately, using most-specific matching rather than list order. An ask decision there requires the client to complete MCP elicitation or a separate approval request, so do not assume every adapter supports an inline prompt. See Permission Policies and Action Gateway Tool Policies.
tools
Requests MCP server attachments. Each list entry is exactly one of two forms.
| Form | Description |
|---|---|
| Catalog | - do.actions for all tools, or - do.actions: [tool, ...] for a selection. The do.* namespace is reserved for DigitalOcean-owned servers, and do.actions is Action Gateway. Selection entries are individual tool names with an optional integer version pin (exa_web_search or exa_web_search@2) or toolbelt:<name>[@<int>] group references. toolbelt references are valid on catalog entries only. |
| Inline | A name plus an https:// url, for an MCP server you host. See the fields below. |
An inline entry takes the following fields.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | 1-64 characters of lowercase letters, digits, -, ., or _, with alphanumeric ends. Must not start with do.. |
url |
string | Yes | An https:// URL. The host is not auto-allowed, so list it under egress. |
tools |
list | No | Tool selection for this server. Accepted and returned with a warning, but it does not yet restrict what the server advertises. |
auth.secret |
string | No | Names a declared, env-visible secrets slot. A slot with env: false is rejected. |
auth.header |
string | No | Defaults to Authorization. |
tools:
- do.actions: [exa_web_search, action_code, toolbelt:read-only@1]
- name: internal-jira
url: https://mcp.example.com/mcp
tools: [create_issue]
auth:
header: Authorization
secret: JIRA_MCP_TOKENAttaching a tool does not grant permission to call it. Allow, ask, and deny dispositions stay in permissions. See Connect Action Gateway to Harness Runtime.
skills
Inline agent skills in the Agent Skills (SKILL.md) shape. A spec accepts a maximum of 32 entries. instructions are capped at 32 KiB per skill and 256 KiB total across the list, and the encoded guest environment value must also fit the 64 KiB variable cap. workspace-repos and sandbox-access are platform-reserved names.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Unique. 1-64 characters of lowercase letters, digits, and single hyphens. |
description |
string | Yes | Load-bearing selection text that tells the agent when to load the skill. Write it as “Use when …”. Maximum 1024 characters. |
instructions |
string | Yes | Markdown body. |
license |
string | No | Standard passthrough. |
metadata |
map | No | Free-form map rendered into front matter. |
allowed_tools |
list | No | Advisory scoping only. Enforcement stays in permissions. |