Skip to content

[obvious-fleet] make local-api serves the capture API on 127.0.0.1 and stays local - #281

Draft
obvious-autobuild[bot] wants to merge 8 commits into
alphafrom
obv/burst11-bs-local-api-serve
Draft

obvious-autobuild[bot] wants to merge 8 commits into
alphafrom
obv/burst11-bs-local-api-serve

Conversation

@obvious-autobuild

@obvious-autobuild obvious-autobuild Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

fleet-packet: burst14-bs-fix-local-api-serve-281

Human author: Sam Gu (sgu07966@gmail.com). Continues the earlier fleet round on this branch (the PORT-default fix, commits 330164c3 + 8c92df18): this round clears the one open Codex P2 review thread on #281, and the Greptile P1 raised on #293 against this PR's launcher, so the iOS Debug path (#286) and the held app-states run (#293) can rely on make local-api.

What changed and why

Two findings, two commits (0bdee232 tests failing-first, dc7cdee1 fix + doc), after merging origin/alpha in (branch was 53 behind; merge commit 2a39e938, no conflicts, no rebase):

  1. Codex P2 on [obvious-fleet] make local-api serves the capture API on 127.0.0.1 and stays local #281 (backend/Makefile:100) — local-api and local-api-smoke were missing from .PHONY, so a stray file named local-api made make local-api report "up to date" and silently never start the server. Both targets joined the existing .PHONY line.

  2. Greptile P1 on Backend user run: drive the local API through every app state over a real socket #293 (backend/scripts/local_capture_api.py:197) — the launcher inherited HSS_ORCH_MODAL_REMOTE from the developer's shell; with =1 a "local" run dispatched to deployed Modal functions that cannot see its memory state or volume (and shared_remote_backends flips state to Modal Dict and storage to GCS). The launcher now forces HSS_ORCH_MODAL_REMOTE=0 alongside the other forced local settings.

    The same audit applied to the rest of the environment — every HSS_*/STATE_*/STORAGE_*/MODAL_* key the code or the Modal client reads that could change where work runs or where bytes go — found eleven more keys, now refused (dropped from the child env, one stderr line naming the keys, never values): MODAL_TOKEN_ID, MODAL_TOKEN_SECRET, MODAL_IDENTITY_TOKEN, MODAL_APP_NAME, MODAL_ENVIRONMENT, MODAL_TASK_ID, STATE_DICT_NAME, HSS_PACKET_CACHE, and the HSS_ORCH_TEST_FAIL_STAGE/HSS_ORCH_TEST_HANG_STAGE/HSS_ORCH_TEST_KILL_COORDINATOR_AFTER fault-injection hooks. The doc's new "What the launcher refuses from the shell" section lists each with its reason, plus what was reviewed and left alone (HSS_LOCAL_OUTCOME/HSS_LOCAL_STAGE_DELAY_S stay deliberate pass-throughs; cache/model-path keys are inert because every stage that reads them runs as a committed stub locally; CAPTURE_API_URL/CAPTURE_API_TOKEN are client-side; HSS_MODAL_SCHEDULE and Modal runtime keys are only read inside Modal images).

Target name, CLI flags and the session.json shape unchanged (pinned verbatim by the existing test). Files touched: backend/Makefile, backend/scripts/local_capture_api.py, backend/tests/test_local_capture_api.py, backend/docs/local-capture-api.md. Nothing under ios/, .github/, backend/infra/, no lockfiles.

How I verified it (commands and results)

Failing-first — the three new regressions ran at parent head 2a39e938 before the fix:

pytest tests/test_local_capture_api.py -k "stray_file or poisoned_shell" — pre-fix
FAILED tests/test_local_capture_api.py::test_make_local_api_targets_survive_a_stray_file
FAILED tests/test_local_capture_api.py::test_child_env_forces_local_dispatch_against_a_poisoned_shell
FAILED tests/test_local_capture_api.py::test_launcher_overrides_a_poisoned_shell_and_stays_healthy
  (the poisoned-shell log shows the pre-fix launcher silently accepting an inherited
   HSS_ORCH_MODAL_REMOTE=1 — healthz answered, memory/volume already forced, dispatch flag not)
3 failed, 10 deselected in 2.13s

After the fix:

Focused module, lint, and the full suite
$ uv run --frozen --extra api pytest tests/test_local_capture_api.py -q
13 passed in 7.6s          (10 existing + 3 new)

$ uv run --frozen ruff check scripts/local_capture_api.py tests/test_local_capture_api.py
All checks passed!
$ uv run --frozen ruff format --check scripts/local_capture_api.py tests/test_local_capture_api.py
2 files already formatted

$ make test
4 failed, 1519 passed, 19 skipped, 8 warnings, 14 errors in 649.40s (0:10:49)

