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
package/dist/core/init.js CHANGED
@@ -670,7 +670,6 @@ export async function initProject(root, opts = {}) {
670
670
  return `${next.trim()}\n`;
671
671
  }
672
672
  async function mergeGlobalCodexConfigIfAvailable(configText = '', configPath = '', opts = {}) {
673
- const selectedRe = /(^|\n)\s*model_provider\s*=\s*"codex-lb"\s*(?:#.*)?(?=\n|$)/;
674
673
  const home = opts.home || process.env.HOME || '';
675
674
  if (!home)
676
675
  return configText;
@@ -681,49 +680,8 @@ export async function initProject(root, opts = {}) {
681
680
  const globalConfig = await readText(globalConfigPath, '');
682
681
  let next = mergeGlobalMcpServers(configText, globalConfig);
683
682
  next = mergeGlobalCodexAppRuntimeTables(next, globalConfig);
684
- if (selectedRe.test(next) && /\[model_providers\.codex-lb\]/.test(next)) {
685
- return `${next.trim()}\n`;
686
- }
687
- const envPath = path.join(codexHome, 'sks-codex-lb.env');
688
- if (!(await exists(envPath)))
689
- return next;
690
- const envText = await readText(envPath, '');
691
- const baseUrl = globalConfig.match(/(^|\n)\[model_providers\.codex-lb\][\s\S]*?\n\s*base_url\s*=\s*"([^"]+)"/)?.[2] || parseCodexLbEnvBaseUrl(envText);
692
- if (!parseCodexLbEnvKey(envText) || !baseUrl)
693
- return next;
694
- const shouldSelectCodexLb = selectedRe.test(next) || selectedRe.test(globalConfig);
695
- next = shouldSelectCodexLb
696
- ? upsertTopLevelTomlString(next, 'model_provider', 'codex-lb')
697
- : removeTopLevelTomlKeyIfValue(next, 'model_provider', 'codex-lb');
698
- next = upsertTomlTable(next, 'model_providers.codex-lb', `[model_providers.codex-lb]\nname = "codex-lb"\nbase_url = "${baseUrl}"\nwire_api = "responses"\nenv_key = "CODEX_LB_API_KEY"\nsupports_websockets = true\nrequires_openai_auth = false`);
699
683
  return `${next.trim()}\n`;
700
684
  }
701
- function parseCodexLbEnvKey(text = '') {
702
- return parseShellEnvValue(text, 'CODEX_LB_API_KEY');
703
- }
704
- function parseCodexLbEnvBaseUrl(text = '') {
705
- const value = parseShellEnvValue(text, 'CODEX_LB_BASE_URL');
706
- if (!value)
707
- return '';
708
- let host = value.trim();
709
- if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(host))
710
- host = `https://${host}`;
711
- host = host.replace(/\/+$/, '');
712
- return /\/backend-api\/codex$/i.test(host) ? host : `${host}/backend-api/codex`;
713
- }
714
- function parseShellEnvValue(text = '', key = '') {
715
- const re = new RegExp(`^\\s*(?:export\\s+)?${escapeRegExp(key)}\\s*=\\s*(.+?)\\s*$`, 'm');
716
- const raw = String(text || '').match(re)?.[1]?.trim() || '';
717
- if (!raw)
718
- return '';
719
- if (raw.startsWith("'"))
720
- return raw.endsWith("'") && raw.length > 1 ? raw.slice(1, -1).replace(/'\\''/g, "'") : '';
721
- if (raw.startsWith('"'))
722
- return raw.endsWith('"') && raw.length > 1 ? raw.slice(1, -1).replace(/\\"/g, '"') : '';
723
- if (raw.includes("'") || raw.includes('"') || /\s/.test(raw))
724
- return '';
725
- return raw;
726
- }
727
685
  function mergeGlobalMcpServers(configText = '', globalConfig = '') {
728
686
  let next = configText;
729
687
  const re = /(?:^|\n)(\[(mcp_servers\.[^\]\r\n]+)\][\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/g;
@@ -762,27 +720,6 @@ export async function initProject(root, opts = {}) {
762
720
  }
763
721
  return lines.filter((_, index) => index < start || index >= end).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
764
722
  }
765
- function removeTopLevelTomlKeyIfValue(text = '', key = '', value = '') {
766
- const lines = String(text || '').split('\n');
767
- const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
768
- const end = firstTable === -1 ? lines.length : firstTable;
769
- const keyPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"${escapeRegExp(value)}"\\s*(?:#.*)?$`);
770
- return lines.filter((line, index) => index >= end || !keyPattern.test(line)).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
771
- }
772
- function upsertTopLevelTomlString(text, key, value) {
773
- const line = `${key} = "${value}"`;
774
- const lines = String(text || '').split('\n');
775
- const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
776
- const end = firstTable === -1 ? lines.length : firstTable;
777
- for (let i = 0; i < end; i += 1) {
778
- if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[i] || '')) {
779
- lines[i] = line;
780
- return lines.join('\n').replace(/\n{3,}/g, '\n\n');
781
- }
782
- }
783
- lines.splice(end, 0, line);
784
- return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
785
- }
786
723
  function upsertTopLevelTomlBoolean(text, key, value) {
787
724
  const line = `${key} = ${value ? 'true' : 'false'}`;
788
725
  const lines = String(text || '').split('\n');
@@ -316,7 +316,7 @@ async function readReceiptBinding(receipt, tarball) {
316
316
  const blockers = [];
317
317
  const sha256 = normalizeSha256(parsed.sha256);
318
318
  const version = typeof parsed.package_version === 'string' ? parsed.package_version : null;
319
- if (parsed.schema !== 'sks.release-pack-receipt.v1' || parsed.ok !== true)
319
+ if (parsed.schema !== 'sks.release-pack-receipt.v2' || parsed.ok !== true)
320
320
  blockers.push('pack_receipt_invalid');
321
321
  if (!sha256)
322
322
  blockers.push('pack_receipt_sha256_invalid');
@@ -447,7 +447,6 @@ Return the measurement method, observed result, uncertainty, and next cheapest e
447
447
  })
448
448
  ]);
449
449
  export const MANAGED_SKILLS = Object.freeze([
450
- 'loop',
451
450
  'naruto',
452
451
  'qa-loop',
453
452
  'research',
@@ -50,8 +50,21 @@ const WINDOW_CHARS = 90;
50
50
  function containsTrigger(haystack) {
51
51
  const lower = haystack.toLowerCase();
52
52
  for (const trigger of GUIDANCE_TRIGGERS) {
53
- if (lower.includes(trigger.toLowerCase()))
54
- return trigger;
53
+ const needle = trigger.toLowerCase();
54
+ let fromIndex = 0;
55
+ while (fromIndex <= lower.length - needle.length) {
56
+ const index = lower.indexOf(needle, fromIndex);
57
+ if (index < 0)
58
+ break;
59
+ const before = index > 0 ? (lower[index - 1] ?? '') : '';
60
+ const afterIndex = index + needle.length;
61
+ const after = afterIndex < lower.length ? (lower[afterIndex] ?? '') : '';
62
+ const englishTrigger = /[a-z]/.test(needle);
63
+ const hasWordBoundaries = !englishTrigger || (!/[a-z0-9_]/.test(before) && !/[a-z0-9_]/.test(after));
64
+ if (hasWordBoundaries)
65
+ return trigger;
66
+ fromIndex = index + 1;
67
+ }
55
68
  }
56
69
  return null;
57
70
  }
@@ -4,8 +4,7 @@ import { buildCodexExecArgs } from '../codex/codex-cli-syntax-builder.js';
4
4
  import { inspectCodexConfigReadability } from '../codex/codex-config-readability.js';
5
5
  import { repairCodexConfigEperm } from '../codex/codex-config-eperm-repair.js';
6
6
  import { splitCodexProjectConfigPolicy } from '../codex/codex-project-config-policy.js';
7
- import { codexLbStatus } from '../../cli/install-helpers.js';
8
- import { codexLbToolOutputRecoveryOverrideAcknowledged } from '../codex-lb/codex-lb-tool-output-recovery.js';
7
+ import { desktopBridgeStatusV3 } from '../codex-lb/desktop-controller-v3.js';
9
8
  export const PARALLEL_PREFLIGHT_SCHEMA = 'sks.parallel-preflight.v1';
10
9
  export async function runParallelPreflight(checks) {
11
10
  const startedAt = nowIso();
@@ -34,7 +33,7 @@ export async function runCodexLaunchPreflight(rootInput = process.cwd(), opts =
34
33
  const readonly = await runParallelPreflight([
35
34
  { id: 'codex_config_readability', run: () => inspectCodexConfigReadability(root, { ...opts, codexProbe: probeCodex, actualCodex: probeCodex, writeReport: false }) },
36
35
  { id: 'codex_project_config_policy', run: () => splitCodexProjectConfigPolicy(root, { ...opts, writeReport: false }) },
37
- { id: 'codex_lb_tool_output_recovery', run: () => inspectCodexLbToolOutputRecoveryForLaunch(opts) }
36
+ { id: 'desktop_bridge_status', run: () => inspectDesktopBridgeForLaunch(opts) }
38
37
  ]);
39
38
  const repair = opts.fix === true
40
39
  ? await repairCodexConfigEperm(root, { ...opts, codexProbe: probeCodex, actualCodex: probeCodex, fix: true, writeReport: false })
@@ -59,7 +58,7 @@ export async function runCodexLaunchPreflight(rootInput = process.cwd(), opts =
59
58
  };
60
59
  const blockers = [...new Set([...(readonly.blockers || []), ...(repair?.blockers || []), ...(fastTierProof.ok ? [] : ['service_tier_not_passed_to_codex'])])];
61
60
  const operatorActions = [...new Set([...(readonly.operator_actions || []), ...(repair?.operator_actions || [])])];
62
- const codexLbToolOutputRecovery = readonly.results.find((result) => result.id === 'codex_lb_tool_output_recovery')?.value || null;
61
+ const desktopBridgeStatus = readonly.results.find((result) => result.id === 'desktop_bridge_status')?.value || null;
63
62
  const report = {
64
63
  schema: 'sks.mad-launch-preflight.v1',
65
64
  generated_at: nowIso(),
@@ -67,7 +66,7 @@ export async function runCodexLaunchPreflight(rootInput = process.cwd(), opts =
67
66
  ok: blockers.length === 0,
68
67
  readonly,
69
68
  repair,
70
- codex_lb_tool_output_recovery: codexLbToolOutputRecovery,
69
+ desktop_bridge_status: desktopBridgeStatus,
71
70
  fast_tier_proof: fastTierProof,
72
71
  blockers,
73
72
  operator_actions: operatorActions
@@ -76,61 +75,38 @@ export async function runCodexLaunchPreflight(rootInput = process.cwd(), opts =
76
75
  await writeJsonAtomic(reportPath, { ...report, report_path: reportPath });
77
76
  return report;
78
77
  }
79
- export async function inspectCodexLbToolOutputRecoveryForLaunch(opts = {}) {
80
- if (opts.skipCodexLbToolOutputRecovery === true) {
78
+ export async function inspectDesktopBridgeForLaunch(opts = {}) {
79
+ if (opts.skipDesktopBridgeStatus === true) {
81
80
  return {
82
- schema: 'sks.codex-lb-launch-recovery-preflight.v1',
81
+ schema: 'sks.desktop-bridge-launch-preflight.v1',
83
82
  ok: true,
84
- status: 'not_applicable',
85
- selected: false,
83
+ status: 'skipped',
84
+ managed: false,
86
85
  blockers: [],
87
86
  operator_actions: []
88
87
  };
89
88
  }
90
- const allowUnverifiedToolOutputRecovery = opts.allowUnverifiedToolOutputRecovery === true
91
- || codexLbToolOutputRecoveryOverrideAcknowledged({ env: opts.env || process.env });
92
- const status = await codexLbStatus({
89
+ const status = await (opts.desktopBridgeStatusImpl || desktopBridgeStatusV3)({
93
90
  ...opts,
94
- ...(opts.home || opts.codexHome ? { home: opts.home || opts.codexHome } : {}),
95
- ...(opts.codexLbConfigPath ? { configPath: opts.codexLbConfigPath } : {}),
96
- ...(opts.codexLbEnvPath ? { envPath: opts.codexLbEnvPath } : {}),
97
- ...(typeof opts.codexLbToolOutputRecoveryFetch === 'function'
98
- ? { toolOutputRecoveryFetch: opts.codexLbToolOutputRecoveryFetch }
99
- : {}),
100
- ...(opts.codexLbToolOutputRecoveryTimeoutMs
101
- ? { toolOutputRecoveryTimeoutMs: opts.codexLbToolOutputRecoveryTimeoutMs }
102
- : {}),
103
- probeToolOutputRecovery: true,
104
- allowUnverifiedToolOutputRecovery
91
+ ...(opts.home || opts.codexHome ? { home: opts.home || opts.codexHome } : {})
105
92
  });
106
- const recovery = status.tool_output_recovery;
107
- const required = status.selected === true;
108
- const providerReady = status.provider_ready === true;
109
- const providerBlockers = Array.isArray(status.blockers)
110
- ? status.blockers.map(String).filter(Boolean)
93
+ const managed = status.management?.managed === true;
94
+ const ready = status.readiness?.ready === true;
95
+ const blockers = managed && !ready
96
+ ? Array.isArray(status.readiness?.blockers)
97
+ ? status.readiness.blockers.map(String).filter(Boolean)
98
+ : ['desktop_bridge_not_ready']
111
99
  : [];
112
- const ok = !required || (providerReady && recovery?.ok === true);
113
100
  return {
114
- schema: 'sks.codex-lb-launch-recovery-preflight.v1',
115
- ok,
116
- status: !required
117
- ? 'not_selected'
118
- : !providerReady
119
- ? providerBlockers[0] || 'provider_not_ready'
120
- : recovery?.status || 'version_unverified',
121
- selected: required,
122
- provider_ready: providerReady,
123
- base_url: status.base_url || null,
124
- tool_output_recovery: recovery || null,
125
- blockers: ok
126
- ? []
127
- : !providerReady
128
- ? providerBlockers.length > 0
129
- ? providerBlockers
130
- : ['codex_lb_provider_not_ready']
131
- : recovery?.blockers || ['codex_lb_tool_output_recovery_version_unverified'],
132
- operator_actions: ok ? [] : recovery?.operator_actions || [
133
- 'Upgrade codex-lb, or run `sks codex-lb disable`; SKS will not change routing or authentication silently.'
134
- ]
101
+ schema: 'sks.desktop-bridge-launch-preflight.v1',
102
+ ok: !managed || ready,
103
+ status: !managed ? 'not_managed' : ready ? 'ready' : 'blocked',
104
+ managed,
105
+ ready,
106
+ correlation_id: status.correlation_id || null,
107
+ blockers,
108
+ operator_actions: managed && !ready && Array.isArray(status.recovery_actions)
109
+ ? status.recovery_actions.map(String).filter(Boolean)
110
+ : []
135
111
  };
136
112
  }
@@ -1,9 +1,10 @@
1
1
  export const PROVIDER_BADGE_SCHEMA = 'sks.provider-badge.v1';
2
2
  export function providerBadgeText(context) {
3
+ const bridgeProvider = context.signals.desktop_bridge_provider;
3
4
  const providerText = context.provider === 'openai'
4
- ? 'OpenAI'
5
- : context.provider === 'codex-lb'
6
- ? 'codex-lb'
5
+ ? 'OpenAI API'
6
+ : context.provider === 'desktop-bridge'
7
+ ? `Desktop Bridge${bridgeProvider ? ` → ${bridgeProvider}` : ''}`
7
8
  : context.provider === 'codex-app'
8
9
  ? 'Codex App OAuth'
9
10
  : 'Unknown';
@@ -15,7 +16,11 @@ export function providerBadgeText(context) {
15
16
  return `Provider: ${providerText} · ${tierText}`;
16
17
  }
17
18
  export function providerPaneLabel(context) {
18
- const provider = context.provider === 'unknown' ? 'provider-unknown' : context.provider;
19
+ const provider = context.provider === 'unknown'
20
+ ? 'provider-unknown'
21
+ : context.provider === 'desktop-bridge' && context.signals.desktop_bridge_provider
22
+ ? `desktop-bridge/${context.signals.desktop_bridge_provider}`
23
+ : context.provider;
19
24
  const tier = context.service_tier === 'unknown' ? 'tier-unknown' : context.service_tier;
20
25
  return `${tier} · ${provider}`;
21
26
  }
@@ -1,58 +1,48 @@
1
1
  import path from 'node:path';
2
- import { exists, nowIso, readJson, readText, writeJsonAtomic } from '../fsx.js';
3
- import { escapeRegExp } from '../text/regex.js';
4
- export const PROVIDER_CONTEXT_SCHEMA = 'sks.provider-context.v1';
2
+ import { exists, nowIso, readJson, writeJsonAtomic } from '../fsx.js';
3
+ export const PROVIDER_CONTEXT_SCHEMA = 'sks.provider-context.v2';
5
4
  export async function resolveProviderContext(input = {}) {
6
5
  const env = input.env || process.env;
7
6
  const root = path.resolve(input.root || process.cwd());
8
- const codexHome = path.resolve(String(input.codexHome || env.CODEX_HOME || path.join(env.HOME || '', '.codex')));
9
- const configText = await readText(path.join(codexHome, 'config.toml'), '').catch(() => '');
10
- const configModelProvider = readTopLevelTomlString(configText, 'model_provider');
11
- const codexLbProviderBlockPresent = hasCodexLbProviderBlock(configText);
12
- const codexLbBearerEnvKey = codexLbProviderBearerEnvKey(configText);
13
- const codexLbGatewayHeaderEnvKey = codexLbProviderGatewayHeaderEnvKey(configText);
14
- const codexLbEnvKey = codexLbGatewayHeaderEnvKey || codexLbBearerEnvKey;
15
- const codexLbRequiresOpenAiAuth = codexLbProviderRequiresOpenAiAuth(configText);
16
- const codexLbProviderValid = codexLbProviderBlockPresent
17
- && codexLbBearerEnvKey === 'CODEX_LB_API_KEY'
18
- && codexLbGatewayHeaderEnvKey === null
19
- && codexLbRequiresOpenAiAuth === false;
20
- const openaiKey = Boolean(String(env.OPENAI_API_KEY || '').trim());
21
- const lbKey = Boolean(String((codexLbEnvKey ? env[codexLbEnvKey] : env.CODEX_LB_API_KEY) || env.CODEX_LB_API_KEY || '').trim());
22
- const envProvider = String(env.SKS_MODEL_PROVIDER || env.CODEX_MODEL_PROVIDER || env.OPENAI_MODEL_PROVIDER || '').trim();
23
- const modelProvider = String(input.modelProvider || envProvider || configModelProvider || '').trim() || null;
24
- const envLbExplicit = env.SKS_PROVIDER === 'codex-lb' || env.SKS_USE_CODEX_LB === '1';
25
- const lbExplicit = modelProvider === 'codex-lb' || envLbExplicit;
7
+ const codexHome = path.resolve(String(input.codexHome || env.CODEX_HOME || path.join(env.HOME || root, '.codex')));
26
8
  const auth = await readJson(path.join(codexHome, 'auth.json'), null).catch(() => null);
27
9
  const appAuthPresent = Boolean(auth) || await exists(path.join(codexHome, 'auth.json'));
28
- const conflict = (lbKey && openaiKey && !lbExplicit && !modelProvider) || (modelProvider === 'codex-lb' && !lbKey && openaiKey);
10
+ const openaiKeyPresent = Boolean(String(env.OPENAI_API_KEY || '').trim());
11
+ let bridgeStatus = null;
12
+ let bridgeStatusAvailable = false;
13
+ try {
14
+ bridgeStatus = Object.prototype.hasOwnProperty.call(input, 'desktopBridgeStatus')
15
+ ? input.desktopBridgeStatus || null
16
+ : await (input.desktopBridgeStatusImpl || currentDesktopBridgeStatus)(input.desktopBridgeStatusOptions || {});
17
+ bridgeStatusAvailable = bridgeStatus?.schema === 'sks.desktop-bridge-status.v3';
18
+ if (!bridgeStatusAvailable)
19
+ bridgeStatus = null;
20
+ }
21
+ catch {
22
+ bridgeStatus = null;
23
+ }
24
+ const bridgeManaged = bridgeStatus?.management.managed === true;
25
+ const bridgeReady = bridgeStatus?.readiness.ready === true;
26
+ const bridgeProvider = selectedBridgeProvider(bridgeStatus);
27
+ const nativeIdentityConfigured = bridgeStatus?.native_identity.configured === true || appAuthPresent;
28
+ const credentialState = bridgeProvider && bridgeStatus
29
+ ? bridgeStatus.providers[bridgeProvider].credential.state
30
+ : null;
29
31
  let provider = 'unknown';
30
32
  let authMode = 'unknown';
31
33
  let source = 'unknown';
32
34
  let confidence = 'low';
33
- if (envLbExplicit && lbKey) {
34
- provider = 'codex-lb';
35
- authMode = 'codex_lb_key';
36
- source = 'codex_lb';
37
- confidence = 'high';
38
- }
39
- else if (modelProvider === 'codex-lb' && codexLbProviderValid && lbKey) {
40
- provider = 'codex-lb';
41
- authMode = 'codex_lb_key';
42
- source = 'config';
43
- confidence = 'high';
35
+ if (bridgeManaged) {
36
+ provider = 'desktop-bridge';
37
+ authMode = nativeIdentityConfigured ? 'chatgpt_oauth' : 'unknown';
38
+ source = 'desktop_bridge';
39
+ confidence = bridgeReady && nativeIdentityConfigured ? 'high' : 'medium';
44
40
  }
45
- else if (modelProvider === 'codex-lb' && codexLbProviderValid) {
46
- provider = 'codex-lb';
47
- authMode = 'unknown';
48
- source = 'config';
49
- confidence = 'low';
50
- }
51
- else if (openaiKey) {
41
+ else if (openaiKeyPresent) {
52
42
  provider = 'openai';
53
43
  authMode = 'api_key';
54
44
  source = 'env';
55
- confidence = conflict ? 'medium' : 'high';
45
+ confidence = 'high';
56
46
  }
57
47
  else if (appAuthPresent) {
58
48
  provider = 'codex-app';
@@ -60,11 +50,12 @@ export async function resolveProviderContext(input = {}) {
60
50
  source = 'codex_app';
61
51
  confidence = 'medium';
62
52
  }
63
- const warnings = [
64
- ...(conflict ? ['provider_conflict'] : []),
65
- ...(modelProvider === 'codex-lb' && !codexLbProviderValid && !envLbExplicit ? ['codex_lb_provider_config_missing_or_invalid'] : []),
66
- ...(provider === 'codex-lb' && !lbKey ? ['codex_lb_selected_without_key'] : [])
67
- ];
53
+ const warnings = unique([
54
+ ...(!bridgeStatusAvailable ? ['desktop_bridge_status_unavailable'] : []),
55
+ ...(bridgeManaged ? bridgeStatus?.readiness.blockers || [] : []),
56
+ ...(bridgeManaged ? bridgeStatus?.routing.blockers || [] : []),
57
+ ...(bridgeManaged && !nativeIdentityConfigured ? ['desktop_bridge_native_identity_required'] : [])
58
+ ]);
68
59
  return {
69
60
  schema: PROVIDER_CONTEXT_SCHEMA,
70
61
  generated_at: nowIso(),
@@ -74,19 +65,17 @@ export async function resolveProviderContext(input = {}) {
74
65
  service_tier: normalizeServiceTier(input.serviceTier || env.SKS_SERVICE_TIER),
75
66
  source,
76
67
  confidence,
77
- conflict,
68
+ conflict: false,
78
69
  warnings,
79
70
  signals: {
80
- openai_api_key_present: openaiKey,
81
- codex_lb_key_present: lbKey,
82
- codex_lb_explicit: lbExplicit,
71
+ openai_api_key_present: openaiKeyPresent,
83
72
  codex_app_auth_present: appAuthPresent,
84
- model_provider: modelProvider,
85
- codex_lb_provider_block_present: codexLbProviderBlockPresent,
86
- codex_lb_env_key: codexLbEnvKey,
87
- codex_lb_gateway_header_env_key: codexLbGatewayHeaderEnvKey,
88
- codex_lb_requires_openai_auth: codexLbRequiresOpenAiAuth,
89
- codex_lb_available: codexLbProviderValid && lbKey
73
+ desktop_bridge_status_available: bridgeStatusAvailable,
74
+ desktop_bridge_managed: bridgeManaged,
75
+ desktop_bridge_ready: bridgeReady,
76
+ desktop_bridge_provider: bridgeProvider,
77
+ desktop_bridge_native_identity_configured: nativeIdentityConfigured,
78
+ desktop_bridge_credential_state: credentialState
90
79
  }
91
80
  };
92
81
  }
@@ -96,6 +85,16 @@ export async function writeProviderContextReport(root = process.cwd(), input = {
96
85
  await writeJsonAtomic(reportPath, report);
97
86
  return { ...report, report_path: reportPath };
98
87
  }
88
+ function selectedBridgeProvider(status) {
89
+ return status?.routing.selected_route?.provider_id
90
+ || status?.routing.policy?.default_provider_id
91
+ || status?.routing.session_pin?.provider_id
92
+ || null;
93
+ }
94
+ async function currentDesktopBridgeStatus(options) {
95
+ const controller = await import('../codex-lb/desktop-controller.js');
96
+ return controller.desktopBridgeStatusV3(options);
97
+ }
99
98
  function normalizeServiceTier(value) {
100
99
  const text = String(value || '').toLowerCase();
101
100
  if (text === 'fast' || text === 'priority')
@@ -104,55 +103,6 @@ function normalizeServiceTier(value) {
104
103
  return 'standard';
105
104
  return 'unknown';
106
105
  }
107
- export function readTopLevelTomlString(text, key) {
108
- const lines = String(text || '').split(/\r?\n/);
109
- for (const line of lines) {
110
- if (/^\s*\[/.test(line))
111
- break;
112
- const match = line.match(new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]*)"\\s*(?:#.*)?$`));
113
- if (match?.[1] != null)
114
- return match[1];
115
- }
116
- return null;
117
- }
118
- export function hasCodexLbProviderBlock(text) {
119
- return codexLbProviderBody(text) != null;
120
- }
121
- export function codexLbProviderEnvKey(text) {
122
- return codexLbProviderGatewayHeaderEnvKey(text) || codexLbProviderBearerEnvKey(text);
123
- }
124
- export function codexLbProviderGatewayHeaderEnvKey(text) {
125
- const body = codexLbProviderBody(text);
126
- if (body == null)
127
- return null;
128
- const inline = body.match(/(?:^|\n)\s*env_http_headers\s*=\s*\{([^}]*)\}/)?.[1] || '';
129
- return inline.match(/"X-Codex-LB-API-Key"\s*=\s*"([^"]+)"/)?.[1] || null;
130
- }
131
- function codexLbProviderBearerEnvKey(text) {
132
- const body = codexLbProviderBody(text);
133
- return body == null ? null : readTopLevelTomlString(body, 'env_key');
134
- }
135
- export function codexLbProviderRequiresOpenAiAuth(text) {
136
- const body = codexLbProviderBody(text);
137
- if (body == null)
138
- return null;
139
- const match = body.match(/^\s*requires_openai_auth\s*=\s*(true|false)\s*(?:#.*)?$/m);
140
- return match?.[1] === 'true' ? true : match?.[1] === 'false' ? false : null;
141
- }
142
- function codexLbProviderBody(text) {
143
- const lines = String(text || '').split(/\r?\n/);
144
- const out = [];
145
- let inTable = false;
146
- for (const line of lines) {
147
- const table = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/)?.[1]?.trim();
148
- if (table) {
149
- if (inTable)
150
- break;
151
- inTable = table === 'model_providers.codex-lb' || table === 'model_providers."codex-lb"' || table === '"model_providers"."codex-lb"';
152
- continue;
153
- }
154
- if (inTable)
155
- out.push(line);
156
- }
157
- return inTable ? out.join('\n') : null;
106
+ function unique(values) {
107
+ return [...new Set(values.map((value) => String(value || '').trim()).filter(Boolean))];
158
108
  }
@@ -96,7 +96,9 @@ export async function testOpenRouterConnection(input = {}) {
96
96
  warnings: []
97
97
  };
98
98
  }
99
- const resolved = await resolveOpenRouterApiKey({ env: input.env || process.env });
99
+ const resolved = input.apiKey
100
+ ? { key: String(input.apiKey), source: 'explicit-validation-snapshot', warnings: [] }
101
+ : await resolveOpenRouterApiKey({ env: input.env || process.env });
100
102
  if (!resolved.key) {
101
103
  return {
102
104
  schema: 'sks.openrouter-test.v1',
@@ -3,6 +3,8 @@ import fs from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { redactOpenRouterKey } from '../../security/redact-secrets.js';
6
+ import { readPrivateCredentialFile, writePrivateTextAtomic } from '../../security/private-credential-file.js';
7
+ import { inspectConfinedPath, isLexicallyConfined, removeManagedPathVerified } from '../../managed-path-safety.js';
6
8
  export const OPENROUTER_KEY_ENV_NAMES = ['OPENROUTER_API_KEY', 'SKS_OPENROUTER_API_KEY'];
7
9
  export function openRouterSecretPaths(env = process.env) {
8
10
  const sksHome = path.resolve(env.SKS_HOME || path.join(env.HOME || os.homedir(), '.sneakoscope'));
@@ -49,7 +51,7 @@ export async function resolveOpenRouterApiKey(input = {}) {
49
51
  }
50
52
  export async function readStoredOpenRouterKey(paths) {
51
53
  try {
52
- const text = await fs.readFile(paths.keyPath, 'utf8');
54
+ const text = (await readPrivateCredentialFile(paths.secretDir, paths.keyPath, 'openrouter_api_key')).bytes.toString('utf8');
53
55
  const key = text.trim();
54
56
  return key || null;
55
57
  }
@@ -66,24 +68,7 @@ export async function writeStoredOpenRouterKey(value, input = {}) {
66
68
  const timestamp = nowIso();
67
69
  const previous = input.previousRecord ?? await readOpenRouterKeyRecord(paths);
68
70
  await ensureSecretDir(paths.secretDir);
69
- const tmp = `${paths.keyPath}.${process.pid}.${crypto.randomBytes(3).toString('hex')}.tmp`;
70
- try {
71
- const handle = await fs.open(tmp, 'w', 0o600);
72
- try {
73
- await handle.writeFile(`${key}\n`, 'utf8');
74
- await handle.sync().catch(() => undefined);
75
- }
76
- finally {
77
- await handle.close().catch(() => undefined);
78
- }
79
- await fs.chmod(tmp, 0o600).catch(() => undefined);
80
- await fs.rename(tmp, paths.keyPath);
81
- await fs.chmod(paths.keyPath, 0o600).catch(() => undefined);
82
- }
83
- catch (err) {
84
- await fs.rm(tmp, { force: true }).catch(() => undefined);
85
- throw err;
86
- }
71
+ await writePrivateTextAtomic(paths.secretDir, paths.keyPath, `${key}\n`, 'openrouter_api_key');
87
72
  const record = {
88
73
  schema: 'sks.openrouter-key.v1',
89
74
  created_at: previous?.created_at || timestamp,
@@ -91,13 +76,13 @@ export async function writeStoredOpenRouterKey(value, input = {}) {
91
76
  key_hash: crypto.createHash('sha256').update(key).digest('hex'),
92
77
  key_preview: redactOpenRouterKey(key)
93
78
  };
94
- await fs.writeFile(paths.metadataPath, `${JSON.stringify(record, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
95
- await fs.chmod(paths.metadataPath, 0o600).catch(() => undefined);
79
+ await writePrivateTextAtomic(paths.secretDir, paths.metadataPath, `${JSON.stringify(record, null, 2)}\n`, 'openrouter_api_key_metadata');
96
80
  return record;
97
81
  }
98
82
  export async function readOpenRouterKeyRecord(paths) {
99
83
  try {
100
- const parsed = JSON.parse(await fs.readFile(paths.metadataPath, 'utf8'));
84
+ const text = (await readPrivateCredentialFile(paths.secretDir, paths.metadataPath, 'openrouter_api_key_metadata')).bytes.toString('utf8');
85
+ const parsed = JSON.parse(text);
101
86
  if (parsed.schema !== 'sks.openrouter-key.v1' || !parsed.key_hash || !parsed.key_preview)
102
87
  return null;
103
88
  return parsed;
@@ -106,6 +91,90 @@ export async function readOpenRouterKeyRecord(paths) {
106
91
  return null;
107
92
  }
108
93
  }
94
+ export async function removeStoredOpenRouterKey(input) {
95
+ if (input.confirmed !== true) {
96
+ return {
97
+ schema: 'sks.openrouter-key-removal.v1',
98
+ removed: false,
99
+ removed_paths: [],
100
+ blockers: ['openrouter_credential_removal_confirmation_required']
101
+ };
102
+ }
103
+ const paths = input.paths || openRouterSecretPaths();
104
+ const secretDir = path.resolve(paths.secretDir);
105
+ const files = [path.resolve(paths.keyPath), path.resolve(paths.metadataPath)];
106
+ if (files.some((file) => file === secretDir || !isLexicallyConfined(secretDir, file))) {
107
+ return {
108
+ schema: 'sks.openrouter-key-removal.v1',
109
+ removed: false,
110
+ removed_paths: [],
111
+ blockers: ['openrouter_credential_path_outside_secret_store']
112
+ };
113
+ }
114
+ if (new Set(files).size !== files.length) {
115
+ return {
116
+ schema: 'sks.openrouter-key-removal.v1',
117
+ removed: false,
118
+ removed_paths: [],
119
+ blockers: ['openrouter_credential_path_collision']
120
+ };
121
+ }
122
+ const secretDirStat = await fs.lstat(secretDir).catch((error) => {
123
+ if (error.code === 'ENOENT')
124
+ return null;
125
+ throw error;
126
+ });
127
+ if (!secretDirStat) {
128
+ return {
129
+ schema: 'sks.openrouter-key-removal.v1',
130
+ removed: false,
131
+ removed_paths: [],
132
+ blockers: []
133
+ };
134
+ }
135
+ if (!secretDirStat.isDirectory() || secretDirStat.isSymbolicLink()) {
136
+ return {
137
+ schema: 'sks.openrouter-key-removal.v1',
138
+ removed: false,
139
+ removed_paths: [],
140
+ blockers: ['openrouter_credential_secret_store_unsafe']
141
+ };
142
+ }
143
+ const existing = [];
144
+ for (const file of files) {
145
+ const inspected = await inspectConfinedPath(secretDir, file).catch(() => null);
146
+ if (!inspected) {
147
+ return {
148
+ schema: 'sks.openrouter-key-removal.v1',
149
+ removed: false,
150
+ removed_paths: [],
151
+ blockers: ['openrouter_credential_path_unsafe']
152
+ };
153
+ }
154
+ if (!inspected.exists)
155
+ continue;
156
+ if (inspected.leafSymlink || !inspected.stat?.isFile()) {
157
+ return {
158
+ schema: 'sks.openrouter-key-removal.v1',
159
+ removed: false,
160
+ removed_paths: [],
161
+ blockers: ['openrouter_credential_path_not_regular_file']
162
+ };
163
+ }
164
+ existing.push(file);
165
+ }
166
+ const removed = [];
167
+ for (const file of existing) {
168
+ await removeManagedPathVerified(secretDir, file);
169
+ removed.push(file);
170
+ }
171
+ return {
172
+ schema: 'sks.openrouter-key-removal.v1',
173
+ removed: removed.length > 0,
174
+ removed_paths: removed,
175
+ blockers: []
176
+ };
177
+ }
109
178
  async function ensureSecretDir(secretDir) {
110
179
  await fs.mkdir(secretDir, { recursive: true, mode: 0o700 });
111
180
  await fs.chmod(secretDir, 0o700).catch(() => undefined);