v0.7.0-alpha: identity layer with character + per-cwd auto-init + nickname addressing#26
Merged
Conversation
…kname addressing First user-visible cut of v0.7.0's identity-first vision (issues #24, #25). Each wire identity now has a deterministic Character (nickname + emoji + 256-color palette) derived from its DID via SHA-256; identities auto-create per cwd on `wire mcp` startup so every Claude tab in a fresh project gets its own real wire identity rather than collapsing onto the machine-wide default; sister sessions are addressable by character nickname OR session name. Three rounds of fixes shipped together — see CHANGELOG for the full alpha.1/alpha.2/alpha.3 breakdown. Stress tests caught three real bugs along the way, all patched and verified: - Same-cwd race: 5 parallel `wire mcp` startups in the same fresh cwd used to race the `is_initialized()` check inside `wire init`, each overwriting the agent-card. Patched with a name-scoped fs2 flock on `<sessions_root>/.auto-init.<name>.lock` that re-checks agent-card existence post-lock and adopts the winner's identity. - Registry write race: 20 parallel auto-inits across different cwds used to lose ~3 registry entries because `write_registry` was read-modify-write without a lock. Added `session::update_registry(modifier)` mirroring `config::update_relay_state`; auto-init now uses it. - Ambiguous nicknames: when two sessions share a nickname (e.g. one auto-derived, one operator-renamed), `resolve_local_session` used to silently pick alphabetical-first. Now returns a structured `ResolveError::Ambiguous(candidates)` and `wire add` surfaces a disambiguation hint with the candidate session names. Plus identity overrides ("let them name themselves") via `wire identity rename --name X --emoji Y` persisted to a `display.json` sidecar; palette stays DID-derived so the visual color identity remains stable across nickname changes. `wire whoami --short` and `--colored` now append cwd display ("🐅 winter-bay · ~/Source/wire") for multi-window operator disambiguation. 176 unit tests pass. Stress tests 1-7 documented in conversation transcript. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: 🐅 winter-bay <wire+wire-b6f47edb@wire.id>
Expanded curated lists to reduce nickname-triple collisions at scale and give every Claude session a fresher, more evocative name. Combo space grows from 921k to 8.64M; empirical collision rate at 100k DIDs drops from 5.24% to 0.59%. - adjectives: 120 → 243 (textures, moods, light, weather) - nouns: 120 → 242 (flora, fauna, landscape, weather, light) - emojis: 64 → 147 (mammals, birds, reptiles, sea, bugs, fruits, plants, music, abstract — all single-codepoint terminal-stable, no flags / ZWJ / skin tones) Behavior change to flag: characters stay deterministic per DID, but the function from DID to character changed because list lengths shifted. Sessions that already exist will render different characters on next `wire whoami`. Operators wanting to preserve old names can write `display.json` overrides pinning the alpha.1 character before upgrading; the operator's chosen override always wins over the auto-derived value. 176 unit tests pass. Demo at `cargo run --example character_demo`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: 🛡 noble-creek <wire+wire-b6f47edb@wire.id>
Live-tested send-by-nickname end-to-end (resolver was wired in alpha.2
but only exercised through `wire add`). Resolution works; surface line
fires correctly ("wire send: resolved nickname `noble-creek` → peer
`wire`"). Verified message lands in outbox; daemon delivery best-effort
as before.
Patched `resolve_peer_handle` to mirror `resolve_local_session`'s
`ResolveError::Ambiguous(candidates)` contract — defensive against the
(rare but possible) DID-hash collision in the adj-noun nickname space.
Single-match unchanged; only previously-silent multi-match case now
errors with a disambiguation hint listing candidate peer handles. Pass-
through for unknown handles preserved (matches existing `wire send`
queue-first-deliver-best-effort semantics).
176 unit tests pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: 🛡 noble-creek <wire+wire-b6f47edb@wire.id>
Deploying wireup-landing with
|
| Latest commit: |
488e0fc
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://93cb07cb.wireup-landing.pages.dev |
| Branch Preview URL: | https://v0-7-0-identity.wireup-landing.pages.dev |
When the operator (or agent) runs `wire identity rename --name X --emoji Y`, we now ALSO update the agent-card with a new `display` field carrying the chosen nickname + emoji, re-sign with the session's private key, and write back. The override is no longer purely local — peers that pair with us afterward see what we call ourselves. Reader side: `Character::from_card(agent_card)` prefers published display.nickname/emoji when present, falls back to DID-hash auto- derived. `cmd_peers` + `resolve_peer_handle` use it so `wire peers` shows peer-chosen names and `wire send <chosen-name>` works. Backward compat: agent-cards without the `display` field land in the auto-derived path automatically. Older wire clients ignore the unknown field. Honest limitation surfaced in the rename CLI: existing pinned peers cache the agent-card at pair time. They won't see the new character until they re-pair (or pull a fresh card via a future refresh command). The stderr line now says so explicitly. 176 unit tests pass. Live-verified that rename writes display to agent-card; re-pair flow refreshes the cached card on the peer side. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: 🛡 noble-creek <wire+wire-b6f47edb@wire.id>
Per persona review (plain-language visitor, marketing strategist, brand/voice keeper, skeptical engineer, existing user, Claude Code operator): drop tech jargon from above-the-fold, restore switchboard voice in bullets, lead with the face/visual hook, put GitHub as a prominent CTA, add one concrete use case, preserve "two friends. two agents." + the 1960s exchange metaphor + the operator-tag ribbon. **README.md (front matter rewrite):** - New title: "agent-to-agent comms, no vendor in the middle" - Three-up CTA row: GitHub primary, install command, demo, discord - New "What wire is" section: phone-line metaphor → bulleted features with restored switchboard voice (face, phone number, switchboard can't listen in, bilateral by default, MCP-native) - One concrete use case (Claude babysitting training pings reviewer Claude when done) - "Get it" install one-liner promoted from buried in quickstart - "Where to go next" link block: github front-door, demo, AGENT.md, docs/, multi-Claude section - Status section retained but compressed: v0.7.0-alpha summary + v0.6 line summary + A2A compat note as blockquote (per "keep but smaller") - Tagline preserved verbatim (per operator instruction) **landing/index.html (text rewrite in existing HTML frame):** - Stamps simplified: v0.7.0-alpha · open source · mcp-native · by slancha - New primary CTA row under stamps: 3 buttons (github front-door, install one-liner, watch demo) — github first as front door - A2A banner: dropped from front-of-page; one-line callout retained with link to A2A_INTEROP doc (per "keep but smaller") - § 01 prose: rewritten plain-language, restored "Operators own the line" + the 1960s exchange metaphor + bigger character emphasis - § 02 cards: simpler install + claim prose, points at github.com directly, drops the three-layer-identity tech paragraph - § 02 closing: focused on "no URL, no SAS, no turn-taking" with the SPAKE2+SAS escape hatch named once, not foregrounded - Tagline preserved verbatim (per operator instruction) - All visual design (burgundy frame, parchment, terminal phosphor, paper-tag dial, operator-tag side ribbon) untouched **landing/demo.cast (re-recorded):** - New 22-second cast against live https://wireup.net - Story: alice claims handle → wire gives her a face → bob does same → bob calls alice → alice accepts (FIX: old cast was missing pair-accept and showed "rejected=1" — was lying about completion) → bob sends to alice BY HER CHARACTER NICKNAME, not handle - Money shot at 17.6s: `wire send yonder-redwood "..."` → `resolved nickname "yonder-redwood" → peer "alice"` — the v0.7 magic - Cast file regenerated via asciinema rec --output-format asciicast-v2 **landing/demo.sh (script):** - Updated to current v0.7-alpha flow: shows wire whoami --short per side, adds explicit wire pair-accept step (was missing — old script silently failed bilateral gate), reads peer's character nickname dynamically and uses it in the marquee send line, cwd suffix stripped from whoami output for cleaner display Site doesn't auto-deploy from this commit — landing HTML is compile- time embedded into the relay binary, so wireup.net updates on next relay redeploy after merge. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: 🛡 noble-creek <wire+wire-b6f47edb@wire.id>
…istency + defensive) Self-review of PR #26 via 5-angle parallel finder surfaced 15 findings; this commit patches the 4 most critical that don't require new architectural surface: **Review-fix #1 — SECURITY (character.rs):** terminal-escape injection via peer-controlled display.nickname / display.emoji. A malicious paired peer could sign an agent-card with display.nickname='\x1b]0;OWNED\x07' (or any OSC/CSI/ANSI sequence), and our colored() render would interpolate it raw → terminal executes the escape on every `wire peers` / `wire whoami` / `wire session list` invocation (window-title rewrite, screen clear, OSC 8 hyperlink spoofing). Auto-derived path was safe (curated word list), override path stripped the guarantee. Fix: `sanitize_display_text(s)` strips Unicode Control category chars (c.is_control() — covers C0 + DEL + C1, including ESC U+001B which gates all ANSI sequences) and caps to MAX_DISPLAY_CHARS=64 codepoints. - Write-time enforcement: `wire identity rename` bails if input fully reduces to empty after stripping (operator typed only control chars); prints stderr warning if any chars were stripped. - Read-time enforcement: `Character::from_card` silently sanitizes peer- published display fields (defense-in-depth against cards signed before this validation shipped, OR against signed-but-malicious peers). **Review-fix #3 — INCONSISTENCY (cli.rs cmd_peers):** the text-mode output loop called Character::from_did (auto-derived, ignored peer override). The --json branch called Character::from_card (honored override). Same data, different display per flag. Operator looking at `wire peers` to find the nickname to type into `wire send` would see the wrong name (auto-derived) while resolve_peer_handle was matching the chosen override. Fix: text loop now reads the already-computed character (set above via from_card with peer's full agent-card) instead of recomputing from DID. Single source of truth, text + JSON agree. **Review-fix #7 — RACE (session.rs write_registry + config.rs):** write_registry used std::fs::write which truncates the file FIRST, then writes the new contents — concurrent unflocked readers (and there are several: detect_session_wire_home, list_sessions, cmd_peers) could observe a 0-byte file mid-write, fail serde parse, fall back to default identity for the write window. update_registry's doc comment claimed "writes atomically" but the implementation didn't. Fix: swap fs::write → write tmp + rename. POSIX rename is atomic, so readers see either the old complete file or the new complete file, never a torn / partial / zero-byte view. Same fix applied to: - session::write_registry - config::write_agent_card (made hot by cmd_identity_rename — power- loss mid-write previously corrupted the entire card) - config::write_display_overrides (consistency with agent-card on the rename two-phase write) **Review-fix #8 — WRAPPER (character.rs from_card):** with a missing / null / non-string `did` field, Character::from_card fell through to from_did("") → SHA-256 of empty bytes → deterministic single character shared across all such peers. Pinned cards on disk that are partially corrupt (e.g. from older wire versions or partial-write code paths) silently collapsed into one fake identity. Fix: `Character::unknown_peer()` sentinel — distinctive "❓ unknown-peer" in gray (ANSI 244), explicitly outside the curated EMOJIS list. Pinned peers with broken cards now show clearly broken instead of masquerading as a single fake winter-bay-class identity. **New tests (+8, total 184):** - sanitize_strips_ansi_escape — verifies ESC + BEL removed - sanitize_preserves_unicode_emoji_and_text — Unicode preserved - sanitize_caps_length — MAX_DISPLAY_CHARS enforced - from_card_with_empty_did_returns_unknown_sentinel - from_card_with_null_did_returns_unknown_sentinel - from_card_strips_escape_from_published_nickname — defense-in-depth - from_card_with_published_override_uses_it — happy path - from_card_without_display_falls_back_to_did — backward compat **Deferred to follow-up commits (logged as tasks #134, #135):** - #2 cmd_identity_rename re-signs the LOCAL agent-card with the new display field but never re-publishes to the relay — federation peers fetching .well-known/wire/agent see the OLD card. The alpha.6 "now peers see what we call ourselves" framing is local-only despite the copy. Needs a relay card-update endpoint OR re-claim flow. - #4 auto-init derives session name from a registry read OUTSIDE the flock — two cwds with same basename can collide on name even with the flock guarding init. Move derive_name_from_cwd inside the lock. 176 → 184 unit tests pass. No regressions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: 🛡 noble-creek <wire+wire-b6f47edb@wire.id>
…ocal + Federation Use case from operator: noble-creek on paul-mac wants to talk to running-light on spark over Wi-Fi without round-tripping wireup.net. v0.5.17 only had Local (loopback) and Federation (public). This adds Lan — same-network across machines, sub-10ms vs ~50–300ms federation. **EndpointScope::Lan** added to src/endpoints.rs with Endpoint::lan() constructor. Routing priority in peer_endpoints_in_priority_order: - Local (loopback, our_local matches) → 0 - Lan (cross-machine same-network) → 1 - Federation (anywhere) → 2 Lan endpoints are kept in routing without the "our_local matches" gate — cross-machine peers won't have matching loopback URLs. pin_peer_endpoints prefers Lan over Local for the legacy relay_url/slot_id/slot_token fields when no Federation present (LAN is cross-machine-reachable; loopback isn't). **CLI flags on `wire session new`:** - `--with-lan` — opt-in to LAN endpoint allocation - `--lan-relay <url>` — operator-typed LAN-reachable relay URL (no auto-detect today; required when --with-lan is set) **try_allocate_lan_slot** parallel to try_allocate_local_slot: probes healthz, allocates a slot at the LAN URL, merges into self.endpoints[] alongside any existing Federation/Local entries. Non-fatal on probe/alloc failure — session stays at existing endpoint mix, operator can retry. **Identity continuity across scopes** (closes task #129): same DID + keypair + character across Local/Lan/Federation. Adding or removing an endpoint never changes the DID, so 🐅 winter-bay stays 🐅 winter-bay whether reachable via loopback, LAN, or wireup.net. Character is DID-derived, so endpoint changes are display-invariant. **Operator flow (the noble-creek ↔ running-light case):** ```bash # paul-mac (LAN IP 192.168.1.50): wire relay-server --bind 192.168.1.50:8771 --local-only & wire session new --with-local --with-lan --lan-relay http://192.168.1.50:8771 # spark (LAN IP 192.168.1.42): wire relay-server --bind 192.168.1.42:8771 --local-only & wire session new --with-local --with-lan --lan-relay http://192.168.1.42:8771 # pair as normal — federation resolves the handle, traffic prefers Lan wire add running-light@spark.local ``` **Deferred to followups** (operator-confirmed v2 surface): - mDNS/Bonjour discovery (today: operator types LAN URL) - LAN-IP auto-detect (today: operator types the address) - Roaming heartbeat / wire session refresh-lan (today: re-run --with-lan --lan-relay <new-url> after IP change) - LAN-only mode (today: federation handle resolution still required for the pair flow) **Match-arm exhaustiveness:** all of cli.rs's EndpointScope match sites updated (endpoint_cursor_key, cmd_add_local_sister scope display, cmd_send delivery scope x2, pull/scope mapping at 7475). last_pulled_event_id_lan cursor key separates LAN reads from loopback Local reads. 184/184 unit tests pass. Linux ARM64 verified on Spark earlier this session (cargo test --lib on /tmp/wire-v07-test). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: 🛡 noble-creek <wire+wire-b6f47edb@wire.id>
Added 3 unit tests locking in the v0.7.0-alpha.9 LAN endpoint routing contract (10 total endpoint tests; full suite 187 passing): - peer_endpoints_lan_beats_federation — peer publishes Local + Lan + Federation, we have matching Local + Federation: priority must be Local(matched) > Lan > Federation - peer_endpoints_lan_kept_when_self_has_no_local — cross-machine peer case: we have only Federation, peer publishes Federation + Lan; Lan must still be kept and preferred (we connect TO their LAN; symmetric Local-of-our-own isn't required) - pin_peer_endpoints_uses_lan_as_legacy_when_no_federation — backward compat: when peer has only Lan + Local (no Federation), the legacy top-level relay_url fields prefer Lan over Local (LAN is cross- machine reachable, loopback isn't) End-to-end stress on this Mac: - 2 sessions w/ --with-local --with-lan against http://127.0.0.1:8771 - Both correctly allocated separate Local + LAN slots - Pair via local-sister: sess-b's pinned trust state for sess-a shows BOTH local + lan endpoints (Lan tag propagated through pair_drop) - pair_drop_ack body carries the full endpoints[] array with scope=lan Adversarial stress on alpha.8 sanitize: - OSC 0 (window title rewrite) injection: ESC + BEL stripped at render - CSI 2J (screen clear) injection: ESC stripped - Pure control-char input: rename CLI bails "empty after stripping" - 1024-char nickname: capped at MAX_DISPLAY_CHARS=64 Peer-card injection (direct trust.json write of malicious display.*): verified Character::from_card sanitizes on read so even pre-validation cards in the wild can't escape-inject. 187 tests pass on macOS arm64; need to re-verify on Spark (Linux ARM64 build was alpha.8; will re-build alpha.10 in a follow-up). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: 🛡 noble-creek <wire+wire-b6f47edb@wire.id>
Surfaced during cross-machine LAN stress test of alpha.9: starting a
LAN-bound relay for the new `--with-lan --lan-relay <url>` flow
required `wire relay-server --bind 192.168.0.x:8772`, but
`--local-only` rejected anything outside 127/8 or ::1. The v0.5.17
gate was right to refuse PUBLIC binds (would expose a "local-only"
relay to the global internet) but too strict for LAN-only relays
that the new alpha.9 LAN feature actively needs.
Relaxed `validate_loopback_bind` to accept loopback OR RFC 1918
private IPv4:
- 10/8
- 172.16/12
- 192.168/16
- 127/8 (loopback, unchanged)
- ::1 (loopback IPv6, unchanged)
Still refuses:
- 0.0.0.0 / :: (wildcards — could land on public iface)
- Public IPv4 / IPv6
- Anything outside the private-or-loopback set
Same `--local-only` semantics otherwise (no phonebook listing, no
.well-known/wire/agent serve). Operator now runs:
wire relay-server --bind 192.168.0.47:8772 --local-only &
wire session new --with-local --with-lan --lan-relay http://192.168.0.47:8772
and a same-LAN peer can reach the slot without going through
wireup.net.
IPv6 ULA (fc00::/7) detection deferred — would need either a feature-
flagged Ipv6Addr::is_unique_local() or a manual segments[0] & 0xfe00
check. Today IPv6 still requires loopback explicitly. Logged as a
followup if anyone hits it.
187 tests pass. No new tests added (the function is dead-trivial to
verify by hand; the bind-side test is the live LAN relay).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: 🛡 noble-creek <wire+wire-b6f47edb@wire.id>
…#135) Two `wire mcp` processes in DIFFERENT cwds with the SAME basename (e.g. /a/projx + /b/projx) could collide on derived name. The alpha.3 per-name flock didn't protect — they queued on different locks (.auto-init.projx.lock for both, but the race was OUTSIDE the lock at registry-read time). Live regression test on my Mac caught it: both cwds ended up with name "projx", both mapped to the same identity — the exact "every Claude tab gets its own identity" failure mode the PR docstring claims to fix. **Fix 1 — single global auto-init lock instead of per-name:** `.auto-init.lock` (was `.auto-init.<name>.lock`). All auto-init across the sessions_root now serializes through one lock. Different cwds no longer race to derive name. Cost: parallel auto-inits in different cwds serialize at ~hundreds of ms each; acceptable because auto-init runs once per cwd per machine, not a hot path. **Fix 2 — release lock AFTER `update_registry`:** Pre-fix released lock at line 6962 then committed registry at 6971. A racing process that just acquired the lock at 6891 would read the registry (still without the prior winner's entry) and derive the SAME name. Now registry update happens inside the lock; release fires after. Next racer's read_registry sees the prior winner's entry, derive_name_from_cwd correctly adds path-hash suffix. **Live regression verification:** ``` $ (cd /tmp/a/projx && wire mcp &) ; (cd /tmp/b/projx && wire mcp &) [ ... ] $ wire session list | grep projx projx /private/tmp/a/projx projx-e12d /private/tmp/b/projx ← unique ``` Two distinct cwds → two distinct sessions/DIDs/identities. 187 tests pass. Closes review-fix #135 from PR #26's stress-test batch (review-fix #134 — federation publish — still pending, separate work). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: 🛡 noble-creek <wire+wire-b6f47edb@wire.id>
…relay (review fix #134)
Closes the alpha.6 "federation publishing" honesty gap. Pre-fix
re-signed the local agent-card.json with the new display field, but
never pushed the updated card to the federation relay. Federated
peers resolving the handle via .well-known/wire/agent fetched the
RELAY'S stored copy — which still had the pre-rename card. Operator
saw "🦊 foxtrot-meadow" locally; peers saw the auto-derived "🐅
winter-bay". The user-facing message ("published to your agent-card;
NEW pairs see this character") was misleading.
**Fix:** after re-signing locally, also call
`relay_client.handle_claim_v2(nick, slot_id, slot_token, ..., signed_card, ...)`
to update the relay's stored copy. Reuses the existing federation
publication path (same call `wire claim` makes for the initial
publish). Best-effort — relay errors log to stderr but don't bail;
the local rename is still useful even if the relay can't be reached
right now.
**Skip cases:**
- Session not bound to any federation relay (e.g. local-only via
`wire session new --local-only`): skip silently
- Bound to loopback or LAN relay (no public phonebook): skip
- handle_claim_v2 returns non-2xx (e.g. token rejected, relay down):
log and proceed
- relay_state malformed (missing slot_id/slot_token): skip
**Updated stderr message:**
- Pre-fix: "published to your agent-card; NEW pairs see this character.
Existing pinned peers have a cached card from pair-time — they will
only see the new name after they re-pair with you (`wire add
--local-sister <yourname>` from their side)."
- Post-fix: "re-published to your federation relay (if bound); future
federation lookups serve the updated card. Existing pinned peers
have a cached card from pair-time and won't see the new name until
they re-pair OR fetch your card fresh."
The "fetch your card fresh" hint points at future work — operators
could refresh pinned peer cards via a future `wire peer refresh
<name>` command (today: re-pair).
187 tests pass. Closes review-fix #134; all 4 followup gaps from
the alpha.8 high-effort code review (#1, #3, #7, #8 fixed inline;
#2 here; #4 fixed in alpha.12) are now patched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: 🛡 noble-creek <wire+wire-b6f47edb@wire.id>
First slice of the v0.7+ identity-first vision's noun-CLI surface (task #141). Operator-facing commands grouped under `wire identity` that previously lived under verb-style aliases (claim, session list/destroy): - `wire identity list` — all identities on this machine (alias for `wire session list`, but the right name for the noun-CLI) - `wire identity publish <nick> [--relay <url>] [--public <url>] [--hidden]` — promote local identity to federation lifecycle: claim a handle on the relay so peers can `wire add <nick>@<domain>`. After v0.7.0-alpha.13 the rename flow auto-republishes; this is the EXPLICIT publication command for first-time federation publishing. - `wire identity destroy <name> [--force]` — alias for `wire session destroy`, scoped here for the noun-CLI surface Deferred from this commit: - `wire identity create [--anonymous|--local]` — requires anonymous identity mode first (in-memory-only keypair, no on-disk persistence until explicit persist). Anonymous lifecycle TODO. - `wire identity persist <name>` — anon → local. Same blocker. - `wire identity demote <name>` — federation → local OR local → anon. Requires selective endpoint removal logic + safety prompts. The "rename" + "show" subcommands already existed (alpha.3/alpha.6); this commit just rounds out the lifecycle verbs operators actually need today. Cargo.toml stays at 0.6.10 per the alpha lineage convention — bumps to 0.7.0 when the full lifecycle (including anonymous mode) ships. 187 tests pass. Closes part of task #141 (the surface area achievable without new architectural work); full lifecycle remains open. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: 🛡 noble-creek <wire+wire-b6f47edb@wire.id>
…et IPs) The alpha.11 patch let `--local-only` bind to RFC 1918 private LANs (10/8, 172.16/12, 192.168/16). This extends the allowlist to RFC 6598 CGNAT (100.64.0.0/10) — the range Tailscale uses for tailnet addresses. **Tailscale-piggyback recipe:** ```bash # Mac (tailnet 100.96.234.16): wire relay-server --bind 100.96.234.16:8772 --local-only & wire session new --with-local --with-lan --lan-relay http://100.96.234.16:8772 # Spark (tailnet 100.91.57.17): wire relay-server --bind 100.91.57.17:8772 --local-only & wire session new --with-local --with-lan --lan-relay http://100.91.57.17:8772 # Pair via federation (handle resolution still goes through wireup.net), # traffic prefers Lan endpoint, which routes over Tailscale's utun. ``` Tailscale gives: stable IPs across machines, end-to-end WireGuard encryption, NAT-traversal, no port forwarding. Wire gives: protocol + identity + character + bilateral consent. Clean separation; the tailnet IP just looks like a LAN address to wire's routing layer. **macOS operators: one-time sudo for ALF.** During cross-machine stress-test we discovered macOS Application Firewall blocks all non-loopback incoming TCP regardless of `socketfilterfw --getappblocked` saying "permitted" — that check is misleading without an explicit `--add`. To unblock: sudo /usr/libexec/ApplicationFirewall/socketfilterfw --add \ ~/.cargo/bin/wire sudo /usr/libexec/ApplicationFirewall/socketfilterfw --unblockapp \ ~/.cargo/bin/wire After that, cross-machine Tailscale wire works in both directions. Linux Spark side: no firewall configuration needed; ufw/iptables on most desktop distros allow per-port already, and tailscaled-tied incoming via utun is generally open. **Why not bake the sudo into wire?** Wire shouldn't fork sudo. The operator already knows their firewall posture; we document and let them choose. Still refuses: public IPv4 / IPv6, wildcards (0.0.0.0/::), link-local, multicast. Those would publish a "local-only" relay to the open internet — defeats the gate. 187 tests pass. No new tests added (the function is dead-trivial to read; the test surface is the live wire relay-server bind). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: 🛡 noble-creek <wire+wire-b6f47edb@wire.id>
…S prior art Three-slice parallel slancha-delegate research on the cross-machine wire connectivity problem. Full findings at .planning/research/transport-substrate-2026-05-23.md. Synthesis pinned to memory at project_wire_transport_substrate_research.md. **Headline (load-bearing for any future wire-on-macOS work):** GUI Tailscale.app on macOS uses a Network/System Extension running a USERSPACE netstack (gVisor). Inbound peer packets terminate INSIDE the extension; they never reach a raw host socket bound to the 100.x tailnet IP. tcpdump on utun8 shows ZERO inbound packets when peers attempt TCP — Tailscale's own disco protocol still works (responder lives in the extension) but raw bind is invisible. Fix is either `tailscale serve --bg --tcp=<port> tcp://localhost:<port>` or switching to brew `tailscaled` (kernel utun, no userspace netstack). This explains the bidirectional connectivity gap we hit when LAN-stress testing v0.7.0-alpha.9/.11/.15. Wire's CGNAT bind acceptance (alpha.15) is correct; the macOS Tailscale variant is the actual gate. **UDS verdict (research convergence):** Don't add for SPEED — 1.3µs win over loopback is invisible behind wire's crypto + poll-loop. Add for SECURITY: no bound TCP port + SO_PEERCRED kernel-attested peer uid = same-uid-only trust anchor for sister sessions. Worth shipping as v0.7.1 if the framing matches; skip if not. **Cross-project pattern (Slice B):** `tailscale serve --tcp` in front of localhost-bound service is Tailscale's official recommended pattern — no sudo, free TLS, ACL applied. Most-envious feature seen: NetBird's extra DNS labels for service discovery. **Net recommendation:** Don't add Tailscale-specific code to wire. Operators choose their substrate; wire stays transport-agnostic. Document the macOS GUI Tailscale gotcha as operator-side OS-config tail. Sources cited inline with trust-prior tags [P/S/T, source, score]. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: 🛡 noble-creek <wire+wire-b6f47edb@wire.id>
…esearch) First half of the v0.7.1 UDS work — the SERVER side. Adds: **`EndpointScope::Uds`** in endpoints.rs alongside Local/Lan/Federation. Routing priority: `Uds > Local(matched) > Lan > Federation`. Same-host trust anchor; `relay_url` carries `unix:///path/to/local.sock`. **`wire relay-server --uds <path>`** binds a Unix Domain Socket instead of TCP. Chmods 0600 (owner-only). Implies --local-only semantics (no phonebook, no .well-known). Removes stale socket on start + on graceful shutdown. Implementation note: axum 0.7's `serve()` is TcpListener-only, so the UDS path uses a manual hyper accept loop with per-connection `hyper::server::conn::http1::Builder`. When axum 0.8 ships with generic `Listener` support, this collapses to one line. **Verified live:** ``` $ wire relay-server --uds /tmp/wire-uds.sock wire relay-server (UDS) listening on unix:///tmp/wire-uds.sock — same-host, owner-uid only $ ls -la /tmp/wire-uds.sock srw------- ... /tmp/wire-uds.sock ← mode 0600 confirmed $ curl --unix-socket /tmp/wire-uds.sock http://localhost/healthz ok ``` **Why UDS — framing follows the alpha.15 research:** NOT for speed (loopback HTTP ~3.6µs vs UDS ~2.3µs round-trip is invisible behind wire's crypto + poll-loop). UDS is the **sister-session security boundary**: - No bound TCP port → kills the macOS firewall / userspace-netstack class of issues entirely for same-host use - 0600 socket + kernel-attested peer uid (SO_PEERCRED equivalent) → same-uid-only by construction. Loopback :8771 is reachable by any local process holding the slot token; UDS is not. **New crate deps** (transitive before, now explicit): - hyper 1 (server, http1 features) - hyper-util 0.1 (server, server-auto, tokio) - tower-service 0.3 **What's NOT in this commit (deferred to alpha.17):** - Client-side UDS support (relay_client needs hand-rolled HTTP/1.1 over UnixStream, or hyperlocal — reqwest has no UDS path) - `wire session new --with-uds` flag - `try_allocate_uds_slot` helper - SO_PEERCRED check on accept (today's 0600 is the only check; uid enforcement at app layer is alpha.18 polish) 187 tests pass. EndpointScope::Uds routing logic + scope variant tested via the existing endpoint priority tests (Uds rank 0 verified by inspection; live integration test will land with the client side). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: 🛡 noble-creek <wire+wire-b6f47edb@wire.id>
Second half of the UDS work — the CLIENT side. Adds `uds_request()`:
a minimal blocking HTTP/1.1 client over Unix Domain Socket for callers
that detect a `unix://` scheme on a peer endpoint URL.
Why hand-rolled (not hyperlocal or similar):
- reqwest has no UDS support
- hyperlocal would add another async dep + bridging
- wire's UDS shape is dead-simple: single POST/GET per call, JSON
in + JSON out, small payloads, no keep-alive, no streaming
- The cost is ~70 lines of straight HTTP/1.1 text writing + parsing —
cheaper than the dep + async-runtime cost
Interface mirrors what `RelayClient::post_event` / `allocate_slot`
need without the reqwest abstraction:
```rust
fn uds_request(
socket_path: &Path,
method: &str,
request_target: &str,
headers: &[(&str, &str)],
body: &[u8],
) -> Result<(u16, Vec<u8>)>;
```
Returns `(status_code, body_bytes)`. Caller decodes body per the
endpoint's content type (`serde_json::from_slice(&body)` typically).
**Three unit tests covering the round-trip** (all pass):
- `uds_request_round_trips_200_with_body` — full POST → 200 OK + body
- `uds_request_surfaces_non_2xx_status` — 4xx surfaces with body so
callers can extract error detail (mirrors reqwest's 4xx-is-not-error
behavior — caller checks status)
- `uds_request_fails_on_nonexistent_socket` — clear error message
pointing at the socket path
Tests use an in-process `UnixListener` returning a canned response —
no dependency on the wire relay binary in test fixtures.
**Live verification (alpha.16 server still works):**
```
$ wire relay-server --uds /tmp/wire-uds.sock &
$ curl --unix-socket /tmp/wire-uds.sock http://localhost/healthz
ok
```
The alpha.17 client now provides programmatic access to the same socket.
**What's NOT in this commit (deferred to alpha.18):**
- `wire session new --with-uds` flag — operator-facing CLI to allocate
a UDS slot at session create time
- `try_allocate_uds_slot` helper using `uds_request` to talk to a UDS
relay's `/v1/slot/allocate` endpoint
- Routing layer: detect `unix://` peer endpoint URL in `cmd_send` /
`cmd_push` and route via `uds_request` instead of reqwest
- Daemon's pull loop UDS support
3 new tests, total 190 lib tests passing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: 🛡 noble-creek <wire+wire-b6f47edb@wire.id>
Third piece of the UDS work (after alpha.16 server + alpha.17 client).
Operator-facing CLI:
```bash
wire relay-server --uds /tmp/wire.sock &
wire session new --with-uds --uds-socket /tmp/wire.sock
```
`try_allocate_uds_slot` mirrors `try_allocate_local_slot` /
`try_allocate_lan_slot` but uses the alpha.17 hand-rolled
`uds_request` HTTP/1.1 client (reqwest has no UDS support):
- Probes healthz first (clear stderr if socket doesn't exist OR isn't
a wire relay)
- POST /v1/slot/allocate
- Parses AllocateResponse from JSON
- Pushes a `unix://<socket-path>` Uds endpoint into the session's
relay.json self.endpoints[]
End-to-end verified live:
```
$ wire session new sess-uds-test --with-local --local-only \
--with-uds --uds-socket /tmp/wire-uds.sock --no-daemon
wire session new: local slot allocated on http://127.0.0.1:8771 (slot_id=ddd9ebc4...)
wire session new: UDS slot allocated on unix:///tmp/wire-uds.sock (slot_id=dd285d42...)
→ relay.json:
local http://127.0.0.1:8771
uds unix:///tmp/wire-uds.sock
```
Both scopes coexist on a single session. Paired peers receive the
endpoint set via the pair_drop body's endpoints[]; alpha.19 (next
commit) wires `cmd_send` to detect `unix://` scheme and route via
uds_request instead of reqwest, so messages actually flow over UDS.
Unix-only — Windows refuses with a stderr line. AF_UNIX is available
in Win 10 1803+ but tokio + reqwest don't expose it.
190 tests pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: 🛡 noble-creek <wire+wire-b6f47edb@wire.id>
Closes the UDS arc (alpha.16 server + alpha.17 client + alpha.18 CLI
allocation + alpha.19 this commit = routing). Wire send now actually
delivers via UDS when peer's pinned endpoint has `unix://` scheme.
**`post_event_to_endpoint(endpoint, event)`** in relay_client.rs:
scheme-aware dispatch helper. Detects `unix://<path>` and routes via
the alpha.17 `uds_request` HTTP/1.1 client; everything else goes via
the existing `RelayClient::post_event` (reqwest). Single API for
callers; no more "detect + branch" boilerplate at each send site.
Updated two routing call sites in cli.rs to use the helper:
- cli.rs:5925 (cmd_send single-peer push loop)
- cli.rs:6340 (cmd_send parallel multi-peer broadcast loop)
Other RelayClient::new + post_event sites left untouched (federation-
only paths: handle_claim, pair-host, etc. — they never see UDS peers).
**End-to-end verified live:**
```
$ wire relay-server --uds /tmp/wire-uds.sock &
$ wire session new sess-a --with-local --local-only --with-uds --uds-socket /tmp/wire-uds.sock
$ wire session new sess-b --with-local --local-only --with-uds --uds-socket /tmp/wire-uds.sock
$ wire add --local-sister sess-b # from sess-a's home
$ wire pair-accept sess-a # from sess-b's home
→ bilateral pair complete
$ cat sess-a/config/wire/relay.json | jq '.peers["sess-b"].endpoints'
[
{"scope": "local", "relay_url": "http://127.0.0.1:8771", ...},
{"scope": "uds", "relay_url": "unix:///tmp/wire-uds.sock", ...}
]
$ wire send sess-b "uds end-to-end stress test" # queued
$ wire daemon --once # pushes via priority-ordered endpoints
$ wire daemon --once # (sess-b)
$ tail -1 sess-b/state/wire/inbox/sess-a.jsonl
{kind: 1001, body: "uds end-to-end stress test", ...}
```
Daemon walked sess-b's priority list: `Uds > Local > Lan > Federation`
(per endpoints.rs:107). UDS is rank 0; daemon's first attempt was UDS.
**Closes task #140 — v0.7.1 UDS transport.**
Full UDS arc summary across 4 commits:
- alpha.16: EndpointScope::Uds + relay-server --uds (manual hyper accept
loop because axum 0.7 serve is TcpListener-only)
- alpha.17: uds_request hand-rolled HTTP/1.1 client over UnixStream
- alpha.18: wire session new --with-uds + try_allocate_uds_slot
- alpha.19: scheme-aware routing in send paths (this commit)
190 tests pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: 🛡 noble-creek <wire+wire-b6f47edb@wire.id>
…loses task #141)
Closes the v0.7+ noun-CLI surface. Operators now have explicit
lifecycle verbs alongside the legacy implicit init/claim pattern.
**`wire identity create [--anonymous|--local] [--name <name>]`** —
explicit identity creation in a chosen lifecycle state.
- `--anonymous`: per-invocation tmpdir (e.g. /var/folders/.../wire-anon-XXX
on macOS, /tmp/wire-anon-XXX on Linux). Identity lives there until
reboot clears tmpfs. Operator gets a `WIRE_HOME=...` export to
activate the identity in their shell. Pragmatic shape ("tmpdir +
sentinel") rather than pure in-memory — the codebase's config layer
is FS-backed; full RAM-only is v1.0 vision territory.
- `--local`: explicit form of today's default (machine-persistent,
no federation). Defaults to this when --anonymous is absent.
**`wire identity persist <name> [--as <new-name>]`** — promote anon → local.
Scans tmpdir for `wire-anon-*/anon-marker.json` matching the name,
moves the session dir to the persistent sessions root, registers in
the cwd map. After persist the identity survives reboot.
**`wire identity demote <name>`** — federation → local. Strips relay
slot binding (top-level relay_url/slot_id/slot_token + federation-scope
entries from endpoints[]). Keeps keypair + agent-card so re-publish
via `wire identity publish <nick>` is one command later. local →
anonymous NOT exposed — destroy + recreate is the safer step-down
for that direction (no surprising "where did my keypair go" semantics).
**End-to-end verified live:**
```
$ wire identity create --anonymous --name scratch-test --json
{"kind": "anonymous", "name": "scratch-test", "did": "did:wire:scratch-test-...",
"session_home": "/var/folders/.../wire-anon-0fd69d39/sessions/scratch-test"}
$ wire identity persist scratch-test --as persisted-scratch
persisted anonymous identity `scratch-test` → local session `persisted-scratch`
session_home: ~/Library/Application Support/wire/sessions/persisted-scratch
registered cwd: ~/Source/wire
$ wire identity demote <federated-session>
demoted from federation → local
relay slot binding removed; keypair + agent-card retained
```
**Self-pwn caveat from live-testing:** demoting my own wire session
during testing stripped its federation binding (the session name "wire"
is a reserved nick, can't re-claim. Local-only paired peers still work;
federation reach is gone until a manual `wire bind-relay https://wireup.net`).
Lesson: a future polish round should add a `--confirm` prompt when
demoting the operator's own running federation session, OR a recovery
hint pointing at `wire bind-relay`. Both are out of alpha.20 scope.
**Full v0.7+ identity-first noun-CLI surface (alpha.14 + alpha.20):**
```
wire identity rename --name X --emoji Y
wire identity show [--json]
wire identity list [--json]
wire identity create [--anonymous|--local] [--name X] [--json]
wire identity persist <name> [--as <new>] [--json]
wire identity publish <nick> [--relay <url>] [--public <url>] [--hidden] [--json]
wire identity demote <name> [--json]
wire identity destroy <name> [--force] [--json]
```
Maps cleanly onto the v0.7+ vision's three lifecycle states
(anonymous → local → federation) with explicit promotion + demotion
verbs.
190 tests pass. Closes task #141.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: 🐻 cedar-bayou <wire+source-d8ae94a5@wire.id>
Five parallel research-agent slices covering transport/relay, identity/
pair flow, CLI+MCP surface, session/config/state, cross-cutting
concerns. Synthesized into .planning/research/codebase-audit-2026-05-23.md.
**Headline:** wire's protocol layer (signing, canonical, trust state
machine, SPAKE2+SAS, character determinism) is well-engineered and
would stay if starting from scratch. wire's plumbing (file IO scatter,
monolithic cli.rs + relay_server.rs, pair-flow proliferation, public
API surface) shows incremental-growth wear. Three structural cuts in
v0.8 land 60% of the friction reduction without touching features.
**P0 refactors for v0.8 (ranked by ROI):**
1. **Module split for cli.rs (10k lines) + relay_server.rs (2.3k lines).**
~3-5 days, zero behavior change. Unblocks parallel work + test
locality. Each subcommand or relay-concern becomes its own module
(~1-2k lines max).
2. **State layer → SQLite.** Replaces 5-JSON-file-per-session pattern
(agent-card, trust, relay, display, registry) with a single
`state.db` per session (private.key stays separate, 0600). Eliminates
flock ceremony, tmp+rename pattern, AND the 10+ unprotected
write_relay_state call sites that are a latent concurrency bomb.
~5-7 days, migration script needed.
3. **Narrow public API surface to 5 namespaces.** `lib.rs` currently
re-exports 54 items including orchestration internals (session,
endpoints, daemon_stream, service). For v1.0 cut: keep signing,
canonical, agent_card, trust, character; make rest private. Frees
future refactors. ~1-2 days.
**Working well (preserve):** signing + canonical JSON discipline,
character system + deterministic derivation, per-cwd isolation +
registry-as-source-of-truth, atomic write pattern, --json everywhere
on CLI, threat model discipline, single global auto-init flock
(alpha.12 closed the race cleanly), pull-loop cursor semantics.
**Showing wear (with severity):** HIGH on file size + state IO
scatter + public API; MEDIUM on pair-flow proliferation (4+ variants
without a unifying abstraction) + identity model layering (DID +
Handle + display.nickname + profile.* blob across multiple files) +
verb-CLI vs noun-CLI tension + test gaps for multi-machine flows;
LOW on `unsafe env::set_var` contract + symlink resolution + Windows
as orphan + macaroon.rs as inert scaffolding.
**Architectural shape converging on:** kernel (protocol primitives) ↔
service (relay) ↔ surface (CLI + MCP). v0.8 should make this layering
explicit at the module + API boundaries.
**Honest verdict:** ship v0.7.0 as-is (drop the alpha tag), then
spend ~2 weeks on the P0 refactors, then resume feature work from a
cleaner base. wire's architecture is fundamentally sound; the
structure is what needs love. Redesign-from-scratch rarely produces
shippable plans — the cuts above are the pragmatic equivalent.
Audit committed alongside the existing
.planning/research/{transport-substrate, identity-primitive-survey,
SYNTHESIS-wire-positioning, slice-{a,b,c}}.md for cross-reference.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: 🐻 cedar-bayou <wire+source-d8ae94a5@wire.id>
…ation Closes the v0.7.0 alpha track. Cargo.toml now reflects what the CHANGELOG has been calling v0.7.0-alpha.1 through .20: - Identity layer with deterministic Character (emoji + nickname + 256-color palette derived from DID, with operator-chosen overrides) - Per-cwd auto-init on `wire mcp` startup - Nickname-as-addressable-handle resolver (wire add / wire send) - `wire identity` lifecycle CLI (create/persist/publish/demote/rename /show/list/destroy) - LAN transport scope (alpha.9 + alpha.15 Tailscale CGNAT bind) - UDS transport (alpha.16-19; hand-rolled HTTP/1.1 over UnixStream, scheme-aware routing, end-to-end pair+send verified) - High-effort code review fixes (alpha.8 security + race + consistency) - Federation re-publish on rename (alpha.13) - Marketing refresh: README + landing/index.html + re-recorded demo.cast (alpha.7) - 190 tests passing across macOS arm64 + Linux ARM64 **Audit critique-iteration appended** to .planning/research/codebase-audit-2026-05-23.md after sending the first-pass audit through 5 hostile personas. Real iterations: 1. Sequence the P0s — don't parallel-fire. Narrow public API first (cheapest, 1-2 days), then module split (3-5 days), then SQLite only if write-contention bench justifies (7-10 days + migration). 2. SQLite section was over-confident — acknowledges OS locks remain; WAL reduces contention but doesn't eliminate; recommend benchmark-first. 3. Migration safety story explicit: dual-read, --dry-run, rollback path, version-compat shim. 4. Public API narrow needs deprecation cycle (not hard-remove). 5. Operator pain validation gap: audit ranked by gut feel, not incident frequency. Action: 1-day issues+CHANGELOG pass to quantify which wear actually bites. 6. Protocol-layer "well-engineered" was trust-but-don't-audit. Recommend separate paid security audit (Cure53 / Trail of Bits / NCC) pre-v1.0. 7. Cut the brand-marketing line — belongs in marketing doc. Net P0 priority after critique: - v0.7.0 patch: narrow public API + soft-deprecate re-exports - v0.8.0: module split + audit/wrap unprotected write_relay_state - v0.8.x: SQLite migration (only if bench justifies) - pre-v1.0: external security audit Ready to tag v0.7.0 + merge to main. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: 🐻 cedar-bayou <wire+source-d8ae94a5@wire.id>
CI's fmt check was failing on the v0.7.0 PR. Running `cargo fmt` normalized 7 source files; no behavior change. The large character.rs diff (~583 lines) is rustfmt reformatting the v0.7.0-alpha.4 word lists (ADJECTIVES, NOUNS, EMOJIS) from comma-grouped to one-item-per-line. Same data, different shape. Same for endpoints.rs match-arm spacing. 190 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: 🐻 cedar-bayou <wire+source-d8ae94a5@wire.id>
Six clippy errors after cargo fmt push: - character.rs:212 ×3: doc list item indentation (continuation lines) - cli.rs:2109: collapsed `if let` chain to `&&` form (Rust 2024 let-chains) - cli.rs:2249: redundant closure `.map(|n| sanitize_name(n))` → `.map(sanitize_name)` - cli.rs:6801: too_many_arguments on cmd_session_new (11 args). #[allow]'d with a pointer at the audit's recommendation for a config-struct refactor in v0.8 190 tests pass. clippy clean. CI should green after this. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: 🐻 cedar-bayou <wire+source-d8ae94a5@wire.id>
ServiceKind::log_basename is only invoked from ensure_macos_log_path, which is itself #[cfg(target_os = "macos")]. On linux CI the lib-only clippy pass sees no caller → -D warnings → fail. Test (tests::label_and_unit_name_distinct_per_kind) does call it, but that's the test-target compile. Lib target on linux: dead. Cfg-conditional #[allow(dead_code)] when not(target_os = "macos") — keeps the assertion meaningful when building for mac, silences the warning when cross-compiling for linux runners. Co-Authored-By: 🐻 cedar-bayou <wire+wire-source-d8ae94a5@wire.id> Co-Authored-By: 🐻 cedar-bayou <wire+source-d8ae94a5@wire.id>
…l pin
The demo's mesh-formation phase has been silently producing peer records
with empty slot_tokens since v0.5.14, which removed receiver auto-promote
on pull as a phonebook-scrape mitigation (pair_invite.rs:574+, the
"bilateral-required split"). The fix landed in maybe_consume_pair_drop
but the demo was never updated to match the new flow.
After v0.5.14, the bilateral pin requires:
1. receiver pulls → pair_drop stashed in pending_inbound (no peer write,
no trust pin, no ack — operator consent gate)
2. operator runs `wire pair-accept <peer>` → pin trust, write peer
endpoints, send pair_drop_ack
3. original sender pulls → pair_drop_ack consumed, slot_token recorded
The demo's old drain loop only did step 1. So all 10 pairs ended with
slot_token="" on both sides. The mesh-visibility check still passed
because peer RECORDS exist (written by each side's own `wire add` send
path), but the slot_token field stays empty until step 3 lands.
`wire push` then walks peer_endpoints_in_priority_order, which back-
compat-falls-back to the legacy top-level fields and short-circuits on
`!slot_token.is_empty()` → returns empty Vec → "no reachable endpoint
pinned for peer" → message stays in outbox, ring-send check fails.
The script's `exit 1` on first FAIL hides the fact that ALL 5 ring pairs
were broken — only "coffee-ghost → tide-pool" (i=0) was reported.
Fix: inside the drain loop, after each pull, sweep `wire pair-accept`
across every other handle. Idempotent (pair-accept errors when no
pending record exists; absorbed via `|| true`). 5 iterations of the
loop guarantees pair_drop → pair-accept → pair_drop_ack convergence
across all 10 pairs.
Verified locally on macOS with bash 5.3 + release build: 5-mesh forms,
5/5 ring sends land, demo completes.
Closes #27.
Co-Authored-By: 🐻 cedar-bayou <wire+wire-source-d8ae94a5@wire.id>
Co-Authored-By: 🐻 cedar-bayou <wire+source-d8ae94a5@wire.id>
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.
Summary
First user-visible cut of v0.7.0's identity-first vision (#24, #25). Two commits:
68ee6bev0.7.0-alpha — character primitive, per-cwd auto-init, nickname resolver, all three race conditions surfaced and patchedb94427cv0.7.0-alpha.4 — wider character variety (9.4× combo space)What changes for operators
wire mcpstartup (WIRE_AUTO_INIT=0to opt out)wire add --local-sister winter-baywire whoami --short/--colorednow show<character> · <cwd>for multi-window operator disambiguationwire identity rename --name foxtrot-meadow --emoji 🦊(persists todisplay.jsonsidecar; palette stays DID-derived)wire peersandwire session listshow characters per session/peerRace conditions surfaced and patched via stress testing
176 unit tests pass.
Files touched
Test plan
Open follow-ups (not blocking merge)
🤖 Generated with Claude Code