@evomap/evolver-proxy 2.0.2 → 2.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/bin/evolver-proxy.d.ts +12 -0
  2. package/dist/bin/evolver-proxy.js +192 -59
  3. package/dist/daemon/proxyDaemon.js +10 -0
  4. package/dist/daemon/systemdNotifier.d.ts +2 -0
  5. package/dist/daemon/systemdNotifier.js +11 -1
  6. package/dist/lifecycle/claimNudge.js +1 -1
  7. package/dist/lifecycle/deployGuard.js +1 -1
  8. package/dist/lifecycle/legacyNodeId.js +1 -1
  9. package/dist/lifecycle/manager.js +1 -1
  10. package/dist/llm/bodyCapture.js +1 -1
  11. package/dist/llm/index.js +1 -1
  12. package/dist/llm/server.js +1 -1
  13. package/dist/llm/traceBackfill.js +1 -1
  14. package/dist/llm/traceConfig.js +1 -1
  15. package/dist/llm/traceControl.js +1 -1
  16. package/dist/llm/traceEnvelope.js +1 -1
  17. package/dist/llm/traceSink.js +1 -1
  18. package/dist/llm/traceUploadPayload.js +1 -1
  19. package/dist/llm/upstream.js +1 -1
  20. package/dist/router/cachePassthrough.js +1 -1
  21. package/dist/router/features.js +1 -1
  22. package/dist/router/index.js +1 -1
  23. package/dist/router/messagesRoute.js +1 -1
  24. package/dist/router/modelRouter.js +1 -1
  25. package/dist/router/providerRoutes.js +1 -1
  26. package/dist/router/sseScan.js +1 -1
  27. package/dist/selfUpdate/bootstrap.d.ts +106 -13
  28. package/dist/selfUpdate/bootstrap.js +3418 -176
  29. package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
  30. package/dist/selfUpdate/bootstrapReadiness.js +153 -0
  31. package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
  32. package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
  33. package/dist/selfUpdate/executor.d.ts +17 -6
  34. package/dist/selfUpdate/executor.js +158 -58
  35. package/dist/selfUpdate/index.d.ts +2 -1
  36. package/dist/selfUpdate/index.js +2 -1
  37. package/dist/selfUpdate/migration.d.ts +80 -15
  38. package/dist/selfUpdate/migration.js +2513 -156
  39. package/dist/selfUpdate/policy.js +2 -8
  40. package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
  41. package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
  42. package/dist/selfUpdate/releaseBinary.d.ts +3 -0
  43. package/dist/selfUpdate/releaseBinary.js +46 -3
  44. package/dist/selfUpdate/transaction.d.ts +8 -0
  45. package/dist/selfUpdate/transaction.js +166 -18
  46. package/dist/selfUpdate/unixController.d.ts +8 -0
  47. package/dist/selfUpdate/unixController.js +364 -38
  48. package/dist/selfUpdate/windowsController.d.ts +14 -2
  49. package/dist/selfUpdate/windowsController.js +482 -103
  50. package/dist/selfUpdate/windowsUpdater.d.ts +25 -0
  51. package/dist/selfUpdate/windowsUpdater.js +174 -7
  52. package/dist/sync/engine.js +1 -1
  53. package/package.json +3 -3
@@ -12,7 +12,7 @@
12
12
  // Concurrency: a process-level mutex guarantees that concurrent force_update messages execute the update EXACTLY
13
13
  // once. The second caller short-circuits with `already_in_progress` and performs no I/O.
14
14
  import { ops } from '@evomap/evolver-core';
15
- import { SELF_UPDATE_FAILURE_CODES, classifySelfUpdateError, codeForDecisionReject, } from './failureCodes.js';
15
+ import { SELF_UPDATE_FAILURE_CODES, classifySelfUpdateError, codeForDecisionReject, selfUpdateFailure, } from './failureCodes.js';
16
16
  const { decideUpdate, verifySelectedManifestArtifact } = ops;
17
17
  // Process-level mutex. Module scope is correct: there is one daemon per process, and v1's _forceUpdateInFlight had
18
18
  // the same lifetime. Guards against two force_update envelopes (or a heartbeat-driven + mailbox-driven trigger)
@@ -32,22 +32,28 @@ function report(deps, result) {
32
32
  return result;
33
33
  }
34
34
  /**
35
- * Execute a force_update directive end to end: decide(mutex) → download → VERIFY → atomic replace → restart.
35
+ * Execute a force_update directive end to end: fast exits leaseresolve/decide → download → VERIFY → replace → restart.
36
36
  *
37
37
  * Order is load-bearing:
38
38
  * 1. policy off → do nothing (explicit opt-out; auto is hard-gated upstream by supervisor + public key).
39
- * 2. decideUpdate (pure): reject bad manifests, NOOP when already satisfied (no download, no restart).
40
- * 3. mutex: exactly one execution; concurrent callers get `already_in_progress` and touch no disk.
41
- * 4. download the staged release.
42
- * 5. verifySelectedManifestArtifact (pure) THE GATE. Fail → no write, no restart.
43
- * 6. atomicReplace, then restart(). Only reached after verification passed.
39
+ * 2. reject malformed/already-satisfied required versions without acquiring the lifecycle owner lease.
40
+ * 3. mutex + lifecycle owner lease: exactly one executor may resolve or mutate release state.
41
+ * 4. resolve and decideUpdate (pure): reject bad manifests or NOOP under the held lease.
42
+ * 5. download the staged release.
43
+ * 6. verifySelectedManifestArtifact (pure) — THE GATE. Fail no write, no restart.
44
+ * 7. atomicReplace, then restart(). Only reached after verification passed.
44
45
  *
45
46
  * Never throws: every failure becomes a structured SelfUpdateResult so the daemon can report it and keep running
46
47
  * on the old (intact) version.
47
48
  */
48
49
  export async function executeForceUpdate(directive, deps) {
50
+ let selectedResult;
51
+ const finish = (result) => {
52
+ selectedResult = result;
53
+ return report(deps, result);
54
+ };
49
55
  if (deps.policy !== 'auto') {
50
- return report(deps, {
56
+ return finish({
51
57
  outcome: 'disabled',
52
58
  reason: deps.policy === 'prompt' ? 'self_update_policy_prompt_requires_approval' : 'self_update_policy_off',
53
59
  });
@@ -56,63 +62,105 @@ export async function executeForceUpdate(directive, deps) {
56
62
  ? ops.normalizeRequiredVersion(directive.required_version)
57
63
  : undefined;
58
64
  if (directive.required_version !== undefined && !requiredFloor) {
59
- return report(deps, {
65
+ return finish({
60
66
  outcome: 'rejected_decision',
61
67
  reason: 'required_version_invalid',
62
68
  failureCode: SELF_UPDATE_FAILURE_CODES.BAD_REQUIRED_VERSION,
63
69
  });
64
70
  }
65
71
  if (requiredFloor && ops.currentSatisfiesRequiredVersion(deps.currentVersion, requiredFloor)) {
66
- return report(deps, { outcome: 'noop', reason: 'already_satisfied', targetVersion: requiredFloor });
72
+ return finish({ outcome: 'noop', reason: 'already_satisfied', targetVersion: requiredFloor });
67
73
  }
68
- let manifest = directive.manifest;
69
- if (deps.resolveManifest && shouldResolveManifest(directive, manifest, Boolean(deps.publicKey))) {
70
- try {
71
- manifest = await deps.resolveManifest(directive, deps.currentVersion);
72
- }
73
- catch (err) {
74
- const targetVersion = ops.normalizeRequiredVersion(directive.required_version);
75
- const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED);
76
- return report(deps, {
77
- outcome: 'download_failed',
78
- reason: `manifest_resolve_failed: ${classified.detail}`,
79
- failureCode: classified.failureCode,
80
- ...(targetVersion ? { targetVersion } : {}),
81
- });
82
- }
83
- }
84
- const effectiveDirective = { ...directive, manifest };
85
- const decision = decideUpdate({
86
- current: deps.currentVersion,
87
- ...(requiredFloor ? { required: requiredFloor } : {}),
88
- manifest,
89
- });
90
- if (decision.action === 'reject') {
91
- return report(deps, {
92
- outcome: 'rejected_decision',
93
- reason: decision.reason,
94
- failureCode: codeForDecisionReject(decision.reason),
95
- ...(decision.targetVersion ? { targetVersion: decision.targetVersion } : {}),
96
- });
97
- }
98
- if (decision.action === 'noop') {
99
- return report(deps, { outcome: 'noop', reason: decision.reason, ...(decision.targetVersion ? { targetVersion: decision.targetVersion } : {}) });
100
- }
101
- // action === 'proceed'. Take the mutex; a concurrent force_update short-circuits here with NO I/O.
74
+ // The mutex and lifecycle lease precede manifest resolution, which is release I/O too.
75
+ // Policy, malformed required-version, and already-satisfied fast exits remain lock-free.
102
76
  if (inFlight) {
103
- return report(deps, { outcome: 'already_in_progress', reason: 'another_update_in_flight' });
77
+ return finish({ outcome: 'already_in_progress', reason: 'another_update_in_flight' });
104
78
  }
105
79
  inFlight = true;
106
- const targetVersion = decision.targetVersion ?? manifest?.version ?? '';
80
+ let targetVersion = requiredFloor ?? '';
81
+ let lifecycleLease;
107
82
  let transaction;
83
+ const assertLifecycleLease = async () => {
84
+ try {
85
+ await lifecycleLease?.assertOwned();
86
+ }
87
+ catch (error) {
88
+ const classified = classifySelfUpdateError(error, SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED);
89
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED, `self_update_lifecycle_owner_lease_lost:${classified.detail}`, { cause: error });
90
+ }
91
+ };
108
92
  try {
93
+ if (deps.acquireLifecycleLease) {
94
+ try {
95
+ lifecycleLease = await deps.acquireLifecycleLease();
96
+ await assertLifecycleLease();
97
+ }
98
+ catch (err) {
99
+ const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.UPDATE_LOCKED);
100
+ return finish({
101
+ outcome: classified.failureCode === SELF_UPDATE_FAILURE_CODES.UPDATE_LOCKED
102
+ ? 'already_in_progress'
103
+ : 'replace_failed',
104
+ reason: classified.detail,
105
+ failureCode: classified.failureCode,
106
+ ...(targetVersion ? { targetVersion } : {}),
107
+ });
108
+ }
109
+ }
110
+ let manifest = directive.manifest;
111
+ if (deps.resolveManifest && shouldResolveManifest(directive, manifest, Boolean(deps.publicKey))) {
112
+ try {
113
+ await assertLifecycleLease();
114
+ manifest = await deps.resolveManifest(directive, deps.currentVersion);
115
+ }
116
+ catch (err) {
117
+ const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED);
118
+ if (classified.failureCode === SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED) {
119
+ return finish({
120
+ outcome: 'replace_failed',
121
+ reason: classified.detail,
122
+ failureCode: classified.failureCode,
123
+ ...(targetVersion ? { targetVersion } : {}),
124
+ });
125
+ }
126
+ return finish({
127
+ outcome: 'download_failed',
128
+ reason: `manifest_resolve_failed: ${classified.detail}`,
129
+ failureCode: classified.failureCode,
130
+ ...(targetVersion ? { targetVersion } : {}),
131
+ });
132
+ }
133
+ }
134
+ const effectiveDirective = { ...directive, manifest };
135
+ const decision = decideUpdate({
136
+ current: deps.currentVersion,
137
+ ...(requiredFloor ? { required: requiredFloor } : {}),
138
+ manifest,
139
+ });
140
+ if (decision.action === 'reject') {
141
+ return finish({
142
+ outcome: 'rejected_decision',
143
+ reason: decision.reason,
144
+ failureCode: codeForDecisionReject(decision.reason),
145
+ ...(decision.targetVersion ? { targetVersion: decision.targetVersion } : {}),
146
+ });
147
+ }
148
+ if (decision.action === 'noop') {
149
+ return finish({
150
+ outcome: 'noop',
151
+ reason: decision.reason,
152
+ ...(decision.targetVersion ? { targetVersion: decision.targetVersion } : {}),
153
+ });
154
+ }
155
+ targetVersion = decision.targetVersion ?? manifest?.version ?? '';
109
156
  if (deps.beginTransaction) {
110
157
  try {
158
+ await assertLifecycleLease();
111
159
  transaction = await deps.beginTransaction(targetVersion);
112
160
  }
113
161
  catch (err) {
114
162
  const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.UPDATE_LOCKED);
115
- return report(deps, {
163
+ return finish({
116
164
  outcome: classified.failureCode === SELF_UPDATE_FAILURE_CODES.UPDATE_LOCKED ? 'already_in_progress' : 'replace_failed',
117
165
  reason: classified.detail,
118
166
  failureCode: classified.failureCode,
@@ -123,12 +171,21 @@ export async function executeForceUpdate(directive, deps) {
123
171
  // 4. Download the staged release.
124
172
  let dl;
125
173
  try {
174
+ await assertLifecycleLease();
126
175
  dl = await deps.download(targetVersion, effectiveDirective);
127
176
  }
128
177
  catch (err) {
129
- await transaction?.abort(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED).catch(() => { });
130
178
  const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED);
131
- return report(deps, {
179
+ await transaction?.abort(classified.failureCode).catch(() => { });
180
+ if (classified.failureCode === SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED) {
181
+ return finish({
182
+ outcome: 'replace_failed',
183
+ reason: classified.detail,
184
+ failureCode: classified.failureCode,
185
+ targetVersion,
186
+ });
187
+ }
188
+ return finish({
132
189
  outcome: 'download_failed',
133
190
  reason: classified.detail,
134
191
  failureCode: classified.failureCode,
@@ -137,12 +194,13 @@ export async function executeForceUpdate(directive, deps) {
137
194
  }
138
195
  if (transaction) {
139
196
  try {
197
+ await assertLifecycleLease();
140
198
  dl = await transaction.adoptDownloaded(dl);
141
199
  }
142
200
  catch (err) {
143
201
  await transaction.abort(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED).catch(() => { });
144
202
  const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED);
145
- return report(deps, {
203
+ return finish({
146
204
  outcome: 'replace_failed',
147
205
  reason: classified.detail,
148
206
  failureCode: classified.failureCode,
@@ -151,11 +209,24 @@ export async function executeForceUpdate(directive, deps) {
151
209
  }
152
210
  }
153
211
  // 5. THE GATE: verify the downloaded bytes against the (optionally signed) manifest BEFORE any write.
212
+ try {
213
+ await assertLifecycleLease();
214
+ }
215
+ catch (err) {
216
+ await transaction?.abort(SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED).catch(() => { });
217
+ const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED);
218
+ return finish({
219
+ outcome: 'replace_failed',
220
+ reason: classified.detail,
221
+ failureCode: classified.failureCode,
222
+ targetVersion,
223
+ });
224
+ }
154
225
  const verification = verifySelectedManifestArtifact(manifest, dl.artifacts, ...(deps.publicKey ? [deps.publicKey] : []));
155
226
  if (!verification.ok) {
156
227
  await transaction?.abort(SELF_UPDATE_FAILURE_CODES.REJECTED_VERIFICATION).catch(() => { });
157
228
  // Verification failed → write NOTHING, restart NOTHING. Old version stays intact and runnable.
158
- return report(deps, {
229
+ return finish({
159
230
  outcome: 'rejected_verification',
160
231
  reason: verification.reason,
161
232
  failureCode: SELF_UPDATE_FAILURE_CODES.REJECTED_VERIFICATION,
@@ -163,12 +234,13 @@ export async function executeForceUpdate(directive, deps) {
163
234
  });
164
235
  }
165
236
  try {
237
+ await assertLifecycleLease();
166
238
  await transaction?.markVerified(dl.artifacts);
167
239
  }
168
240
  catch (err) {
169
241
  await transaction?.abort(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED).catch(() => { });
170
242
  const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED);
171
- return report(deps, {
243
+ return finish({
172
244
  outcome: 'replace_failed',
173
245
  reason: classified.detail,
174
246
  failureCode: classified.failureCode,
@@ -177,6 +249,7 @@ export async function executeForceUpdate(directive, deps) {
177
249
  }
178
250
  // 6. Verified. Atomic replace, then signal restart. A replace failure leaves the old version intact.
179
251
  try {
252
+ await assertLifecycleLease();
180
253
  if (transaction)
181
254
  await transaction.install();
182
255
  else
@@ -184,7 +257,7 @@ export async function executeForceUpdate(directive, deps) {
184
257
  }
185
258
  catch (err) {
186
259
  const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.COPY_FAILED);
187
- return report(deps, {
260
+ return finish({
188
261
  outcome: 'replace_failed',
189
262
  reason: classified.detail,
190
263
  failureCode: classified.failureCode,
@@ -198,9 +271,11 @@ export async function executeForceUpdate(directive, deps) {
198
271
  appliedVia: dl.appliedVia ?? 'binary',
199
272
  ...(transaction ? { confirmationPending: true } : {}),
200
273
  };
201
- report(deps, result);
274
+ finish(result);
202
275
  try {
276
+ await assertLifecycleLease();
203
277
  await transaction?.markRestartRequested();
278
+ await assertLifecycleLease();
204
279
  await deps.restart(); // v1 convention: exit(78) → supervisor relaunches the new version.
205
280
  }
206
281
  catch (err) {
@@ -210,14 +285,14 @@ export async function executeForceUpdate(directive, deps) {
210
285
  }
211
286
  catch (rollbackError) {
212
287
  const rollback = classifySelfUpdateError(rollbackError, SELF_UPDATE_FAILURE_CODES.ROLLBACK_FAILED);
213
- return report(deps, {
288
+ return finish({
214
289
  outcome: 'rollback_failed',
215
290
  reason: rollback.detail,
216
291
  failureCode: rollback.failureCode,
217
292
  targetVersion,
218
293
  });
219
294
  }
220
- return report(deps, {
295
+ return finish({
221
296
  outcome: 'restart_failed',
222
297
  reason: classified.detail,
223
298
  failureCode: classified.failureCode,
@@ -228,11 +303,36 @@ export async function executeForceUpdate(directive, deps) {
228
303
  }
229
304
  finally {
230
305
  await transaction?.release().catch(() => { });
231
- // Released so a later legitimate update (after a failed attempt) can proceed. On the success path the process
232
- // is exiting anyway; releasing is harmless and keeps the mutex honest if restart() is a test fake that returns.
306
+ if (lifecycleLease) {
307
+ try {
308
+ await lifecycleLease.release();
309
+ }
310
+ catch (error) {
311
+ if (selectedResult) {
312
+ selectedResult.cleanupWarning = boundedCleanupWarning(error);
313
+ if (selectedResult.outcome === 'applied') {
314
+ selectedResult.reason = 'verified_and_replaced_lifecycle_lease_release_unconfirmed';
315
+ }
316
+ try {
317
+ deps.onCleanupWarning?.(selectedResult.cleanupWarning, selectedResult);
318
+ }
319
+ catch {
320
+ // Cleanup reporting must not replace the primary update outcome.
321
+ }
322
+ }
323
+ }
324
+ }
233
325
  inFlight = false;
234
326
  }
235
327
  }
328
+ function boundedCleanupWarning(error) {
329
+ const raw = error instanceof Error ? error.message : String(error);
330
+ const normalized = raw
331
+ .replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu, ' ')
332
+ .replace(/\s+/g, ' ')
333
+ .trim();
334
+ return (normalized || 'self_update_lifecycle_owner_lease_release_failed').slice(0, 512);
335
+ }
236
336
  function shouldResolveManifest(directive, manifest, signatureRequired) {
237
337
  if (manifest === undefined)
238
338
  return true;
@@ -5,4 +5,5 @@ export * from './releaseBinary.js';
5
5
  export * from './transaction.js';
6
6
  export * from './failureCodes.js';
7
7
  export * from './unixController.js';
8
- export * from './windowsController.js';
8
+ export * from './windowsController.js';
9
+ export * from './bootstrapReadiness.js';
@@ -5,4 +5,5 @@ export * from './releaseBinary.js';
5
5
  export * from './transaction.js';
6
6
  export * from './failureCodes.js';
7
7
  export * from './unixController.js';
8
- export * from './windowsController.js';
8
+ export * from './windowsController.js';
9
+ export * from './bootstrapReadiness.js';
@@ -1,4 +1,4 @@
1
- import { spawn } from 'node:child_process';
1
+ import { type spawn } from 'node:child_process';
2
2
  import { ops } from '@evomap/evolver-core';
3
3
  import type { DownloadResult, ForceUpdateDirective } from './executor.js';
4
4
  import { type ReleaseBinaryOptions } from './releaseBinary.js';
@@ -6,26 +6,64 @@ import { type StagedBinaryProbe } from './transaction.js';
6
6
  type DownloadedArtifact = ops.DownloadedArtifact;
7
7
  type VerifyResult = ops.VerifyResult;
8
8
  type FetchFn = (input: string | URL, init?: RequestInit) => Promise<Response>;
9
- /** Advisory migration state marker written next to the bootstrap attempt marker. */
10
- export declare const MIGRATION_STATE_FILE = "migration.json";
11
- /**
12
- * The CLI-side service activation itself spawns up to ~60s (lifecycle powershell/schtasks
13
- * activation), so a shorter timeout would kill the child mid-activation and leave the
14
- * installed binary registered as failed. Mirrors the bootstrap timeout rationale.
15
- */
16
- export declare const MIGRATION_REGISTER_TIMEOUT_MS = 90000;
17
9
  /** Minimal stat shape migration needs (symlink guard + regular-file check). */
18
- export interface MigrationFileStat {
10
+ interface MigrationFileStat {
19
11
  isFile(): boolean;
20
12
  isSymbolicLink(): boolean;
13
+ isDirectory?(): boolean;
14
+ size?: number;
15
+ }
16
+ interface MigrationTargetIdentity {
17
+ path: string;
18
+ device: string;
19
+ inode: string;
20
+ size: number;
21
+ linkCount: 1;
22
+ mtimeNs: string;
23
+ ctimeNs: string;
24
+ sha256: string;
25
+ }
26
+ /**
27
+ * Registration is delegated to the same transaction runner used by ordinary first-run
28
+ * bootstrap. The absolute deadlines are part of this request so an adapter cannot silently
29
+ * restart either observation budget during registration and reconciliation.
30
+ */
31
+ export interface MigrationRegistrationRequest {
32
+ env: NodeJS.ProcessEnv;
33
+ platform: NodeJS.Platform;
34
+ version: string;
35
+ startedAtMs: number;
36
+ transactionDeadlineMs: number;
37
+ parentDeadlineMs: number;
38
+ transactionBudgetMs: number;
39
+ timeoutMs: number;
40
+ /** Exact signed target identity the runner must revalidate before spawning the child. */
41
+ targetIdentity: Readonly<MigrationTargetIdentity>;
42
+ /** Sealed verifier over the original identity/dependencies; call immediately before spawn. */
43
+ revalidateTarget: () => Promise<void>;
44
+ /** Sealed durable-intent verifier; call immediately before target revalidation and spawn. */
45
+ assertRegistrationIntentCurrent: () => Promise<void>;
46
+ execPath: string;
47
+ exists?: (path: string) => boolean;
48
+ readFile?: (path: string) => string;
49
+ writeFile?: (path: string, content: string) => void;
50
+ spawnFn?: typeof spawn;
21
51
  }
52
+ interface MigrationRegistrationOutcome {
53
+ ok: boolean;
54
+ reason: string;
55
+ detail?: string;
56
+ /** The runner could not prove child/process-tree ownership or a clean rollback. */
57
+ requiresForegroundExit?: true;
58
+ }
59
+ export type MigrationRegistrationRunner = (request: MigrationRegistrationRequest) => Promise<MigrationRegistrationOutcome>;
22
60
  export interface MigrationOptions {
23
61
  /** Arch override for release asset resolution (defaults to process.arch). */
24
62
  arch?: NodeJS.Architecture;
25
63
  exists?: (path: string) => boolean;
26
64
  /** Sync text read (bootstrap.ts mirror); used for container detection. */
27
65
  readFile?: (path: string) => string;
28
- /** Sync text write for advisory state markers (attempt marker + migration.json). */
66
+ /** Sync text write seam for attempt markers and canonical migration state. */
29
67
  writeFile?: (path: string, content: string) => void;
30
68
  /** Async binary read of the staged artifact (install copy). */
31
69
  readBinary?: (path: string) => Promise<Buffer>;
@@ -35,20 +73,45 @@ export interface MigrationOptions {
35
73
  mkdir?: (path: string, mode: number) => Promise<void>;
36
74
  /** Force/recursive removal (staged tmp dir, leftover tmp copies). */
37
75
  rm?: (path: string) => Promise<void>;
38
- rename?: (from: string, to: string) => Promise<void>;
76
+ /** Owner-verified installed target removal after a proven clean registration rollback. */
77
+ unlink?: (path: string) => Promise<void>;
78
+ /** Remove a transaction-created empty bin directory after clean rollback. */
79
+ rmdir?: (path: string) => Promise<void>;
39
80
  chmod?: (path: string, mode: number) => Promise<void>;
81
+ /** Flush staged executable data/metadata before namespace publication. */
82
+ syncFile?: (path: string) => Promise<void>;
83
+ /** Flush target namespace mutations; Windows may use a documented no-op fallback. */
84
+ syncDirectory?: (path: string) => Promise<void>;
40
85
  /** lstat-shaped stat for the staged-artifact symlink guard. */
41
86
  stat?: (path: string) => Promise<MigrationFileStat>;
87
+ /** Atomic no-replace publication seam. */
88
+ link?: (from: string, to: string) => Promise<void>;
89
+ /** Atomic same-directory move used by exact rollback quarantine. */
90
+ rename?: (from: string, to: string) => Promise<void>;
42
91
  fetchFn?: FetchFn;
43
92
  /** Probe used by the default preflight (real execution of `--version` / `proxy --help`). */
44
93
  probe?: StagedBinaryProbe;
45
94
  spawnFn?: typeof spawn;
46
95
  now?: number;
96
+ /** Clock used to bind the registration runner to absolute deadlines. */
97
+ clock?: () => number;
47
98
  execPath?: string;
48
99
  /** Effective uid (tests / platforms without process.getuid). */
49
100
  uid?: number | undefined;
50
- /** Register-step timeout override (defaults to MIGRATION_REGISTER_TIMEOUT_MS). */
101
+ /** Parent register timeout; must exceed transactionBudgetMs. */
51
102
  timeoutMs?: number;
103
+ /** Child transaction budget; defaults to MIGRATION_REGISTER_TRANSACTION_BUDGET_MS. */
104
+ transactionBudgetMs?: number;
105
+ /** Durable reconciliation + process-tree-contained registration runner. */
106
+ registrationRunner?: MigrationRegistrationRunner;
107
+ /** Test seam for platform ownership/ACL validation; production uses strict host checks. */
108
+ assertDirectoryTrust?: (directory: string) => void | Promise<void>;
109
+ /** Test seam for executable ownership/ACL validation; production uses strict host checks. */
110
+ assertFileTrust?: (path: string) => void | Promise<void>;
111
+ /** Sync test seam for canonical state-directory ownership/ACL validation. */
112
+ assertStateDirectoryTrust?: (directory: string) => void;
113
+ /** Sync test seam for canonical state-file ownership/ACL validation. */
114
+ assertStateFileTrust?: (path: string) => void;
52
115
  /** High-level seam: download leg (defaults to downloadGithubReleaseArtifact). */
53
116
  downloadFn?: (targetVersion: string, directive: ForceUpdateDirective, opts: ReleaseBinaryOptions) => Promise<DownloadResult>;
54
117
  /** High-level seam: manifest verification (defaults to ops.verifySelectedManifestArtifact). */
@@ -68,6 +131,8 @@ export interface MigrationResult {
68
131
  */
69
132
  reason: string;
70
133
  destPath?: string;
134
+ /** Registration ownership is ambiguous, so the foreground proxy must not continue. */
135
+ requiresForegroundExit?: true;
71
136
  /** Operator-facing message (short phrase for skipped/failed; full line for migrated). */
72
137
  message: string;
73
138
  }
@@ -86,8 +151,8 @@ export declare function resolveMigrationDestPath(env: NodeJS.ProcessEnv, platfor
86
151
  export declare function resolveMigrationVersion(env: NodeJS.ProcessEnv): string | undefined;
87
152
  /**
88
153
  * One-time migration of the npm/JS install shape to the standalone release binary.
89
- * Never throws: every failure/skip becomes a structured MigrationResult so degraded
90
- * startup can keep running with self-update off.
154
+ * Never throws: every failure/skip becomes a structured MigrationResult. Only a proven clean
155
+ * failure may continue degraded; ambiguous registration ownership requires foreground exit.
91
156
  */
92
157
  export declare function migrateToStandaloneBinary(env: NodeJS.ProcessEnv, platform?: NodeJS.Platform, options?: MigrationOptions): Promise<MigrationResult>;
93
158
  export {};