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 +139 -0
- package/dist/index.js +37 -41
- package/openclaw.plugin.json +4 -3
- package/package.json +4 -2
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
|
|
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
|
-
|
|
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
|
|
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
|
|
1329
|
+
function writeDeviceInfoFile(keys) {
|
|
1323
1330
|
const stateDir = process.env.OPENCLAW_STATE_DIR || path6.join(os.homedir(), ".openclaw");
|
|
1324
|
-
const
|
|
1325
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
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
|
-
|
|
1395
|
-
|
|
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
|
-
|
|
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
|
|
1747
|
-
|
|
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
|
|
1811
|
-
|
|
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"] ??
|
|
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);
|
package/openclaw.plugin.json
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
"fs.allowedRoots": {
|
|
10
10
|
"type": "array",
|
|
11
11
|
"items": { "type": "string" },
|
|
12
|
-
"
|
|
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":
|
|
17
|
-
"description": "Enable cloud relay for remote browser access.
|
|
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.
|
|
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",
|