codex-overleaf-link 2.5.0 → 2.6.0
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 +155 -392
- package/README.zh-CN.md +154 -393
- 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 +283 -55
- package/extension/src/backgroundBootstrapMigration.js +181 -0
- package/extension/src/backgroundSharedSessions.js +129 -16
- package/extension/src/backgroundUpdateCoordinator.js +242 -29
- package/extension/src/backgroundUpdateSafety.js +173 -0
- package/extension/src/content/activeTurnControl.js +171 -8
- package/extension/src/content/applyResultFormatters.js +46 -73
- package/extension/src/content/assetTransferBroker.js +72 -14
- package/extension/src/content/composerAttachments.js +104 -47
- package/extension/src/content/composerPanel.js +110 -12
- package/extension/src/content/contentRuntime.js +1555 -926
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsController.js +122 -96
- package/extension/src/content/diagnosticsExport.js +19 -5
- package/extension/src/content/diagnosticsPanel.js +48 -0
- package/extension/src/content/directoryCleanupRecovery.js +54 -0
- package/extension/src/content/generated/content.bundle.js +11077 -3412
- package/extension/src/content/generated/content.bundle.meta.json +14 -3
- package/extension/src/content/globalPreferencesController.js +1 -1
- package/extension/src/content/legacyGlobalRegistry.js +10 -0
- package/extension/src/content/localSkillsPanel.js +104 -14
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +47 -11
- package/extension/src/content/modelPicker.js +8 -4
- package/extension/src/content/nativeCompatibilityController.js +74 -30
- package/extension/src/content/otWarmMirror.js +5 -6
- package/extension/src/content/otWarmMirrorController.js +17 -20
- package/extension/src/content/pageBridgeClient.js +17 -4
- package/extension/src/content/panelMaintenance.js +46 -14
- package/extension/src/content/panelRenderer.js +57 -27
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectProviderSelection.js +13 -4
- package/extension/src/content/projectSessionCleanup.js +109 -62
- package/extension/src/content/projectSettingsCoordinator.js +67 -6
- package/extension/src/content/providerSettingsCoordinator.js +46 -8
- package/extension/src/content/providerSettingsDialog.js +73 -14
- package/extension/src/content/recentProjects.js +15 -8
- package/extension/src/content/runActivitySummary.js +160 -18
- package/extension/src/content/runCompletionMeta.js +532 -0
- package/extension/src/content/runController.js +39 -15
- package/extension/src/content/runFailureNotice.js +29 -13
- package/extension/src/content/runGuidanceController.js +72 -27
- package/extension/src/content/runGuidanceView.js +59 -10
- package/extension/src/content/runPresence.js +18 -8
- package/extension/src/content/runQueueScheduler.js +157 -47
- package/extension/src/content/runRecoveryActions.js +147 -0
- package/extension/src/content/runResultActions.js +28 -143
- package/extension/src/content/runReviewController.js +633 -0
- package/extension/src/content/runScrollLayout.js +21 -2
- package/extension/src/content/runTimelineView.js +367 -175
- package/extension/src/content/scopedPersistencePanelState.js +134 -20
- package/extension/src/content/selectionContextView.js +1 -1
- package/extension/src/content/sensitivePreflight.js +153 -0
- package/extension/src/content/sessionForkController.js +80 -17
- package/extension/src/content/sessionManager.js +50 -4
- package/extension/src/content/sessionMenuView.js +54 -18
- package/extension/src/content/sessionPersistence.js +25 -5
- package/extension/src/content/settingsPanel.js +22 -5
- package/extension/src/content/settingsWorkbench.js +132 -89
- package/extension/src/content/sharedSessionViewSync.js +49 -11
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateIdle.js +4 -1
- package/extension/src/content/updateNotice.js +118 -12
- package/extension/src/content/updateSafetyReceiver.js +58 -0
- package/extension/src/content/writebackController.js +53 -4
- package/extension/src/content/writebackOrchestrator.js +116 -94
- package/extension/src/content/writingStyleSettings.js +35 -11
- package/extension/src/page/binaryAssetUploader.js +136 -14
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/fileMover.js +256 -0
- package/extension/src/page/overleafCapabilities.js +4 -1
- 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/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +114 -26
- 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 +103 -155
- package/extension/src/pageBridge.js +126 -69
- package/extension/src/popup.js +62 -10
- package/extension/src/shared/agentTranscript.js +373 -17
- package/extension/src/shared/auditRecords.js +4 -0
- package/extension/src/shared/compatibility.js +22 -6
- package/extension/src/shared/compileAdapter.js +129 -66
- package/extension/src/shared/composerAttachmentDraft.js +156 -0
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/directoryCleanupProof.js +43 -0
- package/extension/src/shared/failureReasons.js +349 -4
- package/extension/src/shared/globalPreferences.js +30 -13
- package/extension/src/shared/governanceRules.js +4 -37
- package/extension/src/shared/i18n.js +590 -24
- package/extension/src/shared/lineReferences.js +38 -31
- 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/panelIcons.js +39 -0
- package/extension/src/shared/projectFiles.js +21 -44
- package/extension/src/shared/providerProfiles.js +5 -3
- package/extension/src/shared/reviewing.js +130 -1
- package/extension/src/shared/runActivityModel.js +70 -4
- package/extension/src/shared/runExecutionSnapshot.js +13 -0
- package/extension/src/shared/runInputQueue.js +48 -5
- package/extension/src/shared/selectionContext.js +12 -3
- package/extension/src/shared/sessionState.js +132 -28
- package/extension/src/shared/sessionTitle.js +14 -1
- package/extension/src/shared/settlementFacts.js +10 -2
- package/extension/src/shared/sharedSessionBridge.js +118 -27
- package/extension/src/shared/sharedSessionTransfer.js +140 -0
- package/extension/src/shared/staleGuard.js +166 -52
- package/extension/src/shared/storageDb.js +90 -26
- package/extension/src/shared/storageMigration.js +78 -112
- package/extension/src/shared/storageRunActions.js +267 -30
- package/extension/src/shared/subagentActivityModel.js +1 -1
- package/extension/src/shared/undoOperations.js +253 -32
- package/extension/src/shared/updateConsent.js +6 -4
- package/extension/src/shared/updateRuntimeIdentity.js +17 -3
- package/extension/src/shared/writebackIntent.js +16 -0
- package/extension/src/shared/writebackOperationEvidence.js +4 -15
- package/extension/src/shared/writebackSettlement.js +58 -40
- package/extension/styles/panel.css +1123 -493
- 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 +269 -0
- package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
- package/native-host/src/chatBridgeRequest.js +60 -6
- package/native-host/src/chatBridgeResponse.js +20 -1
- package/native-host/src/chatCompletionsBridge.js +48 -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 +114 -127
- package/native-host/src/codexProviderTest.js +24 -26
- package/native-host/src/codexSessionRunner.js +234 -198
- 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/providerContextError.js +20 -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 +92 -57
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/tomlStatements.js +140 -0
- package/native-host/src/updateManager.js +409 -46
- package/native-host/src/writingStyleRuntime.js +57 -29
- package/native-host/src/writingStyleStore.js +73 -0
- package/package.json +5 -8
- package/scripts/install-managed.mjs +1 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
const { updateError } = require('./updateTrust');
|
|
7
|
+
const { MUTATION_LOCK_STALE_MS, removeStaleUpdateFileLock } = require('./updateManager');
|
|
8
|
+
|
|
9
|
+
// A full install changes one Native version and a small fixed set of files.
|
|
10
|
+
// Retain just those destinations until registration and journal settlement
|
|
11
|
+
// succeed; this is in-process compensation, not crash recovery for the disk.
|
|
12
|
+
function createInstallRestorePoint({ nativeRoot, version, files }) {
|
|
13
|
+
const rootWasAbsent = !fs.existsSync(nativeRoot);
|
|
14
|
+
let existed = false;
|
|
15
|
+
const backup = path.join(path.dirname(nativeRoot), '.native.rollback-' + crypto.randomUUID());
|
|
16
|
+
const target = path.join(nativeRoot, 'versions', version);
|
|
17
|
+
let targetExisted = false;
|
|
18
|
+
let snapshots = [];
|
|
19
|
+
let lock = null;
|
|
20
|
+
let restored = false;
|
|
21
|
+
try {
|
|
22
|
+
lock = acquireInstallLock(nativeRoot);
|
|
23
|
+
existed = fs.existsSync(path.join(nativeRoot, '.codex-overleaf-managed-native.json'));
|
|
24
|
+
if (!existed && (fs.readdirSync(nativeRoot).some(name => name !== 'updates')
|
|
25
|
+
|| fs.readdirSync(path.join(nativeRoot, 'updates')).some(name => name !== 'mutation.lock'))) {
|
|
26
|
+
throw new Error('Refusing to replace an unmarked managed native root: ' + nativeRoot);
|
|
27
|
+
}
|
|
28
|
+
targetExisted = fs.existsSync(target);
|
|
29
|
+
snapshots = files.map(filename => {
|
|
30
|
+
try {
|
|
31
|
+
const stat = fs.statSync(filename);
|
|
32
|
+
return { filename, bytes: fs.readFileSync(filename), mode: stat.mode & 0o777 };
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error.code !== 'ENOENT') throw error;
|
|
35
|
+
return { filename, bytes: null };
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
if (targetExisted) fs.cpSync(target, backup, { recursive: true });
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (lock) releaseInstallLock(lock);
|
|
41
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
42
|
+
if (rootWasAbsent) {
|
|
43
|
+
// Only remove directories still empty after our failed initialization;
|
|
44
|
+
// never remove another creator's lock, marker or runtime files.
|
|
45
|
+
try { fs.rmdirSync(path.join(nativeRoot, 'updates')); fs.rmdirSync(nativeRoot); } catch (_) { /* retained */ }
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
nativeExisted: existed,
|
|
51
|
+
restore() {
|
|
52
|
+
assertInstallLock(lock);
|
|
53
|
+
const errors = [];
|
|
54
|
+
const attempt = action => { try { action(); } catch (error) { errors.push(error); } };
|
|
55
|
+
attempt(() => {
|
|
56
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
57
|
+
if (targetExisted) fs.renameSync(backup, target);
|
|
58
|
+
});
|
|
59
|
+
for (const snapshot of snapshots) attempt(() => {
|
|
60
|
+
if (snapshot.bytes === null) fs.rmSync(snapshot.filename, { force: true });
|
|
61
|
+
else {
|
|
62
|
+
fs.mkdirSync(path.dirname(snapshot.filename), { recursive: true });
|
|
63
|
+
fs.writeFileSync(snapshot.filename, snapshot.bytes, { mode: snapshot.mode });
|
|
64
|
+
fs.chmodSync(snapshot.filename, snapshot.mode);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
if (errors.length) throw updateError('managed_install_restore_failed',
|
|
68
|
+
'Managed installation failed and the previous installation could not be fully restored.', { cause: errors[0] });
|
|
69
|
+
restored = true;
|
|
70
|
+
},
|
|
71
|
+
finish() {
|
|
72
|
+
let removeEmptyRoot = false;
|
|
73
|
+
try {
|
|
74
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
75
|
+
if (restored && !existed) {
|
|
76
|
+
assertInstallLock(lock);
|
|
77
|
+
// The root was created by this install. Keep its mutation lock while
|
|
78
|
+
// removing our remaining staging/directories, including after all
|
|
79
|
+
// filesystem and browser-registration compensation has completed.
|
|
80
|
+
for (const name of fs.readdirSync(nativeRoot)) {
|
|
81
|
+
if (name !== 'updates') fs.rmSync(path.join(nativeRoot, name), { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
for (const name of fs.readdirSync(path.join(nativeRoot, 'updates'))) {
|
|
84
|
+
if (name !== 'mutation.lock') fs.rmSync(path.join(nativeRoot, 'updates', name), { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
removeEmptyRoot = true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
finally { if (lock) releaseInstallLock(lock); }
|
|
90
|
+
if (removeEmptyRoot) {
|
|
91
|
+
// After release another installer may own these paths. Empty-only
|
|
92
|
+
// removal cannot delete its new lock, metadata, runtime or bridge.
|
|
93
|
+
for (const directory of [path.join(nativeRoot, 'updates'), nativeRoot]) {
|
|
94
|
+
try { fs.rmdirSync(directory); }
|
|
95
|
+
catch (error) { if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error.code)) throw error; }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
release() { if (lock) releaseInstallLock(lock); }
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function assertInstallLock(lock) {
|
|
104
|
+
let owner;
|
|
105
|
+
try { owner = JSON.parse(fs.readFileSync(lock.filename, 'utf8')); } catch (_) { /* unknown */ }
|
|
106
|
+
if (owner?.token !== lock?.token) {
|
|
107
|
+
throw updateError('managed_install_restore_failed', 'The installation no longer owns its recovery lock.');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function acquireInstallLock(nativeRoot) {
|
|
112
|
+
const filename = path.join(nativeRoot, 'updates', 'mutation.lock');
|
|
113
|
+
fs.mkdirSync(path.dirname(filename), { recursive: true });
|
|
114
|
+
const token = crypto.randomUUID();
|
|
115
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
116
|
+
let descriptor;
|
|
117
|
+
try {
|
|
118
|
+
descriptor = fs.openSync(filename, 'wx', 0o600);
|
|
119
|
+
fs.writeFileSync(descriptor, JSON.stringify({ token, pid: process.pid, operationId: '',
|
|
120
|
+
createdAt: Date.now(), expiresAt: Date.now() + MUTATION_LOCK_STALE_MS }));
|
|
121
|
+
fs.closeSync(descriptor);
|
|
122
|
+
return { filename, token };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (descriptor !== undefined) { fs.closeSync(descriptor); fs.rmSync(filename, { force: true }); throw error; }
|
|
125
|
+
if (error.code !== 'EEXIST') throw error;
|
|
126
|
+
if (!removeStaleUpdateFileLock(filename)) {
|
|
127
|
+
throw updateError('update_busy', 'A managed update is in progress. Retry installation after it settles.');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw updateError('update_busy', 'A managed update is in progress. Retry installation after it settles.');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function releaseInstallLock(lock) {
|
|
135
|
+
try {
|
|
136
|
+
if (JSON.parse(fs.readFileSync(lock.filename, 'utf8')).token === lock.token) fs.rmSync(lock.filename);
|
|
137
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { createInstallRestorePoint };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
const { parseSemver, updateError } = require('./updateTrust');
|
|
7
|
+
|
|
8
|
+
function readManagedRuntimeEvidence(context, version, runtimeRoot = path.join(context.extensionRoot, 'runtime')) {
|
|
9
|
+
try {
|
|
10
|
+
if (!parseSemver(version)) throw new Error('Invalid source version');
|
|
11
|
+
const compatibility = fs.readFileSync(path.join(runtimeRoot, 'src/shared/compatibility.js'), 'utf8');
|
|
12
|
+
const declared = compatibility.match(/BUILD_TARGET_VERSION\s*=\s*['"]([^'"]+)['"]/)?.[1];
|
|
13
|
+
if (declared !== version) throw new Error('Extension source version mismatch');
|
|
14
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(runtimeRoot, 'runtime-manifest.json'), 'utf8'));
|
|
15
|
+
const assets = [...(Array.isArray(manifest.js) ? manifest.js : []), ...(Array.isArray(manifest.css) ? manifest.css : [])];
|
|
16
|
+
if (!assets.length) throw new Error('Missing runtime assets');
|
|
17
|
+
for (const asset of assets) {
|
|
18
|
+
if (typeof asset !== 'string' || asset.startsWith('/') || asset.includes('\\')
|
|
19
|
+
|| asset.split('/').some(part => !part || part === '.' || part === '..')
|
|
20
|
+
|| !fs.statSync(path.join(runtimeRoot, asset)).isFile()) throw new Error('Invalid runtime asset');
|
|
21
|
+
}
|
|
22
|
+
const nativeRoot = path.join(context.nativeRoot, 'versions', version);
|
|
23
|
+
if (JSON.parse(fs.readFileSync(path.join(nativeRoot, 'package.json'), 'utf8')).version !== version
|
|
24
|
+
|| !fs.statSync(path.join(nativeRoot, 'native-host/src/index.js')).isFile()) throw new Error('Native source unavailable');
|
|
25
|
+
return { extensionVersion: declared, nativeVersion: version,
|
|
26
|
+
extensionSha256: hashTree(runtimeRoot), nativeSha256: hashTree(nativeRoot) };
|
|
27
|
+
} catch (cause) {
|
|
28
|
+
throw updateError('update_rollback_source_unavailable',
|
|
29
|
+
'The source runtime files could not be verified. Run install-managed to restore the matched installation.', { cause });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hashTree(root) {
|
|
34
|
+
const hash = crypto.createHash('sha256');
|
|
35
|
+
function visit(directory, prefix) {
|
|
36
|
+
// Native processes can start under different LANG/ICU defaults. Evidence
|
|
37
|
+
// ordering must be a fixed code-unit order, never locale collation.
|
|
38
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })
|
|
39
|
+
.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
40
|
+
const filename = path.join(directory, entry.name), relative = prefix + entry.name;
|
|
41
|
+
if (entry.isDirectory()) visit(filename, relative + '/');
|
|
42
|
+
else if (entry.isFile()) {
|
|
43
|
+
const bytes = fs.readFileSync(filename);
|
|
44
|
+
hash.update(JSON.stringify([relative, bytes.length])); hash.update(bytes);
|
|
45
|
+
} else throw new Error('Runtime entry is not a regular file or directory');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
visit(root, '');
|
|
49
|
+
return hash.digest('hex');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sameRuntimeEvidence(left, right) {
|
|
53
|
+
return Boolean(left && right && ['extensionVersion', 'nativeVersion', 'extensionSha256', 'nativeSha256']
|
|
54
|
+
.every(key => left[key] && left[key] === right[key]));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { readManagedRuntimeEvidence, sameRuntimeEvidence };
|
|
@@ -4,6 +4,7 @@ const crypto = require('node:crypto');
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const { getHomeDir } = require('./nativeHostPlatform');
|
|
7
|
+
const { assertProjectPathsForPlatform } = require('./projectPathPlatform');
|
|
7
8
|
|
|
8
9
|
const BASELINE_FILE = 'baseline.json';
|
|
9
10
|
const MAX_BINARY_FILE_BYTES = 10 * 1024 * 1024;
|
|
@@ -37,10 +38,14 @@ async function syncOverleafToMirror({ projectId, project, rootDir }) {
|
|
|
37
38
|
const mirror = getProjectMirror(projectId, { rootDir });
|
|
38
39
|
const normalized = normalizeProjectFilesDetailed(project?.files || []);
|
|
39
40
|
const files = normalized.files;
|
|
40
|
-
const skippedFiles = normalized.skippedFiles;
|
|
41
|
-
const nextPaths = new Set(files.map(file => file.path));
|
|
42
41
|
const previous = readBaseline(mirror.baselinePath);
|
|
43
42
|
const fullProjectSnapshot = project?.capabilities?.fullProjectSnapshot !== false;
|
|
43
|
+
const skippedFiles = resolveUnreadSnapshotFiles(project, normalized, previous, fullProjectSnapshot);
|
|
44
|
+
// Runtime platform is authoritative; request fields cannot opt out of this check.
|
|
45
|
+
assertProjectPathsForPlatform([...files, ...skippedFiles].map(file => file.path), process.platform);
|
|
46
|
+
const unreadPaths = new Set(skippedFiles.map(file => file.path));
|
|
47
|
+
const nextPaths = new Set([...files.map(file => file.path), ...unreadPaths]);
|
|
48
|
+
const completeContents = fullProjectSnapshot && !unreadPaths.size;
|
|
44
49
|
|
|
45
50
|
const mirrorRoot = path.dirname(mirror.projectRoot);
|
|
46
51
|
assertSafeMirrorPathBeforeCreate(mirror.workspacePath, mirrorRoot);
|
|
@@ -82,20 +87,21 @@ async function syncOverleafToMirror({ projectId, project, rootDir }) {
|
|
|
82
87
|
|
|
83
88
|
const now = new Date().toISOString();
|
|
84
89
|
const nextBaselineFiles = fullProjectSnapshot
|
|
85
|
-
? files.map(file => buildBaselineFile(file))
|
|
90
|
+
? [...files.map(file => buildBaselineFile(file)), ...(previous.files || []).filter(file => unreadPaths.has(file.path))]
|
|
86
91
|
: mergePartialBaselineFiles(previous.files || [], files);
|
|
87
92
|
|
|
88
93
|
writeBaseline(mirror.baselinePath, {
|
|
89
94
|
...previous,
|
|
90
95
|
projectKey: mirror.projectKey,
|
|
91
|
-
capturedAt:
|
|
92
|
-
lastFullSyncAt:
|
|
93
|
-
lastPartialSyncAt:
|
|
96
|
+
capturedAt: completeContents ? now : (previous.capturedAt || ''),
|
|
97
|
+
lastFullSyncAt: completeContents ? now : previous.lastFullSyncAt,
|
|
98
|
+
lastPartialSyncAt: completeContents ? previous.lastPartialSyncAt : now,
|
|
94
99
|
lastSyncSource: fullProjectSnapshot ? (project?.capabilities?.method || 'snapshot') : previous.lastSyncSource,
|
|
95
100
|
lastFileCount: files.length,
|
|
96
|
-
dirty:
|
|
97
|
-
dirtyReason:
|
|
98
|
-
dirtyAt:
|
|
101
|
+
dirty: unreadPaths.size > 0 || (!completeContents && previous.dirty === true),
|
|
102
|
+
dirtyReason: unreadPaths.size ? 'snapshot_content_unavailable' : completeContents ? '' : previous.dirtyReason || '',
|
|
103
|
+
dirtyAt: unreadPaths.size ? now : completeContents ? '' : previous.dirtyAt || '',
|
|
104
|
+
unreadFiles: skippedFiles,
|
|
99
105
|
files: nextBaselineFiles
|
|
100
106
|
});
|
|
101
107
|
|
|
@@ -104,10 +110,30 @@ async function syncOverleafToMirror({ projectId, project, rootDir }) {
|
|
|
104
110
|
fileCount: files.length,
|
|
105
111
|
writtenCount,
|
|
106
112
|
skippedFiles,
|
|
113
|
+
contentComplete: completeContents,
|
|
107
114
|
partialSnapshot: !fullProjectSnapshot
|
|
108
115
|
};
|
|
109
116
|
}
|
|
110
117
|
|
|
118
|
+
// A ZIP skip establishes that a path exists, without establishing its current
|
|
119
|
+
// bytes. Keep its last known bytes/baseline, but never promote them to fresh.
|
|
120
|
+
function resolveUnreadSnapshotFiles(project, normalized, previous, fullProjectSnapshot) {
|
|
121
|
+
const filesByPath = new Map();
|
|
122
|
+
const candidates = [
|
|
123
|
+
...(fullProjectSnapshot ? [] : previous.unreadFiles || []),
|
|
124
|
+
...(Array.isArray(project?.capabilities?.skipped) ? project.capabilities.skipped : []),
|
|
125
|
+
...normalized.skippedFiles
|
|
126
|
+
];
|
|
127
|
+
for (const file of candidates) {
|
|
128
|
+
if (!['binary_file_too_large', 'binary_project_too_large'].includes(file?.reason) || typeof file.path !== 'string') continue;
|
|
129
|
+
const filePath = normalizeRelativePath(file.path);
|
|
130
|
+
filesByPath.set(filePath, { path: filePath, kind: 'binary', reason: file.reason,
|
|
131
|
+
...(Number.isFinite(Number(file.size)) && Number(file.size) >= 0 ? { size: Number(file.size) } : {}) });
|
|
132
|
+
}
|
|
133
|
+
for (const file of normalized.files) filesByPath.delete(file.path);
|
|
134
|
+
return Array.from(filesByPath.values());
|
|
135
|
+
}
|
|
136
|
+
|
|
111
137
|
async function collectMirrorChanges({ projectId, rootDir }) {
|
|
112
138
|
return (await collectMirrorChangesDetailed({ projectId, rootDir })).changes;
|
|
113
139
|
}
|
|
@@ -116,6 +142,7 @@ async function collectMirrorChangesDetailed({ projectId, rootDir }) {
|
|
|
116
142
|
const mirror = getProjectMirror(projectId, { rootDir });
|
|
117
143
|
const baseline = readBaseline(mirror.baselinePath);
|
|
118
144
|
const baselineByPath = new Map((baseline.files || []).map(file => [file.path, file]));
|
|
145
|
+
const unreadPaths = new Set((baseline.unreadFiles || []).map(file => file.path));
|
|
119
146
|
const currentPaths = listWorkspaceFiles(mirror.workspacePath);
|
|
120
147
|
const currentByPath = new Map();
|
|
121
148
|
const unsupportedChanges = [];
|
|
@@ -124,6 +151,14 @@ async function collectMirrorChangesDetailed({ projectId, rootDir }) {
|
|
|
124
151
|
const previous = baselineByPath.get(filePath);
|
|
125
152
|
const target = resolveWorkspacePath(mirror.workspacePath, filePath);
|
|
126
153
|
const stat = fs.statSync(target);
|
|
154
|
+
if (unreadPaths.has(filePath)) {
|
|
155
|
+
if (!previous || !workspaceFileMatchesBaseline(target, previous)) {
|
|
156
|
+
unsupportedChanges.push({ type: 'unsupported-local-file', path: filePath,
|
|
157
|
+
reason: 'snapshot_content_unavailable', size: stat.size, previousExists: true,
|
|
158
|
+
previousKind: previous?.kind || 'binary' });
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
127
162
|
if (isGeneratedArtifactPath(filePath, baselineByPath)) {
|
|
128
163
|
if (!previous || !workspaceFileMatchesBaseline(target, previous)) {
|
|
129
164
|
unsupportedChanges.push({
|
|
@@ -208,6 +243,7 @@ async function collectMirrorChangesDetailed({ projectId, rootDir }) {
|
|
|
208
243
|
previousExists: Boolean(previous),
|
|
209
244
|
previousKind: previous?.kind || '',
|
|
210
245
|
previousSize: previous?.size,
|
|
246
|
+
previousSha256: previous?.kind === 'binary' ? previous.hash : undefined,
|
|
211
247
|
size: contentOrBinary.size
|
|
212
248
|
});
|
|
213
249
|
continue;
|
|
@@ -225,6 +261,13 @@ async function collectMirrorChangesDetailed({ projectId, rootDir }) {
|
|
|
225
261
|
}
|
|
226
262
|
|
|
227
263
|
for (const [filePath, previous] of baselineByPath) {
|
|
264
|
+
if (unreadPaths.has(filePath)) {
|
|
265
|
+
if (!currentPaths.includes(filePath)) {
|
|
266
|
+
unsupportedChanges.push({ type: 'unsupported-local-file', path: filePath,
|
|
267
|
+
reason: 'snapshot_content_unavailable', previousExists: true, previousKind: previous.kind });
|
|
268
|
+
}
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
228
271
|
if (previous.kind === 'binary') {
|
|
229
272
|
if (!currentByPath.has(filePath) && !currentPaths.includes(filePath)) {
|
|
230
273
|
unsupportedChanges.push({
|
|
@@ -622,6 +665,7 @@ function hashBytes(bytes) {
|
|
|
622
665
|
function getMirrorStatus(projectId, options = {}) {
|
|
623
666
|
const mirror = getProjectMirror(projectId, options);
|
|
624
667
|
const baseline = readBaseline(mirror.baselinePath);
|
|
668
|
+
const unreadContents = (baseline.unreadFiles || []).length > 0;
|
|
625
669
|
if (!baseline.lastFullSyncAt) {
|
|
626
670
|
return {
|
|
627
671
|
exists: false,
|
|
@@ -633,13 +677,13 @@ function getMirrorStatus(projectId, options = {}) {
|
|
|
633
677
|
lastPartialSyncAt: baseline.lastPartialSyncAt || '',
|
|
634
678
|
lastSyncSource: baseline.lastSyncSource || '',
|
|
635
679
|
lastFileCount: Number.isFinite(Number(baseline.lastFileCount)) ? Number(baseline.lastFileCount) : (baseline.files || []).length,
|
|
636
|
-
dirty: baseline.dirty === true,
|
|
637
|
-
dirtyReason: baseline.dirtyReason || '',
|
|
680
|
+
dirty: unreadContents || baseline.dirty === true,
|
|
681
|
+
dirtyReason: unreadContents ? 'snapshot_content_unavailable' : baseline.dirtyReason || '',
|
|
638
682
|
workspacePath: mirror.workspacePath,
|
|
639
683
|
...buildOtStatusFields(baseline, false)
|
|
640
684
|
};
|
|
641
685
|
}
|
|
642
|
-
if (baseline.dirty === true) {
|
|
686
|
+
if (unreadContents || baseline.dirty === true) {
|
|
643
687
|
return {
|
|
644
688
|
exists: false,
|
|
645
689
|
projectKey: mirror.projectKey,
|
|
@@ -651,7 +695,7 @@ function getMirrorStatus(projectId, options = {}) {
|
|
|
651
695
|
lastSyncSource: baseline.lastSyncSource || '',
|
|
652
696
|
lastFileCount: Number.isFinite(Number(baseline.lastFileCount)) ? Number(baseline.lastFileCount) : (baseline.files || []).length,
|
|
653
697
|
dirty: true,
|
|
654
|
-
dirtyReason: baseline.dirtyReason || 'dirty_mirror',
|
|
698
|
+
dirtyReason: unreadContents ? 'snapshot_content_unavailable' : baseline.dirtyReason || 'dirty_mirror',
|
|
655
699
|
workspacePath: mirror.workspacePath,
|
|
656
700
|
...buildOtStatusFields(baseline, false)
|
|
657
701
|
};
|
|
@@ -747,6 +791,7 @@ function buildOtStatusFields(baseline = {}, trusted) {
|
|
|
747
791
|
&& isFreshOtObservation(file.freshness))
|
|
748
792
|
.map(file => ({
|
|
749
793
|
path: file.path,
|
|
794
|
+
sha256: file.hash,
|
|
750
795
|
source: file.freshness.source,
|
|
751
796
|
state: file.freshness.state,
|
|
752
797
|
lastFullSyncAt: file.freshness.lastFullSyncAt || '',
|
|
@@ -767,12 +812,14 @@ async function applyFileOverlays({ projectId, overlays, rootDir }) {
|
|
|
767
812
|
const mirror = getProjectMirror(projectId, { rootDir });
|
|
768
813
|
const baseline = readBaseline(mirror.baselinePath);
|
|
769
814
|
const filesByPath = new Map((baseline.files || []).map(f => [f.path, f]));
|
|
815
|
+
const unreadPaths = new Set((baseline.unreadFiles || []).map(file => file.path));
|
|
770
816
|
|
|
771
817
|
for (const overlay of overlays || []) {
|
|
772
818
|
if (!overlay?.path || typeof overlay.content !== 'string') {
|
|
773
819
|
continue;
|
|
774
820
|
}
|
|
775
821
|
const normalizedPath = normalizeRelativePath(overlay.path);
|
|
822
|
+
if (unreadPaths.has(normalizedPath)) continue;
|
|
776
823
|
const target = resolveWorkspacePath(mirror.workspacePath, normalizedPath);
|
|
777
824
|
assertSafeWorkspaceTarget(mirror.workspacePath, target, normalizedPath);
|
|
778
825
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
@@ -794,14 +841,15 @@ async function applyFileOverlays({ projectId, overlays, rootDir }) {
|
|
|
794
841
|
}
|
|
795
842
|
|
|
796
843
|
// v1.8.0: after a VERIFIED writeback, the workspace already holds the exact
|
|
797
|
-
// content that landed in Overleaf
|
|
798
|
-
//
|
|
799
|
-
//
|
|
800
|
-
|
|
801
|
-
// back to a full syncOverleafToMirror.
|
|
802
|
-
async function confirmWritebackFiles({ projectId, paths, rootDir }) {
|
|
844
|
+
// content that landed in Overleaf only while its hashes match the page's
|
|
845
|
+
// verified post-images. Check every target before updating any baseline.
|
|
846
|
+
// Missing or mismatched evidence falls back to a full syncOverleafToMirror.
|
|
847
|
+
async function confirmWritebackFiles({ projectId, paths, expectedFiles, rootDir }) {
|
|
803
848
|
const mirror = getProjectMirror(projectId, { rootDir });
|
|
804
849
|
const baseline = readBaseline(mirror.baselinePath);
|
|
850
|
+
if ((baseline.unreadFiles || []).length) {
|
|
851
|
+
return { ok: false, reason: 'snapshot_content_unavailable' };
|
|
852
|
+
}
|
|
805
853
|
if (!baseline.lastFullSyncAt) {
|
|
806
854
|
return { ok: false, reason: 'no_baseline' };
|
|
807
855
|
}
|
|
@@ -810,8 +858,23 @@ async function confirmWritebackFiles({ projectId, paths, rootDir }) {
|
|
|
810
858
|
}
|
|
811
859
|
const filesByPath = new Map((baseline.files || []).map(file => [file.path, file]));
|
|
812
860
|
const confirmed = [];
|
|
813
|
-
|
|
861
|
+
const requestedPaths = Array.isArray(paths) ? paths : [];
|
|
862
|
+
const seenPaths = new Set();
|
|
863
|
+
const expectedByPath = new Map();
|
|
864
|
+
if (Array.isArray(expectedFiles) && expectedFiles.length === requestedPaths.length) {
|
|
865
|
+
for (const file of expectedFiles) {
|
|
866
|
+
if (typeof file?.path !== 'string' || !/^[a-f0-9]{64}$/i.test(file?.sha256 || '')) {
|
|
867
|
+
return { ok: false, reason: 'postimage_proof_invalid' };
|
|
868
|
+
}
|
|
869
|
+
const normalizedPath = normalizeRelativePath(file.path);
|
|
870
|
+
if (expectedByPath.has(normalizedPath)) return { ok: false, reason: 'postimage_proof_invalid' };
|
|
871
|
+
expectedByPath.set(normalizedPath, file.sha256.toLowerCase());
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
for (const rawPath of requestedPaths) {
|
|
814
875
|
const normalizedPath = normalizeRelativePath(rawPath);
|
|
876
|
+
if (seenPaths.has(normalizedPath)) return { ok: false, reason: 'postimage_proof_invalid' };
|
|
877
|
+
seenPaths.add(normalizedPath);
|
|
815
878
|
const baselineFile = filesByPath.get(normalizedPath);
|
|
816
879
|
if (!baselineFile) {
|
|
817
880
|
return { ok: false, reason: 'missing_baseline_file', path: normalizedPath };
|
|
@@ -819,6 +882,9 @@ async function confirmWritebackFiles({ projectId, paths, rootDir }) {
|
|
|
819
882
|
if (baselineFile.kind !== 'text') {
|
|
820
883
|
return { ok: false, reason: 'not_text', path: normalizedPath };
|
|
821
884
|
}
|
|
885
|
+
if (!expectedByPath.has(normalizedPath)) {
|
|
886
|
+
return { ok: false, reason: 'postimage_proof_required', path: normalizedPath };
|
|
887
|
+
}
|
|
822
888
|
let target;
|
|
823
889
|
try {
|
|
824
890
|
target = resolveWorkspacePath(mirror.workspacePath, normalizedPath);
|
|
@@ -835,6 +901,9 @@ async function confirmWritebackFiles({ projectId, paths, rootDir }) {
|
|
|
835
901
|
return { ok: false, reason: 'workspace_read_failed', path: normalizedPath };
|
|
836
902
|
}
|
|
837
903
|
const hash = hashText(content);
|
|
904
|
+
if (hash !== expectedByPath.get(normalizedPath)) {
|
|
905
|
+
return { ok: false, reason: 'postimage_mismatch', path: normalizedPath };
|
|
906
|
+
}
|
|
838
907
|
filesByPath.set(normalizedPath, { ...baselineFile, hash, content });
|
|
839
908
|
confirmed.push({ path: normalizedPath, hash });
|
|
840
909
|
}
|
|
@@ -854,6 +923,7 @@ async function confirmWritebackFiles({ projectId, paths, rootDir }) {
|
|
|
854
923
|
async function patchMirrorFiles({ projectId, files, rootDir, source = 'ot' }) {
|
|
855
924
|
const mirror = getProjectMirror(projectId, { rootDir });
|
|
856
925
|
const baseline = readBaseline(mirror.baselinePath);
|
|
926
|
+
const unreadContents = (baseline.unreadFiles || []).length > 0;
|
|
857
927
|
const baselineByPath = new Map((baseline.files || []).map(file => [file.path, file]));
|
|
858
928
|
const patchedFilesByPath = new Map();
|
|
859
929
|
const appliedFiles = [];
|
|
@@ -895,7 +965,7 @@ async function patchMirrorFiles({ projectId, files, rootDir, source = 'ot' }) {
|
|
|
895
965
|
const nextHash = hashText(patch.nextContent);
|
|
896
966
|
if (patch.baseHash !== baselineFile.hash) {
|
|
897
967
|
if (nextHash === baselineFile.hash) {
|
|
898
|
-
if (baseline.dirty === true) {
|
|
968
|
+
if (unreadContents || baseline.dirty === true) {
|
|
899
969
|
skippedFiles.push({ path: normalized.path, reason: 'dirty_mirror' });
|
|
900
970
|
lastOtErrorCode = 'dirty_mirror';
|
|
901
971
|
continue;
|
|
@@ -922,7 +992,7 @@ async function patchMirrorFiles({ projectId, files, rootDir, source = 'ot' }) {
|
|
|
922
992
|
lastOtErrorCode = 'base_hash_mismatch';
|
|
923
993
|
continue;
|
|
924
994
|
}
|
|
925
|
-
if (baseline.dirty === true) {
|
|
995
|
+
if (unreadContents || baseline.dirty === true) {
|
|
926
996
|
skippedFiles.push({ path: normalized.path, reason: 'dirty_mirror' });
|
|
927
997
|
lastOtErrorCode = 'dirty_mirror';
|
|
928
998
|
continue;
|
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
getNativeHostPlatform,
|
|
11
11
|
getNativeHostRegistrationTarget
|
|
12
12
|
} = require('./nativeHostPlatform');
|
|
13
|
+
const { queryWindowsNativeHostRegistration } = require('./nativeHostRegistration');
|
|
13
14
|
const { decodeFrames, encodeMessage } = require('./nativeMessaging');
|
|
14
15
|
const {
|
|
15
16
|
buildBridgePingParams,
|
|
@@ -68,6 +69,12 @@ async function runDoctor(options = {}) {
|
|
|
68
69
|
}
|
|
69
70
|
};
|
|
70
71
|
|
|
72
|
+
if (registrationTarget.kind === 'registry') {
|
|
73
|
+
const observed = queryWindowsNativeHostRegistration(registrationTarget, { env, spawnSync: options.spawnSync });
|
|
74
|
+
body.registration = { ...body.registration, ...observed,
|
|
75
|
+
...(observed.registeredManifestPath ? { registeredManifestPath: normalizeDiagnosticPath(observed.registeredManifestPath, pathOptions) } : {}) };
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
if (runtimeRoot) {
|
|
72
79
|
body.runtimeRoot = runtimeRoot;
|
|
73
80
|
body.diagnostics.runtimeRoot = runtimeRoot;
|
|
@@ -160,6 +167,10 @@ function classifyDoctorResult(body = {}) {
|
|
|
160
167
|
};
|
|
161
168
|
}
|
|
162
169
|
|
|
170
|
+
if (body.registration?.kind === 'registry' && body.registration.verified !== true) {
|
|
171
|
+
return { exitCode: 2, ok: false, status: 'registration_unverified' };
|
|
172
|
+
}
|
|
173
|
+
|
|
163
174
|
if (body.compatibility?.classification === 'compatible') {
|
|
164
175
|
return {
|
|
165
176
|
exitCode: 0,
|
|
@@ -198,6 +209,10 @@ function formatDoctorHuman(body = {}) {
|
|
|
198
209
|
if (body.registration?.registryKey) {
|
|
199
210
|
lines.push(`Registry key: ${body.registration.registryKey}`);
|
|
200
211
|
}
|
|
212
|
+
if (body.registration?.kind === 'registry') {
|
|
213
|
+
lines.push(`Registry discovery: ${body.registration.status || 'unverified'}`);
|
|
214
|
+
if (body.registration.registeredManifestPath) lines.push(`Registered manifest: ${body.registration.registeredManifestPath}`);
|
|
215
|
+
}
|
|
201
216
|
if (body.bridge?.path) {
|
|
202
217
|
lines.push(`Bridge: ${body.bridge.path}`);
|
|
203
218
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { register } = require('./nativeRunLifecycle');
|
|
4
|
+
|
|
5
|
+
// Helpers own no project mirror lock, but their preparation and process-stop
|
|
6
|
+
// promises must finish before the Host's transport shutdown can finish.
|
|
7
|
+
async function runOwnedNativeHelper(execute, signal) {
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
const lifetime = register({ controller });
|
|
10
|
+
const onAbort = () => controller.abort(signal.reason);
|
|
11
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
12
|
+
if (signal?.aborted) onAbort();
|
|
13
|
+
try {
|
|
14
|
+
return await execute(controller.signal);
|
|
15
|
+
} finally {
|
|
16
|
+
signal?.removeEventListener('abort', onAbort);
|
|
17
|
+
lifetime.complete();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { runOwnedNativeHelper };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
// Query only the selected browser's current-user default registration value.
|
|
6
|
+
// An unreadable/missing value is unknown discovery evidence, never healthy.
|
|
7
|
+
function queryWindowsNativeHostRegistration(target, options = {}) {
|
|
8
|
+
const env = options.env || process.env;
|
|
9
|
+
const run = options.spawnSync || require('node:child_process').spawnSync;
|
|
10
|
+
let result;
|
|
11
|
+
try { result = run(env.CODEX_OVERLEAF_REG_EXE || 'reg.exe', [
|
|
12
|
+
...parseStringArrayEnv(env.CODEX_OVERLEAF_REG_EXE_ARGS_JSON), 'query', target.registryKey, '/ve'
|
|
13
|
+
], {
|
|
14
|
+
env, encoding: 'utf8', windowsHide: true, timeout: 3000, maxBuffer: 64 * 1024
|
|
15
|
+
}); } catch (_) { return { verified: false, status: 'unavailable' }; }
|
|
16
|
+
if (result.error || result.status !== 0) return { verified: false, status: 'unavailable' };
|
|
17
|
+
const values = [...String(result.stdout || '').matchAll(/^\s*.+?\s+REG_SZ\s+(.+?)\s*$/gm)].map(match => match[1]);
|
|
18
|
+
if (values.length !== 1 || !path.win32.isAbsolute(values[0])) return { verified: false, status: 'unavailable' };
|
|
19
|
+
const matches = path.win32.normalize(values[0]).toLowerCase() === path.win32.normalize(target.manifestPath).toLowerCase();
|
|
20
|
+
return { verified: matches, status: matches ? 'verified' : 'mismatch', registeredManifestPath: values[0] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Match the existing install/uninstall registry command prefix contract.
|
|
24
|
+
function parseStringArrayEnv(value) {
|
|
25
|
+
if (!value) return [];
|
|
26
|
+
const parsed = JSON.parse(value);
|
|
27
|
+
if (!Array.isArray(parsed) || !parsed.every(item => typeof item === 'string')) {
|
|
28
|
+
throw new Error('CODEX_OVERLEAF_REG_EXE_ARGS_JSON must be a JSON array of strings');
|
|
29
|
+
}
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { queryWindowsNativeHostRegistration };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const processStopBarrier = require('./processStopBarrier');
|
|
4
|
+
const { logDebug } = require('./debugLog');
|
|
5
|
+
const active = new Set();
|
|
6
|
+
let closingError;
|
|
7
|
+
|
|
8
|
+
// Shutdown ownership is independent of RPC ids: absent/reused ids must not
|
|
9
|
+
// hide a live run from transport shutdown or its durable quarantine.
|
|
10
|
+
function register({ controller, projectKey, rootDir }) {
|
|
11
|
+
let resolveDone;
|
|
12
|
+
const entry = { controller, projectKey, rootDir, processes: [], done: new Promise(resolve => { resolveDone = resolve; }) };
|
|
13
|
+
active.add(entry);
|
|
14
|
+
if (closingError) controller.abort(closingError);
|
|
15
|
+
return {
|
|
16
|
+
trackProcess(identity) {
|
|
17
|
+
if (!Number.isInteger(identity?.processId) || identity.processId <= 0) return;
|
|
18
|
+
entry.processes.push(identity);
|
|
19
|
+
if (closingError) quarantine(entry, identity);
|
|
20
|
+
},
|
|
21
|
+
complete() {
|
|
22
|
+
active.delete(entry);
|
|
23
|
+
try {
|
|
24
|
+
if (entry.processes.some(identity => identity.quarantined)) processStopBarrier.isBlocked(projectKey, rootDir);
|
|
25
|
+
} catch (error) { logDebug('native_transport.quarantine_cleanup_failed', { projectKey, message: error.message }); }
|
|
26
|
+
finally { resolveDone(); }
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function abortAllActiveOperations(reason = 'Native messaging transport disconnected.') {
|
|
32
|
+
closingError = new Error(String(reason || 'Native messaging transport disconnected.'));
|
|
33
|
+
closingError.code = 'native_transport_closed';
|
|
34
|
+
const entries = Array.from(active);
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (!entry.controller.signal.aborted) entry.controller.abort(closingError);
|
|
37
|
+
}
|
|
38
|
+
return Promise.all(entries.map(entry => entry.done));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function quarantineActiveOperations() {
|
|
42
|
+
const failures = [];
|
|
43
|
+
for (const entry of active) {
|
|
44
|
+
// A preparing run has no process to quarantine. Its aborted signal is
|
|
45
|
+
// checked again after provider preparation and immediately before spawn.
|
|
46
|
+
for (const identity of entry.processes) {
|
|
47
|
+
const failure = quarantine(entry, identity);
|
|
48
|
+
if (failure) failures.push(failure);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return failures;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function quarantine(entry, identity) {
|
|
55
|
+
if (identity.quarantined) return null;
|
|
56
|
+
const error = Object.assign(new Error('Native transport closed before run settlement.'), identity, {
|
|
57
|
+
code: 'codex_process_stop_unconfirmed'
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
processStopBarrier.record(entry.projectKey, entry.rootDir, error);
|
|
61
|
+
if (error.stopBarrierPersistenceError) throw new Error(error.stopBarrierPersistenceError);
|
|
62
|
+
identity.quarantined = true;
|
|
63
|
+
return null;
|
|
64
|
+
} catch (failure) {
|
|
65
|
+
const detail = { projectKey: entry.projectKey, message: failure.message };
|
|
66
|
+
logDebug('native_transport.quarantine_failed', detail);
|
|
67
|
+
return detail;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { register, abortAllActiveOperations, quarantineActiveOperations };
|