Skip to content

Commit c91b363

Browse files
Merge branch 'main' into ale-fix-get-thread-context
2 parents 68a0b64 + 898e0b8 commit c91b363

File tree

11 files changed

+184
-227
lines changed

11 files changed

+184
-227
lines changed

AGENTS.md

Lines changed: 100 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Slack Bolt for Python -- a framework for building Slack apps in Python.
1616

1717
## Environment Setup
1818

19+
You can verify the venv is active by checking `echo $VIRTUAL_ENV`. If tools like `black`, `flake8`, `mypy` or `pytest` are not found, ask the user to activate the venv.
20+
1921
A python virtual environment (`venv`) should be activated before running any commands.
2022

2123
```bash
@@ -29,18 +31,30 @@ source .venv/bin/activate
2931
./scripts/install.sh
3032
```
3133

32-
You can verify the venv is active by checking `echo $VIRTUAL_ENV`. If tools like `black`, `flake8`, `mypy` or `pytest` are not found, ask the user to activate the venv.
33-
3434
## Common Commands
3535

36-
### Testing
36+
### Pre-submission Checklist
3737

38-
Always use the project scripts instead of calling `pytest` directly:
38+
Before considering any work complete, you MUST run these commands in order and confirm they all pass:
39+
40+
```bash
41+
./scripts/format.sh --no-install # 1. Format
42+
./scripts/lint.sh --no-install # 2. Lint
43+
./scripts/run_tests.sh <relevant> # 3. Run relevant tests (see Testing below)
44+
./scripts/run_mypy.sh --no-install # 4. Type check
45+
```
46+
47+
To run everything at once (installs deps + formats + lints + tests + typechecks):
3948

4049
```bash
41-
# Install all dependencies and run all tests (formats, lints, tests, typechecks)
4250
./scripts/install_all_and_run_tests.sh
51+
```
4352

53+
### Testing
54+
55+
Always use the project scripts instead of calling `pytest` directly:
56+
57+
```bash
4458
# Run a single test file
4559
./scripts/run_tests.sh tests/scenario_tests/test_app.py
4660

@@ -51,16 +65,70 @@ Always use the project scripts instead of calling `pytest` directly:
5165
### Formatting, Linting, Type Checking
5266

5367
```bash
54-
# Format (black, line-length=125)
68+
# Format -- Black, configured in pyproject.toml
5569
./scripts/format.sh --no-install
5670

57-
# Lint (flake8, line-length=125, ignores: F841,F821,W503,E402)
71+
# Lint -- Flake8, configured in .flake8
5872
./scripts/lint.sh --no-install
5973

60-
# Type check (mypy)
74+
# Type check -- mypy, configured in pyproject.toml
6175
./scripts/run_mypy.sh --no-install
6276
```
6377

78+
## Critical Conventions
79+
80+
### Sync/Async Mirroring Rule
81+
82+
**When modifying any sync module, you MUST also update the corresponding async module (and vice versa).** This is the most important convention in this codebase.
83+
84+
Almost every module has both a sync and async variant. Async files use the `async_` prefix alongside their sync counterpart:
85+
86+
```text
87+
slack_bolt/middleware/custom_middleware.py # sync
88+
slack_bolt/middleware/async_custom_middleware.py # async
89+
90+
slack_bolt/context/say/say.py # sync
91+
slack_bolt/context/say/async_say.py # async
92+
93+
slack_bolt/listener/custom_listener.py # sync
94+
slack_bolt/listener/async_listener.py # async
95+
```
96+
97+
**Modules that come in sync/async pairs:**
98+
99+
- `slack_bolt/app/` -- `app.py` / `async_app.py`
100+
- `slack_bolt/middleware/` -- every middleware has an `async_` counterpart
101+
- `slack_bolt/listener/` -- `listener.py` / `async_listener.py`, plus error/completion/start handlers
102+
- `slack_bolt/listener_matcher/` -- `builtins.py` / `async_builtins.py`
103+
- `slack_bolt/context/` -- each subdirectory (e.g., `say/`, `ack/`, `respond/`) has `async_` variants
104+
- `slack_bolt/kwargs_injection/` -- `args.py` / `async_args.py`, `utils.py` / `async_utils.py`
105+
106+
**Adapters are an exception:** Most adapters are sync-only or async-only depending on the framework. Async-native frameworks (FastAPI, Starlette, Sanic, Tornado, ASGI, Socket Mode) have `async_handler.py`. Sync-only frameworks (Flask, Django, Bottle, CherryPy, Falcon, Pyramid, AWS Lambda, Google Cloud Functions, WSGI) have `handler.py`.
107+
108+
### Prefer the Middleware Pattern
109+
110+
Middleware is the project's preferred approach for cross-cutting concerns. Before adding logic to individual listeners or utility functions, consider whether it belongs as a built-in middleware in the framework.
111+
112+
**When to add built-in middleware:**
113+
114+
- Cross-cutting concerns that apply to many or all requests (logging, metrics, observability)
115+
- Request validation, transformation, or enrichment
116+
- Authorization extensions beyond the built-in `SingleTeamAuthorization`/`MultiTeamsAuthorization`
117+
- Feature-level request handling (the `Assistant` middleware in `slack_bolt/middleware/assistant/assistant.py` is the canonical example -- it intercepts assistant thread events and dispatches them to registered sub-listeners)
118+
119+
**How to add built-in middleware:**
120+
121+
1. Subclass `Middleware` (sync) and implement `process(self, *, req, resp, next)`. Call `next()` to continue the chain.
122+
2. Subclass `AsyncMiddleware` (async) and implement `async_process(self, *, req, resp, next)`. Call `await next()` to continue.
123+
3. Export from `slack_bolt/middleware/__init__.py` (sync) and `slack_bolt/middleware/async_builtins.py` (async).
124+
4. Register the middleware in `App.__init__()` (`slack_bolt/app/app.py`) and `AsyncApp.__init__()` (`slack_bolt/app/async_app.py`) where the default middleware chain is assembled.
125+
126+
**Canonical example:** `AttachingFunctionToken` (`slack_bolt/middleware/attaching_function_token/`) is a good small middleware to follow -- it has a clean sync/async pair, a focused `process()` method, and is properly exported and registered in the app's middleware chain.
127+
128+
### Single Runtime Dependency Rule
129+
130+
The core package depends ONLY on `slack_sdk` (defined in `pyproject.toml`). Never add runtime dependencies to `pyproject.toml`. Additional dependencies go in the appropriate `requirements/*.txt` file.
131+
64132
## Architecture
65133

66134
### Request Processing Pipeline
@@ -90,49 +158,12 @@ Listeners receive arguments by parameter name. The framework inspects function s
90158

91159
Each adapter in `slack_bolt/adapter/` converts between a web framework's request/response types and `BoltRequest`/`BoltResponse`. Adapters exist for: Flask, FastAPI, Django, Starlette, Sanic, Bottle, Tornado, CherryPy, Falcon, Pyramid, AWS Lambda, Google Cloud Functions, Socket Mode, WSGI, ASGI, and more.
92160

93-
### Sync/Async Mirroring Pattern
94-
95-
**This is the most important pattern in this codebase.** Almost every module has both a sync and async variant. When you modify one, you almost always must modify the other.
96-
97-
**File naming convention:** Async files use the `async_` prefix alongside their sync counterpart:
98-
99-
```text
100-
slack_bolt/middleware/custom_middleware.py # sync
101-
slack_bolt/middleware/async_custom_middleware.py # async
102-
103-
slack_bolt/context/say/say.py # sync
104-
slack_bolt/context/say/async_say.py # async
105-
106-
slack_bolt/listener/custom_listener.py # sync
107-
slack_bolt/listener/async_listener.py # async
108-
109-
slack_bolt/adapter/fastapi/async_handler.py # async-only (no sync FastAPI adapter)
110-
slack_bolt/adapter/flask/handler.py # sync-only (no async Flask adapter)
111-
```
112-
113-
**Which modules come in sync/async pairs:**
114-
115-
- `slack_bolt/app/` -- `app.py` / `async_app.py`
116-
- `slack_bolt/middleware/` -- every middleware has an `async_` counterpart
117-
- `slack_bolt/listener/` -- `listener.py` / `async_listener.py`, plus error/completion/start handlers
118-
- `slack_bolt/listener_matcher/` -- `builtins.py` / `async_builtins.py`
119-
- `slack_bolt/context/` -- each subdirectory (e.g., `say/`, `ack/`, `respond/`) has `async_` variants
120-
- `slack_bolt/kwargs_injection/` -- `args.py` / `async_args.py`, `utils.py` / `async_utils.py`
121-
122-
**Adapters are an exception:** Most adapters are sync-only or async-only depending on the framework. Async-native frameworks (FastAPI, Starlette, Sanic, Tornado, ASGI, Socket Mode) have `async_handler.py`. Sync-only frameworks (Flask, Django, Bottle, CherryPy, Falcon, Pyramid, AWS Lambda, Google Cloud Functions, WSGI) have `handler.py`.
123-
124161
### AI Agents & Assistants
125162

126163
`BoltAgent` (`slack_bolt/agent/`) provides `chat_stream()`, `set_status()`, and `set_suggested_prompts()` for AI-powered agents. `Assistant` middleware (`slack_bolt/middleware/assistant/`) handles assistant thread events.
127164

128165
## Key Development Patterns
129166

130-
### Adding or Modifying Middleware
131-
132-
1. Implement the sync version in `slack_bolt/middleware/` (subclass `Middleware`, implement `process()`)
133-
2. Implement the async version with `async_` prefix (subclass `AsyncMiddleware`, implement `async_process()`)
134-
3. Export built-in middleware from `slack_bolt/middleware/__init__.py` (sync) and `async_builtins.py` (async)
135-
136167
### Adding a Context Utility
137168

138169
Each context utility lives in its own subdirectory under `slack_bolt/context/`:
@@ -153,14 +184,21 @@ Then wire it into `BoltContext` (`slack_bolt/context/context.py`) and `AsyncBolt
153184
2. Add `__init__.py` and `handler.py` (or `async_handler.py` for async frameworks)
154185
3. The handler converts the framework's request to `BoltRequest`, calls `app.dispatch()`, and converts `BoltResponse` back
155186
4. Add the framework to `requirements/adapter.txt` with version constraints
156-
5. Add adapter tests in `tests/adapter_tests/` (or `tests/adapter_tests_async/`)
187+
5. Add adapter tests in `tests/adapter_tests/` (sync) or `tests/adapter_tests_async/` (async)
157188

158189
### Adding a Kwargs-Injectable Argument
159190

160191
1. Add the new arg to `slack_bolt/kwargs_injection/args.py` and `async_args.py`
161192
2. Update the `Args` class with the new property
162193
3. Populate the arg in the appropriate context or listener setup code
163194

195+
## Security Considerations
196+
197+
- **Request Verification:** The built-in `RequestVerification` middleware validates `x-slack-signature` and `x-slack-request-timestamp` on every incoming HTTP request. Never disable this in production. It is automatically skipped for `socket_mode` requests.
198+
- **Tokens & Secrets:** `SLACK_SIGNING_SECRET` and `SLACK_BOT_TOKEN` must come from environment variables. Never hardcode or commit secrets.
199+
- **Authorization Middleware:** `SingleTeamAuthorization` and `MultiTeamsAuthorization` verify tokens and inject an authorized `WebClient` into the context. Do not bypass these.
200+
- **Tests:** Always use mock servers (`tests/mock_web_api_server/`) and dummy values. Never use real tokens in tests.
201+
164202
## Dependencies
165203

166204
The core package has a **single required runtime dependency**: `slack_sdk` (defined in `pyproject.toml`). Do not add runtime dependencies.
@@ -176,7 +214,9 @@ The core package has a **single required runtime dependency**: `slack_sdk` (defi
176214

177215
When adding a new dependency: add it to the appropriate `requirements/*.txt` file with version constraints, never to `pyproject.toml` `dependencies` (unless it's a core runtime dep, which is very rare).
178216

179-
## Test Organization
217+
## Test Organization and CI
218+
219+
### Directory Structure
180220

181221
- `tests/scenario_tests/` -- Integration-style tests with realistic Slack payloads
182222
- `tests/slack_bolt/` -- Unit tests mirroring the source structure
@@ -188,15 +228,19 @@ When adding a new dependency: add it to the appropriate `requirements/*.txt` fil
188228

189229
**Mock server:** Many tests use `tests/mock_web_api_server/` to simulate Slack API responses. Look at existing tests for usage patterns rather than making real API calls.
190230

191-
## Code Style
231+
### CI Pipeline
232+
233+
GitHub Actions (`.github/workflows/ci-build.yml`) runs on every push to `main` and every PR:
192234

193-
- **Black** formatter configured in `pyproject.toml` (line-length=125)
194-
- **Flake8** linter configured in `.flake8` (line-length=125, ignores: F841,F821,W503,E402)
195-
- **MyPy** configured in `pyproject.toml`
196-
- **pytest** configured in `pyproject.toml`
235+
- **Lint** -- `./scripts/lint.sh` on latest Python
236+
- **Typecheck** -- `./scripts/run_mypy.sh` on latest Python
237+
- **Unit tests** -- full test suite across Python 3.7--3.14 matrix
238+
- **Code coverage** -- uploaded to Codecov
197239

198-
## GitHub & CI/CD
240+
## PR and Commit Guidelines
199241

200-
- `.github/` -- GitHub-specific configuration and documentation
201-
- `.github/workflows/` -- Continuous integration pipeline definitions that run on GitHub Actions
202-
- `.github/maintainers_guide.md` -- Maintainer workflows and release process
242+
- PRs target the `main` branch
243+
- You MUST run `./scripts/install_all_and_run_tests.sh` before submitting
244+
- PR template (`.github/pull_request_template.md`) requires: Summary, Testing steps, Category checkboxes (`App`, `AsyncApp`, Adapters, Docs, Others)
245+
- Requirements: CLA signed, test suite passes, code review approval
246+
- Commits should be atomic with descriptive messages. Reference related issue numbers.

requirements/tools.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
mypy==1.19.1
22
flake8==7.3.0
3-
black==25.1.0
3+
black==26.3.1

slack_bolt/app/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,7 +1401,7 @@ def _init_context(self, req: BoltRequest):
14011401
# For AI Agents & Assistants
14021402
if is_assistant_event(req.body):
14031403
assistant = AssistantUtilities(
1404-
payload=to_event(req.body), # type:ignore[arg-type]
1404+
payload=to_event(req.body), # type: ignore[arg-type]
14051405
context=req.context,
14061406
thread_context_store=self._assistant_thread_context_store,
14071407
)
@@ -1457,7 +1457,7 @@ def _register_listener(
14571457
CustomListener(
14581458
app_name=self.name,
14591459
ack_function=functions.pop(0),
1460-
lazy_functions=functions, # type:ignore[arg-type]
1460+
lazy_functions=functions, # type: ignore[arg-type]
14611461
matchers=listener_matchers,
14621462
middleware=listener_middleware,
14631463
auto_acknowledgement=auto_acknowledgement,

slack_bolt/app/async_app.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,7 @@ async def async_middleware_next():
616616
self._framework_logger.debug(debug_checking_listener(listener_name))
617617
if await listener.async_matches(req=req, resp=resp): # type: ignore[arg-type]
618618
# run all the middleware attached to this listener first
619-
(middleware_resp, next_was_not_called) = await listener.run_async_middleware(
619+
middleware_resp, next_was_not_called = await listener.run_async_middleware(
620620
req=req, resp=resp # type: ignore[arg-type]
621621
)
622622
if next_was_not_called:
@@ -1434,7 +1434,7 @@ def _init_context(self, req: AsyncBoltRequest):
14341434
# For AI Agents & Assistants
14351435
if is_assistant_event(req.body):
14361436
assistant = AsyncAssistantUtilities(
1437-
payload=to_event(req.body), # type:ignore[arg-type]
1437+
payload=to_event(req.body), # type: ignore[arg-type]
14381438
context=req.context,
14391439
thread_context_store=self._assistant_thread_context_store,
14401440
)
@@ -1495,7 +1495,7 @@ def _register_listener(
14951495
AsyncCustomListener(
14961496
app_name=self.name,
14971497
ack_function=functions.pop(0),
1498-
lazy_functions=functions, # type:ignore[arg-type]
1498+
lazy_functions=functions, # type: ignore[arg-type]
14991499
matchers=listener_matchers,
15001500
middleware=listener_middleware,
15011501
auto_acknowledgement=auto_acknowledgement,

slack_bolt/middleware/assistant/assistant.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def thread_started(
6767
self.build_listener(
6868
listener_or_functions=func,
6969
matchers=all_matchers,
70-
middleware=middleware, # type:ignore[arg-type]
70+
middleware=middleware, # type: ignore[arg-type]
7171
)
7272
)
7373
return func
@@ -106,7 +106,7 @@ def user_message(
106106
self.build_listener(
107107
listener_or_functions=func,
108108
matchers=all_matchers,
109-
middleware=middleware, # type:ignore[arg-type]
109+
middleware=middleware, # type: ignore[arg-type]
110110
)
111111
)
112112
return func
@@ -145,7 +145,7 @@ def bot_message(
145145
self.build_listener(
146146
listener_or_functions=func,
147147
matchers=all_matchers,
148-
middleware=middleware, # type:ignore[arg-type]
148+
middleware=middleware, # type: ignore[arg-type]
149149
)
150150
)
151151
return func
@@ -184,7 +184,7 @@ def thread_context_changed(
184184
self.build_listener(
185185
listener_or_functions=func,
186186
matchers=all_matchers,
187-
middleware=middleware, # type:ignore[arg-type]
187+
middleware=middleware, # type: ignore[arg-type]
188188
)
189189
)
190190
return func
@@ -214,13 +214,13 @@ def _merge_matchers(
214214
):
215215
return [CustomListenerMatcher(app_name=self.app_name, func=primary_matcher)] + (
216216
custom_matchers or []
217-
) # type:ignore[operator]
217+
) # type: ignore[operator]
218218

219219
@staticmethod
220220
def default_thread_context_changed(save_thread_context: SaveThreadContext, payload: dict):
221221
save_thread_context(payload["assistant_thread"]["context"])
222222

223-
def process( # type:ignore[return]
223+
def process( # type: ignore[return]
224224
self, *, req: BoltRequest, resp: BoltResponse, next: Callable[[], BoltResponse]
225225
) -> Optional[BoltResponse]:
226226
if self._thread_context_changed_listeners is None:
@@ -255,8 +255,8 @@ def build_listener(
255255
middleware: Optional[List[Middleware]] = None,
256256
base_logger: Optional[Logger] = None,
257257
) -> Listener:
258-
if isinstance(listener_or_functions, Callable): # type:ignore[arg-type]
259-
listener_or_functions = [listener_or_functions] # type:ignore[list-item]
258+
if isinstance(listener_or_functions, Callable): # type: ignore[arg-type]
259+
listener_or_functions = [listener_or_functions] # type: ignore[list-item]
260260

261261
if isinstance(listener_or_functions, Listener):
262262
return listener_or_functions
@@ -270,7 +270,7 @@ def build_listener(
270270
for matcher in matchers:
271271
if isinstance(matcher, ListenerMatcher):
272272
listener_matchers.append(matcher)
273-
elif isinstance(matcher, Callable): # type:ignore[arg-type]
273+
elif isinstance(matcher, Callable): # type: ignore[arg-type]
274274
listener_matchers.append(
275275
build_listener_matcher(
276276
func=matcher,

0 commit comments

Comments
 (0)