name: agentteam-local-cluster

# Local-only full AgentTeam stack. AgentTeam MaaS and AgentTeam Ops default to sibling checkouts.

x-agentteam-server-image: &agentteam-server-image
  image: agentteam-server:local
  build:
    context: .
    dockerfile: Dockerfile.server
    args:
      AGENTTEAM_SERVER_VERSION: local
      AGENTTEAM_SERVER_GIT_COMMIT: local

x-agentteam-server-environment: &agentteam-server-environment
  AGENTTEAM_ENV: staging
  AGENTTEAM_LOG_LEVEL: debug
  AGENTTEAM_HTTP_ADDRESS: :3001
  AGENTTEAM_PUBLIC_BASE_URL: http://localhost:${AGENTTEAM_PORT:-28080}
  AGENTTEAM_DAEMON_CONNECT_BASE_URL: http://localhost:${AGENTTEAM_PORT:-28080}
  AGENTTEAM_DATABASE_DSN: root@tcp(tidb:4000)/agentteam_core?multiStatements=true&parseTime=true&loc=UTC
  MESHX_DATABASE_DSN: root@tcp(tidb:4000)/agentteam_meshx?multiStatements=true&parseTime=true&loc=UTC
  MESHX_AGS_DATABASE_DSN: root@tcp(tidb:4000)/agentteam_ags?multiStatements=true&parseTime=true&loc=UTC
  MESHX_AGS_GIT_REPO_DIR: /data/meshx/ags-repos
  MESHX_CONTENT_STORE_ENABLED: ${MESHX_CONTENT_STORE_ENABLED:-false}
  MESHX_CONTENT_STORE_FILESYSTEM_ROOT: /data/meshx/content-store
  # Optional: add MESHX_KB_SOURCE_MIRROR_OUTBOUND_PROXY only when the host
  # actually exposes a reachable HTTP proxy.
  MESHX_AGS_ADMIN_LOGIN: meshx-admin
  MESHX_AGS_ADMIN_TOKEN: meshx-local-admin-token
  MESHX_AGS_ALLOW_ANY_TOKEN: "true"
  MESHX_AUTH_ENABLED: "true"
  MESHX_AUTH_TRUSTED_JWT_CLAIMS: "true"
  MESHX_AUTH_TRUSTED_JWT_CLAIMS_PROTECTED_LISTENER: "true"
  MESHX_AUTH_JWT_HEADER: X-AgentTeam-Verified-JWT
  MESHX_KNOWLEDGE_QA_DEBUG_MODEL_OUTPUT: "true"
  MESHX_KNOWLEDGE_QA_WEB_SEARCH_PROVIDER: searxng
  MESHX_KNOWLEDGE_QA_SEARXNG_BASE_URL: http://searxng:8080
  MESHX_AUTOMATION_SERVER_RUNNER_ENABLED: "false"
  MESHX_AUTOMATION_BLOB_FILESYSTEM_ROOT: /data/meshx/automation-blobs
  MESHX_AUTOMATION_CREDENTIALS_MASTER_KEY: nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A=
  MESHX_PERSONAL_ACCESS_TOKEN_CREDENTIALS_MASTER_KEY: 11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=
  MESHX_KB_SOURCE_MIRROR_WORK_DIR: /data/meshx/kb-source-mirrors
  MESHX_KB_PUBLIC_REPO_CACHE_DIR: /data/meshx/public-repo-cache
  MESHX_INTERNAL_ADMIN_SERVICE_TOKEN: meshx-local-admin-token
  MESHX_WIKI_WORK_DIR: /data/meshx/kb-code-wiki
  AGENTTEAM_ARCHIVE_DRIVER: filesystem
  AGENTTEAM_ARCHIVE_FILESYSTEM_ROOT: /data/archive
  AGENTTEAM_MEDIA_OBJECT_STORAGE_DRIVER: disabled
  AGENTTEAM_MAIL_DRIVER: noop
  AGENTTEAM_MOBILE_VERIFY_DRIVER: noop
  AGENTTEAM_PASSWORD_AUTH_ENABLED: "true"
  AGENTTEAM_AUTH_REGISTRATION_MODE: open
  AGENTTEAM_ORG_CREDENTIALS_KEY_FILE: /data/secrets/org_credentials_master_key
  AGENTTEAM_APP_CREDENTIALS_KEY_FILE: /data/secrets/app_credentials_master_key
  AGENTTEAM_INTERNAL_ADMIN_TOKEN: agentteam-local-admin-token
  AGENTTEAM_MESHX_ENABLED: "true"
  AGENTTEAM_MESHX_KNOWLEDGE_BASES_ENABLED: "true"
  AGENTTEAM_MAAS_BASE_URL: http://agentteam-maas:8080
  # This value is projected to a daemon running on the Docker host.
  AGENTTEAM_MAAS_RUNTIME_BASE_URL: http://localhost:${AGENTTEAM_MAAS_PORT:-28082}/maas/v1
  AGENTTEAM_MAAS_INTERNAL_BEARER_TOKEN: agentteam-maas-local-internal-token
  AGENTTEAM_MAAS_CAPABILITY_ED25519_PRIVATE_KEY_FILE: /run/secrets/agentteam-maas-capability-key.json
  AGENTTEAM_BILLING_ENABLED: "false"

