Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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-runtime produce 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-runtime sidecar, sharing a Unix-domain socket.

Containers — for whom, for what

ContainerImage / build todayFor whomFor what
dashboardbuild from source (pnpm --dir dashboard build); image pendingoperatorsSingle-container web UI to observe governance and manage agents, policies and budgets — reads everything via the REST API.
aa-apibuild from source (cargo build -p aa-api); image pendingoperators (via dashboard) + toolsThe REST / OpenAPI surface on :7700 the dashboard reads; fronts the gateway read model.
aa-gatewaybuild from source (cargo build -p aa-gateway); image pendingthe deploymentThe brain — registry, policy evaluation, budgets, audit; decides each action and saves governance records to persistence. gRPC :50051.
persistenceyou run / manage a standard postgres / timescaledb imagethe deploymentDurable audit history + state that the dashboard displays.
aa-runtimeghcr.io/ai-agent-assembly/aa-runtime:latest (pulled)every agentEnforcement 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 imageyouThe 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 controlMitM-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.

ServiceImage / buildCompose profilePublished portRole
aa-runtimeghcr.io/ai-agent-assembly/aa-runtime:latestdefault8080:8080Authoritative enforcement sidecar (health + metrics)
agent-stubalpine:latest (placeholder)default—Stand-in agent sharing the runtime IPC socket
aa-proxybuilt from ../../aa-proxy/Dockerfile (context = repo root)proxy8899:8899Optional egress-interception (MitM HTTPS) proxy

Volumes

VolumeMounted atPurpose
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-runtimeLocal enforcement policy

Environment variables

aa-runtime:

VariableValue in the stackMeaning
AA_AGENT_IDmy-agent-001Agent identity; must match agent-stub. Names the IPC socket.
AA_POLICY_PATH/etc/aa/policy.tomlPath to the mounted local policy file.
AA_METRICS_ADDR0.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:

VariableValue in the stackMeaning
AA_AGENT_IDmy-agent-001Must equal the runtime’s AA_AGENT_ID.
AA_GATEWAY_URLhttps://api.agentassembly.ioGateway 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):

VariableValue in the stackMeaning
AA_PROXY_ADDR0.0.0.0:8899Proxy listen address.
AA_PROXY_LLM_ONLYfalseIntercept all egress, not just LLM calls.
AA_PROXY_MCP_FAIL_OPEN1Demo 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 8080 here is aa-runtime’s health/metrics port, not the API the aasm CLI talks to; a bare aasm status targets the local API server on 7391 (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:

VariableSet it toEffect
AA_POLICYa directory of scoped policy documentsThe 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_REGISTRATIONtrue (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 relayEnables 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-api is backed by Postgres, human operators can sign in with accounts — GET /api/v1/auth/methods advertises 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:

VariableSet it toEffect
AA_SENSITIVE_DATA_PROJECTION_DBa SQLite path on a durable volumeRecords 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