codex-overleaf-link 2.0.0 → 2.1.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 (119) hide show
  1. package/README.md +24 -23
  2. package/extension/bootstrap/manifest.template.json +2 -1
  3. package/extension/runtime-manifest.json +12 -6
  4. package/extension/src/background.js +7 -0
  5. package/extension/src/content/composerPanel.js +13 -3
  6. package/extension/src/content/contentRuntime.js +129 -64
  7. package/extension/src/content/contextTray.js +173 -9
  8. package/extension/src/content/markdownText.js +16 -1
  9. package/extension/src/content/mathText.js +233 -0
  10. package/extension/src/content/modelPicker.js +117 -106
  11. package/extension/src/content/modelPickerSupport.js +115 -0
  12. package/extension/src/content/nativeChannel.js +20 -1
  13. package/extension/src/content/panelMaintenance.js +2 -0
  14. package/extension/src/content/projectSettingsCoordinator.js +2 -0
  15. package/extension/src/content/providerSettingsCoordinator.js +413 -0
  16. package/extension/src/content/providerSettingsDialog.js +1006 -0
  17. package/extension/src/content/runTimelineView.js +24 -5
  18. package/extension/src/content/settingsPanel.js +33 -2
  19. package/extension/src/content/writebackOrchestrator.js +3 -1
  20. package/extension/src/page/overleafProjectSnapshot.js +29 -33
  21. package/extension/src/page/snapshotRouter.js +75 -8
  22. package/extension/src/page/writebackRouter.js +20 -33
  23. package/extension/src/pageBridge.js +1 -1
  24. package/extension/src/shared/agentTranscript.js +130 -0
  25. package/extension/src/shared/compatibility.js +3 -2
  26. package/extension/src/shared/failureReasons.js +75 -0
  27. package/extension/src/shared/i18n.js +22 -0
  28. package/extension/src/shared/models.js +1 -0
  29. package/extension/src/shared/providerProfiles.js +283 -0
  30. package/extension/src/shared/sessionState.js +12 -2
  31. package/extension/src/shared/storageDb.js +1 -1
  32. package/extension/styles/panel.css +689 -1
  33. package/extension/vendor/katex/LICENSE +21 -0
  34. package/extension/vendor/katex/VERSION +2 -0
  35. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
  36. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
  37. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  38. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  39. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  40. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  41. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  42. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  43. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  44. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  45. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  46. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  47. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  48. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  49. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  50. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
  51. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff +0 -0
  52. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  53. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  54. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  55. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  56. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
  57. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff +0 -0
  58. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  59. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
  60. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff +0 -0
  61. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  62. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  63. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  64. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  65. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
  66. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff +0 -0
  67. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  68. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  69. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  70. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  71. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  72. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  73. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  74. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  75. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  76. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  77. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
  78. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff +0 -0
  79. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  80. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
  81. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
  82. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  83. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
  84. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
  85. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  86. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
  87. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
  88. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  89. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
  90. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
  91. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  92. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  93. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  94. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  95. package/extension/vendor/katex/katex.min.css +1 -0
  96. package/extension/vendor/katex/katex.min.js +1 -0
  97. package/native-host/src/anthropicBridgeRequest.js +435 -0
  98. package/native-host/src/anthropicBridgeResponse.js +431 -0
  99. package/native-host/src/anthropicMessagesBridge.js +281 -0
  100. package/native-host/src/chatBridgeRequest.js +274 -0
  101. package/native-host/src/chatBridgeResponse.js +594 -0
  102. package/native-host/src/chatCompletionsBridge.js +485 -0
  103. package/native-host/src/codexCommand.js +25 -0
  104. package/native-host/src/codexPromptAssembly.js +9 -0
  105. package/native-host/src/codexProviderLaunch.js +213 -0
  106. package/native-host/src/codexProviderTest.js +196 -0
  107. package/native-host/src/codexSessionRunner.js +29 -25
  108. package/native-host/src/managedInstall.js +1 -0
  109. package/native-host/src/providerBridgeRoutes.js +11 -0
  110. package/native-host/src/providerModelCatalog.js +107 -0
  111. package/native-host/src/providerProfile.js +454 -0
  112. package/native-host/src/providerReasoning.js +235 -0
  113. package/native-host/src/providerRedaction.js +18 -0
  114. package/native-host/src/providerRuntime.js +365 -0
  115. package/native-host/src/providerStore.js +691 -0
  116. package/native-host/src/readProgressGuard.js +360 -0
  117. package/native-host/src/taskRunnerRuntime.js +39 -3
  118. package/native-host/src/updateArchive.js +1 -0
  119. package/package.json +2 -1
