---
title: Environment Spec Reference (public)
description: Field reference for the YAML environment specification, covering the adapter, sandbox, credentials, egress, tools, skills, and permission fields.
product: Managed Agents
url: https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/reference/environment-spec/
last_updated: "2026-09-23"
---

> **For AI agents:** The documentation index is at [https://docs.digitalocean.com/llms.txt](https://docs.digitalocean.com/llms.txt). Markdown versions of pages use the same URL with `index.html.md` in place of the HTML page (for example, append `index.html.md` to the directory path instead of opening the HTML document).

# Environment Spec Reference (public)

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](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/details/what-you-can-build/index.html.md) for example use cases.

As an alternative to [configuring your environment in the Control Panel](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/how-to/create-managed-agent/index.html.md#create-an-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.

```yaml
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: interactive
```

## Top-Level Fields

The environment specification has the following top-level fields.

| Field | Type | Required | Description |
|---|---|---|---|
| `agent` | string | Yes | Adapter identifier. See [`agent`](#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`](#env). |
| `secrets` | map | No | Declared credentials. See [`secrets`](#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](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/details/features/index.html.md#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`](#keep_warm). |
| `egress` | string, list, or object | No | Outbound network policy request. See [`egress`](#egress). |
| `tools` | list | No | MCP server attachments. See [`tools`](#tools). |
| `permissions` | object | No | Tool-permission policy. See [`permissions`](#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`](#skills). |
| `mode` / `serving` | string / object | No | Lifecycle mode. See [`mode` and `serving`](#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](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/concepts/agent-adapters/index.html.md).

| 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](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/concepts/agent-adapters/index.html.md#openai-agents-api-ownership) and [Run an OpenAI Agents API Session](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/how-to/run-openai-agents-api-session/index.html.md). See also [Connect a DigitalOcean sandbox to an Agents API session](https://developers.openai.com/api/docs/guides/agents-api/environments/providers/digitalocean). |
| `langgraph` | Runs a customer LangGraph application. See [Run a LangGraph Agent](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/how-to/run-langgraph-agent/index.html.md). |
| `hermes` | Runs the Hermes gateway as a long-lived agent with an admin dashboard on loopback port `9119`. See [Run a Hermes Agent](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/how-to/run-hermes-agent/index.html.md). |
| `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`:

```yaml
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:

```yaml
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](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/concepts/secrets/index.html.md#scoping-credentials).

### `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.

```yaml
agent: hermes
keep_warm: true
```

### `egress`

`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](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/concepts/egress/index.html.md).

### `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.

```yaml
secrets:
  OPENAI_API_KEY: ${OPENAI_API_KEY}
  DATASETS_ACCESS_KEY:
    value: ${SPACES_ACCESS_KEY}
    env: false
  DATASETS_SECRET_KEY:
    value: ${SPACES_SECRET_KEY}
    env: false
```

A 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. |

```yaml
secrets:
  PROVIDER_API_KEY:
    value: ${PROVIDER_API_KEY}
    url: https://api.example.com
```

`url` 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](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/concepts/secrets/index.html.md#scoped-secrets).

For guidance on choosing between `env` and `secrets`, see [Secrets and Configuration](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/concepts/secrets/index.html.md).

### `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](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/concepts/permissions/index.html.md) and [Action Gateway Tool Policies](https://docs.digitalocean.com/products/managed-agents/action-gateway/concepts/tool-policies/index.html.md).

### `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`. |

```yaml
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_TOKEN
```

Attaching a tool does not grant permission to call it. Allow, ask, and deny dispositions stay in `permissions`. See [Connect Action Gateway to Harness Runtime](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/how-to/connect-action-gateway/index.html.md).

### `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`. |