Skip to content

auto-fix batch 2026-04-28 (claude/friendly-maxwell-Oggvw)#473

Merged
intendednull merged 10 commits into
mainfrom
claude/friendly-maxwell-Oggvw
Apr 28, 2026
Merged

auto-fix batch 2026-04-28 (claude/friendly-maxwell-Oggvw)#473
intendednull merged 10 commits into
mainfrom
claude/friendly-maxwell-Oggvw

Conversation

@intendednull
Copy link
Copy Markdown
Owner

8 small-scope fixes swept from open issue queue. All sub-PRs merged via local cargo gate (sub-PR base ≠ main → CI no run). Master PR runs full CI on open — load-bearing gate.

Fixes

Skill Evolution

Skill commit on this branch: 75be15a — three sandbox notes folded into .claude/skills/resolving-issues/SKILL.md:

  1. just may be absent in some sandboxes — implementer falls back to raw cargo equivalents (same gate, different binary). Note added to ### Sub-PR rules.
  2. Worktree dir may be pre-populated with prior-session residue — incorporate cleanly, don't blindly --force reset. Note added to ## Setup.
  3. Browser tests may compile-only when wasm-pack / firefox / geckodriver absent — cargo check --target wasm32-unknown-unknown -p willow-web --tests is the fallback gate, real headless run executes on master-PR CI. Note added to ## Quality.

Follow-ups

Lessons Learned

  • Pre-dispatch sync (git fetch + reset --hard origin/<master>) worked clean across 8 dispatches. Each implementer worktree had latest prior merges as base.
  • Webhook-informational rule held — coordinator never broke flow on a sub-PR webhook; implementer always finished its merge gate first.
  • Caveman-mode GH comms kept master + sub-PR bodies tight. Code blocks stayed normal.
  • Implementer-filed follow-ups ([security] wire RatchetCache::clear() into leave-server / sign-out when cache hits decrypt path #468) worked — scope-creep stayed inside this PR, follow-up issue captures deferred work for next scheduled run. No drafts left behind.
  • Several implementers reported pre-existing flaky test willow-actor::send_dead_actor_returns_send_error (passes in isolation, fails under cargo test --workspace). Not blocking — unrelated to this batch.
  • Worktree residue from prior sessions surfaced 3 times across 8 dispatches; implementers handled by inspecting + incorporating rather than force-removing. Worth keeping in skill.

Test plan

  • Master-PR CI runs full just check (fmt + clippy + test + wasm + browser).
  • Manual smoke: just dev + agent-browser navigation through web UI to confirm CSP didn't regress mount, voice config didn't regress join flow, *Signals rename didn't regress reactivity.

Generated by Claude Code

claude and others added 10 commits April 28, 2026 08:25
…462)

Lock down the document head with a CSP that matches the app's actual
loads:

* default-src 'self' / object-src 'none' / frame-ancestors 'none' /
  base-uri 'self' / form-action 'self' — kill the broad injection +
  clickjacking surfaces.
* script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval' — 'wasm-unsafe-eval'
  is required for the Leptos WASM module; 'unsafe-eval' is currently
  required because crates/web still calls js_sys::eval() (theme
  bootstrap, focus helpers, relay-URL probe). Removing it is tracked by
  issues #171 / #425. Documented inline so the next person who reads
  the head sees the rationale.
* style-src 'self' 'unsafe-inline' + Google Fonts host — Leptos views
  emit inline style="…" attributes, and foundation.css @imports the
  Fraunces / IBM Plex Sans / JetBrains Mono stylesheet. font-src
  separately allow-lists fonts.gstatic.com for the actual font files.
* connect-src 'self' ws: wss: https: — relay WebSocket transport plus
  the relay's /bootstrap-id HTTP probe.
* img-src 'self' data: blob: + media-src 'self' blob: — avatar data
  URIs, runtime URL.createObjectURL attachments, the chime.webm.
* worker-src 'self' — service worker registration in init.js.

Add a static-asset test that asserts the meta tag is present and that
each required directive substring is in the content attribute, so any
future loosening / rewording trips the test before regressing the
policy.

Refs #175

Co-authored-by: Claude <noreply@anthropic.com>
three new unit tests in crates/identity/src/lib.rs covering endpoint id hex parsing, garbage profile bytes, and truncated/short-pk envelope rejection paths. tests-only. Refs #424
Voice mutation API in crates/client/src/mutations.rs had zero test
coverage at any tier. Added crates/client/src/tests/voice.rs with one
happy-path test per mutator using the test_client() harness — tier 2
per CLAUDE.md (lowest tier covering behaviour, no DOM needed).

