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,
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,
)
)

View File

@ -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,