@@ -0,0 +1,213 @@
1
+ 'use strict';
2
+
3
+ const { startChatCompletionsBridge } = require('./chatCompletionsBridge');
4
+ const { startAnthropicMessagesBridge } = require('./anthropicMessagesBridge');
5
+ const { prepareProviderModelCatalog } = require('./providerModelCatalog');
6
+
7
+ const PROVIDER_CONFIG_ID = 'codex_overleaf_custom';
8
+ const PROVIDER_API_KEY_ENV = 'CODEX_OVERLEAF_PROVIDER_API_KEY';
9
+ const LOOPBACK_NO_PROXY_HOSTS = ['127.0.0.1', 'localhost', '::1'];
10
+
11
+ function createProviderLaunch({ profile, secret = '', modelId, wireApi, reasoningEffort = '', upstreamResponseMode = '' }) {
12
+ const model = profile.models.find(item => item.id === modelId) || {};
13
+ const requestedResponseMode = ['streaming', 'buffered'].includes(upstreamResponseMode)
14
+ ? upstreamResponseMode
15
+ : model.upstreamResponseMode || 'auto';
16
+ const resolvedResponseMode = requestedResponseMode === 'auto'
17
+ ? (model.resolvedUpstreamResponseMode || 'streaming')
18
+ : requestedResponseMode;
19
+ return Object.freeze({
20
+ providerId: profile.id,
21
+ providerRevision: profile.revision,
22
+ providerName: profile.name,
23
+ baseUrl: profile.baseUrl,
24
+ modelId,
25
+ wireApi,
26
+ upstreamResponseMode: resolvedResponseMode === 'buffered' ? 'buffered' : 'streaming',
27
+ requestTimeoutMs: profile.requestTimeoutMs,
28
+ reasoningAdapter: profile.reasoningAdapter || 'auto',
29
+ reasoningCapability: profile.reasoningCapability || 'auto',
30
+ reasoningEffort,
31
+ reasoningEfforts: model.reasoningEfforts?.length
32
+ ? model.reasoningEfforts.slice()
33
+ : reasoningEffort
34
+ ? [reasoningEffort]
35
+ : [],
36
+ models: profile.models.map(item => ({ ...item })),
37
+ contextWindow: model.contextWindow || profile.contextWindow,
38
+ supportsParallelToolCalls: model.supportsParallelToolCalls ?? profile.supportsParallelToolCalls,
39
+ supportsStreamOptions: Boolean(profile.supportsStreamOptions),
40
+ inputModalities: model.inputModalities || profile.inputModalities,
41
+ authMode: profile.authMode || 'bearer',
42
+ apiKeyHeaderName: profile.apiKeyHeaderName || '',
43
+ fullEndpoint: Boolean(profile.fullEndpoint),
44
+ customHeaders: { ...(profile.customHeaders || {}) },
45
+ queryParams: { ...(profile.queryParams || {}) },
46
+ bodyOverrides: { ...(profile.bodyOverrides || {}) },
47
+ anthropicVersion: profile.anthropicVersion || '2023-06-01',
48
+ anthropicBeta: profile.anthropicBeta || '',
49
+ anthropicThinkingMode: profile.anthropicThinkingMode || 'budget',
50
+ anthropicPromptCaching: profile.anthropicPromptCaching === true,
51
+ impersonateClaudeCode: profile.impersonateClaudeCode === true,
52
+ maxOutputTokens: profile.maxOutputTokens || 32768,
53
+ apiKey: secret
54
+ });
55
+ }
56
+
57
+ async function prepareProviderLaunch(launch, { signal } = {}) {
58
+ const catalog = prepareProviderModelCatalog(launch);
59
+ if (!catalog.launch || !['chat', 'anthropic'].includes(catalog.launch.wireApi)) {
60
+ return { launch: catalog.launch, close: async () => catalog.close() };
61
+ }
62
+ try {
63
+ const routedWireApi = catalog.launch.wireApi;
64
+ const bridge = routedWireApi === 'anthropic'
65
+ ? await startAnthropicMessagesBridge({ launch: catalog.launch, signal })
66
+ : await startChatCompletionsBridge({ launch: catalog.launch, signal });
67
+ return {
68
+ launch: Object.freeze({
69
+ ...catalog.launch,
70
+ upstreamBaseUrl: catalog.launch.baseUrl,
71
+ baseUrl: bridge.baseUrl,
72
+ wireApi: 'responses',
73
+ routedWireApi,
74
+ apiKey: bridge.clientToken,
75
+ authMode: 'bearer',
76
+ apiKeyHeaderName: '',
77
+ fullEndpoint: false,
78
+ customHeaders: {},
79
+ queryParams: {},
80
+ bodyOverrides: {}
81
+ }),
82
+ close: async () => {
83
+ await bridge.close();
84
+ catalog.close();
85
+ }
86
+ };
87
+ } catch (error) {
88
+ catalog.close();
89
+ throw error;
90
+ }
91
+ }
92
+
93
+ function buildProviderConfigArgs(launch) {
94
+ if (!launch) {
95
+ return [];
96
+ }
97
+ const values = [
98
+ ['model', launch.modelId],
99
+ ['model_provider', PROVIDER_CONFIG_ID],
100
+ [`model_providers.${PROVIDER_CONFIG_ID}.name`, launch.providerName],
101
+ [`model_providers.${PROVIDER_CONFIG_ID}.base_url`, launch.baseUrl],
102
+ [`model_providers.${PROVIDER_CONFIG_ID}.wire_api`, launch.wireApi]
103
+ ];
104
+ if (launch.apiKey && (launch.authMode || 'bearer') === 'bearer') {
105
+ values.push([`model_providers.${PROVIDER_CONFIG_ID}.env_key`, PROVIDER_API_KEY_ENV]);
106
+ }
107
+ const args = [];
108
+ for (const [key, value] of values) {
109
+ args.push('-c', `${key}=${encodeTomlString(value)}`);
110
+ }
111
+ args.push('-c', `model_providers.${PROVIDER_CONFIG_ID}.requires_openai_auth=false`);
112
+ if (launch.modelCatalogPath) {
113
+ args.push('-c', `model_catalog_json=${encodeTomlString(launch.modelCatalogPath)}`);
114
+ }
115
+ if (Object.keys(launch.customHeaders || {}).length) {
116
+ args.push('-c', `model_providers.${PROVIDER_CONFIG_ID}.http_headers=${encodeTomlTable(launch.customHeaders)}`);
117
+ }
118
+ if (Object.keys(launch.queryParams || {}).length) {
119
+ args.push('-c', `model_providers.${PROVIDER_CONFIG_ID}.query_params=${encodeTomlTable(launch.queryParams)}`);
120
+ }
121
+ const authHeader = resolveAuthHeader(launch);
122
+ if (launch.apiKey && authHeader) {
123
+ args.push('-c', `model_providers.${PROVIDER_CONFIG_ID}.env_http_headers=${encodeTomlTable({
124
+ [authHeader]: PROVIDER_API_KEY_ENV
125
+ })}`);
126
+ }
127
+ return args;
128
+ }
129
+
130
+ function applyProviderEnvironment(env, launch) {
131
+ const result = { ...(env || process.env) };
132
+ delete result[PROVIDER_API_KEY_ENV];
133
+ if (launch?.apiKey) {
134
+ result[PROVIDER_API_KEY_ENV] = launch.apiKey;
135
+ }
136
+ if (isLoopbackBaseUrl(launch?.baseUrl)) {
137
+ const noProxy = mergeNoProxyValues(result.NO_PROXY, result.no_proxy, LOOPBACK_NO_PROXY_HOSTS);
138
+ result.NO_PROXY = noProxy;
139
+ result.no_proxy = noProxy;
140
+ }
141
+ return result;
142
+ }
143
+
144
+ function isLoopbackBaseUrl(value) {
145
+ try {
146
+ const hostname = new URL(String(value || '')).hostname.toLowerCase().replace(/^\[|\]$/g, '');
147
+ return LOOPBACK_NO_PROXY_HOSTS.includes(hostname);
148
+ } catch (_error) {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ function mergeNoProxyValues(...values) {
154
+ const entries = [];
155
+ const seen = new Set();
156
+ for (const value of values.flat()) {
157
+ for (const entry of String(value || '').split(',')) {
158
+ const normalized = entry.trim();
159
+ const key = normalized.toLowerCase();
160
+ if (!normalized || seen.has(key)) continue;
161
+ seen.add(key);
162
+ entries.push(normalized);
163
+ }
164
+ }
165
+ return entries.includes('*') ? '*' : entries.join(',');
166
+ }
167
+
168
+ function buildProviderSnapshot(launch) {
169
+ if (!launch) {
170
+ return {
171
+ providerId: 'builtin',
172
+ providerRevision: 0,
173
+ providerName: 'Built-in Codex',
174
+ providerKind: 'builtin'
175
+ };
176
+ }
177
+ return {
178
+ providerId: launch.providerId,
179
+ providerRevision: launch.providerRevision,
180
+ providerName: launch.providerName,
181
+ providerKind: 'custom',
182
+ modelId: launch.modelId,
183
+ wireApi: launch.routedWireApi || launch.wireApi,
184
+ upstreamResponseMode: launch.upstreamResponseMode || 'streaming',
185
+ requestTimeoutMs: launch.requestTimeoutMs
186
+ };
187
+ }
188
+
189
+ function encodeTomlString(value) {
190
+ return JSON.stringify(String(value ?? ''));
191
+ }
192
+
193
+ function encodeTomlTable(value) {
194
+ return `{${Object.entries(value || {}).map(([key, item]) => (
195
+ `${encodeTomlString(key)}=${encodeTomlString(item)}`
196
+ )).join(',')}}`;
197
+ }
198
+
199
+ function resolveAuthHeader(launch) {
200
+ if (!launch || launch.authMode === 'none' || (launch.authMode || 'bearer') === 'bearer') return '';
201
+ if (launch.authMode === 'x-api-key') return 'x-api-key';
202
+ if (launch.authMode === 'api-key') return 'api-key';
203
+ return launch.apiKeyHeaderName || '';
204
+ }
205
+
206
+ module.exports = {
207
+ PROVIDER_API_KEY_ENV,
208
+ applyProviderEnvironment,
209
+ buildProviderConfigArgs,
210
+ buildProviderSnapshot,
211
+ createProviderLaunch,
212
+ prepareProviderLaunch
213
+ };
@@ -0,0 +1,196 @@
1
+ 'use strict';
2
+
3
+ const { spawn, spawnSync } = require('node:child_process');
4
+ const crypto = require('node:crypto');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const { resolveCodexCommand, shouldUseShellForCommand } = require('./codexCommand');
9
+ const { applyProviderEnvironment, buildProviderConfigArgs, prepareProviderLaunch } = require('./codexProviderLaunch');
10
+ const { providerError } = require('./providerProfile');
11
+
12
+ const MAX_OUTPUT_BYTES = 256 * 1024;
13
+
14
+ async function runProviderConnectionTest({ launch, env = process.env, signal } = {}) {
15
+ const prepared = await prepareProviderLaunch(launch, { signal });
16
+ try {
17
+ return await runProviderConnectionTestProcess({ launch: prepared.launch, env, signal });
18
+ } finally {
19
+ await prepared.close();
20
+ }
21
+ }
22
+
23
+ function runProviderConnectionTestProcess({ launch, env = process.env, signal } = {}) {
24
+ return new Promise((resolve, reject) => {
25
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-overleaf-provider-test-'));
26
+ const codexHome = path.join(root, 'codex-home');
27
+ const workspace = path.join(root, 'workspace');
28
+ fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 });
29
+ fs.mkdirSync(workspace, { recursive: true, mode: 0o700 });
30
+ const verificationToken = `provider-tool-check-${crypto.randomUUID()}`;
31
+ const secondFileName = `provider-check-${crypto.randomUUID()}.txt`;
32
+ fs.writeFileSync(path.join(workspace, 'provider-step-one.txt'), `${secondFileName}\n`, { mode: 0o600 });
33
+ fs.writeFileSync(path.join(workspace, secondFileName), `${verificationToken}\n`, { mode: 0o600 });
34
+ const childEnv = applyProviderEnvironment({
35
+ ...env,
36
+ CODEX_HOME: codexHome
37
+ }, launch);
38
+ const command = resolveCodexCommand(childEnv);
39
+ if (!command) {
40
+ cleanup(root);
41
+ reject(providerError('codex_not_found', 'Codex CLI was not found locally.'));
42
+ return;
43
+ }
44
+ const args = [
45
+ ...buildProviderConfigArgs(launch),
46
+ 'exec',
47
+ '--skip-git-repo-check',
48
+ '--sandbox',
49
+ 'read-only',
50
+ 'Use the shell tool to read provider-step-one.txt. It contains the name of a second file. Then use a separate shell tool call to read that second file. Reply only with the exact contents of the second file.'
51
+ ];
52
+ const startedAt = Date.now();
53
+ let child;
54
+ try {
55
+ child = spawn(command, args, {
56
+ cwd: workspace,
57
+ env: childEnv,
58
+ shell: shouldUseShellForCommand(command, childEnv),
59
+ detached: process.platform !== 'win32',
60
+ stdio: ['ignore', 'pipe', 'pipe']
61
+ });
62
+ } catch (error) {
63
+ cleanup(root);
64
+ reject(providerError('provider_test_spawn_failed', 'Codex could not start the provider connection test.', { cause: error }));
65
+ return;
66
+ }
67
+ let stdout = '';
68
+ let stderr = '';
69
+ let settled = false;
70
+ const timeoutMs = Math.min(120000, Math.max(5000, Number(launch.requestTimeoutMs) || 30000));
71
+ const timer = setTimeout(() => finish(providerError('provider_connection_timeout', 'The provider connection test timed out.')), timeoutMs);
72
+ const onAbort = () => finish(providerError('provider_test_cancelled', 'Provider connection test was cancelled.'));
73
+ signal?.addEventListener('abort', onAbort, { once: true });
74
+
75
+ child.stdout.setEncoding('utf8');
76
+ child.stderr.setEncoding('utf8');
77
+ child.stdout.on('data', chunk => appendOutput('stdout', chunk));
78
+ child.stderr.on('data', chunk => appendOutput('stderr', chunk));
79
+ child.on('error', error => finish(providerError('provider_test_spawn_failed', 'Codex could not start the provider connection test.', { cause: error })));
80
+ child.on('close', code => {
81
+ if (hasLeakedToolControlTokens(`${stdout}\n${stderr}`)) {
82
+ finish(providerError(
83
+ 'provider_stream_tool_parse_failed',
84
+ 'The provider leaked unparsed tool-call control tokens while streaming.'
85
+ ));
86
+ return;
87
+ }
88
+ if (code === 0 && stdout.includes(verificationToken)) {
89
+ finish(null, {
90
+ durationMs: Date.now() - startedAt,
91
+ capabilities: {
92
+ text: true,
93
+ streaming: true,
94
+ agentTools: true,
95
+ toolRoundTrip: true
96
+ }
97
+ });
98
+ return;
99
+ }
100
+ if (code === 0) {
101
+ finish(providerError(
102
+ 'provider_agent_tools_incompatible',
103
+ 'The provider returned text but did not complete the required Codex tool call.'
104
+ ));
105
+ return;
106
+ }
107
+ finish(classifyProviderFailure(stderr || stdout, code));
108
+ });
109
+
110
+ function appendOutput(target, chunk) {
111
+ if (settled) return;
112
+ if (target === 'stdout') stdout += chunk;
113
+ else stderr += chunk;
114
+ if (Buffer.byteLength(stdout) + Buffer.byteLength(stderr) > MAX_OUTPUT_BYTES) {
115
+ finish(providerError('provider_response_invalid', 'Provider connection test exceeded the output limit.'));
116
+ }
117
+ }
118
+
119
+ function finish(error, result) {
120
+ if (settled) return;
121
+ settled = true;
122
+ clearTimeout(timer);
123
+ signal?.removeEventListener('abort', onAbort);
124
+ if (error) terminateProcessTree(child);
125
+ cleanup(root);
126
+ if (error) reject(error);
127
+ else resolve(result);
128
+ }
129
+ });
130
+ }
131
+
132
+ function classifyProviderFailure(text, exitCode) {
133
+ const message = String(text || '');
134
+ if (hasLeakedToolControlTokens(message) || /provider_stream_tool_parse_failed/i.test(message)) {
135
+ return providerError('provider_stream_tool_parse_failed', 'The provider failed to parse streaming tool calls.');
136
+ }
137
+ if (/\b401\b|unauthori[sz]ed|invalid api key|incorrect api key|authentication/i.test(message)) {
138
+ return providerError('provider_auth_rejected', 'The provider rejected the API key.');
139
+ }
140
+ if (/\b403\b|forbidden/i.test(message)) {
141
+ return providerError('provider_auth_rejected', 'The provider denied access for this credential.');
142
+ }
143
+ if (/\b429\b|rate.?limit|too many requests/i.test(message)) {
144
+ return providerError('provider_rate_limited', 'The provider rate limit was reached.');
145
+ }
146
+ if (/model.{0,40}(not found|does not exist|unknown)|\b404\b.{0,80}model/i.test(message)) {
147
+ return providerError('provider_model_not_found', 'The configured model was not found.');
148
+ }
149
+ if (/certificate|self.signed|tls|ssl/i.test(message)) {
150
+ return providerError('provider_tls_failed', 'TLS verification failed for the provider endpoint.');
151
+ }
152
+ if (/enotfound|eai_again|name or service not known|dns/i.test(message)) {
153
+ return providerError('provider_dns_failed', 'The provider hostname could not be resolved.');
154
+ }
155
+ if (/\b404\b|\b405\b|cannot\s+(?:post|get)|(?:route|endpoint).{0,40}(?:not found|unsupported)|unsupported.{0,30}(?:endpoint|api|protocol)|responses.{0,30}(?:unsupported|not found)|chat\/completions.{0,30}(?:unsupported|not found)|v1\/messages.{0,30}(?:unsupported|not found)/i.test(message)) {
156
+ return providerError('provider_protocol_incompatible', 'The endpoint is incompatible with this API protocol.');
157
+ }
158
+ if (/\b400\b|\b415\b|\b422\b|invalid.{0,30}(?:request|parameter|field)|unknown.{0,20}(?:field|parameter)|unsupported.{0,30}(?:field|parameter|tool|schema)|max[_ -]?tokens|context.{0,30}(?:length|window|limit)/i.test(message)) {
159
+ return providerError('provider_request_rejected', 'The provider rejected this protocol probe request. Review the model and compatibility settings.');
160
+ }
161
+ if (/timed?\s*out|timeout/i.test(message)) {
162
+ return providerError('provider_connection_timeout', 'The provider connection timed out.');
163
+ }
164
+ return providerError('provider_response_invalid', `Provider connection test failed with exit code ${exitCode}.`);
165
+ }
166
+
167
+ function hasLeakedToolControlTokens(value) {
168
+ return /<\|tool_call_(?:begin|argument_begin|end)\|>/i.test(String(value || ''));
169
+ }
170
+
171
+ function terminateProcessTree(child) {
172
+ if (!child?.pid) return;
173
+ try {
174
+ if (process.platform === 'win32') {
175
+ spawnSync('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore', windowsHide: true });
176
+ } else {
177
+ process.kill(-child.pid, 'SIGTERM');
178
+ setTimeout(() => {
179
+ try { process.kill(-child.pid, 'SIGKILL'); } catch (_error) {}
180
+ }, 500).unref?.();
181
+ }
182
+ } catch (_error) {
183
+ try { child.kill('SIGKILL'); } catch (_killError) {}
184
+ }
185
+ }
186
+
187
+ function cleanup(root) {
188
+ try {
189
+ fs.rmSync(root, { recursive: true, force: true });
190
+ } catch (_error) {}
191
+ }
192
+
193
+ module.exports = {
194
+ classifyProviderFailure,
195
+ runProviderConnectionTest
196
+ };
@@ -19,6 +19,9 @@ const {
19
19
  loadSelectedProjectSkills
20
20
  } = require('./localSkills');
