squad-openclaw 2026.2.2007 → 2026.2.2008
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 +109 -32
- package/dist/index.js +59 -57
- package/openclaw.plugin.json +0 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,12 +11,28 @@ OpenClaw gateway plugin for [Squad](https://squad.ceo) — provides entity regis
|
|
|
11
11
|
| `sql_query` | Restricted SQLite query tool — `sqlite3` only, scoped to `~/.openclaw/squad-ceo-data/` |
|
|
12
12
|
| `squad.version.check`, `squad.version.update` | Plugin version management and self-update |
|
|
13
13
|
| `tools.invoke` | RPC-based tool invocation for relay mode — **only invokes this plugin's own tools**, each with its own security restrictions (see below) |
|
|
14
|
-
| Cloud relay client |
|
|
14
|
+
| Cloud relay client | Connects outbound to `relay.squad.ceo` for remote browser access. **Only activates when a claim token or room ID exists** (see Relay Security below) |
|
|
15
|
+
|
|
16
|
+
## State Directory Resolution
|
|
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.
|
|
19
|
+
|
|
20
|
+
| Environment | Typical path | How it's resolved |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| Standard install | `~/.openclaw` | Default — `os.homedir() + "/.openclaw"` |
|
|
23
|
+
| Docker | `/root/data/.openclaw` | `OPENCLAW_STATE_DIR=/root/data/.openclaw` |
|
|
24
|
+
| Custom / NAS | `/mnt/data/.openclaw` | `OPENCLAW_STATE_DIR=/mnt/data/.openclaw` |
|
|
25
|
+
|
|
26
|
+
**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
|
+
|
|
28
|
+
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.
|
|
15
29
|
|
|
16
30
|
## Security Model
|
|
17
31
|
|
|
18
32
|
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
33
|
|
|
34
|
+
> **Note:** Throughout this section, `~/.openclaw` refers to the resolved state directory (see above). In Docker or custom installs, substitute the actual path set by `OPENCLAW_STATE_DIR`.
|
|
35
|
+
|
|
20
36
|
### Layer 1: Blocked Directories (hardcoded, non-configurable)
|
|
21
37
|
|
|
22
38
|
These directories are **completely blocked** from all filesystem operations (read, write, list, delete, rename):
|
|
@@ -45,7 +61,7 @@ These directories are **completely blocked** from all filesystem operations (rea
|
|
|
45
61
|
|
|
46
62
|
### Layer 3: Allowed Roots (configurable, defaults to `~/.openclaw`)
|
|
47
63
|
|
|
48
|
-
Filesystem operations are restricted to configured root directories. By default, only `~/.openclaw/` is accessible — covering all
|
|
64
|
+
Filesystem operations are restricted to configured root directories. By default, only `~/.openclaw/` is accessible — covering all application needs:
|
|
49
65
|
|
|
50
66
|
- `~/.openclaw/squad-ceo-data/` — entity databases, data files
|
|
51
67
|
- `~/.openclaw/media/` — asset uploads
|
|
@@ -66,51 +82,114 @@ These files/directories cannot be written to, even if they fall within `allowedR
|
|
|
66
82
|
|
|
67
83
|
## Relay Security
|
|
68
84
|
|
|
69
|
-
|
|
85
|
+
The cloud relay enables remote browser access to the gateway through `relay.squad.ceo`. This section explains the full architecture for security reviewers.
|
|
86
|
+
|
|
87
|
+
### When Does the Relay Activate?
|
|
70
88
|
|
|
71
|
-
The
|
|
89
|
+
The relay client **only activates** when the relay state file (`~/.openclaw/squad-ceo-data/relay/squad-relay.json`) contains a `claimToken` or `roomId`. This file does not exist by default — it is created when the user runs the onboarding prompt from the Squad web app:
|
|
72
90
|
|
|
73
|
-
|
|
91
|
+
```bash
|
|
92
|
+
mkdir -p ~/.openclaw/squad-ceo-data/relay && \
|
|
93
|
+
echo '{"claimToken":"<token>"}' > ~/.openclaw/squad-ceo-data/relay/squad-relay.json
|
|
94
|
+
```
|
|
74
95
|
|
|
75
|
-
|
|
96
|
+
**If this file does not exist or contains neither key, no relay code runs, no WebSocket is opened, and no connection is made to any external server.** The plugin's entry point (`index.ts`) checks this before calling `startRelayClient()`.
|
|
76
97
|
|
|
77
|
-
|
|
98
|
+
The claim token is a short-lived, single-use code generated by the relay server for the authenticated user. It links this gateway to the user's Squad account. Once consumed, the relay returns a `roomId` for future reconnections, and the claim token is no longer needed.
|
|
78
99
|
|
|
79
|
-
|
|
80
|
-
- **Relay to Gateway:** Single-use claim token (24h expiry) for first connection, stored room ID for reconnection
|
|
81
|
-
- Claim tokens are generated per-user during setup — **not** open access
|
|
100
|
+
### How the Relay Connection Works
|
|
82
101
|
|
|
83
|
-
|
|
102
|
+
```
|
|
103
|
+
┌──────────────┐ JWT auth ┌──────────────────┐ outbound WS ┌────────────────┐
|
|
104
|
+
│ Browser SPA │ ◄──────────────────────► │ relay.squad.ceo │ ◄────────────────────────► │ relay-client │
|
|
105
|
+
│ (squad.ceo) │ (wss://.../user) │ (CF Worker + DO)│ (wss://.../gw) │ (this plugin) │
|
|
106
|
+
└──────────────┘ └──────────────────┘ └───────┬────────┘
|
|
107
|
+
│
|
|
108
|
+
per-user local WS │
|
|
109
|
+
(ws://127.0.0.1: │
|
|
110
|
+
18789) │
|
|
111
|
+
▼
|
|
112
|
+
┌──────────────────┐
|
|
113
|
+
│ OpenClaw Gateway │
|
|
114
|
+
│ (localhost only) │
|
|
115
|
+
└──────────────────┘
|
|
116
|
+
```
|
|
84
117
|
|
|
85
|
-
|
|
118
|
+
1. **Browser → Relay:** The user authenticates with JWT (email/password or Google OAuth). The browser connects via WebSocket to the relay.
|
|
119
|
+
2. **Relay-client → Relay:** The plugin opens an outbound WebSocket to `relay.squad.ceo` using the claim token (first connect) or stored room ID (reconnect). This is the **only** outbound connection the plugin makes.
|
|
120
|
+
3. **Relay-client → Gateway:** For each browser user, the relay-client opens a **separate local WebSocket** to `ws://127.0.0.1:18789` (the gateway's loopback port). Each user gets an isolated session — the gateway sees them as individual clients.
|
|
86
121
|
|
|
87
|
-
###
|
|
122
|
+
### What Data Crosses the Network (relay.squad.ceo)
|
|
88
123
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
124
|
+
The relay server is a message router. Here is exactly what it sees and does not see:
|
|
125
|
+
|
|
126
|
+
| Data | Crosses relay? | Notes |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| Relay protocol envelopes (`relay.forward`, `relay.hello`, etc.) | **Yes** | Routing metadata only |
|
|
129
|
+
| User ID (for message routing) | **Yes** | The relay routes by userId |
|
|
130
|
+
| Gateway device ID and public key | **Yes** | Sent in `relay.hello` for identification |
|
|
131
|
+
| Inner messages (gateway ↔ browser) | **Yes, but opaque** | Wrapped inside `relay.forward` envelopes |
|
|
132
|
+
| **Operator auth token** | **NEVER** | Injected into the local WS connect request **after** the relay boundary (see below) |
|
|
133
|
+
| **Device identity signature** | **NEVER** | Added to the local WS connect handshake only |
|
|
134
|
+
| **Gateway connect handshake params** | **NEVER** | The `connect` request with auth + device identity is sent to `localhost:18789`, not the relay |
|
|
135
|
+
| **Plaintext RPC payloads** (when E2E active) | **NEVER** | E2E encrypts before relay; relay sees ciphertext only |
|
|
136
|
+
|
|
137
|
+
### Operator Token — Never Leaves the Server
|
|
92
138
|
|
|
93
|
-
|
|
139
|
+
The operator token (`gateway.auth.token` in `~/.openclaw/openclaw.json`) is the most sensitive credential. Here is exactly how it flows:
|
|
94
140
|
|
|
95
|
-
The relay-client reads
|
|
141
|
+
1. **Read:** The relay-client reads the token from `~/.openclaw/openclaw.json` via `fs.readFileSync` at startup. This is equivalent to the gateway reading its own config — the plugin runs in the gateway's process.
|
|
96
142
|
|
|
97
|
-
|
|
98
|
-
- The token is **never sent to the relay server or the browser** — it is only injected into **local** `localhost:18789` WebSocket connections on the same machine
|
|
99
|
-
- The token is **never exposed through the filesystem tool API** — `gateway.auth.*` is redacted in `filesystem.ts`
|
|
100
|
-
- Direct file read is used because the plugin config API doesn't expose the full gateway config
|
|
143
|
+
2. **Stored:** The token is held in memory (`this.config.operatorToken`) for the lifetime of the relay-client. It is **never written** to any file, log, or external service.
|
|
101
144
|
|
|
102
|
-
**
|
|
145
|
+
3. **Used:** When a browser user's `connect` request arrives from the relay, the relay-client **injects** the operator token into the request **in memory**, then sends the modified request to `ws://127.0.0.1:18789` (localhost only). The relay server never sees this injection — it only sees the outer `relay.forward` envelope.
|
|
146
|
+
|
|
147
|
+
4. **Protected via filesystem API:** The token is redacted from `~/.openclaw/openclaw.json` when read through this plugin's `fs_read` tool (`gateway.auth.*` → `"[REDACTED]"`). Remote clients (browser, agents) cannot read the token through any tool this plugin provides.
|
|
103
148
|
|
|
104
149
|
```
|
|
105
|
-
Browser ──[connect request]
|
|
150
|
+
Browser ──[connect request]──► relay.squad.ceo ──[relay.forward]──► relay-client
|
|
106
151
|
│
|
|
107
152
|
Token is injected HERE, in memory, │
|
|
108
153
|
into the connect request. │
|
|
109
154
|
▼
|
|
110
|
-
relay-client ──[modified request]
|
|
155
|
+
relay-client ──[modified request]──► localhost:18789 (gateway)
|
|
111
156
|
```
|
|
112
157
|
|
|
113
|
-
|
|
158
|
+
**A compromised relay server cannot intercept the operator token** because the token is injected after the relay boundary — it only exists on the `localhost:18789` path between the relay-client and the gateway, both running on the same machine.
|
|
159
|
+
|
|
160
|
+
### Device Identity and Auto-Pairing
|
|
161
|
+
|
|
162
|
+
The relay-client generates an ed25519 keypair on first run (`device-keys.ts`). The device identity is used to sign the `connect` handshake to the local gateway using the v2 signature protocol:
|
|
163
|
+
|
|
164
|
+
- **Signature payload:** `v2|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce` (pipe-delimited)
|
|
165
|
+
- **deviceId:** SHA-256 fingerprint of the ed25519 public key (hex, 64 chars)
|
|
166
|
+
- **publicKey:** base64url-encoded ed25519 public key (no padding)
|
|
167
|
+
|
|
168
|
+
The gateway **auto-pairs** devices that connect with a valid operator token and a correctly signed device identity. No manual `openclaw devices approve` step is required. The authorization chain is:
|
|
169
|
+
|
|
170
|
+
1. The **user** authenticates with the Squad web app (JWT via email/password or Google OAuth)
|
|
171
|
+
2. The user generates a **claim token** (short-lived, single-use) and gives it to the gateway via the onboarding prompt
|
|
172
|
+
3. The plugin starts the relay-client, which connects to the relay with the claim token
|
|
173
|
+
4. When a browser user connects, the relay-client signs the connect handshake with its device key and the operator token
|
|
174
|
+
5. The gateway validates the signature and auto-pairs the device
|
|
175
|
+
|
|
176
|
+
The claim token is the user's explicit consent to link their gateway to their Squad account. The operator token is the proof of authorization on the gateway side. Together, they form a two-sided trust chain without requiring manual device approval.
|
|
177
|
+
|
|
178
|
+
### E2E Encryption
|
|
179
|
+
|
|
180
|
+
- **Protocol:** ECDH (P-256) key exchange + AES-256-GCM message encryption
|
|
181
|
+
- **No plaintext fallback:** If encryption fails after E2E is established, messages are **dropped** — never sent as plaintext. This is enforced in both `routeFromGateway()` and `broadcastToUsers()`.
|
|
182
|
+
- **Status:** Implemented in the plugin. Currently disabled at the relay level (Durable Object) due to multi-tab session safety — the relay blocks E2E key exchange to prevent mismatched keys across tabs. When per-session E2E is enabled at the relay, the plugin is already hardened.
|
|
183
|
+
|
|
184
|
+
### Authentication Chain Summary
|
|
185
|
+
|
|
186
|
+
For a browser user's RPC call to reach the gateway through the relay, **all of the following must be valid:**
|
|
187
|
+
|
|
188
|
+
1. **Browser JWT** — user authenticated with relay.squad.ceo
|
|
189
|
+
2. **Relay pairing** — user's account is paired with this gateway's Durable Object (via claim token)
|
|
190
|
+
3. **Relay-client connected** — plugin's outbound WS to relay is alive
|
|
191
|
+
4. **Gateway connect handshake** — relay-client's device identity + operator token accepted by local gateway
|
|
192
|
+
5. **Tool-level security** — each tool enforces its own restrictions (blocked dirs, redaction, write protection, SQL path limits)
|
|
114
193
|
|
|
115
194
|
## Remote Tool Invocation (`tools.invoke`)
|
|
116
195
|
|
|
@@ -125,8 +204,6 @@ The `tools.invoke` gateway method allows the browser to call plugin tools over W
|
|
|
125
204
|
|
|
126
205
|
It **cannot** invoke gateway core tools (`exec`, `bash`, `read`, `write`, `web_fetch`, etc.) — only the tools this plugin registers via `api.registerTool()`. Every invoked tool enforces its own security restrictions independently — `tools.invoke` is just a transport layer, not a privilege escalation.
|
|
127
206
|
|
|
128
|
-
**Authentication chain for relay access:** Browser JWT → relay claim token → operator-approved device pairing → operator auth token (localhost only). All four must be valid for a `tools.invoke` call to reach the gateway.
|
|
129
|
-
|
|
130
207
|
## SQL Query Tool
|
|
131
208
|
|
|
132
209
|
> **`sql_query` can only access the plugin's own application data** in `~/.openclaw/squad-ceo-data/`. It cannot read or modify any other files on the system — not system databases, not user documents, not gateway configuration.
|
|
@@ -154,9 +231,7 @@ Configure in your gateway's `openclaw.json` under the plugin section:
|
|
|
154
231
|
|
|
155
232
|
| Key | Type | Default | Description |
|
|
156
233
|
|---|---|---|---|
|
|
157
|
-
| `
|
|
158
|
-
| `relay.url` | `string` | `wss://relay.squad.ceo` | Cloud relay WebSocket URL |
|
|
159
|
-
| `fs.allowedRoots` | `string[]` | `["~/.openclaw"]` | Restrict filesystem operations to these directories |
|
|
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. |
|
|
160
235
|
|
|
161
236
|
## Source Code
|
|
162
237
|
|
|
@@ -165,7 +240,9 @@ Configure in your gateway's `openclaw.json` under the plugin section:
|
|
|
165
240
|
- **Security-critical files:**
|
|
166
241
|
- `src/filesystem.ts` — path blocking, redaction, write protection
|
|
167
242
|
- `src/sql.ts` — restricted SQL execution
|
|
168
|
-
- `src/relay-client.ts` — relay authentication, E2E encryption, device identity
|
|
243
|
+
- `src/relay-client.ts` — relay authentication, E2E encryption, device identity, operator token handling
|
|
244
|
+
- `src/device-keys.ts` — ed25519 key generation, device identity management
|
|
245
|
+
- `src/e2e-crypto.ts` — ECDH key exchange, AES-256-GCM encryption
|
|
169
246
|
|
|
170
247
|
## License
|
|
171
248
|
|
package/dist/index.js
CHANGED
|
@@ -104,7 +104,7 @@ function registerAgentMethods(api) {
|
|
|
104
104
|
|
|
105
105
|
// src/entities.ts
|
|
106
106
|
import { Type as T } from "@sinclair/typebox";
|
|
107
|
-
import
|
|
107
|
+
import path4 from "path";
|
|
108
108
|
import fs3 from "fs";
|
|
109
109
|
|
|
110
110
|
// src/watcher.ts
|
|
@@ -361,20 +361,29 @@ function startWatcher(configDir, onFsChange) {
|
|
|
361
361
|
|
|
362
362
|
// src/filesystem.ts
|
|
363
363
|
import fs2 from "fs";
|
|
364
|
+
import path3 from "path";
|
|
365
|
+
|
|
366
|
+
// src/paths.ts
|
|
364
367
|
import path2 from "path";
|
|
368
|
+
import os from "os";
|
|
369
|
+
function getOpenclawStateDir() {
|
|
370
|
+
return process.env.OPENCLAW_STATE_DIR || path2.join(os.homedir(), ".openclaw");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/filesystem.ts
|
|
365
374
|
var HOME_DIR = process.env.HOME ?? "/root";
|
|
366
|
-
var OPENCLAW_DIR =
|
|
375
|
+
var OPENCLAW_DIR = getOpenclawStateDir();
|
|
367
376
|
var SENSITIVE_BLOCKED_DIRS = [
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
377
|
+
path3.join(OPENCLAW_DIR, "credentials"),
|
|
378
|
+
path3.join(OPENCLAW_DIR, "devices"),
|
|
379
|
+
path3.join(OPENCLAW_DIR, "identity")
|
|
371
380
|
];
|
|
372
381
|
var SENSITIVE_BLOCKED_FILES = [
|
|
373
|
-
|
|
382
|
+
path3.join(OPENCLAW_DIR, "squad-ceo-data", "relay", "squad-relay.json")
|
|
374
383
|
];
|
|
375
384
|
function isSensitivePath(resolvedPath) {
|
|
376
385
|
for (const blocked of SENSITIVE_BLOCKED_DIRS) {
|
|
377
|
-
if (resolvedPath === blocked || resolvedPath.startsWith(blocked +
|
|
386
|
+
if (resolvedPath === blocked || resolvedPath.startsWith(blocked + path3.sep)) {
|
|
378
387
|
return true;
|
|
379
388
|
}
|
|
380
389
|
}
|
|
@@ -383,7 +392,7 @@ function isSensitivePath(resolvedPath) {
|
|
|
383
392
|
return true;
|
|
384
393
|
}
|
|
385
394
|
}
|
|
386
|
-
if (
|
|
395
|
+
if (path3.dirname(resolvedPath) === OPENCLAW_DIR && resolvedPath.endsWith(".bak")) {
|
|
387
396
|
return true;
|
|
388
397
|
}
|
|
389
398
|
return false;
|
|
@@ -432,20 +441,20 @@ function redactOpenclawJson(rawContent) {
|
|
|
432
441
|
return JSON.stringify(config, null, 2);
|
|
433
442
|
}
|
|
434
443
|
function isOpenclawJson(resolvedPath) {
|
|
435
|
-
return
|
|
444
|
+
return path3.basename(resolvedPath) === OPENCLAW_JSON_FILENAME && resolvedPath.startsWith(OPENCLAW_DIR);
|
|
436
445
|
}
|
|
437
446
|
function expandHome(p) {
|
|
438
447
|
if (p.startsWith("~/") || p === "~") {
|
|
439
|
-
return
|
|
448
|
+
return path3.join(HOME_DIR, p.slice(1));
|
|
440
449
|
}
|
|
441
450
|
return p;
|
|
442
451
|
}
|
|
443
452
|
function validatePath(p, allowedRoots) {
|
|
444
|
-
const resolved =
|
|
453
|
+
const resolved = path3.resolve(expandHome(p));
|
|
445
454
|
if (!allowedRoots || allowedRoots.length === 0) return resolved;
|
|
446
455
|
const allowed = allowedRoots.some((root) => {
|
|
447
|
-
const resolvedRoot =
|
|
448
|
-
return resolved === resolvedRoot || resolved.startsWith(resolvedRoot +
|
|
456
|
+
const resolvedRoot = path3.resolve(expandHome(root));
|
|
457
|
+
return resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path3.sep);
|
|
449
458
|
});
|
|
450
459
|
if (!allowed) {
|
|
451
460
|
throw new Error(`Path "${p}" is outside allowed roots`);
|
|
@@ -486,7 +495,7 @@ function listDir(dirPath, opts) {
|
|
|
486
495
|
const results = [];
|
|
487
496
|
for (const dirent of dirents) {
|
|
488
497
|
if (!opts.includeHidden && dirent.name.startsWith(".")) continue;
|
|
489
|
-
const entryPath =
|
|
498
|
+
const entryPath = path3.join(dirPath, dirent.name);
|
|
490
499
|
let type = "other";
|
|
491
500
|
if (dirent.isFile()) type = "file";
|
|
492
501
|
else if (dirent.isDirectory()) type = "directory";
|
|
@@ -593,7 +602,7 @@ function registerFilesystemTools(api) {
|
|
|
593
602
|
const encoding = params.encoding ?? "utf-8";
|
|
594
603
|
const mkdir = params.mkdir !== false;
|
|
595
604
|
if (mkdir) {
|
|
596
|
-
fs2.mkdirSync(
|
|
605
|
+
fs2.mkdirSync(path3.dirname(filePath), { recursive: true });
|
|
597
606
|
}
|
|
598
607
|
fs2.writeFileSync(filePath, content, encoding);
|
|
599
608
|
const stat = fs2.statSync(filePath);
|
|
@@ -793,10 +802,10 @@ function scanAgents(configDir) {
|
|
|
793
802
|
);
|
|
794
803
|
for (const dir of workspaceDirs) {
|
|
795
804
|
const agentId = dir.name === "workspace" ? "main" : dir.name.replace("workspace-", "");
|
|
796
|
-
const workspacePath =
|
|
805
|
+
const workspacePath = path4.join(configDir, dir.name);
|
|
797
806
|
let name = agentId;
|
|
798
807
|
const metadata = { workspacePath };
|
|
799
|
-
const identityPath =
|
|
808
|
+
const identityPath = path4.join(workspacePath, "IDENTITY.md");
|
|
800
809
|
try {
|
|
801
810
|
const content = fs3.readFileSync(identityPath, "utf-8");
|
|
802
811
|
const parsed = parseIdentityName(content);
|
|
@@ -804,7 +813,7 @@ function scanAgents(configDir) {
|
|
|
804
813
|
} catch {
|
|
805
814
|
}
|
|
806
815
|
if (name === agentId) {
|
|
807
|
-
const agentJsonPath =
|
|
816
|
+
const agentJsonPath = path4.join(workspacePath, "agent.json");
|
|
808
817
|
try {
|
|
809
818
|
const raw = fs3.readFileSync(agentJsonPath, "utf-8");
|
|
810
819
|
const config = JSON.parse(raw);
|
|
@@ -831,7 +840,7 @@ function scanAgents(configDir) {
|
|
|
831
840
|
}
|
|
832
841
|
function scanSkills(configDir) {
|
|
833
842
|
const now = Date.now();
|
|
834
|
-
const globalSkillsDir =
|
|
843
|
+
const globalSkillsDir = path4.join(configDir, "skills");
|
|
835
844
|
scanSkillsDir(globalSkillsDir, "global", now);
|
|
836
845
|
let entries;
|
|
837
846
|
try {
|
|
@@ -844,7 +853,7 @@ function scanSkills(configDir) {
|
|
|
844
853
|
continue;
|
|
845
854
|
}
|
|
846
855
|
const agentId = dir.name === "workspace" ? "main" : dir.name.replace("workspace-", "");
|
|
847
|
-
const agentSkillsDir =
|
|
856
|
+
const agentSkillsDir = path4.join(configDir, dir.name, "skills");
|
|
848
857
|
scanSkillsDir(agentSkillsDir, agentId, now);
|
|
849
858
|
}
|
|
850
859
|
}
|
|
@@ -858,12 +867,12 @@ function scanSkillsDir(skillsDir, scope, now) {
|
|
|
858
867
|
for (const entry of entries) {
|
|
859
868
|
if (!entry.isDirectory()) continue;
|
|
860
869
|
const skillKey = entry.name;
|
|
861
|
-
const skillPath =
|
|
870
|
+
const skillPath = path4.join(skillsDir, skillKey);
|
|
862
871
|
let name = skillKey;
|
|
863
872
|
for (const manifestName of ["manifest.json", "package.json"]) {
|
|
864
873
|
try {
|
|
865
874
|
const raw = fs3.readFileSync(
|
|
866
|
-
|
|
875
|
+
path4.join(skillPath, manifestName),
|
|
867
876
|
"utf-8"
|
|
868
877
|
);
|
|
869
878
|
const manifest = JSON.parse(raw);
|
|
@@ -890,7 +899,7 @@ function scanSkillsDir(skillsDir, scope, now) {
|
|
|
890
899
|
}
|
|
891
900
|
function scanPlugins2(configDir) {
|
|
892
901
|
const now = Date.now();
|
|
893
|
-
const extensionsDir =
|
|
902
|
+
const extensionsDir = path4.join(configDir, "extensions");
|
|
894
903
|
let entries;
|
|
895
904
|
try {
|
|
896
905
|
entries = fs3.readdirSync(extensionsDir, { withFileTypes: true });
|
|
@@ -899,8 +908,8 @@ function scanPlugins2(configDir) {
|
|
|
899
908
|
}
|
|
900
909
|
for (const dir of entries) {
|
|
901
910
|
if (!dir.isDirectory()) continue;
|
|
902
|
-
const pluginDir =
|
|
903
|
-
const manifestPath =
|
|
911
|
+
const pluginDir = path4.join(extensionsDir, dir.name);
|
|
912
|
+
const manifestPath = path4.join(pluginDir, "openclaw.plugin.json");
|
|
904
913
|
try {
|
|
905
914
|
const raw = fs3.readFileSync(manifestPath, "utf-8");
|
|
906
915
|
const manifest = JSON.parse(raw);
|
|
@@ -926,7 +935,7 @@ function scanTools(configDir) {
|
|
|
926
935
|
const now = Date.now();
|
|
927
936
|
try {
|
|
928
937
|
const raw = fs3.readFileSync(
|
|
929
|
-
|
|
938
|
+
path4.join(configDir, "openclaw.json"),
|
|
930
939
|
"utf-8"
|
|
931
940
|
);
|
|
932
941
|
const config = JSON.parse(raw);
|
|
@@ -977,12 +986,12 @@ var MIME_MAP = {
|
|
|
977
986
|
".gz": "application/gzip"
|
|
978
987
|
};
|
|
979
988
|
function getMimeType(filename) {
|
|
980
|
-
const ext =
|
|
989
|
+
const ext = path4.extname(filename).toLowerCase();
|
|
981
990
|
return MIME_MAP[ext] ?? "application/octet-stream";
|
|
982
991
|
}
|
|
983
992
|
function scanMedia(configDir) {
|
|
984
993
|
const now = Date.now();
|
|
985
|
-
const mediaDir =
|
|
994
|
+
const mediaDir = path4.join(configDir, "media");
|
|
986
995
|
scanMediaDir(mediaDir, now);
|
|
987
996
|
}
|
|
988
997
|
function scanMediaDir(dirPath, now) {
|
|
@@ -994,7 +1003,7 @@ function scanMediaDir(dirPath, now) {
|
|
|
994
1003
|
}
|
|
995
1004
|
for (const entry of entries) {
|
|
996
1005
|
if (entry.name.startsWith(".")) continue;
|
|
997
|
-
const entryPath =
|
|
1006
|
+
const entryPath = path4.join(dirPath, entry.name);
|
|
998
1007
|
if (isSensitivePath(entryPath)) continue;
|
|
999
1008
|
if (entry.isDirectory()) {
|
|
1000
1009
|
registrySet({
|
|
@@ -1044,7 +1053,7 @@ function fullScan(configDir) {
|
|
|
1044
1053
|
scanMedia(configDir);
|
|
1045
1054
|
}
|
|
1046
1055
|
function registerEntityTools(api, onFsChange) {
|
|
1047
|
-
const configDir =
|
|
1056
|
+
const configDir = getOpenclawStateDir();
|
|
1048
1057
|
api.registerTool({
|
|
1049
1058
|
name: "entity_list",
|
|
1050
1059
|
description: "List all entities in the registry, optionally filtered by type. Returns lightweight entity data for @mention autocomplete.",
|
|
@@ -1132,18 +1141,18 @@ function registerEntityTools(api, onFsChange) {
|
|
|
1132
1141
|
|
|
1133
1142
|
// src/sql.ts
|
|
1134
1143
|
import { execFile } from "child_process";
|
|
1135
|
-
import
|
|
1144
|
+
import path5 from "path";
|
|
1136
1145
|
import fs4 from "fs";
|
|
1137
1146
|
import { Type as T2 } from "@sinclair/typebox";
|
|
1138
1147
|
var HOME_DIR2 = process.env.HOME ?? "/root";
|
|
1139
|
-
var ALLOWED_DATA_DIR =
|
|
1148
|
+
var ALLOWED_DATA_DIR = path5.join(getOpenclawStateDir(), "squad-ceo-data");
|
|
1140
1149
|
function validateDbPath(dbPath) {
|
|
1141
1150
|
let expanded = dbPath;
|
|
1142
1151
|
if (expanded.startsWith("~/") || expanded === "~") {
|
|
1143
|
-
expanded =
|
|
1152
|
+
expanded = path5.join(HOME_DIR2, expanded.slice(1));
|
|
1144
1153
|
}
|
|
1145
|
-
const resolved =
|
|
1146
|
-
if (resolved !== ALLOWED_DATA_DIR && !resolved.startsWith(ALLOWED_DATA_DIR +
|
|
1154
|
+
const resolved = path5.resolve(expanded);
|
|
1155
|
+
if (resolved !== ALLOWED_DATA_DIR && !resolved.startsWith(ALLOWED_DATA_DIR + path5.sep)) {
|
|
1147
1156
|
throw new Error(
|
|
1148
1157
|
`Access denied: database path must be within ~/.openclaw/squad-ceo-data/`
|
|
1149
1158
|
);
|
|
@@ -1217,17 +1226,13 @@ function registerSqlTools(api) {
|
|
|
1217
1226
|
// src/version.ts
|
|
1218
1227
|
import { execSync as execSync2 } from "child_process";
|
|
1219
1228
|
import fs5 from "fs";
|
|
1220
|
-
import
|
|
1229
|
+
import path6 from "path";
|
|
1221
1230
|
import { fileURLToPath } from "url";
|
|
1222
1231
|
var PACKAGE_NAME = "squad-openclaw";
|
|
1223
|
-
var CONFIG_PATH =
|
|
1224
|
-
process.env.HOME ?? "/root",
|
|
1225
|
-
".openclaw",
|
|
1226
|
-
"openclaw.json"
|
|
1227
|
-
);
|
|
1232
|
+
var CONFIG_PATH = path6.join(getOpenclawStateDir(), "openclaw.json");
|
|
1228
1233
|
function getCurrentVersion() {
|
|
1229
1234
|
const thisFile = fileURLToPath(import.meta.url);
|
|
1230
|
-
const pkgPath =
|
|
1235
|
+
const pkgPath = path6.resolve(path6.dirname(thisFile), "..", "package.json");
|
|
1231
1236
|
try {
|
|
1232
1237
|
const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
1233
1238
|
return pkg.version ?? "0.0.0";
|
|
@@ -1354,8 +1359,7 @@ function registerVersionMethods(api) {
|
|
|
1354
1359
|
import { WebSocket as NodeWebSocket } from "ws";
|
|
1355
1360
|
import crypto3 from "crypto";
|
|
1356
1361
|
import fs7 from "fs";
|
|
1357
|
-
import
|
|
1358
|
-
import os2 from "os";
|
|
1362
|
+
import path8 from "path";
|
|
1359
1363
|
|
|
1360
1364
|
// src/e2e-crypto.ts
|
|
1361
1365
|
import crypto from "crypto";
|
|
@@ -1437,11 +1441,10 @@ var E2ECrypto = class {
|
|
|
1437
1441
|
// src/device-keys.ts
|
|
1438
1442
|
import crypto2 from "crypto";
|
|
1439
1443
|
import fs6 from "fs";
|
|
1440
|
-
import
|
|
1441
|
-
|
|
1442
|
-
var
|
|
1443
|
-
var
|
|
1444
|
-
var PENDING_APPROVAL_PATH = path6.join(RELAY_DATA_DIR, "pending-approval.json");
|
|
1444
|
+
import path7 from "path";
|
|
1445
|
+
var RELAY_DATA_DIR = path7.join(getOpenclawStateDir(), "squad-ceo-data", "relay");
|
|
1446
|
+
var RELAY_STATE_PATH = path7.join(RELAY_DATA_DIR, "squad-relay.json");
|
|
1447
|
+
var PENDING_APPROVAL_PATH = path7.join(RELAY_DATA_DIR, "pending-approval.json");
|
|
1445
1448
|
function readRelayState() {
|
|
1446
1449
|
try {
|
|
1447
1450
|
const raw = fs6.readFileSync(RELAY_STATE_PATH, "utf-8");
|
|
@@ -1476,8 +1479,8 @@ function loadOrCreateRelayDeviceKeys() {
|
|
|
1476
1479
|
return keys;
|
|
1477
1480
|
}
|
|
1478
1481
|
function writeDeviceInfoFile(keys) {
|
|
1479
|
-
const stateDir =
|
|
1480
|
-
const infoPath =
|
|
1482
|
+
const stateDir = getOpenclawStateDir();
|
|
1483
|
+
const infoPath = path7.join(stateDir, "squad-ceo-data", "relay", "relay-device-info.json");
|
|
1481
1484
|
const info = {
|
|
1482
1485
|
deviceId: keys.deviceId,
|
|
1483
1486
|
publicKey: keys.publicKey,
|
|
@@ -1494,8 +1497,8 @@ function writeDeviceInfoFile(keys) {
|
|
|
1494
1497
|
|
|
1495
1498
|
// src/relay-client.ts
|
|
1496
1499
|
function readOperatorToken() {
|
|
1497
|
-
const stateDir =
|
|
1498
|
-
const configPath =
|
|
1500
|
+
const stateDir = getOpenclawStateDir();
|
|
1501
|
+
const configPath = path8.join(stateDir, "openclaw.json");
|
|
1499
1502
|
try {
|
|
1500
1503
|
const raw = fs7.readFileSync(configPath, "utf-8");
|
|
1501
1504
|
const config = JSON.parse(raw);
|
|
@@ -1847,9 +1850,8 @@ var RelayClient = class {
|
|
|
1847
1850
|
console.log(`[relay-client] Local WS for user ${userId} closed: ${code} ${reasonStr}`);
|
|
1848
1851
|
if (code === 1008) {
|
|
1849
1852
|
console.error(
|
|
1850
|
-
`[relay-client]
|
|
1851
|
-
|
|
1852
|
-
Or follow the onboarding instructions in the Squad web app.
|
|
1853
|
+
`[relay-client] Gateway rejected device identity (code 1008). The gateway auto-pairs devices with a valid operator token, so this usually means the operator token is missing, expired, or incorrect.
|
|
1854
|
+
Check: ~/.openclaw/openclaw.json \u2192 gateway.auth.token
|
|
1853
1855
|
Device ID: ${this.deviceKeys.deviceId}`
|
|
1854
1856
|
);
|
|
1855
1857
|
}
|
|
@@ -1927,7 +1929,7 @@ Device ID: ${this.deviceKeys.deviceId}`
|
|
|
1927
1929
|
status: "pending"
|
|
1928
1930
|
});
|
|
1929
1931
|
console.log(
|
|
1930
|
-
`[relay-client] Pairing request pending for ${email}.
|
|
1932
|
+
`[relay-client] Pairing request pending for ${email}. The gateway will auto-pair this device if the operator token is valid.`
|
|
1931
1933
|
);
|
|
1932
1934
|
}
|
|
1933
1935
|
// ── E2E Key Exchange ──
|
package/openclaw.plugin.json
CHANGED
|
@@ -11,16 +11,6 @@
|
|
|
11
11
|
"items": { "type": "string" },
|
|
12
12
|
"default": ["~/.openclaw"],
|
|
13
13
|
"description": "Restrict filesystem operations to these directories. Defaults to [\"~/.openclaw\"]. Hardcoded blocks on credentials/, devices/, identity/, relay/squad-relay.json, and .bak files always apply."
|
|
14
|
-
},
|
|
15
|
-
"relay.enabled": {
|
|
16
|
-
"type": "boolean",
|
|
17
|
-
"default": false,
|
|
18
|
-
"description": "Enable cloud relay for remote browser access. Disabled by default — opt-in required."
|
|
19
|
-
},
|
|
20
|
-
"relay.url": {
|
|
21
|
-
"type": "string",
|
|
22
|
-
"default": "wss://relay.squad.ceo",
|
|
23
|
-
"description": "Cloud relay WebSocket URL. Defaults to wss://relay.squad.ceo."
|
|
24
14
|
}
|
|
25
15
|
}
|
|
26
16
|
}
|
package/package.json
CHANGED