Skip to content
Merged
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
28 changes: 27 additions & 1 deletion deploy/compose/compose.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Release template. The publisher pins the initialization image to this release.
# Core and Web default to latest; OAC_IMAGE_* selects another reference.
# Core, Web and the agent host default to latest; OAC_IMAGE_* selects another reference.
# Docker owns data permissions on every host platform.
x-ingress-image: &ingress-image ${OAC_IMAGE_INGRESS:-__OAC_INIT_IMAGE__}
services:
Expand Down Expand Up @@ -54,6 +54,7 @@ services:
environment:
OAC_PUBLIC_URL: &public-url ${OAC_PUBLIC_URL:-http://localhost:8080}
OAC_INSTALLATION_ID_FILE: /run/oac/installation.id
OAC_AGENT_HOST_IDENTITY_FILE: &agent-host-identity /run/agent-host/identity.json
OAC_DATABASE_URL: postgres://agents_api@database:5432/agents_api?sslmode=disable
OAC_DATABASE_PASSWORD_FILE: /run/database/password
OAC_CREDENTIAL_KEY_FILE: /run/oac/credential.key
Expand All @@ -78,11 +79,36 @@ services:
target: /run/database
volume: {nocopy: true, subpath: secrets/database}
read_only: true
- &agent-host-secrets
type: volume
source: data
target: /run/agent-host
volume: {nocopy: true, subpath: secrets/agent-host}
read_only: true
- type: volume
source: data
target: /state
volume: {nocopy: true, subpath: state}

# The agent-host container's flags are in docs/configuration.md. It shares
# Core's network namespace and reaches Core on its loopback listener; the
# Session homes in its state directory outlive the container.
agent-host:
image: ${OAC_IMAGE_AGENT_HOST:-ghcr.io/minimax-ai/openagentcore/agent-host:latest}
restart: unless-stopped
network_mode: service:core
cgroup: private
cap_add: [SYS_ADMIN, NET_ADMIN]
devices: [/dev/fuse]
security_opt: [apparmor=unconfined]
command: [agent-host, --identity-file, *agent-host-identity, --core-url, "http://127.0.0.1:8091"]
volumes:
- *agent-host-secrets
- type: volume
source: data
target: /var/lib/oac/agent-host
volume: {nocopy: true, subpath: agent-host}

web:
ports:
- target: 8080
Expand Down
23 changes: 19 additions & 4 deletions deploy/compose/test_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def render(cls, public_url=None):
env.pop('OAC_PUBLIC_URL', None)
env.pop('OAC_HOST', None)
env.pop('OAC_WEB_PORT', None)
for name in ('OAC_IMAGE_CORE', 'OAC_IMAGE_WEB', 'OAC_IMAGE_INGRESS'):
for name in ('OAC_IMAGE_CORE', 'OAC_IMAGE_WEB', 'OAC_IMAGE_INGRESS', 'OAC_IMAGE_AGENT_HOST'):
env.pop(name, None)
env['OAC_DATA_DIR'] = '/tmp/oac-compose-fixture'
if public_url is not None:
Expand All @@ -53,7 +53,7 @@ def test_compose_uses_private_services_and_ordered_initialization(self):
self.assertEqual(services['database']['depends_on']['init']['condition'], 'service_completed_successfully')
self.assertIn('pg_isready -h 127.0.0.1', services['database']['healthcheck']['test'][1])
self.assertEqual(services['core']['depends_on']['database']['condition'], 'service_healthy')
self.assertEqual(sorted(services), ['core', 'database', 'init', 'web'])
self.assertEqual(sorted(services), ['agent-host', 'core', 'database', 'init', 'web'])
for service in services.values():
self.assertNotIn('build', service)
if service is not services['web']:
Expand All @@ -65,6 +65,19 @@ def test_compose_uses_private_services_and_ordered_initialization(self):
self.assertEqual(volume['source'], 'data')
self.assertNotIn('platform', service)
self.assertEqual({v['target'] for v in services['web']['volumes']}, {'/run/oac', '/node-payload'})
agent_host = services['agent-host']
self.assertEqual(agent_host['network_mode'], 'service:core')
self.assertEqual((agent_host['cgroup'], sorted(agent_host['cap_add']), agent_host['security_opt']),
('private', ['NET_ADMIN', 'SYS_ADMIN'], ['apparmor=unconfined']))
self.assertEqual([device['source'] for device in agent_host['devices']], ['/dev/fuse'])
self.assertEqual(agent_host['command'], ['agent-host', '--identity-file', '/run/agent-host/identity.json',
'--core-url', 'http://127.0.0.1:8091'])
def mounts(name):
return [(v['target'], v['volume']['subpath'], v.get('read_only', False)) for v in services[name]['volumes']]
identity = ('/run/agent-host', 'secrets/agent-host', True)
self.assertIn(identity, mounts('core'))
self.assertEqual(mounts('agent-host'), [identity, ('/var/lib/oac/agent-host', 'agent-host', False)])
self.assertEqual(services['core']['environment']['OAC_AGENT_HOST_IDENTITY_FILE'], '/run/agent-host/identity.json')
self.assertIsNone(services['core']['command'])
self.assertNotIn('OAC_WEB_INSTALLATION_SOCKET', services['web']['environment'])
self.assertEqual(services['init']['command'], ['/usr/local/bin/oac', 'init'])
Expand Down Expand Up @@ -105,11 +118,13 @@ def ports(config):


def test_platform_network_injection_keeps_the_file_valid(self):
# Dokploy isolated deployments attach a project network to every service.
# Dokploy attaches a project network to the services it routes to or the
# operator selects. The agent host shares Core's network and joins none.
transformed = copy.deepcopy(self.compose)
transformed['networks']['platform'] = {}
for service in transformed['services'].values():
service.setdefault('networks', {})['platform'] = None
if 'network_mode' not in service:
service.setdefault('networks', {})['platform'] = None
subprocess.run(
['docker', 'compose', '-f', '-', 'config', '--quiet'],
input=json.dumps(transformed), text=True, check=True)
Expand Down
14 changes: 10 additions & 4 deletions deploy/install.dev.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
# Start this checkout. Core, Web and the init image are built here. Node
# metadata still comes from the release named in deploy/compose/smoke-pins.json.
# Start this checkout. Core, Web, the agent host and the init image are built
# here. Node metadata still comes from the release named in
# deploy/compose/smoke-pins.json.
# The published installer is install.sh.
set -euo pipefail

Expand All @@ -17,8 +18,10 @@ usage() {
cat <<'EOF'
Usage: install.dev.sh [--install-dir DIR] [--host ADDRESS] [--web-port PORT]

Builds Core, Web and the init image from this checkout and starts them.
Open http://localhost:<port> and sign in with the printed Core key.
Builds Core, Web, the agent host and the init image from this checkout and
starts them. The agent-host build takes the Harness inputs that
scripts/build-agent-host-images.sh needs. Open http://localhost:<port> and sign
in with the printed Core key.
EOF
}

Expand Down Expand Up @@ -96,6 +99,8 @@ tag="oac-local"
docker build -q --platform linux/amd64 -t "$tag/core:dev" "$build/core" >/dev/null
docker build -q --platform linux/amd64 -t "$tag/web:dev" "$build/web" >/dev/null
docker build -q --platform linux/amd64 -t "$tag/ingress:dev" "$build/ingress" >/dev/null
OAC_AGENT_HOST_IMAGE="$tag/agent-host:dev" OAC_SANDBOX_IMAGE="$tag/sandbox:dev" \
bash "$repo_root/scripts/build-agent-host-images.sh" -q >/dev/null

python3 - "$repo_root" "$install_dir" <<'PY'
import importlib.util, json, sys
Expand All @@ -119,6 +124,7 @@ OAC_WEB_PORT=$web_port
OAC_IMAGE_CORE=$tag/core:dev
OAC_IMAGE_WEB=$tag/web:dev
OAC_IMAGE_INGRESS=$tag/ingress:dev
OAC_IMAGE_AGENT_HOST=$tag/agent-host:dev
EOF

(
Expand Down
9 changes: 7 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Every setting of a Core installation has exactly one home, in one of three categ
| Category | Examples | Home | Change it with | Takes effect |
| --- | --- | --- | --- | --- |
| [Process settings](#process-settings) | Public URL, ports, logging, harnesses, execution concurrency, audit retention, OAuth origins, Runtime history | `.env` in the installation directory (default `~/.oac/core`) | Edit `.env`, then run `oac apply` | `oac apply` recreates the services that read the changed settings |
| [Secrets](#compose-installations) | Database password, credential encryption key, installation ID, Core key and the Core key digest derived from it | `secrets/` in the Compose data volume, one copy each | Initialization generates them once; `oac rotate-core-key` replaces the Core key and its digest | `oac rotate-core-key` restarts Core and Web |
| [Secrets](#compose-installations) | Database password, credential encryption key, installation ID, agent-host identity, Core key and the Core key digest derived from it | `secrets/` in the Compose data volume, one copy each | Initialization generates them once; `oac rotate-core-key` replaces the Core key and its digest | `oac rotate-core-key` restarts Core and Web |
| [Runtime settings](#runtime-settings-web) | Sandbox backend and size, nodes, Projects and keys, default models, executor credentials | Core's PostgreSQL database | Web, or the Core API (`/core/v1`) with the Core key | Saved without a Core restart; nodes prepare Runtime changes asynchronously |

Web's **System** page shows the installation's addresses, the default models, the sandbox configuration and, under **Startup settings**, the process settings Core loaded. No configuration file defines Projects or API keys.
Expand Down Expand Up @@ -111,7 +111,9 @@ The initialization service generates secrets and the installation ID once, then
| `secrets/database/` | Generated database password | PostgreSQL and Core |
| `secrets/core/` | Credential encryption key, installation ID and Core key digest | Core |
| `secrets/web/` | Generated Core sign-in key | Web |
| `secrets/agent-host/` | `identity.json`, the [agent host's identity](#agent-host-container) | Core and the agent host |
| `state/` | Private Provider state, mounted in Core at `/state`. Each adapter owns a subdirectory; E2B uses `e2b/`, with no group or other access | Core |
| `agent-host/` | The [agent host's state directory](#agent-host-container) | The agent host; initialization checks whether it is empty |
| `node-payload/` | Verified node installation metadata | Web |

Initialization prepares this directory; application services receive their secret directories read-only. `docker compose exec web oac-web core-key` prints the Core key to the operator terminal without writing it to container logs. Database passwords and credential encryption keys are never printed.
Expand Down Expand Up @@ -155,6 +157,8 @@ The container runs `oac-daemon agent-host --identity-file <path> --core-url <ori

The agent host serves each Harness that the image's `/opt/oac/harnesses.json` installs and that declares a view, and it starts and connects with none. It keeps each Session's home, with the Harness's native history, in `/var/lib/oac/agent-host`, which must outlive the container for Sessions to continue after a restart. A lost connection is redialed with backoff; when Core stays unreachable for two minutes, the agent host exits with a nonzero status for its supervisor to restart it.

A Compose installation runs the agent host as the `agent-host` service in Core's network namespace, with `--core-url http://127.0.0.1:8091`. It reads its identity from the [data volume](#compose-installations)'s `secrets/agent-host/`, mounted read-only, and keeps its state directory in the data volume's `agent-host/`.

Never set `GODEBUG=http2debug` for the agent host. With it, Go's HTTP/2 implementation logs every header it encodes, including the model and MCP credentials the agent host adds.

## Installation directory
Expand All @@ -169,7 +173,7 @@ The installer creates `~/.oac/core` by default (`$HOME/.oac/core` on Windows). I

The sibling `<install-dir>.lock` directory remains for synchronization; `<install-dir>.staging` holds unpublished installation files. Neither contains service data. On Unix the installer creates private directories with mode `0700` and configuration files with mode `0600`.

The Compose project is named `oac-<10 hex digits>`. Its services are `init`, `database`, `core` and `web`. Core applies database migrations when it starts. Web serves the console and forwards `/v1` and `/api/v1` to Core; it is the only service with a published port, `OAC_WEB_PORT`. No service receives a Docker socket.
The Compose project is named `oac-<10 hex digits>`. Its services are `init`, `database`, `core`, `agent-host` and `web`. Core applies database migrations when it starts. Web serves the console and forwards `/v1` and `/api/v1` to Core; it is the only service with a published port, `OAC_WEB_PORT`. No service receives a Docker socket.

## Appendix: Core environment without the installer

Expand All @@ -184,6 +188,7 @@ Core reads its process environment. Compose interpolates `.env` into it and moun
| `OAC_CREDENTIAL_KEY_FILE` | `/run/oac/credential.key` |
| `OAC_CORE_KEY_DIGESTS_FILE` | Required. `/run/oac/core-key-digests.json`: a JSON array with the SHA-256 of the Core key |
| `OAC_INSTALLATION_ID_FILE` | `/run/oac/installation.id`: the installation ID, a canonical UUID. It enables the sandbox deployment and node routes and requires `OAC_PUBLIC_URL`. Core refuses an ID other than the one its database recorded |
| `OAC_AGENT_HOST_IDENTITY_FILE` | `/run/agent-host/identity.json`: the [agent host's identity](#agent-host-container), whose `runtime_id` is a canonical UUID. Required with `OAC_PUBLIC_URL`, and only with it. When Core starts it registers the agent host with that ID and credential; a new credential fences the Links the old one authenticated, and a revoked agent host stays revoked |
| `OAC_EXECUTION_CONCURRENCY`, `OAC_DEFAULT_HARNESS`, `OAC_HARNESSES`, `OAC_WRITE_AUDIT_RETENTION`, `OAC_OAUTH_TRUSTED_ORIGINS`, `OAC_HISTORY_SETTINGS_FILE`, `OAC_LOG_LEVEL`, `OAC_LOG_FORMAT`, `OAC_LOG_ADD_SOURCE` | The matching [process settings](#settings). Web reads the three log settings too |
| `OAC_PROVIDER_ROOT` | Absolute adapter artifact root. The Core image sets `/opt/oac`. Each adapter owns its helper paths beneath this root. Core serves self-hosted daemon installers from its `native-installers/` directory when that holds a `catalog.json`, after checking the catalog against its own release. Adapter state lives at `/state`, the data volume's [`state/`](#compose-installations) |

Expand Down
Loading
Loading