Peel tunnels before `Endpoint` binding

Carry tunnel declarations through listener configuration, then strip
them immediately before constructing transport endpoints.

Also allocate random listener addresses from a contacted registry's
overlay, and prove a real TCP listener never stores the wrapper while
the source declaration retains its bindspace metadata.

Prompt-IO: ai/prompt-io/opencode/20260819T213145Z_f81fc5e5_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
wkt/addr_unpacking
Gud Boi 2026-08-19 21:30:10 -04:00
parent d0a9593aff
commit dfad66a00a
5 changed files with 141 additions and 6 deletions

View File

@ -0,0 +1,40 @@
---
model: gpt-5.6-sol
service: opencode
session: tractor-addr-unpacking-followup
timestamp: 2026-08-19T21:31:45Z
git_ref: f81fc5e5
scope: code
substantive: true
raw_file: 20260819T213145Z_f81fc5e5_prompt_io.raw.md
---
## Prompt
The human requested completion of inbound runtime peeling using the
same per-step implementation and commit-plan workflow.
## Response summary
Preserved tunnel declarations through listener configuration, peeled
them immediately before `Endpoint` construction, and used the overlay
for backend-specific random listener allocation after registry
discovery. Added a real listener regression for the reflection and
exact-type boundary.
## Files changed
- `tractor/ipc/_server.py` - accept wrapper declarations and peel at
`Endpoint` construction.
- `tractor/_root.py` - allocate random transport addresses from the
contacted registry's overlay.
- `tests/ipc/test_server_tunnel_boundary.py` - verify a real listener
stores only TCP while preserving the source annotation.
## Human edits
The human selected the runtime-boundary work and previously corrected
the architecture so tractor retains future bindspace provisioning
ownership while `Endpoint` sees only application transports. The agent
implemented and tested that direction; no direct manual source edits
were observed during this step.

View File

@ -0,0 +1,25 @@
---
model: gpt-5.6-sol
service: opencode
timestamp: 2026-08-19T21:31:45Z
git_ref: f81fc5e5
diff_cmd: git diff HEAD~1..HEAD
---
# Raw output - inbound tunnel boundary
The human requested runtime boundary integration while preserving the
future tractor-owned bindspace lifecycle.
> `git diff HEAD~1..HEAD -- tractor/ipc/_server.py tractor/_root.py tests/ipc/test_server_tunnel_boundary.py`
Broadened listener declarations to carry tunnel wrappers until
`_serve_ipc_eps()` and peeled immediately before `Endpoint`
construction. Also peeled a contacted tunnelled registry before
backend-specific random listener allocation. Added a real TCP listener
regression proving `Endpoint` stores only the resolved overlay while
the original declaration retains bindspace metadata.
Verification included `465` collected tests, `84` passing
discovery/IPC tests with two xpasses, Ruff, and the full suite with
`447` passes.

View File

@ -0,0 +1,59 @@
'''
Tunnel annotation peeling at the inbound IPC transport boundary.
'''
from __future__ import annotations
import trio
from tractor.discovery import (
TunnelledAddress,
WGTunnelSpec,
tunnels_of,
)
from tractor.ipc._server import open_ipc_server
from tractor.ipc._tcp import TCPAddress
_PUBKEY: str = 'g3x7z0AdV1rM6UQU22CC7IL3/ivn4DzrE7ikDhCZ/Dc='
def test_server_peels_before_endpoint_construction():
'''
`Endpoint.start_listener()` reflects on its address's declaring
module, so retaining a tunnel wrapper there selects `._tunnel`
instead of the TCP backend. Start a real listener from the
wrapper, assert the resulting `Endpoint` contains only a resolved
`TCPAddress`, and prove the original declaration still carries
its tunnel spec for the future bindspace lifecycle.
'''
overlay = TCPAddress('127.0.0.1', 0)
tunnelled = TunnelledAddress(
overlay=overlay,
tunnel=WGTunnelSpec(
peer_pubkey=_PUBKEY,
bearer=('192.168.1.50', 51820),
),
)
async def main() -> None:
async with open_ipc_server() as server:
eps = await server.listen_on(
accept_addrs=[tunnelled],
)
assert len(eps) == 1
endpoint = eps[0]
assert type(endpoint.addr) is TCPAddress
host, port = endpoint.addr.unwrap()
assert host == overlay.unwrap()[0]
assert port > 0
assert endpoint.addr is not tunnelled
assert tunnels_of(tunnelled) == (
tunnelled.tunnel,
)
server.cancel()
trio.run(main)

