name: agentgate networks: # Agent-side network: no route to the outside world. The demo agent can only # reach the gateway, never the upstream services. agent-net: internal: true # Service-side network: gateway, database, mock-github and (later) web, opa. service-net: volumes: postgres-data: services: postgres: image: postgres:18-alpine networks: [service-net] environment: POSTGRES_USER: agentgate POSTGRES_PASSWORD: agentgate POSTGRES_DB: agentgate ports: # Loopback only. Exposed on the host so `pnpm -r test` can run integration tests against # this instance, and that is the whole reason: the password is `agentgate`, committed in # .env.example, so a port on 0.0.0.0 is an open database for everyone on the network. # Override POSTGRES_PORT when 5432 is already taken. - '127.0.0.1:${POSTGRES_PORT:-5432}:5432' volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: ['CMD-SHELL', 'pg_isready -U agentgate -d agentgate'] interval: 3s timeout: 3s retries: 20 gateway: build: context: . dockerfile: apps/gateway/Dockerfile # On agent-net so the demo agent can reach it, on service-net so it can reach # the database and the upstream services the agent itself cannot see. networks: [agent-net, service-net] depends_on: postgres: condition: service_healthy env_file: .env environment: PORT: 8080 # In-network OPA address; only consulted when POLICY_ENGINE=opa. OPA_URL: http://opa:8181 ports: # Loopback by default, and the one service where that is a decision rather than an # obvious yes: the gateway is the enforcement point, so an agent running on another # machine has a real reason to reach it. It is still bound to 127.0.0.1 out of the box, # because this port carries the management API as well as the proxy — publishing it is # publishing the mint-a-token and approve-an-approval routes, guarded by one shared # ADMIN_TOKEN whose committed dev value is `dev-admin-token-DEV-ONLY`. Exposing it is # therefore a deliberate act: set GATEWAY_BIND=0.0.0.0, after reading THREAT_MODEL.md. # Override GATEWAY_PORT when 8080 is already taken on the host. - '${GATEWAY_BIND:-127.0.0.1}:${GATEWAY_PORT:-8080}:8080' healthcheck: test: [ 'CMD-SHELL', 'node -e "fetch(\"http://127.0.0.1:8080/healthz\").then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"', ] interval: 5s timeout: 3s retries: 20 start_period: 30s mock-github: build: context: . dockerfile: services/mock-github/Dockerfile # service-net ONLY, and no published port: the demo agent sits on agent-net and # must have no route here. Every call it makes has to go through the gateway, # which holds the token this service demands. networks: [service-net] environment: MOCK_GITHUB_TOKEN: ${MOCK_GITHUB_TOKEN:?set MOCK_GITHUB_TOKEN in .env} PORT: 3001 healthcheck: test: [ 'CMD-SHELL', 'node -e "fetch(\"http://127.0.0.1:3001/healthz\").then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"', ] interval: 5s timeout: 3s retries: 20 start_period: 20s opa: image: openpolicyagent/opa:1.19.0 # Optional: the gateway uses its builtin engine unless POLICY_ENGINE=opa. The container # exists so the same rego the parity suite tests can be exercised for real. networks: [service-net] command: - run - --server - --addr - 0.0.0.0:8181 - --log-level - error - /policies volumes: # Read-only: a policy change is a commit, never a container-side edit. - ./policies:/policies:ro ports: # Loopback only: OPA's API has no authentication at all, and `PUT /v1/policies` on an # open one is a way to rewrite this deployment's authorization rules from the network. # Exposed so the parity suite can run against it. Override OPA_PORT when 8181 is taken. - '127.0.0.1:${OPA_PORT:-8181}:8181' web: build: context: . dockerfile: apps/web/Dockerfile # service-net only. The console talks to the gateway's management API and nothing else; it # has no route to the agent network and never touches the database. networks: [service-net] depends_on: gateway: condition: service_healthy # Named one by one rather than through `env_file: .env`, on purpose: that file also holds the # master encryption key and the JWT signing key, and the console has no business being able # to read either. Compose still interpolates these from .env on the host. environment: GATEWAY_URL: http://gateway:8080 ADMIN_TOKEN: ${ADMIN_TOKEN:?set ADMIN_TOKEN in .env} # Displayed on the Policies page, labelled as the deployment's intent: the management API # does not report which engine the gateway booted with. POLICY_ENGINE: ${POLICY_ENGINE:-builtin} PORT: 3000 ports: # Loopback only, and this one is not negotiable. The console has no authentication of its # own: it holds ADMIN_TOKEN server-side and applies it on behalf of whoever reaches its # route handlers, so approving an approval and minting an agent token are available to # anyone who can open the page. Publishing this port beyond localhost publishes those # buttons. See THREAT_MODEL.md, "The console has no authentication". # Override WEB_PORT when 3000 is already taken on the host. - '127.0.0.1:${WEB_PORT:-3000}:3000' healthcheck: test: [ 'CMD-SHELL', 'node -e "fetch(\"http://127.0.0.1:3000/\").then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"', ] interval: 5s timeout: 3s retries: 20 start_period: 20s demo-agent: build: context: . dockerfile: apps/demo-agent/Dockerfile # agent-net ONLY, and this is the whole demonstration: there is no route from here to # mock-github, to the database or to the internet. The gateway is the single address this # container can resolve, so every request it makes is one somebody decided about. networks: [agent-net] # Not started by `docker compose up`: the demo agent runs once, says what happened and # exits. `make demo` starts it with `docker compose run`, which is what injects the # mission token minted for that run. profiles: [demo] depends_on: gateway: condition: service_healthy # No env_file, on purpose. This container's environment holds a gateway url, a mission # token and a credential *alias* — case 2 prints all of it — and .env holds the upstream # token the demo is about keeping out of here. environment: DEMO_MODE: container # No ports: nothing connects to the agent. It only ever calls out, to one place.