Coverage:
- join_voice: active channel set, local peer in participants
- leave_voice: active channel cleared, local peer removed
- toggle_mute: returns new value, is_voice_muted reflects, round-trip
- toggle_deafen: same shape as toggle_mute
- voice_peer_joined: peer inserted into participants, ClientEvent::VoiceJoined fired
- voice_peer_left: peer removed, ClientEvent::VoiceLeft fired

Tradeoffs:
- voice_peer_joined / voice_peer_left arrive in production via the
  gossip listener unpacking WireMessage::VoiceJoin / VoiceLeave.
  Driving them end-to-end through a fake wire message would re-test
  serialisation; the mutators themselves are best exercised directly
  with a synthetic peer id (mirrors what record_typing does in
  multi_peer_sync.rs).
- join_voice / leave_voice's broadcast_on_topic call no-ops without a
  subscribed topic, so the test_client harness suffices — no MemHub
  needed. Wire delivery between peers is exercised in the listener
  tests already.

Refs #414

Co-authored-by: Claude <noreply@anthropic.com>
Adds five tests in crates/client/src/tests/governance.rs covering
the mutators previously only Playwright-covered (or with zero
coverage anywhere):

  * propose_grant_admin     -> Propose { GrantAdmin }
  * propose_revoke_admin    -> Propose { RevokeAdmin }
  * propose_kick_member     -> Propose { KickMember }
  * propose_set_threshold   -> Propose { SetVoteThreshold }
  * delete_role             -> DeleteRole

Each test drives the mutator against a `test_client()` fixture
(genesis author = owner = automatic admin) and inspects the
managed DAG to assert the expected EventKind variant and payload.
Downstream materialisation (vote tally, role removal) is tier-1
state-machine territory and stays out of scope, mirroring the
convention from voice.rs (#464).

Refs #416

Co-authored-by: Claude <noreply@anthropic.com>
… (#469)

RatchetCache stored derived ChannelKey values in BTreeMap +
ratchet_states without any caller-driven way to wipe them. ChannelKey
already implements ZeroizeOnDrop, but secrets only zeroize when the
map drops the entry — so cached keys lingered until eviction pressure
hit max_entries or the cache itself was dropped.

Add `pub fn clear(&mut self)` that empties both maps. BTreeMap::clear
and HashMap::clear drop each value in place, triggering the per-key
ZeroizeOnDrop pass before the buckets are released, so secret material
is wiped synchronously at the call point.

Also add two unit tests:
- ratchet_cache_clear_drops_all_state: populate three epochs, call
  clear(), assert both `cache` and `ratchet_states` are empty + the
  cache is still usable for fresh derivations.
- ratchet_cache_clear_is_idempotent: clear-on-empty is a no-op.

Brainstorm — wiring point. RatchetCache is currently API surface only:
no client/web code calls `derive_or_cached`, so there is no live
production cache to wire into leave_server / sign-out today.
Speculatively integrating the cache into the client decrypt path was
considered (runner-up) and rejected: that's a perf/correctness change
distinct from this security cleanup, and the task explicitly cautions
against scope creep. Filing a follow-up so the wire-up is not
forgotten when the cache lands in the decrypt hot path.

The HashMap<String, ChannelKey> that actually holds production keys
(crates/client/src/state.rs, state_actors.rs) is already cleaned up
correctly: leave_server removes the ServerEntry, dropping its keys
map and zeroizing each ChannelKey via ZeroizeOnDrop.

Verification:
- cargo test -p willow-crypto: 64 passed
- cargo clippy --workspace --all-targets -- -D warnings: clean
- cargo check --target wasm32-unknown-unknown -p willow-crypto -p
  willow-client: clean
- cargo fmt --check: clean

Refs #178

Co-authored-by: Claude <noreply@anthropic.com>
audit #327 listed 13 sites. count drifted to 10 (storage.rs and 2
others removed in earlier work). per-site breakdown:

deletes (genuinely unused):
- crates/web/src/app.rs::toggle_theme — never called; on_toggle_theme
  Callback wires through palette_actions, not this fn.

remove redundant annotation (symbol used):
- crates/web/src/palette_recents.rs::clear — used by browser tests.
- crates/web/tests/browser.rs::TestShell, mount_test_with_shell,
  ensure_components_css_loaded, mod test_support — all referenced
  throughout browser.rs flow tests.
- crates/web/src/state.rs::set_pinned_messages — used in state.rs:952.
- crates/web/src/state.rs::set_pin_labels — fed by struct init only;
  field access not warned because crate-wide #![allow(dead_code)] in
  willow-web/src/lib.rs already masks it. annotation was redundant.
- crates/web/src/components/mobile_shell.rs::MobilePush — match arms
  on Thread/Call/Onboarding count as use; no warning even when one
  variant is constructed.

keep with reason:
- crates/actor/src/fsm.rs::DoorState::Open — test fixture for
  StrictDoor rejection path; never constructed because tests start
  in Closed and assert further transitions are rejected. Kept with
  #[allow(dead_code, reason = ...)] (rust 1.81+).

verification: cargo check --workspace --all-targets clean; cargo
clippy -D warnings clean; cargo test --workspace passes; wasm32 lib
+ tests build clean.

scope-creep guard hit: removing willow-web's crate-wide
#![allow(dead_code)] surfaces 9 unrelated dead symbols across 5
files. left as-is — separate follow-up.

Refs #327

Co-authored-by: Claude <noreply@anthropic.com>
) (#471)

web crate had three types that name-clash with authoritative
willow_state and willow_client types:
- web::state::ServerState  vs willow_state::ServerState
- web::state::ChatState    vs willow_client::state::ChatState
- web::state::VoiceState   vs willow_client::state_actors::VoiceState

spec say willow_state::ServerState is THE single source of truth.
web sidecar holding Leptos signals shouldnt steal the name.

rename web sidecar types to *Signals suffix. signals is what they
hold (ReadSignal<...> fields), so name now describe content not
fight upstream. mechanical find-replace, no surrounding refactor.

callsites: 10 rename touches in 2 files (state.rs + call_page.rs
comment). browser tests had no references. authoritative
ServerState comments in settings.rs/holder_pill.rs/channel_sidebar.rs
left alone — they correctly point at willow_state::ServerState
fields (mute_state, channel_keys).

verification:
- cargo check -p willow-web --target wasm32-unknown-unknown: green
- cargo check -p willow-web --tests --target wasm32-unknown-unknown: green
- cargo fmt --check: clean
- cargo clippy --workspace --all-targets -- -D warnings: zero warnings
- cargo test --workspace: all green

runner-up: ServerViewSignals/etc — rejected, unnecessarily verbose
when *Signals already disambiguates from authoritative *State.

Refs #261

Co-authored-by: Claude <noreply@anthropic.com>
Hardcoded `stun:stun.l.google.com:19302` leaked every voice-call
participant's public IP to Google. Replace it with a configurable list
that defaults to empty — voice ICE now relies on host candidates plus
the iroh relay path, so no third-party server learns the user's IP
unless the deployment opts in.

Knob: `window.__WILLOW_STUN_URLS = ['stun:host:port', ...]`, mirroring
the existing `window.__WILLOW_RELAY_URL` override pattern in
`crates/web/init.js` / `app.rs::resolve_relay_url`. `init.js` ships the
override commented out alongside an example for restoring Google STUN
or pointing at a self-hosted server.

Brainstorm: chose the JS window global (Option A) over a build-time
env var (rebuild-required, less flexible) and over a query-string
override (exposed in URLs/logs, doesn't match the existing pattern).

Tests (`crates/web/tests/browser.rs::stun_config`):
  - default `resolve_stun_urls()` returns empty Vec
  - override populates the Vec
  - default `RTCConfiguration` has no `iceServers` entries
  - override surfaces the supplied URL in the config

Scope: short-term fix only. Self-hosted STUN (medium-term) and
iroh-relay-based ICE traversal (long-term) tracked under #179.

Refs #179

Co-authored-by: Claude <noreply@anthropic.com>
@intendednull intendednull merged commit cf359e2 into main Apr 28, 2026
7 checks passed
intendednull pushed a commit that referenced this pull request Apr 28, 2026
Resolves two conflicts:

1. crates/client/src/lib.rs — both branches added test modules.
   Kept all three: tests_actions (this batch, #420),
   tests_voice + tests_governance (PR #473, #414/#416).
   `cargo check -p willow-client --tests` clean post-resolution.

2. .claude/skills/resolving-issues/SKILL.md — both branches edited
   the skill. Resolved by full rewrite per human review feedback on
   PR #484: drop worktrees and sub-PRs entirely, work sequentially on
   the master branch directly (or via local feature branches that
   merge back into master without going through GitHub PRs).

Skill rewrite captures the new workflow shape:
- One master branch per session, all fixes land sequentially.
- No sub-PRs (they were causing merge confusion + silent CI skip
  when sub-PR base ≠ main).
- No worktrees (sequential = no isolation needed; also dodged the
  sandbox signing-service incompatibility documented in the prior
  skill version).
- Implementer commits directly to master branch (Pattern A) or via
  a local feature branch + `git merge --no-ff` back into master
  (Pattern B). Master PR opens at end of run with everything in it.

Preserved from the prior skill:
- Coordinator-never-codes rule (with two narrow exceptions:
  session-open commit + Lessons Learned skill edits).
- Complexity gate (auto-brainstorm + plan).
- Already-fixed-upstream + stale-audit-with-residual-gap paths.
- Caveman GH comms.
- Sequential between issues, fresh agent per issue, max 10 per run.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment