agent-relay 3.1.8 → 3.1.10

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.
@@ -1,4 +1,4 @@
1
- import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign, type KeyObject } from 'node:crypto';
1
+ import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify, 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';
@@ -16,7 +16,7 @@ import type {
16
16
  } from '@relaycast/sdk';
17
17
  import WebSocket from 'ws';
18
18
 
19
- import { openclawHome } from './config.js';
19
+ import { openclawHome, detectOpenClaw } from './config.js';
20
20
  import { DEFAULT_OPENCLAW_GATEWAY_PORT, type GatewayConfig, type InboundMessage, type DeliveryResult } from './types.js';
21
21
  import { SpawnManager } from './spawn/manager.js';
22
22
  import type { SpawnOptions } from './spawn/types.js';
@@ -71,9 +71,12 @@ const AUTH_PROFILES: Record<string, AuthProfile> = {
71
71
  signatureEncoding: 'base64url',
72
72
  },
73
73
  'clawdbot-v1': {
74
+ // Server (openclaw/openclaw device-identity.ts) accepts both PEM and raw-base64url
75
+ // public keys, and decodes signatures in both base64url and base64. Use base64url
76
+ // for consistency — matches the server's own signDevicePayload() output.
74
77
  name: 'clawdbot-v1',
75
- publicKeyFormat: 'spki-pem',
76
- signatureEncoding: 'base64',
78
+ publicKeyFormat: 'raw-base64url',
79
+ signatureEncoding: 'base64url',
77
80
  },
78
81
  };
79
82
 
@@ -93,9 +96,12 @@ function resolveAuthProfile(): AuthProfile {
93
96
  return AUTH_PROFILES[envVal];
94
97
  }
95
98
 
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
+ // 2. Variant detection via filesystem probing — delegates to openclawHome()
100
+ // which checks valid parseable config files, not just directory existence.
101
+ // Strict suffix check avoids false positives from substring matching.
102
+ const home = openclawHome();
103
+ const homeSuffix = home.replace(/[/\\]+$/, '').split(/[/\\]/).pop() ?? '';
104
+ if (homeSuffix === '.clawdbot' || homeSuffix === 'clawdbot') {
99
105
  return AUTH_PROFILES['clawdbot-v1'];
100
106
  }
101
107
 
@@ -235,6 +241,86 @@ async function loadOrCreateDeviceIdentity(): Promise<DeviceIdentity> {
235
241
  return identity;
236
242
  }
237
243
 
