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.
Files changed (54) hide show
  1. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/PKG-INFO +49 -3
  2. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/README.md +45 -1
  3. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/pyproject.toml +42 -3
  4. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/pyproject.toml.orig +38 -3
  5. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/__init__.py +1 -1
  6. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/auth.py +103 -18
  7. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/__init__.py +4 -2
  8. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/_helpers.py +55 -17
  9. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/card.py +23 -12
  10. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/mcp.py +12 -7
  11. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/message.py +246 -33
  12. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/server.py +152 -14
  13. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/session.py +3 -5
  14. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/task.py +335 -41
  15. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/common/__init__.py +2 -3
  16. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/common/input_validation.py +53 -0
  17. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/mcp/server.py +391 -15
  18. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/servers.py +193 -21
  19. a2a_handler-0.3.1/src/a2a_handler/service.py +1986 -0
  20. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/session.py +3 -5
  21. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/app.py +24 -4
  22. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/app.tcss +37 -0
  23. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/commands/palette.py +11 -1
  24. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/artifacts.py +70 -4
  25. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/auth.py +115 -0
  26. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/card.py +3 -3
  27. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/input.py +45 -2
  28. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/messages.py +149 -13
  29. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/tasks.py +7 -4
  30. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/server/save.py +23 -5
  31. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/server/tab.py +526 -102
  32. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/server/views.py +8 -4
  33. a2a_handler-0.3.1/src/a2a_handler/turn.py +352 -0
  34. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/validation.py +44 -77
  35. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/webhook.py +80 -19
  36. a2a_handler-0.2.0/src/a2a_handler/service.py +0 -745
  37. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/LICENSE +0 -0
  38. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/cli/schema.py +0 -0
  39. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/common/config.py +0 -0
  40. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/common/dotenv.py +0 -0
  41. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/common/logging.py +0 -0
  42. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/common/output.py +0 -0
  43. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/mcp/__init__.py +0 -0
  44. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/__init__.py +0 -0
  45. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/commands/__init__.py +0 -0
  46. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/commands/screens.py +0 -0
  47. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/__init__.py +0 -0
  48. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/headers.py +0 -0
  49. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/components/logs.py +0 -0
  50. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/server/__init__.py +0 -0
  51. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/server/session.py +0 -0
  52. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/server/tabs.py +0 -0
  53. {a2a_handler-0.2.0 → a2a_handler-0.3.1}/src/a2a_handler/tui/server/types.py +0 -0
  54. {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.2.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
  [![CI](https://github.com/alDuncanson/handler/actions/workflows/ci.yml/badge.svg)](https://github.com/alDuncanson/handler/actions/workflows/ci.yml)
46
+ [![Coverage](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FalDuncanson%2FHandler%2Fbadges%2Fcoverage.json)](https://github.com/alDuncanson/Handler/actions/workflows/ci.yml)
47
+ [![SLSA 2](https://slsa.dev/images/gh-badge-level2.svg)](https://github.com/alDuncanson/Handler/attestations)
44
48
  [![A2A Protocol](https://img.shields.io/badge/A2A_Protocol-v1.0.0-blue)](https://a2a-protocol.org/latest/)
45
49
  [![PyPI version](https://img.shields.io/pypi/v/a2a-handler)](https://pypi.org/project/a2a-handler/)
46
50
  [![PyPI - Status](https://img.shields.io/pypi/status/a2a-handler)](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, and OAuth2 client credentials auth.
59
+ API key, HTTP Basic, mTLS, OAuth2 client credentials, and OpenID Connect auth.
56
60
 
57
61
  ![Handler TUI connected to an A2A agent, showing the agent card and a completed assistant response](https://raw.githubusercontent.com/alDuncanson/Handler/73915875903b60dad6e4e404aa7ed91b6d94559f/assets/tui.png)
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
  [![CI](https://github.com/alDuncanson/handler/actions/workflows/ci.yml/badge.svg)](https://github.com/alDuncanson/handler/actions/workflows/ci.yml)
4
+ [![Coverage](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FalDuncanson%2FHandler%2Fbadges%2Fcoverage.json)](https://github.com/alDuncanson/Handler/actions/workflows/ci.yml)
5
+ [![SLSA 2](https://slsa.dev/images/gh-badge-level2.svg)](https://github.com/alDuncanson/Handler/attestations)
4
6
  [![A2A Protocol](https://img.shields.io/badge/A2A_Protocol-v1.0.0-blue)](https://a2a-protocol.org/latest/)
5
7
  [![PyPI version](https://img.shields.io/pypi/v/a2a-handler)](https://pypi.org/project/a2a-handler/)
6
8
  [![PyPI - Status](https://img.shields.io/pypi/status/a2a-handler)](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, and OAuth2 client credentials auth.
17
+ API key, HTTP Basic, mTLS, OAuth2 client credentials, and OpenID Connect auth.
16
18
 
17
19
  ![Handler TUI connected to an A2A agent, showing the agent card and a completed assistant response](https://raw.githubusercontent.com/alDuncanson/Handler/73915875903b60dad6e4e404aa7ed91b6d94559f/assets/tui.png)
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.2.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.10.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.2.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.10.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 = [
@@ -9,4 +9,4 @@ from a2a_handler.tui import HandlerTUI, main
9
9
 
10
10
  __version__ = version("a2a-handler")
11
11
 
12
- __all__ = ["__version__", "HandlerTUI", "main"]
12
+ __all__ = ["HandlerTUI", "__version__", "main"]
@@ -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), and OAuth2 client credentials
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
- client_id: str | None = None # For OAuth2: client ID
53
- client_secret: str | None = None # For OAuth2: client secret
54
- scopes: list[str] | None = None # For OAuth2: optional scopes
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"] = {k: "***" for k in self.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
- headers["Authorization"] = f"Bearer {self.value}"
86
- elif self.auth_type == AuthType.OAUTH2 and self.value:
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 OAuth2 client credentials grant.
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 != AuthType.OAUTH2:
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) -> "AuthCredentials":
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
- else:
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",