Compare commits

...

3 Commits

Author SHA1 Message Date
Gud Boi be108ccc32 Draft the `/tipc` multiaddr protocol
Propose a fixed-width service endpoint carrying the TIPC type,
instance and publication scope, with one canonical structured value
that generic multiaddr parsers can compose normally.

Retain the kernel-standard `tipc` name while using “Cluster Domain
Sockets” as explanatory terminology. Document the binary and text
encodings, WireGuard composition, deployment-management boundary,
upstream sequence, test vector and open maintainer questions.

Prompt-IO: ai/prompt-io/opencode/20260819T003328Z_53516b09_prompt_io.md

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-19 15:03:00 -04:00
Gud Boi ff2b20b7d1 Harden the `tipc` two-host smoke test
Turn the physical-host sketch into an operator runbook covering
cluster identity, interface and bearer setup, link validation,
failure/rejoin testing, diagnostic capture and cleanup.

Explain the cluster-domain-socket analogy and identify a future
`pyroute2` TIPC codec as the path from manual `tipc(8)` commands to
the same netlink management stack planned for WireGuard.

Authorize `host_a_srv` by its stable import name so direct script
execution does not expose only `__main__` while host B requests the
callable's actual `NamespacePath`.

Prompt-IO: ai/prompt-io/opencode/20260819T003327Z_53516b09_prompt_io.md

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-19 14:56:48 -04:00
Gud Boi caf9c3d6d3 Make the `tipc` CI leg blocking
The refreshed PR matrix passes on Ubuntu with the TIPC kernel
module loaded, along with the TCP, UDS and macOS legs. Remove the
temporary `continue-on-error` expression so future TIPC
regressions block CI.

Prompt-IO: ai/prompt-io/opencode/20260819T003326Z_53516b09_prompt_io.md

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-19 14:52:49 -04:00
11 changed files with 459 additions and 18 deletions

View File

@ -92,12 +92,6 @@ jobs:
timeout-minutes: 16
runs-on: ${{ matrix.os }}
# XXX the `tipc` leg stays NON-blocking until the `modprobe`
# step below is proven stable on GH's runners; flip this off
# once it's had a few green runs. See plan 01 §7.3 in
# `ai/tpt-backends/01_tipc_backend.md`.
continue-on-error: ${{ matrix.tpt_proto == 'tipc' }}
strategy:
fail-fast: false
matrix:

View File

@ -0,0 +1,29 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: a998acff-af66-4969-a65e-f77cbf5ef8dd
timestamp: 2026-08-19T00:33:26Z
git_ref: wkt/tipc_backend_378
scope: config
substantive: true
raw_file: 20260819T003326Z_53516b09_prompt_io.raw.md
---
## Prompt
Confirm whether the successful TIPC CI leg can become blocking.
## Response summary
Removed the temporary TIPC-only `continue-on-error` expression after
every refreshed PR #493 check passed.
## Files changed
- `.github/workflows/ci.yml` - make TIPC failures block CI.
## Human edits
The human explicitly requested reconsidering the gate after the CI
run and chose to leave the generated change local for review before
requesting this commit plan.

View File

@ -0,0 +1,23 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-19T00:33:26Z
git_ref: wkt/tipc_backend_378
diff_cmd: git diff HEAD~1..HEAD
---
## Prompt
The user asked whether the now-green TIPC CI leg could stop using
`continue-on-error`.
## Response
All checks on PR #493 passed, including the Ubuntu TIPC matrix job.
The generated config change removes the temporary non-blocking gate
and its obsolete explanatory comment:
> `git diff HEAD~1..HEAD -- .github/workflows/ci.yml`
The remaining matrix, Linux-only exclusion and `modprobe tipc` setup
stay unchanged.

View File

