squad-openclaw 2026.2.2017 → 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.
Files changed (3) hide show
  1. package/README.md +33 -3
  2. package/dist/index.js +95 -2
  3. 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 the `OPENCLAW_STATE_DIR` environment variable when set. This supports Docker and other containerized deployments where the OpenClaw data directory may not be at the default location.
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 | `/root/data/.openclaw` | `OPENCLAW_STATE_DIR=/root/data/.openclaw` |
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[]` | `["~/.openclaw"]` | Restrict filesystem operations to these directories. Hardcoded blocks on `credentials/`, `devices/`, `identity/`, `relay/squad-relay.json`, and `.bak` files always apply regardless. |
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 = ["~/.openclaw"];
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",
@@ -2285,6 +2285,88 @@ function broadcastToUsers(event, payload) {
2285
2285
  relayClient?.broadcastToUsers(event, payload);
2286
2286
  }
2287
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
+
2288
2370
  // src/index.ts
2289
2371
  function squadAppPlugin(api) {
2290
2372
  const toolExecutors = /* @__PURE__ */ new Map();
@@ -2367,6 +2449,17 @@ function squadAppPlugin(api) {
2367
2449
  respond(true, { tools: [...coreTools, ...groups, ...pluginTools] });
2368
2450
  }
2369
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
+ );
2370
2463
  const relayState = readRelayState();
2371
2464
  const relayEnabled = !!(relayState.claimToken || relayState.roomId);
2372
2465
  if (relayEnabled) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squad-openclaw",
3
- "version": "2026.2.2017",
3
+ "version": "2026.2.2018",
4
4
  "description": "Entity registry, filesystem tools, and version management plugin for OpenClaw gateway",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",