mattermost/server/public/pluginapi/grpc/ARCHITECTURE.md
Nick Misasi f1f1ee8d95 docs(13-01): create comprehensive gRPC architecture documentation
Add ARCHITECTURE.md documenting the Python plugin gRPC system including:
- High-level overview and ASCII system architecture diagram
- Component layer documentation (Protocol, Go Infrastructure, Python SDK)
- Process lifecycle (loading, environment variables, shutdown)
- Communication flow diagrams (hooks, API calls, ServeHTTP streaming)
- Key design decisions (embedded AppError, 64KB chunks, APIServerRegistrar)
- Complete file reference table mapping functionality to source files
- Extension guide for adding new API methods and hooks

This documentation enables internal Mattermost engineers to understand,
debug, and extend the Python plugin infrastructure.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 10:38:52 -05:00

566 lines
26 KiB
Markdown

# Python Plugin gRPC Architecture
This document describes the architecture of the Mattermost Python plugin system, which enables server-side plugins written in Python to have full API parity with Go plugins.
## Overview
### Purpose
The Python plugin system extends Mattermost's existing plugin infrastructure to support plugins written in languages beyond Go. It uses gRPC with Protocol Buffers for language-agnostic communication while maintaining the subprocess-per-plugin model and seamless integration with the existing plugin infrastructure.
### Key Value Proposition
**Full API parity**: Every API method and hook available to Go plugins works identically from Python plugins. Python plugins can:
- Receive all server hooks (message events, user events, HTTP requests, etc.)
- Call all 100+ Plugin API methods (users, channels, posts, KV store, etc.)
- Handle HTTP requests via bidirectional streaming
### High-Level Architecture
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Mattermost Server │
│ │
│ ┌──────────────┐ ┌─────────────────┐ ┌────────────────────────────┐ │
│ │ App Layer │───▶│ Plugin Env │───▶│ Python Supervisor │ │
│ │ │ │ (environment.go)│ │ (python_supervisor.go) │ │
│ └──────────────┘ └─────────────────┘ └────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌────────────────────────────┐ │
│ │ hooksGRPCClient │ │ PluginAPI gRPC Server │ │
│ │ (hook dispatch) │ │ (api_server.go) │ │
│ └────────┬────────┘ └────────────┬───────────────┘ │
│ │ │ │
└───────────────────────────────┼──────────────────────────┼──────────────────┘
│ gRPC │ gRPC
Hooks call │ │ API calls
(Server→Plugin) │ (Plugin→Server)
▼ ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Python Plugin Process │
│ │
│ ┌────────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ PluginHooks │ │ Plugin Class │ │ API Client │ │
│ │ gRPC Server │◀───│ (user code) │───▶│ (calls Go API) │ │
│ │ (server.py) │ │ (plugin.py) │ │ (client.py) │ │
│ └────────────────────┘ └─────────────────┘ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
## Component Layers
### Protocol Layer
Protocol Buffer definitions define the contract between Go and Python:
| File | Purpose |
|------|---------|
| `proto/api.proto` | Main PluginAPI service definition (100+ methods) |
| `proto/hooks.proto` | PluginHooks service definition (30+ hooks) |
| `proto/api_user_team.proto` | User and team API message types |
| `proto/api_channel_post.proto` | Channel, post, emoji message types |
| `proto/api_kv_config.proto` | KV store, config, logging message types |
| `proto/api_file_bot.proto` | File and bot message types |
| `proto/api_remaining.proto` | Server, command, preference, group, and other types |
| `proto/hooks_lifecycle.proto` | Lifecycle hook messages (OnActivate, etc.) |
| `proto/hooks_message.proto` | Message hook messages (MessageWillBePosted, etc.) |
| `proto/hooks_user_channel.proto` | User and channel hook messages |
| `proto/hooks_command.proto` | Command, WebSocket, cluster hook messages |
| `proto/hooks_http.proto` | HTTP streaming hook messages (ServeHTTP) |
| `proto/types.proto` | Shared model types (User, Post, Channel, etc.) |
### Go Infrastructure Layer
#### Python Supervisor (`server/public/plugin/python_supervisor.go`)
Manages Python plugin process lifecycle:
- **Runtime Detection**: Detects Python plugins via `manifest.Server.Runtime == "python"` or `.py` extension
- **Interpreter Discovery**: Finds Python interpreter (venv-first, then system PATH)
- **Process Spawning**: Creates exec.Cmd with proper working directory
- **API Server Startup**: Starts gRPC PluginAPI server before subprocess
- **Environment Setup**: Sets `MATTERMOST_PLUGIN_API_TARGET` env var with server address
- **Graceful Shutdown**: 5-second WaitDelay for graceful termination
Key functions:
```go
func isPythonPlugin(manifest *model.Manifest) bool
func findPythonInterpreter(pluginDir string) (string, error)
func startAPIServer(apiImpl API, registrar APIServerRegistrar) (string, func(), error)
func configurePythonCommand(pluginInfo *model.BundleInfo, ...) error
```
#### Hooks gRPC Client (`server/public/plugin/hooks_grpc_client.go`)
Dispatches hook invocations to Python plugins:
- **Hook Registry**: Queries `Implemented()` RPC to know which hooks the plugin handles
- **Hook Dispatch**: Calls appropriate gRPC method for each hook type
- **Timeout Management**: 30-second default timeout per hook call
- **Bidirectional Streaming**: Handles ServeHTTP with request/response streaming
- **Model Conversion**: Converts between model types and proto messages
Key type:
```go
type hooksGRPCClient struct {
client pb.PluginHooksClient
implemented [TotalHooksID]bool
log *mlog.Logger
}
```
#### API Server (`server/public/pluginapi/grpc/server/api_server.go`)
gRPC server that Python plugins call back to:
- **API Wrapping**: Wraps the plugin.API interface for gRPC access
- **Method Implementations**: Each RPC delegates to corresponding API method
- **Error Conversion**: Converts model.AppError to response-embedded errors (not gRPC status)
- **Registration**: `Register(grpcServer, apiImpl)` registers the service
Key type:
```go
type APIServer struct {
pb.UnimplementedPluginAPIServer
impl plugin.API
}
```
#### ServeHTTP Handler (`server/public/pluginapi/grpc/server/serve_http.go`)
Handles bidirectional HTTP streaming:
- **64KB Chunking**: Streams request/response bodies in 64KB chunks
- **Early Response**: Plugin can respond before request body is fully sent
- **Cancellation**: HTTP client disconnect propagates via gRPC context
- **Status Code Validation**: Protects against invalid status codes (100-999 range)
- **Flush Support**: Best-effort flushing when ResponseWriter supports Flusher
Constants:
```go
const DefaultChunkSize = 64 * 1024 // 64KB
```
### Python SDK Layer
#### Plugin Base Class (`python-sdk/src/mattermost_plugin/plugin.py`)
Base class for plugin authors:
- **`__init_subclass__`**: Auto-discovers @hook decorated methods at class definition time
- **Hook Registry**: Builds class-level `_hook_registry` mapping hook names to handlers
- **API Access**: Provides `self.api` property for making API calls
- **Logging**: Provides `self.logger` for plugin logging
```python
class Plugin:
_hook_registry: Dict[str, Callable[..., Any]] = {}
def __init_subclass__(cls, **kwargs):
# Discover @hook decorated methods
@classmethod
def implemented_hooks(cls) -> List[str]:
# Returns list of hook names for Implemented() RPC
```
#### Hook System (`python-sdk/src/mattermost_plugin/hooks.py`)
Decorator-based hook registration:
- **`@hook` Decorator**: Marks methods as hook handlers
- **`HookName` Enum**: All 30+ canonical hook names matching Go
- **Validation**: Rejects unknown hook names at decoration time
- **Async Support**: Handles both sync and async handlers
```python
class HookName(str, Enum):
OnActivate = "OnActivate"
MessageWillBePosted = "MessageWillBePosted"
ServeHTTP = "ServeHTTP"
# ... 30+ hooks
@hook(HookName.OnActivate)
def on_activate(self) -> None:
pass
```
#### API Client (`python-sdk/src/mattermost_plugin/client.py`)
Typed client for calling Mattermost API:
- **Context Manager**: `with PluginAPIClient() as client:`
- **Mixin Architecture**: Organized by API domain (users, teams, channels, etc.)
- **Error Handling**: Converts gRPC errors and AppErrors to SDK exceptions
- **Type Safety**: Full type hints for all methods
```python
class PluginAPIClient(
UsersMixin,
TeamsMixin,
ChannelsMixin,
PostsMixin,
# ... more mixins
):
def get_server_version(self) -> str: ...
def get_user(self, user_id: str) -> User: ...
```
#### gRPC Server (`python-sdk/src/mattermost_plugin/server.py`)
Plugin-side gRPC server for receiving hooks:
- **Async Server**: Uses grpc.aio for async gRPC
- **Health Service**: Registers grpc.health.v1.Health for go-plugin
- **Handshake**: Outputs go-plugin handshake line to stdout
- **Signal Handling**: Graceful shutdown on SIGTERM/SIGINT
```python
async def serve_plugin(plugin_class: Type[Plugin]) -> None:
# 1. Load runtime config from environment
# 2. Create and connect API client
# 3. Instantiate plugin
# 4. Start gRPC server with health service
# 5. Output handshake line
# 6. Wait for termination
```
## Process Lifecycle
### Plugin Loading Sequence
```
┌──────────────────┐
│ 1. Manifest Parse│ Server reads plugin.json, detects runtime="python"
└────────┬─────────┘
┌──────────────────┐
│ 2. API Server │ Start gRPC PluginAPI server on random port
│ Startup │ Store cleanup function for shutdown
└────────┬─────────┘
┌──────────────────┐
│ 3. Python Process│ python_supervisor.go builds exec.Cmd:
│ Spawn │ - Find Python interpreter (venv-first)
│ │ - Set MATTERMOST_PLUGIN_API_TARGET env var
│ │ - Set working directory to plugin path
└────────┬─────────┘
┌──────────────────┐
│ 4. go-plugin │ Wait for handshake line on stdout:
│ Handshake │ "1|1|tcp|127.0.0.1:PORT|grpc"
│ │ Health check confirms plugin is serving
└────────┬─────────┘
┌──────────────────┐
│ 5. Implemented() │ Query which hooks the plugin implements
│ RPC │ Populate implemented[] array for optimization
└────────┬─────────┘
┌──────────────────┐
│ 6. OnActivate() │ Call OnActivate hook if implemented
│ Hook │ Plugin can initialize, register commands
└────────┬─────────┘
┌──────────────────┐
│ 7. Running │ Plugin is now active and receiving hooks
└──────────────────┘
```
### Environment Variables
| Variable | Set By | Used By | Purpose |
|----------|--------|---------|---------|
| `MATTERMOST_PLUGIN_API_TARGET` | Go Supervisor | Python Client | gRPC address for API calls |
| `MATTERMOST_PLUGIN_ID` | Go Supervisor | Python SDK | Plugin identifier |
| `MATTERMOST_LOG_LEVEL` | Go Supervisor | Python SDK | Logging level configuration |
### Shutdown Sequence
```
┌──────────────────┐
│ 1. OnDeactivate()│ Server calls OnDeactivate hook
└────────┬─────────┘
┌──────────────────┐
│ 2. gRPC Client │ Close gRPC connection to plugin
│ Close │
└────────┬─────────┘
┌──────────────────┐
│ 3. Process │ Send termination signal, wait 5 seconds
│ Termination │ (cmd.WaitDelay = 5 * time.Second)
└────────┬─────────┘
┌──────────────────┐
│ 4. API Server │ Call apiServerCleanup() for graceful stop
│ Shutdown │ GracefulStop() drains pending requests
└──────────────────┘
```
## Communication Flow
### Hook Dispatch (Server to Plugin)
```
Go Server Python Plugin
│ │
│ ╔═══════════════════════════════════════╗ │
│ ║ Hook Event (e.g., MessageWillBePosted) ║ │
│ ╚═══════════════════════════════════════╝ │
│ │
├──────────────────────────────────────────────►
│ gRPC: MessageWillBePosted │
│ (context, post proto) │
│ │
│ ┌────────┤
│ │ Plugin │
│ │ @hook │
│ │ handler│
│ └────────┤
│ │
◄──────────────────────────────────────────────┤
│ Response │
│ (modified_post, rejection_reason) │
│ │
```
### API Call (Plugin to Server)
```
Python Plugin Go Server
│ │
│ ╔═══════════════════════════════════════╗ │
│ ║ Plugin code: self.api.get_user(id) ║ │
│ ╚═══════════════════════════════════════╝ │
│ │
├──────────────────────────────────────────────►
│ gRPC: GetUser │
│ (user_id) │
│ │
│ ┌────────┤
│ │ API │
│ │ Server │
│ │ impl │
│ └────────┤
│ │
◄──────────────────────────────────────────────┤
│ Response │
│ (user proto, error) │
│ │
```
### ServeHTTP Bidirectional Streaming
```
Go Server Python Plugin
│ │
│ ╔═══════════════════════════════════════╗ │
│ ║ HTTP Request to /plugins/{id}/... ║ │
│ ╚═══════════════════════════════════════╝ │
│ │
│ ════════ Bidirectional Stream ════════ │
│ │
├───────────────────────────────────────────────►
│ Request Init (method, URL, headers) │
│ + first body chunk │
│ │
├───────────────────────────────────────────────►
│ Body chunk 2 (64KB) │
│ │
├───────────────────────────────────────────────►
│ Body chunk 3 + body_complete=true │
│ │
│ ┌────────┤
│ │ @hook │
│ │ServeHTTP│
│ └────────┤
│ │
◄───────────────────────────────────────────────┤
│ Response Init (status=200, headers) │
│ + first response chunk │
│ │
◄───────────────────────────────────────────────┤
│ Response body chunk + body_complete=true │
│ │
```
## Key Design Decisions
### Response-Embedded AppError (Not gRPC Status)
**Decision**: API errors are encoded in the response message's `error` field, not as gRPC status codes.
**Rationale**:
- Preserves full AppError semantics (id, where, detailed_error, status_code, params)
- gRPC status codes are reserved for transport-level failures only
- Matches existing Go plugin error handling patterns
**Example**:
```protobuf
message GetUserResponse {
AppError error = 1; // Application-level error
User user = 2; // Success response
}
```
### 64KB Streaming Chunks
**Decision**: HTTP request/response bodies are streamed in 64KB chunks.
**Rationale**:
- Matches gRPC best practices for streaming
- Avoids buffering entire request bodies in memory
- Enables early response (plugin can respond before request fully received)
**Constant**: `serveHTTPChunkSize = 64 * 1024`
### JSON Blobs for Complex Types
**Decision**: Some complex types (Config, License, Manifest) are serialized as JSON blobs.
**Rationale**:
- These types have deeply nested structures that would be verbose in proto
- Reduces proto definition maintenance burden
- Types are used infrequently, so serialization overhead is acceptable
**Example**:
```protobuf
message ConfigJson {
bytes config_json = 1; // JSON-serialized model.Config
}
```
### APIServerRegistrar Pattern
**Decision**: Use a function type to break import cycle between plugin and pluginapi/grpc/server.
**Rationale**:
- `plugin` package cannot import `pluginapi/grpc/server` (circular dependency)
- The registrar function is passed in at runtime
- Pattern: `type APIServerRegistrar func(grpcServer *grpc.Server, apiImpl API)`
**Usage** (in app layer):
```go
env.SetAPIServerRegistrar(func(s *grpc.Server, api plugin.API) {
server.Register(s, api)
})
```
### Venv-First Interpreter Discovery
**Decision**: Look for Python in plugin's venv before system PATH.
**Rationale**:
- Plugins can bundle their dependencies in a venv
- Avoids conflicts with system Python or other plugins
- Search order: `venv/bin/python``.venv/bin/python``python3``python`
### go-plugin Protocol
**Decision**: Use HashiCorp's go-plugin library for process management.
**Rationale**:
- Battle-tested subprocess management
- Health checking via grpc.health.v1
- Secure handshake protocol
- Automatic connection management
**Handshake Format**: `CORE-VERSION|APP-VERSION|NETWORK|ADDRESS|PROTOCOL`
- Example: `1|1|tcp|127.0.0.1:54321|grpc`
## File Reference
### Go Server Components
| Functionality | File Path |
|--------------|-----------|
| Plugin environment | `server/public/plugin/environment.go` |
| Python supervisor | `server/public/plugin/python_supervisor.go` |
| Hooks gRPC client | `server/public/plugin/hooks_grpc_client.go` |
| Hook ID constants | `server/public/plugin/hooks.go` |
| Supervisor base | `server/public/plugin/supervisor.go` |
| API Server | `server/public/pluginapi/grpc/server/api_server.go` |
| ServeHTTP handler | `server/public/pluginapi/grpc/server/serve_http.go` |
### Go API Implementation Files
| Functionality | File Path |
|--------------|-----------|
| User/Team APIs | `server/public/pluginapi/grpc/server/api_user_team.go` |
| Channel/Post APIs | `server/public/pluginapi/grpc/server/api_channel_post.go` |
| KV/Config APIs | `server/public/pluginapi/grpc/server/api_kv_config.go` |
| File/Bot APIs | `server/public/pluginapi/grpc/server/api_file_bot.go` |
| Remaining APIs | `server/public/pluginapi/grpc/server/api_remaining.go` |
### Proto Definitions
| Functionality | File Path |
|--------------|-----------|
| Main API service | `server/public/pluginapi/grpc/proto/api.proto` |
| Main hooks service | `server/public/pluginapi/grpc/proto/hooks.proto` |
| Shared types | `server/public/pluginapi/grpc/proto/types.proto` |
| User/Team messages | `server/public/pluginapi/grpc/proto/api_user_team.proto` |
| Channel/Post messages | `server/public/pluginapi/grpc/proto/api_channel_post.proto` |
| KV/Config messages | `server/public/pluginapi/grpc/proto/api_kv_config.proto` |
| File/Bot messages | `server/public/pluginapi/grpc/proto/api_file_bot.proto` |
| Remaining messages | `server/public/pluginapi/grpc/proto/api_remaining.proto` |
| Lifecycle hooks | `server/public/pluginapi/grpc/proto/hooks_lifecycle.proto` |
| Message hooks | `server/public/pluginapi/grpc/proto/hooks_message.proto` |
| User/Channel hooks | `server/public/pluginapi/grpc/proto/hooks_user_channel.proto` |
| Command hooks | `server/public/pluginapi/grpc/proto/hooks_command.proto` |
| HTTP hooks | `server/public/pluginapi/grpc/proto/hooks_http.proto` |
### Python SDK Components
| Functionality | File Path |
|--------------|-----------|
| Plugin base class | `python-sdk/src/mattermost_plugin/plugin.py` |
| Hook decorator | `python-sdk/src/mattermost_plugin/hooks.py` |
| API client | `python-sdk/src/mattermost_plugin/client.py` |
| gRPC server | `python-sdk/src/mattermost_plugin/server.py` |
| Runtime config | `python-sdk/src/mattermost_plugin/runtime_config.py` |
| Hook servicer | `python-sdk/src/mattermost_plugin/servicers/hooks_servicer.py` |
| Channel utilities | `python-sdk/src/mattermost_plugin/_internal/channel.py` |
| API mixins | `python-sdk/src/mattermost_plugin/_internal/mixins/*.py` |
| Exception types | `python-sdk/src/mattermost_plugin/exceptions.py` |
### Generated Code
| Language | Path |
|----------|------|
| Go | `server/public/pluginapi/grpc/generated/go/pluginapiv1/` |
| Python | `python-sdk/src/mattermost_plugin/grpc/` |
## Extending the System
### Adding a New API Method
1. Add RPC to appropriate `.proto` file
2. Run `make generate-grpc` to regenerate code
3. Implement method in Go API server (`server/api_*.go`)
4. Implement method in Python client mixin (`_internal/mixins/*.py`)
5. Add tests for both Go and Python implementations
### Adding a New Hook
1. Add RPC to `hooks.proto`
2. Add message types to appropriate `hooks_*.proto`
3. Run `make generate-grpc` to regenerate code
4. Implement dispatch in `hooks_grpc_client.go`
5. Add to `HookName` enum in Python (`hooks.py`)
6. Handle in `PluginHooksServicerImpl` (`servicers/hooks_servicer.py`)
7. Add tests
### Adding a New Model Type
1. Add message definition to `types.proto`
2. Add conversion functions in Go (`hooks_grpc_client.go` or `server/api_*.go`)
3. Add Python dataclass in `python-sdk/src/mattermost_plugin/models/`
4. Add proto-to-model conversion in Python
---
*This architecture documentation is intended for internal Mattermost engineers working on the Python plugin infrastructure.*