Repository navigation
[obvious-fleet] make local-api serves the capture API on 127.0.0.1 and stays local #281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
obvious-autobuild
wants to merge
8
commits into
alpha
Choose a base branch
from
obv/burst11-bs-local-api-serve
base: alpha
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
64a4803
feat(backend): one-command local capture API on loopback (make local-…
SamGu-NRX 993c362
test: pin the local capture API journey over a real socket
SamGu-NRX d0774fd
docs: local capture API evidence — recorded live journey, smoke run, …
SamGu-NRX 330164c
test(backend): pin the local-api PORT default and loud empty-port ref…
SamGu-NRX 8c92df1
fix(backend): give local-api the documented PORT default; refuse an e…
SamGu-NRX 2a39e93
Merge origin/alpha into obv/burst11-bs-local-api-serve
ObviousApp 0bdee23
test(backend): pin phony targets and local-dispatch env forcing
SamGu-NRX dc7cdee
fix(backend): force local dispatch, refuse inherited shell env, phony…
SamGu-NRX File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # Local capture API — evidence | ||
|
|
||
| Produced 2026-10-16 on a fresh clone of `alpha` at `4bcbedba`, branch | ||
| `obv/burst11-bs-local-api-serve`, head `993c3627`. Everything here was | ||
| captured against the real stack: the launcher script under uvicorn over a | ||
| TCP socket, no TestClient, no drain_finalize. | ||
|
|
||
| | File | What it shows | | ||
| |---|---| | ||
| | `local-api-terminal.png` | Frame from the `make local-api` recording: the launcher banner — base URL, session-file path, the stand-in stage list with `placement` real, and the tokens-never-on-stdout note. | | ||
| | `local-api.cast` | Full asciinema recording of `make local-api` from a clean `runs/`: dependency-free startup, health wait, banner; stopped with Ctrl-C (Error 130 = SIGINT at the end of the recording is the operator stop). | | ||
| | `smoke-terminal.png` / `smoke.cast` | `make local-api-smoke`: the committed packet journey over real HTTP — `6 passed in 6.49s`. | | ||
| | `review-page.png` | Playwright screenshot of `GET /v1/captures/{id}/review` with the reviewer bearer from `session.json`, after the capture reached a terminal status. Renders the dev stub review page — locally the compose stage is a stand-in (`HSS_ORCH_STAGE_STUBS=1`), which is the documented dev configuration; the machine-readable outcome is the result envelope below. | | ||
| | `result-page.png` | Playwright screenshot of `GET /v1/captures/{id}/result` with the app bearer: the device-facing envelope — `status: manual_review`, `outcome.kind: manual_review`, "Review required before a placement is shown.", `reviewUrl`, `runId`. | | ||
| | `live-journey-receipt.json` | Sanitized receipt of the recorded live journey (create → signed uploads → commit → finalize 202 → inline orchestrator run → terminal `manual_review`). Tokens redacted; they are per-run local secrets. | | ||
|
|
||
| Reproduction: `make local-api` in one terminal, `make local-api-smoke` in | ||
| another (both from `backend/`); the driver for the screenshots reused the | ||
| committed smoke-test helpers against the live port. |
12 changes: 12 additions & 0 deletions
12
backend/docs/evidence/local-capture-api/live-journey-receipt.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "what": "live journey against make local-api (recorded to local-api.cast)", | ||
| "driver": "tests/test_local_capture_api.py helpers over real TCP via httpx; no TestClient", | ||
| "baseUrl": "http://127.0.0.1:8000/v1", | ||
| "captureId": "cap_01M51TS13336P96H5B6KFPH9WW", | ||
| "runId": "run_4c05e7d782dfc5f5", | ||
| "outcomeKind": "manual_review", | ||
| "outcomeMessage": "Review required before a placement is shown.", | ||
| "gitSha": "993c362751879bd03f3a8b90dd74c04b0836b78b", | ||
| "tokens": "redacted \u2014 per-run local secrets, never committed", | ||
| "recordedAt": "2026-10-16T07:49:27+00:00" | ||
| } |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Large diffs are not rendered by default.
Oops, something went wrong.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| {"version": 2, "width": 80, "height": 24, "timestamp": 1792136781, "env": {"SHELL": "/bin/bash", "TERM": "tmux-256color"}} | ||
| [0.005238, "o", "uv run --frozen --extra api pytest tests/test_local_capture_api.py -q\r\n"] | ||
| [6.779782, "o", "\u001b[32m.\u001b[0m"] | ||
| [6.802891, "o", "\u001b[32m.\u001b[0m"] | ||
| [6.818502, "o", "\u001b[32m.\u001b[0m"] | ||
| [6.827253, "o", "\u001b[32m.\u001b[0m"] | ||
| [6.828111, "o", "\u001b[32m.\u001b[0m"] | ||
| [6.893632, "o", "\u001b[32m.\u001b[0m"] | ||
| [7.058185, "o", "\u001b[32m [100%]\u001b[0m"] | ||
| [7.061215, "o", "\r\n"] | ||
| [7.061486, "o", "\u001b[32m\u001b[32m\u001b[1m6 passed\u001b[0m\u001b[32m in 6.49s\u001b[0m\u001b[0m\r\n"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| # The local capture API: one command, loopback only | ||
|
|
||
| `make local-api` gives a reviewer, the iOS simulator and `tools/hunter-local-ingest` a working | ||
| capture API on `127.0.0.1` from a fresh checkout. It replaces the hand-assembled uvicorn command in | ||
| `server/api/README.md`, which ran with `HSS_INGEST_HOOKS=null` — so finalize never reached the | ||
| orchestrator and no run ever started. | ||
|
|
||
| ## Fresh checkout | ||
|
|
||
| ```bash | ||
| git clone https://github.com/SamGu-NRX/BaseScanning.git && cd BaseScanning | ||
| git checkout alpha | ||
| cd backend | ||
| uv sync --frozen --extra api | ||
| make local-api # PORT=8000 default; override with PORT=8123 make local-api | ||
| ``` | ||
|
|
||
| Wait for the banner: | ||
|
|
||
| ``` | ||
| local capture API ready: http://127.0.0.1:8000 | ||
| session file: .../backend/runs/local-api/session.json | ||
| stand-in stages: validate, poses, objects, reads, scale, dense, coverage, scene, objects_3d, criteria, result | ||
| real stage: placement | ||
| ``` | ||
|
|
||
| Stop with Ctrl-C. Every run generates fresh tokens; nothing persists across restarts except the | ||
| uploaded bytes under `runs/local-api/volume/`. | ||
|
|
||
| `make local-api-smoke` proves the same launcher end to end: it starts the script as a subprocess on | ||
| an ephemeral port and drives the committed `data/fixtures/packets/mini-arkit` packet over real TCP | ||
| with httpx — create, register, PUT to the signed URLs, commit, finalize, long-poll events to | ||
| terminal `manual_review`, read the result and the reviewer page — plus the auth and error | ||
| boundaries. No TestClient, no `drain_finalize`: the orchestrator is driven inline by `start_run`. | ||
|
|
||
| ## Environment | ||
|
|
||
| | Variable | Value set by `make local-api` | Why | | ||
| |---|---|---| | ||
| | `SMP_ENVIRONMENT` | `dev` | dev posture; no cloud credentials needed | | ||
| | `HSS_AUTH_MODE` | `static` | bearer tokens from `CAPTURE_API_TOKENS` (explicit, because dev otherwise defaults to `none`) | | ||
| | `CAPTURE_API_TOKENS` | `device:<app-token>,reviewer:local-review:<reviewer-token>` | generated per run; tokens are ≥32 chars (`server/api/auth.py`) | | ||
| | `UPLOAD_URL_HMAC_SECRET` | fresh hex per run | signs the volume upload tokens | | ||
| | `STATE_BACKEND` | `memory` | no Modal, no database | | ||
| | `STORAGE_BACKEND` | `volume` | signed PUT URLs a client can actually fetch: `{CAPTURE_API_PUBLIC_URL}/v1/uploads/{token}` (`storage_volume.py`), unlike `storage_local.py`'s `file://` URLs | | ||
| | `VOLUME_ROOT` | `backend/runs/local-api/volume` | uploaded bytes live under gitignored `runs/` | | ||
| | `CAPTURE_API_PUBLIC_URL` | `http://127.0.0.1:<port>` | derived from the bind address, so upload URLs are reachable on this machine | | ||
| | `HSS_INGEST_HOOKS` | `auto` | finalize reaches the orchestrator (this is the point) | | ||
| | `HSS_ORCH_STAGE_STUBS` | `1` | committed stage stubs instead of GPU/vision work | | ||
| | `HSS_ORCH_PREWARM` | `0` | no prewarm work on capture create | | ||
| | `HSS_ORCH_MODAL_REMOTE` | `0` | a 'local' run never dispatches to deployed Modal functions — remote workers cannot see this process's memory state or volume, and remote-on also flips state to a Modal Dict and storage to GCS (`server/common/deploy_switch.py`) | | ||
| | `VISION_EXTERNAL_CALLS` | `off` | no networked model calls | | ||
| | `BASE_WEBHOOK_URL` / `WEBHOOK_HMAC_SECRET` | unset | webhooks off | | ||
|
|
||
| Pass through without interpretation: `HSS_LOCAL_OUTCOME` and `HSS_LOCAL_STAGE_DELAY_S`. The stage | ||
| stubs honor them when the burst11-bs-local-outcome-states packet lands; this launcher never reads | ||
| or changes them, so `HSS_LOCAL_OUTCOME=pass make local-api` just works. | ||
|
|
||
| ### What the launcher refuses from the shell | ||
|
|
||
| Everything above pins **where work runs and where bytes go**. The same audit applied to the rest of | ||
| the environment turned up eleven keys a developer's shell could export that would do exactly that, | ||
| so the launcher drops them before uvicorn imports and prints one line naming what it refused | ||
| (key names only, never values): | ||
|
|
||
| | Refused key | Why | | ||
| |---|---| | ||
| | `MODAL_TOKEN_ID` / `MODAL_TOKEN_SECRET` | Modal credentials any code path reaching for Modal would authenticate with | | ||
| | `MODAL_IDENTITY_TOKEN` | a Modal identity token that authenticates this process | | ||
| | `MODAL_APP_NAME` | names the deployed Modal app a Modal call would target | | ||
| | `MODAL_ENVIRONMENT` | selects which Modal environment the Modal client targets | | ||
| | `MODAL_TASK_ID` | marks the process as inside Modal and flips packet-cache behavior (`orchestrator/artifacts.py`) | | ||
| | `STATE_DICT_NAME` | names the shared Modal Dict state would land in | | ||
| | `HSS_PACKET_CACHE` | redirects packet-cache bytes away from the local volume | | ||
| | `HSS_ORCH_TEST_FAIL_STAGE` / `HSS_ORCH_TEST_HANG_STAGE` / `HSS_ORCH_TEST_KILL_COORDINATOR_AFTER` | fault-injection hooks a stray export would turn into a mysterious local failure or hang | | ||
|
|
||
| Reviewed and left alone, with the reasons: `HSS_LOCAL_OUTCOME` / `HSS_LOCAL_STAGE_DELAY_S` are the | ||
| deliberate harness pass-throughs above; `HSS_OBJECTS_CACHE_DIR`, `HSS_READS_CACHE_DIR` and | ||
| `HSS_SCALE_METER_MODELS_PATH` are local-disk read paths for stages that run as committed stubs | ||
| locally (only `placement` is real, and it reads none of them); `HF_HOME` / `HUGGINGFACE_HUB_CACHE` | ||
| are weight-cache locations no local stage downloads into; `CAPTURE_API_URL` / `CAPTURE_API_TOKEN` | ||
| are client-side harness variables the server never reads; and `HSS_MODAL_SCHEDULE` and the Modal | ||
| runtime keys are only read inside Modal images, which a loopback-only memory-state run never | ||
| reaches. `BASE_WEBHOOK_URL` / `WEBHOOK_HMAC_SECRET` stay unset (webhooks off), as before. | ||
|
|
||
| ## The session file | ||
|
|
||
| `backend/runs/local-api/session.json`, mode `0600` (the directory `runs/` is gitignored): | ||
|
|
||
| ```json | ||
| { | ||
| "baseUrl": "http://127.0.0.1:8000/v1", | ||
| "appToken": "<bearer for the app and harness>", | ||
| "reviewerToken": "<bearer for the review page>", | ||
| "pid": 12345, | ||
| "gitSha": "<git rev-parse HEAD at start>", | ||
| "stageStubs": true, | ||
| "startedAt": "<UTC ISO-8601>" | ||
| } | ||
| ``` | ||
|
|
||
| The shape is pinned verbatim by `tests/test_local_capture_api.py` because the iOS run and later | ||
| packets read it. Two choices to note: the tokens are written **inline** (`appToken`, not | ||
| `appTokenFile`) because a single self-contained file is one less path for every consumer to resolve | ||
| against an unknown working directory; and `gitSha` is the full head SHA so a captured session always | ||
| identifies the code that served it. Tokens appear nowhere else — not in stdout, not in the uvicorn | ||
| access log (which keeps upload URLs as `/v1/uploads/{token}` route shapes anyway). | ||
|
|
||
| ## What is a stand-in | ||
|
|
||
| `HSS_ORCH_STAGE_STUBS=1` runs every one of the twelve orchestrator stages as its committed stub | ||
| **except `placement`**, which is `stubs_exempt` (deterministic CPU work, no GPU/network/credentials) | ||
| and solves for real. A terminal `manual_review` verdict therefore proves the whole HTTP + finalize + | ||
| orchestrator path, not the quality of the vision stages. | ||
|
|
||
| ## The iOS simulator | ||
|
|
||
| The simulator shares the Mac's loopback, so it talks to `http://127.0.0.1:8000/v1` directly — set | ||
| the app's capture API base URL and paste the `appToken` from the session file. **A physical phone | ||
| does not share the Mac's loopback** and is out of scope: the server refuses any non-loopback bind, | ||
| by design. | ||
|
|
||
| ## tools/hunter-local-ingest | ||
|
|
||
| Point it at the session: read `backend/runs/local-api/session.json`, use `baseUrl` as the API base, | ||
| `appToken` as the bearer on every request, and register uploads from the packet directory exactly as | ||
| the smoke test does. The review page needs the `reviewerToken` bearer: | ||
| `GET {baseUrl}/captures/{captureId}/review` with `Authorization: Bearer <reviewerToken>` returns the | ||
| rendered HTML. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.