Skip to content

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 05 Oct 07:05
· 68 commits to main since this release

v0.3.0 — one daemon, many nodes; services by address; the family LAN

One daemon, and the nodes that use it

  • One vox daemon per data root (VOX_DATA_DIR). It holds the machine's one port and runs every node attached to it. A node is one identity: you, and one for each agent working beside you.
  • vox node create | attach | detach | list. vox node attach starts the daemon if none is running.
  • --node <name> (or VOX_NODE), given after the verb, says which node a command acts as. With one node attached, that one is used.
  • Verbs that hold a session start the daemon and attach their node: serve, connect, up, forward, lan up. The one-shot verbs (room, status, trust, share, service, app) act only as an attached node, and when it is not attached they say so: vox node attach <name>.
  • Two commands that start the daemon at the same moment both succeed: the one that loses the race waits for the winner's socket.
  • What attaching a node says reaches your terminal. Skipped anchors-file lines, and that the node is carrying on with no anchor, are printed by the verb that attached it, by vox node attach and in the TUI's notice line, as well as in the daemon's log.
  • A room the daemon does not reopen is named in its log, with the reason.
  • vox node (the anchor) is a daemon with its anchor node attached, not a process of its own kind.
  • The terminal client is a client of the daemon. It has no lock: a node takes its passphrase once, when it attaches, and stops only when it is detached. In the client, :attach attaches your node, :node <name> acts as another, and :link shows a room's link.

Rooms

  • vox room link <room> prints the room's link, and how to send its passphrase: another way than the link (in person, a call, a different app). A member joins with the link and the passphrase.
  • vox room leave <room>: the others are told, and the room is removed from your node. Joining again later works.
  • vox room end <room> ends a room for everyone. Only its creator, or an admin the creator named with vox room admin add <room> <member>, may. An end, or a retention change, made by an admin at the same moment as that admin is taken back does not stand.
  • vox room retention <room> <duration> (1h, 1w, 1m for a month, seconds, or forever) sets how long the room keeps messages, for everything already in it.
  • vox room read shows a structured post in words: its kind, the work item and the body, or "file offered: NAME (N bytes)". --json gives the envelope.
  • A host sees a refused join: in the terminal client's notice line, on the daemon's stderr and in its log, without offering the passphrase.
  • A join in progress is quiet. For a joiner it has just let in, vox serve says nothing about a sync or a dial that fails while the joiner is still settling, until the joiner's first clean session or 60 s. A failure after that is said, and vox status --json counts them all.
  • vox trust add says why your key for the member waits: the room has not synced since you joined it, or the member is not yet admitted. It is sent as soon as that changes.
  • A room is forward-only: a member reads what is posted after you trust it.

Services by address, UDP, sharing

  • A service is reached as <service>.<node>.<room>.vox, and only that way. <node> and <room> are your own names for them (or the node's fingerprint and the room's id). vox serve ssh=22 names the service; a bare port is refused.
  • vox serve names who can reach the service: the members you trust, by name, and the members of the room who cannot, by fingerprint. It says it again when someone joins.
  • A forward into a room not yet synced says it is waiting for the room's first sync, and after its patience is refused with that reason; it never guesses what is shared there.
  • UDP over Vox: vox serve dns=53/udp, and vox forward carries a UDP service.
  • vox share <room> <path> serves a file or a folder to the members you trust until --count fetches or --for a time. They fetch it with vox room get. An offer that has ended is said to be gone; an offer withdrawn while it is being collected is said so, naming who stopped sharing it; a sharer that cannot be reached is said so.

The family LAN (macOS)

  • vox lan up <room> puts a room's members on one virtual LAN, each on its own interface, with discovery (mDNS, broadcast) flowing between members who trust each other. Nothing on your machine is reachable over it unless its port is listed with --allow.
  • Making the interface needs root, and only a small helper has it: sudo vox lan helper in one terminal, vox lan up <room> --allow <ports> as yourself in another. vox lan up checks that the helper answers as one before it does anything, and refuses a socket that is not one at once, naming it.
  • Every LAN interface reaches its room's LAN, with two or more nodes on one Mac: the helper adds a route scoped to each interface, checks it is there, and reports one it could not add.

