@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
@@ -9,12 +9,14 @@ import { readFile } from "node:fs/promises";
9
9
  import { join } from "node:path";
10
10
  import { listAgentSpecs, loadAgentSpec, rynxHome } from "@rynx-ai/core";
11
11
  import { createEmulatorControlDeps } from "@rynx-ai/emulator";
12
+ import { normalizePluginConfig } from "@rynx-ai/plugin-sdk";
12
13
  import { removeAgentSpec, writeAgentSpec } from "./agent-file.js";
13
14
  import { getCatalogSkill, installCatalogSkill, listCatalogSkills, readCatalogSkillFile, removeCatalogSkill, } from "./skills-catalog.js";
14
- import { createChannel, listChannels, removeChannel, renameChannel, setChannelOptions, } from "./channel-store.js";
15
+ import { createChannel, getChannel, listChannels, removeChannel, renameChannel, setChannelOptions, } from "./channel-store.js";
15
16
  import { createInstance, listInstances, removeInstance, setInstanceAgent, setInstanceEnabled, } from "./instance-store.js";
16
17
  import { getChannelType, listChannelTypes } from "./registry.js";
17
18
  import { createSessionMeta, getSessionMeta, listSessionMetas, removeSessionMeta, setSessionTitle, } from "./session-meta-store.js";
19
+ import { consumeControlLink } from "./control-link-store.js";
18
20
  export function buildControlDeps() {
19
21
  return {
20
22
  // ── channel types (from loaded plugins) ──────────────────────────────
@@ -25,22 +27,43 @@ export function buildControlDeps() {
25
27
  })),
26
28
  // ── channels (config + credentials) ──────────────────────────────────
27
29
  listChannels: () => listChannels(),
28
- createChannel: (input) => ({ id: createChannel(input).id }),
30
+ createChannel: (input) => {
31
+ if (!getChannelType(input.type))
32
+ throw new Error(`unknown chat channel type: ${input.type}`);
33
+ return {
34
+ id: createChannel({
35
+ ...input,
36
+ ...(input.options !== undefined
37
+ ? { options: normalizeChannelOptions(input.type, input.options) }
38
+ : {}),
39
+ }).id,
40
+ };
41
+ },
29
42
  setChannel: (id, patch) => {
43
+ let normalizedOptions;
44
+ if (patch.options !== undefined) {
45
+ const current = getChannel(id);
46
+ if (!current)
47
+ throw new Error(`channel not found: ${id}`);
48
+ normalizedOptions = normalizeChannelOptions(current.type, {
49
+ ...(current.options ?? {}),
50
+ ...patch.options,
51
+ });
52
+ }
30
53
  if (patch.name !== undefined)
31
54
  renameChannel(id, patch.name);
32
- if (patch.options !== undefined)
33
- setChannelOptions(id, patch.options);
55
+ if (normalizedOptions !== undefined)
56
+ setChannelOptions(id, normalizedOptions);
34
57
  },
35
58
  removeChannel: (id) => {
36
59
  removeChannel(id);
37
60
  },
38
- authorizeChannel: (type, emit, signal) => {
61
+ authorizeChannel: (type, context, emit, signal) => {
39
62
  const descriptor = getChannelType(type);
40
63
  if (!descriptor?.authorize) {
41
64
  throw new Error(`channel type "${type}" has no authorize flow`);
42
65
  }
43
- return descriptor.authorize(emit, signal);
66
+ return descriptor.authorize(emit, signal, context);
44
67
  },
45
68
  // ── instances (channel + agent binding) ──────────────────────────────
46
69
  listInstancesConfig: () => listInstances().map((i) => ({
@@ -87,9 +110,17 @@ export function buildControlDeps() {
87
110
  createSessionMeta: (meta) => createSessionMeta(meta),
88
111
  setSessionTitle: (id, title) => setSessionTitle(id, title),
89
112
  removeSessionMeta: (id) => removeSessionMeta(id),
113
+ consumeControlLink: (token) => consumeControlLink(token),
90
114
  tailLogs: (instanceId, lines) => tailLogs(instanceId, lines),
91
115
  };
92
116
  }
117
+ function normalizeChannelOptions(type, options) {
118
+ const descriptor = getChannelType(type);
119
+ if (!descriptor)
120
+ throw new Error(`unknown chat channel type: ${type}`);
121
+ const schema = descriptor.configSchema ?? [];
122
+ return schema.length > 0 ? normalizePluginConfig(schema, options) : { ...options };
123
+ }
93
124
  /** Tail the daemon's merged pm2 logs, best-effort filtered by instance id. */
94
125
  async function tailLogs(instanceId, lines) {
95
126
  const dir = join(rynxHome(), "logs");
@@ -0,0 +1,14 @@
1
+ export interface DaemonControlEndpoint {
2
+ origin: string;
3
+ pid: number;
4
+ managementToken: string;
5
+ }
6
+ /**
7
+ * Resolve the single resident daemon advertised by RYNX_HOME. If no live owner
8
+ * exists, start the pm2-supervised daemon and wait for it to publish its
9
+ * endpoint. This deliberately never guesses another port or starts an
10
+ * app-owned daemon.
11
+ */
12
+ export declare function ensureDaemonControlEndpoint(options?: {
13
+ signal?: AbortSignal;
14
+ }): Promise<DaemonControlEndpoint>;
@@ -0,0 +1,124 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { rynxHome } from "@rynx-ai/core";
4
+ import { isPm2DaemonOnline, startDaemon } from "./pm2.js";
5
+ const STARTUP_TIMEOUT_MS = 20_000;
6
+ const PROBE_TIMEOUT_MS = 1_000;
7
+ /**
8
+ * Resolve the single resident daemon advertised by RYNX_HOME. If no live owner
9
+ * exists, start the pm2-supervised daemon and wait for it to publish its
10
+ * endpoint. This deliberately never guesses another port or starts an
11
+ * app-owned daemon.
12
+ */
13
+ export async function ensureDaemonControlEndpoint(options = {}) {
14
+ const { signal } = options;
15
+ signal?.throwIfAborted();
16
+ const deadline = Date.now() + STARTUP_TIMEOUT_MS;
17
+ const current = await readControlEndpoint();
18
+ if (current && isProcessAlive(current.pid)) {
19
+ const recovered = await waitForOwnedEndpoint(current, deadline, signal);
20
+ if (recovered)
21
+ return recovered;
22
+ if (isProcessAlive(current.pid)) {
23
+ throw new Error(`Rynx daemon process ${current.pid} is alive but its control endpoint did not become healthy`);
24
+ }
25
+ }
26
+ if (Date.now() >= deadline) {
27
+ throw new Error("Rynx daemon stopped before its control endpoint became healthy");
28
+ }
29
+ const force = isPm2DaemonOnline();
30
+ const status = startDaemon({ force, quiet: true });
31
+ if (status !== 0)
32
+ throw new Error("failed to start the Rynx daemon");
33
+ while (Date.now() < deadline) {
34
+ signal?.throwIfAborted();
35
+ const endpoint = await readControlEndpoint();
36
+ if (endpoint && isProcessAlive(endpoint.pid) && await isHealthy(endpoint.origin, signal))
37
+ return endpoint;
38
+ await abortableDelay(100, signal);
39
+ }
40
+ throw new Error("Rynx daemon did not publish a ready control endpoint");
41
+ }
42
+ async function waitForOwnedEndpoint(endpoint, deadline, signal) {
43
+ while (Date.now() < deadline && isProcessAlive(endpoint.pid)) {
44
+ signal?.throwIfAborted();
45
+ if (await isHealthy(endpoint.origin, signal))
46
+ return endpoint;
47
+ await abortableDelay(100, signal);
48
+ }
49
+ return null;
50
+ }
51
+ async function readControlEndpoint() {
52
+ try {
53
+ const parsed = JSON.parse(await readFile(path.join(rynxHome(), "control.json"), "utf8"));
54
+ if (typeof parsed.origin !== "string" ||
55
+ typeof parsed.pid !== "number" ||
56
+ !Number.isInteger(parsed.pid) ||
57
+ parsed.pid <= 0 ||
58
+ typeof parsed.managementToken !== "string" ||
59
+ parsed.managementToken.length < 32 ||
60
+ parsed.managementToken.length > 256) {
61
+ return null;
62
+ }
63
+ const origin = new URL(parsed.origin);
64
+ if ((origin.protocol !== "http:" && origin.protocol !== "https:") ||
65
+ origin.username ||
66
+ origin.password ||
67
+ origin.search ||
68
+ origin.hash) {
69
+ return null;
70
+ }
71
+ return {
72
+ origin: origin.toString().replace(/\/$/, ""),
73
+ pid: parsed.pid,
74
+ managementToken: parsed.managementToken,
75
+ };
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ async function isHealthy(origin, signal) {
82
+ try {
83
+ const response = await fetch(`${origin}/health`, {
84
+ signal: signal
85
+ ? AbortSignal.any([signal, AbortSignal.timeout(PROBE_TIMEOUT_MS)])
86
+ : AbortSignal.timeout(PROBE_TIMEOUT_MS),
87
+ });
88
+ return response.ok;
89
+ }
90
+ catch {
91
+ signal?.throwIfAborted();
92
+ return false;
93
+ }
94
+ }
95
+ async function abortableDelay(ms, signal) {
96
+ signal?.throwIfAborted();
97
+ if (!signal) {
98
+ await new Promise((resolve) => setTimeout(resolve, ms));
99
+ return;
100
+ }
101
+ const activeSignal = signal;
102
+ await new Promise((resolve, reject) => {
103
+ const timer = setTimeout(done, ms);
104
+ const abort = () => {
105
+ clearTimeout(timer);
106
+ activeSignal.removeEventListener("abort", abort);
107
+ reject(activeSignal.reason);
108
+ };
109
+ function done() {
110
+ activeSignal.removeEventListener("abort", abort);
111
+ resolve();
112
+ }
113
+ activeSignal.addEventListener("abort", abort, { once: true });
114
+ });
115
+ }
116
+ function isProcessAlive(pid) {
117
+ try {
118
+ process.kill(pid, 0);
119
+ return true;
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ }
@@ -0,0 +1,16 @@
1
+ export interface ControlLink {
2
+ token: string;
3
+ path: string;
4
+ expiresAt: string;
5
+ }
6
+ /** Create a short-lived, one-use link into the generic session console. */
7
+ export declare function createControlLink(input: {
8
+ pluginId: string;
9
+ sessionId: string;
10
+ ttlMs?: number;
11
+ }): ControlLink;
12
+ /** Atomically consume a control link. Expired and replayed links are rejected. */
13
+ export declare function consumeControlLink(tokenInput: string): {
14
+ sessionId: string;
15
+ } | null;
16
+ export declare function pruneControlLinks(now?: string): number;
@@ -0,0 +1,53 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { db } from "./db.js";
3
+ const DEFAULT_TTL_MS = 5 * 60_000;
4
+ const MAX_TTL_MS = 15 * 60_000;
5
+ /** Create a short-lived, one-use link into the generic session console. */
6
+ export function createControlLink(input) {
7
+ const pluginId = required(input.pluginId, "pluginId");
8
+ const sessionId = required(input.sessionId, "sessionId");
9
+ const ttlMs = Math.min(MAX_TTL_MS, Math.max(1_000, input.ttlMs ?? DEFAULT_TTL_MS));
10
+ const token = randomBytes(32).toString("base64url");
11
+ const createdAt = new Date();
12
+ const expiresAt = new Date(createdAt.getTime() + ttlMs).toISOString();
13
+ db()
14
+ .prepare(`INSERT INTO control_links
15
+ (token, plugin_id, session_id, created_at, expires_at, used_at)
16
+ VALUES (?, ?, ?, ?, ?, NULL)`)
17
+ .run(token, pluginId, sessionId, createdAt.toISOString(), expiresAt);
18
+ return { token, path: `/control/link?token=${encodeURIComponent(token)}`, expiresAt };
19
+ }
20
+ /** Atomically consume a control link. Expired and replayed links are rejected. */
21
+ export function consumeControlLink(tokenInput) {
22
+ const token = tokenInput.trim();
23
+ if (!token || token.length > 512)
24
+ return null;
25
+ const now = new Date().toISOString();
26
+ const consume = db().transaction(() => {
27
+ const row = db()
28
+ .prepare(`SELECT session_id
29
+ FROM control_links
30
+ WHERE token = ? AND used_at IS NULL AND expires_at > ?`)
31
+ .get(token, now);
32
+ if (!row)
33
+ return null;
34
+ const updated = db()
35
+ .prepare("UPDATE control_links SET used_at = ? WHERE token = ? AND used_at IS NULL")
36
+ .run(now, token);
37
+ return updated.changes === 1 ? { sessionId: row.session_id } : null;
38
+ });
39
+ return consume.immediate();
40
+ }
41
+ export function pruneControlLinks(now = new Date().toISOString()) {
42
+ return db()
43
+ .prepare("DELETE FROM control_links WHERE expires_at <= ? OR used_at IS NOT NULL")
44
+ .run(now).changes;
45
+ }
46
+ function required(value, label) {
47
+ const normalized = value?.trim();
48
+ if (!normalized)
49
+ throw new Error(`${label} is required`);
50
+ if (normalized.length > 512)
51
+ throw new Error(`${label} is too long`);
52
+ return normalized;
53
+ }
@@ -0,0 +1,14 @@
1
+ export interface DaemonOwnerLease {
2
+ readonly pid: number;
3
+ readonly lockPath: string;
4
+ release(): Promise<void>;
5
+ }
6
+ /**
7
+ * Claim the one daemon owner slot for this RYNX_HOME.
8
+ *
9
+ * A dedicated SQLite exclusive transaction is the lease. It is backed by the
10
+ * operating system's file lock and is therefore released automatically when a
11
+ * process exits, without a PID-check/removal race between competing starters.
12
+ * The adjacent JSON file is diagnostics only and never decides ownership.
13
+ */
14
+ export declare function acquireDaemonOwner(env?: NodeJS.ProcessEnv): Promise<DaemonOwnerLease>;
@@ -0,0 +1,104 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { rynxHome } from "@rynx-ai/core";
5
+ import Database from "better-sqlite3";
6
+ const LOCK_FILE = "daemon-owner.sqlite";
7
+ const OWNER_FILE = "daemon-owner.json";
8
+ /**
9
+ * Claim the one daemon owner slot for this RYNX_HOME.
10
+ *
11
+ * A dedicated SQLite exclusive transaction is the lease. It is backed by the
12
+ * operating system's file lock and is therefore released automatically when a
13
+ * process exits, without a PID-check/removal race between competing starters.
14
+ * The adjacent JSON file is diagnostics only and never decides ownership.
15
+ */
16
+ export async function acquireDaemonOwner(env = process.env) {
17
+ const home = rynxHome(env);
18
+ const lockPath = path.join(home, LOCK_FILE);
19
+ const ownerFile = path.join(home, OWNER_FILE);
20
+ await mkdir(home, { recursive: true, mode: 0o700 });
21
+ const connection = new Database(lockPath, { timeout: 0 });
22
+ try {
23
+ connection.pragma("busy_timeout = 0");
24
+ connection.exec("BEGIN EXCLUSIVE");
25
+ // Force this transaction to hold a write lock even on a brand-new file.
26
+ connection.exec("CREATE TABLE IF NOT EXISTS daemon_owner_guard (id INTEGER PRIMARY KEY)");
27
+ }
28
+ catch (error) {
29
+ connection.close();
30
+ if (hasCode(error, "SQLITE_BUSY") || hasCode(error, "SQLITE_LOCKED")) {
31
+ throw await ownerConflictError(home, ownerFile);
32
+ }
33
+ throw error;
34
+ }
35
+ try {
36
+ await chmod(lockPath, 0o600);
37
+ }
38
+ catch {
39
+ // Best effort on filesystems without POSIX permissions.
40
+ }
41
+ const nonce = randomUUID();
42
+ try {
43
+ await publishOwner(ownerFile, nonce);
44
+ }
45
+ catch (error) {
46
+ rollbackAndClose(connection);
47
+ throw error;
48
+ }
49
+ let released = false;
50
+ return {
51
+ pid: process.pid,
52
+ lockPath,
53
+ async release() {
54
+ if (released)
55
+ return;
56
+ released = true;
57
+ await removeOwnedMetadata(ownerFile, nonce);
58
+ rollbackAndClose(connection);
59
+ },
60
+ };
61
+ }
62
+ async function publishOwner(ownerFile, nonce) {
63
+ const temporary = `${ownerFile}.tmp-${process.pid}-${nonce}`;
64
+ try {
65
+ await writeFile(temporary, `${JSON.stringify({ pid: process.pid, nonce, startedAt: new Date().toISOString() })}\n`, { encoding: "utf8", mode: 0o600 });
66
+ await rename(temporary, ownerFile);
67
+ }
68
+ finally {
69
+ await rm(temporary, { force: true });
70
+ }
71
+ }
72
+ async function removeOwnedMetadata(ownerFile, nonce) {
73
+ try {
74
+ const current = JSON.parse(await readFile(ownerFile, "utf8"));
75
+ if (current.nonce === nonce)
76
+ await rm(ownerFile, { force: true });
77
+ }
78
+ catch {
79
+ // Metadata is diagnostic only. Never let it prevent releasing the OS lock.
80
+ }
81
+ }
82
+ async function ownerConflictError(home, ownerFile) {
83
+ try {
84
+ const current = JSON.parse(await readFile(ownerFile, "utf8"));
85
+ if (typeof current.pid === "number" && Number.isSafeInteger(current.pid) && current.pid > 0) {
86
+ return new Error(`Rynx daemon already owns ${home} (pid ${current.pid})`);
87
+ }
88
+ }
89
+ catch {
90
+ // The SQLite lease is authoritative even when diagnostics are unavailable.
91
+ }
92
+ return new Error(`Rynx daemon already owns ${home}`);
93
+ }
94
+ function rollbackAndClose(connection) {
95
+ try {
96
+ connection.exec("ROLLBACK");
97
+ }
98
+ finally {
99
+ connection.close();
100
+ }
101
+ }
102
+ function hasCode(error, code) {
103
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
104
+ }
@@ -1,7 +1,17 @@
1
1
  import { type AppConfig } from "@rynx-ai/core";
2
- import { startServer } from "@rynx-ai/server";
2
+ import { startServer, startDirectRuntimeServer } from "@rynx-ai/server";
3
+ import { type DaemonOwnerLease } from "./daemon-owner.js";
4
+ import { type DirectRuntimeConfig } from "./direct-runtime-config.js";
3
5
  export interface StartRynxDaemonServerOptions {
4
6
  config?: AppConfig;
5
7
  warn?: (message: string) => void;
8
+ /** Test/deployment seam; failures roll back the already-listening server. */
9
+ writeEndpoint?: (config: AppConfig, managementToken: string) => Promise<void>;
10
+ /** Test seam for owner-lock failure injection. Production always uses the RYNX_HOME lease. */
11
+ acquireOwner?: () => Promise<DaemonOwnerLease>;
12
+ /** Test/deployment seam for the always-available Direct Runtime socket. */
13
+ loadDirectConfig?: () => Promise<DirectRuntimeConfig>;
14
+ /** Test seam for listener lifecycle and bind failures. */
15
+ startDirectServer?: typeof startDirectRuntimeServer;
6
16
  }
7
- export declare function startRynxDaemonServer({ config, warn, }?: StartRynxDaemonServerOptions): Promise<Awaited<ReturnType<typeof startServer>>>;
17
+ export declare function startRynxDaemonServer({ config, warn, writeEndpoint, acquireOwner, loadDirectConfig, startDirectServer, }?: StartRynxDaemonServerOptions): Promise<Awaited<ReturnType<typeof startServer>>>;