Add bindspace capability models
Separate serializable `BindspaceSpec` and `BindspaceIdentity` values from a process-local `BindspaceHandle` carrying FD and ownership authority. Deats, - add global `ProcessLocal` wire guards for local handle structs - derive valid kinds and ownership from their `Literal` aliases - require a positive inode while keeping the mutable name optional - pin supplied FDs to identity inodes with `fstat()` - cover round trips, nested encoding and stale capabilities Caught-during: review remediation Found-via: `/run-tests` test_bindspace_handle_pins_local_capability Prompt-IO: ai/prompt-io/opencode/20260822T042026Z_29141f0b_prompt_io.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
parent
06451f2ea6
commit
349ab98361
|
|
@ -0,0 +1,64 @@
|
||||||
|
---
|
||||||
|
model: gpt-5.6-sol
|
||||||
|
service: opencode
|
||||||
|
session: tractor-addr-unpacking
|
||||||
|
timestamp: 2026-08-22T04:20:26Z
|
||||||
|
git_ref: 29141f0b
|
||||||
|
scope: code
|
||||||
|
substantive: true
|
||||||
|
raw_file: 20260822T042026Z_29141f0b_prompt_io.raw.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
After committing endpoint namespace visibility, the human authorized
|
||||||
|
continued Layer C implementation.
|
||||||
|
|
||||||
|
During staged review, the human requested Literal-derived validation,
|
||||||
|
ownership documentation, stable-inode clarification, explicit
|
||||||
|
non-serialization rationale and consolidated invalid-model tests.
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
Added the foundational bindspace model: serializable declarations and
|
||||||
|
stable identities are separated from a process-local live capability.
|
||||||
|
The handle validates names, ownership and FD identity. A global
|
||||||
|
`ProcessLocal` sentinel blocks default encoding while retaining
|
||||||
|
msgspec struct behavior.
|
||||||
|
Review fixes require a positive inode for every realized netns,
|
||||||
|
derive runtime choices from the Literal aliases and clarify that an FD
|
||||||
|
integer is not transferable capability authority.
|
||||||
|
The human then clarified that msgspec structs are useful generic
|
||||||
|
storage independently of serialization policy, so the live handle now
|
||||||
|
uses a struct while remaining process-local by contract.
|
||||||
|
The human first selected an opaque FD wrapper, then recognized that
|
||||||
|
future process-local handles need the same guard and directed a global
|
||||||
|
marker under `tractor.msg` instead.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `tractor/discovery/_bindspace.py` - declaration, identity and live
|
||||||
|
capability models.
|
||||||
|
- `tractor/discovery/__init__.py` - public bindspace exports.
|
||||||
|
- `tractor/msg/_local.py` - reusable process-local struct marker.
|
||||||
|
- `tractor/msg/__init__.py` - public `ProcessLocal` export.
|
||||||
|
- `tests/discovery/test_bindspace.py` - serialization and capability
|
||||||
|
invariant coverage.
|
||||||
|
- `tests/msg/test_process_local.py` - direct and nested wire rejection.
|
||||||
|
- `ai/tpt-backends/03_wg_tunnel_bindspace.md` - concrete initial model
|
||||||
|
contract.
|
||||||
|
|
||||||
|
## Human edits
|
||||||
|
|
||||||
|
The human selected the foundational Layer C capability model, then
|
||||||
|
reviewed the staged patch and directed all eight follow-up changes:
|
||||||
|
remove distracting ownership variation, consolidate invalid-model
|
||||||
|
tests, derive containment checks from Literal aliases, require a
|
||||||
|
stable inode and explain why the live handle is not a msgspec struct.
|
||||||
|
The human subsequently corrected that last design choice: struct use
|
||||||
|
does not imply IPC authorization, so `BindspaceHandle` should retain
|
||||||
|
msgspec's generic storage benefits. The agent applied those
|
||||||
|
human-directed edits. The human first chose an opaque process-local FD
|
||||||
|
wrapper, then refined that decision to a reusable global marker under
|
||||||
|
`tractor.msg` for future local handles. No direct manual source edits
|
||||||
|
were observed.
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
model: gpt-5.6-sol
|
||||||
|
service: opencode
|
||||||
|
timestamp: 2026-08-22T04:20:26Z
|
||||||
|
git_ref: 29141f0b
|
||||||
|
diff_cmd: git diff HEAD~1..HEAD
|
||||||
|
---
|
||||||
|
|
||||||
|
# Raw output - model bindspace capabilities
|
||||||
|
|
||||||
|
The human authorized the next incremental Layer C change after
|
||||||
|
committing endpoint namespace visibility.
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- tractor/discovery/_bindspace.py tractor/discovery/__init__.py tests/discovery/test_bindspace.py ai/tpt-backends/03_wg_tunnel_bindspace.md`
|
||||||
|
|
||||||
|
Added frozen, serializable `BindspaceSpec` and
|
||||||
|
`BindspaceIdentity` models for the initial Linux netns kind. Added a
|
||||||
|
plain slotted `BindspaceHandle` which carries ownership and an optional
|
||||||
|
namespace FD without being serializable by default msgspec encoding.
|
||||||
|
|
||||||
|
The live handle validates declaration/identity kind and name, ownership
|
||||||
|
values, non-negative FDs and FD/inode correspondence. Serializable
|
||||||
|
models reject empty identities and kinds without implementations.
|
||||||
|
|
||||||
|
Added round-trip, non-serialization, identity-mismatch, empty-identity
|
||||||
|
and unsupported-kind tests. Ruff passed; discovery plus
|
||||||
|
message-serialization coverage passed 125 tests with 2 xpasses.
|
||||||
|
|
@ -413,11 +413,11 @@ class BindspaceSpec(msgspec.Struct, frozen=True):
|
||||||
class BindspaceIdentity(msgspec.Struct, frozen=True):
|
class BindspaceIdentity(msgspec.Struct, frozen=True):
|
||||||
'''Stable identity of the realized platform resource.'''
|
'''Stable identity of the realized platform resource.'''
|
||||||
kind: str
|
kind: str
|
||||||
key: str|None
|
key: str|None # mutable name, absent after unlink
|
||||||
inode: int|None # Linux namespace identity
|
inode: int # stable Linux namespace identity
|
||||||
|
|
||||||
|
|
||||||
class BindspaceHandle:
|
class BindspaceHandle(ProcessLocal):
|
||||||
'''Scoped, non-serializable capability for one live bindspace.'''
|
'''Scoped, non-serializable capability for one live bindspace.'''
|
||||||
spec: BindspaceSpec
|
spec: BindspaceSpec
|
||||||
identity: BindspaceIdentity
|
identity: BindspaceIdentity
|
||||||
|
|
@ -437,13 +437,19 @@ async def open_bindspace(
|
||||||
'''
|
'''
|
||||||
```
|
```
|
||||||
|
|
||||||
The exact field set remains design work; the required split does not:
|
The initial model limits `BindspaceKind` to `netns` while preserving
|
||||||
`BindspaceSpec` crosses config/spawn serialization, while
|
the required lifetime split. `BindspaceSpec` and
|
||||||
`BindspaceHandle` contains live OS resources (especially an open
|
`BindspaceIdentity` are frozen msgspec structs which cross
|
||||||
namespace FD), pins identity/lifetime, and must never cross msgpack.
|
config/spawn serialization. `BindspaceHandle` also uses msgspec's
|
||||||
An FD is a stronger capability than a namespace name: it avoids
|
generic struct storage by inheriting the global
|
||||||
name-resolution TOCTOU, survives rename/unlink, and identifies the
|
`tractor.msg.ProcessLocal` marker. Its hidden unsupported sentinel
|
||||||
exact namespace the parent provisioned.
|
blocks direct and nested default msgspec encoding without a recursive
|
||||||
|
IPC hot-path scan. The handle validates any supplied FD against
|
||||||
|
`BindspaceIdentity.inode`; explicit FD transfer belongs to the
|
||||||
|
supervisor bootstrap path. An FD avoids name-resolution TOCTOU,
|
||||||
|
survives rename/unlink, and identifies the exact namespace the parent
|
||||||
|
provisioned. Extend the kind/field union only when a second platform
|
||||||
|
resource is implemented.
|
||||||
|
|
||||||
`open_bindspace()` is **not** an address factory and does not return a
|
`open_bindspace()` is **not** an address factory and does not return a
|
||||||
`TunnelledAddress`. At the declaration layer, listener allocation can
|
`TunnelledAddress`. At the declaration layer, listener allocation can
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
'''
|
||||||
|
Bindspace declaration, identity and live-capability contracts.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import BinaryIO
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tractor.discovery import (
|
||||||
|
BindspaceHandle,
|
||||||
|
BindspaceIdentity,
|
||||||
|
BindspaceOwnership,
|
||||||
|
BindspaceSpec,
|
||||||
|
)
|
||||||
|
from tractor.msg import ProcessLocal
|
||||||
|
|
||||||
|
|
||||||
|
def test_bindspace_declarations_roundtrip() -> None:
|
||||||
|
'''
|
||||||
|
Spawn configuration and realized identity must cross actor IPC.
|
||||||
|
|
||||||
|
Encode both frozen structs through msgpack and decode with their
|
||||||
|
concrete types, proving names and stable inode identity survive
|
||||||
|
without carrying any process-local capability state.
|
||||||
|
|
||||||
|
'''
|
||||||
|
values: tuple[
|
||||||
|
BindspaceSpec|BindspaceIdentity,
|
||||||
|
...,
|
||||||
|
] = (
|
||||||
|
BindspaceSpec(kind='netns', key='tractor-wg0'),
|
||||||
|
BindspaceIdentity(
|
||||||
|
kind='netns',
|
||||||
|
key='tractor-wg0',
|
||||||
|
inode=1234,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
value: BindspaceSpec|BindspaceIdentity
|
||||||
|
for value in values:
|
||||||
|
encoded: bytes = msgspec.msgpack.encode(value)
|
||||||
|
decoded: BindspaceSpec|BindspaceIdentity = (
|
||||||
|
msgspec.msgpack.decode(
|
||||||
|
encoded,
|
||||||
|
type=type(value),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert decoded == value
|
||||||
|
|
||||||
|
|
||||||
|
def test_bindspace_handle_pins_local_capability(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
A live handle pins one exact FD and realized identity.
|
||||||
|
|
||||||
|
Open a stand-in platform handle, record its inode in the realized
|
||||||
|
identity and construct an owned capability. Prove the generic
|
||||||
|
msgspec struct retains that exact local state. Its ability to
|
||||||
|
encode ordinary fields is not authority to transfer the handle.
|
||||||
|
|
||||||
|
'''
|
||||||
|
token_path: Path = tmp_path / 'bindspace'
|
||||||
|
token_path.touch()
|
||||||
|
namespace_file: BinaryIO
|
||||||
|
with token_path.open('rb') as namespace_file:
|
||||||
|
namespace_fd: int = namespace_file.fileno()
|
||||||
|
inode: int = token_path.stat().st_ino
|
||||||
|
spec: BindspaceSpec = BindspaceSpec(
|
||||||
|
kind='netns',
|
||||||
|
key='tractor-wg0',
|
||||||
|
)
|
||||||
|
identity: BindspaceIdentity = BindspaceIdentity(
|
||||||
|
kind='netns',
|
||||||
|
key='tractor-wg0',
|
||||||
|
inode=inode,
|
||||||
|
)
|
||||||
|
handle: BindspaceHandle = BindspaceHandle(
|
||||||
|
spec=spec,
|
||||||
|
identity=identity,
|
||||||
|
namespace_fd=namespace_fd,
|
||||||
|
ownership='owned',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert handle.spec is spec
|
||||||
|
assert handle.identity is identity
|
||||||
|
assert handle.namespace_fd == namespace_file.fileno()
|
||||||
|
assert handle.ownership == 'owned'
|
||||||
|
assert isinstance(handle, msgspec.Struct)
|
||||||
|
assert isinstance(handle, ProcessLocal)
|
||||||
|
with pytest.raises(
|
||||||
|
TypeError,
|
||||||
|
match='_ProcessLocalToken.*unsupported',
|
||||||
|
):
|
||||||
|
msgspec.msgpack.encode(handle)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bindspace_handle_rejects_mismatched_identity(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
A name or inode mismatch would make a handle stale authority.
|
||||||
|
|
||||||
|
Construct a requested named spec, then prove both a different
|
||||||
|
realized name and an inode not belonging to the supplied FD are
|
||||||
|
rejected before either can become a live capability.
|
||||||
|
|
||||||
|
'''
|
||||||
|
token_path: Path = tmp_path / 'bindspace'
|
||||||
|
token_path.touch()
|
||||||
|
spec: BindspaceSpec = BindspaceSpec(
|
||||||
|
kind='netns',
|
||||||
|
key='tractor-wg0',
|
||||||
|
)
|
||||||
|
# Keep ownership and FD fixed so only identity changes below.
|
||||||
|
ownership: BindspaceOwnership = 'borrowed'
|
||||||
|
namespace_file: BinaryIO
|
||||||
|
with token_path.open('rb') as namespace_file:
|
||||||
|
namespace_fd: int = namespace_file.fileno()
|
||||||
|
wrong_name: BindspaceIdentity = BindspaceIdentity(
|
||||||
|
kind='netns',
|
||||||
|
key='other-wg',
|
||||||
|
inode=token_path.stat().st_ino,
|
||||||
|
)
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError,
|
||||||
|
match='Spec.key.*Identity.key',
|
||||||
|
):
|
||||||
|
BindspaceHandle(
|
||||||
|
spec=spec,
|
||||||
|
identity=wrong_name,
|
||||||
|
namespace_fd=namespace_fd,
|
||||||
|
ownership=ownership,
|
||||||
|
)
|
||||||
|
|
||||||
|
wrong_inode: BindspaceIdentity = BindspaceIdentity(
|
||||||
|
kind='netns',
|
||||||
|
key='tractor-wg0',
|
||||||
|
inode=token_path.stat().st_ino + 1,
|
||||||
|
)
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError,
|
||||||
|
match='FD inode.*identity inode',
|
||||||
|
):
|
||||||
|
BindspaceHandle(
|
||||||
|
spec=spec,
|
||||||
|
identity=wrong_inode,
|
||||||
|
namespace_fd=namespace_fd,
|
||||||
|
ownership=ownership,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
('model', 'kwargs', 'match'),
|
||||||
|
(
|
||||||
|
pytest.param(
|
||||||
|
BindspaceIdentity,
|
||||||
|
{
|
||||||
|
'kind': 'netns',
|
||||||
|
'key': None,
|
||||||
|
'inode': None,
|
||||||
|
},
|
||||||
|
'must be a positive `int`',
|
||||||
|
id='identity-requires-inode',
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
BindspaceSpec,
|
||||||
|
{'kind': 'vrf'},
|
||||||
|
'Unsupported bindspace kind',
|
||||||
|
id='spec-rejects-kind',
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
BindspaceIdentity,
|
||||||
|
{
|
||||||
|
'kind': 'vrf',
|
||||||
|
'key': 'blue',
|
||||||
|
'inode': 1234,
|
||||||
|
},
|
||||||
|
'Unsupported bindspace kind',
|
||||||
|
id='identity-rejects-kind',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_bindspace_models_reject_invalid_values(
|
||||||
|
model: type[BindspaceSpec]|type[BindspaceIdentity],
|
||||||
|
kwargs: dict[str, object],
|
||||||
|
match: str,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Direct msgspec construction does not enforce field annotations.
|
||||||
|
|
||||||
|
Parameterize the missing stable inode and future, unimplemented
|
||||||
|
kinds. Prove neither serializable model can carry invalid
|
||||||
|
identity or provisioning instructions into spawn configuration.
|
||||||
|
|
||||||
|
'''
|
||||||
|
with pytest.raises(ValueError, match=match):
|
||||||
|
model(**kwargs) # type: ignore[arg-type]
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
'''
|
||||||
|
Process-local struct wire-encoding guards.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tractor.msg import ProcessLocal
|
||||||
|
|
||||||
|
|
||||||
|
class LocalHandle(ProcessLocal):
|
||||||
|
'''
|
||||||
|
Minimal process-local struct used to exercise the global marker.
|
||||||
|
|
||||||
|
'''
|
||||||
|
resource_id: int
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'nested',
|
||||||
|
(
|
||||||
|
pytest.param(
|
||||||
|
False,
|
||||||
|
id='direct',
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
True,
|
||||||
|
id='nested',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_process_local_rejects_default_encoding(
|
||||||
|
nested: bool,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Process-local values can appear directly or deep in a payload.
|
||||||
|
|
||||||
|
Embed the same marked struct at both depths and prove msgspec's
|
||||||
|
normal traversal reaches the unsupported sentinel without a
|
||||||
|
tractor-specific recursive payload scan.
|
||||||
|
|
||||||
|
'''
|
||||||
|
handle: LocalHandle = LocalHandle(resource_id=1)
|
||||||
|
value: object = (
|
||||||
|
{'nested': [handle]}
|
||||||
|
if nested
|
||||||
|
else handle
|
||||||
|
)
|
||||||
|
|
||||||
|
assert repr(handle) == 'LocalHandle(resource_id=1)'
|
||||||
|
with pytest.raises(
|
||||||
|
TypeError,
|
||||||
|
match='_ProcessLocalToken.*unsupported',
|
||||||
|
):
|
||||||
|
msgspec.msgpack.encode(value)
|
||||||
|
|
@ -24,6 +24,13 @@ Heavier submodules like ``._addr`` and ``._api`` are NOT imported
|
||||||
here to avoid circular imports; use direct module paths for those.
|
here to avoid circular imports; use direct module paths for those.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
from ._bindspace import (
|
||||||
|
BindspaceHandle as BindspaceHandle,
|
||||||
|
BindspaceIdentity as BindspaceIdentity,
|
||||||
|
BindspaceKind as BindspaceKind,
|
||||||
|
BindspaceOwnership as BindspaceOwnership,
|
||||||
|
BindspaceSpec as BindspaceSpec,
|
||||||
|
)
|
||||||
from ._multiaddr import (
|
from ._multiaddr import (
|
||||||
parse_endpoints as parse_endpoints,
|
parse_endpoints as parse_endpoints,
|
||||||
parse_maddr as parse_maddr,
|
parse_maddr as parse_maddr,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
# tractor: structured concurrent "actors".
|
||||||
|
# Copyright 2018-eternity Tyler Goodlet.
|
||||||
|
|
||||||
|
# This program is free software: you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Affero General Public License
|
||||||
|
# as published by the Free Software Foundation, either version 3 of
|
||||||
|
# the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU Affero General Public
|
||||||
|
# License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
'''
|
||||||
|
Serializable bindspace declarations and live capability handles.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import (
|
||||||
|
get_args,
|
||||||
|
Literal,
|
||||||
|
TypeAlias,
|
||||||
|
)
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
|
||||||
|
from ..msg._local import ProcessLocal
|
||||||
|
|
||||||
|
|
||||||
|
BindspaceKind: TypeAlias = Literal[
|
||||||
|
'netns',
|
||||||
|
]
|
||||||
|
BindspaceOwnership: TypeAlias = Literal[
|
||||||
|
'owned', # manager tears down the resource after final release
|
||||||
|
'borrowed', # manager leaves the pre-existing resource intact
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_bindspace_kind(
|
||||||
|
kind: BindspaceKind,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Reject platform-resource kinds without an implementation.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if kind not in get_args(BindspaceKind):
|
||||||
|
raise ValueError(
|
||||||
|
f'Unsupported bindspace kind: {kind!r}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BindspaceSpec(
|
||||||
|
msgspec.Struct,
|
||||||
|
frozen=True,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Serializable declaration of one requested bindspace.
|
||||||
|
|
||||||
|
'''
|
||||||
|
kind: BindspaceKind
|
||||||
|
key: str|None = None
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
'''
|
||||||
|
Reject an empty platform-resource key.
|
||||||
|
|
||||||
|
'''
|
||||||
|
_validate_bindspace_kind(self.kind)
|
||||||
|
if self.key == '':
|
||||||
|
raise ValueError(
|
||||||
|
'`BindspaceSpec.key` must be non-empty or `None`!'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BindspaceIdentity(
|
||||||
|
msgspec.Struct,
|
||||||
|
frozen=True,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Serializable stable identity of one realized bindspace.
|
||||||
|
|
||||||
|
`.key` is an optional, mutable namespace name. `.inode` is the
|
||||||
|
required kernel identity which remains stable after rename or
|
||||||
|
unlink.
|
||||||
|
|
||||||
|
'''
|
||||||
|
kind: BindspaceKind
|
||||||
|
key: str|None
|
||||||
|
inode: int
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
'''
|
||||||
|
Require a stable platform identity and an optional name.
|
||||||
|
|
||||||
|
'''
|
||||||
|
_validate_bindspace_kind(self.kind)
|
||||||
|
if self.key == '':
|
||||||
|
raise ValueError(
|
||||||
|
'`BindspaceIdentity.key` must be non-empty '
|
||||||
|
'or `None`!'
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
type(self.inode) is not int
|
||||||
|
or
|
||||||
|
self.inode <= 0
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
'`BindspaceIdentity.inode` must be a positive `int`!'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BindspaceHandle(
|
||||||
|
ProcessLocal,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Process-local capability for one live realized bindspace.
|
||||||
|
|
||||||
|
`ProcessLocal` provides compact typed storage plus a default
|
||||||
|
wire-encoding guard. Explicit FD transfer and handle construction
|
||||||
|
belong to the supervisor's spawn/bootstrap path.
|
||||||
|
|
||||||
|
'''
|
||||||
|
spec: BindspaceSpec
|
||||||
|
identity: BindspaceIdentity
|
||||||
|
namespace_fd: int|None
|
||||||
|
ownership: BindspaceOwnership
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
'''
|
||||||
|
Validate and retain one scoped bindspace capability.
|
||||||
|
|
||||||
|
'''
|
||||||
|
spec: BindspaceSpec = self.spec
|
||||||
|
identity: BindspaceIdentity = self.identity
|
||||||
|
namespace_fd: int|None = self.namespace_fd
|
||||||
|
ownership: BindspaceOwnership = self.ownership
|
||||||
|
|
||||||
|
if spec.kind != identity.kind:
|
||||||
|
raise ValueError(
|
||||||
|
'`BindspaceSpec.kind` does not match '
|
||||||
|
'`BindspaceIdentity.kind`!'
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
spec.key is not None
|
||||||
|
and
|
||||||
|
spec.key != identity.key
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
'`BindspaceSpec.key` does not match '
|
||||||
|
'`BindspaceIdentity.key`!'
|
||||||
|
)
|
||||||
|
if ownership not in get_args(BindspaceOwnership):
|
||||||
|
raise ValueError(
|
||||||
|
f'Invalid bindspace ownership: {ownership!r}'
|
||||||
|
)
|
||||||
|
if namespace_fd is not None:
|
||||||
|
if (
|
||||||
|
type(namespace_fd) is not int
|
||||||
|
or
|
||||||
|
namespace_fd < 0
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
'`namespace_fd` must be non-negative or `None`!'
|
||||||
|
)
|
||||||
|
fd_inode: int = os.fstat(namespace_fd).st_ino
|
||||||
|
if identity.inode != fd_inode:
|
||||||
|
raise ValueError(
|
||||||
|
f'Namespace FD inode {fd_inode} does not match '
|
||||||
|
f'identity inode {identity.inode}!'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
'''
|
||||||
|
Render capability identity without dereferencing its FD.
|
||||||
|
|
||||||
|
'''
|
||||||
|
return (
|
||||||
|
f'{type(self).__name__}('
|
||||||
|
f'identity={self.identity!r}, '
|
||||||
|
f'ownership={self.ownership!r}, '
|
||||||
|
f'namespace_fd={self.namespace_fd!r})'
|
||||||
|
)
|
||||||
|
|
@ -27,6 +27,9 @@ from .ptr import (
|
||||||
from .pretty_struct import (
|
from .pretty_struct import (
|
||||||
Struct as Struct,
|
Struct as Struct,
|
||||||
)
|
)
|
||||||
|
from ._local import (
|
||||||
|
ProcessLocal as ProcessLocal,
|
||||||
|
)
|
||||||
from ._codec import (
|
from ._codec import (
|
||||||
_def_msgspec_codec as _def_msgspec_codec,
|
_def_msgspec_codec as _def_msgspec_codec,
|
||||||
_ctxvar_MsgCodec as _ctxvar_MsgCodec,
|
_ctxvar_MsgCodec as _ctxvar_MsgCodec,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
# tractor: structured concurrent "actors".
|
||||||
|
# Copyright 2018-eternity Tyler Goodlet.
|
||||||
|
|
||||||
|
# This program is free software: you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Affero General Public License
|
||||||
|
# as published by the Free Software Foundation, either version 3 of
|
||||||
|
# the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU Affero General Public
|
||||||
|
# License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
'''
|
||||||
|
Markers for process-local values which must not cross actor IPC.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
|
||||||
|
|
||||||
|
class _ProcessLocalToken:
|
||||||
|
'''
|
||||||
|
Unsupported msgspec value embedded in every `ProcessLocal`.
|
||||||
|
|
||||||
|
'''
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
|
||||||
|
_PROCESS_LOCAL_TOKEN: _ProcessLocalToken = _ProcessLocalToken()
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessLocal(
|
||||||
|
msgspec.Struct,
|
||||||
|
kw_only=True,
|
||||||
|
repr_omit_defaults=True,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Generic struct marker which rejects default msgspec encoding.
|
||||||
|
|
||||||
|
The hidden sentinel remains part of the encoded field set, so
|
||||||
|
msgspec encounters `_ProcessLocalToken` and raises `TypeError`
|
||||||
|
even when this value is nested inside another supported payload.
|
||||||
|
A custom encode hook may explicitly override that safeguard.
|
||||||
|
|
||||||
|
Keyword-only fields let subclasses add required fields after the
|
||||||
|
marker's default sentinel.
|
||||||
|
|
||||||
|
'''
|
||||||
|
_process_local: _ProcessLocalToken = _PROCESS_LOCAL_TOKEN
|
||||||
Loading…
Reference in New Issue