@evomap/evolver-proxy 2.0.0-beta.19 → 2.0.0-beta.22

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 (43) hide show
  1. package/dist/bin/evolver-proxy.d.ts +12 -0
  2. package/dist/bin/evolver-proxy.js +257 -56
  3. package/dist/daemon/proxyDaemon.d.ts +12 -0
  4. package/dist/daemon/proxyDaemon.js +313 -18
  5. package/dist/daemon/systemdNotifier.d.ts +2 -0
  6. package/dist/daemon/systemdNotifier.js +11 -1
  7. package/dist/llm/upstream.js +54 -7
  8. package/dist/private/accountAssetCompatibility.d.ts +1 -0
  9. package/dist/private/accountAssetCompatibility.js +3 -3
  10. package/dist/private/adapterLoader.js +4 -3
  11. package/dist/router/messagesRoute.js +9 -3
  12. package/dist/router/providerRoutes.js +7 -3
  13. package/dist/selfUpdate/bootstrap.d.ts +162 -0
  14. package/dist/selfUpdate/bootstrap.js +3524 -0
  15. package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
  16. package/dist/selfUpdate/bootstrapReadiness.js +153 -0
  17. package/dist/selfUpdate/builtinKey.d.ts +4 -0
  18. package/dist/selfUpdate/builtinKey.js +16 -0
  19. package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
  20. package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
  21. package/dist/selfUpdate/executor.d.ts +18 -7
  22. package/dist/selfUpdate/executor.js +159 -59
  23. package/dist/selfUpdate/failureCodes.d.ts +4 -0
  24. package/dist/selfUpdate/failureCodes.js +7 -0
  25. package/dist/selfUpdate/index.d.ts +2 -1
  26. package/dist/selfUpdate/index.js +2 -1
  27. package/dist/selfUpdate/migration.d.ts +158 -0
  28. package/dist/selfUpdate/migration.js +2672 -0
  29. package/dist/selfUpdate/policy.d.ts +19 -2
  30. package/dist/selfUpdate/policy.js +76 -2
  31. package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
  32. package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
  33. package/dist/selfUpdate/releaseBinary.d.ts +3 -0
  34. package/dist/selfUpdate/releaseBinary.js +50 -4
  35. package/dist/selfUpdate/transaction.d.ts +8 -0
  36. package/dist/selfUpdate/transaction.js +166 -18
  37. package/dist/selfUpdate/unixController.d.ts +8 -0
  38. package/dist/selfUpdate/unixController.js +366 -38
  39. package/dist/selfUpdate/windowsController.d.ts +14 -2
  40. package/dist/selfUpdate/windowsController.js +484 -103
  41. package/dist/selfUpdate/windowsUpdater.d.ts +25 -0
  42. package/dist/selfUpdate/windowsUpdater.js +174 -7
  43. 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,4 @@
1
+ /** Ed25519 SPKI public key (base64 DER) matching the v2-beta release environment signing key. */
2
+ export declare const BUILTIN_SELF_UPDATE_PUBLIC_KEY = "MCowBQYDK2VwAyEAAgoV6aWwJd5zlxOcPqWuxkDB+isQnKydFStV8X3DxMk=";
3
+ /** Resolve the self-update verification public key: env override wins, else the built-in key. */
4
+ export declare function resolveSelfUpdatePublicKey(env?: NodeJS.ProcessEnv): string;
@@ -0,0 +1,16 @@
1
+ // Built-in Ed25519 verification public key for the official v2-beta self-update channel.
2
+ //
3
+ // A public key is not secret: embedding it lets nodes installed through a trusted
4
+ // distribution channel (npm tarball or prebuilt release binary) verify signed update
5
+ // manifests with zero per-node configuration. Trust bootstraps from the install
6
+ // channel itself (npm registry integrity / release artifact provenance), and every
7
+ // later self-update stays signature-gated.
8
+ //
9
+ // EVOLVER_SELF_UPDATE_PUBLIC_KEY overrides this when set (rotation / private fleets).
10
+ /** Ed25519 SPKI public key (base64 DER) matching the v2-beta release environment signing key. */
11
+ export const BUILTIN_SELF_UPDATE_PUBLIC_KEY = 'MCowBQYDK2VwAyEAAgoV6aWwJd5zlxOcPqWuxkDB+isQnKydFStV8X3DxMk=';
12
+ /** Resolve the self-update verification public key: env override wins, else the built-in key. */
13
+ export function resolveSelfUpdatePublicKey(env = process.env) {
14
+ const configured = env['EVOLVER_SELF_UPDATE_PUBLIC_KEY']?.trim();
15
+ return configured || BUILTIN_SELF_UPDATE_PUBLIC_KEY;
16
+ }
@@ -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
- * 1. policy off → do nothing (the default-off risk gate; a half-built channel must not auto-apply).
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.
94
+ * 1. policy off → do nothing (explicit opt-out; auto is hard-gated upstream by supervisor + public key).
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.