diff --git a/ai/prompt-io/opencode/20260820T033108Z_ba07e09d_prompt_io.md b/ai/prompt-io/opencode/20260820T033108Z_ba07e09d_prompt_io.md new file mode 100644 index 00000000..f9734961 --- /dev/null +++ b/ai/prompt-io/opencode/20260820T033108Z_ba07e09d_prompt_io.md @@ -0,0 +1,45 @@ +--- +model: gpt-5.6-sol +service: opencode +session: tractor-addr-unpacking +timestamp: 2026-08-20T03:31:08Z +git_ref: ba07e09d +scope: code +substantive: true +raw_file: 20260820T033108Z_ba07e09d_prompt_io.raw.md +--- + +## Prompt + +The human asked the agent to continue after adding tagged readers, +complete canonical address emission without dropping legacy input +compatibility, verify the migration, and prepare a complete multi-commit +package when the turn was done. + +## Response summary + +Changed `.unwrap()` to emit tagged TCP and Unix addresses, updated direct +tuple consumers and spawn payload declarations, and aligned multiaddr, +runtime, IPC, and discovery tests with canonical serialized equality. +Kept untagged tuples and the `uds` spelling readable at input boundaries. + +## Files changed + +- `tractor/discovery/_addr.py` - canonical output alias. +- `tractor/discovery/_multiaddr.py` - tagged address composition. +- `tractor/ipc/_tcp.py` - tagged emission and direct socket dialing. +- `tractor/ipc/_uds.py` - tagged full-path emission. +- `tractor/msg/types.py` - protocol-neutral spawn tuple containers. +- `tests/discovery/test_address_serialization.py` - writer assertions. +- `tests/discovery/test_multiaddr.py` - canonical round-trip assertions. +- `tests/discovery/test_tpt_bind_addrs.py` - tagged bind assertions. +- `tests/ipc/test_each_tpt.py` - canonical runtime address assertions. +- `tests/ipc/test_server_tunnel_boundary.py` - tagged TCP destructuring. +- `tests/test_local.py` - canonical registry comparison. + +## Human edits + +The human established the reader-before-writer sequencing, canonical tag +spellings, retained compatibility expectations, and requested final +multi-commit packaging. The agent implemented and tested those choices; +no direct manual source edits were observed. diff --git a/ai/prompt-io/opencode/20260820T033108Z_ba07e09d_prompt_io.raw.md b/ai/prompt-io/opencode/20260820T033108Z_ba07e09d_prompt_io.raw.md new file mode 100644 index 00000000..98b139d8 --- /dev/null +++ b/ai/prompt-io/opencode/20260820T033108Z_ba07e09d_prompt_io.raw.md @@ -0,0 +1,26 @@ +--- +model: gpt-5.6-sol +service: opencode +timestamp: 2026-08-20T03:31:08Z +git_ref: ba07e09d +diff_cmd: git diff HEAD~1..HEAD +--- + +# Raw output - canonical tagged address writers + +After the reader compatibility boundary, the human asked the agent to +continue the migration and package the completed work as dependency- +ordered commits. + +> `git diff HEAD~1..HEAD -- tractor/discovery/_addr.py tractor/discovery/_multiaddr.py tractor/ipc/_tcp.py tractor/ipc/_uds.py tractor/msg/types.py tests/discovery/test_address_serialization.py tests/discovery/test_multiaddr.py tests/discovery/test_tpt_bind_addrs.py tests/ipc/test_each_tpt.py tests/ipc/test_server_tunnel_boundary.py tests/test_local.py` + +Switched TCP and Unix `.unwrap()` output to canonical tagged tuples, +updated direct transport and multiaddr consumers, widened spawn message +tuple containers for protocol-specific shapes, and migrated runtime and +test comparisons to serialized address equality. Legacy inputs remain +accepted at `wrap_address()` and backend reader boundaries. + +Ruff and focused tests passed. The complete non-debugger TCP suite passed +with 412 tests; the UDS suite reached 80% without failure before the +harness timeout, then all 97 remaining tests passed on resume. Debugger +PTY coverage was excluded after an unrelated timeout. diff --git a/tests/discovery/test_address_serialization.py b/tests/discovery/test_address_serialization.py index 98b4b7d2..ee0450c9 100644 --- a/tests/discovery/test_address_serialization.py +++ b/tests/discovery/test_address_serialization.py @@ -23,13 +23,13 @@ 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. + selects the TCP backend and emits the canonical tagged form. ''' addr = wrap_address(value) assert type(addr) is TCPAddress - assert addr.unwrap() == ('127.0.0.1', 1616) + assert addr.unwrap() == ('tcp', '127.0.0.1', 1616) @pytest.mark.parametrize( @@ -44,8 +44,8 @@ def test_decode_tagged_unix_address( ''' 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. + normalize them to one `UDSAddress`, and emit the canonical `unix` + spelling. ''' value = container((tag, '/tmp/tractor/registry.sock')) @@ -54,8 +54,8 @@ def test_decode_tagged_unix_address( assert type(addr) is UDSAddress assert addr.sockpath == Path('/tmp/tractor/registry.sock') assert addr.unwrap() == ( - '/tmp/tractor', - 'registry.sock', + 'unix', + '/tmp/tractor/registry.sock', ) @@ -75,11 +75,14 @@ def test_decode_legacy_address_forms( ''' 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. + canonical tagged emission is introduced, proving the writer + migration does not break shipped input behavior. ''' - assert type(wrap_address(value)) is expected_type + addr = wrap_address(value) + + assert type(addr) is expected_type + assert addr.unwrap()[0] in {'tcp', 'unix'} def test_tcp_from_native_ipv6_sockname(): @@ -94,4 +97,4 @@ def test_tcp_from_native_ipv6_sockname(): ('::1', 1616, 0, 0) ) - assert addr.unwrap() == ('::1', 1616) + assert addr.unwrap() == ('tcp', '::1', 1616) diff --git a/tests/discovery/test_multiaddr.py b/tests/discovery/test_multiaddr.py index 67d73934..45727213 100644 --- a/tests/discovery/test_multiaddr.py +++ b/tests/discovery/test_multiaddr.py @@ -188,7 +188,7 @@ def test_parse_maddr_tcp_ipv4(): result = parse_maddr('/ip4/127.0.0.1/tcp/1234') assert isinstance(result, TCPAddress) - assert result.unwrap() == ('127.0.0.1', 1234) + assert result.unwrap() == ('tcp', '127.0.0.1', 1234) def test_parse_maddr_tcp_ipv6(): @@ -200,7 +200,7 @@ def test_parse_maddr_tcp_ipv6(): result = parse_maddr('/ip6/::1/tcp/5678') assert isinstance(result, TCPAddress) - assert result.unwrap() == ('::1', 5678) + assert result.unwrap() == ('tcp', '::1', 5678) def test_parse_maddr_uds(): @@ -213,9 +213,10 @@ def test_parse_maddr_uds(): result = parse_maddr('/unix/tmp/tractor_test/test.sock') assert isinstance(result, UDSAddress) - filedir, filename = result.unwrap() - assert filename == 'test.sock' - assert str(filedir) == '/tmp/tractor_test' + assert result.unwrap() == ( + 'unix', + '/tmp/tractor_test/test.sock', + ) def test_parse_maddr_unsupported(): @@ -249,7 +250,7 @@ def test_parse_wg_maddr(): bearer=('192.168.1.50', 51820), ) assert isinstance(parsed.overlay, TCPAddress) - assert parsed.overlay.unwrap() == ('10.0.11.1', 1616) + assert parsed.overlay.unwrap() == ('tcp', '10.0.11.1', 1616) def test_mk_wg_maddr_roundtrip(): @@ -445,7 +446,7 @@ def test_wrap_address_maddr_str(): result = wrap_address('/ip4/127.0.0.1/tcp/9999') assert isinstance(result, TCPAddress) - assert result.unwrap() == ('127.0.0.1', 9999) + assert result.unwrap() == ('tcp', '127.0.0.1', 9999) def test_wrap_address_wg_maddr_str(): @@ -460,7 +461,7 @@ def test_wrap_address_wg_maddr_str(): assert isinstance(result, TunnelledAddress) assert result.tunnel.peer_pubkey == _WG_PUBKEY - assert result.overlay.unwrap() == ('10.0.11.1', 1616) + assert result.overlay.unwrap() == ('tcp', '10.0.11.1', 1616) # ------ parse_endpoints() tests ------ @@ -481,11 +482,11 @@ def test_parse_endpoints_tcp_only(): reg_addr = result['registry'][0] assert isinstance(reg_addr, TCPAddress) - assert reg_addr.unwrap() == ('127.0.0.1', 1616) + assert reg_addr.unwrap() == ('tcp', '127.0.0.1', 1616) feed_addr = result['data_feed'][0] assert isinstance(feed_addr, TCPAddress) - assert feed_addr.unwrap() == ('0.0.0.0', 5555) + assert feed_addr.unwrap() == ('tcp', '0.0.0.0', 5555) def test_parse_endpoints_mixed_tpts(): @@ -505,12 +506,13 @@ def test_parse_endpoints_mixed_tpts(): assert len(addrs) == 2 assert isinstance(addrs[0], TCPAddress) - assert addrs[0].unwrap() == ('127.0.0.1', 4040) + assert addrs[0].unwrap() == ('tcp', '127.0.0.1', 4040) assert isinstance(addrs[1], UDSAddress) - filedir, filename = addrs[1].unwrap() - assert filename == 'broker.sock' - assert str(filedir) == '/tmp/tractor' + assert addrs[1].unwrap() == ( + 'unix', + '/tmp/tractor/broker.sock', + ) def test_parse_endpoints_wg_maddr(): @@ -550,7 +552,7 @@ def test_parse_endpoints_unwrapped_tuples(): addr = result['ems'][0] assert isinstance(addr, TCPAddress) - assert addr.unwrap() == ('127.0.0.1', 6666) + assert addr.unwrap() == ('tcp', '127.0.0.1', 6666) def test_parse_endpoints_mixed_str_and_tuple(): @@ -570,10 +572,10 @@ def test_parse_endpoints_mixed_str_and_tuple(): assert len(addrs) == 2 assert isinstance(addrs[0], TCPAddress) - assert addrs[0].unwrap() == ('127.0.0.1', 7777) + assert addrs[0].unwrap() == ('tcp', '127.0.0.1', 7777) assert isinstance(addrs[1], TCPAddress) - assert addrs[1].unwrap() == ('127.0.0.1', 8888) + assert addrs[1].unwrap() == ('tcp', '127.0.0.1', 8888) def test_parse_endpoints_unsupported_proto(): diff --git a/tests/discovery/test_tpt_bind_addrs.py b/tests/discovery/test_tpt_bind_addrs.py index ae3b3437..482f2b4b 100644 --- a/tests/discovery/test_tpt_bind_addrs.py +++ b/tests/discovery/test_tpt_bind_addrs.py @@ -183,7 +183,7 @@ def test_non_registrar_root_tpt_bind_addrs( for uw_addr in bound: w = wrap_address(uw_addr) if w.proto_key == 'tcp': - _host, port = uw_addr + _, _host, port = uw_addr assert port > 0 trio.run(_main) @@ -255,7 +255,7 @@ def test_tpt_bind_addrs_as_maddr_str( for uw_addr in actor.accept_addrs: w = wrap_address(uw_addr) if w.proto_key == 'tcp': - _host, port = uw_addr + _, _host, port = uw_addr assert port > 0 trio.run(_main) @@ -287,7 +287,7 @@ def test_registrar_merge_binds_union( # actually differ (always true for TCP, may # collide for UDS). expect_disjoint: bool = ( - tuple(reg_addr) != rando.unwrap() + reg_wrapped.unwrap() != rando.unwrap() ) async def _main(): diff --git a/tests/ipc/test_each_tpt.py b/tests/ipc/test_each_tpt.py index 5d1fdea3..ebedee26 100644 --- a/tests/ipc/test_each_tpt.py +++ b/tests/ipc/test_each_tpt.py @@ -57,17 +57,20 @@ def test_uds_bindspace_created_implicitly( root: Actor = tractor.current_actor() assert root.is_registrar + canonical_addr = _addr.wrap_address( + registry_addr, + ).unwrap() - assert registry_addr in root.reg_addrs + assert canonical_addr in root.reg_addrs assert ( - registry_addr + canonical_addr in _state._runtime_vars['_registry_addrs'] ) assert ( - _addr.wrap_address(registry_addr) + canonical_addr in - root.registry_addrs + [addr.unwrap() for addr in root.registry_addrs] ) trio.run(main) diff --git a/tests/ipc/test_server_tunnel_boundary.py b/tests/ipc/test_server_tunnel_boundary.py index 827af420..6c1f6dbb 100644 --- a/tests/ipc/test_server_tunnel_boundary.py +++ b/tests/ipc/test_server_tunnel_boundary.py @@ -46,8 +46,8 @@ def test_server_peels_before_endpoint_construction(): endpoint = eps[0] assert type(endpoint.addr) is TCPAddress - host, port = endpoint.addr.unwrap() - assert host == overlay.unwrap()[0] + _, host, port = endpoint.addr.unwrap() + assert host == overlay.unwrap()[1] assert port > 0 assert endpoint.addr is not tunnelled assert tunnels_of(tunnelled) == ( diff --git a/tests/test_local.py b/tests/test_local.py index b585651c..6b12b31a 100644 --- a/tests/test_local.py +++ b/tests/test_local.py @@ -8,6 +8,7 @@ import trio import tractor from tractor._testing import tractor_test +from tractor.discovery._addr import wrap_address def test_no_runtime(): @@ -48,7 +49,7 @@ async def test_self_is_registered_localportal(reg_addr): with trio.fail_after(0.2): sockaddr = await portal.run_from_ns( 'self', 'wait_for_actor', name='root') - assert sockaddr[0] == reg_addr + assert sockaddr[0] == wrap_address(reg_addr).unwrap() def test_local_actor_async_func(reg_addr): diff --git a/tractor/discovery/_addr.py b/tractor/discovery/_addr.py index 57afa02b..56bda3ca 100644 --- a/tractor/discovery/_addr.py +++ b/tractor/discovery/_addr.py @@ -84,16 +84,15 @@ TaggedAddress: TypeAlias = ( |TaggedUnixAddress ) -# Input-only compatibility forms. `UnwrappedAddress` remains the -# emitted form until the tagged writer migration switches it in the -# next atomic change. +# Input-only compatibility forms retained for older callers and +# serialized payloads. LegacyTCPAddress: TypeAlias = tuple[str, int] LegacyUDSAddress: TypeAlias = tuple[str, str] LegacyUnwrappedAddress: TypeAlias = ( LegacyTCPAddress |LegacyUDSAddress ) -UnwrappedAddress = LegacyUnwrappedAddress +UnwrappedAddress = TaggedAddress # ?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? @@ -104,7 +103,7 @@ UnwrappedAddress = LegacyUnwrappedAddress # TODO, maybe rename to `SocketAddress`? class Address(Protocol): proto_key: ClassVar[str] - unwrapped_type: ClassVar[UnwrappedAddress] + unwrapped_type: ClassVar[type] # TODO, i feel like an `.is_bound()` is a better thing to # support? diff --git a/tractor/discovery/_multiaddr.py b/tractor/discovery/_multiaddr.py index 8f626700..18f168c0 100644 --- a/tractor/discovery/_multiaddr.py +++ b/tractor/discovery/_multiaddr.py @@ -73,7 +73,7 @@ def mk_maddr( match proto_key: case 'tcp': - host, port = addr.unwrap() + _, host, port = addr.unwrap() ip = ipaddress.ip_address(host) net_proto: str = ( 'ip4' if ip.version == 4 @@ -84,13 +84,12 @@ def mk_maddr( ) case 'uds': - filedir, filename = addr.unwrap() - filepath = Path(filedir) / filename + _, sockpath = addr.unwrap() # NOTE, strip any leading `/` to avoid # double-slash `/unix//run/..` which the # multiaddr parser rejects as "empty # protocol path". - fpath_str: str = str(filepath).lstrip('/') + fpath_str: str = sockpath.lstrip('/') return Multiaddr( f'/{maddr_proto}/{fpath_str}' ) diff --git a/tractor/ipc/_tcp.py b/tractor/ipc/_tcp.py index 3fefdf08..819e1c81 100644 --- a/tractor/ipc/_tcp.py +++ b/tractor/ipc/_tcp.py @@ -21,6 +21,7 @@ from __future__ import annotations import ipaddress from typing import ( ClassVar, + TYPE_CHECKING, ) # from contextlib import ( # asynccontextmanager as acm, @@ -42,6 +43,9 @@ from tractor.ipc._transport import ( MsgpackTransport, ) +if TYPE_CHECKING: + from tractor.discovery._addr import TaggedTCPAddress + log = get_logger() @@ -62,7 +66,7 @@ class TCPAddress( ) from valerr proto_key: ClassVar[str] = 'tcp' - unwrapped_type: ClassVar[type] = tuple[str, int] + unwrapped_type: ClassVar[type] = tuple def_bindspace: ClassVar[str] = '127.0.0.1' # ?TODO, actually validate ipv4/6 with stdlib's `ipaddress` @@ -130,8 +134,9 @@ class TCPAddress( f'{addr}\n' ) - def unwrap(self) -> tuple[str, int]: + def unwrap(self) -> TaggedTCPAddress: return ( + self.proto_key, self._host, self._port, ) @@ -230,7 +235,8 @@ class MsgpackTCPStream(MsgpackTransport): **kwargs ) -> MsgpackTCPStream: stream = await trio.open_tcp_stream( - *destaddr.unwrap(), + destaddr._host, + destaddr._port, **kwargs ) return MsgpackTCPStream( diff --git a/tractor/ipc/_uds.py b/tractor/ipc/_uds.py index b1a5235b..dab0d6bb 100644 --- a/tractor/ipc/_uds.py +++ b/tractor/ipc/_uds.py @@ -63,6 +63,7 @@ from tractor.runtime._state import ( ) if TYPE_CHECKING: + from tractor.discovery._addr import TaggedUnixAddress from tractor.runtime._runtime import Actor @@ -114,7 +115,7 @@ class UDSAddress( # -[ ] need to check what other mult-transport frameworks do # like zmq, nng, uri-spec et al! proto_key: ClassVar[str] = 'uds' - unwrapped_type: ClassVar[type] = tuple[str, int] + unwrapped_type: ClassVar[type] = tuple def_bindspace: ClassVar[Path] = get_rt_dir() @property @@ -132,7 +133,7 @@ class UDSAddress( @property def sockpath(self) -> Path: - return self.bindspace / self.filename + return Path(self.bindspace) / self.filename @property def is_valid(self) -> bool: @@ -180,12 +181,10 @@ class UDSAddress( f'{addr!r}\n' ) - def unwrap(self) -> tuple[str, int]: - # XXX NOTE, since this gets passed DIRECTLY to - # `.ipc._uds.open_unix_socket_w_passcred()` + def unwrap(self) -> TaggedUnixAddress: return ( - str(self.filedir), - str(self.filename), + 'unix', + str(self.sockpath), ) @classmethod diff --git a/tractor/msg/types.py b/tractor/msg/types.py index 4f3e33cc..1cc2125c 100644 --- a/tractor/msg/types.py +++ b/tractor/msg/types.py @@ -211,10 +211,11 @@ class SpawnSpec( # module import capability enable_modules: dict[str, str] - # TODO: not just sockaddr pairs? - # -[ ] abstract into a `TransportAddr` type? - reg_addrs: list[tuple[str, str|int]] - bind_addrs: list[tuple[str, str|int]]|None + # Tagged addresses have protocol-specific tuple shapes which + # msgspec cannot express as one decodable union. `wrap_address()` + # validates each tuple at the transport boundary. + reg_addrs: list[tuple] + bind_addrs: list[tuple]|None # TODO: caps based RPC support in the payload?