agent-relay 3.1.6 → 3.1.8

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 (50) hide show
  1. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  2. package/bin/agent-relay-broker-darwin-x64 +0 -0
  3. package/bin/agent-relay-broker-linux-arm64 +0 -0
  4. package/bin/agent-relay-broker-linux-x64 +0 -0
  5. package/dist/index.cjs +130 -107
  6. package/package.json +8 -8
  7. package/packages/acp-bridge/package.json +2 -2
  8. package/packages/config/package.json +1 -1
  9. package/packages/hooks/package.json +4 -4
  10. package/packages/memory/package.json +2 -2
  11. package/packages/openclaw/dist/cli.js +8 -1
  12. package/packages/openclaw/dist/cli.js.map +1 -1
  13. package/packages/openclaw/dist/config.d.ts +6 -2
  14. package/packages/openclaw/dist/config.d.ts.map +1 -1
  15. package/packages/openclaw/dist/config.js +95 -7
  16. package/packages/openclaw/dist/config.js.map +1 -1
  17. package/packages/openclaw/dist/gateway.d.ts +3 -0
  18. package/packages/openclaw/dist/gateway.d.ts.map +1 -1
  19. package/packages/openclaw/dist/gateway.js +114 -16
  20. package/packages/openclaw/dist/gateway.js.map +1 -1
  21. package/packages/openclaw/dist/setup.d.ts.map +1 -1
  22. package/packages/openclaw/dist/setup.js +111 -15
  23. package/packages/openclaw/dist/setup.js.map +1 -1
  24. package/packages/openclaw/package.json +2 -2
  25. package/packages/openclaw/skill/SKILL.md +190 -31
  26. package/packages/openclaw/src/cli.ts +7 -1
  27. package/packages/openclaw/src/config.ts +94 -8
  28. package/packages/openclaw/src/gateway.ts +153 -19
  29. package/packages/openclaw/src/setup.ts +113 -13
  30. package/packages/policy/package.json +2 -2
  31. package/packages/sdk/dist/client.js +6 -8
  32. package/packages/sdk/dist/client.js.map +1 -1
  33. package/packages/sdk/dist/workflows/builder.d.ts +3 -1
  34. package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
  35. package/packages/sdk/dist/workflows/builder.js +1 -0
  36. package/packages/sdk/dist/workflows/builder.js.map +1 -1
  37. package/packages/sdk/dist/workflows/runner.d.ts +15 -1
  38. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  39. package/packages/sdk/dist/workflows/runner.js +146 -117
  40. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  41. package/packages/sdk/package.json +2 -2
  42. package/packages/sdk/scripts/bundle-agent-relay.mjs +11 -1
  43. package/packages/sdk/src/client.ts +6 -8
  44. package/packages/sdk/src/workflows/builder.ts +4 -1
  45. package/packages/sdk/src/workflows/runner.ts +173 -119
  46. package/packages/sdk-py/pyproject.toml +1 -1
  47. package/packages/telemetry/package.json +1 -1
  48. package/packages/trajectory/package.json +2 -2
  49. package/packages/user-directory/package.json +2 -2
  50. package/packages/utils/package.json +2 -2
@@ -1,14 +1,14 @@
1
1
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
- import { join } from 'node:path';
2
+ import { join, dirname, basename } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
- import { existsSync } from 'node:fs';
4
+ import { existsSync, readFileSync } from 'node:fs';
5
5
 
6
6
  import type { GatewayConfig } from './types.js';
7
7
 
