a2a-pack 0.1.0__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.
- a2a_pack-0.1.0/.gitignore +10 -0
- a2a_pack-0.1.0/LICENSE +21 -0
- a2a_pack-0.1.0/PKG-INFO +143 -0
- a2a_pack-0.1.0/README.md +81 -0
- a2a_pack-0.1.0/a2a_pack/__init__.py +144 -0
- a2a_pack-0.1.0/a2a_pack/a2a_client.py +170 -0
- a2a_pack-0.1.0/a2a_pack/agent.py +443 -0
- a2a_pack-0.1.0/a2a_pack/auth.py +35 -0
- a2a_pack-0.1.0/a2a_pack/card.py +88 -0
- a2a_pack-0.1.0/a2a_pack/cli/__init__.py +1 -0
- a2a_pack-0.1.0/a2a_pack/cli/api_client.py +129 -0
- a2a_pack-0.1.0/a2a_pack/cli/credentials.py +68 -0
- a2a_pack-0.1.0/a2a_pack/cli/loader.py +34 -0
- a2a_pack-0.1.0/a2a_pack/cli/main.py +478 -0
- a2a_pack-0.1.0/a2a_pack/cli/manifests.py +133 -0
- a2a_pack-0.1.0/a2a_pack/cli/templates/Dockerfile.tmpl +14 -0
- a2a_pack-0.1.0/a2a_pack/cli/templates/a2a.yaml.tmpl +8 -0
- a2a_pack-0.1.0/a2a_pack/cli/templates/agent.py.tmpl +24 -0
- a2a_pack-0.1.0/a2a_pack/cli/templates/deployment.yaml.tmpl +77 -0
- a2a_pack-0.1.0/a2a_pack/cli/templates/dockerignore.tmpl +7 -0
- a2a_pack-0.1.0/a2a_pack/cli/templates/requirements.txt.tmpl +1 -0
- a2a_pack-0.1.0/a2a_pack/cli/templates/workflow.yml.tmpl +35 -0
- a2a_pack-0.1.0/a2a_pack/context.py +521 -0
- a2a_pack-0.1.0/a2a_pack/discovery.py +176 -0
- a2a_pack-0.1.0/a2a_pack/grants.py +148 -0
- a2a_pack-0.1.0/a2a_pack/mcp/__init__.py +28 -0
- a2a_pack-0.1.0/a2a_pack/mcp/http.py +69 -0
- a2a_pack-0.1.0/a2a_pack/mcp/server.py +241 -0
- a2a_pack-0.1.0/a2a_pack/runtime.py +134 -0
- a2a_pack-0.1.0/a2a_pack/sandbox.py +174 -0
- a2a_pack-0.1.0/a2a_pack/serve/__init__.py +3 -0
- a2a_pack-0.1.0/a2a_pack/serve/asgi.py +312 -0
- a2a_pack-0.1.0/a2a_pack/workspace.py +528 -0
- a2a_pack-0.1.0/pyproject.toml +90 -0
a2a_pack-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 a2a cloud
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
a2a_pack-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: a2a-pack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Developer SDK + CLI for building, packaging, and deploying A2A agents.
|
|
5
|
+
Project-URL: Homepage, https://a2acloud.io
|
|
6
|
+
Project-URL: Documentation, https://docs.a2acloud.io
|
|
7
|
+
Project-URL: Repository, https://gitea.a2acloud.io/gitea_admin/a2a-pack
|
|
8
|
+
Project-URL: Issues, https://gitea.a2acloud.io/gitea_admin/a2a-pack/issues
|
|
9
|
+
Author-email: a2a cloud <hello@a2acloud.io>
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 a2a cloud
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: a2a,agent,agents,ai,llm,marketplace,mcp,microvm,model-context-protocol,sandbox
|
|
33
|
+
Classifier: Development Status :: 4 - Beta
|
|
34
|
+
Classifier: Environment :: Web Environment
|
|
35
|
+
Classifier: Framework :: FastAPI
|
|
36
|
+
Classifier: Intended Audience :: Developers
|
|
37
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
38
|
+
Classifier: Operating System :: OS Independent
|
|
39
|
+
Classifier: Programming Language :: Python :: 3
|
|
40
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
43
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
44
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
45
|
+
Classifier: Typing :: Typed
|
|
46
|
+
Requires-Python: >=3.11
|
|
47
|
+
Requires-Dist: fastapi>=0.110
|
|
48
|
+
Requires-Dist: httpx>=0.27
|
|
49
|
+
Requires-Dist: jinja2>=3
|
|
50
|
+
Requires-Dist: pydantic>=2.6
|
|
51
|
+
Requires-Dist: pyyaml>=6
|
|
52
|
+
Requires-Dist: rich>=13
|
|
53
|
+
Requires-Dist: typer>=0.12
|
|
54
|
+
Requires-Dist: uvicorn[standard]>=0.27
|
|
55
|
+
Provides-Extra: dev
|
|
56
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
57
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
58
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
59
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
60
|
+
Requires-Dist: twine>=5; extra == 'dev'
|
|
61
|
+
Description-Content-Type: text/markdown
|
|
62
|
+
|
|
63
|
+
# a2a-pack
|
|
64
|
+
|
|
65
|
+
**Developer SDK + CLI for building, packaging, and deploying [A2A](https://a2acloud.io) agents.**
|
|
66
|
+
|
|
67
|
+
One Python class becomes a sandboxed, discoverable, MCP-compatible AI
|
|
68
|
+
agent on the [a2a cloud](https://a2acloud.io) platform. Other agents
|
|
69
|
+
reach yours via HMAC-signed grants. The platform owns deployment,
|
|
70
|
+
execution, permissions, and (when you're ready) billing.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install a2a-pack
|
|
74
|
+
a2a signup --email you@example.com --password ...
|
|
75
|
+
a2a init research-agent
|
|
76
|
+
cd research-agent
|
|
77
|
+
a2a deploy
|
|
78
|
+
# → https://research-agent.a2acloud.io (TLS, MCP, OpenAPI, all wired)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## What an agent looks like
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from pydantic import BaseModel
|
|
85
|
+
from a2a_pack import (
|
|
86
|
+
A2AAgent, LLMProvisioning, NoAuth, Pricing, RunContext, skill,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class GreeterConfig(BaseModel):
|
|
91
|
+
suffix: str = "!"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Greeter(A2AAgent[GreeterConfig, NoAuth]):
|
|
95
|
+
name = "greeter"
|
|
96
|
+
description = "Say hi."
|
|
97
|
+
version = "0.1.0"
|
|
98
|
+
config_model = GreeterConfig
|
|
99
|
+
auth_model = NoAuth
|
|
100
|
+
|
|
101
|
+
# Use the caller's own LLM key (forwarded by the platform) — the
|
|
102
|
+
# author's price stays small; the LLM bill goes to the caller's
|
|
103
|
+
# provider directly.
|
|
104
|
+
llm_provisioning = LLMProvisioning.CALLER_PROVIDED
|
|
105
|
+
pricing = Pricing(price_per_call_usd=0.01, caller_pays_llm=True)
|
|
106
|
+
|
|
107
|
+
@skill(description="Greet someone.")
|
|
108
|
+
async def greet(self, ctx: RunContext[NoAuth], who: str) -> str:
|
|
109
|
+
await ctx.emit_progress(f"greeting {who}")
|
|
110
|
+
return f"hello {who}{self.config.suffix}"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
That's it. `a2a deploy` packages the source, the control plane builds
|
|
114
|
+
the image, ArgoCD reconciles, you get a public URL.
|
|
115
|
+
|
|
116
|
+
## Public surface
|
|
117
|
+
|
|
118
|
+
| Concept | Where |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `A2AAgent` base class + `@skill` decorator | `a2a_pack.agent` |
|
|
121
|
+
| `RunContext`, `ctx.llm`, `ctx.ask`, `ctx.request_scope` | `a2a_pack.context` |
|
|
122
|
+
| Grant mint/verify (HMAC, audience-bound, glob-filtered, time-limited) | `a2a_pack.grants` |
|
|
123
|
+
| Workspace negotiation surface | `a2a_pack.workspace` |
|
|
124
|
+
| Sandbox client (microVM via libkrun) | `a2a_pack.sandbox` |
|
|
125
|
+
| Agent-to-agent client (HTTP, in-memory, custom) | `a2a_pack.a2a_client` |
|
|
126
|
+
| MCP server (skills → tools, mountable into your FastAPI app) | `a2a_pack.mcp` |
|
|
127
|
+
| Lifecycle / Resources / Pricing / LLMProvisioning declarations | `a2a_pack.runtime` |
|
|
128
|
+
| Card schema (auto-derived from your class) | `a2a_pack.card` |
|
|
129
|
+
|
|
130
|
+
Full reference + auto-generated docs at **https://docs.a2acloud.io**.
|
|
131
|
+
|
|
132
|
+
## Self-hosting
|
|
133
|
+
|
|
134
|
+
The platform pieces (control plane, sandbox runtime, gitea, ArgoCD,
|
|
135
|
+
MinIO, LiteLLM) live at
|
|
136
|
+
[gitea.a2acloud.io](https://gitea.a2acloud.io) — the SDK is the only
|
|
137
|
+
piece you need on PyPI. If you want to run the whole stack locally
|
|
138
|
+
or in your own cluster, the bootstrap recipe is in the platform
|
|
139
|
+
[README](https://gitea.a2acloud.io/gitea_admin/a2a-pack).
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT — see [LICENSE](LICENSE).
|
a2a_pack-0.1.0/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# a2a-pack
|
|
2
|
+
|
|
3
|
+
**Developer SDK + CLI for building, packaging, and deploying [A2A](https://a2acloud.io) agents.**
|
|
4
|
+
|
|
5
|
+
One Python class becomes a sandboxed, discoverable, MCP-compatible AI
|
|
6
|
+
agent on the [a2a cloud](https://a2acloud.io) platform. Other agents
|
|
7
|
+
reach yours via HMAC-signed grants. The platform owns deployment,
|
|
8
|
+
execution, permissions, and (when you're ready) billing.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install a2a-pack
|
|
12
|
+
a2a signup --email you@example.com --password ...
|
|
13
|
+
a2a init research-agent
|
|
14
|
+
cd research-agent
|
|
15
|
+
a2a deploy
|
|
16
|
+
# → https://research-agent.a2acloud.io (TLS, MCP, OpenAPI, all wired)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What an agent looks like
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from pydantic import BaseModel
|
|
23
|
+
from a2a_pack import (
|
|
24
|
+
A2AAgent, LLMProvisioning, NoAuth, Pricing, RunContext, skill,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GreeterConfig(BaseModel):
|
|
29
|
+
suffix: str = "!"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Greeter(A2AAgent[GreeterConfig, NoAuth]):
|
|
33
|
+
name = "greeter"
|
|
34
|
+
description = "Say hi."
|
|
35
|
+
version = "0.1.0"
|
|
36
|
+
config_model = GreeterConfig
|
|
37
|
+
auth_model = NoAuth
|
|
38
|
+
|
|
39
|
+
# Use the caller's own LLM key (forwarded by the platform) — the
|
|
40
|
+
# author's price stays small; the LLM bill goes to the caller's
|
|
41
|
+
# provider directly.
|
|
42
|
+
llm_provisioning = LLMProvisioning.CALLER_PROVIDED
|
|
43
|
+
pricing = Pricing(price_per_call_usd=0.01, caller_pays_llm=True)
|
|
44
|
+
|
|
45
|
+
@skill(description="Greet someone.")
|
|
46
|
+
async def greet(self, ctx: RunContext[NoAuth], who: str) -> str:
|
|
47
|
+
await ctx.emit_progress(f"greeting {who}")
|
|
48
|
+
return f"hello {who}{self.config.suffix}"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
That's it. `a2a deploy` packages the source, the control plane builds
|
|
52
|
+
the image, ArgoCD reconciles, you get a public URL.
|
|
53
|
+
|
|
54
|
+
## Public surface
|
|
55
|
+
|
|
56
|
+
| Concept | Where |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `A2AAgent` base class + `@skill` decorator | `a2a_pack.agent` |
|
|
59
|
+
| `RunContext`, `ctx.llm`, `ctx.ask`, `ctx.request_scope` | `a2a_pack.context` |
|
|
60
|
+
| Grant mint/verify (HMAC, audience-bound, glob-filtered, time-limited) | `a2a_pack.grants` |
|
|
61
|
+
| Workspace negotiation surface | `a2a_pack.workspace` |
|
|
62
|
+
| Sandbox client (microVM via libkrun) | `a2a_pack.sandbox` |
|
|
63
|
+
| Agent-to-agent client (HTTP, in-memory, custom) | `a2a_pack.a2a_client` |
|
|
64
|
+
| MCP server (skills → tools, mountable into your FastAPI app) | `a2a_pack.mcp` |
|
|
65
|
+
| Lifecycle / Resources / Pricing / LLMProvisioning declarations | `a2a_pack.runtime` |
|
|
66
|
+
| Card schema (auto-derived from your class) | `a2a_pack.card` |
|
|
67
|
+
|
|
68
|
+
Full reference + auto-generated docs at **https://docs.a2acloud.io**.
|
|
69
|
+
|
|
70
|
+
## Self-hosting
|
|
71
|
+
|
|
72
|
+
The platform pieces (control plane, sandbox runtime, gitea, ArgoCD,
|
|
73
|
+
MinIO, LiteLLM) live at
|
|
74
|
+
[gitea.a2acloud.io](https://gitea.a2acloud.io) — the SDK is the only
|
|
75
|
+
piece you need on PyPI. If you want to run the whole stack locally
|
|
76
|
+
or in your own cluster, the bootstrap recipe is in the platform
|
|
77
|
+
[README](https://gitea.a2acloud.io/gitea_admin/a2a-pack).
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""a2a-pack — developer SDK + CLI for the a2a cloud platform.
|
|
2
|
+
|
|
3
|
+
See https://docs.a2acloud.io for the full reference.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Single source of truth — pyproject.toml reads this via hatch.version.
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
from .a2a_client import (
|
|
10
|
+
A2AClient,
|
|
11
|
+
CallResult,
|
|
12
|
+
HttpA2AClient,
|
|
13
|
+
InMemoryA2AClient,
|
|
14
|
+
)
|
|
15
|
+
from .agent import (
|
|
16
|
+
A2AAgent,
|
|
17
|
+
ParamSpec,
|
|
18
|
+
SkillInputError,
|
|
19
|
+
SkillInvocationError,
|
|
20
|
+
SkillNotFound,
|
|
21
|
+
SkillSpec,
|
|
22
|
+
skill,
|
|
23
|
+
)
|
|
24
|
+
from .discovery import (
|
|
25
|
+
ControlPlaneDiscovery,
|
|
26
|
+
DiscoveredAgent,
|
|
27
|
+
DiscoveryClient,
|
|
28
|
+
InMemoryDiscovery,
|
|
29
|
+
)
|
|
30
|
+
from .grants import Grant, GrantInvalid, mint_grant, sign_grant, verify_grant
|
|
31
|
+
from .mcp import (
|
|
32
|
+
MCP_PROTOCOL_VERSION,
|
|
33
|
+
MCPServer,
|
|
34
|
+
build_http_app as build_mcp_http_app,
|
|
35
|
+
mount_http as mount_mcp_http,
|
|
36
|
+
skills_to_tools,
|
|
37
|
+
)
|
|
38
|
+
from .auth import APIKeyAuth, JWTAuth, NoAuth
|
|
39
|
+
from .card import AgentCard, SkillCard
|
|
40
|
+
from .context import (
|
|
41
|
+
AgentEvent,
|
|
42
|
+
ArtifactRef,
|
|
43
|
+
CancelledByCaller,
|
|
44
|
+
LLMCreds,
|
|
45
|
+
LocalRunContext,
|
|
46
|
+
MissingScopes,
|
|
47
|
+
RunContext,
|
|
48
|
+
)
|
|
49
|
+
from .runtime import (
|
|
50
|
+
AgentRuntime,
|
|
51
|
+
EgressPolicy,
|
|
52
|
+
Lifecycle,
|
|
53
|
+
LLMProvisioning,
|
|
54
|
+
Pricing,
|
|
55
|
+
Resources,
|
|
56
|
+
Sandbox,
|
|
57
|
+
SkillPolicy,
|
|
58
|
+
State,
|
|
59
|
+
)
|
|
60
|
+
from .sandbox import (
|
|
61
|
+
ExecResult,
|
|
62
|
+
SandboxClient,
|
|
63
|
+
SandboxHandle,
|
|
64
|
+
SandboxSpec,
|
|
65
|
+
SandboxUnavailable,
|
|
66
|
+
)
|
|
67
|
+
from .workspace import (
|
|
68
|
+
FileMatch,
|
|
69
|
+
FileType,
|
|
70
|
+
LocalWorkspaceClient,
|
|
71
|
+
LocalWorkspaceView,
|
|
72
|
+
WorkspaceAccess,
|
|
73
|
+
WorkspaceClient,
|
|
74
|
+
WorkspaceDenied,
|
|
75
|
+
WorkspaceGrant,
|
|
76
|
+
WorkspaceMode,
|
|
77
|
+
WorkspacePatch,
|
|
78
|
+
WorkspaceView,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
"A2AAgent",
|
|
83
|
+
"A2AClient",
|
|
84
|
+
"APIKeyAuth",
|
|
85
|
+
"AgentCard",
|
|
86
|
+
"AgentEvent",
|
|
87
|
+
"AgentRuntime",
|
|
88
|
+
"ArtifactRef",
|
|
89
|
+
"CallResult",
|
|
90
|
+
"CancelledByCaller",
|
|
91
|
+
"ControlPlaneDiscovery",
|
|
92
|
+
"DiscoveredAgent",
|
|
93
|
+
"DiscoveryClient",
|
|
94
|
+
"EgressPolicy",
|
|
95
|
+
"ExecResult",
|
|
96
|
+
"FileMatch",
|
|
97
|
+
"FileType",
|
|
98
|
+
"Grant",
|
|
99
|
+
"GrantInvalid",
|
|
100
|
+
"HttpA2AClient",
|
|
101
|
+
"InMemoryA2AClient",
|
|
102
|
+
"InMemoryDiscovery",
|
|
103
|
+
"JWTAuth",
|
|
104
|
+
"LLMCreds",
|
|
105
|
+
"LLMProvisioning",
|
|
106
|
+
"Lifecycle",
|
|
107
|
+
"LocalRunContext",
|
|
108
|
+
"MCP_PROTOCOL_VERSION",
|
|
109
|
+
"MCPServer",
|
|
110
|
+
"build_mcp_http_app",
|
|
111
|
+
"mount_mcp_http",
|
|
112
|
+
"skills_to_tools",
|
|
113
|
+
"LocalWorkspaceClient",
|
|
114
|
+
"LocalWorkspaceView",
|
|
115
|
+
"MissingScopes",
|
|
116
|
+
"NoAuth",
|
|
117
|
+
"ParamSpec",
|
|
118
|
+
"Pricing",
|
|
119
|
+
"Resources",
|
|
120
|
+
"RunContext",
|
|
121
|
+
"Sandbox",
|
|
122
|
+
"mint_grant",
|
|
123
|
+
"sign_grant",
|
|
124
|
+
"verify_grant",
|
|
125
|
+
"SandboxClient",
|
|
126
|
+
"SandboxHandle",
|
|
127
|
+
"SandboxSpec",
|
|
128
|
+
"SandboxUnavailable",
|
|
129
|
+
"SkillCard",
|
|
130
|
+
"SkillInputError",
|
|
131
|
+
"SkillInvocationError",
|
|
132
|
+
"SkillNotFound",
|
|
133
|
+
"SkillPolicy",
|
|
134
|
+
"SkillSpec",
|
|
135
|
+
"State",
|
|
136
|
+
"WorkspaceAccess",
|
|
137
|
+
"WorkspaceClient",
|
|
138
|
+
"WorkspaceDenied",
|
|
139
|
+
"WorkspaceGrant",
|
|
140
|
+
"WorkspaceMode",
|
|
141
|
+
"WorkspacePatch",
|
|
142
|
+
"WorkspaceView",
|
|
143
|
+
"skill",
|
|
144
|
+
]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Agent-to-agent invocation surface available via ``ctx.call(...)``.
|
|
2
|
+
|
|
3
|
+
An agent never speaks raw HTTP to another agent. It calls
|
|
4
|
+
``ctx.call(target, skill, args, grant=...)`` and the runtime-attached
|
|
5
|
+
:class:`A2AClient` handles transport: HTTP for cross-pod, in-memory for
|
|
6
|
+
local tests, anything else (gRPC, message bus) for future runtimes.
|
|
7
|
+
|
|
8
|
+
The grant token (see :mod:`a2a_pack.grants`) is the *only* way to hand
|
|
9
|
+
workspace access across agents. Callee-side runtime validates it before
|
|
10
|
+
materializing a :class:`WorkspaceClient`.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .agent import A2AAgent
|
|
20
|
+
from .context import RunContext
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class CallResult:
|
|
25
|
+
"""What an A2A invocation returns to the calling skill."""
|
|
26
|
+
|
|
27
|
+
result: Any
|
|
28
|
+
events: tuple[dict[str, Any], ...] = ()
|
|
29
|
+
artifacts: tuple[dict[str, Any], ...] = ()
|
|
30
|
+
grant_id: str | None = None # echoed for audit
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class A2AClient(ABC):
|
|
34
|
+
"""Transport-shaped agent-to-agent client."""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
async def call(
|
|
38
|
+
self,
|
|
39
|
+
target: str,
|
|
40
|
+
skill: str,
|
|
41
|
+
*,
|
|
42
|
+
args: dict[str, Any] | None = None,
|
|
43
|
+
grant: str | None = None,
|
|
44
|
+
timeout: float | None = None,
|
|
45
|
+
) -> CallResult:
|
|
46
|
+
"""Invoke ``skill`` on ``target`` and return its :class:`CallResult`.
|
|
47
|
+
|
|
48
|
+
``target`` is opaque to this layer — for the HTTP impl it's an agent
|
|
49
|
+
URL; for the in-memory impl it's an agent name.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# In-memory: routes calls to A2AAgent instances in the same process. Useful
|
|
55
|
+
# for the demo + tests.
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class InMemoryA2AClient(A2AClient):
|
|
61
|
+
"""Routes calls to agent instances registered by name.
|
|
62
|
+
|
|
63
|
+
The receiving agent gets a *new* :class:`RunContext` built by the
|
|
64
|
+
``ctx_factory`` callable, so caller and callee don't share state.
|
|
65
|
+
Pass ``ctx_factory=lambda agent, grant: ...`` to control how scoped
|
|
66
|
+
workspaces / sandboxes are wired in.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
agents: dict[str, "A2AAgent"]
|
|
70
|
+
ctx_factory: Any = None # Callable[[A2AAgent, str | None], RunContext]
|
|
71
|
+
|
|
72
|
+
async def call(
|
|
73
|
+
self,
|
|
74
|
+
target: str,
|
|
75
|
+
skill: str,
|
|
76
|
+
*,
|
|
77
|
+
args: dict[str, Any] | None = None,
|
|
78
|
+
grant: str | None = None,
|
|
79
|
+
timeout: float | None = None,
|
|
80
|
+
) -> CallResult:
|
|
81
|
+
if target not in self.agents:
|
|
82
|
+
raise KeyError(f"no agent registered: {target!r}")
|
|
83
|
+
agent = self.agents[target]
|
|
84
|
+
ctx = self.ctx_factory(agent, grant) if self.ctx_factory else None
|
|
85
|
+
if ctx is None:
|
|
86
|
+
from .context import LocalRunContext
|
|
87
|
+
from .auth import NoAuth
|
|
88
|
+
|
|
89
|
+
ctx = LocalRunContext(auth=NoAuth(), task_id=f"a2a-{target}")
|
|
90
|
+
result = await agent.invoke_json(skill, ctx, args or {})
|
|
91
|
+
events = tuple(
|
|
92
|
+
{"kind": e.kind, "payload": e.payload}
|
|
93
|
+
for e in getattr(ctx, "events", ())
|
|
94
|
+
)
|
|
95
|
+
# surface artifacts captured by LocalRunContext, if present
|
|
96
|
+
artifacts: tuple[dict[str, Any], ...] = ()
|
|
97
|
+
local_arts = getattr(ctx, "artifacts", None)
|
|
98
|
+
if isinstance(local_arts, dict):
|
|
99
|
+
artifacts = tuple(
|
|
100
|
+
{"name": name, "size_bytes": len(data)}
|
|
101
|
+
for name, data in local_arts.items()
|
|
102
|
+
)
|
|
103
|
+
return CallResult(
|
|
104
|
+
result=result,
|
|
105
|
+
events=events,
|
|
106
|
+
artifacts=artifacts,
|
|
107
|
+
grant_id=_grant_id_or_none(grant),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
# HTTP: posts to <target>/invoke/<skill> with {arguments, grant} body.
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class HttpA2AClient(A2AClient):
|
|
118
|
+
"""A2A client that POSTs to the standard /invoke/{skill} endpoint."""
|
|
119
|
+
|
|
120
|
+
default_timeout: float = 60.0
|
|
121
|
+
|
|
122
|
+
async def call(
|
|
123
|
+
self,
|
|
124
|
+
target: str,
|
|
125
|
+
skill: str,
|
|
126
|
+
*,
|
|
127
|
+
args: dict[str, Any] | None = None,
|
|
128
|
+
grant: str | None = None,
|
|
129
|
+
timeout: float | None = None,
|
|
130
|
+
) -> CallResult:
|
|
131
|
+
import httpx # late import: server-side needs no client
|
|
132
|
+
|
|
133
|
+
body: dict[str, Any] = {"arguments": args or {}}
|
|
134
|
+
if grant is not None:
|
|
135
|
+
body["grant"] = grant
|
|
136
|
+
url = f"{target.rstrip('/')}/invoke/{skill}"
|
|
137
|
+
async with httpx.AsyncClient(timeout=timeout or self.default_timeout) as c:
|
|
138
|
+
resp = await c.post(url, json=body)
|
|
139
|
+
if resp.status_code >= 400:
|
|
140
|
+
raise RuntimeError(f"a2a {url} -> {resp.status_code}: {resp.text}")
|
|
141
|
+
data = resp.json()
|
|
142
|
+
return CallResult(
|
|
143
|
+
result=data.get("result"),
|
|
144
|
+
events=tuple(data.get("events") or ()),
|
|
145
|
+
artifacts=tuple(data.get("artifacts") or ()),
|
|
146
|
+
grant_id=_grant_id_or_none(grant),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _grant_id_or_none(grant: str | None) -> str | None:
|
|
151
|
+
"""Extract grant_id without re-validating the signature (audit only)."""
|
|
152
|
+
if not grant or "." not in grant:
|
|
153
|
+
return None
|
|
154
|
+
try:
|
|
155
|
+
from .grants import _b64decode
|
|
156
|
+
|
|
157
|
+
payload = _b64decode(grant.rsplit(".", 1)[0])
|
|
158
|
+
import json
|
|
159
|
+
|
|
160
|
+
return json.loads(payload).get("grant_id")
|
|
161
|
+
except Exception: # noqa: BLE001
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
__all__ = [
|
|
166
|
+
"A2AClient",
|
|
167
|
+
"CallResult",
|
|
168
|
+
"HttpA2AClient",
|
|
169
|
+
"InMemoryA2AClient",
|
|
170
|
+
]
|