a2a-handler 0.2.0__tar.gz → 0.3.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_handler-0.2.0 → a2a_handler-0.3.0}/PKG-INFO +46 -3
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/README.md +43 -1
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/pyproject.toml +5 -3
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/pyproject.toml.orig +8 -3
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/auth.py +98 -11
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/cli/__init__.py +3 -1
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/cli/_helpers.py +29 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/cli/card.py +12 -1
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/cli/mcp.py +12 -7
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/cli/message.py +193 -10
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/cli/server.py +102 -2
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/cli/task.py +279 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/common/input_validation.py +53 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/mcp/server.py +237 -3
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/servers.py +94 -19
- a2a_handler-0.3.0/src/a2a_handler/service.py +1841 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/app.py +10 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/app.tcss +37 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/commands/palette.py +5 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/auth.py +115 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/input.py +45 -2
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/messages.py +129 -4
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/save.py +18 -4
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/tab.py +380 -93
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/views.py +4 -0
- a2a_handler-0.3.0/src/a2a_handler/turn.py +335 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/validation.py +44 -77
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/webhook.py +80 -19
- a2a_handler-0.2.0/src/a2a_handler/service.py +0 -745
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/LICENSE +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/__init__.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/cli/schema.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/cli/session.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/common/__init__.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/common/config.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/common/dotenv.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/common/logging.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/common/output.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/mcp/__init__.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/session.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/__init__.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/commands/__init__.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/commands/screens.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/__init__.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/artifacts.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/card.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/headers.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/logs.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/tasks.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/__init__.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/session.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/tabs.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/types.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: a2a-handler
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: An A2A Protocol client TUI and CLI.
|
|
5
5
|
Keywords: a2a,a2a-protocol,agent,agent-to-agent,ai,cli,tui
|
|
6
6
|
Author: Al Duncanson
|
|
@@ -10,7 +10,6 @@ License-File: LICENSE
|
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: Environment :: Console
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
14
13
|
Classifier: Operating System :: OS Independent
|
|
15
14
|
Classifier: Programming Language :: Python :: 3
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -30,12 +29,14 @@ Requires-Dist: textual-serve>=1.1.0
|
|
|
30
29
|
Requires-Dist: mcp[cli]>=1.27.1,<2
|
|
31
30
|
Requires-Dist: truststore>=0.9.0
|
|
32
31
|
Requires-Dist: platformdirs>=4.0.0
|
|
32
|
+
Requires-Dist: a2a-sdk[grpc]>=1.0.0,<2.0.0 ; extra == 'grpc'
|
|
33
33
|
Requires-Python: >=3.11
|
|
34
34
|
Project-URL: Homepage, https://github.com/alDuncanson/handler
|
|
35
35
|
Project-URL: Repository, https://github.com/alDuncanson/handler
|
|
36
36
|
Project-URL: Documentation, https://github.com/alDuncanson/handler/tree/main/docs
|
|
37
37
|
Project-URL: Issues, https://github.com/alDuncanson/handler/issues
|
|
38
38
|
Project-URL: Changelog, https://github.com/alDuncanson/handler/releases
|
|
39
|
+
Provides-Extra: grpc
|
|
39
40
|
Description-Content-Type: text/markdown
|
|
40
41
|
|
|
41
42
|
# Handler
|
|
@@ -52,10 +53,25 @@ client for software engineers building, testing, and operating agentic systems.
|
|
|
52
53
|
It provides an interactive TUI, a scriptable CLI with structured output, and an
|
|
53
54
|
MCP server that lets other agents integrate with A2A services directly. Handler
|
|
54
55
|
also supports global and repo-scoped A2A server configuration with bearer,
|
|
55
|
-
API key, mTLS,
|
|
56
|
+
API key, HTTP Basic, mTLS, OAuth2 client credentials, and OpenID Connect auth.
|
|
56
57
|
|
|
57
58
|

|
|
58
59
|
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
- Streams replies as they arrive, in the TUI and the CLI, and can stop a
|
|
63
|
+
running task or answer an agent that pauses for input
|
|
64
|
+
- Sends text, files (inline or by URL), and structured data, and shows the
|
|
65
|
+
file and data parts agents send back
|
|
66
|
+
- Lists, inspects, cancels, and resubscribes to tasks, and manages push
|
|
67
|
+
notification configs with a bundled local webhook receiver
|
|
68
|
+
- Speaks JSON-RPC and HTTP+JSON out of the box, gRPC with the `grpc` extra,
|
|
69
|
+
and lets the agent card pick the transport
|
|
70
|
+
- Requests A2A extensions and fetches the extended card an agent offers to
|
|
71
|
+
authenticated clients
|
|
72
|
+
- Keeps credentials on the origin you configured, never on a host an agent
|
|
73
|
+
card happens to name
|
|
74
|
+
|
|
59
75
|
## Install
|
|
60
76
|
|
|
61
77
|
Install Handler from the [PyPI package](https://pypi.org/project/a2a-handler/) as a `uv` tool:
|
|
@@ -96,6 +112,18 @@ Send a message from the CLI:
|
|
|
96
112
|
handler message send --url URL --text "hello"
|
|
97
113
|
```
|
|
98
114
|
|
|
115
|
+
Stream the reply, with a file attached:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
handler message stream --url URL --text "Review this" --file ./report.pdf
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
List the agent's tasks:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
handler task list --url URL
|
|
125
|
+
```
|
|
126
|
+
|
|
99
127
|
Open the full documentation:
|
|
100
128
|
|
|
101
129
|
```bash
|
|
@@ -119,3 +147,18 @@ pipx run a2a-handler
|
|
|
119
147
|
## Documentation
|
|
120
148
|
|
|
121
149
|
Read the documentation at <https://handler.alduncanson.com>.
|
|
150
|
+
|
|
151
|
+
### For coding agents
|
|
152
|
+
|
|
153
|
+
The docs ship as an agent skill, regenerated on every deploy. Install it into
|
|
154
|
+
Claude Code, Cursor, Windsurf, or any agent that reads the `SKILL.md` format:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npx skills add https://handler.alduncanson.com
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Or fetch it by hand from <https://handler.alduncanson.com/skill.md>. For live
|
|
161
|
+
lookups, agents can also use the hosted documentation MCP server at
|
|
162
|
+
<https://handler.alduncanson.com/mcp> or the page index at
|
|
163
|
+
<https://handler.alduncanson.com/llms.txt>. See the
|
|
164
|
+
[agent skill guide](https://handler.alduncanson.com/guides/agent-skill).
|
|
@@ -12,10 +12,25 @@ client for software engineers building, testing, and operating agentic systems.
|
|
|
12
12
|
It provides an interactive TUI, a scriptable CLI with structured output, and an
|
|
13
13
|
MCP server that lets other agents integrate with A2A services directly. Handler
|
|
14
14
|
also supports global and repo-scoped A2A server configuration with bearer,
|
|
15
|
-
API key, mTLS,
|
|
15
|
+
API key, HTTP Basic, mTLS, OAuth2 client credentials, and OpenID Connect auth.
|
|
16
16
|
|
|
17
17
|

|
|
18
18
|
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- Streams replies as they arrive, in the TUI and the CLI, and can stop a
|
|
22
|
+
running task or answer an agent that pauses for input
|
|
23
|
+
- Sends text, files (inline or by URL), and structured data, and shows the
|
|
24
|
+
file and data parts agents send back
|
|
25
|
+
- Lists, inspects, cancels, and resubscribes to tasks, and manages push
|
|
26
|
+
notification configs with a bundled local webhook receiver
|
|
27
|
+
- Speaks JSON-RPC and HTTP+JSON out of the box, gRPC with the `grpc` extra,
|
|
28
|
+
and lets the agent card pick the transport
|
|
29
|
+
- Requests A2A extensions and fetches the extended card an agent offers to
|
|
30
|
+
authenticated clients
|
|
31
|
+
- Keeps credentials on the origin you configured, never on a host an agent
|
|
32
|
+
card happens to name
|
|
33
|
+
|
|
19
34
|
## Install
|
|
20
35
|
|
|
21
36
|
Install Handler from the [PyPI package](https://pypi.org/project/a2a-handler/) as a `uv` tool:
|
|
@@ -56,6 +71,18 @@ Send a message from the CLI:
|
|
|
56
71
|
handler message send --url URL --text "hello"
|
|
57
72
|
```
|
|
58
73
|
|
|
74
|
+
Stream the reply, with a file attached:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
handler message stream --url URL --text "Review this" --file ./report.pdf
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
List the agent's tasks:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
handler task list --url URL
|
|
84
|
+
```
|
|
85
|
+
|
|
59
86
|
Open the full documentation:
|
|
60
87
|
|
|
61
88
|
```bash
|
|
@@ -79,3 +106,18 @@ pipx run a2a-handler
|
|
|
79
106
|
## Documentation
|
|
80
107
|
|
|
81
108
|
Read the documentation at <https://handler.alduncanson.com>.
|
|
109
|
+
|
|
110
|
+
### For coding agents
|
|
111
|
+
|
|
112
|
+
The docs ship as an agent skill, regenerated on every deploy. Install it into
|
|
113
|
+
Claude Code, Cursor, Windsurf, or any agent that reads the `SKILL.md` format:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npx skills add https://handler.alduncanson.com
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Or fetch it by hand from <https://handler.alduncanson.com/skill.md>. For live
|
|
120
|
+
lookups, agents can also use the hosted documentation MCP server at
|
|
121
|
+
<https://handler.alduncanson.com/mcp> or the page index at
|
|
122
|
+
<https://handler.alduncanson.com/llms.txt>. See the
|
|
123
|
+
[agent skill guide](https://handler.alduncanson.com/guides/agent-skill).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "a2a-handler"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "An A2A Protocol client TUI and CLI."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -19,7 +19,6 @@ classifiers = [
|
|
|
19
19
|
"Development Status :: 4 - Beta",
|
|
20
20
|
"Environment :: Console",
|
|
21
21
|
"Intended Audience :: Developers",
|
|
22
|
-
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
|
23
22
|
"Operating System :: OS Independent",
|
|
24
23
|
"Programming Language :: Python :: 3",
|
|
25
24
|
"Programming Language :: Python :: 3.11",
|
|
@@ -47,6 +46,9 @@ dependencies = [
|
|
|
47
46
|
name = "Al Duncanson"
|
|
48
47
|
email = "alDuncanson@proton.me"
|
|
49
48
|
|
|
49
|
+
[project.optional-dependencies]
|
|
50
|
+
grpc = ["a2a-sdk[grpc]>=1.0.0,<2.0.0"]
|
|
51
|
+
|
|
50
52
|
[project.urls]
|
|
51
53
|
Homepage = "https://github.com/alDuncanson/handler"
|
|
52
54
|
Repository = "https://github.com/alDuncanson/handler"
|
|
@@ -69,7 +71,7 @@ dev = [
|
|
|
69
71
|
]
|
|
70
72
|
|
|
71
73
|
[build-system]
|
|
72
|
-
requires = ["uv_build>=0.9.15,<0.
|
|
74
|
+
requires = ["uv_build>=0.9.15,<0.13.0"]
|
|
73
75
|
build-backend = "uv_build"
|
|
74
76
|
|
|
75
77
|
[tool.uv.build-backend]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "a2a-handler"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "An A2A Protocol client TUI and CLI."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -22,7 +22,6 @@ classifiers = [
|
|
|
22
22
|
"Development Status :: 4 - Beta",
|
|
23
23
|
"Environment :: Console",
|
|
24
24
|
"Intended Audience :: Developers",
|
|
25
|
-
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
|
26
25
|
"Operating System :: OS Independent",
|
|
27
26
|
"Programming Language :: Python :: 3",
|
|
28
27
|
"Programming Language :: Python :: 3.11",
|
|
@@ -48,6 +47,12 @@ dependencies = [
|
|
|
48
47
|
"platformdirs>=4.0.0",
|
|
49
48
|
]
|
|
50
49
|
|
|
50
|
+
[project.optional-dependencies]
|
|
51
|
+
# Talking to gRPC-only agents needs the SDK's gRPC transport and its
|
|
52
|
+
# grpcio dependencies, which are heavy enough to keep out of the default
|
|
53
|
+
# install.
|
|
54
|
+
grpc = ["a2a-sdk[grpc]>=1.0.0,<2.0.0"]
|
|
55
|
+
|
|
51
56
|
[dependency-groups]
|
|
52
57
|
dev = [
|
|
53
58
|
"ruff>=0.8.0",
|
|
@@ -70,7 +75,7 @@ Changelog = "https://github.com/alDuncanson/handler/releases"
|
|
|
70
75
|
handler = "a2a_handler.cli:main"
|
|
71
76
|
|
|
72
77
|
[build-system]
|
|
73
|
-
requires = ["uv_build>=0.9.15,<0.
|
|
78
|
+
requires = ["uv_build>=0.9.15,<0.13.0"]
|
|
74
79
|
build-backend = "uv_build"
|
|
75
80
|
|
|
76
81
|
[tool.uv.build-backend]
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""Authentication support for A2A protocol.
|
|
2
2
|
|
|
3
3
|
Handles credential storage and HTTP authentication header generation.
|
|
4
|
-
Supports API key, HTTP bearer, mTLS (mutual TLS),
|
|
5
|
-
authentication schemes.
|
|
4
|
+
Supports API key, HTTP bearer, HTTP basic, mTLS (mutual TLS), OAuth2 client
|
|
5
|
+
credentials, and OpenID Connect authentication schemes.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import base64
|
|
10
11
|
import ssl
|
|
11
12
|
import time
|
|
12
13
|
from dataclasses import dataclass, field
|
|
@@ -30,9 +31,18 @@ class AuthType(str, Enum):
|
|
|
30
31
|
"""Supported authentication types."""
|
|
31
32
|
|
|
32
33
|
API_KEY = "api_key"
|
|
34
|
+
BASIC = "basic"
|
|
33
35
|
BEARER = "bearer"
|
|
34
36
|
MTLS = "mtls"
|
|
35
37
|
OAUTH2 = "oauth2"
|
|
38
|
+
OIDC = "oidc"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
#: Auth types whose access token is fetched from a token endpoint and cached.
|
|
42
|
+
TOKEN_FETCHING_AUTH_TYPES = {AuthType.OAUTH2, AuthType.OIDC}
|
|
43
|
+
|
|
44
|
+
#: Path of the OpenID Connect discovery document below the issuer URL.
|
|
45
|
+
OIDC_DISCOVERY_PATH = "/.well-known/openid-configuration"
|
|
36
46
|
|
|
37
47
|
|
|
38
48
|
@dataclass
|
|
@@ -45,13 +55,15 @@ class AuthCredentials:
|
|
|
45
55
|
auth_type: AuthType
|
|
46
56
|
value: str = ""
|
|
47
57
|
header_name: str | None = None # For API key: custom header name
|
|
58
|
+
username: str | None = None # For HTTP basic: username (value is password)
|
|
48
59
|
cert_path: str | None = None # For mTLS: client certificate path
|
|
49
60
|
key_path: str | None = None # For mTLS: client private key path
|
|
50
61
|
ca_cert_path: str | None = None # For mTLS: CA certificate path
|
|
51
|
-
token_url: str | None = None # For OAuth2: token endpoint
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
token_url: str | None = None # For OAuth2/OIDC: token endpoint
|
|
63
|
+
issuer_url: str | None = None # For OIDC: issuer to discover endpoints from
|
|
64
|
+
client_id: str | None = None # For OAuth2/OIDC: client ID
|
|
65
|
+
client_secret: str | None = None # For OAuth2/OIDC: client secret
|
|
66
|
+
scopes: list[str] | None = None # For OAuth2/OIDC: optional scopes
|
|
55
67
|
custom_headers: dict[str, str] | None = None # Additional headers for any auth type
|
|
56
68
|
_token_expires_at: float | None = field(default=None, repr=False)
|
|
57
69
|
|
|
@@ -64,8 +76,9 @@ class AuthCredentials:
|
|
|
64
76
|
"key_path": self.key_path,
|
|
65
77
|
"ca_cert_path": self.ca_cert_path,
|
|
66
78
|
"token_url": self.token_url,
|
|
79
|
+
"issuer_url": self.issuer_url,
|
|
67
80
|
}
|
|
68
|
-
for secret_field in ("value", "client_id", "client_secret"):
|
|
81
|
+
for secret_field in ("value", "username", "client_id", "client_secret"):
|
|
69
82
|
val = getattr(self, secret_field)
|
|
70
83
|
redacted[secret_field] = "***" if val else None
|
|
71
84
|
if self.custom_headers:
|
|
@@ -83,8 +96,12 @@ class AuthCredentials:
|
|
|
83
96
|
headers.update(self.custom_headers)
|
|
84
97
|
if self.auth_type == AuthType.BEARER and self.value:
|
|
85
98
|
headers["Authorization"] = f"Bearer {self.value}"
|
|
86
|
-
elif self.auth_type
|
|
99
|
+
elif self.auth_type in TOKEN_FETCHING_AUTH_TYPES and self.value:
|
|
87
100
|
headers["Authorization"] = f"Bearer {self.value}"
|
|
101
|
+
elif self.auth_type == AuthType.BASIC and self.username is not None:
|
|
102
|
+
userpass = f"{self.username}:{self.value}".encode()
|
|
103
|
+
encoded = base64.b64encode(userpass).decode("ascii")
|
|
104
|
+
headers["Authorization"] = f"Basic {encoded}"
|
|
88
105
|
elif self.auth_type == AuthType.API_KEY:
|
|
89
106
|
header = self.header_name or "X-API-Key"
|
|
90
107
|
headers[header] = self.value
|
|
@@ -122,8 +139,39 @@ class AuthCredentials:
|
|
|
122
139
|
self.value = ""
|
|
123
140
|
self._token_expires_at = None
|
|
124
141
|
|
|
142
|
+
async def discover_oidc_token_endpoint(self) -> str:
|
|
143
|
+
"""Resolve the token endpoint from the OIDC discovery document.
|
|
144
|
+
|
|
145
|
+
Fetches ``<issuer>/.well-known/openid-configuration`` and caches the
|
|
146
|
+
advertised ``token_endpoint`` on ``token_url``.
|
|
147
|
+
"""
|
|
148
|
+
if self.auth_type != AuthType.OIDC:
|
|
149
|
+
raise ValueError("OIDC discovery is only valid for OIDC credentials")
|
|
150
|
+
if self.token_url:
|
|
151
|
+
return self.token_url
|
|
152
|
+
if not self.issuer_url:
|
|
153
|
+
raise ValueError("issuer_url is required for OIDC discovery")
|
|
154
|
+
validate_token_url(self.issuer_url)
|
|
155
|
+
|
|
156
|
+
discovery_url = self.issuer_url.rstrip("/") + OIDC_DISCOVERY_PATH
|
|
157
|
+
async with httpx.AsyncClient(trust_env=False) as discovery_client:
|
|
158
|
+
response = await discovery_client.get(discovery_url)
|
|
159
|
+
response.raise_for_status()
|
|
160
|
+
document = response.json()
|
|
161
|
+
token_endpoint = document.get("token_endpoint")
|
|
162
|
+
if not isinstance(token_endpoint, str) or not token_endpoint:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
f"OIDC discovery document at {discovery_url} has no token_endpoint"
|
|
165
|
+
)
|
|
166
|
+
validate_token_url(token_endpoint)
|
|
167
|
+
self.token_url = token_endpoint
|
|
168
|
+
return token_endpoint
|
|
169
|
+
|
|
125
170
|
async def fetch_oauth2_token(self) -> str:
|
|
126
|
-
"""Fetch an access token using
|
|
171
|
+
"""Fetch an access token using the client credentials grant.
|
|
172
|
+
|
|
173
|
+
Valid for OAuth2 and OIDC credentials; OIDC resolves its token
|
|
174
|
+
endpoint through the issuer's discovery document first.
|
|
127
175
|
|
|
128
176
|
Uses a short-lived HTTP client so that agent auth headers and custom
|
|
129
177
|
headers on the main client are never sent to the token endpoint.
|
|
@@ -131,8 +179,10 @@ class AuthCredentials:
|
|
|
131
179
|
Updates self.value with the new token and returns it.
|
|
132
180
|
Parses ``expires_in`` from the token response to track expiry.
|
|
133
181
|
"""
|
|
134
|
-
if self.auth_type
|
|
135
|
-
raise ValueError("Token fetch is only valid for OAuth2 credentials")
|
|
182
|
+
if self.auth_type not in TOKEN_FETCHING_AUTH_TYPES:
|
|
183
|
+
raise ValueError("Token fetch is only valid for OAuth2 or OIDC credentials")
|
|
184
|
+
if self.auth_type == AuthType.OIDC:
|
|
185
|
+
await self.discover_oidc_token_endpoint()
|
|
136
186
|
if not self.token_url or not self.client_id or not self.client_secret:
|
|
137
187
|
raise ValueError("token_url, client_id, and client_secret are required")
|
|
138
188
|
validate_token_url(self.token_url)
|
|
@@ -168,6 +218,10 @@ class AuthCredentials:
|
|
|
168
218
|
"value": self.value,
|
|
169
219
|
"header_name": self.header_name,
|
|
170
220
|
}
|
|
221
|
+
if self.username is not None:
|
|
222
|
+
data["username"] = self.username
|
|
223
|
+
if self.issuer_url:
|
|
224
|
+
data["issuer_url"] = self.issuer_url
|
|
171
225
|
if self.cert_path:
|
|
172
226
|
data["cert_path"] = self.cert_path
|
|
173
227
|
if self.key_path:
|
|
@@ -197,6 +251,8 @@ class AuthCredentials:
|
|
|
197
251
|
auth_type=AuthType(data["auth_type"]),
|
|
198
252
|
value=data.get("value") or "",
|
|
199
253
|
header_name=data.get("header_name"),
|
|
254
|
+
username=data.get("username"),
|
|
255
|
+
issuer_url=data.get("issuer_url"),
|
|
200
256
|
cert_path=data.get("cert_path"),
|
|
201
257
|
key_path=data.get("key_path"),
|
|
202
258
|
ca_cert_path=data.get("ca_cert_path"),
|
|
@@ -226,6 +282,15 @@ def create_bearer_auth(token: str) -> AuthCredentials:
|
|
|
226
282
|
return AuthCredentials(auth_type=AuthType.BEARER, value=token)
|
|
227
283
|
|
|
228
284
|
|
|
285
|
+
def create_basic_auth(username: str, password: str) -> AuthCredentials:
|
|
286
|
+
"""Create HTTP basic authentication."""
|
|
287
|
+
return AuthCredentials(
|
|
288
|
+
auth_type=AuthType.BASIC,
|
|
289
|
+
username=username,
|
|
290
|
+
value=password,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
229
294
|
def create_api_key_auth(
|
|
230
295
|
key: str,
|
|
231
296
|
header_name: str = "X-API-Key",
|
|
@@ -286,3 +351,25 @@ def create_oauth2_auth(
|
|
|
286
351
|
client_secret=client_secret,
|
|
287
352
|
scopes=scopes,
|
|
288
353
|
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def create_oidc_auth(
|
|
357
|
+
issuer_url: str,
|
|
358
|
+
client_id: str,
|
|
359
|
+
client_secret: str,
|
|
360
|
+
scopes: list[str] | None = None,
|
|
361
|
+
access_token: str = "",
|
|
362
|
+
) -> AuthCredentials:
|
|
363
|
+
"""Create OpenID Connect authentication.
|
|
364
|
+
|
|
365
|
+
The token endpoint is discovered from the issuer's
|
|
366
|
+
``/.well-known/openid-configuration`` document on first token fetch.
|
|
367
|
+
"""
|
|
368
|
+
return AuthCredentials(
|
|
369
|
+
auth_type=AuthType.OIDC,
|
|
370
|
+
value=access_token,
|
|
371
|
+
issuer_url=issuer_url,
|
|
372
|
+
client_id=client_id,
|
|
373
|
+
client_secret=client_secret,
|
|
374
|
+
scopes=scopes,
|
|
375
|
+
)
|
|
@@ -11,6 +11,13 @@ from a2a.client.errors import (
|
|
|
11
11
|
A2AClientTimeoutError,
|
|
12
12
|
AgentCardResolutionError,
|
|
13
13
|
)
|
|
14
|
+
from a2a.utils.errors import (
|
|
15
|
+
A2AError,
|
|
16
|
+
MethodNotFoundError,
|
|
17
|
+
TaskNotCancelableError,
|
|
18
|
+
TaskNotFoundError,
|
|
19
|
+
UnsupportedOperationError,
|
|
20
|
+
)
|
|
14
21
|
|
|
15
22
|
from a2a_handler.auth import (
|
|
16
23
|
AuthCredentials,
|
|
@@ -172,6 +179,14 @@ def handle_client_error(e: Exception, agent_url: str, output: Output | None) ->
|
|
|
172
179
|
log.error("A2A client error: %s", e)
|
|
173
180
|
message = str(e)
|
|
174
181
|
error_code = "a2a_client_error"
|
|
182
|
+
elif isinstance(e, A2AError):
|
|
183
|
+
# The agent answered with a protocol error. That is a normal outcome
|
|
184
|
+
# with a clear meaning, not a crash, so no traceback.
|
|
185
|
+
log.error("Agent returned an error: %s", e)
|
|
186
|
+
message = str(e)
|
|
187
|
+
error_code = "a2a_error"
|
|
188
|
+
details = {"error": type(e).__name__, "agent_url": agent_url}
|
|
189
|
+
suggestion = _protocol_error_suggestion(e)
|
|
175
190
|
elif isinstance(e, httpx.ConnectError):
|
|
176
191
|
log.error("Connection refused to %s", agent_url)
|
|
177
192
|
message = f"Connection refused: Is the server running at {agent_url}?"
|
|
@@ -206,6 +221,20 @@ def handle_client_error(e: Exception, agent_url: str, output: Output | None) ->
|
|
|
206
221
|
print(f"Error [{error_code}]: {message}", file=sys.stderr)
|
|
207
222
|
|
|
208
223
|
|
|
224
|
+
def _protocol_error_suggestion(error: A2AError) -> str | None:
|
|
225
|
+
"""A next step for the protocol errors a user can act on."""
|
|
226
|
+
if isinstance(error, TaskNotCancelableError):
|
|
227
|
+
return (
|
|
228
|
+
"The task has probably already finished; `handler task get` shows "
|
|
229
|
+
"its current state"
|
|
230
|
+
)
|
|
231
|
+
if isinstance(error, TaskNotFoundError):
|
|
232
|
+
return "Check the task ID; the agent may have discarded the task"
|
|
233
|
+
if isinstance(error, (UnsupportedOperationError, MethodNotFoundError)):
|
|
234
|
+
return "This agent does not implement that operation"
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
209
238
|
def _resolve_env_secret(env_var: str, label: str) -> str:
|
|
210
239
|
"""Read a secret from an environment variable."""
|
|
211
240
|
value = os.environ.get(env_var)
|
|
@@ -35,6 +35,11 @@ def card() -> None:
|
|
|
35
35
|
@card.command("get")
|
|
36
36
|
@click.option("--url", "agent_url", help="Agent URL")
|
|
37
37
|
@click.option("--server", "-s", "server_name", help="Named server from servers.toml")
|
|
38
|
+
@click.option(
|
|
39
|
+
"--extended",
|
|
40
|
+
is_flag=True,
|
|
41
|
+
help="Fetch the extended card offered to authenticated clients",
|
|
42
|
+
)
|
|
38
43
|
@click.option(
|
|
39
44
|
"--bearer-env", "-b", help="Env var containing bearer token (overrides saved)"
|
|
40
45
|
)
|
|
@@ -44,6 +49,7 @@ def card() -> None:
|
|
|
44
49
|
def card_get(
|
|
45
50
|
agent_url: Optional[str],
|
|
46
51
|
server_name: Optional[str],
|
|
52
|
+
extended: bool,
|
|
47
53
|
bearer_env: Optional[str],
|
|
48
54
|
api_key_env: Optional[str],
|
|
49
55
|
) -> None:
|
|
@@ -54,6 +60,7 @@ def card_get(
|
|
|
54
60
|
$ handler card get --server my_agent
|
|
55
61
|
$ handler card get --url http://localhost:8000
|
|
56
62
|
$ handler card get --url http://localhost:8000 --bearer-env MY_TOKEN
|
|
63
|
+
$ handler card get --url http://localhost:8000 --extended --bearer-env MY_TOKEN
|
|
57
64
|
"""
|
|
58
65
|
output = Output()
|
|
59
66
|
|
|
@@ -74,7 +81,10 @@ def card_get(
|
|
|
74
81
|
try:
|
|
75
82
|
async with build_http_client(credentials=credentials) as http_client:
|
|
76
83
|
service = A2AService(http_client, resolved_url, credentials=credentials)
|
|
77
|
-
|
|
84
|
+
if extended:
|
|
85
|
+
card_data = await service.get_extended_card()
|
|
86
|
+
else:
|
|
87
|
+
card_data = await service.get_card()
|
|
78
88
|
log.info("Retrieved card for agent: %s", card_data.name)
|
|
79
89
|
|
|
80
90
|
_format_agent_card(card_data, output)
|
|
@@ -162,6 +172,7 @@ def card_validate(
|
|
|
162
172
|
source=resolved_url,
|
|
163
173
|
source_type=ValidationSource.URL,
|
|
164
174
|
agent_card=agent_card,
|
|
175
|
+
raw_data=service.raw_card,
|
|
165
176
|
)
|
|
166
177
|
|
|
167
178
|
_format_validation_result(result, output)
|
|
@@ -38,7 +38,7 @@ def mcp(transport: TransportType) -> None:
|
|
|
38
38
|
\b
|
|
39
39
|
Card Tools:
|
|
40
40
|
- validate_agent_card: Validate an agent card from URL or file
|
|
41
|
-
- get_agent_card: Retrieve an agent's
|
|
41
|
+
- get_agent_card: Retrieve an agent's public or extended card
|
|
42
42
|
|
|
43
43
|
\b
|
|
44
44
|
Message Tools:
|
|
@@ -47,9 +47,15 @@ def mcp(transport: TransportType) -> None:
|
|
|
47
47
|
\b
|
|
48
48
|
Task Tools:
|
|
49
49
|
- get_task: Get task status and details
|
|
50
|
+
- list_tasks: List an agent's tasks, following pagination to the end
|
|
50
51
|
- cancel_task: Cancel a running task
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
|
|
53
|
+
\b
|
|
54
|
+
Push Notification Tools:
|
|
55
|
+
- set_task_notification: Configure a push notification webhook
|
|
56
|
+
- get_task_notification: Get a task's push notification config
|
|
57
|
+
- list_task_notifications: List every push notification config for a task
|
|
58
|
+
- delete_task_notification: Delete a push notification config
|
|
53
59
|
|
|
54
60
|
\b
|
|
55
61
|
Session Tools:
|
|
@@ -57,10 +63,9 @@ def mcp(transport: TransportType) -> None:
|
|
|
57
63
|
- get_session_info: Get session for a specific agent
|
|
58
64
|
- clear_session_data: Clear saved session state
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
- clear_agent_credentials: Remove saved credentials
|
|
66
|
+
Every tool that talks to an agent accepts bearer_token, api_key, mTLS
|
|
67
|
+
paths, and custom_headers; credentials are only sent to the origin of
|
|
68
|
+
the given agent URL.
|
|
64
69
|
|
|
65
70
|
Example configuration for Claude Desktop (claude_desktop_config.json):
|
|
66
71
|
|