diff --git a/ai/tpt-backends/00_shared_backend_contract.md b/ai/tpt-backends/00_shared_backend_contract.md index 690ffa8b..f46873a2 100644 --- a/ai/tpt-backends/00_shared_backend_contract.md +++ b/ai/tpt-backends/00_shared_backend_contract.md @@ -70,6 +70,40 @@ Hard constraints learned from the existing two: `wrap_address()` (`_addr.py:230`), you have a bug that manifests as the wrong transport being loaded — the file's own `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::', 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 runtime.** See the `UDSAddress.get_random()` uuid-token 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. 4. `tractor/discovery/_addr.py:230` `wrap_address()` `match` — add a case iff your `unwrapped_type` isn't already uniquely - matched. Prefer unwrapped forms that are *self-tagging* - (see plan 01 §2.2 and plan 02 §2.2) so this stays cheap. + matched. **Preferably do the proto-key migration in §1.1 + 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, `_msg_transports` list, `_key_to_transport[('msgpack', key)]`, `_addr_to_transport[Address]`. diff --git a/ai/tpt-backends/01_tipc_backend.md b/ai/tpt-backends/01_tipc_backend.md index 8a0fa413..ccbdf3be 100644 --- a/ai/tpt-backends/01_tipc_backend.md +++ b/ai/tpt-backends/01_tipc_backend.md @@ -98,34 +98,34 @@ class TIPCAddress( def_bindspace: ClassVar[int] = TIPC_CLUSTER_SCOPE ``` -**Unwrapped form** (the wire/`SpawnSpec` shape) — must be -uniquely matchable in `wrap_address()`: +**Unwrapped form** (the wire/`SpawnSpec` shape). + +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 -def unwrap(self) -> tuple[str, int]: - # ('tipc::', instance) - return (f'tipc:{self._stype}:{self._scope}', self._instance) +def unwrap(self) -> tuple[str, int, int, int]: + return ('tipc', self._stype, self._instance, self._scope) ``` -i.e. `(str, int)` — *the same coarse shape as `TCPAddress`*, so -`wrap_address()`'s `case (str(), int())` would steal it. Two -options; **pick (a)**: +`wrap_address()` then dispatches `_address_types[addr[0]]` and +the collision class disappears. **This is a prerequisite +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 - one**, guarded on `addr[0].startswith('tipc:')`: - ```python - case (str() as h, int()) if h.startswith('tipc:'): - cls = TIPCAddress - ``` - 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. +⚠️ an earlier revision of this plan proposed a self-tagging +`('tipc::', instance)` string-prefix hack with an +ordered `case` guard. **Dropped** — it papers over the problem, +keeps `wrap_address()` order-sensitive, and doesn't help iroh's +`(str, str)`-vs-UDS collision at all. Do not resurrect it. Note `TIPCAddress` is the first backend where `.unwrap()` is **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 +- **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) - `TIPC_TOP_SRV`-driven push registry in `discovery/_registry.py` (§5.2)