@evomap/evolver-proxy 2.0.2 → 2.0.12

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
@@ -1,43 +1,97 @@
1
1
  // First-run supervision bootstrap for the DEFAULT self-update policy.
2
2
  //
3
3
  // Default auto self-update degrades to 'off' without a durable supervisor attestation (policy.ts).
4
- // To make `npm install` a complete zero-config path, an unsupervised foreground startup may ONCE
4
+ // For a standalone release binary, an unsupervised foreground startup may once
5
5
  // register its own user-level durable launcher (`evolver lifecycle bootstrap`) and hand over to it:
6
6
  // the generated launcher carries the EVOLVER_SELF_UPDATE_SUPERVISOR attestation, so the next
7
7
  // supervised startup runs auto self-update with the unchanged signature/health-check/rollback gates.
8
8
  //
9
9
  // Bootstrap is a convenience, never an escalation: it is skipped for attested runs, explicit
10
10
  // non-auto policies, the EVOLVER_SELF_BOOTSTRAP kill switch, CI, containers, and within a
11
- // cooldown window after a failed attempt. The npm/JS install shape has no bindable
12
- // self-update target, so instead of launcher bootstrap it attempts a one-time migration to
13
- // the standalone release binary (migration.ts). Any failure degrades back to the existing
14
- // 'off + warning' startup.
15
- import { spawn } from 'node:child_process';
11
+ // cooldown window after a safe failed attempt. The npm/JS install shape first attempts the
12
+ // signed standalone migration from migration.ts. Foreground startup continues only when no
13
+ // registration child was created or a clean rollback and process-tree termination are proven;
14
+ // all ambiguous child ownership fails closed.
15
+ import { execFileSync, spawn } from 'node:child_process';
16
+ import { createHash, randomUUID } from 'node:crypto';
16
17
  import { createRequire } from 'node:module';
17
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
18
+ import { closeSync, constants, existsSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, realpathSync, renameSync, rmSync, writeFileSync, writeSync, } from 'node:fs';
18
19
  import { homedir } from 'node:os';
19
- import { basename, dirname, join, resolve as resolvePath } from 'node:path';
20
+ import { basename, dirname, join, parse as parsePath, posix, resolve as resolvePath, win32, } from 'node:path';
20
21
  import { fileURLToPath } from 'node:url';
22
+ import { bootstrap as coreBootstrap, util } from '@evomap/evolver-core';
21
23
  import { isSelfUpdateExplicit, resolveSelfUpdatePolicy, selfUpdateSupervisorAttested, } from './policy.js';
22
- import { expandHomePath } from '../bin/envFile.js';
23
- import { resolveSelfUpdateTarget } from './releaseBinary.js';
24
- import { migrateToStandaloneBinary } from './migration.js';
24
+ import { isStandaloneReleaseBinaryName, resolveSelfUpdateTarget, } from './releaseBinary.js';
25
+ import { expandHomePath, parseEnvFile } from '../bin/envFile.js';
26
+ import { migrateToStandaloneBinary, } from './migration.js';
25
27
  const requireFromHere = createRequire(import.meta.url);
26
- export const BOOTSTRAP_SUCCESS_FILE = 'bootstrap.json';
27
28
  const BOOTSTRAP_ATTEMPT_FILE = 'bootstrap-attempt.json';
29
+ const BOOTSTRAP_REGISTRATION_INTENT_FILE = coreBootstrap.LIFECYCLE_BOOTSTRAP_REGISTRATION_INTENT_FILE;
30
+ const BOOTSTRAP_REGISTRATION_INTENT_TERMINAL_FILE = coreBootstrap.LIFECYCLE_BOOTSTRAP_REGISTRATION_INTENT_TERMINAL_FILE;
31
+ const BOOTSTRAP_REGISTRATION_INTENT_CLEARING_FILE = coreBootstrap.LIFECYCLE_BOOTSTRAP_REGISTRATION_INTENT_CLEARING_FILE;
32
+ const BOOTSTRAP_REGISTRATION_INTENT_PUBLISHING_FILE = 'bootstrap-registration.intent.publishing';
33
+ const BOOTSTRAP_REGISTRATION_INTENT_PUBLISHER_PREFIX = 'bootstrap-registration.intent.publisher.';
34
+ const BOOTSTRAP_REGISTRATION_INTENT_SCHEMA = coreBootstrap.LIFECYCLE_BOOTSTRAP_REGISTRATION_INTENT_SCHEMA;
28
35
  const BOOTSTRAP_FAILURE_COOLDOWN_MS = 24 * 60 * 60 * 1000;
29
- // The CLI-side service activation itself spawns up to ~60s (lifecycle powershell/schtasks
30
- // activation), so a shorter timeout would kill the child mid-activation and leave the
31
- // half-installed state: the service registered but bootstrap judged failed.
32
- const BOOTSTRAP_TIMEOUT_MS = 90_000;
36
+ // The child owns a shorter absolute transaction deadline. The parent waits beyond that deadline
37
+ // so the child can finish its durable rollback, and only then force-terminates the whole process
38
+ // tree if the child failed to honor the contract.
39
+ const BOOTSTRAP_TRANSACTION_BUDGET_MS = 180_000;
40
+ const BOOTSTRAP_PARENT_EXIT_GRACE_MS = 30_000;
41
+ const BOOTSTRAP_TIMEOUT_MS = BOOTSTRAP_TRANSACTION_BUDGET_MS + BOOTSTRAP_PARENT_EXIT_GRACE_MS;
42
+ const BOOTSTRAP_TREE_TERMINATION_GRACE_MS = 5_000;
43
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
33
44
  const BOOTSTRAP_ENV_FILE_HANDOFF = 'EVOLVER_INTERNAL_BOOTSTRAP_ENV_FILE';
45
+ export const RECOVERY_CONTROLLER_LIFECYCLE_OWNER_CAPABILITY_ENV = 'EVOLVER_INTERNAL_RECOVERY_CONTROLLER_LIFECYCLE_OWNER';
46
+ const MAX_RECOVERY_CONTROLLER_OWNER_CAPABILITY_BYTES = 1024;
47
+ const MAX_BOOTSTRAP_OUTPUT_BYTES = 64 * 1024;
48
+ const MAX_BOOTSTRAP_STATE_BYTES = 128 * 1024;
49
+ const MAX_BOOTSTRAP_ATTEMPT_BYTES = 4 * 1024;
50
+ const MAX_BOOTSTRAP_REGISTRATION_INTENT_BYTES = coreBootstrap.MAX_LIFECYCLE_BOOTSTRAP_REGISTRATION_INTENT_BYTES;
51
+ const MAX_BOOTSTRAP_DETAIL_LENGTH = 512;
52
+ const HOST_WINDOWS_SYSTEM_ROOT = process.env['SystemRoot']?.trim() || 'C:\\Windows';
34
53
  /** Lifecycle state dir mirror of evolver-cli lifecyclePaths (kept dependency-free across packages). */
35
54
  export function resolveBootstrapStateDir(env) {
36
55
  const explicit = env['EVOLVER_LIFECYCLE_STATE_DIR']?.trim();
37
56
  const home = env['EVOLVER_HOME'] ?? env['EVOMAP_HOME'] ?? join(homedir(), '.evomap');
38
57
  return resolvePath(explicit || join(home, 'lifecycle'));
39
58
  }
