sneakoscope 9.2.5 → 10.0.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 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.5** — 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 10.0.0** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
26
26
 
27
27
  Use the official latest stable SKS and Codex CLI releases. The Codex compatibility SSOT is always the **current latest stable** host; capability probes measure what that host can actually do. Product docs do not crown a fixed `0.x.y` string as SSOT (release pins and schema directories are measured artifacts for the current package, not a permanent product version claim). Menu Bar / Center induce updates to the latest stable build. Run `sks update-check` for what is installed and read the capability report for what is supported. Install SSOT is npm `sneakoscope@latest`; PATH `sks` and Menu Bar stamped generation must match that version or gates fail. It resolves managed SKS skills from the authoritative global install, preserves a runnable Naruto child slot when `max_threads=2`, and keeps Menu Bar repair transactional so stamped generations remain verifiable. Naruto uses stable opt-in multi-agent V2 when the host exposes it (Codex official multi-agent wrap-only; SKS does not reimplement a parallel runtime). Local code search is mode-separated (`sks search files|text|structure|symbol|context`); `context` is answered by the compiled TriWiki Context Graph (`context-graph.json` is exhaustive authority; `context-pack.json` and managed `AGENTS.md` are bounded projections) — see [docs/architecture/context-graph.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/architecture/context-graph.md) and [docs/PRODUCT-CONTRACT.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/PRODUCT-CONTRACT.md). See [CHANGELOG.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/CHANGELOG.md).
28
28
 
@@ -305,7 +305,7 @@ Adapter rules that keep updates safe:
305
305
  | --- | --- | --- |
306
306
  | Did tests/typecheck fail? | Another model may say so. | Machine check output is tagged `evidence: machine`. |
307
307
  | Are findings ranked? | Usually one blended opinion. | Machine evidence sorts before LLM findings. |
308
- | Can work stop? | The model decides. | Stop gates, Completion Proof, and Honest Mode decide. |
308
+ | Can work stop? | The model decides. | Safety gates decide (`essential`, default: a finished turn finishes; `strict` adds Stop gates, Completion Proof, and Honest Mode). |
309
309
  | Can I inspect agent-thread progress? | Usually no runtime UI. | Use the official Codex subagent/thread surfaces and their proof artifacts. |
310
310
 
311
311
  ## Demo
