sdk: canonical peer IDs, and the SDK examples as testnet canaries - #496
Merged
Merged
Conversation
A libp2p peer ID has several text encodings; every key, ban set and comparison in SAM is on peer.ID.String(), the base58btc form. The SDKs used strings as received in four places: a Peer argument to connect (a bare id or the target of a multiaddr, which js-libp2p keeps in the CIDv1 form it was written in), the peer id of a BANNED event, the ban list in /info, and the router peer read from the credential. None let a banned peer through today, because the connection gater and the inbound handlers key on the libp2p object and the control plane canonicalizes what it signs; but a CIDv1 argument skipped the pre-dial check in JavaScript and failed with a base58 error in Python, and the pattern is the one .gemini/styleguide.md flags. canonicalPeerId / canonical_peer_id (peerIdFromString in JavaScript, py-multiaddr's p2p codec in Python, no new dependency) now runs at each of those boundaries. A verified BANNED event carries the canonical id, and an event or ban entry that is not a peer id is dropped, as handleBannedEvent and reconcileBannedPeers do in internal/node. Tests ban a peer by its CIDv1 form and refuse a connect to it named every way, and the reverse.
Every canary on the testnets was a sam-node sidecar, so nothing there exercised what a user of the packages gets. The example programs the docs embed now run there unchanged: two Deployments (sam-sdk-canary-template) run the JavaScript and the Python serve example as members with no node beside them, publishing mcp://greeter-<lang> and a2a://greeter-<lang>, and an SDK cold-path CronJob (sam-sdk-probe-cronjob-template) runs each SDK's call example every 15 minutes against the everything canary (SDK -> node) and the other SDK's greeter (SDK -> SDK), MCP and A2A. The sam-node probe now also reaches both greeters, its MCP tool through call_remote_tool and its A2A endpoint through the egress proxy (node -> SDK). Every pair of implementations is crossed in both directions on every rollout and every 15 minutes after; deploy.yaml runs both probes once before a deploy is called good. The images (Dockerfile.sam-sdk-js, Dockerfile.sam-sdk-python) are built per commit by deploy.yaml and, without pushing, by the SDK workflow, so a broken Dockerfile fails the pull request. For that the canaries enroll as every other canary does, with the pod's projected service account token: enroll gains jwtPath / jwt_path, the counterpart of bootstrapTokenPath for a workload identity token, and the examples read SAM_JWT_PATH as the alternative to SAM_BOOTSTRAP_TOKEN_PATH and SAM_INSECURE_CONTROL_PLANE for a cluster-local control plane, as sam-node --insecure-control-plane. serve takes the service name as an argument so two instances are distinguishable. TestNativeSDKExamples runs the same configuration: servers enrolled through SAM_JWT_PATH under the canary names, callers crossing to the other language, and a node reaching each greeter the way the probe does.
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces SDK canaries for JavaScript and Python, including Dockerfiles, Kubernetes templates, and integration test updates to verify cross-language service discovery and communication. It also implements peer ID canonicalization across both SDKs to ensure consistent key handling in ban sets and lookups, and adds support for workload identity tokens via jwtPath in enrollment. I have reviewed the code and the suggested improvement regarding explicit UTF-8 encoding for file reading is appropriate and should be applied.
…ry provider
Found by running the canaries on bananas. The Python canary never joined:
a testnet control plane hands out router addresses as
/dnsaddr/bootstrap.<env>.sam-mesh.dev/p2p/<id>, which go-libp2p and
js-libp2p resolve before dialing and py-libp2p does not ("no transport
found for /dnsaddr/..."). dial_addrs resolves /dnsaddr, /dns, /dns4 and
/dns6 through py-multiaddr's DNSResolver, keeps the TCP addresses (the
only transport this host has; the routers also publish QUIC) and is used
wherever the SDK turns an address into a peer to dial: admission, a
relayed path, a direct one, and a provider's advertised addresses.
Then the Python probe overran its deadline: the first provider record of
mcp://everything named a pod the deploy had just replaced, and py-libp2p
has no dial timeout of its own, so the SYN waited on the kernel for about
two minutes and was retried. Every dial is now bounded by sam-node's 15s
(DIAL_TIMEOUT), after which the relayed path is tried and the caller gets
a ConnectionError.
The call examples use the first discovered provider that answers instead
of the first record, as the node probe already did; a record can outlive
its member for as long as the DHT keeps it.
Verified on bananas: the JavaScript and Python canaries serve, the SDK
cold-path probe passes in both languages (SDK -> node, SDK -> other SDK
over MCP and A2A) and the node probe reaches both greeters.
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #490: two things the SDKs still owed.
Peer IDs are canonicalized at the boundary
A libp2p peer ID has several text encodings; every key, ban set and comparison in SAM is on
peer.ID.String(), the base58btc form (.gemini/styleguide.md§1). The SDKs used strings as received in four places: aPeerargument toconnect(a bare id, or the target of a multiaddr, which js-libp2p keeps in the CIDv1 form it was written in), the peer id of aBANNEDevent, the ban list in/info, and the router peer read from the credential. Nothing let a banned peer through today, because the connection gater and the inbound handlers key on the libp2p object and the control plane canonicalizes what it signs; but a CIDv1 argument skipped the pre-dial check in JavaScript and failed with a base58 error in Python.canonicalPeerId/canonical_peer_id(peerIdFromStringin JavaScript; py-multiaddr's p2p codec in Python, no new dependency) now runs at each boundary, ashandleBannedEventandreconcileBannedPeersdo ininternal/node. A verifiedBANNEDevent carries the canonical id; an event or ban entry that is not a peer id is dropped. Tests ban a peer by its CIDv1 form and refuse aconnectto it named every way, and the reverse.The SDK examples run as testnet canaries
Every canary on
hubandbananaswas asam-nodesidecar; nothing exercised what a user of the packages gets. The example programs the docs embed now run there unchanged:.github/k8s/sam-sdk-canary-template.yaml): the JavaScript and the Pythonserveexample as members with no node beside them, publishingmcp://greeter-js/a2a://greeter-jsand…-py. They enroll with the pod's projected service account token, like every other canary.sam-sdk-probe-cronjob-template.yaml): every 15 minutes, each SDK'scallexample enrolls a fresh identity and reaches the everything canary (SDK → node) and the other SDK's greeter (SDK → SDK), MCP and A2A.sam-nodeprobe now also reaches both greeters: the MCP tool throughcall_remote_tool, the A2A endpoint through the egress proxy (node → SDK).Every pair of implementations is crossed in both directions on every rollout and every 15 minutes after;
deploy.yamlruns both probes once before a deploy is called good. The images (Dockerfile.sam-sdk-js,Dockerfile.sam-sdk-python) are built per commit bydeploy.yaml, and built without pushing by the SDK workflow so a broken Dockerfile fails the pull request.SDK changes this needed, each useful on its own:
enrollgainsjwtPath/jwt_path, the counterpart ofbootstrapTokenPathfor a platform's workload identity token.SAM_JWT_PATHas the alternative toSAM_BOOTSTRAP_TOKEN_PATH, andSAM_INSECURE_CONTROL_PLANE=truefor a cluster-local control plane (assam-node --insecure-control-plane).servetakes the service name as an argument.TestNativeSDKExamplesruns the canary configuration: servers enrolled throughSAM_JWT_PATHunder the canary names, callers crossing to the other language, and a node reaching each greeter the way the probe does. The probe's curl sequence against the node's MCP API was also run verbatim against a local mesh.Verified on the bananas cluster
The templates were applied to
sam-canary-bananaswith images built from this branch, which found two Python SDK defects that no local test reaches because a local mesh hands out IP addresses:/dnsaddr/bootstrap.bananas.sam-mesh.dev/p2p/<id>; go-libp2p and js-libp2p resolve that before dialing, py-libp2p reportedno transport foundand the Python canary never joined.dial_addrsresolves/dnsaddr,/dns,/dns4,/dns6through py-multiaddr'sDNSResolver(already a dependency) and keeps the TCP addresses, the only transport this host has; the routers also publish QUIC. Used at admission, on a relayed path, on a direct one and on a provider's advertised addresses.mcp://everythingprovider record named a pod the deploy had just replaced; the SYN waited on the kernel for about two minutes and was retried, and the probe overran its deadline. Every dial is now bounded by sam-node's 15s (DIAL_TIMEOUT), then the relayed path is tried and the caller gets aConnectionError.And one change to the examples:
calluses the first discovered provider that answers instead of the first record, as the node probe already did, since a record can outlive its member for as long as the DHT keeps it.With those, on
bananas: both canaries serve (serving mcp://greeter-py, a2a://greeter-py as 12D3KooW…), the SDK cold-path probe passes in both languages, and the node probe reaches both greeters:The test Deployments and CronJob are still in
sam-canary-bananas, pointing at images in the project's Artifact Registry; the first deploy after merge replaces them with theghcr.ioones.After merge
The first
bananasdeploy frommainbuilds and pushesghcr.io/google/sam-sdk-jsandsam-sdk-python(no registry setup needed beyond what the other images use) and creates the new Deployments and CronJob insam-canary-bananas. Thesam-canaryrole already covers them: they usesam-node-sa.Known gap, not addressed here: an SDK MCP service is reachable from a node through
/sam/mcp/1.0.0(call_remote_tool,find_remote_tools) but not through the node's/sam/<peer>/mcp/<name>HTTP egress path, which needs the provider to serve MCP over/libp2p-httpassam-nodedoes. The probe usescall_remote_toolfor that reason.