squad-openclaw 2026.2.2209 → 2026.2.2702

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1283 -932
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,86 +1,7 @@
1
- // src/relay-client.ts
2
- import { WebSocket as NodeWebSocket } from "ws";
3
- import crypto3 from "crypto";
4
- import fs3 from "fs";
1
+ // src/agents.ts
2
+ import { execSync } from "child_process";
5
3
  import path3 from "path";
6
4
 
7
- // src/e2e-crypto.ts
8
- import crypto from "crypto";
9
- var CURVE = "prime256v1";
10
- var HKDF_SALT = "squad-e2e-v1";
11
- var HKDF_INFO = "aes-gcm-key";
12
- var AES_KEY_LENGTH = 32;
13
- var IV_LENGTH = 12;
14
- var E2ECrypto = class {
15
- ecdh = null;
16
- aesKey = null;
17
- publicKeyB64 = null;
18
- /** Generate an ephemeral ECDH keypair. Returns the public key as base64. */
19
- async generateKeyPair() {
20
- this.ecdh = crypto.createECDH(CURVE);
21
- const publicKey = this.ecdh.generateKeys();
22
- this.publicKeyB64 = publicKey.toString("base64");
23
- return this.publicKeyB64;
24
- }
25
- /** Derive the shared secret from the peer's public key. */
26
- async deriveSharedSecret(peerPublicKeyB64) {
27
- if (!this.ecdh) {
28
- throw new Error("Must call generateKeyPair() first");
29
- }
30
- const peerPublicKey = Buffer.from(peerPublicKeyB64, "base64");
31
- const sharedSecret = this.ecdh.computeSecret(peerPublicKey);
32
- this.aesKey = crypto.hkdfSync(
33
- "sha256",
34
- sharedSecret,
35
- Buffer.from(HKDF_SALT),
36
- Buffer.from(HKDF_INFO),
37
- AES_KEY_LENGTH
38
- );
39
- }
40
- /** Encrypt a plaintext string. Returns base64-encoded ciphertext + iv + tag. */
41
- encrypt(plaintext) {
42
- if (!this.aesKey) {
43
- throw new Error("E2E not established \u2014 call deriveSharedSecret() first");
44
- }
45
- const iv = crypto.randomBytes(IV_LENGTH);
46
- const cipher = crypto.createCipheriv("aes-256-gcm", this.aesKey, iv);
47
- const encrypted = Buffer.concat([
48
- cipher.update(plaintext, "utf-8"),
49
- cipher.final()
50
- ]);
51
- const tag = cipher.getAuthTag();
52
- return {
53
- ciphertext: encrypted.toString("base64"),
54
- iv: iv.toString("base64"),
55
- tag: tag.toString("base64")
56
- };
57
- }
58
- /** Decrypt a payload. Returns the plaintext string. */
59
- decrypt(payload) {
60
- if (!this.aesKey) {
61
- throw new Error("E2E not established \u2014 call deriveSharedSecret() first");
62
- }
63
- const ciphertext = Buffer.from(payload.ciphertext, "base64");
64
- const iv = Buffer.from(payload.iv, "base64");
65
- const tag = Buffer.from(payload.tag, "base64");
66
- const decipher = crypto.createDecipheriv("aes-256-gcm", this.aesKey, iv);
67
- decipher.setAuthTag(tag);
68
- const decrypted = Buffer.concat([
69
- decipher.update(ciphertext),
70
- decipher.final()
71
- ]);
72
- return decrypted.toString("utf-8");
73
- }
74
- /** Whether E2E encryption has been established */
75
- get isEstablished() {
76
- return this.aesKey !== null;
77
- }
78
- /** Get the local public key (base64) */
79
- get publicKey() {
80
- return this.publicKeyB64;
81
- }
82
- };
83
-
84
5
  // src/paths.ts
85
6
  import path from "path";
86
7
  import os from "os";
@@ -105,690 +26,42 @@ function getOpenclawStateDir() {
105
26
  return path.join(os.homedir(), ".openclaw");
106
27
  }
107
28
 
108
- // src/device-keys.ts
109
- import crypto2 from "crypto";
29
+ // src/auth-profiles.ts
110
30
  import fs2 from "fs";
111
31
  import path2 from "path";
