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
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import net from 'node:net';
|
|
3
3
|
import tls from 'node:tls';
|
|
4
|
-
import {
|
|
4
|
+
import { BRIDGE_OFFICIAL_ROUTE_ID } from '../bridge-contracts.js';
|
|
5
|
+
import { buildOfficialPassthroughWebSocketHeaders, buildProviderWebSocketHeaders } from './header-policy.js';
|
|
5
6
|
import { createDesktopBridgeRejectionLogger } from './rejection-log.js';
|
|
6
7
|
import { rewriteLocationHeader } from './location-rewrite.js';
|
|
7
|
-
import { resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader, canonicalSessionId } from './security.js';
|
|
8
|
+
import { desktopBridgeOfficialPassthroughEnabled, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader, canonicalSessionId } from './security.js';
|
|
8
9
|
import { desktopBridgeListenOrigin } from './state.js';
|
|
9
10
|
import { DESKTOP_BRIDGE_DIAGNOSTIC_PROTOCOL, DesktopBridgeError, } from './types.js';
|
|
10
11
|
const MAX_HEAD = 64 * 1024;
|
|
@@ -82,7 +83,7 @@ export async function prepareDesktopBridgeWebSocketRequest(req, config) {
|
|
|
82
83
|
const sessionIdentity = resolveCodexSessionIdentity(req.headers);
|
|
83
84
|
const pinnedModel = websocketPinnedModel(sessionIdentity.thread_id, config);
|
|
84
85
|
const publicModel = singleBridgeHeader(req.headers, 'x-sks-model') || pinnedModel || '';
|
|
85
|
-
if (!publicModel) {
|
|
86
|
+
if (!publicModel && !(desktopBridgeOfficialPassthroughEnabled(config) && config.officialRemote)) {
|
|
86
87
|
throw new DesktopBridgeError('bridge_websocket_route_unresolvable');
|
|
87
88
|
}
|
|
88
89
|
const route = await resolveAndBindDesktopBridgeRouteContext({
|
|
@@ -91,6 +92,8 @@ export async function prepareDesktopBridgeWebSocketRequest(req, config) {
|
|
|
91
92
|
pathname: new URL(req.url || '/', 'http://bridge.invalid').pathname,
|
|
92
93
|
transport: 'websocket', headers: req.headers,
|
|
93
94
|
}, config);
|
|
95
|
+
if (route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID)
|
|
96
|
+
return { route, provider: null, credential: null };
|
|
94
97
|
const provider = config.providers[route.provider_id];
|
|
95
98
|
if (!provider)
|
|
96
99
|
throw new DesktopBridgeError('bridge_provider_route_unavailable');
|
|
@@ -107,6 +110,17 @@ const PERMANENT_UPGRADE_REFUSALS = new Set([
|
|
|
107
110
|
'catalog_model_route_missing',
|
|
108
111
|
'bridge_provider_route_unavailable',
|
|
109
112
|
]);
|
|
113
|
+
export function safeEndUpgradeSocket(socket, payload) {
|
|
114
|
+
socket.on('error', () => undefined);
|
|
115
|
+
if (socket.destroyed || socket.writableEnded)
|
|
116
|
+
return;
|
|
117
|
+
try {
|
|
118
|
+
socket.end(payload);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
socket.destroy();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
110
124
|
function writeUpgradeFailure(client, error, req) {
|
|
111
125
|
const code = safeBridgeErrorCode(error) || 'bridge_websocket_upstream_unavailable';
|
|
112
126
|
const permanent = PERMANENT_UPGRADE_REFUSALS.has(code);
|
|
@@ -119,9 +133,7 @@ function writeUpgradeFailure(client, error, req) {
|
|
|
119
133
|
...(req?.url === undefined ? {} : { url: req.url }),
|
|
120
134
|
status,
|
|
121
135
|
});
|
|
122
|
-
|
|
123
|
-
return;
|
|
124
|
-
client.end(`HTTP/1.1 ${status} ${reason}\r\nContent-Type: application/json\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n`
|
|
136
|
+
safeEndUpgradeSocket(client, `HTTP/1.1 ${status} ${reason}\r\nContent-Type: application/json\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n`
|
|
125
137
|
+ JSON.stringify({ error: { type: 'sks_bridge_error', code, retryable: !permanent } }));
|
|
126
138
|
}
|
|
127
139
|
export async function forwardWebSocket(req, client, head, config, authenticatedLocalBaseUrl = desktopBridgeListenOrigin(config)) {
|
|
@@ -133,11 +145,18 @@ export async function forwardWebSocket(req, client, head, config, authenticatedL
|
|
|
133
145
|
writeUpgradeFailure(client, error, req);
|
|
134
146
|
return;
|
|
135
147
|
}
|
|
136
|
-
const { provider, credential } = prepared;
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
148
|
+
const { provider, credential, route } = prepared;
|
|
149
|
+
const official = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID;
|
|
150
|
+
const remote = official ? config.officialRemote : provider?.remote;
|
|
151
|
+
if (!remote) {
|
|
152
|
+
writeUpgradeFailure(client, new DesktopBridgeError(official ? 'bridge_official_passthrough_unavailable' : 'bridge_provider_route_unavailable'), req);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const upstreamBaseUrl = official ? remote.baseUrl : provider.base_url;
|
|
156
|
+
const target = resolveDesktopBridgeTarget(req.url, remote);
|
|
157
|
+
const upstream = remote.secure
|
|
158
|
+
? tls.connect({ host: remote.address, port: remote.port, ...(remote.tlsServername ? { servername: remote.tlsServername } : {}) })
|
|
159
|
+
: net.connect({ host: remote.address, port: remote.port, family: remote.family });
|
|
141
160
|
const key = String(req.headers['sec-websocket-key'] || '');
|
|
142
161
|
const requestedProtocol = String(req.headers['sec-websocket-protocol'] || '').split(',')[0]?.trim() || null;
|
|
143
162
|
let connected = false;
|
|
@@ -156,7 +175,9 @@ export async function forwardWebSocket(req, client, head, config, authenticatedL
|
|
|
156
175
|
const clientSocket = client;
|
|
157
176
|
if (typeof clientSocket.setKeepAlive === 'function')
|
|
158
177
|
clientSocket.setKeepAlive(true, 30_000);
|
|
159
|
-
const headers =
|
|
178
|
+
const headers = official
|
|
179
|
+
? buildOfficialPassthroughWebSocketHeaders(req.headers, target.host)
|
|
180
|
+
: buildProviderWebSocketHeaders(req.headers, { providerId: provider.provider_id, authTransport: provider.auth_transport, credential: credential }, target.host);
|
|
160
181
|
upstream.write([`${req.method || 'GET'} ${target.pathname}${target.search} HTTP/1.1`, ...serializeHeaders(headers), '', ''].join('\r\n'));
|
|
161
182
|
if (head.length)
|
|
162
183
|
upstream.write(head);
|
|
@@ -174,7 +195,7 @@ export async function forwardWebSocket(req, client, head, config, authenticatedL
|
|
|
174
195
|
const remaining = response.subarray(boundary + 4);
|
|
175
196
|
try {
|
|
176
197
|
validateUpgrade(raw, key, requestedProtocol);
|
|
177
|
-
client.write(rewriteUpgradeResponseHead(raw,
|
|
198
|
+
client.write(rewriteUpgradeResponseHead(raw, upstreamBaseUrl, authenticatedLocalBaseUrl));
|
|
178
199
|
if (remaining.length)
|
|
179
200
|
client.write(remaining);
|
|
180
201
|
}
|
|
@@ -187,7 +208,7 @@ export async function forwardWebSocket(req, client, head, config, authenticatedL
|
|
|
187
208
|
};
|
|
188
209
|
upstream.on('data', onData);
|
|
189
210
|
};
|
|
190
|
-
if (
|
|
211
|
+
if (remote.secure)
|
|
191
212
|
upstream.once('secureConnect', onConnected);
|
|
192
213
|
else
|
|
193
214
|
upstream.once('connect', onConnected);
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
export function authSemanticIdentityPreserved(before, after) {
|
|
2
2
|
if (before.path !== after.path || before.exists !== after.exists)
|
|
3
3
|
return false;
|
|
4
|
-
if (before.sha256 !== after.sha256)
|
|
5
|
-
return false;
|
|
6
4
|
const beforeIsOAuth = before.mode === 'chatgpt_oauth' || before.mode === 'mixed';
|
|
7
5
|
const afterIsOAuth = after.mode === 'chatgpt_oauth' || after.mode === 'mixed';
|
|
8
6
|
if (beforeIsOAuth || afterIsOAuth) {
|
|
@@ -11,7 +9,7 @@ export function authSemanticIdentityPreserved(before, after) {
|
|
|
11
9
|
&& before.semantic_fingerprint !== null
|
|
12
10
|
&& before.semantic_fingerprint === after.semantic_fingerprint;
|
|
13
11
|
}
|
|
14
|
-
return
|
|
12
|
+
return before.sha256 === after.sha256;
|
|
15
13
|
}
|
|
16
14
|
export function buildDesktopBridgeMigrationReceipt(input) {
|
|
17
15
|
const common = {
|
|
@@ -17,7 +17,7 @@ const SETTINGS_V1_KEYS = new Set([
|
|
|
17
17
|
const SETTINGS_V2_KEYS = new Set([
|
|
18
18
|
'schema', 'listen_host', 'listen_port', 'provider_registry', 'route_policy',
|
|
19
19
|
'provider_session_pins', 'client_capability_sha256', 'allowed_origins',
|
|
20
|
-
'connect_timeout_ms', 'idle_timeout_ms',
|
|
20
|
+
'connect_timeout_ms', 'idle_timeout_ms', 'official_passthrough',
|
|
21
21
|
]);
|
|
22
22
|
const TRANSFERABLE_V1_KEYS = [
|
|
23
23
|
'listen_host', 'listen_port', 'allowed_origins', 'connect_timeout_ms', 'idle_timeout_ms',
|
|
@@ -90,8 +90,7 @@ async function migrateDesktopBridgeConfigUnlocked(input) {
|
|
|
90
90
|
const authPreserved = authSemanticIdentityPreserved(authBefore, authAfter);
|
|
91
91
|
const configSha = sha256(currentConfig);
|
|
92
92
|
const configUnchanged = await fileSha256OrMissing(configPath) === configSha;
|
|
93
|
-
|
|
94
|
-
if (!authPreserved || !configUnchanged || !authBytesUnchanged) {
|
|
93
|
+
if (!authPreserved || !configUnchanged) {
|
|
95
94
|
return {
|
|
96
95
|
...baseResult,
|
|
97
96
|
ok: false,
|
|
@@ -101,8 +100,7 @@ async function migrateDesktopBridgeConfigUnlocked(input) {
|
|
|
101
100
|
auth_semantic_identity_preserved: authPreserved,
|
|
102
101
|
blockers: [
|
|
103
102
|
...(!configUnchanged ? ['desktop_bridge_config_changed_during_noop'] : []),
|
|
104
|
-
...(!authPreserved ? ['desktop_oauth_identity_changed'] : [])
|
|
105
|
-
...(authPreserved && !authBytesUnchanged ? ['desktop_auth_changed_during_noop'] : [])
|
|
103
|
+
...(!authPreserved ? ['desktop_oauth_identity_changed'] : [])
|
|
106
104
|
]
|
|
107
105
|
};
|
|
108
106
|
}
|
|
@@ -5,12 +5,12 @@ import { listOpenRouterModels } from '../../providers/openrouter/openrouter-acco
|
|
|
5
5
|
import { buildCombinedBridgeCatalog, readActiveCombinedBridgeCatalog, stageCombinedBridgeCatalog } from '../combined-catalog.js';
|
|
6
6
|
import { COMBINED_BRIDGE_CATALOG_TTL_MS } from '../combined-catalog/contracts.js';
|
|
7
7
|
import { codexLbEnvPath, codexLbMetadataPath, loadCodexLbEnv, readCodexLbModelCatalog } from '../codex-lb-env.js';
|
|
8
|
-
import { bootstrapExistingDesktopBridgeService, desktopBridgeServicePaths, desktopBridgeServiceStatus, resolveDesktopBridgeActivationSettings } from '../desktop-service.js';
|
|
8
|
+
import { bootstrapExistingDesktopBridgeService, desktopBridgeServicePaths, desktopBridgeServiceStatus, readDesktopBridgeServiceSettings, resolveDesktopBridgeActivationSettings, resolveEffectiveOfficialModelsMode } from '../desktop-service.js';
|
|
9
9
|
import { inspectHistoricalDesktopBridgeIntent, migrateDesktopBridgeConfig } from '../desktop-bridge-migration.js';
|
|
10
10
|
import { rollbackDesktopBridgeUnificationReceipt } from '../migration-receipt.js';
|
|
11
11
|
import { recordProviderCredentialValidation } from '../provider-credentials.js';
|
|
12
12
|
import { bridgeProviderRegistryPath, buildStoredBridgeProviderRegistry, loadStoredBridgeProviderRegistry, resolveBridgeProviderRegistry, serializeStoredBridgeProviderRegistry } from '../provider-registry.js';
|
|
13
|
-
import { buildBridgeRoutingPolicy, readBridgeRoutingPolicy } from '../provider-route-policy.js';
|
|
13
|
+
import { applyOfficialModelPassthrough, buildBridgeRoutingPolicy, readBridgeRoutingPolicy } from '../provider-route-policy.js';
|
|
14
14
|
import { sha256Stable } from '../route-index.js';
|
|
15
15
|
import { bridgeBaseUrl, commandResult, controllerEnv, controllerPaths, nowIso, providerCode, providerRegistrySnapshot, resolveRawCredentials, resolveValidatedCredentials, safeCode, serializedSettings, stringArray, timeoutMs, unique } from './shared.js';
|
|
16
16
|
import { desktopBridgeStatusV3 } from './status.js';
|
|
@@ -76,12 +76,14 @@ export async function syncCatalogInternal(options) {
|
|
|
76
76
|
? previousDefault
|
|
77
77
|
: historicalDefault && readyProviders.includes(historicalDefault) ? historicalDefault
|
|
78
78
|
: readyProviders.length === 1 ? readyProviders[0] || null : null;
|
|
79
|
-
const
|
|
79
|
+
const persistedSettings = await readDesktopBridgeServiceSettings(desktopBridgeServicePaths(paths.home).settings_path).catch(() => null);
|
|
80
|
+
const officialModelsMode = await resolveEffectiveOfficialModelsMode(persistedSettings?.official_passthrough, { home: paths.home, authPath: paths.authPath });
|
|
81
|
+
const policy = applyOfficialModelPassthrough(buildBridgeRoutingPolicy({
|
|
80
82
|
route_index: build.route_index,
|
|
81
83
|
catalog_generation: build.catalog.generation,
|
|
82
84
|
default_provider_id: defaultProvider,
|
|
83
85
|
changed_at: nowIso(options)
|
|
84
|
-
});
|
|
86
|
+
}), { mode: officialModelsMode, changedAt: nowIso(options) });
|
|
85
87
|
let settings;
|
|
86
88
|
let managedBridgeBaseUrl;
|
|
87
89
|
try {
|
|
@@ -2,10 +2,11 @@ import path from 'node:path';
|
|
|
2
2
|
import { removeDesktopBridgeManagedConfig } from '../../../cli/install-helpers-codex-lb-config.js';
|
|
3
3
|
import { safeWriteCodexConfigToml } from '../../codex-runtime/codex-desktop-config-policy.js';
|
|
4
4
|
import { exists, readText } from '../../fsx.js';
|
|
5
|
-
import { bootstrapExistingDesktopBridgeService, desktopBridgeServiceStatus, installAndStartDesktopBridgeService, stopDesktopBridgeService } from '../desktop-service.js';
|
|
5
|
+
import { bootstrapExistingDesktopBridgeService, desktopBridgeServicePaths, desktopBridgeServiceStatus, installAndStartDesktopBridgeService, readDesktopBridgeServiceSettings, resolveEffectiveOfficialModelsMode, stopDesktopBridgeService } from '../desktop-service.js';
|
|
6
6
|
import { rollbackDesktopBridgeUnificationReceipt } from '../migration-receipt.js';
|
|
7
|
+
import { DESKTOP_BRIDGE_OFFICIAL_UPSTREAM_BASE_URL } from '../desktop-bridge/index.js';
|
|
7
8
|
import { resolveBridgeRequestRoute } from '../request-route-resolver.js';
|
|
8
|
-
import { setBridgeRoutingDefault, writeBridgeRoutingPolicy } from '../provider-route-policy.js';
|
|
9
|
+
import { applyOfficialModelPassthrough, buildBridgeRoutingPolicy, setBridgeRoutingDefault, writeBridgeRoutingPolicy } from '../provider-route-policy.js';
|
|
9
10
|
import { syncCatalogInternal } from './catalog.js';
|
|
10
11
|
import { commandResult, controllerPaths, nowIso, persistRuntimeSettings, providerCode, providerRegistrySnapshot, stringArray } from './shared.js';
|
|
11
12
|
import { desktopBridgeStatusV3, loadCore, statusFromCore } from './status.js';
|
|
@@ -70,6 +71,39 @@ export async function setDefaultProvider(providerId, options) {
|
|
|
70
71
|
const status = await desktopBridgeStatusV3(options);
|
|
71
72
|
return commandResult('route.set-default', true, status, { provider_id: providerId, policy_generation: policy.policy_generation }, [], options);
|
|
72
73
|
}
|
|
74
|
+
export async function setOfficialModelsMode(mode, options) {
|
|
75
|
+
const core = await loadCore(options);
|
|
76
|
+
if (!core.policy || !core.activeCatalog.ok)
|
|
77
|
+
throw new Error('bridge_route_policy_missing');
|
|
78
|
+
const settingsPath = options.settingsPath || desktopBridgeServicePaths(core.paths.home).settings_path;
|
|
79
|
+
const persisted = await readDesktopBridgeServiceSettings(settingsPath).catch(() => null);
|
|
80
|
+
const nextOfficial = {
|
|
81
|
+
enabled: persisted?.official_passthrough?.enabled ?? true,
|
|
82
|
+
base_url: persisted?.official_passthrough?.base_url || DESKTOP_BRIDGE_OFFICIAL_UPSTREAM_BASE_URL,
|
|
83
|
+
models: mode,
|
|
84
|
+
};
|
|
85
|
+
const effective = mode === 'auto'
|
|
86
|
+
? await resolveEffectiveOfficialModelsMode(nextOfficial, { home: core.paths.home })
|
|
87
|
+
: mode;
|
|
88
|
+
const policy = effective === 'passthrough'
|
|
89
|
+
? applyOfficialModelPassthrough(core.policy, { mode: 'passthrough', changedAt: nowIso(options) })
|
|
90
|
+
: buildBridgeRoutingPolicy({
|
|
91
|
+
route_index: core.activeCatalog.route_index,
|
|
92
|
+
catalog_generation: core.policy.catalog_generation,
|
|
93
|
+
default_provider_id: core.policy.default_provider_id,
|
|
94
|
+
changed_at: nowIso(options)
|
|
95
|
+
});
|
|
96
|
+
await writeBridgeRoutingPolicy(core.paths.routePolicyPath, policy, core.activeCatalog.route_index);
|
|
97
|
+
await persistRuntimeSettings({ ...core, policy, policyBlockers: [] }, {
|
|
98
|
+
...options,
|
|
99
|
+
settings: { ...(options.settings || {}), official_passthrough: nextOfficial },
|
|
100
|
+
});
|
|
101
|
+
const status = await desktopBridgeStatusV3(options);
|
|
102
|
+
const officialModels = Object.entries(policy.model_routes)
|
|
103
|
+
.filter(([, route]) => route.provider_id === 'openai')
|
|
104
|
+
.map(([model]) => model);
|
|
105
|
+
return commandResult('route.official-models', true, status, { mode, effective_mode: effective, official_models: officialModels, policy_generation: policy.policy_generation }, [], options);
|
|
106
|
+
}
|
|
73
107
|
export async function explainRoute(model, options) {
|
|
74
108
|
const core = await loadCore(options);
|
|
75
109
|
if (!core.policy || !core.activeCatalog.ok)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { listSelectableModels, selectExposedModels, syncCatalog } from './desktop-controller-v3/catalog.js';
|
|
2
|
-
import { explainRoute, ensureDesktopBridge, repairDesktopBridge, rollbackDesktopBridge, setDefaultProvider, unmanageDesktopBridge } from './desktop-controller-v3/lifecycle-commands.js';
|
|
2
|
+
import { explainRoute, ensureDesktopBridge, repairDesktopBridge, rollbackDesktopBridge, setDefaultProvider, setOfficialModelsMode, unmanageDesktopBridge } from './desktop-controller-v3/lifecycle-commands.js';
|
|
3
3
|
import { configureProvider, removeCredential, setProviderState, validateProvider } from './desktop-controller-v3/provider-commands.js';
|
|
4
4
|
import { commandResult, safeCode } from './desktop-controller-v3/shared.js';
|
|
5
5
|
import { desktopBridgeStatusV3 } from './desktop-controller-v3/status.js';
|
|
@@ -56,6 +56,8 @@ export async function executeDesktopBridgeCommandV3(request, options = {}) {
|
|
|
56
56
|
}
|
|
57
57
|
if (request.operation === 'route.set-default')
|
|
58
58
|
return setDefaultProvider(request.provider_id, options);
|
|
59
|
+
if (request.operation === 'route.official-models')
|
|
60
|
+
return setOfficialModelsMode(request.mode, options);
|
|
59
61
|
if (request.operation === 'route.explain')
|
|
60
62
|
return explainRoute(request.model, options);
|
|
61
63
|
if (request.operation === 'unmanage')
|
|
@@ -9,9 +9,11 @@ import { loadCodexLbEnv } from './codex-lb-env.js';
|
|
|
9
9
|
import { resolveOpenRouterApiKey } from '../providers/openrouter/openrouter-secret-store.js';
|
|
10
10
|
import { cleanupRetiredDesktopBridgeRuntime, prepareRetiredDesktopBridgeRuntime, } from './desktop-bridge-migration/retired-runtime-cleanup.js';
|
|
11
11
|
import { canonicalizeBridgeModelId, normalizeBridgeUpstreamModelId, sha256Stable } from './route-index.js';
|
|
12
|
+
import { captureCodexAuthSnapshot } from './desktop-auth-invariant.js';
|
|
13
|
+
import { applyOfficialModelPassthrough, bridgeRoutePolicyPath, writeBridgeRoutingPolicy } from './provider-route-policy.js';
|
|
12
14
|
import { desktopBridgeRuntimeVersion, desktopBridgeRuntimeVersionStale } from './desktop-bridge/state.js';
|
|
13
15
|
import { PACKAGE_VERSION } from '../version.js';
|
|
14
|
-
import { DESKTOP_BRIDGE_ALLOWED_PATH_PREFIXES, DESKTOP_BRIDGE_LAUNCHD_LABEL, desktopBridgeConfigGeneration, desktopBridgeLaunchdPlistPath, desktopBridgeProcessExists, desktopBridgeStatePath, getDesktopBridgeStatus, preflightDesktopBridge, readDesktopBridgeState, safeBridgeErrorCode, writeDesktopBridgeLaunchdPlist, selectAvailableDesktopBridgePort, startPreparedDesktopBridge, DESKTOP_BRIDGE_STATE_SCHEMA, DesktopBridgeError, } from './desktop-bridge/index.js';
|
|
16
|
+
import { DESKTOP_BRIDGE_ALLOWED_PATH_PREFIXES, DESKTOP_BRIDGE_LAUNCHD_LABEL, desktopBridgeConfigGeneration, desktopBridgeLaunchdPlistPath, desktopBridgeProcessExists, desktopBridgeStatePath, getDesktopBridgeStatus, preflightDesktopBridge, readDesktopBridgeState, safeBridgeErrorCode, writeDesktopBridgeLaunchdPlist, selectAvailableDesktopBridgePort, startPreparedDesktopBridge, DESKTOP_BRIDGE_STATE_SCHEMA, DESKTOP_BRIDGE_OFFICIAL_UPSTREAM_BASE_URL, DesktopBridgeError, } from './desktop-bridge/index.js';
|
|
15
17
|
export const DEFAULT_DESKTOP_BRIDGE_HOST = '127.0.0.1';
|
|
16
18
|
export const DEFAULT_DESKTOP_BRIDGE_PORT = 49_152;
|
|
17
19
|
export const DESKTOP_BRIDGE_SETTINGS_SCHEMA = 'sks.desktop-bridge-settings.v2';
|
|
@@ -159,7 +161,7 @@ export async function writeDesktopBridgeServiceSettings(file, settings) {
|
|
|
159
161
|
}
|
|
160
162
|
async function writeDesktopBridgeServiceSettingsUnlocked(file, settings) {
|
|
161
163
|
const validated = validateDesktopBridgeServiceSettings(settings);
|
|
162
|
-
const persisted = { schema: validated.schema, listen_host: validated.listen_host, listen_port: validated.listen_port, provider_registry: validated.provider_registry, route_policy: validated.route_policy, provider_session_pins: validated.provider_session_pins, client_capability_sha256: validated.client_capability_sha256, allowed_origins: validated.allowed_origins, connect_timeout_ms: validated.connect_timeout_ms, idle_timeout_ms: validated.idle_timeout_ms };
|
|
164
|
+
const persisted = { schema: validated.schema, listen_host: validated.listen_host, listen_port: validated.listen_port, provider_registry: validated.provider_registry, route_policy: validated.route_policy, provider_session_pins: validated.provider_session_pins, client_capability_sha256: validated.client_capability_sha256, allowed_origins: validated.allowed_origins, connect_timeout_ms: validated.connect_timeout_ms, idle_timeout_ms: validated.idle_timeout_ms, official_passthrough: validated.official_passthrough };
|
|
163
165
|
await writeTextAtomic(file, `${JSON.stringify(persisted, null, 2)}\n`, { mode: 0o600 });
|
|
164
166
|
await fsp.chmod(file, 0o600);
|
|
165
167
|
}
|
|
@@ -246,11 +248,13 @@ export async function resolveDesktopBridgeRuntimeConfig(options = {}) {
|
|
|
246
248
|
if (!Object.keys(settings.route_policy.model_routes).length)
|
|
247
249
|
throw new Error('catalog_model_route_missing');
|
|
248
250
|
const enabledRoutes = new Set(Object.values(settings.route_policy.model_routes).map((route) => route.provider_id));
|
|
249
|
-
|
|
251
|
+
const providerRouteIds = [...enabledRoutes].filter((id) => id === 'codex-lb' || id === 'openrouter');
|
|
252
|
+
const officialOnly = providerRouteIds.length === 0 && settings.official_passthrough.enabled;
|
|
253
|
+
if (!officialOnly && !providerRouteIds.some((id) => credentials.registry.providers[id].credential_state === 'ready'))
|
|
250
254
|
throw new Error('desktop_bridge_provider_credentials_unavailable');
|
|
251
255
|
const persistProviderSessionPins = options.persistProviderSessionPins
|
|
252
256
|
|| ((pins) => persistDesktopBridgeSessionPins(paths.settings_path, settings, pins));
|
|
253
|
-
const config = { providerRegistry: settings.provider_registry, routePolicy: settings.route_policy, providerSessionPins: settings.provider_session_pins, ...(options.resolveRequestRoute ? { resolveRequestRoute: options.resolveRequestRoute } : {}), persistProviderSessionPins, resolveProviderCredential: credentials.resolver, clientCapabilitySha256: settings.client_capability_sha256, listenHost: settings.listen_host, listenPort: settings.listen_port, allowedPathPrefixes: DESKTOP_BRIDGE_ALLOWED_PATH_PREFIXES, allowedOrigins: settings.allowed_origins, connectTimeoutMs: settings.connect_timeout_ms, idleTimeoutMs: settings.idle_timeout_ms };
|
|
257
|
+
const config = { providerRegistry: settings.provider_registry, routePolicy: settings.route_policy, providerSessionPins: settings.provider_session_pins, ...(options.resolveRequestRoute ? { resolveRequestRoute: options.resolveRequestRoute } : {}), persistProviderSessionPins, resolveProviderCredential: credentials.resolver, clientCapabilitySha256: settings.client_capability_sha256, listenHost: settings.listen_host, listenPort: settings.listen_port, allowedPathPrefixes: DESKTOP_BRIDGE_ALLOWED_PATH_PREFIXES, allowedOrigins: settings.allowed_origins, connectTimeoutMs: settings.connect_timeout_ms, idleTimeoutMs: settings.idle_timeout_ms, officialPassthrough: settings.official_passthrough.enabled ? { baseUrl: settings.official_passthrough.base_url } : null };
|
|
254
258
|
const primary = credentials.sources['codex-lb'] || credentials.sources.openrouter;
|
|
255
259
|
if (!primary)
|
|
256
260
|
throw new Error('desktop_bridge_provider_credentials_unavailable');
|
|
@@ -434,24 +438,67 @@ async function installedPackageVersion() {
|
|
|
434
438
|
return null;
|
|
435
439
|
}
|
|
436
440
|
}
|
|
441
|
+
export const DESKTOP_BRIDGE_SKEW_RESTART_COOLDOWN_MS = 30 * 60_000;
|
|
442
|
+
export function desktopBridgeSkewRestartSuppressed(marker, running, installed, nowMs) {
|
|
443
|
+
return Boolean(marker
|
|
444
|
+
&& marker.running === running
|
|
445
|
+
&& marker.installed === installed
|
|
446
|
+
&& nowMs - Date.parse(marker.at) < DESKTOP_BRIDGE_SKEW_RESTART_COOLDOWN_MS);
|
|
447
|
+
}
|
|
448
|
+
async function readSkewRestartMarker(file) {
|
|
449
|
+
try {
|
|
450
|
+
const parsed = JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
451
|
+
if (typeof parsed.running !== 'string' || typeof parsed.installed !== 'string' || !Number.isFinite(Date.parse(String(parsed.at))))
|
|
452
|
+
return null;
|
|
453
|
+
return { running: parsed.running, installed: parsed.installed, at: String(parsed.at) };
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async function autoApplyOfficialModelsAtServe(runtime, home, options) {
|
|
460
|
+
const settings = runtime.settings;
|
|
461
|
+
const mode = await resolveEffectiveOfficialModelsMode(settings.official_passthrough, { home });
|
|
462
|
+
if (mode !== 'passthrough')
|
|
463
|
+
return;
|
|
464
|
+
const flipped = applyOfficialModelPassthrough(settings.route_policy, { mode: 'passthrough' });
|
|
465
|
+
if (flipped.policy_generation === settings.route_policy.policy_generation)
|
|
466
|
+
return;
|
|
467
|
+
const nextSettings = { ...settings, route_policy: flipped };
|
|
468
|
+
await writeDesktopBridgeServiceSettings(options.settingsPath || desktopBridgeServicePaths(home).settings_path, nextSettings);
|
|
469
|
+
await writeBridgeRoutingPolicy(bridgeRoutePolicyPath(path.join(path.resolve(home), '.codex')), flipped).catch(() => undefined);
|
|
470
|
+
settings.route_policy = flipped;
|
|
471
|
+
runtime.config.routePolicy = flipped;
|
|
472
|
+
process.stdout.write(`${JSON.stringify({ schema: 'sks.desktop-bridge-log.v2', event: 'sks.desktop_bridge.official_models_auto_applied', at: new Date().toISOString(), sks_version: PACKAGE_VERSION, mode: 'passthrough', policy_generation: flipped.policy_generation, secret_fields_redacted: true })}\n`);
|
|
473
|
+
}
|
|
437
474
|
export async function serveDesktopBridge(options = {}) {
|
|
438
475
|
let handle = null;
|
|
439
476
|
try {
|
|
440
477
|
const runtime = await resolveDesktopBridgeRuntimeConfig(options);
|
|
478
|
+
const serveHome = options.home || options.env?.HOME || process.env.HOME || os.homedir();
|
|
479
|
+
await autoApplyOfficialModelsAtServe(runtime, serveHome, options);
|
|
441
480
|
const supervised = desktopBridgeIsSupervised();
|
|
481
|
+
const skewMarkerPath = path.join(path.dirname(runtime.paths.state_path), 'desktop-bridge-skew-restart.json');
|
|
442
482
|
handle = await startPreparedDesktopBridge(await preflightDesktopBridge(runtime.config), {
|
|
443
483
|
statePath: runtime.paths.state_path,
|
|
444
484
|
versionSkew: {
|
|
445
485
|
readInstalledVersion: installedPackageVersion,
|
|
446
486
|
onSkew: (installedVersion) => {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
487
|
+
void (async () => {
|
|
488
|
+
const marker = await readSkewRestartMarker(skewMarkerPath);
|
|
489
|
+
const cooldownActive = desktopBridgeSkewRestartSuppressed(marker, PACKAGE_VERSION, installedVersion, Date.now());
|
|
490
|
+
const action = !supervised ? 'logged_only' : cooldownActive ? 'suppressed_cooldown' : 'restarting';
|
|
491
|
+
process.stdout.write(`${JSON.stringify({ schema: 'sks.desktop-bridge-log.v2', event: 'sks.desktop_bridge.version_skew', at: new Date().toISOString(), running: PACKAGE_VERSION, installed: installedVersion, supervised, action, secret_fields_redacted: true })}\n`);
|
|
492
|
+
if (!supervised || cooldownActive)
|
|
493
|
+
return;
|
|
494
|
+
await writeTextAtomic(skewMarkerPath, `${JSON.stringify({ running: PACKAGE_VERSION, installed: installedVersion, at: new Date().toISOString() })}\n`, { mode: 0o600 }).catch(() => undefined);
|
|
495
|
+
await handle?.stop().catch(() => undefined);
|
|
496
|
+
process.exit(64);
|
|
497
|
+
})();
|
|
451
498
|
},
|
|
452
499
|
},
|
|
453
500
|
});
|
|
454
|
-
process.stdout.write(`${JSON.stringify({ schema: 'sks.desktop-bridge-log.v2', event: 'sks.desktop_bridge.started', pid: handle.state.pid, process_generation: handle.state.schema === DESKTOP_BRIDGE_STATE_SCHEMA ? handle.state.process_generation : null, provider_registry_generation: runtime.config.providerRegistry?.generation, route_policy_generation: runtime.config.routePolicy?.policy_generation, secret_fields_redacted: true })}\n`);
|
|
501
|
+
process.stdout.write(`${JSON.stringify({ schema: 'sks.desktop-bridge-log.v2', event: 'sks.desktop_bridge.started', at: new Date().toISOString(), sks_version: PACKAGE_VERSION, pid: handle.state.pid, process_generation: handle.state.schema === DESKTOP_BRIDGE_STATE_SCHEMA ? handle.state.process_generation : null, provider_registry_generation: runtime.config.providerRegistry?.generation, route_policy_generation: runtime.config.routePolicy?.policy_generation, secret_fields_redacted: true })}\n`);
|
|
455
502
|
await waitForShutdown(handle);
|
|
456
503
|
return { schema: 'sks.desktop-bridge-serve.v1', ok: true, status: 'stopped', state: handle.state };
|
|
457
504
|
}
|
|
@@ -470,7 +517,7 @@ function validateDesktopBridgeServiceSettings(value) {
|
|
|
470
517
|
const input = row;
|
|
471
518
|
if (input.schema !== DESKTOP_BRIDGE_SETTINGS_SCHEMA)
|
|
472
519
|
throw new Error('desktop_bridge_settings_schema_invalid');
|
|
473
|
-
const allowedKeys = new Set(['schema', 'listen_host', 'listen_port', 'provider_registry', 'route_policy', 'provider_session_pins', 'client_capability_sha256', 'allowed_origins', 'connect_timeout_ms', 'idle_timeout_ms']);
|
|
520
|
+
const allowedKeys = new Set(['schema', 'listen_host', 'listen_port', 'provider_registry', 'route_policy', 'provider_session_pins', 'client_capability_sha256', 'allowed_origins', 'connect_timeout_ms', 'idle_timeout_ms', 'official_passthrough']);
|
|
474
521
|
if (Object.keys(input).some((key) => !allowedKeys.has(key)))
|
|
475
522
|
throw new Error('desktop_bridge_settings_unknown_field');
|
|
476
523
|
const serialized = JSON.stringify(input);
|
|
@@ -502,7 +549,44 @@ function validateDesktopBridgeServiceSettings(value) {
|
|
|
502
549
|
throw new Error('desktop_bridge_settings_connect_timeout_invalid');
|
|
503
550
|
if (!Number.isFinite(idle) || idle < 1_000 || idle > 86_400_000)
|
|
504
551
|
throw new Error('desktop_bridge_settings_idle_timeout_invalid');
|
|
505
|
-
|
|
552
|
+
const officialPassthrough = validateOfficialPassthroughSettings(input.official_passthrough);
|
|
553
|
+
return { schema: DESKTOP_BRIDGE_SETTINGS_SCHEMA, listen_host: host, listen_port: port, provider_registry: registry, route_policy: policy, provider_session_pins: pins, client_capability_sha256: clientCapabilitySha256, allowed_origins: [...new Set(origins)], connect_timeout_ms: connect, idle_timeout_ms: idle, official_passthrough: officialPassthrough };
|
|
554
|
+
}
|
|
555
|
+
function validateOfficialPassthroughSettings(value) {
|
|
556
|
+
if (value === undefined || value === null) {
|
|
557
|
+
return { enabled: true, base_url: DESKTOP_BRIDGE_OFFICIAL_UPSTREAM_BASE_URL, models: 'auto' };
|
|
558
|
+
}
|
|
559
|
+
if (typeof value !== 'object' || Array.isArray(value))
|
|
560
|
+
throw new Error('desktop_bridge_settings_official_passthrough_invalid');
|
|
561
|
+
const row = value;
|
|
562
|
+
if (Object.keys(row).some((key) => key !== 'enabled' && key !== 'base_url' && key !== 'models'))
|
|
563
|
+
throw new Error('desktop_bridge_settings_official_passthrough_invalid');
|
|
564
|
+
const enabled = row.enabled === true;
|
|
565
|
+
const baseUrl = typeof row.base_url === 'string' && row.base_url.trim() ? row.base_url.trim() : DESKTOP_BRIDGE_OFFICIAL_UPSTREAM_BASE_URL;
|
|
566
|
+
try {
|
|
567
|
+
new URL(baseUrl);
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
throw new Error('desktop_bridge_settings_official_passthrough_invalid');
|
|
571
|
+
}
|
|
572
|
+
const models = row.models === 'passthrough' || row.models === 'gateway' || row.models === 'auto'
|
|
573
|
+
? row.models
|
|
574
|
+
: row.models === undefined ? 'auto' : null;
|
|
575
|
+
if (models === null)
|
|
576
|
+
throw new Error('desktop_bridge_settings_official_passthrough_invalid');
|
|
577
|
+
return { enabled, base_url: baseUrl, models };
|
|
578
|
+
}
|
|
579
|
+
export async function resolveEffectiveOfficialModelsMode(official, input) {
|
|
580
|
+
if (official && official.enabled === false)
|
|
581
|
+
return 'gateway';
|
|
582
|
+
const models = official?.models || 'auto';
|
|
583
|
+
if (models === 'passthrough' || models === 'gateway')
|
|
584
|
+
return models;
|
|
585
|
+
const snapshot = await captureCodexAuthSnapshot({
|
|
586
|
+
home: input.home,
|
|
587
|
+
...(input.authPath ? { authPath: input.authPath } : {}),
|
|
588
|
+
}).catch(() => null);
|
|
589
|
+
return snapshot && (snapshot.mode === 'chatgpt_oauth' || snapshot.mode === 'mixed') ? 'passthrough' : 'gateway';
|
|
506
590
|
}
|
|
507
591
|
function validateProviderSessionPins(value) {
|
|
508
592
|
const pins = Array.isArray(value) ? value : [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { BRIDGE_OFFICIAL_ROUTE_ID } from './bridge-contracts.js';
|
|
4
5
|
import { canonicalizeBridgeModelId, normalizeBridgeUpstreamModelId, sha256Stable } from './route-index.js';
|
|
5
6
|
import { writeJsonAtomic } from '../fsx.js';
|
|
6
7
|
import { withFileLock } from '../locks/file-lock.js';
|
|
@@ -58,6 +59,8 @@ export function validateBridgeRoutingPolicy(policy, routeIndex) {
|
|
|
58
59
|
blockers.push('bridge_route_policy_model_invalid');
|
|
59
60
|
if (routeIndex) {
|
|
60
61
|
for (const [model, target] of Object.entries(canonical)) {
|
|
62
|
+
if (target.provider_id === BRIDGE_OFFICIAL_ROUTE_ID)
|
|
63
|
+
continue;
|
|
61
64
|
const indexed = routeIndex.routes[model];
|
|
62
65
|
if (!indexed || !sameTarget(indexed, target))
|
|
63
66
|
blockers.push('bridge_route_policy_route_index_mismatch');
|
|
@@ -101,7 +104,7 @@ function canonicalRoutes(routes) {
|
|
|
101
104
|
for (const [model, target] of Object.entries(routes)) {
|
|
102
105
|
const canonicalModel = canonicalizeBridgeModelId(model);
|
|
103
106
|
const upstreamModel = normalizeBridgeUpstreamModelId(target.upstream_model);
|
|
104
|
-
if (!canonicalModel || !upstreamModel || !
|
|
107
|
+
if (!canonicalModel || !upstreamModel || !isRouteTargetId(target.provider_id))
|
|
105
108
|
continue;
|
|
106
109
|
rows.push([canonicalModel, { provider_id: target.provider_id, upstream_model: upstreamModel }]);
|
|
107
110
|
}
|
|
@@ -110,6 +113,32 @@ function canonicalRoutes(routes) {
|
|
|
110
113
|
function isProviderId(value) {
|
|
111
114
|
return value === 'codex-lb' || value === 'openrouter';
|
|
112
115
|
}
|
|
116
|
+
function isRouteTargetId(value) {
|
|
117
|
+
return isProviderId(value) || value === BRIDGE_OFFICIAL_ROUTE_ID;
|
|
118
|
+
}
|
|
119
|
+
export const OFFICIAL_MODEL_ID_PATTERN = /^(?:gpt-[0-9]|o[0-9]|codex-mini)/;
|
|
120
|
+
export function applyOfficialModelPassthrough(policy, input = { mode: 'passthrough' }) {
|
|
121
|
+
if (input.mode === 'gateway')
|
|
122
|
+
return policy;
|
|
123
|
+
const routes = {};
|
|
124
|
+
for (const [model, target] of Object.entries(policy.model_routes)) {
|
|
125
|
+
routes[model] = OFFICIAL_MODEL_ID_PATTERN.test(model) && !model.includes(':')
|
|
126
|
+
? { provider_id: BRIDGE_OFFICIAL_ROUTE_ID, upstream_model: model }
|
|
127
|
+
: target;
|
|
128
|
+
}
|
|
129
|
+
const semantic = {
|
|
130
|
+
default_provider_id: policy.default_provider_id,
|
|
131
|
+
fallback: 'none',
|
|
132
|
+
model_routes: canonicalRoutes(routes),
|
|
133
|
+
catalog_generation: policy.catalog_generation
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
schema: 'sks.bridge-routing-policy.v1',
|
|
137
|
+
...semantic,
|
|
138
|
+
policy_generation: sha256Stable(semantic),
|
|
139
|
+
changed_at: input.changedAt || new Date().toISOString()
|
|
140
|
+
};
|
|
141
|
+
}
|
|
113
142
|
function sameTarget(left, right) {
|
|
114
143
|
return left.provider_id === right.provider_id && left.upstream_model === right.upstream_model;
|
|
115
144
|
}
|
|
@@ -51,6 +51,20 @@ export function resolveBridgeRequestRoute(request, policy, options) {
|
|
|
51
51
|
const policyTarget = policy.model_routes[model];
|
|
52
52
|
if (!indexed || !policyTarget)
|
|
53
53
|
return blocked(base, 'catalog_model_route_missing');
|
|
54
|
+
if (policyTarget.provider_id === 'openai') {
|
|
55
|
+
return {
|
|
56
|
+
...base,
|
|
57
|
+
ok: true,
|
|
58
|
+
route: policyTarget,
|
|
59
|
+
endpoint_url: null,
|
|
60
|
+
source: 'route_index',
|
|
61
|
+
proposed_session_pin: null,
|
|
62
|
+
blockers: [],
|
|
63
|
+
recovery_action: null
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (indexed.provider_id === 'openai')
|
|
67
|
+
return blocked(base, 'catalog_route_provider_unknown');
|
|
54
68
|
if (!sameTarget(indexed, policyTarget))
|
|
55
69
|
return blocked(base, 'bridge_route_policy_route_index_mismatch');
|
|
56
70
|
if (pinClaim && !sameTarget(indexed, pinClaim))
|
|
@@ -18,6 +18,28 @@ import { MAX_LIFECYCLE_THREADS } from './subagent-skill-availability-contract.js
|
|
|
18
18
|
import { officialSubagentEvidenceReady, terminalBlockedNarutoGate } from '../subagents/terminal-subagent-state.js';
|
|
19
19
|
const SUBAGENT_LIFECYCLE_CAPTURE_FAILURE_SCHEMA = 'sks.subagent-lifecycle-capture-failure.v1';
|
|
20
20
|
const MAX_SUBAGENT_LIFECYCLE_CAPTURE_FAILURES = 528;
|
|
21
|
+
export const ACTIVE_OFFICIAL_WORKFLOW_IDLE_MS = 2 * 60 * 60 * 1000;
|
|
22
|
+
function parseIsoMs(value) {
|
|
23
|
+
const parsed = Date.parse(String(value || ''));
|
|
24
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
25
|
+
}
|
|
26
|
+
export function officialWorkflowLastActivityMs(input) {
|
|
27
|
+
const times = [
|
|
28
|
+
parseIsoMs(input.plan?.created_at),
|
|
29
|
+
parseIsoMs(input.plan?.updated_at),
|
|
30
|
+
parseIsoMs(input.plan?.wave_lifecycle?.updated_at),
|
|
31
|
+
parseIsoMs(input.plan?.wave_lifecycle?.created_at),
|
|
32
|
+
parseIsoMs(input.gate?.updated_at),
|
|
33
|
+
parseIsoMs(input.gate?.created_at),
|
|
34
|
+
...(input.events || []).map((event) => parseIsoMs(event.occurred_at ?? event.ts))
|
|
35
|
+
];
|
|
36
|
+
return times.reduce((max, value) => (value > max ? value : max), 0);
|
|
37
|
+
}
|
|
38
|
+
export function officialWorkflowIsIdle(activityMs, nowMs = Date.now()) {
|
|
39
|
+
if (activityMs <= 0)
|
|
40
|
+
return true;
|
|
41
|
+
return nowMs - activityMs > ACTIVE_OFFICIAL_WORKFLOW_IDLE_MS;
|
|
42
|
+
}
|
|
21
43
|
export async function inspectActiveOfficialSubagentWorkflow(root, state, sessionKey = null) {
|
|
22
44
|
const missionId = String(state?.mission_id || '').trim();
|
|
23
45
|
const workflowRunId = String(state?.official_subagent_run_id || '').trim();
|
|
@@ -71,8 +93,14 @@ export async function inspectActiveOfficialSubagentWorkflow(root, state, session
|
|
|
71
93
|
if (events.length > 0 && openThreads !== liveThreads.size) {
|
|
72
94
|
return { status: 'invalid', missionId, workflowRunId, reason: 'active_lifecycle_event_mismatch' };
|
|
73
95
|
}
|
|
96
|
+
if (officialWorkflowIsIdle(officialWorkflowLastActivityMs({ plan, gate, events }))) {
|
|
97
|
+
return { status: 'inactive' };
|
|
98
|
+
}
|
|
74
99
|
return { status: 'active', missionId, workflowRunId, openThreads };
|
|
75
100
|
}
|
|
101
|
+
if (officialWorkflowIsIdle(officialWorkflowLastActivityMs({ plan, gate, events }))) {
|
|
102
|
+
return { status: 'inactive' };
|
|
103
|
+
}
|
|
76
104
|
return { status: 'active', missionId, workflowRunId, openThreads: liveThreads.size };
|
|
77
105
|
}
|
|
78
106
|
catch {
|
|
@@ -2,7 +2,7 @@ import fsp from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { projectRoot, readJson, writeJsonAtomic, writeTextAtomic, appendJsonl, nowIso, sha256, packageRoot } from './fsx.js';
|
|
4
4
|
import { looksInteractiveCommand, interactiveCommandReason } from './no-question-guard.js';
|
|
5
|
-
import {
|
|
5
|
+
import { loadOwnedRouteState, missionDir, sessionStateKey, setCurrent, stateFileForSession } from './mission.js';
|
|
6
6
|
import { checkDbOperation, dbBlockReason, handleMadSksUserConfirmation } from './db-safety.js';
|
|
7
7
|
import { maybeRecordMadSksSqlPlaneToolResultFromToolUse } from './mad-sks/sql-plane/result-lifecycle.js';
|
|
8
8
|
import { checkHarnessModification, harnessGuardBlockReason, isHarnessSourceProject } from './harness-guard.js';
|
|
@@ -53,7 +53,7 @@ export { selftestCodexCommitHooks } from './hooks-runtime/codex-commit-hooks-sel
|
|
|
53
53
|
async function loadState(root, payload = {}) {
|
|
54
54
|
const sessionKey = conversationId(payload);
|
|
55
55
|
if (!explicitConversationId(payload))
|
|
56
|
-
return
|
|
56
|
+
return loadOwnedRouteState(root);
|
|
57
57
|
const hashed = sessionStateKey(sessionKey);
|
|
58
58
|
const sessionState = await readJson(stateFileForSession(root, sessionKey), null).catch(() => null);
|
|
59
59
|
return sessionState ? { ...sessionState, _session_key: sessionState._session_key || hashed } : {};
|
|
@@ -18,7 +18,7 @@ export async function resolveDesktopBridgeImagegenTarget(opts = {}) {
|
|
|
18
18
|
const route = model && policy?.fallback === 'none'
|
|
19
19
|
? policy.model_routes?.[model] || null
|
|
20
20
|
: null;
|
|
21
|
-
const provider = route ? status?.providers?.[route.provider_id] || null : null;
|
|
21
|
+
const provider = route && route.provider_id !== 'openai' ? status?.providers?.[route.provider_id] || null : null;
|
|
22
22
|
const providerImagegen = provider?.capabilities?.capabilities?.image_generation || null;
|
|
23
23
|
const blocker = !status || status.schema !== 'sks.desktop-bridge-status.v3'
|
|
24
24
|
? 'desktop_bridge_status_unavailable'
|
|
@@ -50,7 +50,7 @@ export async function resolveDesktopBridgeImagegenTarget(opts = {}) {
|
|
|
50
50
|
model,
|
|
51
51
|
model_source: model ? 'explicit' : null,
|
|
52
52
|
route: route || null,
|
|
53
|
-
provider_id: route
|
|
53
|
+
provider_id: route && route.provider_id !== 'openai' ? route.provider_id : null,
|
|
54
54
|
status_source: injected ? 'injected_fixture' : 'runtime',
|
|
55
55
|
live_evidence_allowed: bridgeVerified && !injected,
|
|
56
56
|
blocker,
|