@ -0,0 +1,37 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: a998acff-af66-4969-a65e-f77cbf5ef8dd
timestamp: 2026-08-19T00:33:27Z
git_ref: wkt/tipc_backend_378
scope: code
substantive: true
raw_file: 20260819T003327Z_53516b09_prompt_io.raw.md
---
## Prompt
Develop a practical bootstrap plan for testing TIPC across two
physical motherboards.
## Response summary
Expanded the two-host example into a reproducible smoke-test runbook
and made host A authorize the stable module name imported by host B.
Documented the cluster-domain-socket analogy and the prospective
`pyroute2` management path.
## Files changed
- `examples/multihost/tipc_cluster/README.md` - document setup,
validation, resilience testing, capture and cleanup.
- `examples/multihost/tipc_cluster/host_a_srv.py` - authorize the
`host_a_srv` RPC module under direct script execution.
## Human edits
The human selected two separate physical motherboards as the target
environment and requested that the resulting local work be packaged
as its own commit boundary. The human also proposed “Cluster Domain
Sockets” as clearer terminology and reusing the planned `pyroute2`
dependency for TIPC management.

View File

@ -0,0 +1,27 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-19T00:33:27Z
git_ref: wkt/tipc_backend_378
diff_cmd: git diff HEAD~1..HEAD
---
## Prompt
The user asked for a bootstrap plan to try TIPC with two real,
physically separate Linux motherboards.
## Response
The generated changes turn the existing sketch into an operator
runbook and correct the RPC module authorization used by direct
script execution:
> `git diff HEAD~1..HEAD -- examples/multihost/tipc_cluster/README.md examples/multihost/tipc_cluster/host_a_srv.py`
The runbook covers matching revisions, cluster identity, interface
selection, Ethernet bearer setup, link and name-table validation,
RPC, failure/rejoin testing, diagnostics, cleanup and a future
network-namespace fixture. It also records “Cluster Domain Sockets”
as explanatory terminology and identifies a future `pyroute2` TIPC
generic-netlink codec as the path away from manual `tipc(8)` calls.

View File

@ -0,0 +1,37 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: a998acff-af66-4969-a65e-f77cbf5ef8dd
timestamp: 2026-08-19T00:33:28Z
git_ref: wkt/tipc_backend_378
scope: docs
substantive: true
raw_file: 20260819T003328Z_53516b09_prompt_io.raw.md
---
## Prompt
Draft an upstream `/tipc` multiaddr issue modeled on the prior `wg`
proposal.
## Response summary
Added a reviewable upstream issue draft with a concrete binary and
text encoding, composition examples, rollout plan and open design
questions. Retained the ecosystem-standard `tipc` name while using
“Cluster Domain Sockets” as explanatory terminology, and separated
the future `pyroute2` management track from address encoding.
## Files changed
- `ai/tpt-backends/04_tipc_multiaddr_upstream.md` - candidate
`multiformats/multiaddr` issue body.
- `ai/tpt-backends/README.md` - roster the upstream proposal.
## Human edits
The human chose to track the draft under `ai/tpt-backends/` instead
of leaving it in the ignored gish cache, and explicitly requested a
Prompt-IO record for this docs-only commit. The human proposed the
`cds` alternative and unifying TIPC/WireGuard management through
`pyroute2`.

View File

@ -0,0 +1,26 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-19T00:33:28Z
git_ref: wkt/tipc_backend_378
diff_cmd: git diff HEAD~1..HEAD
---
## Prompt
The user asked to begin drafting an upstream `/tipc` multiaddr issue
similar to the existing WireGuard proposal, then chose to track the
draft as an AI planning document.
## Response
The generated proposal is intended for `multiformats/multiaddr`:
> `git diff HEAD~1..HEAD -- ai/tpt-backends/04_tipc_multiaddr_upstream.md ai/tpt-backends/README.md`
It proposes a code allocated upstream, a fixed 72-bit value holding
the service type, instance and scope, and the canonical text form
`/tipc/<type>:<instance>:<scope>`. It includes WireGuard composition,
security boundaries, the `tipc` versus `cds` naming decision,
deployment-management separation, implementation phases, test data
and open questions for maintainers.

View File

@ -0,0 +1,191 @@
# `/tipc` multiaddr protocol: upstream issue draft
Candidate issue for `multiformats/multiaddr`, to be submitted after
the encoding questions below have been reviewed locally.
## Context
Linux TIPC (Transparent Inter-Process Communication) addresses a
service by a location-independent `(service type, instance)` name.
A server publishes that name into the kernel-maintained cluster name
table and a client connects by the same name; no host or transport
port forms part of the service identity.
TIPC is also known as **Cluster Domain Sockets**, a useful description
of its relationship to Unix-domain sockets. The registered protocol
name should nevertheless remain `tipc`: it matches Linux's
`AF_TIPC`, socket constants, kernel module and iproute2 frontend.
Registering `cds` would create an ecosystem-specific alias that is
harder to map back to the normative kernel API.
We would like to register a `tipc` multiaddr component so these
service endpoints can be represented directly and composed with a
bearer or tunnel description:
```text
/tipc/1953628160:1616:2
/ip4/192.168.1.50/udp/51820/wg/u<key>/tipc/1953628160:1616:2
\____________ WireGuard bearer ____________/\____ TIPC service ____/
```
In the composed form, the components through `/wg/<key>` identify
the routed bearer and tunnel peer. The `/tipc/...` component is pure
service identity, resolved to a current publisher by the TIPC kernel
name table. Unlike a TCP endpoint, it deliberately has no inner IP
address or port.
This proposal does not imply that parsing the multiaddr configures a
TIPC bearer. In particular, TIPC over a WireGuard interface requires
a separately configured TIPC UDP bearer; WireGuard interfaces are L3
devices and cannot carry TIPC Ethernet media directly.
Today that bearer is configured through iproute2's `tipc` frontend,
which speaks the kernel's `TIPCv2` generic-netlink family. `pyroute2`
already provides WireGuard support and generic-netlink primitives but
has no TIPC codec/module; adding one is a complementary deployment
automation track, not part of this address-format proposal.
## Proposed protocol
- Name: `tipc`
- Code: TBD, allocated in `multiformats/multicodec` under the
`multiaddr` tag before implementations stabilize one
- Size: 72 bits
- Value: service type, service instance and publication scope
### Binary form
Exactly nine bytes with no value-length prefix:
| Offset | Size | Field | Encoding |
| ---: | ---: | --- | --- |
| 0 | 4 bytes | service type | unsigned 32-bit big-endian |
| 4 | 4 bytes | service instance | unsigned 32-bit big-endian |
| 8 | 1 byte | publication scope | unsigned enum byte |
```text
tipc-value = uint32be(type) || uint32be(instance) || uint8(scope)
```
For type `1953628160` (`0x74720000`), instance `1616` and
cluster scope `2`, the payload is:
```text
74 72 00 00 00 00 06 50 02
```
### String form
Use one multiaddr value segment containing three canonical decimal
integers:
```text
/tipc/<type>:<instance>:<scope>
```
Canonical values have no sign, whitespace, alternate radix or
leading zeroes, except that zero itself is `0`. `type` and `instance`
must fit unsigned 32-bit fields. Scope is one of:
- `2`: `TIPC_CLUSTER_SCOPE`
- `3`: `TIPC_NODE_SCOPE`
The existing experimental spelling
`/tipc/<type>/<instance>/<scope>` cannot be registered as one normal
multiaddr protocol: generic parsing treats each slash-delimited name
as another protocol component. A single structured value preserves
TIPC's atomic service-address semantics without registering three
artificial protocols.
## Why scope is included
TIPC scope controls where a bound service publication is visible.
The same address representation is used for listener configuration
and dialing, so retaining scope lets a multiaddr round-trip the full
socket address rather than silently turning a node-local bind into a
cluster publication.
Modern Linux UAPI defines cluster and node scopes. The deprecated
zone spelling should not receive a new wire value; implementations
may normalize legacy input to cluster scope before encoding.
## Composition
Standalone service:
```text
/tipc/1953628160:1616:2
```
TIPC service reached through a WireGuard bearer:
```text
/ip4/192.168.1.50/udp/51820/wg/u<key>/tipc/1953628160:1616:2
```
This differs intentionally from TCP over WireGuard:
```text
/ip4/192.168.1.50/udp/51820/wg/u<key>/ip4/10.0.11.1/tcp/1616
```
TCP repeats an inner locative address. TIPC does not: its service
name is resolved and load-balanced in-kernel across current
publishers.
## Semantics and security
- A TIPC service name identifies a service, not a unique process.
Multiple publishers may bind the same name and connections can be
distributed among them.
- Publication scope is reachability metadata, not authentication.
- A composed `/wg` key authenticates the tunnel peer, not the TIPC
service publisher.
- TIPC's optional native AES-GCM link encryption is independent of
this address codec and of WireGuard.
- Codec implementations should validate field widths and canonical
text only; cluster membership and publisher authorization remain
deployment concerns.
## Implementation plan
1. Reserve a `multiaddr`-tagged code in
`multiformats/multicodec`.
2. Add the fixed-size protocol row and normative encoding text to
`multiformats/multiaddr`.
3. Add codecs and cross-language test vectors, beginning with
`multiformats/py-multiaddr`.
4. Verify standalone and composed `wg` + `tipc` string/binary
round-trips.
## Open questions
1. Is a fixed 72-bit value preferred over a self-describing or
variable-width tuple for this kernel-defined address?
2. Should node scope be representable in a generally shareable
multiaddr, or should the registered form be cluster-only?
3. Does multiaddr have an existing convention for structured numeric
values that should replace the colon-separated text form?
4. Should the specification describe TIPC service *ranges*, or keep
this protocol limited to singleton service names used for
connection endpoints?
## References
- Linux TIPC documentation:
https://docs.kernel.org/networking/tipc.html
- Cluster Domain Sockets terminology:
https://en.wikipedia.org/wiki/Transparent_Inter-process_Communication
- Linux socket UAPI:
https://github.com/torvalds/linux/blob/master/include/uapi/linux/tipc.h
- Linux TIPC generic-netlink UAPI:
https://github.com/torvalds/linux/blob/master/include/uapi/linux/tipc_netlink.h
- pyroute2 WireGuard and generic-netlink APIs:
https://docs.pyroute2.org/wireguard.html
- WireGuard multiaddr implementation discussion:
https://github.com/multiformats/py-multiaddr/issues/107
- WireGuard codec implementation:
https://github.com/multiformats/py-multiaddr/pull/108
- Downstream tracking and prototype:
https://github.com/goodboy/tractor/issues/498

View File

@ -16,6 +16,7 @@ document only their own deltas.
| [01 — TIPC](./01_tipc_backend.md) | [#378] | **none** (stdlib) | small | **landed**, PR [#493] — see the [handoff](./01_tipc_HANDOFF.md) |
| [02 — QUIC/`iroh`](./02_quic_iroh_backend.md) | [#353] | `iroh` (uniffi FFI) | large | needs a prep PR |
| [03 — `wg` bindspace](./03_wg_tunnel_bindspace.md) | [#482], [#443] | `pyroute2` | medium, 3 layers | layer A now |
| [04 — `/tipc` multiaddr](./04_tipc_multiaddr_upstream.md) | [#498] | upstream `multiformats` | issue draft | local review |
Headline conclusions:
@ -52,5 +53,6 @@ lands first ships it.
[#353]: https://github.com/goodboy/tractor/issues/353
[#482]: https://github.com/goodboy/tractor/issues/482
[#443]: https://github.com/goodboy/tractor/issues/443
[#498]: https://github.com/goodboy/tractor/issues/498
[#493]: https://github.com/goodboy/tractor/pull/493

View File

@ -2,6 +2,10 @@
TIPC is a linux-kernel cluster IPC protocol whose service names
live in a **cluster-wide name table maintained by the kernel**.
It is also described as **Cluster Domain Sockets**: the Unix-domain
socket model extended from one kernel to a cluster. That name is a
useful explanation for new users, while the code keeps `tipc` as its
protocol key to match Linux's `AF_TIPC`, kernel module and tooling.
For `tractor` that means:
- an actor's IPC address is a service name `(stype, instance)`,
@ -85,30 +89,55 @@ Everything above is single-node (`modprobe` is enough). To span
hosts you need a **bearer** on both, which is the one thing that
can't be CI'd.
For the first physical test, use two wired Linux hosts on the same
L2 segment. Prefer a direct cable or uncomplicated switch; avoid
Wi-Fi, guest VLANs and port isolation until the basic link works.
Use the same checkout and Python environment on both hosts:
```bash
# on BOTH hosts
git rev-parse HEAD # must match on A and B
uv sync --all-extras --dev
sudo modprobe tipc
# over ethernet (L2) — simplest when the hosts share a segment
sudo tipc bearer enable media eth device eth0
# choose the real wired iface; do not assume `eth0`
ip -br link
IFACE=enp3s0
# inspect existing cluster identity before changing anything
tipc node get address # must differ between hosts
tipc node get netid # must match between hosts
# use one private test netid on BOTH hosts, before enabling bearers
sudo tipc node set netid 37801
# ethernet is simplest when the hosts share an L2 segment
sudo tipc bearer enable media eth device "$IFACE"
# ..or over UDP when L2 isn't available — and MANDATORY over a
# `wg` mesh, see below
sudo tipc bearer enable media udp name uc localip 10.0.11.1
# verify BEFORE running anything: this must list the peer
tipc bearer list
tipc link list
tipc node list
```
Then:
If the link does not appear, first verify carrier, a common TIPC
network ID, distinct node addresses, a common VLAN and compatible
MTUs. Ethernet TIPC uses EtherType traffic rather than IP routing,
so a successful `ping` alone does not prove the bearer can work.
Run each command from `examples/multihost/tipc_cluster/`:
```bash
# host A
python host_a_srv.py
uv run python host_a_srv.py
# host B
python host_b_client.py
watch -n 0.5 tipc nametable show # optional second terminal
uv run python host_b_client.py
```
Note what's absent from both scripts: any IP, hostname or port.
@ -116,6 +145,51 @@ Both sides name the *same service*, and the kernel routes it.
Move `host_a_srv.py` to a third node and host B's dial keeps
working, unchanged.
For a first resilience pass, use a local console or separate
management link so the test does not cut off your own SSH session:
```bash
# host B: record the healthy baseline
tipc link list
tipc link statistics show
# either host: withdraw and recreate the bearer
sudo tipc bearer disable media eth device "$IFACE"
tipc link list
sudo tipc bearer enable media eth device "$IFACE"
tipc link list
# prove name withdrawal/republication and RPC recovery
tipc nametable show
uv run python host_b_client.py
```
Capture `uname -a`, both node addresses, `tipc bearer list`,
`tipc link list`, `tipc link statistics show`, the name table and
both Python transcripts. Those artifacts distinguish an actor bug
from bearer discovery, cluster identity or switch configuration.
Clean up a disposable Ethernet test on both hosts with:
```bash
sudo tipc bearer disable media eth device "$IFACE"
tipc link list
```
Restore any pre-existing network ID only after all bearers are
disabled. The first useful automation target is a two-node network
namespace fixture that asserts link-up, remote publication, RPC,
withdrawal and republication in that order; physical hardware then
remains the validation layer for real NIC and switch behaviour.
The commands above use iproute2's `tipc` frontend, which speaks the
kernel's `TIPCv2` generic-netlink family. The planned `pyroute2`
dependency already manages WireGuard, interfaces and namespaces and
provides generic-netlink primitives, but it does not currently ship a
TIPC message codec. Adding one upstream would let `tractor` replace
these manual commands with one Python netlink stack instead of
shelling out; until then, `tipc(8)` remains the canonical frontend.
### over a `wg` mesh
TIPC over WireGuard is the intended reference multihost
@ -231,12 +305,10 @@ self-skip when the module is absent.
## CI
Single-host TIPC *is* CI-able — the module ships with the
standard Ubuntu kernel package, so a `sudo modprobe tipc` step
plus a `--tpt-proto tipc` matrix entry should work. That's not
wired up yet; verify in a throwaway workflow first, and fall
back to a container job with `--cap-add NET_ADMIN` if the
runners refuse. Cross-node (bearer) testing stays manual — this
README is that smoke test.
standard Ubuntu kernel package. CI loads it with `sudo modprobe
tipc` and runs the suite with `--tpt-proto tipc` as a blocking
matrix leg. Cross-node bearer testing stays manual — this README
is that smoke test.
## normative refs

View File

@ -57,7 +57,10 @@ async def main() -> None:
) as an:
await an.start_actor(
'host_a',
enable_modules=[__name__],
# Host B imports this same module name to construct the
# RPC `NamespacePath`; direct script execution would
# otherwise expose it as `__main__` on host A.
enable_modules=['host_a_srv'],
)
print(
'host_a up — `tipc nametable show` on EITHER host\n'