python-slack-agents 0.8.1__tar.gz → 0.9.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 (172) hide show
  1. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/.gitignore +2 -0
  2. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/.pre-commit-config.yaml +5 -0
  3. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/CHANGELOG.md +42 -3
  4. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/PKG-INFO +4 -3
  5. python_slack_agents-0.9.1/SECURITY.md +13 -0
  6. python_slack_agents-0.9.1/agents/a2a-agent/config.yaml +66 -0
  7. python_slack_agents-0.9.1/agents/a2a-agent/system_prompt.txt +1 -0
  8. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/agents/docs-assistant/system_prompt.txt +1 -1
  9. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/agents/hello-world/system_prompt.txt +0 -1
  10. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/agents/kitchen-sink/config.yaml +1 -1
  11. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/agents/kitchen-sink/system_prompt.txt +1 -1
  12. python_slack_agents-0.9.1/docs/a2a.md +384 -0
  13. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/oauth.md +6 -6
  14. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/private-repo.md +47 -0
  15. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/tools.md +1 -1
  16. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/llms-full.txt +730 -1
  17. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/llms.txt +3 -2
  18. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/pyproject.toml +4 -3
  19. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/__init__.py +7 -0
  20. python_slack_agents-0.9.1/src/slack_agents/a2a/agent.py +461 -0
  21. python_slack_agents-0.9.1/src/slack_agents/a2a/client.py +319 -0
  22. python_slack_agents-0.9.1/src/slack_agents/a2a/delivery.py +131 -0
  23. python_slack_agents-0.9.1/src/slack_agents/a2a/oauth.py +75 -0
  24. python_slack_agents-0.9.1/src/slack_agents/a2a/proxy.py +113 -0
  25. python_slack_agents-0.9.1/src/slack_agents/a2a/push.py +198 -0
  26. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/init.py +15 -0
  27. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/config.py +84 -16
  28. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/main.py +5 -5
  29. python_slack_agents-0.9.1/src/slack_agents/oauth/discovery.py +149 -0
  30. python_slack_agents-0.9.1/src/slack_agents/oauth/errors.py +250 -0
  31. python_slack_agents-0.9.1/src/slack_agents/oauth/flow.py +370 -0
  32. python_slack_agents-0.9.1/src/slack_agents/oauth/scopes.py +53 -0
  33. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/scripts/generate_llms_full.py +2 -0
  34. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/agent.py +153 -29
  35. python_slack_agents-0.9.1/src/slack_agents/tools/mcp_http_oauth.py +394 -0
  36. python_slack_agents-0.9.1/tests/__init__.py +0 -0
  37. python_slack_agents-0.9.1/tests/a2a/__init__.py +0 -0
  38. python_slack_agents-0.9.1/tests/a2a/test_agent_async.py +69 -0
  39. python_slack_agents-0.9.1/tests/a2a/test_agent_sync.py +148 -0
  40. python_slack_agents-0.9.1/tests/a2a/test_client.py +22 -0
  41. python_slack_agents-0.9.1/tests/a2a/test_client_auth.py +63 -0
  42. python_slack_agents-0.9.1/tests/a2a/test_client_files.py +46 -0
  43. python_slack_agents-0.9.1/tests/a2a/test_delivery.py +99 -0
  44. python_slack_agents-0.9.1/tests/a2a/test_delivery_oauth.py +95 -0
  45. python_slack_agents-0.9.1/tests/a2a/test_end_to_end.py +65 -0
  46. python_slack_agents-0.9.1/tests/a2a/test_framework_delivery.py +44 -0
  47. python_slack_agents-0.9.1/tests/a2a/test_integration.py +58 -0
  48. python_slack_agents-0.9.1/tests/a2a/test_oauth.py +372 -0
  49. python_slack_agents-0.9.1/tests/a2a/test_oauth_adapter.py +71 -0
  50. python_slack_agents-0.9.1/tests/a2a/test_proxy.py +83 -0
  51. python_slack_agents-0.9.1/tests/a2a/test_push.py +173 -0
  52. python_slack_agents-0.9.1/tests/a2a/test_push_preferred.py +58 -0
  53. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_access.py +3 -0
  54. python_slack_agents-0.9.1/tests/test_ingress_startup.py +106 -0
  55. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_mcp_http_oauth.py +25 -24
  56. python_slack_agents-0.9.1/tests/test_oauth_discovery.py +98 -0
  57. python_slack_agents-0.9.1/tests/test_oauth_errors.py +47 -0
  58. python_slack_agents-0.9.1/tests/test_oauth_flow.py +109 -0
  59. python_slack_agents-0.9.1/tests/test_oauth_flow_noninteractive.py +46 -0
  60. python_slack_agents-0.9.1/tests/test_oauth_flow_required_scopes.py +68 -0
  61. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_oauth_integration.py +2 -2
  62. python_slack_agents-0.9.1/tests/test_oauth_scopes.py +39 -0
  63. python_slack_agents-0.9.1/tests/test_oauth_validation.py +142 -0
  64. python_slack_agents-0.9.1/uv.lock +2335 -0
  65. python_slack_agents-0.8.1/SECURITY.md +0 -9
  66. python_slack_agents-0.8.1/src/slack_agents/tools/mcp_http_oauth.py +0 -1098
  67. python_slack_agents-0.8.1/tests/test_oauth_validation.py +0 -77
  68. python_slack_agents-0.8.1/uv.lock +0 -2108
  69. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/.dockerignore +0 -0
  70. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/.env.example +0 -0
  71. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/.github/workflows/ci.yml +0 -0
  72. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/.github/workflows/publish.yml +0 -0
  73. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/AGENTS.md +0 -0
  74. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/CODE_OF_CONDUCT.md +0 -0
  75. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/CONTRIBUTING.md +0 -0
  76. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/LICENSE +0 -0
  77. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/README.md +0 -0
  78. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/agents/README.md +0 -0
  79. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/agents/docs-assistant/config.yaml +0 -0
  80. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/agents/hello-world/config.yaml +0 -0
  81. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/access-control.md +0 -0
  82. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/agents.md +0 -0
  83. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/canvas.md +0 -0
  84. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/cli.md +0 -0
  85. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/deployment.md +0 -0
  86. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/llm.md +0 -0
  87. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/media/demo.gif +0 -0
  88. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/observability.md +0 -0
  89. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/setup.md +0 -0
  90. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/slack-app-manifest.json +0 -0
  91. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/storage.md +0 -0
  92. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/user-context.md +0 -0
  93. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/Dockerfile +0 -0
  94. {python_slack_agents-0.8.1/src/slack_agents/access → python_slack_agents-0.9.1/src/slack_agents/a2a}/__init__.py +0 -0
  95. {python_slack_agents-0.8.1/src/slack_agents/scripts → python_slack_agents-0.9.1/src/slack_agents/access}/__init__.py +0 -0
  96. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/access/allow_all.py +0 -0
  97. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/access/allow_list.py +0 -0
  98. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/access/base.py +0 -0
  99. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/agent_loop.py +0 -0
  100. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/__init__.py +0 -0
  101. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/build_docker.py +0 -0
  102. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/export_conversations.py +0 -0
  103. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/export_conversations_html.py +0 -0
  104. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/export_usage.py +0 -0
  105. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/export_usage_csv.py +0 -0
  106. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/healthcheck.py +0 -0
  107. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/run.py +0 -0
  108. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/conversations.py +0 -0
  109. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/files.py +0 -0
  110. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/llm/__init__.py +0 -0
  111. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/llm/anthropic.py +0 -0
  112. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/llm/base.py +0 -0
  113. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/llm/openai.py +0 -0
  114. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/oauth/__init__.py +0 -0
  115. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/oauth/crypto.py +0 -0
  116. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/oauth/prompts.py +0 -0
  117. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/oauth/server.py +0 -0
  118. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/oauth/state.py +0 -0
  119. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/oauth/storage.py +0 -0
  120. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/observability.py +0 -0
  121. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/py.typed +0 -0
  122. {python_slack_agents-0.8.1/src/slack_agents/slack → python_slack_agents-0.9.1/src/slack_agents/scripts}/__init__.py +0 -0
  123. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/scripts/download_fonts.py +0 -0
  124. {python_slack_agents-0.8.1/src/slack_agents/storage → python_slack_agents-0.9.1/src/slack_agents/slack}/__init__.py +0 -0
  125. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/actions.py +0 -0
  126. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/canvas_auth.py +0 -0
  127. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/canvases.py +0 -0
  128. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/files.py +0 -0
  129. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/format.py +0 -0
  130. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/streaming.py +0 -0
  131. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/streaming_formatter.py +0 -0
  132. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/tool_blocks.py +0 -0
  133. {python_slack_agents-0.8.1/src/slack_agents/tools → python_slack_agents-0.9.1/src/slack_agents/storage}/__init__.py +0 -0
  134. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/storage/base.py +0 -0
  135. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/storage/postgres.py +0 -0
  136. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/storage/postgres.sql +0 -0
  137. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/storage/sqlite.py +0 -0
  138. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/storage/sqlite.sql +0 -0
  139. {python_slack_agents-0.8.1/tests → python_slack_agents-0.9.1/src/slack_agents/tools}/__init__.py +0 -0
  140. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/tools/base.py +0 -0
  141. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/tools/canvas.py +0 -0
  142. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/tools/canvas_importer.py +0 -0
  143. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/tools/file_exporter.py +0 -0
  144. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/tools/file_importer.py +0 -0
  145. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/tools/mcp_http.py +0 -0
  146. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/tools/user_context.py +0 -0
  147. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_agent_loop.py +0 -0
  148. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_canvas_auth.py +0 -0
  149. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_canvas_importer.py +0 -0
  150. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_cli.py +0 -0
  151. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_config.py +0 -0
  152. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_conversations.py +0 -0
  153. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_cost.py +0 -0
  154. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_export_documents.py +0 -0
  155. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_export_usage.py +0 -0
  156. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_file_extractors.py +0 -0
  157. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_format.py +0 -0
  158. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_init.py +0 -0
  159. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_llm_error_classification.py +0 -0
  160. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_llm_factory.py +0 -0
  161. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_load_plugin.py +0 -0
  162. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_mcp_client.py +0 -0
  163. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_oauth_crypto.py +0 -0
  164. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_oauth_prompts.py +0 -0
  165. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_oauth_server.py +0 -0
  166. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_oauth_state.py +0 -0
  167. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_oauth_storage.py +0 -0
  168. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_openai_convert.py +0 -0
  169. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_overlay_integration.py +0 -0
  170. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_storage_oauth.py +0 -0
  171. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_tool_blocks.py +0 -0
  172. {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_tool_errors.py +0 -0
@@ -34,6 +34,8 @@ fonts/
34
34
  Thumbs.db
35
35
 
36
36
  agents-local/
37
+ # Local-only agent variants (e.g. agents/a2a-agent-local/)
38
+ *-local/
37
39
 
38
40
  # Certificates
39
41
  *.pem
@@ -24,3 +24,8 @@ repos:
24
24
  - id: check-yaml
25
25
  - id: check-added-large-files
26
26
  - id: check-merge-conflict
27
+
28
+ - repo: https://github.com/gitleaks/gitleaks
29
+ rev: v8.30.1
30
+ hooks:
31
+ - id: gitleaks
@@ -1,4 +1,4 @@
1
- we don# Changelog
1
+ # Changelog
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
@@ -6,6 +6,45 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.1] - 2026-06-09
10
+
11
+ ### Added
12
+
13
+ - **A2A per-user OAuth** (`auth: { type: oauth2 }`). Remote A2A agents can now require per-Slack-user OAuth 2.1: each user authenticates separately and the agent calls on their behalf. Auth metadata is discovered from the Agent Card's `securitySchemes.oauth2` (no `client_id`/`scopes` in YAML); the flow uses Dynamic Client Registration + authorization-code + PKCE + refresh, the same ephemeral "Authenticate" button, the shared `/oauth/*` ingress, and the same `PUBLIC_URL` / `OAUTH_SECRET_KEY` env contract as `mcp_http_oauth`. Long-running tasks polled in the background use the user's stored token non-interactively (silent refresh; a "session expired — please re-ask" notice if it can't be refreshed). When push is registered for a task, the token-dependent poller is skipped (push delivers regardless of later token state). See `docs/a2a.md`.
14
+ - **A2A API-key auth** — `auth: { type: apiKey, name, value }` (sends the raw value as the named header, matching the Agent Card's `apiKey` scheme), alongside the existing `bearer` / `header` / `none` forms.
15
+ - **OAuth user-info logging** (MCP and A2A). On first authentication the user's OIDC identity (`sub` / `preferred_username` / `name` / `email`) is fetched from the userinfo endpoint and logged. The `profile` scope is now requested and registered alongside `openid` / `offline_access`.
16
+
17
+ ### Changed
18
+
19
+ - The provider-agnostic OAuth flow was extracted from `tools/mcp_http_oauth.py` into the `oauth/` package (`scopes`, `errors`, `discovery`, `flow`), parameterized by a discovery strategy — RFC 9728 PRM for MCP, Agent Card for A2A. No behavior change for MCP-OAuth.
20
+ - DCR client registration now requests the OIDC baseline (`openid`, `offline_access`, `profile`) explicitly, so strict authorization-server registration policies (e.g. Keycloak's "Allowed Client Scopes") grant the client those scopes rather than rejecting the later authorize with `invalid_scope`.
21
+ - **BREAKING (A2A):** removed the `name` field from `slack_agents.a2a.agent`. The single tool is now named after the provider's key under `tools:` (slugified to satisfy LLM tool-name rules); rename the key to rename the tool. Keys are unique within `tools:`, so two agents never collide.
22
+ - **BREAKING (A2A):** removed `allowed_functions` from `slack_agents.a2a.agent` — an A2A agent exposes exactly one opaque tool, so there was nothing to filter. A stray `allowed_functions` on an A2A config is ignored at load with a warning.
23
+
24
+ ### Fixed
25
+
26
+ - The in-process OAuth ingress crashed at startup with `TypeError: argument should be a bytes-like object … not 'bool'` whenever `OAUTH_SECRET_KEY` was set — `base64.b64decode(key, True)` passed `True` as the positional `altchars` argument instead of the keyword `validate=`. This was a latent bug in the shared ingress that affected MCP-OAuth and A2A-OAuth agents alike; it had no test coverage and is now exercised by `tests/test_ingress_startup.py`.
27
+ - A2A `call_tool` now maps OAuth-specific failures to actionable results — a user-level authorization denial becomes a permission-denied error naming the missing scope, and an IdP `redirect_uri` rejection clears the stale client registration so the next attempt self-heals — instead of a generic "contact support".
28
+ - The per-user A2A httpx client is closed if Agent Card resolution fails after construction, avoiding a connection-pool leak on the background poller's out-of-band re-auth path.
29
+
30
+ ## [0.9.0] - 2026-06-07
31
+
32
+ ### Added
33
+
34
+ - **A2A push notifications (receiving).** A push-capable agent (`capabilities.pushNotifications`) can deliver task updates — status messages and file artifacts — to the Slack thread out-of-band, including reports that arrive after a synchronous reply. Opt in per agent with `push_notifications: true`. The framework registers the webhook inline on the first send with a random per-task token, validates the `X-A2A-Notification-Token` header, correlates by `taskId`, and de-duplicates by message/artifact id (the server re-pushes the immediate reply). The OAuth HTTP sidecar is generalized into a shared ingress (one listener starting for OAuth or push), configured by the new `PUBLIC_URL` / `HTTP_BIND_HOST` / `HTTP_BIND_PORT` env vars (see Changed). Verified end-to-end against a live agent. See `docs/a2a.md`.
35
+ - **Agent2Agent (A2A) protocol integration** over the official `a2a-sdk` (1.x, protobuf), isolated behind `slack_agents.a2a.client`. Two topologies: `slack_agents.a2a.agent` exposes a remote A2A agent as a single free-text tool a real LLM delegates to (Option A, smart routing), and `slack_agents.a2a.proxy` turns Slack into a dumb frontend for one agent (Option B, dev/debugging). Features: per-thread `contextId` + `taskId` threading for correct multi-turn (`input-required`) conversations; synchronous replies plus detached background polling with out-of-band delivery for long-running (`working`) tasks (real LLMs re-process the result, the proxy posts it raw); bidirectional file attachments (Slack upload → A2A `raw` part, file artifact → Slack upload); static bearer/header auth. Includes an env-gated live integration test (`A2A_TEST_URL`). See `docs/a2a.md`.
36
+ - `docs/private-repo.md` — "Protecting secrets in your overlay" section covering GitHub push protection (server-side block that survives `--no-verify`), a gitleaks pre-commit hook, and a one-time trufflehog history sweep. Aimed at overlay maintainers whose configs reference Slack tokens, LLM API keys, and OAuth client secrets via `{ENV_VAR}` placeholders.
37
+ - `slack-agents init` now prints a visible "SECURITY: protect your secrets before pushing" banner at the end of scaffolding, linking to the new docs section.
38
+ - `SECURITY.md` — pointer for overlay maintainers to the overlay security guidance.
39
+
40
+ ### Changed
41
+
42
+ - **BREAKING (OAuth):** the in-process HTTP listener is now a shared ingress for OAuth callbacks and A2A push. Its env vars were renamed — `OAUTH_PUBLIC_URL` → `PUBLIC_URL`, `OAUTH_BIND_HOST` → `HTTP_BIND_HOST`, `OAUTH_BIND_PORT` → `HTTP_BIND_PORT` — with **no aliases**. (`OAUTH_SECRET_KEY` is unchanged.) Agents using OAuth must rename these in their environment. The startup validator fails fast with a clear message naming `PUBLIC_URL` when OAuth or push is configured but the var is missing.
43
+
44
+ ### Security
45
+
46
+ - Dependency security updates clearing all open advisories. Lifted the speculative `cryptography` upper cap (`<46`) that had blocked the patch (the fix shipped in the next major) and raised the floor to `>=46.0.7`; refreshed the lockfile to current releases: `cryptography` 48, `aiohttp` 3.14.1, `pillow` 12.2.0, `lxml` 6.1.1, `urllib3` 2.7.0, `python-multipart` 0.0.32, `starlette` 1.2.1, `anthropic` 0.107.1, `pygments` 2.20.0.
47
+
9
48
  ## [0.8.1] - 2026-05-07
10
49
 
11
50
  ### Fixed
@@ -101,7 +140,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
101
140
 
102
141
  ### Added
103
142
 
104
- - `slack-agents init` now generates `.gitignore`
143
+ - `slack-agents init` now generates `.gitignore`
105
144
  - `.env.example` template includes comments explaining where to get each token and links to setup guide
106
145
  - `build-docker` lists required environment variables after build completes
107
146
  - `build-docker` errors if `req*.txt` files are found (dependencies must be in `pyproject.toml`)
@@ -109,7 +148,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
109
148
 
110
149
  ### Changed
111
150
 
112
- - `pyproject.toml` template uses `python-slack-agents<2` (no minimum pin)
151
+ - `pyproject.toml` template uses `python-slack-agents<2` (no minimum pin)
113
152
  - Setup flow uses venv-first approach: create venv, install package, then `slack-agents init`
114
153
  - Updated README, docs/setup.md, and docs/private-repo.md with new setup flow
115
154
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-slack-agents
3
- Version: 0.8.1
3
+ Version: 0.9.1
4
4
  Summary: A Python framework for deploying AI agents as Slack bots
5
5
  Project-URL: Homepage, https://github.com/CompareNetworks/python-slack-agents
6
6
  Project-URL: Repository, https://github.com/CompareNetworks/python-slack-agents
@@ -17,11 +17,12 @@ Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Topic :: Communications :: Chat
18
18
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
19
  Requires-Python: >=3.12
20
+ Requires-Dist: a2a-sdk<2,>=1.0
20
21
  Requires-Dist: aiohttp<4,>=3.9
21
22
  Requires-Dist: aiosqlite<1,>=0.20
22
23
  Requires-Dist: anthropic<1,>=0.40
23
24
  Requires-Dist: asyncpg<1,>=0.30
24
- Requires-Dist: cryptography<46,>=42
25
+ Requires-Dist: cryptography>=46.0.7
25
26
  Requires-Dist: fpdf2<3,>=2.8
26
27
  Requires-Dist: httpx<1,>=0.27
27
28
  Requires-Dist: mcp<2,>=1.0
@@ -41,7 +42,7 @@ Requires-Dist: slack-sdk[socket-mode-handlers]<4,>=3.33
41
42
  Provides-Extra: dev
42
43
  Requires-Dist: pre-commit<5,>=4.0; extra == 'dev'
43
44
  Requires-Dist: pytest-asyncio<1,>=0.24; extra == 'dev'
44
- Requires-Dist: pytest<9,>=8.0; extra == 'dev'
45
+ Requires-Dist: pytest<10,>=8.0; extra == 'dev'
45
46
  Requires-Dist: ruff<1,>=0.8; extra == 'dev'
46
47
  Description-Content-Type: text/markdown
47
48
 
@@ -0,0 +1,13 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ Please report security vulnerabilities privately using [GitHub's security advisory feature](https://github.com/CompareNetworks/python-slack-agents/security/advisories/new).
6
+
7
+ Do **not** open public issues for security concerns.
8
+
9
+ We will acknowledge reports within 72 hours and aim to release fixes promptly.
10
+
11
+ ## For overlay maintainers
12
+
13
+ If you operate an overlay repository (your own `agents/`, `src/`, and configs built on top of this framework), see [Protecting secrets in your overlay](docs/private-repo.md#protecting-secrets-in-your-overlay) for the recommended setup: GitHub push protection, a gitleaks pre-commit hook, and a one-time trufflehog history sweep.
@@ -0,0 +1,66 @@
1
+ # Agent2Agent (A2A) example — connects to a remote A2A agent.
2
+ #
3
+ # It points at the official "helloworld" reference agent (a2a-sdk 1.x, no API
4
+ # key), run locally in Docker. START THE SERVER FIRST:
5
+ #
6
+ # git clone --depth 1 https://github.com/a2aproject/a2a-samples /tmp/a2a-samples
7
+ # cd /tmp/a2a-samples/samples/python/agents/helloworld
8
+ # # upstream binds 127.0.0.1 inside the container; make it bind 0.0.0.0:
9
+ # sed -i '' "s/host='127.0.0.1'/host='0.0.0.0'/" __main__.py # GNU sed: use sed -i
10
+ # docker build -f Containerfile -t helloworld-a2a-server .
11
+ # docker run -d -p 9999:9999 helloworld-a2a-server
12
+ #
13
+ # Then run this agent: slack-agents run agents/a2a-agent
14
+
15
+ version: "1.0.0"
16
+ schema: "slack-agents/v1"
17
+
18
+ slack:
19
+ bot_token: "{SLACK_BOT_TOKEN}"
20
+ app_token: "{SLACK_APP_TOKEN}"
21
+
22
+ access:
23
+ type: slack_agents.access.allow_all
24
+
25
+ # --- Routing: enable exactly ONE of the two llm blocks below. ---
26
+
27
+ # Option A (default) — smart routing: a real LLM delegates to the A2A agent as a
28
+ # tool, decides when to call it, and synthesizes the reply.
29
+
30
+ # # Try in Slack: "ask the a2a agent to say hello to Jim" -> it sends "Hello Jim!" to the agent.
31
+
32
+ llm:
33
+ type: slack_agents.llm.anthropic
34
+ model: claude-sonnet-4-6
35
+ api_key: "{ANTHROPIC_API_KEY}"
36
+ max_tokens: 4096
37
+ max_input_tokens: 200000
38
+
39
+ # Option B — dumb frontend (dev/debugging): no local LLM. Every user message is
40
+ # forwarded straight to the single A2A agent and its reply relayed back. To use
41
+ # it, comment out the Option A block above and uncomment this one. `model` is the
42
+ # a2a tool's key under `tools:` below.
43
+
44
+ # Try in Slack: "Jim" -> it echoes your request.
45
+
46
+ #llm:
47
+ # type: slack_agents.a2a.proxy
48
+ # model: "mya2a"
49
+
50
+ storage:
51
+ type: slack_agents.storage.sqlite
52
+ path: ":memory:"
53
+
54
+ tools:
55
+ # The remote A2A agent, exposed to the LLM as one free-text tool named after this
56
+ # key ("mya2a"). Rename the key to rename the tool.
57
+ mya2a:
58
+ type: slack_agents.a2a.agent
59
+ url: "http://127.0.0.1:9999"
60
+ auth: {}
61
+
62
+ # Lets the framework ACCEPT Slack file uploads; the a2a tool then forwards the
63
+ # raw bytes to the agent. (With Option A the LLM also sees the extracted text.)
64
+ import-files:
65
+ type: slack_agents.tools.file_importer
66
+ allowed_functions: [".*"]
@@ -0,0 +1 @@
1
+ You are a helpful assistant. Be concise and friendly.
@@ -1,4 +1,4 @@
1
1
  You are a documentation assistant. You can search GitHub repositories for
2
2
  documentation using the DeepWiki tool and create PDF documents.
3
3
 
4
- When asked about a project, search for its documentation first, then summarize.
4
+ When asked about a project, search for its documentation first, then summarize.
@@ -1,3 +1,2 @@
1
1
  You are a helpful assistant. Be concise and friendly.
2
2
  Ask people if they want to know the secret but only tell them if they ask: "yellow submarine".
3
-
@@ -146,4 +146,4 @@ tools:
146
146
  # model: "langfuse.observation.model.name"
147
147
  # input_tokens: "gen_ai.usage.input_tokens"
148
148
  # output_tokens: "gen_ai.usage.output_tokens"
149
- # usage: "langfuse.observation.usage_details"
149
+ # usage: "langfuse.observation.usage_details"
@@ -1 +1 @@
1
- You are a helpful assistant. This agent is configured as an example showcasing all available config options.
1
+ You are a helpful assistant. This agent is configured as an example showcasing all available config options.
@@ -0,0 +1,384 @@
1
+ # Agent2Agent (A2A) integration
2
+
3
+ `slack_agents.a2a.agent` and `slack_agents.a2a.proxy` connect this framework
4
+ to remote agents over the
5
+ [Agent2Agent protocol](https://a2a-protocol.org). A2A is an interaction
6
+ protocol — the remote agent is an opaque, possibly-conversational message
7
+ endpoint that describes itself via an Agent Card. The framework treats it as a
8
+ single free-text channel (not a menu of typed tools the way MCP does).
9
+
10
+ Built on the official [`a2a-sdk`](https://github.com/a2aproject/a2a-python)
11
+ (1.x, protobuf-based). All SDK-specific code is isolated in
12
+ `slack_agents.a2a.client`; the rest of the package depends only on a small
13
+ stable interface, so SDK version changes are contained to one module.
14
+
15
+ Two deployment topologies are supported from one codebase:
16
+
17
+ - **Option A — smart router.** A real local LLM (`slack_agents.llm.anthropic`,
18
+ etc.) orchestrates a conversation and delegates to one or more remote A2A
19
+ agents exposed as tools (`slack_agents.a2a.agent`). The local LLM decides
20
+ when and how to invoke each agent, synthesizes the replies, and continues the
21
+ conversation normally. Multiple A2A agents can coexist alongside other tool
22
+ providers.
23
+ - **Option B — dumb frontend.** No local reasoning at all. `slack_agents.a2a.proxy`
24
+ forwards every user message to a single remote A2A agent and relays the reply
25
+ back. Slack becomes a thin chat front-end; the intelligence lives entirely in
26
+ the remote agent. This is primarily a **development/debugging convenience** for
27
+ poking at an A2A agent directly through Slack — Option A is the production shape.
28
+
29
+ ## Configuration
30
+
31
+ ### Option A — smart router
32
+
33
+ ```yaml
34
+ llm:
35
+ type: slack_agents.llm.anthropic
36
+ model: claude-sonnet-4-6
37
+ api_key: "{ANTHROPIC_API_KEY}"
38
+ max_tokens: 4096
39
+ max_input_tokens: 200000
40
+ tools:
41
+ research-agent:
42
+ type: slack_agents.a2a.agent
43
+ url: "https://research.example.com"
44
+ auth: { type: bearer, token: "{RESEARCH_A2A_TOKEN}" }
45
+ poll_interval: 5
46
+ max_task_lifetime: 3600
47
+ ```
48
+
49
+ ### Option B — dumb frontend
50
+
51
+ ```yaml
52
+ llm:
53
+ type: slack_agents.a2a.proxy
54
+ model: "mya2a"
55
+ max_input_tokens: 200000
56
+ tools:
57
+ mya2a:
58
+ type: slack_agents.a2a.agent
59
+ url: "https://remote-agent.example.com"
60
+ auth: { type: bearer, token: "{A2A_API_KEY}" }
61
+ ```
62
+
63
+ The system prompt is ignored in Option B; it is used normally in Option A.
64
+
65
+ ## `a2a.agent` — tool provider
66
+
67
+ `slack_agents.a2a.agent` is a `BaseToolProvider`. At startup it fetches the
68
+ remote agent's Agent Card and registers a single tool — named after the
69
+ provider's key under `tools:` — using the card's description.
70
+
71
+ | Field | Type | Default | Description |
72
+ |-------|------|---------|-------------|
73
+ | `url` | `str` | required | Base URL of the remote agent. The framework tries `<url>/.well-known/agent-card.json` automatically. |
74
+ | `auth` | `dict` | none | Auth credentials. See [Auth](#auth) below. |
75
+ | `timeout` | `float` | `300` | HTTP timeout in seconds for individual A2A requests. |
76
+ | `poll_interval` | `float` | `5` | Seconds between polling attempts for long-running tasks. |
77
+ | `max_task_lifetime` | `float` | `3600` | Maximum seconds to poll before abandoning a task and delivering a timeout message. |
78
+
79
+ **One tool per agent.** A2A is a single opaque channel — it has no mechanism to
80
+ advertise a menu of typed tools the way MCP does. Each `a2a.agent` provider
81
+ exposes exactly one free-text tool, named after its key under `tools:`
82
+ (slugified to satisfy the LLM tool-name rules — letters, digits, `_`, `-`).
83
+ Rename the key to rename the tool; the key is also unique within `tools:`, so
84
+ two agents never collide:
85
+
86
+ ```json
87
+ {
88
+ "name": "<tools: key, slugified>",
89
+ "description": "<from Agent Card>",
90
+ "input_schema": {
91
+ "type": "object",
92
+ "properties": { "message": { "type": "string" } },
93
+ "required": ["message"]
94
+ }
95
+ }
96
+ ```
97
+
98
+ **Conversation continuity.** The framework tracks the A2A `contextId` (and, for
99
+ multi-turn tasks, the `taskId`) per Slack thread, transparently — the LLM never
100
+ sees or passes these. Successive messages in the same thread are sent on the
101
+ same `contextId` so the remote agent can keep conversational state.
102
+
103
+ When the agent replies with an **`input-required`** (or `auth-required`) state —
104
+ i.e. it is mid-task and wants another message — the framework relays the agent's
105
+ prompt to the user and **threads the same `taskId`** on the next message, so the
106
+ exchange continues the *same* Task (and its server-side state). When the task
107
+ reaches a terminal state, the saved `taskId` is cleared and the next message
108
+ starts a fresh Task. This is what makes multi-turn agents (e.g. a step-by-step
109
+ form or a guessing game) behave correctly instead of restarting each turn.
110
+
111
+ **Files.** File attachments flow in both directions. A file a user uploads in
112
+ Slack is forwarded to the agent as an A2A `raw` part (alongside the text); a
113
+ file the agent returns as an artifact is surfaced back into the Slack thread as
114
+ an upload. Non-text artifacts (CSV, PDF, …) come through as files; text
115
+ artifacts are used as the reply.
116
+
117
+ To **send** files you must also configure a file-import handler (e.g.
118
+ `slack_agents.tools.file_importer`) in `tools:` — the framework only accepts
119
+ uploads it has a handler for, and the a2a tool then forwards the raw bytes:
120
+
121
+ ```yaml
122
+ tools:
123
+ mya2a:
124
+ type: slack_agents.a2a.agent
125
+ url: "https://remote-agent.example.com"
126
+ import-files:
127
+ type: slack_agents.tools.file_importer
128
+ allowed_functions: [".*"]
129
+ ```
130
+
131
+ ## `a2a.proxy` — passthrough LLM
132
+
133
+ `slack_agents.a2a.proxy` is a `BaseLLMProvider` that does no local reasoning.
134
+ It drives the same conversation loop as any other LLM provider, but instead of
135
+ calling an LLM it routes directly to the configured A2A tool.
136
+
137
+ | Field | Type | Default | Description |
138
+ |-------|------|---------|-------------|
139
+ | `model` | `str` | — | Name of the target `a2a.agent` tool (the key under `tools:` in `config.yaml`). When set, it is always used. When omitted, the proxy auto-selects the tool **only if exactly one** is configured; with more than one tool and no `model:`, startup fails with an error. |
140
+ | `max_input_tokens` | `int` | `200000` | Context-window guard, passed through to the loop. |
141
+
142
+ ## Auth
143
+
144
+ ### Static / API-key auth (service-level)
145
+
146
+ Credentials are resolved from environment variables at startup via the standard
147
+ `{ENV_VAR}` interpolation. These are shared across all users.
148
+
149
+ ```yaml
150
+ # Bearer token — Authorization: Bearer <token>
151
+ auth: { type: bearer, token: "{A2A_API_KEY}" }
152
+
153
+ # Arbitrary header
154
+ auth: { type: header, name: "X-API-Key", value: "{A2A_API_KEY}" }
155
+
156
+ # API-key scheme — sends the raw value as the named header
157
+ # (matches an Agent Card that advertises apiKey security)
158
+ auth: { type: apiKey, name: "Authorization", value: "{A2A_API_KEY}" }
159
+
160
+ # No auth (default when omitted)
161
+ auth: { type: none }
162
+ ```
163
+
164
+ ### Per-user OAuth (`auth: { type: oauth2 }`)
165
+
166
+ Each Slack user authenticates separately to the remote agent. The framework
167
+ uses that user's access token on subsequent calls, exactly as it does for
168
+ OAuth-protected MCP servers (see [docs/oauth.md](oauth.md)).
169
+
170
+ ```yaml
171
+ tools:
172
+ research-agent:
173
+ type: slack_agents.a2a.agent
174
+ url: "https://research.example.com"
175
+ auth: { type: oauth2 }
176
+ ```
177
+
178
+ There is intentionally no `client_id`/`scopes` field. The provider discovers
179
+ OAuth metadata from the remote agent's **Agent Card** (`securitySchemes.oauth2`
180
+ → `oauth2MetadataUrl`), performs Dynamic Client Registration, and uses the
181
+ auth-code + PKCE flow. This means a server whose RFC 9728 protected-resource-metadata
182
+ endpoint is missing or internal still works as long as its Agent Card advertises
183
+ the oauth2 scheme and `oauth2MetadataUrl`.
184
+
185
+ **User experience.** The first time a user invokes an OAuth-protected A2A agent,
186
+ the framework posts an ephemeral "Authenticate" button in Slack. Clicking it
187
+ opens the remote agent's auth server in the user's browser. After consent the
188
+ browser redirects to the shared callback URL and the original request is
189
+ completed automatically.
190
+
191
+ **Required environment variables.** Per-user OAuth needs the shared ingress
192
+ (same as MCP OAuth) — configure `PUBLIC_URL` and `OAUTH_SECRET_KEY` exactly
193
+ as described in [docs/oauth.md](oauth.md#required-environment-variables). These
194
+ are validated at startup; the agent refuses to start if they are missing or
195
+ malformed.
196
+
197
+ ## Long-running tasks
198
+
199
+ The A2A protocol distinguishes between tasks that complete quickly and tasks
200
+ that may take minutes or hours. The framework handles both transparently.
201
+
202
+ The client sends every message with `blocking: true`, requesting that the
203
+ server wait for completion before responding. This is only a hint — the server
204
+ is free to return a non-terminal (`working`) task for long jobs.
205
+
206
+ **Synchronous path.** If the remote agent returns a terminal result
207
+ (`completed`, `failed`, `canceled`, or `rejected`) immediately, the result is
208
+ returned inline to the LLM (Option A) or relayed directly to Slack (Option B).
209
+
210
+ **Background-polling path.** If the remote agent returns a non-terminal
211
+ (`submitted` or `working`) task, the framework:
212
+
213
+ 1. Persists an in-flight record (task ID, context, thread, channel) to the
214
+ storage backend.
215
+ 2. Returns an acknowledgement to the LLM/proxy immediately
216
+ ("Started a longer task — I'll post the result here when it's ready.").
217
+ 3. Spawns a background poller that calls `tasks/get` every `poll_interval`
218
+ seconds until the task reaches a terminal state or `max_task_lifetime` is
219
+ exceeded.
220
+ 4. On completion, delivers the result **out-of-band** into the original Slack
221
+ thread:
222
+ - **Option A (real LLM):** the result is injected as a synthetic inbound
223
+ turn and the standard loop re-runs, so the local LLM can interpret or act
224
+ on it before replying to the user.
225
+ - **Option B (proxy):** the result text is posted directly to the thread
226
+ without re-entering the loop (the proxy has no intelligence to add).
227
+
228
+ This is entirely outbound — the background poller calls the remote agent's
229
+ `tasks/get` endpoint on a timer. No public URL or inbound HTTP listener is
230
+ needed. The Socket Mode deploy-anywhere property is preserved.
231
+
232
+ **Crash resilience.** In-flight task records are persisted in the storage
233
+ backend. If the agent process restarts while tasks are in flight, they are
234
+ re-discovered at startup and pollers are resumed automatically.
235
+
236
+ **OAuth and async delivery.** The two async paths behave differently with
237
+ respect to the user's token, and the framework is **push-preferred**:
238
+
239
+ - **Push (webhook).** Delivery is *inbound* — the agent POSTs updates to the
240
+ ingress, which relays them to Slack without ever calling the agent back. No
241
+ token is involved at delivery time, so a task started while the user was
242
+ authenticated **still delivers even if their OAuth token later expires or is
243
+ revoked.** When a push webhook is registered for a task, the framework does
244
+ **not** also run the poller for it (avoiding redundant — potentially double —
245
+ delivery).
246
+ - **Polling (fallback, no push).** Only used when push is not registered (the
247
+ agent doesn't support it, or no `PUBLIC_URL`). The poller calls `tasks/get`
248
+ *outbound*, which requires the user's token: it builds a fresh per-user client
249
+ from the stored token and refreshes silently — no Slack interaction happens
250
+ out-of-band. If the token cannot be refreshed (revoked, or the refresh
251
+ expired), the bot posts "Your session for this task has expired — please ask
252
+ again and re-authenticate when prompted." to the thread, instead of prompting
253
+ out-of-band where there is no safe way to do so.
254
+
255
+ ## What a Slack user sees
256
+
257
+ **For a fast response (synchronous path):**
258
+
259
+ 1. User asks the bot something.
260
+ 2. The bot forwards the message to the remote A2A agent and replies with the
261
+ result, same as any other tool call.
262
+
263
+ **For a long-running task (async path):**
264
+
265
+ 1. User asks the bot something.
266
+ 2. The bot replies: "Started a longer task — I'll post the result here when
267
+ it's ready."
268
+ 3. When the remote agent finishes, the result appears in the same Slack thread.
269
+
270
+ ## Trying it against a reference agent
271
+
272
+ You don't need to write an A2A agent to try the integration — point it at the
273
+ official [`helloworld`](https://github.com/a2aproject/a2a-samples/tree/main/samples/python/agents/helloworld)
274
+ sample (a2a-sdk 1.x, no API key). It's a simple echo, so it exercises the core
275
+ path — Agent Card resolution, send, completion, artifact handling — but not
276
+ multi-turn or files.
277
+
278
+ ```bash
279
+ git clone --depth 1 https://github.com/a2aproject/a2a-samples /tmp/a2a-samples
280
+ cd /tmp/a2a-samples/samples/python/agents/helloworld
281
+
282
+ # The upstream Containerfile's CMD passes --host 0.0.0.0, but __main__.py hardcodes
283
+ # 127.0.0.1, so it binds container-loopback and isn't reachable from the host.
284
+ # Make it bind 0.0.0.0 (the card's advertised url stays 127.0.0.1:9999, which is
285
+ # correct for a client on the host):
286
+ sed -i '' "s/host='127.0.0.1'/host='0.0.0.0'/" __main__.py # GNU sed: use sed -i
287
+
288
+ docker build -f Containerfile -t helloworld-a2a-server .
289
+ docker run -d -p 9999:9999 helloworld-a2a-server
290
+ ```
291
+
292
+ Set `url: "http://127.0.0.1:9999"` in your `a2a.agent` config and message the
293
+ bot — it replies `Hello, World! I have received your request (...)`. A ready-made
294
+ example lives at [`agents/a2a-agent/config.yaml`](../agents/a2a-agent/config.yaml),
295
+ which documents both the smart-routing and proxy topologies (proxy commented out
296
+ for easy switching) and repeats these server-start steps inline.
297
+
298
+ **Integration test.** `tests/a2a/test_integration.py` drives the real client
299
+ against a live agent and is **skipped unless `A2A_TEST_URL` is set**, so it
300
+ never affects CI:
301
+
302
+ ```bash
303
+ A2A_TEST_URL=http://127.0.0.1:9999 pytest tests/a2a/test_integration.py -v
304
+ ```
305
+
306
+ The assertions are agent-agnostic — the same test works against any conformant
307
+ A2A agent.
308
+
309
+ ## Push notifications
310
+
311
+ When a remote agent supports push (`capabilities.pushNotifications`), the
312
+ framework can register a **webhook** so the agent delivers task updates —
313
+ status messages and file artifacts — to the Slack thread out-of-band, including
314
+ reports that arrive *after* a synchronous reply. This is the unified,
315
+ **push-preferred** async path; the background poller remains the fallback for
316
+ agents that don't support push.
317
+
318
+ **Enable it** with `push_notifications: true` on the `a2a.agent` config (opt-in,
319
+ because push requires a publicly reachable URL):
320
+
321
+ ```yaml
322
+ tools:
323
+ mya2a:
324
+ type: slack_agents.a2a.agent
325
+ url: "https://remote-agent.example.com"
326
+ push_notifications: true
327
+ ```
328
+
329
+ **Ingress.** Push needs the in-process HTTP listener (shared with OAuth) and a
330
+ public URL the agent can POST to. Configure:
331
+
332
+ | Env | Default | Purpose |
333
+ |-----|---------|---------|
334
+ | `PUBLIC_URL` | — (required) | Public base URL of the ingress; the webhook is `<PUBLIC_URL>/a2a/push`. |
335
+ | `HTTP_BIND_HOST` | `0.0.0.0` | Listener bind host. |
336
+ | `HTTP_BIND_PORT` | `8080` | Listener bind port. |
337
+
338
+ This is the one A2A feature that needs **inbound** HTTP — unlike the polling
339
+ path, which is outbound-only. For local testing the URL can be a loopback
340
+ (`http://127.0.0.1:8080`) reachable by an agent on the same host.
341
+
342
+ **How it works.** On the first send the framework registers the webhook inline
343
+ (`SendMessageConfiguration.task_push_notification_config`) with a random
344
+ per-task token, and persists a `taskId → thread` record. Incoming POSTs are
345
+ validated against the token, correlated by `taskId`, **de-duplicated** by
346
+ message/artifact id (the server re-pushes the immediate reply, which we already
347
+ delivered synchronously), and any genuinely-new text/files are posted/uploaded
348
+ to the thread.
349
+
350
+ **Security & limits.** We validate the shared-secret token and only act on tasks
351
+ we registered. The protocol's signing (JWS) and SSRF-allowlist are server-side
352
+ concerns we don't yet rely on. There are no delivery retries (the agent sends a
353
+ single POST), and a *server* restart drops its own registration — so a
354
+ previously-registered task simply stops pushing.
355
+
356
+ ## Current limitations
357
+
358
+ The following are not yet implemented. They are planned for future releases.
359
+
360
+ - **Files: image uploads and the async path.** *Receiving* files works for any
361
+ type. When *sending*, only non-image uploads (CSV, PDF, DOCX, …) are forwarded
362
+ — images are not yet — and files are forwarded/surfaced only on the synchronous
363
+ path (a long-running task delivered out-of-band carries its text, not files).
364
+ - **Atomic responses.** Responses are collected in full before being posted.
365
+ Token-by-token streaming to Slack is not supported yet.
366
+ - **No A2A server mode.** This framework cannot be addressed as an A2A agent
367
+ by other agents. It is a client only.
368
+
369
+ ## Troubleshooting
370
+
371
+ **"A2A agent returned a long-running task but async delivery is unavailable."**
372
+ — the `a2a.agent` provider was not given a `framework_ctx` (injected
373
+ automatically by the framework when the provider is loaded via `config.yaml`).
374
+ This should not happen in normal operation; it can appear in test setups that
375
+ construct the provider directly without the framework context.
376
+
377
+ **The background poller never delivers a result.** Check that `poll_interval`
378
+ and `max_task_lifetime` are appropriate for the remote agent. If the agent
379
+ takes longer than `max_task_lifetime`, the task is abandoned and a timeout
380
+ message is posted to the thread. Increase `max_task_lifetime` if needed.
381
+
382
+ **"a2a.proxy: set `model:` to the target a2a tool name"** — you have more than
383
+ one tool configured and the proxy doesn't know which one to route to. Set
384
+ `model:` under the `llm:` section to the key of the intended `a2a.agent` tool.
@@ -35,10 +35,10 @@ single consolidated error message.
35
35
 
36
36
  | Variable | Required | Default | Description |
37
37
  |---|---|---|---|
38
- | `OAUTH_PUBLIC_URL` | yes | — | Externally reachable base URL of this agent process. Must be `https://`, or `http://` with a loopback host (`localhost`, `127.0.0.1`, `[::1]`) for local dev. |
38
+ | `PUBLIC_URL` | yes | — | Externally reachable base URL of this agent process (shared with A2A push). Must be `https://`, or `http://` with a loopback host (`localhost`, `127.0.0.1`, `[::1]`) for local dev. |
39
39
  | `OAUTH_SECRET_KEY` | yes | — | Root key for HKDF; ≥32 bytes after base64 decode. Used to sign OAuth state tokens and encrypt refresh tokens at rest. |
40
- | `OAUTH_BIND_HOST` | no | `0.0.0.0` | Interface the in-process callback listener binds to. |
41
- | `OAUTH_BIND_PORT` | no | `8080` | TCP port for the callback listener. |
40
+ | `HTTP_BIND_HOST` | no | `0.0.0.0` | Interface the in-process listener binds to (shared ingress). |
41
+ | `HTTP_BIND_PORT` | no | `8080` | TCP port for the listener (shared ingress). |
42
42
 
43
43
  ### Generating `OAUTH_SECRET_KEY`
44
44
 
@@ -67,13 +67,13 @@ ngrok http 8080
67
67
  # → forwards https://abcd-1234.ngrok-free.app to localhost:8080
68
68
 
69
69
  # Terminal 2 — set env vars and run the agent:
70
- export OAUTH_PUBLIC_URL=https://abcd-1234.ngrok-free.app
70
+ export PUBLIC_URL=https://abcd-1234.ngrok-free.app
71
71
  export OAUTH_SECRET_KEY=$(openssl rand -base64 32)
72
72
  slack-agents run agents/my-agent
73
73
  ```
74
74
 
75
75
  If you'd rather not use a tunnel, you can run with
76
- `OAUTH_PUBLIC_URL=http://localhost:8080` — the validator allows loopback
76
+ `PUBLIC_URL=http://localhost:8080` — the validator allows loopback
77
77
  addresses over plain HTTP per RFC 8252.
78
78
 
79
79
  ## What a Slack user sees
@@ -190,7 +190,7 @@ plaintext (still in the private DB).
190
190
 
191
191
  ## Troubleshooting
192
192
 
193
- **"Configuration error: ... OAUTH_PUBLIC_URL is not set"** — set the env vars
193
+ **"Configuration error: ... PUBLIC_URL is not set"** — set the env vars
194
194
  listed in the message and restart.
195
195
 
196
196
  **"Authentication timed out"** — the user didn't click the link within