112
- var RELAY_DATA_DIR = path2.join(getOpenclawStateDir(), "squad-ceo-data", "relay");
113
- var RELAY_STATE_PATH = path2.join(RELAY_DATA_DIR, "squad-relay.json");
114
- var PENDING_APPROVAL_PATH = path2.join(RELAY_DATA_DIR, "pending-approval.json");
115
- function readRelayState() {
116
- try {
117
- const raw = fs2.readFileSync(RELAY_STATE_PATH, "utf-8");
118
- return JSON.parse(raw);
119
- } catch {
120
- return {};
121
- }
122
- }
123
- function writeRelayState(state) {
124
- if (!fs2.existsSync(RELAY_DATA_DIR)) {
125
- fs2.mkdirSync(RELAY_DATA_DIR, { recursive: true });
126
- }
127
- fs2.writeFileSync(RELAY_STATE_PATH, JSON.stringify(state, null, 2), { mode: 384 });
128
- }
129
- function toBase64Url(buf) {
130
- return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
32
+ function getMainAuthProfilesPath(stateDir) {
33
+ return path2.join(stateDir, "agents", "main", "agent", "auth-profiles.json");
131
34
  }
132
- function loadOrCreateRelayDeviceKeys() {
133
- const state = readRelayState();
134
- if (state.deviceKeys) {
135
- return state.deviceKeys;
136
- }
137
- const { publicKey, privateKey } = crypto2.generateKeyPairSync("ed25519");
138
- const pubDer = publicKey.export({ type: "spki", format: "der" });
139
- const rawPub = pubDer.subarray(pubDer.length - 32);
140
- const deviceId = crypto2.createHash("sha256").update(rawPub).digest("hex");
141
- const publicKeyB64 = toBase64Url(rawPub);
142
- const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
143
- const keys = { deviceId, publicKey: publicKeyB64, privateKeyPem };
144
- writeRelayState({ ...state, deviceKeys: keys });
145
- console.log(`[device-keys] Generated relay device identity: ${deviceId.substring(0, 12)}...`);
146
- return keys;
147
- }
148
- function writeDeviceInfoFile(keys) {
149
- const stateDir = getOpenclawStateDir();
150
- const infoPath = path2.join(stateDir, "squad-ceo-data", "relay", "relay-device-info.json");
151
- const info = {
152
- deviceId: keys.deviceId,
153
- publicKey: keys.publicKey,
154
- displayName: "squad-relay",
155
- platform: process.platform,
156
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
157
- };
158
- try {
159
- fs2.writeFileSync(infoPath, JSON.stringify(info, null, 2));
160
- } catch (err2) {
161
- console.error("[device-keys] Failed to write relay-device-info.json:", err2);
162
- }
35
+ function getAgentAuthProfilesPath(stateDir, agentId) {
36
+ return path2.join(stateDir, "agents", agentId, "agent", "auth-profiles.json");
163
37
  }
164
-
165
- // src/relay-client.ts
166
- function readOperatorToken() {
38
+ function ensureAgentAuthProfiles(agentId) {
39
+ const normalizedAgentId = agentId.trim();
40
+ if (!normalizedAgentId || normalizedAgentId === "main") return false;
167
41
  const stateDir = getOpenclawStateDir();
168
- const configPath = path3.join(stateDir, "openclaw.json");
169
- try {
170
- const raw = fs3.readFileSync(configPath, "utf-8");
171
- const config = JSON.parse(raw);
172
- return config?.gateway?.auth?.token ?? config?.gateway?.remote?.token ?? config?.gateway?.token ?? null;
173
- } catch {
174
- return null;
175
- }
176
- }
177
- function readGatewayLocalWsConfig() {
178
- const defaults = {
179
- port: 18789,
180
- // Try IPv4, hostname, then IPv6 loopback.
181
- hosts: ["127.0.0.1", "localhost", "[::1]"]
182
- };
42
+ const sourcePath = getMainAuthProfilesPath(stateDir);
43
+ const targetPath = getAgentAuthProfilesPath(stateDir, normalizedAgentId);
44
+ if (!fs2.existsSync(sourcePath) || fs2.existsSync(targetPath)) return false;
45
+ fs2.mkdirSync(path2.dirname(targetPath), { recursive: true });
46
+ fs2.copyFileSync(sourcePath, targetPath);
47
+ return true;
48
+ }
49
+ function backfillAgentAuthProfiles() {
183
50
  const stateDir = getOpenclawStateDir();
184
- const configPath = path3.join(stateDir, "openclaw.json");
185
- try {
186
- const raw = fs3.readFileSync(configPath, "utf-8");
187
- const config = JSON.parse(raw);
188
- const parsedPort = Number(config?.gateway?.port);
189
- if (Number.isFinite(parsedPort) && parsedPort > 0) {
190
- defaults.port = parsedPort;
191
- }
192
- } catch {
193
- }
194
- return defaults;
195
- }
196
- function signDeviceIdentity(keys, clientId, clientMode, role, scopes, token, challengeNonce) {
197
- const signedAtMs = Date.now();
198
- const nonce = challengeNonce || crypto3.randomBytes(16).toString("hex");
199
- const scopeStr = scopes.join(",");
200
- const payload = `v2|${keys.deviceId}|${clientId}|${clientMode}|${role}|${scopeStr}|${signedAtMs}|${token ?? ""}|${nonce}`;
201
- const privateKey = crypto3.createPrivateKey(keys.privateKeyPem);
202
- const signature = crypto3.sign(null, Buffer.from(payload), privateKey);
203
- return {
204
- id: keys.deviceId,
205
- publicKey: keys.publicKey,
206
- signature: toBase64Url(signature),
207
- signedAt: signedAtMs,
208
- nonce
209
- };
210
- }
211
- var RelayClient = class {
212
- config;
213
- relayWs = null;
214
- userConnections = /* @__PURE__ */ new Map();
215
- localConnectAttempts = /* @__PURE__ */ new Map();
216
- reconnectAttempts = 0;
217
- maxReconnectAttempts = 100;
218
- autoConnectCounter = 0;
219
- reconnectTimer = null;
220
- shouldReconnect = true;
221
- destroyed = false;
222
- /** Pending claim token — sent on first successful connect, then cleared */
223
- pendingClaimToken = null;
224
- /** Device keys for authenticating local WS connections to the gateway */
225
- deviceKeys;
226
- constructor(config) {
227
- const state = readRelayState();
228
- const localWs = readGatewayLocalWsConfig();
229
- this.config = {
230
- relayUrl: config.relayUrl,
231
- localGatewayPort: config.localGatewayPort ?? localWs.port,
232
- localGatewayHosts: config.localGatewayHosts ?? localWs.hosts,
233
- operatorToken: config.operatorToken ?? readOperatorToken(),
234
- claimToken: config.claimToken ?? state.claimToken ?? null,
235
- roomId: config.roomId ?? state.roomId ?? null
236
- };
237
- this.pendingClaimToken = this.config.roomId ? null : this.config.claimToken;
238
- this.deviceKeys = loadOrCreateRelayDeviceKeys();
239
- writeDeviceInfoFile(this.deviceKeys);
240
- console.log(`[relay-client] Device ID: ${this.deviceKeys.deviceId}`);
241
- }
242
- /** Start connecting to the relay */
243
- start() {
244
- if (!this.config.roomId && !this.pendingClaimToken) {
245
- console.log("[relay-client] No room ID or claim token found.");
246
- console.log("[relay-client] Complete the setup from the Squad web app to generate a claim token.");
247
- return;
248
- }
249
- console.log(`[relay-client] Starting relay connection to ${this.config.relayUrl}`);
250
- if (this.config.roomId) {
251
- console.log(`[relay-client] Room ID: ${this.config.roomId.substring(0, 8)}...`);
252
- } else {
253
- console.log(`[relay-client] Using claim token for first connect`);
254
- }
255
- this.connectToRelay();
256
- }
257
- /** Stop the relay client and close all connections */
258
- destroy() {
259
- this.destroyed = true;
260
- this.shouldReconnect = false;
261
- if (this.reconnectTimer) {
262
- clearTimeout(this.reconnectTimer);
263
- this.reconnectTimer = null;
264
- }
265
- for (const [userId, conn] of this.userConnections) {
266
- try {
267
- conn.localWs.close(1e3, "Relay client shutting down");
268
- } catch {
269
- }
270
- this.userConnections.delete(userId);
271
- }
272
- if (this.relayWs) {
273
- try {
274
- this.relayWs.close(1e3, "Relay client shutting down");
275
- } catch {
276
- }
277
- this.relayWs = null;
278
- }
279
- }
280
- // ── Relay Connection ──
281
- connectToRelay() {
282
- if (this.destroyed) return;
283
- let wsUrl;
284
- if (this.pendingClaimToken) {
285
- wsUrl = `${this.config.relayUrl}/gw?claim=${encodeURIComponent(this.pendingClaimToken)}`;
286
- console.log(`[relay-client] Connecting with claim token`);
287
- } else if (this.config.roomId) {
288
- wsUrl = `${this.config.relayUrl}/gw?room=${encodeURIComponent(this.config.roomId)}`;
289
- console.log(`[relay-client] Reconnecting with room ID`);
290
- } else {
291
- console.error("[relay-client] No claim token or room ID \u2014 cannot connect");
292
- return;
293
- }
294
- try {
295
- this.relayWs = new NodeWebSocket(wsUrl);
296
- } catch (err2) {
297
- console.error("[relay-client] Failed to create WebSocket:", err2);
298
- this.scheduleReconnect();
299
- return;
300
- }
301
- this.relayWs.on("open", () => {
302
- console.log("[relay-client] Connected to relay");
303
- this.reconnectAttempts = 0;
304
- this.sendToRelay({
305
- type: "relay.hello",
306
- deviceId: this.deviceKeys.deviceId,
307
- publicKey: this.deviceKeys.publicKey
308
- });
309
- });
310
- this.relayWs.on("message", (data) => {
311
- try {
312
- const msg = JSON.parse(data.toString());
313
- this.handleRelayMessage(msg);
314
- } catch {
315
- }
316
- });
317
- this.relayWs.on("close", (code, reason) => {
318
- const reasonStr = reason.toString();
319
- console.log(`[relay-client] Relay connection closed: ${code} ${reasonStr}`);
320
- this.relayWs = null;
321
- if (code === 1e3 && reasonStr.includes("Replaced")) {
322
- console.log("[relay-client] Replaced by newer instance, stopping reconnect");
323
- this.shouldReconnect = false;
324
- this.destroyed = true;
325
- }
326
- for (const [userId, conn] of this.userConnections) {
327
- try {
328
- conn.localWs.close(1001, "Relay disconnected");
329
- } catch {
330
- }
331
- this.userConnections.delete(userId);
332
- }
333
- if (this.shouldReconnect) {
334
- this.scheduleReconnect();
335
- }
336
- });
337
- this.relayWs.on("error", (err2) => {
338
- console.error("[relay-client] Relay WebSocket error:", err2.message);
339
- });
340
- this.relayWs.on("unexpected-response", (_req, res) => {
341
- console.warn(`[relay-client] Unexpected response: ${res.statusCode}`);
342
- if (res.statusCode === 401 && this.pendingClaimToken) {
343
- console.log("[relay-client] Claim token rejected \u2014 checking for stored room ID");
344
- this.pendingClaimToken = null;
345
- const state = readRelayState();
346
- if (state.roomId) {
347
- this.config.roomId = state.roomId;
348
- console.log(`[relay-client] Found stored room ID, will use on next reconnect`);
349
- }
350
- }
351
- this.relayWs = null;
352
- this.scheduleReconnect();
353
- });
354
- }
355
- scheduleReconnect() {
356
- if (this.destroyed || !this.shouldReconnect) return;
357
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
358
- console.error("[relay-client] Max reconnect attempts reached");
359
- return;
360
- }
361
- const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 6e4);
362
- this.reconnectAttempts++;
363
- console.log(`[relay-client] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
364
- this.reconnectTimer = setTimeout(() => {
365
- this.reconnectTimer = null;
366
- this.connectToRelay();
367
- }, delay);
368
- }
369
- // ── Message Handling ──
370
- handleRelayMessage(msg) {
371
- switch (msg.type) {
372
- case "relay.welcome":
373
- this.handleWelcome(msg);
374
- break;
375
- case "relay.forward":
376
- if (msg.userId && msg.inner) {
377
- this.routeToUser(msg.userId, msg.inner);
378
- }
379
- break;
380
- case "relay.pair.request":
381
- if (msg.userId && msg.email) {
382
- this.handlePairingRequest(msg.userId, msg.email);
383
- }
384
- break;
385
- case "relay.e2e.exchange":
386
- if (msg.userId && msg.publicKey) {
387
- this.handleE2EExchange(msg.userId, msg.publicKey);
388
- }
389
- break;
390
- case "relay.ping":
391
- this.sendToRelay({ type: "relay.pong" });
392
- break;
393
- default:
394
- console.log(`[relay-client] Unknown relay message type: ${msg.type}`);
395
- }
396
- }
397
- /** Handle relay.welcome — store room ID for reconnection */
398
- handleWelcome(msg) {
399
- if (msg.roomId) {
400
- console.log(`[relay-client] Received room ID: ${msg.roomId.substring(0, 8)}...`);
401
- this.config.roomId = msg.roomId;
402
- this.pendingClaimToken = null;
403
- const state = readRelayState();
404
- state.roomId = msg.roomId;
405
- writeRelayState(state);
406
- }
407
- }
408
- /** Route a message from the relay to the appropriate user's local WS */
409
- routeToUser(userId, innerMsg) {
410
- let msg = innerMsg;
411
- if (msg.type === "event" && typeof msg.event === "string" && msg.event.startsWith("relay.")) {
412
- if (msg.event === "relay.user.connected") {
413
- console.log(`[relay-client] User ${userId} connected via relay \u2014 creating local WS`);
414
- this.createUserConnection(userId);
415
- }
416
- return;
417
- }
418
- if (typeof msg.type === "string" && msg.type.startsWith("relay.")) {
419
- if (msg.type === "relay.e2e.exchange" && msg.publicKey) {
420
- this.handleE2EExchange(userId, msg.publicKey);
421
- }
422
- return;
423
- }
424
- let conn = this.userConnections.get(userId);
425
- if (!conn || conn.localWs.readyState >= NodeWebSocket.CLOSING) {
426
- this.createUserConnection(userId);
427
- conn = this.userConnections.get(userId);
428
- if (!conn) return;
429
- }
430
- if (msg._e2e && conn.e2e) {
431
- try {
432
- const plaintext = conn.e2e.decrypt({
433
- ciphertext: msg.ciphertext,
434
- iv: msg.iv,
435
- tag: msg.tag
436
- });
437
- msg = JSON.parse(plaintext);
438
- } catch (err2) {
439
- console.error(`[relay-client] E2E decrypt error for ${userId}:`, err2);
440
- return;
441
- }
442
- }
443
- if (msg.type === "req" && msg.method === "connect") {
444
- const connectRequestId = typeof msg.id === "string" ? msg.id : null;
445
- if (conn.connectHandshakeComplete) {
446
- if (connectRequestId && conn.lastSuccessfulConnectRequestId === connectRequestId) {
447
- console.log(`[relay-client] Duplicate connect id for ${userId} (${connectRequestId}) \u2014 reusing existing session`);
448
- this.sendToRelay({
449
- type: "relay.forward",
450
- userId,
451
- inner: {
452
- type: "res",
453
- id: connectRequestId,
454
- ok: true,
455
- payload: { protocol: 3 }
456
- }
457
- });
458
- return;
459
- }
460
- console.log(`[relay-client] New connect from ${userId} \u2014 creating fresh local WS for handshake`);
461
- this.createUserConnection(userId, {
462
- pendingConnect: null,
463
- pendingMessages: conn.pendingMessages,
464
- e2e: conn.e2e
465
- });
466
- conn = this.userConnections.get(userId);
467
- if (!conn) return;
468
- }
469
- if (!conn.challengeNonce) {
470
- console.log(`[relay-client] Connect request for ${userId} deferred \u2014 waiting for challenge nonce`);
471
- conn.pendingConnect = msg;
472
- return;
473
- }
474
- this.injectDeviceIdentity(conn, msg);
475
- if (conn.localWs.readyState === NodeWebSocket.CONNECTING) {
476
- conn.localWs.once("open", () => {
477
- conn.localWs.send(JSON.stringify(msg));
478
- });
479
- } else {
480
- conn.localWs.send(JSON.stringify(msg));
481
- }
482
- return;
483
- }
484
- if (!conn.connectHandshakeComplete) {
485
- conn.pendingMessages.push(msg);
486
- if (!conn.pendingConnect) {
487
- const autoConnect = this.buildAutoConnectRequest();
488
- conn.pendingConnect = autoConnect;
489
- console.log(`[relay-client] Auto-starting local connect handshake for ${userId} (${String(autoConnect.id)})`);
490
- if (conn.challengeNonce) {
491
- const pending = conn.pendingConnect;
492
- conn.pendingConnect = null;
493
- if (pending) {
494
- this.injectDeviceIdentity(conn, pending);
495
- if (conn.localWs.readyState === NodeWebSocket.OPEN) {
496
- conn.localWs.send(JSON.stringify(pending));
497
- }
498
- }
499
- }
500
- }
501
- return;
502
- }
503
- if (conn.localWs.readyState === NodeWebSocket.CONNECTING) {
504
- conn.localWs.once("open", () => {
505
- conn.localWs.send(JSON.stringify(msg));
506
- });
507
- return;
508
- }
509
- conn.localWs.send(JSON.stringify(msg));
510
- }
511
- buildAutoConnectRequest() {
512
- return {
513
- type: "req",
514
- id: `connect-auto-${++this.autoConnectCounter}`,
515
- method: "connect",
516
- params: {
517
- minProtocol: 3,
518
- maxProtocol: 3,
519
- client: {
520
- id: "cli",
521
- version: "relay-client-auto",
522
- platform: "gateway",
523
- mode: "ui"
524
- },
525
- role: "operator",
526
- scopes: ["operator.admin", "operator.read", "operator.write"],
527
- auth: {}
528
- }
529
- };
530
- }
531
- /**
532
- * Inject auth token and device identity into a connect request.
533
- *
534
- * SECURITY: The token is added to the message IN MEMORY, then sent to the
535
- * LOCAL gateway WebSocket (localhost:18789). It NEVER traverses the relay —
536
- * the relay only sees the outer relay.forward envelope. A compromised relay
537
- * server cannot intercept this token.
538
- */
539
- injectDeviceIdentity(conn, msg) {
540
- const params = msg.params ?? {};
541
- if (this.config.operatorToken) {
542
- params.auth = { token: this.config.operatorToken };
543
- }
544
- const client = params.client ?? {};
545
- const role = params.role ?? "operator";
546
- const scopes = params.scopes ?? [];
547
- params.device = signDeviceIdentity(
548
- this.deviceKeys,
549
- client.id ?? "cli",
550
- client.mode ?? "ui",
551
- role,
552
- scopes,
553
- this.config.operatorToken,
554
- conn.challengeNonce
555
- );
556
- msg.params = params;
557
- conn.connectHandshakeComplete = false;
558
- console.log(`[relay-client] Injected device identity for ${conn.userId}: nonce=${conn.challengeNonce?.substring(0, 12)}...`);
559
- }
560
- /** Create a local WS connection to the gateway for a specific user */
561
- createUserConnection(userId, carry) {
562
- const existing = this.userConnections.get(userId);
563
- if (existing) {
564
- try {
565
- existing.localWs.close(1e3, "Replaced");
566
- } catch {
567
- }
568
- }
569
- const attempt = this.localConnectAttempts.get(userId) ?? 0;
570
- const host = this.config.localGatewayHosts[attempt % this.config.localGatewayHosts.length];
571
- const localUrl = `ws://${host}:${this.config.localGatewayPort}`;
572
- console.log(`[relay-client] Creating local WS for user ${userId} \u2192 ${localUrl}`);
573
- const localWs = new NodeWebSocket(localUrl);
574
- const conn = {
575
- localWs,
576
- userId,
577
- e2e: carry?.e2e ?? null,
578
- connectHandshakeComplete: false,
579
- challengeNonce: null,
580
- pendingConnect: carry?.pendingConnect ?? null,
581
- pendingMessages: carry?.pendingMessages ?? [],
582
- lastSuccessfulConnectRequestId: null
583
- };
584
- this.userConnections.set(userId, conn);
585
- localWs.on("open", () => {
586
- console.log(`[relay-client] Local WS for user ${userId} connected`);
587
- this.localConnectAttempts.delete(userId);
588
- });
589
- localWs.on("message", (data) => {
590
- try {
591
- const msg = JSON.parse(data.toString());
592
- this.routeFromGateway(userId, msg);
593
- } catch {
594
- }
595
- });
596
- localWs.on("close", (code, reason) => {
597
- const reasonStr = reason.toString();
598
- console.log(`[relay-client] Local WS for user ${userId} closed: ${code} ${reasonStr}`);
599
- if (code === 1008) {
600
- console.error(
601
- `[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.
602
- Check: ~/.openclaw/openclaw.json \u2192 gateway.auth.token
603
- Device ID: ${this.deviceKeys.deviceId}`
604
- );
605
- }
606
- const current = this.userConnections.get(userId);
607
- if (current && current.localWs === localWs) {
608
- this.userConnections.delete(userId);
609
- const nextAttempt = (this.localConnectAttempts.get(userId) ?? 0) + 1;
610
- const shouldRetryLocalConnect = code === 1006 && !conn.connectHandshakeComplete && nextAttempt <= 8 && this.relayWs?.readyState === NodeWebSocket.OPEN;
611
- if (shouldRetryLocalConnect) {
612
- this.localConnectAttempts.set(userId, nextAttempt);
613
- const delay = Math.min(300 * nextAttempt, 2e3);
614
- console.log(
615
- `[relay-client] Local WS unavailable for ${userId}, retrying in ${delay}ms (attempt ${nextAttempt}/8)`
616
- );
617
- const carry2 = {
618
- pendingConnect: conn.pendingConnect,
619
- pendingMessages: conn.pendingMessages,
620
- e2e: conn.e2e
621
- };
622
- setTimeout(() => {
623
- if (this.destroyed) return;
624
- if (this.relayWs?.readyState !== NodeWebSocket.OPEN) return;
625
- if (!this.userConnections.has(userId)) {
626
- this.createUserConnection(userId, carry2);
627
- }
628
- }, delay);
629
- return;
630
- }
631
- this.localConnectAttempts.delete(userId);
632
- this.sendToRelay({
633
- type: "relay.forward",
634
- userId,
635
- inner: {
636
- type: "event",
637
- event: "relay.gateway.connection.closed",
638
- payload: { code }
639
- }
640
- });
641
- }
642
- });
643
- localWs.on("error", (err2) => {
644
- console.error(`[relay-client] Local WS error for user ${userId}:`, err2.message);
645
- });
646
- }
647
- /** Route a message from the gateway back through the relay to the user */
648
- routeFromGateway(userId, msg) {
649
- const conn = this.userConnections.get(userId);
650
- if (!conn) return;
651
- const parsed = msg;
652
- if (parsed.type === "event" && parsed.event === "connect.challenge") {
653
- const payload = parsed.payload;
654
- if (payload?.nonce) {
655
- conn.challengeNonce = payload.nonce;
656
- console.log(`[relay-client] Captured challenge nonce for ${userId}: ${conn.challengeNonce.substring(0, 12)}...`);
657
- if (conn.pendingConnect) {
658
- const pending = conn.pendingConnect;
659
- conn.pendingConnect = null;
660
- console.log(`[relay-client] Flushing deferred connect for ${userId}`);
661
- this.injectDeviceIdentity(conn, pending);
662
- if (conn.localWs.readyState === NodeWebSocket.OPEN) {
663
- conn.localWs.send(JSON.stringify(pending));
664
- }
665
- }
666
- }
667
- }
668
- if (parsed.type === "res" && typeof parsed.id === "string" && parsed.id.startsWith("connect-") && parsed.ok) {
669
- conn.connectHandshakeComplete = true;
670
- conn.lastSuccessfulConnectRequestId = parsed.id;
671
- if (conn.pendingMessages.length > 0) {
672
- console.log(`[relay-client] Flushing ${conn.pendingMessages.length} buffered messages for ${userId}`);
673
- for (const queued of conn.pendingMessages) {
674
- conn.localWs.send(JSON.stringify(queued));
675
- }
676
- conn.pendingMessages = [];
677
- }
678
- }
679
- let innerMsg = msg;
680
- if (conn.e2e) {
681
- try {
682
- const encrypted = conn.e2e.encrypt(JSON.stringify(msg));
683
- innerMsg = { _e2e: true, ...encrypted };
684
- } catch (err2) {
685
- console.error(`[relay-client] E2E encrypt failed for ${userId} \u2014 dropping message:`, err2);
686
- return;
687
- }
688
- }
689
- this.sendToRelay({
690
- type: "relay.forward",
691
- userId,
692
- inner: innerMsg
693
- });
694
- }
695
- // ── Pairing ──
696
- handlePairingRequest(userId, email) {
697
- console.log(`[relay-client] Pairing request from ${email} (${userId})`);
698
- this.sendToRelay({
699
- type: "relay.pair.status",
700
- userId,
701
- status: "pending"
702
- });
703
- console.log(
704
- `[relay-client] Pairing request pending for ${email}. The gateway will auto-pair this device if the operator token is valid.`
705
- );
706
- }
707
- // ── E2E Key Exchange ──
708
- async handleE2EExchange(userId, browserPublicKey) {
709
- console.log(`[relay-client] E2E key exchange with user ${userId}`);
710
- const conn = this.userConnections.get(userId);
711
- if (!conn) return;
712
- try {
713
- const e2e = new E2ECrypto();
714
- const gatewayPublicKey = await e2e.generateKeyPair();
715
- await e2e.deriveSharedSecret(browserPublicKey);
716
- conn.e2e = e2e;
717
- this.sendToRelay({
718
- type: "relay.forward",
719
- userId,
720
- inner: {
721
- type: "relay.e2e.exchange",
722
- publicKey: gatewayPublicKey
723
- }
724
- });
725
- console.log(`[relay-client] E2E established for user ${userId}`);
726
- } catch (err2) {
727
- console.error(`[relay-client] E2E exchange failed for ${userId}:`, err2);
728
- }
729
- }
730
- // ── Send to Relay ──
731
- sendToRelay(msg) {
732
- if (!this.relayWs || this.relayWs.readyState !== NodeWebSocket.OPEN) return;
733
- try {
734
- this.relayWs.send(JSON.stringify(msg));
735
- } catch {
736
- }
737
- }
738
- /** Broadcast an event to all connected users, E2E encrypted per-user */
739
- broadcastToUsers(event, payload) {
740
- const msg = { type: "event", event, payload };
741
- for (const [userId, conn] of this.userConnections) {
742
- if (!conn.connectHandshakeComplete) continue;
743
- let innerMsg = msg;
744
- if (conn.e2e) {
745
- try {
746
- const encrypted = conn.e2e.encrypt(JSON.stringify(msg));
747
- innerMsg = { _e2e: true, ...encrypted };
748
- } catch (err2) {
749
- console.error(`[relay-client] E2E encrypt failed for broadcast to ${userId} \u2014 skipping:`, err2);
750
- continue;
751
- }
752
- }
753
- this.sendToRelay({
754
- type: "relay.forward",
755
- userId,
756
- inner: innerMsg
757
- });
51
+ const agentsDir = path2.join(stateDir, "agents");
52
+ if (!fs2.existsSync(agentsDir)) return [];
53
+ const copied = [];
54
+ for (const entry of fs2.readdirSync(agentsDir, { withFileTypes: true })) {
55
+ if (!entry.isDirectory()) continue;
56
+ const agentId = entry.name;
57
+ if (ensureAgentAuthProfiles(agentId)) {
58
+ copied.push(agentId);
758
59
  }
759
60
  }
760
- };
761
- var relayClient = null;
762
- function startRelayClient(api, relayUrl) {
763
- relayClient = new RelayClient({
764
- relayUrl
765
- });
766
- relayClient.start();
767
- api.registerGatewayMethod(
768
- "squad.relay.status",
769
- async ({ respond }) => {
770
- respond(true, {
771
- connected: relayClient !== null,
772
- relayUrl
773
- });
774
- }
775
- );
776
- const cleanup = () => {
777
- if (relayClient) {
778
- relayClient.destroy();
779
- relayClient = null;
780
- }
781
- };
782
- process.on("SIGTERM", cleanup);
783
- process.on("SIGINT", cleanup);
784
- }
785
- function broadcastToUsers(event, payload) {
786
- relayClient?.broadcastToUsers(event, payload);
61
+ return copied;
787
62
  }
788
63
 
789
64
  // src/agents.ts
790
- import { execSync } from "child_process";
791
- import path4 from "path";
792
65
  function deriveAgentIdFromName(name) {
793
66
  const normalized = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
794
67
  return normalized || "agent";
@@ -825,7 +98,7 @@ function registerAgentMethods(api) {
825
98
  return;
826
99
  }
827
100
  const effectiveAgentId = providedAgentId || deriveAgentIdFromName(safeName);
828
- const defaultWorkspace = path4.join(
101
+ const defaultWorkspace = path3.join(
829
102
  getOpenclawStateDir(),
830
103
  effectiveAgentId === "main" ? "workspace" : `workspace-${effectiveAgentId}`
831
104
  );
@@ -840,6 +113,11 @@ function registerAgentMethods(api) {
840
113
  encoding: "utf-8",
841
114
  stdio: ["pipe", "pipe", "pipe"]
842
115
  });
116
+ try {
117
+ ensureAgentAuthProfiles(effectiveAgentId);
118
+ } catch (authErr) {
119
+ console.warn("[squad.agents.add] failed to copy main auth profile to agent:", authErr);
120
+ }
843
121
  respond(true, { ok: true, output: output.slice(0, 1e3) });
844
122
  } catch (err2) {
845
123
  const msg = err2 instanceof Error ? err2.message : String(err2);
@@ -985,12 +263,12 @@ function registerAgentMethods(api) {
985
263
 
986
264
  // src/entities.ts
987
265
  import { Type as T } from "@sinclair/typebox";
988
- import path8 from "path";
989
- import fs7 from "fs";
266
+ import path7 from "path";
267
+ import fs6 from "fs";
990
268
 
991
269
  // src/watcher.ts
992
- import path5 from "path";
993
- import fs4 from "fs";
270
+ import path4 from "path";
271
+ import fs3 from "fs";
994
272
  import chokidar from "chokidar";
995
273
  var debounceTimers = /* @__PURE__ */ new Map();
996
274
  var DEBOUNCE_MS = 500;
@@ -1020,29 +298,29 @@ function debouncedFs(relPath, action, fn) {
1020
298
  );
1021
299
  }
1022
300
  function isWorkspaceIdentity(filePath, configDir) {
1023
- const rel = path5.relative(configDir, filePath);
301
+ const rel = path4.relative(configDir, filePath);
1024
302
  const match = rel.match(/^(workspace(?:-([^/]+))?)\/IDENTITY\.md$/);
1025
303
  if (!match) return null;
1026
304
  const dirName = match[1];
1027
305
  const agentId = match[2] ?? "main";
1028
- return { agentId, workspacePath: path5.join(configDir, dirName) };
306
+ return { agentId, workspacePath: path4.join(configDir, dirName) };
1029
307
  }
1030
308
  function isWorkspaceAgentJson(filePath, configDir) {
1031
- const rel = path5.relative(configDir, filePath);
309
+ const rel = path4.relative(configDir, filePath);
1032
310
  const match = rel.match(/^(workspace(?:-([^/]+))?)\/agent\.json$/);
1033
311
  if (!match) return null;
1034
312
  const dirName = match[1];
1035
313
  const agentId = match[2] ?? "main";
1036
- return { agentId, workspacePath: path5.join(configDir, dirName) };
314
+ return { agentId, workspacePath: path4.join(configDir, dirName) };
1037
315
  }
1038
316
  function isGlobalSkillDir(filePath, configDir) {
1039
- const rel = path5.relative(configDir, filePath);
317
+ const rel = path4.relative(configDir, filePath);
1040
318
  const match = rel.match(/^skills\/([^/]+)\/?$/);
1041
319
  if (!match) return null;
1042
320
  return { skillKey: match[1] };
1043
321
  }
1044
322
  function isWorkspaceSkillDir(filePath, configDir) {
1045
- const rel = path5.relative(configDir, filePath);
323
+ const rel = path4.relative(configDir, filePath);
1046
324
  const match = rel.match(
1047
325
  /^workspace(?:-([^/]+))?\/skills\/([^/]+)\/?$/
1048
326
  );
@@ -1050,21 +328,21 @@ function isWorkspaceSkillDir(filePath, configDir) {
1050
328
  return { agentId: match[1] ?? "main", skillKey: match[2] };
1051
329
  }
1052
330
  function isPluginManifest(filePath, configDir) {
1053
- const rel = path5.relative(configDir, filePath);
331
+ const rel = path4.relative(configDir, filePath);
1054
332
  const match = rel.match(/^extensions\/([^/]+)\/openclaw\.plugin\.json$/);
1055
333
  if (!match) return null;
1056
334
  return { pluginDirName: match[1] };
1057
335
  }
1058
336
  function isOpenClawConfig(filePath, configDir) {
1059
- return path5.relative(configDir, filePath) === "openclaw.json";
337
+ return path4.relative(configDir, filePath) === "openclaw.json";
1060
338
  }
1061
339
  function updateAgent(agentId, workspacePath) {
1062
340
  const now = Date.now();
1063
341
  let name = agentId;
1064
342
  const metadata = { workspacePath };
1065
343
  try {
1066
- const content = fs4.readFileSync(
1067
- path5.join(workspacePath, "IDENTITY.md"),
344
+ const content = fs3.readFileSync(
345
+ path4.join(workspacePath, "IDENTITY.md"),
1068
346
  "utf-8"
1069
347
  );
1070
348
  const parsed = parseIdentityName(content);
@@ -1073,8 +351,8 @@ function updateAgent(agentId, workspacePath) {
1073
351
  }
1074
352
  if (name === agentId) {
1075
353
  try {
1076
- const raw = fs4.readFileSync(
1077
- path5.join(workspacePath, "agent.json"),
354
+ const raw = fs3.readFileSync(
355
+ path4.join(workspacePath, "agent.json"),
1078
356
  "utf-8"
1079
357
  );
1080
358
  const config = JSON.parse(raw);
@@ -1098,14 +376,14 @@ function updateAgent(agentId, workspacePath) {
1098
376
  }
1099
377
  function updatePlugin(pluginDirName, configDir) {
1100
378
  const now = Date.now();
1101
- const manifestPath = path5.join(
379
+ const manifestPath = path4.join(
1102
380
  configDir,
1103
381
  "extensions",
1104
382
  pluginDirName,
1105
383
  "openclaw.plugin.json"
1106
384
  );
1107
385
  try {
1108
- const raw = fs4.readFileSync(manifestPath, "utf-8");
386
+ const raw = fs3.readFileSync(manifestPath, "utf-8");
1109
387
  const manifest = JSON.parse(raw);
1110
388
  const pluginId = manifest.id || pluginDirName;
1111
389
  const name = manifest.name || pluginId;
@@ -1115,7 +393,7 @@ function updatePlugin(pluginDirName, configDir) {
1115
393
  name,
1116
394
  title: name,
1117
395
  description: manifest.description || null,
1118
- metadata: { pluginId, pluginDir: path5.dirname(manifestPath) },
396
+ metadata: { pluginId, pluginDir: path4.dirname(manifestPath) },
1119
397
  source: "filesystem",
1120
398
  source_key: manifestPath,
1121
399
  created_at: now,
@@ -1142,7 +420,7 @@ function startWatcher(configDir, onFsChange) {
1142
420
  });
1143
421
  const emitFsChange = (action, filePath) => {
1144
422
  if (!onFsChange) return;
1145
- const rel = path5.relative(configDir, filePath);
423
+ const rel = path4.relative(configDir, filePath);
1146
424
  debouncedFs(rel, action, () => {
1147
425
  onFsChange({ action, path: rel });
1148
426
  });
@@ -1196,7 +474,7 @@ function startWatcher(configDir, onFsChange) {
1196
474
  );
1197
475
  return;
1198
476
  }
1199
- const rel = path5.relative(configDir, dirPath);
477
+ const rel = path4.relative(configDir, dirPath);
1200
478
  if (/^workspace(-[^/]+)?$/.test(rel)) {
1201
479
  debounced("agents", () => scanAgents(configDir));
1202
480
  return;
@@ -1204,7 +482,7 @@ function startWatcher(configDir, onFsChange) {
1204
482
  };
1205
483
  const handleUnlinkDir = (dirPath) => {
1206
484
  emitFsChange("unlinkDir", dirPath);
1207
- const rel = path5.relative(configDir, dirPath);
485
+ const rel = path4.relative(configDir, dirPath);
1208
486
  const wsMatch = rel.match(/^workspace(?:-([^/]+))?$/);
1209
487
  if (wsMatch) {
1210
488
  const agentId = wsMatch[1] ?? "main";
@@ -1241,26 +519,26 @@ function startWatcher(configDir, onFsChange) {
1241
519
  }
1242
520
 
1243
521
  // src/filesystem.ts
1244
- import fs6 from "fs";
1245
- import path7 from "path";
1246
-
1247
- // src/layout.ts
1248
522
  import fs5 from "fs";
1249
523
  import path6 from "path";
524
+
525
+ // src/layout.ts
526
+ import fs4 from "fs";
527
+ import path5 from "path";
1250
528
  function resolveMaybeRelativePath(stateDir, p) {
1251
- if (path6.isAbsolute(p)) return path6.resolve(p);
1252
- return path6.resolve(stateDir, p);
529
+ if (path5.isAbsolute(p)) return path5.resolve(p);
530
+ return path5.resolve(stateDir, p);
1253
531
  }
1254
532
  function listWorkspaceFallbacks(stateDir) {
1255
533
  let entries;
1256
534
  try {
1257
- entries = fs5.readdirSync(stateDir, { withFileTypes: true });
535
+ entries = fs4.readdirSync(stateDir, { withFileTypes: true });
1258
536
  } catch {
1259
537
  return [];
1260
538
  }
1261
539
  return entries.filter((entry) => entry.isDirectory() && (entry.name === "workspace" || entry.name.startsWith("workspace-"))).map((entry) => {
1262
540
  const agentId = entry.name === "workspace" ? "main" : entry.name.replace("workspace-", "");
1263
- const workspacePath = path6.join(stateDir, entry.name);
541
+ const workspacePath = path5.join(stateDir, entry.name);
1264
542
  return {
1265
543
  agentId,
1266
544
  path: workspacePath,
@@ -1271,7 +549,7 @@ function listWorkspaceFallbacks(stateDir) {
1271
549
  }
1272
550
  function readOpenclawConfig(configPath) {
1273
551
  try {
1274
- const raw = fs5.readFileSync(configPath, "utf-8");
552
+ const raw = fs4.readFileSync(configPath, "utf-8");
1275
553
  return JSON.parse(raw);
1276
554
  } catch {
1277
555
  return null;
@@ -1279,7 +557,7 @@ function readOpenclawConfig(configPath) {
1279
557
  }
1280
558
  function resolveGatewayLayout() {
1281
559
  const stateDir = getOpenclawStateDir();
1282
- const configPath = path6.join(stateDir, "openclaw.json");
560
+ const configPath = path5.join(stateDir, "openclaw.json");
1283
561
  const config = readOpenclawConfig(configPath);
1284
562
  const workspaces = [];
1285
563
  if (config?.agents?.main?.workspace || config?.agents?.main?.workspacePath) {
@@ -1290,7 +568,7 @@ function resolveGatewayLayout() {
1290
568
  agentId: "main",
1291
569
  path: resolvedPath,
1292
570
  source: "config",
1293
- exists: fs5.existsSync(resolvedPath)
571
+ exists: fs4.existsSync(resolvedPath)
1294
572
  });
1295
573
  }
1296
574
  }
@@ -1303,7 +581,7 @@ function resolveGatewayLayout() {
1303
581
  agentId,
1304
582
  path: resolvedPath,
1305
583
  source: "config",
1306
- exists: fs5.existsSync(resolvedPath)
584
+ exists: fs4.existsSync(resolvedPath)
1307
585
  });
1308
586
  }
1309
587
  const deduped = /* @__PURE__ */ new Map();
@@ -1317,9 +595,9 @@ function resolveGatewayLayout() {
1317
595
  return {
1318
596
  stateDir,
1319
597
  configPath,
1320
- mediaDir: path6.join(stateDir, "media"),
1321
- skillsDir: path6.join(stateDir, "skills"),
1322
- extensionsDir: path6.join(stateDir, "extensions"),
598
+ mediaDir: path5.join(stateDir, "media"),
599
+ skillsDir: path5.join(stateDir, "skills"),
600
+ extensionsDir: path5.join(stateDir, "extensions"),
1323
601
  defaultFileBrowserRoot,
1324
602
  workspaces: resolvedWorkspaces
1325
603
  };
@@ -1329,16 +607,16 @@ function resolveGatewayLayout() {
1329
607
  var HOME_DIR = process.env.HOME ?? "/root";
1330
608
  var OPENCLAW_DIR = getOpenclawStateDir();
1331
609
  var SENSITIVE_BLOCKED_DIRS = [
1332
- path7.join(OPENCLAW_DIR, "credentials"),
1333
- path7.join(OPENCLAW_DIR, "devices"),
1334
- path7.join(OPENCLAW_DIR, "identity")
610
+ path6.join(OPENCLAW_DIR, "credentials"),
611
+ path6.join(OPENCLAW_DIR, "devices"),
612
+ path6.join(OPENCLAW_DIR, "identity")
1335
613
  ];
1336
614
  var SENSITIVE_BLOCKED_FILES = [
1337
- path7.join(OPENCLAW_DIR, "squad-ceo-data", "relay", "squad-relay.json")
615
+ path6.join(OPENCLAW_DIR, "squad-ceo-data", "relay", "squad-relay.json")
1338
616
  ];
1339
617
  function isSensitivePath(resolvedPath) {
1340
618
  for (const blocked of SENSITIVE_BLOCKED_DIRS) {
1341
- if (resolvedPath === blocked || resolvedPath.startsWith(blocked + path7.sep)) {
619
+ if (resolvedPath === blocked || resolvedPath.startsWith(blocked + path6.sep)) {
1342
620
  return true;
1343
621
  }
1344
622
  }
@@ -1347,7 +625,7 @@ function isSensitivePath(resolvedPath) {
1347
625
  return true;
1348
626
  }
1349
627
  }
1350
- if (path7.dirname(resolvedPath) === OPENCLAW_DIR && resolvedPath.endsWith(".bak")) {
628
+ if (path6.dirname(resolvedPath) === OPENCLAW_DIR && resolvedPath.endsWith(".bak")) {
1351
629
  return true;
1352
630
  }
1353
631
  return false;
@@ -1396,20 +674,20 @@ function redactOpenclawJson(rawContent) {
1396
674
  return JSON.stringify(config, null, 2);
1397
675
  }
1398
676
  function isOpenclawJson(resolvedPath) {
1399
- return path7.basename(resolvedPath) === OPENCLAW_JSON_FILENAME && resolvedPath.startsWith(OPENCLAW_DIR);
677
+ return path6.basename(resolvedPath) === OPENCLAW_JSON_FILENAME && resolvedPath.startsWith(OPENCLAW_DIR);
1400
678
  }
1401
679
  function expandHome(p) {
1402
680
  if (p.startsWith("~/") || p === "~") {
1403
- return path7.join(HOME_DIR, p.slice(1));
681
+ return path6.join(HOME_DIR, p.slice(1));
1404
682
  }
1405
683
  return p;
1406
684
  }
1407
685
  function validatePath(p, allowedRoots) {
1408
- const resolved = path7.resolve(expandHome(p));
686
+ const resolved = path6.resolve(expandHome(p));
1409
687
  if (!allowedRoots || allowedRoots.length === 0) return resolved;
1410
688
  const allowed = allowedRoots.some((root) => {
1411
- const resolvedRoot = path7.resolve(expandHome(root));
1412
- return resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path7.sep);
689
+ const resolvedRoot = path6.resolve(expandHome(root));
690
+ return resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path6.sep);
1413
691
  });
1414
692
  if (!allowed) {
1415
693
  throw new Error(`Path "${p}" is outside allowed roots`);
@@ -1446,18 +724,18 @@ function err(message) {
1446
724
  };
1447
725
  }
1448
726
  function listDir(dirPath, opts) {
1449
- const dirents = fs6.readdirSync(dirPath, { withFileTypes: true });
727
+ const dirents = fs5.readdirSync(dirPath, { withFileTypes: true });
1450
728
  const results = [];
1451
729
  for (const dirent of dirents) {
1452
730
  if (!opts.includeHidden && dirent.name.startsWith(".")) continue;
1453
- const entryPath = path7.join(dirPath, dirent.name);
731
+ const entryPath = path6.join(dirPath, dirent.name);
1454
732
  let type = "other";
1455
733
  if (dirent.isFile()) type = "file";
1456
734
  else if (dirent.isDirectory()) type = "directory";
1457
735
  else if (dirent.isSymbolicLink()) type = "symlink";
1458
736
  const entry = { name: dirent.name, path: entryPath, type };
1459
737
  try {
1460
- const stat = fs6.statSync(entryPath);
738
+ const stat = fs5.statSync(entryPath);
1461
739
  entry.size = stat.size;
1462
740
  entry.modified = stat.mtime.toISOString();
1463
741
  } catch {
@@ -1510,8 +788,8 @@ function registerFilesystemTools(api) {
1510
788
  try {
1511
789
  const filePath = validateAndBlockSensitive(params.path, allowedRoots);
1512
790
  const encoding = params.encoding ?? "utf-8";
1513
- let content = fs6.readFileSync(filePath, encoding);
1514
- const stat = fs6.statSync(filePath);
791
+ let content = fs5.readFileSync(filePath, encoding);
792
+ const stat = fs5.statSync(filePath);
1515
793
  if (isOpenclawJson(filePath) && encoding === "utf-8") {
1516
794
  content = redactOpenclawJson(content);
1517
795
  }
@@ -1561,10 +839,10 @@ function registerFilesystemTools(api) {
1561
839
  const encoding = params.encoding ?? "utf-8";
1562
840
  const mkdir = params.mkdir !== false;
1563
841
  if (mkdir) {
1564
- fs6.mkdirSync(path7.dirname(filePath), { recursive: true });
842
+ fs5.mkdirSync(path6.dirname(filePath), { recursive: true });
1565
843
  }
1566
- fs6.writeFileSync(filePath, content, encoding);
1567
- const stat = fs6.statSync(filePath);
844
+ fs5.writeFileSync(filePath, content, encoding);
845
+ const stat = fs5.statSync(filePath);
1568
846
  return ok({
1569
847
  path: filePath,
1570
848
  size: stat.size,
@@ -1633,7 +911,7 @@ function registerFilesystemTools(api) {
1633
911
  async execute(_id, params) {
1634
912
  try {
1635
913
  const targetPath = validateWritePath(params.path, allowedRoots);
1636
- fs6.mkdirSync(targetPath, { recursive: true });
914
+ fs5.mkdirSync(targetPath, { recursive: true });
1637
915
  return ok({
1638
916
  path: targetPath,
1639
917
  created: true
@@ -1666,7 +944,7 @@ function registerFilesystemTools(api) {
1666
944
  try {
1667
945
  const resolvedOld = validateWritePath(params.oldPath, allowedRoots);
1668
946
  const resolvedNew = validateWritePath(params.newPath, allowedRoots);
1669
- fs6.renameSync(resolvedOld, resolvedNew);
947
+ fs5.renameSync(resolvedOld, resolvedNew);
1670
948
  return ok({
1671
949
  oldPath: resolvedOld,
1672
950
  newPath: resolvedNew,
@@ -1695,12 +973,12 @@ function registerFilesystemTools(api) {
1695
973
  async execute(_id, params) {
1696
974
  try {
1697
975
  const targetPath = validateWritePath(params.path, allowedRoots);
1698
- const stat = fs6.statSync(targetPath);
976
+ const stat = fs5.statSync(targetPath);
1699
977
  const wasDirectory = stat.isDirectory();
1700
978
  if (wasDirectory) {
1701
- fs6.rmSync(targetPath, { recursive: true });
979
+ fs5.rmSync(targetPath, { recursive: true });
1702
980
  } else {
1703
- fs6.unlinkSync(targetPath);
981
+ fs5.unlinkSync(targetPath);
1704
982
  }
1705
983
  return ok({
1706
984
  path: targetPath,
@@ -1752,7 +1030,7 @@ function scanAgents(configDir) {
1752
1030
  const now = Date.now();
1753
1031
  let entries;
1754
1032
  try {
1755
- entries = fs7.readdirSync(configDir, { withFileTypes: true });
1033
+ entries = fs6.readdirSync(configDir, { withFileTypes: true });
1756
1034
  } catch {
1757
1035
  return;
1758
1036
  }
@@ -1761,20 +1039,20 @@ function scanAgents(configDir) {
1761
1039
  );
1762
1040
  for (const dir of workspaceDirs) {
1763
1041
  const agentId = dir.name === "workspace" ? "main" : dir.name.replace("workspace-", "");
1764
- const workspacePath = path8.join(configDir, dir.name);
1042
+ const workspacePath = path7.join(configDir, dir.name);
1765
1043
  let name = agentId;
1766
1044
  const metadata = { workspacePath };
1767
- const identityPath = path8.join(workspacePath, "IDENTITY.md");
1045
+ const identityPath = path7.join(workspacePath, "IDENTITY.md");
1768
1046
  try {
1769
- const content = fs7.readFileSync(identityPath, "utf-8");
1047
+ const content = fs6.readFileSync(identityPath, "utf-8");
1770
1048
  const parsed = parseIdentityName(content);
1771
1049
  if (parsed) name = parsed;
1772
1050
  } catch {
1773
1051
  }
1774
1052
  if (name === agentId) {
1775
- const agentJsonPath = path8.join(workspacePath, "agent.json");
1053
+ const agentJsonPath = path7.join(workspacePath, "agent.json");
1776
1054
  try {
1777
- const raw = fs7.readFileSync(agentJsonPath, "utf-8");
1055
+ const raw = fs6.readFileSync(agentJsonPath, "utf-8");
1778
1056
  const config = JSON.parse(raw);
1779
1057
  if (config.displayName) name = config.displayName;
1780
1058
  if (config.model) metadata.model = config.model;
@@ -1799,11 +1077,11 @@ function scanAgents(configDir) {
1799
1077
  }
1800
1078
  function scanSkills(configDir) {
1801
1079
  const now = Date.now();
1802
- const globalSkillsDir = path8.join(configDir, "skills");
1080
+ const globalSkillsDir = path7.join(configDir, "skills");
1803
1081
  scanSkillsDir(globalSkillsDir, "global", now);
1804
1082
  let entries;
1805
1083
  try {
1806
- entries = fs7.readdirSync(configDir, { withFileTypes: true });
1084
+ entries = fs6.readdirSync(configDir, { withFileTypes: true });
1807
1085
  } catch {
1808
1086
  return;
1809
1087
  }
@@ -1812,26 +1090,26 @@ function scanSkills(configDir) {
1812
1090
  continue;
1813
1091
  }
1814
1092
  const agentId = dir.name === "workspace" ? "main" : dir.name.replace("workspace-", "");
1815
- const agentSkillsDir = path8.join(configDir, dir.name, "skills");
1093
+ const agentSkillsDir = path7.join(configDir, dir.name, "skills");
1816
1094
  scanSkillsDir(agentSkillsDir, agentId, now);
1817
1095
  }
1818
1096
  }
1819
1097
  function scanSkillsDir(skillsDir, scope, now) {
1820
1098
  let entries;
1821
1099
  try {
1822
- entries = fs7.readdirSync(skillsDir, { withFileTypes: true });
1100
+ entries = fs6.readdirSync(skillsDir, { withFileTypes: true });
1823
1101
  } catch {
1824
1102
  return;
1825
1103
  }
1826
1104
  for (const entry of entries) {
1827
1105
  if (!entry.isDirectory()) continue;
1828
1106
  const skillKey = entry.name;
1829
- const skillPath = path8.join(skillsDir, skillKey);
1107
+ const skillPath = path7.join(skillsDir, skillKey);
1830
1108
  let name = skillKey;
1831
1109
  for (const manifestName of ["manifest.json", "package.json"]) {
1832
1110
  try {
1833
- const raw = fs7.readFileSync(
1834
- path8.join(skillPath, manifestName),
1111
+ const raw = fs6.readFileSync(
1112
+ path7.join(skillPath, manifestName),
1835
1113
  "utf-8"
1836
1114
  );
1837
1115
  const manifest = JSON.parse(raw);
@@ -1858,19 +1136,19 @@ function scanSkillsDir(skillsDir, scope, now) {
1858
1136
  }
1859
1137
  function scanPlugins2(configDir) {
1860
1138
  const now = Date.now();
1861
- const extensionsDir = path8.join(configDir, "extensions");
1139
+ const extensionsDir = path7.join(configDir, "extensions");
1862
1140
  let entries;
1863
1141
  try {
1864
- entries = fs7.readdirSync(extensionsDir, { withFileTypes: true });
1142
+ entries = fs6.readdirSync(extensionsDir, { withFileTypes: true });
1865
1143
  } catch {
1866
1144
  return;
1867
1145
  }
1868
1146
  for (const dir of entries) {
1869
1147
  if (!dir.isDirectory()) continue;
1870
- const pluginDir = path8.join(extensionsDir, dir.name);
1871
- const manifestPath = path8.join(pluginDir, "openclaw.plugin.json");
1148
+ const pluginDir = path7.join(extensionsDir, dir.name);
1149
+ const manifestPath = path7.join(pluginDir, "openclaw.plugin.json");
1872
1150
  try {
1873
- const raw = fs7.readFileSync(manifestPath, "utf-8");
1151
+ const raw = fs6.readFileSync(manifestPath, "utf-8");
1874
1152
  const manifest = JSON.parse(raw);
1875
1153
  const pluginId = manifest.id || dir.name;
1876
1154
  const name = manifest.name || pluginId;
@@ -1893,8 +1171,8 @@ function scanPlugins2(configDir) {
1893
1171
  function scanTools(configDir) {
1894
1172
  const now = Date.now();
1895
1173
  try {
1896
- const raw = fs7.readFileSync(
1897
- path8.join(configDir, "openclaw.json"),
1174
+ const raw = fs6.readFileSync(
1175
+ path7.join(configDir, "openclaw.json"),
1898
1176
  "utf-8"
1899
1177
  );
1900
1178
  const config = JSON.parse(raw);
@@ -1945,24 +1223,24 @@ var MIME_MAP = {
1945
1223
  ".gz": "application/gzip"
1946
1224
  };
1947
1225
  function getMimeType(filename) {
1948
- const ext = path8.extname(filename).toLowerCase();
1226
+ const ext = path7.extname(filename).toLowerCase();
1949
1227
  return MIME_MAP[ext] ?? "application/octet-stream";
1950
1228
  }
1951
1229
  function scanMedia(configDir) {
1952
1230
  const now = Date.now();
1953
- const mediaDir = path8.join(configDir, "media");
1231
+ const mediaDir = path7.join(configDir, "media");
1954
1232
  scanMediaDir(mediaDir, now);
1955
1233
  }
1956
1234
  function scanMediaDir(dirPath, now) {
1957
1235
  let entries;
1958
1236
  try {
1959
- entries = fs7.readdirSync(dirPath, { withFileTypes: true });
1237
+ entries = fs6.readdirSync(dirPath, { withFileTypes: true });
1960
1238
  } catch {
1961
1239
  return;
1962
1240
  }
1963
1241
  for (const entry of entries) {
1964
1242
  if (entry.name.startsWith(".")) continue;
1965
- const entryPath = path8.join(dirPath, entry.name);
1243
+ const entryPath = path7.join(dirPath, entry.name);
1966
1244
  if (isSensitivePath(entryPath)) continue;
1967
1245
  if (entry.isDirectory()) {
1968
1246
  registrySet({
@@ -1983,7 +1261,7 @@ function scanMediaDir(dirPath, now) {
1983
1261
  let size;
1984
1262
  let mtime = now;
1985
1263
  try {
1986
- const stat = fs7.statSync(entryPath);
1264
+ const stat = fs6.statSync(entryPath);
1987
1265
  size = stat.size;
1988
1266
  mtime = stat.mtimeMs;
1989
1267
  } catch {
@@ -2100,24 +1378,24 @@ function registerEntityTools(api, onFsChange) {
2100
1378
 
2101
1379
  // src/sql.ts
2102
1380
  import { execFile } from "child_process";
2103
- import path9 from "path";
2104
- import fs8 from "fs";
1381
+ import path8 from "path";
1382
+ import fs7 from "fs";
2105
1383
  import { Type as T2 } from "@sinclair/typebox";
2106
1384
  var HOME_DIR2 = process.env.HOME ?? "/root";
2107
- var ALLOWED_DATA_DIR = path9.join(getOpenclawStateDir(), "squad-ceo-data");
1385
+ var ALLOWED_DATA_DIR = path8.join(getOpenclawStateDir(), "squad-ceo-data");
2108
1386
  function validateDbPath(dbPath) {
2109
1387
  let expanded = dbPath;
2110
1388
  if (expanded.startsWith("~/") || expanded === "~") {
2111
- expanded = path9.join(HOME_DIR2, expanded.slice(1));
1389
+ expanded = path8.join(HOME_DIR2, expanded.slice(1));
2112
1390
  }
2113
- const resolved = path9.resolve(expanded);
2114
- if (resolved !== ALLOWED_DATA_DIR && !resolved.startsWith(ALLOWED_DATA_DIR + path9.sep)) {
1391
+ const resolved = path8.resolve(expanded);
1392
+ if (resolved !== ALLOWED_DATA_DIR && !resolved.startsWith(ALLOWED_DATA_DIR + path8.sep)) {
2115
1393
  throw new Error(
2116
1394
  `Access denied: database path must be within ~/.openclaw/squad-ceo-data/`
2117
1395
  );
2118
1396
  }
2119
1397
  try {
2120
- const stat = fs8.statSync(resolved);
1398
+ const stat = fs7.statSync(resolved);
2121
1399
  if (!stat.isFile()) {
2122
1400
  throw new Error(`Not a file: ${dbPath}`);
2123
1401
  }
@@ -2184,11 +1462,11 @@ function registerSqlTools(api) {
2184
1462
 
2185
1463
  // src/version.ts
2186
1464
  import { spawn } from "child_process";
2187
- import fs9 from "fs";
2188
- import path10 from "path";
1465
+ import fs8 from "fs";
1466
+ import path9 from "path";
2189
1467
  import { fileURLToPath } from "url";
2190
1468
  var PACKAGE_NAME = "squad-openclaw";
2191
- var CONFIG_PATH = path10.join(getOpenclawStateDir(), "openclaw.json");
1469
+ var CONFIG_PATH = path9.join(getOpenclawStateDir(), "openclaw.json");
2192
1470
  var updateInProgress = false;
2193
1471
  var VERIFY_TIMEOUT_MS = 2e4;
2194
1472
  var VERIFY_INTERVAL_MS = 500;
@@ -2198,7 +1476,7 @@ var COMMAND_OUTPUT_LIMIT = 8e3;
2198
1476
  var COMMAND_KILL_GRACE_MS = 2e3;
2199
1477
  function readInstalledVersionFromConfig() {
2200
1478
  try {
2201
- const raw = fs9.readFileSync(CONFIG_PATH, "utf-8");
1479
+ const raw = fs8.readFileSync(CONFIG_PATH, "utf-8");
2202
1480
  const cfg = JSON.parse(raw);
2203
1481
  const v = cfg?.plugins?.installs?.[PACKAGE_NAME]?.version;
2204
1482
  return typeof v === "string" ? v : null;
@@ -2209,7 +1487,7 @@ function readInstalledVersionFromConfig() {
2209
1487
  function reconcileInstallMetadata(verification) {
2210
1488
  if (!verification.installPath || !verification.packageVersion) return;
2211
1489
  try {
2212
- const raw = fs9.readFileSync(CONFIG_PATH, "utf-8");
1490
+ const raw = fs8.readFileSync(CONFIG_PATH, "utf-8");
2213
1491
  const config = JSON.parse(raw);
2214
1492
  if (!config.plugins || typeof config.plugins !== "object") config.plugins = {};
2215
1493
  if (!config.plugins.installs || typeof config.plugins.installs !== "object") {
@@ -2232,15 +1510,15 @@ function reconcileInstallMetadata(verification) {
2232
1510
  ...entry,
2233
1511
  enabled: true
2234
1512
  };
2235
- fs9.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
1513
+ fs8.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
2236
1514
  } catch {
2237
1515
  }
2238
1516
  }
2239
1517
  function getCurrentVersion() {
2240
1518
  const thisFile = fileURLToPath(import.meta.url);
2241
- const pkgPath = path10.resolve(path10.dirname(thisFile), "..", "package.json");
1519
+ const pkgPath = path9.resolve(path9.dirname(thisFile), "..", "package.json");
2242
1520
  try {
2243
- const pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
1521
+ const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
2244
1522
  return pkg.version ?? "0.0.0";
2245
1523
  } catch {
2246
1524
  return "0.0.0";
@@ -2335,7 +1613,7 @@ function compareVersions(a, b) {
2335
1613
  function verifyInstalledPluginState() {
2336
1614
  let configRaw;
2337
1615
  try {
2338
- configRaw = fs9.readFileSync(CONFIG_PATH, "utf-8");
1616
+ configRaw = fs8.readFileSync(CONFIG_PATH, "utf-8");
2339
1617
  } catch (err2) {
2340
1618
  const msg = err2 instanceof Error ? err2.message : String(err2);
2341
1619
  return {
@@ -2375,11 +1653,11 @@ function verifyInstalledPluginState() {
2375
1653
  };
2376
1654
  }
2377
1655
  const requiredFiles = [
2378
- path10.join(installPath, "package.json"),
2379
- path10.join(installPath, "openclaw.plugin.json"),
2380
- path10.join(installPath, "dist", "index.js")
1656
+ path9.join(installPath, "package.json"),
1657
+ path9.join(installPath, "openclaw.plugin.json"),
1658
+ path9.join(installPath, "dist", "index.js")
2381
1659
  ];
2382
- const requiredFilesMissing = requiredFiles.filter((p) => !fs9.existsSync(p));
1660
+ const requiredFilesMissing = requiredFiles.filter((p) => !fs8.existsSync(p));
2383
1661
  if (requiredFilesMissing.length > 0) {
2384
1662
  return {
2385
1663
  ok: false,
@@ -2393,7 +1671,7 @@ function verifyInstalledPluginState() {
2393
1671
  let installedPackage;
2394
1672
  try {
2395
1673
  installedPackage = JSON.parse(
2396
- fs9.readFileSync(path10.join(installPath, "package.json"), "utf-8")
1674
+ fs8.readFileSync(path9.join(installPath, "package.json"), "utf-8")
2397
1675
  );
2398
1676
  } catch (err2) {
2399
1677
  const msg = err2 instanceof Error ? err2.message : String(err2);
@@ -2408,7 +1686,7 @@ function verifyInstalledPluginState() {
2408
1686
  }
2409
1687
  try {
2410
1688
  JSON.parse(
2411
- fs9.readFileSync(path10.join(installPath, "openclaw.plugin.json"), "utf-8")
1689
+ fs8.readFileSync(path9.join(installPath, "openclaw.plugin.json"), "utf-8")
2412
1690
  );
2413
1691
  } catch (err2) {
2414
1692
  const msg = err2 instanceof Error ? err2.message : String(err2);
@@ -2511,7 +1789,7 @@ function registerVersionMethods(api) {
2511
1789
  let updateOutput = "";
2512
1790
  let configBackup = null;
2513
1791
  try {
2514
- configBackup = fs9.readFileSync(CONFIG_PATH, "utf-8");
1792
+ configBackup = fs8.readFileSync(CONFIG_PATH, "utf-8");
2515
1793
  } catch {
2516
1794
  }
2517
1795
  await runCommand(["doctor", "--fix"], 3e4);
@@ -2536,7 +1814,7 @@ function registerVersionMethods(api) {
2536
1814
  } catch (installErr) {
2537
1815
  if (configBackup) {
2538
1816
  try {
2539
- fs9.writeFileSync(CONFIG_PATH, configBackup, "utf-8");
1817
+ fs8.writeFileSync(CONFIG_PATH, configBackup, "utf-8");
2540
1818
  } catch {
2541
1819
  }
2542
1820
  }
@@ -2556,7 +1834,7 @@ function registerVersionMethods(api) {
2556
1834
  if (!verification.ok) {
2557
1835
  if (configBackup) {
2558
1836
  try {
2559
- fs9.writeFileSync(CONFIG_PATH, configBackup, "utf-8");
1837
+ fs8.writeFileSync(CONFIG_PATH, configBackup, "utf-8");
2560
1838
  } catch {
2561
1839
  }
2562
1840
  }
@@ -2682,34 +1960,22 @@ function registerQuestionMethods(api) {
2682
1960
  }
2683
1961
 
2684
1962
  // src/sessions.ts
2685
- import fs10 from "fs";
2686
- import path11 from "path";
2687
- import crypto4 from "crypto";
1963
+ import fs9 from "fs";
1964
+ import path10 from "path";
1965
+ import crypto from "crypto";
1966
+
1967
+ // src/gateway-invoke.ts
2688
1968
  function asRecord(value) {
2689
1969
  return value && typeof value === "object" ? value : null;
2690
1970
  }
2691
- function extractSessionKey(value) {
2692
- const record = asRecord(value);
2693
- if (!record) return null;
2694
- if (typeof record.key === "string" && record.key.trim()) {
2695
- return record.key.trim();
2696
- }
2697
- const nestedKey = asRecord(record.key);
2698
- if (nestedKey && typeof nestedKey.key === "string" && nestedKey.key.trim()) {
2699
- return nestedKey.key.trim();
2700
- }
2701
- if (typeof record.sessionKey === "string" && record.sessionKey.trim()) {
2702
- return record.sessionKey.trim();
2703
- }
2704
- const nestedSession = asRecord(record.session);
2705
- if (nestedSession && typeof nestedSession.key === "string" && nestedSession.key.trim()) {
2706
- return nestedSession.key.trim();
2707
- }
2708
- return null;
2709
- }
2710
1971
  function isInvoker(fn) {
2711
1972
  return typeof fn === "function";
2712
1973
  }
1974
+ function isUnknownGatewayMethodError(message) {
1975
+ return /unknown method|method .* unavailable|not found|invalid[_ ]request|does not exist/i.test(
1976
+ message
1977
+ );
1978
+ }
2713
1979
  async function callGatewayAny(ctx, api, method, params) {
2714
1980
  const ctxGateway = asRecord(ctx.gateway);
2715
1981
  const apiGateway = asRecord(api?.gateway);
@@ -2735,9 +2001,7 @@ async function callGatewayAny(ctx, api, method, params) {
2735
2001
  } catch (err2) {
2736
2002
  lastErr = err2;
2737
2003
  const msg = err2 instanceof Error ? err2.message : String(err2);
2738
- if (/unknown method|method .* unavailable|not found|invalid[_ ]request|does not exist/i.test(
2739
- msg
2740
- )) {
2004
+ if (isUnknownGatewayMethodError(msg)) {
2741
2005
  continue;
2742
2006
  }
2743
2007
  throw err2;
@@ -2746,16 +2010,40 @@ async function callGatewayAny(ctx, api, method, params) {
2746
2010
  if (lastErr) throw lastErr;
2747
2011
  throw new Error("Gateway method invocation API unavailable in plugin context");
2748
2012
  }
2013
+
2014
+ // src/sessions.ts
2015
+ function asRecord2(value) {
2016
+ return value && typeof value === "object" ? value : null;
2017
+ }
2018
+ function extractSessionKey(value) {
2019
+ const record = asRecord2(value);
2020
+ if (!record) return null;
2021
+ if (typeof record.key === "string" && record.key.trim()) {
2022
+ return record.key.trim();
2023
+ }
2024
+ const nestedKey = asRecord2(record.key);
2025
+ if (nestedKey && typeof nestedKey.key === "string" && nestedKey.key.trim()) {
2026
+ return nestedKey.key.trim();
2027
+ }
2028
+ if (typeof record.sessionKey === "string" && record.sessionKey.trim()) {
2029
+ return record.sessionKey.trim();
2030
+ }
2031
+ const nestedSession = asRecord2(record.session);
2032
+ if (nestedSession && typeof nestedSession.key === "string" && nestedSession.key.trim()) {
2033
+ return nestedSession.key.trim();
2034
+ }
2035
+ return null;
2036
+ }
2749
2037
  function parseAgentIdFromSessionKey(sessionKey) {
2750
2038
  const m = sessionKey.match(/^agent:([^:]+):/);
2751
2039
  return m?.[1] ?? null;
2752
2040
  }
2753
2041
  function ensureDir(dirPath) {
2754
- fs10.mkdirSync(dirPath, { recursive: true });
2042
+ fs9.mkdirSync(dirPath, { recursive: true });
2755
2043
  }
2756
2044
  function readSessionsMap(sessionsJsonPath) {
2757
2045
  try {
2758
- const raw = fs10.readFileSync(sessionsJsonPath, "utf-8");
2046
+ const raw = fs9.readFileSync(sessionsJsonPath, "utf-8");
2759
2047
  const parsed = JSON.parse(raw);
2760
2048
  if (parsed && typeof parsed === "object") {
2761
2049
  return parsed;
@@ -2765,22 +2053,22 @@ function readSessionsMap(sessionsJsonPath) {
2765
2053
  return {};
2766
2054
  }
2767
2055
  function writeSessionsMap(sessionsJsonPath, sessionsMap) {
2768
- fs10.writeFileSync(sessionsJsonPath, JSON.stringify(sessionsMap, null, 2), "utf-8");
2056
+ fs9.writeFileSync(sessionsJsonPath, JSON.stringify(sessionsMap, null, 2), "utf-8");
2769
2057
  }
2770
2058
  function createSessionOnDisk(input) {
2771
2059
  const requestedSessionKey = input.requestedSessionKey?.trim();
2772
2060
  const requestedAgentId = input.requestedAgentId?.trim();
2773
2061
  const derivedAgentId = requestedAgentId || (requestedSessionKey ? parseAgentIdFromSessionKey(requestedSessionKey) : null) || "main";
2774
- const sessionKey = requestedSessionKey || `agent:${derivedAgentId}:${crypto4.randomUUID()}`;
2062
+ const sessionKey = requestedSessionKey || `agent:${derivedAgentId}:${crypto.randomUUID()}`;
2775
2063
  const stateDir = getOpenclawStateDir();
2776
- const sessionsDir = path11.join(stateDir, "agents", derivedAgentId, "sessions");
2777
- const sessionsJsonPath = path11.join(sessionsDir, "sessions.json");
2064
+ const sessionsDir = path10.join(stateDir, "agents", derivedAgentId, "sessions");
2065
+ const sessionsJsonPath = path10.join(sessionsDir, "sessions.json");
2778
2066
  ensureDir(sessionsDir);
2779
2067
  const now = Date.now();
2780
2068
  const sessionsMap = readSessionsMap(sessionsJsonPath);
2781
2069
  const existing = sessionsMap[sessionKey];
2782
- const sessionId = typeof existing?.sessionId === "string" && existing.sessionId.trim() ? existing.sessionId : crypto4.randomUUID();
2783
- const jsonlPath = path11.join(sessionsDir, `${sessionId}.jsonl`);
2070
+ const sessionId = typeof existing?.sessionId === "string" && existing.sessionId.trim() ? existing.sessionId : crypto.randomUUID();
2071
+ const jsonlPath = path10.join(sessionsDir, `${sessionId}.jsonl`);
2784
2072
  sessionsMap[sessionKey] = {
2785
2073
  ...existing ?? {},
2786
2074
  sessionId,
@@ -2791,8 +2079,8 @@ function createSessionOnDisk(input) {
2791
2079
  lastAssistantHasText: typeof existing?.lastAssistantHasText === "boolean" ? existing.lastAssistantHasText : true
2792
2080
  };
2793
2081
  writeSessionsMap(sessionsJsonPath, sessionsMap);
2794
- if (!fs10.existsSync(jsonlPath)) {
2795
- fs10.writeFileSync(jsonlPath, "", "utf-8");
2082
+ if (!fs9.existsSync(jsonlPath)) {
2083
+ fs9.writeFileSync(jsonlPath, "", "utf-8");
2796
2084
  }
2797
2085
  return {
2798
2086
  key: sessionKey,
@@ -2834,15 +2122,16 @@ function registerSessionMethods(api) {
2834
2122
  "sessions.create",
2835
2123
  createParams
2836
2124
  );
2837
- const normalizedKey = extractSessionKey(nativeResult) ?? (typeof sessionKey === "string" ? sessionKey.trim() : null);
2125
+ const normalizedKey = extractSessionKey(nativeResult);
2838
2126
  if (!normalizedKey) {
2839
- respond(false, {
2840
- error: "Session creation succeeded but no session key was returned",
2841
- nativeResult
2127
+ const local = createSessionOnDisk({
2128
+ requestedSessionKey: typeof sessionKey === "string" ? sessionKey : void 0,
2129
+ requestedAgentId: typeof agentId === "string" ? agentId : void 0
2842
2130
  });
2131
+ respond(true, local);
2843
2132
  return;
2844
2133
  }
2845
- const nativeRecord = asRecord(nativeResult);
2134
+ const nativeRecord = asRecord2(nativeResult);
2846
2135
  respond(true, {
2847
2136
  ...nativeRecord ?? {},
2848
2137
  key: normalizedKey
@@ -2873,6 +2162,294 @@ function registerSessionMethods(api) {
2873
2162
  );
2874
2163
  }
2875
2164
 
2165
+ // src/agent-send.ts
2166
+ import fs10 from "fs";
2167
+ import path11 from "path";
2168
+ import { execFile as execFile2 } from "child_process";
2169
+ import { promisify } from "util";
2170
+ var ASSET_REFERENCE_RE = /\{\{asset:([^}]+)\}\}/g;
2171
+ var RETRY_INTERVAL_MS = 1e3;
2172
+ var RETRY_BUDGET_MS = 2e4;
2173
+ var CLI_AGENT_TIMEOUT_MS = 3e4;
2174
+ var execFileAsync = promisify(execFile2);
2175
+ var IMAGE_MIME_BY_EXT = {
2176
+ ".png": "image/png",
2177
+ ".jpg": "image/jpeg",
2178
+ ".jpeg": "image/jpeg",
2179
+ ".gif": "image/gif",
2180
+ ".webp": "image/webp",
2181
+ ".bmp": "image/bmp",
2182
+ ".svg": "image/svg+xml",
2183
+ ".ico": "image/x-icon",
2184
+ ".tif": "image/tiff",
2185
+ ".tiff": "image/tiff",
2186
+ ".avif": "image/avif"
2187
+ };
2188
+ function asRecord3(value) {
2189
+ return value && typeof value === "object" ? value : null;
2190
+ }
2191
+ function sleep2(ms) {
2192
+ return new Promise((resolve) => setTimeout(resolve, ms));
2193
+ }
2194
+ function pathWithin(root, candidate) {
2195
+ return candidate === root || candidate.startsWith(`${root}${path11.sep}`);
2196
+ }
2197
+ function parseAssetPathsFromMessage(message) {
2198
+ const result = [];
2199
+ const re = new RegExp(ASSET_REFERENCE_RE.source, "g");
2200
+ let match;
2201
+ while ((match = re.exec(message)) !== null) {
2202
+ const rawPath = String(match[1] ?? "").trim();
2203
+ if (rawPath) result.push(rawPath);
2204
+ }
2205
+ return result;
2206
+ }
2207
+ function parseAssetPathsFromContext(params) {
2208
+ const context = asRecord3(params.context);
2209
+ const assets = context?.assets;
2210
+ if (!Array.isArray(assets)) return [];
2211
+ const result = [];
2212
+ for (const entry of assets) {
2213
+ if (typeof entry === "string") {
2214
+ const trimmed = entry.trim();
2215
+ if (trimmed) result.push(trimmed);
2216
+ continue;
2217
+ }
2218
+ const record = asRecord3(entry);
2219
+ if (!record) continue;
2220
+ const assetPath = typeof record.path === "string" ? record.path.trim() : "";
2221
+ if (assetPath) result.push(assetPath);
2222
+ }
2223
+ return result;
2224
+ }
2225
+ function resolveOpenClawPath(rawPath, stateDir) {
2226
+ const trimmed = rawPath.trim();
2227
+ if (!trimmed) return "";
2228
+ if (trimmed === "~/.openclaw") return stateDir;
2229
+ if (trimmed.startsWith("~/.openclaw/")) {
2230
+ return path11.join(stateDir, trimmed.slice("~/.openclaw/".length));
2231
+ }
2232
+ return trimmed;
2233
+ }
2234
+ function resolveMediaAssetCandidate(rawPath, stateDir) {
2235
+ const mapped = resolveOpenClawPath(rawPath, stateDir);
2236
+ if (!mapped) return null;
2237
+ const mediaRoot = path11.resolve(path11.join(stateDir, "media"));
2238
+ const resolved = path11.resolve(mapped);
2239
+ if (!pathWithin(mediaRoot, resolved)) return null;
2240
+ return {
2241
+ sourcePath: rawPath,
2242
+ resolvedPath: resolved
2243
+ };
2244
+ }
2245
+ function dedupeAssetCandidates(candidates) {
2246
+ const seen = /* @__PURE__ */ new Set();
2247
+ const deduped = [];
2248
+ for (const candidate of candidates) {
2249
+ if (seen.has(candidate.resolvedPath)) continue;
2250
+ seen.add(candidate.resolvedPath);
2251
+ deduped.push(candidate);
2252
+ }
2253
+ return deduped;
2254
+ }
2255
+ async function waitForAvailableAssets(candidates) {
2256
+ if (candidates.length === 0) return [];
2257
+ const pending = new Map(
2258
+ candidates.map((candidate) => [candidate.resolvedPath, candidate])
2259
+ );
2260
+ const deadline = Date.now() + RETRY_BUDGET_MS;
2261
+ while (pending.size > 0) {
2262
+ for (const [resolvedPath, candidate] of pending.entries()) {
2263
+ try {
2264
+ const stat = await fs10.promises.stat(resolvedPath);
2265
+ if (stat.isFile()) {
2266
+ pending.delete(resolvedPath);
2267
+ }
2268
+ } catch {
2269
+ }
2270
+ }
2271
+ if (pending.size === 0) break;
2272
+ if (Date.now() >= deadline) break;
2273
+ await sleep2(RETRY_INTERVAL_MS);
2274
+ }
2275
+ return Array.from(pending.values());
2276
+ }
2277
+ function resolveImageMimeType(filePath) {
2278
+ const ext = path11.extname(filePath).toLowerCase();
2279
+ return IMAGE_MIME_BY_EXT[ext] ?? "application/octet-stream";
2280
+ }
2281
+ function extractGatewayToken(config) {
2282
+ const gateway = asRecord3(config.gateway);
2283
+ const auth = asRecord3(gateway?.auth);
2284
+ const remote = asRecord3(gateway?.remote);
2285
+ const fromAuth = typeof auth?.token === "string" ? auth.token.trim() : "";
2286
+ if (fromAuth) return fromAuth;
2287
+ const fromRemote = typeof remote?.token === "string" ? remote.token.trim() : "";
2288
+ if (fromRemote) return fromRemote;
2289
+ const fromGateway = typeof gateway?.token === "string" ? gateway.token.trim() : "";
2290
+ if (fromGateway) return fromGateway;
2291
+ return void 0;
2292
+ }
2293
+ function resolveGatewayUrlFromConfig(config) {
2294
+ const gateway = asRecord3(config.gateway);
2295
+ const host = typeof gateway?.host === "string" && gateway.host.trim() ? gateway.host.trim() : "127.0.0.1";
2296
+ const port = typeof gateway?.port === "number" && Number.isFinite(gateway.port) ? gateway.port : 18789;
2297
+ const tlsRaw = gateway?.tls;
2298
+ const tls = typeof tlsRaw === "boolean" ? tlsRaw : Boolean(asRecord3(tlsRaw)?.enabled);
2299
+ return `${tls ? "wss" : "ws"}://${host}:${port}`;
2300
+ }
2301
+ function extractCliErrorMessage(payload) {
2302
+ const payloadRecord = asRecord3(payload);
2303
+ if (typeof payloadRecord?.error === "string") return payloadRecord.error;
2304
+ const errorRecord = asRecord3(payloadRecord?.error);
2305
+ if (typeof errorRecord?.message === "string") return errorRecord.message;
2306
+ const nestedPayload = asRecord3(payloadRecord?.payload);
2307
+ if (typeof nestedPayload?.error === "string") return nestedPayload.error;
2308
+ return "";
2309
+ }
2310
+ function parseCliJson(raw) {
2311
+ const trimmed = raw.trim();
2312
+ if (!trimmed) return null;
2313
+ try {
2314
+ const parsed = JSON.parse(trimmed);
2315
+ return asRecord3(parsed);
2316
+ } catch {
2317
+ return null;
2318
+ }
2319
+ }
2320
+ function isGatewayCallUnavailableError(message) {
2321
+ return /gateway method invocation api unavailable in plugin context/i.test(message);
2322
+ }
2323
+ async function callAgentViaCli(stateDir, params) {
2324
+ const configPath = path11.join(stateDir, "openclaw.json");
2325
+ const configRaw = fs10.readFileSync(configPath, "utf-8");
2326
+ const parsedConfig = JSON.parse(configRaw);
2327
+ const token = extractGatewayToken(parsedConfig);
2328
+ const url = resolveGatewayUrlFromConfig(parsedConfig);
2329
+ const args = [
2330
+ "gateway",
2331
+ "call",
2332
+ "agent",
2333
+ "--json",
2334
+ "--timeout",
2335
+ String(CLI_AGENT_TIMEOUT_MS),
2336
+ "--params",
2337
+ JSON.stringify(params),
2338
+ "--url",
2339
+ url
2340
+ ];
2341
+ if (token) {
2342
+ args.push("--token", token);
2343
+ }
2344
+ try {
2345
+ const { stdout } = await execFileAsync("openclaw", args, {
2346
+ env: process.env,
2347
+ maxBuffer: 10 * 1024 * 1024
2348
+ });
2349
+ const json2 = parseCliJson(stdout);
2350
+ if (!json2) {
2351
+ throw new Error("openclaw gateway call agent returned non-JSON output");
2352
+ }
2353
+ if (json2.ok === false) {
2354
+ const msg = extractCliErrorMessage(json2) || JSON.stringify(json2.error ?? json2);
2355
+ throw new Error(msg);
2356
+ }
2357
+ if ("payload" in json2) return json2.payload;
2358
+ return json2;
2359
+ } catch (err2) {
2360
+ const error = err2;
2361
+ const stdoutJson = parseCliJson(typeof error.stdout === "string" ? error.stdout : "");
2362
+ if (stdoutJson) {
2363
+ if (stdoutJson.ok === false) {
2364
+ const msg = extractCliErrorMessage(stdoutJson) || JSON.stringify(stdoutJson.error ?? stdoutJson);
2365
+ throw new Error(msg);
2366
+ }
2367
+ if ("payload" in stdoutJson) return stdoutJson.payload;
2368
+ return stdoutJson;
2369
+ }
2370
+ const stderr = typeof error.stderr === "string" ? error.stderr.trim() : "";
2371
+ const message = stderr || (typeof error.message === "string" ? error.message : String(err2));
2372
+ throw new Error(message);
2373
+ }
2374
+ }
2375
+ async function buildImageAttachments(candidates) {
2376
+ const attachments = [];
2377
+ for (const candidate of candidates) {
2378
+ const buffer = await fs10.promises.readFile(candidate.resolvedPath);
2379
+ attachments.push({
2380
+ type: "image",
2381
+ mimeType: resolveImageMimeType(candidate.resolvedPath),
2382
+ fileName: path11.basename(candidate.resolvedPath),
2383
+ content: buffer.toString("base64")
2384
+ });
2385
+ }
2386
+ return attachments;
2387
+ }
2388
+ function registerAgentSendMethod(api) {
2389
+ api.registerGatewayMethod(
2390
+ "squad.agent.send",
2391
+ async (ctx) => {
2392
+ const { params, respond } = ctx;
2393
+ const message = typeof params?.message === "string" ? params.message : "";
2394
+ if (!message.trim()) {
2395
+ respond(false, { error: "Invalid 'message' parameter (must be a non-empty string)" });
2396
+ return;
2397
+ }
2398
+ const stateDir = getOpenclawStateDir();
2399
+ const rawAssetPaths = [
2400
+ ...parseAssetPathsFromMessage(message),
2401
+ ...parseAssetPathsFromContext(params)
2402
+ ];
2403
+ const candidates = dedupeAssetCandidates(
2404
+ rawAssetPaths.map((rawPath) => resolveMediaAssetCandidate(rawPath, stateDir)).filter((candidate) => candidate !== null)
2405
+ );
2406
+ if (rawAssetPaths.length > 0 && candidates.length === 0) {
2407
+ respond(false, {
2408
+ error: "Referenced image assets must be under ~/.openclaw/media/"
2409
+ });
2410
+ return;
2411
+ }
2412
+ if (candidates.length > 0) {
2413
+ const missing = await waitForAvailableAssets(candidates);
2414
+ if (missing.length > 0) {
2415
+ const missingPaths = missing.map((entry) => entry.sourcePath).join(", ");
2416
+ respond(false, {
2417
+ error: `Referenced image assets not available after ${RETRY_BUDGET_MS}ms: ${missingPaths}`
2418
+ });
2419
+ return;
2420
+ }
2421
+ }
2422
+ try {
2423
+ const imageAttachments = await buildImageAttachments(candidates);
2424
+ const existingAttachments = Array.isArray(params.attachments) ? params.attachments : [];
2425
+ const forwardedParams = {
2426
+ ...params,
2427
+ attachments: [...existingAttachments, ...imageAttachments]
2428
+ };
2429
+ let nativeResult;
2430
+ try {
2431
+ nativeResult = await callGatewayAny(
2432
+ ctx,
2433
+ api,
2434
+ "agent",
2435
+ forwardedParams
2436
+ );
2437
+ } catch (err2) {
2438
+ const msg = err2 instanceof Error ? err2.message : String(err2);
2439
+ if (!isGatewayCallUnavailableError(msg)) {
2440
+ throw err2;
2441
+ }
2442
+ nativeResult = await callAgentViaCli(stateDir, forwardedParams);
2443
+ }
2444
+ respond(true, nativeResult);
2445
+ } catch (err2) {
2446
+ const msg = err2 instanceof Error ? err2.message : String(err2);
2447
+ respond(false, { error: msg });
2448
+ }
2449
+ }
2450
+ );
2451
+ }
2452
+
2876
2453
  // src/shared-api.ts
2877
2454
  var CORE_TOOLS = [
2878
2455
  "exec",
@@ -2926,6 +2503,7 @@ function registerSquadSharedApi(api, onFsChange) {
2926
2503
  registerAgentMethods(api);
2927
2504
  registerQuestionMethods(api);
2928
2505
  registerSessionMethods(api);
2506
+ registerAgentSendMethod(api);
2929
2507
  const invokeTool = async (tool, args) => {
2930
2508
  const executeFn = toolExecutors.get(tool);
2931
2509
  if (!executeFn) {
@@ -2980,14 +2558,79 @@ function registerSquadSharedApi(api, onFsChange) {
2980
2558
  }
2981
2559
 
2982
2560
  // src/migrations/runner.ts
2983
- import fs11 from "fs";
2984
- import path12 from "path";
2561
+ import fs12 from "fs";
2562
+ import path13 from "path";
2985
2563
 
2986
2564
  // src/migrations/001-enable-main-subagent-access.ts
2565
+ import fs11 from "fs";
2566
+ import path12 from "path";
2567
+ function asRecord4(value) {
2568
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2569
+ return value;
2570
+ }
2571
+ function mergeStringArrayWithWildcard(value) {
2572
+ const next = /* @__PURE__ */ new Set();
2573
+ if (Array.isArray(value)) {
2574
+ for (const entry of value) {
2575
+ if (typeof entry === "string" && entry.trim()) next.add(entry.trim());
2576
+ }
2577
+ }
2578
+ next.add("*");
2579
+ return Array.from(next);
2580
+ }
2581
+ function patchConfigOnDisk() {
2582
+ const configPath = path12.join(getOpenclawStateDir(), "openclaw.json");
2583
+ const raw = fs11.readFileSync(configPath, "utf-8");
2584
+ const parsed = JSON.parse(raw);
2585
+ const agents = asRecord4(parsed.agents) ?? {};
2586
+ const defaults = asRecord4(agents.defaults) ?? {};
2587
+ const subagentsDefaults = asRecord4(defaults.subagents) ?? {};
2588
+ defaults.maxConcurrent = 4;
2589
+ defaults.subagents = {
2590
+ ...subagentsDefaults,
2591
+ maxConcurrent: 8
2592
+ };
2593
+ const listRaw = Array.isArray(agents.list) ? agents.list : [];
2594
+ const list = listRaw.map((entry) => asRecord4(entry)).filter((entry) => Boolean(entry));
2595
+ const mainIndex = list.findIndex((entry) => entry.id === "main");
2596
+ const existingMain = mainIndex >= 0 ? list[mainIndex] : {};
2597
+ const existingIdentity = asRecord4(existingMain.identity) ?? {};
2598
+ const existingTools = asRecord4(existingMain.tools) ?? {};
2599
+ const existingSubagents = asRecord4(existingMain.subagents) ?? {};
2600
+ const nextMain = {
2601
+ ...existingMain,
2602
+ id: "main",
2603
+ identity: {
2604
+ ...existingIdentity,
2605
+ name: typeof existingIdentity.name === "string" && existingIdentity.name.trim() ? existingIdentity.name : "Pepper"
2606
+ },
2607
+ tools: {
2608
+ ...existingTools,
2609
+ allow: mergeStringArrayWithWildcard(existingTools.allow)
2610
+ },
2611
+ subagents: {
2612
+ ...existingSubagents,
2613
+ allowAgents: mergeStringArrayWithWildcard(existingSubagents.allowAgents)
2614
+ }
2615
+ };
2616
+ if (mainIndex >= 0) list[mainIndex] = nextMain;
2617
+ else list.push(nextMain);
2618
+ parsed.agents = {
2619
+ ...agents,
2620
+ defaults,
2621
+ list
2622
+ };
2623
+ fs11.writeFileSync(configPath, `${JSON.stringify(parsed, null, 2)}
2624
+ `, "utf-8");
2625
+ }
2987
2626
  var migration = {
2988
2627
  id: "001-enable-main-subagent-access",
2989
2628
  description: "Enable full main-agent tool and subagent spawning defaults",
2990
2629
  run: async ({ gatewayCall }) => {
2630
+ const isGatewayApiUnavailableError = (error) => {
2631
+ const msg = error instanceof Error ? error.message : String(error);
2632
+ return /gateway method invocation api unavailable in plugin context/i.test(msg);
2633
+ };
2991
2634
  const doPatch = async (baseHash) => {
2992
2635
  await gatewayCall("config.patch", {
2993
2636
  ...baseHash ? { baseHash } : {},
@@ -3017,28 +2660,63 @@ var migration = {
3017
2660
  })
3018
2661
  });
3019
2662
  };
3020
- let snapshot = await gatewayCall("config.get", {});
2663
+ let snapshot;
2664
+ try {
2665
+ snapshot = await gatewayCall("config.get", {});
2666
+ } catch (error) {
2667
+ if (isGatewayApiUnavailableError(error)) {
2668
+ patchConfigOnDisk();
2669
+ return;
2670
+ }
2671
+ throw error;
2672
+ }
3021
2673
  try {
3022
2674
  await doPatch(snapshot?.hash);
3023
2675
  } catch (firstErr) {
3024
2676
  const msg = firstErr instanceof Error ? firstErr.message : String(firstErr);
2677
+ if (isGatewayApiUnavailableError(firstErr)) {
2678
+ patchConfigOnDisk();
2679
+ return;
2680
+ }
3025
2681
  if (!/config changed since last load/i.test(msg)) throw firstErr;
3026
- snapshot = await gatewayCall("config.get", {});
2682
+ try {
2683
+ snapshot = await gatewayCall("config.get", {});
2684
+ } catch (secondErr) {
2685
+ if (isGatewayApiUnavailableError(secondErr)) {
2686
+ patchConfigOnDisk();
2687
+ return;
2688
+ }
2689
+ throw secondErr;
2690
+ }
3027
2691
  await doPatch(snapshot?.hash);
3028
2692
  }
3029
2693
  }
3030
2694
  };
3031
2695
  var enable_main_subagent_access_default = migration;
3032
2696
 
2697
+ // src/migrations/002-backfill-agent-auth-profiles.ts
2698
+ var migration2 = {
2699
+ id: "002-backfill-agent-auth-profiles",
2700
+ description: "Ensure non-main agents have auth-profiles copied from main when missing",
2701
+ run: async () => {
2702
+ const copied = backfillAgentAuthProfiles();
2703
+ if (copied.length > 0) {
2704
+ console.log(`[startup-migrations] auth profile backfill copied: ${copied.join(", ")}`);
2705
+ }
2706
+ }
2707
+ };
2708
+ var backfill_agent_auth_profiles_default = migration2;
2709
+
3033
2710
  // src/migrations/index.ts
3034
2711
  var STARTUP_MIGRATIONS = [
3035
- enable_main_subagent_access_default
2712
+ enable_main_subagent_access_default,
2713
+ backfill_agent_auth_profiles_default
3036
2714
  // Append new startup migrations here.
3037
2715
  ];
3038
2716
 
3039
2717
  // src/migrations/runner.ts
3040
- var MIGRATIONS_DIR = path12.join(getOpenclawStateDir(), "squad-ceo-data");
3041
- var MIGRATIONS_PATH = path12.join(MIGRATIONS_DIR, "migrations.json");
2718
+ var MIGRATIONS_DIR = path13.join(getOpenclawStateDir(), "squad-ceo-data");
2719
+ var MIGRATIONS_PATH = path13.join(MIGRATIONS_DIR, "migrations.json");
3042
2720
  function defaultState() {
3043
2721
  return {
3044
2722
  version: 1,
@@ -3047,7 +2725,7 @@ function defaultState() {
3047
2725
  }
3048
2726
  function readState() {
3049
2727
  try {
3050
- const raw = fs11.readFileSync(MIGRATIONS_PATH, "utf-8");
2728
+ const raw = fs12.readFileSync(MIGRATIONS_PATH, "utf-8");
3051
2729
  const parsed = JSON.parse(raw);
3052
2730
  if (!Array.isArray(parsed.completed)) return defaultState();
3053
2731
  return {
@@ -3059,8 +2737,8 @@ function readState() {
3059
2737
  }
3060
2738
  }
3061
2739
  function writeState(state) {
3062
- fs11.mkdirSync(MIGRATIONS_DIR, { recursive: true });
3063
- fs11.writeFileSync(MIGRATIONS_PATH, JSON.stringify(state, null, 2), "utf-8");
2740
+ fs12.mkdirSync(MIGRATIONS_DIR, { recursive: true });
2741
+ fs12.writeFileSync(MIGRATIONS_PATH, JSON.stringify(state, null, 2), "utf-8");
3064
2742
  }
3065
2743
  function makeGatewayCaller(api) {
3066
2744
  return async (method, params = {}) => {
@@ -3075,11 +2753,11 @@ function makeGatewayCaller(api) {
3075
2753
  throw new Error("Gateway method invocation API unavailable in plugin context");
3076
2754
  };
3077
2755
  }
3078
- async function runMigration(state, migration2, gatewayCall) {
3079
- if (state.completed.some((row) => row.id === migration2.id)) {
2756
+ async function runMigration(state, migration3, gatewayCall) {
2757
+ if (state.completed.some((row) => row.id === migration3.id)) {
3080
2758
  return { state, applied: false };
3081
2759
  }
3082
- await migration2.run({ gatewayCall });
2760
+ await migration3.run({ gatewayCall });
3083
2761
  return {
3084
2762
  applied: true,
3085
2763
  state: {
@@ -3087,7 +2765,7 @@ async function runMigration(state, migration2, gatewayCall) {
3087
2765
  completed: [
3088
2766
  ...state.completed,
3089
2767
  {
3090
- id: migration2.id,
2768
+ id: migration3.id,
3091
2769
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
3092
2770
  }
3093
2771
  ]
@@ -3097,34 +2775,707 @@ async function runMigration(state, migration2, gatewayCall) {
3097
2775
  async function runStartupMigrations(api) {
3098
2776
  const gatewayCall = makeGatewayCaller(api);
3099
2777
  let state = readState();
3100
- for (const migration2 of STARTUP_MIGRATIONS) {
2778
+ for (const migration3 of STARTUP_MIGRATIONS) {
3101
2779
  try {
3102
- const result = await runMigration(state, migration2, gatewayCall);
2780
+ const result = await runMigration(state, migration3, gatewayCall);
3103
2781
  state = result.state;
3104
2782
  if (result.applied) {
3105
2783
  writeState(state);
3106
- console.log(`[startup-migrations] applied: ${migration2.id}`);
2784
+ console.log(`[startup-migrations] applied: ${migration3.id}`);
3107
2785
  }
3108
2786
  } catch (err2) {
3109
2787
  const msg = err2 instanceof Error ? err2.message : String(err2);
3110
- console.warn(`[startup-migrations] failed: ${migration2.id}: ${msg}`);
2788
+ console.warn(`[startup-migrations] failed: ${migration3.id}: ${msg}`);
3111
2789
  break;
3112
2790
  }
3113
2791
  }
3114
2792
  }
3115
2793
 
2794
+ // src/http-routes.ts
2795
+ import crypto2 from "crypto";
2796
+ var DEFAULT_ALLOWED_ORIGINS = [
2797
+ "https://squad.ceo",
2798
+ "https://www.squad.ceo",
2799
+ "https://squad.pages.dev",
2800
+ "http://localhost:5174",
2801
+ "http://localhost:5800"
2802
+ ];
2803
+ var PAIRING_REQUEST_METHODS = [
2804
+ "node.pair.request",
2805
+ "devices.pair.request",
2806
+ "device.pair.request"
2807
+ ];
2808
+ var PAIRING_STATUS_METHODS = [
2809
+ "node.pair.status",
2810
+ "devices.pair.status",
2811
+ "device.pair.status",
2812
+ "node.pair.get"
2813
+ ];
2814
+ var PROOF_MAX_SKEW_MS = 5 * 60 * 1e3;
2815
+ var DEFAULT_PAIRING_TTL_MS = 15 * 60 * 1e3;
2816
+ var NONCE_TTL_MS = 10 * 60 * 1e3;
2817
+ var LOCATOR_REFRESH_INTERVAL_MS = 15 * 60 * 1e3;
2818
+ var ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
2819
+ function asRecord5(value) {
2820
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2821
+ return value;
2822
+ }
2823
+ function pickString(value) {
2824
+ if (typeof value !== "string") return null;
2825
+ const trimmed = value.trim();
2826
+ return trimmed.length > 0 ? trimmed : null;
2827
+ }
2828
+ function pickNumber(value) {
2829
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
2830
+ return value;
2831
+ }
2832
+ function json(body, status = 200) {
2833
+ return new Response(JSON.stringify(body), {
2834
+ status,
2835
+ headers: {
2836
+ "Content-Type": "application/json",
2837
+ "Cache-Control": "no-store"
2838
+ }
2839
+ });
2840
+ }
2841
+ function withCors(request, response, allowMethods = "GET, POST, OPTIONS") {
2842
+ const headers = new Headers(response.headers);
2843
+ const origin = request.headers.get("origin");
2844
+ const allowedOrigin = resolveAllowedOrigin(origin);
2845
+ if (allowedOrigin) {
2846
+ headers.set("Access-Control-Allow-Origin", allowedOrigin);
2847
+ headers.set("Access-Control-Allow-Methods", allowMethods);
2848
+ headers.set("Access-Control-Allow-Headers", "Content-Type");
2849
+ headers.set("Vary", "Origin");
2850
+ }
2851
+ return new Response(response.body, {
2852
+ status: response.status,
2853
+ statusText: response.statusText,
2854
+ headers
2855
+ });
2856
+ }
2857
+ function jsonError(request, code, message, status = 500, extra = {}) {
2858
+ return withCors(request, json({ ok: false, code, error: message, ...extra }, status));
2859
+ }
2860
+ function resolveAllowedOrigin(origin) {
2861
+ if (!origin) return null;
2862
+ const configured = process.env.SQUAD_ALLOWED_ORIGINS ? process.env.SQUAD_ALLOWED_ORIGINS.split(",").map((item) => item.trim()).filter(Boolean) : [];
2863
+ const allowed = configured.length > 0 ? configured : DEFAULT_ALLOWED_ORIGINS;
2864
+ return allowed.includes(origin) ? origin : null;
2865
+ }
2866
+ function isTailnetContext(request) {
2867
+ if (/^(1|true)$/i.test(process.env.SQUAD_ALLOW_NON_TAILNET_INTERNAL ?? "")) {
2868
+ return true;
2869
+ }
2870
+ const hasTailnetHeader = !!request.headers.get("x-tailscale-user-login") || !!request.headers.get("x-tailscale-user-name") || !!request.headers.get("x-tailscale-user-email") || !!request.headers.get("x-tailscale-node-name") || !!request.headers.get("x-tailscale-tailnet");
2871
+ if (hasTailnetHeader) return true;
2872
+ try {
2873
+ const url = new URL(request.url);
2874
+ if (url.hostname.toLowerCase().endsWith(".ts.net")) return true;
2875
+ } catch {
2876
+ }
2877
+ const forwardedHost = request.headers.get("x-forwarded-host")?.toLowerCase() ?? "";
2878
+ return forwardedHost.endsWith(".ts.net");
2879
+ }
2880
+ function ensureOriginAllowed(request) {
2881
+ const origin = request.headers.get("origin");
2882
+ if (!origin) return null;
2883
+ return resolveAllowedOrigin(origin);
2884
+ }
2885
+ function decodeBase64Url(value) {
2886
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
2887
+ const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
2888
+ return Uint8Array.from(Buffer.from(padded, "base64"));
2889
+ }
2890
+ function base64UrlEncode(bytes) {
2891
+ return Buffer.from(bytes).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
2892
+ }
2893
+ function encodeUtf8(value) {
2894
+ return new TextEncoder().encode(value);
2895
+ }
2896
+ function normalizeDevicePublicKeyBase64Url(publicKey) {
2897
+ try {
2898
+ if (publicKey.includes("BEGIN")) {
2899
+ const spki = crypto2.createPublicKey(publicKey).export({
2900
+ type: "spki",
2901
+ format: "der"
2902
+ });
2903
+ if (spki.length === ED25519_SPKI_PREFIX.length + 32 && spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
2904
+ return base64UrlEncode(spki.subarray(ED25519_SPKI_PREFIX.length));
2905
+ }
2906
+ return null;
2907
+ }
2908
+ return base64UrlEncode(decodeBase64Url(publicKey));
2909
+ } catch {
2910
+ return null;
2911
+ }
2912
+ }
2913
+ function deriveDeviceIdFromPublicKey(publicKey) {
2914
+ try {
2915
+ const normalized = normalizeDevicePublicKeyBase64Url(publicKey);
2916
+ if (!normalized) return null;
2917
+ const raw = decodeBase64Url(normalized);
2918
+ if (raw.length !== 32) return null;
2919
+ return crypto2.createHash("sha256").update(raw).digest("hex");
2920
+ } catch {
2921
+ return null;
2922
+ }
2923
+ }
2924
+ function verifyEd25519Signature(publicKey, payload, signatureBase64Url) {
2925
+ try {
2926
+ const normalized = normalizeDevicePublicKeyBase64Url(publicKey);
2927
+ if (!normalized) return false;
2928
+ const key = crypto2.createPublicKey({
2929
+ key: Buffer.concat([ED25519_SPKI_PREFIX, decodeBase64Url(normalized)]),
2930
+ type: "spki",
2931
+ format: "der"
2932
+ });
2933
+ const signature = (() => {
2934
+ try {
2935
+ return Buffer.from(decodeBase64Url(signatureBase64Url));
2936
+ } catch {
2937
+ return Buffer.from(signatureBase64Url, "base64");
2938
+ }
2939
+ })();
2940
+ return crypto2.verify(null, Buffer.from(payload, "utf8"), key, signature);
2941
+ } catch {
2942
+ return false;
2943
+ }
2944
+ }
2945
+ function canonicalizeP256Jwk(value) {
2946
+ const record = asRecord5(value);
2947
+ const kty = pickString(record?.kty);
2948
+ const crv = pickString(record?.crv);
2949
+ const x = pickString(record?.x);
2950
+ const y = pickString(record?.y);
2951
+ if (kty !== "EC" || crv !== "P-256" || !x || !y) {
2952
+ throw new Error("INVALID_PROOF_KEY");
2953
+ }
2954
+ return { kty: "EC", crv: "P-256", x, y };
2955
+ }
2956
+ function computeDeviceIdFromJwk(jwk) {
2957
+ const canonical = JSON.stringify(jwk);
2958
+ return crypto2.createHash("sha256").update(canonical).digest("hex");
2959
+ }
2960
+ function buildProofPayload(action, deviceId, nonce, signedAt, origin) {
2961
+ return `squad.${action}|${deviceId}|${nonce}|${signedAt}|${origin}`;
2962
+ }
2963
+ async function verifyBrowserProof(payload, origin, action, usedProofNonces) {
2964
+ const deviceId = pickString(payload.deviceId);
2965
+ const signature = pickString(payload.signature);
2966
+ const nonce = pickString(payload.nonce);
2967
+ const signedAtRaw = pickNumber(payload.signedAt);
2968
+ if (!deviceId || !signature || !nonce || signedAtRaw == null) {
2969
+ return {
2970
+ ok: false,
2971
+ code: "INVALID_PROOF",
2972
+ status: 400,
2973
+ message: "Missing browser proof fields"
2974
+ };
2975
+ }
2976
+ const signedAt = Math.trunc(signedAtRaw);
2977
+ if (Math.abs(Date.now() - signedAt) > PROOF_MAX_SKEW_MS) {
2978
+ return {
2979
+ ok: false,
2980
+ code: "INVALID_PROOF",
2981
+ status: 401,
2982
+ message: "Proof timestamp expired"
2983
+ };
2984
+ }
2985
+ const nonceKey = `${deviceId}:${nonce}`;
2986
+ const existingNonce = usedProofNonces.get(nonceKey);
2987
+ if (existingNonce && existingNonce > Date.now()) {
2988
+ return {
2989
+ ok: false,
2990
+ code: "INVALID_PROOF",
2991
+ status: 401,
2992
+ message: "Proof nonce replay detected"
2993
+ };
2994
+ }
2995
+ const proofPayload = buildProofPayload(action, deviceId, nonce, signedAt, origin);
2996
+ const publicKey = pickString(payload.publicKey);
2997
+ let verified = false;
2998
+ if (publicKey) {
2999
+ const expectedDeviceId = deriveDeviceIdFromPublicKey(publicKey);
3000
+ if (!expectedDeviceId) {
3001
+ return {
3002
+ ok: false,
3003
+ code: "INVALID_PROOF",
3004
+ status: 400,
3005
+ message: "Invalid browser public key"
3006
+ };
3007
+ }
3008
+ if (expectedDeviceId !== deviceId) {
3009
+ return {
3010
+ ok: false,
3011
+ code: "INVALID_PROOF",
3012
+ status: 401,
3013
+ message: "Proof deviceId does not match browser key"
3014
+ };
3015
+ }
3016
+ verified = verifyEd25519Signature(publicKey, proofPayload, signature);
3017
+ } else {
3018
+ let jwk;
3019
+ try {
3020
+ jwk = canonicalizeP256Jwk(payload.publicKeyJwk);
3021
+ } catch {
3022
+ return {
3023
+ ok: false,
3024
+ code: "INVALID_PROOF",
3025
+ status: 400,
3026
+ message: "Invalid browser public key"
3027
+ };
3028
+ }
3029
+ const expectedDeviceId = computeDeviceIdFromJwk(jwk);
3030
+ if (expectedDeviceId !== deviceId) {
3031
+ return {
3032
+ ok: false,
3033
+ code: "INVALID_PROOF",
3034
+ status: 401,
3035
+ message: "Proof deviceId does not match browser key"
3036
+ };
3037
+ }
3038
+ try {
3039
+ const key = await crypto2.webcrypto.subtle.importKey(
3040
+ "jwk",
3041
+ jwk,
3042
+ { name: "ECDSA", namedCurve: "P-256" },
3043
+ false,
3044
+ ["verify"]
3045
+ );
3046
+ verified = await crypto2.webcrypto.subtle.verify(
3047
+ { name: "ECDSA", hash: "SHA-256" },
3048
+ key,
3049
+ decodeBase64Url(signature),
3050
+ encodeUtf8(proofPayload)
3051
+ );
3052
+ } catch {
3053
+ verified = false;
3054
+ }
3055
+ }
3056
+ if (!verified) {
3057
+ return {
3058
+ ok: false,
3059
+ code: "INVALID_PROOF",
3060
+ status: 401,
3061
+ message: "Proof signature verification failed"
3062
+ };
3063
+ }
3064
+ usedProofNonces.set(nonceKey, Date.now() + NONCE_TTL_MS);
3065
+ return { ok: true, deviceId };
3066
+ }
3067
+ function normalizePairingStatus(value) {
3068
+ if (typeof value !== "string") return "unknown";
3069
+ const normalized = value.trim().toLowerCase();
3070
+ if (["pending", "requested", "waiting", "open"].includes(normalized)) return "pending";
3071
+ if (["approved", "paired", "accepted", "granted", "success"].includes(normalized)) return "approved";
3072
+ if (["rejected", "denied", "failed"].includes(normalized)) return "rejected";
3073
+ if (["expired", "timeout", "timed_out"].includes(normalized)) return "expired";
3074
+ return "unknown";
3075
+ }
3076
+ function extractRequestId(result) {
3077
+ const obj = asRecord5(result);
3078
+ if (!obj) return null;
3079
+ const nestedRequest = asRecord5(obj.request);
3080
+ return pickString(obj.requestId) ?? pickString(obj.id) ?? pickString(nestedRequest?.requestId) ?? pickString(nestedRequest?.id);
3081
+ }
3082
+ function extractExpiresAt(result) {
3083
+ const obj = asRecord5(result);
3084
+ const candidates = [
3085
+ obj?.expiresAt,
3086
+ obj?.expiresAtMs,
3087
+ asRecord5(obj?.request)?.expiresAt,
3088
+ asRecord5(obj?.request)?.expiresAtMs
3089
+ ];
3090
+ for (const candidate of candidates) {
3091
+ if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > Date.now()) {
3092
+ return Math.trunc(candidate);
3093
+ }
3094
+ if (typeof candidate === "string") {
3095
+ const asNumber = Number(candidate);
3096
+ if (Number.isFinite(asNumber) && asNumber > Date.now()) {
3097
+ return Math.trunc(asNumber);
3098
+ }
3099
+ const parsedDate = Date.parse(candidate);
3100
+ if (Number.isFinite(parsedDate) && parsedDate > Date.now()) {
3101
+ return Math.trunc(parsedDate);
3102
+ }
3103
+ }
3104
+ }
3105
+ return Date.now() + DEFAULT_PAIRING_TTL_MS;
3106
+ }
3107
+ function extractPairingStatus(result) {
3108
+ const obj = asRecord5(result);
3109
+ if (!obj) return "unknown";
3110
+ const candidates = [
3111
+ obj.status,
3112
+ asRecord5(obj.request)?.status,
3113
+ asRecord5(obj.pairing)?.status
3114
+ ];
3115
+ for (const candidate of candidates) {
3116
+ const parsed = normalizePairingStatus(candidate);
3117
+ if (parsed !== "unknown") return parsed;
3118
+ }
3119
+ return "unknown";
3120
+ }
3121
+ function isRateLimited(bucket, key, limit, windowMs) {
3122
+ const now = Date.now();
3123
+ const existing = bucket.get(key);
3124
+ if (!existing || existing.resetAt <= now) {
3125
+ bucket.set(key, { count: 1, resetAt: now + windowMs });
3126
+ return false;
3127
+ }
3128
+ if (existing.count >= limit) {
3129
+ return true;
3130
+ }
3131
+ existing.count += 1;
3132
+ return false;
3133
+ }
3134
+ function normalizeTailnetHostname(value) {
3135
+ const trimmed = value.trim();
3136
+ if (!trimmed) return null;
3137
+ const withProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
3138
+ try {
3139
+ const parsed = new URL(withProtocol);
3140
+ const hostname = parsed.hostname.toLowerCase();
3141
+ if (!hostname.endsWith(".ts.net")) return null;
3142
+ return hostname.endsWith(".") ? hostname.slice(0, -1) : hostname;
3143
+ } catch {
3144
+ return null;
3145
+ }
3146
+ }
3147
+ function readRegistrationConfig() {
3148
+ const token = pickString(process.env.SQUAD_TAILNET_REGISTRATION_TOKEN) ?? pickString(process.env.OPENCLAW_SQUAD_TAILNET_REGISTRATION_TOKEN);
3149
+ const hostname = pickString(process.env.SQUAD_TAILNET_HOSTNAME) ?? pickString(process.env.TS_HOSTNAME) ?? pickString(process.env.TAILSCALE_HOSTNAME);
3150
+ const normalizedHostname = hostname ? normalizeTailnetHostname(hostname) : null;
3151
+ if (!token || !normalizedHostname) return null;
3152
+ const relayUrlRaw = pickString(process.env.OPENCLAW_SQUAD_RELAY_HTTP_URL) ?? pickString(process.env.SQUAD_RELAY_HTTP_URL) ?? pickString(process.env.OPENCLAW_SQUAD_RELAY_URL) ?? pickString(process.env.SQUAD_RELAY_URL) ?? "https://relay.squad.ceo";
3153
+ const relayApiBaseUrl = relayUrlRaw.replace(/^wss:/i, "https:").replace(/^ws:/i, "http:").replace(/\/+$/, "");
3154
+ const version = pickString(process.env.SQUAD_PLUGIN_VERSION) ?? pickString(process.env.npm_package_version) ?? "unknown";
3155
+ const displayName = pickString(process.env.SQUAD_TAILNET_DISPLAY_NAME) ?? "squad-openclaw";
3156
+ return {
3157
+ relayApiBaseUrl,
3158
+ registrationToken: token,
3159
+ hostname: normalizedHostname,
3160
+ displayName,
3161
+ version
3162
+ };
3163
+ }
3164
+ async function registerLocatorToRelay(config) {
3165
+ const response = await fetch(`${config.relayApiBaseUrl}/api/gateway/register-tailnet`, {
3166
+ method: "POST",
3167
+ headers: {
3168
+ "Content-Type": "application/json",
3169
+ Authorization: `Bearer ${config.registrationToken}`
3170
+ },
3171
+ body: JSON.stringify({
3172
+ hostname: config.hostname,
3173
+ displayName: config.displayName,
3174
+ version: config.version
3175
+ })
3176
+ });
3177
+ if (!response.ok) {
3178
+ const text = await response.text();
3179
+ throw new Error(`Locator registration failed (${response.status}): ${text}`);
3180
+ }
3181
+ }
3182
+ function registerTailnetInternalRoutes(api) {
3183
+ const pendingPairings = /* @__PURE__ */ new Map();
3184
+ const proofNonces = /* @__PURE__ */ new Map();
3185
+ const rateLimitBucket = /* @__PURE__ */ new Map();
3186
+ let preferredPairingRequestMethod = null;
3187
+ let preferredPairingStatusMethod = null;
3188
+ const cleanupCaches = () => {
3189
+ const now = Date.now();
3190
+ for (const [key, value] of pendingPairings) {
3191
+ if (value.expiresAt <= now) pendingPairings.delete(key);
3192
+ }
3193
+ for (const [key, expiresAt] of proofNonces) {
3194
+ if (expiresAt <= now) proofNonces.delete(key);
3195
+ }
3196
+ for (const [key, bucket] of rateLimitBucket) {
3197
+ if (bucket.resetAt <= now) rateLimitBucket.delete(key);
3198
+ }
3199
+ };
3200
+ const callGatewayMethod = async (method, params) => {
3201
+ return callGatewayAny({}, api, method, params);
3202
+ };
3203
+ const requestPairingFromGateway = async () => {
3204
+ const methods = preferredPairingRequestMethod ? [preferredPairingRequestMethod, ...PAIRING_REQUEST_METHODS.filter((m) => m !== preferredPairingRequestMethod)] : [...PAIRING_REQUEST_METHODS];
3205
+ let lastError = null;
3206
+ for (const method of methods) {
3207
+ try {
3208
+ const result = await callGatewayMethod(method, { displayName: "squad-browser" });
3209
+ const requestId = extractRequestId(result);
3210
+ if (!requestId) {
3211
+ throw new Error("Pairing request created but no requestId was returned");
3212
+ }
3213
+ preferredPairingRequestMethod = method;
3214
+ return {
3215
+ requestId,
3216
+ expiresAt: extractExpiresAt(result)
3217
+ };
3218
+ } catch (error) {
3219
+ lastError = error;
3220
+ const message = error instanceof Error ? error.message : String(error);
3221
+ if (isUnknownGatewayMethodError(message)) {
3222
+ continue;
3223
+ }
3224
+ }
3225
+ }
3226
+ const details = lastError instanceof Error ? lastError.message : String(lastError ?? "");
3227
+ throw new Error(
3228
+ details ? `PAIRING_REQUEST_UNAVAILABLE: ${details}` : "PAIRING_REQUEST_UNAVAILABLE: No pairing request method supported"
3229
+ );
3230
+ };
3231
+ const readPairingStatusFromGateway = async (requestId) => {
3232
+ const methods = preferredPairingStatusMethod ? [preferredPairingStatusMethod, ...PAIRING_STATUS_METHODS.filter((m) => m !== preferredPairingStatusMethod)] : [...PAIRING_STATUS_METHODS];
3233
+ for (const method of methods) {
3234
+ try {
3235
+ const result = await callGatewayMethod(method, { requestId });
3236
+ const status = extractPairingStatus(result);
3237
+ preferredPairingStatusMethod = method;
3238
+ return status;
3239
+ } catch (error) {
3240
+ const message = error instanceof Error ? error.message : String(error);
3241
+ if (isUnknownGatewayMethodError(message)) continue;
3242
+ }
3243
+ }
3244
+ return "unknown";
3245
+ };
3246
+ const handle = async (request) => {
3247
+ const url = new URL(request.url);
3248
+ const path14 = url.pathname;
3249
+ cleanupCaches();
3250
+ if (request.method === "OPTIONS" && path14.startsWith("/squad-internal/")) {
3251
+ const origin = ensureOriginAllowed(request);
3252
+ if (!origin) {
3253
+ return new Response(null, { status: 403 });
3254
+ }
3255
+ return withCors(
3256
+ request,
3257
+ new Response(null, {
3258
+ status: 204,
3259
+ headers: {
3260
+ "Access-Control-Allow-Origin": origin,
3261
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
3262
+ "Access-Control-Allow-Headers": "Content-Type",
3263
+ "Access-Control-Max-Age": "86400"
3264
+ }
3265
+ })
3266
+ );
3267
+ }
3268
+ if (request.method === "GET" && path14 === "/squad-internal/health") {
3269
+ if (!isTailnetContext(request)) {
3270
+ return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
3271
+ }
3272
+ return withCors(
3273
+ request,
3274
+ json({
3275
+ ok: true,
3276
+ mode: "tailnet-direct",
3277
+ pairing: {
3278
+ requestSupported: preferredPairingRequestMethod ?? PAIRING_REQUEST_METHODS[0],
3279
+ statusSupported: preferredPairingStatusMethod ?? "auto"
3280
+ }
3281
+ })
3282
+ );
3283
+ }
3284
+ if (request.method === "POST" && path14 === "/squad-internal/pairing/request") {
3285
+ const origin = ensureOriginAllowed(request);
3286
+ if (!origin) {
3287
+ return jsonError(request, "ORIGIN_NOT_ALLOWED", "Origin is not allowed", 403);
3288
+ }
3289
+ if (!isTailnetContext(request)) {
3290
+ return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
3291
+ }
3292
+ let body;
3293
+ try {
3294
+ body = await request.json();
3295
+ } catch {
3296
+ return jsonError(request, "INVALID_REQUEST", "Invalid JSON body", 400);
3297
+ }
3298
+ const proofCheck = await verifyBrowserProof(body, origin, "pairing.request", proofNonces);
3299
+ if (!proofCheck.ok) {
3300
+ return jsonError(request, proofCheck.code, proofCheck.message, proofCheck.status);
3301
+ }
3302
+ const forwardedFor = request.headers.get("x-forwarded-for") ?? "unknown";
3303
+ const ipKey = `ip:${forwardedFor}`;
3304
+ const deviceKey = `device:${proofCheck.deviceId}`;
3305
+ if (isRateLimited(rateLimitBucket, ipKey, 20, 6e4) || isRateLimited(rateLimitBucket, deviceKey, 8, 6e4)) {
3306
+ return jsonError(request, "RATE_LIMITED", "Too many pairing requests", 429);
3307
+ }
3308
+ try {
3309
+ const pairing = await requestPairingFromGateway();
3310
+ pendingPairings.set(pairing.requestId, {
3311
+ requestId: pairing.requestId,
3312
+ deviceId: proofCheck.deviceId,
3313
+ createdAt: Date.now(),
3314
+ expiresAt: pairing.expiresAt
3315
+ });
3316
+ return withCors(
3317
+ request,
3318
+ json({
3319
+ ok: true,
3320
+ requestId: pairing.requestId,
3321
+ approveCommand: `openclaw devices approve ${pairing.requestId}`,
3322
+ expiresAt: pairing.expiresAt
3323
+ })
3324
+ );
3325
+ } catch (error) {
3326
+ const message = error instanceof Error ? error.message : String(error);
3327
+ return jsonError(
3328
+ request,
3329
+ "PAIRING_REQUEST_UNAVAILABLE",
3330
+ message.replace(/^PAIRING_REQUEST_UNAVAILABLE:\s*/i, ""),
3331
+ 501,
3332
+ { nextStep: "openclaw devices list --json" }
3333
+ );
3334
+ }
3335
+ }
3336
+ if (request.method === "GET" && path14 === "/squad-internal/pairing/status") {
3337
+ const origin = ensureOriginAllowed(request);
3338
+ if (!origin) {
3339
+ return jsonError(request, "ORIGIN_NOT_ALLOWED", "Origin is not allowed", 403);
3340
+ }
3341
+ if (!isTailnetContext(request)) {
3342
+ return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
3343
+ }
3344
+ const requestId = pickString(url.searchParams.get("requestId"));
3345
+ const deviceId = pickString(url.searchParams.get("deviceId"));
3346
+ if (!requestId || !deviceId) {
3347
+ return jsonError(request, "INVALID_REQUEST", "requestId and deviceId are required", 400);
3348
+ }
3349
+ const record = pendingPairings.get(requestId);
3350
+ if (!record || record.deviceId !== deviceId) {
3351
+ return jsonError(request, "NOT_FOUND", "No pending pairing request for this device", 404);
3352
+ }
3353
+ if (record.expiresAt <= Date.now()) {
3354
+ pendingPairings.delete(requestId);
3355
+ return withCors(request, json({ ok: true, status: "expired" }));
3356
+ }
3357
+ const forwardedFor = request.headers.get("x-forwarded-for") ?? "unknown";
3358
+ if (isRateLimited(rateLimitBucket, `status:${forwardedFor}`, 90, 6e4)) {
3359
+ return jsonError(request, "RATE_LIMITED", "Too many status checks", 429);
3360
+ }
3361
+ const status = await readPairingStatusFromGateway(requestId);
3362
+ if (status === "approved" || status === "expired" || status === "rejected") {
3363
+ pendingPairings.delete(requestId);
3364
+ }
3365
+ return withCors(request, json({ ok: true, status }));
3366
+ }
3367
+ if (request.method === "POST" && path14 === "/squad-internal/locator/register") {
3368
+ const origin = ensureOriginAllowed(request);
3369
+ if (!origin) {
3370
+ return jsonError(request, "ORIGIN_NOT_ALLOWED", "Origin is not allowed", 403);
3371
+ }
3372
+ if (!isTailnetContext(request)) {
3373
+ return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
3374
+ }
3375
+ let body;
3376
+ try {
3377
+ body = await request.json();
3378
+ } catch {
3379
+ return jsonError(request, "INVALID_REQUEST", "Invalid JSON body", 400);
3380
+ }
3381
+ const proofCheck = await verifyBrowserProof(body, origin, "locator.register", proofNonces);
3382
+ if (!proofCheck.ok) {
3383
+ return jsonError(request, proofCheck.code, proofCheck.message, proofCheck.status);
3384
+ }
3385
+ const registrationToken = pickString(body.registrationToken);
3386
+ const hostname = normalizeTailnetHostname(pickString(body.hostname) ?? "");
3387
+ if (!registrationToken || !hostname) {
3388
+ return jsonError(
3389
+ request,
3390
+ "INVALID_REQUEST",
3391
+ "registrationToken and hostname (*.ts.net) are required",
3392
+ 400
3393
+ );
3394
+ }
3395
+ const relayApiBaseUrl = pickString(body.relayApiBaseUrl) ?? readRegistrationConfig()?.relayApiBaseUrl ?? "https://relay.squad.ceo";
3396
+ const displayName = pickString(body.displayName) ?? "squad-openclaw";
3397
+ const version = pickString(body.version) ?? pickString(process.env.SQUAD_PLUGIN_VERSION) ?? pickString(process.env.npm_package_version) ?? "unknown";
3398
+ const config = {
3399
+ relayApiBaseUrl: relayApiBaseUrl.replace(/\/+$/, ""),
3400
+ registrationToken,
3401
+ hostname,
3402
+ displayName,
3403
+ version
3404
+ };
3405
+ try {
3406
+ await registerLocatorToRelay(config);
3407
+ return withCors(
3408
+ request,
3409
+ json({
3410
+ ok: true,
3411
+ locator: {
3412
+ hostname,
3413
+ source: "plugin",
3414
+ lastSeenAt: Date.now(),
3415
+ displayName,
3416
+ version
3417
+ }
3418
+ })
3419
+ );
3420
+ } catch (error) {
3421
+ return jsonError(
3422
+ request,
3423
+ "LOCATOR_REGISTRATION_FAILED",
3424
+ error instanceof Error ? error.message : String(error),
3425
+ 502
3426
+ );
3427
+ }
3428
+ }
3429
+ return new Response("Not Found", { status: 404 });
3430
+ };
3431
+ if (typeof api.registerHttpHandler === "function") {
3432
+ api.registerHttpHandler(handle);
3433
+ } else if (typeof api.registerHttpRoute === "function") {
3434
+ api.registerHttpRoute("/squad-internal/health", handle);
3435
+ api.registerHttpRoute("/squad-internal/pairing/request", handle);
3436
+ api.registerHttpRoute("/squad-internal/pairing/status", handle);
3437
+ api.registerHttpRoute("/squad-internal/locator/register", handle);
3438
+ api.registerHttpRoute("/squad-internal/*", handle);
3439
+ } else if (typeof api.registerHttpMiddleware === "function") {
3440
+ api.registerHttpMiddleware(handle);
3441
+ } else {
3442
+ console.warn("[squad-openclaw] no supported HTTP registration API found for tailnet routes");
3443
+ }
3444
+ const registrationConfig = readRegistrationConfig();
3445
+ if (!registrationConfig) return;
3446
+ const runRegistration = async () => {
3447
+ try {
3448
+ await registerLocatorToRelay(registrationConfig);
3449
+ console.log("[squad-openclaw] tailnet locator registered");
3450
+ } catch (error) {
3451
+ console.warn(
3452
+ "[squad-openclaw] tailnet locator registration failed:",
3453
+ error instanceof Error ? error.message : String(error)
3454
+ );
3455
+ }
3456
+ };
3457
+ void runRegistration();
3458
+ const timer = setInterval(() => {
3459
+ void runRegistration();
3460
+ }, LOCATOR_REFRESH_INTERVAL_MS);
3461
+ const teardown = () => {
3462
+ clearInterval(timer);
3463
+ };
3464
+ if (typeof api.onShutdown === "function") {
3465
+ api.onShutdown(teardown);
3466
+ } else if (typeof api.on === "function") {
3467
+ const maybeOff = api.on("shutdown", teardown);
3468
+ if (typeof maybeOff === "function") {
3469
+ }
3470
+ }
3471
+ }
3472
+
3116
3473
  // src/index.ts
3117
3474
  function squadAppPlugin(api) {
3118
- const onFsChange = (evt) => broadcastToUsers("fs.change", evt);
3119
- const sharedApi = registerSquadSharedApi(api, onFsChange);
3475
+ const sharedApi = registerSquadSharedApi(api);
3120
3476
  sharedApi.registerCoreGatewayMethods();
3477
+ registerTailnetInternalRoutes(api);
3121
3478
  void runStartupMigrations(api);
3122
- const relayState = readRelayState();
3123
- const relayEnabled = !!(relayState.claimToken || relayState.roomId);
3124
- if (relayEnabled) {
3125
- const relayUrl = process.env.OPENCLAW_SQUAD_RELAY_URL || process.env.SQUAD_RELAY_URL || "wss://relay.squad.ceo";
3126
- startRelayClient(api, relayUrl);
3127
- }
3128
3479
  }
3129
3480
  export {
3130
3481
  squadAppPlugin as default