Turn CI green, encrypt sync for real, and fix two silent no-ops#2
Merged
Conversation
… untrack hone-ide-debug; mark private
…nd fix textSetFontFamily arity (font was never applied)
CI's typecheck job had failed on every run since May and gated nothing. Two
separate causes, both fixed here.
1. The job itself was broken. It piped tsc through a ~30-pattern "known noise"
filter, but ran the pipe under `set -o pipefail`, so tsc's non-zero exit
killed the step before the filter was ever consulted. The filter was dead
code. Removed it — the step is now a plain `bun run typecheck`.
2. The 39 errors it was hiding were mostly real. Fixed at the source:
- perry/i18n had NO ambient declaration while 28 files import { t } from it.
Added one matching Perry's actual API (t + the locale format wrappers).
hone-ide does ship 12 locales, so this import is live, not decorative.
- fs/child_process decls still described hone's old Perry fork. Added the
upstream members the compat shims actually use (statSync/lstatSync/openSync
+ a Node-shaped Stats, spawn + ChildProcess). perry-ui was missing
widgetSetTooltip.
- tests/agentic/setup/seed-files is a self-contained fixture workspace with
its own package.json/tsconfig, copied into a temp dir by the agentic tests.
It was never meant to typecheck under this config — excluded.
3. Two errors the filter hid were genuine bugs:
- findBarGetViewportStart() read `viewport.visibleRange.startLine`, but
ViewportManager exposes getVisibleRange(). The property is undefined, so
the find bar's viewport callback would throw at runtime.
- textSetFontFamily was declared (widget, size, family) — hone's old fork
signature. Perry's real signature is (widget, family): see the dispatch
table at perry-dispatch/src/ui_table/part_a.rs (args: [Widget, Str]) and
perry_ui_text_set_font_family(handle, family_ptr) in every perry-ui-*.
Perry's table dispatch treats a declared-arity mismatch as side-effect-only
lowering (perry-codegen/src/lower_call/ui_tables.rs:433-440) — it evaluates
the args and returns WITHOUT emitting the call. So all 38 three-arg sites
were silent no-ops and monoFont() has never actually been applied: no
crash, just the proportional fallback everywhere Menlo was intended
(commit graph ASCII art, code blocks, diagnostics, chat, context chips).
All 38 sites already set the size via textSetFontSize within ±3 lines, so
dropping the redundant middle arg loses nothing.
Neither typecheck nor `perry compile` could have caught #3 — the stub and the
call sites agreed with each other, just not with Perry. Verified: typecheck
39→0, `perry compile` clean (24.4MB), app launches and renders.
Cross-device sync advertised X25519 + AES-256-GCM end-to-end encryption while
every crypto primitive was stubbed to a no-op in both sync-host.ts and
sync-guest.ts:
function ccAes256GcmEncrypt(plaintext, keyHex, nonceHex) { return plaintext; }
function ccHkdfSha256(...) { return ''; } // -> project key stays empty
So the relay saw file deltas, AI prompts, and Claude Code transcripts in
cleartext, while hone.codes says "end-to-end encrypted" eight times over
(AUDIT-2026-07.md H1). This closes that gap.
The stubs blamed "custom Perry crypto intrinsics that the current toolchain no
longer provides" — accurate but no longer a blocker: Perry maps node:crypto
natively, so the primitives are now implemented against the standard API in a
new shared sync-crypto.ts (they were duplicated verbatim in both files).
Also fixed, both flagged in the same audit finding:
- generateProjectKey stitched two 12-byte nonces and ran them through HKDF to
"reach" 32 bytes. HKDF cannot manufacture entropy it wasn't given — that was
24 bytes of randomness wearing a 32-byte coat. Now randomBytes(32).
- hostSecret was 'secret_' + deviceId + '_' + Date.now(), guessable from a
known deviceId and an approximate launch time. Now 256 CSPRNG bits.
Fail-closed on decrypt. decryptDelta previously returned the ciphertext
unchanged when the key was missing or the format was unexpected, handing
unauthenticated attacker-controlled bytes back as if they were plaintext. It
now returns '' on any failure (bad GCM tag, truncation, no key) and the relay
message handler in render.ts drops the message instead of interpreting it.
KEY ENCODING CAVEAT (documented in sync-crypto.ts): Perry does not emit real
SPKI/PKCS8 DER for X25519 — export() returns an opaque surrogate string
("PERRY-X25519-PUBLIC:<base64url>") that createPublicKey accepts back. Fine
for Hone (every client is Perry-compiled), but the pairing wire format is
Perry-specific and will not interop with a third-party client.
Verified with a Perry-compiled probe driving the actual protocol shape:
PROJECT_KEY_LEN=64 DH_KEYS_MATCH=true
PROJECT_KEY_TRANSFERRED=true (host wrap -> guest unwrap)
CIPHERTEXT_IS_NOT_PLAINTEXT=true DELTA_ROUNDTRIP=true
TAMPER_REJECTED=true WRONG_KEY_REJECTED=true NONCES_UNIQUE=true
IDE typechecks 0 errors, compiles clean (24.4MB), launches and renders.
STILL REQUIRED before advertising this as shipped (GTM Gate B):
- relay-side packet capture asserting every non-handshake envelope is ciphertext
- key rotation policy for long-lived rooms; revoke flow when a device is unpaired
- the guest/iOS side needs the same end-to-end run on device
18 tests over the sync crypto primitives. The load-bearing one is the dullest: ciphertext is not plaintext. That is precisely the bug that shipped — the old ccAes256GcmEncrypt returned its plaintext argument unchanged — so it is worth a test that would have failed loudly. Covers: nonce length + non-repetition across 1000 draws (GCM nonce reuse is catastrophic), HKDF determinism/length/domain-separation/salt sensitivity, GCM round-trip incl. unicode and empty payloads, 16-byte tag framing, distinct ciphertext per nonce, and fail-closed rejection of tampered ciphertext, tampered tag, wrong key, wrong nonce, and under-length payloads. Deliberately does NOT test the X25519 helpers. These run under bun (node crypto), and that path is not portable: Perry's KeyObject.export() returns an opaque surrogate string rather than real SPKI/PKCS8 DER and ignores the requested encoding, so ccX25519Keypair's String(export(...)) round-trip works on Perry and would not here. Pairing is covered by a Perry-compiled probe instead; the header explains this so nobody "fixes" the gap by making the test pass against the wrong runtime. Suite: 183 pass (was 165), same 4 pre-existing failures (2 layout, 2 theme — present at a14633c, before this branch's work). Typecheck 0.
AUDIT-2026-07.md H1 lists "relay-side packet capture asserting every non-handshake envelope ships ciphertext" as required before sync can be advertised as encrypted. This asserts the same property at the point the envelope is built — no relay, no network, runs on every commit. It could not be tested where it lived. sync-transport.ts imports Perry's `ws` extensions (sendToClient/isOpen/receive), which npm's `ws` does not export, so the module cannot be imported under `bun test` at all. The one security property that matters had no test — which is how sync shipped as a plaintext pass-through while claiming E2E encryption. So the envelope construction and the encrypt-or-not decision move to a new sync-envelope.ts with no ws/FFI imports. sendToRelayTarget now calls buildRelayEnvelope(...); behavior is unchanged (same fields, same escaping, same gate). isPairingHandshake moves with it rather than being duplicated — two copies of "which messages may ship cleartext" is the kind of rule that drifts apart. 13 tests. The ones worth naming: - ciphertext, not plaintext, once encryption is ready - "encrypted" flag always describes what actually happened — never optimistic - the pre-key window ships cleartext and says so honestly - the handshake exemption is ANCHORED: a payload merely containing "PAIR_REQ|" further in doesn't win the exemption and slip past the gate unencrypted - a regression guard pinning the exact H1 shape: with an identity "encrypt", the flag reads true while the wire carries plaintext — the test fails loudly if ccAes256GcmEncrypt ever goes back to returning its argument - a payload of '","encrypted":true,"x":"' cannot break out of the envelope Typecheck 0, compile clean (24.4MB), 200 tests / 196 pass (same 4 pre-existing layout+theme failures, present before this branch).
…unaided Perry >=0.5.1235 refuses to link a dependency's perry.nativeLibrary unless the host package.json approves it (PerryTS/perry#497). hone-ide declared no `perry` block at all, so a plain `perry compile src/app.ts` fails outright and the only way through is PERRY_ALLOW_PERRY_FEATURES=1 — which switches the check off globally instead of approving anything in particular. Approves the three first-party libraries we actually link: @honeide/editor/perry — Core Text / tree-sitter renderer @honeide/terminal/perry/live — PTY @honeide/lsp-bridge/perry/live — LSP transport Entries match the IMPORT SPECIFIER, not the package name (hence /perry/live) — the error message quotes the specifier, and allow-listing "@honeide/terminal" does nothing. Verified: `perry compile src/app.ts` now exits 0 with a clean environment (24.4MB); typecheck still 0. This does NOT unblock CI's perry-compile-macos job (still `if: false`). That job runs `perry compile` bare, with no install step, on a runner that has no Perry toolchain — and a local compile links against a locally-built libperry_stdlib.a from the Perry source tree, which CI has no way to obtain today. The `if: false` comment ("enabled once perry runner image is published") is still the accurate description. Worth revisiting when that image exists: the compile path is the only thing that would catch broken FFI wiring, and it has never run in CI — which is how a wrong perry/ui declaration silently no-op'd 38 call sites (see d5e0a9c). Note for follow-up: @honeide/lsp-bridge is imported by src/workbench/views/lsp/ lsp-bridge.ts but is absent from this package.json's dependencies, yet still resolves. Worth understanding before it bites someone on a clean checkout.
…the chat auto-scroll bug Follow-up to d5e0a9c. textSetFontFamily turned out to be a silent no-op because our hand-written .d.ts disagreed with Perry's real signature, and Perry lowers a declared-arity mismatch to side-effect-only evaluation instead of erroring. That is invisible to both tsc and `perry compile`, so the obvious question was: what else is wrong? Swept all 70 declarations in perry-ui.d.ts against the 285-entry dispatch table in perry-dispatch/src/ui_table/. Result: 63 match. 4 aren't in the table (App/Button/HStack/VStack — specially handled in codegen's native_ui_widgets_branch.rs, fine). 3 disagreed: - Picker: declared (label, onChange, style), Perry takes [Closure]. Zero call sites — a dead declaration, left alone. - saveFileDialog: declared defaultName/directory as optional; Perry takes [Closure, Str, Str]. Both call sites pass all three so it works today, but the `?` was a loaded gun — a 1-arg call would typecheck and the dialog would just never open. Now required. - scrollViewScrollTo: an invented overload. Fixed below. scrollViewScrollTo is an UPSTREAM PERRY BUG, and our code is the correct side. All 8 platform runtimes (macos/ios/visionos/watchos/tvos/gtk4/android/windows) implement perry_ui_scrollview_scroll_to(scroll_handle, child_handle) — two handles, scroll-to-child. The dispatch table (part_a.rs:206) declares [Widget, F64, F64]. The table is wrong; the runtimes are unanimous. So chat-panel.ts's scrollToBottom() — scrollViewScrollTo(view, widget) — is right and is being compiled away. AI chat has never auto-scrolled to new messages. The (scrollView, x, y) overload someone added here to match the table is fiction: no runtime accepts it, and calling it that way would pass x where a child widget handle is expected. Removed it, kept the real 2-arg signature, and documented the bug at both the declaration and the call site so nobody "fixes" this by adding a third argument and quietly makes it worse. scrollViewSetOffset is not an escape hatch either — there the runtimes disagree with each OTHER (iOS takes 3 params, macOS 2). Not patching Perry's table here: it's a different repo and a codegen change for every Perry user. Needs filing upstream — one line, args: &[Widget, Widget], after which our call works unchanged. Typecheck 0, 200 tests / 196 pass (4 pre-existing), compile clean on a clean env.
The PR run exposed what local runs could not: typecheck passed on my machine and failed on CI with TS2307 on @honeide/editor/perry, @honeide/terminal/perry/ live and @honeide/lsp-bridge/perry/live. Two distinct causes, both environment, not code — and both previously hidden by the noise filter removed in d5e0a9c (its `@honeide/.+/perry/live` pattern was papering over a broken CI environment rather than a broken import). 1. core/editor/terminal are `file:../hone-*` siblings living in their own repos. The typecheck job checks out only this repo, so they cannot resolve. Added the same "Clone dependencies" step release.yml already uses (the HONE_*_REPO secrets exist). 2. @honeide/lsp-bridge resolved locally only via a hand-made symlink in node_modules created back in March, pointing at native/lsp — which is *in this repo* and declares `name: @honeide/lsp-bridge`. It was never in dependencies, so `bun install` had no reason to create it: a clean checkout could not typecheck, by anyone, anywhere. Now declared honestly as `file:./native/lsp`. Verified by reproducing the CI condition locally: deleting the symlink reproduces the failure; `bun install --no-save` then provisions lsp-bridge from the in-repo path and typecheck returns to 0 with no symlink involved. 200 tests / 196 pass unchanged (same 4 pre-existing). This is the first time CI has actually run this suite, which is the point of d5e0a9c — a green checkmark that never executed was worth nothing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Six commits. Two themes: CI never actually gated anything, and sync was never actually encrypted.
CI was red since May and gating nothing
The typecheck job piped
tscunderset -o pipefail, so a non-zero exit killed the step before its own ~30-pattern "known noise" filter ever ran. The filter was dead code. Removed it — the step is now a plainbun run typecheck.The "noise" it was hiding was mostly real. Typecheck is 39 → 0:
perry/i18nhad no ambient declaration at all, while 28 files import{ t }from it (the IDE genuinely ships 12 locales — this import is live).fs/child_processdeclarations still described hone's old Perry fork. Added the upstream members the compat shims actually use (statSync/lstatSync/openSync+ a Node-shapedStats,spawn+ChildProcess);perry-uiwas missingwidgetSetTooltip.tests/agentic/setup/seed-filesis a self-contained fixture workspace (ownpackage.json/tsconfig, imports a.pyfile) copied into a temp dir by the agentic tests. It was never meant to typecheck here — excluded.Sync advertised E2E encryption and shipped plaintext
Every crypto primitive was stubbed in both
sync-host.tsandsync-guest.ts:So the relay saw file deltas, AI prompts, and Claude Code transcripts in cleartext while hone.codes says "end-to-end encrypted" eight times (AUDIT-2026-07.md H1).
The stubs blamed missing Perry crypto intrinsics — true, but no longer a blocker: Perry maps
node:cryptonatively. Now implemented for real in a sharedsync-crypto.ts(the stubs were duplicated verbatim in both files). No Rust, no FFI.Also fixed under the same finding:
generateProjectKeystitched two 12-byte nonces and ran them through HKDF to "reach" 32 bytes. HKDF cannot manufacture entropy it wasn't given — that was 24 bytes of randomness in a 32-byte coat. NowrandomBytes(32).hostSecretwas'secret_' + deviceId + '_' + Date.now()— guessable from a known deviceId and an approximate launch time. Now 256 CSPRNG bits.''on any failure and the relay handler drops the message.Two silent no-ops that no gate could catch
Perry lowers a declared-arity mismatch to side-effect-only evaluation — it evaluates the args and returns without emitting the runtime call (
perry-codegen/src/lower_call/ui_tables.rs:433-440). No compile error, no crash, no linked symbol. So a wrong hand-written.d.tstype-checks and compiles while doing nothing.textSetFontFamilywas declared(widget, size, family); Perry's real signature is(widget, family). All 38 three-arg sites were no-ops —monoFont()has never been applied: commit-graph ASCII art, code blocks, diagnostics, chat, context chips all render proportional. Every site already set the size viatextSetFontSizewithin ±3 lines, so dropping the redundant middle arg loses nothing. Fixed.scrollViewScrollTo— swept all 70 perry/ui declarations against Perry's 285-entry dispatch table (63 match;Pickerwrong but zero call sites;saveFileDialogoptionals hardened). This one is an upstream Perry bug and our code is the correct side: all 8 platform runtimes implementperry_ui_scrollview_scroll_to(scroll_handle, child_handle), but the table declares[Widget, F64, F64]. SoscrollToBottom()is compiled away and AI chat has never auto-scrolled. Removed the invented(scrollView, x, y)overload (no runtime accepts it) and documented the bug at both the declaration and the call site so nobody "fixes" it by adding a third argument and makes it worse. Fix is one line upstream:args: &[Widget, Widget].Also fixed a genuine crash path:
findBarGetViewportStart()readviewport.visibleRange.startLine, butViewportManageronly exposesgetVisibleRange()— it's wired into the find bar and would throw.Tests
31 new. The load-bearing ones are the dullest: ciphertext is not plaintext, and the envelope's
encryptedflag always describes what actually happened. Envelope construction moved to aws-freesync-envelope.tsbecausesync-transport.tsimports Perry'swsextensions and cannot be imported underbun testat all — which is how the one property that mattered had no test.Includes a regression guard pinning the exact H1 shape (with an identity "encrypt", the flag reads true while the wire carries plaintext) and a check that the
PAIR_REQ|cleartext exemption is anchored, so a payload merely containing that marker can't slip past the gate unencrypted.Build
perry compile src/app.tsnow works on a clean environment — addedperry.allow.nativeLibrary(Perry #497). Previously the only way through wasPERRY_ALLOW_PERRY_FEATURES=1, which disables the check globally rather than approving anything. Entries match the import specifier, not the package name.Verification
a14633c, before this branchperry compileclean on a clean env (24.4MB); app launches and rendersNotes for review
KeyObject.export()returns an opaque surrogate string, not real SPKI/PKCS8 DER. Fine today (every Hone client is Perry-compiled), but pairing will not interop with a third-party client. Documented insync-crypto.ts.perry compileis still not tested in CI (if: false— no Perry toolchain on the runner). That gap is why the font bug survived: the stub and the call sites agreed with each other, just not with Perry.@honeide/lsp-bridgeis imported byviews/lsp/lsp-bridge.tsbut absent frompackage.jsondependencies, yet still resolves. Worth understanding before it bites someone on a clean checkout.