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
abfb5e3adf
commit
fc0049da18
|
|
@ -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):
|
||||
'''Stable identity of the realized platform resource.'''
|
||||
kind: str
|
||||
key: str|None
|
||||
inode: int|None # Linux namespace identity
|
||||
key: str|None # mutable name, absent after unlink
|
||||
inode: int # stable Linux namespace identity
|
||||
|
||||
|
||||
class BindspaceHandle:
|
||||
class BindspaceHandle(ProcessLocal):
|
||||
'''Scoped, non-serializable capability for one live bindspace.'''
|
||||
spec: BindspaceSpec
|
||||
identity: BindspaceIdentity
|
||||
|
|
@ -437,13 +437,19 @@ async def open_bindspace(
|
|||
'''
|
||||
```
|
||||
|
||||
The exact field set remains design work; the required split does not:
|
||||
`BindspaceSpec` crosses config/spawn serialization, while
|
||||
`BindspaceHandle` contains live OS resources (especially an open
|
||||
namespace FD), pins identity/lifetime, and must never cross msgpack.
|
||||
An FD is a stronger capability than a namespace name: it avoids
|
||||
name-resolution TOCTOU, survives rename/unlink, and identifies the
|
||||
exact namespace the parent provisioned.
|
||||
The initial model limits `BindspaceKind` to `netns` while preserving
|
||||
the required lifetime split. `BindspaceSpec` and
|
||||
`BindspaceIdentity` are frozen msgspec structs which cross
|
||||
config/spawn serialization. `BindspaceHandle` also uses msgspec's
|
||||
generic struct storage by inheriting the global
|
||||
`tractor.msg.ProcessLocal` marker. Its hidden unsupported sentinel
|
||||
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
|
||||
`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.
|
||||
|
||||
'''
|
||||
from ._bindspace import (
|
||||
BindspaceHandle as BindspaceHandle,
|
||||
BindspaceIdentity as BindspaceIdentity,
|
||||
BindspaceKind as BindspaceKind,
|
||||
BindspaceOwnership as BindspaceOwnership,
|
||||
BindspaceSpec as BindspaceSpec,
|
||||
)
|
||||
from ._multiaddr import (
|
||||
parse_endpoints as parse_endpoints,
|
||||
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 (
|
||||
Struct as Struct,
|
||||
)
|
||||
from ._local import (
|
||||
ProcessLocal as ProcessLocal,
|
||||
)
|
||||
from ._codec import (
|
||||
_def_msgspec_codec as _def_msgspec_codec,
|
||||
_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