Skip to content

Add an AF_TIPC transport backend - #493

Open
goodboy wants to merge 41 commits into
ng_tpts_planningfrom
wkt/tipc_backend_378
Open

Add an AF_TIPC transport backend#493
goodboy wants to merge 41 commits into
ng_tpts_planningfrom
wkt/tipc_backend_378

Conversation

@goodboy

@goodboy goodboy commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Add an AF_TIPC transport backend

Motivation

Finally attempting to implement #378, per #492 🏄🏼

Every other tractor transport hands us a pipe and leaves discovery
to us: the registrar actor, the find_actor() round-trip, the whole
tractor.discovery apparatus. TIPC is different in a way that
actually matters — its service names live in a cluster-wide name
table the kernel itself maintains
. So a .bind() is service
registration and a .connect()-by-name is the lookup, resolved and
load-balanced in-kernel.

That makes it the cheapest new backend we can add (stdlib-only, zero
new deps, and trio.SocketStream/SocketListener turn out to be
fully address-family agnostic) while simultaneously being the only
one that gives us cluster-wide discovery for free. It's also the
right first backend to land of the three planned in #492: unlike the
iroh/QUIC work it needs no generalization of _server.py or
transport_from_stream(), so it's a cheap proof that the
table-registration story works for a genuinely new proto.

