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 (
|
||||
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)
|
||||
TaggedTCPAddress: TypeAlias = tuple[
|
||||
Literal['tcp'],
|
||||
str,
|
||||
int,
|
||||
]
|
||||
# ?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"
|
||||
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())
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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()`
|
||||
|
|
|
|||
Loading…
Reference in New Issue