squad-openclaw 2026.2.2015 → 2026.2.2018
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 +33 -3
- package/dist/index.js +150 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,18 +15,48 @@ OpenClaw gateway plugin for [Squad](https://squad.ceo) — provides entity regis
|
|
|
15
15
|
|
|
16
16
|
## State Directory Resolution
|
|
17
17
|
|
|
18
|
-
All paths in this plugin (and throughout this README) that reference `~/.openclaw` resolve via
|
|
18
|
+
All paths in this plugin (and throughout this README) that reference `~/.openclaw` resolve via environment override when set. This supports Docker and other containerized deployments where the OpenClaw data directory may not be at the default location.
|
|
19
|
+
|
|
20
|
+
Resolution priority:
|
|
21
|
+
|
|
22
|
+
1. `OPENCLAW_STATE_DIR` (canonical)
|
|
23
|
+
2. `OPENCLAW_DIR` (compat alias)
|
|
24
|
+
3. `os.homedir() + "/.openclaw"` (default)
|
|
19
25
|
|
|
20
26
|
| Environment | Typical path | How it's resolved |
|
|
21
27
|
|---|---|---|
|
|
22
28
|
| Standard install | `~/.openclaw` | Default — `os.homedir() + "/.openclaw"` |
|
|
23
|
-
| Docker | `/
|
|
29
|
+
| Docker | `/data/.openclaw` | `OPENCLAW_STATE_DIR=/data/.openclaw` (or `OPENCLAW_DIR=/data/.openclaw`) |
|
|
24
30
|
| Custom / NAS | `/mnt/data/.openclaw` | `OPENCLAW_STATE_DIR=/mnt/data/.openclaw` |
|
|
25
31
|
|
|
26
32
|
**This variable only controls where the plugin looks for OpenClaw's own data directory.** It does not grant the plugin access to the parent directory or any other part of the filesystem. All security restrictions (blocked directories, allowed roots, write protection) are enforced relative to the resolved state directory — not the filesystem root.
|
|
27
33
|
|
|
28
34
|
The resolution logic lives in a single shared module ([`src/paths.ts`](src/paths.ts)) imported by every file that needs the state directory path.
|
|
29
35
|
|
|
36
|
+
### Docker Workspace Mapping
|
|
37
|
+
|
|
38
|
+
If your container mounts the real workspace outside the OpenClaw state directory (for example, `/data/workspace`), create a symlink so OpenClaw-compatible tools can still resolve `.../.openclaw/workspace`:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
mkdir -p /data/.openclaw /data/workspace
|
|
42
|
+
ln -sfn /data/workspace /data/.openclaw/workspace
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then start your gateway/server with explicit env vars (useful when `.env` is not loaded):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
PORT=3334 \
|
|
49
|
+
OPENCLAW_STATE_DIR=/data/.openclaw \
|
|
50
|
+
OPENCLAW_DIR=/data/.openclaw \
|
|
51
|
+
node server.js
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Quick verification:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
ls -ld /data/.openclaw /data/.openclaw/workspace /data/workspace /data/.openclaw/openclaw.json
|
|
58
|
+
```
|
|
59
|
+
|
|
30
60
|
## Security Model
|
|
31
61
|
|
|
32
62
|
This plugin enforces a **defense-in-depth** security model with four independent layers. All security rules are hard-coded and non-configurable (except `allowedRoots`) so they can be verified by reading the source code. The bundle is intentionally **not minified** to allow security auditing of the distributed code.
|
|
@@ -231,7 +261,7 @@ Configure in your gateway's `openclaw.json` under the plugin section:
|
|
|
231
261
|
|
|
232
262
|
| Key | Type | Default | Description |
|
|
233
263
|
|---|---|---|---|
|
|
234
|
-
| `fs.allowedRoots` | `string[]` | `[
|
|
264
|
+
| `fs.allowedRoots` | `string[]` | `[resolved OpenClaw state dir]` | Restrict filesystem operations to these directories. Hardcoded blocks on `credentials/`, `devices/`, `identity/`, `relay/squad-relay.json`, and `.bak` files always apply regardless. |
|
|
235
265
|
|
|
236
266
|
## Source Code
|
|
237
267
|
|
package/dist/index.js
CHANGED
|
@@ -367,7 +367,7 @@ import path3 from "path";
|
|
|
367
367
|
import path2 from "path";
|
|
368
368
|
import os from "os";
|
|
369
369
|
function getOpenclawStateDir() {
|
|
370
|
-
return process.env.OPENCLAW_STATE_DIR || path2.join(os.homedir(), ".openclaw");
|
|
370
|
+
return process.env.OPENCLAW_STATE_DIR || process.env.OPENCLAW_DIR || path2.join(os.homedir(), ".openclaw");
|
|
371
371
|
}
|
|
372
372
|
|
|
373
373
|
// src/filesystem.ts
|
|
@@ -526,7 +526,7 @@ function filterSensitiveEntries(entries) {
|
|
|
526
526
|
});
|
|
527
527
|
}
|
|
528
528
|
function registerFilesystemTools(api) {
|
|
529
|
-
const DEFAULT_ALLOWED_ROOTS = [
|
|
529
|
+
const DEFAULT_ALLOWED_ROOTS = [OPENCLAW_DIR];
|
|
530
530
|
const allowedRoots = api.pluginConfig?.["fs.allowedRoots"] ?? DEFAULT_ALLOWED_ROOTS;
|
|
531
531
|
api.registerTool({
|
|
532
532
|
name: "fs_read",
|
|
@@ -1244,6 +1244,36 @@ function readInstalledVersionFromConfig() {
|
|
|
1244
1244
|
return null;
|
|
1245
1245
|
}
|
|
1246
1246
|
}
|
|
1247
|
+
function reconcileInstallMetadata(verification) {
|
|
1248
|
+
if (!verification.installPath || !verification.packageVersion) return;
|
|
1249
|
+
try {
|
|
1250
|
+
const raw = fs5.readFileSync(CONFIG_PATH, "utf-8");
|
|
1251
|
+
const config = JSON.parse(raw);
|
|
1252
|
+
if (!config.plugins || typeof config.plugins !== "object") config.plugins = {};
|
|
1253
|
+
if (!config.plugins.installs || typeof config.plugins.installs !== "object") {
|
|
1254
|
+
config.plugins.installs = {};
|
|
1255
|
+
}
|
|
1256
|
+
if (!config.plugins.entries || typeof config.plugins.entries !== "object") {
|
|
1257
|
+
config.plugins.entries = {};
|
|
1258
|
+
}
|
|
1259
|
+
const current = config.plugins.installs[PACKAGE_NAME] ?? {};
|
|
1260
|
+
config.plugins.installs[PACKAGE_NAME] = {
|
|
1261
|
+
...current,
|
|
1262
|
+
source: "npm",
|
|
1263
|
+
spec: PACKAGE_NAME,
|
|
1264
|
+
installPath: verification.installPath,
|
|
1265
|
+
version: verification.packageVersion,
|
|
1266
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1267
|
+
};
|
|
1268
|
+
const entry = config.plugins.entries[PACKAGE_NAME] ?? {};
|
|
1269
|
+
config.plugins.entries[PACKAGE_NAME] = {
|
|
1270
|
+
...entry,
|
|
1271
|
+
enabled: true
|
|
1272
|
+
};
|
|
1273
|
+
fs5.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
1274
|
+
} catch {
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1247
1277
|
function getCurrentVersion() {
|
|
1248
1278
|
const thisFile = fileURLToPath(import.meta.url);
|
|
1249
1279
|
const pkgPath = path6.resolve(path6.dirname(thisFile), "..", "package.json");
|
|
@@ -1280,6 +1310,16 @@ function runDoctorFixSilently() {
|
|
|
1280
1310
|
function sleep(ms) {
|
|
1281
1311
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1282
1312
|
}
|
|
1313
|
+
function compareVersions(a, b) {
|
|
1314
|
+
const pa = a.split(".").map((x) => Number(x) || 0);
|
|
1315
|
+
const pb = b.split(".").map((x) => Number(x) || 0);
|
|
1316
|
+
const len = Math.max(pa.length, pb.length);
|
|
1317
|
+
for (let i = 0; i < len; i++) {
|
|
1318
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
1319
|
+
if (d !== 0) return d;
|
|
1320
|
+
}
|
|
1321
|
+
return 0;
|
|
1322
|
+
}
|
|
1283
1323
|
function verifyInstalledPluginState() {
|
|
1284
1324
|
let configRaw;
|
|
1285
1325
|
try {
|
|
@@ -1380,16 +1420,6 @@ function verifyInstalledPluginState() {
|
|
|
1380
1420
|
requiredFilesMissing: []
|
|
1381
1421
|
};
|
|
1382
1422
|
}
|
|
1383
|
-
if (configVersion && configVersion !== packageVersion) {
|
|
1384
|
-
return {
|
|
1385
|
-
ok: false,
|
|
1386
|
-
reason: `Version mismatch: config=${configVersion}, package=${packageVersion}`,
|
|
1387
|
-
installPath,
|
|
1388
|
-
configVersion,
|
|
1389
|
-
packageVersion,
|
|
1390
|
-
requiredFilesMissing: []
|
|
1391
|
-
};
|
|
1392
|
-
}
|
|
1393
1423
|
return {
|
|
1394
1424
|
ok: true,
|
|
1395
1425
|
installPath,
|
|
@@ -1447,6 +1477,12 @@ function registerVersionMethods(api) {
|
|
|
1447
1477
|
try {
|
|
1448
1478
|
const before = getCurrentVersion();
|
|
1449
1479
|
const beforeInstalledVersion = readInstalledVersionFromConfig();
|
|
1480
|
+
let latestVersion = null;
|
|
1481
|
+
try {
|
|
1482
|
+
latestVersion = await fetchLatestVersion();
|
|
1483
|
+
} catch {
|
|
1484
|
+
latestVersion = null;
|
|
1485
|
+
}
|
|
1450
1486
|
let updateOutput = "";
|
|
1451
1487
|
let configBackup = null;
|
|
1452
1488
|
try {
|
|
@@ -1498,11 +1534,15 @@ function registerVersionMethods(api) {
|
|
|
1498
1534
|
});
|
|
1499
1535
|
return;
|
|
1500
1536
|
}
|
|
1501
|
-
|
|
1537
|
+
reconcileInstallMetadata(verification);
|
|
1538
|
+
const verificationAfterReconcile = verifyInstalledPluginState();
|
|
1539
|
+
if (beforeInstalledVersion && verificationAfterReconcile.packageVersion && beforeInstalledVersion === verificationAfterReconcile.packageVersion) {
|
|
1540
|
+
const alreadyLatest = !!latestVersion && compareVersions(verificationAfterReconcile.packageVersion, latestVersion) >= 0;
|
|
1502
1541
|
respond(false, {
|
|
1503
|
-
error: `Update command completed but installed version did not change (${
|
|
1542
|
+
error: alreadyLatest ? `Already at latest version (${verificationAfterReconcile.packageVersion}).` : `Update command completed but installed version did not change (${verificationAfterReconcile.packageVersion}).`,
|
|
1504
1543
|
output: updateOutput.slice(0, 500),
|
|
1505
|
-
verification
|
|
1544
|
+
verification: verificationAfterReconcile,
|
|
1545
|
+
latestVersion
|
|
1506
1546
|
});
|
|
1507
1547
|
return;
|
|
1508
1548
|
}
|
|
@@ -1513,7 +1553,8 @@ function registerVersionMethods(api) {
|
|
|
1513
1553
|
updated: true,
|
|
1514
1554
|
restartRequired: true,
|
|
1515
1555
|
restartInMs: RESTART_BUFFER_MS,
|
|
1516
|
-
verification,
|
|
1556
|
+
verification: verificationAfterReconcile,
|
|
1557
|
+
latestVersion,
|
|
1517
1558
|
output: updateOutput.slice(0, 500)
|
|
1518
1559
|
});
|
|
1519
1560
|
await sleep(RESTART_BUFFER_MS);
|
|
@@ -2244,6 +2285,88 @@ function broadcastToUsers(event, payload) {
|
|
|
2244
2285
|
relayClient?.broadcastToUsers(event, payload);
|
|
2245
2286
|
}
|
|
2246
2287
|
|
|
2288
|
+
// src/layout.ts
|
|
2289
|
+
import fs8 from "fs";
|
|
2290
|
+
import path9 from "path";
|
|
2291
|
+
function resolveMaybeRelativePath(stateDir, p) {
|
|
2292
|
+
if (path9.isAbsolute(p)) return path9.resolve(p);
|
|
2293
|
+
return path9.resolve(stateDir, p);
|
|
2294
|
+
}
|
|
2295
|
+
function listWorkspaceFallbacks(stateDir) {
|
|
2296
|
+
let entries;
|
|
2297
|
+
try {
|
|
2298
|
+
entries = fs8.readdirSync(stateDir, { withFileTypes: true });
|
|
2299
|
+
} catch {
|
|
2300
|
+
return [];
|
|
2301
|
+
}
|
|
2302
|
+
return entries.filter((entry) => entry.isDirectory() && (entry.name === "workspace" || entry.name.startsWith("workspace-"))).map((entry) => {
|
|
2303
|
+
const agentId = entry.name === "workspace" ? "main" : entry.name.replace("workspace-", "");
|
|
2304
|
+
const workspacePath = path9.join(stateDir, entry.name);
|
|
2305
|
+
return {
|
|
2306
|
+
agentId,
|
|
2307
|
+
path: workspacePath,
|
|
2308
|
+
source: "filesystem",
|
|
2309
|
+
exists: true
|
|
2310
|
+
};
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
function readOpenclawConfig(configPath) {
|
|
2314
|
+
try {
|
|
2315
|
+
const raw = fs8.readFileSync(configPath, "utf-8");
|
|
2316
|
+
return JSON.parse(raw);
|
|
2317
|
+
} catch {
|
|
2318
|
+
return null;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
function resolveGatewayLayout() {
|
|
2322
|
+
const stateDir = getOpenclawStateDir();
|
|
2323
|
+
const configPath = path9.join(stateDir, "openclaw.json");
|
|
2324
|
+
const config = readOpenclawConfig(configPath);
|
|
2325
|
+
const workspaces = [];
|
|
2326
|
+
if (config?.agents?.main?.workspace || config?.agents?.main?.workspacePath) {
|
|
2327
|
+
const rawPath = config.agents.main.workspace ?? config.agents.main.workspacePath;
|
|
2328
|
+
if (rawPath) {
|
|
2329
|
+
const resolvedPath = resolveMaybeRelativePath(stateDir, rawPath);
|
|
2330
|
+
workspaces.push({
|
|
2331
|
+
agentId: "main",
|
|
2332
|
+
path: resolvedPath,
|
|
2333
|
+
source: "config",
|
|
2334
|
+
exists: fs8.existsSync(resolvedPath)
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
for (const agent of config?.agents?.list ?? []) {
|
|
2339
|
+
const agentId = typeof agent.id === "string" && agent.id.trim() ? agent.id : null;
|
|
2340
|
+
const rawPath = agent.workspace ?? agent.workspacePath;
|
|
2341
|
+
if (!agentId || !rawPath) continue;
|
|
2342
|
+
const resolvedPath = resolveMaybeRelativePath(stateDir, rawPath);
|
|
2343
|
+
workspaces.push({
|
|
2344
|
+
agentId,
|
|
2345
|
+
path: resolvedPath,
|
|
2346
|
+
source: "config",
|
|
2347
|
+
exists: fs8.existsSync(resolvedPath)
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
2351
|
+
for (const ws of [...workspaces, ...listWorkspaceFallbacks(stateDir)]) {
|
|
2352
|
+
if (!deduped.has(ws.agentId)) {
|
|
2353
|
+
deduped.set(ws.agentId, ws);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
const resolvedWorkspaces = Array.from(deduped.values());
|
|
2357
|
+
const mainWorkspace = resolvedWorkspaces.find((ws) => ws.agentId === "main");
|
|
2358
|
+
const defaultFileBrowserRoot = mainWorkspace?.path ?? stateDir;
|
|
2359
|
+
return {
|
|
2360
|
+
stateDir,
|
|
2361
|
+
configPath,
|
|
2362
|
+
mediaDir: path9.join(stateDir, "media"),
|
|
2363
|
+
skillsDir: path9.join(stateDir, "skills"),
|
|
2364
|
+
extensionsDir: path9.join(stateDir, "extensions"),
|
|
2365
|
+
defaultFileBrowserRoot,
|
|
2366
|
+
workspaces: resolvedWorkspaces
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2247
2370
|
// src/index.ts
|
|
2248
2371
|
function squadAppPlugin(api) {
|
|
2249
2372
|
const toolExecutors = /* @__PURE__ */ new Map();
|
|
@@ -2326,6 +2449,17 @@ function squadAppPlugin(api) {
|
|
|
2326
2449
|
respond(true, { tools: [...coreTools, ...groups, ...pluginTools] });
|
|
2327
2450
|
}
|
|
2328
2451
|
);
|
|
2452
|
+
api.registerGatewayMethod(
|
|
2453
|
+
"squad.layout.get",
|
|
2454
|
+
async ({ respond }) => {
|
|
2455
|
+
try {
|
|
2456
|
+
respond(true, resolveGatewayLayout());
|
|
2457
|
+
} catch (err2) {
|
|
2458
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
2459
|
+
respond(false, { errorMessage: msg });
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
);
|
|
2329
2463
|
const relayState = readRelayState();
|
|
2330
2464
|
const relayEnabled = !!(relayState.claimToken || relayState.roomId);
|
|
2331
2465
|
if (relayEnabled) {
|
package/package.json
CHANGED