From b8295f3c350bd6fb1b5952a7d3226df4b5caa72b Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Thu, 12 Mar 2026 17:04:30 -0400 Subject: [PATCH 1/2] chore: improve the AGENTS.md --- AGENTS.md | 163 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 107 insertions(+), 56 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 537cabfcf..994c22e93 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,8 @@ Slack Bolt for Python -- a framework for building Slack apps in Python. ## Environment Setup +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. + A python virtual environment (`venv`) should be activated before running any commands. ```bash @@ -29,18 +31,30 @@ source .venv/bin/activate ./scripts/install.sh ``` -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. - ## Common Commands -### Testing +### Pre-submission Checklist -Always use the project scripts instead of calling `pytest` directly: +Before considering any work complete, you MUST run these commands in order and confirm they all pass: + +```bash +./scripts/format.sh --no-install # 1. Format +./scripts/lint.sh --no-install # 2. Lint +./scripts/run_tests.sh # 3. Run relevant tests (see Testing below) +./scripts/run_mypy.sh --no-install # 4. Type check +``` + +To run everything at once (installs deps + formats + lints + tests + typechecks): ```bash -# Install all dependencies and run all tests (formats, lints, tests, typechecks) ./scripts/install_all_and_run_tests.sh +``` +### Testing + +Always use the project scripts instead of calling `pytest` directly: + +```bash # Run a single test file ./scripts/run_tests.sh tests/scenario_tests/test_app.py @@ -51,16 +65,77 @@ Always use the project scripts instead of calling `pytest` directly: ### Formatting, Linting, Type Checking ```bash -# Format (black, line-length=125) +# Format -- Black, configured in pyproject.toml ./scripts/format.sh --no-install -# Lint (flake8, line-length=125, ignores: F841,F821,W503,E402) +# Lint -- Flake8, configured in .flake8 ./scripts/lint.sh --no-install -# Type check (mypy) +# Type check -- mypy, configured in pyproject.toml ./scripts/run_mypy.sh --no-install ``` +## Critical Conventions + +### Sync/Async Mirroring Rule + +**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. + +Almost every module has both a sync and async variant. Async files use the `async_` prefix alongside their sync counterpart: + +```text +slack_bolt/middleware/custom_middleware.py # sync +slack_bolt/middleware/async_custom_middleware.py # async + +slack_bolt/context/say/say.py # sync +slack_bolt/context/say/async_say.py # async + +slack_bolt/listener/custom_listener.py # sync +slack_bolt/listener/async_listener.py # async +``` + +**Modules that come in sync/async pairs:** + +- `slack_bolt/app/` -- `app.py` / `async_app.py` +- `slack_bolt/middleware/` -- every middleware has an `async_` counterpart +- `slack_bolt/listener/` -- `listener.py` / `async_listener.py`, plus error/completion/start handlers +- `slack_bolt/listener_matcher/` -- `builtins.py` / `async_builtins.py` +- `slack_bolt/context/` -- each subdirectory (e.g., `say/`, `ack/`, `respond/`) has `async_` variants +- `slack_bolt/kwargs_injection/` -- `args.py` / `async_args.py`, `utils.py` / `async_utils.py` + +**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`. + +### Prefer the Middleware Pattern + +Middleware is the project's preferred approach for cross-cutting concerns. Before adding logic to individual listeners or utility functions, consider whether it belongs in the middleware chain. + +**When to use middleware:** + +- Cross-cutting concerns that apply to many or all requests (logging, metrics, observability) +- Request validation, transformation, or enrichment +- Authorization extensions beyond the built-in `SingleTeamAuthorization`/`MultiTeamsAuthorization` +- 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) + +**How to implement middleware:** + +1. Subclass `Middleware` (sync) and implement `process(self, *, req, resp, next)`. Call `next()` to continue the chain. +2. Subclass `AsyncMiddleware` (async) and implement `async_process(self, *, req, resp, next)`. Call `await next()` to continue. +3. Export from `slack_bolt/middleware/__init__.py` (sync) and `slack_bolt/middleware/async_builtins.py` (async). +4. Register via `App(middleware=[...])` or the `@app.middleware` decorator. + +**Simple example using the decorator:** + +```python +@app.middleware +def log_request(logger, body, next): + logger.debug(f"Incoming request: {body.get('type')}") + return next() +``` + +### Single Runtime Dependency Rule + +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. + ## Architecture ### Request Processing Pipeline @@ -90,49 +165,12 @@ Listeners receive arguments by parameter name. The framework inspects function s 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. -### Sync/Async Mirroring Pattern - -**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. - -**File naming convention:** Async files use the `async_` prefix alongside their sync counterpart: - -```text -slack_bolt/middleware/custom_middleware.py # sync -slack_bolt/middleware/async_custom_middleware.py # async - -slack_bolt/context/say/say.py # sync -slack_bolt/context/say/async_say.py # async - -slack_bolt/listener/custom_listener.py # sync -slack_bolt/listener/async_listener.py # async - -slack_bolt/adapter/fastapi/async_handler.py # async-only (no sync FastAPI adapter) -slack_bolt/adapter/flask/handler.py # sync-only (no async Flask adapter) -``` - -**Which modules come in sync/async pairs:** - -- `slack_bolt/app/` -- `app.py` / `async_app.py` -- `slack_bolt/middleware/` -- every middleware has an `async_` counterpart -- `slack_bolt/listener/` -- `listener.py` / `async_listener.py`, plus error/completion/start handlers -- `slack_bolt/listener_matcher/` -- `builtins.py` / `async_builtins.py` -- `slack_bolt/context/` -- each subdirectory (e.g., `say/`, `ack/`, `respond/`) has `async_` variants -- `slack_bolt/kwargs_injection/` -- `args.py` / `async_args.py`, `utils.py` / `async_utils.py` - -**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`. - ### AI Agents & Assistants `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. ## Key Development Patterns -### Adding or Modifying Middleware - -1. Implement the sync version in `slack_bolt/middleware/` (subclass `Middleware`, implement `process()`) -2. Implement the async version with `async_` prefix (subclass `AsyncMiddleware`, implement `async_process()`) -3. Export built-in middleware from `slack_bolt/middleware/__init__.py` (sync) and `async_builtins.py` (async) - ### Adding a Context Utility Each context utility lives in its own subdirectory under `slack_bolt/context/`: @@ -153,7 +191,7 @@ Then wire it into `BoltContext` (`slack_bolt/context/context.py`) and `AsyncBolt 2. Add `__init__.py` and `handler.py` (or `async_handler.py` for async frameworks) 3. The handler converts the framework's request to `BoltRequest`, calls `app.dispatch()`, and converts `BoltResponse` back 4. Add the framework to `requirements/adapter.txt` with version constraints -5. Add adapter tests in `tests/adapter_tests/` (or `tests/adapter_tests_async/`) +5. Add adapter tests in `tests/adapter_tests/` (sync) or `tests/adapter_tests_async/` (async) ### Adding a Kwargs-Injectable Argument @@ -161,6 +199,13 @@ Then wire it into `BoltContext` (`slack_bolt/context/context.py`) and `AsyncBolt 2. Update the `Args` class with the new property 3. Populate the arg in the appropriate context or listener setup code +## Security Considerations + +- **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. +- **Tokens & Secrets:** `SLACK_SIGNING_SECRET` and `SLACK_BOT_TOKEN` must come from environment variables. Never hardcode or commit secrets. +- **Authorization Middleware:** `SingleTeamAuthorization` and `MultiTeamsAuthorization` verify tokens and inject an authorized `WebClient` into the context. Do not bypass these. +- **Tests:** Always use mock servers (`tests/mock_web_api_server/`) and dummy values. Never use real tokens in tests. + ## Dependencies The core package has a **single required runtime dependency**: `slack_sdk` (defined in `pyproject.toml`). Do not add runtime dependencies. @@ -176,7 +221,9 @@ The core package has a **single required runtime dependency**: `slack_sdk` (defi 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). -## Test Organization +## Test Organization and CI + +### Directory Structure - `tests/scenario_tests/` -- Integration-style tests with realistic Slack payloads - `tests/slack_bolt/` -- Unit tests mirroring the source structure @@ -188,15 +235,19 @@ When adding a new dependency: add it to the appropriate `requirements/*.txt` fil **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. -## Code Style +### CI Pipeline + +GitHub Actions (`.github/workflows/ci-build.yml`) runs on every push to `main` and every PR: -- **Black** formatter configured in `pyproject.toml` (line-length=125) -- **Flake8** linter configured in `.flake8` (line-length=125, ignores: F841,F821,W503,E402) -- **MyPy** configured in `pyproject.toml` -- **pytest** configured in `pyproject.toml` +- **Lint** -- `./scripts/lint.sh` on latest Python +- **Typecheck** -- `./scripts/run_mypy.sh` on latest Python +- **Unit tests** -- full test suite across Python 3.7--3.14 matrix +- **Code coverage** -- uploaded to Codecov -## GitHub & CI/CD +## PR and Commit Guidelines -- `.github/` -- GitHub-specific configuration and documentation -- `.github/workflows/` -- Continuous integration pipeline definitions that run on GitHub Actions -- `.github/maintainers_guide.md` -- Maintainer workflows and release process +- PRs target the `main` branch +- You MUST run `./scripts/install_all_and_run_tests.sh` before submitting +- PR template (`.github/pull_request_template.md`) requires: Summary, Testing steps, Category checkboxes (`App`, `AsyncApp`, Adapters, Docs, Others) +- Requirements: CLA signed, test suite passes, code review approval +- Commits should be atomic with descriptive messages. Reference related issue numbers. From f2b9d07e3e13c83b7dd378db5aa4dc41b47c147e Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Thu, 12 Mar 2026 17:15:31 -0400 Subject: [PATCH 2/2] Update AGENTS.md --- AGENTS.md | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 994c22e93..57f2fa588 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -107,30 +107,23 @@ slack_bolt/listener/async_listener.py # async ### Prefer the Middleware Pattern -Middleware is the project's preferred approach for cross-cutting concerns. Before adding logic to individual listeners or utility functions, consider whether it belongs in the middleware chain. +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. -**When to use middleware:** +**When to add built-in middleware:** - Cross-cutting concerns that apply to many or all requests (logging, metrics, observability) - Request validation, transformation, or enrichment - Authorization extensions beyond the built-in `SingleTeamAuthorization`/`MultiTeamsAuthorization` - 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) -**How to implement middleware:** +**How to add built-in middleware:** 1. Subclass `Middleware` (sync) and implement `process(self, *, req, resp, next)`. Call `next()` to continue the chain. 2. Subclass `AsyncMiddleware` (async) and implement `async_process(self, *, req, resp, next)`. Call `await next()` to continue. 3. Export from `slack_bolt/middleware/__init__.py` (sync) and `slack_bolt/middleware/async_builtins.py` (async). -4. Register via `App(middleware=[...])` or the `@app.middleware` decorator. +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. -**Simple example using the decorator:** - -```python -@app.middleware -def log_request(logger, body, next): - logger.debug(f"Incoming request: {body.get('type')}") - return next() -``` +**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. ### Single Runtime Dependency Rule