sneakoscope 9.2.4 → 9.2.6

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 CHANGED
@@ -22,7 +22,7 @@ Proof-first orchestration for Codex CLI, ChatGPT Desktop, AI coding agents, mult
22
22
  Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.
23
23
  <!-- END SKS SEARCH VISIBILITY MARKETING -->
24
24
 
25
- This README documents package **SKS 9.2.4** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
25
+ This README documents package **SKS 9.2.6** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
26
26
 
27
27
  Use the official latest stable SKS and Codex CLI releases. The Codex compatibility SSOT is always the **current latest stable** host; capability probes measure what that host can actually do. Product docs do not crown a fixed `0.x.y` string as SSOT (release pins and schema directories are measured artifacts for the current package, not a permanent product version claim). Menu Bar / Center induce updates to the latest stable build. Run `sks update-check` for what is installed and read the capability report for what is supported. Install SSOT is npm `sneakoscope@latest`; PATH `sks` and Menu Bar stamped generation must match that version or gates fail. It resolves managed SKS skills from the authoritative global install, preserves a runnable Naruto child slot when `max_threads=2`, and keeps Menu Bar repair transactional so stamped generations remain verifiable. Naruto uses stable opt-in multi-agent V2 when the host exposes it (Codex official multi-agent wrap-only; SKS does not reimplement a parallel runtime). Local code search is mode-separated (`sks search files|text|structure|symbol|context`); `context` is answered by the compiled TriWiki Context Graph (`context-graph.json` is exhaustive authority; `context-pack.json` and managed `AGENTS.md` are bounded projections) — see [docs/architecture/context-graph.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/architecture/context-graph.md) and [docs/PRODUCT-CONTRACT.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/PRODUCT-CONTRACT.md). See [CHANGELOG.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/CHANGELOG.md).
28
28
 
