agent-yes 1.300.3 → 1.300.4
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-CWJr4TJH.js → SUPPORTED_CLIS-BIg_X-jE.js} +2 -2
- package/dist/{SUPPORTED_CLIS-ChSO8FiS.js → SUPPORTED_CLIS-D7lOxOnw.js} +3 -3
- package/dist/{agentShare-NkW0b0mg.js → agentShare-BfHUZgk4.js} +4 -4
- package/dist/{callback-CjlaFXp1.js → callback-COR0F2eB.js} +3 -3
- package/dist/{callback-B8CUyag4.js → callback-yWhCmi4k.js} +3 -3
- package/dist/{channels-aYwLMbFV.js → channels-rhxQArqN.js} +2 -2
- package/dist/cli.js +5 -5
- package/dist/{cmdPs-C8DIl8V6.js → cmdPs-B3jTE4f3.js} +3 -3
- package/dist/index.js +2 -2
- package/dist/{notifyDaemon-DWIfzEE7.js → notifyDaemon-BIo7Nkk2.js} +3 -3
- package/dist/{parentPingLoop-Bh3KuFcW.js → parentPingLoop-9T4Pzp6c.js} +2 -2
- package/dist/{remotes-B4v0Nowo.js → remotes-B4CNXwDS.js} +2 -2
- package/dist/{remotes-DnMQWaqQ.js → remotes-CQTEY8Gt.js} +1 -1
- package/dist/{rustBinary-gI_r3g9K.js → rustBinary-CJXUPUWd.js} +48 -5
- package/dist/rustBinary-FhGG3Yfl.js +4 -0
- package/dist/{schedule-DryC--yx.js → schedule-DmijwnTA.js} +4 -4
- package/dist/{serve-DVoYVgBo.js → serve-F5tXYO0-.js} +99 -32
- package/dist/{serveRust-DDQiZmMf.js → serveRust-BAawe83i.js} +16 -2
- package/dist/{setup--1zRHaGW.js → setup-mCb1JbTX.js} +2 -2
- package/dist/{share-C9oGvjXT.js → share-CLataFnI.js} +18 -12
- package/dist/share-nmgSIjYP.js +4 -0
- package/dist/{shareCmd-BI3hRWGO.js → shareCmd-aOKimJeT.js} +25 -7
- package/dist/{subcommands-BviFOQZE.js → subcommands-DRq0MgSm.js} +2 -2
- package/dist/{subcommands-UWLkF5pq.js → subcommands-Ddo2TYZs.js} +404 -77
- package/dist/{terminal-DviH1E3C.js → terminal-DEXrWOa9.js} +2 -2
- package/dist/trayApp-0RVjo43f.js +5 -0
- package/dist/{trayApp-PJrwfyDs.js → trayApp-BUXx7cES.js} +2 -2
- package/dist/{ts--JmSY117.js → ts-CRbycTni.js} +5 -5
- package/dist/{versionChecker-CCGSbjio.js → versionChecker-DeQEhRGB.js} +2 -2
- package/dist/{webrtcRemote-Cd4vTrgv.js → webrtcRemote-Cy5kB8aR.js} +115 -14
- package/dist/{widget-DgBDIGRQ.js → widget-DneXqnL8.js} +3 -3
- package/dist/{ws-DnBg8-g-.js → ws-DPw1lVTi.js} +3 -3
- package/dist/{ws-dc7MP054.js → ws-Dr-0Ckq7.js} +2 -2
- package/package.json +1 -1
- package/ts/remoteCache.bun.spec.ts +191 -0
- package/ts/remoteCache.spec.ts +191 -0
- package/ts/remoteCache.ts +193 -0
- package/ts/rustBinary.bun.spec.ts +39 -2
- package/ts/rustBinary.spec.ts +53 -2
- package/ts/rustBinary.ts +61 -3
- package/ts/serve.spec.ts +103 -0
- package/ts/serve.ts +100 -15
- package/ts/serveRust.spec.ts +24 -1
- package/ts/serveRust.ts +19 -0
- package/ts/share.bun.spec.ts +54 -2
- package/ts/share.spec.ts +54 -2
- package/ts/share.ts +40 -15
- package/ts/shareCmd.ts +33 -2
- package/ts/strayServe.ts +1 -0
- package/ts/subcommands.spec.ts +36 -3
- package/ts/subcommands.ts +384 -98
- package/ts/webrtcRemote.ts +66 -20
- package/ts/webrtcSignal.bun.spec.ts +120 -0
- package/ts/webrtcSignal.spec.ts +120 -0
- package/ts/webrtcSignal.ts +143 -0
- package/dist/rustBinary-BNmAJEBb.js +0 -4
- package/dist/share-Bmnd_qT0.js +0 -4
- package/dist/trayApp-BDIb8fOj.js +0 -5
package/ts/serve.spec.ts
CHANGED
|
@@ -2,6 +2,9 @@ import { readFile } from "fs/promises";
|
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
import {
|
|
4
4
|
installerArgv,
|
|
5
|
+
LEGACY_AYRS_TASK,
|
|
6
|
+
legacyAyrsTaskArgv,
|
|
7
|
+
serveLauncher,
|
|
5
8
|
probeDaemon,
|
|
6
9
|
isNoNodeExecError,
|
|
7
10
|
oxmgrCmd,
|
|
@@ -284,3 +287,103 @@ describe("vendorAssetType", () => {
|
|
|
284
287
|
expect(vendorAssetType("")).toBeNull();
|
|
285
288
|
});
|
|
286
289
|
});
|
|
290
|
+
|
|
291
|
+
// The daemon manager stores this argv verbatim and re-runs it on every
|
|
292
|
+
// restart: a baked-in `~/.bun/bin/ay` symlink (recreated on each global
|
|
293
|
+
// upgrade, absent on some images until the first one) or a bare `ay` that a
|
|
294
|
+
// manager's minimal PATH cannot resolve both turn into an ENOENT restart loop.
|
|
295
|
+
describe("serveLauncher", () => {
|
|
296
|
+
const realpath = (p: string) =>
|
|
297
|
+
p.replace(
|
|
298
|
+
"/home/alice/.bun/bin/ay",
|
|
299
|
+
"/home/alice/.bun/install/global/node_modules/agent-yes/dist/cli.js",
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
it("resolves the PATH symlink to the real script and runs it under bun", () => {
|
|
303
|
+
expect(
|
|
304
|
+
serveLauncher({
|
|
305
|
+
which: "/home/alice/.bun/bin/ay",
|
|
306
|
+
script: "/x/cli.js",
|
|
307
|
+
execPath: "/usr/bin/bun",
|
|
308
|
+
platform: "linux",
|
|
309
|
+
realpath,
|
|
310
|
+
}),
|
|
311
|
+
).toEqual([
|
|
312
|
+
"/usr/bin/bun",
|
|
313
|
+
"/home/alice/.bun/install/global/node_modules/agent-yes/dist/cli.js",
|
|
314
|
+
]);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("falls back to the running script, never to a bare `ay`, when nothing is on PATH", () => {
|
|
318
|
+
expect(
|
|
319
|
+
serveLauncher({
|
|
320
|
+
which: null,
|
|
321
|
+
script: "/repo/alpha/dist/cli.js",
|
|
322
|
+
execPath: "/usr/bin/bun",
|
|
323
|
+
platform: "linux",
|
|
324
|
+
realpath,
|
|
325
|
+
}),
|
|
326
|
+
).toEqual(["/usr/bin/bun", "/repo/alpha/dist/cli.js"]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("survives a realpath failure by keeping the path it was given", () => {
|
|
330
|
+
const boom = () => {
|
|
331
|
+
throw new Error("ENOENT");
|
|
332
|
+
};
|
|
333
|
+
expect(
|
|
334
|
+
serveLauncher({
|
|
335
|
+
which: "/opt/ay",
|
|
336
|
+
script: undefined,
|
|
337
|
+
execPath: "/usr/bin/bun",
|
|
338
|
+
platform: "linux",
|
|
339
|
+
realpath: boom,
|
|
340
|
+
}),
|
|
341
|
+
).toEqual(["/usr/bin/bun", "/opt/ay"]);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("runs a Windows exe shim directly and a script under bun", () => {
|
|
345
|
+
const id = (p: string) => p;
|
|
346
|
+
expect(
|
|
347
|
+
serveLauncher({
|
|
348
|
+
which: "C:/Users/alice/.bun/bin/ay.exe",
|
|
349
|
+
script: undefined,
|
|
350
|
+
execPath: "C:/bun.exe",
|
|
351
|
+
platform: "win32",
|
|
352
|
+
realpath: id,
|
|
353
|
+
}),
|
|
354
|
+
).toEqual(["C:/Users/alice/.bun/bin/ay.exe"]);
|
|
355
|
+
expect(
|
|
356
|
+
serveLauncher({
|
|
357
|
+
which: null,
|
|
358
|
+
script: "C:/repo/alpha/dist/cli.js",
|
|
359
|
+
execPath: "C:/bun.exe",
|
|
360
|
+
platform: "win32",
|
|
361
|
+
realpath: id,
|
|
362
|
+
}),
|
|
363
|
+
).toEqual(["C:/bun.exe", "C:/repo/alpha/dist/cli.js"]);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// A scheduled-task host from before the oxmgr move keeps the WebRTC room; the
|
|
368
|
+
// new daemon then waits on it forever. Install and uninstall both clear it.
|
|
369
|
+
describe("legacyAyrsTaskArgv", () => {
|
|
370
|
+
it("names the task ayrs serve install registered, with the right schtasks verbs", () => {
|
|
371
|
+
expect(LEGACY_AYRS_TASK).toBe("com.snomiao.ayrs-serve");
|
|
372
|
+
expect(legacyAyrsTaskArgv("query")).toEqual([
|
|
373
|
+
"schtasks",
|
|
374
|
+
"/query",
|
|
375
|
+
"/tn",
|
|
376
|
+
LEGACY_AYRS_TASK,
|
|
377
|
+
"/fo",
|
|
378
|
+
"LIST",
|
|
379
|
+
]);
|
|
380
|
+
expect(legacyAyrsTaskArgv("end")).toEqual(["schtasks", "/end", "/tn", LEGACY_AYRS_TASK]);
|
|
381
|
+
expect(legacyAyrsTaskArgv("delete")).toEqual([
|
|
382
|
+
"schtasks",
|
|
383
|
+
"/delete",
|
|
384
|
+
"/tn",
|
|
385
|
+
LEGACY_AYRS_TASK,
|
|
386
|
+
"/f",
|
|
387
|
+
]);
|
|
388
|
+
});
|
|
389
|
+
});
|
package/ts/serve.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
unlink,
|
|
11
11
|
writeFile,
|
|
12
12
|
} from "fs/promises";
|
|
13
|
-
import { existsSync, renameSync, watch, writeFileSync } from "node:fs";
|
|
13
|
+
import { existsSync, realpathSync, renameSync, watch, writeFileSync } from "node:fs";
|
|
14
14
|
import { execFileSync } from "node:child_process";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
@@ -949,16 +949,72 @@ export function oxmgrCmd(argv: string[]): string {
|
|
|
949
949
|
return argv.map((a) => (/[\\\s"]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a)).join(" ");
|
|
950
950
|
}
|
|
951
951
|
|
|
952
|
+
/**
|
|
953
|
+
* The argv a daemon manager runs for `ay serve …`. Pure so the contract is
|
|
954
|
+
* testable: `which` is the `ay` on PATH (or null), `script` is THIS process's
|
|
955
|
+
* own entry (`process.argv[1]`, always present), `realpath` resolves symlinks.
|
|
956
|
+
*
|
|
957
|
+
* Never bake in the `~/.bun/bin/ay` symlink: `bun add -g` removes and recreates
|
|
958
|
+
* it on every upgrade, and some images ship without it until the first update
|
|
959
|
+
* — a manager that stored it then restarts into ENOENT thousands of times.
|
|
960
|
+
* Resolve to the real file instead, and when nothing is on PATH fall back to
|
|
961
|
+
* the script running this very install (never a bare `ay`, which a manager's
|
|
962
|
+
* minimal PATH cannot resolve). On POSIX the entry is a `#!/usr/bin/env bun`
|
|
963
|
+
* script run via bun; on Windows it is a self-contained exe.
|
|
964
|
+
*/
|
|
965
|
+
export function serveLauncher(opts: {
|
|
966
|
+
which: string | null;
|
|
967
|
+
script: string | undefined;
|
|
968
|
+
execPath: string;
|
|
969
|
+
platform: NodeJS.Platform;
|
|
970
|
+
realpath: (p: string) => string;
|
|
971
|
+
}): string[] {
|
|
972
|
+
const resolve = (p: string) => {
|
|
973
|
+
try {
|
|
974
|
+
return opts.realpath(p);
|
|
975
|
+
} catch {
|
|
976
|
+
return p;
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
const entry = opts.which ? resolve(opts.which) : opts.script ? resolve(opts.script) : null;
|
|
980
|
+
if (!entry) return ["ay"];
|
|
981
|
+
if (opts.platform === "win32")
|
|
982
|
+
return /\.(exe|cmd|bat)$/i.test(entry) ? [entry] : [opts.execPath, entry];
|
|
983
|
+
return [opts.execPath, entry];
|
|
984
|
+
}
|
|
985
|
+
|
|
952
986
|
function ayServeArgv(args: string[]): string[] {
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
:
|
|
987
|
+
const launcher = serveLauncher({
|
|
988
|
+
which: Bun.which("ay"),
|
|
989
|
+
script: process.argv[1],
|
|
990
|
+
execPath: process.execPath,
|
|
991
|
+
platform: process.platform,
|
|
992
|
+
realpath: realpathSync,
|
|
993
|
+
});
|
|
959
994
|
return [...launcher, "serve", ...args];
|
|
960
995
|
}
|
|
961
996
|
|
|
997
|
+
// The Task Scheduler entry `ayrs serve install` registered before the daemon
|
|
998
|
+
// moved under oxmgr. A leftover one keeps hosting the WebRTC room, and the new
|
|
999
|
+
// daemon then waits forever on "another ayrs host is already serving room …".
|
|
1000
|
+
// Mirrors rs/src/serve/service.rs LABEL / uninstall_quiet.
|
|
1001
|
+
export const LEGACY_AYRS_TASK = "com.snomiao.ayrs-serve";
|
|
1002
|
+
|
|
1003
|
+
export function legacyAyrsTaskArgv(step: "query" | "end" | "delete"): string[] {
|
|
1004
|
+
const base = ["schtasks", `/${step}`, "/tn", LEGACY_AYRS_TASK];
|
|
1005
|
+
return step === "query" ? [...base, "/fo", "LIST"] : step === "delete" ? [...base, "/f"] : base;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async function stopLegacyAyrsTask(): Promise<void> {
|
|
1009
|
+
if (process.platform !== "win32") return;
|
|
1010
|
+
if ((await spawnExit(legacyAyrsTaskArgv("query"))) !== 0) return; // not registered
|
|
1011
|
+
process.stdout.write(
|
|
1012
|
+
`stopping legacy scheduled task '${LEGACY_AYRS_TASK}' (it would keep hosting the WebRTC room)\n`,
|
|
1013
|
+
);
|
|
1014
|
+
await spawnExit(legacyAyrsTaskArgv("end"));
|
|
1015
|
+
await spawnExit(legacyAyrsTaskArgv("delete"));
|
|
1016
|
+
}
|
|
1017
|
+
|
|
962
1018
|
// Per-user login auto-start entry on Windows. pm2 core has no Windows startup
|
|
963
1019
|
// integration (`pm2 startup` errors "Init system not found"), so we register a
|
|
964
1020
|
// HKCU Run value that runs `pm2 resurrect` at login — no admin, removed on
|
|
@@ -1197,6 +1253,14 @@ async function warnStrayServeProcesses(): Promise<void> {
|
|
|
1197
1253
|
// mints a persisted room instead). Mirrors how cmdServe resolves argv.webrtc/argv.share,
|
|
1198
1254
|
// but over the raw arg list install holds (oxmgr splits the command on whitespace →
|
|
1199
1255
|
// `--webrtc url`; pm2/`=` → `--webrtc=url`).
|
|
1256
|
+
// Which runtime a daemon registered with `args` hosts its room under — see
|
|
1257
|
+
// serveRust.ts daemonShareRuntime; the binary check is the live one cmdServe makes.
|
|
1258
|
+
async function daemonShareRuntime(args: string[]): Promise<"ts" | "rust"> {
|
|
1259
|
+
const { daemonShareRuntime: decide } = await import("./serveRust.ts");
|
|
1260
|
+
const { findAyrsBinary } = await import("./rustBinary.ts");
|
|
1261
|
+
return decide(args, () => !!findAyrsBinary());
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1200
1264
|
function explicitWebrtcUrl(args: string[]): string | undefined {
|
|
1201
1265
|
for (let i = 0; i < args.length; i++) {
|
|
1202
1266
|
const a = args[i]!;
|
|
@@ -1329,6 +1393,7 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
|
|
|
1329
1393
|
}
|
|
1330
1394
|
if (removed === 0 && failed === 0)
|
|
1331
1395
|
process.stdout.write(`no '${DAEMON_NAME}' daemon registered with any manager\n`);
|
|
1396
|
+
await stopLegacyAyrsTask();
|
|
1332
1397
|
await warnStrayServeProcesses();
|
|
1333
1398
|
return failed ? 1 : 0;
|
|
1334
1399
|
}
|
|
@@ -1380,11 +1445,20 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
|
|
|
1380
1445
|
const webrtcDaemon = effArgs.some((a) => a.startsWith("--webrtc") || a.startsWith("--share"));
|
|
1381
1446
|
let shareLink: string | null = null;
|
|
1382
1447
|
let shareLinkMinted = false; // auto-minted (persisted/rotatable) vs explicit URL
|
|
1448
|
+
// The daemon's `ay serve …` delegates to ayrs whenever it can (serveRust.ts),
|
|
1449
|
+
// and ayrs persists its room in a DIFFERENT file (.share-room-ayrs) — mint
|
|
1450
|
+
// and print from the file of the runtime that will actually host.
|
|
1451
|
+
let roomFile = ".share-room";
|
|
1383
1452
|
if (webrtcDaemon) {
|
|
1384
1453
|
try {
|
|
1385
|
-
const { loadOrCreateShareRoom, shareLinkFromRoomUrl } =
|
|
1454
|
+
const { loadOrCreateShareRoom, shareLinkFromRoomUrl, shareRoomFileName } =
|
|
1455
|
+
await import("./share.ts");
|
|
1456
|
+
const runtime = await daemonShareRuntime(effArgs);
|
|
1457
|
+
roomFile = shareRoomFileName(runtime);
|
|
1386
1458
|
const explicit = explicitWebrtcUrl(effArgs);
|
|
1387
|
-
shareLink = shareLinkFromRoomUrl(
|
|
1459
|
+
shareLink = shareLinkFromRoomUrl(
|
|
1460
|
+
explicit ?? (await loadOrCreateShareRoom(undefined, runtime)),
|
|
1461
|
+
);
|
|
1388
1462
|
shareLinkMinted = !explicit;
|
|
1389
1463
|
} catch {
|
|
1390
1464
|
/* best effort — fall back to the .share-link file hint in emitShareLink */
|
|
@@ -1397,7 +1471,7 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
|
|
|
1397
1471
|
`\nshared over WebRTC — open this link (the token is eaten from the URL on open):\n` +
|
|
1398
1472
|
` ${shareLink}\n` +
|
|
1399
1473
|
(shareLinkMinted
|
|
1400
|
-
? ` (persistent room — same link across restarts; delete ~/.agent-yes
|
|
1474
|
+
? ` (persistent room — same link across restarts; delete ~/.agent-yes/${roomFile} to rotate)\n`
|
|
1401
1475
|
: ``),
|
|
1402
1476
|
);
|
|
1403
1477
|
// Offer to jump straight into the console (default yes); no-ops on a
|
|
@@ -1407,8 +1481,8 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
|
|
|
1407
1481
|
} else
|
|
1408
1482
|
process.stdout.write(
|
|
1409
1483
|
`\nthe WebRTC share link carries a secret, so the daemon does NOT log it —\n` +
|
|
1410
|
-
`read it
|
|
1411
|
-
`~/.agent-yes
|
|
1484
|
+
`read it with \`ay share status\`. The room persists in\n` +
|
|
1485
|
+
`~/.agent-yes/${roomFile}, so the link survives restarts.\n`,
|
|
1412
1486
|
);
|
|
1413
1487
|
};
|
|
1414
1488
|
|
|
@@ -1439,6 +1513,9 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
|
|
|
1439
1513
|
await spawnExit([mgr.bin, "stop", DAEMON_NAME]);
|
|
1440
1514
|
await spawnExit([mgr.bin, "delete", DAEMON_NAME]);
|
|
1441
1515
|
}
|
|
1516
|
+
// A pre-oxmgr scheduled-task host left behind would keep the WebRTC room
|
|
1517
|
+
// and the new daemon would wait on it forever.
|
|
1518
|
+
await stopLegacyAyrsTask();
|
|
1442
1519
|
await warnStrayServeProcesses();
|
|
1443
1520
|
|
|
1444
1521
|
// oxmgr takes the command as one string; pm2 takes the binary plus its
|
|
@@ -1627,11 +1704,18 @@ async function cmdServeStatus(args: string[]): Promise<number> {
|
|
|
1627
1704
|
const json = args.includes("--json");
|
|
1628
1705
|
const mgr = await resolveActiveManager();
|
|
1629
1706
|
const token = await loadTokenReadOnly();
|
|
1630
|
-
const shareLink = await readShareLink();
|
|
1631
1707
|
|
|
1632
1708
|
// A non-null arg list means the daemon is registered with the manager.
|
|
1633
1709
|
const daemonArgs = mgr ? await readDaemonServeArgs(mgr) : null;
|
|
1634
1710
|
const installed = daemonArgs !== null;
|
|
1711
|
+
// The link of the room the registered daemon HOSTS: derived read-only from
|
|
1712
|
+
// the room file of its runtime (ayrs by default — a different file from the
|
|
1713
|
+
// TS host's). `.share-link` is only ever written by the TS host, so for a
|
|
1714
|
+
// Rust daemon it is stale or absent; keep it as the fallback for TS daemons
|
|
1715
|
+
// and for the uninstalled-but-left-over case shareLinkStatus flags.
|
|
1716
|
+
const { daemonShareLink } = await import("./shareCmd.ts");
|
|
1717
|
+
const shareLink =
|
|
1718
|
+
(installed ? await daemonShareLink(daemonArgs) : null) ?? (await readShareLink());
|
|
1635
1719
|
// Manager-reported process state — the only up/down signal for webrtc daemons.
|
|
1636
1720
|
const running = installed ? await readDaemonRunning(mgr!) : false;
|
|
1637
1721
|
const a = daemonArgs ?? [];
|
|
@@ -1781,8 +1865,9 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1781
1865
|
` agent-yes.com, or pass a room link to join that room:\n` +
|
|
1782
1866
|
` https://agent-yes.com/room/#room=<id>&s=e1.<secret>).\n` +
|
|
1783
1867
|
` Alone it needs NO port — combine with --http for both.\n` +
|
|
1784
|
-
` The minted room persists in ~/.agent-yes/.share-room\n` +
|
|
1785
|
-
` (stable link across restarts
|
|
1868
|
+
` The minted room persists in ~/.agent-yes/.share-room-ayrs\n` +
|
|
1869
|
+
` (.share-room under --no-rust); stable link across restarts,\n` +
|
|
1870
|
+
` delete the file to rotate.\n` +
|
|
1786
1871
|
` --share [URL] Legacy alias for --http --webrtc\n\n` +
|
|
1787
1872
|
`Runtime: like \`ay <cli>\`, the Rust daemon (ayrs serve) runs by default for\n` +
|
|
1788
1873
|
` the flags it supports — --webrtc [webrtc://url], --port N, --sighost H.\n` +
|
package/ts/serveRust.spec.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { planRustServe } from "./serveRust.ts";
|
|
2
|
+
import { daemonShareRuntime, planRustServe } from "./serveRust.ts";
|
|
3
3
|
|
|
4
4
|
// `ay serve` runs the Rust daemon (`ayrs serve`) by default, like `ay <cli>`
|
|
5
5
|
// runs the Rust agent runtime — but only for invocations ayrs can express.
|
|
@@ -83,3 +83,26 @@ describe("planRustServe", () => {
|
|
|
83
83
|
});
|
|
84
84
|
});
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
// Every printed share link must come from the room file of the runtime that
|
|
88
|
+
// hosts the room: ayrs persists its room in .share-room-ayrs, TS in .share-room.
|
|
89
|
+
// A link derived from the wrong file points at a room nobody serves.
|
|
90
|
+
describe("daemonShareRuntime", () => {
|
|
91
|
+
const env = {};
|
|
92
|
+
const ayrs = () => true;
|
|
93
|
+
const noAyrs = () => false;
|
|
94
|
+
|
|
95
|
+
it("is rust for the args ayrs serve accepts, when the binary exists", () => {
|
|
96
|
+
expect(daemonShareRuntime(["--webrtc"], ayrs, env)).toBe("rust");
|
|
97
|
+
expect(daemonShareRuntime(["--webrtc", "--sighost", "s.example"], ayrs, env)).toBe("rust");
|
|
98
|
+
expect(daemonShareRuntime(["--webrtc", "--port", "0"], ayrs, env)).toBe("rust");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("is ts whenever cmdServe would fall back to the TypeScript host", () => {
|
|
102
|
+
expect(daemonShareRuntime(["--webrtc"], noAyrs, env)).toBe("ts");
|
|
103
|
+
expect(daemonShareRuntime(["--webrtc", "--no-rust"], ayrs, env)).toBe("ts");
|
|
104
|
+
expect(daemonShareRuntime(["--webrtc"], ayrs, { AGENT_YES_NO_RUST: "1" })).toBe("ts");
|
|
105
|
+
expect(daemonShareRuntime(["--share"], ayrs, env)).toBe("ts");
|
|
106
|
+
expect(daemonShareRuntime([], ayrs, env)).toBe("ts");
|
|
107
|
+
});
|
|
108
|
+
});
|
package/ts/serveRust.ts
CHANGED
|
@@ -112,3 +112,22 @@ export function planRustServe(
|
|
|
112
112
|
}
|
|
113
113
|
return { kind: "rust", argv };
|
|
114
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Which runtime a daemon registered with these `ay serve` args actually runs —
|
|
118
|
+
* and therefore which persisted room file holds the room it hosts
|
|
119
|
+
* (`.share-room` for TypeScript, `.share-room-ayrs` for ayrs; see share.ts).
|
|
120
|
+
*
|
|
121
|
+
* Mirrors cmdServe exactly: an argv that planRustServe delegates runs under
|
|
122
|
+
* ayrs whenever the binary exists, and falls back to TypeScript otherwise. Any
|
|
123
|
+
* link we print for a daemon must be derived through this, or `ay share` hands
|
|
124
|
+
* out a link to a room nobody serves. `hasAyrs` is injected so the decision
|
|
125
|
+
* stays a pure function for tests.
|
|
126
|
+
*/
|
|
127
|
+
export function daemonShareRuntime(
|
|
128
|
+
args: string[],
|
|
129
|
+
hasAyrs: () => boolean,
|
|
130
|
+
env: Record<string, string | undefined> = process.env,
|
|
131
|
+
): "ts" | "rust" {
|
|
132
|
+
return planRustServe(args, env).kind === "rust" && hasAyrs() ? "rust" : "ts";
|
|
133
|
+
}
|
package/ts/share.bun.spec.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, it, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
formatRoomLink,
|
|
4
|
+
loadOrCreateShareRoom,
|
|
5
|
+
readShareRoom,
|
|
6
|
+
shareLinkFromRoomUrl,
|
|
7
|
+
shareRoomFileName,
|
|
8
|
+
shareRoomPath,
|
|
9
|
+
} from "./share.ts";
|
|
10
|
+
import { mkdtemp, readFile, rm, writeFile } from "fs/promises";
|
|
11
|
+
import { tmpdir } from "os";
|
|
12
|
+
import path from "path";
|
|
3
13
|
import { parseWebrtcLink, toWebrtcUrl } from "./webrtcLink.ts";
|
|
4
14
|
import { MARKER } from "../lab/ui/e2e.js";
|
|
5
15
|
|
|
@@ -56,3 +66,45 @@ describe("formatRoomLink \u2194 the CLI parser", () => {
|
|
|
56
66
|
expect(parseWebrtcLink(link)).toMatchObject({ room: "r1a2b3c", host: "localhost:7778" });
|
|
57
67
|
});
|
|
58
68
|
});
|
|
69
|
+
|
|
70
|
+
// The two hosts persist their rooms in different files (same format). Minting
|
|
71
|
+
// and reading must target the file of the runtime that hosts, and a read must
|
|
72
|
+
// never mint — `ay serve status` is read-only.
|
|
73
|
+
describe("share room files per runtime", () => {
|
|
74
|
+
let home: string;
|
|
75
|
+
const prev = process.env.AGENT_YES_HOME;
|
|
76
|
+
beforeEach(async () => {
|
|
77
|
+
home = await mkdtemp(path.join(tmpdir(), "ay-share-room-"));
|
|
78
|
+
process.env.AGENT_YES_HOME = home;
|
|
79
|
+
});
|
|
80
|
+
afterEach(async () => {
|
|
81
|
+
if (prev === undefined) delete process.env.AGENT_YES_HOME;
|
|
82
|
+
else process.env.AGENT_YES_HOME = prev;
|
|
83
|
+
await rm(home, { recursive: true, force: true });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("maps each runtime to its own file", () => {
|
|
87
|
+
expect(shareRoomFileName("ts")).toBe(".share-room");
|
|
88
|
+
expect(shareRoomFileName("rust")).toBe(".share-room-ayrs");
|
|
89
|
+
expect(shareRoomPath("rust")).toBe(path.join(home, ".share-room-ayrs"));
|
|
90
|
+
expect(shareRoomPath()).toBe(path.join(home, ".share-room"));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("mints into the rust file and leaves the ts file alone", async () => {
|
|
94
|
+
const url = await loadOrCreateShareRoom("s.example", "rust");
|
|
95
|
+
expect(url).toMatch(/^webrtc:\/\/r[0-9a-f]{12}:e1\.[0-9a-f]{64}@s\.example$/);
|
|
96
|
+
expect((await readFile(path.join(home, ".share-room-ayrs"), "utf-8")).trim()).toBe(url);
|
|
97
|
+
await expect(readFile(path.join(home, ".share-room"))).rejects.toThrow();
|
|
98
|
+
// Stable: a second load returns the persisted room, no re-mint.
|
|
99
|
+
expect(await loadOrCreateShareRoom("s.example", "rust")).toBe(url);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("reads without minting, and ignores a legacy unencrypted room", async () => {
|
|
103
|
+
expect(await readShareRoom("rust")).toBeNull();
|
|
104
|
+
await expect(readFile(path.join(home, ".share-room-ayrs"))).rejects.toThrow();
|
|
105
|
+
await writeFile(path.join(home, ".share-room"), `webrtc://room0:${S}@s.example`);
|
|
106
|
+
expect(await readShareRoom("ts")).toBeNull();
|
|
107
|
+
await writeFile(path.join(home, ".share-room"), `webrtc://room0:${TOK}@s.example`);
|
|
108
|
+
expect(await readShareRoom("ts")).toBe(`webrtc://room0:${TOK}@s.example`);
|
|
109
|
+
});
|
|
110
|
+
});
|
package/ts/share.spec.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
formatRoomLink,
|
|
4
|
+
loadOrCreateShareRoom,
|
|
5
|
+
readShareRoom,
|
|
6
|
+
shareLinkFromRoomUrl,
|
|
7
|
+
shareRoomFileName,
|
|
8
|
+
shareRoomPath,
|
|
9
|
+
} from "./share.ts";
|
|
10
|
+
import { mkdtemp, readFile, rm, writeFile } from "fs/promises";
|
|
11
|
+
import { tmpdir } from "os";
|
|
12
|
+
import path from "path";
|
|
3
13
|
import { parseWebrtcLink, toWebrtcUrl } from "./webrtcLink.ts";
|
|
4
14
|
import { MARKER } from "../lab/ui/e2e.js";
|
|
5
15
|
|
|
@@ -56,3 +66,45 @@ describe("formatRoomLink \u2194 the CLI parser", () => {
|
|
|
56
66
|
expect(parseWebrtcLink(link)).toMatchObject({ room: "r1a2b3c", host: "localhost:7778" });
|
|
57
67
|
});
|
|
58
68
|
});
|
|
69
|
+
|
|
70
|
+
// The two hosts persist their rooms in different files (same format). Minting
|
|
71
|
+
// and reading must target the file of the runtime that hosts, and a read must
|
|
72
|
+
// never mint — `ay serve status` is read-only.
|
|
73
|
+
describe("share room files per runtime", () => {
|
|
74
|
+
let home: string;
|
|
75
|
+
const prev = process.env.AGENT_YES_HOME;
|
|
76
|
+
beforeEach(async () => {
|
|
77
|
+
home = await mkdtemp(path.join(tmpdir(), "ay-share-room-"));
|
|
78
|
+
process.env.AGENT_YES_HOME = home;
|
|
79
|
+
});
|
|
80
|
+
afterEach(async () => {
|
|
81
|
+
if (prev === undefined) delete process.env.AGENT_YES_HOME;
|
|
82
|
+
else process.env.AGENT_YES_HOME = prev;
|
|
83
|
+
await rm(home, { recursive: true, force: true });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("maps each runtime to its own file", () => {
|
|
87
|
+
expect(shareRoomFileName("ts")).toBe(".share-room");
|
|
88
|
+
expect(shareRoomFileName("rust")).toBe(".share-room-ayrs");
|
|
89
|
+
expect(shareRoomPath("rust")).toBe(path.join(home, ".share-room-ayrs"));
|
|
90
|
+
expect(shareRoomPath()).toBe(path.join(home, ".share-room"));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("mints into the rust file and leaves the ts file alone", async () => {
|
|
94
|
+
const url = await loadOrCreateShareRoom("s.example", "rust");
|
|
95
|
+
expect(url).toMatch(/^webrtc:\/\/r[0-9a-f]{12}:e1\.[0-9a-f]{64}@s\.example$/);
|
|
96
|
+
expect((await readFile(path.join(home, ".share-room-ayrs"), "utf-8")).trim()).toBe(url);
|
|
97
|
+
await expect(readFile(path.join(home, ".share-room"))).rejects.toThrow();
|
|
98
|
+
// Stable: a second load returns the persisted room, no re-mint.
|
|
99
|
+
expect(await loadOrCreateShareRoom("s.example", "rust")).toBe(url);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("reads without minting, and ignores a legacy unencrypted room", async () => {
|
|
103
|
+
expect(await readShareRoom("rust")).toBeNull();
|
|
104
|
+
await expect(readFile(path.join(home, ".share-room-ayrs"))).rejects.toThrow();
|
|
105
|
+
await writeFile(path.join(home, ".share-room"), `webrtc://room0:${S}@s.example`);
|
|
106
|
+
expect(await readShareRoom("ts")).toBeNull();
|
|
107
|
+
await writeFile(path.join(home, ".share-room"), `webrtc://room0:${TOK}@s.example`);
|
|
108
|
+
expect(await readShareRoom("ts")).toBe(`webrtc://room0:${TOK}@s.example`);
|
|
109
|
+
});
|
|
110
|
+
});
|
package/ts/share.ts
CHANGED
|
@@ -154,30 +154,54 @@ export interface ShareOpts {
|
|
|
154
154
|
// browser that saved the room) survives restarts — important for daemons,
|
|
155
155
|
// which would otherwise mint a new link on every restart. Delete the file to
|
|
156
156
|
// rotate the room.
|
|
157
|
-
|
|
157
|
+
//
|
|
158
|
+
// Two hosts, two files. The TypeScript server persists its room in
|
|
159
|
+
// `.share-room`; the Rust daemon (`ayrs serve --webrtc`, the default whenever
|
|
160
|
+
// the ayrs binary exists — see serveRust.ts) persists its own in
|
|
161
|
+
// `.share-room-ayrs`, so the two can run side by side without fighting over one
|
|
162
|
+
// room (they hold different host locks). Same on-disk format. Every link we
|
|
163
|
+
// PRINT must therefore come from the file of the runtime that actually hosts,
|
|
164
|
+
// or the operator hands out a link to a room nobody serves.
|
|
165
|
+
export type ShareRuntime = "ts" | "rust";
|
|
166
|
+
|
|
167
|
+
export function shareRoomFileName(runtime: ShareRuntime): string {
|
|
168
|
+
return runtime === "rust" ? ".share-room-ayrs" : ".share-room";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function shareRoomPath(runtime: ShareRuntime = "ts"): string {
|
|
158
172
|
const home = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
|
|
159
|
-
return path.join(home,
|
|
173
|
+
return path.join(home, shareRoomFileName(runtime));
|
|
160
174
|
}
|
|
161
175
|
|
|
162
|
-
|
|
176
|
+
// Read-only: the persisted room url, or null when none (or a legacy markerless
|
|
177
|
+
// one) is on disk. `ay serve status` must never mint.
|
|
178
|
+
export async function readShareRoom(runtime: ShareRuntime = "ts"): Promise<string | null> {
|
|
163
179
|
try {
|
|
164
|
-
const url = (await readFile(shareRoomPath(), "utf-8")).trim();
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
180
|
+
const url = (await readFile(shareRoomPath(runtime), "utf-8")).trim();
|
|
181
|
+
// A v2 (encrypted) room carries the e1. marker on its secret — reuse it.
|
|
182
|
+
// A legacy markerless room is rotated to a fresh encrypted room by
|
|
183
|
+
// loadOrCreateShareRoom: the signaling DO has pinned the old room to its
|
|
184
|
+
// plaintext token, so we must mint a NEW room name. This is a one-time,
|
|
185
|
+
// deliberate security upgrade — old share links stop working; re-open the
|
|
186
|
+
// new printed link.
|
|
187
|
+
if (url.startsWith("webrtc://") && parseShareUrl(url).token.startsWith(MARKER)) return url;
|
|
173
188
|
} catch {
|
|
174
189
|
/* not yet minted */
|
|
175
190
|
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function loadOrCreateShareRoom(
|
|
195
|
+
sighost = DEFAULT_SIGHOST,
|
|
196
|
+
runtime: ShareRuntime = "ts",
|
|
197
|
+
): Promise<string> {
|
|
198
|
+
const existing = await readShareRoom(runtime);
|
|
199
|
+
if (existing) return existing;
|
|
176
200
|
const room = "r" + randomBytes(6).toString("hex");
|
|
177
201
|
const s = randomBytes(32).toString("hex");
|
|
178
202
|
const url = `webrtc://${room}:${MARKER}${s}@${sighost}`;
|
|
179
|
-
await mkdir(path.dirname(shareRoomPath()), { recursive: true });
|
|
180
|
-
await writeFile(shareRoomPath(), url, { mode: 0o600 });
|
|
203
|
+
await mkdir(path.dirname(shareRoomPath(runtime)), { recursive: true });
|
|
204
|
+
await writeFile(shareRoomPath(runtime), url, { mode: 0o600 });
|
|
181
205
|
return url;
|
|
182
206
|
}
|
|
183
207
|
|
|
@@ -236,7 +260,8 @@ export function shareLinkFromRoomUrl(url: string): string {
|
|
|
236
260
|
const { s, v2 } = parseSecret(token);
|
|
237
261
|
if (!v2)
|
|
238
262
|
throw new Error(
|
|
239
|
-
"refusing to derive a link for an unencrypted room — delete
|
|
263
|
+
"refusing to derive a link for an unencrypted room — delete the persisted room file " +
|
|
264
|
+
"(~/.agent-yes/.share-room, or .share-room-ayrs for the Rust daemon) to rotate to an encrypted link",
|
|
240
265
|
);
|
|
241
266
|
return formatShareLink(room, s, host);
|
|
242
267
|
}
|
package/ts/shareCmd.ts
CHANGED
|
@@ -132,6 +132,27 @@ const HELP =
|
|
|
132
132
|
`The URL carries this host's serve token: whoever holds it can read and steer\n` +
|
|
133
133
|
`every agent here. Rotate: rm ~/.agent-yes/.serve-token && ay serve install\n`;
|
|
134
134
|
|
|
135
|
+
// Which runtime hosts the room for a daemon registered with `args` — the Rust
|
|
136
|
+
// daemon (ayrs) whenever the args delegate and the binary exists, else TS.
|
|
137
|
+
async function shareRuntimeOf(args: string[] | null): Promise<"ts" | "rust"> {
|
|
138
|
+
const { daemonShareRuntime } = await import("./serveRust.ts");
|
|
139
|
+
const { findAyrsBinary } = await import("./rustBinary.ts");
|
|
140
|
+
return daemonShareRuntime(args ?? [], () => !!findAyrsBinary());
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// The console link for the room a `--webrtc` daemon hosts, derived read-only
|
|
144
|
+
// from the persisted room file of its runtime; null when nothing is minted yet.
|
|
145
|
+
export async function daemonShareLink(args: string[] | null): Promise<string | null> {
|
|
146
|
+
const { readShareRoom, shareLinkFromRoomUrl } = await import("./share.ts");
|
|
147
|
+
const room = await readShareRoom(await shareRuntimeOf(args));
|
|
148
|
+
if (!room) return null;
|
|
149
|
+
try {
|
|
150
|
+
return shareLinkFromRoomUrl(room);
|
|
151
|
+
} catch {
|
|
152
|
+
return null; // legacy unencrypted room — the host itself rotates it on start
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
135
156
|
// ay share status — end-to-end health of each way this machine is shared.
|
|
136
157
|
async function cmdShareStatus(mount: string): Promise<number> {
|
|
137
158
|
const { inspectServeDaemon } = await import("./serve.ts");
|
|
@@ -148,7 +169,12 @@ async function cmdShareStatus(mount: string): Promise<number> {
|
|
|
148
169
|
d.port ? (d.httpUp ? `up on 127.0.0.1:${d.port}` : `DOWN (127.0.0.1:${d.port})`) : "off",
|
|
149
170
|
);
|
|
150
171
|
if (d.consoleUrl) line("portless:", d.httpUp ? d.consoleUrl.replace(/#.*/, "") : "DOWN");
|
|
151
|
-
|
|
172
|
+
if (webrtc) {
|
|
173
|
+
// Print the link the RUNNING host serves — from the room file of the
|
|
174
|
+
// runtime that hosts it (Rust by default), read-only: status never mints.
|
|
175
|
+
const link = await daemonShareLink(d.args);
|
|
176
|
+
line("webrtc:", link ? `on — ${link}` : "on (room not minted yet — run `ay share webrtc`)");
|
|
177
|
+
} else line("webrtc:", "off");
|
|
152
178
|
if (!ts) {
|
|
153
179
|
line("tailscale:", Bun.which("tailscale") ? "not running" : "not installed");
|
|
154
180
|
return d.httpUp || webrtc ? 0 : 1;
|
|
@@ -306,8 +332,13 @@ export async function cmdShare(rest: string[]): Promise<number> {
|
|
|
306
332
|
let url: string;
|
|
307
333
|
const port = state.port ?? (portArg ? Number(portArg) : DEFAULT_SHARE_PORT);
|
|
308
334
|
if (mode === "webrtc") {
|
|
335
|
+
// The daemon just (re)installed runs `ay serve --webrtc`, which is the Rust
|
|
336
|
+
// host whenever ayrs exists — read ITS room file, not the TS one, or the
|
|
337
|
+
// link points at a room nobody hosts.
|
|
309
338
|
const { loadOrCreateShareRoom, shareLinkFromRoomUrl } = await import("./share.ts");
|
|
310
|
-
url = shareLinkFromRoomUrl(
|
|
339
|
+
url = shareLinkFromRoomUrl(
|
|
340
|
+
await loadOrCreateShareRoom(undefined, await shareRuntimeOf(state.args)),
|
|
341
|
+
);
|
|
311
342
|
} else if (mode === "portless") {
|
|
312
343
|
url = state.consoleUrl ?? httpShareUrl("https://agent-yes.localhost", state.token);
|
|
313
344
|
} else if (mode === "local") {
|
package/ts/strayServe.ts
CHANGED
|
@@ -7,6 +7,7 @@ const MANAGEMENT_COMMAND_RE = /\b(?:install|uninstall|status|logs|restart)\b/;
|
|
|
7
7
|
function isServeCommand(command: string): boolean {
|
|
8
8
|
return (
|
|
9
9
|
/\bay(?:\.js)?\s+serve\b/.test(command) ||
|
|
10
|
+
/\bayrs(?:\.exe)?\s+serve\b/.test(command) ||
|
|
10
11
|
/\bagent-yes(?:\.js)?\s+serve\b/.test(command) ||
|
|
11
12
|
/\bagent-yes\.js\s+serve\b/.test(command)
|
|
12
13
|
);
|