8
8
  export interface OpenClawDetection {
9
9
  /** Whether OpenClaw is installed. */
10
10
  installed: boolean;
11
- /** Path to ~/.openclaw/ */
11
+ /** Path to ~/.openclaw/ (or ~/.clawdbot/ for Clawdbot variant) */
12
12
  homeDir: string;
13
13
  /** Path to ~/.openclaw/workspace/ */
14
14
  workspaceDir: string;
@@ -16,19 +16,105 @@ export interface OpenClawDetection {
16
16
  configFile: string | null;
17
17
  /** Parsed openclaw.json (if exists). */
18
18
  config: Record<string, unknown> | null;
19
+ /** Detected variant: 'clawdbot' or 'openclaw'. */
20
+ variant: 'clawdbot' | 'openclaw';
21
+ /** Config filename (e.g. 'clawdbot.json' or 'openclaw.json'). */
22
+ configFilename: string;
19
23
  }
20
24
 
21
- /** Default OpenClaw config directory. Prefers OPENCLAW_HOME env var. */
25
+ /**
26
+ * Determine whether a directory has a valid, parseable config file.
27
+ * Uses sync I/O — only called during startup, not on hot path.
28
+ */
29
+ function hasValidConfig(dir: string, filename: string): boolean {
30
+ const configPath = join(dir, filename);
31
+ if (!existsSync(configPath)) return false;
32
+ try {
33
+ const raw = readFileSync(configPath, 'utf-8');
34
+ JSON.parse(raw);
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ /** Default OpenClaw config directory. Checks env vars and probes for Clawdbot variant. */
22
42
  export function openclawHome(): string {
23
- return process.env.OPENCLAW_HOME || join(homedir(), '.openclaw');
43
+ if (process.env.OPENCLAW_CONFIG_PATH) {
44
+ // Direct config file path — return its parent directory
45
+ return dirname(process.env.OPENCLAW_CONFIG_PATH);
46
+ }
47
+ if (process.env.OPENCLAW_HOME) {
48
+ return process.env.OPENCLAW_HOME;
49
+ }
50
+ // Probe by valid config file presence (not just directory existence).
51
+ // When both dirs exist, prefer the one with a valid config file.
52
+ const clawdbotHome = join(homedir(), '.clawdbot');
53
+ const openclawHomePath = join(homedir(), '.openclaw');
54
+ const clawdbotValid = hasValidConfig(clawdbotHome, 'clawdbot.json');
55
+ const openclawValid = hasValidConfig(openclawHomePath, 'openclaw.json');
56
+
57
+ if (clawdbotValid && !openclawValid) return clawdbotHome;
58
+ if (openclawValid && !clawdbotValid) return openclawHomePath;
59
+ // Both valid or neither valid — prefer clawdbot if its dir exists (marketplace image)
60
+ if (existsSync(clawdbotHome)) return clawdbotHome;
61
+ return openclawHomePath;
24
62
  }
25
63
 
26
64
  /**
27
65
  * Detect whether OpenClaw is installed and return paths/config.
28
66
  */
29
67
  export async function detectOpenClaw(): Promise<OpenClawDetection> {
30
- const homeDir = openclawHome();
31
- const configPath = join(homeDir, 'openclaw.json');
68
+ // Determine variant and config filename
69
+ let homeDir: string;
70
+ let variant: 'clawdbot' | 'openclaw';
71
+ let configFilename: string;
72
+
73
+ if (process.env.OPENCLAW_CONFIG_PATH) {
74
+ // Direct config file path provided
75
+ homeDir = dirname(process.env.OPENCLAW_CONFIG_PATH);
76
+ const base = basename(process.env.OPENCLAW_CONFIG_PATH);
77
+ configFilename = base;
78
+ variant = base === 'clawdbot.json' ? 'clawdbot' : 'openclaw';
79
+ } else if (process.env.OPENCLAW_HOME) {
80
+ homeDir = process.env.OPENCLAW_HOME;
81
+ // Check if the home dir looks like a Clawdbot installation
82
+ const clawdbotConfig = join(homeDir, 'clawdbot.json');
83
+ if (existsSync(clawdbotConfig)) {
84
+ variant = 'clawdbot';
85
+ configFilename = 'clawdbot.json';
86
+ } else {
87
+ variant = 'openclaw';
88
+ configFilename = 'openclaw.json';
89
+ }
90
+ } else {
91
+ // Probe by valid config file, not just directory existence.
92
+ const clawdbotHome = join(homedir(), '.clawdbot');
93
+ const openclawHomePath = join(homedir(), '.openclaw');
94
+ const clawdbotValid = hasValidConfig(clawdbotHome, 'clawdbot.json');
95
+ const openclawValid = hasValidConfig(openclawHomePath, 'openclaw.json');
96
+
97
+ if (clawdbotValid && !openclawValid) {
98
+ homeDir = clawdbotHome;
99
+ variant = 'clawdbot';
100
+ configFilename = 'clawdbot.json';
101
+ } else if (openclawValid && !clawdbotValid) {
102
+ homeDir = openclawHomePath;
103
+ variant = 'openclaw';
104
+ configFilename = 'openclaw.json';
105
+ } else if (existsSync(clawdbotHome)) {
106
+ // Both valid or neither — prefer clawdbot if present (marketplace image)
107
+ homeDir = clawdbotHome;
108
+ variant = 'clawdbot';
109
+ configFilename = 'clawdbot.json';
110
+ } else {
111
+ homeDir = openclawHomePath;
112
+ variant = 'openclaw';
113
+ configFilename = 'openclaw.json';
114
+ }
115
+ }
116
+
117
+ const configPath = join(homeDir, configFilename);
32
118
  const workspaceDir = join(homeDir, 'workspace');
33
119
 
34
120
  const installed = existsSync(homeDir);
@@ -45,7 +131,7 @@ export async function detectOpenClaw(): Promise<OpenClawDetection> {
45
131
  }
46
132
  }
47
133
 
48
- return { installed, homeDir, workspaceDir, configFile, config };
134
+ return { installed, homeDir, workspaceDir, configFile, config, variant, configFilename };
49
135
  }
50
136
 
51
137
  /**
@@ -1,4 +1,4 @@
1
- import { createHash, createPrivateKey, generateKeyPairSync, sign, type KeyObject } from 'node:crypto';
1
+ import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign, type KeyObject } from 'node:crypto';
2
2
  import { chmod, readFile, rename, writeFile, mkdir } from 'node:fs/promises';
3
3
  import { createServer, type Server as HttpServer, type IncomingMessage, type ServerResponse } from 'node:http';
4
4
  import { join } from 'node:path';
@@ -49,13 +49,75 @@ function normalizeChannelName(channel: string): string {
49
49
  // Ed25519 device identity for OpenClaw gateway WebSocket auth
50
50
  // ---------------------------------------------------------------------------
51
51
 
52
+ // ---------------------------------------------------------------------------
53
+ // Auth profile system — deterministic profile selection for WS auth across
54
+ // OpenClaw/Clawdbot versions. Profiles define key encoding, signature format,
55
+ // and payload canonicalization for the device auth handshake.
56
+ // ---------------------------------------------------------------------------
57
+
58
+ interface AuthProfile {
59
+ /** Human-readable profile name (logged on each auth attempt). */
60
+ name: string;
61
+ /** Encoding for the public key sent in the connect message. */
62
+ publicKeyFormat: 'raw-base64url' | 'spki-pem';
63
+ /** Encoding for the Ed25519 signature. */
64
+ signatureEncoding: 'base64url' | 'base64';
65
+ }
66
+
67
+ const AUTH_PROFILES: Record<string, AuthProfile> = {
68
+ default: {
69
+ name: 'default',
70
+ publicKeyFormat: 'raw-base64url',
71
+ signatureEncoding: 'base64url',
72
+ },
73
+ 'clawdbot-v1': {
74
+ name: 'clawdbot-v1',
75
+ publicKeyFormat: 'spki-pem',
76
+ signatureEncoding: 'base64',
77
+ },
78
+ };
79
+
80
+ /**
81
+ * Resolve the auth profile to use. Selection priority:
82
+ * 1. Explicit env var `OPENCLAW_WS_AUTH_COMPAT` (manual override, highest priority)
83
+ * 2. Variant detection: `~/.clawdbot/` detected → clawdbot-v1
84
+ * 3. Default profile (standard OpenClaw, unchanged)
85
+ */
86
+ function resolveAuthProfile(): AuthProfile {
87
+ // 1. Manual override (highest priority)
88
+ const envVal = process.env.OPENCLAW_WS_AUTH_COMPAT;
89
+ if (envVal === 'clawdbot' || envVal === 'clawdbot-v1') {
90
+ return AUTH_PROFILES['clawdbot-v1'];
91
+ }
92
+ if (envVal && AUTH_PROFILES[envVal]) {
93
+ return AUTH_PROFILES[envVal];
94
+ }
95
+
96
+ // 2. Variant detection via config path
97
+ const home = process.env.OPENCLAW_HOME || process.env.OPENCLAW_CONFIG_PATH || '';
98
+ if (home.includes('.clawdbot') || home.includes('clawdbot')) {
99
+ return AUTH_PROFILES['clawdbot-v1'];
100
+ }
101
+
102
+ // 3. Default
103
+ return AUTH_PROFILES['default'];
104
+ }
105
+
106
+ /** Backward-compat helper — returns 'clawdbot' when using clawdbot profile. */
107
+ type WsAuthCompat = 'clawdbot' | undefined;
108
+ function getWsAuthCompat(): WsAuthCompat {
109
+ const profile = resolveAuthProfile();
110
+ return profile.name === 'clawdbot-v1' ? 'clawdbot' : undefined;
111
+ }
112
+
52
113
  interface DeviceIdentity {
53
- publicKeyB64: string; // base64url-encoded raw Ed25519 public key
114
+ publicKeyB64: string; // base64url-encoded raw Ed25519 public key (default mode)
115
+ publicKeyPem?: string; // PEM-encoded SPKI public key (clawdbot compat mode)
54
116
  privateKeyObj: KeyObject; // Node.js KeyObject for signing
55
117
  deviceId: string; // SHA-256 hex of the raw public key
56
118
  }
57
119
 
58
- function generateDeviceIdentity(): DeviceIdentity {
120
+ function generateDeviceIdentity(compat?: WsAuthCompat): DeviceIdentity {
59
121
  const { publicKey, privateKey } = generateKeyPairSync('ed25519');
60
122
 
61
123
  // Extract raw 32-byte public key from SPKI DER (12-byte header for Ed25519)
@@ -64,11 +126,17 @@ function generateDeviceIdentity(): DeviceIdentity {
64
126
  const deviceId = createHash('sha256').update(rawPublicBytes).digest('hex');
65
127
  const publicKeyB64 = Buffer.from(rawPublicBytes).toString('base64url');
66
128
 
67
- return {
129
+ const identity: DeviceIdentity = {
68
130
  publicKeyB64,
69
131
  privateKeyObj: privateKey,
70
132
  deviceId,
71
133
  };
134
+
135
+ if (compat === 'clawdbot') {
136
+ identity.publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }) as string;
137
+ }
138
+
139
+ return identity;
72
140
  }
73
141
 
74
142
  /** Path to persisted device identity file. */
@@ -80,6 +148,10 @@ interface PersistedDevice {
80
148
  publicKeyB64: string;
81
149
  privateKeyPkcs8B64: string; // base64-encoded PKCS#8 DER
82
150
  deviceId: string;
151
+ /** PEM-encoded SPKI public key — present when generated with clawdbot compat mode. */
152
+ publicKeyPem?: string;
153
+ /** PEM-encoded PKCS#8 private key — present when generated with clawdbot compat mode. */
154
+ privateKeyPem?: string;
83
155
  }
84
156
 
85
157
  /**
@@ -89,6 +161,7 @@ interface PersistedDevice {
89
161
  */
90
162
  async function loadOrCreateDeviceIdentity(): Promise<DeviceIdentity> {
91
163
  const filePath = deviceIdentityPath();
164
+ const compat = getWsAuthCompat();
92
165
 
93
166
  // Attempt to load existing identity (no existsSync — just try the read)
94
167
  try {
@@ -102,11 +175,31 @@ async function loadOrCreateDeviceIdentity(): Promise<DeviceIdentity> {
102
175
  // Ensure permissions are tight even if file was created with looser perms
103
176
  await chmod(filePath, 0o600).catch(() => {});
104
177
  console.log(`[openclaw-ws] Loaded persisted device identity (deviceId=${persisted.deviceId.slice(0, 12)}...)`);
105
- return {
178
+
179
+ const identity: DeviceIdentity = {
106
180
  publicKeyB64: persisted.publicKeyB64,
107
181
  privateKeyObj,
108
182
  deviceId: persisted.deviceId,
109
183
  };
184
+
185
+ // If compat mode is clawdbot but the persisted device has no PEM keys,
186
+ // derive them on-the-fly from the existing DER key material.
187
+ if (compat === 'clawdbot') {
188
+ if (persisted.publicKeyPem) {
189
+ identity.publicKeyPem = persisted.publicKeyPem;
190
+ } else {
191
+ // Reconstruct SPKI public key from the stored base64url raw bytes
192
+ const rawPublicBytes = Buffer.from(persisted.publicKeyB64, 'base64url');
193
+ // Ed25519 SPKI DER = 12-byte header + 32-byte raw key
194
+ const spkiHeader = Buffer.from('302a300506032b6570032100', 'hex');
195
+ const spkiDer = Buffer.concat([spkiHeader, rawPublicBytes]);
196
+ const publicKeyObj = createPublicKey({ key: spkiDer, format: 'der', type: 'spki' });
197
+ identity.publicKeyPem = publicKeyObj.export({ type: 'spki', format: 'pem' }) as string;
198
+ console.log('[openclaw-ws] Derived PEM public key from existing DER key for clawdbot compat mode');
199
+ }
200
+ }
201
+
202
+ return identity;
110
203
  } catch (err) {
111
204
  // ENOENT is expected on first run; other errors mean corruption
112
205
  if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
@@ -115,7 +208,7 @@ async function loadOrCreateDeviceIdentity(): Promise<DeviceIdentity> {
115
208
  }
116
209
 
117
210
  // Generate fresh and persist via atomic write-then-rename
118
- const identity = generateDeviceIdentity();
211
+ const identity = generateDeviceIdentity(compat);
119
212
  const pkcs8Der = identity.privateKeyObj.export({ type: 'pkcs8', format: 'der' });
120
213
  const persisted: PersistedDevice = {
121
214
  publicKeyB64: identity.publicKeyB64,
@@ -123,6 +216,11 @@ async function loadOrCreateDeviceIdentity(): Promise<DeviceIdentity> {
123
216
  deviceId: identity.deviceId,
124
217
  };
125
218
 
219
+ if (compat === 'clawdbot' && identity.publicKeyPem) {
220
+ persisted.publicKeyPem = identity.publicKeyPem;
221
+ persisted.privateKeyPem = identity.privateKeyObj.export({ type: 'pkcs8', format: 'pem' }) as string;
222
+ }
223
+
126
224
  try {
127
225
  const dir = join(openclawHome(), 'workspace', 'relaycast');
128
226
  await mkdir(dir, { recursive: true });
@@ -151,6 +249,8 @@ function signConnectPayload(
151
249
  nonce: string;
152
250
  },
153
251
  ): string {
252
+ const profile = resolveAuthProfile();
253
+
154
254
  // v3 payload format: v3|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce|platform|deviceFamily
155
255
  const payload = [
156
256
  'v3',
@@ -168,9 +268,14 @@ function signConnectPayload(
168
268
 
169
269
  const payloadBytes = Buffer.from(payload, 'utf-8');
170
270
 
271
+ // Diagnostic logging: selected profile + pre-auth fingerprint (no secrets).
272
+ const payloadHash = createHash('sha256').update(payloadBytes).digest('hex').slice(0, 16);
273
+ console.log(`[ws-auth] profile=${profile.name} deviceId=${device.deviceId.slice(0, 16)}... keyFormat=${profile.publicKeyFormat} sigEncoding=${profile.signatureEncoding} payloadHash=${payloadHash}`);
274
+
171
275
  // Ed25519 sign — no hash algorithm needed (null), it's built into Ed25519
172
276
  const signature = sign(null, payloadBytes, device.privateKeyObj);
173
- return Buffer.from(signature).toString('base64url');
277
+
278
+ return Buffer.from(signature).toString(profile.signatureEncoding);
174
279
  }
175
280
 
176
281
 
@@ -206,11 +311,13 @@ export class OpenClawGatewayClient {
206
311
  private static readonly MAX_CONSECUTIVE_FAILURES = 5;
207
312
  private static readonly BASE_RECONNECT_MS = 3_000;
208
313
  private static readonly MAX_RECONNECT_MS = 30_000;
314
+ /** Slow retry interval after pairing rejection or max failures (60s). */
315
+ private static readonly PAIRING_RETRY_MS = 60_000;
209
316
 
210
317
  constructor(token: string, port: number, device?: DeviceIdentity) {
211
318
  this.token = token;
212
319
  this.port = port;
213
- this.device = device ?? generateDeviceIdentity();
320
+ this.device = device ?? generateDeviceIdentity(getWsAuthCompat());
214
321
  }
215
322
 
216
323
  /**
@@ -292,7 +399,8 @@ export class OpenClawGatewayClient {
292
399
  // Detect pairing rejection: code 1008 (Policy Violation) with pairing reason
293
400
  if (code === 1008 && /pairing|not.paired/i.test(reasonStr)) {
294
401
  console.error('[openclaw-ws] Connection closed due to pairing policy. Device is not paired.');
295
- console.error('[openclaw-ws] Ensure OPENCLAW_GATEWAY_TOKEN matches ~/.openclaw/openclaw.json gateway.auth.token');
402
+ console.error(`[openclaw-ws] Device ID: ${this.device.deviceId.slice(0, 16)}...`);
403
+ console.error('[openclaw-ws] Run: openclaw devices approve <requestId> (check gateway logs for requestId)');
296
404
  this.pairingRejected = true;
297
405
  }
298
406
 
@@ -310,7 +418,7 @@ export class OpenClawGatewayClient {
310
418
  this.connectReject = null;
311
419
  this.connectResolve = null;
312
420
  }
313
- if (!this.stopped && !this.pairingRejected) {
421
+ if (!this.stopped) {
314
422
  this.scheduleReconnect();
315
423
  }
316
424
  });
@@ -360,6 +468,12 @@ export class OpenClawGatewayClient {
360
468
  nonce: payload.nonce,
361
469
  });
362
470
 
471
+ // Select public key format based on resolved auth profile.
472
+ const profile = resolveAuthProfile();
473
+ const publicKeyField = profile.publicKeyFormat === 'spki-pem' && this.device.publicKeyPem
474
+ ? this.device.publicKeyPem
475
+ : this.device.publicKeyB64;
476
+
363
477
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
364
478
  console.warn('[openclaw-ws] WebSocket not open when trying to send connect');
365
479
  return;
@@ -388,7 +502,7 @@ export class OpenClawGatewayClient {
388
502
  userAgent: 'relaycast-gateway/1.0.0',
389
503
  device: {
390
504
  id: this.device.deviceId,
391
- publicKey: this.device.publicKeyB64,
505
+ publicKey: publicKeyField,
392
506
  signature,
393
507
  signedAt,
394
508
  nonce: payload.nonce,
@@ -413,8 +527,17 @@ export class OpenClawGatewayClient {
413
527
  const isPairing = /pairing.required|not.paired/i.test(errStr);
414
528
 
415
529
  if (isPairing) {
530
+ const errObj = msg.error as Record<string, unknown> | undefined;
531
+ const requestId = errObj?.requestId ?? errObj?.request_id ?? '';
416
532
  console.error('[openclaw-ws] Pairing rejected — device is not paired with the OpenClaw gateway.');
417
- console.error('[openclaw-ws] Ensure OPENCLAW_GATEWAY_TOKEN matches ~/.openclaw/openclaw.json gateway.auth.token');
533
+ if (requestId) {
534
+ console.error(`[openclaw-ws] Approve this device: openclaw devices approve ${requestId}`);
535
+ }
536
+ console.error(`[openclaw-ws] Device ID: ${this.device.deviceId.slice(0, 16)}...`);
537
+ const configHint = getWsAuthCompat() === 'clawdbot'
538
+ ? '~/.clawdbot/clawdbot.json'
539
+ : '~/.openclaw/openclaw.json';
540
+ console.error(`[openclaw-ws] Ensure OPENCLAW_GATEWAY_TOKEN matches ${configHint} gateway.auth.token`);
418
541
  this.pairingRejected = true;
419
542
  } else {
420
543
  console.warn(`[openclaw-ws] Auth rejected: ${errStr}`);
@@ -494,16 +617,27 @@ export class OpenClawGatewayClient {
494
617
  }
495
618
 
496
619
  private scheduleReconnect(): void {
497
- if (this.stopped || this.pairingRejected || this.reconnectTimer) return;
498
-
499
- this.consecutiveFailures++;
500
-
501
- if (this.consecutiveFailures >= OpenClawGatewayClient.MAX_CONSECUTIVE_FAILURES) {
502
- console.warn(`[openclaw-ws] ${this.consecutiveFailures} consecutive connection failures — stopping reconnect.`);
503
- console.warn('[openclaw-ws] Check that the OpenClaw gateway is running and OPENCLAW_GATEWAY_TOKEN is correct.');
620
+ if (this.stopped || this.reconnectTimer) return;
621
+
622
+ // After pairing rejection or max failures, switch to slow periodic retry
623
+ // so the gateway can self-heal once pairing is approved externally.
624
+ if (this.pairingRejected || this.consecutiveFailures >= OpenClawGatewayClient.MAX_CONSECUTIVE_FAILURES) {
625
+ if (this.consecutiveFailures === OpenClawGatewayClient.MAX_CONSECUTIVE_FAILURES) {
626
+ console.warn(`[openclaw-ws] ${this.consecutiveFailures} consecutive failures switching to slow retry (every 60s).`);
627
+ console.warn('[openclaw-ws] Check that the OpenClaw gateway is running and OPENCLAW_GATEWAY_TOKEN is correct.');
628
+ }
629
+ this.consecutiveFailures++;
630
+ console.log(`[openclaw-ws] Slow retry in ${OpenClawGatewayClient.PAIRING_RETRY_MS / 1000}s...`);
631
+ this.reconnectTimer = setTimeout(() => {
632
+ this.reconnectTimer = null;
633
+ this.pairingRejected = false; // Clear flag so connect attempt proceeds
634
+ this.doConnect();
635
+ }, OpenClawGatewayClient.PAIRING_RETRY_MS);
504
636
  return;
505
637
  }
506
638
 
639
+ this.consecutiveFailures++;
640
+
507
641
  const delay = Math.min(
508
642
  OpenClawGatewayClient.BASE_RECONNECT_MS * Math.pow(2, this.consecutiveFailures - 1),
509
643
  OpenClawGatewayClient.MAX_RECONNECT_MS,
@@ -5,6 +5,7 @@ import { existsSync } from 'node:fs';
5
5
  import { hostname } from 'node:os';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { spawn as spawnProcess, execFileSync } from 'node:child_process';
8
+ import { randomBytes } from 'node:crypto';
8
9
 
9
10
  import { RelayCast } from '@relaycast/sdk';
10
11
 
@@ -25,6 +26,23 @@ function extractNestedValue(obj: unknown, path: string): unknown {
25
26
  return current;
26
27
  }
27
28
 
29
+ /**
30
+ * Set a deeply nested value in an object by dot-separated path, creating
31
+ * intermediate objects as needed.
32
+ */
33
+ function setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void {
34
+ const keys = path.split('.');
35
+ let current: Record<string, unknown> = obj;
36
+ for (let i = 0; i < keys.length - 1; i++) {
37
+ const key = keys[i];
38
+ if (current[key] == null || typeof current[key] !== 'object') {
39
+ current[key] = {};
40
+ }
41
+ current = current[key] as Record<string, unknown>;
42
+ }
43
+ current[keys[keys.length - 1]] = value;
44
+ }
45
+
28
46
  /**
29
47
  * Resolve how to invoke mcporter. Prefers a global binary, falls back to npx.
30
48
  */
@@ -98,14 +116,18 @@ export async function setup(options: SetupOptions): Promise<SetupResult> {
98
116
  const baseUrl = options.baseUrl ?? 'https://api.relaycast.dev';
99
117
  const channels = options.channels ?? ['general'];
100
118
 
119
+ // CLI name for restart reminder messages (based on detected variant)
120
+ const cliName = detection.variant === 'clawdbot' ? 'clawdbot' : 'openclaw';
121
+ const serviceName = detection.variant === 'clawdbot' ? 'clawdbot' : 'openclaw';
122
+
101
123
  if (!detection.installed) {
102
124
  // Auto-create ~/.openclaw/ if OpenClaw binary is available but the config dir
103
125
  // doesn't exist yet (common in Docker images before onboarding).
104
126
  try {
105
127
  await mkdir(detection.homeDir, { recursive: true });
106
128
  await mkdir(join(detection.homeDir, 'workspace'), { recursive: true });
107
- // Write a minimal openclaw.json so MCP servers can be registered
108
- const configPath = join(detection.homeDir, 'openclaw.json');
129
+ // Write a minimal config file so MCP servers can be registered
130
+ const configPath = join(detection.homeDir, detection.configFilename);
109
131
  if (!existsSync(configPath)) {
110
132
  await writeFile(configPath, JSON.stringify({ mcpServers: {} }, null, 2) + '\n', 'utf-8');
111
133
  }
@@ -126,14 +148,48 @@ export async function setup(options: SetupOptions): Promise<SetupResult> {
126
148
 
127
149
  // Enable the OpenResponses HTTP API so the inbound gateway can inject
128
150
  // messages via POST /v1/responses on the local OpenClaw gateway.
129
- try {
130
- execFileSync('openclaw', [
151
+ // Try CLI names in order: openclaw, clawdbot, clawdbot-cli.sh.
152
+ // If all CLI calls fail, mutate the config JSON directly.
153
+ let configMutated = false;
154
+ {
155
+ const httpEndpointArgs = [
131
156
  'config', 'set',
132
157
  'gateway.http.endpoints.responses.enabled', 'true',
133
- ], { stdio: 'pipe' });
134
- } catch {
135
- console.warn('Could not enable OpenResponses API (non-fatal). Enable manually:');
136
- console.warn(' openclaw config set gateway.http.endpoints.responses.enabled true');
158
+ ];
159
+ const cliCandidates = ['openclaw', 'clawdbot', 'clawdbot-cli.sh'];
160
+ let cliSuccess = false;
161
+
162
+ for (const cli of cliCandidates) {
163
+ try {
164
+ execFileSync(cli, httpEndpointArgs, { stdio: 'pipe' });
165
+ cliSuccess = true;
166
+ break;
167
+ } catch {
168
+ // Try next candidate
169
+ }
170
+ }
171
+
172
+ if (!cliSuccess) {
173
+ // Fall back to direct JSON config file mutation
174
+ if (detection.configFile) {
175
+ try {
176
+ const raw = await readFile(detection.configFile, 'utf-8');
177
+ const cfg = JSON.parse(raw) as Record<string, unknown>;
178
+ setNestedValue(cfg, 'gateway.http.endpoints.responses.enabled', true);
179
+ await writeFile(detection.configFile, JSON.stringify(cfg, null, 2) + '\n', 'utf-8');
180
+ // Reload config in detection
181
+ detection.config = cfg;
182
+ configMutated = true;
183
+ console.log('[setup] Enabled gateway.http.endpoints.responses.enabled via config file.');
184
+ } catch (writeErr) {
185
+ console.warn('Could not enable OpenResponses API (non-fatal). Enable manually:');
186
+ console.warn(` ${cliName} config set gateway.http.endpoints.responses.enabled true`);
187
+ }
188
+ } else {
189
+ console.warn('Could not enable OpenResponses API (non-fatal). Enable manually:');
190
+ console.warn(` ${cliName} config set gateway.http.endpoints.responses.enabled true`);
191
+ }
192
+ }
137
193
  }
138
194
 
139
195
  // Resolve API key: use provided key or create a new workspace
@@ -220,19 +276,63 @@ export async function setup(options: SetupOptions): Promise<SetupResult> {
220
276
  );
221
277
  }
222
278
 
223
- // Extract gateway auth from openclaw.json (if available)
224
- const openclawGatewayToken =
279
+ // Extract gateway auth from config (if available). Auto-generate if missing.
280
+ let openclawGatewayToken: string | undefined =
225
281
  process.env.OPENCLAW_GATEWAY_TOKEN ??
226
282
  (extractNestedValue(detection.config, 'gateway.auth.token') as string | undefined);
283
+
227
284
  const openclawGatewayPortRaw =
228
285
  process.env.OPENCLAW_GATEWAY_PORT ??
229
286
  (extractNestedValue(detection.config, 'gateway.port') as number | string | undefined);
230
287
  const openclawGatewayPort = openclawGatewayPortRaw ? Number(openclawGatewayPortRaw) : undefined;
231
288
 
232
289
  if (!openclawGatewayToken) {
233
- console.warn('[setup] No gateway token found in openclaw.json or OPENCLAW_GATEWAY_TOKEN env.');
234
- console.warn('[setup] Inbound gateway may fail to pair. Set it manually:');
235
- console.warn('[setup] export OPENCLAW_GATEWAY_TOKEN=$(cat ~/.openclaw/openclaw.json | jq -r .gateway.auth.token)');
290
+ // Generate a random token and persist it to the config file
291
+ const generated = randomBytes(16).toString('hex');
292
+ openclawGatewayToken = generated;
293
+ console.log('[setup] No gateway token found — generating one and writing to config file.');
294
+
295
+ if (detection.configFile) {
296
+ try {
297
+ const raw = await readFile(detection.configFile, 'utf-8');
298
+ const cfg = JSON.parse(raw) as Record<string, unknown>;
299
+ setNestedValue(cfg, 'gateway.auth.token', generated);
300
+ await writeFile(detection.configFile, JSON.stringify(cfg, null, 2) + '\n', 'utf-8');
301
+ detection.config = cfg;
302
+ configMutated = true;
303
+ } catch (writeErr) {
304
+ console.warn(`[setup] Could not write generated token to config file: ${writeErr instanceof Error ? writeErr.message : String(writeErr)}`);
305
+ }
306
+ } else {
307
+ console.warn('[setup] No config file available to persist generated token. Set manually:');
308
+ console.warn(`[setup] export OPENCLAW_GATEWAY_TOKEN=${generated}`);
309
+ }
310
+ }
311
+
312
+ // Print restart reminder if any config mutations were made
313
+ if (configMutated) {
314
+ console.log('');
315
+ console.log('Config changes detected. Restart the gateway to apply:');
316
+ console.log(` systemctl restart ${serviceName}`);
317
+ if (serviceName !== 'openclaw') {
318
+ console.log(` # or: systemctl restart openclaw`);
319
+ }
320
+ console.log(` # or restart manually if not using systemd`);
321
+ console.log('');
322
+ }
323
+
324
+ // Exec policy preflight warning — warn when security is missing OR not 'full'
325
+ {
326
+ const execSecurity = extractNestedValue(detection.config, 'tools.exec.security') as string | undefined;
327
+ if (execSecurity !== 'full') {
328
+ console.warn('');
329
+ console.warn('Warning: Execution policies may be locked down. If the agent can only chat:');
330
+ console.warn(` ${cliName} config set tools.exec.host gateway`);
331
+ console.warn(` ${cliName} config set tools.exec.ask off`);
332
+ console.warn(` ${cliName} config set tools.exec.security full`);
333
+ console.warn(` systemctl restart ${serviceName}`);
334
+ console.warn('');
335
+ }
236
336
  }
237
337
 
238
338
  // Save gateway config (.env)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/policy",
3
- "version": "3.1.6",
3
+ "version": "3.1.8",
4
4
  "description": "Agent policy management with multi-level fallback (repo, local PRPM, cloud workspace)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "test:watch": "vitest"
23
23
  },
24
24
  "dependencies": {
25
- "@agent-relay/config": "3.1.6"
25
+ "@agent-relay/config": "3.1.8"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.19.3",
@@ -516,6 +516,7 @@ function detectPlatformSuffix() {
516
516
  const platformMap = {
517
517
  darwin: { arm64: 'darwin-arm64', x64: 'darwin-x64' },
518
518
  linux: { arm64: 'linux-arm64', x64: 'linux-x64' },
519
+ win32: { x64: 'win32-x64' },
519
520
  };
520
521
  return platformMap[process.platform]?.[process.arch] ?? null;
521
522
  }
@@ -595,21 +596,18 @@ function resolveDefaultBinaryPath() {
595
596
  if (fs.existsSync(workspaceRelease)) {
596
597
  return workspaceRelease;
597
598
  }
598
- // 2. Check for bundled broker binary in SDK package (npm install)
599
- // Try platform-specific name first (CI publishes per-platform binaries),
600
- // then fall back to the generic name (local dev / postinstall copy).
599
+ // 2. Check for bundled platform-specific broker binary in SDK package (npm install).
600
+ // Only use binaries that match the current platform to avoid running
601
+ // e.g. a macOS binary on Linux (or vice-versa).
601
602
  const binDir = path.resolve(moduleDir, '..', 'bin');
602
603
  const suffix = detectPlatformSuffix();
603
604
  if (suffix) {
604
- const platformBinary = path.join(binDir, `agent-relay-broker-${suffix}`);
605
+ const ext = process.platform === 'win32' ? '.exe' : '';
606
+ const platformBinary = path.join(binDir, `agent-relay-broker-${suffix}${ext}`);
605
607
  if (fs.existsSync(platformBinary)) {
606
608
  return platformBinary;
607
609
  }
608
610
  }
609
- const bundled = path.join(binDir, brokerExe);
610
- if (fs.existsSync(bundled)) {
611
- return bundled;
612
- }
613
611
  // 3. Check for standalone broker binary in ~/.agent-relay/bin/ (install.sh)
614
612
  const homeDir = process.env.HOME || process.env.USERPROFILE || '';
615
613
  const standaloneBroker = path.join(homeDir, '.agent-relay', 'bin', brokerExe);