@borgee/agents-host 0.2.28 → 0.2.29

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.
package/README.md CHANGED
@@ -54,6 +54,7 @@ agents-host start-managed <serverUrl> <apiKey> [agent-options]
54
54
  agents-host describe-managed <serverUrl>
55
55
  agents-host apply-managed <serverUrl> --stdin
56
56
  agents-host apply-managed <serverUrl> --spec-json <json>
57
+ agents-host cleanup-managed <serverUrl> [--purge]
57
58
  agents-host log <serverUrl> [--lines <n>] [--follow]
58
59
  agents-host log <serverUrl> --path
59
60
  agents-host start --config <path-to-host-config> [--debug]
@@ -77,7 +78,28 @@ RUNTIME_PROVIDER=claude \
77
78
  pnpm --filter @borgee/agents-host dev
78
79
  ```
79
80
 
80
- Codex remains opt-in. Select `RUNTIME_PROVIDER=codex` only when `AGENTS_HOST_INTERNAL_COMPATIBILITY_GATES` includes `codex-provider`.
81
+ Codex now ships on the same default hosted-provider surface as Claude and Copilot. Select `RUNTIME_PROVIDER=codex` directly unless you are explicitly rolling that path back with `AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES=codex-provider`.
82
+
83
+ ### Shipped internal compatibility defaults and rollback controls
84
+
85
+ The current shipped internal compatibility surface resolves gates in this order:
86
+
87
+ 1. start from the shipped defaults: `claude-provider-v2`, `copilot-provider-v2`, `codex-provider`, `managed-runtime-convergence`, and `policy-audit-enforcement`
88
+ 2. add any explicit entries from `AGENTS_HOST_INTERNAL_COMPATIBILITY_GATES`
89
+ 3. subtract any explicit entries from `AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES`
90
+
91
+ The shipped default set now also includes `connections-state-layer`, `context-injection`, `skill-runtime`, `localhost-gateway`, `token-binding`, `collaboration-outcome-model`, `task-thread-collaboration-contract`, and `collaboration-capabilities-diagnostics`. `attention-follow-semantics`, `collaboration-skill-first`, and hosted `praestoclaw-provider` still remain opt-in.
92
+
93
+ Managed-runtime settings still preserve the managed rollback rule for `connections-state-layer` plus `token-binding`: when `managed-runtime-convergence` remains enabled, the managed surface forces that pair on; explicitly disabling `managed-runtime-convergence` removes that pair from the managed surface again unless you also explicitly re-enable them.
94
+
95
+ Managed runtime precedence stays intentional: detached daemon respawns always reuse the persisted `managed-runtime-settings.json` snapshot, bare `start-managed <serverUrl>` and `apply-managed` treat the caller's current env as an explicit request to update that managed runtime surface, and full `start-managed <serverUrl> <apiKey> ...` validates the agent upsert against the existing managed-root snapshot before ambient rollback env can reject an otherwise valid existing surface.
96
+ Legacy managed roots created before `managed-runtime-settings.json` existed need one bare `agents-host start-managed <serverUrl>` migration pass before full per-agent updates can rely on that snapshot-backed precedence.
97
+
98
+ Rollback and hardening controls stay explicit:
99
+
100
+ - `AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES=<comma-list>` disables shipped gates for rollback
101
+ - `AGENTS_HOST_INTERNAL_PROVIDER_IMPLEMENTATIONS=claude:v1,copilot:v1` forces Claude and/or Copilot back to the v1 adapters while their v2 gates stay enabled
102
+ - `AGENTS_HOST_INTERNAL_POLICY_MODE=enforce` hardens the shipped `policy-audit-enforcement` gate beyond its default `audit-only` mode
81
103
 
82
104
  Or with the CLI:
83
105
 
@@ -117,6 +139,7 @@ and resumable in that case.
117
139
  Each managed runtime root keeps:
118
140
 
119
141
  - `agents-host.yaml` and `agents/` as the stable generated local-config entrypoints
142
+ - `managed-runtime-settings.json` as the persisted managed runtime surface snapshot
120
143
  - `daemon.log` as the detached managed daemon stdout/stderr log file
121
144
  - `.state/` as the per-agent runtime state base
122
145
  - `ctl.sock` as the local control socket
@@ -210,8 +233,8 @@ Notes:
210
233
  | `BORGEE_AGENT_API_KEY` | positional `<apiKey>` | yes | — | Agent API key from the web UI |
211
234
  | `AGENTS_HOST_DEBUG` | `--debug` | no | off | Extra host/provider lifecycle logs. Set `AGENTS_HOST_DEBUG=1` to enable without the CLI flag. |
212
235
  | `BORGEE_AGENT_NAME` | `--name` | no | `Assistant` | Display name used in prompts |
213
- | `RUNTIME_PROVIDER` | `--provider` | no | `claude` | `claude`, `copilot`, or `codex` when `codex-provider` is enabled |
214
- | `CLAUDE_COMMAND` / `CLAUDE_ARGS` | `--claude-command` / `--claude-args` | no | `claude` / `--print` | Local Claude CLI command + args |
236
+ | `RUNTIME_PROVIDER` | `--provider` | no | `claude` | `claude`, `copilot`, or `codex` |
237
+ | `CLAUDE_COMMAND` / `CLAUDE_ARGS` | `--claude-command` / `--claude-args` | no | `claude` / `--print --permission-mode bypassPermissions` | Local Claude CLI command + args |
215
238
  | `CODEX_COMMAND` / `CODEX_ARGS` | `--codex-command` / `--codex-args` | no | `codex-acp` / empty | Local Codex ACP adapter command + args. The default launch resolves the bundled `@agentclientprotocol/codex-acp` entrypoint. |
216
239
  | `COPILOT_COMMAND` / `COPILOT_ARGS` | `--copilot-command` / `--copilot-args` | no | `copilot` / parsed but ignored by ACP runtime | Local Copilot CLI command. The persistent Copilot ACP prototype always launches `copilot --acp`. |
217
240
  | `COPILOT_SESSION_TTL_MINUTES` | `--copilot-session-ttl-minutes` | no | `2880` | Idle TTL for per-channel Copilot ACP sessions |
@@ -414,7 +437,7 @@ Agent config fields:
414
437
  - `key` (**required**): stable identity used for diffing and reload decisions
415
438
  - `name` (**required**)
416
439
  - `apiKey` (**required**)
417
- - `provider` (**required**): `claude`, `copilot`, or gated `codex`
440
+ - `provider` (**required**): `claude`, `copilot`, or `codex` (roll back Codex only with `AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES=codex-provider`)
418
441
  - `enabled` (optional, default `true`)
419
442
  - optional overrides for `claudeCommand`, `claudeArgs`, `codexCommand`,
420
443
  `codexArgs`, `copilotCommand`, `copilotArgs`, and
@@ -14,6 +14,7 @@ export declare const TOKEN_BINDING_COMPATIBILITY_GATE = "token-binding";
14
14
  export declare const POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE = "policy-audit-enforcement";
15
15
  export declare const MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE = "managed-runtime-convergence";
16
16
  export declare const COMPATIBILITY_GATES_ENV = "AGENTS_HOST_INTERNAL_COMPATIBILITY_GATES";
17
+ export declare const INTERNAL_DISABLED_COMPATIBILITY_GATES_ENV = "AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES";
17
18
  export declare const INTERNAL_POLICY_MODE_ENV = "AGENTS_HOST_INTERNAL_POLICY_MODE";
18
19
  export declare const INTERNAL_PROVIDER_IMPLEMENTATIONS_ENV = "AGENTS_HOST_INTERNAL_PROVIDER_IMPLEMENTATIONS";
19
20
  export declare const MANAGED_RUNTIME_SETTINGS_SCHEMA_VERSION = 1;
@@ -27,8 +28,10 @@ export interface ManagedRuntimeSettingsSnapshot {
27
28
  providerImplementationOverrides: string[];
28
29
  internalPolicyMode: InternalPolicyMode;
29
30
  }
31
+ export declare const DEFAULT_INTERNAL_COMPATIBILITY_GATES: readonly string[];
30
32
  export declare function parseCompatibilityGates(rawValue: string | undefined): ReadonlySet<string>;
31
- export declare function resolveInternalCompatibilityGates(): ReadonlySet<string>;
33
+ export declare function resolveInternalCompatibilityGates(env?: NodeJS.ProcessEnv): ReadonlySet<string>;
34
+ export declare function resolveDisabledDefaultCompatibilityGates(compatibilityGates: Iterable<string>): string[];
32
35
  export declare function normalizeInternalProviderImplementationOverrides(rawValue: string | undefined): string[];
33
36
  export declare function parseInternalProviderImplementationOverrides(rawValue: string | undefined): InternalProviderImplementationOverrides;
34
37
  export declare function resolveManagedRuntimeProviderImplementationOverrides(env?: NodeJS.ProcessEnv, compatibilityGates?: string[]): string[];
@@ -15,10 +15,26 @@ export const TOKEN_BINDING_COMPATIBILITY_GATE = 'token-binding';
15
15
  export const POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE = 'policy-audit-enforcement';
16
16
  export const MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE = 'managed-runtime-convergence';
17
17
  export const COMPATIBILITY_GATES_ENV = 'AGENTS_HOST_INTERNAL_COMPATIBILITY_GATES';
18
+ export const INTERNAL_DISABLED_COMPATIBILITY_GATES_ENV = 'AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES';
18
19
  export const INTERNAL_POLICY_MODE_ENV = 'AGENTS_HOST_INTERNAL_POLICY_MODE';
19
20
  export const INTERNAL_PROVIDER_IMPLEMENTATIONS_ENV = 'AGENTS_HOST_INTERNAL_PROVIDER_IMPLEMENTATIONS';
20
21
  export const MANAGED_RUNTIME_SETTINGS_SCHEMA_VERSION = 1;
21
22
  const VALID_PROVIDER_IMPLEMENTATION_ENTRIES = '"claude:v1", "claude:v2", "copilot:v1", or "copilot:v2"';
23
+ export const DEFAULT_INTERNAL_COMPATIBILITY_GATES = Object.freeze([
24
+ CLAUDE_PROVIDER_V2_COMPATIBILITY_GATE,
25
+ COLLABORATION_CAPABILITIES_DIAGNOSTICS_COMPATIBILITY_GATE,
26
+ COLLABORATION_OUTCOME_MODEL_COMPATIBILITY_GATE,
27
+ CONNECTIONS_STATE_LAYER_COMPATIBILITY_GATE,
28
+ CONTEXT_INJECTION_COMPATIBILITY_GATE,
29
+ CODEX_PROVIDER_COMPATIBILITY_GATE,
30
+ COPILOT_PROVIDER_V2_COMPATIBILITY_GATE,
31
+ LOCALHOST_GATEWAY_COMPATIBILITY_GATE,
32
+ MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE,
33
+ POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE,
34
+ SKILL_RUNTIME_COMPATIBILITY_GATE,
35
+ TASK_THREAD_COLLABORATION_CONTRACT_COMPATIBILITY_GATE,
36
+ TOKEN_BINDING_COMPATIBILITY_GATE,
37
+ ]);
22
38
  const PROVIDER_IMPLEMENTATION_COMPATIBILITY_GATES = {
23
39
  claude: CLAUDE_PROVIDER_V2_COMPATIBILITY_GATE,
24
40
  copilot: COPILOT_PROVIDER_V2_COMPATIBILITY_GATE,
@@ -50,8 +66,19 @@ export function parseCompatibilityGates(rawValue) {
50
66
  }
51
67
  return gates;
52
68
  }
53
- export function resolveInternalCompatibilityGates() {
54
- return parseCompatibilityGates(process.env[COMPATIBILITY_GATES_ENV]);
69
+ export function resolveInternalCompatibilityGates(env = process.env) {
70
+ const compatibilityGates = new Set(DEFAULT_INTERNAL_COMPATIBILITY_GATES);
71
+ for (const gate of parseCompatibilityGates(env[COMPATIBILITY_GATES_ENV])) {
72
+ compatibilityGates.add(gate);
73
+ }
74
+ for (const gate of parseCompatibilityGates(env[INTERNAL_DISABLED_COMPATIBILITY_GATES_ENV])) {
75
+ compatibilityGates.delete(gate);
76
+ }
77
+ return compatibilityGates;
78
+ }
79
+ export function resolveDisabledDefaultCompatibilityGates(compatibilityGates) {
80
+ const enabledGates = new Set(compatibilityGates);
81
+ return DEFAULT_INTERNAL_COMPATIBILITY_GATES.filter((gate) => !enabledGates.has(gate));
55
82
  }
56
83
  export function normalizeInternalProviderImplementationOverrides(rawValue) {
57
84
  if (!rawValue || rawValue.trim().length === 0) {
@@ -95,11 +122,20 @@ export function resolveManagedRuntimeProviderImplementationOverrides(env = proce
95
122
  });
96
123
  }
97
124
  export function resolveManagedRuntimeCompatibilityGates(env = process.env) {
98
- const compatibilityGates = new Set(parseCompatibilityGates(env[COMPATIBILITY_GATES_ENV]));
125
+ const compatibilityGates = new Set(resolveInternalCompatibilityGates(env));
126
+ const explicitlyEnabledGates = parseCompatibilityGates(env[COMPATIBILITY_GATES_ENV]);
99
127
  if (compatibilityGates.has(MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE)) {
100
128
  compatibilityGates.add(CONNECTIONS_STATE_LAYER_COMPATIBILITY_GATE);
101
129
  compatibilityGates.add(TOKEN_BINDING_COMPATIBILITY_GATE);
102
130
  }
131
+ else {
132
+ if (!explicitlyEnabledGates.has(CONNECTIONS_STATE_LAYER_COMPATIBILITY_GATE)) {
133
+ compatibilityGates.delete(CONNECTIONS_STATE_LAYER_COMPATIBILITY_GATE);
134
+ }
135
+ if (!explicitlyEnabledGates.has(TOKEN_BINDING_COMPATIBILITY_GATE)) {
136
+ compatibilityGates.delete(TOKEN_BINDING_COMPATIBILITY_GATE);
137
+ }
138
+ }
103
139
  return sortUniqueStrings(compatibilityGates);
104
140
  }
105
141
  export function resolveInternalPolicyMode(gateEnabled, env = process.env) {
package/dist/config.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { resolveSingleAgentStateRoot } from './state-paths.js';
2
- import { CODEX_PROVIDER_COMPATIBILITY_GATE, COMPATIBILITY_GATES_ENV, parseCompatibilityGates, } from './compatibility-gates.js';
2
+ import { CODEX_PROVIDER_COMPATIBILITY_GATE, resolveInternalCompatibilityGates, } from './compatibility-gates.js';
3
3
  const MAX_TIMER_DELAY_MS = 2_147_483_647;
4
4
  export const MAX_COPILOT_SESSION_TTL_MINUTES = MAX_TIMER_DELAY_MS / 60_000;
5
5
  export const DEFAULT_COPILOT_SESSION_TTL_MINUTES = 2 * 24 * 60;
@@ -69,9 +69,9 @@ export function assertProviderCompatibility(provider, sourceLabel, env = process
69
69
  if (provider !== 'codex') {
70
70
  return;
71
71
  }
72
- const compatibilityGates = parseCompatibilityGates(env[COMPATIBILITY_GATES_ENV]);
72
+ const compatibilityGates = resolveInternalCompatibilityGates(env);
73
73
  if (!compatibilityGates.has(CODEX_PROVIDER_COMPATIBILITY_GATE)) {
74
- throw new Error(`${sourceLabel}: provider "codex" requires ${COMPATIBILITY_GATES_ENV} to include ${CODEX_PROVIDER_COMPATIBILITY_GATE}`);
74
+ throw new Error(`${sourceLabel}: provider "codex" requires the shipped ${CODEX_PROVIDER_COMPATIBILITY_GATE} path to remain enabled`);
75
75
  }
76
76
  }
77
77
  export function resolveProviderCommandConfig(overrides = {}) {
@@ -5,7 +5,7 @@ import { createConnection, createServer } from 'node:net';
5
5
  import { dirname, join, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
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';
8
+ import { COMPATIBILITY_GATES_ENV, createManagedRuntimeSettingsFingerprint, INTERNAL_DISABLED_COMPATIBILITY_GATES_ENV, INTERNAL_POLICY_MODE_ENV, INTERNAL_PROVIDER_IMPLEMENTATIONS_ENV, MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE, MANAGED_RUNTIME_SETTINGS_SCHEMA_VERSION, resolveDisabledDefaultCompatibilityGates, resolveManagedRuntimeSettingsSnapshot, normalizeInternalProviderImplementationOverrides, } from './compatibility-gates.js';
9
9
  import { loadConfigFromEnv } from './config.js';
10
10
  import { loadLocalConfigGenerateSpec, materializeLocalConfig, parseGenerateConfigSpec, resolveLocalConfigLayout, } from './local-config.js';
11
11
  import { normalizeManagedRuntimeKey, resolveManagedBootstrapLockPath, resolveManagedDaemonLogPath, resolveManagedDaemonSocketPath, resolveManagedRuntimeRoot, resolveManagedRuntimeSettingsPath, } from './state-paths.js';
@@ -222,15 +222,45 @@ function buildManagedRuntimeSettingsEnv(baseEnv, snapshot) {
222
222
  return {
223
223
  ...baseEnv,
224
224
  [COMPATIBILITY_GATES_ENV]: snapshot.compatibilityGates.join(','),
225
+ [INTERNAL_DISABLED_COMPATIBILITY_GATES_ENV]: resolveDisabledDefaultCompatibilityGates(snapshot.compatibilityGates).join(','),
225
226
  [INTERNAL_PROVIDER_IMPLEMENTATIONS_ENV]: snapshot.providerImplementationOverrides.join(','),
226
227
  [INTERNAL_POLICY_MODE_ENV]: snapshot.internalPolicyMode,
227
228
  };
228
229
  }
229
- function getManagedRuntimeFingerprintMismatch(status, desiredSettings) {
230
- if (!desiredSettings.convergenceEnabled) {
231
- return status.managedRuntimeFingerprint != null;
230
+ function buildDesiredManagedRuntimeSettingsFromSnapshot(snapshot) {
231
+ return {
232
+ snapshot,
233
+ fingerprint: createManagedRuntimeSettingsFingerprint(snapshot),
234
+ convergenceEnabled: snapshot.compatibilityGates.includes(MANAGED_RUNTIME_CONVERGENCE_COMPATIBILITY_GATE),
235
+ };
236
+ }
237
+ async function getManagedRuntimeFingerprintMismatch(rootPath, status, desiredSettings) {
238
+ if (status.managedRuntimeFingerprint != null) {
239
+ return status.managedRuntimeFingerprint !== desiredSettings.fingerprint;
240
+ }
241
+ if (desiredSettings.convergenceEnabled) {
242
+ return true;
243
+ }
244
+ const persistedSettingsSnapshot = await loadManagedRuntimeSettingsSnapshot(rootPath);
245
+ if (!persistedSettingsSnapshot) {
246
+ return true;
247
+ }
248
+ return createManagedRuntimeSettingsFingerprint(persistedSettingsSnapshot) !== desiredSettings.fingerprint;
249
+ }
250
+ async function resolveAuthoritativeManagedRuntimeSettings(rootPath, effectiveProcessEnv) {
251
+ const persistedSettingsSnapshot = await loadManagedRuntimeSettingsSnapshot(rootPath);
252
+ if (!persistedSettingsSnapshot) {
253
+ return {
254
+ desiredSettings: resolveDesiredManagedRuntimeSettings(effectiveProcessEnv),
255
+ processEnv: effectiveProcessEnv,
256
+ persistedSettingsSnapshot: null,
257
+ };
232
258
  }
233
- return status.managedRuntimeFingerprint !== desiredSettings.fingerprint;
259
+ return {
260
+ desiredSettings: buildDesiredManagedRuntimeSettingsFromSnapshot(persistedSettingsSnapshot),
261
+ processEnv: buildManagedRuntimeSettingsEnv(effectiveProcessEnv, persistedSettingsSnapshot),
262
+ persistedSettingsSnapshot,
263
+ };
234
264
  }
235
265
  function cloneLocalAgentConfig(agent) {
236
266
  return {
@@ -311,6 +341,57 @@ export function buildManagedLocalAgentConfig(options) {
311
341
  },
312
342
  };
313
343
  }
344
+ function mergeManagedLocalAgentConfig(existingAgent, generatedAgent, envOverrides) {
345
+ const overrides = envOverrides ?? {};
346
+ const hasOverride = (key) => Object.prototype.hasOwnProperty.call(overrides, key);
347
+ return {
348
+ ...cloneLocalAgentConfig(existingAgent),
349
+ key: generatedAgent.key,
350
+ apiKey: generatedAgent.apiKey,
351
+ enabled: true,
352
+ name: hasOverride('BORGEE_AGENT_NAME') ? generatedAgent.name : existingAgent.name,
353
+ provider: hasOverride('RUNTIME_PROVIDER') ? generatedAgent.provider : existingAgent.provider,
354
+ claudeCommand: hasOverride('CLAUDE_COMMAND') ? generatedAgent.claudeCommand : existingAgent.claudeCommand,
355
+ claudeArgs: hasOverride('CLAUDE_ARGS')
356
+ ? [...(generatedAgent.claudeArgs ?? [])]
357
+ : [...(existingAgent.claudeArgs ?? [])],
358
+ codexCommand: hasOverride('CODEX_COMMAND') ? generatedAgent.codexCommand : existingAgent.codexCommand,
359
+ codexArgs: hasOverride('CODEX_ARGS')
360
+ ? [...(generatedAgent.codexArgs ?? [])]
361
+ : [...(existingAgent.codexArgs ?? [])],
362
+ copilotCommand: hasOverride('COPILOT_COMMAND')
363
+ ? generatedAgent.copilotCommand
364
+ : existingAgent.copilotCommand,
365
+ copilotArgs: hasOverride('COPILOT_ARGS')
366
+ ? [...(generatedAgent.copilotArgs ?? [])]
367
+ : [...(existingAgent.copilotArgs ?? [])],
368
+ copilotSessionTtlMinutes: hasOverride('COPILOT_SESSION_TTL_MINUTES')
369
+ ? generatedAgent.copilotSessionTtlMinutes
370
+ : existingAgent.copilotSessionTtlMinutes,
371
+ };
372
+ }
373
+ function collectExplicitManagedAgentEnvOverrides(processEnv, optionEnv) {
374
+ const explicitOverrides = {};
375
+ for (const key of [
376
+ 'BORGEE_AGENT_NAME',
377
+ 'RUNTIME_PROVIDER',
378
+ 'CLAUDE_COMMAND',
379
+ 'CLAUDE_ARGS',
380
+ 'CODEX_COMMAND',
381
+ 'CODEX_ARGS',
382
+ 'COPILOT_COMMAND',
383
+ 'COPILOT_ARGS',
384
+ 'COPILOT_SESSION_TTL_MINUTES',
385
+ ]) {
386
+ if (processEnv[key] !== undefined) {
387
+ explicitOverrides[key] = processEnv[key];
388
+ }
389
+ if (optionEnv && Object.prototype.hasOwnProperty.call(optionEnv, key)) {
390
+ explicitOverrides[key] = optionEnv[key];
391
+ }
392
+ }
393
+ return explicitOverrides;
394
+ }
314
395
  function resolveManagedRuntimeLayoutForServerUrl(serverUrl, processEnv = process.env) {
315
396
  const rootPath = resolveManagedRuntimeRoot(serverUrl, processEnv);
316
397
  return {
@@ -884,8 +965,6 @@ export async function bootstrapManagedDaemonStart(options, deps = {}) {
884
965
  ...(options.processEnv ?? process.env),
885
966
  ...options.env,
886
967
  };
887
- const loadSpec = deps.loadSpec
888
- ?? ((hostConfigPath) => loadLocalConfigGenerateSpec(hostConfigPath, { env: effectiveProcessEnv }));
889
968
  const materialize = deps.materialize ?? materializeLocalConfig;
890
969
  const sendRequest = deps.sendRequest ?? sendManagedDaemonRequest;
891
970
  const spawnDaemonProcess = deps.spawnDaemon ?? spawnManagedDaemonProcess;
@@ -895,19 +974,51 @@ export async function bootstrapManagedDaemonStart(options, deps = {}) {
895
974
  ?? (async (rootPath) => {
896
975
  await waitForManagedDaemonShutdown(rootPath, canConnectToSocket);
897
976
  });
898
- const desiredSettings = resolveDesiredManagedRuntimeSettings(effectiveProcessEnv);
899
- const resolvedManagedAgent = options.apiKey === undefined ? null : buildManagedLocalAgentConfig(options);
900
- const resolved = resolvedManagedAgent ??
901
- resolveManagedRuntimeLayoutForServerUrl(options.serverUrl, options.processEnv);
977
+ const requestedSettings = resolveDesiredManagedRuntimeSettings(effectiveProcessEnv);
978
+ const resolved = resolveManagedRuntimeLayoutForServerUrl(options.serverUrl, options.processEnv);
902
979
  const releaseBootstrapLock = await acquireLock(resolved.rootPath);
903
980
  try {
981
+ const currentRuntimeSettings = await resolveAuthoritativeManagedRuntimeSettings(resolved.rootPath, effectiveProcessEnv);
982
+ const loadManagedSpecWithCurrentRuntimeEnv = (hostConfigPath) => deps.loadSpec
983
+ ? deps.loadSpec(hostConfigPath)
984
+ : loadLocalConfigGenerateSpec(hostConfigPath, {
985
+ env: currentRuntimeSettings.processEnv,
986
+ });
987
+ if (options.apiKey !== undefined
988
+ && currentRuntimeSettings.persistedSettingsSnapshot == null
989
+ && await fs.access(resolved.hostConfigPath).then(() => true).catch(() => false)) {
990
+ throw new Error(`Managed runtime root ${resolved.rootPath} predates managed-runtime-settings.json; run \`agents-host start-managed ${options.serverUrl}\` once to migrate it before updating an individual agent`);
991
+ }
992
+ const explicitManagedAgentOverrides = collectExplicitManagedAgentEnvOverrides(options.processEnv ?? process.env, options.env);
993
+ const currentManagedSpec = options.apiKey === undefined
994
+ ? undefined
995
+ : await loadManagedSpecOrUndefined(resolved.hostConfigPath, loadManagedSpecWithCurrentRuntimeEnv);
996
+ const resolvedManagedAgent = options.apiKey === undefined
997
+ ? null
998
+ : (() => {
999
+ const generatedManagedAgent = buildManagedLocalAgentConfig({
1000
+ ...options,
1001
+ processEnv: currentRuntimeSettings.processEnv,
1002
+ env: options.env,
1003
+ });
1004
+ const existingAgent = currentManagedSpec?.agents.find((agent) => agent.key === generatedManagedAgent.agent.key);
1005
+ return existingAgent
1006
+ ? {
1007
+ ...generatedManagedAgent,
1008
+ agent: mergeManagedLocalAgentConfig(existingAgent, generatedManagedAgent.agent, explicitManagedAgentOverrides),
1009
+ }
1010
+ : generatedManagedAgent;
1011
+ })();
1012
+ const desiredBootstrapSettings = options.apiKey === undefined ? requestedSettings : currentRuntimeSettings.desiredSettings;
1013
+ const desiredBootstrapProcessEnv = options.apiKey === undefined ? effectiveProcessEnv : currentRuntimeSettings.processEnv;
904
1014
  let statusResponse = await readReadyManagedDaemonStatus(resolved.rootPath, sendRequest);
905
- if (statusResponse && getManagedRuntimeFingerprintMismatch(statusResponse, desiredSettings)) {
1015
+ if (statusResponse
1016
+ && await getManagedRuntimeFingerprintMismatch(resolved.rootPath, statusResponse, desiredBootstrapSettings)) {
906
1017
  await recycleManagedDaemonWithDesiredSettings({
907
1018
  rootPath: resolved.rootPath,
908
1019
  debug: options.debug === true,
909
- processEnv: effectiveProcessEnv,
910
- desiredSettings,
1020
+ processEnv: desiredBootstrapProcessEnv,
1021
+ desiredSettings: desiredBootstrapSettings,
911
1022
  sendRequest,
912
1023
  waitForShutdown,
913
1024
  spawnDaemonProcess,
@@ -917,11 +1028,17 @@ export async function bootstrapManagedDaemonStart(options, deps = {}) {
917
1028
  }
918
1029
  if (!statusResponse) {
919
1030
  if (options.apiKey === undefined) {
920
- await ensurePersistedManagedAgentsExist(options.serverUrl, resolved.hostConfigPath, loadSpec);
1031
+ await ensurePersistedManagedAgentsExist(options.serverUrl, resolved.hostConfigPath, (hostConfigPath) => options.apiKey === undefined
1032
+ ? (deps.loadSpec
1033
+ ? deps.loadSpec(hostConfigPath)
1034
+ : loadLocalConfigGenerateSpec(hostConfigPath, {
1035
+ env: desiredBootstrapProcessEnv,
1036
+ }))
1037
+ : loadManagedSpecWithCurrentRuntimeEnv(hostConfigPath));
921
1038
  }
922
1039
  else {
923
1040
  try {
924
- await loadSpec(resolved.hostConfigPath);
1041
+ await loadManagedSpecWithCurrentRuntimeEnv(resolved.hostConfigPath);
925
1042
  }
926
1043
  catch (error) {
927
1044
  if (isNotFoundError(error)) {
@@ -938,8 +1055,8 @@ export async function bootstrapManagedDaemonStart(options, deps = {}) {
938
1055
  await startManagedDaemonWithPersistedSettings({
939
1056
  rootPath: resolved.rootPath,
940
1057
  debug: options.debug === true,
941
- processEnv: effectiveProcessEnv,
942
- desiredSettings,
1058
+ processEnv: desiredBootstrapProcessEnv,
1059
+ desiredSettings: desiredBootstrapSettings,
943
1060
  spawnDaemonProcess,
944
1061
  waitForDaemon,
945
1062
  sendRequest,
@@ -948,7 +1065,11 @@ export async function bootstrapManagedDaemonStart(options, deps = {}) {
948
1065
  }
949
1066
  if (options.apiKey === undefined) {
950
1067
  if (!statusResponse) {
951
- await ensurePersistedManagedAgentsExist(options.serverUrl, resolved.hostConfigPath, loadSpec);
1068
+ await ensurePersistedManagedAgentsExist(options.serverUrl, resolved.hostConfigPath, (hostConfigPath) => deps.loadSpec
1069
+ ? deps.loadSpec(hostConfigPath)
1070
+ : loadLocalConfigGenerateSpec(hostConfigPath, {
1071
+ env: desiredBootstrapProcessEnv,
1072
+ }));
952
1073
  }
953
1074
  else {
954
1075
  assertManagedRuntimeBinding(statusResponse.borgeeBaseUrl, options.serverUrl);
@@ -1079,10 +1200,11 @@ async function terminateChildProcessAndWait(child) {
1079
1200
  }
1080
1201
  await waitForChildExit(child, DAEMON_FORCE_KILL_WAIT_MS);
1081
1202
  }
1082
- async function rollbackManagedApplyFailure(rootPath, previousSpec, materialize, clearRuntimeImpl, applyError) {
1203
+ async function rollbackManagedApplyFailure(rootPath, previousSpec, previousSettingsSnapshot, materialize, clearRuntimeImpl, applyError) {
1083
1204
  const original = toManagedErrorPayload(applyError, 'MANAGED_RELOAD_FAILED');
1084
1205
  try {
1085
1206
  if (previousSpec) {
1207
+ await restoreManagedRuntimeSettingsSnapshot(rootPath, previousSettingsSnapshot);
1086
1208
  await materialize(rootPath, cloneLocalConfigGenerateSpec(previousSpec));
1087
1209
  }
1088
1210
  else {
@@ -1139,8 +1261,6 @@ function validateDaemonUpsertAgentInput(currentSpec, serverUrl, agent, env = pro
1139
1261
  }
1140
1262
  export async function describeManagedSpec(options, deps = {}) {
1141
1263
  const effectiveProcessEnv = options.processEnv ?? process.env;
1142
- const loadSpec = deps.loadSpec
1143
- ?? ((hostConfigPath) => loadLocalConfigGenerateSpec(hostConfigPath, { env: effectiveProcessEnv }));
1144
1264
  const sendRequest = deps.sendRequest ?? sendManagedDaemonRequest;
1145
1265
  const resolved = resolveManagedRuntimeLayoutForServerUrl(options.serverUrl, options.processEnv);
1146
1266
  try {
@@ -1167,7 +1287,12 @@ export async function describeManagedSpec(options, deps = {}) {
1167
1287
  }
1168
1288
  }
1169
1289
  try {
1170
- const persistedSpec = await loadManagedSpecOrUndefined(resolved.hostConfigPath, loadSpec);
1290
+ const currentRuntimeSettings = await resolveAuthoritativeManagedRuntimeSettings(resolved.rootPath, effectiveProcessEnv);
1291
+ const persistedSpec = await loadManagedSpecOrUndefined(resolved.hostConfigPath, (hostConfigPath) => deps.loadSpec
1292
+ ? deps.loadSpec(hostConfigPath)
1293
+ : loadLocalConfigGenerateSpec(hostConfigPath, {
1294
+ env: currentRuntimeSettings.processEnv,
1295
+ }));
1171
1296
  if (!persistedSpec) {
1172
1297
  return { ok: true, present: false };
1173
1298
  }
@@ -1207,27 +1332,13 @@ async function applySpecToRunningManagedDaemon(rootPath, borgeeBaseUrl, spec, se
1207
1332
  }
1208
1333
  export async function applyManagedSpec(options, deps = {}) {
1209
1334
  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 }));
1335
+ const desiredSettings = resolveDesiredManagedRuntimeSettings(effectiveProcessEnv);
1214
1336
  const sendRequest = deps.sendRequest ?? sendManagedDaemonRequest;
1215
1337
  const spawnDaemonProcess = deps.spawnDaemon ?? spawnManagedDaemonProcess;
1216
1338
  const waitForDaemon = deps.waitForDaemonReady ?? waitForManagedDaemonReady;
1217
1339
  const acquireLock = deps.acquireBootstrapLock ?? acquireBootstrapLock;
1218
1340
  const clearManagedRuntimeImpl = deps.clearManagedRuntime ?? clearManagedRuntime;
1219
1341
  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
1342
  const waitForShutdown = deps.waitForShutdown
1232
1343
  ?? (async (rootPath) => {
1233
1344
  await waitForManagedDaemonShutdown(rootPath, canConnectToSocket);
@@ -1240,6 +1351,32 @@ export async function applyManagedSpec(options, deps = {}) {
1240
1351
  return toApplyManagedColdStartErrorResponse(error);
1241
1352
  }
1242
1353
  try {
1354
+ const currentRuntimeSettings = await resolveAuthoritativeManagedRuntimeSettings(resolved.rootPath, effectiveProcessEnv);
1355
+ const loadSpecWithCurrentRuntimeEnv = (hostConfigPath) => deps.loadSpec
1356
+ ? deps.loadSpec(hostConfigPath)
1357
+ : loadLocalConfigGenerateSpec(hostConfigPath, {
1358
+ env: currentRuntimeSettings.processEnv,
1359
+ });
1360
+ const materializeWithDesiredEnv = (rootPath, nextSpec) => deps.materialize
1361
+ ? deps.materialize(rootPath, nextSpec)
1362
+ : materializeLocalConfig(rootPath, nextSpec, {
1363
+ env: effectiveProcessEnv,
1364
+ });
1365
+ const materializeWithCurrentRuntimeEnv = (rootPath, nextSpec) => deps.materialize
1366
+ ? deps.materialize(rootPath, nextSpec)
1367
+ : materializeLocalConfig(rootPath, nextSpec, {
1368
+ env: currentRuntimeSettings.processEnv,
1369
+ });
1370
+ let spec;
1371
+ try {
1372
+ spec = validateApplyManagedInput(options.spec, options.serverUrl, effectiveProcessEnv);
1373
+ }
1374
+ catch (error) {
1375
+ if (isManagedOperationError(error)) {
1376
+ return toManagedErrorResponse(error);
1377
+ }
1378
+ return toManagedErrorResponse(managedOperationError('MANAGED_SPEC_INVALID', `Invalid managed spec: ${toErrorMessage(error)}`));
1379
+ }
1243
1380
  let statusResponse;
1244
1381
  try {
1245
1382
  statusResponse = await readReadyManagedDaemonStatus(resolved.rootPath, sendRequest);
@@ -1250,7 +1387,8 @@ export async function applyManagedSpec(options, deps = {}) {
1250
1387
  }
1251
1388
  throw error;
1252
1389
  }
1253
- if (statusResponse && getManagedRuntimeFingerprintMismatch(statusResponse, desiredSettings)) {
1390
+ if (statusResponse
1391
+ && await getManagedRuntimeFingerprintMismatch(resolved.rootPath, statusResponse, desiredSettings)) {
1254
1392
  try {
1255
1393
  await recycleManagedDaemonWithDesiredSettings({
1256
1394
  rootPath: resolved.rootPath,
@@ -1273,7 +1411,7 @@ export async function applyManagedSpec(options, deps = {}) {
1273
1411
  }
1274
1412
  let previousSpec;
1275
1413
  try {
1276
- previousSpec = await loadManagedSpecOrUndefined(resolved.hostConfigPath, loadSpec);
1414
+ previousSpec = await loadManagedSpecOrUndefined(resolved.hostConfigPath, loadSpecWithCurrentRuntimeEnv);
1277
1415
  if (previousSpec) {
1278
1416
  assertManagedSpecBinding(previousSpec, options.serverUrl);
1279
1417
  }
@@ -1282,10 +1420,10 @@ export async function applyManagedSpec(options, deps = {}) {
1282
1420
  return toApplyManagedColdStartErrorResponse(error);
1283
1421
  }
1284
1422
  try {
1285
- await materialize(resolved.rootPath, cloneLocalConfigGenerateSpec(spec));
1423
+ await materializeWithDesiredEnv(resolved.rootPath, cloneLocalConfigGenerateSpec(spec));
1286
1424
  }
1287
1425
  catch (error) {
1288
- return rollbackManagedApplyFailure(resolved.rootPath, previousSpec, materialize, clearManagedRuntimeImpl, error);
1426
+ return rollbackManagedApplyFailure(resolved.rootPath, previousSpec, currentRuntimeSettings.persistedSettingsSnapshot ?? undefined, materializeWithCurrentRuntimeEnv, clearManagedRuntimeImpl, error);
1289
1427
  }
1290
1428
  try {
1291
1429
  await startManagedDaemonWithPersistedSettings({
@@ -1300,7 +1438,7 @@ export async function applyManagedSpec(options, deps = {}) {
1300
1438
  const status = await sendRequest(resolved.rootPath, { type: 'status' });
1301
1439
  if (!status.ok) {
1302
1440
  const managedError = getManagedErrorPayload(status);
1303
- return rollbackManagedApplyFailure(resolved.rootPath, previousSpec, materialize, clearManagedRuntimeImpl, managedOperationError(managedError.code, managedError.message));
1441
+ return rollbackManagedApplyFailure(resolved.rootPath, previousSpec, currentRuntimeSettings.persistedSettingsSnapshot ?? undefined, materializeWithCurrentRuntimeEnv, clearManagedRuntimeImpl, managedOperationError(managedError.code, managedError.message));
1304
1442
  }
1305
1443
  if (status.type !== 'status') {
1306
1444
  throw new Error('Unexpected managed daemon response for status');
@@ -1314,7 +1452,7 @@ export async function applyManagedSpec(options, deps = {}) {
1314
1452
  };
1315
1453
  }
1316
1454
  catch (error) {
1317
- return rollbackManagedApplyFailure(resolved.rootPath, previousSpec, materialize, clearManagedRuntimeImpl, error);
1455
+ return rollbackManagedApplyFailure(resolved.rootPath, previousSpec, currentRuntimeSettings.persistedSettingsSnapshot ?? undefined, materializeWithCurrentRuntimeEnv, clearManagedRuntimeImpl, error);
1318
1456
  }
1319
1457
  }
1320
1458
  finally {
@@ -1350,9 +1488,7 @@ export class ManagedAgentsHostDaemon {
1350
1488
  this.loadSpec = deps.loadSpec ?? loadLocalConfigGenerateSpec;
1351
1489
  this.materialize = deps.materialize ?? materializeLocalConfig;
1352
1490
  const managedRuntimeSettings = resolveDesiredManagedRuntimeSettings(process.env);
1353
- this.managedRuntimeFingerprint = managedRuntimeSettings.convergenceEnabled
1354
- ? managedRuntimeSettings.fingerprint
1355
- : null;
1491
+ this.managedRuntimeFingerprint = managedRuntimeSettings.fingerprint;
1356
1492
  this.logger = deps.logger ?? console;
1357
1493
  this.logPath = deps.logPath ?? null;
1358
1494
  this.server = createServer((socket) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@borgee/agents-host",
3
- "version": "0.2.28",
3
+ "version": "0.2.29",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"