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`))
parent
f538c4f5d1
commit
33b349bf25
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ import trio
|
|||
from ..msg._local import ProcessLocal
|
||||
from ._bindspace import (
|
||||
Bindspace,
|
||||
BindspaceRef,
|
||||
BindspaceSpec,
|
||||
open_bindspace,
|
||||
)
|
||||
|
|
@ -858,6 +859,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
|
||||
|
|
@ -872,6 +874,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 ----
|
||||
|
||||
|
|
@ -911,14 +939,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'
|
||||
|
|
|
|||
Loading…
Reference in New Issue