# Single source of truth for values shared by embabel-agent-runtime and litellm.
# YAML anchors keep each literal in exactly ONE place (BR-6); docker compose
# resolves them before interpolation -- verified with `docker compose config`.
x-ollama-base-url: &ollama-base-url ${OLLAMA_BASE_URL:-http://host.docker.internal:11434}
x-ollama-model: &ollama-model ${OLLAMA_MODEL:-qwen3.6:27b}
x-litellm-master-key: &litellm-master-key ${LITELLM_MASTER_KEY:-sk-openjcockpit-litellm-local}
x-litellm-virtual-key: &litellm-virtual-key ${LITELLM_VIRTUAL_KEY:-sk-openjcockpit-embabel-local}
# MCP-only LiteLLM key of embabel-agent-service (checkpoint commits through the git MCP server; no models).
x-litellm-agent-service-key: &litellm-agent-service-key ${LITELLM_AGENT_SERVICE_KEY:-sk-openjcockpit-agent-service-local}
# Service identities across the management/execution boundary (seeded by local-seed, client_credentials):
# embabel-agent-service -> embabel-agent-runtime (run agents, drafts) and back (bindings, run scopes, policy).
x-agent-service-client-secret: &agent-service-client-secret ${AGENT_SERVICE_CLIENT_SECRET:-openjcockpit-agent-service-local-secret}
x-agent-runtime-client-secret: &agent-runtime-client-secret ${AGENT_RUNTIME_CLIENT_SECRET:-openjcockpit-agent-runtime-local-secret}
# The one A2A client of the LiteLLM provider (seeded by local-seed): LiteLLM key + Keycloak client
# secret (client openjcockpit-a2a issues the JWT, with tenant_id=local, that embabel requires).
x-litellm-a2a-key: &litellm-a2a-key ${LITELLM_A2A_KEY:-sk-openjcockpit-a2a-local}
x-a2a-client-secret: &a2a-client-secret ${A2A_CLIENT_SECRET:-openjcockpit-a2a-local-secret}
# Local-only secrets of the git-workspace flow (git MCP server via LiteLLM, credentials pulled server-side).
x-git-mcp-client-secret: &git-mcp-client-secret ${GIT_MCP_CLIENT_SECRET:-openjcockpit-git-mcp-local-secret}
x-ai-control-client-secret: &ai-control-client-secret ${AI_CONTROL_CLIENT_SECRET:-openjcockpit-ai-control-local-secret}
x-litellm-ai-control-key: &litellm-ai-control-key ${LITELLM_AI_CONTROL_KEY:-sk-openjcockpit-ai-control-local}
# Local-only key that encrypts the secrets of A2A services added in the dashboard (salt: hex).
x-a2a-service-encryption-secret: &a2a-service-encryption-secret ${A2A_SERVICE_ENCRYPTION_SECRET:-openjcockpit-a2a-services-local-secret}
x-a2a-service-encryption-salt: &a2a-service-encryption-salt ${A2A_SERVICE_ENCRYPTION_SALT:-a2a5e7c1ce5a17ed}
# LOCAL-ONLY placeholder: HS256 key shared by ai-control-service (signs runner tokens) and
# embabel-agent-service (verifies them). At least 32 bytes; override in any shared environment.
x-spec-queue-runner-secret: &spec-queue-runner-secret ${SPEC_QUEUE_RUNNER_TOKEN_SECRET:-local-only-spec-queue-runner-secret-CHANGE-ME-outside-local}
services:
  postgres:
    image: postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea
    container_name: openjcockpit-postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: keycloak
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"]
      interval: 10s
      timeout: 5s
      retries: 10
  postgres-init:
    image: postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      PGPASSWORD: keycloak
    volumes:
      - ./infrastructure/postgres/ensure-openjcockpit-db.sh:/ensure-openjcockpit-db.sh:ro
    command: ["/bin/sh", "/ensure-openjcockpit-db.sh"]
    restart: "no"

  keycloak:
    image: quay.io/keycloak/keycloak:26.6.4@sha256:0aae0de7fca85525f727d3354df17896092de8bb26ae4c12d89c77e5df8cbce4
    container_name: openjcockpit-keycloak
    restart: unless-stopped
    command: ["start-dev", "--import-realm"]
    environment:
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: admin
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: keycloak
      KC_HTTP_ENABLED: "true"
      KC_HOSTNAME_STRICT: "false"
      KC_HEALTH_ENABLED: "true"
      KC_SPI_THEME_CACHE_THEMES: "false"
      KC_SPI_THEME_CACHE_TEMPLATES: "false"
      KC_SPI_THEME_STATIC_MAX_AGE: "-1"
      # Keycloak 26 uses hostname v2 (KC_HOSTNAME_URL is a removed v1 option and was ignored). A full URL
      # pins the token issuer to http://localhost:8080 for every caller, including service clients that
      # request tokens inside the compose network (http://keycloak:8080): the services validate exactly
      # that issuer (openjcockpit.security.jwt-issuer). Backchannel requests keep using the request host.
      KC_HOSTNAME: "http://localhost:8080"
      KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true"
    healthcheck:
      # KC_HEALTH_ENABLED exposes /health/ready on the management port (9000).
      # The Keycloak image ships no curl/wget, so use bash's /dev/tcp.
      test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9000 && echo -e 'GET /health/ready HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' >&3 && cat <&3 | grep -q '\"status\": \"UP\"'"]
      interval: 10s
      timeout: 5s
      retries: 30
      start_period: 20s
    ports:
      - "8080:8080"
    depends_on:
      postgres:
        condition: service_healthy
    volumes:
      - ./infrastructure/keycloak/openjcockpit-realm.json:/opt/keycloak/data/import/openjcockpit-realm.json:ro
      - ./infrastructure/themes:/opt/keycloak/themes:ro

  # One-shot, idempotent seeding of the openjcockpit-admin realm role (covers existing
  # Keycloak databases, where --import-realm is skipped). See
  # infrastructure/keycloak/seed-realm-roles.sh.
  keycloak-realm-roles:
    image: curlimages/curl:8.21.0@sha256:7c12af72ceb38b7432ab85e1a265cff6ae58e06f95539d539b654f2cfa64bb13
    depends_on:
      keycloak:
        condition: service_healthy
    environment:
      KC_URL: http://keycloak:8080
      KC_REALM: openjcockpit
      # Mirrors the bootstrap admin of the keycloak service above (local dev only,
      # no new secret is introduced by this service).
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: admin
      KC_ADMIN_ROLE_USERS: tony
    volumes:
      - ./infrastructure/keycloak/seed-realm-roles.sh:/seed-realm-roles.sh:ro
    entrypoint: ["/bin/sh", "/seed-realm-roles.sh"]
    restart: "no"

  # One-shot seeding of per-developer, gitignored Keycloak users.
  # See infrastructure/keycloak/local-users/README.md. No-op when the directory
  # contains no *.local.json files, which is the default for a fresh checkout.
  keycloak-local-users:
    image: curlimages/curl:8.21.0@sha256:7c12af72ceb38b7432ab85e1a265cff6ae58e06f95539d539b654f2cfa64bb13
    depends_on:
      keycloak:
        condition: service_healthy
      keycloak-realm-roles:
        condition: service_completed_successfully
    environment:
      KC_URL: http://keycloak:8080
      KC_REALM: openjcockpit
      # Mirrors the bootstrap admin of the keycloak service above (local dev only,
      # no new secret is introduced by this service).
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: admin
    volumes:
      - ./infrastructure/keycloak/local-users:/local-users:ro
      - ./infrastructure/keycloak/seed-local-users.sh:/seed-local-users.sh:ro
    entrypoint: ["/bin/sh", "/seed-local-users.sh"]
    restart: "no"

  # One-shot seeding of the fixed Playwright E2E test user into the
  # openjcockpit realm. See infrastructure/keycloak/seed-e2e-user.sh.
  keycloak-e2e-user:
    image: curlimages/curl:8.21.0@sha256:7c12af72ceb38b7432ab85e1a265cff6ae58e06f95539d539b654f2cfa64bb13
    depends_on:
      keycloak:
        condition: service_healthy
    environment:
      KC_URL: http://keycloak:8080
      KC_REALM: openjcockpit
      # Mirrors the bootstrap admin of the keycloak service above (local dev only,
      # no new secret is introduced by this service).
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: admin
      E2E_SEED_USERNAME: e2e
      E2E_SEED_EMAIL: e2e@openjcockpit.local
      # E2E_SEED_PASSWORD is deliberately not set here: seed-e2e-user.sh
      # already defaults it to the same literal, and this is one of exactly
      # four places (architecture §7.3) that literal is allowed to appear —
      # a fifth, redundant copy here would make a future rotation a
      # five-edit change instead of four.
    volumes:
      - ./infrastructure/keycloak/seed-e2e-user.sh:/seed-e2e-user.sh:ro
    entrypoint: ["/bin/sh", "/seed-e2e-user.sh"]
    restart: "no"

  # OpenAI-compatible gateway in front of the HOST's Ollama daemon. This is now the
  # only route embabel-agent-runtime takes to the local model (BR-13: OpenAI is NOT
  # proxied). Host port 4100 -- LiteLLM's conventional 4000 collides with `dashboard`.
  litellm:
    image: ghcr.io/berriai/litellm:v1.101.6@sha256:fc0e36a0965e4ffe75bc509b3a1085f5d6c6b78b16efa2ffbb525e122e51f83b
    container_name: openjcockpit-litellm
    restart: unless-stopped
    ports:
      - "4100:4000"
    environment:
      CONFIG_FILE_PATH: /app/config.yaml
      DATABASE_URL: postgresql://litellm_app:litellm_app@postgres:5432/litellm
      ENFORCE_PRISMA_MIGRATION_CHECK: "True"
      LITELLM_MASTER_KEY: *litellm-master-key
      LITELLM_SALT_KEY: ${LITELLM_SALT_KEY:-sk-openjcockpit-litellm-salt-local}
      UI_USERNAME: ${LITELLM_UI_USERNAME:-ricky}
      UI_PASSWORD: ${LITELLM_UI_PASSWORD:-Welkom01!}
      STORE_MODEL_IN_DB: "True"
      OLLAMA_BASE_URL: *ollama-base-url
      OLLAMA_MODEL: *ollama-model
      # OPENAI_API_KEY is deliberately ABSENT (BR-13, AC-12).
    volumes:
      - ./infrastructure/litellm/config.yaml:/app/config.yaml:ro
    # The gateway is now the component that reaches the host daemon (BR-17).
    extra_hosts:
      - "host.docker.internal:host-gateway"
    healthcheck:
      # BR-5/BR-16: /health/liveliness is unauthenticated and touches NOTHING but this
      # process. NEVER use /health -- it is authenticated and health-checks every model
      # in config.yaml, i.e. it would call Ollama (its own docstring says
      # "USE /health/liveliness to health check the proxy").
      test: ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:4000/health/liveliness')\""]
      interval: 10s
      timeout: 5s
      retries: 30
      start_period: 40s
    depends_on:
      git-mcp-server:
        condition: service_started
      postgres:
        condition: service_healthy
      postgres-init:
        condition: service_completed_successfully

  # One-shot local development seed, runs on every `docker compose up` (idempotent):
  # Keycloak service clients (A2A, git MCP, ai-control, agent service <-> agent runtime) + LiteLLM keys
  # (runtime model key, agent service MCP key, the one LiteLLM A2A key, ai-control MCP key).
  # See infrastructure/local-seed/seed-local.sh.
  local-seed:
    image: curlimages/curl:8.21.0@sha256:7c12af72ceb38b7432ab85e1a265cff6ae58e06f95539d539b654f2cfa64bb13
    depends_on:
      litellm:
        condition: service_healthy
      keycloak:
        condition: service_healthy
    environment:
      LITELLM_URL: http://litellm:4000
      LITELLM_MASTER_KEY: *litellm-master-key
      LITELLM_VIRTUAL_KEY: *litellm-virtual-key
      LITELLM_KEY_ALIAS: embabel-agent-runtime
      LITELLM_KEY_MODEL: *ollama-model
      LITELLM_A2A_KEY: *litellm-a2a-key
      KC_URL: http://keycloak:8080
      KC_REALM: openjcockpit
      # Mirrors the bootstrap admin of the keycloak service (local dev only).
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: admin
      A2A_CLIENT_ID: openjcockpit-a2a
      A2A_CLIENT_SECRET: *a2a-client-secret
      A2A_TENANT_CLAIM: tenant_id
      A2A_TENANT: local
      GIT_MCP_CLIENT_SECRET: *git-mcp-client-secret
      AI_CONTROL_CLIENT_SECRET: *ai-control-client-secret
      LITELLM_AI_CONTROL_KEY: *litellm-ai-control-key
      LITELLM_AGENT_SERVICE_KEY: *litellm-agent-service-key
      AGENT_SERVICE_CLIENT_SECRET: *agent-service-client-secret
      AGENT_RUNTIME_CLIENT_SECRET: *agent-runtime-client-secret
    volumes:
      - ./infrastructure/local-seed:/local-seed:ro
    entrypoint: ["/bin/sh", "/local-seed/seed-local.sh"]
    restart: "no"

  dashboard:
    build:
      context: frontend
      dockerfile: apps/dashboard/Dockerfile
      args:
        BUILD_SHA: ${BUILD_SHA:-dev}
    container_name: openjcockpit-dashboard
    restart: unless-stopped
    ports:
      - "4000:8080"
    depends_on:
      - ai-control-service

  # Proxied access to the Docker daemon for services within the compose network.
  # SECURITY NOTE: this exposes the Docker daemon without TLS on the internal
  # Docker network (port 2375, not published to the host). Every container in this
  # network therefore gets full control over the host — local dev only.
  docker-socat:
    image: alpine/socat@sha256:fb3862c949af7c79996d0e514ee96128644d840835ed5368eeecb14a869de361
    container_name: openjcockpit-docker-socat
    restart: unless-stopped
    command: tcp-listen:2375,fork,reuseaddr unix-connect:/var/run/docker.sock
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

  ai-control-service:
    build:
      context: .
      dockerfile: services/ai-control-service/Dockerfile
    container_name: openjcockpit-ai-control
    restart: unless-stopped
    ports:
      - "9080:8080"
    environment:
      EMBABEL_AGENT_SERVICE_BASE_URL: http://embabel-agent-service:8081
      SPEC_REPOSITORY_PATH: /spec-repository
      OPENAI_API_KEY: ${OPENAI_API_KEY}
      DB_URL: jdbc:postgresql://postgres:5432/openjcockpit
      DB_USERNAME: openjcockpit_app
      DB_PASSWORD: openjcockpit_app
      ENCRYPTION_SECRET: ${ENCRYPTION_SECRET:-prod-change-this-32chars-minimum!}
      ENCRYPTION_SALT: ${ENCRYPTION_SALT:-0123456789abcdef}
      # Workflow container settings
      # SECURITY NOTE: access to the Docker daemon gives ai-control-service full control
      # over the host. Only use this in a trusted environment. Consider Docker-in-Docker or
      # Kubernetes jobs in production.
      # The daemon is reached via the docker-socat service in this compose network; that also
      # works on macOS, where a unix-socket mount from inside a container is not reliable.
      AGENTIC_DOCKER_HOST: ${AGENTIC_DOCKER_HOST:-tcp://docker-socat:2375}
      WORKFLOW_WORKSPACE_BASE: /workflow-workspaces
      AGENTIC_CONTAINER_ENABLED: ${AGENTIC_CONTAINER_ENABLED:-true}
      AGENTIC_CONTAINER_IMAGE: ${AGENTIC_CONTAINER_IMAGE:-eclipse-temurin:25-jre}
      SPEC_QUEUE_RUNNER_ENABLED: ${SPEC_QUEUE_RUNNER_ENABLED:-true}
      SPEC_QUEUE_POLL_INTERVAL: ${SPEC_QUEUE_POLL_INTERVAL:-PT20S}
      SPEC_QUEUE_RUNNER_TOKEN_SECRET: *spec-queue-runner-secret
      # A job waits for its whole workflow run; four agents on the local Ollama model with git tool calls
      # take well over the 600 s service default.
      WORKFLOW_AGENT_TIMEOUT: ${WORKFLOW_AGENT_TIMEOUT:-3600}
      # Git-workspace jobs: clone/branch/write/commit/push in the job's git MCP workspace via LiteLLM,
      # and start the workflow in embabel with ai-control's own service identity (seeded by local-seed).
      LITELLM_MCP_BASE_URL: http://litellm:4000
      LITELLM_AI_CONTROL_KEY: *litellm-ai-control-key
      AI_CONTROL_SERVICE_TOKEN_URL: http://keycloak:8080/realms/openjcockpit/protocol/openid-connect/token
      AI_CONTROL_SERVICE_CLIENT_ID: openjcockpit-ai-control
      AI_CONTROL_SERVICE_CLIENT_SECRET: *ai-control-client-secret
    depends_on:
      postgres:
        condition: service_healthy
      postgres-init:
        condition: service_completed_successfully
      docker-socat:
        condition: service_started
      local-seed:
        condition: service_completed_successfully
    volumes:
      - ./infrastructure/spec-repository-stub:/spec-repository:ro
      - workflow-workspaces:/workflow-workspaces

  # Management plane: workflows and their orchestration, agent catalog, tenant bindings, approval gates,
  # OPA policy decisions and run history (PostgreSQL). It runs no agent and no LLM: every agent runs in
  # embabel-agent-runtime, reached through EMBABEL_AGENT_RUNTIME_BASE_URL (any host or HTTPS URL works).
  embabel-agent-service:
    build:
      context: .
      dockerfile: services/embabel-agent-service/Dockerfile
    image: openjcockpit/embabel-agent-service:${OPENJCOCKPIT_IMAGE_TAG:-local}
    container_name: openjcockpit-embabel-agents
    restart: unless-stopped
    ports:
      - "8091:8081"
    environment:
      DB_URL: jdbc:postgresql://postgres:5432/embabel
      DB_USERNAME: embabel_app
      DB_PASSWORD: embabel_app
      WORKFLOW_DEFINITIONS_PATH: /workflow-definitions
      OPA_ENABLED: ${OPA_ENABLED:-true}
      OPA_BASE_URL: http://opa:8181
      MCP_CLIENT_ENABLED: ${MCP_CLIENT_ENABLED:-true}
      GIT_MCP_SERVER_URL: http://git-mcp-server:8093
      # Spring profile `local` (application-local.yml): agent catalog, the LiteLLM A2A provider client, the
      # agent runtime connection and the tenant 'local' seed. Only container paths and secrets are set here.
      SPRING_PROFILES_ACTIVE: local
      # The execution plane and this service's identity towards it (client openjcockpit-agent-service).
      EMBABEL_AGENT_RUNTIME_BASE_URL: ${EMBABEL_AGENT_RUNTIME_BASE_URL:-http://embabel-agent-runtime:8082}
      AGENT_SERVICE_CLIENT_SECRET: *agent-service-client-secret
      LITELLM_A2A_KEY: *litellm-a2a-key
      A2A_CLIENT_SECRET: *a2a-client-secret
      A2A_SERVICE_ENCRYPTION_SECRET: *a2a-service-encryption-secret
      A2A_SERVICE_ENCRYPTION_SALT: *a2a-service-encryption-salt
      LITELLM_PUBLIC_URL: ${LITELLM_PUBLIC_URL:-http://localhost:4100}
      # checkpoint commits of runs on a project (git MCP server through LiteLLM; MCP-only key, no models)
      LITELLM_AGENT_SERVICE_KEY: *litellm-agent-service-key
      AGENT_CATALOG_LOCAL_PATH: /agents
      APPROVAL_GATE_MAX_FEEDBACK_ITERATIONS: ${APPROVAL_GATE_MAX_FEEDBACK_ITERATIONS:-3}
      APPROVAL_GATE_COMMENT_MAX_LENGTH: ${APPROVAL_GATE_COMMENT_MAX_LENGTH:-4000}
      APPROVAL_GATE_MAX_CHANGED_PATHS: ${APPROVAL_GATE_MAX_CHANGED_PATHS:-50}
      # Workflow-chaining tunables: max wait for a sequential child, max chain depth, max orbs
      # per workflow, and a runtime kill switch.
      WORKFLOW_TRIGGER_SEQUENTIAL_MAX_WAIT: ${WORKFLOW_TRIGGER_SEQUENTIAL_MAX_WAIT:-30m}
      WORKFLOW_TRIGGER_MAX_CHAIN_DEPTH: ${WORKFLOW_TRIGGER_MAX_CHAIN_DEPTH:-3}
      WORKFLOW_TRIGGER_MAX_ORBS_PER_WORKFLOW: ${WORKFLOW_TRIGGER_MAX_ORBS_PER_WORKFLOW:-5}
      WORKFLOW_TRIGGER_ENABLED: ${WORKFLOW_TRIGGER_ENABLED:-true}
      # Must be identical to the value given to ai-control-service.
      SPEC_QUEUE_RUNNER_TOKEN_SECRET: *spec-queue-runner-secret
    # No dependency on embabel-agent-runtime: both start independently; local agents and drafts fail with
    # "runtime unavailable" until it is reachable.
    depends_on:
      postgres:
        condition: service_healthy
      postgres-init:
        condition: service_completed_successfully
      opa:
        condition: service_started
      git-mcp-server:
        condition: service_started
      litellm:
        condition: service_healthy                    # BR-16
      local-seed:
        condition: service_completed_successfully     # D-2: the keys and clients must exist before the first call
    volumes:
      - workflow-workspaces:/workflow-workspaces
      - workflow-definitions:/workflow-definitions
      # the agent catalog (dashboard: Workflow design -> Agents); its definitions travel to the runtime per run
      - ./agents:/agents:ro

  # Execution plane: the Embabel agent platform, configuration-driven agents (definitions ship in the image),
  # LLM access (LiteLLM, host Ollama), agent tools (git MCP via LiteLLM) and the inbound A2A endpoints that
  # LiteLLM routes to. No database and no shared volume: tenant bindings, workflow runs and tool-call policy
  # decisions come from embabel-agent-service over HTTP, so this container can run on another host by
  # changing EMBABEL_AGENT_SERVICE_BASE_URL / A2A_PUBLIC_BASE_URL (and the LiteLLM agent URLs) only.
  embabel-agent-runtime:
    build:
      context: .
      dockerfile: services/embabel-agent-runtime/Dockerfile
    image: openjcockpit/embabel-agent-runtime:${OPENJCOCKPIT_IMAGE_TAG:-local}
    container_name: openjcockpit-embabel-runtime
    restart: unless-stopped
    ports:
      - "8092:8082"
    environment:
      SPRING_PROFILES_ACTIVE: local
      OPENAI_API_KEY: ${OPENAI_API_KEY}
      # Local Ollama for the default/coding LLM. The container reaches the HOST's Ollama daemon via
      # host.docker.internal (see extra_hosts below); the in-code default is loopback for host-run execution.
      # No Ollama container is part of this stack -- the operator installs Ollama on the host and runs
      # `ollama pull qwen3.6:27b` before starting the stack.
      # WARNING: setting OLLAMA_BASE_URL in .env to a host-oriented value such as http://localhost:11434
      # leaks into this container and will break it (nothing listens on the container's own loopback).
      OLLAMA_BASE_URL: *ollama-base-url
      OLLAMA_MODEL: *ollama-model
      # The default/coding LLM and the agent model aliases transit the LiteLLM gateway (BR-13/BR-14).
      LITELLM_BASE_URL: http://litellm:4000/v1
      LITELLM_VIRTUAL_KEY: *litellm-virtual-key
      # The management plane and this runtime's identity towards it (client openjcockpit-agent-runtime).
      EMBABEL_AGENT_SERVICE_BASE_URL: ${EMBABEL_AGENT_SERVICE_BASE_URL:-http://embabel-agent-service:8081}
      AGENT_RUNTIME_CLIENT_SECRET: *agent-runtime-client-secret
      # Address advertised in Agent Cards; LiteLLM forwards to it (infrastructure/litellm/config.yaml).
      A2A_PUBLIC_BASE_URL: ${A2A_PUBLIC_BASE_URL:-http://embabel-agent-runtime:8082}
      AGENT_DEFINITIONS_LOCAL_ROOT: /agent-definitions
    # Required so the container can resolve the host on Linux Docker (where host.docker.internal is not
    # built in). Host access is granted to exactly two services -- litellm, because it is the component that
    # calls the host Ollama daemon, and embabel-agent-runtime, because retaining that grant is what makes a
    # revert to direct Ollama access (LLM_CODING_ROUTE=direct) a configuration-only change during an incident.
    extra_hosts:
      - "host.docker.internal:host-gateway"
    healthcheck:
      # readiness of the runtime itself (no database); the image ships bash but no curl
      test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8082 && printf 'GET /actuator/health/readiness HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' >&3 && grep -q '\"status\":\"UP\"' <&3"]
      interval: 15s
      timeout: 5s
      retries: 20
      start_period: 60s
    depends_on:
      litellm:
        condition: service_healthy                    # BR-16
      local-seed:
        condition: service_completed_successfully     # the model key and the service clients must exist
    volumes:
      # optional local override of the definitions baked into the image (edit without rebuilding)
      - ./services/embabel-agent-runtime/agent-definitions:/agent-definitions:ro

  # Separate MCP server that exposes the git integration as tools (checkout, create branch,
  # pull, read/write files, commit, push). Registered with LiteLLM (infrastructure/litellm/config.yaml,
  # mcp_servers.openjcockpit_git) and reached through the gateway; local-seed grants it to the
  # embabel-agent-runtime key (agents' git tools), the embabel-agent-service key (checkpoint commits) and
  # the ai-control-service key (git-workspace jobs).
  # Git-workspace jobs clone into a workspace here (job-<uuid>); credentials are resolved server-side.
  git-mcp-server:
    build:
      context: .
      dockerfile: services/git-mcp-server/Dockerfile
    container_name: openjcockpit-git-mcp
    restart: unless-stopped
    # Not published on the host: it has no authentication of its own and resolves job credentials
    # server-side, so the only way in is LiteLLM (key + object_permission.mcp_servers).
    expose:
      - "8093"
    environment:
      GIT_MCP_WORKSPACE_BASE: /git-mcp-workspaces
      # Retention window and reap interval for the per-run git workspace isolation directories
      # under the existing git-mcp-workspaces named volume — no new volume is created.
      GIT_MCP_RUN_WORKSPACE_RETENTION: ${GIT_MCP_RUN_WORKSPACE_RETENTION:-PT24H}
      GIT_MCP_RUN_WORKSPACE_REAP_INTERVAL: ${GIT_MCP_RUN_WORKSPACE_REAP_INTERVAL:-PT1H}
      # Job workspaces (job-<uuid>) get their git credentials from ai-control, never as tool arguments;
      # git-mcp-server authenticates with its own Keycloak service client (seeded by local-seed).
      GIT_MCP_AI_CONTROL_URL: http://ai-control-service:8080
      GIT_MCP_TOKEN_URL: http://keycloak:8080/realms/openjcockpit/protocol/openid-connect/token
      GIT_MCP_CLIENT_ID: openjcockpit-git-mcp
      GIT_MCP_CLIENT_SECRET: *git-mcp-client-secret
    volumes:
      - git-mcp-workspaces:/git-mcp-workspaces

  opa:
    image: openpolicyagent/opa:0.70.0@sha256:2b1c4b231f6dfa6a2baea4af4109203b10cd4b65e836d72f03bfe5d8dfce7095
    container_name: openjcockpit-opa
    restart: unless-stopped
    # Policies are mounted as plain files (no bundle service) so edits under
    # infrastructure/opa/policies are picked up on container restart without a rebuild.
    command:
      - "run"
      - "--server"
      - "--addr=0.0.0.0:8181"
      - "--config-file=/config/config.yaml"
      - "/policies"
    ports:
      - "8181:8181"
    volumes:
      - ./infrastructure/opa/config.yaml:/config/config.yaml:ro
      - ./infrastructure/opa/policies:/policies:ro

volumes:
  workflow-workspaces:
  workflow-definitions:
  git-mcp-workspaces:
