Skip to content

Add impl plans for TIPC/QUIC/wg tpt backends - #492

Open
goodboy wants to merge 13 commits into
mainfrom
ng_tpts_planning
Open

Add impl plans for TIPC/QUIC/wg tpt backends#492
goodboy wants to merge 13 commits into
mainfrom
ng_tpts_planning

Conversation

@goodboy

@goodboy goodboy commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Add impl plans for TIPC/QUIC/wg tpt backends

Motivation

Three prospective tpt backends have been sitting in issues as
sketches — TIPC (#378), QUIC-via-iroh (#353), and wg (#482,
#443) — each with enough design surface that starting to code any
one of them means re-deriving the same .ipc contract from scratch,
and probably deriving it differently each time. Worse, two of the
three quietly break assumptions the current 2-backend _addr.py gets
away with, and you don't find that out until you're deep in an impl.

So this is planning work landed as docs: no runtime code is
touched
. The point is to get the shared contract and the three
designs reviewed before anyone writes a backend against them,
because two conclusions below are wire-format-affecting and much
cheaper to argue about now than after a backend ships.

Everything here is written so a different contributor — or a
different model — can pick up any one plan and work it independently,
w/o the other two landing first.

Src of research

Verified against sources rather than recalled; the plans treat kernel
headers + the upstream PRs as normative over docs sites,

Summary of changes

  • add ai/tpt-backends/ — a normative shared backend contract spec
    plus one plan doc per prospective backend, indexed by a README.
    The contract pins the duck-type every backend must satisfy (a
    frozen msgspec.Struct <Proto>Address, module-level
    start_listener()/close_listener(), a
    Msgpack<Proto>Stream(MsgpackTransport)), documents the
    inspect.getmodule(self.addr) reflection in
    Endpoint.start_listener() that forces an Address class and its
    listener fns to live in the same mod, and carries a 10-item
    registration checklist so adding a backend is mechanical.
  • plan TIPC as the cheapest of the three: verified that trio's
    SocketStream/SocketListener are address-family agnostic
    the only ctor checks are "is a trio sock" + type == SOCK_STREAM
    plus an OSError-suppressed SO_ACCEPTCONN probe, w/ no AF_*
    check anywhere — so a TIPC SOCK_STREAM sock drops straight into
    trio.serve_listeners() on our existing framing, zero new deps.
    The interesting part is discovery: bind() is registration and
    connect() is lookup, w/ TIPC_TOP_SRV subscriptions as a
    push-based registry.
  • plan QUIC on iroh (over aioquic/quiche) for node-id
    addressing + hole punching + relay fallback, w/ the asyncio
    question answered by a ~40-line TrioToken.run_sync_soon() bridge
    rather than trio-asyncio. Since an iroh listener isn't a sock, it
    also needs trio.abc.Listener/HalfCloseableStream adapters + a
    small prep patch to _server.py/_types.py — landable w/ tcp+uds
    as the only backends.
  • plan wg as a bindspace, not a tpt. It's transparent to
    socket(2), so it belongs as a scoped @acm-managed net ctx that
    an existing L4 tpt binds inside, via a TunnelledAddress wrapper
    whose .proto_key/.unwrap() delegate to .inner — nothing new
    crosses the wire and every existing table lookup keeps working.
    Three independently shippable layers: declarative maddrs,
    pyroute2 replacing the sudo wg show shelling, then the
    netns/iface @acms.
  • correct the wg maddr grammar the wg multiaddr: LAN deployment examples #482 examples used: /wg/ is
    infix, not suffix. Installed baudco/py-multiaddr@wg_support
    in a throwaway venv and round-tripped each candidate form — segs
    before /wg/ are the bearer (the underlay (ip, udp-port)
    wg(8) itself listens on), segs after are the overlay ep, and
    only that last part is ours to bind. The suffix form parses, which
    is why it slipped through, but it's semantically inverted.
  • conclude that shape-matching in wrap_address() does not survive
    4 backends
    and propose proto-key-tagging the unwrapped form.
    TIPC's natural unwrapped shape is a (str, int), indistinguishable
    from TCPAddress; iroh's is a (str, str), already swallowed by
    the existing UDS case. The fix is an explicit proto-key spelled w/
    the multiaddr proto name — ('tcp', host, port), ('unix', path), ('tipc', stype, inst, scope) — collapsing
    wrap_address() to _address_types[addr[0]].
  • add examples/multihost/wg_lan/ — the wg multiaddr: LAN deployment examples #482 example set re-rendered
    against the corrected grammar, as the plan's layer A. A frozen
    msgspec tunnelled addr w/ pure parse/render helpers, an
    explicitly-impure verify_wg_peer() kept out of the parse path,
    and the two host scripts passing only addr.inner to the runtime.
    They live under multihost/ bc test_docs_examples.py walks
    examples/ recursively and subproc-runs everything it collects
    (w/o even filtering by extension) — 'multihost' is already in its
    exclusion list.
  • log the whole arc under ai/prompt-io/claude/ per the NLNet
    policy, in diff-ref mode so the code isn't duplicated.

Two claims in here are deliberately marked as unverified
assumptions
rather than findings, and are the first things worth
attacking: that TIPC accepts duplicate binders on a name and
round-robins (making an instance collision silent cross-talk rather
than EADDRINUSE), and that uniffi uses asyncio only as the
executor for a rust-future poll loop. Both are gated behind a step-0
probe in their respective plans.

TODOs before landing

Docs-only, so nothing is mechanically blocking — but these are design
calls that get much more expensive after a backend ships, and are the
reason this is up for review rather than merged:

  • agree (or not) on proto-key-tagging UnwrappedAddress. It's a
    wire-format change (SpawnSpec, _root_mailbox,
    _registry_addrs) plus every fixture and downstream config
    (piker's [network] table). Land it before the first new
    backend, or after?
  • agree (or not) that wg is a bindspace rather than a tpt — the
    wg multiaddr: LAN deployment examples #482 sketch assumed a WGAddress in _address_types, and the plan
    argues that breaks the table's 1:1 intent.
  • sanity-check the iroh selection, given it drags in an FFI
    bridge. aioquic stays documented as the fallback and the adapters
    are shaped to be ~90% reusable against a sans-io core.
  • decide the QUIC sub-stream story: one bi-stream per Channel
    (as planned) vs one per tractor.Context for independent
    flow-control + cancellation.

Future follow up

  • land the proto-key UnwrappedAddress migration — its own
    branch off main, and the natural base for the three backend PRs
    since they all depend on it. See contract §1.1 for the blast
    radius.

  • stop handing raw unwrapped tuples to users — make Address
    the public currency and UnwrappedAddress an internal
    serialization detail, the discipline ipaddress uses. Public API
    accepts Address|maddr-str; bare tuples become legacy-tolerated,
    ideally deprecated.

  • run examples/multihost/wg_lan/ against a live tunnel
    they're runnable-shaped but have never touched real wg ifaces.
    Until they do, treat them as executable pseudo-code.

  • register /tipc in the multiaddr spec — mirrors the wg
    track (py-multiaddr#107/Multiple issues on the code examples present in the README file. #108, tractor wg multiaddr protocol: upstream spec submission plan #483). Worth proposing
    alongside the wg submission rather than as a one-off.

  • settle how an iroh node-id is spelled as a maddr
    /p2p/…? It's the third unregistered proto blocking the
    MsgTransport.maddr standardization in Follow-up: multiaddr_support (PR #429) #443.

  • implement Address.namespace — spec'd in the Address
    protocol since day one and implemented by no backend; there's
    already a !TODO, always be ns aware! + |_netns: placeholder
    sitting in Endpoint.pformat(). The wg/netns work is its first
    consumer.

  • veth-in-netns as the first bindspace, before wg
    plan 03 argues it makes a fully self-contained two-"host"
    integration test possible w/o wg at all.

Links

(this pr content was generated in some part by claude-code using
claude-opus-5 (anthropic))

goodboy added 10 commits August 12, 2026 20:08
First doc of a new `ai/tpt-backends/` set: the normative
description of what a `tractor` tpt backend *is* as of `main`,
written so the 3 sibling plans (TIPC, QUIC, `wg`) can be worked
independently (by another model/provider) w/o design drift.

Deats,
- the backend duck-type as empirically derived from
  `_tcp.py`/`_uds.py`: the `Address` protocol surface, the
  mod-level `start_listener()`/`close_listener()` pair and
  `Msgpack<Proto>Stream(MsgpackTransport)`.
- the ONE reflection you can't break:
  `Endpoint.start_listener()` resolves the tpt mod via
  `inspect.getmodule(self.addr)`, so an `Address` type and its
  listener fns MUST live in the same mod.
- a 10-item registration checklist (`_address_types`,
  `_key_to_transport`, `_addr_to_transport`, `wrap_address()`
  match-cases, `TransportProtocolKey`, maddr tables, ..) incl.
  the import-time `_default_lo_addrs` trap.
- where the `trio.SocketListener` assumption is *actually*
  load-bearing (just the `getsockname()` reconcile) vs. merely
  annotated.
- the handshake/discovery invariants a new backend inherits,
  dep policy (extras + import-laziness per the #470 boot-latency
  budget), `--tpt-proto` harness plumbing and code style.

Also, records a verified finding the plans lean on hard:
`trio.SocketStream`/`SocketListener` are addr-*family* agnostic
— the only ctor checks are "is a trio sock" + `SOCK_STREAM` (+
an `OSError`-suppressed `SO_ACCEPTCONN`) — so any `SOCK_STREAM`
family CPython can make drops into the existing
`trio.serve_listeners()` path unmodified.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Plan doc for gh #378, the cheapest new backend we can add: it's
stdlib-only (CPython ships `AF_TIPC` + 23 `TIPC_*` consts) and
per the contract doc `trio`'s stream/listener wrappers don't care
about the addr family, so `MsgpackTransport` framing and
`trio.serve_listeners()` are reused verbatim.

Deats,
- `TIPCAddress` as a *service name* `(type, instance)` w/ scope
  as the `.bindspace`; `bind()` publishes the singleton
  name-range, peers `connect()` by name and the kernel resolves
  + load-balances. I.e. registration/lookup for free, no
  registrar in the loop.
- the self-tagging `('tipc:<stype>:<scope>', instance)` unwrapped
  form + why it must be match-ordered before `TCPAddress`'s.
- `get_random()` via a blake2b digest of the actor id (there's no
  `port=0` analogue) and the silent-crosstalk risk that follows:
  TIPC *allows* dup binders and round-robins, so a collision
  doesn't `EADDRINUSE`, it cross-talks.
- an `Address.rebind_from_sockname` ClassVar to opt out of
  `Endpoint.start_listener()`'s `getsockname()` reconcile, which
  for TIPC always returns a port-id, never the bound name.
- the `TIPC_TOP_SRV` topology-service subscription as an `@acm`
  yielding a chan of typed name-table events — push-based
  register/dereg, the real "end game cluster proto" bit.
- commit sequencing, hard capability gating (`modprobe tipc`;
  bare `AF_TIPC` is `EAFNOSUPPORT` on a stock box), CI matrix
  notes, risks + follow-up seeds.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Plan doc for gh #353. Picks `iroh` (the `uniffi` FFI pkg) over
`aioquic`/`quiche` bc node-id addressing + hole-punching + relay
fallback is the whole point; `aioquic` stays documented as the
fallback since ~90% of the adapters here are reusable against a
sans-io core.

Deats,
- the layering: iroh `Endpoint` per actor, `Connection` per peer
  (pooled via `trionics.maybe_open_context()`, not a hand-rolled
  cache), one bi-stream per `Channel`. 4-byte prefix framing
  stays so `MsgpackTransport` is untouched.
- `_uniffi_trio.py`: uniffi only uses `asyncio` as the executor
  for its rust-future poll loop, so a ~40-line
  `TrioToken.run_sync_soon()` bridge replaces it. Spells out the
  real hazards — strong ref on the `ctypes` trampoline, poll-code
  propagation, and a *bounded* shielded cancel-drain so a wedged
  rust future can't make an actor un-cancellable.
- `IrohAddress` w/ ALPN as the `.bindspace`, the `(str, str)`
  unwrapped form's collision w/ the UDS match-case, and why
  `get_root()` needs a persisted secret key -> a lazy
  `default_lo_addrs()` + a pure-getter/explicit-setter split.
- `QuicMsgStream(trio.abc.HalfCloseableStream)` +
  `QuicListener(trio.abc.Listener)`, incl. the exact
  EOF/reset/use-after-close semantics `_transport.py` already
  match-cases on, and hanging the acceptor tasks off the
  existing `Endpoint.listen_tn`.
- a prep-PR boundary: annotation widening, the shared
  `rebind_from_sockname` gate and a `tpt_key`-based
  `transport_from_stream()` dispatch, all landable w/ tcp/uds as
  the only backends.

Further, notes this is our first tpt w/ real transport security
+ peer auth, so an inbound node-id allowlist hook belongs here —
and that it says nothing about the other backends.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Plan doc for gh #482 + the tunnelled-maddr item of #443. Pushes
back on the framing that `wg` is a tpt: it's transparent to
`socket(2)`, so it belongs as a *bindspace* — a scoped
`@acm`-managed net ctx that an existing L4 tpt binds *inside* —
and it's what finally implements the long-spec'd (never
implemented) `Address.namespace`.

Deats, 3 independently-shippable layers,
- A) declarative: commit #482's examples, teach `parse_maddr()`
  the `/…/wg/u<key>` suffix -> a `TunnelledAddress` wrapper whose
  `.proto_key`/`.unwrap()` delegate to `.inner` so nothing new
  crosses the wire and every existing table lookup keeps working.
- B) swap the `subprocess.run(['sudo', 'wg', 'show'])` shelling
  for `pyroute2`. Default to `trio.to_thread` around the sync API
  (these are one-shot ops at bind/teardown, never hot-path), w/
  sans-io codecs + a trio `AF_NETLINK` sock as the follow-up for
  the read paths. Explicitly forbids dragging `trio-asyncio` in.
- C) `open_bindspace()`/`open_netns()`/`open_wg_iface()` `@acm`s
  folded w/ an `AsyncExitStack`, + filling in the
  `# !TODO, always be ns aware!` placeholder already sitting in
  `Endpoint.pformat()`.

Also flags the subtlest bug in the whole thing: `setns(2)` is
*per-thread*, so a `pyroute2` query issued via `trio.to_thread`
lands in the *original* netns. Test-first, per usual.

Further, designs for the generalization (`TunnelSpec` union +
`match` dispatch) while only implementing `wg`+netns, and calls
out `veth`-in-netns as the better *first* one bc it makes a
fully self-contained two-"host" integration test possible w/o
`wg` at all.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Landing page for `ai/tpt-backends/`: points at the contract spec
as required first reading, tables the 3 plans against their
issues/deps/size, and states the landing order + why.

Deats,
- TIPC first as the cheap proof the table-registration story
  generalizes to a genuinely new proto (stdlib-only, and
  `trio`'s sock wrappers are family-agnostic).
- `wg` layer-A next since it's deployable-today doc/example work.
- QUIC last, gated on its own prep PR.
- notes that plans 01 and 02 both want the same
  `Address.rebind_from_sockname` gate, so whichever lands first
  ships it.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Shape-matching in `wrap_address()` doesn't survive 4 backends and
the plans were papering over it: TIPC's natural unwrapped form is
a `(str, int)`, indistinguishable from `TCPAddress`, and iroh's
is a `(str, str)`, which the *existing* UDS case
(`case (_, filename) if type(filename) is str`) already swallows.

So the contract doc (§1.1) now carries the conclusion as a
**recommended prerequisite for all three backends**: make the
unwrapped form carry an explicit proto-key spelled with the
`multiaddr` protocol name — `('tcp', host, port)`,
`('unix', path)`, `('tipc', stype, inst, scope)`. `wrap_address()`
then collapses from an order-sensitive `match` to
`_address_types[addr[0]]` and the whole collision class stops
existing, while the on-wire form finally agrees w/
`mk_maddr()`/`parse_maddr()` instead of being an independent
invention.

Two consequences spelled out: it's a wire-format change
(`SpawnSpec`, `_root_mailbox`, `_registry_addrs`) + every fixture
+ downstream config, so it wants its own migration commit landed
*before* any new backend; and it's the moment to stop handing raw
tuples to users at all — `Address` becomes the public currency
and `UnwrappedAddress` an internal serialization detail, the same
discipline `ipaddress` uses (you pass `IPv4Address`, never a
4-tuple).

Plan 01 §2.2 is rewritten to match and to explicitly **retract**
its own earlier `('tipc:<stype>:<scope>', instance)` self-tagging
prefix hack — it keeps `wrap_address()` order-sensitive and does
nothing for the iroh/UDS collision, so the doc says don't
resurrect it. Registration checklist item 4 likewise becomes "do
the migration first, then this is a one-line `_address_types`
entry".

Also seeds a `/tipc` multiaddr-spec submission as a follow-up,
mirroring the `wg` track (multiformats/py-multiaddr#107/#108 + gh

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
The prior revision (and gh #482's examples) had it as a suffix,
`/ip4/10.0.11.1/tcp/1616/wg/u<key>`. Wrong: verified against
`baudco/py-multiaddr@wg_support` (py-multiaddr#108) installed in
a throwaway venv, the canonical form is

  /ip4/192.168.1.50/udp/51820/wg/u<A_pub>/ip4/10.0.11.1/tcp/1616

where segs *before* `/wg/` are the **bearer** — the underlay
`(ip, udp-port)` `wg(8)` itself listens on (`ListenPort`), per
the codec docstring's own example — and segs *after* are the
**overlay** ep, the only part we ever bind. The suffix form does
parse, which is why it slipped through, but it's semantically
inverted: overlay addr where the bearer belongs, `tcp` where
wg's `udp` goes, and no overlay ep declared at all.

Records the observed `[p.name for p in m.protocols()]` lists so
the `match` can be written against fact, and replaces the
"composed vs not" framing w/ what's actually the design axis:
three parts, three **owners** — bearer bound by the kernel via
`wg-quick`/`pyroute2`, `/wg/u<key>` bound by nothing (it's an
identity, verified out-of-band), overlay bound by our
`IPCServer` as `.inner`. `_peel_tunnel_segs()` correspondingly
grows a 3rd return, splitting *at* the tunnel seg so nested
tunnels fall out for free.

Also hoists the netns conclusion to the top of §5.3 where it
can't be missed: netns is a **runtime-level config API, not an
actor-app-code one**. It's a spawn/boot-time input alongside
`enable_transports`/`tpt_bind_addrs`, deliberately w/ no
`await actor.enter_netns(...)`, because `setns(2)` neither moves
already-created sockets nor applies beyond the calling thread —
so a mid-life API would silently leave the IPC server bound in
the old ns.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Re-renders gh #482's examples w/ the corrected (infix) maddr
grammar, as the "layer A" slice of the wg plan: declarative
maddrs only, tunnel pre-provisioned out-of-band, zero runtime
changes.

- `wg_maddr.py`: a `frozen=True` `msgspec.Struct` addr carrying
  `bearer`/`peer_pubkey`/`inner` (+ `inner_proto`), a `.maddr`
  property that re-renders the canonical form, and pure
  `mb_pubkey()`/`wg8_pubkey()`/`parse_wg_maddr()`. The parser
  rejects #482's inverted suffix form w/ an actionable error and
  stays **side-effect free** — `verify_wg_peer()` is a separate,
  explicitly impure step the caller composes, never something a
  parse path shells out to.
- `host_a_srv.py`/`host_b_client.py`: the two-host runs, passing
  only `addr.inner` into `open_nursery()`/`open_root_actor()`,
  which is the whole point — the bearer + key layers are already
  established before any bind happens.
- `README.md`: the grammar + the 3-owners table, the `#108`
  branch install line, tunnel setup, and a "what changed vs
  #482" section enumerating the corrections.

Runnable-shaped but **not yet run against a live tunnel**; that's
next, and the reason these sit on the planning branch rather than
in `examples/` proper. `_segments()` marks its stopgap for when
the `wg` codec isn't installed.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
`tests/test_docs_examples.py` walks `examples/` **recursively**
and subproc-runs every collected file asserting `rc == 0`. Ran
its exact filter against the tree: all 4 of our files were being
collected — including `README.md`, since the filter never checks
the extension, so CI would have literally tried `python
README.md`. These need a real second host + a live `wg` tunnel,
so they can't ever satisfy that gate.

`'multihost' not in p[0]` is already in the test's exclusion
list w/ no dir yet using it, so this is a pure `git mv` — zero
test changes — and it's what the exclusion was plainly there
for. Collection drops 24 -> 20 files, 0 of them ours.

Also records *why* in the two places someone would look before
adding the next one: a callout at the top of the example README
and a note on plan 03's §3.4 deliverables. Anything needing a
second host or live tunnel goes under `examples/multihost/`.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
One record covering all 9 commits on this branch, per the NLNet
generative-AI policy and the existing `ai/prompt-io/claude/`
convention.

Uses diff-ref mode for both the plan docs and the example code
(`git diff main..ng_tpts_planning -- <path>`) rather than
duplicating content already in `git log -p`. Kept verbatim in
the `.raw.md`: the four verified findings (trio's
family-agnostic `SocketStream`/`SocketListener`, the round-trip
table proving `/wg/` is infix, the proto-key `UnwrappedAddress`
rationale, and `setns(2)`'s per-thread reality), since those are
reasoning rather than diffable output.

`## Human edits` records that the steering here was substantial
and mid-session rather than post-hoc: two model claims about wg
maddr semantics were challenged and retracted (incl. in an
already-posted issue comment), and the proto-key +
netns-as-runtime-config framings were human-directed. Also notes
the one model-initiated correction — a pre-publication
self-review that downgraded the `uniffi`/asyncio thesis and the
TIPC duplicate-binder claim to explicitly-flagged assumptions.

Prompt-IO: ai/prompt-io/claude/20260813T001102Z_27c34aeb_prompt_io.md

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Copilot AI lite review requested due to automatic review settings August 13, 2026 00:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds planning documentation for “next-gen” tractor.ipc transport backends (TIPC, QUIC/iroh, WireGuard-as-bindspace), plus a two-host WireGuard multiaddr example under examples/multihost/.

Changes:

  • Add a set of detailed backend plan/spec markdown docs under ai/tpt-backends/.
  • Add WireGuard tunnelled-multiaddr parsing + verification helper and two multihost example scripts.
  • Add prompt I/O artifacts capturing the doc-generation session output.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
examples/multihost/wg_lan/wg_maddr.py Example-local WG tunnelled-maddr struct + parse/render helpers + optional peer verification
examples/multihost/wg_lan/README.md Setup and usage guide for the multihost WireGuard example and the maddr grammar
examples/multihost/wg_lan/host_a_srv.py Host A server-side example binding tractor on the WG overlay endpoint
examples/multihost/wg_lan/host_b_client.py Host B client-side example dialing Host A via the WG overlay endpoint
ai/tpt-backends/README.md Index/overview of the backend plan docs and their sequencing
ai/tpt-backends/00_shared_backend_contract.md Shared “backend contract” doc: conventions, registration checklist, dependency policy, test harness notes
ai/tpt-backends/01_tipc_backend.md Plan/spec for a TIPC backend using kernel discovery semantics
ai/tpt-backends/02_quic_iroh_backend.md Plan/spec for QUIC via iroh, including an approach to a trio-native uniffi bridge
ai/tpt-backends/03_wg_tunnel_bindspace.md Plan/spec for WG as a nested bindspace and tunnelled multiaddr grammar
ai/prompt-io/claude/20260813T001102Z_27c34aeb_prompt_io.raw.md Raw captured output for the doc-generation session
ai/prompt-io/claude/20260813T001102Z_27c34aeb_prompt_io.md Curated summary of the doc-generation session output

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +92 to +94
import multibase
raw: bytes = base64.b64decode(wg8_key)
return multibase.encode('base64url', raw).decode('ascii')
Comment on lines +127 to +130
bearer_segs: list[str] = segs[:wg_at]
mb_key: str = segs[wg_at + 1]
inner_segs: list[str] = segs[wg_at + 2:]

Comment on lines +78 to +84
b_host, b_port = self.bearer
i_host, i_port = self.inner
return (
f'/ip4/{b_host}/udp/{b_port}'
f'/wg/{mb_pubkey(self.peer_pubkey)}'
f'/ip4/{i_host}/{self.inner_proto}/{i_port}'
)
@goodboy goodboy changed the title Ng tpts planning Add impl plans for TIPC/QUIC/wg tpt backends Aug 13, 2026
@goodboy goodboy added IPC and transport streaming dependencies Pull requests that update a dependency file integration Optional/loose support for 3rd party libs/apps/projects enhancement New feature or request experiment Exploratory design and testing labels Aug 13, 2026
py-multiaddr#108 (the `/wg/u<key>` maddr proto) merged upstream
on 2026-07-28 as `f86519da`, but ships in no release yet — the
latest `0.2.0` predates it by ~4 months and carries no `wg`
codec at all. So `examples/multihost/wg_lan/` can't parse its
own maddrs off PyPI.

Pinned by `rev` and not `branch` so CI stays reproducible. Note
the lock now records the git source *instead of* the `>=0.2.0`
specifier, i.e. the dep floor above is fully overridden for as
long as this pin lives.

TODO, drop the pin (and bump that floor) the moment a release
carries the codec; the only consumer is the `wg_lan` example
set.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
`_segments()` called `Multiaddr(maddr)` purely to validate, then
swallowed every failure under `except Exception: pass`. That was
harmless pre-#108 — w/o a `wg` codec there was nothing to
validate — but now that the codec is pinned in, the swallow is
load-bearing and disabled: a malformed key sails past validation
into `wg8_pubkey()`, which happily emits a corrupt b64 str, and
the returned struct then fails its own `.maddr` round-trip. No
raise, just quietly wrong output.

Deats,
- add `_have_wg_maddr_proto()`, the gate plan-03 already
  referenced but which never actually existed. Impl'd as
  `protocols.protocol_with_name('wg')` under
  `except ProtocolNotFoundError` and cached in a mod global,
  same shape as the TIPC plan's `is_tipc_available()`.
- only validate when that gate is `True`, and let
  `StringParseError` propagate — a maddr which doesn't parse
  must NOT reach `wg8_pubkey()`.
- keep the degraded split for a pre-#108 install, now w/ an
  explicit `XXX` naming the validation you give up.

So parsing stays pure but becomes total-or-raises. Our own
`ValueError`s (missing `/wg/` seg, bare tunnel w/o an overlay
ep) are unaffected, as is the `wg(8)` b64 round-trip.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
it lands" framing in plan-03 and the example README was stale in
both directions: the branch pin is obsolete, yet you still can't
just `pip install multiaddr`.

Deats,
- §3.2's grammar table is now re-verified against the upstream
  merge (`f86519da`) rather than only `baudco@wg_support` in a
  throwaway venv. Also notes the codec enforces a 32-byte key,
  so a truncated one is a `StringParseError` and not a silently
  mangled parse.
- §1 says merged-but-unreleased; the still-open work is spec
  registration (py-multiaddr#107 + gh #483).
- §3.4 swaps "pin the branch" for the `[tool.uv.sources]` `rev`
  pin, and fixes the `_have_wg_maddr_proto()` recipe it
  suggested — probing w/ `Multiaddr('/wg/uAAAA')` now ALWAYS
  raises bc the codec wants 32B, i.e. that feature-detect would
  report `False` even w/ the proto perfectly well known.
- risk table row goes "#108 not merged" -> "merged but
  unreleased".
- example README: `uv sync` alone now suffices bc of the pin;
  documents the 32B check and points at
  `_have_wg_maddr_proto()` as the gate.

The one surviving `baudco` mention is deliberate, it records
where the grammar was *first* verified.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
@goodboy goodboy mentioned this pull request Aug 14, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file enhancement New feature or request experiment Exploratory design and testing integration Optional/loose support for 3rd party libs/apps/projects IPC and transport streaming

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants