Proto-key the unwrapped-addr form in the plans

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`))
ng_tpts_planning
Gud Boi 2026-08-12 19:37:08 -04:00
parent f60bdc44c7
commit 208876cc3b
2 changed files with 66 additions and 25 deletions

View File

@ -70,6 +70,40 @@ Hard constraints learned from the existing two:
`wrap_address()` (`_addr.py:230`), you have a bug that `wrap_address()` (`_addr.py:230`), you have a bug that
manifests as the wrong transport being loaded — the file's own manifests as the wrong transport being loaded — the file's own
`XXX NOTE` warns about precisely this. `XXX NOTE` warns about precisely this.
⚠️ **and shape-matching does not survive 4 backends.** Adding
TIPC and iroh breaks it outright: TIPC's natural 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. Ordering hacks and prefix-tagging (an earlier
revision of plan 01 proposed `('tipc:<stype>:<scope>', inst)`)
paper over it at best.
**The fix, and the recommended prerequisite for all three
backends: make the unwrapped form carry an explicit
proto-key, using the `multiaddr` protocol name as the
canonical spelling** — `('tcp', host, port)`,
`('unix', path)`, `('udp', ...)`, `('tipc', stype, inst,
scope)`. Then `wrap_address()` collapses from an
order-sensitive `match` to `_address_types[addr[0]]`, and the
whole collision class stops existing. Note this *also* aligns
the on-wire form with `mk_maddr()`/`parse_maddr()`, so the two
representations stop being independent inventions.
Two consequences to plan for:
- it's a **wire-format change** (`SpawnSpec`,
`_root_mailbox`, `_registry_addrs`) plus every test fixture
and downstream config (`piker`'s `[network]` table). It
wants its **own migration commit, landed before any new
backend**, not smuggled into one.
- it's the moment to **stop handing raw unwrapped tuples to
users at all.** The long-term shape is: `Address` subtypes
are the public currency and `UnwrappedAddress` becomes an
internal serialization detail — the same discipline
`ipaddress` uses (you pass `IPv4Address`, not a 4-tuple).
Public API should accept `Address|maddr-str` and treat bare
tuples as legacy-tolerated input, ideally deprecated.
- **`.get_random()` must be collision-free without a live - **`.get_random()` must be collision-free without a live
runtime.** See the `UDSAddress.get_random()` uuid-token runtime.** See the `UDSAddress.get_random()` uuid-token
comment (`_uds.py:207-220`): with no `current_actor()` the comment (`_uds.py:207-220`): with no `current_actor()` the
@ -214,8 +248,9 @@ Adding a backend touches these and only these:
entry lazy — propose that refactor explicitly. entry lazy — propose that refactor explicitly.
4. `tractor/discovery/_addr.py:230` `wrap_address()` `match` 4. `tractor/discovery/_addr.py:230` `wrap_address()` `match`
add a case iff your `unwrapped_type` isn't already uniquely add a case iff your `unwrapped_type` isn't already uniquely
matched. Prefer unwrapped forms that are *self-tagging* matched. **Preferably do the proto-key migration in §1.1
(see plan 01 §2.2 and plan 02 §2.2) so this stays cheap. first**, after which this step becomes a one-line
`_address_types` entry instead of an order-sensitive `case`.
5. `tractor/ipc/_types.py``Address` union alias, 5. `tractor/ipc/_types.py``Address` union alias,
`_msg_transports` list, `_key_to_transport[('msgpack', key)]`, `_msg_transports` list, `_key_to_transport[('msgpack', key)]`,
`_addr_to_transport[<Proto>Address]`. `_addr_to_transport[<Proto>Address]`.

View File

@ -98,34 +98,34 @@ class TIPCAddress(
def_bindspace: ClassVar[int] = TIPC_CLUSTER_SCOPE def_bindspace: ClassVar[int] = TIPC_CLUSTER_SCOPE
``` ```
**Unwrapped form** (the wire/`SpawnSpec` shape) — must be **Unwrapped form** (the wire/`SpawnSpec` shape).
uniquely matchable in `wrap_address()`:
TIPC's natural form is `(stype, instance, scope)` — but a
2-tuple squeeze of it is a `(str, int)`, i.e. *the same coarse
shape as `TCPAddress`*, so `wrap_address()`'s
`case (str(), int())` steals it. This backend is therefore the
forcing function for the contract-doc's conclusion (§1.1):
> **make the unwrapped form carry an explicit proto-key, spelled
> with the `multiaddr` protocol name.**
```python ```python
def unwrap(self) -> tuple[str, int]: def unwrap(self) -> tuple[str, int, int, int]:
# ('tipc:<stype>:<scope>', instance) return ('tipc', self._stype, self._instance, self._scope)
return (f'tipc:{self._stype}:{self._scope}', self._instance)
``` ```
i.e. `(str, int)`*the same coarse shape as `TCPAddress`*, so `wrap_address()` then dispatches `_address_types[addr[0]]` and
`wrap_address()`'s `case (str(), int())` would steal it. Two the collision class disappears. **This is a prerequisite
options; **pick (a)**: migration commit, not part of this backend** — see contract §1.1
for its blast radius (wire format + every fixture + `piker`
config) and for the follow-on "stop handing raw tuples to users
at all, à la `ipaddress`" direction.
- **(a) self-tagging prefix + a `case` ordered before the TCP ⚠️ an earlier revision of this plan proposed a self-tagging
one**, guarded on `addr[0].startswith('tipc:')`: `('tipc:<stype>:<scope>', instance)` string-prefix hack with an
```python ordered `case` guard. **Dropped** — it papers over the problem,
case (str() as h, int()) if h.startswith('tipc:'): keeps `wrap_address()` order-sensitive, and doesn't help iroh's
cls = TIPCAddress `(str, str)`-vs-UDS collision at all. Do not resurrect it.
```
Cheap, string-y, but honest about the fact that
`UnwrappedAddress` is a deliberately-degenerate
`tuple[str, int|str]` (see the `_addr.py:43-65` TODO block).
It also keeps `TCPAddress.__post_init__`'s
`ipaddress.ip_address()` validation from being reached with a
non-IP host.
- (b) widen `UnwrappedAddress` to a 3-tuple for tipc. Rejected:
ripples into every `reg_addr` fixture, `_root_mailbox`, and
downstream (`piker`) config.
Note `TIPCAddress` is the first backend where `.unwrap()` is Note `TIPCAddress` is the first backend where `.unwrap()` is
**not** a lossless view of the live socket — `maybe_node`/ **not** a lossless view of the live socket — `maybe_node`/
@ -689,6 +689,12 @@ single best demo this backend has; lead with it.
## 10. Follow-up issue seeds ## 10. Follow-up issue seeds
- **register `/tipc` in the multiaddr spec**, mirroring the `wg`
track (multiformats/py-multiaddr#107/#108 + gh #483). Same
shape of work: propose the proto + code, land a codec in
`py-multiaddr`, then drop our `str`-maddr fallback (§4). Worth
filing *alongside* the `wg` spec-submission issue so both
proposals go up together rather than as one-offs.
- registrar-less discovery fast path via name derivation (§5.1) - registrar-less discovery fast path via name derivation (§5.1)
- `TIPC_TOP_SRV`-driven push registry in - `TIPC_TOP_SRV`-driven push registry in
`discovery/_registry.py` (§5.2) `discovery/_registry.py` (§5.2)