Agents

  • Each agent is its own node, never a person's. vox agent plugin claude|codex|opencode --node <name> prints the integration for that node, and the hook acts only as the --node it is given.
  • No character reaches a reader or an agent unseen. Zero-width and other invisible characters, the tag characters that can spell text invisibly, bidi controls and other control characters are all shown as ⟨U+XXXX⟩ in the terminal client, in vox room read and in what an agent's hook gives it. Emoji built with joiners (👨‍👩‍👧, 🏳️‍🌈) and the subdivision flags are shown whole.
  • An agent's hook never hangs its harness. A hook whose node is detaching is refused after 8 s, and its registration is bounded at 10 s.
  • Proved with real agents. Optional live proofs, each run sandboxed, drive real Claude Code, Codex and OpenCode turns against a room, each through its node's hook; all were green on a live run.

Words

  • Help and messages speak of nodes, rooms, room links and trust, and name no design document.
  • Errors are said in plain words. A reply a command did not expect is said as a sentence, never as a debug dump; a node detached while a command waits says "node was detached from the vox daemon, so this stopped". The same goes for the app's notices, claim outcomes and a ping's reach.
  • A peer's reason for closing a connection is shown, also when the close is larger than the path MTU.

Reliability

  • A crash costs you neither a room nor a member. A daemon killed during vox trust add keeps the posts made after its restart readable to the member just trusted: where the decision stands in the room's order is saved before the trust itself. A daemon killed in the middle of a join leaves the room either absent or one that reopens; a room your node holds closed can be joined again with its passphrase.
  • A host going offline while a joiner reads the room is said so and retried, like a room not yet on the board. A failed join prints each of its steps once.
  • A vox connect that is stopped lets go of the room at once: a client that hangs up mid-request is noticed straight away, so its node's goodbye does not wait on the request.
  • A member opens sync sessions only with members. Anchors' boards are read, so a member that restarted learns where the others are and can release keys to them.
  • vox status counts a flow on a retired connection: datagrams on a peer's connection that a better path replaced are reported.
  • Two members dialling each other through a relay at the same moment both get through and keep the same connections at both ends; a circuit that is not kept is closed.
  • A first relayed connection asks for its circuit at once, with the request for a direct dial-back running beside it.
  • A dial at an address heard on the local network is one attempt among others. If the socket cannot use the address (an IPv4 address heard on an IPv6-only node), the member is still reached through the anchor at once.

Documentation

  • The user manual, docs/manual/, describes v0.3.0 command by command.
  • ADR-017: trusting a member is the operator's act, made by any process running as the node's user, person or agent, behind the 30-minute passphrase window.
  • ADR-012 N-49–N-58: network-change detection, gateway discovery beyond Linux and the PCP mapping lifecycle. These are planned for v0.3.1; v0.3.0 does not have them.
  • The v0.4.0 UX research (docs/ux/): the research, the interview answers and the review notes.

