Image-to-Text
PyTorch
Safetensors
PEFT
English
remote-sensing
satellite-imagery
earth-observation
change-detection
visual-grounding
image-captioning
visual-question-answering
optical-sar-fusion
sar
multimodal
lora
Instructions to use thundercode/SatQuery with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use thundercode/SatQuery with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
File size: 45,966 Bytes
08c9328 5ce82be 08c9328 5ce82be af0705e 5ce82be af0705e 944cdb9 af0705e 08c9328 af0705e 08c9328 af0705e 5ce82be 08c9328 5ce82be 944cdb9 5ce82be af0705e 08c9328 af0705e 08c9328 af0705e 08c9328 5ce82be 944cdb9 5ce82be af0705e 5ce82be 08c9328 5ce82be af0705e 08c9328 5ce82be 08c9328 af0705e 08c9328 5ce82be 08c9328 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be 08c9328 af0705e 5ce82be 08c9328 af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be 08c9328 af0705e 08c9328 af0705e 5ce82be 08c9328 944cdb9 af0705e 08c9328 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 08c9328 5ce82be 08c9328 5ce82be 08c9328 5ce82be af0705e 5ce82be 08c9328 5ce82be af0705e 08c9328 5ce82be af0705e 08c9328 5ce82be 08c9328 af0705e 5ce82be 08c9328 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 08c9328 5ce82be 08c9328 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be 08c9328 5ce82be af0705e 5ce82be 08c9328 af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e 5ce82be af0705e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 | # 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**, serving a **static frontend** on Cloudflare Pages, a **thin orchestrator / gateway** on
Render, and a **CPU inference service** in a GitHub Codespace reached over an **outbound tunnel**. The
monorepo working copy is **not** the deployed source.
This document is the exhaustive deployment reference: the four tiers, every live revision, every
environment variable (with measured live values), the deploy mechanics per tier, cold-start
behaviour, the five historical backend blockers, the platform traps, and the superseded design that
the active one replaced. It is written to be readable *without* the source tree, but every non-obvious
claim carries the file it came from.
> **The single most important trap in this document.** `deploy/` inside the monorepo is **stale and
> untracked**. It is **not** the deployed source. The deployed backend is `SatQuery-Backend/main.py`;
> the monorepo's `deploy/render/main.py` is an earlier, tunnel-less revision. Edits must go to the
> three real repositories, never to the local `deploy/` tree.
> **Hostnames are deliberately not published.** The orchestrator's public hostname appears throughout
> this release as `<backend-host>`. The deployment is documented for reproducibility — the topology,
> the environment-variable *names*, the timeout chain and the failure modes are all exact — without
> advertising the live endpoint. The three deployment repositories are private and are not part of
> this release.
**Companions.** [`architecture/02-deployment-topology.md`](architecture/02-deployment-topology.md)
(the long-form topology treatment), [`OPERATIONS.md`](OPERATIONS.md) (running the live system),
[`SECURITY.md`](SECURITY.md) (the trust boundary),
[`architecture/10-observability-and-ops.md`](architecture/10-observability-and-ops.md) (health,
traces and the operator surface), [`LIMITATIONS.md`](LIMITATIONS.md) §4 (operational limitations),
[`RESEARCH_NOTES.md`](RESEARCH_NOTES.md) §6 (the `auto`-mode fallthrough), and
[`TESTING.md`](TESTING.md) (how the deploy-time properties are tested).
---
## 1. How to read this document
| Convention | Meaning |
|---|---|
| **VERIFIED** | Read from a live endpoint, a git ref, or a file on disk during the release reconnaissance |
| **MEASURED** | A value with a recorded number and its source |
| **IMPLEMENTED** | Code exists; whether it ran is stated separately |
| **NOT RUN** | The work has not been executed |
| **OPEN** | A known defect or gap that is not closed |
| **BY DESIGN** | Deliberately absent, and the reason is recorded |
| **UNKNOWN** | `UNKNOWN — not established from the available evidence` |
Every revision, environment variable and finding below was read from a file or a live endpoint. Where
a value could not be established, the text says so rather than guessing.
### 1.1 The three deploy sources versus the working copy
| Artifact | Location | Role |
|---|---|---|
| Frontend source | `Anish-lab-blip/SatQuery-Frontend` (private) | staged from the monorepo's `frontend/` contents |
| Backend source | `Anish-lab-blip/SatQuery-Backend` (private) | Render orchestrator (`main.py`, tunnel client) |
| Inference source | `Anish-lab-blip/SatQuery-Inference` (private) | Codespace FastAPI + `deploy/codespace/tunnel_agent.py` |
| Public umbrella | `Anish-lab-blip/SatQuery-AI` (public) | the intended release home |
| Working copy | `C:/Users/anish/satquery-ai` | **local only, no git remote** |
The working copy's `deploy/` is untracked (`git ls-files deploy/` returns empty) and stale. Its
`deploy/render/main.py` is a **tunnel-less** revision (532 lines by the project's own record); the
deployed `SatQuery-Backend/main.py` is **768 lines** and carries the tunnel client
(`docs/FINAL_DELIVERY_TODO.md` §1.1). The tunnel agent
(`deploy/codespace/tunnel_agent.py`) is **not present in the monorepo working copy at all** — it lives
in the `SatQuery-Inference` repository, and the monorepo's `deploy/codespace/launch.sh` refers to it as
a path that only exists in the deployed checkout.
---
## 2. Live revisions (VERIFIED)
Read from the GitHub API during the release reconnaissance (`release/CURRENT_RELEASE_STATE.md` §1).
| 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 → `<backend-host>` |
| 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 |
| Hugging Face | `thundercode/SatQuery` | **public** | `main` | lastModified `2026-09-25T16:26:53Z` | 2 files only: `.gitattributes`, 25-byte `README.md` |
Notes that must not be smoothed over:
- The public umbrella `SatQuery-AI` contains **only** `README.md` (13 bytes: `# SatQuery-AI`). At the
time of the reconnaissance it was effectively empty; it is the intended home for this release.
- The dirty-entry count is a **snapshot**. `release/CURRENT_RELEASE_STATE.md` §1 records **334** dirty
entries at release reconnaissance; `docs/FINAL_DELIVERY_TODO.md` §1.1 records **323** (294 untracked,
20 modified, 8 deleted) at an earlier capture. The working tree changed between the two captures.
- The three deployed repositories are **private**. Their GitHub links return `404` for an outside
audience. This is **BY DESIGN** (see §11.4).
- The Hugging Face repository `thundercode/SatQuery` carried **only two files** at reconnaissance
(`.gitattributes` and a 25-byte `README.md`). It is **not** the runtime inference host; the Codespace
resolves pinned backbones from the Hub at run time.
### 2.1 A note on the "HEAD re-read" verification
Nine deployed frontend files were re-read from the GitHub API and found **sha256 byte-identical** to
the local copies, with the deployed HEAD re-read independently (`verify_deployed_head.py`,
`release/CURRENT_RELEASE_STATE.md` §5). Three live-validation passes ran against successive HEADs:
pass 1 against `ff46eba42b18` + `d413d3672311`, passes 2 and 3 against the final HEAD `2d7ae53b482d`.
No run id is shared between passes.
---
## 3. The four tiers
```
Browser
│ HTTPS
▼
Cloudflare Pages — satquery.pages.dev (static frontend, 11 pages)
│ HTTPS / JSON → /api/*
▼
Render — <backend-host> (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
```
The important inversion: the middle arrow is **outbound from the inference host**, not inbound to it.
That is the whole reason the design works for a private repository (§4).
### 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 (`mission.html`), which calls the gateway.
- **Staged by:** `scripts/stage_pages.mjs` (builds a Pages bundle).
- **Deployed with:** `npx wrangler pages deploy`.
- **Deploy result (measured, `docs/DEPLOYMENT_DECISION.md` §7):** 60 files staged, 39,173,936 B
(37.36 MiB) total, largest file `assets/video/satquery-launch-50s.mp4` at 22,710,313 B (21.66 MiB),
with 3,504,087 B of headroom under the 25 MiB per-file limit; 0 missing references; **0 external
network dependencies (HERMETIC)**; exit 0.
- **Env vars:** none (static). The Pages project name / domain is still open (§12).
> **The hermetic claim is scoped.** `docs/DEPLOYMENT_DECISION.md` §3 audited `frontend/` (excluding
> `.tools/`) and found zero occurrences of `fetch(`, `XMLHttpRequest`, `axios`, `EventSource`,
> `WebSocket`, `/v1/`, `import.meta.env` or `process.env`. `docs/DEPLOYMENT_TOPOLOGY.md` §3.1 narrows
> this: the "no API calls of any kind" statement holds for every **static** page **except**
> `mission.html`, which calls the orchestrator. The monorepo `README.md`'s older claim that the
> frontend is "hermetic — no backend calls" is materially stale.
### 3.2 Render — the gateway
A deliberately **thin, stateless** orchestrator. It holds **no model, no state, no database**, and
performs **no auth** (`deploy/render/main.py` module docstring; plan §73/§74). 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** (`docs/DEPLOYMENT_ARCHITECTURE.md` §5.2) |
| CORS allowlist | the Pages origin; **never `*`** |
| Request ids | correlate a request across tiers |
| Timeouts | sit inside the task budget (§7.5) |
| Secret custody | holds credentials that must never reach the browser |
| Error translation | upstream failures → the documented error envelope (§7.6) |
It is **not** a model host. It has **no database, no auth, and no queue**.
The gateway's proxied routes (from `deploy/render/main.py`):
| Gateway route | Upstream | Notes |
|---|---|---|
| `GET /api/health` | answered **locally** | reports the orchestrator's own config; never answers for the Codespace |
| `POST /api/infer` | `POST {codespace}/v1/analyze` | wake-then-proxy; sets `X-SatQuery-State: waking|ready` |
| `GET /api/capabilities` | `GET {codespace}/v1/capabilities` | **no second copy** of the capability table |
| `POST /api/assets` | `POST {codespace}/v1/assets` | raw/multipart body relayed verbatim |
> **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. The reason is recorded in code
> (`deploy/render/main.py` docstring; `docs/STEP7_BACKEND_CHAIN_REPORT.md` §11).
> **No second capability table.** The gateway proxies `/v1/capabilities` and nothing else decides
> "what can this deployment do?". The authoritative sources are `core.planner.CAPABILITY_ASSETS` and
> `SpecialistSpec.requires_assets` (`docs/DEPLOYMENT_ARCHITECTURE.md` §2.2).
#### 3.2.1 The CORS allowlist is assembled, not just read
`deploy/render/main.py::_allowed_origins` assembles the allowlist in a fixed order:
1. `SATQUERY_ALLOWED_ORIGINS` — the operator's comma-separated list (authoritative for extra origins).
2. `_PRODUCTION_ORIGINS` — `https://satquery.pages.dev`, **always present**, so a missing env var
cannot take the live site down.
3. `_DEV_ORIGINS` — 10 explicit `host:port` pairs (`localhost` and `127.0.0.1` × ports
`3000/5500/5173/8000/8080`), added unless `SATQUERY_ALLOW_DEV_ORIGINS` is one of `0`/`false`/`no`/`""`.
A wildcard `*` raises `ValueError` — checked both in `_allowed_origins` and in
`GatewayConfig.__post_init__`, because `CORSMiddleware` does not run that validator
(`deploy/render/main.py`). The list is deliberately explicit, never a regex or suffix match, so
allowing localhost for development cannot admit an arbitrary remote site. The health payload reports
the **effective** list, so a production deployment can prove from outside that the dev origins were
turned off.
#### 3.2.2 The wake flow
`deploy/render/main.py::ensure_codespace_up()` returns `(base_url, woke)`:
1. `GET` the Codespace via the GitHub API (`deploy/render/codespaces.py::get_codespace`).
2. If `state != "available"`, `POST .../start` (`start_codespace`; GitHub returns `202`, and `204` is
also seen in practice).
3. Poll `GET {base}/v1/health` until `200` or until `SATQUERY_WAKE_TIMEOUT_S` elapses.
Polling knobs: `_WAKE_POLL_INTERVAL_S = 2.0`, `_WAKE_HEALTH_TIMEOUT_S = 10.0`
(`deploy/render/main.py`). The public base URL is derived by `forwarded_url()`, which prefers the
Codespace JSON's `web_url` and rewrites its trailing port segment, falling back to
`https://{name}-{port}.app.github.dev`. That host pattern is an **isolated assumption**: the module's
own docstring records that it "was **not verifiable from the build environment** (no live Codespace to
inspect)".
### 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
(`docs/DEPLOYMENT_ARCHITECTURE.md` §3.3).
The serve entrypoint is deliberately tiny (`deploy/codespace/serve.py`):
```python
from app.space_app import build_space_app
import uvicorn
app = build_space_app()
if __name__ == "__main__":
port = int(os.environ.get("PORT", "8000"))
uvicorn.run(app, host="0.0.0.0", port=port)
```
The composition root (`app/serving.py::build_serving_controller`) resolves `device` from
`SATQUERY_DEVICE`. It wires three capabilities through the registry's `builders=` seam **without
editing `configs/base.yaml`**:
| Capability | Wired artifact | Why the seam |
|---|---|---|
| `change` | `artifacts/change/levir_change_v001/head.pt` | `change.checkpoint_path` is unset; adding it to config would move `Config.hash` |
| `change_vqa` | `artifacts/change_vqa/run/head.pt` + the same STANet | closes the F2 **train/serve skew** (training and serving must share one detector) |
| `optical_sar` | CROMA (resolved from the pinned identity) + `artifacts/optical_sar/fusion_head_production_v001/head.pt` | `croma.checkpoint_path` is unset, so the encoder was unreachable by default |
The seam is a **call-site argument** (`core/registry.py`'s `builders=` override), not config, so
`Config.hash` stays `78f1e3700da15aa1` (`app/serving.py` docstring). **Degrade, do not crash:** absent
artifacts yield `available: false` **with a reason**; *corrupt* artifacts raise `ModelLoadError`. The
two are deliberately not conflated.
#### 3.3.1 The Codespace launcher and its survivability design
`.devcontainer/devcontainer.json` sets `postStartCommand: bash deploy/codespace/launch.sh`, so the
inference server and the tunnel agent start on **every** Codespace start. `launch.sh` is more
defensive than it looks, and the reasons are recorded in the script:
- **Preflight (refuse to start half-configured).** It checks `import yaml, pydantic, fastapi, uvicorn,
httpx` and `import app.space_app`, exiting non-zero with a diagnostic if either fails. `httpx` is
checked explicitly because `tunnel_agent.py` imports it directly and it was previously absent from
`requirements.txt`, so the agent "died instantly and the supervised restart loop hid the error in a
log file".
- **Stale-serve detection.** A stamp file (`/tmp/satquery-serve.stamp`) records `rev=<HEAD>
asset_enabled=<…> asset_dir=<…>`. If the running server's stamp disagrees with the current checkout
and environment, the serve process is restarted, because "a stale serve process is worse than no
process: it answers `/v1/health` and `/v1/capabilities` from OLD code".
- **The tunnel agent is supervised and immortal.** `setsid` alone is not enough in Codespaces — the
lifecycle shell that runs `postStartCommand` can still reap the process group, which "showed up in
production as 'the agent announced once, then vanished'". The launcher therefore uses
`setsid + nohup + </dev/null` around a supervising `while true` wrapper that re-launches the agent
if it exits, so the agent is "effectively immortal for the life of the Codespace".
- **Post-launch verification.** After a 4-second wait it checks the agent process is alive and that
the log contains a successful announce (`announced to hub`), because "backgrounding with all output
discarded means a crashing agent is completely invisible".
> `launch.sh` refers to `bash deploy/codespace/doctor.sh` in two diagnostics. `doctor.sh` is **not
> present in the monorepo working copy**; it lives in the deployed `SatQuery-Inference` checkout.
> `UNKNOWN — not established from the available evidence` whether it is present in that repository, as
> the private repository was not readable for this documentation pass.
#### 3.3.2 The `warm_cache.py` pre-warm
`deploy/codespace/post_create.sh` (`postCreateCommand`) installs the lean CPU requirements and runs
`python deploy/codespace/warm_cache.py`, which pre-downloads the pinned HF models into the HF cache so
the first `/v1/analyze` is fast. It is idempotent, reports per-model OK/SKIPPED/FAILED status, and
never aborts on a single miss (`deploy/codespace/README.md`).
### 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. At reconnaissance the public
repository `thundercode/SatQuery` contained **two files only** (`.gitattributes` and a 25-byte
`README.md`) — the model card / weights publication is a separate workstream from this deployment.
---
## 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` (from `SatQuery-Inference`);
- 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**, and GitHub's
port-forwarding relay, port visibility and the repository's visibility are all irrelevant. 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` (`release/CURRENT_RELEASE_STATE.md` §1; `docs/FINAL_DELIVERY_TODO.md`
§6 E-03).
When the Codespace is stopped, the agent stops polling → `GET /api/health` reports
`tunnel.agent_connected: false` and `POST /api/infer` parks until `SATQUERY_TUNNEL_TIMEOUT_S` (150 s),
then returns `tunnel_offline` (503, `recoverable: true`) (`docs/DEPLOYMENT_TOPOLOGY.md` §2).
> **The forwarded-port path is dead**, not merely unused: it returns `302` for the private repo. The
> GitHub-API wake path (`POST /user/codespaces/{name}/start`) still exists in
> `deploy/render/codespaces.py`, but the tunnel design relies on the agent reconnecting on Codespace
> start via the devcontainer `postStartCommand`.
---
## 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
}
}
```
Source: `release/CURRENT_RELEASE_STATE.md` §1. The `completed` counter is a live, monotonically
increasing value — later captures recorded `completed: 314` (`docs/FINAL_DELIVERY_TODO.md` §1.4) and
`completed: 338` (`docs/FINAL_DELIVERY_REPORT.md` §3). The count is a runtime fact, not a fixed
constant; do not quote it as a stable figure.
Two things in this payload are load-bearing:
1. **`codespace_name` still carries a trailing `\n`.** This is **B-02**, cosmetic and `OPEN`; the wake
path strips it (`_codespace_name()` calls `.strip()`), so only the `/api/health` reporting payload
shows the raw value (§8.2).
2. **`transport_mode` is `auto`.** This is the root shape of **B-07** (§8.1).
### 5.1 The live capability contract (VERIFIED, probed)
`GET /api/capabilities` → `schema_version 1.0`, **six entries, all `available: true`**
(`release/CURRENT_RELEASE_STATE.md` §1):
| task | requires_pair | max_assets | notes |
|---|---|---|---|
| `vqa` | false | 1 | SmolVLM weights fetched from the HF Hub on first use |
| `caption` | false | 1 | SmolVLM weights fetched from the HF Hub on first use |
| `grounding` | false | 1 | RemoteCLIP encoder fetched from the HF Hub on first use |
| `change` | true | 2 | — |
| `change_vqa` | true | 2 | — |
| `optical_sar` | true | 2 | `modalities: ["optical","sar"]` |
The capability table is served by the **single adapter** `app/deployment.py`, derived from the
registry's spec table plus filesystem presence. The adapter emits only contract vocabulary
(`loaded`/`absent`/`unavailable`/`not_requested`/`evicted`) and — precisely because it must not load a
model to answer a metadata request — it **never emits `loaded` or `evicted`**
(`docs/DEPLOYMENT_ARCHITECTURE.md` §3.3.1). `available: false` always carries a non-null `reason`.
---
## 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 (§8.1) |
| `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 (existence only; never recorded here) |
| `PORT` | platform-supplied | Render's own listen port |
Source: `docs/DEPLOYMENT_TOPOLOGY.md` (measured 2026-09-25 live note) and
`release/CURRENT_RELEASE_STATE.md` §1.
`render.yaml` in the monorepo declares the blueprint's env vars: `PORT`, `SATQUERY_ALLOWED_ORIGINS`,
`GITHUB_TOKEN`, `CODESPACE_NAME` (`sync: false` — set in the dashboard), plus `CODESPACE_PORT: "8000"`,
`SATQUERY_DEVICE: "cpu"`, `SATQUERY_WAKE_TIMEOUT_S: "120"`, `SATQUERY_UPSTREAM_TIMEOUT_S: "90"`
(`render.yaml`). The blueprint does **not** declare `SATQUERY_TRANSPORT` or `SATQUERY_TUNNEL_TIMEOUT_S`
— those are set in the live dashboard and are part of the deployed `SatQuery-Backend` revision, not the
monorepo's stale blueprint.
> **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 and `docs/DEPLOYMENT_ARCHITECTURE.md` §4, which predate the
> tunnel design (`release/CURRENT_RELEASE_STATE.md` §1 note; `docs/FINAL_DELIVERY_TODO.md` §1.7 item 4).
### 6.2 Codespace (inference)
| Variable | Purpose |
|---|---|
| `PORT` | platform-assigned; **must be read** (historical blocker #2, §9) |
| `SATQUERY_DEVICE` | `cpu` \| `cuda` \| `mps` \| `null`; read **without importing torch** |
| `SATQUERY_MAX_FILE_BYTES` | per-file cap, shared with Render so the two layers cannot disagree |
| `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 |
| `SATQUERY_HUB_URL` | the Render orchestrator the tunnel agent dials out to; default `https://<backend-host>` |
Sources: `docs/DEPLOYMENT_TOPOLOGY.md` §3.3; `deploy/codespace/launch.sh`.
`.devcontainer/devcontainer.json` sets `containerEnv`: `SATQUERY_DEVICE=cpu`, `PORT=8000`,
`SATQUERY_ASSET_ENABLED=1`, `SATQUERY_ASSET_DIR=/tmp/satquery-assets`. The launcher re-exports the
asset variables on every start because `containerEnv` is only applied when the container is
**created** — "setting it there alone would leave an already-running Codespace unconfigured until a
rebuild. This script runs on every start and is therefore the effective source of truth"
(`deploy/codespace/launch.sh`).
Asset-store defaults, from `docs/DEPLOYMENT_ARCHITECTURE.md` §4: handle capacity `32`, TTL `900 s`. A
malformed or non-positive value falls back to the default rather than becoming a zero TTL. The store
**refuses rather than evicts** a live handle, so a full store answers `503` (ambiguous with an
unconfigured store — see `docs/DEPLOYMENT_ARCHITECTURE.md` §5.1).
### 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` on the (historical) ZeroGPU target — and the loader
hard-fails on `deployment.torch_compile is True` (`core/config.py`; `configs/deploy.yaml` header;
`docs/STEP8_FINAL_CONFORMANCE_AUDIT.md` §6.1). This is an example of the loader refusing an incoherent
configuration rather than silently accepting it.
### 6.4 The environment-variable vocabulary, and where it moved
The active design kept the **env-var vocabulary** and moved only the host names. The superseded
design used `SATQUERY_SPACE_URL`; the tunnel design uses the Codespace name/port pair plus
`SATQUERY_HUB_URL` on the inference side (`docs/DEPLOYMENT_TOPOLOGY.md` §5). The older
`SATQUERY_UPSTREAM_URL` name is **not** set live.
---
## 7. Deploy mechanics per tier
| 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 Frontend deploy (measured)
```bash
cd C:/Users/anish/satquery-ai
node scripts/stage_pages.mjs \
--out=.deploy/dist-final \
--include=_headers \
--include=robots.txt \
--include=assets/img/eo/provenance.json \
--include=assets/img/eo/CREDITS.md
npx wrangler pages deploy "C:/Users/anish/satquery-ai/.deploy/dist-final" --project-name <name>
```
(`docs/DEPLOYMENT_DECISION.md` §7.) `_headers` and `robots.txt` must be **force-included** because no
page references them; `provenance.json` and `CREDITS.md` likewise. The measured staging result is
quoted in §3.1.
> **`_headers` cannot un-cache an asset — it concatenates.** See §10 for the Cloudflare trap and the
> cache-busting consequence (the EO pair was renamed to new `-720` URLs rather than given a new rule).
### 7.2 Backend deploy
`render.yaml` is the blueprint: `runtime: python`, `plan: free`, `buildCommand: pip install -r
deploy/render/requirements.txt`, `startCommand: uvicorn deploy.render.main:app --host 0.0.0.0 --port
$PORT`, `healthCheckPath: /api/health`. The deployed `SatQuery-Backend` repository is the source of
truth; the monorepo's `render.yaml` is a snapshot of the tunnel-less revision.
### 7.3 Inference deploy
`deploy/codespace/serve.py` binds `build_space_app()` to `$PORT`. `.devcontainer/devcontainer.json`
forwards `8000` as **public** and runs `launch.sh` on every start. The launcher's stale-serve detection
(§3.3.1) means a code or environment change causes the running server to be restarted rather than left
answering from old code.
### 7.4 Repository writes: the GitHub Git Data API
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**
(`blob → tree → commit → PATCH ref`). 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`;
`release/CURRENT_RELEASE_STATE.md` §5).
### 7.5 The timeout relationship (do not invert)
```
gateway upstream timeout < agent.timeout_seconds ≤ the inference host's own request budget
```
Both bounds are **derived from the frozen config**, not chosen
(`docs/BACKEND_DEPLOYMENT_RUNBOOK.md` §4.2):
| Quantity | Value | Source |
|---|---|---|
| `agent.timeout_seconds` | **120 s** | `configs/base.yaml` |
| `gpu_duration_vqa` | **20 s** | `configs/deploy.yaml` |
| `gpu_duration_grounding` | **45 s** | `configs/deploy.yaml` |
| `gpu_duration_change` | **30 s** | `configs/deploy.yaml` |
| `gpu_duration_optical_sar` | **45 s** | `configs/deploy.yaml` |
| Largest single `gpu_duration_*` | **45 s** | derived |
So the upstream timeout belongs **above 45 s** (the longest a single call may run) and **below 120 s**
(the host's own request budget). `GatewayConfig.__post_init__` refuses a timeout ≤ 45 s and ≥ 120 s
(`docs/PHASE19_FINAL_HARDENING.md` §3.2). The live value is `SATQUERY_UPSTREAM_TIMEOUT_S = 90`.
### 7.6 The error contract
Upstream failures are wrapped in the v1 envelope
`{"error": {"code", "message", "detail", "recoverable"}}` (`deploy/render/main.py` docstring;
`docs/DEPLOYMENT_ARCHITECTURE.md` §2.3):
| Condition | Status | `recoverable` | Code |
|---|---|---|---|
| Connection error to the Codespace | `502` | `true` | `upstream_unreachable` |
| Wake times out | `504` | `true` | `wake_timeout` |
| Non-JSON upstream body | `502` | `true` | `schema_validation_error` |
| Missing `GITHUB_TOKEN` / `CODESPACE_NAME` | `500` | `false` | `orchestrator_config_error` |
| Malformed request JSON | `400` | `false` | `invalid_request` |
The `code` is passed through **unchanged** — the gateway must not remap the taxonomy in
`core/errors.py`, because a gateway that remapped codes would make the frontend's error handling
unpredictable (`docs/DEPLOYMENT_ARCHITECTURE.md` §2.3). A non-JSON upstream error is never relayed
verbatim (defect **G-4**, `docs/STEP7_BACKEND_CHAIN_REPORT.md` §13).
---
## 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 |
| Codespace idle timeout | 30 min (GitHub REST: `idle_timeout_minutes=30`) |
Sources: `docs/DEPLOYMENT_TOPOLOGY.md` §2; `docs/FINAL_DELIVERY_TODO.md` §6 E-04.
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 (`release/CURRENT_RELEASE_STATE.md` §6; `docs/FINAL_DELIVERY_TODO.md` §5 B-07:
"in `auto` transport mode a tunnel timeout **falls through** to the forward path
(`SatQuery-Backend/main.py:546`), which then burns `wake_timeout_s=120` on a 302 → the observed 504").
A patch (`fix-b07-forward-unavailable.patch`) was authored and verified (`git apply --check` clean,
`py_compile` clean, applies to the deployed `89d80eaddec5`). It adds:
- `forward_unavailable` (**503**, terminal `302`/`401`/`403` on the forward path), and
- `upstream_timeout` (**504**, tunnel healthy but slow), and
- 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` and the fallthrough remains live.
### 8.2 B-02 — the trailing newline (`OPEN`, cosmetic)
The `/api/health` payload reports `codespace_name` with a trailing `\n`. This is **B-02**, confirmed
**still live** during the reconnaissance. It is **cosmetic**: the wake path is safe because
`_codespace_name()` calls `.strip()` (`SatQuery-Backend/main.py:123-124`) and the wake path uses it
(`main.py:357`); only the health-reporting payload (`main.py:619`) reads the raw env var
(`docs/FINAL_DELIVERY_TODO.md` §4 P2-T03). Fix = change line 619 to `_codespace_name()`, then Render
redeploys. **Deferred** because a live-backend redeploy before the demonstration was not judged worth
the risk. **Status: `OPEN` (cosmetic).**
---
## 9. The five historical backend blockers
Before any backend could boot, five verified blockers had to be closed. Each was re-verified as a real
blocker (`docs/DEPLOYMENT_DECISION.md` §8), and 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 `CORSMiddleware` |
| 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 |
`docs/DEPLOYMENT_TOPOLOGY.md` §4 records these honestly as "closed by construction / to be verified on
the first live run" at the time it was written. The live system subsequently ran and served all six
tasks (`docs/FINAL_DELIVERY_REPORT.md` §4), which is the evidence that the blockers are closed in
production.
### 9.1 The defects the first real run found
`docs/STEP7_BACKEND_CHAIN_REPORT.md` §13 records that the ASGI layer had never executed, and that
running it surfaced four live defects immediately:
| ID | Defect | Severity | Status |
|---|---|---|---|
| **G-1** | `request: Request` never resolved (an in-function import left `Request` out of `__globals__`), so **every POST body was misread as a missing query parameter** and no handler ever ran | **Critical** | **FIXED** |
| **G-2** | an unsupported `force_task` enum value was forwarded upstream instead of refused locally | High | **FIXED** |
| **G-3** | an empty `HF_TOKEN` produced `Authorization: Bearer `, which httpx rejects → a crash reported as an upstream failure | High | **FIXED** |
| **G-4** | a non-JSON upstream error body was relayed verbatim, breaking the error contract and leaking internal text | High | **FIXED** |
The lesson recorded there is worth carrying: "the first hour of actually running the gateway found a
critical defect that had been invisible for as long as nobody could run it". The gateway is the
validation boundary; a gateway that misreads every body while the tests stay green is exactly the
failure a documented blocker hides.
---
## 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**. Measured live 2026-09-25 (`docs/FINAL_DELIVERY_TODO.md` §1.7 item 9). |
| **Cloudflare 308 redirect** | `X.html` → `/X`. Reference the extensionless path. |
| **Forwarded port returns 302** | for a private repo — this is *why* the tunnel exists (§4). |
| **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 (§3.2). |
| **`deploy/` is stale and untracked** | not the deployed source (§1.1). |
| **Edge-cache serves deleted files** | The old EO pair URLs still answer `200` from Cloudflare's edge cache (`CF-Cache-Status: HIT`, `Age: 1076`) although the files are deleted; a cache-busted request returns `404`. Nothing references them (`LIVE_VALIDATION_POSTFIX.md`, "Known residuals"). |
| **`containerEnv` applies only at container creation** | hence `launch.sh` re-exports the asset variables on every start (§6.2). |
| **`setsid` alone does not survive `postStartCommand`** | the lifecycle shell can reap the process group; the launcher uses `setsid + nohup + </dev/null` plus a supervising wrapper (§3.3.1). |
---
## 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 (`docs/DEPLOYMENT_ARCHITECTURE.md` §1;
`docs/DEPLOYMENT_TOPOLOGY.md` §5). 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()`). ZeroGPU's GPU-minute quota and `@spaces.GPU`
decoration are no longer on the critical path (`docs/DEPLOYMENT_DECISION.md` §5).
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` → the Codespace name/port pair) |
| 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
(`docs/DEPLOYMENT_DECISION.md` §4; `configs/deploy.yaml` header).
### 11.1 The ZeroGPU/Gradio target, in full, and why it is inert
`configs/deploy.yaml` carries `registry: false`, which makes its non-membership in the config registry
machine-readable; `core/config.py` reads exactly one file (`configs/base.yaml`) through a single
`yaml.safe_load` and never globs `configs/*.yaml`. `scripts/validate_deploy_config.py` asserts the
manifest is inert and that its `deployment:` block is byte-for-byte equal to `configs/base.yaml`'s.
There is **no Gradio runtime**: no `import gradio`, no `gr.Blocks`, no `gr.Interface`, and the one
ZeroGPU code path — `spaces.GPU(duration=…)` inside `decorate_gpu()` — "is never applied to any
route"; routes use plain `@api.get`/`@api.post` (`docs/DEPLOYMENT_DECISION.md` §4). The real
entrypoint is FastAPI: `build_space_app()`.
> The `spaces` package is not installed, so the `@spaces.GPU(duration=…)` path has **never executed**;
> `decorate_gpu()` returns an identity decorator when `spaces` is absent, which is the correct CPU
> behaviour (`docs/PHASE19_FINAL_HARDENING.md` §5.2). **Status: REJECTED (superseded; frozen
> paperwork only).**
### 11.2 The stale `hf/` docs
`hf/SETUP.md` and `hf/README.md` assert that the project "does not own any model weights … ships no
weights, no binaries, and no large artifacts" and that "this environment has no Hugging Face
credentials". Both were **false** at release time — six trained artifacts exist
(`release/CURRENT_RELEASE_STATE.md` §6). This is a documentation defect, not a deployment defect; it is
recorded in [`LIMITATIONS.md`](LIMITATIONS.md) §6.
### 11.3 The stale monorepo `README.md`
The monorepo `README.md` calls the frontend *"hermetic — no backend calls"* (it calls `/api/*` on
Render), puts Render/Codespace as *"in progress"* (both deployed), describes a 4-endpoint `/v1/*`
contract (the live gateway contract is `/api/*`), omits the tunnel, and points at the stale untracked
`deploy/` as the deployment source (`release/CURRENT_RELEASE_STATE.md` §6).
### 11.4 Why the three deploy repos are private
`SatQuery-Frontend`, `SatQuery-Backend` and `SatQuery-Inference` are **private**; their links `404` for
an outside audience. This is **BY DESIGN** — the public umbrella `SatQuery-AI` is the audience-facing
home. The only public code repository at reconnaissance was `Anish-lab-blip/SatQuery-AI`
(`docs/FINAL_DELIVERY_TODO.md` §4 P9-T01).
---
## 12. Deployment preconditions (open items)
1. **Cloudflare Pages project name / domain** — needed for the deploy command and the `Sitemap:` line
in `robots.txt`. `sitemap.xml` was deliberately **not** created because it needs a canonical
production domain (`docs/DEPLOYMENT_DECISION.md` §6).
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 (measured
absence, §6.1).
4. **Codespace `.devcontainer/` forwarding `:8000` and starting the tunnel agent** — both present in
the live deployment.
5. **The five blockers in §9 closed and verified on the first live run** — closed; the live system
served all six tasks (`docs/FINAL_DELIVERY_REPORT.md` §4).
6. **The B-07 patch deployed** — **NOT done**; B-07 remains `OPEN` (§8.1).
---
## 13. What is `NOT RUN` / `OPEN` / `BLOCKED` for this topic
| Item | State |
|---|---|
| B-07 tunnel fallthrough (`auto` mode ≈ 249 s worst case) | **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) |
| Auth / user accounts / queue | **BY DESIGN** (absent; plan §73/§74) |
| The ZeroGPU/Gradio deployment target | **REJECTED** (superseded; frozen paperwork only) |
| `doctor.sh` present in the deployed Inference repo | `UNKNOWN — not established from the available evidence` |
| A latency benchmark of the deployed path | **NOT RUN** (see [`PERFORMANCE.md`](PERFORMANCE.md), [`LIMITATIONS.md`](LIMITATIONS.md) §3) |
---
## 14. Where the evidence lives
| Evidence | Location |
|---|---|
| Live health + capabilities probe | `release/CURRENT_RELEASE_STATE.md` §1 |
| Active topology contract | `docs/DEPLOYMENT_TOPOLOGY.md` |
| Backend contract (gateway responsibilities, env vocabulary, failure modes) | `docs/DEPLOYMENT_ARCHITECTURE.md` §1.1, §2, §3.3, §4, §5 |
| Deployment decision + the frozen HF manifest | `docs/DEPLOYMENT_DECISION.md` |
| Operator runbook (local verification, timeout window, rollback) | `docs/BACKEND_DEPLOYMENT_RUNBOOK.md` |
| Deployment packaging / open items | `docs/PHASE18_DEPLOYMENT_PACKAGING.md` |
| Final hardening + ASGI defects | `docs/PHASE19_FINAL_HARDENING.md`, `docs/STEP7_BACKEND_CHAIN_REPORT.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, 24 runs) | `.workbuddy-ai/scratch/live_validation/` |
| Delivery status + blocker register | `docs/FINAL_DELIVERY_TODO.md`, `docs/FINAL_DELIVERY_REPORT.md` |
|