sam-mesh 0.1.0rc4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. sam_mesh-0.1.0rc4/.gitignore +69 -0
  2. sam_mesh-0.1.0rc4/PKG-INFO +209 -0
  3. sam_mesh-0.1.0rc4/README.md +187 -0
  4. sam_mesh-0.1.0rc4/examples/call.py +65 -0
  5. sam_mesh-0.1.0rc4/examples/serve.py +64 -0
  6. sam_mesh-0.1.0rc4/proto/circuit.proto +77 -0
  7. sam_mesh-0.1.0rc4/pyproject.toml +40 -0
  8. sam_mesh-0.1.0rc4/src/agent_mesh/__init__.py +125 -0
  9. sam_mesh-0.1.0rc4/src/agent_mesh/_gen/__init__.py +1 -0
  10. sam_mesh-0.1.0rc4/src/agent_mesh/_gen/datalog.json +54 -0
  11. sam_mesh-0.1.0rc4/src/agent_mesh/_proto/__init__.py +1 -0
  12. sam_mesh-0.1.0rc4/src/agent_mesh/_proto/circuit_pb2.py +39 -0
  13. sam_mesh-0.1.0rc4/src/agent_mesh/_proto/circuit_pb2.pyi +80 -0
  14. sam_mesh-0.1.0rc4/src/agent_mesh/_proto/sam_pb2.py +141 -0
  15. sam_mesh-0.1.0rc4/src/agent_mesh/_proto/sam_pb2.pyi +562 -0
  16. sam_mesh-0.1.0rc4/src/agent_mesh/auth.py +111 -0
  17. sam_mesh-0.1.0rc4/src/agent_mesh/authorizer.py +168 -0
  18. sam_mesh-0.1.0rc4/src/agent_mesh/base58.py +40 -0
  19. sam_mesh-0.1.0rc4/src/agent_mesh/biscuit.py +135 -0
  20. sam_mesh-0.1.0rc4/src/agent_mesh/challenges.py +43 -0
  21. sam_mesh-0.1.0rc4/src/agent_mesh/conformance.py +81 -0
  22. sam_mesh-0.1.0rc4/src/agent_mesh/conformance_join.py +217 -0
  23. sam_mesh-0.1.0rc4/src/agent_mesh/controlplane.py +350 -0
  24. sam_mesh-0.1.0rc4/src/agent_mesh/credential.py +121 -0
  25. sam_mesh-0.1.0rc4/src/agent_mesh/discovery.py +196 -0
  26. sam_mesh-0.1.0rc4/src/agent_mesh/host.py +114 -0
  27. sam_mesh-0.1.0rc4/src/agent_mesh/identity.py +131 -0
  28. sam_mesh-0.1.0rc4/src/agent_mesh/mcp_client.py +169 -0
  29. sam_mesh-0.1.0rc4/src/agent_mesh/mesh.py +284 -0
  30. sam_mesh-0.1.0rc4/src/agent_mesh/relay.py +130 -0
  31. sam_mesh-0.1.0rc4/src/agent_mesh/serve.py +503 -0
  32. sam_mesh-0.1.0rc4/src/agent_mesh/session.py +577 -0
  33. sam_mesh-0.1.0rc4/src/agent_mesh/sync.py +97 -0
  34. sam_mesh-0.1.0rc4/tests/__init__.py +0 -0
  35. sam_mesh-0.1.0rc4/tests/test_authorizer.py +145 -0
  36. sam_mesh-0.1.0rc4/tests/test_biscuit.py +79 -0
  37. sam_mesh-0.1.0rc4/tests/test_controlplane.py +246 -0
  38. sam_mesh-0.1.0rc4/tests/test_host.py +116 -0
  39. sam_mesh-0.1.0rc4/tests/test_identity.py +86 -0
  40. sam_mesh-0.1.0rc4/tests/test_mcp.py +218 -0
  41. sam_mesh-0.1.0rc4/tests/test_mesh.py +195 -0
  42. sam_mesh-0.1.0rc4/tests/test_serve.py +212 -0
  43. sam_mesh-0.1.0rc4/tests/test_session.py +223 -0
  44. sam_mesh-0.1.0rc4/tests/test_sync.py +294 -0
