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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { V as listRecords } from "./subcommands-
|
|
1
|
+
import { V as listRecords } from "./subcommands-Cc6bmEnB.js";
|
|
2
2
|
import { a as getProvisionRoot } from "./workspaceConfig-CWGh8o36.js";
|
|
3
3
|
import { existsSync } from "fs";
|
|
4
4
|
import { lstat, opendir } from "fs/promises";
|
|
@@ -424,4 +424,4 @@ async function cmdWs(args) {
|
|
|
424
424
|
|
|
425
425
|
//#endregion
|
|
426
426
|
export { isPathInside as a, walkWorkspaces as c, importProvisionModule as i, workspaceStatus as l, cmdWs as n, loadProvision as o, collectWorkspaces as r, resolveOperand as s, WS_JSON_SCHEMA as t };
|
|
427
|
-
//# sourceMappingURL=ws-
|
|
427
|
+
//# sourceMappingURL=ws-DCcdDRXH.js.map
|
|
@@ -5,10 +5,10 @@ import "./pidStore-433kvh9D.js";
|
|
|
5
5
|
import "./messageLog-Dq_ka-Bt.js";
|
|
6
6
|
import "./e2e-BdLf8DvY.js";
|
|
7
7
|
import "./configShared-De4bR5hH.js";
|
|
8
|
-
import "./subcommands-
|
|
8
|
+
import "./subcommands-Cc6bmEnB.js";
|
|
9
9
|
import "./notifyStore-DI4iKFut.js";
|
|
10
|
-
import "./webrtcLink-
|
|
11
|
-
import "./remotes-
|
|
12
|
-
import { a as isPathInside, c as walkWorkspaces, i as importProvisionModule, l as workspaceStatus, n as cmdWs, o as loadProvision, r as collectWorkspaces, s as resolveOperand, t as WS_JSON_SCHEMA } from "./ws-
|
|
10
|
+
import "./webrtcLink-bWxiCBTV.js";
|
|
11
|
+
import "./remotes-COBEhmlr.js";
|
|
12
|
+
import { a as isPathInside, c as walkWorkspaces, i as importProvisionModule, l as workspaceStatus, n as cmdWs, o as loadProvision, r as collectWorkspaces, s as resolveOperand, t as WS_JSON_SCHEMA } from "./ws-DCcdDRXH.js";
|
|
13
13
|
|
|
14
14
|
export { WS_JSON_SCHEMA, cmdWs, collectWorkspaces, isPathInside, loadProvision, walkWorkspaces, workspaceStatus };
|
package/lab/ui/index.html
CHANGED
|
@@ -8223,7 +8223,8 @@ my.translate = async (text, lang) => {
|
|
|
8223
8223
|
} catch {}
|
|
8224
8224
|
return;
|
|
8225
8225
|
}
|
|
8226
|
-
|
|
8226
|
+
// An ay-share room: its own source plus one per further member.
|
|
8227
|
+
for (const s of roomSources(name)) removeSource(s.id);
|
|
8227
8228
|
}
|
|
8228
8229
|
|
|
8229
8230
|
// Exponential-backoff reconnect for an agent-yes share room (the RTCClient
|
|
@@ -8257,7 +8258,10 @@ my.translate = async (text, lang) => {
|
|
|
8257
8258
|
try {
|
|
8258
8259
|
s.client?.close?.(); // drop the stale peer AND its signaling socket
|
|
8259
8260
|
} catch {}
|
|
8260
|
-
|
|
8261
|
+
// One client per room member: a member source names its member; the
|
|
8262
|
+
// room's own source sticks to the member it first got (see syncRtcMembers).
|
|
8263
|
+
const c = new RTCClient(s.host, s.room || s.id, s.token, { member: s.member });
|
|
8264
|
+
if (!s.parent) c.onroster = () => syncRtcMembers(s);
|
|
8261
8265
|
c.onstate = (st) => {
|
|
8262
8266
|
if (st === "failed" || st === "closed") {
|
|
8263
8267
|
s.live = false;
|
|
@@ -8269,6 +8273,12 @@ my.translate = async (text, lang) => {
|
|
|
8269
8273
|
await c.connect();
|
|
8270
8274
|
} catch (e) {
|
|
8271
8275
|
c.close(); // connect() already opened the signaling socket — don't
|
|
8276
|
+
// A member that left: its own source goes away; the room's source
|
|
8277
|
+
// takes whichever member the room lists first next time.
|
|
8278
|
+
if (String(e?.message) === "that host left the room") {
|
|
8279
|
+
if (s.parent) removeSource(s.id);
|
|
8280
|
+
else s.member = null;
|
|
8281
|
+
}
|
|
8272
8282
|
throw e; // leak it on every failed retry of an offline room
|
|
8273
8283
|
}
|
|
8274
8284
|
if (s.removed) {
|
|
@@ -8277,6 +8287,10 @@ my.translate = async (text, lang) => {
|
|
|
8277
8287
|
}
|
|
8278
8288
|
s.client = c;
|
|
8279
8289
|
s.tx = rtcTx(c);
|
|
8290
|
+
if (!s.parent) {
|
|
8291
|
+
s.member = c.member;
|
|
8292
|
+
syncRtcMembers(s);
|
|
8293
|
+
}
|
|
8280
8294
|
// Negotiate the stdin fast path explicitly. A new page can connect to
|
|
8281
8295
|
// an older host during a rolling upgrade; sending an unknown envelope
|
|
8282
8296
|
// there would look successful while silently dropping every key. Keep
|
|
@@ -8363,6 +8377,59 @@ my.translate = async (text, lang) => {
|
|
|
8363
8377
|
}, delay);
|
|
8364
8378
|
}
|
|
8365
8379
|
|
|
8380
|
+
// A share room with several hosts (`ay serve --webrtc <link> --join`):
|
|
8381
|
+
// the room's own source talks to one member, and every other member the
|
|
8382
|
+
// room lists gets a source of its own, "<room>/<member>", so its agents
|
|
8383
|
+
// show under the room like a codehost room's machines do.
|
|
8384
|
+
function syncRtcMembers(s) {
|
|
8385
|
+
const roster = s.client?.roster;
|
|
8386
|
+
if (!roster || s.removed) return;
|
|
8387
|
+
const want = new Set(roster.map((h) => h.peer).filter((p) => p !== s.client.member));
|
|
8388
|
+
let changed = false;
|
|
8389
|
+
for (const peer of want) {
|
|
8390
|
+
const id = s.id + "/" + peer;
|
|
8391
|
+
if (sources.has(id)) continue;
|
|
8392
|
+
const m = {
|
|
8393
|
+
id,
|
|
8394
|
+
room: s.id,
|
|
8395
|
+
parent: s.id,
|
|
8396
|
+
member: peer,
|
|
8397
|
+
host: s.host,
|
|
8398
|
+
token: s.token,
|
|
8399
|
+
kind: "rtc",
|
|
8400
|
+
tx: null,
|
|
8401
|
+
client: null,
|
|
8402
|
+
live: false,
|
|
8403
|
+
tried: false,
|
|
8404
|
+
devices: new Set(),
|
|
8405
|
+
serverCount: 0,
|
|
8406
|
+
};
|
|
8407
|
+
sources.set(id, m);
|
|
8408
|
+
changed = true;
|
|
8409
|
+
connectRtcSource(m)
|
|
8410
|
+
.then(() => {
|
|
8411
|
+
m.live = true;
|
|
8412
|
+
subscribeSource(m);
|
|
8413
|
+
})
|
|
8414
|
+
.catch(() => {
|
|
8415
|
+
m.live = false;
|
|
8416
|
+
m.failCount = (m.failCount || 0) + 1;
|
|
8417
|
+
scheduleRtcReconnect(m);
|
|
8418
|
+
})
|
|
8419
|
+
.finally(() => {
|
|
8420
|
+
m.tried = true;
|
|
8421
|
+
renderRoomsIfOpen();
|
|
8422
|
+
loadList();
|
|
8423
|
+
});
|
|
8424
|
+
}
|
|
8425
|
+
for (const x of [...sources.values()])
|
|
8426
|
+
if (x.parent === s.id && !want.has(x.member)) {
|
|
8427
|
+
removeSource(x.id);
|
|
8428
|
+
changed = true;
|
|
8429
|
+
}
|
|
8430
|
+
if (changed) renderRoomsIfOpen();
|
|
8431
|
+
}
|
|
8432
|
+
|
|
8366
8433
|
// Add a room to the fleet and connect it — WITHOUT dropping the others, so
|
|
8367
8434
|
// every saved room streams its agents at once. Idempotent: a room already
|
|
8368
8435
|
// in the fleet just refreshes. The connection runs in the background; the
|
|
@@ -8472,7 +8539,8 @@ my.translate = async (text, lang) => {
|
|
|
8472
8539
|
return token ? { room: chName(token), token, host: CH_HOST } : null;
|
|
8473
8540
|
}
|
|
8474
8541
|
const m = /^([A-Za-z0-9_-]+):([^@\s]+)(?:@(.+))?$/.exec(s);
|
|
8475
|
-
|
|
8542
|
+
// A host link's join secret (":j1.…") is for member hosts only.
|
|
8543
|
+
return m ? { room: m[1], token: m[2].replace(/:j1\..*$/, ""), host: m[3] } : null;
|
|
8476
8544
|
}
|
|
8477
8545
|
|
|
8478
8546
|
// Re-render the rooms panel only if it's currently open (so live-count
|
package/package.json
CHANGED
package/ts/remotes.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdir, readFile, writeFile } from "fs/promises";
|
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import yaml from "yaml";
|
|
5
|
-
import { isWebrtcSpec, parseWebrtcLink } from "./webrtcLink.ts";
|
|
5
|
+
import { hasJoinSecret, isWebrtcSpec, parseWebrtcLink, toWebrtcUrl } from "./webrtcLink.ts";
|
|
6
6
|
|
|
7
7
|
function remotesPath(): string {
|
|
8
8
|
const dir = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
|
|
@@ -171,8 +171,10 @@ async function resolveWebrtc(
|
|
|
171
171
|
label: string,
|
|
172
172
|
): Promise<ResolvedRemote> {
|
|
173
173
|
const { startWebrtcBridge } = await import("./webrtcRemote.ts");
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
// In a room with several hosts the keyword also picks the host
|
|
175
|
+
// (`<alias>:<host>:<agent>`, or the one host with a matching agent).
|
|
176
|
+
const bridge = await startWebrtcBridge(link, { keyword });
|
|
177
|
+
return { url: bridge.baseUrl, token: bridge.token, keyword: bridge.keyword, label };
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
// ---------------------------------------------------------------------------
|
|
@@ -414,7 +416,12 @@ export async function cmdConnect(rest: string[]): Promise<number> {
|
|
|
414
416
|
const alias = aliasArg ?? defaultConnectAlias(link);
|
|
415
417
|
|
|
416
418
|
if (!parsed) {
|
|
417
|
-
|
|
419
|
+
// A host link carries the room's join secret, which a viewer never needs:
|
|
420
|
+
// save the view part only.
|
|
421
|
+
await writeRemoteAlias(alias, {
|
|
422
|
+
url: hasJoinSecret(link) ? (toWebrtcUrl(link) ?? link) : link,
|
|
423
|
+
token: "",
|
|
424
|
+
});
|
|
418
425
|
} else {
|
|
419
426
|
try {
|
|
420
427
|
const r = await fetch(`${parsed.url}/api/version`, {
|
package/ts/serve.ts
CHANGED
|
@@ -1875,7 +1875,11 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1875
1875
|
` The minted room persists in ~/.agent-yes/.share-room-ayrs\n` +
|
|
1876
1876
|
` (.share-room under --no-rust); stable link across restarts,\n` +
|
|
1877
1877
|
` delete the file to rotate.\n` +
|
|
1878
|
-
` --share [URL] Legacy alias for --http --webrtc\n
|
|
1878
|
+
` --share [URL] Legacy alias for --http --webrtc\n` +
|
|
1879
|
+
` --webrtc URL --join\n` +
|
|
1880
|
+
` Host URL's room as one more member next to its other\n` +
|
|
1881
|
+
` hosts (all members' agents show up for every viewer).\n` +
|
|
1882
|
+
` Needs AY_JOIN_SECRET; AY_NAME names this member.\n\n` +
|
|
1879
1883
|
`Runtime: like \`ay <cli>\`, the Rust daemon (ayrs serve) runs by default for\n` +
|
|
1880
1884
|
` the flags it supports — --webrtc [webrtc://url], --port N, --sighost H.\n` +
|
|
1881
1885
|
` Everything else (bare \`ay serve\`, --share/--http/--host/--tls-*, the\n` +
|
|
@@ -1895,7 +1899,10 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1895
1899
|
` ay serve install install as background daemon (oxmgr; pm2 fallback on Windows without the winfix build)\n` +
|
|
1896
1900
|
` ay serve status show daemon/server status (add --json for scripts)\n` +
|
|
1897
1901
|
` ay serve uninstall remove daemon\n` +
|
|
1898
|
-
` ay serve logs view daemon logs\n
|
|
1902
|
+
` ay serve logs view daemon logs\n` +
|
|
1903
|
+
` ay serve join-secret [--webrtc URL]\n` +
|
|
1904
|
+
` print AY_ROOM + AY_JOIN_SECRET for members of this\n` +
|
|
1905
|
+
` host's room (mints the secret; the room then requires it)\n\n` +
|
|
1899
1906
|
`Once running, connect from another machine:\n` +
|
|
1900
1907
|
` ay ls <token>@<host>:<port>\n` +
|
|
1901
1908
|
` ay remote add <alias> http://<token>@<host>:<port>\n`,
|
|
@@ -1909,6 +1916,16 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1909
1916
|
{
|
|
1910
1917
|
const { planRustServe } = await import("./serveRust.ts");
|
|
1911
1918
|
const plan = planRustServe(rest);
|
|
1919
|
+
if (plan.kind === "rust-only") {
|
|
1920
|
+
const { findAyrsBinary } = await import("./rustBinary.ts");
|
|
1921
|
+
const ayrs = findAyrsBinary();
|
|
1922
|
+
if (ayrs) return runAyrsServe(ayrs, plan.argv);
|
|
1923
|
+
process.stderr.write(
|
|
1924
|
+
`ay serve: ${plan.feature} needs the Rust daemon (ayrs), which was not found ` +
|
|
1925
|
+
`(bun run build:rs, or reinstall)\n`,
|
|
1926
|
+
);
|
|
1927
|
+
return 1;
|
|
1928
|
+
}
|
|
1912
1929
|
if (plan.kind === "rust") {
|
|
1913
1930
|
const { findAyrsBinary } = await import("./rustBinary.ts");
|
|
1914
1931
|
const ayrs = findAyrsBinary();
|
|
@@ -1919,6 +1936,11 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1919
1936
|
);
|
|
1920
1937
|
} else {
|
|
1921
1938
|
rest = plan.rest;
|
|
1939
|
+
// Never let the TypeScript server quietly host its own room instead.
|
|
1940
|
+
if (rest[0] === "join-secret" || rest.includes("--join")) {
|
|
1941
|
+
process.stderr.write(`ay serve: ${plan.reason}: joining rooms needs the Rust daemon\n`);
|
|
1942
|
+
return 1;
|
|
1943
|
+
}
|
|
1922
1944
|
}
|
|
1923
1945
|
}
|
|
1924
1946
|
|
package/ts/serveRust.spec.ts
CHANGED
|
@@ -65,13 +65,30 @@ describe("planRustServe", () => {
|
|
|
65
65
|
kind: "ts",
|
|
66
66
|
reason: "--host",
|
|
67
67
|
});
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
).toMatchObject({
|
|
68
|
+
// Any room link spelling reaches ayrs as webrtc://; a non-link stays on TS.
|
|
69
|
+
expect(planRustServe(["--webrtc", "https://agent-yes.com/room/#room=r1&s=e1.ab"], env)).toEqual(
|
|
70
|
+
{ kind: "rust", argv: ["serve", "--webrtc", "webrtc://r1:e1.ab@s.agent-yes.com"] },
|
|
71
|
+
);
|
|
72
|
+
expect(planRustServe(["--webrtc", "nonsense"], env)).toMatchObject({
|
|
73
73
|
kind: "ts",
|
|
74
|
-
reason: expect.stringContaining("
|
|
74
|
+
reason: expect.stringContaining("not a room link"),
|
|
75
|
+
});
|
|
76
|
+
// Joining a room as a member is ayrs-only, and needs the room.
|
|
77
|
+
expect(planRustServe(["--webrtc", "webrtc://r1:e1.ab@s.example", "--join"], env)).toEqual({
|
|
78
|
+
kind: "rust-only",
|
|
79
|
+
argv: ["serve", "--webrtc", "webrtc://r1:e1.ab@s.example", "--join"],
|
|
80
|
+
feature: "--join",
|
|
81
|
+
});
|
|
82
|
+
expect(planRustServe(["--webrtc", "--join"], env)).toMatchObject({ kind: "ts" });
|
|
83
|
+
expect(planRustServe(["join-secret"], env)).toEqual({
|
|
84
|
+
kind: "rust-only",
|
|
85
|
+
argv: ["serve", "join-secret"],
|
|
86
|
+
feature: "join-secret",
|
|
87
|
+
});
|
|
88
|
+
expect(planRustServe(["join-secret", "--webrtc", "webrtc://r1:e1.ab@s.example"], env)).toEqual({
|
|
89
|
+
kind: "rust-only",
|
|
90
|
+
argv: ["serve", "--webrtc", "webrtc://r1:e1.ab@s.example", "join-secret"],
|
|
91
|
+
feature: "join-secret",
|
|
75
92
|
});
|
|
76
93
|
expect(planRustServe(["--port", "abc"], env)).toMatchObject({
|
|
77
94
|
kind: "ts",
|
package/ts/serveRust.ts
CHANGED
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
*
|
|
11
11
|
* What delegates today — exactly the flags `ayrs serve` accepts:
|
|
12
12
|
* --webrtc (bare: load/mint the persisted room)
|
|
13
|
-
* --webrtc <
|
|
13
|
+
* --webrtc <link> / --webrtc=<link> (a room link, any spelling; passed on
|
|
14
|
+
* as webrtc://)
|
|
15
|
+
* --join (host that room as one of its members; ayrs only)
|
|
16
|
+
* join-secret (print the link + join secret members need; ayrs only)
|
|
14
17
|
* --port <n> / --port=<n> (loopback HTTP console; 0 = free port)
|
|
15
18
|
* --sighost <host> / --sighost=<host>
|
|
16
19
|
*
|
|
@@ -22,10 +25,12 @@
|
|
|
22
25
|
* healthcheck) and -d/--daemon: ayrs has its own OS-supervisor install and
|
|
23
26
|
* the two must not manage each other's registration
|
|
24
27
|
* - --share (http+webrtc over Portless), --http, --host, --token, --tls-*,
|
|
25
|
-
* --local
|
|
28
|
+
* --local
|
|
26
29
|
*/
|
|
27
30
|
|
|
28
31
|
/** Subcommands `ay serve` dispatches before flag parsing — never delegated. */
|
|
32
|
+
import { toWebrtcUrl } from "./webrtcLink.ts";
|
|
33
|
+
|
|
29
34
|
const SERVE_SUBCOMMANDS = new Set([
|
|
30
35
|
"install",
|
|
31
36
|
"uninstall",
|
|
@@ -38,7 +43,9 @@ const SERVE_SUBCOMMANDS = new Set([
|
|
|
38
43
|
|
|
39
44
|
export type RustServePlan =
|
|
40
45
|
| { kind: "rust"; argv: string[] }
|
|
41
|
-
| { kind: "ts"; reason: string; rest: string[] }
|
|
46
|
+
| { kind: "ts"; reason: string; rest: string[] }
|
|
47
|
+
/** Only ayrs can do this; there is no TypeScript fallback. */
|
|
48
|
+
| { kind: "rust-only"; argv: string[]; feature: string };
|
|
42
49
|
|
|
43
50
|
/**
|
|
44
51
|
* Decide whether `rest` (the argv after `serve`) can run as `ayrs serve`.
|
|
@@ -60,6 +67,15 @@ export function planRustServe(
|
|
|
60
67
|
return { kind: "ts", reason: "bare `ay serve` (Portless HTTPS console)", rest };
|
|
61
68
|
}
|
|
62
69
|
const first = rest[0]!;
|
|
70
|
+
if (first === "join-secret") {
|
|
71
|
+
const flags = rest.slice(1);
|
|
72
|
+
const plan = flags.length
|
|
73
|
+
? planRustServe(flags, env)
|
|
74
|
+
: ({ kind: "rust", argv: ["serve"] } as const);
|
|
75
|
+
if (plan.kind !== "rust" || plan.argv.some((a) => a === "--port" || a === "--join"))
|
|
76
|
+
return { kind: "ts", reason: `\`ay serve join-secret\` takes only --webrtc/--sighost`, rest };
|
|
77
|
+
return { kind: "rust-only", argv: [...plan.argv, "join-secret"], feature: first };
|
|
78
|
+
}
|
|
63
79
|
if (SERVE_SUBCOMMANDS.has(first)) {
|
|
64
80
|
return { kind: "ts", reason: `\`ay serve ${first}\``, rest };
|
|
65
81
|
}
|
|
@@ -83,15 +99,21 @@ export function planRustServe(
|
|
|
83
99
|
switch (name) {
|
|
84
100
|
case "--webrtc": {
|
|
85
101
|
const v = takeNext();
|
|
102
|
+
const url = v === undefined ? undefined : toWebrtcUrl(v, { join: true });
|
|
86
103
|
if (v === undefined) {
|
|
87
104
|
argv.push("--webrtc");
|
|
88
|
-
} else if (
|
|
89
|
-
argv.push("--webrtc",
|
|
105
|
+
} else if (url) {
|
|
106
|
+
argv.push("--webrtc", url);
|
|
90
107
|
} else {
|
|
91
|
-
return { kind: "ts", reason: `--webrtc ${v} (
|
|
108
|
+
return { kind: "ts", reason: `--webrtc ${v} (not a room link)`, rest };
|
|
92
109
|
}
|
|
93
110
|
break;
|
|
94
111
|
}
|
|
112
|
+
case "--join": {
|
|
113
|
+
if (inline !== undefined) return { kind: "ts", reason: a, rest };
|
|
114
|
+
argv.push("--join");
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
95
117
|
case "--port": {
|
|
96
118
|
const v = takeNext();
|
|
97
119
|
if (v === undefined || !/^\d+$/.test(v)) {
|
|
@@ -110,6 +132,11 @@ export function planRustServe(
|
|
|
110
132
|
return { kind: "ts", reason: a, rest };
|
|
111
133
|
}
|
|
112
134
|
}
|
|
135
|
+
if (argv.includes("--join")) {
|
|
136
|
+
if (!argv.some((x) => x.startsWith("webrtc://")))
|
|
137
|
+
return { kind: "ts", reason: "--join needs --webrtc <room link>", rest };
|
|
138
|
+
return { kind: "rust-only", argv, feature: "--join" };
|
|
139
|
+
}
|
|
113
140
|
return { kind: "rust", argv };
|
|
114
141
|
}
|
|
115
142
|
|
|
@@ -129,5 +156,5 @@ export function daemonShareRuntime(
|
|
|
129
156
|
hasAyrs: () => boolean,
|
|
130
157
|
env: Record<string, string | undefined> = process.env,
|
|
131
158
|
): "ts" | "rust" {
|
|
132
|
-
return planRustServe(args, env).kind
|
|
159
|
+
return planRustServe(args, env).kind !== "ts" && hasAyrs() ? "rust" : "ts";
|
|
133
160
|
}
|
package/ts/subcommands.ts
CHANGED
|
@@ -2524,7 +2524,10 @@ async function runAllRemotesLs(opts: {
|
|
|
2524
2524
|
const rows: HostedRow[] = [];
|
|
2525
2525
|
for (const { host, records, cached } of byHost) {
|
|
2526
2526
|
for (const { record, prefix } of flattenForest(buildAgentForest(records))) {
|
|
2527
|
-
|
|
2527
|
+
// A room with several hosts tags each row with its member: `alias/member`
|
|
2528
|
+
// is also how `ay send alias:member:<agent>` addresses it.
|
|
2529
|
+
const where = typeof record.member === "string" ? `${host}/${record.member}` : host;
|
|
2530
|
+
rows.push({ host: where, rec: record, prefix, cached: cached === true });
|
|
2528
2531
|
}
|
|
2529
2532
|
}
|
|
2530
2533
|
|
package/ts/webrtcLink.spec.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_SIGHOST,
|
|
4
|
+
hasJoinSecret,
|
|
5
|
+
isWebrtcSpec,
|
|
6
|
+
parseWebrtcLink,
|
|
7
|
+
toWebrtcUrl,
|
|
8
|
+
} from "./webrtcLink.ts";
|
|
3
9
|
|
|
4
10
|
// A representative v2 share secret (e1.<64 hex>); parseSecret keeps the hex `s`.
|
|
5
11
|
const SECRET = "e1.982610a3034f065bfe9700037b306a6afeb7dc48567064058e6c4bbc09e502c2";
|
|
@@ -87,3 +93,22 @@ describe("toWebrtcUrl", () => {
|
|
|
87
93
|
expect(toWebrtcUrl("just-an-alias")).toBeNull();
|
|
88
94
|
});
|
|
89
95
|
});
|
|
96
|
+
|
|
97
|
+
describe("host links (view link + join secret)", () => {
|
|
98
|
+
const v = `e1.${"ab".repeat(32)}`;
|
|
99
|
+
const j = `j1.${"cd".repeat(32)}`;
|
|
100
|
+
|
|
101
|
+
it("keeps the join secret apart from the view secret, in every spelling", () => {
|
|
102
|
+
for (const link of [
|
|
103
|
+
`https://agent-yes.com/w/#r1:${v}:${j}`,
|
|
104
|
+
`https://agent-yes.com/room/#room=r1&s=${v}&j=${j}`,
|
|
105
|
+
`webrtc://r1:${v}:${j}@s.agent-yes.com`,
|
|
106
|
+
]) {
|
|
107
|
+
expect(hasJoinSecret(link)).toBe(true);
|
|
108
|
+
expect(parseWebrtcLink(link)).toMatchObject({ room: "r1", s: "ab".repeat(32) });
|
|
109
|
+
expect(toWebrtcUrl(link)).toBe(`webrtc://r1:${v}@s.agent-yes.com`);
|
|
110
|
+
expect(toWebrtcUrl(link, { join: true })).toBe(`webrtc://r1:${v}:${j}@s.agent-yes.com`);
|
|
111
|
+
}
|
|
112
|
+
expect(hasJoinSecret(`https://agent-yes.com/w/#r1:${v}`)).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|
package/ts/webrtcLink.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface WebrtcLink {
|
|
|
11
11
|
room: string;
|
|
12
12
|
s: string;
|
|
13
13
|
host: string;
|
|
14
|
+
/** The room's join secret (`j1.<64hex>`), on a HOST link only. */
|
|
15
|
+
join?: string;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/** room + RAW token (marker and all), before parseSecret strips the `e1.`. */
|
|
@@ -18,6 +20,18 @@ interface RawLink {
|
|
|
18
20
|
room: string;
|
|
19
21
|
token: string;
|
|
20
22
|
host: string;
|
|
23
|
+
join?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// A HOST link is a view link that also carries the room's join secret, for the
|
|
27
|
+
// machines that host in the room as its members (`ay serve --webrtc <host link>
|
|
28
|
+
// --join`): `#<room>:e1.<view>:j1.<join>[@<sighost>]`, `&j=j1.<join>` in the
|
|
29
|
+
// key=value form, `webrtc://<room>:e1.<view>:j1.<join>@<host>`. Viewers ignore
|
|
30
|
+
// the join part; it lets a machine host in the room, so keep host links private.
|
|
31
|
+
function splitJoin(raw: RawLink): RawLink {
|
|
32
|
+
const i = raw.token.indexOf(":j1.");
|
|
33
|
+
if (i < 0) return raw;
|
|
34
|
+
return { ...raw, token: raw.token.slice(0, i), join: raw.token.slice(i + 1) };
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
// The fragment of a /room/ link is a key=value set, parsed with URLSearchParams:
|
|
@@ -40,19 +54,24 @@ function parseFragment(frag: string): RawLink | null {
|
|
|
40
54
|
const room = q.get("room");
|
|
41
55
|
const token = q.get("s");
|
|
42
56
|
if (!room || !token) return null;
|
|
43
|
-
|
|
57
|
+
const join = q.get("j") || undefined;
|
|
58
|
+
return { room, token, host: q.get("sig") || DEFAULT_SIGHOST, ...(join ? { join } : {}) };
|
|
44
59
|
}
|
|
45
60
|
const at = frag.split("@");
|
|
46
61
|
const seg = at[0]!;
|
|
47
62
|
const i = seg.indexOf(":");
|
|
48
63
|
if (i < 0) return null;
|
|
49
|
-
return {
|
|
64
|
+
return splitJoin({
|
|
65
|
+
room: seg.slice(0, i),
|
|
66
|
+
token: seg.slice(i + 1),
|
|
67
|
+
host: at[1] || DEFAULT_SIGHOST,
|
|
68
|
+
});
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
/** Parse a share link WITHOUT validating/stripping the secret's version marker. */
|
|
53
72
|
function parseRaw(link: string): RawLink | null {
|
|
54
73
|
const wr = /^webrtc:\/\/([^:]+):([^@]+)@(.+)$/.exec(link);
|
|
55
|
-
if (wr) return { room: wr[1]!, token: wr[2]!, host: wr[3]! };
|
|
74
|
+
if (wr) return splitJoin({ room: wr[1]!, token: wr[2]!, host: wr[3]! });
|
|
56
75
|
if (/^https?:\/\//.test(link) && link.includes("#"))
|
|
57
76
|
return parseFragment(link.split("#")[1] ?? "");
|
|
58
77
|
return null;
|
|
@@ -78,9 +97,17 @@ export function parseWebrtcLink(link: string): WebrtcLink | null {
|
|
|
78
97
|
* Returns the RAW token, so a legacy markerless room round-trips unchanged
|
|
79
98
|
* instead of being handed a marker it never had.
|
|
80
99
|
*/
|
|
81
|
-
export function toWebrtcUrl(link: string): string | null {
|
|
100
|
+
export function toWebrtcUrl(link: string, opts: { join?: boolean } = {}): string | null {
|
|
82
101
|
const raw = parseRaw(link);
|
|
83
|
-
|
|
102
|
+
if (!raw) return null;
|
|
103
|
+
// The join secret goes on only where a member host needs it (ayrs --join).
|
|
104
|
+
const join = opts.join && raw.join ? `:${raw.join}` : "";
|
|
105
|
+
return `webrtc://${raw.room}:${raw.token}${join}@${raw.host}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** True when the link is a HOST link (carries the room's join secret). */
|
|
109
|
+
export function hasJoinSecret(link: string): boolean {
|
|
110
|
+
return !!parseRaw(link)?.join;
|
|
84
111
|
}
|
|
85
112
|
|
|
86
113
|
/** True if `spec` looks like a WebRTC share link (vs. an http remote or alias). */
|