@evomap/evolver-proxy 2.0.0-beta.19 → 2.0.0-beta.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/evolver-proxy.d.ts +12 -0
- package/dist/bin/evolver-proxy.js +257 -56
- package/dist/daemon/proxyDaemon.d.ts +12 -0
- package/dist/daemon/proxyDaemon.js +313 -18
- package/dist/daemon/systemdNotifier.d.ts +2 -0
- package/dist/daemon/systemdNotifier.js +11 -1
- package/dist/llm/upstream.js +54 -7
- package/dist/private/accountAssetCompatibility.d.ts +1 -0
- package/dist/private/accountAssetCompatibility.js +3 -3
- package/dist/private/adapterLoader.js +4 -3
- package/dist/router/messagesRoute.js +9 -3
- package/dist/router/providerRoutes.js +7 -3
- package/dist/selfUpdate/bootstrap.d.ts +162 -0
- package/dist/selfUpdate/bootstrap.js +3524 -0
- package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
- package/dist/selfUpdate/bootstrapReadiness.js +153 -0
- package/dist/selfUpdate/builtinKey.d.ts +4 -0
- package/dist/selfUpdate/builtinKey.js +16 -0
- package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
- package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
- package/dist/selfUpdate/executor.d.ts +18 -7
- package/dist/selfUpdate/executor.js +159 -59
- package/dist/selfUpdate/failureCodes.d.ts +4 -0
- package/dist/selfUpdate/failureCodes.js +7 -0
- package/dist/selfUpdate/index.d.ts +2 -1
- package/dist/selfUpdate/index.js +2 -1
- package/dist/selfUpdate/migration.d.ts +158 -0
- package/dist/selfUpdate/migration.js +2672 -0
- package/dist/selfUpdate/policy.d.ts +19 -2
- package/dist/selfUpdate/policy.js +76 -2
- package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
- package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
- package/dist/selfUpdate/releaseBinary.d.ts +3 -0
- package/dist/selfUpdate/releaseBinary.js +50 -4
- package/dist/selfUpdate/transaction.d.ts +8 -0
- package/dist/selfUpdate/transaction.js +166 -18
- package/dist/selfUpdate/unixController.d.ts +8 -0
- package/dist/selfUpdate/unixController.js +366 -38
- package/dist/selfUpdate/windowsController.d.ts +14 -2
- package/dist/selfUpdate/windowsController.js +484 -103
- package/dist/selfUpdate/windowsUpdater.d.ts +25 -0
- package/dist/selfUpdate/windowsUpdater.js +174 -7
- 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:
|
|
35
|
+
* Execute a force_update directive end to end: fast exits → lease → resolve/decide → download → VERIFY → replace → restart.
|
|
36
36
|
*
|
|
37
37
|
* Order is load-bearing:
|
|
38
|
-
* 1. policy off → do nothing (
|
|
39
|
-
* 2.
|
|
40
|
-
* 3. mutex: exactly one
|
|
41
|
-
* 4.
|
|
42
|
-
* 5.
|
|
43
|
-
* 6.
|
|
38
|
+
* 1. policy off → do nothing (explicit opt-out; auto is hard-gated upstream by supervisor + public key).
|
|
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
|
|
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
|
|
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
|
|
72
|
+
return finish({ outcome: 'noop', reason: 'already_satisfied', targetVersion: requiredFloor });
|
|
67
73
|
}
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
77
|
+
return finish({ outcome: 'already_in_progress', reason: 'another_update_in_flight' });
|
|
104
78
|
}
|
|
105
79
|
inFlight = true;
|
|
106
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
232
|
-
|
|
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;
|
|
@@ -23,6 +23,10 @@ export declare const SELF_UPDATE_FAILURE_CODES: Readonly<{
|
|
|
23
23
|
readonly RESTART_FAILED: "restart_failed";
|
|
24
24
|
readonly READ_BACK_FAILED: "read_back_failed";
|
|
25
25
|
readonly ROLLBACK_FAILED: "rollback_failed";
|
|
26
|
+
readonly MIGRATION_DOWNLOAD_FAILED: "migration_download_failed";
|
|
27
|
+
readonly MIGRATION_VERIFY_FAILED: "migration_verify_failed";
|
|
28
|
+
readonly MIGRATION_INSTALL_FAILED: "migration_install_failed";
|
|
29
|
+
readonly MIGRATION_REGISTER_FAILED: "migration_register_failed";
|
|
26
30
|
}>;
|
|
27
31
|
export type SelfUpdateFailureCode = typeof SELF_UPDATE_FAILURE_CODES[keyof typeof SELF_UPDATE_FAILURE_CODES];
|
|
28
32
|
export interface ClassifiedSelfUpdateError {
|
|
@@ -28,6 +28,13 @@ export const SELF_UPDATE_FAILURE_CODES = Object.freeze({
|
|
|
28
28
|
RESTART_FAILED: 'restart_failed',
|
|
29
29
|
READ_BACK_FAILED: 'read_back_failed',
|
|
30
30
|
ROLLBACK_FAILED: 'rollback_failed',
|
|
31
|
+
// One-time npm/JS → standalone binary migration (migration.ts). Append-only:
|
|
32
|
+
// these surface in attempt-marker detail strings and operator messages so
|
|
33
|
+
// telemetry can attribute first-run migration failures by phase.
|
|
34
|
+
MIGRATION_DOWNLOAD_FAILED: 'migration_download_failed',
|
|
35
|
+
MIGRATION_VERIFY_FAILED: 'migration_verify_failed',
|
|
36
|
+
MIGRATION_INSTALL_FAILED: 'migration_install_failed',
|
|
37
|
+
MIGRATION_REGISTER_FAILED: 'migration_register_failed',
|
|
31
38
|
});
|
|
32
39
|
export class SelfUpdateFailureError extends Error {
|
|
33
40
|
failureCode;
|
|
@@ -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';
|
package/dist/selfUpdate/index.js
CHANGED
|
@@ -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';
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { type spawn } from 'node:child_process';
|
|
2
|
+
import { ops } from '@evomap/evolver-core';
|
|
3
|
+
import type { DownloadResult, ForceUpdateDirective } from './executor.js';
|
|
4
|
+
import { type ReleaseBinaryOptions } from './releaseBinary.js';
|
|
5
|
+
import { type StagedBinaryProbe } from './transaction.js';
|
|
6
|
+
type DownloadedArtifact = ops.DownloadedArtifact;
|
|
7
|
+
type VerifyResult = ops.VerifyResult;
|
|
8
|
+
type FetchFn = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
9
|
+
/** Minimal stat shape migration needs (symlink guard + regular-file check). */
|
|
10
|
+
interface MigrationFileStat {
|
|
11
|
+
isFile(): boolean;
|
|
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;
|
|
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>;
|
|
60
|
+
export interface MigrationOptions {
|
|
61
|
+
/** Arch override for release asset resolution (defaults to process.arch). */
|
|
62
|
+
arch?: NodeJS.Architecture;
|
|
63
|
+
exists?: (path: string) => boolean;
|
|
64
|
+
/** Sync text read (bootstrap.ts mirror); used for container detection. */
|
|
65
|
+
readFile?: (path: string) => string;
|
|
66
|
+
/** Sync text write seam for attempt markers and canonical migration state. */
|
|
67
|
+
writeFile?: (path: string, content: string) => void;
|
|
68
|
+
/** Async binary read of the staged artifact (install copy). */
|
|
69
|
+
readBinary?: (path: string) => Promise<Buffer>;
|
|
70
|
+
/** Async binary write with mode (install tmp copy). */
|
|
71
|
+
writeBinary?: (path: string, content: Buffer, mode: number) => Promise<void>;
|
|
72
|
+
/** Recursive mkdir with mode (install dest dir). */
|
|
73
|
+
mkdir?: (path: string, mode: number) => Promise<void>;
|
|
74
|
+
/** Force/recursive removal (staged tmp dir, leftover tmp copies). */
|
|
75
|
+
rm?: (path: 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>;
|
|
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>;
|
|
85
|
+
/** lstat-shaped stat for the staged-artifact symlink guard. */
|
|
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>;
|
|
91
|
+
fetchFn?: FetchFn;
|
|
92
|
+
/** Probe used by the default preflight (real execution of `--version` / `proxy --help`). */
|
|
93
|
+
probe?: StagedBinaryProbe;
|
|
94
|
+
spawnFn?: typeof spawn;
|
|
95
|
+
now?: number;
|
|
96
|
+
/** Clock used to bind the registration runner to absolute deadlines. */
|
|
97
|
+
clock?: () => number;
|
|
98
|
+
execPath?: string;
|
|
99
|
+
/** Effective uid (tests / platforms without process.getuid). */
|
|
100
|
+
uid?: number | undefined;
|
|
101
|
+
/** Parent register timeout; must exceed transactionBudgetMs. */
|
|
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;
|
|
115
|
+
/** High-level seam: download leg (defaults to downloadGithubReleaseArtifact). */
|
|
116
|
+
downloadFn?: (targetVersion: string, directive: ForceUpdateDirective, opts: ReleaseBinaryOptions) => Promise<DownloadResult>;
|
|
117
|
+
/** High-level seam: manifest verification (defaults to ops.verifySelectedManifestArtifact). */
|
|
118
|
+
verifyFn?: (manifest: unknown, downloaded: readonly DownloadedArtifact[], publicKey: string) => VerifyResult;
|
|
119
|
+
/** High-level seam: preflight (defaults to preflightManagedStagedBinary with options.probe). */
|
|
120
|
+
preflightFn?: (targetPath: string, expectedVersion: string) => Promise<void>;
|
|
121
|
+
}
|
|
122
|
+
export interface MigrationResult {
|
|
123
|
+
outcome: 'migrated' | 'skipped' | 'failed';
|
|
124
|
+
/**
|
|
125
|
+
* Structured reason: 'migrated' / 'disabled' / 'root_user' / 'ci_environment' /
|
|
126
|
+
* 'container_environment' / 'cooldown' / 'unsupported_platform' /
|
|
127
|
+
* 'invalid_version_override' / 'version_unresolvable' /
|
|
128
|
+
* 'migration_download_failed:<detail>' / 'migration_verify_failed:<reason>' /
|
|
129
|
+
* 'migration_install_failed:<detail>' / 'migration_register_failed:<detail>' /
|
|
130
|
+
* 'migration_register_timeout'.
|
|
131
|
+
*/
|
|
132
|
+
reason: string;
|
|
133
|
+
destPath?: string;
|
|
134
|
+
/** Registration ownership is ambiguous, so the foreground proxy must not continue. */
|
|
135
|
+
requiresForegroundExit?: true;
|
|
136
|
+
/** Operator-facing message (short phrase for skipped/failed; full line for migrated). */
|
|
137
|
+
message: string;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Migration install home — mirrors the CLI-side lifecyclePaths home resolution
|
|
141
|
+
* (kept dependency-free across packages): EVOLVER_HOME ?? EVOMAP_HOME ?? ~/.evomap.
|
|
142
|
+
*/
|
|
143
|
+
export declare function resolveMigrationHome(env: NodeJS.ProcessEnv): string;
|
|
144
|
+
/** Migration install path for this platform: <home>/bin/<releaseAssetName>. Throws on unsupported platforms. */
|
|
145
|
+
export declare function resolveMigrationDestPath(env: NodeJS.ProcessEnv, platform?: NodeJS.Platform, arch?: NodeJS.Architecture): string;
|
|
146
|
+
/**
|
|
147
|
+
* Resolve the migration target version: EVOLVER_BOOTSTRAP_MIGRATION_VERSION override
|
|
148
|
+
* (normalized through the self-update version contract) wins, else the current package
|
|
149
|
+
* version. Returns undefined when nothing normalizes to a concrete semver.
|
|
150
|
+
*/
|
|
151
|
+
export declare function resolveMigrationVersion(env: NodeJS.ProcessEnv): string | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* One-time migration of the npm/JS install shape to the standalone release binary.
|
|
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.
|
|
156
|
+
*/
|
|
157
|
+
export declare function migrateToStandaloneBinary(env: NodeJS.ProcessEnv, platform?: NodeJS.Platform, options?: MigrationOptions): Promise<MigrationResult>;
|
|
158
|
+
export {};
|