@@ -0,0 +1,69 @@
1
+ # If you prefer the allow list template instead of the deny list, see community template:
2
+ # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3
+ #
4
+ # Binaries for programs and plugins
5
+ *.exe
6
+ *.exe~
7
+ *.dll
8
+ *.so
9
+ *.dylib
10
+ *.pyc
11
+
12
+ # Test binary, built with `go test -c`
13
+ *.test
14
+
15
+ # Code coverage profiles and other test artifacts
16
+ *.out
17
+ coverage.*
18
+ *.coverprofile
19
+ profile.cov
20
+
21
+ # Dependency directories (remove the comment below to include it)
22
+ # vendor/
23
+
24
+ # Go workspace file
25
+ go.work
26
+ go.work.sum
27
+
28
+ # env file
29
+ .env
30
+
31
+ # Editor/IDE
32
+ # .idea/
33
+ # .vscode/
34
+ #
35
+ bin/
36
+ dist/
37
+ # Stray binaries from `go build ./cmd/<name>/` in the repo root
38
+ /sam-node
39
+ /sam-box
40
+ /sam-router
41
+ /sam-console
42
+
43
+ # Playwright console UI tests
44
+ node_modules/
45
+ tests/ui/playwright-report/
46
+ tests/ui/test-results/
47
+ /sam-control-plane
48
+ /mcp-client
49
+ /nano-init
50
+ tests/e2e/logs/
51
+ tests/integration/logs/
52
+ tests/integration/scratch/
53
+ __pycache__/
54
+ .venv/
55
+ *.egg-info/
56
+ keys.db
57
+ mobile/logcat.txt
58
+
59
+ # Guest images built by scripts/build-rootfs.sh; gigabytes, and reproducible
60
+ # from the script, so they belong in a bucket rather than in history.
61
+ /rootfs.ext4
62
+ /rootfs.tar
63
+
64
+ # Android upload keystore and Firebase config: secrets, wherever they land.
65
+ *.jks
66
+ *.keystore
67
+ key.properties
68
+ google-services.json
69
+
@@ -0,0 +1,209 @@
1
+ Metadata-Version: 2.5
2
+ Name: sam-mesh
3
+ Version: 0.1.0-rc.4
4
+ Summary: Native SDK for joining a SAM agent mesh from inside a Python agent process
5
+ Project-URL: Homepage, https://github.com/google/sam
6
+ Project-URL: Source, https://github.com/google/sam/tree/main/sdk/python
7
+ License-Expression: Apache-2.0
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: anyio>=4
10
+ Requires-Dist: biscuit-python>=0.4
11
+ Requires-Dist: cryptography>=42
12
+ Requires-Dist: h11>=0.14
13
+ Requires-Dist: httpx>=0.27
14
+ Requires-Dist: libp2p<0.8,>=0.7
15
+ Requires-Dist: mcp<3,>=2
16
+ Requires-Dist: multiaddr>=0.2
17
+ Requires-Dist: protobuf<8,>=4.25
18
+ Requires-Dist: trio>=0.26
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest>=8; extra == 'test'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # sam-mesh (Python)
24
+
25
+ Native Python SDK for joining a SAM agent mesh from inside the agent
26
+ process. It replaces the `sam-node` sidecar for agents written in Python:
27
+ the agent enrolls with the control plane, joins the mesh through a router,
28
+ finds services and calls them, publishes services of its own, and follows
29
+ the control plane's keys, bans and policy while it runs. Import it as
30
+ `agent_mesh`.
31
+
32
+ Guide: [sam-mesh.dev/docs/guides/native-sdks](https://sam-mesh.dev/docs/guides/native-sdks/),
33
+ from an empty machine to two programs on a mesh.
34
+ Source: [github.com/google/sam/tree/main/sdk/python](https://github.com/google/sam/tree/main/sdk/python).
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install sam-mesh
40
+ ```
41
+
42
+ Python 3.11 or later. The SDK runs on trio (py-libp2p is trio-based); under
43
+ asyncio, use it through `anyio` with the trio backend. One dependency,
44
+ `fastecdsa`, builds from source against GMP (`libgmp-dev` on Debian).
45
+
46
+ ## Use
47
+
48
+ Both programs below are in
49
+ [`examples/`](https://github.com/google/sam/tree/main/sdk/python/examples)
50
+ and run against a real mesh in the repository's tests. They read the mesh
51
+ from `SAM_CONTROL_PLANE_URL` and the enrollment token from
52
+ `SAM_BOOTSTRAP_TOKEN_PATH`; the guide shows how to get both from `sam-one`
53
+ or from the operator of an existing mesh.
54
+
55
+ Find a service and call it:
56
+
57
+ <!-- embed: sdk/python/examples/call.py -->
58
+ ```python
59
+ """Finds a service on the mesh and calls it: a tool of an MCP service, or a
60
+ path of an inference or A2A service.
61
+
62
+ python call.py mcp://greeter greet '{"name": "Ada"}'
63
+ python call.py a2a://greeter /card
64
+ python call.py inference://ollama /v1/models
65
+
66
+ SAM_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
67
+ SAM_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or SAM_JWT_PATH
68
+ (a workload identity token your platform issues, such as a Kubernetes
69
+ projected service account token), and keeps the identity and credential in
70
+ SAM_STATE_DIR; later runs resume from there without it.
71
+ """
72
+
73
+ import json
74
+ import os
75
+ import sys
76
+
77
+ import trio
78
+ from agent_mesh import AgentMesh
79
+
80
+ argv = sys.argv[1:]
81
+ service = argv[0] if len(argv) > 0 else "mcp://greeter"
82
+ tool_or_path = argv[1] if len(argv) > 1 else "greet"
83
+ args = json.loads(argv[2]) if len(argv) > 2 else {"name": "world"}
84
+
85
+ mesh = AgentMesh.enroll(
86
+ os.environ.get("SAM_CONTROL_PLANE_URL", "https://mesh.example.com"),
87
+ bootstrap_token_path=os.environ.get("SAM_BOOTSTRAP_TOKEN_PATH"),
88
+ jwt_path=os.environ.get("SAM_JWT_PATH"),
89
+ state_dir=os.environ.get("SAM_STATE_DIR", "~/.config/sam-mesh/caller"),
90
+ # A plaintext http:// control plane is otherwise accepted only on loopback.
91
+ allow_insecure=os.environ.get("SAM_INSECURE_CONTROL_PLANE") == "true",
92
+ )
93
+
94
+
95
+ async def main() -> None:
96
+ async with mesh.join() as session:
97
+ print(f"on the mesh as {session.peer_id}")
98
+
99
+ providers = await session.discover(service)
100
+ if not providers:
101
+ raise SystemExit(f"no member of the mesh serves {service}")
102
+ # A provider record can outlive its member; the first that answers is used.
103
+ for provider in providers:
104
+ try:
105
+ await session.connect(provider)
106
+ break
107
+ except (ConnectionError, PermissionError) as err:
108
+ print(f"{provider.peer_id}: {err}", file=sys.stderr)
109
+ else:
110
+ raise SystemExit(f"no provider of {service} is reachable")
111
+ print(f"{service} is served by {provider.peer_id}")
112
+
113
+ if service.startswith("mcp://"):
114
+ tools = await session.list_tools(provider, service)
115
+ print("tools:", ", ".join(t.name for t in tools))
116
+ result = await session.call_tool(provider, service, tool_or_path, args)
117
+ print("\n".join(result.text))
118
+ else:
119
+ response = await session.request(provider, service, tool_or_path)
120
+ print(response.status, response.text)
121
+
122
+
123
+ trio.run(main)
124
+ ```
125
+ <!-- /embed -->
126
+
127
+ Publish services of your own:
128
+
129
+ <!-- embed: sdk/python/examples/serve.py -->
130
+ ```python
131
+ """Publishes services on the mesh and answers callers until stopped: an MCP
132
+ tool, an A2A endpoint and, when OLLAMA_URL is set, the Ollama server running
133
+ beside this program as an inference service. The mesh policy decides which
134
+ members may call; the SDK turns the others away before anything reaches this
135
+ code or Ollama.
136
+
137
+ python serve.py # publishes mcp://greeter and a2a://greeter
138
+ python serve.py greeter-2 # the same under another name
139
+
140
+ SAM_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
141
+ SAM_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or SAM_JWT_PATH
142
+ (a workload identity token your platform issues, such as a Kubernetes
143
+ projected service account token), and keeps the identity and credential in
144
+ SAM_STATE_DIR; later runs resume from there without it.
145
+ """
146
+
147
+ import json
148
+ import os
149
+ import sys
150
+
151
+ import trio
152
+ from agent_mesh import AgentMesh, HTTPRequest, HTTPResponse, HTTPService, MCPService, VerifiedBiscuit
153
+ from mcp.server.mcpserver import MCPServer
154
+
155
+ service_name = sys.argv[1] if len(sys.argv) > 1 else "greeter"
156
+
157
+ mesh = AgentMesh.enroll(
158
+ os.environ.get("SAM_CONTROL_PLANE_URL", "https://mesh.example.com"),
159
+ bootstrap_token_path=os.environ.get("SAM_BOOTSTRAP_TOKEN_PATH"),
160
+ jwt_path=os.environ.get("SAM_JWT_PATH"),
161
+ state_dir=os.environ.get("SAM_STATE_DIR", f"~/.config/sam-mesh/{service_name}"),
162
+ # A plaintext http:// control plane is otherwise accepted only on loopback.
163
+ allow_insecure=os.environ.get("SAM_INSECURE_CONTROL_PLANE") == "true",
164
+ )
165
+
166
+
167
+ def create_server() -> MCPServer:
168
+ server = MCPServer(service_name)
169
+
170
+ @server.tool(description="Greets someone by name")
171
+ def greet(name: str) -> str:
172
+ return f"hello {name}"
173
+
174
+ return server
175
+
176
+
177
+ async def card(request: HTTPRequest, caller: VerifiedBiscuit) -> HTTPResponse:
178
+ body = json.dumps({"name": service_name, "path": request.path, "caller": caller.peer_id})
179
+ return HTTPResponse(status=200, headers={"content-type": "application/json"}, body=body.encode())
180
+
181
+
182
+ async def main() -> None:
183
+ async with mesh.join() as session:
184
+ await session.serve(MCPService(name=service_name, create_server=create_server))
185
+ await session.serve(HTTPService(type="a2a", name=service_name, target=card))
186
+ if "OLLAMA_URL" in os.environ:
187
+ await session.serve(HTTPService(type="inference", name="ollama", target=os.environ["OLLAMA_URL"]))
188
+
189
+ served = ", ".join(f"{t}://{n}" for t, n, _ in session.services.list())
190
+ print(f"serving {served} as {session.peer_id}", flush=True)
191
+ await trio.sleep_forever()
192
+
193
+
194
+ trio.run(main)
195
+ ```
196
+ <!-- /embed -->
197
+
198
+ `enroll` reuses the identity and credential saved in `state_dir` when they
199
+ are still valid for that control plane, and needs exactly one of
200
+ `bootstrap_token_path`, `bootstrap_token` or `jwt` otherwise. Read tokens
201
+ from a file or the environment; do not put them on a command line.
202
+
203
+ A plaintext `http://` control plane is accepted only on loopback. Pass
204
+ `allow_insecure=True` for a network you trust.
205
+
206
+ ## License
207
+
208
+ Apache-2.0. Issues and contributions at
209
+ [github.com/google/sam](https://github.com/google/sam).
@@ -0,0 +1,187 @@
1
+ # sam-mesh (Python)
2
+
3
+ Native Python SDK for joining a SAM agent mesh from inside the agent
4
+ process. It replaces the `sam-node` sidecar for agents written in Python:
5
+ the agent enrolls with the control plane, joins the mesh through a router,
6
+ finds services and calls them, publishes services of its own, and follows
7
+ the control plane's keys, bans and policy while it runs. Import it as
8
+ `agent_mesh`.
9
+
10
+ Guide: [sam-mesh.dev/docs/guides/native-sdks](https://sam-mesh.dev/docs/guides/native-sdks/),
11
+ from an empty machine to two programs on a mesh.
12
+ Source: [github.com/google/sam/tree/main/sdk/python](https://github.com/google/sam/tree/main/sdk/python).
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install sam-mesh
18
+ ```
19
+
20
+ Python 3.11 or later. The SDK runs on trio (py-libp2p is trio-based); under
21
+ asyncio, use it through `anyio` with the trio backend. One dependency,
22
+ `fastecdsa`, builds from source against GMP (`libgmp-dev` on Debian).
23
+
24
+ ## Use
25
+
26
+ Both programs below are in
27
+ [`examples/`](https://github.com/google/sam/tree/main/sdk/python/examples)
28
+ and run against a real mesh in the repository's tests. They read the mesh
29
+ from `SAM_CONTROL_PLANE_URL` and the enrollment token from
30
+ `SAM_BOOTSTRAP_TOKEN_PATH`; the guide shows how to get both from `sam-one`
31
+ or from the operator of an existing mesh.
32
+
33
+ Find a service and call it:
34
+
35
+ <!-- embed: sdk/python/examples/call.py -->
36
+ ```python
37
+ """Finds a service on the mesh and calls it: a tool of an MCP service, or a
38
+ path of an inference or A2A service.
39
+
40
+ python call.py mcp://greeter greet '{"name": "Ada"}'
41
+ python call.py a2a://greeter /card
42
+ python call.py inference://ollama /v1/models
43
+
44
+ SAM_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
45
+ SAM_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or SAM_JWT_PATH
46
+ (a workload identity token your platform issues, such as a Kubernetes
47
+ projected service account token), and keeps the identity and credential in
48
+ SAM_STATE_DIR; later runs resume from there without it.
49
+ """
50
+
51
+ import json
52
+ import os
53
+ import sys
54
+
55
+ import trio
56
+ from agent_mesh import AgentMesh
57
+
58
+ argv = sys.argv[1:]
59
+ service = argv[0] if len(argv) > 0 else "mcp://greeter"
60
+ tool_or_path = argv[1] if len(argv) > 1 else "greet"
61
+ args = json.loads(argv[2]) if len(argv) > 2 else {"name": "world"}
62
+
63
+ mesh = AgentMesh.enroll(
64
+ os.environ.get("SAM_CONTROL_PLANE_URL", "https://mesh.example.com"),
65
+ bootstrap_token_path=os.environ.get("SAM_BOOTSTRAP_TOKEN_PATH"),
66
+ jwt_path=os.environ.get("SAM_JWT_PATH"),
67
+ state_dir=os.environ.get("SAM_STATE_DIR", "~/.config/sam-mesh/caller"),
68
+ # A plaintext http:// control plane is otherwise accepted only on loopback.
69
+ allow_insecure=os.environ.get("SAM_INSECURE_CONTROL_PLANE") == "true",
70
+ )
71
+
72
+
73
+ async def main() -> None:
74
+ async with mesh.join() as session:
75
+ print(f"on the mesh as {session.peer_id}")
76
+
77
+ providers = await session.discover(service)
78
+ if not providers:
79
+ raise SystemExit(f"no member of the mesh serves {service}")
80
+ # A provider record can outlive its member; the first that answers is used.
81
+ for provider in providers:
82
+ try:
83
+ await session.connect(provider)
84
+ break
85
+ except (ConnectionError, PermissionError) as err:
86
+ print(f"{provider.peer_id}: {err}", file=sys.stderr)
87
+ else:
88
+ raise SystemExit(f"no provider of {service} is reachable")
89
+ print(f"{service} is served by {provider.peer_id}")
90
+
91
+ if service.startswith("mcp://"):
92
+ tools = await session.list_tools(provider, service)
93
+ print("tools:", ", ".join(t.name for t in tools))
94
+ result = await session.call_tool(provider, service, tool_or_path, args)
95
+ print("\n".join(result.text))
96
+ else:
97
+ response = await session.request(provider, service, tool_or_path)
98
+ print(response.status, response.text)
99
+
100
+
101
+ trio.run(main)
102
+ ```
103
+ <!-- /embed -->
104
+
105
+ Publish services of your own:
106
+
107
+ <!-- embed: sdk/python/examples/serve.py -->
108
+ ```python
109
+ """Publishes services on the mesh and answers callers until stopped: an MCP
110
+ tool, an A2A endpoint and, when OLLAMA_URL is set, the Ollama server running
111
+ beside this program as an inference service. The mesh policy decides which
112
+ members may call; the SDK turns the others away before anything reaches this
113
+ code or Ollama.
114
+
115
+ python serve.py # publishes mcp://greeter and a2a://greeter
116
+ python serve.py greeter-2 # the same under another name
117
+
118
+ SAM_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
119
+ SAM_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or SAM_JWT_PATH
120
+ (a workload identity token your platform issues, such as a Kubernetes
121
+ projected service account token), and keeps the identity and credential in
122
+ SAM_STATE_DIR; later runs resume from there without it.
123
+ """
124
+
125
+ import json
126
+ import os
127
+ import sys
128
+
129
+ import trio
130
+ from agent_mesh import AgentMesh, HTTPRequest, HTTPResponse, HTTPService, MCPService, VerifiedBiscuit
131
+ from mcp.server.mcpserver import MCPServer
132
+
133
+ service_name = sys.argv[1] if len(sys.argv) > 1 else "greeter"
134
+
135
+ mesh = AgentMesh.enroll(
136
+ os.environ.get("SAM_CONTROL_PLANE_URL", "https://mesh.example.com"),
137
+ bootstrap_token_path=os.environ.get("SAM_BOOTSTRAP_TOKEN_PATH"),
138
+ jwt_path=os.environ.get("SAM_JWT_PATH"),
139
+ state_dir=os.environ.get("SAM_STATE_DIR", f"~/.config/sam-mesh/{service_name}"),
140
+ # A plaintext http:// control plane is otherwise accepted only on loopback.
141
+ allow_insecure=os.environ.get("SAM_INSECURE_CONTROL_PLANE") == "true",
142
+ )
143
+
144
+
145
+ def create_server() -> MCPServer:
146
+ server = MCPServer(service_name)
147
+
148
+ @server.tool(description="Greets someone by name")
149
+ def greet(name: str) -> str:
150
+ return f"hello {name}"
151
+
152
+ return server
153
+
154
+
155
+ async def card(request: HTTPRequest, caller: VerifiedBiscuit) -> HTTPResponse:
156
+ body = json.dumps({"name": service_name, "path": request.path, "caller": caller.peer_id})
157
+ return HTTPResponse(status=200, headers={"content-type": "application/json"}, body=body.encode())
158
+
159
+
160
+ async def main() -> None:
161
+ async with mesh.join() as session:
162
+ await session.serve(MCPService(name=service_name, create_server=create_server))
163
+ await session.serve(HTTPService(type="a2a", name=service_name, target=card))
164
+ if "OLLAMA_URL" in os.environ:
165
+ await session.serve(HTTPService(type="inference", name="ollama", target=os.environ["OLLAMA_URL"]))
166
+
167
+ served = ", ".join(f"{t}://{n}" for t, n, _ in session.services.list())
168
+ print(f"serving {served} as {session.peer_id}", flush=True)
169
+ await trio.sleep_forever()
170
+
171
+
172
+ trio.run(main)
173
+ ```
174
+ <!-- /embed -->
175
+
176
+ `enroll` reuses the identity and credential saved in `state_dir` when they
177
+ are still valid for that control plane, and needs exactly one of
178
+ `bootstrap_token_path`, `bootstrap_token` or `jwt` otherwise. Read tokens
179
+ from a file or the environment; do not put them on a command line.
180
+
181
+ A plaintext `http://` control plane is accepted only on loopback. Pass
182
+ `allow_insecure=True` for a network you trust.
183
+
184
+ ## License
185
+
186
+ Apache-2.0. Issues and contributions at
187
+ [github.com/google/sam](https://github.com/google/sam).
@@ -0,0 +1,65 @@
1
+ """Finds a service on the mesh and calls it: a tool of an MCP service, or a
2
+ path of an inference or A2A service.
3
+
4
+ python call.py mcp://greeter greet '{"name": "Ada"}'
5
+ python call.py a2a://greeter /card
6
+ python call.py inference://ollama /v1/models
7
+
8
+ SAM_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
9
+ SAM_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or SAM_JWT_PATH
10
+ (a workload identity token your platform issues, such as a Kubernetes
11
+ projected service account token), and keeps the identity and credential in
12
+ SAM_STATE_DIR; later runs resume from there without it.
13
+ """
14
+
15
+ import json
16
+ import os
17
+ import sys
18
+
19
+ import trio
20
+ from agent_mesh import AgentMesh
21
+
22
+ argv = sys.argv[1:]
23
+ service = argv[0] if len(argv) > 0 else "mcp://greeter"
24
+ tool_or_path = argv[1] if len(argv) > 1 else "greet"
25
+ args = json.loads(argv[2]) if len(argv) > 2 else {"name": "world"}
26
+
27
+ mesh = AgentMesh.enroll(
28
+ os.environ.get("SAM_CONTROL_PLANE_URL", "https://mesh.example.com"),
29
+ bootstrap_token_path=os.environ.get("SAM_BOOTSTRAP_TOKEN_PATH"),
30
+ jwt_path=os.environ.get("SAM_JWT_PATH"),
31
+ state_dir=os.environ.get("SAM_STATE_DIR", "~/.config/sam-mesh/caller"),
32
+ # A plaintext http:// control plane is otherwise accepted only on loopback.
33
+ allow_insecure=os.environ.get("SAM_INSECURE_CONTROL_PLANE") == "true",
34
+ )
35
+
36
+
37
+ async def main() -> None:
38
+ async with mesh.join() as session:
39
+ print(f"on the mesh as {session.peer_id}")
40
+
41
+ providers = await session.discover(service)
42
+ if not providers:
43
+ raise SystemExit(f"no member of the mesh serves {service}")
44
+ # A provider record can outlive its member; the first that answers is used.
45
+ for provider in providers:
46
+ try:
47
+ await session.connect(provider)
48
+ break
49
+ except (ConnectionError, PermissionError) as err:
50
+ print(f"{provider.peer_id}: {err}", file=sys.stderr)
51
+ else:
52
+ raise SystemExit(f"no provider of {service} is reachable")
53
+ print(f"{service} is served by {provider.peer_id}")
54
+
55
+ if service.startswith("mcp://"):
56
+ tools = await session.list_tools(provider, service)
57
+ print("tools:", ", ".join(t.name for t in tools))
58
+ result = await session.call_tool(provider, service, tool_or_path, args)
59
+ print("\n".join(result.text))
60
+ else:
61
+ response = await session.request(provider, service, tool_or_path)
62
+ print(response.status, response.text)
63
+
64
+
65
+ trio.run(main)
@@ -0,0 +1,64 @@
1
+ """Publishes services on the mesh and answers callers until stopped: an MCP
2
+ tool, an A2A endpoint and, when OLLAMA_URL is set, the Ollama server running
3
+ beside this program as an inference service. The mesh policy decides which
4
+ members may call; the SDK turns the others away before anything reaches this
5
+ code or Ollama.
6
+
7
+ python serve.py # publishes mcp://greeter and a2a://greeter
8
+ python serve.py greeter-2 # the same under another name
9
+
10
+ SAM_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
11
+ SAM_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or SAM_JWT_PATH
12
+ (a workload identity token your platform issues, such as a Kubernetes
13
+ projected service account token), and keeps the identity and credential in
14
+ SAM_STATE_DIR; later runs resume from there without it.
15
+ """
16
+
17
+ import json
18
+ import os
19
+ import sys
20
+
21
+ import trio
22
+ from agent_mesh import AgentMesh, HTTPRequest, HTTPResponse, HTTPService, MCPService, VerifiedBiscuit
23
+ from mcp.server.mcpserver import MCPServer
24
+
25
+ service_name = sys.argv[1] if len(sys.argv) > 1 else "greeter"
26
+
27
+ mesh = AgentMesh.enroll(
28
+ os.environ.get("SAM_CONTROL_PLANE_URL", "https://mesh.example.com"),
29
+ bootstrap_token_path=os.environ.get("SAM_BOOTSTRAP_TOKEN_PATH"),
30
+ jwt_path=os.environ.get("SAM_JWT_PATH"),
31
+ state_dir=os.environ.get("SAM_STATE_DIR", f"~/.config/sam-mesh/{service_name}"),
32
+ # A plaintext http:// control plane is otherwise accepted only on loopback.
33
+ allow_insecure=os.environ.get("SAM_INSECURE_CONTROL_PLANE") == "true",
34
+ )
35
+
36
+
37
+ def create_server() -> MCPServer:
38
+ server = MCPServer(service_name)
39
+
40
+ @server.tool(description="Greets someone by name")
41
+ def greet(name: str) -> str:
42
+ return f"hello {name}"
43
+
44
+ return server
45
+
46
+
47
+ async def card(request: HTTPRequest, caller: VerifiedBiscuit) -> HTTPResponse:
48
+ body = json.dumps({"name": service_name, "path": request.path, "caller": caller.peer_id})
49
+ return HTTPResponse(status=200, headers={"content-type": "application/json"}, body=body.encode())
50
+
51
+
52
+ async def main() -> None:
53
+ async with mesh.join() as session:
54
+ await session.serve(MCPService(name=service_name, create_server=create_server))
55
+ await session.serve(HTTPService(type="a2a", name=service_name, target=card))
56
+ if "OLLAMA_URL" in os.environ:
57
+ await session.serve(HTTPService(type="inference", name="ollama", target=os.environ["OLLAMA_URL"]))
58
+
59
+ served = ", ".join(f"{t}://{n}" for t, n, _ in session.services.list())
60
+ print(f"serving {served} as {session.peer_id}", flush=True)
61
+ await trio.sleep_forever()
62
+
63
+
64
+ trio.run(main)
@@ -0,0 +1,77 @@
1
+ // Copyright 2026 Google LLC
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ // The libp2p circuit relay v2 wire format, copied from go-libp2p
16
+ // (p2p/protocol/circuitv2/pb/circuit.proto) so a client that only needs a
17
+ // reservation can speak to a router without depending on a relay client
18
+ // implementation. Messages travel varint-length-prefixed on the
19
+ // /libp2p/circuit/relay/0.2.0/hop stream.
20
+
21
+ syntax = "proto3";
22
+
23
+ package circuit.pb;
24
+
25
+ message HopMessage {
26
+ enum Type {
27
+ RESERVE = 0;
28
+ CONNECT = 1;
29
+ STATUS = 2;
30
+ }
31
+
32
+ optional Type type = 1;
33
+ optional Peer peer = 2;
34
+ optional Reservation reservation = 3;
35
+ optional Limit limit = 4;
36
+ optional Status status = 5;
37
+ }
38
+
39
+ message StopMessage {
40
+ enum Type {
41
+ CONNECT = 0;
42
+ STATUS = 1;
43
+ }
44
+
45
+ optional Type type = 1;
46
+ optional Peer peer = 2;
47
+ optional Limit limit = 3;
48
+ optional Status status = 4;
49
+ }
50
+
51
+ message Peer {
52
+ optional bytes id = 1;
53
+ repeated bytes addrs = 2;
54
+ }
55
+
56
+ message Reservation {
57
+ optional uint64 expire = 1; // Unix expiration time (UTC)
58
+ repeated bytes addrs = 2; // relay addrs for reserving peer
59
+ optional bytes voucher = 3; // reservation voucher
60
+ }
61
+
62
+ message Limit {
63
+ optional uint32 duration = 1; // seconds
64
+ optional uint64 data = 2; // bytes
65
+ }
66
+
67
+ enum Status {
68
+ UNUSED = 0;
69
+ OK = 100;
70
+ RESERVATION_REFUSED = 200;
71
+ RESOURCE_LIMIT_EXCEEDED = 201;
72
+ PERMISSION_DENIED = 202;
73
+ CONNECTION_FAILED = 203;
74
+ NO_RESERVATION = 204;
75
+ MALFORMED_MESSAGE = 400;
76
+ UNEXPECTED_MESSAGE = 401;
77
+ }