diff --git a/ai/prompt-io/opencode/20260822T032520Z_d35c802b_prompt_io.md b/ai/prompt-io/opencode/20260822T032520Z_d35c802b_prompt_io.md new file mode 100644 index 00000000..aef0a7d7 --- /dev/null +++ b/ai/prompt-io/opencode/20260822T032520Z_d35c802b_prompt_io.md @@ -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. diff --git a/ai/prompt-io/opencode/20260822T032520Z_d35c802b_prompt_io.raw.md b/ai/prompt-io/opencode/20260822T032520Z_d35c802b_prompt_io.raw.md new file mode 100644 index 00000000..30cdc291 --- /dev/null +++ b/ai/prompt-io/opencode/20260822T032520Z_d35c802b_prompt_io.raw.md @@ -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. diff --git a/ai/tpt-backends/03_wg_tunnel_bindspace.md b/ai/tpt-backends/03_wg_tunnel_bindspace.md index f11cb74d..2b8ad1c6 100644 --- a/ai/tpt-backends/03_wg_tunnel_bindspace.md +++ b/ai/tpt-backends/03_wg_tunnel_bindspace.md @@ -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 `TunnelledAddress`. In either case wrappers remain until the final 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 tunnel/bindspace layer: @@ -501,16 +505,13 @@ required local provisioning/ownership differs (§5.4). - `TunnelledAddress.namespace` → `(kind, id)` e.g. `('netns', 'tractor-wg0')`. -- **and** the existing backends should implement it as `None` - explicitly (they currently just don't define it), so the - Protocol stops lying. -- consumers to audit: nothing reads `.namespace` today — so - adding it is safe, but the *point* is that - `Endpoint`/`Server.pformat()` should start showing it (there's - already a `# !TODO, always be ns aware!` + - `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. +- existing plain backends implement it explicitly as `None`, so the + Protocol does not lie and tunnel delegation needs no `getattr()` + fallback. +- `Endpoint.namespace` reads the retained declaration rather than its + peeled transport addr; both `Endpoint.pformat()` and + `Server.pformat()` expose that value as the cheapest proof the layer + is wired. Use `github/ns_aware@e4688cad` as prototype evidence, not code to cherry-pick unchanged. Its `/proc//ns/` inode reader and diff --git a/tests/discovery/test_tunnelled_addr.py b/tests/discovery/test_tunnelled_addr.py index e9b6b471..e6b6e156 100644 --- a/tests/discovery/test_tunnelled_addr.py +++ b/tests/discovery/test_tunnelled_addr.py @@ -27,6 +27,7 @@ from tractor.discovery._addr import ( wrap_address, ) from tractor.ipc._tcp import TCPAddress +from tractor.ipc._uds import UDSAddress # a valid-looking std-base64 `wg(8)` pubkey (32B -> 44 chars) @@ -174,16 +175,13 @@ def test_namespace_comes_from_the_tunnel( overlay: TCPAddress, ): ''' - First real consumer of `Address.namespace`, spec'd in the - protocol since day one and implemented by no backend. + Plain transport addresses explicitly select no namespace, while + a tunnel can select one for the same concrete overlay. ''' - # XXX, "no backend implements it" is literal — the member - # isn't even declared, so this is `AttributeError` not `None`. - # This assert is the guard: when a backend finally declares - # `.namespace`, it fails and the `getattr()` fallback in - # `TunnelledAddress.namespace` can go. - assert not hasattr(overlay, 'namespace') + uds_addr: UDSAddress = UDSAddress('/tmp', 'tractor-test.sock') + assert overlay.namespace is None + assert uds_addr.namespace is None no_ns = TunnelledAddress( overlay=overlay, diff --git a/tests/ipc/test_server_tunnel_boundary.py b/tests/ipc/test_server_tunnel_boundary.py index 6c1f6dbb..1e5de26b 100644 --- a/tests/ipc/test_server_tunnel_boundary.py +++ b/tests/ipc/test_server_tunnel_boundary.py @@ -18,14 +18,15 @@ from tractor.ipc._tcp import TCPAddress _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 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. + `TCPAddress`. Prove `Endpoint.declared_addr` still retains the + original tunnel namespace for diagnostics and the future + bindspace lifecycle. ''' overlay = TCPAddress('127.0.0.1', 0) @@ -34,6 +35,7 @@ def test_server_peels_before_endpoint_construction(): tunnel=WGTunnelSpec( peer_pubkey=_PUBKEY, 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 port > 0 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) == ( tunnelled.tunnel, ) diff --git a/tractor/discovery/_tunnel.py b/tractor/discovery/_tunnel.py index 1fb3139a..234e503c 100644 --- a/tractor/discovery/_tunnel.py +++ b/tractor/discovery/_tunnel.py @@ -432,20 +432,9 @@ class TunnelledAddress( ''' 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: - return getattr(self.overlay, 'namespace', None) + return self.overlay.namespace return ('netns', netns) diff --git a/tractor/ipc/_server.py b/tractor/ipc/_server.py index 9155f842..f298ea57 100644 --- a/tractor/ipc/_server.py +++ b/tractor/ipc/_server.py @@ -631,6 +631,7 @@ class Endpoint(Struct): ''' addr: Address + declared_addr: Address|TunnelledAddress listen_tn: Nursery stream_handler_tn: Nursery|None = None @@ -645,15 +646,27 @@ class Endpoint(Struct): 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( self, indent: int = 0, privates: bool = False, ) -> str: type_repr: str = type(self).__name__ + namespace: tuple[str, str|int]|None = self.namespace fmtstr: str = ( - # !TODO, always be ns aware! - # f'|_netns: {netns}\n' + f' |.namespace: {namespace!r}\n' f' |.addr: {self.addr!r}\n' f' |_peers: {len(self.peer_tpts)}\n' ) @@ -931,9 +944,17 @@ class Server(Struct): ep.addr for ep in eps ] 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 += ( f' |_endpoints: {repr_eps}\n' + f' |_namespaces: {repr_namespaces}\n' # ^TODO? how to indent closing ']'.. ) @@ -1086,6 +1107,7 @@ async def _serve_ipc_eps( addr=addr, listen_tn=listen_tn, stream_handler_tn=stream_handler_tn, + declared_addr=declared_addr, ) try: ep_sclang: str = nest_from_op( diff --git a/tractor/ipc/_tcp.py b/tractor/ipc/_tcp.py index dc88358c..0caf3f22 100644 --- a/tractor/ipc/_tcp.py +++ b/tractor/ipc/_tcp.py @@ -107,6 +107,14 @@ class TCPAddress( def bindspace(self) -> str: return self._host + @property + def namespace(self) -> None: + ''' + Report that plain TCP uses the process's current namespace. + + ''' + return None + @property def domain(self) -> str: return self._host diff --git a/tractor/ipc/_uds.py b/tractor/ipc/_uds.py index 72987c1f..56e84e44 100644 --- a/tractor/ipc/_uds.py +++ b/tractor/ipc/_uds.py @@ -165,6 +165,14 @@ class UDSAddress( self.def_bindspace ) + @property + def namespace(self) -> None: + ''' + Report that plain UDS uses the process's current namespace. + + ''' + return None + @property def sockpath(self) -> Path: return Path(self.bindspace) / self.filename