Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions docs/design/agent-workflows/documentation/running-the-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,19 @@ env file points the chat slice at
These are the agent-relevant variables. The example file lists them commented out
(`hosting/docker-compose/ee/env.ee.dev.example`, lines 119 onward).

- `AGENTA_AGENT_RUNNER_URL`. Where the Python service finds the runner. Default
`http://sandbox-agent:8765`. When unset, the Python service spawns the runner CLI locally
instead (see `runner_url` and `select_backend` in `services/oss/src/agent/`).
- `AGENTA_AGENT_RUNNER_URL`. Where the Python service finds the runner when no per-run `uri`
override is set. Default `http://sandbox-agent:8765`. When unset, the Python service spawns the
runner CLI locally instead (see `runner_url` and `select_backend` in `services/oss/src/agent/`).
- `AGENTA_AGENT_RUNNER_URI_ALLOWLIST`. Comma-separated list of trusted sidecar origins
(`scheme://host[:port]`). The agent config's optional `uri` field is honored only when its
origin is on this list; **default empty means every override is rejected** (the feature ships
off, only `AGENTA_AGENT_RUNNER_URL` / the local CLI work). The gate exists because the service
ships resolved provider keys and bearer tokens to whatever address it picks, so a
caller-supplied address is an SSRF / secret-exfiltration risk. See `validate_runner_uri` in
`services/oss/src/agent/config.py`.
- `AGENTA_AGENT_ENABLE_MCP`. Gates MCP server resolution. Default `false`.
- `SANDBOX_AGENT_PROVIDER`. `local` or `daytona`. Default `local`.
- `SANDBOX_AGENT_PROVIDER`. `local` or `daytona`. Default `local`. Each sidecar picks its own
sandbox provider from this env; there is no per-run sandbox selector on the wire.
- `SANDBOX_AGENT_DAYTONA_API_KEY`, `_API_URL`, `_TARGET`, `_SNAPSHOT`, `_IMAGE`,
`_INSTALL_PI`. Daytona credentials the runner reads for the `daytona` sandbox provider.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ group by job:
{
// agent + placement
"harness": "pi_core", // "pi_core" | "pi_agenta" | "claude"; selects the ACP agent (no engine selector)
"sandbox": "local", // "local" | "daytona"
"sandbox": "local", // constant default; the sidecar picks its own sandbox provider from its env (no per-run selector). The service routes to the sidecar by the config's `uri` instead.
"sessionId": "sess_ab12...", // external id; cold runtime still gets full history

// instructions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ the order it runs in is itself a contract.
The handler (`_agent` in `app.py`) takes the workflow envelope's pieces:

- `parameters`: carries the agent config under `agent`. The run-selection fields (`harness`,
`sandbox`, `permission_policy`) live on that same `agent` object.
`uri`, `permission_policy`) live on that same `agent` object.
- `messages` or `inputs.messages`: the turn history (it checks `messages` first).
- `stream`: batch versus streaming.
- `session_id`: the external conversation id.

## What it does, in order

1. Parse the config: `AgentConfig.from_params(params, defaults=...)`. One parse covers
everything, including the run-selection fields (`harness`, `sandbox`, `permission_policy`).
everything, including the run-selection fields (`harness`, `uri`, `permission_policy`).
2. Convert the request messages to neutral `Message[]`.
3. Resolve tools into builtin names, runnable specs, and a tool callback.
4. Resolve MCP servers.
Expand All @@ -28,7 +28,11 @@ The handler (`_agent` in `app.py`) takes the workflow envelope's pieces:
connections degrade tolerantly to an empty `env` rather than failing the run.
6. Build one `SessionConfig` carrying all of the above plus trace context and session id.
7. Select the backend (`SandboxAgentBackend`) and make the harness, which validates that the
harness is supported.
harness is supported. `select_backend` routes by the config's `uri`: routing precedence is
the config's `uri` (validated against the server-side allowlist) -> `AGENTA_AGENT_RUNNER_URL`
-> the local runner CLI. A set-but-disallowed `uri` fails loud (no silent fallback). The
sidecar at the resolved address is configured local-or-Daytona by its own env, so the
service sends a constant `sandbox` default on the wire rather than a per-run selector.
8. Run: stream Vercel parts, or await one batch turn that returns
`{"role": "assistant", "content": result.output}`.
9. Record usage.
Expand Down Expand Up @@ -64,3 +68,8 @@ the instrumented handler and merges the registered interface (the passed `schema
The tolerant default path is deliberate. Reordering can turn a clean reject into a leak or
a hard failure.
- **Batch versus streaming.** Two execution paths return two shapes. Keep them in sync.
- **Sidecar routing and the allowlist.** A caller-supplied `uri` controls where the service
ships resolved secrets and bearer tokens, so it is honored only when its origin is on
`AGENTA_AGENT_RUNNER_URI_ALLOWLIST` (default empty = every override rejected, feature off).
Loosening the gate is a security change. `resolve_runner_url` / `validate_runner_uri` live in
`services/oss/src/agent/config.py`.
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,17 @@ The fields and the full schema follow.
| `tools` | `ToolConfig[]` | `[]` | Runnable tools: `builtin`, `gateway`, `code`, or `client`. See [Tool models and resolution](../in-service/tool-models-and-resolution.md). |
| `mcp_servers` | `MCPServerConfig[]` | `[]` | Declared MCP servers; secret env resolved from the vault at run time. See [MCP models and resolution](../in-service/mcp-models-and-resolution.md). |
| `harness` | `"pi_core" \| "claude" \| "pi_agenta"` (see slug+name note) | `"pi_core"` | The coding agent to drive. `pi_core` and `pi_agenta` both drive the `pi` ACP agent; `pi_agenta` adds Agenta's forced skills, prompt, and policy. |
| `sandbox` | `"local" \| "daytona"` | `"local"` | Where it runs. |
| `uri` | string \| null | `null` (key omitted) | Optional sidecar (agent runner) address that routes this run. When set, the server routes there (gated by `AGENTA_AGENT_RUNNER_URI_ALLOWLIST`, default empty); when unset, it falls back to its env-var runner resolution. The sidecar is configured local-or-Daytona by its own env, not per-run. Operator/testing concern, not a normal authoring field, so the playground does not surface a control for it. See [Agent service handler](../in-service/agent-service-handler.md). |
| `permission_policy` | `"auto" \| "deny"` | `"auto"` | How a gating harness (Claude Code) handles tool-use prompts in a headless run. |
| `sandbox_permission` | `SandboxPermission \| null` | `null` (form pre-fills one) | The declared network and filesystem boundary. See [Sandbox permission](../in-service/sandbox-permission.md). |
| `skills` | `(SkillConfig \| EmbedRef)[]` | one embedded default skill | Inline SKILL.md packages, or `@ag.embed` references the backend inlines before the runner sees them. |

Note that `harness`, `sandbox`, and `permission_policy` are the run-selection fields. They
Note that `harness`, `uri`, and `permission_policy` are the run-selection fields. They
live on `AgentConfig` itself, under `data.parameters.agent`, and the handler reads them in the
one `AgentConfig.from_params(...)` parse along with the rest of the config. There is one agent
config, not a config plus a separate selection object.
config, not a config plus a separate selection object. `uri` replaced the old `sandbox`
selector: the sidecar address drives routing, and each sidecar picks its own sandbox provider
(local or Daytona) from its own environment, so there is no per-run sandbox field anymore.

### Harness as a slug + display name

Expand Down Expand Up @@ -75,7 +77,6 @@ every field in its default state:
"tools": [],
"mcp_servers": [],
"harness": "pi_core",
"sandbox": "local",
"permission_policy": "auto",
"sandbox_permission": {
"network": { "mode": "on", "allowlist": [] },
Expand Down
54 changes: 54 additions & 0 deletions docs/design/agent-workflows/projects/sidecar-uri-config/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Sidecar URI in the agent config

Index for the workspace that adds an **optional `uri`** to the agent config, pointing at the
address of the sidecar (the agent runner). When set, the service routes the `/run` request to
that address (gated by a server-side allowlist); when unset, it falls back to today's
environment-variable resolution (`AGENTA_AGENT_RUNNER_URL`, else the local runner directory).

Spun out of PR [#4821](https://github.com/Agenta-AI/agenta/pull/4821) review comment
[3469613625](https://github.com/Agenta-AI/agenta/pull/4821#discussion_r3469613625):

> *"instead we should have an optional uri that points to sidecar and provide an address of
> the thing (the sandbox should probably use this uri to determine where to route the
> request). if the uri is not set then we use the environment variables"*

**Status: IMPLEMENTED.** The reviewer decided (D7) that `uri` **replaces** the `sandbox` field
outright — the sidecar address drives routing, and each sidecar picks its own sandbox provider
(local or Daytona) from its own environment. POC / pre-production, so there is **no back-compat
constraint** and no migration: the env-var path stays as the fallback purely because it is the
sensible default. Built on [#4840](https://github.com/Agenta-AI/agenta/pull/4840)
(config-structure cleanup), which retired `RunSelection` and moved the run-selection fields onto
`AgentConfig`.

## Files

- [context.md](context.md) — why this exists, the reviewer's ask, goals, non-goals.
- [research.md](research.md) — where routing is decided today, what `RunSelection` vs
`AgentConfig` carry, how `select_backend` / `runner_url()` / `runner_dir()` work, and the
exact seam a `uri` would slot into.
- [plan.md](plan.md) — the proposed change: the field, where it lives on the wire and in the
schema, how routing prefers it, the env fallback, tests, and rollout.
- [security.md](security.md) — the one real concern: a caller-supplied sidecar address. Threat
model, why it is dangerous, and the proposed restriction (server-side allowlist, default-off),
cross-linked to the sidecar-trust project.
- [status.md](status.md) — current state, decisions, and the open questions for the reviewer.

## What landed

Routing is decided in exactly one place — `select_backend(agent_config)` in
`services/oss/src/agent/app.py`, which now builds
`SandboxAgentBackend(url=resolve_runner_url(agent_config.uri), ...)`. `uri` is a **run-selection**
field on `AgentConfig` (it says *where* a run goes); it **replaced** `sandbox`. Precedence:
`uri` (validated) -> `AGENTA_AGENT_RUNNER_URL` -> the local CLI. It is **not a new `/run` wire
field**: it never crosses the service→runner boundary; it only decides which runner the service
opens that boundary to. The single load-bearing decision is the security one: a client-supplied
sidecar address is an SSRF / secret-exfiltration risk because the service ships resolved
provider keys and bearer tokens in every `/run` body, so a caller-supplied address is
**restricted server-side** by `AGENTA_AGENT_RUNNER_URI_ALLOWLIST` (default empty = feature off);
a disallowed `uri` fails loud (no silent fallback) — see [security.md](security.md).

The old per-run `sandbox` selector is gone; each sidecar picks its sandbox provider from its own
env (`SANDBOX_AGENT_PROVIDER`). The wire still carries a constant `sandbox: "local"` so the
unchanged runner defaults correctly; **removing the wire-level `sandbox` field is a deferred
runner-branch follow-up** (it would force a `protocol.ts` / golden change, out of scope for this
config/service/FE slice).
72 changes: 72 additions & 0 deletions docs/design/agent-workflows/projects/sidecar-uri-config/context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Context

## Why this exists

Today the agent service decides which runner (sidecar) to call from environment variables
alone. The agent service (`services/oss/src/agent/`) is the control plane; the Node runner
sidecar (`services/agent/`) is the execution plane. The control plane reaches the execution
plane at `POST /run`. Which sidecar it reaches is fixed at deploy time by
`AGENTA_AGENT_RUNNER_URL` (HTTP) or, when that is unset, a local TypeScript runner spawned from
`AGENTA_AGENT_RUNNER_DIR`.

The reviewer on PR #4821 asked for a per-run override: an **optional `uri`** in the agent
config that names the sidecar address, and routing that **prefers** that address when present
and **falls back** to the env vars when it is absent.

The original review thread sits on the agent-config schema doc
(`docs/design/agent-workflows/interfaces/public-edge/agent-config-schema.md`, line 27), which is
why the field is being designed as part of the config surface rather than, say, a deployment
knob.

## The reviewer's ask, verbatim

> instead we should have an optional uri that points to sidecar and provide an address of the
> thing (the sandbox should probably use this uri to determine where to route the request). if
> the uri is not set then we use the environment variables

Two parts:

1. **An optional `uri`** in the config that gives the sidecar address.
2. **Routing uses it** to decide where `/run` goes; **unset → env-var resolution** as today.

## What "the config" means here, precisely

The agent surface has two distinct config models (see the agent-config-schema interface doc):

- **`AgentConfig`** (neutral) — *what the agent is*: instructions, model, tools, MCP servers,
skills, harness options. It is harness-agnostic and is the thing that would, one day, be
stored as a versioned artifact.
- **`RunSelection`** — *where and how a run executes*: `harness`, `sandbox`,
`permission_policy`. The handler reads it from the same `parameters` object but keeps it
deliberately out of the neutral `AgentConfig`.

A sidecar address is a **routing** fact (where the run goes), exactly like `sandbox`. So this
design places `uri` in `RunSelection`, not in `AgentConfig`. The field is surfaced on the
playground-facing `AgentConfigSchema`, which already carries the run-selection trio
(`harness`/`sandbox`/`permission_policy`) for editing. See [research.md](research.md) for the
evidence and [plan.md](plan.md) for the exact placement.

## Goals

- Let a run name its sidecar address and have routing honor it.
- Keep the env-var path as the fallback when `uri` is unset (the default, unchanged).
- Make the change in the single routing seam (`select_backend`) without touching the
service→runner `/run` wire contract or the golden fixtures.
- Decide and document the security posture of a caller-supplied address before any code lands.

## Non-goals

- **No `/run` wire field.** `uri` decides which runner the service opens the boundary to; it
does not cross that boundary. The runner never receives it. The golden wire fixtures stay
byte-identical.
- **No back-compat machinery.** Pre-production; the env-var fallback is kept because it is the
right default, not for compatibility.
- **No new transport.** Reuses the existing HTTP delivery (`deliver_http` /
`deliver_http_stream`). A `uri` only changes the URL, not how bytes move.
- **No sidecar trust/transport hardening here.** mTLS, a `/run` shared token, scoped tokens —
those belong to the [sidecar-trust-and-sandbox-enforcement](../sidecar-trust-and-sandbox-enforcement/README.md)
project. This project only adds the *address selection* and the *restriction* that a
caller-supplied address requires (see [security.md](security.md)).
- **No deployment-shape changes.** The env contract, Compose/Helm/Railway wiring, and the
`sandbox-agent` naming are the [sidecar-deployment-proposal](../sidecar-deployment-proposal/README.md)
project's scope. This `uri` is the per-run override layered *on top* of that default contract.
Loading
Loading