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`))
Gud Boi 2026-08-27 19:14:25 -04:00
parent 932722b325
commit b8b89268fd
6 changed files with 226 additions and 12 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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()

View File

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