244
+ /** Hash helper for diagnostics (no secrets leaked — just truncated SHA-256). */
245
+ function shortHash(data: string | Buffer): string {
246
+ const buf = typeof data === 'string' ? Buffer.from(data, 'utf-8') : data;
247
+ return createHash('sha256').update(buf).digest('hex').slice(0, 16);
248
+ }
249
+
250
+ /**
251
+ * Canonicalization variants to try for debugging. Each produces a different
252
+ * pipe-delimited payload string. The server should match exactly one.
253
+ */
254
+ function buildCanonicalVariants(
255
+ device: DeviceIdentity,
256
+ params: {
257
+ clientId: string;
258
+ clientMode: string;
259
+ platform: string;
260
+ deviceFamily: string;
261
+ role: string;
262
+ scopes: string[];
263
+ signedAt: number;
264
+ token: string;
265
+ nonce: string;
266
+ },
267
+ ): Array<{ name: string; payload: string }> {
268
+ const signedAtMs = String(params.signedAt);
269
+ const signedAtSec = String(Math.floor(params.signedAt / 1000));
270
+ const scopesCsv = params.scopes.join(',');
271
+
272
+ return [
273
+ // V0: current default order (v3|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce|platform|deviceFamily)
274
+ {
275
+ name: 'v3-default-ms',
276
+ payload: ['v3', device.deviceId, params.clientId, params.clientMode, params.role, scopesCsv, signedAtMs, params.token || '', params.nonce, params.platform, params.deviceFamily].join('|'),
277
+ },
278
+ // V1: signedAt in seconds instead of milliseconds
279
+ {
280
+ name: 'v3-default-sec',
281
+ payload: ['v3', device.deviceId, params.clientId, params.clientMode, params.role, scopesCsv, signedAtSec, params.token || '', params.nonce, params.platform, params.deviceFamily].join('|'),
282
+ },
283
+ // V2: no token in payload (token omitted entirely)
284
+ {
285
+ name: 'v3-no-token-ms',
286
+ payload: ['v3', device.deviceId, params.clientId, params.clientMode, params.role, scopesCsv, signedAtMs, params.nonce, params.platform, params.deviceFamily].join('|'),
287
+ },
288
+ // V3: nonce before token (swapped positions)
289
+ {
290
+ name: 'v3-nonce-first-ms',
291
+ payload: ['v3', device.deviceId, params.clientId, params.clientMode, params.role, scopesCsv, signedAtMs, params.nonce, params.token || '', params.platform, params.deviceFamily].join('|'),
292
+ },
293
+ // V4: fewer fields — just core identity + nonce + signedAt (minimal)
294
+ {
295
+ name: 'v3-minimal',
296
+ payload: ['v3', device.deviceId, signedAtMs, params.nonce].join('|'),
297
+ },
298
+ // V5: signedAt seconds + no token
299
+ {
300
+ name: 'v3-no-token-sec',
301
+ payload: ['v3', device.deviceId, params.clientId, params.clientMode, params.role, scopesCsv, signedAtSec, params.nonce, params.platform, params.deviceFamily].join('|'),
302
+ },
303
+ // V6: v2 format (no platform/deviceFamily) — used by older gateway versions
304
+ {
305
+ name: 'v2-default-ms',
306
+ payload: ['v2', device.deviceId, params.clientId, params.clientMode, params.role, scopesCsv, signedAtMs, params.token || '', params.nonce].join('|'),
307
+ },
308
+ // V7: v2 with signedAt in seconds
309
+ {
310
+ name: 'v2-default-sec',
311
+ payload: ['v2', device.deviceId, params.clientId, params.clientMode, params.role, scopesCsv, signedAtSec, params.token || '', params.nonce].join('|'),
312
+ },
313
+ // V8: v2 without token
314
+ {
315
+ name: 'v2-no-token-ms',
316
+ payload: ['v2', device.deviceId, params.clientId, params.clientMode, params.role, scopesCsv, signedAtMs, params.nonce].join('|'),
317
+ },
318
+ ];
319
+ }
320
+
321
+ /** Payload version override for v3↔v2 fallback. */
322
+ type PayloadVersionOverride = 'v2' | 'v3' | null;
323
+
238
324
  function signConnectPayload(
239
325
  device: DeviceIdentity,
240
326
  params: {
@@ -248,34 +334,75 @@ function signConnectPayload(
248
334
  token: string;
249
335
  nonce: string;
250
336
  },
337
+ versionOverride?: PayloadVersionOverride,
251
338
  ): string {
252
339
  const profile = resolveAuthProfile();
253
340
 
254
- // v3 payload format: v3|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce|platform|deviceFamily
255
- const payload = [
256
- 'v3',
257
- device.deviceId,
258
- params.clientId,
259
- params.clientMode,
260
- params.role,
261
- params.scopes.join(','),
262
- String(params.signedAt),
263
- params.token || '',
264
- params.nonce,
265
- params.platform,
266
- params.deviceFamily,
267
- ].join('|');
268
-
269
- const payloadBytes = Buffer.from(payload, 'utf-8');
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}`);
341
+ // Build canonicalization variants for diagnostics
342
+ const variants = buildCanonicalVariants(device, params);
343
+
344
+ // Select primary payload version:
345
+ // 1. If versionOverride is set (from fallback), use that directly
346
+ // 2. clawdbot-v1 defaults to v2 (older gateway compat)
347
+ // 3. default profile uses v3
348
+ let primaryName: string;
349
+ if (versionOverride === 'v2') {
350
+ primaryName = 'v2-default-ms';
351
+ } else if (versionOverride === 'v3') {
352
+ primaryName = 'v3-default-ms';
353
+ } else {
354
+ primaryName = profile.name === 'clawdbot-v1' ? 'v2-default-ms' : 'v3-default-ms';
355
+ }
356
+ const primary = variants.find(v => v.name === primaryName) ?? variants[0];
357
+
358
+ const payloadBytes = Buffer.from(primary.payload, 'utf-8');
359
+
360
+ const isDebug = process.env.RELAY_LOG_LEVEL === 'DEBUG' || process.env.OPENCLAW_WS_DEBUG === '1';
361
+
362
+ // Concise production log — one line with essential info
363
+ console.log(`[ws-auth] profile=${profile.name} payload=${primary.name} device=${device.deviceId.slice(0, 12)}...${versionOverride ? ` override=${versionOverride}` : ''}`);
364
+
365
+ // Verbose debug logging — field hashes and canonicalization matrix
366
+ if (isDebug) {
367
+ console.log(`[ws-auth-debug] signedAt=${params.signedAt}ms nonce=${shortHash(params.nonce)} keyFormat=${profile.publicKeyFormat} sigEncoding=${profile.signatureEncoding}`);
368
+ console.log(`[ws-auth-debug] field hashes: deviceId=${shortHash(device.deviceId)} clientId=${shortHash(params.clientId)} role=${shortHash(params.role)} scopes=${shortHash(params.scopes.join(','))} token=${shortHash(params.token || '')} nonce=${shortHash(params.nonce)}`);
369
+ console.log('[ws-auth-debug] canonicalization matrix:');
370
+ for (const v of variants) {
371
+ console.log(` ${v.name}: hash=${shortHash(v.payload)}`);
372
+ }
373
+ console.log(`[ws-auth-debug] payloadHash=${shortHash(primary.payload)}`);
374
+ }
274
375
 
275
376
  // Ed25519 sign — no hash algorithm needed (null), it's built into Ed25519
276
377
  const signature = sign(null, payloadBytes, device.privateKeyObj);
378
+ const encoded = Buffer.from(signature).toString(profile.signatureEncoding);
277
379
 
278
- return Buffer.from(signature).toString(profile.signatureEncoding);
380
+ // Self-verification (debug only): confirm our signature is valid locally.
381
+ if (isDebug) {
382
+ try {
383
+ // Derive public key from private key (same as server would use from our publicKey field)
384
+ const pubKey = createPublicKey(device.privateKeyObj);
385
+ const selfVerifyRaw = verify(null, payloadBytes, pubKey, signature);
386
+
387
+ // Also verify the round-trip: decode our encoded signature like the server would
388
+ const decodedSig = Buffer.from(encoded, profile.signatureEncoding === 'base64url' ? 'base64url' : 'base64');
389
+ const selfVerifyEncoded = verify(null, payloadBytes, pubKey, decodedSig);
390
+
391
+ // Verify deviceId matches public key
392
+ const rawPubBytes = pubKey.export({ type: 'spki', format: 'der' }).subarray(12);
393
+ const derivedDeviceId = createHash('sha256').update(rawPubBytes).digest('hex');
394
+ const deviceIdMatch = derivedDeviceId === device.deviceId;
395
+
396
+ console.log(`[ws-auth-debug] self-verify: raw=${selfVerifyRaw} encoded=${selfVerifyEncoded} deviceIdMatch=${deviceIdMatch} derivedId=${derivedDeviceId.slice(0, 16)}...`);
397
+ if (!deviceIdMatch) {
398
+ console.error(`[ws-auth-debug] DEVICE ID MISMATCH: derived=${derivedDeviceId} sent=${device.deviceId}`);
399
+ }
400
+ } catch (err) {
401
+ console.error(`[ws-auth-debug] self-verify error: ${err instanceof Error ? err.message : String(err)}`);
402
+ }
403
+ }
404
+
405
+ return encoded;
279
406
  }
280
407
 
281
408
 
@@ -305,6 +432,13 @@ export class OpenClawGatewayClient {
305
432
  private connectTimeout: ReturnType<typeof setTimeout> | null = null;
306
433
  private pairingRejected = false;
307
434
  private consecutiveFailures = 0;
435
+ /** Payload version override for v3↔v2 fallback (null = use profile default). */
436
+ private payloadVersionOverride: PayloadVersionOverride = null;
437
+ /** Whether a fallback attempt has already been tried this connection cycle. */
438
+ private fallbackAttempted = false;
439
+ /** Auth rejection counters for observability. */
440
+ private authRejectCount = 0;
441
+ private authFallbackCount = 0;
308
442
 
309
443
  /** Default timeout for initial connection (30 seconds). */
310
444
  private static readonly CONNECT_TIMEOUT_MS = 30_000;
@@ -337,6 +471,9 @@ export class OpenClawGatewayClient {
337
471
  // Explicit connect() clears pairing rejection so users can retry after fixing their token
338
472
  this.pairingRejected = false;
339
473
  this.stopped = false;
474
+ // Reset fallback state for fresh connection attempts
475
+ this.payloadVersionOverride = null;
476
+ this.fallbackAttempted = false;
340
477
 
341
478
  // Cancel any pending reconnect timer to prevent orphaned WebSocket connections
342
479
  if (this.reconnectTimer) {
@@ -374,23 +511,29 @@ export class OpenClawGatewayClient {
374
511
  private doConnect(): void {
375
512
  if (this.stopped) return;
376
513
 
514
+ let ws: WebSocket;
377
515
  try {
378
- this.ws = new WebSocket(`ws://127.0.0.1:${this.port}`);
516
+ ws = new WebSocket(`ws://127.0.0.1:${this.port}`);
379
517
  } catch (err) {
380
518
  console.warn(`[openclaw-ws] Connection failed: ${err instanceof Error ? err.message : String(err)}`);
381
519
  this.scheduleReconnect();
382
520
  return;
383
521
  }