Work items delivered

  • #18 R15: Addressing uses fingerprints on the wire and shows keyring names
  • #19 R17: The filer can hard-lock a claim; a silent holder can be taken over after a window
  • #52 R1: A room has no lifetime message limit; reopen, restart and cold catch-up work at any size
  • #53 R2: A room works with about 500 member nodes
  • #54 R4: Sync stays bounded against a malformed or oversized request
  • #56 R6: History is kept forever by default
  • #57 R7: A room admin can set and change disappearing messages
  • #58 R8: Shortening retention applies retroactively on every node
  • #59 R9: A node can set its own retention; the shortest wins
  • #60 R10: An expired message leaves nothing visible, but sync and fork detection keep working
  • #62 R11: Members never online together can read each other through an always-on member
  • #63 R12: An approver chooses "from approval onward" or "full history" per newcomer
  • #64 R13: Messages appear in the same order on every node
  • #65 R14: Sender keys no longer needed are deleted
  • #68 R22: Untrusting a node or removing a service cuts live sessions immediately
  • #69 R23: A refused tunnel fails immediately and names the reason locally
  • #70 R24: A forward survives the host restarting or the path changing
  • #72 R25: Vox carries any UDP traffic between members
  • #73 R26: Packets larger than one datagram are fragmented and reassembled inside Vox
  • #74 R27: Relayed UDP drops late packets and never stalls later ones
  • #75 R28: A family-LAN mode provides a virtual interface for discovery-reliant apps
  • #77 R29: A separate program can open a live stream or datagram flow to a member node
  • #81 R33: Any always-on node can act as an anchor
  • #82 R34: An anchor stores nothing for rooms it is not a member of
  • #84 R35: vox status shows rooms, peers and paths, tunnels and UDP flows, last sync and anything unhealthy
  • #85 R36: Every failure is logged with a specific cause
  • #86 R37: Unhealthy rooms or anchors raise a desktop or phone notification
  • #87 R38: A metrics endpoint exposes node health
  • #89 R40: A chat message between two online nodes arrives in under 1 s, direct or relayed
  • #91 R42: A first connection to a peer, including NAT traversal, completes in under 2 s
  • #93 R43: Deniable mode is removed from the code
  • #94 R44: The withdrawn capability model is removed everywhere, including IPC T_GRANT
  • #95 R45: The rate quota and the anchor log store are removed
  • #161 udp_tunnel_proof fails in setup: vox connect cannot reach anyone, and a relayed restart goes quiet
  • #169 M19.11-live: A trusted Codex hook is proved to fire in a live turn
  • #170 R16: An urgent message wakes an idle agent on another node within seconds
  • #171 R18: Files are shared with a pull model: vox share
  • #172 R19: Injected text keeps every message attributed to its true author
  • #226 V030-01: v0.3.0 carries v0.2.10, with the control protocol reconciled
  • #227 V030-02: v0.3.0's proofs run every participant as the shipped vox
  • #228 V030-03: The two formerly excluded causal_order tests are fixed, not excluded
  • #236 V030-04: vox lan up serves a control socket and metrics
  • #237 V030-05: The profile-busy message is true while vox lan up holds the profile
  • #238 V030-06: A service can be added to a running daemon
  • #239 V030-07: Proof claims weakened by the conversion are accepted by the decider or restored by product work
  • #244 V030-08: A node can leave, forget and end rooms cleanly
  • #245 V030-09: Room creation does not slow down as rooms accumulate
  • #276 V030-10: A pruned message never makes its author unreadable
  • #314 V030-11: Tunnels can be found and closed
  • #315 V030-12: A stranger's held relayed joins never keep a relayed joiner from another host off the board
  • #319 V030-13: A room's creator delegates admins (vox room admin add|remove)
  • #320 V030-14: A leave or an end withdraws its records from anchors at once
  • #325 V030-15: A wake announces; the message itself arrives once, through the room read
  • #326 V030-16: vox agent doctor says whether a session is wired up, and vox room ping checks it from the other side
  • #327 V030-17: A sender is told how each addressee can be reached
  • #328 V030-18: The per-turn room read spends its tokens on what is addressed to the agent
  • #329 V030-19: A reply shows what it answers
  • #330 V030-20: An idle agent is told when a reply to it is waiting
  • #331 V030-21: Room text can never close its own fence on OpenCode, nor be labelled the user's
  • #335 V030-22: A node that cannot dial a peer asks it, over the anchor's coordination stream, to dial back
  • #337 V210-126: The metrics endpoint answers a request that arrives in pieces
  • #338 V030-24: vox service list works while the daemon runs
  • #339 V030-25: A shared service is reached as service.node.room.vox, and only that way
  • #349 V030-27: A host that reaches a just-joined guest it cannot dial waits for the dial-back, not an anchor circuit
  • #351 V030-29: Every ADR is a tight set of RFC 2119 instructions
  • #357 V210-138: A retention-pruned entry is never owed forever to a late member
  • #380 V030-32: Room governance is only "the creator or an admin sets the room's retention"; the unused rest is removed
  • #382 V030-33: The identity certificate and the session record state only true values
  • #383 V030-34: Per-flow UDP counters are visible in vox status and metrics
  • #398 V030-36: An empty identity or room passphrase is accepted
  • #399 V030-35 D1: Layout, node names, migration, per-node config
  • #400 V030-35 D2: Self-dial spike: one endpoint dials itself
  • #401 V030-35 D3: Identity exchange and neutral daemon leaf
  • #402 V030-35 D4: Per-node process-wide state and circuits
  • #403 V030-35 D5: Shared presence: one endpoint for every attached node
  • #404 V030-35 D6: Router, IPC v9 and the account socket
  • #405 V030-35 D7: The vox daemon process and auto-start
  • #406 V030-35 D8: Clients: --node, verbs as clients, vox node commands
  • #407 V030-35 D9: Anchor fold-in
  • #408 V030-35 D10: Agents: hook acts only as --node
  • #409 V030-35 D11: TUI as a daemon client
  • #410 V030-35 D12: Proofs: identity, co-hosting, lifecycle, harness and suite rebase
  • #411 V030-37: A member runs no sync session with an anchor that is not a member
  • #412 V030-38: A crash in the middle of a trust decision or a join never costs a member the room