@mclawnet/agent 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/hub-connection.ts
2
+ import { hostname as osHostname } from "os";
2
3
  import WebSocket from "ws";
3
4
  var DEFAULT_HEARTBEAT_MS = 3e4;
4
5
  var DEFAULT_RECONNECT_MS = 1e3;
@@ -9,22 +10,32 @@ var HubConnection = class {
9
10
  reconnectTimer = null;
10
11
  reconnectDelay;
11
12
  destroyed = false;
13
+ authState = "pending";
14
+ proxySessions = /* @__PURE__ */ new Map();
12
15
  hubUrl;
13
16
  token;
17
+ hostname;
18
+ gatewayPort;
19
+ gatewayToken;
14
20
  heartbeatInterval;
15
21
  maxReconnectDelay;
22
+ /** Agent ID assigned by Hub after successful auth */
23
+ agentId = null;
16
24
  onMessage;
17
- onConnect;
25
+ onConnectCb;
18
26
  onDisconnect;
19
27
  onError;
20
28
  constructor(opts) {
21
29
  this.hubUrl = opts.hubUrl;
22
30
  this.token = opts.token;
31
+ this.hostname = opts.hostname ?? osHostname();
32
+ this.gatewayPort = opts.gatewayPort ?? 18789;
33
+ this.gatewayToken = opts.gatewayToken ?? "";
23
34
  this.heartbeatInterval = opts.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS;
24
35
  this.reconnectDelay = opts.reconnectDelay ?? DEFAULT_RECONNECT_MS;
25
36
  this.maxReconnectDelay = opts.maxReconnectDelay ?? DEFAULT_MAX_RECONNECT_MS;
26
37
  this.onMessage = opts.onMessage;
27
- this.onConnect = opts.onConnect;
38
+ this.onConnectCb = opts.onConnect;
28
39
  this.onDisconnect = opts.onDisconnect;
29
40
  this.onError = opts.onError;
30
41
  }
@@ -33,31 +44,53 @@ var HubConnection = class {
33
44
  return this.ws?.readyState ?? WebSocket.CLOSED;
34
45
  }
35
46
  get isConnected() {
36
- return this.ws?.readyState === WebSocket.OPEN;
47
+ return this.ws?.readyState === WebSocket.OPEN && this.authState === "authenticated";
37
48
  }
38
- /** Open the connection to the hub */
49
+ /** Open the connection to the hub (no token in URL) */
39
50
  connect() {
40
51
  if (this.destroyed) return;
41
52
  this.cleanup();
42
- const url = new URL(this.hubUrl);
43
- url.searchParams.set("token", this.token);
44
- this.ws = new WebSocket(url.toString());
53
+ this.authState = "pending";
54
+ this.ws = new WebSocket(this.hubUrl);
45
55
  this.ws.on("open", () => {
46
56
  this.reconnectDelay = DEFAULT_RECONNECT_MS;
47
- this.startHeartbeat();
48
- this.onConnect?.();
49
57
  });
50
58
  this.ws.on("message", (raw) => {
59
+ let data;
51
60
  try {
52
- const data = JSON.parse(raw.toString());
53
- this.onMessage?.(data);
61
+ data = JSON.parse(raw.toString());
54
62
  } catch {
55
- this.onMessage?.(raw.toString());
63
+ return;
64
+ }
65
+ if (this.authState === "pending" && data.type === "auth_required") {
66
+ this.authState = "authenticating";
67
+ this.ws.send(JSON.stringify({
68
+ type: "auth",
69
+ token: this.token,
70
+ hostname: this.hostname
71
+ }));
72
+ return;
73
+ }
74
+ if (this.authState === "authenticating" && data.type === "registered") {
75
+ this.authState = "authenticated";
76
+ this.agentId = data.agentId ?? null;
77
+ this.startHeartbeat();
78
+ this.onConnectCb?.(this.agentId);
79
+ return;
80
+ }
81
+ if (this.authState === "authenticated") {
82
+ if (this.handleProxyMessage(data)) return;
83
+ this.onMessage?.(data);
56
84
  }
57
85
  });
58
86
  this.ws.on("close", (code, reason) => {
59
87
  this.stopHeartbeat();
88
+ this.authState = "pending";
60
89
  this.onDisconnect?.(code, reason.toString());
90
+ if (code === 4002) {
91
+ console.error("[clawnet] Auth failed \u2014 not reconnecting. Check your token.");
92
+ return;
93
+ }
61
94
  this.scheduleReconnect();
62
95
  });
63
96
  this.ws.on("error", (err) => {
@@ -66,7 +99,7 @@ var HubConnection = class {
66
99
  }
67
100
  /** Send a JSON message to the hub */
68
101
  send(data) {
69
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;
102
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || this.authState !== "authenticated") return false;
70
103
  this.ws.send(JSON.stringify(data));
71
104
  return true;
72
105
  }
@@ -96,6 +129,7 @@ var HubConnection = class {
96
129
  }
97
130
  cleanup() {
98
131
  this.stopHeartbeat();
132
+ this.closeAllProxySessions();
99
133
  if (this.reconnectTimer) {
100
134
  clearTimeout(this.reconnectTimer);
101
135
  this.reconnectTimer = null;
@@ -108,6 +142,96 @@ var HubConnection = class {
108
142
  this.ws = null;
109
143
  }
110
144
  }
145
+ // ── Proxy session management ──────────────────────────────────────
146
+ /**
147
+ * Handle proxy protocol messages from the Hub.
148
+ * Returns true if the message was handled.
149
+ */
150
+ handleProxyMessage(msg) {
151
+ if (msg.type === "proxy.open" && msg.sessionId) {
152
+ this.openLocalGateway(msg.sessionId, msg.gatewayPort ?? this.gatewayPort);
153
+ return true;
154
+ }
155
+ if (msg.type === "proxy.data" && msg.sessionId) {
156
+ const session = this.proxySessions.get(msg.sessionId);
157
+ if (session && session.localWs.readyState === WebSocket.OPEN && msg.data) {
158
+ const patched = this.patchBrowserConnect(msg.data, session);
159
+ session.localWs.send(patched);
160
+ }
161
+ return true;
162
+ }
163
+ if (msg.type === "proxy.close" && msg.sessionId) {
164
+ this.closeProxySession(msg.sessionId);
165
+ return true;
166
+ }
167
+ return false;
168
+ }
169
+ openLocalGateway(sessionId, port) {
170
+ const localWs = new WebSocket(`ws://127.0.0.1:${port}/`, {
171
+ headers: { Origin: `http://127.0.0.1:${port}` }
172
+ });
173
+ const session = {
174
+ sessionId,
175
+ localWs,
176
+ gatewayNonce: null
177
+ };
178
+ this.proxySessions.set(sessionId, session);
179
+ localWs.on("open", () => {
180
+ this.send({ type: "proxy.opened", sessionId });
181
+ });
182
+ localWs.on("message", (raw) => {
183
+ const text = raw.toString();
184
+ try {
185
+ const frame = JSON.parse(text);
186
+ if (frame.type === "evt" && frame.event === "connect.challenge" && frame.payload?.nonce) {
187
+ session.gatewayNonce = frame.payload.nonce;
188
+ }
189
+ } catch {
190
+ }
191
+ this.send({ type: "proxy.data", sessionId, data: text });
192
+ });
193
+ localWs.on("close", () => {
194
+ this.proxySessions.delete(sessionId);
195
+ this.send({ type: "proxy.close", sessionId });
196
+ });
197
+ localWs.on("error", () => {
198
+ this.proxySessions.delete(sessionId);
199
+ this.send({ type: "proxy.close", sessionId });
200
+ });
201
+ }
202
+ /**
203
+ * Intercept browser's "connect" request and patch it for the local gateway:
204
+ * - Inject gateway auth token
205
+ * - Remove `device` object (browser's device identity/signature doesn't apply
206
+ * through the proxy; the gateway will use token-only auth)
207
+ */
208
+ patchBrowserConnect(raw, session) {
209
+ if (!this.gatewayToken) return raw;
210
+ try {
211
+ const frame = JSON.parse(raw);
212
+ if (frame.type === "req" && frame.method === "connect" && frame.params) {
213
+ if (!frame.params.auth) frame.params.auth = {};
214
+ frame.params.auth.token = this.gatewayToken;
215
+ delete frame.params.device;
216
+ return JSON.stringify(frame);
217
+ }
218
+ } catch {
219
+ }
220
+ return raw;
221
+ }
222
+ closeProxySession(sessionId) {
223
+ const session = this.proxySessions.get(sessionId);
224
+ if (!session) return;
225
+ this.proxySessions.delete(sessionId);
226
+ if (session.localWs.readyState === WebSocket.OPEN || session.localWs.readyState === WebSocket.CONNECTING) {
227
+ session.localWs.close();
228
+ }
229
+ }
230
+ closeAllProxySessions() {
231
+ for (const [sessionId] of this.proxySessions) {
232
+ this.closeProxySession(sessionId);
233
+ }
234
+ }
111
235
  };
112
236
 
113
237
  // src/config.ts
@@ -272,6 +396,15 @@ function ensureGatewayToken() {
272
396
  config.gateway.mode = "local";
273
397
  dirty = true;
274
398
  }
399
+ if (!config.gateway.controlUi) config.gateway.controlUi = {};
400
+ if (!config.gateway.controlUi.allowedOrigins) {
401
+ config.gateway.controlUi.allowedOrigins = ["*"];
402
+ dirty = true;
403
+ }
404
+ if (config.gateway.controlUi.allowInsecureAuth === void 0) {
405
+ config.gateway.controlUi.allowInsecureAuth = true;
406
+ dirty = true;
407
+ }
275
408
  if (dirty) {
276
409
  writeFileSync2(file, JSON.stringify(config, null, 2) + "\n");
277
410
  }
@@ -548,6 +681,8 @@ async function startAgent(opts) {
548
681
  const hubConnection = new HubConnection({
549
682
  hubUrl: config.hubUrl,
550
683
  token: config.token,
684
+ gatewayPort: config.port,
685
+ gatewayToken,
551
686
  onConnect: () => {
552
687
  console.log("[clawnet] Connected to Hub");
553
688
  },
package/dist/start.d.ts CHANGED
@@ -3,11 +3,14 @@ import { ChildProcess } from 'node:child_process';
3
3
  interface HubConnectionOptions {
4
4
  hubUrl: string;
5
5
  token: string;
6
+ hostname?: string;
7
+ gatewayPort?: number;
8
+ gatewayToken?: string;
6
9
  heartbeatInterval?: number;
7
10
  reconnectDelay?: number;
8
11
  maxReconnectDelay?: number;
9
12
  onMessage?: (data: unknown) => void;
10
- onConnect?: () => void;
13
+ onConnect?: (agentId: string) => void;
11
14
  onDisconnect?: (code: number, reason: string) => void;
12
15
  onError?: (err: Error) => void;
13
16
  }
@@ -17,19 +20,26 @@ declare class HubConnection {
17
20
  private reconnectTimer;
18
21
  private reconnectDelay;
19
22
  private destroyed;
23
+ private authState;
24
+ private proxySessions;
20
25
  readonly hubUrl: string;
21
26
  readonly token: string;
27
+ readonly hostname: string;
28
+ readonly gatewayPort: number;
29
+ readonly gatewayToken: string;
22
30
  readonly heartbeatInterval: number;
23
31
  readonly maxReconnectDelay: number;
32
+ /** Agent ID assigned by Hub after successful auth */
33
+ agentId: string | null;
24
34
  private onMessage?;
25
- private onConnect?;
35
+ private onConnectCb?;
26
36
  private onDisconnect?;
27
37
  private onError?;
28
38
  constructor(opts: HubConnectionOptions);
29
39
  /** Current WebSocket readyState (or CLOSED if no socket) */
30
40
  get readyState(): number;
31
41
  get isConnected(): boolean;
32
- /** Open the connection to the hub */
42
+ /** Open the connection to the hub (no token in URL) */
33
43
  connect(): void;
34
44
  /** Send a JSON message to the hub */
35
45
  send(data: unknown): boolean;
@@ -39,6 +49,21 @@ declare class HubConnection {
39
49
  private stopHeartbeat;
40
50
  private scheduleReconnect;
41
51
  private cleanup;
52
+ /**
53
+ * Handle proxy protocol messages from the Hub.
54
+ * Returns true if the message was handled.
55
+ */
56
+ private handleProxyMessage;
57
+ private openLocalGateway;
58
+ /**
59
+ * Intercept browser's "connect" request and patch it for the local gateway:
60
+ * - Inject gateway auth token
61
+ * - Remove `device` object (browser's device identity/signature doesn't apply
62
+ * through the proxy; the gateway will use token-only auth)
63
+ */
64
+ private patchBrowserConnect;
65
+ private closeProxySession;
66
+ private closeAllProxySessions;
42
67
  }
43
68
 
44
69
  interface AgentConfig {
package/dist/start.js CHANGED
@@ -148,6 +148,15 @@ function ensureGatewayToken() {
148
148
  config.gateway.mode = "local";
149
149
  dirty = true;
150
150
  }
151
+ if (!config.gateway.controlUi) config.gateway.controlUi = {};
152
+ if (!config.gateway.controlUi.allowedOrigins) {
153
+ config.gateway.controlUi.allowedOrigins = ["*"];
154
+ dirty = true;
155
+ }
156
+ if (config.gateway.controlUi.allowInsecureAuth === void 0) {
157
+ config.gateway.controlUi.allowInsecureAuth = true;
158
+ dirty = true;
159
+ }
151
160
  if (dirty) {
152
161
  writeFileSync2(file, JSON.stringify(config, null, 2) + "\n");
153
162
  }
@@ -382,6 +391,7 @@ function promptChoice(max) {
382
391
  }
383
392
 
384
393
  // src/hub-connection.ts
394
+ import { hostname as osHostname } from "os";
385
395
  import WebSocket from "ws";
386
396
  var DEFAULT_HEARTBEAT_MS = 3e4;
387
397
  var DEFAULT_RECONNECT_MS = 1e3;
@@ -392,22 +402,32 @@ var HubConnection = class {
392
402
  reconnectTimer = null;
393
403
  reconnectDelay;
394
404
  destroyed = false;
405
+ authState = "pending";
406
+ proxySessions = /* @__PURE__ */ new Map();
395
407
  hubUrl;
396
408
  token;
409
+ hostname;
410
+ gatewayPort;
411
+ gatewayToken;
397
412
  heartbeatInterval;
398
413
  maxReconnectDelay;
414
+ /** Agent ID assigned by Hub after successful auth */
415
+ agentId = null;
399
416
  onMessage;
400
- onConnect;
417
+ onConnectCb;
401
418
  onDisconnect;
402
419
  onError;
403
420
  constructor(opts) {
404
421
  this.hubUrl = opts.hubUrl;
405
422
  this.token = opts.token;
423
+ this.hostname = opts.hostname ?? osHostname();
424
+ this.gatewayPort = opts.gatewayPort ?? 18789;
425
+ this.gatewayToken = opts.gatewayToken ?? "";
406
426
  this.heartbeatInterval = opts.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS;
407
427
  this.reconnectDelay = opts.reconnectDelay ?? DEFAULT_RECONNECT_MS;
408
428
  this.maxReconnectDelay = opts.maxReconnectDelay ?? DEFAULT_MAX_RECONNECT_MS;
409
429
  this.onMessage = opts.onMessage;
410
- this.onConnect = opts.onConnect;
430
+ this.onConnectCb = opts.onConnect;
411
431
  this.onDisconnect = opts.onDisconnect;
412
432
  this.onError = opts.onError;
413
433
  }
@@ -416,31 +436,53 @@ var HubConnection = class {
416
436
  return this.ws?.readyState ?? WebSocket.CLOSED;
417
437
  }
418
438
  get isConnected() {
419
- return this.ws?.readyState === WebSocket.OPEN;
439
+ return this.ws?.readyState === WebSocket.OPEN && this.authState === "authenticated";
420
440
  }
421
- /** Open the connection to the hub */
441
+ /** Open the connection to the hub (no token in URL) */
422
442
  connect() {
423
443
  if (this.destroyed) return;
424
444
  this.cleanup();
425
- const url = new URL(this.hubUrl);
426
- url.searchParams.set("token", this.token);
427
- this.ws = new WebSocket(url.toString());
445
+ this.authState = "pending";
446
+ this.ws = new WebSocket(this.hubUrl);
428
447
  this.ws.on("open", () => {
429
448
  this.reconnectDelay = DEFAULT_RECONNECT_MS;
430
- this.startHeartbeat();
431
- this.onConnect?.();
432
449
  });
433
450
  this.ws.on("message", (raw) => {
451
+ let data;
434
452
  try {
435
- const data = JSON.parse(raw.toString());
436
- this.onMessage?.(data);
453
+ data = JSON.parse(raw.toString());
437
454
  } catch {
438
- this.onMessage?.(raw.toString());
455
+ return;
456
+ }
457
+ if (this.authState === "pending" && data.type === "auth_required") {
458
+ this.authState = "authenticating";
459
+ this.ws.send(JSON.stringify({
460
+ type: "auth",
461
+ token: this.token,
462
+ hostname: this.hostname
463
+ }));
464
+ return;
465
+ }
466
+ if (this.authState === "authenticating" && data.type === "registered") {
467
+ this.authState = "authenticated";
468
+ this.agentId = data.agentId ?? null;
469
+ this.startHeartbeat();
470
+ this.onConnectCb?.(this.agentId);
471
+ return;
472
+ }
473
+ if (this.authState === "authenticated") {
474
+ if (this.handleProxyMessage(data)) return;
475
+ this.onMessage?.(data);
439
476
  }
440
477
  });
441
478
  this.ws.on("close", (code, reason) => {
442
479
  this.stopHeartbeat();
480
+ this.authState = "pending";
443
481
  this.onDisconnect?.(code, reason.toString());
482
+ if (code === 4002) {
483
+ console.error("[clawnet] Auth failed \u2014 not reconnecting. Check your token.");
484
+ return;
485
+ }
444
486
  this.scheduleReconnect();
445
487
  });
446
488
  this.ws.on("error", (err) => {
@@ -449,7 +491,7 @@ var HubConnection = class {
449
491
  }
450
492
  /** Send a JSON message to the hub */
451
493
  send(data) {
452
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;
494
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || this.authState !== "authenticated") return false;
453
495
  this.ws.send(JSON.stringify(data));
454
496
  return true;
455
497
  }
@@ -479,6 +521,7 @@ var HubConnection = class {
479
521
  }
480
522
  cleanup() {
481
523
  this.stopHeartbeat();
524
+ this.closeAllProxySessions();
482
525
  if (this.reconnectTimer) {
483
526
  clearTimeout(this.reconnectTimer);
484
527
  this.reconnectTimer = null;
@@ -491,6 +534,96 @@ var HubConnection = class {
491
534
  this.ws = null;
492
535
  }
493
536
  }
537
+ // ── Proxy session management ──────────────────────────────────────
538
+ /**
539
+ * Handle proxy protocol messages from the Hub.
540
+ * Returns true if the message was handled.
541
+ */
542
+ handleProxyMessage(msg) {
543
+ if (msg.type === "proxy.open" && msg.sessionId) {
544
+ this.openLocalGateway(msg.sessionId, msg.gatewayPort ?? this.gatewayPort);
545
+ return true;
546
+ }
547
+ if (msg.type === "proxy.data" && msg.sessionId) {
548
+ const session = this.proxySessions.get(msg.sessionId);
549
+ if (session && session.localWs.readyState === WebSocket.OPEN && msg.data) {
550
+ const patched = this.patchBrowserConnect(msg.data, session);
551
+ session.localWs.send(patched);
552
+ }
553
+ return true;
554
+ }
555
+ if (msg.type === "proxy.close" && msg.sessionId) {
556
+ this.closeProxySession(msg.sessionId);
557
+ return true;
558
+ }
559
+ return false;
560
+ }
561
+ openLocalGateway(sessionId, port) {
562
+ const localWs = new WebSocket(`ws://127.0.0.1:${port}/`, {
563
+ headers: { Origin: `http://127.0.0.1:${port}` }
564
+ });
565
+ const session = {
566
+ sessionId,
567
+ localWs,
568
+ gatewayNonce: null
569
+ };
570
+ this.proxySessions.set(sessionId, session);
571
+ localWs.on("open", () => {
572
+ this.send({ type: "proxy.opened", sessionId });
573
+ });
574
+ localWs.on("message", (raw) => {
575
+ const text = raw.toString();
576
+ try {
577
+ const frame = JSON.parse(text);
578
+ if (frame.type === "evt" && frame.event === "connect.challenge" && frame.payload?.nonce) {
579
+ session.gatewayNonce = frame.payload.nonce;
580
+ }
581
+ } catch {
582
+ }
583
+ this.send({ type: "proxy.data", sessionId, data: text });
584
+ });
585
+ localWs.on("close", () => {
586
+ this.proxySessions.delete(sessionId);
587
+ this.send({ type: "proxy.close", sessionId });
588
+ });
589
+ localWs.on("error", () => {
590
+ this.proxySessions.delete(sessionId);
591
+ this.send({ type: "proxy.close", sessionId });
592
+ });
593
+ }
594
+ /**
595
+ * Intercept browser's "connect" request and patch it for the local gateway:
596
+ * - Inject gateway auth token
597
+ * - Remove `device` object (browser's device identity/signature doesn't apply
598
+ * through the proxy; the gateway will use token-only auth)
599
+ */
600
+ patchBrowserConnect(raw, session) {
601
+ if (!this.gatewayToken) return raw;
602
+ try {
603
+ const frame = JSON.parse(raw);
604
+ if (frame.type === "req" && frame.method === "connect" && frame.params) {
605
+ if (!frame.params.auth) frame.params.auth = {};
606
+ frame.params.auth.token = this.gatewayToken;
607
+ delete frame.params.device;
608
+ return JSON.stringify(frame);
609
+ }
610
+ } catch {
611
+ }
612
+ return raw;
613
+ }
614
+ closeProxySession(sessionId) {
615
+ const session = this.proxySessions.get(sessionId);
616
+ if (!session) return;
617
+ this.proxySessions.delete(sessionId);
618
+ if (session.localWs.readyState === WebSocket.OPEN || session.localWs.readyState === WebSocket.CONNECTING) {
619
+ session.localWs.close();
620
+ }
621
+ }
622
+ closeAllProxySessions() {
623
+ for (const [sessionId] of this.proxySessions) {
624
+ this.closeProxySession(sessionId);
625
+ }
626
+ }
494
627
  };
495
628
 
496
629
  // src/start.ts
@@ -536,6 +669,8 @@ async function startAgent(opts) {
536
669
  const hubConnection = new HubConnection({
537
670
  hubUrl: config.hubUrl,
538
671
  token: config.token,
672
+ gatewayPort: config.port,
673
+ gatewayToken,
539
674
  onConnect: () => {
540
675
  console.log("[clawnet] Connected to Hub");
541
676
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mclawnet/agent",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "ClawNet Agent — Run OpenClaw with Hub connectivity",
5
5
  "license": "MIT",
6
6
  "type": "module",