522
+ this.ws = ws;
384
523
 
385
- this.ws.on('open', () => {
524
+ ws.on('open', () => {
386
525
  console.log('[openclaw-ws] Connected to OpenClaw gateway');
387
526
  });
388
527
 
389
- this.ws.on('message', (data) => {
528
+ ws.on('message', (data) => {
390
529
  this.handleMessage(data.toString());
391
530
  });
392
531
 
393
- this.ws.on('close', (code, reason) => {
532
+ ws.on('close', (code, reason) => {
533
+ // Guard: ignore close events from superseded WebSocket instances.
534
+ // During v3↔v2 fallback, the old WS is replaced before its close fires.
535
+ if (this.ws !== ws) return;
536
+
394
537
  const reasonStr = reason.toString();
395
538
  console.warn(`[openclaw-ws] Disconnected: ${code} ${reasonStr}`);
396
539
  const wasAuthenticated = this.authenticated;
@@ -423,7 +566,10 @@ export class OpenClawGatewayClient {
423
566
  }
424
567
  });
425
568
 
426
- this.ws.on('error', (err) => {
569
+ ws.on('error', (err) => {
570
+ // Guard: ignore error events from superseded WebSocket instances.
571
+ if (this.ws !== ws) return;
572
+
427
573
  console.warn(`[openclaw-ws] Error: ${err.message}`);
428
574
  // If we weren't authenticated yet, reject the connect promise
429
575
  if (!this.authenticated && this.connectReject) {
@@ -447,6 +593,10 @@ export class OpenClawGatewayClient {
447
593
  if (msg.type === 'event' && msg.event === 'connect.challenge') {
448
594
  const payload = msg.payload as { nonce: string; ts: number };
449
595
  console.log('[openclaw-ws] Received connect.challenge, signing...');
596
+ // Log raw challenge payload for debugging canonicalization issues
597
+ if (process.env.RELAY_LOG_LEVEL === 'DEBUG' || process.env.OPENCLAW_WS_DEBUG === '1') {
598
+ console.log(`[ws-auth-debug] challenge payload: ${JSON.stringify(payload)}`);
599
+ }
450
600
 
451
601
  const signedAt = Date.now();
452
602
  const clientId = 'cli';
@@ -466,7 +616,7 @@ export class OpenClawGatewayClient {
466
616
  signedAt,
467
617
  token: this.token,
468
618
  nonce: payload.nonce,
469
- });
619
+ }, this.payloadVersionOverride);
470
620
 
471
621
  // Select public key format based on resolved auth profile.
472
622
  const profile = resolveAuthProfile();
@@ -514,9 +664,11 @@ export class OpenClawGatewayClient {
514
664
 
515
665
  // Handle connect response
516
666
  if (msg.type === 'res' && msg.id === 'connect-1') {
517
- this.clearConnectTimeout();
518
667
  if (msg.ok) {
519
- console.log('[openclaw-ws] Authenticated successfully');
668
+ this.clearConnectTimeout();
669
+ const versionUsed = this.payloadVersionOverride
670
+ ?? (resolveAuthProfile().name === 'clawdbot-v1' ? 'v2' : 'v3');
671
+ console.log(`[openclaw-ws] Authenticated successfully (payload=${versionUsed}${this.fallbackAttempted ? ', via fallback' : ''})`);
520
672
  this.authenticated = true;
521
673
  this.consecutiveFailures = 0;
522
674
  this.connectResolve?.();
@@ -525,8 +677,10 @@ export class OpenClawGatewayClient {
525
677
  } else {
526
678
  const errStr = msg.error ? JSON.stringify(msg.error) : 'Authentication rejected';
527
679
  const isPairing = /pairing.required|not.paired/i.test(errStr);
680
+ const isSignatureInvalid = /signature.invalid|device.signature|invalid.signature/i.test(errStr);
528
681
 
529
682
  if (isPairing) {
683
+ this.clearConnectTimeout();
530
684
  const errObj = msg.error as Record<string, unknown> | undefined;
531
685
  const requestId = errObj?.requestId ?? errObj?.request_id ?? '';
532
686
  console.error('[openclaw-ws] Pairing rejected — device is not paired with the OpenClaw gateway.');
@@ -539,8 +693,31 @@ export class OpenClawGatewayClient {
539
693
  : '~/.openclaw/openclaw.json';
540
694
  console.error(`[openclaw-ws] Ensure OPENCLAW_GATEWAY_TOKEN matches ${configHint} gateway.auth.token`);
541
695
  this.pairingRejected = true;
696
+ } else if (isSignatureInvalid && !this.fallbackAttempted) {
697
+ // Signature rejected — try the alternate payload version once.
698
+ // Do NOT clear connect timeout — it protects the fallback attempt too.
699
+ this.authRejectCount++;
700
+ this.authFallbackCount++;
701
+ const profile = resolveAuthProfile();
702
+ const currentVersion = this.payloadVersionOverride
703
+ ?? (profile.name === 'clawdbot-v1' ? 'v2' : 'v3');
704
+ const fallbackVersion: PayloadVersionOverride = currentVersion === 'v2' ? 'v3' : 'v2';
705
+
706
+ console.warn(`[ws-auth] Signature rejected with ${currentVersion} payload — retrying with ${fallbackVersion} fallback (rejects=${this.authRejectCount} fallbacks=${this.authFallbackCount})`);
707
+ this.payloadVersionOverride = fallbackVersion;
708
+ this.fallbackAttempted = true;
709
+
710
+ // Close current WS and reconnect with the alternate payload.
711
+ // Setting this.ws = null ensures the old WS's close/error handlers
712
+ // no-op via the `this.ws !== ws` guard in doConnect().
713
+ try { this.ws?.close(); } catch {}
714
+ this.ws = null;
715
+ setTimeout(() => this.doConnect(), 0);
716
+ return; // Don't reject the connect promise yet — fallback attempt in progress
542
717
  } else {
543
- console.warn(`[openclaw-ws] Auth rejected: ${errStr}`);
718
+ this.clearConnectTimeout();
719
+ this.authRejectCount++;
720
+ console.warn(`[openclaw-ws] Auth rejected (rejects=${this.authRejectCount}): ${errStr}`);
544
721
  }
545
722
 
546
723
  this.connectReject?.(new Error(`OpenClaw gateway auth failed: ${errStr}`));
@@ -576,6 +753,7 @@ export class OpenClawGatewayClient {
576
753
 
577
754
  /** Send a chat.send RPC. Returns true if accepted. */
578
755
  async sendChatMessage(text: string, idempotencyKey?: string): Promise<boolean> {
756
+ if (this.stopped) return false;
579
757
  if (!this.authenticated || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
580
758
  // Try to reconnect
581
759
  try {
@@ -30,8 +30,15 @@ function extractNestedValue(obj: unknown, path: string): unknown {
30
30
  * Set a deeply nested value in an object by dot-separated path, creating
31
31
  * intermediate objects as needed.
32
32
  */
33
+ const DANGEROUS_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
34
+
33
35
  function setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void {
34
36
  const keys = path.split('.');
37
+ for (const key of keys) {
38
+ if (DANGEROUS_KEYS.has(key)) {
39
+ throw new Error(`Refusing to set dangerous key "${key}" in path "${path}"`);
40
+ }
41
+ }
35
42
  let current: Record<string, unknown> = obj;
36
43
  for (let i = 0; i < keys.length - 1; i++) {
37
44
  const key = keys[i];
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/policy",
3
- "version": "3.1.8",
3
+ "version": "3.1.10",
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.8"
25
+ "@agent-relay/config": "3.1.10"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.19.3",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/sdk",
3
- "version": "3.1.8",
3
+ "version": "3.1.10",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -81,7 +81,7 @@
81
81
  "typescript": "^5.7.3"
82
82
  },
83
83
  "dependencies": {
84
- "@agent-relay/config": "3.1.8",
84
+ "@agent-relay/config": "3.1.10",
85
85
  "@relaycast/sdk": "^0.4.0",
86
86
  "yaml": "^2.7.0"
87
87
  }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agent-relay-sdk"
7
- version = "3.1.8"
7
+ version = "3.1.10"
8
8
  description = "Python SDK for Agent Relay workflows"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/telemetry",
3
- "version": "3.1.8",
3
+ "version": "3.1.10",
4
4
  "description": "Anonymous telemetry for Agent Relay usage analytics",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/trajectory",
3
- "version": "3.1.8",
3
+ "version": "3.1.10",
4
4
  "description": "Trajectory integration utilities (trail/PDERO) for Relay",
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.8"
25
+ "@agent-relay/config": "3.1.10"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.19.3",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/user-directory",
3
- "version": "3.1.8",
3
+ "version": "3.1.10",
4
4
  "description": "User directory service for agent-relay (per-user credential storage)",
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/utils": "3.1.8"
25
+ "@agent-relay/utils": "3.1.10"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.19.3",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/utils",
3
- "version": "3.1.8",
3
+ "version": "3.1.10",
4
4
  "description": "Shared utilities for agent-relay: logging, name generation, command resolution, update checking",
5
5
  "type": "module",
6
6
  "main": "dist/cjs/index.js",
@@ -112,7 +112,7 @@
112
112
  "vitest": "^3.2.4"
113
113
  },
114
114
  "dependencies": {
115
- "@agent-relay/config": "3.1.8",
115
+ "@agent-relay/config": "3.1.10",
116
116
  "compare-versions": "^6.1.1"
117
117
  },
118
118
  "publishConfig": {