@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,15 +7,24 @@
7
7
  */
8
8
  import { randomBytes, randomUUID } from "node:crypto";
9
9
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
10
+ import { createRequire } from "node:module";
11
+ import { fileURLToPath } from "node:url";
10
12
  import { dirname, join } from "node:path";
13
+ import WebSocket from "ws";
11
14
  import { loadConfig, rynxHome } from "@rynx-ai/core";
15
+ import { LOCAL_MANAGEMENT_PROTOCOL, } from "@rynx-ai/protocol/control";
12
16
  import { REMOTE_RUNTIME_CORE_PROTOCOL } from "@rynx-ai/protocol/remote-runtime";
17
+ import { RUNTIME_BROWSER_INSPECT_MAX_MESSAGE_BYTES, RUNTIME_BROWSER_INSPECT_SEMANTIC_CAPABILITY, } from "@rynx-ai/protocol/runtime-browser-inspect";
13
18
  import { RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY } from "@rynx-ai/protocol/runtime-browser-surface";
14
19
  import { RUNTIME_EMULATOR_SURFACE_SEMANTIC_CAPABILITY } from "@rynx-ai/protocol/runtime-emulator-surface";
15
- import { createDaemonRuntimeHost, createDesktopFirstSessionBrowserHost, createRunnerSessionTerminalHost, createSessionRuntimeServices, DesktopBrowserHostRegistry, DirectRuntimeConnectionHub, MachineSessionService, RemoteRuntimeDispatcher, SessionBrowserService, SessionBrowserSurfaceCoordinator, SessionEmulatorService, startServer, startDirectRuntimeServer, } from "@rynx-ai/server";
20
+ import { createDaemonRuntimeHost, createDesktopFirstSessionBrowserHost, createRunnerSessionTerminalHost, createSessionRuntimeServices, DesktopBrowserHostAbsentError, DesktopBrowserHostRegistry, DirectRuntimeConnectionHub, DirectRuntimeBrowserInspectHostError, MachineSessionService, RemoteRuntimeDispatcher, SessionBrowserService, SessionBrowserServiceError, SessionBrowserSurfaceCoordinator, SessionEmulatorService, startServer, startDirectRuntimeServer, } from "@rynx-ai/server";
21
+ import { APP_BROWSER_HOST_BOOTSTRAP_TYPE, AppBrowserHostSupervisor, parseAppBrowserHostLaunchConfig, } from "./app-browser-host-supervisor.js";
16
22
  import { buildControlDeps } from "./control-deps.js";
23
+ import { createBrowserArtifactManagementService } from "./browser-artifact-management.js";
24
+ import { createDaemonBuildStatus } from "./daemon-build-status.js";
17
25
  import { BrowserRunnerSessionContextProvider } from "./browser-runner-session-context.js";
18
26
  import { isChromeForTestingHostSupported } from "./chrome-for-testing-store.js";
27
+ import { ChromeInspectionManager, } from "./chrome-inspection-manager.js";
19
28
  import { createHeadlessBrowserHost } from "./headless-browser-host.js";
20
29
  import { acquireDaemonOwner } from "./daemon-owner.js";
21
30
  import { DaemonDirectRuntimeAuthenticator } from "./direct-runtime-authenticator.js";
@@ -23,15 +32,19 @@ import { DurableEmbeddedBrowserProfileHost } from "./embedded-browser-profile-cl
23
32
  import { loadDirectRuntimeConfig, } from "./direct-runtime-config.js";
24
33
  import { isDaemonDirectRuntimeHostingSupported, loadOrCreateDaemonDirectRuntimeEncryptionIdentity, loadOrCreateDaemonInstallationIdentity, } from "./installation-id.js";
25
34
  import { cleanupLegacySessions } from "./migrations/cleanup-legacy-sessions.js";
35
+ import { createMaintenanceManagementService } from "./maintenance-management.js";
36
+ import { PluginManagementService } from "./plugin-management-service.js";
26
37
  import { PluginSupervisor } from "./plugin-supervisor.js";
27
38
  import { pruneInactivePluginInstallations } from "./plugin-installer.js";
28
39
  import { runPluginCliCommand } from "./plugin-cli.js";
29
40
  import { getPlugin, isRunnablePluginRecord, listEnabledPlugins } from "./plugin-store.js";
30
41
  import { loadPlugins, resolveChannelInstances } from "./registry.js";
31
42
  import { SqliteSessionLogStore } from "./session-log-store.js";
43
+ import { SqliteSessionResourceStore } from "./session-resource-store.js";
32
44
  import { SessionBrowserCapabilityStore } from "./session-browser-capability-store.js";
33
45
  import { SqliteSessionEmulatorBindingStore } from "./session-emulator-binding-store.js";
34
46
  import { sessionRegistry } from "./session-meta-store.js";
47
+ import { sessionPendingMessageStore } from "./session-pending-message-store.js";
35
48
  import { RemoteRuntimeAccessStore } from "./remote-runtime-access-store.js";
36
49
  import { DaemonRemoteRuntimeAdmin } from "./remote-runtime-admin.js";
37
50
  import { FileRemoteRuntimeCredentialStore } from "./remote-runtime-credential-store.js";
@@ -40,7 +53,15 @@ import { DaemonRuntimeTargetControl } from "./remote-runtime-target-control.js";
40
53
  import { RemoteRuntimeTargetStore } from "./remote-runtime-target-store.js";
41
54
  import { ProviderCliService } from "./provider-cli-service.js";
42
55
  const PLUGIN_CLI_OUTPUT_MAX_BYTES = 256 * 1024;