A lovely side-effect of leaning into this proto is that it already
contains
a built-in discovery system, which would let
us avoid R&D-ing something more involved of our own medium-term
(#184, #216). There's also a lot to leverage from the sophisticated
msging system including load-balancing in ideal cases and
fail-over connectivity for worst.

Src of research

The tipc.io docs are stale in places, so the kernel sources are
treated as the only normative reference throughout,

Every behavioural claim below was settled by probing a live kernel
(modprobe tipc, py3.13) rather than reasoning from docs — several
turned out to contradict the plan.

Summary of changes

  • an actor's TIPC address is a service name (stype, instance),
    wrapped as TIPCAddress. Binding the singleton TIPC_ADDR_NAMESEQ
    range publishes it — it shows up in tipc nametable show — and
    MsgpackTIPCStream.connect_to() dials it by name. .unwrap() is
    proto-keyed as ('tipc', stype, inst, scope) using the multiaddr
    proto spelling so wrap_address() can't confuse it with the
    tcp/uds 2-tuples.
  • .bindspace is the TIPC scope, which is about as literal a
    reading of that property's "set of hosts this bind is reachable
    from" docstring as exists. TIPC_ZONE_SCOPE is deprecated/aliased
    in modern kernels so it's folded to cluster on input.
  • a new Address.rebind_from_sockname: ClassVar[bool] gates
    Endpoint.start_listener()'s post-bind getsockname()
    reconciliation (cca3a70d). That reconciliation exists
    only to learn a kernel-assigned port from a port=0 tcp bind;
    TIPC has no such late-binding and its getsockname() answers a
    TIPC_ADDR_ID port-id, so rebinding from it would swap a dialable
    service name for an un-dialable one. True on tcp/uds keeps
    today's behaviour bit-for-bit.
  • layer B: open_topology_events() subscribes to TIPC_TOP_SRV and
    yields a trio receive-chan of TIPCNameEventpush-based
    discovery, where the kernel tells us the instant any actor anywhere
    in the cluster publishes or withdraws a name. This is the
    groundwork for a registrar that never polls find_actor(). Event
    delivery now aborts explicitly on user-space overflow so a consumer
    must resubscribe instead of silently trusting stale topology state.
  • full registration across every table in the shared-contract
    checklist (_state.TransportProtocolKey,
    _addr._address_types/._default_lo_addrs/wrap_address(),
    _types' four tables + transport_from_stream()), plus
    test-harness plumbing so --tpt-proto tipc is a first-class suite
    mode.
  • an interim str-only /tipc/<stype>/<instance>/<scope> maddr
    grammar; parse_maddr() special-cases the prefix before
    Multiaddr(), which would otherwise reject the unregistered proto
    name outright.
  • a blocking --tpt-proto=tipc CI leg with a gated sudo modprobe tipc step. The experimental continue-on-error was removed after
    repeated green runs (partially addresses Run (various) test suite(s) under different tpt protocols in CI #420).
  • docs: a docs/guide/tipc.rst page and an
    examples/multihost/tipc_cluster/ set. Both single-host examples
    were run against a live kernel and their real output is what's
    pasted in the README. The two-physical-host example is now an
    operator runbook covering cluster identity, bearer setup,
    failure/rejoin and diagnostic capture; it introduces “Cluster
    Domain Sockets” as the newcomer-facing explanation while retaining
    tipc as the interoperable key. Plus
    ai/tpt-backends/01_tipc_HANDOFF.md, a provider-neutral cold-start
    handoff carrying the verified-behaviour table and closed decisions.
  • an upstream /tipc multiaddr issue draft with a fixed-width
    (type, instance, scope) value, canonical structured text form,
    WireGuard composition and separation from future pyroute2
    generic-netlink deployment management (#498).

Two fixes fell out that are not TIPC-specific,

  • pformat_caller_frame() was passing an indent='' kwarg
    pformat_boxed_tb() has never accepted, so EVERY send-side
    MsgTypeError died with a TypeError while formatting itself,
    masking the real msg-spec violation (f9f98eeb). Dates
    to 888af602; present on main and every wkt/* branch. It is now
    isolated in PR #503 for landing before this stack.
  • SpawnSpec.reg_addrs/.bind_addrs pinned the wire shape to a
    2-tuple, so every TIPC subactor died at Expected array of length 2, got 4 (22049794). Widened to UnwrappedAddress
    which SpawnSpec's own # TODO already asked for — and note the
    alias had to become variadic (tuple[str|int, ...]) bc
    msgspec refuses a union holding more than one array-like type.
    First real bite of the proto-key migration in Add impl plans for TIPC/QUIC/wg tpt backends #492's shared
    contract.

Verified kernel behaviour

Several of these contradict what the plan assumed, and two were
hazards it never anticipated,

  • duplicate name binds both succeed and connects round-robin
    between them (six dials alternated strictly). So a get_random()
    instance collision is silent crosstalk, never EADDRINUSE
    which is why the instance is a blake2b digest of the actor
    identity.
  • dialing an unpublished name answers EHOSTUNREACH instantly,
    no SYN-timeout wait — better discovery-ping behaviour than TCP. But
    python maps it to a bare OSError, NOT a ConnectionError
    subtype the way ECONNREFUSED maps to ConnectionRefusedError, so
    the _reraise_as_connerr() normalization is required by the shared
    contract's handshake rules rather than being polish.
  • a connect-then-drop peer used to kill the whole actor. TIPC
    answers ENOTCONN from getpeername() once the peer's gone
    (tcp/uds keep answering until we close), and
    MsgpackTransport.__init__() calls .get_stream_addrs() before
    the handshake — so the OSError escaped
    handle_stream_from_peer()'s handshake tolerance. A port scan was
    a remote actor-kill. Found by our own daemon fixture's readiness
    probe.
  • SO_ACCEPTCONN works on AF_TIPC (answers 1); trio's
    except OSError carve-out isn't load-bearing here after all.
  • the topology struct tipc_event is 48 bytes, not 40, and
    native '=' byte-order is accepted — so the plan's proposed
    _detect_topsrv_endianness() '>'-retry probe was deleted as
    unnecessary. TIPC_WAIT_FOREVER is -1 in python and must be
    masked before packing as an unsigned field.
  • graceful peer close surfaces as BrokenResourceError/ECONNRESET
    rather than a clean 0-byte EOF. Benign — _iter_packets() already
    classifies it as a normal disconnect — but it looks alarming in
    transport logs.

The plan doc was reconciled against all of the above in
7e20585f, per the shared contract's "if the doc disagrees
with the code, the code wins" rule.

Testing

The acceptance bar for any backend is that the entire existing
suite passes under it unmodified. The refreshed blocking matrix passes
on Linux with TCP, UDS and TIPC and on macOS with TCP, alongside sdist
and docs. Local verification collected 479 tests, passed all
43 IPC tests, and passed all 35 TIPC-specific tests.

TODOs before landing

Future follow up

All filed as follow-up-labelled issues,

  • #495TIPC_IMPORTANCE supervision QoS on the parent<->child
    chan. Genuinely novel: no other backend can rank a conn's traffic
    under congestion.
  • #496TIPC_TOP_SRV-driven push registry in
    discovery._registry. The consumer side of
    open_topology_events(), and what would move the needle on Discovery and concensus: research and discussion. #184 /
    Multi-root discovery: pragmatic, simple consensus. #216.
  • #497 — dual-link resiliency as two TIPC udp bearers over
    two distinct wg paths, rather than assuming two physical NICs.
  • #498/tipc multiaddr spec submission, with the composed
    /ip4/…/udp/…/wg/u<key>/tipc/<stype>/<inst>/<scope> form as the
    real target. Goes up alongside wg multiaddr protocol: upstream spec submission plan #483 and unblocks the "return
    Multiaddr everywhere" item in Follow-up: multiaddr_support (PR #429) #443.
  • #499 — registrar-less discovery via name derivation, since a
    bind already is a registration.
  • #500 — TIPC multicast / group msging as a broadcast tpt for
    tractor.trionics fan-out (explicitly NOT a MsgTransport).
  • #501 — post-bind verification for instance collisions, the
    escalation if the blake2b digest ever proves too narrow.
  • #502TIPC over a wg mesh, the intended reference
    multihost deployment. NB the motivation is not confidentiality
    (TIPC ships its own AES-GCM crypto); it's public-key identity, NAT
    traversal and one overlay every tpt can share.

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

(this update was generated in some part by opencode using
gpt-5.6-sol (openai))

Links

goodboy added 22 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`))
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`))
`pformat_boxed_tb()` has never accepted an `indent` kwarg but
`pformat_caller_frame(box_tb=True)` has been passing one since
`888af602`. Nothing in the suite covered the branch, so the
`TypeError` only ever surfaced from `_mk_send_mte()` — i.e.
EVERY send-side `MsgTypeError` blew up while formatting itself
and masked the real msg-spec violation behind a bogus
`TypeError`.

Red on purpose per the test-first convention; the 1-line fix
lands next.

Also pin `pformat_boxed_tb()`s signature so a future typo'd
kwarg fails loudly at the call site instead of only when some
rare error path runs.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Just drop it — `pformat_boxed_tb()` spells its knobs
`tb_box_indent`/`tb_body_indent`, and that fn's default
(1-space box indent) is what the caller wanted anyway.

Regressed-by: 888af60 (`pformat_cs()` mv into `.devx.pformat`)
Found-via: `/run-tests` test_pformat_caller_frame_renders

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Guard test for `.start_listener()`s post-bind
`getsockname()`-vs-`.addr` round-trip, landed *before* that
reconciliation gets gated on an opt-out `ClassVar`.

- tcp: a `port=0` bind MUST still learn the kernel-picked
  port, since the reconciliation is the only path that ever
  does.
- uds: the sock-file path must survive the `.from_addr()`
  round-trip unchanged.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Gate `Endpoint.start_listener()`s `getsockname()`-vs-`.addr`
reconciliation on a new per-addr-type `ClassVar[bool]`, set
`True` on both `TCPAddress` and `UDSAddress` so existing
behaviour is bit-for-bit unchanged.

That reconciliation exists ONLY to learn a kernel-assigned
port from a `port=0` tcp bind (its own comment says so). The
incoming `tipc` backend (gh #378) has no late-binding
analogue AND its `getsockname()` answers a `TIPC_ADDR_ID`
port-id rather than the name-seq it published — rebinding
from that would swap a dialable service name for an
un-dialable, un-reconstructable port id.

So opting out is semantically right rather than a hack.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
First slice of the `AF_TIPC` tpt backend: the addr type, the
`is_tipc_available()` capability predicate and the
name-publishing listener. No `MsgTransport` yet.

An actor's TIPC addr is a *service name* `(stype, instance)`:
`.bind()`ing the singleton `TIPC_ADDR_NAMESEQ` range IS the
service registration (it shows up in `tipc nametable show`)
and a peer's `.connect()`-by-name IS the lookup — so the
kernel does discovery for us, no registrar hop.

Deats,
- `.unwrap()` is proto-keyed as `('tipc', stype, inst, scope)`
  using the `multiaddr` proto spelling so `wrap_address()`
  can't confuse it with `tcp`s or `uds`s 2-tuples.
- `.rebind_from_sockname = False` bc `getsockname()` answers
  a port-id; `.from_addr()` raises on a bare `TIPC_ADDR_ID`
  rather than fabricate an un-dialable addr.
- `.bindspace` is the TIPC *scope*, i.e. literally the set of
  hosts a published name is reachable from. `ZONE` scope is
  deprecated/aliased so fold it to `CLUSTER` on input.
- mod stays importable on non-linux (uapi-value fallbacks,
  the `_uds.SO_PASSCRED` precedent) bc `._addr` builds its
  registration tables at import time.

XXX a `.get_random()` clash does NOT raise `EADDRINUSE` —
TIPC accepts multiple publishers of one name and round-robins
connects between them (verified against a live kernel), so a
collision is *silent crosstalk*. Hence the `blake2b` digest
and its (birthday-bounded) collision test.

Also,
- a generic `.is_available() -> (ok, why_not)` classmethod;
  deliberately spelled generically (NOT `is_tipc_*`) so the
  sibling env-dependent backends — `quic`/`iroh` (gh #353)
  and the `wg` netns bindspace (gh #482) — get the same gate
  for free. Its consumer lands w/ the reg tables.
- register a `tipc` pytest mark; the kernel-touching cases
  self-skip unless `sudo modprobe tipc` has been run.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Wire `.connect_to()` (dial by service name), `.connected()`
and `.get_stream_addrs()` on top of `MsgpackTransport` so
`trio.SocketStream` + the existing `<I`-prefix framing carry
`msgpack` msgs over TIPC unchanged.

XXX both ends of a connected TIPC sock answer `TIPC_ADDR_ID`
port-ids and a port-id carries NO service name, so,
- the *dialling* side re-asserts the name it actually dialled
  over `._raddr` (same move as `MsgpackUDSStream`s peer-pid
  re-assign),
- the *accepting* side keeps a `TIPC_NAME_UNKNOWN` sentinel
  plus the observed `(node, ref)`. It doesn't need more — the
  `Aid` from `._do_handshake()` already carries the peer's
  logical identity.

Also normalize dial failures: TIPC answers an unpublished-name
lookup with `EHOSTUNREACH`, which python maps to a **bare**
`OSError` and NOT a `ConnectionError` subtype the way
`ECONNREFUSED` maps to `ConnectionRefusedError`. The
discovery-ping path needs the `ConnectionError` shape, so the
`_reraise_as_connerr()` wrap is load-bearing, not polish.

XXX ALSO tolerate a dead peer in `.get_stream_addrs()`!
Unlike tcp/uds — where the kernel keeps answering the peer
addr until *we* close — TIPC answers `ENOTCONN` once the peer
is gone. Since `MsgpackTransport.__init__()` calls
`.get_stream_addrs()` (via `Channel.from_stream()`) BEFORE the
handshake, an unguarded `OSError` there escapes
`handle_stream_from_peer()`s handshake tolerance (contract §4)
and tears down the WHOLE actor. Any connect-then-drop peer — a
port scan, a liveness probe, a cancelled dial — was a remote
actor-kill. A dead peer must cost us an addr, not the runtime.

Deats,
- `TIPC_IMPORTANCE` exposed as a `.connect_to()` kwarg — TIPC
  can rank a conn's traffic under congestion, which no other
  backend can do. Defaulted to the kernel default for now;
  wiring the parent<->child chan to `HIGH` is a follow-up.
- `TIPC_DEST_DROPPABLE = 0` so undeliverable msgs surface as
  errors instead of being silently dropped.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
`SpawnSpec.reg_addrs`/`.bind_addrs` pinned the wire shape to
a 2-tuple, so a `tipc` addr (`('tipc', stype, inst, scope)`)
died at the child w/ `msgspec.ValidationError: Expected array
of length 2, got 4` -> `invalid SpawnSpec IPC msg`.

Point those fields at `UnwrappedAddress` (which `SpawnSpec`s
own TODO already asked for) and widen the alias.

XXX VARIADIC (`tuple[str|int, ...]`) rather than a union of
the two concrete shapes, bc `msgspec` refuses a union holding
more than one array-like type.

?TODO, the real fix is the full proto-key migration (contract
§1.1) after which this becomes a tagged union keyed off elem
0 and per-proto validation comes back.

Note the alias is declared TWICE — `.msg.types` re-declares it
to dodge a circular import (`._addr` -> `.ipc._tcp` -> `.msg`)
and *that* copy is what actually validates the wire msg.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Wire the backend through every registration site (contract §2)
so `--tpt-proto tipc` is a first-class suite mode,
- `_state.TransportProtocolKey` gains the key
- `_addr._address_types` + `._default_lo_addrs`
- `_addr.wrap_address()` gets a `case ('tipc', *_)`; being a
  4-elem seq it can't collide w/ `tcp`s or `uds`s 2-tuple
  cases, so NO ordering hazard (and a bare seq-pattern matches
  the `list` form `msgpack` decodes to).
- `_types`: the `Address` union, `_msg_transports`,
  `_key_to_transport`, `_addr_to_transport` and the
  `transport_from_stream()` family match. That last one keys
  off `._tipc.AF_TIPC` (which carries the uapi fallback) NOT
  `socket.AF_TIPC` which is linux-only.

Test-harness side,
- `get_rando_addr()` gains a `tipc` branch; `.get_random()`
  already salts w/ `uuid4`+pid so both within- and cross-proc
  isolation come for free.
- the `tpt_protos` fixture calls an addr-type's optional
  `.is_available()` and `pytest.fail()`s w/ its reason. Keeps
  a module-less box from turning `--tpt-proto tipc` into a few
  hundred confusing connect-timeouts. Generic on purpose —
  plans 02/03 need the same hook.
- the discovery `daemon` fixture's readiness probe learns to
  dial a TIPC service name (it previously assumed tcp-or-uds
  and blew up on the 4-tuple).

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
`mk_maddr()`/`parse_maddr()` learn,

    /tipc/<stype>/<instance>/<scope>

mirroring how `uds` maps onto the spec-legal `/unix`.

XXX `str`-ONLY for now: there is no registered `/tipc` proto
in the multiaddr table (upstream track gh #483 +
multiformats/py-multiaddr#107) and `Multiaddr()` rejects an
unregistered name outright. `MsgTransport.maddr`s return type
is already `Multiaddr|str` (and `MsgpackUDSStream` already
exercises the `str` branch), so this fits — but it IS why gh

`parse_maddr()` therefore special-cases the `/tipc/` prefix
BEFORE handing anything to `Multiaddr()`.

Also drive the maddr mapping-table tests off `_address_types`
instead of a hardcoded len/dict so the next backend can't
fail them for the wrong reason.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
@goodboy
goodboy force-pushed the wkt/tipc_backend_378 branch from a2e6c10 to 51d7133 Compare August 14, 2026 14:02
@goodboy
goodboy changed the base branch from main to ng_tpts_planning August 14, 2026 14:03
@goodboy goodboy changed the title TIPC tpt protocol support 😲 TIPC (draft) support 😲 Aug 14, 2026
@goodboy goodboy added (typed) IPC and transport (protos) messaging messaging patterns and protocols discovery protos, systems, frameworks labels Aug 14, 2026
@goodboy
goodboy requested review from guilledk and a lite review from Copilot August 14, 2026 14:42
Follow-on to 4aa7a89 now that the encryption premise is
corrected: reframe *why* we want a `wg` mesh under TIPC (#502)
rather than leaving a "wg adds the crypto TIPC lacks" reading
lying around, since that reading is flat wrong.

The motivation is different but still real,
- TIPC's keys are **symmetric + pre-shared**, so distribution,
  rotation and revocation are all on the operator; `wg` brings
  public-key identity and a handshake.
- `wg` is an overlay *every* tpt can sit on (tcp now, quic
  later), not a TIPC-only mechanism.
- NAT traversal / roaming, which raw TIPC bearers have no story
  for at all.

Which to actually default to wants **benchmarking** — native
crypto skips a tunnel hop and may win for LAN-local clusters.

Also lean much harder on the udp-bearer-only caveat in the
handoff doc; it's the one that bites. A wg iface is L3/`tun` w/
no L2 addr, so there's no device for `media eth` to name — which
means #378's "ethernet bearers pair most excellently w/ wg
tunnelling" framing does NOT hold: on a given link the L2 path
and the wg path are mutually exclusive. Any design assuming both
is broken from the start.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Record #493's current draft head, #492's advanced planning
tip and the exact restack sequence before final landing.

Also,
- keep the unrelated `pformat` red-test/fix pair ordered for
  its standalone `main` PR
- distinguish the 17 substantive arc commits from the
  local-cache ignore
- make the in-repo handoff authoritative over agent memory
- preserve digest/drift checks for already-authorized forge
  writes

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))

@goodboy goodboy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[P1] Keep Linux-only constants out of macOS collection

tests/ipc/test_tipc.py:12-17 | confidence: high | category: portability

The module imports SOL_TIPC directly from socket. On macOS that symbol does not exist, and pytest collects this module even during a TCP run. Consequently, the entire macOS suite aborts during collection.

Evidence: the current #493 CI job fails with:

ImportError: cannot import name 'SOL_TIPC' from 'socket'

Recommendation: import SOL_TIPC from tractor.ipc._tipc, which already provides the cross-platform UAPI fallback, or otherwise guard the Linux-only import.

[P1] Include actor UUID in generated service names

tractor/ipc/_tipc.py:382-403 | confidence: high | category: correctness

With a live runtime, TIPCAddress.get_random() hashes only actor.aid.name and the process-local PID. Actors with the same name and PID on different hosts therefore publish the same cluster-wide TIPC name.

Because duplicate binds succeed and connections round-robin, this produces silent cross-tree routing rather than EADDRINUSE. PID overlap across hosts is normal, so this is materially more likely than the documented random 32-bit collision.

The collision test at tests/ipc/test_tipc.py:179-204 exercises only the no-runtime branch, which includes a per-call UUID and therefore misses the production branch.

Recommendation: derive the instance from actor.aid.uuid, optionally retaining the actor name for domain separation. Add a regression test where two actors have equal names/PIDs but different UUIDs.

Related follow-ups: #499 owns stable (name, uuid) derivation;
#501 owns post-bind collision verification.

[P2] Make the two-host walkthrough executable

examples/multihost/tipc_cluster/host_a_srv.py:50-55
examples/multihost/tipc_cluster/host_b_client.py:29-47
confidence: high | category: correctness

The advertised two-host example has two deterministic blockers:

  • host_a is the registrar itself and is never registered in its own Registrar._registry, so find_actor('host_a') returns None.
  • If a portal is obtained another way, Portal.open_context() receives the string 'host_a_srv:echo', but NamespacePath.from_ref() requires a callable and accesses ref.__module__.

The documented multihost demonstration therefore cannot reach echo().

Recommendation: spawn and register a named service actor on host A, and import/pass the enabled echo callable on host B, following the existing working RPC examples.

Related follow-up: #502 owns the reference multihost deployment.

[P2] Handle peer withdrawal during post-connect setup

tractor/ipc/_tipc.py:685-700 | confidence: high | category: reliability

MsgpackTIPCStream.__init__() tolerates getpeername() failing after a peer disconnects, but connect_to() immediately calls sock.getpeername() again without that protection.

A service that accepts and closes promptly can make this second call raise ENOTCONN. The successful dial then fails with a raw OSError, and the now-wrapped socket is not deterministically closed.

Recommendation: reuse the tolerant address already obtained during construction, retain destaddr without a port ID when unavailable, and keep socket/stream ownership under cleanup protection until initialization completes.

[P2] Do not fabricate topology-event scope

tractor/ipc/_tipc.py:941-949
tractor/ipc/_tipc.py:1003-1062
confidence: high | category: correctness

The scope argument is not encoded in struct tipc_subscr, the topology event contains no scope, and this implementation does not include it in the topology-server connection. Calls differing only by scope therefore send identical kernel requests.

Nevertheless, every received event is labeled with the caller-supplied scope. Consumers can consequently treat a publication as cluster-visible even though that scope was never observed or filtered.

Recommendation: represent event scope as unknown or explicitly as caller context. Remove the claim that subscriptions are per-scope unless an actual filtering mechanism is added.

Related follow-up: this is prerequisite feed correctness for #496.

[P2] Surface topology-stream overflow

tractor/ipc/_tipc.py:979-989 | confidence: high | category: reliability

When the memory channel fills, publication and withdrawal events are dropped with only a warning. A push registry can then permanently retain a withdrawn actor or miss a newly published actor while continuing to treat its state as authoritative.

Recommendation: terminate the stream with an explicit overflow/resync signal, or use backpressure if kernel-queue behavior permits it. Consumers must not continue without knowing their view is incomplete.

Related follow-up: #496 is the consumer that needs an authoritative
feed or an explicit resync signal.

[P3] Close finite subscriptions after timeout

tractor/ipc/_tipc.py:929-999 | confidence: high | category: reliability

A TIPC_SUBSCR_TIMEOUT event is forwarded and then the reader resumes waiting. The socket has no remaining subscription, so a caller that receives the timeout and asks for another event waits indefinitely.

Recommendation: return from _stream_name_events() after forwarding the timeout event so the send channel closes.

Related follow-up: #496 will consume this channel lifecycle.

Stack Context
The current submitted PR and contextual branches are not at one common tip:

  • #493 head: 1298ba945f9d0a2dfcde014be39c10d8e9169878
  • Forge-reported #493 base snapshot: ee17ed9f6e13d955029b2f30c296d036aacc1434
  • Current #492 / ng_tpts_planning: d9a6e2e9b4213bb0900b99deda851cb2eaaa2b1b
  • Local merge base: ee17ed9f
  • Current-upstream divergence: 2 commits on ng_tpts_planning, 19 on #493

The staged wkt/addr_unpacking work is prospective context, not part of #493. Before combining them:

  • Preserve #493’s variadic UnwrappedAddress and TIPC registration; the WIP starts from the older two-element alias.
  • Peel TunnelledAddress before transport_from_addr() and Endpoint; both currently dispatch by exact wrapper type/module.
  • Generalize the WIP’s string-only bindspace annotation because TIPCAddress.bindspace is an integer scope.
  • Either peel before endpoint reconciliation or delegate rebind_from_sockname; TIPC must retain False.
  • The WIP’s intentionally lossy .unwrap() remains compatible with TIPC’s four-element overlay descriptor.

The composed-address specification is tracked by #498, while #502
owns the resulting WireGuard/TIPC deployment.

The WIP’s unstaged .claude/settings.local.json was excluded.

Checks Run

  • Submitted range whitespace check passed:
    git diff --check ee17ed9f...1298ba94
  • Prospective current-upstream range check passed:
    git diff --check d9a6e2e9...1298ba94
  • WIP staged diff check passed for its four staged source/test files.
  • Current CI inspected at head 1298ba94.
  • Ubuntu TCP, UDS, TIPC, sdist, and Sphinx checks passed.
  • macOS TCP failed during collection as described above.

Checks Not Run
No local tests or analyzers were executed because this was a read-only review. Existing CI results were used as runtime evidence.

Scope
Reviewed GitHub PR #493 at exact range ee17ed9f...1298ba94, covering all 30 changed paths. Provider diff-base OID was unavailable. Current #492 and the staged wkt/addr_unpacking changes were inspected as prospective integration context, not folded into the submitted PR diff.

(this review was generated in some part by opencode using gpt-5.6-sol
(openai))

Restrict proto-key matching to numeric 3- or 4-element
descriptors so a UDS directory named `tipc` stays UDS.

Route `/tipc` parsing through `TIPCAddress.from_addr()` to
normalize zone scope and report malformed input clearly. Also
align UDS unwrapped metadata with its actual `(str, str)` shape.

Keep the TIPC test module portable by importing `SOL_TIPC` from
the backend's UAPI fallback instead of the host `socket` module.

Review: PR #493 (copilot-pull-request-reviewer[bot],goodboy)
#493

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
TIPC service names span the cluster while PIDs remain host-local.
Hashing only `(name, pid)` could therefore make same-named actors
on different hosts silently share one round-robin service name.

Derive the live-runtime seed from `Aid.uid` so the actor UUID
separates those names while keeping each identity reproducible.
Pin both properties with a deterministic regression test.

Review: PR #493 (copilot-pull-request-reviewer[bot],goodboy)
#493

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
Reject TIPC availability outside Linux before probing the fallback
socket-family integer, which can alias an unrelated family on
another OS.

Keep dialled sockets under setup ownership through transport
construction, then reuse the constructor's tolerant peer
observation. A peer withdrawing after `.connect()` can no longer
trigger a second raw `getpeername()` or leak setup resources.

Review: PR #493 (copilot-pull-request-reviewer[bot],goodboy)
#493

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
Stop labeling topology events with caller-supplied scope that the
kernel never reports. Event addresses now carry an explicit unknown
scope instead of fabricated reachability.

Apply memory-channel backpressure rather than silently dropping
publish/withdraw transitions, and close the stream after delivering
the terminal event from a finite subscription.

Review: PR #493 (copilot-pull-request-reviewer[bot],goodboy)
#493

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
A registrar root does not register itself in its own actor-name
registry, so host B could never discover the advertised `host_a`.
Boot that service as a child actor under the `TIPC` registrar
instead.

Import and pass the enabled `echo` callable to `.open_context()`;
the prior module-path string could not produce a `NamespacePath`.

Review: PR #493 (copilot-pull-request-reviewer[bot],goodboy)
#493

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
Keep `_stream_name_events()` non-blocking so a slow memory-channel
consumer cannot back up the kernel topology queue. Raise
`TIPCNameEventOverflow` and end the subscription rather than drop a
transition or let the socket reader stall. Discovery consumers must
then resubscribe and rebuild their name-table view.

Also,
- document topology semantics and scope with Linux references
- diagram the `.connect()`/`.getpeername()` withdrawal schedules
- explain the child-service and callable requirements in the
  two-host example

Review: PR #493 (copilot-pull-request-reviewer[bot],goodboy)
#493

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
@goodboy

goodboy commented Aug 18, 2026

Copy link
Copy Markdown
Owner Author

Review follow-up

All findings from the full review are addressed at current head 53516b09:

  • macOS-safe SOL_TIPC import and strict address dispatch: a19a639d
  • UUID-derived actor service names: d52c78c1
  • post-connect peer-withdrawal handling: 145782d3
  • unknown topology scope and finite-timeout closure: be6f9e86
  • executable two-host service example: c1501a36
  • explicit topology overflow/resync failure: 53516b09

Regression coverage was added with each fix. Local IPC verification passed (43 passed), full collection passed (479 tests collected), and the refreshed CI run is in progress. The four Copilot inline threads also have commit-linked responses.

The refreshed PR matrix passes on Ubuntu with the TIPC kernel
module loaded, along with the TCP, UDS and macOS legs. Remove the
temporary `continue-on-error` expression so future TIPC
regressions block CI.

Prompt-IO: ai/prompt-io/opencode/20260819T003326Z_53516b09_prompt_io.md

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
Turn the physical-host sketch into an operator runbook covering
cluster identity, interface and bearer setup, link validation,
failure/rejoin testing, diagnostic capture and cleanup.

Explain the cluster-domain-socket analogy and identify a future
`pyroute2` TIPC codec as the path from manual `tipc(8)` commands to
the same netlink management stack planned for WireGuard.

Authorize `host_a_srv` by its stable import name so direct script
execution does not expose only `__main__` while host B requests the
callable's actual `NamespacePath`.

Prompt-IO: ai/prompt-io/opencode/20260819T003327Z_53516b09_prompt_io.md

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
Propose a fixed-width service endpoint carrying the TIPC type,
instance and publication scope, with one canonical structured value
that generic multiaddr parsers can compose normally.

Retain the kernel-standard `tipc` name while using “Cluster Domain
Sockets” as explanatory terminology. Document the binary and text
encodings, WireGuard composition, deployment-management boundary,
upstream sequence, test vector and open maintainer questions.

Prompt-IO: ai/prompt-io/opencode/20260819T003328Z_53516b09_prompt_io.md

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
@goodboy
goodboy marked this pull request as ready for review August 19, 2026 19:22
@goodboy goodboy changed the title TIPC (draft) support 😲 Add an AF_TIPC transport backend Aug 19, 2026
@goodboy
goodboy requested review from ryanhiebert and salotz August 19, 2026 20:40
@goodboy goodboy added enhancement New feature or request experiment Exploratory design and testing api streaming examples dependencies Pull requests that update a dependency file integration Optional/loose support for 3rd party libs/apps/projects labels Aug 20, 2026
@goodboy
goodboy force-pushed the ng_tpts_planning branch 3 times, most recently from 4730b4c to 768b531 Compare August 30, 2026 02:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api dependencies Pull requests that update a dependency file discovery protos, systems, frameworks enhancement New feature or request examples experiment Exploratory design and testing integration Optional/loose support for 3rd party libs/apps/projects messaging messaging patterns and protocols streaming (typed) IPC and transport (protos)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants