@rynx-ai/daemon 0.1.9 → 0.1.10-beta.2
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/app-browser-host-supervisor.d.ts +97 -0
- package/dist/app-browser-host-supervisor.js +529 -0
- package/dist/browser-artifact-management.d.ts +20 -0
- package/dist/browser-artifact-management.js +61 -0
- package/dist/chrome-for-testing-store.js +1 -1
- package/dist/chrome-inspection-gateway.d.ts +58 -0
- package/dist/chrome-inspection-gateway.js +806 -0
- package/dist/chrome-inspection-manager.d.ts +94 -0
- package/dist/chrome-inspection-manager.js +573 -0
- package/dist/cli.js +13 -2
- package/dist/control-client.d.ts +9 -0
- package/dist/control-client.js +63 -0
- package/dist/daemon-build-status.d.ts +8 -0
- package/dist/daemon-build-status.js +18 -0
- package/dist/daemon-server.d.ts +19 -2
- package/dist/daemon-server.js +615 -59
- package/dist/db.js +56 -0
- package/dist/desktop-browser-host-client.d.ts +2 -1
- package/dist/desktop-browser-host-client.js +3 -1
- package/dist/direct-runtime-authenticator.js +11 -6
- package/dist/headless-browser-host.js +18 -1
- package/dist/index-daemon.js +28 -1
- package/dist/maintenance-management.d.ts +11 -0
- package/dist/maintenance-management.js +13 -0
- package/dist/plugin-installer.d.ts +35 -0
- package/dist/plugin-installer.js +195 -94
- package/dist/plugin-management-service.d.ts +58 -0
- package/dist/plugin-management-service.js +240 -0
- package/dist/plugin-package.d.ts +26 -3
- package/dist/plugin-package.js +235 -56
- package/dist/pm2.js +37 -5
- package/dist/remote-runtime-access-store.d.ts +1 -0
- package/dist/remote-runtime-access-store.js +7 -0
- package/dist/remote-runtime-admin.d.ts +3 -0
- package/dist/remote-runtime-admin.js +3 -0
- package/dist/remote-runtime-connection-manager.d.ts +12 -1
- package/dist/remote-runtime-connection-manager.js +170 -4
- package/dist/remote-runtime-target-control.d.ts +5 -1
- package/dist/remote-runtime-target-control.js +62 -7
- package/dist/remote-runtime-target-store.d.ts +4 -1
- package/dist/remote-runtime-target-store.js +21 -2
- package/dist/session-log-store.js +29 -0
- package/dist/session-meta-store.js +3 -1
- package/dist/session-pending-message-store.d.ts +2 -0
- package/dist/session-pending-message-store.js +41 -0
- package/dist/session-resource-store.d.ts +32 -0
- package/dist/session-resource-store.js +700 -0
- package/dist/setup.d.ts +16 -0
- package/dist/setup.js +136 -2
- package/package.json +14 -9
package/dist/plugin-package.js
CHANGED
|
@@ -7,13 +7,22 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
|
-
import { cpSync, existsSync,
|
|
10
|
+
import { constants as fsConstants, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
11
|
+
import { lstat as lstatAsync, open as openFile, opendir, readlink as readlinkAsync, realpath as realpathAsync, } from "node:fs/promises";
|
|
11
12
|
import { homedir } from "node:os";
|
|
12
13
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
14
15
|
import { create as createTar } from "tar";
|
|
15
16
|
import { parsePluginManifest, } from "@rynx-ai/plugin-sdk";
|
|
16
17
|
const MAX_METADATA_BYTES = 1024 * 1024;
|
|
18
|
+
const PACKAGE_HASH_STREAM_BYTES = 256 * 1024;
|
|
19
|
+
const DEFAULT_COMMAND_ABORT_GRACE_MS = 2_000;
|
|
20
|
+
export const DEFAULT_PLUGIN_PACKAGE_INSPECTION_LIMITS = Object.freeze({
|
|
21
|
+
maxFileBytes: 128 * 1024 * 1024,
|
|
22
|
+
maxTotalBytes: 512 * 1024 * 1024,
|
|
23
|
+
maxEntries: 50_000,
|
|
24
|
+
maxDepth: 64,
|
|
25
|
+
});
|
|
17
26
|
/** Classify provenance without fetching or executing anything. */
|
|
18
27
|
export function detectPluginSource(spec) {
|
|
19
28
|
if (looksLocal(spec))
|
|
@@ -85,7 +94,7 @@ async function materializeLocalDirectory(sourceDir, destination, onProgress, sig
|
|
|
85
94
|
});
|
|
86
95
|
// The rewritten file: specs are a staging-only resolution aid. Keep the
|
|
87
96
|
// installed plugin's own package metadata identical to its source.
|
|
88
|
-
|
|
97
|
+
copyBoundedMetadataFile(join(sourceReal, "package.json"), join(destination, "package.json"), "package.json");
|
|
89
98
|
return;
|
|
90
99
|
}
|
|
91
100
|
const localArchiveRoot = join(resolve(destination, ".."), "local-package");
|
|
@@ -126,8 +135,8 @@ async function cloneGitPackage(spec, destination, onProgress, signal) {
|
|
|
126
135
|
const { url, ref } = parseGitSpec(spec);
|
|
127
136
|
rmSync(destination, { recursive: true, force: true });
|
|
128
137
|
const args = ["clone", "--no-checkout", "--filter=blob:none", "--", url, destination];
|
|
129
|
-
await
|
|
130
|
-
await
|
|
138
|
+
await runPluginPackageCommand("git", args, onProgress, { signal });
|
|
139
|
+
await runPluginPackageCommand("git", ["-C", destination, "checkout", "--detach", ref ?? "HEAD"], onProgress, { signal });
|
|
131
140
|
}
|
|
132
141
|
export function parseGitSpec(spec) {
|
|
133
142
|
let value = spec.trim();
|
|
@@ -194,9 +203,11 @@ async function materializeWithNpm({ spec, prefix, destination, onProgress, signa
|
|
|
194
203
|
copyPackageTree(packageReal, destination, false);
|
|
195
204
|
}
|
|
196
205
|
/** Parse and validate static metadata, validate all executable entry paths, and hash the tree. */
|
|
197
|
-
export function inspectPluginPackage(packageDir, options = {}) {
|
|
206
|
+
export async function inspectPluginPackage(packageDir, options = {}) {
|
|
207
|
+
throwIfInspectionAborted(options.signal);
|
|
198
208
|
const packageRoot = realpathSync(packageDir);
|
|
199
|
-
|
|
209
|
+
const integrity = await hashPackageTree(packageRoot, normalizedInspectionLimits(options.limits), options.signal);
|
|
210
|
+
throwIfInspectionAborted(options.signal);
|
|
200
211
|
const packageJsonPath = requireContainedFile(packageRoot, join(packageRoot, "package.json"), "package.json", "file");
|
|
201
212
|
const packageJson = readJsonObject(packageJsonPath, "package.json");
|
|
202
213
|
if (!isRecord(packageJson.rynx) || typeof packageJson.rynx.manifest !== "string") {
|
|
@@ -215,39 +226,9 @@ export function inspectPluginPackage(packageDir, options = {}) {
|
|
|
215
226
|
return {
|
|
216
227
|
manifest,
|
|
217
228
|
...(version ? { version } : {}),
|
|
218
|
-
integrity
|
|
229
|
+
integrity,
|
|
219
230
|
};
|
|
220
231
|
}
|
|
221
|
-
/**
|
|
222
|
-
* An installed package must be an immutable closure. Internal npm `.bin`
|
|
223
|
-
* links are valid, but a link to mutable content outside the promoted package
|
|
224
|
-
* would make the recorded integrity depend only on the link text.
|
|
225
|
-
*/
|
|
226
|
-
function validatePackageSymlinkClosure(root) {
|
|
227
|
-
visit(root, "");
|
|
228
|
-
function visit(candidate, rel) {
|
|
229
|
-
const stat = lstatSync(candidate);
|
|
230
|
-
const normalized = rel.split(sep).join("/");
|
|
231
|
-
if (stat.isSymbolicLink()) {
|
|
232
|
-
let target;
|
|
233
|
-
try {
|
|
234
|
-
target = realpathSync(candidate);
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
throw new Error(`plugin package contains a broken symlink: ${normalized}`);
|
|
238
|
-
}
|
|
239
|
-
if (!isWithin(root, target)) {
|
|
240
|
-
throw new Error(`plugin package symlink resolves outside the package: ${normalized}`);
|
|
241
|
-
}
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
if (!stat.isDirectory())
|
|
245
|
-
return;
|
|
246
|
-
for (const name of readdirSync(candidate)) {
|
|
247
|
-
visit(join(candidate, name), join(rel, name));
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
232
|
/** Run package/dependency build scripts only after explicit user approval. */
|
|
252
233
|
export async function rebuildPluginPackage(packageDir, onProgress, signal) {
|
|
253
234
|
await runNpm([
|
|
@@ -474,58 +455,241 @@ function readJsonObject(path, label) {
|
|
|
474
455
|
throw new Error(`invalid ${label}: ${messageOf(error)}`);
|
|
475
456
|
}
|
|
476
457
|
}
|
|
477
|
-
function
|
|
458
|
+
function copyBoundedMetadataFile(source, destination, label) {
|
|
459
|
+
try {
|
|
460
|
+
const stat = statSync(source);
|
|
461
|
+
if (!stat.isFile())
|
|
462
|
+
throw new Error("not a file");
|
|
463
|
+
if (stat.size > MAX_METADATA_BYTES)
|
|
464
|
+
throw new Error("metadata is too large");
|
|
465
|
+
copyFileSync(source, destination);
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
throw new Error(`cannot copy ${label}: ${messageOf(error)}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async function hashPackageTree(root, limits, signal) {
|
|
478
472
|
const hash = createHash("sha256");
|
|
479
473
|
hash.update("rynx-plugin-package-v1\0");
|
|
480
|
-
|
|
474
|
+
let entryCount = 0;
|
|
475
|
+
let totalBytes = 0;
|
|
476
|
+
await visit(root, "", 0);
|
|
481
477
|
return `sha256-${hash.digest("base64")}`;
|
|
482
|
-
function visit(path, rel) {
|
|
483
|
-
|
|
478
|
+
async function visit(path, rel, depth) {
|
|
479
|
+
throwIfInspectionAborted(signal);
|
|
480
|
+
entryCount += 1;
|
|
481
|
+
if (entryCount > limits.maxEntries) {
|
|
482
|
+
throw new Error(`plugin package exceeds ${limits.maxEntries} entries`);
|
|
483
|
+
}
|
|
484
|
+
if (depth > limits.maxDepth) {
|
|
485
|
+
throw new Error(`plugin package exceeds maximum depth ${limits.maxDepth}`);
|
|
486
|
+
}
|
|
487
|
+
const stat = await lstatAsync(path);
|
|
484
488
|
const normalized = rel.split(sep).join("/");
|
|
485
489
|
if (stat.isDirectory()) {
|
|
486
490
|
hash.update(`d\0${normalized}\0`);
|
|
487
|
-
|
|
488
|
-
|
|
491
|
+
const names = [];
|
|
492
|
+
const directory = await opendir(path);
|
|
493
|
+
for await (const entry of directory) {
|
|
494
|
+
throwIfInspectionAborted(signal);
|
|
495
|
+
names.push(entry.name);
|
|
496
|
+
if (entryCount + names.length > limits.maxEntries) {
|
|
497
|
+
throw new Error(`plugin package exceeds ${limits.maxEntries} entries`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
for (const name of names.sort()) {
|
|
501
|
+
await visit(join(path, name), join(rel, name), depth + 1);
|
|
502
|
+
}
|
|
489
503
|
return;
|
|
490
504
|
}
|
|
491
505
|
if (stat.isFile()) {
|
|
506
|
+
if (stat.size > limits.maxFileBytes) {
|
|
507
|
+
throw new Error(`plugin package file exceeds ${limits.maxFileBytes} bytes: ${normalized}`);
|
|
508
|
+
}
|
|
509
|
+
if (totalBytes + stat.size > limits.maxTotalBytes) {
|
|
510
|
+
throw new Error(`plugin package exceeds ${limits.maxTotalBytes} total bytes`);
|
|
511
|
+
}
|
|
512
|
+
totalBytes += stat.size;
|
|
492
513
|
hash.update(`f\0${normalized}\0${stat.size}\0`);
|
|
493
|
-
|
|
514
|
+
const noFollow = process.platform === "win32" ? 0 : fsConstants.O_NOFOLLOW;
|
|
515
|
+
const file = await openFile(path, fsConstants.O_RDONLY | noFollow);
|
|
516
|
+
try {
|
|
517
|
+
const before = await file.stat();
|
|
518
|
+
if (!before.isFile() || before.size !== stat.size) {
|
|
519
|
+
throw new Error(`plugin package file changed during inspection: ${normalized}`);
|
|
520
|
+
}
|
|
521
|
+
let streamedBytes = 0;
|
|
522
|
+
const stream = file.createReadStream({
|
|
523
|
+
autoClose: false,
|
|
524
|
+
highWaterMark: PACKAGE_HASH_STREAM_BYTES,
|
|
525
|
+
...(signal ? { signal } : {}),
|
|
526
|
+
});
|
|
527
|
+
for await (const chunk of stream) {
|
|
528
|
+
throwIfInspectionAborted(signal);
|
|
529
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
530
|
+
streamedBytes += bytes.byteLength;
|
|
531
|
+
if (streamedBytes > limits.maxFileBytes ||
|
|
532
|
+
totalBytes - stat.size + streamedBytes > limits.maxTotalBytes) {
|
|
533
|
+
throw new Error(`plugin package file changed during inspection: ${normalized}`);
|
|
534
|
+
}
|
|
535
|
+
hash.update(bytes);
|
|
536
|
+
}
|
|
537
|
+
const after = await file.stat();
|
|
538
|
+
if (streamedBytes !== before.size ||
|
|
539
|
+
after.size !== before.size ||
|
|
540
|
+
after.mtimeMs !== before.mtimeMs) {
|
|
541
|
+
throw new Error(`plugin package file changed during inspection: ${normalized}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
finally {
|
|
545
|
+
await file.close();
|
|
546
|
+
}
|
|
494
547
|
return;
|
|
495
548
|
}
|
|
496
549
|
if (stat.isSymbolicLink()) {
|
|
497
|
-
|
|
550
|
+
let target;
|
|
551
|
+
try {
|
|
552
|
+
target = await realpathAsync(path);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
throw new Error(`plugin package contains a broken symlink: ${normalized}`);
|
|
556
|
+
}
|
|
557
|
+
if (!isWithin(root, target)) {
|
|
558
|
+
throw new Error(`plugin package symlink resolves outside the package: ${normalized}`);
|
|
559
|
+
}
|
|
560
|
+
const link = await readlinkAsync(path);
|
|
561
|
+
hash.update(`l\0${normalized}\0${link}\0`);
|
|
498
562
|
return;
|
|
499
563
|
}
|
|
500
564
|
throw new Error(`unsupported package entry: ${normalized}`);
|
|
501
565
|
}
|
|
502
566
|
}
|
|
567
|
+
function normalizedInspectionLimits(overrides) {
|
|
568
|
+
const limits = {
|
|
569
|
+
...DEFAULT_PLUGIN_PACKAGE_INSPECTION_LIMITS,
|
|
570
|
+
...overrides,
|
|
571
|
+
};
|
|
572
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
573
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
574
|
+
throw new Error(`plugin package inspection ${name} must be a positive safe integer`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return limits;
|
|
578
|
+
}
|
|
579
|
+
function throwIfInspectionAborted(signal) {
|
|
580
|
+
if (signal?.aborted) {
|
|
581
|
+
throw new Error("plugin package inspection aborted", { cause: signal.reason });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
503
584
|
function isSafeNpmPackageName(name) {
|
|
504
585
|
return /^(?:@[A-Za-z0-9][A-Za-z0-9._-]*\/)?[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name);
|
|
505
586
|
}
|
|
506
587
|
function runNpm(args, onProgress, signal) {
|
|
507
|
-
return
|
|
588
|
+
return runPluginPackageCommand("npm", args, onProgress, { signal });
|
|
508
589
|
}
|
|
509
|
-
|
|
590
|
+
/**
|
|
591
|
+
* Run one package tool without a shell and tear down its full POSIX process
|
|
592
|
+
* group on cancellation. Windows cannot signal process groups through Node,
|
|
593
|
+
* so it safely degrades to TERM→KILL on the direct child.
|
|
594
|
+
*/
|
|
595
|
+
export function runPluginPackageCommand(command, args, onProgress, options = {}) {
|
|
510
596
|
return new Promise((resolveDone, reject) => {
|
|
597
|
+
const { signal, cwd } = options;
|
|
598
|
+
const abortGraceMs = options.abortGraceMs ?? DEFAULT_COMMAND_ABORT_GRACE_MS;
|
|
599
|
+
if (!Number.isSafeInteger(abortGraceMs) || abortGraceMs <= 0 || abortGraceMs > 60_000) {
|
|
600
|
+
reject(new Error("plugin command abort grace must be 1-60000ms"));
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
511
603
|
if (signal?.aborted) {
|
|
512
604
|
reject(new Error("plugin installation aborted"));
|
|
513
605
|
return;
|
|
514
606
|
}
|
|
607
|
+
const useProcessGroup = process.platform !== "win32";
|
|
515
608
|
const child = spawn(command, args, {
|
|
516
609
|
cwd,
|
|
517
610
|
stdio: ["ignore", "pipe", "pipe"],
|
|
611
|
+
detached: useProcessGroup,
|
|
518
612
|
});
|
|
519
613
|
let settled = false;
|
|
614
|
+
let aborting = false;
|
|
615
|
+
let childClosed = false;
|
|
616
|
+
let escalationTimer;
|
|
617
|
+
let hardStopTimer;
|
|
618
|
+
let groupPollTimer;
|
|
520
619
|
const settle = (callback) => {
|
|
521
620
|
if (settled)
|
|
522
621
|
return;
|
|
523
622
|
settled = true;
|
|
524
623
|
signal?.removeEventListener("abort", abort);
|
|
624
|
+
if (escalationTimer)
|
|
625
|
+
clearTimeout(escalationTimer);
|
|
626
|
+
if (hardStopTimer)
|
|
627
|
+
clearTimeout(hardStopTimer);
|
|
628
|
+
if (groupPollTimer)
|
|
629
|
+
clearInterval(groupPollTimer);
|
|
525
630
|
callback();
|
|
526
631
|
};
|
|
527
|
-
const
|
|
632
|
+
const rejectAborted = () => settle(() => reject(new Error("plugin installation aborted", {
|
|
633
|
+
cause: signal?.reason,
|
|
634
|
+
})));
|
|
635
|
+
const processGroupAlive = () => {
|
|
636
|
+
if (!useProcessGroup || child.pid === undefined)
|
|
637
|
+
return !childClosed;
|
|
638
|
+
try {
|
|
639
|
+
process.kill(-child.pid, 0);
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
return error.code !== "ESRCH";
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
const signalProcessTree = (processSignal) => {
|
|
647
|
+
if (useProcessGroup && child.pid !== undefined) {
|
|
648
|
+
try {
|
|
649
|
+
process.kill(-child.pid, processSignal);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
if (error.code !== "ESRCH") {
|
|
654
|
+
try {
|
|
655
|
+
child.kill(processSignal);
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
// The bounded hard-stop path below still resolves cancellation.
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
try {
|
|
665
|
+
child.kill(processSignal);
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
// A concurrently exited child is already stopped.
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
const finishWhenStopped = () => {
|
|
672
|
+
if (!aborting || settled)
|
|
673
|
+
return;
|
|
674
|
+
if (childClosed && !processGroupAlive())
|
|
675
|
+
rejectAborted();
|
|
676
|
+
};
|
|
677
|
+
const abort = () => {
|
|
678
|
+
if (aborting || settled)
|
|
679
|
+
return;
|
|
680
|
+
aborting = true;
|
|
681
|
+
signalProcessTree("SIGTERM");
|
|
682
|
+
groupPollTimer = setInterval(finishWhenStopped, 25);
|
|
683
|
+
escalationTimer = setTimeout(() => {
|
|
684
|
+
signalProcessTree("SIGKILL");
|
|
685
|
+
finishWhenStopped();
|
|
686
|
+
}, abortGraceMs);
|
|
687
|
+
hardStopTimer = setTimeout(rejectAborted, abortGraceMs * 2);
|
|
688
|
+
finishWhenStopped();
|
|
689
|
+
};
|
|
528
690
|
signal?.addEventListener("abort", abort, { once: true });
|
|
691
|
+
if (signal?.aborted)
|
|
692
|
+
abort();
|
|
529
693
|
const onData = (buffer) => {
|
|
530
694
|
for (const line of buffer.toString().split(/\r?\n/)) {
|
|
531
695
|
if (line.trim())
|
|
@@ -534,15 +698,30 @@ function runCommand(command, args, onProgress, signal, cwd) {
|
|
|
534
698
|
};
|
|
535
699
|
child.stdout?.on("data", onData);
|
|
536
700
|
child.stderr?.on("data", onData);
|
|
537
|
-
child.on("error", (error) =>
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
reject(new Error("plugin installation aborted"));
|
|
541
|
-
else if (code === 0)
|
|
542
|
-
resolveDone();
|
|
701
|
+
child.on("error", (error) => {
|
|
702
|
+
if (aborting)
|
|
703
|
+
rejectAborted();
|
|
543
704
|
else
|
|
544
|
-
|
|
545
|
-
})
|
|
705
|
+
settle(() => reject(error));
|
|
706
|
+
});
|
|
707
|
+
child.on("close", (code) => {
|
|
708
|
+
childClosed = true;
|
|
709
|
+
if (aborting || signal?.aborted) {
|
|
710
|
+
if (!aborting)
|
|
711
|
+
abort();
|
|
712
|
+
if (!useProcessGroup)
|
|
713
|
+
rejectAborted();
|
|
714
|
+
else
|
|
715
|
+
finishWhenStopped();
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
settle(() => {
|
|
719
|
+
if (code === 0)
|
|
720
|
+
resolveDone();
|
|
721
|
+
else
|
|
722
|
+
reject(new Error(`${command} exited with code ${code ?? "null"}`));
|
|
723
|
+
});
|
|
724
|
+
});
|
|
546
725
|
});
|
|
547
726
|
}
|
|
548
727
|
function isRecord(value) {
|
package/dist/pm2.js
CHANGED
|
@@ -8,16 +8,18 @@
|
|
|
8
8
|
* shipped `ecosystem.cjs` to keep in sync.
|
|
9
9
|
*/
|
|
10
10
|
import { spawn, spawnSync } from "node:child_process";
|
|
11
|
-
import { mkdirSync } from "node:fs";
|
|
11
|
+
import { mkdirSync, realpathSync } from "node:fs";
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
|
-
import { dirname, join } from "node:path";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { loadConfig } from "@rynx-ai/core";
|
|
17
|
+
import { daemonEntryRestartRequired } from "./daemon-build-status.js";
|
|
17
18
|
const require = createRequire(import.meta.url);
|
|
18
19
|
/** pm2 app name — single resident daemon per machine. */
|
|
19
20
|
const APP_NAME = "rynx";
|
|
20
21
|
const PM2_COMMAND_TIMEOUT_MS = 5_000;
|
|
22
|
+
const DAEMON_PACKAGE_VERSION = require("../package.json").version;
|
|
21
23
|
/**
|
|
22
24
|
* Build a dialable control-console URL from the daemon's bind host/port. Bind-all
|
|
23
25
|
* addresses (`0.0.0.0`, `::`) aren't dialable, so show a loopback host instead.
|
|
@@ -120,13 +122,18 @@ export function startDaemon({ force, quiet = false }) {
|
|
|
120
122
|
console.error(error instanceof Error ? error.message : String(error));
|
|
121
123
|
return 1;
|
|
122
124
|
}
|
|
123
|
-
|
|
125
|
+
const staleBuild = existing?.pm2_env?.status === "online" &&
|
|
126
|
+
residentBuildRestartRequired(existing);
|
|
127
|
+
if (!force && existing?.pm2_env?.status === "online" && !staleBuild) {
|
|
124
128
|
if (!quiet) {
|
|
125
129
|
console.log(`rynx is already running (pid ${existing.pid ?? "?"}). Use \`rynx restart\` to reload.`);
|
|
126
130
|
console.log(`Control console: ${styleUrl(controlConsoleUrl())}`);
|
|
127
131
|
}
|
|
128
132
|
return 0;
|
|
129
133
|
}
|
|
134
|
+
if (staleBuild && !quiet) {
|
|
135
|
+
console.log("A newer daemon build is present on disk; restarting Rynx.");
|
|
136
|
+
}
|
|
130
137
|
const dir = logsDir();
|
|
131
138
|
if (existing) {
|
|
132
139
|
const removed = runPm2(["delete", APP_NAME], quiet);
|
|
@@ -182,8 +189,10 @@ export function statusDaemon() {
|
|
|
182
189
|
const state = app.pm2_env?.status ?? "unknown";
|
|
183
190
|
const pid = app.pid ? ` pid=${app.pid}` : "";
|
|
184
191
|
const restarts = app.pm2_env?.restart_time != null ? ` restarts=${app.pm2_env.restart_time}` : "";
|
|
185
|
-
|
|
186
|
-
|
|
192
|
+
const staleBuild = state === "online" && residentBuildRestartRequired(app);
|
|
193
|
+
console.log(`rynx: ${state}${pid}${restarts}` +
|
|
194
|
+
(staleBuild ? " restart-required (run `rynx restart`)" : ""));
|
|
195
|
+
return state === "online" && !staleBuild ? 0 : 1;
|
|
187
196
|
}
|
|
188
197
|
/** Stream the daemon's pm2 logs until the user detaches (Ctrl-C). */
|
|
189
198
|
export function streamLogs() {
|
|
@@ -193,3 +202,26 @@ export function streamLogs() {
|
|
|
193
202
|
});
|
|
194
203
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
195
204
|
}
|
|
205
|
+
function residentBuildRestartRequired(app) {
|
|
206
|
+
const runningEntry = app.pm2_env?.pm_exec_path;
|
|
207
|
+
if (runningEntry && !sameEntryPath(runningEntry, daemonEntryPath()))
|
|
208
|
+
return true;
|
|
209
|
+
const runningVersion = app.pm2_env?.version;
|
|
210
|
+
if (runningVersion &&
|
|
211
|
+
typeof DAEMON_PACKAGE_VERSION === "string" &&
|
|
212
|
+
runningVersion !== DAEMON_PACKAGE_VERSION) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
const startedAt = app.pm2_env?.pm_uptime;
|
|
216
|
+
return typeof startedAt === "number" &&
|
|
217
|
+
Number.isFinite(startedAt) &&
|
|
218
|
+
daemonEntryRestartRequired(daemonEntryPath(), startedAt);
|
|
219
|
+
}
|
|
220
|
+
function sameEntryPath(left, right) {
|
|
221
|
+
try {
|
|
222
|
+
return realpathSync(left) === realpathSync(right);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return resolve(left) === resolve(right);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -54,6 +54,7 @@ export declare class RemoteRuntimeAccessStore {
|
|
|
54
54
|
/** Recheck an already authenticated peer before each dispatched operation. */
|
|
55
55
|
isClientGrantActive(grantIdInput: string, clientIdInput: string): boolean;
|
|
56
56
|
revokeClientGrant(grantIdInput: string): boolean;
|
|
57
|
+
deleteRevokedClientGrant(grantIdInput: string): boolean;
|
|
57
58
|
private replayConsumedClaim;
|
|
58
59
|
private deleteRetainedExpiredClaims;
|
|
59
60
|
private now;
|
|
@@ -197,6 +197,13 @@ export class RemoteRuntimeAccessStore {
|
|
|
197
197
|
});
|
|
198
198
|
return revoke.immediate();
|
|
199
199
|
}
|
|
200
|
+
deleteRevokedClientGrant(grantIdInput) {
|
|
201
|
+
const grantId = validId(grantIdInput, "grantId", "grant_not_found");
|
|
202
|
+
return this.connection
|
|
203
|
+
.prepare(`DELETE FROM runtime_client_grants
|
|
204
|
+
WHERE grant_id = ? AND revoked_at IS NOT NULL`)
|
|
205
|
+
.run(grantId).changes === 1;
|
|
206
|
+
}
|
|
200
207
|
replayConsumedClaim(claim, clientId, clientPublicKey) {
|
|
201
208
|
if (claim.consumed_client_id === null ||
|
|
202
209
|
claim.consumed_client_public_key === null ||
|
|
@@ -47,6 +47,9 @@ export class DaemonRemoteRuntimeAdmin {
|
|
|
47
47
|
const closedConnections = revoked ? this.connections.revokeGrant(grantId) : 0;
|
|
48
48
|
return { revoked, closedConnections };
|
|
49
49
|
}
|
|
50
|
+
deleteRevokedClientGrant(grantId) {
|
|
51
|
+
return { deleted: this.access.deleteRevokedClientGrant(grantId) };
|
|
52
|
+
}
|
|
50
53
|
}
|
|
51
54
|
function advertisedEndpoint(address, listenerPort) {
|
|
52
55
|
const input = address?.trim() || "127.0.0.1";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type RuntimeBrowserInspectTarget } from "@rynx-ai/protocol/runtime-browser-inspect";
|
|
1
2
|
import { type RuntimeBrowserSurfaceOpenFrame } from "@rynx-ai/protocol/runtime-browser-surface";
|
|
2
|
-
import { type DirectRuntimeConnection, type DirectRuntimeBrowserSurfaceConnection, type DirectRuntimeCredential } from "@rynx-ai/remote-runtime-client";
|
|
3
|
+
import { type DirectRuntimeConnection, type DirectRuntimeBrowserInspectConnection, type DirectRuntimeBrowserSurfaceConnection, type DirectRuntimeCredential } from "@rynx-ai/remote-runtime-client";
|
|
3
4
|
import type { DirectRuntimeBrowserSurfaceHost, DirectRuntimeEmulatorSurfaceHost, RemoteRuntimeDispatcher, RuntimeConnectionLease, RuntimeConnectionResolverHost } from "@rynx-ai/server";
|
|
4
5
|
import { type FileRemoteRuntimeCredentialStore, type RemoteRuntimeClientCredential } from "./remote-runtime-credential-store.js";
|
|
5
6
|
import { type RemoteRuntimeTarget, type RemoteRuntimeTargetStore } from "./remote-runtime-target-store.js";
|
|
@@ -19,6 +20,10 @@ interface RuntimeCredentialReader {
|
|
|
19
20
|
}
|
|
20
21
|
interface RuntimeClientAdapter {
|
|
21
22
|
connect(credential: DirectRuntimeCredential): Promise<DirectRuntimeConnection>;
|
|
23
|
+
openBrowserInspect?(credential: DirectRuntimeCredential, target: RuntimeBrowserInspectTarget, options?: {
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
}): Promise<DirectRuntimeBrowserInspectConnection>;
|
|
22
27
|
openBrowserSurface?(credential: DirectRuntimeCredential, frame: RuntimeBrowserSurfaceOpenFrame, options?: {
|
|
23
28
|
timeoutMs?: number;
|
|
24
29
|
signal?: AbortSignal;
|
|
@@ -49,6 +54,8 @@ export declare class DaemonRuntimeConnectionManager implements RuntimeConnection
|
|
|
49
54
|
private readonly maxIdleConnections;
|
|
50
55
|
private readonly slots;
|
|
51
56
|
private readonly generations;
|
|
57
|
+
/** Independent inspect sockets must outlive shared control-channel eviction. */
|
|
58
|
+
private readonly browserInspectClosers;
|
|
52
59
|
private localStatusSnapshot?;
|
|
53
60
|
private closed;
|
|
54
61
|
private managerClosePromise?;
|
|
@@ -71,6 +78,7 @@ export declare class DaemonRuntimeConnectionManager implements RuntimeConnection
|
|
|
71
78
|
private wrapDirectSessionSubscription;
|
|
72
79
|
private openDedicatedDirectTerminal;
|
|
73
80
|
private openDedicatedDirectBrowserSurface;
|
|
81
|
+
private openDedicatedDirectBrowserInspect;
|
|
74
82
|
private openDedicatedDirectEmulatorSurface;
|
|
75
83
|
private slotFor;
|
|
76
84
|
private openDirectConnection;
|
|
@@ -78,6 +86,9 @@ export declare class DaemonRuntimeConnectionManager implements RuntimeConnection
|
|
|
78
86
|
private closeSlot;
|
|
79
87
|
private trackDedicatedTerminal;
|
|
80
88
|
private trackDedicatedBrowserSurface;
|
|
89
|
+
private trackDedicatedBrowserInspect;
|
|
90
|
+
private registerBrowserInspectCloser;
|
|
91
|
+
private closeBrowserInspects;
|
|
81
92
|
private trackDedicatedEmulatorSurface;
|
|
82
93
|
private registerDedicatedCloser;
|
|
83
94
|
private scheduleIdleClose;
|