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.
Files changed (67) hide show
  1. package/README.md +12 -181
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/commands/bridge.js +61 -1
  5. package/dist/commands/codex-app.js +4 -1
  6. package/dist/config/skills-manifest.json +1 -1
  7. package/dist/core/agents/native-worker-backend-router.js +55 -0
  8. package/dist/core/codex-app/codex-app-restart-policy.js +20 -0
  9. package/dist/core/codex-app/codex-context-window.js +61 -24
  10. package/dist/core/codex-app/menubar/constants.js +4 -0
  11. package/dist/core/codex-compat/codex-hook-output-builders.js +3 -1
  12. package/dist/core/codex-lb/bridge-cli-contract.js +2 -0
  13. package/dist/core/codex-lb/desktop-bridge/exclusive-provider-guard.js +59 -0
  14. package/dist/core/codex-lb/desktop-bridge/http-forward.js +23 -15
  15. package/dist/core/codex-lb/desktop-bridge/index.js +1 -0
  16. package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +10 -3
  17. package/dist/core/codex-lb/desktop-bridge/security.js +63 -1
  18. package/dist/core/codex-lb/desktop-bridge/server.js +5 -0
  19. package/dist/core/codex-lb/desktop-bridge/state.js +3 -0
  20. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +5 -1
  21. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +31 -5
  22. package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +23 -4
  23. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-catalog.js +39 -0
  24. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +286 -0
  25. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-main-model.js +101 -0
  26. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-status.js +68 -0
  27. package/dist/core/codex-lb/desktop-controller-v3/status.js +8 -5
  28. package/dist/core/codex-lb/desktop-controller-v3.js +26 -2
  29. package/dist/core/codex-lb/desktop-service.js +13 -2
  30. package/dist/core/commands/naruto-command.js +1 -1
  31. package/dist/core/decisions/child-model-choice.js +107 -0
  32. package/dist/core/decisions/cli.js +19 -2
  33. package/dist/core/decisions/routing.js +5 -4
  34. package/dist/core/doctor/command-alias-cleanup.js +2 -0
  35. package/dist/core/doctor/skill-legacy-surface.js +46 -1
  36. package/dist/core/hooks-runtime/hook-context.js +21 -7
  37. package/dist/core/hooks-runtime/hook-io.js +3 -2
  38. package/dist/core/hooks-runtime/jev-spawn-routing.js +83 -5
  39. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +3 -2
  40. package/dist/core/hooks-runtime/parent-orchestration-gate.js +18 -1
  41. package/dist/core/hooks-runtime/subagent-context.js +13 -2
  42. package/dist/core/hooks-runtime/subagent-spawn-policy.js +49 -6
  43. package/dist/core/hooks-runtime.js +12 -5
  44. package/dist/core/managed-assets/managed-assets-manifest.js +9 -4
  45. package/dist/core/subagents/child-model-allowlist.js +179 -0
  46. package/dist/core/subagents/child-model-plan.js +117 -0
  47. package/dist/core/subagents/naruto-host-credentials.js +82 -13
  48. package/dist/core/subagents/official-subagent-config.js +66 -15
  49. package/dist/core/subagents/official-subagent-preparation.js +45 -12
  50. package/dist/core/subagents/official-subagent-prompt.js +82 -11
  51. package/dist/core/subagents/official-subagent-runner.js +3 -0
  52. package/dist/core/subagents/read-only-list-role.js +73 -0
  53. package/dist/core/subagents/role-model-preferences.js +54 -17
  54. package/dist/core/update/registry-availability.js +58 -0
  55. package/dist/core/update-check.js +8 -26
  56. package/dist/core/version.js +1 -1
  57. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +2 -0
  58. package/dist/native/sks-menubar/Sources/OpenRouterOnlyState.swift +314 -0
  59. package/dist/native/sks-menubar/Sources/ProcessClient.swift +2 -1
  60. package/dist/native/sks-menubar/Sources/ProvidersModelExposure.swift +15 -3
  61. package/dist/native/sks-menubar/Sources/ProvidersOpenRouterOnly.swift +162 -0
  62. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +36 -18
  63. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +10 -4
  64. package/dist/native/sks-menubar/Sources/SidebarItem.swift +2 -0
  65. package/dist/native/sks-menubar/Sources/SubagentModelsModels.swift +201 -0
  66. package/dist/native/sks-menubar/Sources/SubagentModelsViewController.swift +373 -0
  67. 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
- function bodyCarriesModel(rawUrl) {
50
- const pathname = new URL(String(rawUrl || '/'), 'http://bridge.invalid').pathname;
51
- return pathname === '/backend-api/codex/responses' || pathname === '/api/v1/responses' || pathname === '/v1/responses';
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
- if (bodyCarriesModel(req.url)) {
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
- try {
96
- const parsed = JSON.parse(decoded.toString('utf8'));
97
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
98
- throw new Error();
99
- payload = parsed;
100
- }
101
- catch {
102
- throw new DesktopBridgeError('bridge_responses_body_invalid_json');
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_') ? 409 : 502, {
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 } }));
@@ -1,5 +1,6 @@
1
1
  export * from './types.js';
2
2
  export * from './security.js';
3
+ export * from './exclusive-provider-guard.js';
3
4
  export * from './header-policy.js';
4
5
  export * from './location-rewrite.js';
5
6
  export * from './state.js';
@@ -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
- if (!bound)
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
- return { thread_id: threadId, session_id: sessionId };
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 catalogs = await fetchProviderCatalogs(initialRegistry, rawCredentials, paths, options);
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 build = buildCombinedBridgeCatalog(registry, { catalogs, created_at: nowIso(options), selection });
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 = previousDefault && readyProviders.includes(previousDefault)
81
+ const defaultProvider = openRouterOnly.enabled && priorPolicy.policy
76
82
  ? previousDefault
77
- : historicalDefault && readyProviders.includes(historicalDefault) ? historicalDefault
78
- : readyProviders.length === 1 ? readyProviders[0] || null : null;
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, { auth_priority: status.auth_priority }, [], options);
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
- }, [], options);
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
+ }