Expose tunnel namespaces on IPC endpoints
Make `TCPAddress` and `UDSAddress` explicitly satisfy `Address.namespace`, then retain each original listener declaration beside its peeled, resolved transport address. Deats, - remove `TunnelledAddress`'s attribute fallback - add required `Endpoint.declared_addr` metadata - report declaration namespaces in endpoint/server formatting - preserve concrete `Endpoint.addr` for transport reflection - cover plain and tunneled namespace visibility Prompt-IO: ai/prompt-io/opencode/20260822T032520Z_d35c802b_prompt_io.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
parent
f27db92c62
commit
06451f2ea6
|
|
@ -0,0 +1,44 @@
|
||||||
|
---
|
||||||
|
model: gpt-5.6-sol
|
||||||
|
service: opencode
|
||||||
|
session: tractor-addr-unpacking
|
||||||
|
timestamp: 2026-08-22T03:25:20Z
|
||||||
|
git_ref: d35c802b
|
||||||
|
scope: code
|
||||||
|
substantive: true
|
||||||
|
raw_file: 20260822T032520Z_d35c802b_prompt_io.raw.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
The human reported the explicit WireGuard verification commit done,
|
||||||
|
asked the agent to check it, and authorized the next incremental
|
||||||
|
Layer C change.
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
Verified commit `d35c802b`, made plain transport namespace behavior
|
||||||
|
explicit, and retained each original listener declaration beside its
|
||||||
|
peeled transport address. Endpoint and server diagnostics can now
|
||||||
|
report a tunnel's namespace without violating the transport boundary.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `tractor/ipc/_tcp.py` - explicit plain-address namespace property.
|
||||||
|
- `tractor/ipc/_uds.py` - explicit plain-address namespace property.
|
||||||
|
- `tractor/discovery/_tunnel.py` - direct overlay namespace delegation.
|
||||||
|
- `tractor/ipc/_server.py` - retained declaration and namespace
|
||||||
|
diagnostics.
|
||||||
|
- `tests/discovery/test_tunnelled_addr.py` - plain and tunnel namespace
|
||||||
|
behavior.
|
||||||
|
- `tests/ipc/test_server_tunnel_boundary.py` - declaration retention
|
||||||
|
and diagnostic coverage.
|
||||||
|
- `ai/tpt-backends/03_wg_tunnel_bindspace.md` - concrete endpoint
|
||||||
|
boundary contract.
|
||||||
|
|
||||||
|
## Human edits
|
||||||
|
|
||||||
|
The human selected continued incremental implementation after
|
||||||
|
reviewing and committing the preceding verification layer. The agent
|
||||||
|
implemented this dependency-ordered namespace slice; no direct manual
|
||||||
|
edits or follow-up corrections were observed.
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
model: gpt-5.6-sol
|
||||||
|
service: opencode
|
||||||
|
timestamp: 2026-08-22T03:25:20Z
|
||||||
|
git_ref: d35c802b
|
||||||
|
diff_cmd: git diff HEAD~1..HEAD
|
||||||
|
---
|
||||||
|
|
||||||
|
# Raw output - retain endpoint namespace declarations
|
||||||
|
|
||||||
|
The human reported the explicit WireGuard verification commit done,
|
||||||
|
asked for it to be checked, and authorized the next incremental
|
||||||
|
change.
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- tractor/ipc/_tcp.py tractor/ipc/_uds.py tractor/discovery/_tunnel.py tractor/ipc/_server.py tests/discovery/test_tunnelled_addr.py tests/ipc/test_server_tunnel_boundary.py ai/tpt-backends/03_wg_tunnel_bindspace.md`
|
||||||
|
|
||||||
|
Confirmed commit `d35c802b` and a clean worktree, then implemented the
|
||||||
|
smallest dependency-ordered Layer C slice. Plain TCP and UDS addresses
|
||||||
|
now explicitly report no namespace, allowing `TunnelledAddress` to
|
||||||
|
delegate without an attribute fallback.
|
||||||
|
|
||||||
|
Added required `Endpoint.declared_addr` metadata beside the peeled,
|
||||||
|
resolved `Endpoint.addr`. Endpoint and server diagnostics expose the
|
||||||
|
declaration's namespace without passing a tunnel wrapper into
|
||||||
|
transport reflection. Updated the Layer C plan and tests for plain,
|
||||||
|
tunnelled, endpoint and server namespace behavior.
|
||||||
|
|
||||||
|
Ruff passed. Focused namespace tests passed 13 tests; combined
|
||||||
|
discovery and IPC coverage passed 101 tests with 2 xpasses.
|
||||||
|
|
@ -465,6 +465,10 @@ contract open. A concrete transport call returns a concrete overlay;
|
||||||
a declaration-level call may replace the overlay and return a new
|
a declaration-level call may replace the overlay and return a new
|
||||||
`TunnelledAddress`. In either case wrappers remain until the final
|
`TunnelledAddress`. In either case wrappers remain until the final
|
||||||
transport bind/dial boundary, where `strip_tunnels()` is mandatory.
|
transport bind/dial boundary, where `strip_tunnels()` is mandatory.
|
||||||
|
At the listener boundary, keep the split explicit:
|
||||||
|
`Endpoint.addr` is the peeled concrete address used for transport
|
||||||
|
reflection, while `Endpoint.declared_addr` retains the original
|
||||||
|
wrapper for namespace diagnostics and later bindspace orchestration.
|
||||||
|
|
||||||
Per-platform provisioning still composes one resource context per
|
Per-platform provisioning still composes one resource context per
|
||||||
tunnel/bindspace layer:
|
tunnel/bindspace layer:
|
||||||
|
|
@ -501,16 +505,13 @@ required local provisioning/ownership differs (§5.4).
|
||||||
|
|
||||||
- `TunnelledAddress.namespace` → `(kind, id)` e.g.
|
- `TunnelledAddress.namespace` → `(kind, id)` e.g.
|
||||||
`('netns', 'tractor-wg0')`.
|
`('netns', 'tractor-wg0')`.
|
||||||
- **and** the existing backends should implement it as `None`
|
- existing plain backends implement it explicitly as `None`, so the
|
||||||
explicitly (they currently just don't define it), so the
|
Protocol does not lie and tunnel delegation needs no `getattr()`
|
||||||
Protocol stops lying.
|
fallback.
|
||||||
- consumers to audit: nothing reads `.namespace` today — so
|
- `Endpoint.namespace` reads the retained declaration rather than its
|
||||||
adding it is safe, but the *point* is that
|
peeled transport addr; both `Endpoint.pformat()` and
|
||||||
`Endpoint`/`Server.pformat()` should start showing it (there's
|
`Server.pformat()` expose that value as the cheapest proof the layer
|
||||||
already a `# !TODO, always be ns aware!` +
|
is wired.
|
||||||
`f'|_netns: {netns}\n'` placeholder sitting in
|
|
||||||
`Endpoint.pformat()`, `_server.py:645`). Fill that in; it's
|
|
||||||
the cheapest possible proof the layer is wired.
|
|
||||||
|
|
||||||
Use `github/ns_aware@e4688cad` as prototype evidence, not code to
|
Use `github/ns_aware@e4688cad` as prototype evidence, not code to
|
||||||
cherry-pick unchanged. Its `/proc/<pid>/ns/<type>` inode reader and
|
cherry-pick unchanged. Its `/proc/<pid>/ns/<type>` inode reader and
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ from tractor.discovery._addr import (
|
||||||
wrap_address,
|
wrap_address,
|
||||||
)
|
)
|
||||||
from tractor.ipc._tcp import TCPAddress
|
from tractor.ipc._tcp import TCPAddress
|
||||||
|
from tractor.ipc._uds import UDSAddress
|
||||||
|
|
||||||
|
|
||||||
# a valid-looking std-base64 `wg(8)` pubkey (32B -> 44 chars)
|
# a valid-looking std-base64 `wg(8)` pubkey (32B -> 44 chars)
|
||||||
|
|
@ -174,16 +175,13 @@ def test_namespace_comes_from_the_tunnel(
|
||||||
overlay: TCPAddress,
|
overlay: TCPAddress,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
First real consumer of `Address.namespace`, spec'd in the
|
Plain transport addresses explicitly select no namespace, while
|
||||||
protocol since day one and implemented by no backend.
|
a tunnel can select one for the same concrete overlay.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# XXX, "no backend implements it" is literal — the member
|
uds_addr: UDSAddress = UDSAddress('/tmp', 'tractor-test.sock')
|
||||||
# isn't even declared, so this is `AttributeError` not `None`.
|
assert overlay.namespace is None
|
||||||
# This assert is the guard: when a backend finally declares
|
assert uds_addr.namespace is None
|
||||||
# `.namespace`, it fails and the `getattr()` fallback in
|
|
||||||
# `TunnelledAddress.namespace` can go.
|
|
||||||
assert not hasattr(overlay, 'namespace')
|
|
||||||
|
|
||||||
no_ns = TunnelledAddress(
|
no_ns = TunnelledAddress(
|
||||||
overlay=overlay,
|
overlay=overlay,
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,15 @@ from tractor.ipc._tcp import TCPAddress
|
||||||
_PUBKEY: str = 'g3x7z0AdV1rM6UQU22CC7IL3/ivn4DzrE7ikDhCZ/Dc='
|
_PUBKEY: str = 'g3x7z0AdV1rM6UQU22CC7IL3/ivn4DzrE7ikDhCZ/Dc='
|
||||||
|
|
||||||
|
|
||||||
def test_server_peels_before_endpoint_construction():
|
def test_server_peels_before_endpoint_construction() -> None:
|
||||||
'''
|
'''
|
||||||
`Endpoint.start_listener()` reflects on its address's declaring
|
`Endpoint.start_listener()` reflects on its address's declaring
|
||||||
module, so retaining a tunnel wrapper there selects `._tunnel`
|
module, so retaining a tunnel wrapper there selects `._tunnel`
|
||||||
instead of the TCP backend. Start a real listener from the
|
instead of the TCP backend. Start a real listener from the
|
||||||
wrapper, assert the resulting `Endpoint` contains only a resolved
|
wrapper, assert the resulting `Endpoint` contains only a resolved
|
||||||
`TCPAddress`, and prove the original declaration still carries
|
`TCPAddress`. Prove `Endpoint.declared_addr` still retains the
|
||||||
its tunnel spec for the future bindspace lifecycle.
|
original tunnel namespace for diagnostics and the future
|
||||||
|
bindspace lifecycle.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
overlay = TCPAddress('127.0.0.1', 0)
|
overlay = TCPAddress('127.0.0.1', 0)
|
||||||
|
|
@ -34,6 +35,7 @@ def test_server_peels_before_endpoint_construction():
|
||||||
tunnel=WGTunnelSpec(
|
tunnel=WGTunnelSpec(
|
||||||
peer_pubkey=_PUBKEY,
|
peer_pubkey=_PUBKEY,
|
||||||
bearer=('192.168.1.50', 51820),
|
bearer=('192.168.1.50', 51820),
|
||||||
|
netns='actor-net',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -50,6 +52,16 @@ def test_server_peels_before_endpoint_construction():
|
||||||
assert host == overlay.unwrap()[1]
|
assert host == overlay.unwrap()[1]
|
||||||
assert port > 0
|
assert port > 0
|
||||||
assert endpoint.addr is not tunnelled
|
assert endpoint.addr is not tunnelled
|
||||||
|
assert endpoint.declared_addr is tunnelled
|
||||||
|
namespace: tuple[str, str] = ('netns', 'actor-net')
|
||||||
|
assert endpoint.namespace == namespace
|
||||||
|
endpoint_repr: str = endpoint.pformat()
|
||||||
|
server_repr: str = server.pformat()
|
||||||
|
expected_namespace: str = f'namespace: {namespace!r}'
|
||||||
|
assert expected_namespace in endpoint_repr
|
||||||
|
assert ' |_namespaces:' in server_repr
|
||||||
|
assert 'netns' in server_repr
|
||||||
|
assert 'actor-net' in server_repr
|
||||||
assert tunnels_of(tunnelled) == (
|
assert tunnels_of(tunnelled) == (
|
||||||
tunnelled.tunnel,
|
tunnelled.tunnel,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -432,20 +432,9 @@ class TunnelledAddress(
|
||||||
'''
|
'''
|
||||||
The tunnel's netns, when it declares one.
|
The tunnel's netns, when it declares one.
|
||||||
|
|
||||||
This is the first real consumer of `Address.namespace`,
|
|
||||||
spec'd in the `Address` protocol since day one and
|
|
||||||
implemented by no backend.
|
|
||||||
|
|
||||||
XXX NOTE, "implemented by no backend" is literal: neither
|
|
||||||
`TCPAddress` nor `UDSAddress` defines `.namespace` at all,
|
|
||||||
so a plain attr access on an overlay raises
|
|
||||||
`AttributeError` rather than yielding `None`. Hence the
|
|
||||||
`getattr()` — drop it once the backends actually declare
|
|
||||||
the member.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if (netns := self.tunnel.netns) is None:
|
if (netns := self.tunnel.netns) is None:
|
||||||
return getattr(self.overlay, 'namespace', None)
|
return self.overlay.namespace
|
||||||
|
|
||||||
return ('netns', netns)
|
return ('netns', netns)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -631,6 +631,7 @@ class Endpoint(Struct):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
addr: Address
|
addr: Address
|
||||||
|
declared_addr: Address|TunnelledAddress
|
||||||
listen_tn: Nursery
|
listen_tn: Nursery
|
||||||
stream_handler_tn: Nursery|None = None
|
stream_handler_tn: Nursery|None = None
|
||||||
|
|
||||||
|
|
@ -645,15 +646,27 @@ class Endpoint(Struct):
|
||||||
MsgTransport, # handle to encoded-msg transport stream
|
MsgTransport, # handle to encoded-msg transport stream
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def namespace(self) -> tuple[str, str|int]|None:
|
||||||
|
'''
|
||||||
|
Return the original address declaration's namespace.
|
||||||
|
|
||||||
|
`Endpoint.addr` is peeled to its concrete transport before
|
||||||
|
listener reflection, so `.declared_addr` retains bindspace
|
||||||
|
metadata for diagnostics and later provisioning.
|
||||||
|
|
||||||
|
'''
|
||||||
|
return self.declared_addr.namespace
|
||||||
|
|
||||||
def pformat(
|
def pformat(
|
||||||
self,
|
self,
|
||||||
indent: int = 0,
|
indent: int = 0,
|
||||||
privates: bool = False,
|
privates: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
type_repr: str = type(self).__name__
|
type_repr: str = type(self).__name__
|
||||||
|
namespace: tuple[str, str|int]|None = self.namespace
|
||||||
fmtstr: str = (
|
fmtstr: str = (
|
||||||
# !TODO, always be ns aware!
|
f' |.namespace: {namespace!r}\n'
|
||||||
# f'|_netns: {netns}\n'
|
|
||||||
f' |.addr: {self.addr!r}\n'
|
f' |.addr: {self.addr!r}\n'
|
||||||
f' |_peers: {len(self.peer_tpts)}\n'
|
f' |_peers: {len(self.peer_tpts)}\n'
|
||||||
)
|
)
|
||||||
|
|
@ -931,9 +944,17 @@ class Server(Struct):
|
||||||
ep.addr for ep in eps
|
ep.addr for ep in eps
|
||||||
]
|
]
|
||||||
repr_eps: str = ppfmt(addrs)
|
repr_eps: str = ppfmt(addrs)
|
||||||
|
namespaces: list[
|
||||||
|
tuple[str, str|int]|None
|
||||||
|
] = []
|
||||||
|
ep: Endpoint
|
||||||
|
for ep in eps:
|
||||||
|
namespaces.append(ep.namespace)
|
||||||
|
repr_namespaces: str = ppfmt(namespaces)
|
||||||
|
|
||||||
fmtstr += (
|
fmtstr += (
|
||||||
f' |_endpoints: {repr_eps}\n'
|
f' |_endpoints: {repr_eps}\n'
|
||||||
|
f' |_namespaces: {repr_namespaces}\n'
|
||||||
# ^TODO? how to indent closing ']'..
|
# ^TODO? how to indent closing ']'..
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1086,6 +1107,7 @@ async def _serve_ipc_eps(
|
||||||
addr=addr,
|
addr=addr,
|
||||||
listen_tn=listen_tn,
|
listen_tn=listen_tn,
|
||||||
stream_handler_tn=stream_handler_tn,
|
stream_handler_tn=stream_handler_tn,
|
||||||
|
declared_addr=declared_addr,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
ep_sclang: str = nest_from_op(
|
ep_sclang: str = nest_from_op(
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,14 @@ class TCPAddress(
|
||||||
def bindspace(self) -> str:
|
def bindspace(self) -> str:
|
||||||
return self._host
|
return self._host
|
||||||
|
|
||||||
|
@property
|
||||||
|
def namespace(self) -> None:
|
||||||
|
'''
|
||||||
|
Report that plain TCP uses the process's current namespace.
|
||||||
|
|
||||||
|
'''
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def domain(self) -> str:
|
def domain(self) -> str:
|
||||||
return self._host
|
return self._host
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,14 @@ class UDSAddress(
|
||||||
self.def_bindspace
|
self.def_bindspace
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def namespace(self) -> None:
|
||||||
|
'''
|
||||||
|
Report that plain UDS uses the process's current namespace.
|
||||||
|
|
||||||
|
'''
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sockpath(self) -> Path:
|
def sockpath(self) -> Path:
|
||||||
return Path(self.bindspace) / self.filename
|
return Path(self.bindspace) / self.filename
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue