@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.
Files changed (50) hide show
  1. package/dist/app-browser-host-supervisor.d.ts +97 -0
  2. package/dist/app-browser-host-supervisor.js +529 -0
  3. package/dist/browser-artifact-management.d.ts +20 -0
  4. package/dist/browser-artifact-management.js +61 -0
  5. package/dist/chrome-for-testing-store.js +1 -1
  6. package/dist/chrome-inspection-gateway.d.ts +58 -0
  7. package/dist/chrome-inspection-gateway.js +806 -0
  8. package/dist/chrome-inspection-manager.d.ts +94 -0
  9. package/dist/chrome-inspection-manager.js +573 -0
  10. package/dist/cli.js +13 -2
  11. package/dist/control-client.d.ts +9 -0
  12. package/dist/control-client.js +63 -0
  13. package/dist/daemon-build-status.d.ts +8 -0
  14. package/dist/daemon-build-status.js +18 -0
  15. package/dist/daemon-server.d.ts +19 -2
  16. package/dist/daemon-server.js +615 -59
  17. package/dist/db.js +56 -0
  18. package/dist/desktop-browser-host-client.d.ts +2 -1
  19. package/dist/desktop-browser-host-client.js +3 -1
  20. package/dist/direct-runtime-authenticator.js +11 -6
  21. package/dist/headless-browser-host.js +18 -1
  22. package/dist/index-daemon.js +28 -1
  23. package/dist/maintenance-management.d.ts +11 -0
  24. package/dist/maintenance-management.js +13 -0
  25. package/dist/plugin-installer.d.ts +35 -0
  26. package/dist/plugin-installer.js +195 -94
  27. package/dist/plugin-management-service.d.ts +58 -0
  28. package/dist/plugin-management-service.js +240 -0
  29. package/dist/plugin-package.d.ts +26 -3
  30. package/dist/plugin-package.js +235 -56
  31. package/dist/pm2.js +37 -5
  32. package/dist/remote-runtime-access-store.d.ts +1 -0
  33. package/dist/remote-runtime-access-store.js +7 -0
  34. package/dist/remote-runtime-admin.d.ts +3 -0
  35. package/dist/remote-runtime-admin.js +3 -0
  36. package/dist/remote-runtime-connection-manager.d.ts +12 -1
  37. package/dist/remote-runtime-connection-manager.js +170 -4
  38. package/dist/remote-runtime-target-control.d.ts +5 -1
  39. package/dist/remote-runtime-target-control.js +62 -7
  40. package/dist/remote-runtime-target-store.d.ts +4 -1
  41. package/dist/remote-runtime-target-store.js +21 -2
  42. package/dist/session-log-store.js +29 -0
  43. package/dist/session-meta-store.js +3 -1
  44. package/dist/session-pending-message-store.d.ts +2 -0
  45. package/dist/session-pending-message-store.js +41 -0
  46. package/dist/session-resource-store.d.ts +32 -0
  47. package/dist/session-resource-store.js +700 -0
  48. package/dist/setup.d.ts +16 -0
  49. package/dist/setup.js +136 -2
  50. package/package.json +14 -9
@@ -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, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, writeFileSync, } from "node:fs";
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
- writeFileSync(join(destination, "package.json"), readFileSync(join(sourceReal, "package.json")));
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 runCommand("git", args, onProgress, signal);
130
- await runCommand("git", ["-C", destination, "checkout", "--detach", ref ?? "HEAD"], onProgress, signal);
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
- validatePackageSymlinkClosure(packageRoot);
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: hashPackageTree(packageRoot),
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 hashPackageTree(root) {
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
- visit(root, "");
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
- const stat = lstatSync(path);
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
- for (const name of readdirSync(path).sort())
488
- visit(join(path, name), join(rel, name));
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
- hash.update(readFileSync(path));
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
- hash.update(`l\0${normalized}\0${readlinkSync(path)}\0`);
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 runCommand("npm", args, onProgress, signal);
588
+ return runPluginPackageCommand("npm", args, onProgress, { signal });
508
589
  }
509
- function runCommand(command, args, onProgress, signal, cwd) {
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 abort = () => child.kill();
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) => settle(() => reject(error)));
538
- child.on("close", (code) => settle(() => {
539
- if (signal?.aborted)
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
- reject(new Error(`${command} exited with code ${code ?? "null"}`));
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
- if (!force && existing?.pm2_env?.status === "online") {
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
- console.log(`rynx: ${state}${pid}${restarts}`);
186
- return state === "online" ? 0 : 1;
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 ||
@@ -29,4 +29,7 @@ export declare class DaemonRemoteRuntimeAdmin implements RemoteRuntimeAdminHost
29
29
  revoked: boolean;
30
30
  closedConnections: number;
31
31
  };
32
+ deleteRevokedClientGrant(grantId: string): {
33
+ deleted: boolean;
34
+ };
32
35
  }
@@ -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;