squidclaw 2.6.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.
@@ -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
- await botInfo.bot.api.sendDocument(chatId, new InputFile(buffer, fileName || 'file'), {
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
@@ -228,6 +228,18 @@ export class SquidclawEngine {
228
228
  if (pending.c > 0) console.log(` โฐ Reminders: ${pending.c} pending`);
229
229
  } catch {}
230
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
+
231
243
  // Plugins
232
244
  try {
233
245
  const { PluginManager } = await import('./features/plugins.js');
@@ -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,48 @@ 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
+
148
190
  if (cmd === '/plugins' || cmd === '/plugin') {
149
191
  if (!ctx.engine.plugins) { await ctx.reply('โŒ Plugin system not available'); return; }
150
192
  const args = msg.split(/\s+/).slice(1);
@@ -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.',
@@ -611,10 +618,15 @@ export class ToolRouter {
611
618
  case 'exec':
612
619
  case 'shell':
613
620
  case 'run': {
614
- const { ShellTool } = await import('./shell.js');
615
- const sh = new ShellTool();
616
- const result = sh.exec(toolArg);
617
- toolResult = result.error ? 'Error: ' + result.error : result.output || '(no output)';
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
+ }
618
630
  break;
619
631
  }
620
632
  case 'readfile': {
@@ -643,14 +655,44 @@ export class ToolRouter {
643
655
  toolResult = result.error || result.files.map(f => (f.type === 'dir' ? '๐Ÿ“ ' : '๐Ÿ“„ ') + f.name).join('\n');
644
656
  break;
645
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
+ }
646
669
  case 'python':
647
670
  case 'py': {
648
- const { ShellTool } = await import('./shell.js');
649
- const sh = new ShellTool();
650
- // Write Python script then execute
651
- sh.writeFile('_run.py', toolArg);
652
- const result = sh.exec('python3 _run.py');
653
- toolResult = result.error ? 'Error: ' + result.error : result.output || '(no output)';
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
+ }
654
696
  break;
655
697
  }
656
698
  case 'spawn': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squidclaw",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "๐Ÿฆ‘ AI agent platform โ€” human-like agents for WhatsApp, Telegram & more",
5
5
  "main": "lib/engine.js",
6
6
  "bin": {