Compare commits

..

9 Commits

Author SHA1 Message Date
Gud Boi a2e6c10118 Add the interim `/tipc/` maddr grammar
`mk_maddr()`/`parse_maddr()` learn,

    /tipc/<stype>/<instance>/<scope>

mirroring how `uds` maps onto the spec-legal `/unix`.

XXX `str`-ONLY for now: there is no registered `/tipc` proto
in the multiaddr table (upstream track gh #483 +
multiformats/py-multiaddr#107) and `Multiaddr()` rejects an
unregistered name outright. `MsgTransport.maddr`s return type
is already `Multiaddr|str` (and `MsgpackUDSStream` already
exercises the `str` branch), so this fits — but it IS why gh

`parse_maddr()` therefore special-cases the `/tipc/` prefix
BEFORE handing anything to `Multiaddr()`.

Also drive the maddr mapping-table tests off `_address_types`
instead of a hardcoded len/dict so the next backend can't
fail them for the wrong reason.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-14 09:55:01 -04:00
Gud Boi 95360fcdf1 Register `tipc` in the tpt tables + test harness
Wire the backend through every registration site (contract §2)
so `--tpt-proto tipc` is a first-class suite mode,
- `_state.TransportProtocolKey` gains the key
- `_addr._address_types` + `._default_lo_addrs`
- `_addr.wrap_address()` gets a `case ('tipc', *_)`; being a
  4-elem seq it can't collide w/ `tcp`s or `uds`s 2-tuple
  cases, so NO ordering hazard (and a bare seq-pattern matches
  the `list` form `msgpack` decodes to).
- `_types`: the `Address` union, `_msg_transports`,
  `_key_to_transport`, `_addr_to_transport` and the
  `transport_from_stream()` family match. That last one keys
  off `._tipc.AF_TIPC` (which carries the uapi fallback) NOT
  `socket.AF_TIPC` which is linux-only.

Test-harness side,
- `get_rando_addr()` gains a `tipc` branch; `.get_random()`
  already salts w/ `uuid4`+pid so both within- and cross-proc
  isolation come for free.
- the `tpt_protos` fixture calls an addr-type's optional
  `.is_available()` and `pytest.fail()`s w/ its reason. Keeps
  a module-less box from turning `--tpt-proto tipc` into a few
  hundred confusing connect-timeouts. Generic on purpose —
  plans 02/03 need the same hook.
- the discovery `daemon` fixture's readiness probe learns to
  dial a TIPC service name (it previously assumed tcp-or-uds
  and blew up on the 4-tuple).

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-14 09:52:42 -04:00
Gud Boi e9cc5f4a7b Widen `UnwrappedAddress` to admit proto-keyed addrs
`SpawnSpec.reg_addrs`/`.bind_addrs` pinned the wire shape to
a 2-tuple, so a `tipc` addr (`('tipc', stype, inst, scope)`)
died at the child w/ `msgspec.ValidationError: Expected array
of length 2, got 4` -> `invalid SpawnSpec IPC msg`.

Point those fields at `UnwrappedAddress` (which `SpawnSpec`s
own TODO already asked for) and widen the alias.

XXX VARIADIC (`tuple[str|int, ...]`) rather than a union of
the two concrete shapes, bc `msgspec` refuses a union holding
more than one array-like type.

?TODO, the real fix is the full proto-key migration (contract
§1.1) after which this becomes a tagged union keyed off elem
0 and per-proto validation comes back.

Note the alias is declared TWICE — `.msg.types` re-declares it
to dodge a circular import (`._addr` -> `.ipc._tcp` -> `.msg`)
and *that* copy is what actually validates the wire msg.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-14 09:52:33 -04:00
Gud Boi 5f0334c6c3 Add `MsgpackTIPCStream`, the `AF_TIPC` `MsgTransport`
Wire `.connect_to()` (dial by service name), `.connected()`
and `.get_stream_addrs()` on top of `MsgpackTransport` so
`trio.SocketStream` + the existing `<I`-prefix framing carry
`msgpack` msgs over TIPC unchanged.

XXX both ends of a connected TIPC sock answer `TIPC_ADDR_ID`
port-ids and a port-id carries NO service name, so,
- the *dialling* side re-asserts the name it actually dialled
  over `._raddr` (same move as `MsgpackUDSStream`s peer-pid
  re-assign),
- the *accepting* side keeps a `TIPC_NAME_UNKNOWN` sentinel
  plus the observed `(node, ref)`. It doesn't need more — the
  `Aid` from `._do_handshake()` already carries the peer's
  logical identity.

Also normalize dial failures: TIPC answers an unpublished-name
lookup with `EHOSTUNREACH`, which python maps to a **bare**
`OSError` and NOT a `ConnectionError` subtype the way
`ECONNREFUSED` maps to `ConnectionRefusedError`. The
discovery-ping path needs the `ConnectionError` shape, so the
`_reraise_as_connerr()` wrap is load-bearing, not polish.

XXX ALSO tolerate a dead peer in `.get_stream_addrs()`!
Unlike tcp/uds — where the kernel keeps answering the peer
addr until *we* close — TIPC answers `ENOTCONN` once the peer
is gone. Since `MsgpackTransport.__init__()` calls
`.get_stream_addrs()` (via `Channel.from_stream()`) BEFORE the
handshake, an unguarded `OSError` there escapes
`handle_stream_from_peer()`s handshake tolerance (contract §4)
and tears down the WHOLE actor. Any connect-then-drop peer — a
port scan, a liveness probe, a cancelled dial — was a remote
actor-kill. A dead peer must cost us an addr, not the runtime.

Deats,
- `TIPC_IMPORTANCE` exposed as a `.connect_to()` kwarg — TIPC
  can rank a conn's traffic under congestion, which no other
  backend can do. Defaulted to the kernel default for now;
  wiring the parent<->child chan to `HIGH` is a follow-up.
- `TIPC_DEST_DROPPABLE = 0` so undeliverable msgs surface as
  errors instead of being silently dropped.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-14 09:52:25 -04:00
Gud Boi 175712ae04 Add `TIPCAddress` + `start_listener()`, gh #378
First slice of the `AF_TIPC` tpt backend: the addr type, the
`is_tipc_available()` capability predicate and the
name-publishing listener. No `MsgTransport` yet.

An actor's TIPC addr is a *service name* `(stype, instance)`:
`.bind()`ing the singleton `TIPC_ADDR_NAMESEQ` range IS the
service registration (it shows up in `tipc nametable show`)
and a peer's `.connect()`-by-name IS the lookup — so the
kernel does discovery for us, no registrar hop.

Deats,
- `.unwrap()` is proto-keyed as `('tipc', stype, inst, scope)`
  using the `multiaddr` proto spelling so `wrap_address()`
  can't confuse it with `tcp`s or `uds`s 2-tuples.
- `.rebind_from_sockname = False` bc `getsockname()` answers
  a port-id; `.from_addr()` raises on a bare `TIPC_ADDR_ID`
  rather than fabricate an un-dialable addr.
- `.bindspace` is the TIPC *scope*, i.e. literally the set of
  hosts a published name is reachable from. `ZONE` scope is
  deprecated/aliased so fold it to `CLUSTER` on input.
- mod stays importable on non-linux (uapi-value fallbacks,
  the `_uds.SO_PASSCRED` precedent) bc `._addr` builds its
  registration tables at import time.

XXX a `.get_random()` clash does NOT raise `EADDRINUSE` —
TIPC accepts multiple publishers of one name and round-robins
connects between them (verified against a live kernel), so a
collision is *silent crosstalk*. Hence the `blake2b` digest
and its (birthday-bounded) collision test.

Also,
- a generic `.is_available() -> (ok, why_not)` classmethod;
  deliberately spelled generically (NOT `is_tipc_*`) so the
  sibling env-dependent backends — `quic`/`iroh` (gh #353)
  and the `wg` netns bindspace (gh #482) — get the same gate
  for free. Its consumer lands w/ the reg tables.
- register a `tipc` pytest mark; the kernel-touching cases
  self-skip unless `sudo modprobe tipc` has been run.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-14 09:52:17 -04:00
Gud Boi 2637e6bdc5 Add `Address.rebind_from_sockname` opt-out
Gate `Endpoint.start_listener()`s `getsockname()`-vs-`.addr`
reconciliation on a new per-addr-type `ClassVar[bool]`, set
`True` on both `TCPAddress` and `UDSAddress` so existing
behaviour is bit-for-bit unchanged.

That reconciliation exists ONLY to learn a kernel-assigned
port from a `port=0` tcp bind (its own comment says so). The
incoming `tipc` backend (gh #378) has no late-binding
analogue AND its `getsockname()` answers a `TIPC_ADDR_ID`
port-id rather than the name-seq it published — rebinding
from that would swap a dialable service name for an
un-dialable, un-reconstructable port id.

So opting out is semantically right rather than a hack.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-14 09:50:27 -04:00
Gud Boi 4817819f1c Pin `Endpoint` addr-reconciliation for tcp/uds
Guard test for `.start_listener()`s post-bind
`getsockname()`-vs-`.addr` round-trip, landed *before* that
reconciliation gets gated on an opt-out `ClassVar`.

- tcp: a `port=0` bind MUST still learn the kernel-picked
  port, since the reconciliation is the only path that ever
  does.
- uds: the sock-file path must survive the `.from_addr()`
  round-trip unchanged.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-14 09:34:41 -04:00
Gud Boi e14acb6550 Fix `pformat_caller_frame()`s bogus `indent` kwarg
Just drop it — `pformat_boxed_tb()` spells its knobs
`tb_box_indent`/`tb_body_indent`, and that fn's default
(1-space box indent) is what the caller wanted anyway.

Regressed-by: 888af602 (`pformat_cs()` mv into `.devx.pformat`)
Found-via: `/run-tests` test_pformat_caller_frame_renders

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-14 09:34:18 -04:00
Gud Boi 08eb63a8c8 Add a `pformat_caller_frame()` render guard test
`pformat_boxed_tb()` has never accepted an `indent` kwarg but
`pformat_caller_frame(box_tb=True)` has been passing one since
`888af602`. Nothing in the suite covered the branch, so the
`TypeError` only ever surfaced from `_mk_send_mte()` — i.e.
EVERY send-side `MsgTypeError` blew up while formatting itself
and masked the real msg-spec violation behind a bogus
`TypeError`.

Red on purpose per the test-first convention; the 1-line fix
lands next.

Also pin `pformat_boxed_tb()`s signature so a future typo'd
kwarg fails loudly at the call site instead of only when some
rare error path runs.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-14 09:34:05 -04:00
5 changed files with 43 additions and 101 deletions

View File

@ -26,11 +26,10 @@ onto `trio` as the library's sans-io layer allows.
`/ip4|ip6/<h>/tcp/<p>` and `/unix/<p>`; a `.../wg/u<key>` `/ip4|ip6/<h>/tcp/<p>` and `/unix/<p>`; a `.../wg/u<key>`
maddr raises `ValueError('Unsupported multiaddr protocol maddr raises `ValueError('Unsupported multiaddr protocol
combo')`. combo')`.
- there is no `wg` proto in the multiaddr *spec* yet, but - there is no `wg` proto in the multiaddr spec; the first-draft
multiformats/py-multiaddr#108 (key form `u<base64url>`) is upstream PR is multiformats/py-multiaddr#108 with key form
**merged** as of 2026-07-28 (`f86519da`) — and unreleased, the `u<base64url>` (commit `8be3a8b`), tracked by
latest `0.2.0` predating it. Spec registration is still tracked multiformats/py-multiaddr#107 and gh #483.
by multiformats/py-multiaddr#107 and gh #483.
- so **today's deployable story is declarative**: run `wg-quick` - so **today's deployable story is declarative**: run `wg-quick`
out-of-band, parse the maddr, strip to the inner out-of-band, parse the maddr, strip to the inner
`(host, port)`, verify the pubkey against the live tunnel, `(host, port)`, verify the pubkey against the live tunnel,
@ -113,12 +112,9 @@ class WGTunnelSpec(
### 3.2 `parse_maddr()`/`mk_maddr()` ### 3.2 `parse_maddr()`/`mk_maddr()`
Grammar — **verified** against py-multiaddr#108, first on the Grammar — **verified** against py-multiaddr#108
`baudco/py-multiaddr@wg_support` branch and re-verified after it (`baudco/py-multiaddr@wg_support`, installed in a throwaway venv;
merged upstream (`multiformats/py-multiaddr@f86519da`); all three all three forms below parse *and* round-trip):
forms below parse *and* round-trip. Note the codec also validates
that the key decodes to exactly 32 bytes, so a truncated key is a
`StringParseError`, not a silently-mangled parse:
``` ```
/ip4/192.168.1.50/udp/51820/wg/u<A_pub>/ip4/10.0.11.1/tcp/1616 /ip4/192.168.1.50/udp/51820/wg/u<A_pub>/ip4/10.0.11.1/tcp/1616
@ -173,16 +169,13 @@ Observed protocol-name lists, for writing the `match`:
(bearer_names, tunnel_specs, inner_names)`. This is also what (bearer_names, tunnel_specs, inner_names)`. This is also what
makes a wg-inside-wg stack fall out for free. makes a wg-inside-wg stack fall out for free.
- `mk_maddr()` inverse for `TunnelledAddress`. - `mk_maddr()` inverse for `TunnelledAddress`.
- **pending an upstream release**: py-multiaddr#108 is merged, so - **blocked on upstream**: `Multiaddr('/…/wg/u…')` only parses
`Multiaddr('/…/wg/u…')` parses — but off a `[tool.uv.sources]` once py-multiaddr#108 lands. Until then: pin the branch in the
`rev` pin, since no release carries the codec. Gate the tests `wg` extra / dev-group and gate the tests on
on `_have_wg_maddr_proto()`, implemented as `_have_wg_maddr_proto()` (a cheap try/except around
`protocols.protocol_with_name('wg')` under `Multiaddr('/wg/uAAAA')`). Do **not** hand-roll a `wg` parser
`except ProtocolNotFoundError`. Do **not** probe by parsing a in `tractor` — the whole point of #429 was dropping the NIH
dummy like `Multiaddr('/wg/uAAAA')` — the codec enforces a parser.
32-byte key, so that raises even when the proto *is* known. Do
**not** hand-roll a `wg` parser in `tractor` — the whole point
of #429 was dropping the NIH parser.
### 3.3 verification helper (pure, composable) ### 3.3 verification helper (pure, composable)
@ -444,7 +437,7 @@ consider doing it *first* for exactly that reason.
| risk | mitigation | | risk | mitigation |
| --- | --- | | --- | --- |
| `to_thread` worker runs in the wrong netns | §5.3; pass `netns=` to pyroute2 or pin a worker; test-first | | `to_thread` worker runs in the wrong netns | §5.3; pass `netns=` to pyroute2 or pin a worker; test-first |
| py-multiaddr#108 merged but unreleased | `[tool.uv.sources]` `rev` pin + `_have_wg_maddr_proto()` gate; layer A's inner-addr path works regardless | | py-multiaddr#108 not merged | branch pin + `_have_wg_maddr_proto()` gate; layer A's inner-addr path works regardless |
| `TunnelledAddress` leaks into `Endpoint` and breaks `inspect.getmodule()` | unwrap at parse/bindspace boundary; assert `not isinstance(ep.addr, TunnelledAddress)` in `Endpoint.__post_init__` | | `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 | | 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) | | 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) |

View File

@ -39,27 +39,16 @@ this composed form parses and round-trips
## requirements ## requirements
py-multiaddr #108 is **merged** (2026-07-28) but ships in no The `wg` proto isn't in released `py-multiaddr` yet (`0.2.0` has
release yet — the latest `0.2.0` (2026-03-17) predates it and has no `wg` codec), so until #108 lands:
no `wg` codec. So `pyproject.toml` carries a temporary
`[tool.uv.sources]` `rev` pin at the merge commit, and a plain
```bash ```bash
uv sync uv pip install 'git+https://github.com/baudco/py-multiaddr.git@wg_support' multibase
``` ```
gets you a `wg`-aware `multiaddr`. That pin goes away once a `wg_maddr.py` degrades to a plain segment split when the codec is
release carries the codec. You also need `multibase`: absent, so the examples still run — but you lose per-segment
validation. It deliberately does **not** hand-roll a `wg` codec
```bash
uv pip install multibase
```
Without the codec `wg_maddr.py` degrades to a plain segment split
— the examples still run, but you lose per-segment validation
(incl. the 32-byte key-length check), so a malformed key reaches
the returned struct instead of raising. `_have_wg_maddr_proto()`
is the gate. It deliberately does **not** hand-roll a `wg` codec
(gh #429 was about *dropping* our NIH parser). (gh #429 was about *dropping* our NIH parser).
## 0. tunnel setup (out-of-band, both hosts) ## 0. tunnel setup (out-of-band, both hosts)

View File

@ -2,9 +2,9 @@
r''' r'''
Parse `wg`-tunnelled multiaddrs into `tractor`-ready addrs. Parse `wg`-tunnelled multiaddrs into `tractor`-ready addrs.
The canonical form (per py-multiaddr #108, verified to parse + The canonical form (per py-multiaddr PR #108, verified to parse +
round-trip against its upstream merge) nests the *overlay* round-trip on that branch) nests the *overlay* endpoint **after**
endpoint **after** the `/wg/` segment: the `/wg/` segment:
/ip4/10.0.0.1/udp/51820/wg/u<key>/ip4/10.0.11.1/tcp/1616 /ip4/10.0.0.1/udp/51820/wg/u<key>/ip4/10.0.11.1/tcp/1616
\_______ wg bearer ______/\_ key _/\____ tractor ep _____/ \_______ wg bearer ______/\_ key _/\____ tractor ep _____/
@ -111,10 +111,8 @@ def parse_wg_maddr(
Split a `wg`-tunnelled maddr into its bearer/key/overlay Split a `wg`-tunnelled maddr into its bearer/key/overlay
parts. Pure no I/O. parts. Pure no I/O.
Total-or-raises: with a `wg`-aware `py-multiaddr` (#108) an Uses `py-multiaddr` when it knows the `wg` proto (PR #108),
unparseable maddr raises instead of yielding a struct built else falls back to a minimal segment split.
from garbage segments. See `_segments()` for the degraded
pre-#108 path.
''' '''
segs: list[str] = _segments(maddr) segs: list[str] = _segments(maddr)
@ -166,53 +164,22 @@ def parse_wg_maddr(
) )
_wg_proto_known: bool|None = None
def _have_wg_maddr_proto() -> bool:
'''
True iff the installed `py-multiaddr` knows the `/wg/` proto,
i.e. carries py-multiaddr#108.
Merged upstream 2026-07-28 but in no release as of `0.2.0`,
hence the `[tool.uv.sources]` `rev` pin.
Pure predicate; result cached since it can't change without a
reinstall.
'''
global _wg_proto_known
if _wg_proto_known is None:
from multiaddr.protocols import protocol_with_name
from multiaddr.exceptions import ProtocolNotFoundError
try:
protocol_with_name('wg')
_wg_proto_known = True
except ProtocolNotFoundError:
_wg_proto_known = False
return _wg_proto_known
def _segments(maddr: str) -> list[str]: def _segments(maddr: str) -> list[str]:
''' '''
Deliver a maddr's `/`-split segments, validating via the real Deliver a maddr's `/`-split segments, preferring the real
parser whenever it knows `wg`. parser when it supports `wg`.
''' '''
if _have_wg_maddr_proto(): from multiaddr import Multiaddr
from multiaddr import Multiaddr try:
# the real thing: validates every proto + value, incl. # the real thing: validates every proto + value
# that the `wg` key decodes to exactly 32 bytes. Let it
# raise — a maddr that doesn't parse must NOT reach
# `wg8_pubkey()`, which would happily emit a corrupt key.
Multiaddr(maddr) Multiaddr(maddr)
except Exception:
# XXX, degraded path for a pre-#108 `py-multiaddr` ONLY: no # XXX STOPGAP, only until py-multiaddr#108 lands; then
# per-segment validation, so a malformed key survives to the # this branch is dead and `Multiaddr` is authoritative.
# returned struct. We deliberately DON'T hand-roll a `wg` # We deliberately DON'T hand-roll a `wg` codec (the whole
# codec (the whole point of gh #429 was dropping the NIH # point of gh #429 was dropping the NIH parser).
# parser) — install the pinned rev to get validation back. pass
return [s for s in maddr.split('/') if s] return [s for s in maddr.split('/') if s]

View File

@ -166,17 +166,6 @@ sync_pause = {requires-python = ">=3.13, <3.14"}
# linux kernel networking # linux kernel networking
# 'pyroute2 # 'pyroute2
# XXX TEMP, the `/wg/u<key>` maddr proto is MERGED upstream (in
# py-multiaddr#108, 2026-07-28) but is in NO release yet; the
# latest `0.2.0` (2026-03-17) predates the merge by ~4 months.
# Pinned by `rev` (not `branch`) so CI stays reproducible.
#
# Drop this pin (and bump the `multiaddr` dep floor above) the
# moment a release carries the `wg` codec; the only consumer is
# `examples/multihost/wg_lan/`.
# |_https://github.com/multiformats/py-multiaddr/pull/108
multiaddr = { git = 'https://github.com/multiformats/py-multiaddr.git', rev = 'f86519daaa21699023d0037c58cdff600313dd09' }
# ------ tool.uv.sources ------ # ------ tool.uv.sources ------
[tool.uv] [tool.uv]

View File

@ -518,7 +518,7 @@ wheels = [
[[package]] [[package]]
name = "multiaddr" name = "multiaddr"
version = "0.2.0" version = "0.2.0"
source = { git = "https://github.com/multiformats/py-multiaddr.git?rev=f86519daaa21699023d0037c58cdff600313dd09#f86519daaa21699023d0037c58cdff600313dd09" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "base58" }, { name = "base58" },
{ name = "dnspython" }, { name = "dnspython" },
@ -533,6 +533,10 @@ dependencies = [
{ name = "trio-typing" }, { name = "trio-typing" },
{ name = "varint" }, { name = "varint" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c7/10/4e26a8577cfce1c0febc8d83087e1373e93c695c6e73ad010546fb67e229/multiaddr-0.2.0.tar.gz", hash = "sha256:acb6b25c332ec1b2f1f8fef8d03a8c63385d34a87d690df0f4bba43cdf6efe8d", size = 58356, upload-time = "2026-03-17T21:51:00.274Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/13/56e503d01218d1ca27ea9fda862045a4b400cae5e756f47315f5aaba0eee/multiaddr-0.2.0-py3-none-any.whl", hash = "sha256:bcff7bf3d7de3d6da0b865b25423bcb411de1d20d70cc6abfacf75170d17866c", size = 40424, upload-time = "2026-03-17T21:50:58.833Z" },
]
[[package]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
@ -1174,7 +1178,7 @@ requires-dist = [
{ name = "bidict", specifier = ">=0.23.1" }, { name = "bidict", specifier = ">=0.23.1" },
{ name = "colorlog", specifier = ">=6.8.2,<7" }, { name = "colorlog", specifier = ">=6.8.2,<7" },
{ name = "msgspec", specifier = ">=0.20.0" }, { name = "msgspec", specifier = ">=0.20.0" },
{ name = "multiaddr", git = "https://github.com/multiformats/py-multiaddr.git?rev=f86519daaa21699023d0037c58cdff600313dd09" }, { name = "multiaddr", specifier = ">=0.2.0" },
{ name = "pdbp", specifier = ">=1.8.2,<2" }, { name = "pdbp", specifier = ">=1.8.2,<2" },
{ name = "platformdirs", specifier = ">=4.4.0" }, { name = "platformdirs", specifier = ">=4.4.0" },
{ name = "setproctitle", specifier = ">=1.3,<2" }, { name = "setproctitle", specifier = ">=1.3,<2" },