mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 17:00:45 +00:00
mom: Slack bot with abort support, streaming console output, removed sandbox
This commit is contained in:
parent
a7423b954e
commit
aa9e058249
22 changed files with 2741 additions and 58 deletions
95
packages/mom/docs/sandbox.md
Normal file
95
packages/mom/docs/sandbox.md
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# Mom Sandbox Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
Mom uses [@anthropic-ai/sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime) to restrict what the bash tool can do at the OS level.
|
||||
|
||||
## Current Implementation
|
||||
|
||||
Located in `src/sandbox.ts`:
|
||||
|
||||
```typescript
|
||||
import { SandboxManager, type SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime";
|
||||
|
||||
const runtimeConfig: SandboxRuntimeConfig = {
|
||||
network: {
|
||||
allowedDomains: [], // Currently no network - should be ["*"] for full access
|
||||
deniedDomains: [],
|
||||
},
|
||||
filesystem: {
|
||||
denyRead: ["~/.ssh", "~/.aws", ...], // Sensitive paths
|
||||
allowWrite: [channelDir, scratchpadDir], // Only mom's folders
|
||||
denyWrite: [],
|
||||
},
|
||||
};
|
||||
|
||||
await SandboxManager.initialize(runtimeConfig);
|
||||
const sandboxedCommand = await SandboxManager.wrapWithSandbox(command);
|
||||
```
|
||||
|
||||
## Key Limitation: Read Access
|
||||
|
||||
**Read is deny-only** - allowed everywhere by default. We can only deny specific paths, NOT allow only specific paths.
|
||||
|
||||
This means:
|
||||
- ❌ Cannot say "only allow reads from channelDir and scratchpadDir"
|
||||
- ✅ Can say "deny reads from ~/.ssh, ~/.aws, etc."
|
||||
|
||||
The bash tool CAN read files outside the mom data folder. We mitigate by denying sensitive directories.
|
||||
|
||||
## Write Access
|
||||
|
||||
**Write is allow-only** - denied everywhere by default. This works perfectly for our use case:
|
||||
- Only `channelDir` and `scratchpadDir` can be written to
|
||||
- Everything else is blocked
|
||||
|
||||
## Network Access
|
||||
|
||||
- `allowedDomains: []` = no network access
|
||||
- `allowedDomains: ["*"]` = full network access
|
||||
- `allowedDomains: ["github.com", "*.github.com"]` = specific domains
|
||||
|
||||
## How It Works
|
||||
|
||||
- **macOS**: Uses `sandbox-exec` with Seatbelt profiles
|
||||
- **Linux**: Uses `bubblewrap` for containerization
|
||||
|
||||
The sandbox wraps commands - `SandboxManager.wrapWithSandbox("ls")` returns a modified command that runs inside the sandbox.
|
||||
|
||||
## Files
|
||||
|
||||
- `src/sandbox.ts` - Sandbox initialization and command wrapping
|
||||
- `src/tools/bash.ts` - Uses `wrapCommand()` before executing
|
||||
|
||||
## Usage in Agent
|
||||
|
||||
```typescript
|
||||
// In runAgent():
|
||||
await initializeSandbox({ channelDir, scratchpadDir });
|
||||
try {
|
||||
// ... run agent
|
||||
} finally {
|
||||
await resetSandbox();
|
||||
}
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
1. **Update network config** - Change `allowedDomains: []` to `["*"]` for full network access
|
||||
2. **Consider stricter read restrictions** - Current approach denies known sensitive paths but allows reads elsewhere
|
||||
3. **Test on Linux** - Requires `bubblewrap` and `socat` installed
|
||||
|
||||
## Dependencies
|
||||
|
||||
macOS:
|
||||
- `ripgrep` (brew install ripgrep)
|
||||
|
||||
Linux:
|
||||
- `bubblewrap` (apt install bubblewrap)
|
||||
- `socat` (apt install socat)
|
||||
- `ripgrep` (apt install ripgrep)
|
||||
|
||||
## Reference
|
||||
|
||||
- [sandbox-runtime README](https://github.com/anthropic-experimental/sandbox-runtime)
|
||||
- [Claude Code Sandboxing Docs](https://docs.claude.com/en/docs/claude-code/sandboxing)
|
||||
399
packages/mom/docs/slack-bot-minimal-guide.md
Normal file
399
packages/mom/docs/slack-bot-minimal-guide.md
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
# Minimal Slack Bot Setup (No Web Server, WebSocket Only)
|
||||
|
||||
Here's how to connect your Node.js agent to Slack using **Socket Mode** - no Express, no HTTP server, just WebSockets and callbacks.
|
||||
|
||||
---
|
||||
|
||||
## 1. Dependencies
|
||||
|
||||
```bash
|
||||
npm install @slack/socket-mode @slack/web-api
|
||||
```
|
||||
|
||||
That's it. Two packages:
|
||||
- `@slack/socket-mode` - Receives events via WebSocket
|
||||
- `@slack/web-api` - Sends messages back to Slack
|
||||
|
||||
---
|
||||
|
||||
## 2. Get Your Tokens
|
||||
|
||||
You need **TWO tokens**:
|
||||
|
||||
### A. Bot Token (`xoxb-...`)
|
||||
1. Go to https://api.slack.com/apps
|
||||
2. Create app → "From scratch"
|
||||
3. Click "OAuth & Permissions" in sidebar
|
||||
4. Add **Bot Token Scopes** (all 16):
|
||||
```
|
||||
app_mentions:read
|
||||
channels:history
|
||||
channels:join
|
||||
channels:read
|
||||
chat:write
|
||||
files:read
|
||||
files:write
|
||||
groups:history
|
||||
groups:read
|
||||
im:history
|
||||
im:read
|
||||
im:write
|
||||
mpim:history
|
||||
mpim:read
|
||||
mpim:write
|
||||
users:read
|
||||
```
|
||||
5. Click "Install to Workspace" at top
|
||||
6. Copy the **Bot User OAuth Token** (starts with `xoxb-`)
|
||||
|
||||
### B. App-Level Token (`xapp-...`)
|
||||
1. In same app, click "Basic Information" in sidebar
|
||||
2. Scroll to "App-Level Tokens"
|
||||
3. Click "Generate Token and Scopes"
|
||||
4. Name it whatever (e.g., "socket-token")
|
||||
5. Add scope: `connections:write`
|
||||
6. Click "Generate"
|
||||
7. Copy the token (starts with `xapp-`)
|
||||
|
||||
---
|
||||
|
||||
## 3. Enable Socket Mode
|
||||
|
||||
1. Go to https://api.slack.com/apps → select your app
|
||||
2. Click **"Socket Mode"** in sidebar
|
||||
3. Toggle **"Enable Socket Mode"** to ON
|
||||
4. This routes your app's interactions and events over WebSockets instead of public HTTP endpoints
|
||||
5. Done - no webhook URL needed!
|
||||
|
||||
**Note:** Socket Mode is intended for internal apps in development or behind a firewall. Not for apps distributed via Slack Marketplace.
|
||||
|
||||
---
|
||||
|
||||
## 4. Enable Direct Messages
|
||||
|
||||
1. Go to https://api.slack.com/apps → select your app
|
||||
2. Click **"App Home"** in sidebar
|
||||
3. Scroll to **"Show Tabs"** section
|
||||
4. Check **"Allow users to send Slash commands and messages from the messages tab"**
|
||||
5. Save
|
||||
|
||||
---
|
||||
|
||||
## 5. Subscribe to Events
|
||||
|
||||
1. Go to https://api.slack.com/apps → select your app
|
||||
2. Click **"Event Subscriptions"** in sidebar
|
||||
3. Toggle **"Enable Events"** to ON
|
||||
4. **Important:** No Request URL needed (Socket Mode handles this)
|
||||
5. Expand **"Subscribe to bot events"**
|
||||
6. Click **"Add Bot User Event"** and add:
|
||||
- `app_mention` (required - to see when bot is mentioned)
|
||||
- `message.channels` (required - to log all channel messages for context)
|
||||
- `message.groups` (optional - to see private channel messages)
|
||||
- `message.im` (required - to see DMs)
|
||||
7. Click **"Save Changes"** at bottom
|
||||
|
||||
---
|
||||
|
||||
## 6. Store Tokens
|
||||
|
||||
Create `.env` file:
|
||||
|
||||
```bash
|
||||
SLACK_BOT_TOKEN=xoxb-your-bot-token-here
|
||||
SLACK_APP_TOKEN=xapp-your-app-token-here
|
||||
```
|
||||
|
||||
Add to `.gitignore`:
|
||||
|
||||
```bash
|
||||
echo ".env" >> .gitignore
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Minimal Working Code
|
||||
|
||||
```javascript
|
||||
require('dotenv').config();
|
||||
const { SocketModeClient } = require('@slack/socket-mode');
|
||||
const { WebClient } = require('@slack/web-api');
|
||||
|
||||
const socketClient = new SocketModeClient({
|
||||
appToken: process.env.SLACK_APP_TOKEN
|
||||
});
|
||||
|
||||
const webClient = new WebClient(process.env.SLACK_BOT_TOKEN);
|
||||
|
||||
// Listen for app mentions (@mom do something)
|
||||
socketClient.on('app_mention', async ({ event, ack }) => {
|
||||
try {
|
||||
// Acknowledge receipt
|
||||
await ack();
|
||||
|
||||
console.log('Mentioned:', event.text);
|
||||
console.log('Channel:', event.channel);
|
||||
console.log('User:', event.user);
|
||||
|
||||
// Process with your agent
|
||||
const response = await yourAgentFunction(event.text);
|
||||
|
||||
// Send response
|
||||
await webClient.chat.postMessage({
|
||||
channel: event.channel,
|
||||
text: response
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Start the connection
|
||||
(async () => {
|
||||
await socketClient.start();
|
||||
console.log('⚡️ Bot connected and listening!');
|
||||
})();
|
||||
|
||||
// Your existing agent logic
|
||||
async function yourAgentFunction(text) {
|
||||
// Your code here
|
||||
return "I processed: " + text;
|
||||
}
|
||||
```
|
||||
|
||||
**That's it. No web server. Just run it:**
|
||||
|
||||
```bash
|
||||
node bot.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Listen to ALL Events (Not Just Mentions)
|
||||
|
||||
If you want to see every message in channels/DMs the bot is in:
|
||||
|
||||
```javascript
|
||||
// Listen to all Slack events
|
||||
socketClient.on('slack_event', async ({ event, body, ack }) => {
|
||||
await ack();
|
||||
|
||||
console.log('Event type:', event.type);
|
||||
console.log('Event data:', event);
|
||||
|
||||
if (event.type === 'message' && event.subtype === undefined) {
|
||||
// Regular message (not bot message, not edited, etc.)
|
||||
console.log('Message:', event.text);
|
||||
console.log('Channel:', event.channel);
|
||||
console.log('User:', event.user);
|
||||
|
||||
// Your logic here
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Common Operations
|
||||
|
||||
### Send a message
|
||||
```javascript
|
||||
await webClient.chat.postMessage({
|
||||
channel: 'C12345', // or channel ID from event
|
||||
text: 'Hello!'
|
||||
});
|
||||
```
|
||||
|
||||
### Send a DM
|
||||
```javascript
|
||||
// Open DM channel with user
|
||||
const result = await webClient.conversations.open({
|
||||
users: 'U12345' // user ID
|
||||
});
|
||||
|
||||
// Send message to that DM
|
||||
await webClient.chat.postMessage({
|
||||
channel: result.channel.id,
|
||||
text: 'Hey there!'
|
||||
});
|
||||
```
|
||||
|
||||
### List channels
|
||||
```javascript
|
||||
const channels = await webClient.conversations.list({
|
||||
types: 'public_channel,private_channel'
|
||||
});
|
||||
console.log(channels.channels);
|
||||
```
|
||||
|
||||
### Get channel members
|
||||
```javascript
|
||||
const members = await webClient.conversations.members({
|
||||
channel: 'C12345'
|
||||
});
|
||||
console.log(members.members); // Array of user IDs
|
||||
```
|
||||
|
||||
### Get user info
|
||||
```javascript
|
||||
const user = await webClient.users.info({
|
||||
user: 'U12345'
|
||||
});
|
||||
console.log(user.user.name);
|
||||
console.log(user.user.real_name);
|
||||
```
|
||||
|
||||
### Join a channel
|
||||
```javascript
|
||||
await webClient.conversations.join({
|
||||
channel: 'C12345'
|
||||
});
|
||||
```
|
||||
|
||||
### Upload a file
|
||||
```javascript
|
||||
await webClient.files.uploadV2({
|
||||
channel_id: 'C12345',
|
||||
file: fs.createReadStream('./file.pdf'),
|
||||
filename: 'document.pdf',
|
||||
title: 'My Document'
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Complete Example with Your Agent
|
||||
|
||||
```javascript
|
||||
require('dotenv').config();
|
||||
const { SocketModeClient } = require('@slack/socket-mode');
|
||||
const { WebClient } = require('@slack/web-api');
|
||||
|
||||
const socketClient = new SocketModeClient({
|
||||
appToken: process.env.SLACK_APP_TOKEN
|
||||
});
|
||||
|
||||
const webClient = new WebClient(process.env.SLACK_BOT_TOKEN);
|
||||
|
||||
// Your existing agent/AI/whatever
|
||||
class MyAgent {
|
||||
async process(message, context) {
|
||||
// Your complex logic here
|
||||
// context has: user, channel, etc.
|
||||
return `Processed: ${message}`;
|
||||
}
|
||||
}
|
||||
|
||||
const agent = new MyAgent();
|
||||
|
||||
// Handle mentions
|
||||
socketClient.on('app_mention', async ({ event, ack }) => {
|
||||
await ack();
|
||||
|
||||
try {
|
||||
// Remove the @mention from text
|
||||
const text = event.text.replace(/<@[A-Z0-9]+>/g, '').trim();
|
||||
|
||||
// Process with your agent
|
||||
const response = await agent.process(text, {
|
||||
user: event.user,
|
||||
channel: event.channel
|
||||
});
|
||||
|
||||
// Send response
|
||||
await webClient.chat.postMessage({
|
||||
channel: event.channel,
|
||||
text: response
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error processing mention:', error);
|
||||
|
||||
// Send error message
|
||||
await webClient.chat.postMessage({
|
||||
channel: event.channel,
|
||||
text: 'Sorry, something went wrong!'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Start
|
||||
(async () => {
|
||||
await socketClient.start();
|
||||
console.log('⚡️ Agent connected to Slack!');
|
||||
})();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Available Event Types
|
||||
|
||||
You subscribed to these in step 4:
|
||||
|
||||
- `app_mention` - Someone @mentioned the bot
|
||||
- `message` - Any message in a channel/DM the bot is in
|
||||
|
||||
Event object structure:
|
||||
|
||||
```javascript
|
||||
{
|
||||
type: 'app_mention' or 'message',
|
||||
text: 'the message text',
|
||||
user: 'U12345', // who sent it
|
||||
channel: 'C12345', // where it was sent
|
||||
ts: '1234567890.123456' // timestamp
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Advantages of Socket Mode
|
||||
|
||||
✅ **No web server needed** - just run your script
|
||||
✅ **No public URL needed** - works behind firewall
|
||||
✅ **No ngrok** - works on localhost
|
||||
✅ **Auto-reconnect** - SDK handles connection drops
|
||||
✅ **Event-driven** - just listen to callbacks
|
||||
|
||||
---
|
||||
|
||||
## 13. Disadvantages
|
||||
|
||||
❌ Can't distribute to Slack App Directory (only for your workspace)
|
||||
❌ Script must be running to receive messages (unlike webhooks)
|
||||
❌ Max 10 concurrent connections per app
|
||||
|
||||
---
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **You MUST call `ack()`** on every event or Slack will retry
|
||||
2. **Bot token** (`xoxb-`) is for sending messages
|
||||
3. **App token** (`xapp-`) is for receiving events via WebSocket
|
||||
4. **Connection is persistent** - your script stays running
|
||||
5. **No URL validation** needed (unlike HTTP webhooks)
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "invalid_auth" error
|
||||
- Check you're using the right tokens
|
||||
- Bot token for WebClient, App token for SocketModeClient
|
||||
|
||||
### "missing_scope" error
|
||||
- Make sure you added all 16 bot scopes
|
||||
- Reinstall the app after adding scopes
|
||||
|
||||
### Not receiving events
|
||||
- Check Socket Mode is enabled
|
||||
- Check you subscribed to events in "Event Subscriptions"
|
||||
- Make sure bot is in the channel (or use `channels:join`)
|
||||
|
||||
### Bot doesn't respond to mentions
|
||||
- Must subscribe to `app_mention` event
|
||||
- Bot must be installed to workspace
|
||||
- Check `await ack()` is called
|
||||
|
||||
---
|
||||
|
||||
That's it. No HTTP server bullshit. Just WebSockets and callbacks.
|
||||
Loading…
Add table
Add a link
Reference in a new issue