21
21
  const { createSubagentBroker } = require('./subagentBroker');
22
+ const { resolveCodexCommand, shouldUseShellForCommand } = require('./codexCommand');
23
+ const { applyProviderEnvironment, buildProviderConfigArgs, prepareProviderLaunch } = require('./codexProviderLaunch');
24
+ const { buildReadProgressRules, createReadProgressController } = require('./readProgressGuard');
22
25
 
23
26
  const PARALLEL_SUBAGENTS_SKILL_ID = 'parallel-subagents';
24
27
 
@@ -27,7 +30,7 @@ const MAX_TURN_ATTACHMENT_BYTES = 12 * 1024 * 1024;
27
30
  const MAX_TURN_ATTACHMENTS = 8;
28
31
  const MAX_TURN_ATTACHMENT_TOTAL_BYTES = MAX_TURN_ATTACHMENT_BYTES * MAX_TURN_ATTACHMENTS;
29
32
 
30
- async function runCodexSession({ params = {}, env = process.env, emit = () => {}, rootDir, executeCodex, signal } = {}) {
33
+ async function runCodexSession({ params = {}, env = process.env, emit = () => {}, rootDir, executeCodex, providerLaunch, signal } = {}) {
31
34
  throwIfAborted(signal);
32
35
  const projectId = params.projectId || params.project?.projectId || params.project?.id || params.project?.url || 'overleaf-project';
33
36
  const skillInvocation = normalizeSkillInvocation(params.skillInvocation);
@@ -117,6 +120,7 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
117
120
  disableCodexOverleafSkillIds: [PARALLEL_SUBAGENTS_SKILL_ID],
118
121
  sandboxMode: settings.sandboxMode,
119
122
  approvalPolicy: settings.approvalPolicy,
123
+ providerLaunch,
120
124
  env,
121
125
  // Worker raw events stay out of the parent timeline (spec §8);
122
126
  // lifecycle events come from the broker, full text from result files.
@@ -152,6 +156,7 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
152
156
  projectLocalSkills: null,
153
157
  sandboxMode: settings.sandboxMode,
154
158
  approvalPolicy: settings.approvalPolicy,
159
+ providerLaunch,
155
160
  env,
156
161
  emit,
157
162
  // While subagent workers are active, the parent idle watchdog resets
@@ -281,7 +286,7 @@ function buildCodexTurnPrompt(params = {}, mirror = {}, projectLocalSkills, turn
281
286
  turnAttachments,
282
287
  codexSkillInvocationContext
283
288
  });
284
- return [prompt.systemPrompt, prompt.userPrompt].filter(Boolean).join('\n\n');
289
+ return [prompt.systemPrompt, buildReadProgressRules(), prompt.userPrompt].filter(Boolean).join('\n\n');
285
290
  }
286
291
 
287
292
  function materializeTurnAttachments(attachments = [], workspacePath = '') {
@@ -585,7 +590,8 @@ function buildThreadResumeParams(input = {}) {
585
590
 
586
591
  function buildCodexAppServerArgs(input = {}) {
587
592
  const args = [
588
- ...buildCodexSpeedArgs(normalizeSpeedTier(input.speedTier))
593
+ ...buildCodexSpeedArgs(normalizeSpeedTier(input.speedTier)),
594
+ ...buildProviderConfigArgs(input.providerLaunch)
589
595
  ];
590
596
  if (input.loadCodexLocalSkills === false) {
591
597
  args.push('--disable', 'plugins');
@@ -764,18 +770,27 @@ function supportsReasoningSummary(model) {
764
770
  return String(model || '').toLowerCase() !== 'gpt-5.3-codex-spark';
765
771
  }
766
772
 
767
- function runCodexAppServerSession(input) {
773
+ async function runCodexAppServerSession(input) {
774
+ const prepared = await prepareProviderLaunch(input.providerLaunch, { signal: input.signal });
775
+ try {
776
+ return await runCodexAppServerProcess({ ...input, providerLaunch: prepared.launch });
777
+ } finally {
778
+ await prepared.close();
779
+ }
780
+ }
781
+
782
+ function runCodexAppServerProcess(input) {
768
783
  return new Promise((resolve, reject) => {
769
784
  if (input.signal?.aborted) {
770
785
  reject(getAbortReason(input.signal));
771
786
  return;
772
787
  }
773
- const childEnv = buildCodexHomeEnv(input.env || process.env, {
788
+ const childEnv = applyProviderEnvironment(buildCodexHomeEnv(input.env || process.env, {
774
789
  loadCodexLocalSkills: input.loadCodexLocalSkills !== false,
775
790
  loadCodexOverleafSkills: input.loadCodexOverleafSkills !== false,
776
791
  installCodexOverleafSkillsTarget: input.installCodexOverleafSkillsTarget === true,
777
792
  projectLocalSkills: input.projectLocalSkills || null
778
- });
793
+ }), input.providerLaunch);
779
794
  const codexCommand = resolveCodexCommand(childEnv);
780
795
  if (!codexCommand) {
781
796
  reject(new Error('Codex CLI was not found. Install Codex or make sure the `codex` command is available in your login shell.'));
@@ -795,6 +810,11 @@ function runCodexAppServerSession(input) {
795
810
  let activeTurnId = '';
796
811
  const assistantMessages = new Map();
797
812
  const assistantMessageOrder = [];
813
+ const readProgressController = createReadProgressController({
814
+ input, request, fail,
815
+ getTurn: () => ({ threadId: activeThreadId, turnId: activeTurnId }),
816
+ emitEvent: (...args) => emitCodexEvent(input.emit, ...args)
817
+ });
798
818
  let settled = false;
799
819
  // Two-layer timeout strategy:
800
820
  // 1. Optional absolute deadline (CODEX_OVERLEAF_CODEX_TIMEOUT_MS) —
@@ -890,6 +910,7 @@ function runCodexAppServerSession(input) {
890
910
 
891
911
  const turnResponse = await startTurnWithSummaryFallback(activeThreadId);
892
912
  activeTurnId = turnResponse?.turn?.id || '';
913
+ readProgressController.flush();
893
914
  }
894
915
 
895
916
  async function startTurnWithSummaryFallback(threadId) {
@@ -976,6 +997,8 @@ function runCodexAppServerSession(input) {
976
997
  method: message.method,
977
998
  params: message.params || {}
978
999
  }, inferNotificationStatus(message));
1000
+ if (message.method === 'item/completed' &&
1001
+ !readProgressController.observe(message.params?.item || {})) return;
979
1002
  if (message.method === 'turn/completed' && (!activeTurnId || message.params?.turn?.id === activeTurnId || message.params?.turnId === activeTurnId)) {
980
1003
  succeed();
981
1004
  }
@@ -1378,25 +1401,6 @@ function getAbortReason(signal) {
1378
1401
  return error;
1379
1402
  }
1380
1403
 
1381
- function resolveCodexCommand(env = process.env) {
1382
- if (
1383
- env.CODEX_OVERLEAF_ENV_READY === '1' ||
1384
- Object.prototype.hasOwnProperty.call(env, 'CODEX_OVERLEAF_CODEX_PATH')
1385
- ) {
1386
- return env.CODEX_OVERLEAF_CODEX_PATH || '';
1387
- }
1388
- return 'codex';
1389
- }
1390
-
1391
- function shouldUseShellForCommand(command, env = process.env) {
1392
- const platform = env.CODEX_OVERLEAF_PLATFORM || process.platform;
1393
- if (platform !== 'win32') {
1394
- return false;
1395
- }
1396
- const text = String(command || '');
1397
- return text === 'codex' || /\.(?:cmd|bat)$/i.test(text);
1398
- }
1399
-
1400
1404
  function buildFinalAssistantMessage(messages = new Map(), order = []) {
1401
1405
  const values = [];
1402
1406
  const seenIds = new Set();
@@ -131,6 +131,7 @@ function buildManagedExtensionTree(options = {}) {
131
131
  copyPackageTree(packageRoot, 'extension/assets', targetRoot, 'assets', { allowedFiles });
132
132
  copyPackageTree(packageRoot, 'extension/src', targetRoot, 'runtime/src', { allowedFiles });
133
133
  copyPackageTree(packageRoot, 'extension/styles', targetRoot, 'runtime/styles', { allowedFiles });
134
+ copyPackageTree(packageRoot, 'extension/vendor', targetRoot, 'runtime/vendor', { allowedFiles });
134
135
  copyPackageFile(packageRoot, 'extension/runtime-manifest.json', targetRoot, 'runtime/runtime-manifest.json', allowedFiles);
135
136
  fs.writeFileSync(path.join(targetRoot, EXTENSION_MARKER), JSON.stringify({
136
137
  managedBy: MANAGED_BY,
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ function classifyResponsesRoute(method, pathname) {
4
+ if (String(method || '').toUpperCase() !== 'POST') return '';
5
+ const path = String(pathname || '');
6
+ if (/\/responses\/compact\/?$/.test(path)) return 'compact';
7
+ if (/\/responses\/?$/.test(path)) return 'responses';
8
+ return '';
9
+ }
10
+
11
+ module.exports = { classifyResponsesRoute };
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const DEFAULT_CONTEXT_WINDOW = 262144;
8
+
9
+ function buildProviderModelCatalogData(launch = {}) {
10
+ const configured = Array.isArray(launch.models) ? launch.models : [];
11
+ const fallbackId = String(launch.modelId || '').trim();
12
+ const models = configured.length > 0
13
+ ? configured
14
+ : fallbackId
15
+ ? [{ id: fallbackId, label: fallbackId }]
16
+ : [];
17
+ return {
18
+ models: models
19
+ .filter(model => model && typeof model.id === 'string' && model.id.trim())
20
+ .map((model, index) => buildModelEntry(model, launch, index))
21
+ };
22
+ }
23
+
24
+ function prepareProviderModelCatalog(launch) {
25
+ if (!launch) {
26
+ return { launch, close() {} };
27
+ }
28
+ const catalog = buildProviderModelCatalogData(launch);
29
+ if (!catalog.models.length) {
30
+ return { launch, close() {} };
31
+ }
32
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-overleaf-models-'));
33
+ const modelCatalogPath = path.join(directory, 'models.json');
34
+ fs.writeFileSync(modelCatalogPath, `${JSON.stringify(catalog, null, 2)}\n`, { mode: 0o600 });
35
+ return {
36
+ launch: Object.freeze({ ...launch, modelCatalogPath }),
37
+ close() {
38
+ fs.rmSync(directory, { recursive: true, force: true });
39
+ }
40
+ };
41
+ }
42
+
43
+ function buildModelEntry(model, launch, index) {
44
+ const id = String(model.id || '').trim();
45
+ const efforts = normalizeEfforts(model.reasoningEfforts, launch.reasoningEfforts);
46
+ const defaultReasoning = efforts.find(effort => effort !== 'none') || efforts[0] || 'none';
47
+ const contextWindow = normalizeContextWindow(model.contextWindow || launch.contextWindow);
48
+ return {
49
+ slug: id,
50
+ display_name: String(model.label || id),
51
+ description: `Custom provider model ${id}`,
52
+ base_instructions: String(model.baseInstructions || launch.baseInstructions || (
53
+ 'You are a coding agent. Use the available shell tool to inspect and modify files, follow the user request precisely, and report the result concisely.'
54
+ )),
55
+ default_reasoning_level: defaultReasoning,
56
+ supported_reasoning_levels: efforts.map(effort => ({
57
+ effort,
58
+ description: effort === 'none' ? 'Reasoning disabled' : `${effort} reasoning effort`
59
+ })),
60
+ shell_type: 'shell_command',
61
+ visibility: 'list',
62
+ supported_in_api: true,
63
+ priority: 10000 - index,
64
+ supports_reasoning_summaries: efforts.some(effort => effort !== 'none'),
65
+ default_reasoning_summary: 'none',
66
+ support_verbosity: false,
67
+ truncation_policy: { mode: 'bytes', limit: 10000 },
68
+ supports_parallel_tool_calls: Boolean(
69
+ model.supportsParallelToolCalls ?? launch.supportsParallelToolCalls
70
+ ),
71
+ supports_image_detail_original: false,
72
+ context_window: contextWindow,
73
+ max_context_window: contextWindow,
74
+ effective_context_window_percent: 95,
75
+ experimental_supported_tools: [],
76
+ input_modalities: normalizeInputModalities(model.inputModalities || launch.inputModalities),
77
+ supports_search_tool: false
78
+ };
79
+ }
80
+
81
+ function normalizeEfforts(...values) {
82
+ const allowed = new Set(['none', 'minimal', 'low', 'medium', 'high', 'xhigh']);
83
+ const normalized = values
84
+ .flatMap(value => Array.isArray(value) ? value : [])
85
+ .map(value => String(value || '').trim().toLowerCase())
86
+ .filter(value => allowed.has(value));
87
+ return Array.from(new Set(normalized.length ? normalized : ['none']));
88
+ }
89
+
90
+ function normalizeContextWindow(value) {
91
+ const parsed = Number.parseInt(String(value ?? ''), 10);
92
+ if (!Number.isFinite(parsed)) return DEFAULT_CONTEXT_WINDOW;
93
+ return Math.min(4000000, Math.max(8192, parsed));
94
+ }
95
+
96
+ function normalizeInputModalities(values) {
97
+ const normalized = (Array.isArray(values) ? values : ['text'])
98
+ .map(value => String(value || '').trim().toLowerCase())
99
+ .filter(value => value === 'text' || value === 'image');
100
+ const unique = Array.from(new Set(normalized));
101
+ return unique.includes('text') ? unique : ['text', ...unique];
102
+ }
103
+
104
+ module.exports = {
105
+ buildProviderModelCatalogData,
106
+ prepareProviderModelCatalog
107
+ };