let-them-talk 3.2.3 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +122 -0
- package/README.md +17 -0
- package/SECURITY.md +58 -0
- package/cli.js +23 -11
- package/dashboard.html +1 -1
- package/dashboard.js +11 -1
- package/package.json +4 -2
- package/server.js +57 -13
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [3.0.0] - 2026-03-14
|
|
4
|
+
|
|
5
|
+
### Added — Agent Profiles
|
|
6
|
+
- New tool: `update_profile` (display_name, avatar, bio, role)
|
|
7
|
+
- 12 built-in SVG robot avatar icons with hash-based defaults
|
|
8
|
+
- Profiles auto-created on register, persist across restarts
|
|
9
|
+
- Profile data shown in dashboard (avatars, role badges, profile popup)
|
|
10
|
+
|
|
11
|
+
### Added — Agent Workspaces
|
|
12
|
+
- 3 new tools: `workspace_write`, `workspace_read`, `workspace_list`
|
|
13
|
+
- Per-agent key-value storage (50 keys max, 100KB per value)
|
|
14
|
+
- Agents can read anyone's workspace, write only their own
|
|
15
|
+
- Dashboard "Workspaces" tab with collapsible accordion UI
|
|
16
|
+
|
|
17
|
+
### Added — Workflow Automation
|
|
18
|
+
- 3 new tools: `create_workflow`, `advance_workflow`, `workflow_status`
|
|
19
|
+
- Multi-step pipelines with auto-handoff to step assignees
|
|
20
|
+
- Dashboard "Workflows" tab with horizontal pipeline visualization
|
|
21
|
+
- Dashboard can advance/skip workflow steps
|
|
22
|
+
|
|
23
|
+
### Added — Conversation Branching
|
|
24
|
+
- 3 new tools: `fork_conversation`, `switch_branch`, `list_branches`
|
|
25
|
+
- Fork at any message point with isolated branch history
|
|
26
|
+
- All message tools branch-aware (backward compatible — main branch uses existing files)
|
|
27
|
+
- Branch tabs in dashboard
|
|
28
|
+
|
|
29
|
+
### Added — Plugin System
|
|
30
|
+
- Dynamic tool loading from `plugins/*.js` files
|
|
31
|
+
- Sandboxed execution with 30s timeout
|
|
32
|
+
- CLI: `npx let-them-talk plugin add/list/remove/enable/disable`
|
|
33
|
+
- Dashboard plugin cards with enable/disable toggles
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
- MCP tools: 17 → 27 + dynamic plugins
|
|
37
|
+
- Dashboard tabs: 2 → 4 (Messages, Tasks, Workspaces, Workflows)
|
|
38
|
+
- Branch-aware history API (`?branch=` query param)
|
|
39
|
+
- Version bump across all files (server, dashboard, CLI, package.json)
|
|
40
|
+
|
|
41
|
+
## [2.5.0] - 2026-03-14
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
- Task management system: `create_task`, `update_task`, `list_tasks` tools
|
|
45
|
+
- Kanban board in dashboard (Messages/Tasks toggle)
|
|
46
|
+
- Agent stats panel (sent/received/avg response time per agent)
|
|
47
|
+
- Shareable HTML export (/api/export endpoint)
|
|
48
|
+
- Export dropdown (HTML + Markdown formats)
|
|
49
|
+
- Conversation bookmarks (star messages, localStorage)
|
|
50
|
+
- Sound notification toggle (Web Audio API)
|
|
51
|
+
- Typing indicator for processing agents
|
|
52
|
+
- Connection quality display (SSE latency)
|
|
53
|
+
- Date separators between message groups
|
|
54
|
+
- Message grouping for consecutive same-sender messages
|
|
55
|
+
- Project auto-discover (scan nearby folders)
|
|
56
|
+
- Copy-to-clipboard prompts in onboarding
|
|
57
|
+
- Dynamic tab title with message count
|
|
58
|
+
- Dashboard footer with version
|
|
59
|
+
|
|
60
|
+
### Security
|
|
61
|
+
- Path traversal fix in `share_file` (restricted to project dir)
|
|
62
|
+
- Path traversal fix in `?project=` param (validate against registered projects)
|
|
63
|
+
- 1MB message size limit on send/broadcast/handoff
|
|
64
|
+
- 1MB request body limit on dashboard POST endpoints
|
|
65
|
+
- XSS fix in HTML export (escape agent names)
|
|
66
|
+
- CORS restricted to localhost only (was wildcard)
|
|
67
|
+
- Dashboard binds to 127.0.0.1 only (was 0.0.0.0)
|
|
68
|
+
- Registration guard on `reset` tool
|
|
69
|
+
- Removed absolute file paths from share_file responses
|
|
70
|
+
|
|
71
|
+
## [2.3.0] - 2026-03-14
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
- `handoff` tool for structured work delegation
|
|
75
|
+
- `share_file` tool for sending file contents between agents
|
|
76
|
+
- `broadcast` tool for messaging all agents at once
|
|
77
|
+
- `get_summary` tool for conversation recaps
|
|
78
|
+
- Server-Sent Events for real-time dashboard updates
|
|
79
|
+
- `fs.watch()` on data directory with debounced SSE push
|
|
80
|
+
- Graceful SSE fallback to polling
|
|
81
|
+
- Handoff message rendering (purple banner)
|
|
82
|
+
- File share message rendering (file icon + size)
|
|
83
|
+
|
|
84
|
+
## [2.1.0] - 2026-03-14
|
|
85
|
+
|
|
86
|
+
### Added
|
|
87
|
+
- Multi-agent support (any name, not just A/B)
|
|
88
|
+
- `list_agents` tool with alive/dead status
|
|
89
|
+
- `listen` tool (blocks indefinitely, never times out)
|
|
90
|
+
- Conversation threading (`reply_to` + auto `thread_id`)
|
|
91
|
+
- Message acknowledgments (`ack_message` tool)
|
|
92
|
+
- Heartbeat system (10s interval, `last_activity` tracking)
|
|
93
|
+
- Agent status: active/sleeping/dead with idle time
|
|
94
|
+
- Listening status tracking (`listening_since`)
|
|
95
|
+
- Auto-compact messages.jsonl when >500 lines
|
|
96
|
+
- Auto-archive conversations before reset
|
|
97
|
+
- Context hints when conversation exceeds 50 messages
|
|
98
|
+
- Dead recipient warnings in `send_message`
|
|
99
|
+
- Message sequence numbers for ordering
|
|
100
|
+
- `pending_count` and `agents_online` in delivery responses
|
|
101
|
+
- 4 agent templates: pair, team, review, debate
|
|
102
|
+
- CLI: `npx let-them-talk templates` command
|
|
103
|
+
- CLI: `--template` flag for guided setup
|
|
104
|
+
- Multi-CLI support: Claude Code, Gemini CLI, Codex CLI
|
|
105
|
+
- `AGENT_BRIDGE_DATA_DIR` env var in MCP config
|
|
106
|
+
|
|
107
|
+
### Fixed
|
|
108
|
+
- Heartbeat timer `.unref()` to prevent zombie processes
|
|
109
|
+
- Process exit cleanup (deregister agent on exit)
|
|
110
|
+
- Re-registration cleanup (old name removed)
|
|
111
|
+
- Stale byte offset recovery on file truncation
|
|
112
|
+
|
|
113
|
+
## [2.0.0] - 2026-03-14
|
|
114
|
+
|
|
115
|
+
### Added
|
|
116
|
+
- Initial release
|
|
117
|
+
- MCP server with stdio transport
|
|
118
|
+
- 6 tools: register, send_message, wait_for_reply, check_messages, get_history, reset
|
|
119
|
+
- Web dashboard with real-time monitoring
|
|
120
|
+
- Message injection from dashboard
|
|
121
|
+
- Dark theme UI with markdown rendering
|
|
122
|
+
- `.mcp.json` project-level configuration
|
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/let-them-talk)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://discord.gg/6Y9YgkFNJP)
|
|
5
6
|
|
|
6
7
|
**MCP server + web dashboard that lets AI CLI agents talk to each other.**
|
|
7
8
|
|
|
@@ -154,6 +155,22 @@ npx let-them-talk plugin disable <name> # Disable a plugin
|
|
|
154
155
|
npx let-them-talk help # Show help
|
|
155
156
|
```
|
|
156
157
|
|
|
158
|
+
## Updating
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# If using npx (recommended) — clear cache to get latest version
|
|
162
|
+
npx clear-npx-cache
|
|
163
|
+
npx let-them-talk init # Re-run to update MCP config paths
|
|
164
|
+
|
|
165
|
+
# If installed globally
|
|
166
|
+
npm update -g let-them-talk
|
|
167
|
+
|
|
168
|
+
# Check your version
|
|
169
|
+
npx let-them-talk help # Shows version in header
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
After updating, restart your CLI terminals to pick up the new MCP server.
|
|
173
|
+
|
|
157
174
|
## Plugins
|
|
158
175
|
|
|
159
176
|
Extend Let Them Talk with custom tools. Plugins are `.js` files in the `.agent-bridge/plugins/` directory.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
| ------- | ------------------ |
|
|
7
|
+
| 3.x.x | Yes |
|
|
8
|
+
| 2.x.x | No |
|
|
9
|
+
| < 2.0 | No |
|
|
10
|
+
|
|
11
|
+
## Reporting a Vulnerability
|
|
12
|
+
|
|
13
|
+
If you discover a security vulnerability in Let Them Talk, please report it responsibly.
|
|
14
|
+
|
|
15
|
+
**Do NOT open a public GitHub issue for security vulnerabilities.**
|
|
16
|
+
|
|
17
|
+
Instead, please email **security@dos-technology.com** or use [GitHub's private vulnerability reporting](https://github.com/Dekelelz/let-them-talk/security/advisories/new).
|
|
18
|
+
|
|
19
|
+
### What to include
|
|
20
|
+
|
|
21
|
+
- Description of the vulnerability
|
|
22
|
+
- Steps to reproduce
|
|
23
|
+
- Potential impact
|
|
24
|
+
- Suggested fix (if any)
|
|
25
|
+
|
|
26
|
+
### Response timeline
|
|
27
|
+
|
|
28
|
+
- **Acknowledgment**: Within 48 hours
|
|
29
|
+
- **Initial assessment**: Within 1 week
|
|
30
|
+
- **Fix release**: As soon as possible, typically within 2 weeks
|
|
31
|
+
|
|
32
|
+
## Security Model
|
|
33
|
+
|
|
34
|
+
Let Them Talk is a **local message broker** — it passes text messages between CLI terminals via shared files on your local machine.
|
|
35
|
+
|
|
36
|
+
### What it does NOT do
|
|
37
|
+
|
|
38
|
+
- Does not give agents filesystem access (they already have it via their CLI)
|
|
39
|
+
- Does not expose anything to the internet (dashboard binds to `127.0.0.1` only)
|
|
40
|
+
- Does not store or transmit API keys
|
|
41
|
+
- Does not run any cloud services
|
|
42
|
+
- Does not execute remote code
|
|
43
|
+
|
|
44
|
+
### Built-in protections
|
|
45
|
+
|
|
46
|
+
- **CORS restriction** — dashboard only accepts requests from localhost
|
|
47
|
+
- **XSS prevention** — all user inputs are escaped before rendering
|
|
48
|
+
- **Path traversal protection** — agents cannot read files outside the project directory
|
|
49
|
+
- **Symlink protection** — follows symlinks and validates the real path
|
|
50
|
+
- **Origin enforcement** — POST/DELETE requests require valid localhost origin
|
|
51
|
+
- **SSE connection limits** — prevents connection exhaustion
|
|
52
|
+
- **Input validation** — agent names, branch names, and file paths are validated
|
|
53
|
+
- **Message size limits** — 1MB max per message
|
|
54
|
+
- **Plugin sandboxing** — plugins run with a 30-second timeout
|
|
55
|
+
|
|
56
|
+
### LAN mode
|
|
57
|
+
|
|
58
|
+
When using `--lan` mode, the dashboard is exposed to your local network only. It is never accessible from the internet.
|
package/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ const command = process.argv[2];
|
|
|
8
8
|
|
|
9
9
|
function printUsage() {
|
|
10
10
|
console.log(`
|
|
11
|
-
Let Them Talk — Agent Bridge v3.
|
|
11
|
+
Let Them Talk — Agent Bridge v3.3.0
|
|
12
12
|
MCP message broker for inter-agent communication
|
|
13
13
|
Supports: Claude Code, Gemini CLI, Codex CLI
|
|
14
14
|
|
|
@@ -331,24 +331,33 @@ function pluginCmd() {
|
|
|
331
331
|
const absPath = path.resolve(filePath);
|
|
332
332
|
if (!fs.existsSync(absPath)) { console.error(' File not found: ' + absPath); process.exit(1); }
|
|
333
333
|
|
|
334
|
-
// Validate plugin
|
|
334
|
+
// Validate plugin structure without executing it (no require — prevents RCE on install)
|
|
335
335
|
try {
|
|
336
|
-
const
|
|
337
|
-
if (!
|
|
336
|
+
const src = fs.readFileSync(absPath, 'utf8');
|
|
337
|
+
if (!src.includes('module.exports') || !src.includes('name') || !src.includes('handler')) {
|
|
338
|
+
console.error(' Plugin must export name, description, and handler (module.exports = { name, handler })');
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Extract plugin name from source using regex (no eval)
|
|
343
|
+
const nameMatch = src.match(/name\s*:\s*['"]([^'"]+)['"]/);
|
|
344
|
+
const descMatch = src.match(/description\s*:\s*['"]([^'"]+)['"]/);
|
|
345
|
+
const pluginName = nameMatch ? nameMatch[1] : path.basename(absPath, '.js');
|
|
346
|
+
const pluginDesc = descMatch ? descMatch[1] : '';
|
|
338
347
|
|
|
339
348
|
if (!fs.existsSync(pluginsDir)) fs.mkdirSync(pluginsDir, { recursive: true });
|
|
340
349
|
const destFile = path.join(pluginsDir, path.basename(absPath));
|
|
341
350
|
fs.copyFileSync(absPath, destFile);
|
|
342
351
|
|
|
343
352
|
const reg = getRegistry();
|
|
344
|
-
if (!reg.find(p => p.name ===
|
|
345
|
-
reg.push({ name:
|
|
353
|
+
if (!reg.find(p => p.name === pluginName)) {
|
|
354
|
+
reg.push({ name: pluginName, description: pluginDesc, file: path.basename(absPath), enabled: true, added_at: new Date().toISOString() });
|
|
346
355
|
saveRegistry(reg);
|
|
347
356
|
}
|
|
348
|
-
console.log(' Plugin "' +
|
|
349
|
-
console.log(' Restart CLI to load the new tool.');
|
|
357
|
+
console.log(' Plugin "' + pluginName + '" installed successfully.');
|
|
358
|
+
console.log(' Restart CLI to load the new tool (runs sandboxed).');
|
|
350
359
|
} catch (e) {
|
|
351
|
-
console.error(' Failed to
|
|
360
|
+
console.error(' Failed to install plugin: ' + e.message);
|
|
352
361
|
process.exit(1);
|
|
353
362
|
}
|
|
354
363
|
break;
|
|
@@ -362,8 +371,11 @@ function pluginCmd() {
|
|
|
362
371
|
const newReg = reg.filter(p => p.name !== name);
|
|
363
372
|
saveRegistry(newReg);
|
|
364
373
|
if (plugin.file) {
|
|
365
|
-
const pluginFile = path.
|
|
366
|
-
|
|
374
|
+
const pluginFile = path.resolve(pluginsDir, plugin.file);
|
|
375
|
+
// Prevent path traversal — only delete files inside pluginsDir
|
|
376
|
+
if (pluginFile.startsWith(path.resolve(pluginsDir) + path.sep) && fs.existsSync(pluginFile)) {
|
|
377
|
+
fs.unlinkSync(pluginFile);
|
|
378
|
+
}
|
|
367
379
|
}
|
|
368
380
|
console.log(' Plugin "' + name + '" removed.');
|
|
369
381
|
break;
|
package/dashboard.html
CHANGED
|
@@ -2471,7 +2471,7 @@
|
|
|
2471
2471
|
</div>
|
|
2472
2472
|
</div>
|
|
2473
2473
|
<div class="app-footer">
|
|
2474
|
-
<span>Let Them Talk v3.
|
|
2474
|
+
<span>Let Them Talk v3.3.0</span>
|
|
2475
2475
|
</div>
|
|
2476
2476
|
<div class="profile-popup" id="profile-popup" onclick="event.stopPropagation()">
|
|
2477
2477
|
<div class="profile-popup-header">
|
package/dashboard.js
CHANGED
|
@@ -690,8 +690,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
690
690
|
return;
|
|
691
691
|
}
|
|
692
692
|
|
|
693
|
-
// CSRF protection: validate
|
|
693
|
+
// CSRF + DNS rebinding protection: validate Host and Origin on mutating requests
|
|
694
694
|
if (req.method === 'POST' || req.method === 'DELETE') {
|
|
695
|
+
// Check Host header to block DNS rebinding attacks
|
|
696
|
+
const host = (req.headers.host || '').replace(/:\d+$/, '');
|
|
697
|
+
const validHosts = ['localhost', '127.0.0.1'];
|
|
698
|
+
if (LAN_MODE && getLanIP()) validHosts.push(getLanIP());
|
|
699
|
+
if (!validHosts.includes(host)) {
|
|
700
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
701
|
+
res.end(JSON.stringify({ error: 'Forbidden: invalid host' }));
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
// Check Origin header to block cross-site requests
|
|
695
705
|
const origin = req.headers.origin || '';
|
|
696
706
|
const referer = req.headers.referer || '';
|
|
697
707
|
const source = origin || referer;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "let-them-talk",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"description": "MCP message broker + web dashboard for inter-agent communication. Let AI CLI agents talk to each other.",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"cli.js",
|
|
23
23
|
"templates/",
|
|
24
24
|
"logo.png",
|
|
25
|
-
"LICENSE"
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"SECURITY.md",
|
|
27
|
+
"CHANGELOG.md"
|
|
26
28
|
],
|
|
27
29
|
"keywords": [
|
|
28
30
|
"mcp",
|
package/server.js
CHANGED
|
@@ -23,11 +23,27 @@ const PLUGINS_DIR = path.join(DATA_DIR, 'plugins');
|
|
|
23
23
|
|
|
24
24
|
// In-memory state for this process
|
|
25
25
|
let registeredName = null;
|
|
26
|
+
let registeredToken = null; // auth token for re-registration
|
|
26
27
|
let lastReadOffset = 0; // byte offset into messages.jsonl for efficient polling
|
|
27
28
|
let heartbeatInterval = null; // heartbeat timer reference
|
|
28
29
|
let messageSeq = 0; // monotonic sequence counter for message ordering
|
|
29
30
|
let currentBranch = 'main'; // which branch this agent is on
|
|
30
31
|
|
|
32
|
+
// Rate limiting — prevent broadcast storms and message flooding
|
|
33
|
+
const rateLimitWindow = 60000; // 1 minute window
|
|
34
|
+
const rateLimitMax = 30; // max 30 messages per minute per agent
|
|
35
|
+
let rateLimitMessages = []; // timestamps of recent messages
|
|
36
|
+
|
|
37
|
+
function checkRateLimit() {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
rateLimitMessages = rateLimitMessages.filter(t => now - t < rateLimitWindow);
|
|
40
|
+
if (rateLimitMessages.length >= rateLimitMax) {
|
|
41
|
+
return { error: `Rate limit exceeded: max ${rateLimitMax} messages per minute. Wait before sending more.` };
|
|
42
|
+
}
|
|
43
|
+
rateLimitMessages.push(now);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
// --- Helpers ---
|
|
32
48
|
|
|
33
49
|
function ensureDataDir() {
|
|
@@ -112,7 +128,13 @@ function validateContentSize(content) {
|
|
|
112
128
|
}
|
|
113
129
|
|
|
114
130
|
function generateId() {
|
|
115
|
-
return Date.now().toString(36) +
|
|
131
|
+
try { return Date.now().toString(36) + require('crypto').randomBytes(6).toString('hex'); }
|
|
132
|
+
catch { return Date.now().toString(36) + Math.random().toString(36).slice(2, 8); }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function generateToken() {
|
|
136
|
+
try { return require('crypto').randomBytes(16).toString('hex'); }
|
|
137
|
+
catch { return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2); }
|
|
116
138
|
}
|
|
117
139
|
|
|
118
140
|
function sleep(ms) {
|
|
@@ -230,9 +252,11 @@ function autoCompact() {
|
|
|
230
252
|
return false;
|
|
231
253
|
});
|
|
232
254
|
|
|
233
|
-
// Rewrite messages.jsonl
|
|
255
|
+
// Rewrite messages.jsonl atomically — write to temp file then rename
|
|
234
256
|
const newContent = active.map(m => JSON.stringify(m)).join('\n') + (active.length ? '\n' : '');
|
|
235
|
-
|
|
257
|
+
const tmpFile = msgFile + '.tmp';
|
|
258
|
+
fs.writeFileSync(tmpFile, newContent);
|
|
259
|
+
fs.renameSync(tmpFile, msgFile);
|
|
236
260
|
lastReadOffset = Buffer.byteLength(newContent, 'utf8');
|
|
237
261
|
|
|
238
262
|
// Trim consumed ID files — keep only IDs still in active messages
|
|
@@ -366,7 +390,15 @@ function toolRegister(name, provider = null) {
|
|
|
366
390
|
|
|
367
391
|
const agents = getAgents();
|
|
368
392
|
if (agents[name] && agents[name].pid !== process.pid && isPidAlive(agents[name].pid)) {
|
|
369
|
-
return { error: `Agent "${name}" is already registered by a live process
|
|
393
|
+
return { error: `Agent "${name}" is already registered by a live process. Choose a different name.` };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// If name was previously registered by a dead process, verify token to prevent impersonation
|
|
397
|
+
if (agents[name] && agents[name].token && !isPidAlive(agents[name].pid)) {
|
|
398
|
+
// Dead agent — only allow re-registration from the same process (same token)
|
|
399
|
+
if (registeredToken && registeredToken !== agents[name].token) {
|
|
400
|
+
return { error: `Agent "${name}" was previously registered by another process. Choose a different name.` };
|
|
401
|
+
}
|
|
370
402
|
}
|
|
371
403
|
|
|
372
404
|
// Clean up old registration if re-registering with a different name
|
|
@@ -375,9 +407,11 @@ function toolRegister(name, provider = null) {
|
|
|
375
407
|
}
|
|
376
408
|
|
|
377
409
|
const now = new Date().toISOString();
|
|
378
|
-
agents[name]
|
|
410
|
+
const token = (agents[name] && agents[name].token) || generateToken();
|
|
411
|
+
agents[name] = { pid: process.pid, timestamp: now, last_activity: now, provider: provider || 'unknown', branch: currentBranch, token };
|
|
379
412
|
saveAgents(agents);
|
|
380
413
|
registeredName = name;
|
|
414
|
+
registeredToken = token;
|
|
381
415
|
|
|
382
416
|
// Auto-create profile if not exists
|
|
383
417
|
const profiles = getProfiles();
|
|
@@ -459,6 +493,9 @@ function toolSendMessage(content, to = null, reply_to = null) {
|
|
|
459
493
|
return { error: 'You must call register() first' };
|
|
460
494
|
}
|
|
461
495
|
|
|
496
|
+
const rateErr = checkRateLimit();
|
|
497
|
+
if (rateErr) return rateErr;
|
|
498
|
+
|
|
462
499
|
const agents = getAgents();
|
|
463
500
|
const otherAgents = Object.keys(agents).filter(n => n !== registeredName);
|
|
464
501
|
|
|
@@ -531,6 +568,9 @@ function toolBroadcast(content) {
|
|
|
531
568
|
return { error: 'You must call register() first' };
|
|
532
569
|
}
|
|
533
570
|
|
|
571
|
+
const rateErr = checkRateLimit();
|
|
572
|
+
if (rateErr) return rateErr;
|
|
573
|
+
|
|
534
574
|
const sizeErr = validateContentSize(content);
|
|
535
575
|
if (sizeErr) return sizeErr;
|
|
536
576
|
|
|
@@ -1909,13 +1949,16 @@ function loadPlugins() {
|
|
|
1909
1949
|
const enabledNames = new Set(registry.filter(p => p.enabled !== false).map(p => p.name));
|
|
1910
1950
|
|
|
1911
1951
|
try {
|
|
1952
|
+
const vm = require('vm');
|
|
1912
1953
|
const files = fs.readdirSync(PLUGINS_DIR).filter(f => f.endsWith('.js'));
|
|
1913
1954
|
for (const file of files) {
|
|
1914
1955
|
try {
|
|
1915
1956
|
const pluginPath = path.join(PLUGINS_DIR, file);
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
const
|
|
1957
|
+
const code = fs.readFileSync(pluginPath, 'utf8');
|
|
1958
|
+
// Run plugin in a sandboxed VM context — no require, no process, no child_process
|
|
1959
|
+
const sandbox = { module: { exports: {} }, exports: {}, console: { log: () => {}, error: () => {}, warn: () => {} } };
|
|
1960
|
+
vm.runInNewContext(code, sandbox, { filename: file, timeout: 5000 });
|
|
1961
|
+
const plugin = sandbox.module.exports;
|
|
1919
1962
|
if (!plugin.name || !plugin.description || !plugin.handler) {
|
|
1920
1963
|
console.error(`Plugin ${file}: missing name, description, or handler`);
|
|
1921
1964
|
continue;
|
|
@@ -1927,7 +1970,7 @@ function loadPlugins() {
|
|
|
1927
1970
|
inputSchema: plugin.inputSchema || { type: 'object', properties: {} },
|
|
1928
1971
|
handler: plugin.handler,
|
|
1929
1972
|
});
|
|
1930
|
-
console.error(`Plugin loaded: ${plugin.name}`);
|
|
1973
|
+
console.error(`Plugin loaded: ${plugin.name} (sandboxed)`);
|
|
1931
1974
|
} catch (e) {
|
|
1932
1975
|
console.error(`Plugin ${file} failed to load: ${e.message}`);
|
|
1933
1976
|
}
|
|
@@ -1941,17 +1984,18 @@ function executePlugin(pluginName, args) {
|
|
|
1941
1984
|
|
|
1942
1985
|
const context = {
|
|
1943
1986
|
registeredName,
|
|
1944
|
-
dataDir: DATA_DIR,
|
|
1945
1987
|
sendMessage: (to, content) => toolSendMessage(content, to),
|
|
1946
1988
|
getAgents: () => toolListAgents().agents,
|
|
1947
1989
|
getHistory: (limit) => toolGetHistory(limit),
|
|
1948
1990
|
readFile: (filePath) => {
|
|
1949
1991
|
const resolved = path.resolve(filePath);
|
|
1950
1992
|
const allowedRoot = path.resolve(process.cwd());
|
|
1951
|
-
|
|
1993
|
+
let realPath;
|
|
1994
|
+
try { realPath = fs.realpathSync(resolved); } catch { throw new Error('File not found'); }
|
|
1995
|
+
if (!realPath.startsWith(allowedRoot + path.sep) && realPath !== allowedRoot) {
|
|
1952
1996
|
throw new Error('File path must be within the project directory');
|
|
1953
1997
|
}
|
|
1954
|
-
return fs.readFileSync(
|
|
1998
|
+
return fs.readFileSync(realPath, 'utf8');
|
|
1955
1999
|
},
|
|
1956
2000
|
};
|
|
1957
2001
|
|
|
@@ -1977,7 +2021,7 @@ async function main() {
|
|
|
1977
2021
|
loadPlugins();
|
|
1978
2022
|
const transport = new StdioServerTransport();
|
|
1979
2023
|
await server.connect(transport);
|
|
1980
|
-
console.error('Agent Bridge MCP server v3.
|
|
2024
|
+
console.error('Agent Bridge MCP server v3.3.1 running (' + (27 + loadedPlugins.length) + ' tools)');
|
|
1981
2025
|
}
|
|
1982
2026
|
|
|
1983
2027
|
main().catch(console.error);
|