@evomap/evolver-proxy 2.0.0-beta.9 → 2.0.1

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 (59) hide show
  1. package/dist/bin/evolver-proxy.d.ts +26 -2
  2. package/dist/bin/evolver-proxy.js +284 -51
  3. package/dist/daemon/atpConsent.js +5 -2
  4. package/dist/daemon/collaborationFacade.js +23 -13
  5. package/dist/daemon/proxyDaemon.d.ts +52 -0
  6. package/dist/daemon/proxyDaemon.js +1067 -24
  7. package/dist/daemon/publishRecallVerifier.d.ts +114 -0
  8. package/dist/daemon/publishRecallVerifier.js +495 -0
  9. package/dist/daemon/selectHub.js +5 -3
  10. package/dist/daemon/systemdNotifier.d.ts +46 -0
  11. package/dist/daemon/systemdNotifier.js +153 -0
  12. package/dist/index.d.ts +4 -1
  13. package/dist/index.js +4 -1
  14. package/dist/lifecycle/claimNudge.d.ts +20 -0
  15. package/dist/lifecycle/claimNudge.js +1 -0
  16. package/dist/lifecycle/deployGuard.js +1 -53
  17. package/dist/lifecycle/legacyNodeId.js +1 -178
  18. package/dist/lifecycle/manager.d.ts +4 -0
  19. package/dist/lifecycle/manager.js +1 -390
  20. package/dist/llm/bodyCapture.js +1 -293
  21. package/dist/llm/index.js +1 -3
  22. package/dist/llm/server.js +1 -359
  23. package/dist/llm/traceBackfill.js +1 -525
  24. package/dist/llm/traceConfig.js +1 -44
  25. package/dist/llm/traceControl.js +1 -85
  26. package/dist/llm/traceEnvelope.js +1 -286
  27. package/dist/llm/traceSink.js +1 -278
  28. package/dist/llm/traceUploadPayload.js +1 -27
  29. package/dist/llm/upstream.d.ts +5 -1
  30. package/dist/llm/upstream.js +1 -491
  31. package/dist/private/accountAssetCompatibility.d.ts +29 -0
  32. package/dist/private/accountAssetCompatibility.js +196 -0
  33. package/dist/private/adapterLoader.d.ts +21 -1
  34. package/dist/private/adapterLoader.js +242 -7
  35. package/dist/private/nodeCredentialStore.d.ts +23 -0
  36. package/dist/private/nodeCredentialStore.js +210 -0
  37. package/dist/router/cachePassthrough.js +1 -13
  38. package/dist/router/features.js +1 -52
  39. package/dist/router/index.js +1 -6
  40. package/dist/router/messagesRoute.js +1 -809
  41. package/dist/router/modelRouter.js +1 -66
  42. package/dist/router/providerRoutes.js +1 -1579
  43. package/dist/router/sseScan.js +1 -543
  44. package/dist/selfUpdate/bootstrap.d.ts +69 -0
  45. package/dist/selfUpdate/bootstrap.js +282 -0
  46. package/dist/selfUpdate/builtinKey.d.ts +4 -0
  47. package/dist/selfUpdate/builtinKey.js +16 -0
  48. package/dist/selfUpdate/executor.d.ts +1 -1
  49. package/dist/selfUpdate/executor.js +1 -1
  50. package/dist/selfUpdate/failureCodes.d.ts +4 -0
  51. package/dist/selfUpdate/failureCodes.js +7 -0
  52. package/dist/selfUpdate/migration.d.ts +93 -0
  53. package/dist/selfUpdate/migration.js +315 -0
  54. package/dist/selfUpdate/policy.d.ts +19 -2
  55. package/dist/selfUpdate/policy.js +82 -2
  56. package/dist/selfUpdate/releaseBinary.js +4 -1
  57. package/dist/sync/engine.d.ts +12 -0
  58. package/dist/sync/engine.js +1 -505
  59. package/package.json +7 -4
@@ -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/unrecognized → 'off' (fail-closed). */
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
- /** Resolve EVOLVER_SELF_UPDATE to a policy. Unset/unrecognized 'off' (fail-closed). */
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
- const name = basename(execPath).toLowerCase();
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}`);
@@ -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;