name: Audit production runtime (read-only) on: workflow_dispatch: permissions: contents: read concurrency: group: audit-production-runtime cancel-in-progress: false jobs: audit: runs-on: ubuntu-latest steps: - name: Inspect production over SSH uses: appleboy/ssh-action@v1.0.3 with: host: ${{ secrets.EC2_HOST }} username: ubuntu key: ${{ secrets.EC2_SSH_KEY }} command_timeout: 10m script: | set -eu cd ~/app printf '%s\n' '=== source ===' printf 'git_sha=' git rev-parse HEAD printf 'git_branch=' git branch --show-current cd infra/docker ai_id=$(sudo docker compose -f docker-compose.prod.yml ps -q ai-service) web_id=$(sudo docker compose -f docker-compose.prod.yml ps -q web) postgres_id=$(sudo docker compose -f docker-compose.prod.yml ps -q postgres) qdrant_id=$(sudo docker compose -f docker-compose.prod.yml ps -q qdrant) test -n "$ai_id" test -n "$web_id" test -n "$postgres_id" test -n "$qdrant_id" printf '%s\n' '=== containers ===' for entry in "ai-service:$ai_id" "web:$web_id" "postgres:$postgres_id" "qdrant:$qdrant_id"; do service=${entry%%:*} container=${entry#*:} state=$(sudo docker inspect --format '{{.State.Status}}' "$container") image_id=$(sudo docker inspect --format '{{.Image}}' "$container") printf '%s state=%s image_id=%s\n' "$service" "$state" "$image_id" done printf '%s\n' '=== ai_runtime_contract ===' sudo docker exec -i "$ai_id" python - <<'PY' import json from config import Settings settings = Settings() safe_fields = ( "app_name", "environment", "qdrant_url", "qdrant_collection", "embedding_provider", "embedding_dimensions", "evidence_minimum_score", "aws_region", "answer_provider", "answer_model_id", "rerank_enabled", "metrics_enabled", "otel_enabled", "otel_service_name", "otel_exporter_otlp_endpoint", "otel_sample_ratio", "entities_path", "max_wall_clock_ms", "max_llm_calls_per_turn", ) contract = {name: str(getattr(settings, name)) for name in safe_fields} print(json.dumps(contract, ensure_ascii=True, sort_keys=True)) PY printf '%s\n' '=== persistent_mounts ===' for entry in "postgres:$postgres_id" "qdrant:$qdrant_id"; do service=${entry%%:*} container=${entry#*:} sudo docker inspect --format \ "$service {{range .Mounts}}{{.Type}}:{{.Name}}:{{.Destination}} {{end}}" \ "$container" done printf '%s\n' '=== datastore_identity ===' sudo docker exec -i "$ai_id" python - <<'PY' import json import urllib.request from config import Settings settings = Settings() url = settings.qdrant_url.rstrip("/") + "/collections/" + settings.qdrant_collection with urllib.request.urlopen(url, timeout=10) as response: payload = json.load(response) result = payload.get("result", {}) config = result.get("config", {}).get("params", {}).get("vectors", {}) print(json.dumps({ "collection": settings.qdrant_collection, "points_count": result.get("points_count"), "status": result.get("status"), "vector_config": config, }, ensure_ascii=True, sort_keys=True)) PY printf '%s\n' '=== health ===' sudo docker exec -i "$ai_id" python - <<'PY' import urllib.request for path in ("/health", "/ready"): with urllib.request.urlopen("http://127.0.0.1:8000" + path, timeout=10) as response: print(path, response.status) PY