sneakoscope 9.1.0 → 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +135 -49
- 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/server.js +7 -15
- package/dist/core/codex-lb/desktop-bridge/types.js +1 -0
- package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +35 -14
- 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.js +3 -1
- package/dist/core/codex-lb/desktop-service.js +95 -11
- 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/hooks-runtime/official-subagent-lifecycle.js +28 -0
- package/dist/core/hooks-runtime.js +2 -2
- package/dist/core/imagegen/desktop-bridge-imagegen-target.js +2 -2
- package/dist/core/init/skills.js +19 -4
- package/dist/core/provider/provider-context.js +2 -1
- package/dist/core/release/gate-affected-globs.js +9 -0
- package/dist/core/update/update-migration-state/simple-stages.js +22 -16
- 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.0** — 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) {
|
|
@@ -148,6 +151,34 @@ function safeUpstreamErrorId(value) {
|
|
|
148
151
|
const text = String(value ?? '').trim();
|
|
149
152
|
return /^[A-Za-z0-9_.:-]{1,64}$/.test(text) ? text : null;
|
|
150
153
|
}
|
|
154
|
+
const TRANSIENT_UPSTREAM_STATUSES = new Set([500, 502, 503, 524]);
|
|
155
|
+
const TRANSIENT_UPSTREAM_IDENTIFIERS = new Set(['upstream_error', 'upstream_request_timeout']);
|
|
156
|
+
export const TRANSIENT_UPSTREAM_REPLAY_LIMIT = 3;
|
|
157
|
+
const TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS = [200, 400, 800];
|
|
158
|
+
function isTransientUpstreamIdentifier(value) {
|
|
159
|
+
return Boolean(value && TRANSIENT_UPSTREAM_IDENTIFIERS.has(value));
|
|
160
|
+
}
|
|
161
|
+
function delay(ms) {
|
|
162
|
+
return new Promise((resolve) => {
|
|
163
|
+
const timer = setTimeout(resolve, ms);
|
|
164
|
+
timer.unref();
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function isTransientUpstreamFailure(statusCode, upstreamType, upstreamCode) {
|
|
168
|
+
if (statusCode === 429)
|
|
169
|
+
return false;
|
|
170
|
+
if (statusCode === 404) {
|
|
171
|
+
return isTransientUpstreamIdentifier(upstreamType) || isTransientUpstreamIdentifier(upstreamCode);
|
|
172
|
+
}
|
|
173
|
+
return TRANSIENT_UPSTREAM_STATUSES.has(statusCode);
|
|
174
|
+
}
|
|
175
|
+
function redactedUpstreamClientMessage(statusCode, transient) {
|
|
176
|
+
if (statusCode === 429)
|
|
177
|
+
return 'rate_limited';
|
|
178
|
+
if (transient)
|
|
179
|
+
return 'temporary_upstream_failure';
|
|
180
|
+
return 'bridge_upstream_request_failed';
|
|
181
|
+
}
|
|
151
182
|
async function readRedactedUpstreamError(response) {
|
|
152
183
|
let total = 0;
|
|
153
184
|
const chunks = [];
|
|
@@ -169,19 +200,18 @@ async function readRedactedUpstreamError(response) {
|
|
|
169
200
|
}
|
|
170
201
|
catch {
|
|
171
202
|
}
|
|
172
|
-
return {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
};
|
|
203
|
+
return { upstreamCode, upstreamType };
|
|
204
|
+
}
|
|
205
|
+
function buildRedactedUpstreamErrorBody(statusCode, upstreamCode, upstreamType, transient) {
|
|
206
|
+
return Buffer.from(JSON.stringify({
|
|
207
|
+
error: {
|
|
208
|
+
type: 'upstream_error',
|
|
209
|
+
code: 'bridge_upstream_request_failed',
|
|
210
|
+
message: redactedUpstreamClientMessage(statusCode, transient),
|
|
211
|
+
...(upstreamType ? { upstream_type: upstreamType } : {}),
|
|
212
|
+
...(upstreamCode ? { upstream_code: upstreamCode } : {}),
|
|
213
|
+
},
|
|
214
|
+
}));
|
|
185
215
|
}
|
|
186
216
|
const upstreamAgents = new Map();
|
|
187
217
|
function upstreamAgent(secure, key, idleTimeoutMs) {
|
|
@@ -224,23 +254,32 @@ class StalePooledSocketFailure extends Error {
|
|
|
224
254
|
export async function forwardHttp(req, res, config, prepared, authenticatedLocalBaseUrl = desktopBridgeListenOrigin(config)) {
|
|
225
255
|
try {
|
|
226
256
|
const request = prepared || await prepareDesktopBridgeRequest(req, config);
|
|
227
|
-
const
|
|
228
|
-
|
|
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)
|
|
229
260
|
throw new DesktopBridgeError('bridge_provider_route_unavailable');
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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);
|
|
235
274
|
if (request.body)
|
|
236
275
|
headers['content-length'] = String(request.body.length);
|
|
237
276
|
else
|
|
238
277
|
delete headers['content-length'];
|
|
239
278
|
if (request.contentEncodingStripped)
|
|
240
279
|
delete headers['content-encoding'];
|
|
241
|
-
const agent = upstreamAgent(
|
|
280
|
+
const agent = upstreamAgent(remote.secure, `${remote.address}:${remote.port}`, config.idleTimeoutMs);
|
|
242
281
|
const replayable = Buffer.isBuffer(request.body);
|
|
243
|
-
const attempt = (useFreshConnection) => new Promise((resolve, reject) => {
|
|
282
|
+
const attempt = (useFreshConnection, canReplay) => new Promise((resolve, reject) => {
|
|
244
283
|
let responseStarted = false;
|
|
245
284
|
let settled = false;
|
|
246
285
|
const abort = () => { if (!res.writableEnded)
|
|
@@ -254,10 +293,10 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
254
293
|
error ? reject(error) : resolve();
|
|
255
294
|
};
|
|
256
295
|
const upstream = transport.request({
|
|
257
|
-
protocol: target.protocol, hostname:
|
|
258
|
-
port:
|
|
296
|
+
protocol: target.protocol, hostname: remote.address, family: remote.family,
|
|
297
|
+
port: remote.port, method: req.method, path: `${target.pathname}${target.search}`, headers,
|
|
259
298
|
agent: useFreshConnection ? false : agent,
|
|
260
|
-
...(
|
|
299
|
+
...(remote.tlsServername ? { servername: remote.tlsServername } : {}),
|
|
261
300
|
});
|
|
262
301
|
connectTimeout(upstream, config);
|
|
263
302
|
upstream.setTimeout(config.idleTimeoutMs, () => upstream.destroy(new DesktopBridgeError('bridge_upstream_idle_timeout')));
|
|
@@ -272,16 +311,48 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
272
311
|
});
|
|
273
312
|
upstream.once('response', (response) => {
|
|
274
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
|
+
}
|
|
275
344
|
if (statusCode >= 400) {
|
|
276
|
-
void readRedactedUpstreamError(response).then(({
|
|
277
|
-
const transientMislabel = statusCode
|
|
278
|
-
|
|
279
|
-
if (transientMislabel && replayable && !useFreshConnection) {
|
|
345
|
+
void readRedactedUpstreamError(response).then(({ upstreamCode, upstreamType }) => {
|
|
346
|
+
const transientMislabel = isTransientUpstreamFailure(statusCode, upstreamType, upstreamCode);
|
|
347
|
+
if (transientMislabel && replayable && canReplay) {
|
|
280
348
|
responseStarted = true;
|
|
281
|
-
finish(new StalePooledSocketFailure(new DesktopBridgeError('
|
|
349
|
+
finish(new StalePooledSocketFailure(new DesktopBridgeError(upstreamCode === 'upstream_request_timeout'
|
|
350
|
+
? 'bridge_upstream_request_timeout'
|
|
351
|
+
: 'bridge_upstream_transient_mislabel')));
|
|
282
352
|
return;
|
|
283
353
|
}
|
|
284
354
|
const clientStatus = transientMislabel ? 503 : statusCode;
|
|
355
|
+
const body = buildRedactedUpstreamErrorBody(statusCode, upstreamCode, upstreamType, transientMislabel);
|
|
285
356
|
logHttpRejection({
|
|
286
357
|
code: transientMislabel
|
|
287
358
|
? `bridge_upstream_status_${statusCode}_translated_503`
|
|
@@ -294,10 +365,12 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
294
365
|
public_model: request.route.public_model,
|
|
295
366
|
});
|
|
296
367
|
responseStarted = true;
|
|
297
|
-
const responseHeaders = rewriteResponseHeaders(response.headers,
|
|
368
|
+
const responseHeaders = rewriteResponseHeaders(response.headers, upstreamBaseUrl, authenticatedLocalBaseUrl);
|
|
298
369
|
responseHeaders['content-length'] = String(body.length);
|
|
299
370
|
delete responseHeaders['transfer-encoding'];
|
|
300
371
|
if (transientMislabel)
|
|
372
|
+
responseHeaders['retry-after'] = String(responseHeaders['retry-after'] || '10');
|
|
373
|
+
if (statusCode === 429 && !responseHeaders['retry-after'])
|
|
301
374
|
responseHeaders['retry-after'] = '10';
|
|
302
375
|
res.writeHead(clientStatus, responseHeaders);
|
|
303
376
|
res.end(body, () => finish());
|
|
@@ -306,7 +379,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
306
379
|
}
|
|
307
380
|
responseStarted = true;
|
|
308
381
|
try {
|
|
309
|
-
res.writeHead(statusCode, rewriteResponseHeaders(response.headers,
|
|
382
|
+
res.writeHead(statusCode, rewriteResponseHeaders(response.headers, upstreamBaseUrl, authenticatedLocalBaseUrl));
|
|
310
383
|
}
|
|
311
384
|
catch (error) {
|
|
312
385
|
response.destroy(error instanceof Error ? error : undefined);
|
|
@@ -321,21 +394,34 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
321
394
|
void pipeline(req, upstream).catch((error) => { if (!responseStarted)
|
|
322
395
|
finish(error); });
|
|
323
396
|
});
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
397
|
+
let remainingReplays = replayable ? TRANSIENT_UPSTREAM_REPLAY_LIMIT : 0;
|
|
398
|
+
let useFreshConnection = false;
|
|
399
|
+
for (;;) {
|
|
400
|
+
try {
|
|
401
|
+
await attempt(useFreshConnection, remainingReplays > 0);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
if (!(error instanceof StalePooledSocketFailure) || remainingReplays <= 0)
|
|
406
|
+
throw error;
|
|
407
|
+
remainingReplays -= 1;
|
|
408
|
+
if (!useFreshConnection) {
|
|
409
|
+
agent.destroy();
|
|
410
|
+
upstreamAgents.delete(`${remote.secure ? 'https' : 'http'}:${remote.address}:${remote.port}`);
|
|
411
|
+
}
|
|
412
|
+
logHttpRejection({
|
|
413
|
+
code: `bridge_upstream_socket_stale_replayed:${underlyingErrorCode(error.reason) || 'unknown'}`,
|
|
414
|
+
transport: 'http',
|
|
415
|
+
...(req.method === undefined ? {} : { method: req.method }),
|
|
416
|
+
...(req.url === undefined ? {} : { url: req.url }),
|
|
417
|
+
});
|
|
418
|
+
useFreshConnection = true;
|
|
419
|
+
const backoffIndex = TRANSIENT_UPSTREAM_REPLAY_LIMIT - remainingReplays - 1;
|
|
420
|
+
const backoffMs = TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS[Math.max(0, backoffIndex)]
|
|
421
|
+
?? TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS[TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS.length - 1];
|
|
422
|
+
if (backoffMs)
|
|
423
|
+
await delay(backoffMs);
|
|
424
|
+
}
|
|
339
425
|
}
|
|
340
426
|
}
|
|
341
427
|
catch (error) {
|
|
@@ -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);
|
|
@@ -8,7 +8,7 @@ import { createDesktopBridgePublicState, desktopBridgeListenOrigin, desktopBridg
|
|
|
8
8
|
import { DesktopBridgeError } from './types.js';
|
|
9
9
|
import { PACKAGE_VERSION } from '../../version.js';
|
|
10
10
|
import { DESKTOP_BRIDGE_CLIENT_PATH_PREFIX, DESKTOP_BRIDGE_DIAGNOSTIC_HEALTH_PATH, DESKTOP_BRIDGE_DIAGNOSTIC_PATH, DESKTOP_BRIDGE_DIAGNOSTIC_PROTOCOL } from './types.js';
|
|
11
|
-
import { forwardWebSocket } from './websocket-forward.js';
|
|
11
|
+
import { forwardWebSocket, safeEndUpgradeSocket } from './websocket-forward.js';
|
|
12
12
|
function authenticateDesktopBridgeClient(req, input) {
|
|
13
13
|
const raw = String(req.url || '/');
|
|
14
14
|
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw) || raw.startsWith('//') || /[\r\n\0]/.test(raw)) {
|
|
@@ -135,22 +135,14 @@ function writeUpgradeRejection(socket, error, req) {
|
|
|
135
135
|
url: req?.url,
|
|
136
136
|
status: rejectionStatus(safeBridgeErrorCode(error)),
|
|
137
137
|
});
|
|
138
|
-
socket.on('error', () => undefined);
|
|
139
|
-
if (socket.destroyed || socket.writableEnded)
|
|
140
|
-
return;
|
|
141
138
|
const code = safeBridgeErrorCode(error);
|
|
142
139
|
const status = rejectionStatus(code);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
+ JSON.stringify({ error: { type: 'sks_bridge_rejection', code, message: code } }));
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
socket.destroy();
|
|
153
|
-
}
|
|
140
|
+
safeEndUpgradeSocket(socket, `HTTP/1.1 ${status} ${rejectionStatusText(status)}\r\n`
|
|
141
|
+
+ 'Content-Type: application/json\r\n'
|
|
142
|
+
+ 'Cache-Control: no-store\r\n'
|
|
143
|
+
+ 'Connection: close\r\n'
|
|
144
|
+
+ '\r\n'
|
|
145
|
+
+ JSON.stringify({ error: { type: 'sks_bridge_rejection', code, message: code } }));
|
|
154
146
|
}
|
|
155
147
|
function writeDiagnosticHealth(req, res, input) {
|
|
156
148
|
if (req.method !== 'GET')
|
|
@@ -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) {
|