@@ -18,6 +18,7 @@
18
18
  {
19
19
  "canonical_name": "sks-answer",
20
20
  "trusted_sha256": [
21
+ "0e461537093215960734aaeafdccb125d2c3ca139dab5db8548d5c0bdd14d652",
21
22
  "3056ac543fa074868c7023097fdcb042d7175ab557d8e18cb7d92ce872785452",
22
23
  "bc8e2fc157b6c9819d32bb0a41bdf7f599a07693df18b0d80aa0ba103594f6eb"
23
24
  ]
@@ -65,6 +66,7 @@
65
66
  {
66
67
  "canonical_name": "sks-computer-use-fast",
67
68
  "trusted_sha256": [
69
+ "1d191ed0f9490fc320dd17b871ca556fa5835bfb372c38b62f3985e3369f685a",
68
70
  "57e4c1d04ae785b0a53711e53210f2ddabd6131041b97edb4b6d99f18b24155e",
69
71
  "b270d82e4057934d7f92ed4241d4ae86e22d0a9a08bceba1aa179d7535bda042",
70
72
  "b8736b2621398dfc85f7a2ff35d7a2fc728db0dfc0e964de69aebff93b5ed897"
@@ -207,6 +209,7 @@
207
209
  {
208
210
  "canonical_name": "sks-honest-mode",
209
211
  "trusted_sha256": [
212
+ "4a0722737a837b0de895b7c950da216a8641922e4da53d6b9c1681b957340e82",
210
213
  "d1216afadecd4d9c85a2fb54bd31ee77ba45f8071f43bb0e21305c5fcd870095",
211
214
  "4543d942cac510b1a23e2e842f944d033087bbc761f6abc4a27d0a015bf22235"
212
215
  ]
@@ -283,6 +286,7 @@
283
286
  {
284
287
  "canonical_name": "sks-pipeline-runner",
285
288
  "trusted_sha256": [
289
+ "66e105773b8e042eec5d5679b97b7cb305cad8d45671034907fbfa728fc4d68a",
286
290
  "020ba4da2f96bcda23cee4596b1e790816780178f1917dee748ff4b289cba0e5",
287
291
  "ffcbe0ea4b585e3d2886f1d9fa7a3b88706aaf42182fb177b2371b76beb0ddcd"
288
292
  ]
@@ -259,7 +259,7 @@ dependencies = [
259
259
 
260
260
  [[package]]
261
261
  name = "sks-core"
262
- version = "9.2.5"
262
+ version = "10.0.0"
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.5"
3
+ version = "10.0.0"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -11,9 +11,63 @@ function rootJsonFastInline(fs, cwd = process.cwd()) {
11
11
  using_global_root: !project
12
12
  })}\n`);
13
13
  }
14
+ async function fastDesktopBridgeUpstreamEvidence(home) {
15
+ const notChecked = (reason) => ({
16
+ section: { schema: 'sks.desktop-bridge-fast-status.v1', status: 'not_checked', reason, secret_stores_read: false },
17
+ warnings: [], nextActions: [],
18
+ });
19
+ try {
20
+ const getBuiltinModule = process.getBuiltinModule;
21
+ const fs = typeof getBuiltinModule === 'function' ? getBuiltinModule('node:fs') : await import('node:fs');
22
+ const runtime = joinPath(joinPath(home, '.codex'), 'sks');
23
+ const statePath = joinPath(runtime, 'desktop-bridge-state.json');
24
+ if (!fs.existsSync(statePath))
25
+ return notChecked('bridge_state_missing');
26
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
27
+ const startedAt = typeof state.started_at === 'string' ? state.started_at : null;
28
+ const logPath = joinPath(joinPath(runtime, 'logs'), 'desktop-bridge.out.log');
29
+ const { BRIDGE_LOG_TAIL_BYTES, UNREACHABLE_UPSTREAM_RECOVERY_ACTION, detectUnreachableUpstreamEvidence } = await import('../core/codex-lb/desktop-bridge/upstream-evidence.js');
30
+ let tail = '';
31
+ if (fs.existsSync(logPath)) {
32
+ const handle = fs.openSync(logPath, 'r');
33
+ try {
34
+ const size = fs.fstatSync(handle).size;
35
+ const start = Math.max(0, size - BRIDGE_LOG_TAIL_BYTES);
36
+ const buffer = Buffer.alloc(size - start);
37
+ fs.readSync(handle, buffer, 0, buffer.length, start);
38
+ tail = buffer.toString('utf8');
39
+ }
40
+ finally {
41
+ fs.closeSync(handle);
42
+ }
43
+ }
44
+ const code = detectUnreachableUpstreamEvidence(tail, startedAt, Date.now());
45
+ const blockers = code ? [`desktop_bridge_upstream_unreachable:${code}`] : [];
46
+ return {
47
+ section: {
48
+ schema: 'sks.desktop-bridge-fast-status.v1',
49
+ status: code ? 'upstream_unreachable_evidence' : 'log_evidence_clear',
50
+ reason: 'fast_readonly_json_log_evidence',
51
+ secret_stores_read: false,
52
+ serving_pid: typeof state.pid === 'number' ? state.pid : null,
53
+ started_at: startedAt,
54
+ blockers,
55
+ recovery_actions: code ? [UNREACHABLE_UPSTREAM_RECOVERY_ACTION] : [],
56
+ },
57
+ warnings: blockers,
58
+ nextActions: code ? [UNREACHABLE_UPSTREAM_RECOVERY_ACTION] : [],
59
+ };
60
+ }
61
+ catch {
62
+ return notChecked('evidence_read_failed');
63
+ }
64
+ }
14
65
  async function doctorJsonFastInline(input = {}) {
15
66
  const startedAt = Date.now();
16
67
  const write = input.write || ((text) => process.stdout.write(text));
68
+ const env = input.processEnv || process.env;
69
+ const home = input.home || env.HOME || env.USERPROFILE || process.cwd();
70
+ const bridge = await fastDesktopBridgeUpstreamEvidence(home);
17
71
  write(`${JSON.stringify({
18
72
  schema: 'sks.doctor-status.v3',
19
73
  elapsed_ms: Math.max(0, Date.now() - startedAt),
@@ -23,7 +77,7 @@ async function doctorJsonFastInline(input = {}) {
23
77
  deep_diagnostics_skipped: true,
24
78
  deep_ok: null,
25
79
  not_counted_as_full_doctor: true,
26
- next_actions: ['Run sks doctor --full --json for deep diagnostics.'],
80
+ next_actions: ['Run sks doctor --full --json for deep diagnostics.', ...bridge.nextActions],
27
81
  fast_path: true,
28
82
  profile: 'fast-readonly',
29
83
  root: process.cwd(),
@@ -40,13 +94,8 @@ async function doctorJsonFastInline(input = {}) {
40
94
  },
41
95
  doctor_fix_transaction: null,
42
96
  blockers: [],
43
- warnings: ['fast_readonly_doctor_skipped_optional_deep_diagnostics'],
44
- desktop_bridge: {
45
- schema: 'sks.desktop-bridge-fast-status.v1',
46
- status: 'not_checked',
47
- reason: 'fast_readonly_json',
48
- secret_stores_read: false
49
- }
97
+ warnings: ['fast_readonly_doctor_skipped_optional_deep_diagnostics', ...bridge.warnings],
98
+ desktop_bridge: bridge.section
50
99
  }, null, 2)}\n`);
51
100
  }
52
101
  async function narutoHelpJsonFastInline() {
@@ -27,7 +27,7 @@ async function runSks(args) {
27
27
  const { hookUserPromptSubmitPerfInline } = await import('./fast-inline.js');
28
28
  await hookUserPromptSubmitPerfInline();
29
29
  }
30
- else if (args[0] === 'hook' && args[1] && process.env.SKS_HOOK_DAEMON === '1') {
30
+ else if (args[0] === 'hook' && args[1] && (await import('../core/verification-profile.js')).hookDaemonEnabled()) {
31
31
  const { hookDaemonInline } = await import('../core/daemon/sksd-hook-dispatch.js');
32
32
  await hookDaemonInline(args[1]);
33
33
  }
@@ -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,19 @@ 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
+ const warnings = (status.readiness.warnings || []).map(String).filter(Boolean);
77
+ if (expected && status.readiness.ready !== true && status.readiness.state === 'degraded' && blockers.length === 0) {
78
+ warnings.push('desktop_bridge_readiness_degraded:transport_unverified_for_current_process');
79
+ recoveryActions.push('Run `sks bridge verify --level transport` (or `sks doctor --fix`, which re-verifies after its repair) to bind a transport diagnostic to the serving bridge.');
80
+ }
81
+ if (expected && status.management?.managed === true) {
82
+ const evidence = await (deps.desktopBridgeUpstreamEvidenceImpl || readDesktopBridgeUnreachableUpstreamEvidence)(home).catch(() => null);
83
+ if (evidence) {
84
+ blockers.push(`desktop_bridge_upstream_unreachable:${evidence.code}`);
85
+ recoveryActions.push('Run `sks doctor --fix` (or `sks bridge repair`) to restart the Desktop Bridge and re-resolve its upstream address.');
86
+ }
87
+ }
75
88
  return {
76
89
  schema: 'sks.doctor-desktop-bridge.v1',
77
90
  ok: blockers.length === 0,
@@ -81,8 +94,8 @@ export async function inspectDoctorDesktopBridgeStatus(input = {}, deps = {}) {
81
94
  status,
82
95
  providers: status.providers,
83
96
  blockers,
84
- warnings: (status.readiness.warnings || []).map(String).filter(Boolean),
85
- recovery_actions: (status.recovery_actions || []).map(String).filter(Boolean)
97
+ warnings,
98
+ recovery_actions: recoveryActions
86
99
  };
87
100
  }
88
101
  catch (error) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "sks.skills-manifest.v1",
3
- "package_version": "9.2.5",
3
+ "package_version": "10.0.0",
4
4
  "skills": [
5
5
  {
6
6
  "canonical_name": "sks",
@@ -23,8 +23,9 @@
23
23
  {
24
24
  "canonical_name": "sks-answer",
25
25
  "type": "official",
26
- "content_sha256": "3056ac543fa074868c7023097fdcb042d7175ab557d8e18cb7d92ce872785452",
26
+ "content_sha256": "0e461537093215960734aaeafdccb125d2c3ca139dab5db8548d5c0bdd14d652",
27
27
  "hash_history": [
28
+ "3056ac543fa074868c7023097fdcb042d7175ab557d8e18cb7d92ce872785452",
28
29
  "bc8e2fc157b6c9819d32bb0a41bdf7f599a07693df18b0d80aa0ba103594f6eb"
29
30
  ],
30
31
  "deprecated_aliases": []
@@ -82,8 +83,9 @@
82
83
  {
83
84
  "canonical_name": "sks-computer-use-fast",
84
85
  "type": "official",
85
- "content_sha256": "57e4c1d04ae785b0a53711e53210f2ddabd6131041b97edb4b6d99f18b24155e",
86
+ "content_sha256": "1d191ed0f9490fc320dd17b871ca556fa5835bfb372c38b62f3985e3369f685a",
86
87
  "hash_history": [
88
+ "57e4c1d04ae785b0a53711e53210f2ddabd6131041b97edb4b6d99f18b24155e",
87
89
  "b270d82e4057934d7f92ed4241d4ae86e22d0a9a08bceba1aa179d7535bda042",
88
90
  "b8736b2621398dfc85f7a2ff35d7a2fc728db0dfc0e964de69aebff93b5ed897"
89
91
  ],
@@ -263,8 +265,9 @@
263
265
  {
264
266
  "canonical_name": "sks-honest-mode",
265
267
  "type": "official",
266
- "content_sha256": "d1216afadecd4d9c85a2fb54bd31ee77ba45f8071f43bb0e21305c5fcd870095",
268
+ "content_sha256": "4a0722737a837b0de895b7c950da216a8641922e4da53d6b9c1681b957340e82",
267
269
  "hash_history": [
270
+ "d1216afadecd4d9c85a2fb54bd31ee77ba45f8071f43bb0e21305c5fcd870095",
268
271
  "4543d942cac510b1a23e2e842f944d033087bbc761f6abc4a27d0a015bf22235"
269
272
  ],
270
273
  "deprecated_aliases": []
@@ -351,8 +354,9 @@
351
354
  {
352
355
  "canonical_name": "sks-pipeline-runner",
353
356
  "type": "official",
354
- "content_sha256": "020ba4da2f96bcda23cee4596b1e790816780178f1917dee748ff4b289cba0e5",
357
+ "content_sha256": "66e105773b8e042eec5d5679b97b7cb305cad8d45671034907fbfa728fc4d68a",
355
358
  "hash_history": [
359
+ "020ba4da2f96bcda23cee4596b1e790816780178f1917dee748ff4b289cba0e5",
356
360
  "ffcbe0ea4b585e3d2886f1d9fa7a3b88706aaf42182fb177b2371b76beb0ddcd"
357
361
  ],
358
362
  "deprecated_aliases": []
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { ensureDir, nowIso, readText, writeTextAtomic } from '../fsx.js';
3
3
  import { CODEX_HOOK_EVENTS } from '../codex-compat/codex-hook-events.js';
4
+ import { postToolEvidenceEnabled } from '../verification-profile.js';
4
5
  import { buildCodexCommandHookToml, matcherApplies } from './codex-hook-config-writer.js';
5
6
  import { readCodexHookActualState } from './codex-hook-actual-discovery.js';
6
7
  import { writeTrustedHashStateForHooksFile } from './codex-hook-state-writer.js';
@@ -22,12 +23,13 @@ export async function installManagedCodexHooks(root, opts = {}) {
22
23
  const scriptPath = path.join(managedDir, 'sks-managed-hook.sh');
23
24
  const tomlPath = path.join(managedDir, 'sks-managed-hooks.toml');
24
25
  const binCommand = opts.binCommand || 'sks hook';
25
- const hooksToml = CODEX_HOOK_EVENTS.map((event) => buildCodexCommandHookToml({
26
+ const installedEvents = CODEX_HOOK_EVENTS.filter((event) => event !== 'PostToolUse' || postToolEvidenceEnabled(root));
27
+ const hooksToml = installedEvents.map((event) => buildCodexCommandHookToml({
26
28
  event,
27
29
  matcher: matcherApplies(event) ? '*' : null,
28
30
  command: `${scriptPath} ${HOOK_SUBCOMMANDS[event] || event}`,
29
31
  timeout: event === 'Stop' ? 60 : 30,
30
- statusMessage: event === 'Stop' ? 'SKS validating completion proof' : null
32
+ statusMessage: event === 'Stop' ? 'SKS checking done gate' : null
31
33
  })).join('\n');
32
34
  const requirementsToml = mergeRequirementsToml(await readText(requirementsPath, ''), {
33
35
  managedDir,
@@ -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);
@@ -306,7 +306,7 @@ export async function startPreparedDesktopBridge(input, options = {}) {
306
306
  }
307
307
  const freshnessMs = input.stateFreshnessMs ?? 5 * 60_000;
308
308
  const heartbeat = statePath ? setInterval(() => {
309
- void refreshDesktopBridgeState(statePath, state, new Date(), freshnessMs).catch(() => undefined);
309
+ void refreshDesktopBridgeState(statePath, state, new Date(), freshnessMs, { preserveVerifiedProbeIds: true }).catch(() => undefined);
310
310
  }, Math.max(1_000, Math.floor(freshnessMs / 3))) : null;
311
311
  heartbeat?.unref();
312
312
  let skewCandidate = null;
@@ -69,11 +69,13 @@ export function isDesktopBridgePublicState(value) {
69
69
  && Array.isArray(row.last_verified_probe_ids) && row.last_verified_probe_ids.every(isGeneration)
70
70
  && typeof row.config_generation === 'string' && /^[a-f0-9]{64}$/.test(row.config_generation);
71
71
  }
72
- export async function refreshDesktopBridgeState(file, state, now = new Date(), freshnessMs = DEFAULT_FRESHNESS_MS) {
72
+ export async function refreshDesktopBridgeState(file, state, now = new Date(), freshnessMs = DEFAULT_FRESHNESS_MS, options = {}) {
73
73
  const current = await readDesktopBridgeState(file).catch(() => null);
74
74
  if (!current || current.schema !== DESKTOP_BRIDGE_STATE_SCHEMA || current.pid !== state.pid
75
75
  || current.process_generation !== state.process_generation || current.config_generation !== state.config_generation)
76
76
  return false;
77
+ if (options.preserveVerifiedProbeIds)
78
+ state.last_verified_probe_ids = [...current.last_verified_probe_ids];
77
79
  state.updated_at = now.toISOString();
78
80
  state.stale_after = new Date(now.getTime() + freshnessMs).toISOString();
79
81
  await writeDesktopBridgeState(file, state);
@@ -0,0 +1,42 @@
1
+ export const BRIDGE_UNREACHABLE_EVIDENCE_WINDOW_MS = 10 * 60_000;
2
+ export const BRIDGE_LOG_TAIL_BYTES = 256 * 1024;
3
+ const UNREACHABLE_REJECTION_CODE = /^bridge_(?:websocket_)?upstream_unavailable/;
4
+ const REROUTED_REJECTION_CODE = /^bridge_upstream_unreachable_rerouted/;
5
+ export function detectUnreachableUpstreamEvidence(logTail, startedAt, nowMs) {
6
+ const startedMs = startedAt ? Date.parse(startedAt) : Number.NaN;
7
+ const cutoffMs = Math.max(Number.isFinite(startedMs) ? startedMs : 0, nowMs - BRIDGE_UNREACHABLE_EVIDENCE_WINDOW_MS);
8
+ let latest = null;
9
+ let latestReroute = Number.NEGATIVE_INFINITY;
10
+ for (const line of logTail.split('\n')) {
11
+ const trimmed = line.trim();
12
+ if (!trimmed.startsWith('{') || !trimmed.includes('"sks.desktop_bridge.rejected'))
13
+ continue;
14
+ let record;
15
+ try {
16
+ record = JSON.parse(trimmed);
17
+ }
18
+ catch {
19
+ continue;
20
+ }
21
+ if (record.event !== 'sks.desktop_bridge.rejected' && record.event !== 'sks.desktop_bridge.rejected_summary')
22
+ continue;
23
+ const code = typeof record.code === 'string' ? record.code : '';
24
+ const unreachable = UNREACHABLE_REJECTION_CODE.test(code);
25
+ const rerouted = !unreachable && REROUTED_REJECTION_CODE.test(code);
26
+ if (!unreachable && !rerouted)
27
+ continue;
28
+ const at = typeof record.at === 'string' ? Date.parse(record.at) : Number.NaN;
29
+ if (!Number.isFinite(at) || at < cutoffMs)
30
+ continue;
31
+ if (rerouted) {
32
+ latestReroute = Math.max(latestReroute, at);
33
+ continue;
34
+ }
35
+ if (!latest || at > latest.at)
36
+ latest = { at, code };
37
+ }
38
+ if (!latest || latest.at <= latestReroute)
39
+ return null;
40
+ return latest.code;
41
+ }
42
+ export const UNREACHABLE_UPSTREAM_RECOVERY_ACTION = 'Run `sks doctor --fix` (or `sks bridge repair`) to restart the Desktop Bridge and re-resolve its upstream address.';