Compare commits
No commits in common. "5d92595fb45e1db3adacadebcacc3eba17070a29" and "f81fc5e516f1b7c9dd7146b4100bf2268fcb788a" have entirely different histories.
5d92595fb4
...
f81fc5e516
|
|
@ -1,37 +0,0 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
session: tractor-addr-unpacking-followup
|
||||
timestamp: 2026-08-19T21:31:44Z
|
||||
git_ref: f81fc5e5
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260819T213144Z_f81fc5e5_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
The human requested runtime boundary integration with the established
|
||||
per-step implementation and commit-plan workflow.
|
||||
|
||||
## Response summary
|
||||
|
||||
Kept `TunnelledAddress` available to callers while peeling it at the
|
||||
last outbound boundary before transport lookup and dialing. Added a
|
||||
regression which captures both transport arguments and confirms plain
|
||||
TCP behavior is unchanged.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `tractor/ipc/_chan.py` - peel tunnel annotations before outbound
|
||||
transport dispatch and connection.
|
||||
- `tests/ipc/test_channel_tunnel_boundary.py` - verify plain and
|
||||
tunnelled channel inputs deliver only TCP overlays.
|
||||
|
||||
## Human edits
|
||||
|
||||
The human chose the runtime-boundary slice, required the existing
|
||||
per-step commit-plan flow, and previously established that wrappers
|
||||
must retain bindspace metadata without impersonating transports. The
|
||||
agent implemented those constraints; no direct manual source edits were
|
||||
observed during this step.
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-08-19T21:31:44Z
|
||||
git_ref: f81fc5e5
|
||||
diff_cmd: git diff HEAD~1..HEAD
|
||||
---
|
||||
|
||||
# Raw output - outbound tunnel boundary
|
||||
|
||||
The human requested the next tunnelled-address slice using the same
|
||||
per-step commit-plan flow. Existing design decisions require retaining
|
||||
tunnel metadata until the narrow IPC transport boundary and never
|
||||
teaching exact-type transport tables about tunnel wrappers.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- tractor/ipc/_chan.py tests/ipc/test_channel_tunnel_boundary.py`
|
||||
|
||||
Extended channel address inputs to accept tunnel declarations, then
|
||||
called `strip_tunnels()` immediately before exact-type transport lookup
|
||||
and `connect_to()`. Added plain/tunnel parameterized coverage proving
|
||||
both operations receive the identical TCP overlay while the original
|
||||
wrapper retains its tunnel spec.
|
||||
|
||||
Verification included focused IPC tests, Ruff, discovery/IPC suites,
|
||||
and the full tractor suite.
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
session: tractor-addr-unpacking-followup
|
||||
timestamp: 2026-08-19T21:31:45Z
|
||||
git_ref: f81fc5e5
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260819T213145Z_f81fc5e5_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
The human requested completion of inbound runtime peeling using the
|
||||
same per-step implementation and commit-plan workflow.
|
||||
|
||||
## Response summary
|
||||
|
||||
Preserved tunnel declarations through listener configuration, peeled
|
||||
them immediately before `Endpoint` construction, and used the overlay
|
||||
for backend-specific random listener allocation after registry
|
||||
discovery. Added a real listener regression for the reflection and
|
||||
exact-type boundary.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `tractor/ipc/_server.py` - accept wrapper declarations and peel at
|
||||
`Endpoint` construction.
|
||||
- `tractor/_root.py` - allocate random transport addresses from the
|
||||
contacted registry's overlay.
|
||||
- `tests/ipc/test_server_tunnel_boundary.py` - verify a real listener
|
||||
stores only TCP while preserving the source annotation.
|
||||
|
||||
## Human edits
|
||||
|
||||
The human selected the runtime-boundary work and previously corrected
|
||||
the architecture so tractor retains future bindspace provisioning
|
||||
ownership while `Endpoint` sees only application transports. The agent
|
||||
implemented and tested that direction; no direct manual source edits
|
||||
were observed during this step.
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-08-19T21:31:45Z
|
||||
git_ref: f81fc5e5
|
||||
diff_cmd: git diff HEAD~1..HEAD
|
||||
---
|
||||
|
||||
# Raw output - inbound tunnel boundary
|
||||
|
||||
The human requested runtime boundary integration while preserving the
|
||||
future tractor-owned bindspace lifecycle.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- tractor/ipc/_server.py tractor/_root.py tests/ipc/test_server_tunnel_boundary.py`
|
||||
|
||||
Broadened listener declarations to carry tunnel wrappers until
|
||||
`_serve_ipc_eps()` and peeled immediately before `Endpoint`
|
||||
construction. Also peeled a contacted tunnelled registry before
|
||||
backend-specific random listener allocation. Added a real TCP listener
|
||||
regression proving `Endpoint` stores only the resolved overlay while
|
||||
the original declaration retains bindspace metadata.
|
||||
|
||||
Verification included `465` collected tests, `84` passing
|
||||
discovery/IPC tests with two xpasses, Ruff, and the full suite with
|
||||
`447` passes.
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
session: tractor-addr-unpacking-followup
|
||||
timestamp: 2026-08-20T02:15:16Z
|
||||
git_ref: dfad66a0
|
||||
scope: docs
|
||||
substantive: true
|
||||
raw_file: 20260820T021516Z_dfad66a0_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
The human requested that the bindspace plan preserve the agreed
|
||||
capability, spawn-bootstrap, endpoint-role, namespace augmentation,
|
||||
random-address, and teardown semantics, using `github/ns_aware` as
|
||||
prototype input.
|
||||
|
||||
## Response summary
|
||||
|
||||
Updated plan-03 and the shared backend contract to separate serializable
|
||||
bindspace declarations from scoped live capabilities, make namespace
|
||||
entry a pre-runtime spawn operation, keep maddr paths role-neutral, and
|
||||
define listen/dial provisioning plus ownership-sensitive teardown.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `ai/tpt-backends/03_wg_tunnel_bindspace.md` - layer-C capability,
|
||||
bootstrap, role, teardown, test, and risk model.
|
||||
- `ai/tpt-backends/00_shared_backend_contract.md` - distinguish
|
||||
transport bind selectors from process namespace lifecycle.
|
||||
|
||||
## Human edits
|
||||
|
||||
The human supplied the core architecture: structured scoped
|
||||
capabilities, spawn-time namespace entry, orthogonal namespace
|
||||
augmentation, source/destination-dependent provisioning, and
|
||||
role-dependent teardown. They also rejected premature assumptions about
|
||||
`open_bindspace()` returning an address and requested grounding in the
|
||||
existing namespace prototype. The agent translated those decisions into
|
||||
the plan text; no direct manual source edits were observed.
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-08-20T02:15:16Z
|
||||
git_ref: dfad66a0
|
||||
diff_cmd: git diff HEAD~1..HEAD
|
||||
---
|
||||
|
||||
# Raw output - bindspace capability design
|
||||
|
||||
The human corrected the layer-C design around local network-stack
|
||||
realization. They established that bindspace state should be both
|
||||
structured and a scoped capability; namespace entry belongs in
|
||||
subactor bootstrap; maddrs can describe source or destination network
|
||||
paths while namespace selection augments them orthogonally; random
|
||||
address and teardown behavior depend on operation role and ownership.
|
||||
|
||||
They directed comparison with the prototype on `github/ns_aware` and
|
||||
requested these decisions be preserved in the plan.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- ai/tpt-backends/03_wg_tunnel_bindspace.md ai/tpt-backends/00_shared_backend_contract.md`
|
||||
|
||||
Reworked layer C around serializable `BindspaceSpec`, stable
|
||||
`BindspaceIdentity`, and scoped non-serializable `BindspaceHandle`
|
||||
concepts. Namespace FDs pin identity and lifetime; parent/supervisor
|
||||
provisioning transfers entry capability through spawn; the child enters
|
||||
before runtime, channels, listeners, sockets, or worker threads and then
|
||||
drops authority. Listen/dial roles and owned/borrowed teardown are
|
||||
explicit, while maddrs remain role-neutral network-path declarations.
|
||||
|
||||
The shared backend contract now separates transport-level `.bindspace`
|
||||
selectors from process namespace lifecycle. Added tests/risks for FD
|
||||
identity, bootstrap ordering, privilege drop, role ownership, and
|
||||
shared-resource teardown.
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
session: tractor-addr-unpacking
|
||||
timestamp: 2026-08-20T03:31:07Z
|
||||
git_ref: ba07e09d
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260820T033107Z_ba07e09d_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
The human requested canonical tagged transport addresses with a
|
||||
reader-first migration. TCP should decode `('tcp', host, port)`, Unix
|
||||
should decode `('unix', path)`, `uds` should remain an accepted input
|
||||
alias and internal transport key, and legacy tuple/list inputs must keep
|
||||
working before writers switch formats.
|
||||
|
||||
## Response summary
|
||||
|
||||
Introduced canonical and compatibility address aliases, explicit tagged
|
||||
dispatch, transport-specific tagged readers, and focused serialization
|
||||
tests. Kept legacy pair inputs and native IPv6 socket values readable so
|
||||
this boundary can ship before tagged emission.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `tractor/discovery/_addr.py` - address aliases and tagged dispatch.
|
||||
- `tractor/ipc/_tcp.py` - tagged, legacy, and IPv6 TCP decoding.
|
||||
- `tractor/ipc/_uds.py` - canonical Unix and UDS-alias decoding.
|
||||
- `tests/discovery/test_address_serialization.py` - reader compatibility
|
||||
coverage.
|
||||
|
||||
## Human edits
|
||||
|
||||
The human supplied the canonical `tcp` and `unix` forms, chose `uds` as
|
||||
an input-only serialization alias while preserving it as the runtime
|
||||
transport key, and required a reader-first commit boundary. The agent
|
||||
implemented those decisions; no direct manual source edits were
|
||||
observed.
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-08-20T03:31:07Z
|
||||
git_ref: ba07e09d
|
||||
diff_cmd: git diff HEAD~1..HEAD
|
||||
---
|
||||
|
||||
# Raw output - tagged address readers
|
||||
|
||||
The human requested a migration away from ambiguous untagged transport
|
||||
tuples. They established `('tcp', host, port)` and `('unix', path)` as
|
||||
canonical forms, retained `('uds', path)` as an input alias, and required
|
||||
a reader-first compatibility boundary before changing emitted values.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- tractor/discovery/_addr.py tractor/ipc/_tcp.py tractor/ipc/_uds.py tests/discovery/test_address_serialization.py`
|
||||
|
||||
Added explicit tagged address aliases and dispatch, taught TCP and UDS
|
||||
readers to decode tagged tuple/list payloads, preserved legacy pair input,
|
||||
and retained native IPv6 socket-address decoding. Added focused tests for
|
||||
canonical tags, the UDS alias, msgpack-style lists, legacy pairs, and IPv6
|
||||
socket values.
|
||||
|
||||
Focused reader tests and Ruff checks passed before the writer migration
|
||||
was applied.
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
session: tractor-addr-unpacking
|
||||
timestamp: 2026-08-20T03:31:08Z
|
||||
git_ref: ba07e09d
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260820T033108Z_ba07e09d_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
The human asked the agent to continue after adding tagged readers,
|
||||
complete canonical address emission without dropping legacy input
|
||||
compatibility, verify the migration, and prepare a complete multi-commit
|
||||
package when the turn was done.
|
||||
|
||||
## Response summary
|
||||
|
||||
Changed `.unwrap()` to emit tagged TCP and Unix addresses, updated direct
|
||||
tuple consumers and spawn payload declarations, and aligned multiaddr,
|
||||
runtime, IPC, and discovery tests with canonical serialized equality.
|
||||
Kept untagged tuples and the `uds` spelling readable at input boundaries.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `tractor/discovery/_addr.py` - canonical output alias.
|
||||
- `tractor/discovery/_multiaddr.py` - tagged address composition.
|
||||
- `tractor/ipc/_tcp.py` - tagged emission and direct socket dialing.
|
||||
- `tractor/ipc/_uds.py` - tagged full-path emission.
|
||||
- `tractor/msg/types.py` - protocol-neutral spawn tuple containers.
|
||||
- `tests/discovery/test_address_serialization.py` - writer assertions.
|
||||
- `tests/discovery/test_multiaddr.py` - canonical round-trip assertions.
|
||||
- `tests/discovery/test_tpt_bind_addrs.py` - tagged bind assertions.
|
||||
- `tests/ipc/test_each_tpt.py` - canonical runtime address assertions.
|
||||
- `tests/ipc/test_server_tunnel_boundary.py` - tagged TCP destructuring.
|
||||
- `tests/test_local.py` - canonical registry comparison.
|
||||
|
||||
## Human edits
|
||||
|
||||
The human established the reader-before-writer sequencing, canonical tag
|
||||
spellings, retained compatibility expectations, and requested final
|
||||
multi-commit packaging. The agent implemented and tested those choices;
|
||||
no direct manual source edits were observed.
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-08-20T03:31:08Z
|
||||
git_ref: ba07e09d
|
||||
diff_cmd: git diff HEAD~1..HEAD
|
||||
---
|
||||
|
||||
# Raw output - canonical tagged address writers
|
||||
|
||||
After the reader compatibility boundary, the human asked the agent to
|
||||
continue the migration and package the completed work as dependency-
|
||||
ordered commits.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- tractor/discovery/_addr.py tractor/discovery/_multiaddr.py tractor/ipc/_tcp.py tractor/ipc/_uds.py tractor/msg/types.py tests/discovery/test_address_serialization.py tests/discovery/test_multiaddr.py tests/discovery/test_tpt_bind_addrs.py tests/ipc/test_each_tpt.py tests/ipc/test_server_tunnel_boundary.py tests/test_local.py`
|
||||
|
||||
Switched TCP and Unix `.unwrap()` output to canonical tagged tuples,
|
||||
updated direct transport and multiaddr consumers, widened spawn message
|
||||
tuple containers for protocol-specific shapes, and migrated runtime and
|
||||
test comparisons to serialized address equality. Legacy inputs remain
|
||||
accepted at `wrap_address()` and backend reader boundaries.
|
||||
|
||||
Ruff and focused tests passed. The complete non-debugger TCP suite passed
|
||||
with 412 tests; the UDS suite reached 80% without failure before the
|
||||
harness timeout, then all 97 remaining tests passed on resume. Debugger
|
||||
PTY coverage was excluded after an unrelated timeout.
|
||||
|
|
@ -112,12 +112,7 @@ Hard constraints learned from the existing two:
|
|||
- **`.bindspace` semantics**: "the address' bindable space" —
|
||||
ip/host for `tcp`, the socket-file *directory* for `uds`. For
|
||||
the new backends: the TIPC *scope* (§1 of plan 01), the iroh
|
||||
*ALPN + relay/discovery realm* (plan 02). Do not overload this
|
||||
transport-level bind selector with process namespace lifecycle.
|
||||
Plan 03 augments an maddr/address declaration with a serializable
|
||||
`BindspaceSpec` and a scoped, non-serializable `BindspaceHandle`;
|
||||
the latter owns namespace identity/FD/lifetime and is consumed at
|
||||
spawn bootstrap before a concrete address reaches transport bind.
|
||||
*ALPN + relay/discovery realm* (plan 02), the netns (plan 03).
|
||||
`Address.namespace` is already spec'd in the Protocol as
|
||||
"the if-available OS-specific network namespace key" and is
|
||||
currently unimplemented by both backends — plan 03 is the
|
||||
|
|
|
|||
|
|
@ -329,109 +329,39 @@ for the overlay application socket.
|
|||
|
||||
### 5.1 the composition
|
||||
|
||||
The maddr describes the composed network path and can be used as
|
||||
either a source/listen or destination/dial handle. It does **not**
|
||||
select the local instance of that network stack. A netns, VRF,
|
||||
interface, user namespace, or equivalent platform resource is
|
||||
orthogonal augmentation carried alongside/below the maddr.
|
||||
|
||||
Keep two bindspace representations with deliberately different
|
||||
lifetimes:
|
||||
|
||||
```python
|
||||
class BindspaceSpec(msgspec.Struct, frozen=True):
|
||||
'''Serializable spawn/config declaration.'''
|
||||
kind: str # `netns`, later `vrf`, ...
|
||||
key: str|None # requested name/key, if any
|
||||
|
||||
|
||||
class BindspaceIdentity(msgspec.Struct, frozen=True):
|
||||
'''Stable identity of the realized platform resource.'''
|
||||
kind: str
|
||||
key: str|None
|
||||
inode: int|None # Linux namespace identity
|
||||
|
||||
|
||||
class BindspaceHandle:
|
||||
'''Scoped, non-serializable capability for one live bindspace.'''
|
||||
spec: BindspaceSpec
|
||||
identity: BindspaceIdentity
|
||||
namespace_fd: int|None
|
||||
ownership: Literal['owned', 'borrowed']
|
||||
|
||||
|
||||
@acm
|
||||
async def open_bindspace(
|
||||
spec: BindspaceSpec,
|
||||
*,
|
||||
role: Literal['listen', 'dial'],
|
||||
) -> AsyncGenerator[BindspaceHandle, None]:
|
||||
addr: TunnelledAddress,
|
||||
) -> AsyncGenerator[Address, None]:
|
||||
'''
|
||||
Provision/borrow one bindspace and yield its live capability.
|
||||
Enter the net-bindspace implied by `addr`'s tunnel stack,
|
||||
yielding the *overlay* `Address` ready to bind/connect.
|
||||
|
||||
Nests: one `@acm` per tunnel segment, outermost-first, so
|
||||
a 2-deep stack is just two nested `async with`s and the
|
||||
teardown order is guaranteed by `trio`.
|
||||
|
||||
'''
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
`open_bindspace()` is **not** an address factory and does not return a
|
||||
`TunnelledAddress`. At the declaration layer, listener allocation can
|
||||
use the handle to replace an overlay while preserving every tunnel:
|
||||
|
||||
```python
|
||||
async with open_bindspace(
|
||||
bindspace_spec,
|
||||
role='listen',
|
||||
) as bindspace:
|
||||
listen_decl = declared_addr.get_random(
|
||||
bindspace=bindspace,
|
||||
)
|
||||
transport_addr = strip_tunnels(listen_decl)
|
||||
```
|
||||
|
||||
That sketch intentionally leaves the `.get_random()`/bindspace value
|
||||
contract open. A concrete transport call returns a concrete overlay;
|
||||
a declaration-level call may replace the overlay and return a new
|
||||
`TunnelledAddress`. In either case wrappers remain until the final
|
||||
transport bind/dial boundary, where `strip_tunnels()` is mandatory.
|
||||
|
||||
Per-platform provisioning still composes one resource context per
|
||||
tunnel/bindspace layer:
|
||||
with per-tunnel-kind implementations:
|
||||
|
||||
```python
|
||||
@acm
|
||||
async def open_netns(
|
||||
spec: BindspaceSpec,
|
||||
role: Literal['listen', 'dial'],
|
||||
) -> AsyncGenerator[BindspaceHandle, None]: ...
|
||||
|
||||
async def open_netns(name: str) -> AsyncGenerator[None, None]: ...
|
||||
@acm
|
||||
async def open_wg_iface(
|
||||
spec: WGTunnelSpec,
|
||||
bindspace: BindspaceHandle,
|
||||
role: Literal['listen', 'dial'],
|
||||
) -> AsyncGenerator[WGTunnelSpec, None]: ...
|
||||
async def open_wg_iface(spec: WGTunnelSpec) -> AsyncGenerator[WGTunnelSpec, None]: ...
|
||||
```
|
||||
|
||||
and a driver that folds a list of specs into nested contexts
|
||||
(`contextlib.AsyncExitStack` for the N-deep case). The
|
||||
`parse_endpoints()` API (`_multiaddr.py:153`) is the front door:
|
||||
it already returns
|
||||
`dict[name, list[Address|TunnelledAddress]]` and the
|
||||
it already returns `dict[name, list[Address]]` and the
|
||||
`multiaddr_declare_eps.md` sketch anticipates the recursive
|
||||
`dict[str, list[Address]]|dict[...]` return for tunnelled
|
||||
entries. Extend it to carry the tunnel stack, not to *enter* it.
|
||||
|
||||
The caller supplies `role`; do not infer it from maddr shape. The same
|
||||
composed maddr can name a server source or client destination, and the
|
||||
required local provisioning/ownership differs (§5.3).
|
||||
|
||||
### 5.2 `Address.namespace`, at last
|
||||
|
||||
- `TunnelledAddress.namespace` → `(kind, id)` e.g.
|
||||
|
|
@ -447,13 +377,6 @@ required local provisioning/ownership differs (§5.3).
|
|||
`Endpoint.pformat()`, `_server.py:645`). Fill that in; it's
|
||||
the cheapest possible proof the layer is wired.
|
||||
|
||||
Use `github/ns_aware@e4688cad` as prototype evidence, not code to
|
||||
cherry-pick unchanged. Its `/proc/<pid>/ns/<type>` inode reader and
|
||||
`ip netns identify` probe establish the useful `(key, inode)` identity
|
||||
pair. Layer C should move that shape into `BindspaceIdentity`, avoid a
|
||||
subprocess where netlink/procfs suffices, and hold the namespace FD in
|
||||
`BindspaceHandle` to pin the identity.
|
||||
|
||||
### 5.3 the netns/process reality — read this before designing
|
||||
|
||||
**The headline consequence, stated up front**: netns is a
|
||||
|
|
@ -481,52 +404,26 @@ server bound in the old namespace.
|
|||
- entering a netns is *process-global-ish and irreversible-ish*
|
||||
in practice. Therefore: **netns membership belongs to the
|
||||
actor process, decided before the runtime binds**, not to a
|
||||
mid-life actor API. Design:
|
||||
- the root/parent decides the `BindspaceSpec`, provisions or
|
||||
borrows it, and passes the spec plus an inherited/transferred
|
||||
namespace-FD capability through the spawn backend (there's already
|
||||
mid-life `@acm`. Design:
|
||||
- the root/parent decides the netns for a subactor and passes
|
||||
it in the spawn spec (there's already
|
||||
`enable_transports`/`accept_addrs` plumbing at
|
||||
`_runtime.py:1595-1615` — the netns rides alongside).
|
||||
- the child spawn/bootstrap trampoline calls `setns()` **before**
|
||||
`_runtime.async_main()`, `IPCServer.listen_on()`, parent-channel
|
||||
connection, or creation of any worker thread/socket.
|
||||
- only after successful entry does the child drop namespace-entry
|
||||
privileges and initialize the actor runtime.
|
||||
- a root/single-actor process follows the same ordering: enter during
|
||||
root bootstrap, never after actor runtime startup.
|
||||
- iface/route/WG provisioning is genuinely scoped and remains under
|
||||
the parent/supervisor's `BindspaceHandle` context.
|
||||
- the child, in `_runtime.async_main()` **before**
|
||||
`IPCServer.listen_on()`, enters it.
|
||||
- the mid-life `@acm` form is then only for the *root* /
|
||||
single-actor case, and for iface creation (which is
|
||||
genuinely scoped).
|
||||
- document the constraint rather than hiding it; a
|
||||
`RuntimeError` if namespace entry is attempted after bootstrap.
|
||||
- capabilities: iface/netns creation/config needs `CAP_NET_ADMIN`;
|
||||
entering an existing Linux namespace normally requires
|
||||
`CAP_SYS_ADMIN` in the owning user namespace. Never `sudo` from
|
||||
inside the runtime. A privileged parent/helper should provision the
|
||||
stack and open the namespace FD; the child receives only the scoped
|
||||
capability and temporary authority needed to enter it, then drops
|
||||
that authority before actor code runs. This separates create/config
|
||||
authority from enter/use authority and fits user-namespace/capability
|
||||
deployments without granting every actor broad ambient caps.
|
||||
Two supported modes remain:
|
||||
`RuntimeError` if `open_netns()` is entered after any
|
||||
listener exists.
|
||||
- privileges: iface/netns creation needs `CAP_NET_ADMIN`.
|
||||
Never `sudo` from inside the runtime. Two supported modes:
|
||||
(i) pre-provisioned out-of-band (layers A/B — the default,
|
||||
and what #482 documents), (ii) runtime-managed when the supervising
|
||||
process/helper holds the required caps. Probe exact required caps and
|
||||
*fail loudly with an actionable message* otherwise.
|
||||
- role semantics are explicit:
|
||||
- `listen`: may create/own the local bindspace, iface, routes, WG
|
||||
peer/listener state, and random local overlay; lifetime normally
|
||||
extends through all listeners and the actor process.
|
||||
- `dial`: may borrow an actor-wide bindspace or ensure local routing
|
||||
and tunnel state reaches the remote stack; it does not own the
|
||||
remote maddr and may need no new local resource at all.
|
||||
- source/destination use is an operation property, never permanently
|
||||
encoded into the maddr or inferred from segment ordering.
|
||||
- teardown follows capability ownership, not just address type:
|
||||
- owned listener bindspaces tear down after endpoints/channels and
|
||||
the actor process have exited;
|
||||
- borrowed dial/actor-wide bindspaces only release their handle;
|
||||
- nested resources exit inside-out, but shared resources remain until
|
||||
their owning supervisor drops the final capability.
|
||||
and what #482 documents), (ii) runtime-managed when the
|
||||
process already holds the cap. Detect with a cheap
|
||||
`os.geteuid()==0 or CAP_NET_ADMIN in /proc/self/status`
|
||||
probe and *fail loudly with an actionable message* otherwise.
|
||||
- teardown must be idempotent and tolerant: an iface/netns
|
||||
already gone must not strand the rest of the teardown — the
|
||||
exact lesson `_uds.close_listener()`'s `FileNotFoundError`
|
||||
|
|
@ -545,13 +442,6 @@ server bound in the old namespace.
|
|||
self-contained — no second host, no `sudo` in the test body.
|
||||
- the `to_thread`-netns-mismatch regression from §5.3, written
|
||||
**first** (red), then the fix (green), per project convention.
|
||||
- bootstrap ordering: assert the child reports the expected namespace
|
||||
inode before parent-channel connect and listener creation.
|
||||
- FD capability: rename/unlink the namespace name after opening its FD
|
||||
and prove child entry still selects the pinned inode.
|
||||
- privilege drop: prove actor code lacks provisioning caps after entry.
|
||||
- role/ownership: fake listen/dial resources and assert owned listener
|
||||
teardown versus borrowed dial-handle release.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -585,12 +475,8 @@ consider doing it *first* for exactly that reason.
|
|||
| risk | mitigation |
|
||||
| --- | --- |
|
||||
| `to_thread` worker runs in the wrong netns | §5.3; pass `netns=` to pyroute2 or pin a worker; test-first |
|
||||
| namespace name is renamed/replaced between provision and spawn | pass an open namespace FD; verify `(key, inode)` after child entry |
|
||||
| child starts sockets/threads before `setns()` | enter in the spawn bootstrap trampoline before `_runtime.async_main()`; assert inode ordering |
|
||||
| ambient capabilities leak into actor app code | split provision/enter authority and drop caps before runtime initialization |
|
||||
| dial path tears down a shared actor bindspace | encode ownership in `BindspaceHandle`; borrowed handles never remove resources |
|
||||
| py-multiaddr#108 merged but unreleased | PEP 621 direct-revision pin + `_wg_proto_code()` gate; replace with a release floor once published |
|
||||
| `TunnelledAddress` leaks into transport reflection/type dispatch | keep wrappers through declaration/bindspace handling, call `strip_tunnels()` at channel/endpoint boundaries, and retain the boundary regressions |
|
||||
| `TunnelledAddress` leaks into `Endpoint` and breaks `inspect.getmodule()` | unwrap at parse/bindspace boundary; assert `not isinstance(ep.addr, TunnelledAddress)` in `Endpoint.__post_init__` |
|
||||
| privileged ops in a library | never `sudo`; explicit cap probe + actionable error; pre-provisioned is the default |
|
||||
| pyroute2 0.9 asyncio core drags a loop into the actor | option (1) is a *thread*, not a loop; forbid `trio-asyncio` here (§4.1) |
|
||||
| netns teardown strands actor teardown | idempotent/tolerant teardown mirroring `_uds.close_listener()` |
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
'''
|
||||
Canonical tagged-address decoding and legacy input compatibility.
|
||||
|
||||
'''
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tractor.discovery._addr import wrap_address
|
||||
from tractor.ipc._tcp import TCPAddress
|
||||
from tractor.ipc._uds import UDSAddress
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value',
|
||||
[
|
||||
('tcp', '127.0.0.1', 1616),
|
||||
['tcp', '127.0.0.1', 1616],
|
||||
],
|
||||
)
|
||||
def test_decode_tagged_tcp_address(value):
|
||||
'''
|
||||
Shape-only decoding cannot distinguish future transport address
|
||||
forms. Feed canonical tuple and msgpack-style list values through
|
||||
the compatibility boundary and prove the explicit `tcp` tag
|
||||
selects the TCP backend and emits the canonical tagged form.
|
||||
|
||||
'''
|
||||
addr = wrap_address(value)
|
||||
|
||||
assert type(addr) is TCPAddress
|
||||
assert addr.unwrap() == ('tcp', '127.0.0.1', 1616)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'tag',
|
||||
['unix', 'uds'],
|
||||
)
|
||||
@pytest.mark.parametrize('container', [tuple, list])
|
||||
def test_decode_tagged_unix_address(
|
||||
tag: str,
|
||||
container: type,
|
||||
):
|
||||
'''
|
||||
Multiaddr calls the protocol `unix` while tractor's transport key
|
||||
remains `uds`. Decode both spellings from tuple/list containers,
|
||||
normalize them to one `UDSAddress`, and emit the canonical `unix`
|
||||
spelling.
|
||||
|
||||
'''
|
||||
value = container((tag, '/tmp/tractor/registry.sock'))
|
||||
addr = wrap_address(value)
|
||||
|
||||
assert type(addr) is UDSAddress
|
||||
assert addr.sockpath == Path('/tmp/tractor/registry.sock')
|
||||
assert addr.unwrap() == (
|
||||
'unix',
|
||||
'/tmp/tractor/registry.sock',
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value, expected_type',
|
||||
[
|
||||
(('127.0.0.1', 1616), TCPAddress),
|
||||
(['127.0.0.1', 1616], TCPAddress),
|
||||
(('/tmp/tractor', 'registry.sock'), UDSAddress),
|
||||
(['/tmp/tractor', 'registry.sock'], UDSAddress),
|
||||
],
|
||||
)
|
||||
def test_decode_legacy_address_forms(
|
||||
value,
|
||||
expected_type: type,
|
||||
):
|
||||
'''
|
||||
Existing callers, config, and older msgpack payloads still
|
||||
provide untagged pairs. Keep tuple/list forms readable while
|
||||
canonical tagged emission is introduced, proving the writer
|
||||
migration does not break shipped input behavior.
|
||||
|
||||
'''
|
||||
addr = wrap_address(value)
|
||||
|
||||
assert type(addr) is expected_type
|
||||
assert addr.unwrap()[0] in {'tcp', 'unix'}
|
||||
|
||||
|
||||
def test_tcp_from_native_ipv6_sockname():
|
||||
'''
|
||||
`socket.getsockname()` returns a four-item IPv6 sockaddr which is
|
||||
neither a wire form nor a legacy two-item pair. Preserve it as an
|
||||
OS compatibility boundary and intentionally ignore unsupported
|
||||
flow-info/scope-id fields when constructing `TCPAddress`.
|
||||
|
||||
'''
|
||||
addr = TCPAddress.from_addr(
|
||||
('::1', 1616, 0, 0)
|
||||
)
|
||||
|
||||
assert addr.unwrap() == ('tcp', '::1', 1616)
|
||||
|
|
@ -188,7 +188,7 @@ def test_parse_maddr_tcp_ipv4():
|
|||
result = parse_maddr('/ip4/127.0.0.1/tcp/1234')
|
||||
|
||||
assert isinstance(result, TCPAddress)
|
||||
assert result.unwrap() == ('tcp', '127.0.0.1', 1234)
|
||||
assert result.unwrap() == ('127.0.0.1', 1234)
|
||||
|
||||
|
||||
def test_parse_maddr_tcp_ipv6():
|
||||
|
|
@ -200,7 +200,7 @@ def test_parse_maddr_tcp_ipv6():
|
|||
result = parse_maddr('/ip6/::1/tcp/5678')
|
||||
|
||||
assert isinstance(result, TCPAddress)
|
||||
assert result.unwrap() == ('tcp', '::1', 5678)
|
||||
assert result.unwrap() == ('::1', 5678)
|
||||
|
||||
|
||||
def test_parse_maddr_uds():
|
||||
|
|
@ -213,10 +213,9 @@ def test_parse_maddr_uds():
|
|||
result = parse_maddr('/unix/tmp/tractor_test/test.sock')
|
||||
|
||||
assert isinstance(result, UDSAddress)
|
||||
assert result.unwrap() == (
|
||||
'unix',
|
||||
'/tmp/tractor_test/test.sock',
|
||||
)
|
||||
filedir, filename = result.unwrap()
|
||||
assert filename == 'test.sock'
|
||||
assert str(filedir) == '/tmp/tractor_test'
|
||||
|
||||
|
||||
def test_parse_maddr_unsupported():
|
||||
|
|
@ -250,7 +249,7 @@ def test_parse_wg_maddr():
|
|||
bearer=('192.168.1.50', 51820),
|
||||
)
|
||||
assert isinstance(parsed.overlay, TCPAddress)
|
||||
assert parsed.overlay.unwrap() == ('tcp', '10.0.11.1', 1616)
|
||||
assert parsed.overlay.unwrap() == ('10.0.11.1', 1616)
|
||||
|
||||
|
||||
def test_mk_wg_maddr_roundtrip():
|
||||
|
|
@ -446,7 +445,7 @@ def test_wrap_address_maddr_str():
|
|||
result = wrap_address('/ip4/127.0.0.1/tcp/9999')
|
||||
|
||||
assert isinstance(result, TCPAddress)
|
||||
assert result.unwrap() == ('tcp', '127.0.0.1', 9999)
|
||||
assert result.unwrap() == ('127.0.0.1', 9999)
|
||||
|
||||
|
||||
def test_wrap_address_wg_maddr_str():
|
||||
|
|
@ -461,7 +460,7 @@ def test_wrap_address_wg_maddr_str():
|
|||
|
||||
assert isinstance(result, TunnelledAddress)
|
||||
assert result.tunnel.peer_pubkey == _WG_PUBKEY
|
||||
assert result.overlay.unwrap() == ('tcp', '10.0.11.1', 1616)
|
||||
assert result.overlay.unwrap() == ('10.0.11.1', 1616)
|
||||
|
||||
|
||||
# ------ parse_endpoints() tests ------
|
||||
|
|
@ -482,11 +481,11 @@ def test_parse_endpoints_tcp_only():
|
|||
|
||||
reg_addr = result['registry'][0]
|
||||
assert isinstance(reg_addr, TCPAddress)
|
||||
assert reg_addr.unwrap() == ('tcp', '127.0.0.1', 1616)
|
||||
assert reg_addr.unwrap() == ('127.0.0.1', 1616)
|
||||
|
||||
feed_addr = result['data_feed'][0]
|
||||
assert isinstance(feed_addr, TCPAddress)
|
||||
assert feed_addr.unwrap() == ('tcp', '0.0.0.0', 5555)
|
||||
assert feed_addr.unwrap() == ('0.0.0.0', 5555)
|
||||
|
||||
|
||||
def test_parse_endpoints_mixed_tpts():
|
||||
|
|
@ -506,13 +505,12 @@ def test_parse_endpoints_mixed_tpts():
|
|||
|
||||
assert len(addrs) == 2
|
||||
assert isinstance(addrs[0], TCPAddress)
|
||||
assert addrs[0].unwrap() == ('tcp', '127.0.0.1', 4040)
|
||||
assert addrs[0].unwrap() == ('127.0.0.1', 4040)
|
||||
|
||||
assert isinstance(addrs[1], UDSAddress)
|
||||
assert addrs[1].unwrap() == (
|
||||
'unix',
|
||||
'/tmp/tractor/broker.sock',
|
||||
)
|
||||
filedir, filename = addrs[1].unwrap()
|
||||
assert filename == 'broker.sock'
|
||||
assert str(filedir) == '/tmp/tractor'
|
||||
|
||||
|
||||
def test_parse_endpoints_wg_maddr():
|
||||
|
|
@ -552,7 +550,7 @@ def test_parse_endpoints_unwrapped_tuples():
|
|||
|
||||
addr = result['ems'][0]
|
||||
assert isinstance(addr, TCPAddress)
|
||||
assert addr.unwrap() == ('tcp', '127.0.0.1', 6666)
|
||||
assert addr.unwrap() == ('127.0.0.1', 6666)
|
||||
|
||||
|
||||
def test_parse_endpoints_mixed_str_and_tuple():
|
||||
|
|
@ -572,10 +570,10 @@ def test_parse_endpoints_mixed_str_and_tuple():
|
|||
|
||||
assert len(addrs) == 2
|
||||
assert isinstance(addrs[0], TCPAddress)
|
||||
assert addrs[0].unwrap() == ('tcp', '127.0.0.1', 7777)
|
||||
assert addrs[0].unwrap() == ('127.0.0.1', 7777)
|
||||
|
||||
assert isinstance(addrs[1], TCPAddress)
|
||||
assert addrs[1].unwrap() == ('tcp', '127.0.0.1', 8888)
|
||||
assert addrs[1].unwrap() == ('127.0.0.1', 8888)
|
||||
|
||||
|
||||
def test_parse_endpoints_unsupported_proto():
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ def test_non_registrar_root_tpt_bind_addrs(
|
|||
for uw_addr in bound:
|
||||
w = wrap_address(uw_addr)
|
||||
if w.proto_key == 'tcp':
|
||||
_, _host, port = uw_addr
|
||||
_host, port = uw_addr
|
||||
assert port > 0
|
||||
|
||||
trio.run(_main)
|
||||
|
|
@ -255,7 +255,7 @@ def test_tpt_bind_addrs_as_maddr_str(
|
|||
for uw_addr in actor.accept_addrs:
|
||||
w = wrap_address(uw_addr)
|
||||
if w.proto_key == 'tcp':
|
||||
_, _host, port = uw_addr
|
||||
_host, port = uw_addr
|
||||
assert port > 0
|
||||
|
||||
trio.run(_main)
|
||||
|
|
@ -287,7 +287,7 @@ def test_registrar_merge_binds_union(
|
|||
# actually differ (always true for TCP, may
|
||||
# collide for UDS).
|
||||
expect_disjoint: bool = (
|
||||
reg_wrapped.unwrap() != rando.unwrap()
|
||||
tuple(reg_addr) != rando.unwrap()
|
||||
)
|
||||
|
||||
async def _main():
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
'''
|
||||
Tunnel annotation peeling at the outbound IPC transport boundary.
|
||||
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
import trio
|
||||
|
||||
from tractor.discovery import (
|
||||
TunnelledAddress,
|
||||
WGTunnelSpec,
|
||||
tunnels_of,
|
||||
)
|
||||
from tractor.ipc import _chan
|
||||
from tractor.ipc._tcp import TCPAddress
|
||||
|
||||
|
||||
_PUBKEY: str = 'g3x7z0AdV1rM6UQU22CC7IL3/ivn4DzrE7ikDhCZ/Dc='
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def overlay() -> TCPAddress:
|
||||
return TCPAddress('127.0.0.1', 0)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tunnelled(
|
||||
overlay: TCPAddress,
|
||||
) -> TunnelledAddress:
|
||||
return TunnelledAddress(
|
||||
overlay=overlay,
|
||||
tunnel=WGTunnelSpec(
|
||||
peer_pubkey=_PUBKEY,
|
||||
bearer=('192.168.1.50', 51820),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('use_tunnel', [False, True])
|
||||
def test_channel_peels_before_transport_dispatch(
|
||||
monkeypatch,
|
||||
overlay: TCPAddress,
|
||||
tunnelled: TunnelledAddress,
|
||||
use_tunnel: bool,
|
||||
):
|
||||
'''
|
||||
Exact-type transport lookup cannot dispatch a `TunnelledAddress`,
|
||||
and passing one onward would make TCP dial the wrong object. Feed
|
||||
both a plain overlay and its annotated wrapper into
|
||||
`Channel.from_addr()`, capture lookup and connect arguments, and
|
||||
prove both transport operations receive only the same bindable
|
||||
TCP address while the caller's tunnel metadata remains intact.
|
||||
|
||||
'''
|
||||
seen: list[tuple[str, TCPAddress]] = []
|
||||
|
||||
class FakeTransport:
|
||||
@classmethod
|
||||
async def connect_to(
|
||||
cls,
|
||||
addr: TCPAddress,
|
||||
**kwargs,
|
||||
) -> FakeTransport:
|
||||
seen.append(('connect', addr))
|
||||
return cls()
|
||||
|
||||
def fake_transport_from_addr(
|
||||
addr: TCPAddress,
|
||||
) -> type[FakeTransport]:
|
||||
seen.append(('lookup', addr))
|
||||
return FakeTransport
|
||||
|
||||
monkeypatch.setattr(
|
||||
_chan,
|
||||
'transport_from_addr',
|
||||
fake_transport_from_addr,
|
||||
)
|
||||
|
||||
async def main() -> None:
|
||||
declared = tunnelled if use_tunnel else overlay
|
||||
chan = await _chan.Channel.from_addr(declared)
|
||||
assert isinstance(chan.transport, FakeTransport)
|
||||
|
||||
trio.run(main)
|
||||
|
||||
assert seen == [
|
||||
('lookup', overlay),
|
||||
('connect', overlay),
|
||||
]
|
||||
assert tunnels_of(tunnelled) == (tunnelled.tunnel,)
|
||||
|
|
@ -57,20 +57,17 @@ def test_uds_bindspace_created_implicitly(
|
|||
|
||||
root: Actor = tractor.current_actor()
|
||||
assert root.is_registrar
|
||||
canonical_addr = _addr.wrap_address(
|
||||
registry_addr,
|
||||
).unwrap()
|
||||
|
||||
assert canonical_addr in root.reg_addrs
|
||||
assert registry_addr in root.reg_addrs
|
||||
assert (
|
||||
canonical_addr
|
||||
registry_addr
|
||||
in
|
||||
_state._runtime_vars['_registry_addrs']
|
||||
)
|
||||
assert (
|
||||
canonical_addr
|
||||
_addr.wrap_address(registry_addr)
|
||||
in
|
||||
[addr.unwrap() for addr in root.registry_addrs]
|
||||
root.registry_addrs
|
||||
)
|
||||
|
||||
trio.run(main)
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
'''
|
||||
Tunnel annotation peeling at the inbound IPC transport boundary.
|
||||
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import trio
|
||||
|
||||
from tractor.discovery import (
|
||||
TunnelledAddress,
|
||||
WGTunnelSpec,
|
||||
tunnels_of,
|
||||
)
|
||||
from tractor.ipc._server import open_ipc_server
|
||||
from tractor.ipc._tcp import TCPAddress
|
||||
|
||||
|
||||
_PUBKEY: str = 'g3x7z0AdV1rM6UQU22CC7IL3/ivn4DzrE7ikDhCZ/Dc='
|
||||
|
||||
|
||||
def test_server_peels_before_endpoint_construction():
|
||||
'''
|
||||
`Endpoint.start_listener()` reflects on its address's declaring
|
||||
module, so retaining a tunnel wrapper there selects `._tunnel`
|
||||
instead of the TCP backend. Start a real listener from the
|
||||
wrapper, assert the resulting `Endpoint` contains only a resolved
|
||||
`TCPAddress`, and prove the original declaration still carries
|
||||
its tunnel spec for the future bindspace lifecycle.
|
||||
|
||||
'''
|
||||
overlay = TCPAddress('127.0.0.1', 0)
|
||||
tunnelled = TunnelledAddress(
|
||||
overlay=overlay,
|
||||
tunnel=WGTunnelSpec(
|
||||
peer_pubkey=_PUBKEY,
|
||||
bearer=('192.168.1.50', 51820),
|
||||
),
|
||||
)
|
||||
|
||||
async def main() -> None:
|
||||
async with open_ipc_server() as server:
|
||||
eps = await server.listen_on(
|
||||
accept_addrs=[tunnelled],
|
||||
)
|
||||
assert len(eps) == 1
|
||||
endpoint = eps[0]
|
||||
|
||||
assert type(endpoint.addr) is TCPAddress
|
||||
_, host, port = endpoint.addr.unwrap()
|
||||
assert host == overlay.unwrap()[1]
|
||||
assert port > 0
|
||||
assert endpoint.addr is not tunnelled
|
||||
assert tunnels_of(tunnelled) == (
|
||||
tunnelled.tunnel,
|
||||
)
|
||||
|
||||
server.cancel()
|
||||
|
||||
trio.run(main)
|
||||
|
|
@ -8,7 +8,6 @@ import trio
|
|||
import tractor
|
||||
|
||||
from tractor._testing import tractor_test
|
||||
from tractor.discovery._addr import wrap_address
|
||||
|
||||
|
||||
def test_no_runtime():
|
||||
|
|
@ -49,7 +48,7 @@ async def test_self_is_registered_localportal(reg_addr):
|
|||
with trio.fail_after(0.2):
|
||||
sockaddr = await portal.run_from_ns(
|
||||
'self', 'wait_for_actor', name='root')
|
||||
assert sockaddr[0] == wrap_address(reg_addr).unwrap()
|
||||
assert sockaddr[0] == reg_addr
|
||||
|
||||
|
||||
def test_local_actor_async_func(reg_addr):
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ from .discovery._addr import (
|
|||
mk_uuid,
|
||||
wrap_address,
|
||||
)
|
||||
from .discovery._tunnel import strip_tunnels
|
||||
from .trionics import (
|
||||
is_multi_cancelled,
|
||||
collapse_eg,
|
||||
|
|
@ -535,7 +534,6 @@ async def open_root_actor(
|
|||
# proto if not already provided.
|
||||
if not tpt_bind_addrs:
|
||||
for addr in ponged_addrs:
|
||||
bindable_addr: Address = strip_tunnels(addr)
|
||||
tpt_bind_addrs.append(
|
||||
# XXX, these are `Address` NOT `UnwrappedAddress`.
|
||||
#
|
||||
|
|
@ -543,8 +541,8 @@ async def open_root_actor(
|
|||
# protos we allocate port=0 such that the system
|
||||
# allocates a random value at bind time; this
|
||||
# happens in the `.ipc.*` stack's backend.
|
||||
bindable_addr.get_random(
|
||||
bindspace=bindable_addr.bindspace,
|
||||
addr.get_random(
|
||||
bindspace=addr.bindspace,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,9 +18,7 @@ from uuid import uuid4
|
|||
from typing import (
|
||||
Protocol,
|
||||
ClassVar,
|
||||
Literal,
|
||||
Type,
|
||||
TypeAlias,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
|
|
@ -66,44 +64,25 @@ log = get_logger()
|
|||
# seems like the right name as per,
|
||||
# https://www.geeksforgeeks.org/introduction-to-address-descriptor/
|
||||
#
|
||||
TaggedTCPAddress: TypeAlias = tuple[
|
||||
Literal['tcp'],
|
||||
str,
|
||||
int,
|
||||
]
|
||||
TaggedUnixAddress: TypeAlias = tuple[
|
||||
Literal['unix'],
|
||||
str,
|
||||
]
|
||||
TaggedUDSAlias: TypeAlias = tuple[
|
||||
Literal['uds'],
|
||||
str,
|
||||
]
|
||||
TaggedAddress: TypeAlias = (
|
||||
TaggedTCPAddress
|
||||
|TaggedUnixAddress
|
||||
UnwrappedAddress = (
|
||||
# tcp/udp/uds
|
||||
tuple[
|
||||
str, # host/domain(tcp), filesys-dir(uds)
|
||||
int|str, # port/path(uds)
|
||||
]
|
||||
# ?TODO? should we also include another 2 fields from
|
||||
# our `Aid` msg such that we include the runtime `Actor.uid`
|
||||
# of `.name` and `.uuid`?
|
||||
# - would ensure uniqueness across entire net?
|
||||
# - allows for easier runtime-level filtering of "actors by
|
||||
# service name"
|
||||
)
|
||||
|
||||
# Input-only compatibility forms retained for older callers and
|
||||
# serialized payloads.
|
||||
LegacyTCPAddress: TypeAlias = tuple[str, int]
|
||||
LegacyUDSAddress: TypeAlias = tuple[str, str]
|
||||
LegacyUnwrappedAddress: TypeAlias = (
|
||||
LegacyTCPAddress
|
||||
|LegacyUDSAddress
|
||||
)
|
||||
UnwrappedAddress = TaggedAddress
|
||||
# ?TODO? should we also include another 2 fields from our `Aid` msg
|
||||
# such that we include the runtime `Actor.uid` of `.name` and `.uuid`?
|
||||
# - would ensure uniqueness across entire net?
|
||||
# - allows for easier runtime-level filtering of "actors by service
|
||||
# name"
|
||||
|
||||
|
||||
# TODO, maybe rename to `SocketAddress`?
|
||||
class Address(Protocol):
|
||||
proto_key: ClassVar[str]
|
||||
unwrapped_type: ClassVar[type]
|
||||
unwrapped_type: ClassVar[UnwrappedAddress]
|
||||
|
||||
# TODO, i feel like an `.is_bound()` is a better thing to
|
||||
# support?
|
||||
|
|
@ -237,15 +216,7 @@ def mk_uuid() -> str:
|
|||
|
||||
|
||||
def wrap_address(
|
||||
addr: (
|
||||
TaggedAddress
|
||||
|TaggedUDSAlias
|
||||
|LegacyUnwrappedAddress
|
||||
|list[str|int]
|
||||
|str
|
||||
|Address
|
||||
|TunnelledAddress
|
||||
),
|
||||
addr: UnwrappedAddress|str|Address|TunnelledAddress,
|
||||
) -> Address|TunnelledAddress:
|
||||
'''
|
||||
Wrap an `UnwrappedAddress` as an `Address`-type based
|
||||
|
|
@ -268,20 +239,6 @@ def wrap_address(
|
|||
# import pdbp; pdbp.set_trace()
|
||||
match addr:
|
||||
|
||||
case (
|
||||
('tcp', str(), int())
|
||||
|
|
||||
['tcp', str(), int()]
|
||||
):
|
||||
return TCPAddress.from_addr(addr)
|
||||
|
||||
case (
|
||||
(('unix' | 'uds'), str())
|
||||
|
|
||||
[('unix' | 'uds'), str()]
|
||||
):
|
||||
return UDSAddress.from_addr(addr)
|
||||
|
||||
# classic network socket-address as tuple/list
|
||||
case (
|
||||
(str(), int())
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ def mk_maddr(
|
|||
|
||||
match proto_key:
|
||||
case 'tcp':
|
||||
_, host, port = addr.unwrap()
|
||||
host, port = addr.unwrap()
|
||||
ip = ipaddress.ip_address(host)
|
||||
net_proto: str = (
|
||||
'ip4' if ip.version == 4
|
||||
|
|
@ -84,12 +84,13 @@ def mk_maddr(
|
|||
)
|
||||
|
||||
case 'uds':
|
||||
_, sockpath = addr.unwrap()
|
||||
filedir, filename = addr.unwrap()
|
||||
filepath = Path(filedir) / filename
|
||||
# NOTE, strip any leading `/` to avoid
|
||||
# double-slash `/unix//run/..` which the
|
||||
# multiaddr parser rejects as "empty
|
||||
# protocol path".
|
||||
fpath_str: str = sockpath.lstrip('/')
|
||||
fpath_str: str = str(filepath).lstrip('/')
|
||||
return Multiaddr(
|
||||
f'/{maddr_proto}/{fpath_str}'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -45,10 +45,6 @@ from tractor.discovery._addr import (
|
|||
Address,
|
||||
UnwrappedAddress,
|
||||
)
|
||||
from tractor.discovery._tunnel import (
|
||||
TunnelledAddress,
|
||||
strip_tunnels,
|
||||
)
|
||||
from tractor.log import get_logger
|
||||
from tractor._exceptions import (
|
||||
MsgTypeError,
|
||||
|
|
@ -185,17 +181,16 @@ class Channel:
|
|||
@classmethod
|
||||
async def from_addr(
|
||||
cls,
|
||||
addr: UnwrappedAddress|Address|TunnelledAddress,
|
||||
addr: UnwrappedAddress,
|
||||
**kwargs
|
||||
) -> Channel:
|
||||
|
||||
if not is_wrapped_addr(addr):
|
||||
addr = wrap_address(addr)
|
||||
addr: Address = wrap_address(addr)
|
||||
|
||||
transport_addr: Address = strip_tunnels(addr)
|
||||
transport_cls = transport_from_addr(transport_addr)
|
||||
transport_cls = transport_from_addr(addr)
|
||||
transport = await transport_cls.connect_to(
|
||||
transport_addr,
|
||||
addr,
|
||||
**kwargs,
|
||||
)
|
||||
# XXX, for UDS *no!* since we recv the peer-pid and build out
|
||||
|
|
@ -523,7 +518,7 @@ class Channel:
|
|||
|
||||
@acm
|
||||
async def _connect_chan(
|
||||
addr: UnwrappedAddress|Address|TunnelledAddress,
|
||||
addr: UnwrappedAddress
|
||||
) -> typing.AsyncGenerator[Channel, None]:
|
||||
'''
|
||||
Create and connect a `Channel` to the provided `addr`, disconnect
|
||||
|
|
|
|||
|
|
@ -59,17 +59,13 @@ from ..msg import (
|
|||
from ..trionics import maybe_open_nursery
|
||||
from ..runtime import _state
|
||||
from .. import log
|
||||
from ..discovery._addr import (
|
||||
Address,
|
||||
UnwrappedAddress,
|
||||
)
|
||||
from ..discovery._addr import Address
|
||||
from ._chan import Channel
|
||||
from ._transport import MsgTransport
|
||||
from ._uds import UDSAddress
|
||||
from ._tcp import TCPAddress
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..discovery._tunnel import TunnelledAddress
|
||||
from ..runtime._runtime import Actor
|
||||
from ..runtime._supervise import ActorNursery
|
||||
|
||||
|
|
@ -963,9 +959,7 @@ class Server(Struct):
|
|||
async def listen_on(
|
||||
self,
|
||||
*,
|
||||
accept_addrs: list[
|
||||
UnwrappedAddress|Address|TunnelledAddress
|
||||
]|None = None,
|
||||
accept_addrs: list[tuple[str, int|str]]|None = None,
|
||||
stream_handler_nursery: Nursery|None = None,
|
||||
) -> list[Endpoint]:
|
||||
'''
|
||||
|
|
@ -1048,7 +1042,7 @@ async def _serve_ipc_eps(
|
|||
*,
|
||||
server: IPCServer,
|
||||
stream_handler_tn: Nursery,
|
||||
listen_addrs: list[Address|TunnelledAddress],
|
||||
listen_addrs: list[tuple[str, int|str]],
|
||||
|
||||
task_status: TaskStatus[
|
||||
Nursery,
|
||||
|
|
@ -1064,8 +1058,6 @@ async def _serve_ipc_eps(
|
|||
`.cancel_server()` is called.
|
||||
|
||||
'''
|
||||
from ..discovery._tunnel import strip_tunnels
|
||||
|
||||
try:
|
||||
listen_tn: Nursery
|
||||
async with trio.open_nursery() as listen_tn:
|
||||
|
|
@ -1074,8 +1066,7 @@ async def _serve_ipc_eps(
|
|||
# XXX NOTE, required to call `serve_listeners()` below.
|
||||
# ?TODO, maybe just pass `list(eps.values()` tho?
|
||||
listeners: list[trio.abc.Listener] = []
|
||||
for declared_addr in listen_addrs:
|
||||
addr: Address = strip_tunnels(declared_addr)
|
||||
for addr in listen_addrs:
|
||||
ep = Endpoint(
|
||||
addr=addr,
|
||||
listen_tn=listen_tn,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ from __future__ import annotations
|
|||
import ipaddress
|
||||
from typing import (
|
||||
ClassVar,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
# from contextlib import (
|
||||
# asynccontextmanager as acm,
|
||||
|
|
@ -43,9 +42,6 @@ from tractor.ipc._transport import (
|
|||
MsgpackTransport,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tractor.discovery._addr import TaggedTCPAddress
|
||||
|
||||
|
||||
log = get_logger()
|
||||
|
||||
|
|
@ -66,7 +62,7 @@ class TCPAddress(
|
|||
) from valerr
|
||||
|
||||
proto_key: ClassVar[str] = 'tcp'
|
||||
unwrapped_type: ClassVar[type] = tuple
|
||||
unwrapped_type: ClassVar[type] = tuple[str, int]
|
||||
def_bindspace: ClassVar[str] = '127.0.0.1'
|
||||
|
||||
# ?TODO, actually validate ipv4/6 with stdlib's `ipaddress`
|
||||
|
|
@ -108,35 +104,19 @@ class TCPAddress(
|
|||
@classmethod
|
||||
def from_addr(
|
||||
cls,
|
||||
addr: tuple|list,
|
||||
addr: tuple[str, int]
|
||||
) -> TCPAddress:
|
||||
match addr:
|
||||
case (
|
||||
('tcp', str() as host, int() as port)
|
||||
|
|
||||
['tcp', str() as host, int() as port]
|
||||
|
|
||||
(str() as host, int() as port)
|
||||
|
|
||||
[str() as host, int() as port]
|
||||
|
|
||||
(
|
||||
str() as host,
|
||||
int() as port,
|
||||
int(),
|
||||
int(),
|
||||
)
|
||||
):
|
||||
return TCPAddress(host, port)
|
||||
case (str(), int()):
|
||||
return TCPAddress(addr[0], addr[1])
|
||||
case _:
|
||||
raise ValueError(
|
||||
f'Invalid unwrapped address for {cls}\n'
|
||||
f'{addr}\n'
|
||||
)
|
||||
|
||||
def unwrap(self) -> TaggedTCPAddress:
|
||||
def unwrap(self) -> tuple[str, int]:
|
||||
return (
|
||||
self.proto_key,
|
||||
self._host,
|
||||
self._port,
|
||||
)
|
||||
|
|
@ -235,8 +215,7 @@ class MsgpackTCPStream(MsgpackTransport):
|
|||
**kwargs
|
||||
) -> MsgpackTCPStream:
|
||||
stream = await trio.open_tcp_stream(
|
||||
destaddr._host,
|
||||
destaddr._port,
|
||||
*destaddr.unwrap(),
|
||||
**kwargs
|
||||
)
|
||||
return MsgpackTCPStream(
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ from tractor.runtime._state import (
|
|||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tractor.discovery._addr import TaggedUnixAddress
|
||||
from tractor.runtime._runtime import Actor
|
||||
|
||||
|
||||
|
|
@ -115,7 +114,7 @@ class UDSAddress(
|
|||
# -[ ] need to check what other mult-transport frameworks do
|
||||
# like zmq, nng, uri-spec et al!
|
||||
proto_key: ClassVar[str] = 'uds'
|
||||
unwrapped_type: ClassVar[type] = tuple
|
||||
unwrapped_type: ClassVar[type] = tuple[str, int]
|
||||
def_bindspace: ClassVar[Path] = get_rt_dir()
|
||||
|
||||
@property
|
||||
|
|
@ -133,7 +132,7 @@ class UDSAddress(
|
|||
|
||||
@property
|
||||
def sockpath(self) -> Path:
|
||||
return Path(self.bindspace) / self.filename
|
||||
return self.bindspace / self.filename
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
|
|
@ -147,26 +146,16 @@ class UDSAddress(
|
|||
def from_addr(
|
||||
cls,
|
||||
addr: (
|
||||
tuple|list|Path|str
|
||||
tuple[Path|str, Path|str]|Path|str
|
||||
),
|
||||
) -> UDSAddress:
|
||||
match addr:
|
||||
case (
|
||||
(('unix' | 'uds'), str()|Path() as sockpath)
|
||||
|
|
||||
[('unix' | 'uds'), str()|Path() as sockpath]
|
||||
):
|
||||
path = Path(sockpath)
|
||||
return UDSAddress(*unwrap_sockpath(path))
|
||||
|
||||
case (
|
||||
(str()|Path() as filedir, str()|Path() as filename)
|
||||
|
|
||||
[str()|Path() as filedir, str()|Path() as filename]
|
||||
):
|
||||
case tuple()|list():
|
||||
filedir = Path(addr[0])
|
||||
filename = Path(addr[1])
|
||||
return UDSAddress(
|
||||
filedir=Path(filedir),
|
||||
filename=Path(filename),
|
||||
filedir=filedir,
|
||||
filename=filename,
|
||||
# maybe_pid=pid,
|
||||
)
|
||||
# NOTE, in case we ever decide to just `.unwrap()`
|
||||
|
|
@ -181,10 +170,12 @@ class UDSAddress(
|
|||
f'{addr!r}\n'
|
||||
)
|
||||
|
||||
def unwrap(self) -> TaggedUnixAddress:
|
||||
def unwrap(self) -> tuple[str, int]:
|
||||
# XXX NOTE, since this gets passed DIRECTLY to
|
||||
# `.ipc._uds.open_unix_socket_w_passcred()`
|
||||
return (
|
||||
'unix',
|
||||
str(self.sockpath),
|
||||
str(self.filedir),
|
||||
str(self.filename),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -211,11 +211,10 @@ class SpawnSpec(
|
|||
# module import capability
|
||||
enable_modules: dict[str, str]
|
||||
|
||||
# Tagged addresses have protocol-specific tuple shapes which
|
||||
# msgspec cannot express as one decodable union. `wrap_address()`
|
||||
# validates each tuple at the transport boundary.
|
||||
reg_addrs: list[tuple]
|
||||
bind_addrs: list[tuple]|None
|
||||
# TODO: not just sockaddr pairs?
|
||||
# -[ ] abstract into a `TransportAddr` type?
|
||||
reg_addrs: list[tuple[str, str|int]]
|
||||
bind_addrs: list[tuple[str, str|int]]|None
|
||||
|
||||
|
||||
# TODO: caps based RPC support in the payload?
|
||||
|
|
|
|||
Loading…
Reference in New Issue