@evomap/evolver-proxy 2.0.2 → 2.0.8

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 (53) hide show
  1. package/dist/bin/evolver-proxy.d.ts +12 -0
  2. package/dist/bin/evolver-proxy.js +192 -59
  3. package/dist/daemon/proxyDaemon.js +10 -0
  4. package/dist/daemon/systemdNotifier.d.ts +2 -0
  5. package/dist/daemon/systemdNotifier.js +11 -1
  6. package/dist/lifecycle/claimNudge.js +1 -1
  7. package/dist/lifecycle/deployGuard.js +1 -1
  8. package/dist/lifecycle/legacyNodeId.js +1 -1
  9. package/dist/lifecycle/manager.js +1 -1
  10. package/dist/llm/bodyCapture.js +1 -1
  11. package/dist/llm/index.js +1 -1
  12. package/dist/llm/server.js +1 -1
  13. package/dist/llm/traceBackfill.js +1 -1
  14. package/dist/llm/traceConfig.js +1 -1
  15. package/dist/llm/traceControl.js +1 -1
  16. package/dist/llm/traceEnvelope.js +1 -1
  17. package/dist/llm/traceSink.js +1 -1
  18. package/dist/llm/traceUploadPayload.js +1 -1
  19. package/dist/llm/upstream.js +1 -1
  20. package/dist/router/cachePassthrough.js +1 -1
  21. package/dist/router/features.js +1 -1
  22. package/dist/router/index.js +1 -1
  23. package/dist/router/messagesRoute.js +1 -1
  24. package/dist/router/modelRouter.js +1 -1
  25. package/dist/router/providerRoutes.js +1 -1
  26. package/dist/router/sseScan.js +1 -1
  27. package/dist/selfUpdate/bootstrap.d.ts +106 -13
  28. package/dist/selfUpdate/bootstrap.js +3418 -176
  29. package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
  30. package/dist/selfUpdate/bootstrapReadiness.js +153 -0
  31. package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
  32. package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
  33. package/dist/selfUpdate/executor.d.ts +17 -6
  34. package/dist/selfUpdate/executor.js +158 -58
  35. package/dist/selfUpdate/index.d.ts +2 -1
  36. package/dist/selfUpdate/index.js +2 -1
  37. package/dist/selfUpdate/migration.d.ts +80 -15
  38. package/dist/selfUpdate/migration.js +2513 -156
  39. package/dist/selfUpdate/policy.js +2 -8
  40. package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
  41. package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
  42. package/dist/selfUpdate/releaseBinary.d.ts +3 -0
  43. package/dist/selfUpdate/releaseBinary.js +46 -3
  44. package/dist/selfUpdate/transaction.d.ts +8 -0
  45. package/dist/selfUpdate/transaction.js +166 -18
  46. package/dist/selfUpdate/unixController.d.ts +8 -0
  47. package/dist/selfUpdate/unixController.js +364 -38
  48. package/dist/selfUpdate/windowsController.d.ts +14 -2
  49. package/dist/selfUpdate/windowsController.js +482 -103
  50. package/dist/selfUpdate/windowsUpdater.d.ts +25 -0
  51. package/dist/selfUpdate/windowsUpdater.js +174 -7
  52. package/dist/sync/engine.js +1 -1
  53. package/package.json +3 -3
@@ -0,0 +1,9 @@
1
+ import { util } from '@evomap/evolver-core';
2
+ export declare function publishLifecycleBootstrapReadiness(input: {
3
+ env: NodeJS.ProcessEnv;
4
+ pid?: number;
5
+ supervisorPid?: number;
6
+ readProcessStartIdentity?: typeof util.readFileLockProcessStartIdentity;
7
+ startedAt: string;
8
+ ipcUrl: string;
9
+ }): void;
@@ -0,0 +1,153 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, lstatSync, openSync, readSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
3
+ import { basename, dirname, join } from 'node:path';
4
+ import { bootstrap as coreBootstrap, util } from '@evomap/evolver-core';
5
+ import { resolveBootstrapStateDir } from './bootstrap.js';
6
+ const MAX_READINESS_BYTES = 16 * 1024;
7
+ function isErrno(error, code) {
8
+ return typeof error === 'object' && error !== null && error.code === code;
9
+ }
10
+ function syncDirectory(path) {
11
+ let descriptor;
12
+ try {
13
+ descriptor = openSync(path, constants.O_RDONLY);
14
+ fsyncSync(descriptor);
15
+ }
16
+ catch (error) {
17
+ if (isErrno(error, 'EINVAL'))
18
+ return;
19
+ if (process.platform === 'win32' && (isErrno(error, 'EPERM') || isErrno(error, 'EACCES')))
20
+ return;
21
+ throw error;
22
+ }
23
+ finally {
24
+ if (descriptor !== undefined)
25
+ closeSync(descriptor);
26
+ }
27
+ }
28
+ function readExistingReadiness(path) {
29
+ try {
30
+ const before = lstatSync(path, { bigint: true });
31
+ if (!before.isFile() || before.isSymbolicLink() || before.size > BigInt(MAX_READINESS_BYTES)
32
+ || before.dev <= 0n || before.ino <= 0n) {
33
+ throw new Error('bootstrap readiness path is not a bounded regular file');
34
+ }
35
+ const descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
36
+ try {
37
+ const opened = fstatSync(descriptor, { bigint: true });
38
+ if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino) {
39
+ throw new Error('bootstrap readiness path changed while opening');
40
+ }
41
+ const bytes = Buffer.alloc(MAX_READINESS_BYTES + 1);
42
+ let offset = 0;
43
+ while (offset < bytes.length) {
44
+ const count = readSync(descriptor, bytes, offset, bytes.length - offset, null);
45
+ if (count === 0)
46
+ break;
47
+ offset += count;
48
+ }
49
+ if (offset > MAX_READINESS_BYTES) {
50
+ throw new Error('bootstrap readiness path is not a bounded regular file');
51
+ }
52
+ const raw = bytes.subarray(0, offset).toString('utf8');
53
+ const after = fstatSync(descriptor, { bigint: true });
54
+ if (after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size
55
+ || after.mtimeNs !== opened.mtimeNs || after.ctimeNs !== opened.ctimeNs) {
56
+ throw new Error('bootstrap readiness path changed while reading');
57
+ }
58
+ const parsed = coreBootstrap.parseLifecycleBootstrapReadinessJson(raw);
59
+ if (!parsed)
60
+ throw new Error('bootstrap readiness receipt is corrupt');
61
+ return parsed;
62
+ }
63
+ finally {
64
+ closeSync(descriptor);
65
+ }
66
+ }
67
+ catch (error) {
68
+ if (isErrno(error, 'ENOENT'))
69
+ return undefined;
70
+ throw error;
71
+ }
72
+ }
73
+ export function publishLifecycleBootstrapReadiness(input) {
74
+ const transactionId = input.env[coreBootstrap.LIFECYCLE_BOOTSTRAP_TRANSACTION_ENV]?.trim();
75
+ if (!transactionId)
76
+ return;
77
+ const pid = input.pid ?? process.pid;
78
+ const supervisorPid = input.supervisorPid ?? process.ppid;
79
+ const readProcessStartIdentity = input.readProcessStartIdentity
80
+ ?? util.readFileLockProcessStartIdentity;
81
+ const pidProcessStartIdentity = readProcessStartIdentity(pid);
82
+ const supervisorProcessStartIdentity = readProcessStartIdentity(supervisorPid);
83
+ if (!pidProcessStartIdentity || !supervisorProcessStartIdentity) {
84
+ throw new Error('lifecycle bootstrap readiness process identity is unavailable');
85
+ }
86
+ const readiness = {
87
+ schema: coreBootstrap.LIFECYCLE_BOOTSTRAP_READINESS_SCHEMA,
88
+ transactionId,
89
+ pid,
90
+ pidProcessStartIdentity,
91
+ supervisorPid,
92
+ supervisorProcessStartIdentity,
93
+ startedAt: input.startedAt,
94
+ ipcUrl: input.ipcUrl,
95
+ };
96
+ if (!coreBootstrap.parseLifecycleBootstrapReadiness(readiness)) {
97
+ throw new Error('invalid lifecycle bootstrap readiness receipt');
98
+ }
99
+ const stateDir = resolveBootstrapStateDir(input.env);
100
+ const state = lstatSync(stateDir);
101
+ if (!state.isDirectory() || state.isSymbolicLink()) {
102
+ throw new Error('lifecycle bootstrap state directory is not trusted');
103
+ }
104
+ if (process.platform !== 'win32') {
105
+ const uid = typeof process.getuid === 'function' ? process.getuid() : undefined;
106
+ if ((uid !== undefined && state.uid !== uid) || (state.mode & 0o077) !== 0) {
107
+ throw new Error('lifecycle bootstrap state directory is not owner-only');
108
+ }
109
+ }
110
+ const path = join(stateDir, coreBootstrap.LIFECYCLE_BOOTSTRAP_READINESS_FILE);
111
+ const lockPath = join(stateDir, coreBootstrap.LIFECYCLE_BOOTSTRAP_READINESS_LOCK_FILE);
112
+ util.acquireLock(lockPath, { maxTries: 500, waitMs: 10 });
113
+ let publicationError;
114
+ let publicationFailed = false;
115
+ try {
116
+ const existing = readExistingReadiness(path);
117
+ if (existing && existing.transactionId !== transactionId) {
118
+ throw new Error('lifecycle bootstrap readiness is owned by another transaction');
119
+ }
120
+ const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
121
+ let descriptor;
122
+ try {
123
+ descriptor = openSync(temporary, 'wx', 0o600);
124
+ writeFileSync(descriptor, `${JSON.stringify(readiness)}\n`, { encoding: 'utf8' });
125
+ fchmodSync(descriptor, 0o600);
126
+ fsyncSync(descriptor);
127
+ closeSync(descriptor);
128
+ descriptor = undefined;
129
+ renameSync(temporary, path);
130
+ syncDirectory(stateDir);
131
+ }
132
+ finally {
133
+ if (descriptor !== undefined)
134
+ closeSync(descriptor);
135
+ rmSync(temporary, { force: true });
136
+ }
137
+ }
138
+ catch (error) {
139
+ publicationError = error;
140
+ publicationFailed = true;
141
+ }
142
+ const released = util.releaseLock(lockPath);
143
+ if (!released.released
144
+ || (released.reason !== 'released' && released.reason !== 'released_with_cleanup_error')) {
145
+ throw new Error(`lifecycle bootstrap readiness lock release failed: ${released.reason}`, {
146
+ cause: publicationFailed
147
+ ? new AggregateError([publicationError, new Error(released.reason)])
148
+ : undefined,
149
+ });
150
+ }
151
+ if (publicationFailed)
152
+ throw publicationError;
153
+ }
@@ -0,0 +1,45 @@
1
+ import type { util } from '@evomap/evolver-core';
2
+ import { type PreparedRecoveryChildStartGate, type RecoveryChildStartGateRole } from './recoveryChildStartGate.js';
3
+ interface RecoveryControllerLifecycleLease {
4
+ readonly owner: util.FileLockOwnerRecord;
5
+ assertOwned(): void;
6
+ armProcess(pid: number): util.FileLockOwnerRecord;
7
+ disarmProcess(): void;
8
+ retainProcess(): void;
9
+ transferToProcess(pid: number): util.FileLockOwnerRecord;
10
+ release(): void;
11
+ }
12
+ type RecoveryControllerLifecycleLeaseAcquirer = (env: NodeJS.ProcessEnv, options: {
13
+ maxTries: number;
14
+ waitMs: number;
15
+ }) => RecoveryControllerLifecycleLease;
16
+ type SupervisedActivationDelegationAssertion = (env: NodeJS.ProcessEnv) => void;
17
+ interface RecoveryControllerPreparedOwnerCapability {
18
+ env: NodeJS.ProcessEnv;
19
+ startupAckToken: string;
20
+ }
21
+ interface RecoveryControllerPreparedChild {
22
+ env: NodeJS.ProcessEnv;
23
+ startupAckToken?: string;
24
+ startupGateToken: string;
25
+ }
26
+ type RecoveryControllerChildCapabilityFactory = (env: NodeJS.ProcessEnv, owner: util.FileLockOwnerRecord) => RecoveryControllerPreparedOwnerCapability;
27
+ type RecoveryControllerStartGateFactory = (env: NodeJS.ProcessEnv, role: RecoveryChildStartGateRole, expectedParent?: Pick<util.FileLockOwnerRecord, 'pid' | 'processStartIdentity'>) => PreparedRecoveryChildStartGate;
28
+ export interface RecoveryControllerAuthorityDependencies {
29
+ acquireOwnerLease?: RecoveryControllerLifecycleLeaseAcquirer;
30
+ assertActivationDelegation?: SupervisedActivationDelegationAssertion;
31
+ prepareOwnerCapability?: RecoveryControllerChildCapabilityFactory;
32
+ prepareStartGate?: RecoveryControllerStartGateFactory;
33
+ }
34
+ export interface RecoveryControllerAuthority {
35
+ readonly kind: 'owned' | 'delegated';
36
+ assertAuthorized(): void;
37
+ prepareTarget(env: NodeJS.ProcessEnv): RecoveryControllerPreparedChild;
38
+ prepareWorker(env: NodeJS.ProcessEnv): PreparedRecoveryChildStartGate;
39
+ armProcess(pid: number): void;
40
+ disarmProcess(): void;
41
+ retainProcess(): void;
42
+ release(): void;
43
+ }
44
+ export declare function resolveRecoveryControllerAuthority(env: NodeJS.ProcessEnv, dependencies?: RecoveryControllerAuthorityDependencies): Promise<RecoveryControllerAuthority>;
45
+ export {};
@@ -0,0 +1,61 @@
1
+ import { prepareRecoveryChildStartGate, } from './recoveryChildStartGate.js';
2
+ export async function resolveRecoveryControllerAuthority(env, dependencies = {}) {
3
+ const bootstrap = dependencies.acquireOwnerLease
4
+ && dependencies.assertActivationDelegation
5
+ && dependencies.prepareOwnerCapability
6
+ ? undefined
7
+ : await import('./bootstrap.js');
8
+ const acquireOwnerLease = dependencies.acquireOwnerLease
9
+ ?? bootstrap.acquireLifecycleBootstrapOwnerLease;
10
+ const assertActivationDelegation = dependencies.assertActivationDelegation
11
+ ?? bootstrap.assertActiveSupervisedLifecycleBootstrapDelegation;
12
+ const prepareOwnerCapability = dependencies.prepareOwnerCapability
13
+ ?? bootstrap.prepareRecoveryControllerLifecycleOwnerCapability;
14
+ const prepareStartGate = dependencies.prepareStartGate ?? prepareRecoveryChildStartGate;
15
+ try {
16
+ const lease = acquireOwnerLease(env, { maxTries: 2, waitMs: 0 });
17
+ return {
18
+ kind: 'owned',
19
+ assertAuthorized: () => { lease.assertOwned(); },
20
+ prepareTarget: (childEnv) => {
21
+ lease.assertOwned();
22
+ const ownerCapability = prepareOwnerCapability(childEnv, lease.owner);
23
+ const startGate = prepareStartGate(ownerCapability.env, 'proxy-target', lease.owner);
24
+ return { ...ownerCapability, ...startGate };
25
+ },
26
+ prepareWorker: (childEnv) => {
27
+ lease.assertOwned();
28
+ return prepareStartGate(childEnv, 'windows-updater', lease.owner);
29
+ },
30
+ armProcess: (pid) => { lease.armProcess(pid); },
31
+ disarmProcess: () => { lease.disarmProcess(); },
32
+ retainProcess: () => { lease.retainProcess(); },
33
+ release: () => { lease.release(); },
34
+ };
35
+ }
36
+ catch (leaseError) {
37
+ try {
38
+ assertActivationDelegation(env);
39
+ }
40
+ catch (delegationError) {
41
+ throw new AggregateError([leaseError, delegationError], 'self_update_recovery_controller_authority_unavailable');
42
+ }
43
+ return {
44
+ kind: 'delegated',
45
+ assertAuthorized: () => { assertActivationDelegation(env); },
46
+ prepareTarget: (childEnv) => {
47
+ assertActivationDelegation(env);
48
+ return prepareStartGate(childEnv, 'proxy-target');
49
+ },
50
+ prepareWorker: () => {
51
+ throw new Error('self_update_recovery_controller_owner_lease_required');
52
+ },
53
+ armProcess: () => {
54
+ throw new Error('self_update_recovery_controller_delegated_guardian_unavailable');
55
+ },
56
+ disarmProcess: () => { },
57
+ retainProcess: () => { },
58
+ release: () => { },
59
+ };
60
+ }
61
+ }
@@ -23,6 +23,8 @@ export interface SelfUpdateResult {
23
23
  appliedVia?: 'binary' | 'tarball';
24
24
  /** Durable installs are not successful until the relaunched daemon completes startup health checks. */
25
25
  confirmationPending?: true;
26
+ /** A bounded integrity warning from cleanup that did not change the primary update outcome. */
27
+ cleanupWarning?: string;
26
28
  }
27
29
  /** The hub's force_update directive (inbound message payload). */
28
30
  export interface ForceUpdateDirective {
@@ -60,6 +62,8 @@ export interface SelfUpdateDeps {
60
62
  policy: 'off' | 'prompt' | 'auto';
61
63
  /** Current installed version (read from package.json by the caller). */
62
64
  currentVersion: string;
65
+ /** Lazy cross-process lease that serializes self-update with lifecycle mutations. */
66
+ acquireLifecycleLease?: () => Promise<SelfUpdateLifecycleLease> | SelfUpdateLifecycleLease;
63
67
  /** Resolve a trusted manifest when the hub directive does not carry one. */
64
68
  resolveManifest?: (directive: ForceUpdateDirective, currentVersion: string) => Promise<unknown> | unknown;
65
69
  /** Download the staged release for the target version. Throws/rejects on failure. */
@@ -74,19 +78,26 @@ export interface SelfUpdateDeps {
74
78
  publicKey?: string;
75
79
  /** Best-effort telemetry sink for the structured outcome (never throws into the update path). */
76
80
  onTelemetry?: (result: SelfUpdateResult) => void;
81
+ /** One-shot operator warning sink for a lifecycle lease cleanup integrity failure. */
82
+ onCleanupWarning?: (warning: string, result: SelfUpdateResult) => void;
83
+ }
84
+ export interface SelfUpdateLifecycleLease {
85
+ assertOwned(): Promise<void> | void;
86
+ release(): Promise<void> | void;
77
87
  }
78
88
  /** Test-only: reset the mutex between cases. Not part of the public update path. */
79
89
  export declare function _resetSelfUpdateMutex(): void;
80
90
  /**
81
- * Execute a force_update directive end to end: decide(mutex) → download → VERIFY → atomic replace → restart.
91
+ * Execute a force_update directive end to end: fast exits leaseresolve/decide → download → VERIFY → replace → restart.
82
92
  *
83
93
  * Order is load-bearing:
84
94
  * 1. policy off → do nothing (explicit opt-out; auto is hard-gated upstream by supervisor + public key).
85
- * 2. decideUpdate (pure): reject bad manifests, NOOP when already satisfied (no download, no restart).
86
- * 3. mutex: exactly one execution; concurrent callers get `already_in_progress` and touch no disk.
87
- * 4. download the staged release.
88
- * 5. verifySelectedManifestArtifact (pure) THE GATE. Fail → no write, no restart.
89
- * 6. atomicReplace, then restart(). Only reached after verification passed.
95
+ * 2. reject malformed/already-satisfied required versions without acquiring the lifecycle owner lease.
96
+ * 3. mutex + lifecycle owner lease: exactly one executor may resolve or mutate release state.
97
+ * 4. resolve and decideUpdate (pure): reject bad manifests or NOOP under the held lease.
98
+ * 5. download the staged release.
99
+ * 6. verifySelectedManifestArtifact (pure) — THE GATE. Fail no write, no restart.
100
+ * 7. atomicReplace, then restart(). Only reached after verification passed.
90
101
  *
91
102
  * Never throws: every failure becomes a structured SelfUpdateResult so the daemon can report it and keep running
92
103
  * on the old (intact) version.