From 284a967936708e7ae9f1a6720fec81783b16a034 Mon Sep 17 00:00:00 2001 From: goodboy Date: Thu, 20 Aug 2026 10:18:54 -0400 Subject: [PATCH] Decode tagged transport addresses - Define canonical `tcp` and `unix` tuple shapes while retaining legacy pair aliases as the emitted `UnwrappedAddress`. - Dispatch tagged tuple/list payloads explicitly, accept `uds` as a Unix input alias, and preserve legacy TCP, UDS, and native IPv6 readers. - Cover tag aliases, msgpack-style lists, legacy payloads, and IPv6 socket addresses before switching writers. Prompt-IO: ai/prompt-io/opencode/20260820T033107Z_ba07e09d_prompt_io.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`)) --- .../20260820T033107Z_ba07e09d_prompt_io.md | 41 ++++++++ ...20260820T033107Z_ba07e09d_prompt_io.raw.md | 25 +++++ tests/discovery/test_address_serialization.py | 97 +++++++++++++++++++ tractor/discovery/_addr.py | 70 ++++++++++--- tractor/ipc/_tcp.py | 21 +++- tractor/ipc/_uds.py | 22 +++-- 6 files changed, 254 insertions(+), 22 deletions(-) create mode 100644 ai/prompt-io/opencode/20260820T033107Z_ba07e09d_prompt_io.md create mode 100644 ai/prompt-io/opencode/20260820T033107Z_ba07e09d_prompt_io.raw.md create mode 100644 tests/discovery/test_address_serialization.py diff --git a/ai/prompt-io/opencode/20260820T033107Z_ba07e09d_prompt_io.md b/ai/prompt-io/opencode/20260820T033107Z_ba07e09d_prompt_io.md new file mode 100644 index 00000000..cca3ad1f --- /dev/null +++ b/ai/prompt-io/opencode/20260820T033107Z_ba07e09d_prompt_io.md @@ -0,0 +1,41 @@ +--- +model: gpt-5.6-sol +service: opencode +session: tractor-addr-unpacking +timestamp: 2026-08-20T03:31:07Z +git_ref: ba07e09d +scope: code +substantive: true +raw_file: 20260820T033107Z_ba07e09d_prompt_io.raw.md +--- + +## Prompt + +The human requested canonical tagged transport addresses with a +reader-first migration. TCP should decode `('tcp', host, port)`, Unix +should decode `('unix', path)`, `uds` should remain an accepted input +alias and internal transport key, and legacy tuple/list inputs must keep +working before writers switch formats. + +## Response summary + +Introduced canonical and compatibility address aliases, explicit tagged +dispatch, transport-specific tagged readers, and focused serialization +tests. Kept legacy pair inputs and native IPv6 socket values readable so +this boundary can ship before tagged emission. + +## Files changed + +- `tractor/discovery/_addr.py` - address aliases and tagged dispatch. +- `tractor/ipc/_tcp.py` - tagged, legacy, and IPv6 TCP decoding. +- `tractor/ipc/_uds.py` - canonical Unix and UDS-alias decoding. +- `tests/discovery/test_address_serialization.py` - reader compatibility + coverage. + +## Human edits + +The human supplied the canonical `tcp` and `unix` forms, chose `uds` as +an input-only serialization alias while preserving it as the runtime +transport key, and required a reader-first commit boundary. The agent +implemented those decisions; no direct manual source edits were +observed. diff --git a/ai/prompt-io/opencode/20260820T033107Z_ba07e09d_prompt_io.raw.md b/ai/prompt-io/opencode/20260820T033107Z_ba07e09d_prompt_io.raw.md new file mode 100644 index 00000000..73748f06 --- /dev/null +++ b/ai/prompt-io/opencode/20260820T033107Z_ba07e09d_prompt_io.raw.md @@ -0,0 +1,25 @@ +--- +model: gpt-5.6-sol +service: opencode +timestamp: 2026-08-20T03:31:07Z +git_ref: ba07e09d +diff_cmd: git diff HEAD~1..HEAD +--- + +# Raw output - tagged address readers + +The human requested a migration away from ambiguous untagged transport +tuples. They established `('tcp', host, port)` and `('unix', path)` as +canonical forms, retained `('uds', path)` as an input alias, and required +a reader-first compatibility boundary before changing emitted values. + +> `git diff HEAD~1..HEAD -- tractor/discovery/_addr.py tractor/ipc/_tcp.py tractor/ipc/_uds.py tests/discovery/test_address_serialization.py` + +Added explicit tagged address aliases and dispatch, taught TCP and UDS +readers to decode tagged tuple/list payloads, preserved legacy pair input, +and retained native IPv6 socket-address decoding. Added focused tests for +canonical tags, the UDS alias, msgpack-style lists, legacy pairs, and IPv6 +socket values. + +Focused reader tests and Ruff checks passed before the writer migration +was applied. diff --git a/tests/discovery/test_address_serialization.py b/tests/discovery/test_address_serialization.py new file mode 100644 index 00000000..98b4b7d2 --- /dev/null +++ b/tests/discovery/test_address_serialization.py @@ -0,0 +1,97 @@ +''' +Canonical tagged-address decoding and legacy input compatibility. + +''' +from pathlib import Path + +import pytest + +from tractor.discovery._addr import wrap_address +from tractor.ipc._tcp import TCPAddress +from tractor.ipc._uds import UDSAddress + + +@pytest.mark.parametrize( + 'value', + [ + ('tcp', '127.0.0.1', 1616), + ['tcp', '127.0.0.1', 1616], + ], +) +def test_decode_tagged_tcp_address(value): + ''' + Shape-only decoding cannot distinguish future transport address + forms. Feed canonical tuple and msgpack-style list values through + the compatibility boundary and prove the explicit `tcp` tag + selects the TCP backend without changing legacy emission yet. + + ''' + addr = wrap_address(value) + + assert type(addr) is TCPAddress + assert addr.unwrap() == ('127.0.0.1', 1616) + + +@pytest.mark.parametrize( + 'tag', + ['unix', 'uds'], +) +@pytest.mark.parametrize('container', [tuple, list]) +def test_decode_tagged_unix_address( + tag: str, + container: type, +): + ''' + Multiaddr calls the protocol `unix` while tractor's transport key + remains `uds`. Decode both spellings from tuple/list containers, + normalize them to one `UDSAddress`, and retain legacy pair + emission until the writer migration lands. + + ''' + value = container((tag, '/tmp/tractor/registry.sock')) + addr = wrap_address(value) + + assert type(addr) is UDSAddress + assert addr.sockpath == Path('/tmp/tractor/registry.sock') + assert addr.unwrap() == ( + '/tmp/tractor', + 'registry.sock', + ) + + +@pytest.mark.parametrize( + 'value, expected_type', + [ + (('127.0.0.1', 1616), TCPAddress), + (['127.0.0.1', 1616], TCPAddress), + (('/tmp/tractor', 'registry.sock'), UDSAddress), + (['/tmp/tractor', 'registry.sock'], UDSAddress), + ], +) +def test_decode_legacy_address_forms( + value, + expected_type: type, +): + ''' + Existing callers, config, and older msgpack payloads still + provide untagged pairs. Keep tuple/list forms readable while + canonical tagged decoding is introduced, proving this + reader-first commit does not break shipped input behavior. + + ''' + assert type(wrap_address(value)) is expected_type + + +def test_tcp_from_native_ipv6_sockname(): + ''' + `socket.getsockname()` returns a four-item IPv6 sockaddr which is + neither a wire form nor a legacy two-item pair. Preserve it as an + OS compatibility boundary and intentionally ignore unsupported + flow-info/scope-id fields when constructing `TCPAddress`. + + ''' + addr = TCPAddress.from_addr( + ('::1', 1616, 0, 0) + ) + + assert addr.unwrap() == ('::1', 1616) diff --git a/tractor/discovery/_addr.py b/tractor/discovery/_addr.py index 8d688432..57afa02b 100644 --- a/tractor/discovery/_addr.py +++ b/tractor/discovery/_addr.py @@ -18,7 +18,9 @@ from uuid import uuid4 from typing import ( Protocol, ClassVar, + Literal, Type, + TypeAlias, TYPE_CHECKING, ) @@ -64,20 +66,40 @@ log = get_logger() # seems like the right name as per, # https://www.geeksforgeeks.org/introduction-to-address-descriptor/ # -UnwrappedAddress = ( - # tcp/udp/uds - tuple[ - str, # host/domain(tcp), filesys-dir(uds) - int|str, # port/path(uds) - ] - # ?TODO? should we also include another 2 fields from - # our `Aid` msg such that we include the runtime `Actor.uid` - # of `.name` and `.uuid`? - # - would ensure uniqueness across entire net? - # - allows for easier runtime-level filtering of "actors by - # service name" +TaggedTCPAddress: TypeAlias = tuple[ + Literal['tcp'], + str, + int, +] +TaggedUnixAddress: TypeAlias = tuple[ + Literal['unix'], + str, +] +TaggedUDSAlias: TypeAlias = tuple[ + Literal['uds'], + str, +] +TaggedAddress: TypeAlias = ( + TaggedTCPAddress + |TaggedUnixAddress ) +# Input-only compatibility forms. `UnwrappedAddress` remains the +# emitted form until the tagged writer migration switches it in the +# next atomic change. +LegacyTCPAddress: TypeAlias = tuple[str, int] +LegacyUDSAddress: TypeAlias = tuple[str, str] +LegacyUnwrappedAddress: TypeAlias = ( + LegacyTCPAddress + |LegacyUDSAddress +) +UnwrappedAddress = LegacyUnwrappedAddress +# ?TODO? should we also include another 2 fields from our `Aid` msg +# such that we include the runtime `Actor.uid` of `.name` and `.uuid`? +# - would ensure uniqueness across entire net? +# - allows for easier runtime-level filtering of "actors by service +# name" + # TODO, maybe rename to `SocketAddress`? class Address(Protocol): @@ -216,7 +238,15 @@ def mk_uuid() -> str: def wrap_address( - addr: UnwrappedAddress|str|Address|TunnelledAddress, + addr: ( + TaggedAddress + |TaggedUDSAlias + |LegacyUnwrappedAddress + |list[str|int] + |str + |Address + |TunnelledAddress + ), ) -> Address|TunnelledAddress: ''' Wrap an `UnwrappedAddress` as an `Address`-type based @@ -239,6 +269,20 @@ def wrap_address( # import pdbp; pdbp.set_trace() match addr: + case ( + ('tcp', str(), int()) + | + ['tcp', str(), int()] + ): + return TCPAddress.from_addr(addr) + + case ( + (('unix' | 'uds'), str()) + | + [('unix' | 'uds'), str()] + ): + return UDSAddress.from_addr(addr) + # classic network socket-address as tuple/list case ( (str(), int()) diff --git a/tractor/ipc/_tcp.py b/tractor/ipc/_tcp.py index 293ae4be..3fefdf08 100644 --- a/tractor/ipc/_tcp.py +++ b/tractor/ipc/_tcp.py @@ -104,11 +104,26 @@ class TCPAddress( @classmethod def from_addr( cls, - addr: tuple[str, int] + addr: tuple|list, ) -> TCPAddress: match addr: - case (str(), int()): - return TCPAddress(addr[0], addr[1]) + case ( + ('tcp', str() as host, int() as port) + | + ['tcp', str() as host, int() as port] + | + (str() as host, int() as port) + | + [str() as host, int() as port] + | + ( + str() as host, + int() as port, + int(), + int(), + ) + ): + return TCPAddress(host, port) case _: raise ValueError( f'Invalid unwrapped address for {cls}\n' diff --git a/tractor/ipc/_uds.py b/tractor/ipc/_uds.py index d1e9d2f6..b1a5235b 100644 --- a/tractor/ipc/_uds.py +++ b/tractor/ipc/_uds.py @@ -146,16 +146,26 @@ class UDSAddress( def from_addr( cls, addr: ( - tuple[Path|str, Path|str]|Path|str + tuple|list|Path|str ), ) -> UDSAddress: match addr: - case tuple()|list(): - filedir = Path(addr[0]) - filename = Path(addr[1]) + case ( + (('unix' | 'uds'), str()|Path() as sockpath) + | + [('unix' | 'uds'), str()|Path() as sockpath] + ): + path = Path(sockpath) + return UDSAddress(*unwrap_sockpath(path)) + + case ( + (str()|Path() as filedir, str()|Path() as filename) + | + [str()|Path() as filedir, str()|Path() as filename] + ): return UDSAddress( - filedir=filedir, - filename=filename, + filedir=Path(filedir), + filename=Path(filename), # maybe_pid=pid, ) # NOTE, in case we ever decide to just `.unwrap()`