View File

@ -57,6 +57,7 @@ from .discovery._addr import (
mk_uuid, mk_uuid,
wrap_address, wrap_address,
) )
from .discovery._tunnel import strip_tunnels
from .trionics import ( from .trionics import (
is_multi_cancelled, is_multi_cancelled,
collapse_eg, collapse_eg,
@ -534,6 +535,7 @@ async def open_root_actor(
# proto if not already provided. # proto if not already provided.
if not tpt_bind_addrs: if not tpt_bind_addrs:
for addr in ponged_addrs: for addr in ponged_addrs:
bindable_addr: Address = strip_tunnels(addr)
tpt_bind_addrs.append( tpt_bind_addrs.append(
# XXX, these are `Address` NOT `UnwrappedAddress`. # XXX, these are `Address` NOT `UnwrappedAddress`.
# #
@ -541,8 +543,8 @@ async def open_root_actor(
# protos we allocate port=0 such that the system # protos we allocate port=0 such that the system
# allocates a random value at bind time; this # allocates a random value at bind time; this
# happens in the `.ipc.*` stack's backend. # happens in the `.ipc.*` stack's backend.
addr.get_random( bindable_addr.get_random(
bindspace=addr.bindspace, bindspace=bindable_addr.bindspace,
) )
) )

View File

@ -59,13 +59,17 @@ from ..msg import (
from ..trionics import maybe_open_nursery from ..trionics import maybe_open_nursery
from ..runtime import _state from ..runtime import _state
from .. import log from .. import log
from ..discovery._addr import Address from ..discovery._addr import (
Address,
UnwrappedAddress,
)
from ._chan import Channel from ._chan import Channel
from ._transport import MsgTransport from ._transport import MsgTransport
from ._uds import UDSAddress from ._uds import UDSAddress
from ._tcp import TCPAddress from ._tcp import TCPAddress
if TYPE_CHECKING: if TYPE_CHECKING:
from ..discovery._tunnel import TunnelledAddress
from ..runtime._runtime import Actor from ..runtime._runtime import Actor
from ..runtime._supervise import ActorNursery from ..runtime._supervise import ActorNursery
@ -959,7 +963,9 @@ class Server(Struct):
async def listen_on( async def listen_on(
self, self,
*, *,
accept_addrs: list[tuple[str, int|str]]|None = None, accept_addrs: list[
UnwrappedAddress|Address|TunnelledAddress
]|None = None,
stream_handler_nursery: Nursery|None = None, stream_handler_nursery: Nursery|None = None,
) -> list[Endpoint]: ) -> list[Endpoint]:
''' '''
@ -1042,7 +1048,7 @@ async def _serve_ipc_eps(
*, *,
server: IPCServer, server: IPCServer,
stream_handler_tn: Nursery, stream_handler_tn: Nursery,
listen_addrs: list[tuple[str, int|str]], listen_addrs: list[Address|TunnelledAddress],
task_status: TaskStatus[ task_status: TaskStatus[
Nursery, Nursery,
@ -1058,6 +1064,8 @@ async def _serve_ipc_eps(
`.cancel_server()` is called. `.cancel_server()` is called.
''' '''
from ..discovery._tunnel import strip_tunnels
try: try:
listen_tn: Nursery listen_tn: Nursery
async with trio.open_nursery() as listen_tn: async with trio.open_nursery() as listen_tn:
@ -1066,7 +1074,8 @@ async def _serve_ipc_eps(
# XXX NOTE, required to call `serve_listeners()` below. # XXX NOTE, required to call `serve_listeners()` below.
# ?TODO, maybe just pass `list(eps.values()` tho? # ?TODO, maybe just pass `list(eps.values()` tho?
listeners: list[trio.abc.Listener] = [] listeners: list[trio.abc.Listener] = []
for addr in listen_addrs: for declared_addr in listen_addrs:
addr: Address = strip_tunnels(declared_addr)
ep = Endpoint( ep = Endpoint(
addr=addr, addr=addr,
listen_tn=listen_tn, listen_tn=listen_tn,