a2a-handler 0.1.15__tar.gz → 0.1.17__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 (43) hide show
  1. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/PKG-INFO +3 -3
  2. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/pyproject.toml +3 -3
  3. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/cli/__init__.py +3 -2
  4. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/cli/card.py +5 -1
  5. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/server/app.py +1 -0
  6. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/service.py +36 -1
  7. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/tui/app.py +19 -4
  8. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/tui/components/auth.py +10 -0
  9. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/tui/components/messages.py +5 -0
  10. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/validation.py +19 -1
  11. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/LICENSE +0 -0
  12. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/README.md +0 -0
  13. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/__init__.py +0 -0
  14. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/auth.py +0 -0
  15. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/cli/_config.py +0 -0
  16. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/cli/_helpers.py +0 -0
  17. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/cli/auth.py +0 -0
  18. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/cli/mcp.py +0 -0
  19. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/cli/message.py +0 -0
  20. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/cli/server.py +0 -0
  21. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/cli/session.py +0 -0
  22. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/cli/task.py +0 -0
  23. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/common/__init__.py +0 -0
  24. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/common/config.py +0 -0
  25. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/common/logging.py +0 -0
  26. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/common/output.py +0 -0
  27. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/mcp/__init__.py +0 -0
  28. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/mcp/server.py +0 -0
  29. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/server/__init__.py +0 -0
  30. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/server/agent.py +0 -0
  31. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/server/card.py +0 -0
  32. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/server/ollama.py +0 -0
  33. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/session.py +0 -0
  34. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/tui/__init__.py +0 -0
  35. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/tui/app.tcss +0 -0
  36. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/tui/components/__init__.py +0 -0
  37. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/tui/components/artifacts.py +0 -0
  38. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/tui/components/card.py +0 -0
  39. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/tui/components/contact.py +0 -0
  40. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/tui/components/input.py +0 -0
  41. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/tui/components/logs.py +0 -0
  42. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/tui/components/tasks.py +0 -0
  43. {a2a_handler-0.1.15 → a2a_handler-0.1.17}/src/a2a_handler/webhook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: a2a-handler
3
- Version: 0.1.15
3
+ Version: 0.1.17
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
@@ -18,10 +18,10 @@ Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
20
  Classifier: Typing :: Typed
21
- Requires-Dist: a2a-sdk>=0.3.22
21
+ Requires-Dist: a2a-sdk>=0.3.24
22
22
  Requires-Dist: click>=8.0.0
23
23
  Requires-Dist: rich-click>=1.8.0
24
- Requires-Dist: google-adk>=0.5.0
24
+ Requires-Dist: google-adk>=1.26.0
25
25
  Requires-Dist: httpx>=0.27.0
26
26
  Requires-Dist: litellm>=1.0.0
27
27
  Requires-Dist: python-dotenv>=1.0.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "a2a-handler"
3
- version = "0.1.15"
3
+ version = "0.1.17"
4
4
  description = "An A2A Protocol client TUI and CLI."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -32,10 +32,10 @@ classifiers = [
32
32
  "Typing :: Typed",
33
33
  ]
