Repository navigation
Conversation
…iner's packet Two Z10 audit findings, as tests that fail on alpha (5 failed, 1 passed): - A packet with NaN, -Infinity or 1e999 in a still's gravity, or NaN in session.startedAtUptime, is accepted with 202 and reaches processing. - Two API containers that both load a capture before either accepts can both accept different packets; the first client is told about a run that never starts. The identical-packet replay test passes before and after; it guards the fix. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tance Non-finite numbers: json.loads reads NaN, Infinity, -Infinity and turns 1e999 into inf, and the packet models take any float. A still's gravity or session.startedAtUptime carrying one was accepted and reached processing; poses and intrinsics were caught only by the background manifest check, after the 202 and the full upload. accept_packet now answers 422 schema with a pointer to the number before anything is stored. Acceptance claim: the capture lock is per process, but the API runs on up to four containers. Two containers that loaded the capture before either accepted could both accept different packets; the first client was told about a run that never started and its packet.json was overwritten. accept_packet now claims accepted_packet:<capture>:<amendment> with put_if_absent before writing; a different digest gets the usual 409 packet_sha_conflict, the same digest proceeds as before. A storage write failure releases the claim so a corrected packet can still finalize. server/api/tests/test_finalize_hostile.py: 5 failed, 1 passed on alpha; 6 passed here. server/api/tests: 217 passed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Guard for the claim added in the previous commit, not a failing-first test: alpha has no claim to release. With delete_kv made a no-op the test fails with 409 packet_sha_conflict; with the fix it passes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
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. Comment |
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. Nice work! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Review of #327 at 3c24e69: an identical finalize on a second container passed the claim check and wrote packet.json again while the first finalize worker read it. The volume backend's write truncates before it writes, so the worker could read an empty file and fail with JSONDecodeError; test_a_second_container_with_the_same_packet_still_replays failed about one run in three. Only the claimant now writes packet.json and schedules the finish. A retry that finds its own digest already claimed replays the claimant's 202 (processing, or awaiting_files with the same missing list); the run id is derived from the bytes, so it matches. test_a_same_packet_retry_on_another_container_writes_nothing counts packet writes and finish jobs. At 3c24e69 it fails (2 writes); here it passes. The hostile file passed 20 of 20 runs; server/api/tests: 220 passed (OMP_NUM_THREADS=1). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…claim Astra review of #327 at 3864c43: the claim became durable before packet.json was written or work was scheduled. A claimant that died in between left every same-digest retry answering processing with nothing scheduled, and the capture stayed uploading. The claim now records claimed_at and a token. A same-digest retry still replays without writing when the stored packet matches the digest, or when the claim is younger than web_function_timeout_s(), the timeout modal_entry.py gives the API function, so the claimant may still be running. Otherwise the claim is abandoned and the retry takes it over. The store has no compare-and-swap, so the takeover inserts <claim>:takeover:<old token> with put_if_absent: of all retries that read the same abandoned claim, exactly one inserts it, writes the packet and schedules the finish. Tests (server/api/tests/test_finalize_hostile.py): - test_a_retry_takes_over_an_abandoned_claim and test_concurrent_takeovers_schedule_once fail on 3864c43 with (0, 0) == (1, 1) packet writes and finish jobs; both pass here. The concurrent test holds both retries at the takeover until both have read the abandoned claim. - test_a_retry_leaves_a_fresh_claim_alone passes on 3864c43 as well, because that commit never schedules on a retry. With the bound replaced by -1 it fails with (1, 1) == (0, 0). The file passed 20 of 20 runs (11 passed each); server/api/tests: 223 passed with OMP_NUM_THREADS=1. ruff check and format clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Zephyrus task Z10: audit of the server pipeline on
alphaat7e6b2ef3. Step 1 ran thebackend,verificationandreconsuites. Step 2 sent hostile input through the real FastAPI apps and the placement solver. This branch fixes the two most severe new problems; the rest are in the table below. There is no parent PR, so there were no review threads to read.What changed
Finalize refuses NaN and infinite numbers before accepting a packet
Python's
json.loadsreadsNaN,Infinityand-Infinity, and turns an overflowing literal such as1e999intoinf. None of these is JSON, and the packet models take any float (backend/server/contracts/_base.py:24). On alpha,accept_packet(backend/server/api/finalize.py:210) stored such a packet and answered 202. Two outcomes followed, both shown by the probes:NaNor1e999instills[].gravity,stills[].timestamporsession.startedAtUptimewent on toprocessingand reached the pipeline.NaNorinfin a keyframe pose, intrinsics, timestamp or tap ray was caught by the background manifest check, but only after the 202 and the full upload. The capture then endedfailedwithnext: none.accept_packetnow walks the parsed packet and answers422 schemawith a JSON pointer to the first non-finite number (for example/stills/0/gravity/1), before it stores anything. The capture stays inuploading, so a corrected packet still finalizes. The check sits at the API boundary rather than in_base.py, because the shared contract types are also used for output that this audit did not trace.A second API container can no longer accept a different packet
The per-capture finalize lock (
backend/server/api/routes/finalize.py:36,backend/server/api/locks.py) works within one process.modal_entry.py:35allows up to four API containers.accept_packetdecides whether a packet was already accepted from arecordit read before the request body (finalize.py:192). So two containers that both load the capture before either writes can both accept.I reproduced this with two app instances that share one state store and one volume, two threads, and a 300 ms delay on the packet write to stand in for a GCS PUT. Container A answered 202 with
run_2c51…. Container B answered 202 with a different packet andrun_337e…, and B's bytes replacedpacket.json. Only B's run started, so A's client was polling for a run that never existed. Identical packets were already deduplicated downstream by the run claim inorchestrator/api.py:55.Before writing the packet,
accept_packetnow claimsaccepted_packet:<capture>:<amendment or ->withstate.put_if_absent, which is atomic on both state backends (Modal Dict usesskip_if_exists). A different digest gets the 409packet_sha_conflictthat a single container already returns. The same digest, already claimed, writes nothing and schedules nothing: only the claimant writespacket.jsonand starts the finish, and the retry replays the claimant's 202 (processing, orawaiting_fileswith the same missing list; the run id comes from the bytes, so it matches). An earlier version let the retry writepacket.jsonagain. The volume backend's write truncates before it writes (storage_local.py:32), so the first worker could read an empty file and fail withJSONDecodeError. Review of3c24e69acaught it, and3864c436fixes it. If the packet write fails, the claim is released, so a corrected packet can still finalize. With the fix, the same two-thread probe returns 202 and 409, and the stored packet is A's.Step 1: suites on alpha
verificationuvx --from "uv>=0.11.15,<0.12" uv sync --locked && … uv run pytest -qreconbackend(as documented)uv sync --locked && OMP_NUM_THREADS=1 uv run pytest -qserver/api/tests/test_events_orchestrator_wake.py:18importsuvicorn, which is only in theapiandios-kitextras, not thedevgroup. No CI workflow runsbackend/, so nothing caught this.backendwith--extra apiuv sync --locked --extra api && OMP_NUM_THREADS=1 uv run --extra api pytest -qtests/test_capture_cold_restart.py, finding 1. 4 failures inserver/eval/tests/test_gates.py, finding 3. 1 failure inserver/packet/tests/test_round2.py::test_many_missing_files_stay_linear, finding 6.backendruffuv run ruff check . && uv run ruff format --check .server/orchestrator/tests/test_greptile_followup.py:163is not formatted.verification/pyproject.toml:21andrecon/pyproject.toml:17pinrequired-version = ">=0.11.15,<0.12". The WSL machine has uv 0.13.5, so plainuv sync --lockedrefuses to run there. I ran both throughuvxwith a uv inside the pin.pytest-xdist is not installed in any of the three environments, so every run was single-process.
Step 2: other findings
backend/scripts/capture_cold_restart.py:82-88tests/test_capture_cold_restart.py. All 14 tests error withImportError: cannot import name load_receipt.596ed9a9(Oct 16) replacedload_receiptwithreceipt_probleminscripts/replay_capture.py.3c24e69a: restored theload_receiptJSON wrapper inreplay_capture.py; collection now succeeds and the touched suite passes 30 tests.backend/pyproject.tomldev group;test_events_orchestrator_wake.py:18uv sync --locked && uv run pytest.uvicornto thedevgroup and relock. I left this alone because lockfiles are off limits for this task. A backend CI job would also need.github/, which is off limits too.backend/server/eval/gates.yaml:42known_failure.until: "2026-10-04"on the gt-laser-gravel footprint3in row has expired, so the row reportsFAILrather thanKNOWN_FAIL. Four tests read that row: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_expiryandtest_refresh_rewrites_pins_after_a_committed_edit.untilor fix the SD-96 miss. Eval thresholds are off limits for this task. The tests could also pin today's date so that expiry is tested on purpose.backend/server/api/routes/files.py:231-241metawhoseposeholdsNaN, sent at file registration. It answers 200 and the record keepsnan.c7094b1c: registration rejects non-finite metadata with a schema pointer before persistence. New test failed on the test-only commit7007328cand passes after the fix.backend/server/placement/scene_bridge.py:328baseline_mrepeats its first point./walls/0/baseline, but the field isbaseline_m.c7094b1c: pointer now names/walls/0/baseline_m. New test failed on the test-only commit7007328cand passes after the fix.backend/server/packet/tests/test_round2.py:302time.process_time) instead of wall time, or compare against a smaller run so the check tests growth rather than speed.backend/server/orchestrator/tests/test_greptile_followup.py:163ruff format --check .ruff formaton the file.Inputs that the code already handles correctly, each checked through the real app:
server/api.py), zip bundles: zip64 entries,.., backslash, absolute and drive-letter entry names, a declared size of 4 GiB, ascene.jsonwhose declared size is smaller than its real size, an 11 MiBscene.json, LZMA and stored compression, and a truncated zip. Each one gets the documented 4xx. The bound on the central directory's entry count arrives with Server hardening: camera-evidence coverage, solver budgets, bounded bundles, provenance #221, which Z8 is porting.NaN,Infinity,1e999,-1e999, 100,000-deep nesting and a 5,000-digit integer. All get a 422.pass,rejectormanual_review. None returned a 500. Through the backend bridge (scene_bridge.py), the same walls plusNaNandinfare refused withplacement_scene_invalid.backend/server/api):formatVersionvalues0.5,0.4.1,1.0," 0.4",0.40, empty, null and the number0.4, both at create and at finalize. File paths with..,./,//, a leading/,%2e%2e, a backslash, a dotlessı, a NUL byte, a leading dot andpacket.json. Packetimgandidvalues with... A gzip bomb (49 KB inflating to 49 MiB), truncated gzip,deflate,GZIPandgzip, identity. Bodies over 1 MiB and over 48 MiB, including chunked bodies with no length.Verification
Failing first. The tests are in
backend/server/api/tests/test_finalize_hostile.py, and the test-only commitc6586261sits on top of alpha.The four non-finite cases fail on alpha with
202 == 422. The two-container case fails withDID NOT RAISE ApiFailure. The identical-packet replay test guards against the claim turning a retry into a conflict. At3c24e69ait failed about one run in three, because the retry rewrotepacket.jsonwhile the worker read it.test_a_same_packet_retry_on_another_container_writes_nothingmakes that race deterministic by counting packet writes and finish jobs.test_a_failed_packet_write_releases_the_claimcannot fail on alpha, because alpha has no claim to release. To check that the test works, I madedelete_kva no-op, and it then fails with 409.OMP_NUM_THREADS=1 uv run --extra api pytest -q server/api/tests: 223 passed at49904399.test_a_retry_leaves_a_fresh_claim_alonecannot fail on3864c436, because that commit never schedules on a retry. With the abandonment bound replaced by -1, it fails with(1, 1) == (0, 0).uv run --extra api pytest -q, single process), run at7d3a996eand not rerun since. On the branch, 1523 passed, 4 failed, 14 errors and 16 skipped. On alpha, 1515 passed, 5 failed, 14 errors and 16 skipped. The difference is the 7 new tests, plustest_many_missing_files_stay_linear, which passed this time under lighter load. The 4 failures and 14 errors left are findings 3 and 1, and they are the same on both.uv run ruff check .passes.uv run ruff format --check .reports only the alpha file in finding 7.The probes are throwaway scripts that drive
create_app()throughTestClient,httpx.ASGITransportand two threads, against the volume storage and memory state backends. Nothing ran against Modal, GCS or a real Modal Dict. The cross-container race was reproduced with threads and a delayed write, not with real containers.Risk
The claim adds one
put_if_absent, plus oneget_kvwhen another request already holds it. A capture accepted before this deploy already haspacket_sha256set, so it never reaches the claim. Only a retry holding a stale record reaches it, so later retries skip the extra packet read.A claimant can die after it claims and before it writes
packet.json. A same-packet retry then takes the claim over once the claim is older thanweb_function_timeout_s()(common/deploy_switch.py:28), which is 600 s, or 1560 s with real runs. That is the timeoutmodal_entry.py:121gives the API function, so no live request can be older. No new constant was added. Until the bound passes, retries answerprocessingand schedule nothing. The store has no compare-and-swap. The takeover inserts<claim>:takeover:<old token>withput_if_absentinstead, so exactly one of the retries that read the same abandoned claim wins. The tests cover this on the memory backend only. The Modal Dict path relies onskip_if_existsbeing atomic, as the first claim already does.One gap is left. If the claimant writes a matching
packet.jsonand dies before it saves the acceptance and schedules the finish, the packet check sees a valid packet. Retries then keep answeringprocessingwithout scheduling, until the capture's finalize window expires. Closing it would mean checking for the published acceptance as well as the packet. I didn't do that here.Claim and takeover keys are not deleted with the capture. That matches the other per-capture keys, and a deleted capture answers 410 anyway.
🤖 Generated with Claude Code