squad-openclaw 2026.2.1906 → 2026.2.2002
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/README.md +39 -6
- package/dist/index.js +743 -665
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/entities.ts
|
|
2
2
|
import { Type as T } from "@sinclair/typebox";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import path3 from "path";
|
|
4
|
+
import fs3 from "fs";
|
|
5
5
|
|
|
6
6
|
// src/watcher.ts
|
|
7
7
|
import path from "path";
|
|
@@ -255,517 +255,154 @@ function startWatcher(configDir, onFsChange) {
|
|
|
255
255
|
};
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
// src/
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
]
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const name = match?.[1]?.trim();
|
|
287
|
-
if (!name) return null;
|
|
288
|
-
if (/^_\(.+\)_$/.test(name)) return null;
|
|
289
|
-
return name;
|
|
258
|
+
// src/filesystem.ts
|
|
259
|
+
import fs2 from "fs";
|
|
260
|
+
import path2 from "path";
|
|
261
|
+
var HOME_DIR = process.env.HOME ?? "/root";
|
|
262
|
+
var OPENCLAW_DIR = path2.join(HOME_DIR, ".openclaw");
|
|
263
|
+
var SENSITIVE_BLOCKED_DIRS = [
|
|
264
|
+
path2.join(OPENCLAW_DIR, "credentials"),
|
|
265
|
+
path2.join(OPENCLAW_DIR, "devices"),
|
|
266
|
+
path2.join(OPENCLAW_DIR, "identity")
|
|
267
|
+
];
|
|
268
|
+
var SENSITIVE_BLOCKED_FILES = [
|
|
269
|
+
path2.join(OPENCLAW_DIR, "squad-ceo-data", "squad-relay.json")
|
|
270
|
+
];
|
|
271
|
+
function isSensitivePath(resolvedPath) {
|
|
272
|
+
for (const blocked of SENSITIVE_BLOCKED_DIRS) {
|
|
273
|
+
if (resolvedPath === blocked || resolvedPath.startsWith(blocked + path2.sep)) {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (const blocked of SENSITIVE_BLOCKED_FILES) {
|
|
278
|
+
if (resolvedPath === blocked) {
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (path2.dirname(resolvedPath) === OPENCLAW_DIR && resolvedPath.endsWith(".bak")) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
290
286
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
let
|
|
287
|
+
var OPENCLAW_JSON_FILENAME = "openclaw.json";
|
|
288
|
+
function redactOpenclawJson(rawContent) {
|
|
289
|
+
let config;
|
|
294
290
|
try {
|
|
295
|
-
|
|
291
|
+
config = JSON.parse(rawContent);
|
|
296
292
|
} catch {
|
|
297
|
-
return;
|
|
293
|
+
return rawContent;
|
|
298
294
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
try {
|
|
309
|
-
const content = fs2.readFileSync(identityPath, "utf-8");
|
|
310
|
-
const parsed = parseIdentityName(content);
|
|
311
|
-
if (parsed) name = parsed;
|
|
312
|
-
} catch {
|
|
295
|
+
let redactedCount = 0;
|
|
296
|
+
const channels = config.channels;
|
|
297
|
+
if (channels && typeof channels === "object") {
|
|
298
|
+
for (const channelKey of Object.keys(channels)) {
|
|
299
|
+
const channel = channels[channelKey];
|
|
300
|
+
if (channel && typeof channel === "object" && "botToken" in channel) {
|
|
301
|
+
channel.botToken = "[REDACTED]";
|
|
302
|
+
redactedCount++;
|
|
303
|
+
}
|
|
313
304
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (config.skills) metadata.skills = config.skills;
|
|
323
|
-
} catch {
|
|
305
|
+
}
|
|
306
|
+
const gateway = config.gateway;
|
|
307
|
+
if (gateway && typeof gateway === "object") {
|
|
308
|
+
if (gateway.auth && typeof gateway.auth === "object") {
|
|
309
|
+
const auth = gateway.auth;
|
|
310
|
+
for (const key of Object.keys(auth)) {
|
|
311
|
+
auth[key] = "[REDACTED]";
|
|
312
|
+
redactedCount++;
|
|
324
313
|
}
|
|
325
314
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
created_at: now,
|
|
336
|
-
updated_at: now
|
|
337
|
-
});
|
|
315
|
+
if ("token" in gateway) {
|
|
316
|
+
gateway.token = "[REDACTED]";
|
|
317
|
+
redactedCount++;
|
|
318
|
+
}
|
|
319
|
+
const remote = gateway.remote;
|
|
320
|
+
if (remote && typeof remote === "object" && "token" in remote) {
|
|
321
|
+
remote.token = "[REDACTED]";
|
|
322
|
+
redactedCount++;
|
|
323
|
+
}
|
|
338
324
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const now = Date.now();
|
|
342
|
-
const globalSkillsDir = path2.join(configDir, "skills");
|
|
343
|
-
scanSkillsDir(globalSkillsDir, "global", now);
|
|
344
|
-
let entries;
|
|
345
|
-
try {
|
|
346
|
-
entries = fs2.readdirSync(configDir, { withFileTypes: true });
|
|
347
|
-
} catch {
|
|
348
|
-
return;
|
|
325
|
+
if (redactedCount > 0) {
|
|
326
|
+
console.log(`[security] Redacted ${redactedCount} sensitive field(s) from openclaw.json before returning to client`);
|
|
349
327
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
328
|
+
return JSON.stringify(config, null, 2);
|
|
329
|
+
}
|
|
330
|
+
function isOpenclawJson(resolvedPath) {
|
|
331
|
+
return path2.basename(resolvedPath) === OPENCLAW_JSON_FILENAME && resolvedPath.startsWith(OPENCLAW_DIR);
|
|
332
|
+
}
|
|
333
|
+
function expandHome(p) {
|
|
334
|
+
if (p.startsWith("~/") || p === "~") {
|
|
335
|
+
return path2.join(HOME_DIR, p.slice(1));
|
|
357
336
|
}
|
|
337
|
+
return p;
|
|
358
338
|
}
|
|
359
|
-
function
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
return;
|
|
339
|
+
function validatePath(p, allowedRoots) {
|
|
340
|
+
const resolved = path2.resolve(expandHome(p));
|
|
341
|
+
if (!allowedRoots || allowedRoots.length === 0) return resolved;
|
|
342
|
+
const allowed = allowedRoots.some((root) => {
|
|
343
|
+
const resolvedRoot = path2.resolve(expandHome(root));
|
|
344
|
+
return resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path2.sep);
|
|
345
|
+
});
|
|
346
|
+
if (!allowed) {
|
|
347
|
+
throw new Error(`Path "${p}" is outside allowed roots`);
|
|
365
348
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
path2.join(skillPath, manifestName),
|
|
375
|
-
"utf-8"
|
|
376
|
-
);
|
|
377
|
-
const manifest = JSON.parse(raw);
|
|
378
|
-
if (manifest.name) name = manifest.name;
|
|
379
|
-
break;
|
|
380
|
-
} catch {
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
const entityId = scope === "global" ? `skill:${skillKey}` : `skill:${scope}:${skillKey}`;
|
|
385
|
-
registrySet({
|
|
386
|
-
id: entityId,
|
|
387
|
-
type: "skill",
|
|
388
|
-
name,
|
|
389
|
-
title: name,
|
|
390
|
-
description: null,
|
|
391
|
-
metadata: { skillKey, scope, skillPath },
|
|
392
|
-
source: "filesystem",
|
|
393
|
-
source_key: skillPath,
|
|
394
|
-
created_at: now,
|
|
395
|
-
updated_at: now
|
|
396
|
-
});
|
|
349
|
+
return resolved;
|
|
350
|
+
}
|
|
351
|
+
function validateAndBlockSensitive(p, allowedRoots) {
|
|
352
|
+
const resolved = validatePath(p, allowedRoots);
|
|
353
|
+
if (isSensitivePath(resolved)) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`Access denied: path "${p}" is inside a protected directory (credentials/devices/identity)`
|
|
356
|
+
);
|
|
397
357
|
}
|
|
358
|
+
return resolved;
|
|
398
359
|
}
|
|
399
|
-
function
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
} catch {
|
|
406
|
-
return;
|
|
360
|
+
function validateWritePath(p, allowedRoots) {
|
|
361
|
+
const resolved = validateAndBlockSensitive(p, allowedRoots);
|
|
362
|
+
if (isOpenclawJson(resolved)) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`Write denied: "${p}" is a protected configuration file (openclaw.json)`
|
|
365
|
+
);
|
|
407
366
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
367
|
+
return resolved;
|
|
368
|
+
}
|
|
369
|
+
function ok(data) {
|
|
370
|
+
return {
|
|
371
|
+
content: [{ type: "text", text: JSON.stringify(data) }]
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function err(message) {
|
|
375
|
+
return {
|
|
376
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
377
|
+
isError: true
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function listDir(dirPath, opts) {
|
|
381
|
+
const dirents = fs2.readdirSync(dirPath, { withFileTypes: true });
|
|
382
|
+
const results = [];
|
|
383
|
+
for (const dirent of dirents) {
|
|
384
|
+
if (!opts.includeHidden && dirent.name.startsWith(".")) continue;
|
|
385
|
+
const entryPath = path2.join(dirPath, dirent.name);
|
|
386
|
+
let type = "other";
|
|
387
|
+
if (dirent.isFile()) type = "file";
|
|
388
|
+
else if (dirent.isDirectory()) type = "directory";
|
|
389
|
+
else if (dirent.isSymbolicLink()) type = "symlink";
|
|
390
|
+
const entry = { name: dirent.name, path: entryPath, type };
|
|
412
391
|
try {
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const name = manifest.name || pluginId;
|
|
417
|
-
registrySet({
|
|
418
|
-
id: `plugin:${pluginId}`,
|
|
419
|
-
type: "plugin",
|
|
420
|
-
name,
|
|
421
|
-
title: name,
|
|
422
|
-
description: manifest.description || null,
|
|
423
|
-
metadata: { pluginId, pluginDir },
|
|
424
|
-
source: "filesystem",
|
|
425
|
-
source_key: manifestPath,
|
|
426
|
-
created_at: now,
|
|
427
|
-
updated_at: now
|
|
428
|
-
});
|
|
392
|
+
const stat = fs2.statSync(entryPath);
|
|
393
|
+
entry.size = stat.size;
|
|
394
|
+
entry.modified = stat.mtime.toISOString();
|
|
429
395
|
} catch {
|
|
430
396
|
}
|
|
397
|
+
if (type === "directory" && opts.recursive && opts.depth < opts.maxDepth) {
|
|
398
|
+
try {
|
|
399
|
+
entry.children = listDir(entryPath, { ...opts, depth: opts.depth + 1 });
|
|
400
|
+
} catch {
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
results.push(entry);
|
|
431
404
|
}
|
|
432
|
-
|
|
433
|
-
function scanTools(configDir) {
|
|
434
|
-
const now = Date.now();
|
|
435
|
-
try {
|
|
436
|
-
const raw = fs2.readFileSync(
|
|
437
|
-
path2.join(configDir, "openclaw.json"),
|
|
438
|
-
"utf-8"
|
|
439
|
-
);
|
|
440
|
-
const config = JSON.parse(raw);
|
|
441
|
-
const allowedTools = config?.tools?.allow ?? [];
|
|
442
|
-
for (const toolName of allowedTools) {
|
|
443
|
-
registrySet({
|
|
444
|
-
id: `tool:${toolName}`,
|
|
445
|
-
type: "tool",
|
|
446
|
-
name: toolName,
|
|
447
|
-
title: toolName,
|
|
448
|
-
description: null,
|
|
449
|
-
metadata: { tool_name: toolName },
|
|
450
|
-
source: "filesystem",
|
|
451
|
-
source_key: "openclaw.json:tools.allow",
|
|
452
|
-
created_at: now,
|
|
453
|
-
updated_at: now
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
} catch {
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
var MIME_MAP = {
|
|
460
|
-
".png": "image/png",
|
|
461
|
-
".jpg": "image/jpeg",
|
|
462
|
-
".jpeg": "image/jpeg",
|
|
463
|
-
".gif": "image/gif",
|
|
464
|
-
".webp": "image/webp",
|
|
465
|
-
".svg": "image/svg+xml",
|
|
466
|
-
".bmp": "image/bmp",
|
|
467
|
-
".ico": "image/x-icon",
|
|
468
|
-
".mp4": "video/mp4",
|
|
469
|
-
".webm": "video/webm",
|
|
470
|
-
".mov": "video/quicktime",
|
|
471
|
-
".avi": "video/x-msvideo",
|
|
472
|
-
".mkv": "video/x-matroska",
|
|
473
|
-
".mp3": "audio/mpeg",
|
|
474
|
-
".wav": "audio/wav",
|
|
475
|
-
".ogg": "audio/ogg",
|
|
476
|
-
".flac": "audio/flac",
|
|
477
|
-
".aac": "audio/aac",
|
|
478
|
-
".pdf": "application/pdf",
|
|
479
|
-
".json": "application/json",
|
|
480
|
-
".txt": "text/plain",
|
|
481
|
-
".md": "text/markdown",
|
|
482
|
-
".csv": "text/csv",
|
|
483
|
-
".zip": "application/zip",
|
|
484
|
-
".tar": "application/x-tar",
|
|
485
|
-
".gz": "application/gzip"
|
|
486
|
-
};
|
|
487
|
-
function getMimeType(filename) {
|
|
488
|
-
const ext = path2.extname(filename).toLowerCase();
|
|
489
|
-
return MIME_MAP[ext] ?? "application/octet-stream";
|
|
490
|
-
}
|
|
491
|
-
function scanMedia(configDir) {
|
|
492
|
-
const now = Date.now();
|
|
493
|
-
const mediaDir = path2.join(configDir, "media");
|
|
494
|
-
let entries;
|
|
495
|
-
try {
|
|
496
|
-
entries = fs2.readdirSync(mediaDir, { withFileTypes: true });
|
|
497
|
-
} catch {
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
for (const entry of entries) {
|
|
501
|
-
if (!entry.isFile()) continue;
|
|
502
|
-
const filePath = path2.join(mediaDir, entry.name);
|
|
503
|
-
const mimeType = getMimeType(entry.name);
|
|
504
|
-
let size;
|
|
505
|
-
let mtime = now;
|
|
506
|
-
try {
|
|
507
|
-
const stat = fs2.statSync(filePath);
|
|
508
|
-
size = stat.size;
|
|
509
|
-
mtime = stat.mtimeMs;
|
|
510
|
-
} catch {
|
|
511
|
-
}
|
|
512
|
-
registrySet({
|
|
513
|
-
id: `asset:${entry.name}`,
|
|
514
|
-
type: "asset",
|
|
515
|
-
name: entry.name,
|
|
516
|
-
title: entry.name,
|
|
517
|
-
description: null,
|
|
518
|
-
metadata: { path: filePath, size, mime_type: mimeType, original_name: entry.name },
|
|
519
|
-
source: "filesystem",
|
|
520
|
-
source_key: filePath,
|
|
521
|
-
created_at: mtime,
|
|
522
|
-
updated_at: mtime
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
function fullScan(configDir) {
|
|
527
|
-
registry.clear();
|
|
528
|
-
scanAgents(configDir);
|
|
529
|
-
scanSkills(configDir);
|
|
530
|
-
scanPlugins2(configDir);
|
|
531
|
-
scanTools(configDir);
|
|
532
|
-
scanMedia(configDir);
|
|
533
|
-
}
|
|
534
|
-
function registerEntityTools(api, onFsChange) {
|
|
535
|
-
const configDir = process.env.HOME + "/.openclaw";
|
|
536
|
-
api.registerTool({
|
|
537
|
-
name: "entity_list",
|
|
538
|
-
description: "List all entities in the registry, optionally filtered by type. Returns lightweight entity data for @mention autocomplete.",
|
|
539
|
-
parameters: T.Object({
|
|
540
|
-
type: T.Optional(EntityType),
|
|
541
|
-
limit: T.Optional(
|
|
542
|
-
T.Number({ description: "Max results (default 500)" })
|
|
543
|
-
)
|
|
544
|
-
}),
|
|
545
|
-
async execute(_id, params, _ctx) {
|
|
546
|
-
const results = registryList(params.type);
|
|
547
|
-
const limit = params.limit ?? 500;
|
|
548
|
-
return {
|
|
549
|
-
content: [
|
|
550
|
-
{ type: "text", text: JSON.stringify(results.slice(0, limit)) }
|
|
551
|
-
]
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
});
|
|
555
|
-
api.registerTool({
|
|
556
|
-
name: "entity_search",
|
|
557
|
-
description: "Search entities by name/title substring match for @mention autocomplete.",
|
|
558
|
-
parameters: T.Object({
|
|
559
|
-
query: T.String({ description: "Search query text" }),
|
|
560
|
-
type: T.Optional(
|
|
561
|
-
T.String({ description: "Filter results by entity type" })
|
|
562
|
-
),
|
|
563
|
-
limit: T.Optional(
|
|
564
|
-
T.Number({ description: "Max results (default 20)" })
|
|
565
|
-
)
|
|
566
|
-
}),
|
|
567
|
-
async execute(_id, params, _ctx) {
|
|
568
|
-
const q = (params.query ?? "").toLowerCase();
|
|
569
|
-
const limit = params.limit ?? 20;
|
|
570
|
-
let results = Array.from(registry.values());
|
|
571
|
-
if (params.type) {
|
|
572
|
-
results = results.filter((e) => e.type === params.type);
|
|
573
|
-
}
|
|
574
|
-
if (q) {
|
|
575
|
-
results = results.filter(
|
|
576
|
-
(e) => e.name.toLowerCase().includes(q) || (e.title ?? "").toLowerCase().includes(q)
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
return {
|
|
580
|
-
content: [
|
|
581
|
-
{ type: "text", text: JSON.stringify(results.slice(0, limit)) }
|
|
582
|
-
]
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
});
|
|
586
|
-
api.registerTool({
|
|
587
|
-
name: "entity_sync",
|
|
588
|
-
description: "Re-scan the filesystem to refresh the entity registry. Call after configuration changes for immediate updates.",
|
|
589
|
-
parameters: T.Object({}),
|
|
590
|
-
async execute(_id, _params, _ctx) {
|
|
591
|
-
const before = registry.size;
|
|
592
|
-
fullScan(configDir);
|
|
593
|
-
return {
|
|
594
|
-
content: [
|
|
595
|
-
{
|
|
596
|
-
type: "text",
|
|
597
|
-
text: JSON.stringify({ synced: registry.size, previous: before })
|
|
598
|
-
}
|
|
599
|
-
]
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
});
|
|
603
|
-
try {
|
|
604
|
-
fullScan(configDir);
|
|
605
|
-
} catch (err2) {
|
|
606
|
-
console.error("[squad-openclaw] Initial scan failed:", err2);
|
|
607
|
-
}
|
|
608
|
-
let stopWatcher = null;
|
|
609
|
-
try {
|
|
610
|
-
stopWatcher = startWatcher(configDir, onFsChange);
|
|
611
|
-
} catch (err2) {
|
|
612
|
-
console.error("[squad-openclaw] Watcher failed to start:", err2);
|
|
613
|
-
}
|
|
614
|
-
const cleanup = () => {
|
|
615
|
-
stopWatcher?.();
|
|
616
|
-
};
|
|
617
|
-
process.on("SIGTERM", cleanup);
|
|
618
|
-
process.on("SIGINT", cleanup);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// src/filesystem.ts
|
|
622
|
-
import fs3 from "fs";
|
|
623
|
-
import path3 from "path";
|
|
624
|
-
var HOME_DIR = process.env.HOME ?? "/root";
|
|
625
|
-
var OPENCLAW_DIR = path3.join(HOME_DIR, ".openclaw");
|
|
626
|
-
var SENSITIVE_BLOCKED_DIRS = [
|
|
627
|
-
path3.join(OPENCLAW_DIR, "credentials"),
|
|
628
|
-
path3.join(OPENCLAW_DIR, "devices"),
|
|
629
|
-
path3.join(OPENCLAW_DIR, "identity")
|
|
630
|
-
];
|
|
631
|
-
var SENSITIVE_BLOCKED_FILES = [
|
|
632
|
-
path3.join(OPENCLAW_DIR, "squad-ceo-data", "squad-relay.json")
|
|
633
|
-
];
|
|
634
|
-
function isSensitivePath(resolvedPath) {
|
|
635
|
-
for (const blocked of SENSITIVE_BLOCKED_DIRS) {
|
|
636
|
-
if (resolvedPath === blocked || resolvedPath.startsWith(blocked + path3.sep)) {
|
|
637
|
-
return true;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
for (const blocked of SENSITIVE_BLOCKED_FILES) {
|
|
641
|
-
if (resolvedPath === blocked) {
|
|
642
|
-
return true;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
if (path3.dirname(resolvedPath) === OPENCLAW_DIR && resolvedPath.endsWith(".bak")) {
|
|
646
|
-
return true;
|
|
647
|
-
}
|
|
648
|
-
return false;
|
|
649
|
-
}
|
|
650
|
-
var OPENCLAW_JSON_FILENAME = "openclaw.json";
|
|
651
|
-
function redactOpenclawJson(rawContent) {
|
|
652
|
-
let config;
|
|
653
|
-
try {
|
|
654
|
-
config = JSON.parse(rawContent);
|
|
655
|
-
} catch {
|
|
656
|
-
return rawContent;
|
|
657
|
-
}
|
|
658
|
-
let redactedCount = 0;
|
|
659
|
-
const channels = config.channels;
|
|
660
|
-
if (channels && typeof channels === "object") {
|
|
661
|
-
for (const channelKey of Object.keys(channels)) {
|
|
662
|
-
const channel = channels[channelKey];
|
|
663
|
-
if (channel && typeof channel === "object" && "botToken" in channel) {
|
|
664
|
-
channel.botToken = "[REDACTED]";
|
|
665
|
-
redactedCount++;
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
const gateway = config.gateway;
|
|
670
|
-
if (gateway && typeof gateway === "object") {
|
|
671
|
-
if (gateway.auth && typeof gateway.auth === "object") {
|
|
672
|
-
const auth = gateway.auth;
|
|
673
|
-
for (const key of Object.keys(auth)) {
|
|
674
|
-
auth[key] = "[REDACTED]";
|
|
675
|
-
redactedCount++;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
if ("token" in gateway) {
|
|
679
|
-
gateway.token = "[REDACTED]";
|
|
680
|
-
redactedCount++;
|
|
681
|
-
}
|
|
682
|
-
const remote = gateway.remote;
|
|
683
|
-
if (remote && typeof remote === "object" && "token" in remote) {
|
|
684
|
-
remote.token = "[REDACTED]";
|
|
685
|
-
redactedCount++;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
if (redactedCount > 0) {
|
|
689
|
-
console.log(`[security] Redacted ${redactedCount} sensitive field(s) from openclaw.json before returning to client`);
|
|
690
|
-
}
|
|
691
|
-
return JSON.stringify(config, null, 2);
|
|
692
|
-
}
|
|
693
|
-
function isOpenclawJson(resolvedPath) {
|
|
694
|
-
return path3.basename(resolvedPath) === OPENCLAW_JSON_FILENAME && resolvedPath.startsWith(OPENCLAW_DIR);
|
|
695
|
-
}
|
|
696
|
-
function expandHome(p) {
|
|
697
|
-
if (p.startsWith("~/") || p === "~") {
|
|
698
|
-
return path3.join(HOME_DIR, p.slice(1));
|
|
699
|
-
}
|
|
700
|
-
return p;
|
|
701
|
-
}
|
|
702
|
-
function validatePath(p, allowedRoots) {
|
|
703
|
-
const resolved = path3.resolve(expandHome(p));
|
|
704
|
-
if (!allowedRoots || allowedRoots.length === 0) return resolved;
|
|
705
|
-
const allowed = allowedRoots.some((root) => {
|
|
706
|
-
const resolvedRoot = path3.resolve(expandHome(root));
|
|
707
|
-
return resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path3.sep);
|
|
708
|
-
});
|
|
709
|
-
if (!allowed) {
|
|
710
|
-
throw new Error(`Path "${p}" is outside allowed roots`);
|
|
711
|
-
}
|
|
712
|
-
return resolved;
|
|
713
|
-
}
|
|
714
|
-
function validateAndBlockSensitive(p, allowedRoots) {
|
|
715
|
-
const resolved = validatePath(p, allowedRoots);
|
|
716
|
-
if (isSensitivePath(resolved)) {
|
|
717
|
-
throw new Error(
|
|
718
|
-
`Access denied: path "${p}" is inside a protected directory (credentials/devices/identity)`
|
|
719
|
-
);
|
|
720
|
-
}
|
|
721
|
-
return resolved;
|
|
722
|
-
}
|
|
723
|
-
function validateWritePath(p, allowedRoots) {
|
|
724
|
-
const resolved = validateAndBlockSensitive(p, allowedRoots);
|
|
725
|
-
if (isOpenclawJson(resolved)) {
|
|
726
|
-
throw new Error(
|
|
727
|
-
`Write denied: "${p}" is a protected configuration file (openclaw.json)`
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
return resolved;
|
|
731
|
-
}
|
|
732
|
-
function ok(data) {
|
|
733
|
-
return {
|
|
734
|
-
content: [{ type: "text", text: JSON.stringify(data) }]
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
function err(message) {
|
|
738
|
-
return {
|
|
739
|
-
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
740
|
-
isError: true
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
function listDir(dirPath, opts) {
|
|
744
|
-
const dirents = fs3.readdirSync(dirPath, { withFileTypes: true });
|
|
745
|
-
const results = [];
|
|
746
|
-
for (const dirent of dirents) {
|
|
747
|
-
if (!opts.includeHidden && dirent.name.startsWith(".")) continue;
|
|
748
|
-
const entryPath = path3.join(dirPath, dirent.name);
|
|
749
|
-
let type = "other";
|
|
750
|
-
if (dirent.isFile()) type = "file";
|
|
751
|
-
else if (dirent.isDirectory()) type = "directory";
|
|
752
|
-
else if (dirent.isSymbolicLink()) type = "symlink";
|
|
753
|
-
const entry = { name: dirent.name, path: entryPath, type };
|
|
754
|
-
try {
|
|
755
|
-
const stat = fs3.statSync(entryPath);
|
|
756
|
-
entry.size = stat.size;
|
|
757
|
-
entry.modified = stat.mtime.toISOString();
|
|
758
|
-
} catch {
|
|
759
|
-
}
|
|
760
|
-
if (type === "directory" && opts.recursive && opts.depth < opts.maxDepth) {
|
|
761
|
-
try {
|
|
762
|
-
entry.children = listDir(entryPath, { ...opts, depth: opts.depth + 1 });
|
|
763
|
-
} catch {
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
results.push(entry);
|
|
767
|
-
}
|
|
768
|
-
return results;
|
|
405
|
+
return results;
|
|
769
406
|
}
|
|
770
407
|
function filterSensitiveEntries(entries) {
|
|
771
408
|
return entries.filter((entry) => !isSensitivePath(entry.path)).map((entry) => {
|
|
@@ -801,209 +438,592 @@ function registerFilesystemTools(api) {
|
|
|
801
438
|
try {
|
|
802
439
|
const filePath = validateAndBlockSensitive(params.path, allowedRoots);
|
|
803
440
|
const encoding = params.encoding ?? "utf-8";
|
|
804
|
-
let content =
|
|
805
|
-
const stat =
|
|
441
|
+
let content = fs2.readFileSync(filePath, encoding);
|
|
442
|
+
const stat = fs2.statSync(filePath);
|
|
806
443
|
if (isOpenclawJson(filePath) && encoding === "utf-8") {
|
|
807
444
|
content = redactOpenclawJson(content);
|
|
808
445
|
}
|
|
809
446
|
return ok({
|
|
810
|
-
path: filePath,
|
|
811
|
-
content,
|
|
812
|
-
size: stat.size,
|
|
813
|
-
modified: stat.mtime.toISOString()
|
|
447
|
+
path: filePath,
|
|
448
|
+
content,
|
|
449
|
+
size: stat.size,
|
|
450
|
+
modified: stat.mtime.toISOString()
|
|
451
|
+
});
|
|
452
|
+
} catch (e) {
|
|
453
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
454
|
+
return err(`fs_read failed: ${msg}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
api.registerTool({
|
|
459
|
+
name: "fs_write",
|
|
460
|
+
label: "Write File",
|
|
461
|
+
description: "Write content to a file on the server filesystem. Creates parent directories if they don't exist. Supports ~ for home directory expansion. Writes to protected directories (credentials, devices, identity) and config files (openclaw.json) are denied.",
|
|
462
|
+
parameters: {
|
|
463
|
+
type: "object",
|
|
464
|
+
properties: {
|
|
465
|
+
path: {
|
|
466
|
+
type: "string",
|
|
467
|
+
description: "Absolute or ~-prefixed path to the file to write"
|
|
468
|
+
},
|
|
469
|
+
content: {
|
|
470
|
+
type: "string",
|
|
471
|
+
description: "Content to write to the file"
|
|
472
|
+
},
|
|
473
|
+
encoding: {
|
|
474
|
+
type: "string",
|
|
475
|
+
description: "File encoding (default: utf-8)",
|
|
476
|
+
enum: ["utf-8", "base64", "ascii", "latin1"]
|
|
477
|
+
},
|
|
478
|
+
mkdir: {
|
|
479
|
+
type: "boolean",
|
|
480
|
+
description: "Create parent directories if they don't exist (default: true)"
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
required: ["path", "content"]
|
|
484
|
+
},
|
|
485
|
+
async execute(_id, params) {
|
|
486
|
+
try {
|
|
487
|
+
const filePath = validateWritePath(params.path, allowedRoots);
|
|
488
|
+
const content = params.content;
|
|
489
|
+
const encoding = params.encoding ?? "utf-8";
|
|
490
|
+
const mkdir = params.mkdir !== false;
|
|
491
|
+
if (mkdir) {
|
|
492
|
+
fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
|
|
493
|
+
}
|
|
494
|
+
fs2.writeFileSync(filePath, content, encoding);
|
|
495
|
+
const stat = fs2.statSync(filePath);
|
|
496
|
+
return ok({
|
|
497
|
+
path: filePath,
|
|
498
|
+
size: stat.size,
|
|
499
|
+
written: true
|
|
500
|
+
});
|
|
501
|
+
} catch (e) {
|
|
502
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
503
|
+
return err(`fs_write failed: ${msg}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
api.registerTool({
|
|
508
|
+
name: "fs_list",
|
|
509
|
+
label: "List Directory",
|
|
510
|
+
description: "List contents of a directory on the server filesystem. Returns file metadata including name, type, size, and modification time. Supports ~ for home directory expansion. Protected directories (credentials, devices, identity) are excluded from results.",
|
|
511
|
+
parameters: {
|
|
512
|
+
type: "object",
|
|
513
|
+
properties: {
|
|
514
|
+
path: {
|
|
515
|
+
type: "string",
|
|
516
|
+
description: "Absolute or ~-prefixed path to the directory to list"
|
|
517
|
+
},
|
|
518
|
+
recursive: {
|
|
519
|
+
type: "boolean",
|
|
520
|
+
description: "List recursively (default: false, max depth 3)"
|
|
521
|
+
},
|
|
522
|
+
includeHidden: {
|
|
523
|
+
type: "boolean",
|
|
524
|
+
description: "Include hidden files/directories starting with . (default: false)"
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
required: ["path"]
|
|
528
|
+
},
|
|
529
|
+
async execute(_id, params) {
|
|
530
|
+
try {
|
|
531
|
+
const dirPath = validateAndBlockSensitive(params.path, allowedRoots);
|
|
532
|
+
const recursive = params.recursive === true;
|
|
533
|
+
const includeHidden = params.includeHidden === true;
|
|
534
|
+
let entries = listDir(dirPath, { recursive, includeHidden, depth: 0, maxDepth: 3 });
|
|
535
|
+
entries = filterSensitiveEntries(entries);
|
|
536
|
+
return ok({
|
|
537
|
+
path: dirPath,
|
|
538
|
+
count: entries.length,
|
|
539
|
+
entries
|
|
540
|
+
});
|
|
541
|
+
} catch (e) {
|
|
542
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
543
|
+
return err(`fs_list failed: ${msg}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
api.registerTool({
|
|
548
|
+
name: "fs_mkdir",
|
|
549
|
+
label: "Create Directory",
|
|
550
|
+
description: "Create a directory on the server filesystem. Creates parent directories as needed. Supports ~ for home directory expansion. Cannot create directories inside protected paths (credentials, devices, identity).",
|
|
551
|
+
parameters: {
|
|
552
|
+
type: "object",
|
|
553
|
+
properties: {
|
|
554
|
+
path: {
|
|
555
|
+
type: "string",
|
|
556
|
+
description: "Absolute or ~-prefixed path of the directory to create"
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
required: ["path"]
|
|
560
|
+
},
|
|
561
|
+
async execute(_id, params) {
|
|
562
|
+
try {
|
|
563
|
+
const targetPath = validateWritePath(params.path, allowedRoots);
|
|
564
|
+
fs2.mkdirSync(targetPath, { recursive: true });
|
|
565
|
+
return ok({
|
|
566
|
+
path: targetPath,
|
|
567
|
+
created: true
|
|
568
|
+
});
|
|
569
|
+
} catch (e) {
|
|
570
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
571
|
+
return err(`fs_mkdir failed: ${msg}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
api.registerTool({
|
|
576
|
+
name: "fs_rename",
|
|
577
|
+
label: "Rename / Move",
|
|
578
|
+
description: "Rename or move a file or directory on the server filesystem. Supports ~ for home directory expansion. Cannot move files into or out of protected directories.",
|
|
579
|
+
parameters: {
|
|
580
|
+
type: "object",
|
|
581
|
+
properties: {
|
|
582
|
+
oldPath: {
|
|
583
|
+
type: "string",
|
|
584
|
+
description: "Current absolute or ~-prefixed path"
|
|
585
|
+
},
|
|
586
|
+
newPath: {
|
|
587
|
+
type: "string",
|
|
588
|
+
description: "New absolute or ~-prefixed path"
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
required: ["oldPath", "newPath"]
|
|
592
|
+
},
|
|
593
|
+
async execute(_id, params) {
|
|
594
|
+
try {
|
|
595
|
+
const resolvedOld = validateWritePath(params.oldPath, allowedRoots);
|
|
596
|
+
const resolvedNew = validateWritePath(params.newPath, allowedRoots);
|
|
597
|
+
fs2.renameSync(resolvedOld, resolvedNew);
|
|
598
|
+
return ok({
|
|
599
|
+
oldPath: resolvedOld,
|
|
600
|
+
newPath: resolvedNew,
|
|
601
|
+
renamed: true
|
|
814
602
|
});
|
|
815
603
|
} catch (e) {
|
|
816
604
|
const msg = e instanceof Error ? e.message : String(e);
|
|
817
|
-
return err(`
|
|
605
|
+
return err(`fs_rename failed: ${msg}`);
|
|
818
606
|
}
|
|
819
607
|
}
|
|
820
608
|
});
|
|
821
609
|
api.registerTool({
|
|
822
|
-
name: "
|
|
823
|
-
label: "
|
|
824
|
-
description: "
|
|
610
|
+
name: "fs_delete",
|
|
611
|
+
label: "Delete File or Directory",
|
|
612
|
+
description: "Delete a file or directory from the server filesystem. For directories, removes recursively. Supports ~ for home directory expansion. Cannot delete protected directories or config files.",
|
|
825
613
|
parameters: {
|
|
826
614
|
type: "object",
|
|
827
615
|
properties: {
|
|
828
616
|
path: {
|
|
829
617
|
type: "string",
|
|
830
|
-
description: "Absolute or ~-prefixed path to the file to
|
|
831
|
-
},
|
|
832
|
-
content: {
|
|
833
|
-
type: "string",
|
|
834
|
-
description: "Content to write to the file"
|
|
835
|
-
},
|
|
836
|
-
encoding: {
|
|
837
|
-
type: "string",
|
|
838
|
-
description: "File encoding (default: utf-8)",
|
|
839
|
-
enum: ["utf-8", "base64", "ascii", "latin1"]
|
|
840
|
-
},
|
|
841
|
-
mkdir: {
|
|
842
|
-
type: "boolean",
|
|
843
|
-
description: "Create parent directories if they don't exist (default: true)"
|
|
618
|
+
description: "Absolute or ~-prefixed path to the file or directory to delete"
|
|
844
619
|
}
|
|
845
620
|
},
|
|
846
|
-
required: ["path"
|
|
621
|
+
required: ["path"]
|
|
847
622
|
},
|
|
848
623
|
async execute(_id, params) {
|
|
849
624
|
try {
|
|
850
|
-
const
|
|
851
|
-
const
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
625
|
+
const targetPath = validateWritePath(params.path, allowedRoots);
|
|
626
|
+
const stat = fs2.statSync(targetPath);
|
|
627
|
+
const wasDirectory = stat.isDirectory();
|
|
628
|
+
if (wasDirectory) {
|
|
629
|
+
fs2.rmSync(targetPath, { recursive: true });
|
|
630
|
+
} else {
|
|
631
|
+
fs2.unlinkSync(targetPath);
|
|
632
|
+
}
|
|
633
|
+
return ok({
|
|
634
|
+
path: targetPath,
|
|
635
|
+
deleted: true,
|
|
636
|
+
type: wasDirectory ? "directory" : "file"
|
|
637
|
+
});
|
|
638
|
+
} catch (e) {
|
|
639
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
640
|
+
return err(`fs_delete failed: ${msg}`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/entities.ts
|
|
647
|
+
var EntityType = T.Union([
|
|
648
|
+
T.Literal("agent"),
|
|
649
|
+
T.Literal("skill"),
|
|
650
|
+
T.Literal("tool"),
|
|
651
|
+
T.Literal("plugin"),
|
|
652
|
+
T.Literal("session"),
|
|
653
|
+
T.Literal("file"),
|
|
654
|
+
T.Literal("directory"),
|
|
655
|
+
T.Literal("url"),
|
|
656
|
+
T.Literal("memory"),
|
|
657
|
+
T.Literal("asset")
|
|
658
|
+
]);
|
|
659
|
+
var registry = /* @__PURE__ */ new Map();
|
|
660
|
+
function registrySet(entity) {
|
|
661
|
+
registry.set(entity.id, entity);
|
|
662
|
+
}
|
|
663
|
+
function registryDelete(id) {
|
|
664
|
+
registry.delete(id);
|
|
665
|
+
}
|
|
666
|
+
function registryList(type) {
|
|
667
|
+
const all = Array.from(registry.values());
|
|
668
|
+
if (!type) return all;
|
|
669
|
+
return all.filter((e) => e.type === type);
|
|
670
|
+
}
|
|
671
|
+
var IDENTITY_NAME_RE = /\*\*Name:\*\*\s*\n?\s*(.+?)(?=\n|$)/;
|
|
672
|
+
function parseIdentityName(content) {
|
|
673
|
+
const match = content.match(IDENTITY_NAME_RE);
|
|
674
|
+
const name = match?.[1]?.trim();
|
|
675
|
+
if (!name) return null;
|
|
676
|
+
if (/^_\(.+\)_$/.test(name)) return null;
|
|
677
|
+
return name;
|
|
678
|
+
}
|
|
679
|
+
function scanAgents(configDir) {
|
|
680
|
+
const now = Date.now();
|
|
681
|
+
let entries;
|
|
682
|
+
try {
|
|
683
|
+
entries = fs3.readdirSync(configDir, { withFileTypes: true });
|
|
684
|
+
} catch {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const workspaceDirs = entries.filter(
|
|
688
|
+
(e) => e.isDirectory() && (e.name === "workspace" || e.name.startsWith("workspace-"))
|
|
689
|
+
);
|
|
690
|
+
for (const dir of workspaceDirs) {
|
|
691
|
+
const agentId = dir.name === "workspace" ? "main" : dir.name.replace("workspace-", "");
|
|
692
|
+
const workspacePath = path3.join(configDir, dir.name);
|
|
693
|
+
let name = agentId;
|
|
694
|
+
const metadata = { workspacePath };
|
|
695
|
+
const identityPath = path3.join(workspacePath, "IDENTITY.md");
|
|
696
|
+
try {
|
|
697
|
+
const content = fs3.readFileSync(identityPath, "utf-8");
|
|
698
|
+
const parsed = parseIdentityName(content);
|
|
699
|
+
if (parsed) name = parsed;
|
|
700
|
+
} catch {
|
|
701
|
+
}
|
|
702
|
+
if (name === agentId) {
|
|
703
|
+
const agentJsonPath = path3.join(workspacePath, "agent.json");
|
|
704
|
+
try {
|
|
705
|
+
const raw = fs3.readFileSync(agentJsonPath, "utf-8");
|
|
706
|
+
const config = JSON.parse(raw);
|
|
707
|
+
if (config.displayName) name = config.displayName;
|
|
708
|
+
if (config.model) metadata.model = config.model;
|
|
709
|
+
if (config.tools) metadata.tools = config.tools;
|
|
710
|
+
if (config.skills) metadata.skills = config.skills;
|
|
711
|
+
} catch {
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
registrySet({
|
|
715
|
+
id: agentId,
|
|
716
|
+
type: "agent",
|
|
717
|
+
name,
|
|
718
|
+
title: name,
|
|
719
|
+
description: null,
|
|
720
|
+
metadata,
|
|
721
|
+
source: "filesystem",
|
|
722
|
+
source_key: workspacePath,
|
|
723
|
+
created_at: now,
|
|
724
|
+
updated_at: now
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function scanSkills(configDir) {
|
|
729
|
+
const now = Date.now();
|
|
730
|
+
const globalSkillsDir = path3.join(configDir, "skills");
|
|
731
|
+
scanSkillsDir(globalSkillsDir, "global", now);
|
|
732
|
+
let entries;
|
|
733
|
+
try {
|
|
734
|
+
entries = fs3.readdirSync(configDir, { withFileTypes: true });
|
|
735
|
+
} catch {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
for (const dir of entries) {
|
|
739
|
+
if (!dir.isDirectory() || !(dir.name === "workspace" || dir.name.startsWith("workspace-"))) {
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const agentId = dir.name === "workspace" ? "main" : dir.name.replace("workspace-", "");
|
|
743
|
+
const agentSkillsDir = path3.join(configDir, dir.name, "skills");
|
|
744
|
+
scanSkillsDir(agentSkillsDir, agentId, now);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function scanSkillsDir(skillsDir, scope, now) {
|
|
748
|
+
let entries;
|
|
749
|
+
try {
|
|
750
|
+
entries = fs3.readdirSync(skillsDir, { withFileTypes: true });
|
|
751
|
+
} catch {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
for (const entry of entries) {
|
|
755
|
+
if (!entry.isDirectory()) continue;
|
|
756
|
+
const skillKey = entry.name;
|
|
757
|
+
const skillPath = path3.join(skillsDir, skillKey);
|
|
758
|
+
let name = skillKey;
|
|
759
|
+
for (const manifestName of ["manifest.json", "package.json"]) {
|
|
760
|
+
try {
|
|
761
|
+
const raw = fs3.readFileSync(
|
|
762
|
+
path3.join(skillPath, manifestName),
|
|
763
|
+
"utf-8"
|
|
764
|
+
);
|
|
765
|
+
const manifest = JSON.parse(raw);
|
|
766
|
+
if (manifest.name) name = manifest.name;
|
|
767
|
+
break;
|
|
768
|
+
} catch {
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
const entityId = scope === "global" ? `skill:${skillKey}` : `skill:${scope}:${skillKey}`;
|
|
773
|
+
registrySet({
|
|
774
|
+
id: entityId,
|
|
775
|
+
type: "skill",
|
|
776
|
+
name,
|
|
777
|
+
title: name,
|
|
778
|
+
description: null,
|
|
779
|
+
metadata: { skillKey, scope, skillPath },
|
|
780
|
+
source: "filesystem",
|
|
781
|
+
source_key: skillPath,
|
|
782
|
+
created_at: now,
|
|
783
|
+
updated_at: now
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function scanPlugins2(configDir) {
|
|
788
|
+
const now = Date.now();
|
|
789
|
+
const extensionsDir = path3.join(configDir, "extensions");
|
|
790
|
+
let entries;
|
|
791
|
+
try {
|
|
792
|
+
entries = fs3.readdirSync(extensionsDir, { withFileTypes: true });
|
|
793
|
+
} catch {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
for (const dir of entries) {
|
|
797
|
+
if (!dir.isDirectory()) continue;
|
|
798
|
+
const pluginDir = path3.join(extensionsDir, dir.name);
|
|
799
|
+
const manifestPath = path3.join(pluginDir, "openclaw.plugin.json");
|
|
800
|
+
try {
|
|
801
|
+
const raw = fs3.readFileSync(manifestPath, "utf-8");
|
|
802
|
+
const manifest = JSON.parse(raw);
|
|
803
|
+
const pluginId = manifest.id || dir.name;
|
|
804
|
+
const name = manifest.name || pluginId;
|
|
805
|
+
registrySet({
|
|
806
|
+
id: `plugin:${pluginId}`,
|
|
807
|
+
type: "plugin",
|
|
808
|
+
name,
|
|
809
|
+
title: name,
|
|
810
|
+
description: manifest.description || null,
|
|
811
|
+
metadata: { pluginId, pluginDir },
|
|
812
|
+
source: "filesystem",
|
|
813
|
+
source_key: manifestPath,
|
|
814
|
+
created_at: now,
|
|
815
|
+
updated_at: now
|
|
816
|
+
});
|
|
817
|
+
} catch {
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
function scanTools(configDir) {
|
|
822
|
+
const now = Date.now();
|
|
823
|
+
try {
|
|
824
|
+
const raw = fs3.readFileSync(
|
|
825
|
+
path3.join(configDir, "openclaw.json"),
|
|
826
|
+
"utf-8"
|
|
827
|
+
);
|
|
828
|
+
const config = JSON.parse(raw);
|
|
829
|
+
const allowedTools = config?.tools?.allow ?? [];
|
|
830
|
+
for (const toolName of allowedTools) {
|
|
831
|
+
registrySet({
|
|
832
|
+
id: `tool:${toolName}`,
|
|
833
|
+
type: "tool",
|
|
834
|
+
name: toolName,
|
|
835
|
+
title: toolName,
|
|
836
|
+
description: null,
|
|
837
|
+
metadata: { tool_name: toolName },
|
|
838
|
+
source: "filesystem",
|
|
839
|
+
source_key: "openclaw.json:tools.allow",
|
|
840
|
+
created_at: now,
|
|
841
|
+
updated_at: now
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
} catch {
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
var MIME_MAP = {
|
|
848
|
+
".png": "image/png",
|
|
849
|
+
".jpg": "image/jpeg",
|
|
850
|
+
".jpeg": "image/jpeg",
|
|
851
|
+
".gif": "image/gif",
|
|
852
|
+
".webp": "image/webp",
|
|
853
|
+
".svg": "image/svg+xml",
|
|
854
|
+
".bmp": "image/bmp",
|
|
855
|
+
".ico": "image/x-icon",
|
|
856
|
+
".mp4": "video/mp4",
|
|
857
|
+
".webm": "video/webm",
|
|
858
|
+
".mov": "video/quicktime",
|
|
859
|
+
".avi": "video/x-msvideo",
|
|
860
|
+
".mkv": "video/x-matroska",
|
|
861
|
+
".mp3": "audio/mpeg",
|
|
862
|
+
".wav": "audio/wav",
|
|
863
|
+
".ogg": "audio/ogg",
|
|
864
|
+
".flac": "audio/flac",
|
|
865
|
+
".aac": "audio/aac",
|
|
866
|
+
".pdf": "application/pdf",
|
|
867
|
+
".json": "application/json",
|
|
868
|
+
".txt": "text/plain",
|
|
869
|
+
".md": "text/markdown",
|
|
870
|
+
".csv": "text/csv",
|
|
871
|
+
".zip": "application/zip",
|
|
872
|
+
".tar": "application/x-tar",
|
|
873
|
+
".gz": "application/gzip"
|
|
874
|
+
};
|
|
875
|
+
function getMimeType(filename) {
|
|
876
|
+
const ext = path3.extname(filename).toLowerCase();
|
|
877
|
+
return MIME_MAP[ext] ?? "application/octet-stream";
|
|
878
|
+
}
|
|
879
|
+
function scanMedia(configDir) {
|
|
880
|
+
const now = Date.now();
|
|
881
|
+
const mediaDir = path3.join(configDir, "media");
|
|
882
|
+
scanMediaDir(mediaDir, now);
|
|
883
|
+
}
|
|
884
|
+
function scanMediaDir(dirPath, now) {
|
|
885
|
+
let entries;
|
|
886
|
+
try {
|
|
887
|
+
entries = fs3.readdirSync(dirPath, { withFileTypes: true });
|
|
888
|
+
} catch {
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
for (const entry of entries) {
|
|
892
|
+
if (entry.name.startsWith(".")) continue;
|
|
893
|
+
const entryPath = path3.join(dirPath, entry.name);
|
|
894
|
+
if (isSensitivePath(entryPath)) continue;
|
|
895
|
+
if (entry.isDirectory()) {
|
|
896
|
+
registrySet({
|
|
897
|
+
id: entryPath,
|
|
898
|
+
type: "directory",
|
|
899
|
+
name: entry.name,
|
|
900
|
+
title: entry.name,
|
|
901
|
+
description: null,
|
|
902
|
+
metadata: { path: entryPath },
|
|
903
|
+
source: "filesystem",
|
|
904
|
+
source_key: entryPath,
|
|
905
|
+
created_at: now,
|
|
906
|
+
updated_at: now
|
|
907
|
+
});
|
|
908
|
+
scanMediaDir(entryPath, now);
|
|
909
|
+
} else if (entry.isFile()) {
|
|
910
|
+
const mimeType = getMimeType(entry.name);
|
|
911
|
+
let size;
|
|
912
|
+
let mtime = now;
|
|
913
|
+
try {
|
|
914
|
+
const stat = fs3.statSync(entryPath);
|
|
915
|
+
size = stat.size;
|
|
916
|
+
mtime = stat.mtimeMs;
|
|
917
|
+
} catch {
|
|
867
918
|
}
|
|
919
|
+
registrySet({
|
|
920
|
+
id: entryPath,
|
|
921
|
+
type: "asset",
|
|
922
|
+
name: entry.name,
|
|
923
|
+
title: entry.name,
|
|
924
|
+
description: null,
|
|
925
|
+
metadata: { path: entryPath, size, mime_type: mimeType, original_name: entry.name },
|
|
926
|
+
source: "filesystem",
|
|
927
|
+
source_key: entryPath,
|
|
928
|
+
created_at: mtime,
|
|
929
|
+
updated_at: mtime
|
|
930
|
+
});
|
|
868
931
|
}
|
|
869
|
-
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
function fullScan(configDir) {
|
|
935
|
+
registry.clear();
|
|
936
|
+
scanAgents(configDir);
|
|
937
|
+
scanSkills(configDir);
|
|
938
|
+
scanPlugins2(configDir);
|
|
939
|
+
scanTools(configDir);
|
|
940
|
+
scanMedia(configDir);
|
|
941
|
+
}
|
|
942
|
+
function registerEntityTools(api, onFsChange) {
|
|
943
|
+
const configDir = process.env.HOME + "/.openclaw";
|
|
870
944
|
api.registerTool({
|
|
871
|
-
name: "
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
description: "Include hidden files/directories starting with . (default: false)"
|
|
888
|
-
}
|
|
889
|
-
},
|
|
890
|
-
required: ["path"]
|
|
891
|
-
},
|
|
892
|
-
async execute(_id, params) {
|
|
893
|
-
try {
|
|
894
|
-
const dirPath = validateAndBlockSensitive(params.path, allowedRoots);
|
|
895
|
-
const recursive = params.recursive === true;
|
|
896
|
-
const includeHidden = params.includeHidden === true;
|
|
897
|
-
let entries = listDir(dirPath, { recursive, includeHidden, depth: 0, maxDepth: 3 });
|
|
898
|
-
entries = filterSensitiveEntries(entries);
|
|
899
|
-
return ok({
|
|
900
|
-
path: dirPath,
|
|
901
|
-
count: entries.length,
|
|
902
|
-
entries
|
|
903
|
-
});
|
|
904
|
-
} catch (e) {
|
|
905
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
906
|
-
return err(`fs_list failed: ${msg}`);
|
|
907
|
-
}
|
|
945
|
+
name: "entity_list",
|
|
946
|
+
description: "List all entities in the registry, optionally filtered by type. Returns lightweight entity data for @mention autocomplete.",
|
|
947
|
+
parameters: T.Object({
|
|
948
|
+
type: T.Optional(EntityType),
|
|
949
|
+
limit: T.Optional(
|
|
950
|
+
T.Number({ description: "Max results (default 500)" })
|
|
951
|
+
)
|
|
952
|
+
}),
|
|
953
|
+
async execute(_id, params, _ctx) {
|
|
954
|
+
const results = registryList(params.type);
|
|
955
|
+
const limit = params.limit ?? 500;
|
|
956
|
+
return {
|
|
957
|
+
content: [
|
|
958
|
+
{ type: "text", text: JSON.stringify(results.slice(0, limit)) }
|
|
959
|
+
]
|
|
960
|
+
};
|
|
908
961
|
}
|
|
909
962
|
});
|
|
910
963
|
api.registerTool({
|
|
911
|
-
name: "
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
type:
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
return ok({
|
|
929
|
-
path: targetPath,
|
|
930
|
-
created: true
|
|
931
|
-
});
|
|
932
|
-
} catch (e) {
|
|
933
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
934
|
-
return err(`fs_mkdir failed: ${msg}`);
|
|
964
|
+
name: "entity_search",
|
|
965
|
+
description: "Search entities by name/title substring match for @mention autocomplete.",
|
|
966
|
+
parameters: T.Object({
|
|
967
|
+
query: T.String({ description: "Search query text" }),
|
|
968
|
+
type: T.Optional(
|
|
969
|
+
T.String({ description: "Filter results by entity type" })
|
|
970
|
+
),
|
|
971
|
+
limit: T.Optional(
|
|
972
|
+
T.Number({ description: "Max results (default 20)" })
|
|
973
|
+
)
|
|
974
|
+
}),
|
|
975
|
+
async execute(_id, params, _ctx) {
|
|
976
|
+
const q = (params.query ?? "").toLowerCase();
|
|
977
|
+
const limit = params.limit ?? 20;
|
|
978
|
+
let results = Array.from(registry.values());
|
|
979
|
+
if (params.type) {
|
|
980
|
+
results = results.filter((e) => e.type === params.type);
|
|
935
981
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
label: "Rename / Move",
|
|
941
|
-
description: "Rename or move a file or directory on the server filesystem. Supports ~ for home directory expansion. Cannot move files into or out of protected directories.",
|
|
942
|
-
parameters: {
|
|
943
|
-
type: "object",
|
|
944
|
-
properties: {
|
|
945
|
-
oldPath: {
|
|
946
|
-
type: "string",
|
|
947
|
-
description: "Current absolute or ~-prefixed path"
|
|
948
|
-
},
|
|
949
|
-
newPath: {
|
|
950
|
-
type: "string",
|
|
951
|
-
description: "New absolute or ~-prefixed path"
|
|
952
|
-
}
|
|
953
|
-
},
|
|
954
|
-
required: ["oldPath", "newPath"]
|
|
955
|
-
},
|
|
956
|
-
async execute(_id, params) {
|
|
957
|
-
try {
|
|
958
|
-
const resolvedOld = validateWritePath(params.oldPath, allowedRoots);
|
|
959
|
-
const resolvedNew = validateWritePath(params.newPath, allowedRoots);
|
|
960
|
-
fs3.renameSync(resolvedOld, resolvedNew);
|
|
961
|
-
return ok({
|
|
962
|
-
oldPath: resolvedOld,
|
|
963
|
-
newPath: resolvedNew,
|
|
964
|
-
renamed: true
|
|
965
|
-
});
|
|
966
|
-
} catch (e) {
|
|
967
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
968
|
-
return err(`fs_rename failed: ${msg}`);
|
|
982
|
+
if (q) {
|
|
983
|
+
results = results.filter(
|
|
984
|
+
(e) => e.name.toLowerCase().includes(q) || (e.title ?? "").toLowerCase().includes(q)
|
|
985
|
+
);
|
|
969
986
|
}
|
|
987
|
+
return {
|
|
988
|
+
content: [
|
|
989
|
+
{ type: "text", text: JSON.stringify(results.slice(0, limit)) }
|
|
990
|
+
]
|
|
991
|
+
};
|
|
970
992
|
}
|
|
971
993
|
});
|
|
972
994
|
api.registerTool({
|
|
973
|
-
name: "
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
try {
|
|
988
|
-
const targetPath = validateWritePath(params.path, allowedRoots);
|
|
989
|
-
const stat = fs3.statSync(targetPath);
|
|
990
|
-
const wasDirectory = stat.isDirectory();
|
|
991
|
-
if (wasDirectory) {
|
|
992
|
-
fs3.rmSync(targetPath, { recursive: true });
|
|
993
|
-
} else {
|
|
994
|
-
fs3.unlinkSync(targetPath);
|
|
995
|
-
}
|
|
996
|
-
return ok({
|
|
997
|
-
path: targetPath,
|
|
998
|
-
deleted: true,
|
|
999
|
-
type: wasDirectory ? "directory" : "file"
|
|
1000
|
-
});
|
|
1001
|
-
} catch (e) {
|
|
1002
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
1003
|
-
return err(`fs_delete failed: ${msg}`);
|
|
1004
|
-
}
|
|
995
|
+
name: "entity_sync",
|
|
996
|
+
description: "Re-scan the filesystem to refresh the entity registry. Call after configuration changes for immediate updates.",
|
|
997
|
+
parameters: T.Object({}),
|
|
998
|
+
async execute(_id, _params, _ctx) {
|
|
999
|
+
const before = registry.size;
|
|
1000
|
+
fullScan(configDir);
|
|
1001
|
+
return {
|
|
1002
|
+
content: [
|
|
1003
|
+
{
|
|
1004
|
+
type: "text",
|
|
1005
|
+
text: JSON.stringify({ synced: registry.size, previous: before })
|
|
1006
|
+
}
|
|
1007
|
+
]
|
|
1008
|
+
};
|
|
1005
1009
|
}
|
|
1006
1010
|
});
|
|
1011
|
+
try {
|
|
1012
|
+
fullScan(configDir);
|
|
1013
|
+
} catch (err2) {
|
|
1014
|
+
console.error("[squad-openclaw] Initial scan failed:", err2);
|
|
1015
|
+
}
|
|
1016
|
+
let stopWatcher = null;
|
|
1017
|
+
try {
|
|
1018
|
+
stopWatcher = startWatcher(configDir, onFsChange);
|
|
1019
|
+
} catch (err2) {
|
|
1020
|
+
console.error("[squad-openclaw] Watcher failed to start:", err2);
|
|
1021
|
+
}
|
|
1022
|
+
const cleanup = () => {
|
|
1023
|
+
stopWatcher?.();
|
|
1024
|
+
};
|
|
1025
|
+
process.on("SIGTERM", cleanup);
|
|
1026
|
+
process.on("SIGINT", cleanup);
|
|
1007
1027
|
}
|
|
1008
1028
|
|
|
1009
1029
|
// src/sql.ts
|
|
@@ -1186,7 +1206,15 @@ function registerVersionMethods(api) {
|
|
|
1186
1206
|
console.log(
|
|
1187
1207
|
`[version] Plugin update command succeeded (was ${before}), restarting gateway in 2s...`
|
|
1188
1208
|
);
|
|
1189
|
-
setTimeout(() =>
|
|
1209
|
+
setTimeout(() => {
|
|
1210
|
+
try {
|
|
1211
|
+
execSync("openclaw gateway restart 2>&1", {
|
|
1212
|
+
timeout: 3e4,
|
|
1213
|
+
encoding: "utf-8"
|
|
1214
|
+
});
|
|
1215
|
+
} catch {
|
|
1216
|
+
}
|
|
1217
|
+
}, 2e3);
|
|
1190
1218
|
} catch (e) {
|
|
1191
1219
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1192
1220
|
respond(false, { error: msg });
|
|
@@ -1619,7 +1647,14 @@ var RelayClient = class {
|
|
|
1619
1647
|
}
|
|
1620
1648
|
conn.localWs.send(JSON.stringify(msg));
|
|
1621
1649
|
}
|
|
1622
|
-
/**
|
|
1650
|
+
/**
|
|
1651
|
+
* Inject auth token and device identity into a connect request.
|
|
1652
|
+
*
|
|
1653
|
+
* SECURITY: The token is added to the message IN MEMORY, then sent to the
|
|
1654
|
+
* LOCAL gateway WebSocket (localhost:18789). It NEVER traverses the relay —
|
|
1655
|
+
* the relay only sees the outer relay.forward envelope. A compromised relay
|
|
1656
|
+
* server cannot intercept this token.
|
|
1657
|
+
*/
|
|
1623
1658
|
injectDeviceIdentity(conn, msg) {
|
|
1624
1659
|
const params = msg.params ?? {};
|
|
1625
1660
|
if (this.config.operatorToken) {
|
|
@@ -1883,6 +1918,49 @@ function squadAppPlugin(api) {
|
|
|
1883
1918
|
}
|
|
1884
1919
|
}
|
|
1885
1920
|
);
|
|
1921
|
+
api.registerGatewayMethod(
|
|
1922
|
+
"tools.list",
|
|
1923
|
+
async ({ respond }) => {
|
|
1924
|
+
const coreTools = [
|
|
1925
|
+
"exec",
|
|
1926
|
+
"bash",
|
|
1927
|
+
"process",
|
|
1928
|
+
"read",
|
|
1929
|
+
"write",
|
|
1930
|
+
"edit",
|
|
1931
|
+
"apply_patch",
|
|
1932
|
+
"web_search",
|
|
1933
|
+
"web_fetch",
|
|
1934
|
+
"browser",
|
|
1935
|
+
"canvas",
|
|
1936
|
+
"nodes",
|
|
1937
|
+
"image",
|
|
1938
|
+
"message",
|
|
1939
|
+
"cron",
|
|
1940
|
+
"gateway",
|
|
1941
|
+
"sessions_list",
|
|
1942
|
+
"sessions_history",
|
|
1943
|
+
"sessions_send",
|
|
1944
|
+
"sessions_spawn",
|
|
1945
|
+
"session_status",
|
|
1946
|
+
"agents_list",
|
|
1947
|
+
"memory_search"
|
|
1948
|
+
];
|
|
1949
|
+
const groups = [
|
|
1950
|
+
"group:fs",
|
|
1951
|
+
"group:runtime",
|
|
1952
|
+
"group:sessions",
|
|
1953
|
+
"group:memory",
|
|
1954
|
+
"group:web",
|
|
1955
|
+
"group:ui",
|
|
1956
|
+
"group:automation",
|
|
1957
|
+
"group:messaging",
|
|
1958
|
+
"group:nodes"
|
|
1959
|
+
];
|
|
1960
|
+
const pluginTools = Array.from(toolExecutors.keys());
|
|
1961
|
+
respond(true, { tools: [...coreTools, ...groups, ...pluginTools] });
|
|
1962
|
+
}
|
|
1963
|
+
);
|
|
1886
1964
|
const relayEnabled = api.pluginConfig?.["relay.enabled"] ?? false;
|
|
1887
1965
|
if (relayEnabled) {
|
|
1888
1966
|
const relayUrl = api.pluginConfig?.["relay.url"] || "wss://relay.squad.ceo";
|