SatQuery / docs /DEPLOYMENT.md
thundercode's picture
release: add docs/DEPLOYMENT.md
5ce82be verified
|
Raw History Blame
16 kB
# Deployment
**Status tags:** `IMPLEMENTED` Β· `VERIFIED` Β· `MEASURED` Β· `OPEN` Β· `DEFERRED` Β· `BLOCKED` Β· `BY DESIGN`.
The live SatQuery AI system runs across **three private repositories** plus one **public umbrella
repository**. The monorepo working copy is **not** the deployed source. This document records the live
topology, every environment variable, the deploy mechanics, the cold-start behaviour, the known
issues, and the platform traps that cost real debugging time.
> **The single most important trap in this document.** `deploy/` inside the monorepo is **stale and
> untracked**. It is **not** the deployed source. Edits must go to the three real repositories.
---
## 1. Live revisions (VERIFIED)
Read from the GitHub API during the release reconnaissance.
| Component | Repository | Visibility | Branch | Revision | Host |
|---|---|---|---|---|---|
| Frontend | `Anish-lab-blip/SatQuery-Frontend` | **private** | `main` | **`2d7ae53b482d`** | Cloudflare Pages β†’ `satquery.pages.dev` |
| Backend / orchestrator | `Anish-lab-blip/SatQuery-Backend` | **private** | `main` | **`89d80eaddec5`** | Render β†’ `satquery-backend-m4yv.onrender.com` |
| Inference | `Anish-lab-blip/SatQuery-Inference` | **private** | `main` | **`5a0936ace491`** | Codespace `potential-space-trout-r4ppw969w45j2pvvw`, port 8000 |
| Public umbrella | `Anish-lab-blip/SatQuery-AI` | **public** | `main` | `3dcabd32da41` ("Initial commit") | this release home |
| Monorepo (working copy) | `C:/Users/anish/satquery-ai` | local only | `master` | `9d57aed` | **no git remote**; 334 dirty entries |
The public umbrella repository previously contained **only a 13-byte README** (`# SatQuery-AI`). It is
the intended home for this release.
## 2. The four tiers
```
Browser
β”‚ HTTPS
β–Ό
Cloudflare Pages β€” satquery.pages.dev (static frontend, 11 pages)
β”‚ HTTPS / JSON β†’ /api/*
β–Ό
Render β€” satquery-backend-m4yv.onrender.com (orchestrator / gateway)
β”‚ outbound long-poll POST /tunnel/agent
β–Ό
GitHub Codespace β€” FastAPI inference, CPU, port 8000
β”‚ build_space_app()
β–Ό
specialists: SmolVLM Β· RemoteCLIP Β· MiniLM Β· CROMA Β· STANet
β”‚
β–Ό
ResultEnvelope β†’ tunnel β†’ Render β†’ browser
```
```mermaid
flowchart LR
U[Browser] -->|HTTPS| CF["Cloudflare Pages<br/>static frontend"]
CF -->|"HTTPS JSON<br/>/api/health Β· /api/capabilities Β· /api/infer Β· /api/assets"| R["Render<br/>orchestrator / gateway"]
R -->|"outbound long-poll<br/>POST /tunnel/agent"| C["GitHub Codespace<br/>FastAPI inference :8000"]
C --> S[(SmolVLM Β· RemoteCLIP<br/>MiniLM Β· CROMA Β· STANet)]
C -->|ResultEnvelope| R
R -->|"envelope + error translation"| CF
```
## 3. Why each tier exists
### 3.1 Cloudflare Pages β€” the static tier
Serves the frontend. **No backend, no secrets, and no API calls of its own** on the static pages. The
one exception is the Analyze console, which calls the gateway. Staged by `scripts/stage_pages.mjs` and
deployed with `npx wrangler pages deploy`.
### 3.2 Render β€” the gateway
A deliberately **thin, stateless** orchestrator. Its responsibilities, from
`docs/DEPLOYMENT_ARCHITECTURE.md` Β§2:
| Responsibility | Detail |
|---|---|
| Schema validation | reject malformed requests before they cost inference |
| Size limits | whole-request body cap, shared with the Codespace |
| Rate limiting | per-IP count + window β€” **fairness, NOT a security control** |
| CORS allowlist | the Pages origin; **never `*`** |
| Request ids | correlate a request across tiers |
| Timeouts | sit inside the task budget |
| Secret custody | holds credentials that must never reach the browser |
| Error translation | upstream failures β†’ the documented error envelope |
It is **not** a model host. It has **no database, no auth, and no queue**.
> **Hard rule.** The gateway must **not** retry `POST /api/infer` on its own β€” a retry would consume
> inference a second time. The client decides on retry.
### 3.3 GitHub Codespace β€” the inference tier
Runs the real inference service: `build_space_app()` from `app/space_app.py`, served by
`deploy/codespace/serve.py` on `$PORT`, in **CPU mode**. It honours the four-endpoint contract
(`/v1/health`, `/v1/capabilities`, `/v1/analyze`, `/v1/assets`), imports cheaply without torch, reuses
`app/serving.py` as the composition root, and **degrades rather than crashes** on absent artifacts.
### 3.4 Hugging Face β€” the model tier
Holds the six trained artifacts and the model card. It is **not** the runtime inference host; the
Codespace resolves the pinned backbones from the Hub at run time.
## 4. Why the transport is an outbound tunnel
The inference host is a Codespace in a **private** repository. A forwarded port for a private repo
returns **`302`**, so an inbound-forwarding design cannot work. Instead:
- the Codespace runs `deploy/codespace/tunnel_agent.py`;
- the agent **dials out** to `POST /tunnel/agent` and long-polls;
- work is executed against `http://127.0.0.1:8000` **locally**.
This inverts the usual direction: the inference host needs **no inbound firewall hole**. It also means
the transport is only alive while the agent is polling.
**Measured:** `GET /api/health` reported `tunnel.agent_connected: true` with a non-zero `completed`
counter, and `POST /api/infer {}` returned `422 invalid_request` with the response header
`x-satquery-transport: tunnel`.
## 5. The full live health payload (VERIFIED, probed)
```json
{
"status": "ok",
"service": "satquery-orchestrator",
"tunnel": {
"agent_connected": true,
"agent_id": "codespaces-fd1038",
"pending": 0,
"completed": 97
},
"config": {
"codespace_name": "potential-space-trout-r4ppw969w45j2pvvw\n",
"codespace_port": 8000,
"transport_mode": "auto",
"tunnel_timeout_s": 150.0,
"wake_timeout_s": 120.0,
"upstream_timeout_s": 90.0,
"device": "cpu",
"has_github_token": true
}
}
```
Note `codespace_name` still carries a **trailing `\n`** β€” this is **B-02**, cosmetic and `OPEN`; the
wake path strips it (`_codespace_name()`), so only the health payload reports the raw value.
## 6. Environment variables
### 6.1 Render (gateway) β€” measured live values
| Variable | Value (live) | Purpose |
|---|---|---|
| `CODESPACE_NAME` | `potential-space-trout-r4ppw969w45j2pvvw` | which Codespace to wake |
| `CODESPACE_PORT` | `8000` | the inference port |
| `SATQUERY_ALLOWED_ORIGINS` | `https://satquery.pages.dev` | CORS allowlist (never `*`) |
| `SATQUERY_DEVICE` | `cpu` | device preference |
| `SATQUERY_TRANSPORT` | `auto` | tunnel first, then forward |
| `SATQUERY_TUNNEL_TIMEOUT_S` | `150` | how long to wait on the tunnel |
| `SATQUERY_WAKE_TIMEOUT_S` | `120` | how long to wait for a cold start |
| `SATQUERY_UPSTREAM_TIMEOUT_S` | `90` | gateway β†’ upstream budget |
| `GITHUB_TOKEN` | present | Codespace control |
| `PORT` | platform-supplied | Render's own listen port |
> **Measured absence.** There is **no** `SATQUERY_UPSTREAM_URL` and **no** `HF_TOKEN` in the live
> config. The transport is the outbound tunnel, not a forwarded port. This contradicts the older
> `docs/DEPLOYMENT_TOPOLOGY.md` Β§3.2 table, which predates the tunnel design.
### 6.2 Codespace (inference)
| Variable | Purpose |
|---|---|
| `PORT` | platform-assigned; **must be read** (a historical blocker was that no code read it) |
| `SATQUERY_DEVICE` | `cpu` \| `cuda` \| `mps` \| `null`; read **without importing torch** |
| `SATQUERY_MAX_FILE_BYTES` | per-file cap, shared with Render |
| `SATQUERY_ASSET_ENABLED` / `SATQUERY_ASSET_DIR` | both required for `/v1/assets`; **fails closed (503)** otherwise |
| `SATQUERY_ASSET_MAX_FILES` / `SATQUERY_ASSET_TTL_S` | optional handle capacity / lifetime |
### 6.3 Config-loader environment overrides
Two registry values can be overridden from the environment without editing the YAML
(`core/config.py`):
| Variable | Effect |
|---|---|
| `SATQUERY_PRECISION` | overrides `training.precision` |
| `SATQUERY_TORCH_COMPILE` | overrides `deployment.torch_compile` (`"true"` β†’ `True`) |
Both are still validated by the loader β€” setting `SATQUERY_TORCH_COMPILE=true` **fails startup**
because finding C-8 forbids `torch.compile`.
## 7. Deploy mechanics
| Tier | Mechanism |
|---|---|
| Frontend β†’ Cloudflare Pages | `scripts/stage_pages.mjs` builds a Pages bundle; `npx wrangler pages deploy` |
| Backend β†’ Render | `render.yaml` blueprint; `main.py` exposes the ASGI object `app` |
| Inference β†’ Codespace | `deploy/codespace/serve.py` binds `build_space_app()` to `$PORT`; `.devcontainer/` forwards 8000 and starts the tunnel agent via `postStartCommand` |
| Repository writes | the **GitHub Git Data API** β€” blob β†’ tree β†’ commit β†’ `PATCH` ref |
### 7.1 Why the Git Data API instead of `git push`
Every deployed file is uploaded as a **blob** whose sha256 is **computed locally and verified against
the uploaded blob**, then assembled into a tree, committed, and the branch ref patched. This means:
- each file is **content-verified** rather than trusted;
- deletions are expressed explicitly as `sha: null` tree entries;
- the deploy is **idempotent** β€” re-running it with identical content produces no change.
**Measured:** 9 deployed files were re-read from the API and found **sha256 byte-identical** to the
local copies, with the deployed HEAD re-read independently (`verify_deployed_head.py`).
## 8. Cold start (documented, not hidden)
Render's free tier sleeps when idle, and the Codespace may be stopped. Before a request can be served,
Render must start the Codespace (if stopped) and wait for the tunnel agent to reconnect. The frontend
shows *"Waking inference engine…"* during this.
| Property | Value |
|---|---|
| Cold start | **tens of seconds** |
| Tunnel wait before falling through | `SATQUERY_TUNNEL_TIMEOUT_S` = 150 s |
| Wake wait | `SATQUERY_WAKE_TIMEOUT_S` = 120 s |
| Upstream budget | `SATQUERY_UPSTREAM_TIMEOUT_S` = 90 s |
Cold start is **documented rather than papered over**: an honest "this will take a while the first
time" is better than a silent hang.
### 8.1 The `transport_mode: auto` fallthrough β€” B-07 (`OPEN`)
`SATQUERY_TRANSPORT=auto` means: **try the tunnel; on timeout, fall through to the forward path.**
The forward path to a **private** repo returns `302` quickly β€” but the wake step still consumes
`SATQUERY_WAKE_TIMEOUT_S` (120 s) **first**. So a worst-case failed request takes roughly
```
150 s (tunnel timeout) + 120 s (wake timeout on a 302) β‰ˆ 249 s
```
This is the **root shape** of the observed transient tunnel gap, and it is why a request can appear to
hang and then fail. A patch (`fix-b07-forward-unavailable.patch`) was authored and verified
(`py_compile` clean, applies cleanly to the deployed `main.py`), adding distinct
`forward_unavailable` (503, terminal 302/401/403 on the forward path) and `upstream_timeout`
(504, tunnel healthy but slow) codes, plus the `codespace_name` `.strip()` fix.
> **Status: B-07 is `OPEN`.** The patch is **prepared but NOT deployed.** The deployed health payload
> still shows the trailing `\n`.
## 9. The five historical backend blockers
Before any backend could boot, five verified blockers had to be closed. The current design closes them:
| # | Blocker (verified) | How it is closed |
|---|---|---|
| 1 | `requirements.txt` declared no `fastapi` / `uvicorn` / `httpx` / `starlette` | the runtime installs the ASGI stack so `build_space_app()` and the gateway can import |
| 2 | no code read `$PORT` β€” a platform port would be ignored | `deploy/codespace/serve.py` binds to `$PORT`; Render reads its own |
| 3 | hand-rolled CORS raised `405` on `OPTIONS`, so browser preflight failed | the gateway registers `OPTIONS` explicitly / uses Starlette's CORS middleware |
| 4 | module-level `app = create_app()` swallowed config errors into `app = None` | construction errors now **propagate** (fail-fast) instead of leaving a dead `app` |
| 5 | adapter integrity unverified on load (`_adapter_sha256` computed but never compared) | the load path compares the digest against an expected value, or fails startup |
## 10. Platform traps (recorded so they are not rediscovered)
| Trap | Detail |
|---|---|
| **Cloudflare `_headers` CONCATENATE** | Two matching rules are **merged, not overridden**. A specific rule nested under a broad `/assets/img/*` rule yields `max-age=604800, …, max-age=0, must-revalidate` β€” and Chromium takes the **FIRST** `max-age`. The file's own "later rules override" comment is **false**. |
| **Cloudflare 308 redirect** | `X.html` β†’ `/X`. Reference the extensionless path. |
| **Forwarded port returns 302** | for a private repo β€” this is *why* the tunnel exists. |
| **Tunnel agent must start on boot** | via the devcontainer `postStartCommand`, or a restarted Codespace comes up with `agent_connected: false`. |
| **Never retry `/api/infer` at the gateway** | a retry consumes inference twice. |
| **`deploy/` is stale and untracked** | not the deployed source. |
## 11. The superseded design, and what did NOT change
The earlier design ran inference on an **HF Space with ZeroGPU** (5 GPU-min/day, `@spaces.GPU`
decoration) behind a **Railway** gateway. The active design changes three things:
1. **CPU-first instead of ZeroGPU.** No code change was required β€” `device_preference` honours
`SATQUERY_DEVICE` and defaults to CPU, every specialist defaults to `device="cpu"`, and all
placement is `.to(device)` (never `.cuda()`).
2. **A real, always-buildable inference environment.** A Codespace gives a reproducible container
without a GPU quota or a Space's cold-start constraint. The wake flow replaces ZeroGPU lazy
loading as the cold-start story.
3. **No GPU quota to protect at the gateway.** Rate/size limits remain, but as **fairness** controls
rather than quota protection.
**What did NOT change:**
| Unchanged | Detail |
|---|---|
| the four-endpoint contract | `/v1/health`, `/v1/capabilities`, `/v1/analyze`, `/v1/assets` |
| the gateway responsibility table | Β§3.2 above |
| the env-var vocabulary | only host names moved (`SATQUERY_SPACE_URL` β†’ `SATQUERY_UPSTREAM_URL`) |
| the config freeze | `78f1e3700da15aa1` |
`configs/deploy.yaml` still describes the old HF-Space/ZeroGPU target (`platform: huggingface-spaces`,
`sdk: gradio`, `zerogpu: true`, the `gpu_duration_*` values). It is **frozen paperwork**: no Gradio
runtime exists in code, and editing it would move `Config.hash`. It is left undisturbed.
## 12. Deployment preconditions (open items)
1. Cloudflare Pages project name / domain (needed for the deploy command and `robots.txt` sitemap).
2. Artifacts present, **or** capabilities honestly ship `available: false` β€” the service degrades
rather than crashes on absent artifacts.
3. `HF_TOKEN` on Render **if** the HF proxy path is used (not used in the live config).
4. Codespace `.devcontainer/` forwarding `:8000` **and** starting the tunnel agent.
5. The five blockers in Β§9 closed and verified on the first live run.
## 13. What is `NOT RUN` / `OPEN` / `BLOCKED` for this topic
| Item | State |
|---|---|
| B-07 tunnel fallthrough | **OPEN** β€” patch prepared, NOT deployed |
| B-02 `codespace_name` trailing `\n` | **OPEN** (cosmetic) |
| A deployed system-level load test | **NOT RUN** |
| Multi-region / HA deployment | **NOT RUN** |
| A production database / persistence | **BY DESIGN** (absent) |
| The ZeroGPU/Gradio deployment target | **REJECTED** (superseded; frozen paperwork only) |
## 14. Where the evidence lives
| Evidence | Location |
|---|---|
| Live health + capabilities probe | `release/CURRENT_RELEASE_STATE.md` Β§1 |
| Topology contract | `docs/DEPLOYMENT_TOPOLOGY.md`, `docs/DEPLOYMENT_ARCHITECTURE.md` |
| The undeployed B-07 patch | session scratch: `fix-b07-forward-unavailable.patch` |
| Deployed-artifact integrity check | `verify_deployed_head.py` (session scratch) |
| Live validation (3 passes) | `.workbuddy-ai/scratch/live_validation/` |