40
- function bootstrapChildEnv(env) {
59
+ export function withRecoveryControllerLifecycleOwnerCapability(env, owner) {
60
+ return prepareRecoveryControllerLifecycleOwnerCapability(env, owner).env;
61
+ }
62
+ export function prepareRecoveryControllerLifecycleOwnerCapability(env, owner) {
63
+ const startupAckToken = randomUUID();
64
+ return {
65
+ env: {
66
+ ...env,
67
+ [RECOVERY_CONTROLLER_LIFECYCLE_OWNER_CAPABILITY_ENV]: JSON.stringify({
68
+ v: 1,
69
+ pid: owner.pid,
70
+ token: owner.token,
71
+ processStartIdentity: owner.processStartIdentity,
72
+ startupAckToken,
73
+ }),
74
+ },
75
+ startupAckToken,
76
+ };
77
+ }
78
+ export function clearRecoveryControllerLifecycleOwnerCapability(env) {
79
+ delete env[RECOVERY_CONTROLLER_LIFECYCLE_OWNER_CAPABILITY_ENV];
80
+ }
81
+ export function publishRecoveryControllerLifecycleStartupAttestation(env, descriptor = 3) {
82
+ if (env[RECOVERY_CONTROLLER_LIFECYCLE_OWNER_CAPABILITY_ENV] === undefined)
83
+ return false;
84
+ const capability = recoveryControllerLifecycleOwnerCapability(env);
85
+ if (!capability) {
86
+ throw new Error('self_update_recovery_controller_lifecycle_capability_invalid');
87
+ }
88
+ assertRecoveryControllerLifecycleOwnerLease(env, capability.owner);
89
+ writeSync(descriptor, `${capability.startupAckToken}\n`, undefined, 'utf8');
90
+ closeSync(descriptor);
91
+ clearRecoveryControllerLifecycleOwnerCapability(env);
92
+ return true;
93
+ }
94
+ function bootstrapChildEnv(env, transactionDeadlineMs) {
41
95
  const childEnv = { ...env };
42
96
  const envFile = env['EVOLVER_ENV_FILE']?.trim();
43
97
  delete childEnv['EVOLVER_ENV_FILE'];
@@ -47,178 +101,3195 @@ function bootstrapChildEnv(env) {
47
101
  // Resolve while the foreground proxy still owns cwd, then carry that identity through the
48
102
  // bootstrap child and generated service launcher. Service managers do not share one cwd.
49
103
  childEnv['EVOLVER_LIFECYCLE_STATE_DIR'] = resolveBootstrapStateDir(env);
104
+ childEnv[coreBootstrap.LIFECYCLE_BOOTSTRAP_DEADLINE_ENV] = String(transactionDeadlineMs);
50
105
  return childEnv;
51
106
  }
52
- const defaultReadTextFile = (path) => readFileSync(path, 'utf8');
53
- export function looksLikeContainer(exists, readFile) {
54
- if (exists('/.dockerenv'))
107
+ export function lifecycleBootstrapStatePresent(env, exists = bootstrapStateEntryPresent) {
108
+ const stateDir = resolveBootstrapStateDir(env);
109
+ return [
110
+ coreBootstrap.LIFECYCLE_BOOTSTRAP_SUCCESS_FILE,
111
+ coreBootstrap.LIFECYCLE_BOOTSTRAP_JOURNAL_FILE,
112
+ coreBootstrap.LIFECYCLE_BOOTSTRAP_OWNER_LOCK_FILE,
113
+ coreBootstrap.LIFECYCLE_BOOTSTRAP_READINESS_FILE,
114
+ coreBootstrap.LIFECYCLE_BOOTSTRAP_READINESS_LOCK_FILE,
115
+ coreBootstrap.LIFECYCLE_BOOTSTRAP_MANUAL_TRANSITION_FILE,
116
+ ].some((name) => exists(join(stateDir, name)));
117
+ }
118
+ function bootstrapStateEntryPresent(path) {
119
+ try {
120
+ lstatSync(path);
55
121
  return true;
122
+ }
123
+ catch (error) {
124
+ return !isErrno(error, 'ENOENT');
125
+ }
126
+ }
127
+ const defaultReadTextFile = (path) => readFileSync(path, 'utf8');
128
+ const BOOTSTRAP_JOURNAL_SCHEMA = 'evolver.lifecycle-bootstrap-transaction.v1';
129
+ const BOOTSTRAP_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
130
+ const BOOTSTRAP_JOURNAL_STAGES = new Set([
131
+ 'prepared', 'installing', 'installed', 'activating', 'activated', 'committing',
132
+ 'committed', 'rollback_pending', 'rolled_back',
133
+ ]);
134
+ function boundedBootstrapDetail(value) {
135
+ let normalized = '';
136
+ let previousWasSpace = false;
137
+ for (const character of value) {
138
+ const next = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(character) ? ' ' : character;
139
+ if (next === ' ' && previousWasSpace)
140
+ continue;
141
+ normalized += next;
142
+ previousWasSpace = next === ' ';
143
+ if (normalized.length >= MAX_BOOTSTRAP_DETAIL_LENGTH)
144
+ break;
145
+ }
146
+ return normalized.slice(0, MAX_BOOTSTRAP_DETAIL_LENGTH);
147
+ }
148
+ function readBoundedBootstrapStateFile(path, options, maxBytes = MAX_BOOTSTRAP_STATE_BYTES) {
149
+ const injected = options.exists !== undefined || options.readFile !== undefined;
150
+ try {
151
+ if (injected) {
152
+ if (!(options.exists ?? existsSync)(path))
153
+ return { status: 'absent' };
154
+ const raw = (options.readFile ?? defaultReadTextFile)(path);
155
+ if (Buffer.byteLength(raw, 'utf8') > maxBytes) {
156
+ return { status: 'invalid', detail: `oversized durable state: ${basename(path)}` };
157
+ }
158
+ return { status: 'present', raw };
159
+ }
160
+ const before = lstatSync(path, { bigint: true });
161
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n
162
+ || before.size > BigInt(maxBytes)
163
+ || before.dev <= 0n || before.ino <= 0n) {
164
+ return { status: 'invalid', detail: `unsafe durable state: ${basename(path)}` };
165
+ }
166
+ (options.assertIntentFileTrust ?? assertNativeBootstrapIntentFileTrust)(path);
167
+ const descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
168
+ try {
169
+ const opened = fstatSync(descriptor, { bigint: true });
170
+ if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino
171
+ || opened.size !== before.size) {
172
+ return { status: 'invalid', detail: `changed durable state: ${basename(path)}` };
173
+ }
174
+ const bytes = Buffer.alloc(maxBytes + 1);
175
+ let offset = 0;
176
+ while (offset < bytes.length) {
177
+ const count = readSync(descriptor, bytes, offset, bytes.length - offset, null);
178
+ if (count === 0)
179
+ break;
180
+ offset += count;
181
+ }
182
+ if (offset > maxBytes) {
183
+ return { status: 'invalid', detail: `oversized durable state: ${basename(path)}` };
184
+ }
185
+ const raw = bytes.subarray(0, offset).toString('utf8');
186
+ const after = fstatSync(descriptor, { bigint: true });
187
+ if (after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size
188
+ || after.mtimeNs !== opened.mtimeNs || after.ctimeNs !== opened.ctimeNs) {
189
+ return { status: 'invalid', detail: `changed durable state: ${basename(path)}` };
190
+ }
191
+ (options.assertIntentFileTrust ?? assertNativeBootstrapIntentFileTrust)(path);
192
+ const settled = lstatSync(path, { bigint: true });
193
+ if (!settled.isFile() || settled.isSymbolicLink() || settled.nlink !== 1n
194
+ || settled.dev !== opened.dev || settled.ino !== opened.ino || settled.size !== opened.size
195
+ || settled.mtimeNs !== opened.mtimeNs || settled.ctimeNs !== opened.ctimeNs) {
196
+ return { status: 'invalid', detail: `changed durable state: ${basename(path)}` };
197
+ }
198
+ return { status: 'present', raw };
199
+ }
200
+ finally {
201
+ closeSync(descriptor);
202
+ }
203
+ }
204
+ catch (error) {
205
+ if (isErrno(error, 'ENOENT'))
206
+ return { status: 'absent' };
207
+ return {
208
+ status: 'invalid',
209
+ detail: boundedBootstrapDetail(`unreadable durable state ${basename(path)}: ${error instanceof Error ? error.message : String(error)}`),
210
+ };
211
+ }
212
+ }
213
+ function sameBootstrapStateFileSnapshot(left, right) {
214
+ if (left.status !== right.status)
215
+ return false;
216
+ if (left.status === 'present' && right.status === 'present') {
217
+ return left.raw === right.raw;
218
+ }
219
+ return left.status === 'absent' && right.status === 'absent';
220
+ }
221
+ function legacyBootstrapPathKey(path, target) {
222
+ return target === 'windows'
223
+ ? win32.normalize(path).toLowerCase()
224
+ : posix.normalize(path);
225
+ }
226
+ function assertNativeBootstrapLegacyProofFileTrust(path) {
227
+ const info = lstatSync(path);
228
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink < 1) {
229
+ throw new Error('legacy bootstrap proof file is unsafe');
230
+ }
231
+ if (process.platform === 'win32') {
232
+ assertWindowsBootstrapIntentAclTrusted([{ path, parentOnly: false }]);
233
+ return;
234
+ }
235
+ const uid = typeof process.getuid === 'function' ? process.getuid() : undefined;
236
+ if (uid === undefined
237
+ || (info.uid !== uid && info.uid !== 0)
238
+ || (info.mode & 0o022) !== 0) {
239
+ throw new Error('legacy bootstrap proof file is not trusted');
240
+ }
241
+ }
242
+ function readLegacyBootstrapProofFile(receipt, options) {
243
+ if (receipt.size < 1 || receipt.size > MAX_BOOTSTRAP_STATE_BYTES) {
244
+ throw new Error('legacy bootstrap proof receipt has an invalid size');
245
+ }
246
+ const injected = options.exists !== undefined || options.readFile !== undefined;
247
+ const exists = options.exists ?? existsSync;
248
+ const readFile = options.readFile ?? defaultReadTextFile;
249
+ if (injected) {
250
+ if (!exists(receipt.path))
251
+ throw new Error('legacy bootstrap proof file is missing');
252
+ const first = readFile(receipt.path);
253
+ options.afterLegacyProofRead?.(receipt.path);
254
+ const second = readFile(receipt.path);
255
+ const bytes = Buffer.from(first, 'utf8');
256
+ if (first !== second
257
+ || bytes.length !== receipt.size
258
+ || createHash('sha256').update(bytes).digest('hex') !== receipt.sha256) {
259
+ throw new Error('legacy bootstrap proof file changed after adoption');
260
+ }
261
+ return first;
262
+ }
263
+ const assertDirectoryTrust = options.assertLegacyProofDirectoryTrust
264
+ ?? ((directory) => assertNativeBootstrapIntentDirectoryTrust(directory, false));
265
+ const assertFileTrust = options.assertLegacyProofFileTrust
266
+ ?? assertNativeBootstrapLegacyProofFileTrust;
267
+ const directory = dirname(receipt.path);
268
+ assertDirectoryTrust(directory);
269
+ assertFileTrust(receipt.path);
270
+ const before = lstatSync(receipt.path, { bigint: true });
271
+ if (!before.isFile() || before.isSymbolicLink()
272
+ || before.dev <= 0n || before.ino <= 0n
273
+ || before.size !== BigInt(receipt.size)
274
+ || before.dev !== BigInt(receipt.device ?? '0')
275
+ || before.ino !== BigInt(receipt.inode ?? '0')) {
276
+ throw new Error('legacy bootstrap proof identity changed after adoption');
277
+ }
278
+ const descriptor = openSync(receipt.path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
279
+ try {
280
+ const opened = fstatSync(descriptor, { bigint: true });
281
+ if (!opened.isFile()
282
+ || opened.dev !== before.dev || opened.ino !== before.ino
283
+ || opened.size !== before.size
284
+ || opened.mtimeNs !== before.mtimeNs || opened.ctimeNs !== before.ctimeNs) {
285
+ throw new Error('legacy bootstrap proof changed while opening');
286
+ }
287
+ const readPass = () => {
288
+ const bytes = Buffer.alloc(receipt.size + 1);
289
+ let offset = 0;
290
+ while (offset < bytes.length) {
291
+ const count = readSync(descriptor, bytes, offset, bytes.length - offset, offset);
292
+ if (count === 0)
293
+ break;
294
+ offset += count;
295
+ }
296
+ return bytes.subarray(0, offset);
297
+ };
298
+ const first = readPass();
299
+ options.afterLegacyProofRead?.(receipt.path);
300
+ const second = readPass();
301
+ const after = fstatSync(descriptor, { bigint: true });
302
+ if (!first.equals(second)
303
+ || first.length !== receipt.size
304
+ || createHash('sha256').update(first).digest('hex') !== receipt.sha256
305
+ || after.dev !== opened.dev || after.ino !== opened.ino
306
+ || after.size !== opened.size
307
+ || after.mtimeNs !== opened.mtimeNs || after.ctimeNs !== opened.ctimeNs) {
308
+ throw new Error('legacy bootstrap proof changed while reading');
309
+ }
310
+ assertDirectoryTrust(directory);
311
+ assertFileTrust(receipt.path);
312
+ const settled = lstatSync(receipt.path, { bigint: true });
313
+ if (!settled.isFile() || settled.isSymbolicLink()
314
+ || settled.dev !== opened.dev || settled.ino !== opened.ino
315
+ || settled.size !== opened.size
316
+ || settled.mtimeNs !== opened.mtimeNs || settled.ctimeNs !== opened.ctimeNs) {
317
+ throw new Error('legacy bootstrap proof path changed while reading');
318
+ }
319
+ return first.toString('utf8');
320
+ }
321
+ finally {
322
+ closeSync(descriptor);
323
+ }
324
+ }
325
+ function validateLegacyBootstrapStateRootProof(marker, stateDir, env, options) {
326
+ const proof = marker.legacyStateRootProof;
327
+ if (!proof)
328
+ return undefined;
329
+ const key = (path) => legacyBootstrapPathKey(path, marker.target);
330
+ if (key(proof.stateDir) !== key(stateDir)
331
+ || key(env['EVOLVER_LIFECYCLE_STATE_DIR']?.trim() ?? '') !== key(proof.stateDir)
332
+ || key(env['EVOLVER_ENV_FILE']?.trim() ?? '') !== key(proof.envFilePath)) {
333
+ return 'legacy bootstrap state-root binding no longer matches the supervised environment';
334
+ }
335
+ const receipt = marker.preservedArtifacts?.find((artifact) => key(artifact.path) === key(proof.envFilePath));
336
+ if (!receipt)
337
+ return 'legacy bootstrap state-root proof has no preserved receipt';
338
+ try {
339
+ const raw = readLegacyBootstrapProofFile(receipt, options);
340
+ const configured = parseEnvFile(raw)['EVOLVER_LIFECYCLE_STATE_DIR'];
341
+ if (!configured || key(configured) !== key(proof.stateDir)) {
342
+ return 'legacy bootstrap env file no longer pins the adopted state root';
343
+ }
344
+ }
345
+ catch {
346
+ return 'legacy bootstrap state-root proof could not be revalidated';
347
+ }
348
+ return undefined;
349
+ }
350
+ function parseBootstrapLockOwnerJson(raw) {
351
+ try {
352
+ const parsed = JSON.parse(raw);
353
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
354
+ return undefined;
355
+ const record = parsed;
356
+ const keys = Object.keys(record).sort();
357
+ if (keys.length !== 4
358
+ || keys[0] !== 'pid'
359
+ || keys[1] !== 'processStart'
360
+ || keys[2] !== 'token'
361
+ || keys[3] !== 'v'
362
+ || record['v'] !== 2
363
+ || !Number.isSafeInteger(record['pid']) || record['pid'] <= 0
364
+ || typeof record['token'] !== 'string' || !BOOTSTRAP_UUID_RE.test(record['token'])) {
365
+ return undefined;
366
+ }
367
+ const processStartIdentity = util.parseFileLockProcessStartIdentity(record['processStart']);
368
+ if (!processStartIdentity)
369
+ return undefined;
370
+ return {
371
+ pid: record['pid'],
372
+ token: record['token'],
373
+ processStartIdentity,
374
+ };
375
+ }
376
+ catch {
377
+ return undefined;
378
+ }
379
+ }
380
+ function parseBootstrapJournalOwner(value) {
381
+ if (!value || typeof value !== 'object' || Array.isArray(value))
382
+ return undefined;
383
+ const record = value;
384
+ const processStartIdentity = util.parseFileLockProcessStartIdentity(record['processStartIdentity']);
385
+ if (!Number.isSafeInteger(record['pid']) || record['pid'] <= 0
386
+ || typeof record['token'] !== 'string' || !BOOTSTRAP_UUID_RE.test(record['token'])
387
+ || !processStartIdentity
388
+ || typeof record['acquiredAt'] !== 'string'
389
+ || Number.isNaN(Date.parse(record['acquiredAt']))) {
390
+ return undefined;
391
+ }
392
+ return {
393
+ pid: record['pid'],
394
+ token: record['token'],
395
+ processStartIdentity,
396
+ };
397
+ }
398
+ function recoveryControllerLifecycleOwnerCapability(env, parentPid = process.ppid) {
399
+ const raw = env[RECOVERY_CONTROLLER_LIFECYCLE_OWNER_CAPABILITY_ENV];
400
+ if (!raw || Buffer.byteLength(raw, 'utf8') > MAX_RECOVERY_CONTROLLER_OWNER_CAPABILITY_BYTES) {
401
+ return undefined;
402
+ }
403
+ try {
404
+ const parsed = JSON.parse(raw);
405
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
406
+ return undefined;
407
+ const record = parsed;
408
+ const keys = Object.keys(record).sort();
409
+ if (keys.length !== 5
410
+ || keys[0] !== 'pid'
411
+ || keys[1] !== 'processStartIdentity'
412
+ || keys[2] !== 'startupAckToken'
413
+ || keys[3] !== 'token'
414
+ || keys[4] !== 'v'
415
+ || record['v'] !== 1
416
+ || !Number.isSafeInteger(record['pid'])
417
+ || record['pid'] !== parentPid
418
+ || typeof record['token'] !== 'string'
419
+ || !BOOTSTRAP_UUID_RE.test(record['token'])
420
+ || typeof record['startupAckToken'] !== 'string'
421
+ || !BOOTSTRAP_UUID_RE.test(record['startupAckToken'])) {
422
+ return undefined;
423
+ }
424
+ const processStartIdentity = util.parseFileLockProcessStartIdentity(record['processStartIdentity']);
425
+ if (!processStartIdentity)
426
+ return undefined;
427
+ const owner = {
428
+ pid: record['pid'],
429
+ token: record['token'],
430
+ processStartIdentity,
431
+ };
432
+ if (util.inspectFileLockOwnerProcess(owner) !== 'current')
433
+ return undefined;
434
+ return { owner, startupAckToken: record['startupAckToken'] };
435
+ }
436
+ catch {
437
+ return undefined;
438
+ }
439
+ }
440
+ function assertRecoveryControllerLifecycleOwnerLease(env, owner, options = {}) {
441
+ const durable = inspectLifecycleBootstrapDurableState(env, {
442
+ ...options,
443
+ expectedRecoveryOwner: owner,
444
+ });
445
+ if (durable.status === 'invalid') {
446
+ throw new Error('self_update_recovery_controller_lifecycle_capability_invalid:'
447
+ + boundedBootstrapDetail(durable.detail));
448
+ }
449
+ }
450
+ function sameBootstrapLockOwner(left, right) {
451
+ return left.pid === right.pid
452
+ && left.token === right.token
453
+ && util.sameFileLockProcessStartIdentity(left.processStartIdentity, right.processStartIdentity);
454
+ }
455
+ function bootstrapLockProcessStatus(owner, options) {
456
+ try {
457
+ return (options.registrationOwnerProcessStatus
458
+ ?? util.inspectFileLockOwnerProcess)(owner);
459
+ }
460
+ catch {
461
+ return 'unverifiable';
462
+ }
463
+ }
464
+ function inspectBootstrapRecoveryLock(owner, options) {
465
+ if (!owner)
466
+ return { recoveryStatus: 'active_or_unverifiable' };
467
+ const processStatus = bootstrapLockProcessStatus(owner, options);
468
+ return {
469
+ owner,
470
+ processStatus,
471
+ recoveryStatus: processStatus === 'dead' || processStatus === 'pid_reused'
472
+ ? 'stale'
473
+ : 'active_or_unverifiable',
474
+ };
475
+ }
476
+ function normalBootstrapActivationContractValid(record, owner, target, deadlineMs) {
477
+ const managerBinding = record['managerBinding'];
478
+ return owner !== undefined
479
+ && target !== undefined
480
+ && deadlineMs !== undefined
481
+ && record['operation'] === undefined
482
+ && record['terminalAction'] === undefined
483
+ && typeof record['service'] === 'string'
484
+ && record['service'].length > 0
485
+ && record['service'].length <= 128
486
+ && record['managerBefore'] === 'absent'
487
+ && managerBinding !== null
488
+ && typeof managerBinding === 'object'
489
+ && !Array.isArray(managerBinding)
490
+ && managerBinding['kind'] === 'transaction'
491
+ && typeof managerBinding['artifactPath'] === 'string'
492
+ && managerBinding['artifactPath'].length > 0
493
+ && managerBinding['artifactPath'].length <= 4_096
494
+ && Array.isArray(record['artifacts'])
495
+ && record['artifacts'].length > 0
496
+ && record['artifacts'].length <= 32
497
+ && typeof record['updatedAt'] === 'string'
498
+ && !Number.isNaN(Date.parse(record['updatedAt']));
499
+ }
500
+ function parseBootstrapJournalReceipt(raw) {
501
+ try {
502
+ const parsed = JSON.parse(raw);
503
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
504
+ return undefined;
505
+ const record = parsed;
506
+ if (record['schema'] !== BOOTSTRAP_JOURNAL_SCHEMA
507
+ || typeof record['transactionId'] !== 'string' || !BOOTSTRAP_UUID_RE.test(record['transactionId'])
508
+ || typeof record['stage'] !== 'string' || !BOOTSTRAP_JOURNAL_STAGES.has(record['stage'])) {
509
+ return undefined;
510
+ }
511
+ if (record['terminalAction'] === undefined) {
512
+ if (record['operation'] !== undefined
513
+ || (record['target'] !== undefined
514
+ && (typeof record['target'] !== 'string'
515
+ || !['launchd', 'systemd', 'windows'].includes(record['target'])))
516
+ || (record['owner'] !== undefined && !parseBootstrapJournalOwner(record['owner']))
517
+ || (record['deadlineMs'] !== undefined
518
+ && (!Number.isSafeInteger(record['deadlineMs'])
519
+ || record['deadlineMs'] <= 0))
520
+ || (record['activationStarted'] !== undefined
521
+ && typeof record['activationStarted'] !== 'boolean')) {
522
+ return undefined;
523
+ }
524
+ const target = record['target'];
525
+ const owner = record['owner'] === undefined
526
+ ? undefined
527
+ : parseBootstrapJournalOwner(record['owner']);
528
+ const deadlineMs = record['deadlineMs'];
529
+ return {
530
+ transactionId: record['transactionId'],
531
+ stage: record['stage'],
532
+ ...(target ? { target } : {}),
533
+ ...(owner ? { owner } : {}),
534
+ ...(deadlineMs !== undefined ? { deadlineMs } : {}),
535
+ ...(record['activationStarted'] !== undefined
536
+ ? { activationStarted: record['activationStarted'] }
537
+ : {}),
538
+ ...(normalBootstrapActivationContractValid(record, owner, target, deadlineMs)
539
+ ? { activationContractValid: true }
540
+ : {}),
541
+ };
542
+ }
543
+ if (record['operation'] === 'legacy-v907-remove'
544
+ && record['terminalAction'] === 'remove_committed'
545
+ && ['prepared', 'rollback_pending', 'rolled_back'].includes(record['stage'])
546
+ && typeof record['target'] === 'string'
547
+ && ['launchd', 'systemd', 'windows'].includes(record['target'])) {
548
+ return {
549
+ transactionId: record['transactionId'],
550
+ stage: record['stage'],
551
+ target: record['target'],
552
+ terminalAction: 'remove_committed',
553
+ operation: 'legacy-v907-remove',
554
+ };
555
+ }
556
+ if (record['terminalAction'] !== 'remove_committed'
557
+ || !['rollback_pending', 'rolled_back'].includes(record['stage'])
558
+ || typeof record['target'] !== 'string'
559
+ || !['launchd', 'systemd', 'windows'].includes(record['target'])) {
560
+ return undefined;
561
+ }
562
+ return {
563
+ transactionId: record['transactionId'],
564
+ stage: record['stage'],
565
+ target: record['target'],
566
+ terminalAction: 'remove_committed',
567
+ };
568
+ }
569
+ catch {
570
+ return undefined;
571
+ }
572
+ }
573
+ function inspectLifecycleBootstrapDurableState(env, options) {
574
+ const stateDir = resolveBootstrapStateDir(env);
575
+ if (options.exists === undefined && options.readFile === undefined) {
576
+ try {
577
+ const state = lstatSync(stateDir);
578
+ if (!state.isDirectory() || state.isSymbolicLink()) {
579
+ return { status: 'invalid', detail: 'lifecycle state directory is not trusted' };
580
+ }
581
+ if (process.platform !== 'win32') {
582
+ const uid = typeof process.getuid === 'function' ? process.getuid() : undefined;
583
+ if ((uid !== undefined && state.uid !== uid) || (state.mode & 0o077) !== 0) {
584
+ return { status: 'invalid', detail: 'lifecycle state directory is not owner-only' };
585
+ }
586
+ }
587
+ }
588
+ catch (error) {
589
+ if (isErrno(error, 'ENOENT'))
590
+ return { status: 'clean' };
591
+ return { status: 'invalid', detail: 'lifecycle state directory is unreadable' };
592
+ }
593
+ }
594
+ const markerFile = readBoundedBootstrapStateFile(join(stateDir, coreBootstrap.LIFECYCLE_BOOTSTRAP_SUCCESS_FILE), options);
595
+ const journalFile = readBoundedBootstrapStateFile(join(stateDir, coreBootstrap.LIFECYCLE_BOOTSTRAP_JOURNAL_FILE), options);
596
+ const ownerLockFile = readBoundedBootstrapStateFile(join(stateDir, coreBootstrap.LIFECYCLE_BOOTSTRAP_OWNER_LOCK_FILE), options, util.MAX_LOCK_OWNER_BYTES);
597
+ const readinessFile = readBoundedBootstrapStateFile(join(stateDir, coreBootstrap.LIFECYCLE_BOOTSTRAP_READINESS_FILE), options);
598
+ const readinessLockFile = readBoundedBootstrapStateFile(join(stateDir, coreBootstrap.LIFECYCLE_BOOTSTRAP_READINESS_LOCK_FILE), options, util.MAX_LOCK_OWNER_BYTES);
599
+ const manualTransitionFile = readBoundedBootstrapStateFile(join(stateDir, coreBootstrap.LIFECYCLE_BOOTSTRAP_MANUAL_TRANSITION_FILE), options, coreBootstrap.MAX_LIFECYCLE_BOOTSTRAP_MANUAL_TRANSITION_BYTES);
600
+ for (const state of [
601
+ markerFile,
602
+ journalFile,
603
+ ownerLockFile,
604
+ readinessFile,
605
+ readinessLockFile,
606
+ manualTransitionFile,
607
+ ]) {
608
+ if (state.status === 'invalid')
609
+ return { status: 'invalid', detail: state.detail };
610
+ }
611
+ const marker = markerFile.status === 'present'
612
+ ? coreBootstrap.parseLifecycleBootstrapMarkerJson(markerFile.raw)
613
+ : undefined;
614
+ const legacyMarker = markerFile.status === 'present' && !marker
615
+ ? coreBootstrap.parseLegacyLifecycleBootstrapMarkerJson(markerFile.raw)
616
+ : undefined;
617
+ if (markerFile.status === 'present' && !marker && !legacyMarker) {
618
+ return { status: 'invalid', detail: 'bootstrap success marker is corrupt' };
619
+ }
620
+ const readiness = readinessFile.status === 'present'
621
+ ? coreBootstrap.parseLifecycleBootstrapReadinessJson(readinessFile.raw)
622
+ : undefined;
623
+ if (readinessFile.status === 'present' && !readiness) {
624
+ return { status: 'invalid', detail: 'bootstrap readiness receipt is corrupt' };
625
+ }
626
+ const journal = journalFile.status === 'present'
627
+ ? parseBootstrapJournalReceipt(journalFile.raw)
628
+ : undefined;
629
+ if (journalFile.status === 'present' && !journal) {
630
+ return { status: 'invalid', detail: 'bootstrap recovery journal is corrupt' };
631
+ }
632
+ const manualTransition = manualTransitionFile.status === 'present'
633
+ ? coreBootstrap.parseLifecycleBootstrapManualTransitionJson(manualTransitionFile.raw)
634
+ : undefined;
635
+ if (manualTransitionFile.status === 'present' && !manualTransition) {
636
+ return { status: 'invalid', detail: 'bootstrap manual-transition tombstone is corrupt' };
637
+ }
638
+ const ownerLockOwner = ownerLockFile.status === 'present'
639
+ ? parseBootstrapLockOwnerJson(ownerLockFile.raw)
640
+ : undefined;
641
+ const expectedRecoveryOwner = options.expectedRecoveryOwner;
642
+ if (expectedRecoveryOwner !== undefined
643
+ && (ownerLockFile.status !== 'present'
644
+ || ownerLockOwner === undefined
645
+ || !sameBootstrapLockOwner(ownerLockOwner, expectedRecoveryOwner))) {
646
+ return {
647
+ status: 'invalid',
648
+ detail: 'lifecycle bootstrap owner lease is no longer held by the expected owner',
649
+ };
650
+ }
651
+ const ownerLockIgnored = ownerLockOwner !== undefined
652
+ && expectedRecoveryOwner !== undefined
653
+ && sameBootstrapLockOwner(ownerLockOwner, expectedRecoveryOwner);
654
+ const ownerLockPresent = ownerLockFile.status === 'present' && !ownerLockIgnored;
655
+ const readinessLockPresent = readinessLockFile.status === 'present';
656
+ const ownerLock = ownerLockPresent
657
+ ? inspectBootstrapRecoveryLock(ownerLockOwner, options)
658
+ : undefined;
659
+ const readinessLockOwner = readinessLockFile.status === 'present'
660
+ ? parseBootstrapLockOwnerJson(readinessLockFile.raw)
661
+ : undefined;
662
+ const readinessLock = readinessLockPresent
663
+ ? inspectBootstrapRecoveryLock(readinessLockOwner, options)
664
+ : undefined;
665
+ const changedAfterLockProbe = (prior, path, maxBytes) => {
666
+ const current = readBoundedBootstrapStateFile(path, options, maxBytes);
667
+ if (current.status === 'invalid')
668
+ return current.detail;
669
+ return sameBootstrapStateFileSnapshot(prior, current)
670
+ ? undefined
671
+ : `durable lifecycle state changed during lock owner inspection: ${basename(path)}`;
672
+ };
673
+ const snapshotFailure = changedAfterLockProbe(ownerLockFile, join(stateDir, coreBootstrap.LIFECYCLE_BOOTSTRAP_OWNER_LOCK_FILE), util.MAX_LOCK_OWNER_BYTES) ?? changedAfterLockProbe(readinessLockFile, join(stateDir, coreBootstrap.LIFECYCLE_BOOTSTRAP_READINESS_LOCK_FILE), util.MAX_LOCK_OWNER_BYTES) ?? changedAfterLockProbe(journalFile, join(stateDir, coreBootstrap.LIFECYCLE_BOOTSTRAP_JOURNAL_FILE), MAX_BOOTSTRAP_STATE_BYTES);
674
+ if (snapshotFailure)
675
+ return { status: 'invalid', detail: snapshotFailure };
676
+ const recoveryLocks = {
677
+ ...(ownerLock ? { owner: ownerLock.recoveryStatus } : {}),
678
+ ...(readinessLock ? { readiness: readinessLock.recoveryStatus } : {}),
679
+ };
680
+ const recoveryLockState = Object.keys(recoveryLocks).length > 0
681
+ ? recoveryLocks
682
+ : undefined;
683
+ const activeRecoveryLock = Object.values(recoveryLocks)
684
+ .some((status) => status === 'active_or_unverifiable');
685
+ const withRecoveryLocks = recoveryLockState ? { recoveryLocks: recoveryLockState } : {};
686
+ if (journal && readiness && journal.transactionId !== readiness.transactionId) {
687
+ return { status: 'invalid', detail: 'bootstrap journal and readiness owners differ' };
688
+ }
689
+ if (journal?.target && marker && journal.target !== marker.target) {
690
+ return { status: 'invalid', detail: 'bootstrap journal and marker targets differ' };
691
+ }
692
+ if (legacyMarker) {
693
+ if (readiness) {
694
+ return {
695
+ status: 'invalid',
696
+ detail: 'legacy bootstrap marker conflicts with transaction durable state',
697
+ };
698
+ }
699
+ if (journal) {
700
+ if (manualTransition && (journal.terminalAction !== 'remove_committed'
701
+ || journal.transactionId !== manualTransition.removedTransactionId
702
+ || journal.target !== manualTransition.target)) {
703
+ return {
704
+ status: 'invalid',
705
+ detail: 'legacy bootstrap removal journal conflicts with manual-transition state',
706
+ };
707
+ }
708
+ return {
709
+ status: 'pending',
710
+ detail: 'legacy bootstrap recovery journal requires CLI recovery',
711
+ ...withRecoveryLocks,
712
+ ...(manualTransition ? {
713
+ manualTransition: {
714
+ transitionId: manualTransition.transitionId,
715
+ target: manualTransition.target,
716
+ },
717
+ } : {}),
718
+ };
719
+ }
720
+ if (manualTransition) {
721
+ return {
722
+ status: 'invalid',
723
+ detail: 'legacy bootstrap marker conflicts with manual-transition state',
724
+ };
725
+ }
726
+ return {
727
+ status: 'pending',
728
+ detail: 'legacy bootstrap marker requires exact CLI adoption',
729
+ ...withRecoveryLocks,
730
+ };
731
+ }
732
+ if (manualTransition) {
733
+ if (journal) {
734
+ if (journal.terminalAction !== 'remove_committed'
735
+ || journal.transactionId !== manualTransition.removedTransactionId
736
+ || journal.target !== manualTransition.target) {
737
+ return {
738
+ status: 'invalid',
739
+ detail: 'bootstrap manual-transition tombstone conflicts with recovery journal',
740
+ };
741
+ }
742
+ if ((marker && (marker.transactionId !== manualTransition.removedTransactionId
743
+ || marker.target !== manualTransition.target))
744
+ || (readiness && readiness.transactionId !== manualTransition.removedTransactionId)) {
745
+ return {
746
+ status: 'invalid',
747
+ detail: 'bootstrap manual-transition tombstone conflicts with committed state',
748
+ };
749
+ }
750
+ return {
751
+ status: 'pending',
752
+ detail: 'committed service removal recovery is pending',
753
+ ...withRecoveryLocks,
754
+ manualTransition: {
755
+ transitionId: manualTransition.transitionId,
756
+ target: manualTransition.target,
757
+ },
758
+ };
759
+ }
760
+ if ((marker && (marker.transactionId !== manualTransition.removedTransactionId
761
+ || marker.target !== manualTransition.target))
762
+ || (readiness && (!marker
763
+ || readiness.transactionId !== manualTransition.removedTransactionId))) {
764
+ return {
765
+ status: 'invalid',
766
+ detail: 'bootstrap manual-transition tombstone conflicts with committed state',
767
+ };
768
+ }
769
+ if (marker) {
770
+ return {
771
+ status: 'pending',
772
+ detail: 'manual service removal has not retired the committed marker; rerun remove-service',
773
+ ...withRecoveryLocks,
774
+ manualTransition: {
775
+ transitionId: manualTransition.transitionId,
776
+ target: manualTransition.target,
777
+ },
778
+ };
779
+ }
780
+ if (activeRecoveryLock) {
781
+ return {
782
+ status: 'pending',
783
+ detail: 'manual service transition lifecycle lock ownership is active or unverifiable',
784
+ ...withRecoveryLocks,
785
+ manualTransition: {
786
+ transitionId: manualTransition.transitionId,
787
+ target: manualTransition.target,
788
+ },
789
+ };
790
+ }
791
+ return {
792
+ status: 'manual_transition',
793
+ transitionId: manualTransition.transitionId,
794
+ target: manualTransition.target,
795
+ };
796
+ }
797
+ if (!marker && !journal && !ownerLockPresent && !readiness && !readinessLockPresent) {
798
+ return { status: 'clean' };
799
+ }
800
+ if (marker?.managerBindingKind === coreBootstrap.LIFECYCLE_BOOTSTRAP_LEGACY_BINDING) {
801
+ const proofFailure = validateLegacyBootstrapStateRootProof(marker, stateDir, env, options);
802
+ if (proofFailure) {
803
+ return { status: 'invalid', detail: proofFailure };
804
+ }
805
+ if (readiness) {
806
+ return {
807
+ status: 'invalid',
808
+ detail: 'legacy bootstrap binding cannot own a transaction readiness receipt',
809
+ };
810
+ }
811
+ if (journal) {
812
+ return {
813
+ status: 'pending',
814
+ detail: 'legacy bootstrap adoption journal requires CLI recovery',
815
+ ...withRecoveryLocks,
816
+ };
817
+ }
818
+ if (activeRecoveryLock) {
819
+ return {
820
+ status: 'pending',
821
+ detail: 'legacy bootstrap adoption owner lock remains active or readiness lock is unverifiable',
822
+ ...withRecoveryLocks,
823
+ };
824
+ }
825
+ return {
826
+ status: 'committed',
827
+ transactionId: marker.transactionId,
828
+ target: marker.target,
829
+ };
830
+ }
831
+ if (marker && readiness) {
832
+ if (marker.transactionId !== readiness.transactionId) {
833
+ return { status: 'invalid', detail: 'bootstrap marker and readiness owners differ' };
834
+ }
835
+ if (journal && (journal.transactionId !== marker.transactionId
836
+ || !['committing', 'committed'].includes(journal.stage))) {
837
+ return { status: 'invalid', detail: 'bootstrap journal conflicts with committed receipt' };
838
+ }
839
+ if (activeRecoveryLock) {
840
+ return {
841
+ status: 'pending',
842
+ detail: 'committed lifecycle state still has active or unverifiable lock ownership',
843
+ ...withRecoveryLocks,
844
+ };
845
+ }
846
+ return {
847
+ status: 'committed',
848
+ transactionId: marker.transactionId,
849
+ target: marker.target,
850
+ };
851
+ }
852
+ const now = options.now ?? Date.now();
853
+ const supervisedActivation = !marker
854
+ && journal?.stage === 'activating'
855
+ && journal.activationStarted === true
856
+ && journal.activationContractValid === true
857
+ && journal.target !== undefined
858
+ && journal.owner !== undefined
859
+ && journal.deadlineMs !== undefined
860
+ && Number.isSafeInteger(now)
861
+ && now >= 0
862
+ && journal.deadlineMs > now
863
+ && ownerLock?.processStatus === 'current'
864
+ && ownerLock.owner !== undefined
865
+ && sameBootstrapLockOwner(ownerLock.owner, journal.owner)
866
+ && readinessLock?.recoveryStatus !== 'active_or_unverifiable'
867
+ && (!readiness || readiness.transactionId === journal.transactionId)
868
+ ? {
869
+ transactionId: journal.transactionId,
870
+ target: journal.target,
871
+ }
872
+ : undefined;
873
+ const present = [
874
+ marker ? 'marker' : undefined,
875
+ journal ? 'journal' : undefined,
876
+ ownerLockPresent ? 'owner lock' : undefined,
877
+ readiness ? 'readiness' : undefined,
878
+ readinessLockPresent ? 'readiness lock' : undefined,
879
+ ].filter((value) => value !== undefined);
880
+ return {
881
+ status: 'pending',
882
+ detail: `partial durable bootstrap state: ${present.join(',')}`,
883
+ ...withRecoveryLocks,
884
+ ...(supervisedActivation ? { supervisedActivation } : {}),
885
+ };
886
+ }
887
+ export function assertSupervisedLifecycleBootstrapState(env, options = {}) {
888
+ const capabilityRaw = env[RECOVERY_CONTROLLER_LIFECYCLE_OWNER_CAPABILITY_ENV];
889
+ const controllerCapability = capabilityRaw === undefined
890
+ ? undefined
891
+ : recoveryControllerLifecycleOwnerCapability(env, options.recoveryControllerParentPid ?? process.ppid);
892
+ if (capabilityRaw !== undefined && !controllerCapability) {
893
+ throw new Error('self_update_recovery_controller_lifecycle_capability_invalid');
894
+ }
895
+ if (!selfUpdateSupervisorAttested(env)) {
896
+ if (controllerCapability) {
897
+ assertRecoveryControllerLifecycleOwnerLease(env, controllerCapability.owner, options);
898
+ }
899
+ return;
900
+ }
901
+ const supervisor = env['EVOLVER_SELF_UPDATE_SUPERVISOR']?.trim();
902
+ const supervisorTarget = supervisor === 'systemd'
903
+ ? 'systemd'
904
+ : supervisor === 'launchd'
905
+ ? 'launchd'
906
+ : supervisor === 'windows-scheduled-task'
907
+ ? 'windows'
908
+ : undefined;
909
+ const transactionValue = env[coreBootstrap.LIFECYCLE_BOOTSTRAP_TRANSACTION_ENV];
910
+ const transactionId = transactionValue?.trim();
911
+ if (!lifecycleBootstrapStatePresent(env, options.exists ?? bootstrapStateEntryPresent)) {
912
+ if (transactionValue !== undefined || options.requireLifecycleState) {
913
+ throw new Error('self_update_supervisor_bootstrap_state_invalid:'
914
+ + (transactionValue !== undefined
915
+ ? 'transaction-bound supervisor has no durable lifecycle state'
916
+ : 'unpinned legacy supervisor has no durable lifecycle state'));
917
+ }
918
+ return;
919
+ }
920
+ if (controllerCapability && options.expectedRecoveryOwner
921
+ && !sameBootstrapLockOwner(controllerCapability.owner, options.expectedRecoveryOwner)) {
922
+ throw new Error('self_update_recovery_controller_lifecycle_capability_invalid:expected owner differs');
923
+ }
924
+ const durable = inspectLifecycleBootstrapDurableState(env, {
925
+ ...options,
926
+ expectedRecoveryOwner: options.expectedRecoveryOwner ?? controllerCapability?.owner,
927
+ });
928
+ if (durable.status === 'committed') {
929
+ if (durable.target !== supervisorTarget) {
930
+ throw new Error('self_update_supervisor_bootstrap_state_invalid:'
931
+ + 'committed lifecycle target does not match supervisor attestation');
932
+ }
933
+ if (transactionValue !== undefined
934
+ && (!transactionId
935
+ || !BOOTSTRAP_UUID_RE.test(transactionId)
936
+ || transactionId !== durable.transactionId)) {
937
+ throw new Error('self_update_supervisor_bootstrap_state_invalid:'
938
+ + 'committed lifecycle transaction does not match supervisor attestation');
939
+ }
940
+ return;
941
+ }
942
+ if (durable.status === 'pending' && durable.supervisedActivation) {
943
+ if (!transactionId
944
+ || !BOOTSTRAP_UUID_RE.test(transactionId)
945
+ || transactionId !== durable.supervisedActivation.transactionId
946
+ || durable.supervisedActivation.target !== supervisorTarget) {
947
+ throw new Error('self_update_supervisor_bootstrap_state_invalid:'
948
+ + 'activating lifecycle transaction does not match supervisor attestation');
949
+ }
950
+ return;
951
+ }
952
+ const detail = durable.status === 'pending' || durable.status === 'invalid'
953
+ ? durable.detail
954
+ : 'supervised lifecycle state is not committed';
955
+ throw new Error('self_update_supervisor_bootstrap_state_invalid:' + boundedBootstrapDetail(detail));
956
+ }
957
+ /**
958
+ * Revalidate the narrow parent-owned activation window used by a newly launched recovery
959
+ * controller. This deliberately rejects committed supervision: callers use it only as delegated
960
+ * authority while the lifecycle parent still owns the mutation lock and is waiting for readiness.
961
+ */
962
+ export function assertActiveSupervisedLifecycleBootstrapDelegation(env, options = {}) {
963
+ const supervisor = env['EVOLVER_SELF_UPDATE_SUPERVISOR']?.trim();
964
+ const supervisorTarget = supervisor === 'systemd'
965
+ ? 'systemd'
966
+ : supervisor === 'launchd'
967
+ ? 'launchd'
968
+ : supervisor === 'windows-scheduled-task'
969
+ ? 'windows'
970
+ : undefined;
971
+ const transactionId = env[coreBootstrap.LIFECYCLE_BOOTSTRAP_TRANSACTION_ENV]?.trim();
972
+ if (!selfUpdateSupervisorAttested(env)
973
+ || !transactionId
974
+ || !BOOTSTRAP_UUID_RE.test(transactionId)
975
+ || !lifecycleBootstrapStatePresent(env, options.exists ?? bootstrapStateEntryPresent)) {
976
+ throw new Error('self_update_supervisor_bootstrap_delegation_invalid:'
977
+ + 'controller has no valid activating lifecycle attestation');
978
+ }
979
+ const durable = inspectLifecycleBootstrapDurableState(env, options);
980
+ if (durable.status === 'pending'
981
+ && durable.supervisedActivation?.transactionId === transactionId
982
+ && durable.supervisedActivation.target === supervisorTarget) {
983
+ return;
984
+ }
985
+ const detail = durable.status === 'pending' || durable.status === 'invalid'
986
+ ? durable.detail
987
+ : 'lifecycle state is not an active delegated activation';
988
+ throw new Error('self_update_supervisor_bootstrap_delegation_invalid:'
989
+ + boundedBootstrapDetail(detail));
990
+ }
991
+ /**
992
+ * Revalidate a transaction-bound launcher before any self-update operation. Unlike the startup
993
+ * assertion above, this never accepts the narrow activating window used to publish readiness.
994
+ */
995
+ export function assertCommittedLifecycleBootstrapState(env, options = {}) {
996
+ const transactionValue = env[coreBootstrap.LIFECYCLE_BOOTSTRAP_TRANSACTION_ENV];
997
+ if (transactionValue === undefined)
998
+ return;
999
+ const transactionId = transactionValue.trim();
1000
+ const supervisor = env['EVOLVER_SELF_UPDATE_SUPERVISOR']?.trim();
1001
+ const supervisorTarget = supervisor === 'systemd'
1002
+ ? 'systemd'
1003
+ : supervisor === 'launchd'
1004
+ ? 'launchd'
1005
+ : supervisor === 'windows-scheduled-task'
1006
+ ? 'windows'
1007
+ : undefined;
1008
+ if (!selfUpdateSupervisorAttested(env)
1009
+ || !transactionId
1010
+ || !BOOTSTRAP_UUID_RE.test(transactionId)
1011
+ || !lifecycleBootstrapStatePresent(env, options.exists ?? bootstrapStateEntryPresent)) {
1012
+ throw new Error('self_update_supervisor_bootstrap_state_invalid:'
1013
+ + 'transaction-bound self-update has no valid committed lifecycle attestation');
1014
+ }
1015
+ const durable = inspectLifecycleBootstrapDurableState(env, options);
1016
+ if (durable.status === 'committed'
1017
+ && durable.transactionId === transactionId
1018
+ && durable.target === supervisorTarget) {
1019
+ return;
1020
+ }
1021
+ const detail = durable.status === 'pending' || durable.status === 'invalid'
1022
+ ? durable.detail
1023
+ : 'transaction-bound self-update lifecycle state is not committed';
1024
+ throw new Error('self_update_supervisor_bootstrap_state_invalid:' + boundedBootstrapDetail(detail));
1025
+ }
1026
+ /**
1027
+ * Serialize self-update with every lifecycle bootstrap, recovery, and manual-transition writer.
1028
+ * Acquisition is deliberately fail-fast: a heartbeat must not block the daemon while another
1029
+ * lifecycle owner is active. Transaction-bound launchers revalidate the committed receipt under
1030
+ * the exact acquired generation; legacy launchers still hold and recheck the shared owner lock.
1031
+ */
1032
+ export function acquireLifecycleBootstrapOwnerLease(env, lockOptions = { maxTries: 2, waitMs: 0 }) {
1033
+ const transactionBound = env[coreBootstrap.LIFECYCLE_BOOTSTRAP_TRANSACTION_ENV] !== undefined;
1034
+ if (transactionBound)
1035
+ assertCommittedLifecycleBootstrapState(env);
1036
+ const stateDir = resolveBootstrapStateDir(env);
1037
+ assertNativeBootstrapIntentDirectoryTrust(stateDir, true);
1038
+ mkdirSync(stateDir, { recursive: true, mode: 0o700 });
1039
+ assertNativeBootstrapIntentDirectoryTrust(stateDir, true);
1040
+ const path = join(stateDir, coreBootstrap.LIFECYCLE_BOOTSTRAP_OWNER_LOCK_FILE);
1041
+ const owner = util.acquireLock(path, lockOptions);
1042
+ let state = 'active';
1043
+ let guardian;
1044
+ const assertOwned = () => {
1045
+ if (state !== 'active') {
1046
+ throw new Error('self_update_lifecycle_owner_lease_not_active');
1047
+ }
1048
+ const lock = readBoundedBootstrapStateFile(path, { assertIntentFileTrust: () => undefined }, util.MAX_LOCK_OWNER_BYTES);
1049
+ const current = lock.status === 'present'
1050
+ ? parseBootstrapLockOwnerJson(lock.raw)
1051
+ : undefined;
1052
+ if (!current || !sameBootstrapLockOwner(current, owner)) {
1053
+ throw new Error('self_update_lifecycle_owner_lease_ownership_changed');
1054
+ }
1055
+ };
1056
+ const release = () => {
1057
+ if (state === 'transferred')
1058
+ return;
1059
+ if (state === 'released')
1060
+ return;
1061
+ if (state === 'release_failed') {
1062
+ throw new Error('self_update_lifecycle_owner_lease_release_failed');
1063
+ }
1064
+ const result = util.releaseLock(path);
1065
+ if (result.released
1066
+ && (result.reason === 'released' || result.reason === 'released_with_cleanup_error')) {
1067
+ state = 'released';
1068
+ return;
1069
+ }
1070
+ state = 'release_failed';
1071
+ throw new Error(`self_update_lifecycle_owner_lease_release_failed:${result.reason}`);
1072
+ };
1073
+ const armProcess = (pid) => {
1074
+ assertOwned();
1075
+ if (guardian !== undefined) {
1076
+ throw new Error('self_update_lifecycle_owner_lease_guardian_already_armed');
1077
+ }
1078
+ const result = util.attachLockGuardianToProcess(path, owner, pid);
1079
+ if (!result.attached) {
1080
+ throw new Error(`self_update_lifecycle_owner_lease_guardian_arm_failed:${result.reason}`);
1081
+ }
1082
+ guardian = result.guardian;
1083
+ return guardian;
1084
+ };
1085
+ const disarmProcess = () => {
1086
+ assertOwned();
1087
+ if (guardian === undefined)
1088
+ return;
1089
+ const result = util.clearLockGuardianForProcess(path, owner, guardian);
1090
+ if (!result.cleared) {
1091
+ throw new Error(`self_update_lifecycle_owner_lease_guardian_disarm_failed:${result.reason}`);
1092
+ }
1093
+ guardian = undefined;
1094
+ };
1095
+ const retainProcess = () => {
1096
+ assertOwned();
1097
+ if (guardian === undefined) {
1098
+ throw new Error('self_update_lifecycle_owner_lease_guardian_not_armed');
1099
+ }
1100
+ const result = util.retainLockGuardianForProcess(path, owner, guardian);
1101
+ if (!result.retained) {
1102
+ throw new Error(`self_update_lifecycle_owner_lease_guardian_retain_failed:${result.reason}`);
1103
+ }
1104
+ state = 'transferred';
1105
+ };
1106
+ const transferToProcess = (pid) => {
1107
+ const attached = armProcess(pid);
1108
+ retainProcess();
1109
+ return attached;
1110
+ };
1111
+ try {
1112
+ assertOwned();
1113
+ const options = { expectedRecoveryOwner: owner };
1114
+ if (transactionBound) {
1115
+ assertCommittedLifecycleBootstrapState(env, options);
1116
+ }
1117
+ else {
1118
+ const durable = inspectLifecycleBootstrapDurableState(env, options);
1119
+ if (durable.status === 'invalid') {
1120
+ throw new Error('self_update_lifecycle_owner_lease_invalid:'
1121
+ + boundedBootstrapDetail(durable.detail));
1122
+ }
1123
+ if (durable.status === 'pending' || durable.status === 'manual_transition') {
1124
+ const detail = durable.status === 'pending'
1125
+ ? durable.detail
1126
+ : 'manual_transition';
1127
+ throw new Error('self_update_lifecycle_owner_lease_state_blocked:'
1128
+ + boundedBootstrapDetail(detail));
1129
+ }
1130
+ const supervisor = env['EVOLVER_SELF_UPDATE_SUPERVISOR']?.trim();
1131
+ const supervisorTarget = supervisor === 'systemd'
1132
+ ? 'systemd'
1133
+ : supervisor === 'launchd'
1134
+ ? 'launchd'
1135
+ : supervisor === 'windows-scheduled-task'
1136
+ ? 'windows'
1137
+ : undefined;
1138
+ if (durable.status === 'committed'
1139
+ && supervisorTarget !== undefined
1140
+ && durable.target !== supervisorTarget) {
1141
+ throw new Error('self_update_lifecycle_owner_lease_state_blocked:'
1142
+ + 'committed lifecycle target does not match supervisor attestation');
1143
+ }
1144
+ }
1145
+ }
1146
+ catch (error) {
1147
+ try {
1148
+ release();
1149
+ }
1150
+ catch (releaseError) {
1151
+ throw new AggregateError([error, releaseError], 'self_update_lifecycle_owner_lease_validation_and_release_failed');
1152
+ }
1153
+ throw error;
1154
+ }
1155
+ return {
1156
+ path,
1157
+ owner,
1158
+ assertOwned,
1159
+ armProcess,
1160
+ disarmProcess,
1161
+ retainProcess,
1162
+ transferToProcess,
1163
+ release,
1164
+ };
1165
+ }
1166
+ export function looksLikeContainer(exists, readFile) {
1167
+ if (exists('/.dockerenv'))
1168
+ return true;
1169
+ try {
1170
+ return /docker|containerd|kubepods|podman|lxc/.test(readFile('/proc/1/cgroup'));
1171
+ }
1172
+ catch {
1173
+ return false;
1174
+ }
1175
+ }
1176
+ const BOOTSTRAP_ATTEMPT_OUTCOMES = new Set([
1177
+ 'already_supervised',
1178
+ 'already_bootstrapped',
1179
+ 'unsupported_install_shape',
1180
+ 'policy_not_auto',
1181
+ 'bootstrap_disabled',
1182
+ 'ci_environment',
1183
+ 'container_environment',
1184
+ 'recent_failure',
1185
+ 'failed',
1186
+ 'cli_not_found',
1187
+ 'rolled_back',
1188
+ 'bootstrapped',
1189
+ 'bootstrapped_lock_release_unconfirmed',
1190
+ 'blocked',
1191
+ 'ambiguous',
1192
+ 'termination_unconfirmed',
1193
+ 'timeout',
1194
+ 'signal',
1195
+ 'migrated',
1196
+ 'migration_failed',
1197
+ 'migration_timeout',
1198
+ 'migration_ambiguous',
1199
+ ]);
1200
+ const BOOTSTRAP_ATTEMPT_PENDING_OUTCOMES = new Set([
1201
+ 'blocked',
1202
+ 'ambiguous',
1203
+ 'termination_unconfirmed',
1204
+ 'timeout',
1205
+ 'signal',
1206
+ 'migration_ambiguous',
1207
+ ]);
1208
+ function readBootstrapAttempt(env, options) {
1209
+ try {
1210
+ const state = readBoundedBootstrapStateFile(join(resolveBootstrapStateDir(env), BOOTSTRAP_ATTEMPT_FILE), options, MAX_BOOTSTRAP_ATTEMPT_BYTES);
1211
+ if (state.status === 'absent')
1212
+ return { status: 'absent' };
1213
+ if (state.status === 'invalid')
1214
+ return { status: 'invalid' };
1215
+ const parsed = JSON.parse(state.raw);
1216
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1217
+ return { status: 'invalid' };
1218
+ }
1219
+ const record = parsed;
1220
+ if (typeof record['outcome'] !== 'string'
1221
+ || !BOOTSTRAP_ATTEMPT_OUTCOMES.has(record['outcome'])
1222
+ || typeof record['attemptedAt'] !== 'string') {
1223
+ return { status: 'invalid' };
1224
+ }
1225
+ const attemptedAt = Date.parse(record['attemptedAt']);
1226
+ if (!Number.isFinite(attemptedAt)
1227
+ || new Date(attemptedAt).toISOString() !== record['attemptedAt']
1228
+ || (record['detail'] !== undefined
1229
+ && typeof record['detail'] !== 'string')) {
1230
+ return { status: 'invalid' };
1231
+ }
1232
+ return BOOTSTRAP_ATTEMPT_PENDING_OUTCOMES.has(record['outcome'])
1233
+ ? { status: 'ambiguous', attemptedAt }
1234
+ : { status: 'safe_non_ambiguous', attemptedAt, outcome: record['outcome'] };
1235
+ }
1236
+ catch {
1237
+ return { status: 'invalid' };
1238
+ }
1239
+ }
1240
+ function bootstrapRegistrationIntentPresent(env) {
1241
+ const stateDir = resolveBootstrapStateDir(env);
1242
+ return bootstrapStateEntryPresent(join(stateDir, BOOTSTRAP_REGISTRATION_INTENT_FILE))
1243
+ || bootstrapStateEntryPresent(join(stateDir, BOOTSTRAP_REGISTRATION_INTENT_PUBLISHING_FILE))
1244
+ || bootstrapStateEntryPresent(join(stateDir, BOOTSTRAP_REGISTRATION_INTENT_TERMINAL_FILE))
1245
+ || bootstrapStateEntryPresent(join(stateDir, BOOTSTRAP_REGISTRATION_INTENT_CLEARING_FILE))
1246
+ || bootstrapRegistrationPublisherEntryPresent(stateDir);
1247
+ }
1248
+ function bootstrapRegistrationPublisherEntryPresent(stateDir) {
1249
+ try {
1250
+ return readdirSync(stateDir).some((name) => name.startsWith(BOOTSTRAP_REGISTRATION_INTENT_PUBLISHER_PREFIX));
1251
+ }
1252
+ catch (error) {
1253
+ return !isErrno(error, 'ENOENT');
1254
+ }
1255
+ }
1256
+ // Only outcomes that prove no competing child remains may suppress a retry. Ambiguous bootstrap
1257
+ // outcomes deliberately stay out of this set so recovery is never bypassed by the cooldown.
1258
+ const BOOTSTRAP_COOLDOWN_OUTCOMES = new Set([
1259
+ 'failed',
1260
+ 'cli_not_found',
1261
+ 'rolled_back',
1262
+ 'migration_failed',
1263
+ 'migration_timeout',
1264
+ ]);
1265
+ /** True when a recent bootstrap/migration attempt failed within the cooldown window. */
1266
+ export function recentBootstrapFailure(env, optionsOrReadFile, now) {
1267
+ const options = typeof optionsOrReadFile === 'function'
1268
+ ? { readFile: optionsOrReadFile }
1269
+ : optionsOrReadFile;
1270
+ const attempt = readBootstrapAttempt(env, options);
1271
+ if (attempt.status !== 'safe_non_ambiguous'
1272
+ || !BOOTSTRAP_COOLDOWN_OUTCOMES.has(attempt.outcome))
1273
+ return false;
1274
+ const ageMs = now - attempt.attemptedAt;
1275
+ return ageMs >= 0 && ageMs < BOOTSTRAP_FAILURE_COOLDOWN_MS;
1276
+ }
1277
+ /**
1278
+ * Decide whether an unsupervised (degraded) startup should attempt first-run bootstrap.
1279
+ * Pure — filesystem access is injectable for tests.
1280
+ */
1281
+ export function shouldBootstrap(env, platform = process.platform, options = {}) {
1282
+ if (selfUpdateSupervisorAttested(env)) {
1283
+ try {
1284
+ resolveSelfUpdateTarget({ env, processExecPath: options.execPath });
1285
+ }
1286
+ catch {
1287
+ return { proceed: false, reason: 'unsupported_install_shape' };
1288
+ }
1289
+ return { proceed: false, reason: 'already_supervised' };
1290
+ }
1291
+ const exists = options.exists ?? existsSync;
1292
+ // The no-replace intent predates child creation and therefore outranks every lifecycle
1293
+ // state fragment. Recovery must classify it without creating another child.
1294
+ if (bootstrapRegistrationIntentPresent(env)) {
1295
+ return { proceed: false, reason: 'bootstrap_intent_pending' };
1296
+ }
1297
+ if (lifecycleBootstrapStatePresent(env, options.exists ?? bootstrapStateEntryPresent)) {
1298
+ try {
1299
+ resolveSelfUpdateTarget({ env, processExecPath: options.execPath });
1300
+ return { proceed: true };
1301
+ }
1302
+ catch {
1303
+ // Durable lifecycle state must be reconciled, but an npm/JS foreground cannot safely
1304
+ // perform that reconciliation through ordinary lifecycle bootstrap: it would bind the
1305
+ // service manager back to the same unreplaceable Node launcher. Route recovery through
1306
+ // the signed standalone migration and keep every non-migrated outcome fail-closed.
1307
+ return { proceed: false, reason: 'bootstrap_attempt_pending' };
1308
+ }
1309
+ }
1310
+ const attempt = readBootstrapAttempt(env, options);
1311
+ if (attempt.status === 'invalid') {
1312
+ return { proceed: false, reason: 'bootstrap_attempt_invalid' };
1313
+ }
1314
+ if (attempt.status === 'ambiguous') {
1315
+ try {
1316
+ resolveSelfUpdateTarget({ env, processExecPath: options.execPath });
1317
+ return { proceed: true, reason: 'bootstrap_attempt_pending' };
1318
+ }
1319
+ catch {
1320
+ // An npm/JS foreground cannot safely run ordinary lifecycle bootstrap. Retry the
1321
+ // signed standalone migration, whose reconciler keeps this ambiguity fail-closed.
1322
+ return { proceed: false, reason: 'bootstrap_attempt_pending' };
1323
+ }
1324
+ }
1325
+ const bootstrapSwitch = env['EVOLVER_SELF_BOOTSTRAP']?.trim();
1326
+ if (bootstrapSwitch === '0' || bootstrapSwitch === 'off')
1327
+ return { proceed: false, reason: 'bootstrap_disabled' };
1328
+ if (isSelfUpdateExplicit(env) && resolveSelfUpdatePolicy(env) !== 'auto') {
1329
+ return { proceed: false, reason: 'policy_not_auto' };
1330
+ }
1331
+ // The npm/JS install shape has no replaceable standalone binary target, so the launcher
1332
+ // bootstrap would register a supervised instance that crashes at self-update target
1333
+ // resolution on every startup (crash-loop under the service manager). Skip it; an explicit
1334
+ // EVOLVER_SELF_UPDATE_TARGET_PATH keeps the target bindable and bypasses this guard.
1335
+ try {
1336
+ resolveSelfUpdateTarget({ env, processExecPath: options.execPath });
1337
+ }
1338
+ catch {
1339
+ return { proceed: false, reason: 'unsupported_install_shape' };
1340
+ }
1341
+ const ci = env['CI']?.trim();
1342
+ if (ci && ci.toLowerCase() !== 'false' && ci !== '0')
1343
+ return { proceed: false, reason: 'ci_environment' };
1344
+ if (platform === 'linux' && looksLikeContainer(exists, options.readFile ?? defaultReadTextFile)) {
1345
+ return { proceed: false, reason: 'container_environment' };
1346
+ }
1347
+ if (recentBootstrapFailure(env, options, options.now ?? Date.now())) {
1348
+ return { proceed: false, reason: 'recent_failure' };
1349
+ }
1350
+ return { proceed: true };
1351
+ }
1352
+ /**
1353
+ * Resolve the `lifecycle bootstrap` invocation for the current install shape: standalone binary,
1354
+ * CLI entry through node, or the npm-installed @evomap/evolver-cli sibling. Returns undefined when
1355
+ * no CLI can be located. The caller may continue only after confirming durable state is clean.
1356
+ */
1357
+ export function resolveBootstrapCliInvocation(options = {}) {
1358
+ const execPath = options.execPath ?? process.execPath;
1359
+ const argv1 = options.argv1 ?? process.argv[1];
1360
+ const exists = options.exists ?? existsSync;
1361
+ const executableName = basename(execPath).toLowerCase();
1362
+ if (isStandaloneReleaseBinaryName(executableName)) {
1363
+ return { command: execPath, args: ['lifecycle', 'bootstrap'] };
1364
+ }
1365
+ if (argv1 && basename(argv1).toLowerCase() === 'cli.js') {
1366
+ return { command: execPath, args: [argv1, 'lifecycle', 'bootstrap'] };
1367
+ }
1368
+ try {
1369
+ const entry = requireFromHere.resolve('@evomap/evolver-cli');
1370
+ const cliPath = join(dirname(entry), 'cli.js');
1371
+ if (exists(cliPath))
1372
+ return { command: execPath, args: [cliPath, 'lifecycle', 'bootstrap'] };
1373
+ }
1374
+ catch {
1375
+ // Not resolvable from the installed proxy package — fall through to the monorepo layout.
1376
+ }
1377
+ const local = fileURLToPath(new URL('../../../evolver-cli/dist/cli.js', import.meta.url));
1378
+ if (exists(local))
1379
+ return { command: execPath, args: [local, 'lifecycle', 'bootstrap'] };
1380
+ return undefined;
1381
+ }
1382
+ /** Best-effort attempt marker; never throws — bootstrap bookkeeping must not break startup. */
1383
+ export function recordBootstrapAttempt(env, outcome, options = {}) {
1384
+ const path = join(resolveBootstrapStateDir(env), BOOTSTRAP_ATTEMPT_FILE);
1385
+ const record = {
1386
+ attemptedAt: new Date(options.now ?? Date.now()).toISOString(),
1387
+ outcome: outcome.reason,
1388
+ ...(outcome.detail ? { detail: boundedBootstrapDetail(outcome.detail) } : {}),
1389
+ };
1390
+ try {
1391
+ const writeFile = options.writeFile ?? ((target, content) => {
1392
+ mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
1393
+ const temporary = join(dirname(target), `.${basename(target)}.${process.pid}.${randomUUID()}.tmp`);
1394
+ let descriptor;
1395
+ try {
1396
+ descriptor = openSync(temporary, 'wx', 0o600);
1397
+ writeFileSync(descriptor, content, { encoding: 'utf8' });
1398
+ fsyncSync(descriptor);
1399
+ closeSync(descriptor);
1400
+ descriptor = undefined;
1401
+ renameSync(temporary, target);
1402
+ }
1403
+ finally {
1404
+ if (descriptor !== undefined)
1405
+ closeSync(descriptor);
1406
+ rmSync(temporary, { force: true });
1407
+ }
1408
+ });
1409
+ writeFile(path, `${JSON.stringify(record)}\n`);
1410
+ }
1411
+ catch {
1412
+ // Marker is advisory; startup continues regardless.
1413
+ }
1414
+ }
1415
+ function captureBootstrapOutput(stream) {
1416
+ const chunks = [];
1417
+ let capturedBytes = 0;
1418
+ let truncated = false;
1419
+ stream?.on('data', (chunk) => {
1420
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1421
+ const available = Math.max(0, MAX_BOOTSTRAP_OUTPUT_BYTES - capturedBytes);
1422
+ if (bytes.byteLength > available)
1423
+ truncated = true;
1424
+ if (available > 0) {
1425
+ const captured = bytes.subarray(0, available);
1426
+ chunks.push(captured);
1427
+ capturedBytes += captured.byteLength;
1428
+ }
1429
+ });
1430
+ return {
1431
+ get truncated() { return truncated; },
1432
+ text: () => Buffer.concat(chunks, capturedBytes).toString('utf8'),
1433
+ };
1434
+ }
1435
+ function parseBootstrapCliResult(output) {
1436
+ if (output.truncated)
1437
+ return undefined;
1438
+ const raw = output.text().trim();
1439
+ if (!raw)
1440
+ return undefined;
1441
+ try {
1442
+ const parsed = JSON.parse(raw);
1443
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
1444
+ return undefined;
1445
+ const record = parsed;
1446
+ if (record['status'] === 'bootstrap-committed-lock-release-unconfirmed') {
1447
+ const result = record['result'];
1448
+ const resultRecord = result && typeof result === 'object' && !Array.isArray(result)
1449
+ ? result
1450
+ : undefined;
1451
+ const transactionId = resultRecord?.['transactionId'];
1452
+ if (record['outcome'] !== 'committed'
1453
+ || !resultRecord
1454
+ || !['bootstrapped', 'already-bootstrapped']
1455
+ .includes(resultRecord['status'])
1456
+ || typeof transactionId !== 'string'
1457
+ || !BOOTSTRAP_UUID_RE.test(transactionId)
1458
+ || record['transactionId'] !== transactionId) {
1459
+ return undefined;
1460
+ }
1461
+ const detail = typeof record['detail'] === 'string' && record['detail'].trim()
1462
+ ? boundedBootstrapDetail(record['detail'])
1463
+ : undefined;
1464
+ if (!detail)
1465
+ return undefined;
1466
+ return { outcome: 'committed_lock_release_unconfirmed', transactionId, detail };
1467
+ }
1468
+ if (record['status'] === 'bootstrapped' || record['status'] === 'already-bootstrapped') {
1469
+ const transactionId = record['transactionId'];
1470
+ if (typeof transactionId !== 'string' || !BOOTSTRAP_UUID_RE.test(transactionId)) {
1471
+ return undefined;
1472
+ }
1473
+ return { outcome: 'bootstrapped', transactionId };
1474
+ }
1475
+ if (record['status'] !== 'bootstrap-failed')
1476
+ return undefined;
1477
+ const detail = typeof record['detail'] === 'string'
1478
+ ? boundedBootstrapDetail(record['detail'])
1479
+ : undefined;
1480
+ if (record['outcome'] === 'rolled_back') {
1481
+ return { outcome: 'rolled_back', ...(detail ? { detail } : {}) };
1482
+ }
1483
+ if (record['outcome'] === 'blocked') {
1484
+ return { outcome: 'blocked', ...(detail ? { detail } : {}) };
1485
+ }
1486
+ return undefined;
1487
+ }
1488
+ catch {
1489
+ return undefined;
1490
+ }
1491
+ }
1492
+ function observationDetail(observation, stdout, stderr) {
1493
+ const parts = [];
1494
+ if ('detail' in observation && observation.detail)
1495
+ parts.push(observation.detail);
1496
+ if (stdout.truncated)
1497
+ parts.push('stdout_truncated');
1498
+ if (stderr.truncated)
1499
+ parts.push('stderr_truncated');
1500
+ const stderrText = boundedBootstrapDetail(stderr.text().trim());
1501
+ if (stderrText)
1502
+ parts.push(stderrText);
1503
+ return parts.length > 0 ? boundedBootstrapDetail(parts.join('; ')) : undefined;
1504
+ }
1505
+ function reconcileBootstrapOutcome(observation, state, stdout, stderr) {
1506
+ const detail = observationDetail(observation, stdout, stderr);
1507
+ const failClosed = (reason, failureDetail) => ({
1508
+ ok: false,
1509
+ reason,
1510
+ ...(failureDetail ? { detail: boundedBootstrapDetail(failureDetail) } : {}),
1511
+ requiresForegroundExit: true,
1512
+ });
1513
+ const stateDetail = state.status === 'pending' || state.status === 'invalid' ? state.detail : undefined;
1514
+ const combinedDetail = [detail, stateDetail].filter((value) => Boolean(value)).join('; ');
1515
+ if (observation.kind === 'termination_unconfirmed') {
1516
+ return failClosed('termination_unconfirmed', combinedDetail || observation.detail);
1517
+ }
1518
+ if (observation.kind === 'timeout')
1519
+ return failClosed('timeout', combinedDetail || undefined);
1520
+ if (observation.kind === 'signal') {
1521
+ return failClosed('signal', combinedDetail || `signal ${observation.signal}`);
1522
+ }
1523
+ if (observation.kind === 'child_ambiguous') {
1524
+ return failClosed('ambiguous', combinedDetail || observation.detail);
1525
+ }
1526
+ if (state.status === 'invalid')
1527
+ return failClosed('ambiguous', combinedDetail);
1528
+ if (state.status === 'pending')
1529
+ return failClosed('blocked', combinedDetail);
1530
+ if (state.status === 'committed') {
1531
+ if (observation.kind === 'exit' && observation.code === 0
1532
+ && observation.cli?.outcome === 'bootstrapped'
1533
+ && observation.cli.transactionId === state.transactionId
1534
+ && !stdout.truncated) {
1535
+ return { ok: true, reason: 'bootstrapped' };
1536
+ }
1537
+ if (observation.kind === 'exit' && observation.code === 1
1538
+ && observation.cli?.outcome === 'committed_lock_release_unconfirmed'
1539
+ && observation.cli.transactionId === state.transactionId
1540
+ && !stdout.truncated) {
1541
+ return {
1542
+ ok: true,
1543
+ reason: 'bootstrapped_lock_release_unconfirmed',
1544
+ ...(observation.cli.detail ? { detail: observation.cli.detail } : {}),
1545
+ };
1546
+ }
1547
+ return failClosed('ambiguous', combinedDetail || 'durable bootstrap committed without a matching transaction-bound CLI success result');
1548
+ }
1549
+ if (observation.kind === 'no_child') {
1550
+ return {
1551
+ ok: false,
1552
+ reason: observation.reason,
1553
+ ...(detail ? { detail } : {}),
1554
+ };
1555
+ }
1556
+ if (observation.cli?.outcome === 'rolled_back' && observation.code !== 0 && !stdout.truncated) {
1557
+ return {
1558
+ ok: false,
1559
+ reason: 'rolled_back',
1560
+ ...(observation.cli.detail ? { detail: observation.cli.detail } : {}),
1561
+ };
1562
+ }
1563
+ if (observation.cli?.outcome === 'blocked') {
1564
+ return failClosed('blocked', combinedDetail || observation.cli.detail);
1565
+ }
1566
+ return failClosed('ambiguous', combinedDetail || `CLI result did not prove commit or rollback (exit ${observation.code ?? 'null'})`);
1567
+ }
1568
+ function isErrno(error, code) {
1569
+ return typeof error === 'object' && error !== null && error.code === code;
1570
+ }
1571
+ function trustedWindowsSystemExecutable(name) {
1572
+ if (!win32.isAbsolute(HOST_WINDOWS_SYSTEM_ROOT) || /[\r\n\0]/.test(HOST_WINDOWS_SYSTEM_ROOT)) {
1573
+ throw new Error('Windows SystemRoot is not an absolute trusted path');
1574
+ }
1575
+ return win32.join(HOST_WINDOWS_SYSTEM_ROOT, 'System32', name);
1576
+ }
1577
+ function remainingMs(deadlineMs) {
1578
+ return Math.max(0, deadlineMs - Date.now());
1579
+ }
1580
+ async function waitForCondition(predicate, deadlineMs) {
1581
+ while (!predicate()) {
1582
+ const remaining = remainingMs(deadlineMs);
1583
+ if (remaining === 0)
1584
+ return false;
1585
+ await new Promise((resolvePromise) => {
1586
+ setTimeout(resolvePromise, Math.min(10, remaining));
1587
+ });
1588
+ }
1589
+ return true;
1590
+ }
1591
+ function requestWindowsProcessTreeTermination(pid, spawnFn, deadlineMs) {
1592
+ return new Promise((resolvePromise) => {
1593
+ let settled = false;
1594
+ let killer;
1595
+ let timer;
1596
+ const settle = (accepted) => {
1597
+ if (settled)
1598
+ return;
1599
+ settled = true;
1600
+ if (timer)
1601
+ clearTimeout(timer);
1602
+ resolvePromise(accepted);
1603
+ };
1604
+ try {
1605
+ killer = spawnFn(trustedWindowsSystemExecutable('taskkill.exe'), ['/PID', String(pid), '/T', '/F'], {
1606
+ shell: false,
1607
+ windowsHide: true,
1608
+ stdio: 'ignore',
1609
+ });
1610
+ killer.once('error', () => settle(false));
1611
+ killer.once('close', (code) => settle(code === 0));
1612
+ const remaining = remainingMs(deadlineMs);
1613
+ if (remaining === 0) {
1614
+ try {
1615
+ killer.kill('SIGKILL');
1616
+ }
1617
+ catch { /* best-effort watchdog cleanup */ }
1618
+ settle(false);
1619
+ return;
1620
+ }
1621
+ timer = setTimeout(() => {
1622
+ try {
1623
+ killer?.kill('SIGKILL');
1624
+ }
1625
+ catch { /* best-effort watchdog cleanup */ }
1626
+ settle(false);
1627
+ }, remaining);
1628
+ }
1629
+ catch {
1630
+ settle(false);
1631
+ }
1632
+ });
1633
+ }
1634
+ async function terminateBootstrapProcessTree(child, platform, childClosed, deadlineMs, processKill, treeKillSpawnFn) {
1635
+ const pid = child.pid;
1636
+ if (!Number.isSafeInteger(pid) || typeof pid !== 'number' || pid <= 0) {
1637
+ try {
1638
+ child.kill('SIGKILL');
1639
+ }
1640
+ catch { /* direct-child fallback only */ }
1641
+ await waitForCondition(childClosed, deadlineMs);
1642
+ return { confirmed: false, detail: 'process_tree_termination_unconfirmed' };
1643
+ }
1644
+ if (platform === 'win32') {
1645
+ const treeKillAccepted = await requestWindowsProcessTreeTermination(pid, treeKillSpawnFn, deadlineMs);
1646
+ if (!treeKillAccepted) {
1647
+ try {
1648
+ child.kill('SIGKILL');
1649
+ }
1650
+ catch { /* direct-child fallback only */ }
1651
+ }
1652
+ const directChildClosed = await waitForCondition(childClosed, deadlineMs);
1653
+ return treeKillAccepted && directChildClosed
1654
+ ? { confirmed: true }
1655
+ : { confirmed: false, detail: 'process_tree_termination_unconfirmed' };
1656
+ }
1657
+ let groupKillAccepted = false;
1658
+ try {
1659
+ processKill(-pid, 'SIGKILL');
1660
+ groupKillAccepted = true;
1661
+ }
1662
+ catch (error) {
1663
+ groupKillAccepted = isErrno(error, 'ESRCH');
1664
+ if (!groupKillAccepted) {
1665
+ try {
1666
+ child.kill('SIGKILL');
1667
+ }
1668
+ catch { /* direct-child fallback only */ }
1669
+ }
1670
+ }
1671
+ const processGroupGone = () => {
1672
+ try {
1673
+ processKill(-pid, 0);
1674
+ return false;
1675
+ }
1676
+ catch (error) {
1677
+ return isErrno(error, 'ESRCH');
1678
+ }
1679
+ };
1680
+ const confirmed = groupKillAccepted
1681
+ && await waitForCondition(() => childClosed() && processGroupGone(), deadlineMs);
1682
+ return confirmed
1683
+ ? { confirmed: true }
1684
+ : { confirmed: false, detail: 'process_tree_termination_unconfirmed' };
1685
+ }
1686
+ /** Spawn `evolver lifecycle bootstrap` and await its result within a bounded timeout. */
1687
+ export async function runBootstrap(options) {
1688
+ const emptyOutput = () => captureBootstrapOutput(undefined);
1689
+ const reconcileNoChild = (reason, detail) => reconcileBootstrapOutcome({ kind: 'no_child', reason, ...(detail ? { detail } : {}) }, inspectLifecycleBootstrapDurableState(options.env, options), emptyOutput(), emptyOutput());
1690
+ const invocation = resolveBootstrapCliInvocation(options);
1691
+ if (!invocation)
1692
+ return reconcileNoChild('cli_not_found');
1693
+ const spawnFn = options.spawnFn ?? spawn;
1694
+ const timeoutMs = options.timeoutMs ?? BOOTSTRAP_TIMEOUT_MS;
1695
+ const transactionBudgetMs = options.transactionBudgetMs ?? BOOTSTRAP_TRANSACTION_BUDGET_MS;
1696
+ const terminationGraceMs = options.terminationGraceMs ?? BOOTSTRAP_TREE_TERMINATION_GRACE_MS;
1697
+ const startedAt = options.now ?? Date.now();
1698
+ if (!Number.isSafeInteger(timeoutMs)
1699
+ || !Number.isSafeInteger(transactionBudgetMs)
1700
+ || !Number.isSafeInteger(terminationGraceMs)
1701
+ || !Number.isSafeInteger(startedAt)
1702
+ || !Number.isSafeInteger(startedAt + transactionBudgetMs)
1703
+ || timeoutMs <= transactionBudgetMs
1704
+ || timeoutMs > MAX_TIMER_DELAY_MS
1705
+ || transactionBudgetMs <= 0
1706
+ || terminationGraceMs <= 0
1707
+ || terminationGraceMs > MAX_TIMER_DELAY_MS
1708
+ || startedAt < 0) {
1709
+ return reconcileNoChild('failed', 'invalid_bootstrap_timeout_contract');
1710
+ }
1711
+ const platform = options.platform ?? process.platform;
1712
+ return new Promise((resolvePromise) => {
1713
+ let child;
1714
+ try {
1715
+ child = spawnFn(invocation.command, invocation.args, {
1716
+ detached: platform !== 'win32',
1717
+ shell: false,
1718
+ stdio: ['ignore', 'pipe', 'pipe'],
1719
+ windowsHide: true,
1720
+ env: bootstrapChildEnv(options.env, startedAt + transactionBudgetMs),
1721
+ });
1722
+ }
1723
+ catch (error) {
1724
+ resolvePromise(reconcileNoChild('failed', error instanceof Error ? error.message : String(error)));
1725
+ return;
1726
+ }
1727
+ const stdout = captureBootstrapOutput(child.stdout);
1728
+ const stderr = captureBootstrapOutput(child.stderr);
1729
+ let settled = false;
1730
+ let containmentStarted = false;
1731
+ let childClosed = false;
1732
+ let childStarted = Number.isSafeInteger(child.pid) && typeof child.pid === 'number' && child.pid > 0;
1733
+ const timerRef = {};
1734
+ const settle = (observation) => {
1735
+ if (settled)
1736
+ return;
1737
+ settled = true;
1738
+ if (timerRef.value)
1739
+ clearTimeout(timerRef.value);
1740
+ resolvePromise(reconcileBootstrapOutcome(observation, inspectLifecycleBootstrapDurableState(options.env, options), stdout, stderr));
1741
+ };
1742
+ const containAndSettle = (confirmedObservation) => {
1743
+ if (settled || containmentStarted)
1744
+ return;
1745
+ containmentStarted = true;
1746
+ const confirmationDeadlineMs = Date.now() + terminationGraceMs;
1747
+ void terminateBootstrapProcessTree(child, platform, () => childClosed, confirmationDeadlineMs, options.processKill ?? process.kill.bind(process), options.treeKillSpawnFn ?? spawn).then((termination) => {
1748
+ if (!termination.confirmed) {
1749
+ settle({ kind: 'termination_unconfirmed', detail: termination.detail ?? 'process_tree_termination_unconfirmed' });
1750
+ return;
1751
+ }
1752
+ settle(confirmedObservation);
1753
+ }).catch(() => {
1754
+ settle({ kind: 'termination_unconfirmed', detail: 'process_tree_termination_unconfirmed' });
1755
+ });
1756
+ };
1757
+ child.once('spawn', () => {
1758
+ childStarted = true;
1759
+ });
1760
+ child.once('close', (code, signal) => {
1761
+ childClosed = true;
1762
+ if (settled || containmentStarted)
1763
+ return;
1764
+ if (signal) {
1765
+ containAndSettle({ kind: 'signal', signal });
1766
+ return;
1767
+ }
1768
+ const cli = parseBootstrapCliResult(stdout);
1769
+ settle({
1770
+ kind: 'exit',
1771
+ code,
1772
+ cli,
1773
+ ...((stdout.truncated || !cli)
1774
+ ? { detail: stdout.truncated ? 'bootstrap_cli_stdout_truncated' : 'bootstrap_cli_result_invalid' }
1775
+ : {}),
1776
+ });
1777
+ });
1778
+ timerRef.value = setTimeout(() => {
1779
+ containAndSettle({ kind: 'timeout' });
1780
+ }, timeoutMs);
1781
+ child.once('error', (error) => {
1782
+ if (settled || containmentStarted)
1783
+ return;
1784
+ if (!childStarted && (!Number.isSafeInteger(child.pid) || !child.pid || child.pid <= 0)) {
1785
+ settle({ kind: 'no_child', reason: 'failed', detail: error.message });
1786
+ return;
1787
+ }
1788
+ containAndSettle({ kind: 'child_ambiguous', detail: error.message });
1789
+ });
1790
+ });
1791
+ }
1792
+ function windowsBootstrapIntentAclScript(checks) {
1793
+ const encodedChecks = Buffer.from(JSON.stringify(checks), 'utf8').toString('base64');
1794
+ const d = String.fromCharCode(36);
1795
+ return [
1796
+ `${d}ErrorActionPreference = 'Stop'`,
1797
+ 'try {',
1798
+ ` ${d}json = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedChecks}'))`,
1799
+ ` ${d}checks = ${d}json | ConvertFrom-Json`,
1800
+ ` ${d}userSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value`,
1801
+ ` ${d}trustedInstaller = 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464'`,
1802
+ ` ${d}trustedOwners = @(${d}userSid, 'S-1-5-18', 'S-1-5-32-544', ${d}trustedInstaller)`,
1803
+ ` ${d}trustedWriters = @(${d}userSid, 'S-1-5-18', 'S-1-5-32-544', ${d}trustedInstaller, 'S-1-3-0', 'S-1-3-4')`,
1804
+ ` ${d}parentDanger = [System.Security.AccessControl.FileSystemRights]::Delete -bor [System.Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor [System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor [System.Security.AccessControl.FileSystemRights]::TakeOwnership`,
1805
+ ` ${d}contentDanger = [System.Security.AccessControl.FileSystemRights]::WriteData -bor [System.Security.AccessControl.FileSystemRights]::AppendData -bor [System.Security.AccessControl.FileSystemRights]::CreateFiles -bor [System.Security.AccessControl.FileSystemRights]::CreateDirectories`,
1806
+ ` foreach (${d}check in @(${d}checks)) {`,
1807
+ ` ${d}acl = Get-Acl -LiteralPath ${d}check.path`,
1808
+ ` ${d}owner = ${d}acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value`,
1809
+ ` if (${d}trustedOwners -notcontains ${d}owner) { exit 21 }`,
1810
+ ` ${d}danger = if (${d}check.parentOnly) { ${d}parentDanger } else { ${d}parentDanger -bor ${d}contentDanger }`,
1811
+ ` foreach (${d}rule in @(${d}acl.Access)) {`,
1812
+ ` if (${d}rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) { continue }`,
1813
+ ` if ((${d}rule.PropagationFlags -band [System.Security.AccessControl.PropagationFlags]::InheritOnly) -ne 0) { continue }`,
1814
+ ` if ((${d}rule.FileSystemRights -band ${d}danger) -eq 0) { continue }`,
1815
+ ` try { ${d}sid = ${d}rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value } catch { exit 22 }`,
1816
+ ` if (${d}trustedWriters -notcontains ${d}sid) { exit 23 }`,
1817
+ ' }',
1818
+ ' }',
1819
+ '} catch { exit 24 }',
1820
+ ].join('; ');
1821
+ }
1822
+ function assertWindowsBootstrapIntentAclTrusted(checks) {
1823
+ if (process.platform !== 'win32' || checks.length === 0)
1824
+ return;
1825
+ try {
1826
+ execFileSync(trustedWindowsSystemExecutable('WindowsPowerShell\\v1.0\\powershell.exe'), ['-NoProfile', '-NonInteractive', '-Command', windowsBootstrapIntentAclScript(checks)], { stdio: 'ignore', timeout: 10_000, windowsHide: true });
1827
+ }
1828
+ catch (error) {
1829
+ throw new Error('bootstrap registration intent Windows ACL chain is not trusted', { cause: error });
1830
+ }
1831
+ }
1832
+ function bootstrapPathKey(path) {
1833
+ const resolved = resolvePath(path);
1834
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
1835
+ }
1836
+ function assertNativeBootstrapIntentDirectoryTrust(directory, requireOwnerOnlyLeaf) {
1837
+ const absoluteDirectory = resolvePath(directory);
1838
+ let current = absoluteDirectory;
1839
+ const root = parsePath(current).root;
1840
+ const windowsChecks = [];
1841
+ const uid = typeof process.getuid === 'function' ? process.getuid() : undefined;
1842
+ let nearestExisting = true;
1843
+ let privateUserAnchor = false;
1844
+ let directoryExists = false;
1845
+ for (;;) {
1846
+ let info;
1847
+ try {
1848
+ info = lstatSync(current);
1849
+ }
1850
+ catch (error) {
1851
+ if (!isErrno(error, 'ENOENT'))
1852
+ throw error;
1853
+ if (current === root)
1854
+ break;
1855
+ current = dirname(current);
1856
+ continue;
1857
+ }
1858
+ if (info.isSymbolicLink() || !info.isDirectory()) {
1859
+ throw new Error('bootstrap registration intent directory chain is unsafe');
1860
+ }
1861
+ if (bootstrapPathKey(current) === bootstrapPathKey(absoluteDirectory))
1862
+ directoryExists = true;
1863
+ if (process.platform === 'win32') {
1864
+ windowsChecks.push({ path: current, parentOnly: bootstrapPathKey(current) !== bootstrapPathKey(absoluteDirectory) });
1865
+ }
1866
+ else {
1867
+ if (uid !== undefined && info.uid !== uid && (nearestExisting || info.uid !== 0)) {
1868
+ throw new Error('bootstrap registration intent directory owner is untrusted');
1869
+ }
1870
+ if (requireOwnerOnlyLeaf && bootstrapPathKey(current) === bootstrapPathKey(absoluteDirectory)
1871
+ && (info.mode & 0o077) !== 0) {
1872
+ throw new Error('bootstrap registration intent directory is not owner-only');
1873
+ }
1874
+ const writableByOthers = (info.mode & 0o022) !== 0;
1875
+ const trustedStickyAncestor = !nearestExisting
1876
+ && privateUserAnchor
1877
+ && info.uid === 0
1878
+ && (info.mode & 0o1000) !== 0;
1879
+ if (writableByOthers && !trustedStickyAncestor) {
1880
+ throw new Error('bootstrap registration intent directory chain is writable by another principal');
1881
+ }
1882
+ if (info.uid === uid && (info.mode & 0o077) === 0)
1883
+ privateUserAnchor = true;
1884
+ }
1885
+ nearestExisting = false;
1886
+ if (current === root)
1887
+ break;
1888
+ current = dirname(current);
1889
+ }
1890
+ if (directoryExists
1891
+ && bootstrapPathKey(realpathSync(absoluteDirectory)) !== bootstrapPathKey(absoluteDirectory)) {
1892
+ throw new Error('bootstrap registration intent directory is not canonical');
1893
+ }
1894
+ assertWindowsBootstrapIntentAclTrusted(windowsChecks);
1895
+ }
1896
+ function assertNativeBootstrapIntentFileTrust(path, expectedLinkCount = 1) {
1897
+ const info = lstatSync(path);
1898
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink !== expectedLinkCount) {
1899
+ throw new Error('bootstrap registration intent file is unsafe');
1900
+ }
1901
+ if (process.platform === 'win32') {
1902
+ assertWindowsBootstrapIntentAclTrusted([{ path, parentOnly: false }]);
1903
+ return;
1904
+ }
1905
+ const uid = typeof process.getuid === 'function' ? process.getuid() : undefined;
1906
+ if ((uid !== undefined && info.uid !== uid) || (info.mode & 0o077) !== 0) {
1907
+ throw new Error('bootstrap registration intent file is not owner-only');
1908
+ }
1909
+ }
1910
+ function assertBootstrapIntentDirectoryTrust(options, directory, requireOwnerOnlyLeaf) {
1911
+ if (options.assertIntentDirectoryTrust) {
1912
+ options.assertIntentDirectoryTrust(directory);
1913
+ return;
1914
+ }
1915
+ assertNativeBootstrapIntentDirectoryTrust(directory, requireOwnerOnlyLeaf);
1916
+ }
1917
+ function assertBootstrapIntentFileTrust(options, path, expectedLinkCount = 1) {
1918
+ if (options.assertIntentFileTrust) {
1919
+ options.assertIntentFileTrust(path);
1920
+ return;
1921
+ }
1922
+ assertNativeBootstrapIntentFileTrust(path, expectedLinkCount);
1923
+ }
1924
+ function readBootstrapRegistrationProcessStartIdentity(options, pid) {
1925
+ return (options.readRegistrationProcessStartIdentity
1926
+ ?? util.readFileLockProcessStartIdentity)(pid);
1927
+ }
1928
+ function bootstrapRegistrationProcessIdentityDigest(identity) {
1929
+ const canonical = identity.source === 'linux-proc'
1930
+ ? ['linux-proc', identity.bootId, identity.startTicks]
1931
+ : identity.source === 'windows-powershell'
1932
+ ? ['windows-powershell', identity.startTimeTicks]
1933
+ : ['darwin-ps', identity.startTime];
1934
+ return createHash('sha256').update(JSON.stringify(canonical), 'utf8').digest('hex');
1935
+ }
1936
+ function bootstrapRegistrationPublisherProcessStatus(options, publisher) {
1937
+ if (options.registrationPublisherProcessStatus) {
1938
+ return options.registrationPublisherProcessStatus({
1939
+ pid: publisher.pid,
1940
+ token: publisher.token,
1941
+ processIdentityDigest: publisher.processIdentityDigest,
1942
+ });
1943
+ }
1944
+ try {
1945
+ process.kill(publisher.pid, 0);
1946
+ }
1947
+ catch (error) {
1948
+ if (isErrno(error, 'ESRCH'))
1949
+ return 'dead';
1950
+ if (!isErrno(error, 'EPERM'))
1951
+ return 'unverifiable';
1952
+ }
1953
+ const current = readBootstrapRegistrationProcessStartIdentity(options, publisher.pid);
1954
+ if (current === null)
1955
+ return 'unverifiable';
1956
+ return bootstrapRegistrationProcessIdentityDigest(current) === publisher.processIdentityDigest
1957
+ ? 'current'
1958
+ : 'pid_reused';
1959
+ }
1960
+ function assertBootstrapRegistrationOwnerCurrent(options, owner) {
1961
+ const current = readBootstrapRegistrationProcessStartIdentity(options, owner.pid);
1962
+ if (current === null
1963
+ || !util.sameFileLockProcessStartIdentity(owner.processStartIdentity, current)) {
1964
+ throw new Error('bootstrap registration parent process identity is no longer current');
1965
+ }
1966
+ }
1967
+ function bootstrapRegistrationOwnerProcessStatus(options, owner) {
1968
+ return (options.registrationOwnerProcessStatus
1969
+ ?? util.inspectFileLockOwnerProcess)(owner);
1970
+ }
1971
+ function acquireBootstrapRegistrationRecoveryLock(options) {
1972
+ const stateDir = resolveBootstrapStateDir(options.env);
1973
+ assertBootstrapIntentDirectoryTrust(options, stateDir, true);
1974
+ const path = join(stateDir, coreBootstrap.LIFECYCLE_BOOTSTRAP_OWNER_LOCK_FILE);
1975
+ const owner = util.acquireLock(path, { maxTries: 2, waitMs: 0 });
1976
+ let released = false;
1977
+ return {
1978
+ path,
1979
+ owner,
1980
+ release: () => {
1981
+ if (released)
1982
+ return;
1983
+ const result = util.releaseLock(path);
1984
+ if (!result.released
1985
+ || (result.reason !== 'released' && result.reason !== 'released_with_cleanup_error')) {
1986
+ throw new Error(`bootstrap registration recovery lock release failed: ${result.reason}`);
1987
+ }
1988
+ released = true;
1989
+ },
1990
+ };
1991
+ }
1992
+ function parseBootstrapRegistrationIntent(raw) {
1993
+ return coreBootstrap.parseLifecycleBootstrapRegistrationIntentJson(raw);
1994
+ }
1995
+ function syncBootstrapDirectory(path) {
1996
+ let descriptor;
1997
+ try {
1998
+ descriptor = openSync(path, constants.O_RDONLY);
1999
+ fsyncSync(descriptor);
2000
+ }
2001
+ catch (error) {
2002
+ if (isErrno(error, 'EINVAL'))
2003
+ return;
2004
+ if (process.platform === 'win32' && (isErrno(error, 'EPERM') || isErrno(error, 'EACCES')))
2005
+ return;
2006
+ throw error;
2007
+ }
2008
+ finally {
2009
+ if (descriptor !== undefined)
2010
+ closeSync(descriptor);
2011
+ }
2012
+ }
2013
+ function readBootstrapRegistrationIntentFile(options, path, registrationPath, terminalPath, clearingPath, expectedLinkCount = 1n) {
2014
+ try {
2015
+ assertBootstrapIntentFileTrust(options, path, Number(expectedLinkCount));
2016
+ const before = lstatSync(path, { bigint: true });
2017
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== expectedLinkCount
2018
+ || before.dev <= 0n || before.ino <= 0n
2019
+ || before.size > BigInt(MAX_BOOTSTRAP_REGISTRATION_INTENT_BYTES)
2020
+ || (process.platform !== 'win32' && (before.mode & 63n) !== 0n)) {
2021
+ return { status: 'invalid', detail: 'registration intent file is unsafe' };
2022
+ }
2023
+ const trustedFile = {
2024
+ path,
2025
+ device: before.dev,
2026
+ inode: before.ino,
2027
+ linkCount: before.nlink,
2028
+ size: before.size,
2029
+ mtimeNs: before.mtimeNs,
2030
+ ctimeNs: before.ctimeNs,
2031
+ };
2032
+ const descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
2033
+ let raw;
2034
+ try {
2035
+ const opened = fstatSync(descriptor, { bigint: true });
2036
+ if (!opened.isFile() || opened.nlink !== expectedLinkCount
2037
+ || opened.dev !== before.dev || opened.ino !== before.ino || opened.size !== before.size) {
2038
+ return { status: 'invalid', detail: 'registration intent changed before read' };
2039
+ }
2040
+ const bytes = Buffer.alloc(MAX_BOOTSTRAP_REGISTRATION_INTENT_BYTES + 1);
2041
+ let offset = 0;
2042
+ while (offset < bytes.length) {
2043
+ const count = readSync(descriptor, bytes, offset, bytes.length - offset, null);
2044
+ if (count === 0)
2045
+ break;
2046
+ offset += count;
2047
+ }
2048
+ if (offset > MAX_BOOTSTRAP_REGISTRATION_INTENT_BYTES) {
2049
+ return { status: 'invalid', detail: 'registration intent is oversized', trustedFile };
2050
+ }
2051
+ raw = bytes.subarray(0, offset).toString('utf8');
2052
+ const settled = fstatSync(descriptor, { bigint: true });
2053
+ if (settled.dev !== opened.dev || settled.ino !== opened.ino
2054
+ || settled.size !== opened.size || settled.mtimeNs !== opened.mtimeNs
2055
+ || settled.ctimeNs !== opened.ctimeNs) {
2056
+ return { status: 'invalid', detail: 'registration intent changed during read' };
2057
+ }
2058
+ }
2059
+ finally {
2060
+ closeSync(descriptor);
2061
+ }
2062
+ const after = lstatSync(path, { bigint: true });
2063
+ if (!after.isFile() || after.isSymbolicLink() || after.nlink !== expectedLinkCount
2064
+ || after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size
2065
+ || after.mtimeNs !== before.mtimeNs || after.ctimeNs !== before.ctimeNs) {
2066
+ return { status: 'invalid', detail: 'registration intent changed after read' };
2067
+ }
2068
+ assertBootstrapIntentFileTrust(options, path, Number(expectedLinkCount));
2069
+ const record = parseBootstrapRegistrationIntent(raw);
2070
+ if (!record) {
2071
+ return { status: 'invalid', detail: 'registration intent payload is invalid', trustedFile };
2072
+ }
2073
+ return {
2074
+ status: 'present',
2075
+ lease: {
2076
+ path: registrationPath,
2077
+ terminalPath,
2078
+ clearingPath,
2079
+ currentPath: path,
2080
+ device: before.dev,
2081
+ inode: before.ino,
2082
+ size: before.size,
2083
+ mtimeNs: before.mtimeNs,
2084
+ ctimeNs: before.ctimeNs,
2085
+ record,
2086
+ },
2087
+ };
2088
+ }
2089
+ catch (error) {
2090
+ return {
2091
+ status: 'invalid',
2092
+ detail: boundedBootstrapDetail(`registration intent is unreadable: ${error instanceof Error ? error.message : String(error)}`),
2093
+ };
2094
+ }
2095
+ }
2096
+ function sameBootstrapRegistrationIntentOwner(left, right) {
2097
+ return left.createdAt === right.createdAt
2098
+ && left.owner.pid === right.owner.pid
2099
+ && left.owner.token === right.owner.token
2100
+ && util.sameFileLockProcessStartIdentity(left.owner.processStartIdentity, right.owner.processStartIdentity);
2101
+ }
2102
+ function bootstrapRegistrationIntentIdentity(lease, linkCount) {
2103
+ return {
2104
+ path: lease.currentPath,
2105
+ device: lease.device,
2106
+ inode: lease.inode,
2107
+ linkCount,
2108
+ size: lease.size,
2109
+ mtimeNs: lease.mtimeNs,
2110
+ ctimeNs: lease.ctimeNs,
2111
+ };
2112
+ }
2113
+ function sameBootstrapRegistrationIntentFileIdentity(left, right) {
2114
+ return left.path === right.path
2115
+ && left.device === right.device
2116
+ && left.inode === right.inode
2117
+ && left.linkCount === right.linkCount
2118
+ && left.size === right.size
2119
+ && left.mtimeNs === right.mtimeNs
2120
+ && left.ctimeNs === right.ctimeNs;
2121
+ }
2122
+ const BOOTSTRAP_REGISTRATION_PUBLISHER_RE = /^bootstrap-registration\.intent\.publisher\.([1-9][0-9]{0,15})\.([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.([0-9a-f]{64})$/;
2123
+ function readBootstrapRegistrationPublisherClaim(options, stateDir) {
2124
+ let names;
2125
+ try {
2126
+ names = readdirSync(stateDir).filter((name) => name.startsWith(BOOTSTRAP_REGISTRATION_INTENT_PUBLISHER_PREFIX));
2127
+ }
2128
+ catch (error) {
2129
+ if (isErrno(error, 'ENOENT'))
2130
+ return { status: 'absent' };
2131
+ return {
2132
+ status: 'invalid',
2133
+ detail: boundedBootstrapDetail(`registration publisher claim directory is unreadable: ${error instanceof Error ? error.message : String(error)}`),
2134
+ };
2135
+ }
2136
+ if (names.length === 0)
2137
+ return { status: 'absent' };
2138
+ if (names.length !== 1) {
2139
+ return { status: 'invalid', detail: 'registration publisher claim count is invalid' };
2140
+ }
2141
+ const name = names[0];
2142
+ const match = BOOTSTRAP_REGISTRATION_PUBLISHER_RE.exec(name);
2143
+ if (!match)
2144
+ return { status: 'invalid', detail: 'registration publisher claim name is invalid' };
2145
+ const pid = Number(match[1]);
2146
+ if (!Number.isSafeInteger(pid) || pid <= 0) {
2147
+ return { status: 'invalid', detail: 'registration publisher claim pid is invalid' };
2148
+ }
2149
+ const path = join(stateDir, name);
2150
+ try {
2151
+ assertBootstrapIntentFileTrust(options, path);
2152
+ const before = lstatSync(path, { bigint: true });
2153
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n
2154
+ || before.size !== 0n || before.dev <= 0n || before.ino <= 0n) {
2155
+ return { status: 'invalid', detail: 'registration publisher claim is unsafe' };
2156
+ }
2157
+ let descriptor;
2158
+ try {
2159
+ descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
2160
+ const opened = fstatSync(descriptor, { bigint: true });
2161
+ if (!opened.isFile() || opened.nlink !== 1n || opened.size !== 0n
2162
+ || opened.dev !== before.dev || opened.ino !== before.ino
2163
+ || opened.mtimeNs !== before.mtimeNs || opened.ctimeNs !== before.ctimeNs) {
2164
+ return { status: 'invalid', detail: 'registration publisher claim changed before read' };
2165
+ }
2166
+ }
2167
+ finally {
2168
+ if (descriptor !== undefined)
2169
+ closeSync(descriptor);
2170
+ }
2171
+ const after = lstatSync(path, { bigint: true });
2172
+ if (!after.isFile() || after.isSymbolicLink() || after.nlink !== 1n || after.size !== 0n
2173
+ || after.dev !== before.dev || after.ino !== before.ino
2174
+ || after.mtimeNs !== before.mtimeNs || after.ctimeNs !== before.ctimeNs) {
2175
+ return { status: 'invalid', detail: 'registration publisher claim changed after read' };
2176
+ }
2177
+ assertBootstrapIntentFileTrust(options, path);
2178
+ return {
2179
+ status: 'present',
2180
+ claim: {
2181
+ path,
2182
+ pid,
2183
+ token: match[2],
2184
+ processIdentityDigest: match[3],
2185
+ identity: {
2186
+ path,
2187
+ device: before.dev,
2188
+ inode: before.ino,
2189
+ linkCount: 1n,
2190
+ size: 0n,
2191
+ mtimeNs: before.mtimeNs,
2192
+ ctimeNs: before.ctimeNs,
2193
+ },
2194
+ },
2195
+ };
2196
+ }
2197
+ catch (error) {
2198
+ return {
2199
+ status: 'invalid',
2200
+ detail: boundedBootstrapDetail(`registration publisher claim is unreadable: ${error instanceof Error ? error.message : String(error)}`),
2201
+ };
2202
+ }
2203
+ }
2204
+ function bootstrapRegistrationPublisherOwnsIntent(publisher, record) {
2205
+ return record.owner.pid === publisher.pid
2206
+ && record.owner.token === publisher.token
2207
+ && bootstrapRegistrationProcessIdentityDigest(record.owner.processStartIdentity)
2208
+ === publisher.processIdentityDigest;
2209
+ }
2210
+ function readBootstrapRegistrationIntent(options) {
2211
+ const stateDir = resolveBootstrapStateDir(options.env);
2212
+ const path = join(stateDir, BOOTSTRAP_REGISTRATION_INTENT_FILE);
2213
+ const publishingPath = join(stateDir, BOOTSTRAP_REGISTRATION_INTENT_PUBLISHING_FILE);
2214
+ const terminalPath = join(stateDir, BOOTSTRAP_REGISTRATION_INTENT_TERMINAL_FILE);
2215
+ const clearingPath = join(stateDir, BOOTSTRAP_REGISTRATION_INTENT_CLEARING_FILE);
2216
+ try {
2217
+ const pathPresent = bootstrapStateEntryPresent(path);
2218
+ const publishingPresent = bootstrapStateEntryPresent(publishingPath);
2219
+ const terminalPresent = bootstrapStateEntryPresent(terminalPath);
2220
+ const clearingPresent = bootstrapStateEntryPresent(clearingPath);
2221
+ const publisherEntryPresent = bootstrapRegistrationPublisherEntryPresent(stateDir);
2222
+ if (!pathPresent && !publishingPresent && !terminalPresent && !clearingPresent
2223
+ && !publisherEntryPresent) {
2224
+ return { status: 'absent' };
2225
+ }
2226
+ assertBootstrapIntentDirectoryTrust(options, stateDir, true);
2227
+ const publisher = readBootstrapRegistrationPublisherClaim(options, stateDir);
2228
+ if (publisher.status === 'invalid') {
2229
+ return { status: 'invalid', detail: publisher.detail };
2230
+ }
2231
+ if (publishingPresent || publisher.status === 'present') {
2232
+ if (terminalPresent || clearingPresent) {
2233
+ return {
2234
+ status: 'invalid',
2235
+ detail: 'registration intent publication conflicts with a terminal transition',
2236
+ };
2237
+ }
2238
+ if (publisher.status !== 'present') {
2239
+ return {
2240
+ status: 'invalid',
2241
+ detail: 'registration intent publication has no authenticated publisher claim',
2242
+ };
2243
+ }
2244
+ if (pathPresent && publishingPresent) {
2245
+ const registration = readBootstrapRegistrationIntentFile(options, path, path, terminalPath, clearingPath, 2n);
2246
+ const publishing = readBootstrapRegistrationIntentFile(options, publishingPath, path, terminalPath, clearingPath, 2n);
2247
+ if (registration.status !== 'present'
2248
+ || publishing.status !== 'present'
2249
+ || registration.lease.record.state !== 'registering'
2250
+ || publishing.lease.record.state !== 'registering'
2251
+ || !bootstrapRegistrationPublisherOwnsIntent(publisher.claim, registration.lease.record)
2252
+ || !bootstrapRegistrationPublisherOwnsIntent(publisher.claim, publishing.lease.record)
2253
+ || !sameBootstrapRegistrationIntentOwner(registration.lease.record, publishing.lease.record)) {
2254
+ return {
2255
+ status: 'invalid',
2256
+ detail: 'registration intent publication identity is invalid',
2257
+ };
2258
+ }
2259
+ const registrationIdentity = bootstrapRegistrationIntentIdentity(registration.lease, 2n);
2260
+ const publishingIdentity = bootstrapRegistrationIntentIdentity(publishing.lease, 2n);
2261
+ if (registrationIdentity.device !== publishingIdentity.device
2262
+ || registrationIdentity.inode !== publishingIdentity.inode
2263
+ || registrationIdentity.size !== publishingIdentity.size
2264
+ || registrationIdentity.mtimeNs !== publishingIdentity.mtimeNs
2265
+ || registrationIdentity.ctimeNs !== publishingIdentity.ctimeNs) {
2266
+ return {
2267
+ status: 'invalid',
2268
+ detail: 'registration intent publication does not own the canonical claim',
2269
+ };
2270
+ }
2271
+ return {
2272
+ status: 'incomplete_publication',
2273
+ publication: {
2274
+ publisher: publisher.claim,
2275
+ publishing: publishingIdentity,
2276
+ registration: {
2277
+ lease: registration.lease,
2278
+ identity: registrationIdentity,
2279
+ },
2280
+ },
2281
+ };
2282
+ }
2283
+ if (pathPresent) {
2284
+ const registration = readBootstrapRegistrationIntentFile(options, path, path, terminalPath, clearingPath);
2285
+ if (registration.status !== 'present'
2286
+ || registration.lease.record.state !== 'registering'
2287
+ || !bootstrapRegistrationPublisherOwnsIntent(publisher.claim, registration.lease.record)) {
2288
+ return {
2289
+ status: 'invalid',
2290
+ detail: 'registration intent canonical publisher claim is invalid',
2291
+ };
2292
+ }
2293
+ return {
2294
+ status: 'incomplete_publication',
2295
+ publication: {
2296
+ publisher: publisher.claim,
2297
+ registration: {
2298
+ lease: registration.lease,
2299
+ identity: bootstrapRegistrationIntentIdentity(registration.lease, 1n),
2300
+ },
2301
+ },
2302
+ };
2303
+ }
2304
+ if (publishingPresent) {
2305
+ const publishing = readBootstrapRegistrationIntentFile(options, publishingPath, path, terminalPath, clearingPath);
2306
+ if (publishing.status === 'present') {
2307
+ if (publishing.lease.record.state !== 'registering'
2308
+ || !bootstrapRegistrationPublisherOwnsIntent(publisher.claim, publishing.lease.record)) {
2309
+ return {
2310
+ status: 'invalid',
2311
+ detail: 'registration intent publication payload is invalid',
2312
+ };
2313
+ }
2314
+ return {
2315
+ status: 'incomplete_publication',
2316
+ publication: {
2317
+ publisher: publisher.claim,
2318
+ publishing: bootstrapRegistrationIntentIdentity(publishing.lease, 1n),
2319
+ },
2320
+ };
2321
+ }
2322
+ if (publishing.detail === 'registration intent payload is invalid'
2323
+ && publishing.trustedFile !== undefined) {
2324
+ return {
2325
+ status: 'incomplete_publication',
2326
+ publication: {
2327
+ publisher: publisher.claim,
2328
+ publishing: publishing.trustedFile,
2329
+ },
2330
+ };
2331
+ }
2332
+ return { status: 'invalid', detail: publishing.detail };
2333
+ }
2334
+ return {
2335
+ status: 'incomplete_publication',
2336
+ publication: { publisher: publisher.claim },
2337
+ };
2338
+ }
2339
+ if (clearingPresent && (pathPresent || terminalPresent)) {
2340
+ return { status: 'invalid', detail: 'registration intent clearing conflicts with another state' };
2341
+ }
2342
+ if (clearingPresent) {
2343
+ const clearing = readBootstrapRegistrationIntentFile(options, clearingPath, path, terminalPath, clearingPath);
2344
+ if (clearing.status !== 'present')
2345
+ return { status: 'invalid', detail: clearing.detail };
2346
+ if (clearing.lease.record.state !== 'terminal') {
2347
+ return { status: 'invalid', detail: 'registration clearing receipt is invalid' };
2348
+ }
2349
+ return { status: 'present', lease: clearing.lease };
2350
+ }
2351
+ const registration = pathPresent
2352
+ ? readBootstrapRegistrationIntentFile(options, path, path, terminalPath, clearingPath)
2353
+ : undefined;
2354
+ const terminal = terminalPresent
2355
+ ? readBootstrapRegistrationIntentFile(options, terminalPath, path, terminalPath, clearingPath)
2356
+ : undefined;
2357
+ if (registration && terminal) {
2358
+ if (registration.status !== 'present' || registration.lease.record.state !== 'registering') {
2359
+ return { status: 'invalid', detail: 'registration intent source is invalid during terminal publication' };
2360
+ }
2361
+ if (terminal.status === 'invalid') {
2362
+ if (!terminal.trustedFile)
2363
+ return { status: 'invalid', detail: terminal.detail };
2364
+ return {
2365
+ status: 'incomplete_terminal',
2366
+ registering: registration.lease,
2367
+ terminalFile: terminal.trustedFile,
2368
+ };
2369
+ }
2370
+ if (terminal.lease.record.state !== 'terminal'
2371
+ || !sameBootstrapRegistrationIntentOwner(registration.lease.record, terminal.lease.record)) {
2372
+ return { status: 'invalid', detail: 'registration terminal receipt has a different owner' };
2373
+ }
2374
+ return {
2375
+ status: 'present',
2376
+ lease: {
2377
+ ...terminal.lease,
2378
+ staleRegistering: {
2379
+ path,
2380
+ device: registration.lease.device,
2381
+ inode: registration.lease.inode,
2382
+ size: registration.lease.size,
2383
+ mtimeNs: registration.lease.mtimeNs,
2384
+ ctimeNs: registration.lease.ctimeNs,
2385
+ },
2386
+ },
2387
+ };
2388
+ }
2389
+ if (registration) {
2390
+ if (registration.status !== 'present')
2391
+ return { status: 'invalid', detail: registration.detail };
2392
+ if (registration.lease.record.state !== 'registering') {
2393
+ return { status: 'invalid', detail: 'registration intent payload is invalid' };
2394
+ }
2395
+ return { status: 'present', lease: registration.lease };
2396
+ }
2397
+ if (!terminal)
2398
+ return { status: 'invalid', detail: 'registration terminal receipt is absent' };
2399
+ if (terminal.status !== 'present')
2400
+ return { status: 'invalid', detail: terminal.detail };
2401
+ if (terminal.lease.record.state !== 'terminal') {
2402
+ return { status: 'invalid', detail: 'registration terminal receipt is invalid' };
2403
+ }
2404
+ return { status: 'present', lease: terminal.lease };
2405
+ }
2406
+ catch (error) {
2407
+ return {
2408
+ status: 'invalid',
2409
+ detail: boundedBootstrapDetail(`registration intent is unreadable: ${error instanceof Error ? error.message : String(error)}`),
2410
+ };
2411
+ }
2412
+ }
2413
+ function writeBootstrapRegistrationIntentBytes(descriptor, content, start, end) {
2414
+ let offset = start;
2415
+ while (offset < end) {
2416
+ const written = writeSync(descriptor, content, offset, end - offset, null);
2417
+ if (written <= 0)
2418
+ throw new Error('bootstrap registration intent write made no progress');
2419
+ offset += written;
2420
+ }
2421
+ }
2422
+ function createBootstrapRegistrationPublisherClaim(options, stateDir, owner) {
2423
+ const processIdentityDigest = bootstrapRegistrationProcessIdentityDigest(owner.processStartIdentity);
2424
+ const path = join(stateDir, `${BOOTSTRAP_REGISTRATION_INTENT_PUBLISHER_PREFIX}${owner.pid}.${owner.token}.${processIdentityDigest}`);
2425
+ let descriptor;
2426
+ let opened;
2427
+ try {
2428
+ descriptor = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
2429
+ fsyncSync(descriptor);
2430
+ opened = fstatSync(descriptor, { bigint: true });
2431
+ if (!opened.isFile() || opened.nlink !== 1n || opened.size !== 0n
2432
+ || opened.dev <= 0n || opened.ino <= 0n
2433
+ || (process.platform !== 'win32' && (opened.mode & 63n) !== 0n)) {
2434
+ throw new Error('unsafe bootstrap registration publisher claim');
2435
+ }
2436
+ }
2437
+ finally {
2438
+ if (descriptor !== undefined)
2439
+ closeSync(descriptor);
2440
+ }
2441
+ if (!opened)
2442
+ throw new Error('bootstrap registration publisher claim failed');
2443
+ syncBootstrapDirectory(stateDir);
2444
+ assertBootstrapIntentFileTrust(options, path);
2445
+ const settled = lstatSync(path, { bigint: true });
2446
+ if (!settled.isFile() || settled.isSymbolicLink() || settled.nlink !== 1n
2447
+ || settled.size !== 0n || settled.dev !== opened.dev || settled.ino !== opened.ino
2448
+ || settled.mtimeNs !== opened.mtimeNs || settled.ctimeNs !== opened.ctimeNs) {
2449
+ throw new Error('bootstrap registration publisher claim is unconfirmed');
2450
+ }
2451
+ return {
2452
+ path,
2453
+ pid: owner.pid,
2454
+ token: owner.token,
2455
+ processIdentityDigest,
2456
+ identity: {
2457
+ path,
2458
+ device: settled.dev,
2459
+ inode: settled.ino,
2460
+ linkCount: 1n,
2461
+ size: 0n,
2462
+ mtimeNs: settled.mtimeNs,
2463
+ ctimeNs: settled.ctimeNs,
2464
+ },
2465
+ };
2466
+ }
2467
+ function retireBootstrapRegistrationPublisherClaim(options, publisher) {
2468
+ const stateDir = dirname(publisher.path);
2469
+ assertBootstrapRegistrationIntentFileIdentity(options, publisher.identity);
2470
+ rmSync(publisher.path);
2471
+ syncBootstrapDirectory(stateDir);
2472
+ const remaining = readBootstrapRegistrationPublisherClaim(options, stateDir);
2473
+ if (remaining.status !== 'absent') {
2474
+ throw new Error('bootstrap registration publisher claim retirement is unconfirmed');
2475
+ }
2476
+ }
2477
+ function publishBootstrapRegistrationIntent(options) {
2478
+ const stateDir = resolveBootstrapStateDir(options.env);
2479
+ assertBootstrapIntentDirectoryTrust(options, stateDir, false);
2480
+ mkdirSync(stateDir, { recursive: true, mode: 0o700 });
2481
+ assertBootstrapIntentDirectoryTrust(options, stateDir, true);
2482
+ const path = join(stateDir, BOOTSTRAP_REGISTRATION_INTENT_FILE);
2483
+ const publishingPath = join(stateDir, BOOTSTRAP_REGISTRATION_INTENT_PUBLISHING_FILE);
2484
+ const terminalPath = join(stateDir, BOOTSTRAP_REGISTRATION_INTENT_TERMINAL_FILE);
2485
+ const clearingPath = join(stateDir, BOOTSTRAP_REGISTRATION_INTENT_CLEARING_FILE);
2486
+ options.beforeIntentPublish?.();
2487
+ const publicationLock = acquireBootstrapRegistrationRecoveryLock(options);
2488
+ try {
2489
+ for (const unavailablePath of [path, publishingPath, terminalPath, clearingPath]) {
2490
+ try {
2491
+ lstatSync(unavailablePath);
2492
+ throw new Error('bootstrap registration intent transition is incomplete');
2493
+ }
2494
+ catch (error) {
2495
+ if (!isErrno(error, 'ENOENT'))
2496
+ throw error;
2497
+ }
2498
+ }
2499
+ if (readBootstrapRegistrationPublisherClaim(options, stateDir).status !== 'absent') {
2500
+ throw new Error('bootstrap registration publisher claim already exists');
2501
+ }
2502
+ const token = randomUUID();
2503
+ const createdAtMs = options.now ?? Date.now();
2504
+ if (!Number.isSafeInteger(createdAtMs) || createdAtMs < 0) {
2505
+ throw new Error('invalid bootstrap registration intent clock');
2506
+ }
2507
+ const processStartIdentity = readBootstrapRegistrationProcessStartIdentity(options, process.pid);
2508
+ if (processStartIdentity === null) {
2509
+ throw new Error('bootstrap registration parent process identity is unavailable');
2510
+ }
2511
+ const record = {
2512
+ schema: BOOTSTRAP_REGISTRATION_INTENT_SCHEMA,
2513
+ state: 'registering',
2514
+ owner: { pid: process.pid, token, processStartIdentity },
2515
+ createdAt: new Date(createdAtMs).toISOString(),
2516
+ };
2517
+ const content = Buffer.from(`${JSON.stringify(record)}\n`, 'utf8');
2518
+ if (content.byteLength > MAX_BOOTSTRAP_REGISTRATION_INTENT_BYTES) {
2519
+ throw new Error('bootstrap registration intent publication is oversized');
2520
+ }
2521
+ const publisher = createBootstrapRegistrationPublisherClaim(options, stateDir, record.owner);
2522
+ let descriptor;
2523
+ let opened;
2524
+ try {
2525
+ descriptor = openSync(publishingPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
2526
+ options.afterIntentPublicationStep?.('create', publishingPath);
2527
+ const split = Math.max(1, Math.floor(content.byteLength / 2));
2528
+ writeBootstrapRegistrationIntentBytes(descriptor, content, 0, split);
2529
+ options.afterIntentPublicationStep?.('partial_write', publishingPath);
2530
+ writeBootstrapRegistrationIntentBytes(descriptor, content, split, content.byteLength);
2531
+ fsyncSync(descriptor);
2532
+ options.afterIntentPublicationStep?.('file_fsync', publishingPath);
2533
+ opened = fstatSync(descriptor, { bigint: true });
2534
+ if (!opened.isFile() || opened.nlink !== 1n || opened.dev <= 0n || opened.ino <= 0n
2535
+ || opened.size !== BigInt(content.byteLength)
2536
+ || (process.platform !== 'win32' && (opened.mode & 63n) !== 0n)) {
2537
+ throw new Error('unsafe bootstrap registration intent publication');
2538
+ }
2539
+ }
2540
+ finally {
2541
+ if (descriptor !== undefined)
2542
+ closeSync(descriptor);
2543
+ }
2544
+ if (!opened)
2545
+ throw new Error('bootstrap registration intent publication failed');
2546
+ assertBootstrapIntentFileTrust(options, publishingPath);
2547
+ const prepared = lstatSync(publishingPath, { bigint: true });
2548
+ if (!prepared.isFile() || prepared.isSymbolicLink() || prepared.nlink !== 1n
2549
+ || prepared.dev !== opened.dev || prepared.ino !== opened.ino
2550
+ || prepared.size !== opened.size || prepared.mtimeNs !== opened.mtimeNs
2551
+ || prepared.ctimeNs !== opened.ctimeNs) {
2552
+ throw new Error('bootstrap registration intent temp changed before publication');
2553
+ }
2554
+ linkSync(publishingPath, path);
2555
+ options.afterIntentPublicationStep?.('link', path);
2556
+ syncBootstrapDirectory(stateDir);
2557
+ options.afterIntentPublicationStep?.('directory_fsync', stateDir);
2558
+ assertBootstrapIntentFileTrust(options, publishingPath, 2);
2559
+ assertBootstrapIntentFileTrust(options, path, 2);
2560
+ const linkedTemp = lstatSync(publishingPath, { bigint: true });
2561
+ const linkedCanonical = lstatSync(path, { bigint: true });
2562
+ if (!linkedTemp.isFile() || linkedTemp.isSymbolicLink() || linkedTemp.nlink !== 2n
2563
+ || !linkedCanonical.isFile() || linkedCanonical.isSymbolicLink()
2564
+ || linkedCanonical.nlink !== 2n
2565
+ || linkedTemp.dev !== opened.dev || linkedTemp.ino !== opened.ino
2566
+ || linkedCanonical.dev !== opened.dev || linkedCanonical.ino !== opened.ino
2567
+ || linkedTemp.size !== opened.size || linkedCanonical.size !== opened.size
2568
+ || linkedTemp.mtimeNs !== opened.mtimeNs || linkedCanonical.mtimeNs !== opened.mtimeNs) {
2569
+ throw new Error('bootstrap registration intent canonical claim is unconfirmed');
2570
+ }
2571
+ rmSync(publishingPath);
2572
+ syncBootstrapDirectory(stateDir);
2573
+ if (bootstrapStateEntryPresent(publishingPath)) {
2574
+ throw new Error('bootstrap registration intent temp retirement is unconfirmed');
2575
+ }
2576
+ assertBootstrapIntentFileTrust(options, path);
2577
+ const published = lstatSync(path, { bigint: true });
2578
+ if (!published.isFile() || published.isSymbolicLink() || published.nlink !== 1n
2579
+ || published.dev !== opened.dev || published.ino !== opened.ino
2580
+ || published.size !== opened.size || published.mtimeNs !== opened.mtimeNs) {
2581
+ throw new Error('bootstrap registration intent changed during publication');
2582
+ }
2583
+ retireBootstrapRegistrationPublisherClaim(options, publisher);
2584
+ return {
2585
+ path,
2586
+ terminalPath,
2587
+ clearingPath,
2588
+ currentPath: path,
2589
+ device: published.dev,
2590
+ inode: published.ino,
2591
+ size: published.size,
2592
+ mtimeNs: published.mtimeNs,
2593
+ ctimeNs: published.ctimeNs,
2594
+ record,
2595
+ };
2596
+ }
2597
+ finally {
2598
+ publicationLock.release();
2599
+ }
2600
+ }
2601
+ function terminalizeBootstrapRegistrationIntent(options, lease, outcome, transactionId) {
2602
+ if (outcome === 'committed' && (!transactionId || !BOOTSTRAP_UUID_RE.test(transactionId))) {
2603
+ throw new Error('bootstrap registration intent commit identity is invalid');
2604
+ }
2605
+ if (outcome !== 'committed' && transactionId !== undefined) {
2606
+ throw new Error('bootstrap registration intent terminal identity is unexpected');
2607
+ }
2608
+ if (lease.record.state !== 'registering' || lease.currentPath !== lease.path) {
2609
+ throw new Error('bootstrap registration intent is not an active registration');
2610
+ }
2611
+ assertBootstrapIntentDirectoryTrust(options, dirname(lease.currentPath), true);
2612
+ assertBootstrapIntentFileTrust(options, lease.currentPath);
2613
+ const current = lstatSync(lease.currentPath, { bigint: true });
2614
+ if (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1n
2615
+ || current.dev !== lease.device || current.ino !== lease.inode || current.size !== lease.size
2616
+ || current.mtimeNs !== lease.mtimeNs || current.ctimeNs !== lease.ctimeNs) {
2617
+ throw new Error('bootstrap registration intent ownership changed');
2618
+ }
2619
+ const terminalAtMs = options.now ?? Date.now();
2620
+ if (!Number.isSafeInteger(terminalAtMs) || terminalAtMs < 0) {
2621
+ throw new Error('invalid bootstrap registration intent terminal clock');
2622
+ }
2623
+ const record = {
2624
+ schema: BOOTSTRAP_REGISTRATION_INTENT_SCHEMA,
2625
+ state: 'terminal',
2626
+ owner: lease.record.owner,
2627
+ createdAt: lease.record.createdAt,
2628
+ outcome,
2629
+ terminalAt: new Date(terminalAtMs).toISOString(),
2630
+ ...(transactionId !== undefined ? { transactionId } : {}),
2631
+ };
2632
+ const content = `${JSON.stringify(record)}\n`;
2633
+ if (Buffer.byteLength(content, 'utf8') > MAX_BOOTSTRAP_REGISTRATION_INTENT_BYTES) {
2634
+ throw new Error('bootstrap registration intent terminal receipt is oversized');
2635
+ }
2636
+ for (const unavailablePath of [lease.terminalPath, lease.clearingPath]) {
2637
+ try {
2638
+ lstatSync(unavailablePath);
2639
+ throw new Error('bootstrap registration intent terminal path exists');
2640
+ }
2641
+ catch (error) {
2642
+ if (!isErrno(error, 'ENOENT'))
2643
+ throw error;
2644
+ }
2645
+ }
2646
+ let descriptor;
2647
+ let settled;
56
2648
  try {
57
- return /docker|containerd|kubepods|podman|lxc/.test(readFile('/proc/1/cgroup'));
2649
+ descriptor = openSync(lease.terminalPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
2650
+ writeFileSync(descriptor, content, { encoding: 'utf8' });
2651
+ options.beforeIntentTerminalFsync?.(lease.terminalPath);
2652
+ fsyncSync(descriptor);
2653
+ settled = fstatSync(descriptor, { bigint: true });
2654
+ if (!settled.isFile() || settled.nlink !== 1n
2655
+ || settled.dev <= 0n || settled.ino <= 0n
2656
+ || settled.size !== BigInt(Buffer.byteLength(content, 'utf8'))) {
2657
+ throw new Error('bootstrap registration intent terminalization is unsafe');
2658
+ }
58
2659
  }
59
- catch {
60
- return false;
2660
+ finally {
2661
+ if (descriptor !== undefined)
2662
+ closeSync(descriptor);
2663
+ }
2664
+ if (!settled)
2665
+ throw new Error('bootstrap registration intent terminal publication failed');
2666
+ assertBootstrapIntentFileTrust(options, lease.terminalPath);
2667
+ const after = lstatSync(lease.terminalPath, { bigint: true });
2668
+ if (!after.isFile() || after.isSymbolicLink() || after.nlink !== 1n
2669
+ || after.dev !== settled.dev || after.ino !== settled.ino
2670
+ || after.size !== settled.size || after.mtimeNs !== settled.mtimeNs
2671
+ || after.ctimeNs !== settled.ctimeNs) {
2672
+ throw new Error('bootstrap registration intent changed after terminalization');
2673
+ }
2674
+ const stateDir = dirname(lease.currentPath);
2675
+ syncBootstrapDirectory(stateDir);
2676
+ options.afterIntentTerminalPublish?.();
2677
+ const active = lstatSync(lease.path, { bigint: true });
2678
+ if (!active.isFile() || active.isSymbolicLink() || active.nlink !== 1n
2679
+ || active.dev !== lease.device || active.ino !== lease.inode || active.size !== lease.size
2680
+ || active.mtimeNs !== lease.mtimeNs || active.ctimeNs !== lease.ctimeNs) {
2681
+ throw new Error('bootstrap registration intent source changed before retirement');
2682
+ }
2683
+ rmSync(lease.path);
2684
+ syncBootstrapDirectory(stateDir);
2685
+ if (bootstrapStateEntryPresent(lease.path)) {
2686
+ throw new Error('bootstrap registration intent source retirement is unconfirmed');
61
2687
  }
2688
+ return {
2689
+ ...lease,
2690
+ currentPath: lease.terminalPath,
2691
+ device: settled.dev,
2692
+ inode: settled.ino,
2693
+ size: settled.size,
2694
+ mtimeNs: settled.mtimeNs,
2695
+ ctimeNs: settled.ctimeNs,
2696
+ record,
2697
+ };
62
2698
  }
63
- function readBootstrapAttempt(env, readFile) {
64
- try {
65
- const parsed = JSON.parse(readFile(join(resolveBootstrapStateDir(env), BOOTSTRAP_ATTEMPT_FILE)));
66
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
67
- return undefined;
68
- return parsed;
2699
+ function clearBootstrapRegistrationIntent(options, lease) {
2700
+ if (lease.record.state !== 'terminal') {
2701
+ throw new Error('bootstrap registration intent is not terminal');
69
2702
  }
70
- catch {
71
- return undefined;
2703
+ const stateDir = dirname(lease.currentPath);
2704
+ assertBootstrapIntentDirectoryTrust(options, stateDir, true);
2705
+ if (lease.staleRegistering) {
2706
+ assertBootstrapIntentFileTrust(options, lease.staleRegistering.path);
2707
+ const stale = lstatSync(lease.staleRegistering.path, { bigint: true });
2708
+ if (!stale.isFile() || stale.isSymbolicLink() || stale.nlink !== 1n
2709
+ || stale.dev !== lease.staleRegistering.device || stale.ino !== lease.staleRegistering.inode
2710
+ || stale.size !== lease.staleRegistering.size
2711
+ || stale.mtimeNs !== lease.staleRegistering.mtimeNs
2712
+ || stale.ctimeNs !== lease.staleRegistering.ctimeNs) {
2713
+ throw new Error('bootstrap registration intent source changed before recovery retirement');
2714
+ }
2715
+ rmSync(lease.staleRegistering.path);
2716
+ syncBootstrapDirectory(stateDir);
2717
+ if (bootstrapStateEntryPresent(lease.staleRegistering.path)) {
2718
+ throw new Error('bootstrap registration intent source recovery retirement is unconfirmed');
2719
+ }
2720
+ }
2721
+ assertBootstrapIntentFileTrust(options, lease.currentPath);
2722
+ const current = lstatSync(lease.currentPath, { bigint: true });
2723
+ if (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1n
2724
+ || current.dev !== lease.device || current.ino !== lease.inode || current.size !== lease.size
2725
+ || current.mtimeNs !== lease.mtimeNs || current.ctimeNs !== lease.ctimeNs) {
2726
+ throw new Error('bootstrap registration intent ownership changed');
2727
+ }
2728
+ let clearingLease = lease;
2729
+ if (lease.currentPath === lease.terminalPath) {
2730
+ try {
2731
+ lstatSync(lease.clearingPath);
2732
+ throw new Error('bootstrap registration intent clearing path exists');
2733
+ }
2734
+ catch (error) {
2735
+ if (!isErrno(error, 'ENOENT'))
2736
+ throw error;
2737
+ }
2738
+ renameSync(lease.terminalPath, lease.clearingPath);
2739
+ syncBootstrapDirectory(stateDir);
2740
+ const renamed = lstatSync(lease.clearingPath, { bigint: true });
2741
+ if (!renamed.isFile() || renamed.isSymbolicLink() || renamed.nlink !== 1n
2742
+ || renamed.dev !== lease.device || renamed.ino !== lease.inode
2743
+ || renamed.size !== lease.size) {
2744
+ throw new Error('bootstrap registration intent clear rename changed ownership');
2745
+ }
2746
+ clearingLease = {
2747
+ ...lease,
2748
+ currentPath: lease.clearingPath,
2749
+ size: renamed.size,
2750
+ mtimeNs: renamed.mtimeNs,
2751
+ ctimeNs: renamed.ctimeNs,
2752
+ };
2753
+ options.afterIntentClearRename?.();
2754
+ }
2755
+ else if (lease.currentPath !== lease.clearingPath) {
2756
+ throw new Error('bootstrap registration intent path is unexpected');
2757
+ }
2758
+ assertBootstrapIntentFileTrust(options, clearingLease.currentPath);
2759
+ const moved = lstatSync(clearingLease.currentPath, { bigint: true });
2760
+ if (!moved.isFile() || moved.isSymbolicLink() || moved.nlink !== 1n
2761
+ || moved.dev !== clearingLease.device || moved.ino !== clearingLease.inode
2762
+ || moved.size !== clearingLease.size || moved.mtimeNs !== clearingLease.mtimeNs
2763
+ || moved.ctimeNs !== clearingLease.ctimeNs) {
2764
+ throw new Error('bootstrap registration intent clear changed ownership');
2765
+ }
2766
+ rmSync(clearingLease.currentPath);
2767
+ syncBootstrapDirectory(stateDir);
2768
+ if (bootstrapStateEntryPresent(lease.path)
2769
+ || bootstrapStateEntryPresent(lease.terminalPath)
2770
+ || bootstrapStateEntryPresent(lease.clearingPath)) {
2771
+ throw new Error('bootstrap registration intent clear is unconfirmed');
72
2772
  }
73
2773
  }
74
- // Cooldown-worthy attempt outcomes. Migration adds its own failure/timeout outcomes so a
75
- // broken release (download/verify/install/register) does not get retried on every startup.
76
- const BOOTSTRAP_FAILURE_OUTCOMES = new Set([
77
- 'failed', 'timeout', 'cli_not_found', 'migration_failed', 'migration_timeout',
78
- ]);
79
- /** True when a recent bootstrap/migration attempt failed within the cooldown window. */
80
- export function recentBootstrapFailure(env, readFile, now) {
81
- const attempt = readBootstrapAttempt(env, readFile);
82
- if (!attempt)
2774
+ function sameBootstrapIncompletePublication(left, right) {
2775
+ if (left.publisher.pid !== right.publisher.pid
2776
+ || left.publisher.token !== right.publisher.token
2777
+ || left.publisher.processIdentityDigest !== right.publisher.processIdentityDigest
2778
+ || !sameBootstrapRegistrationIntentFileIdentity(left.publisher.identity, right.publisher.identity)) {
83
2779
  return false;
84
- if (typeof attempt['outcome'] !== 'string' || !BOOTSTRAP_FAILURE_OUTCOMES.has(attempt['outcome']))
2780
+ }
2781
+ if ((left.publishing === undefined) !== (right.publishing === undefined))
85
2782
  return false;
86
- if (typeof attempt['attemptedAt'] !== 'string')
2783
+ if (left.publishing && right.publishing
2784
+ && !sameBootstrapRegistrationIntentFileIdentity(left.publishing, right.publishing)) {
87
2785
  return false;
88
- const attemptedAt = Date.parse(attempt['attemptedAt']);
89
- if (Number.isNaN(attemptedAt))
2786
+ }
2787
+ if ((left.registration === undefined) !== (right.registration === undefined))
90
2788
  return false;
91
- return now - attemptedAt < BOOTSTRAP_FAILURE_COOLDOWN_MS;
2789
+ if (!left.registration || !right.registration)
2790
+ return true;
2791
+ return sameBootstrapRegistrationIntentFileIdentity(left.registration.identity, right.registration.identity) && sameBootstrapRegistrationIntentOwner(left.registration.lease.record, right.registration.lease.record);
92
2792
  }
93
- /**
94
- * Decide whether an unsupervised (degraded) startup should attempt first-run bootstrap.
95
- * Pure filesystem access is injectable for tests.
96
- */
97
- export function shouldBootstrap(env, platform = process.platform, options = {}) {
98
- if (selfUpdateSupervisorAttested(env))
99
- return { proceed: false, reason: 'already_supervised' };
100
- const bootstrapSwitch = env['EVOLVER_SELF_BOOTSTRAP']?.trim();
101
- if (bootstrapSwitch === '0' || bootstrapSwitch === 'off')
102
- return { proceed: false, reason: 'bootstrap_disabled' };
103
- if (isSelfUpdateExplicit(env) && resolveSelfUpdatePolicy(env) !== 'auto') {
104
- return { proceed: false, reason: 'policy_not_auto' };
2793
+ function assertBootstrapRegistrationIntentFileIdentity(options, expected) {
2794
+ assertBootstrapIntentFileTrust(options, expected.path, Number(expected.linkCount));
2795
+ const current = lstatSync(expected.path, { bigint: true });
2796
+ if (!current.isFile() || current.isSymbolicLink() || current.nlink !== expected.linkCount
2797
+ || current.dev !== expected.device || current.ino !== expected.inode
2798
+ || current.size !== expected.size || current.mtimeNs !== expected.mtimeNs
2799
+ || current.ctimeNs !== expected.ctimeNs) {
2800
+ throw new Error('incomplete registration publication identity changed');
105
2801
  }
106
- const exists = options.exists ?? existsSync;
107
- if (exists(join(resolveBootstrapStateDir(env), BOOTSTRAP_SUCCESS_FILE))) {
108
- return { proceed: false, reason: 'already_bootstrapped' };
109
- }
110
- // The npm/JS install shape has no replaceable standalone binary target, so the launcher
111
- // bootstrap would register a supervised instance that crashes at self-update target
112
- // resolution on every startup (crash-loop under the service manager). Skip it; an explicit
113
- // EVOLVER_SELF_UPDATE_TARGET_PATH keeps the target bindable and bypasses this guard.
114
- try {
115
- resolveSelfUpdateTarget({ env, processExecPath: options.execPath });
2802
+ }
2803
+ function clearBootstrapIncompletePublication(options, publication) {
2804
+ const stateDir = resolveBootstrapStateDir(options.env);
2805
+ assertBootstrapIntentDirectoryTrust(options, stateDir, true);
2806
+ assertBootstrapRegistrationIntentFileIdentity(options, publication.publisher.identity);
2807
+ if (publication.publishing) {
2808
+ assertBootstrapRegistrationIntentFileIdentity(options, publication.publishing);
116
2809
  }
117
- catch {
118
- return { proceed: false, reason: 'unsupported_install_shape' };
2810
+ if (publication.registration) {
2811
+ assertBootstrapRegistrationIntentFileIdentity(options, publication.registration.identity);
2812
+ rmSync(publication.registration.identity.path);
2813
+ syncBootstrapDirectory(stateDir);
2814
+ if (bootstrapStateEntryPresent(publication.registration.identity.path)) {
2815
+ throw new Error('incomplete canonical registration claim retirement is unconfirmed');
2816
+ }
2817
+ if (publication.publishing
2818
+ && publication.registration.identity.device === publication.publishing.device
2819
+ && publication.registration.identity.inode === publication.publishing.inode) {
2820
+ assertBootstrapIntentFileTrust(options, publication.publishing.path);
2821
+ const remaining = lstatSync(publication.publishing.path, { bigint: true });
2822
+ if (!remaining.isFile() || remaining.isSymbolicLink() || remaining.nlink !== 1n
2823
+ || remaining.dev !== publication.publishing.device
2824
+ || remaining.ino !== publication.publishing.inode
2825
+ || remaining.size !== publication.publishing.size
2826
+ || remaining.mtimeNs !== publication.publishing.mtimeNs) {
2827
+ throw new Error('incomplete registration publication changed after claim retirement');
2828
+ }
2829
+ }
119
2830
  }
120
- const ci = env['CI']?.trim();
121
- if (ci && ci.toLowerCase() !== 'false' && ci !== '0')
122
- return { proceed: false, reason: 'ci_environment' };
123
- if (platform === 'linux' && looksLikeContainer(exists, options.readFile ?? defaultReadTextFile)) {
124
- return { proceed: false, reason: 'container_environment' };
2831
+ if (publication.publishing) {
2832
+ if (!publication.registration
2833
+ || publication.registration.identity.device !== publication.publishing.device
2834
+ || publication.registration.identity.inode !== publication.publishing.inode) {
2835
+ assertBootstrapRegistrationIntentFileIdentity(options, publication.publishing);
2836
+ }
2837
+ rmSync(publication.publishing.path);
2838
+ syncBootstrapDirectory(stateDir);
2839
+ if (bootstrapStateEntryPresent(publication.publishing.path)) {
2840
+ throw new Error('incomplete registration publication retirement is unconfirmed');
2841
+ }
125
2842
  }
126
- if (recentBootstrapFailure(env, options.readFile ?? defaultReadTextFile, options.now ?? Date.now())) {
127
- return { proceed: false, reason: 'recent_failure' };
2843
+ retireBootstrapRegistrationPublisherClaim(options, publication.publisher);
2844
+ if (bootstrapStateEntryPresent(join(stateDir, BOOTSTRAP_REGISTRATION_INTENT_FILE))
2845
+ || bootstrapStateEntryPresent(join(stateDir, BOOTSTRAP_REGISTRATION_INTENT_PUBLISHING_FILE))
2846
+ || bootstrapRegistrationPublisherEntryPresent(stateDir)) {
2847
+ throw new Error('incomplete registration publication retirement is unconfirmed');
128
2848
  }
129
- return { proceed: true };
130
2849
  }
131
- /**
132
- * Resolve the `lifecycle bootstrap` invocation for the current install shape: standalone binary,
133
- * CLI entry through node, or the npm-installed @evomap/evolver-cli sibling. Returns undefined when
134
- * no CLI can be located (degrade to the existing warning instead of failing startup).
135
- */
136
- export function resolveBootstrapCliInvocation(options = {}) {
137
- const execPath = options.execPath ?? process.execPath;
138
- const argv1 = options.argv1 ?? process.argv[1];
139
- const exists = options.exists ?? existsSync;
140
- const executableName = basename(execPath).toLowerCase();
141
- if (/^evolver(?:\.exe|-(?:darwin-(?:arm64|x64)|linux-(?:arm64|x64)|windows-x64\.exe))?$/.test(executableName)) {
142
- return { command: execPath, args: ['lifecycle', 'bootstrap'] };
143
- }
144
- if (argv1 && basename(argv1).toLowerCase() === 'cli.js') {
145
- return { command: execPath, args: [argv1, 'lifecycle', 'bootstrap'] };
2850
+ function bootstrapRegistrationOwnerIsStale(status) {
2851
+ return status === 'dead' || status === 'pid_reused';
2852
+ }
2853
+ function recoverBootstrapRegistrationIntent(options) {
2854
+ const intent = readBootstrapRegistrationIntent(options);
2855
+ if (intent.status === 'absent' || intent.status === 'invalid') {
2856
+ return {
2857
+ status: 'blocked',
2858
+ detail: intent.status === 'invalid' ? intent.detail : 'registration intent disappeared during recovery',
2859
+ };
146
2860
  }
147
2861
  try {
148
- const entry = requireFromHere.resolve('@evomap/evolver-cli');
149
- const cliPath = join(dirname(entry), 'cli.js');
150
- if (exists(cliPath))
151
- return { command: execPath, args: [cliPath, 'lifecycle', 'bootstrap'] };
2862
+ if (intent.status === 'incomplete_publication') {
2863
+ const observedPublisherStatus = bootstrapRegistrationPublisherProcessStatus(options, intent.publication.publisher);
2864
+ if (!bootstrapRegistrationOwnerIsStale(observedPublisherStatus)) {
2865
+ return {
2866
+ status: 'blocked',
2867
+ detail: `registration publisher owner is ${observedPublisherStatus}`,
2868
+ };
2869
+ }
2870
+ let recoveryLock;
2871
+ try {
2872
+ recoveryLock = acquireBootstrapRegistrationRecoveryLock(options);
2873
+ }
2874
+ catch {
2875
+ return {
2876
+ status: 'blocked',
2877
+ detail: 'registration publisher or another recovery owns the bootstrap transaction lock',
2878
+ };
2879
+ }
2880
+ try {
2881
+ const lockedIntent = readBootstrapRegistrationIntent(options);
2882
+ if (lockedIntent.status !== 'incomplete_publication'
2883
+ || !sameBootstrapIncompletePublication(intent.publication, lockedIntent.publication)) {
2884
+ return {
2885
+ status: 'blocked',
2886
+ detail: 'registration publication changed while acquiring recovery ownership',
2887
+ };
2888
+ }
2889
+ const lockedPublisherStatus = bootstrapRegistrationPublisherProcessStatus(options, lockedIntent.publication.publisher);
2890
+ if (!bootstrapRegistrationOwnerIsStale(lockedPublisherStatus)) {
2891
+ return {
2892
+ status: 'blocked',
2893
+ detail: `registration publisher owner became ${lockedPublisherStatus}`,
2894
+ };
2895
+ }
2896
+ const lockedDurable = inspectLifecycleBootstrapDurableState(options.env, { ...options, expectedRecoveryOwner: recoveryLock.owner });
2897
+ if (lockedDurable.status !== 'clean'
2898
+ && lockedDurable.status !== 'committed'
2899
+ && lockedDurable.status !== 'manual_transition') {
2900
+ return {
2901
+ status: 'blocked',
2902
+ detail: lockedDurable.status === 'invalid' || lockedDurable.status === 'pending'
2903
+ ? lockedDurable.detail
2904
+ : 'incomplete registration publication durable state is unresolved',
2905
+ };
2906
+ }
2907
+ clearBootstrapIncompletePublication(options, lockedIntent.publication);
2908
+ return lockedDurable.status === 'committed'
2909
+ ? { status: 'committed', transactionId: lockedDurable.transactionId }
2910
+ : {
2911
+ status: 'safe_terminal',
2912
+ outcome: lockedDurable.status === 'manual_transition'
2913
+ ? 'rolled_back'
2914
+ : 'cancelled',
2915
+ };
2916
+ }
2917
+ finally {
2918
+ recoveryLock.release();
2919
+ }
2920
+ }
2921
+ if (intent.status === 'present' && intent.lease.record.state === 'registering') {
2922
+ const observedStatus = bootstrapRegistrationOwnerProcessStatus(options, intent.lease.record.owner);
2923
+ if (!bootstrapRegistrationOwnerIsStale(observedStatus)) {
2924
+ return {
2925
+ status: 'blocked',
2926
+ detail: `registration intent owner is ${observedStatus}`,
2927
+ };
2928
+ }
2929
+ let recoveryLock;
2930
+ try {
2931
+ recoveryLock = acquireBootstrapRegistrationRecoveryLock(options);
2932
+ }
2933
+ catch {
2934
+ return {
2935
+ status: 'blocked',
2936
+ detail: 'registration child or another recovery owns the bootstrap transaction lock',
2937
+ };
2938
+ }
2939
+ try {
2940
+ const lockedIntent = readBootstrapRegistrationIntent(options);
2941
+ if (lockedIntent.status !== 'present'
2942
+ || lockedIntent.lease.record.state !== 'registering'
2943
+ || lockedIntent.lease.currentPath !== intent.lease.currentPath
2944
+ || lockedIntent.lease.device !== intent.lease.device
2945
+ || lockedIntent.lease.inode !== intent.lease.inode
2946
+ || lockedIntent.lease.size !== intent.lease.size
2947
+ || lockedIntent.lease.mtimeNs !== intent.lease.mtimeNs
2948
+ || lockedIntent.lease.ctimeNs !== intent.lease.ctimeNs
2949
+ || !sameBootstrapRegistrationIntentOwner(lockedIntent.lease.record, intent.lease.record)) {
2950
+ return {
2951
+ status: 'blocked',
2952
+ detail: 'registration intent changed while acquiring recovery ownership',
2953
+ };
2954
+ }
2955
+ const lockedOwnerStatus = bootstrapRegistrationOwnerProcessStatus(options, lockedIntent.lease.record.owner);
2956
+ if (!bootstrapRegistrationOwnerIsStale(lockedOwnerStatus)) {
2957
+ return {
2958
+ status: 'blocked',
2959
+ detail: `registration intent owner became ${lockedOwnerStatus}`,
2960
+ };
2961
+ }
2962
+ const lockedDurable = inspectLifecycleBootstrapDurableState(options.env, { ...options, expectedRecoveryOwner: recoveryLock.owner });
2963
+ if (lockedDurable.status !== 'clean'
2964
+ && lockedDurable.status !== 'committed'
2965
+ && lockedDurable.status !== 'manual_transition') {
2966
+ return {
2967
+ status: 'blocked',
2968
+ detail: lockedDurable.status === 'invalid' || lockedDurable.status === 'pending'
2969
+ ? lockedDurable.detail
2970
+ : 'registration intent durable state is unresolved',
2971
+ };
2972
+ }
2973
+ const terminal = terminalizeBootstrapRegistrationIntent(options, lockedIntent.lease, lockedDurable.status === 'committed'
2974
+ ? 'committed'
2975
+ : lockedDurable.status === 'manual_transition'
2976
+ ? 'rolled_back'
2977
+ : 'cancelled', lockedDurable.status === 'committed' ? lockedDurable.transactionId : undefined);
2978
+ clearBootstrapRegistrationIntent(options, terminal);
2979
+ return lockedDurable.status === 'committed'
2980
+ ? { status: 'committed', transactionId: lockedDurable.transactionId }
2981
+ : {
2982
+ status: 'safe_terminal',
2983
+ outcome: lockedDurable.status === 'manual_transition'
2984
+ ? 'rolled_back'
2985
+ : 'cancelled',
2986
+ };
2987
+ }
2988
+ finally {
2989
+ recoveryLock.release();
2990
+ }
2991
+ }
2992
+ const durable = inspectLifecycleBootstrapDurableState(options.env, options);
2993
+ let lease;
2994
+ if (intent.status === 'incomplete_terminal') {
2995
+ if (durable.status !== 'clean'
2996
+ && durable.status !== 'committed'
2997
+ && durable.status !== 'manual_transition') {
2998
+ return {
2999
+ status: 'blocked',
3000
+ detail: durable.status === 'invalid' || durable.status === 'pending'
3001
+ ? durable.detail
3002
+ : 'incomplete terminal receipt has no safe durable proof',
3003
+ };
3004
+ }
3005
+ assertBootstrapIntentFileTrust(options, intent.terminalFile.path);
3006
+ const partial = lstatSync(intent.terminalFile.path, { bigint: true });
3007
+ if (!partial.isFile() || partial.isSymbolicLink() || partial.nlink !== 1n
3008
+ || partial.dev !== intent.terminalFile.device || partial.ino !== intent.terminalFile.inode) {
3009
+ throw new Error('incomplete terminal receipt ownership changed');
3010
+ }
3011
+ rmSync(intent.terminalFile.path);
3012
+ syncBootstrapDirectory(dirname(intent.terminalFile.path));
3013
+ if (bootstrapStateEntryPresent(intent.terminalFile.path)) {
3014
+ throw new Error('incomplete terminal receipt retirement is unconfirmed');
3015
+ }
3016
+ lease = terminalizeBootstrapRegistrationIntent(options, intent.registering, durable.status === 'committed'
3017
+ ? 'committed'
3018
+ : durable.status === 'manual_transition'
3019
+ ? 'rolled_back'
3020
+ : 'no_child', durable.status === 'committed' ? durable.transactionId : undefined);
3021
+ }
3022
+ else {
3023
+ lease = intent.lease;
3024
+ }
3025
+ if (lease.record.state === 'registering') {
3026
+ if (durable.status === 'manual_transition') {
3027
+ lease = terminalizeBootstrapRegistrationIntent(options, lease, 'rolled_back');
3028
+ }
3029
+ else if (durable.status !== 'committed') {
3030
+ const detail = durable.status === 'clean'
3031
+ ? 'registration intent has no durable child completion proof'
3032
+ : durable.detail;
3033
+ return { status: 'blocked', detail };
3034
+ }
3035
+ else {
3036
+ lease = terminalizeBootstrapRegistrationIntent(options, lease, 'committed', durable.transactionId);
3037
+ }
3038
+ }
3039
+ const terminal = lease.record;
3040
+ if (terminal.state !== 'terminal') {
3041
+ return { status: 'blocked', detail: 'registration intent did not reach a terminal state' };
3042
+ }
3043
+ if (terminal.outcome === 'committed') {
3044
+ if (durable.status !== 'committed'
3045
+ || durable.transactionId !== terminal.transactionId) {
3046
+ return { status: 'blocked', detail: 'registration intent commit receipt conflicts with durable state' };
3047
+ }
3048
+ clearBootstrapRegistrationIntent(options, lease);
3049
+ return { status: 'committed', transactionId: durable.transactionId };
3050
+ }
3051
+ const safeRolledBackTransition = terminal.outcome === 'rolled_back'
3052
+ && durable.status === 'manual_transition';
3053
+ if (durable.status !== 'clean' && !safeRolledBackTransition) {
3054
+ return {
3055
+ status: 'blocked',
3056
+ detail: durable.status === 'invalid' || durable.status === 'pending'
3057
+ ? durable.detail
3058
+ : 'registration intent terminal receipt conflicts with committed state',
3059
+ };
3060
+ }
3061
+ const outcome = terminal.outcome;
3062
+ clearBootstrapRegistrationIntent(options, lease);
3063
+ return { status: 'safe_terminal', outcome };
152
3064
  }
153
- catch {
154
- // Not resolvable from the installed proxy package — fall through to the monorepo layout.
3065
+ catch (error) {
3066
+ return {
3067
+ status: 'blocked',
3068
+ detail: boundedBootstrapDetail(`registration intent recovery failed: ${error instanceof Error ? error.message : String(error)}`),
3069
+ };
155
3070
  }
156
- const local = fileURLToPath(new URL('../../../evolver-cli/dist/cli.js', import.meta.url));
157
- if (exists(local))
158
- return { command: execPath, args: [local, 'lifecycle', 'bootstrap'] };
159
- return undefined;
160
3071
  }
161
- /** Best-effort attempt marker; never throws — bootstrap bookkeeping must not break startup. */
162
- export function recordBootstrapAttempt(env, outcome, options = {}) {
163
- const path = join(resolveBootstrapStateDir(env), BOOTSTRAP_ATTEMPT_FILE);
164
- const record = {
165
- attemptedAt: new Date(options.now ?? Date.now()).toISOString(),
166
- outcome: outcome.reason,
167
- ...(outcome.detail ? { detail: outcome.detail } : {}),
168
- };
3072
+ async function runBootstrapWithRegistrationIntent(options, prepareRun) {
3073
+ let lease;
169
3074
  try {
170
- const writeFile = options.writeFile ?? ((target, content) => {
171
- mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
172
- writeFileSync(target, content, { encoding: 'utf8', mode: 0o600 });
173
- });
174
- writeFile(path, `${JSON.stringify(record)}\n`);
3075
+ lease = publishBootstrapRegistrationIntent(options);
175
3076
  }
176
3077
  catch {
177
- // Marker is advisory; startup continues regardless.
3078
+ return {
3079
+ ok: false,
3080
+ reason: 'blocked',
3081
+ detail: 'bootstrap registration intent publication is unavailable',
3082
+ requiresForegroundExit: true,
3083
+ };
178
3084
  }
179
- }
180
- /** Spawn `evolver lifecycle bootstrap` and await its result within a bounded timeout. */
181
- export async function runBootstrap(options) {
182
- const invocation = resolveBootstrapCliInvocation(options);
183
- if (!invocation)
184
- return { ok: false, reason: 'cli_not_found' };
185
- const spawnFn = options.spawnFn ?? spawn;
186
- const timeoutMs = options.timeoutMs ?? BOOTSTRAP_TIMEOUT_MS;
187
- return new Promise((resolvePromise) => {
188
- let child;
3085
+ let runOptions = options;
3086
+ let outcome;
3087
+ let parentCurrentAtFinalClock = false;
3088
+ if (prepareRun) {
189
3089
  try {
190
- child = spawnFn(invocation.command, invocation.args, {
191
- stdio: 'ignore',
192
- windowsHide: true,
193
- env: bootstrapChildEnv(options.env),
194
- });
3090
+ runOptions = {
3091
+ ...options,
3092
+ ...await prepareRun(() => {
3093
+ assertBootstrapRegistrationOwnerCurrent(options, lease.record.owner);
3094
+ parentCurrentAtFinalClock = true;
3095
+ }),
3096
+ };
3097
+ if (!parentCurrentAtFinalClock) {
3098
+ throw new Error('bootstrap registration preparation omitted parent identity revalidation');
3099
+ }
195
3100
  }
196
3101
  catch (error) {
197
- resolvePromise({ ok: false, reason: 'failed', detail: error instanceof Error ? error.message : String(error) });
198
- return;
3102
+ // The preparation hook runs before child creation, so a strict terminal no-child receipt
3103
+ // can retire this bootstrap intent while the migration transaction handles its own state.
3104
+ outcome = {
3105
+ ok: false,
3106
+ reason: 'failed',
3107
+ detail: boundedBootstrapDetail(error instanceof Error ? error.message : String(error)),
3108
+ };
199
3109
  }
200
- let settled = false;
201
- const settle = (outcome) => {
202
- if (settled)
203
- return;
204
- settled = true;
205
- clearTimeout(timer);
206
- resolvePromise(outcome);
3110
+ }
3111
+ if (!outcome) {
3112
+ try {
3113
+ // Re-read the parent process identity after every asynchronous sealed check, then bind the
3114
+ // exact unique intent token into only the child environment. runBootstrap reaches spawn
3115
+ // synchronously, so no await or mutation window exists between this check and child creation.
3116
+ if (!parentCurrentAtFinalClock) {
3117
+ assertBootstrapRegistrationOwnerCurrent(runOptions, lease.record.owner);
3118
+ }
3119
+ runOptions = {
3120
+ ...runOptions,
3121
+ env: {
3122
+ ...runOptions.env,
3123
+ [coreBootstrap.LIFECYCLE_BOOTSTRAP_REGISTRATION_TOKEN_ENV]: lease.record.owner.token,
3124
+ },
3125
+ };
3126
+ }
3127
+ catch (error) {
3128
+ outcome = {
3129
+ ok: false,
3130
+ reason: 'failed',
3131
+ detail: boundedBootstrapDetail(error instanceof Error ? error.message : String(error)),
3132
+ };
3133
+ }
3134
+ }
3135
+ if (!outcome) {
3136
+ try {
3137
+ outcome = await runBootstrap(runOptions);
3138
+ }
3139
+ catch {
3140
+ // The no-replace intent remains authoritative until an operator or durable recovery clears it.
3141
+ return {
3142
+ ok: false,
3143
+ reason: 'ambiguous',
3144
+ detail: 'bootstrap registration runner failed unexpectedly',
3145
+ requiresForegroundExit: true,
3146
+ };
3147
+ }
3148
+ }
3149
+ if (outcome.requiresForegroundExit === true)
3150
+ return outcome;
3151
+ try {
3152
+ const durable = inspectLifecycleBootstrapDurableState(runOptions.env, runOptions);
3153
+ if (outcome.ok) {
3154
+ if (durable.status !== 'committed') {
3155
+ throw new Error('bootstrap success lacks a durable committed transaction');
3156
+ }
3157
+ lease = terminalizeBootstrapRegistrationIntent(runOptions, lease, 'committed', durable.transactionId);
3158
+ }
3159
+ else if (outcome.reason === 'rolled_back') {
3160
+ if (durable.status !== 'clean' && durable.status !== 'manual_transition') {
3161
+ throw new Error('bootstrap rollback did not restore a clean durable state');
3162
+ }
3163
+ lease = terminalizeBootstrapRegistrationIntent(runOptions, lease, 'rolled_back');
3164
+ }
3165
+ else if (['failed', 'cli_not_found'].includes(outcome.reason)) {
3166
+ if (durable.status !== 'clean') {
3167
+ throw new Error('bootstrap no-child failure left durable state');
3168
+ }
3169
+ lease = terminalizeBootstrapRegistrationIntent(runOptions, lease, 'no_child');
3170
+ }
3171
+ else {
3172
+ throw new Error('bootstrap result does not prove a safe terminal state');
3173
+ }
3174
+ clearBootstrapRegistrationIntent(runOptions, lease);
3175
+ }
3176
+ catch {
3177
+ return {
3178
+ ok: false,
3179
+ reason: 'blocked',
3180
+ detail: 'bootstrap registration intent clear is unconfirmed',
3181
+ requiresForegroundExit: true,
207
3182
  };
208
- const timer = setTimeout(() => {
209
- child.kill();
210
- settle({ ok: false, reason: 'timeout' });
211
- }, timeoutMs);
212
- child.once('error', (error) => {
213
- settle({ ok: false, reason: 'failed', detail: error.message });
214
- });
215
- child.once('exit', (code) => {
216
- if (code === 0)
217
- settle({ ok: true, reason: 'bootstrapped' });
218
- else
219
- settle({ ok: false, reason: 'failed', detail: `exit ${code ?? 'null'}` });
220
- });
3183
+ }
3184
+ return outcome;
3185
+ }
3186
+ function defaultMigrationRegistrationRunner(options) {
3187
+ const clock = options.migration?.clock ?? Date.now;
3188
+ const failed = (detail) => ({
3189
+ ok: false,
3190
+ reason: 'failed',
3191
+ detail: boundedBootstrapDetail(detail),
221
3192
  });
3193
+ const readClock = () => {
3194
+ try {
3195
+ const value = clock();
3196
+ return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
3197
+ }
3198
+ catch {
3199
+ return undefined;
3200
+ }
3201
+ };
3202
+ return async (request) => {
3203
+ if (request.execPath !== request.targetIdentity.path
3204
+ || request.env['EVOLVER_SELF_UPDATE_TARGET_PATH'] !== request.execPath
3205
+ || !Number.isSafeInteger(request.startedAtMs)
3206
+ || request.startedAtMs < 0
3207
+ || !Number.isSafeInteger(request.transactionBudgetMs)
3208
+ || request.transactionBudgetMs <= 0
3209
+ || !Number.isSafeInteger(request.timeoutMs)
3210
+ || request.timeoutMs > MAX_TIMER_DELAY_MS
3211
+ || !Number.isSafeInteger(request.transactionDeadlineMs)
3212
+ || !Number.isSafeInteger(request.parentDeadlineMs)
3213
+ || request.transactionDeadlineMs - request.startedAtMs !== request.transactionBudgetMs
3214
+ || request.parentDeadlineMs - request.startedAtMs !== request.timeoutMs
3215
+ || request.timeoutMs <= request.transactionBudgetMs) {
3216
+ return failed('invalid migration registration binding');
3217
+ }
3218
+ return runBootstrapWithRegistrationIntent({
3219
+ env: request.env,
3220
+ platform: request.platform,
3221
+ execPath: request.execPath,
3222
+ ...(request.exists !== undefined ? { exists: request.exists } : {}),
3223
+ ...(request.readFile !== undefined ? { readFile: request.readFile } : {}),
3224
+ ...(request.writeFile !== undefined ? { writeFile: request.writeFile } : {}),
3225
+ ...(request.spawnFn !== undefined ? { spawnFn: request.spawnFn } : {}),
3226
+ ...(options.treeKillSpawnFn !== undefined ? { treeKillSpawnFn: options.treeKillSpawnFn } : {}),
3227
+ ...(options.processKill !== undefined ? { processKill: options.processKill } : {}),
3228
+ ...(options.terminationGraceMs !== undefined ? { terminationGraceMs: options.terminationGraceMs } : {}),
3229
+ ...(options.beforeIntentPublish !== undefined ? { beforeIntentPublish: options.beforeIntentPublish } : {}),
3230
+ ...(options.afterIntentPublicationStep !== undefined
3231
+ ? { afterIntentPublicationStep: options.afterIntentPublicationStep }
3232
+ : {}),
3233
+ ...(options.afterIntentTerminalPublish !== undefined
3234
+ ? { afterIntentTerminalPublish: options.afterIntentTerminalPublish }
3235
+ : {}),
3236
+ ...(options.beforeIntentTerminalFsync !== undefined
3237
+ ? { beforeIntentTerminalFsync: options.beforeIntentTerminalFsync }
3238
+ : {}),
3239
+ ...(options.afterIntentClearRename !== undefined
3240
+ ? { afterIntentClearRename: options.afterIntentClearRename }
3241
+ : {}),
3242
+ ...(options.assertIntentDirectoryTrust !== undefined
3243
+ ? { assertIntentDirectoryTrust: options.assertIntentDirectoryTrust }
3244
+ : {}),
3245
+ ...(options.assertIntentFileTrust !== undefined
3246
+ ? { assertIntentFileTrust: options.assertIntentFileTrust }
3247
+ : {}),
3248
+ ...(options.readRegistrationProcessStartIdentity !== undefined
3249
+ ? { readRegistrationProcessStartIdentity: options.readRegistrationProcessStartIdentity }
3250
+ : {}),
3251
+ ...(options.registrationOwnerProcessStatus !== undefined
3252
+ ? { registrationOwnerProcessStatus: options.registrationOwnerProcessStatus }
3253
+ : {}),
3254
+ ...(options.registrationPublisherProcessStatus !== undefined
3255
+ ? { registrationPublisherProcessStatus: options.registrationPublisherProcessStatus }
3256
+ : {}),
3257
+ }, async (assertParentCurrent) => {
3258
+ // The bootstrap intent is durable before these sealed checks. No await occurs between
3259
+ // successful target revalidation, remaining-budget calculation, and runBootstrap's spawn.
3260
+ const beforeRevalidation = readClock();
3261
+ if (beforeRevalidation === undefined
3262
+ || beforeRevalidation < request.startedAtMs
3263
+ || beforeRevalidation >= request.transactionDeadlineMs
3264
+ || beforeRevalidation >= request.parentDeadlineMs) {
3265
+ throw new Error('migration registration deadline expired before target revalidation');
3266
+ }
3267
+ try {
3268
+ await request.assertRegistrationIntentCurrent();
3269
+ await request.revalidateTarget();
3270
+ }
3271
+ catch {
3272
+ throw new Error('migration registration intent or target revalidation failed');
3273
+ }
3274
+ // This native observation can block. Run it before the final absolute-deadline clock read
3275
+ // so its elapsed time is subtracted instead of silently extending either remaining budget.
3276
+ assertParentCurrent();
3277
+ const current = readClock();
3278
+ if (current === undefined || current < beforeRevalidation) {
3279
+ throw new Error('migration registration clock regressed');
3280
+ }
3281
+ const transactionBudgetMs = request.transactionDeadlineMs - current;
3282
+ const timeoutMs = request.parentDeadlineMs - current;
3283
+ if (!Number.isSafeInteger(transactionBudgetMs)
3284
+ || !Number.isSafeInteger(timeoutMs)
3285
+ || transactionBudgetMs <= 0
3286
+ || timeoutMs <= transactionBudgetMs
3287
+ || timeoutMs > MAX_TIMER_DELAY_MS) {
3288
+ throw new Error('migration registration deadline expired before child creation');
3289
+ }
3290
+ return { now: current, transactionBudgetMs, timeoutMs };
3291
+ });
3292
+ };
222
3293
  }
223
3294
  /**
224
3295
  * Orchestrate bootstrap for a degraded (default-auto, unsupervised) startup: decide, attempt,
@@ -229,52 +3300,223 @@ export async function bootstrapDegradedSelfUpdateStartup(env, platform = process
229
3300
  ...env,
230
3301
  EVOLVER_LIFECYCLE_STATE_DIR: resolveBootstrapStateDir(env),
231
3302
  };
3303
+ const migrateUnbindableInstall = async (reason) => {
3304
+ if (reason === 'bootstrap_attempt_pending') {
3305
+ const durable = inspectLifecycleBootstrapDurableState(bootstrapEnv, options);
3306
+ if (durable.status === 'manual_transition') {
3307
+ return {
3308
+ disposition: 'continue',
3309
+ handedOver: false,
3310
+ message: '[evolver-proxy] self-update: automatic bootstrap remains disabled by a durable '
3311
+ + 'manual service transition; continuing degraded without downloading, replacing, or '
3312
+ + 'registering a standalone binary. Run evolver lifecycle install-service --target='
3313
+ + durable.target
3314
+ + ' explicitly to resume.',
3315
+ };
3316
+ }
3317
+ if (durable.status === 'pending' && durable.manualTransition) {
3318
+ return {
3319
+ disposition: 'fail_closed',
3320
+ handedOver: false,
3321
+ exitCode: 1,
3322
+ message: '[evolver-proxy] self-update: manual service transition recovery is incomplete; '
3323
+ + 'exiting without downloading, replacing, or registering a standalone binary. '
3324
+ + `(${boundedBootstrapDetail(durable.detail)}) Run evolver lifecycle remove-service --target=`
3325
+ + durable.manualTransition.target
3326
+ + ' to finish the interrupted removal before explicitly reinstalling the service.',
3327
+ };
3328
+ }
3329
+ if (durable.status === 'pending'
3330
+ && Object.values(durable.recoveryLocks ?? {})
3331
+ .some((status) => status === 'active_or_unverifiable')) {
3332
+ return {
3333
+ disposition: 'fail_closed',
3334
+ handedOver: false,
3335
+ exitCode: 1,
3336
+ message: '[evolver-proxy] self-update: lifecycle recovery lock ownership is active or '
3337
+ + 'unverifiable; exiting before migration download, target mutation, or child creation. ('
3338
+ + boundedBootstrapDetail(durable.detail)
3339
+ + ')',
3340
+ };
3341
+ }
3342
+ if (durable.status === 'invalid') {
3343
+ return {
3344
+ disposition: 'fail_closed',
3345
+ handedOver: false,
3346
+ exitCode: 1,
3347
+ message: '[evolver-proxy] self-update: lifecycle recovery state is invalid; exiting without '
3348
+ + 'migration, target mutation, or child creation. ('
3349
+ + boundedBootstrapDetail(durable.detail)
3350
+ + ')',
3351
+ };
3352
+ }
3353
+ }
3354
+ // The npm/JS install shape cannot safely run ordinary lifecycle bootstrap because it would
3355
+ // bind supervision back to an unreplaceable Node launcher. Only the signed standalone
3356
+ // migration may register recovery, and pending ownership stays fail-closed on every skip.
3357
+ const migration = await migrateToStandaloneBinary(bootstrapEnv, platform, {
3358
+ ...options.migration,
3359
+ registrationRunner: options.migration?.registrationRunner
3360
+ ?? defaultMigrationRegistrationRunner(options),
3361
+ ...(options.execPath !== undefined ? { execPath: options.execPath } : {}),
3362
+ ...(options.exists !== undefined ? { exists: options.exists } : {}),
3363
+ ...(options.readFile !== undefined ? { readFile: options.readFile } : {}),
3364
+ ...(options.writeFile !== undefined ? { writeFile: options.writeFile } : {}),
3365
+ ...(options.spawnFn !== undefined ? { spawnFn: options.spawnFn } : {}),
3366
+ ...(options.now !== undefined ? { now: options.now } : {}),
3367
+ });
3368
+ const recoveringPendingOwnership = reason === 'migration_ambiguous'
3369
+ || reason === 'bootstrap_attempt_pending';
3370
+ if (migration.requiresForegroundExit === true
3371
+ || (recoveringPendingOwnership && migration.outcome !== 'migrated')) {
3372
+ return {
3373
+ disposition: 'fail_closed',
3374
+ handedOver: false,
3375
+ exitCode: 1,
3376
+ message: '[evolver-proxy] self-update: standalone migration registration did not prove a clean rollback; '
3377
+ + 'exiting the foreground proxy to avoid concurrent IPC ownership. '
3378
+ + 'Inspect the lifecycle bootstrap transaction state before retrying.',
3379
+ };
3380
+ }
3381
+ if (migration.outcome === 'migrated') {
3382
+ return {
3383
+ disposition: 'handoff',
3384
+ handedOver: true,
3385
+ exitCode: 0,
3386
+ message: migration.message,
3387
+ };
3388
+ }
3389
+ if (migration.outcome === 'skipped' && migration.reason !== 'cooldown') {
3390
+ recordBootstrapAttempt(bootstrapEnv, { ok: false, reason }, options);
3391
+ }
3392
+ return {
3393
+ disposition: 'continue',
3394
+ handedOver: false,
3395
+ message: '[evolver-proxy] self-update: running from the npm/JS install shape, which has no standalone '
3396
+ + 'binary target for self-update; bootstrap skipped, continuing with self-update off. '
3397
+ + 'Install the standalone binary from GitHub Releases and start it to enable self-update. ('
3398
+ + migration.message
3399
+ + ')',
3400
+ };
3401
+ };
232
3402
  const decision = shouldBootstrap(bootstrapEnv, platform, options);
233
3403
  if (!decision.proceed) {
234
3404
  const reason = decision.reason ?? 'skipped';
235
- recordBootstrapAttempt(bootstrapEnv, { ok: false, reason }, options);
236
- if (reason === 'unsupported_install_shape') {
237
- // Do not suggest `evolver lifecycle bootstrap` here: under the npm/JS install shape it
238
- // would register a supervised service that crashes at self-update target resolution on
239
- // every startup (crash-loop). Only a standalone release binary can host self-update —
240
- // so attempt the one-time migration to it; any skip/failure keeps the degraded startup.
241
- const migration = await migrateToStandaloneBinary(env, platform, {
242
- ...options.migration,
243
- ...(options.execPath !== undefined ? { execPath: options.execPath } : {}),
244
- ...(options.exists !== undefined ? { exists: options.exists } : {}),
245
- ...(options.readFile !== undefined ? { readFile: options.readFile } : {}),
246
- ...(options.writeFile !== undefined ? { writeFile: options.writeFile } : {}),
247
- ...(options.spawnFn !== undefined ? { spawnFn: options.spawnFn } : {}),
248
- ...(options.now !== undefined ? { now: options.now } : {}),
3405
+ if (reason === 'bootstrap_attempt_invalid') {
3406
+ return {
3407
+ disposition: 'fail_closed',
3408
+ handedOver: false,
3409
+ exitCode: 1,
3410
+ message: '[evolver-proxy] self-update: bootstrap attempt recovery state is invalid; '
3411
+ + 'exiting without replacing it because prior process ownership cannot be classified safely. '
3412
+ + 'Inspect the lifecycle state before retrying.',
3413
+ };
3414
+ }
3415
+ if (reason === 'bootstrap_intent_pending') {
3416
+ const recovery = recoverBootstrapRegistrationIntent({
3417
+ env: bootstrapEnv,
3418
+ platform,
3419
+ ...options,
249
3420
  });
250
- if (migration.outcome === 'migrated') {
251
- return { handedOver: true, message: migration.message };
3421
+ if (recovery.status === 'committed') {
3422
+ try {
3423
+ resolveSelfUpdateTarget({ env: bootstrapEnv, processExecPath: options.execPath });
3424
+ return {
3425
+ disposition: 'handoff',
3426
+ handedOver: true,
3427
+ exitCode: 0,
3428
+ message: '[evolver-proxy] self-update: recovered a committed lifecycle bootstrap registration; '
3429
+ + 'handing over to the service manager without creating another child.',
3430
+ };
3431
+ }
3432
+ catch {
3433
+ // Exact intent recovery retired the old no-replace authority. An npm/JS foreground
3434
+ // still cannot prove that the committed launcher binds a standalone executable.
3435
+ return migrateUnbindableInstall('bootstrap_attempt_pending');
3436
+ }
3437
+ }
3438
+ if (recovery.status === 'safe_terminal') {
3439
+ return {
3440
+ disposition: 'continue',
3441
+ handedOver: false,
3442
+ message: '[evolver-proxy] self-update: recovered and cleared a bootstrap registration '
3443
+ + `${recovery.outcome} receipt without creating another child; continuing degraded.`,
3444
+ };
252
3445
  }
253
3446
  return {
3447
+ disposition: 'fail_closed',
254
3448
  handedOver: false,
255
- message: '[evolver-proxy] self-update: running from the npm/JS install shape, which has no standalone '
256
- + 'binary target for self-update; bootstrap skipped, continuing with self-update off. '
257
- + 'Install the standalone binary from GitHub Releases and start it to enable self-update. '
258
- + `(${migration.message})`,
3449
+ exitCode: 1,
3450
+ message: '[evolver-proxy] self-update: prior bootstrap registration ownership remains unresolved; '
3451
+ + 'exiting without migration, target mutation, or child creation. '
3452
+ + `(${boundedBootstrapDetail(recovery.detail)})`,
259
3453
  };
260
3454
  }
3455
+ if (reason === 'unsupported_install_shape'
3456
+ || reason === 'migration_ambiguous'
3457
+ || reason === 'bootstrap_attempt_pending') {
3458
+ // Do not suggest `evolver lifecycle bootstrap` here: under the npm/JS install shape it
3459
+ // would register a supervised service that crashes at self-update target resolution on
3460
+ // every startup (crash-loop). Only a standalone release binary can host self-update —
3461
+ // so attempt the one-time migration to it; any skip/failure keeps the degraded startup.
3462
+ return migrateUnbindableInstall(reason);
3463
+ }
3464
+ if (reason !== 'recent_failure') {
3465
+ recordBootstrapAttempt(bootstrapEnv, { ok: false, reason }, options);
3466
+ }
261
3467
  return {
3468
+ disposition: 'continue',
262
3469
  handedOver: false,
263
3470
  message: '[evolver-proxy] self-update: default auto requires a durable supervisor attestation; '
264
3471
  + `running with self-update off (bootstrap skipped: ${reason}). `
265
3472
  + 'Run `evolver lifecycle bootstrap` or `evolver lifecycle install-service` to enable.',
266
3473
  };
267
3474
  }
268
- const outcome = await runBootstrap({ env: bootstrapEnv, platform, ...options });
269
- recordBootstrapAttempt(bootstrapEnv, outcome, options);
3475
+ const outcome = await runBootstrapWithRegistrationIntent({ env: bootstrapEnv, platform, ...options });
3476
+ const recoveringPendingOwnership = decision.reason === 'migration_ambiguous'
3477
+ || decision.reason === 'bootstrap_attempt_pending';
3478
+ if (!recoveringPendingOwnership || outcome.ok) {
3479
+ recordBootstrapAttempt(bootstrapEnv, outcome, options);
3480
+ }
270
3481
  if (outcome.ok) {
3482
+ const lockReleaseWarning = outcome.reason === 'bootstrapped_lock_release_unconfirmed'
3483
+ ? ' The lifecycle commit is durable, but lock release could not be confirmed'
3484
+ + ` (${boundedBootstrapDetail(outcome.detail ?? 'bootstrap lifecycle lock release is unconfirmed')}).`
3485
+ + ' Inspect the lifecycle state before the next lifecycle mutation.'
3486
+ : '';
271
3487
  return {
3488
+ disposition: 'handoff',
272
3489
  handedOver: true,
3490
+ exitCode: 0,
273
3491
  message: '[evolver-proxy] self-update: registered durable service supervision via `evolver lifecycle bootstrap`; '
274
- + 'handing over to the service manager and exiting so it can take the IPC port.',
3492
+ + 'handing over to the service manager and exiting so it can take the IPC port.'
3493
+ + lockReleaseWarning,
3494
+ };
3495
+ }
3496
+ if (outcome.requiresForegroundExit) {
3497
+ const termination = outcome.reason === 'termination_unconfirmed'
3498
+ ? 'process-tree termination could not be confirmed'
3499
+ : `the child result was ${outcome.reason} and did not prove a clean rollback`;
3500
+ return {
3501
+ disposition: 'fail_closed',
3502
+ handedOver: false,
3503
+ exitCode: 1,
3504
+ message: `[evolver-proxy] self-update: bootstrap ${termination}; exiting the foreground proxy to avoid concurrent IPC ownership. `
3505
+ + 'Inspect the lifecycle bootstrap transaction state before retrying.',
3506
+ };
3507
+ }
3508
+ if (recoveringPendingOwnership) {
3509
+ return {
3510
+ disposition: 'fail_closed',
3511
+ handedOver: false,
3512
+ exitCode: 1,
3513
+ message: '[evolver-proxy] self-update: prior standalone migration ownership remains unresolved; '
3514
+ + 'exiting the foreground proxy without replacing the ambiguity marker. '
3515
+ + 'Inspect the lifecycle bootstrap transaction state before retrying.',
275
3516
  };
276
3517
  }
277
3518
  return {
3519
+ disposition: 'continue',
278
3520
  handedOver: false,
279
3521
  message: `[evolver-proxy] self-update: first-run bootstrap failed (${outcome.reason}); `
280
3522
  + 'running with self-update off. Run `evolver lifecycle install-service` manually to enable.',