Self-hosting the open-source stack
Agent Assembly is open source. You can self-host it yourself — stand the
infrastructure up, run it, and maintain it — using the sample Docker Compose stack
in examples/docker-compose/.
This page is the quick path for developers: it shows the infrastructure
architecture (which containers exist, who each is for, and what each does), the
exact configuration, and how to bring it up.
“Open source” here means scope, not a crippled build. Self-hosting runs the components that live in this open-source repository. The hosted SaaS edition additionally runs everything for you as a managed, multi-tenant service and adds the cloud/enterprise control-plane features that live outside this repo (managed persistence, SSO, compliance reporting). Nothing here is deliberately feature-limited — the example simply starts with the components that already ship a container image today.
Infrastructure architecture
The full self-host topology is a short, end-to-end chain. Operators work in the
dashboard; the dashboard reads everything through the REST API (aa-api);
the API fronts the gateway (the brain), which evaluates policy and saves
governance records to persistence; and your agents run co-located with an
aa-runtime enforcement sidecar, which checks the actions it receives with the gateway. So
the very actions your agents take are recorded in persistence and surface back in
the dashboard:
you ⇄ dashboard ⇄ aa-api ⇄ aa-gateway ⇄ persistence ⇆ aa-runtime ⇄ your agents
flowchart TB
user(["Operator — you, in a browser"])
subgraph present["Observe and manage"]
dash["dashboard<br/>single container · React / Vite"]
api["aa-api<br/>REST / OpenAPI :7700"]
end
gw["aa-gateway<br/>registry · policy · budgets · audit<br/>gRPC :50051"]
db[("persistence — you run and manage<br/>Postgres / TimescaleDB + cache")]
subgraph workload["Your workload — agent + program, co-located"]
agent["your agent(s)"]
rt["aa-runtime<br/>enforcement sidecar · :8080"]
end
user -->|HTTPS| dash
dash <-->|HTTP / WS :7700| api
api <-->|read model| gw
gw <-->|audit and state| db
agent <-->|IPC over UDS socket| rt
rt -->|CheckAction gRPC :50051| gw
gw -->|Allow / Deny| rt
gw -->|save governance records| db
Two things to read from it:
- The observability loop. Your agents +
aa-runtimeproduce the governance data (decisions, audit, budget usage); the gateway persists it; the dashboard reads it back through the API. That is how what your agents do shows up on screen. - What runs where. The dashboard is a single container; persistence is a
standard datastore you run and manage (e.g. Postgres / TimescaleDB); the
gateway + API are the control plane; and each agent runs next to its own
aa-runtimesidecar, sharing a Unix-domain socket.
Containers — for whom, for what
| Container | Image / build today | For whom | For what |
|---|---|---|---|
dashboard | build from source (pnpm --dir dashboard build); image pending | operators | Single-container web UI to observe governance and manage agents, policies and budgets — reads everything via the REST API. |
aa-api | build from source (cargo build -p aa-api); image pending | operators (via dashboard) + tools | The REST / OpenAPI surface on :7700 the dashboard reads; fronts the gateway read model. |
aa-gateway | build from source (cargo build -p aa-gateway); image pending | the deployment | The brain — registry, policy evaluation, budgets, audit; decides each action and saves governance records to persistence. gRPC :50051. |
| persistence | you run / manage a standard postgres / timescaledb image | the deployment | Durable audit history + state that the dashboard displays. |
aa-runtime | ghcr.io/ai-agent-assembly/aa-runtime:latest (pulled) | every agent | Enforcement sidecar co-located with the agent — the authoritative chokepoint that checks each action with the gateway. Serves health/metrics on :8080. |
| your agent(s) | your image | you | The workload being governed — runs beside its runtime, sharing the UDS socket. |
aa-proxy (optional) | build from aa-proxy/Dockerfile (proxy profile) | teams wanting code-free egress control | MitM-intercepts outbound HTTPS to apply network-egress policy without touching agent code. |
Everything above is open source in this repository. Today aa-runtime ships a
published image and aa-proxy builds from a Dockerfile; aa-gateway, aa-api and
dashboard you build from source (first-class images are tracked as follow-up); and
persistence is any standard Postgres / TimescaleDB you run. The hosted SaaS
edition runs this whole stack managed for you and adds the cloud / enterprise
control-plane features that live outside this repo.
What the example Compose wires today
The sample docker-compose.yml starts the subset that ships container images out of
the box — your agent placeholder, its aa-runtime sidecar, and the optional egress
proxy — so you see enforcement working immediately, then grow toward the full
topology above by adding persistence, gateway, API and dashboard.
flowchart LR
subgraph compose["docker compose — your machine or CI"]
stub["agent-stub<br/>(replace with your agent)"]
rt["aa-runtime<br/>enforcement sidecar<br/>:8080 health and metrics"]
proxy["aa-proxy<br/>egress MitM :8899<br/>optional · proxy profile"]
pol[/"policy.toml<br/>bind mount"/]
sock(["aa-runtime-socket<br/>shared UDS volume"])
end
stub <-->|IPC over shared UDS socket| sock
rt <--> sock
pol -->|AA_POLICY_PATH| rt
stub -.->|outbound HTTPS| proxy
proxy -.->|decisions| rt
The compose file (examples/docker-compose/docker-compose.yml) defines these
services. Values below are taken directly from that file.
| Service | Image / build | Compose profile | Published port | Role |
|---|---|---|---|---|
aa-runtime | ghcr.io/ai-agent-assembly/aa-runtime:latest | default | 8080:8080 | Authoritative enforcement sidecar (health + metrics) |
agent-stub | alpine:latest (placeholder) | default | — | Stand-in agent sharing the runtime IPC socket |
aa-proxy | built from ../../aa-proxy/Dockerfile (context = repo root) | proxy | 8899:8899 | Optional egress-interception (MitM HTTPS) proxy |
Volumes
| Volume | Mounted at | Purpose |
|---|---|---|
aa-runtime-socket | /tmp (in aa-runtime and agent-stub) | Shared Unix domain socket — the IPC channel lives at /tmp/aa-runtime-<AA_AGENT_ID>.sock |
../policy.toml (bind) | /etc/aa/policy.toml (read-only) in aa-runtime | Local enforcement policy |
Environment variables
aa-runtime:
| Variable | Value in the stack | Meaning |
|---|---|---|
AA_AGENT_ID | my-agent-001 | Agent identity; must match agent-stub. Names the IPC socket. |
AA_POLICY_PATH | /etc/aa/policy.toml | Path to the mounted local policy file. |
AA_METRICS_ADDR | 0.0.0.0:8080 (default) | Bind address for the health/metrics HTTP server. |
AA_GATEWAY_ENDPOINT | (unset) | Left unset for standalone, gateway-less enforcement. Set it to call a gateway (self-hosted from source, or the SaaS endpoint) instead. |
AA_GATEWAY_CREDENTIAL_TOKEN | (unset) | Authenticates the op-control (live kill-switch) subscription to a gateway that enforces per-RPC credential auth — every shipped gateway does, so without this the subscription is permanently rejected and the kill switch never connects (AAASM-5009). Requires AA_GATEWAY_AGENT_ID to also be set; startup fails otherwise. |
AA_GATEWAY_AGENT_ID | (unset) | The registered did:key identity the credential above was issued for — not the same value as AA_AGENT_ID above. |
agent-stub:
| Variable | Value in the stack | Meaning |
|---|---|---|
AA_AGENT_ID | my-agent-001 | Must equal the runtime’s AA_AGENT_ID. |
AA_GATEWAY_URL | https://api.agentassembly.io | Gateway URL a real agent SDK would use (SaaS endpoint shown; point it at your self-hosted gateway if you run one). |
AA_API_KEY | ${AA_API_KEY} | Read from your shell environment. |
aa-proxy (only under the proxy profile):
| Variable | Value in the stack | Meaning |
|---|---|---|
AA_PROXY_ADDR | 0.0.0.0:8899 | Proxy listen address. |
AA_PROXY_LLM_ONLY | false | Intercept all egress, not just LLM calls. |
AA_PROXY_MCP_FAIL_OPEN | 1 | Demo only — lets the proxy start without a reachable gateway. The proxy normally fails closed when its gateway is unreachable. Governs only request-side tools/call checks and start-up; a failed gateway evaluation of an MCP response is always withheld regardless of this setting. |
AA_PROXY_GATEWAY_ENDPOINT | (unset) | Set to a gateway endpoint (self-hosted or SaaS) to enforce through it. |
Quickstart
From a clone of the repository:
cd examples/docker-compose
# Runtime sidecar path (default profile: aa-runtime + agent-stub)
AA_API_KEY=dev-local-key docker compose up
aa-runtime starts, enforces locally from ../policy.toml, exposes the IPC socket
at /tmp/aa-runtime-my-agent-001.sock, and serves health/metrics on :8080:
Port
8080here isaa-runtime’s health/metrics port, not the API theaasmCLI talks to; a bareaasm statustargets the local API server on7391(aasm start).
curl http://localhost:8080/ready
curl http://localhost:8080/health
curl http://localhost:8080/metrics
/health reports status: "healthy" when something is installed to decide on the runtime’s
calls: local policy rules that block or require approval for something, a gateway client, or
lease governance. “Installed” is not “working”: an invalid AA_GATEWAY_ENDPOINT installs no
client, a valid but unreachable one still counts (the runtime does not report reachability),
and with AA_GATEWAY_FAIL_CLOSED=false an unreachable gateway falls back to the local rules
and then allows (flagged as governance.gateway_fail_open). A runtime with none of those
reports status: "ungoverned" and this runtime evaluates and allows every call; its
governance.state field and startup log say how to govern it (AA_LEASE_POLICY,
AA_POLICY_PATH or AA_GATEWAY_ENDPOINT). If the lease gate trips (state record corrupted
or removed) status becomes "degraded" and governance.state "denied_all": every call is
denied. /health answers HTTP 200 for ungoverned and degraded too, so an orchestrator’s
liveness/readiness probe keeps passing while every call is allowed or denied; alert on
status != "healthy" or on governance.state instead. A fail-closed gateway that is installed but
unreachable denies every call while status stays "healthy", and a fail-open one that is dead with
no effective local rules allows every call (visible only as gateway_fail_open). A local rule counts
only if it names a real action type (TOOL_CALL, NETWORK_CALL, FILE_OPERATION, …; exact and
case-sensitive); unknown names match nothing and are warned about when the policy loads. Other interception layers (eBPF is observe-only; the proxy only enforces through a
gateway endpoint) add no decision source of their own here.
One runtime per agent id
aa-runtime refuses to start (exit status 78, reason on stderr) when another runtime
already holds the same AA_AGENT_ID or, with a loopback AA_METRICS_ADDR, its health
port. It takes an exclusive lock on /tmp/aa-runtime-<agent id>.lock for the life of the
process (the kernel releases it on any exit, including kill -9), then checks that
nothing is listening on /tmp/aa-runtime-<agent id>.sock; a socket file left by a killed
runtime is replaced as before. Give each runtime on a host its own AA_AGENT_ID and
metrics port.
The lock file is refused (distinct from “a peer holds it”) if it is a symlink, not a regular
file, owned by another user, or group/other-writable, so a local user cannot squat the
name to keep an agent id from starting. Residual: both files live in shared /tmp because
the SDKs and tooling address /tmp/aa-runtime-<agent id>.sock by that name; the socket path
could already be squatted before this check existed, and still can be.
Recovery: a lock or socket file for the same AA_AGENT_ID that is owned by another user (for
example left by a sudo run) makes a non-root start refuse; remove it as that user or as root, or
use a different AA_AGENT_ID. A /tmp reaper (macOS periodic cleanup, systemd-tmpfiles) can unlink
the lock file under a running runtime; the socket probe is then the only protection. The lock is held
until the process exits, not only until the runtime’s main loop returns, and a shutting-down runtime
unlinks the socket only if it is still the file it bound.
Limits: the default AA_METRICS_ADDR (0.0.0.0:8080) is non-loopback without
AA_METRICS_ALLOW_REMOTE=1, so under the defaults no health server is started and only the
agent-id protection applies. The health port is bound at startup but served at the end of it,
so use GET /health returning 200, not a TCP connect, to detect readiness. A runtime
older than this check does not take the lock, so racing one at startup can still lose its
socket path. On macOS a full accept backlog is reported as a refused connection and so reads as stale. A probe that cannot tell (an over-full backlog elsewhere, no answer within 2 s, an over-long path)
refuses rather than guessing stale; the no-answer-within-2 s branch has no test that executes it.
To additionally build and run the optional egress proxy on :8899:
AA_API_KEY=dev-local-key docker compose --profile proxy up
Tear down when finished:
docker compose down
# or, if you started the proxy profile:
docker compose --profile proxy down
Replacing the agent stub
agent-stub is an alpine placeholder (the Python SDK is not yet published —
tracked in AAASM-55). To run a real agent, replace its image: with your agent
image, keep AA_AGENT_ID identical to aa-runtime, and keep the
aa-runtime-socket volume mounted at /tmp. See the example’s
README
for details.
Configuring the aa-api service
Once you grow the stack past the runtime-only quickstart and run the aa-api
control-plane service (the one the dashboard reads), a few AA_* environment
variables on that service shape what operators see and how they sign in. The full
reference lives in Configuration → aa-api server environment
variables;
the self-host essentials:
| Variable | Set it to | Effect |
|---|---|---|
AA_POLICY | a directory of scoped policy documents | The dashboard’s capability-matrix, topology-chain, and team-policy projections show the real policy cascade. A single file shows one policy; leaving it unset makes the projections render Unknown / Unconfigured — never a fabricated allow. |
AA_AUTH_OPEN_REGISTRATION | true (optional) | Opens self-registration for native accounts. Default is closed: the first account bootstraps as owner, then it is invite-only. |
AA_SMTP_HOST (+ AA_SMTP_PORT / USER / PASS / FROM) | your SMTP relay | Enables password-reset email delivery. When unset, resets still return 202 but no email is sent (a logging-mailer fallback). |
Native email/password login requires a Postgres-backed deployment. The in-memory / runtime-only quickstart above stays API-key-only. Once
aa-apiis backed by Postgres, human operators can sign in with accounts —GET /api/v1/auth/methodsadvertises whether the password path is available, and the login page degrades honestly when it is not. See Authentication for the account, invite, and reset flows.
Configuring the aa-gateway service
Growing the stack further — past aa-api to the gateway itself — adds one
AA_* variable a self-hosting operator is expected to decide about, because
nothing turns it on for you:
| Variable | Set it to | Effect |
|---|---|---|
AA_SENSITIVE_DATA_PROJECTION_DB | a SQLite path on a durable volume | Records classification decisions and their findings to the durable sensitive-data projection. Left unset — the default — the gateway still evaluates those decisions, but writes none of them to this table. |
Two conditions apply and neither is visible from the variable’s name. It is read
only under the gateway’s default legacy-grpc mode, so a container whose command
carries --mode local or --mode remote ignores it without complaint. And a
path that cannot be opened or migrated fails the gateway’s boot — deliberate,
so an unwritable volume mount surfaces at start-up rather than as a table nobody
notices is empty. Sensitive-data
projection covers both, how to
revert, and what an incomplete projection looks like under load.
When you want it fully managed
If you would rather not run and maintain the infrastructure yourself — and want the managed, multi-tenant control plane with durable audit history, the operator dashboard, central registry, team budgets, SSO and compliance reporting — use the hosted SaaS edition, which runs the complete stack for you.
Last updated: 2026-10-26 by Chisanan232