Skip to content

Commit d4bdbfa

Browse files
committed
sdk: describe discovery through the held routers
The SDK README still described service discovery as a Kademlia lookup on /mesh/kad/1.0.0 with gossip announcements beside it. It now describes the directory request to a router the session holds, and names the files that derive the keys and serve the requests. The service-key fixture says the same.
1 parent cd3b6f3 commit d4bdbfa

3 files changed

Lines changed: 32 additions & 36 deletions

File tree

‎sdk/README.md‎

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ An SDK member is an agent, not a service provider. It calls what the mesh
2323
offers: MCP tools, inference and A2A services that `agentmesh-node`s publish and
2424
the discovery table names. Inbound, it accepts one thing: A2A requests for
2525
itself, as `a2a://<name>`, reached by peer ID through a router. It publishes
26-
no service, writes no provider record and reports no catalog; a tool, a
26+
no service, writes no directory record and reports no catalog; a tool, a
2727
model or a named agent that others should find by name runs behind a
2828
`agentmesh-node`. The reasons are in [Agents, not services](#agents-not-services).
2929

@@ -44,7 +44,7 @@ Milestones 1 to 5 are implemented and tested in both languages:
4444
| `/mesh/auth/1.0.0`, both sides; join = pick `k` routers (default 2) by selector, preference and load, handshake and check role | yes | yes |
4545
| Circuit relay v2 reservation on each held router; dial and accept through them | yes | yes |
4646
| `/mesh/goaway/1.0.0`: shun a draining or overloaded router for `retry_after` and replace it from the catalog; redial (`2s, 4s, 8s`) before failing over on a dropped connection | yes | yes |
47-
| Service discovery in the mesh DHT (`/mesh/kad/1.0.0`) | yes | yes |
47+
| Service discovery through a held router (`/mesh/directory/1.0.0`) | yes | yes |
4848
| `/mesh/mcp/1.0.0` client: list and call a provider's tools, or its catalog | yes | yes |
4949
| Caller-side label requirements on the provider's credential | yes | yes |
5050
| `/libp2p-http` client: call inference and A2A services on the mesh; response bodies stream | yes | yes |
@@ -58,7 +58,7 @@ Milestones 1 to 5 are implemented and tested in both languages:
5858
| Published to a registry from the release workflow | npm `@agentmesh-p2p/sdk` | PyPI `agentmesh-p2p` |
5959

6060
A member built this way is on the mesh and uses it in both directions: it
61-
finds a service in the DHT and calls it through the router, verifying the
61+
finds a service in the directory and calls it through the router, verifying the
6262
provider on every call, and it answers A2A requests for its own agent from
6363
a `agentmesh-node` or another SDK member that names it by peer ID, authorizing
6464
every caller with the same Datalog the Go node evaluates. It follows the
@@ -125,14 +125,11 @@ pinned by a test:
125125
host dials such addresses by name (libp2p/py-libp2p#1549). It also dials
126126
`wss` with certificate verification off unless given a TLS context
127127
(libp2p/py-libp2p#1550); the host gives it the system roots.
128-
- py-libp2p's Kademlia client takes a protocol prefix but its provider
129-
lookups still speak `/ipfs/kad/1.0.0`, so it cannot reach the mesh DHT.
130-
The Python SDK does a bounded GET_PROVIDERS walk itself on
131-
`/mesh/kad/1.0.0`, seeded by the routers (`agent_mesh/discovery.py`).
132-
js-libp2p's `@libp2p/kad-dht` takes the protocol and works as is.
133-
- go-libp2p-kad-dht keys provider records by the multihash, not the CID.
134-
`sdk/testdata/service_keys.json` pins the keys both SDKs derive against
135-
`internal/node/service.go`.
128+
- A member keeps no DHT. Both SDKs ask a router they hold over
129+
`/mesh/directory/1.0.0` (`discovery.ts`, `agent_mesh/discovery.py`), one
130+
`DirectoryRequest` per stream, and the router runs the lookup in the DHT
131+
the routers keep among themselves. `sdk/testdata/service_keys.json` pins
132+
the keys both SDKs derive against `api/directory_keys.go`.
136133
- go-libp2p-http is plain HTTP/1.1 on a stream, one request per stream,
137134
with `Host` set to the peer ID. The JS SDK bridges the stream to a Node
138135
duplex and runs Node's HTTP server and client on it; the Python SDK
@@ -144,7 +141,7 @@ pinned by a test:
144141
`http://mesh/mesh/<peer-id>/<type>/<name>/<path>`, the shape of `agentmesh-node`'s
145142
egress proxy and of an agent card rewritten for the mesh; the host is ignored.
146143
- A member that publishes nothing has announced no address, so nothing in
147-
the DHT or a router's peerstore names one. `agentmesh-node` dials
144+
the directory names one. `agentmesh-node` dials
148145
`/p2p/<router>/p2p-circuit` for every router it authenticated with when
149146
it knows nothing else about a peer, as both SDKs do for a peer named by
150147
ID (`internal/node/mcp.go`, `preparePeerAddrs`).
@@ -243,7 +240,7 @@ messages in `api/agentmesh.proto`. Bodies are capped at 1 MiB on both sides.
243240
not.
244241
Challenges are defined in `api/network.go`. It is the one instant on the
245242
wire that is an `int64`: it is the number in the signed text. Every other
246-
instant (`expire_time`, `sign_time`, `event_time`, `announce_time`) is a
243+
instant (`expire_time`, `sign_time`, `event_time`) is a
247244
`google.protobuf.Timestamp`, and a receiver rejects a message whose
248245
required instant is unset.
249246
- A bootstrap enrollment answers `PENDING` until an operator approves it,
@@ -283,7 +280,8 @@ messages in `api/agentmesh.proto`. Bodies are capped at 1 MiB on both sides.
283280
- Muxer: yamux (libp2p default).
284281
- Relay: circuit relay v2, with the routers as static relays; hole punching
285282
is on. A node behind NAT is reached through a router.
286-
- DHT: Kademlia with protocol prefix `/mesh`, mode auto.
283+
- Directory: `/mesh/directory/1.0.0` to a router the session holds; a
284+
member keeps no DHT and speaks no Kademlia.
287285

288286
### Streams
289287

@@ -334,15 +332,15 @@ length, then the bytes), with a 64 KiB cap on the first frame.
334332
(`2s, 4s, 8s`) and, if it does not come back or refuses auth, shuns it and
335333
tops up from the catalog.
336334

337-
### Discovery (`internal/node/service.go`)
335+
### Discovery (`api/directory_keys.go`, `internal/node/directory_client.go`)
338336

339-
A provider announces a service as a DHT provider record for
340-
`cidv1(raw, sha256("mesh:service:<type>[:<name>]"))`, once for the type and
341-
once for the name (`<type>` is `mcp`, `inference`, `a2a` or `egress`). A consumer
342-
looks up providers for the same CID, dials one and opens `/mesh/mcp/1.0.0`.
343-
Gossip topics under `/mesh/discovery/v1/...` carry `ServiceAnnounce` for
344-
interest-scoped updates; the DHT is the source of truth. The SDKs look
345-
records up and announce none.
337+
A provider announces a service to a router it holds as a record under
338+
the key `sha256("mesh:service:<type>[:<name>]")`, once for the type and once
339+
for the name (`<type>` is `mcp`, `inference`, `a2a` or `egress`), with the
340+
addresses it is reached at; the router keeps the record and places it on
341+
the routers closest to the key. A consumer asks a router it holds for the
342+
providers of the same key, dials one and opens `/mesh/mcp/1.0.0`. The SDKs
343+
look records up and announce none.
346344

347345
A peer that announced nothing is still reachable by peer ID: every router
348346
relays for the peers it admitted, so a caller dials
@@ -385,17 +383,17 @@ of it is derived in the SDK:
385383

386384
An SDK member calls services and accepts A2A requests for its own agent.
387385
It does not publish services: no MCP server, no named inference or A2A
388-
service, no DHT record, no catalog entry. The reasons:
386+
service, no directory record, no catalog entry. The reasons:
389387

390388
- **The agent is the principal; a service is infrastructure.** A harness
391389
(LangChain, Claude Code, a browser agent) is an MCP client. The tools it
392390
should see through the mesh are the ones `agentmesh-node`s publish. Something
393391
that must be reachable by name, a tool, a model, a named agent, is a
394392
deployment concern, and the sidecar model exists for it: the `agentmesh-node`
395393
beside it publishes the service and enforces the policy once, in Go.
396-
- **Publishing is the expensive part.** Being a provider needs a DHT
397-
provide loop, a catalog report and provider records that go stale on
398-
every rollout. Accepting requests for the agent itself needs the relay
394+
- **Publishing is the expensive part.** Being a provider needs an
395+
announcement loop, a catalog report and records that go stale on every
396+
rollout. Accepting requests for the agent itself needs the relay
399397
reservation, the auth handshake and the authorizer, all of which a member
400398
needs anyway to be verified by a `agentmesh-node` before it is called.
401399
- **One inbound protocol.** A2A is the protocol for talking to an agent,
@@ -411,7 +409,7 @@ service, no DHT record, no catalog entry. The reasons:
411409

412410
What an SDK agent is on the wire: a peer with a relay reservation on a
413411
router, answering `/mesh/auth/1.0.0` and `/libp2p-http` for `a2a://<name>`,
414-
with no DHT record and no catalog entry. To a `agentmesh-node` it is a peer whose
412+
with no directory record and no catalog entry. To a `agentmesh-node` it is a peer whose
415413
address nobody announced, reached through the router that admitted both,
416414
and whose credential it verifies before forwarding
417415
`/mesh/<peer>/a2a/<name>/...`. Routers, the control plane and the Datalog
@@ -483,10 +481,8 @@ holds against the control plane's records.
483481
### Milestone 3 — call tools (done)
484482

485483
- Discovery: `session.discover("mcp://calc")`, or `(type, name)`, or a type
486-
alone, looks the mesh DHT up for the service key
487-
`internal/node/service.go` derives. JS: `@libp2p/kad-dht` in client mode
488-
on `/mesh/kad/1.0.0`; Python: the SDK's own bounded GET_PROVIDERS walk on
489-
the same protocol (see the facts above).
484+
alone, asks a router the session holds, over `/mesh/directory/1.0.0`,
485+
for the providers of the service key `api/directory_keys.go` derives.
490486
- Naming a peer: every call takes a `Peer`, which is a provider `discover`
491487
returned, a peer id, or a multiaddr. For the first two the SDK dials the
492488
advertised addresses and `<router>/p2p-circuit/p2p/<peer>` through every
@@ -521,7 +517,7 @@ holds against the control plane's records.
521517
it, reads its catalog, and is refused for missing labels, an untrusted
522518
provider, a forged caller credential and an unknown service. Integration:
523519
the mesh of `sdk_mesh_test.go` has an MCP backend behind the `agentmesh-node`
524-
(`calc`, tool `add`); each SDK member discovers it in the DHT, lists and
520+
(`calc`, tool `add`); each SDK member discovers it through its router, lists and
525521
calls `add` through the router, reads the node's catalog and is refused a
526522
service the node does not have. The runners took `discover`, `tools` and
527523
`call` commands for it.
@@ -673,7 +669,7 @@ same commit as the Go components they talk to.
673669

674670
- Rewriting the control plane, router or sandbox components; they stay Go.
675671
- Publishing services from an SDK: an MCP server, a named inference or A2A
676-
service, a DHT record or a catalog entry. That is `agentmesh-node`'s job; see
672+
service, a directory record or a catalog entry. That is `agentmesh-node`'s job; see
677673
[Agents, not services](#agents-not-services).
678674
- Any SDK-only wire protocol. If an SDK needs something the Go node does not
679675
speak, the Go node learns it first.
@@ -750,7 +746,7 @@ one follows is named so a change on one side can be carried to the others.
750746
| `auth.ts` | `auth.py` | `/mesh/auth/1.0.0` on both sides, as `HandleAuthHandshake` |
751747
| — | `relay.py` | circuit relay v2 client (py-libp2p's cannot talk to go-libp2p) |
752748
| `session.ts` | `session.py` | `MeshSession`: join, relay reservation, refresh loop, control plane sync loop, gossip events, `acceptA2A()` / `accept_a2a()`, `fetch()` |
753-
| `discovery.ts` | `discovery.py` | service keys and DHT lookups as `internal/node/service.go`; lookups only, no records |
749+
| `discovery.ts` | `discovery.py` | service keys and directory requests to a held router, as `internal/node/directory_client.go`; lookups only, no records |
754750
| `mcp.ts` | `mcp_client.py` | MCP over `/mesh/mcp/1.0.0`, the client side of `internal/node/gate.go` |
755751
| `authorizer.ts` | `authorizer.py` | the provider authorizer, as `internal/node.(*AgentMeshNode).Authorize`, over the generated baseline and `datalog_rules` |
756752
| `libp2p-http.ts` | `libp2p_http.py` | `/libp2p-http` client (streaming) and the A2A ingress for the agent, as go-libp2p-http and `StartIngressServer`; mesh URLs |

‎sdk/python/tests/test_mcp.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def frame(biscuit: bytes, target: str) -> bytes:
153153
return pb.AuthFrame(biscuit=biscuit, target_service=target).SerializeToString()
154154

155155

156-
def test_service_keys_match_internal_node_service():
156+
def test_service_keys_match_api_service_key():
157157
for s in FIXTURE["services"]:
158158
assert service_key(s["type"], s["name"] or None).hex() == s["multihash"], f"{s['type']}:{s['name']}"
159159
assert parse_service_target("mcp://calc") == ("mcp", "calc")

‎sdk/testdata/service_keys.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"_comment": "Service discovery keys as internal/node/service.go derives them: cid = cidv1(raw, sha256(\"mesh:service:\u003ctype\u003e[:\u003cname\u003e]\")); go-libp2p-kad-dht keys the provider record by the multihash (hex).",
2+
"_comment": "Service directory keys as api.ServiceKey derives them: multihash = sha256(\"mesh:service:<type>[:<name>]\") (hex); cid is the same key as cidv1(raw, multihash), the form the routers' DHT stores it under.",
33
"services": [
44
{
55
"type": "mcp",

0 commit comments

Comments
 (0)