squidclaw 2.5.0 ā 2.7.0
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/lib/builtin-plugins/analytics/index.js +23 -0
- package/lib/builtin-plugins/analytics/plugin.json +7 -0
- package/lib/builtin-plugins/auto-responder/index.js +16 -0
- package/lib/builtin-plugins/auto-responder/plugin.json +7 -0
- package/lib/builtin-plugins/translator/index.js +22 -0
- package/lib/builtin-plugins/translator/plugin.json +7 -0
- package/lib/channels/telegram/bot.js +2 -1
- package/lib/engine.js +23 -0
- package/lib/features/plugins.js +278 -0
- package/lib/features/sandbox.js +299 -0
- package/lib/middleware/commands.js +100 -0
- package/lib/middleware/plugins.js +41 -0
- package/lib/tools/router.js +74 -11
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
let messageCount = 0;
|
|
2
|
+
let startTime = Date.now();
|
|
3
|
+
|
|
4
|
+
export async function onMessage({ message, contactId, agentId, engine }) {
|
|
5
|
+
messageCount++;
|
|
6
|
+
return null; // Don't intercept, just count
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getTools() {
|
|
10
|
+
return [
|
|
11
|
+
'### Analytics Report',
|
|
12
|
+
'---TOOL:analytics:report---',
|
|
13
|
+
'Show messaging analytics and stats for this session.',
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function onTool({ toolName, engine }) {
|
|
18
|
+
if (toolName !== 'analytics') return null;
|
|
19
|
+
const uptime = ((Date.now() - startTime) / 3600000).toFixed(1);
|
|
20
|
+
return {
|
|
21
|
+
result: `š Session Analytics:\n⢠Messages this session: ${messageCount}\n⢠Session uptime: ${uptime}h\n⢠Engine uptime: ${(process.uptime() / 3600).toFixed(1)}h\n⢠Memory: ${(process.memoryUsage().rss / 1024 / 1024).toFixed(0)} MB`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
let responses = {};
|
|
2
|
+
|
|
3
|
+
export async function onLoad({ config }) {
|
|
4
|
+
responses = config.responses || {
|
|
5
|
+
'ping': 'pong! š',
|
|
6
|
+
'marco': 'polo! š',
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function onMessage({ message }) {
|
|
11
|
+
const lower = message.toLowerCase().trim();
|
|
12
|
+
if (responses[lower]) {
|
|
13
|
+
return { handled: true, response: responses[lower] };
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function getTools() {
|
|
2
|
+
return [
|
|
3
|
+
'### Translate Text',
|
|
4
|
+
'---TOOL:translate:target_language|text to translate---',
|
|
5
|
+
'Translate text to another language. Examples: translate:arabic|Hello world, translate:english|Ł
Ų±ŲŲØŲ§',
|
|
6
|
+
];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function onTool({ toolName, toolArg, engine }) {
|
|
10
|
+
if (toolName !== 'translate') return null;
|
|
11
|
+
const pipeIdx = toolArg.indexOf('|');
|
|
12
|
+
if (pipeIdx === -1) return { result: 'Format: language|text' };
|
|
13
|
+
const lang = toolArg.slice(0, pipeIdx).trim();
|
|
14
|
+
const text = toolArg.slice(pipeIdx + 1).trim();
|
|
15
|
+
|
|
16
|
+
const response = await engine.aiGateway.chat([
|
|
17
|
+
{ role: 'system', content: `Translate the following text to ${lang}. Return ONLY the translation, nothing else.` },
|
|
18
|
+
{ role: 'user', content: text },
|
|
19
|
+
], { model: engine.config.ai?.defaultModel });
|
|
20
|
+
|
|
21
|
+
return { result: response.content };
|
|
22
|
+
}
|
|
@@ -208,7 +208,8 @@ export class TelegramManager {
|
|
|
208
208
|
const chatId = contactId.replace('tg_', '');
|
|
209
209
|
const { readFileSync } = await import('fs');
|
|
210
210
|
const buffer = readFileSync(filePath);
|
|
211
|
-
|
|
211
|
+
const { InputFile: IF } = await import("grammy");
|
|
212
|
+
await botInfo.bot.api.sendDocument(chatId, new IF(buffer, fileName || 'file'), {
|
|
212
213
|
caption: caption || '',
|
|
213
214
|
});
|
|
214
215
|
logger.info('telegram', 'Sent document: ' + fileName);
|
package/lib/engine.js
CHANGED
|
@@ -63,6 +63,8 @@ export class SquidclawEngine {
|
|
|
63
63
|
pipeline.use('auto-links', autoLinksMiddleware);
|
|
64
64
|
pipeline.use('auto-memory', autoMemoryMiddleware);
|
|
65
65
|
const { configChatMiddleware } = await import('./middleware/config-chat.js');
|
|
66
|
+
const { pluginMiddleware } = await import('./middleware/plugins.js');
|
|
67
|
+
pipeline.use('plugins', pluginMiddleware);
|
|
66
68
|
pipeline.use('config-chat', configChatMiddleware);
|
|
67
69
|
pipeline.use('skill-check', skillCheckMiddleware);
|
|
68
70
|
pipeline.use('typing', typingMiddleware); // wraps AI call with typing indicator
|
|
@@ -226,6 +228,27 @@ export class SquidclawEngine {
|
|
|
226
228
|
if (pending.c > 0) console.log(` ā° Reminders: ${pending.c} pending`);
|
|
227
229
|
} catch {}
|
|
228
230
|
|
|
231
|
+
// Sandbox
|
|
232
|
+
try {
|
|
233
|
+
const { Sandbox } = await import('./features/sandbox.js');
|
|
234
|
+
this.sandbox = new Sandbox({
|
|
235
|
+
timeout: 15000,
|
|
236
|
+
maxMemory: 64,
|
|
237
|
+
maxFiles: 100,
|
|
238
|
+
});
|
|
239
|
+
// Cleanup old files every hour
|
|
240
|
+
setInterval(() => this.sandbox.cleanup(), 3600000);
|
|
241
|
+
} catch (err) { logger.error('engine', 'Sandbox init failed: ' + err.message); }
|
|
242
|
+
|
|
243
|
+
// Plugins
|
|
244
|
+
try {
|
|
245
|
+
const { PluginManager } = await import('./features/plugins.js');
|
|
246
|
+
this.plugins = new PluginManager(this);
|
|
247
|
+
const count = await this.plugins.loadAll();
|
|
248
|
+
if (count > 0) console.log(' š Plugins: ' + count + ' loaded');
|
|
249
|
+
await this.plugins.fireHook('onStart', { engine: this });
|
|
250
|
+
} catch (err) { logger.error('engine', 'Plugins init failed: ' + err.message); }
|
|
251
|
+
|
|
229
252
|
// Sessions
|
|
230
253
|
try {
|
|
231
254
|
const { SessionManager } = await import('./features/sessions.js');
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* š¦ Plugin System
|
|
3
|
+
* Load/unload plugins dynamically from ~/.squidclaw/plugins/
|
|
4
|
+
*
|
|
5
|
+
* Plugin structure:
|
|
6
|
+
* plugins/my-plugin/
|
|
7
|
+
* plugin.json ā manifest (name, version, description, hooks)
|
|
8
|
+
* index.js ā main entry (exports hooks)
|
|
9
|
+
* README.md ā optional docs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { logger } from '../core/logger.js';
|
|
13
|
+
import { readdirSync, existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
import { getHome } from '../core/config.js';
|
|
16
|
+
import { pathToFileURL } from 'url';
|
|
17
|
+
|
|
18
|
+
export class PluginManager {
|
|
19
|
+
constructor(engine) {
|
|
20
|
+
this.engine = engine;
|
|
21
|
+
this.plugins = new Map(); // name -> { manifest, module, status }
|
|
22
|
+
this.pluginsDir = join(getHome(), 'plugins');
|
|
23
|
+
mkdirSync(this.pluginsDir, { recursive: true });
|
|
24
|
+
this._initDb();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_initDb() {
|
|
28
|
+
this.engine.storage.db.exec(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS plugins (
|
|
30
|
+
name TEXT PRIMARY KEY,
|
|
31
|
+
enabled INTEGER DEFAULT 1,
|
|
32
|
+
config TEXT DEFAULT '{}',
|
|
33
|
+
installed_at TEXT DEFAULT (datetime('now'))
|
|
34
|
+
)
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// āā Load all plugins āā
|
|
39
|
+
|
|
40
|
+
async loadAll() {
|
|
41
|
+
if (!existsSync(this.pluginsDir)) return;
|
|
42
|
+
|
|
43
|
+
const dirs = readdirSync(this.pluginsDir, { withFileTypes: true })
|
|
44
|
+
.filter(d => d.isDirectory())
|
|
45
|
+
.map(d => d.name);
|
|
46
|
+
|
|
47
|
+
let loaded = 0;
|
|
48
|
+
for (const dir of dirs) {
|
|
49
|
+
try {
|
|
50
|
+
await this.load(dir);
|
|
51
|
+
loaded++;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
logger.error('plugins', `Failed to load ${dir}: ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (loaded > 0) {
|
|
58
|
+
logger.info('plugins', `Loaded ${loaded}/${dirs.length} plugins`);
|
|
59
|
+
}
|
|
60
|
+
return loaded;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// āā Load single plugin āā
|
|
64
|
+
|
|
65
|
+
async load(name) {
|
|
66
|
+
const pluginDir = join(this.pluginsDir, name);
|
|
67
|
+
const manifestPath = join(pluginDir, 'plugin.json');
|
|
68
|
+
const indexPath = join(pluginDir, 'index.js');
|
|
69
|
+
|
|
70
|
+
if (!existsSync(manifestPath)) {
|
|
71
|
+
throw new Error('No plugin.json found');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
75
|
+
manifest.name = manifest.name || name;
|
|
76
|
+
|
|
77
|
+
// Check if disabled in DB
|
|
78
|
+
const dbEntry = this.engine.storage.db.prepare('SELECT * FROM plugins WHERE name = ?').get(manifest.name);
|
|
79
|
+
if (dbEntry && !dbEntry.enabled) {
|
|
80
|
+
this.plugins.set(manifest.name, { manifest, module: null, status: 'disabled' });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Load module
|
|
85
|
+
if (!existsSync(indexPath)) {
|
|
86
|
+
throw new Error('No index.js found');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Dynamic import with cache busting for hot reload
|
|
90
|
+
const moduleUrl = pathToFileURL(indexPath).href + '?t=' + Date.now();
|
|
91
|
+
const module = await import(moduleUrl);
|
|
92
|
+
|
|
93
|
+
// Register in DB
|
|
94
|
+
this.engine.storage.db.prepare(
|
|
95
|
+
'INSERT OR REPLACE INTO plugins (name, enabled, config) VALUES (?, 1, ?)'
|
|
96
|
+
).run(manifest.name, JSON.stringify(dbEntry?.config ? JSON.parse(dbEntry.config) : {}));
|
|
97
|
+
|
|
98
|
+
// Call onLoad hook
|
|
99
|
+
if (module.onLoad) {
|
|
100
|
+
const pluginConfig = dbEntry ? JSON.parse(dbEntry.config || '{}') : {};
|
|
101
|
+
await module.onLoad({ engine: this.engine, config: pluginConfig, logger });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.plugins.set(manifest.name, { manifest, module, status: 'active', dir: pluginDir });
|
|
105
|
+
logger.info('plugins', `Loaded: ${manifest.name} v${manifest.version || '?'}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// āā Unload plugin āā
|
|
109
|
+
|
|
110
|
+
async unload(name) {
|
|
111
|
+
const plugin = this.plugins.get(name);
|
|
112
|
+
if (!plugin) throw new Error('Plugin not found: ' + name);
|
|
113
|
+
|
|
114
|
+
if (plugin.module?.onUnload) {
|
|
115
|
+
await plugin.module.onUnload({ engine: this.engine });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.plugins.delete(name);
|
|
119
|
+
logger.info('plugins', `Unloaded: ${name}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// āā Enable/Disable āā
|
|
123
|
+
|
|
124
|
+
enable(name) {
|
|
125
|
+
this.engine.storage.db.prepare('UPDATE plugins SET enabled = 1 WHERE name = ?').run(name);
|
|
126
|
+
logger.info('plugins', `Enabled: ${name}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
disable(name) {
|
|
130
|
+
this.engine.storage.db.prepare('UPDATE plugins SET enabled = 0 WHERE name = ?').run(name);
|
|
131
|
+
this.unload(name).catch(() => {});
|
|
132
|
+
logger.info('plugins', `Disabled: ${name}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// āā Hot reload āā
|
|
136
|
+
|
|
137
|
+
async reload(name) {
|
|
138
|
+
await this.unload(name).catch(() => {});
|
|
139
|
+
await this.load(name);
|
|
140
|
+
logger.info('plugins', `Reloaded: ${name}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// āā Fire hooks āā
|
|
144
|
+
|
|
145
|
+
async fireHook(hookName, context) {
|
|
146
|
+
const results = [];
|
|
147
|
+
for (const [name, plugin] of this.plugins) {
|
|
148
|
+
if (plugin.status !== 'active' || !plugin.module) continue;
|
|
149
|
+
|
|
150
|
+
const hook = plugin.module[hookName];
|
|
151
|
+
if (typeof hook === 'function') {
|
|
152
|
+
try {
|
|
153
|
+
const result = await hook(context);
|
|
154
|
+
if (result) results.push({ plugin: name, result });
|
|
155
|
+
} catch (err) {
|
|
156
|
+
logger.error('plugins', `${name}.${hookName} failed: ${err.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Hook types:
|
|
165
|
+
* - onLoad({ engine, config, logger }) ā plugin loaded
|
|
166
|
+
* - onUnload({ engine }) ā plugin unloading
|
|
167
|
+
* - onMessage({ message, contactId, agentId, metadata, engine }) ā incoming message, return { handled, response } to intercept
|
|
168
|
+
* - onResponse({ response, contactId, agentId, engine }) ā before sending response, can modify
|
|
169
|
+
* - onTool({ toolName, toolArg, agentId, engine }) ā custom tool handler, return { result } to handle
|
|
170
|
+
* - onStart({ engine }) ā engine started
|
|
171
|
+
* - onStop({ engine }) ā engine stopping
|
|
172
|
+
* - getTools() ā return array of tool descriptions for AI prompt
|
|
173
|
+
*/
|
|
174
|
+
|
|
175
|
+
// āā Get plugin tool descriptions āā
|
|
176
|
+
|
|
177
|
+
getToolDescriptions() {
|
|
178
|
+
const tools = [];
|
|
179
|
+
for (const [name, plugin] of this.plugins) {
|
|
180
|
+
if (plugin.status !== 'active' || !plugin.module?.getTools) continue;
|
|
181
|
+
try {
|
|
182
|
+
const pluginTools = plugin.module.getTools();
|
|
183
|
+
if (Array.isArray(pluginTools)) {
|
|
184
|
+
tools.push(...pluginTools);
|
|
185
|
+
}
|
|
186
|
+
} catch {}
|
|
187
|
+
}
|
|
188
|
+
return tools;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// āā Handle custom tool calls āā
|
|
192
|
+
|
|
193
|
+
async handleTool(toolName, toolArg, agentId) {
|
|
194
|
+
const results = await this.fireHook('onTool', { toolName, toolArg, agentId, engine: this.engine });
|
|
195
|
+
if (results.length > 0 && results[0].result) {
|
|
196
|
+
return results[0].result;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// āā List plugins āā
|
|
202
|
+
|
|
203
|
+
list() {
|
|
204
|
+
return Array.from(this.plugins.entries()).map(([name, p]) => ({
|
|
205
|
+
name,
|
|
206
|
+
version: p.manifest.version || '?',
|
|
207
|
+
description: p.manifest.description || '',
|
|
208
|
+
status: p.status,
|
|
209
|
+
hooks: p.module ? Object.keys(p.module).filter(k => k.startsWith('on') || k === 'getTools') : [],
|
|
210
|
+
author: p.manifest.author || '',
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// āā Create plugin scaffold āā
|
|
215
|
+
|
|
216
|
+
scaffold(name, description) {
|
|
217
|
+
const dir = join(this.pluginsDir, name);
|
|
218
|
+
mkdirSync(dir, { recursive: true });
|
|
219
|
+
|
|
220
|
+
writeFileSync(join(dir, 'plugin.json'), JSON.stringify({
|
|
221
|
+
name,
|
|
222
|
+
version: '1.0.0',
|
|
223
|
+
description: description || 'A Squidclaw plugin',
|
|
224
|
+
author: '',
|
|
225
|
+
hooks: ['onMessage', 'onTool', 'getTools'],
|
|
226
|
+
}, null, 2));
|
|
227
|
+
|
|
228
|
+
writeFileSync(join(dir, 'index.js'), `/**
|
|
229
|
+
* ${name} ā Squidclaw Plugin
|
|
230
|
+
* ${description || ''}
|
|
231
|
+
*/
|
|
232
|
+
|
|
233
|
+
// Called when plugin loads
|
|
234
|
+
export async function onLoad({ engine, config, logger }) {
|
|
235
|
+
logger.info('${name}', 'Plugin loaded!');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Called when plugin unloads
|
|
239
|
+
export async function onUnload({ engine }) {}
|
|
240
|
+
|
|
241
|
+
// Called on every incoming message ā return { handled: true, response } to intercept
|
|
242
|
+
export async function onMessage({ message, contactId, agentId, engine }) {
|
|
243
|
+
// Example: respond to a specific keyword
|
|
244
|
+
// if (message.toLowerCase().includes('hello plugin')) {
|
|
245
|
+
// return { handled: true, response: 'Hello from ${name} plugin! š' };
|
|
246
|
+
// }
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Called before response is sent ā can modify
|
|
251
|
+
export async function onResponse({ response, contactId, agentId, engine }) {
|
|
252
|
+
return response; // return modified response or null
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Handle custom tool calls
|
|
256
|
+
export async function onTool({ toolName, toolArg, agentId, engine }) {
|
|
257
|
+
// if (toolName === 'my_custom_tool') {
|
|
258
|
+
// return { result: 'Custom tool result!' };
|
|
259
|
+
// }
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Return tool descriptions for the AI prompt
|
|
264
|
+
export function getTools() {
|
|
265
|
+
return [
|
|
266
|
+
// '### My Custom Tool',
|
|
267
|
+
// '---TOOL:my_custom_tool:argument---',
|
|
268
|
+
// 'Description of what this tool does.',
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
`);
|
|
272
|
+
|
|
273
|
+
writeFileSync(join(dir, 'README.md'), `# ${name}\n\n${description || 'A Squidclaw plugin.'}\n\n## Installation\n\nCopy to \`~/.squidclaw/plugins/${name}/\`\n`);
|
|
274
|
+
|
|
275
|
+
logger.info('plugins', `Scaffolded: ${dir}`);
|
|
276
|
+
return dir;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* š¦ Proper Sandbox
|
|
3
|
+
* Isolated code execution with resource limits
|
|
4
|
+
* - VM isolation (vm module)
|
|
5
|
+
* - CPU timeout
|
|
6
|
+
* - Memory limits
|
|
7
|
+
* - File system jail
|
|
8
|
+
* - Network restrictions
|
|
9
|
+
* - Safe eval for user code
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { logger } from '../core/logger.js';
|
|
13
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync, statSync, unlinkSync } from 'fs';
|
|
14
|
+
import { join, resolve } from 'path';
|
|
15
|
+
import { execSync, spawn } from 'child_process';
|
|
16
|
+
import vm from 'vm';
|
|
17
|
+
|
|
18
|
+
export class Sandbox {
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.jailDir = options.jailDir || '/tmp/squidclaw-sandbox';
|
|
21
|
+
this.timeout = options.timeout || 10000; // 10s default
|
|
22
|
+
this.maxMemory = options.maxMemory || 64; // 64MB
|
|
23
|
+
this.maxFileSize = options.maxFileSize || 1024 * 1024; // 1MB
|
|
24
|
+
this.maxFiles = options.maxFiles || 50;
|
|
25
|
+
this.maxOutputSize = options.maxOutputSize || 50000; // 50KB
|
|
26
|
+
this.allowNetwork = options.allowNetwork || false;
|
|
27
|
+
|
|
28
|
+
mkdirSync(this.jailDir, { recursive: true });
|
|
29
|
+
|
|
30
|
+
// Blocked patterns for shell commands
|
|
31
|
+
this.blockedCommands = [
|
|
32
|
+
/rm\s+(-rf?|--recursive)\s+\//, /mkfs/, /dd\s+if=/, /:\(\)\{/,
|
|
33
|
+
/chmod\s+777\s+\//, /chown\s.*\//, /passwd/, /userdel/, /useradd/,
|
|
34
|
+
/shutdown/, /reboot/, /halt/, /poweroff/,
|
|
35
|
+
/iptables/, /ufw\s/, /firewall/,
|
|
36
|
+
/wget.*\|.*sh/, /curl.*\|.*sh/, /eval\s*\(/, // download & execute
|
|
37
|
+
/\/etc\/shadow/, /\/etc\/passwd/,
|
|
38
|
+
/ssh\s/, /scp\s/, /rsync\s/, // no remote access
|
|
39
|
+
/npm\s+install\s+-g/, /pip\s+install/, // no global installs
|
|
40
|
+
/systemctl/, /service\s/, // no service control
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// āā JavaScript VM Execution āā
|
|
45
|
+
|
|
46
|
+
evalJS(code, context = {}) {
|
|
47
|
+
const sandbox = {
|
|
48
|
+
console: {
|
|
49
|
+
log: (...args) => { output.push(args.map(String).join(' ')); },
|
|
50
|
+
error: (...args) => { output.push('ERROR: ' + args.map(String).join(' ')); },
|
|
51
|
+
warn: (...args) => { output.push('WARN: ' + args.map(String).join(' ')); },
|
|
52
|
+
},
|
|
53
|
+
Math, Date, JSON, parseInt, parseFloat, isNaN, isFinite,
|
|
54
|
+
String, Number, Boolean, Array, Object, Map, Set, RegExp,
|
|
55
|
+
setTimeout: undefined, setInterval: undefined, // blocked
|
|
56
|
+
fetch: undefined, // blocked unless allowNetwork
|
|
57
|
+
require: undefined, // blocked
|
|
58
|
+
process: { env: {} }, // sanitized
|
|
59
|
+
Buffer: undefined, // blocked
|
|
60
|
+
...context,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const output = [];
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const vmContext = vm.createContext(sandbox, {
|
|
67
|
+
codeGeneration: { strings: false, wasm: false },
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const script = new vm.Script(code, {
|
|
71
|
+
timeout: this.timeout,
|
|
72
|
+
filename: 'sandbox.js',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = script.runInContext(vmContext, {
|
|
76
|
+
timeout: this.timeout,
|
|
77
|
+
breakOnSigint: true,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (result !== undefined && !output.length) {
|
|
81
|
+
output.push(String(result));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const finalOutput = output.join('\n').slice(0, this.maxOutputSize);
|
|
85
|
+
logger.info('sandbox', `JS eval OK (${code.length} chars)`);
|
|
86
|
+
return { success: true, output: finalOutput, type: 'javascript' };
|
|
87
|
+
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const errMsg = err.code === 'ERR_SCRIPT_EXECUTION_TIMEOUT'
|
|
90
|
+
? 'Execution timeout (' + (this.timeout / 1000) + 's limit)'
|
|
91
|
+
: err.message;
|
|
92
|
+
return { success: false, error: errMsg, output: output.join('\n'), type: 'javascript' };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// āā Python Execution (process-isolated) āā
|
|
97
|
+
|
|
98
|
+
async runPython(code, options = {}) {
|
|
99
|
+
const filename = 'run_' + Date.now() + '.py';
|
|
100
|
+
const filepath = join(this.jailDir, filename);
|
|
101
|
+
const timeout = options.timeout || this.timeout;
|
|
102
|
+
|
|
103
|
+
// Safety: wrap in resource limits
|
|
104
|
+
const wrappedCode = `
|
|
105
|
+
import sys, os, signal
|
|
106
|
+
signal.alarm(${Math.ceil(timeout / 1000)})
|
|
107
|
+
sys.path = ['.']
|
|
108
|
+
os.chdir('${this.jailDir}')
|
|
109
|
+
${code}
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
writeFileSync(filepath, wrappedCode);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const result = await this._execProcess('python3', [filepath], {
|
|
116
|
+
timeout,
|
|
117
|
+
cwd: this.jailDir,
|
|
118
|
+
maxOutput: this.maxOutputSize,
|
|
119
|
+
});
|
|
120
|
+
return { ...result, type: 'python' };
|
|
121
|
+
} finally {
|
|
122
|
+
try { unlinkSync(filepath); } catch {}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// āā Shell Execution (sandboxed) āā
|
|
127
|
+
|
|
128
|
+
async runShell(command, options = {}) {
|
|
129
|
+
// Security check
|
|
130
|
+
for (const pattern of this.blockedCommands) {
|
|
131
|
+
if (pattern.test(command)) {
|
|
132
|
+
return { success: false, error: 'Blocked: dangerous command pattern', type: 'shell' };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const timeout = options.timeout || this.timeout;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const output = execSync(command, {
|
|
140
|
+
cwd: options.cwd || this.jailDir,
|
|
141
|
+
timeout,
|
|
142
|
+
maxBuffer: this.maxOutputSize,
|
|
143
|
+
encoding: 'utf8',
|
|
144
|
+
env: {
|
|
145
|
+
PATH: '/usr/local/bin:/usr/bin:/bin',
|
|
146
|
+
HOME: this.jailDir,
|
|
147
|
+
LANG: 'en_US.UTF-8',
|
|
148
|
+
// No AWS keys, no tokens, no secrets
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
logger.info('sandbox', `Shell OK: ${command.slice(0, 50)}`);
|
|
153
|
+
return { success: true, output: output.trim().slice(0, this.maxOutputSize), type: 'shell' };
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
output: (err.stdout || '').trim().slice(0, 5000),
|
|
158
|
+
error: (err.stderr || err.message).trim().slice(0, 5000),
|
|
159
|
+
exitCode: err.status || 1,
|
|
160
|
+
type: 'shell',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// āā File System (jailed) āā
|
|
166
|
+
|
|
167
|
+
writeFile(name, content) {
|
|
168
|
+
const safePath = this._safePath(name);
|
|
169
|
+
|
|
170
|
+
if (content.length > this.maxFileSize) {
|
|
171
|
+
throw new Error('File too large: ' + (content.length / 1024).toFixed(0) + 'KB (max ' + (this.maxFileSize / 1024) + 'KB)');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const fileCount = this._countFiles();
|
|
175
|
+
if (fileCount >= this.maxFiles) {
|
|
176
|
+
throw new Error('Too many files in sandbox (max ' + this.maxFiles + ')');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const dir = resolve(safePath, '..');
|
|
180
|
+
mkdirSync(dir, { recursive: true });
|
|
181
|
+
writeFileSync(safePath, content);
|
|
182
|
+
return { path: safePath, size: content.length };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
readFile(name) {
|
|
186
|
+
const safePath = this._safePath(name);
|
|
187
|
+
if (!existsSync(safePath)) throw new Error('File not found: ' + name);
|
|
188
|
+
|
|
189
|
+
const stat = statSync(safePath);
|
|
190
|
+
if (stat.size > this.maxFileSize) throw new Error('File too large to read');
|
|
191
|
+
|
|
192
|
+
return readFileSync(safePath, 'utf8');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
listFiles(subdir = '.') {
|
|
196
|
+
const safePath = this._safePath(subdir);
|
|
197
|
+
if (!existsSync(safePath)) return [];
|
|
198
|
+
|
|
199
|
+
return readdirSync(safePath, { withFileTypes: true }).map(d => ({
|
|
200
|
+
name: d.name,
|
|
201
|
+
type: d.isDirectory() ? 'dir' : 'file',
|
|
202
|
+
size: d.isFile() ? statSync(join(safePath, d.name)).size : 0,
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
deleteFile(name) {
|
|
207
|
+
const safePath = this._safePath(name);
|
|
208
|
+
if (existsSync(safePath)) unlinkSync(safePath);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// āā Cleanup āā
|
|
212
|
+
|
|
213
|
+
cleanup() {
|
|
214
|
+
try {
|
|
215
|
+
execSync(`find ${this.jailDir} -type f -mmin +60 -delete 2>/dev/null`, { timeout: 5000 });
|
|
216
|
+
logger.info('sandbox', 'Cleaned old files');
|
|
217
|
+
} catch {}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// āā Stats āā
|
|
221
|
+
|
|
222
|
+
getStats() {
|
|
223
|
+
const files = this._countFiles();
|
|
224
|
+
let totalSize = 0;
|
|
225
|
+
try {
|
|
226
|
+
const output = execSync(`du -sb ${this.jailDir} 2>/dev/null`, { encoding: 'utf8' });
|
|
227
|
+
totalSize = parseInt(output.split('\t')[0]) || 0;
|
|
228
|
+
} catch {}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
files,
|
|
232
|
+
maxFiles: this.maxFiles,
|
|
233
|
+
totalSize,
|
|
234
|
+
maxFileSize: this.maxFileSize,
|
|
235
|
+
timeout: this.timeout,
|
|
236
|
+
maxMemory: this.maxMemory,
|
|
237
|
+
jailDir: this.jailDir,
|
|
238
|
+
allowNetwork: this.allowNetwork,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// āā Internal āā
|
|
243
|
+
|
|
244
|
+
_safePath(name) {
|
|
245
|
+
const resolved = resolve(this.jailDir, name);
|
|
246
|
+
if (!resolved.startsWith(this.jailDir)) {
|
|
247
|
+
throw new Error('Path traversal blocked');
|
|
248
|
+
}
|
|
249
|
+
return resolved;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
_countFiles() {
|
|
253
|
+
try {
|
|
254
|
+
const output = execSync(`find ${this.jailDir} -type f | wc -l`, { encoding: 'utf8', timeout: 3000 });
|
|
255
|
+
return parseInt(output.trim()) || 0;
|
|
256
|
+
} catch { return 0; }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async _execProcess(cmd, args, options = {}) {
|
|
260
|
+
return new Promise((resolve) => {
|
|
261
|
+
let stdout = '';
|
|
262
|
+
let stderr = '';
|
|
263
|
+
const timeout = options.timeout || this.timeout;
|
|
264
|
+
const maxOutput = options.maxOutput || this.maxOutputSize;
|
|
265
|
+
|
|
266
|
+
const proc = spawn(cmd, args, {
|
|
267
|
+
cwd: options.cwd || this.jailDir,
|
|
268
|
+
timeout,
|
|
269
|
+
env: {
|
|
270
|
+
PATH: '/usr/local/bin:/usr/bin:/bin',
|
|
271
|
+
HOME: this.jailDir,
|
|
272
|
+
LANG: 'en_US.UTF-8',
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const timer = setTimeout(() => {
|
|
277
|
+
proc.kill('SIGKILL');
|
|
278
|
+
resolve({ success: false, error: 'Timeout (' + (timeout / 1000) + 's)', output: stdout.slice(0, maxOutput) });
|
|
279
|
+
}, timeout);
|
|
280
|
+
|
|
281
|
+
proc.stdout.on('data', d => { stdout += d; if (stdout.length > maxOutput) proc.kill(); });
|
|
282
|
+
proc.stderr.on('data', d => { stderr += d; });
|
|
283
|
+
|
|
284
|
+
proc.on('close', (code) => {
|
|
285
|
+
clearTimeout(timer);
|
|
286
|
+
if (code === 0) {
|
|
287
|
+
resolve({ success: true, output: stdout.trim().slice(0, maxOutput) });
|
|
288
|
+
} else {
|
|
289
|
+
resolve({ success: false, output: stdout.trim().slice(0, 5000), error: stderr.trim().slice(0, 5000), exitCode: code });
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
proc.on('error', (err) => {
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
resolve({ success: false, error: err.message });
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -145,6 +145,106 @@ export async function commandsMiddleware(ctx, next) {
|
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
if (cmd === '/sandbox') {
|
|
149
|
+
if (!ctx.engine.sandbox) { await ctx.reply('ā Sandbox not available'); return; }
|
|
150
|
+
const args = msg.slice(9).trim();
|
|
151
|
+
|
|
152
|
+
if (!args || args === 'stats') {
|
|
153
|
+
const stats = ctx.engine.sandbox.getStats();
|
|
154
|
+
await ctx.reply('š *Sandbox*\n\nš Files: ' + stats.files + '/' + stats.maxFiles + '\nš¾ Size: ' + (stats.totalSize / 1024).toFixed(0) + ' KB\nā±ļø Timeout: ' + (stats.timeout / 1000) + 's\nš§ Max memory: ' + stats.maxMemory + ' MB\nš Network: ' + (stats.allowNetwork ? 'ā
' : 'š«'));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (args === 'files') {
|
|
159
|
+
const files = ctx.engine.sandbox.listFiles();
|
|
160
|
+
if (files.length === 0) { await ctx.reply('š Sandbox is empty'); return; }
|
|
161
|
+
const lines = files.map(f => (f.type === 'dir' ? 'š ' : 'š ') + f.name + (f.size ? ' (' + (f.size / 1024).toFixed(1) + ' KB)' : ''));
|
|
162
|
+
await ctx.reply('š *Sandbox Files*\n\n' + lines.join('\n'));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (args === 'clean') {
|
|
167
|
+
ctx.engine.sandbox.cleanup();
|
|
168
|
+
await ctx.reply('š§¹ Sandbox cleaned');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (args.startsWith('js ')) {
|
|
173
|
+
const code = args.slice(3);
|
|
174
|
+
const result = ctx.engine.sandbox.evalJS(code);
|
|
175
|
+
await ctx.reply(result.success ? '```\n' + (result.output || '(no output)') + '\n```' : 'ā ' + result.error);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (args.startsWith('py ')) {
|
|
180
|
+
const code = args.slice(3);
|
|
181
|
+
const result = await ctx.engine.sandbox.runPython(code);
|
|
182
|
+
await ctx.reply(result.success ? '```\n' + (result.output || '(no output)') + '\n```' : 'ā ' + (result.error || 'Failed'));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await ctx.reply('š *Sandbox Commands*\n\n/sandbox ā stats\n/sandbox files ā list files\n/sandbox clean ā remove old files\n/sandbox js <code> ā run JavaScript\n/sandbox py <code> ā run Python');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (cmd === '/plugins' || cmd === '/plugin') {
|
|
191
|
+
if (!ctx.engine.plugins) { await ctx.reply('ā Plugin system not available'); return; }
|
|
192
|
+
const args = msg.split(/\s+/).slice(1);
|
|
193
|
+
const sub = args[0];
|
|
194
|
+
|
|
195
|
+
if (!sub || sub === 'list') {
|
|
196
|
+
const plugins = ctx.engine.plugins.list();
|
|
197
|
+
if (plugins.length === 0) {
|
|
198
|
+
await ctx.reply('š No plugins installed\n\nCreate one: /plugin new <name>\nOr drop a plugin folder in ~/.squidclaw/plugins/');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const lines = plugins.map(p =>
|
|
202
|
+
(p.status === 'active' ? 'š¢' : 'āøļø') + ' *' + p.name + '* v' + p.version + '\n ' + p.description + '\n Hooks: ' + p.hooks.join(', ')
|
|
203
|
+
);
|
|
204
|
+
await ctx.reply('š *Plugins*\n\n' + lines.join('\n\n'));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (sub === 'new' || sub === 'create') {
|
|
209
|
+
const name = args[1] || 'my-plugin';
|
|
210
|
+
const desc = args.slice(2).join(' ') || '';
|
|
211
|
+
const dir = ctx.engine.plugins.scaffold(name, desc);
|
|
212
|
+
await ctx.reply('š Plugin scaffolded!\n\nš ' + dir + '\n\nEdit index.js to add your logic, then /plugin reload ' + name);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (sub === 'reload') {
|
|
217
|
+
const name = args[1];
|
|
218
|
+
if (!name) { await ctx.reply('Usage: /plugin reload <name>'); return; }
|
|
219
|
+
try {
|
|
220
|
+
await ctx.engine.plugins.reload(name);
|
|
221
|
+
await ctx.reply('š Reloaded: ' + name);
|
|
222
|
+
} catch (err) { await ctx.reply('ā ' + err.message); }
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (sub === 'enable') {
|
|
227
|
+
ctx.engine.plugins.enable(args[1]);
|
|
228
|
+
await ctx.reply('ā
Enabled: ' + args[1]);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (sub === 'disable') {
|
|
233
|
+
ctx.engine.plugins.disable(args[1]);
|
|
234
|
+
await ctx.reply('āøļø Disabled: ' + args[1]);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (sub === 'unload') {
|
|
239
|
+
await ctx.engine.plugins.unload(args[1]);
|
|
240
|
+
await ctx.reply('š Unloaded: ' + args[1]);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
await ctx.reply('š *Plugin Commands*\n\n/plugin list ā show plugins\n/plugin new <name> ā create plugin\n/plugin reload <name> ā hot reload\n/plugin enable <name>\n/plugin disable <name>');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
148
248
|
if (cmd === '/sessions') {
|
|
149
249
|
if (!ctx.engine.sessions) { await ctx.reply('ā Sessions not available'); return; }
|
|
150
250
|
const args = msg.slice(10).trim();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin middleware ā fires onMessage hook before AI processing
|
|
3
|
+
*/
|
|
4
|
+
export async function pluginMiddleware(ctx, next) {
|
|
5
|
+
if (!ctx.engine.plugins) { await next(); return; }
|
|
6
|
+
|
|
7
|
+
// Fire onMessage hooks
|
|
8
|
+
const results = await ctx.engine.plugins.fireHook('onMessage', {
|
|
9
|
+
message: ctx.message,
|
|
10
|
+
contactId: ctx.contactId,
|
|
11
|
+
agentId: ctx.agentId,
|
|
12
|
+
metadata: ctx.metadata,
|
|
13
|
+
engine: ctx.engine,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Check if any plugin handled it
|
|
17
|
+
for (const r of results) {
|
|
18
|
+
if (r.result?.handled) {
|
|
19
|
+
ctx.response = {
|
|
20
|
+
messages: [r.result.response],
|
|
21
|
+
reaction: r.result.reaction || null,
|
|
22
|
+
};
|
|
23
|
+
ctx.handled = true;
|
|
24
|
+
// Skip to response sender
|
|
25
|
+
await next();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await next();
|
|
31
|
+
|
|
32
|
+
// Fire onResponse hooks (post-AI)
|
|
33
|
+
if (ctx.response) {
|
|
34
|
+
await ctx.engine.plugins.fireHook('onResponse', {
|
|
35
|
+
response: ctx.response,
|
|
36
|
+
contactId: ctx.contactId,
|
|
37
|
+
agentId: ctx.agentId,
|
|
38
|
+
engine: ctx.engine,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
package/lib/tools/router.js
CHANGED
|
@@ -169,6 +169,13 @@ export class ToolRouter {
|
|
|
169
169
|
'---TOOL:handoff:reason---',
|
|
170
170
|
'Transfer the conversation to a human agent. Use when you cannot help further.');
|
|
171
171
|
|
|
172
|
+
tools.push('', '### Run JavaScript (Sandboxed)',
|
|
173
|
+
'---TOOL:js:console.log(2 + 2)---',
|
|
174
|
+
'Execute JavaScript in a secure VM sandbox. No network, no filesystem, no require. Safe for math, logic, data processing.',
|
|
175
|
+
'', '### Sandbox Info',
|
|
176
|
+
'---TOOL:sandbox_info:stats---',
|
|
177
|
+
'Show sandbox stats (files, size, limits).');
|
|
178
|
+
|
|
172
179
|
tools.push('', '### Run Command',
|
|
173
180
|
'---TOOL:exec:ls -la---',
|
|
174
181
|
'Execute a shell command. Output is returned. Sandboxed for safety.',
|
|
@@ -244,6 +251,15 @@ export class ToolRouter {
|
|
|
244
251
|
|
|
245
252
|
tools.push('', '**Important:** Use tools when needed. The tool result will be injected into the conversation automatically. Only use one tool per response.');
|
|
246
253
|
|
|
254
|
+
// Plugin tools
|
|
255
|
+
if (this._engine?.plugins) {
|
|
256
|
+
const pluginTools = this._engine.plugins.getToolDescriptions();
|
|
257
|
+
if (pluginTools.length > 0) {
|
|
258
|
+
tools.push('', '## Plugin Tools');
|
|
259
|
+
tools.push(...pluginTools);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
247
263
|
return tools.join('\n');
|
|
248
264
|
}
|
|
249
265
|
|
|
@@ -602,10 +618,15 @@ export class ToolRouter {
|
|
|
602
618
|
case 'exec':
|
|
603
619
|
case 'shell':
|
|
604
620
|
case 'run': {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
621
|
+
if (this._engine?.sandbox) {
|
|
622
|
+
const result = await this._engine.sandbox.runShell(toolArg);
|
|
623
|
+
toolResult = result.success ? (result.output || '(no output)') : 'Error: ' + (result.error || 'Unknown');
|
|
624
|
+
} else {
|
|
625
|
+
const { ShellTool } = await import('./shell.js');
|
|
626
|
+
const sh = new ShellTool();
|
|
627
|
+
const result = sh.exec(toolArg);
|
|
628
|
+
toolResult = result.error ? 'Error: ' + result.error : result.output || '(no output)';
|
|
629
|
+
}
|
|
609
630
|
break;
|
|
610
631
|
}
|
|
611
632
|
case 'readfile': {
|
|
@@ -634,14 +655,44 @@ export class ToolRouter {
|
|
|
634
655
|
toolResult = result.error || result.files.map(f => (f.type === 'dir' ? 'š ' : 'š ') + f.name).join('\n');
|
|
635
656
|
break;
|
|
636
657
|
}
|
|
658
|
+
case 'js':
|
|
659
|
+
case 'javascript':
|
|
660
|
+
case 'eval': {
|
|
661
|
+
if (this._engine?.sandbox) {
|
|
662
|
+
const result = this._engine.sandbox.evalJS(toolArg);
|
|
663
|
+
toolResult = result.success ? (result.output || '(no output)') : 'Error: ' + (result.error || 'Unknown');
|
|
664
|
+
} else {
|
|
665
|
+
toolResult = 'Sandbox not available';
|
|
666
|
+
}
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
637
669
|
case 'python':
|
|
638
670
|
case 'py': {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
671
|
+
if (this._engine?.sandbox) {
|
|
672
|
+
const result = await this._engine.sandbox.runPython(toolArg);
|
|
673
|
+
toolResult = result.success ? (result.output || '(no output)') : 'Error: ' + (result.error || 'Unknown');
|
|
674
|
+
} else {
|
|
675
|
+
const { ShellTool } = await import('./shell.js');
|
|
676
|
+
const sh = new ShellTool();
|
|
677
|
+
sh.writeFile('_run.py', toolArg);
|
|
678
|
+
const result = sh.exec('python3 _run.py');
|
|
679
|
+
toolResult = result.error ? 'Error: ' + result.error : result.output || '(no output)';
|
|
680
|
+
}
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
case 'sandbox_info': {
|
|
684
|
+
if (this._engine?.sandbox) {
|
|
685
|
+
const stats = this._engine.sandbox.getStats();
|
|
686
|
+
toolResult = 'š Sandbox Stats:\n' +
|
|
687
|
+
'š Files: ' + stats.files + '/' + stats.maxFiles + '\n' +
|
|
688
|
+
'š¾ Size: ' + (stats.totalSize / 1024).toFixed(0) + ' KB\n' +
|
|
689
|
+
'ā±ļø Timeout: ' + (stats.timeout / 1000) + 's\n' +
|
|
690
|
+
'š§ Max memory: ' + stats.maxMemory + ' MB\n' +
|
|
691
|
+
'š Network: ' + (stats.allowNetwork ? 'allowed' : 'blocked') + '\n' +
|
|
692
|
+
'š Jail: ' + stats.jailDir;
|
|
693
|
+
} else {
|
|
694
|
+
toolResult = 'Sandbox not available';
|
|
695
|
+
}
|
|
645
696
|
break;
|
|
646
697
|
}
|
|
647
698
|
case 'spawn': {
|
|
@@ -805,7 +856,19 @@ export class ToolRouter {
|
|
|
805
856
|
toolResult = `Email sent to ${to}`;
|
|
806
857
|
break;
|
|
807
858
|
|
|
808
|
-
default:
|
|
859
|
+
default: {
|
|
860
|
+
// Check plugins for custom tool handlers
|
|
861
|
+
if (this._engine?.plugins) {
|
|
862
|
+
const pluginResult = await this._engine.plugins.handleTool(toolName, toolArg, agentId);
|
|
863
|
+
if (pluginResult) {
|
|
864
|
+
if (pluginResult.filePath) {
|
|
865
|
+
return { toolUsed: true, toolName, toolResult: pluginResult.result || 'Done', filePath: pluginResult.filePath, fileName: pluginResult.fileName, cleanResponse };
|
|
866
|
+
}
|
|
867
|
+
toolResult = pluginResult.result || pluginResult;
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
809
872
|
toolResult = `Unknown tool: ${toolName}`;
|
|
810
873
|
}
|
|
811
874
|
} catch (err) {
|