agentmesh-p2p 0.1.0rc10__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 (50) hide show
  1. agentmesh_p2p-0.1.0rc10/.gitignore +78 -0
  2. agentmesh_p2p-0.1.0rc10/PKG-INFO +216 -0
  3. agentmesh_p2p-0.1.0rc10/README.md +191 -0
  4. agentmesh_p2p-0.1.0rc10/examples/a2a_agent.py +92 -0
  5. agentmesh_p2p-0.1.0rc10/examples/a2a_call.py +60 -0
  6. agentmesh_p2p-0.1.0rc10/examples/agent.py +50 -0
  7. agentmesh_p2p-0.1.0rc10/examples/call.py +76 -0
  8. agentmesh_p2p-0.1.0rc10/proto/circuit.proto +77 -0
  9. agentmesh_p2p-0.1.0rc10/pyproject.toml +46 -0
  10. agentmesh_p2p-0.1.0rc10/src/agent_mesh/__init__.py +225 -0
  11. agentmesh_p2p-0.1.0rc10/src/agent_mesh/_gen/__init__.py +1 -0
  12. agentmesh_p2p-0.1.0rc10/src/agent_mesh/_gen/datalog.json +82 -0
  13. agentmesh_p2p-0.1.0rc10/src/agent_mesh/_proto/__init__.py +1 -0
  14. agentmesh_p2p-0.1.0rc10/src/agent_mesh/_proto/agentmesh_pb2.py +222 -0
  15. agentmesh_p2p-0.1.0rc10/src/agent_mesh/_proto/agentmesh_pb2.pyi +1002 -0
  16. agentmesh_p2p-0.1.0rc10/src/agent_mesh/_proto/circuit_pb2.py +39 -0
  17. agentmesh_p2p-0.1.0rc10/src/agent_mesh/_proto/circuit_pb2.pyi +80 -0
  18. agentmesh_p2p-0.1.0rc10/src/agent_mesh/auth.py +186 -0
  19. agentmesh_p2p-0.1.0rc10/src/agent_mesh/authorizer.py +196 -0
  20. agentmesh_p2p-0.1.0rc10/src/agent_mesh/base58.py +40 -0
  21. agentmesh_p2p-0.1.0rc10/src/agent_mesh/biscuit.py +330 -0
  22. agentmesh_p2p-0.1.0rc10/src/agent_mesh/challenges.py +64 -0
  23. agentmesh_p2p-0.1.0rc10/src/agent_mesh/conformance.py +81 -0
  24. agentmesh_p2p-0.1.0rc10/src/agent_mesh/conformance_join.py +265 -0
  25. agentmesh_p2p-0.1.0rc10/src/agent_mesh/controlplane.py +434 -0
  26. agentmesh_p2p-0.1.0rc10/src/agent_mesh/credential.py +137 -0
  27. agentmesh_p2p-0.1.0rc10/src/agent_mesh/discovery.py +179 -0
  28. agentmesh_p2p-0.1.0rc10/src/agent_mesh/host.py +220 -0
  29. agentmesh_p2p-0.1.0rc10/src/agent_mesh/httpx_transport.py +98 -0
  30. agentmesh_p2p-0.1.0rc10/src/agent_mesh/identity.py +131 -0
  31. agentmesh_p2p-0.1.0rc10/src/agent_mesh/libp2p_http.py +617 -0
  32. agentmesh_p2p-0.1.0rc10/src/agent_mesh/mcp_client.py +198 -0
  33. agentmesh_p2p-0.1.0rc10/src/agent_mesh/mesh.py +402 -0
  34. agentmesh_p2p-0.1.0rc10/src/agent_mesh/relay.py +152 -0
  35. agentmesh_p2p-0.1.0rc10/src/agent_mesh/session.py +1381 -0
  36. agentmesh_p2p-0.1.0rc10/src/agent_mesh/sync.py +97 -0
  37. agentmesh_p2p-0.1.0rc10/src/agent_mesh/tar.py +352 -0
  38. agentmesh_p2p-0.1.0rc10/tests/__init__.py +0 -0
  39. agentmesh_p2p-0.1.0rc10/tests/test_authorizer.py +306 -0
  40. agentmesh_p2p-0.1.0rc10/tests/test_biscuit.py +79 -0
  41. agentmesh_p2p-0.1.0rc10/tests/test_controlplane.py +326 -0
  42. agentmesh_p2p-0.1.0rc10/tests/test_discovery.py +246 -0
  43. agentmesh_p2p-0.1.0rc10/tests/test_host.py +380 -0
  44. agentmesh_p2p-0.1.0rc10/tests/test_identity.py +86 -0
  45. agentmesh_p2p-0.1.0rc10/tests/test_libp2p_http.py +482 -0
  46. agentmesh_p2p-0.1.0rc10/tests/test_mcp.py +340 -0
  47. agentmesh_p2p-0.1.0rc10/tests/test_mesh.py +310 -0
  48. agentmesh_p2p-0.1.0rc10/tests/test_relay.py +70 -0
  49. agentmesh_p2p-0.1.0rc10/tests/test_session.py +1081 -0
  50. agentmesh_p2p-0.1.0rc10/tests/test_sync.py +296 -0
@@ -0,0 +1,78 @@
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
+ # State of `make testnet` (database, router key, tokens)
38
+ .agentmesh-one/
39
+ # Stray binaries from `go build ./cmd/<name>/` in the repo root
40
+ /agentmesh-node
41
+ /agentmesh-one
42
+ /agentmesh-box
43
+ /agentmesh-router
44
+ /agentmesh-console
45
+
46
+ # Playwright console UI tests
47
+ node_modules/
48
+ tests/ui/playwright-report/
49
+ tests/ui/test-results/
50
+ /agentmesh-control-plane
51
+ /agentmesh-bench
52
+ /mcp-client
53
+ /nano-init
54
+ tests/e2e/logs/
55
+ tests/integration/logs/
56
+ tests/integration/scratch/
57
+ __pycache__/
58
+ .pytest_cache/
59
+ .worktrees/
60
+ .venv/
61
+ *.egg-info/
62
+ keys.db
63
+ mobile/logcat.txt
64
+
65
+ # Guest images built by scripts/build-rootfs.sh; gigabytes, and reproducible
66
+ # from the script, so they belong in a bucket rather than in history.
67
+ /rootfs.ext4
68
+ /rootfs.tar
69
+
70
+ # Android upload keystore and Firebase config: secrets, wherever they land.
71
+ *.jks
72
+ *.keystore
73
+ key.properties
74
+ google-services.json
75
+
76
+
77
+ # Recordings of development/examples/sandboxed-agent/record.sh
78
+ development/examples/sandboxed-agent/out/
@@ -0,0 +1,216 @@
1
+ Metadata-Version: 2.5
2
+ Name: agentmesh-p2p
3
+ Version: 0.1.0-rc.10
4
+ Summary: Native SDK for joining an Agent Mesh from inside a Python agent process
5
+ Project-URL: Homepage, https://github.com/google/agentmesh
6
+ Project-URL: Source, https://github.com/google/agentmesh/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.9,>=0.8
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: examples
20
+ Requires-Dist: a2a-sdk[http-server]>=1.1; extra == 'examples'
21
+ Requires-Dist: uvicorn>=0.30; extra == 'examples'
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest>=8; extra == 'test'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # agentmesh-p2p (Python)
27
+
28
+ Native Python SDK for joining an Agent Mesh from inside the agent
29
+ process. It replaces the `agentmesh-node` sidecar for agents written in Python:
30
+ the agent enrolls with the control plane, joins the mesh through a router,
31
+ finds services and calls them, answers A2A requests for the agent itself,
32
+ and follows the control plane's keys, bans and policy while it runs. It
33
+ publishes no service; a tool or a model others should find by name runs
34
+ behind a `agentmesh-node`. Import it as
35
+ `agent_mesh`.
36
+
37
+ Guide: [google.github.io/agentmesh/docs/guides/native-sdks](https://google.github.io/agentmesh/docs/guides/native-sdks/),
38
+ from an empty machine to two programs on a mesh.
39
+ Source: [github.com/google/agentmesh/tree/main/sdk/python](https://github.com/google/agentmesh/tree/main/sdk/python).
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install agentmesh-p2p
45
+ ```
46
+
47
+ Python 3.11 or later. The SDK runs on trio (py-libp2p is trio-based); under
48
+ asyncio, use it through `anyio` with the trio backend. One dependency,
49
+ `fastecdsa`, builds from source against GMP (`libgmp-dev` on Debian).
50
+
51
+ ## Use
52
+
53
+ Both programs below are in
54
+ [`examples/`](https://github.com/google/agentmesh/tree/main/sdk/python/examples)
55
+ and run against a real mesh in the repository's tests. They read the mesh
56
+ from `AGENTMESH_CONTROL_PLANE_URL` and the enrollment token from
57
+ `AGENTMESH_BOOTSTRAP_TOKEN_PATH`; the guide shows how to get both from `agentmesh-one`
58
+ or from the operator of an existing mesh.
59
+
60
+ Find a service and call it:
61
+
62
+ <!-- embed: sdk/python/examples/call.py -->
63
+ ```python
64
+ """Calls something on the mesh: a tool of an MCP service or a path of an
65
+ inference or A2A service someone published, found by name, or an agent that
66
+ published nothing, reached by its peer ID.
67
+
68
+ python call.py mcp://everything echo '{"message": "hi"}'
69
+ python call.py inference://ollama /v1/models
70
+ python call.py 12D3KooW... a2a://agent /card
71
+
72
+ AGENTMESH_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
73
+ AGENTMESH_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or AGENTMESH_JWT_PATH
74
+ (a workload identity token your platform issues, such as a Kubernetes
75
+ projected service account token), and keeps the identity and credential in
76
+ AGENTMESH_STATE_DIR; later runs resume from there without it.
77
+ """
78
+
79
+ import json
80
+ import os
81
+ import sys
82
+
83
+ import trio
84
+ from agent_mesh import AgentMesh
85
+
86
+ argv = sys.argv[1:]
87
+ # A first argument that is not a service target is the peer ID of an agent.
88
+ peer_id = None
89
+ if argv and "://" not in argv[0]:
90
+ peer_id, argv = argv[0], argv[1:]
91
+ service = argv[0] if len(argv) > 0 else ("a2a://agent" if peer_id else "mcp://everything")
92
+ tool_or_path = argv[1] if len(argv) > 1 else ("/card" if peer_id else "echo")
93
+ args = json.loads(argv[2]) if len(argv) > 2 else {"message": "hi"}
94
+
95
+ mesh = AgentMesh.enroll(
96
+ os.environ.get("AGENTMESH_CONTROL_PLANE_URL", "https://mesh.example.com"),
97
+ bootstrap_token_path=os.environ.get("AGENTMESH_BOOTSTRAP_TOKEN_PATH"),
98
+ jwt_path=os.environ.get("AGENTMESH_JWT_PATH"),
99
+ state_dir=os.environ.get("AGENTMESH_STATE_DIR", "~/.config/agentmesh/caller"),
100
+ # A plaintext http:// control plane is otherwise accepted only on loopback.
101
+ allow_insecure=os.environ.get("AGENTMESH_INSECURE_CONTROL_PLANE") == "true",
102
+ )
103
+
104
+
105
+ async def main() -> None:
106
+ async with mesh.join() as session:
107
+ print(f"on the mesh as {session.peer_id}")
108
+
109
+ if peer_id:
110
+ # An agent is not in the discovery table; the SDK finds the path
111
+ # to its peer ID through the routers.
112
+ await session.connect(peer_id)
113
+ provider = peer_id
114
+ else:
115
+ providers = await session.discover(service)
116
+ if not providers:
117
+ raise SystemExit(f"no member of the mesh serves {service}")
118
+ # A provider record can outlive its member; the first that answers is used.
119
+ for provider in providers:
120
+ try:
121
+ await session.connect(provider)
122
+ break
123
+ except (ConnectionError, PermissionError) as err:
124
+ print(f"{provider.peer_id}: {err}", file=sys.stderr)
125
+ else:
126
+ raise SystemExit(f"no provider of {service} is reachable")
127
+ print(f"{service} is served by {peer_id or provider.peer_id}")
128
+
129
+ if service.startswith("mcp://"):
130
+ tools = await session.list_tools(provider, service)
131
+ print("tools:", ", ".join(t.name for t in tools))
132
+ result = await session.call_tool(provider, service, tool_or_path, args)
133
+ print("\n".join(result.text))
134
+ else:
135
+ response = await session.request(provider, service, tool_or_path)
136
+ print(response.status, response.text)
137
+
138
+
139
+ trio.run(main)
140
+ ```
141
+ <!-- /embed -->
142
+
143
+ Be an agent others can call. Nothing is published; a caller reaches the
144
+ agent by its peer ID, the one it prints, through a router:
145
+
146
+ <!-- embed: sdk/python/examples/agent.py -->
147
+ ```python
148
+ """An agent on the mesh: joins, then answers A2A requests from other members
149
+ until stopped. It publishes nothing. There is no service name to look up; a
150
+ caller reaches the agent by its peer ID, through a router, as `a2a://agent`.
151
+ The mesh policy decides which members may call; the SDK turns the others away
152
+ before anything reaches this code.
153
+
154
+ python agent.py # answered by the handler below
155
+ python agent.py http://127.0.0.1:9999 # forwarded to an A2A server beside it
156
+
157
+ AGENTMESH_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
158
+ AGENTMESH_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or AGENTMESH_JWT_PATH
159
+ (a workload identity token your platform issues, such as a Kubernetes
160
+ projected service account token), and keeps the identity and credential in
161
+ AGENTMESH_STATE_DIR; later runs resume from there, as the same peer, without it.
162
+ """
163
+
164
+ import json
165
+ import os
166
+ import sys
167
+
168
+ import trio
169
+ from agent_mesh import AgentMesh, HTTPRequest, HTTPResponse, VerifiedBiscuit
170
+
171
+ backend_url = sys.argv[1] if len(sys.argv) > 1 else None
172
+
173
+ mesh = AgentMesh.enroll(
174
+ os.environ.get("AGENTMESH_CONTROL_PLANE_URL", "https://mesh.example.com"),
175
+ bootstrap_token_path=os.environ.get("AGENTMESH_BOOTSTRAP_TOKEN_PATH"),
176
+ jwt_path=os.environ.get("AGENTMESH_JWT_PATH"),
177
+ state_dir=os.environ.get("AGENTMESH_STATE_DIR", "~/.config/agentmesh/agent"),
178
+ # A plaintext http:// control plane is otherwise accepted only on loopback.
179
+ allow_insecure=os.environ.get("AGENTMESH_INSECURE_CONTROL_PLANE") == "true",
180
+ )
181
+
182
+
183
+ async def card(request: HTTPRequest, caller: VerifiedBiscuit) -> HTTPResponse:
184
+ """Answers every path with who was asked and who asked; a real agent runs
185
+ an A2A server here, or beside this process at backend_url."""
186
+ body = json.dumps({"name": "agent", "path": request.path, "caller": caller.peer_id})
187
+ return HTTPResponse(status=200, headers={"content-type": "application/json"}, body=body.encode())
188
+
189
+
190
+ async def main() -> None:
191
+ async with mesh.join() as session:
192
+ target = await session.accept_a2a(backend_url or card)
193
+ print(f"accepting {target} as {session.peer_id}", flush=True)
194
+ await trio.sleep_forever()
195
+
196
+
197
+ trio.run(main)
198
+ ```
199
+ <!-- /embed -->
200
+
201
+ `enroll` reuses the identity and credential saved in `state_dir` when they
202
+ are still valid for that control plane, and needs exactly one of
203
+ `bootstrap_token_path`, `bootstrap_token`, `jwt_path` or `jwt` (a token string
204
+ or a `Callable[[], str]` invoked at enrollment and on every refresh)
205
+ otherwise. Read tokens from a file, a callback or the environment; do not put
206
+ them on a command line. `labels` are attested at enrollment;
207
+ `join(egress_require_labels=)` is the floor every peer the session calls must
208
+ attest, all of it, held for the session.
209
+
210
+ A plaintext `http://` control plane is accepted only on loopback. Pass
211
+ `allow_insecure=True` for a network you trust.
212
+
213
+ ## License
214
+
215
+ Apache-2.0. Issues and contributions at
216
+ [github.com/google/agentmesh](https://github.com/google/agentmesh).
@@ -0,0 +1,191 @@
1
+ # agentmesh-p2p (Python)
2
+
3
+ Native Python SDK for joining an Agent Mesh from inside the agent
4
+ process. It replaces the `agentmesh-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, answers A2A requests for the agent itself,
7
+ and follows the control plane's keys, bans and policy while it runs. It
8
+ publishes no service; a tool or a model others should find by name runs
9
+ behind a `agentmesh-node`. Import it as
10
+ `agent_mesh`.
11
+
12
+ Guide: [google.github.io/agentmesh/docs/guides/native-sdks](https://google.github.io/agentmesh/docs/guides/native-sdks/),
13
+ from an empty machine to two programs on a mesh.
14
+ Source: [github.com/google/agentmesh/tree/main/sdk/python](https://github.com/google/agentmesh/tree/main/sdk/python).
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pip install agentmesh-p2p
20
+ ```
21
+
22
+ Python 3.11 or later. The SDK runs on trio (py-libp2p is trio-based); under
23
+ asyncio, use it through `anyio` with the trio backend. One dependency,
24
+ `fastecdsa`, builds from source against GMP (`libgmp-dev` on Debian).
25
+
26
+ ## Use
27
+
28
+ Both programs below are in
29
+ [`examples/`](https://github.com/google/agentmesh/tree/main/sdk/python/examples)
30
+ and run against a real mesh in the repository's tests. They read the mesh
31
+ from `AGENTMESH_CONTROL_PLANE_URL` and the enrollment token from
32
+ `AGENTMESH_BOOTSTRAP_TOKEN_PATH`; the guide shows how to get both from `agentmesh-one`
33
+ or from the operator of an existing mesh.
34
+
35
+ Find a service and call it:
36
+
37
+ <!-- embed: sdk/python/examples/call.py -->
38
+ ```python
39
+ """Calls something on the mesh: a tool of an MCP service or a path of an
40
+ inference or A2A service someone published, found by name, or an agent that
41
+ published nothing, reached by its peer ID.
42
+
43
+ python call.py mcp://everything echo '{"message": "hi"}'
44
+ python call.py inference://ollama /v1/models
45
+ python call.py 12D3KooW... a2a://agent /card
46
+
47
+ AGENTMESH_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
48
+ AGENTMESH_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or AGENTMESH_JWT_PATH
49
+ (a workload identity token your platform issues, such as a Kubernetes
50
+ projected service account token), and keeps the identity and credential in
51
+ AGENTMESH_STATE_DIR; later runs resume from there without it.
52
+ """
53
+
54
+ import json
55
+ import os
56
+ import sys
57
+
58
+ import trio
59
+ from agent_mesh import AgentMesh
60
+
61
+ argv = sys.argv[1:]
62
+ # A first argument that is not a service target is the peer ID of an agent.
63
+ peer_id = None
64
+ if argv and "://" not in argv[0]:
65
+ peer_id, argv = argv[0], argv[1:]
66
+ service = argv[0] if len(argv) > 0 else ("a2a://agent" if peer_id else "mcp://everything")
67
+ tool_or_path = argv[1] if len(argv) > 1 else ("/card" if peer_id else "echo")
68
+ args = json.loads(argv[2]) if len(argv) > 2 else {"message": "hi"}
69
+
70
+ mesh = AgentMesh.enroll(
71
+ os.environ.get("AGENTMESH_CONTROL_PLANE_URL", "https://mesh.example.com"),
72
+ bootstrap_token_path=os.environ.get("AGENTMESH_BOOTSTRAP_TOKEN_PATH"),
73
+ jwt_path=os.environ.get("AGENTMESH_JWT_PATH"),
74
+ state_dir=os.environ.get("AGENTMESH_STATE_DIR", "~/.config/agentmesh/caller"),
75
+ # A plaintext http:// control plane is otherwise accepted only on loopback.
76
+ allow_insecure=os.environ.get("AGENTMESH_INSECURE_CONTROL_PLANE") == "true",
77
+ )
78
+
79
+
80
+ async def main() -> None:
81
+ async with mesh.join() as session:
82
+ print(f"on the mesh as {session.peer_id}")
83
+
84
+ if peer_id:
85
+ # An agent is not in the discovery table; the SDK finds the path
86
+ # to its peer ID through the routers.
87
+ await session.connect(peer_id)
88
+ provider = peer_id
89
+ else:
90
+ providers = await session.discover(service)
91
+ if not providers:
92
+ raise SystemExit(f"no member of the mesh serves {service}")
93
+ # A provider record can outlive its member; the first that answers is used.
94
+ for provider in providers:
95
+ try:
96
+ await session.connect(provider)
97
+ break
98
+ except (ConnectionError, PermissionError) as err:
99
+ print(f"{provider.peer_id}: {err}", file=sys.stderr)
100
+ else:
101
+ raise SystemExit(f"no provider of {service} is reachable")
102
+ print(f"{service} is served by {peer_id or provider.peer_id}")
103
+
104
+ if service.startswith("mcp://"):
105
+ tools = await session.list_tools(provider, service)
106
+ print("tools:", ", ".join(t.name for t in tools))
107
+ result = await session.call_tool(provider, service, tool_or_path, args)
108
+ print("\n".join(result.text))
109
+ else:
110
+ response = await session.request(provider, service, tool_or_path)
111
+ print(response.status, response.text)
112
+
113
+
114
+ trio.run(main)
115
+ ```
116
+ <!-- /embed -->
117
+
118
+ Be an agent others can call. Nothing is published; a caller reaches the
119
+ agent by its peer ID, the one it prints, through a router:
120
+
121
+ <!-- embed: sdk/python/examples/agent.py -->
122
+ ```python
123
+ """An agent on the mesh: joins, then answers A2A requests from other members
124
+ until stopped. It publishes nothing. There is no service name to look up; a
125
+ caller reaches the agent by its peer ID, through a router, as `a2a://agent`.
126
+ The mesh policy decides which members may call; the SDK turns the others away
127
+ before anything reaches this code.
128
+
129
+ python agent.py # answered by the handler below
130
+ python agent.py http://127.0.0.1:9999 # forwarded to an A2A server beside it
131
+
132
+ AGENTMESH_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
133
+ AGENTMESH_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or AGENTMESH_JWT_PATH
134
+ (a workload identity token your platform issues, such as a Kubernetes
135
+ projected service account token), and keeps the identity and credential in
136
+ AGENTMESH_STATE_DIR; later runs resume from there, as the same peer, without it.
137
+ """
138
+
139
+ import json
140
+ import os
141
+ import sys
142
+
143
+ import trio
144
+ from agent_mesh import AgentMesh, HTTPRequest, HTTPResponse, VerifiedBiscuit
145
+
146
+ backend_url = sys.argv[1] if len(sys.argv) > 1 else None
147
+
148
+ mesh = AgentMesh.enroll(
149
+ os.environ.get("AGENTMESH_CONTROL_PLANE_URL", "https://mesh.example.com"),
150
+ bootstrap_token_path=os.environ.get("AGENTMESH_BOOTSTRAP_TOKEN_PATH"),
151
+ jwt_path=os.environ.get("AGENTMESH_JWT_PATH"),
152
+ state_dir=os.environ.get("AGENTMESH_STATE_DIR", "~/.config/agentmesh/agent"),
153
+ # A plaintext http:// control plane is otherwise accepted only on loopback.
154
+ allow_insecure=os.environ.get("AGENTMESH_INSECURE_CONTROL_PLANE") == "true",
155
+ )
156
+
157
+
158
+ async def card(request: HTTPRequest, caller: VerifiedBiscuit) -> HTTPResponse:
159
+ """Answers every path with who was asked and who asked; a real agent runs
160
+ an A2A server here, or beside this process at backend_url."""
161
+ body = json.dumps({"name": "agent", "path": request.path, "caller": caller.peer_id})
162
+ return HTTPResponse(status=200, headers={"content-type": "application/json"}, body=body.encode())
163
+
164
+
165
+ async def main() -> None:
166
+ async with mesh.join() as session:
167
+ target = await session.accept_a2a(backend_url or card)
168
+ print(f"accepting {target} 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`, `jwt_path` or `jwt` (a token string
179
+ or a `Callable[[], str]` invoked at enrollment and on every refresh)
180
+ otherwise. Read tokens from a file, a callback or the environment; do not put
181
+ them on a command line. `labels` are attested at enrollment;
182
+ `join(egress_require_labels=)` is the floor every peer the session calls must
183
+ attest, all of it, held for the session.
184
+
185
+ A plaintext `http://` control plane is accepted only on loopback. Pass
186
+ `allow_insecure=True` for a network you trust.
187
+
188
+ ## License
189
+
190
+ Apache-2.0. Issues and contributions at
191
+ [github.com/google/agentmesh](https://github.com/google/agentmesh).
@@ -0,0 +1,92 @@
1
+ """An agent written with the A2A SDK, on the mesh. The A2A SDK's server runs
2
+ as it always does, a Starlette app under uvicorn on a loopback port; the mesh
3
+ SDK accepts requests for a2a://agent and forwards them to it. Other members
4
+ reach the agent by peer ID through a router; the mesh policy decides which
5
+ ones, and the SDK turns the rest away before a request reaches the app.
6
+
7
+ python a2a_agent.py
8
+
9
+ AGENTMESH_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
10
+ AGENTMESH_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or AGENTMESH_JWT_PATH
11
+ (a workload identity token your platform issues, such as a Kubernetes
12
+ projected service account token), and keeps the identity and credential in
13
+ AGENTMESH_STATE_DIR; later runs resume from there, as the same peer, without it.
14
+ """
15
+
16
+ import os
17
+ import socket
18
+ import threading
19
+
20
+ import trio
21
+ import uvicorn
22
+ from a2a.helpers import get_message_text, new_text_message
23
+ from a2a.server.agent_execution import AgentExecutor, RequestContext
24
+ from a2a.server.events import EventQueue
25
+ from a2a.server.request_handlers import DefaultRequestHandler
26
+ from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
27
+ from a2a.server.tasks import InMemoryTaskStore
28
+ from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill, Role
29
+ from agent_mesh import AgentMesh, MeshSession
30
+ from starlette.applications import Starlette
31
+
32
+ mesh = AgentMesh.enroll(
33
+ os.environ.get("AGENTMESH_CONTROL_PLANE_URL", "https://mesh.example.com"),
34
+ bootstrap_token_path=os.environ.get("AGENTMESH_BOOTSTRAP_TOKEN_PATH"),
35
+ jwt_path=os.environ.get("AGENTMESH_JWT_PATH"),
36
+ state_dir=os.environ.get("AGENTMESH_STATE_DIR", "~/.config/agentmesh/a2a-agent"),
37
+ # A plaintext http:// control plane is otherwise accepted only on loopback.
38
+ allow_insecure=os.environ.get("AGENTMESH_INSECURE_CONTROL_PLANE") == "true",
39
+ )
40
+
41
+
42
+ class EchoExecutor(AgentExecutor):
43
+ """One message in, one message out. X-Peer-Id is the caller the mesh
44
+ verified; the SDK sets it after authorizing the request."""
45
+
46
+ async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
47
+ caller = context.call_context.state["headers"].get("x-peer-id", "someone") if context.call_context else "someone"
48
+ said = get_message_text(context.message) if context.message else ""
49
+ await event_queue.enqueue_event(new_text_message(f"{caller} said: {said}", role=Role.ROLE_AGENT, context_id=context.context_id))
50
+
51
+ async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
52
+ pass
53
+
54
+
55
+ def a2a_app(agent_url: str) -> Starlette:
56
+ """The A2A SDK's server, as its samples build it. The card names the agent
57
+ as the mesh reaches it: http://mesh/mesh/<peer-id>/a2a/agent."""
58
+ card = AgentCard(
59
+ name="Echo agent",
60
+ description="Answers every message with what it said and who sent it.",
61
+ version="1.0.0",
62
+ default_input_modes=["text/plain"],
63
+ default_output_modes=["text/plain"],
64
+ capabilities=AgentCapabilities(streaming=True),
65
+ supported_interfaces=[AgentInterface(protocol_binding="JSONRPC", url=agent_url, protocol_version="1.0")],
66
+ skills=[AgentSkill(id="echo", name="Echo", description="Repeats the message.", tags=["echo"], examples=["hello"])],
67
+ )
68
+ handler = DefaultRequestHandler(agent_executor=EchoExecutor(), task_store=InMemoryTaskStore(), agent_card=card)
69
+ return Starlette(routes=[*create_agent_card_routes(card), *create_jsonrpc_routes(handler, "/")])
70
+
71
+
72
+ def serve_locally(app: Starlette) -> str:
73
+ """Runs uvicorn on a free loopback port in a thread and returns its URL.
74
+ The A2A SDK runs on asyncio, the mesh SDK on trio; a thread keeps them apart."""
75
+ with socket.socket() as probe:
76
+ probe.bind(("127.0.0.1", 0))
77
+ port = probe.getsockname()[1]
78
+ server = uvicorn.Server(uvicorn.Config(app, host="127.0.0.1", port=port, log_level="warning"))
79
+ threading.Thread(target=server.run, daemon=True).start()
80
+ return f"http://127.0.0.1:{port}"
81
+
82
+
83
+ async def main() -> None:
84
+ backend_url = serve_locally(a2a_app(MeshSession.mesh_url(mesh.identity.peer_id, "a2a://agent")))
85
+ async with mesh.join() as session:
86
+ target = await session.accept_a2a(backend_url)
87
+ print(f"accepting {target} as {session.peer_id}", flush=True)
88
+ print(f"agent card at {session.agent_url}/.well-known/agent-card.json", flush=True)
89
+ await trio.sleep_forever()
90
+
91
+
92
+ trio.run(main)
@@ -0,0 +1,60 @@
1
+ """Talks to an agent on the mesh with the A2A SDK's client. The mesh SDK is an
2
+ httpx transport; the A2A client fetches the agent card and sends messages as
3
+ it would to any A2A server, and every request travels to the peer through a
4
+ router with this member's credential.
5
+
6
+ python a2a_call.py 12D3KooW... "hello"
7
+
8
+ AGENTMESH_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
9
+ AGENTMESH_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or AGENTMESH_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
+ AGENTMESH_STATE_DIR; later runs resume from there without it.
13
+ """
14
+
15
+ import os
16
+ import sys
17
+
18
+ import httpx
19
+ import trio
20
+ from a2a.client import A2ACardResolver, ClientConfig, create_client
21
+ from a2a.helpers import get_message_text, new_text_message
22
+ from a2a.types import Role, SendMessageRequest
23
+ from agent_mesh import AgentMesh, MeshSession, MeshTransport
24
+
25
+ if len(sys.argv) < 2:
26
+ raise SystemExit("usage: a2a_call.py <peer-id> [text]")
27
+ peer_id = sys.argv[1]
28
+ text = sys.argv[2] if len(sys.argv) > 2 else "hello"
29
+
30
+ mesh = AgentMesh.enroll(
31
+ os.environ.get("AGENTMESH_CONTROL_PLANE_URL", "https://mesh.example.com"),
32
+ bootstrap_token_path=os.environ.get("AGENTMESH_BOOTSTRAP_TOKEN_PATH"),
33
+ jwt_path=os.environ.get("AGENTMESH_JWT_PATH"),
34
+ state_dir=os.environ.get("AGENTMESH_STATE_DIR", "~/.config/agentmesh/a2a-caller"),
35
+ # A plaintext http:// control plane is otherwise accepted only on loopback.
36
+ allow_insecure=os.environ.get("AGENTMESH_INSECURE_CONTROL_PLANE") == "true",
37
+ )
38
+
39
+
40
+ async def main() -> None:
41
+ async with mesh.join() as session:
42
+ print(f"on the mesh as {session.peer_id}")
43
+
44
+ # The agent's URL on the mesh, and an httpx client that carries requests to it.
45
+ agent_url = MeshSession.mesh_url(peer_id, "a2a://agent")
46
+ async with httpx.AsyncClient(transport=MeshTransport(session), timeout=60) as http:
47
+ card = await A2ACardResolver(http, agent_url).get_agent_card()
48
+ print(f"agent: {card.name}, {card.description}")
49
+
50
+ client = await create_client(card, ClientConfig(httpx_client=http, streaming=False))
51
+ request = SendMessageRequest(message=new_text_message(text, role=Role.ROLE_USER))
52
+ async for response in client.send_message(request):
53
+ if response.HasField("message"):
54
+ print(get_message_text(response.message))
55
+ elif response.HasField("task") and response.task.status.HasField("message"):
56
+ print(get_message_text(response.task.status.message))
57
+ await client.close()
58
+
59
+
60
+ trio.run(main)