56
+ const BROWSER_INSPECT_OPEN_TIMEOUT_MS = 10_000;
57
+ const BROWSER_INSPECT_CLOSE_TIMEOUT_MS = 1_000;
58
+ const BROWSER_INSPECT_MAX_QUEUED_BYTES = 32 * 1024 * 1024;
43
59
  const DIRECT_RUNTIME_HOSTING_UNSUPPORTED = "Direct Runtime hosting is supported only on macOS and Linux until protected server-key storage is available on other operating systems";
60
+ const require = createRequire(import.meta.url);
61
+ const DAEMON_PACKAGE_VERSION = readDaemonPackageVersion();
62
+ export function daemonActivityBlocksShutdown(activity) {
63
+ return activity.runningTurns > 0 || activity.pendingInteractions > 0;
64
+ }
44
65
  function defaultWarn(message) {
45
66
  console.warn(JSON.stringify({ level: "warn", type: "channel-registry", msg: message }));
46
67
  }
@@ -53,6 +74,7 @@ function unsupportedRemoteRuntimeAdmin(reason) {
53
74
  createPairingOffer: unsupported,
54
75
  listClientGrants: unsupported,
55
76
  revokeClientGrant: unsupported,
77
+ deleteRevokedClientGrant: unsupported,
56
78
  };
57
79
  }
58
80
  function logLegacyCleanup(config) {
@@ -81,6 +103,14 @@ function logLegacyCleanup(config) {
81
103
  function dialableHost(host) {
82
104
  return host === "0.0.0.0" || host === "::" || host.trim() === "" ? "127.0.0.1" : host;
83
105
  }
106
+ function daemonControlOrigin(config) {
107
+ const host = dialableHost(config.HOST);
108
+ const unwrappedHost = host.replace(/^\[|\]$/g, "");
109
+ const authorityHost = unwrappedHost.includes(":")
110
+ ? `[${unwrappedHost}]`
111
+ : unwrappedHost;
112
+ return `http://${authorityHost}:${config.PORT}`;
113
+ }
84
114
  function controlEndpointFile() {
85
115
  return join(rynxHome(), "control.json");
86
116
  }
@@ -90,11 +120,13 @@ async function writeControlEndpoint(config, managementToken) {
90
120
  await mkdir(dirname(file), { recursive: true });
91
121
  try {
92
122
  await writeFile(temporary, `${JSON.stringify({
93
- origin: `http://${dialableHost(config.HOST)}:${config.PORT}`,
123
+ origin: daemonControlOrigin(config),
94
124
  host: config.HOST,
95
125
  port: config.PORT,
96
126
  pid: process.pid,
97
127
  managementToken,
128
+ managementProtocol: LOCAL_MANAGEMENT_PROTOCOL,
129
+ ...controlEndpointDistributionMetadata(),
98
130
  updatedAt: new Date().toISOString(),
99
131
  }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
100
132
  await rename(temporary, file);
@@ -103,6 +135,38 @@ async function writeControlEndpoint(config, managementToken) {
103
135
  await rm(temporary, { force: true });
104
136
  }
105
137
  }
138
+ function controlEndpointDistributionMetadata() {
139
+ const distribution = process.env.RYNX_DISTRIBUTION === "app"
140
+ ? "app"
141
+ : "standalone";
142
+ const productVersion = boundedEnvironmentMetadata(process.env.RYNX_PRODUCT_VERSION, 128) ?? DAEMON_PACKAGE_VERSION;
143
+ const buildId = boundedEnvironmentMetadata(process.env.RYNX_BUILD_ID, 256) ?? `${distribution}:${productVersion}`;
144
+ const requestedLifecycle = process.env.RYNX_DAEMON_LIFECYCLE;
145
+ const daemonLifecycle = distribution === "standalone"
146
+ ? "standalone"
147
+ : requestedLifecycle === "resident"
148
+ ? "resident"
149
+ : "follow-app";
150
+ return { distribution, productVersion, buildId, daemonLifecycle };
151
+ }
152
+ function boundedEnvironmentMetadata(value, maxLength) {
153
+ if (!value ||
154
+ value.length > maxLength ||
155
+ value.trim() !== value ||
156
+ /[\u0000-\u001f\u007f]/.test(value)) {
157
+ return undefined;
158
+ }
159
+ return value;
160
+ }
161
+ function readDaemonPackageVersion() {
162
+ try {
163
+ const version = require("../package.json").version;
164
+ return boundedEnvironmentMetadata(typeof version === "string" ? version : undefined, 128) ?? "development";
165
+ }
166
+ catch {
167
+ return "development";
168
+ }
169
+ }
106
170
  async function removeControlEndpointForCurrentProcess() {
107
171
  try {
108
172
  const file = controlEndpointFile();
@@ -115,13 +179,14 @@ async function removeControlEndpointForCurrentProcess() {
115
179
  // Best effort only; stale endpoint files are ignored by the client fallback.
116
180
  }
117
181
  }
118
- export async function startRynxDaemonServer({ config, warn = defaultWarn, writeEndpoint = writeControlEndpoint, acquireOwner = acquireDaemonOwner, loadDirectConfig = loadDirectRuntimeConfig, startDirectServer = startDirectRuntimeServer, } = {}) {
182
+ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeEndpoint = writeControlEndpoint, acquireOwner = acquireDaemonOwner, loadDirectConfig = loadDirectRuntimeConfig, startDirectServer = startDirectRuntimeServer, createChromeInspectionManager = (options) => ChromeInspectionManager.create(options), createBrowserHostSupervisor = (options) => new AppBrowserHostSupervisor(options), } = {}) {
119
183
  const owner = await acquireOwner();
120
184
  let ownerManagedByServer = false;
121
185
  try {
122
186
  const appConfig = config ?? loadConfig();
187
+ const appBrowserHostLaunch = parseAppBrowserHostLaunchConfig();
123
188
  const directHostingSupported = isDaemonDirectRuntimeHostingSupported();
124
- const runtimeBrowserSupported = isChromeForTestingHostSupported();
189
+ const runtimeBrowserSupported = appBrowserHostLaunch !== undefined || isChromeForTestingHostSupported();
125
190
  const directConfig = directHostingSupported
126
191
  ? await loadDirectConfig()
127
192
  : undefined;
@@ -134,6 +199,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
134
199
  warn(DIRECT_RUNTIME_HOSTING_UNSUPPORTED);
135
200
  const daemonInstanceId = randomUUID();
136
201
  const daemonStartedAt = new Date();
202
+ const daemonBuildStatus = createDaemonBuildStatus(fileURLToPath(import.meta.url), daemonStartedAt.getTime());
137
203
  const remoteRuntime = createDaemonRuntimeHost({
138
204
  daemonId: installationIdentity.installationId,
139
205
  daemonInstanceId,
@@ -150,12 +216,17 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
150
216
  "semantic.session.launch-options.v2",
151
217
  "semantic.session.create.v1",
152
218
  "semantic.session.message.v1",
219
+ "semantic.session.resources.v1",
220
+ "semantic.session.message-queue.v1",
153
221
  "semantic.session.delete.v1",
154
222
  "semantic.session.interaction.v1",
155
223
  "semantic.provider-cli.status.v1",
156
224
  ...(runtimeBrowserSupported ? ["semantic.browser.v1"] : []),
157
225
  ...(runtimeBrowserSupported && directHostingSupported
158
- ? [RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY]
226
+ ? [
227
+ RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY,
228
+ RUNTIME_BROWSER_INSPECT_SEMANTIC_CAPABILITY,
229
+ ]
159
230
  : []),
160
231
  ...(control.emulator
161
232
  ? ["semantic.emulator.unary.v1", "semantic.session.emulator.v1"]
@@ -167,6 +238,19 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
167
238
  startedAt: daemonStartedAt,
168
239
  });
169
240
  const managementToken = randomBytes(32).toString("base64url");
241
+ const desktopBrowserHosts = new DesktopBrowserHostRegistry();
242
+ const appBrowserHostSupervisor = appBrowserHostLaunch
243
+ ? createBrowserHostSupervisor({
244
+ launch: appBrowserHostLaunch,
245
+ bootstrap: {
246
+ type: APP_BROWSER_HOST_BOOTSTRAP_TYPE,
247
+ schemaVersion: 1,
248
+ origin: daemonControlOrigin(appConfig),
249
+ managementToken,
250
+ },
251
+ isDesktopHostConnected: () => desktopBrowserHosts.directConnected,
252
+ })
253
+ : undefined;
170
254
  try {
171
255
  for (const removed of pruneInactivePluginInstallations()) {
172
256
  console.log(JSON.stringify({ level: "info", type: "plugin-gc", removed }));
@@ -186,6 +270,11 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
186
270
  output(JSON.stringify(entry));
187
271
  },
188
272
  });
273
+ const pluginManagement = new PluginManagementService();
274
+ const browserArtifactManagement = createBrowserArtifactManagementService();
275
+ const maintenanceManagement = createMaintenanceManagementService({
276
+ codexStorePath: appConfig.AGENT_SESSION_STORE_PATH,
277
+ });
189
278
  await loadPlugins(pluginSupervisor, warn);
190
279
  logLegacyCleanup(appConfig);
191
280
  const reloadPluginRuntime = async (pluginId) => {
@@ -253,21 +342,12 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
253
342
  statusSnapshot: pluginSupervisor.statusSnapshot.bind(pluginSupervisor),
254
343
  dispose: pluginSupervisor.dispose.bind(pluginSupervisor),
255
344
  };
256
- const daemonManagement = {
257
- managementToken,
258
- identity: () => ({
259
- installationId: installationIdentity.installationId,
260
- algorithm: installationIdentity.algorithm,
261
- publicKey: installationIdentity.publicKey,
262
- }),
263
- invokePluginCli,
264
- };
265
345
  const remoteRuntimeAccess = new RemoteRuntimeAccessStore();
266
346
  const directConnections = new DirectRuntimeConnectionHub();
267
347
  const sessionLog = new SqliteSessionLogStore();
348
+ const sessionResources = new SqliteSessionResourceStore();
268
349
  const providerClis = new ProviderCliService();
269
350
  const sessionBrowserCapabilities = new SessionBrowserCapabilityStore();
270
- const desktopBrowserHosts = new DesktopBrowserHostRegistry();
271
351
  const durableDesktopBrowserHost = new DurableEmbeddedBrowserProfileHost(desktopBrowserHosts, { warn });
272
352
  const browserRunnerSessionContexts = new BrowserRunnerSessionContextProvider({
273
353
  rootDir: join(rynxHome(), "run", "browser-session-contexts"),
@@ -280,7 +360,9 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
280
360
  onMirrorError: (error, sessionId) => warn(`Session mirror failed for ${sessionId}: ${error instanceof Error ? error.message : String(error)}`),
281
361
  });
282
362
  const sessionBrowsers = new SessionBrowserService({
283
- host: createDesktopFirstSessionBrowserHost(durableDesktopBrowserHost, createHeadlessBrowserHost()),
363
+ host: appBrowserHostSupervisor
364
+ ? createSupervisedAppSessionBrowserHost(durableDesktopBrowserHost, appBrowserHostSupervisor)
365
+ : createDesktopFirstSessionBrowserHost(durableDesktopBrowserHost, createHeadlessBrowserHost()),
284
366
  sessions: {
285
367
  exists: async (sessionId) => Boolean(sessionRegistry.get(sessionId) ??
286
368
  await sessionRuntimeServices.sessionStore.get(sessionId)),
@@ -356,6 +438,8 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
356
438
  agents: {
357
439
  list: () => control.listAgents(),
358
440
  },
441
+ pendingMessages: sessionPendingMessageStore,
442
+ resources: sessionResources,
359
443
  execution: {
360
444
  runtime: sessionRuntimeServices.conversationRuntime,
361
445
  runner: sessionRuntimeServices.runnerManager,
@@ -377,6 +461,53 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
377
461
  return { cwd };
378
462
  },
379
463
  });
464
+ let shutdownAccepted = false;
465
+ let requestServerShutdown;
466
+ const daemonManagement = {
467
+ managementToken,
468
+ identity: () => ({
469
+ installationId: installationIdentity.installationId,
470
+ algorithm: installationIdentity.algorithm,
471
+ publicKey: installationIdentity.publicKey,
472
+ }),
473
+ buildStatus: daemonBuildStatus,
474
+ pluginManagement: {
475
+ listPlugins: () => pluginManagement.list(),
476
+ setPluginEnabled: (pluginId, enabled) => pluginManagement.setEnabled(pluginId, enabled),
477
+ uninstallPlugin: (pluginId) => pluginManagement.uninstall(pluginId),
478
+ prepareInstallation: (input, context) => pluginManagement.prepareInstallation(input, context),
479
+ commitInstallation: (token, input, context) => pluginManagement.commitInstallation(token, input, context),
480
+ cancelPreparation: (token) => pluginManagement.cancelPreparation(token),
481
+ dispose: () => pluginManagement.dispose(),
482
+ },
483
+ browserArtifacts: browserArtifactManagement,
484
+ maintenance: maintenanceManagement,
485
+ shutdownIfIdle: () => {
486
+ if (shutdownAccepted)
487
+ return { outcome: "accepted" };
488
+ const activity = {
489
+ ...sessionRuntimeServices.sessionRuntimeIndex.activitySnapshot(),
490
+ activeTerminals: sessionTerminals.activeTerminalCount(),
491
+ activeBrowsers: sessionBrowsers.activeBrowserCount(),
492
+ };
493
+ if (daemonActivityBlocksShutdown(activity)) {
494
+ return { outcome: "busy", activity };
495
+ }
496
+ shutdownAccepted = true;
497
+ const shutdown = requestServerShutdown;
498
+ if (!shutdown) {
499
+ shutdownAccepted = false;
500
+ throw new Error("idle daemon shutdown was requested before server lifecycle was ready");
501
+ }
502
+ // Start admission fences in this event-loop turn, before another
503
+ // request or Direct dispatch can be accepted.
504
+ void shutdown().catch((error) => {
505
+ warn(`idle daemon shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
506
+ });
507
+ return { outcome: "accepted" };
508
+ },
509
+ invokePluginCli,
510
+ };
380
511
  const remoteRuntimeDispatcher = new RemoteRuntimeDispatcher({
381
512
  runtime: remoteRuntime,
382
513
  emulator: control.emulator,
@@ -401,39 +532,57 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
401
532
  credentials: runtimeCredentials,
402
533
  });
403
534
  const runtimeTargetControl = new DaemonRuntimeTargetControl(remoteRuntime, runtimeTargets, runtimeCredentials, runtimeConnectionManager);
404
- const server = await startServer({
405
- config: appConfig,
406
- loadInstances: () => resolveChannelInstances(warn),
407
- control,
408
- sessionLog,
409
- sessionRegistry,
410
- sessionRuntimeServices,
411
- pluginRuntime,
412
- daemonManagement,
413
- remoteRuntime,
414
- remoteRuntimeAdmin,
415
- runtimeTargetControl,
416
- runtimeConnectionResolver: runtimeConnectionManager,
417
- providerClis,
418
- sessionEmulators,
419
- sessionLifecycle: sessionResourceLifecycle,
420
- runtimeLocalBrowser: {
421
- authorizeCredential: (credential) => sessionBrowserCapabilities.verify(credential.sessionId, credential.capability),
422
- endpointForCredential: async (credential) => {
423
- if (!sessionBrowserCapabilities.verify(credential.sessionId, credential.capability))
424
- return undefined;
425
- return sessionBrowsers.getRuntimeLocalEndpoint(credential.sessionId);
426
- },
427
- endpointForSession: (sessionId) => sessionBrowsers.getRuntimeLocalEndpoint(sessionId),
428
- },
429
- desktopBrowserHost: {
430
- registry: desktopBrowserHosts,
431
- daemonInstanceId,
432
- },
433
- });
434
- const baseShutdown = server.shutdown.bind(server);
535
+ const directBrowserInspectHost = createDirectRuntimeBrowserInspectHost(sessionBrowsers);
536
+ let chromeInspectionManager;
537
+ let server;
538
+ let baseShutdown;
435
539
  let directServer;
436
540
  try {
541
+ const initializedChromeInspectionManager = await createChromeInspectionManager({
542
+ localDaemonInstanceId: daemonInstanceId,
543
+ browsers: sessionBrowsers,
544
+ runtimeTargets,
545
+ runtimeConnections: runtimeConnectionManager,
546
+ onError: (error, context) => warn(`Chrome inspection ${context} failed: ${error.message}`),
547
+ });
548
+ chromeInspectionManager = initializedChromeInspectionManager;
549
+ daemonManagement.chromeInspection = {
550
+ status: async () => asDaemonChromeInspectionStatus(await initializedChromeInspectionManager.status()),
551
+ configure: async (input) => asDaemonChromeInspectionStatus(await initializedChromeInspectionManager.configure(input)),
552
+ };
553
+ server = await startServer({
554
+ config: appConfig,
555
+ loadInstances: () => resolveChannelInstances(warn),
556
+ control,
557
+ sessionLog,
558
+ sessionRegistry,
559
+ sessionRuntimeServices,
560
+ pluginRuntime,
561
+ daemonManagement,
562
+ remoteRuntime,
563
+ remoteRuntimeAdmin,
564
+ runtimeTargetControl,
565
+ runtimeConnectionResolver: runtimeConnectionManager,
566
+ localSessionResources: machineSessions,
567
+ providerClis,
568
+ sessionEmulators,
569
+ sessionLifecycle: sessionResourceLifecycle,
570
+ sessionTerminals,
571
+ runtimeLocalBrowser: {
572
+ authorizeCredential: (credential) => sessionBrowserCapabilities.verify(credential.sessionId, credential.capability),
573
+ endpointForCredential: async (credential) => {
574
+ if (!sessionBrowserCapabilities.verify(credential.sessionId, credential.capability))
575
+ return undefined;
576
+ return sessionBrowsers.getRuntimeLocalEndpoint(credential.sessionId);
577
+ },
578
+ endpointForSession: (sessionId) => sessionBrowsers.getRuntimeLocalEndpoint(sessionId),
579
+ },
580
+ desktopBrowserHost: {
581
+ registry: desktopBrowserHosts,
582
+ daemonInstanceId,
583
+ },
584
+ });
585
+ baseShutdown = server.shutdown.bind(server);
437
586
  if (directConfig && directEncryptionIdentity) {
438
587
  const startedDirect = await startDirectListener(startDirectServer, directConfig, {
439
588
  e2ee: {
@@ -445,6 +594,9 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
445
594
  isPeerActive: (peer) => remoteRuntimeAccess.isClientGrantActive(peer.grantId, peer.clientId),
446
595
  connectionHub: directConnections,
447
596
  browserSurface: { host: sessionBrowserSurfaces },
597
+ ...(runtimeBrowserSupported
598
+ ? { browserInspect: { host: directBrowserInspectHost } }
599
+ : {}),
448
600
  ...(sessionEmulatorSurface
449
601
  ? { emulatorSurface: { host: sessionEmulatorSurface } }
450
602
  : {}),
@@ -464,6 +616,15 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
464
616
  }
465
617
  catch (startupError) {
466
618
  const rollbackErrors = [startupError];
619
+ server?.stopAccepting();
620
+ if (chromeInspectionManager) {
621
+ try {
622
+ await chromeInspectionManager.shutdown();
623
+ }
624
+ catch (shutdownError) {
625
+ rollbackErrors.push(shutdownError);
626
+ }
627
+ }
467
628
  if (directServer) {
468
629
  try {
469
630
  await directServer.shutdown();
@@ -473,7 +634,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
473
634
  }
474
635
  }
475
636
  try {
476
- await baseShutdown();
637
+ await runtimeConnectionManager.close();
477
638
  }
478
639
  catch (shutdownError) {
479
640
  rollbackErrors.push(shutdownError);
@@ -487,25 +648,47 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
487
648
  finally {
488
649
  durableDesktopBrowserHost.dispose();
489
650
  }
490
- try {
491
- await runtimeConnectionManager.close();
651
+ if (appBrowserHostSupervisor) {
652
+ try {
653
+ await appBrowserHostSupervisor.stop();
654
+ }
655
+ catch (shutdownError) {
656
+ rollbackErrors.push(shutdownError);
657
+ }
492
658
  }
493
- catch (shutdownError) {
494
- rollbackErrors.push(shutdownError);
659
+ if (baseShutdown) {
660
+ try {
661
+ await baseShutdown();
662
+ }
663
+ catch (shutdownError) {
664
+ rollbackErrors.push(shutdownError);
665
+ }
495
666
  }
496
667
  if (rollbackErrors.length > 1) {
497
- throw new AggregateError(rollbackErrors, "failed to start Direct Runtime and roll back the local server");
668
+ throw new AggregateError(rollbackErrors, "failed to start daemon resources and roll back the local server");
498
669
  }
499
670
  throw startupError;
500
671
  }
672
+ if (!server || !baseShutdown || !chromeInspectionManager) {
673
+ throw new Error("daemon startup completed without all required resources");
674
+ }
501
675
  let shutdownPromise;
502
676
  server.shutdown = () => {
503
677
  if (shutdownPromise)
504
678
  return shutdownPromise;
679
+ // Fence Local admission without closing the Desktop Browser Host WS.
680
+ // Browser handles can therefore close gracefully before full teardown.
681
+ server.stopAccepting();
505
682
  shutdownPromise = (async () => {
506
683
  let shutdownComplete = false;
507
684
  try {
508
685
  const shutdownErrors = [];
686
+ try {
687
+ await chromeInspectionManager.shutdown();
688
+ }
689
+ catch (error) {
690
+ shutdownErrors.push(error);
691
+ }
509
692
  if (directServer) {
510
693
  try {
511
694
  await directServer.shutdown();
@@ -514,12 +697,6 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
514
697
  shutdownErrors.push(error);
515
698
  }
516
699
  }
517
- try {
518
- await baseShutdown();
519
- }
520
- catch (error) {
521
- shutdownErrors.push(error);
522
- }
523
700
  try {
524
701
  await runtimeConnectionManager.close();
525
702
  }
@@ -535,6 +712,20 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
535
712
  finally {
536
713
  durableDesktopBrowserHost.dispose();
537
714
  }
715
+ if (appBrowserHostSupervisor) {
716
+ try {
717
+ await appBrowserHostSupervisor.stop();
718
+ }
719
+ catch (error) {
720
+ shutdownErrors.push(error);
721
+ }
722
+ }
723
+ try {
724
+ await baseShutdown();
725
+ }
726
+ catch (error) {
727
+ shutdownErrors.push(error);
728
+ }
538
729
  try {
539
730
  browserRunnerSessionContexts.closeAll();
540
731
  sessionBrowserCapabilities.revokeAll();
@@ -558,6 +749,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
558
749
  })();
559
750
  return shutdownPromise;
560
751
  };
752
+ requestServerShutdown = server.shutdown.bind(server);
561
753
  ownerManagedByServer = true;
562
754
  try {
563
755
  await writeEndpoint(appConfig, managementToken);
@@ -579,6 +771,370 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
579
771
  throw error;
580
772
  }
581
773
  }
774
+ export function createSupervisedAppSessionBrowserHost(desktop, supervisor) {
775
+ const withOnDemandSidecar = async (operation) => {
776
+ try {
777
+ return await operation();
778
+ }
779
+ catch (error) {
780
+ if (!(error instanceof DesktopBrowserHostAbsentError))
781
+ throw error;
782
+ }
783
+ try {
784
+ await supervisor.ensureConnected();
785
+ }
786
+ catch (startupError) {
787
+ try {
788
+ return await operation();
789
+ }
790
+ catch (retryError) {
791
+ if (retryError instanceof DesktopBrowserHostAbsentError)
792
+ throw startupError;
793
+ throw retryError;
794
+ }
795
+ }
796
+ try {
797
+ return await operation();
798
+ }
799
+ catch (error) {
800
+ if (!(error instanceof DesktopBrowserHostAbsentError))
801
+ throw error;
802
+ await supervisor.restart();
803
+ return operation();
804
+ }
805
+ };
806
+ return {
807
+ createBrowser: (input) => withOnDemandSidecar(() => desktop.createBrowser(input)),
808
+ ...(desktop.deleteSessionData
809
+ ? {
810
+ deleteSessionData: (sessionId) => withOnDemandSidecar(() => desktop.deleteSessionData(sessionId)),
811
+ }
812
+ : {}),
813
+ };
814
+ }
815
+ function asDaemonChromeInspectionStatus(status) {
816
+ if (status.runtime.address !== "127.0.0.1:9333") {
817
+ throw new Error("resident Chrome inspection must use the fixed 127.0.0.1:9333 endpoint");
818
+ }
819
+ return {
820
+ ...status,
821
+ runtime: {
822
+ ...status.runtime,
823
+ address: "127.0.0.1:9333",
824
+ },
825
+ };
826
+ }
827
+ /**
828
+ * Resolve the authenticated semantic target inside its owning Runtime and
829
+ * attach directly to that Page's loopback CDP WebSocket. No endpoint is ever
830
+ * projected into the Direct protocol.
831
+ */
832
+ export function createDirectRuntimeBrowserInspectHost(browsers, options = {}) {
833
+ const openTimeoutMs = options.openTimeoutMs ??
834
+ BROWSER_INSPECT_OPEN_TIMEOUT_MS;
835
+ if (!Number.isSafeInteger(openTimeoutMs) || openTimeoutMs <= 0) {
836
+ throw new TypeError("Browser Inspect open timeout must be a positive integer");
837
+ }
838
+ return {
839
+ open: async (target, context) => {
840
+ const deadline = AbortSignal.timeout(openTimeoutMs);
841
+ const openingSignal = AbortSignal.any([context.signal, deadline]);
842
+ let socket;
843
+ try {
844
+ const endpoint = await waitForBrowserInspectValue(browsers.getInspectionPageEndpoint(target), openingSignal);
845
+ const url = parseRuntimeLocalPageEndpoint(endpoint.endpoint);
846
+ socket = new WebSocket(url, {
847
+ followRedirects: false,
848
+ handshakeTimeout: openTimeoutMs,
849
+ maxPayload: RUNTIME_BROWSER_INSPECT_MAX_MESSAGE_BYTES,
850
+ perMessageDeflate: false,
851
+ });
852
+ await waitForBrowserInspectSocketOpen(socket, openingSignal);
853
+ return browserInspectAttachment(socket, context.signal);
854
+ }
855
+ catch (error) {
856
+ if (socket?.readyState !== WebSocket.CLOSED)
857
+ socket?.terminate();
858
+ throw mapBrowserInspectOpenError(error, context.signal.aborted, deadline.aborted);
859
+ }
860
+ },
861
+ };
862
+ }
863
+ function browserInspectAttachment(socket, signal) {
864
+ const queue = new BrowserInspectMessageQueue(BROWSER_INSPECT_MAX_QUEUED_BYTES, () => socket.pause(), () => {
865
+ if (socket.readyState === WebSocket.OPEN)
866
+ socket.resume();
867
+ });
868
+ let closed = false;
869
+ const onMessage = (data, binary) => {
870
+ const message = binary
871
+ ? { binary: true, data: copyBrowserInspectBytes(data) }
872
+ : { binary: false, data: browserInspectText(data) };
873
+ if (!queue.push(message)) {
874
+ const error = new Error("Browser Inspect local Page output queue is full");
875
+ queue.fail(error);
876
+ void closeBrowserInspectSocket(socket, 1_009, "Browser Inspect output capacity exceeded");
877
+ }
878
+ };
879
+ const onError = (error) => queue.fail(error);
880
+ const onClose = () => queue.end();
881
+ const onAbort = () => {
882
+ queue.fail(signal.reason instanceof Error
883
+ ? signal.reason
884
+ : new Error("Browser Inspect attachment was cancelled"));
885
+ void closeBrowserInspectSocket(socket, 1_001, "Browser Inspect cancelled");
886
+ };
887
+ socket.on("message", onMessage);
888
+ socket.once("error", onError);
889
+ socket.once("close", onClose);
890
+ signal.addEventListener("abort", onAbort, { once: true });
891
+ if (signal.aborted)
892
+ onAbort();
893
+ const close = async (reason) => {
894
+ if (closed)
895
+ return;
896
+ closed = true;
897
+ signal.removeEventListener("abort", onAbort);
898
+ socket.removeListener("message", onMessage);
899
+ socket.removeListener("error", onError);
900
+ socket.removeListener("close", onClose);
901
+ queue.end();
902
+ await closeBrowserInspectSocket(socket, reason === "client_closed" ? 1_000 : 1_001, `Browser Inspect ${reason.replaceAll("_", " ")}`);
903
+ };
904
+ return {
905
+ messages: queue,
906
+ send: async (message) => {
907
+ if (closed || socket.readyState !== WebSocket.OPEN) {
908
+ throw new Error("Browser Inspect local Page is closed");
909
+ }
910
+ const byteLength = message.binary
911
+ ? message.data.byteLength
912
+ : Buffer.byteLength(message.data, "utf8");
913
+ if (byteLength > RUNTIME_BROWSER_INSPECT_MAX_MESSAGE_BYTES ||
914
+ socket.bufferedAmount + byteLength >
915
+ BROWSER_INSPECT_MAX_QUEUED_BYTES) {
916
+ throw new Error("Browser Inspect local Page input capacity exceeded");
917
+ }
918
+ await new Promise((resolve, reject) => {
919
+ socket.send(message.data, { binary: message.binary, compress: false }, (error) => error ? reject(error) : resolve());
920
+ });
921
+ },
922
+ close,
923
+ };
924
+ }
925
+ class BrowserInspectMessageQueue {
926
+ maxBytes;
927
+ onBuffered;
928
+ onDrained;
929
+ values = [];
930
+ waiters = [];
931
+ queuedBytes = 0;
932
+ ended = false;
933
+ failure;
934
+ constructor(maxBytes, onBuffered, onDrained) {
935
+ this.maxBytes = maxBytes;
936
+ this.onBuffered = onBuffered;
937
+ this.onDrained = onDrained;
938
+ }
939
+ push(message) {
940
+ if (this.ended || this.failure !== undefined)
941
+ return false;
942
+ const byteLength = message.binary
943
+ ? message.data.byteLength
944
+ : Buffer.byteLength(message.data, "utf8");
945
+ if (byteLength > RUNTIME_BROWSER_INSPECT_MAX_MESSAGE_BYTES)
946
+ return false;
947
+ const waiter = this.waiters.shift();
948
+ if (waiter) {
949
+ waiter.resolve({ value: message, done: false });
950
+ return true;
951
+ }
952
+ if (this.queuedBytes + byteLength > this.maxBytes)
953
+ return false;
954
+ this.values.push({ message, byteLength });
955
+ this.queuedBytes += byteLength;
956
+ if (this.values.length === 1)
957
+ this.onBuffered();
958
+ return true;
959
+ }
960
+ end() {
961
+ if (this.ended || this.failure !== undefined)
962
+ return;
963
+ this.ended = true;
964
+ for (const waiter of this.waiters.splice(0)) {
965
+ waiter.resolve({ value: undefined, done: true });
966
+ }
967
+ }
968
+ fail(error) {
969
+ if (this.ended || this.failure !== undefined)
970
+ return;
971
+ this.failure = error;
972
+ this.values.length = 0;
973
+ this.queuedBytes = 0;
974
+ this.onDrained();
975
+ for (const waiter of this.waiters.splice(0))
976
+ waiter.reject(error);
977
+ }
978
+ [Symbol.asyncIterator]() {
979
+ return {
980
+ next: () => {
981
+ if (this.failure !== undefined)
982
+ return Promise.reject(this.failure);
983
+ const entry = this.values.shift();
984
+ if (entry) {
985
+ this.queuedBytes -= entry.byteLength;
986
+ if (this.values.length === 0)
987
+ this.onDrained();
988
+ return Promise.resolve({ value: entry.message, done: false });
989
+ }
990
+ if (this.ended) {
991
+ return Promise.resolve({ value: undefined, done: true });
992
+ }
993
+ return new Promise((resolve, reject) => this.waiters.push({ resolve, reject }));
994
+ },
995
+ };
996
+ }
997
+ }
998
+ function parseRuntimeLocalPageEndpoint(endpoint) {
999
+ let url;
1000
+ try {
1001
+ url = new URL(endpoint);
1002
+ }
1003
+ catch {
1004
+ throw new Error("Browser Inspect local Page endpoint is invalid");
1005
+ }
1006
+ if (url.protocol !== "ws:" ||
1007
+ url.hostname !== "127.0.0.1" ||
1008
+ !url.port ||
1009
+ url.username ||
1010
+ url.password ||
1011
+ url.search ||
1012
+ url.hash ||
1013
+ !/^\/devtools\/page\/[A-Za-z0-9._~%-]+$/.test(url.pathname)) {
1014
+ throw new Error("Browser Inspect local Page endpoint must be an exact loopback Page WebSocket");
1015
+ }
1016
+ return url.href;
1017
+ }
1018
+ function waitForBrowserInspectValue(promise, signal) {
1019
+ if (signal.aborted) {
1020
+ return Promise.reject(signal.reason ?? new Error("Browser Inspect opening was cancelled"));
1021
+ }
1022
+ return new Promise((resolve, reject) => {
1023
+ let settled = false;
1024
+ const finish = (callback) => {
1025
+ if (settled)
1026
+ return;
1027
+ settled = true;
1028
+ signal.removeEventListener("abort", onAbort);
1029
+ callback();
1030
+ };
1031
+ const onAbort = () => finish(() => reject(signal.reason ?? new Error("Browser Inspect opening was cancelled")));
1032
+ signal.addEventListener("abort", onAbort, { once: true });
1033
+ promise.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
1034
+ });
1035
+ }
1036
+ function waitForBrowserInspectSocketOpen(socket, signal) {
1037
+ if (socket.readyState === WebSocket.OPEN)
1038
+ return Promise.resolve();
1039
+ return new Promise((resolve, reject) => {
1040
+ const cleanup = () => {
1041
+ signal.removeEventListener("abort", onAbort);
1042
+ socket.removeListener("open", onOpen);
1043
+ socket.removeListener("error", onError);
1044
+ socket.removeListener("unexpected-response", onUnexpectedResponse);
1045
+ };
1046
+ const onOpen = () => {
1047
+ cleanup();
1048
+ resolve();
1049
+ };
1050
+ const onError = (error) => {
1051
+ cleanup();
1052
+ reject(error);
1053
+ };
1054
+ const onUnexpectedResponse = (_request, response) => {
1055
+ response.resume();
1056
+ cleanup();
1057
+ reject(new Error("Browser Inspect local Page rejected the connection"));
1058
+ };
1059
+ const onAbort = () => {
1060
+ cleanup();
1061
+ socket.terminate();
1062
+ reject(signal.reason ?? new Error("Browser Inspect opening was cancelled"));
1063
+ };
1064
+ if (signal.aborted)
1065
+ return onAbort();
1066
+ signal.addEventListener("abort", onAbort, { once: true });
1067
+ socket.once("open", onOpen);
1068
+ socket.once("error", onError);
1069
+ socket.once("unexpected-response", onUnexpectedResponse);
1070
+ });
1071
+ }
1072
+ function closeBrowserInspectSocket(socket, code, reason) {
1073
+ if (socket.readyState === WebSocket.CLOSED)
1074
+ return Promise.resolve();
1075
+ return new Promise((resolve) => {
1076
+ let settled = false;
1077
+ const finish = () => {
1078
+ if (settled)
1079
+ return;
1080
+ settled = true;
1081
+ clearTimeout(timer);
1082
+ socket.removeListener("close", finish);
1083
+ resolve();
1084
+ };
1085
+ const timer = setTimeout(() => {
1086
+ if (socket.readyState !== WebSocket.CLOSED)
1087
+ socket.terminate();
1088
+ finish();
1089
+ }, BROWSER_INSPECT_CLOSE_TIMEOUT_MS);
1090
+ timer.unref?.();
1091
+ socket.once("close", finish);
1092
+ if (socket.readyState === WebSocket.OPEN) {
1093
+ socket.close(code, reason.slice(0, 123));
1094
+ }
1095
+ else {
1096
+ socket.terminate();
1097
+ }
1098
+ });
1099
+ }
1100
+ function copyBrowserInspectBytes(data) {
1101
+ if (data instanceof ArrayBuffer)
1102
+ return new Uint8Array(data.slice(0));
1103
+ if (Array.isArray(data))
1104
+ return new Uint8Array(Buffer.concat(data));
1105
+ return new Uint8Array(Buffer.from(data));
1106
+ }
1107
+ function browserInspectText(data) {
1108
+ return Buffer.from(copyBrowserInspectBytes(data)).toString("utf8");
1109
+ }
1110
+ function mapBrowserInspectOpenError(error, callerAborted, deadlineAborted) {
1111
+ if (error instanceof DirectRuntimeBrowserInspectHostError)
1112
+ return error;
1113
+ if (callerAborted) {
1114
+ return new DirectRuntimeBrowserInspectHostError("cancelled", "Browser Inspect attachment was cancelled", { cause: error });
1115
+ }
1116
+ if (deadlineAborted) {
1117
+ return new DirectRuntimeBrowserInspectHostError("internal_error", "Browser Inspect local Page opening timed out", { cause: error });
1118
+ }
1119
+ if (error instanceof SessionBrowserServiceError) {
1120
+ const reason = error.code === "generation_mismatch"
1121
+ ? "generation_changed"
1122
+ : error.code === "not_found" || error.code === "invalid_request"
1123
+ ? "not_found"
1124
+ : error.code === "capacity"
1125
+ ? "capacity"
1126
+ : error.code === "unavailable" &&
1127
+ error.message.includes("does not support")
1128
+ ? "unsupported"
1129
+ : error.code === "unavailable" || error.code === "shutting_down"
1130
+ ? "browser_closed"
1131
+ : "internal_error";
1132
+ return new DirectRuntimeBrowserInspectHostError(reason, error.message, {
1133
+ cause: error,
1134
+ });
1135
+ }
1136
+ return new DirectRuntimeBrowserInspectHostError("internal_error", "Browser Inspect local Page is unavailable", { cause: error });
1137
+ }
582
1138
  async function startDirectListener(start, config, options) {
583
1139
  const candidates = [...new Set([config.port, ...(config.fallbackPorts ?? [])])];
584
1140
  for (let index = 0; index < candidates.length; index += 1) {