Skip to content

Commit 60a703f

Browse files
committed
feat: add opencode-slack-plugin with Socket Mode integration
Slack plugin for KubeOpenCode that runs inside the opencode serve process via Socket Mode (outbound WebSocket, no public URL needed). Features: - Per-thread session mapping (Slack thread <-> OpenCode session 1:1) - PromptQueue for serializing concurrent messages per session - Permission request forwarding with timeout and external resolution handling - Channel-binding (SLACK_CHANNEL) for multi-instance isolation - K8s heartbeat to prevent Agent standby auto-suspend - Graceful shutdown on server.instance.disposed - LRU session eviction with user notification - Event deduplication (message + app_mention) Signed-off-by: xuezhaojun <xuezhaokeepgoing@gmail.com>
0 parents  commit 60a703f

9 files changed

Lines changed: 3782 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Development Guidelines for kubeopencode-plugins
2+
3+
This repository contains officially supported OpenCode plugins for the [KubeOpenCode](https://github.com/kubeopencode/kubeopencode) project.
4+
5+
## Project Overview
6+
7+
**KubeOpenCode** is a Kubernetes-native AI Agent Platform that wraps [OpenCode](https://opencode.ai) with enterprise infrastructure — governance, RBAC, persistence, scheduling, and multi-tenant agent management. Agents run as Kubernetes Deployments with OpenCode as the coding engine inside each Pod.
8+
9+
**This repo** (`kubeopencode-plugins`) houses first-party plugins that extend Agent capabilities. Each plugin is an independent npm package that follows OpenCode's plugin API and can be installed into any Agent via `spec.plugins`.
10+
11+
> **IMPORTANT**: The OpenCode project source is at `../opencode/` and the KubeOpenCode project source is at `../kubeopencode/`. Always search local codebases before using web search.
12+
13+
## Key References
14+
15+
- **OpenCode plugin API**: `../opencode/packages/plugin/src/index.ts` — defines `Plugin`, `PluginModule`, `Hooks`, `PluginInput`
16+
- **OpenCode plugin loader**: `../opencode/packages/opencode/src/plugin/index.ts` — how plugins are loaded and hooks are wired
17+
- **KubeOpenCode plugin install flow**: `../kubeopencode/cmd/kubeopencode/plugin_init.go` — the `plugin-init` init container that runs `npm install`
18+
- **KubeOpenCode Agent plugin spec**: `../kubeopencode/api/v1alpha1/agent_types.go` (lines 139-183) — CRD fields for declaring plugins
19+
- **Existing Slack plugin reference**: `../kubeopencode/plugins/slack/dist/` — the original Slack plugin shipped with KubeOpenCode
20+
21+
## Plugin Architecture
22+
23+
### How Plugins Work in KubeOpenCode
24+
25+
1. Plugins are declared in the Agent spec:
26+
```yaml
27+
spec:
28+
plugins:
29+
- name: "opencode-slack-plugin"
30+
target: server
31+
```
32+
33+
2. The controller creates a **plugin-init** init container that runs `npm install --production` into a shared `/plugins` volume.
34+
35+
3. The executor container loads plugins via OpenCode's config `plugin` array using `file:///plugins/node_modules/<package>` paths.
36+
37+
4. The executor container **does not need npm** — it reads pre-installed packages.
38+
39+
### Plugin Targets
40+
41+
- **`server`** (default): Runs inside `opencode serve`. Has access to `PluginInput.client` (full SDK: sessions, prompts, events). This is the target for all plugins in this repo.
42+
- **`tui`**: Runs during interactive terminal sessions. Provides UI extensions.
43+
44+
### PluginInput
45+
46+
Every server plugin receives a `PluginInput` with:
47+
48+
| Field | Type | Description |
49+
|-------|------|-------------|
50+
| `client` | `OpencodeClient` | Full SDK client (session.create, session.prompt, event.subscribe, etc.). Calls bypass HTTP — direct function invocation inside the process. |
51+
| `project` | `Project` | Current project info |
52+
| `directory` | `string` | Working directory |
53+
| `worktree` | `string` | Git worktree root |
54+
| `serverUrl` | `URL` | Server URL |
55+
| `$` | `BunShell` | Bun shell API |
56+
57+
### Available Hooks
58+
59+
Plugins return a `Hooks` object. Key hooks used by plugins in this repo:
60+
61+
| Hook | Description |
62+
|------|-------------|
63+
| `event` | Receives ALL bus events (session.idle, message.part.updated, permission.asked, server.instance.disposed, etc.) |
64+
| `tool` | Register custom tools |
65+
| `chat.context` | Inject context into LLM prompts |
66+
| `chat.message` | Intercept new messages |
67+
| `permission.ask` | Intercept permission requests |
68+
69+
## Plugin Conventions
70+
71+
### Module Format
72+
73+
Use the `PluginModule` format with an `id` field:
74+
75+
```typescript
76+
import type { PluginModule } from "@opencode-ai/plugin"
77+
78+
const plugin: PluginModule = {
79+
id: "my-plugin",
80+
server: async (input) => {
81+
// initialization
82+
return {
83+
event: async ({ event }) => { /* ... */ },
84+
}
85+
},
86+
}
87+
88+
export default plugin
89+
```
90+
91+
### Environment Variables
92+
93+
- Plugins receive credentials via `process.env`, injected from Agent `spec.credentials` (Kubernetes Secrets)
94+
- If required env vars are missing, log a warning and return empty hooks `{}`
95+
- Never hard-fail — a misconfigured plugin should not crash the Agent
96+
97+
### Logging
98+
99+
Use `console.log` / `console.warn` / `console.error` with a consistent prefix:
100+
101+
```typescript
102+
console.log("[my-plugin] Connected successfully")
103+
console.warn("[my-plugin] Heartbeat disabled: missing env vars")
104+
```
105+
106+
### Heartbeat (KubeOpenCode-specific)
107+
108+
KubeOpenCode Agents support **standby mode** — auto-suspend after idle, auto-resume on new task. Plugins that maintain persistent connections (WebSocket, long-polling) must send heartbeat annotations to prevent unexpected auto-suspend:
109+
110+
- Annotation: `kubeopencode.io/last-connection-active`
111+
- Interval: 60 seconds (match `ConnectionHeartbeatInterval` in controller)
112+
- Stop heartbeat after 5 minutes of inactivity to allow idle timer
113+
- Read ServiceAccount token from `/var/run/secrets/kubernetes.io/serviceaccount/token`
114+
- If AGENT_NAME/AGENT_NAMESPACE/K8s API are unavailable, silently disable heartbeat
115+
116+
See `opencode-slack-plugin/src/index.ts` for the reference implementation.
117+
118+
### Graceful Shutdown
119+
120+
Listen for the `server.instance.disposed` event to clean up connections:
121+
122+
```typescript
123+
if (evt.type === "server.instance.disposed") {
124+
heartbeat.stop()
125+
connection.disconnect()
126+
}
127+
```
128+
129+
### Multi-Instance Safety
130+
131+
Plugins may run in multiple Agent pods simultaneously. Design for this:
132+
133+
- All state is per-process (in-memory Maps, closures) — no shared files or databases
134+
- Session maps are keyed by unique identifiers (e.g., Slack channel + thread timestamp)
135+
- Bounded collections with eviction to prevent memory leaks
136+
137+
## Repository Structure
138+
139+
```
140+
kubeopencode-plugins/
141+
AGENTS.md # This file
142+
opencode-slack-plugin/ # Slack Socket Mode integration
143+
src/index.ts # Plugin source
144+
package.json # npm package config
145+
tsconfig.json
146+
dist/ # Built output (tsup)
147+
README.md # Setup instructions
148+
```
149+
150+
## Development
151+
152+
### Building a Plugin
153+
154+
```bash
155+
cd opencode-slack-plugin
156+
npm install
157+
npm run typecheck # tsc --noEmit
158+
npm run build # tsup -> dist/
159+
```
160+
161+
### Testing Locally
162+
163+
Copy the built plugin to your OpenCode plugins directory:
164+
165+
```bash
166+
cp dist/index.js ~/.config/opencode/plugins/slack-plugin.js
167+
```
168+
169+
Or add to `opencode.json`:
170+
171+
```json
172+
{
173+
"plugin": ["file:///path/to/opencode-slack-plugin"]
174+
}
175+
```
176+
177+
### Testing with KubeOpenCode
178+
179+
Create an Agent with the plugin:
180+
181+
```yaml
182+
apiVersion: kubeopencode.io/v1alpha1
183+
kind: Agent
184+
metadata:
185+
name: my-agent
186+
spec:
187+
plugins:
188+
- name: "opencode-slack-plugin"
189+
credentials:
190+
- secretRef:
191+
name: slack-credentials
192+
# ...
193+
```
194+
195+
## Style Guide
196+
197+
- TypeScript, ESM (`"type": "module"`)
198+
- Use `@opencode-ai/plugin` as a peer dependency
199+
- Prefer `const` over `let`; use early returns over else blocks
200+
- Avoid `try/catch` when possible; use `.catch(() => {})` for best-effort operations
201+
- Keep each plugin in a single file unless complexity demands splitting
202+
- English comments only

opencode-slack-plugin/.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
SLACK_BOT_TOKEN=xoxb-your-bot-token
2+
SLACK_APP_TOKEN=xapp-your-app-token

opencode-slack-plugin/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
dist/
3+
.env
4+
*.log

opencode-slack-plugin/README.md

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# opencode-slack-plugin
2+
3+
OpenCode plugin that connects to Slack via Socket Mode. Run it as part of your `opencode serve` process — zero port exposure, no separate bot process needed.
4+
5+
## Architecture
6+
7+
```
8+
Your machine (MacBook / Linux / K8s Pod)
9+
+----------------------------------------------+
10+
| opencode serve |
11+
| +-- plugins/ |
12+
| +-- opencode-slack-plugin |
13+
| | |
14+
| | WebSocket (outbound only) |
15+
| v |
16+
| Slack Socket Mode API |
17+
| |
18+
| Zero ports exposed |
19+
+----------------------------------------------+
20+
```
21+
22+
- The plugin runs **inside** the OpenCode process
23+
- Connects to Slack via **Socket Mode** (outbound WebSocket, no public URL)
24+
- Each Slack thread maps to an independent OpenCode session
25+
- Tool call progress and permission requests are forwarded to Slack in real time
26+
27+
## Setup
28+
29+
### 1. Create a Slack App
30+
31+
1. Go to [api.slack.com/apps](https://api.slack.com/apps) and click **Create New App** > **From scratch**
32+
2. Enable **Socket Mode** (sidebar > Socket Mode > toggle ON)
33+
- Generate an App-Level Token with scope `connections:write`
34+
- Copy the `xapp-...` token — this is your `SLACK_APP_TOKEN`
35+
3. Add **Bot Token Scopes** (sidebar > OAuth & Permissions > Scopes):
36+
- `chat:write`
37+
- `app_mentions:read`
38+
- `channels:history`
39+
- `groups:history`
40+
- `im:history`
41+
4. Subscribe to **Bot Events** (sidebar > Event Subscriptions > toggle ON):
42+
- `app_mention`
43+
- `message.im`
44+
5. Enable **App Home** > Messages Tab (check "Allow users to send Slash commands and messages from the messages tab")
45+
6. **Install to Workspace** and copy the `xoxb-...` Bot Token — this is your `SLACK_BOT_TOKEN`
46+
47+
### 2. Install the Plugin
48+
49+
#### From npm (once published)
50+
51+
Add to your `opencode.json`:
52+
53+
```json
54+
{
55+
"plugin": ["opencode-slack-plugin"]
56+
}
57+
```
58+
59+
#### Local install
60+
61+
Copy the built output or the source file into your OpenCode plugins directory:
62+
63+
```bash
64+
# Option A: copy source directly
65+
cp src/index.ts ~/.config/opencode/plugins/opencode-slack.ts
66+
67+
# Option B: build and copy dist
68+
npm run build
69+
cp dist/index.js ~/.config/opencode/plugins/opencode-slack.js
70+
```
71+
72+
Then add dependencies to `~/.config/opencode/package.json`:
73+
74+
```json
75+
{
76+
"dependencies": {
77+
"@slack/socket-mode": "^2.0.5",
78+
"@slack/web-api": "^7.13.0"
79+
}
80+
}
81+
```
82+
83+
### 3. Configure Environment Variables
84+
85+
```bash
86+
export SLACK_BOT_TOKEN=xoxb-your-bot-token
87+
export SLACK_APP_TOKEN=xapp-your-app-token
88+
```
89+
90+
### 4. Run OpenCode
91+
92+
```bash
93+
opencode serve
94+
# or just
95+
opencode
96+
```
97+
98+
The plugin activates automatically when both `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` are set. If either is missing, the plugin silently skips initialization.
99+
100+
## Usage
101+
102+
- **DM the bot** directly for private conversations
103+
- **@mention the bot** in a channel to start a threaded conversation
104+
- Each Slack thread creates a separate OpenCode session with its own context
105+
- Session share links are posted automatically when a new thread starts
106+
107+
### Permission Requests
108+
109+
When OpenCode needs permission (e.g., to write a file), the request is forwarded to the Slack thread:
110+
111+
```
112+
Permission Request
113+
write file
114+
Pattern: src/index.ts
115+
116+
1. Yes (once)
117+
2. Always
118+
3. No (reject)
119+
120+
Reply: 1/y/yes, 2/always, or 3/n/no
121+
```
122+
123+
Reply with the corresponding number or keyword.
124+
125+
### Tool Updates
126+
127+
Completed tool calls are posted to the thread in real time:
128+
129+
```
130+
*file_write* - wrote src/index.ts
131+
*bash* - ran tests
132+
```
133+
134+
## KubeOpenCode Integration
135+
136+
When running inside a KubeOpenCode Agent, the plugin additionally:
137+
138+
- **Heartbeat**: Patches the `kubeopencode.io/last-connection-active` annotation to prevent standby auto-suspend while Slack conversations are active
139+
- **Graceful shutdown**: Disconnects cleanly when the OpenCode server is disposed
140+
141+
Deploy via the Agent spec:
142+
143+
```yaml
144+
apiVersion: kubeopencode.io/v1alpha1
145+
kind: Agent
146+
metadata:
147+
name: my-agent
148+
spec:
149+
plugins:
150+
- name: "opencode-slack-plugin"
151+
credentials:
152+
- secretRef:
153+
name: slack-credentials # Secret with SLACK_BOT_TOKEN and SLACK_APP_TOKEN
154+
```
155+
156+
The heartbeat requires `AGENT_NAME` and `AGENT_NAMESPACE` env vars (auto-injected by the controller) and a ServiceAccount with RBAC permission to patch Agent resources. If any of these are missing, heartbeat is silently disabled.
157+
158+
## Environment Variables
159+
160+
| Variable | Required | Description |
161+
|----------|----------|-------------|
162+
| `SLACK_BOT_TOKEN` | Yes | Bot User OAuth Token (`xoxb-...`) |
163+
| `SLACK_APP_TOKEN` | Yes | App-Level Token for Socket Mode (`xapp-...`) |
164+
| `AGENT_NAME` | No | KubeOpenCode Agent name (for heartbeat, auto-injected) |
165+
| `AGENT_NAMESPACE` | No | KubeOpenCode Agent namespace (for heartbeat, auto-injected) |
166+
167+
## How It Differs from `@opencode-ai/slack`
168+
169+
| | This plugin | `@opencode-ai/slack` |
170+
|---|---|---|
171+
| Architecture | Runs inside OpenCode process | Separate Node.js process |
172+
| Communication | Internal function calls (no HTTP) | HTTP to OpenCode server |
173+
| Deployment | Just `opencode serve` | Must run bot + server separately |
174+
| Port exposure | Zero | OpenCode server port (at least localhost) |
175+
| Install | Add to `opencode.json` plugins | `bun run packages/slack` |
176+
| K8s heartbeat | Yes (prevents standby auto-suspend) | No |
177+
| Graceful shutdown | Yes (listens for server.instance.disposed) | No |
178+
179+
## License
180+
181+
MIT

0 commit comments

Comments
 (0)