Skip to content

ios: bind placement answers to the submitted scene - #198

Merged
SamGu-NRX merged 2 commits into
codex/public-assembly-20260930from
codex/result-input-binding
Oct 2, 2026
Merged

SamGu-NRX merged 2 commits into
codex/public-assembly-20260930from
codex/result-input-binding

Conversation

@SamGu-NRX

@SamGu-NRX SamGu-NRX commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #191 (codex/public-assembly-20260930). Addresses the response-binding half of #144; it doesn't close the issue. The local-network purpose-string half stays open there.

What changed

The placement client decoded the answer's stats.input_sha256 but never compared it with the scene it had sent. Now HTTPResultClient.submit runs ResultBinding.check on every 2xx answer before returning it. An answer is used only when it declares the SHA-256 of the exact bytes this phone sent.

The server hashes the scene bytes it parses: server/api.py _parse passes the bare scene.json body to parse_scene, and server/scene.py:751 hashes it. server/schemas/result.schema.json requires the field as 64 lowercase hex digits. The check fails closed:

  • If the check can't read the answer, or the field is missing, null or not a string, the answer is refused.
  • If the value isn't 64 lowercase hex digits, the answer is refused.
  • If the hash is for another scene, the answer is refused.

A refused answer is never shown. It goes through the existing "We couldn't read the server's answer" recovery (UploadFailureKind.unreadableAnswer), which leads back to the review and isn't retried.

SampleResultClient stays unchecked, and its comment now says so. The bundled sample answers no particular scene, since its hash is all zeros. The general PlacementResult decoder and the UI-test fixtures are unchanged. The UI tests use the sample client, a real server through HOUSESCAN_SERVER_URL, or -uiDemoResultFile, which never goes through HTTPResultClient.

Why

This is defence in depth. A server answer is only useful for the scene it was computed from, and the field to prove that already existed. Nothing shows the current server sending an answer for another scene.

Verification

  • ResultBindingTests is built from a real server answer to the synthetic replay. It covers:

    • a matching answer, which passes and decodes;
    • a mismatched hash;
    • an absent, null or numeric hash;
    • an uppercase, short or non-hex hash;
    • a body that isn't JSON;
    • a mismatched answer that also carries an unparseable number elsewhere.

    Every refusal classifies as unreadableAnswer and isn't retryable.

  • Locally, through the shared QA queue: HouseScanKit passed 650/650 tests in 92 suites, and the app built (xcodebuild … -destination generic/platform=iOS, no signing).

  • No UI test was run locally. No phone or live server was used.

Risk

Checklist

  • The scope is focused; related changes belong together
  • I explained what changed and why
  • N/A: before/after images. No screen changes; a refused answer reuses the existing failure screen.
  • N/A: video. No motion or interaction changes.

🤖 Generated with Claude Code

…s sent

The placement client decoded the answer's stats.input_sha256 but never
compared it with what it sent (#144). The server hashes the exact scene
bytes it parses: for a bare scene.json that is the request body (server/api.py
_parse, server/scene.py). The result schema requires the field as 64
lowercase hex digits.

HTTPResultClient now runs ResultBinding.check on every 2xx answer before
returning it. The check reads stats.input_sha256 with JSONDecoder, requires
the schema's form, and compares it with the SHA-256 of the bytes sent. It
fails closed:
- an answer it can't read, or one without the field, is refused as
  noInputHash;
- a value that isn't 64 lowercase hex digits is refused as
  malformedInputHash;
- a hash for another scene is refused as mismatch.

Each refusal reaches the existing "We couldn't read the server's answer"
recovery and isn't retried. SampleResultClient answers no particular scene
(its hash is all zeros), so it stays explicitly unchecked. The general
PlacementResult decoder and the test fixtures are unchanged.

This is defence in depth. Nothing shows the current server sending an answer
for another scene.

ResultBindingTests is built from a real server answer to the synthetic
replay. It covers a matching answer, a mismatch, an absent, null or numeric
hash, an uppercase, short or non-hex hash, a body that isn't JSON, and a
mismatched answer carrying an unparseable number elsewhere.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: e1e881f2-a4c6-4af6-b05f-e732ca64a64a

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
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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:M 30-99 effective changed lines. label Oct 2, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 2, 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-02T19:01:12.058236Z 7c617dc PR opened
ℹ️ 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.

@codemetrics-ai

codemetrics-ai Bot commented Oct 2, 2026

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 2, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Adds validation that server answers match the submitted scene.

The PR appears safe to merge based on the reviewed changes.

Summary

The PR checks that each successful HTTP placement answer declares the SHA-256 of the exact scene bytes submitted, refusing missing, malformed, or mismatched hashes. The latest changes remove hashes from refusal descriptions logged by the app and add a regression test for those descriptions.

Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Submit scene bytes] --> B[Receive 2xx answer]
  B --> C{Declared hash matches submitted bytes?}
  C -- Yes --> D[Return answer for decoding]
  C -- No or unreadable --> E[Refuse answer]
Loading

Reviews (2) · Last reviewed commit: "ios: a refused placement answer's log li..."

Comment thread ios/HouseScanKit/Sources/HouseScanKit/Scene/ResultBinding.swift Outdated
Comment thread ios/HouseScanKit/Tests/HouseScanKitTests/ResultBindingTests.swift
ScanEngine logs an upload failure's description with public privacy.
ResultBinding.Refusal's description included the full SHA-256 of the scene
sent, and for a malformed value the server's declared text (Greptile on
#198). A scene's hash fingerprints the homeowner's scan, so it shouldn't be
in the device log.

The description now says what went wrong and, for a malformed value, its
length. The hashes stay in the enum cases for callers and tests.
aRefusalsDescriptionNamesNoHash checks all three refusal kinds.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@SamGu-NRX
SamGu-NRX merged commit ef1a4cf into codex/public-assembly-20260930 Oct 2, 2026
6 checks passed
@SamGu-NRX
SamGu-NRX deleted the codex/result-input-binding branch October 15, 2026 11:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 effective changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant