Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 100 additions & 56 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <relevant> # 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

Expand All @@ -51,16 +65,70 @@ 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 as a built-in middleware in the framework.

**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 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 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.

**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

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
Expand Down Expand Up @@ -90,49 +158,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/`:
Expand All @@ -153,14 +184,21 @@ 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

1. Add the new arg to `slack_bolt/kwargs_injection/args.py` and `async_args.py`
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.
Expand All @@ -176,7 +214,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
Expand All @@ -188,15 +228,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.
Loading