Restructure docs: move to docs/, rename CHANGES->CHANGELOG, add SECURITY.md
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
# Design Decisions
|
||||
|
||||
This file records architectural choices and the alternatives considered.
|
||||
New entries are added when a design decision is resolved (not when options
|
||||
are brainstormed). Unresolved questions live in DESIGN.md §10.
|
||||
|
||||
---
|
||||
|
||||
## Why Mattermost Plugin vs Bot Account
|
||||
|
||||
**Context:** AI assistants in Mattermost can be implemented as plugins
|
||||
(running inside the Mattermost server process) or as bot accounts
|
||||
(independent services using the REST API and WebSocket).
|
||||
|
||||
**Alternatives considered:**
|
||||
1. **Plugin** — hooks into slash commands, message hooks, KV store, System
|
||||
Console config UI, and the plugin lifecycle. No separate process to
|
||||
manage. Deployment is a single `.tar.gz` upload.
|
||||
2. **Bot account** — separate service, more language freedom, but requires
|
||||
managing OAuth tokens, WebSocket reconnection, a separate process with
|
||||
its own lifecycle, and the bot account itself in Mattermost.
|
||||
|
||||
**Outcome:** Plugin chosen for v0.1.0 because:
|
||||
- Single deployment artifact
|
||||
- Built-in config UI via System Console
|
||||
- No bot account to create and maintain
|
||||
- KV store for persistence without a database
|
||||
- Simpler for the target audience (self-hosted teams)
|
||||
|
||||
**Trade-off:** Plugin runs inside the Mattermost process — bugs can affect
|
||||
server stability. Mitigated by standard isolation practices (panic recovery,
|
||||
goroutine lifecycle management).
|
||||
|
||||
---
|
||||
|
||||
## Why Ollama
|
||||
|
||||
**Context:** Backend LLM provider for the plugin.
|
||||
|
||||
**Alternatives considered:**
|
||||
1. **Ollama** — Single binary, simple REST API, streaming, model management,
|
||||
self-hosted (data never leaves the infrastructure). Community Edition
|
||||
friendly.
|
||||
2. **OpenAI / Anthropic API** — SaaS, API key required, data leaves the
|
||||
network, ongoing API costs. Not aligned with self-hosted Mattermost
|
||||
ethos.
|
||||
3. **Local inference (llama.cpp, etc.)** — More complex to set up and
|
||||
manage. Ollama wraps this with a clean API.
|
||||
|
||||
**Outcome:** Ollama. It matches the self-hosted, no-data-leaves paradigm of
|
||||
Mattermost Community Edition and provides the simplest API surface for a
|
||||
plugin to consume.
|
||||
|
||||
---
|
||||
|
||||
## Why Per-Channel Per-User Context (Proposed)
|
||||
|
||||
**Context:** When a user sends `/ai <prompt>`, should the plugin remember
|
||||
previous exchanges?
|
||||
|
||||
**Alternatives considered:**
|
||||
1. **Per-user global** — Context follows the user across channels. Simple
|
||||
but confusing: context from a #general question leaks into #dev.
|
||||
2. **Per-channel per-user** — Each user gets separate context in each
|
||||
channel. Natural mapping: different channels are different topics.
|
||||
3. **No context** — Stateless. Simplest but every prompt is isolated;
|
||||
users can't have a conversation.
|
||||
|
||||
**Outcome (proposed):** Per-channel per-user. Marked as D2 in DESIGN.md —
|
||||
not final until v0.1.0 implementation.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Why @-mention + Engagement Lifecycle over Slash-Command-Only
|
||||
|
||||
**Context:** How does the user interact with the AI agent? Slash commands
|
||||
(`/ai`) are explicit but unnatural — users must learn a command syntax.
|
||||
Bot @-mentions match how users already talk to each other in Mattermost.
|
||||
|
||||
**Alternatives considered:**
|
||||
1. **Slash command only** (`/ai <prompt>`) — Explicit, discoverable via
|
||||
`/ai help`. But every interaction requires the prefix, and thread
|
||||
continuation is ambiguous.
|
||||
2. **@-mention only** (`@mattermore <prompt>`) — Natural, familiar UX.
|
||||
Users already @-mention teammates. Thread replies continue the
|
||||
conversation without re-mentioning. Risk of accidental triggers.
|
||||
3. **Channel whisper (reply to everything)** — Simplest for the user but
|
||||
noisy and invasive. The user explicitly rejected this.
|
||||
4. **Hybrid: @-mention primary + /ai fallback** — Both work identically.
|
||||
|
||||
**Outcome:** Hybrid (#4) chosen because:
|
||||
- @-mention is the primary, most natural interface
|
||||
- `/ai` is available for users who prefer it or when @-mention is
|
||||
inconvenient (e.g. automated scripts, mobile)
|
||||
- Both paths go through the same `EngagementEngine` — identical behaviour
|
||||
- Thread replies auto-continue without re-mentioning (D6 resolved — core
|
||||
feature, not deferred)
|
||||
|
||||
**Trade-off:** @-mention requires a bot account (`API.CreateBot()`). This
|
||||
adds a one-time setup step and a DB row, but the plugin handles creation
|
||||
and deactivation automatically. The bot account is not a separate service —
|
||||
it's an identity within the plugin process, so there's no extra deployment
|
||||
artifact or OAuth management.
|
||||
|
||||
## Why Thread Posts Instead of Ephemeral Messages
|
||||
|
||||
**Context:** Where does the bot's response appear?
|
||||
|
||||
**Alternatives considered:**
|
||||
1. **Ephemeral post** — Only the requesting user sees it. Clean but
|
||||
invisible to the rest of the channel/reviewers.
|
||||
2. **Channel-wide post** — Visible to everyone. Can be noisy if the bot
|
||||
posts long responses.
|
||||
3. **Thread post** — Visible to everyone but collapsed into a thread.
|
||||
Channel stays clean, conversation history is easy to follow. Thread
|
||||
replies naturally continue the conversation.
|
||||
|
||||
**Outcome:** Thread post (#3) chosen because:
|
||||
- Threads keep the channel tidy
|
||||
- Community can see and follow AI interactions
|
||||
- Thread replies map 1:1 to conversation continuation
|
||||
- Users can collapse/hide threads they don't care about
|
||||
|
||||
**Trade-off:** Threads require the user to click into them to see the full
|
||||
response on some clients. Mitigated by showing a preview snippet in the
|
||||
channel.
|
||||
|
||||
## Why No Webapp UI
|
||||
|
||||
**Context:** Should the plugin ship a webapp component (channel header
|
||||
button, right-hand side panel, custom settings page)?
|
||||
|
||||
**Alternatives considered:**
|
||||
1. **No webapp** — Pure server-side plugin. @-mention + `/ai` + thread
|
||||
replies covers everything. Simplest to build and maintain.
|
||||
2. **Channel header button** — Button to open an AI chat panel. Adds
|
||||
discoverability but duplicates what @-mention already does.
|
||||
3. **Custom settings page** — Duplicates System Console settings in the
|
||||
main UI. Adds complexity for marginal benefit.
|
||||
|
||||
**Outcome:** No webapp (#1) chosen because:
|
||||
- @-mention is already discoverable (Mattermost autocompletes)
|
||||
- Thread replies provide a natural conversation UI
|
||||
- No frontend to build, bundle, or version
|
||||
- All configuration stays in System Console where admins expect it
|
||||
|
||||
**Trade-off:** Users must know the bot exists to @-mention it. Mitigated by
|
||||
mentioning it in channel header, onboarding posts, or `/ai help`.
|
||||
|
||||
---
|
||||
|
||||
*This file follows the template from the `project-docs` skill.*
|
||||
+571
@@ -0,0 +1,571 @@
|
||||
# Design: Matty — forkless' annoying Mattermost sidekick
|
||||
|
||||
> **Status:** v0.1.16 — Implemented and deployed
|
||||
> **Last updated:** 2026-06-16
|
||||
> **Bot username:** @matty
|
||||
> **Default model:** dolphin-llama3:latest (configurable in System Console)
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose & Scope
|
||||
|
||||
### 1.1 What It Is
|
||||
|
||||
Matty is a Mattermost (Community Edition) plugin that lets users chat
|
||||
with remote LLMs through an Ollama backend. Users address the bot by
|
||||
**@-mentioning** it in any channel (e.g. `@matty write a poem`). The
|
||||
bot responds **only when addressed** and disengages naturally — it never
|
||||
reads every message in the room. Once addressed, the user enters an
|
||||
**active session** where follow-up messages continue the conversation
|
||||
without needing to re-@mention. Sessions end on stop words ("thanks",
|
||||
"bye"), explicit `@matty stop`, or 5 minutes of inactivity.
|
||||
|
||||
A slash command (`/ai`) remains available as an alternative entry point.
|
||||
|
||||
### 1.2 What It Is Not (Explicit Out-of-Scope)
|
||||
|
||||
| Out of scope | Rationale |
|
||||
|---|---|
|
||||
| Hosting its own LLM inference | Ollama is the sole backend; the plugin is a thin proxy |
|
||||
| Multi-backend abstraction (OpenAI, Anthropic, etc.) | If needed later, abstract behind an `LLMProvider` interface — not now |
|
||||
| Training / fine-tuning | Ollama handles that |
|
||||
| **Channel whisper mode** (replying to every message) | The agent only activates on @-mention or active session. It never reads unprompted messages. |
|
||||
| Bot account management | Plugin creates the bot account automatically via `API.CreateBot()` |
|
||||
| Webapp UI | No frontend — @-mention + `/ai` covers all interaction |
|
||||
|
||||
### 1.3 Target Audience
|
||||
|
||||
- Teams self-hosting Mattermost who want AI assistant access without data
|
||||
leaving their infrastructure
|
||||
- Users comfortable with @-mentioning a bot the same way they @-mention a
|
||||
human teammate
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture Overview
|
||||
|
||||
### 2.1 Component Diagram (text)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Mattermost Server (Community Edition) │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ Matty Plugin │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────────┐ ┌────────────────────────┐ │ │
|
||||
│ │ │ MessageHasBeen │──▶│ Plugin (Go) │ │ │
|
||||
│ │ │ Posted (primary) │ │ │ │ │
|
||||
│ │ └──────────────────┘ │ ┌──────────────────┐ │ │ │
|
||||
│ │ ┌──────────────────┐ │ │ OllamaClient │──┼───┼───┼──▶ Ollama
|
||||
│ │ │ SlashCommand │──▶│ └──────────────────┘ │ │ │ (remote)
|
||||
│ │ │ /ai (alt.) │ │ ┌──────────────────┐ │ │ │
|
||||
│ │ └──────────────────┘ │ │ ConversationStore│ │ │ │
|
||||
│ │ ┌──────────────────┐ │ └──────────────────┘ │ │ │
|
||||
│ │ │ EngagementEngine │ │ ┌──────────────────┐ │ │ │
|
||||
│ │ │ sleep / active │──▶│ │ RateLimiter │ │ │ │
|
||||
│ │ └──────────────────┘ │ └──────────────────┘ │ │ │
|
||||
│ │ ┌──────────────────┐ │ ┌──────────────────┐ │ │ │
|
||||
│ │ │ PersonaLibrary │ │ │ Config (KV) │ │ │ │
|
||||
│ │ │ resolvePersona() │ │ └──────────────────┘ │ │ │
|
||||
│ │ └──────────────────┘ │ │ │ │
|
||||
│ │ ┌──────────────────┐ │ │ │ │
|
||||
│ │ │ Bot Account │ │ │ │ │
|
||||
│ │ │ (auto-created) │ │ │ │ │
|
||||
│ │ └──────────────────┘ │ │ │ │
|
||||
│ │ ┌──────────────────┐ │ │ │ │
|
||||
│ │ │ Webapp (none) │ │ │ │ │
|
||||
│ │ └──────────────────┘ │ │ │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ KV Store (per-plugin) │ │
|
||||
│ │ • ctx_{user}_{channel} — conversation history │ │
|
||||
│ │ • persona_{user}_{ch} — persona override │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Request Flow (happy path — @-mention)
|
||||
|
||||
```
|
||||
User types "@matty write a release note for v2.1"
|
||||
│
|
||||
▼
|
||||
MessageHasBeenPosted fires
|
||||
│
|
||||
▼
|
||||
EngagementEngine.Wake(userId, channelId) → mark session Active
|
||||
│
|
||||
▼
|
||||
@-mention detected → strip "@matty " prefix → "write a release note for v2.1"
|
||||
│
|
||||
▼
|
||||
RateLimiter.Allow(userId) → 200
|
||||
│
|
||||
▼
|
||||
Check stop words → no match
|
||||
│
|
||||
▼
|
||||
Check persona override (KV store) → resolvePersona() → full description
|
||||
│
|
||||
▼
|
||||
ConversationStore.BuildMessages(userId, channelId, prompt) → up to 10 turns
|
||||
│
|
||||
▼
|
||||
Trim context based on Brevity (e.g. brevity=1 → keep only current message)
|
||||
│
|
||||
▼
|
||||
Prepend system prompt: "You are {persona}. Respond in character. ... Reply in {N} sentences."
|
||||
│
|
||||
▼
|
||||
Calculate token cap: maxTokens = auto from brevity, or manual override
|
||||
│
|
||||
▼
|
||||
OllamaClient.ChatCompletionStream(model, messages, options)
|
||||
│
|
||||
▼
|
||||
Ollama returns ndjson stream → accumulate all tokens
|
||||
│
|
||||
▼
|
||||
Create single post with full response (not a thread reply, not live-updated)
|
||||
```
|
||||
|
||||
### 2.3 Engagement Session Flow (follow-up without @-mention)
|
||||
|
||||
```
|
||||
User: "@matty hello" → bot responds, session starts
|
||||
User: "tell me a joke" (no @) → IsActive() → true → bot responds
|
||||
User: "what about weather?" (no @) → IsActive() → true → bot responds
|
||||
User: "thanks" → stopWordMatch → Sleep() → bot signs off
|
||||
User: "hello again" (no @, 10 min later)→ IsActive() → false → ignored
|
||||
User: "@matty hello again" → Wake() → bot responds, new session
|
||||
```
|
||||
|
||||
### 2.4 Error Flow
|
||||
|
||||
| Failure point | Behaviour |
|
||||
|---|---|
|
||||
| Ollama unreachable | Post: "Matty is asleep. @matty when you need me." |
|
||||
| Rate limit hit | Post: "Rate limit exceeded. Please wait before sending another request." |
|
||||
| Invalid persona | Ephemeral: "Unknown persona. Available: ..." |
|
||||
| Timeout (>30 s) | Post truncated with "(Matty is asleep...)" |
|
||||
|
||||
---
|
||||
|
||||
## 3. Plugin Manifest & Identity
|
||||
|
||||
### 3.1 `plugin.json`
|
||||
|
||||
Current settings (v0.1.16):
|
||||
|
||||
| Key | Type | Default | Purpose |
|
||||
|---|---|---|---|
|
||||
| `OllamaURL` | text | `http://localhost:11434` | Ollama server endpoint |
|
||||
| `DefaultModel` | dropdown | `dolphin-llama3:latest` | Model for chat |
|
||||
| `Persona` | dropdown | `assistant` | Character for the bot |
|
||||
| `RepeatPenalty` | number | `11` (1.1) | Penalise repeated tokens |
|
||||
| `FrequencyPenalty` | number | `1` (0.1) | Penalise frequent phrases |
|
||||
| `Brevity` | dropdown | `3` | Max sentences |
|
||||
| `DebugLogging` | bool | `false` | Enable verbose logging |
|
||||
| `CustomPersona` | text | `""` | Override dropdown persona |
|
||||
| `MaxTokens` | number | `0` | Manual token cap (0 = auto from Brevity) |
|
||||
| `RateLimitPerMinute` | number | `0` | Requests/min/user (0 = unlimited) |
|
||||
| `AllowedUserIDs` | text | `""` | Restrict to specific users |
|
||||
| `StopWords` | text | `thanks, bye, goodbye, done, stop, quit, end` | Disengagement triggers |
|
||||
|
||||
### 3.2 Versioning
|
||||
|
||||
Follow `release-workflow` skill: `v0.1.x` for small iterations. Bump PATCH for
|
||||
bug fixes, MINOR for features. No MAJOR bump until stable release.
|
||||
|
||||
---
|
||||
|
||||
## 4. Server-Side Components
|
||||
|
||||
### 4.1 Plugin Lifecycle Hooks
|
||||
|
||||
| Hook | Purpose |
|
||||
|---|---|
|
||||
| `OnActivate()` | Register `/ai` command, create bot account via `ensureBot()`, initialise `OllamaClient`, `ConversationStore`, `RateLimiter`, `EngagementEngine` |
|
||||
| `OnConfigurationChange()` | Re-read System Console settings, validate OllamaURL, sync debug logging flag to OllamaClient |
|
||||
| `MessageHasBeenPosted()` | **Primary entry point** — detect @-mentions and active sessions, delegate to EngagementEngine |
|
||||
| `ExecuteCommand()` | **Alternative entry point** — intercept `/ai` and subcommands (`/ai persona`, `/ai new`, `/ai help`) |
|
||||
| `Implemented()` | Declares handled hooks: `OnActivate`, `OnDeactivate`, `OnConfigurationChange`, `ExecuteCommand`, `MessageHasBeenPosted` |
|
||||
|
||||
### 4.2 Interaction Model
|
||||
|
||||
#### 4.2.1 @-mention (primary)
|
||||
|
||||
User types `@matty <prompt>` in any channel. The plugin:
|
||||
|
||||
1. Detects the @-mention via `MessageHasBeenPosted` → string match on `@matty`
|
||||
2. Strips the bot username from the prompt
|
||||
3. Checks stop words, rate limit, authorization
|
||||
4. Looks up persona override from KV store (set via `/ai persona <name>`)
|
||||
5. Builds conversation context from `ConversationStore` (up to 10 turns)
|
||||
6. Trims context based on Brevity setting (e.g. brevity 1 = only current message)
|
||||
7. Prepends persona system prompt with general instructions + brevity
|
||||
8. Calculates token cap (auto from Brevity or manual MaxTokens override)
|
||||
9. Wakes the `EngagementEngine` for this user+channel
|
||||
10. Sends to Ollama via `ChatCompletionStream`
|
||||
11. Accumulates all tokens, creates a single channel post with the full response
|
||||
|
||||
**When the bot does NOT respond:**
|
||||
- Messages without `@matty` while the EngagementEngine is Sleeping
|
||||
- Messages during the Disengaging → Sleeping transition
|
||||
- Messages from other bot accounts (infinite-loop guard)
|
||||
|
||||
#### 4.2.2 Engagement Session (continuation)
|
||||
|
||||
Once a user is in an Active session (via @-mention or `/ai`), their subsequent
|
||||
non-@-mention messages in the same channel are treated as continuations:
|
||||
|
||||
```
|
||||
User enters Active → subsequent messages auto-continue → stop word → Sleep
|
||||
```
|
||||
|
||||
The session expires after 5 minutes of inactivity.
|
||||
|
||||
#### 4.2.3 Slash command (alternative)
|
||||
|
||||
`/ai <prompt>` works as a fallback. Behaviour is identical.
|
||||
|
||||
**Full syntax:**
|
||||
|
||||
```
|
||||
/ai <prompt> — Chat with default model
|
||||
/ai persona — Show current active persona
|
||||
/ai persona list — List all available personas with descriptions
|
||||
/ai persona <name> — Switch persona (persisted)
|
||||
/ai new — Reset conversation context + end session
|
||||
/ai help — Show help
|
||||
```
|
||||
|
||||
### 4.3 OllamaClient
|
||||
|
||||
Located in `server/ollama.go` (package main, flat file).
|
||||
|
||||
Key methods:
|
||||
|
||||
| Method | Purpose |
|
||||
|---|---|
|
||||
| `ChatCompletion(req)` | Non-streaming call (not used by main flow) |
|
||||
| `ChatCompletionStream(req)` | **Primary** — returns `< -chan StreamEvent`, reads ndjson |
|
||||
| `ListModels()` | Returns available model names |
|
||||
| `Ping()` | Health check |
|
||||
|
||||
The client accepts a `logFunc` callback for debug logging (hooked to
|
||||
`p.API.LogInfo` when `DebugLogging` is enabled).
|
||||
|
||||
### 4.4 ConversationStore
|
||||
|
||||
Stored in KV store per `ctx_{userID}_{channelID}`. Contains the last 10
|
||||
messages (5 user + 5 assistant). Context is trimmed further based on
|
||||
Brevity setting before sending to Ollama.
|
||||
|
||||
| Brevity | Messages kept |
|
||||
|---|---|
|
||||
| 1 | 1 (current only) |
|
||||
| 2 | 2 (last exchange) |
|
||||
| 3 | 4 (last 2 exchanges) |
|
||||
| 5 | 6 |
|
||||
| 10 | 10 |
|
||||
|
||||
### 4.5 RateLimiter
|
||||
|
||||
Token-bucket per user. Configurable from System Console.
|
||||
Default: 0 (unlimited). Resets on config change. Stored in memory.
|
||||
|
||||
### 4.6 EngagementEngine
|
||||
|
||||
Tracks active conversations per user+channel. Two states:
|
||||
|
||||
| State | Behaviour | Entered when |
|
||||
|---|---|---|
|
||||
| **Sleeping** | Ignores all messages | Plugin start, stop word, `/ai new`, 5 min timeout |
|
||||
| **Active** | Responds to @-mentions and follow-ups | @-mention or `/ai` |
|
||||
|
||||
The timeout is 5 minutes of inactivity. On expiry, the session is silently
|
||||
removed — next @-mention starts fresh.
|
||||
|
||||
### 4.7 Persona System
|
||||
|
||||
Located in `server/persona.go`. A library of 19 personas mapped by stable
|
||||
short IDs:
|
||||
|
||||
```
|
||||
assistant a helpful assistant. Be concise, 2-3 sentences max...
|
||||
canadian an overly polite Canadian who apologises excessively...
|
||||
carlin George Carlin. Acerbic, observational, profane comedy...
|
||||
dnd a Dungeons & Dragons dungeon master...
|
||||
infomercial a 1980s TV infomercial host. Over-enthusiastic, loud...
|
||||
jester a medieval court jester who mocks everyone...
|
||||
mime a French mime. Respond with dramatic pauses...
|
||||
motivational a motivational speaker who turns everything into a life lesson...
|
||||
noir a noir detective who answers every question with another...
|
||||
philosopher a philosopher who overcomplicates everything...
|
||||
pirate a pirate captain. Use pirate slang...
|
||||
pirro a drunk Jeanine Pirro. Slur your words...
|
||||
ramsay Gordon Ramsay but for code reviews. Aggressive chef...
|
||||
scammer a tech support scammer from 2008. Alarmist, pushy...
|
||||
shakespeare William Shakespeare himself. Respond in iambic pentameter...
|
||||
surfer a surfer dude explaining complex topics badly...
|
||||
sysadmin a grumpy sysadmin who thinks every question is stupid...
|
||||
tiredparent a tired parent who's given up on life...
|
||||
trump Donald Trump. Boastful, hyperbolic, third-person...
|
||||
```
|
||||
|
||||
Priority: **KV override** (`/ai persona <id>`) → **CustomPersona** (System Console text field) → **Persona** dropdown.
|
||||
|
||||
`resolvePersona(id)` looks up the short ID in the library. Returns empty
|
||||
for unknown IDs (so `/ai persona shakespear` is properly rejected).
|
||||
|
||||
### 4.8 Debug Logging
|
||||
|
||||
When `DebugLogging` is enabled in System Console, the plugin logs:
|
||||
|
||||
| Log message | Source | What it contains |
|
||||
|---|---|---|
|
||||
| `"Config loaded"` | `configuration.go` | All settings on save |
|
||||
| `"Chat request"` | `hooks.go` | 13 fields: model, url, persona, brevity, tokens, repeat, freq, rate limit, allowed users, stop words, full system prompt |
|
||||
| `"Ollama API call"` | `ollama.go` | Full request body sent to Ollama (model, messages, options) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Ollama Integration
|
||||
|
||||
### 5.1 API Mapping
|
||||
|
||||
| Plugin Action | Ollama Endpoint | Method | Stream |
|
||||
|---|---|---|---|
|
||||
| Chat completion | `/api/chat` | POST | Yes |
|
||||
| List models | `/api/tags` | GET | No |
|
||||
| Health check | `/api/tags` | GET | No |
|
||||
|
||||
### 5.2 Streaming Strategy
|
||||
|
||||
1. Send POST to `/api/chat` with `"stream": true`.
|
||||
2. Read ndjson response body line by line.
|
||||
3. Accumulate all tokens in a buffer (no live post updates — avoids "Edited" spam).
|
||||
4. On `done:true`, create a single channel post with the full content.
|
||||
|
||||
### 5.3 Token Auto-Calculation
|
||||
|
||||
Brevity auto-calculates the token cap. Formula: `100 + brevity × 75`.
|
||||
|
||||
| Brevity | Auto tokens | Override with |
|
||||
|---|---|---|
|
||||
| 1 | 175 | MaxTokens > 175 |
|
||||
| 2 | 250 | MaxTokens > 250 |
|
||||
| 3 | 325 | MaxTokens > 325 |
|
||||
| 5 | 475 | MaxTokens > 475 |
|
||||
| 10 | 850 | MaxTokens > 850 |
|
||||
| Unlimited (0) | Falls back to MaxTokens (0 = Ollama default) | MaxTokens > 0 |
|
||||
|
||||
### 5.4 Penalties
|
||||
|
||||
RepeatPenalty and FrequencyPenalty use whole numbers divided by 10:
|
||||
|
||||
| UI value | Sent to Ollama |
|
||||
|---|---|
|
||||
| 10 | 1.0 (none) |
|
||||
| 11 | 1.1 (mild) |
|
||||
| 12 | 1.2 (strong) |
|
||||
|
||||
| UI value | Sent to Ollama |
|
||||
|---|---|
|
||||
| 0 | 0.0 (none) |
|
||||
| 1 | 0.1 (mild) |
|
||||
| 5 | 0.5 (strong) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Configuration
|
||||
|
||||
All configuration is done via **System Console → Plugins → Matty**.
|
||||
The 12 settings are described in §3.1.
|
||||
|
||||
`OnConfigurationChange()` reloads all settings when saved. Plugins do not
|
||||
have a `SavePluginConfiguration` API in Mattermost v11.x, so `/ai persona`
|
||||
uses the KV store to persist overrides instead.
|
||||
|
||||
---
|
||||
|
||||
## 7. Data Flow — Detailed
|
||||
|
||||
### 7.1 MessageHasBeenPosted (full lifecycle)
|
||||
|
||||
```
|
||||
Step User / System Component
|
||||
──── ────────────────────────────── ──────────────────
|
||||
1 User types "@matty explain TCP"
|
||||
2 MessageHasBeenPosted fires
|
||||
3 Check: not bot's own post Loop guard
|
||||
4 Check: not from OAuth bot Loop guard
|
||||
5 Check: has @matty? yes Mention detection
|
||||
6 Check: EngagementEngine.IsActive? EngagementEngine
|
||||
7 Strip @matty → "explain TCP"
|
||||
8 Check stop words StopWordSet
|
||||
9 Check rate limit RateLimiter
|
||||
10 Check authorization AllowedUserIDs
|
||||
11 EngagementEngine.Wake() EngagementEngine
|
||||
12 GetPersonaOverride → resolvePersona KV Store / personaLibrary
|
||||
13 BuildMessages → conversation history ConversationStore
|
||||
14 Trim context by Brevity hooks.go
|
||||
15 Build system prompt + brevity hooks.go
|
||||
16 Calculate token cap hooks.go
|
||||
17 Build Ollama options (penalties) hooks.go
|
||||
18 Log Chat request (if debug) hooks.go
|
||||
19 ChatCompletionStream() OllamaClient
|
||||
20 Accumulate tokens hooks.go
|
||||
21 CreatePost with full response postReply()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Security Model
|
||||
|
||||
### 8.1 Input Sanitization
|
||||
|
||||
- Stop words checked before sending to Ollama (saves round-trip)
|
||||
- Rate limiting per user (configurable, default: unlimited)
|
||||
|
||||
### 8.2 Network Security
|
||||
|
||||
- `OllamaURL` validated as http/https at config load
|
||||
- TLS enabled by default
|
||||
|
||||
### 8.3 Authorization
|
||||
|
||||
- `AllowedUserIDs` restricts by Mattermost user ID
|
||||
- Empty = all authenticated users
|
||||
- Bot account created by plugin, managed automatically
|
||||
|
||||
### 8.4 Infinite-Loop Guard
|
||||
|
||||
The plugin never processes messages from:
|
||||
- Its own bot account (`post.UserId == botUserID`)
|
||||
- Other OAuth bots (`post.IsFromOAuthBot()`)
|
||||
|
||||
---
|
||||
|
||||
## 9. Observability
|
||||
|
||||
### 9.1 Logging
|
||||
|
||||
When Debug Logging is enabled:
|
||||
|
||||
| Level | When |
|
||||
|---|---|
|
||||
| `INFO` | Chat request (all 13 settings), Config loaded, Ollama API call body |
|
||||
| `ERROR` | Ollama unreachable, config validation failure, KV store error |
|
||||
| `WARN` | Rate limit hit (with debug on) |
|
||||
|
||||
### 9.2 Error Reporting
|
||||
|
||||
- Ollama unreachable → "Matty is asleep. @matty when you need me."
|
||||
- Rate limit → "Rate limit exceeded..."
|
||||
- Unknown persona → "Unknown persona. Available: ..."
|
||||
- Stream error → appended as "(Matty is asleep...)" in the response
|
||||
|
||||
---
|
||||
|
||||
## 10. Resolved & Open Design Decisions
|
||||
|
||||
### Resolved
|
||||
|
||||
| # | Question | Decision |
|
||||
|---|---|---|
|
||||
| D1 | Reply style | **Direct channel posts** (not ephemeral, not threads) |
|
||||
| D4 | Webapp UI | **None** — @-mention + `/ai` + `/ai persona` covers everything |
|
||||
| D6 | Thread replies | **EngagementEngine** — active sessions track follow-ups without threads |
|
||||
| D11 | Engagement scope | **Per-user+channel** — separate sessions per channel |
|
||||
| D12 | Thread timeout | **5 minutes** (not configurable yet) |
|
||||
|
||||
### Still Open
|
||||
|
||||
| # | Question | Proposed |
|
||||
|---|---|---|
|
||||
| D2 | Context scope | Per-channel per-user (implemented) |
|
||||
| D3 | Context turns | Currently 10 messages; Brevity trims further |
|
||||
| D5 | Model governance | All models available via System Console dropdown |
|
||||
| D7 | `num_ctx` per request | Not implemented; Brevity controls token cap |
|
||||
| D8 | Auto-pull models | No (returns error with list) |
|
||||
| D9 | Prompt injection guard | Deferred — document risk |
|
||||
| D10 | Metrics endpoint | Deferred |
|
||||
|
||||
---
|
||||
|
||||
## 11. Deployment & Lifecycle
|
||||
|
||||
### 11.1 Build
|
||||
|
||||
```bash
|
||||
make dist
|
||||
```
|
||||
|
||||
Produces `dist/com.forkless.mattermore-{version}.tar.gz` containing the plugin
|
||||
manifest and platform binaries.
|
||||
|
||||
### 11.2 Upload
|
||||
|
||||
Via Mattermost System Console → Plugins → Upload Plugin, or via API.
|
||||
|
||||
### 11.3 Upgrade
|
||||
|
||||
Upload new `.tar.gz` — Mattermost replaces the plugin binary.
|
||||
Settings are preserved via KV store. Bot account is re-fetched (not re-created).
|
||||
|
||||
### 11.4 Local Testing
|
||||
|
||||
```bash
|
||||
docker compose -f dev/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
Spins up Mattermost 11.6.1 + PostgreSQL 16 at http://localhost:8065.
|
||||
Auto-creates admin user (admin/password).
|
||||
Test rig is disposable: `docker compose down -v` wipes everything.
|
||||
|
||||
---
|
||||
|
||||
## 12. File Structure
|
||||
|
||||
```
|
||||
mattermore/
|
||||
├── plugin.json — Plugin manifest
|
||||
├── Makefile — Build, dist, clean
|
||||
├── .golangci.yml — Linter config
|
||||
├── DESIGN.md — This document
|
||||
├── PROJECT.md
|
||||
├── CHANGES.md
|
||||
├── DECISIONS.md
|
||||
├── README.md
|
||||
├── dev/
|
||||
│ ├── docker-compose.yml — Local Mattermost + PostgreSQL
|
||||
│ └── init.sh — Admin auto-creation script
|
||||
├── server/
|
||||
│ ├── main.go — plugin.ClientMain
|
||||
│ ├── plugin.go — Plugin struct
|
||||
│ ├── hooks.go — OnActivate, MessageHasBeenPosted, streaming
|
||||
│ ├── command.go — /ai and subcommands, persona override
|
||||
│ ├── configuration.go — Config struct, validation, OnConfigurationChange
|
||||
│ ├── ollama.go — OllamaClient (streaming + blocking)
|
||||
│ ├── store.go — ConversationStore (KV-backed)
|
||||
│ ├── rate.go — RateLimiter (token bucket)
|
||||
│ ├── engagement.go — EngagementEngine (sleep/active)
|
||||
│ └── persona.go — PersonaLibrary with 19 personas
|
||||
└── dist/
|
||||
└── com.forkless.mattermore-*.tar.gz
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Future Considerations (v0.2+)
|
||||
|
||||
- **Multi-modal inputs** — Ollama supports images via `/api/chat` with
|
||||
`images` field.
|
||||
- **Tool calling** — Ollama supports tool definitions for external APIs.
|
||||
- **Conversation export** — Allow users to export AI threads as markdown.
|
||||
- **Streaming into message attachments** — Render structured data (tables,
|
||||
JSON, code blocks) with better formatting.
|
||||
@@ -0,0 +1,77 @@
|
||||
# Project: Matty
|
||||
|
||||
## Purpose
|
||||
|
||||
Matty is a Mattermost (Community Edition) plugin that connects chat to
|
||||
remote LLMs via Ollama. Users address the bot by **@-mentioning** it in any
|
||||
channel (e.g. `@matty write a poem`). The bot responds only when addressed
|
||||
and disengages naturally. Follow-up messages continue the conversation
|
||||
without re-@mentioning while the session is active.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
mattermore/
|
||||
├── plugin.json — Mattermost plugin manifest (v0.1.x)
|
||||
├── Makefile — Build, lint, dist targets
|
||||
├── .golangci.yml — Linter configuration
|
||||
├── CHANGELOG.md — Per-release changelog
|
||||
├── README.md — User-facing docs
|
||||
├── DESIGN.md — Functional design document
|
||||
├── DECISIONS.md — Design rationale log
|
||||
├── PROJECT.md — This file
|
||||
├── .gitignore — Go + plugin ignores
|
||||
├── dev/
|
||||
│ ├── docker-compose.yml — Local Mattermost + PostgreSQL test rig
|
||||
│ └── init.sh — Admin auto-creation script
|
||||
├── server/
|
||||
│ ├── main.go — plugin.ClientMain entry point
|
||||
│ ├── plugin.go — Plugin struct with all fields
|
||||
│ ├── hooks.go — OnActivate, MessageHasBeenPosted, streaming
|
||||
│ ├── command.go — /ai and subcommands, persona override
|
||||
│ ├── configuration.go — Config struct, validation, OnConfigurationChange
|
||||
│ ├── ollama.go — OllamaClient (streaming + blocking chat)
|
||||
│ ├── store.go — ConversationStore (KV-backed context)
|
||||
│ ├── rate.go — RateLimiter (per-user token bucket)
|
||||
│ ├── engagement.go — EngagementEngine (sleep/active states)
|
||||
│ └── persona.go — PersonaLibrary with 19 personas
|
||||
└── dist/
|
||||
└── com.forkless.mattermore-*.tar.gz
|
||||
```
|
||||
|
||||
## Key Files
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `plugin.json` | Manifest with 12 configurable settings |
|
||||
| `server/hooks.go` | Core interaction logic (@-mention detection, streaming) |
|
||||
| `server/command.go` | `/ai` and `/ai persona` commands |
|
||||
| `server/ollama.go` | HTTP client for Ollama REST API |
|
||||
| `server/store.go` | Context persistence via Mattermost KV store |
|
||||
| `server/engagement.go` | Active session tracking (5-min timeout) |
|
||||
| `server/persona.go` | 19 personas with stable short IDs |
|
||||
| `server/configuration.go` | 12 settings, validation, debug logging |
|
||||
| `DESIGN.md` | Full functional design (up to date with v0.1.16) |
|
||||
| `CHANGELOG.md` | All versions from v0.1.0 to v0.1.16 |
|
||||
| `dev/docker-compose.yml` | Disposable Mattermost + PostgreSQL test rig |
|
||||
|
||||
## External Dependencies
|
||||
|
||||
- **Go ≥ 1.21** (toolchain)
|
||||
- **Mattermost Server ≥ v11.6** (plugin API)
|
||||
- **Ollama** (remote inference server, any version)
|
||||
- Go modules (see `go.mod`):
|
||||
- `github.com/mattermost/mattermost/server/public` — Plugin SDK
|
||||
|
||||
## Release Cadence
|
||||
|
||||
- Tags: `v0.x.y`
|
||||
- Built manually via `make dist`, uploaded via System Console
|
||||
- No CI/CD pipeline yet
|
||||
|
||||
## Documentation
|
||||
|
||||
- `docs/DESIGN.md` — full functional design with architecture, interaction model
|
||||
- `docs/DECISIONS.md` — why key decisions were made
|
||||
- `CHANGELOG.md` — per-version changelog (17 releases)
|
||||
- `README.md` — quick start, config reference, build instructions
|
||||
@@ -0,0 +1,50 @@
|
||||
# Security — Matty AI Chat Agent
|
||||
|
||||
## Ollama Network Exposure
|
||||
|
||||
Matty connects to an Ollama server to run LLM inference. **Ollama does not
|
||||
natively support authentication, TLS, or access control.** If exposed to the
|
||||
public internet, anyone who knows your Ollama URL can:
|
||||
|
||||
- Run inference on your models (costly and potentially abusive)
|
||||
- Access any model you have pulled
|
||||
- Potentially execute arbitrary code through model exploits
|
||||
|
||||
### Recommendations
|
||||
|
||||
**DO NOT expose your Ollama server directly to the public internet.**
|
||||
|
||||
Instead, access it through one of these methods:
|
||||
|
||||
1. **Private network** — Run Ollama on the same LAN/VLAN as your Mattermost
|
||||
server. No public exposure needed.
|
||||
|
||||
2. **WireGuard / Tailscale tunnel** — If Mattermost and Ollama are on
|
||||
different networks, use a WireGuard or Tailscale tunnel between them.
|
||||
No public ports required.
|
||||
|
||||
3. **Reverse proxy with IP allowlist** — If a tunnel isn't possible,
|
||||
put Ollama behind a reverse proxy (nginx, Caddy, HAProxy) that:
|
||||
- Restricts access to the Mattermost server's IP address only
|
||||
- Terminates TLS (Ollama doesn't support HTTPS natively)
|
||||
- Logs all access for auditing
|
||||
|
||||
4. **SSH tunnel** — For temporary access: `ssh -L 11434:localhost:11434 user@ollama-host`
|
||||
|
||||
### Plugin Configuration
|
||||
|
||||
The `OllamaURL` setting in the System Console accepts `http://` or `https://`
|
||||
URLs. The plugin validates the scheme at config load and rejects invalid URLs.
|
||||
TLS verification is enabled by default.
|
||||
|
||||
## Other Security Considerations
|
||||
|
||||
- **User access**: Restrict bot usage to specific Mattermost user IDs via
|
||||
the `AllowedUserIDs` setting in the System Console (empty = all users).
|
||||
- **Rate limiting**: Set `RateLimitPerMinute` to prevent abuse (0 = unlimited).
|
||||
- **Stop words**: The bot disengages on configurable stop words ("thanks",
|
||||
"bye", "stop") to prevent unintended continued conversation.
|
||||
- **Logging**: Enable Debug Logging only during troubleshooting — it logs
|
||||
full conversation content and Ollama API requests.
|
||||
- **Plugin updates**: Upload plugin bundles from trusted sources. Verify
|
||||
integrity via your Gitea release artifacts.
|
||||
Reference in New Issue
Block a user