a2a-handler 0.2.0__tar.gz → 0.3.1__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.1}/PKG-INFO +49 -3
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/README.md +45 -1
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/pyproject.toml +42 -3
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/pyproject.toml.orig +38 -3
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/__init__.py +1 -1
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/auth.py +103 -18
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/__init__.py +4 -2
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/_helpers.py +55 -17
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/card.py +23 -12
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/mcp.py +12 -7
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/message.py +246 -33
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/server.py +152 -14
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/session.py +3 -5
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/task.py +335 -41
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/common/__init__.py +2 -3
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/common/input_validation.py +53 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/mcp/server.py +391 -15
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/servers.py +193 -21
- a2a_handler-0.3.1/src/a2a_handler/service.py +1986 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/session.py +3 -5
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/app.py +24 -4
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/app.tcss +37 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/commands/palette.py +11 -1
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/artifacts.py +70 -4
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/auth.py +115 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/card.py +3 -3
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/input.py +45 -2
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/messages.py +149 -13
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/tasks.py +7 -4
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/server/save.py +23 -5
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/server/tab.py +526 -102
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/server/views.py +8 -4
- a2a_handler-0.3.1/src/a2a_handler/turn.py +352 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/validation.py +44 -77
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/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.1}/LICENSE +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/schema.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/common/config.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/common/dotenv.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/common/logging.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/common/output.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/mcp/__init__.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/__init__.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/commands/__init__.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/commands/screens.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/__init__.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/headers.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/logs.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/server/__init__.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/server/session.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/server/tabs.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/server/types.py +0 -0
- {a2a_handler-0.2.0 → a2a_handler-0.3.1}/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.1
|
|
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,12 +10,12 @@ 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
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
20
|
Classifier: Typing :: Typed
|
|
21
21
|
Requires-Dist: a2a-sdk[http-server]>=1.0.0,<2.0.0
|
|
@@ -30,17 +30,21 @@ Requires-Dist: textual-serve>=1.1.0
|
|
|
30
30
|
Requires-Dist: mcp[cli]>=1.27.1,<2
|
|
31
31
|
Requires-Dist: truststore>=0.9.0
|
|
32
32
|
Requires-Dist: platformdirs>=4.0.0
|
|
33
|
+
Requires-Dist: a2a-sdk[grpc]>=1.0.0,<2.0.0 ; extra == 'grpc'
|
|
33
34
|
Requires-Python: >=3.11
|
|
34
35
|
Project-URL: Homepage, https://github.com/alDuncanson/handler
|
|
35
36
|
Project-URL: Repository, https://github.com/alDuncanson/handler
|
|
36
37
|
Project-URL: Documentation, https://github.com/alDuncanson/handler/tree/main/docs
|
|
37
38
|
Project-URL: Issues, https://github.com/alDuncanson/handler/issues
|
|
38
39
|
Project-URL: Changelog, https://github.com/alDuncanson/handler/releases
|
|
40
|
+
Provides-Extra: grpc
|
|
39
41
|
Description-Content-Type: text/markdown
|
|
40
42
|
|
|
41
43
|
# Handler
|
|
42
44
|
|
|
43
45
|
[](https://github.com/alDuncanson/handler/actions/workflows/ci.yml)
|
|
46
|
+
[](https://github.com/alDuncanson/Handler/actions/workflows/ci.yml)
|
|
47
|
+
[](https://github.com/alDuncanson/Handler/attestations)
|
|
44
48
|
[](https://a2a-protocol.org/latest/)
|
|
45
49
|
[](https://pypi.org/project/a2a-handler/)
|
|
46
50
|
[](https://pypi.org/project/a2a-handler/)
|
|
@@ -52,10 +56,25 @@ client for software engineers building, testing, and operating agentic systems.
|
|
|
52
56
|
It provides an interactive TUI, a scriptable CLI with structured output, and an
|
|
53
57
|
MCP server that lets other agents integrate with A2A services directly. Handler
|
|
54
58
|
also supports global and repo-scoped A2A server configuration with bearer,
|
|
55
|
-
API key, mTLS,
|
|
59
|
+
API key, HTTP Basic, mTLS, OAuth2 client credentials, and OpenID Connect auth.
|
|
56
60
|
|
|
57
61
|

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

|
|
18
20
|
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- Streams replies as they arrive, in the TUI and the CLI, and can stop a
|
|
24
|
+
running task or answer an agent that pauses for input
|
|
25
|
+
- Sends text, files (inline or by URL), and structured data, and shows the
|
|
26
|
+
file and data parts agents send back
|
|
27
|
+
- Lists, inspects, cancels, and resubscribes to tasks, and manages push
|
|
28
|
+
notification configs with a bundled local webhook receiver
|
|
29
|
+
- Speaks JSON-RPC and HTTP+JSON out of the box, gRPC with the `grpc` extra,
|
|
30
|
+
and lets the agent card pick the transport
|
|
31
|
+
- Requests A2A extensions and fetches the extended card an agent offers to
|
|
32
|
+
authenticated clients
|
|
33
|
+
- Keeps credentials on the origin you configured, never on a host an agent
|
|
34
|
+
card happens to name
|
|
35
|
+
|
|
19
36
|
## Install
|
|
20
37
|
|
|
21
38
|
Install Handler from the [PyPI package](https://pypi.org/project/a2a-handler/) as a `uv` tool:
|
|
@@ -56,6 +73,18 @@ Send a message from the CLI:
|
|
|
56
73
|
handler message send --url URL --text "hello"
|
|
57
74
|
```
|
|
58
75
|
|
|
76
|
+
Stream the reply, with a file attached:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
handler message stream --url URL --text "Review this" --file ./report.pdf
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
List the agent's tasks:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
handler task list --url URL
|
|
86
|
+
```
|
|
87
|
+
|
|
59
88
|
Open the full documentation:
|
|
60
89
|
|
|
61
90
|
```bash
|
|
@@ -79,3 +108,18 @@ pipx run a2a-handler
|
|
|
79
108
|
## Documentation
|
|
80
109
|
|
|
81
110
|
Read the documentation at <https://handler.alduncanson.com>.
|
|
111
|
+
|
|
112
|
+
### For coding agents
|
|
113
|
+
|
|
114
|
+
The docs ship as an agent skill, regenerated on every deploy. Install it into
|
|
115
|
+
Claude Code, Cursor, Windsurf, or any agent that reads the `SKILL.md` format:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npx skills add https://handler.alduncanson.com
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Or fetch it by hand from <https://handler.alduncanson.com/skill.md>. For live
|
|
122
|
+
lookups, agents can also use the hosted documentation MCP server at
|
|
123
|
+
<https://handler.alduncanson.com/mcp> or the page index at
|
|
124
|
+
<https://handler.alduncanson.com/llms.txt>. See the
|
|
125
|
+
[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.1"
|
|
4
4
|
description = "An A2A Protocol client TUI and CLI."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -19,12 +19,12 @@ 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",
|
|
26
25
|
"Programming Language :: Python :: 3.12",
|
|
27
26
|
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
28
28
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
29
29
|
"Typing :: Typed",
|
|
30
30
|
]
|
|
@@ -47,6 +47,9 @@ dependencies = [
|
|
|
47
47
|
name = "Al Duncanson"
|
|
48
48
|
email = "alDuncanson@proton.me"
|
|
49
49
|
|
|
50
|
+
[project.optional-dependencies]
|
|
51
|
+
grpc = ["a2a-sdk[grpc]>=1.0.0,<2.0.0"]
|
|
52
|
+
|
|
50
53
|
[project.urls]
|
|
51
54
|
Homepage = "https://github.com/alDuncanson/handler"
|
|
52
55
|
Repository = "https://github.com/alDuncanson/handler"
|
|
@@ -69,7 +72,7 @@ dev = [
|
|
|
69
72
|
]
|
|
70
73
|
|
|
71
74
|
[build-system]
|
|
72
|
-
requires = ["uv_build>=0.9.15,<0.
|
|
75
|
+
requires = ["uv_build>=0.9.15,<0.14.0"]
|
|
73
76
|
build-backend = "uv_build"
|
|
74
77
|
|
|
75
78
|
[tool.uv.build-backend]
|
|
@@ -83,13 +86,49 @@ default = true
|
|
|
83
86
|
[tool.pytest.ini_options]
|
|
84
87
|
asyncio_mode = "auto"
|
|
85
88
|
|
|
89
|
+
[tool.ruff.lint]
|
|
90
|
+
select = [
|
|
91
|
+
"E",
|
|
92
|
+
"W",
|
|
93
|
+
"F",
|
|
94
|
+
"I",
|
|
95
|
+
"UP",
|
|
96
|
+
"B",
|
|
97
|
+
"A",
|
|
98
|
+
"C4",
|
|
99
|
+
"SIM",
|
|
100
|
+
"PT",
|
|
101
|
+
"ASYNC",
|
|
102
|
+
"S",
|
|
103
|
+
"LOG",
|
|
104
|
+
"G",
|
|
105
|
+
"PERF",
|
|
106
|
+
"N",
|
|
107
|
+
"ERA",
|
|
108
|
+
"RUF",
|
|
109
|
+
]
|
|
110
|
+
ignore = [
|
|
111
|
+
"E501",
|
|
112
|
+
"S101",
|
|
113
|
+
]
|
|
114
|
+
|
|
86
115
|
[tool.ruff.lint.per-file-ignores]
|
|
87
116
|
"src/a2a_handler/cli/__init__.py" = ["E402"]
|
|
117
|
+
"src/a2a_handler/tui/**" = ["A002"]
|
|
118
|
+
"tests/**" = [
|
|
119
|
+
"B009",
|
|
120
|
+
"B010",
|
|
121
|
+
"S104",
|
|
122
|
+
"S105",
|
|
123
|
+
"S106",
|
|
124
|
+
"S108",
|
|
125
|
+
]
|
|
88
126
|
|
|
89
127
|
[tool.coverage.run]
|
|
90
128
|
source = ["a2a_handler"]
|
|
91
129
|
|
|
92
130
|
[tool.coverage.report]
|
|
131
|
+
fail_under = 88
|
|
93
132
|
show_missing = true
|
|
94
133
|
skip_empty = true
|
|
95
134
|
exclude_lines = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "a2a-handler"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
description = "An A2A Protocol client TUI and CLI."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -22,12 +22,12 @@ 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",
|
|
29
28
|
"Programming Language :: Python :: 3.12",
|
|
30
29
|
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Programming Language :: Python :: 3.14",
|
|
31
31
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
32
32
|
"Typing :: Typed",
|
|
33
33
|
]
|
|
@@ -48,6 +48,12 @@ dependencies = [
|
|
|
48
48
|
"platformdirs>=4.0.0",
|
|
49
49
|
]
|
|
50
50
|
|
|
51
|
+
[project.optional-dependencies]
|
|
52
|
+
# Talking to gRPC-only agents needs the SDK's gRPC transport and its
|
|
53
|
+
# grpcio dependencies, which are heavy enough to keep out of the default
|
|
54
|
+
# install.
|
|
55
|
+
grpc = ["a2a-sdk[grpc]>=1.0.0,<2.0.0"]
|
|
56
|
+
|
|
51
57
|
[dependency-groups]
|
|
52
58
|
dev = [
|
|
53
59
|
"ruff>=0.8.0",
|
|
@@ -70,7 +76,7 @@ Changelog = "https://github.com/alDuncanson/handler/releases"
|
|
|
70
76
|
handler = "a2a_handler.cli:main"
|
|
71
77
|
|
|
72
78
|
[build-system]
|
|
73
|
-
requires = ["uv_build>=0.9.15,<0.
|
|
79
|
+
requires = ["uv_build>=0.9.15,<0.14.0"]
|
|
74
80
|
build-backend = "uv_build"
|
|
75
81
|
|
|
76
82
|
[tool.uv.build-backend]
|
|
@@ -88,13 +94,42 @@ default = true
|
|
|
88
94
|
[tool.pytest.ini_options]
|
|
89
95
|
asyncio_mode = "auto"
|
|
90
96
|
|
|
97
|
+
[tool.ruff.lint]
|
|
98
|
+
select = [
|
|
99
|
+
"E", "W", # pycodestyle
|
|
100
|
+
"F", # pyflakes
|
|
101
|
+
"I", # isort
|
|
102
|
+
"UP", # pyupgrade
|
|
103
|
+
"B", # bugbear
|
|
104
|
+
"A", # shadowed builtins
|
|
105
|
+
"C4", # comprehensions
|
|
106
|
+
"SIM", # simplify
|
|
107
|
+
"PT", # pytest style
|
|
108
|
+
"ASYNC", # async pitfalls
|
|
109
|
+
"S", # bandit
|
|
110
|
+
"LOG", "G", # logging
|
|
111
|
+
"PERF", # perflint
|
|
112
|
+
"N", # pep8-naming
|
|
113
|
+
"ERA", # commented-out code
|
|
114
|
+
"RUF", # ruff-specific
|
|
115
|
+
]
|
|
116
|
+
ignore = [
|
|
117
|
+
"E501", # line length is ruff format's job
|
|
118
|
+
"S101", # asserts narrow types for ty; Handler never runs under -O
|
|
119
|
+
]
|
|
120
|
+
|
|
91
121
|
[tool.ruff.lint.per-file-ignores]
|
|
92
122
|
"src/a2a_handler/cli/__init__.py" = ["E402"]
|
|
123
|
+
# Textual widgets take `id`, like their base classes.
|
|
124
|
+
"src/a2a_handler/tui/**" = ["A002"]
|
|
125
|
+
# Tests monkeypatch with setattr and hold fixture secrets, paths, and bind addresses.
|
|
126
|
+
"tests/**" = ["B009", "B010", "S104", "S105", "S106", "S108"]
|
|
93
127
|
|
|
94
128
|
[tool.coverage.run]
|
|
95
129
|
source = ["a2a_handler"]
|
|
96
130
|
|
|
97
131
|
[tool.coverage.report]
|
|
132
|
+
fail_under = 88
|
|
98
133
|
show_missing = true
|
|
99
134
|
skip_empty = true
|
|
100
135
|
exclude_lines = [
|
|
@@ -1,18 +1,20 @@
|
|
|
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
|
|
13
14
|
from enum import Enum
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
|
|
17
|
+
import httpx
|
|
16
18
|
|
|
17
19
|
from a2a_handler.common.input_validation import (
|
|
18
20
|
check_key_file_permissions,
|
|
@@ -20,9 +22,6 @@ from a2a_handler.common.input_validation import (
|
|
|
20
22
|
validate_token_url,
|
|
21
23
|
)
|
|
22
24
|
|
|
23
|
-
import httpx
|
|
24
|
-
|
|
25
|
-
|
|
26
25
|
_TOKEN_EXPIRY_MARGIN = 30 # seconds before expiry to trigger refresh
|
|
27
26
|
|
|
28
27
|
|
|
@@ -30,9 +29,18 @@ class AuthType(str, Enum):
|
|
|
30
29
|
"""Supported authentication types."""
|
|
31
30
|
|
|
32
31
|
API_KEY = "api_key"
|
|
32
|
+
BASIC = "basic"
|
|
33
33
|
BEARER = "bearer"
|
|
34
34
|
MTLS = "mtls"
|
|
35
35
|
OAUTH2 = "oauth2"
|
|
36
|
+
OIDC = "oidc"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
#: Auth types whose access token is fetched from a token endpoint and cached.
|
|
40
|
+
TOKEN_FETCHING_AUTH_TYPES = {AuthType.OAUTH2, AuthType.OIDC}
|
|
41
|
+
|
|
42
|
+
#: Path of the OpenID Connect discovery document below the issuer URL.
|
|
43
|
+
OIDC_DISCOVERY_PATH = "/.well-known/openid-configuration"
|
|
36
44
|
|
|
37
45
|
|
|
38
46
|
@dataclass
|
|
@@ -45,13 +53,15 @@ class AuthCredentials:
|
|
|
45
53
|
auth_type: AuthType
|
|
46
54
|
value: str = ""
|
|
47
55
|
header_name: str | None = None # For API key: custom header name
|
|
56
|
+
username: str | None = None # For HTTP basic: username (value is password)
|
|
48
57
|
cert_path: str | None = None # For mTLS: client certificate path
|
|
49
58
|
key_path: str | None = None # For mTLS: client private key path
|
|
50
59
|
ca_cert_path: str | None = None # For mTLS: CA certificate path
|
|
51
|
-
token_url: str | None = None # For OAuth2: token endpoint
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
token_url: str | None = None # For OAuth2/OIDC: token endpoint
|
|
61
|
+
issuer_url: str | None = None # For OIDC: issuer to discover endpoints from
|
|
62
|
+
client_id: str | None = None # For OAuth2/OIDC: client ID
|
|
63
|
+
client_secret: str | None = None # For OAuth2/OIDC: client secret
|
|
64
|
+
scopes: list[str] | None = None # For OAuth2/OIDC: optional scopes
|
|
55
65
|
custom_headers: dict[str, str] | None = None # Additional headers for any auth type
|
|
56
66
|
_token_expires_at: float | None = field(default=None, repr=False)
|
|
57
67
|
|
|
@@ -64,12 +74,13 @@ class AuthCredentials:
|
|
|
64
74
|
"key_path": self.key_path,
|
|
65
75
|
"ca_cert_path": self.ca_cert_path,
|
|
66
76
|
"token_url": self.token_url,
|
|
77
|
+
"issuer_url": self.issuer_url,
|
|
67
78
|
}
|
|
68
|
-
for secret_field in ("value", "client_id", "client_secret"):
|
|
79
|
+
for secret_field in ("value", "username", "client_id", "client_secret"):
|
|
69
80
|
val = getattr(self, secret_field)
|
|
70
81
|
redacted[secret_field] = "***" if val else None
|
|
71
82
|
if self.custom_headers:
|
|
72
|
-
redacted["custom_headers"] =
|
|
83
|
+
redacted["custom_headers"] = dict.fromkeys(self.custom_headers, "***")
|
|
73
84
|
return f"AuthCredentials({redacted})"
|
|
74
85
|
|
|
75
86
|
def to_headers(self) -> dict[str, str]:
|
|
@@ -81,10 +92,14 @@ class AuthCredentials:
|
|
|
81
92
|
headers: dict[str, str] = {}
|
|
82
93
|
if self.custom_headers:
|
|
83
94
|
headers.update(self.custom_headers)
|
|
84
|
-
if self.auth_type == AuthType.BEARER and self.value
|
|
85
|
-
|
|
86
|
-
|
|
95
|
+
if (self.auth_type == AuthType.BEARER and self.value) or (
|
|
96
|
+
self.auth_type in TOKEN_FETCHING_AUTH_TYPES and self.value
|
|
97
|
+
):
|
|
87
98
|
headers["Authorization"] = f"Bearer {self.value}"
|
|
99
|
+
elif self.auth_type == AuthType.BASIC and self.username is not None:
|
|
100
|
+
userpass = f"{self.username}:{self.value}".encode()
|
|
101
|
+
encoded = base64.b64encode(userpass).decode("ascii")
|
|
102
|
+
headers["Authorization"] = f"Basic {encoded}"
|
|
88
103
|
elif self.auth_type == AuthType.API_KEY:
|
|
89
104
|
header = self.header_name or "X-API-Key"
|
|
90
105
|
headers[header] = self.value
|
|
@@ -122,8 +137,39 @@ class AuthCredentials:
|
|
|
122
137
|
self.value = ""
|
|
123
138
|
self._token_expires_at = None
|
|
124
139
|
|
|
140
|
+
async def discover_oidc_token_endpoint(self) -> str:
|
|
141
|
+
"""Resolve the token endpoint from the OIDC discovery document.
|
|
142
|
+
|
|
143
|
+
Fetches ``<issuer>/.well-known/openid-configuration`` and caches the
|
|
144
|
+
advertised ``token_endpoint`` on ``token_url``.
|
|
145
|
+
"""
|
|
146
|
+
if self.auth_type != AuthType.OIDC:
|
|
147
|
+
raise ValueError("OIDC discovery is only valid for OIDC credentials")
|
|
148
|
+
if self.token_url:
|
|
149
|
+
return self.token_url
|
|
150
|
+
if not self.issuer_url:
|
|
151
|
+
raise ValueError("issuer_url is required for OIDC discovery")
|
|
152
|
+
validate_token_url(self.issuer_url)
|
|
153
|
+
|
|
154
|
+
discovery_url = self.issuer_url.rstrip("/") + OIDC_DISCOVERY_PATH
|
|
155
|
+
async with httpx.AsyncClient(trust_env=False) as discovery_client:
|
|
156
|
+
response = await discovery_client.get(discovery_url)
|
|
157
|
+
response.raise_for_status()
|
|
158
|
+
document = response.json()
|
|
159
|
+
token_endpoint = document.get("token_endpoint")
|
|
160
|
+
if not isinstance(token_endpoint, str) or not token_endpoint:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"OIDC discovery document at {discovery_url} has no token_endpoint"
|
|
163
|
+
)
|
|
164
|
+
validate_token_url(token_endpoint)
|
|
165
|
+
self.token_url = token_endpoint
|
|
166
|
+
return token_endpoint
|
|
167
|
+
|
|
125
168
|
async def fetch_oauth2_token(self) -> str:
|
|
126
|
-
"""Fetch an access token using
|
|
169
|
+
"""Fetch an access token using the client credentials grant.
|
|
170
|
+
|
|
171
|
+
Valid for OAuth2 and OIDC credentials; OIDC resolves its token
|
|
172
|
+
endpoint through the issuer's discovery document first.
|
|
127
173
|
|
|
128
174
|
Uses a short-lived HTTP client so that agent auth headers and custom
|
|
129
175
|
headers on the main client are never sent to the token endpoint.
|
|
@@ -131,8 +177,10 @@ class AuthCredentials:
|
|
|
131
177
|
Updates self.value with the new token and returns it.
|
|
132
178
|
Parses ``expires_in`` from the token response to track expiry.
|
|
133
179
|
"""
|
|
134
|
-
if self.auth_type
|
|
135
|
-
raise ValueError("Token fetch is only valid for OAuth2 credentials")
|
|
180
|
+
if self.auth_type not in TOKEN_FETCHING_AUTH_TYPES:
|
|
181
|
+
raise ValueError("Token fetch is only valid for OAuth2 or OIDC credentials")
|
|
182
|
+
if self.auth_type == AuthType.OIDC:
|
|
183
|
+
await self.discover_oidc_token_endpoint()
|
|
136
184
|
if not self.token_url or not self.client_id or not self.client_secret:
|
|
137
185
|
raise ValueError("token_url, client_id, and client_secret are required")
|
|
138
186
|
validate_token_url(self.token_url)
|
|
@@ -168,6 +216,10 @@ class AuthCredentials:
|
|
|
168
216
|
"value": self.value,
|
|
169
217
|
"header_name": self.header_name,
|
|
170
218
|
}
|
|
219
|
+
if self.username is not None:
|
|
220
|
+
data["username"] = self.username
|
|
221
|
+
if self.issuer_url:
|
|
222
|
+
data["issuer_url"] = self.issuer_url
|
|
171
223
|
if self.cert_path:
|
|
172
224
|
data["cert_path"] = self.cert_path
|
|
173
225
|
if self.key_path:
|
|
@@ -187,7 +239,7 @@ class AuthCredentials:
|
|
|
187
239
|
return data
|
|
188
240
|
|
|
189
241
|
@classmethod
|
|
190
|
-
def from_dict(cls, data: dict) ->
|
|
242
|
+
def from_dict(cls, data: dict) -> AuthCredentials:
|
|
191
243
|
"""Deserialize credentials from storage."""
|
|
192
244
|
custom_headers_raw = data.get("custom_headers")
|
|
193
245
|
custom_headers = (
|
|
@@ -197,6 +249,8 @@ class AuthCredentials:
|
|
|
197
249
|
auth_type=AuthType(data["auth_type"]),
|
|
198
250
|
value=data.get("value") or "",
|
|
199
251
|
header_name=data.get("header_name"),
|
|
252
|
+
username=data.get("username"),
|
|
253
|
+
issuer_url=data.get("issuer_url"),
|
|
200
254
|
cert_path=data.get("cert_path"),
|
|
201
255
|
key_path=data.get("key_path"),
|
|
202
256
|
ca_cert_path=data.get("ca_cert_path"),
|
|
@@ -226,6 +280,15 @@ def create_bearer_auth(token: str) -> AuthCredentials:
|
|
|
226
280
|
return AuthCredentials(auth_type=AuthType.BEARER, value=token)
|
|
227
281
|
|
|
228
282
|
|
|
283
|
+
def create_basic_auth(username: str, password: str) -> AuthCredentials:
|
|
284
|
+
"""Create HTTP basic authentication."""
|
|
285
|
+
return AuthCredentials(
|
|
286
|
+
auth_type=AuthType.BASIC,
|
|
287
|
+
username=username,
|
|
288
|
+
value=password,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
229
292
|
def create_api_key_auth(
|
|
230
293
|
key: str,
|
|
231
294
|
header_name: str = "X-API-Key",
|
|
@@ -286,3 +349,25 @@ def create_oauth2_auth(
|
|
|
286
349
|
client_secret=client_secret,
|
|
287
350
|
scopes=scopes,
|
|
288
351
|
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def create_oidc_auth(
|
|
355
|
+
issuer_url: str,
|
|
356
|
+
client_id: str,
|
|
357
|
+
client_secret: str,
|
|
358
|
+
scopes: list[str] | None = None,
|
|
359
|
+
access_token: str = "",
|
|
360
|
+
) -> AuthCredentials:
|
|
361
|
+
"""Create OpenID Connect authentication.
|
|
362
|
+
|
|
363
|
+
The token endpoint is discovered from the issuer's
|
|
364
|
+
``/.well-known/openid-configuration`` document on first token fetch.
|
|
365
|
+
"""
|
|
366
|
+
return AuthCredentials(
|
|
367
|
+
auth_type=AuthType.OIDC,
|
|
368
|
+
value=access_token,
|
|
369
|
+
issuer_url=issuer_url,
|
|
370
|
+
client_id=client_id,
|
|
371
|
+
client_secret=client_secret,
|
|
372
|
+
scopes=scopes,
|
|
373
|
+
)
|
|
@@ -151,8 +151,10 @@ def cli(
|
|
|
151
151
|
setup_logging(level="DEBUG")
|
|
152
152
|
elif verbose:
|
|
153
153
|
setup_logging(level="INFO")
|
|
154
|
-
|
|
154
|
+
elif quiet:
|
|
155
155
|
setup_logging(level="ERROR")
|
|
156
|
+
else:
|
|
157
|
+
setup_logging(level="WARNING")
|
|
156
158
|
|
|
157
159
|
|
|
158
160
|
cli.add_command(message)
|
|
@@ -221,7 +223,7 @@ def update() -> None:
|
|
|
221
223
|
)
|
|
222
224
|
raise SystemExit(1)
|
|
223
225
|
|
|
224
|
-
result = subprocess.run(command, check=False)
|
|
226
|
+
result = subprocess.run(command, check=False) # noqa: S603
|
|
225
227
|
if result.returncode != 0:
|
|
226
228
|
output.error(
|
|
227
229
|
code="update_failed",
|