Skip to content

feat(result): serve a reviewer page that stays judgeable past render time - #284

Open
obvious-autobuild[bot] wants to merge 10 commits into
alphafrom
obv/burst11-bs-review-evidence-view
Open

obvious-autobuild[bot] wants to merge 10 commits into
alphafrom
obv/burst11-bs-review-evidence-view

Conversation

@obvious-autobuild

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

Copy link
Copy Markdown

Summary

The reviewer page is composed once, at result time, and the artifact links baked into it are signed upload URLs with a TTL. After the TTL lapses — typically long before a human opens the page — every link dies and the page stops being judgeable. This PR makes GET /v1/captures/{capture_id}/review serve a page that stays judgeable indefinitely, without weakening the signed-URL model that protects uploads.

What changed

  • Serve-time rewrite (backend/server/api/routes/result.py) — when a reviewer fetches the page, the route rewrites every baked /v1/uploads/… signed URL (photos, raw artifacts) into reviewer-gated /review/files/{rel} links, so nothing on the page depends on a token that may have expired. Link targets are validated against the run's recorded artifact list; unknown rels 404, and path traversal (including URL-encoded segments) is rejected.
  • What ran is on the page — the served page gains a run-status section: per-stage status with error codes, degraded stage names, run id, and the serving git SHA. A reviewer can see what ran, what was a stand-in and why, without opening a second tool.
  • three.js without network access — the page's 3D viewer imports three.js from a CDN. The route now rewrites that import to a same-origin vendored asset (backend/server/api/assets/three.module.js, three.js 0.160.0, MIT license header retained), so the preview loads in an offline/restricted environment.
  • Access unchanged — /review, /review/files/*, and /review-static/* are all reviewer-token gated, same as before; device tokens do not gain access.
  • Docs (backend/docs/INTEGRATION.md) — documents the rewrite behavior and the new routes.

Verification

  • Unit tests (backend/server/api/tests/test_review_view.py, new) — 12 tests: reviewer-only access on all three routes, 404 result_not_ready, expired-signed-link handling, photo + raw-file link rewriting, local three.js serving, injected stage/provenance fields, traversal rejection. The full API suite passes 4 consecutive runs (184 tests).
  • Import contracts — lint-imports: 5 kept, 0 broken. Ruff check + format clean.
  • make result-golden — synthetic, photo, and reject fixtures render and validate.
  • Real-socket before/after evidence (~/work/bs-evidence, alpha 4bcbedba vs branch head):
    • Both trees run the dev journey over real sockets (11/11 packet files committed).
    • Before: the stub result stage serves a 37-byte placeholder page.
    • After: the journey publishes its page, the real composer is run over the journey's artifacts exactly as the real result stage would (17 KB page), and the reviewer fetch returns it with zero stale /v1/uploads/ references — every baked link rewritten to a gated /review/files/ link — plus a same-origin three.js import and the injected run-status table.
    • Desktop + mobile screenshots taken well past the 2-second signed-URL TTL: after pages titled Review cap_…, zero broken images. (Honest scope note: the dev journey's scene is a stub, so the page shows no photos and reports the point-cloud preview as unavailable — the link rewriting for photos is proven by unit tests with real stills; the demo page proves the raw-file rewrite, the rewrite counts, and the run-status surface.)
    • A 56-second asciinema walkthrough records the whole before/after run (in the evidence bundle below).
  • Full make test — 4 failed, 1,433 passed, 19 skipped, 14 errors in 8:27. The 4 failures are server/eval/tests/test_gates.py (the expired SD-96 eval gates, documented pre-existing on alpha) and the 14 errors are the pre-existing test_capture_cold_restart.py collection errors, both identical on the alpha base 4bcbedba. No failure or error originates from this PR.
  • Evidence bundle — driver scripts, both journeys' committed packets, served pages and headers, screenshots, the asciinema cast, server logs, and the full-suite log — uploaded as burst11-review-evidence.zip (Obvious file fl_kwyoz9EH, https://api.app.obvious.ai/prepare/files/link/fl_kwyoz9EH/burst11-review-evidence.zip). Mobile captures (390×844) are included in the bundle but excluded from the QA evidence upload, which enforces a 720px shorter-edge minimum.

Not in scope (deliberate)

  • The dev journey keeps its stub result contract (device-facing needs_more_photos path, internal record shape, unconditional preview.ply). A first cut briefly routed stub journeys through the real composer; it changed the device-facing outcome and six orchestrator tests caught it, so that wiring was reverted (b3045efb) and the evidence instead drives the real composer over the journey's artifacts, as the real result stage does.
  • Token-expiry behavior of the upload route itself is unchanged; the rewrite is additive.

Review-thread fixes, round two (ebed0c8, cadd812)

Both unresolved P1 review threads are fixed on this branch; replies are posted inline and each finding names its line.

  • Review links ignored their run claim — get_review_file and get_preview resolved the capture's latest run instead of the run named in the signed token, so after an amendment started a second run, an earlier page's link read the later run's bytes, and a file that run 1 never produced was served from run 2. The artifact now resolves from the token's signed run; a file that run lacks is refused with 404 result_not_ready instead of cross-run content. A reviewer's own bearer fetch still follows the latest run. Regression tests cover an amendment flow whose run-1 link keeps returning run-1 bytes and refuses a file run 1 lacks.
  • No-secret deployments rewrote onto unauthenticatable URLs — with UPLOAD_URL_HMAC_SECRET unset (allowed for GCS/R2), no review token can be minted, but the page still rewrote the three.js import and the preview fetch onto the bearer-gated same-origin routes, which a browser module/fetch request cannot authenticate — the 3D preview died silently. The served page now keeps its stored URLs (whose signed links still work while they live) and injects an explicit "3D preview unavailable: no link secret configured" alert instead of rewriting onto dead URLs.
  • Found while dogfooding: the PLY parser rejected every standard file — parseBinaryPly's property-line destructuring read the property name as its type, so any property float x threw "unsupported property type x" and the preview always fell back to the error box. Fixed, with a node-backed regression test (test_parse_binary_ply_reads_standard_xyz_properties) that extracts the rendered page's parser and runs it against the header shape the scene pipeline writes.

Local validation: focused suites test_review_view.py 17 passed and test_review_page.py 5 passed (the new amendment-isolation and no-secret tests fail on the pre-fix code); ruff check + format clean on changed files; import contracts 5 kept / 0 broken.

Browser evidence (Playwright, headless Chromium; the page navigation carries the reviewer bearer, every same-origin subresource request has the Authorization header stripped — exactly a browser's position):

Full make test after the fixes — 4 failed, 1,523 passed, 19 skipped, 14 errors in 11:17 — the same pre-existing failure set as before (4 expired SD-96 eval gates + 14 cold-restart collection errors), with 90 more passing tests than the previous run from the merged alpha.

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

Human author: Sam Gu (sgu07966@gmail.com)

ObviousApp and others added 5 commits October 16, 2026 07:45
…time

The stored review.html is composed once but served many times, so baked
signed URLs expire after HSS_INGEST_RESULT_URL_TTL_S and the three.js
import points at a CDN — the page goes dark exactly when a reviewer opens
it days later (#255 review finding). Now GET /v1/captures/{id}/review
serves the stored page with, at serve time:

- photo and raw-file links rewritten to reviewer-gated /review/files/
  URLs (photos and verdicts/results only), so an expired URL never
  reaches the served page and keyframes load from live storage
- the three.js import rewritten to /v1/review-static/three.module.js,
  a vendored pinned module, so the 3D viewer needs no network
- a Run status section appended: run id, git SHA, per-stage status and
  error codes (stand-ins labeled), and the run's degraded codes

Review files stay reviewer-only, 404 with result_not_ready when the run
has no result yet, and reject ".." path segments. Tests render the real
template by file path into volume storage and read the exact HTML back.

Co-authored-by: Sam Gu <sgu07966@gmail.com>
The result stage is deterministic CPU work — verdicts, scene and the
catalogue are all local — so it is stubs_exempt like placement: the local
dev journey now publishes a real reviewer page instead of the 37-byte
placeholder. That page is what a reviewer opens, and it discloses which
stages were stand-ins, which is exactly what it is for.

Also harden the TTL test sleep: tokens mint exp=int(now)+ttl with a
strict `<` check, so expiry is only guaranteed once more than a full
second has passed past the TTL — 2.5s raced that boundary.

Co-authored-by: Sam Gu <sgu07966@gmail.com>
The helper accepted the flag but dropped it before building the StageSpec,
so the result stage kept dispatching to its stub on the dev journey.

Co-authored-by: Sam Gu <sgu07966@gmail.com>
The /review line in the integration guide now names what reviewers
actually receive: rewritten artifact links, the vendored three.js
module, and the injected run-status section.

Co-authored-by: Sam Gu <sgu07966@gmail.com>
Real compose mixed with stub criteria verdicts changes the device-facing
outcome (manual_review where the design says needs_more_photos), reshapes
the internal record, and drops preview.ply when the stub scene has no
points. The result stage returns to the stub path; real page delivery is
proven by result-golden and the review-view tests instead.

Co-authored-by: Sam Gu <sgu07966@gmail.com>
@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: 78922e53-afc9-4e72-a108-801bbb6df74d

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.

@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:16:15.286704Z cadd812 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.

@github-actions github-actions Bot added the size:XXL 1,000+ effective changed lines. label Oct 16, 2026
@codemetrics-ai

Copy link
Copy Markdown

Note

Review skipped

Codemetrics is installed on this repository, but it hasn't been added to your team yet.

To enable AI code reviews for this repo, go to your dashboard and add BaseScanning to your team's repositories.

@greptile-apps

greptile-apps Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 2/5 Tier: apex

[Critical impact] Rewrites review pages at serve time with token-gated file access.

Do not merge until uploaded files cannot run scripts and normal gallery loads stop exhausting the reviewer’s request budget.

Fix All in Claude CodeFindings

  1. P1 Security Uploaded HTML can steal tokens ▶
  2. P1 Photo loads block other reads ▶
Fix with agent prompt
### Issue 1
backend/server/api/routes/result.py:553-554
The new file route can serve device-uploaded HTML as a page on the API origin. A device can upload `evidence.html` as `other` with `application/octet-stream`; the capture-folder fallback then returns its bytes as `text/html`. If a reviewer opens that file with reviewer authentication, its script can read any `hss-review-token` saved by the confirmation form. Force untrusted files to download, or restrict which files may render inline.

**How this was verified:** Upload rules allow `.html` names, promotion preserves the uploaded bytes, and this route chooses an inline content type from the filename.

### Issue 2
backend/server/api/routes/result.py:530-532
Every signed photo request consumes the reviewer’s shared 120-request `get` budget. A gallery with more than 120 photos, or several pages opened together, can exhaust it while images load. Remaining photos, `preview.ply`, and ordinary result/status reads then receive 429 responses. The image tags do not retry. Give page resources a separate, suitably sized budget, or pace and retry their requests.

---

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

Summary

Rewrites stored review pages when fetched, adds run details, serves a pinned three.js copy, and fixes PLY parsing.

  • Review pages keep their file links tied to the run they show.
  • Review pages show what each run did and why stages changed.
  • The point-cloud viewer reads standard PLY coordinate properties.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Reviewer bearer] --> B[Fetch review page]
  B --> C{Link secret configured?}
  C -->|Yes| D[Rewrite links and add run status]
  D --> E[HTML with fresh scoped tokens]
  E --> F[Browser fetches without bearer]
  F --> G[Check token scope and expiry]
  G --> H[Serve file or local module]
  C -->|No| I[Keep stored URLs and show alert]
Loading

Reviews (5) · Last reviewed commit: "fix(result): parse the standard PLY head..." · Reviewed by Greptile

Comment thread backend/server/api/routes/result.py Outdated
Comment thread backend/server/api/routes/result.py
Comment thread backend/server/api/routes/result.py
Comment thread backend/docs/INTEGRATION.md Outdated
Review-page browser subresources (photos, raw files, preview.ply, the
vendored three.module.js) cannot send the reviewer's bearer header, so
the page rewrote them into links only the middleware's bearer gate could
authorize — dead images for the reviewer whose page they belong to. The
routes now accept a short-lived signed review token (?rtok=..., the
upload-token HMAC machinery, bound to one capture and file, expiring
after HSS_INGEST_RESULT_URL_TTL_S) or the reviewer bearer; anonymous and
device requests are still refused, and the middleware exempts only those
route shapes, which enforce the pair themselves.

A deployment whose image lacks the vendored three.module.js no longer
rewrites the import to a same-origin 404: the page keeps its original
CDN import and injects a visible alert naming the missing file.

Stand-in labels in the run-status table come from the run's recorded
stage_stubs flag plus the graph's stubs_exempt stages instead of a guess
from status alone, so real work under stubs never reads as a stand-in.

The integration doc's review routes were corrected: /review/files/{name}
and a capture-free /v1/review-static/three.module.js are the shapes the
app serves. A regression test drives every documented review path past
Starlette's router.

Co-authored-by: Sam Gu <sgu07966@gmail.com>
@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: ee8863d259

ℹ️ 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/server/api/routes/result.py
Comment thread backend/server/api/routes/result.py
ObviousApp and others added 4 commits October 16, 2026 17:51
…o-secret page

Regressions for the two open PR #284 findings, written to fail against
the current code:

- After an amendment starts a second run, a link minted for run 1 must
  serve run 1's bytes and a file run 1 lacks must be refused, not served
  from the later run.
- With UPLOAD_URL_HMAC_SECRET unset, the page must keep its stored URLs
  and name the missing link secret instead of rewriting onto routes a
  browser cannot authenticate; in the secret-configured mode every
  subresource the page references must load with no bearer header.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
get_review_file and get_preview resolved the capture's latest run instead
of the run named in the signed token, so after an amendment started a
second run, an earlier link read the later run's files and an open page
could show the wrong run. The artifact now resolves from the token's
signed run, and a file that run lacks is refused with 404
result_not_ready instead of served from another run. A reviewer's own
bearer fetch still follows the latest run.

When UPLOAD_URL_HMAC_SECRET is unset (allowed for GCS/R2) no review token
can be minted, but the page still rewrote the three.module.js import and
the preview fetch onto the bearer-gated same-origin routes, which a
browser module/fetch request cannot authenticate — the 3D preview died
silently. The page now keeps its stored URLs, whose signed links still
work while they live, and injects an explicit '3D preview unavailable: no
link secret configured' alert instead of rewriting onto dead URLs.

Regression tests cover both findings: an amendment flow whose run-1 link
must keep returning run-1 bytes and refuse a file run 1 lacks, and a
no-secret page sweep that fetches every subresource URL the page
references with no bearer header in each mode.

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

Dogfooding the page in a real browser showed 'Point cloud preview
unavailable: unsupported property type x' for any standard preview.ply:
the property-line destructuring read the NAME as the TYPE, so every
'property float x' threw. parseBinaryPly now reads [type, name] from the
split line.

test_parse_binary_ply_reads_standard_xyz_properties extracts the rendered
page's parser and runs it in node against the header shape the scene
pipeline writes (float xyz + uchar generated); it fails on the old
destructure and passes on the fix.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
@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: cadd812bcd

ℹ️ 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 on lines +406 to +411
if _token_secret(deps.settings) is None:
# The Codex finding: with no HMAC secret no review token can be
# minted, so a rewritten link would demand a bearer header a browser
# cannot send. Keep the stored page's original URLs — its signed
# links still work while they live — and say explicitly that the
# 3D preview will not load here.

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 Badge Allow unsigned rewrites in auth-none mode

When HSS_AUTH_MODE is none and UPLOAD_URL_HMAC_SECRET is unset, browser subresource requests are still accepted as the demo principal by authenticate, so the page can safely use the local preview and vendored module routes without a token. This unconditional early return instead leaves the CDN/relative URLs untouched and injects a misleading “3D preview unavailable” warning, causing the demo/local deployment to miss the offline viewer despite having no authentication barrier; gate this fallback on authentication actually requiring a bearer header.

Useful? React with 👍 / 👎.

@SamGu-NRX

Copy link
Copy Markdown
Owner

@greptileai review this at apex

Comment on lines +553 to +554
media_type = mimetypes.guess_type(safe)[0] or "application/octet-stream"
return Response(content=data, media_type=media_type)

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 security Uploaded HTML can steal tokens

The new file route can serve device-uploaded HTML as a page on the API origin. A device can upload evidence.html as other with application/octet-stream; the capture-folder fallback then returns its bytes as text/html. If a reviewer opens that file with reviewer authentication, its script can read any hss-review-token saved by the confirmation form. Force untrusted files to download, or restrict which files may render inline.

How this was verified: Upload rules allow .html names, promotion preserves the uploaded bytes, and this route chooses an inline content type from the filename.

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/server/api/routes/result.py
Line: 553-554

Comment:
**Uploaded HTML can steal tokens**

The new file route can serve device-uploaded HTML as a page on the API origin. A device can upload `evidence.html` as `other` with `application/octet-stream`; the capture-folder fallback then returns its bytes as `text/html`. If a reviewer opens that file with reviewer authentication, its script can read any `hss-review-token` saved by the confirmation form. Force untrusted files to download, or restrict which files may render inline.

**How this was verified:** Upload rules allow `.html` names, promotion preserves the uploaded bytes, and this route chooses an inline content type from the filename.

---

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 +530 to +532
payload = _review_token(request, deps.settings, op="review-file", capture_id=capture_id, name=name)
if payload is not None:
deps.charge(str(payload.get("for") or "review-token"), "get", 120)

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 Photo loads block other reads

Every signed photo request consumes the reviewer’s shared 120-request get budget. A gallery with more than 120 photos, or several pages opened together, can exhaust it while images load. Remaining photos, preview.ply, and ordinary result/status reads then receive 429 responses. The image tags do not retry. Give page resources a separate, suitably sized budget, or pace and retry their requests.

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/server/api/routes/result.py
Line: 530-532

Comment:
**Photo loads block other reads**

Every signed photo request consumes the reviewer’s shared 120-request `get` budget. A gallery with more than 120 photos, or several pages opened together, can exhaust it while images load. Remaining photos, `preview.ply`, and ordinary result/status reads then receive 429 responses. The image tags do not retry. Give page resources a separate, suitably sized budget, or pace and retry their requests.

---

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:XXL 1,000+ effective changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants