agent-yes 1.302.2 → 1.303.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.
Files changed (47) hide show
  1. package/dist/{SUPPORTED_CLIS-C5Uhdl61.js → SUPPORTED_CLIS-BvWBFlrN.js} +3 -3
  2. package/dist/{SUPPORTED_CLIS-FBshN0v2.js → SUPPORTED_CLIS-CqmE-ESB.js} +2 -2
  3. package/dist/{agentShare-CNgCQMY9.js → agentShare-Gb4vxJzE.js} +4 -4
  4. package/dist/{callback-ayc9yXqJ.js → callback-CuIY-75l.js} +3 -3
  5. package/dist/{callback-CbZ-H_Pn.js → callback-ZzRPkYjf.js} +4 -4
  6. package/dist/cli.js +5 -5
  7. package/dist/{cmdPs-CDj-l5Pk.js → cmdPs-DSKYjOKS.js} +4 -4
  8. package/dist/index.js +2 -2
  9. package/dist/{notifyDaemon-DZbd5fTj.js → notifyDaemon-C_gTWqYZ.js} +4 -4
  10. package/dist/{parentPingLoop-Bim6u0OH.js → parentPingLoop-BhVR1EQm.js} +2 -2
  11. package/dist/{remotes-ByjpYjQ9.js → remotes-C36aI8Ty.js} +2 -2
  12. package/dist/{remotes-D10E_xUn.js → remotes-COBEhmlr.js} +6 -6
  13. package/dist/{rustBinary-DklBSefL.js → rustBinary-Ep0x7WrS.js} +2 -2
  14. package/dist/{rustBinary-Btf18sk5.js → rustBinary-eOcvZ5yS.js} +2 -2
  15. package/dist/{schedule-DzqTosvJ.js → schedule-E2QlsME5.js} +4 -4
  16. package/dist/{serve-DdHbUWtG.js → serve-BWy7zL7j.js} +43 -26
  17. package/dist/{serveRust-BEzTqFRy.js → serveRust-BgY5Te5E.js} +50 -6
  18. package/dist/{setup-CzKkVYp8.js → setup-CD9-RyhT.js} +2 -2
  19. package/dist/{shareCmd-DinezAHY.js → shareCmd-Drxb-SjM.js} +7 -7
  20. package/dist/{subcommands-oIARj_mJ.js → subcommands-C627cuOl.js} +3 -3
  21. package/dist/{subcommands-fX4Ygx3a.js → subcommands-Cc6bmEnB.js} +30 -27
  22. package/dist/{terminal-DlW0KNym.js → terminal-BddOL2fx.js} +2 -2
  23. package/dist/trayApp-CdLbJK5F.js +5 -0
  24. package/dist/{trayApp-DWVdr0Pr.js → trayApp-D0UYVT6n.js} +2 -2
  25. package/dist/{ts-D29CMeWZ.js → ts-Bm6FhYS4.js} +3 -3
  26. package/dist/{versionChecker-DrjMn-XI.js → versionChecker-CFyZKym2.js} +2 -2
  27. package/dist/webrtcLink-CHLgPKWj.js +4 -0
  28. package/dist/{webrtcLink-CKnjNGaV.js → webrtcLink-bWxiCBTV.js} +26 -9
  29. package/dist/webrtcRemote-RmkbzY6Y.js +683 -0
  30. package/dist/{widget-CvcvPWF9.js → widget-C6-P26Ue.js} +3 -3
  31. package/dist/{ws-Cbx5sfsd.js → ws-DCcdDRXH.js} +2 -2
  32. package/dist/{ws-DDB8jh0f.js → ws-DmiSL50p.js} +4 -4
  33. package/lab/ui/index.html +71 -3
  34. package/package.json +1 -1
  35. package/ts/remotes.ts +11 -4
  36. package/ts/serve.ts +24 -2
  37. package/ts/serveRust.spec.ts +23 -6
  38. package/ts/serveRust.ts +34 -7
  39. package/ts/subcommands.ts +4 -1
  40. package/ts/webrtcLink.spec.ts +26 -1
  41. package/ts/webrtcLink.ts +32 -5
  42. package/ts/webrtcRemote.ts +406 -102
  43. package/ts/webrtcSignal.spec.ts +40 -0
  44. package/ts/webrtcSignal.ts +59 -3
  45. package/dist/trayApp-F_Xs3C2N.js +0 -5
  46. package/dist/webrtcLink-CAdvnRM0.js +0 -4
  47. package/dist/webrtcRemote-BYy2lTTl.js +0 -408
@@ -0,0 +1,683 @@
1
+ import { a as computeTranscriptHash, c as open, d as randomHex, f as seal, l as packEnvelope, n as FLAG_CONFIRM, o as deriveAuthToken, p as unpackEnvelope, s as deriveDirKeys } from "./e2e-BdLf8DvY.js";
2
+ import { a as parseWebrtcLink, i as isWebrtcSpec, n as SIGNAL_SUBPROTOCOL } from "./webrtcLink-bWxiCBTV.js";
3
+ import { hostname } from "os";
4
+ import { RTCPeerConnection } from "node-datachannel/polyfill";
5
+
6
+ //#region ts/signalingHint.ts
7
+ /**
8
+ * Reach the signaling host over ordinary HTTPS. ANY HTTP response counts as
9
+ * reachable — a 404 or a 400 still proves egress, TLS and DNS all worked, which
10
+ * is the whole question. Never throws; a failure IS the answer.
11
+ */
12
+ async function probeSignalingHost(host, timeoutMs = 4e3, doFetch = fetch) {
13
+ try {
14
+ await doFetch(`https://${host}/`, {
15
+ method: "HEAD",
16
+ signal: AbortSignal.timeout(timeoutMs),
17
+ redirect: "manual"
18
+ });
19
+ return "https-ok";
20
+ } catch {
21
+ return "https-failed";
22
+ }
23
+ }
24
+ /**
25
+ * The operator-facing explanation, given the original error and what the probe
26
+ * found. Pure, so the wording is testable without a network.
27
+ *
28
+ * Every branch keeps the original error text: it is the only part that is
29
+ * certainly true, and a hint that replaces evidence is worse than no hint.
30
+ */
31
+ function signalingFailureHint(cause, probe, host) {
32
+ const head = `cannot reach the signaling host ${host}: ${cause}`;
33
+ switch (probe) {
34
+ case "https-ok": return `${head}\n HTTPS to ${host} works from here, so DNS, TLS and egress are fine and the\n server is up — only the WebSocket did not connect. Something in between is\n allowing HTTPS and refusing the upgrade, which is what a coding-agent\n sandbox or an intercepting proxy does.\n If you are running inside a sandbox (Codex and similar): re-run with\n escalated / external-network permission. If you are behind a corporate\n proxy: it must allow WebSocket upgrades to ${host}.`;
35
+ case "https-failed": return `${head}\n HTTPS to ${host} does not work from here either, so this is not specific to\n WebSockets — either this environment has no outbound network, or the host\n is genuinely unreachable.\n If you are running inside a sandbox (Codex and similar): re-run with\n escalated / external-network permission. Otherwise check connectivity and\n whether ${host} is up.`;
36
+ default: return `${head}\n If you are running inside a coding-agent sandbox, outbound WebSockets are\n often blocked — re-run with escalated / external-network permission.`;
37
+ }
38
+ }
39
+
40
+ //#endregion
41
+ //#region ts/webrtcSignal.ts
42
+ var ConnectError = class extends Error {
43
+ constructor(phase, message) {
44
+ super(message);
45
+ this.phase = phase;
46
+ this.name = "ConnectError";
47
+ }
48
+ };
49
+ /** Classify one signaling frame against the current state. */
50
+ function reduceSignal(state, m) {
51
+ if (!m || typeof m !== "object") return { kind: "ignore" };
52
+ switch (m.type) {
53
+ case "welcome":
54
+ if (m.hostPresent === false) return {
55
+ kind: "fail",
56
+ error: new ConnectError("no-host", "the room is open but no host is attached to it")
57
+ };
58
+ return { kind: "ignore" };
59
+ case "offer":
60
+ if (state.offered) return { kind: "duplicate-offer" };
61
+ return {
62
+ kind: "offer",
63
+ sdp: m.sdp,
64
+ iceServers: m.iceServers || [],
65
+ from: m.from
66
+ };
67
+ case "candidate": return {
68
+ kind: "candidate",
69
+ candidate: m.candidate,
70
+ queue: !state.offered
71
+ };
72
+ default: return { kind: "ignore" };
73
+ }
74
+ }
75
+ /** The error for a socket that closed before the channel was confirmed. */
76
+ function closeError(state, code, reason) {
77
+ const why = `signaling closed (code ${code}${reason ? `: ${reason}` : ""})`;
78
+ if (!state.opened) return new ConnectError("ws-not-open", `${why} before the socket opened`);
79
+ if (code === 1008 || reason) return new ConnectError("rejected", why);
80
+ if (!state.offered) return new ConnectError("no-host", `${why} before the host offered`);
81
+ return new ConnectError("ice-timeout", `${why} during the WebRTC handshake`);
82
+ }
83
+ /** The error for the connect deadline expiring. */
84
+ function timeoutError(state, timeoutMs) {
85
+ const s = `${Math.round(timeoutMs / 1e3)}s`;
86
+ if (!state.opened) return new ConnectError("ws-not-open", `signaling did not open within ${s}`);
87
+ if (!state.offered) return new ConnectError("no-host", `no host answered the join within ${s}`);
88
+ return new ConnectError("ice-timeout", `no WebRTC data channel came up within ${s}`);
89
+ }
90
+ /** The error for a WebSocket `error` event. */
91
+ function socketError(state, detail) {
92
+ return state.opened ? new ConnectError("rejected", `signaling error after connect: ${detail}`) : new ConnectError("ws-not-open", `signaling error: ${detail}`);
93
+ }
94
+ /**
95
+ * The operator-facing explanation. `sandboxHint` is the HTTPS-probe-derived
96
+ * note from signalingHint.ts and is consulted ONLY for a socket that never
97
+ * opened: every later phase proves the WebSocket upgrade worked, so blaming a
98
+ * sandbox or proxy there would be wrong — and sent one reader down exactly
99
+ * that path.
100
+ */
101
+ function describeConnectFailure(err, host, sandboxHint) {
102
+ const phase = err instanceof ConnectError ? err.phase : "ws-not-open";
103
+ const cause = err.message;
104
+ switch (phase) {
105
+ case "no-host": return Promise.resolve(`no host in the room at ${host}: ${cause}\n The signaling server is reachable and accepted the join — the daemon on the\n other machine is not attached to this room. There, run \`ay share status\`:\n if it prints a different link, the room rotated (or the daemon was\n reinstalled) and this saved remote is stale — re-add it with the new link.`);
106
+ case "rejected": return Promise.resolve(`join rejected by the signaling host ${host}: ${cause}\n The room id or secret in this saved remote no longer matches what the host\n serves (it rotated, or the link was copied from a different machine).\n On the host run \`ay share status\` and re-add the remote with that link.`);
107
+ case "ice-timeout": return Promise.resolve(`WebRTC handshake with the host behind ${host} failed: ${cause}\n Signaling worked and the host answered, so this is not a sandbox or proxy\n problem — the two machines could not open a data channel. Usual cause: a\n host behind Docker Desktop or another symmetric NAT. There, publish one UDP\n port and set AGENT_YES_ICE_UDP_PORT + AGENT_YES_ICE_PUBLIC_IPS, or set\n AGENT_YES_SIGNAL_RELAY=1 on both ends to relay through the signaling server.`);
108
+ default: return sandboxHint(cause);
109
+ }
110
+ }
111
+ /**
112
+ * The sender of a host frame. A signaling server without multi-host rooms
113
+ * does not tag host frames; its one host is "host".
114
+ */
115
+ function memberOf(m) {
116
+ return typeof m?.from === "string" && m.from ? m.from : "host";
117
+ }
118
+ /**
119
+ * Which members a `<alias>:<keyword>` reference names, when the keyword starts
120
+ * with a member's name (`<alias>:<member>:<keyword>`). Returns the member and
121
+ * the rest of the keyword, or null when the first segment names no member.
122
+ */
123
+ function splitMemberKeyword(keyword, members) {
124
+ const i = keyword.indexOf(":");
125
+ const head = i === -1 ? keyword : keyword.slice(0, i);
126
+ const member = members.find((m) => m.name === head || m.peer === head);
127
+ if (!member) return null;
128
+ return {
129
+ member,
130
+ keyword: (i === -1 ? "" : keyword.slice(i + 1)) || void 0
131
+ };
132
+ }
133
+ /**
134
+ * The members a viewer should talk to: everyone but the machine it runs on
135
+ * (a member listing its own room would show its agents twice), unless that
136
+ * would leave nobody.
137
+ */
138
+ function otherMembers(members, selfName) {
139
+ const others = members.filter((m) => m.name !== selfName);
140
+ return others.length ? others : members;
141
+ }
142
+ /** `/api/ls` rows from several members, each row tagged with its member. */
143
+ function mergeMemberLs(parts) {
144
+ const out = [];
145
+ for (const { member, rows } of parts) if (Array.isArray(rows)) {
146
+ for (const r of rows) if (r && typeof r === "object") out.push({
147
+ ...r,
148
+ member: member.name || member.peer
149
+ });
150
+ }
151
+ return out;
152
+ }
153
+
154
+ //#endregion
155
+ //#region ts/webrtcRemote.ts
156
+ const CONNECT_TIMEOUT_MS = 25e3;
157
+ const RELAY_AFTER_MS = 8e3;
158
+ const signalRelayEnabled = () => [
159
+ "1",
160
+ "true",
161
+ "yes"
162
+ ].includes((process.env.AGENT_YES_SIGNAL_RELAY ?? "").trim());
163
+ /**
164
+ * A live, key-confirmed WebRTC connection to one host (member) of a share
165
+ * room. Its signaling travels over the room's shared socket (RoomSignal).
166
+ */
167
+ var HostConn = class {
168
+ pc = null;
169
+ dc = null;
170
+ send = { sendCtr: 0n };
171
+ recv = { lastSeen: -1n };
172
+ keyC2H = null;
173
+ keyH2C = null;
174
+ th = null;
175
+ myNonce = randomHex(16);
176
+ confirmedIn = false;
177
+ confirmedOut = false;
178
+ confirmed = false;
179
+ sig = {
180
+ opened: true,
181
+ offered: false,
182
+ confirmed: false
183
+ };
184
+ earlyCandidates = [];
185
+ sendChain = Promise.resolve();
186
+ recvChain = Promise.resolve();
187
+ idCounter = 0;
188
+ pending = /* @__PURE__ */ new Map();
189
+ ready;
190
+ resolveReady;
191
+ rejectReady;
192
+ constructor(link, member, signal) {
193
+ this.link = link;
194
+ this.member = member;
195
+ this.signal = signal;
196
+ this.ready = new Promise((res, rej) => {
197
+ this.resolveReady = res;
198
+ this.rejectReady = rej;
199
+ });
200
+ this.ready.then(() => this.state = "ok", (e) => (this.state = "failed", this.error = e));
201
+ const timer = setTimeout(() => this.rejectReady(timeoutError(this.sig, CONNECT_TIMEOUT_MS)), CONNECT_TIMEOUT_MS);
202
+ this.ready.then(() => clearTimeout(timer), () => clearTimeout(timer));
203
+ }
204
+ get isConfirmed() {
205
+ return this.confirmed;
206
+ }
207
+ /** Frames go over the signaling socket (see RelayState in rs/src/serve/share.rs). */
208
+ relayed = false;
209
+ /** A sealed frame the host relayed over signaling. */
210
+ onRelay(data) {
211
+ if (!this.relayed) return;
212
+ const bytes = Buffer.from(data, "base64");
213
+ this.recvChain = this.recvChain.then(() => this.onFrame(bytes)).catch(() => {});
214
+ }
215
+ state = "pending";
216
+ error = null;
217
+ async onCandidate(candidate) {
218
+ if (!this.pc) this.earlyCandidates.push(candidate);
219
+ else await this.pc.addIceCandidate(candidate).catch(() => {});
220
+ }
221
+ async onOffer(sdp, iceServers, relay = false) {
222
+ if (this.sig.offered) return;
223
+ this.sig.offered = true;
224
+ const pc = new RTCPeerConnection({ iceServers: iceServers || [] });
225
+ this.pc = pc;
226
+ let held = [];
227
+ const sendCandidate = (candidate) => this.signal({
228
+ type: "candidate",
229
+ candidate
230
+ });
231
+ pc.onicecandidate = (e) => {
232
+ if (!e.candidate) return;
233
+ if (held) held.push(e.candidate);
234
+ else sendCandidate(e.candidate);
235
+ };
236
+ pc.ondatachannel = (e) => {
237
+ const dc = e.channel;
238
+ this.dc = dc;
239
+ dc.binaryType = "arraybuffer";
240
+ dc.onopen = () => {
241
+ if (!this.relayed) this.enqueueSeal(FLAG_CONFIRM, {
242
+ t: "confirm",
243
+ nonce: this.myNonce
244
+ });
245
+ };
246
+ dc.onmessage = (ev2) => {
247
+ this.recvChain = this.recvChain.then(() => this.onFrame(ev2.data)).catch(() => {});
248
+ };
249
+ };
250
+ await pc.setRemoteDescription({
251
+ type: "offer",
252
+ sdp
253
+ });
254
+ const early = this.earlyCandidates;
255
+ this.earlyCandidates = [];
256
+ for (const c of early) await pc.addIceCandidate(c).catch(() => {});
257
+ const answer = await pc.createAnswer();
258
+ await pc.setLocalDescription(answer);
259
+ this.th = await computeTranscriptHash(sdp, pc.localDescription.sdp);
260
+ const keys = await deriveDirKeys(this.link.s, this.th);
261
+ this.keyC2H = keys.keyC2H;
262
+ this.keyH2C = keys.keyH2C;
263
+ this.signal({
264
+ type: "answer",
265
+ sdp: pc.localDescription.sdp
266
+ });
267
+ const queued = held;
268
+ held = null;
269
+ for (const c of queued) sendCandidate(c);
270
+ if (relay && signalRelayEnabled()) setTimeout(() => {
271
+ if (this.confirmed || this.dc?.readyState === "open") return;
272
+ process.stderr.write(`[webrtc] no direct path to ${this.member.name || this.member.peer}; using the signaling relay\n`);
273
+ this.relayed = true;
274
+ this.enqueueSeal(FLAG_CONFIRM, {
275
+ t: "confirm",
276
+ nonce: this.myNonce
277
+ });
278
+ }, RELAY_AFTER_MS).unref?.();
279
+ }
280
+ enqueueSeal(flags, obj) {
281
+ this.sendChain = this.sendChain.then(async () => {
282
+ if (!this.keyC2H || !this.th) return;
283
+ if (!this.relayed && (!this.dc || this.dc.readyState !== "open")) return;
284
+ const frame = await seal(this.keyC2H, this.send, flags, this.th, packEnvelope(obj));
285
+ if (this.relayed) {
286
+ this.signal({
287
+ type: "relay",
288
+ data: Buffer.from(frame).toString("base64")
289
+ });
290
+ return;
291
+ }
292
+ try {
293
+ this.dc.send(frame);
294
+ } catch {}
295
+ });
296
+ return this.sendChain;
297
+ }
298
+ async onFrame(data) {
299
+ if (typeof data === "string" || !this.keyH2C || !this.th) return;
300
+ let env;
301
+ try {
302
+ const { plaintext } = await open(this.keyH2C, data, this.th, this.recv);
303
+ env = unpackEnvelope(plaintext);
304
+ } catch {
305
+ return;
306
+ }
307
+ if (!this.confirmed) {
308
+ if (!env || env.t !== "confirm") return;
309
+ if (typeof env.nonce === "string" && !this.confirmedOut) {
310
+ await this.enqueueSeal(FLAG_CONFIRM, {
311
+ t: "confirm",
312
+ nonce: this.myNonce,
313
+ echo: env.nonce
314
+ });
315
+ this.confirmedOut = true;
316
+ }
317
+ if (env.echo && env.echo === this.myNonce) this.confirmedIn = true;
318
+ if (this.confirmedIn && this.confirmedOut) {
319
+ this.confirmed = true;
320
+ this.sig.confirmed = true;
321
+ this.resolveReady();
322
+ }
323
+ return;
324
+ }
325
+ if (!env || env.t === "confirm") return;
326
+ const p = this.pending.get(env.id);
327
+ if (!p) return;
328
+ if (env.t === "res") p.onRes(env.status, env.ct);
329
+ else if (env.t === "data") p.onData(env.chunk);
330
+ else if (env.t === "end") p.onEnd(env.error);
331
+ }
332
+ /**
333
+ * Issue one request over the channel. Resolves once the response head (status,
334
+ * content-type) arrives; the body is a ReadableStream fed by `data` frames as
335
+ * they land, so streaming endpoints (SSE `tail`) flow without buffering.
336
+ */
337
+ request(method, path, body) {
338
+ const id = String(++this.idCounter);
339
+ return new Promise((resolve, reject) => {
340
+ let controller;
341
+ const enc = new TextEncoder();
342
+ let head = false;
343
+ const stream = new ReadableStream({
344
+ start: (c) => {
345
+ controller = c;
346
+ },
347
+ cancel: () => {
348
+ this.pending.delete(id);
349
+ this.enqueueSeal(0, {
350
+ t: "abort",
351
+ id
352
+ });
353
+ }
354
+ });
355
+ this.pending.set(id, {
356
+ onRes: (status, ct) => {
357
+ if (!head) {
358
+ head = true;
359
+ resolve({
360
+ status,
361
+ ct,
362
+ stream
363
+ });
364
+ }
365
+ },
366
+ onData: (chunk) => {
367
+ try {
368
+ controller.enqueue(enc.encode(chunk));
369
+ } catch {}
370
+ },
371
+ onEnd: (error) => {
372
+ this.pending.delete(id);
373
+ if (error) {
374
+ if (!head) {
375
+ head = true;
376
+ reject(new Error(error));
377
+ }
378
+ try {
379
+ controller.error(new Error(error));
380
+ } catch {}
381
+ } else try {
382
+ controller.close();
383
+ } catch {}
384
+ }
385
+ });
386
+ this.enqueueSeal(0, {
387
+ t: "req",
388
+ id,
389
+ method,
390
+ path,
391
+ body
392
+ });
393
+ });
394
+ }
395
+ close() {
396
+ try {
397
+ this.pc?.close();
398
+ } catch {}
399
+ }
400
+ };
401
+ /**
402
+ * The room's signaling socket, shared by one HostConn per member. A room can
403
+ * have several hosts (lab/ui/cf/roomRouting.ts); the welcome lists them, and
404
+ * each one offers to us on its own. An older signaling server lists nobody and
405
+ * has one host, "host".
406
+ */
407
+ var RoomSignal = class {
408
+ ws;
409
+ sig = {
410
+ opened: false,
411
+ offered: false,
412
+ confirmed: false
413
+ };
414
+ /** Members the welcome named; null until it arrives or when the server predates rooms with several hosts. */
415
+ roster = null;
416
+ conns = /* @__PURE__ */ new Map();
417
+ settled = false;
418
+ /** Resolves with the members that connected, once every one has connected or failed. */
419
+ ready;
420
+ resolveReady;
421
+ rejectReady;
422
+ constructor(link, skip) {
423
+ this.link = link;
424
+ this.skip = skip;
425
+ this.ready = new Promise((res, rej) => {
426
+ this.resolveReady = res;
427
+ this.rejectReady = rej;
428
+ });
429
+ const timer = setTimeout(() => this.fail(timeoutError(this.sig, CONNECT_TIMEOUT_MS)), CONNECT_TIMEOUT_MS + 1e3);
430
+ this.ready.then(() => clearTimeout(timer), () => clearTimeout(timer));
431
+ this.ws = this.dial();
432
+ }
433
+ fail(e) {
434
+ if (this.settled) return;
435
+ if (this.connected().length) return this.settle();
436
+ this.settled = true;
437
+ for (const c of this.conns.values()) c.rejectReady(e);
438
+ this.rejectReady(e);
439
+ }
440
+ connected() {
441
+ return [...this.conns.values()].filter((c) => c.isConfirmed);
442
+ }
443
+ settle() {
444
+ if (this.settled) return;
445
+ this.settled = true;
446
+ this.resolveReady(this.connected());
447
+ }
448
+ /** Settle once every member we expect has connected or failed. */
449
+ check() {
450
+ if (this.settled || !this.roster) return;
451
+ const expected = this.roster.filter((m) => !this.skip(m));
452
+ const conns = expected.map((m) => this.conns.get(m.peer));
453
+ if (!expected.length || conns.some((c) => !c || c.state === "pending")) return;
454
+ if (this.connected().length) return this.settle();
455
+ this.fail(conns.find((c) => c.error)?.error ?? /* @__PURE__ */ new Error("no host connected"));
456
+ }
457
+ dial() {
458
+ const { room, host } = this.link;
459
+ const scheme = /^(localhost|127\.)/.test(host) ? "ws" : "wss";
460
+ const ws = new WebSocket(`${scheme}://${host}/${room}`, [SIGNAL_SUBPROTOCOL]);
461
+ ws.onopen = async () => {
462
+ this.sig.opened = true;
463
+ const authToken = await deriveAuthToken(this.link.s, room, host);
464
+ ws.send(JSON.stringify({
465
+ type: "hello",
466
+ role: "client",
467
+ v: 2,
468
+ token: authToken,
469
+ multi: true
470
+ }));
471
+ };
472
+ ws.onerror = (e) => this.fail(socketError(this.sig, String(e?.message ?? e)));
473
+ ws.onclose = (ev) => {
474
+ this.fail(closeError(this.sig, Number(ev?.code ?? 0), String(ev?.reason ?? "")));
475
+ };
476
+ ws.onmessage = (ev) => void this.onSignal(ev);
477
+ return ws;
478
+ }
479
+ memberConn(peer) {
480
+ let c = this.conns.get(peer);
481
+ if (c) return c;
482
+ if (this.settled) return null;
483
+ const member = this.roster?.find((m) => m.peer === peer) ?? {
484
+ peer,
485
+ name: ""
486
+ };
487
+ if (this.skip(member)) return null;
488
+ c = new HostConn(this.link, member, (msg) => this.ws.send(JSON.stringify({
489
+ ...msg,
490
+ to: peer
491
+ })));
492
+ this.conns.set(peer, c);
493
+ c.ready.then(() => this.check(), () => this.check());
494
+ if (!this.roster) c.ready.then(() => this.settle(), (e) => this.fail(e));
495
+ return c;
496
+ }
497
+ async onSignal(ev) {
498
+ const m = JSON.parse(typeof ev.data === "string" ? ev.data : await ev.data.text());
499
+ if (m?.type === "welcome") {
500
+ if (Array.isArray(m.hosts)) {
501
+ this.roster = m.hosts.filter((h) => h && typeof h.peer === "string").map((h) => ({
502
+ peer: h.peer,
503
+ name: String(h.name ?? "")
504
+ }));
505
+ for (const h of this.roster) this.join(h);
506
+ }
507
+ const action = reduceSignal(this.sig, m);
508
+ if (action.kind === "fail") this.fail(action.error);
509
+ return;
510
+ }
511
+ if (m?.type === "host-join" && this.roster && typeof m.peer === "string") {
512
+ if (!this.roster.some((h) => h.peer === m.peer)) {
513
+ const h = {
514
+ peer: m.peer,
515
+ name: String(m.name ?? "")
516
+ };
517
+ this.roster.push(h);
518
+ this.join(h);
519
+ }
520
+ return;
521
+ }
522
+ if (m?.type === "host-leave" && this.roster && typeof m.peer === "string") {
523
+ this.roster = this.roster.filter((h) => h.peer !== m.peer);
524
+ this.conns.get(m.peer)?.rejectReady(new ConnectError("no-host", "the host left the room"));
525
+ this.check();
526
+ return;
527
+ }
528
+ if (m?.type === "offer") {
529
+ this.sig.offered = true;
530
+ await this.memberConn(memberOf(m))?.onOffer(m.sdp, m.iceServers || [], m.relay === true);
531
+ } else if (m?.type === "candidate") await this.memberConn(memberOf(m))?.onCandidate(m.candidate);
532
+ else if (m?.type === "relay" && typeof m.data === "string") this.conns.get(memberOf(m))?.onRelay(m.data);
533
+ }
534
+ join(m) {
535
+ if (this.settled || this.skip(m) || this.conns.has(m.peer)) return;
536
+ this.ws.send(JSON.stringify({
537
+ type: "join",
538
+ to: m.peer
539
+ }));
540
+ }
541
+ /** Every member the room listed (or the one that offered, on an older server). */
542
+ members() {
543
+ return this.roster ?? [...this.conns.values()].map((c) => c.member);
544
+ }
545
+ /** Drop the members we will not use, so their hosts can free the peer. */
546
+ keepOnly(keep) {
547
+ for (const [peer, c] of this.conns) if (!keep.includes(c)) {
548
+ c.close();
549
+ this.conns.delete(peer);
550
+ }
551
+ }
552
+ close() {
553
+ try {
554
+ this.ws.close();
555
+ } catch {}
556
+ for (const c of this.conns.values()) c.close();
557
+ }
558
+ };
559
+ /** This machine's member name, as ayrs reports it (rs/src/serve/room_member.rs). */
560
+ function selfMemberName() {
561
+ return process.env.AY_NAME?.trim() || hostname();
562
+ }
563
+ async function readJson(conn, path) {
564
+ const { status, stream } = await conn.request("GET", path);
565
+ const text = await new Response(stream).text();
566
+ if (status < 200 || status >= 300) throw new Error(`${status} ${text.slice(0, 200)}`);
567
+ return JSON.parse(text);
568
+ }
569
+ /**
570
+ * The member a `<alias>:<keyword>` reference means in a room with several
571
+ * hosts: the one named by a leading `<member>:`, else the only one with an
572
+ * agent matching the keyword. Throws with the candidates when that is not
573
+ * exactly one.
574
+ */
575
+ async function chooseMember(conns, keyword) {
576
+ const named = splitMemberKeyword(keyword, conns.map((c) => c.member));
577
+ if (named) return {
578
+ conn: conns.find((c) => c.member === named.member),
579
+ keyword: named.keyword
580
+ };
581
+ const q = `/api/ls?all=1&keyword=${encodeURIComponent(keyword)}`;
582
+ const found = (await Promise.all(conns.map(async (c) => {
583
+ const rows = await readJson(c, q).catch(() => []);
584
+ return Array.isArray(rows) && rows.length ? c : null;
585
+ }))).filter((c) => !!c);
586
+ if (found.length === 1) return {
587
+ conn: found[0],
588
+ keyword
589
+ };
590
+ const names = (found.length ? found : conns).map((c) => c.member.name || c.member.peer);
591
+ throw new Error(found.length ? `"${keyword}" matches agents on several hosts in this room: ${names.join(", ")} — say which as <remote>:<host>:${keyword}` : `no agent matches "${keyword}" on any host in this room (${names.join(", ")})`);
592
+ }
593
+ /**
594
+ * Connect to a share room and start a local HTTP server that forwards every
595
+ * request over the encrypted DataChannel. Returns the loopback base URL the
596
+ * existing remote commands can `fetch()` against. The process owns teardown:
597
+ * `ay` subcommands `process.exit()` when done, which closes the socket/peer.
598
+ *
599
+ * A room with several hosts: with a `keyword`, the bridge talks to the one
600
+ * member it means (see chooseMember); without one, `GET /api/ls` lists every
601
+ * member's agents (each row tagged `member`) and anything else is refused
602
+ * with a hint to name the member.
603
+ */
604
+ async function startWebrtcBridge(link, opts = {}) {
605
+ const parsed = parseWebrtcLink(link);
606
+ if (!parsed) throw new Error(`not a WebRTC share link: ${link}`);
607
+ const self = selfMemberName();
608
+ const room = new RoomSignal(parsed, (m) => m.name === self && otherMembers(room?.members?.() ?? [m], self).every((o) => o.peer !== m.peer));
609
+ let conns;
610
+ try {
611
+ conns = await room.ready;
612
+ } catch (e) {
613
+ room.close();
614
+ const err = e instanceof Error ? e : new Error(String(e));
615
+ throw new Error(await describeConnectFailure(err, parsed.host, async (cause) => signalingFailureHint(cause, await probeSignalingHost(parsed.host), parsed.host)));
616
+ }
617
+ let keyword = opts.keyword;
618
+ const named = keyword ? splitMemberKeyword(keyword, room.members()) : null;
619
+ if (named) {
620
+ const conn = conns.find((c) => c.member.peer === named.member.peer);
621
+ if (!conn) {
622
+ room.close();
623
+ throw new Error(named.member.name === self ? `"${named.member.name}" is this machine — address its agents locally (ay send <agent>)` : `room member "${named.member.name || named.member.peer}" did not connect`);
624
+ }
625
+ conns = [conn];
626
+ keyword = named.keyword;
627
+ } else if (conns.length > 1 && keyword) try {
628
+ const pick = await chooseMember(conns, keyword);
629
+ conns = [pick.conn];
630
+ keyword = pick.keyword;
631
+ } catch (e) {
632
+ room.close();
633
+ throw e;
634
+ }
635
+ room.keepOnly(conns);
636
+ const members = room.members();
637
+ const server = Bun.serve({
638
+ port: 0,
639
+ hostname: "127.0.0.1",
640
+ idleTimeout: 0,
641
+ async fetch(req) {
642
+ const u = new URL(req.url);
643
+ const pathWithQuery = u.pathname + u.search;
644
+ const body = req.method !== "GET" && req.method !== "HEAD" ? await req.text() : void 0;
645
+ if (conns.length > 1) {
646
+ if (req.method === "GET" && u.pathname === "/api/ls") {
647
+ const parts = await Promise.all(conns.map(async (c) => ({
648
+ member: c.member,
649
+ rows: await readJson(c, pathWithQuery).catch(() => [])
650
+ })));
651
+ return Response.json(mergeMemberLs(parts));
652
+ }
653
+ const names = conns.map((c) => c.member.name || c.member.peer).join(", ");
654
+ return new Response(`this room has several hosts (${names}); address one as <remote>:<host>:<agent>`, { status: 409 });
655
+ }
656
+ try {
657
+ const { status, ct, stream } = await conns[0].request(req.method, pathWithQuery, body);
658
+ return new Response(stream, {
659
+ status,
660
+ headers: ct ? { "content-type": ct } : {}
661
+ });
662
+ } catch (e) {
663
+ return new Response(String(e?.message ?? e), { status: 502 });
664
+ }
665
+ }
666
+ });
667
+ return {
668
+ baseUrl: `http://127.0.0.1:${server.port}`,
669
+ token: "webrtc",
670
+ members,
671
+ keyword,
672
+ close: () => {
673
+ try {
674
+ server.stop(true);
675
+ } catch {}
676
+ room.close();
677
+ }
678
+ };
679
+ }
680
+
681
+ //#endregion
682
+ export { startWebrtcBridge };
683
+ //# sourceMappingURL=webrtcRemote-RmkbzY6Y.js.map
@@ -28,7 +28,7 @@ function parseFlags(args, known) {
28
28
  }
29
29
  /** Resolve the local daemon base URL + token (flags override discovery). */
30
30
  async function daemonTarget(flags) {
31
- const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-DdHbUWtG.js");
31
+ const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-BWy7zL7j.js");
32
32
  const base = typeof flags.base === "string" ? flags.base : await resolveDaemonHttpBase();
33
33
  if (!base) throw new Error("no running ay serve daemon found — start `ay serve` or pass --base <url> (e.g. http://127.0.0.1:PORT)");
34
34
  const token = typeof flags.token === "string" ? flags.token : await loadTokenReadOnly() ?? "";
@@ -186,7 +186,7 @@ async function cmdMint(args) {
186
186
  const bad = caps.filter((c) => !KNOWN_CAPS.has(c));
187
187
  if (bad.length) throw new Error(`unknown cap(s): ${bad.join(", ")} (valid: ${[...KNOWN_CAPS].join(", ")})`);
188
188
  const ttlSec = parseTtlSec(typeof flags.ttl === "string" ? flags.ttl : "15m");
189
- const { mintScopedTermToken } = await import("./serve-DdHbUWtG.js");
189
+ const { mintScopedTermToken } = await import("./serve-BWy7zL7j.js");
190
190
  const r = await mintScopedTermToken(target, {
191
191
  ttlSec,
192
192
  caps
@@ -202,4 +202,4 @@ async function cmdMint(args) {
202
202
 
203
203
  //#endregion
204
204
  export { cmdMint, cmdWidget };
205
- //# sourceMappingURL=widget-CvcvPWF9.js.map
205
+ //# sourceMappingURL=widget-C6-P26Ue.js.map