@evomap/evolver-proxy 2.0.0-beta.9 → 2.0.0
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/dist/bin/evolver-proxy.d.ts +26 -2
- package/dist/bin/evolver-proxy.js +284 -51
- package/dist/daemon/atpConsent.js +5 -2
- package/dist/daemon/collaborationFacade.js +23 -13
- package/dist/daemon/proxyDaemon.d.ts +52 -0
- package/dist/daemon/proxyDaemon.js +1067 -24
- package/dist/daemon/publishRecallVerifier.d.ts +114 -0
- package/dist/daemon/publishRecallVerifier.js +495 -0
- package/dist/daemon/selectHub.js +5 -3
- package/dist/daemon/systemdNotifier.d.ts +46 -0
- package/dist/daemon/systemdNotifier.js +153 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/lifecycle/claimNudge.d.ts +20 -0
- package/dist/lifecycle/claimNudge.js +124 -0
- package/dist/lifecycle/manager.d.ts +4 -0
- package/dist/lifecycle/manager.js +15 -2
- package/dist/llm/server.js +24 -4
- package/dist/llm/upstream.d.ts +5 -1
- package/dist/llm/upstream.js +24 -1
- package/dist/private/accountAssetCompatibility.d.ts +29 -0
- package/dist/private/accountAssetCompatibility.js +196 -0
- package/dist/private/adapterLoader.d.ts +21 -1
- package/dist/private/adapterLoader.js +242 -7
- package/dist/private/nodeCredentialStore.d.ts +23 -0
- package/dist/private/nodeCredentialStore.js +210 -0
- package/dist/selfUpdate/bootstrap.d.ts +69 -0
- package/dist/selfUpdate/bootstrap.js +282 -0
- package/dist/selfUpdate/builtinKey.d.ts +4 -0
- package/dist/selfUpdate/builtinKey.js +16 -0
- package/dist/selfUpdate/executor.d.ts +1 -1
- package/dist/selfUpdate/executor.js +1 -1
- package/dist/selfUpdate/failureCodes.d.ts +4 -0
- package/dist/selfUpdate/failureCodes.js +7 -0
- package/dist/selfUpdate/migration.d.ts +93 -0
- package/dist/selfUpdate/migration.js +315 -0
- package/dist/selfUpdate/policy.d.ts +19 -2
- package/dist/selfUpdate/policy.js +82 -2
- package/dist/selfUpdate/releaseBinary.js +4 -1
- package/dist/sync/engine.d.ts +12 -0
- package/dist/sync/engine.js +255 -64
- package/package.json +7 -4
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { ops } from '@evomap/evolver-core';
|
|
3
|
+
import type { DownloadResult, ForceUpdateDirective } from './executor.js';
|
|
4
|
+
import { type ReleaseBinaryOptions } from './releaseBinary.js';
|
|
5
|
+
import { type StagedBinaryProbe } from './transaction.js';
|
|
6
|
+
type DownloadedArtifact = ops.DownloadedArtifact;
|
|
7
|
+
type VerifyResult = ops.VerifyResult;
|
|
8
|
+
type FetchFn = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
9
|
+
/** Advisory migration state marker written next to the bootstrap attempt marker. */
|
|
10
|
+
export declare const MIGRATION_STATE_FILE = "migration.json";
|
|
11
|
+
/**
|
|
12
|
+
* The CLI-side service activation itself spawns up to ~60s (lifecycle powershell/schtasks
|
|
13
|
+
* activation), so a shorter timeout would kill the child mid-activation and leave the
|
|
14
|
+
* installed binary registered as failed. Mirrors the bootstrap timeout rationale.
|
|
15
|
+
*/
|
|
16
|
+
export declare const MIGRATION_REGISTER_TIMEOUT_MS = 90000;
|
|
17
|
+
/** Minimal stat shape migration needs (symlink guard + regular-file check). */
|
|
18
|
+
export interface MigrationFileStat {
|
|
19
|
+
isFile(): boolean;
|
|
20
|
+
isSymbolicLink(): boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface MigrationOptions {
|
|
23
|
+
/** Arch override for release asset resolution (defaults to process.arch). */
|
|
24
|
+
arch?: NodeJS.Architecture;
|
|
25
|
+
exists?: (path: string) => boolean;
|
|
26
|
+
/** Sync text read (bootstrap.ts mirror); used for container detection. */
|
|
27
|
+
readFile?: (path: string) => string;
|
|
28
|
+
/** Sync text write for advisory state markers (attempt marker + migration.json). */
|
|
29
|
+
writeFile?: (path: string, content: string) => void;
|
|
30
|
+
/** Async binary read of the staged artifact (install copy). */
|
|
31
|
+
readBinary?: (path: string) => Promise<Buffer>;
|
|
32
|
+
/** Async binary write with mode (install tmp copy). */
|
|
33
|
+
writeBinary?: (path: string, content: Buffer, mode: number) => Promise<void>;
|
|
34
|
+
/** Recursive mkdir with mode (install dest dir). */
|
|
35
|
+
mkdir?: (path: string, mode: number) => Promise<void>;
|
|
36
|
+
/** Force/recursive removal (staged tmp dir, leftover tmp copies). */
|
|
37
|
+
rm?: (path: string) => Promise<void>;
|
|
38
|
+
rename?: (from: string, to: string) => Promise<void>;
|
|
39
|
+
chmod?: (path: string, mode: number) => Promise<void>;
|
|
40
|
+
/** lstat-shaped stat for the staged-artifact symlink guard. */
|
|
41
|
+
stat?: (path: string) => Promise<MigrationFileStat>;
|
|
42
|
+
fetchFn?: FetchFn;
|
|
43
|
+
/** Probe used by the default preflight (real execution of `--version` / `proxy --help`). */
|
|
44
|
+
probe?: StagedBinaryProbe;
|
|
45
|
+
spawnFn?: typeof spawn;
|
|
46
|
+
now?: number;
|
|
47
|
+
execPath?: string;
|
|
48
|
+
/** Effective uid (tests / platforms without process.getuid). */
|
|
49
|
+
uid?: number | undefined;
|
|
50
|
+
/** Register-step timeout override (defaults to MIGRATION_REGISTER_TIMEOUT_MS). */
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
/** High-level seam: download leg (defaults to downloadGithubReleaseArtifact). */
|
|
53
|
+
downloadFn?: (targetVersion: string, directive: ForceUpdateDirective, opts: ReleaseBinaryOptions) => Promise<DownloadResult>;
|
|
54
|
+
/** High-level seam: manifest verification (defaults to ops.verifySelectedManifestArtifact). */
|
|
55
|
+
verifyFn?: (manifest: unknown, downloaded: readonly DownloadedArtifact[], publicKey: string) => VerifyResult;
|
|
56
|
+
/** High-level seam: preflight (defaults to preflightManagedStagedBinary with options.probe). */
|
|
57
|
+
preflightFn?: (targetPath: string, expectedVersion: string) => Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
export interface MigrationResult {
|
|
60
|
+
outcome: 'migrated' | 'skipped' | 'failed';
|
|
61
|
+
/**
|
|
62
|
+
* Structured reason: 'migrated' / 'disabled' / 'root_user' / 'ci_environment' /
|
|
63
|
+
* 'container_environment' / 'cooldown' / 'unsupported_platform' /
|
|
64
|
+
* 'invalid_version_override' / 'version_unresolvable' /
|
|
65
|
+
* 'migration_download_failed:<detail>' / 'migration_verify_failed:<reason>' /
|
|
66
|
+
* 'migration_install_failed:<detail>' / 'migration_register_failed:<detail>' /
|
|
67
|
+
* 'migration_register_timeout'.
|
|
68
|
+
*/
|
|
69
|
+
reason: string;
|
|
70
|
+
destPath?: string;
|
|
71
|
+
/** Operator-facing message (short phrase for skipped/failed; full line for migrated). */
|
|
72
|
+
message: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Migration install home — mirrors the CLI-side lifecyclePaths home resolution
|
|
76
|
+
* (kept dependency-free across packages): EVOLVER_HOME ?? EVOMAP_HOME ?? ~/.evomap.
|
|
77
|
+
*/
|
|
78
|
+
export declare function resolveMigrationHome(env: NodeJS.ProcessEnv): string;
|
|
79
|
+
/** Migration install path for this platform: <home>/bin/<releaseAssetName>. Throws on unsupported platforms. */
|
|
80
|
+
export declare function resolveMigrationDestPath(env: NodeJS.ProcessEnv, platform?: NodeJS.Platform, arch?: NodeJS.Architecture): string;
|
|
81
|
+
/**
|
|
82
|
+
* Resolve the migration target version: EVOLVER_BOOTSTRAP_MIGRATION_VERSION override
|
|
83
|
+
* (normalized through the self-update version contract) wins, else the current package
|
|
84
|
+
* version. Returns undefined when nothing normalizes to a concrete semver.
|
|
85
|
+
*/
|
|
86
|
+
export declare function resolveMigrationVersion(env: NodeJS.ProcessEnv): string | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* One-time migration of the npm/JS install shape to the standalone release binary.
|
|
89
|
+
* Never throws: every failure/skip becomes a structured MigrationResult so degraded
|
|
90
|
+
* startup can keep running with self-update off.
|
|
91
|
+
*/
|
|
92
|
+
export declare function migrateToStandaloneBinary(env: NodeJS.ProcessEnv, platform?: NodeJS.Platform, options?: MigrationOptions): Promise<MigrationResult>;
|
|
93
|
+
export {};
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// One-time npm/JS → standalone binary migration for the DEFAULT self-update policy.
|
|
2
|
+
//
|
|
3
|
+
// The npm/JS install shape has no replaceable standalone binary target, so the supervision
|
|
4
|
+
// bootstrap (bootstrap.ts) refuses to register a supervised instance for it (it would crash
|
|
5
|
+
// at self-update target resolution on every startup). Instead, the first degraded startup
|
|
6
|
+
// may ONCE download the signed standalone release binary for this platform into the user's
|
|
7
|
+
// evolver home (`<home>/bin`, mirroring the CLI-side lifecyclePaths home) and hand over:
|
|
8
|
+
//
|
|
9
|
+
// RESOLVE (version / asset name / dest path)
|
|
10
|
+
// DOWNLOAD (signed manifest + binary staged under tmpdir)
|
|
11
|
+
// VERIFY (ed25519 manifest signature + sha256 + real preflight probe)
|
|
12
|
+
// INSTALL (atomic copy into <home>/bin; unverified bytes are never written)
|
|
13
|
+
// REGISTER (spawn `evolver lifecycle bootstrap` with EVOLVER_SELF_UPDATE_TARGET_PATH=dest)
|
|
14
|
+
//
|
|
15
|
+
// Migration is a convenience, never an escalation: it is skipped when disabled
|
|
16
|
+
// (EVOLVER_BOOTSTRAP_MIGRATION=0|off), for root (non-win32), CI, containers, within a
|
|
17
|
+
// bootstrap-failure cooldown window, and on unsupported platforms. Every failure degrades
|
|
18
|
+
// back to the existing 'off + warning' startup and records a bootstrap attempt marker so
|
|
19
|
+
// the cooldown applies. All I/O is injectable for tests.
|
|
20
|
+
import { spawn } from 'node:child_process';
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { chmod as fsChmod, lstat, mkdir as fsMkdir, readFile as fsReadFile, rename as fsRename, rm as fsRm, writeFile as fsWriteFile, } from 'node:fs/promises';
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
import { dirname, join } from 'node:path';
|
|
25
|
+
import { ops } from '@evomap/evolver-core';
|
|
26
|
+
import { SELF_UPDATE_FAILURE_CODES } from './failureCodes.js';
|
|
27
|
+
import { resolveSelfUpdatePublicKey } from './builtinKey.js';
|
|
28
|
+
import { downloadGithubReleaseArtifact, releaseAssetName, resolveGithubReleaseManifest, } from './releaseBinary.js';
|
|
29
|
+
import { preflightManagedStagedBinary } from './transaction.js';
|
|
30
|
+
import { getCurrentVersion } from './version.js';
|
|
31
|
+
import { looksLikeContainer, recentBootstrapFailure, recordBootstrapAttempt, resolveBootstrapCliInvocation, resolveBootstrapStateDir, } from './bootstrap.js';
|
|
32
|
+
/** Advisory migration state marker written next to the bootstrap attempt marker. */
|
|
33
|
+
export const MIGRATION_STATE_FILE = 'migration.json';
|
|
34
|
+
/**
|
|
35
|
+
* The CLI-side service activation itself spawns up to ~60s (lifecycle powershell/schtasks
|
|
36
|
+
* activation), so a shorter timeout would kill the child mid-activation and leave the
|
|
37
|
+
* installed binary registered as failed. Mirrors the bootstrap timeout rationale.
|
|
38
|
+
*/
|
|
39
|
+
export const MIGRATION_REGISTER_TIMEOUT_MS = 90_000;
|
|
40
|
+
/**
|
|
41
|
+
* Migration install home — mirrors the CLI-side lifecyclePaths home resolution
|
|
42
|
+
* (kept dependency-free across packages): EVOLVER_HOME ?? EVOMAP_HOME ?? ~/.evomap.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveMigrationHome(env) {
|
|
45
|
+
return env['EVOLVER_HOME'] ?? env['EVOMAP_HOME'] ?? join(homedir(), '.evomap');
|
|
46
|
+
}
|
|
47
|
+
/** Migration install path for this platform: <home>/bin/<releaseAssetName>. Throws on unsupported platforms. */
|
|
48
|
+
export function resolveMigrationDestPath(env, platform = process.platform, arch = process.arch) {
|
|
49
|
+
return join(resolveMigrationHome(env), 'bin', releaseAssetName(platform, arch));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the migration target version: EVOLVER_BOOTSTRAP_MIGRATION_VERSION override
|
|
53
|
+
* (normalized through the self-update version contract) wins, else the current package
|
|
54
|
+
* version. Returns undefined when nothing normalizes to a concrete semver.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveMigrationVersion(env) {
|
|
57
|
+
const override = env['EVOLVER_BOOTSTRAP_MIGRATION_VERSION']?.trim();
|
|
58
|
+
if (override)
|
|
59
|
+
return ops.normalizeRequiredVersion(override);
|
|
60
|
+
return ops.normalizeConcreteVersion(getCurrentVersion());
|
|
61
|
+
}
|
|
62
|
+
const defaultReadTextFile = (path) => readFileSync(path, 'utf8');
|
|
63
|
+
const defaultWriteTextFile = (path, content) => {
|
|
64
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
65
|
+
writeFileSync(path, content, { encoding: 'utf8', mode: 0o600 });
|
|
66
|
+
};
|
|
67
|
+
function resolveProcessUid() {
|
|
68
|
+
const getuid = process.getuid;
|
|
69
|
+
return typeof getuid === 'function' ? getuid.call(process) : undefined;
|
|
70
|
+
}
|
|
71
|
+
/** Best-effort advisory migration state log; never throws — bookkeeping must not break startup. */
|
|
72
|
+
function writeMigrationState(env, record, options) {
|
|
73
|
+
try {
|
|
74
|
+
const payload = { ...record, attemptedAt: new Date(options.now ?? Date.now()).toISOString() };
|
|
75
|
+
const write = options.writeFile ?? defaultWriteTextFile;
|
|
76
|
+
write(join(resolveBootstrapStateDir(env), MIGRATION_STATE_FILE), `${JSON.stringify(payload)}\n`);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// State log is advisory; startup continues regardless.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function rmSafe(rm, path) {
|
|
83
|
+
try {
|
|
84
|
+
await rm(path);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Best-effort cleanup only.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function errorDetail(err) {
|
|
91
|
+
if (err instanceof Error)
|
|
92
|
+
return err.message;
|
|
93
|
+
return String(err);
|
|
94
|
+
}
|
|
95
|
+
/** Spawn `evolver lifecycle bootstrap` with the migration target bound; bounded timeout. */
|
|
96
|
+
function registerMigratedBinary(env, destPath, options, exists) {
|
|
97
|
+
const invocation = resolveBootstrapCliInvocation({ execPath: options.execPath, exists });
|
|
98
|
+
if (!invocation)
|
|
99
|
+
return Promise.resolve({ ok: false, reason: 'cli_not_found' });
|
|
100
|
+
// resolveBootstrapCliInvocation already targets `lifecycle bootstrap`; normalize the tail
|
|
101
|
+
// explicitly while preserving any CLI entry prefix (node <cli.js> ...).
|
|
102
|
+
const args = [...invocation.args.slice(0, Math.max(0, invocation.args.length - 2)), 'lifecycle', 'bootstrap'];
|
|
103
|
+
const spawnFn = options.spawnFn ?? spawn;
|
|
104
|
+
const childEnv = { ...env, EVOLVER_SELF_UPDATE_TARGET_PATH: destPath };
|
|
105
|
+
const timeoutMs = options.timeoutMs ?? MIGRATION_REGISTER_TIMEOUT_MS;
|
|
106
|
+
return new Promise((resolvePromise) => {
|
|
107
|
+
let child;
|
|
108
|
+
try {
|
|
109
|
+
const spawnOptions = { stdio: 'ignore', windowsHide: true, env: childEnv };
|
|
110
|
+
child = spawnFn(invocation.command, args, spawnOptions);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
resolvePromise({ ok: false, reason: 'failed', detail: errorDetail(error) });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
let settled = false;
|
|
117
|
+
const settle = (outcome) => {
|
|
118
|
+
if (settled)
|
|
119
|
+
return;
|
|
120
|
+
settled = true;
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
resolvePromise(outcome);
|
|
123
|
+
};
|
|
124
|
+
const timer = setTimeout(() => {
|
|
125
|
+
child.kill();
|
|
126
|
+
settle({ ok: false, reason: 'timeout' });
|
|
127
|
+
}, timeoutMs);
|
|
128
|
+
child.once('error', (error) => {
|
|
129
|
+
settle({ ok: false, reason: 'failed', detail: error.message });
|
|
130
|
+
});
|
|
131
|
+
child.once('exit', (code) => {
|
|
132
|
+
if (code === 0)
|
|
133
|
+
settle({ ok: true, reason: 'registered' });
|
|
134
|
+
else
|
|
135
|
+
settle({ ok: false, reason: 'failed', detail: `exit ${code ?? 'null'}` });
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* One-time migration of the npm/JS install shape to the standalone release binary.
|
|
141
|
+
* Never throws: every failure/skip becomes a structured MigrationResult so degraded
|
|
142
|
+
* startup can keep running with self-update off.
|
|
143
|
+
*/
|
|
144
|
+
export async function migrateToStandaloneBinary(env, platform = process.platform, options = {}) {
|
|
145
|
+
const now = options.now ?? Date.now();
|
|
146
|
+
const exists = options.exists ?? existsSync;
|
|
147
|
+
const readText = options.readFile ?? defaultReadTextFile;
|
|
148
|
+
const recordOptions = { now, ...(options.writeFile ? { writeFile: options.writeFile } : {}) };
|
|
149
|
+
const skipped = (reason, message) => {
|
|
150
|
+
writeMigrationState(env, { state: 'skipped', reason }, options);
|
|
151
|
+
return { outcome: 'skipped', reason, message };
|
|
152
|
+
};
|
|
153
|
+
// Gate order: switch → root → CI → container → cooldown. shouldBootstrap returns
|
|
154
|
+
// unsupported_install_shape BEFORE its own CI/container checks, so migration must
|
|
155
|
+
// re-check every environment guard itself.
|
|
156
|
+
const migrationSwitch = env['EVOLVER_BOOTSTRAP_MIGRATION']?.trim();
|
|
157
|
+
if (migrationSwitch === '0' || migrationSwitch === 'off') {
|
|
158
|
+
return skipped('disabled', 'one-time standalone migration disabled (EVOLVER_BOOTSTRAP_MIGRATION)');
|
|
159
|
+
}
|
|
160
|
+
const uid = options.uid ?? resolveProcessUid();
|
|
161
|
+
if (platform !== 'win32' && uid === 0) {
|
|
162
|
+
return skipped('root_user', 'one-time standalone migration skipped for root');
|
|
163
|
+
}
|
|
164
|
+
const ci = env['CI']?.trim();
|
|
165
|
+
if (ci && ci.toLowerCase() !== 'false' && ci !== '0') {
|
|
166
|
+
return skipped('ci_environment', 'one-time standalone migration skipped in CI');
|
|
167
|
+
}
|
|
168
|
+
if (platform === 'linux' && looksLikeContainer(exists, readText)) {
|
|
169
|
+
return skipped('container_environment', 'one-time standalone migration skipped in a container');
|
|
170
|
+
}
|
|
171
|
+
if (recentBootstrapFailure(env, readText, now)) {
|
|
172
|
+
return skipped('cooldown', 'one-time standalone migration skipped (recent bootstrap failure cooldown)');
|
|
173
|
+
}
|
|
174
|
+
// RESOLVE: version, asset name, dest path.
|
|
175
|
+
const version = resolveMigrationVersion(env);
|
|
176
|
+
if (!version) {
|
|
177
|
+
const overrideSet = Boolean(env['EVOLVER_BOOTSTRAP_MIGRATION_VERSION']?.trim());
|
|
178
|
+
const reason = overrideSet ? 'invalid_version_override' : 'version_unresolvable';
|
|
179
|
+
return skipped(reason, `one-time standalone migration skipped (${reason})`);
|
|
180
|
+
}
|
|
181
|
+
let assetName;
|
|
182
|
+
try {
|
|
183
|
+
assetName = releaseAssetName(platform, options.arch ?? process.arch);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Unsupported platform (e.g. win-arm64): skip without recording a cooldown-worthy attempt.
|
|
187
|
+
return skipped('unsupported_platform', `one-time standalone migration skipped (unsupported platform ${platform}/${options.arch ?? process.arch})`);
|
|
188
|
+
}
|
|
189
|
+
const destPath = join(resolveMigrationHome(env), 'bin', assetName);
|
|
190
|
+
writeMigrationState(env, { state: 'in_progress', version, destPath }, options);
|
|
191
|
+
const fail = (reason, message, attemptDetail) => {
|
|
192
|
+
recordBootstrapAttempt(env, { ok: false, reason: 'migration_failed', detail: attemptDetail ?? reason }, recordOptions);
|
|
193
|
+
writeMigrationState(env, { state: 'failed', version, destPath, reason }, options);
|
|
194
|
+
return { outcome: 'failed', reason, destPath, message };
|
|
195
|
+
};
|
|
196
|
+
const failTimeout = (reason, message, attemptDetail) => {
|
|
197
|
+
recordBootstrapAttempt(env, { ok: false, reason: 'migration_timeout', detail: attemptDetail }, recordOptions);
|
|
198
|
+
writeMigrationState(env, { state: 'failed', version, destPath, reason }, options);
|
|
199
|
+
return { outcome: 'failed', reason, destPath, message };
|
|
200
|
+
};
|
|
201
|
+
const readBinary = options.readBinary ?? ((path) => fsReadFile(path));
|
|
202
|
+
const writeBinary = options.writeBinary
|
|
203
|
+
?? ((path, content, mode) => fsWriteFile(path, content, { mode }));
|
|
204
|
+
const mkdir = options.mkdir ?? ((path, mode) => fsMkdir(path, { recursive: true, mode }).then(() => undefined));
|
|
205
|
+
const rm = options.rm ?? ((path) => fsRm(path, { recursive: true, force: true }));
|
|
206
|
+
const rename = options.rename ?? ((from, to) => fsRename(from, to));
|
|
207
|
+
const chmod = options.chmod ?? ((path, mode) => fsChmod(path, mode));
|
|
208
|
+
const stat = options.stat ?? ((path) => lstat(path));
|
|
209
|
+
const preflightFn = options.preflightFn
|
|
210
|
+
?? ((targetPath, expectedVersion) => preflightManagedStagedBinary(targetPath, expectedVersion, options.probe));
|
|
211
|
+
const downloadFn = options.downloadFn ?? downloadGithubReleaseArtifact;
|
|
212
|
+
const verifyFn = options.verifyFn
|
|
213
|
+
?? ((manifest, downloaded, publicKey) => ops.verifySelectedManifestArtifact(manifest, downloaded, publicKey));
|
|
214
|
+
// Fast path: an existing dest that passes the real preflight only needs registration.
|
|
215
|
+
if (exists(destPath)) {
|
|
216
|
+
try {
|
|
217
|
+
await preflightFn(destPath, version);
|
|
218
|
+
return register(env, version, destPath, options, exists, recordOptions, fail, failTimeout, writeMigrationState);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Existing binary is unusable — fall through and download a fresh copy over it.
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// DOWNLOAD: signed manifest + staged binary under tmpdir.
|
|
225
|
+
const directive = { required_version: version };
|
|
226
|
+
const releaseOpts = {
|
|
227
|
+
env,
|
|
228
|
+
platform,
|
|
229
|
+
arch: options.arch,
|
|
230
|
+
fetchFn: options.fetchFn,
|
|
231
|
+
requireSignedManifest: true,
|
|
232
|
+
};
|
|
233
|
+
let manifest;
|
|
234
|
+
let download;
|
|
235
|
+
try {
|
|
236
|
+
manifest = await resolveGithubReleaseManifest(directive, releaseOpts);
|
|
237
|
+
download = await downloadFn(version, directive, releaseOpts);
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_DOWNLOAD_FAILED;
|
|
241
|
+
return fail(`${code}:${errorDetail(err)}`, `one-time standalone migration failed (${code})`, `${code}: ${errorDetail(err)}`);
|
|
242
|
+
}
|
|
243
|
+
const stagedDir = dirname(download.stagedPath);
|
|
244
|
+
// VERIFY: ed25519 signature over the complete manifest + sha256 of the staged bytes.
|
|
245
|
+
// Fail closed: unverified bytes are NEVER written into the user's home.
|
|
246
|
+
const verification = verifyFn(manifest, download.artifacts, resolveSelfUpdatePublicKey(env));
|
|
247
|
+
if (!verification.ok) {
|
|
248
|
+
await rmSafe(rm, stagedDir);
|
|
249
|
+
const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_VERIFY_FAILED;
|
|
250
|
+
return fail(`${code}:${verification.reason}`, `one-time standalone migration failed (${code})`, `${code}: ${verification.reason}`);
|
|
251
|
+
}
|
|
252
|
+
// Preflight the staged binary for real (`--version` + `proxy --help`) before install.
|
|
253
|
+
try {
|
|
254
|
+
await preflightFn(download.stagedPath, version);
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
await rmSafe(rm, stagedDir);
|
|
258
|
+
const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_VERIFY_FAILED;
|
|
259
|
+
const detail = `preflight:${errorDetail(err)}`;
|
|
260
|
+
return fail(`${code}:${detail}`, `one-time standalone migration failed (${code})`, `${code}: ${detail}`);
|
|
261
|
+
}
|
|
262
|
+
// INSTALL: atomic copy into <home>/bin (verified bytes only).
|
|
263
|
+
try {
|
|
264
|
+
await mkdir(dirname(destPath), 0o700);
|
|
265
|
+
const stagedStat = await stat(download.stagedPath);
|
|
266
|
+
if (stagedStat.isSymbolicLink() || !stagedStat.isFile()) {
|
|
267
|
+
throw new Error('staged_artifact_not_regular_file');
|
|
268
|
+
}
|
|
269
|
+
const bytes = await readBinary(download.stagedPath);
|
|
270
|
+
const tmpDest = `${destPath}.tmp`;
|
|
271
|
+
try {
|
|
272
|
+
await writeBinary(tmpDest, bytes, 0o755);
|
|
273
|
+
await chmod(tmpDest, 0o755);
|
|
274
|
+
await rename(tmpDest, destPath);
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
await rmSafe(rm, tmpDest);
|
|
278
|
+
throw err;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
await rmSafe(rm, stagedDir);
|
|
283
|
+
const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_INSTALL_FAILED;
|
|
284
|
+
return fail(`${code}:${errorDetail(err)}`, `one-time standalone migration failed (${code})`, `${code}: ${errorDetail(err)}`);
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
await rmSafe(rm, stagedDir);
|
|
288
|
+
}
|
|
289
|
+
// REGISTER: hand the installed binary to `evolver lifecycle bootstrap`.
|
|
290
|
+
return register(env, version, destPath, options, exists, recordOptions, fail, failTimeout, writeMigrationState);
|
|
291
|
+
}
|
|
292
|
+
async function register(env, version, destPath, options, exists, recordOptions, fail, failTimeout, writeState) {
|
|
293
|
+
const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_REGISTER_FAILED;
|
|
294
|
+
const registration = await registerMigratedBinary(env, destPath, options, exists);
|
|
295
|
+
if (registration.reason === 'cli_not_found') {
|
|
296
|
+
return fail(`${code}:cli_not_found`, `one-time standalone migration failed (${code}:cli_not_found)`, `${code}: cli_not_found`);
|
|
297
|
+
}
|
|
298
|
+
if (registration.reason === 'timeout') {
|
|
299
|
+
return failTimeout('migration_register_timeout', `one-time standalone migration failed (${code}:timeout)`, `${code}: timeout`);
|
|
300
|
+
}
|
|
301
|
+
if (!registration.ok) {
|
|
302
|
+
const detail = registration.detail ?? 'unknown';
|
|
303
|
+
return fail(`${code}:${detail}`, `one-time standalone migration failed (${code})`, `${code}: ${detail}`);
|
|
304
|
+
}
|
|
305
|
+
recordBootstrapAttempt(env, { ok: true, reason: 'migrated', detail: destPath }, recordOptions);
|
|
306
|
+
writeState(env, { state: 'migrated', version, destPath }, options);
|
|
307
|
+
return {
|
|
308
|
+
outcome: 'migrated',
|
|
309
|
+
reason: 'migrated',
|
|
310
|
+
destPath,
|
|
311
|
+
message: `[evolver-proxy] self-update: installed standalone binary ${version} at ${destPath} and registered `
|
|
312
|
+
+ 'durable service supervision via `evolver lifecycle bootstrap`; handing over to the service manager '
|
|
313
|
+
+ 'and exiting so it can take the IPC port.',
|
|
314
|
+
};
|
|
315
|
+
}
|
|
@@ -1,3 +1,20 @@
|
|
|
1
1
|
export type SelfUpdatePolicy = 'off' | 'prompt' | 'auto';
|
|
2
|
-
/** Resolve EVOLVER_SELF_UPDATE to a policy. Unset
|
|
3
|
-
export declare function resolveSelfUpdatePolicy(env?: NodeJS.ProcessEnv): SelfUpdatePolicy;
|
|
2
|
+
/** Resolve EVOLVER_SELF_UPDATE to a policy. Unset → 'auto' (still gated by supervisor + public key). Unrecognized → 'off' (fail-closed). */
|
|
3
|
+
export declare function resolveSelfUpdatePolicy(env?: NodeJS.ProcessEnv): SelfUpdatePolicy;
|
|
4
|
+
/** True when the operator set EVOLVER_SELF_UPDATE to any non-blank value. */
|
|
5
|
+
export declare function isSelfUpdateExplicit(env?: NodeJS.ProcessEnv): boolean;
|
|
6
|
+
/** Attested only by the generated durable launchers; env files and operators cannot forge the marker. */
|
|
7
|
+
export declare function selfUpdateSupervisorAttested(env: NodeJS.ProcessEnv): boolean;
|
|
8
|
+
export interface EffectiveSelfUpdatePolicy {
|
|
9
|
+
policy: SelfUpdatePolicy;
|
|
10
|
+
/** True when a DEFAULT auto was degraded to 'off' because the supervisor attestation is missing. */
|
|
11
|
+
degraded: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the policy that startup should actually run with. A default (unset) 'auto' without durable
|
|
15
|
+
* supervisor attestation OR without any available public key OR without a bindable self-update target
|
|
16
|
+
* (npm/JS install shape) degrades to 'off' so direct/foreground runs — and residual launchers from a
|
|
17
|
+
* bad bootstrap on a JS install — keep working; an explicit 'auto' passes through untouched and fails
|
|
18
|
+
* closed at assembly time. `execPath` overrides process.execPath for the target-bindability check.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveEffectiveSelfUpdatePolicy(env?: NodeJS.ProcessEnv, execPath?: string): EffectiveSelfUpdatePolicy;
|
|
@@ -1,9 +1,89 @@
|
|
|
1
|
-
|
|
1
|
+
// Self-update policy resolution from the EVOLVER_SELF_UPDATE env var.
|
|
2
|
+
//
|
|
3
|
+
// Default is AUTO. An auto-applying self-update channel is the single highest-value attack surface
|
|
4
|
+
// in the fleet, so the auto default stays hard-gated: production auto requires supervisor attestation
|
|
5
|
+
// (a generated durable launcher owns relaunch) plus an Ed25519 public key for signed-manifest
|
|
6
|
+
// verification, and anything missing fails closed before any update is applied.
|
|
7
|
+
//
|
|
8
|
+
// The DEFAULT auto is an install-convenience, not an operator assertion: when no durable supervisor
|
|
9
|
+
// attestation is present OR no verification public key is available (configured env value or the
|
|
10
|
+
// built-in distribution key) OR no self-update target is bindable (npm/JS install shape without a
|
|
11
|
+
// standalone binary), resolveEffectiveSelfUpdatePolicy
|
|
12
|
+
// degrades it to 'off' (with a warning at startup) so unsupervised foreground runs still start —
|
|
13
|
+
// keeping 'auto' would just make createSelfUpdateDeps throw at assembly and crash startup. The
|
|
14
|
+
// target-bindability check also rescues residual bad launchers left behind by older bootstraps:
|
|
15
|
+
// an attested supervisor running through node degrades to 'off' instead of crash-looping.
|
|
16
|
+
// An EXPLICIT EVOLVER_SELF_UPDATE=auto keeps failing closed at assembly (createSelfUpdateDeps throws)
|
|
17
|
+
// — the operator asked for auto, silence would hide a misconfiguration.
|
|
18
|
+
// Operators can still narrow it explicitly:
|
|
19
|
+
// off : never apply.
|
|
20
|
+
// prompt : recognized but not auto-applied until an approval store/UI exists.
|
|
21
|
+
// auto : apply automatically after verification (default).
|
|
22
|
+
// Enterprise/air-gap deployments should pin 'off' explicitly in their env file (decided by the
|
|
23
|
+
// private adapter, not core).
|
|
24
|
+
import { resolveSelfUpdatePublicKey } from './builtinKey.js';
|
|
25
|
+
import { resolveSelfUpdateTarget } from './releaseBinary.js';
|
|
26
|
+
/** Resolve EVOLVER_SELF_UPDATE to a policy. Unset → 'auto' (still gated by supervisor + public key). Unrecognized → 'off' (fail-closed). */
|
|
2
27
|
export function resolveSelfUpdatePolicy(env = process.env) {
|
|
3
28
|
const raw = (env['EVOLVER_SELF_UPDATE'] ?? '').trim().toLowerCase();
|
|
29
|
+
if (raw === 'off')
|
|
30
|
+
return 'off';
|
|
4
31
|
if (raw === 'prompt')
|
|
5
32
|
return 'prompt';
|
|
6
|
-
if (raw === 'auto')
|
|
33
|
+
if (raw === 'auto' || raw === '')
|
|
7
34
|
return 'auto';
|
|
8
35
|
return 'off';
|
|
36
|
+
}
|
|
37
|
+
/** True when the operator set EVOLVER_SELF_UPDATE to any non-blank value. */
|
|
38
|
+
export function isSelfUpdateExplicit(env = process.env) {
|
|
39
|
+
return (env['EVOLVER_SELF_UPDATE'] ?? '').trim() !== '';
|
|
40
|
+
}
|
|
41
|
+
/** Attested only by the generated durable launchers; env files and operators cannot forge the marker. */
|
|
42
|
+
export function selfUpdateSupervisorAttested(env) {
|
|
43
|
+
const supervisor = env['EVOLVER_SELF_UPDATE_SUPERVISOR']?.trim();
|
|
44
|
+
return supervisor === 'systemd'
|
|
45
|
+
|| supervisor === 'launchd'
|
|
46
|
+
|| supervisor === 'windows-scheduled-task';
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* True when a verification public key will be available at assembly time: a configured
|
|
50
|
+
* EVOLVER_SELF_UPDATE_PUBLIC_KEY or the built-in distribution key. Must stay aligned with
|
|
51
|
+
* the resolver used by createSelfUpdateDeps so policy never degrades for a key that
|
|
52
|
+
* assembly would accept.
|
|
53
|
+
*/
|
|
54
|
+
function selfUpdatePublicKeyAvailable(env) {
|
|
55
|
+
return Boolean(resolveSelfUpdatePublicKey(env).trim());
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* True when a self-update target can be bound at assembly time: an explicit
|
|
59
|
+
* EVOLVER_SELF_UPDATE_TARGET_PATH or a standalone release binary execPath. Must stay aligned
|
|
60
|
+
* with resolveSelfUpdateTarget as used by createSelfUpdateDeps so policy never keeps 'auto'
|
|
61
|
+
* for a target that assembly would reject.
|
|
62
|
+
*/
|
|
63
|
+
function selfUpdateTargetBindable(env, execPath) {
|
|
64
|
+
try {
|
|
65
|
+
resolveSelfUpdateTarget({ env, processExecPath: execPath });
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolve the policy that startup should actually run with. A default (unset) 'auto' without durable
|
|
74
|
+
* supervisor attestation OR without any available public key OR without a bindable self-update target
|
|
75
|
+
* (npm/JS install shape) degrades to 'off' so direct/foreground runs — and residual launchers from a
|
|
76
|
+
* bad bootstrap on a JS install — keep working; an explicit 'auto' passes through untouched and fails
|
|
77
|
+
* closed at assembly time. `execPath` overrides process.execPath for the target-bindability check.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveEffectiveSelfUpdatePolicy(env = process.env, execPath) {
|
|
80
|
+
const policy = resolveSelfUpdatePolicy(env);
|
|
81
|
+
if (policy === 'auto'
|
|
82
|
+
&& !isSelfUpdateExplicit(env)
|
|
83
|
+
&& (!selfUpdateSupervisorAttested(env)
|
|
84
|
+
|| !selfUpdatePublicKeyAvailable(env)
|
|
85
|
+
|| !selfUpdateTargetBindable(env, execPath))) {
|
|
86
|
+
return { policy: 'off', degraded: true };
|
|
87
|
+
}
|
|
88
|
+
return { policy, degraded: false };
|
|
9
89
|
}
|
|
@@ -205,7 +205,10 @@ export function resolveSelfUpdateTarget(opts = {}) {
|
|
|
205
205
|
if (explicitTarget)
|
|
206
206
|
return { path: explicitTarget, explicit: true };
|
|
207
207
|
const execPath = opts.processExecPath ?? process.execPath;
|
|
208
|
-
|
|
208
|
+
// Split on both separators so a win32-shaped path (backslashes) is classified
|
|
209
|
+
// correctly even when this check runs on a POSIX host (shape probes, tests).
|
|
210
|
+
const segments = execPath.split(/[/\\]+/).filter(Boolean);
|
|
211
|
+
const name = (segments.length > 0 ? segments[segments.length - 1] : basename(execPath)).toLowerCase();
|
|
209
212
|
if (name.startsWith('evolver'))
|
|
210
213
|
return { path: execPath, explicit: false };
|
|
211
214
|
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.INSTALL_GUARD_UNREADABLE, `self_update_target_required:${execPath}`);
|
package/dist/sync/engine.d.ts
CHANGED
|
@@ -23,6 +23,13 @@ export interface SyncEngineDeps {
|
|
|
23
23
|
onOutboundSucceeded?: (envelope: Envelope, result: unknown) => void | Promise<void>;
|
|
24
24
|
/** Cache a durable facade terminal result after SyncEngine moved the envelope to DLQ. */
|
|
25
25
|
onOutboundTerminal?: (envelope: Envelope, error: unknown) => void | Promise<void>;
|
|
26
|
+
/** Return the durable marker that proves a racing facade request already reached external acceptance. */
|
|
27
|
+
acceptedOutcomeKey?: (envelope: Envelope) => string | undefined;
|
|
28
|
+
/** Return a replayable terminal result that must be committed atomically with the mailbox DLQ transition. */
|
|
29
|
+
terminalOutcome?: (envelope: Envelope, error: unknown) => {
|
|
30
|
+
key: string;
|
|
31
|
+
result: unknown;
|
|
32
|
+
} | undefined;
|
|
26
33
|
/** Canonicalize an inbound envelope before durable insertion. */
|
|
27
34
|
normalizeInboundEnvelope?: (envelope: Envelope) => Envelope;
|
|
28
35
|
onOutboundFlushed?: (result: OutboundResult) => void | Promise<void>;
|
|
@@ -44,6 +51,7 @@ export interface InboundResult {
|
|
|
44
51
|
}
|
|
45
52
|
export declare class SyncEngine {
|
|
46
53
|
private readonly deps;
|
|
54
|
+
private readonly acceptedTaskOutboundMemory;
|
|
47
55
|
private lastActivityAt;
|
|
48
56
|
constructor(deps: SyncEngineDeps);
|
|
49
57
|
/** 出站: claim proxy 消息 → 经 proxyHandler 推 hub → complete; 终态(publish reject)直进 DLQ 不重试(money-safety). */
|
|
@@ -59,6 +67,10 @@ export declare class SyncEngine {
|
|
|
59
67
|
private notifyOutboundFlushed;
|
|
60
68
|
private normalizeOutbound;
|
|
61
69
|
private outboundDedupKey;
|
|
70
|
+
private acceptedTaskOutbound;
|
|
71
|
+
private durableAcceptedTaskOutbound;
|
|
72
|
+
private completeAcceptedOutcome;
|
|
73
|
+
private rememberAcceptedTaskOutbound;
|
|
62
74
|
private safeAck;
|
|
63
75
|
/** AgentEvent → core Envelope; 未知类型(不在目录)跳过. */
|
|
64
76
|
private toEnvelope;
|