squad-openclaw 2026.2.1905 → 2026.2.1906

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 ADDED
@@ -0,0 +1,139 @@
1
+ # squad-openclaw
2
+
3
+ OpenClaw gateway plugin for [Squad](https://squad.ceo) — provides entity registry, filesystem tools, SQL queries, version management, and a cloud relay client for remote browser access.
4
+
5
+ ## Features
6
+
7
+ | Tool / Method | Description |
8
+ |---|---|
9
+ | `entity_list`, `entity_search`, `entity_sync` | In-memory entity registry with filesystem watching (agents, skills, plugins, tools, media) |
10
+ | `fs_read`, `fs_write`, `fs_list`, `fs_delete`, `fs_rename`, `fs_mkdir` | Remote filesystem access for browser clients (subject to security restrictions below) |
11
+ | `sql_query` | Restricted SQLite query tool — `sqlite3` only, scoped to `~/.openclaw/squad-ceo-data/` |
12
+ | `squad.version.check`, `squad.version.update` | Plugin version management and self-update |
13
+ | `tools.invoke` | RPC-based tool invocation for relay mode (WebSocket) |
14
+ | Cloud relay client | Connects outbound to `relay.squad.ceo` for remote browser access |
15
+
16
+ ## Security Model
17
+
18
+ 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.
19
+
20
+ ### Layer 1: Blocked Directories (hardcoded, non-configurable)
21
+
22
+ These directories are **completely blocked** from all filesystem operations (read, write, list, delete, rename):
23
+
24
+ | Path | Contents |
25
+ |---|---|
26
+ | `~/.openclaw/credentials/` | OAuth tokens, API keys |
27
+ | `~/.openclaw/devices/` | Device pairing secrets, private keys |
28
+ | `~/.openclaw/identity/` | Operator identity material |
29
+
30
+ ### Layer 1b: Blocked Files (hardcoded, non-configurable)
31
+
32
+ | Path | Reason |
33
+ |---|---|
34
+ | `~/.openclaw/squad-ceo-data/squad-relay.json` | Contains ed25519 private key for relay device identity |
35
+ | `~/.openclaw/*.bak` | Backup files at the top level contain unredacted config (tokens, keys) that would bypass redaction |
36
+
37
+ ### Layer 2: Redacted Files (hardcoded, non-configurable)
38
+
39
+ `~/.openclaw/openclaw.json` is **readable** but with sensitive fields replaced with `"[REDACTED]"` before returning to clients:
40
+
41
+ - `channels.*.botToken` — channel bot tokens
42
+ - `gateway.auth.*` — all auth keys
43
+ - `gateway.token` — legacy token location
44
+ - `gateway.remote.token` — legacy remote token
45
+
46
+ ### Layer 3: Allowed Roots (configurable, defaults to `~/.openclaw`)
47
+
48
+ Filesystem operations are restricted to configured root directories. By default, only `~/.openclaw/` is accessible — covering all SPA needs:
49
+
50
+ - `~/.openclaw/squad-ceo-data/` — entity databases, data files
51
+ - `~/.openclaw/media/` — asset uploads
52
+ - `~/.openclaw/workspace*/` — agent workspaces
53
+ - `~/.openclaw/skills/` — skill definitions
54
+ - `~/.openclaw/extensions/` — plugin manifests
55
+
56
+ Operators can customize via the `fs.allowedRoots` config option.
57
+
58
+ ### Layer 4: Write Protection (hardcoded, non-configurable)
59
+
60
+ These files/directories cannot be written to, even if they fall within `allowedRoots`:
61
+
62
+ - `~/.openclaw/openclaw.json` — operator configuration (read-only with redaction)
63
+ - `~/.openclaw/squad-ceo-data/squad-relay.json` — relay device private key
64
+ - All blocked directories above (credentials, devices, identity)
65
+ - All `.bak` files at `~/.openclaw/` top level
66
+
67
+ ## Relay Security
68
+
69
+ The cloud relay enables remote browser access to the gateway through `relay.squad.ceo`.
70
+
71
+ ### Opt-in Only
72
+
73
+ The relay is **disabled by default**. It must be explicitly enabled by setting `relay.enabled: true` in the plugin configuration. No outbound network connections are made unless the operator opts in.
74
+
75
+ ### Authentication
76
+
77
+ - **Browser to Relay:** JWT authentication (email/password or Google OAuth)
78
+ - **Relay to Gateway:** Single-use claim token (24h expiry) for first connection, stored room ID for reconnection
79
+ - Claim tokens are generated per-user during setup — **not** open access
80
+
81
+ ### Device Pairing
82
+
83
+ The relay-client's device identity must be **explicitly approved by the operator** before it can proxy user connections to the gateway. The plugin does **not** auto-pair or self-approve. During the onboarding flow, the AI guides the user through reading the device identity and confirming approval.
84
+
85
+ ### E2E Encryption
86
+
87
+ - **Protocol:** ECDH (P-256) key exchange + AES-256-GCM message encryption
88
+ - **No plaintext fallback:** If encryption fails after E2E is established, messages are dropped — never sent as plaintext
89
+ - **Status:** Implemented in the plugin. Currently disabled at the relay level (Durable Object) due to multi-tab session safety. When per-session E2E is enabled at the relay, the plugin is already hardened.
90
+
91
+ ### Operator Token
92
+
93
+ The relay-client reads `gateway.auth.token` from `~/.openclaw/openclaw.json` via direct `fs.readFileSync`. This is intentional and safe:
94
+
95
+ - The relay-client runs **server-side, in the gateway's own process** — equivalent to the gateway reading its own config
96
+ - The token is **never sent to the relay server or the browser** — only injected into local `localhost:18789` WebSocket connections
97
+ - The token is **never exposed through the filesystem tool API** — `gateway.auth.*` is redacted in `filesystem.ts`
98
+ - Direct file read is used because the plugin config API doesn't expose the full gateway config
99
+
100
+ ## SQL Query Tool
101
+
102
+ The `sql_query` tool provides restricted SQLite access:
103
+
104
+ - **Path restriction:** Database files must be within `~/.openclaw/squad-ceo-data/`
105
+ - **No shell:** Uses `execFile` (not `exec`) — arguments are passed as an argv array, preventing command injection
106
+ - **No arbitrary commands:** Only `sqlite3` is executed
107
+
108
+ ## Build Transparency
109
+
110
+ The build configuration (`tsup.config.ts`) is optimized for security auditing:
111
+
112
+ | Setting | Value | Reason |
113
+ |---|---|---|
114
+ | `minify` | `false` | Unminified bundle for human/AI security review |
115
+ | `sourcemap` | `false` | No internal path exposure |
116
+ | `treeshake` | `false` | All code preserved for complete auditing |
117
+
118
+ ## Configuration
119
+
120
+ Configure in your gateway's `openclaw.json` under the plugin section:
121
+
122
+ | Key | Type | Default | Description |
123
+ |---|---|---|---|
124
+ | `relay.enabled` | `boolean` | `false` | Enable cloud relay. Opt-in required. |
125
+ | `relay.url` | `string` | `wss://relay.squad.ceo` | Cloud relay WebSocket URL |
126
+ | `fs.allowedRoots` | `string[]` | `["~/.openclaw"]` | Restrict filesystem operations to these directories |
127
+
128
+ ## Source Code
129
+
130
+ - **Repository:** [github.com/WorldBrain/squad](https://github.com/WorldBrain/squad)
131
+ - **Plugin directory:** `extensions/squad-openclaw/`
132
+ - **Security-critical files:**
133
+ - `src/filesystem.ts` — path blocking, redaction, write protection
134
+ - `src/sql.ts` — restricted SQL execution
135
+ - `src/relay-client.ts` — relay authentication, E2E encryption, device identity
136
+
137
+ ## License
138
+
139
+ MIT
package/dist/index.js CHANGED
@@ -280,10 +280,13 @@ function registryList(type) {
280
280
  if (!type) return all;
281
281
  return all.filter((e) => e.type === type);
282
282
  }
283
- var IDENTITY_NAME_RE = /\*\*Name:\*\*\s*\n\s*(.+?)(?=\n|$)/;
283
+ var IDENTITY_NAME_RE = /\*\*Name:\*\*\s*\n?\s*(.+?)(?=\n|$)/;
284
284
  function parseIdentityName(content) {
285
285
  const match = content.match(IDENTITY_NAME_RE);
286
- return match?.[1]?.trim() || null;
286
+ const name = match?.[1]?.trim();
287
+ if (!name) return null;
288
+ if (/^_\(.+\)_$/.test(name)) return null;
289
+ return name;
287
290
  }
288
291
  function scanAgents(configDir) {
289
292
  const now = Date.now();
@@ -639,6 +642,9 @@ function isSensitivePath(resolvedPath) {
639
642
  return true;
640
643
  }
641
644
  }
645
+ if (path3.dirname(resolvedPath) === OPENCLAW_DIR && resolvedPath.endsWith(".bak")) {
646
+ return true;
647
+ }
642
648
  return false;
643
649
  }
644
650
  var OPENCLAW_JSON_FILENAME = "openclaw.json";
@@ -770,7 +776,8 @@ function filterSensitiveEntries(entries) {
770
776
  });
771
777
  }
772
778
  function registerFilesystemTools(api) {
773
- const allowedRoots = api.pluginConfig?.["fs.allowedRoots"] ?? [];
779
+ const DEFAULT_ALLOWED_ROOTS = ["~/.openclaw"];
780
+ const allowedRoots = api.pluginConfig?.["fs.allowedRoots"] ?? DEFAULT_ALLOWED_ROOTS;
774
781
  api.registerTool({
775
782
  name: "fs_read",
776
783
  label: "Read File",
@@ -1319,38 +1326,21 @@ function loadOrCreateRelayDeviceKeys() {
1319
1326
  console.log(`[relay-client] Generated relay device identity: ${deviceId.substring(0, 12)}...`);
1320
1327
  return keys;
1321
1328
  }
1322
- function ensureDevicePaired(keys, operatorToken) {
1329
+ function writeDeviceInfoFile(keys) {
1323
1330
  const stateDir = process.env.OPENCLAW_STATE_DIR || path6.join(os.homedir(), ".openclaw");
1324
- const pairedPath = path6.join(stateDir, "devices", "paired.json");
1325
- let paired = {};
1326
- try {
1327
- paired = JSON.parse(fs6.readFileSync(pairedPath, "utf-8"));
1328
- } catch {
1329
- }
1330
- if (paired[keys.deviceId]) return false;
1331
- const now = Date.now();
1332
- const scopes = ["operator.admin", "operator.approvals", "operator.pairing"];
1333
- paired[keys.deviceId] = {
1331
+ const infoPath = path6.join(stateDir, "squad-ceo-data", "relay-device-info.json");
1332
+ const info = {
1334
1333
  deviceId: keys.deviceId,
1335
1334
  publicKey: keys.publicKey,
1335
+ displayName: "squad-relay",
1336
1336
  platform: process.platform,
1337
- clientId: "cli",
1338
- clientMode: "backend",
1339
- role: "operator",
1340
- roles: ["operator"],
1341
- scopes,
1342
- tokens: operatorToken ? {
1343
- operator: { token: operatorToken, role: "operator", scopes, createdAtMs: now, lastUsedAtMs: now }
1344
- } : {},
1345
- createdAtMs: now,
1346
- approvedAtMs: now,
1347
- displayName: "squad-relay"
1337
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1348
1338
  };
1349
- const dir = path6.dirname(pairedPath);
1350
- if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
1351
- fs6.writeFileSync(pairedPath, JSON.stringify(paired, null, 2), { mode: 384 });
1352
- console.log(`[relay-client] Device pairing entry created in paired.json`);
1353
- return true;
1339
+ try {
1340
+ fs6.writeFileSync(infoPath, JSON.stringify(info, null, 2));
1341
+ } catch (err2) {
1342
+ console.error("[relay-client] Failed to write relay-device-info.json:", err2);
1343
+ }
1354
1344
  }
1355
1345
  function signDeviceIdentity(keys, clientId, clientMode, role, scopes, token, challengeNonce) {
1356
1346
  const signedAtMs = Date.now();
@@ -1391,11 +1381,8 @@ var RelayClient = class {
1391
1381
  };
1392
1382
  this.pendingClaimToken = this.config.roomId ? null : this.config.claimToken;
1393
1383
  this.deviceKeys = loadOrCreateRelayDeviceKeys();
1394
- const newEntry = ensureDevicePaired(this.deviceKeys, this.config.operatorToken);
1395
- if (newEntry) {
1396
- console.log("[relay-client] New device pairing entry created \u2014 restarting gateway to reload pairing store...");
1397
- setTimeout(() => process.exit(0), 2e3);
1398
- }
1384
+ writeDeviceInfoFile(this.deviceKeys);
1385
+ console.log(`[relay-client] Device ID: ${this.deviceKeys.deviceId}`);
1399
1386
  }
1400
1387
  /** Start connecting to the relay */
1401
1388
  start() {
@@ -1687,7 +1674,16 @@ var RelayClient = class {
1687
1674
  }
1688
1675
  });
1689
1676
  localWs.on("close", (code, reason) => {
1690
- console.log(`[relay-client] Local WS for user ${userId} closed: ${code} ${reason.toString()}`);
1677
+ const reasonStr = reason.toString();
1678
+ console.log(`[relay-client] Local WS for user ${userId} closed: ${code} ${reasonStr}`);
1679
+ if (code === 1008) {
1680
+ console.error(
1681
+ `[relay-client] Device not paired with gateway. To approve, run on the gateway machine:
1682
+ openclaw devices approve
1683
+ Or follow the onboarding instructions in the Squad web app.
1684
+ Device ID: ${this.deviceKeys.deviceId}`
1685
+ );
1686
+ }
1691
1687
  const current = this.userConnections.get(userId);
1692
1688
  if (current && current.localWs === localWs) {
1693
1689
  this.userConnections.delete(userId);
@@ -1743,8 +1739,8 @@ var RelayClient = class {
1743
1739
  const encrypted = conn.e2e.encrypt(JSON.stringify(msg));
1744
1740
  innerMsg = { _e2e: true, ...encrypted };
1745
1741
  } catch (err2) {
1746
- console.error(`[relay-client] E2E encrypt error for ${userId}:`, err2);
1747
- innerMsg = msg;
1742
+ console.error(`[relay-client] E2E encrypt failed for ${userId} \u2014 dropping message:`, err2);
1743
+ return;
1748
1744
  }
1749
1745
  }
1750
1746
  this.sendToRelay({
@@ -1807,8 +1803,8 @@ var RelayClient = class {
1807
1803
  const encrypted = conn.e2e.encrypt(JSON.stringify(msg));
1808
1804
  innerMsg = { _e2e: true, ...encrypted };
1809
1805
  } catch (err2) {
1810
- console.error(`[relay-client] E2E encrypt error for broadcast to ${userId}:`, err2);
1811
- innerMsg = msg;
1806
+ console.error(`[relay-client] E2E encrypt failed for broadcast to ${userId} \u2014 skipping:`, err2);
1807
+ continue;
1812
1808
  }
1813
1809
  }
1814
1810
  this.sendToRelay({
@@ -1887,7 +1883,7 @@ function squadAppPlugin(api) {
1887
1883
  }
1888
1884
  }
1889
1885
  );
1890
- const relayEnabled = api.pluginConfig?.["relay.enabled"] ?? true;
1886
+ const relayEnabled = api.pluginConfig?.["relay.enabled"] ?? false;
1891
1887
  if (relayEnabled) {
1892
1888
  const relayUrl = api.pluginConfig?.["relay.url"] || "wss://relay.squad.ceo";
1893
1889
  startRelayClient(api, relayUrl);
@@ -9,12 +9,13 @@
9
9
  "fs.allowedRoots": {
10
10
  "type": "array",
11
11
  "items": { "type": "string" },
12
- "description": "Restrict fs_read/fs_write/fs_list/fs_delete/fs_rename to these directories. Empty or omitted = allow all."
12
+ "default": ["~/.openclaw"],
13
+ "description": "Restrict filesystem operations to these directories. Defaults to [\"~/.openclaw\"]. Hardcoded blocks on credentials/, devices/, identity/, squad-relay.json, and .bak files always apply."
13
14
  },
14
15
  "relay.enabled": {
15
16
  "type": "boolean",
16
- "default": true,
17
- "description": "Enable cloud relay for remote browser access. Enabled by default."
17
+ "default": false,
18
+ "description": "Enable cloud relay for remote browser access. Disabled by default — opt-in required."
18
19
  },
19
20
  "relay.url": {
20
21
  "type": "string",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squad-openclaw",
3
- "version": "2026.2.1905",
3
+ "version": "2026.2.1906",
4
4
  "description": "Entity registry, filesystem tools, and version management plugin for OpenClaw gateway",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -16,9 +16,11 @@
16
16
  "./dist/index.js"
17
17
  ]
18
18
  },
19
+ "license": "MIT",
19
20
  "files": [
20
21
  "dist/",
21
- "openclaw.plugin.json"
22
+ "openclaw.plugin.json",
23
+ "README.md"
22
24
  ],
23
25
  "scripts": {
24
26
  "build": "tsup",