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.
Files changed (168) hide show
  1. package/README.md +38 -26
  2. package/README.zh-CN.md +39 -27
  3. package/extension/bootstrap/background.js +52 -9
  4. package/extension/bootstrap/manifest.template.json +1 -1
  5. package/extension/bootstrap/popup.html +1 -0
  6. package/extension/bootstrap/popupFallback.js +55 -0
  7. package/extension/bootstrap/updateStatus.js +1 -0
  8. package/extension/src/background.js +206 -53
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +74 -14
  11. package/extension/src/backgroundUpdateCoordinator.js +118 -21
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +8 -2
  14. package/extension/src/content/applyResultFormatters.js +46 -73
  15. package/extension/src/content/assetTransferBroker.js +72 -14
  16. package/extension/src/content/composerAttachments.js +93 -45
  17. package/extension/src/content/composerPanel.js +18 -0
  18. package/extension/src/content/contentRuntime.js +661 -676
  19. package/extension/src/content/diagnosticsController.js +122 -96
  20. package/extension/src/content/diagnosticsExport.js +19 -5
  21. package/extension/src/content/diagnosticsPanel.js +30 -0
  22. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  23. package/extension/src/content/generated/content.bundle.js +6879 -2786
  24. package/extension/src/content/generated/content.bundle.meta.json +11 -3
  25. package/extension/src/content/globalPreferencesController.js +1 -1
  26. package/extension/src/content/legacyGlobalRegistry.js +6 -0
  27. package/extension/src/content/localSkillsPanel.js +102 -12
  28. package/extension/src/content/markdownText.js +5 -6
  29. package/extension/src/content/modelPicker.js +7 -4
  30. package/extension/src/content/nativeCompatibilityController.js +45 -20
  31. package/extension/src/content/otWarmMirror.js +4 -5
  32. package/extension/src/content/otWarmMirrorController.js +17 -20
  33. package/extension/src/content/pageBridgeClient.js +15 -2
  34. package/extension/src/content/panelMaintenance.js +40 -12
  35. package/extension/src/content/panelRenderer.js +6 -0
  36. package/extension/src/content/projectProviderSelection.js +13 -4
  37. package/extension/src/content/projectSessionCleanup.js +109 -62
  38. package/extension/src/content/projectSettingsCoordinator.js +39 -2
  39. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  40. package/extension/src/content/providerSettingsDialog.js +71 -13
  41. package/extension/src/content/recentProjects.js +11 -5
  42. package/extension/src/content/runActivitySummary.js +40 -6
  43. package/extension/src/content/runCompletionMeta.js +344 -0
  44. package/extension/src/content/runController.js +37 -14
  45. package/extension/src/content/runFailureNotice.js +21 -12
  46. package/extension/src/content/runGuidanceController.js +72 -27
  47. package/extension/src/content/runGuidanceView.js +59 -10
  48. package/extension/src/content/runQueueScheduler.js +148 -42
  49. package/extension/src/content/runRecoveryActions.js +146 -0
  50. package/extension/src/content/runResultActions.js +8 -125
  51. package/extension/src/content/runReviewController.js +574 -0
  52. package/extension/src/content/runScrollLayout.js +21 -2
  53. package/extension/src/content/runTimelineView.js +146 -153
  54. package/extension/src/content/scopedPersistencePanelState.js +51 -6
  55. package/extension/src/content/selectionContextView.js +1 -1
  56. package/extension/src/content/sensitivePreflight.js +140 -0
  57. package/extension/src/content/sessionForkController.js +80 -17
  58. package/extension/src/content/sessionManager.js +1 -1
  59. package/extension/src/content/settingsPanel.js +7 -3
  60. package/extension/src/content/settingsWorkbench.js +5 -0
  61. package/extension/src/content/sharedSessionViewSync.js +45 -8
  62. package/extension/src/content/updateIdle.js +4 -1
  63. package/extension/src/content/updateNotice.js +60 -8
  64. package/extension/src/content/updateSafetyReceiver.js +58 -0
  65. package/extension/src/content/writebackController.js +53 -4
  66. package/extension/src/content/writebackOrchestrator.js +82 -73
  67. package/extension/src/content/writingStyleSettings.js +35 -11
  68. package/extension/src/page/binaryAssetUploader.js +136 -14
  69. package/extension/src/page/fileMover.js +256 -0
  70. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  71. package/extension/src/page/referenceProjects.js +1 -0
  72. package/extension/src/page/reviewContext.js +113 -0
  73. package/extension/src/page/saveState.js +6 -4
  74. package/extension/src/page/snapshotRouter.js +84 -39
  75. package/extension/src/page/textFileCreator.js +105 -25
  76. package/extension/src/page/trackedChangeCapture.js +12 -2
  77. package/extension/src/page/trackedChangeOwnership.js +19 -1
  78. package/extension/src/page/trackedChangeReplay.js +41 -21
  79. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  80. package/extension/src/page/treeOperations.js +7 -10
  81. package/extension/src/page/updateSafetyProbe.js +41 -0
  82. package/extension/src/page/writebackReceiptJournal.js +2 -0
  83. package/extension/src/page/writebackRouter.js +80 -79
  84. package/extension/src/pageBridge.js +19 -3
  85. package/extension/src/popup.js +62 -10
  86. package/extension/src/shared/agentTranscript.js +23 -0
  87. package/extension/src/shared/auditRecords.js +2 -0
  88. package/extension/src/shared/compatibility.js +22 -6
  89. package/extension/src/shared/compileAdapter.js +79 -57
  90. package/extension/src/shared/composerAttachmentDraft.js +151 -0
  91. package/extension/src/shared/directoryCleanupProof.js +43 -0
  92. package/extension/src/shared/failureReasons.js +309 -3
  93. package/extension/src/shared/globalPreferences.js +30 -13
  94. package/extension/src/shared/governanceRules.js +4 -37
  95. package/extension/src/shared/i18n.js +298 -2
  96. package/extension/src/shared/managedUpdateProjection.js +2 -2
  97. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  98. package/extension/src/shared/pageRpcContract.js +5 -4
  99. package/extension/src/shared/projectFiles.js +21 -44
  100. package/extension/src/shared/providerProfiles.js +5 -3
  101. package/extension/src/shared/runInputQueue.js +48 -5
  102. package/extension/src/shared/selectionContext.js +12 -3
  103. package/extension/src/shared/sessionState.js +72 -27
  104. package/extension/src/shared/settlementFacts.js +10 -2
  105. package/extension/src/shared/sharedSessionBridge.js +37 -14
  106. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  107. package/extension/src/shared/staleGuard.js +118 -13
  108. package/extension/src/shared/storageDb.js +58 -26
  109. package/extension/src/shared/storageMigration.js +71 -111
  110. package/extension/src/shared/storageRunActions.js +209 -26
  111. package/extension/src/shared/subagentActivityModel.js +1 -1
  112. package/extension/src/shared/undoOperations.js +136 -13
  113. package/extension/src/shared/updateConsent.js +6 -4
  114. package/extension/src/shared/updateRuntimeIdentity.js +1 -1
  115. package/extension/src/shared/writebackIntent.js +16 -0
  116. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  117. package/extension/src/shared/writebackSettlement.js +38 -38
  118. package/extension/styles/panel.css +19 -5
  119. package/native-host/src/anthropicBridgeRequest.js +4 -2
  120. package/native-host/src/anthropicBridgeResponse.js +6 -2
  121. package/native-host/src/anthropicMessagesBridge.js +24 -16
  122. package/native-host/src/bootstrapMigration.js +296 -0
  123. package/native-host/src/bootstrapMigrationContract.js +101 -0
  124. package/native-host/src/bootstrapUpdate.js +233 -0
  125. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  126. package/native-host/src/chatBridgeResponse.js +10 -0
  127. package/native-host/src/chatCompletionsBridge.js +21 -15
  128. package/native-host/src/codexConfigPaths.js +106 -0
  129. package/native-host/src/codexHome.js +66 -73
  130. package/native-host/src/codexHomeScope.js +124 -0
  131. package/native-host/src/codexInheritedAuth.js +90 -0
  132. package/native-host/src/codexLocalSkills.js +301 -0
  133. package/native-host/src/codexPromptAssembly.js +31 -12
  134. package/native-host/src/codexProviderTest.js +24 -26
  135. package/native-host/src/codexSessionRunner.js +110 -183
  136. package/native-host/src/codexSessionTiming.js +18 -8
  137. package/native-host/src/codexSkillIsolation.js +170 -0
  138. package/native-host/src/codexThreadFork.js +19 -3
  139. package/native-host/src/index.js +19 -7
  140. package/native-host/src/managedInstall.js +110 -35
  141. package/native-host/src/managedInstallRecovery.js +140 -0
  142. package/native-host/src/managedRuntimeEvidence.js +57 -0
  143. package/native-host/src/mirrorWorkspace.js +92 -22
  144. package/native-host/src/nativeDoctor.js +15 -0
  145. package/native-host/src/nativeHelperLifecycle.js +21 -0
  146. package/native-host/src/nativeHostRegistration.js +33 -0
  147. package/native-host/src/nativeRunLifecycle.js +71 -0
  148. package/native-host/src/processStopBarrier.js +42 -7
  149. package/native-host/src/projectPathPlatform.js +30 -0
  150. package/native-host/src/providerHttp.js +80 -0
  151. package/native-host/src/providerProfile.js +4 -2
  152. package/native-host/src/providerReasoning.js +2 -7
  153. package/native-host/src/providerRuntime.js +1 -0
  154. package/native-host/src/providerStore.js +72 -14
  155. package/native-host/src/responsesPassthroughBridge.js +266 -52
  156. package/native-host/src/runtimeInstaller.js +21 -1
  157. package/native-host/src/selectionScope.js +36 -8
  158. package/native-host/src/subagentAdmission.js +26 -0
  159. package/native-host/src/subagentBroker.js +79 -66
  160. package/native-host/src/subagentTelemetry.js +19 -10
  161. package/native-host/src/subagentWorkspacePath.js +5 -2
  162. package/native-host/src/taskRunnerRuntime.js +77 -54
  163. package/native-host/src/tomlStatements.js +140 -0
  164. package/native-host/src/updateManager.js +297 -39
  165. package/native-host/src/writingStyleRuntime.js +57 -29
  166. package/native-host/src/writingStyleStore.js +73 -0
  167. package/package.json +1 -1
  168. package/scripts/install-managed.mjs +1 -0
@@ -0,0 +1,170 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { getCodexOverleafSkillsRoot } = require('./localSkills');
6
+ const { createCodexLocalSkillIdentity } = require('./codexLocalSkills');
7
+
8
+ async function applyCodexSkillIsolation({ input = {}, childEnv = process.env, request, emit = () => {} } = {}) {
9
+ if (input.loadCodexLocalSkills !== false && input.loadCodexOverleafSkills !== false
10
+ && !Array.isArray(input.enabledCodexOverleafSkillIds) && !Array.isArray(input.disableCodexOverleafSkillIds)
11
+ && !Array.isArray(input.enabledCodexLocalSkillIds)) {
12
+ return { disabled: [] };
13
+ }
14
+ if (typeof request !== 'function') {
15
+ throw new Error('Codex skill isolation requires an app-server request function');
16
+ }
17
+
18
+ const listResult = await request('skills/list', {
19
+ cwd: input.workspacePath,
20
+ includeDisabled: true,
21
+ ...(input.workspacePath ? { cwds: [input.workspacePath] } : {}),
22
+ forceReload: true
23
+ });
24
+ if (Array.isArray(input.enabledCodexLocalSkillIds) && (!Array.isArray(listResult?.data)
25
+ || listResult.data.some(group => !Array.isArray(group?.skills)
26
+ || (group.errors !== undefined && !Array.isArray(group.errors))))) {
27
+ throw Object.assign(new Error('Local Codex skills could not be prepared. Refresh the skill list and try again.'),
28
+ { code: 'codex_local_skill_unavailable' });
29
+ }
30
+ const skills = flattenCodexSkillsList(listResult);
31
+ const disabled = [];
32
+ const enabled = [];
33
+ for (const skill of skills) {
34
+ const localSelection = getLocalSkillSelection(skill, input, childEnv);
35
+ const shouldDisable = shouldDisableCodexSkillForIsolation(skill, input, childEnv);
36
+ if (localSelection === true && skill?.enabled === false) {
37
+ const params = buildSkillDisableParams(skill, { preferPath: true });
38
+ if (params) {
39
+ await request('skills/config/write', { ...params, enabled: true });
40
+ enabled.push(String(skill.name || skill.path || '').trim());
41
+ }
42
+ continue;
43
+ }
44
+ if (skill?.enabled === false || !shouldDisable) {
45
+ continue;
46
+ }
47
+ const params = buildSkillDisableParams(skill, { preferPath: localSelection !== null });
48
+ if (!params) {
49
+ continue;
50
+ }
51
+ await request('skills/config/write', params);
52
+ disabled.push(String(skill.name || skill.path || '').trim());
53
+ }
54
+ if (disabled.length) {
55
+ emitCodexEvent(emit, 'codex.skill_isolation.applied', 'Applied Codex skill loading settings for this turn', {
56
+ disabledSkillNames: disabled.filter(Boolean)
57
+ }, 'completed');
58
+ }
59
+ return { disabled, enabled };
60
+ }
61
+
62
+ function getLocalSkillSelection(skill, input, childEnv) {
63
+ if (!Array.isArray(input.enabledCodexLocalSkillIds)
64
+ || getOverleafSkillId(skill.path, input, childEnv)) return null;
65
+ if (isCodexSystemSkill(skill) && isAllowedSystemSkillForIsolation(skill, input)) return true;
66
+ if (input.loadCodexLocalSkills === false) return false;
67
+ const id = createCodexLocalSkillIdentity(skill, childEnv);
68
+ return Boolean(id) && input.enabledCodexLocalSkillIds.includes(id);
69
+ }
70
+
71
+ function flattenCodexSkillsList(listResult = {}) {
72
+ const data = Array.isArray(listResult?.data) ? listResult.data : [];
73
+ return data.flatMap(entry => Array.isArray(entry?.skills) ? entry.skills : []);
74
+ }
75
+
76
+ function shouldDisableCodexSkillForIsolation(skill = {}, input = {}, childEnv = process.env) {
77
+ const localSelection = getLocalSkillSelection(skill, input, childEnv);
78
+ if (localSelection !== null) return !localSelection;
79
+ if (isCodexSystemSkill(skill)) {
80
+ return input.loadCodexLocalSkills === false && !isAllowedSystemSkillForIsolation(skill, input);
81
+ }
82
+ const id = getOverleafSkillId(skill.path, input, childEnv);
83
+ if (id && input.installCodexOverleafSkillsTarget !== true) {
84
+ if (input.loadCodexOverleafSkills === false) return true;
85
+ if (Array.isArray(input.enabledCodexOverleafSkillIds) && !input.enabledCodexOverleafSkillIds.includes(id)) return true;
86
+ if (Array.isArray(input.disableCodexOverleafSkillIds) && input.disableCodexOverleafSkillIds.includes(id)) return true;
87
+ }
88
+ return input.loadCodexLocalSkills === false && !isAllowedCodexOverleafSkillPath(skill.path, input, childEnv);
89
+ }
90
+
91
+ function getOverleafSkillId(skillPath, input, childEnv) {
92
+ if (!skillPath || !path.isAbsolute(String(skillPath))) return '';
93
+ const roots = [getCodexOverleafSkillsRoot({ env: childEnv })];
94
+ // A composed home with local inheritance disabled contains only plugin or
95
+ // explicitly materialized project skills. Real paths identify plugin aliases.
96
+ if (input.loadCodexLocalSkills === false) roots.push(path.join(String(childEnv.CODEX_HOME || ''), 'skills'));
97
+ for (const root of roots) for (const base of comparablePaths(root)) for (const candidate of comparablePaths(skillPath)) {
98
+ const relative = path.relative(base, candidate);
99
+ if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) return relative.split(path.sep)[0];
100
+ }
101
+ return '';
102
+ }
103
+
104
+ function isCodexSystemSkill(skill = {}) {
105
+ return String(skill.scope || '') === 'system' || isSystemSkillPath(skill.path);
106
+ }
107
+
108
+ function isAllowedSystemSkillForIsolation(skill = {}, input = {}) {
109
+ return input.installCodexOverleafSkillsTarget === true && String(skill.name || '') === 'skill-installer';
110
+ }
111
+
112
+ function isAllowedCodexOverleafSkillPath(skillPath, input = {}, childEnv = process.env) {
113
+ if (input.loadCodexOverleafSkills === false && input.installCodexOverleafSkillsTarget !== true) {
114
+ return false;
115
+ }
116
+ const pathText = String(skillPath || '');
117
+ if (!pathText || !path.isAbsolute(pathText) || isSystemSkillPath(pathText)) {
118
+ return false;
119
+ }
120
+ const roots = [
121
+ path.join(String(childEnv.CODEX_HOME || ''), 'skills'),
122
+ getCodexOverleafSkillsRoot({ env: childEnv })
123
+ ].filter(Boolean);
124
+ return roots.some(root => isInsideOrSamePath(pathText, root));
125
+ }
126
+
127
+ function isSystemSkillPath(skillPath) {
128
+ return String(skillPath || '').split(path.sep).includes('.system');
129
+ }
130
+
131
+ function buildSkillDisableParams(skill = {}, { preferPath = false } = {}) {
132
+ const name = String(skill.name || '').trim();
133
+ const skillPath = String(skill.path || '').trim();
134
+ if (preferPath && path.isAbsolute(skillPath)) return { path: skillPath, enabled: false };
135
+ if (isCodexSystemSkill(skill) && name) {
136
+ return { name, enabled: false };
137
+ }
138
+ if (path.isAbsolute(skillPath)) {
139
+ return { path: skillPath, enabled: false };
140
+ }
141
+ if (name) {
142
+ return { name, enabled: false };
143
+ }
144
+ return null;
145
+ }
146
+
147
+ function isInsideOrSamePath(target, root) {
148
+ const targetPaths = comparablePaths(target);
149
+ const rootPaths = comparablePaths(root);
150
+ return targetPaths.some(targetPath => rootPaths.some(rootPath => (
151
+ targetPath === rootPath || targetPath.startsWith(rootPath + path.sep)
152
+ )));
153
+ }
154
+
155
+ function comparablePaths(value) {
156
+ const resolved = path.resolve(String(value || ''));
157
+ const candidates = [resolved];
158
+ try {
159
+ candidates.push(fs.realpathSync.native(resolved));
160
+ } catch (_) {
161
+ // Fall back to the lexical path when the file is not present yet.
162
+ }
163
+ return Array.from(new Set(candidates));
164
+ }
165
+
166
+ function emitCodexEvent(emit, type, title, detail = {}, status = 'running') {
167
+ emit({ type, title, status, detail, timestamp: new Date().toISOString() });
168
+ }
169
+
170
+ module.exports = { applyCodexSkillIsolation };
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { runOwnedNativeHelper } = require('./nativeHelperLifecycle');
4
+ const { stopCodexAppServer, throwIfAborted } = require('./codexSessionTiming');
3
5
  const { spawn } = require('node:child_process');
4
6
  const { version: PACKAGE_VERSION } = require('../../package.json');
5
7
  const { buildCodexHomeEnv } = require('./codexHome');
@@ -7,9 +9,17 @@ const { resolveCodexCommand, shouldUseShellForCommand } = require('./codexComman
7
9
  const { buildCodexAppServerArgs } = require('./codexSessionRunner');
8
10
 
9
11
  function forkCodexThread(params = {}, env = process.env) {
12
+ return runOwnedNativeHelper(signal => runForkProcess(params, env, signal));
13
+ }
14
+
15
+ function runForkProcess(params, env, signal) {
16
+ throwIfAborted(signal);
10
17
  const threadId = cleanId(params.threadId, 'threadId');
11
18
  const lastTurnId = cleanId(params.lastTurnId, 'lastTurnId');
12
19
  const childEnv = buildCodexHomeEnv(env, {
20
+ isolateRuntimeHome: true,
21
+ enabledCodexOverleafSkillIds: params.enabledCodexOverleafSkillIds,
22
+ enabledCodexLocalSkillIds: params.enabledCodexLocalSkillIds,
13
23
  loadCodexLocalSkills: params.loadCodexLocalSkills !== false,
14
24
  loadCodexOverleafSkills: params.loadCodexOverleafSkills !== false
15
25
  });
@@ -23,6 +33,7 @@ function forkCodexThread(params = {}, env = process.env) {
23
33
  }), {
24
34
  env: childEnv,
25
35
  shell: shouldUseShellForCommand(command, childEnv),
36
+ detached: process.platform !== 'win32',
26
37
  stdio: ['pipe', 'pipe', 'pipe']
27
38
  });
28
39
  const pending = new Map();
@@ -30,6 +41,8 @@ function forkCodexThread(params = {}, env = process.env) {
30
41
  let buffer = '';
31
42
  let stderr = '';
32
43
  let settled = false;
44
+ const onAbort = () => finish(signal.reason);
45
+ signal.addEventListener('abort', onAbort, { once: true });
33
46
  const timeout = setTimeout(() => finish(codedError('codex_thread_fork_timeout', 'Codex thread fork timed out.')), 30000);
34
47
 
35
48
  child.stdout.setEncoding('utf8');
@@ -41,12 +54,14 @@ function forkCodexThread(params = {}, env = process.env) {
41
54
  for (const line of lines) if (line.trim()) handleMessage(line);
42
55
  });
43
56
  child.stderr.on('data', chunk => { stderr += chunk; });
57
+ child.stdin.on('error', finish);
44
58
  child.on('error', finish);
45
59
  child.on('close', code => {
46
60
  if (!settled) finish(codedError('codex_thread_fork_failed', stderr || `Codex app-server exited with code ${code}.`));
47
61
  });
48
62
 
49
63
  (async () => {
64
+ throwIfAborted(signal);
50
65
  await request('initialize', {
51
66
  clientInfo: { name: 'codex-overleaf-link', version: PACKAGE_VERSION },
52
67
  capabilities: null
@@ -85,11 +100,12 @@ function forkCodexThread(params = {}, env = process.env) {
85
100
  if (settled) return;
86
101
  settled = true;
87
102
  clearTimeout(timeout);
103
+ signal.removeEventListener('abort', onAbort);
88
104
  for (const waiter of pending.values()) waiter.reject(error || codedError('codex_thread_fork_closed', 'Fork process closed.'));
89
105
  pending.clear();
90
- try { child.stdin.end(); } catch (_error) { /* best effort */ }
91
- try { child.kill(); } catch (_error) { /* best effort */ }
92
- if (error) reject(error); else resolve(result);
106
+ stopCodexAppServer(child, { processGroup: process.platform !== 'win32', reason: error }).then(() => {
107
+ if (error) reject(error); else resolve(result);
108
+ }, reject);
93
109
  }
94
110
  });
95
111
  }
@@ -4,8 +4,8 @@
4
4
  const { decodeInputFrames, encodeOutputFrame } = require('./nativeTransportEnvelope');
5
5
  const { logDebug } = require('./debugLog');
6
6
  const { handleRequest } = require('./taskRunner');
7
- const { abortAllActiveOperations, getActiveNativeWorkState } = require('./taskRunnerRuntime');
8
- const { handleUpdateRequest, isUpdateMethod } = require('./updateManager');
7
+ const { abortAllActiveOperations, quarantineActiveOperations, getActiveNativeWorkState } = require('./taskRunnerRuntime');
8
+ const { handleUpdateRequest, isUpdateMethod, assertBootstrapBusinessAvailable } = require('./updateManager');
9
9
  const { buildNativeRuntimeEnv, summarizeNativeEnvironment } = require('./nativeEnvironment');
10
10
 
11
11
  let buffered = Buffer.alloc(0);
@@ -41,12 +41,14 @@ process.stdin.on('data', chunk => {
41
41
  });
42
42
 
43
43
  async function handleDecodedMessage(message) {
44
+ if (nativeDisconnectHandled) return;
44
45
  try {
45
46
  logDebug('request.received', summarizeRequest(message));
47
+ assertBootstrapBusinessAvailable(message, { env: runtimeEnv });
46
48
  const response = isUpdateMethod(message?.method)
47
49
  ? await handleUpdateRequest(message, {
48
50
  env: runtimeEnv,
49
- getWorkState: getActiveNativeWorkState
51
+ getWorkState: () => getActiveNativeWorkState(runtimeEnv)
50
52
  })
51
53
  : await handleRequest(message, runtimeEnv, event => {
52
54
  writeResponse({
@@ -62,7 +64,7 @@ async function handleDecodedMessage(message) {
62
64
  id: message?.id,
63
65
  ok: false,
64
66
  error: {
65
- code: 'internal_error',
67
+ code: /^[a-z0-9_]{1,80}$/.test(String(error?.code || '')) ? error.code : 'internal_error',
66
68
  message: error.message
67
69
  }
68
70
  };
@@ -145,10 +147,20 @@ function handleNativeDisconnect(source, error) {
145
147
  code: error?.code,
146
148
  message: error?.message
147
149
  });
148
- abortAllActiveOperations('Native messaging transport disconnected.');
149
150
  process.exitCode = 0;
150
- const exitTimer = setTimeout(() => process.exit(0), 1500);
151
- exitTimer.unref?.();
151
+ // Publish protection before stopping: another Host may reconnect while the
152
+ // old owned process is still inside its interrupt/termination budget.
153
+ quarantineActiveOperations();
154
+ const exitTimer = setTimeout(() => {
155
+ const failures = quarantineActiveOperations();
156
+ logDebug('native_transport.shutdown_deadline', { persistenceFailures: failures });
157
+ process.exit(failures.length ? 1 : 0);
158
+ }, 5000);
159
+ // Keep the prior grace for other entry points while owned Codex runs drain.
160
+ Promise.all([
161
+ abortAllActiveOperations('Native messaging transport disconnected.'),
162
+ new Promise(resolve => setTimeout(resolve, 1500))
163
+ ]).then(() => { clearTimeout(exitTimer); process.exit(0); });
152
164
  }
153
165
 
154
166
  function summarizeRequest(message) {
@@ -18,8 +18,11 @@ const {
18
18
  getDefaultManagedRoot,
19
19
  getNativeHostRegistrationTarget
20
20
  } = require('./nativeHostPlatform');
21
+ const { queryWindowsNativeHostRegistration } = require('./nativeHostRegistration');
21
22
  const { buildRuntimeFileManifest } = require('./runtimeInstaller');
22
23
  const { BOOTSTRAP_PROTOCOL, parseSemver, updateError } = require('./updateTrust');
24
+ const { createInstallRestorePoint } = require('./managedInstallRecovery');
25
+ const { settleReinstalledUpdate } = require('./updateManager');
23
26
 
24
27
  const MANAGED_BY = 'codex-overleaf-link';
25
28
  const EXTENSION_MARKER = '.codex-overleaf-managed-extension.json';
@@ -61,51 +64,91 @@ function installManagedDistribution(options = {}) {
61
64
  version,
62
65
  releaseRef,
63
66
  releaseChannel,
64
- allowedFiles: options.allowedFiles
67
+ allowedFiles: options.allowedFiles,
68
+ installIdentity: { managedRoot, browser: browser === 'auto' ? 'chrome' : browser, extensionId, platform }
65
69
  });
66
70
  } catch (error) {
67
71
  safeRemove(extensionStage);
68
72
  throw error;
69
73
  }
70
74
 
71
- const previousExtensionMarker = readManagedMarker(extensionRoot, EXTENSION_MARKER);
75
+ let previousExtensionMarker = readManagedMarker(extensionRoot, EXTENSION_MARKER);
72
76
  if (fs.existsSync(extensionRoot) && !isManagedMarker(previousExtensionMarker, 'extension')) {
73
77
  safeRemove(extensionStage);
74
78
  throw new Error('Refusing to replace an unmarked managed extension root: ' + extensionRoot);
75
79
  }
76
80
  let extensionRollbackCreated = false;
81
+ let restorePoint;
82
+ let previousTransaction;
83
+ const registration = getNativeHostRegistrationTarget({ platform, browser, env });
84
+ const bridgePath = getDefaultBridgePath({ platform, browser, env });
85
+ let previousRegistration = null;
86
+ let registrationAttempted = false;
77
87
  try {
88
+ const nativeMarker = readManagedMarker(nativeRoot, NATIVE_MARKER);
89
+ if (fs.existsSync(nativeRoot) && !isManagedMarker(nativeMarker, 'native')) {
90
+ throw new Error('Refusing to replace an unmarked managed native root: ' + nativeRoot);
91
+ }
92
+ restorePoint = createInstallRestorePoint({ nativeRoot, version, files: [
93
+ path.join(nativeRoot, 'active-version'), path.join(nativeRoot, 'previous-version'),
94
+ path.join(nativeRoot, NATIVE_MARKER), path.join(nativeRoot, 'bootstrap', 'launcher.js'),
95
+ path.join(nativeRoot, 'updates', 'transaction.json'), path.join(nativeRoot, 'updates', 'authorization.json'),
96
+ path.join(managedRoot, '.codex-overleaf-managed.json'), bridgePath, registration.manifestPath
97
+ ] });
98
+ previousExtensionMarker = readManagedMarker(extensionRoot, EXTENSION_MARKER);
99
+ if (fs.existsSync(extensionRoot) && !isManagedMarker(previousExtensionMarker, 'extension')) {
100
+ throw new Error('Refusing to replace an unmarked managed extension root: ' + extensionRoot);
101
+ }
102
+ previousRegistration = registration.kind === 'registry'
103
+ ? queryWindowsNativeHostRegistration(registration, { env }) : null;
104
+ previousTransaction = readManagedMarker(path.join(nativeRoot, 'updates'), 'transaction.json');
78
105
  if (fs.existsSync(extensionRoot)) {
79
106
  fs.renameSync(extensionRoot, extensionRollback);
80
107
  extensionRollbackCreated = true;
81
108
  }
82
109
  fs.renameSync(extensionStage, extensionRoot);
83
- } catch (error) {
84
- safeRemove(extensionStage);
85
- if (extensionRollbackCreated && !fs.existsSync(extensionRoot)) {
86
- fs.renameSync(extensionRollback, extensionRoot);
87
- }
88
- throw error;
89
- }
90
-
91
- try {
92
- installManagedNativeVersion({ packageRoot, nativeRoot, version, releaseRef, releaseChannel });
110
+ installManagedNativeVersion({ packageRoot, nativeRoot, version, releaseRef, releaseChannel,
111
+ allowUnmarkedRoot: !restorePoint.nativeExisted });
93
112
  installManagedNativeRegistration({
94
113
  browser,
95
114
  env,
96
115
  extensionId,
97
116
  managedRoot,
98
117
  nativeRoot,
99
- platform
118
+ platform,
119
+ onRegistryMutation: () => { registrationAttempted = true; }
100
120
  });
101
- safeRemove(extensionRollback);
121
+ settleReinstalledUpdate({ managed: true, nativeRoot, extensionRoot,
122
+ updatesRoot: path.join(nativeRoot, 'updates'), env }, version, previousTransaction);
102
123
  } catch (error) {
103
- safeRemove(extensionRoot);
104
- if (extensionRollbackCreated && fs.existsSync(extensionRollback)) {
105
- fs.renameSync(extensionRollback, extensionRoot);
124
+ const failures = [];
125
+ try {
126
+ safeRemove(extensionStage);
127
+ if (extensionRollbackCreated && fs.existsSync(extensionRollback)) {
128
+ safeRemove(extensionRoot);
129
+ fs.renameSync(extensionRollback, extensionRoot);
130
+ } else if (!previousExtensionMarker && restorePoint) safeRemove(extensionRoot);
131
+ } catch (failure) { failures.push(failure); }
132
+ if (restorePoint) {
133
+ try { restorePoint.restore(); } catch (failure) { failures.push(failure); }
134
+ if (registrationAttempted && registration.kind === 'registry') {
135
+ try {
136
+ if (!previousRegistration?.registeredManifestPath) throw new Error('Previous browser registration could not be verified.');
137
+ runRegistry(['add', registration.registryKey, '/ve', '/t', 'REG_SZ', '/d', previousRegistration.registeredManifestPath, '/f']);
138
+ } catch (failure) { failures.push(failure); }
139
+ }
140
+ try { failures.length ? restorePoint.release() : restorePoint.finish(); }
141
+ catch (failure) { failures.push(failure); }
106
142
  }
143
+ if (failures.length) throw updateError('managed_install_restore_failed',
144
+ 'Managed installation failed and the previous installation could not be fully restored.', { cause: error });
107
145
  throw error;
108
146
  }
147
+ // The installed pair is committed. Cleanup errors must not trigger a second
148
+ // rollback after some of its retained files have already been removed.
149
+ const warnings = [];
150
+ try { safeRemove(extensionRollback); } catch (_) { warnings.push('Previous extension cleanup is incomplete.'); }
151
+ try { restorePoint.finish(); } catch (_) { warnings.push('Previous Native installation cleanup is incomplete.'); }
109
152
 
110
153
  return {
111
154
  ok: true,
@@ -117,7 +160,8 @@ function installManagedDistribution(options = {}) {
117
160
  extensionRoot,
118
161
  nativeRoot,
119
162
  loadUnpackedPath: extensionRoot,
120
- allowedOrigin: buildAllowedOrigin(extensionId)
163
+ allowedOrigin: buildAllowedOrigin(extensionId),
164
+ ...(warnings.length ? { warning: warnings.join(' ') } : {})
121
165
  };
122
166
  }
123
167
 
@@ -154,6 +198,7 @@ function buildManagedExtensionTree(options = {}) {
154
198
  fs.writeFileSync(path.join(targetRoot, EXTENSION_MARKER), JSON.stringify({
155
199
  managedBy: MANAGED_BY,
156
200
  kind: 'extension',
201
+ ...(options.installIdentity ? { installIdentity: options.installIdentity } : {}),
157
202
  bootstrapProtocol: BOOTSTRAP_PROTOCOL,
158
203
  version,
159
204
  releaseRef,
@@ -163,9 +208,9 @@ function buildManagedExtensionTree(options = {}) {
163
208
  return { targetRoot, version };
164
209
  }
165
210
 
166
- function installManagedNativeVersion({ packageRoot, nativeRoot, version, releaseRef = `v${version}`, releaseChannel = 'stable' }) {
211
+ function installManagedNativeVersion({ packageRoot, nativeRoot, version, releaseRef = `v${version}`, releaseChannel = 'stable', allowUnmarkedRoot = false }) {
167
212
  const marker = readManagedMarker(nativeRoot, NATIVE_MARKER);
168
- if (fs.existsSync(nativeRoot) && !isManagedMarker(marker, 'native')) {
213
+ if (fs.existsSync(nativeRoot) && !isManagedMarker(marker, 'native') && !allowUnmarkedRoot) {
169
214
  throw new Error('Refusing to replace an unmarked managed native root: ' + nativeRoot);
170
215
  }
171
216
  fs.mkdirSync(path.join(nativeRoot, 'versions'), { recursive: true });
@@ -174,18 +219,21 @@ function installManagedNativeVersion({ packageRoot, nativeRoot, version, release
174
219
 
175
220
  const stage = path.join(nativeRoot, 'versions', '.' + version + '.staging-' + process.pid + '-' + Date.now());
176
221
  const target = path.join(nativeRoot, 'versions', version);
177
- for (const entry of buildRuntimeFileManifest({ packageRoot })) {
178
- copyFile(entry.sourcePath, path.join(stage, entry.relativePath));
179
- }
180
- fs.writeFileSync(path.join(stage, '.codex-overleaf-version.json'), JSON.stringify({
181
- version,
182
- releaseRef,
183
- releaseChannel
184
- }, null, 2) + '\n');
185
- if (fs.existsSync(target)) {
186
- safeRemove(target);
222
+ try {
223
+ for (const entry of buildRuntimeFileManifest({ packageRoot })) {
224
+ copyFile(entry.sourcePath, path.join(stage, entry.relativePath));
225
+ }
226
+ fs.writeFileSync(path.join(stage, '.codex-overleaf-version.json'), JSON.stringify({
227
+ version,
228
+ releaseRef,
229
+ releaseChannel
230
+ }, null, 2) + '\n');
231
+ if (fs.existsSync(target)) safeRemove(target);
232
+ fs.renameSync(stage, target);
233
+ } catch (error) {
234
+ safeRemove(stage);
235
+ throw error;
187
236
  }
188
- fs.renameSync(stage, target);
189
237
 
190
238
  copyFile(
191
239
  path.join(packageRoot, 'native-host/src/managedLauncherRuntime.js'),
@@ -205,7 +253,7 @@ function installManagedNativeVersion({ packageRoot, nativeRoot, version, release
205
253
  }, null, 2) + '\n', 'utf8');
206
254
  }
207
255
 
208
- function installManagedNativeRegistration({ browser, env, extensionId, managedRoot, nativeRoot, platform }) {
256
+ function installManagedNativeRegistration({ browser, env, extensionId, managedRoot, nativeRoot, platform, onRegistryMutation }) {
209
257
  const bridgePath = getDefaultBridgePath({ platform, browser, env });
210
258
  const platformPath = platform === 'win32' ? path.win32 : path.posix;
211
259
  const bootstrapEntry = platformPath.join(nativeRoot, 'bootstrap', 'launcher.js');
@@ -226,6 +274,7 @@ function installManagedNativeRegistration({ browser, env, extensionId, managedRo
226
274
  fs.mkdirSync(platformPath.dirname(target.manifestPath), { recursive: true });
227
275
  fs.writeFileSync(target.manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
228
276
  if (target.kind === 'registry') {
277
+ onRegistryMutation?.();
229
278
  runRegistry(['add', target.registryKey, '/ve', '/t', 'REG_SZ', '/d', target.manifestPath, '/f']);
230
279
  }
231
280
  fs.writeFileSync(path.join(managedRoot, '.codex-overleaf-managed.json'), JSON.stringify({
@@ -255,11 +304,19 @@ function uninstallManagedDistribution(options = {}) {
255
304
  }
256
305
  const registration = getNativeHostRegistrationTarget({ platform, browser, env });
257
306
  const bridgePath = getDefaultBridgePath({ platform, browser, env });
258
- if (registration.kind === 'registry') {
307
+ const bridgeOwned = () => isBridgeForManagedRoot(bridgePath, nativeRoot, platform);
308
+ const manifestOwned = () => {
309
+ try {
310
+ const manifest = readJson(registration.manifestPath);
311
+ return bridgeOwned() && manifest.name === 'com.codex.overleaf' && manifest.path === bridgePath;
312
+ } catch (_) { return false; }
313
+ };
314
+ if (registration.kind === 'registry' && manifestOwned()
315
+ && queryWindowsNativeHostRegistration(registration, { env }).verified) {
259
316
  runRegistry(['delete', registration.registryKey, '/f'], { allowMissing: true });
260
317
  }
261
- fs.rmSync(registration.manifestPath, { force: true });
262
- fs.rmSync(bridgePath, { force: true });
318
+ if (manifestOwned()) fs.rmSync(registration.manifestPath, { force: true });
319
+ if (bridgeOwned()) fs.rmSync(bridgePath, { force: true });
263
320
  safeRemove(extensionRoot);
264
321
  safeRemove(nativeRoot);
265
322
  try {
@@ -272,6 +329,24 @@ function uninstallManagedDistribution(options = {}) {
272
329
  return { ok: true, managedRoot, extensionRoot, nativeRoot, removed: true };
273
330
  }
274
331
 
332
+ // Older installs lack ownership metadata. Compare the existing launcher's
333
+ // exact target argument and agent assignment; the Node executable may change.
334
+ function isBridgeForManagedRoot(bridgePath, nativeRoot, platform) {
335
+ try {
336
+ const platformPath = platform === 'win32' ? path.win32 : path.posix;
337
+ const entry = platformPath.join(nativeRoot, 'bootstrap', 'launcher.js');
338
+ const expected = buildLauncher({ platform, nodePath: '__node__', bridgeEntryPath: entry, agentPath: entry }).split(/\r?\n/);
339
+ const actual = fs.readFileSync(bridgePath, 'utf8').split(/\r?\n/);
340
+ const assignment = expected.find(line => line.includes('CODEX_OVERLEAF_AGENT_ARGS_JSON='));
341
+ const launch = expected.find(line => platform === 'win32' ? line.startsWith('"__node__" ') : line.startsWith('exec "__node__" '));
342
+ const target = launch.slice(launch.indexOf('"__node__"') + '"__node__"'.length);
343
+ const invocation = platform === 'win32' ? /^"(?:[^"]|"")*"/ : /^exec "(?:[^"\\]|\\.)+"/;
344
+ return actual.includes(assignment) && actual.some(line => {
345
+ const first = line.match(invocation); return first && line.slice(first[0].length) === target;
346
+ });
347
+ } catch (_) { return false; }
348
+ }
349
+
275
350
  function assertSafeManagedRoot(root, options = {}) {
276
351
  const platformPath = options.platformPath || path;
277
352
  const resolved = platformPath.resolve(root);