All notable changes to wire are tracked here. Format: Keep a Changelog, semver-ish.
Generated from git tag annotations; for richer context see the PR description linked in each section.
-
OpenCode gets per-session wire identities via a shipped plugin (#92 follow-on): OpenCode forwards no session-id env var to spawned MCP servers — measured on 1.18.25, the MCP child sees only
OPENCODE/OPENCODE_PID— so everywire mcpboot either reused one static key across all sessions or minted a throwaway identity per launch (one box measured 8,861 by-key homes). The gap cannot be closed from wire's side, because the key must exist beforewire mcpexecs. But OpenCode firessession.created~0.5s before booting local MCP servers (measured 16:03:39.600Z → 16:03:40.101Z), and its plugin system runs in-process with aconfighook that can still mutate the MCP env in that window. Shipsopencode-plugin/wire-session.js: drop it in~/.config/opencode/plugin/and each OpenCode session resolvesWIRE_SESSION_ID=opencode-<sessionID>— so birth identity and resume identity are the same key (-c/-sincluded), and no two sessions share one inbox. Resolution priority: exportedWIRE_SESSION_ID(pinned persona wins) → first top-levelsession.created/session.updatedevent →-s <id>argv or-cresolved read-only againstopencode.db(newest top-level session for the cwd;--forkgets a fresh key) → fresh UUID, which yields a new persona rather than ever a foreign one. Live-verified withopencode run+wire_wire_whoami: fresh→-sresume kept one persona; fresh→-c×2 kept one persona; consecutive fresh runs differed; exportedWIRE_SESSION_IDoverrode all of it. A lost event race falls back to the UUID path — a new persona, never a wrong one.docs/integrations/OPENCODE.mdrewritten against what was measured. -
Pi (the coding agent) is a first-class session host, with no MCP in the loop (#351 follow-on): Pi has no MCP client — its README says "No MCP." — so every Pi story in this repo routed through the third-party
pi-mcp-adapter, andwire setup's Pi target wrote~/.pi/agent/mcp.json, a file Pi proper never reads. Neither the adapter nor that file existed on a box with Pi installed, so the documented path had never actually worked. Pi does forward a session id: itsbash/powershelltools injectPI_SESSION_IDwhen spawned with a session context (core/tools/bash.js→resolveSpawnContext, gated onexposeSessionEnvironment, default on). AddedPI_SESSION_IDtoresolve_session_keyat priority 3 (labelpi), between Claude Code and Codex, so a Pi shell resolvessessions/by-key/<hash>instead of falling through to the machine default and sharing one inbox with every other session on the box. Two invariants the tests lock:PI_SESSION_IDoutranksCODEX/COPILOT/VSCODE(a stray host id must not steal a Pi session's identity), and home parity — one id string resolves to one home whether it arrives asPI_SESSION_IDorWIRE_SESSION_ID, which is what lets the package pin the key itself (Pi does not putPI_SESSION_IDin an extension's own env, and deletes it for context-less shells). Shipspi-plugin/: a Pi package with 13 native tools over the wire CLI, awire-piskill, and/wire-watchfor the session-lifetime inbox stream;wire_acceptandwire_setupare consent-gated so nothing mints a relay claim or grants peer access on an agent's initiative. Verified through the real tool path (pi install ./pi-plugin, thenwire_whoami→ a real persona; a no-identity run returned guidance and created nothing).docs/integrations/PI.mdrewritten against what was verified;docs/PLUGIN.mdgained the Pi section it lacked. -
wire session migrate— pre-RFC-006 named homes are reachable again: RFC-006 Part A madesessions/by-key/<hash>the one layout and the readers followed (list_sessionsscansby-keyonly;session_dirhashes intoby-key), but homes still sitting atsessions/<name>were left unreachable with an intact keypair. The failure is quiet and the error message sends you the wrong way: the registry entry still resolves (wire session currentnames the project,wire session env <name>looks like the right command), but that command answersno session named "slancha-api" on this machinewhile the keypair for that exact name sits on disk one directory level up — so the nextwire upmints a second identity for a project that already has one. Reads as "the cwd registry names a different agent than my session." The new verb renames the home into the 1.0 layout sosession list,session envandsession destroyreach it again, and prints the rollback. Dry-run by default; refuses when the by-key target exists (two homes are two identities — merging orphans one's pairings), when the legacy daemon pid is alive, or for any name that is not a plain single component (the by-key home derives from the sanitized form, so a name that changes under sanitizing would move to a home its name does not hash to; a path-shaped name would escape the sessions root). Verified in a temp root: invisible before, listed after, same handle through the new home, re-run is a no-op, planted collision refuses.
-
Persona collisions no longer resolve in readdir order (#351 follow-on): a persona nickname is
ADJECTIVES[243] × NOUNS[242]= 58,806 names seeded from the DID's 8-hex fingerprint suffix, and v0.11 made that nickname the addressable handle baked into the DID — so collision odds hit 50% at ~285 identities. Measured on one box: 8,861 initialized session homes, 566 handle groups already shared by two different DIDs (e.g.did:wire:agate-heron-aead0646/did:wire:agate-heron-4c4d66bc).resolve_local_sisterreturned the first match inread_dirorder, sowire dial <nick>could pair with, andwire send <nick>could write signed events to, whichever of two identities the filesystem enumerated first. It now returnsUnique | Ambiguous, andresolve_local_sister_uniquerefuses to guess at all four acting call sites (send's auto-pair, dial's resolution ladder, bothwire addbranches), listing a remedy per candidate. Two details that would each have silently defeated the guard:list_sessionsoverridesnameto the persona handle, so colliding homes arrive with an identicalname(dedupe keys onhome_dir, notname); and the first remedy printed was a dead end — neither the by-key home nor a full DID was matched byresolve_local_sisterorresolve_local_session, so both now match both, the unique token is the home, and both printed forms were confirmed to resolve. One identity at two homes collapses toUniquerather than a refusal (one agent is not a choice between agents); measured first, so that case is prevented, not observed. -
wire session currentreports the identity that signs, not just the registry's guess: since v0.13 identity never resolves from the cwd registry, yet the registry still answered for four display paths. Verified disagreement on a live box: in a registered cwd the command printedslancha-apiwhilewire whoamisigned ascobalt-nettle— the machine default. It now reportsoperative_handle,session_source,config_dir,wire_homeandagreesbeside the registry name;agreesisnullwhen there was nothing to compare rather than claiming agreement unchecked. stdout keeps its historical single-line answer so parsers hold; the disagreement note goes to stderr. -
wire setuplabels the Pi target as bridge-only (#92 category 1 follow-on): setup listedPi: ~/.pi/agent/mcp.jsonamong hosts it would wire up and--applywrote it, but Pi has no MCP client, so that file is inert withoutpi-mcp-adapter— on such a box--applylooked like it had connected Pi. The target stays (adapter users still want the write); the note beside it now names the bridge, points atpi install <wire-checkout>/pi-pluginfor everyone else, and flags that the shared snippet pinsWIRE_SESSION_IDto${CLAUDE_CODE_SESSION_ID}, the wrong variable under Pi. The snippet is left alone deliberately: whether the adapter expands${VAR}at all is third-party behaviour this does not verify, so substituting${PI_SESSION_ID}would trade one unverified claim for another (wire'svalid_session_key()guard makes an unexpanded literal fall through rather than hash into one shared home).
- The "two names" identity split — self-healed at its source + made self-reporting (#351): a Claude session's statusline showed one wire identity while the session operated on wire as another (confirmed live: a session whose
CLAUDE_CODE_SESSION_IDderiveddaydream-gorgewas sending/pairing asmerry-spindle, a key matching no live session; box-wide, 115 of 434 runningwireprocs served an identity no live session used). Root cause: the long-livedwire mcpserver resolves + freezes its identity at launch, but the statusline re-resolves live each render — and when the MCP boots before Claude writes its session PID-file,resolve_session_key()misses and the server mints a throwawaymcp-proc-*key, pins it intoWIRE_SESSION_ID, and serves it for life. Fixes: (1) before minting, retry the PID-file (walk the ancestor chain once, cached — no per-hoppsre-fork; the #353 Windows trap avoided) so the live session is recovered instead of frozen; (2) the mint no longer pinsWIRE_SESSION_ID(that slot is the operator-override/live channel — a minted value there beat the live session on re-resolve); (3)detect_identity_splitrewritten to compare the served identity vs the live PID-file identity, so the frozen MCP itself detects the drift the old env-vs-pidfile check structurally couldn't — surfaced as anidentity_splitfield onwire_status/wire_whoamiso an agent self-detects at the session-start health check and recommends/mcp reconnect; the RFC-008 deliberate fleet-share (WIRE_HOMEpin) is exempt. Also fixed a latent bug the redesign exposed:handle_for_keyhashed the sanitized (32-char-truncated) key while homes are created from the raw key hash, so a 36-char session-id uuid resolved to the wrong home. (4)wire setupnow bakesenv:{WIRE_SESSION_ID:"${CLAUDE_CODE_SESSION_ID}"}into the MCP entry it writes, so the forward survives--apply(inert on non-Claude hosts). Existing frozen MCPs heal on the next/mcp reconnect; the self-report makes that one-command recovery visible.
-
Nostr transport is now bidirectional through the daemon (#227 D3, RFC-007):
run_sync_pullnow also pulls Nostr-delivered events — it pulls events#p-tagged to our npub (kind:1) from the relays we're reachable on, transport-verifies them (NIP-01 id + schnorr), and feeds them through the sameprocess_eventspath as HTTP-pulled events (inner Ed25519 signature + trust pin + inbox dedup all reused). The pull set isself.nostr_relays[](relays we paired/fetched over — where peers publish our inbound, so it's correct even when pairing is asymmetric) unioned with the peer-transport relays;wire nostr pair/accept/fetch --relay Xrecords X. Together with the send-path fallback (#322), atransport: nostrpeer now fully round-trips through the daemon. Strictly additive — a no-op when this session isn'twire enroll nostr'd or has no nostr relay recorded (the HTTP-slot pull is byte-identical). The last RFC-007 D3 piece; live public-relay e2e (AC-3) remains a manual dogfood viawire nostr. -
1.0 surface freeze — deprecation policy + a golden MCP-catalog lock (ROAD_TO_1.0 §6): published
docs/DEPRECATION_POLICY.md— from 1.0, frozen surfaces (CLI verbs,--jsonshapes, the MCP tool catalog, on-disk state, protocol) change only through a deprecation window (announce → runtime warn → ≥1 MINOR & ≥90 days → remove in the next MAJOR), never a silent break. Added amcp_catalog_schema_is_frozentest that golden-locks all 27 MCP tools' name + input-schema props +requiredlist, so the agent-facing API can't drift unnoticed. Also promoted the hello-world first-connection round-trip to a required CI gate (hello-worldjob) now that daemon-survival is fixed (#263). -
wire sendroutes over Nostr when no HTTP slot is reachable (#227 D3.4, RFC-007): the send-path half of daemon routing.send::attempt_deliver(the single chokepoint behindwire send, group send, MCPtool_send, and probes) now falls back to the peer's recordednostr_transportwhen no HTTP endpoint reaches them — it encodes the same signed wire event as a NIP-01 event (nostr_event::wire_to_nostr_addressed, full inner Ed25519 sig intact,p-tagged to the peer's npub, schnorr-signed by ourwire enroll nostrkey) and publishes it to the paired relay. NewSyncDelivery::DeliveredNostrverdict (status: "delivered_nostr", carriestransport: "nostr"+ the peer's npub; counts as relay-reached so the peer canwire nostr fetchit). Strictly a fallback — when the peer has nonostr_transport(or this session isn't enrolled) the HTTP-slot path is byte-identical to before. The pull-loop side (daemon auto-pulls Nostr-delivered events into the inbox) is the next slice. -
Nostr peer reachability recorded on accept (#227 D3.4, RFC-007):
wire nostr acceptnow records the peer's Nostr transport coords — their npub + thewss://relay — underpeers[<handle>].nostr_transportin relay-state (additiveendpoints::pin_peer_nostr_transport/peer_nostr_transport; preserved across HTTP-endpoint re-pins per the #162 monotonic-state rule). Reachability only — trust is the separate VERIFIED pin. This is the data foundation for routing awire sendover Nostr; the send-path + pull-loop routing (and recording the initiator's side from the pair-ack) are the next slices. No behavior change to the HTTP-slot path. -
wire nostr accept— complete a bilateral pair over Nostr (#227 D3.4, RFC-007): the consent gate for Nostr pairing.wire nostr accept <npub> --relay wss://…pulls the peer's NIP-W1 pair-request, verifies the card (signature) and its nostr binding resolves to the exact npub that sent it (so a relayed copy of someone else's card can't be accepted), pins the peer VERIFIED (the explicit accept is the consent — same gate semantics aswire accept, #245 collision guard applies), and sends a pair-ack back. This closes the bilateral trust handshake over a public relay (pair → accept → ack); routing regular wire messages over Nostr (vs the trust pin) remains the deferred daemon-routing step. -
wire nostrCLI — pair + fetch over a Nostr relay (#227 D3.4, RFC-007): the runnable capstone of the Nostr stack.wire nostr pair <npub> --relay wss://…sends an encrypted NIP-W1 pair-request (your signed card) to a peer's npub;wire nostr fetch --relay wss://…pulls + decrypts the events addressed to your npub (pair-requests, pair-acks, messages) and lists who they're from. Uses this session's secp transport key (wire enroll nostr); errors clearly if not enrolled. Ties together binding + codec + relay protocol + websocket + NIP-44 + NIP-W1 into something an operator can actually run against a live relay (the manual D3.4 dogfood path). Deliberately additive — does NOT touch the daemon send/pull hot path; auto-routingtransport: nostrpeers from the daemon remains the deferred integration. -
NIP-W1 wire pairing over Nostr — no-SPAKE2 codec (#227 D3.4, RFC-007): pairing over Nostr without reintroducing the SPAKE2/SAS flow wire removed post-RFC-005 (operator decision).
nip_w1::build_pair_eventseals an agent's signed card (+ D3.1 binding) as a NIP-44-encrypted Nostr event (kind 21050request /21051ack),p-tagged to the peer's npub and schnorr-signed by the sender's transport key;open_pair_eventverifies the transport (id + schnorr), NIP-44-decrypts (only the addressee's key works — symmetric ECDH), and returns the sender's card for the existing bilateralwire acceptconsent gate to act on. No PAKE/SAS: identity = the signed cards + cross-signed bindings, trust = the explicit accept. Addsnostr_event::verify_transport(content-agnostic id+sig check). Offline + 5 unit tests (A→B + ack roundtrip, third-party-can't-decrypt, tampered-content fails transport, non-pairing-kind rejected). The receiver consent-gate wiring + daemon routing integration remain the deferred slices. -
NIP-44 interop proven against the official vectors (#227 D3.3 follow-up, RFC-007): closes the gap D3.3 flagged. The canonical NIP-44 v2 conformance vectors (the
validsubset of the referencenip44.vectors.json, public domain) are vendored atsrc/testdata/nip44_official_vectors.jsonand asserted byte-exact — all 35 conversation-key cases, 10 encrypt/decrypt cases (including exact ciphertext payloads under fixed nonces), and 24 padded-length cases pass. wire's NIP-44 implementation now demonstrably interoperates with every other NIP-44 implementation; the D3.3 implementation was correct as written. -
NIP-44 v2 encrypted DMs — the Nostr confidentiality layer (#227 D3.3, RFC-007): consumes RFC-006's reserved
encslot with a vetted spec (thereuse > buildprinciple) instead of bespoke crypto.nip44::encrypt/decryptbetween two secp256k1 keys (the D3.1 Nostr transport keys) make a wire DM sent over Nostr confidential to the relay. Faithful to NIP-44 v2: conversation key =HKDF-Extract(salt="nip44-v2", IKM=ecdh_x)(symmetric across both parties), per-message keys viaHKDF-Expand(ck, info=nonce), ChaCha20 over a length-prefixed power-of-two-padded plaintext (size leaks only a coarse bucket),HMAC-SHA256(nonce‖ciphertext)verified in constant time before decryption, payload =base64(0x02‖nonce‖ct‖mac). 9 unit tests cover ECDH conversation-key symmetry, cross-party decrypt (the real DM path), tamper→MAC-fail, wrong-key→MAC-fail, the padding table, and version/length rejection. Honest gap (flagged in-module): cross-implementation interop is not yet proven against the officialnip44.vectors.json— that vector test is a required follow-up before claiming wire DMs interop with other NIP-44 implementations. Remaining D3: NIP-W1 pairing + live third-party-relay e2e (D3.4), and wiring the transport into the daemon's send/pull routing. -
Nostr WebSocket transport —
NostrWspublishes + pulls overwss://(#227 D3.2b-ii, RFC-007): the network half of the Nostr binding, carrying the NIP-01 messages (D3.2b-i) over a real WebSocket.NostrWs::connectopensws:///wss://;publishsends anEVENTand awaits the relay'sOK;pullopens aREQ(e.g. a#pfilter for events addressed to our npub), collectsEVENTs untilEOSE, thenCLOSEs. Each pulled event is transport-verified (nostr_event::verify_and_decode— recompute NIP-01 id + check the schnorr sig) before it's returned, so a hostile relay can't inject a forged event; the caller still verifies the inner Ed25519 wire signature + the D3.1 binding. Every relay read is timeout-bounded so a silent relay can't wedge the caller. Two#[tokio::test]integration tests run against an in-process NIP-01 relay mock (no subprocess): a publish→pull roundtrip, and a hostile-relay test proving tampered events are dropped. New deps:tokio-tungstenite0.29 (rustls/webpki-roots, matching the existing TLS stack). TheTransporttrait unifying this with the synchronous HTTP-slot relay is deferred (RFC-007 §1) until a second async transport justifies the abstraction. Remaining D3: NIP-44 v2 DM (D3.3) + NIP-W1 pairing & live third-party-relay e2e (D3.4). -
Nostr NIP-01 relay protocol — client⇄relay message framing (#227 D3.2b-i, RFC-007): the JSON-array messages a Nostr relay speaks, on top of the D3.2a event codec.
nostr_relaymodels client→relay (EVENTpublish,REQsubscribe with aFilter,CLOSE) and relay→client (EVENT,OK,EOSE,CLOSED,NOTICE) messages with pure serialize/parse.Filtercarries the fields wire needs (#pto pull events addressed to its npub,kinds,since, …) and omits empties. Unknown relay message types parse toRelayMessage::Unknownrather than erroring, so a relay extension can't wedge the read loop. Pure + offline; 9 unit tests cover the NIP-01 message shapes, the missing-OK-message tolerance, forward-compat, and malformed-input rejection. TheNostrWsWebSocket that carries these messages overwss://(+ theTransporttrait its first impl defines) is the D3.2b-ii slice — where the async websocket dependency + a relay-mock integration test enter. -
Nostr NIP-01 event codec — wire events ⇄ Nostr events (#227 D3.2a, RFC-007): the protocol heart of the Nostr transport, building on the D3.1 dual-key binding.
nostr_event::wire_to_nostrpacks a signed wire event into a NIP-01 event —id = sha256([0,pubkey,created_at,kind,tags,content]), schnorr-signed by the D3.1 secp transport key — carrying the full signed wire event incontentso its inner Ed25519 signature survives intact.verify_and_decodeauthenticates the transport hop (recompute id + verify schnorr) and hands back the inner wire event. The result is a two-signature chain: the outer secp/transport sig proves "this npub sent it", the D3.1 binding proves "this npub is thatdid:wire", and the inner Ed25519 sig proves "that did authored it" — no single signature load-bearing alone, identity anchor stays Ed25519. Pure + offline; 7 unit tests lock the NIP-01 serialization, roundtrip, and tamper/forge rejection. TheNostrWsWebSocket transport +Transporttrait that carry these events overwss://are the D3.2b slice; NIP-44 DMs (D3.3) and NIP-W1 pairing + live-relay e2e (D3.4) follow. -
Nostr transport binding — the dual-key identity foundation (#227 D3.1, RFC-007 / curve spike Option 1): the first slice of speaking Nostr. An agent can now mint a transport-only secp256k1 key (
wire enroll nostr) that is cross-signed by its Ed25519 identity and carried as an additivenostr_pubkeycard field. The curve spike resolved the Ed25519↔secp256k1 gap to dual-key, never derived (scalar-reuse across curves is the anti-pattern SLIP-0010 exists to prevent), so the secp key is a transport endpoint, never an identity anchor — the ONE-NAME invariant holds (did:wirestays the only name). The binding is mutual: the Ed25519 identity signswire-nostr-binding-v1|<session_did>|<npub_hex>("this npub is my Nostr transport") AND the secp key schnorr-signs the same message (proof-of-possession), so a card can't squat someone else's npub.wire whoamisurfaces the npub only when the binding verifies. Fully offline + additive (a session with no transport key is byte-identical to before; pre-binding cards unaffected). Crypto core is unit-tested (roundtrip, wrong-identity, npub-squat-without-possession, tamper); the producer flow is covered bytests/it/110-nostr-binding.sh. This unblocks the rest of RFC-007 D3 — theNostrWsWebSocket transport (D3.2), NIP-44 DMs (D3.3), and NIP-W1 pairing + live-relay e2e (D3.4) — which remain the follow-up slices. -
Same-machine signed attestation — your own sessions on one box auto-pair, cryptographically (#182, RFC-001 amendment): wire already auto-pinned sister sessions by reading their card off local disk, but that filesystem witness is weak — anything that can write the data-dir tree could mint a "sibling". Now an op-enrolled session carries a
same_machine_attestationon its card: the operator root key (op_sk) signswire-same-machine-v1|<fingerprint_hex>|<session_did>, where the fingerprint issha256(machine_id ‖ os_user_id ‖ tag). A receiver auto-pins the sender atORG_VERIFIEDonly when (a) the peer's op-chain verifies, (b) itsop_didequals the receiver's own (same operator — and because a wireop_didis a key commitment, that forces the same op key), (c) the attestation's fingerprint byte-equals the receiver's own freshly-recomputed one (a remote sender can't know it without already being on the box; per-OS-user salt blocks cross-user pairing on shared hosts), and (d) the signature verifies. Never crosses intoVERIFIED(that still needs the bilateral SAS gesture). New verbwire enroll fleet-link [--dry-run] [--rotate-machine]batch-attaches the attestation to every enrolled sibling session; idempotent (the signed message is deterministic). Field-additive — pre-v0.15 receivers tolerate the field as an opaque extra. Producer + receiver + crypto core are unit-tested (incl. hostile-forge and different-uid rejection); the verb is covered bytests/it/100-fleet-link.sh. Deviations from the amendment doc (sha256 vs blake2b; domain-separated string payload reusing the audited cert path;id -u/whoamifor the uid salt; the plugin-hook auto-dispatch left as a tracked follow-up) are documented insrc/same_machine.rs. -
Rotation-refresh: an already-trusted peer that rotates its relay slot re-pairs automatically (#15): a rude slot rotation (no
wire_close) used to leave peers holding a now-410slot, and re-pairing required a fresh manualwire accepton the receiver. Now a re-intro from a peer whose DID is already pinned at a consented tier (we accepted that exact identity) is treated as a transport refresh — the receiver's daemon re-pins their new endpoints and re-acks (restoring the write-token) with no manual accept, tier unchanged. Safe: keyed on the full DID with the verified card signature + the #245 collision guard, so only the real key-holder triggers it; a first-contact stranger still goes to pending-inbound (consent gate unchanged). (Sender-side auto-trigger of the re-intro on aslot_stalesend — so no manualwire dialis needed at all — is a tracked follow-up on #15.) -
wire ping <peer>— connection health probing (RFC-004 Tier-1) (#142): liveness-probe a paired peer and get the round-trip time. The peer's daemon auto-responds — no LLM / MCP on the responder side (RFC-004's AC-HP2 kill criterion). Probe + ack ride the existingkind=100heartbeat carrier with a bodytdiscriminator (probe/probe_ack), not a new top-level kind; they're plaintext (only a correlation nonce) and trust-neutral (never mutate a tier). Per-peer ack rate-limit bounds a probe flood (AC-HP3). Validated end-to-end (tests/it/80-health-probe.sh). Tier-2 (responder_stateintrospection),wire healthrendering, and MCPwire_pingparity are tracked follow-ups.
- Relay: backstop ceilings on slot / handle / pair / invite counts (#291 H1): the relay's in-memory maps (and their persistence files) grew without bound — an unauthenticated client could
allocate_slot(or open pairs / register invites) in a loop to exhaust RAM + disk. Allocating handlers now refuse with503once a generous ceiling is reached (200k slots / 100k handles / 50k pairs / 50k invites); same-DID handle re-claims are exempt (they don't grow the map). The remaining #291 items — per-IP rate keying (currently global; deferred-behind-WAF + fiddly across the UDS path) and governing/paginating the read endpoints — stay tracked.
wire unclaim+ relayDELETE /v1/handle/claim/:nick— release a claimed handle (#247 finding 1): a handle claim was FCFS-permanent (no expiry, no unclaim), so an abandoned/rotated nick squatted the directory forever. You can now release your persona:wire unclaim(owner-gated by your slot token) frees the nick so it stops resolving via.well-known/wire/agentand can be re-claimed. (Operator-TTL auto-expiry — the other half of #247.1 — needs persisted slot-activity to avoid evicting quiet-but-live agents on relay restart, and stays tracked.)wire status --wait-daemon-running [--timeout <secs>](#284.2): a bounded, in-process replacement for fragile external shell loops likeuntil wire status … | grep -q 'daemon_running":true'; do sleep 3; done. The external pattern + a never-healthy daemon piled up 254 stalewire.exeprocesses on Willard's box (eachwire statusinvocation hanging on a wedged probe, the loop spawning a fresh one every 3s). The new flag polls the daemon-liveness snapshot every 200ms in-process, exits 0 with the full status whendaemon_running:true, or bails after--timeout(default 30s) with the last-seenpidfile_pid/pgrep_pidson stderr so the operator knows what wasn't healthy. Replaces the loop with one bounded subcommand call; no spawn pressure, no orphan accumulation. Pure-logicwait_stepdecision is split out and unit-tested.
-
MCP
wire_acceptnow pins a routable peer + accept flags an unreachable reply path (#277): two gaps. (1) The MCP accept path still wrote the peer pin in the flatpeers[h]={relay_url,slot_id,slot_token}shape that RFC-006 Part B no longer reads — so a peer accepted over MCP got an emptyendpoints[]andwire sendcouldn't route to them. It now pins viaendpoints[]like the CLI path. (2) Both accept paths (CLIwire accept+ MCP) returnedbilateral_acceptedeven when the peer advertised only loopback/same-host endpoints — silently reporting success on a reply path that can't reach a remote peer. They now surfacereply_path_reachable: false(+ a remediation warning on the CLI) via a newendpoints::endpoints_are_local_only, which infers reachability from the URL so a loopback address mislabeledfederation(the exact #277 case) is still caught. -
MCP
wire_dialno longer wipes a peer's reply token on re-dial (#277, RFC-006 Part B): the MCP dial path read the peer's carry-forwardslot_tokenfrom the flatpeers[h].slot_tokenfield that Part B (#268) stopped writing, so it always read empty — every MCP re-dial of an already-acked peer reset the reply token to"", breaking the reply path until a fresh ack. Both dial paths (CLI + MCP) now read the token through one canonicalendpoints::peer_federation_tokenreader so they can't drift apart again; the two other stale peer-flat reads #268 missed (a deadrelay.rsre-resolve fallback and thetrust.rsPENDING_ACK check) are removed. -
Relay: per-nick
/v1/handle/introrate limit closes the unauthenticated pair-intro flood (#247 finding 3): the intro endpoint is unauthenticated by design (a stranger drops a pair-intro), and was protected only by the shared per-slot byte quota — so an attacker could flood a known nick's slot toMAX_SLOT_BYTES(~25s) and DoS the victim (the global governor doesn't help: it throttles everyone and 10/s × 256 KiB still fills 64 MB in ~25s). Intros to a given nick are now capped (5 per 5 min, sliding window) →429over the limit. -
Trust pins are no longer lost-updated under concurrency (#246): #288 made each
write_trustatomic, but a foreground pin (wire add/accept) and the daemon's pull-path pin could still eachread_trust→ modify →write_trustfrom the same snapshot, dropping one of the two pins (last-write-wins). Newupdate_trustread-modify-write transaction holds thetrust.lockacross the whole read+modify+write (mirroringupdate_relay_state); the concurrent pin call sites (pair-invite,wire add/accept, sister-pair, MCP accept/dial) now route through it. -
Trust pins can no longer be hijacked by a grindable nick collision (#245, partial): the trust store is keyed by the peer's persona nick, which is a deterministic function of the keypair over a ~65k word-list — so an attacker could grind keys (or simply spoof a card's
handlefield) until their nick matched a victim's, then OVERWRITE the victim's pin with their own DID + keys, hijacking the nick (the real victim's messages then fail signature verify, the attacker's verify as the victim).add_agent_card_pinnow refuses to overwrite an existing nick whose pinneddiddiffers from the incoming card'sdid(DIDs carry the key fingerprint, so they're not grindable to match a specific full DID; same-identity re-pins / key-succession keep the DID, so legitimate updates pass). The complete fix — re-keying the whole store by DID so two distinct identities that collide on a nick can coexist — remains tracked on #245. -
relay.lockacquisition is now bounded + stale-owner-reclaimable (#284.5): a hung wire daemon (or any wire process stuck in a relay long-poll — see #284.1) heldrelay.lockindefinitely, since the kernel only releases the flock at PID exit and the wedged process never exited. Every subsequentwire status/wire send/wire daemonthen blocked onlock_exclusive()forever; on Windows this was the engine behind the 254-wire.exe-process pile-up Willard's SessionStartuntil wire status …loop spawned. The acquire path now (1) opens +try_lock_exclusivenon-blocking; (2) on contention, reads the owner PID from a newrelay.lock.ownersidecar and consults a pure-logic classifier — if the owner is dead/absent it retries immediately (the OS auto-released the flock on PID exit), if the owner is live it bounded-exponential-backoffs up toWIRE_RELAY_LOCK_TIMEOUT_SECS(default 10s) and then fails with the holder PID surfaced in the error sowire doctorcan name a target. The sidecar pattern is needed because WindowsLockFileExdenies reads to other handles against the locked file, so "who holds this?" can't be read out ofrelay.lockitself. No behavior change in the uncontended fast path. -
warn_on_identity_collisionnow actually catches collisions on Windows (#247 finding 4): the v0.6.10 collision check shelled out to POSIXpgrepand read/proc/<pid>/environto compareWIRE_HOMEbetween live wire processes — both POSIX-only, so Windows silently no-op'd. Twowire mcpservers in the same cwd shared one identity, raced the inbox cursor, and operators burned hours on "they look identical." The enumerator now routes throughcrate::platform::find_processes_by_cmdline(the existing PowerShell +Get-CimInstance Win32_Processadapter), and the cross-processWIRE_HOMElookup on Windows walkslist_sessions()× every inbox-owning role's pidfile (<session_home>/state/wire/<role>.pid) and reverse-maps the candidate PID to its serving home. To make that reverse-map work for every role — not just daemon —wire mcp,wire monitor, andwire notifynow drop their own<role>.pidon startup via a newensure_up::write_self_role_pid, mirroring whatwrite_self_daemon_pidhas always done for the daemon. Pure-logicfind_home_for_pidis split out and unit-tested; the path-based reader issession::session_role_pid(home, role). POSIX path is unchanged (still/proc/<pid>/environ/ps -E). -
wire status/wire up/wire doctorare now bounded on a wedged probe path (#284.1): the Windows process-enumeration shell-outs inplatform.rs(Get-CimInstance Win32_Processandtasklist) had no timeout, so any wedged CIM call — observed on Willard's box where 254 stalewire.exeprocesses piled up under heavy WMI contention, but the same shape on any corrupted CIM repository — blocked every CLI command that ran a process probe forever. A newcrate::platform::run_with_timeout(cmd, dur)wraps every Windows shell-out in this module: spawn, handwait_with_outputto a reader thread,recv_timeouton the main thread, kill the wedged child by PID on timeout viataskkill /F /T /PID. Default 5s, overridable viaWIRE_PLATFORM_TIMEOUT_SECS. On timeout each call falls through to its existing tool-error fallback (empty Vec,None,false) so the caller sees "no answer" instead of hanging forever, andwire status/wire doctorreturn promptly with whatever local state is readable. -
wire upgradeprebuilt-download path no longer hangs silently (#284.3): the toolchain-free update path calledresp.bytes(), which blocks until the entire release binary lands with zero stderr output. On Willard's Windows host that looked indistinguishable from a hard hang — no progress, no error, mtime unchanged, even though crates.io and github.com were both reachable. The download now streams in 64 KiB chunks and prints awire upgrade: downloaded N / TOTAL bytes (PCT%)line in-place every ≤500ms, so an operator sees forward motion (or sees it stall) instead of staring at a frozen terminal. The existing 120s wall-clock timeout on the blocking reqwest client stays as the hard backstop. Pure-logicformat_download_progressis split out and unit-tested (clamps percent at 100 on overshoot; falls back to "unknown size" whenContent-Lengthis missing or zero). -
Long-running wire roles now warn (or fail-fast under
WIRE_STRICT_SESSION=1) when the launcher dropped the session key (#284.4): if a child process inherits neitherWIRE_HOMEnorWIRE_SESSION_ID(and no host-adapter env likeCLAUDE_CODE_SESSION_ID), the session adapter falls through to a freshly-minted per-process key (MCP) or to the machine-default identity (CLI), and the operator only learns about it minutes later when they notice a sibling daemon racing the inbox cursor. Each of the four inbox-owning entry points (wire mcp,wire daemon,wire monitor,wire notify) now calls a newcrate::session::warn_if_unexpected_session_source(role)at startup: it forces a clear stderr line naming the resolved source (machine-default/minted) and the remediation (setWIRE_SESSION_IDorWIRE_HOME), and — whenWIRE_STRICT_SESSION=1is set — exits with code 2 instead of letting downstream init/bind block on a shared lock or quietly corrupt a sibling session's state. Pure-logicis_unexpected_session_sourcepredicate is unit-tested across every adapter label. -
Windows local dev —
cargo test/cargo clippygates now compile clean (developer-experience hygiene): three small Windows-only pre-existing breakages blocked any local TDD on a Windows host. (1)config::tests::private_key_is_mode_0600used unix-onlyPermissionsExt::mode()without acfg(unix)guard, failing to compile undercargo test --libon MSVC. (2)cli::upgrade::tests::no_shadow_warning_when_active_symlink_resolves_to_current_exedeclaredlet linkthat's only used inside its#[cfg(unix)]symlink call, triggeringunused_variableson Windows. (3)cli::lifecyclehad a collapsible-if pattern in the Windows-onlytaskkillenumerator and anunneeded_returnon the Windows-onlypurge_binary_and_shellbranch, both firing clippy under-D warningson MSVC. All three are test/scaffold-only, no runtime behavior change; CI'sinstall-smoke-windowsjob is untouched. -
wire_whoami(MCP) flagsstale_binarywhen the server's code has drifted from the daemon (#247 finding 5): a long-livedwire mcpserver keeps serving the binary it was spawned from, so afterwire upgradeswaps the daemon it silently runs pre-upgrade code in memory (the "ghost identity" drift behind today's 0.14.1-vs-0.16.0 confusion).wire_whoaminow compares the bakedserver_versionagainst the live daemon's recorded version and emitsserver_version,daemon_version, and on mismatchstale_binary:true+ a note to/mcpreconnect. -
peer_unknownon a send now says WHY, and the MCPwire_sendschema exposesqueue(#284 parts 6-7): a VERIFIED/pinned peer whose relay slot had an emptyslot_token(thepair_drop_ackhadn't landed — common right after a daemon/MCP restart) returned the misleading "peer not pinned — run wire dial." The reason is now classified against live state — not pinned vs pinned but no endpoint vs pinned but slot has no token — and points at the full<peer>@<relay>dial (the bare nickname reportsalready_pinnedwithout re-registering the slot). Separately, the MCPwire_sendtool documentedqueue:truebut omitted it from its input schema (the handler already supported it) — now exposed. -
Relay: validate claimant
relay_url, hardenresponder-healthpath, correct a false rate-limit comment (relay security audit):POST /v1/handle/claimnow rejects arelay_urlthat isn't cleanhttp(s)://host[:port]— no userinfo (handle@relay), no alternate scheme — so a poisonedendpointcan't be planted in the public A2A directory.POST /v1/slot/:slot_id/responder-healthnow validatesslot_idshape before using it in a filesystem path (defense-in-depth). Thehandle_introdoc comment that wrongly claimed governor coverage is corrected — that route is unauthenticated and ungoverned (per-nick intro rate-limit tracked in #247.3). -
wire whois/wire dial/wire addnow surface the resolved DID + key fingerprint and refuse a poisoned card (#247 finding 4): relay-mediated discovery was trusted implicitly — a malicious/compromised relay could serve a card under someone else's DID pre-pair. These verbs now print the resolved key fingerprint with a "verify out-of-band — discovery is trusted for routing, not identity" reminder, surfacefingerprint/fingerprint_matches_didin--json, and hard-refuse the pair when the card's advertised key doesn't hash to the fingerprint baked into its claimed DID. -
wire upgradeno longer corrupts systemd/launchd units after acargo installin-place replace (#274): the kernel marks the replaced running binary's/proc/self/exewith a trailing(deleted), which was written verbatim intoExecStart=, leaving the daemon flapping forever (error: unrecognized subcommand '(deleted)'). The exe path is now resolved (marker stripped) before it reaches a unit file. -
wire upgradeno longer false-warns about a PATH shadow when the active PATH entry is a symlink to the upgraded binary (#276): the same(deleted)marker madecurrent_exe()un-canonicalizable, so the symlink never matched and an "off-PATH / old binary" warning fired even though both PATH entries resolved to the freshly-upgraded binary. -
wire upgrade --refresh-stale-childrenno longer kills stale daemons the supervisor can't respawn (#275): the flag killed every stale-binary session daemon, but the--all-sessionssupervisor only respawns sessions it's eligible for (registry-bound OR active within the idle cutoff). A free-floating/unbound+idle session was killed and never brought back — its identity silently stopped syncing while the upgrade reported success. It now kills only the daemons the supervisor will respawn and leaves the rest running, surfacing them (in human + JSON output,wire status, and--check) as "relaunch manually." -
wire claim --relay <URL>(andwire add --relay, accept-invite) now honor the requested relay (#279): the sharedensure_self_with_relayreturned any existing self slot regardless of the relay asked for, soclaim --relay wireup.netreused a loopback primary slot and POSTed the claim to127.0.0.1. It now reuses a slot only if it's already on the requested relay, otherwise allocates one there (additively). -
wire tail/ thewire_tailMCP tool / thewire://inboxresource no longer surface raw ciphertext for encrypted DMs they can't decrypt (#281): anenc=wire-x25519.v1body that this build couldn't open (decrypt failed, no key, or an unknownencscheme) was rendered as its raw{"ct":…}blob with a greensig ✓and no warning — an operator (or a reading agent over MCP) saw gibberish and believed they'd read the message. All three read surfaces now render an explicit<encrypted DM … run \wire upgrade`>placeholder and setdecryptable:false(via one sharedencpredicate). Forward-compatible for any futureenc` discriminator. -
wire add <nick>@<domain>now warns when you are not federation-reachable (#278): a federation add advertised whatever self slot existed — including a loopback one a remote peer can never reach — and reporteddrop_sentwith no hint that the pair could never complete (the peer'spair_drop_ackhas nowhere to land → permanentPENDING_ACK). It now detects a loopback-only self endpoint, warns with remediation (wire bind-relay/wire up), and surfacesself_reachable: falsein--json. -
write_trustis now atomic + lock-serialized (#246): the trust store was written with a raw, locklessfs::write, so the daemon's pull-path pin and a concurrent foregroundwire add/accept/promotecould interleave bytes into a torn, unparseabletrust.json(the same failure class as the relay.json Bug #3). It now writes via flock + tmp+rename, mirroringwrite_relay_state, so every reader sees a whole file. (Read-modify-write lost-updates remain a tracked follow-up under #246.)
v0.16.0 — the 1.0 format freeze + onboarding cleanup. RFC-006 collapses the two dual on-disk representations into one each; the CLI stops pretending you name your own identity. BREAKING — on-disk session/peer state and a few CLI args changed; wire nuke resets a machine.
Pre-1.0 housekeeping: freeze the on-disk formats so they never need migration, and make the identity surface honest (the crypto names you, not a typed flag). No production users, so it breaks freely.
- Part A — one session store (#269): sessions live only under
sessions/by-key/<hash>/; the legacy top-levelsessions/<name>/layout is gone. A named session keys off its name, an agent session off its session-id — both into the one store. Kills thecwd → identitystraddle behind the #170/#174 fork-storms. Sessions now surface uniformly by their persona handle. - Part B — one peer-routing source (#268): a peer's relay slot lives only in
endpoints[]; the flatrelay_url/slot_id/slot_tokentop-level peer fields are gone. Delivery iteratesendpoints[]with priority failover. (Fixed aneffective_tierreader missed in the migration that wrongly showed freshly-paired peers asPENDING_ACK.)
- No name to type. Removed
wire init <handle>(vestigial seed),wire init --name/wire up --name(these published a free-choice display name ≠ your handle — a one-name violation). Your persona is derived from the keypair at init; the card's display name is the handle, always. - One onboarding verb.
wire upis the front door;wire up --offlinefolds in offline keygen;wire initis demoted to an internal primitive users never type. wire session new <name>kept — that name is a home locator, not an identity.
- Daemon no longer self-aborts on its own singleton pidfile (#263); orphan-daemon detection scoped to the current
WIRE_HOME(#248); the nuke unit-test no longer tears down the live host service (#243). - Path-traversal rejected in group ids before any filesystem write (#238); five operator-input/IO error-path bugs in the CLI fixed, TDD'd (#239). DID-key binding gap + fresh-install status orphan fixed (#244/#248).
- Threat-model truth pass — honest D1 DM-sealing posture (#266); landing corrected to v0.15 reality (#264); root decluttered (#265); the 15.7k-line
cli.rssplit into a module tree (#242).
- Real integration suite (
tests/it/): boots actual relays and drives the shipped binary — zero-paste pairing, on-box sister mesh, onboarding, nuke/recovery, group join-by-code cross-member verified read, and federation over a non-loopback remote relay. - New CI jobs:
integration-tests,install-script-smoke(runs the realinstall.sh), and the demo jobs wired into the localtest-envgate so it mirrors CI. mainis branch-protected: all checks must pass (and be up-to-date) before merge, admins included.
v0.15.0 — the de-deprecation: every backwards-compatibility surface removed. wire dial is now the sole pairing path; agents only ever see canonical verbs. BREAKING — old on-disk state is incompatible; wire nuke resets a machine.
The deprecated surface was actively confusing agents — the MCP tool list advertised legacy wire_pair_* tools alongside the canonical verbs, so an LLM picked the wrong one. This release rips out all backwards compatibility (RFC-005 + follow-on). No production users, so it breaks freely; the new wire nuke is the clean-slate reset.
wire nuke— hard machine reset: kills daemons + the--all-sessionssupervisor, removes the launchd/systemd/schtasks service units, de-registers thewireMCP entry from every host config (Claude Code / Cursor / Copilot / VS Code / OpenCode — so a "fresh" machine doesn't show the agent a deadwireserver), and wipes all wire dirs. Keeps the binary.wire nuke --purgealso removes the binary + scrubs shell PATH/env lines (Windows prints the manualdel). Safety:--dry-run→ typed-nukeconfirm →--force/--yes. Newinstall-smokeCI (Linux + Windows) + a reproducible Rusttest-envcontainer.
- Deprecated MCP alias tools —
wire_pair_accept/wire_pair_reject/wire_pair_list_inbound(pure aliases of canonicalwire_accept/wire_reject/wire_pending) gone fromtools/list. Old names return a helpful "usewire_accept" redirect; canonical verbs are the only thing advertised. - Deprecated CLI alias verbs —
wire pair(→wire dial),wire pair-accept/pair-reject/pair-list-inbound(→accept/reject/pending), and thedeprecation_warnshim.wire accept <url>no longer silent-forwards an invite URL — usewire accept-invite <url>.
- The entire SAS / SPAKE2 / code-phrase / SAS-digit pairing flow is gone — modules
sas.rs,pending_pair.rs,pair_session.rs; CLIpair-host/pair-join/pair-confirm/pair-list/pair-cancel/pair-watch/pair-abandon; MCPwire_pair_initiate/join/confirm/check+ the_detachedvariants.wire dial(relay pair-drop) + bilateralwire acceptis now the sole canonical pairing path. Shared identity-bootstrap (init_self_idempotent) relocated tosrc/init.rs; the daemon pidfile write relocated toensure_up.
- Legacy bare-integer pidfile (
PidRecord::LegacyInt) and the pre-v0.5.7 no-suffix DID builder. Version-tolerance shims with no current reader: v3.1 agent-card read path, pre-v0.5.19 relay "discoverable default", v0.4-card profile default. The redundantWIRE_QUIET_AUTOSESSIONsession.rs check (the TTY check subsumes it).
- The v0.6 named-session layout (
wire session new/list/env/destroyuse it;by-key/<hash>is a parallel agent-resolution layout, not a replacement) and the flat peer-endpoint fields (the live invite flow reads/writes them) are current code, not dead shims. Consolidating them to a single representation is a deliberate redesign with the #170/#174 fork-storm risk — tracked in RFC-006, not forced here.
- BREAKING: pre-v0.15 on-disk state (old pidfiles, no-suffix DIDs, v3.1 cards) is no longer read. Run
wire nuketo reset, thenwire up. Old SAS pairings can't be resumed — re-pair withwire dial. - Full per-PR detail: Phase 1 #220, Phase 2 #231, Phase 3 #232, Phase 4 #233, SAS removal #236. Design:
docs/rfc/0005-remove-backwards-compat.md.
v0.14.2 — the multi-session ops batch + the queue collapse: silent-send class closed, --all-sessions supervisor architecture lands, four hotfixes caught by live dogfood, send + pull both become synchronous verdict-on-demand verbs.
honey-pine's 2026-06-01 multi-system dogfood (#162) surfaced an interlocking set of bugs that broke wire's daemon layer on any operator with more than one session: silent send failures, daemon-up-but-not-syncing on launchd, false-negative wire status, tier flap, notification storms. Paul (same day): "why are we dealing with this whole outbox queued delivered thing it's a headache and always breaks can we streamline and collapse steps." This release closes all of the dogfood bugs AND collapses the send/receive paths so the "queued ≠ delivered" silent-drop class can't reappear in the default surface.
Dozens of PRs since the v0.14.1 tag — the #162 multi-session batch below, plus a launch-hardening + reliability follow-on (see the final subsection). No trust ladder change, no protocol bump (v3.2 still the constant). Operator action required after upgrade: re-run wire service install (idempotent) so the launchd plist / systemd unit picks up the new wire daemon --all-sessions --interval 5 ProgramArguments. wire upgrade does this automatically when a service was previously installed.
- Send-lifecycle log +
pending_push_count(#167). Each successful relay POST appends<outbox_dir>/<peer>.pushed.jsonlso the caller can audit "queued → pushed" lifecycle.wire_status(MCP) now surfacespending_push_count— the count of events in the outbox that have NOT yet appeared in the pushed log. Withstale_sync == true, that's the diagnostic for the silent-send class honey-pine reported. - Canonical
to:DID on outbound send (#165). Pre-fixwire sendwroteto: did:wire:<peer-handle>(bare); peers running v0.14.x rejected onto:mismatch against their fulldid:wire:<handle>-<fingerprint>.cmd_send/tool_sendnow resolve the canonical pinned DID via the newtrust::resolve_peer_did. This was THE silent-drop root cause; honey-pine's "BUG 1" closed. - Durable
bilateral_completed_atfield stops tier flap (#166).effective_peer_tierpreviously read slot_token presence to compute "VERIFIED vs PENDING_ACK". Re-pinning peer endpoints (which legitimately rewrites the slot_token block) made the tier flap.bilateral_completed_atis set monotonically on receipt of pair_drop_ack; never cleared.pin_peer_endpointspreserves it across re-pin events. - Daemon-down lifecycle visibility (#163). Per-process pidfile singleton guard with same-pid Drop preservation.
last_sync.jsonwritten after every cycle sowire_statuscan reportlast_sync_age_secondsdistinct from "anywire daemonprocess exists". - Send response includes
daemon_seen+stale_sync(#164).wire_send(MCP) annotates its response with daemon-health hints so a caller seeingstatus: queuedimmediately knows whether the daemon is actually running and pushing or just silently no-op'ing into an outbox no daemon will drain. - Stale
pending_inboundclear on bilateral completion (#171). Whenmaybe_consume_pair_drop_ackflips a peer to bilateral-VERIFIED via the inbound ack path, any leftoverpending_inboundrecord from the earlier pair_drop is cleared idempotently. Pre-fix: VERIFIED peer lingered inpending_pairs.inbound_handles("sunlit-aurora" in honey-pine's report). - Daemon stream resilience (#168). SSE subscriber's TCP keepalive tightened from 60s → 30s. New
stream_state.jsonwriter tracksstate(connecting/connected/reconnecting/error),last_event_at,reconnect_count. Surfaces viawire_status. honey's "BUG 2" closed.
-
wire daemon --all-sessionssupervisor (#170). Multi-session orchestrator: reads the session registry, fork-execs one childwire daemonper initialized session withWIRE_HOMEenv pinned to that session's home dir. Per-machine singleton on<sessions_root>/supervisor.pid. 10s registry-poll, rapid-failure backoff (1s → 60s cap), session-removal kill, pre-spawn pidfile check so operator-spawned tmux daemons coexist. Launchd plist + systemd unit + Windows task XML ProgramArguments updated. Closes the launchd-blind cwd-resolves-default-WIRE_HOME failure mode honey-pine spent multiple sessions diagnosing. -
🚨 Supervisor fork-bomb hotfix (#174). Caught immediately via live dogfood of #170 on a 133-session box. Supervisor was passing
--session <character-name>to each child as a belt-and-suspenders check, butsession_dir(name)only resolves the legacy v0.6 top-level layout — v0.13 by-key sessions wherenameis the persona handle bailed. Fix: drop the redundant flag entirely;WIRE_HOMEenv is the sole contract. -
🚨 TLS hotfix:
rustls-tls-native-roots→rustls-tls-webpki-roots(#176). Also caught via the same dogfood pass. Once the supervisor put every daemon in launchd, every TLS handshake to wireup.net failedUnknownIssuer: launchd-spawned processes don't inherit Aqua-session keychain access on macOS, sorustls-native-certsreturned an empty root set. Mozilla's bundled webpki-roots work in any process context. Was a temporary trade-off: corporate CA / AV-resign transparency lost; superseded same-day by #183 below. -
Dual-roots TLS verifier (#183, closes #177). The proper #176 replacement. New
tls::shared_client_config()builds a singleArc<rustls::ClientConfig>consumed by every wire HTTPS surface via reqwest'suse_preconfigured_tls. Webpki bundled roots ALWAYS loaded (the launchd-safe baseline).rustls-native-certsqueried ADDITIVELY — contributes corp CAs / AV-resign roots / on-prem CAs when accessible, gracefully empty otherwise. Fail-soft on partial native-cert errors with a stderr breadcrumb (wire tls: trust roots loaded — N webpki + M native = T total).WIRE_INSECURE_SKIP_TLS_VERIFY=1still bypasses. Restores #176's corp-CA capability without re-breaking the launchd context #176 unblocked. -
wire daemon --session <name>resolves v0.13 by-key (#180). Operator-facing counterpart to #174:cmd_daemon's--sessionresolver now uses the newsession::find_session_home_by_namewhich handles both v0.6 top-level and v0.13 by-key/persona-handle layouts.
-
CLI status surface gap closer (#169).
pending_push_count+stale_sync+stream_state(added to MCPwire_statusin #167/#168) hoisted into CLIwire status. Shared helpers inconfig.rs(compute_pending_push_count,read_stream_state,stale_sync) so MCP / CLI / future doctor stay in lock-step. Plain-textwire statusadds three lines in operator-triage order: last sync → pending push → stream. -
Orphan-pid session annotation (#173). When
wire statusflags orphanwire daemonprocesses (pidfile pid ≠ pgrep), each one now annotates with cmdline + parsed--sessionarg. Helps operators distinguish a stale leftover from a legitimate per-session child the supervisor is managing. -
Per-session pidfile walk for annotation (#175). Post-#174 supervisor children no longer carry
--sessionin their cmdline, soparse_session_argmisses them. Newsession::pid_to_session_mapwalks every session's<home>/state/wire/daemon.pidand builds{pid: session_name}.cmd_statusconsults the map first when annotating orphans; falls back to cmdline--sessionarg. Bonus: fixed a latent bug insession::session_daemon_pidwhere a legacy bare-integer pidfile silently returned None because the JSON parser swallowed it as a Number without a.pidfield. -
wire supervisorCLI (#178). New top-level command.wire statusanswers "is THIS session syncing?";wire supervisoranswers "what is the supervisor (and every session's daemon) doing across the box?". Pretty output collapses to one summary line when every session is healthy; JSON emits the full per-session topology. Closes honey-pine's BUG 3 ("wire daemon status CLI") ask. -
Supervisor surfaces stale-binary daemon sessions (#198). Caught via coral's 2026-06-01 dogfood probe: 1 of 10 sampled session daemons was still running v0.13.5 while the CLI was v0.14.1. Supervisor's existing-pidfile check intentionally protects alive daemons from respawn (would interrupt in-flight syncs), so mid-upgrade fleets accumulate version-drifted children silently.
SupervisedSession.daemon_versionreads from the JSON pidfile;SupervisorState.stale_binary_sessionsis the derived list (alive + recorded version<CARGO_PKG_VERSIONvia a hand-rolled dotted-integer compare so0.9.0 < 0.10.0comes out right).wire supervisorpretty + JSON both surface; operator cankill <pid>to let the supervisor respawn on the fresh binary. Auto kill-and-respawn deferred — keeps operator-controlled timing. -
🚨 Cross-process toast dedup (#179). Caught via live operator complaint within minutes of #176 landing. The 134-daemon supervisor turned the existing in-process
Mutex<HashMap>toast dedup into theater — every daemon polled its own inbox, every daemon fired its own toast, the operator saw the same notification 134 times within seconds. Fix: cross-process atomic claim viaO_CREAT|O_EXCLon a sha256-named touch file under<cache_dir>/wire/toast-dedup/. Once a key is claimed, no wire process anywhere on the host re-emits — ever. Baretoast()now defers totoast_dedup()with a content-hash key so the 5 legacy bare-toast sites inherit the cross-process guarantee without per-site changes.
-
wire sendreturns the actual relay verdict synchronously (#187). Pre-fix, everywire send(CLI and MCP) wrote to<outbox_dir>/<peer>.jsonland returnedstatus: "queued". The daemon's 5s push loop later POSTed to the relay. Three distinct silent-drop classes hid in those steps: outbox-write-without-push (daemon dead, on wrong WIRE_HOME, TLS broken), stale-slot-token (peer rotated, half-paired), content-hash-dedup-blocks-retry. New default:wire sendPOSTs directly, returnsdelivered/duplicate/peer_unknown/slot_stale/transport_errorinline, exits 2 on any non-delivered status.--queueCLI flag (andqueue: trueMCP arg) opts back into the legacy outbox→daemon-push path for offline-buffer / batch / pre-pair queueing. Newsrc/send.rsmodule extracts the delivery primitive (SyncDeliveryenum +attempt_deliver) shared by both CLI and MCP. Pushed-log append on the sync path keepspending_push_countaccurate across both paths. -
MCP
wire_pull— symmetric receive primitive (#189). Paul (post-#187): "This is the same on pull as push now?" CLIwire pullhas always been a sync GET-from-relay that drains the slot, verifies signatures, writes inbox, returns counts inline. The MCP surface had no equivalent — agents calling tools through Claude Code / Pi / OpenCode could only wait for the daemon's 5s pull cycle. Newwire_pullMCP tool callscli::run_sync_pull()directly; same code path the daemon uses. Returnswritten[]/rejected[]/total_seen/cursor_blocked/endpoints_pulled. Agents in tight loops no longer sleep 5s between turns.
- Pi + OpenCode adapters in
wire setup(#185). Pi (https://pi.dev) uses the standardmcpServersshape — targets$PI_CODING_AGENT_DIR/mcp.jsonor~/.pi/agent/mcp.json. OpenCode (https://opencode.ai) uses a custom shape: top-levelmcp.<name>(nomcpServersroot, nomcp.serversintermediate), withtype: "local", combinedcommand: ["wire", "mcp"]array, andenabled: true.upsert_mcp_entrydetects OpenCode by path (opencode.json/opencode/opencode.json) and re-shapes the entry on write. Targets:$XDG_CONFIG_HOME/opencode/opencode.json,~/.config/opencode/opencode.json, project-localopencode.json. Closes the docs→code gap from v0.14.1's PI.md + OPENCODE.md.
Coral's 2026-06-01 dogfood found three status surfaces (wire status, wire peers, wire here) reporting different tiers for the same wedged peer + a wire send --queue path that silently accepted writes to peers that could never receive them. Four PRs tighten the surface:
-
Per-peer pending-push attribution (#199).
pending push: 3 event(s)was unactionable on its own — operator had to manually walk per-peer outbox files to learn which peer was wedged. Newconfig::compute_pending_push_breakdown()returns one entry per peer with a tier and the unpushed event count, usingtrust::effective_tierso trust-promoted-but-handshake-incomplete peers show asPENDING_ACKnotVERIFIED. CLIwire statusexpands the line with a tier-keyed hint (pair never completed; daemon won't push until accept/reject). MCPwire_statusgainsdaemon.pending_push_breakdown.effective_tiermoved fromcli.rstotrust.rsso any future surface gets the canonical answer. -
wire send --queuewarns on unpushable peer (#200). Coral's outbox carried a year-oldno-such-peer.jsonlfrom a typo'd send — CLI exited 0 withqueued ... daemon will pushbut the daemon had nowhere to push.--queueis the documented pre-pair-best-effort path, so refusing isn't right, but silent-success is a lie. CLI emits a stderr WARN (JSON stdout untouched) when the peer is BOTH not pinned in trust AND has no pending pair (outboundPendingPair.peer_didmatch or inboundPendingInboundPair.peer_handlematch). MCPwire_sendadds a structuredwarningfield on the queued response so agents can branch. -
wire hereuses effective tier (#201). Same surface drift caught the third status command:wire herewas reading rawagent.tierfrom trust.json, so orchid-savanna showedVERIFIEDhere whilewire peersandwire statusshowedPENDING_ACK. Route throughtrust::effective_tierso all three surfaces agree. -
wire doctorsurfaces pre-#171 stalepending_inbound(#202).wire status --jsoncarriedpending_pairs.stale_inbound_handles: [...]for VERIFIED peers with leftover records from before #171's bilateral-completion cleanup. The pretty status surface filtered them out (intentional) but operators had no command pointing at the cleanup. Newstale-inbound-pairsdoctor check (8th of 8) emits WARN with chainedwire reject <handle>commands in thefixstring. -
MCP
tool_peersuses effective tier (#205). The CLI's three status surfaces (status/peers/here) all routed throughtrust::effective_tierafter #199 + #201;tool_peerswas the remaining holdout still emitting raw trust-promoted VERIFIED. Agents callingwire_peersvia MCP got a different answer than the same operator runningwire peers— automation built on the MCP shape would believe a wedged peer was healthy + try to send. Fixed.
-
daemon-streampreserveslast_event_at+reconnect_countacross reconnect (#204). Every successful SSE reconnect overwrotestream_state.jsonwithlast_event_at:null, reconnect_count:0. The "connected" writer insideconnect_and_readhad no access to the accumulated state in the outerrun_subscriberscope, so the file always reset the moment a fresh connection landed. Operator surface always readlast event neveron long-running daemons even after events had arrived. Threadedaccumulated_last_event_at+accumulated_reconnectsthrough the inner so the "connected" write preserves both — same pattern the outer writers already use. -
Daemon-written sender-side staleness signal (#207, closes #14). Extends
relay_state.peers[<handle>]withlast_inbound_event_at(RFC3339), stamped by the daemon on every successful pull where that peer was the sender.check_peer_stalenessnow prefers this field over inbox file mtime (which breaks on backup/restore,cp -a,touch, FAT32 resolution); falls back to mtime when the field is absent so upgrades are graceful. The asymmetric-stale-pin case #14 surfaced — push-to-dead-slot returns 200 OK + peer never sees us, both sides report green — now flips the doctor WARN with a daemon-side signal mtime can't corrupt.
wire upgradeno longer fork-execs a redundant transient daemon (#186). Pre-fix,wire upgrade --localoutput includedwire upgrade: spawned fresh daemon (pid N v0.14.1)even when launchd was about to (re)start the--all-sessionssupervisor on the new binary. The transient daemon lived ~30s before the supervisor's singleton-guard no-op'd it; operators saw the wrong pid as "the long-lived owner". Now readswire upgrade: daemon refresh deferred to launchd supervisor (will spawn within 10s)when the OS bootstrap succeeded. Safety net intact: foreground spawn still fires when no service is installed or bootstrap failed.
-
wire enroll republishrefreshescapabilities[](#172, closes #126). slate-lotus's v0.14.1 audit found that republish bumpedschema_versionv3.1 → v3.2 but leftcapabilities=["wire/v3.1"]stale, opening a stealth-skip vector for any future cap-gated feature.rebuild_card_with_current_claimsnow refreshes the wire/* entry to the binary's currentCARD_SCHEMA_VERSIONwhile preserving operator-defined non-wire caps (custom task tags, future feature flags) in their original order. -
wire enroll add-membership— ingest an org-issued cert (#206, closes #127). slate-lotus's same audit also flagged a load-bearing DX hole:wire enroll org-add-memberprinted the{org_did, org_pubkey, member_cert}bundle but the receiver had no verb to store it. Joining an org required hand-editing<config>/wire/memberships.json— exactly the friction the offline-minimal RFC-001 subset was meant to remove. New verb accepts either--bundle '<verbatim-json>'or the three flags separately, verifies the cert againstorg_pubkey+ this op_did viaidentity::verify_member_certBEFORE storing (a wrong-key bundle now fails at ingest, not at next republish), refuses when this operator isn't enrolled, and is idempotent on the org_did key. 4 lib tests covering happy-path + the 3 distinct rejection branches.
- AC-HP7 heartbeat body roundtrip proptest (#160). Property-based test verifying that heartbeat bodies encode → relay → decode → re-encode → decode losslessly. WILLARDKLEIN.
- Re-run-friendly CI flake noted.
uds_request_round_trips_200_with_bodyhit again in this batch (the 4th observed instance perfeedback_uds_round_trips_ci_flake). Recommend dedicated investigation alongside the dual-roots TLS work (#177); not yet a release blocker.
wire upgradeAUTOMATICALLY refreshes installed service units (rewrites plist / systemd unit with the new ProgramArguments). 0.14.1 → 0.14.2 operators don't need to manually re-runwire service installunlesswire upgradeis skipped.- The
--all-sessionssupervisor manages every session by default. To opt out (e.g. one specific session running in a tmux pane), the operator-spawnedwire daemonclaims the pidfile first and the supervisor's pre-spawn check honors it. - Notification dedup state survives across daemon restarts. To re-see a notification class:
rm -rf ~/Library/Caches/wire/toast-dedup(macOS) or~/.cache/wire/toast-dedup(linux).
The batch that took the supervisor work from "lands" to "safe to put in front of strangers."
wire upgrade --refresh-stale-children(#209). Companion to #198's stale-binary detection: force-reaps session daemons stuck on the old binary so an upgrade actually converges the whole box, instead of leaving version-drifted children alive behind the supervisor's respawn-protection.- 🚨
--all-sessionsfork-storm fix — idle filter (#212). The supervisor spawned one daemon per session home, and a long-lived box accumulates hundreds of ephemeral persona homes (one per Claude tab /wire session new) → 100+ daemons for a handful of real sessions.supervisor_eligiblenow keeps a session only if it has a registry cwd binding OR synced within an idle cutoff (WIRE_ALL_SESSIONS_MAX_IDLE_DAYS, default 7;0disables).list_sessions()untouched — filter applied only at the supervisor call site. - Hermetic kill-switch tests — suite de-flake (#213). Four
os_notifytests mutatedWIRE_HOME/WIRE_NO_TOASTSwithout the sharedENV_LOCK, racing every other test under the default parallel runner (a different subset failed each run;--test-threads=1masked it). Routed throughwith_temp_home; 20× parallel runs now 0 failures. - REUSE-compliant trio-license + stale-doc fix (#214). The AGPL-server / Apache-spec / MIT-client split was sound but
LICENSE.mdstill scoped it by pre-Rust-rewrite Python paths, and GitHub couldn't classify it (NOASSERTION). Texts moved toLICENSES/, aREUSE.tomlencodes the per-path mapping, docs corrected. - README launch hardening (#215). Status / API-stability section (pre-1.0, pin versions,
--jsonis the stable surface), a realCONTRIBUTING.md(gates, DCO sign-off, per-component license), and good-first-issue pointers. - Fresh-user install-smoke CI + reproducible Rust test-env (#217). New CI job builds the PR binary, installs it to PATH, and runs the offline first-run sequence from a clean
WIRE_HOME— catches breakage that compiles + passes tests but breaks the out-of-the-box experience. Plustest-env/— a pinned-toolchain container that runs the exact CI gate against a mounted checkout. - Dead-code sweep (#218). Dropped the unused
towerdirect dep, fixed the two real dead doc links, and fixed abash -lcPATH bug in the test-env container's default gate command.
- After upgrading to 0.14.2, the
--all-sessionssupervisor will spawn daemons only for cwd-registered or recently-active sessions. If you rely on a long-idle unbound session keeping a daemon, setWIRE_ALL_SESSIONS_MAX_IDLE_DAYShigher (or0to disable the filter).
v0.14.1 — v0.14.x DX completion: identity layer visible end-to-end, operator quality-of-life fixes.
Closes every documented v0.14.0 follow-up plus operator-felt UX gaps surfaced during heavy dogfooding. 12 PRs since the v0.14.0 tag, no trust ladder change, no protocol bump (v3.2 was already the constant). All cards remain backward-compatible with v3.1 readers.
- CLI op-claims surfacing (#114).
wire whoami/wire peers/wire whoisJSON + human output now surface inlineop_did/op_pubkey/op_cert/org_memberships/schema_versionwhen present on the stored card. Pre-patch: the marquee v0.14 identity layer existed on disk but was stripped by every read surface — operators couldn't tell fromwire whoami --jsonwhether enrollment had taken. Single helpercli::op_claims_from_cardcentralizes the field list; pre-v0.14 / unenrolled cards surface identically (no JSONnull-spam). - MCP op-claims surfacing (#115).
mcp__wire__wire_whoamiandmcp__wire__wire_peersextend the same shared helper, closing the parallel-serializer gap (CLI and MCP had separate hard-coded JSON shapes that both stripped op_*). - MCP
wire_whoisbare-nick (#122). Previously rejected bare nicks withmissing '@' separator, even when the nick was a pinned peer or local sister already in the trust ring. Now routes throughcli::resolve_name_to_targetfirst (mirroringwire whois <name>in the CLI), federation handles fall through unchanged. Closes the agent-discovery surface for v0.14's marquee identity layer. schema_versionwrite-side bump (#121). Cards carrying op claims now emitv3.2(was stuck atv3.1despite v0.14 fields). Bumps atagent_card::with_identity_claimsvia the newmax_schema_versionhelper. Monotonic — a card already atv3.5(hypothetical future extension peer) is NOT downgraded. Numeric parse sov3.10 > v3.2(not lexicographic). Readers can now discriminate "carries op claims" from the version field alone.wire enroll republish(#110). Rebuilds the stored card with the current enrollment state and republishes. Closes the enroll-after-initDX gap: claims are normally attached at card-build time, but an operator who enrolls AFTERinithad a stored card that pre-dated the claims. Idempotent: not-enrolled rebuilds a claims-free card; not-bound prints "local only".
wire quiet on/off/status(#117). Operator kill switch for desktop toasts. File-based (<config_dir>/quiet) for per-session silence + env-based (WIRE_NO_TOASTS=1) for launchd-spawned daemons. Both check at the singleos_notify::toastguard — disabled means disabled, no dedup leakage. Status reports mechanism (via file/via env/none).wire upgradewarns about stalewire mcpserver subprocesses (#123).wire upgrade --localswaps daemons but not thewire mcpserver subprocesses that Claude Code / Claude.app pin at session start (macOS mmap semantics keep the old code in already-open processes when the file path is replaced). Now--check, action-run, and the--jsonshape surface the stale MCP pid list with explicit "each Claude tab must/mcpreconnect" guidance. The MCP procs are NEVER added to the kill set — killing them would disconnect every tab's wire toolset until each one explicitly reconnects. Warn-only, behavior unchanged.- Drop redundant
WIRE_SESSION_IDenv mapping inwire setup(#124). Closes the MCP Config Diagnostics validator warningMissing environment variables: CLAUDE_CODE_SESSION_ID. Modern Claude Code (verified 2026-05-30) propagatesCLAUDE_CODE_SESSION_IDinto every MCP subprocess by default; wire'ssession::resolve_session_keyreads it natively as a fallback. The historical mapping{"WIRE_SESSION_ID": "${CLAUDE_CODE_SESSION_ID}"}was duplicating a value wire already had AND triggering the validator warning. Dropped from thewire setuptemplate; the existing fallback chain preserves per-session identity exactly. - Notify-mode wiring (#112 → shipped via #113 by branch-state accident). The receive-side
notifyorg-policy mode parses toFileOrgPolicy::Notifyand routes the pending-stash toast through an enriched org-aware path (notify-pair:<handle>dedup key, "org-verified pair request from<handle>" wording). Pre-patch:notifyparsed-but-unwired since v0.14.0; the generic toast fired regardless. Default-deny +ORG_VERIFIEDceiling + auto-wins-over-notify property all preserved.
- CI serializes test threads (#111).
cargo test --all-targets -- --test-threads=1eliminates heavy-e2e parallel-self-contention (a busy-polling subprocess-spawning e2e was starving sibling real-daemon e2es under--test-threads=$(nproc)). - Dead code removed (#118).
signing::strip_did_wire(added with#[allow(dead_code)]"kept for v0.6 — once a caller exists" comment) was never adopted;agent_card::display_handle_from_didcovers the same need. Repo#[allow]count 4 → 3. - Repo-wide format-arg modernization (#119).
cargo clippy --fix -- -W clippy::uninlined_format_argsacross 14 files:format!("{}", x)→format!("{x}")and the same forprintln!/eprintln!/assert_eq!/panic!/anyhow!/bail!/write!. Net −16 LOC. - RFC-001 typo fix (#109). "doubled" → "tail length quadrupled (8 hex → 32 hex)" in the rationale for op/org DID suffix width.
- SSO connectors prompt (#113 + #116).
docs/PROMPT_v0.15_sso_connectors.md— self-contained, paste-able prompt for the v0.15 SSO-connector buildout (auth flow runner + token lifecycle + verify + group/role enumeration + SCIM 2.0 ingest + deprovisioning hooks + CLI + card-emit + receive-side branch). 21 providers spec'd; 9 hard constraints; 12-PR landing order. Iterated via #116 to fold in the v0.14.x regression-debug lessons (CLI+MCP serializers must stay in lock-step via the shared helper; dual-surface JSON-RPC tests; post-/compactbranch-state verification; MCP-server-pin inwire upgrade). - Repo-scrub prompt (#120).
docs/PROMPT_repo_scrub.md— durable artifact encoding the bounded-cleanup discipline (5-phase order of operations, per-PRdonedefinition, suggested cuts by priority, anti-patterns to instant-reject, stop conditions).
Lib: 341 → 355 tests (+14 across the surfacing + identity + cleanup PRs). No release-surface protocol change. No trust ladder mutation. Six cross-platform binaries (aarch64-apple-darwin, x86_64-pc-windows-msvc, aarch64-unknown-linux-{gnu,musl}, x86_64-unknown-linux-{gnu,musl}) + .sha256s built via release.yml on tag push.
v0.14.0 — RFC-001 identity layer: operator + organization + project, fully-offline self-certifying.
Ships the offline-minimal subset of RFC-001 — three optional, orthogonal-axis claims on the agent-card (op_did, org_memberships[], project), a new tier ORG_VERIFIED between UNTRUSTED and VERIFIED, and the smallest receive-side surface that closes the N²-pair-discovery problem inside trust scopes without weakening the v0.5.14 phonebook-scrape closure or the bilateral SAS invariant.
The shipped design is fully-offline self-certifying. Each card carries op_pubkey and a per-membership org_pubkey inline; each operator/org DID is a hash commitment to its key (agent_card::long_fingerprint = first 16 B of sha256(pubkey)), so an attacker can't substitute an inline pubkey without breaking the DID match. identity::verify_op_cert / verify_member_cert take the inline pubkeys directly — no resolver, no registry, no did:web, no DNS-TXT, no /v1/org/claim on the pairing hot path. (Those = v0.15, per the "Implementation status (as-built, v0.14)" note added to the RFC in #106.)
wire enrollCLI (#99, #102). Three subcommands:wire enroll op --handle <h>mints this machine's operator root key + printsop_did;wire enroll org-create --handle <h>mints an organisation root key + printsorg_did;wire enroll org-add-member <op_did> --org <org_did>issues a membership cert binding the operator to the org and stores it locally. Keys saved 0600 underconfig/wire/{op.key, orgs/<sanitized>.key}; memberships persisted inconfig/wire/memberships.json.- Card-emit wiring (#103, #104). When an enrolled agent runs
wire init/pair_session::init_self, the card-build path attachesop_did+op_cert+op_pubkey+ every stored membership inline before signing. Fail-soft: a corrupt identity config (unparseablememberships.json, etc.) degrades to "no claims" + a stderr warning rather than breaking card-build —init/upare critical-path and must never fail because of a stored-config parse error. Not-enrolled cards are byte-identical to v0.13 cards; v3.2 readers tolerate both schemas. - Receive-side auto-pin (#101). When a peer pair_drop arrives carrying a v3.2 card with a verified membership in an org the receiver has explicitly opted into (
config/wire/org_policies.json→{"orgs":{"<org_did>":{"inbound":"auto"}}}), the receiver auto-pins the peer atORG_VERIFIEDand emits the pair_drop_ack — bypassing the default-deny pending gate for that org. Fail-closed: an empty / missing / malformed policy file → no auto-pin → default-deny intact. A peer whose membership doesn't commit to the inline org_pubkey (substitution attempt) is rejected atevaluate_card_membership. Tier::OrgVerified<Tier::Verified(#90). Strict bytrust::tier_order(NOT a derivedOrd). An auto-pinned org-mate satisfies>= ORG_VERIFIEDpolicy checks but NOT>= VERIFIED— bilateral SAS (SPAKE2 invite path) or thewire add/pair-acceptgesture is still the only path to VERIFIED. Property-tested intests/trust_ceiling_prop.rs.- File-backed org policy (#95, #98). Minimal receiver-side per-org store. Two modes:
auto(auto-pin on contact, wired in v0.14),notify(eligible — designated UI surface, parsed but not yet wired into the live receive path; lands in v0.14.x). Empty/malformed → empty policy → no easing. - SSO provider-adapter trait (#100). Pluggable seam (
SsoProvider::normalize) over Google (hd), Azure AD (tid), Keycloak (realm), and a generic IdP — claims-shape normalization only. The verify path (binding an SSO identity to anop_didvia a relay attestation) is deferred to v0.15 per the SSO amendment. - Live two-process e2e (#105 + #107).
tests/e2e_org_verified.rsdrives the realwirebinary across twoWIRE_HOMEs + an in-process relay: A enrolls op+org+self-membership →wire init(card carries claims) → A dials B → B (withorg_policies.jsonauto-trusting A's org) pulls → B auto-pins A atORG_VERIFIEDpurely from the offline membership. Negative control: non-member dialer still gated to pending.#[ignore]d (run viacargo test --test e2e_org_verified -- --ignored --test-threads=1) — heavy real-process e2e + a gentle 750 ms poll cadence to avoid starving the other real-daemon e2e binaries undercargo test --all-targets. - A2A interop docs (#91 — @dthoma1 / swift-harbor).
docs/a2a-extension/wire-identity-v1.mdformalises wire as an A2A v1.0 AgentCard extension (URIhttps://slancha.ai/wire/ext/v0.5);docs/did-methods/did-wire-method.mdis thedid:wiremethod specification covering session / operator / organisation shapes (<handle>-<8hex>and<handle>-<32hex>ofsha256(pubkey));docs/PROTOCOL.mdaudited through v0.13.5 with the v3.2 additions inlined; an introductory blog atdocs/blog/wire-and-a2a.mdcovers what wire adds on top of the A2A floor. - RFC-001 as-built alignment (#106). Added "Implementation status (as-built, v0.14)" demarcating built (offline-minimal) vs deferred (DNS-TXT /
did:web/ wireup registry / roster bundle / SSO verify / cross-relay → v0.15). Fixed the §1 card snippet that had omitted the inlineop_pubkeyand per-membershiporg_pubkey.
Net: an operator who runs the three wire enroll commands once on each machine, then sets a one-line org_policies.json opt-in for any org they're willing to auto-pair with, eliminates the SAS dance for every session-pair inside that org — bilaterally, offline, on the v0.13 mailbox substrate. Wire-format additions are ≤ 2 KB per card, backward-compatible with v3.1 readers; nothing on the relay protocol changed.
Known follow-ups (v0.14.x): wire enroll … --republish (or rebuild-on-enroll) to close the DX gap that enroll-after-init doesn't republish claims today (card is built at init only); wire notify mode into the live receive surface; serialize the heavy real-daemon e2e binaries behind a dedicated -- --ignored --test-threads=1 CI step.
v0.13.5 — Reliable per-session identity (the PID-file adapter) + unexpanded-${} guard.
v0.13.4's env-forward (WIRE_SESSION_ID=${CLAUDE_CODE_SESSION_ID}) proved unreliable: Claude Code only expands ${} when the var is in its OWN env, which on a clean top-level terminal (esp. Windows CC 2.1.150) it is NOT — so CC passes the LITERAL string, which wire hashed into ONE fixed identity (every session in every folder collapsed onto one persona). Two fixes:
${...}literal guard —resolve_session_keytreats any unexpanded${...}value (and empty) as unset, so it never hashes a placeholder into a shared identity.- Claude Code PID-file adapter (thanks @WILLARDKLEIN, #56) — when the session id isn't in the env, wire walks its parent-process chain to the owning
claudeprocess and reads~/.claude/sessions/<pid>.json→sessionId. Deterministic, race-free, zero env/handshake dependency, cross-platform — validated on Windows (3 concurrent terminals → 3 distinct personas) and macOS. The MCP server now recovers the SAME session id the CLI uses, so CLI and MCP unify on one per-session identity, stable across reconnects.
Net: true per-session identity everywhere, even when Claude Code doesn't put the session id in the MCP env. Ships a reference cross-platform proxy shim (contrib/wire-mcp-proxy.py) for builds without the native adapter.
v0.13.4 — Per-session identity (MCP + Windows), statusline fix, group chat, wire update.
- Per-session identity, fixed on the MCP path — the Windows "same persona every session" bug. Claude Code sets
CLAUDE_CODE_SESSION_IDfor Bash-tool subprocesses but NOT for the stdio MCP server, and the MCPinitializehandshake carries no session id — so the MCP server had no per-session signal and fell back to cwd-detection, collapsing every Claude session under a shared dir (~/Source,C:\Users\<user>) onto ONE identity. Two fixes: (1)wire setupnow writes the MCP entry with"env": {"WIRE_SESSION_ID": "${CLAUDE_CODE_SESSION_ID}"}, which Claude Code expands into the MCP env at launch (validated on Win10 — a fresh session resolves to a distinct by-key persona); (2) cwd resolution is removed everywhere — the MCP mints a distinct per-process identity when no session id is present, and the CLI never cwd-resolves. Identity is the session, period. Re-runwire setup --applyon this version — and note a PROJECT-scoped.mcp.jsontakes precedence over the global config, so make sure the env block landed in the file your host actually uses. - Statusline shows the session's own persona. The bundled renderer bridges the
session_idClaude Code passes on STDIN intoWIRE_SESSION_IDbefore callingwire whoami, so the bottom-of-terminal persona matches the session (was resolving a cwd default / nothing). - Group chat —
wire group create / add / send / tail / list / invite / join+ MCPwire_group_*tools. A group is a shared relay-room slot; the creator-signed roster carries each member's key (introduce-on-vouch), so members verify each other without pairing.invitemints a self-contained join code; redeemers land at Introduced tier. wire update≡wire upgrade— one verb (alias). Always checks crates.io; installs a newer release if there is one (cargo install, else prebuilt + SHA-256 self-replace), then does the atomic daemon swap.--checkreports;--localskips the fetch.- CI: GitHub Actions bumped off deprecated Node 20 (checkout@v5, upload-artifact@v7, download-artifact@v8).
- Docs: AGENTS.md + AGENT_INTEGRATION.md now document the v0.13 session-keyed identity model (were the stale pre-v0.13 cwd model).
v0.13.3 — Group chat + one update command.
wire group— bidirectional group chat.create / add / send / tail / list. A group is a shared relay-room slot: the creator allocates one slot and its token is the room key, distributed only to vouched members; everyone posts + pulls that one slot (no relay change, no daemon auto-rebroadcast, no per-member credential mesh — chosen because aslot_tokenis a read+write credential, so direct member-to-member delivery would leak each member's personal mailbox token). Membership is a creator-signed roster with aGroupTier(creator/member/introduced) that is a SEPARATE axis from bilateral peer trust.addtakes a bilaterally-VERIFIED peer (T22 consent) and distributes agroup_invitecarrying the signed roster + room coords. On ingesting an invite a member introduce-pins every other member — adds their key to trust at bilateral UNTRUSTED so their group messages verify, WITHOUT granting bilateral trust and never lowering an existing tier (the axes stay disjoint). Net: members who never paired with each other can post to the room and read each other's messages with a verified signature, vouched by the creator's signature in place of a direct SAS handshake. Verified live on wireup.net + by an e2e star-topology test (one member reads another's message verified though they never paired).wire update≡wire upgrade— merged. One verb (updateis an alias). It ALWAYS checks crates.io; if a newer stable release is published it installs it (cargo install slancha-wirewhen a Rust toolchain is on PATH, else downloads + SHA-256-verifies the prebuilt release binary and self-replaces — toolchain-free), then runs the atomic daemon swap so the restart picks up the new binary. No newer version → it skips the install and just restarts. A crates.io/network failure degrades to a warning and never blocks the restart.--checkreports the available update + the processes that would restart without acting;--localskips the crates.io check (offline / local dev build).
v0.13.2 — Windows hardening + wire setup --statusline. Three Windows bugs (found by a paired Windows session dogfooding v0.13.1 over wire) plus the long-missing persona statusline and removal of the dead reactor:
- relay.json torn-write fixed (CRITICAL). A foreground
wire dialracing the background daemon corruptedrelay.json: both did a non-atomic, locklessfs::write, interleaving into invalid JSON ("trailing characters at line N") that broke ALL push/pull until hand-repaired.write_relay_statenow writes via tmp+rename and holds the existingrelay.lockflock (the RMW path calls an unlocked inner to avoid re-entrant deadlock). The race was cross-platform; Windows file-sharing semantics made it easy to hit. wire status/doctorfalse-DOWN fixed. Daemon-liveness had duplicate Linux/Unix-only copies (ensure_up::pid_is_alive,daemon_liveness'spgrep,ensure_up::process_alive,pending_pair::process_alive) runningkill -0/pgrep— absent on Windows → always false → daemon reported DOWN while alive. All now route through the already-Windows-correctplatform::process_alive/find_processes_by_cmdline(tasklist / PowerShell CIM). Same root cause behind thewire up/upgrade500 ms self-spawn probe orphaningwire.exe.wire setup --statusline. Installs a Claude Code statusLine showing your persona — liveness dot + emoji + nickname in the persona's accent color + cwd (● 🪻 bright-camellia · ~/project). Writes a bundled renderer + merges astatusLineblock into settings.json (preserves keys, refuses to clobber invalid JSON, idempotent,--removeto uninstall, honors$CLAUDE_CONFIG_DIR). Closes the gap where personas existed but nothing installed the statusline that displays them.wire reactorremoved. Theclaude -pshell-out reactor was superseded by live-session monitoring + auto-reply baked into the MCP instructions; removed the command, handler, helper, and landing section.- Same-box discovery fixed (v0.13 regression). v0.13 moved session homes under
sessions/by-key/<hash>, butlist_sessionsonly scanned the top level (so every v0.13 session was invisible towire session list-local/pair-all-local), andsessions_root()'s inside-session fallback only walked one level up (so an inside-sessionWIRE_HOMEresolved to a nonexistent nested dir). Both fixed:list_sessionsdescends intoby-key/(labeling each home by its persona), andsessions_root()walks up to the nearestsessionsancestor. Same-box sisters are visible to each other again. (The broader in-band local-pairing UX — additiveadd-peer-slot, loopback-relay dial, a leak-safe same-box pair verb — is tracked separately indocs/V0_13_2_PLATFORM_HARDENING.md.) wire upgradeis now session-scoped — fixes daemon accumulation on Windows (critical). Repeatedwire upgradespawned fresh daemons without killing the old ones (glossy-magnolia: 2→5→8→11 over three cycles — real multiple daemons racing the pull cursor). The old design was box-wide (kill everywire daemonprocess found, wipe every session's pidfile, respawn every session), which is wrong for a multi-session / shared-relay box AND broke on Windows: the CIM scan can't match the quoted"...\wire.exe" daemoncommand line (no contiguouswire daemon), so it found nothing to kill, then the respawn loop accumulated.wire upgradenow refreshes only THIS session: it kills this session's own daemon via its pidfile pid (reliable, CIM-independent) plus any TRUE orphans (wire daemonowned by no session), and SPARES sibling sessions' daemons and the shared127.0.0.1:8771relay-server (killing it would break every same-box session's loopback). Each session refreshes itself on its ownwire upgrade.wire monitorno longer dies silently. wisp-blossom sawwire monitorexit 1 with zero output when a cursor-block (untrusted signer's pair event) tripped the watcher — indistinguishable from "still watching." The poll loop now surfaces the error to stderr and keeps watching instead of exiting on a swallowed?.- Re-dial no longer clobbers a peer's local endpoint or bleeds the federation token (E3-dial).
wire dial peer@relay(→cmd_add) REPLACED the whole peer entry with a flat federation-only one and seeded the federation token from the entry's top-levelslot_token. After a prior localadd-peer-slot, that top-level token was the LOCAL token — so re-dialing made the federation endpoint inherit a stale local bearer (federation delivery would 401), and dropped the local endpoint entirely. Now the federation endpoint is merged additively intoendpoints[](local preserved), and its token is seeded only from a prior federation endpoint on the same relay (re-dial of an already-acked peer), never a local one — empty until thepair_drop_acklands otherwise. (glossy-magnolia pinpointed the re-pin path; add-peer-slot itself was innocent.) wire add-peer-slotis now additive (E3). It used to REPLACE the whole peer entry, so pinning a local loopback slot clobbered the peer's federation endpoint — the peer became loopback-only and lost its public route (glossy-magnolia + wisp-blossom repro). Now it merges into the peer'sendpoints[](upsert by relay_url), mirroringbind-relay's additive semantics, so a local slot ADDS to the federation route instead of dropping it.- Orphan-daemon detection is session-scoped (A2). On a multi-session box (wire's core use case) every session runs its own daemon, but the orphan check flagged any
wire daemonwhose pid ≠ this session's pidfile as an orphan — so sibling sessions' legitimate daemons showed as orphans,wire doctorFAILed on a healthy shared box, andwire upgradewould cross-session-kill a sibling's daemon. A true orphan is now a wire daemon owned by NO session: detection excludes every session's pidfile pid (session_daemon_pidacrosslist_sessions), not just the current one. - Daemon now services ALL slots, not just the primary (E2).
run_sync_pull(the background daemon's pull) only pulledself_primary_endpoint— the federation slot — so a session that additively bound a local loopback slot never had it serviced by the daemon (same-box loopback messages silently undelivered until a manual restart re-seeded the startup-only stream subscriber). Now it pulls every self endpoint with an independent per-slot cursor (self.cursors.<slot_id>), one endpoint's failure doesn't stall the others, and the legacy global cursor stays in sync with the primary for back-compat. (Manualwire pullwas already multi-slot; this brings the daemon in line.) - No more phantom "?" sisters in
list-local(E8).maybe_adopt_session_wire_homecreated a session home unconditionally on every resolution — before any identity existed — so transient/probe session keys left permanent empty homes that surfaced as phantom handle-less sisters (degrading the same discovery the by-key fix restored). Homes are now created lazily on first real write, andlist_sessionsskips homes with no agent-card.
v0.13.1 — one name, one command. Identity UX simplified; the last "same handle" leak closed. A persona review found the one-name promise (v0.11) was still violated in several places, and that the real fix was to stop letting anyone type a name they will not get.
- One-name now holds on EVERY init path.
init_self_idempotent— the auto-init used bywire claim, MCPwire_init, and all pairing — previously used the machine hostname (default_handle()) as the handle and never applied the persona derivation thatwire initdid. Result: every auto-initialized session on a box becamedid:wire:<hostname>-<fp>, all displaying the same hostname after the fp-strip — a second, more visible root of the Windows "every new session has the same handle" bug (v0.13 fixed the colliding HOME; this fixes the colliding displayed name). Both branches now derive the persona from the keypair fingerprint, so distinct sessions always get distinct fp-derived personas. Re-init with a different typed handle is now an idempotent no-op (the typed handle is vestigial) instead of an error. - Onboarding is one nickless command.
wire up [relay]does everything (init + bind + claim your persona + local dual-bind + daemon) and no longer takes a<nick>— your handle is your DID-derived persona, so there was never a name to type. Accepts@wireup.net, a bare host, a full URL, or nothing (defaults to the public relay). wire init,wire claim,wire identity publishare hidden. All three accepted a name the one-name rule ignores — terrible UX (you typealice, you getwinter-bay; worse, on a fresh box the ignored name could be the invalid hostname and the command would fail). They are folded intowire upand removed from--help, kept callable for scripts/offline keygen.wire init's handle arg is nowOption(None= no typed name);wire claim/wire_claimcoerce any typed nick to your persona (MCPnickis now optional + advisory).landing/install.shwas stale. The installer embedded in the relay and served atwireup.net/install.shwas an older, separate script showing a 3-stepinit→claim→addflow with the deprecatedwire addverb — the first thing every new user saw, contradicting the model. Now byte-identical to the canonical rootinstall.sh(one-commandwire up, canonicalwire dial, Windows MSYS/Cygwin detection, post-installwire upgradestale-cleanup pass).- README quick-start rewritten around
wire up+ the one-name model (dropped the pre-v0.11wire init alice→winter-bay (alice)two-name example).
v0.13 — session-keyed identity. Replaces the cwd-registry + machine-wide-default session model with a host-agnostic session-key chain (WIRE_SESSION_ID > CLAUDE_CODE_SESSION_ID > legacy cwd-detect). Each session resolves to a unique, deterministic, cwd-independent WIRE_HOME (sessions/by-key/<sha256(key)[:16]>), so two sessions can never collapse onto a shared default. Fixes the Windows "every new session gets the same handle" bug at the root — there is no path string to mis-normalize or miss.
- MCP startup auto-bootstraps a fresh session home once (one-name init + federation slot + phonebook claim), so each session is its own reachable, claimed identity. Gated on
WIRE_MCP_SKIP_AUTO_UP+ already-initialized; best-effort on network. - Behavior change: two windows in the same project are now DISTINCT identities (previously shared, via the bug). Existing sessions re-key on first run under v0.13.
- Deferred: migration bridge, GC of orphaned session homes (see the design spec). The Windows fix is provisional until verified on a real Windows box.
Design: docs/superpowers/specs/2026-05-24-session-keyed-identity-design.md.
v0.12.3 — auto-collaborate, baked in. The MCP server instructions (shipped in the binary, read by any agent that connects wire mcp) now DIRECT connecting agents to: (1) arm a persistent wire monitor stream-watcher on session start so peer messages surface live, and (2) reply to peer messages in their own live context without waiting for the operator to prompt them. Previously this was a soft "recommended"; now it's a baked-in directive, so anyone who installs wire gets auto-collaboration between paired agents — no per-machine hook required.
v0.12.2 — persona rename cleanup. Finishes the character → persona surface rename from v0.12.
Fixed:
wire session list/wire herecolumn header was stillCHARACTER→ nowPERSONA.wire initone-name message said "DID-derived character" → "DID-derived persona".docs/STATUSLINE.mdjq examples read the old.character.paletteJSON key (returnsnullsince v0.12's key rename) → fixed to.persona.palette, with persona terminology throughout and a naming note.
v0.12.1 — wire up claims the persona; phonebook shows the face. Closes the last one-name gap from v0.12.
Fixed:
wire up <nick>@<relay>now claims your DID-derived PERSONA, not the typed<nick>. Under the v0.11 one-name rule the typed nick is vestigial (it can't select an identity), butup's claim step was still registering it on the relay — re-opening a two-name split (claimed handle ≠ persona).upnow resolves the persona from the freshly-inited card and claims that. It also no longer bails when the typed nick differs from the existing persona (the mismatch isn't an error — the nick is ignored).- Phonebook (
/v1/handles) now shows the DID-derived emoji next to every name, even when the claimant set no explicit profile emoji. The relay computesCharacter::from_did(did).emojias a fallback, so🦨 pine-puffinrenders instead of a barepine-puffin.
v0.12 — additive multi-relay, zero-config dual-bind, persona surfacing. Onboarding and identity-surface polish on top of the v0.11 one-name rule.
Added:
wire bind-relayis additive. Binding a new relay appends toself.endpoints[]instead of overwriting, so an agent can hold a local relay AND a federation relay simultaneously. New--scope <federation|local|lan|uds>(inferred from the URL by default) and--replace(the old destructive single-slot behavior, still guarded against black-holing pinned peers). A new-relay bind never black-holes pinned peers — resolves issue #7 by design.wire upopportunistic local dual-bind. After the federation bind+claim,wire upadditively binds a local relay slot for sub-millisecond same-box sister routing.--with-local <url>overrides the defaulthttp://127.0.0.1:8771probe;--no-localopts out. Local relays carry no handle directory, so nothing is claimed there.
Changed:
- Persona surfacing. The serialized output key
character→persona(andcharacter_override→persona_override) inwire whoami/here/peers. MCPwire_whoamiandwire_peersnow include the persona (nickname + emoji + palette) — previously they emitted only the raw handle.wire notifyOS toasts now show the persona (wire ← 🦨 pine-puffin) instead of the handle. The internal RustCharactertype name is unchanged.
Fixed:
- MCP
wire_dialread a requiredhandlearg while the schema providedname, so every dial over MCP erroredmissing 'handle'. It now readsnameand routes federation handles correctly. - MCP
wire_initwithrelay_urlno longer no-ops the relay binding when the identity is already initialized but unbound — it binds the requested relay (additively) so a subsequentwire claimdoesn't 404.
Breaking:
- Consumers parsing the
characterJSON key fromwire whoami/here/peers(e.g. statusline scripts) must readpersonainstead.
v0.11 — one immutable name. The DID-derived character nickname IS the addressable handle. Operator-typed wire init <name> arg is ignored at init time; agent-card.handle is synthesized from the keypair fingerprint via Character::from_did so every peer sees you by the same name everywhere (statusline, wire peers, federation handle, inbox/outbox file path, route results, mesh-status, commit trailers). Closes the long-running "two names" footgun where a UI nickname could differ from the wire address.
Breaking:
wire identity renameremoved — there is no separate rename verb. If you want a different face, regenerate your identity (new DID → new character).agent-card.handleno longer reflects thewire init <name>argument. It isCharacter::from_did(synthesized_did).nickname. Init now prints "operator-typed<X>ignored in favor of DID-derived character<Y>. Peers will reach you as<Y>" when the two differ.- Production code paths (already-paired check in
session pair-all-local,drive_bilateral_pair,cmd_session_mesh_status) now key the in-memory peers map by handle, not session name — previously they conflated session name with handle and the local-sister pair-accept could fail when a session's directory name differed from its character.
Compat:
Character::from_didnow seeds from the 8-hex fingerprint suffix only (not the full DID string) to break the circular dependency where handle change → DID change → character change → infinite loop. Legacy DIDs without the-<fp>suffix fall through to the v0.10 seed-the-whole-DID behavior.- Federation flow (
wire add <h>@<host>) is unchanged on the wire — peers still reach you by your card handle, which is now always the character.
v0.9.5 — shell completions (bash/zsh/fish/elvish/powershell) + interactive init prompt
v0.9.4 — split wire accept into wire accept + wire accept-invite (kill smart-dispatch ambiguity)
v0.9.3 — conversational surfaces (wire here, prose pending, emoji fallback, README rewrite)
v0.9.2 — helpful errors (fuzzy resolution, miss-returns-empty in JSON, deprecation banner suppressed in JSON + once-per-session)
v0.9.1 — surface cleanup (hide deprecated, smart-default init, JSON-when-piped, quiet auto-detect)
v0.9.0 — clean cut
Six operator-facing verbs (was ~20): wire dial / send / pending / accept / reject / whois
One canonical public name per identity (DID-derived character). Operator-rename is local-display-only — no longer publishes on agent-card.
Structural fixes (silent-fail class closed):
- wire init refuses slotless sessions (root cause of 2026-05-23 incident)
- single self_primary_endpoint() reader everywhere
- wire send auto-pairs on miss for local sisters
- wire dial routes federation via @relay
- 12 legacy pair verbs collapse to 3 (pending/accept/reject)
- legacy verbs still work but emit deprecation banner (v1.0 removes)
PR #35 merged. Pre-existing test flake (detached_pair_survives_ daemon_restart_mid_handshake) carried over from main; unrelated.
Co-Authored-By: 🛡 noble-creek wire+wire-b6f47edb@wire.id
v0.7.5 — nickname-add + silent-fail pair_drop_ack fix
v0.7.3 — thorough cross-platform wire upgrade + AGENT.md §0.5
wire upgrade now sweeps daemons AND relay-servers, refreshes
installed service units to point at the new binary path before the
OS auto-respawns, and works on Windows (was hard-fail pre-0.7.3).
Cosmetic fix: wire session list now reports correct daemon
liveness on Windows (was always down because kill -0 is unix-only).
AGENT.md §0.5 redirects local agents to wire session pair-all-local
instead of the federation wire pair-host / wire pair-join flow
they kept reaching for.
New src/platform.rs exposes cross-platform process_alive /
find_processes_by_cmdline / kill_process primitives.
PR #32, merged. Full suite (193) green.
Co-Authored-By: 🛡 noble-creek wire+wire-b6f47edb@wire.id
v0.7.2 — Windows service backend (Task Scheduler)
Closes the cross-platform parity gap: wire service install and
wire service install --local-relay now register hidden, restart-
on-failure, run-at-logon scheduled tasks on Windows via schtasks.exe
- Task Scheduler 1.2 XML.
LeastPrivilege + InteractiveToken — no UAC, no stored password. Matches the user-scope footprint of launchd's gui/ + systemd --user paths.
PR #31, merged. Linux + macOS paths unchanged. Full release suite (190 + 3 bind + 8 service) clean.
Co-Authored-By: 🛡 noble-creek wire+wire-b6f47edb@wire.id
v0.7.1 — wire session bind
Adds wire session bind <name> to attach an existing session to the
current cwd without going through destroy+new. Fixes the case where
a registered ancestor dir (e.g. ~/Source) is shadowing leaf-project
identities, collapsing two CC sessions onto the same Character.
PR #28, merged 92af54b. Doc sweep 7b85d15.
Behavior:
wire session bind(no name) auto-derives from basename(cwd)- Errors loudly if the named session doesn't exist
- Idempotent: re-binding to the current binding is a no-op
- Re-binding to a different session overwrites with a stderr warning
- Uses update_registry (flock-serialized) so it composes safely with concurrent MCP auto-init writes
Tests: 3 new session_bind_* in tests/cli.rs. Full suite 190 + 3 pass.
Co-Authored-By: 🛡 noble-creek wire+wire-b6f47edb@wire.id
v0.7.0 — identity lifecycle + scope-aware routing + UDS transport
The v0.7.0-identity alpha track (22 commits) lands four arcs:
-
Deterministic Character per session: DID-hash → emoji + adj-noun nickname
- 256-color palette. Operator-stable visual ID across sessions, statusline, peer listings, commit trailers.
-
wire identitylifecycle CLI: create / persist / publish / demote / rename / show / list / destroy. Anonymous-mode sessions (local-only, no federation) can be promoted to federation slots later; published identities can be demoted back to local-only. -
Operator-chosen overrides preserved across renames; palette stays DID-derived for hash-stability.
-
EndpointScope enum unifies Federation / Local / Lan / Uds.
-
Priority order: Uds → Local-loopback (with matching self) → Lan → Federation.
-
Per-endpoint cursors for pull; per-endpoint dispatch for push.
-
post_event_to_endpoint(endpoint, event)helper: scheme-aware POST that routesunix://...via uds_request, everything else via reqwest. -
Hand-rolled HTTP/1.1 over UnixStream (axum 0.7 serve is TcpListener-only).
-
wire relay-server --uds /path/to/sockfor same-host trust-anchored IPC. -
wire session new --with-udsallocates UDS slots. -
Same-uid, same-host sister-session shape — see project_wire_transport_substrate_research.
-
Cross-machine same-network reachability between Federation and Local.
-
Tailscale CGNAT (100.64.0.0/10) bind acceptance for
--local-only. -
demo-hotline: fixed pair-accept-in-drain-loop regression (broken since v0.5.14 anti-phonebook-scrape change removed receiver auto-promote). All 5 ring sends now land. (#27)
-
Clippy clean across all alpha-track commits.
-
190 tests pass.
-
Holistic codebase audit at .planning/research/codebase-audit-2026-05-23.md with critique iteration. P0 priorities surfaced for v0.7.x and v0.8.
22 alpha commits preserved via merge commit (not squashed).
Co-Authored-By: 🐻 cedar-bayou wire+wire-source-d8ae94a5@wire.id