Skip to content

Commit b9259cc

Browse files
authored
Merge pull request #496 from aojea/sdk-peer-id
sdk: canonical peer IDs, and the SDK examples as testnet canaries
2 parents e17f503 + 5077d2e commit b9259cc

37 files changed

Lines changed: 1332 additions & 197 deletions

‎.dockerignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ __pycache__/
3333
*.out
3434
coverage.*
3535
mobile/sam-node-app/build/
36+
sdk/js/build/
3637
site/public/
3738
site/resources/
3839
rootfs.ext4

‎.github/k8s/sam-probe-cronjob-template.yaml‎

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# The cold path, on a schedule: a node with no identity enrolls with the
22
# control plane, authenticates to a router, finds a service on the mesh and
3-
# speaks MCP to it through the mesh, then exits. The long-lived canaries only
4-
# do this when a pod restarts, so between rollouts nobody re-checks that a new
5-
# user could still join. Each run enrolls a fresh identity; the control
6-
# plane's --node-retention sweep reclaims the rows.
3+
# speaks MCP to it through the mesh, then reaches the services the SDK
4+
# canaries serve, and exits. The long-lived canaries only do this when a pod
5+
# restarts, so between rollouts nobody re-checks that a new user could still
6+
# join. Each run enrolls a fresh identity; the control plane's
7+
# --node-retention sweep reclaims the rows.
78
apiVersion: batch/v1
89
kind: CronJob
910
metadata:
@@ -19,8 +20,9 @@ spec:
1920
# One attempt: a retry would hide exactly the flakiness this exists to
2021
# measure. A failed Job is the signal.
2122
backoffLimit: 0
22-
# 180s to be ready plus 120s to reach a provider, with headroom.
23-
activeDeadlineSeconds: 360
23+
# 180s to be ready, 120s to reach a provider, 120s for the SDK
24+
# canaries, with headroom.
25+
activeDeadlineSeconds: 480
2426
template:
2527
metadata:
2628
labels:
@@ -78,6 +80,10 @@ spec:
7880
# The everything canary's MCP server (sam-node-everything-template.yaml):
7981
# a real server, so the node advertises it and initialize succeeds.
8082
value: everything
83+
- name: SDK_SERVICES
84+
# What the SDK canaries publish (sam-sdk-canary-template.yaml),
85+
# each with a `greet` tool and an A2A endpoint.
86+
value: "greeter-js greeter-py"
8187
resources:
8288
requests:
8389
cpu: 10m
@@ -157,8 +163,57 @@ spec:
157163
done
158164
REACH_S=$(( $(date +%s) - REACH_START ))
159165
160-
printf '{"probe":"sam-cold-path","ok":true,"ready_s":%d,"router_latency_ms":%s,"connected_peers":%s,"reach_s":%d,"providers_tried":%d,"call_s":%s,"provider":"%s","elapsed_s":%d}\n' \
161-
"$READY_S" "${ROUTER_MS:-null}" "${PEERS:-null}" "$REACH_S" "$TRIED" "$CALL_S" "$PEER" "$(( $(date +%s) - START ))"
166+
# 4. A service an SDK member serves (sam-sdk-canary-template.yaml)
167+
# is reachable from a node the way an agent behind a node
168+
# reaches it: its MCP tool through the node's own MCP API
169+
# (call_remote_tool opens /sam/mcp/1.0.0 to the provider), and
170+
# its A2A endpoint through the egress proxy. The node's MCP
171+
# endpoint is stateful, so one session is opened first.
172+
MCP=http://localhost/mcp
173+
SESSION=""
174+
mcp_post() {
175+
curl -s --unix-socket "$SOCK" -o "$2" -D /tmp/mcp.hdr -X POST "$MCP" \
176+
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
177+
${SESSION:+-H "Mcp-Session-Id: $SESSION"} -d "$1"
178+
}
179+
mcp_post '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"sam-probe","version":"0"}}}' /tmp/mcp-init.out \
180+
|| fail sdk "initialize on the node's MCP API failed"
181+
SESSION=$(grep -i '^mcp-session-id:' /tmp/mcp.hdr | tr -d '\r' | cut -d' ' -f2)
182+
[ -n "$SESSION" ] || fail sdk "the node's MCP API returned no session"
183+
mcp_post '{"jsonrpc":"2.0","method":"notifications/initialized"}' /dev/null
184+
185+
SDK_START=$(date +%s)
186+
SDK_PROVIDERS=""
187+
for svc in ${SDK_SERVICES}; do
188+
SDK_PEER=""
189+
LAST_ERR=""
190+
while [ -z "$SDK_PEER" ]; do
191+
if curl -sf --unix-socket "$SOCK" -o /tmp/sdk-providers.json \
192+
"http://localhost/sam/service/discover?type=mcp&name=${svc}&timeout=20s"; then
193+
for p in $(grep -oE '"peer_id":"[^"]+"' /tmp/sdk-providers.json | cut -d'"' -f4); do
194+
mcp_post '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"call_remote_tool","arguments":{"peer_id":"'"$p"'","tool_name":"mcp://'"$svc"'/greet","arguments":{"name":"sam-probe"}}}}' /tmp/sdk-call.out
195+
if grep -q 'hello sam-probe' /tmp/sdk-call.out; then
196+
CODE=$(curl -s --unix-socket "$SOCK" -o /tmp/sdk-card.out -w '%{http_code}' "http://localhost/sam/${p}/a2a/${svc}/card")
197+
if [ "$CODE" = "200" ] && grep -q '"caller"' /tmp/sdk-card.out; then
198+
SDK_PEER=$p
199+
break
200+
fi
201+
LAST_ERR="${p}: a2a card HTTP ${CODE} $(head -c 120 /tmp/sdk-card.out | tr -d '"\n')"
202+
else
203+
LAST_ERR="${p}: $(head -c 160 /tmp/sdk-call.out | tr -d '"\n')"
204+
fi
205+
done
206+
fi
207+
[ -n "$SDK_PEER" ] && break
208+
[ $(( $(date +%s) - SDK_START )) -ge 120 ] && fail sdk "no provider of ${svc} answered greet and its card in 120s (last: ${LAST_ERR:-none discovered})"
209+
sleep 5
210+
done
211+
SDK_PROVIDERS="${SDK_PROVIDERS}${SDK_PROVIDERS:+,}\"${svc}\":\"${SDK_PEER}\""
212+
done
213+
SDK_S=$(( $(date +%s) - SDK_START ))
214+
215+
printf '{"probe":"sam-cold-path","ok":true,"ready_s":%d,"router_latency_ms":%s,"connected_peers":%s,"reach_s":%d,"providers_tried":%d,"call_s":%s,"provider":"%s","sdk_s":%d,"sdk_providers":{%s},"elapsed_s":%d}\n' \
216+
"$READY_S" "${ROUTER_MS:-null}" "${PEERS:-null}" "$REACH_S" "$TRIED" "$CALL_S" "$PEER" "$SDK_S" "$SDK_PROVIDERS" "$(( $(date +%s) - START ))"
162217
volumes:
163218
- name: config-volume
164219
configMap:
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Two members with no sam-node beside them: the JavaScript and Python SDK
2+
# example servers (sdk/js/examples/serve.ts, sdk/python/examples/serve.py),
3+
# unchanged, each publishing mcp://greeter-<lang> and a2a://greeter-<lang>.
4+
# They enroll with the pod's projected service account token, the way every
5+
# other canary does, and keep their identity in an emptyDir, so a restart
6+
# resumes and a new pod enrolls afresh. What the mesh sees from them is what a
7+
# user of the packages gets.
8+
apiVersion: apps/v1
9+
kind: Deployment
10+
metadata:
11+
name: js-canary-${ENV_NAME}
12+
namespace: sam-canary-${ENV_NAME}
13+
spec:
14+
replicas: 1
15+
selector:
16+
matchLabels:
17+
app: js-canary-${ENV_NAME}
18+
template:
19+
metadata:
20+
labels:
21+
app: js-canary-${ENV_NAME}
22+
sam-canary: "true"
23+
spec:
24+
serviceAccountName: sam-node-sa
25+
containers:
26+
- name: sdk
27+
image: ghcr.io/google/sam-sdk-js:${IMAGE_TAG}
28+
args: ["build/examples/serve.js", "greeter-js"]
29+
env:
30+
- name: SAM_CONTROL_PLANE_URL
31+
value: "http://sam-control-plane-${ENV_NAME}.${NAMESPACE}.svc.cluster.local:8080"
32+
- name: SAM_INSECURE_CONTROL_PLANE
33+
value: "true"
34+
- name: SAM_JWT_PATH
35+
value: /var/run/secrets/tokens/sam-token
36+
- name: SAM_STATE_DIR
37+
value: /var/run/sam/state
38+
securityContext:
39+
allowPrivilegeEscalation: false
40+
capabilities:
41+
drop: ["ALL"]
42+
resources:
43+
requests:
44+
cpu: 50m
45+
memory: 128Mi
46+
limits:
47+
cpu: 500m
48+
memory: 512Mi
49+
volumeMounts:
50+
- name: sam-token
51+
mountPath: /var/run/secrets/tokens
52+
readOnly: true
53+
- name: sam-state
54+
mountPath: /var/run/sam
55+
volumes:
56+
- name: sam-state
57+
emptyDir: {}
58+
- name: sam-token
59+
projected:
60+
sources:
61+
- serviceAccountToken:
62+
path: sam-token
63+
expirationSeconds: 3600
64+
audience: "sam-control-plane-audience"
65+
---
66+
apiVersion: apps/v1
67+
kind: Deployment
68+
metadata:
69+
name: python-canary-${ENV_NAME}
70+
namespace: sam-canary-${ENV_NAME}
71+
spec:
72+
replicas: 1
73+
selector:
74+
matchLabels:
75+
app: python-canary-${ENV_NAME}
76+
template:
77+
metadata:
78+
labels:
79+
app: python-canary-${ENV_NAME}
80+
sam-canary: "true"
81+
spec:
82+
serviceAccountName: sam-node-sa
83+
containers:
84+
- name: sdk
85+
image: ghcr.io/google/sam-sdk-python:${IMAGE_TAG}
86+
args: ["examples/serve.py", "greeter-py"]
87+
env:
88+
- name: SAM_CONTROL_PLANE_URL
89+
value: "http://sam-control-plane-${ENV_NAME}.${NAMESPACE}.svc.cluster.local:8080"
90+
- name: SAM_INSECURE_CONTROL_PLANE
91+
value: "true"
92+
- name: SAM_JWT_PATH
93+
value: /var/run/secrets/tokens/sam-token
94+
- name: SAM_STATE_DIR
95+
value: /var/run/sam/state
96+
- name: PYTHONUNBUFFERED
97+
value: "1"
98+
securityContext:
99+
allowPrivilegeEscalation: false
100+
capabilities:
101+
drop: ["ALL"]
102+
resources:
103+
requests:
104+
cpu: 50m
105+
memory: 128Mi
106+
limits:
107+
cpu: 500m
108+
memory: 512Mi
109+
volumeMounts:
110+
- name: sam-token
111+
mountPath: /var/run/secrets/tokens
112+
readOnly: true
113+
- name: sam-state
114+
mountPath: /var/run/sam
115+
volumes:
116+
- name: sam-state
117+
emptyDir: {}
118+
- name: sam-token
119+
projected:
120+
sources:
121+
- serviceAccountToken:
122+
path: sam-token
123+
expirationSeconds: 3600
124+
audience: "sam-control-plane-audience"

0 commit comments

Comments
 (0)