@borgee/agents-host 0.2.1 → 0.2.26

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 (78) hide show
  1. package/README.md +184 -21
  2. package/dist/agents-host-supervisor.d.ts +7 -5
  3. package/dist/agents-host-supervisor.js +24 -4
  4. package/dist/agents-host.d.ts +89 -15
  5. package/dist/agents-host.js +2099 -142
  6. package/dist/chat/chat-control-plane.d.ts +14 -3
  7. package/dist/chat/sdk-chat-control-plane.d.ts +16 -4
  8. package/dist/chat/sdk-chat-control-plane.js +69 -9
  9. package/dist/cli-args.d.ts +46 -5
  10. package/dist/cli-args.js +313 -32
  11. package/dist/cli.d.ts +9 -0
  12. package/dist/cli.js +112 -5
  13. package/dist/compatibility-gates.d.ts +35 -0
  14. package/dist/compatibility-gates.js +127 -0
  15. package/dist/config.d.ts +1 -0
  16. package/dist/config.js +23 -5
  17. package/dist/connections-state-store.d.ts +81 -0
  18. package/dist/connections-state-store.js +228 -0
  19. package/dist/context/injection.d.ts +109 -0
  20. package/dist/context/injection.js +350 -0
  21. package/dist/context/prompt.d.ts +4 -1
  22. package/dist/context/prompt.js +170 -1
  23. package/dist/context/turn-preparation.d.ts +9 -0
  24. package/dist/context/turn-preparation.js +106 -0
  25. package/dist/debug.d.ts +44 -0
  26. package/dist/debug.js +135 -0
  27. package/dist/gateway/localhost-gateway.d.ts +52 -0
  28. package/dist/gateway/localhost-gateway.js +857 -0
  29. package/dist/index.js +7 -5
  30. package/dist/local-config.d.ts +4 -1
  31. package/dist/local-config.js +24 -7
  32. package/dist/managed-daemon-log.d.ts +34 -0
  33. package/dist/managed-daemon-log.js +261 -0
  34. package/dist/managed-daemon.d.ts +220 -0
  35. package/dist/managed-daemon.js +1601 -0
  36. package/dist/policy/authorization-audit.d.ts +63 -0
  37. package/dist/policy/authorization-audit.js +94 -0
  38. package/dist/policy/copilot-permission.d.ts +15 -0
  39. package/dist/policy/copilot-permission.js +193 -0
  40. package/dist/policy/gateway-authorization.d.ts +42 -0
  41. package/dist/policy/gateway-authorization.js +162 -0
  42. package/dist/providers/awaiting-user.d.ts +12 -0
  43. package/dist/providers/awaiting-user.js +151 -0
  44. package/dist/providers/claude/adapter.d.ts +3 -1
  45. package/dist/providers/claude/adapter.js +8 -12
  46. package/dist/providers/claude/cli-client.d.ts +12 -5
  47. package/dist/providers/claude/cli-client.js +184 -37
  48. package/dist/providers/claude/session-store.d.ts +24 -0
  49. package/dist/providers/claude/session-store.js +65 -12
  50. package/dist/providers/codex/adapter.d.ts +11 -0
  51. package/dist/providers/codex/adapter.js +19 -0
  52. package/dist/providers/codex/cli-client.d.ts +103 -0
  53. package/dist/providers/codex/cli-client.js +1133 -0
  54. package/dist/providers/codex/project-doc.d.ts +3 -0
  55. package/dist/providers/codex/project-doc.js +66 -0
  56. package/dist/providers/codex/session-store.d.ts +38 -0
  57. package/dist/providers/codex/session-store.js +150 -0
  58. package/dist/providers/copilot/adapter.d.ts +3 -1
  59. package/dist/providers/copilot/adapter.js +8 -12
  60. package/dist/providers/copilot/cli-client.d.ts +20 -2
  61. package/dist/providers/copilot/cli-client.js +251 -71
  62. package/dist/providers/copilot/session-store.d.ts +24 -0
  63. package/dist/providers/copilot/session-store.js +65 -12
  64. package/dist/providers/create-provider.d.ts +11 -2
  65. package/dist/providers/create-provider.js +131 -12
  66. package/dist/run.d.ts +1 -0
  67. package/dist/run.js +5 -2
  68. package/dist/state-paths.d.ts +13 -1
  69. package/dist/state-paths.js +84 -3
  70. package/dist/task-thread-resolution.d.ts +10 -0
  71. package/dist/task-thread-resolution.js +48 -0
  72. package/dist/types.d.ts +174 -1
  73. package/dist/visible-mentions.d.ts +3 -0
  74. package/dist/visible-mentions.js +15 -0
  75. package/package.json +19 -17
  76. package/skills/borgee-agent/SKILL.md +33 -0
  77. package/skills/borgee-agent/borgee-agent.mjs +473 -0
  78. package/skills/borgee-agent/borgee-agent.py +409 -0
@@ -0,0 +1,1601 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import { spawn } from 'node:child_process';
4
+ import { createConnection, createServer } from 'node:net';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { AgentsHostSupervisor } from './agents-host-supervisor.js';
8
+ import { COMPATIBILITY_GATES_ENV, createManagedRuntimeSettingsFingerprint, INTERNAL_POLICY_MODE_ENV, INTERNAL_PROVIDER_IMPLEMENTATIONS_ENV, MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE, MANAGED_RUNTIME_SETTINGS_SCHEMA_VERSION, resolveManagedRuntimeSettingsSnapshot, normalizeInternalProviderImplementationOverrides, } from './compatibility-gates.js';
9
+ import { loadConfigFromEnv } from './config.js';
10
+ import { loadLocalConfigGenerateSpec, materializeLocalConfig, parseGenerateConfigSpec, resolveLocalConfigLayout, } from './local-config.js';
11
+ import { normalizeManagedRuntimeKey, resolveManagedBootstrapLockPath, resolveManagedDaemonLogPath, resolveManagedDaemonSocketPath, resolveManagedRuntimeRoot, resolveManagedRuntimeSettingsPath, } from './state-paths.js';
12
+ const MANAGED_ROOT_MODE = 0o700;
13
+ const MANAGED_CONFIG_MODE = 0o600;
14
+ const BOOTSTRAP_LOCK_WAIT_MS = 10_000;
15
+ const BOOTSTRAP_LOCK_RETRY_MS = 50;
16
+ const HOLDER_METADATA_FILENAME = '.holder.json';
17
+ const HOLDER_MAX_AGE_MS = 5 * 60_000;
18
+ // Managed daemon startup includes the full supervisor bootstrap for every
19
+ // persisted agent in the runtime root. On a cold local restart that can take
20
+ // materially longer than a bare socket bind, so the bootstrap/resume path needs
21
+ // a longer wait window than the original 10s probe to avoid false failures.
22
+ const DAEMON_READY_WAIT_MS = 60_000;
23
+ const DAEMON_RECONCILE_WAIT_MS = 60_000;
24
+ const DAEMON_READY_RETRY_MS = 100;
25
+ const CONTROL_SOCKET_CONNECT_TIMEOUT_MS = 1_000;
26
+ const DAEMON_EXIT_WAIT_MS = 2_000;
27
+ const DAEMON_FORCE_KILL_WAIT_MS = 1_000;
28
+ export const MANAGED_DAEMON_ERROR_CODES = [
29
+ 'MANAGED_BINDING_MISMATCH',
30
+ 'MANAGED_RUNTIME_NOT_PRESENT',
31
+ 'MANAGED_SPEC_INVALID',
32
+ 'MANAGED_BOOTSTRAP_LOCK_TIMEOUT',
33
+ 'MANAGED_DAEMON_START_FAILED',
34
+ 'MANAGED_DAEMON_NOT_READY',
35
+ 'MANAGED_RELOAD_FAILED',
36
+ 'MANAGED_CONVERGENCE_FAILED',
37
+ ];
38
+ const MANAGED_DAEMON_NOT_READY_MESSAGE = 'Managed daemon is still starting';
39
+ function isNodeErrorWithCode(error, code) {
40
+ return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
41
+ }
42
+ function isNotFoundError(error) {
43
+ return isNodeErrorWithCode(error, 'ENOENT');
44
+ }
45
+ function isAlreadyExistsError(error) {
46
+ return isNodeErrorWithCode(error, 'EEXIST');
47
+ }
48
+ function isSocketBusyError(error) {
49
+ return isNodeErrorWithCode(error, 'EADDRINUSE');
50
+ }
51
+ function delay(milliseconds) {
52
+ return new Promise((resolveDelay) => {
53
+ setTimeout(resolveDelay, milliseconds);
54
+ });
55
+ }
56
+ function toErrorMessage(error) {
57
+ return error instanceof Error ? error.message : String(error);
58
+ }
59
+ function isManagedDaemonErrorCode(value) {
60
+ return MANAGED_DAEMON_ERROR_CODES.includes(value);
61
+ }
62
+ function resolveLegacyManagedErrorCode(message, fallbackCode) {
63
+ return message === MANAGED_DAEMON_NOT_READY_MESSAGE ? 'MANAGED_DAEMON_NOT_READY' : fallbackCode;
64
+ }
65
+ class ManagedDaemonOperationError extends Error {
66
+ code;
67
+ constructor(code, message) {
68
+ super(message);
69
+ this.name = 'ManagedDaemonOperationError';
70
+ this.code = code;
71
+ }
72
+ }
73
+ function managedOperationError(code, message) {
74
+ return new ManagedDaemonOperationError(code, message);
75
+ }
76
+ function isManagedOperationError(error) {
77
+ return error instanceof ManagedDaemonOperationError;
78
+ }
79
+ function toManagedErrorPayload(error, fallbackCode = 'MANAGED_RELOAD_FAILED') {
80
+ if (isManagedOperationError(error)) {
81
+ return {
82
+ code: error.code,
83
+ message: error.message,
84
+ };
85
+ }
86
+ if (typeof error === 'string') {
87
+ return {
88
+ code: resolveLegacyManagedErrorCode(error, fallbackCode),
89
+ message: error,
90
+ };
91
+ }
92
+ if (typeof error === 'object' &&
93
+ error !== null &&
94
+ 'message' in error &&
95
+ typeof error.message === 'string') {
96
+ const code = 'code' in error && typeof error.code === 'string' && isManagedDaemonErrorCode(error.code)
97
+ ? error.code
98
+ : resolveLegacyManagedErrorCode(error.message, fallbackCode);
99
+ return {
100
+ code,
101
+ message: error.message,
102
+ };
103
+ }
104
+ return {
105
+ code: fallbackCode,
106
+ message: toErrorMessage(error),
107
+ };
108
+ }
109
+ function toManagedErrorResponse(error, fallbackCode = 'MANAGED_RELOAD_FAILED') {
110
+ return {
111
+ ok: false,
112
+ error: toManagedErrorPayload(error, fallbackCode),
113
+ };
114
+ }
115
+ function isManagedErrorResponse(response) {
116
+ return response.ok === false;
117
+ }
118
+ function normalizeManagedErrorResponse(response) {
119
+ return {
120
+ ok: false,
121
+ error: toManagedErrorPayload(response.error),
122
+ };
123
+ }
124
+ function getManagedErrorPayload(response) {
125
+ return normalizeManagedErrorResponse(response).error;
126
+ }
127
+ function isExpectedManagedFailure(error) {
128
+ return isManagedOperationError(error);
129
+ }
130
+ function combineManagedRollbackFailure(applyError, rollbackError) {
131
+ const original = toManagedErrorPayload(applyError, 'MANAGED_RELOAD_FAILED');
132
+ const rollback = toManagedErrorPayload(rollbackError, 'MANAGED_RELOAD_FAILED');
133
+ return managedOperationError(original.code, `${original.message}; rollback failed: ${rollback.message}`);
134
+ }
135
+ function toApplyManagedColdStartErrorResponse(error) {
136
+ if (isManagedOperationError(error)) {
137
+ return toManagedErrorResponse(error);
138
+ }
139
+ return toManagedErrorResponse(managedOperationError('MANAGED_RELOAD_FAILED', toErrorMessage(error)));
140
+ }
141
+ function resolveDesiredManagedRuntimeSettings(env = process.env) {
142
+ const snapshot = resolveManagedRuntimeSettingsSnapshot(env);
143
+ return {
144
+ snapshot,
145
+ fingerprint: createManagedRuntimeSettingsFingerprint(snapshot),
146
+ convergenceEnabled: snapshot.compatibilityGates.includes(MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE),
147
+ };
148
+ }
149
+ function normalizeManagedRuntimeSettingsSnapshot(snapshot) {
150
+ const compatibilityGates = [...new Set(snapshot.compatibilityGates.map((gate) => gate.trim()))]
151
+ .filter((gate) => gate.length > 0)
152
+ .sort((left, right) => left.localeCompare(right));
153
+ const providerImplementationOverrides = normalizeInternalProviderImplementationOverrides(snapshot.providerImplementationOverrides.join(','));
154
+ return {
155
+ schemaVersion: MANAGED_RUNTIME_SETTINGS_SCHEMA_VERSION,
156
+ compatibilityGates,
157
+ providerImplementationOverrides,
158
+ internalPolicyMode: snapshot.internalPolicyMode,
159
+ };
160
+ }
161
+ function parseManagedRuntimeSettingsSnapshotRecord(value, sourceLabel) {
162
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
163
+ throw new Error(`Invalid ${sourceLabel}: expected an object`);
164
+ }
165
+ const record = value;
166
+ if (record.schemaVersion !== MANAGED_RUNTIME_SETTINGS_SCHEMA_VERSION) {
167
+ throw new Error(`Invalid ${sourceLabel}: expected schemaVersion ${MANAGED_RUNTIME_SETTINGS_SCHEMA_VERSION}`);
168
+ }
169
+ if (!Array.isArray(record.compatibilityGates)
170
+ || !record.compatibilityGates.every((entry) => typeof entry === 'string')) {
171
+ throw new Error(`Invalid ${sourceLabel}: expected compatibilityGates to be a string array`);
172
+ }
173
+ if (!Array.isArray(record.providerImplementationOverrides)
174
+ || !record.providerImplementationOverrides.every((entry) => typeof entry === 'string')) {
175
+ throw new Error(`Invalid ${sourceLabel}: expected providerImplementationOverrides to be a string array`);
176
+ }
177
+ if (record.internalPolicyMode !== 'audit-only' && record.internalPolicyMode !== 'enforce') {
178
+ throw new Error(`Invalid ${sourceLabel}: expected internalPolicyMode to be "audit-only" or "enforce"`);
179
+ }
180
+ return normalizeManagedRuntimeSettingsSnapshot({
181
+ schemaVersion: MANAGED_RUNTIME_SETTINGS_SCHEMA_VERSION,
182
+ compatibilityGates: [...record.compatibilityGates],
183
+ providerImplementationOverrides: [...record.providerImplementationOverrides],
184
+ internalPolicyMode: record.internalPolicyMode,
185
+ });
186
+ }
187
+ async function loadManagedRuntimeSettingsSnapshot(rootPath) {
188
+ const settingsPath = resolveManagedRuntimeSettingsPath(rootPath);
189
+ try {
190
+ const raw = await fs.readFile(settingsPath, 'utf8');
191
+ return parseManagedRuntimeSettingsSnapshotRecord(JSON.parse(raw), `managed runtime settings snapshot ${settingsPath}`);
192
+ }
193
+ catch (error) {
194
+ if (isNotFoundError(error)) {
195
+ return undefined;
196
+ }
197
+ if (error instanceof SyntaxError) {
198
+ throw new Error(`Invalid managed runtime settings snapshot ${settingsPath}: ${error.message}`);
199
+ }
200
+ throw error;
201
+ }
202
+ }
203
+ async function writeManagedRuntimeSettingsSnapshot(rootPath, snapshot) {
204
+ await ensureDirectory(rootPath, MANAGED_ROOT_MODE);
205
+ const settingsPath = resolveManagedRuntimeSettingsPath(rootPath);
206
+ const normalizedSnapshot = normalizeManagedRuntimeSettingsSnapshot(snapshot);
207
+ await fs.writeFile(settingsPath, `${JSON.stringify(normalizedSnapshot)}\n`, {
208
+ encoding: 'utf8',
209
+ mode: MANAGED_CONFIG_MODE,
210
+ });
211
+ await fs.chmod(settingsPath, MANAGED_CONFIG_MODE);
212
+ }
213
+ async function restoreManagedRuntimeSettingsSnapshot(rootPath, snapshot) {
214
+ const settingsPath = resolveManagedRuntimeSettingsPath(rootPath);
215
+ if (!snapshot) {
216
+ await fs.rm(settingsPath, { force: true });
217
+ return;
218
+ }
219
+ await writeManagedRuntimeSettingsSnapshot(rootPath, snapshot);
220
+ }
221
+ function buildManagedRuntimeSettingsEnv(baseEnv, snapshot) {
222
+ return {
223
+ ...baseEnv,
224
+ [COMPATIBILITY_GATES_ENV]: snapshot.compatibilityGates.join(','),
225
+ [INTERNAL_PROVIDER_IMPLEMENTATIONS_ENV]: snapshot.providerImplementationOverrides.join(','),
226
+ [INTERNAL_POLICY_MODE_ENV]: snapshot.internalPolicyMode,
227
+ };
228
+ }
229
+ function getManagedRuntimeFingerprintMismatch(status, desiredSettings) {
230
+ if (!desiredSettings.convergenceEnabled) {
231
+ return status.managedRuntimeFingerprint != null;
232
+ }
233
+ return status.managedRuntimeFingerprint !== desiredSettings.fingerprint;
234
+ }
235
+ function cloneLocalAgentConfig(agent) {
236
+ return {
237
+ ...agent,
238
+ claudeArgs: agent.claudeArgs ? [...agent.claudeArgs] : undefined,
239
+ codexArgs: agent.codexArgs ? [...agent.codexArgs] : undefined,
240
+ copilotArgs: agent.copilotArgs ? [...agent.copilotArgs] : undefined,
241
+ };
242
+ }
243
+ function cloneLocalHostGenerateConfig(spec) {
244
+ return {
245
+ ...spec,
246
+ defaults: spec.defaults
247
+ ? {
248
+ ...spec.defaults,
249
+ claudeArgs: spec.defaults.claudeArgs ? [...spec.defaults.claudeArgs] : undefined,
250
+ codexArgs: spec.defaults.codexArgs ? [...spec.defaults.codexArgs] : undefined,
251
+ copilotArgs: spec.defaults.copilotArgs ? [...spec.defaults.copilotArgs] : undefined,
252
+ }
253
+ : undefined,
254
+ };
255
+ }
256
+ function cloneLocalConfigGenerateSpec(spec) {
257
+ return {
258
+ host: cloneLocalHostGenerateConfig(spec.host),
259
+ agents: spec.agents.map((agent) => cloneLocalAgentConfig(agent)),
260
+ };
261
+ }
262
+ function normalizeRequestedManagedBinding(serverUrl) {
263
+ return normalizeManagedRuntimeKey(serverUrl);
264
+ }
265
+ function countEnabledManagedAgents(agents) {
266
+ return agents.filter((agent) => agent.enabled !== false).length;
267
+ }
268
+ function listEnabledManagedAgentKeys(agents) {
269
+ return agents
270
+ .filter((agent) => agent.enabled !== false)
271
+ .map((agent) => agent.key)
272
+ .sort((left, right) => left.localeCompare(right));
273
+ }
274
+ function hashApiKey(apiKey) {
275
+ return createHash('sha256').update(apiKey).digest('hex').slice(0, 16);
276
+ }
277
+ export function resolveManagedAgentKey(apiKey) {
278
+ return `agent-${hashApiKey(apiKey)}`;
279
+ }
280
+ export function buildManagedLocalAgentConfig(options) {
281
+ if (!options.apiKey) {
282
+ throw new Error('Managed bootstrap requires an apiKey');
283
+ }
284
+ const processEnv = options.processEnv ?? process.env;
285
+ const effectiveEnv = {
286
+ ...processEnv,
287
+ ...options.env,
288
+ BORGEE_BASE_URL: options.serverUrl,
289
+ BORGEE_AGENT_API_KEY: options.apiKey,
290
+ };
291
+ const config = loadConfigFromEnv(effectiveEnv);
292
+ const rootPath = resolveManagedRuntimeRoot(config.borgeeBaseUrl, effectiveEnv);
293
+ const hostConfigPath = resolveLocalConfigLayout(rootPath).hostConfigPath;
294
+ return {
295
+ rootPath,
296
+ hostConfigPath,
297
+ borgeeBaseUrl: config.borgeeBaseUrl,
298
+ agent: {
299
+ key: resolveManagedAgentKey(config.agent.agentApiKey),
300
+ name: config.agent.agentName,
301
+ apiKey: config.agent.agentApiKey,
302
+ provider: config.agent.provider,
303
+ enabled: true,
304
+ claudeCommand: config.claudeCommand,
305
+ claudeArgs: [...config.claudeArgs],
306
+ codexCommand: config.codexCommand,
307
+ codexArgs: [...config.codexArgs],
308
+ copilotCommand: config.copilotCommand,
309
+ copilotArgs: [...config.copilotArgs],
310
+ copilotSessionTtlMinutes: config.copilotSessionTtlMinutes,
311
+ },
312
+ };
313
+ }
314
+ function resolveManagedRuntimeLayoutForServerUrl(serverUrl, processEnv = process.env) {
315
+ const rootPath = resolveManagedRuntimeRoot(serverUrl, processEnv);
316
+ return {
317
+ rootPath,
318
+ hostConfigPath: resolveLocalConfigLayout(rootPath).hostConfigPath,
319
+ borgeeBaseUrl: normalizeRequestedManagedBinding(serverUrl),
320
+ };
321
+ }
322
+ function createMissingManagedAgentError(serverUrl) {
323
+ return managedOperationError('MANAGED_RUNTIME_NOT_PRESENT', `No managed agents have been bootstrapped for ${normalizeManagedRuntimeKey(serverUrl)}; run agents-host start-managed <serverUrl> <apiKey> first`);
324
+ }
325
+ function assertManagedRuntimeBinding(runtimeServerUrl, requestedServerUrl) {
326
+ if (normalizeManagedRuntimeKey(runtimeServerUrl) !==
327
+ normalizeRequestedManagedBinding(requestedServerUrl)) {
328
+ throw managedOperationError('MANAGED_BINDING_MISMATCH', 'Managed runtime root is bound to a different Borgee base URL');
329
+ }
330
+ }
331
+ function assertManagedSpecBinding(spec, requestedServerUrl) {
332
+ assertManagedRuntimeBinding(spec.host.borgeeBaseUrl, requestedServerUrl);
333
+ }
334
+ async function loadManagedSpecOrUndefined(hostConfigPath, loadSpec) {
335
+ try {
336
+ return await loadSpec(hostConfigPath);
337
+ }
338
+ catch (error) {
339
+ if (isNotFoundError(error)) {
340
+ return undefined;
341
+ }
342
+ throw error;
343
+ }
344
+ }
345
+ function buildDescribeSpecResponse(spec) {
346
+ if (!spec) {
347
+ return {
348
+ ok: true,
349
+ type: 'describeSpec',
350
+ present: false,
351
+ };
352
+ }
353
+ return {
354
+ ok: true,
355
+ type: 'describeSpec',
356
+ present: true,
357
+ spec,
358
+ };
359
+ }
360
+ function ensureCurrentManagedSpec(spec) {
361
+ if (!spec) {
362
+ throw managedOperationError('MANAGED_RUNTIME_NOT_PRESENT', 'Managed runtime config is not present for this managed root');
363
+ }
364
+ return spec;
365
+ }
366
+ async function ensurePersistedManagedAgentsExist(serverUrl, hostConfigPath, loadSpec) {
367
+ let spec;
368
+ try {
369
+ spec = await loadSpec(hostConfigPath);
370
+ }
371
+ catch (error) {
372
+ if (isNotFoundError(error)) {
373
+ throw createMissingManagedAgentError(serverUrl);
374
+ }
375
+ throw error;
376
+ }
377
+ assertManagedRuntimeBinding(spec.host.borgeeBaseUrl, serverUrl);
378
+ if (spec.agents.length === 0) {
379
+ throw createMissingManagedAgentError(serverUrl);
380
+ }
381
+ }
382
+ async function ensureDirectory(path, mode) {
383
+ await fs.mkdir(path, { recursive: true, mode });
384
+ await fs.chmod(path, mode);
385
+ }
386
+ async function readHolderMetadata(holderPath) {
387
+ try {
388
+ const raw = await fs.readFile(`${holderPath}/${HOLDER_METADATA_FILENAME}`, 'utf8');
389
+ const parsed = JSON.parse(raw);
390
+ if (typeof parsed !== 'object' || parsed === null) {
391
+ return undefined;
392
+ }
393
+ const pid = 'pid' in parsed ? parsed.pid : undefined;
394
+ const createdAt = 'createdAt' in parsed ? parsed.createdAt : undefined;
395
+ const token = 'token' in parsed ? parsed.token : undefined;
396
+ if (typeof pid !== 'number' ||
397
+ !Number.isSafeInteger(pid) ||
398
+ pid <= 0 ||
399
+ typeof createdAt !== 'number' ||
400
+ !Number.isFinite(createdAt) ||
401
+ typeof token !== 'string' ||
402
+ token.length === 0) {
403
+ return undefined;
404
+ }
405
+ return { pid, createdAt, token };
406
+ }
407
+ catch (error) {
408
+ if (isNotFoundError(error)) {
409
+ return undefined;
410
+ }
411
+ throw error;
412
+ }
413
+ }
414
+ function isProcessAlive(pid) {
415
+ try {
416
+ process.kill(pid, 0);
417
+ return true;
418
+ }
419
+ catch (error) {
420
+ if (isNodeErrorWithCode(error, 'ESRCH')) {
421
+ return false;
422
+ }
423
+ return undefined;
424
+ }
425
+ }
426
+ async function isStaleHolderDirectory(holderPath, stats) {
427
+ const metadata = await readHolderMetadata(holderPath);
428
+ if (!metadata) {
429
+ return Date.now() - stats.mtimeMs >= HOLDER_MAX_AGE_MS;
430
+ }
431
+ const alive = isProcessAlive(metadata.pid);
432
+ return (alive === false || (alive === undefined && Date.now() - metadata.createdAt >= HOLDER_MAX_AGE_MS));
433
+ }
434
+ export async function acquireBootstrapLock(rootPath) {
435
+ await ensureDirectory(rootPath, MANAGED_ROOT_MODE);
436
+ const lockPath = resolveManagedBootstrapLockPath(rootPath);
437
+ const deadline = Date.now() + BOOTSTRAP_LOCK_WAIT_MS;
438
+ while (true) {
439
+ const metadata = {
440
+ pid: process.pid,
441
+ createdAt: Date.now(),
442
+ token: randomUUID(),
443
+ };
444
+ try {
445
+ await fs.mkdir(lockPath, { mode: MANAGED_ROOT_MODE });
446
+ try {
447
+ await fs.writeFile(`${lockPath}/${HOLDER_METADATA_FILENAME}`, `${JSON.stringify(metadata)}\n`, {
448
+ encoding: 'utf8',
449
+ mode: MANAGED_CONFIG_MODE,
450
+ });
451
+ await fs.chmod(lockPath, MANAGED_ROOT_MODE);
452
+ }
453
+ catch (error) {
454
+ await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);
455
+ throw error;
456
+ }
457
+ return async () => {
458
+ const currentMetadata = await readHolderMetadata(lockPath).catch(() => undefined);
459
+ if (currentMetadata?.token === metadata.token) {
460
+ await fs.rm(lockPath, { recursive: true, force: true });
461
+ }
462
+ };
463
+ }
464
+ catch (error) {
465
+ if (!isAlreadyExistsError(error)) {
466
+ throw error;
467
+ }
468
+ const stats = await fs.lstat(lockPath).catch((lstatError) => {
469
+ if (isNotFoundError(lstatError)) {
470
+ return undefined;
471
+ }
472
+ throw lstatError;
473
+ });
474
+ if (stats && (!stats.isDirectory() || stats.isSymbolicLink())) {
475
+ throw new Error(`Managed bootstrap lock has an unexpected value: ${lockPath}`);
476
+ }
477
+ if (stats && (await isStaleHolderDirectory(lockPath, stats))) {
478
+ await fs.rm(lockPath, { recursive: true, force: true });
479
+ continue;
480
+ }
481
+ if (Date.now() >= deadline) {
482
+ throw managedOperationError('MANAGED_BOOTSTRAP_LOCK_TIMEOUT', `Timed out waiting for managed bootstrap lock: ${lockPath}`);
483
+ }
484
+ await delay(BOOTSTRAP_LOCK_RETRY_MS);
485
+ }
486
+ }
487
+ }
488
+ async function canConnectToSocket(socketPath) {
489
+ return new Promise((resolveConnection) => {
490
+ const socket = createConnection(socketPath);
491
+ const finalize = (value) => {
492
+ socket.removeAllListeners();
493
+ socket.destroy();
494
+ resolveConnection(value);
495
+ };
496
+ socket.setTimeout(CONTROL_SOCKET_CONNECT_TIMEOUT_MS);
497
+ socket.once('connect', () => finalize(true));
498
+ socket.once('timeout', () => finalize(false));
499
+ socket.once('error', () => finalize(false));
500
+ });
501
+ }
502
+ async function removeStaleControlSocket(socketPath) {
503
+ const stats = await fs.lstat(socketPath).catch((error) => {
504
+ if (isNotFoundError(error)) {
505
+ return undefined;
506
+ }
507
+ throw error;
508
+ });
509
+ if (!stats) {
510
+ return false;
511
+ }
512
+ if (stats.isDirectory() || stats.isSymbolicLink()) {
513
+ throw new Error(`Managed control socket path has an unexpected value: ${socketPath}`);
514
+ }
515
+ if (await canConnectToSocket(socketPath)) {
516
+ return false;
517
+ }
518
+ if (!stats.isSocket() && !stats.isFile()) {
519
+ throw new Error(`Managed control socket path has an unexpected value: ${socketPath}`);
520
+ }
521
+ await fs.rm(socketPath, { force: true });
522
+ return true;
523
+ }
524
+ function parseDaemonResponse(raw) {
525
+ const parsed = JSON.parse(raw);
526
+ if (typeof parsed !== 'object' ||
527
+ parsed === null ||
528
+ typeof parsed.ok !== 'boolean') {
529
+ throw new Error('Invalid daemon response payload');
530
+ }
531
+ if (parsed.ok === false) {
532
+ return normalizeManagedErrorResponse(parsed);
533
+ }
534
+ return parsed;
535
+ }
536
+ function parseDaemonRequest(raw) {
537
+ let parsed;
538
+ try {
539
+ parsed = JSON.parse(raw);
540
+ }
541
+ catch (error) {
542
+ throw managedOperationError('MANAGED_SPEC_INVALID', `Invalid managed daemon request JSON: ${toErrorMessage(error)}`);
543
+ }
544
+ if (typeof parsed !== 'object' ||
545
+ parsed === null ||
546
+ typeof parsed.type !== 'string') {
547
+ throw managedOperationError('MANAGED_SPEC_INVALID', 'Invalid managed daemon request payload');
548
+ }
549
+ return parsed;
550
+ }
551
+ function isManagedDaemonConnectionError(error) {
552
+ return (isNodeErrorWithCode(error, 'ECONNREFUSED') ||
553
+ isNodeErrorWithCode(error, 'ENOENT') ||
554
+ isNodeErrorWithCode(error, 'ENOTSOCK') ||
555
+ isNodeErrorWithCode(error, 'ECONNRESET'));
556
+ }
557
+ export async function sendManagedDaemonRequest(rootPath, request) {
558
+ const socketPath = resolveManagedDaemonSocketPath(rootPath);
559
+ const requestTimeoutMs = request.type === 'upsertAgent' || request.type === 'applySpec'
560
+ ? DAEMON_RECONCILE_WAIT_MS
561
+ : DAEMON_READY_WAIT_MS;
562
+ return new Promise((resolveRequest, rejectRequest) => {
563
+ const socket = createConnection(socketPath);
564
+ let settled = false;
565
+ let responseBuffer = '';
566
+ const rejectOnce = (error) => {
567
+ if (settled) {
568
+ return;
569
+ }
570
+ settled = true;
571
+ socket.destroy();
572
+ rejectRequest(error);
573
+ };
574
+ const resolveOnce = (response) => {
575
+ if (settled) {
576
+ return;
577
+ }
578
+ settled = true;
579
+ socket.destroy();
580
+ resolveRequest(response);
581
+ };
582
+ socket.setEncoding('utf8');
583
+ socket.setTimeout(requestTimeoutMs);
584
+ socket.once('timeout', () => rejectOnce(new Error(`Timed out talking to managed daemon: ${socketPath}`)));
585
+ socket.once('error', rejectOnce);
586
+ socket.on('data', (chunk) => {
587
+ responseBuffer += chunk;
588
+ const newlineIndex = responseBuffer.indexOf('\n');
589
+ if (newlineIndex === -1) {
590
+ return;
591
+ }
592
+ try {
593
+ resolveOnce(parseDaemonResponse(responseBuffer.slice(0, newlineIndex)));
594
+ }
595
+ catch (error) {
596
+ rejectOnce(error);
597
+ }
598
+ });
599
+ socket.once('connect', () => {
600
+ socket.write(`${JSON.stringify(request)}\n`);
601
+ });
602
+ socket.once('end', () => {
603
+ if (!settled) {
604
+ rejectOnce(new Error('Managed daemon closed the control socket before sending a response'));
605
+ }
606
+ });
607
+ });
608
+ }
609
+ export async function waitForManagedDaemonReady(rootPath, sendRequest, child) {
610
+ const deadline = Date.now() + DAEMON_READY_WAIT_MS;
611
+ let childExited = false;
612
+ let childExitSummary = 'unknown exit';
613
+ const onExit = (code, signal) => {
614
+ childExited = true;
615
+ childExitSummary = `code=${code ?? 'null'} signal=${signal ?? 'null'}`;
616
+ };
617
+ child.once('exit', onExit);
618
+ try {
619
+ while (Date.now() < deadline) {
620
+ if (childExited) {
621
+ throw managedOperationError('MANAGED_DAEMON_START_FAILED', `Managed daemon exited before becoming ready (${childExitSummary})`);
622
+ }
623
+ try {
624
+ const response = await sendRequest(rootPath, { type: 'status' });
625
+ if (response.ok && response.type === 'status') {
626
+ return;
627
+ }
628
+ if (isManagedErrorResponse(response)) {
629
+ const managedError = getManagedErrorPayload(response);
630
+ if (managedError.code !== 'MANAGED_DAEMON_NOT_READY') {
631
+ throw managedOperationError(managedError.code, managedError.message);
632
+ }
633
+ }
634
+ }
635
+ catch (error) {
636
+ if (isManagedOperationError(error)) {
637
+ throw error;
638
+ }
639
+ // Retry until the deadline or an early child exit.
640
+ }
641
+ await delay(DAEMON_READY_RETRY_MS);
642
+ }
643
+ throw managedOperationError('MANAGED_DAEMON_NOT_READY', `Timed out waiting for managed daemon readiness: ${rootPath}`);
644
+ }
645
+ finally {
646
+ child.off('exit', onExit);
647
+ }
648
+ }
649
+ async function waitForExistingManagedDaemonReady(rootPath, sendRequest) {
650
+ const deadline = Date.now() + DAEMON_READY_WAIT_MS;
651
+ while (Date.now() < deadline) {
652
+ let response;
653
+ try {
654
+ response = await sendRequest(rootPath, { type: 'status' });
655
+ }
656
+ catch {
657
+ await delay(DAEMON_READY_RETRY_MS);
658
+ continue;
659
+ }
660
+ if (response.ok && response.type === 'status') {
661
+ return response;
662
+ }
663
+ if (isManagedErrorResponse(response)) {
664
+ const managedError = getManagedErrorPayload(response);
665
+ if (managedError.code !== 'MANAGED_DAEMON_NOT_READY') {
666
+ throw managedOperationError(managedError.code, managedError.message);
667
+ }
668
+ }
669
+ await delay(DAEMON_READY_RETRY_MS);
670
+ }
671
+ throw managedOperationError('MANAGED_DAEMON_NOT_READY', `Timed out waiting for managed daemon readiness: ${rootPath}`);
672
+ }
673
+ async function readReadyManagedDaemonStatus(rootPath, sendRequest) {
674
+ try {
675
+ const status = await sendRequest(rootPath, { type: 'status' });
676
+ if (status.ok && status.type === 'status') {
677
+ return status;
678
+ }
679
+ if (isManagedErrorResponse(status)) {
680
+ return waitForExistingManagedDaemonReady(rootPath, sendRequest);
681
+ }
682
+ return undefined;
683
+ }
684
+ catch (error) {
685
+ if (isManagedDaemonConnectionError(error)) {
686
+ return undefined;
687
+ }
688
+ throw error;
689
+ }
690
+ }
691
+ async function waitForManagedDaemonShutdown(rootPath, canConnect) {
692
+ const deadline = Date.now() + DAEMON_READY_WAIT_MS;
693
+ const socketPath = resolveManagedDaemonSocketPath(rootPath);
694
+ while (Date.now() < deadline) {
695
+ const listening = await canConnect(socketPath).catch((error) => {
696
+ if (isNodeErrorWithCode(error, 'ENOENT')) {
697
+ return false;
698
+ }
699
+ throw error;
700
+ });
701
+ if (!listening) {
702
+ return;
703
+ }
704
+ await delay(DAEMON_READY_RETRY_MS);
705
+ }
706
+ throw managedOperationError('MANAGED_RELOAD_FAILED', `Timed out waiting for managed daemon shutdown: ${rootPath}`);
707
+ }
708
+ async function shutdownManagedDaemon(rootPath, sendRequest, waitForShutdown) {
709
+ const response = await sendRequest(rootPath, { type: 'shutdown' });
710
+ if (!response.ok) {
711
+ const managedError = getManagedErrorPayload(response);
712
+ throw managedOperationError(managedError.code, managedError.message);
713
+ }
714
+ if (response.type !== 'shutdown') {
715
+ throw new Error('Unexpected managed daemon response for shutdown');
716
+ }
717
+ await waitForShutdown(rootPath);
718
+ }
719
+ export async function cleanupManagedRuntime(options, deps = {}) {
720
+ const sendRequest = deps.sendRequest ?? sendManagedDaemonRequest;
721
+ const removeRuntimeRoot = deps.removeRuntimeRoot ??
722
+ (async (rootPath) => {
723
+ await fs.rm(rootPath, { recursive: true, force: true });
724
+ });
725
+ const confirmSocket = deps.canConnectToSocket ?? canConnectToSocket;
726
+ const resolved = resolveManagedRuntimeLayoutForServerUrl(options.serverUrl, options.processEnv);
727
+ const waitForShutdown = deps.waitForShutdown ??
728
+ (async (rootPath) => {
729
+ await waitForManagedDaemonShutdown(rootPath, confirmSocket);
730
+ });
731
+ let requestedShutdown = false;
732
+ try {
733
+ const response = await sendRequest(resolved.rootPath, { type: 'shutdown' });
734
+ if (!response.ok) {
735
+ const managedError = getManagedErrorPayload(response);
736
+ throw managedOperationError(managedError.code, managedError.message);
737
+ }
738
+ if (response.type !== 'shutdown') {
739
+ throw new Error('Unexpected managed daemon response for shutdown');
740
+ }
741
+ requestedShutdown = true;
742
+ }
743
+ catch (error) {
744
+ if (!isManagedDaemonConnectionError(error)) {
745
+ if (isExpectedManagedFailure(error)) {
746
+ throw error;
747
+ }
748
+ throw error;
749
+ }
750
+ }
751
+ if (requestedShutdown) {
752
+ await waitForShutdown(resolved.rootPath);
753
+ }
754
+ if (options.purge === true) {
755
+ await removeRuntimeRoot(resolved.rootPath);
756
+ }
757
+ }
758
+ export async function openManagedDaemonLogFile(rootPath) {
759
+ await ensureDirectory(rootPath, MANAGED_ROOT_MODE);
760
+ const logPath = resolveManagedDaemonLogPath(rootPath);
761
+ const logFileHandle = await fs.open(logPath, 'a', MANAGED_CONFIG_MODE);
762
+ try {
763
+ await logFileHandle.chmod(MANAGED_CONFIG_MODE);
764
+ return {
765
+ logPath,
766
+ fd: logFileHandle.fd,
767
+ async close() {
768
+ await logFileHandle.close();
769
+ },
770
+ };
771
+ }
772
+ catch (error) {
773
+ await logFileHandle.close().catch(() => undefined);
774
+ throw error;
775
+ }
776
+ }
777
+ export async function spawnDetachedManagedProcess(options, deps = {}) {
778
+ const openLogFile = deps.openLogFile ?? openManagedDaemonLogFile;
779
+ const spawnProcess = deps.spawnProcess ?? spawn;
780
+ const logFile = await openLogFile(options.rootPath);
781
+ try {
782
+ return spawnProcess(options.command, options.args, {
783
+ detached: true,
784
+ env: options.env,
785
+ stdio: ['ignore', logFile.fd, logFile.fd],
786
+ });
787
+ }
788
+ finally {
789
+ await logFile.close().catch(() => undefined);
790
+ }
791
+ }
792
+ export async function spawnManagedDaemonProcess(options, deps = {}) {
793
+ const spawnDetachedProcess = deps.spawnDetachedProcess ?? spawnDetachedManagedProcess;
794
+ const managedRuntimeSettingsSnapshot = await loadManagedRuntimeSettingsSnapshot(options.rootPath);
795
+ if (!managedRuntimeSettingsSnapshot) {
796
+ throw new Error(`Managed runtime settings snapshot is missing for ${resolveManagedRuntimeSettingsPath(options.rootPath)}`);
797
+ }
798
+ const thisPath = fileURLToPath(import.meta.url);
799
+ const cliEntrypointPath = join(dirname(thisPath), thisPath.endsWith('.ts') ? 'cli.ts' : 'cli.js');
800
+ return spawnDetachedProcess({
801
+ rootPath: options.rootPath,
802
+ command: process.execPath,
803
+ env: {
804
+ ...buildManagedRuntimeSettingsEnv({
805
+ ...process.env,
806
+ ...(options.processEnv ?? {}),
807
+ }, managedRuntimeSettingsSnapshot),
808
+ AGENTS_HOST_MANAGED_DAEMON_LOG_PATH: resolveManagedDaemonLogPath(options.rootPath),
809
+ AGENTS_HOST_INTERNAL_TASK_WORKSPACE_ROOT_DIR: options.rootPath,
810
+ },
811
+ args: [
812
+ ...process.execArgv,
813
+ cliEntrypointPath,
814
+ 'daemon',
815
+ '--root',
816
+ options.rootPath,
817
+ ...(options.debug ? ['--debug'] : []),
818
+ ],
819
+ });
820
+ }
821
+ async function startManagedDaemonWithPersistedSettings(options) {
822
+ const previousSettingsSnapshot = await loadManagedRuntimeSettingsSnapshot(options.rootPath);
823
+ await writeManagedRuntimeSettingsSnapshot(options.rootPath, options.desiredSettings.snapshot);
824
+ let child;
825
+ try {
826
+ child = await options.spawnDaemonProcess({
827
+ rootPath: options.rootPath,
828
+ debug: options.debug,
829
+ processEnv: options.processEnv,
830
+ });
831
+ child.unref();
832
+ await options.waitForDaemon(options.rootPath, options.sendRequest, child);
833
+ }
834
+ catch (error) {
835
+ if (child) {
836
+ await terminateChildProcessAndWait(child);
837
+ }
838
+ await restoreManagedRuntimeSettingsSnapshot(options.rootPath, previousSettingsSnapshot);
839
+ throw error;
840
+ }
841
+ }
842
+ async function recycleManagedDaemonWithDesiredSettings(options) {
843
+ const previousSettingsSnapshot = await loadManagedRuntimeSettingsSnapshot(options.rootPath);
844
+ await shutdownManagedDaemon(options.rootPath, options.sendRequest, options.waitForShutdown);
845
+ try {
846
+ await startManagedDaemonWithPersistedSettings({
847
+ rootPath: options.rootPath,
848
+ debug: options.debug,
849
+ processEnv: options.processEnv,
850
+ desiredSettings: options.desiredSettings,
851
+ spawnDaemonProcess: options.spawnDaemonProcess,
852
+ waitForDaemon: options.waitForDaemon,
853
+ sendRequest: options.sendRequest,
854
+ });
855
+ }
856
+ catch (error) {
857
+ if (!previousSettingsSnapshot) {
858
+ throw error;
859
+ }
860
+ try {
861
+ await writeManagedRuntimeSettingsSnapshot(options.rootPath, previousSettingsSnapshot);
862
+ await startManagedDaemonWithPersistedSettings({
863
+ rootPath: options.rootPath,
864
+ debug: options.debug,
865
+ processEnv: options.processEnv,
866
+ desiredSettings: {
867
+ snapshot: previousSettingsSnapshot,
868
+ fingerprint: createManagedRuntimeSettingsFingerprint(previousSettingsSnapshot),
869
+ convergenceEnabled: previousSettingsSnapshot.compatibilityGates.includes(MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE),
870
+ },
871
+ spawnDaemonProcess: options.spawnDaemonProcess,
872
+ waitForDaemon: options.waitForDaemon,
873
+ sendRequest: options.sendRequest,
874
+ });
875
+ }
876
+ catch (rollbackError) {
877
+ throw combineManagedRollbackFailure(error, rollbackError);
878
+ }
879
+ throw error;
880
+ }
881
+ }
882
+ export async function bootstrapManagedDaemonStart(options, deps = {}) {
883
+ const effectiveProcessEnv = {
884
+ ...(options.processEnv ?? process.env),
885
+ ...options.env,
886
+ };
887
+ const loadSpec = deps.loadSpec
888
+ ?? ((hostConfigPath) => loadLocalConfigGenerateSpec(hostConfigPath, { env: effectiveProcessEnv }));
889
+ const materialize = deps.materialize ?? materializeLocalConfig;
890
+ const sendRequest = deps.sendRequest ?? sendManagedDaemonRequest;
891
+ const spawnDaemonProcess = deps.spawnDaemon ?? spawnManagedDaemonProcess;
892
+ const waitForDaemon = deps.waitForDaemonReady ?? waitForManagedDaemonReady;
893
+ const acquireLock = deps.acquireBootstrapLock ?? acquireBootstrapLock;
894
+ const waitForShutdown = deps.waitForShutdown
895
+ ?? (async (rootPath) => {
896
+ await waitForManagedDaemonShutdown(rootPath, canConnectToSocket);
897
+ });
898
+ const desiredSettings = resolveDesiredManagedRuntimeSettings(effectiveProcessEnv);
899
+ const resolvedManagedAgent = options.apiKey === undefined ? null : buildManagedLocalAgentConfig(options);
900
+ const resolved = resolvedManagedAgent ??
901
+ resolveManagedRuntimeLayoutForServerUrl(options.serverUrl, options.processEnv);
902
+ const releaseBootstrapLock = await acquireLock(resolved.rootPath);
903
+ try {
904
+ let statusResponse = await readReadyManagedDaemonStatus(resolved.rootPath, sendRequest);
905
+ if (statusResponse && getManagedRuntimeFingerprintMismatch(statusResponse, desiredSettings)) {
906
+ await recycleManagedDaemonWithDesiredSettings({
907
+ rootPath: resolved.rootPath,
908
+ debug: options.debug === true,
909
+ processEnv: effectiveProcessEnv,
910
+ desiredSettings,
911
+ sendRequest,
912
+ waitForShutdown,
913
+ spawnDaemonProcess,
914
+ waitForDaemon,
915
+ });
916
+ statusResponse = await readReadyManagedDaemonStatus(resolved.rootPath, sendRequest);
917
+ }
918
+ if (!statusResponse) {
919
+ if (options.apiKey === undefined) {
920
+ await ensurePersistedManagedAgentsExist(options.serverUrl, resolved.hostConfigPath, loadSpec);
921
+ }
922
+ else {
923
+ try {
924
+ await loadSpec(resolved.hostConfigPath);
925
+ }
926
+ catch (error) {
927
+ if (isNotFoundError(error)) {
928
+ await materialize(resolved.rootPath, {
929
+ host: { borgeeBaseUrl: resolved.borgeeBaseUrl },
930
+ agents: [],
931
+ });
932
+ }
933
+ else {
934
+ throw error;
935
+ }
936
+ }
937
+ }
938
+ await startManagedDaemonWithPersistedSettings({
939
+ rootPath: resolved.rootPath,
940
+ debug: options.debug === true,
941
+ processEnv: effectiveProcessEnv,
942
+ desiredSettings,
943
+ spawnDaemonProcess,
944
+ waitForDaemon,
945
+ sendRequest,
946
+ });
947
+ statusResponse = undefined;
948
+ }
949
+ if (options.apiKey === undefined) {
950
+ if (!statusResponse) {
951
+ await ensurePersistedManagedAgentsExist(options.serverUrl, resolved.hostConfigPath, loadSpec);
952
+ }
953
+ else {
954
+ assertManagedRuntimeBinding(statusResponse.borgeeBaseUrl, options.serverUrl);
955
+ if (statusResponse.agentCount === 0) {
956
+ throw createMissingManagedAgentError(options.serverUrl);
957
+ }
958
+ }
959
+ return;
960
+ }
961
+ if (!resolvedManagedAgent) {
962
+ return;
963
+ }
964
+ const upsertResponse = await sendRequest(resolved.rootPath, {
965
+ type: 'upsertAgent',
966
+ borgeeBaseUrl: resolvedManagedAgent.borgeeBaseUrl,
967
+ agent: resolvedManagedAgent.agent,
968
+ });
969
+ if (!upsertResponse.ok) {
970
+ const managedError = getManagedErrorPayload(upsertResponse);
971
+ throw managedOperationError(managedError.code, managedError.message);
972
+ }
973
+ }
974
+ finally {
975
+ await releaseBootstrapLock();
976
+ }
977
+ }
978
+ function buildApplySpecSuccessResponse(hostConfigPath, logPath, spec) {
979
+ return {
980
+ ok: true,
981
+ type: 'applySpec',
982
+ hostConfigPath,
983
+ ...(logPath ? { logPath } : {}),
984
+ agentCount: spec.agents.length,
985
+ enabledAgentCount: countEnabledManagedAgents(spec.agents),
986
+ };
987
+ }
988
+ function assertManagedSupervisorConvergence(supervisor, previousSpec, nextSpec) {
989
+ const nextEnabledKeys = new Set(listEnabledManagedAgentKeys(nextSpec.agents));
990
+ const missingEnabledKeys = [...nextEnabledKeys]
991
+ .filter((key) => !supervisor.hasActiveAgent(key))
992
+ .sort();
993
+ const staleDisabledOrDeletedKeys = listEnabledManagedAgentKeys(previousSpec.agents)
994
+ .filter((key) => !nextEnabledKeys.has(key) && supervisor.hasActiveAgent(key))
995
+ .sort();
996
+ if (missingEnabledKeys.length === 0 && staleDisabledOrDeletedKeys.length === 0) {
997
+ return;
998
+ }
999
+ const parts = [];
1000
+ if (missingEnabledKeys.length > 0) {
1001
+ parts.push(`missing active enabled agents: ${missingEnabledKeys.join(', ')}`);
1002
+ }
1003
+ if (staleDisabledOrDeletedKeys.length > 0) {
1004
+ parts.push(`stale active deleted/disabled agents: ${staleDisabledOrDeletedKeys.join(', ')}`);
1005
+ }
1006
+ throw managedOperationError('MANAGED_CONVERGENCE_FAILED', `Managed runtime did not converge exactly after reload (${parts.join('; ')})`);
1007
+ }
1008
+ async function clearManagedRuntime(rootPath) {
1009
+ const normalizedRootPath = resolve(rootPath);
1010
+ const lockPath = resolveManagedBootstrapLockPath(normalizedRootPath);
1011
+ const entries = await fs.readdir(normalizedRootPath, { withFileTypes: true }).catch((error) => {
1012
+ if (isNotFoundError(error)) {
1013
+ return [];
1014
+ }
1015
+ throw error;
1016
+ });
1017
+ await Promise.all(entries.map(async (entry) => {
1018
+ const entryPath = join(normalizedRootPath, entry.name);
1019
+ if (entryPath === lockPath) {
1020
+ return;
1021
+ }
1022
+ await fs.rm(entryPath, { recursive: true, force: true });
1023
+ }));
1024
+ }
1025
+ function sendSignalToChildProcess(child, signal) {
1026
+ if (typeof child.pid !== 'number' || child.pid <= 0) {
1027
+ return false;
1028
+ }
1029
+ try {
1030
+ process.kill(child.pid, signal);
1031
+ return true;
1032
+ }
1033
+ catch (error) {
1034
+ if (!isNodeErrorWithCode(error, 'ESRCH')) {
1035
+ throw error;
1036
+ }
1037
+ return false;
1038
+ }
1039
+ }
1040
+ function hasChildExited(child) {
1041
+ return child.exitCode != null || child.signalCode != null;
1042
+ }
1043
+ async function waitForChildExit(child, timeoutMs) {
1044
+ if (hasChildExited(child)) {
1045
+ return true;
1046
+ }
1047
+ return new Promise((resolveWait) => {
1048
+ const onExit = () => {
1049
+ cleanup();
1050
+ resolveWait(true);
1051
+ };
1052
+ const timer = setTimeout(() => {
1053
+ cleanup();
1054
+ resolveWait(false);
1055
+ }, timeoutMs);
1056
+ const cleanup = () => {
1057
+ clearTimeout(timer);
1058
+ child.off('exit', onExit);
1059
+ };
1060
+ child.once('exit', onExit);
1061
+ if (hasChildExited(child)) {
1062
+ cleanup();
1063
+ resolveWait(true);
1064
+ }
1065
+ });
1066
+ }
1067
+ async function terminateChildProcessAndWait(child) {
1068
+ if (hasChildExited(child)) {
1069
+ return;
1070
+ }
1071
+ if (!sendSignalToChildProcess(child, 'SIGTERM')) {
1072
+ return;
1073
+ }
1074
+ if (await waitForChildExit(child, DAEMON_EXIT_WAIT_MS)) {
1075
+ return;
1076
+ }
1077
+ if (!sendSignalToChildProcess(child, 'SIGKILL')) {
1078
+ return;
1079
+ }
1080
+ await waitForChildExit(child, DAEMON_FORCE_KILL_WAIT_MS);
1081
+ }
1082
+ async function rollbackManagedApplyFailure(rootPath, previousSpec, materialize, clearRuntimeImpl, applyError) {
1083
+ const original = toManagedErrorPayload(applyError, 'MANAGED_RELOAD_FAILED');
1084
+ try {
1085
+ if (previousSpec) {
1086
+ await materialize(rootPath, cloneLocalConfigGenerateSpec(previousSpec));
1087
+ }
1088
+ else {
1089
+ await clearRuntimeImpl(rootPath);
1090
+ }
1091
+ }
1092
+ catch (rollbackError) {
1093
+ return toManagedErrorResponse(combineManagedRollbackFailure(applyError, rollbackError));
1094
+ }
1095
+ return {
1096
+ ok: false,
1097
+ error: original,
1098
+ };
1099
+ }
1100
+ function assertManagedFullSetSpecHasAgents(spec, sourceLabel) {
1101
+ if (spec.agents.length === 0) {
1102
+ throw managedOperationError('MANAGED_SPEC_INVALID', `Invalid managed spec in ${sourceLabel}: full-set managed specs must include at least one agent`);
1103
+ }
1104
+ }
1105
+ function validateApplyManagedInput(spec, serverUrl, env = process.env) {
1106
+ const parsed = parseGenerateConfigSpec(spec, '<apply-managed>', env);
1107
+ assertManagedSpecBinding(parsed, serverUrl);
1108
+ assertManagedFullSetSpecHasAgents(parsed, '<apply-managed>');
1109
+ return parsed;
1110
+ }
1111
+ function validateDaemonApplySpecInput(spec, serverUrl, env = process.env) {
1112
+ try {
1113
+ const parsed = parseGenerateConfigSpec(spec, '<managed-daemon applySpec>', env);
1114
+ assertManagedSpecBinding(parsed, serverUrl);
1115
+ assertManagedFullSetSpecHasAgents(parsed, '<managed-daemon applySpec>');
1116
+ return parsed;
1117
+ }
1118
+ catch (error) {
1119
+ if (isManagedOperationError(error)) {
1120
+ throw error;
1121
+ }
1122
+ throw managedOperationError('MANAGED_SPEC_INVALID', toErrorMessage(error));
1123
+ }
1124
+ }
1125
+ function validateDaemonUpsertAgentInput(currentSpec, serverUrl, agent, env = process.env) {
1126
+ try {
1127
+ assertManagedRuntimeBinding(currentSpec.host.borgeeBaseUrl, serverUrl);
1128
+ return parseGenerateConfigSpec({
1129
+ host: cloneLocalHostGenerateConfig(currentSpec.host),
1130
+ agents: [agent],
1131
+ }, '<managed-daemon upsertAgent>', env).agents[0];
1132
+ }
1133
+ catch (error) {
1134
+ if (isManagedOperationError(error)) {
1135
+ throw error;
1136
+ }
1137
+ throw managedOperationError('MANAGED_SPEC_INVALID', toErrorMessage(error));
1138
+ }
1139
+ }
1140
+ export async function describeManagedSpec(options, deps = {}) {
1141
+ const effectiveProcessEnv = options.processEnv ?? process.env;
1142
+ const loadSpec = deps.loadSpec
1143
+ ?? ((hostConfigPath) => loadLocalConfigGenerateSpec(hostConfigPath, { env: effectiveProcessEnv }));
1144
+ const sendRequest = deps.sendRequest ?? sendManagedDaemonRequest;
1145
+ const resolved = resolveManagedRuntimeLayoutForServerUrl(options.serverUrl, options.processEnv);
1146
+ try {
1147
+ const response = await sendRequest(resolved.rootPath, {
1148
+ type: 'describeSpec',
1149
+ borgeeBaseUrl: resolved.borgeeBaseUrl,
1150
+ });
1151
+ if (response.ok && response.type === 'describeSpec') {
1152
+ return response.present && response.spec
1153
+ ? { ok: true, present: true, spec: response.spec }
1154
+ : { ok: true, present: false };
1155
+ }
1156
+ if (isManagedErrorResponse(response)) {
1157
+ return normalizeManagedErrorResponse(response);
1158
+ }
1159
+ throw new Error('Unexpected managed daemon response for describeSpec');
1160
+ }
1161
+ catch (error) {
1162
+ if (!isManagedDaemonConnectionError(error)) {
1163
+ if (isExpectedManagedFailure(error)) {
1164
+ return toManagedErrorResponse(error);
1165
+ }
1166
+ throw error;
1167
+ }
1168
+ }
1169
+ try {
1170
+ const persistedSpec = await loadManagedSpecOrUndefined(resolved.hostConfigPath, loadSpec);
1171
+ if (!persistedSpec) {
1172
+ return { ok: true, present: false };
1173
+ }
1174
+ assertManagedSpecBinding(persistedSpec, options.serverUrl);
1175
+ return {
1176
+ ok: true,
1177
+ present: true,
1178
+ spec: persistedSpec,
1179
+ };
1180
+ }
1181
+ catch (error) {
1182
+ if (isExpectedManagedFailure(error)) {
1183
+ return toManagedErrorResponse(error);
1184
+ }
1185
+ throw error;
1186
+ }
1187
+ }
1188
+ async function applySpecToRunningManagedDaemon(rootPath, borgeeBaseUrl, spec, sendRequest) {
1189
+ const response = await sendRequest(rootPath, {
1190
+ type: 'applySpec',
1191
+ borgeeBaseUrl,
1192
+ spec: cloneLocalConfigGenerateSpec(spec),
1193
+ });
1194
+ if (!response.ok) {
1195
+ return normalizeManagedErrorResponse(response);
1196
+ }
1197
+ if (response.type !== 'applySpec') {
1198
+ throw new Error('Unexpected managed daemon response for applySpec');
1199
+ }
1200
+ return {
1201
+ ok: true,
1202
+ daemon: 'reused',
1203
+ agentCount: response.agentCount,
1204
+ enabledAgentCount: response.enabledAgentCount,
1205
+ ...(response.logPath ? { logPath: response.logPath } : {}),
1206
+ };
1207
+ }
1208
+ export async function applyManagedSpec(options, deps = {}) {
1209
+ const effectiveProcessEnv = options.processEnv ?? process.env;
1210
+ const loadSpec = deps.loadSpec
1211
+ ?? ((hostConfigPath) => loadLocalConfigGenerateSpec(hostConfigPath, { env: effectiveProcessEnv }));
1212
+ const materialize = deps.materialize
1213
+ ?? ((rootPath, spec) => materializeLocalConfig(rootPath, spec, { env: effectiveProcessEnv }));
1214
+ const sendRequest = deps.sendRequest ?? sendManagedDaemonRequest;
1215
+ const spawnDaemonProcess = deps.spawnDaemon ?? spawnManagedDaemonProcess;
1216
+ const waitForDaemon = deps.waitForDaemonReady ?? waitForManagedDaemonReady;
1217
+ const acquireLock = deps.acquireBootstrapLock ?? acquireBootstrapLock;
1218
+ const clearManagedRuntimeImpl = deps.clearManagedRuntime ?? clearManagedRuntime;
1219
+ const resolved = resolveManagedRuntimeLayoutForServerUrl(options.serverUrl, options.processEnv);
1220
+ let spec;
1221
+ try {
1222
+ spec = validateApplyManagedInput(options.spec, options.serverUrl, effectiveProcessEnv);
1223
+ }
1224
+ catch (error) {
1225
+ if (isManagedOperationError(error)) {
1226
+ return toManagedErrorResponse(error);
1227
+ }
1228
+ return toManagedErrorResponse(managedOperationError('MANAGED_SPEC_INVALID', `Invalid managed spec: ${toErrorMessage(error)}`));
1229
+ }
1230
+ const desiredSettings = resolveDesiredManagedRuntimeSettings(effectiveProcessEnv);
1231
+ const waitForShutdown = deps.waitForShutdown
1232
+ ?? (async (rootPath) => {
1233
+ await waitForManagedDaemonShutdown(rootPath, canConnectToSocket);
1234
+ });
1235
+ let releaseBootstrapLock;
1236
+ try {
1237
+ releaseBootstrapLock = await acquireLock(resolved.rootPath);
1238
+ }
1239
+ catch (error) {
1240
+ return toApplyManagedColdStartErrorResponse(error);
1241
+ }
1242
+ try {
1243
+ let statusResponse;
1244
+ try {
1245
+ statusResponse = await readReadyManagedDaemonStatus(resolved.rootPath, sendRequest);
1246
+ }
1247
+ catch (error) {
1248
+ if (isExpectedManagedFailure(error)) {
1249
+ return toManagedErrorResponse(error);
1250
+ }
1251
+ throw error;
1252
+ }
1253
+ if (statusResponse && getManagedRuntimeFingerprintMismatch(statusResponse, desiredSettings)) {
1254
+ try {
1255
+ await recycleManagedDaemonWithDesiredSettings({
1256
+ rootPath: resolved.rootPath,
1257
+ debug: options.debug === true,
1258
+ processEnv: effectiveProcessEnv,
1259
+ desiredSettings,
1260
+ sendRequest,
1261
+ waitForShutdown,
1262
+ spawnDaemonProcess,
1263
+ waitForDaemon,
1264
+ });
1265
+ }
1266
+ catch (error) {
1267
+ return toManagedErrorResponse(error);
1268
+ }
1269
+ statusResponse = await readReadyManagedDaemonStatus(resolved.rootPath, sendRequest);
1270
+ }
1271
+ if (statusResponse) {
1272
+ return applySpecToRunningManagedDaemon(resolved.rootPath, resolved.borgeeBaseUrl, spec, sendRequest);
1273
+ }
1274
+ let previousSpec;
1275
+ try {
1276
+ previousSpec = await loadManagedSpecOrUndefined(resolved.hostConfigPath, loadSpec);
1277
+ if (previousSpec) {
1278
+ assertManagedSpecBinding(previousSpec, options.serverUrl);
1279
+ }
1280
+ }
1281
+ catch (error) {
1282
+ return toApplyManagedColdStartErrorResponse(error);
1283
+ }
1284
+ try {
1285
+ await materialize(resolved.rootPath, cloneLocalConfigGenerateSpec(spec));
1286
+ }
1287
+ catch (error) {
1288
+ return rollbackManagedApplyFailure(resolved.rootPath, previousSpec, materialize, clearManagedRuntimeImpl, error);
1289
+ }
1290
+ try {
1291
+ await startManagedDaemonWithPersistedSettings({
1292
+ rootPath: resolved.rootPath,
1293
+ debug: options.debug === true,
1294
+ processEnv: effectiveProcessEnv,
1295
+ desiredSettings,
1296
+ spawnDaemonProcess,
1297
+ waitForDaemon,
1298
+ sendRequest,
1299
+ });
1300
+ const status = await sendRequest(resolved.rootPath, { type: 'status' });
1301
+ if (!status.ok) {
1302
+ const managedError = getManagedErrorPayload(status);
1303
+ return rollbackManagedApplyFailure(resolved.rootPath, previousSpec, materialize, clearManagedRuntimeImpl, managedOperationError(managedError.code, managedError.message));
1304
+ }
1305
+ if (status.type !== 'status') {
1306
+ throw new Error('Unexpected managed daemon response for status');
1307
+ }
1308
+ return {
1309
+ ok: true,
1310
+ daemon: 'started',
1311
+ agentCount: status.agentCount,
1312
+ enabledAgentCount: status.enabledAgentCount,
1313
+ ...(status.logPath ? { logPath: status.logPath } : {}),
1314
+ };
1315
+ }
1316
+ catch (error) {
1317
+ return rollbackManagedApplyFailure(resolved.rootPath, previousSpec, materialize, clearManagedRuntimeImpl, error);
1318
+ }
1319
+ }
1320
+ finally {
1321
+ await releaseBootstrapLock?.();
1322
+ }
1323
+ }
1324
+ export class ManagedAgentsHostDaemon {
1325
+ rootPath;
1326
+ hostConfigPath;
1327
+ socketPath;
1328
+ createSupervisor;
1329
+ supervisor;
1330
+ loadSpec;
1331
+ materialize;
1332
+ managedRuntimeFingerprint;
1333
+ logger;
1334
+ logPath;
1335
+ server;
1336
+ requestQueue = Promise.resolve();
1337
+ started = false;
1338
+ ready = false;
1339
+ ownsSocket = false;
1340
+ shutdownRequested = false;
1341
+ ownedSocketInode = null;
1342
+ constructor(rootPath, debug = false, deps = {}) {
1343
+ const layout = resolveLocalConfigLayout(rootPath);
1344
+ this.rootPath = layout.root;
1345
+ this.hostConfigPath = layout.hostConfigPath;
1346
+ this.socketPath = resolveManagedDaemonSocketPath(this.rootPath);
1347
+ this.createSupervisor =
1348
+ deps.createSupervisor ??
1349
+ ((configPath, daemonDebug) => new AgentsHostSupervisor(configPath, { debug: daemonDebug }));
1350
+ this.loadSpec = deps.loadSpec ?? loadLocalConfigGenerateSpec;
1351
+ this.materialize = deps.materialize ?? materializeLocalConfig;
1352
+ const managedRuntimeSettings = resolveDesiredManagedRuntimeSettings(process.env);
1353
+ this.managedRuntimeFingerprint = managedRuntimeSettings.convergenceEnabled
1354
+ ? managedRuntimeSettings.fingerprint
1355
+ : null;
1356
+ this.logger = deps.logger ?? console;
1357
+ this.logPath = deps.logPath ?? null;
1358
+ this.server = createServer((socket) => {
1359
+ void this.handleSocket(socket);
1360
+ });
1361
+ this.server.on('error', (error) => {
1362
+ this.logger.error('[agents-host] managed daemon control socket error', {
1363
+ rootPath: this.rootPath,
1364
+ error,
1365
+ });
1366
+ });
1367
+ this.supervisor = this.createSupervisor(this.hostConfigPath, debug);
1368
+ }
1369
+ async start() {
1370
+ if (this.started) {
1371
+ return;
1372
+ }
1373
+ this.started = true;
1374
+ this.ready = false;
1375
+ await ensureDirectory(this.rootPath, MANAGED_ROOT_MODE);
1376
+ try {
1377
+ await this.listen();
1378
+ await this.supervisor.start();
1379
+ this.ready = true;
1380
+ this.logger.log('[agents-host] managed daemon ready', {
1381
+ rootPath: this.rootPath,
1382
+ hostConfigPath: this.hostConfigPath,
1383
+ socketPath: this.socketPath,
1384
+ });
1385
+ }
1386
+ catch (error) {
1387
+ this.started = false;
1388
+ this.ready = false;
1389
+ await this.stop().catch(() => undefined);
1390
+ throw error;
1391
+ }
1392
+ }
1393
+ async stop() {
1394
+ this.ready = false;
1395
+ try {
1396
+ await this.supervisor.stop();
1397
+ }
1398
+ finally {
1399
+ await new Promise((resolveStop) => {
1400
+ if (!this.server.listening) {
1401
+ resolveStop();
1402
+ return;
1403
+ }
1404
+ this.server.close(() => resolveStop());
1405
+ });
1406
+ if (this.ownsSocket) {
1407
+ const socketStats = await fs.lstat(this.socketPath).catch(() => undefined);
1408
+ if (socketStats && socketStats.ino === this.ownedSocketInode) {
1409
+ await fs.rm(this.socketPath, { force: true }).catch(() => undefined);
1410
+ }
1411
+ this.ownsSocket = false;
1412
+ this.ownedSocketInode = null;
1413
+ }
1414
+ this.started = false;
1415
+ this.shutdownRequested = false;
1416
+ }
1417
+ }
1418
+ async listen() {
1419
+ for (let attempt = 0; attempt < 2; attempt++) {
1420
+ try {
1421
+ await new Promise((resolveListen, rejectListen) => {
1422
+ const onError = (error) => {
1423
+ this.server.off('listening', onListening);
1424
+ rejectListen(error);
1425
+ };
1426
+ const onListening = () => {
1427
+ this.server.off('error', onError);
1428
+ resolveListen();
1429
+ };
1430
+ this.server.once('error', onError);
1431
+ this.server.once('listening', onListening);
1432
+ this.server.listen(this.socketPath);
1433
+ });
1434
+ await fs.chmod(this.socketPath, MANAGED_CONFIG_MODE);
1435
+ this.ownedSocketInode = (await fs.lstat(this.socketPath)).ino;
1436
+ this.ownsSocket = true;
1437
+ return;
1438
+ }
1439
+ catch (error) {
1440
+ if (!isSocketBusyError(error) || !(await removeStaleControlSocket(this.socketPath))) {
1441
+ throw error;
1442
+ }
1443
+ }
1444
+ }
1445
+ throw new Error(`Unable to bind managed daemon control socket: ${this.socketPath}`);
1446
+ }
1447
+ async handleSocket(socket) {
1448
+ let buffer = '';
1449
+ socket.setEncoding('utf8');
1450
+ socket.on('data', (chunk) => {
1451
+ buffer += chunk;
1452
+ const newlineIndex = buffer.indexOf('\n');
1453
+ if (newlineIndex === -1) {
1454
+ return;
1455
+ }
1456
+ const rawRequest = buffer.slice(0, newlineIndex);
1457
+ void this.respond(socket, rawRequest);
1458
+ buffer = '';
1459
+ });
1460
+ socket.on('error', (error) => {
1461
+ this.logger.error('[agents-host] managed daemon client socket error', {
1462
+ rootPath: this.rootPath,
1463
+ error,
1464
+ });
1465
+ });
1466
+ }
1467
+ async respond(socket, rawRequest) {
1468
+ try {
1469
+ const request = parseDaemonRequest(rawRequest);
1470
+ const response = await this.handleRequest(request);
1471
+ if (request.type === 'shutdown') {
1472
+ socket.end(`${JSON.stringify(response)}\n`, () => {
1473
+ if (this.shutdownRequested) {
1474
+ return;
1475
+ }
1476
+ this.shutdownRequested = true;
1477
+ void this.stop().catch((error) => {
1478
+ this.logger.error('[agents-host] managed daemon shutdown failed', {
1479
+ rootPath: this.rootPath,
1480
+ error,
1481
+ });
1482
+ });
1483
+ });
1484
+ return;
1485
+ }
1486
+ socket.end(`${JSON.stringify(response)}\n`);
1487
+ }
1488
+ catch (error) {
1489
+ socket.end(`${JSON.stringify(toManagedErrorResponse(error, 'MANAGED_RELOAD_FAILED'))}\n`);
1490
+ }
1491
+ }
1492
+ async handleRequest(request) {
1493
+ if (request.type === 'shutdown') {
1494
+ return { ok: true, type: 'shutdown' };
1495
+ }
1496
+ if (!this.ready) {
1497
+ return toManagedErrorResponse(managedOperationError('MANAGED_DAEMON_NOT_READY', MANAGED_DAEMON_NOT_READY_MESSAGE));
1498
+ }
1499
+ if (request.type === 'status') {
1500
+ const spec = ensureCurrentManagedSpec(await loadManagedSpecOrUndefined(this.hostConfigPath, this.loadSpec));
1501
+ return {
1502
+ ok: true,
1503
+ type: 'status',
1504
+ borgeeBaseUrl: spec.host.borgeeBaseUrl,
1505
+ hostConfigPath: this.hostConfigPath,
1506
+ ...(this.logPath ? { logPath: this.logPath } : {}),
1507
+ agentCount: spec.agents.length,
1508
+ enabledAgentCount: countEnabledManagedAgents(spec.agents),
1509
+ ...(this.managedRuntimeFingerprint
1510
+ ? { managedRuntimeFingerprint: this.managedRuntimeFingerprint }
1511
+ : {}),
1512
+ };
1513
+ }
1514
+ if (request.type === 'describeSpec') {
1515
+ const spec = await loadManagedSpecOrUndefined(this.hostConfigPath, this.loadSpec);
1516
+ if (spec) {
1517
+ assertManagedRuntimeBinding(spec.host.borgeeBaseUrl, request.borgeeBaseUrl);
1518
+ }
1519
+ return buildDescribeSpecResponse(spec);
1520
+ }
1521
+ if (request.type === 'upsertAgent') {
1522
+ return this.enqueue(async () => {
1523
+ const currentSpec = ensureCurrentManagedSpec(await loadManagedSpecOrUndefined(this.hostConfigPath, this.loadSpec));
1524
+ const validatedAgent = validateDaemonUpsertAgentInput(currentSpec, request.borgeeBaseUrl, request.agent, process.env);
1525
+ const agentsByKey = new Map(currentSpec.agents.map((agent) => [agent.key, cloneLocalAgentConfig(agent)]));
1526
+ agentsByKey.set(validatedAgent.key, cloneLocalAgentConfig(validatedAgent));
1527
+ const nextSpec = {
1528
+ host: cloneLocalHostGenerateConfig(currentSpec.host),
1529
+ agents: [...agentsByKey.values()].sort((left, right) => left.key.localeCompare(right.key)),
1530
+ };
1531
+ await this.publishSpecWithRollback(currentSpec, nextSpec, () => `Managed daemon reconcile finished without starting agent ${validatedAgent.key}`);
1532
+ return {
1533
+ ok: true,
1534
+ type: 'upsertAgent',
1535
+ agentKey: validatedAgent.key,
1536
+ hostConfigPath: this.hostConfigPath,
1537
+ ...(this.logPath ? { logPath: this.logPath } : {}),
1538
+ agentCount: nextSpec.agents.length,
1539
+ };
1540
+ });
1541
+ }
1542
+ if (request.type === 'applySpec') {
1543
+ return this.enqueue(async () => {
1544
+ const currentSpec = ensureCurrentManagedSpec(await loadManagedSpecOrUndefined(this.hostConfigPath, this.loadSpec));
1545
+ assertManagedRuntimeBinding(currentSpec.host.borgeeBaseUrl, request.borgeeBaseUrl);
1546
+ const nextSpec = validateDaemonApplySpecInput(request.spec, request.borgeeBaseUrl, process.env);
1547
+ await this.publishSpecWithRollback(currentSpec, nextSpec);
1548
+ return buildApplySpecSuccessResponse(this.hostConfigPath, this.logPath, nextSpec);
1549
+ });
1550
+ }
1551
+ throw managedOperationError('MANAGED_SPEC_INVALID', 'Unsupported managed daemon request');
1552
+ }
1553
+ async enqueue(operation) {
1554
+ const next = this.requestQueue.then(operation, operation);
1555
+ this.requestQueue = next.then(() => undefined, () => undefined);
1556
+ return next;
1557
+ }
1558
+ async publishSpecWithRollback(previousSpec, nextSpec, convergenceMessage) {
1559
+ let publishedNextSpec = false;
1560
+ try {
1561
+ await this.materialize(this.rootPath, cloneLocalConfigGenerateSpec(nextSpec));
1562
+ publishedNextSpec = true;
1563
+ try {
1564
+ await this.supervisor.reloadNow(true);
1565
+ }
1566
+ catch (error) {
1567
+ throw managedOperationError('MANAGED_RELOAD_FAILED', `Managed daemon reload failed: ${toErrorMessage(error)}`);
1568
+ }
1569
+ try {
1570
+ assertManagedSupervisorConvergence(this.supervisor, previousSpec, nextSpec);
1571
+ }
1572
+ catch (error) {
1573
+ if (convergenceMessage &&
1574
+ isManagedOperationError(error) &&
1575
+ error.code === 'MANAGED_CONVERGENCE_FAILED') {
1576
+ throw managedOperationError('MANAGED_CONVERGENCE_FAILED', convergenceMessage());
1577
+ }
1578
+ throw error;
1579
+ }
1580
+ }
1581
+ catch (error) {
1582
+ if (!publishedNextSpec) {
1583
+ throw error;
1584
+ }
1585
+ try {
1586
+ await this.materialize(this.rootPath, cloneLocalConfigGenerateSpec(previousSpec));
1587
+ try {
1588
+ await this.supervisor.reloadNow(true);
1589
+ }
1590
+ catch (rollbackReloadError) {
1591
+ throw managedOperationError('MANAGED_RELOAD_FAILED', `Managed daemon rollback reload failed: ${toErrorMessage(rollbackReloadError)}`);
1592
+ }
1593
+ assertManagedSupervisorConvergence(this.supervisor, nextSpec, previousSpec);
1594
+ }
1595
+ catch (rollbackError) {
1596
+ throw combineManagedRollbackFailure(error, rollbackError);
1597
+ }
1598
+ throw error;
1599
+ }
1600
+ }
1601
+ }