> ## Documentation Index
> Fetch the complete documentation index at: https://docs.danubeai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Data Plane

> Run tool execution inside your own network so resolved secrets, and optionally results, never reach Danube

The data-plane agent is a small container that runs **inside your network** and executes tools for your organization's `local_only` services. Danube's control plane keeps doing policy, routing, metering and audit, but it never makes the credentialed call and never receives your secrets. It queues an un-credentialed dispatch (tool definition, parameters and credential *references*); the agent resolves the references locally, makes the call and reports the outcome back.

The data plane is included with the Team and Enterprise plans. An owner or admin of an organization on either plan can enroll an agent from the dashboard; nothing needs to be enabled for you.

## Quick start

<Steps>
  <Step title="Generate an enrollment token">
    Go to [Dashboard > Systems > Data plane](https://danubeai.com/dashboard/organization/systems?tab=dataplane) and click **Generate enrollment token**. Tokens are valid for one hour and single use: two agents started with the same token yield one enrolled agent and one refused. Admins and owners only.
  </Step>

  <Step title="Run the agent">
    ```bash theme={null}
    docker run -d --name danube-dataplane --restart unless-stopped \
      -v danube-dataplane-data:/data \
      -e DANUBE_ENROLL_TOKEN=<your-enroll-token> \
      -e DANUBE_API_URL=https://api.danubeai.com \
      ghcr.io/danubeai/dataplane-agent:latest
    ```

    The image is published to `ghcr.io/danubeai/dataplane-agent` with three kinds of tag: `latest`, the agent version (for example `0.4.0`) and `sha-<short>` for the exact commit. Pin the version tag in production.

    On first boot the agent exchanges the token for a durable identity (an agent id and secret) and writes it to the `/data` volume. Restarts reuse it and the token is not needed again, which is why the volume and the restart policy are in the command: without the volume a restart would enroll a new agent every time.
  </Step>

  <Step title="Confirm it is online">
    The agent appears in the Data Plane page within a few seconds of its first poll, with the version it is running and an **Online** badge. An agent that stops polling for 90 seconds shows **Offline**; calls to the organization's `local_only` tools then fail at once with `no_dataplane_agent` instead of waiting for the dispatch budget. Revoke an agent from the same page at any time; a revoked agent is rejected on its next request.
  </Step>

  <Step title="Store credentials as references">
    Data-plane execution needs credentials stored as references, not values: `env://VAR_NAME` for an environment variable on the agent's host, or `vault://<mount>/<path>#<field>` for HashiCorp Vault (KV v2). A credential held only as a value in Danube is never shipped to the agent; the dispatch fails with `credential_reference_unresolved` telling you to re-store it as a reference.
  </Step>
</Steps>

## Configuration

| Variable | Default | Purpose |
| - | - | - |
| `DANUBE_API_URL` | `https://api.danubeai.com` | Danube control-plane base URL |
| `DANUBE_ENROLL_TOKEN` | | One-time enrollment token (first boot only) |
| `DANUBE_AGENT_STATE_PATH` | `/data/agent-state.json` | Persisted identity, audit sequence and retry buffer |
| `DANUBE_AGENT_NAME` | | Display name shown in the dashboard |
| `DANUBE_ALLOWED_DESTINATIONS` | (any) | Comma-separated hosts, or host patterns, the agent may call; anything else fails with `destination_blocked` before a connection is opened |
| `DANUBE_MAX_CONCURRENCY` | `4` | Dispatches one agent runs at the same time |
| `DANUBE_BUDGET_STALENESS_SECONDS` | `900` | Fail-closed threshold for the local spend snapshot |
| `DANUBE_HEALTH_MAX_AGE_SECONDS` | `120` | The container health check fails when the last successful poll is older than this |
| `SECRET_REDACTION_MODE` | `enforce` | `enforce` masks credential values in results before they leave the agent; `log_only` reports without masking; `off` disables the pass. The control plane masks again on receipt either way |
| `VAULT_ADDR`, `VAULT_TOKEN` | | HashiCorp Vault for `vault://` references |
| `HTTPS_PROXY`, `NO_PROXY` | | Standard proxy variables, honoured for both the control plane and the tools the agent calls |
| `SSL_CERT_FILE` | | A CA bundle that includes your private CA, for internal services with certificates it issued |

## Network

The agent makes **outbound HTTPS calls only**: to `DANUBE_API_URL` to poll for work, report results and stream audit events, and to the hosts of the tools it runs. It listens on no port, so nothing has to be opened inbound and no public address is needed.

* **Egress allowlist.** If your network restricts egress, allow `api.danubeai.com` on 443 plus the hosts of your internal services. `DANUBE_ALLOWED_DESTINATIONS` is the agent-side counterpart: it refuses a dispatch whose host is not on the list, so a tool definition edited to point elsewhere cannot make the agent call it.
* **Proxy.** Set `HTTPS_PROXY` (and `NO_PROXY` for internal hosts that should be reached directly).
* **Private CA.** Mount your CA bundle into the container and point `SSL_CERT_FILE` at it. The agent does not disable certificate verification.

## Several agents

An organization can enroll any number of agents; each enrollment uses its own token. Dispatches are claimed by whichever agent polls first, so running two or more agents gives you throughput and lets one restart without a gap. Within one agent, `DANUBE_MAX_CONCURRENCY` sets how many dispatches it runs at once; the default is four, and one is the right setting for an internal service that cannot take parallel load.

Each agent shows its own online state and version in the dashboard, and the version updates on the next poll after you deploy a new image.

## Data handling

Two organization settings, under **Organization > Settings** (owner or admin), decide what Danube keeps about data-plane calls. The same card holds **Hosted AI can use internal tools**, which is about prompts rather than storage and is described under [Organizations](/organizations/overview#hosted-ai-and-internal-tools); the audit log retention sits in the same card and is described under [Audit Log](/organizations/audit-log#retention).

* **Store execution payloads.** On by default. Off is residency mode: the request parameters and the result of a data-plane call are never written to Danube's database. The dispatch row keeps the request only until an agent has claimed it (it is the transport queue) and then holds `{"stored": false}`; the result is recorded as `{"stored": false, "sha256": ..., "bytes": n}`, a hash of the canonical JSON and its size, and the execution log stores the same digest for both the request and the result. The caller that made the request still receives the full result; only Danube's copy is withheld. Status, timing, the error text and the error type are kept either way, since the dashboard and the reliability numbers read them.
* **Keep execution history.** Indefinitely, or 30, 90 or 365 days. With a retention set, a nightly job deletes the organization's execution rows older than that, in batches, so a large backlog is drained over several nights rather than in one long transaction.

Dispatch rows themselves are purged hourly once they are a week past completion, and the replay-protection nonces after a day, regardless of these settings.

## How failures are attributed

Every failed call carries an `error_type` and a `fault` (`caller`, `upstream` or `danube`), on the data plane like everywhere else. The agent classifies what it saw and the control plane passes that through unchanged, so a `404` from your internal service is `not_found` and a rejected body is `bad_request`, not a generic data-plane error.

| `error_type` | `fault` | Meaning |
| - | - | - |
| `no_dataplane_agent` | caller | No agent of the organization has polled in the last two minutes. Start one and retry |
| `destination_blocked` | caller | The host is outside `DANUBE_ALLOWED_DESTINATIONS` |
| `credential_reference_unresolved` | caller | An `env://` or `vault://` reference could not be resolved on the agent's host |
| `usage_limit_reached` | caller | The agent's local copy of the API key's spend cap refused the call. The organization's own [calls-per-minute and daily spend caps](/organizations/policies#organization-caps) refuse before a dispatch is queued, with the same `error_type` as an HTTP `429` or `403` |
| `bad_request`, `auth_required`, `not_found`, `conflict`, `rate_limited` | caller or upstream, as for any HTTP tool | What the internal service answered |
| `server_error`, `connection_error`, `timeout` | upstream | The internal service failed or could not be reached from the agent |
| `dataplane_timeout` | upstream | The agent claimed the dispatch but did not report within the dispatch budget |
| `dataplane_execution_failed` | upstream | The agent reported a failure without saying which kind (older agent builds) |
| `upstream_rejected` | caller | An MCP server ran the call and answered `isError` (see MCP servers below) |
| `upstream_tool_missing` | danube | An MCP server no longer offers the tool the catalog row points at; refresh the service's tools |
| `mcp_transport_error` | upstream | An MCP server was reachable but did not speak the protocol (no JSON-RPC reply, an unexpected body) |
| `dataplane_unsupported_service` | danube | A `local_only` service of a type the agent cannot run (see limits) |

Only `upstream` failures move a tool's reliability score. A `caller` failure, an offline agent included, is reported next to it and never pushes agents away from a working tool.

## MCP servers

A remote MCP server inside your network can be registered as `local_only` like an HTTP service: pick **Runs inside your network** in Create Tools, or send `{"source": "mcp", "visibility": "local_only", "mcp": {"server_url": "http://mcp.internal:8080/mcp", "transport": "http", "auth_type": "bearer"}}` to `POST /v1/organizations/{org_id}/services`. Danube never connects to the server, so the address does not have to be reachable from the internet and is not checked against the outbound-address policy; the agent's `DANUBE_ALLOWED_DESTINATIONS` is what gates it.

* **Transports.** Streamable HTTP (`transport: "http"`; the agent keeps the server's `Mcp-Session-Id` and ends the session when the call is done) and the older HTTP+SSE transport (`transport: "sse"`; the agent opens the event stream, posts messages to the endpoint the server names and reads replies off the stream). The agent initializes with protocol version `2025-06-18` and falls back to `2024-11-05` when the server refuses it. A local (stdio) MCP server, a subprocess rather than a URL, cannot be registered for an organization.
* **Authentication.** `auth_type` is `none`, `bearer` (`Authorization: Bearer`), `api_key` (the header named by `auth_config.header_name`, `X-API-Key` by default) or `basic`. The credential is stored as a reference like any other data-plane credential: `env://MCP_TOKEN` or a `vault://` path in the `value` field (`username` and `password` for basic). The agent resolves it and builds the header; the control plane ships the auth type and the header name only. A `{api_key}` placeholder in the server URL is filled from the reference too, with no header sent.
* **Tool discovery runs through the agent.** Right after registration, and again from **Refresh tools** on the service page, Danube queues a `tools/list` dispatch instead of connecting itself. The service shows **Discovering tools** while it runs. With no agent online the sync records `no_dataplane_agent` and the service page says so; start an agent and refresh. The daily catalog reconcile that keeps public MCP services current does not touch organization services, so Refresh tools is how a renamed or added tool reaches the catalog.
* **Results.** A call returns the MCP result envelope (`content`, `structuredContent`, `isError`) as the tool's `result`, redacted by the agent before it leaves your network. An `isError` result is reported as `upstream_rejected` with the server's text; a tool the server no longer offers is `upstream_tool_missing`.

## Security model

* **Secrets never leave your network.** Dispatches carry credential references only. The agent resolves them in-process at call time.
* **Signed requests.** Every agent request is authenticated with HMAC-SHA256 over the method, path, timestamp, nonce and body hash, using the agent secret. Requests with more than 300 seconds of clock skew, or a reused nonce, are rejected.
* **Revocation.** An admin can revoke an agent at any time.
* **Spend limits fail closed.** The agent syncs your organization's API key spend caps and enforces them before each call. If its snapshot is older than the staleness threshold and cannot be refreshed, execution is denied, not allowed.
* **Audit stream.** Every execution and denial is sent to the [audit log](/organizations/audit-log) with a per-agent sequence number, buffered and retried, and ingested idempotently. The control plane writes its own `tool.execute` row for every reported result (`actor_type: dataplane`, with `duration_ms`, `spend_cents` and `payloads_stored` in the metadata). A result that arrives after the control plane has already expired the dispatch is accepted and audited as a late result rather than treated as an error.
* **Results are masked twice.** The agent masks credential values in a result before it leaves your network; the control plane runs the same pass on receipt in case an older agent did not.

## Limits in this release

* **Remote servers only.** HTTP tools and remote MCP servers (streamable HTTP or SSE) run over the data plane; a local (stdio) MCP server does not, since it is a subprocess rather than an address. OAuth-authenticated services are not supported over the data plane either, since the token refresh runs in the control plane; an OAuth-protected MCP server needs a token stored as a reference.
* **60 second dispatch budget.** The control plane waits 60 seconds for an agent to report; a longer call is marked `dataplane_timeout` and its late result is recorded in the audit log but not returned.
* Supported credential injection: bearer or HTTP auth header, API key header, API key query parameter; for MCP servers bearer, API key header and basic.

## Operations

* Logs go to stdout: `docker logs danube-dataplane`.
* The image carries a health check that fails when the agent has not completed a poll in `DANUBE_HEALTH_MAX_AGE_SECONDS`, so an orchestrator restarts a wedged agent on its own.
* Graceful shutdown on `SIGTERM` or `SIGINT`: in-flight dispatches finish and the audit buffer is flushed.
* Network errors back off exponentially, from 1 second to a 60 second cap.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.