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.
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/.gitignore +2 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/.pre-commit-config.yaml +5 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/CHANGELOG.md +42 -3
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/PKG-INFO +4 -3
- python_slack_agents-0.9.1/SECURITY.md +13 -0
- python_slack_agents-0.9.1/agents/a2a-agent/config.yaml +66 -0
- python_slack_agents-0.9.1/agents/a2a-agent/system_prompt.txt +1 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/agents/docs-assistant/system_prompt.txt +1 -1
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/agents/hello-world/system_prompt.txt +0 -1
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/agents/kitchen-sink/config.yaml +1 -1
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/agents/kitchen-sink/system_prompt.txt +1 -1
- python_slack_agents-0.9.1/docs/a2a.md +384 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/oauth.md +6 -6
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/private-repo.md +47 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/tools.md +1 -1
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/llms-full.txt +730 -1
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/llms.txt +3 -2
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/pyproject.toml +4 -3
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/__init__.py +7 -0
- python_slack_agents-0.9.1/src/slack_agents/a2a/agent.py +461 -0
- python_slack_agents-0.9.1/src/slack_agents/a2a/client.py +319 -0
- python_slack_agents-0.9.1/src/slack_agents/a2a/delivery.py +131 -0
- python_slack_agents-0.9.1/src/slack_agents/a2a/oauth.py +75 -0
- python_slack_agents-0.9.1/src/slack_agents/a2a/proxy.py +113 -0
- python_slack_agents-0.9.1/src/slack_agents/a2a/push.py +198 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/init.py +15 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/config.py +84 -16
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/main.py +5 -5
- python_slack_agents-0.9.1/src/slack_agents/oauth/discovery.py +149 -0
- python_slack_agents-0.9.1/src/slack_agents/oauth/errors.py +250 -0
- python_slack_agents-0.9.1/src/slack_agents/oauth/flow.py +370 -0
- python_slack_agents-0.9.1/src/slack_agents/oauth/scopes.py +53 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/scripts/generate_llms_full.py +2 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/agent.py +153 -29
- python_slack_agents-0.9.1/src/slack_agents/tools/mcp_http_oauth.py +394 -0
- python_slack_agents-0.9.1/tests/__init__.py +0 -0
- python_slack_agents-0.9.1/tests/a2a/__init__.py +0 -0
- python_slack_agents-0.9.1/tests/a2a/test_agent_async.py +69 -0
- python_slack_agents-0.9.1/tests/a2a/test_agent_sync.py +148 -0
- python_slack_agents-0.9.1/tests/a2a/test_client.py +22 -0
- python_slack_agents-0.9.1/tests/a2a/test_client_auth.py +63 -0
- python_slack_agents-0.9.1/tests/a2a/test_client_files.py +46 -0
- python_slack_agents-0.9.1/tests/a2a/test_delivery.py +99 -0
- python_slack_agents-0.9.1/tests/a2a/test_delivery_oauth.py +95 -0
- python_slack_agents-0.9.1/tests/a2a/test_end_to_end.py +65 -0
- python_slack_agents-0.9.1/tests/a2a/test_framework_delivery.py +44 -0
- python_slack_agents-0.9.1/tests/a2a/test_integration.py +58 -0
- python_slack_agents-0.9.1/tests/a2a/test_oauth.py +372 -0
- python_slack_agents-0.9.1/tests/a2a/test_oauth_adapter.py +71 -0
- python_slack_agents-0.9.1/tests/a2a/test_proxy.py +83 -0
- python_slack_agents-0.9.1/tests/a2a/test_push.py +173 -0
- python_slack_agents-0.9.1/tests/a2a/test_push_preferred.py +58 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_access.py +3 -0
- python_slack_agents-0.9.1/tests/test_ingress_startup.py +106 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_mcp_http_oauth.py +25 -24
- python_slack_agents-0.9.1/tests/test_oauth_discovery.py +98 -0
- python_slack_agents-0.9.1/tests/test_oauth_errors.py +47 -0
- python_slack_agents-0.9.1/tests/test_oauth_flow.py +109 -0
- python_slack_agents-0.9.1/tests/test_oauth_flow_noninteractive.py +46 -0
- python_slack_agents-0.9.1/tests/test_oauth_flow_required_scopes.py +68 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_oauth_integration.py +2 -2
- python_slack_agents-0.9.1/tests/test_oauth_scopes.py +39 -0
- python_slack_agents-0.9.1/tests/test_oauth_validation.py +142 -0
- python_slack_agents-0.9.1/uv.lock +2335 -0
- python_slack_agents-0.8.1/SECURITY.md +0 -9
- python_slack_agents-0.8.1/src/slack_agents/tools/mcp_http_oauth.py +0 -1098
- python_slack_agents-0.8.1/tests/test_oauth_validation.py +0 -77
- python_slack_agents-0.8.1/uv.lock +0 -2108
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/.dockerignore +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/.env.example +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/.github/workflows/ci.yml +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/.github/workflows/publish.yml +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/AGENTS.md +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/CODE_OF_CONDUCT.md +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/CONTRIBUTING.md +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/LICENSE +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/README.md +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/agents/README.md +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/agents/docs-assistant/config.yaml +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/agents/hello-world/config.yaml +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/access-control.md +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/agents.md +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/canvas.md +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/cli.md +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/deployment.md +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/llm.md +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/media/demo.gif +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/observability.md +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/setup.md +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/slack-app-manifest.json +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/storage.md +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/docs/user-context.md +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/Dockerfile +0 -0
- {python_slack_agents-0.8.1/src/slack_agents/access → python_slack_agents-0.9.1/src/slack_agents/a2a}/__init__.py +0 -0
- {python_slack_agents-0.8.1/src/slack_agents/scripts → python_slack_agents-0.9.1/src/slack_agents/access}/__init__.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/access/allow_all.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/access/allow_list.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/access/base.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/agent_loop.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/__init__.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/build_docker.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/export_conversations.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/export_conversations_html.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/export_usage.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/export_usage_csv.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/healthcheck.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/cli/run.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/conversations.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/files.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/llm/__init__.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/llm/anthropic.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/llm/base.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/llm/openai.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/oauth/__init__.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/oauth/crypto.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/oauth/prompts.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/oauth/server.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/oauth/state.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/oauth/storage.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/observability.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/py.typed +0 -0
- {python_slack_agents-0.8.1/src/slack_agents/slack → python_slack_agents-0.9.1/src/slack_agents/scripts}/__init__.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/scripts/download_fonts.py +0 -0
- {python_slack_agents-0.8.1/src/slack_agents/storage → python_slack_agents-0.9.1/src/slack_agents/slack}/__init__.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/actions.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/canvas_auth.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/canvases.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/files.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/format.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/streaming.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/streaming_formatter.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/slack/tool_blocks.py +0 -0
- {python_slack_agents-0.8.1/src/slack_agents/tools → python_slack_agents-0.9.1/src/slack_agents/storage}/__init__.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/storage/base.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/storage/postgres.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/storage/postgres.sql +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/storage/sqlite.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/storage/sqlite.sql +0 -0
- {python_slack_agents-0.8.1/tests → python_slack_agents-0.9.1/src/slack_agents/tools}/__init__.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/tools/base.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/tools/canvas.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/tools/canvas_importer.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/tools/file_exporter.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/tools/file_importer.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/tools/mcp_http.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/src/slack_agents/tools/user_context.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_agent_loop.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_canvas_auth.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_canvas_importer.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_cli.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_config.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_conversations.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_cost.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_export_documents.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_export_usage.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_file_extractors.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_format.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_init.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_llm_error_classification.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_llm_factory.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_load_plugin.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_mcp_client.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_oauth_crypto.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_oauth_prompts.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_oauth_server.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_oauth_state.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_oauth_storage.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_openai_convert.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_overlay_integration.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_storage_oauth.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_tool_blocks.py +0 -0
- {python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/tests/test_tool_errors.py +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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.
|
|
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
|
|
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<
|
|
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.
|
{python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/agents/docs-assistant/system_prompt.txt
RENAMED
|
@@ -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.
|
{python_slack_agents-0.8.1 → python_slack_agents-0.9.1}/agents/kitchen-sink/system_prompt.txt
RENAMED
|
@@ -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
|
-
| `
|
|
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
|
-
| `
|
|
41
|
-
| `
|
|
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
|
|
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
|
-
`
|
|
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: ...
|
|
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
|