The 4 failures are the known expired SD-96 gate window in server/eval/tests/test_gates.py
(Sam decides that window; untouched by this PR):
  test_committed_fixtures_are_scored_against_their_labels
  test_offline_table_skips_the_engine_and_passes_the_unpatched_reference
  test_route_over_the_bar_is_known_until_expiry
  test_refresh_rewrites_pins_after_a_committed_edit
The 14 errors are the documented pre-existing collection errors in
tests/test_capture_cold_restart.py on the alpha baseline. No failure or error
in any file this PR touches; the 3 new tests pass.

The poisoned-shell regression starts the launcher with HSS_ORCH_MODAL_REMOTE=1, STATE_BACKEND=modal_dict, STORAGE_BACKEND=gcs, MODAL_TOKEN_ID, and HSS_ORCH_TEST_FAIL_STAGE in the parent env, then asserts /v1/healthz answers {"status":"ok","state":"memory","storage":"volume"} and the log names the refused keys — the exact failure the P1 describes, now pinned.

What is not verified

  • iOS build/simulator behavior — no Xcode in this sandbox; the change touches no iOS code, and target name, flags and session shape are unchanged.
  • make -n behavior verified with GNU Make on Debian only.
  • No Modal, GCS, GPU, paid, or networked calls were exercised — stubs on, VISION_EXTERNAL_CALLS=off, loopback only.

Assumptions


🔗 Obvious Project · 🧵 Obvious Thread · 🔀 Obvious PR Artifact

SamGu-NRX and others added 3 commits October 16, 2026 07:35
…api)

scripts/local_capture_api.py starts the real app under uvicorn, loopback
only, fresh tokens + HMAC secret per run, memory state and volume storage,
ingest hooks auto so finalize drives the orchestrator inline; it writes
runs/local-api/session.json (0600) with the base URL, tokens, pid, sha.
make local-api-smoke drives the mini-arkit packet over real TCP via httpx
against the script as a subprocess — no TestClient, no drain_finalize.

Co-authored-by: Obvious <agents@obvious.ai>
Fixes from the first runs: create pins Idempotency-Key to the packet id so
each journey retags its packet copy; /review gates the reviewer before the
not-ready check, so 404 result_not_ready needs the reviewer bearer while an
app bearer gets 403; the event log carries stage events for the run, and a
wrong bearer is unauthorized. Smoke suite passes twice in a row; ruff clean.

Co-authored-by: Obvious <agents@obvious.ai>
…pages

Captured on a fresh alpha clone: make local-api recording (banner, clean
Ctrl-C), make local-api-smoke recording (6 passed over real TCP), Playwright
review/result pages, sanitized journey receipt reaching terminal manual_review
with the placement stage real and compose stubbed. Tokens redacted (scan-verified).

Co-authored-by: Obvious <agents@obvious.ai>
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9876e75c-5ce9-404d-a1ae-d32a30f2e5d7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the size:L 100-499 effective changed lines. label Oct 16, 2026
@SamGu-NRX

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T18:23:41.672293Z dc7cdee Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d0774fdbc1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/Makefile
…usal

Codex P1 on #281: bare `make local-api` hands the launcher a bare --port.
Pin the documented default (PORT=8000), the PORT=8123 override, the
exported-but-empty PORT guard, and an actionable launcher refusal for
`--port=`. Fails at the parent head by design.
…mpty --port loudly

Codex P1 on #281: bare `make local-api` expanded $(PORT) to nothing, so
the launcher got a bare --port and argparse exited instead of serving.

- backend/Makefile: PORT ?= 8000 with an empty-value guard, so unset and
  exported-but-empty PORT both serve the documented default; PORT=8123
  make local-api still overrides.
- scripts/local_capture_api.py: --port parses through _port(), which
  refuses an empty value with an actionable message (exit 2) instead of
  argparse's bare "invalid int value: ''".

Target name and CLI flags unchanged. Tests in the parent commit fail at
that head and pass here; smoke module 10 passed; make lint clean.
@obvious-autobuild obvious-autobuild Bot changed the title Backend: one command serves the capture API locally [obvious-fleet] Backend: one command serves the capture API locally Oct 16, 2026
@obvious-autobuild

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8c92df18d0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread backend/Makefile
ObviousApp and others added 3 commits October 16, 2026 17:50
Three regressions for the open review findings on this PR and #293:
a stray file named local-api must not make the target 'up to date',
an inherited HSS_ORCH_MODAL_REMOTE=1 must not reach the child env, and
a launcher started from a poisoned shell must still answer healthz on
memory state and volume storage.

Co-authored-by: obvious-autobuild[bot] <obvious-autobuild[bot]@users.noreply.github.com>
… targets

