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.
Files changed (110) hide show
  1. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/AGENTS.md +3 -1
  2. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/CHANGELOG.md +20 -0
  3. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/PKG-INFO +6 -6
  4. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/README.md +5 -5
  5. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/agents/kitchen-sink/config.yaml +9 -1
  6. python_slack_agents-0.6.2/docs/canvas.md +118 -0
  7. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/llms-full.txt +50 -15
  8. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/pyproject.toml +1 -1
  9. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/__init__.py +2 -1
  10. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/files.py +3 -0
  11. python_slack_agents-0.6.2/src/slack_agents/slack/canvas_auth.py +58 -0
  12. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/canvases.py +4 -47
  13. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/files.py +6 -1
  14. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/tools/canvas.py +109 -88
  15. python_slack_agents-0.6.2/src/slack_agents/tools/canvas_importer.py +88 -0
  16. python_slack_agents-0.6.2/tests/test_canvas_auth.py +217 -0
  17. python_slack_agents-0.6.2/tests/test_canvas_importer.py +108 -0
  18. python_slack_agents-0.6.1/docs/canvas.md +0 -83
  19. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/.dockerignore +0 -0
  20. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/.env.example +0 -0
  21. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/.github/workflows/ci.yml +0 -0
  22. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/.github/workflows/publish.yml +0 -0
  23. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/.gitignore +0 -0
  24. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/.pre-commit-config.yaml +0 -0
  25. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/CODE_OF_CONDUCT.md +0 -0
  26. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/CONTRIBUTING.md +0 -0
  27. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/LICENSE +0 -0
  28. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/SECURITY.md +0 -0
  29. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/agents/README.md +0 -0
  30. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/agents/docs-assistant/config.yaml +0 -0
  31. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/agents/docs-assistant/system_prompt.txt +0 -0
  32. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/agents/hello-world/config.yaml +0 -0
  33. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/agents/hello-world/system_prompt.txt +0 -0
  34. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/agents/kitchen-sink/system_prompt.txt +0 -0
  35. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/access-control.md +0 -0
  36. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/agents.md +0 -0
  37. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/cli.md +0 -0
  38. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/deployment.md +0 -0
  39. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/llm.md +0 -0
  40. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/media/demo.gif +0 -0
  41. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/observability.md +0 -0
  42. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/private-repo.md +0 -0
  43. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/setup.md +0 -0
  44. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/slack-app-manifest.json +0 -0
  45. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/storage.md +0 -0
  46. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/tools.md +0 -0
  47. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/docs/user-context.md +0 -0
  48. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/llms.txt +0 -0
  49. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/Dockerfile +0 -0
  50. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/access/__init__.py +0 -0
  51. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/access/allow_all.py +0 -0
  52. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/access/allow_list.py +0 -0
  53. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/access/base.py +0 -0
  54. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/agent_loop.py +0 -0
  55. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/__init__.py +0 -0
  56. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/build_docker.py +0 -0
  57. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/export_conversations.py +0 -0
  58. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/export_conversations_html.py +0 -0
  59. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/export_usage.py +0 -0
  60. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/export_usage_csv.py +0 -0
  61. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/healthcheck.py +0 -0
  62. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/init.py +0 -0
  63. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/cli/run.py +0 -0
  64. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/config.py +0 -0
  65. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/conversations.py +0 -0
  66. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/llm/__init__.py +0 -0
  67. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/llm/anthropic.py +0 -0
  68. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/llm/base.py +0 -0
  69. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/llm/openai.py +0 -0
  70. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/main.py +0 -0
  71. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/observability.py +0 -0
  72. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/py.typed +0 -0
  73. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/scripts/__init__.py +0 -0
  74. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/scripts/download_fonts.py +0 -0
  75. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/scripts/generate_llms_full.py +0 -0
  76. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/__init__.py +0 -0
  77. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/actions.py +0 -0
  78. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/agent.py +0 -0
  79. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/format.py +0 -0
  80. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/streaming.py +0 -0
  81. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/streaming_formatter.py +0 -0
  82. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/slack/tool_blocks.py +0 -0
  83. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/storage/__init__.py +0 -0
  84. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/storage/base.py +0 -0
  85. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/storage/postgres.py +0 -0
  86. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/storage/postgres.sql +0 -0
  87. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/storage/sqlite.py +0 -0
  88. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/storage/sqlite.sql +0 -0
  89. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/tools/__init__.py +0 -0
  90. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/tools/base.py +0 -0
  91. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/tools/file_exporter.py +0 -0
  92. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/tools/file_importer.py +0 -0
  93. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/tools/mcp_http.py +0 -0
  94. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/src/slack_agents/tools/user_context.py +0 -0
  95. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/__init__.py +0 -0
  96. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_access.py +0 -0
  97. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_agent_loop.py +0 -0
  98. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_cli.py +0 -0
  99. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_config.py +0 -0
  100. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_conversations.py +0 -0
  101. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_cost.py +0 -0
  102. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_export_documents.py +0 -0
  103. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_export_usage.py +0 -0
  104. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_file_extractors.py +0 -0
  105. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_format.py +0 -0
  106. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_llm_factory.py +0 -0
  107. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_mcp_client.py +0 -0
  108. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_openai_convert.py +0 -0
  109. {python_slack_agents-0.6.1 → python_slack_agents-0.6.2}/tests/test_tool_blocks.py +0 -0
  110. {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 explicit user approval before committing or pushing.** Never commit or push autonomously.
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.1
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
- If this framework isn't the right fit, here are some good alternatives:
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
- - **[Slack Machine](https://github.com/DonDebonair/slack-machine)** — A mature Slack bot framework with a great plugin system for traditional chatops. No AI/LLM layer, but solid if you don't need agents.
336
- - **[bolt-python-ai-chatbot](https://github.com/slack-samples/bolt-python-ai-chatbot)** — Official Slack sample app for AI chatbots. Good starting point if you want to build from scratch rather than use a framework.
337
- - **[OpenAI Agents SDK](https://github.com/openai/openai-agents-python)**, **[Pydantic AI](https://github.com/pydantic/pydantic-ai)**, **[MCP Agent](https://github.com/lastmile-ai/mcp-agent)** — Powerful general-purpose agent frameworks. None include Slack integration, but if you already use one and want to wire it up to Slack yourself, they're excellent.
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, but well-built if you prefer Go.
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
- If this framework isn't the right fit, here are some good alternatives:
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
- - **[Slack Machine](https://github.com/DonDebonair/slack-machine)** — A mature Slack bot framework with a great plugin system for traditional chatops. No AI/LLM layer, but solid if you don't need agents.
290
- - **[bolt-python-ai-chatbot](https://github.com/slack-samples/bolt-python-ai-chatbot)** — Official Slack sample app for AI chatbots. Good starting point if you want to build from scratch rather than use a framework.
291
- - **[OpenAI Agents SDK](https://github.com/openai/openai-agents-python)**, **[Pydantic AI](https://github.com/pydantic/pydantic-ai)**, **[MCP Agent](https://github.com/lastmile-ai/mcp-agent)** — Powerful general-purpose agent frameworks. None include Slack integration, but if you already use one and want to wire it up to Slack yourself, they're excellent.
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, but well-built if you prefer Go.
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, edit, list, delete)
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 (`![](URL)`)
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, list, 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.
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 and list canvases |
615
+ | `canvases:read` | Read canvas content |
616
616
  | `canvases:write` | Create, update, delete canvases and manage access |
617
- | `files:read` | List canvases and read content (uses `files.list` / `files.info` APIs) |
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
- ## Slack plan requirements
642
+ ### 3. Canvas file importer (optional)
644
643
 
645
- - **Free plans**: 1 canvas per channel/DM. `canvas_create` requires `channel_id`.
646
- - **Paid plans** (Pro, Business+, Enterprise Grid): Unlimited canvases. `channel_id` is optional.
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. Optional `channel_id` to share it. |
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 or channels. |
674
- | `canvas_access_remove` | Remove access for users or channels. |
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 in a channel:**
679
- > "Create a canvas in #project-updates titled 'Q1 Roadmap' with our milestone list"
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 another team:**
685
- > "Give the #design channel write access to canvas F12345"
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.1"
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, optionally posting it to a channel.
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 and/or channels.
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 and/or channels."""
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, mimetype, filename, user_conversation_context, storage
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":