codex-overleaf-link 2.5.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -26
- package/README.zh-CN.md +39 -27
- package/extension/bootstrap/background.js +52 -9
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/bootstrap/popup.html +1 -0
- package/extension/bootstrap/popupFallback.js +55 -0
- package/extension/bootstrap/updateStatus.js +1 -0
- package/extension/src/background.js +206 -53
- package/extension/src/backgroundBootstrapMigration.js +181 -0
- package/extension/src/backgroundSharedSessions.js +74 -14
- package/extension/src/backgroundUpdateCoordinator.js +118 -21
- package/extension/src/backgroundUpdateSafety.js +173 -0
- package/extension/src/content/activeTurnControl.js +8 -2
- package/extension/src/content/applyResultFormatters.js +46 -73
- package/extension/src/content/assetTransferBroker.js +72 -14
- package/extension/src/content/composerAttachments.js +93 -45
- package/extension/src/content/composerPanel.js +18 -0
- package/extension/src/content/contentRuntime.js +661 -676
- package/extension/src/content/diagnosticsController.js +122 -96
- package/extension/src/content/diagnosticsExport.js +19 -5
- package/extension/src/content/diagnosticsPanel.js +30 -0
- package/extension/src/content/directoryCleanupRecovery.js +54 -0
- package/extension/src/content/generated/content.bundle.js +6879 -2786
- package/extension/src/content/generated/content.bundle.meta.json +11 -3
- package/extension/src/content/globalPreferencesController.js +1 -1
- package/extension/src/content/legacyGlobalRegistry.js +6 -0
- package/extension/src/content/localSkillsPanel.js +102 -12
- package/extension/src/content/markdownText.js +5 -6
- package/extension/src/content/modelPicker.js +7 -4
- package/extension/src/content/nativeCompatibilityController.js +45 -20
- package/extension/src/content/otWarmMirror.js +4 -5
- package/extension/src/content/otWarmMirrorController.js +17 -20
- package/extension/src/content/pageBridgeClient.js +15 -2
- package/extension/src/content/panelMaintenance.js +40 -12
- package/extension/src/content/panelRenderer.js +6 -0
- package/extension/src/content/projectProviderSelection.js +13 -4
- package/extension/src/content/projectSessionCleanup.js +109 -62
- package/extension/src/content/projectSettingsCoordinator.js +39 -2
- package/extension/src/content/providerSettingsCoordinator.js +46 -8
- package/extension/src/content/providerSettingsDialog.js +71 -13
- package/extension/src/content/recentProjects.js +11 -5
- package/extension/src/content/runActivitySummary.js +40 -6
- package/extension/src/content/runCompletionMeta.js +344 -0
- package/extension/src/content/runController.js +37 -14
- package/extension/src/content/runFailureNotice.js +21 -12
- package/extension/src/content/runGuidanceController.js +72 -27
- package/extension/src/content/runGuidanceView.js +59 -10
- package/extension/src/content/runQueueScheduler.js +148 -42
- package/extension/src/content/runRecoveryActions.js +146 -0
- package/extension/src/content/runResultActions.js +8 -125
- package/extension/src/content/runReviewController.js +574 -0
- package/extension/src/content/runScrollLayout.js +21 -2
- package/extension/src/content/runTimelineView.js +146 -153
- package/extension/src/content/scopedPersistencePanelState.js +51 -6
- package/extension/src/content/selectionContextView.js +1 -1
- package/extension/src/content/sensitivePreflight.js +140 -0
- package/extension/src/content/sessionForkController.js +80 -17
- package/extension/src/content/sessionManager.js +1 -1
- package/extension/src/content/settingsPanel.js +7 -3
- package/extension/src/content/settingsWorkbench.js +5 -0
- package/extension/src/content/sharedSessionViewSync.js +45 -8
- package/extension/src/content/updateIdle.js +4 -1
- package/extension/src/content/updateNotice.js +60 -8
- package/extension/src/content/updateSafetyReceiver.js +58 -0
- package/extension/src/content/writebackController.js +53 -4
- package/extension/src/content/writebackOrchestrator.js +82 -73
- package/extension/src/content/writingStyleSettings.js +35 -11
- package/extension/src/page/binaryAssetUploader.js +136 -14
- package/extension/src/page/fileMover.js +256 -0
- package/extension/src/page/ownedDirectoryCleanup.js +94 -0
- package/extension/src/page/referenceProjects.js +1 -0
- package/extension/src/page/reviewContext.js +113 -0
- package/extension/src/page/saveState.js +6 -4
- package/extension/src/page/snapshotRouter.js +84 -39
- package/extension/src/page/textFileCreator.js +105 -25
- package/extension/src/page/trackedChangeCapture.js +12 -2
- package/extension/src/page/trackedChangeOwnership.js +19 -1
- package/extension/src/page/trackedChangeReplay.js +41 -21
- package/extension/src/page/trackedChangesLifecycle.js +217 -199
- package/extension/src/page/treeOperations.js +7 -10
- package/extension/src/page/updateSafetyProbe.js +41 -0
- package/extension/src/page/writebackReceiptJournal.js +2 -0
- package/extension/src/page/writebackRouter.js +80 -79
- package/extension/src/pageBridge.js +19 -3
- package/extension/src/popup.js +62 -10
- package/extension/src/shared/agentTranscript.js +23 -0
- package/extension/src/shared/auditRecords.js +2 -0
- package/extension/src/shared/compatibility.js +22 -6
- package/extension/src/shared/compileAdapter.js +79 -57
- package/extension/src/shared/composerAttachmentDraft.js +151 -0
- package/extension/src/shared/directoryCleanupProof.js +43 -0
- package/extension/src/shared/failureReasons.js +309 -3
- package/extension/src/shared/globalPreferences.js +30 -13
- package/extension/src/shared/governanceRules.js +4 -37
- package/extension/src/shared/i18n.js +298 -2
- package/extension/src/shared/managedUpdateProjection.js +2 -2
- package/extension/src/shared/nativeRequestIdentity.js +1 -0
- package/extension/src/shared/pageRpcContract.js +5 -4
- package/extension/src/shared/projectFiles.js +21 -44
- package/extension/src/shared/providerProfiles.js +5 -3
- package/extension/src/shared/runInputQueue.js +48 -5
- package/extension/src/shared/selectionContext.js +12 -3
- package/extension/src/shared/sessionState.js +72 -27
- package/extension/src/shared/settlementFacts.js +10 -2
- package/extension/src/shared/sharedSessionBridge.js +37 -14
- package/extension/src/shared/sharedSessionTransfer.js +140 -0
- package/extension/src/shared/staleGuard.js +118 -13
- package/extension/src/shared/storageDb.js +58 -26
- package/extension/src/shared/storageMigration.js +71 -111
- package/extension/src/shared/storageRunActions.js +209 -26
- package/extension/src/shared/subagentActivityModel.js +1 -1
- package/extension/src/shared/undoOperations.js +136 -13
- package/extension/src/shared/updateConsent.js +6 -4
- package/extension/src/shared/updateRuntimeIdentity.js +1 -1
- package/extension/src/shared/writebackIntent.js +16 -0
- package/extension/src/shared/writebackOperationEvidence.js +4 -15
- package/extension/src/shared/writebackSettlement.js +38 -38
- package/extension/styles/panel.css +19 -5
- package/native-host/src/anthropicBridgeRequest.js +4 -2
- package/native-host/src/anthropicBridgeResponse.js +6 -2
- package/native-host/src/anthropicMessagesBridge.js +24 -16
- package/native-host/src/bootstrapMigration.js +296 -0
- package/native-host/src/bootstrapMigrationContract.js +101 -0
- package/native-host/src/bootstrapUpdate.js +233 -0
- package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
- package/native-host/src/chatBridgeResponse.js +10 -0
- package/native-host/src/chatCompletionsBridge.js +21 -15
- package/native-host/src/codexConfigPaths.js +106 -0
- package/native-host/src/codexHome.js +66 -73
- package/native-host/src/codexHomeScope.js +124 -0
- package/native-host/src/codexInheritedAuth.js +90 -0
- package/native-host/src/codexLocalSkills.js +301 -0
- package/native-host/src/codexPromptAssembly.js +31 -12
- package/native-host/src/codexProviderTest.js +24 -26
- package/native-host/src/codexSessionRunner.js +110 -183
- package/native-host/src/codexSessionTiming.js +18 -8
- package/native-host/src/codexSkillIsolation.js +170 -0
- package/native-host/src/codexThreadFork.js +19 -3
- package/native-host/src/index.js +19 -7
- package/native-host/src/managedInstall.js +110 -35
- package/native-host/src/managedInstallRecovery.js +140 -0
- package/native-host/src/managedRuntimeEvidence.js +57 -0
- package/native-host/src/mirrorWorkspace.js +92 -22
- package/native-host/src/nativeDoctor.js +15 -0
- package/native-host/src/nativeHelperLifecycle.js +21 -0
- package/native-host/src/nativeHostRegistration.js +33 -0
- package/native-host/src/nativeRunLifecycle.js +71 -0
- package/native-host/src/processStopBarrier.js +42 -7
- package/native-host/src/projectPathPlatform.js +30 -0
- package/native-host/src/providerHttp.js +80 -0
- package/native-host/src/providerProfile.js +4 -2
- package/native-host/src/providerReasoning.js +2 -7
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/providerStore.js +72 -14
- package/native-host/src/responsesPassthroughBridge.js +266 -52
- package/native-host/src/runtimeInstaller.js +21 -1
- package/native-host/src/selectionScope.js +36 -8
- package/native-host/src/subagentAdmission.js +26 -0
- package/native-host/src/subagentBroker.js +79 -66
- package/native-host/src/subagentTelemetry.js +19 -10
- package/native-host/src/subagentWorkspacePath.js +5 -2
- package/native-host/src/taskRunnerRuntime.js +77 -54
- package/native-host/src/tomlStatements.js +140 -0
- package/native-host/src/updateManager.js +297 -39
- package/native-host/src/writingStyleRuntime.js +57 -29
- package/native-host/src/writingStyleStore.js +73 -0
- package/package.json +1 -1
- package/scripts/install-managed.mjs +1 -0
|
@@ -14,6 +14,8 @@ const {
|
|
|
14
14
|
const { extractVerifiedUpdateBundle } = require('./updateArchive');
|
|
15
15
|
const { requestRelease } = require('./releaseTransport');
|
|
16
16
|
const updateNetwork = require('./updateNetworkControl');
|
|
17
|
+
const { readManagedRuntimeEvidence, sameRuntimeEvidence } = require('./managedRuntimeEvidence');
|
|
18
|
+
const BootstrapUpdate = require('./bootstrapUpdate');
|
|
17
19
|
const RUNNING_VERSION = require('../../package.json').version;
|
|
18
20
|
|
|
19
21
|
const GITHUB_LATEST_URL = 'https://github.com/Ghqqqq/codex-overleaf-link/releases/latest';
|
|
@@ -30,9 +32,12 @@ const AUTHORIZATION_TTL_MS = 2 * 60 * 60 * 1000;
|
|
|
30
32
|
const MUTATION_LOCK_WAIT_MS = 10 * 1000;
|
|
31
33
|
const MUTATION_LOCK_STALE_MS = 15 * 60 * 1000;
|
|
32
34
|
const ACTIVATION_DELAY_MS = 125;
|
|
35
|
+
const APPLYING_TRANSACTION_STATES = new Set(['activation_pending', 'applying', 'awaiting_health',
|
|
36
|
+
...BootstrapUpdate.BOOTSTRAP_STATES]);
|
|
33
37
|
const CONSENT_MIGRATION_TARGET_VERSION = '1.9.4';
|
|
34
38
|
const UPDATE_METHODS = new Set([
|
|
35
39
|
'update.status',
|
|
40
|
+
'update.bootstrap',
|
|
36
41
|
'update.check',
|
|
37
42
|
'update.authorize',
|
|
38
43
|
'update.stage',
|
|
@@ -77,7 +82,14 @@ async function handleUpdateRequest(request, options = {}) {
|
|
|
77
82
|
}
|
|
78
83
|
switch (request.method) {
|
|
79
84
|
case 'update.status':
|
|
80
|
-
return okResponse(request.id, readStatusWithoutWaiting(context));
|
|
85
|
+
return okResponse(request.id, readStatusWithoutWaiting(context, request.params || {}));
|
|
86
|
+
case 'update.bootstrap':
|
|
87
|
+
return okResponse(request.id, await withUpdateMutex(context, () => {
|
|
88
|
+
const gate = getApplyGate(options);
|
|
89
|
+
if (!gate.idle) throw updateError('update_native_busy', 'Native host is still processing a Codex task.');
|
|
90
|
+
return BootstrapUpdate.advanceBootstrapMigration(context, readJournal(context),
|
|
91
|
+
request.params || {}, bootstrapDependencies(context), options.bootstrapMigration || {});
|
|
92
|
+
}));
|
|
81
93
|
case 'update.check':
|
|
82
94
|
return okResponse(request.id, await runNetworkUpdate(context, request, options,
|
|
83
95
|
networkOptions => checkForUpdate(context, request.params || {}, networkOptions)));
|
|
@@ -112,7 +124,9 @@ async function handleUpdateRequest(request, options = {}) {
|
|
|
112
124
|
throw updateError('unknown_update_method', 'Unknown managed update method.');
|
|
113
125
|
}
|
|
114
126
|
} catch (error) {
|
|
115
|
-
|
|
127
|
+
const code = safeErrorCode(error);
|
|
128
|
+
return errorResponse(request.id, request.method === 'update.bootstrap' && code === 'update_internal_error'
|
|
129
|
+
? 'update_bootstrap_failed' : code, safeErrorMessage(error));
|
|
116
130
|
}
|
|
117
131
|
}
|
|
118
132
|
|
|
@@ -127,7 +141,7 @@ async function runNetworkUpdate(context, request, options, action) {
|
|
|
127
141
|
|
|
128
142
|
async function cancelNetworkUpdate(context, params) {
|
|
129
143
|
const journal = readJournal(context);
|
|
130
|
-
if (journal &&
|
|
144
|
+
if (journal && (APPLYING_TRANSACTION_STATES.has(journal.state) || journal.sourceActivationPending)) {
|
|
131
145
|
throw updateError('update_revoke_too_late', 'The update is already being installed.');
|
|
132
146
|
}
|
|
133
147
|
return updateNetwork.cancel(context, params.operationId);
|
|
@@ -185,9 +199,16 @@ async function acquireUpdateFileLock(context) {
|
|
|
185
199
|
|
|
186
200
|
function removeStaleUpdateFileLock(lockPath) {
|
|
187
201
|
const owner = readJsonSafe(lockPath, null);
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
202
|
+
const pid = Number(owner?.pid);
|
|
203
|
+
if (Number.isInteger(pid) && pid > 0) {
|
|
204
|
+
// Expiry is not exit evidence. A slow filesystem must not let another
|
|
205
|
+
// process steal a live install/apply lock.
|
|
206
|
+
if (isProcessAlive(pid)) return false;
|
|
207
|
+
} else {
|
|
208
|
+
// The creator may be between exclusive open and writing its owner record.
|
|
209
|
+
try { if (Date.now() - fs.statSync(lockPath).mtimeMs < MUTATION_LOCK_STALE_MS) return false; }
|
|
210
|
+
catch (_error) { return false; }
|
|
211
|
+
}
|
|
191
212
|
try {
|
|
192
213
|
fs.rmSync(lockPath, { force: true });
|
|
193
214
|
return true;
|
|
@@ -212,6 +233,46 @@ function isProcessAlive(pid) {
|
|
|
212
233
|
}
|
|
213
234
|
}
|
|
214
235
|
|
|
236
|
+
function bootstrapDependencies(context) {
|
|
237
|
+
return {
|
|
238
|
+
runningVersion: RUNNING_VERSION,
|
|
239
|
+
verifyManifest: verifySignedReleaseManifest,
|
|
240
|
+
verifyArchive: verifyStagedArchive,
|
|
241
|
+
writeJournal: value => writeJournal(context, value),
|
|
242
|
+
assertAuthorization(journal) {
|
|
243
|
+
const authorization = requireAuthorization(context, journal.targetVersion, ['bound'], { activeVersion: journal.sourceVersion });
|
|
244
|
+
if (authorization.id !== journal.authorizationId || authorization.transactionId !== journal.id) {
|
|
245
|
+
throw updateError('update_consent_mismatch', 'Bootstrap update does not match the authorized transaction.');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function recoverBootstrapTransaction(options, transactionId, token) {
|
|
252
|
+
const context = getManagedContext(options);
|
|
253
|
+
if (!context.managed) throw updateError('update_not_managed', 'Managed installation markers are unavailable.');
|
|
254
|
+
return withUpdateMutex(context, () => {
|
|
255
|
+
const journal = readJournal(context);
|
|
256
|
+
if (journal?.id !== transactionId || journal.bootstrapMigration?.watchdogToken !== token) return { state: 'obsolete' };
|
|
257
|
+
if (journal.sourceActivationPending && journal.state === 'rolled_back') {
|
|
258
|
+
finishSourceActivation(context, journal);
|
|
259
|
+
return { state: 'rolled_back', version: journal.sourceVersion };
|
|
260
|
+
}
|
|
261
|
+
if (!BootstrapUpdate.bootstrapExpired(journal)) return { state: journal.state };
|
|
262
|
+
return rollbackUpdate(context, { transactionId, expectedState: journal.state, reasonCode: 'update_bootstrap_health_timeout' });
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function assertBootstrapBusinessAvailable(request, options = {}) {
|
|
267
|
+
if (isUpdateMethod(request?.method) || ['bridge.ping', 'codex.cancel'].includes(request?.method)) return;
|
|
268
|
+
const context = getManagedContext(options);
|
|
269
|
+
if (!context.managed) return;
|
|
270
|
+
const journal = readJournal(context);
|
|
271
|
+
if (BootstrapUpdate.migrationHint(context, journal) || journal?.sourceActivationPending) {
|
|
272
|
+
throw updateError('update_applying', 'The managed update is still restoring or loading its startup files.');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
215
276
|
function delay(milliseconds) {
|
|
216
277
|
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
217
278
|
}
|
|
@@ -238,7 +299,8 @@ async function checkForUpdate(context, params = {}, options = {}) {
|
|
|
238
299
|
});
|
|
239
300
|
if (releaseResponse.status === 304) {
|
|
240
301
|
const candidate = readJsonSafe(path.join(context.updatesRoot, CANDIDATE_FILE), null);
|
|
241
|
-
if (candidate?.manifestBase64 && candidate?.signatureBase64
|
|
302
|
+
if (candidate?.manifestBase64 && candidate?.signatureBase64
|
|
303
|
+
&& params.etag && candidate.etag === params.etag && candidate.currentVersion === currentVersion) {
|
|
242
304
|
try {
|
|
243
305
|
const manifest = verifySignedReleaseManifest(
|
|
244
306
|
Buffer.from(candidate.manifestBase64, 'base64'),
|
|
@@ -258,7 +320,7 @@ async function checkForUpdate(context, params = {}, options = {}) {
|
|
|
258
320
|
// Invalid cached candidates are treated as cache misses.
|
|
259
321
|
}
|
|
260
322
|
}
|
|
261
|
-
const manual = readManualCandidate(context, currentVersion);
|
|
323
|
+
const manual = readManualCandidate(context, currentVersion, params.etag);
|
|
262
324
|
if (manual) return { ...manual, etag: params.etag || manual.etag || '', cached: true };
|
|
263
325
|
return { managed: true, available: false, reason: 'not_modified', currentVersion, etag: params.etag || '' };
|
|
264
326
|
}
|
|
@@ -270,6 +332,10 @@ async function checkForUpdate(context, params = {}, options = {}) {
|
|
|
270
332
|
throw updateError('update_github_response_invalid', 'GitHub latest release did not resolve to a stable semantic-version tag.');
|
|
271
333
|
}
|
|
272
334
|
const latestVersion = releaseTag.slice(1);
|
|
335
|
+
// A successful latest response replaces the previous discovery result,
|
|
336
|
+
// including when a release was withdrawn or is now in cooldown.
|
|
337
|
+
fs.rmSync(path.join(context.updatesRoot, MANUAL_CANDIDATE_FILE), { force: true });
|
|
338
|
+
fs.rmSync(path.join(context.updatesRoot, CANDIDATE_FILE), { force: true });
|
|
273
339
|
if (!isNewerStableVersion(latestVersion, currentVersion)) {
|
|
274
340
|
const activeMarker = readJsonSafe(path.join(context.extensionRoot, EXTENSION_MARKER), null);
|
|
275
341
|
const equalVersionPrerelease = compareSemver(latestVersion, currentVersion) === 0
|
|
@@ -287,7 +353,8 @@ async function checkForUpdate(context, params = {}, options = {}) {
|
|
|
287
353
|
}
|
|
288
354
|
const cooldowns = readJsonSafe(path.join(context.updatesRoot, COOLDOWN_FILE), {});
|
|
289
355
|
if (Number(cooldowns[latestVersion]?.until || 0) > Date.now()) {
|
|
290
|
-
return { managed: true, available: false, reason: 'cooldown', currentVersion, latestVersion,
|
|
356
|
+
return { managed: true, available: false, reason: 'cooldown', currentVersion, latestVersion,
|
|
357
|
+
cooldownUntil: Number(cooldowns[latestVersion].until), etag: releaseResponse.headers.get('etag') || '' };
|
|
291
358
|
}
|
|
292
359
|
|
|
293
360
|
const manifestBytes = await fetchReleaseAsset(
|
|
@@ -318,7 +385,12 @@ async function checkForUpdate(context, params = {}, options = {}) {
|
|
|
318
385
|
manualInstall: manifest.manualInstall?.reason ? { reason: manifest.manualInstall.reason } : {},
|
|
319
386
|
etag: releaseResponse.headers.get('etag') || ''
|
|
320
387
|
};
|
|
321
|
-
atomicWriteJson(path.join(context.updatesRoot, MANUAL_CANDIDATE_FILE), {
|
|
388
|
+
atomicWriteJson(path.join(context.updatesRoot, MANUAL_CANDIDATE_FILE), {
|
|
389
|
+
...manual,
|
|
390
|
+
checkedAt: new Date().toISOString(),
|
|
391
|
+
manifestBase64: manifestBytes.toString('base64'),
|
|
392
|
+
signatureBase64: signatureBytes.toString('base64')
|
|
393
|
+
});
|
|
322
394
|
return manual;
|
|
323
395
|
}
|
|
324
396
|
const candidate = {
|
|
@@ -334,13 +406,29 @@ async function checkForUpdate(context, params = {}, options = {}) {
|
|
|
334
406
|
return { managed: true, available: true, currentVersion, latestVersion, etag: candidate.etag };
|
|
335
407
|
}
|
|
336
408
|
|
|
337
|
-
function readManualCandidate(context, currentVersion) {
|
|
409
|
+
function readManualCandidate(context, currentVersion, etag) {
|
|
338
410
|
const manual = readJsonSafe(path.join(context.updatesRoot, MANUAL_CANDIDATE_FILE), null);
|
|
339
|
-
if (manual?.
|
|
340
|
-
|| !
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
411
|
+
if (!etag || manual?.etag !== etag || manual.currentVersion !== currentVersion
|
|
412
|
+
|| !manual.manifestBase64 || !manual.signatureBase64) return null;
|
|
413
|
+
try {
|
|
414
|
+
const manifest = verifySignedReleaseManifest(
|
|
415
|
+
Buffer.from(manual.manifestBase64, 'base64'),
|
|
416
|
+
Buffer.from(manual.signatureBase64, 'base64'),
|
|
417
|
+
{ allowBootstrapMismatch: true }
|
|
418
|
+
);
|
|
419
|
+
if (!isNewerStableVersion(manifest.version, currentVersion)
|
|
420
|
+
|| manifest.bootstrapProtocol === BOOTSTRAP_PROTOCOL) return null;
|
|
421
|
+
return {
|
|
422
|
+
managed: true, available: false, reason: 'manual_install_required',
|
|
423
|
+
currentVersion, latestVersion: manifest.version,
|
|
424
|
+
installedBootstrapProtocol: BOOTSTRAP_PROTOCOL,
|
|
425
|
+
requiredBootstrapProtocol: manifest.bootstrapProtocol,
|
|
426
|
+
manualInstall: manifest.manualInstall?.reason ? { reason: manifest.manualInstall.reason } : {},
|
|
427
|
+
etag
|
|
428
|
+
};
|
|
429
|
+
} catch (_error) {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
344
432
|
}
|
|
345
433
|
|
|
346
434
|
function readAuthorizedCandidate(context, currentVersion) {
|
|
@@ -381,6 +469,10 @@ function buildReleaseAssetUrl(tag, assetName) {
|
|
|
381
469
|
}
|
|
382
470
|
|
|
383
471
|
function authorizeUpdate(context, params = {}) {
|
|
472
|
+
const activeJournal = readJournal(context);
|
|
473
|
+
if (BootstrapUpdate.migrationHint(context, activeJournal) || activeJournal?.sourceActivationPending) {
|
|
474
|
+
throw updateError('update_transaction_in_progress', 'The current Bootstrap update must finish before authorizing another update.');
|
|
475
|
+
}
|
|
384
476
|
const authorizationId = String(params.authorizationId || '');
|
|
385
477
|
const targetVersion = String(params.targetVersion || '');
|
|
386
478
|
const requestedCurrentVersion = String(params.currentVersion || '');
|
|
@@ -428,6 +520,10 @@ function authorizeUpdate(context, params = {}) {
|
|
|
428
520
|
}
|
|
429
521
|
|
|
430
522
|
async function stageCandidate(context, options = {}) {
|
|
523
|
+
const activeJournal = readJournal(context);
|
|
524
|
+
if (BootstrapUpdate.migrationHint(context, activeJournal) || activeJournal?.sourceActivationPending) {
|
|
525
|
+
throw updateError('update_transaction_in_progress', 'The current Bootstrap update must finish before staging another update.');
|
|
526
|
+
}
|
|
431
527
|
const candidate = readJsonSafe(path.join(context.updatesRoot, CANDIDATE_FILE), null);
|
|
432
528
|
if (!candidate) {
|
|
433
529
|
throw updateError('update_candidate_missing', 'Check for an update before staging it.');
|
|
@@ -467,7 +563,7 @@ async function stageCandidate(context, options = {}) {
|
|
|
467
563
|
);
|
|
468
564
|
}
|
|
469
565
|
}
|
|
470
|
-
if (existingJournal &&
|
|
566
|
+
if (existingJournal && APPLYING_TRANSACTION_STATES.has(existingJournal.state)) {
|
|
471
567
|
throw updateError('update_transaction_in_progress', 'A managed update transaction is already being applied.');
|
|
472
568
|
}
|
|
473
569
|
if (authorization.state === 'bound') {
|
|
@@ -533,6 +629,7 @@ function getApplyGate(options = {}) {
|
|
|
533
629
|
const blockers = [];
|
|
534
630
|
if (Number(state.projectLocks || 0) > 0) blockers.push('native_project_locked');
|
|
535
631
|
if (Number(state.runControllers || 0) > 0) blockers.push('native_run_active');
|
|
632
|
+
if (Number(state.processStopBarriers || 0) > 0) blockers.push('native_process_stop_unconfirmed');
|
|
536
633
|
return { idle: blockers.length === 0, blockers, workState: state };
|
|
537
634
|
}
|
|
538
635
|
|
|
@@ -577,7 +674,8 @@ function scheduleStagedActivation(context, params = {}, options = {}) {
|
|
|
577
674
|
|
|
578
675
|
function recordScheduledActivationFailure(context, pending, error) {
|
|
579
676
|
const current = readJournal(context);
|
|
580
|
-
if (!current || current.id !== pending.id || ['awaiting_health', 'rolled_back'
|
|
677
|
+
if (!current || current.id !== pending.id || ['awaiting_health', 'rolled_back', 'committed',
|
|
678
|
+
...BootstrapUpdate.BOOTSTRAP_STATES].includes(current.state)) return;
|
|
581
679
|
writeJournal(context, {
|
|
582
680
|
...current,
|
|
583
681
|
state: 'rolled_back',
|
|
@@ -687,6 +785,7 @@ function confirmUpdate(context, params = {}) {
|
|
|
687
785
|
if (params.transactionId !== journal.id || params.extensionVersion !== journal.targetVersion || params.nativeVersion !== journal.targetVersion) {
|
|
688
786
|
throw updateError('update_health_version_mismatch', 'Extension and native host did not confirm the same target version.');
|
|
689
787
|
}
|
|
788
|
+
BootstrapUpdate.assertBootstrapConfirmation(context, journal, params);
|
|
690
789
|
if (!isLegacyInboundConsentMigration(journal)) {
|
|
691
790
|
const authorization = requireAuthorization(context, journal.targetVersion, ['bound'], {
|
|
692
791
|
activeVersion: journal.sourceVersion
|
|
@@ -699,9 +798,15 @@ function confirmUpdate(context, params = {}) {
|
|
|
699
798
|
if (readVersionPointer(context.nativeRoot, 'active-version') !== journal.targetVersion || manifest?.version !== journal.targetVersion) {
|
|
700
799
|
throw updateError('update_health_version_mismatch', 'Managed files do not match the confirmed target version.');
|
|
701
800
|
}
|
|
801
|
+
let committedEvidence;
|
|
802
|
+
try { committedEvidence = readManagedRuntimeEvidence(context, journal.targetVersion); }
|
|
803
|
+
catch (cause) {
|
|
804
|
+
throw updateError('update_health_failed', 'The installed runtime files did not pass the update health check.', { cause });
|
|
805
|
+
}
|
|
702
806
|
writeJournal(context, {
|
|
703
807
|
...journal,
|
|
704
808
|
state: 'committed',
|
|
809
|
+
committedEvidence,
|
|
705
810
|
confirmedAt: new Date().toISOString(),
|
|
706
811
|
updatedAt: new Date().toISOString()
|
|
707
812
|
});
|
|
@@ -713,35 +818,53 @@ function confirmUpdate(context, params = {}) {
|
|
|
713
818
|
}
|
|
714
819
|
|
|
715
820
|
function rollbackUpdate(context, params = {}) {
|
|
716
|
-
|
|
821
|
+
let journal = readJournal(context);
|
|
717
822
|
if (params.transactionId && journal?.id !== params.transactionId) {
|
|
718
823
|
throw updateError('update_transaction_mismatch', 'Rollback transaction identity changed.');
|
|
719
824
|
}
|
|
720
825
|
if (params.expectedState && journal?.state !== params.expectedState) {
|
|
721
826
|
throw updateError('update_transaction_state_changed', 'The update state changed before rollback.');
|
|
722
827
|
}
|
|
723
|
-
|
|
828
|
+
journal = BootstrapUpdate.bindBootstrapMigration(context, journal, bootstrapDependencies(context));
|
|
829
|
+
if (!journal || !['applying', 'awaiting_health', 'committed', 'bootstrap_pending', 'bootstrap_rolling_back'].includes(journal.state)) {
|
|
724
830
|
throw updateError('update_rollback_unavailable', 'No applied update is available for rollback.');
|
|
725
831
|
}
|
|
726
|
-
|
|
832
|
+
if (journal.bootstrapMigration && !params.transactionId && journal.bootstrapMigration.phase !== 'pending') {
|
|
833
|
+
throw updateError('update_transaction_mismatch', 'Bootstrap rollback must identify its update transaction.');
|
|
834
|
+
}
|
|
835
|
+
const migration = Boolean(journal.bootstrapMigration);
|
|
836
|
+
if (migration) writeJournal(context, { ...journal, state: 'bootstrap_rolling_back' });
|
|
837
|
+
const rollbackEvidence = rollbackFiles(context, journal, { deferSourceActivation: migration });
|
|
727
838
|
const cooldowns = readJsonSafe(path.join(context.updatesRoot, COOLDOWN_FILE), {});
|
|
728
839
|
cooldowns[journal.targetVersion] = {
|
|
729
840
|
until: Date.now() + 24 * 60 * 60 * 1000,
|
|
730
841
|
reasonCode: normalizeReasonCode(params.reasonCode)
|
|
731
842
|
};
|
|
732
843
|
atomicWriteJson(path.join(context.updatesRoot, COOLDOWN_FILE), cooldowns);
|
|
733
|
-
|
|
844
|
+
const restoredJournal = {
|
|
734
845
|
...journal,
|
|
735
846
|
state: 'rolled_back',
|
|
736
847
|
reasonCode: normalizeReasonCode(params.reasonCode),
|
|
737
848
|
rolledBackAt: new Date().toISOString(),
|
|
738
849
|
updatedAt: new Date().toISOString(),
|
|
739
|
-
previousManifest: undefined
|
|
740
|
-
|
|
850
|
+
previousManifest: undefined,
|
|
851
|
+
rollbackEvidence,
|
|
852
|
+
...(migration ? { bootstrapMigration: { ...journal.bootstrapMigration, phase: 'restored',
|
|
853
|
+
sourceUnchanged: rollbackEvidence.bootstrapSourceUnchanged === true },
|
|
854
|
+
sourceActivationPending: true } : {})
|
|
855
|
+
};
|
|
856
|
+
writeJournal(context, restoredJournal);
|
|
857
|
+
// The old launcher must not select a source process that cannot understand
|
|
858
|
+
// our recovery record until every file and the durable terminal record exist.
|
|
859
|
+
if (migration) {
|
|
860
|
+
settleAuthorization(context, journal.authorizationId, 'revoked');
|
|
861
|
+
atomicWriteText(path.join(context.nativeRoot, 'active-version'), journal.sourceVersion + '\n');
|
|
862
|
+
writeJournal(context, { ...restoredJournal, sourceActivationPending: false });
|
|
863
|
+
}
|
|
741
864
|
settleAuthorization(context, journal.authorizationId, 'revoked');
|
|
742
865
|
cleanupStage(journal.stageRoot);
|
|
743
866
|
cleanupOrphanStageRoots(context.updatesRoot);
|
|
744
|
-
return { version: journal.sourceVersion, state: 'rolled_back' };
|
|
867
|
+
return { transactionId: journal.id, version: journal.sourceVersion, state: 'rolled_back', restoredPair: rollbackEvidence };
|
|
745
868
|
}
|
|
746
869
|
|
|
747
870
|
function revokeUpdate(context, params = {}) {
|
|
@@ -753,7 +876,7 @@ function revokeUpdate(context, params = {}) {
|
|
|
753
876
|
}
|
|
754
877
|
const journal = readJournal(context);
|
|
755
878
|
if (journal?.targetVersion === targetVersion &&
|
|
756
|
-
|
|
879
|
+
(APPLYING_TRANSACTION_STATES.has(journal.state) || journal.state === 'committed' || journal.sourceActivationPending)) {
|
|
757
880
|
throw updateError('update_revoke_too_late', 'The update is already being applied.');
|
|
758
881
|
}
|
|
759
882
|
const authorization = readAuthorization(context);
|
|
@@ -782,9 +905,15 @@ function revokeUpdate(context, params = {}) {
|
|
|
782
905
|
return { state: 'revoked', targetVersion };
|
|
783
906
|
}
|
|
784
907
|
|
|
785
|
-
function rollbackFiles(context, journal) {
|
|
908
|
+
function rollbackFiles(context, journal, options = {}) {
|
|
786
909
|
const runtime = path.join(context.extensionRoot, 'runtime');
|
|
787
910
|
const previousRuntime = path.join(context.extensionRoot, 'slots', 'previous', 'runtime');
|
|
911
|
+
// An early interrupted apply may still have the source in the current slot.
|
|
912
|
+
// A missing previous slot is recoverable only when these actual bytes match
|
|
913
|
+
// the source pair; the source manifest and markers alone are insufficient.
|
|
914
|
+
const source = readManagedRuntimeEvidence(context, journal.sourceVersion,
|
|
915
|
+
fs.existsSync(previousRuntime) ? previousRuntime : runtime);
|
|
916
|
+
if (journal.bootstrapMigration) BootstrapUpdate.restoreBootstrap(context, journal);
|
|
788
917
|
if (fs.existsSync(previousRuntime)) {
|
|
789
918
|
fs.rmSync(runtime, { recursive: true, force: true });
|
|
790
919
|
fs.renameSync(previousRuntime, runtime);
|
|
@@ -793,7 +922,7 @@ function rollbackFiles(context, journal) {
|
|
|
793
922
|
if (journal.previousManifest) {
|
|
794
923
|
atomicWriteText(path.join(context.extensionRoot, 'manifest.json'), journal.previousManifest);
|
|
795
924
|
}
|
|
796
|
-
if (parseSemver(journal.sourceVersion)) {
|
|
925
|
+
if (parseSemver(journal.sourceVersion) && !options.deferSourceActivation) {
|
|
797
926
|
atomicWriteText(path.join(context.nativeRoot, 'active-version'), journal.sourceVersion + '\n');
|
|
798
927
|
}
|
|
799
928
|
const sourcePrevious = parseSemver(journal.sourcePreviousVersion) ? journal.sourcePreviousVersion : '';
|
|
@@ -805,6 +934,18 @@ function rollbackFiles(context, journal) {
|
|
|
805
934
|
if (parseSemver(journal.sourceVersion)) {
|
|
806
935
|
syncManagedMarkerVersions(context, journal.sourceVersion);
|
|
807
936
|
}
|
|
937
|
+
const restored = readManagedRuntimeEvidence(context, journal.sourceVersion);
|
|
938
|
+
if (!sameRuntimeEvidence(source, restored)) {
|
|
939
|
+
throw updateError('update_rollback_unconfirmed', 'The source runtime readback did not match the restored files.');
|
|
940
|
+
}
|
|
941
|
+
if (journal.bootstrapMigration) {
|
|
942
|
+
const bootstrap = BootstrapUpdate.bootstrapEvidence(context, journal, 'source');
|
|
943
|
+
if (bootstrap) {
|
|
944
|
+
restored.bootstrapSha256 = bootstrap.digest;
|
|
945
|
+
if (bootstrap.unmodified) restored.bootstrapSourceUnchanged = true;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return restored;
|
|
808
949
|
}
|
|
809
950
|
|
|
810
951
|
function syncManagedMarkerVersions(context, version) {
|
|
@@ -843,8 +984,11 @@ function cleanupOrphanStageRoots(updatesRoot, retainedRoots = []) {
|
|
|
843
984
|
}
|
|
844
985
|
}
|
|
845
986
|
|
|
846
|
-
function recoverAndReadStatus(context) {
|
|
987
|
+
function recoverAndReadStatus(context, params = {}) {
|
|
847
988
|
let journal = readJournal(context);
|
|
989
|
+
if (journal?.sourceActivationPending && journal.state === 'rolled_back') {
|
|
990
|
+
journal = finishSourceActivation(context, journal);
|
|
991
|
+
}
|
|
848
992
|
const alignedVersion = readAlignedManagedVersion(context);
|
|
849
993
|
journal = supersedeObsoleteJournal(context, journal, alignedVersion);
|
|
850
994
|
if (journal?.state === 'activation_pending') {
|
|
@@ -852,33 +996,120 @@ function recoverAndReadStatus(context) {
|
|
|
852
996
|
writeJournal(context, journal);
|
|
853
997
|
}
|
|
854
998
|
if (journal?.state === 'applying') {
|
|
855
|
-
rollbackFiles(context, journal);
|
|
856
|
-
journal = { ...journal, state: 'rolled_back', reasonCode: 'update_interrupted', updatedAt: new Date().toISOString() };
|
|
999
|
+
const rollbackEvidence = rollbackFiles(context, journal);
|
|
1000
|
+
journal = { ...journal, rollbackEvidence, state: 'rolled_back', reasonCode: 'update_interrupted', updatedAt: new Date().toISOString() };
|
|
857
1001
|
writeJournal(context, journal);
|
|
858
1002
|
settleAuthorization(context, journal.authorizationId, 'revoked');
|
|
859
1003
|
}
|
|
1004
|
+
journal = BootstrapUpdate.bindBootstrapMigration(context, journal, bootstrapDependencies(context));
|
|
1005
|
+
if (BootstrapUpdate.bootstrapExpired(journal, { recoveryRequested: params.bootstrapRecovery === true, context })) {
|
|
1006
|
+
rollbackUpdate(context, { transactionId: journal.id, expectedState: journal.state, reasonCode: 'update_bootstrap_health_timeout' });
|
|
1007
|
+
journal = readJournal(context);
|
|
1008
|
+
}
|
|
860
1009
|
if (journal?.state === 'rolled_back' || journal?.state === 'superseded') {
|
|
1010
|
+
if (journal.state === 'superseded') cleanupStage(journal.stageRoot);
|
|
861
1011
|
pruneNativeVersions(context.nativeRoot, new Set([
|
|
862
1012
|
readVersionPointer(context.nativeRoot, 'active-version'),
|
|
863
1013
|
readVersionPointer(context.nativeRoot, 'previous-version')
|
|
864
1014
|
]));
|
|
865
1015
|
}
|
|
1016
|
+
if (journal?.state === 'committed' && journal.committedEvidence && alignedVersion === journal.targetVersion) {
|
|
1017
|
+
const observed = readManagedRuntimeEvidence(context, journal.targetVersion);
|
|
1018
|
+
if (sameRuntimeEvidence(journal.committedEvidence, observed)) {
|
|
1019
|
+
if (journal.bootstrapMigration) BootstrapUpdate.assertBootstrapConfirmation(context, journal,
|
|
1020
|
+
{ bootstrapRevision: journal.bootstrapMigration.targetRevision });
|
|
1021
|
+
const authorization = readAuthorization(context);
|
|
1022
|
+
if (['bound', 'consumed'].includes(authorization?.state) && authorization.id === journal.authorizationId
|
|
1023
|
+
&& authorization.transactionId === journal.id && authorization.sourceVersion === journal.sourceVersion
|
|
1024
|
+
&& authorization.targetVersion === journal.targetVersion) {
|
|
1025
|
+
settleAuthorization(context, journal.authorizationId, 'consumed');
|
|
1026
|
+
}
|
|
1027
|
+
cleanupStage(journal.stageRoot);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
866
1030
|
return currentStatus(context, journal);
|
|
867
1031
|
}
|
|
868
1032
|
|
|
1033
|
+
function finishSourceActivation(context, journal) {
|
|
1034
|
+
const actual = readManagedRuntimeEvidence(context, journal.sourceVersion);
|
|
1035
|
+
if (!sameRuntimeEvidence(actual, journal.rollbackEvidence)) {
|
|
1036
|
+
throw updateError('update_rollback_unconfirmed', 'The recovered source files changed before activation.');
|
|
1037
|
+
}
|
|
1038
|
+
BootstrapUpdate.bootstrapEvidence(context, journal, 'source');
|
|
1039
|
+
settleAuthorization(context, journal.authorizationId, 'revoked');
|
|
1040
|
+
atomicWriteText(path.join(context.nativeRoot, 'active-version'), journal.sourceVersion + '\n');
|
|
1041
|
+
const finished = { ...journal, sourceActivationPending: false };
|
|
1042
|
+
writeJournal(context, finished);
|
|
1043
|
+
cleanupStage(journal.stageRoot);
|
|
1044
|
+
return finished;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Called by the full installer while holding the same mutation lock, only
|
|
1048
|
+
// after both runtime trees and browser registration have succeeded.
|
|
1049
|
+
function settleReinstalledUpdate(context, version, expected) {
|
|
1050
|
+
if (!expected?.id || !parseSemver(expected.sourceVersion) || !parseSemver(expected.targetVersion)
|
|
1051
|
+
|| compareSemver(version, expected.targetVersion) < 0
|
|
1052
|
+
|| compareSemver(version, expected.sourceVersion) <= 0) return;
|
|
1053
|
+
const journal = readJournal(context);
|
|
1054
|
+
if (!journal || ['id', 'state', 'sourceVersion', 'targetVersion'].some(key => journal[key] !== expected[key])) {
|
|
1055
|
+
throw updateError('update_transaction_mismatch', 'The update transaction changed during full installation.');
|
|
1056
|
+
}
|
|
1057
|
+
if (assertManagedLayout(context) !== version) {
|
|
1058
|
+
throw updateError('update_managed_layout_invalid', 'Full installation did not produce the requested pair.');
|
|
1059
|
+
}
|
|
1060
|
+
const reinstalledPair = readManagedRuntimeEvidence(context, version);
|
|
1061
|
+
const timestamp = new Date().toISOString();
|
|
1062
|
+
writeJournal(context, {
|
|
1063
|
+
...journal, state: 'superseded', supersededFromState: journal.state,
|
|
1064
|
+
supersededByVersion: version, supersededAt: timestamp,
|
|
1065
|
+
updatedAt: timestamp, reasonCode: 'update_reinstalled', reinstalledPair
|
|
1066
|
+
});
|
|
1067
|
+
const authorization = readAuthorization(context);
|
|
1068
|
+
if (authorization?.state === 'bound' && authorization.id === journal.authorizationId
|
|
1069
|
+
&& authorization.transactionId === journal.id && authorization.sourceVersion === journal.sourceVersion
|
|
1070
|
+
&& authorization.targetVersion === journal.targetVersion) {
|
|
1071
|
+
settleAuthorization(context, authorization.id, 'revoked');
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
869
1075
|
function currentStatus(context, journal = readJournal(context)) {
|
|
1076
|
+
let restoredPair = null;
|
|
1077
|
+
let reinstalledPair = null;
|
|
1078
|
+
if (journal?.state === 'rolled_back') {
|
|
1079
|
+
try {
|
|
1080
|
+
const observed = readManagedRuntimeEvidence(context, journal.sourceVersion);
|
|
1081
|
+
if (!journal.rollbackEvidence || sameRuntimeEvidence(journal.rollbackEvidence, observed)) {
|
|
1082
|
+
if (journal.bootstrapMigration) {
|
|
1083
|
+
const bootstrap = BootstrapUpdate.bootstrapEvidence(context, journal, 'source');
|
|
1084
|
+
if (bootstrap) observed.bootstrapSha256 = bootstrap.digest;
|
|
1085
|
+
if (journal.rollbackEvidence?.bootstrapSha256 !== observed.bootstrapSha256) throw new Error('Bootstrap restoration changed.');
|
|
1086
|
+
}
|
|
1087
|
+
restoredPair = observed;
|
|
1088
|
+
}
|
|
1089
|
+
} catch (_) { /* Missing or changed bytes cannot confirm a recovered pair. */ }
|
|
1090
|
+
}
|
|
1091
|
+
if (journal?.state === 'superseded' && journal.reasonCode === 'update_reinstalled' && journal.reinstalledPair) {
|
|
1092
|
+
try {
|
|
1093
|
+
const observed = readManagedRuntimeEvidence(context, journal.supersededByVersion);
|
|
1094
|
+
if (sameRuntimeEvidence(journal.reinstalledPair, observed)) reinstalledPair = observed;
|
|
1095
|
+
} catch (_) { /* A reinstall receipt cannot authenticate later changed files. */ }
|
|
1096
|
+
}
|
|
870
1097
|
return {
|
|
871
1098
|
managed: true,
|
|
872
1099
|
activeVersion: readVersionPointer(context.nativeRoot, 'active-version'),
|
|
873
1100
|
runtimeVersion: context.env?.CODEX_OVERLEAF_ACTIVE_VERSION || RUNNING_VERSION,
|
|
874
1101
|
installedAligned: Boolean(readAlignedManagedVersion(context)),
|
|
875
1102
|
previousVersion: readVersionPointer(context.nativeRoot, 'previous-version'),
|
|
876
|
-
transaction: journal ? publicTransaction(journal)
|
|
877
|
-
|
|
1103
|
+
transaction: journal ? { ...publicTransaction(journal),
|
|
1104
|
+
...(journal.state === 'awaiting_health' && BootstrapUpdate.migrationHint(context, journal)
|
|
1105
|
+
&& journal.bootstrapMigration?.phase !== 'loaded' ? { state: 'bootstrap_pending' } : {}) } : null,
|
|
1106
|
+
authorization: publicAuthorization(readAuthorization(context)),
|
|
1107
|
+
restoredPair,
|
|
1108
|
+
reinstalledPair
|
|
878
1109
|
};
|
|
879
1110
|
}
|
|
880
1111
|
|
|
881
|
-
function readStatusWithoutWaiting(context) {
|
|
1112
|
+
function readStatusWithoutWaiting(context, params = {}) {
|
|
882
1113
|
const lockPath = path.join(context.updatesRoot, MUTATION_LOCK_FILE);
|
|
883
1114
|
const token = crypto.randomUUID();
|
|
884
1115
|
let descriptor;
|
|
@@ -888,7 +1119,7 @@ function readStatusWithoutWaiting(context) {
|
|
|
888
1119
|
if (error.code !== 'EEXIST') throw error;
|
|
889
1120
|
const owner = readJsonSafe(lockPath, null);
|
|
890
1121
|
if (Number.isInteger(owner?.pid) && !isProcessAlive(owner.pid) && removeStaleUpdateFileLock(lockPath)) {
|
|
891
|
-
return readStatusWithoutWaiting(context);
|
|
1122
|
+
return readStatusWithoutWaiting(context, params);
|
|
892
1123
|
}
|
|
893
1124
|
// A live mutation owns recovery; report an atomic-file snapshot without
|
|
894
1125
|
// queueing behind its network transfer or changing its journal.
|
|
@@ -897,7 +1128,7 @@ function readStatusWithoutWaiting(context) {
|
|
|
897
1128
|
try {
|
|
898
1129
|
fs.writeFileSync(descriptor, JSON.stringify({ token, pid: process.pid,
|
|
899
1130
|
createdAt: Date.now(), expiresAt: Date.now() + MUTATION_LOCK_STALE_MS }));
|
|
900
|
-
return recoverAndReadStatus(context);
|
|
1131
|
+
return recoverAndReadStatus(context, params);
|
|
901
1132
|
} finally {
|
|
902
1133
|
fs.closeSync(descriptor);
|
|
903
1134
|
releaseUpdateFileLock({ lockPath, token });
|
|
@@ -913,12 +1144,21 @@ function readAlignedManagedVersion(context) {
|
|
|
913
1144
|
}
|
|
914
1145
|
|
|
915
1146
|
function supersedeObsoleteJournal(context, journal, activeVersion) {
|
|
916
|
-
if (!journal ||
|
|
1147
|
+
if (!journal || !parseSemver(activeVersion) ||
|
|
917
1148
|
!parseSemver(journal.sourceVersion) || !parseSemver(journal.targetVersion) ||
|
|
918
1149
|
compareSemver(activeVersion, journal.sourceVersion) <= 0 ||
|
|
919
1150
|
compareSemver(activeVersion, journal.targetVersion) <= 0) {
|
|
920
1151
|
return journal;
|
|
921
1152
|
}
|
|
1153
|
+
// A previous runtime may already have retired the journal without settling
|
|
1154
|
+
// its bound authorization. Never consume an authorization owned elsewhere.
|
|
1155
|
+
const authorization = readAuthorization(context);
|
|
1156
|
+
if (authorization?.state === 'bound' && journal.authorizationId
|
|
1157
|
+
&& authorization.id === journal.authorizationId && authorization.transactionId === journal.id
|
|
1158
|
+
&& authorization.sourceVersion === journal.sourceVersion && authorization.targetVersion === journal.targetVersion) {
|
|
1159
|
+
settleAuthorization(context, authorization.id, 'revoked');
|
|
1160
|
+
}
|
|
1161
|
+
if (journal.state === 'superseded') return journal;
|
|
922
1162
|
const timestamp = new Date().toISOString();
|
|
923
1163
|
const superseded = {
|
|
924
1164
|
...journal,
|
|
@@ -1098,6 +1338,7 @@ function publicTransaction(journal) {
|
|
|
1098
1338
|
state: journal.state,
|
|
1099
1339
|
sourceVersion: journal.sourceVersion,
|
|
1100
1340
|
targetVersion: journal.targetVersion,
|
|
1341
|
+
...(journal.bootstrapMigration ? { bootstrapRevision: journal.bootstrapMigration.targetRevision } : {}),
|
|
1101
1342
|
createdAt: journal.createdAt,
|
|
1102
1343
|
activationRequestedAt: journal.activationRequestedAt,
|
|
1103
1344
|
appliedAt: journal.appliedAt,
|
|
@@ -1189,10 +1430,22 @@ function atomicWriteJson(target, value) {
|
|
|
1189
1430
|
}
|
|
1190
1431
|
|
|
1191
1432
|
function atomicWriteText(target, value) {
|
|
1192
|
-
const temp = target + '.tmp-' + process.pid + '-' +
|
|
1433
|
+
const temp = target + '.tmp-' + process.pid + '-' + crypto.randomBytes(6).toString('hex');
|
|
1193
1434
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
1194
|
-
fs.
|
|
1435
|
+
let descriptor = fs.openSync(temp, 'wx', 0o600);
|
|
1436
|
+
try {
|
|
1437
|
+
// Windows requires a writable handle for FlushFileBuffers (Node fsync).
|
|
1438
|
+
fs.writeFileSync(descriptor, value);
|
|
1439
|
+
fs.fsyncSync(descriptor);
|
|
1440
|
+
} finally { if (descriptor !== undefined) fs.closeSync(descriptor); }
|
|
1195
1441
|
fs.renameSync(temp, target);
|
|
1442
|
+
descriptor = undefined;
|
|
1443
|
+
try {
|
|
1444
|
+
descriptor = fs.openSync(path.dirname(target), 'r');
|
|
1445
|
+
fs.fsyncSync(descriptor);
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
if (process.platform !== 'win32' || !['EPERM', 'EISDIR', 'EINVAL', 'EBADF', 'EACCES'].includes(error.code)) throw error;
|
|
1448
|
+
} finally { if (descriptor !== undefined) fs.closeSync(descriptor); }
|
|
1196
1449
|
}
|
|
1197
1450
|
|
|
1198
1451
|
function readJsonSafe(target, fallback) {
|
|
@@ -1246,8 +1499,10 @@ function errorResponse(id, code, message) {
|
|
|
1246
1499
|
|
|
1247
1500
|
module.exports = {
|
|
1248
1501
|
GITHUB_LATEST_URL,
|
|
1502
|
+
MUTATION_LOCK_STALE_MS,
|
|
1249
1503
|
applyStagedUpdate,
|
|
1250
1504
|
assertManagedLayout,
|
|
1505
|
+
assertBootstrapBusinessAvailable,
|
|
1251
1506
|
authorizeUpdate,
|
|
1252
1507
|
checkForUpdate,
|
|
1253
1508
|
cleanupOrphanStageRoots,
|
|
@@ -1257,8 +1512,11 @@ module.exports = {
|
|
|
1257
1512
|
handleUpdateRequest,
|
|
1258
1513
|
isUpdateMethod,
|
|
1259
1514
|
revokeUpdate,
|
|
1515
|
+
removeStaleUpdateFileLock,
|
|
1516
|
+
recoverBootstrapTransaction,
|
|
1260
1517
|
rollbackUpdate,
|
|
1261
1518
|
scheduleStagedActivation,
|
|
1519
|
+
settleReinstalledUpdate,
|
|
1262
1520
|
stageCandidate,
|
|
1263
1521
|
syncManagedMarkerVersions
|
|
1264
1522
|
};
|