- backend/Makefile: local-api and local-api-smoke join .PHONY, so a stray
  file named local-api can no longer make the target report 'up to date'
  and silently never start the server (open Codex P2 on #281).
- scripts/local_capture_api.py: force HSS_ORCH_MODAL_REMOTE=0 (Greptile P1
  raised on #293 — an inherited =1 dispatched a 'local' run to deployed
  Modal functions that cannot see its memory state or volume, and flips
  state to Modal Dict and storage to GCS via shared_remote_backends), and
  refuse eleven further parent-shell keys that would move work or bytes
  off this machine: Modal credentials and app/environment targeting,
  MODAL_TASK_ID, STATE_DICT_NAME, HSS_PACKET_CACHE, and the
  HSS_ORCH_TEST_* fault-injection hooks. Refusals print one stderr line
  naming the keys, never their values.
- docs/local-capture-api.md: the forced-env table gains the
  HSS_ORCH_MODAL_REMOTE row and a refusal table with per-key reasons,
  plus what was reviewed and left alone.

Target name, CLI flags and the session.json shape are unchanged.

Co-authored-by: obvious-autobuild[bot] <obvious-autobuild[bot]@users.noreply.github.com>
@github-actions github-actions Bot added size:XL 500-999 effective changed lines. and removed size:L 100-499 effective changed lines. labels Oct 16, 2026
@obvious-autobuild obvious-autobuild Bot changed the title [obvious-fleet] Backend: one command serves the capture API locally [obvious-fleet] make local-api serves the capture API on 127.0.0.1 and stays local Oct 16, 2026
@obvious-autobuild

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: dc7cdee111

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@SamGu-NRX

Copy link
Copy Markdown
Owner

@greptileai review this at plus

@greptile-apps

greptile-apps Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 2/5 Tier: plus

[Medium impact] Adds a local development server for the capture API.

Fix the session overwrite, proxy-sensitive readiness check, and destructive smoke test before merging.

Fix All in Claude CodeFindings

  1. P1 Second launch loses working credentials ▶
  2. P1 Proxy settings stop local startup ▶
  3. P1 Smoke test erases local files ▶
  4. P2 Port override fails smoke tests ▶
Fix with agent prompt
### Issue 1
backend/scripts/local_capture_api.py:265
If `make local-api` is already running, a second launch replaces `session.json` before trying to bind the same port. The second process then fails, but the saved tokens now belong to it rather than the running server. Clients reading the file get 401 responses, and the working credentials are lost.

Preserve any live session and publish new credentials only after the new server owns its listener.

### Issue 2
backend/scripts/local_capture_api.py:130
If the shell sets `http_proxy` without a loopback exception in `no_proxy`, the default `urlopen` sends this readiness request through the proxy instead of directly to the local API. If the proxy cannot reach that address, the launcher kills an otherwise healthy server after 60 seconds.

Use a proxy-free opener for this loopback-only check.

```suggestion
            with urllib.request.build_opener(urllib.request.ProxyHandler({})).open(
                f"{base}/v1/healthz", timeout=2
            ) as response:
```

### Issue 3
backend/tests/test_local_capture_api.py:439-442
If `backend/local-api` or `backend/local-api-smoke` already exists, this test truncates it and then unconditionally deletes it in the `finally` block. Running the smoke tests can therefore destroy a developer’s existing files.

Copy the Makefile into `tmp_path`, run Make there, and create the stray files only in that temporary directory.

### Issue 4
backend/tests/test_local_capture_api.py:400-403
If the shell exports `PORT=8123`, `_make_n_local_api()` inherits it and Make correctly emits `--port 8123`. This test still requires `--port 8000`, so `make local-api-smoke` reports a false failure for a supported setting.

Remove `PORT` from the test environment before checking the unset default.

```suggestion
def test_make_local_api_without_port_passes_the_documented_default(monkeypatch) -> None:
    # The documented flow (docs/local-capture-api.md) is bare `make local-api`
    # on port 8000; the recipe must not hand the launcher a bare --port.
    monkeypatch.delenv("PORT", raising=False)
    assert "--port 8000" in _make_n_local_api()
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds make local-api with loopback-only serving, fresh credentials, local storage, and real HTTP smoke tests.

  • A local capture API starts on loopback and writes a ready-to-use session file.
  • Local API runs stay on local state and ignore shell overrides.

Diagram

sequenceDiagram
  participant C as Local client
  participant L as Launcher
  participant A as Capture API
  participant O as Inline orchestrator
  participant V as Local volume
  L->>L: Force local settings and generate tokens
  L->>L: Write session.json
  L->>A: Start Uvicorn on loopback
  L->>A: Check health
  C->>A: Create capture and register files
  C->>A: Upload bytes and commit
  A->>V: Save uploaded bytes
  C->>A: Finalize packet
  A->>O: Start local run with shared state and storage
  O->>V: Save stage results
  C->>A: Read events and result
Loading

Reviews (1) · Last reviewed commit: "fix(backend): force local dispatch, refu..." · Reviewed by Greptile

"stageStubs": True,
"startedAt": datetime.now(UTC).isoformat(),
}
_write_session(session_path, session)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Second launch loses working credentials

If make local-api is already running, a second launch replaces session.json before trying to bind the same port. The second process then fails, but the saved tokens now belong to it rather than the running server. Clients reading the file get 401 responses, and the working credentials are lost.

Preserve any live session and publish new credentials only after the new server owns its listener.

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/scripts/local_capture_api.py
Line: 265

Comment:
**Second launch loses working credentials**

If `make local-api` is already running, a second launch replaces `session.json` before trying to bind the same port. The second process then fails, but the saved tokens now belong to it rather than the running server. Clients reading the file get 401 responses, and the working credentials are lost.

Preserve any live session and publish new credentials only after the new server owns its listener.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

deadline = time.monotonic() + timeout_s
while time.monotonic() < deadline:
try:
with urllib.request.urlopen(f"{base}/v1/healthz", timeout=2) as response:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Proxy settings stop local startup

If the shell sets http_proxy without a loopback exception in no_proxy, the default urlopen sends this readiness request through the proxy instead of directly to the local API. If the proxy cannot reach that address, the launcher kills an otherwise healthy server after 60 seconds.

Use a proxy-free opener for this loopback-only check.

Suggested change
with urllib.request.urlopen(f"{base}/v1/healthz", timeout=2) as response:
with urllib.request.build_opener(urllib.request.ProxyHandler({})).open(
f"{base}/v1/healthz", timeout=2
) as response:
Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/scripts/local_capture_api.py
Line: 130

Comment:
**Proxy settings stop local startup**

If the shell sets `http_proxy` without a loopback exception in `no_proxy`, the default `urlopen` sends this readiness request through the proxy instead of directly to the local API. If the proxy cannot reach that address, the launcher kills an otherwise healthy server after 60 seconds.

Use a proxy-free opener for this loopback-only check.

```suggestion
            with urllib.request.build_opener(urllib.request.ProxyHandler({})).open(
                f"{base}/v1/healthz", timeout=2
            ) as response:
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

Comment on lines +439 to +442
strays = [ROOT / "local-api", ROOT / "local-api-smoke"]
try:
for stray in strays:
stray.write_text("")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Smoke test erases local files

If backend/local-api or backend/local-api-smoke already exists, this test truncates it and then unconditionally deletes it in the finally block. Running the smoke tests can therefore destroy a developer’s existing files.

Copy the Makefile into tmp_path, run Make there, and create the stray files only in that temporary directory.

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/tests/test_local_capture_api.py
Line: 439-442

Comment:
**Smoke test erases local files**

If `backend/local-api` or `backend/local-api-smoke` already exists, this test truncates it and then unconditionally deletes it in the `finally` block. Running the smoke tests can therefore destroy a developer’s existing files.

Copy the Makefile into `tmp_path`, run Make there, and create the stray files only in that temporary directory.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

Comment on lines +400 to +403
def test_make_local_api_without_port_passes_the_documented_default() -> None:
# The documented flow (docs/local-capture-api.md) is bare `make local-api`
# on port 8000; the recipe must not hand the launcher a bare --port.
assert "--port 8000" in _make_n_local_api()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Port override fails smoke tests

If the shell exports PORT=8123, _make_n_local_api() inherits it and Make correctly emits --port 8123. This test still requires --port 8000, so make local-api-smoke reports a false failure for a supported setting.

Remove PORT from the test environment before checking the unset default.

Suggested change
def test_make_local_api_without_port_passes_the_documented_default() -> None:
# The documented flow (docs/local-capture-api.md) is bare `make local-api`
# on port 8000; the recipe must not hand the launcher a bare --port.
assert "--port 8000" in _make_n_local_api()
def test_make_local_api_without_port_passes_the_documented_default(monkeypatch) -> None:
# The documented flow (docs/local-capture-api.md) is bare `make local-api`
# on port 8000; the recipe must not hand the launcher a bare --port.
monkeypatch.delenv("PORT", raising=False)
assert "--port 8000" in _make_n_local_api()
Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/tests/test_local_capture_api.py
Line: 400-403

Comment:
**Port override fails smoke tests**

If the shell exports `PORT=8123`, `_make_n_local_api()` inherits it and Make correctly emits `--port 8123`. This test still requires `--port 8000`, so `make local-api-smoke` reports a false failure for a supported setting.

Remove `PORT` from the test environment before checking the unset default.

```suggestion
def test_make_local_api_without_port_passes_the_documented_default(monkeypatch) -> None:
    # The documented flow (docs/local-capture-api.md) is bare `make local-api`
    # on port 8000; the recipe must not hand the launcher a bare --port.
    monkeypatch.delenv("PORT", raising=False)
    assert "--port 8000" in _make_n_local_api()
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 effective changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants