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.
- package/dist/{SUPPORTED_CLIS-C5Uhdl61.js → SUPPORTED_CLIS-BvWBFlrN.js} +3 -3
- package/dist/{SUPPORTED_CLIS-FBshN0v2.js → SUPPORTED_CLIS-CqmE-ESB.js} +2 -2
- package/dist/{agentShare-CNgCQMY9.js → agentShare-Gb4vxJzE.js} +4 -4
- package/dist/{callback-ayc9yXqJ.js → callback-CuIY-75l.js} +3 -3
- package/dist/{callback-CbZ-H_Pn.js → callback-ZzRPkYjf.js} +4 -4
- package/dist/cli.js +5 -5
- package/dist/{cmdPs-CDj-l5Pk.js → cmdPs-DSKYjOKS.js} +4 -4
- package/dist/index.js +2 -2
- package/dist/{notifyDaemon-DZbd5fTj.js → notifyDaemon-C_gTWqYZ.js} +4 -4
- package/dist/{parentPingLoop-Bim6u0OH.js → parentPingLoop-BhVR1EQm.js} +2 -2
- package/dist/{remotes-ByjpYjQ9.js → remotes-C36aI8Ty.js} +2 -2
- package/dist/{remotes-D10E_xUn.js → remotes-COBEhmlr.js} +6 -6
- package/dist/{rustBinary-DklBSefL.js → rustBinary-Ep0x7WrS.js} +2 -2
- package/dist/{rustBinary-Btf18sk5.js → rustBinary-eOcvZ5yS.js} +2 -2
- package/dist/{schedule-DzqTosvJ.js → schedule-E2QlsME5.js} +4 -4
- package/dist/{serve-DdHbUWtG.js → serve-BWy7zL7j.js} +43 -26
- package/dist/{serveRust-BEzTqFRy.js → serveRust-BgY5Te5E.js} +50 -6
- package/dist/{setup-CzKkVYp8.js → setup-CD9-RyhT.js} +2 -2
- package/dist/{shareCmd-DinezAHY.js → shareCmd-Drxb-SjM.js} +7 -7
- package/dist/{subcommands-oIARj_mJ.js → subcommands-C627cuOl.js} +3 -3
- package/dist/{subcommands-fX4Ygx3a.js → subcommands-Cc6bmEnB.js} +30 -27
- package/dist/{terminal-DlW0KNym.js → terminal-BddOL2fx.js} +2 -2
- package/dist/trayApp-CdLbJK5F.js +5 -0
- package/dist/{trayApp-DWVdr0Pr.js → trayApp-D0UYVT6n.js} +2 -2
- package/dist/{ts-D29CMeWZ.js → ts-Bm6FhYS4.js} +3 -3
- package/dist/{versionChecker-DrjMn-XI.js → versionChecker-CFyZKym2.js} +2 -2
- package/dist/webrtcLink-CHLgPKWj.js +4 -0
- package/dist/{webrtcLink-CKnjNGaV.js → webrtcLink-bWxiCBTV.js} +26 -9
- package/dist/webrtcRemote-RmkbzY6Y.js +683 -0
- package/dist/{widget-CvcvPWF9.js → widget-C6-P26Ue.js} +3 -3
- package/dist/{ws-Cbx5sfsd.js → ws-DCcdDRXH.js} +2 -2
- package/dist/{ws-DDB8jh0f.js → ws-DmiSL50p.js} +4 -4
- package/lab/ui/index.html +71 -3
- package/package.json +1 -1
- package/ts/remotes.ts +11 -4
- package/ts/serve.ts +24 -2
- package/ts/serveRust.spec.ts +23 -6
- package/ts/serveRust.ts +34 -7
- package/ts/subcommands.ts +4 -1
- package/ts/webrtcLink.spec.ts +26 -1
- package/ts/webrtcLink.ts +32 -5
- package/ts/webrtcRemote.ts +406 -102
- package/ts/webrtcSignal.spec.ts +40 -0
- package/ts/webrtcSignal.ts +59 -3
- package/dist/trayApp-F_Xs3C2N.js +0 -5
- package/dist/webrtcLink-CAdvnRM0.js +0 -4
- 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-
|
|
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-
|
|
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-
|
|
205
|
+
//# sourceMappingURL=widget-C6-P26Ue.js.map
|