@@ -259,7 +259,7 @@ dependencies = [
259
259
 
260
260
  [[package]]
261
261
  name = "sks-core"
262
- version = "9.2.4"
262
+ version = "9.2.6"
263
263
  dependencies = [
264
264
  "globset",
265
265
  "grep-matcher",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "9.2.4"
3
+ version = "9.2.6"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -8,7 +8,7 @@ import { getCodexInfo } from '../core/codex-adapter.js';
8
8
  import { rustInfo } from '../core/rust-accelerator.js';
9
9
  import { codexAppIntegrationStatus } from '../core/codex-app.js';
10
10
  import { desktopBridgeStatusV3 } from '../core/codex-lb/desktop-controller-v3.js';
11
- import { repairDoctorDesktopBridgeCatalog } from '../core/doctor/desktop-bridge-catalog-repair.js';
11
+ import { readDesktopBridgeUnreachableUpstreamEvidence, repairDoctorDesktopBridgeCatalog } from '../core/doctor/desktop-bridge-catalog-repair.js';
12
12
  import { inspectCodexConfigReadability } from '../core/codex/codex-config-readability.js';
13
13
  import { inspectOAuthCallbackPortConflict, oauthCallbackDoctorGuidance } from '../core/codex/oauth-callback-port-diagnostic.js';
14
14
  import { inventoryCodexPermissionProfiles } from '../core/codex/codex-permission-profiles.js';
@@ -72,6 +72,14 @@ export async function inspectDoctorDesktopBridgeStatus(input = {}, deps = {}) {
72
72
  const blockers = expected && status.readiness.ready !== true
73
73
  ? (status.readiness.blockers || []).map(String).filter(Boolean)
74
74
  : [];
75
+ const recoveryActions = (status.recovery_actions || []).map(String).filter(Boolean);
76
+ if (expected && status.management?.managed === true) {
77
+ const evidence = await (deps.desktopBridgeUpstreamEvidenceImpl || readDesktopBridgeUnreachableUpstreamEvidence)(home).catch(() => null);
78
+ if (evidence) {
79
+ blockers.push(`desktop_bridge_upstream_unreachable:${evidence.code}`);
80
+ recoveryActions.push('Run `sks doctor --fix` (or `sks bridge repair`) to restart the Desktop Bridge and re-resolve its upstream address.');
81
+ }
82
+ }
75
83
  return {
76
84
  schema: 'sks.doctor-desktop-bridge.v1',
77
85
  ok: blockers.length === 0,
@@ -82,7 +90,7 @@ export async function inspectDoctorDesktopBridgeStatus(input = {}, deps = {}) {
82
90
  providers: status.providers,
83
91
  blockers,
84
92
  warnings: (status.readiness.warnings || []).map(String).filter(Boolean),
85
- recovery_actions: (status.recovery_actions || []).map(String).filter(Boolean)
93
+ recovery_actions: recoveryActions
86
94
  };
87
95
  }
88
96
  catch (error) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "sks.skills-manifest.v1",
3
- "package_version": "9.2.4",
3
+ "package_version": "9.2.6",
4
4
  "skills": [
5
5
  {
6
6
  "canonical_name": "sks",
@@ -2,9 +2,9 @@ export const MANAGED_CODEX_FEATURE_FLAGS = Object.freeze(['hooks', 'fast_mode',
2
2
  export const REMOVED_CODEX_FEATURE_FLAGS = Object.freeze([
3
3
  'fast_mode_ui',
4
4
  'codex_hooks',
5
+ 'multi_agent_mode',
5
6
  'remote_control',
6
7
  'codex_git_commit',
7
8
  'plugin_hooks',
8
- 'js_repl',
9
- 'multi_agent_mode'
9
+ 'js_repl'
10
10
  ]);
@@ -3,7 +3,10 @@ import path from 'node:path';
3
3
  import { findCodexBinary } from '../codex-adapter.js';
4
4
  import { ensureDir, runProcess } from '../fsx.js';
5
5
  import { codexCurrentCoreProbeTail, skippedCodexCurrentCoreProbe } from './codex-current-core-real-probes.js';
6
- const CURRENT_COLLAB_TOOLS = ['spawnAgent', 'sendInput', 'resumeAgent', 'wait', 'closeAgent'];
6
+ const CURRENT_COLLAB_TOOLS = [
7
+ 'spawnAgent', 'sendInput', 'resumeAgent', 'wait', 'closeAgent',
8
+ 'sendMessage', 'followupTask', 'interruptAgent', 'listAgents'
9
+ ];
7
10
  export async function runCodexCurrentCollabAgentToolSchemaRealProbe(input) {
8
11
  const started = Date.now();
9
12
  const codexBin = input.codexBin || await findCodexBinary();
@@ -23,7 +26,7 @@ export async function runCodexCurrentCollabAgentToolSchemaRealProbe(input) {
23
26
  ? schema.definitions.CollabAgentTool.enum.map(String)
24
27
  : [];
25
28
  const currentNamesPresent = CURRENT_COLLAB_TOOLS.every((name) => tools.includes(name));
26
- const legacyInterruptAbsent = !tools.includes('interruptAgent') && !tools.includes('interrupt_agent');
29
+ const legacyInterruptAbsent = !tools.includes('interrupt_agent');
27
30
  const collabItemPresent = JSON.stringify(schema?.definitions?.ThreadItem || {}).includes('collabAgentToolCall')
28
31
  || JSON.stringify(schema || {}).includes('collabAgentToolCall');
29
32
  const processExitedSuccessfully = result.code === 0;
@@ -5,7 +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 { resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader } from './security.js';
8
+ import { ensureDesktopBridgeRemoteTarget, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader } from './security.js';
9
9
  import { desktopBridgeListenOrigin } from './state.js';
10
10
  import { DesktopBridgeError } from './types.js';
11
11
  const MAX_UPSTREAM_ERROR_BODY_BYTES = 1024 * 1024;
@@ -251,6 +251,8 @@ class StalePooledSocketFailure extends Error {
251
251
  this.reason = reason;
252
252
  }
253
253
  }
254
+ class UnreachableUpstreamFailure extends StalePooledSocketFailure {
255
+ }
254
256
  export async function forwardHttp(req, res, config, prepared, authenticatedLocalBaseUrl = desktopBridgeListenOrigin(config)) {
255
257
  try {
256
258
  const request = prepared || await prepareDesktopBridgeRequest(req, config);
@@ -264,6 +266,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
264
266
  const upstreamBaseUrl = official ? remote.baseUrl : provider.base_url;
265
267
  if (!official && !request.credential)
266
268
  throw new DesktopBridgeError('bridge_provider_credential_invalid');
269
+ await ensureDesktopBridgeRemoteTarget(remote, config.remoteLookup);
267
270
  const target = resolveDesktopBridgeTarget(req.url, remote);
268
271
  const transport = remote.secure ? https : http;
269
272
  const headers = official
@@ -303,6 +306,13 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
303
306
  req.once('aborted', abort);
304
307
  res.once('close', abort);
305
308
  upstream.once('error', (error) => {
309
+ if (!responseStarted && isUnreachableUpstreamError(error)) {
310
+ if (replayable && canReplay) {
311
+ finish(new UnreachableUpstreamFailure(error));
312
+ return;
313
+ }
314
+ void refreshDesktopBridgeRemoteTarget(remote, config.remoteLookup);
315
+ }
306
316
  if (!responseStarted && replayable && !useFreshConnection && isStalePooledSocketFailure(upstream, error)) {
307
317
  finish(new StalePooledSocketFailure(error));
308
318
  return;
@@ -405,16 +415,30 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
405
415
  if (!(error instanceof StalePooledSocketFailure) || remainingReplays <= 0)
406
416
  throw error;
407
417
  remainingReplays -= 1;
408
- if (!useFreshConnection) {
418
+ if (error instanceof UnreachableUpstreamFailure) {
419
+ const staleAddress = remote.address;
420
+ await refreshDesktopBridgeRemoteTarget(remote, config.remoteLookup);
409
421
  agent.destroy();
410
- upstreamAgents.delete(`${remote.secure ? 'https' : 'http'}:${remote.address}:${remote.port}`);
422
+ upstreamAgents.delete(`${remote.secure ? 'https' : 'http'}:${staleAddress}:${remote.port}`);
423
+ logHttpRejection({
424
+ code: `bridge_upstream_unreachable_rerouted:${underlyingErrorCode(error.reason) || 'unknown'}`,
425
+ transport: 'http',
426
+ ...(req.method === undefined ? {} : { method: req.method }),
427
+ ...(req.url === undefined ? {} : { url: req.url }),
428
+ });
429
+ }
430
+ else {
431
+ if (!useFreshConnection) {
432
+ agent.destroy();
433
+ upstreamAgents.delete(`${remote.secure ? 'https' : 'http'}:${remote.address}:${remote.port}`);
434
+ }
435
+ logHttpRejection({
436
+ code: `bridge_upstream_socket_stale_replayed:${underlyingErrorCode(error.reason) || 'unknown'}`,
437
+ transport: 'http',
438
+ ...(req.method === undefined ? {} : { method: req.method }),
439
+ ...(req.url === undefined ? {} : { url: req.url }),
440
+ });
411
441
  }
412
- logHttpRejection({
413
- code: `bridge_upstream_socket_stale_replayed:${underlyingErrorCode(error.reason) || 'unknown'}`,
414
- transport: 'http',
415
- ...(req.method === undefined ? {} : { method: req.method }),
416
- ...(req.url === undefined ? {} : { url: req.url }),
417
- });
418
442
  useFreshConnection = true;
419
443
  const backoffIndex = TRANSIENT_UPSTREAM_REPLAY_LIMIT - remainingReplays - 1;
420
444
  const backoffMs = TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS[Math.max(0, backoffIndex)]
@@ -7,6 +7,7 @@ const MIN_HIGH_PORT = 49_152;
7
7
  const MAX_PORT = 65_535;
8
8
  const MAX_SESSION_PINS = 10_000;
9
9
  const sessionPinMutationQueues = new WeakMap();
10
+ const remoteRefreshStates = new WeakMap();
10
11
  const FORBIDDEN_REMOTE_ADDRESSES = new net.BlockList();
11
12
  FORBIDDEN_REMOTE_ADDRESSES.addSubnet('0.0.0.0', 8, 'ipv4');
12
13
  FORBIDDEN_REMOTE_ADDRESSES.addSubnet('10.0.0.0', 8, 'ipv4');
@@ -369,7 +370,7 @@ const defaultLookup = async (hostname) => {
369
370
  return { address: row.address, family: row.family };
370
371
  });
371
372
  };
372
- export async function resolveDesktopBridgeRemoteTarget(raw, lookup = defaultLookup) {
373
+ export async function resolveDesktopBridgeRemoteTarget(raw, lookup = defaultLookup, options = {}) {
373
374
  const remote = validateRemoteUrl(raw);
374
375
  const hostname = stripIpv6Brackets(remote.hostname);
375
376
  const family = net.isIP(hostname);
@@ -388,7 +389,9 @@ export async function resolveDesktopBridgeRemoteTarget(raw, lookup = defaultLook
388
389
  if (!loopback && addresses.some((row) => FORBIDDEN_REMOTE_ADDRESSES.check(row.address, row.family === 4 ? 'ipv4' : 'ipv6'))) {
389
390
  throw new DesktopBridgeError('bridge_remote_dns_private_address');
390
391
  }
391
- const selected = addresses[0];
392
+ const selected = (options.preferAddress ? addresses.find((row) => row.address === options.preferAddress) : undefined)
393
+ ?? addresses.find((row) => row.address !== options.avoidAddress)
394
+ ?? addresses[0];
392
395
  if (!selected)
393
396
  throw new DesktopBridgeError('bridge_remote_dns_empty');
394
397
  const target = {
@@ -396,8 +399,85 @@ export async function resolveDesktopBridgeRemoteTarget(raw, lookup = defaultLook
396
399
  port: Number(remote.port || (remote.protocol === 'https:' ? 443 : 80)), secure: remote.protocol === 'https:',
397
400
  address: selected.address, family: selected.family, ...(family ? {} : { tlsServername: hostname }),
398
401
  };
402
+ remoteRefreshStates.set(target, { resolvedAt: Date.now(), lastRefreshAt: null, inflight: null });
399
403
  return target;
400
404
  }
405
+ function deferredRemoteTarget(remote, hostname) {
406
+ return {
407
+ baseUrl: remote.toString().replace(/\/$/, ''), origin: remote.origin, hostname,
408
+ port: Number(remote.port || (remote.protocol === 'https:' ? 443 : 80)), secure: remote.protocol === 'https:',
409
+ address: '0.0.0.0', family: 4, ...(net.isIP(hostname) ? {} : { tlsServername: hostname }),
410
+ unresolved: true,
411
+ };
412
+ }
413
+ async function resolveOrDeferRemoteTarget(raw, lookup) {
414
+ try {
415
+ return await resolveDesktopBridgeRemoteTarget(raw, lookup);
416
+ }
417
+ catch (error) {
418
+ if (!(error instanceof DesktopBridgeError) || error.code !== 'bridge_remote_dns_failed')
419
+ throw error;
420
+ const remote = validateRemoteUrl(raw);
421
+ return deferredRemoteTarget(remote, stripIpv6Brackets(remote.hostname));
422
+ }
423
+ }
424
+ const UNREACHABLE_UPSTREAM_ERROR_CODES = new Set([
425
+ 'EHOSTUNREACH', 'ENETUNREACH', 'ENETDOWN', 'EHOSTDOWN', 'EADDRNOTAVAIL', 'ECONNREFUSED', 'ETIMEDOUT',
426
+ 'ERR_TLS_CERT_ALTNAME_INVALID',
427
+ ]);
428
+ export function isUnreachableUpstreamError(error) {
429
+ if (error instanceof DesktopBridgeError)
430
+ return error.code === 'bridge_upstream_connect_timeout';
431
+ const code = error?.code;
432
+ return typeof code === 'string' && UNREACHABLE_UPSTREAM_ERROR_CODES.has(code);
433
+ }
434
+ export const REMOTE_REFRESH_COOLDOWN_MS = 5_000;
435
+ export const REMOTE_TARGET_TTL_MS = 5 * 60_000;
436
+ export async function refreshDesktopBridgeRemoteTarget(remote, lookup = defaultLookup, reason = 'unreachable') {
437
+ if (net.isIP(remote.hostname))
438
+ return false;
439
+ const state = remoteRefreshStates.get(remote) ?? { resolvedAt: null, lastRefreshAt: null, inflight: null };
440
+ if (state.inflight)
441
+ return state.inflight;
442
+ if (state.lastRefreshAt !== null && Date.now() - state.lastRefreshAt < REMOTE_REFRESH_COOLDOWN_MS)
443
+ return false;
444
+ const inflight = (async () => {
445
+ let resolvedAt = state.resolvedAt;
446
+ try {
447
+ const fresh = await resolveDesktopBridgeRemoteTarget(remote.baseUrl, lookup, reason === 'stale'
448
+ ? { preferAddress: remote.address }
449
+ : reason === 'unreachable' ? { avoidAddress: remote.address } : {});
450
+ const changed = remote.unresolved === true || fresh.address !== remote.address || fresh.family !== remote.family;
451
+ remote.address = fresh.address;
452
+ remote.family = fresh.family;
453
+ delete remote.unresolved;
454
+ resolvedAt = Date.now();
455
+ return changed;
456
+ }
457
+ catch {
458
+ return false;
459
+ }
460
+ finally {
461
+ remoteRefreshStates.set(remote, { resolvedAt, lastRefreshAt: Date.now(), inflight: null });
462
+ }
463
+ })();
464
+ remoteRefreshStates.set(remote, { ...state, inflight });
465
+ return inflight;
466
+ }
467
+ export async function ensureDesktopBridgeRemoteTarget(remote, lookup = defaultLookup, ttlMs = REMOTE_TARGET_TTL_MS) {
468
+ if (remote.unresolved) {
469
+ await refreshDesktopBridgeRemoteTarget(remote, lookup, 'unresolved');
470
+ if (remote.unresolved)
471
+ throw new DesktopBridgeError('bridge_remote_dns_failed');
472
+ return;
473
+ }
474
+ if (net.isIP(remote.hostname))
475
+ return;
476
+ const state = remoteRefreshStates.get(remote);
477
+ if (state?.resolvedAt !== null && state?.resolvedAt !== undefined && Date.now() - state.resolvedAt >= ttlMs) {
478
+ await refreshDesktopBridgeRemoteTarget(remote, lookup, 'stale');
479
+ }
480
+ }
401
481
  async function prepareProvider(provider, lookup) {
402
482
  const remote = validateRemoteUrl(provider.base_url);
403
483
  const hostname = stripIpv6Brackets(remote.hostname);
@@ -414,7 +494,7 @@ async function prepareProvider(provider, lookup) {
414
494
  },
415
495
  };
416
496
  }
417
- const target = await resolveDesktopBridgeRemoteTarget(provider.base_url, lookup);
497
+ const target = await resolveOrDeferRemoteTarget(provider.base_url, lookup);
418
498
  return { ...provider, base_url: target.baseUrl, remote: target };
419
499
  }
420
500
  function assertRegistryAndPolicy(config, registry) {
@@ -525,9 +605,9 @@ export async function prepareDesktopBridgeConfig(config, lookup = defaultLookup)
525
605
  }));
526
606
  const providers = Object.fromEntries(entries);
527
607
  const officialRemote = config.officialPassthrough
528
- ? await resolveDesktopBridgeRemoteTarget(config.officialPassthrough.baseUrl, lookup)
608
+ ? await resolveOrDeferRemoteTarget(config.officialPassthrough.baseUrl, lookup)
529
609
  : null;
530
- return { ...config, providers, officialRemote };
610
+ return { ...config, providers, officialRemote, remoteLookup: lookup };
531
611
  }
532
612
  export function validatePreparedDesktopBridgeConfig(config) {
533
613
  validateDesktopBridgeConfig(config);
@@ -5,7 +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 { desktopBridgeOfficialPassthroughEnabled, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader, canonicalSessionId } from './security.js';
8
+ import { desktopBridgeOfficialPassthroughEnabled, ensureDesktopBridgeRemoteTarget, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader, canonicalSessionId } from './security.js';
9
9
  import { desktopBridgeListenOrigin } from './state.js';
10
10
  import { DESKTOP_BRIDGE_DIAGNOSTIC_PROTOCOL, DesktopBridgeError, } from './types.js';
11
11
  const MAX_HEAD = 64 * 1024;
@@ -152,71 +152,103 @@ export async function forwardWebSocket(req, client, head, config, authenticatedL
152
152
  writeUpgradeFailure(client, new DesktopBridgeError(official ? 'bridge_official_passthrough_unavailable' : 'bridge_provider_route_unavailable'), req);
153
153
  return;
154
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 });
155
+ const pin = remote;
156
+ const upstreamBaseUrl = official ? pin.baseUrl : provider.base_url;
157
+ const target = resolveDesktopBridgeTarget(req.url, pin);
158
+ try {
159
+ await ensureDesktopBridgeRemoteTarget(pin, config.remoteLookup);
160
+ }
161
+ catch (error) {
162
+ writeUpgradeFailure(client, error, req);
163
+ return;
164
+ }
160
165
  const key = String(req.headers['sec-websocket-key'] || '');
161
166
  const requestedProtocol = String(req.headers['sec-websocket-protocol'] || '').split(',')[0]?.trim() || null;
162
- let connected = false;
163
- let response = Buffer.alloc(0);
164
- const timer = setTimeout(() => upstream.destroy(new DesktopBridgeError('bridge_upstream_connect_timeout')), config.connectTimeoutMs);
165
- timer.unref();
166
- const fail = (error) => { clearTimeout(timer); if (!connected)
167
- writeUpgradeFailure(client, error, req);
168
- else
169
- client.destroy(); };
170
- const onConnected = () => {
171
- connected = true;
172
- clearTimeout(timer);
173
- upstream.setNoDelay(true);
174
- upstream.setKeepAlive(true, 30_000);
175
- const clientSocket = client;
176
- if (typeof clientSocket.setKeepAlive === 'function')
177
- clientSocket.setKeepAlive(true, 30_000);
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);
181
- upstream.write([`${req.method || 'GET'} ${target.pathname}${target.search} HTTP/1.1`, ...serializeHeaders(headers), '', ''].join('\r\n'));
182
- if (head.length)
183
- upstream.write(head);
184
- const onData = (chunk) => {
185
- response = Buffer.concat([response, chunk]);
186
- if (response.length > MAX_HEAD) {
187
- upstream.destroy(new DesktopBridgeError('bridge_websocket_response_headers_too_large'));
188
- return;
189
- }
190
- const boundary = response.indexOf('\r\n\r\n');
191
- if (boundary < 0)
192
- return;
193
- upstream.off('data', onData);
194
- const raw = response.subarray(0, boundary);
195
- const remaining = response.subarray(boundary + 4);
196
- try {
197
- validateUpgrade(raw, key, requestedProtocol);
198
- client.write(rewriteUpgradeResponseHead(raw, upstreamBaseUrl, authenticatedLocalBaseUrl));
199
- if (remaining.length)
200
- client.write(remaining);
201
- }
202
- catch (error) {
203
- upstream.destroy(error instanceof Error ? error : undefined);
204
- return;
205
- }
206
- client.pipe(upstream);
207
- upstream.pipe(client);
167
+ connectUpstreamForUpgrade(1);
168
+ function connectUpstreamForUpgrade(retriesLeft) {
169
+ const upstream = pin.secure
170
+ ? tls.connect({ host: pin.address, port: pin.port, ...(pin.tlsServername ? { servername: pin.tlsServername } : {}) })
171
+ : net.connect({ host: pin.address, port: pin.port, family: pin.family });
172
+ let connected = false;
173
+ let retrying = false;
174
+ let response = Buffer.alloc(0);
175
+ const timer = setTimeout(() => upstream.destroy(new DesktopBridgeError('bridge_upstream_connect_timeout')), config.connectTimeoutMs);
176
+ timer.unref();
177
+ const fail = (error) => { clearTimeout(timer); if (!connected)
178
+ writeUpgradeFailure(client, error, req);
179
+ else
180
+ client.destroy(); };
181
+ const onConnected = () => {
182
+ connected = true;
183
+ clearTimeout(timer);
184
+ upstream.setNoDelay(true);
185
+ upstream.setKeepAlive(true, 30_000);
186
+ const clientSocket = client;
187
+ if (typeof clientSocket.setKeepAlive === 'function')
188
+ clientSocket.setKeepAlive(true, 30_000);
189
+ const headers = official
190
+ ? buildOfficialPassthroughWebSocketHeaders(req.headers, target.host)
191
+ : buildProviderWebSocketHeaders(req.headers, { providerId: provider.provider_id, authTransport: provider.auth_transport, credential: credential }, target.host);
192
+ upstream.write([`${req.method || 'GET'} ${target.pathname}${target.search} HTTP/1.1`, ...serializeHeaders(headers), '', ''].join('\r\n'));
193
+ if (head.length)
194
+ upstream.write(head);
195
+ const onData = (chunk) => {
196
+ response = Buffer.concat([response, chunk]);
197
+ if (response.length > MAX_HEAD) {
198
+ upstream.destroy(new DesktopBridgeError('bridge_websocket_response_headers_too_large'));
199
+ return;
200
+ }
201
+ const boundary = response.indexOf('\r\n\r\n');
202
+ if (boundary < 0)
203
+ return;
204
+ upstream.off('data', onData);
205
+ const raw = response.subarray(0, boundary);
206
+ const remaining = response.subarray(boundary + 4);
207
+ try {
208
+ validateUpgrade(raw, key, requestedProtocol);
209
+ client.write(rewriteUpgradeResponseHead(raw, upstreamBaseUrl, authenticatedLocalBaseUrl));
210
+ if (remaining.length)
211
+ client.write(remaining);
212
+ }
213
+ catch (error) {
214
+ upstream.destroy(error instanceof Error ? error : undefined);
215
+ return;
216
+ }
217
+ client.pipe(upstream);
218
+ upstream.pipe(client);
219
+ };
220
+ upstream.on('data', onData);
208
221
  };
209
- upstream.on('data', onData);
210
- };
211
- if (remote.secure)
212
- upstream.once('secureConnect', onConnected);
213
- else
214
- upstream.once('connect', onConnected);
215
- upstream.once('error', (error) => fail(error));
216
- upstream.once('close', () => { clearTimeout(timer); if (!client.destroyed)
217
- client.end(); });
218
- client.once('error', () => upstream.destroy());
219
- client.once('close', () => upstream.destroy());
222
+ if (pin.secure)
223
+ upstream.once('secureConnect', onConnected);
224
+ else
225
+ upstream.once('connect', onConnected);
226
+ upstream.once('error', (error) => {
227
+ if (!connected && isUnreachableUpstreamError(error)) {
228
+ const refreshed = refreshDesktopBridgeRemoteTarget(pin, config.remoteLookup);
229
+ if (retriesLeft > 0 && !client.destroyed) {
230
+ retrying = true;
231
+ clearTimeout(timer);
232
+ const cause = error?.code;
233
+ logWebSocketRejection({
234
+ code: `bridge_upstream_unreachable_rerouted:${typeof cause === 'string' && /^[A-Za-z0-9_]{1,64}$/.test(cause) ? cause : 'unknown'}`,
235
+ transport: 'websocket',
236
+ ...(req.method === undefined ? {} : { method: req.method }),
237
+ ...(req.url === undefined ? {} : { url: req.url }),
238
+ });
239
+ void refreshed.then(() => { if (!client.destroyed)
240
+ connectUpstreamForUpgrade(retriesLeft - 1); });
241
+ return;
242
+ }
243
+ }
244
+ fail(error);
245
+ });
246
+ upstream.once('close', () => { clearTimeout(timer); if (retrying)
247
+ return; if (!client.destroyed)
248
+ client.end(); });
249
+ client.once('error', () => upstream.destroy());
250
+ client.once('close', () => upstream.destroy());
251
+ }
220
252
  }
221
253
  function encodeClientFrame(opcode, payload) {
222
254
  if (payload.length > 125)
@@ -14,6 +14,7 @@ import { captureCodexAuthSnapshot } from './desktop-auth-invariant.js';
14
14
  import { applyOfficialModelPassthrough, bridgeRoutePolicyPath, writeBridgeRoutingPolicy } from './provider-route-policy.js';
15
15
  import { desktopBridgeRuntimeVersion, desktopBridgeRuntimeVersionStale } from './desktop-bridge/state.js';
16
16
  import { PACKAGE_VERSION } from '../version.js';
17
+ import { BRIDGE_OFFICIAL_ROUTE_ID } from './bridge-contracts.js';
17
18
  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';
18
19
  export const DEFAULT_DESKTOP_BRIDGE_HOST = '127.0.0.1';
19
20
  export const DEFAULT_DESKTOP_BRIDGE_PORT = 49_152;
@@ -465,17 +466,15 @@ async function autoApplyOfficialModelsAtServe(runtime, home, options) {
465
466
  home,
466
467
  codexLbRegistered: desktopBridgeCodexLbRegistered(settings.provider_registry.providers),
467
468
  });
468
- if (mode !== 'passthrough')
469
+ const converged = applyOfficialModelPassthrough(settings.route_policy, { mode });
470
+ if (converged.policy_generation === settings.route_policy.policy_generation)
469
471
  return;
470
- const flipped = applyOfficialModelPassthrough(settings.route_policy, { mode: 'passthrough' });
471
- if (flipped.policy_generation === settings.route_policy.policy_generation)
472
- return;
473
- const nextSettings = { ...settings, route_policy: flipped };
472
+ const nextSettings = { ...settings, route_policy: converged };
474
473
  await writeDesktopBridgeServiceSettings(options.settingsPath || desktopBridgeServicePaths(home).settings_path, nextSettings);
475
- await writeBridgeRoutingPolicy(bridgeRoutePolicyPath(path.join(path.resolve(home), '.codex')), flipped).catch(() => undefined);
476
- settings.route_policy = flipped;
477
- runtime.config.routePolicy = flipped;
478
- process.stdout.write(`${JSON.stringify({ schema: 'sks.desktop-bridge-log.v2', event: 'sks.desktop_bridge.official_models_auto_applied', at: new Date().toISOString(), sks_version: PACKAGE_VERSION, mode: 'passthrough', policy_generation: flipped.policy_generation, secret_fields_redacted: true })}\n`);
474
+ await writeBridgeRoutingPolicy(bridgeRoutePolicyPath(path.join(path.resolve(home), '.codex')), converged).catch(() => undefined);
475
+ settings.route_policy = converged;
476
+ runtime.config.routePolicy = converged;
477
+ 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, policy_generation: converged.policy_generation, secret_fields_redacted: true })}\n`);
479
478
  }
480
479
  export async function serveDesktopBridge(options = {}) {
481
480
  let handle = null;
@@ -485,7 +484,12 @@ export async function serveDesktopBridge(options = {}) {
485
484
  await autoApplyOfficialModelsAtServe(runtime, serveHome, options);
486
485
  const supervised = desktopBridgeIsSupervised();
487
486
  const skewMarkerPath = path.join(path.dirname(runtime.paths.state_path), 'desktop-bridge-skew-restart.json');
488
- handle = await startPreparedDesktopBridge(await preflightDesktopBridge(runtime.config), {
487
+ const prepared = await preflightDesktopBridge(runtime.config);
488
+ const deferredUpstreams = [
489
+ ...Object.values(prepared.providers).filter((provider) => provider.enabled && provider.remote.unresolved).map((provider) => provider.provider_id),
490
+ ...(prepared.officialRemote?.unresolved ? [BRIDGE_OFFICIAL_ROUTE_ID] : []),
491
+ ];
492
+ handle = await startPreparedDesktopBridge(prepared, {
489
493
  statePath: runtime.paths.state_path,
490
494
  versionSkew: {
491
495
  readInstalledVersion: installedPackageVersion,
@@ -504,7 +508,7 @@ export async function serveDesktopBridge(options = {}) {
504
508
  },
505
509
  },
506
510
  });
507
- 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`);
511
+ 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, ...(deferredUpstreams.length ? { deferred_upstreams: deferredUpstreams } : {}), secret_fields_redacted: true })}\n`);
508
512
  await waitForShutdown(handle);
509
513
  return { schema: 'sks.desktop-bridge-serve.v1', ok: true, status: 'stopped', state: handle.state };
510
514
  }
@@ -118,14 +118,18 @@ function isRouteTargetId(value) {
118
118
  }
119
119
  export const OFFICIAL_MODEL_ID_PATTERN = /^(?:gpt-[0-9]|o[0-9]|codex-mini)/;
120
120
  export function applyOfficialModelPassthrough(policy, input = { mode: 'passthrough' }) {
121
- if (input.mode === 'gateway')
122
- return policy;
123
121
  const routes = {};
122
+ let changed = false;
124
123
  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;
124
+ const next = input.mode === 'passthrough'
125
+ ? passthroughRouteTarget(model, target)
126
+ : gatewayRouteTarget(model, target, policy);
127
+ routes[model] = next;
128
+ if (next !== target)
129
+ changed = true;
128
130
  }
131
+ if (!changed)
132
+ return policy;
129
133
  const semantic = {
130
134
  default_provider_id: policy.default_provider_id,
131
135
  fallback: 'none',
@@ -139,6 +143,23 @@ export function applyOfficialModelPassthrough(policy, input = { mode: 'passthrou
139
143
  changed_at: input.changedAt || new Date().toISOString()
140
144
  };
141
145
  }
146
+ function passthroughRouteTarget(model, target) {
147
+ if (!OFFICIAL_MODEL_ID_PATTERN.test(model) || model.includes(':'))
148
+ return target;
149
+ if (target.provider_id === BRIDGE_OFFICIAL_ROUTE_ID && target.upstream_model === model)
150
+ return target;
151
+ return { provider_id: BRIDGE_OFFICIAL_ROUTE_ID, upstream_model: model };
152
+ }
153
+ function gatewayRouteTarget(model, target, policy) {
154
+ if (target.provider_id !== BRIDGE_OFFICIAL_ROUTE_ID)
155
+ return target;
156
+ if (!OFFICIAL_MODEL_ID_PATTERN.test(model) || model.includes(':'))
157
+ return target;
158
+ const twin = policy.model_routes[`codex-lb:${model}`];
159
+ if (!twin || twin.provider_id !== 'codex-lb')
160
+ return target;
161
+ return { provider_id: twin.provider_id, upstream_model: twin.upstream_model };
162
+ }
142
163
  function sameTarget(left, right) {
143
164
  return left.provider_id === right.provider_id && left.upstream_model === right.upstream_model;
144
165
  }
@@ -1,30 +1,110 @@
1
+ import fsp from 'node:fs/promises';
1
2
  import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { desktopBridgeStatusV3, executeDesktopBridgeCommandV3 } from '../codex-lb/desktop-controller-v3.js';
4
- import { bootstrapExistingDesktopBridgeService, desktopBridgeServiceStatus } from '../codex-lb/desktop-service.js';
5
- import { desktopBridgeRuntimeVersion, desktopBridgeRuntimeVersionStale } from '../codex-lb/desktop-bridge/state.js';
5
+ import { bootstrapExistingDesktopBridgeService, desktopBridgeServicePaths, desktopBridgeServiceStatus } from '../codex-lb/desktop-service.js';
6
+ import { desktopBridgeRuntimeVersion, desktopBridgeRuntimeVersionStale, readDesktopBridgeState } from '../codex-lb/desktop-bridge/state.js';
6
7
  import { PACKAGE_VERSION } from '../version.js';
7
- export async function restartStaleDesktopBridgeRuntime(input) {
8
- if (process.env.SKS_TEST_ISOLATION === '1' || process.env.SKS_RELEASE_UPGRADE_SMOKE === '1') {
8
+ export const BRIDGE_UNREACHABLE_EVIDENCE_WINDOW_MS = 10 * 60_000;
9
+ const BRIDGE_LOG_TAIL_BYTES = 256 * 1024;
10
+ const UNREACHABLE_REJECTION_CODE = /^bridge_(?:websocket_)?upstream_unavailable/;
11
+ const REROUTED_REJECTION_CODE = /^bridge_upstream_unreachable_rerouted/;
12
+ export function detectUnreachableUpstreamEvidence(logTail, startedAt, nowMs) {
13
+ const startedMs = startedAt ? Date.parse(startedAt) : Number.NaN;
14
+ const cutoffMs = Math.max(Number.isFinite(startedMs) ? startedMs : 0, nowMs - BRIDGE_UNREACHABLE_EVIDENCE_WINDOW_MS);
15
+ let latest = null;
16
+ let latestReroute = Number.NEGATIVE_INFINITY;
17
+ for (const line of logTail.split('\n')) {
18
+ const trimmed = line.trim();
19
+ if (!trimmed.startsWith('{') || !trimmed.includes('"sks.desktop_bridge.rejected'))
20
+ continue;
21
+ let record;
22
+ try {
23
+ record = JSON.parse(trimmed);
24
+ }
25
+ catch {
26
+ continue;
27
+ }
28
+ if (record.event !== 'sks.desktop_bridge.rejected' && record.event !== 'sks.desktop_bridge.rejected_summary')
29
+ continue;
30
+ const code = typeof record.code === 'string' ? record.code : '';
31
+ const unreachable = UNREACHABLE_REJECTION_CODE.test(code);
32
+ const rerouted = !unreachable && REROUTED_REJECTION_CODE.test(code);
33
+ if (!unreachable && !rerouted)
34
+ continue;
35
+ const at = typeof record.at === 'string' ? Date.parse(record.at) : Number.NaN;
36
+ if (!Number.isFinite(at) || at < cutoffMs)
37
+ continue;
38
+ if (rerouted) {
39
+ latestReroute = Math.max(latestReroute, at);
40
+ continue;
41
+ }
42
+ if (!latest || at > latest.at)
43
+ latest = { at, code };
44
+ }
45
+ if (!latest || latest.at <= latestReroute)
46
+ return null;
47
+ return latest.code;
48
+ }
49
+ export async function readDesktopBridgeUnreachableUpstreamEvidence(home, deps = {}) {
50
+ const paths = desktopBridgeServicePaths(home);
51
+ const state = await readDesktopBridgeState(paths.state_path).catch(() => null);
52
+ if (!state)
53
+ return null;
54
+ const tail = await (deps.readLogTailImpl || readBridgeLogTail)(paths.stdout_log_path, BRIDGE_LOG_TAIL_BYTES).catch(() => '');
55
+ const code = detectUnreachableUpstreamEvidence(tail, state.started_at, (deps.nowMs || Date.now)());
56
+ return code ? { code, started_at: state.started_at } : null;
57
+ }
58
+ async function readBridgeLogTail(logPath, maxBytes) {
59
+ const handle = await fsp.open(logPath, 'r');
60
+ try {
61
+ const { size } = await handle.stat();
62
+ const start = Math.max(0, size - maxBytes);
63
+ const buffer = Buffer.alloc(size - start);
64
+ await handle.read(buffer, 0, buffer.length, start);
65
+ return buffer.toString('utf8');
66
+ }
67
+ finally {
68
+ await handle.close();
69
+ }
70
+ }
71
+ export async function restartStaleDesktopBridgeRuntime(input, deps = {}) {
72
+ const usingRealImpls = !deps.serviceStatusImpl && !deps.bootstrapImpl && !deps.readLogTailImpl;
73
+ if (usingRealImpls && (process.env.SKS_TEST_ISOLATION === '1' || process.env.SKS_RELEASE_UPGRADE_SMOKE === '1')) {
9
74
  return { restarted: false, warnings: [], blockers: [] };
10
75
  }
11
- const service = await desktopBridgeServiceStatus({ home: input.home }).catch(() => null);
12
- if (!service?.running || !desktopBridgeRuntimeVersionStale(service.state)) {
76
+ const service = await (deps.serviceStatusImpl || desktopBridgeServiceStatus)({ home: input.home }).catch(() => null);
77
+ if (!service?.running)
13
78
  return { restarted: false, warnings: [], blockers: [] };
79
+ const versionStale = desktopBridgeRuntimeVersionStale(service.state);
80
+ let unreachable = null;
81
+ if (!versionStale) {
82
+ const logPath = service.paths?.stdout_log_path;
83
+ const tail = logPath
84
+ ? await (deps.readLogTailImpl || readBridgeLogTail)(logPath, BRIDGE_LOG_TAIL_BYTES).catch(() => '')
85
+ : '';
86
+ unreachable = detectUnreachableUpstreamEvidence(tail, service.state?.started_at, (deps.nowMs || Date.now)());
87
+ if (!unreachable)
88
+ return { restarted: false, warnings: [], blockers: [] };
14
89
  }
15
90
  const running = desktopBridgeRuntimeVersion(service.state) || 'pre-8.6.2';
91
+ const blocker = versionStale
92
+ ? `desktop_bridge_runtime_version_stale:${running}:${PACKAGE_VERSION}`
93
+ : `desktop_bridge_upstream_unreachable:${unreachable}`;
16
94
  if (!input.fix) {
17
- return {
18
- restarted: false,
19
- warnings: [],
20
- blockers: [`desktop_bridge_runtime_version_stale:${running}:${PACKAGE_VERSION}`]
21
- };
95
+ return { restarted: false, warnings: [], blockers: [blocker] };
22
96
  }
23
- const restarted = await bootstrapExistingDesktopBridgeService({ home: input.home }).catch(() => null);
97
+ const restarted = await (deps.bootstrapImpl || bootstrapExistingDesktopBridgeService)({ home: input.home }).catch(() => null);
24
98
  const nowRunning = restarted?.running === true && !desktopBridgeRuntimeVersionStale(restarted.state);
25
- return nowRunning
26
- ? { restarted: true, warnings: [`desktop_bridge_runtime_restarted:${running}:${PACKAGE_VERSION}`], blockers: [] }
27
- : { restarted: false, warnings: [], blockers: [`desktop_bridge_runtime_version_stale:${running}:${PACKAGE_VERSION}`] };
99
+ if (!nowRunning)
100
+ return { restarted: false, warnings: [], blockers: [blocker] };
101
+ return {
102
+ restarted: true,
103
+ warnings: [versionStale
104
+ ? `desktop_bridge_runtime_restarted:${running}:${PACKAGE_VERSION}`
105
+ : `desktop_bridge_upstream_unreachable_restarted:${unreachable}`],
106
+ blockers: []
107
+ };
28
108
  }
29
109
  export const CATALOG_REPAIR_MAX_ATTEMPTS = 2;
30
110
  export async function repairDoctorDesktopBridgeCatalog(input, deps = {}) {
@@ -0,0 +1,185 @@
1
+ import path from 'node:path';
2
+ import fsp from 'node:fs/promises';
3
+ import { readJson } from '../fsx.js';
4
+ import { inspectConfinedPath, publicPathError, removeManagedPathVerified } from '../managed-path-safety.js';
5
+ export const LEGACY_RUNTIME_DATA_GC_SCHEMA = 'sks.legacy-runtime-data-gc.v1';
6
+ const CONFIG_BACKUP_NAME = /^config\.toml\.(?:backup-[A-Za-z0-9_.:-]+|bak(?:[-.][A-Za-z0-9_.-]+)?|[A-Za-z0-9_.-]+\.bak)$/;
7
+ const BRIDGE_GENERATION_DIR = /^[0-9a-f]{64}\.[0-9a-f]{64}\.[0-9a-f]{64}$/;
8
+ const RETIRED_VERSION_CACHE = /^codex-0\d{2,3}-(?:capability|doctor)\.json$/;
9
+ const RETIRED_CHROME_HOSTS_V1 = 'chrome-native-hosts.json';
10
+ const CHROME_HOSTS_V2 = 'chrome-native-hosts-v2.json';
11
+ export const LEGACY_CONFIG_BACKUP_KEEP_COUNT = 3;
12
+ export const LEGACY_BRIDGE_GENERATION_KEEP_COUNT = 1;
13
+ export async function reconcileLegacyRuntimeData(input) {
14
+ const codexHome = path.resolve(input.codexHome);
15
+ const configBackups = await reconcileConfigBackups(codexHome, input.fix);
16
+ const bridgeGenerations = await reconcileBridgeGenerations(codexHome, input.fix);
17
+ const retiredVersionCaches = await reconcileRetiredVersionCaches(input.stateRoots, input.fix);
18
+ const retiredSingletons = await reconcileRetiredSingletons(codexHome, input.fix);
19
+ const categories = [configBackups, bridgeGenerations, retiredVersionCaches, retiredSingletons];
20
+ const remainingCount = categories.reduce((total, category) => total + category.remaining, 0);
21
+ const errorCount = categories.reduce((total, category) => total + category.errors.length, 0);
22
+ return {
23
+ schema: LEGACY_RUNTIME_DATA_GC_SCHEMA,
24
+ ok: remainingCount === 0 && errorCount === 0,
25
+ fix: input.fix,
26
+ config_backups: configBackups,
27
+ bridge_generations: bridgeGenerations,
28
+ retired_version_caches: retiredVersionCaches,
29
+ retired_singletons: retiredSingletons,
30
+ remaining_count: remainingCount,
31
+ error_count: errorCount
32
+ };
33
+ }
34
+ function emptyCategory() {
35
+ return { detected: 0, removed: 0, kept: 0, remaining: 0, errors: [] };
36
+ }
37
+ async function reconcileConfigBackups(codexHome, fix) {
38
+ const report = emptyCategory();
39
+ const names = await readdirOrNull(codexHome);
40
+ if (!names)
41
+ return report;
42
+ const candidates = [];
43
+ for (const name of names) {
44
+ if (!CONFIG_BACKUP_NAME.test(name))
45
+ continue;
46
+ const inspected = await inspectOwnedRegularFile(codexHome, path.join(codexHome, name), report);
47
+ if (!inspected)
48
+ continue;
49
+ report.detected += 1;
50
+ candidates.push({ file: inspected.path, mtimeMs: inspected.stat?.mtimeMs || 0 });
51
+ }
52
+ candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
53
+ report.kept = Math.min(candidates.length, LEGACY_CONFIG_BACKUP_KEEP_COUNT);
54
+ for (const candidate of candidates.slice(LEGACY_CONFIG_BACKUP_KEEP_COUNT)) {
55
+ await removeOrCount(codexHome, candidate.file, fix, report);
56
+ }
57
+ return report;
58
+ }
59
+ async function reconcileBridgeGenerations(codexHome, fix) {
60
+ const report = emptyCategory();
61
+ const runtimeRoot = path.join(codexHome, 'sks');
62
+ const generationsRoot = path.join(runtimeRoot, '.sks-bridge-generations');
63
+ const names = await readdirOrNull(generationsRoot);
64
+ if (!names)
65
+ return report;
66
+ const pointer = await readJson(path.join(runtimeRoot, 'sks-bridge-active-generation.json'), null);
67
+ const activeName = typeof pointer?.bundle_directory === 'string'
68
+ ? path.basename(pointer.bundle_directory)
69
+ : null;
70
+ const candidates = [];
71
+ for (const name of names) {
72
+ if (!BRIDGE_GENERATION_DIR.test(name))
73
+ continue;
74
+ const target = path.join(generationsRoot, name);
75
+ let inspected;
76
+ try {
77
+ inspected = await inspectConfinedPath(generationsRoot, target);
78
+ }
79
+ catch (error) {
80
+ report.errors.push(publicPathError(error, target));
81
+ continue;
82
+ }
83
+ if (!inspected.exists || inspected.leafSymlink || !inspected.stat?.isDirectory() || !ownedByCurrentUid(inspected))
84
+ continue;
85
+ report.detected += 1;
86
+ if (name === activeName) {
87
+ report.kept += 1;
88
+ continue;
89
+ }
90
+ candidates.push({ file: target, name, mtimeMs: inspected.stat.mtimeMs });
91
+ }
92
+ if (!activeName || !BRIDGE_GENERATION_DIR.test(activeName)) {
93
+ report.kept += candidates.length;
94
+ return report;
95
+ }
96
+ candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
97
+ report.kept += Math.min(candidates.length, LEGACY_BRIDGE_GENERATION_KEEP_COUNT);
98
+ for (const candidate of candidates.slice(LEGACY_BRIDGE_GENERATION_KEEP_COUNT)) {
99
+ await removeOrCount(generationsRoot, candidate.file, fix, report);
100
+ }
101
+ return report;
102
+ }
103
+ async function reconcileRetiredVersionCaches(stateRoots, fix) {
104
+ const report = emptyCategory();
105
+ for (const root of [...new Set(stateRoots.map((value) => path.resolve(value)))]) {
106
+ const names = await readdirOrNull(root);
107
+ if (!names)
108
+ continue;
109
+ for (const name of names) {
110
+ if (!RETIRED_VERSION_CACHE.test(name))
111
+ continue;
112
+ const inspected = await inspectOwnedRegularFile(root, path.join(root, name), report);
113
+ if (!inspected)
114
+ continue;
115
+ report.detected += 1;
116
+ await removeOrCount(root, inspected.path, fix, report);
117
+ }
118
+ }
119
+ return report;
120
+ }
121
+ async function reconcileRetiredSingletons(codexHome, fix) {
122
+ const report = emptyCategory();
123
+ const v1 = path.join(codexHome, RETIRED_CHROME_HOSTS_V1);
124
+ const v2 = path.join(codexHome, CHROME_HOSTS_V2);
125
+ const v1Inspected = await inspectOwnedRegularFile(codexHome, v1, report, { quiet: true });
126
+ if (!v1Inspected)
127
+ return report;
128
+ const v2Inspected = await inspectOwnedRegularFile(codexHome, v2, report, { quiet: true });
129
+ report.detected += 1;
130
+ if (!v2Inspected) {
131
+ report.kept += 1;
132
+ return report;
133
+ }
134
+ await removeOrCount(codexHome, v1, fix, report);
135
+ return report;
136
+ }
137
+ async function removeOrCount(boundary, file, fix, report) {
138
+ if (!fix) {
139
+ report.remaining += 1;
140
+ return;
141
+ }
142
+ try {
143
+ await removeManagedPathVerified(boundary, file);
144
+ report.removed += 1;
145
+ }
146
+ catch (error) {
147
+ report.errors.push(publicPathError(error, file));
148
+ report.remaining += 1;
149
+ }
150
+ }
151
+ async function inspectOwnedRegularFile(boundary, file, report, options = {}) {
152
+ let inspected;
153
+ try {
154
+ inspected = await inspectConfinedPath(boundary, file);
155
+ }
156
+ catch (error) {
157
+ if (!options.quiet)
158
+ report.errors.push(publicPathError(error, file));
159
+ return null;
160
+ }
161
+ if (!inspected.exists || inspected.leafSymlink || !inspected.stat?.isFile() || !ownedByCurrentUid(inspected))
162
+ return null;
163
+ return inspected;
164
+ }
165
+ function ownedByCurrentUid(inspected) {
166
+ if (typeof process.getuid !== 'function')
167
+ return true;
168
+ return inspected.stat?.uid === process.getuid();
169
+ }
170
+ async function readdirOrNull(directory) {
171
+ try {
172
+ const stat = await fsp.lstat(directory);
173
+ if (!stat.isDirectory() || stat.isSymbolicLink())
174
+ return null;
175
+ }
176
+ catch {
177
+ return null;
178
+ }
179
+ try {
180
+ return (await fsp.readdir(directory)).sort();
181
+ }
182
+ catch {
183
+ return null;
184
+ }
185
+ }
@@ -6,6 +6,7 @@ import { hasExplicitSksManagedCodexConfigMarker, writeCodexConfigGuarded } from
6
6
  import { validateCodexConfigRoundTrip } from '../codex/codex-config-toml.js';
7
7
  import { collectNestedProjectRoots } from '../doctor/current-project-guidance-nested.js';
8
8
  import { reconcileRetiredManagedResidue } from '../doctor/retired-managed-residue.js';
9
+ import { reconcileLegacyRuntimeData } from '../doctor/legacy-runtime-data-gc.js';
9
10
  import { readJson, readText } from '../fsx.js';
10
11
  import { removeMcpServerBlock, mcpServerBlockWithChildren } from '../mcp/mcp-config-preservation.js';
11
12
  import { reconcileRetiredSksConfigText } from '../auto-review.js';
@@ -54,6 +55,11 @@ export async function reconcileLegacyManagedGeneration(input) {
54
55
  for (const runtimeRoot of runtimeRoots) {
55
56
  retiredRuntimeScopes.push(await reconcileRuntimeScope(runtimeRoot, input.fix));
56
57
  }
58
+ const runtimeDataGc = await reconcileLegacyRuntimeData({
59
+ codexHome,
60
+ stateRoots: runtimeRoots.map((runtimeRoot) => path.join(runtimeRoot, '.sneakoscope')),
61
+ fix: input.fix
62
+ });
57
63
  const managedConfigs = await reconcileManagedConfigs({
58
64
  projectRoots,
59
65
  home,
@@ -68,6 +74,7 @@ export async function reconcileLegacyManagedGeneration(input) {
68
74
  ok: skillsOk
69
75
  && retiredAgentRoles.ok
70
76
  && runtimeOk
77
+ && runtimeDataGc.ok
71
78
  && managedConfigs.ok
72
79
  && blockers.length === 0
73
80
  && nested.errorCount === 0,
@@ -79,6 +86,7 @@ export async function reconcileLegacyManagedGeneration(input) {
79
86
  project_skills: projectSkills,
80
87
  retired_agent_roles: retiredAgentRoles,
81
88
  retired_runtime_scopes: retiredRuntimeScopes,
89
+ runtime_data_gc: runtimeDataGc,
82
90
  managed_configs: managedConfigs,
83
91
  blockers,
84
92
  warnings
@@ -133,6 +141,7 @@ async function reconcileManagedConfigs(input) {
133
141
  rewritten_count: 0,
134
142
  retired_mcp_block_count: 0,
135
143
  retired_config_entry_count: 0,
144
+ compacted_marker_line_count: 0,
136
145
  preserved_user_config_count: 0,
137
146
  remaining_count: 0,
138
147
  error_count: 0,
@@ -179,7 +188,8 @@ async function reconcileManagedConfigTarget(target, fix, report) {
179
188
  const before = await readText(target.configPath, '');
180
189
  const ownership = await managedConfigOwnershipProof(target.ownerRoot, target.configPath, before);
181
190
  const retiredConfig = reconcileRetiredSksConfigText(before);
182
- let next = retiredConfig.user_authored_conflict && !ownership ? before : retiredConfig.text;
191
+ const retiredApplied = !(retiredConfig.user_authored_conflict && !ownership);
192
+ let next = retiredApplied ? retiredConfig.text : before;
183
193
  let retiredMcpCount = 0;
184
194
  if (ownership) {
185
195
  for (const server of RETIRED_SKS_MCP_SERVERS) {
@@ -189,8 +199,11 @@ async function reconcileManagedConfigTarget(target, fix, report) {
189
199
  retiredMcpCount += 1;
190
200
  }
191
201
  }
192
- const retiredConfigCount = next === before ? 0 : retiredConfig.detected_count;
193
- const detectedCount = retiredConfigCount + retiredMcpCount;
202
+ const compacted = compactSksMovedConfigMarkers(next);
203
+ const markerCount = compacted.removed_count;
204
+ next = compacted.text;
205
+ const retiredConfigCount = retiredApplied && retiredConfig.detected_count > 0 ? retiredConfig.detected_count : 0;
206
+ const detectedCount = retiredConfigCount + retiredMcpCount + markerCount;
194
207
  if (retiredConfig.user_authored_conflict && !ownership) {
195
208
  report.preserved_user_config_count += 1;
196
209
  report.preserved_user_configs.push(target.configPath);
@@ -200,6 +213,7 @@ async function reconcileManagedConfigTarget(target, fix, report) {
200
213
  report.detected_count += detectedCount;
201
214
  report.retired_config_entry_count += retiredConfigCount;
202
215
  report.retired_mcp_block_count += retiredMcpCount;
216
+ report.compacted_marker_line_count += markerCount;
203
217
  if (!fix) {
204
218
  report.remaining_count += detectedCount;
205
219
  return;
@@ -211,8 +225,9 @@ async function reconcileManagedConfigTarget(target, fix, report) {
211
225
  }
212
226
  try {
213
227
  const normalized = normalizeConfigText(next);
214
- const exactRetiredConfigAuthorized = retiredConfig.detected_count > 0
215
- && retiredConfig.user_authored_conflict !== true;
228
+ const exactRetiredConfigAuthorized = (retiredConfig.detected_count > 0
229
+ && retiredConfig.user_authored_conflict !== true)
230
+ || markerCount > 0;
216
231
  const guarded = await writeCodexConfigGuarded({
217
232
  root: target.ownerRoot,
218
233
  configPath: target.configPath,
@@ -254,6 +269,16 @@ function normalizeConfigText(text) {
254
269
  const value = String(text || '').trimEnd().replace(/\n{3,}/g, '\n\n');
255
270
  return value ? `${value}\n` : '';
256
271
  }
272
+ const SKS_MOVED_MARKER_LINE = /^\s*#\s*SKS moved machine-local Codex config\b/i;
273
+ function compactSksMovedConfigMarkers(text) {
274
+ const lines = String(text || '').split('\n');
275
+ const markerIndexes = lines.flatMap((line, index) => (SKS_MOVED_MARKER_LINE.test(line) ? [index] : []));
276
+ if (markerIndexes.length < 2)
277
+ return { text, removed_count: 0 };
278
+ const keep = markerIndexes[markerIndexes.length - 1];
279
+ const kept = lines.filter((line, index) => !SKS_MOVED_MARKER_LINE.test(line) || index === keep);
280
+ return { text: kept.join('\n'), removed_count: markerIndexes.length - 1 };
281
+ }
257
282
  function recordConfigError(report, file, error) {
258
283
  report.error_count += 1;
259
284
  report.errors.push(`${file}:${error instanceof Error ? error.message : String(error)}`);
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = '9.2.4';
1
+ export const PACKAGE_VERSION = '9.2.6';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "9.2.4",
4
+ "version": "9.2.6",
5
5
  "description": "Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
@@ -200,7 +200,7 @@
200
200
  "dependencies": {
201
201
  "@modelcontextprotocol/client": "2.0.0",
202
202
  "@modelcontextprotocol/server": "2.0.0",
203
- "@openai/codex-sdk": "0.147.0",
203
+ "@openai/codex-sdk": "0.150.1",
204
204
  "smol-toml": "^1.7.0",
205
205
  "typescript": "^5.9.3"
206
206
  },