Skip to content

0.6.0 — QR passthrough, typed errors

Choose a tag to compare

@Sy-D Sy-D released this 02 Sep 20:35
· 5 commits to main since this release

QR passthrough, typed errors, and the bench that prices an approval against a takeover.

Scan QR. A device-change check (reCAPTCHA's "scan to verify", WhatsApp Web, authenticator enrolment) draws a QR code and asks for a phone. handraise's human is holding that phone, looking at the code on its screen, and a phone cannot scan itself. Now the phone has a Scan QR key: the agent takes a full-resolution screenshot, decodes it on a worker thread (jsqr plus a bounded PNG decoder over node:zlib; BarcodeDetector does not exist in Solari's Chromium), and hands the phone what it said. http, https and mailto open, shown as the canonical address with the host as the loud part; tel: and otpauth: are decoded and copyable but never one tap away. Measured: a 4K screenshot used to hold the agent's event loop for 2.1 s and now holds it for 1 ms; an 815 KB PNG bomb that cost 873 MB is refused before a byte is inflated. reCAPTCHA itself could not be reproduced and is documented as untested.

Typed errors. Everything raiseHand throws is a HandraiseError with a code — missing_api_key, invalid_mode, empty_action, browser_unusable, relay_start_failed, concurrency_limit, relay_not_ready — and the SDK error as cause with credentials redacted. A broken logger (throwing, throwing getter, async-rejecting) can no longer end a handoff. The relay health poll enforces its deadline per request. The preview token is redacted from every message it could reach, in every encoding a proxy produces.

Measured. bun run bench:mixed: 20 workflows, interleaved takeover and approval, 20/20 completed. An approval costs 1 frame, 25 KB and 5.3 s of relay sandbox; a takeover 14 frames, 142 KB and 10.7 s. Time to visible is the same, because the relay cold start dominates both.

Changed, read this: a page that is already dead now throws browser_unusable before anything is created instead of returning { outcome: "disconnected" } — if you only branched on result.outcome, add a catch. Solari SDK errors are wrapped; the class is on error.cause. HandoffEvent gains two required numbers, qrScans and qrHits. Node 20 is now declared as the floor it always was.

Reviewed by two Opus passes and a GPT-5.6 Sol security pass per PR, with fix rounds; 289 tests; live e2e against Solari including the QR round trip. ADR: docs/adr/0008-qr-passthrough.md. Full CHANGELOG.