sneakoscope 9.1.1 → 9.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/commands/bridge.js +11 -0
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/codex-app.js +1 -1
- package/dist/core/codex-control/codex-sdk-env-policy.js +44 -9
- package/dist/core/codex-lb/bridge-contracts.js +1 -0
- package/dist/core/codex-lb/desktop-bridge/header-policy.js +38 -0
- package/dist/core/codex-lb/desktop-bridge/http-forward.js +58 -16
- package/dist/core/codex-lb/desktop-bridge/rejection-log.js +1 -0
- package/dist/core/codex-lb/desktop-bridge/security.js +55 -3
- package/dist/core/codex-lb/desktop-bridge/types.js +1 -0
- package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +23 -11
- package/dist/core/codex-lb/desktop-bridge-migration/receipt.js +1 -3
- package/dist/core/codex-lb/desktop-bridge-migration/retired-runtime-cleanup.js +1 -1
- package/dist/core/codex-lb/desktop-bridge-migration.js +2 -4
- package/dist/core/codex-lb/desktop-controller-v3/catalog.js +6 -4
- package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +36 -2
- package/dist/core/codex-lb/desktop-controller-v3/shared.js +2 -1
- package/dist/core/codex-lb/desktop-controller-v3.js +3 -1
- package/dist/core/codex-lb/desktop-service.js +97 -12
- package/dist/core/codex-lb/provider-route-policy.js +30 -1
- package/dist/core/codex-lb/request-route-resolver.js +14 -0
- package/dist/core/imagegen/desktop-bridge-imagegen-target.js +2 -2
- package/dist/core/provider/provider-context.js +2 -1
- package/dist/core/release/gate-affected-globs.js +9 -0
- package/dist/core/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Proof-first orchestration for Codex CLI, ChatGPT Desktop, AI coding agents, mult
|
|
|
22
22
|
Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.
|
|
23
23
|
<!-- END SKS SEARCH VISIBILITY MARKETING -->
|
|
24
24
|
|
|
25
|
-
This README documents package **SKS 9.
|
|
25
|
+
This README documents package **SKS 9.2.1** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
|
|
26
26
|
|
|
27
27
|
Use the official latest stable SKS and Codex CLI releases. The Codex compatibility SSOT is always the **current latest stable** host; capability probes measure what that host can actually do. Product docs do not crown a fixed `0.x.y` string as SSOT (release pins and schema directories are measured artifacts for the current package, not a permanent product version claim). Menu Bar / Center induce updates to the latest stable build. Run `sks update-check` for what is installed and read the capability report for what is supported. Install SSOT is npm `sneakoscope@latest`; PATH `sks` and Menu Bar stamped generation must match that version or gates fail. It resolves managed SKS skills from the authoritative global install, preserves a runnable Naruto child slot when `max_threads=2`, and keeps Menu Bar repair transactional so stamped generations remain verifiable. Naruto uses stable opt-in multi-agent V2 when the host exposes it (Codex official multi-agent wrap-only; SKS does not reimplement a parallel runtime). Local code search is mode-separated (`sks search files|text|structure|symbol|context`); `context` is answered by the compiled TriWiki Context Graph (`context-graph.json` is exhaustive authority; `context-pack.json` and managed `AGENTS.md` are bounded projections) — see [docs/architecture/context-graph.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/architecture/context-graph.md) and [docs/PRODUCT-CONTRACT.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/PRODUCT-CONTRACT.md). See [CHANGELOG.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/CHANGELOG.md).
|
|
28
28
|
|
package/dist/commands/bridge.js
CHANGED
|
@@ -27,6 +27,7 @@ export function usage(command = 'bridge') {
|
|
|
27
27
|
` sks ${command} models select --set <public-id,...> [--json]`,
|
|
28
28
|
` sks ${command} route list [--json]`,
|
|
29
29
|
` sks ${command} route set-default <codex-lb|openrouter> [--json]`,
|
|
30
|
+
` sks ${command} route official-models <passthrough|gateway|auto> [--json]`,
|
|
30
31
|
` sks ${command} route explain <model> [--json]`,
|
|
31
32
|
` sks ${command} unmanage --confirm [--json]`,
|
|
32
33
|
` sks ${command} rollback <receipt-id> --confirm [--json]`,
|
|
@@ -236,6 +237,16 @@ async function parseInvocation(args, io) {
|
|
|
236
237
|
label: `Default bridge provider set to ${providerId}`
|
|
237
238
|
};
|
|
238
239
|
}
|
|
240
|
+
if (action === 'official-models' && target !== undefined && extra === undefined) {
|
|
241
|
+
allowOnly(parsed, ['--json'], []);
|
|
242
|
+
if (target !== 'passthrough' && target !== 'gateway' && target !== 'auto')
|
|
243
|
+
throw new BridgeCliError('bridge_route_official_models_mode_invalid');
|
|
244
|
+
return {
|
|
245
|
+
...base,
|
|
246
|
+
request: { operation: 'route.official-models', mode: target },
|
|
247
|
+
label: `Official models routing set to ${target}`
|
|
248
|
+
};
|
|
249
|
+
}
|
|
239
250
|
if (action === 'explain' && target !== undefined && extra === undefined) {
|
|
240
251
|
allowOnly(parsed, ['--json'], []);
|
|
241
252
|
return {
|
package/dist/core/codex-app.js
CHANGED
|
@@ -894,7 +894,7 @@ function providerModelUiStatusFromDesktopBridge(status) {
|
|
|
894
894
|
|| status.routing.policy?.default_provider_id
|
|
895
895
|
|| status.routing.session_pin?.provider_id
|
|
896
896
|
|| null;
|
|
897
|
-
const selectedProfile = selectedProvider ? status.providers[selectedProvider] : null;
|
|
897
|
+
const selectedProfile = selectedProvider && selectedProvider !== 'openai' ? status.providers[selectedProvider] : null;
|
|
898
898
|
const selectedProviderBlockers = uniqueStrings([
|
|
899
899
|
...status.readiness.blockers,
|
|
900
900
|
...status.routing.blockers,
|
|
@@ -160,16 +160,51 @@ export async function prepareNativeCodexAuthBridge(env, opts = {}) {
|
|
|
160
160
|
applyCleanupProof(bridgeProof, result);
|
|
161
161
|
return result;
|
|
162
162
|
}
|
|
163
|
-
|
|
164
|
-
const hostAuth = { ...tempAuth, ...originalApiKeyFields };
|
|
165
|
-
const hostAuthText = `${JSON.stringify(hostAuth, null, 2)}\n`;
|
|
166
|
-
await writeAuthFileAtomic(sourceAuthCandidate, hostAuthText, async () => {
|
|
163
|
+
try {
|
|
167
164
|
await assertSourceAuthUnchanged(sourceAuthCandidate, sourceAuthIdentity, sourceAuthFingerprint);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
165
|
+
const hostAuth = { ...tempAuth, ...originalApiKeyFields };
|
|
166
|
+
const hostAuthText = `${JSON.stringify(hostAuth, null, 2)}\n`;
|
|
167
|
+
await writeAuthFileAtomic(sourceAuthCandidate, hostAuthText, async () => {
|
|
168
|
+
await assertSourceAuthUnchanged(sourceAuthCandidate, sourceAuthIdentity, sourceAuthFingerprint);
|
|
169
|
+
});
|
|
170
|
+
await removeOwnedTempRoot();
|
|
171
|
+
const result = { ok: true, status: 'cleaned', outcome: 'refreshed_persisted', cleanup_required: false, blockers: [] };
|
|
172
|
+
applyCleanupProof(bridgeProof, result);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (String(error?.message || error) !== 'native_codex_auth_source_conflict')
|
|
177
|
+
throw error;
|
|
178
|
+
const currentText = await readValidatedAuthFile(sourceAuthCandidate, {
|
|
179
|
+
requirePrivateMode: true,
|
|
180
|
+
requireSingleLink: true,
|
|
181
|
+
errorPrefix: 'native_codex_auth_source'
|
|
182
|
+
});
|
|
183
|
+
const currentAuth = parseStrictChatGptAuth(currentText);
|
|
184
|
+
const sameAccount = String(currentAuth.tokens?.account_id || '') === String(tempAuth.tokens?.account_id || '');
|
|
185
|
+
const hostRefreshedAt = Date.parse(String(currentAuth.last_refresh || ''));
|
|
186
|
+
const oursRefreshedAt = Date.parse(String(tempAuth.last_refresh || ''));
|
|
187
|
+
const hostWins = !sameAccount
|
|
188
|
+
|| !Number.isFinite(oursRefreshedAt)
|
|
189
|
+
|| (Number.isFinite(hostRefreshedAt) && hostRefreshedAt >= oursRefreshedAt);
|
|
190
|
+
if (hostWins) {
|
|
191
|
+
await removeOwnedTempRoot();
|
|
192
|
+
const result = { ok: true, status: 'cleaned', outcome: 'host_newer_kept', cleanup_required: false, blockers: [] };
|
|
193
|
+
applyCleanupProof(bridgeProof, result);
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
const currentStat = await fsp.lstat(sourceAuthCandidate);
|
|
197
|
+
const retryIdentity = { dev: currentStat.dev, ino: currentStat.ino };
|
|
198
|
+
const retryFingerprint = fingerprintText(currentText);
|
|
199
|
+
const mergedAuth = { ...currentAuth, tokens: tempAuth.tokens, last_refresh: tempAuth.last_refresh };
|
|
200
|
+
await writeAuthFileAtomic(sourceAuthCandidate, `${JSON.stringify(mergedAuth, null, 2)}\n`, async () => {
|
|
201
|
+
await assertSourceAuthUnchanged(sourceAuthCandidate, retryIdentity, retryFingerprint);
|
|
202
|
+
});
|
|
203
|
+
await removeOwnedTempRoot();
|
|
204
|
+
const result = { ok: true, status: 'cleaned', outcome: 'refreshed_persisted_after_conflict', cleanup_required: false, blockers: [] };
|
|
205
|
+
applyCleanupProof(bridgeProof, result);
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
173
208
|
}
|
|
174
209
|
catch (error) {
|
|
175
210
|
const reason = String(error?.message || error || 'native_codex_auth_cleanup_failed');
|
|
@@ -54,6 +54,44 @@ export function buildProviderUpstreamHeaders(inbound, context, upstreamHost) {
|
|
|
54
54
|
injectCredential(result, context.providerId, context.authTransport, context.credential);
|
|
55
55
|
return result;
|
|
56
56
|
}
|
|
57
|
+
const OFFICIAL_PASSTHROUGH_STRIP = new Set([
|
|
58
|
+
'forwarded', 'proxy-authorization', 'x-api-key', 'x-codex-lb-api-key',
|
|
59
|
+
'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-port', 'x-forwarded-proto', 'x-real-ip',
|
|
60
|
+
'host',
|
|
61
|
+
]);
|
|
62
|
+
export function buildOfficialPassthroughHeaders(inbound, upstreamHost) {
|
|
63
|
+
const result = {};
|
|
64
|
+
const dynamic = connectionTokens(inbound);
|
|
65
|
+
for (const [rawName, rawValue] of Object.entries(inbound)) {
|
|
66
|
+
if (rawValue === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
const name = rawName.toLowerCase();
|
|
69
|
+
if (HOP_BY_HOP.has(name) || dynamic.has(name))
|
|
70
|
+
continue;
|
|
71
|
+
if (name.startsWith(INTERNAL_PREFIX))
|
|
72
|
+
continue;
|
|
73
|
+
if (OFFICIAL_PASSTHROUGH_STRIP.has(name))
|
|
74
|
+
continue;
|
|
75
|
+
result[name] = rawValue;
|
|
76
|
+
}
|
|
77
|
+
result.host = upstreamHost;
|
|
78
|
+
if (result['x-codex-lb-api-key'] !== undefined)
|
|
79
|
+
throw new DesktopBridgeError('bridge_provider_credential_invalid');
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
export function buildOfficialPassthroughWebSocketHeaders(inbound, upstreamHost) {
|
|
83
|
+
const result = buildOfficialPassthroughHeaders(inbound, upstreamHost);
|
|
84
|
+
for (const [rawName, rawValue] of Object.entries(inbound)) {
|
|
85
|
+
if (rawValue === undefined)
|
|
86
|
+
continue;
|
|
87
|
+
const name = rawName.toLowerCase();
|
|
88
|
+
if (WEBSOCKET_HEADER_ALLOWLIST.has(name))
|
|
89
|
+
result[name] = rawValue;
|
|
90
|
+
}
|
|
91
|
+
result.connection = 'Upgrade';
|
|
92
|
+
result.upgrade = 'websocket';
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
57
95
|
export function buildProviderWebSocketHeaders(inbound, context, upstreamHost) {
|
|
58
96
|
const result = buildProviderUpstreamHeaders(inbound, context, upstreamHost);
|
|
59
97
|
for (const [rawName, rawValue] of Object.entries(inbound)) {
|
|
@@ -2,7 +2,8 @@ import http, {} from 'node:http';
|
|
|
2
2
|
import https from 'node:https';
|
|
3
3
|
import zlib from 'node:zlib';
|
|
4
4
|
import { pipeline } from 'node:stream/promises';
|
|
5
|
-
import {
|
|
5
|
+
import { BRIDGE_OFFICIAL_ROUTE_ID } from '../bridge-contracts.js';
|
|
6
|
+
import { buildOfficialPassthroughHeaders, buildProviderUpstreamHeaders, rewriteResponseHeaders } from './header-policy.js';
|
|
6
7
|
import { createDesktopBridgeRejectionLogger } from './rejection-log.js';
|
|
7
8
|
import { resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader } from './security.js';
|
|
8
9
|
import { desktopBridgeListenOrigin } from './state.js';
|
|
@@ -60,6 +61,8 @@ async function readBoundedBody(req, maximum) {
|
|
|
60
61
|
return Buffer.concat(chunks);
|
|
61
62
|
}
|
|
62
63
|
async function resolveCredential(config, route) {
|
|
64
|
+
if (route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID)
|
|
65
|
+
throw new DesktopBridgeError('bridge_provider_route_unavailable');
|
|
63
66
|
const provider = config.providers[route.provider_id];
|
|
64
67
|
if (!provider)
|
|
65
68
|
throw new DesktopBridgeError('bridge_provider_route_unavailable');
|
|
@@ -107,7 +110,7 @@ export async function prepareDesktopBridgeRequest(req, config) {
|
|
|
107
110
|
payload.model = route.upstream_model;
|
|
108
111
|
body = Buffer.from(JSON.stringify(payload));
|
|
109
112
|
}
|
|
110
|
-
const credential = await resolveCredential(config, route);
|
|
113
|
+
const credential = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID ? null : await resolveCredential(config, route);
|
|
111
114
|
return { body, route, credential, contentEncodingStripped };
|
|
112
115
|
}
|
|
113
116
|
function connectTimeout(request, config) {
|
|
@@ -251,21 +254,30 @@ class StalePooledSocketFailure extends Error {
|
|
|
251
254
|
export async function forwardHttp(req, res, config, prepared, authenticatedLocalBaseUrl = desktopBridgeListenOrigin(config)) {
|
|
252
255
|
try {
|
|
253
256
|
const request = prepared || await prepareDesktopBridgeRequest(req, config);
|
|
254
|
-
const
|
|
255
|
-
|
|
257
|
+
const official = request.route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID;
|
|
258
|
+
const provider = official ? null : config.providers[request.route.provider_id];
|
|
259
|
+
if (!official && !provider)
|
|
256
260
|
throw new DesktopBridgeError('bridge_provider_route_unavailable');
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
261
|
+
const remote = official ? config.officialRemote : provider?.remote;
|
|
262
|
+
if (!remote)
|
|
263
|
+
throw new DesktopBridgeError('bridge_official_passthrough_unavailable');
|
|
264
|
+
const upstreamBaseUrl = official ? remote.baseUrl : provider.base_url;
|
|
265
|
+
if (!official && !request.credential)
|
|
266
|
+
throw new DesktopBridgeError('bridge_provider_credential_invalid');
|
|
267
|
+
const target = resolveDesktopBridgeTarget(req.url, remote);
|
|
268
|
+
const transport = remote.secure ? https : http;
|
|
269
|
+
const headers = official
|
|
270
|
+
? buildOfficialPassthroughHeaders(req.headers, target.host)
|
|
271
|
+
: buildProviderUpstreamHeaders(req.headers, {
|
|
272
|
+
providerId: provider.provider_id, authTransport: provider.auth_transport, credential: request.credential,
|
|
273
|
+
}, target.host);
|
|
262
274
|
if (request.body)
|
|
263
275
|
headers['content-length'] = String(request.body.length);
|
|
264
276
|
else
|
|
265
277
|
delete headers['content-length'];
|
|
266
278
|
if (request.contentEncodingStripped)
|
|
267
279
|
delete headers['content-encoding'];
|
|
268
|
-
const agent = upstreamAgent(
|
|
280
|
+
const agent = upstreamAgent(remote.secure, `${remote.address}:${remote.port}`, config.idleTimeoutMs);
|
|
269
281
|
const replayable = Buffer.isBuffer(request.body);
|
|
270
282
|
const attempt = (useFreshConnection, canReplay) => new Promise((resolve, reject) => {
|
|
271
283
|
let responseStarted = false;
|
|
@@ -281,10 +293,10 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
281
293
|
error ? reject(error) : resolve();
|
|
282
294
|
};
|
|
283
295
|
const upstream = transport.request({
|
|
284
|
-
protocol: target.protocol, hostname:
|
|
285
|
-
port:
|
|
296
|
+
protocol: target.protocol, hostname: remote.address, family: remote.family,
|
|
297
|
+
port: remote.port, method: req.method, path: `${target.pathname}${target.search}`, headers,
|
|
286
298
|
agent: useFreshConnection ? false : agent,
|
|
287
|
-
...(
|
|
299
|
+
...(remote.tlsServername ? { servername: remote.tlsServername } : {}),
|
|
288
300
|
});
|
|
289
301
|
connectTimeout(upstream, config);
|
|
290
302
|
upstream.setTimeout(config.idleTimeoutMs, () => upstream.destroy(new DesktopBridgeError('bridge_upstream_idle_timeout')));
|
|
@@ -299,6 +311,36 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
299
311
|
});
|
|
300
312
|
upstream.once('response', (response) => {
|
|
301
313
|
const statusCode = response.statusCode || 502;
|
|
314
|
+
if (official) {
|
|
315
|
+
if (statusCode >= 400 && TRANSIENT_UPSTREAM_STATUSES.has(statusCode) && replayable && canReplay) {
|
|
316
|
+
response.resume();
|
|
317
|
+
responseStarted = true;
|
|
318
|
+
finish(new StalePooledSocketFailure(new DesktopBridgeError('bridge_upstream_transient_mislabel')));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (statusCode >= 400) {
|
|
322
|
+
logHttpRejection({
|
|
323
|
+
code: `bridge_upstream_status_${statusCode}`,
|
|
324
|
+
transport: 'http',
|
|
325
|
+
...(req.method === undefined ? {} : { method: req.method }),
|
|
326
|
+
...(req.url === undefined ? {} : { url: req.url }),
|
|
327
|
+
status: statusCode,
|
|
328
|
+
provider_id: BRIDGE_OFFICIAL_ROUTE_ID,
|
|
329
|
+
public_model: request.route.public_model,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
responseStarted = true;
|
|
333
|
+
try {
|
|
334
|
+
res.writeHead(statusCode, rewriteResponseHeaders(response.headers, upstreamBaseUrl, authenticatedLocalBaseUrl));
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
response.destroy(error instanceof Error ? error : undefined);
|
|
338
|
+
finish(error);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
void pipeline(response, res).then(() => finish(), finish);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
302
344
|
if (statusCode >= 400) {
|
|
303
345
|
void readRedactedUpstreamError(response).then(({ upstreamCode, upstreamType }) => {
|
|
304
346
|
const transientMislabel = isTransientUpstreamFailure(statusCode, upstreamType, upstreamCode);
|
|
@@ -323,7 +365,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
323
365
|
public_model: request.route.public_model,
|
|
324
366
|
});
|
|
325
367
|
responseStarted = true;
|
|
326
|
-
const responseHeaders = rewriteResponseHeaders(response.headers,
|
|
368
|
+
const responseHeaders = rewriteResponseHeaders(response.headers, upstreamBaseUrl, authenticatedLocalBaseUrl);
|
|
327
369
|
responseHeaders['content-length'] = String(body.length);
|
|
328
370
|
delete responseHeaders['transfer-encoding'];
|
|
329
371
|
if (transientMislabel)
|
|
@@ -337,7 +379,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
337
379
|
}
|
|
338
380
|
responseStarted = true;
|
|
339
381
|
try {
|
|
340
|
-
res.writeHead(statusCode, rewriteResponseHeaders(response.headers,
|
|
382
|
+
res.writeHead(statusCode, rewriteResponseHeaders(response.headers, upstreamBaseUrl, authenticatedLocalBaseUrl));
|
|
341
383
|
}
|
|
342
384
|
catch (error) {
|
|
343
385
|
response.destroy(error instanceof Error ? error : undefined);
|
|
@@ -365,7 +407,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
365
407
|
remainingReplays -= 1;
|
|
366
408
|
if (!useFreshConnection) {
|
|
367
409
|
agent.destroy();
|
|
368
|
-
upstreamAgents.delete(`${
|
|
410
|
+
upstreamAgents.delete(`${remote.secure ? 'https' : 'http'}:${remote.address}:${remote.port}`);
|
|
369
411
|
}
|
|
370
412
|
logHttpRejection({
|
|
371
413
|
code: `bridge_upstream_socket_stale_replayed:${underlyingErrorCode(error.reason) || 'unknown'}`,
|
|
@@ -33,6 +33,7 @@ export function createDesktopBridgeRejectionLogger(options = {}) {
|
|
|
33
33
|
write(`${JSON.stringify({
|
|
34
34
|
schema: DESKTOP_BRIDGE_LOG_SCHEMA,
|
|
35
35
|
sks_version: PACKAGE_VERSION,
|
|
36
|
+
at: new Date(now()).toISOString(),
|
|
36
37
|
secret_fields_redacted: true,
|
|
37
38
|
...payload,
|
|
38
39
|
})}\n`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import dns from 'node:dns/promises';
|
|
2
2
|
import net from 'node:net';
|
|
3
|
+
import { BRIDGE_OFFICIAL_ROUTE_ID } from '../bridge-contracts.js';
|
|
3
4
|
import { canonicalizeBridgeModelId, normalizeBridgeUpstreamModelId, } from '../route-index.js';
|
|
4
5
|
import { DESKTOP_BRIDGE_CLIENT_PATH_PREFIX, DesktopBridgeError } from './types.js';
|
|
5
6
|
const MIN_HIGH_PORT = 49_152;
|
|
@@ -150,6 +151,19 @@ function canonicalPublicModel(value) {
|
|
|
150
151
|
throw new DesktopBridgeError('catalog_model_route_missing');
|
|
151
152
|
return model;
|
|
152
153
|
}
|
|
154
|
+
export function desktopBridgeOfficialPassthroughEnabled(config) {
|
|
155
|
+
return Boolean(config.officialPassthrough?.baseUrl);
|
|
156
|
+
}
|
|
157
|
+
function officialRouteContext(publicModel, policy, upstreamModel) {
|
|
158
|
+
return {
|
|
159
|
+
provider_id: BRIDGE_OFFICIAL_ROUTE_ID,
|
|
160
|
+
public_model: publicModel,
|
|
161
|
+
upstream_model: upstreamModel || publicModel,
|
|
162
|
+
catalog_generation: policy.catalog_generation,
|
|
163
|
+
route_policy_generation: policy.policy_generation,
|
|
164
|
+
session_pin: null,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
153
167
|
export function resolveBridgeRequestRoute(request, policy, pins) {
|
|
154
168
|
const publicModel = canonicalPublicModel(request.public_model);
|
|
155
169
|
const sessionId = request.session_id ? canonicalSessionId(request.session_id) : null;
|
|
@@ -179,6 +193,8 @@ export function resolveBridgeRequestRoute(request, policy, pins) {
|
|
|
179
193
|
const route = policy.model_routes[publicModel];
|
|
180
194
|
if (!route)
|
|
181
195
|
throw new DesktopBridgeError('catalog_model_route_missing');
|
|
196
|
+
if (route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID)
|
|
197
|
+
return officialRouteContext(publicModel, policy, route.upstream_model);
|
|
182
198
|
const nextPin = sessionId ? {
|
|
183
199
|
thread_id: sessionId,
|
|
184
200
|
provider_id: route.provider_id,
|
|
@@ -199,8 +215,36 @@ export function resolveBridgeRequestRoute(request, policy, pins) {
|
|
|
199
215
|
}
|
|
200
216
|
export function assertDesktopBridgeRouteContext(request, config) {
|
|
201
217
|
const resolver = config.resolveRequestRoute || resolveBridgeRequestRoute;
|
|
202
|
-
const route = resolver(request, config.routePolicy, config.providerSessionPins);
|
|
203
218
|
const policy = config.routePolicy;
|
|
219
|
+
let route;
|
|
220
|
+
try {
|
|
221
|
+
route = resolver(request, policy, config.providerSessionPins);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (desktopBridgeOfficialPassthroughEnabled(config)
|
|
225
|
+
&& error instanceof DesktopBridgeError
|
|
226
|
+
&& (error.code === 'catalog_model_route_missing' || error.code === 'session_pin_route_unavailable')) {
|
|
227
|
+
const model = canonicalizeBridgeModelId(request.public_model) || '';
|
|
228
|
+
const live = model ? policy.model_routes[model] : undefined;
|
|
229
|
+
if (!live || live.provider_id === BRIDGE_OFFICIAL_ROUTE_ID) {
|
|
230
|
+
route = officialRouteContext(model, policy, live?.upstream_model);
|
|
231
|
+
}
|
|
232
|
+
else
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
else
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
if (route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID) {
|
|
239
|
+
if (!desktopBridgeOfficialPassthroughEnabled(config))
|
|
240
|
+
throw new DesktopBridgeError('bridge_official_passthrough_unavailable');
|
|
241
|
+
if (route.catalog_generation !== policy.catalog_generation || route.route_policy_generation !== policy.policy_generation) {
|
|
242
|
+
throw new DesktopBridgeError('session_pin_route_unavailable');
|
|
243
|
+
}
|
|
244
|
+
if (route.session_pin)
|
|
245
|
+
throw new DesktopBridgeError('bridge_session_pin_invalid');
|
|
246
|
+
return route;
|
|
247
|
+
}
|
|
204
248
|
const expected = policy.model_routes[route.public_model];
|
|
205
249
|
if (!expected || expected.provider_id !== route.provider_id || expected.upstream_model !== route.upstream_model) {
|
|
206
250
|
throw new DesktopBridgeError('catalog_model_route_missing');
|
|
@@ -399,9 +443,12 @@ function assertRegistryAndPolicy(config, registry) {
|
|
|
399
443
|
if (policy.schema !== 'sks.bridge-routing-policy.v1' || policy.fallback !== 'none' || !policy.catalog_generation || !policy.policy_generation) {
|
|
400
444
|
throw new DesktopBridgeError('bridge_route_policy_invalid');
|
|
401
445
|
}
|
|
446
|
+
const officialConfigured = desktopBridgeOfficialPassthroughEnabled(config);
|
|
402
447
|
for (const [model, route] of Object.entries(policy.model_routes)) {
|
|
448
|
+
const routeTargetAllowed = ids.includes(route.provider_id)
|
|
449
|
+
|| (route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID && officialConfigured);
|
|
403
450
|
if (canonicalizeBridgeModelId(model) !== model
|
|
404
|
-
|| !
|
|
451
|
+
|| !routeTargetAllowed
|
|
405
452
|
|| normalizeBridgeUpstreamModelId(route.upstream_model) !== route.upstream_model) {
|
|
406
453
|
throw new DesktopBridgeError('bridge_route_policy_invalid');
|
|
407
454
|
}
|
|
@@ -460,6 +507,8 @@ export function validateDesktopBridgeConfig(config) {
|
|
|
460
507
|
throw new DesktopBridgeError('bridge_client_capability_invalid');
|
|
461
508
|
if (typeof config.resolveProviderCredential !== 'function')
|
|
462
509
|
throw new DesktopBridgeError('bridge_provider_credential_resolver_missing');
|
|
510
|
+
if (config.officialPassthrough)
|
|
511
|
+
validateRemoteUrl(config.officialPassthrough.baseUrl);
|
|
463
512
|
assertRegistryAndPolicy(config, config.providerRegistry);
|
|
464
513
|
if (!Object.values(config.providerRegistry.providers).some((provider) => provider.enabled)) {
|
|
465
514
|
throw new DesktopBridgeError('bridge_provider_registry_no_enabled_provider');
|
|
@@ -475,7 +524,10 @@ export async function prepareDesktopBridgeConfig(config, lookup = defaultLookup)
|
|
|
475
524
|
return [id, prepared];
|
|
476
525
|
}));
|
|
477
526
|
const providers = Object.fromEntries(entries);
|
|
478
|
-
|
|
527
|
+
const officialRemote = config.officialPassthrough
|
|
528
|
+
? await resolveDesktopBridgeRemoteTarget(config.officialPassthrough.baseUrl, lookup)
|
|
529
|
+
: null;
|
|
530
|
+
return { ...config, providers, officialRemote };
|
|
479
531
|
}
|
|
480
532
|
export function validatePreparedDesktopBridgeConfig(config) {
|
|
481
533
|
validateDesktopBridgeConfig(config);
|
|
@@ -12,6 +12,7 @@ export const DESKTOP_BRIDGE_ALLOWED_PATH_PREFIXES = [
|
|
|
12
12
|
'/api/v1/',
|
|
13
13
|
'/v1/',
|
|
14
14
|
];
|
|
15
|
+
export const DESKTOP_BRIDGE_OFFICIAL_UPSTREAM_BASE_URL = 'https://chatgpt.com/backend-api/codex';
|
|
15
16
|
export class DesktopBridgeError extends Error {
|
|
16
17
|
code;
|
|
17
18
|
constructor(code, options) {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import net from 'node:net';
|
|
3
3
|
import tls from 'node:tls';
|
|
4
|
-
import {
|
|
4
|
+
import { BRIDGE_OFFICIAL_ROUTE_ID } from '../bridge-contracts.js';
|
|
5
|
+
import { buildOfficialPassthroughWebSocketHeaders, buildProviderWebSocketHeaders } from './header-policy.js';
|
|
5
6
|
import { createDesktopBridgeRejectionLogger } from './rejection-log.js';
|
|
6
7
|
import { rewriteLocationHeader } from './location-rewrite.js';
|
|
7
|
-
import { resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader, canonicalSessionId } from './security.js';
|
|
8
|
+
import { desktopBridgeOfficialPassthroughEnabled, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader, canonicalSessionId } from './security.js';
|
|
8
9
|
import { desktopBridgeListenOrigin } from './state.js';
|
|
9
10
|
import { DESKTOP_BRIDGE_DIAGNOSTIC_PROTOCOL, DesktopBridgeError, } from './types.js';
|
|
10
11
|
const MAX_HEAD = 64 * 1024;
|
|
@@ -82,7 +83,7 @@ export async function prepareDesktopBridgeWebSocketRequest(req, config) {
|
|
|
82
83
|
const sessionIdentity = resolveCodexSessionIdentity(req.headers);
|
|
83
84
|
const pinnedModel = websocketPinnedModel(sessionIdentity.thread_id, config);
|
|
84
85
|
const publicModel = singleBridgeHeader(req.headers, 'x-sks-model') || pinnedModel || '';
|
|
85
|
-
if (!publicModel) {
|
|
86
|
+
if (!publicModel && !(desktopBridgeOfficialPassthroughEnabled(config) && config.officialRemote)) {
|
|
86
87
|
throw new DesktopBridgeError('bridge_websocket_route_unresolvable');
|
|
87
88
|
}
|
|
88
89
|
const route = await resolveAndBindDesktopBridgeRouteContext({
|
|
@@ -91,6 +92,8 @@ export async function prepareDesktopBridgeWebSocketRequest(req, config) {
|
|
|
91
92
|
pathname: new URL(req.url || '/', 'http://bridge.invalid').pathname,
|
|
92
93
|
transport: 'websocket', headers: req.headers,
|
|
93
94
|
}, config);
|
|
95
|
+
if (route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID)
|
|
96
|
+
return { route, provider: null, credential: null };
|
|
94
97
|
const provider = config.providers[route.provider_id];
|
|
95
98
|
if (!provider)
|
|
96
99
|
throw new DesktopBridgeError('bridge_provider_route_unavailable');
|
|
@@ -142,11 +145,18 @@ export async function forwardWebSocket(req, client, head, config, authenticatedL
|
|
|
142
145
|
writeUpgradeFailure(client, error, req);
|
|
143
146
|
return;
|
|
144
147
|
}
|
|
145
|
-
const { provider, credential } = prepared;
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
const { provider, credential, route } = prepared;
|
|
149
|
+
const official = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID;
|
|
150
|
+
const remote = official ? config.officialRemote : provider?.remote;
|
|
151
|
+
if (!remote) {
|
|
152
|
+
writeUpgradeFailure(client, new DesktopBridgeError(official ? 'bridge_official_passthrough_unavailable' : 'bridge_provider_route_unavailable'), req);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const upstreamBaseUrl = official ? remote.baseUrl : provider.base_url;
|
|
156
|
+
const target = resolveDesktopBridgeTarget(req.url, remote);
|
|
157
|
+
const upstream = remote.secure
|
|
158
|
+
? tls.connect({ host: remote.address, port: remote.port, ...(remote.tlsServername ? { servername: remote.tlsServername } : {}) })
|
|
159
|
+
: net.connect({ host: remote.address, port: remote.port, family: remote.family });
|
|
150
160
|
const key = String(req.headers['sec-websocket-key'] || '');
|
|
151
161
|
const requestedProtocol = String(req.headers['sec-websocket-protocol'] || '').split(',')[0]?.trim() || null;
|
|
152
162
|
let connected = false;
|
|
@@ -165,7 +175,9 @@ export async function forwardWebSocket(req, client, head, config, authenticatedL
|
|
|
165
175
|
const clientSocket = client;
|
|
166
176
|
if (typeof clientSocket.setKeepAlive === 'function')
|
|
167
177
|
clientSocket.setKeepAlive(true, 30_000);
|
|
168
|
-
const headers =
|
|
178
|
+
const headers = official
|
|
179
|
+
? buildOfficialPassthroughWebSocketHeaders(req.headers, target.host)
|
|
180
|
+
: buildProviderWebSocketHeaders(req.headers, { providerId: provider.provider_id, authTransport: provider.auth_transport, credential: credential }, target.host);
|
|
169
181
|
upstream.write([`${req.method || 'GET'} ${target.pathname}${target.search} HTTP/1.1`, ...serializeHeaders(headers), '', ''].join('\r\n'));
|
|
170
182
|
if (head.length)
|
|
171
183
|
upstream.write(head);
|
|
@@ -183,7 +195,7 @@ export async function forwardWebSocket(req, client, head, config, authenticatedL
|
|
|
183
195
|
const remaining = response.subarray(boundary + 4);
|
|
184
196
|
try {
|
|
185
197
|
validateUpgrade(raw, key, requestedProtocol);
|
|
186
|
-
client.write(rewriteUpgradeResponseHead(raw,
|
|
198
|
+
client.write(rewriteUpgradeResponseHead(raw, upstreamBaseUrl, authenticatedLocalBaseUrl));
|
|
187
199
|
if (remaining.length)
|
|
188
200
|
client.write(remaining);
|
|
189
201
|
}
|
|
@@ -196,7 +208,7 @@ export async function forwardWebSocket(req, client, head, config, authenticatedL
|
|
|
196
208
|
};
|
|
197
209
|
upstream.on('data', onData);
|
|
198
210
|
};
|
|
199
|
-
if (
|
|
211
|
+
if (remote.secure)
|
|
200
212
|
upstream.once('secureConnect', onConnected);
|
|
201
213
|
else
|
|
202
214
|
upstream.once('connect', onConnected);
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
export function authSemanticIdentityPreserved(before, after) {
|
|
2
2
|
if (before.path !== after.path || before.exists !== after.exists)
|
|
3
3
|
return false;
|
|
4
|
-
if (before.sha256 !== after.sha256)
|
|
5
|
-
return false;
|
|
6
4
|
const beforeIsOAuth = before.mode === 'chatgpt_oauth' || before.mode === 'mixed';
|
|
7
5
|
const afterIsOAuth = after.mode === 'chatgpt_oauth' || after.mode === 'mixed';
|
|
8
6
|
if (beforeIsOAuth || afterIsOAuth) {
|
|
@@ -11,7 +9,7 @@ export function authSemanticIdentityPreserved(before, after) {
|
|
|
11
9
|
&& before.semantic_fingerprint !== null
|
|
12
10
|
&& before.semantic_fingerprint === after.semantic_fingerprint;
|
|
13
11
|
}
|
|
14
|
-
return
|
|
12
|
+
return before.sha256 === after.sha256;
|
|
15
13
|
}
|
|
16
14
|
export function buildDesktopBridgeMigrationReceipt(input) {
|
|
17
15
|
const common = {
|
|
@@ -17,7 +17,7 @@ const SETTINGS_V1_KEYS = new Set([
|
|
|
17
17
|
const SETTINGS_V2_KEYS = new Set([
|
|
18
18
|
'schema', 'listen_host', 'listen_port', 'provider_registry', 'route_policy',
|
|
19
19
|
'provider_session_pins', 'client_capability_sha256', 'allowed_origins',
|
|
20
|
-
'connect_timeout_ms', 'idle_timeout_ms',
|
|
20
|
+
'connect_timeout_ms', 'idle_timeout_ms', 'official_passthrough',
|
|
21
21
|
]);
|
|
22
22
|
const TRANSFERABLE_V1_KEYS = [
|
|
23
23
|
'listen_host', 'listen_port', 'allowed_origins', 'connect_timeout_ms', 'idle_timeout_ms',
|
|
@@ -90,8 +90,7 @@ async function migrateDesktopBridgeConfigUnlocked(input) {
|
|
|
90
90
|
const authPreserved = authSemanticIdentityPreserved(authBefore, authAfter);
|
|
91
91
|
const configSha = sha256(currentConfig);
|
|
92
92
|
const configUnchanged = await fileSha256OrMissing(configPath) === configSha;
|
|
93
|
-
|
|
94
|
-
if (!authPreserved || !configUnchanged || !authBytesUnchanged) {
|
|
93
|
+
if (!authPreserved || !configUnchanged) {
|
|
95
94
|
return {
|
|
96
95
|
...baseResult,
|
|
97
96
|
ok: false,
|
|
@@ -101,8 +100,7 @@ async function migrateDesktopBridgeConfigUnlocked(input) {
|
|
|
101
100
|
auth_semantic_identity_preserved: authPreserved,
|
|
102
101
|
blockers: [
|
|
103
102
|
...(!configUnchanged ? ['desktop_bridge_config_changed_during_noop'] : []),
|
|
104
|
-
...(!authPreserved ? ['desktop_oauth_identity_changed'] : [])
|
|
105
|
-
...(authPreserved && !authBytesUnchanged ? ['desktop_auth_changed_during_noop'] : [])
|
|
103
|
+
...(!authPreserved ? ['desktop_oauth_identity_changed'] : [])
|
|
106
104
|
]
|
|
107
105
|
};
|
|
108
106
|
}
|
|
@@ -5,12 +5,12 @@ import { listOpenRouterModels } from '../../providers/openrouter/openrouter-acco
|
|
|
5
5
|
import { buildCombinedBridgeCatalog, readActiveCombinedBridgeCatalog, stageCombinedBridgeCatalog } from '../combined-catalog.js';
|
|
6
6
|
import { COMBINED_BRIDGE_CATALOG_TTL_MS } from '../combined-catalog/contracts.js';
|
|
7
7
|
import { codexLbEnvPath, codexLbMetadataPath, loadCodexLbEnv, readCodexLbModelCatalog } from '../codex-lb-env.js';
|
|
8
|
-
import { bootstrapExistingDesktopBridgeService, desktopBridgeServicePaths, desktopBridgeServiceStatus, resolveDesktopBridgeActivationSettings } from '../desktop-service.js';
|
|
8
|
+
import { bootstrapExistingDesktopBridgeService, desktopBridgeServicePaths, desktopBridgeServiceStatus, readDesktopBridgeServiceSettings, resolveDesktopBridgeActivationSettings, resolveEffectiveOfficialModelsMode } from '../desktop-service.js';
|
|
9
9
|
import { inspectHistoricalDesktopBridgeIntent, migrateDesktopBridgeConfig } from '../desktop-bridge-migration.js';
|
|
10
10
|
import { rollbackDesktopBridgeUnificationReceipt } from '../migration-receipt.js';
|
|
11
11
|
import { recordProviderCredentialValidation } from '../provider-credentials.js';
|
|
12
12
|
import { bridgeProviderRegistryPath, buildStoredBridgeProviderRegistry, loadStoredBridgeProviderRegistry, resolveBridgeProviderRegistry, serializeStoredBridgeProviderRegistry } from '../provider-registry.js';
|
|
13
|
-
import { buildBridgeRoutingPolicy, readBridgeRoutingPolicy } from '../provider-route-policy.js';
|
|
13
|
+
import { applyOfficialModelPassthrough, buildBridgeRoutingPolicy, readBridgeRoutingPolicy } from '../provider-route-policy.js';
|
|
14
14
|
import { sha256Stable } from '../route-index.js';
|
|
15
15
|
import { bridgeBaseUrl, commandResult, controllerEnv, controllerPaths, nowIso, providerCode, providerRegistrySnapshot, resolveRawCredentials, resolveValidatedCredentials, safeCode, serializedSettings, stringArray, timeoutMs, unique } from './shared.js';
|
|
16
16
|
import { desktopBridgeStatusV3 } from './status.js';
|
|
@@ -76,12 +76,14 @@ export async function syncCatalogInternal(options) {
|
|
|
76
76
|
? previousDefault
|
|
77
77
|
: historicalDefault && readyProviders.includes(historicalDefault) ? historicalDefault
|
|
78
78
|
: readyProviders.length === 1 ? readyProviders[0] || null : null;
|
|
79
|
-
const
|
|
79
|
+
const persistedSettings = await readDesktopBridgeServiceSettings(desktopBridgeServicePaths(paths.home).settings_path).catch(() => null);
|
|
80
|
+
const officialModelsMode = await resolveEffectiveOfficialModelsMode(persistedSettings?.official_passthrough, { home: paths.home, authPath: paths.authPath });
|
|
81
|
+
const policy = applyOfficialModelPassthrough(buildBridgeRoutingPolicy({
|
|
80
82
|
route_index: build.route_index,
|
|
81
83
|
catalog_generation: build.catalog.generation,
|
|
82
84
|
default_provider_id: defaultProvider,
|
|
83
85
|
changed_at: nowIso(options)
|
|
84
|
-
});
|
|
86
|
+
}), { mode: officialModelsMode, changedAt: nowIso(options) });
|
|
85
87
|
let settings;
|
|
86
88
|
let managedBridgeBaseUrl;
|
|
87
89
|
try {
|
|
@@ -2,10 +2,11 @@ import path from 'node:path';
|
|
|
2
2
|
import { removeDesktopBridgeManagedConfig } from '../../../cli/install-helpers-codex-lb-config.js';
|
|
3
3
|
import { safeWriteCodexConfigToml } from '../../codex-runtime/codex-desktop-config-policy.js';
|
|
4
4
|
import { exists, readText } from '../../fsx.js';
|
|
5
|
-
import { bootstrapExistingDesktopBridgeService, desktopBridgeServiceStatus, installAndStartDesktopBridgeService, stopDesktopBridgeService } from '../desktop-service.js';
|
|
5
|
+
import { bootstrapExistingDesktopBridgeService, desktopBridgeServicePaths, desktopBridgeServiceStatus, installAndStartDesktopBridgeService, readDesktopBridgeServiceSettings, resolveEffectiveOfficialModelsMode, stopDesktopBridgeService } from '../desktop-service.js';
|
|
6
6
|
import { rollbackDesktopBridgeUnificationReceipt } from '../migration-receipt.js';
|
|
7
|
+
import { DESKTOP_BRIDGE_OFFICIAL_UPSTREAM_BASE_URL } from '../desktop-bridge/index.js';
|
|
7
8
|
import { resolveBridgeRequestRoute } from '../request-route-resolver.js';
|
|
8
|
-
import { setBridgeRoutingDefault, writeBridgeRoutingPolicy } from '../provider-route-policy.js';
|
|
9
|
+
import { applyOfficialModelPassthrough, buildBridgeRoutingPolicy, setBridgeRoutingDefault, writeBridgeRoutingPolicy } from '../provider-route-policy.js';
|
|
9
10
|
import { syncCatalogInternal } from './catalog.js';
|
|
10
11
|
import { commandResult, controllerPaths, nowIso, persistRuntimeSettings, providerCode, providerRegistrySnapshot, stringArray } from './shared.js';
|
|
11
12
|
import { desktopBridgeStatusV3, loadCore, statusFromCore } from './status.js';
|
|
@@ -70,6 +71,39 @@ export async function setDefaultProvider(providerId, options) {
|
|
|
70
71
|
const status = await desktopBridgeStatusV3(options);
|
|
71
72
|
return commandResult('route.set-default', true, status, { provider_id: providerId, policy_generation: policy.policy_generation }, [], options);
|
|
72
73
|
}
|
|
74
|
+
export async function setOfficialModelsMode(mode, options) {
|
|
75
|
+
const core = await loadCore(options);
|
|
76
|
+
if (!core.policy || !core.activeCatalog.ok)
|
|
77
|
+
throw new Error('bridge_route_policy_missing');
|
|
78
|
+
const settingsPath = options.settingsPath || desktopBridgeServicePaths(core.paths.home).settings_path;
|
|
79
|
+
const persisted = await readDesktopBridgeServiceSettings(settingsPath).catch(() => null);
|
|
80
|
+
const nextOfficial = {
|
|
81
|
+
enabled: persisted?.official_passthrough?.enabled ?? true,
|
|
82
|
+
base_url: persisted?.official_passthrough?.base_url || DESKTOP_BRIDGE_OFFICIAL_UPSTREAM_BASE_URL,
|
|
83
|
+
models: mode,
|
|
84
|
+
};
|
|
85
|
+
const effective = mode === 'auto'
|
|
86
|
+
? await resolveEffectiveOfficialModelsMode(nextOfficial, { home: core.paths.home })
|
|
87
|
+
: mode;
|
|
88
|
+
const policy = effective === 'passthrough'
|
|
89
|
+
? applyOfficialModelPassthrough(core.policy, { mode: 'passthrough', changedAt: nowIso(options) })
|
|
90
|
+
: buildBridgeRoutingPolicy({
|
|
91
|
+
route_index: core.activeCatalog.route_index,
|
|
92
|
+
catalog_generation: core.policy.catalog_generation,
|
|
93
|
+
default_provider_id: core.policy.default_provider_id,
|
|
94
|
+
changed_at: nowIso(options)
|
|
95
|
+
});
|
|
96
|
+
await writeBridgeRoutingPolicy(core.paths.routePolicyPath, policy, core.activeCatalog.route_index);
|
|
97
|
+
await persistRuntimeSettings({ ...core, policy, policyBlockers: [] }, {
|
|
98
|
+
...options,
|
|
99
|
+
settings: { ...(options.settings || {}), official_passthrough: nextOfficial },
|
|
100
|
+
});
|
|
101
|
+
const status = await desktopBridgeStatusV3(options);
|
|
102
|
+
const officialModels = Object.entries(policy.model_routes)
|
|
103
|
+
.filter(([, route]) => route.provider_id === 'openai')
|
|
104
|
+
.map(([model]) => model);
|
|
105
|
+
return commandResult('route.official-models', true, status, { mode, effective_mode: effective, official_models: officialModels, policy_generation: policy.policy_generation }, [], options);
|
|
106
|
+
}
|
|
73
107
|
export async function explainRoute(model, options) {
|
|
74
108
|
const core = await loadCore(options);
|
|
75
109
|
if (!core.policy || !core.activeCatalog.ok)
|
|
@@ -150,7 +150,8 @@ export function serializedSettings(settings) {
|
|
|
150
150
|
client_capability_sha256: settings.client_capability_sha256,
|
|
151
151
|
allowed_origins: settings.allowed_origins,
|
|
152
152
|
connect_timeout_ms: settings.connect_timeout_ms,
|
|
153
|
-
idle_timeout_ms: settings.idle_timeout_ms
|
|
153
|
+
idle_timeout_ms: settings.idle_timeout_ms,
|
|
154
|
+
official_passthrough: settings.official_passthrough
|
|
154
155
|
}, null, 2)}\n`;
|
|
155
156
|
}
|
|
156
157
|
export async function bridgeBaseUrl(settings, options) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { listSelectableModels, selectExposedModels, syncCatalog } from './desktop-controller-v3/catalog.js';
|
|
2
|
-
import { explainRoute, ensureDesktopBridge, repairDesktopBridge, rollbackDesktopBridge, setDefaultProvider, unmanageDesktopBridge } from './desktop-controller-v3/lifecycle-commands.js';
|
|
2
|
+
import { explainRoute, ensureDesktopBridge, repairDesktopBridge, rollbackDesktopBridge, setDefaultProvider, setOfficialModelsMode, unmanageDesktopBridge } from './desktop-controller-v3/lifecycle-commands.js';
|
|
3
3
|
import { configureProvider, removeCredential, setProviderState, validateProvider } from './desktop-controller-v3/provider-commands.js';
|
|
4
4
|
import { commandResult, safeCode } from './desktop-controller-v3/shared.js';
|
|
5
5
|
import { desktopBridgeStatusV3 } from './desktop-controller-v3/status.js';
|
|
@@ -56,6 +56,8 @@ export async function executeDesktopBridgeCommandV3(request, options = {}) {
|
|
|
56
56
|
}
|
|
57
57
|
if (request.operation === 'route.set-default')
|
|
58
58
|
return setDefaultProvider(request.provider_id, options);
|
|
59
|
+
if (request.operation === 'route.official-models')
|
|
60
|
+
return setOfficialModelsMode(request.mode, options);
|
|
59
61
|
if (request.operation === 'route.explain')
|
|
60
62
|
return explainRoute(request.model, options);
|
|
61
63
|
if (request.operation === 'unmanage')
|
|
@@ -9,9 +9,11 @@ import { loadCodexLbEnv } from './codex-lb-env.js';
|
|
|
9
9
|
import { resolveOpenRouterApiKey } from '../providers/openrouter/openrouter-secret-store.js';
|
|
10
10
|
import { cleanupRetiredDesktopBridgeRuntime, prepareRetiredDesktopBridgeRuntime, } from './desktop-bridge-migration/retired-runtime-cleanup.js';
|
|
11
11
|
import { canonicalizeBridgeModelId, normalizeBridgeUpstreamModelId, sha256Stable } from './route-index.js';
|
|
12
|
+
import { captureCodexAuthSnapshot } from './desktop-auth-invariant.js';
|
|
13
|
+
import { applyOfficialModelPassthrough, bridgeRoutePolicyPath, writeBridgeRoutingPolicy } from './provider-route-policy.js';
|
|
12
14
|
import { desktopBridgeRuntimeVersion, desktopBridgeRuntimeVersionStale } from './desktop-bridge/state.js';
|
|
13
15
|
import { PACKAGE_VERSION } from '../version.js';
|
|
14
|
-
import { DESKTOP_BRIDGE_ALLOWED_PATH_PREFIXES, DESKTOP_BRIDGE_LAUNCHD_LABEL, desktopBridgeConfigGeneration, desktopBridgeLaunchdPlistPath, desktopBridgeProcessExists, desktopBridgeStatePath, getDesktopBridgeStatus, preflightDesktopBridge, readDesktopBridgeState, safeBridgeErrorCode, writeDesktopBridgeLaunchdPlist, selectAvailableDesktopBridgePort, startPreparedDesktopBridge, DESKTOP_BRIDGE_STATE_SCHEMA, DesktopBridgeError, } from './desktop-bridge/index.js';
|
|
16
|
+
import { DESKTOP_BRIDGE_ALLOWED_PATH_PREFIXES, DESKTOP_BRIDGE_LAUNCHD_LABEL, desktopBridgeConfigGeneration, desktopBridgeLaunchdPlistPath, desktopBridgeProcessExists, desktopBridgeStatePath, getDesktopBridgeStatus, preflightDesktopBridge, readDesktopBridgeState, safeBridgeErrorCode, writeDesktopBridgeLaunchdPlist, selectAvailableDesktopBridgePort, startPreparedDesktopBridge, DESKTOP_BRIDGE_STATE_SCHEMA, DESKTOP_BRIDGE_OFFICIAL_UPSTREAM_BASE_URL, DesktopBridgeError, } from './desktop-bridge/index.js';
|
|
15
17
|
export const DEFAULT_DESKTOP_BRIDGE_HOST = '127.0.0.1';
|
|
16
18
|
export const DEFAULT_DESKTOP_BRIDGE_PORT = 49_152;
|
|
17
19
|
export const DESKTOP_BRIDGE_SETTINGS_SCHEMA = 'sks.desktop-bridge-settings.v2';
|
|
@@ -111,7 +113,8 @@ export function defaultDesktopBridgeServiceSettings(input = {}) {
|
|
|
111
113
|
schema: DESKTOP_BRIDGE_SETTINGS_SCHEMA, listen_host: input.listen_host || DEFAULT_DESKTOP_BRIDGE_HOST,
|
|
112
114
|
listen_port: input.listen_port ?? DEFAULT_DESKTOP_BRIDGE_PORT, provider_registry: registry, route_policy: policy,
|
|
113
115
|
provider_session_pins: [...(input.provider_session_pins || [])], client_capability_sha256: input.client_capability_sha256 || '0'.repeat(64), allowed_origins: [...(input.allowed_origins || DEFAULT_ALLOWED_ORIGINS)],
|
|
114
|
-
connect_timeout_ms: input.connect_timeout_ms ?? 10_000, idle_timeout_ms: input.idle_timeout_ms ?? 300_000
|
|
116
|
+
connect_timeout_ms: input.connect_timeout_ms ?? 10_000, idle_timeout_ms: input.idle_timeout_ms ?? 300_000,
|
|
117
|
+
...(input.official_passthrough === undefined ? {} : { official_passthrough: input.official_passthrough })
|
|
115
118
|
});
|
|
116
119
|
}
|
|
117
120
|
export async function resolveDesktopBridgeActivationSettings(options = {}) {
|
|
@@ -159,7 +162,7 @@ export async function writeDesktopBridgeServiceSettings(file, settings) {
|
|
|
159
162
|
}
|
|
160
163
|
async function writeDesktopBridgeServiceSettingsUnlocked(file, settings) {
|
|
161
164
|
const validated = validateDesktopBridgeServiceSettings(settings);
|
|
162
|
-
const persisted = { schema: validated.schema, listen_host: validated.listen_host, listen_port: validated.listen_port, provider_registry: validated.provider_registry, route_policy: validated.route_policy, provider_session_pins: validated.provider_session_pins, client_capability_sha256: validated.client_capability_sha256, allowed_origins: validated.allowed_origins, connect_timeout_ms: validated.connect_timeout_ms, idle_timeout_ms: validated.idle_timeout_ms };
|
|
165
|
+
const persisted = { schema: validated.schema, listen_host: validated.listen_host, listen_port: validated.listen_port, provider_registry: validated.provider_registry, route_policy: validated.route_policy, provider_session_pins: validated.provider_session_pins, client_capability_sha256: validated.client_capability_sha256, allowed_origins: validated.allowed_origins, connect_timeout_ms: validated.connect_timeout_ms, idle_timeout_ms: validated.idle_timeout_ms, official_passthrough: validated.official_passthrough };
|
|
163
166
|
await writeTextAtomic(file, `${JSON.stringify(persisted, null, 2)}\n`, { mode: 0o600 });
|
|
164
167
|
await fsp.chmod(file, 0o600);
|
|
165
168
|
}
|
|
@@ -246,11 +249,13 @@ export async function resolveDesktopBridgeRuntimeConfig(options = {}) {
|
|
|
246
249
|
if (!Object.keys(settings.route_policy.model_routes).length)
|
|
247
250
|
throw new Error('catalog_model_route_missing');
|
|
248
251
|
const enabledRoutes = new Set(Object.values(settings.route_policy.model_routes).map((route) => route.provider_id));
|
|
249
|
-
|
|
252
|
+
const providerRouteIds = [...enabledRoutes].filter((id) => id === 'codex-lb' || id === 'openrouter');
|
|
253
|
+
const officialOnly = providerRouteIds.length === 0 && settings.official_passthrough.enabled;
|
|
254
|
+
if (!officialOnly && !providerRouteIds.some((id) => credentials.registry.providers[id].credential_state === 'ready'))
|
|
250
255
|
throw new Error('desktop_bridge_provider_credentials_unavailable');
|
|
251
256
|
const persistProviderSessionPins = options.persistProviderSessionPins
|
|
252
257
|
|| ((pins) => persistDesktopBridgeSessionPins(paths.settings_path, settings, pins));
|
|
253
|
-
const config = { providerRegistry: settings.provider_registry, routePolicy: settings.route_policy, providerSessionPins: settings.provider_session_pins, ...(options.resolveRequestRoute ? { resolveRequestRoute: options.resolveRequestRoute } : {}), persistProviderSessionPins, resolveProviderCredential: credentials.resolver, clientCapabilitySha256: settings.client_capability_sha256, listenHost: settings.listen_host, listenPort: settings.listen_port, allowedPathPrefixes: DESKTOP_BRIDGE_ALLOWED_PATH_PREFIXES, allowedOrigins: settings.allowed_origins, connectTimeoutMs: settings.connect_timeout_ms, idleTimeoutMs: settings.idle_timeout_ms };
|
|
258
|
+
const config = { providerRegistry: settings.provider_registry, routePolicy: settings.route_policy, providerSessionPins: settings.provider_session_pins, ...(options.resolveRequestRoute ? { resolveRequestRoute: options.resolveRequestRoute } : {}), persistProviderSessionPins, resolveProviderCredential: credentials.resolver, clientCapabilitySha256: settings.client_capability_sha256, listenHost: settings.listen_host, listenPort: settings.listen_port, allowedPathPrefixes: DESKTOP_BRIDGE_ALLOWED_PATH_PREFIXES, allowedOrigins: settings.allowed_origins, connectTimeoutMs: settings.connect_timeout_ms, idleTimeoutMs: settings.idle_timeout_ms, officialPassthrough: settings.official_passthrough.enabled ? { baseUrl: settings.official_passthrough.base_url } : null };
|
|
254
259
|
const primary = credentials.sources['codex-lb'] || credentials.sources.openrouter;
|
|
255
260
|
if (!primary)
|
|
256
261
|
throw new Error('desktop_bridge_provider_credentials_unavailable');
|
|
@@ -434,24 +439,67 @@ async function installedPackageVersion() {
|
|
|
434
439
|
return null;
|
|
435
440
|
}
|
|
436
441
|
}
|
|
442
|
+
export const DESKTOP_BRIDGE_SKEW_RESTART_COOLDOWN_MS = 30 * 60_000;
|
|
443
|
+
export function desktopBridgeSkewRestartSuppressed(marker, running, installed, nowMs) {
|
|
444
|
+
return Boolean(marker
|
|
445
|
+
&& marker.running === running
|
|
446
|
+
&& marker.installed === installed
|
|
447
|
+
&& nowMs - Date.parse(marker.at) < DESKTOP_BRIDGE_SKEW_RESTART_COOLDOWN_MS);
|
|
448
|
+
}
|
|
449
|
+
async function readSkewRestartMarker(file) {
|
|
450
|
+
try {
|
|
451
|
+
const parsed = JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
452
|
+
if (typeof parsed.running !== 'string' || typeof parsed.installed !== 'string' || !Number.isFinite(Date.parse(String(parsed.at))))
|
|
453
|
+
return null;
|
|
454
|
+
return { running: parsed.running, installed: parsed.installed, at: String(parsed.at) };
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
async function autoApplyOfficialModelsAtServe(runtime, home, options) {
|
|
461
|
+
const settings = runtime.settings;
|
|
462
|
+
const mode = await resolveEffectiveOfficialModelsMode(settings.official_passthrough, { home });
|
|
463
|
+
if (mode !== 'passthrough')
|
|
464
|
+
return;
|
|
465
|
+
const flipped = applyOfficialModelPassthrough(settings.route_policy, { mode: 'passthrough' });
|
|
466
|
+
if (flipped.policy_generation === settings.route_policy.policy_generation)
|
|
467
|
+
return;
|
|
468
|
+
const nextSettings = { ...settings, route_policy: flipped };
|
|
469
|
+
await writeDesktopBridgeServiceSettings(options.settingsPath || desktopBridgeServicePaths(home).settings_path, nextSettings);
|
|
470
|
+
await writeBridgeRoutingPolicy(bridgeRoutePolicyPath(path.join(path.resolve(home), '.codex')), flipped).catch(() => undefined);
|
|
471
|
+
settings.route_policy = flipped;
|
|
472
|
+
runtime.config.routePolicy = flipped;
|
|
473
|
+
process.stdout.write(`${JSON.stringify({ schema: 'sks.desktop-bridge-log.v2', event: 'sks.desktop_bridge.official_models_auto_applied', at: new Date().toISOString(), sks_version: PACKAGE_VERSION, mode: 'passthrough', policy_generation: flipped.policy_generation, secret_fields_redacted: true })}\n`);
|
|
474
|
+
}
|
|
437
475
|
export async function serveDesktopBridge(options = {}) {
|
|
438
476
|
let handle = null;
|
|
439
477
|
try {
|
|
440
478
|
const runtime = await resolveDesktopBridgeRuntimeConfig(options);
|
|
479
|
+
const serveHome = options.home || options.env?.HOME || process.env.HOME || os.homedir();
|
|
480
|
+
await autoApplyOfficialModelsAtServe(runtime, serveHome, options);
|
|
441
481
|
const supervised = desktopBridgeIsSupervised();
|
|
482
|
+
const skewMarkerPath = path.join(path.dirname(runtime.paths.state_path), 'desktop-bridge-skew-restart.json');
|
|
442
483
|
handle = await startPreparedDesktopBridge(await preflightDesktopBridge(runtime.config), {
|
|
443
484
|
statePath: runtime.paths.state_path,
|
|
444
485
|
versionSkew: {
|
|
445
486
|
readInstalledVersion: installedPackageVersion,
|
|
446
487
|
onSkew: (installedVersion) => {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
488
|
+
void (async () => {
|
|
489
|
+
const marker = await readSkewRestartMarker(skewMarkerPath);
|
|
490
|
+
const cooldownActive = desktopBridgeSkewRestartSuppressed(marker, PACKAGE_VERSION, installedVersion, Date.now());
|
|
491
|
+
const action = !supervised ? 'logged_only' : cooldownActive ? 'suppressed_cooldown' : 'restarting';
|
|
492
|
+
process.stdout.write(`${JSON.stringify({ schema: 'sks.desktop-bridge-log.v2', event: 'sks.desktop_bridge.version_skew', at: new Date().toISOString(), running: PACKAGE_VERSION, installed: installedVersion, supervised, action, secret_fields_redacted: true })}\n`);
|
|
493
|
+
if (!supervised || cooldownActive)
|
|
494
|
+
return;
|
|
495
|
+
await writeTextAtomic(skewMarkerPath, `${JSON.stringify({ running: PACKAGE_VERSION, installed: installedVersion, at: new Date().toISOString() })}\n`, { mode: 0o600 }).catch(() => undefined);
|
|
496
|
+
await handle?.stop().catch(() => undefined);
|
|
497
|
+
process.exit(64);
|
|
498
|
+
})();
|
|
451
499
|
},
|
|
452
500
|
},
|
|
453
501
|
});
|
|
454
|
-
process.stdout.write(`${JSON.stringify({ schema: 'sks.desktop-bridge-log.v2', event: 'sks.desktop_bridge.started', pid: handle.state.pid, process_generation: handle.state.schema === DESKTOP_BRIDGE_STATE_SCHEMA ? handle.state.process_generation : null, provider_registry_generation: runtime.config.providerRegistry?.generation, route_policy_generation: runtime.config.routePolicy?.policy_generation, secret_fields_redacted: true })}\n`);
|
|
502
|
+
process.stdout.write(`${JSON.stringify({ schema: 'sks.desktop-bridge-log.v2', event: 'sks.desktop_bridge.started', at: new Date().toISOString(), sks_version: PACKAGE_VERSION, pid: handle.state.pid, process_generation: handle.state.schema === DESKTOP_BRIDGE_STATE_SCHEMA ? handle.state.process_generation : null, provider_registry_generation: runtime.config.providerRegistry?.generation, route_policy_generation: runtime.config.routePolicy?.policy_generation, secret_fields_redacted: true })}\n`);
|
|
455
503
|
await waitForShutdown(handle);
|
|
456
504
|
return { schema: 'sks.desktop-bridge-serve.v1', ok: true, status: 'stopped', state: handle.state };
|
|
457
505
|
}
|
|
@@ -470,7 +518,7 @@ function validateDesktopBridgeServiceSettings(value) {
|
|
|
470
518
|
const input = row;
|
|
471
519
|
if (input.schema !== DESKTOP_BRIDGE_SETTINGS_SCHEMA)
|
|
472
520
|
throw new Error('desktop_bridge_settings_schema_invalid');
|
|
473
|
-
const allowedKeys = new Set(['schema', 'listen_host', 'listen_port', 'provider_registry', 'route_policy', 'provider_session_pins', 'client_capability_sha256', 'allowed_origins', 'connect_timeout_ms', 'idle_timeout_ms']);
|
|
521
|
+
const allowedKeys = new Set(['schema', 'listen_host', 'listen_port', 'provider_registry', 'route_policy', 'provider_session_pins', 'client_capability_sha256', 'allowed_origins', 'connect_timeout_ms', 'idle_timeout_ms', 'official_passthrough']);
|
|
474
522
|
if (Object.keys(input).some((key) => !allowedKeys.has(key)))
|
|
475
523
|
throw new Error('desktop_bridge_settings_unknown_field');
|
|
476
524
|
const serialized = JSON.stringify(input);
|
|
@@ -502,7 +550,44 @@ function validateDesktopBridgeServiceSettings(value) {
|
|
|
502
550
|
throw new Error('desktop_bridge_settings_connect_timeout_invalid');
|
|
503
551
|
if (!Number.isFinite(idle) || idle < 1_000 || idle > 86_400_000)
|
|
504
552
|
throw new Error('desktop_bridge_settings_idle_timeout_invalid');
|
|
505
|
-
|
|
553
|
+
const officialPassthrough = validateOfficialPassthroughSettings(input.official_passthrough);
|
|
554
|
+
return { schema: DESKTOP_BRIDGE_SETTINGS_SCHEMA, listen_host: host, listen_port: port, provider_registry: registry, route_policy: policy, provider_session_pins: pins, client_capability_sha256: clientCapabilitySha256, allowed_origins: [...new Set(origins)], connect_timeout_ms: connect, idle_timeout_ms: idle, official_passthrough: officialPassthrough };
|
|
555
|
+
}
|
|
556
|
+
function validateOfficialPassthroughSettings(value) {
|
|
557
|
+
if (value === undefined || value === null) {
|
|
558
|
+
return { enabled: true, base_url: DESKTOP_BRIDGE_OFFICIAL_UPSTREAM_BASE_URL, models: 'auto' };
|
|
559
|
+
}
|
|
560
|
+
if (typeof value !== 'object' || Array.isArray(value))
|
|
561
|
+
throw new Error('desktop_bridge_settings_official_passthrough_invalid');
|
|
562
|
+
const row = value;
|
|
563
|
+
if (Object.keys(row).some((key) => key !== 'enabled' && key !== 'base_url' && key !== 'models'))
|
|
564
|
+
throw new Error('desktop_bridge_settings_official_passthrough_invalid');
|
|
565
|
+
const enabled = row.enabled === true;
|
|
566
|
+
const baseUrl = typeof row.base_url === 'string' && row.base_url.trim() ? row.base_url.trim() : DESKTOP_BRIDGE_OFFICIAL_UPSTREAM_BASE_URL;
|
|
567
|
+
try {
|
|
568
|
+
new URL(baseUrl);
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
throw new Error('desktop_bridge_settings_official_passthrough_invalid');
|
|
572
|
+
}
|
|
573
|
+
const models = row.models === 'passthrough' || row.models === 'gateway' || row.models === 'auto'
|
|
574
|
+
? row.models
|
|
575
|
+
: row.models === undefined ? 'auto' : null;
|
|
576
|
+
if (models === null)
|
|
577
|
+
throw new Error('desktop_bridge_settings_official_passthrough_invalid');
|
|
578
|
+
return { enabled, base_url: baseUrl, models };
|
|
579
|
+
}
|
|
580
|
+
export async function resolveEffectiveOfficialModelsMode(official, input) {
|
|
581
|
+
if (official && official.enabled === false)
|
|
582
|
+
return 'gateway';
|
|
583
|
+
const models = official?.models || 'auto';
|
|
584
|
+
if (models === 'passthrough' || models === 'gateway')
|
|
585
|
+
return models;
|
|
586
|
+
const snapshot = await captureCodexAuthSnapshot({
|
|
587
|
+
home: input.home,
|
|
588
|
+
...(input.authPath ? { authPath: input.authPath } : {}),
|
|
589
|
+
}).catch(() => null);
|
|
590
|
+
return snapshot && (snapshot.mode === 'chatgpt_oauth' || snapshot.mode === 'mixed') ? 'passthrough' : 'gateway';
|
|
506
591
|
}
|
|
507
592
|
function validateProviderSessionPins(value) {
|
|
508
593
|
const pins = Array.isArray(value) ? value : [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { BRIDGE_OFFICIAL_ROUTE_ID } from './bridge-contracts.js';
|
|
4
5
|
import { canonicalizeBridgeModelId, normalizeBridgeUpstreamModelId, sha256Stable } from './route-index.js';
|
|
5
6
|
import { writeJsonAtomic } from '../fsx.js';
|
|
6
7
|
import { withFileLock } from '../locks/file-lock.js';
|
|
@@ -58,6 +59,8 @@ export function validateBridgeRoutingPolicy(policy, routeIndex) {
|
|
|
58
59
|
blockers.push('bridge_route_policy_model_invalid');
|
|
59
60
|
if (routeIndex) {
|
|
60
61
|
for (const [model, target] of Object.entries(canonical)) {
|
|
62
|
+
if (target.provider_id === BRIDGE_OFFICIAL_ROUTE_ID)
|
|
63
|
+
continue;
|
|
61
64
|
const indexed = routeIndex.routes[model];
|
|
62
65
|
if (!indexed || !sameTarget(indexed, target))
|
|
63
66
|
blockers.push('bridge_route_policy_route_index_mismatch');
|
|
@@ -101,7 +104,7 @@ function canonicalRoutes(routes) {
|
|
|
101
104
|
for (const [model, target] of Object.entries(routes)) {
|
|
102
105
|
const canonicalModel = canonicalizeBridgeModelId(model);
|
|
103
106
|
const upstreamModel = normalizeBridgeUpstreamModelId(target.upstream_model);
|
|
104
|
-
if (!canonicalModel || !upstreamModel || !
|
|
107
|
+
if (!canonicalModel || !upstreamModel || !isRouteTargetId(target.provider_id))
|
|
105
108
|
continue;
|
|
106
109
|
rows.push([canonicalModel, { provider_id: target.provider_id, upstream_model: upstreamModel }]);
|
|
107
110
|
}
|
|
@@ -110,6 +113,32 @@ function canonicalRoutes(routes) {
|
|
|
110
113
|
function isProviderId(value) {
|
|
111
114
|
return value === 'codex-lb' || value === 'openrouter';
|
|
112
115
|
}
|
|
116
|
+
function isRouteTargetId(value) {
|
|
117
|
+
return isProviderId(value) || value === BRIDGE_OFFICIAL_ROUTE_ID;
|
|
118
|
+
}
|
|
119
|
+
export const OFFICIAL_MODEL_ID_PATTERN = /^(?:gpt-[0-9]|o[0-9]|codex-mini)/;
|
|
120
|
+
export function applyOfficialModelPassthrough(policy, input = { mode: 'passthrough' }) {
|
|
121
|
+
if (input.mode === 'gateway')
|
|
122
|
+
return policy;
|
|
123
|
+
const routes = {};
|
|
124
|
+
for (const [model, target] of Object.entries(policy.model_routes)) {
|
|
125
|
+
routes[model] = OFFICIAL_MODEL_ID_PATTERN.test(model) && !model.includes(':')
|
|
126
|
+
? { provider_id: BRIDGE_OFFICIAL_ROUTE_ID, upstream_model: model }
|
|
127
|
+
: target;
|
|
128
|
+
}
|
|
129
|
+
const semantic = {
|
|
130
|
+
default_provider_id: policy.default_provider_id,
|
|
131
|
+
fallback: 'none',
|
|
132
|
+
model_routes: canonicalRoutes(routes),
|
|
133
|
+
catalog_generation: policy.catalog_generation
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
schema: 'sks.bridge-routing-policy.v1',
|
|
137
|
+
...semantic,
|
|
138
|
+
policy_generation: sha256Stable(semantic),
|
|
139
|
+
changed_at: input.changedAt || new Date().toISOString()
|
|
140
|
+
};
|
|
141
|
+
}
|
|
113
142
|
function sameTarget(left, right) {
|
|
114
143
|
return left.provider_id === right.provider_id && left.upstream_model === right.upstream_model;
|
|
115
144
|
}
|
|
@@ -51,6 +51,20 @@ export function resolveBridgeRequestRoute(request, policy, options) {
|
|
|
51
51
|
const policyTarget = policy.model_routes[model];
|
|
52
52
|
if (!indexed || !policyTarget)
|
|
53
53
|
return blocked(base, 'catalog_model_route_missing');
|
|
54
|
+
if (policyTarget.provider_id === 'openai') {
|
|
55
|
+
return {
|
|
56
|
+
...base,
|
|
57
|
+
ok: true,
|
|
58
|
+
route: policyTarget,
|
|
59
|
+
endpoint_url: null,
|
|
60
|
+
source: 'route_index',
|
|
61
|
+
proposed_session_pin: null,
|
|
62
|
+
blockers: [],
|
|
63
|
+
recovery_action: null
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (indexed.provider_id === 'openai')
|
|
67
|
+
return blocked(base, 'catalog_route_provider_unknown');
|
|
54
68
|
if (!sameTarget(indexed, policyTarget))
|
|
55
69
|
return blocked(base, 'bridge_route_policy_route_index_mismatch');
|
|
56
70
|
if (pinClaim && !sameTarget(indexed, pinClaim))
|
|
@@ -18,7 +18,7 @@ export async function resolveDesktopBridgeImagegenTarget(opts = {}) {
|
|
|
18
18
|
const route = model && policy?.fallback === 'none'
|
|
19
19
|
? policy.model_routes?.[model] || null
|
|
20
20
|
: null;
|
|
21
|
-
const provider = route ? status?.providers?.[route.provider_id] || null : null;
|
|
21
|
+
const provider = route && route.provider_id !== 'openai' ? status?.providers?.[route.provider_id] || null : null;
|
|
22
22
|
const providerImagegen = provider?.capabilities?.capabilities?.image_generation || null;
|
|
23
23
|
const blocker = !status || status.schema !== 'sks.desktop-bridge-status.v3'
|
|
24
24
|
? 'desktop_bridge_status_unavailable'
|
|
@@ -50,7 +50,7 @@ export async function resolveDesktopBridgeImagegenTarget(opts = {}) {
|
|
|
50
50
|
model,
|
|
51
51
|
model_source: model ? 'explicit' : null,
|
|
52
52
|
route: route || null,
|
|
53
|
-
provider_id: route
|
|
53
|
+
provider_id: route && route.provider_id !== 'openai' ? route.provider_id : null,
|
|
54
54
|
status_source: injected ? 'injected_fixture' : 'runtime',
|
|
55
55
|
live_evidence_allowed: bridgeVerified && !injected,
|
|
56
56
|
blocker,
|
|
@@ -86,7 +86,8 @@ export async function writeProviderContextReport(root = process.cwd(), input = {
|
|
|
86
86
|
return { ...report, report_path: reportPath };
|
|
87
87
|
}
|
|
88
88
|
function selectedBridgeProvider(status) {
|
|
89
|
-
|
|
89
|
+
const selected = status?.routing.selected_route?.provider_id;
|
|
90
|
+
return (selected === 'codex-lb' || selected === 'openrouter' ? selected : null)
|
|
90
91
|
|| status?.routing.policy?.default_provider_id
|
|
91
92
|
|| status?.routing.session_pin?.provider_id
|
|
92
93
|
|| null;
|
|
@@ -59,6 +59,15 @@ export function affectedGlobsFor(id) {
|
|
|
59
59
|
'package-lock.json',
|
|
60
60
|
'src/core/codex-app.ts',
|
|
61
61
|
'src/core/codex-lb-circuit.ts',
|
|
62
|
+
'src/core/codex-lb/**',
|
|
63
|
+
`src/scripts/${prefix}-*.ts`
|
|
64
|
+
];
|
|
65
|
+
case 'desktop-bridge':
|
|
66
|
+
return [
|
|
67
|
+
'src/core/codex-lb/desktop-bridge/**',
|
|
68
|
+
'src/core/codex-lb/desktop-service.ts',
|
|
69
|
+
'src/core/codex-lb/provider-route-policy.ts',
|
|
70
|
+
'src/core/codex-lb/request-route-resolver.ts',
|
|
62
71
|
`src/scripts/${prefix}-*.ts`
|
|
63
72
|
];
|
|
64
73
|
case 'context-graph-v2':
|
package/dist/core/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '9.
|
|
1
|
+
export const PACKAGE_VERSION = '9.2.1';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "9.
|
|
4
|
+
"version": "9.2.1",
|
|
5
5
|
"description": "Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|