services:
  tidb:
    image: pingcap/tidb:v8.5.6
    command:
      - --store=unistore
      - --path=/var/lib/tidb
      - --host=0.0.0.0
      - --status=10080
    ports:
      - "127.0.0.1:${TIDB_PORT:-24000}:4000"
      - "127.0.0.1:${TIDB_STATUS_PORT:-21080}:10080"
    volumes:
      - tidb-data:/var/lib/tidb
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:10080/status >/dev/null 2>&1"]
      interval: 2s
      timeout: 3s
      retries: 60
      start_period: 10s
    restart: unless-stopped

  databases:
    image: mysql:8.4
    depends_on:
      tidb:
        condition: service_healthy
    entrypoint: ["/bin/sh", "-ec"]
    command:
      - |
        until mysql --protocol=TCP --host=tidb --port=4000 --user=root --ssl-mode=DISABLED --execute='SELECT 1' >/dev/null 2>&1; do
          sleep 2
        done
        mysql --protocol=TCP --host=tidb --port=4000 --user=root --ssl-mode=DISABLED <<'SQL'
        CREATE DATABASE IF NOT EXISTS agentteam_core CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
        CREATE DATABASE IF NOT EXISTS agentteam_meshx CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
        CREATE DATABASE IF NOT EXISTS agentteam_ags CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
        CREATE DATABASE IF NOT EXISTS agentteam_maas CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
        CREATE DATABASE IF NOT EXISTS agentteam_ops CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
        SQL
    restart: "no"

  agentteam-schema:
    <<: *agentteam-server-image
    command: ["/app/bin/agentteam-schema", "init"]
    environment: *agentteam-server-environment
    depends_on:
      databases:
        condition: service_completed_successfully
    volumes:
      - agentteam-data:/data
    restart: "no"

  agentteam-server:
    <<: *agentteam-server-image
    environment: *agentteam-server-environment
    configs:
      - source: agentteam-maas-capability-key
        target: /run/secrets/agentteam-maas-capability-key.json
        mode: 0444
    depends_on:
      agentteam-schema:
        condition: service_completed_successfully
      agentteam-maas:
        condition: service_healthy
      searxng:
        condition: service_started
    volumes:
      - agentteam-data:/data
    healthcheck:
      test: ["CMD", "bash", "-ec", "exec 3<>/dev/tcp/127.0.0.1/3001; printf 'GET /readyz HTTP/1.0\\r\\nHost: localhost\\r\\n\\r\\n' >&3; grep -q 'HTTP/1.[01] 200' <&3"]
      interval: 3s
      timeout: 3s
      retries: 30
      start_period: 10s
    restart: unless-stopped

  agentteam-web:
    image: agentteam-web-meshx:local
    build:
      context: .
      dockerfile: Dockerfile.web
      args:
        AGENTTEAM_DEV_FEATURES: MESHX
    restart: unless-stopped

  agentteam-ingress:
    image: nginx:1.27-alpine
    depends_on:
      agentteam-server:
        condition: service_healthy
      agentteam-web:
        condition: service_started
    ports:
      - "127.0.0.1:${AGENTTEAM_PORT:-28080}:80"
    configs:
      - source: agentteam-ingress-nginx
        target: /etc/nginx/conf.d/default.conf
        mode: 0444
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1/readyz >/dev/null 2>&1"]
      interval: 5s
      timeout: 3s
      retries: 30
      start_period: 20s
    restart: unless-stopped

  agentteam-maas:
    image: agentteam-maas:local
    build:
      context: ${AGENTTEAM_MAAS_SOURCE_DIR:-${HOME}/work/code/agentteam-maas}
      dockerfile: Dockerfile
    environment:
      AGENTTEAM_MAAS_ADDR: :8080
      AGENTTEAM_MAAS_DATABASE_DSN: root@tcp(tidb:4000)/agentteam_maas?multiStatements=true&parseTime=true&loc=UTC
      AGENTTEAM_MAAS_CAPABILITY_ISSUER: agentteam
      AGENTTEAM_MAAS_CAPABILITY_AUDIENCE: agentteam-maas
      AGENTTEAM_MAAS_CAPABILITY_ED25519_PUBLIC_KEYS: '{"agentteam-local-dev":"11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo="}'
      AGENTTEAM_MAAS_INTERNAL_BEARER_TOKEN: agentteam-maas-local-internal-token
    depends_on:
      databases:
        condition: service_completed_successfully
    ports:
      - "127.0.0.1:${AGENTTEAM_MAAS_PORT:-28082}:8080"
    healthcheck:
      test: ["CMD", "bash", "-ec", "exec 3<>/dev/tcp/127.0.0.1/8080; printf 'GET /healthz HTTP/1.0\\r\\nHost: localhost\\r\\n\\r\\n' >&3; grep -q 'HTTP/1.[01] 200' <&3"]
      interval: 3s
      timeout: 3s
      retries: 30
      start_period: 10s
    restart: unless-stopped

  searxng:
    image: ${SEARXNG_IMAGE:-docker.io/searxng/searxng:latest}
    environment:
      FORCE_OWNERSHIP: "false"
    volumes:
      - ./docker/searxng:/etc/searxng:ro
      - searxng-data:/var/cache/searxng
    expose:
      - "8080"
    restart: unless-stopped

  agentteam-ops-server:
    image: agentteam-ops-server:local
    build:
      context: ${AGENTTEAM_OPS_SOURCE_DIR:-${HOME}/work/code/agentteam-ops}
      dockerfile: Dockerfile.server
    environment:
      AGENTTEAM_OPS_HTTP_ADDRESS: :3002
      AGENTTEAM_OPS_STORAGE_DSN: root@tcp(tidb:4000)/agentteam_ops?multiStatements=true&parseTime=true&loc=UTC
      AGENTTEAM_CORE_STORAGE_DSN: root@tcp(tidb:4000)/agentteam_core?multiStatements=true&parseTime=true&loc=UTC
      AGENTTEAM_MAAS_DATABASE_DSN: root@tcp(tidb:4000)/agentteam_maas?multiStatements=true&parseTime=true&loc=UTC
      # OIDC is optional for local account login, but the server validates these fields.
      AGENTTEAM_OPS_AUTH_ISSUER: http://localhost:${AGENTTEAM_OPS_PORT:-28081}/local-oidc
      AGENTTEAM_OPS_AUTH_CLIENT_ID: agentteam-ops-local
      AGENTTEAM_OPS_AUTH_CLIENT_SECRET: agentteam-ops-local-secret
      AGENTTEAM_OPS_AUTH_REDIRECT_URI: http://localhost:${AGENTTEAM_OPS_PORT:-28081}/oidc-callback
      AGENTTEAM_ADMIN_BASE_URL: http://agentteam-server:3001
      AGENTTEAM_ADMIN_SERVICE_TOKEN: agentteam-local-admin-token
      MESHX_ADMIN_BASE_URL: http://agentteam-server:3001
      MESHX_ADMIN_SERVICE_TOKEN: meshx-local-admin-token
      AGENTTEAM_MAAS_BASE_URL: http://agentteam-maas:8080
      AGENTTEAM_MAAS_INTERNAL_BEARER_TOKEN: agentteam-maas-local-internal-token
      AGENTTEAM_MEDIA_OBJECT_STORAGE_DRIVER: disabled
    depends_on:
      agentteam-schema:
        condition: service_completed_successfully
      agentteam-maas:
        condition: service_healthy
      agentteam-server:
        condition: service_healthy
    restart: unless-stopped

  agentteam-ops-seed:
    image: mysql:8.4
    environment:
      # Login: admin@agentteam.local / agentteam-local-admin (local development only).
      OPS_ADMIN_PASSWORD_HASH: '$$2a$$10$$6kL87NtF6LcIp5U1gND3tOuudx6Pita/74nONyWs/oJ8qAircBRc2'
    depends_on:
      agentteam-ops-server:
        condition: service_started
    entrypoint: ["/bin/sh", "-ec"]
    command:
      - |
        attempts=0
        until mysql --protocol=TCP --host=tidb --port=4000 --user=root --ssl-mode=DISABLED --database=agentteam_ops --execute='SELECT 1 FROM users LIMIT 1' >/dev/null 2>&1; do
          attempts=$$((attempts + 1))
          if [ "$$attempts" -ge 30 ]; then
            echo 'agentteam-ops users schema did not become ready' >&2
            exit 1
          fi
          sleep 2
        done
        mysql --protocol=TCP --host=tidb --port=4000 --user=root --ssl-mode=DISABLED --database=agentteam_ops --execute="
          INSERT IGNORE INTO users (
            id, email, name, display_name, avatar_url, password_hash, role, status, created_at, updated_at
          ) VALUES (
            'opsusr_local_admin', 'admin@agentteam.local', 'Local Admin', 'Local Admin', '',
            '$${OPS_ADMIN_PASSWORD_HASH}', 'admin', 'active', UTC_TIMESTAMP(), UTC_TIMESTAMP()
          );
        "
    restart: "no"

  agentteam-ops-web:
    image: agentteam-ops-web:local
    build:
      context: ${AGENTTEAM_OPS_SOURCE_DIR:-${HOME}/work/code/agentteam-ops}
      dockerfile: Dockerfile.web
    depends_on:
      agentteam-ops-seed:
        condition: service_completed_successfully
      agentteam-server:
        condition: service_healthy
    ports:
      - "127.0.0.1:${AGENTTEAM_OPS_PORT:-28081}:80"
    configs:
      - source: agentteam-ops-nginx
        target: /etc/nginx/conf.d/default.conf
        mode: 0444
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1/readyz >/dev/null 2>&1"]
      interval: 5s
      timeout: 3s
      retries: 30
      start_period: 20s
    restart: unless-stopped

