• Timmy review build 2026-10-31 — image ingress hardening (#16)
    Quality gates / quality (push) Canceled after 0s
    Pre-Release

    rockachopa released this 2026-10-31 12:44:27 +00:00 | 5 commits to main since this release

    Prerelease for review — tag daily-2026-10-31 on commit 9067ce53067473467d7cc6a0cc681e8ff2054af6 (America/New_York release date 2026-10-31, built 2026-10-31T12:36Z).

    What changed

    The upload boundary stops trusting the caller (issue #16, PR #82). /api/analyze used to decide whether an upload was a photo from the data-URL MIME type the caller supplied. That claim is now ignored: every decision comes from the bytes, and every limit fails before the provider is asked.

    Control Module Refusal
    Container sniffed from magic bytes; a mismatch is refused src/image-ingress.js 400
    Container walked to its own end marker; trailing payload refused (polyglot) src/image-ingress.js 400
    JPEG APP1/APP13/COM and PNG eXIf/tEXt/zTXt/iTXt/tIME refused src/image-ingress.js 400
    Dimensions read from the header; >4096px edge or >16MP refused src/image-ingress.js 400
    Per-caller sliding-window budget, 12 per 60s, with retry-after src/rate-limit.js 429
    6MB ceiling and 15s body deadline; a half-read socket is dropped src/http-body.js 413 / 408
    No bytes, hash, or address in any log line or error body server.mjs + tests —

    What a user sees when a file is not a photo is now its own state — “That file isn’t a photo Timmy can read” and “Nothing was uploaded” — instead of the failed-analysis copy. Manual logging stays one tap away. File handling is self-contained enough that this release adds no new dependency.

    What the release job had to correct before it could ship

    Stated plainly, because each one was a real defect found by a gate rather than by reading:

    1. PR #82 could not pass the gate it added. The builder runs its browser gates with TIMMY_VISION_ENABLED=false, and the 503 AI analysis is disabled branch ran before the request budget, body limits, and byte-level guard. Every hostile upload therefore answered 503, the refusal boundary was never exercised, and test:ingress-capture failed with expected 400, saw 503. Fixed by putting refusals in cost order regardless of model availability (commit 8ce5bc1 on the branch). Verified with vision disabled: hostile bytes → 400 not_an_image, a valid synthetic photo → 503 with the existing copy.
    2. The demo only showed the device guard. The boundary the ticket is actually about — the server deciding from bytes — was never on camera. The recording now lifts its own /api/analyze interceptor for one probe, posts non-image bytes to the running app, and reads the status and copy back out of that response; a 200 fails the recording (commit 2019d51). Both new captions are required gates.
    3. That probe then broke the demo's error budget. The recording allows no browser console errors except the ones a scene produces on purpose, and it enumerated only two fail-closed 503s. The budget is now two 503s, one 400, and nothing else (commit 9067ce5). The first full build stopped here instead of shipping a recording that had never exercised the boundary.

    The RC attack suite now says GO

    Yesterday's release shipped VERDICT: BLOCKED on two P0 ingress holds — ingress-polyglot-html and ingress-garbage-bytes both reached the model worker where they should have been refused. Re-run against this commit:

    VERDICT: GO
    ingress-oversized-body    P0 pass    ingress-polyglot-html     P0 pass
    ingress-garbage-bytes     P0 pass    consent-required          P0 pass
    model-output-malformed    P0 pass    replay-duplicate-request  P2 pass
    flood-concurrency         P1 pass    log-leak-scan             P0 pass
    deletion-withdrawal       P0 covered-elsewhere
    load: 8 requests · p50 17ms · p95 68ms · errors 0 · abstentions 8
    

    Scope note, unchanged from yesterday: the suite is a command line, not a screen in the app, and it is not yet wired into scripts/build_release.py's mandatory gate list. It is evidence the release job read, and it now reads GO instead of BLOCKED. Wiring it in as a blocking gate belongs with #40, which stays open.

    Completed ticket / PR

    • Issue #16 — Harden image ingress: magic bytes, re-encode, limits, metadata stripping, and rate control — closed by this merge.
    • PR #82 merged as c31d0a5, branch head 8ce5bc1. Diff review: 21 files, +1091/−30, code, tests, and docs only — no secrets, model weights, or real medical imagery (the only image-path fixture is a committed text file named .jpg); links a non-human-gate issue (labels area/backend, area/privacy, priority/P0, type/security, type/test).
    • Pre-merge verification on the branch head: npm test 277/277; ingress-capture, ui, photo, mobile-capture, sleek, manual-fallback, provisional, followup, vision-refusal, pattern-summary, grounded-chat, photo-lifecycle all green under the builder's own environment (vision disabled), with served-byte hashes checked against the checkout.

    Gates — 29/29 passed

    npm test 277/277 (up from 244 at the previous release); image ingress refusal; mobile green path; photo-first acceptance; sleek Hermes chat; manual fallback + draft recovery; provisional suggestion state; hard-stool follow-up; vision corpus refusal; abstention quality gate; worker container receipts; worker post-load readiness; longitudinal pattern summary; grounded chat citations; training contribution consent; corpus manifest provenance; reviewer adjudication blind; dataset card; calibration harness; operator backup restore; production runbook commands; portable backup restore; dependency audit (0 high); syntax (node/bash/python); secret scan; forbidden artifacts; demo H.264 720×1280 probe; complete MP4 decode; release delta verified painted on camera (99/99 captions, delta caption 4 lines / padBottom 14 px).

    Artifacts

    • source archive timmy-talking-turd-2026-10-31-9067ce530674.tar.gz — 371,298 bytes — sha256 e84bd1ed3cfb3df7cf51d464d070ab84d4ccce9f8b3977401109a7813d4ce295
    • demo video timmy-talking-turd-2026-10-31-9067ce530674-demo.mp4 — 29,041,842 bytes — H.264 720×1280 yuv420p, 25 fps, 457.72 s — sha256 2c01253110c2633af549e600ab478035444dacf82f81bdbc7b6c570df457b564

    Model / profile

    Built and recorded by Hermes (profile default, model deepseek-flash). The demo runs the real app from the built commit with TIMMY_VISION_ENABLED=false; vision replies replay the committed CC0 synthetic corpus through the app's real analysis path, and the configured vision profile remains self-hosted SmolVLM2-2.2B-Instruct (pinned GGUF) with the hosted profile as fallback. The new IMAGE INGRESS, REFUSED BY THE SERVER card is not a fixture: it is one real HTTP request to the running app, and the card prints the status and copy that response returned. It measures the refusal boundary, not model quality — no live inference appears in the video and no real photo is used anywhere.

    Demonstrated on camera

    • A file that only claims to be a photo is refused on the device — nothing is uploaded — the committed text fixture chosen as photo.jpg, refused before any upload, with manual logging one tap away
    • The device guard is not the only one: the same bytes, sent straight to the running server — the demo's own interceptor lifted for one probe
    • IMAGE INGRESS, REFUSED BY THE SERVER card — status 400 refused, code not_an_image, copy That file is not a JPEG, PNG, or WebP photo., provider never called, budget counted by the per-device limiter, after the journal, the draft, and manual logging are untouched, and a scope row naming which cases this probe does not cover
    • Refused at the server too: 400 not_an_image, before any model work

    Known accuracy limitation

    Unchanged by this release, and not made better by it. Bristol accuracy is not clinically validated; the calibration receipt still reports BLOCKED because no candidate model clears every gate. User confirmation remains mandatory everywhere: photo analysis may suggest visible Bristol form, broad color, and image quality only, and never symptoms or a diagnosis. Visible color is not blood.

    On the ingress boundary specifically, the claims are bounded by what was tested: the suite's synthetic attacks and 33 new tests, not every conceivable container or decoder exploit. Residual risks are stated in docs/PHOTO-DATAFLOW-THREAT-MODEL.md rather than glossed — no server-side transcode, so unusual-but-valid containers are refused, not repaired; PNG CRCs are not validated; the rate budget is per process, so it does not survive a restart and is not shared across workers.

    Downloads