squidclaw 2.6.0 → 2.8.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/channels/telegram/bot.js +2 -1
- package/lib/engine.js +22 -0
- package/lib/features/nodes.js +315 -0
- package/lib/features/sandbox.js +299 -0
- package/lib/middleware/commands.js +90 -0
- package/lib/tools/router.js +120 -10
- package/package.json +2 -1
|
@@ -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
|
@@ -228,6 +228,28 @@ export class SquidclawEngine {
|
|
|
228
228
|
if (pending.c > 0) console.log(` ⏰ Reminders: ${pending.c} pending`);
|
|
229
229
|
} catch {}
|
|
230
230
|
|
|
231
|
+
// Nodes (Paired Devices)
|
|
232
|
+
try {
|
|
233
|
+
const { NodeManager } = await import('./features/nodes.js');
|
|
234
|
+
this.nodeManager = new NodeManager(this);
|
|
235
|
+
await this.nodeManager.startServer(9501);
|
|
236
|
+
const nodeCount = this.nodeManager.list('*').length;
|
|
237
|
+
if (nodeCount > 0) console.log(' 📡 Nodes: ' + nodeCount + ' registered');
|
|
238
|
+
else console.log(' 📡 Nodes: ws://0.0.0.0:9501');
|
|
239
|
+
} catch (err) { logger.error('engine', 'Nodes init failed: ' + err.message); }
|
|
240
|
+
|
|
241
|
+
// Sandbox
|
|
242
|
+
try {
|
|
243
|
+
const { Sandbox } = await import('./features/sandbox.js');
|
|
244
|
+
this.sandbox = new Sandbox({
|
|
245
|
+
timeout: 15000,
|
|
246
|
+
maxMemory: 64,
|
|
247
|
+
maxFiles: 100,
|
|
248
|
+
});
|
|
249
|
+
// Cleanup old files every hour
|
|
250
|
+
setInterval(() => this.sandbox.cleanup(), 3600000);
|
|
251
|
+
} catch (err) { logger.error('engine', 'Sandbox init failed: ' + err.message); }
|
|
252
|
+
|
|
231
253
|
// Plugins
|
|
232
254
|
try {
|
|
233
255
|
const { PluginManager } = await import('./features/plugins.js');
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦑 Nodes — Paired Devices
|
|
3
|
+
* Connect phones, PCs, servers, IoT devices
|
|
4
|
+
* Real-time control via WebSocket
|
|
5
|
+
*
|
|
6
|
+
* Pairing: token-based (generate token → enter on device)
|
|
7
|
+
* Communication: WebSocket (ws://host:9501)
|
|
8
|
+
* Commands: camera, screen, location, run, notify, status
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { logger } from '../core/logger.js';
|
|
12
|
+
import crypto from 'crypto';
|
|
13
|
+
import { getHome } from '../core/config.js';
|
|
14
|
+
|
|
15
|
+
export class NodeManager {
|
|
16
|
+
constructor(engine) {
|
|
17
|
+
this.engine = engine;
|
|
18
|
+
this.nodes = new Map(); // nodeId -> { info, ws, status, lastSeen }
|
|
19
|
+
this.pendingPairs = new Map(); // token -> { agentId, createdAt, name }
|
|
20
|
+
this.wsServer = null;
|
|
21
|
+
this._initDb();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_initDb() {
|
|
25
|
+
this.engine.storage.db.exec(`
|
|
26
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
27
|
+
id TEXT PRIMARY KEY,
|
|
28
|
+
agent_id TEXT NOT NULL,
|
|
29
|
+
name TEXT NOT NULL,
|
|
30
|
+
type TEXT DEFAULT 'generic',
|
|
31
|
+
platform TEXT DEFAULT 'unknown',
|
|
32
|
+
capabilities TEXT DEFAULT '[]',
|
|
33
|
+
paired_at TEXT DEFAULT (datetime('now')),
|
|
34
|
+
last_seen TEXT,
|
|
35
|
+
status TEXT DEFAULT 'offline',
|
|
36
|
+
metadata TEXT DEFAULT '{}'
|
|
37
|
+
)
|
|
38
|
+
`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── WebSocket Server ──
|
|
42
|
+
|
|
43
|
+
async startServer(port = 9501) {
|
|
44
|
+
try {
|
|
45
|
+
const { WebSocketServer } = await import('ws');
|
|
46
|
+
|
|
47
|
+
this.wsServer = new WebSocketServer({ port, path: '/nodes' });
|
|
48
|
+
|
|
49
|
+
this.wsServer.on('connection', (ws, req) => {
|
|
50
|
+
let nodeId = null;
|
|
51
|
+
|
|
52
|
+
ws.on('message', (data) => {
|
|
53
|
+
try {
|
|
54
|
+
const msg = JSON.parse(data.toString());
|
|
55
|
+
this._handleMessage(ws, msg).then(id => {
|
|
56
|
+
if (id) nodeId = id;
|
|
57
|
+
});
|
|
58
|
+
} catch (err) {
|
|
59
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Invalid message' }));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
ws.on('close', () => {
|
|
64
|
+
if (nodeId) {
|
|
65
|
+
const node = this.nodes.get(nodeId);
|
|
66
|
+
if (node) {
|
|
67
|
+
node.status = 'offline';
|
|
68
|
+
node.ws = null;
|
|
69
|
+
this.engine.storage.db.prepare("UPDATE nodes SET status = 'offline' WHERE id = ?").run(nodeId);
|
|
70
|
+
logger.info('nodes', `Node disconnected: ${node.info?.name || nodeId}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
ws.on('error', () => {});
|
|
76
|
+
|
|
77
|
+
// Send hello
|
|
78
|
+
ws.send(JSON.stringify({ type: 'hello', message: 'Squidclaw Node Server 🦑' }));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Load existing nodes from DB
|
|
82
|
+
const dbNodes = this.engine.storage.db.prepare('SELECT * FROM nodes').all();
|
|
83
|
+
for (const n of dbNodes) {
|
|
84
|
+
this.nodes.set(n.id, {
|
|
85
|
+
info: n,
|
|
86
|
+
ws: null,
|
|
87
|
+
status: 'offline',
|
|
88
|
+
lastSeen: n.last_seen,
|
|
89
|
+
pendingCommands: new Map(),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
logger.info('nodes', `WebSocket server on port ${port} (${dbNodes.length} registered nodes)`);
|
|
94
|
+
return true;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
logger.error('nodes', `Server failed: ${err.message}`);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Message Handler ──
|
|
102
|
+
|
|
103
|
+
async _handleMessage(ws, msg) {
|
|
104
|
+
const { type } = msg;
|
|
105
|
+
|
|
106
|
+
// Pairing
|
|
107
|
+
if (type === 'pair') {
|
|
108
|
+
return this._handlePair(ws, msg);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Auth (reconnect with nodeId)
|
|
112
|
+
if (type === 'auth') {
|
|
113
|
+
const node = this.nodes.get(msg.nodeId);
|
|
114
|
+
if (!node) {
|
|
115
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Unknown node' }));
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
node.ws = ws;
|
|
119
|
+
node.status = 'online';
|
|
120
|
+
node.lastSeen = new Date().toISOString();
|
|
121
|
+
this.engine.storage.db.prepare("UPDATE nodes SET status = 'online', last_seen = datetime('now') WHERE id = ?").run(msg.nodeId);
|
|
122
|
+
ws.send(JSON.stringify({ type: 'auth_ok', nodeId: msg.nodeId }));
|
|
123
|
+
logger.info('nodes', `Node reconnected: ${node.info.name}`);
|
|
124
|
+
return msg.nodeId;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Command response
|
|
128
|
+
if (type === 'response') {
|
|
129
|
+
const node = this.nodes.get(msg.nodeId);
|
|
130
|
+
if (node) {
|
|
131
|
+
const pending = node.pendingCommands?.get(msg.commandId);
|
|
132
|
+
if (pending) {
|
|
133
|
+
pending.resolve(msg.data);
|
|
134
|
+
node.pendingCommands.delete(msg.commandId);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return msg.nodeId;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Status update
|
|
141
|
+
if (type === 'status') {
|
|
142
|
+
const node = this.nodes.get(msg.nodeId);
|
|
143
|
+
if (node) {
|
|
144
|
+
node.lastSeen = new Date().toISOString();
|
|
145
|
+
if (msg.data) {
|
|
146
|
+
node.info = { ...node.info, ...msg.data };
|
|
147
|
+
}
|
|
148
|
+
this.engine.storage.db.prepare("UPDATE nodes SET last_seen = datetime('now') WHERE id = ?").run(msg.nodeId);
|
|
149
|
+
}
|
|
150
|
+
return msg.nodeId;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Pairing ──
|
|
157
|
+
|
|
158
|
+
_handlePair(ws, msg) {
|
|
159
|
+
const token = msg.token;
|
|
160
|
+
const pending = this.pendingPairs.get(token);
|
|
161
|
+
|
|
162
|
+
if (!pending) {
|
|
163
|
+
ws.send(JSON.stringify({ type: 'pair_error', message: 'Invalid or expired token' }));
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const nodeId = 'node_' + crypto.randomBytes(4).toString('hex');
|
|
168
|
+
|
|
169
|
+
// Register node
|
|
170
|
+
const info = {
|
|
171
|
+
id: nodeId,
|
|
172
|
+
agent_id: pending.agentId,
|
|
173
|
+
name: pending.name || msg.name || 'Device',
|
|
174
|
+
type: msg.deviceType || 'generic',
|
|
175
|
+
platform: msg.platform || 'unknown',
|
|
176
|
+
capabilities: JSON.stringify(msg.capabilities || []),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
this.engine.storage.db.prepare(
|
|
180
|
+
'INSERT INTO nodes (id, agent_id, name, type, platform, capabilities, status) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
181
|
+
).run(info.id, info.agent_id, info.name, info.type, info.platform, info.capabilities, 'online');
|
|
182
|
+
|
|
183
|
+
this.nodes.set(nodeId, {
|
|
184
|
+
info,
|
|
185
|
+
ws,
|
|
186
|
+
status: 'online',
|
|
187
|
+
lastSeen: new Date().toISOString(),
|
|
188
|
+
pendingCommands: new Map(),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
this.pendingPairs.delete(token);
|
|
192
|
+
|
|
193
|
+
ws.send(JSON.stringify({ type: 'paired', nodeId, name: info.name }));
|
|
194
|
+
logger.info('nodes', `New node paired: ${info.name} (${nodeId}) type=${info.type}`);
|
|
195
|
+
|
|
196
|
+
return nodeId;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Generate pairing token ──
|
|
200
|
+
|
|
201
|
+
generatePairToken(agentId, name = 'Device') {
|
|
202
|
+
const token = crypto.randomBytes(3).toString('hex').toUpperCase(); // 6-char code
|
|
203
|
+
this.pendingPairs.set(token, {
|
|
204
|
+
agentId,
|
|
205
|
+
name,
|
|
206
|
+
createdAt: Date.now(),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Expire after 5 minutes
|
|
210
|
+
setTimeout(() => this.pendingPairs.delete(token), 300000);
|
|
211
|
+
|
|
212
|
+
return token;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Send command to node ──
|
|
216
|
+
|
|
217
|
+
async sendCommand(nodeId, command, data = {}, timeout = 30000) {
|
|
218
|
+
const node = this.nodes.get(nodeId);
|
|
219
|
+
if (!node) throw new Error('Node not found: ' + nodeId);
|
|
220
|
+
if (!node.ws || node.status !== 'online') throw new Error('Node offline: ' + (node.info?.name || nodeId));
|
|
221
|
+
|
|
222
|
+
const commandId = 'cmd_' + Date.now().toString(36);
|
|
223
|
+
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const timer = setTimeout(() => {
|
|
226
|
+
node.pendingCommands?.delete(commandId);
|
|
227
|
+
reject(new Error('Command timeout'));
|
|
228
|
+
}, timeout);
|
|
229
|
+
|
|
230
|
+
node.pendingCommands = node.pendingCommands || new Map();
|
|
231
|
+
node.pendingCommands.set(commandId, {
|
|
232
|
+
resolve: (data) => { clearTimeout(timer); resolve(data); },
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
node.ws.send(JSON.stringify({
|
|
236
|
+
type: 'command',
|
|
237
|
+
commandId,
|
|
238
|
+
command,
|
|
239
|
+
data,
|
|
240
|
+
}));
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── High-level commands ──
|
|
245
|
+
|
|
246
|
+
async camera(nodeId, options = {}) {
|
|
247
|
+
return this.sendCommand(nodeId, 'camera', {
|
|
248
|
+
facing: options.facing || 'back',
|
|
249
|
+
...options,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async screenshot(nodeId) {
|
|
254
|
+
return this.sendCommand(nodeId, 'screenshot');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async location(nodeId) {
|
|
258
|
+
return this.sendCommand(nodeId, 'location');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async notify(nodeId, title, body) {
|
|
262
|
+
return this.sendCommand(nodeId, 'notify', { title, body });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async run(nodeId, command) {
|
|
266
|
+
return this.sendCommand(nodeId, 'run', { command }, 60000);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async screenRecord(nodeId, durationMs = 10000) {
|
|
270
|
+
return this.sendCommand(nodeId, 'screen_record', { durationMs }, durationMs + 10000);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── List/Status ──
|
|
274
|
+
|
|
275
|
+
list(agentId) {
|
|
276
|
+
return this.engine.storage.db.prepare(
|
|
277
|
+
'SELECT * FROM nodes WHERE agent_id = ? ORDER BY name'
|
|
278
|
+
).all(agentId).map(n => {
|
|
279
|
+
const live = this.nodes.get(n.id);
|
|
280
|
+
return {
|
|
281
|
+
...n,
|
|
282
|
+
capabilities: JSON.parse(n.capabilities || '[]'),
|
|
283
|
+
status: live?.status || 'offline',
|
|
284
|
+
lastSeen: live?.lastSeen || n.last_seen,
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
getNode(nodeId) {
|
|
290
|
+
const db = this.engine.storage.db.prepare('SELECT * FROM nodes WHERE id = ?').get(nodeId);
|
|
291
|
+
if (!db) return null;
|
|
292
|
+
const live = this.nodes.get(nodeId);
|
|
293
|
+
return { ...db, status: live?.status || 'offline', capabilities: JSON.parse(db.capabilities || '[]') };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
removeNode(nodeId) {
|
|
297
|
+
const node = this.nodes.get(nodeId);
|
|
298
|
+
if (node?.ws) {
|
|
299
|
+
node.ws.send(JSON.stringify({ type: 'unpaired' }));
|
|
300
|
+
node.ws.close();
|
|
301
|
+
}
|
|
302
|
+
this.nodes.delete(nodeId);
|
|
303
|
+
this.engine.storage.db.prepare('DELETE FROM nodes WHERE id = ?').run(nodeId);
|
|
304
|
+
logger.info('nodes', `Removed node: ${nodeId}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── Cleanup ──
|
|
308
|
+
|
|
309
|
+
stop() {
|
|
310
|
+
if (this.wsServer) {
|
|
311
|
+
this.wsServer.close();
|
|
312
|
+
logger.info('nodes', 'WebSocket server stopped');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -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,96 @@ export async function commandsMiddleware(ctx, next) {
|
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
if (cmd === '/nodes' || cmd === '/devices') {
|
|
149
|
+
if (!ctx.engine.nodeManager) { await ctx.reply('❌ Nodes not available'); return; }
|
|
150
|
+
const args = msg.split(/\s+/).slice(1);
|
|
151
|
+
const sub = args[0];
|
|
152
|
+
|
|
153
|
+
if (!sub || sub === 'list') {
|
|
154
|
+
const nodes = ctx.engine.nodeManager.list(ctx.agentId);
|
|
155
|
+
if (nodes.length === 0) {
|
|
156
|
+
await ctx.reply('📡 No paired devices\n\nUse /nodes pair <name> to add one');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const lines = nodes.map(n =>
|
|
160
|
+
(n.status === 'online' ? '🟢' : '🔴') + ' *' + n.name + '* (' + n.type + ')\n ' + n.platform + ' · ' + n.id
|
|
161
|
+
);
|
|
162
|
+
await ctx.reply('📡 *Paired Devices*\n\n' + lines.join('\n\n'));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (sub === 'pair') {
|
|
167
|
+
const name = args.slice(1).join(' ') || 'My Device';
|
|
168
|
+
const token = ctx.engine.nodeManager.generatePairToken(ctx.agentId, name);
|
|
169
|
+
await ctx.reply('📡 *Pair: ' + name + '*\n\nCode: `' + token + '`\n\nEnter this on the device.\nExpires in 5 minutes.\n\nWebSocket: ws://' + (ctx.engine.config.api?.host || '0.0.0.0') + ':9501/nodes');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (sub === 'remove') {
|
|
174
|
+
const id = args[1];
|
|
175
|
+
if (!id) { await ctx.reply('Usage: /nodes remove <id>'); return; }
|
|
176
|
+
ctx.engine.nodeManager.removeNode(id);
|
|
177
|
+
await ctx.reply('✅ Device removed');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (sub === 'notify') {
|
|
182
|
+
const id = args[1];
|
|
183
|
+
const message = args.slice(2).join(' ');
|
|
184
|
+
if (!id || !message) { await ctx.reply('Usage: /nodes notify <id> <message>'); return; }
|
|
185
|
+
try {
|
|
186
|
+
await ctx.engine.nodeManager.notify(id, 'Squidclaw', message);
|
|
187
|
+
await ctx.reply('📨 Notification sent');
|
|
188
|
+
} catch (err) { await ctx.reply('❌ ' + err.message); }
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await ctx.reply('📡 *Node Commands*\n\n/nodes — list devices\n/nodes pair <name> — generate pairing code\n/nodes remove <id> — unpair\n/nodes notify <id> <msg> — send notification');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (cmd === '/sandbox') {
|
|
197
|
+
if (!ctx.engine.sandbox) { await ctx.reply('❌ Sandbox not available'); return; }
|
|
198
|
+
const args = msg.slice(9).trim();
|
|
199
|
+
|
|
200
|
+
if (!args || args === 'stats') {
|
|
201
|
+
const stats = ctx.engine.sandbox.getStats();
|
|
202
|
+
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 ? '✅' : '🚫'));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (args === 'files') {
|
|
207
|
+
const files = ctx.engine.sandbox.listFiles();
|
|
208
|
+
if (files.length === 0) { await ctx.reply('📁 Sandbox is empty'); return; }
|
|
209
|
+
const lines = files.map(f => (f.type === 'dir' ? '📁 ' : '📄 ') + f.name + (f.size ? ' (' + (f.size / 1024).toFixed(1) + ' KB)' : ''));
|
|
210
|
+
await ctx.reply('📁 *Sandbox Files*\n\n' + lines.join('\n'));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (args === 'clean') {
|
|
215
|
+
ctx.engine.sandbox.cleanup();
|
|
216
|
+
await ctx.reply('🧹 Sandbox cleaned');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (args.startsWith('js ')) {
|
|
221
|
+
const code = args.slice(3);
|
|
222
|
+
const result = ctx.engine.sandbox.evalJS(code);
|
|
223
|
+
await ctx.reply(result.success ? '```\n' + (result.output || '(no output)') + '\n```' : '❌ ' + result.error);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (args.startsWith('py ')) {
|
|
228
|
+
const code = args.slice(3);
|
|
229
|
+
const result = await ctx.engine.sandbox.runPython(code);
|
|
230
|
+
await ctx.reply(result.success ? '```\n' + (result.output || '(no output)') + '\n```' : '❌ ' + (result.error || 'Failed'));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
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');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
148
238
|
if (cmd === '/plugins' || cmd === '/plugin') {
|
|
149
239
|
if (!ctx.engine.plugins) { await ctx.reply('❌ Plugin system not available'); return; }
|
|
150
240
|
const args = msg.split(/\s+/).slice(1);
|
package/lib/tools/router.js
CHANGED
|
@@ -169,6 +169,30 @@ 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('', '### Pair Device',
|
|
173
|
+
'---TOOL:node_pair:device name---',
|
|
174
|
+
'Generate a pairing code for a new device (phone, PC, server).',
|
|
175
|
+
'', '### List Devices',
|
|
176
|
+
'---TOOL:node_list:all---',
|
|
177
|
+
'Show all paired devices and their status.',
|
|
178
|
+
'', '### Send to Device',
|
|
179
|
+
'---TOOL:node_cmd:node_id|command|data---',
|
|
180
|
+
'Send a command to a paired device. Commands: camera, screenshot, location, notify, run.',
|
|
181
|
+
'Example: ---TOOL:node_cmd:node_abc123|notify|Meeting in 5 minutes!---',
|
|
182
|
+
'', '### Device Camera',
|
|
183
|
+
'---TOOL:node_camera:node_id---',
|
|
184
|
+
'Take a photo from a paired device camera.',
|
|
185
|
+
'', '### Device Location',
|
|
186
|
+
'---TOOL:node_location:node_id---',
|
|
187
|
+
'Get GPS location of a paired device.');
|
|
188
|
+
|
|
189
|
+
tools.push('', '### Run JavaScript (Sandboxed)',
|
|
190
|
+
'---TOOL:js:console.log(2 + 2)---',
|
|
191
|
+
'Execute JavaScript in a secure VM sandbox. No network, no filesystem, no require. Safe for math, logic, data processing.',
|
|
192
|
+
'', '### Sandbox Info',
|
|
193
|
+
'---TOOL:sandbox_info:stats---',
|
|
194
|
+
'Show sandbox stats (files, size, limits).');
|
|
195
|
+
|
|
172
196
|
tools.push('', '### Run Command',
|
|
173
197
|
'---TOOL:exec:ls -la---',
|
|
174
198
|
'Execute a shell command. Output is returned. Sandboxed for safety.',
|
|
@@ -611,10 +635,15 @@ export class ToolRouter {
|
|
|
611
635
|
case 'exec':
|
|
612
636
|
case 'shell':
|
|
613
637
|
case 'run': {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
638
|
+
if (this._engine?.sandbox) {
|
|
639
|
+
const result = await this._engine.sandbox.runShell(toolArg);
|
|
640
|
+
toolResult = result.success ? (result.output || '(no output)') : 'Error: ' + (result.error || 'Unknown');
|
|
641
|
+
} else {
|
|
642
|
+
const { ShellTool } = await import('./shell.js');
|
|
643
|
+
const sh = new ShellTool();
|
|
644
|
+
const result = sh.exec(toolArg);
|
|
645
|
+
toolResult = result.error ? 'Error: ' + result.error : result.output || '(no output)';
|
|
646
|
+
}
|
|
618
647
|
break;
|
|
619
648
|
}
|
|
620
649
|
case 'readfile': {
|
|
@@ -643,14 +672,95 @@ export class ToolRouter {
|
|
|
643
672
|
toolResult = result.error || result.files.map(f => (f.type === 'dir' ? '📁 ' : '📄 ') + f.name).join('\n');
|
|
644
673
|
break;
|
|
645
674
|
}
|
|
675
|
+
case 'js':
|
|
676
|
+
case 'javascript':
|
|
677
|
+
case 'eval': {
|
|
678
|
+
if (this._engine?.sandbox) {
|
|
679
|
+
const result = this._engine.sandbox.evalJS(toolArg);
|
|
680
|
+
toolResult = result.success ? (result.output || '(no output)') : 'Error: ' + (result.error || 'Unknown');
|
|
681
|
+
} else {
|
|
682
|
+
toolResult = 'Sandbox not available';
|
|
683
|
+
}
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
646
686
|
case 'python':
|
|
647
687
|
case 'py': {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
688
|
+
if (this._engine?.sandbox) {
|
|
689
|
+
const result = await this._engine.sandbox.runPython(toolArg);
|
|
690
|
+
toolResult = result.success ? (result.output || '(no output)') : 'Error: ' + (result.error || 'Unknown');
|
|
691
|
+
} else {
|
|
692
|
+
const { ShellTool } = await import('./shell.js');
|
|
693
|
+
const sh = new ShellTool();
|
|
694
|
+
sh.writeFile('_run.py', toolArg);
|
|
695
|
+
const result = sh.exec('python3 _run.py');
|
|
696
|
+
toolResult = result.error ? 'Error: ' + result.error : result.output || '(no output)';
|
|
697
|
+
}
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
case 'node_pair': {
|
|
701
|
+
if (!this._engine?.nodeManager) { toolResult = 'Nodes not available'; break; }
|
|
702
|
+
const token = this._engine.nodeManager.generatePairToken(agentId, toolArg || 'Device');
|
|
703
|
+
toolResult = '📡 Pairing code: *' + token + '*\n\nEnter this code on the device to pair.\nExpires in 5 minutes.\n\nConnect to: ws://<server>:9501/nodes';
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
case 'node_list': {
|
|
707
|
+
if (!this._engine?.nodeManager) { toolResult = 'Nodes not available'; break; }
|
|
708
|
+
const nodes = this._engine.nodeManager.list(agentId);
|
|
709
|
+
if (nodes.length === 0) { toolResult = 'No paired devices. Use node_pair to add one.'; break; }
|
|
710
|
+
toolResult = nodes.map(n =>
|
|
711
|
+
(n.status === 'online' ? '🟢' : '🔴') + ' ' + n.name + ' (' + n.type + ')\n ID: ' + n.id + '\n Platform: ' + n.platform + '\n Last seen: ' + (n.lastSeen || 'never')
|
|
712
|
+
).join('\n\n');
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
case 'node_cmd': {
|
|
716
|
+
if (!this._engine?.nodeManager) { toolResult = 'Nodes not available'; break; }
|
|
717
|
+
try {
|
|
718
|
+
const parts = toolArg.split('|').map(p => p.trim());
|
|
719
|
+
const [nodeId, command, ...dataParts] = parts;
|
|
720
|
+
const data = dataParts.join('|');
|
|
721
|
+
|
|
722
|
+
if (command === 'notify') {
|
|
723
|
+
await this._engine.nodeManager.notify(nodeId, 'Squidclaw', data);
|
|
724
|
+
toolResult = '📨 Notification sent to device';
|
|
725
|
+
} else if (command === 'run') {
|
|
726
|
+
const result = await this._engine.nodeManager.run(nodeId, data);
|
|
727
|
+
toolResult = 'Output: ' + JSON.stringify(result);
|
|
728
|
+
} else {
|
|
729
|
+
const result = await this._engine.nodeManager.sendCommand(nodeId, command, { data });
|
|
730
|
+
toolResult = JSON.stringify(result);
|
|
731
|
+
}
|
|
732
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
case 'node_camera': {
|
|
736
|
+
if (!this._engine?.nodeManager) { toolResult = 'Nodes not available'; break; }
|
|
737
|
+
try {
|
|
738
|
+
const result = await this._engine.nodeManager.camera(toolArg.trim());
|
|
739
|
+
toolResult = 'Camera captured: ' + JSON.stringify(result);
|
|
740
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
case 'node_location': {
|
|
744
|
+
if (!this._engine?.nodeManager) { toolResult = 'Nodes not available'; break; }
|
|
745
|
+
try {
|
|
746
|
+
const result = await this._engine.nodeManager.location(toolArg.trim());
|
|
747
|
+
toolResult = 'Location: ' + JSON.stringify(result);
|
|
748
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
case 'sandbox_info': {
|
|
752
|
+
if (this._engine?.sandbox) {
|
|
753
|
+
const stats = this._engine.sandbox.getStats();
|
|
754
|
+
toolResult = '🔒 Sandbox Stats:\n' +
|
|
755
|
+
'📁 Files: ' + stats.files + '/' + stats.maxFiles + '\n' +
|
|
756
|
+
'💾 Size: ' + (stats.totalSize / 1024).toFixed(0) + ' KB\n' +
|
|
757
|
+
'⏱️ Timeout: ' + (stats.timeout / 1000) + 's\n' +
|
|
758
|
+
'🧠 Max memory: ' + stats.maxMemory + ' MB\n' +
|
|
759
|
+
'🌐 Network: ' + (stats.allowNetwork ? 'allowed' : 'blocked') + '\n' +
|
|
760
|
+
'📂 Jail: ' + stats.jailDir;
|
|
761
|
+
} else {
|
|
762
|
+
toolResult = 'Sandbox not available';
|
|
763
|
+
}
|
|
654
764
|
break;
|
|
655
765
|
}
|
|
656
766
|
case 'spawn': {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squidclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "🦑 AI agent platform — human-like agents for WhatsApp, Telegram & more",
|
|
5
5
|
"main": "lib/engine.js",
|
|
6
6
|
"bin": {
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"qrcode-terminal": "^0.12.0",
|
|
57
57
|
"sharp": "^0.34.5",
|
|
58
58
|
"undici": "^7.22.0",
|
|
59
|
+
"ws": "^8.19.0",
|
|
59
60
|
"yaml": "^2.8.2",
|
|
60
61
|
"zod": "^4.3.6"
|
|
61
62
|
}
|