configs:
  # RFC 8032 Ed25519 test vector; local development only, never use in production.
  agentteam-maas-capability-key:
    content: |
      {"key_id":"agentteam-local-dev","public_key":"11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=","private_key":"nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2DXWpgBgrEKt9VL/tPJZAc6DuFy89qmIyWvAhpo9wdRGg=="}

  agentteam-ingress-nginx:
    content: |
      map $$http_upgrade $$connection_upgrade {
          default upgrade;
          "" close;
      }

      # Docker service IPs change when Compose recreates containers.
      resolver 127.0.0.11 valid=5s ipv6=off;
      resolver_timeout 3s;

      upstream agentteam_server {
          zone agentteam_server 64k;
          server agentteam-server:3001 resolve;
      }

      upstream agentteam_web {
          zone agentteam_web 64k;
          server agentteam-web:80 resolve;
      }

      server {
          listen 80;
          server_name _;
          client_max_body_size 100m;

          location ~ ^/(api|internal|daemon|socket\.io)(/|$$) {
              proxy_http_version 1.1;
              proxy_set_header Host $$host;
              proxy_set_header X-Real-IP $$remote_addr;
              proxy_set_header X-Forwarded-For $$proxy_add_x_forwarded_for;
              proxy_set_header X-Forwarded-Proto $$scheme;
              proxy_set_header Upgrade $$http_upgrade;
              proxy_set_header Connection $$connection_upgrade;
              proxy_read_timeout 90s;
              proxy_pass http://agentteam_server;
          }

          location = /healthz {
              proxy_pass http://agentteam_server;
          }

          location = /readyz {
              proxy_pass http://agentteam_server;
          }

          location / {
              proxy_pass http://agentteam_web;
          }
      }

  agentteam-ops-nginx:
    content: |
      # Docker service IPs change when Compose recreates containers.
      resolver 127.0.0.11 valid=5s ipv6=off;
      resolver_timeout 3s;

      upstream agentteam_ops_server {
          zone agentteam_ops_server 64k;
          server agentteam-ops-server:3002 resolve;
      }

      upstream agentteam_server {
          zone agentteam_server 64k;
          server agentteam-server:3001 resolve;
      }

      server {
          listen 80;
          server_name _;
          root /usr/share/nginx/html;
          index index.html;
          client_max_body_size 100m;

          location ^~ /api/ops/ {
              proxy_http_version 1.1;
              proxy_set_header Host $$host;
              proxy_set_header X-Real-IP $$remote_addr;
              proxy_set_header X-Forwarded-For $$proxy_add_x_forwarded_for;
              proxy_set_header X-Forwarded-Proto $$scheme;
              proxy_pass http://agentteam_ops_server;
          }

          location ~ ^/(api|internal|daemon|socket\.io)(/|$$) {
              proxy_http_version 1.1;
              proxy_set_header Host $$host;
              proxy_set_header X-Real-IP $$remote_addr;
              proxy_set_header X-Forwarded-For $$proxy_add_x_forwarded_for;
              proxy_set_header X-Forwarded-Proto $$scheme;
              proxy_pass http://agentteam_server;
          }

          location = /healthz {
              proxy_pass http://agentteam_ops_server;
          }

          location = /readyz {
              proxy_pass http://agentteam_ops_server;
          }

          location / {
              try_files $$uri $$uri/ /index.html;
          }
      }

volumes:
  tidb-data:
    name: ${AGENTTEAM_LOCAL_TIDB_VOLUME:-loop-local-cluster-tidb-data}
  agentteam-data:
  searxng-data:
