sneakoscope 10.3.6 → 10.3.8
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 +12 -181
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/commands/bridge.js +61 -1
- package/dist/commands/codex-app.js +4 -1
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/agents/native-worker-backend-router.js +55 -0
- package/dist/core/codex-app/codex-app-restart-policy.js +20 -0
- package/dist/core/codex-app/codex-context-window.js +61 -24
- package/dist/core/codex-app/menubar/constants.js +4 -0
- package/dist/core/codex-compat/codex-hook-output-builders.js +3 -1
- package/dist/core/codex-lb/bridge-cli-contract.js +2 -0
- package/dist/core/codex-lb/desktop-bridge/exclusive-provider-guard.js +59 -0
- package/dist/core/codex-lb/desktop-bridge/http-forward.js +23 -15
- package/dist/core/codex-lb/desktop-bridge/index.js +1 -0
- package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +10 -3
- package/dist/core/codex-lb/desktop-bridge/security.js +63 -1
- package/dist/core/codex-lb/desktop-bridge/server.js +5 -0
- package/dist/core/codex-lb/desktop-bridge/state.js +3 -0
- package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +5 -1
- package/dist/core/codex-lb/desktop-controller-v3/catalog.js +31 -5
- package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +23 -4
- package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-catalog.js +39 -0
- package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +286 -0
- package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-main-model.js +101 -0
- package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-status.js +68 -0
- package/dist/core/codex-lb/desktop-controller-v3/status.js +8 -5
- package/dist/core/codex-lb/desktop-controller-v3.js +26 -2
- package/dist/core/codex-lb/desktop-service.js +13 -2
- package/dist/core/commands/naruto-command.js +1 -1
- package/dist/core/decisions/child-model-choice.js +107 -0
- package/dist/core/decisions/cli.js +19 -2
- package/dist/core/decisions/routing.js +5 -4
- package/dist/core/doctor/command-alias-cleanup.js +2 -0
- package/dist/core/doctor/skill-legacy-surface.js +46 -1
- package/dist/core/hooks-runtime/hook-context.js +21 -7
- package/dist/core/hooks-runtime/hook-io.js +3 -2
- package/dist/core/hooks-runtime/jev-spawn-routing.js +83 -5
- package/dist/core/hooks-runtime/official-subagent-lifecycle.js +3 -2
- package/dist/core/hooks-runtime/parent-orchestration-gate.js +18 -1
- package/dist/core/hooks-runtime/subagent-context.js +13 -2
- package/dist/core/hooks-runtime/subagent-spawn-policy.js +49 -6
- package/dist/core/hooks-runtime.js +12 -5
- package/dist/core/managed-assets/managed-assets-manifest.js +9 -4
- package/dist/core/subagents/child-model-allowlist.js +179 -0
- package/dist/core/subagents/child-model-plan.js +117 -0
- package/dist/core/subagents/naruto-host-credentials.js +82 -13
- package/dist/core/subagents/official-subagent-config.js +66 -15
- package/dist/core/subagents/official-subagent-preparation.js +45 -12
- package/dist/core/subagents/official-subagent-prompt.js +82 -11
- package/dist/core/subagents/official-subagent-runner.js +3 -0
- package/dist/core/subagents/read-only-list-role.js +73 -0
- package/dist/core/subagents/role-model-preferences.js +54 -17
- package/dist/core/update/registry-availability.js +58 -0
- package/dist/core/update-check.js +8 -26
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +2 -0
- package/dist/native/sks-menubar/Sources/OpenRouterOnlyState.swift +314 -0
- package/dist/native/sks-menubar/Sources/ProcessClient.swift +2 -1
- package/dist/native/sks-menubar/Sources/ProvidersModelExposure.swift +15 -3
- package/dist/native/sks-menubar/Sources/ProvidersOpenRouterOnly.swift +162 -0
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +36 -18
- package/dist/native/sks-menubar/Sources/SettingsViewController.swift +10 -4
- package/dist/native/sks-menubar/Sources/SidebarItem.swift +2 -0
- package/dist/native/sks-menubar/Sources/SubagentModelsModels.swift +201 -0
- package/dist/native/sks-menubar/Sources/SubagentModelsViewController.swift +373 -0
- package/package.json +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { canonicalizeBridgeModelId } from '../route-index.js';
|
|
2
|
+
import { DesktopBridgeError } from './types.js';
|
|
3
|
+
export const OPENROUTER_ONLY_ROUTE_BLOCKED = 'openrouter_only_route_blocked';
|
|
4
|
+
export const OPENROUTER_ONLY_SUBAGENT_MODEL_BLOCKED = 'openrouter_only_subagent_model_blocked';
|
|
5
|
+
export const OPENROUTER_ONLY_REFUSAL_CODES = new Set([
|
|
6
|
+
OPENROUTER_ONLY_ROUTE_BLOCKED,
|
|
7
|
+
OPENROUTER_ONLY_SUBAGENT_MODEL_BLOCKED,
|
|
8
|
+
]);
|
|
9
|
+
const MAX_SUBAGENT_MODEL_IDS = 64;
|
|
10
|
+
const OPENROUTER_ROUTE_QUALIFIER = 'openrouter:';
|
|
11
|
+
export function validateOpenRouterOnlyConfig(value) {
|
|
12
|
+
const row = value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
13
|
+
const models = row?.subagent_models;
|
|
14
|
+
if (!row
|
|
15
|
+
|| Object.keys(row).some((key) => key !== 'enabled' && key !== 'subagent_models')
|
|
16
|
+
|| typeof row.enabled !== 'boolean'
|
|
17
|
+
|| !Array.isArray(models)
|
|
18
|
+
|| models.length > MAX_SUBAGENT_MODEL_IDS
|
|
19
|
+
|| models.some((model) => typeof model !== 'string' || canonicalizeBridgeModelId(model) !== model)
|
|
20
|
+
|| new Set(models).size !== models.length) {
|
|
21
|
+
throw new DesktopBridgeError('bridge_openrouter_only_config_invalid');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function openRouterOnlyEnabled(config) {
|
|
25
|
+
return config.openRouterOnly?.enabled === true;
|
|
26
|
+
}
|
|
27
|
+
export function codexRequestIsChild(identity) {
|
|
28
|
+
if (!identity)
|
|
29
|
+
return false;
|
|
30
|
+
if (identity.parent_thread_id)
|
|
31
|
+
return true;
|
|
32
|
+
const kind = identity.subagent_kind?.toLowerCase() ?? null;
|
|
33
|
+
if (kind === 'compact')
|
|
34
|
+
return false;
|
|
35
|
+
return Boolean(kind) || identity.thread_source?.toLowerCase() === 'subagent';
|
|
36
|
+
}
|
|
37
|
+
function requestCarriesModel(request) {
|
|
38
|
+
return String(request.public_model ?? '').trim() !== '';
|
|
39
|
+
}
|
|
40
|
+
function subagentListKey(publicModel) {
|
|
41
|
+
const model = canonicalizeBridgeModelId(publicModel) || '';
|
|
42
|
+
return model.startsWith(OPENROUTER_ROUTE_QUALIFIER) ? model.slice(OPENROUTER_ROUTE_QUALIFIER.length) : model;
|
|
43
|
+
}
|
|
44
|
+
export function openRouterOnlyRefusesPinnedProvider(request, pinnedProviderId, config) {
|
|
45
|
+
return openRouterOnlyEnabled(config) && requestCarriesModel(request)
|
|
46
|
+
&& pinnedProviderId !== null && pinnedProviderId !== 'openrouter';
|
|
47
|
+
}
|
|
48
|
+
export function assertOpenRouterOnlyRoute(request, route, config) {
|
|
49
|
+
const mode = config.openRouterOnly;
|
|
50
|
+
if (!mode || mode.enabled !== true || !requestCarriesModel(request))
|
|
51
|
+
return;
|
|
52
|
+
if (route.provider_id !== 'openrouter')
|
|
53
|
+
throw new DesktopBridgeError(OPENROUTER_ONLY_ROUTE_BLOCKED);
|
|
54
|
+
if (!codexRequestIsChild(request.identity))
|
|
55
|
+
return;
|
|
56
|
+
const key = subagentListKey(route.public_model);
|
|
57
|
+
if (!key || !mode.subagent_models.includes(key))
|
|
58
|
+
throw new DesktopBridgeError(OPENROUTER_ONLY_SUBAGENT_MODEL_BLOCKED);
|
|
59
|
+
}
|
|
@@ -5,6 +5,7 @@ import { pipeline } from 'node:stream/promises';
|
|
|
5
5
|
import { BRIDGE_OFFICIAL_ROUTE_ID } from '../bridge-contracts.js';
|
|
6
6
|
import { buildOfficialPassthroughHeaders, buildProviderUpstreamHeaders, rewriteResponseHeaders } from './header-policy.js';
|
|
7
7
|
import { createDesktopBridgeRejectionLogger } from './rejection-log.js';
|
|
8
|
+
import { OPENROUTER_ONLY_REFUSAL_CODES, openRouterOnlyEnabled } from './exclusive-provider-guard.js';
|
|
8
9
|
import { ensureDesktopBridgeRemoteTarget, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader } from './security.js';
|
|
9
10
|
import { desktopBridgeListenOrigin } from './state.js';
|
|
10
11
|
import { DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES, DesktopBridgeError, DesktopBridgeRequestBodyTooLargeError } from './types.js';
|
|
@@ -46,9 +47,12 @@ function decodeBridgeRequestBody(body, encoding, maximum) {
|
|
|
46
47
|
throw new DesktopBridgeError('bridge_responses_body_invalid_json');
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const RESPONSES_PATHS = ['/backend-api/codex/responses', '/api/v1/responses', '/v1/responses'];
|
|
51
|
+
function modelBodyKind(req, pathname, config) {
|
|
52
|
+
if (RESPONSES_PATHS.includes(pathname))
|
|
53
|
+
return 'responses';
|
|
54
|
+
return openRouterOnlyEnabled(config) && req.method === 'POST'
|
|
55
|
+
&& RESPONSES_PATHS.some((prefix) => pathname.startsWith(`${prefix}/`)) ? 'sub-endpoint' : null;
|
|
52
56
|
}
|
|
53
57
|
async function readBoundedBody(req, maximum) {
|
|
54
58
|
const declared = Number(req.headers['content-length'] || 0);
|
|
@@ -83,7 +87,8 @@ export async function prepareDesktopBridgeRequest(req, config) {
|
|
|
83
87
|
let body = null;
|
|
84
88
|
let payload = null;
|
|
85
89
|
let contentEncodingStripped = false;
|
|
86
|
-
|
|
90
|
+
const bodyKind = modelBodyKind(req, pathname, config);
|
|
91
|
+
if (bodyKind) {
|
|
87
92
|
const maximum = config.maxRequestBodyBytes ?? DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES;
|
|
88
93
|
body = await readBoundedBody(req, maximum);
|
|
89
94
|
const encoding = String(req.headers['content-encoding'] || '').trim().toLowerCase();
|
|
@@ -92,14 +97,16 @@ export async function prepareDesktopBridgeRequest(req, config) {
|
|
|
92
97
|
decoded = decodeBridgeRequestBody(body, encoding, maximum);
|
|
93
98
|
contentEncodingStripped = true;
|
|
94
99
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
if (bodyKind === 'responses' || decoded.length > 0) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(decoded.toString('utf8'));
|
|
103
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
104
|
+
throw new Error();
|
|
105
|
+
payload = parsed;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw new DesktopBridgeError('bridge_responses_body_invalid_json');
|
|
109
|
+
}
|
|
103
110
|
}
|
|
104
111
|
if (contentEncodingStripped)
|
|
105
112
|
body = decoded;
|
|
@@ -109,9 +116,9 @@ export async function prepareDesktopBridgeRequest(req, config) {
|
|
|
109
116
|
const sessionIdentity = resolveCodexSessionIdentity(req.headers, payload);
|
|
110
117
|
const route = await resolveAndBindDesktopBridgeRouteContext({
|
|
111
118
|
public_model: String(model || ''), session_id: sessionIdentity.thread_id,
|
|
112
|
-
pathname, transport: 'http', headers: req.headers,
|
|
119
|
+
pathname, transport: 'http', headers: req.headers, identity: sessionIdentity,
|
|
113
120
|
}, config);
|
|
114
|
-
if (payload && payload.model !== route.upstream_model) {
|
|
121
|
+
if (payload && (bodyKind === 'responses' || typeof payload.model === 'string') && payload.model !== route.upstream_model) {
|
|
115
122
|
payload.model = route.upstream_model;
|
|
116
123
|
body = Buffer.from(JSON.stringify(payload));
|
|
117
124
|
}
|
|
@@ -147,7 +154,8 @@ function writeHttpBridgeError(res, error, req) {
|
|
|
147
154
|
res.destroy(error instanceof Error ? error : undefined);
|
|
148
155
|
return;
|
|
149
156
|
}
|
|
150
|
-
res.writeHead(code.startsWith('catalog_') || code.startsWith('session_') || code.includes('route_')
|
|
157
|
+
res.writeHead(code.startsWith('catalog_') || code.startsWith('session_') || code.includes('route_')
|
|
158
|
+
|| OPENROUTER_ONLY_REFUSAL_CODES.has(code) ? 409 : 502, {
|
|
151
159
|
'content-type': 'application/json', 'cache-control': 'no-store', connection: 'close',
|
|
152
160
|
});
|
|
153
161
|
res.end(JSON.stringify({ error: { type: 'sks_bridge_error', code, message: code } }));
|
|
@@ -17,6 +17,14 @@ export function isResponsesWebSocketRequest(req) {
|
|
|
17
17
|
function bytes(data) {
|
|
18
18
|
return Buffer.isBuffer(data) ? data : Array.isArray(data) ? Buffer.concat(data) : Buffer.from(data);
|
|
19
19
|
}
|
|
20
|
+
function withLineage(identity, next) {
|
|
21
|
+
return {
|
|
22
|
+
...identity,
|
|
23
|
+
parent_thread_id: identity.parent_thread_id ?? next.parent_thread_id,
|
|
24
|
+
subagent_kind: identity.subagent_kind ?? next.subagent_kind,
|
|
25
|
+
thread_source: next.thread_source === 'subagent' ? next.thread_source : identity.thread_source ?? next.thread_source,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
20
28
|
export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
21
29
|
const upgradeIdentity = resolveCodexSessionIdentity(req.headers);
|
|
22
30
|
const maxBytes = config.maxRequestBodyBytes ?? DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES;
|
|
@@ -90,7 +98,7 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
90
98
|
fail(error); });
|
|
91
99
|
}
|
|
92
100
|
function routeRequest(model) {
|
|
93
|
-
return { public_model: model, session_id: identity.thread_id, pathname: new URL(req.url || '/', 'http://bridge.invalid').pathname, transport: 'websocket', headers: req.headers };
|
|
101
|
+
return { public_model: model, session_id: identity.thread_id, pathname: new URL(req.url || '/', 'http://bridge.invalid').pathname, transport: 'websocket', headers: req.headers, identity };
|
|
94
102
|
}
|
|
95
103
|
function assertBinding(route) {
|
|
96
104
|
if (!bound)
|
|
@@ -112,8 +120,7 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
112
120
|
|| (nextIdentity.session_id && nextIdentity.session_id !== identity.session_id))) {
|
|
113
121
|
throw new DesktopBridgeError('bridge_codex_session_identity_conflict');
|
|
114
122
|
}
|
|
115
|
-
|
|
116
|
-
identity = nextIdentity;
|
|
123
|
+
identity = bound ? withLineage(identity, nextIdentity) : nextIdentity;
|
|
117
124
|
const request = routeRequest(model);
|
|
118
125
|
assertBinding(assertDesktopBridgeRouteContext(request, config));
|
|
119
126
|
const route = await resolveAndBindDesktopBridgeRouteContext(request, config);
|
|
@@ -3,6 +3,7 @@ import net from 'node:net';
|
|
|
3
3
|
import { BRIDGE_OFFICIAL_ROUTE_ID } from '../bridge-contracts.js';
|
|
4
4
|
import { canonicalizeBridgeModelId, normalizeBridgeUpstreamModelId, } from '../route-index.js';
|
|
5
5
|
import { DESKTOP_BRIDGE_CLIENT_PATH_PREFIX, DesktopBridgeError } from './types.js';
|
|
6
|
+
import { OPENROUTER_ONLY_ROUTE_BLOCKED, assertOpenRouterOnlyRoute, openRouterOnlyRefusesPinnedProvider, validateOpenRouterOnlyConfig, } from './exclusive-provider-guard.js';
|
|
6
7
|
const MIN_HIGH_PORT = 49_152;
|
|
7
8
|
const MAX_PORT = 65_535;
|
|
8
9
|
const MAX_SESSION_PINS = 10_000;
|
|
@@ -61,6 +62,37 @@ export function singleBridgeHeader(headers, name) {
|
|
|
61
62
|
throw new DesktopBridgeError('bridge_policy_header_ambiguous');
|
|
62
63
|
return values[0] || null;
|
|
63
64
|
}
|
|
65
|
+
const MAX_LINEAGE_CHARS = 256;
|
|
66
|
+
function lineageText(value) {
|
|
67
|
+
if (typeof value !== 'string')
|
|
68
|
+
return null;
|
|
69
|
+
const text = value.trim();
|
|
70
|
+
return text && text.length <= MAX_LINEAGE_CHARS && !/[\u0000-\u001f\u007f]/.test(text) ? text : null;
|
|
71
|
+
}
|
|
72
|
+
function firstLineage(values) {
|
|
73
|
+
for (const value of values) {
|
|
74
|
+
const text = lineageText(value);
|
|
75
|
+
if (text)
|
|
76
|
+
return text;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
function lenientHeader(headers, name) {
|
|
81
|
+
const value = headers[name];
|
|
82
|
+
return lineageText(Array.isArray(value) ? value[0] : value);
|
|
83
|
+
}
|
|
84
|
+
function nestedTurnMetadata(clientMetadata) {
|
|
85
|
+
const raw = clientMetadata?.['x-codex-turn-metadata'];
|
|
86
|
+
if (typeof raw !== 'string' || Buffer.byteLength(raw) > 16 * 1024)
|
|
87
|
+
return null;
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(raw);
|
|
90
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
64
96
|
function codexMetadataObject(value, source) {
|
|
65
97
|
if (value === undefined || value === null)
|
|
66
98
|
return null;
|
|
@@ -108,7 +140,18 @@ export function resolveCodexSessionIdentity(headers, payload = null) {
|
|
|
108
140
|
], 'session');
|
|
109
141
|
if (sessionId && !threadId)
|
|
110
142
|
throw new DesktopBridgeError('bridge_codex_thread_id_missing');
|
|
111
|
-
|
|
143
|
+
const nested = nestedTurnMetadata(clientMetadata);
|
|
144
|
+
const lineage = (key, header = null) => [
|
|
145
|
+
turnMetadata?.[key], clientMetadata?.[key], nested?.[key],
|
|
146
|
+
...(header ? [clientMetadata?.[header], lenientHeader(headers, header)] : []),
|
|
147
|
+
];
|
|
148
|
+
return {
|
|
149
|
+
thread_id: threadId,
|
|
150
|
+
session_id: sessionId,
|
|
151
|
+
parent_thread_id: firstLineage(lineage('parent_thread_id', 'x-codex-parent-thread-id')),
|
|
152
|
+
subagent_kind: firstLineage(lineage('subagent_kind', 'x-openai-subagent')),
|
|
153
|
+
thread_source: firstLineage(lineage('thread_source')),
|
|
154
|
+
};
|
|
112
155
|
}
|
|
113
156
|
function comparableOrigin(value, referer) {
|
|
114
157
|
if (value === 'null')
|
|
@@ -216,6 +259,18 @@ export function resolveBridgeRequestRoute(request, policy, pins) {
|
|
|
216
259
|
session_pin: nextPin,
|
|
217
260
|
};
|
|
218
261
|
}
|
|
262
|
+
function threadPinProvider(request, config) {
|
|
263
|
+
if (!request.session_id)
|
|
264
|
+
return null;
|
|
265
|
+
let threadId;
|
|
266
|
+
try {
|
|
267
|
+
threadId = canonicalSessionId(request.session_id);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
return config.providerSessionPins.find((pin) => pin.thread_id === threadId)?.provider_id ?? null;
|
|
273
|
+
}
|
|
219
274
|
export function assertDesktopBridgeRouteContext(request, config) {
|
|
220
275
|
const resolver = config.resolveRequestRoute || resolveBridgeRequestRoute;
|
|
221
276
|
const policy = config.routePolicy;
|
|
@@ -235,9 +290,14 @@ export function assertDesktopBridgeRouteContext(request, config) {
|
|
|
235
290
|
else
|
|
236
291
|
throw error;
|
|
237
292
|
}
|
|
293
|
+
else if (error instanceof DesktopBridgeError && error.code === 'session_pin_route_unavailable'
|
|
294
|
+
&& openRouterOnlyRefusesPinnedProvider(request, threadPinProvider(request, config), config)) {
|
|
295
|
+
throw new DesktopBridgeError(OPENROUTER_ONLY_ROUTE_BLOCKED);
|
|
296
|
+
}
|
|
238
297
|
else
|
|
239
298
|
throw error;
|
|
240
299
|
}
|
|
300
|
+
assertOpenRouterOnlyRoute(request, route, config);
|
|
241
301
|
if (route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID) {
|
|
242
302
|
if (!desktopBridgeOfficialPassthroughEnabled(config))
|
|
243
303
|
throw new DesktopBridgeError('bridge_official_passthrough_unavailable');
|
|
@@ -603,6 +663,8 @@ export function validateDesktopBridgeConfig(config) {
|
|
|
603
663
|
throw new DesktopBridgeError('bridge_provider_credential_resolver_missing');
|
|
604
664
|
if (config.officialPassthrough)
|
|
605
665
|
validateRemoteUrl(config.officialPassthrough.baseUrl);
|
|
666
|
+
if (config.openRouterOnly !== undefined && config.openRouterOnly !== null)
|
|
667
|
+
validateOpenRouterOnlyConfig(config.openRouterOnly);
|
|
606
668
|
assertRegistryAndPolicy(config, config.providerRegistry);
|
|
607
669
|
if (!Object.values(config.providerRegistry.providers).some((provider) => provider.enabled)) {
|
|
608
670
|
throw new DesktopBridgeError('bridge_provider_registry_no_enabled_provider');
|
|
@@ -3,6 +3,7 @@ import http, {} from 'node:http';
|
|
|
3
3
|
import net, {} from 'node:net';
|
|
4
4
|
import { destroyDesktopBridgeUpstreamAgents, forwardHttp, prepareDesktopBridgeRequest } from './http-forward.js';
|
|
5
5
|
import { createDesktopBridgeRejectionLogger } from './rejection-log.js';
|
|
6
|
+
import { OPENROUTER_ONLY_REFUSAL_CODES } from './exclusive-provider-guard.js';
|
|
6
7
|
import { assertAllowedOrigin, assertAllowedPath, assertLoopbackPeer, assertLoopbackListenHost, assertWebSocketUpgrade, prepareDesktopBridgeConfig, safeBridgeErrorCode, validatePreparedDesktopBridgeConfig, } from './security.js';
|
|
7
8
|
import { createDesktopBridgePublicState, desktopBridgeListenOrigin, desktopBridgeStatePath, refreshDesktopBridgeState, removeDesktopBridgeStateIfOwned, writeDesktopBridgeState, } from './state.js';
|
|
8
9
|
import { DesktopBridgeError, DesktopBridgeRequestBodyTooLargeError } from './types.js';
|
|
@@ -85,6 +86,8 @@ function handleDiagnosticWebSocket(req, socket, head) {
|
|
|
85
86
|
consume();
|
|
86
87
|
}
|
|
87
88
|
function rejectionStatus(code) {
|
|
89
|
+
if (OPENROUTER_ONLY_REFUSAL_CODES.has(code))
|
|
90
|
+
return 409;
|
|
88
91
|
if (code === 'bridge_request_body_too_large')
|
|
89
92
|
return 413;
|
|
90
93
|
if (code === 'bridge_path_not_allowed')
|
|
@@ -98,6 +101,8 @@ function rejectionStatus(code) {
|
|
|
98
101
|
return 400;
|
|
99
102
|
}
|
|
100
103
|
function rejectionStatusText(status) {
|
|
104
|
+
if (status === 409)
|
|
105
|
+
return 'Conflict';
|
|
101
106
|
if (status === 413)
|
|
102
107
|
return 'Payload Too Large';
|
|
103
108
|
if (status === 404)
|
|
@@ -23,6 +23,9 @@ export function desktopBridgeConfigGeneration(config) {
|
|
|
23
23
|
client_capability_sha256: config.clientCapabilitySha256,
|
|
24
24
|
allowed_paths: [...config.allowedPathPrefixes], allowed_origins: [...config.allowedOrigins].sort(),
|
|
25
25
|
connect_timeout_ms: config.connectTimeoutMs, idle_timeout_ms: config.idleTimeoutMs,
|
|
26
|
+
...(config.openRouterOnly?.enabled === true
|
|
27
|
+
? { openrouter_only: [...config.openRouterOnly.subagent_models].sort() }
|
|
28
|
+
: {}),
|
|
26
29
|
}));
|
|
27
30
|
}
|
|
28
31
|
export function createDesktopBridgePublicState(config, options = {}) {
|
|
@@ -5,6 +5,7 @@ import { BRIDGE_OFFICIAL_ROUTE_ID } from '../bridge-contracts.js';
|
|
|
5
5
|
import { buildOfficialPassthroughWebSocketHeaders, buildProviderWebSocketHeaders } from './header-policy.js';
|
|
6
6
|
import { createDesktopBridgeRejectionLogger } from './rejection-log.js';
|
|
7
7
|
import { rewriteLocationHeader } from './location-rewrite.js';
|
|
8
|
+
import { OPENROUTER_ONLY_REFUSAL_CODES, openRouterOnlyEnabled } from './exclusive-provider-guard.js';
|
|
8
9
|
import { desktopBridgeOfficialPassthroughEnabled, ensureDesktopBridgeRemoteTarget, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader, canonicalSessionId } from './security.js';
|
|
9
10
|
import { desktopBridgeListenOrigin } from './state.js';
|
|
10
11
|
import { forwardResponsesWebSocket, isResponsesWebSocketRequest } from './responses-websocket.js';
|
|
@@ -78,6 +79,8 @@ function websocketPinnedModel(threadId, config) {
|
|
|
78
79
|
return null;
|
|
79
80
|
}
|
|
80
81
|
const pin = config.providerSessionPins.find((entry) => entry.thread_id === canonical);
|
|
82
|
+
if (pin && openRouterOnlyEnabled(config) && pin.provider_id !== 'openrouter')
|
|
83
|
+
return null;
|
|
81
84
|
return pin?.public_model || null;
|
|
82
85
|
}
|
|
83
86
|
export async function prepareDesktopBridgeWebSocketRequest(req, config) {
|
|
@@ -91,7 +94,7 @@ export async function prepareDesktopBridgeWebSocketRequest(req, config) {
|
|
|
91
94
|
public_model: publicModel,
|
|
92
95
|
session_id: sessionIdentity.thread_id,
|
|
93
96
|
pathname: new URL(req.url || '/', 'http://bridge.invalid').pathname,
|
|
94
|
-
transport: 'websocket', headers: req.headers,
|
|
97
|
+
transport: 'websocket', headers: req.headers, identity: sessionIdentity,
|
|
95
98
|
}, config);
|
|
96
99
|
if (route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID)
|
|
97
100
|
return { route, provider: null, credential: null };
|
|
@@ -110,6 +113,7 @@ const PERMANENT_UPGRADE_REFUSALS = new Set([
|
|
|
110
113
|
'bridge_websocket_route_unresolvable',
|
|
111
114
|
'catalog_model_route_missing',
|
|
112
115
|
'bridge_provider_route_unavailable',
|
|
116
|
+
...OPENROUTER_ONLY_REFUSAL_CODES,
|
|
113
117
|
]);
|
|
114
118
|
export function safeEndUpgradeSocket(socket, payload) {
|
|
115
119
|
socket.on('error', () => undefined);
|
|
@@ -14,6 +14,7 @@ import { applyOfficialModelPassthrough, buildBridgeRoutingPolicy, readBridgeRout
|
|
|
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';
|
|
17
|
+
import { openRouterOnlyExposure, readControllerOpenRouterOnlyState, withoutCodexLbCatalog } from './openrouter-only-catalog.js';
|
|
17
18
|
export async function syncCatalog(options) {
|
|
18
19
|
const result = await syncCatalogInternal(options);
|
|
19
20
|
const status = await desktopBridgeStatusV3(options);
|
|
@@ -40,14 +41,19 @@ export async function syncCatalogInternal(options, behavior = {}) {
|
|
|
40
41
|
credentials: rawCredentials,
|
|
41
42
|
storedRegistry
|
|
42
43
|
});
|
|
43
|
-
const
|
|
44
|
+
const openRouterOnly = behavior.openRouterOnly ?? readControllerOpenRouterOnlyState(paths);
|
|
45
|
+
const catalogRegistry = (value) => openRouterOnly.enabled ? withoutCodexLbCatalog(value) : value;
|
|
46
|
+
const catalogs = await fetchProviderCatalogs(catalogRegistry(initialRegistry), rawCredentials, paths, options);
|
|
44
47
|
const credentials = await resolveValidatedCredentials(options, paths);
|
|
45
48
|
const registry = await resolveBridgeProviderRegistry({ home: paths.home, credentials, storedRegistry });
|
|
46
49
|
const selectionState = await readBridgeModelSelectionState(paths.home, nowIso(options));
|
|
47
50
|
const selection = selectionState.stored
|
|
48
51
|
? selectionState.selection
|
|
49
52
|
: seedSelectionFromConfiguredModel(selectionState.selection, await readText(paths.configPath, ''));
|
|
50
|
-
const
|
|
53
|
+
const exposure = openRouterOnly.enabled
|
|
54
|
+
? openRouterOnlyExposure(selection, openRouterOnly, await readText(paths.configPath, ''))
|
|
55
|
+
: selection;
|
|
56
|
+
const build = buildCombinedBridgeCatalog(catalogRegistry(registry), { catalogs, created_at: nowIso(options), selection: exposure });
|
|
51
57
|
if (!selectionState.stored)
|
|
52
58
|
await writeBridgeModelSelection(paths.home, selection).catch(() => undefined);
|
|
53
59
|
await writeJsonAtomic(bridgeAvailableModelsPath(paths.home), {
|
|
@@ -72,10 +78,12 @@ export async function syncCatalogInternal(options, behavior = {}) {
|
|
|
72
78
|
&& Object.values(build.route_index.routes).some((route) => route.provider_id === providerId));
|
|
73
79
|
const previousDefault = priorPolicy.policy?.default_provider_id || null;
|
|
74
80
|
const historicalDefault = historicalIntent.default_provider_id;
|
|
75
|
-
const defaultProvider =
|
|
81
|
+
const defaultProvider = openRouterOnly.enabled && priorPolicy.policy
|
|
76
82
|
? previousDefault
|
|
77
|
-
:
|
|
78
|
-
|
|
83
|
+
: previousDefault && readyProviders.includes(previousDefault)
|
|
84
|
+
? previousDefault
|
|
85
|
+
: historicalDefault && readyProviders.includes(historicalDefault) ? historicalDefault
|
|
86
|
+
: readyProviders.length === 1 ? readyProviders[0] || null : null;
|
|
79
87
|
const persistedSettings = await readDesktopBridgeServiceSettings(options.settingsPath || desktopBridgeServicePaths(paths.home).settings_path).catch(() => null);
|
|
80
88
|
const officialModelsMode = await resolveEffectiveOfficialModelsMode(persistedSettings?.official_passthrough, {
|
|
81
89
|
home: paths.home,
|
|
@@ -154,6 +162,24 @@ export async function syncCatalogInternal(options, behavior = {}) {
|
|
|
154
162
|
active_generation_preserved: rollback?.ok === true
|
|
155
163
|
};
|
|
156
164
|
}
|
|
165
|
+
if (behavior.onActivated) {
|
|
166
|
+
try {
|
|
167
|
+
await behavior.onActivated();
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
const rollback = migration.receipt
|
|
171
|
+
? await rollbackDesktopBridgeUnificationReceipt({ receipt: migration.receipt })
|
|
172
|
+
: null;
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
build,
|
|
176
|
+
activation: activationResult(staging, false, [safeCode(error, 'combined_catalog_commit_failed')]),
|
|
177
|
+
migration,
|
|
178
|
+
rollback,
|
|
179
|
+
active_generation_preserved: rollback?.ok === true
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
157
183
|
const serviceBefore = await (options.serviceStatusImpl || desktopBridgeServiceStatus)({ ...options, home: paths.home });
|
|
158
184
|
if (behavior.restartService !== false && (serviceBefore.installed || serviceBefore.running)) {
|
|
159
185
|
await (options.bootstrapServiceImpl || bootstrapExistingDesktopBridgeService)({
|
|
@@ -8,6 +8,8 @@ import { DESKTOP_BRIDGE_OFFICIAL_UPSTREAM_BASE_URL } from '../desktop-bridge/ind
|
|
|
8
8
|
import { resolveBridgeRequestRoute } from '../request-route-resolver.js';
|
|
9
9
|
import { applyOfficialModelPassthrough, buildBridgeRoutingPolicy, setBridgeRoutingDefault, writeBridgeRoutingPolicy } from '../provider-route-policy.js';
|
|
10
10
|
import { syncCatalogInternal } from './catalog.js';
|
|
11
|
+
import { codexRestartBlockers, releaseOpenRouterOnlyForAuthPriority, releaseOpenRouterOnlyWithoutBridge, restartCodexAfterOpenRouterOnlyOff } from './openrouter-only-commands.js';
|
|
12
|
+
import { openRouterOnlyStatus } from './openrouter-only-status.js';
|
|
11
13
|
import { commandResult, controllerPaths, nowIso, persistRuntimeSettings, providerCode, providerRegistrySnapshot, stringArray } from './shared.js';
|
|
12
14
|
import { desktopBridgeStatusV3, loadCore, statusFromCore } from './status.js';
|
|
13
15
|
import { verifyDesktopBridgeV3 } from './verification.js';
|
|
@@ -107,6 +109,15 @@ export async function setOfficialModelsMode(mode, options) {
|
|
|
107
109
|
return commandResult('route.official-models', true, status, { mode, effective_mode: effective, official_models: officialModels, policy_generation: policy.policy_generation }, [], options);
|
|
108
110
|
}
|
|
109
111
|
export async function setAuthPriority(enabled, options) {
|
|
112
|
+
const released = enabled ? await releaseOpenRouterOnlyForAuthPriority(options) : null;
|
|
113
|
+
if (released && !released.ok) {
|
|
114
|
+
const status = await desktopBridgeStatusV3(options);
|
|
115
|
+
return commandResult('auth-priority.set', false, status, {
|
|
116
|
+
auth_priority: status.auth_priority,
|
|
117
|
+
openrouter_only: await openRouterOnlyStatus(options),
|
|
118
|
+
catalog_sync: released.catalog_sync
|
|
119
|
+
}, released.blockers, options);
|
|
120
|
+
}
|
|
110
121
|
const core = await loadCore(options);
|
|
111
122
|
const settingsPath = options.settingsPath || desktopBridgeServicePaths(core.paths.home).settings_path;
|
|
112
123
|
const persisted = await readDesktopBridgeServiceSettings(settingsPath);
|
|
@@ -127,8 +138,13 @@ export async function setAuthPriority(enabled, options) {
|
|
|
127
138
|
else {
|
|
128
139
|
await writeDesktopBridgeServiceSettings(settingsPath, nextSettings);
|
|
129
140
|
}
|
|
141
|
+
const codexRestart = released?.changed ? await restartCodexAfterOpenRouterOnlyOff(released, options) : null;
|
|
130
142
|
const status = await desktopBridgeStatusV3(options);
|
|
131
|
-
return commandResult('auth-priority.set', true, status, {
|
|
143
|
+
return commandResult('auth-priority.set', true, status, {
|
|
144
|
+
auth_priority: status.auth_priority,
|
|
145
|
+
openrouter_only: await openRouterOnlyStatus(options),
|
|
146
|
+
...(released?.changed ? { openrouter_only_off: { ...released, codex_restart: codexRestart } } : {})
|
|
147
|
+
}, [...(released?.blockers || []), ...codexRestartBlockers(codexRestart)], options);
|
|
132
148
|
}
|
|
133
149
|
export async function explainRoute(model, options) {
|
|
134
150
|
const core = await loadCore(options);
|
|
@@ -166,6 +182,7 @@ export async function unmanageDesktopBridge(options) {
|
|
|
166
182
|
return commandResult('unmanage', false, status, { write, service: stopped, recovery }, [error.message], options);
|
|
167
183
|
}
|
|
168
184
|
const cleaned = await removePreservedServiceArtifacts(options, paths.home, 'unmanage');
|
|
185
|
+
const openRouterOnlyOff = await releaseOpenRouterOnlyWithoutBridge(options);
|
|
169
186
|
const status = await desktopBridgeStatusV3(options);
|
|
170
187
|
requireUnmanaged(status.management.managed, 'unmanage');
|
|
171
188
|
return commandResult('unmanage', true, status, {
|
|
@@ -173,8 +190,9 @@ export async function unmanageDesktopBridge(options) {
|
|
|
173
190
|
credentials_deleted: false,
|
|
174
191
|
service: cleaned,
|
|
175
192
|
stopped_service: stopped,
|
|
176
|
-
config_backup_path: write.backup_path || null
|
|
177
|
-
|
|
193
|
+
config_backup_path: write.backup_path || null,
|
|
194
|
+
...(openRouterOnlyOff ? { openrouter_only_off: openRouterOnlyOff } : {})
|
|
195
|
+
}, openRouterOnlyOff?.blockers || [], options);
|
|
178
196
|
}
|
|
179
197
|
export async function rollbackDesktopBridge(receiptId, options) {
|
|
180
198
|
if (!/^[A-Za-z0-9._-]{1,160}$/.test(receiptId))
|
|
@@ -205,6 +223,7 @@ export async function rollbackDesktopBridge(receiptId, options) {
|
|
|
205
223
|
return commandResult('rollback', false, status, { rollback, service: stopped, recovery }, [rollback.status], options);
|
|
206
224
|
}
|
|
207
225
|
const cleaned = await removePreservedServiceArtifacts(options, paths.home, 'rollback');
|
|
226
|
+
const openRouterOnlyOff = await releaseOpenRouterOnlyWithoutBridge(options);
|
|
208
227
|
const observed = await desktopBridgeStatusV3(options);
|
|
209
228
|
if (observed.management.managed)
|
|
210
229
|
throw new Error('desktop_bridge_rollback_final_state_managed');
|
|
@@ -215,7 +234,7 @@ export async function rollbackDesktopBridge(receiptId, options) {
|
|
|
215
234
|
reason: 'rollback_complete'
|
|
216
235
|
}
|
|
217
236
|
};
|
|
218
|
-
return commandResult('rollback', true, status, { rollback, service: cleaned, stopped_service: stopped }, [], options);
|
|
237
|
+
return commandResult('rollback', true, status, { rollback, service: cleaned, stopped_service: stopped, ...(openRouterOnlyOff ? { openrouter_only_off: openRouterOnlyOff } : {}) }, openRouterOnlyOff?.blockers || [], options);
|
|
219
238
|
}
|
|
220
239
|
async function currentServiceStatus(options, home) {
|
|
221
240
|
return (options.serviceStatusImpl || desktopBridgeServiceStatus)({ ...options, home });
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readTopLevelTomlString } from '../../codex-app/codex-model-catalog.js';
|
|
2
|
+
import { isOpenRouterModelId } from '../../imagegen/imagegen-config.js';
|
|
3
|
+
import { readOpenRouterOnlyStateSync } from '../../subagents/child-model-allowlist.js';
|
|
4
|
+
import { MAX_SELECTED_OPENROUTER_MODELS } from '../combined-catalog/model-selection.js';
|
|
5
|
+
import { canonicalizeBridgeModelId } from '../route-index.js';
|
|
6
|
+
export function openRouterOnlyStoreLocation(paths) {
|
|
7
|
+
return { home: paths.home, env: { HOME: paths.home, CODEX_HOME: paths.codexHome } };
|
|
8
|
+
}
|
|
9
|
+
export function readControllerOpenRouterOnlyState(paths) {
|
|
10
|
+
return readOpenRouterOnlyStateSync(openRouterOnlyStoreLocation(paths));
|
|
11
|
+
}
|
|
12
|
+
export function openRouterOnlyExposure(selection, state, configText) {
|
|
13
|
+
const configured = readTopLevelTomlString(configText, 'model');
|
|
14
|
+
const ordered = [
|
|
15
|
+
...state.subagent_models.map((entry) => entry.model),
|
|
16
|
+
...(isOpenRouterModelId(configured) ? [configured] : []),
|
|
17
|
+
...selection.openrouter.public_ids
|
|
18
|
+
];
|
|
19
|
+
const ids = [];
|
|
20
|
+
for (const raw of ordered) {
|
|
21
|
+
const id = canonicalizeBridgeModelId(raw);
|
|
22
|
+
if (id && !ids.includes(id))
|
|
23
|
+
ids.push(id);
|
|
24
|
+
}
|
|
25
|
+
return { ...selection, openrouter: { mode: 'selected', public_ids: ids.slice(0, MAX_SELECTED_OPENROUTER_MODELS) } };
|
|
26
|
+
}
|
|
27
|
+
export function withoutCodexLbCatalog(registry) {
|
|
28
|
+
return {
|
|
29
|
+
...registry,
|
|
30
|
+
profiles: {
|
|
31
|
+
...registry.profiles,
|
|
32
|
+
'codex-lb': { ...registry.profiles['codex-lb'], enabled: false, state: 'disabled' }
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function hasOpenRouterRoute(routeIndex, model) {
|
|
37
|
+
const key = canonicalizeBridgeModelId(model);
|
|
38
|
+
return Boolean(key && routeIndex?.routes[key]?.provider_id === 'openrouter');
|
|
39
|
+
}
|