From dfad66a00a74ee4ba58de1295ef15ba4be693ec4 Mon Sep 17 00:00:00 2001 From: goodboy Date: Wed, 19 Aug 2026 21:30:10 -0400 Subject: [PATCH] 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`)) --- .../20260819T213145Z_f81fc5e5_prompt_io.md | 40 +++++++++++++ ...20260819T213145Z_f81fc5e5_prompt_io.raw.md | 25 ++++++++ tests/ipc/test_server_tunnel_boundary.py | 59 +++++++++++++++++++ tractor/_root.py | 6 +- tractor/ipc/_server.py | 17 ++++-- 5 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 ai/prompt-io/opencode/20260819T213145Z_f81fc5e5_prompt_io.md create mode 100644 ai/prompt-io/opencode/20260819T213145Z_f81fc5e5_prompt_io.raw.md create mode 100644 tests/ipc/test_server_tunnel_boundary.py diff --git a/ai/prompt-io/opencode/20260819T213145Z_f81fc5e5_prompt_io.md b/ai/prompt-io/opencode/20260819T213145Z_f81fc5e5_prompt_io.md new file mode 100644 index 00000000..d110da3b --- /dev/null +++ b/ai/prompt-io/opencode/20260819T213145Z_f81fc5e5_prompt_io.md @@ -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. diff --git a/ai/prompt-io/opencode/20260819T213145Z_f81fc5e5_prompt_io.raw.md b/ai/prompt-io/opencode/20260819T213145Z_f81fc5e5_prompt_io.raw.md new file mode 100644 index 00000000..acb71449 --- /dev/null +++ b/ai/prompt-io/opencode/20260819T213145Z_f81fc5e5_prompt_io.raw.md @@ -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. diff --git a/tests/ipc/test_server_tunnel_boundary.py b/tests/ipc/test_server_tunnel_boundary.py new file mode 100644 index 00000000..827af420 --- /dev/null +++ b/tests/ipc/test_server_tunnel_boundary.py @@ -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) diff --git a/tractor/_root.py b/tractor/_root.py index c123631b..0d029e09 100644 --- a/tractor/_root.py +++ b/tractor/_root.py @@ -57,6 +57,7 @@ from .discovery._addr import ( mk_uuid, wrap_address, ) +from .discovery._tunnel import strip_tunnels from .trionics import ( is_multi_cancelled, collapse_eg, @@ -534,6 +535,7 @@ async def open_root_actor( # proto if not already provided. if not tpt_bind_addrs: for addr in ponged_addrs: + bindable_addr: Address = strip_tunnels(addr) tpt_bind_addrs.append( # XXX, these are `Address` NOT `UnwrappedAddress`. # @@ -541,8 +543,8 @@ async def open_root_actor( # protos we allocate port=0 such that the system # allocates a random value at bind time; this # happens in the `.ipc.*` stack's backend. - addr.get_random( - bindspace=addr.bindspace, + bindable_addr.get_random( + bindspace=bindable_addr.bindspace, ) ) diff --git a/tractor/ipc/_server.py b/tractor/ipc/_server.py index 31f4c6b0..3113333f 100644 --- a/tractor/ipc/_server.py +++ b/tractor/ipc/_server.py @@ -59,13 +59,17 @@ from ..msg import ( from ..trionics import maybe_open_nursery from ..runtime import _state from .. import log -from ..discovery._addr import Address +from ..discovery._addr import ( + Address, + UnwrappedAddress, +) from ._chan import Channel from ._transport import MsgTransport from ._uds import UDSAddress from ._tcp import TCPAddress if TYPE_CHECKING: + from ..discovery._tunnel import TunnelledAddress from ..runtime._runtime import Actor from ..runtime._supervise import ActorNursery @@ -959,7 +963,9 @@ class Server(Struct): async def listen_on( self, *, - accept_addrs: list[tuple[str, int|str]]|None = None, + accept_addrs: list[ + UnwrappedAddress|Address|TunnelledAddress + ]|None = None, stream_handler_nursery: Nursery|None = None, ) -> list[Endpoint]: ''' @@ -1042,7 +1048,7 @@ async def _serve_ipc_eps( *, server: IPCServer, stream_handler_tn: Nursery, - listen_addrs: list[tuple[str, int|str]], + listen_addrs: list[Address|TunnelledAddress], task_status: TaskStatus[ Nursery, @@ -1058,6 +1064,8 @@ async def _serve_ipc_eps( `.cancel_server()` is called. ''' + from ..discovery._tunnel import strip_tunnels + try: listen_tn: Nursery 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. # ?TODO, maybe just pass `list(eps.values()` tho? listeners: list[trio.abc.Listener] = [] - for addr in listen_addrs: + for declared_addr in listen_addrs: + addr: Address = strip_tunnels(declared_addr) ep = Endpoint( addr=addr, listen_tn=listen_tn,