# ANT — self-hosted cloud override (multi-user, Google sign-in, NO billing). # # docker compose -f docker-compose.yml -f docker-compose.cloud.yml up -d # # Requires a .env (or exported shell vars) with at least: # ANT_JWT_PUBLIC_KEY + ANT_JWT_PRIVATE_KEY (see below), # GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET # See docs/guides/self-host-cloud.md for the full checklist. # # YAML anchors are file-local, so this file cannot merge into the base's # `x-ant-env-common` anchor. Instead each ant service sets its own # `environment:` keys — compose merges per-service environment maps key-wise, # so ANT_REDIS_URL / ANT_WORKSPACE_BASE_PATH etc. from the base still apply. # # ANT_REQUIRE_BILLING is deliberately NOT set here — it is reserved for the # managed deployment (fails boot when the @ant/cloud billing overlay is # absent). Self-hosted cloud runs unmetered. # # Secrets are scoped to the services that CONSUME them, not applied uniformly. # ant-preview spawns user-authored install and dev commands, and those children # share its UID and process namespace — so anything in that service's # environment is reachable from user code via /proc. Sign-in secrets have no # consumer there, so they are not granted there (report C-001). # # Sessions are ES256 only. Generate a P-256 pair once and put the private half # on ant-api only: # # openssl ecparam -genkey -name prime256v1 -noout \ # | openssl pkcs8 -topk8 -nocrypt -out jwt-private.pem # openssl ec -in jwt-private.pem -pubout -out jwt-public.pem # ANT_JWT_PRIVATE_KEY="$(cat jwt-private.pem)" # ANT_JWT_PUBLIC_KEY="$(cat jwt-public.pem)" # # Child UID isolation (ANT_CHILD_UID/GID below) is the complementary control. # Every ant service: mode + the origin CORS and cookie derivation read. x-ant-env-cloud-base: &ant-env-cloud-base ANT_SERVER_MODE: cloud FRONTEND_URL: ${FRONTEND_URL:-http://localhost:4200} # Session VERIFICATION (ant-realtime, ant-preview). The ES256 public key # carries no minting authority — safe even where user-authored code can read # the env via /proc (C-001, M-NEW-013). x-ant-env-cloud-verify: &ant-env-cloud-verify ANT_JWT_PUBLIC_KEY: ${ANT_JWT_PUBLIC_KEY:-} # Session SIGNING. Consumed ONLY by ant-api, the only process that mounts the # auth router and therefore the only one that mints a session. x-ant-env-cloud-sign: &ant-env-cloud-sign ANT_JWT_PRIVATE_KEY: ${ANT_JWT_PRIVATE_KEY:-} # Sign-in and administration. Consumed ONLY by ant-api, which is the only # process that mounts the auth and admin routers. x-ant-env-cloud-identity: &ant-env-cloud-identity GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:?set GOOGLE_CLIENT_ID in .env (Google Cloud Console OAuth client)} GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:?set GOOGLE_CLIENT_SECRET in .env (Google Cloud Console OAuth client)} GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:-} # empty = derived as ${FRONTEND_URL}/api/auth/google/callback ANT_SUPER_ADMIN_EMAILS: ${ANT_SUPER_ADMIN_EMAILS:-} services: ant-api: environment: <<: [*ant-env-cloud-base, *ant-env-cloud-verify, *ant-env-cloud-sign, *ant-env-cloud-identity] ant-realtime: environment: <<: [*ant-env-cloud-base, *ant-env-cloud-verify] # No HTTP surface → verifies nothing, signs nothing. # # Cloud mode REFUSES user-authored spawns (run_command, stdio MCP) unless # children can be dropped to a different UID *and GID* (childIdentity gate). # The image ships the `ant-child` account (uid/gid 10001, no login); changing # UID/GID needs CAP_SETUID/CAP_SETGID, which a non-root service does not have — # so the two user-code services run as container root and every user-authored # child is dropped to ant-child with its supplementary groups cleared. # # ANT_CHILD_GID MUST be non-empty: an empty GID emits no setpriv --regid, and # the dropped child keeps egid 0 + root's groups (H-014). It defaults to the # ant-child gid; override both only to match a different image account. # # The credentialed dependency FETCH gets a THIRD identity (ant-acquire, 10002): # it carries a user's registry PAT in its environment, and /proc//environ # is readable by any process sharing its UID — so it must not share the one # every other user-code child runs under (M-NEW-001). Same group as ant-child # so the installed tree stays writable by the credential-free lifecycle pass. ant-job: user: "0:0" # bwrap builds each user-authored child's mount namespace from an # unprivileged user namespace. Docker's default seccomp and AppArmor # profiles refuse CLONE_NEWUSER without CAP_SYS_ADMIN, so lift them for the # two services that run user code (cheaper than granting SYS_ADMIN). security_opt: - seccomp=unconfined - apparmor=unconfined environment: <<: *ant-env-cloud-base ANT_CHILD_UID: ${ANT_CHILD_UID:-10001} ANT_CHILD_GID: ${ANT_CHILD_GID:-10001} # Credentialed dependency-FETCH runs under its OWN uid so a same-UID peer # cannot read the user's PAT from /proc//environ (M-NEW-001). ANT_CHILD_ACQUIRE_UID: ${ANT_CHILD_ACQUIRE_UID:-10002} ANT_CHILD_ACQUIRE_GID: ${ANT_CHILD_ACQUIRE_GID:-10001} # Verifies proxy ownership; never serves sign-in. Runs user-authored code, so # it receives the public key ONLY. ant-preview: user: "0:0" # bwrap builds each user-authored child's mount namespace from an # unprivileged user namespace. Docker's default seccomp and AppArmor # profiles refuse CLONE_NEWUSER without CAP_SYS_ADMIN, so lift them for the # two services that run user code (cheaper than granting SYS_ADMIN). security_opt: - seccomp=unconfined - apparmor=unconfined environment: <<: [*ant-env-cloud-base, *ant-env-cloud-verify] ANT_CHILD_UID: ${ANT_CHILD_UID:-10001} ANT_CHILD_GID: ${ANT_CHILD_GID:-10001} # Credentialed dependency-FETCH runs under its OWN uid so a same-UID peer # cannot read the user's PAT from /proc//environ (M-NEW-001). ANT_CHILD_ACQUIRE_UID: ${ANT_CHILD_ACQUIRE_UID:-10002} ANT_CHILD_ACQUIRE_GID: ${ANT_CHILD_ACQUIRE_GID:-10001}