@rynx-ai/daemon 0.1.0 → 0.1.9

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 (89) hide show
  1. package/dist/browser-cli-args.d.ts +20 -0
  2. package/dist/browser-cli-args.js +100 -0
  3. package/dist/browser-runner-session-context.d.ts +24 -0
  4. package/dist/browser-runner-session-context.js +113 -0
  5. package/dist/cdp-keyboard-input.d.ts +6 -0
  6. package/dist/cdp-keyboard-input.js +209 -0
  7. package/dist/channel-store.d.ts +0 -6
  8. package/dist/channel-store.js +2 -18
  9. package/dist/chrome-for-testing-release-resolver.d.ts +34 -0
  10. package/dist/chrome-for-testing-release-resolver.js +214 -0
  11. package/dist/chrome-for-testing-store.d.ts +85 -0
  12. package/dist/chrome-for-testing-store.js +836 -0
  13. package/dist/cli.js +467 -30
  14. package/dist/control-client.d.ts +113 -0
  15. package/dist/control-client.js +792 -0
  16. package/dist/control-deps.js +37 -6
  17. package/dist/control-endpoint.d.ts +14 -0
  18. package/dist/control-endpoint.js +124 -0
  19. package/dist/control-link-store.d.ts +16 -0
  20. package/dist/control-link-store.js +53 -0
  21. package/dist/daemon-owner.d.ts +14 -0
  22. package/dist/daemon-owner.js +104 -0
  23. package/dist/daemon-server.d.ts +12 -2
  24. package/dist/daemon-server.js +559 -28
  25. package/dist/db.js +140 -10
  26. package/dist/desktop-browser-host-client.d.ts +34 -0
  27. package/dist/desktop-browser-host-client.js +413 -0
  28. package/dist/direct-runtime-authenticator.d.ts +13 -0
  29. package/dist/direct-runtime-authenticator.js +127 -0
  30. package/dist/direct-runtime-config.d.ts +20 -0
  31. package/dist/direct-runtime-config.js +101 -0
  32. package/dist/embedded-browser-profile-cleanup.d.ts +58 -0
  33. package/dist/embedded-browser-profile-cleanup.js +338 -0
  34. package/dist/headless-browser-host.d.ts +14 -0
  35. package/dist/headless-browser-host.js +2156 -0
  36. package/dist/index-daemon.js +28 -5
  37. package/dist/installation-id.d.ts +33 -0
  38. package/dist/installation-id.js +692 -0
  39. package/dist/migrations/cleanup-legacy-sessions.d.ts +0 -3
  40. package/dist/migrations/cleanup-legacy-sessions.js +10 -28
  41. package/dist/plugin-cli.d.ts +30 -0
  42. package/dist/plugin-cli.js +270 -0
  43. package/dist/plugin-host-rpc.d.ts +85 -0
  44. package/dist/plugin-host-rpc.js +878 -0
  45. package/dist/plugin-install-restart.d.ts +13 -0
  46. package/dist/plugin-install-restart.js +24 -0
  47. package/dist/plugin-installer.d.ts +36 -15
  48. package/dist/plugin-installer.js +420 -155
  49. package/dist/plugin-package.d.ts +38 -0
  50. package/dist/plugin-package.js +553 -0
  51. package/dist/plugin-runtime-control.d.ts +2 -0
  52. package/dist/plugin-runtime-control.js +37 -0
  53. package/dist/plugin-store.d.ts +37 -8
  54. package/dist/plugin-store.js +80 -15
  55. package/dist/plugin-supervisor.d.ts +160 -0
  56. package/dist/plugin-supervisor.js +1143 -0
  57. package/dist/pm2.d.ts +3 -1
  58. package/dist/pm2.js +55 -13
  59. package/dist/provider-cli-service.d.ts +46 -0
  60. package/dist/provider-cli-service.js +510 -0
  61. package/dist/registry.d.ts +6 -19
  62. package/dist/registry.js +22 -87
  63. package/dist/remote-runtime-access-store.d.ts +61 -0
  64. package/dist/remote-runtime-access-store.js +386 -0
  65. package/dist/remote-runtime-admin.d.ts +32 -0
  66. package/dist/remote-runtime-admin.js +107 -0
  67. package/dist/remote-runtime-connection-manager.d.ts +92 -0
  68. package/dist/remote-runtime-connection-manager.js +1409 -0
  69. package/dist/remote-runtime-credential-store.d.ts +45 -0
  70. package/dist/remote-runtime-credential-store.js +639 -0
  71. package/dist/remote-runtime-target-control.d.ts +47 -0
  72. package/dist/remote-runtime-target-control.js +433 -0
  73. package/dist/remote-runtime-target-store.d.ts +53 -0
  74. package/dist/remote-runtime-target-store.js +307 -0
  75. package/dist/runtime-share-addresses.d.ts +21 -0
  76. package/dist/runtime-share-addresses.js +50 -0
  77. package/dist/session-browser-capability-store.d.ts +13 -0
  78. package/dist/session-browser-capability-store.js +58 -0
  79. package/dist/session-emulator-binding-store.d.ts +10 -0
  80. package/dist/session-emulator-binding-store.js +61 -0
  81. package/dist/session-launch-operations.d.ts +41 -0
  82. package/dist/session-launch-operations.js +124 -0
  83. package/dist/session-meta-store.d.ts +3 -2
  84. package/dist/session-meta-store.js +57 -7
  85. package/dist/setup.js +9 -4
  86. package/dist/skills-catalog.js +8 -4
  87. package/dist/update.d.ts +11 -2
  88. package/dist/update.js +0 -27
  89. package/package.json +18 -10
@@ -5,19 +5,56 @@
5
5
  * channel env hydration, plugin catalog loading, control deps, session log, and
6
6
  * session registry. Keeping that wiring here avoids a second app-specific fork.
7
7
  */
8
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
8
+ import { randomBytes, randomUUID } from "node:crypto";
9
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
9
10
  import { dirname, join } from "node:path";
10
11
  import { loadConfig, rynxHome } from "@rynx-ai/core";
11
- import { startServer } from "@rynx-ai/server";
12
- import { applyChannelEnv } from "./channel-store.js";
12
+ import { REMOTE_RUNTIME_CORE_PROTOCOL } from "@rynx-ai/protocol/remote-runtime";
13
+ import { RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY } from "@rynx-ai/protocol/runtime-browser-surface";
14
+ 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";
13
16
  import { buildControlDeps } from "./control-deps.js";
17
+ import { BrowserRunnerSessionContextProvider } from "./browser-runner-session-context.js";
18
+ import { isChromeForTestingHostSupported } from "./chrome-for-testing-store.js";
19
+ import { createHeadlessBrowserHost } from "./headless-browser-host.js";
20
+ import { acquireDaemonOwner } from "./daemon-owner.js";
21
+ import { DaemonDirectRuntimeAuthenticator } from "./direct-runtime-authenticator.js";
22
+ import { DurableEmbeddedBrowserProfileHost } from "./embedded-browser-profile-cleanup.js";
23
+ import { loadDirectRuntimeConfig, } from "./direct-runtime-config.js";
24
+ import { isDaemonDirectRuntimeHostingSupported, loadOrCreateDaemonDirectRuntimeEncryptionIdentity, loadOrCreateDaemonInstallationIdentity, } from "./installation-id.js";
14
25
  import { cleanupLegacySessions } from "./migrations/cleanup-legacy-sessions.js";
26
+ import { PluginSupervisor } from "./plugin-supervisor.js";
27
+ import { pruneInactivePluginInstallations } from "./plugin-installer.js";
28
+ import { runPluginCliCommand } from "./plugin-cli.js";
29
+ import { getPlugin, isRunnablePluginRecord, listEnabledPlugins } from "./plugin-store.js";
15
30
  import { loadPlugins, resolveChannelInstances } from "./registry.js";
16
31
  import { SqliteSessionLogStore } from "./session-log-store.js";
32
+ import { SessionBrowserCapabilityStore } from "./session-browser-capability-store.js";
33
+ import { SqliteSessionEmulatorBindingStore } from "./session-emulator-binding-store.js";
17
34
  import { sessionRegistry } from "./session-meta-store.js";
35
+ import { RemoteRuntimeAccessStore } from "./remote-runtime-access-store.js";
36
+ import { DaemonRemoteRuntimeAdmin } from "./remote-runtime-admin.js";
37
+ import { FileRemoteRuntimeCredentialStore } from "./remote-runtime-credential-store.js";
38
+ import { DaemonRuntimeConnectionManager } from "./remote-runtime-connection-manager.js";
39
+ import { DaemonRuntimeTargetControl } from "./remote-runtime-target-control.js";
40
+ import { RemoteRuntimeTargetStore } from "./remote-runtime-target-store.js";
41
+ import { ProviderCliService } from "./provider-cli-service.js";
42
+ const PLUGIN_CLI_OUTPUT_MAX_BYTES = 256 * 1024;
43
+ 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";
18
44
  function defaultWarn(message) {
19
45
  console.warn(JSON.stringify({ level: "warn", type: "channel-registry", msg: message }));
20
46
  }
47
+ function unsupportedRemoteRuntimeAdmin(reason) {
48
+ const unsupported = () => {
49
+ throw new Error(reason);
50
+ };
51
+ return {
52
+ listShareAddresses: unsupported,
53
+ createPairingOffer: unsupported,
54
+ listClientGrants: unsupported,
55
+ revokeClientGrant: unsupported,
56
+ };
57
+ }
21
58
  function logLegacyCleanup(config) {
22
59
  try {
23
60
  const result = cleanupLegacySessions({ codexStorePath: config.AGENT_SESSION_STORE_PATH });
@@ -47,16 +84,24 @@ function dialableHost(host) {
47
84
  function controlEndpointFile() {
48
85
  return join(rynxHome(), "control.json");
49
86
  }
50
- async function writeControlEndpoint(config) {
87
+ async function writeControlEndpoint(config, managementToken) {
51
88
  const file = controlEndpointFile();
89
+ const temporary = `${file}.tmp-${process.pid}-${randomUUID()}`;
52
90
  await mkdir(dirname(file), { recursive: true });
53
- await writeFile(file, `${JSON.stringify({
54
- origin: `http://${dialableHost(config.HOST)}:${config.PORT}`,
55
- host: config.HOST,
56
- port: config.PORT,
57
- pid: process.pid,
58
- updatedAt: new Date().toISOString(),
59
- }, null, 2)}\n`, "utf8");
91
+ try {
92
+ await writeFile(temporary, `${JSON.stringify({
93
+ origin: `http://${dialableHost(config.HOST)}:${config.PORT}`,
94
+ host: config.HOST,
95
+ port: config.PORT,
96
+ pid: process.pid,
97
+ managementToken,
98
+ updatedAt: new Date().toISOString(),
99
+ }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
100
+ await rename(temporary, file);
101
+ }
102
+ finally {
103
+ await rm(temporary, { force: true });
104
+ }
60
105
  }
61
106
  async function removeControlEndpointForCurrentProcess() {
62
107
  try {
@@ -70,21 +115,507 @@ async function removeControlEndpointForCurrentProcess() {
70
115
  // Best effort only; stale endpoint files are ignored by the client fallback.
71
116
  }
72
117
  }
73
- export async function startRynxDaemonServer({ config, warn = defaultWarn, } = {}) {
74
- applyChannelEnv();
75
- await loadPlugins(warn);
76
- const appConfig = config ?? loadConfig();
77
- logLegacyCleanup(appConfig);
78
- const server = await startServer({
79
- config: appConfig,
80
- loadInstances: () => resolveChannelInstances(warn),
81
- control: buildControlDeps(),
82
- sessionLog: new SqliteSessionLogStore(),
83
- sessionRegistry,
84
- });
85
- await writeControlEndpoint(appConfig);
86
- server.on("close", () => {
87
- void removeControlEndpointForCurrentProcess();
88
- });
89
- return server;
118
+ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeEndpoint = writeControlEndpoint, acquireOwner = acquireDaemonOwner, loadDirectConfig = loadDirectRuntimeConfig, startDirectServer = startDirectRuntimeServer, } = {}) {
119
+ const owner = await acquireOwner();
120
+ let ownerManagedByServer = false;
121
+ try {
122
+ const appConfig = config ?? loadConfig();
123
+ const directHostingSupported = isDaemonDirectRuntimeHostingSupported();
124
+ const runtimeBrowserSupported = isChromeForTestingHostSupported();
125
+ const directConfig = directHostingSupported
126
+ ? await loadDirectConfig()
127
+ : undefined;
128
+ const control = buildControlDeps();
129
+ const installationIdentity = await loadOrCreateDaemonInstallationIdentity();
130
+ const directEncryptionIdentity = directHostingSupported
131
+ ? await loadOrCreateDaemonDirectRuntimeEncryptionIdentity()
132
+ : undefined;
133
+ if (!directHostingSupported)
134
+ warn(DIRECT_RUNTIME_HOSTING_UNSUPPORTED);
135
+ const daemonInstanceId = randomUUID();
136
+ const daemonStartedAt = new Date();
137
+ const remoteRuntime = createDaemonRuntimeHost({
138
+ daemonId: installationIdentity.installationId,
139
+ daemonInstanceId,
140
+ coreProtocolVersion: REMOTE_RUNTIME_CORE_PROTOCOL.currentVersion,
141
+ minimumCompatibleCoreVersion: REMOTE_RUNTIME_CORE_PROTOCOL.minimumCompatibleVersion,
142
+ semanticCapabilities: [
143
+ "semantic.session.list.v1",
144
+ "semantic.session.snapshot.v1",
145
+ "semantic.session.events.v1",
146
+ "semantic.session.terminal.v1",
147
+ "semantic.session.terminal-start.v1",
148
+ "semantic.session.interrupt.v1",
149
+ "semantic.session.agent-options.v1",
150
+ "semantic.session.launch-options.v2",
151
+ "semantic.session.create.v1",
152
+ "semantic.session.message.v1",
153
+ "semantic.session.delete.v1",
154
+ "semantic.session.interaction.v1",
155
+ "semantic.provider-cli.status.v1",
156
+ ...(runtimeBrowserSupported ? ["semantic.browser.v1"] : []),
157
+ ...(runtimeBrowserSupported && directHostingSupported
158
+ ? [RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY]
159
+ : []),
160
+ ...(control.emulator
161
+ ? ["semantic.emulator.unary.v1", "semantic.session.emulator.v1"]
162
+ : []),
163
+ ...(control.emulator && directHostingSupported
164
+ ? [RUNTIME_EMULATOR_SURFACE_SEMANTIC_CAPABILITY]
165
+ : []),
166
+ ],
167
+ startedAt: daemonStartedAt,
168
+ });
169
+ const managementToken = randomBytes(32).toString("base64url");
170
+ try {
171
+ for (const removed of pruneInactivePluginInstallations()) {
172
+ console.log(JSON.stringify({ level: "info", type: "plugin-gc", removed }));
173
+ }
174
+ }
175
+ catch (error) {
176
+ warn(`plugin package cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
177
+ }
178
+ const pluginSupervisor = new PluginSupervisor(listEnabledPlugins(), {
179
+ daemonInstanceId,
180
+ log: (entry) => {
181
+ const output = entry.level === "error"
182
+ ? console.error
183
+ : entry.level === "warn"
184
+ ? console.warn
185
+ : console.log;
186
+ output(JSON.stringify(entry));
187
+ },
188
+ });
189
+ await loadPlugins(pluginSupervisor, warn);
190
+ logLegacyCleanup(appConfig);
191
+ const reloadPluginRuntime = async (pluginId) => {
192
+ const record = getPlugin(pluginId);
193
+ if (!record)
194
+ throw new Error(`plugin "${pluginId}" is not installed`);
195
+ if (!record.enabled)
196
+ throw new Error(`plugin "${pluginId}" is disabled`);
197
+ if (!isRunnablePluginRecord(record)) {
198
+ throw new Error(`plugin "${pluginId}" must be reinstalled before its runtime can start`);
199
+ }
200
+ const status = await pluginSupervisor.reloadPluginRuntime(record);
201
+ await loadPlugins(pluginSupervisor, warn);
202
+ return { pluginId, status };
203
+ };
204
+ const ensurePluginRuntime = async (pluginId) => {
205
+ const record = getPlugin(pluginId);
206
+ if (!record)
207
+ throw new Error(`plugin "${pluginId}" is not installed`);
208
+ if (!record.enabled)
209
+ throw new Error(`plugin "${pluginId}" is disabled`);
210
+ if (!isRunnablePluginRecord(record)) {
211
+ throw new Error(`plugin "${pluginId}" must be reinstalled before its runtime can start`);
212
+ }
213
+ const status = await pluginSupervisor.ensurePluginRuntime(record);
214
+ await loadPlugins(pluginSupervisor, warn);
215
+ return { pluginId, status };
216
+ };
217
+ const invokePluginCli = async ({ pluginId, args, stdin }, { signal }) => {
218
+ const result = await runPluginCliCommand(pluginId, args, {
219
+ stdio: "pipe",
220
+ ...(stdin !== undefined ? { stdin } : {}),
221
+ captureOutputBytes: PLUGIN_CLI_OUTPUT_MAX_BYTES,
222
+ signal,
223
+ });
224
+ if (result.code === 0) {
225
+ const reload = result.effects.some((effect) => effect.effect === "runtime.reload");
226
+ const ensure = result.effects.some((effect) => effect.effect === "runtime.ensure");
227
+ if (reload)
228
+ await reloadPluginRuntime(pluginId);
229
+ else if (ensure)
230
+ await ensurePluginRuntime(pluginId);
231
+ }
232
+ const output = result.output;
233
+ return {
234
+ code: result.code,
235
+ stdout: output?.stdout ?? "",
236
+ stderr: output?.stderr ?? "",
237
+ truncated: Boolean(output?.stdoutTruncated || output?.stderrTruncated),
238
+ };
239
+ };
240
+ const pluginRuntime = {
241
+ bindHostServices: (services) => pluginSupervisor.bindHostServices({
242
+ ...services,
243
+ installationId: installationIdentity.installationId,
244
+ installationProof: {
245
+ algorithm: installationIdentity.algorithm,
246
+ publicKey: installationIdentity.publicKey,
247
+ sign: installationIdentity.sign,
248
+ },
249
+ }),
250
+ startPluginRuntimes: pluginSupervisor.startPluginRuntimes.bind(pluginSupervisor),
251
+ reloadPluginRuntime,
252
+ ensurePluginRuntime,
253
+ statusSnapshot: pluginSupervisor.statusSnapshot.bind(pluginSupervisor),
254
+ dispose: pluginSupervisor.dispose.bind(pluginSupervisor),
255
+ };
256
+ const daemonManagement = {
257
+ managementToken,
258
+ identity: () => ({
259
+ installationId: installationIdentity.installationId,
260
+ algorithm: installationIdentity.algorithm,
261
+ publicKey: installationIdentity.publicKey,
262
+ }),
263
+ invokePluginCli,
264
+ };
265
+ const remoteRuntimeAccess = new RemoteRuntimeAccessStore();
266
+ const directConnections = new DirectRuntimeConnectionHub();
267
+ const sessionLog = new SqliteSessionLogStore();
268
+ const providerClis = new ProviderCliService();
269
+ const sessionBrowserCapabilities = new SessionBrowserCapabilityStore();
270
+ const desktopBrowserHosts = new DesktopBrowserHostRegistry();
271
+ const durableDesktopBrowserHost = new DurableEmbeddedBrowserProfileHost(desktopBrowserHosts, { warn });
272
+ const browserRunnerSessionContexts = new BrowserRunnerSessionContextProvider({
273
+ rootDir: join(rynxHome(), "run", "browser-session-contexts"),
274
+ capabilities: sessionBrowserCapabilities,
275
+ });
276
+ const sessionRuntimeServices = createSessionRuntimeServices({
277
+ config: appConfig,
278
+ sessionLog,
279
+ sessionContextProvider: browserRunnerSessionContexts,
280
+ onMirrorError: (error, sessionId) => warn(`Session mirror failed for ${sessionId}: ${error instanceof Error ? error.message : String(error)}`),
281
+ });
282
+ const sessionBrowsers = new SessionBrowserService({
283
+ host: createDesktopFirstSessionBrowserHost(durableDesktopBrowserHost, createHeadlessBrowserHost()),
284
+ sessions: {
285
+ exists: async (sessionId) => Boolean(sessionRegistry.get(sessionId) ??
286
+ await sessionRuntimeServices.sessionStore.get(sessionId)),
287
+ },
288
+ });
289
+ const sessionEmulators = control.emulator
290
+ ? new SessionEmulatorService({
291
+ emulator: control.emulator,
292
+ store: new SqliteSessionEmulatorBindingStore(),
293
+ sessions: {
294
+ exists: async (sessionId) => Boolean(sessionRegistry.get(sessionId) ??
295
+ await sessionRuntimeServices.sessionStore.get(sessionId)),
296
+ },
297
+ })
298
+ : undefined;
299
+ const sessionBrowserSurfaces = new SessionBrowserSurfaceCoordinator(sessionBrowsers);
300
+ const sessionEmulatorSurface = sessionEmulators
301
+ ? {
302
+ open: async (frame, context) => {
303
+ const surface = await sessionEmulators.openSurface({
304
+ sessionId: frame.sessionId,
305
+ bindingId: frame.bindingId,
306
+ signal: context.signal,
307
+ });
308
+ let closed = false;
309
+ const close = () => {
310
+ if (closed)
311
+ return;
312
+ closed = true;
313
+ context.signal.removeEventListener("abort", close);
314
+ surface.close();
315
+ };
316
+ context.signal.addEventListener("abort", close, { once: true });
317
+ if (context.signal.aborted)
318
+ close();
319
+ return {
320
+ ready: {
321
+ type: "emulator.surface.ready",
322
+ surfaceId: frame.surfaceId,
323
+ sessionId: frame.sessionId,
324
+ bindingId: frame.bindingId,
325
+ backend: surface.backend,
326
+ deviceId: surface.deviceId,
327
+ format: surface.format,
328
+ },
329
+ capture: () => surface.capture(),
330
+ handle: (input) => surface.touch({ type: input.phase, x: input.x, y: input.y }),
331
+ close,
332
+ };
333
+ },
334
+ }
335
+ : undefined;
336
+ const sessionResourceLifecycle = {
337
+ beforeDelete: async (sessionId) => {
338
+ await Promise.all([
339
+ sessionBrowsers.cleanupSession(sessionId),
340
+ sessionEmulators?.cleanupSession(sessionId),
341
+ ]);
342
+ },
343
+ afterDelete: (sessionId) => {
344
+ sessionBrowserCapabilities.revoke(sessionId);
345
+ },
346
+ };
347
+ const machineSessions = new MachineSessionService({
348
+ registry: sessionRegistry,
349
+ log: sessionLog,
350
+ events: sessionRuntimeServices.sessionBus,
351
+ runtimeState: sessionRuntimeServices.sessionRuntimeIndex,
352
+ interruption: {
353
+ interrupt: (sessionId) => sessionRuntimeServices.runnerManager.interruptLiveSession(sessionId),
354
+ },
355
+ lifecycle: sessionResourceLifecycle,
356
+ agents: {
357
+ list: () => control.listAgents(),
358
+ },
359
+ execution: {
360
+ runtime: sessionRuntimeServices.conversationRuntime,
361
+ runner: sessionRuntimeServices.runnerManager,
362
+ sessionStore: sessionRuntimeServices.sessionStore,
363
+ },
364
+ });
365
+ const directAuthenticator = new DaemonDirectRuntimeAuthenticator(installationIdentity, remoteRuntime, remoteRuntimeAccess);
366
+ const sessionTerminals = createRunnerSessionTerminalHost({
367
+ runnerManager: sessionRuntimeServices.runnerManager,
368
+ resolveLiveSession: async (sessionId) => {
369
+ const bound = await sessionRuntimeServices.sessionStore.get(sessionId);
370
+ const registered = sessionRegistry.get(sessionId);
371
+ const cwd = bound?.cwd ?? registered?.config?.osEnv?.cwd;
372
+ if ((!bound && !registered) ||
373
+ !cwd ||
374
+ !sessionRuntimeServices.runnerManager.hasLiveSession(sessionId)) {
375
+ return undefined;
376
+ }
377
+ return { cwd };
378
+ },
379
+ });
380
+ const remoteRuntimeDispatcher = new RemoteRuntimeDispatcher({
381
+ runtime: remoteRuntime,
382
+ emulator: control.emulator,
383
+ sessionEmulators,
384
+ sessions: machineSessions,
385
+ terminals: sessionTerminals,
386
+ browsers: sessionBrowsers,
387
+ providerClis,
388
+ onError: (error) => warn(`Remote Runtime dispatch failed: ${error instanceof Error ? error.message : String(error)}`),
389
+ });
390
+ const remoteRuntimeAdmin = directConfig && directEncryptionIdentity
391
+ ? new DaemonRemoteRuntimeAdmin(installationIdentity, directEncryptionIdentity, remoteRuntimeAccess, directConfig, directConnections)
392
+ : unsupportedRemoteRuntimeAdmin(DIRECT_RUNTIME_HOSTING_UNSUPPORTED);
393
+ const runtimeTargets = new RemoteRuntimeTargetStore();
394
+ const runtimeCredentials = new FileRemoteRuntimeCredentialStore();
395
+ const runtimeConnectionManager = new DaemonRuntimeConnectionManager({
396
+ localDaemonId: remoteRuntime.status().daemonId,
397
+ dispatcher: remoteRuntimeDispatcher,
398
+ localBrowserSurface: sessionBrowserSurfaces,
399
+ localEmulatorSurface: sessionEmulatorSurface,
400
+ targets: runtimeTargets,
401
+ credentials: runtimeCredentials,
402
+ });
403
+ 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);
435
+ let directServer;
436
+ try {
437
+ if (directConfig && directEncryptionIdentity) {
438
+ const startedDirect = await startDirectListener(startDirectServer, directConfig, {
439
+ e2ee: {
440
+ serverPublicKey: directEncryptionIdentity.publicKey,
441
+ deriveSharedSecret: (clientPublicKey) => directEncryptionIdentity.deriveSharedSecret(clientPublicKey),
442
+ },
443
+ authenticator: directAuthenticator,
444
+ dispatcher: remoteRuntimeDispatcher,
445
+ isPeerActive: (peer) => remoteRuntimeAccess.isClientGrantActive(peer.grantId, peer.clientId),
446
+ connectionHub: directConnections,
447
+ browserSurface: { host: sessionBrowserSurfaces },
448
+ ...(sessionEmulatorSurface
449
+ ? { emulatorSurface: { host: sessionEmulatorSurface } }
450
+ : {}),
451
+ onError: (error, context) => warn(`Direct Runtime ${context} failed: ${error instanceof Error ? error.message : String(error)}`),
452
+ });
453
+ directServer = startedDirect.server;
454
+ // The admin object retains this config reference, so pairing links use
455
+ // the actual OS-selected fallback port instead of the requested zero.
456
+ directConfig.port = startedDirect.port;
457
+ try {
458
+ await directConfig.recordListeningPort?.(startedDirect.port);
459
+ }
460
+ catch (error) {
461
+ warn(`Direct Runtime fallback port could not be persisted: ${error instanceof Error ? error.message : String(error)}`);
462
+ }
463
+ }
464
+ }
465
+ catch (startupError) {
466
+ const rollbackErrors = [startupError];
467
+ if (directServer) {
468
+ try {
469
+ await directServer.shutdown();
470
+ }
471
+ catch (shutdownError) {
472
+ rollbackErrors.push(shutdownError);
473
+ }
474
+ }
475
+ try {
476
+ await baseShutdown();
477
+ }
478
+ catch (shutdownError) {
479
+ rollbackErrors.push(shutdownError);
480
+ }
481
+ try {
482
+ await sessionBrowsers.closeAll();
483
+ }
484
+ catch (shutdownError) {
485
+ rollbackErrors.push(shutdownError);
486
+ }
487
+ finally {
488
+ durableDesktopBrowserHost.dispose();
489
+ }
490
+ try {
491
+ await runtimeConnectionManager.close();
492
+ }
493
+ catch (shutdownError) {
494
+ rollbackErrors.push(shutdownError);
495
+ }
496
+ if (rollbackErrors.length > 1) {
497
+ throw new AggregateError(rollbackErrors, "failed to start Direct Runtime and roll back the local server");
498
+ }
499
+ throw startupError;
500
+ }
501
+ let shutdownPromise;
502
+ server.shutdown = () => {
503
+ if (shutdownPromise)
504
+ return shutdownPromise;
505
+ shutdownPromise = (async () => {
506
+ let shutdownComplete = false;
507
+ try {
508
+ const shutdownErrors = [];
509
+ if (directServer) {
510
+ try {
511
+ await directServer.shutdown();
512
+ }
513
+ catch (error) {
514
+ shutdownErrors.push(error);
515
+ }
516
+ }
517
+ try {
518
+ await baseShutdown();
519
+ }
520
+ catch (error) {
521
+ shutdownErrors.push(error);
522
+ }
523
+ try {
524
+ await runtimeConnectionManager.close();
525
+ }
526
+ catch (error) {
527
+ shutdownErrors.push(error);
528
+ }
529
+ try {
530
+ await sessionBrowsers.closeAll();
531
+ }
532
+ catch (error) {
533
+ shutdownErrors.push(error);
534
+ }
535
+ finally {
536
+ durableDesktopBrowserHost.dispose();
537
+ }
538
+ try {
539
+ browserRunnerSessionContexts.closeAll();
540
+ sessionBrowserCapabilities.revokeAll();
541
+ }
542
+ catch (error) {
543
+ shutdownErrors.push(error);
544
+ }
545
+ if (shutdownErrors.length > 0) {
546
+ throw new AggregateError(shutdownErrors, "failed to shut down all daemon listeners");
547
+ }
548
+ shutdownComplete = true;
549
+ }
550
+ finally {
551
+ await removeControlEndpointForCurrentProcess();
552
+ // A failed resource shutdown may leave plugin transports alive. Keep
553
+ // the process-scoped owner lease in that fail-closed state; it will be
554
+ // released by the OS when the process exits.
555
+ if (shutdownComplete)
556
+ await owner.release();
557
+ }
558
+ })();
559
+ return shutdownPromise;
560
+ };
561
+ ownerManagedByServer = true;
562
+ try {
563
+ await writeEndpoint(appConfig, managementToken);
564
+ }
565
+ catch (startupError) {
566
+ try {
567
+ await server.shutdown();
568
+ }
569
+ catch (shutdownError) {
570
+ throw new AggregateError([startupError, shutdownError], "failed to publish the daemon endpoint and roll back the server");
571
+ }
572
+ throw startupError;
573
+ }
574
+ return server;
575
+ }
576
+ catch (error) {
577
+ if (!ownerManagedByServer)
578
+ await owner.release();
579
+ throw error;
580
+ }
581
+ }
582
+ async function startDirectListener(start, config, options) {
583
+ const candidates = [...new Set([config.port, ...(config.fallbackPorts ?? [])])];
584
+ for (let index = 0; index < candidates.length; index += 1) {
585
+ const requestedPort = candidates[index];
586
+ try {
587
+ const server = await start({
588
+ ...options,
589
+ host: config.bindHost,
590
+ port: requestedPort,
591
+ });
592
+ const address = server.address();
593
+ if (address && typeof address !== "string" && address.port > 0) {
594
+ return { server, port: address.port };
595
+ }
596
+ if (requestedPort > 0)
597
+ return { server, port: requestedPort };
598
+ try {
599
+ await server.shutdown();
600
+ }
601
+ catch (shutdownError) {
602
+ throw new AggregateError([new Error("Direct Runtime did not report its OS-selected port"), shutdownError], "failed to resolve Direct Runtime listening port");
603
+ }
604
+ throw new Error("Direct Runtime did not report its OS-selected port");
605
+ }
606
+ catch (error) {
607
+ const hasAnotherCandidate = index + 1 < candidates.length;
608
+ const failedPersistedPreference = index === 0 && config.preferredPortIsPersistedFallback === true;
609
+ if (!hasAnotherCandidate || (!failedPersistedPreference && !isAddressInUse(error))) {
610
+ throw error;
611
+ }
612
+ }
613
+ }
614
+ throw new Error("Direct Runtime has no listener port candidate");
615
+ }
616
+ function isAddressInUse(error) {
617
+ return Boolean(error &&
618
+ typeof error === "object" &&
619
+ "code" in error &&
620
+ error.code === "EADDRINUSE");
90
621
  }