From d1ffb436d0c3f632212b1f6963ddee14b7a1c864 Mon Sep 17 00:00:00 2001 From: goodboy Date: Thu, 27 Aug 2026 19:14:25 -0400 Subject: [PATCH] Retain realized `BindspaceRef` metadata Add `TunnelledAddress.with_bindspace_ref()` to annotate frozen tunnel declarations with stable, serializable namespace metadata. Deats, - omit absent refs from the existing msgspec shape - reject declared and realized namespace-name mismatches - report the declared key before realization and inode afterward - keep transport peeling free of live bindspace capability state - expose realized refs through real listener diagnostics Prompt-IO: ai/prompt-io/opencode/20260826T030534Z_d130431c_prompt_io.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`)) --- .../20260826T030534Z_d130431c_prompt_io.md | 54 +++++++++++++++ ...20260826T030534Z_d130431c_prompt_io.raw.md | 31 +++++++++ ai/tpt-backends/03_wg_tunnel_bindspace.md | 8 ++- tests/discovery/test_tunnelled_addr.py | 66 +++++++++++++++++++ tests/ipc/test_server_tunnel_boundary.py | 28 +++++--- tractor/discovery/_tunnel.py | 51 +++++++++++++- 6 files changed, 226 insertions(+), 12 deletions(-) create mode 100644 ai/prompt-io/opencode/20260826T030534Z_d130431c_prompt_io.md create mode 100644 ai/prompt-io/opencode/20260826T030534Z_d130431c_prompt_io.raw.md diff --git a/ai/prompt-io/opencode/20260826T030534Z_d130431c_prompt_io.md b/ai/prompt-io/opencode/20260826T030534Z_d130431c_prompt_io.md new file mode 100644 index 00000000..42465427 --- /dev/null +++ b/ai/prompt-io/opencode/20260826T030534Z_d130431c_prompt_io.md @@ -0,0 +1,54 @@ +--- +model: gpt-5.6-sol +service: opencode +session: tractor-addr-unpacking +timestamp: 2026-08-26T03:05:34Z +git_ref: d130431c +scope: code +substantive: true +raw_file: 20260826T030534Z_d130431c_prompt_io.raw.md +--- + +## Prompt + +After committing the reviewed bindspace/interface stack, the human +asked to continue with the next established Layer C step. + +## Response summary + +Retained a serializable realized `BindspaceRef` on frozen tunnelled +address declarations without carrying the process-local `Bindspace` +capability. The namespace API remains tuple-shaped, using the declared +key before realization and stable inode afterward. Existing transport +peeling and unrealized serialization remain unchanged. + +## Files changed + +- `tractor/discovery/_tunnel.py` - realized bindspace-ref retention and + immutable annotation method. +- `tests/discovery/test_tunnelled_addr.py` - ref, compatibility and + mismatch coverage. +- `tests/ipc/test_server_tunnel_boundary.py` - real listener diagnostic + coverage for the realized inode. +- `ai/tpt-backends/03_wg_tunnel_bindspace.md` - realized namespace + visibility contract. + +## Human edits + +The human accepted and committed the preceding reviewed composition +boundary, then requested the next step. Their stated preference for +end-to-end tests informed use of the existing real TCP listener path to +verify endpoint/server diagnostics, while focused unit coverage handles +metadata invariants without pyroute2 overhead. During review, the human +chose the final unshipped terminology: `BindspaceRef` for the +serializable non-owning reference, `Bindspace` for the live +process-local capability, `.ref` for that capability's reference, and +`bindspace_ref` at the tunnel declaration API. +They then requested the prerequisite model rename as a separate commit +before this feature. The human also replaced the module-level helper +with `TunnelledAddress.with_bindspace_ref()` and requested inline +msgspec encode/decode expressions in the serialization assertions. The +human chose not to add a second typed `.namespace` projection, and +requested native tagged `TunnelledAddress` decoding remain as a separate +design-plan follow-up. The agent applied those human-directed changes; +no direct manual source edits were observed. diff --git a/ai/prompt-io/opencode/20260826T030534Z_d130431c_prompt_io.raw.md b/ai/prompt-io/opencode/20260826T030534Z_d130431c_prompt_io.raw.md new file mode 100644 index 00000000..8e3805e4 --- /dev/null +++ b/ai/prompt-io/opencode/20260826T030534Z_d130431c_prompt_io.raw.md @@ -0,0 +1,31 @@ +--- +model: gpt-5.6-sol +service: opencode +timestamp: 2026-08-26T03:05:34Z +git_ref: d130431c +diff_cmd: git diff HEAD~1..HEAD +--- + +# Raw output - retain realized bindspace identity + +After committing WireGuard bindspace composition, the human requested +the next incremental Layer C change. + +> `git diff HEAD~1..HEAD -- tractor/discovery/_tunnel.py tractor/discovery/__init__.py tests/discovery/test_tunnelled_addr.py tests/ipc/test_server_tunnel_boundary.py ai/tpt-backends/03_wg_tunnel_bindspace.md` + +Added optional `BindspaceIdentity` metadata to frozen +`TunnelledAddress` declarations and a pure +`with_bindspace_identity()` annotation helper. Unrealized declarations +retain their prior serialized shape. Realized declarations retain only +serializable key/inode identity, never the FD-bearing capability. + +The existing `.namespace` tuple contract remains compatible: +unrealized declarations report `(kind, key)`, while realized +declarations report the stable `(kind, inode)`. Name mismatches between +the tunnel declaration and realized bindspace are rejected. + +Unit coverage verifies immutability, serialization, delegation and +mismatch handling. The existing real TCP listener test proves endpoint +and server diagnostics expose the retained inode without implying that +the process entered that namespace. Ruff passed and focused tunnel, +listener and bindspace coverage passed 30 tests. diff --git a/ai/tpt-backends/03_wg_tunnel_bindspace.md b/ai/tpt-backends/03_wg_tunnel_bindspace.md index be064a4b..295528e7 100644 --- a/ai/tpt-backends/03_wg_tunnel_bindspace.md +++ b/ai/tpt-backends/03_wg_tunnel_bindspace.md @@ -557,8 +557,12 @@ composed maddr can name a server source or client destination (§5.4). ### 5.3 `Address.namespace`, at last -- `TunnelledAddress.namespace` → `(kind, id)` e.g. - `('netns', 'tractor-wg0')`. +- an unrealized `TunnelledAddress.namespace` reports its declared name + as `(kind, key)`, e.g. `('netns', 'tractor-wg0')`; +- `TunnelledAddress.with_bindspace_ref()` returns a frozen declaration + annotated with `bindspace.ref`, never the FD-bearing `Bindspace`. + Its `.namespace` reports `(kind, inode)` so the + realized ref remains stable across rename or unlink; - existing plain backends implement it explicitly as `None`, so the Protocol does not lie and tunnel delegation needs no `getattr()` fallback. diff --git a/tests/discovery/test_tunnelled_addr.py b/tests/discovery/test_tunnelled_addr.py index e6b6e156..38e32121 100644 --- a/tests/discovery/test_tunnelled_addr.py +++ b/tests/discovery/test_tunnelled_addr.py @@ -15,6 +15,7 @@ import msgspec import pytest from tractor.discovery import ( + BindspaceRef, TunnelledAddress, WGTunnelSpec, mb_pubkey, @@ -196,6 +197,71 @@ def test_namespace_comes_from_the_tunnel( assert in_ns.namespace == ('netns', 'wg-test') +def test_realized_namespace_uses_stable_ref( + overlay: TCPAddress, +) -> None: + ''' + Realization must retain a stable ref without mutating the maddr. + + Build an unrealized named declaration, annotate it with the + matching realized key and inode, and prove the frozen original is + unchanged. The annotated copy must preserve transport delegation + and expose the stable inode through `.namespace`. Direct msgspec + encoding also proves only serializable ref metadata was retained. + + ''' + declared: TunnelledAddress = TunnelledAddress( + overlay=overlay, + tunnel=WGTunnelSpec( + peer_pubkey=_PUBKEY, + netns='wg-test', + ), + ) + ref: BindspaceRef = BindspaceRef( + kind='netns', + key='wg-test', + inode=1234, + ) + realized: TunnelledAddress = declared.with_bindspace_ref( + ref, + ) + + assert declared.bindspace_ref is None + assert realized.bindspace_ref is ref + assert realized.namespace == ('netns', 1234) + assert realized.overlay is declared.overlay + assert realized.tunnel is declared.tunnel + assert realized.unwrap() == declared.unwrap() + assert realized.bindspace == declared.bindspace + + declared_payload: dict[str, object] = msgspec.msgpack.decode( + msgspec.msgpack.encode(declared) + ) + assert 'bindspace_ref' not in declared_payload + + decoded: dict[str, object] = msgspec.msgpack.decode( + msgspec.msgpack.encode(realized) + ) + assert decoded['bindspace_ref'] == { + 'kind': 'netns', + 'key': 'wg-test', + 'inode': 1234, + } + + mismatched: BindspaceRef = BindspaceRef( + kind='netns', + key='other-netns', + inode=5678, + ) + with pytest.raises( + ValueError, + match='wg-test.*other-netns', + ): + declared.with_bindspace_ref( + mismatched, + ) + + def test_strip_tunnels( tunnelled: TunnelledAddress, overlay: TCPAddress, diff --git a/tests/ipc/test_server_tunnel_boundary.py b/tests/ipc/test_server_tunnel_boundary.py index 1e5de26b..f54e8dca 100644 --- a/tests/ipc/test_server_tunnel_boundary.py +++ b/tests/ipc/test_server_tunnel_boundary.py @@ -7,6 +7,7 @@ from __future__ import annotations import trio from tractor.discovery import ( + BindspaceRef, TunnelledAddress, WGTunnelSpec, tunnels_of, @@ -25,8 +26,9 @@ def test_server_peels_before_endpoint_construction() -> None: instead of the TCP backend. Start a real listener from the wrapper, assert the resulting `Endpoint` contains only a resolved `TCPAddress`. Prove `Endpoint.declared_addr` still retains the - original tunnel namespace for diagnostics and the future - bindspace lifecycle. + original declaration and realized bindspace ref for + diagnostics. This retained metadata does not claim the listener + process entered that namespace. ''' overlay = TCPAddress('127.0.0.1', 0) @@ -38,11 +40,19 @@ def test_server_peels_before_endpoint_construction() -> None: netns='actor-net', ), ) + ref: BindspaceRef = BindspaceRef( + kind='netns', + key='actor-net', + inode=1234, + ) + declared: TunnelledAddress = tunnelled.with_bindspace_ref( + ref, + ) async def main() -> None: async with open_ipc_server() as server: eps = await server.listen_on( - accept_addrs=[tunnelled], + accept_addrs=[declared], ) assert len(eps) == 1 endpoint = eps[0] @@ -51,9 +61,9 @@ def test_server_peels_before_endpoint_construction() -> None: _, host, port = endpoint.addr.unwrap() 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.addr is not declared + assert endpoint.declared_addr is declared + namespace: tuple[str, int] = ('netns', 1234) assert endpoint.namespace == namespace endpoint_repr: str = endpoint.pformat() server_repr: str = server.pformat() @@ -61,9 +71,9 @@ def test_server_peels_before_endpoint_construction() -> None: 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, + assert '1234' in server_repr + assert tunnels_of(declared) == ( + declared.tunnel, ) server.cancel() diff --git a/tractor/discovery/_tunnel.py b/tractor/discovery/_tunnel.py index a3984a7e..f31724f2 100644 --- a/tractor/discovery/_tunnel.py +++ b/tractor/discovery/_tunnel.py @@ -94,6 +94,7 @@ import trio from ..msg._local import ProcessLocal from ._bindspace import ( Bindspace, + BindspaceRef, BindspaceSpec, open_bindspace, ) @@ -864,6 +865,7 @@ def _wg_proto_code() -> int: class TunnelledAddress( msgspec.Struct, frozen=True, + omit_defaults=True, ): ''' An `Address` annotated with the tunnel it must be reached @@ -878,6 +880,32 @@ class TunnelledAddress( ''' overlay: Address|TunnelledAddress tunnel: TunnelSpec + bindspace_ref: BindspaceRef|None = None + + def __post_init__(self) -> None: + ''' + Validate the retained ref against the tunnel declaration. + + ''' + ref: BindspaceRef|None = self.bindspace_ref + if ref is None: + return + if not isinstance(ref, BindspaceRef): + raise TypeError( + '`TunnelledAddress.bindspace_ref` must be a ' + '`BindspaceRef` or `None`!' + ) + + declared_netns: str|None = self.tunnel.netns + if ( + declared_netns is not None + and + ref.key != declared_netns + ): + raise ValueError( + f'Declared netns {declared_netns!r} does not match ' + f'realized bindspace key {ref.key!r}!' + ) # ---- delegated, so the runtime can't tell the difference ---- @@ -917,14 +945,35 @@ class TunnelledAddress( @property def namespace(self) -> tuple[str, str|int]|None: ''' - The tunnel's netns, when it declares one. + Return the realized ref or declared tunnel netns. ''' + ref: BindspaceRef|None = self.bindspace_ref + if ref is not None: + return ( + ref.kind, + ref.inode, + ) + if (netns := self.tunnel.netns) is None: return self.overlay.namespace return ('netns', netns) + def with_bindspace_ref( + self, + ref: BindspaceRef, + ) -> TunnelledAddress: + ''' + Return a copy retaining one realized bindspace ref. + + ''' + realized: TunnelledAddress = msgspec.structs.replace( + self, + bindspace_ref=ref, + ) + return realized + def __repr__(self) -> str: return ( f'{type(self).__name__}(\n'