34
34
  dependencies = [
35
- "a2a-sdk>=0.3.22",
35
+ "a2a-sdk>=0.3.24",
36
36
  "click>=8.0.0",
37
37
  "rich-click>=1.8.0",
38
- "google-adk>=0.5.0",
38
+ "google-adk>=1.26.0",
39
39
  "httpx>=0.27.0",
40
40
  "litellm>=1.0.0",
41
41
  "python-dotenv>=1.0.0",
@@ -67,11 +67,12 @@ def version() -> None:
67
67
 
68
68
 
69
69
  @cli.command()
70
- def tui() -> None:
70
+ @click.option("--bearer", "-b", "bearer_token", help="Bearer token for agent auth")
71
+ def tui(bearer_token: str | None) -> None:
71
72
  """Launch the interactive terminal interface."""
72
73
  log.info("Launching TUI")
73
74
  logging.getLogger().handlers = []
74
- app = HandlerTUI()
75
+ app = HandlerTUI(initial_bearer_token=bearer_token)
75
76
  app.run()
76
77
 
77
78
 
@@ -9,6 +9,7 @@ from a2a.types import AgentCard
9
9
 
10
10
  from a2a_handler.common import Output, get_logger
11
11
  from a2a_handler.service import A2AService
12
+ from a2a_handler.session import get_credentials
12
13
  from a2a_handler.validation import (
13
14
  ValidationResult,
14
15
  validate_agent_card_from_file,
@@ -34,12 +35,15 @@ def card() -> None:
34
35
  def card_get(agent_url: str, authenticated: bool) -> None:
35
36
  """Retrieve an agent's card."""
36
37
  log.info("Fetching agent card from %s", agent_url)
38
+ credentials = get_credentials(agent_url) if authenticated else None
39
+ if authenticated and credentials is None:
40
+ log.warning("No saved credentials found for %s", agent_url)
37
41
 
38
42
  async def do_get() -> None:
39
43
  output = Output()
40
44
  try:
41
45
  async with build_http_client() as http_client:
42
- service = A2AService(http_client, agent_url)
46
+ service = A2AService(http_client, agent_url, credentials=credentials)
43
47
  card_data = await service.get_card()
44
48
  log.info("Retrieved card for agent: %s", card_data.name)
45
49
 
@@ -48,6 +48,7 @@ class APIKeyAuthMiddleware(BaseHTTPMiddleware):
48
48
 
49
49
  OPEN_PATHS = {
50
50
  "/.well-known/agent-card.json",
51
+ "/.well-known/agent.json",
51
52
  "/health",
52
53
  }
53
54
 
@@ -9,6 +9,7 @@ from typing import AsyncIterator
9
9
 
10
10
  import httpx
11
11
  from a2a.client import A2ACardResolver, Client, ClientConfig, ClientFactory
12
+ from a2a.client.errors import A2AClientHTTPError
12
13
  from a2a.types import (
13
14
  AgentCard,
14
15
  GetTaskPushNotificationConfigParams,
@@ -27,6 +28,11 @@ from a2a.types import (
27
28
  TransportProtocol,
28
29
  )
29
30
 
31
+ from a2a.utils.constants import (
32
+ AGENT_CARD_WELL_KNOWN_PATH,
33
+ PREV_AGENT_CARD_WELL_KNOWN_PATH,
34
+ )
35
+
30
36
  from a2a_handler.auth import AuthCredentials
31
37
  from a2a_handler.common import get_logger
32
38
 
@@ -255,18 +261,47 @@ class A2AService:
255
261
  auth_headers = credentials.to_headers()
256
262
  self.http_client.headers.update(auth_headers)
257
263
  self._applied_auth_headers = set(auth_headers.keys())
264
+ # Rebuild the SDK client so updated headers are guaranteed to be used.
265
+ self._cached_client = None
258
266
  logger.debug("Applied authentication headers: %s", list(auth_headers.keys()))
259
267
 
268
+ def clear_credentials(self) -> None:
269
+ """Clear authentication credentials from the service and HTTP client."""
270
+ for header_name in self._applied_auth_headers:
271
+ self.http_client.headers.pop(header_name, None)
272
+ self._applied_auth_headers.clear()
273
+ self.credentials = None
274
+ # Rebuild the SDK client so cleared headers are guaranteed to be used.
275
+ self._cached_client = None
276
+ logger.debug("Cleared authentication headers")
277
+
260
278
  async def get_card(self) -> AgentCard:
261
279
  """Fetch and cache the agent card.
262
280
 
281
+ Tries the standard well-known path first (``agent-card.json``), then
282
+ falls back to the previous path (``agent.json``) used by older ADK
283
+ versions.
284
+
263
285
  Returns:
264
286
  The agent's card with metadata and capabilities
265
287
  """
266
288
  if self._cached_agent_card is None:
267
289
  logger.info("Fetching agent card from %s", self.agent_url)
268
290
  card_resolver = A2ACardResolver(self.http_client, self.agent_url)
269
- self._cached_agent_card = await card_resolver.get_agent_card()
291
+ try:
292
+ self._cached_agent_card = await card_resolver.get_agent_card()
293
+ except (A2AClientHTTPError, httpx.HTTPStatusError):
294
+ logger.info(
295
+ "Agent card not found at %s, trying %s",
296
+ AGENT_CARD_WELL_KNOWN_PATH,
297
+ PREV_AGENT_CARD_WELL_KNOWN_PATH,
298
+ )
299
+ fallback_resolver = A2ACardResolver(
300
+ self.http_client,
301
+ self.agent_url,
302
+ agent_card_path=PREV_AGENT_CARD_WELL_KNOWN_PATH,
303
+ )
304
+ self._cached_agent_card = await fallback_resolver.get_agent_card()
270
305
  logger.info("Connected to agent: %s", self._cached_agent_card.name)
271
306
  return self._cached_agent_card
272
307
 
@@ -19,6 +19,7 @@ from textual.logging import TextualHandler
19
19
  from textual.screen import Screen
20
20
  from textual.widgets import Button, Footer, Input
21
21
 
22
+ from a2a_handler.auth import AuthCredentials
22
23
  from a2a_handler.common import get_theme, install_tui_log_handler, save_theme
23
24
  from a2a_handler.service import A2AService
24
25
  from a2a_handler.tui.components import (
@@ -72,7 +73,7 @@ class HandlerTUI(App[Any]):
72
73
  return False
73
74
  return True
74
75
 
75
- def __init__(self, **kwargs: Any) -> None:
76
+ def __init__(self, initial_bearer_token: str | None = None, **kwargs: Any) -> None:
76
77
  super().__init__(**kwargs)
77
78
  self.current_agent_card: AgentCard | None = None
78
79
  self.http_client: httpx.AsyncClient | None = None
@@ -80,6 +81,7 @@ class HandlerTUI(App[Any]):
80
81
  self.current_agent_url: str | None = None
81
82
  self._agent_service: A2AService | None = None
82
83
  self._is_maximized: bool = False
84
+ self._initial_bearer_token = initial_bearer_token
83
85
 
84
86
  def compose(self) -> ComposeResult:
85
87
  with Container(id="root-container"):
@@ -102,6 +104,8 @@ class HandlerTUI(App[Any]):
102
104
 
103
105
  messages_panel = self.query_one("#messages-container", TabbedMessagesPanel)
104
106
  messages_panel.load_logs(tui_log_handler.get_lines())
107
+ if self._initial_bearer_token:
108
+ messages_panel.set_bearer_token(self._initial_bearer_token)
105
109
 
106
110
  contact_panel = self.query_one("#contact-container", ContactPanel)
107
111
  contact_panel.set_version(__version__)
@@ -125,12 +129,20 @@ class HandlerTUI(App[Any]):
125
129
  agent_card_panel = self.query_one("#agent-card-container", AgentCardPanel)
126
130
  agent_card_panel.refresh_theme()
127
131
 
128
- async def _connect_to_agent(self, agent_url: str) -> AgentCard:
132
+ async def _connect_to_agent(
133
+ self,
134
+ agent_url: str,
135
+ credentials: AuthCredentials | None = None,
136
+ ) -> AgentCard:
129
137
  if not self.http_client:
130
138
  raise RuntimeError("HTTP client not initialized")
131
139
 
132
140
  logger.info("Connecting to agent at %s", agent_url)
133
- self._agent_service = A2AService(self.http_client, agent_url)
141
+ self._agent_service = A2AService(
142
+ self.http_client,
143
+ agent_url,
144
+ credentials=credentials,
145
+ )
134
146
  return await self._agent_service.get_card()
135
147
 
136
148
  def _update_ui_for_connected_state(self, agent_card: AgentCard) -> None:
@@ -155,7 +167,8 @@ class HandlerTUI(App[Any]):
155
167
  messages_panel.add_system_message(f"Connecting to {agent_url}...")
156
168
 
157
169
  try:
158
- agent_card = await self._connect_to_agent(agent_url)
170
+ credentials = messages_panel.get_auth_credentials()
171
+ agent_card = await self._connect_to_agent(agent_url, credentials)
159
172
 
160
173
  self.current_agent_card = agent_card
161
174
  self.current_agent_url = agent_url
@@ -214,6 +227,8 @@ class HandlerTUI(App[Any]):
214
227
  credentials = messages_panel.get_auth_credentials()
215
228
  if credentials:
216
229
  self._agent_service.set_credentials(credentials)
230
+ else:
231
+ self._agent_service.clear_credentials()
217
232
 
218
233
  send_result = await self._agent_service.send(
219
234
  message_text,
@@ -97,3 +97,13 @@ class AuthPanel(Vertical):
97
97
  elif pressed.id == "auth-bearer":
98
98
  return AuthType.BEARER
99
99
  return None
100
+
101
+ def set_bearer_token(self, token: str) -> None:
102
+ """Preconfigure bearer token authentication."""
103
+ self.query_one("#bearer-token-input", Input).value = token
104
+ self.query_one("#auth-bearer", RadioButton).value = True
105
+
106
+ # Ensure fields are visible even if no RadioSet event is emitted.
107
+ self.query_one("#api-key-fields", Vertical).add_class("hidden")
108
+ self.query_one("#bearer-fields", Vertical).remove_class("hidden")
109
+ logger.debug("Preconfigured bearer token authentication")
@@ -251,6 +251,11 @@ class TabbedMessagesPanel(Container):
251
251
  auth_panel = self._get_auth_panel()
252
252
  return auth_panel.get_credentials()
253
253
 
254
+ def set_bearer_token(self, token: str) -> None:
255
+ """Preconfigure bearer token authentication in the auth panel."""
256
+ auth_panel = self._get_auth_panel()
257
+ auth_panel.set_bearer_token(token)
258
+
254
259
  def add_task(self, task: "Task") -> None:
255
260
  """Add a task to the tasks panel."""
256
261
  tasks_panel = self._get_tasks_panel()
@@ -11,7 +11,12 @@ from typing import Any
11
11
 
12
12
  import httpx
13
13
  from a2a.client import A2ACardResolver
14
+ from a2a.client.errors import A2AClientHTTPError
14
15
  from a2a.types import AgentCard
16
+ from a2a.utils.constants import (
17
+ AGENT_CARD_WELL_KNOWN_PATH,
18
+ PREV_AGENT_CARD_WELL_KNOWN_PATH,
19
+ )
15
20
  from pydantic import ValidationError
16
21
 
17
22
  from a2a_handler.common import get_logger
@@ -101,7 +106,20 @@ async def validate_agent_card_from_url(
101
106
 
102
107
  try:
103
108
  resolver = A2ACardResolver(http_client, agent_url)
104
- agent_card = await resolver.get_agent_card()
109
+ try:
110
+ agent_card = await resolver.get_agent_card()
111
+ except (A2AClientHTTPError, httpx.HTTPStatusError):
112
+ logger.info(
113
+ "Agent card not found at %s, trying %s",
114
+ AGENT_CARD_WELL_KNOWN_PATH,
115
+ PREV_AGENT_CARD_WELL_KNOWN_PATH,
116
+ )
117
+ fallback_resolver = A2ACardResolver(
118
+ http_client,
119
+ agent_url,
120
+ agent_card_path=PREV_AGENT_CARD_WELL_KNOWN_PATH,
121
+ )
122
+ agent_card = await fallback_resolver.get_agent_card()
105
123
 
106
124
  logger.info("Agent card validation successful for %s", agent_card.name)
107
125
  return ValidationResult(
File without changes
File without changes