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`))wkt/addr_unpacking
parent
ba07e09d2a
commit
284a967936
|
|
@ -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.
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -18,7 +18,9 @@ from uuid import uuid4
|
||||||
from typing import (
|
from typing import (
|
||||||
Protocol,
|
Protocol,
|
||||||
ClassVar,
|
ClassVar,
|
||||||
|
Literal,
|
||||||
Type,
|
Type,
|
||||||
|
TypeAlias,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -64,20 +66,40 @@ log = get_logger()
|
||||||
# seems like the right name as per,
|
# seems like the right name as per,
|
||||||
# https://www.geeksforgeeks.org/introduction-to-address-descriptor/
|
# https://www.geeksforgeeks.org/introduction-to-address-descriptor/
|
||||||
#
|
#
|
||||||
UnwrappedAddress = (
|
TaggedTCPAddress: TypeAlias = tuple[
|
||||||
# tcp/udp/uds
|
Literal['tcp'],
|
||||||
tuple[
|
str,
|
||||||
str, # host/domain(tcp), filesys-dir(uds)
|
int,
|
||||||
int|str, # port/path(uds)
|
|
||||||
]
|
]
|
||||||
# ?TODO? should we also include another 2 fields from
|
TaggedUnixAddress: TypeAlias = tuple[
|
||||||
# our `Aid` msg such that we include the runtime `Actor.uid`
|
Literal['unix'],
|
||||||
# of `.name` and `.uuid`?
|
str,
|
||||||
# - would ensure uniqueness across entire net?
|
]
|
||||||
# - allows for easier runtime-level filtering of "actors by
|
TaggedUDSAlias: TypeAlias = tuple[
|
||||||
# service name"
|
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`?
|
# TODO, maybe rename to `SocketAddress`?
|
||||||
class Address(Protocol):
|
class Address(Protocol):
|
||||||
|
|
@ -216,7 +238,15 @@ def mk_uuid() -> str:
|
||||||
|
|
||||||
|
|
||||||
def wrap_address(
|
def wrap_address(
|
||||||
addr: UnwrappedAddress|str|Address|TunnelledAddress,
|
addr: (
|
||||||
|
TaggedAddress
|
||||||
|
|TaggedUDSAlias
|
||||||
|
|LegacyUnwrappedAddress
|
||||||
|
|list[str|int]
|
||||||
|
|str
|
||||||
|
|Address
|
||||||
|
|TunnelledAddress
|
||||||
|
),
|
||||||
) -> Address|TunnelledAddress:
|
) -> Address|TunnelledAddress:
|
||||||
'''
|
'''
|
||||||
Wrap an `UnwrappedAddress` as an `Address`-type based
|
Wrap an `UnwrappedAddress` as an `Address`-type based
|
||||||
|
|
@ -239,6 +269,20 @@ def wrap_address(
|
||||||
# import pdbp; pdbp.set_trace()
|
# import pdbp; pdbp.set_trace()
|
||||||
match addr:
|
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
|
# classic network socket-address as tuple/list
|
||||||
case (
|
case (
|
||||||
(str(), int())
|
(str(), int())
|
||||||
|
|
|
||||||
|
|
@ -104,11 +104,26 @@ class TCPAddress(
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_addr(
|
def from_addr(
|
||||||
cls,
|
cls,
|
||||||
addr: tuple[str, int]
|
addr: tuple|list,
|
||||||
) -> TCPAddress:
|
) -> TCPAddress:
|
||||||
match addr:
|
match addr:
|
||||||
case (str(), int()):
|
case (
|
||||||
return TCPAddress(addr[0], addr[1])
|
('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 _:
|
case _:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'Invalid unwrapped address for {cls}\n'
|
f'Invalid unwrapped address for {cls}\n'
|
||||||
|
|
|
||||||
|
|
@ -146,16 +146,26 @@ class UDSAddress(
|
||||||
def from_addr(
|
def from_addr(
|
||||||
cls,
|
cls,
|
||||||
addr: (
|
addr: (
|
||||||
tuple[Path|str, Path|str]|Path|str
|
tuple|list|Path|str
|
||||||
),
|
),
|
||||||
) -> UDSAddress:
|
) -> UDSAddress:
|
||||||
match addr:
|
match addr:
|
||||||
case tuple()|list():
|
case (
|
||||||
filedir = Path(addr[0])
|
(('unix' | 'uds'), str()|Path() as sockpath)
|
||||||
filename = Path(addr[1])
|
|
|
||||||
|
[('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(
|
return UDSAddress(
|
||||||
filedir=filedir,
|
filedir=Path(filedir),
|
||||||
filename=filename,
|
filename=Path(filename),
|
||||||
# maybe_pid=pid,
|
# maybe_pid=pid,
|
||||||
)
|
)
|
||||||
# NOTE, in case we ever decide to just `.unwrap()`
|
# NOTE, in case we ever decide to just `.unwrap()`
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue