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 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 | **Disabled by default (opt-in).** Connects outbound to `relay.squad.ceo` for remote browser access |
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 SPA needs:
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
- > **The cloud relay is DISABLED by default. No outbound connections are made unless the operator explicitly sets `relay.enabled: true`.** Installing this plugin alone does NOT create any network surface — the relay code is never executed until opted in.
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 cloud relay enables remote browser access to the gateway through `relay.squad.ceo`.
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
- ### Opt-in Only
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
- The relay is **disabled by default** (`relay.enabled` defaults to `false`). The plugin entry point checks this flag **before** calling `startRelayClient()` — if the flag is not set or is `false`, no relay code runs, no WebSocket is opened, and no connection metadata is sent anywhere. The operator must explicitly enable it by setting `relay.enabled: true` in the plugin configuration.
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
- ### Authentication
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
- - **Browser to Relay:** JWT authentication (email/password or Google OAuth)
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
- ### Device Pairing
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
- 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.
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
- ### E2E Encryption
122
+ ### What Data Crosses the Network (relay.squad.ceo)
88
123
 
89
- - **Protocol:** ECDH (P-256) key exchange + AES-256-GCM message encryption
90
- - **No plaintext fallback:** If encryption fails after E2E is established, messages are dropped — never sent as plaintext
91
- - **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.
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
- ### Operator Token
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 `gateway.auth.token` from `~/.openclaw/openclaw.json` via direct `fs.readFileSync`. This is intentional and safe:
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
- - The relay-client runs **server-side, in the gateway's own process** equivalent to the gateway reading its own config
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
- **Token flow (important for security auditing):**
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]──> relay.squad.ceo ──[relay.forward]──> relay-client
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]──> localhost:18789 (gateway)
155
+ relay-client ──[modified request]──► localhost:18789 (gateway)
111
156
  ```
112
157
 
113
- The relay server only sees the outer `relay.forward` envelope. It **never** receives the modified request containing the token. The token injection happens entirely within the relay-client process, and the modified message is sent over a **local loopback** connection to the gateway. A compromised relay server cannot intercept the operator token because it never traverses the relay — it only exists on the `localhost:18789` path.
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
- | `relay.enabled` | `boolean` | `false` | Enable cloud relay. Opt-in required. |
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 path3 from "path";
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 = path2.join(HOME_DIR, ".openclaw");
375
+ var OPENCLAW_DIR = getOpenclawStateDir();
367
376
  var SENSITIVE_BLOCKED_DIRS = [
368
- path2.join(OPENCLAW_DIR, "credentials"),
369
- path2.join(OPENCLAW_DIR, "devices"),
370
- path2.join(OPENCLAW_DIR, "identity")
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
- path2.join(OPENCLAW_DIR, "squad-ceo-data", "relay", "squad-relay.json")
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 + path2.sep)) {
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 (path2.dirname(resolvedPath) === OPENCLAW_DIR && resolvedPath.endsWith(".bak")) {
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 path2.basename(resolvedPath) === OPENCLAW_JSON_FILENAME && resolvedPath.startsWith(OPENCLAW_DIR);
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 path2.join(HOME_DIR, p.slice(1));
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 = path2.resolve(expandHome(p));
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 = path2.resolve(expandHome(root));
448
- return resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path2.sep);
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 = path2.join(dirPath, dirent.name);
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(path2.dirname(filePath), { recursive: true });
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 = path3.join(configDir, dir.name);
805
+ const workspacePath = path4.join(configDir, dir.name);
797
806
  let name = agentId;
798
807
  const metadata = { workspacePath };
799
- const identityPath = path3.join(workspacePath, "IDENTITY.md");
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 = path3.join(workspacePath, "agent.json");
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 = path3.join(configDir, "skills");
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 = path3.join(configDir, dir.name, "skills");
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 = path3.join(skillsDir, skillKey);
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
- path3.join(skillPath, manifestName),
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 = path3.join(configDir, "extensions");
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 = path3.join(extensionsDir, dir.name);
903
- const manifestPath = path3.join(pluginDir, "openclaw.plugin.json");
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
- path3.join(configDir, "openclaw.json"),
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 = path3.extname(filename).toLowerCase();
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 = path3.join(configDir, "media");
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 = path3.join(dirPath, entry.name);
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 = process.env.HOME + "/.openclaw";
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 path4 from "path";
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 = path4.join(HOME_DIR2, ".openclaw", "squad-ceo-data");
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 = path4.join(HOME_DIR2, expanded.slice(1));
1152
+ expanded = path5.join(HOME_DIR2, expanded.slice(1));
1144
1153
  }
1145
- const resolved = path4.resolve(expanded);
1146
- if (resolved !== ALLOWED_DATA_DIR && !resolved.startsWith(ALLOWED_DATA_DIR + path4.sep)) {
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 path5 from "path";
1229
+ import path6 from "path";
1221
1230
  import { fileURLToPath } from "url";
1222
1231
  var PACKAGE_NAME = "squad-openclaw";
1223
- var CONFIG_PATH = path5.join(
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 = path5.resolve(path5.dirname(thisFile), "..", "package.json");
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 path7 from "path";
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 path6 from "path";
1441
- import os from "os";
1442
- var RELAY_DATA_DIR = path6.join(os.homedir(), ".openclaw", "squad-ceo-data", "relay");
1443
- var RELAY_STATE_PATH = path6.join(RELAY_DATA_DIR, "squad-relay.json");
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 = process.env.OPENCLAW_STATE_DIR || path6.join(os.homedir(), ".openclaw");
1480
- const infoPath = path6.join(stateDir, "squad-ceo-data", "relay", "relay-device-info.json");
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 = process.env.OPENCLAW_STATE_DIR || path7.join(os2.homedir(), ".openclaw");
1498
- const configPath = path7.join(stateDir, "openclaw.json");
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] Device not paired with gateway. To approve, run on the gateway machine:
1851
- openclaw devices approve
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}. Approve with: openclaw nodes approve (or configure relay.autoApprove)`
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 ──
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squad-openclaw",
3
- "version": "2026.2.2007",
3
+ "version": "2026.2.2008",
4
4
  "description": "Entity registry, filesystem tools, and version management plugin for OpenClaw gateway",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",