@omercnet/paseo-shared-browser 0.3.1-next.72.1

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.
@@ -0,0 +1,402 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { createConnection, type Socket } from "node:net";
3
+ import { randomUUID } from "node:crypto";
4
+ import {
5
+ RUNTIME_PROTOCOL_VERSION,
6
+ RuntimeProtocolError,
7
+ isRuntimeResponse,
8
+ type AgentBrowserOperation,
9
+ type BridgeLease,
10
+ type JsonValue,
11
+ type RuntimeDescriptor,
12
+ type RuntimeRequest,
13
+ type RuntimeResponse,
14
+ type RuntimeResult,
15
+ } from "./runtime-protocol";
16
+ import { resolveSupervisorPaths, type SupervisorPaths } from "./supervisor";
17
+
18
+ interface PendingRequest {
19
+ resolve(value: RuntimeResult): void;
20
+ reject(error: Error): void;
21
+ }
22
+
23
+ interface EndpointFile {
24
+ version: number;
25
+ socket: string;
26
+ }
27
+ type ClientRuntimeRequest<Request extends RuntimeRequest = RuntimeRequest> =
28
+ Request extends RuntimeRequest ? Omit<Request, "id" | "token" | "version"> : never;
29
+
30
+ export interface SupervisorClientOptions {
31
+ bridgeId: string;
32
+ paths?: SupervisorPaths;
33
+ takeover?: boolean;
34
+ }
35
+
36
+ export class SupervisorClient {
37
+ private readonly bridgeId: string;
38
+ private readonly paths: SupervisorPaths;
39
+ private readonly pending = new Map<string, PendingRequest>();
40
+ private socket: Socket | null = null;
41
+ private token = "";
42
+ private epoch = 0;
43
+ private heartbeatTimer: NodeJS.Timeout | null = null;
44
+ private receiveBuffer = "";
45
+ private readonly takeover: boolean;
46
+ private reconnecting: Promise<BridgeLease> | null = null;
47
+ private lease: BridgeLease | null = null;
48
+ private closed = false;
49
+
50
+ constructor(options: SupervisorClientOptions) {
51
+ this.bridgeId = options.bridgeId;
52
+ this.paths = options.paths ?? resolveSupervisorPaths();
53
+ this.takeover = options.takeover === true;
54
+ }
55
+
56
+ async connect(): Promise<BridgeLease> {
57
+ if (this.socket && !this.socket.destroyed && this.lease) return this.lease;
58
+ if (!this.reconnecting) {
59
+ this.closed = false;
60
+ this.reconnecting = this.connectOnce();
61
+ }
62
+ try {
63
+ return await this.reconnecting;
64
+ } catch (error) {
65
+ this.socket?.destroy();
66
+ this.socket = null;
67
+ this.epoch = 0;
68
+ this.lease = null;
69
+ throw error;
70
+ } finally {
71
+ this.reconnecting = null;
72
+ }
73
+ }
74
+
75
+ private async connectOnce(): Promise<BridgeLease> {
76
+ const [endpointText, token] = await Promise.all([
77
+ readPrivateFile(this.paths.endpoint),
78
+ readPrivateFile(this.paths.token),
79
+ ]);
80
+ const parsed: unknown = JSON.parse(endpointText);
81
+ if (
82
+ typeof parsed !== "object" ||
83
+ parsed === null ||
84
+ !("version" in parsed) ||
85
+ !("socket" in parsed)
86
+ ) {
87
+ throw new Error("Invalid supervisor endpoint file");
88
+ }
89
+ const endpoint = parsed as EndpointFile;
90
+ if (endpoint.version !== RUNTIME_PROTOCOL_VERSION || typeof endpoint.socket !== "string")
91
+ throw new Error("Supervisor endpoint protocol mismatch");
92
+ this.token = token.trim();
93
+ this.socket = await new Promise<Socket>((resolve, reject) => {
94
+ const socket = createConnection(endpoint.socket);
95
+ socket.once("connect", () => resolve(socket));
96
+ socket.once("error", reject);
97
+ });
98
+ this.socket.setEncoding("utf8");
99
+ this.socket.on("data", (chunk: string) => this.consume(chunk));
100
+ this.socket.once("close", () => this.handleClose(new Error("Supervisor connection closed")));
101
+ this.socket.on("error", (error) => this.handleClose(error));
102
+ const lease = (await this.send({
103
+ method: "bridge.claim",
104
+ bridgeId: this.bridgeId,
105
+ takeover: this.takeover,
106
+ })) as BridgeLease;
107
+ this.epoch = lease.epoch;
108
+ this.lease = lease;
109
+ this.armHeartbeat(lease.heartbeatIntervalMs);
110
+ return lease;
111
+ }
112
+
113
+ async ensureWorkspace(workspaceId: string): Promise<RuntimeDescriptor> {
114
+ await this.ensureLease();
115
+ return (await this.send({
116
+ method: "workspace.ensure",
117
+ bridgeId: this.bridgeId,
118
+ epoch: this.epoch,
119
+ workspaceId,
120
+ })) as RuntimeDescriptor;
121
+ }
122
+
123
+ async requestWorkspace(
124
+ workspaceId: string,
125
+ operation: string,
126
+ input: JsonValue,
127
+ ): Promise<JsonValue> {
128
+ await this.ensureLease();
129
+ return (await this.send({
130
+ method: "workspace.request",
131
+ bridgeId: this.bridgeId,
132
+ epoch: this.epoch,
133
+ workspaceId,
134
+ operation,
135
+ input,
136
+ })) as JsonValue;
137
+ }
138
+
139
+ async archiveWorkspace(workspaceId: string): Promise<void> {
140
+ await this.ensureLease();
141
+ await this.send({
142
+ method: "workspace.archive",
143
+ bridgeId: this.bridgeId,
144
+ epoch: this.epoch,
145
+ workspaceId,
146
+ });
147
+ }
148
+ async requestBrowser<Result = JsonValue>(operation: string, input: JsonValue): Promise<Result> {
149
+ await this.ensureLease();
150
+ return (await this.send({
151
+ method: "browser.request",
152
+ bridgeId: this.bridgeId,
153
+ epoch: this.epoch,
154
+ operation,
155
+ input,
156
+ })) as unknown as Result;
157
+ }
158
+
159
+ async issueAgentTicket(ticket: string): Promise<void> {
160
+ await this.ensureLease();
161
+ await this.send({
162
+ method: "ticket.issue",
163
+ bridgeId: this.bridgeId,
164
+ epoch: this.epoch,
165
+ ticket,
166
+ });
167
+ }
168
+
169
+ async bindAgentTicket(ticket: string, agentId: string, workspaceId: string): Promise<void> {
170
+ await this.ensureLease();
171
+ await this.send({
172
+ method: "ticket.bind",
173
+ bridgeId: this.bridgeId,
174
+ epoch: this.epoch,
175
+ ticket,
176
+ agentId,
177
+ workspaceId,
178
+ });
179
+ }
180
+
181
+ async revokeAgent(agentId: string): Promise<void> {
182
+ await this.ensureLease();
183
+ await this.send({
184
+ method: "agent.revoke",
185
+ bridgeId: this.bridgeId,
186
+ epoch: this.epoch,
187
+ agentId,
188
+ });
189
+ }
190
+
191
+ disconnect(): void {
192
+ this.closed = true;
193
+ this.clearHeartbeat();
194
+ this.socket?.destroy();
195
+ this.lease = null;
196
+ this.socket = null;
197
+ }
198
+
199
+ private async send(request: ClientRuntimeRequest): Promise<RuntimeResult> {
200
+ const socket = this.socket;
201
+ if (!socket || socket.destroyed) throw new Error("Supervisor client is not connected");
202
+ const id = randomUUID();
203
+ const message = {
204
+ ...request,
205
+ id,
206
+ token: this.token,
207
+ version: RUNTIME_PROTOCOL_VERSION,
208
+ } as RuntimeRequest;
209
+ const result = new Promise<RuntimeResult>((resolve, reject) =>
210
+ this.pending.set(id, { resolve, reject }),
211
+ );
212
+ socket.write(`${JSON.stringify(message)}\n`);
213
+ return await result;
214
+ }
215
+
216
+ private consume(chunk: string): void {
217
+ this.receiveBuffer += chunk;
218
+ let newline = this.receiveBuffer.indexOf("\n");
219
+ while (newline >= 0) {
220
+ const line = this.receiveBuffer.slice(0, newline);
221
+ this.receiveBuffer = this.receiveBuffer.slice(newline + 1);
222
+ if (line.length > 0) {
223
+ let parsed: unknown;
224
+ try {
225
+ parsed = JSON.parse(line);
226
+ } catch {
227
+ this.handleClose(new Error("Supervisor returned invalid JSON"));
228
+ return;
229
+ }
230
+ if (!isRuntimeResponse(parsed)) {
231
+ this.handleClose(new Error("Supervisor returned an invalid response"));
232
+ return;
233
+ }
234
+ this.settle(parsed);
235
+ }
236
+ newline = this.receiveBuffer.indexOf("\n");
237
+ }
238
+ }
239
+
240
+ private settle(response: RuntimeResponse): void {
241
+ const pending = this.pending.get(response.id);
242
+ if (!pending) return;
243
+ this.pending.delete(response.id);
244
+ if (response.ok) pending.resolve(response.result);
245
+ else pending.reject(new RuntimeProtocolError(response.error.code, response.error.message));
246
+ }
247
+
248
+ private armHeartbeat(intervalMs: number): void {
249
+ this.clearHeartbeat();
250
+ this.heartbeatTimer = setInterval(() => {
251
+ void this.send({ method: "bridge.heartbeat", bridgeId: this.bridgeId, epoch: this.epoch })
252
+ .then((result) => {
253
+ const lease = result as BridgeLease;
254
+ this.epoch = lease.epoch;
255
+ this.lease = lease;
256
+ })
257
+ .catch((error: unknown) =>
258
+ this.handleClose(
259
+ error instanceof Error ? error : new Error("Supervisor heartbeat failed"),
260
+ ),
261
+ );
262
+ }, intervalMs);
263
+ this.heartbeatTimer.unref();
264
+ }
265
+
266
+ private clearHeartbeat(): void {
267
+ if (!this.heartbeatTimer) return;
268
+ clearInterval(this.heartbeatTimer);
269
+ this.heartbeatTimer = null;
270
+ }
271
+
272
+ private async ensureLease(): Promise<void> {
273
+ if (this.epoch !== 0 && this.socket && !this.socket.destroyed) return;
274
+ await this.connect();
275
+ }
276
+
277
+ private handleClose(error: Error): void {
278
+ if (this.closed && this.pending.size === 0) return;
279
+ this.clearHeartbeat();
280
+ this.socket?.destroy();
281
+ this.socket = null;
282
+ this.epoch = 0;
283
+ this.lease = null;
284
+ for (const pending of this.pending.values()) pending.reject(error);
285
+ this.pending.clear();
286
+ }
287
+ }
288
+
289
+ export interface AgentSupervisorClientOptions {
290
+ ticket: string;
291
+ paths?: SupervisorPaths;
292
+ }
293
+
294
+ export class AgentSupervisorClient {
295
+ private readonly ticket: string;
296
+ private readonly paths: SupervisorPaths;
297
+ private readonly pending = new Map<string, PendingRequest>();
298
+ private socket: Socket | null = null;
299
+ private receiveBuffer = "";
300
+ private closed = false;
301
+
302
+ constructor(options: AgentSupervisorClientOptions) {
303
+ this.ticket = options.ticket;
304
+ this.paths = options.paths ?? resolveSupervisorPaths();
305
+ }
306
+
307
+ async open(): Promise<void> {
308
+ if (this.socket) throw new Error("Agent supervisor client is already connected");
309
+ this.closed = false;
310
+ const endpointText = await readPrivateFile(this.paths.endpoint);
311
+ const parsed: unknown = JSON.parse(endpointText);
312
+ if (
313
+ typeof parsed !== "object" ||
314
+ parsed === null ||
315
+ !("version" in parsed) ||
316
+ !("socket" in parsed)
317
+ )
318
+ throw new Error("Invalid supervisor endpoint file");
319
+ const endpoint = parsed as EndpointFile;
320
+ if (endpoint.version !== RUNTIME_PROTOCOL_VERSION || typeof endpoint.socket !== "string")
321
+ throw new Error("Supervisor endpoint protocol mismatch");
322
+ this.socket = await new Promise<Socket>((resolve, reject) => {
323
+ const socket = createConnection(endpoint.socket);
324
+ socket.once("connect", () => resolve(socket));
325
+ socket.once("error", reject);
326
+ });
327
+ this.socket.setEncoding("utf8");
328
+ this.socket.on("data", (chunk: string) => this.consume(chunk));
329
+ this.socket.once("close", () => this.handleClose(new Error("Supervisor connection closed")));
330
+ this.socket.on("error", (error) => this.handleClose(error));
331
+ }
332
+ async request(operation: AgentBrowserOperation, input: JsonValue): Promise<JsonValue> {
333
+ const socket = this.socket;
334
+ if (!socket || socket.destroyed) throw new Error("Agent supervisor client is not connected");
335
+ const id = randomUUID();
336
+ const message: RuntimeRequest = {
337
+ id,
338
+ version: RUNTIME_PROTOCOL_VERSION,
339
+ method: "agent.request",
340
+ ticket: this.ticket,
341
+ operation,
342
+ input,
343
+ };
344
+ const result = new Promise<RuntimeResult>((resolve, reject) =>
345
+ this.pending.set(id, { resolve, reject }),
346
+ );
347
+ socket.write(`${JSON.stringify(message)}\n`);
348
+ return (await result) as JsonValue;
349
+ }
350
+
351
+ disconnect(): void {
352
+ this.closed = true;
353
+ this.socket?.destroy();
354
+ this.socket = null;
355
+ }
356
+
357
+ private consume(chunk: string): void {
358
+ this.receiveBuffer += chunk;
359
+ let newline = this.receiveBuffer.indexOf("\n");
360
+ while (newline >= 0) {
361
+ const line = this.receiveBuffer.slice(0, newline);
362
+ this.receiveBuffer = this.receiveBuffer.slice(newline + 1);
363
+ if (line.length > 0) {
364
+ let parsed: unknown;
365
+ try {
366
+ parsed = JSON.parse(line);
367
+ } catch {
368
+ this.handleClose(new Error("Supervisor returned invalid JSON"));
369
+ return;
370
+ }
371
+ if (!isRuntimeResponse(parsed)) {
372
+ this.handleClose(new Error("Supervisor returned an invalid response"));
373
+ return;
374
+ }
375
+ const pending = this.pending.get(parsed.id);
376
+ if (pending) {
377
+ this.pending.delete(parsed.id);
378
+ if (parsed.ok) pending.resolve(parsed.result);
379
+ else pending.reject(new RuntimeProtocolError(parsed.error.code, parsed.error.message));
380
+ }
381
+ }
382
+ newline = this.receiveBuffer.indexOf("\n");
383
+ }
384
+ }
385
+
386
+ private handleClose(error: Error): void {
387
+ if (this.closed && this.pending.size === 0) return;
388
+ this.socket?.destroy();
389
+ this.socket = null;
390
+ for (const pending of this.pending.values()) pending.reject(error);
391
+ this.pending.clear();
392
+ }
393
+ }
394
+
395
+ async function readPrivateFile(path: string): Promise<string> {
396
+ const metadata = await stat(path);
397
+ if ((metadata.mode & 0o077) !== 0) throw new Error(`Supervisor file is not private: ${path}`);
398
+ if (typeof process.getuid === "function" && metadata.uid !== process.getuid()) {
399
+ throw new Error(`Supervisor file is owned by another user: ${path}`);
400
+ }
401
+ return await readFile(path, "utf8");
402
+ }
@@ -0,0 +1,9 @@
1
+ import { createRuntimeOwner } from "./runtime-owner";
2
+ import { runStandaloneSupervisor } from "./supervisor";
3
+
4
+ void createRuntimeOwner()
5
+ .then(runStandaloneSupervisor)
6
+ .catch((error: unknown) => {
7
+ console.error(error);
8
+ process.exitCode = 1;
9
+ });