sneakoscope 8.1.2 → 8.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.
Files changed (261) hide show
  1. package/README.md +96 -58
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/bin/fast-inline.js +5 -51
  5. package/dist/cli/command-manifest-lite.js +2 -1
  6. package/dist/cli/command-registry.js +2 -1
  7. package/dist/cli/global-mode-router.js +1 -1
  8. package/dist/cli/install-helpers-codex-lb-config.js +112 -914
  9. package/dist/cli/install-helpers-codex-lb-shared.js +3 -44
  10. package/dist/cli/install-helpers-install-support.js +4 -20
  11. package/dist/cli/install-helpers.js +36 -2716
  12. package/dist/cli/router.js +9 -9
  13. package/dist/commands/bridge-command-output.js +111 -0
  14. package/dist/commands/bridge.js +320 -0
  15. package/dist/commands/codex-app.js +39 -123
  16. package/dist/commands/doctor-helpers.js +380 -0
  17. package/dist/commands/doctor-profile.js +3 -0
  18. package/dist/commands/doctor.js +114 -873
  19. package/dist/commands/mad-sks.js +0 -4
  20. package/dist/config/skills-manifest.json +1 -1
  21. package/dist/core/agents/agent-effort-policy.js +11 -21
  22. package/dist/core/agents/agent-recursion-guard.js +26 -0
  23. package/dist/core/agents/agent-runner-codex-exec.js +7 -9
  24. package/dist/core/architecture-hardening/contracts/contracts.js +225 -161
  25. package/dist/core/architecture-hardening/state/state-service.js +49 -13
  26. package/dist/core/auto-review.js +15 -9
  27. package/dist/core/codex/codex-config-guard.js +8 -0
  28. package/dist/core/codex-adapter.js +8 -10
  29. package/dist/core/codex-app/codex-app-fast-ui-repair.js +7 -29
  30. package/dist/core/codex-app/codex-app-launcher.js +10 -12
  31. package/dist/core/codex-app/codex-app-restart.js +10 -12
  32. package/dist/core/codex-app/menubar/constants.js +2 -5
  33. package/dist/core/codex-app/menubar/global-install.js +3 -2
  34. package/dist/core/codex-app/menubar/installer/result.js +23 -0
  35. package/dist/core/codex-app/menubar/installer/runtime.js +28 -0
  36. package/dist/core/codex-app/menubar/installer.js +5 -96
  37. package/dist/core/codex-app/menubar/migration.js +1 -42
  38. package/dist/core/codex-app/openrouter-provider.js +0 -78
  39. package/dist/core/codex-app.js +97 -360
  40. package/dist/core/codex-control/codex-app-server-runtime-env.js +8 -28
  41. package/dist/core/codex-control/codex-current-image-path-real-probe.js +2 -3
  42. package/dist/core/codex-control/codex-current-web-search-probe.js +2 -3
  43. package/dist/core/codex-control/codex-sdk-adapter.js +5 -48
  44. package/dist/core/codex-control/codex-task-runner.js +16 -60
  45. package/dist/core/codex-control/desktop-bridge-launch-guard.js +227 -0
  46. package/dist/core/codex-exec-output-schema.js +11 -13
  47. package/dist/core/codex-lb/bridge-contracts.js +2 -0
  48. package/dist/core/codex-lb/bridge-runtime-validation/capability.js +104 -0
  49. package/dist/core/codex-lb/bridge-runtime-validation/catalog.js +52 -0
  50. package/dist/core/codex-lb/bridge-runtime-validation/shared.js +109 -0
  51. package/dist/core/codex-lb/bridge-runtime-validation/status.js +250 -0
  52. package/dist/core/codex-lb/bridge-runtime-validation.js +106 -0
  53. package/dist/core/codex-lb/capability-runner.js +505 -201
  54. package/dist/core/codex-lb/codex-lb-env.js +179 -12
  55. package/dist/core/codex-lb/combined-catalog/builder.js +86 -0
  56. package/dist/core/codex-lb/combined-catalog/contracts.js +5 -0
  57. package/dist/core/codex-lb/combined-catalog/normalize.js +170 -0
  58. package/dist/core/codex-lb/combined-catalog/persistence.js +207 -0
  59. package/dist/core/codex-lb/combined-catalog/shared.js +12 -0
  60. package/dist/core/codex-lb/combined-catalog/storage.js +171 -0
  61. package/dist/core/codex-lb/combined-catalog.js +12 -0
  62. package/dist/core/codex-lb/desktop-bridge/client.js +4 -1
  63. package/dist/core/codex-lb/desktop-bridge/header-policy.js +48 -63
  64. package/dist/core/codex-lb/desktop-bridge/http-forward.js +115 -83
  65. package/dist/core/codex-lb/desktop-bridge/launchd.js +20 -13
  66. package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +4 -1
  67. package/dist/core/codex-lb/desktop-bridge/security.js +381 -267
  68. package/dist/core/codex-lb/desktop-bridge/server.js +148 -19
  69. package/dist/core/codex-lb/desktop-bridge/state.js +56 -70
  70. package/dist/core/codex-lb/desktop-bridge/types.js +6 -1
  71. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +281 -106
  72. package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +245 -0
  73. package/dist/core/codex-lb/desktop-bridge-migration/historical-toml.js +56 -0
  74. package/dist/core/codex-lb/desktop-bridge-migration/metadata.js +73 -0
  75. package/dist/core/codex-lb/desktop-bridge-migration/receipt.js +52 -0
  76. package/dist/core/codex-lb/desktop-bridge-migration/retired-runtime-cleanup.js +106 -0
  77. package/dist/core/codex-lb/desktop-bridge-migration/types.js +1 -0
  78. package/dist/core/codex-lb/desktop-bridge-migration.js +238 -0
  79. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +257 -0
  80. package/dist/core/codex-lb/desktop-controller-v3/diagnostics.js +62 -0
  81. package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +203 -0
  82. package/dist/core/codex-lb/desktop-controller-v3/live-probes.js +293 -0
  83. package/dist/core/codex-lb/desktop-controller-v3/preflight-probes.js +144 -0
  84. package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +182 -0
  85. package/dist/core/codex-lb/desktop-controller-v3/shared.js +279 -0
  86. package/dist/core/codex-lb/desktop-controller-v3/status.js +269 -0
  87. package/dist/core/codex-lb/desktop-controller-v3/types.js +1 -0
  88. package/dist/core/codex-lb/desktop-controller-v3/verification.js +89 -0
  89. package/dist/core/codex-lb/desktop-controller-v3.js +67 -0
  90. package/dist/core/codex-lb/desktop-controller.js +2 -1730
  91. package/dist/core/codex-lb/desktop-service.js +462 -627
  92. package/dist/core/codex-lb/migration-receipt.js +560 -118
  93. package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +37 -53
  94. package/dist/core/codex-lb/probes/bridge-probe.js +125 -64
  95. package/dist/core/codex-lb/probes/catalog-probe.js +36 -136
  96. package/dist/core/codex-lb/probes/computer-use-probe.js +63 -39
  97. package/dist/core/codex-lb/probes/image-generation-probe.js +98 -41
  98. package/dist/core/codex-lb/probes/probe-evidence.js +32 -33
  99. package/dist/core/codex-lb/probes/voice-realtime-probe.js +69 -43
  100. package/dist/core/codex-lb/provider-credentials/configuration.js +127 -0
  101. package/dist/core/codex-lb/provider-credentials/locks.js +11 -0
  102. package/dist/core/codex-lb/provider-credentials/private-snapshot.js +28 -0
  103. package/dist/core/codex-lb/provider-credentials/removal.js +33 -0
  104. package/dist/core/codex-lb/provider-credentials/resolution.js +59 -0
  105. package/dist/core/codex-lb/provider-credentials/runtime.js +55 -0
  106. package/dist/core/codex-lb/provider-credentials/types.js +1 -0
  107. package/dist/core/codex-lb/provider-credentials/validation-store.js +137 -0
  108. package/dist/core/codex-lb/provider-credentials.js +5 -0
  109. package/dist/core/codex-lb/provider-registry.js +323 -0
  110. package/dist/core/codex-lb/provider-route-policy.js +118 -0
  111. package/dist/core/codex-lb/request-route-resolver.js +172 -0
  112. package/dist/core/codex-lb/route-index.js +148 -0
  113. package/dist/core/codex-lb/trusted-deep-evidence.js +158 -286
  114. package/dist/core/commands/basic-cli.js +1 -1
  115. package/dist/core/commands/check-command.js +3 -10
  116. package/dist/core/commands/gate-result-contract.js +10 -14
  117. package/dist/core/commands/intent-normalization/normalizer.js +1 -1
  118. package/dist/core/commands/mad-sks-command.js +2 -6
  119. package/dist/core/commands/release-command.js +11 -1
  120. package/dist/core/commands/telegram-command.js +46 -6
  121. package/dist/core/doctor/doctor-readiness-matrix.js +9 -7
  122. package/dist/core/doctor/skill-legacy-surface.js +5 -5
  123. package/dist/core/evidence/v2/evidence-key.js +30 -3
  124. package/dist/core/feature-fixtures.js +8 -1
  125. package/dist/core/feature-registry.js +3 -2
  126. package/dist/core/hooks-runtime/stop-finalization.js +11 -1
  127. package/dist/core/hooks-runtime/subagent-context.js +15 -1
  128. package/dist/core/hooks-runtime/subagent-skill-availability-contract.js +1 -1
  129. package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
  130. package/dist/core/hooks-runtime.js +73 -87
  131. package/dist/core/image-ux-review/imagegen-adapter.js +108 -94
  132. package/dist/core/image-ux-review/real-callout-extractor.js +144 -14
  133. package/dist/core/imagegen/desktop-bridge-imagegen-target.js +77 -0
  134. package/dist/core/imagegen/imagegen-capability.js +43 -151
  135. package/dist/core/init/skills/filesystem.js +89 -0
  136. package/dist/core/init/skills/inventory.js +97 -0
  137. package/dist/core/init/skills/manifest.js +151 -0
  138. package/dist/core/init/skills.js +5 -325
  139. package/dist/core/init.js +0 -63
  140. package/dist/core/install/installed-package-smoke.js +1 -1
  141. package/dist/core/managed-assets/managed-assets-manifest.js +0 -1
  142. package/dist/core/policy/latest-version-guidance.js +15 -2
  143. package/dist/core/preflight/parallel-preflight-engine.js +27 -51
  144. package/dist/core/provider/provider-badge.js +9 -4
  145. package/dist/core/provider/provider-context.js +57 -107
  146. package/dist/core/providers/openrouter/openrouter-account.js +3 -1
  147. package/dist/core/providers/openrouter/openrouter-secret-store.js +91 -22
  148. package/dist/core/release/desktop-bridge-release-evidence.js +457 -0
  149. package/dist/core/release/gate-manifest.js +0 -1
  150. package/dist/core/release/main-push-guard.js +13 -0
  151. package/dist/core/release/main-push-receipt.js +11 -0
  152. package/dist/core/release/physical-release-gates.js +664 -61
  153. package/dist/core/release/publish-preflight.js +3 -3
  154. package/dist/core/release/release-gate-batch-runner.js +19 -6
  155. package/dist/core/release/release-gate-dag.js +33 -6
  156. package/dist/core/release/release-gate-node.js +1 -3
  157. package/dist/core/release/release-gate-output-contract.js +55 -0
  158. package/dist/core/release/release-pack-contract.js +1 -1
  159. package/dist/core/release/release-pack-receipt.js +209 -8
  160. package/dist/core/release/release-real-contract.js +1 -1
  161. package/dist/core/release/stage-publish.js +106 -28
  162. package/dist/core/retention/temp-lease.js +204 -0
  163. package/dist/core/retention.js +18 -195
  164. package/dist/core/routes.js +1 -2
  165. package/dist/core/safety/intent-contract/intent-contract.js +3 -3
  166. package/dist/core/subagents/naruto-host-credentials.js +6 -1
  167. package/dist/core/subagents/official-subagent-config.js +27 -9
  168. package/dist/core/subagents/official-subagent-runner.js +14 -31
  169. package/dist/core/subagents/wave-lifecycle.js +36 -8
  170. package/dist/core/subagents/wave-parent-guidance.js +2 -1
  171. package/dist/core/telegram/doctor.js +2 -2
  172. package/dist/core/telegram/liveness.js +10 -4
  173. package/dist/core/triwiki/context-graph/compiler/index.js +18 -18
  174. package/dist/core/update/update-migration-state/fast-mode-config.js +172 -0
  175. package/dist/core/update/update-migration-state/session-state-split.js +247 -0
  176. package/dist/core/update/update-migration-state/simple-stages.js +46 -0
  177. package/dist/core/update/update-migration-state.js +4 -481
  178. package/dist/core/version.js +1 -1
  179. package/dist/native/sks-menubar/Sources/OperationCoordinator.swift +28 -4
  180. package/dist/native/sks-menubar/Sources/OperationModels.swift +112 -2
  181. package/dist/native/sks-menubar/Sources/ProviderRouteExplanation.swift +21 -0
  182. package/dist/native/sks-menubar/Sources/ProvidersBridgeCatalog.swift +140 -0
  183. package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +107 -470
  184. package/dist/native/sks-menubar/Sources/ProvidersReliability.swift +27 -197
  185. package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +335 -269
  186. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +233 -437
  187. package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +16 -7
  188. package/dist/native/sks-menubar/Sources/SecureProcessEnvelope.swift +39 -9
  189. package/dist/native/sks-menubar/Sources/TelegramSupport.swift +56 -0
  190. package/dist/scripts/canonical-test-runner.js +7 -1
  191. package/dist/scripts/check-runtime-schemas.js +7 -2
  192. package/dist/scripts/codex-sdk-real-smoke-check.js +3 -1
  193. package/dist/scripts/desktop-bridge-capabilities-check.js +123 -0
  194. package/dist/scripts/{codex-lb-desktop-bridge-latency-check.js → desktop-bridge-latency-check.js} +61 -16
  195. package/dist/scripts/desktop-bridge-real-evidence-check.js +64 -0
  196. package/dist/scripts/{codex-lb-desktop-bridge-check.js → desktop-bridge-transport-check.js} +92 -155
  197. package/dist/scripts/desktop-bridge-transport-client.js +116 -0
  198. package/dist/scripts/desktop-bridge-unification-check.js +276 -0
  199. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +12 -8
  200. package/dist/scripts/gate-policy-audit-check.js +52 -2
  201. package/dist/scripts/provider-badge-context-check.js +55 -12
  202. package/dist/scripts/publish-preflight.js +1 -2
  203. package/dist/scripts/release-affected-selector-check.js +1 -2
  204. package/dist/scripts/release-aggressive-resource-governor-check.js +1 -1
  205. package/dist/scripts/release-cache-glob-hashing-check.js +1 -0
  206. package/dist/scripts/release-cache-version-neutral-fixture-check.js +1 -0
  207. package/dist/scripts/release-full-parallelism-blackbox.js +1 -1
  208. package/dist/scripts/release-gate-batch-runner-check.js +9 -0
  209. package/dist/scripts/release-gate-dag-runner-check.js +6 -0
  210. package/dist/scripts/release-gate-dag-runner.js +4 -2
  211. package/dist/scripts/release-metadata-check.js +16 -3
  212. package/dist/scripts/release-pack-receipt.js +29 -6
  213. package/dist/scripts/release-physical-gates-check.js +39 -0
  214. package/dist/scripts/release-real-check.js +7 -5
  215. package/package.json +6 -8
  216. package/release-gates.v2.json +156 -157
  217. package/runtime-required-scripts.json +11 -11
  218. package/schemas/codex/desktop-bridge-status-v3.schema.json +286 -0
  219. package/schemas/codex/desktop-capabilities-v3.schema.json +198 -0
  220. package/schemas/release/release-gate-node.schema.json +2 -3
  221. package/dist/cli/codex-lb-setup-warning-output.js +0 -17
  222. package/dist/cli/install-helpers-codex-lb-chain.js +0 -529
  223. package/dist/commands/codex-lb.js +0 -1232
  224. package/dist/core/architecture-hardening/migration/migration.js +0 -111
  225. package/dist/core/codex-app/auxiliary-oauth-policy.js +0 -36
  226. package/dist/core/codex-app/catalog-compat/catalog-service.js +0 -104
  227. package/dist/core/codex-app/child-policy/child-policy.js +0 -35
  228. package/dist/core/codex-app/desktop-routing-snapshot.js +0 -233
  229. package/dist/core/codex-app/glm-model-profile.js +0 -1
  230. package/dist/core/codex-app/glm-profile-installer.js +0 -191
  231. package/dist/core/codex-app/glm-profile-schema.js +0 -42
  232. package/dist/core/codex-app/multi-provider-router.js +0 -391
  233. package/dist/core/codex-app/openrouter-activate.js +0 -405
  234. package/dist/core/codex-app/openrouter-model-catalog.js +0 -217
  235. package/dist/core/codex-app/provider-child-policy.js +0 -64
  236. package/dist/core/codex-app/provider-mode.js +0 -91
  237. package/dist/core/codex-app/provider-session-policy.js +0 -85
  238. package/dist/core/codex-app/session-policy/session-pinning.js +0 -68
  239. package/dist/core/codex-control/codex-lb-launch-recovery.js +0 -319
  240. package/dist/core/codex-lb/codex-lb-setup.js +0 -293
  241. package/dist/core/codex-lb/codex-lb-tool-catalog.js +0 -456
  242. package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +0 -290
  243. package/dist/core/codex-lb/desktop-center-credentials.js +0 -1083
  244. package/dist/core/codex-lb/desktop-mode.js +0 -32
  245. package/dist/core/codex-lb/desktop-real-evidence.js +0 -168
  246. package/dist/core/codex-lb/legacy-migration.js +0 -576
  247. package/dist/core/codex-lb/native-openai-transport/transport.js +0 -68
  248. package/dist/core/codex-lb/provider-routing/provider-router.js +0 -58
  249. package/dist/core/codex-lb/routing-truth.js +0 -327
  250. package/dist/core/imagegen/codex-lb-imagegen-target.js +0 -92
  251. package/dist/native/sks-menubar/Sources/ProvidersConnectTest.swift +0 -118
  252. package/dist/native/sks-menubar/Sources/ProvidersFastMode.swift +0 -17
  253. package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +0 -331
  254. package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +0 -260
  255. package/dist/scripts/codex-lb-catalog-passthrough-check.js +0 -120
  256. package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +0 -169
  257. package/dist/scripts/codex-lb-desktop-capabilities-check.js +0 -287
  258. package/dist/scripts/codex-lb-desktop-real-evidence-check.js +0 -123
  259. package/dist/scripts/codex-lb-fast-mode-truth-check.js +0 -114
  260. package/dist/scripts/codex-lb-fast-ui-preservation-check.js +0 -152
  261. package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +0 -231
@@ -1,321 +1,387 @@
1
1
  import Foundation
2
2
 
3
- /// Parses `sks.codex-lb-status.v2` / activation payloads without treating
4
- /// `provider.selected` as "codex-lb is selected". In cli-provider and native-
5
- /// bridge modes, `provider.selected == true` means the built-in OpenAI /
6
- /// ChatGPT OAuth provider is selected.
7
- struct ProviderRoutingTruth {
8
- enum MeasuredState: Equatable {
9
- case inactive
10
- case active
11
- case degraded
12
- case unverified
3
+ enum ProviderFacadeError: Error, Equatable, CustomStringConvertible {
4
+ case schemaInvalid(String)
5
+
6
+ var description: String {
7
+ switch self { case .schemaInvalid(let reason): return reason }
13
8
  }
9
+ }
14
10
 
15
- struct MeasuredRoute {
16
- let selected: Bool
17
- let measured: Bool
18
- let fresh: Bool
19
- let ok: Bool
20
- let status: String
21
- let configuredHost: String?
22
- let actualHost: String?
23
- let authTransport: String?
24
- let authOutcome: String?
25
- let httpStatus: Int?
26
- let measuredAt: String?
27
- let latencyMs: Int?
28
- let blockers: [String]
11
+ enum CapabilityProbeState: String, Codable, CaseIterable {
12
+ case notAttempted = "not_attempted", running, verified, degraded, blocked, failed, unsupported, stale
13
+ }
29
14
 
30
- var active: Bool {
31
- selected && measured && fresh && ok && status == "verified"
32
- }
15
+ enum CapabilityProbeStage: String, Codable {
16
+ case preflight, process, tcpConnect = "tcp_connect", httpHealth = "http_health"
17
+ case websocketUpgrade = "websocket_upgrade", websocketProtocol = "websocket_protocol"
18
+ case frameRoundTrip = "frame_round_trip", cleanClose = "clean_close"
19
+ case providerAuth = "provider_auth", catalogSync = "catalog_sync", modelRoute = "model_route"
20
+ case featureRequest = "feature_request", featureResponse = "feature_response"
21
+ case artifactValidation = "artifact_validation", complete
22
+ }
33
23
 
34
- var state: MeasuredState {
35
- guard selected else { return .inactive }
36
- if active { return .active }
37
- if !measured || !fresh || status == "selected_unmeasured" || status == "stale" {
38
- return .unverified
39
- }
40
- return .degraded
24
+ enum CapabilityScope: String, Codable, CaseIterable {
25
+ case bridge
26
+ case nativeIdentity = "native-identity"
27
+ case codexLb = "provider:codex-lb"
28
+ case openRouter = "provider:openrouter"
29
+ case combinedCatalog = "catalog:combined"
30
+
31
+ var displayName: String {
32
+ switch self {
33
+ case .bridge: return "Bridge"
34
+ case .nativeIdentity: return "OAuth Identity"
35
+ case .codexLb: return "Codex-LB"
36
+ case .openRouter: return "OpenRouter"
37
+ case .combinedCatalog: return "Combined Catalog"
41
38
  }
42
39
  }
40
+ }
43
41
 
44
- struct Snapshot {
45
- let mode: String?
46
- let desktopFullRouting: Bool
47
- let legacyCodexLbSelected: Bool
48
- let chatgptOauthPresent: Bool
49
- let cliProviderStored: Bool
50
- let cliCredentialsConfigured: Bool
51
- let legacyDestructive: Bool
52
- let oauthPreservedFlag: Bool?
53
- let authMutated: Bool?
54
- }
42
+ enum JSONValue: Codable, Equatable {
43
+ case string(String), number(Double), boolean(Bool), object([String: JSONValue]), array([JSONValue]), null
55
44
 
56
- static func snapshot(from json: [String: Any]) -> Snapshot {
57
- let mode = desktopMode(json)
58
- let oauth = json["oauth"] as? [String: Any]
59
- let provider = json["provider"] as? [String: Any]
60
- let capabilities = json["capabilities"] as? [String: Any]
61
- let bridge = json["bridge"] as? [String: Any]
62
- let legacyPayload = (json["codex_lb"] as? [String: Any]) ?? json
63
- let chatgptOauthPresent = json["chatgpt_oauth_present"] as? Bool
64
- ?? (oauth?["present"] as? Bool == true
65
- || ["chatgpt_oauth", "mixed"].contains(oauth?["mode"] as? String ?? ""))
66
- let legacyCodexLbSelected = json["legacy_codex_lb_selected"] as? Bool
67
- ?? isLegacyCodexLbSelected(json: json, mode: mode, provider: provider)
68
- let cliProviderStored = ["cli-provider", "desktop-native-bridge", "desktop-dual-auth-compat"].contains(mode ?? "")
69
- || legacyPayload["provider_configured"] as? Bool == true
70
- || ((provider?["contract"] as? String)?.hasPrefix("codex-lb") == true)
71
- let keyFingerprint = bridge?["key_fingerprint"] as? String
72
- let cliCredentialsConfigured = legacyPayload["env_key_configured"] as? Bool == true
73
- || legacyPayload["key_configured"] as? Bool == true
74
- || json["configured"] as? Bool == true
75
- || (keyFingerprint?.isEmpty == false)
76
- let oauthPreservedFlag = (json["oauth_preserved"] as? Bool)
77
- ?? (capabilities?["oauth_preserved"] as? Bool)
78
- ?? (oauth?["preserved"] as? Bool)
79
- return Snapshot(
80
- mode: mode,
81
- desktopFullRouting: mode == "desktop-native-bridge",
82
- legacyCodexLbSelected: legacyCodexLbSelected,
83
- chatgptOauthPresent: chatgptOauthPresent,
84
- cliProviderStored: cliProviderStored,
85
- cliCredentialsConfigured: cliCredentialsConfigured,
86
- legacyDestructive: isLegacyDestructive(json),
87
- oauthPreservedFlag: oauthPreservedFlag,
88
- authMutated: json["auth_mutated"] as? Bool ?? json["desktop_auth_mutated"] as? Bool
89
- )
45
+ init(from decoder: Decoder) throws {
46
+ let value = try decoder.singleValueContainer()
47
+ if value.decodeNil() { self = .null }
48
+ else if let boolean = try? value.decode(Bool.self) { self = .boolean(boolean) }
49
+ else if let number = try? value.decode(Double.self) { self = .number(number) }
50
+ else if let string = try? value.decode(String.self) { self = .string(string) }
51
+ else if let object = try? value.decode([String: JSONValue].self) { self = .object(object) }
52
+ else { self = .array(try value.decode([JSONValue].self)) }
90
53
  }
91
54
 
92
- static func measuredRoute(from json: [String: Any]) -> MeasuredRoute? {
93
- guard let truth = json["routing_truth"] as? [String: Any],
94
- truth["schema"] as? String == "sks.codex-lb-routing-truth.v1" else {
95
- return nil
55
+ func encode(to encoder: Encoder) throws {
56
+ var value = encoder.singleValueContainer()
57
+ switch self {
58
+ case .string(let raw): try value.encode(raw)
59
+ case .number(let raw): try value.encode(raw)
60
+ case .boolean(let raw): try value.encode(raw)
61
+ case .object(let raw): try value.encode(raw)
62
+ case .array(let raw): try value.encode(raw)
63
+ case .null: try value.encodeNil()
96
64
  }
97
- return MeasuredRoute(
98
- selected: truth["selected"] as? Bool == true,
99
- measured: truth["measured"] as? Bool == true,
100
- fresh: truth["fresh"] as? Bool == true,
101
- ok: truth["ok"] as? Bool == true,
102
- status: truth["status"] as? String ?? "unavailable",
103
- configuredHost: truth["configured_host"] as? String,
104
- actualHost: truth["actual_host"] as? String,
105
- authTransport: truth["auth_transport"] as? String,
106
- authOutcome: truth["auth_outcome"] as? String,
107
- httpStatus: (truth["http_status"] as? NSNumber)?.intValue,
108
- measuredAt: truth["measured_at"] as? String ?? truth["checked_at"] as? String,
109
- latencyMs: (truth["latency_ms"] as? NSNumber)?.intValue,
110
- blockers: truth["blockers"] as? [String] ?? []
111
- )
112
65
  }
66
+ }
113
67
 
114
- static func desktopMode(_ json: [String: Any]) -> String? {
115
- if let mode = json["desktop_mode"] as? String, !mode.isEmpty { return mode }
116
- if let mode = json["mode"] as? String, !mode.isEmpty { return mode }
117
- if let routing = json["desktop_routing"] as? [String: Any],
118
- let mode = routing["mode"] as? String, !mode.isEmpty {
119
- return mode
120
- }
121
- return nil
68
+ struct CapabilityProbeResultV3: Codable, Equatable {
69
+ let schema: String
70
+ let capability: String
71
+ let scope: CapabilityScope
72
+ let requestedLevel: String
73
+ let stage: CapabilityProbeStage
74
+ let state: CapabilityProbeState
75
+ let checkedAt: String
76
+ let reportId: String
77
+ let correlationId: String
78
+ let sessionId: String
79
+ let attemptId: Int
80
+ let terminal: Bool
81
+ let rootCause: String?
82
+ let blockers: [String]
83
+ let warnings: [String]
84
+ let retryable: Bool
85
+ let recoveryAction: String?
86
+ let source: String
87
+ let evidence: [String: JSONValue]
88
+
89
+ enum CodingKeys: String, CodingKey {
90
+ case schema, capability, scope, stage, state, terminal, blockers, warnings, retryable, source, evidence
91
+ case requestedLevel = "requested_level", checkedAt = "checked_at", reportId = "report_id"
92
+ case correlationId = "correlation_id", sessionId = "session_id", attemptId = "attempt_id"
93
+ case rootCause = "root_cause", recoveryAction = "recovery_action"
122
94
  }
95
+ }
123
96
 
124
- private static func isLegacyCodexLbSelected(
125
- json: [String: Any],
126
- mode: String?,
127
- provider: [String: Any]?
128
- ) -> Bool {
129
- if let provider = provider {
130
- let id = provider["id"] as? String
131
- let selected = provider["selected"] as? Bool
132
- if id == "codex-lb", selected == true { return true }
133
- // status.v2 marks built-in OpenAI as selected=false when model_provider is still codex-lb
134
- // (legacy destructive selection). Do not treat ordinary disabled/OAuth mode as legacy.
135
- if id == "openai", selected == false,
136
- ["cli-provider", "desktop-native-bridge"].contains(mode ?? "") {
137
- return true
138
- }
139
- return false
140
- }
141
- // Legacy status payloads used a top-level selected flag for codex-lb activation.
142
- return json["selected"] as? Bool == true
97
+ struct ScopeCapabilitySummaryV3: Codable, Equatable {
98
+ let schema: String
99
+ let scope: CapabilityScope
100
+ let state: CapabilityProbeState
101
+ let checkedAt: String
102
+ let capabilities: [String: CapabilityProbeResultV3]
103
+ let blockers: [String]
104
+ let warnings: [String]
105
+
106
+ enum CodingKeys: String, CodingKey { case schema, scope, state, capabilities, blockers, warnings; case checkedAt = "checked_at" }
107
+ }
108
+
109
+ struct CatalogSyncStateV2: Codable, Equatable {
110
+ let schema: String
111
+ let providerId: String
112
+ let state: String
113
+ let source: String?
114
+ let generation: String?
115
+ let digest: String?
116
+ let modelCount: Int?
117
+ let checkedAt: String?
118
+ let expiresAt: String?
119
+ let blockers: [String]
120
+ let warnings: [String]
121
+ let recoveryAction: String?
122
+
123
+ enum CodingKeys: String, CodingKey {
124
+ case schema, state, source, generation, digest, blockers, warnings
125
+ case providerId = "provider_id", modelCount = "model_count", checkedAt = "checked_at"
126
+ case expiresAt = "expires_at", recoveryAction = "recovery_action"
143
127
  }
128
+ }
144
129
 
145
- private static func isLegacyDestructive(_ json: [String: Any]) -> Bool {
146
- if json["legacy_destructive_state"] as? Bool == true
147
- || json["legacy_migration_required"] as? Bool == true {
148
- return true
149
- }
150
- if let blockers = json["blockers"] as? [String],
151
- blockers.contains(where: { $0.contains("legacy") && $0.contains("migration") }) {
152
- return true
153
- }
154
- if let status = json["status"] as? String {
155
- return status.contains("legacy") && (status.contains("required") || status.contains("destructive"))
156
- }
157
- return false
130
+ struct CombinedCatalogSyncStatusV1: Codable, Equatable {
131
+ let schema: String
132
+ let state: String
133
+ let generation: String?
134
+ let digest: String?
135
+ let modelCount: Int?
136
+ let routeCount: Int?
137
+ let conflictCount: Int
138
+ let checkedAt: String?
139
+ let providers: [String: CatalogSyncStateV2]
140
+ let blockers: [String]
141
+ let warnings: [String]
142
+ let recoveryAction: String?
143
+
144
+ enum CodingKeys: String, CodingKey {
145
+ case schema, state, generation, digest, providers, blockers, warnings
146
+ case modelCount = "model_count", routeCount = "route_count", conflictCount = "conflict_count"
147
+ case checkedAt = "checked_at", recoveryAction = "recovery_action"
158
148
  }
159
149
  }
160
150
 
161
- struct CapabilityVerificationTruth {
162
- static func deepEvidenceTrusted(in value: Any) -> Bool {
163
- if let object = value as? [String: Any] {
164
- if let validation = object["deep_evidence_validation"] as? [String: Any],
165
- validation["schema"] as? String == "sks.codex-lb-deep-evidence-validation.v1",
166
- validation["state"] as? String == "verified",
167
- validation["trusted"] as? Bool == true,
168
- (validation["blockers"] as? [Any])?.isEmpty == true { return true }
169
- return object.values.contains { deepEvidenceTrusted(in: $0) }
151
+ struct DesktopCapabilityReportV3: Codable, Equatable {
152
+ struct Execution: Codable, Equatable { let ok: Bool; let status: String; let blockers: [String] }
153
+ struct Summary: Codable, Equatable {
154
+ let bridgeReady: Bool
155
+ let activeRoutesReady: Bool
156
+ let levelSatisfied: Bool
157
+ let transportLevelSatisfied: Bool
158
+ let deepLevelSatisfied: Bool
159
+ let fullFeatureVerified: Bool
160
+ let inactiveProviderFailures: [String]
161
+ let blockers: [String]
162
+ let warnings: [String]
163
+ enum CodingKeys: String, CodingKey {
164
+ case blockers, warnings
165
+ case bridgeReady = "bridge_ready", activeRoutesReady = "active_routes_ready", levelSatisfied = "level_satisfied"
166
+ case transportLevelSatisfied = "transport_level_satisfied", deepLevelSatisfied = "deep_level_satisfied"
167
+ case fullFeatureVerified = "full_feature_verified", inactiveProviderFailures = "inactive_provider_failures"
170
168
  }
171
- if let array = value as? [Any] { return array.contains { deepEvidenceTrusted(in: $0) } }
172
- return false
173
169
  }
174
170
 
175
- static func blockers(in value: Any) -> [String] {
176
- var result: [String] = []
177
- collectBlockers(value, into: &result)
178
- return Array(NSOrderedSet(array: result).array.compactMap { $0 as? String })
171
+ let schema: String
172
+ let reportId: String
173
+ let correlationId: String
174
+ let sessionId: String
175
+ let requestedLevel: String
176
+ let checkedAt: String
177
+ let catalogGeneration: String?
178
+ let execution: Execution
179
+ let bridge: ScopeCapabilitySummaryV3
180
+ let nativeIdentity: ScopeCapabilitySummaryV3
181
+ let providers: [String: ScopeCapabilitySummaryV3]
182
+ let combinedCatalog: ScopeCapabilitySummaryV3
183
+ let summary: Summary
184
+ let catalogSync: CombinedCatalogSyncStatusV1
185
+
186
+ enum CodingKeys: String, CodingKey {
187
+ case schema, execution, bridge, providers, summary
188
+ case reportId = "report_id", correlationId = "correlation_id", sessionId = "session_id"
189
+ case requestedLevel = "requested_level", checkedAt = "checked_at", catalogGeneration = "catalog_generation"
190
+ case nativeIdentity = "native_identity", combinedCatalog = "combined_catalog", catalogSync = "catalog_sync"
179
191
  }
180
192
 
181
- private static func collectBlockers(_ value: Any, into result: inout [String]) {
182
- if let object = value as? [String: Any] {
183
- if let blockers = object["blockers"] as? [String] { result.append(contentsOf: blockers) }
184
- for child in object.values { collectBlockers(child, into: &result) }
185
- } else if let array = value as? [Any] {
186
- for child in array { collectBlockers(child, into: &result) }
187
- }
193
+ var maximumAttemptId: Int { allProbes.map(\.attemptId).max() ?? 0 }
194
+ var allProbes: [CapabilityProbeResultV3] { scopeSummaries.flatMap { $0.capabilities.values } }
195
+ var scopeSummaries: [ScopeCapabilitySummaryV3] {
196
+ [bridge, nativeIdentity, providers["codex-lb"], providers["openrouter"], combinedCatalog].compactMap { $0 }
188
197
  }
189
- }
190
198
 
191
- extension ProvidersViewController {
192
- func describeRoutingTruth(_ route: ProviderRoutingTruth.MeasuredRoute?) -> String {
193
- guard let route = route else { return "measured route unavailable · blocker=codex_lb_routing_truth_unavailable" }
194
- let host = route.actualHost ?? route.configuredHost ?? "host unavailable"
195
- let auth = route.authOutcome ?? "auth unmeasured"
196
- let measuredAt = route.measuredAt ?? "time unavailable"
197
- let latency = route.latencyMs.map { "\($0) ms" } ?? "latency unavailable"
198
- let http = route.httpStatus.map { " · HTTP \($0)" } ?? ""
199
- let blockers = route.blockers.isEmpty ? "" : " · blockers=\(route.blockers.prefix(3).joined(separator: ", "))"
200
- return "status=\(route.status) · host=\(host) · auth=\(auth) · measured=\(measuredAt) · latency=\(latency)\(http)\(blockers)"
199
+ static func decode(from json: [String: Any]) throws -> DesktopCapabilityReportV3 {
200
+ guard json["schema"] as? String == "sks.desktop-capabilities.v3",
201
+ json["catalog_sync"] is [String: Any] else {
202
+ throw ProviderFacadeError.schemaInvalid("capability_schema_invalid: catalog_sync missing")
203
+ }
204
+ do {
205
+ let report = try JSONDecoder().decode(Self.self, from: JSONSerialization.data(withJSONObject: json))
206
+ try report.validate()
207
+ return report
208
+ } catch let error as ProviderFacadeError { throw error }
209
+ catch { throw ProviderFacadeError.schemaInvalid("capability_schema_invalid: \(error)") }
201
210
  }
202
211
 
203
- func renderMeasuredRoutingBadge(_ route: ProviderRoutingTruth.MeasuredRoute?, routeExpected: Bool) {
204
- guard let route = route else {
205
- if routeExpected {
206
- ControlKit.setBadge(activeProviderBadge, text: "Codex LB · unverified · blocker=codex_lb_routing_truth_unavailable", tone: .warning)
212
+ private func validate() throws {
213
+ guard schema == "sks.desktop-capabilities.v3", !reportId.isEmpty, !correlationId.isEmpty, !sessionId.isEmpty,
214
+ ["shallow", "transport", "deep"].contains(requestedLevel),
215
+ providers["codex-lb"] != nil, providers["openrouter"] != nil,
216
+ bridge.scope == .bridge, nativeIdentity.scope == .nativeIdentity,
217
+ providers["codex-lb"]?.scope == .codexLb, providers["openrouter"]?.scope == .openRouter,
218
+ combinedCatalog.scope == .combinedCatalog else {
219
+ throw ProviderFacadeError.schemaInvalid("capability_schema_invalid")
220
+ }
221
+ guard catalogSync.schema == "sks.combined-catalog-sync.v1",
222
+ catalogSync.providers["codex-lb"]?.schema == "sks.catalog-sync-state.v2",
223
+ catalogSync.providers["openrouter"]?.schema == "sks.catalog-sync-state.v2" else {
224
+ throw ProviderFacadeError.schemaInvalid("capability_schema_invalid: catalog_sync invalid")
225
+ }
226
+ for scope in scopeSummaries {
227
+ guard scope.schema == "sks.scope-capability-summary.v1" else { throw ProviderFacadeError.schemaInvalid("capability_schema_invalid: scope summary invalid") }
228
+ for (key, probe) in scope.capabilities {
229
+ guard probe.schema == "sks.capability-probe.v3", probe.scope == scope.scope,
230
+ probe.capability == key, probe.reportId == reportId,
231
+ probe.correlationId == correlationId, probe.sessionId == sessionId, probe.attemptId >= 0 else {
232
+ throw ProviderFacadeError.schemaInvalid("capability_schema_invalid: probe identity invalid")
233
+ }
207
234
  }
208
- return
209
235
  }
210
- guard route.selected else { return }
211
- switch route.state {
212
- case .active:
213
- ControlKit.setBadge(activeProviderBadge, text: "Codex LB · active · \(describeRoutingTruth(route))", tone: .ok)
214
- case .degraded:
215
- ControlKit.setBadge(activeProviderBadge, text: "Codex LB · degraded · \(describeRoutingTruth(route))", tone: .warning)
216
- case .unverified:
217
- ControlKit.setBadge(activeProviderBadge, text: "Codex LB · unverified · \(describeRoutingTruth(route))", tone: .warning)
218
- case .inactive:
219
- break
236
+ let correlations = Set(allProbes.map(\.correlationId))
237
+ guard correlations.count == 1 else {
238
+ throw ProviderFacadeError.schemaInvalid("capability_schema_invalid: correlation mismatch")
239
+ }
240
+ guard catalogGeneration == catalogSync.generation else {
241
+ throw ProviderFacadeError.schemaInvalid("capability_schema_invalid: catalog generation mismatch")
220
242
  }
221
243
  }
222
244
  }
223
245
 
224
- struct CodexLbConnectTestTruth {
225
- static let schema = "sks.codex-lb-connect-test.v1"
226
- static let maximumAcceptedOutputTokens = 128
227
- static let maximumRenderedReplyCharacters = 240
228
-
229
- struct Success {
230
- let model: String
231
- let latencyMs: Int
232
- let responseId: String
233
- let reply: String
234
- let inputTokens: Int
235
- let outputTokens: Int
236
- let totalTokens: Int
237
- let httpStatus: Int
246
+ struct DesktopBridgeStatusV3Truth {
247
+ private static let keys: Set<String> = [
248
+ "schema", "checked_at", "correlation_id", "management", "service",
249
+ "http_probe", "websocket_probe", "native_identity", "providers", "routing",
250
+ "catalog_sync", "capabilities", "readiness", "recovery_actions"
251
+ ]
252
+ let raw: [String: Any]
253
+ let checkedAt: String
254
+ let correlationId: String
255
+ let capabilities: DesktopCapabilityReportV3?
238
256
 
239
- var renderedSummary: String {
240
- "Model \(model) · latency \(latencyMs) ms · tokens \(inputTokens) in / \(outputTokens) out / \(totalTokens) total · reply: \(reply)"
257
+ static func decode(from json: [String: Any]) throws -> DesktopBridgeStatusV3Truth {
258
+ guard Set(json.keys) == keys,
259
+ json["schema"] as? String == "sks.desktop-bridge-status.v3",
260
+ let checkedAt = nonempty(json["checked_at"]), let correlationId = nonempty(json["correlation_id"]),
261
+ let management = json["management"] as? [String: Any],
262
+ let service = json["service"] as? [String: Any],
263
+ let identity = json["native_identity"] as? [String: Any],
264
+ let providers = json["providers"] as? [String: Any],
265
+ let routing = json["routing"] as? [String: Any],
266
+ let catalog = json["catalog_sync"] as? [String: Any],
267
+ let readiness = json["readiness"] as? [String: Any],
268
+ json["recovery_actions"] is [String] else {
269
+ throw ProviderFacadeError.schemaInvalid("desktop_bridge_status_schema_invalid")
241
270
  }
242
- }
243
-
244
- static func success(from json: [String: Any]) -> Success? {
245
- guard validationFailure(from: json) == "none",
246
- let model = nonemptyString(json["model"]),
247
- let latencyMs = integer(json["latency_ms"]),
248
- let responseId = nonemptyString(json["response_id"]),
249
- let rawReply = nonemptyString(json["result"]),
250
- let usage = json["usage"] as? [String: Any],
251
- let inputTokens = integer(usage["input_tokens"]),
252
- let outputTokens = integer(usage["output_tokens"]),
253
- let totalTokens = integer(usage["total_tokens"]),
254
- let httpStatus = integer(json["http_status"]) else { return nil }
255
- return Success(
256
- model: model,
257
- latencyMs: latencyMs,
258
- responseId: responseId,
259
- reply: boundedReply(rawReply),
260
- inputTokens: inputTokens,
261
- outputTokens: outputTokens,
262
- totalTokens: totalTokens,
263
- httpStatus: httpStatus
264
- )
265
- }
266
-
267
- static func validationFailure(from json: [String: Any]) -> String {
268
- guard json["schema"] as? String == schema else { return "unexpected or missing schema" }
269
- guard json["ok"] as? Bool == true else { return publicFailure(from: json) }
270
- guard json["status"] as? String == "connected" else { return "status is not connected" }
271
- guard nonemptyString(json["model"]) != nil else { return "model evidence is missing" }
272
- guard let latencyMs = integer(json["latency_ms"]), latencyMs >= 0 else { return "latency evidence is invalid" }
273
- guard nonemptyString(json["response_id"]) != nil else { return "response id is missing" }
274
- guard nonemptyString(json["result"]) != nil else { return "returned reply is empty" }
275
- guard json["result_truncated"] as? Bool != nil else { return "result bound evidence is missing" }
276
- guard let httpStatus = integer(json["http_status"]), (200..<300).contains(httpStatus) else {
277
- return "HTTP success evidence is invalid"
271
+ let managed = management["managed"] as? Bool == true
272
+ let managementValid = managed
273
+ ? management["runtime"] as? String == "desktop-bridge" && management["reason"] is NSNull
274
+ : management["runtime"] is NSNull && ["not_installed", "stopped"].contains(management["state"] as? String ?? "")
275
+ guard managementValid,
276
+ service["installed"] is Bool, service["loaded"] is Bool, service["running"] is Bool,
277
+ nonempty(service["checked_at"]) != nil, identity["configured"] is Bool,
278
+ routing["fallback"] as? String == "none", readiness["ready"] is Bool,
279
+ ["ready", "awaiting_provider", "degraded", "blocked", "unmanaged"].contains(readiness["state"] as? String ?? ""),
280
+ profileValid(providers["codex-lb"], id: "codex-lb"),
281
+ profileValid(providers["openrouter"], id: "openrouter"),
282
+ transportProbeValid(json["http_probe"], websocket: false),
283
+ transportProbeValid(json["websocket_probe"], websocket: true) else {
284
+ throw ProviderFacadeError.schemaInvalid("desktop_bridge_status_schema_invalid")
278
285
  }
279
- guard let blockers = json["blockers"] as? [String], blockers.isEmpty else {
280
- return "success blockers are not empty"
286
+ if let policy = routing["policy"] as? [String: Any] {
287
+ guard policy["schema"] as? String == "sks.bridge-routing-policy.v1", policy["fallback"] as? String == "none" else {
288
+ throw ProviderFacadeError.schemaInvalid("desktop_bridge_status_schema_invalid: routing policy")
289
+ }
290
+ }
291
+ guard let data = try? JSONSerialization.data(withJSONObject: catalog),
292
+ let decodedCatalog = try? JSONDecoder().decode(CombinedCatalogSyncStatusV1.self, from: data),
293
+ decodedCatalog.schema == "sks.combined-catalog-sync.v1" else {
294
+ throw ProviderFacadeError.schemaInvalid("desktop_bridge_status_schema_invalid: catalog_sync")
281
295
  }
282
- guard let usage = json["usage"] as? [String: Any],
283
- let inputTokens = integer(usage["input_tokens"]), inputTokens >= 0,
284
- let outputTokens = integer(usage["output_tokens"]), outputTokens >= 0,
285
- let totalTokens = integer(usage["total_tokens"]), totalTokens >= 0,
286
- totalTokens == inputTokens + outputTokens else { return "token usage evidence is invalid" }
287
- guard outputTokens <= maximumAcceptedOutputTokens else { return "low-token request evidence is invalid" }
288
- return "none"
296
+ let report: DesktopCapabilityReportV3?
297
+ if json["capabilities"] is NSNull || json["capabilities"] == nil { report = nil }
298
+ else if let capabilities = json["capabilities"] as? [String: Any] {
299
+ report = try DesktopCapabilityReportV3.decode(from: capabilities)
300
+ } else { throw ProviderFacadeError.schemaInvalid("desktop_bridge_status_schema_invalid: capabilities") }
301
+ return DesktopBridgeStatusV3Truth(raw: json, checkedAt: checkedAt, correlationId: correlationId, capabilities: report)
289
302
  }
290
303
 
291
- static func boundedReply(_ value: String) -> String {
292
- let compact = value.split(whereSeparator: { $0.isWhitespace }).joined(separator: " ")
293
- guard compact.count > maximumRenderedReplyCharacters else { return compact }
294
- return String(compact.prefix(maximumRenderedReplyCharacters)) + "…"
304
+ private static func transportProbeValid(_ value: Any?, websocket: Bool) -> Bool {
305
+ if value is NSNull { return true }
306
+ guard let probe = value as? [String: Any] else { return false }
307
+ let expected: Set<String> = websocket
308
+ ? ["schema", "state", "terminal_stage", "root_cause", "status_code", "negotiated_protocol",
309
+ "upgrade_verified", "protocol_verified", "frame_round_trip_verified", "clean_close_verified",
310
+ "latency_ms", "blockers", "warnings"]
311
+ : ["schema", "state", "terminal_stage", "root_cause", "status_code", "latency_ms", "blockers", "warnings"]
312
+ guard Set(probe.keys) == expected,
313
+ probe["schema"] as? String == (websocket ? "sks.desktop-bridge-websocket-probe.v2" : "sks.desktop-bridge-http-probe.v1"),
314
+ let state = probe["state"] as? String,
315
+ let stage = probe["terminal_stage"] as? String,
316
+ probe["root_cause"] is NSNull || probe["root_cause"] is String,
317
+ probe["status_code"] is NSNull || probe["status_code"] is NSNumber,
318
+ probe["latency_ms"] is NSNull || probe["latency_ms"] is NSNumber,
319
+ probe["blockers"] is [String], probe["warnings"] is [String] else { return false }
320
+ if websocket {
321
+ guard ["not_attempted", "verified", "degraded", "blocked", "failed", "unsupported"].contains(state),
322
+ ["tcp_connect", "websocket_upgrade", "websocket_protocol", "frame_round_trip", "clean_close", "complete"].contains(stage),
323
+ probe["negotiated_protocol"] is NSNull || probe["negotiated_protocol"] is String,
324
+ probe["upgrade_verified"] is Bool, probe["protocol_verified"] is Bool,
325
+ probe["frame_round_trip_verified"] is Bool, probe["clean_close_verified"] is Bool else { return false }
326
+ if state == "verified" {
327
+ return stage == "complete" && probe["root_cause"] is NSNull
328
+ && probe["upgrade_verified"] as? Bool == true
329
+ && probe["protocol_verified"] as? Bool == true
330
+ && probe["frame_round_trip_verified"] as? Bool == true
331
+ && probe["clean_close_verified"] as? Bool == true
332
+ }
333
+ return state != "not_attempted"
334
+ || probe["root_cause"] is NSNull && (probe["blockers"] as? [String])?.isEmpty == true
335
+ }
336
+ guard ["verified", "blocked", "failed", "unsupported"].contains(state),
337
+ ["tcp_connect", "http_health", "complete"].contains(stage) else { return false }
338
+ return state != "verified" || stage == "complete" && probe["root_cause"] is NSNull
295
339
  }
296
340
 
297
- private static func publicFailure(from json: [String: Any]) -> String {
298
- if let blockers = json["blockers"] as? [String], !blockers.isEmpty {
299
- return "blocked: \(blockers.prefix(3).joined(separator: ", "))"
300
- }
301
- if let error = nonemptyString(json["error"]) { return boundedReply(error) }
302
- if let status = nonemptyString(json["status"]) { return "status=\(boundedReply(status))" }
303
- return "ok was not true"
341
+ private static func profileValid(_ value: Any?, id: String) -> Bool {
342
+ guard let profile = value as? [String: Any], profile["schema"] as? String == "sks.bridge-provider-profile-status.v1",
343
+ profile["provider_id"] as? String == id, profile["enabled"] is Bool,
344
+ profile["credential"] is [String: Any], profile["endpoint"] is [String: Any],
345
+ let catalog = profile["catalog"] as? [String: Any],
346
+ let capabilities = profile["capabilities"] as? [String: Any] else { return false }
347
+ return catalog["schema"] as? String == "sks.catalog-sync-state.v2"
348
+ && capabilities["schema"] as? String == "sks.scope-capability-summary.v1"
349
+ && capabilities["scope"] as? String == "provider:\(id)"
304
350
  }
305
351
 
306
- private static func nonemptyString(_ value: Any?) -> String? {
307
- guard let raw = value as? String else { return nil }
308
- let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
352
+ private static func nonempty(_ value: Any?) -> String? {
353
+ guard let string = value as? String else { return nil }
354
+ let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines)
309
355
  return trimmed.isEmpty ? nil : trimmed
310
356
  }
357
+ }
358
+
359
+ struct CapabilityDisplayRow: Equatable {
360
+ let scope: CapabilityScope
361
+ let capability: String
362
+ let state: CapabilityProbeState
363
+ let route: String
364
+ let oauthRequirement: String
365
+ let checkedAt: String
366
+ let stage: CapabilityProbeStage
367
+ let rootCause: String?
368
+ let recoveryAction: String?
369
+
370
+ static func rows(from report: DesktopCapabilityReportV3) -> [CapabilityDisplayRow] {
371
+ report.scopeSummaries.flatMap { summary in
372
+ summary.capabilities.keys.sorted().compactMap { key in
373
+ guard let probe = summary.capabilities[key] else { return nil }
374
+ let route = evidenceString(probe.evidence["route"]) ?? evidenceString(probe.evidence["provider_route"]) ?? "none"
375
+ let oauth = evidenceString(probe.evidence["oauth_requirement"]) ?? (summary.scope == .nativeIdentity ? "required" : "not required")
376
+ return CapabilityDisplayRow(scope: summary.scope, capability: key, state: probe.state, route: route,
377
+ oauthRequirement: oauth, checkedAt: probe.checkedAt, stage: probe.stage,
378
+ rootCause: probe.rootCause, recoveryAction: probe.recoveryAction)
379
+ }
380
+ }
381
+ }
311
382
 
312
- private static func integer(_ value: Any?) -> Int? {
313
- guard !(value is Bool), let number = value as? NSNumber else { return nil }
314
- let double = number.doubleValue
315
- guard double.isFinite,
316
- double.rounded(.towardZero) == double,
317
- double >= Double(Int.min),
318
- double <= Double(Int.max) else { return nil }
319
- return number.intValue
383
+ private static func evidenceString(_ value: JSONValue?) -> String? {
384
+ if case .string(let raw)? = value { return raw }
385
+ return nil
320
386
  }
321
387
  }