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,499 +1,136 @@
1
1
  import Cocoa
2
2
 
3
3
  extension ProvidersViewController {
4
- func makeActiveProviderCard() -> NSBox {
5
- NativeView.card(
6
- title: "Active Provider",
7
- subtitle: "Live provider path. Codex LB uses Authorization: Bearer. Switch with Use Codex LB, OpenRouter Activate, or Advanced · Use ChatGPT OAuth Only.",
8
- views: [activeProviderBadge, oauthCredentialStatus]
9
- )
10
- }
11
-
12
- /// One-line truth for "what is Codex using right now", fed by the three
13
- /// independent status probes as their responses arrive.
14
- func renderActiveProviderSummary() {
15
- if openRouterSelectedNow {
16
- let model = openRouterActiveModel.isEmpty || openRouterActiveModel == "unset" ? "model unset" : openRouterActiveModel
17
- ControlKit.setBadge(activeProviderBadge, text: "OpenRouter · \(model)", tone: .ok)
18
- } else if routerSelectedNow {
19
- let model = routerActiveModel.isEmpty ? "model unset" : routerActiveModel
20
- ControlKit.setBadge(activeProviderBadge, text: "Multi-Provider Router · \(model)", tone: .ok)
21
- } else if desktopFullRoutingNow {
22
- ControlKit.setBadge(
23
- activeProviderBadge,
24
- text: chatgptOauthPresentNow
25
- ? "Codex LB · Desktop Bridge · ChatGPT OAuth + built-in OpenAI"
26
- : "Codex LB · Desktop Bridge · ChatGPT OAuth missing — run codex login",
27
- tone: chatgptOauthPresentNow ? .ok : .warning
28
- )
29
- } else if codexLbSelectedNow {
30
- // Measured CLI selection is not legacy migration (that badge comes only from refresh snapshot markers).
31
- if codexLbProvedNow {
32
- ControlKit.setBadge(activeProviderBadge, text: "Codex LB · CLI · verified", tone: .ok)
33
- } else {
34
- ControlKit.setBadge(activeProviderBadge, text: "Codex LB · CLI · selected · connection unproved", tone: .warning)
35
- }
36
- } else if chatgptOauthPresentNow {
37
- ControlKit.setBadge(activeProviderBadge, text: "ChatGPT OAuth mode · built-in OpenAI models", tone: .neutral)
38
- } else {
39
- ControlKit.setBadge(activeProviderBadge, text: "ChatGPT OAuth mode unavailable · run codex login", tone: .warning)
40
- }
41
- }
42
-
43
- func makeOpenRouterCard() -> NSBox {
44
- openRouterModelField.delegate = self
45
- openRouterModelPopup.addItem(withTitle: "Choose from catalog…")
46
- openRouterModelPopup.target = self
47
- openRouterModelPopup.action = #selector(selectOpenRouterModel(_:))
48
- openRouterModelPopup.setAccessibilityLabel("OpenRouter model catalog")
49
- openRouterModelPopup.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
50
- openRouterModelPopup.widthAnchor.constraint(equalToConstant: 230).isActive = true
51
- openRouterModelPopup.isEnabled = false
52
- openRouterModelField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
53
-
54
- let refreshModels = NativeView.button("Refresh Models", target: self, action: #selector(refreshOpenRouterModelsAction(_:)))
55
- openRouterRefreshButton = refreshModels
56
- let saveKey = NativeView.button("Save OpenRouter key…", target: self, action: #selector(saveOpenRouterKey))
57
- let test = NativeView.button("Test Model", target: self, action: #selector(testOpenRouterConnection))
58
- test.setAccessibilityLabel("Test the selected OpenRouter model")
59
- let activate = ControlKit.primaryButton("Activate Selected Model", target: self, action: #selector(useOpenRouter))
60
- activate.setAccessibilityLabel("Activate selected OpenRouter model and restart Codex App")
61
- let restorePrevious = NativeView.button("Restore previous provider", target: self, action: #selector(restorePreviousDesktopRouting))
62
- restorePrevious.setAccessibilityLabel("Restore the previous Codex provider so hidden chats and picker state return")
63
- // Stays disabled until openrouter-status reports a restorable
64
- // cross-provider snapshot, so the button never promises a restore the
65
- // CLI would reject with snapshot_missing.
66
- restorePrevious.isEnabled = false
67
- openRouterRestoreButton = restorePrevious
68
- registerProviderAction(refreshModels, id: "sks-provider-refresh-openrouter-catalog")
69
- registerProviderAction(saveKey, id: "sks-provider-save-openrouter-key")
70
- registerProviderAction(test, id: "sks-provider-test-openrouter")
71
- registerProviderAction(activate, id: "sks-provider-activate-openrouter")
72
- registerProviderAction(restorePrevious, id: "sks-provider-restore-previous")
73
- actionButtons += [refreshModels, saveKey, test, activate, restorePrevious]
74
-
75
- let catalogLabel = NSTextField(labelWithString: "Catalog")
76
- catalogLabel.setContentHuggingPriority(.required, for: .horizontal)
77
- let manualLabel = NSTextField(labelWithString: "Model ID")
78
- manualLabel.setContentHuggingPriority(.required, for: .horizontal)
79
- return NativeView.card(
80
- title: "OpenRouter",
81
- subtitle: "Saving the key prepares OpenRouter but does not switch providers. Activate switches Desktop routing; SKS remaps the local thread sidebar so prior provider chats stay visible, and keeps a one-click restore snapshot.",
4
+ func describeOpenRouterStatus(_ json: [String: Any]) -> String {
5
+ let enabled = json["enabled"] as? Bool == true
6
+ return "OpenRouter profile · \(enabled ? "enabled" : "disabled")"
7
+ }
8
+
9
+ func makeProviderCredentialsCard() -> NSBox {
10
+ let lbConfigure = NativeView.button(ProviderReconnectLabel.codexLb, target: self, action: #selector(configureCodexLbProfile))
11
+ let lbValidate = NativeView.button("Validate", target: self, action: #selector(validateCodexLbProfile))
12
+ let lbToggle = NativeView.button("Enable / Disable", target: self, action: #selector(toggleCodexLbProfile))
13
+ let orConfigure = NativeView.button(ProviderReconnectLabel.openRouter, target: self, action: #selector(configureOpenRouterProfile))
14
+ let orValidate = NativeView.button("Validate", target: self, action: #selector(validateOpenRouterProfile))
15
+ let orToggle = NativeView.button("Enable / Disable", target: self, action: #selector(toggleOpenRouterProfile))
16
+ let codexSignIn = NativeView.button(ProviderReconnectLabel.codexSignIn, target: self, action: #selector(openCodexSignInAction))
17
+ registerProviderAction(lbConfigure, id: "sks-provider-reconnect-codex-lb")
18
+ registerProviderAction(lbValidate, id: "sks-provider-validate-codex-lb")
19
+ registerProviderAction(lbToggle, id: "sks-provider-toggle-codex-lb")
20
+ registerProviderAction(orConfigure, id: "sks-provider-reconnect-openrouter")
21
+ registerProviderAction(orValidate, id: "sks-provider-validate-openrouter")
22
+ registerProviderAction(orToggle, id: "sks-provider-toggle-openrouter")
23
+ registerProviderAction(codexSignIn, id: "sks-provider-open-codex-sign-in")
24
+ providerButtons["codex-lb"] = [lbConfigure, lbValidate, lbToggle]
25
+ providerButtons["openrouter"] = [orConfigure, orValidate, orToggle]
26
+ actionButtons += [lbConfigure, lbValidate, lbToggle, orConfigure, orValidate, orToggle, codexSignIn]
27
+ let card = NativeView.card(
28
+ title: "Provider Credentials",
29
+ subtitle: "Profiles coexist. Enable, disable, validate, rotate, or remove one credential without changing the other or Codex-owned ChatGPT OAuth.",
82
30
  views: [
83
- openRouterCredentialStatus, openRouterKeychainStatus,
84
- openRouterActiveStatus,
85
- NativeView.row([catalogLabel, openRouterModelPopup, refreshModels]),
86
- openRouterCatalogStatus,
87
- NativeView.row([manualLabel, openRouterModelField]),
88
- NativeView.row([saveKey, test, activate]),
89
- NativeView.row([restorePrevious]),
90
- openRouterStatus
31
+ NativeView.sectionTitle("Codex-LB"), codexLbKeychainStatus, cliProviderStatus,
32
+ ControlKit.actionRow([lbConfigure, lbValidate, lbToggle]),
33
+ NativeView.sectionTitle("OpenRouter"), openRouterKeychainStatus, openRouterCredentialStatus,
34
+ ControlKit.actionRow([orConfigure, orValidate, orToggle]),
35
+ oauthCredentialStatus,
36
+ ControlKit.actionRow([codexSignIn])
91
37
  ]
92
38
  )
39
+ card.setAccessibilityIdentifier("sks-provider-card-credentials")
40
+ return card
93
41
  }
94
42
 
95
- func refreshOpenRouterStatus() {
96
- processClient.run(["codex-app", "openrouter-status", "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
97
- guard let self = self else { return }
98
- guard let json = self.json(result.output) else {
99
- if !self.busy {
100
- self.openRouterCredentialStatus.stringValue = "Credential: status unavailable — no saved key was assumed."
101
- self.openRouterActiveStatus.stringValue = "Active provider: status unavailable — no active provider was assumed."
102
- }
103
- // An unreadable status cannot vouch for a restore point, so the
104
- // button must not keep an enabled state from an earlier reply.
105
- self.openRouterRestoreAvailable = false
106
- self.openRouterRestoreButton?.isEnabled = false
107
- self.openRouterRestoreButton?.toolTip = "Restore availability is unknown — refresh the OpenRouter status first."
108
- return
109
- }
110
- let keyPresent = json["key_present"] as? Bool == true
111
- let keyValidated = json["key_validated"] as? Bool == true
112
- || json["credential_validated"] as? Bool == true
113
- let providerPresent = json["provider_present"] as? Bool == true
114
- let selected = json["selected"] as? Bool == true
115
- let activeModel = json["model"] as? String ?? "unset"
116
- let current = self.openRouterModelField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
117
- if selected, activeModel != "unset", !self.openRouterModelSelectionPending, current.isEmpty {
118
- self.openRouterModelField.stringValue = activeModel
119
- }
120
- self.synchronizeOpenRouterPopupSelection()
121
- self.openRouterSelectedNow = selected
122
- self.openRouterCredentialValidatedNow = keyValidated
123
- self.openRouterActiveModel = activeModel
124
- self.renderActiveProviderSummary()
125
- self.openRouterCredentialStatus.stringValue = keyValidated
126
- ? "Credential: validated\(providerPresent ? " · provider route ready" : " · provider setup needs repair")"
127
- : keyPresent
128
- ? "Credential: stored but not validated · models remain hidden until authenticated refresh succeeds"
129
- : "Credential: missing · save a key before testing or activation"
130
- self.openRouterActiveStatus.stringValue = selected
131
- ? "Active provider: OpenRouter · main model \(activeModel)"
132
- : "Active provider: not OpenRouter · saved credentials remain available"
133
- if !self.openRouterActionRan {
134
- let summary = self.describeOpenRouterStatus(json)
135
- self.openRouterStatus.stringValue = result.code == 0
136
- ? summary
137
- : "\(summary) \(self.structuredPublicDetail(json, fallback: result.output, codePrefix: "E-OR"))"
138
- }
139
- self.openRouterCredentialStatus.textColor = keyValidated ? .secondaryLabelColor : .systemOrange
140
- if !keyValidated { self.clearOpenRouterModels(reason: "Credential not validated · model and child-agent lists are hidden. Choose Reconnect, then Refresh Models.") }
141
- self.openRouterActiveStatus.textColor = selected ? .systemGreen : .secondaryLabelColor
142
- self.openRouterRestoreAvailable = json["previous_routing_restore_available"] as? Bool == true
143
- self.openRouterRestoreButton?.isEnabled = !self.busy && self.openRouterRestoreAvailable
144
- self.openRouterRestoreButton?.toolTip = self.openRouterRestoreAvailable
145
- ? "Restore the snapshotted pre-OpenRouter provider, model, and catalog."
146
- : "No previous provider snapshot to restore."
147
- }
148
- }
149
-
150
- func describeOpenRouterStatus(_ json: [String: Any]) -> String {
151
- let keyPresent = json["key_present"] as? Bool == true
152
- let providerPresent = json["provider_present"] as? Bool == true
153
- let selected = json["selected"] as? Bool == true
154
- let activeModel = json["model"] as? String ?? "unset"
155
- if !keyPresent { return "OpenRouter: key missing. Next: save a key, refresh models, then test the model." }
156
- if !providerPresent { return "OpenRouter: provider missing. Next: save the key again to repair the provider block." }
157
- if selected { return "OpenRouter: active · main model \(activeModel). Next: test after changing models, or choose another provider to switch away." }
158
- return "OpenRouter: key stored · activation model \(selectedOpenRouterModel()) · not selected. Next: test, then activate the selected model."
159
- }
160
-
161
- @objc func refreshOpenRouterModelsAction(_ sender: NSButton) { refreshOpenRouterModels() }
162
-
163
- func refreshOpenRouterModels() {
164
- guard !catalogRefreshInFlight else { return }
165
- catalogRefreshInFlight = true
166
- clearOpenRouterModels(reason: "Validating the saved credential before exposing models…")
167
- openRouterRefreshButton?.isEnabled = false
168
- openRouterCatalogStatus.stringValue = "Loading OpenRouter model catalog…"
169
- processClient.run(["codex-app", "openrouter-models", "--ids-only", "--json"], timeout: NativeView.mutationTimeout) { [weak self] result in
170
- guard let self = self else { return }
171
- self.catalogRefreshInFlight = false
172
- self.openRouterRefreshButton?.isEnabled = !self.busy
173
- guard let json = self.json(result.output) else {
174
- self.clearOpenRouterModels(reason: "Catalog unavailable · no model list is exposed. Next: reconnect the key or retry.")
175
- return
176
- }
177
- guard result.code == 0, json["ok"] as? Bool == true else {
178
- self.clearOpenRouterModels(reason: "Catalog unavailable · \(self.structuredPublicDetail(json, fallback: result.output, codePrefix: "E-OR")) No model list is exposed.")
179
- return
180
- }
181
- guard json["authenticated"] as? Bool == true else {
182
- self.clearOpenRouterModels(reason: "Credential validation failed · model and child-agent lists were withdrawn. Choose Reconnect OpenRouter credential.")
183
- return
184
- }
185
- let models = self.openRouterModelIds(json)
186
- guard !models.isEmpty else {
187
- self.clearOpenRouterModels(reason: "Authenticated catalog returned no selectable models · no model list is exposed.")
188
- return
189
- }
190
- self.openRouterCredentialValidatedNow = true
191
- self.openRouterModels = models
192
- self.openRouterModelPopup.removeAllItems()
193
- self.openRouterModelPopup.addItem(withTitle: "Choose from \(models.count) models…")
194
- self.openRouterModelPopup.addItems(withTitles: models)
195
- self.synchronizeOpenRouterPopupSelection()
196
- self.openRouterModelPopup.isEnabled = !self.busy
197
- self.openRouterCatalogStatus.stringValue = "Catalog ready · \(models.count) models · saved key authenticated. Selecting one copies its id into the editable field."
198
- }
199
- }
200
-
201
- private func clearOpenRouterModels(reason: String) {
202
- openRouterCredentialValidatedNow = false
203
- openRouterModels = []
204
- openRouterModelPopup.removeAllItems()
205
- openRouterModelPopup.addItem(withTitle: "Models hidden until credential validation")
206
- openRouterModelPopup.isEnabled = false
207
- openRouterModelField.stringValue = ""
208
- openRouterModelField.isEnabled = false
209
- openRouterCatalogStatus.stringValue = reason
210
- openRouterCatalogStatus.textColor = .systemOrange
211
- }
212
-
213
- private func openRouterModelIds(_ json: [String: Any]) -> [String] {
214
- let raw = (json["models"] as? [Any])
215
- ?? (json["data"] as? [Any])
216
- ?? ((json["catalog"] as? [String: Any])?["models"] as? [Any])
217
- ?? []
218
- let values = raw.compactMap { entry -> String? in
219
- if let value = entry as? String { return value }
220
- guard let row = entry as? [String: Any] else { return nil }
221
- return (row["id"] as? String) ?? (row["model"] as? String) ?? (row["slug"] as? String)
222
- }.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty }
223
- return Array(Set(values)).sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
224
- }
225
-
226
- @objc private func selectOpenRouterModel(_ sender: NSPopUpButton) {
227
- guard sender.indexOfSelectedItem > 0, let model = sender.titleOfSelectedItem else { return }
228
- openRouterModelSelectionPending = true
229
- openRouterActionRan = true
230
- openRouterModelField.stringValue = model
231
- openRouterStatus.stringValue = "Selected \(model) from the catalog. Next: test the connection; activation remains unchanged."
232
- }
233
-
234
- func controlTextDidChange(_ notification: Notification) {
235
- guard let field = notification.object as? NSTextField else { return }
236
- if field === openRouterModelField {
237
- openRouterModelSelectionPending = true
238
- openRouterActionRan = true
239
- synchronizeOpenRouterPopupSelection()
240
- openRouterStatus.stringValue = "Manual model selection changed. Next: test the connection; activation remains unchanged."
241
- } else if field === multiProvider.model {
242
- multiProvider.modelSelectionPending = true
243
- synchronizeMultiProviderPopupSelection()
244
- multiProvider.status.stringValue = "Manual routed model selection changed. Check the router before activation."
245
- multiProvider.status.textColor = .secondaryLabelColor
246
- }
247
- }
248
-
249
- private func synchronizeOpenRouterPopupSelection() {
250
- let current = selectedOpenRouterModel()
251
- if let index = openRouterModels.firstIndex(of: current) {
252
- openRouterModelPopup.selectItem(at: index + 1)
253
- } else {
254
- openRouterModelPopup.selectItem(at: 0)
255
- }
256
- }
257
-
258
- @objc func saveOpenRouterKey() {
259
- guard let window = view.window else { return }
260
- openRouterActionRan = true
261
- promptForSecretKey(
262
- window: window,
263
- sheetTitle: "OpenRouter API Key",
264
- sheetMessage: "Paste your OpenRouter API key. It is sent through stdin and never logged. Saving does not switch the active provider.",
265
- placeholder: "sk-or-…",
266
- args: ["codex-app", "set-openrouter-key", "--api-key-stdin", "--json"],
267
- kind: "openrouter-set-key",
268
- title: "Save OpenRouter key",
269
- credential: .openRouterApiKey,
270
- statusLabel: openRouterStatus,
271
- successSummary: "OpenRouter key saved",
272
- failSummary: "OpenRouter key save failed",
273
- codePrefix: "E-OR"
274
- )
275
- }
276
-
277
- @objc private func testOpenRouterConnection() {
278
- let model = selectedOpenRouterModel()
279
- openRouterActionRan = true
280
- guard !model.isEmpty else {
281
- openRouterStatus.stringValue = "Connection test blocked · model id is empty. Next: choose or enter a model."
43
+ func renderProviderProfiles(_ json: [String: Any]) {
44
+ guard let providers = json["providers"] as? [String: Any] else {
45
+ cliProviderStatus.stringValue = "Codex-LB · schema unavailable · no state assumed"
46
+ openRouterCredentialStatus.stringValue = "OpenRouter · schema unavailable · no state assumed"
47
+ cliProviderStatus.textColor = .systemRed
48
+ openRouterCredentialStatus.textColor = .systemRed
282
49
  return
283
50
  }
284
- guard !busy else { openRouterStatus.stringValue = "Another provider action is already running."; return }
285
- guard let snapshot = operations.begin(kind: "openrouter-test", mutationGroup: nil, summary: "Test OpenRouter model") else {
286
- openRouterStatus.stringValue = "Another guarded mutation is already running. Wait or open Diagnostics."
287
- return
288
- }
289
- setBusy(true)
290
- openRouterStatus.stringValue = "Testing OpenRouter with \(model)…"
291
- _ = operations.update(snapshot, state: .running, stage: "testing", progress: nil, summary: "Test OpenRouter model")
292
- processClient.run(["codex-app", "openrouter-test", "--model", model, "--json"], timeout: NativeView.mutationTimeout) { [weak self] result in
293
- guard let self = self else { return }
294
- self.setBusy(false)
295
- let json = self.json(result.output)
296
- let ok = result.code == 0 && json?["ok"] as? Bool == true
297
- let status = json?["status"] as? String ?? (ok ? "connected" : "failed")
298
- let detail = self.structuredPublicDetail(json, fallback: result.output, codePrefix: "E-OR")
299
- _ = self.operations.update(snapshot, state: ok ? .succeeded : .failed, stage: "complete", progress: 1, summary: ok ? "OpenRouter connection ready" : "OpenRouter connection needs action")
300
- self.openRouterStatus.stringValue = ok
301
- ? "Connection test passed · \(model) · \(status). Next: activate it if you want it as the main model."
302
- : "Connection test failed · \(model) · \(status) · \(detail)"
303
- self.refreshOpenRouterStatus()
304
- }
305
- }
306
-
307
- @objc func useOpenRouter() {
308
- let model = selectedOpenRouterModel()
309
- openRouterActionRan = true
310
- guard !model.isEmpty else {
311
- openRouterStatus.stringValue = "Enter an OpenRouter model id, then click Activate Selected Model."
312
- return
313
- }
314
- guard !busy else { openRouterStatus.stringValue = "Another provider action is already running."; return }
51
+ renderProviderProfile(providers["codex-lb"] as? [String: Any], id: "codex-lb", label: cliProviderStatus)
52
+ renderProviderProfile(providers["openrouter"] as? [String: Any], id: "openrouter", label: openRouterCredentialStatus)
53
+ let identity = json["native_identity"] as? [String: Any]
54
+ let identityState = identity?["state"] as? String ?? "not_attempted"
55
+ oauthCredentialStatus.stringValue = "ChatGPT OAuth Identity · \(identityState) · Codex-owned; never copied into provider profiles"
56
+ oauthCredentialStatus.textColor = identityState == "verified" ? .systemGreen : .secondaryLabelColor
57
+ }
58
+
59
+ private func renderProviderProfile(_ profile: [String: Any]?, id: String, label: NSTextField) {
60
+ guard profile?["schema"] as? String == "sks.bridge-provider-profile-status.v1",
61
+ profile?["provider_id"] as? String == id,
62
+ let credential = profile?["credential"] as? [String: Any],
63
+ let catalog = profile?["catalog"] as? [String: Any],
64
+ catalog["schema"] as? String == "sks.catalog-sync-state.v2" else {
65
+ label.stringValue = "\(displayProvider(id)) · capability schema invalid"
66
+ label.textColor = .systemRed; return
67
+ }
68
+ let enabled = profile?["enabled"] as? Bool == true
69
+ providerEnabled[id] = enabled
70
+ let credentialState = credential["state"] as? String ?? "unavailable"
71
+ let endpoint = profile?["endpoint"] as? [String: Any]
72
+ let origin = ProviderSecretRedactor.redactEndpoint(endpoint?["origin_redacted"] as? String ?? "not configured")
73
+ let auth = endpoint?["auth_transport"] as? String ?? "unreported"
74
+ let catalogState = catalog["state"] as? String ?? "failed"
75
+ let checked = credential["checked_at"] as? String ?? catalog["checked_at"] as? String ?? "never"
76
+ label.stringValue = "\(displayProvider(id)) · \(enabled ? "enabled" : "disabled") · credential \(credentialState) · endpoint \(origin) · auth \(auth) · catalog \(catalogState) · last \(checked)"
77
+ label.textColor = credentialState == "ready" && enabled ? .systemGreen
78
+ : ["rejected", "unavailable"].contains(credentialState) ? .systemRed : .systemOrange
79
+ }
80
+
81
+ @objc func configureCodexLbProfile() {
315
82
  guard let window = view.window else { return }
316
- AlertFactory.confirmSheet(
317
- window: window,
318
- title: "Activate OpenRouter?",
319
- message: "\(model) becomes the Codex main model and Codex App restarts. SKS remaps the Desktop thread sidebar so prior provider chats stay listed, snapshots the previous provider for Restore, and merges the model cache instead of wiping it. Codex Desktop may still label third-party models as Custom.",
320
- destructive: false
321
- ) { [weak self] approved in
322
- guard let self = self, approved else { return }
323
- self.performUseOpenRouter(model: model)
83
+ AlertFactory.textSheet(window: window, title: "Codex-LB Endpoint", message: "Enter the Codex-LB endpoint. Its credential is stored independently from OpenRouter and ChatGPT OAuth.", secure: false, placeholder: "https://lb.example.com") { [weak self] host in
84
+ guard let self = self, let host = host?.trimmingCharacters(in: .whitespacesAndNewlines), !host.isEmpty else { return }
85
+ self.promptForSecretKey(window: window, sheetTitle: "Codex-LB API Key", sheetMessage: "Paste the Codex-LB key. It is sent through stdin and is never rendered in status, receipts, or logs.", placeholder: "sk-clb-…", args: ["bridge", "provider", "configure", "codex-lb", "--host", host, "--api-key-stdin", "--json"], kind: "bridge-provider-configure-codex-lb", title: "Configure Codex-LB profile", credential: .codexLbApiKey, statusLabel: self.cliProviderStatus, successSummary: "Codex-LB profile configured; OpenRouter preserved", failSummary: "Codex-LB profile configuration failed", providerId: "codex-lb")
324
86
  }
325
87
  }
326
88
 
327
- @objc func restorePreviousDesktopRouting() {
328
- openRouterActionRan = true
329
- guard !busy else { openRouterStatus.stringValue = "Another provider action is already running."; return }
89
+ @objc func configureOpenRouterProfile() {
330
90
  guard let window = view.window else { return }
331
- AlertFactory.confirmSheet(
332
- window: window,
333
- title: "Restore previous provider?",
334
- message: "Restores the last pre-OpenRouter Desktop provider/model/catalog snapshot, reverses sidebar remap when present, and restarts Codex App.",
335
- destructive: false
336
- ) { [weak self] approved in
337
- guard let self = self, approved else { return }
338
- self.performRestorePreviousDesktopRouting()
339
- }
340
- }
341
-
342
- private func performRestorePreviousDesktopRouting() {
343
- guard let snapshot = operations.begin(kind: "desktop-routing-restore", mutationGroup: "codex-config", summary: "Restore previous Desktop provider") else {
344
- openRouterStatus.stringValue = "Another guarded mutation is already running. Wait or open Diagnostics."
345
- return
346
- }
347
- setBusy(true)
348
- openRouterStatus.stringValue = "Restoring previous Desktop provider and restarting Codex App…"
349
- _ = operations.update(snapshot, state: .running, stage: "restoring", progress: nil, summary: "Restore previous Desktop provider")
350
- processClient.run(["codex-app", "restore-desktop-routing", "--restart-app", "--json"], timeout: NativeView.mutationTimeout) { [weak self] result in
351
- guard let self = self else { return }
352
- self.setBusy(false)
353
- let json = self.json(result.output)
354
- let ok = result.code == 0 && json?["ok"] as? Bool == true
355
- let status = json?["status"] as? String ?? (ok ? "restored" : "failed")
356
- let detail = self.structuredPublicDetail(json, fallback: result.output, codePrefix: "E-OR")
357
- // Routing rolls back before the sidebar retag, so a blocked retag is
358
- // partial success: the snapshot is kept and re-running finishes it.
359
- let sidebarBlocked = status.hasPrefix("restored_sidebar")
360
- _ = self.operations.update(snapshot, state: ok ? .succeeded : .failed, stage: "complete", progress: 1, summary: ok ? "Previous Desktop provider restored" : sidebarBlocked ? "Desktop provider restored; sidebar retag pending" : "Desktop provider restore needs action")
361
- self.openRouterStatus.stringValue = ok
362
- ? "Previous provider restored · \(status). Fully quit and reopen Codex App if chats or the picker still look stale."
363
- : sidebarBlocked
364
- ? "Provider restored, but prior-provider chats stay hidden · fully quit Codex App, then run Restore previous provider again."
365
- : "Restore failed · \(status) · \(detail)"
366
- self.refreshOpenRouterStatus()
367
- self.refresh()
368
- }
91
+ promptForSecretKey(window: window, sheetTitle: "OpenRouter API Key", sheetMessage: "Paste the OpenRouter key. Codex-LB and ChatGPT OAuth remain unchanged.", placeholder: "sk-or-…", args: ["bridge", "provider", "configure", "openrouter", "--api-key-stdin", "--json"], kind: "bridge-provider-configure-openrouter", title: "Configure OpenRouter profile", credential: .openRouterApiKey, statusLabel: openRouterCredentialStatus, successSummary: "OpenRouter profile configured; Codex-LB preserved", failSummary: "OpenRouter profile configuration failed", codePrefix: "E-OR", providerId: "openrouter")
369
92
  }
370
93
 
371
- private func performUseOpenRouter(model: String) {
372
- guard openRouterCredentialValidatedNow else {
373
- openRouterStatus.stringValue = "Activation blocked · OpenRouter credential is not validated. Models remain hidden; choose Reconnect, then Refresh Models."
374
- return
375
- }
376
- guard let snapshot = beginProviderApply(kind: "openrouter-use", summary: "Use OpenRouter", mode: "openrouter", model: model) else {
377
- openRouterStatus.stringValue = "Another guarded mutation is already running. Wait or open Diagnostics."
378
- return
379
- }
380
- setBusy(true)
381
- openRouterStatus.stringValue = "Activating OpenRouter main model \(model) and restarting Codex App…"
382
- let running = operations.update(snapshot, state: .running, stage: "activating", progress: nil, summary: "Use OpenRouter")
383
- processClient.run(["codex-app", "use-openrouter", "--model", model, "--restart-app", "--json"], timeout: NativeView.mutationTimeout) { [weak self] activation in
384
- guard let self = self else { return }
385
- _ = self.operations.update(running, state: .running, stage: "verifying", progress: nil, summary: "Verify OpenRouter main model")
386
- self.processClient.run(["codex-app", "openrouter-status", "--json"], timeout: NativeView.statusTimeout) { [weak self] status in
387
- guard let self = self else { return }
388
- self.setBusy(false)
389
- let activationJson = self.json(activation.output)
390
- let statusJson = self.json(status.output)
391
- let selected = status.code == 0 && statusJson?["selected"] as? Bool == true
392
- let activeModel = statusJson?["model"] as? String
393
- let configApplied = activation.code == 0
394
- && activationJson?["config_applied"] as? Bool == true
395
- && selected
396
- && activeModel == model
397
- if configApplied {
398
- self.openRouterModelSelectionPending = false
399
- }
400
- let restartOK = activationJson?["restart_ok"] as? Bool == true
401
- let remapped = (activationJson?["thread_sidebar_remap"] as? [String: Any])?["remapped"] as? Int ?? 0
402
- let restoreAvailable = activationJson?["previous_routing_restore_available"] as? Bool == true
403
- var applyJson = activationJson ?? [:]
404
- applyJson["catalog_refreshed"] = status.code == 0
405
- && statusJson?["key_validated"] as? Bool == true
406
- && activeModel == model
407
- applyJson["new_session_ready"] = restartOK
408
- let applied = self.recordProviderApplyResult(
409
- running,
410
- json: applyJson,
411
- configurationSaved: configApplied,
412
- proxyApplied: selected && activeModel == model
413
- )
414
- let complete = configApplied && restartOK && applied.providerApply?.allSucceeded == true
415
- if complete {
416
- var parts = ["Activation complete · OpenRouter is active · main model \(model)"]
417
- if remapped > 0 { parts.append("sidebar kept \(remapped) prior-provider chats visible") }
418
- if restoreAvailable { parts.append("Restore previous provider is available") }
419
- parts.append("Desktop may still show Custom for third-party models")
420
- self.openRouterStatus.stringValue = parts.joined(separator: " · ") + "."
421
- } else if configApplied {
422
- self.openRouterStatus.stringValue = "Configuration saved · main model \(model) is selected, but Codex App did not restart. Next: reopen Codex App, then verify status."
423
- } else {
424
- self.openRouterStatus.stringValue = "Activation incomplete · requested \(model), observed \(activeModel ?? "unknown") · \(self.structuredPublicDetail(activationJson, fallback: activation.output, codePrefix: "E-OR"))"
425
- }
426
- self.refreshOpenRouterStatus()
427
- }
428
- }
429
- }
94
+ @objc func validateCodexLbProfile() { runProviderAction("codex-lb", args: ["bridge", "provider", "validate", "codex-lb", "--json"], title: "Validate Codex-LB", mutationGroup: nil) }
95
+ @objc func validateOpenRouterProfile() { runProviderAction("openrouter", args: ["bridge", "provider", "validate", "openrouter", "--json"], title: "Validate OpenRouter", mutationGroup: nil) }
96
+ @objc func toggleCodexLbProfile() { toggleProvider("codex-lb") }
97
+ @objc func toggleOpenRouterProfile() { toggleProvider("openrouter") }
430
98
 
431
- private func selectedOpenRouterModel() -> String {
432
- openRouterModelField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
99
+ private func toggleProvider(_ id: String) {
100
+ let verb = providerEnabled[id] == true ? "disable" : "enable"
101
+ runProviderAction(id, args: ["bridge", "provider", verb, id, "--json"], title: "\(verb.capitalized) \(displayProvider(id))", mutationGroup: "codex-config")
433
102
  }
434
103
 
435
- /// One shared failure format: stable code · HTTP · transport · public message · Next: one action.
436
- /// `codePrefix` keeps codes honest per surface (E-LB / E-OR); the neutral default next-step keeps OpenRouter failures from advertising Codex LB recovery actions.
437
- func structuredPublicDetail(_ json: [String: Any]?, fallback: String, codePrefix: String = "E-LB", fallbackNext: String = "review the key, model id, and network, then retry") -> String {
438
- let code = publicFailureCode(json, codePrefix: codePrefix)
439
- let http = publicHttpStatus(json).map { "HTTP \($0)" }
440
- let transport = publicAuthTransport(json)
441
- let error = publicError(json)
442
- var parts: [String] = [code]
443
- if let http { parts.append(http) }
444
- if let transport { parts.append("transport \(transport)") }
445
- if let error, !error.isEmpty { parts.append(error) }
446
- if let blockers = json?["blockers"] as? [String], !blockers.isEmpty {
447
- parts.append(blockers.prefix(2).joined(separator: ", "))
448
- }
449
- let next = firstGuidance(json) ?? (json?["hint"] as? String) ?? fallbackNext
450
- return "\(parts.joined(separator: " · ")). Next: \(next)."
451
- }
452
-
453
- func publicFailureCode(_ json: [String: Any]?, codePrefix: String = "E-LB") -> String {
454
- let blockers = json?["blockers"] as? [String] ?? []
455
- let truth = json?["routing_truth"] as? [String: Any]
456
- let truthBlockers = truth?["blockers"] as? [String] ?? []
457
- let all = blockers + truthBlockers
458
- if all.contains(where: { $0.contains("auth_rejected") || $0.contains("gateway_auth_rejected") }) {
459
- return "\(codePrefix)-AUTH"
104
+ private func runProviderAction(_ id: String, args: [String], title: String, mutationGroup: String?) {
105
+ guard !providerActionInFlight.contains(id), let snapshot = operations.begin(kind: "bridge-provider-\(id)", mutationGroup: mutationGroup, summary: title) else { return }
106
+ setProviderActionBusy(id, true)
107
+ let label = id == "codex-lb" ? cliProviderStatus : openRouterCredentialStatus
108
+ label.stringValue = "\(title)…"
109
+ _ = operations.update(snapshot, state: .running, stage: "running", progress: nil, summary: title)
110
+ processClient.run(args, timeout: NativeView.mutationTimeout) { [weak self] result in
111
+ guard let self = self else { return }
112
+ self.setProviderActionBusy(id, false)
113
+ let parsed = self.json(result.output)
114
+ let generated = parsed != nil && parsed?["schema"] != nil
115
+ _ = self.operations.update(snapshot, state: generated ? .succeeded : .failed, stage: "complete", progress: 1, summary: generated ? "\(title) report generated" : "\(title) failed")
116
+ if !generated { label.stringValue = "\(title) failed · \(self.structuredPublicDetail(parsed, fallback: result.output))" }
117
+ self.refresh()
460
118
  }
461
- if all.contains(where: { $0.contains("unreachable") }) { return "\(codePrefix)-UNREACHABLE" }
462
- if all.contains(where: { $0.contains("legacy") && $0.contains("migration") }) { return "\(codePrefix)-LEGACY-MIGRATE" }
463
- if let http = publicHttpStatus(json), http >= 400 { return "\(codePrefix)-HTTP-\(http)" }
464
- if json == nil || json?["ok"] as? Bool == false { return "\(codePrefix)-FAILED" }
465
- return "\(codePrefix)-INFO"
466
- }
467
-
468
- func publicHttpStatus(_ json: [String: Any]?) -> Int? {
469
- if let value = json?["http_status"] as? Int { return value }
470
- if let value = (json?["http_status"] as? NSNumber)?.intValue { return value }
471
- let truth = json?["routing_truth"] as? [String: Any]
472
- if let value = truth?["http_status"] as? Int { return value }
473
- return (truth?["http_status"] as? NSNumber)?.intValue
474
- }
475
-
476
- func publicAuthTransport(_ json: [String: Any]?) -> String? {
477
- if let value = json?["gateway_auth_transport"] as? String, !value.isEmpty { return value }
478
- if let value = json?["auth_transport"] as? String, !value.isEmpty { return value }
479
- let truth = json?["routing_truth"] as? [String: Any]
480
- if let value = truth?["auth_transport"] as? String, !value.isEmpty { return value }
481
- return nil
482
119
  }
483
120
 
484
- func firstGuidance(_ json: [String: Any]?) -> String? {
485
- if let rows = json?["guidance"] as? [String] {
486
- return rows.first { !$0.isEmpty }
121
+ func structuredPublicDetail(_ json: [String: Any]?, fallback: String, codePrefix: String = "E-PROVIDER", fallbackNext: String = "Retry the operation") -> String {
122
+ let code = publicFailureCode(json, fallback: codePrefix)
123
+ if let blockers = json?["blockers"] as? [String], let first = blockers.first {
124
+ return "\(code) · \(ProviderSecretRedactor.redact(first)) · Next: \(fallbackNext)"
487
125
  }
488
- return nil
126
+ return "\(code) · \(ProviderSecretRedactor.redact(NativeView.redactPreview(fallback))) · Next: \(fallbackNext)"
489
127
  }
490
128
 
491
- func publicError(_ json: [String: Any]?) -> String? {
492
- if let value = json?["error"] as? String { return value }
493
- guard let value = json?["error"] as? [String: Any] else { return nil }
494
- let code = value["code"] as? String
495
- let message = value["message"] as? String
496
- return [code, message].compactMap { $0 }.joined(separator: ": ")
129
+ func publicFailureCode(_ json: [String: Any]?, fallback: String = "E-PROVIDER") -> String {
130
+ if let code = json?["code"] as? String, !code.isEmpty { return ProviderSecretRedactor.redact(code) }
131
+ if let blocker = (json?["blockers"] as? [String])?.first, !blocker.isEmpty { return ProviderSecretRedactor.redact(blocker) }
132
+ return fallback
497
133
  }
498
134
 
135
+ private func displayProvider(_ id: String) -> String { id == "codex-lb" ? "Codex-LB" : "OpenRouter" }
499
136
  }