python-slack-agents 0.6.1__tar.gz → 0.6.2__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.6.1 → python_slack_agents-0.6.2}/AGENTS.md +3 -1
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/CHANGELOG.md +20 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/PKG-INFO +6 -6
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/README.md +5 -5
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/agents/kitchen-sink/config.yaml +9 -1
- python_slack_agents-0.6.2/docs/canvas.md +118 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/llms-full.txt +50 -15
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/pyproject.toml +1 -1
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/__init__.py +2 -1
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/files.py +3 -0
- python_slack_agents-0.6.2/src/slack_agents/slack/canvas_auth.py +58 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/canvases.py +4 -47
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/files.py +6 -1
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/tools/canvas.py +109 -88
- python_slack_agents-0.6.2/src/slack_agents/tools/canvas_importer.py +88 -0
- python_slack_agents-0.6.2/tests/test_canvas_auth.py +217 -0
- python_slack_agents-0.6.2/tests/test_canvas_importer.py +108 -0
- python_slack_agents-0.6.1/docs/canvas.md +0 -83
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/.dockerignore +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/.env.example +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/.github/workflows/ci.yml +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/.github/workflows/publish.yml +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/.gitignore +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/.pre-commit-config.yaml +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/CODE_OF_CONDUCT.md +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/CONTRIBUTING.md +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/LICENSE +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/SECURITY.md +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/agents/README.md +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/agents/docs-assistant/config.yaml +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/agents/docs-assistant/system_prompt.txt +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/agents/hello-world/config.yaml +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/agents/hello-world/system_prompt.txt +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/agents/kitchen-sink/system_prompt.txt +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/access-control.md +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/agents.md +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/cli.md +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/deployment.md +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/llm.md +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/media/demo.gif +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/observability.md +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/private-repo.md +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/setup.md +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/slack-app-manifest.json +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/storage.md +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/tools.md +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/user-context.md +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/llms.txt +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/Dockerfile +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/access/__init__.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/access/allow_all.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/access/allow_list.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/access/base.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/agent_loop.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/__init__.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/build_docker.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/export_conversations.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/export_conversations_html.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/export_usage.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/export_usage_csv.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/healthcheck.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/init.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/run.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/config.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/conversations.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/llm/__init__.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/llm/anthropic.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/llm/base.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/llm/openai.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/main.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/observability.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/py.typed +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/scripts/__init__.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/scripts/download_fonts.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/scripts/generate_llms_full.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/__init__.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/actions.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/agent.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/format.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/streaming.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/streaming_formatter.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/tool_blocks.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/storage/__init__.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/storage/base.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/storage/postgres.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/storage/postgres.sql +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/storage/sqlite.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/storage/sqlite.sql +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/tools/__init__.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/tools/base.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/tools/file_exporter.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/tools/file_importer.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/tools/mcp_http.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/tools/user_context.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/__init__.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_access.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_agent_loop.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_cli.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_config.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_conversations.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_cost.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_export_documents.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_export_usage.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_file_extractors.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_format.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_llm_factory.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_mcp_client.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_openai_convert.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_tool_blocks.py +0 -0
- {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/uv.lock +0 -0
|
@@ -113,8 +113,10 @@ The PyPI deployment requires manual approval in the GitHub Actions UI. Do NOT pu
|
|
|
113
113
|
|
|
114
114
|
## Style
|
|
115
115
|
|
|
116
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for full coding and commit conventions.
|
|
117
|
+
|
|
116
118
|
- Python 3.12+, line length 100
|
|
117
119
|
- Ruff rules: E, F, I (errors, pyflakes, isort)
|
|
118
120
|
- Keep it simple. Minimal abstractions, no unnecessary indirection.
|
|
119
121
|
- Commit messages: Conventional Commits — `feat:`, `fix:`, `docs:`, `chore:`, `test:`, `refactor:`. Lowercase, imperative, under 72 chars.
|
|
120
|
-
- **Always propose the commit message and wait for
|
|
122
|
+
- **NEVER run `git commit` or `git push` without explicit user approval.** Always propose the commit message and file list, then STOP and wait for the user to say "go", "commit", "yes", or similar. This is non-negotiable — even if the user says "prepare a release" or "let's commit", you must present the plan and wait. "Prepare" ≠ "execute".
|
|
@@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.6.2] - 2026-03-19
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Canvas user-level authorization — tools enforce requesting user's access level via `files.info` metadata
|
|
14
|
+
- Canvas file importer (`application/vnd.slack-docs`) — users can attach canvases to messages
|
|
15
|
+
- `file_id` field on `InputFile` — file import pipeline now passes Slack file IDs to handlers
|
|
16
|
+
- `org_access` parameter on `canvas_access_add` for workspace-wide access
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Canvas tool descriptions instruct the LLM to guide users to attach canvases via Slack's + button (never ask for IDs)
|
|
21
|
+
- Canvas tool errors now return structured JSON instead of plain text
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
|
|
25
|
+
- `canvas_list` tool (scaling concern with batch `files.info`; users discover canvases via Slack UI)
|
|
26
|
+
- `channel_id` parameter from `canvas_create` (standalone canvases only)
|
|
27
|
+
- `channel_ids` parameter from `canvas_access_add` and `canvas_access_remove`
|
|
28
|
+
|
|
9
29
|
## [0.6.1] - 2026-03-19
|
|
10
30
|
|
|
11
31
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-slack-agents
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
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
|
|
@@ -329,13 +329,13 @@ If you're an AI agent or coding assistant, see [`llms-full.txt`](https://raw.git
|
|
|
329
329
|
|
|
330
330
|
## Related Projects
|
|
331
331
|
|
|
332
|
-
|
|
332
|
+
Other projects in this space:
|
|
333
333
|
|
|
334
334
|
- **[Bolt for Python](https://github.com/slackapi/bolt-python)** — The official Slack SDK. python-slack-agents uses it internally. Use Bolt directly if you want full control over Slack interactions without an agent abstraction.
|
|
335
|
-
- **[
|
|
336
|
-
- **[bolt-python-
|
|
337
|
-
- **[
|
|
338
|
-
- **[slack-mcp-client](https://github.com/tuannvm/slack-mcp-client)** — A Go application bridging Slack and MCP servers. Deployed app rather than a library
|
|
335
|
+
- **[bolt-python-ai-chatbot](https://github.com/slack-samples/bolt-python-ai-chatbot)** — Official Slack sample app for AI chatbots. A starting point if you want to build from scratch rather than use a framework.
|
|
336
|
+
- **[bolt-python-assistant-template](https://github.com/slack-samples/bolt-python-assistant-template)** — Official Slack template for building Agents & Assistants with Bolt and OpenAI.
|
|
337
|
+
- **[langgraph-messaging-integrations](https://github.com/langchain-ai/langgraph-messaging-integrations)** — Connects LangGraph agents to Slack and other messaging platforms.
|
|
338
|
+
- **[slack-mcp-client](https://github.com/tuannvm/slack-mcp-client)** — A Go application bridging Slack and MCP servers. Deployed app rather than a library.
|
|
339
339
|
|
|
340
340
|
## Disclaimer
|
|
341
341
|
|
|
@@ -283,13 +283,13 @@ If you're an AI agent or coding assistant, see [`llms-full.txt`](https://raw.git
|
|
|
283
283
|
|
|
284
284
|
## Related Projects
|
|
285
285
|
|
|
286
|
-
|
|
286
|
+
Other projects in this space:
|
|
287
287
|
|
|
288
288
|
- **[Bolt for Python](https://github.com/slackapi/bolt-python)** — The official Slack SDK. python-slack-agents uses it internally. Use Bolt directly if you want full control over Slack interactions without an agent abstraction.
|
|
289
|
-
- **[
|
|
290
|
-
- **[bolt-python-
|
|
291
|
-
- **[
|
|
292
|
-
- **[slack-mcp-client](https://github.com/tuannvm/slack-mcp-client)** — A Go application bridging Slack and MCP servers. Deployed app rather than a library
|
|
289
|
+
- **[bolt-python-ai-chatbot](https://github.com/slack-samples/bolt-python-ai-chatbot)** — Official Slack sample app for AI chatbots. A starting point if you want to build from scratch rather than use a framework.
|
|
290
|
+
- **[bolt-python-assistant-template](https://github.com/slack-samples/bolt-python-assistant-template)** — Official Slack template for building Agents & Assistants with Bolt and OpenAI.
|
|
291
|
+
- **[langgraph-messaging-integrations](https://github.com/langchain-ai/langgraph-messaging-integrations)** — Connects LangGraph agents to Slack and other messaging platforms.
|
|
292
|
+
- **[slack-mcp-client](https://github.com/tuannvm/slack-mcp-client)** — A Go application bridging Slack and MCP servers. Deployed app rather than a library.
|
|
293
293
|
|
|
294
294
|
## Disclaimer
|
|
295
295
|
|
|
@@ -95,13 +95,21 @@ tools:
|
|
|
95
95
|
# - "export_csv"
|
|
96
96
|
# - "export_pptx"
|
|
97
97
|
|
|
98
|
-
# Slack canvases (create, read,
|
|
98
|
+
# Slack canvases (create, read, update, delete + access management)
|
|
99
99
|
# Requires Slack scopes: canvases:read, canvases:write, files:read
|
|
100
|
+
# Enforces user-level authorization — agent acts as delegate for the requesting user
|
|
100
101
|
# canvas:
|
|
101
102
|
# type: slack_agents.tools.canvas
|
|
102
103
|
# bot_token: "{SLACK_BOT_TOKEN}"
|
|
103
104
|
# allowed_functions: [".*"]
|
|
104
105
|
|
|
106
|
+
# Canvas file importer — reads canvases attached to messages
|
|
107
|
+
# Requires Slack scopes: canvases:read, files:read
|
|
108
|
+
# canvas-importer:
|
|
109
|
+
# type: slack_agents.tools.canvas_importer
|
|
110
|
+
# bot_token: "{SLACK_BOT_TOKEN}"
|
|
111
|
+
# allowed_functions: [".*"]
|
|
112
|
+
|
|
105
113
|
# Per-user memory backed by Slack canvases
|
|
106
114
|
# Remembers user preferences and context across conversations.
|
|
107
115
|
# Requires Slack scopes: canvases:read, canvases:write, files:read
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Canvas Tool
|
|
2
|
+
|
|
3
|
+
The canvas tool lets your agent create, read, update, and delete [Slack canvases](https://slack.com/features/canvas) — rich documents that live inside Slack. It exposes a simple file-like API: no section IDs or low-level operations needed.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
### 1. Add Slack scopes
|
|
8
|
+
|
|
9
|
+
In your Slack app settings (**OAuth & Permissions → Scopes → Bot Token Scopes**), add:
|
|
10
|
+
|
|
11
|
+
| Scope | Purpose |
|
|
12
|
+
|-------|---------|
|
|
13
|
+
| `canvases:read` | Read canvas content |
|
|
14
|
+
| `canvases:write` | Create, update, delete canvases and manage access |
|
|
15
|
+
| `files:read` | Read canvas content and check user access (uses `files.info` API) |
|
|
16
|
+
|
|
17
|
+
After adding scopes, reinstall the app to your workspace.
|
|
18
|
+
|
|
19
|
+
### 2. Configure the tool
|
|
20
|
+
|
|
21
|
+
Add the canvas tool to your agent's `config.yaml`:
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
tools:
|
|
25
|
+
canvas:
|
|
26
|
+
type: slack_agents.tools.canvas
|
|
27
|
+
bot_token: "{SLACK_BOT_TOKEN}"
|
|
28
|
+
allowed_functions: [".*"] # all canvas tools
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
To expose only specific tools:
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
allowed_functions:
|
|
35
|
+
- "canvas_create"
|
|
36
|
+
- "canvas_get"
|
|
37
|
+
- "canvas_update"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 3. Canvas file importer (optional)
|
|
41
|
+
|
|
42
|
+
To let users attach canvases to messages and have the agent read them automatically, add the canvas importer:
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
tools:
|
|
46
|
+
canvas-importer:
|
|
47
|
+
type: slack_agents.tools.canvas_importer
|
|
48
|
+
bot_token: "{SLACK_BOT_TOKEN}"
|
|
49
|
+
allowed_functions: [".*"]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
When a user attaches a canvas (mimetype `application/vnd.slack-docs`) to a message, the importer reads its markdown content via the Slack API and includes it in the conversation context. Authorization is enforced — the agent only reads canvases the requesting user can access.
|
|
53
|
+
|
|
54
|
+
## Authorization model
|
|
55
|
+
|
|
56
|
+
All canvas operations enforce **user-level permissions**. The agent acts as a delegate for the requesting user — it will not access canvases the user can't access themselves.
|
|
57
|
+
|
|
58
|
+
Access is resolved from `files.info` metadata (no extra storage or scopes needed):
|
|
59
|
+
|
|
60
|
+
| Check | Source field |
|
|
61
|
+
|-------|-------------|
|
|
62
|
+
| Is user the creator? | `user` / `canvas_creator_id` |
|
|
63
|
+
| Per-user access | `dm_mpdm_users_with_file_access` |
|
|
64
|
+
| Workspace-wide access | `org_or_workspace_access` |
|
|
65
|
+
|
|
66
|
+
**Access levels** (higher includes lower): `owner` > `write` > `read`
|
|
67
|
+
|
|
68
|
+
**Required access per tool:**
|
|
69
|
+
|
|
70
|
+
| Tool | Required |
|
|
71
|
+
|------|----------|
|
|
72
|
+
| `canvas_create` | — (no existing canvas) |
|
|
73
|
+
| `canvas_get` | read |
|
|
74
|
+
| `canvas_update` | write |
|
|
75
|
+
| `canvas_delete` | owner |
|
|
76
|
+
| `canvas_access_get` | read |
|
|
77
|
+
| `canvas_access_add` | owner |
|
|
78
|
+
| `canvas_access_remove` | owner |
|
|
79
|
+
|
|
80
|
+
If the user lacks sufficient access, the tool returns an error message explaining what access level is needed.
|
|
81
|
+
|
|
82
|
+
## Canvas content format
|
|
83
|
+
|
|
84
|
+
Canvas content is **markdown**. Supported elements:
|
|
85
|
+
|
|
86
|
+
- Headings (`#`, `##`, `###`)
|
|
87
|
+
- Bullet and numbered lists
|
|
88
|
+
- Tables
|
|
89
|
+
- Code blocks
|
|
90
|
+
- Block quotes
|
|
91
|
+
- Links
|
|
92
|
+
- Mentions (`<@U1234567890>`)
|
|
93
|
+
- Unfurls / embeds (``)
|
|
94
|
+
|
|
95
|
+
Block Kit is **not** supported in canvases.
|
|
96
|
+
|
|
97
|
+
## Available tools
|
|
98
|
+
|
|
99
|
+
| Tool | Description |
|
|
100
|
+
|------|-------------|
|
|
101
|
+
| `canvas_create` | Create a standalone canvas with title + content. |
|
|
102
|
+
| `canvas_get` | Get a canvas by ID. Returns title, full markdown content, and permalink. |
|
|
103
|
+
| `canvas_update` | Update a canvas — replace content, rename title, or both. |
|
|
104
|
+
| `canvas_delete` | Permanently delete a canvas. |
|
|
105
|
+
| `canvas_access_get` | Get sharing/access info for a canvas. |
|
|
106
|
+
| `canvas_access_add` | Grant read/write/owner access to users. Optionally set `org_access` for workspace-wide access. |
|
|
107
|
+
| `canvas_access_remove` | Remove access for users. |
|
|
108
|
+
|
|
109
|
+
## Example usage
|
|
110
|
+
|
|
111
|
+
**Create a canvas:**
|
|
112
|
+
> "Create a canvas titled 'Q1 Roadmap' with our milestone list"
|
|
113
|
+
|
|
114
|
+
**Read and update a canvas:**
|
|
115
|
+
> "Get the canvas F12345 and update it with the latest status"
|
|
116
|
+
|
|
117
|
+
**Share a canvas with specific users:**
|
|
118
|
+
> "Give users U123 and U456 write access to canvas F12345"
|
|
@@ -602,7 +602,7 @@ Any extra keys beyond `type` are passed as keyword arguments to the `Provider` c
|
|
|
602
602
|
|
|
603
603
|
# Canvas Tool
|
|
604
604
|
|
|
605
|
-
The canvas tool lets your agent create, read, update,
|
|
605
|
+
The canvas tool lets your agent create, read, update, and delete [Slack canvases](https://slack.com/features/canvas) — rich documents that live inside Slack. It exposes a simple file-like API: no section IDs or low-level operations needed.
|
|
606
606
|
|
|
607
607
|
## Setup
|
|
608
608
|
|
|
@@ -612,9 +612,9 @@ In your Slack app settings (**OAuth & Permissions → Scopes → Bot Token Scope
|
|
|
612
612
|
|
|
613
613
|
| Scope | Purpose |
|
|
614
614
|
|-------|---------|
|
|
615
|
-
| `canvases:read` | Read canvas content
|
|
615
|
+
| `canvases:read` | Read canvas content |
|
|
616
616
|
| `canvases:write` | Create, update, delete canvases and manage access |
|
|
617
|
-
| `files:read` |
|
|
617
|
+
| `files:read` | Read canvas content and check user access (uses `files.info` API) |
|
|
618
618
|
|
|
619
619
|
After adding scopes, reinstall the app to your workspace.
|
|
620
620
|
|
|
@@ -637,13 +637,49 @@ To expose only specific tools:
|
|
|
637
637
|
- "canvas_create"
|
|
638
638
|
- "canvas_get"
|
|
639
639
|
- "canvas_update"
|
|
640
|
-
- "canvas_list"
|
|
641
640
|
```
|
|
642
641
|
|
|
643
|
-
|
|
642
|
+
### 3. Canvas file importer (optional)
|
|
644
643
|
|
|
645
|
-
|
|
646
|
-
|
|
644
|
+
To let users attach canvases to messages and have the agent read them automatically, add the canvas importer:
|
|
645
|
+
|
|
646
|
+
```yaml
|
|
647
|
+
tools:
|
|
648
|
+
canvas-importer:
|
|
649
|
+
type: slack_agents.tools.canvas_importer
|
|
650
|
+
bot_token: "{SLACK_BOT_TOKEN}"
|
|
651
|
+
allowed_functions: [".*"]
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
When a user attaches a canvas (mimetype `application/vnd.slack-docs`) to a message, the importer reads its markdown content via the Slack API and includes it in the conversation context. Authorization is enforced — the agent only reads canvases the requesting user can access.
|
|
655
|
+
|
|
656
|
+
## Authorization model
|
|
657
|
+
|
|
658
|
+
All canvas operations enforce **user-level permissions**. The agent acts as a delegate for the requesting user — it will not access canvases the user can't access themselves.
|
|
659
|
+
|
|
660
|
+
Access is resolved from `files.info` metadata (no extra storage or scopes needed):
|
|
661
|
+
|
|
662
|
+
| Check | Source field |
|
|
663
|
+
|-------|-------------|
|
|
664
|
+
| Is user the creator? | `user` / `canvas_creator_id` |
|
|
665
|
+
| Per-user access | `dm_mpdm_users_with_file_access` |
|
|
666
|
+
| Workspace-wide access | `org_or_workspace_access` |
|
|
667
|
+
|
|
668
|
+
**Access levels** (higher includes lower): `owner` > `write` > `read`
|
|
669
|
+
|
|
670
|
+
**Required access per tool:**
|
|
671
|
+
|
|
672
|
+
| Tool | Required |
|
|
673
|
+
|------|----------|
|
|
674
|
+
| `canvas_create` | — (no existing canvas) |
|
|
675
|
+
| `canvas_get` | read |
|
|
676
|
+
| `canvas_update` | write |
|
|
677
|
+
| `canvas_delete` | owner |
|
|
678
|
+
| `canvas_access_get` | read |
|
|
679
|
+
| `canvas_access_add` | owner |
|
|
680
|
+
| `canvas_access_remove` | owner |
|
|
681
|
+
|
|
682
|
+
If the user lacks sufficient access, the tool returns an error message explaining what access level is needed.
|
|
647
683
|
|
|
648
684
|
## Canvas content format
|
|
649
685
|
|
|
@@ -664,25 +700,24 @@ Block Kit is **not** supported in canvases.
|
|
|
664
700
|
|
|
665
701
|
| Tool | Description |
|
|
666
702
|
|------|-------------|
|
|
667
|
-
| `canvas_create` | Create a canvas with title + content.
|
|
703
|
+
| `canvas_create` | Create a standalone canvas with title + content. |
|
|
668
704
|
| `canvas_get` | Get a canvas by ID. Returns title, full markdown content, and permalink. |
|
|
669
705
|
| `canvas_update` | Update a canvas — replace content, rename title, or both. |
|
|
670
706
|
| `canvas_delete` | Permanently delete a canvas. |
|
|
671
|
-
| `canvas_list` | List canvases visible to the bot. Optional `channel_id` filter. |
|
|
672
707
|
| `canvas_access_get` | Get sharing/access info for a canvas. |
|
|
673
|
-
| `canvas_access_add` | Grant read/write/owner access to users
|
|
674
|
-
| `canvas_access_remove` | Remove access for users
|
|
708
|
+
| `canvas_access_add` | Grant read/write/owner access to users. Optionally set `org_access` for workspace-wide access. |
|
|
709
|
+
| `canvas_access_remove` | Remove access for users. |
|
|
675
710
|
|
|
676
711
|
## Example usage
|
|
677
712
|
|
|
678
|
-
**Create a canvas
|
|
679
|
-
> "Create a canvas
|
|
713
|
+
**Create a canvas:**
|
|
714
|
+
> "Create a canvas titled 'Q1 Roadmap' with our milestone list"
|
|
680
715
|
|
|
681
716
|
**Read and update a canvas:**
|
|
682
717
|
> "Get the canvas F12345 and update it with the latest status"
|
|
683
718
|
|
|
684
|
-
**Share a canvas with
|
|
685
|
-
> "Give
|
|
719
|
+
**Share a canvas with specific users:**
|
|
720
|
+
> "Give users U123 and U456 write access to canvas F12345"
|
|
686
721
|
|
|
687
722
|
---
|
|
688
723
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-slack-agents"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.2"
|
|
8
8
|
description = "A Python framework for deploying AI agents as Slack bots"
|
|
9
9
|
authors = [{name = "Eric Pichon", email = "epichon@comparenetworks.com"}]
|
|
10
10
|
readme = "README.md"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""slack-agents: A Python framework for deploying AI agents as Slack bots."""
|
|
2
2
|
|
|
3
3
|
from importlib.metadata import version
|
|
4
|
-
from typing import TypedDict
|
|
4
|
+
from typing import NotRequired, TypedDict
|
|
5
5
|
|
|
6
6
|
__version__ = version("python-slack-agents")
|
|
7
7
|
|
|
@@ -23,3 +23,4 @@ class InputFile(TypedDict):
|
|
|
23
23
|
file_bytes: bytes
|
|
24
24
|
mimetype: str
|
|
25
25
|
filename: str
|
|
26
|
+
file_id: NotRequired[str]
|
|
@@ -34,6 +34,7 @@ class FileHandlerRegistry:
|
|
|
34
34
|
filename: str,
|
|
35
35
|
user_conversation_context: UserConversationContext,
|
|
36
36
|
storage: BaseStorageProvider,
|
|
37
|
+
file_id: str | None = None,
|
|
37
38
|
) -> ContentBlock | None:
|
|
38
39
|
entry = self._mime_map.get(mimetype)
|
|
39
40
|
if not entry:
|
|
@@ -50,6 +51,8 @@ class FileHandlerRegistry:
|
|
|
50
51
|
),
|
|
51
52
|
}
|
|
52
53
|
input_file = InputFile(file_bytes=file_bytes, mimetype=mimetype, filename=filename)
|
|
54
|
+
if file_id is not None:
|
|
55
|
+
input_file["file_id"] = file_id
|
|
53
56
|
try:
|
|
54
57
|
return await provider.call_tool(
|
|
55
58
|
handler_name, input_file, user_conversation_context, storage
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Canvas user-level authorization.
|
|
2
|
+
|
|
3
|
+
Resolves a user's access level for a canvas using ``files.info`` metadata
|
|
4
|
+
from the Slack API — no additional storage or scopes required.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from slack_sdk.web.async_client import AsyncWebClient
|
|
8
|
+
|
|
9
|
+
from slack_agents.slack.canvases import CanvasError, get_canvas_info
|
|
10
|
+
|
|
11
|
+
# Level hierarchy (higher index = more permissive)
|
|
12
|
+
_LEVEL_RANK = {"read": 1, "write": 2, "owner": 3}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CanvasAccessDenied(CanvasError):
|
|
16
|
+
"""Raised when a user lacks sufficient access to a canvas."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def resolve_user_access(file_info: dict, user_id: str) -> str | None:
|
|
20
|
+
"""Return the user's access level for a canvas, or ``None`` if denied.
|
|
21
|
+
|
|
22
|
+
Returns one of ``"owner"``, ``"write"``, ``"read"``, or ``None``.
|
|
23
|
+
"""
|
|
24
|
+
# Creator is the owner
|
|
25
|
+
creator = file_info.get("user") or file_info.get("canvas_creator_id")
|
|
26
|
+
if creator and creator == user_id:
|
|
27
|
+
return "owner"
|
|
28
|
+
|
|
29
|
+
# Explicit per-user access list
|
|
30
|
+
for entry in file_info.get("dm_mpdm_users_with_file_access", []):
|
|
31
|
+
if entry.get("user_id") == user_id:
|
|
32
|
+
return entry.get("access")
|
|
33
|
+
|
|
34
|
+
# Org/workspace-wide access
|
|
35
|
+
org_access = file_info.get("org_or_workspace_access", "none")
|
|
36
|
+
if org_access != "none":
|
|
37
|
+
return org_access
|
|
38
|
+
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def check_canvas_access(
|
|
43
|
+
client: AsyncWebClient,
|
|
44
|
+
*,
|
|
45
|
+
canvas_id: str,
|
|
46
|
+
user_id: str,
|
|
47
|
+
required_level: str,
|
|
48
|
+
) -> dict:
|
|
49
|
+
"""Verify that *user_id* has at least *required_level* access to *canvas_id*.
|
|
50
|
+
|
|
51
|
+
Returns the ``file_info`` dict on success.
|
|
52
|
+
Raises :class:`CanvasAccessDenied` if the user lacks sufficient access.
|
|
53
|
+
"""
|
|
54
|
+
file_info = await get_canvas_info(client, canvas_id=canvas_id)
|
|
55
|
+
access = resolve_user_access(file_info, user_id)
|
|
56
|
+
if access is None or _LEVEL_RANK.get(access, 0) < _LEVEL_RANK.get(required_level, 0):
|
|
57
|
+
raise CanvasAccessDenied(f"You don't have {required_level} access to canvas {canvas_id}")
|
|
58
|
+
return file_info
|
|
@@ -30,27 +30,9 @@ async def create_canvas(
|
|
|
30
30
|
*,
|
|
31
31
|
title: str | None = None,
|
|
32
32
|
markdown: str | None = None,
|
|
33
|
-
channel_id: str | None = None,
|
|
34
33
|
) -> dict:
|
|
35
|
-
"""Create a new canvas
|
|
36
|
-
|
|
37
|
-
If *channel_id* is provided the canvas is created inside that
|
|
38
|
-
conversation (required on free Slack plans). Otherwise a
|
|
39
|
-
standalone canvas is created via ``canvases.create``.
|
|
40
|
-
"""
|
|
41
|
-
if channel_id:
|
|
42
|
-
kwargs: dict = {}
|
|
43
|
-
if title:
|
|
44
|
-
kwargs["title"] = title
|
|
45
|
-
if markdown is not None:
|
|
46
|
-
kwargs["document_content"] = {"type": "markdown", "markdown": markdown}
|
|
47
|
-
resp = await client.api_call(
|
|
48
|
-
"conversations.canvases.create",
|
|
49
|
-
json={"channel_id": channel_id, **kwargs},
|
|
50
|
-
)
|
|
51
|
-
return _check(resp, "create")
|
|
52
|
-
|
|
53
|
-
kwargs = {}
|
|
34
|
+
"""Create a new standalone canvas via ``canvases.create``."""
|
|
35
|
+
kwargs: dict = {}
|
|
54
36
|
if title:
|
|
55
37
|
kwargs["title"] = title
|
|
56
38
|
if markdown is not None:
|
|
@@ -168,42 +150,20 @@ async def rename_canvas(
|
|
|
168
150
|
return _check(resp, "rename")
|
|
169
151
|
|
|
170
152
|
|
|
171
|
-
async def list_canvases(
|
|
172
|
-
client: AsyncWebClient,
|
|
173
|
-
*,
|
|
174
|
-
channel: str | None = None,
|
|
175
|
-
count: int = 20,
|
|
176
|
-
) -> list[dict]:
|
|
177
|
-
"""List canvases visible to the bot.
|
|
178
|
-
|
|
179
|
-
Uses ``files.list`` with ``types=canvas``. Optionally filter by
|
|
180
|
-
*channel*.
|
|
181
|
-
"""
|
|
182
|
-
kwargs: dict = {"types": "canvas", "count": count}
|
|
183
|
-
if channel:
|
|
184
|
-
kwargs["channel"] = channel
|
|
185
|
-
resp = await client.api_call("files.list", params=kwargs)
|
|
186
|
-
_check(resp, "list")
|
|
187
|
-
return resp.get("files", [])
|
|
188
|
-
|
|
189
|
-
|
|
190
153
|
async def set_canvas_access(
|
|
191
154
|
client: AsyncWebClient,
|
|
192
155
|
*,
|
|
193
156
|
canvas_id: str,
|
|
194
157
|
access_level: str,
|
|
195
158
|
user_ids: list[str] | None = None,
|
|
196
|
-
channel_ids: list[str] | None = None,
|
|
197
159
|
) -> dict:
|
|
198
|
-
"""Grant access to a canvas for users
|
|
160
|
+
"""Grant access to a canvas for users.
|
|
199
161
|
|
|
200
162
|
*access_level* is one of ``read``, ``write``, or ``owner``.
|
|
201
163
|
"""
|
|
202
164
|
payload: dict = {"canvas_id": canvas_id, "access_level": access_level}
|
|
203
165
|
if user_ids:
|
|
204
166
|
payload["user_ids"] = user_ids
|
|
205
|
-
if channel_ids:
|
|
206
|
-
payload["channel_ids"] = channel_ids
|
|
207
167
|
resp = await client.api_call("canvases.access.set", json=payload)
|
|
208
168
|
return _check(resp, "access.set")
|
|
209
169
|
|
|
@@ -213,13 +173,10 @@ async def delete_canvas_access(
|
|
|
213
173
|
*,
|
|
214
174
|
canvas_id: str,
|
|
215
175
|
user_ids: list[str] | None = None,
|
|
216
|
-
channel_ids: list[str] | None = None,
|
|
217
176
|
) -> dict:
|
|
218
|
-
"""Remove access to a canvas for users
|
|
177
|
+
"""Remove access to a canvas for users."""
|
|
219
178
|
payload: dict = {"canvas_id": canvas_id}
|
|
220
179
|
if user_ids:
|
|
221
180
|
payload["user_ids"] = user_ids
|
|
222
|
-
if channel_ids:
|
|
223
|
-
payload["channel_ids"] = channel_ids
|
|
224
181
|
resp = await client.api_call("canvases.access.delete", json=payload)
|
|
225
182
|
return _check(resp, "access.delete")
|
|
@@ -63,7 +63,12 @@ async def process_files_for_message(
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
block = await registry.process_file(
|
|
66
|
-
file_bytes,
|
|
66
|
+
file_bytes,
|
|
67
|
+
mimetype,
|
|
68
|
+
filename,
|
|
69
|
+
user_conversation_context,
|
|
70
|
+
storage,
|
|
71
|
+
file_id=file_info.get("id"),
|
|
67
72
|
)
|
|
68
73
|
if block is not None:
|
|
69
74
|
if block.get("type") != "image":
|