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,16 +1,21 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
+ import { constants as fsConstants } from 'node:fs';
2
3
  import fsp from 'node:fs/promises';
3
4
  import os from 'node:os';
4
5
  import path from 'node:path';
5
6
  import { messageOf as errorMessage } from '../errors/message.js';
6
- import { ensureDir, exists, readText, writeTextAtomic } from '../fsx.js';
7
- export function codexLbMigrationReceiptDir(home = process.env.HOME || os.homedir()) {
8
- return path.join(home, '.codex', 'sks-codex-lb-migrations');
7
+ import { ensureDir, exists, writeTextAtomic } from '../fsx.js';
8
+ import { withFileLock } from '../locks/file-lock.js';
9
+ export function desktopBridgeUnificationReceiptDir(home = process.env.HOME || os.homedir()) {
10
+ return path.join(home, '.codex', 'sks-desktop-bridge-migrations');
9
11
  }
10
- export function createCodexLbMigrationReceiptId(now = new Date()) {
12
+ export function desktopBridgeMigrationTransactionLockPath(home) {
13
+ return path.join(path.resolve(home), '.codex', 'sks', 'locks', 'desktop-bridge-migration.lock');
14
+ }
15
+ export function createDesktopBridgeUnificationReceiptId(now = new Date()) {
11
16
  return `${now.toISOString().replace(/[-:.TZ]/g, '')}-${randomUUID().slice(0, 8)}`;
12
17
  }
13
- export async function backupCodexLbMigrationFile(filePath, backupDir, ownedBySks) {
18
+ export async function backupDesktopBridgeMigrationFile(filePath, backupDir, ownedBySks) {
14
19
  const before = await readRegularFileOrMissing(filePath);
15
20
  if (!before) {
16
21
  return {
@@ -20,7 +25,7 @@ export async function backupCodexLbMigrationFile(filePath, backupDir, ownedBySks
20
25
  owned_by_sks: ownedBySks
21
26
  };
22
27
  }
23
- await ensureDir(backupDir);
28
+ await ensurePrivateDirectory(backupDir);
24
29
  const backupPath = path.join(backupDir, `${createHash('sha256').update(path.resolve(filePath)).digest('hex').slice(0, 16)}.before`);
25
30
  await writeBufferAtomic(backupPath, before, 0o600);
26
31
  return {
@@ -30,160 +35,337 @@ export async function backupCodexLbMigrationFile(filePath, backupDir, ownedBySks
30
35
  owned_by_sks: ownedBySks
31
36
  };
32
37
  }
33
- export async function finalizeCodexLbMigrationReceiptFiles(backups) {
38
+ export async function finalizeDesktopBridgeMigrationReceiptFiles(backups) {
34
39
  return Promise.all(backups.map(async (backup) => ({
35
40
  ...backup,
36
41
  after_sha256: await fileSha256OrMissing(backup.path)
37
42
  })));
38
43
  }
39
- export async function writeCodexLbMigrationReceipt(receipt, input = {}) {
40
- validateReceipt(receipt);
41
- const receiptDir = input.receiptDir || codexLbMigrationReceiptDir();
42
- const receiptPath = input.receiptPath || path.join(receiptDir, `${receipt.id}.json`);
44
+ export async function writeDesktopBridgeUnificationReceipt(receipt, input = {}) {
45
+ normalizeHistoricalSelectionCompatibility(receipt);
46
+ validateDesktopBridgeUnificationReceipt(receipt);
47
+ const receiptDir = input.receiptDir || desktopBridgeUnificationReceiptDir();
48
+ const receiptPath = input.receiptPath || path.join(receiptDir, `${receipt.receipt_id}.json`);
49
+ await ensurePrivateDirectory(path.dirname(receiptPath));
43
50
  await writeTextAtomic(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`, { mode: 0o600 });
44
51
  await fsp.chmod(receiptPath, 0o600).catch(() => { });
45
52
  return receiptPath;
46
53
  }
47
- export async function readCodexLbMigrationReceipt(receiptPath) {
48
- const text = await readText(receiptPath);
54
+ export async function readDesktopBridgeUnificationReceipt(receiptPath) {
55
+ const text = (await readSecureOwnerFile(receiptPath, 1024 * 1024, 'desktop_bridge_receipt')).toString('utf8');
49
56
  let parsed;
50
57
  try {
51
58
  parsed = JSON.parse(text);
52
59
  }
53
60
  catch {
54
- throw new Error('invalid_codex_lb_migration_receipt');
61
+ throw new Error('invalid_desktop_bridge_unification_receipt');
55
62
  }
56
- validateReceipt(parsed);
63
+ normalizeHistoricalSelectionCompatibility(parsed);
64
+ validateDesktopBridgeUnificationReceipt(parsed);
57
65
  return parsed;
58
66
  }
59
- export async function rollbackCodexLbMigrationReceipt(input) {
67
+ export async function rollbackDesktopBridgeUnificationReceipt(input) {
60
68
  let receipt;
61
69
  try {
62
70
  if (input.receipt) {
63
- validateReceipt(input.receipt);
71
+ normalizeHistoricalSelectionCompatibility(input.receipt);
72
+ validateDesktopBridgeUnificationReceipt(input.receipt);
64
73
  receipt = input.receipt;
65
74
  }
66
75
  else if (input.receiptPath) {
67
- receipt = await readCodexLbMigrationReceipt(input.receiptPath);
76
+ receipt = await readDesktopBridgeUnificationReceipt(input.receiptPath);
68
77
  }
69
78
  else {
70
- throw new Error('missing_codex_lb_migration_receipt');
79
+ throw new Error('missing_desktop_bridge_unification_receipt');
71
80
  }
72
81
  }
73
82
  catch (error) {
74
83
  return {
75
- schema: 'sks.codex-lb-migration-rollback.v1',
84
+ schema: 'sks.desktop-bridge-unification-rollback.v1',
76
85
  ok: false,
77
86
  status: 'invalid_receipt',
78
87
  receipt_id: null,
79
88
  restored_files: [],
89
+ credentials_overwritten: false,
90
+ auth_overwritten: false,
80
91
  conflicts: [],
81
92
  error: errorMessage(error)
82
93
  };
83
94
  }
84
- const conflicts = [];
85
- for (const file of receipt.files) {
86
- const currentSha = await fileSha256OrMissing(file.path);
87
- if (currentSha !== file.after_sha256) {
88
- conflicts.push({
89
- path: file.path,
90
- expected_after_sha256: file.after_sha256,
91
- current_sha256: currentSha,
92
- reason: 'current_file_changed_after_migration'
93
- });
94
- continue;
95
- }
96
- if (file.before_sha256 !== null) {
97
- if (!file.backup_path || !(await exists(file.backup_path))) {
98
- conflicts.push({
99
- path: file.path,
100
- expected_after_sha256: file.after_sha256,
101
- current_sha256: currentSha,
102
- reason: 'before_backup_missing'
103
- });
104
- continue;
105
- }
106
- const backupSha = await fileSha256OrMissing(file.backup_path);
107
- if (backupSha !== file.before_sha256) {
108
- conflicts.push({
109
- path: file.path,
110
- expected_after_sha256: file.after_sha256,
111
- current_sha256: currentSha,
112
- reason: 'before_backup_hash_mismatch'
113
- });
114
- }
115
- }
116
- }
117
- if (conflicts.length) {
95
+ if (receipt.migration_status === 'already_migrated') {
118
96
  return {
119
- schema: 'sks.codex-lb-migration-rollback.v1',
120
- ok: false,
121
- status: 'rollback_conflict',
122
- receipt_id: receipt.id,
97
+ schema: 'sks.desktop-bridge-unification-rollback.v1',
98
+ ok: true,
99
+ status: 'nothing_to_rollback',
100
+ receipt_id: receipt.receipt_id,
123
101
  restored_files: [],
124
- conflicts
102
+ credentials_overwritten: false,
103
+ auth_overwritten: false,
104
+ conflicts: []
125
105
  };
126
106
  }
127
- const currentBytes = new Map();
128
- for (const file of receipt.files)
129
- currentBytes.set(file.path, await readRegularFileOrMissing(file.path));
130
- const restoredFiles = [];
107
+ if (input.transactionLockHeld !== true) {
108
+ const configTarget = receipt.rollback_metadata.files.find((file) => file.kind === 'config');
109
+ if (!configTarget) {
110
+ return {
111
+ schema: 'sks.desktop-bridge-unification-rollback.v1',
112
+ ok: false,
113
+ status: 'invalid_receipt',
114
+ receipt_id: receipt.receipt_id,
115
+ restored_files: [],
116
+ credentials_overwritten: false,
117
+ auth_overwritten: false,
118
+ conflicts: [],
119
+ error: 'desktop_bridge_rollback_config_target_missing'
120
+ };
121
+ }
122
+ const home = path.dirname(path.dirname(path.resolve(configTarget.path)));
123
+ return withFileLock({
124
+ lockPath: desktopBridgeMigrationTransactionLockPath(home),
125
+ timeoutMs: 30_000,
126
+ staleMs: 120_000
127
+ }, () => rollbackDesktopBridgeUnificationReceipt({
128
+ receipt,
129
+ ...(input.beforeTargetLocks ? { beforeTargetLocks: input.beforeTargetLocks } : {}),
130
+ transactionLockHeld: true
131
+ }));
132
+ }
133
+ const files = receipt.rollback_metadata.files;
134
+ const initial = await inspectRollbackFiles(files);
135
+ if (initial.conflicts.length)
136
+ return rollbackConflict(receipt.receipt_id, initial.conflicts);
131
137
  try {
132
- for (const file of receipt.files) {
133
- if (file.before_sha256 === null) {
134
- await fsp.rm(file.path, { force: true });
135
- }
136
- else {
137
- const backupPath = file.backup_path;
138
- if (!backupPath)
139
- throw new Error(`before_backup_missing:${file.path}`);
140
- await writeBufferAtomic(file.path, await fsp.readFile(backupPath), 0o600);
138
+ await input.beforeTargetLocks?.();
139
+ return await withRollbackTargetLocks(files, async () => {
140
+ const locked = await inspectRollbackFiles(files, initial.snapshots);
141
+ if (locked.conflicts.length)
142
+ return rollbackConflict(receipt.receipt_id, locked.conflicts);
143
+ const restoredFiles = [];
144
+ try {
145
+ for (const file of files) {
146
+ if (file.before_sha256 === null) {
147
+ await fsp.rm(file.path, { force: true });
148
+ }
149
+ else {
150
+ if (!file.backup_path)
151
+ throw new Error(`before_backup_missing:${file.path}`);
152
+ const backup = locked.backupBytes.get(path.resolve(file.path));
153
+ if (!backup)
154
+ throw new Error(`before_backup_unavailable:${file.path}`);
155
+ await writeBufferAtomic(file.path, backup, 0o600);
156
+ }
157
+ if ((await fileSha256OrMissing(file.path)) !== file.before_sha256) {
158
+ throw new Error(`rollback_readback_failed:${file.path}`);
159
+ }
160
+ restoredFiles.push(file.path);
161
+ }
141
162
  }
142
- if ((await fileSha256OrMissing(file.path)) !== file.before_sha256) {
143
- throw new Error(`rollback_readback_failed:${file.path}`);
163
+ catch (error) {
164
+ for (const file of files) {
165
+ const snapshot = locked.snapshots.get(path.resolve(file.path));
166
+ try {
167
+ if (!snapshot || snapshot.bytes === null)
168
+ await fsp.rm(file.path, { force: true });
169
+ else
170
+ await writeBufferAtomic(file.path, snapshot.bytes, 0o600);
171
+ }
172
+ catch { }
173
+ }
174
+ return {
175
+ schema: 'sks.desktop-bridge-unification-rollback.v1',
176
+ ok: false,
177
+ status: 'rollback_failed',
178
+ receipt_id: receipt.receipt_id,
179
+ restored_files: [],
180
+ credentials_overwritten: false,
181
+ auth_overwritten: false,
182
+ conflicts: [],
183
+ error: errorMessage(error)
184
+ };
144
185
  }
145
- restoredFiles.push(file.path);
146
- }
186
+ return {
187
+ schema: 'sks.desktop-bridge-unification-rollback.v1',
188
+ ok: true,
189
+ status: 'rolled_back',
190
+ receipt_id: receipt.receipt_id,
191
+ restored_files: restoredFiles,
192
+ credentials_overwritten: false,
193
+ auth_overwritten: false,
194
+ conflicts: []
195
+ };
196
+ });
147
197
  }
148
198
  catch (error) {
149
- for (const [filePath, bytes] of currentBytes) {
150
- try {
151
- if (bytes === null)
152
- await fsp.rm(filePath, { force: true });
153
- else
154
- await writeBufferAtomic(filePath, bytes, 0o600);
155
- }
156
- catch { }
157
- }
158
199
  return {
159
- schema: 'sks.codex-lb-migration-rollback.v1',
200
+ schema: 'sks.desktop-bridge-unification-rollback.v1',
160
201
  ok: false,
161
202
  status: 'rollback_failed',
162
- receipt_id: receipt.id,
203
+ receipt_id: receipt.receipt_id,
163
204
  restored_files: [],
205
+ credentials_overwritten: false,
206
+ auth_overwritten: false,
164
207
  conflicts: [],
165
208
  error: errorMessage(error)
166
209
  };
167
210
  }
211
+ }
212
+ async function inspectRollbackFiles(files, expectedSnapshots) {
213
+ const backups = await inspectRollbackBackups(files);
214
+ const targets = await inspectRollbackTargets(files, expectedSnapshots);
168
215
  return {
169
- schema: 'sks.codex-lb-migration-rollback.v1',
170
- ok: true,
171
- status: 'rolled_back',
172
- receipt_id: receipt.id,
173
- restored_files: restoredFiles,
174
- conflicts: []
216
+ snapshots: targets.snapshots,
217
+ backupBytes: backups.backupBytes,
218
+ conflicts: [...backups.conflicts, ...targets.conflicts]
175
219
  };
176
220
  }
221
+ async function inspectRollbackBackups(files) {
222
+ const backupBytes = new Map();
223
+ const conflicts = [];
224
+ for (const file of files) {
225
+ if (file.before_sha256 === null)
226
+ continue;
227
+ const resolved = path.resolve(file.path);
228
+ if (!file.backup_path || !(await exists(file.backup_path))) {
229
+ conflicts.push(rollbackFileConflict(file, null, 'before_backup_missing'));
230
+ continue;
231
+ }
232
+ let backup;
233
+ try {
234
+ backup = await readSecureOwnerFile(file.backup_path, 16 * 1024 * 1024, 'desktop_bridge_backup');
235
+ }
236
+ catch {
237
+ conflicts.push(rollbackFileConflict(file, null, 'before_backup_unsafe'));
238
+ continue;
239
+ }
240
+ if (sha256(backup) !== file.before_sha256) {
241
+ conflicts.push(rollbackFileConflict(file, null, 'before_backup_hash_mismatch'));
242
+ continue;
243
+ }
244
+ backupBytes.set(resolved, backup);
245
+ }
246
+ return { backupBytes, conflicts };
247
+ }
248
+ async function inspectRollbackTargets(files, expectedSnapshots) {
249
+ const snapshots = new Map();
250
+ const conflicts = [];
251
+ for (const file of files) {
252
+ const resolved = path.resolve(file.path);
253
+ let snapshot;
254
+ try {
255
+ snapshot = await readSecureRollbackTarget(resolved);
256
+ snapshots.set(resolved, snapshot);
257
+ }
258
+ catch {
259
+ conflicts.push(rollbackFileConflict(file, null, 'current_file_unsafe'));
260
+ continue;
261
+ }
262
+ if (snapshot.sha256 !== file.after_sha256) {
263
+ conflicts.push(rollbackFileConflict(file, snapshot.sha256, 'current_file_changed_after_migration'));
264
+ continue;
265
+ }
266
+ const expected = expectedSnapshots?.get(resolved);
267
+ if (expected && !sameRollbackTargetIdentity(expected, snapshot)) {
268
+ conflicts.push(rollbackFileConflict(file, snapshot.sha256, 'current_file_identity_changed_during_rollback'));
269
+ }
270
+ }
271
+ return { snapshots, conflicts };
272
+ }
273
+ async function readSecureRollbackTarget(filePath) {
274
+ const parent = await fsp.lstat(path.dirname(filePath));
275
+ const expectedUid = typeof process.getuid === 'function' ? process.getuid() : null;
276
+ if (!parent.isDirectory() || parent.isSymbolicLink() || (expectedUid !== null && parent.uid !== expectedUid)) {
277
+ throw new Error(`desktop_bridge_rollback_parent_unsafe:${filePath}`);
278
+ }
279
+ const parentIdentity = parentStatIdentity(parent);
280
+ let entry;
281
+ try {
282
+ entry = await fsp.lstat(filePath);
283
+ }
284
+ catch (error) {
285
+ if (isMissingFileError(error)) {
286
+ return { bytes: null, sha256: null, identity: null, parent_identity: parentIdentity };
287
+ }
288
+ throw error;
289
+ }
290
+ if (!entry.isFile() || entry.isSymbolicLink())
291
+ throw new Error(`desktop_bridge_rollback_target_unsafe:${filePath}`);
292
+ const flags = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0);
293
+ const handle = await fsp.open(filePath, flags);
294
+ try {
295
+ const stat = await handle.stat();
296
+ if (!stat.isFile()
297
+ || stat.dev !== entry.dev
298
+ || stat.ino !== entry.ino
299
+ || (expectedUid !== null && stat.uid !== expectedUid)
300
+ || (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)) {
301
+ throw new Error(`desktop_bridge_rollback_target_unsafe:${filePath}`);
302
+ }
303
+ const bytes = await handle.readFile();
304
+ return {
305
+ bytes,
306
+ sha256: sha256(bytes),
307
+ identity: statIdentity(stat),
308
+ parent_identity: parentIdentity
309
+ };
310
+ }
311
+ finally {
312
+ await handle.close().catch(() => undefined);
313
+ }
314
+ }
315
+ function statIdentity(stat) {
316
+ return [stat.dev, stat.ino, stat.uid, stat.mode & 0o777, stat.size, stat.mtimeMs, stat.ctimeMs].join(':');
317
+ }
318
+ function parentStatIdentity(stat) {
319
+ return [stat.dev, stat.ino, stat.uid, stat.mode & 0o777].join(':');
320
+ }
321
+ function sameRollbackTargetIdentity(before, after) {
322
+ return before.identity === after.identity && before.parent_identity === after.parent_identity;
323
+ }
324
+ function rollbackFileConflict(file, currentSha, reason) {
325
+ return {
326
+ path: file.path,
327
+ expected_after_sha256: file.after_sha256,
328
+ current_sha256: currentSha,
329
+ reason
330
+ };
331
+ }
332
+ function rollbackConflict(receiptId, conflicts) {
333
+ return {
334
+ schema: 'sks.desktop-bridge-unification-rollback.v1',
335
+ ok: false,
336
+ status: 'rollback_conflict',
337
+ receipt_id: receiptId,
338
+ restored_files: [],
339
+ credentials_overwritten: false,
340
+ auth_overwritten: false,
341
+ conflicts
342
+ };
343
+ }
344
+ export function desktopBridgeRollbackTargetLockPath(filePath) {
345
+ return `${path.resolve(filePath)}.lock`;
346
+ }
347
+ async function withRollbackTargetLocks(files, operation) {
348
+ const targets = [...new Set(files.map((file) => path.resolve(file.path)))]
349
+ .sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
350
+ const acquire = async (index) => {
351
+ const target = targets[index];
352
+ if (!target)
353
+ return operation();
354
+ return withFileLock({
355
+ lockPath: desktopBridgeRollbackTargetLockPath(target),
356
+ timeoutMs: 10_000,
357
+ staleMs: 60_000
358
+ }, () => acquire(index + 1));
359
+ };
360
+ return acquire(0);
361
+ }
177
362
  export async function fileSha256OrMissing(filePath) {
178
363
  const bytes = await readRegularFileOrMissing(filePath);
179
364
  return bytes === null ? null : sha256(bytes);
180
365
  }
181
366
  async function readRegularFileOrMissing(filePath) {
182
367
  try {
183
- const stat = await fsp.lstat(filePath);
184
- if (!stat.isFile() || stat.isSymbolicLink())
185
- throw new Error(`migration_file_not_regular:${filePath}`);
186
- return await fsp.readFile(filePath);
368
+ return await readFileWithoutFollowingSymlinks(filePath, Number.MAX_SAFE_INTEGER, 'migration_file');
187
369
  }
188
370
  catch (error) {
189
371
  if (isMissingFileError(error))
@@ -191,6 +373,44 @@ async function readRegularFileOrMissing(filePath) {
191
373
  throw error;
192
374
  }
193
375
  }
376
+ async function readSecureOwnerFile(filePath, maxBytes, prefix) {
377
+ return readFileWithoutFollowingSymlinks(filePath, maxBytes, prefix, true);
378
+ }
379
+ async function readFileWithoutFollowingSymlinks(filePath, maxBytes, prefix, ownerOnly = false) {
380
+ const flags = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0);
381
+ const handle = await fsp.open(filePath, flags);
382
+ try {
383
+ const stat = await handle.stat();
384
+ if (!stat.isFile())
385
+ throw new Error(`${prefix}_not_regular:${filePath}`);
386
+ if (stat.size > maxBytes)
387
+ throw new Error(`${prefix}_too_large:${filePath}`);
388
+ if (ownerOnly) {
389
+ const expectedUid = typeof process.getuid === 'function' ? process.getuid() : null;
390
+ if (expectedUid !== null && stat.uid !== expectedUid)
391
+ throw new Error(`${prefix}_owner_mismatch:${filePath}`);
392
+ if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)
393
+ throw new Error(`${prefix}_permissions_unsafe:${filePath}`);
394
+ }
395
+ return await handle.readFile();
396
+ }
397
+ finally {
398
+ await handle.close().catch(() => { });
399
+ }
400
+ }
401
+ async function ensurePrivateDirectory(directory) {
402
+ await fsp.mkdir(directory, { recursive: true, mode: 0o700 });
403
+ const stat = await fsp.lstat(directory);
404
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
405
+ throw new Error(`desktop_bridge_private_directory_invalid:${directory}`);
406
+ }
407
+ const expectedUid = typeof process.getuid === 'function' ? process.getuid() : null;
408
+ if (expectedUid !== null && stat.uid !== expectedUid) {
409
+ throw new Error(`desktop_bridge_private_directory_owner_mismatch:${directory}`);
410
+ }
411
+ if (process.platform !== 'win32')
412
+ await fsp.chmod(directory, 0o700);
413
+ }
194
414
  async function writeBufferAtomic(filePath, bytes, mode) {
195
415
  await ensureDir(path.dirname(filePath));
196
416
  const tempPath = `${filePath}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
@@ -212,31 +432,253 @@ async function writeBufferAtomic(filePath, bytes, mode) {
212
432
  throw error;
213
433
  }
214
434
  }
215
- function validateReceipt(value) {
216
- if (!value || typeof value !== 'object')
217
- throw new Error('invalid_codex_lb_migration_receipt');
435
+ function validateDesktopBridgeUnificationReceipt(value) {
436
+ if (!value || typeof value !== 'object') {
437
+ throw new Error('invalid_desktop_bridge_unification_receipt');
438
+ }
218
439
  const receipt = value;
219
- if (receipt.schema !== 'sks.codex-lb-migration-receipt.v1'
220
- || typeof receipt.id !== 'string'
221
- || !receipt.id
440
+ if (receipt.schema !== 'sks.desktop-bridge-unification-receipt.v1'
441
+ || typeof receipt.receipt_id !== 'string'
442
+ || !/^[A-Za-z0-9._-]{1,160}$/.test(receipt.receipt_id)
222
443
  || typeof receipt.created_at !== 'string'
223
- || typeof receipt.from_mode !== 'string'
224
- || typeof receipt.to_mode !== 'string'
225
- || !Array.isArray(receipt.files)
226
- || typeof receipt.oauth_preserved !== 'boolean') {
227
- throw new Error('invalid_codex_lb_migration_receipt');
228
- }
229
- for (const file of receipt.files) {
230
- if (!file
231
- || typeof file.path !== 'string'
232
- || !file.path
233
- || (file.before_sha256 !== null && typeof file.before_sha256 !== 'string')
234
- || (file.after_sha256 !== null && typeof file.after_sha256 !== 'string')
235
- || (file.backup_path !== null && typeof file.backup_path !== 'string')
236
- || typeof file.owned_by_sks !== 'boolean') {
237
- throw new Error('invalid_codex_lb_migration_receipt_file');
444
+ || receipt.baseline_version !== '8.1.2'
445
+ || receipt.target_version !== '8.1.3'
446
+ || typeof receipt.config_before_sha256 !== 'string'
447
+ || typeof receipt.config_after_sha256 !== 'string'
448
+ || typeof receipt.auth_before_sha256 !== 'string'
449
+ || typeof receipt.auth_after_sha256 !== 'string'
450
+ || typeof receipt.auth_semantic_identity_preserved !== 'boolean'
451
+ || !receipt.historical_state
452
+ || typeof receipt.historical_state !== 'object'
453
+ || !Array.isArray(receipt.migrated_profiles)
454
+ || receipt.credentials_deleted !== false
455
+ || receipt.new_runtime !== 'desktop-bridge'
456
+ || !Array.isArray(receipt.backup_paths)
457
+ || typeof receipt.rollback_supported !== 'boolean'
458
+ || !Array.isArray(receipt.blockers)
459
+ || (receipt.migration_status !== 'migrated' && receipt.migration_status !== 'already_migrated')
460
+ || receipt.rollback_metadata?.schema !== 'sks.desktop-bridge-unification-rollback-metadata.v1'
461
+ || !Array.isArray(receipt.rollback_metadata.files)) {
462
+ throw new Error('invalid_desktop_bridge_unification_receipt');
463
+ }
464
+ for (const key of [
465
+ 'desktop_mode',
466
+ 'historical_provider_selection',
467
+ 'model_provider',
468
+ 'catalog_path'
469
+ ]) {
470
+ const entry = receipt.historical_state[key];
471
+ if (entry !== null && typeof entry !== 'string') {
472
+ throw new Error('invalid_desktop_bridge_unification_receipt_historical_state');
238
473
  }
239
474
  }
475
+ if (receipt.migrated_profiles.some((entry) => entry !== 'codex-lb' && entry !== 'openrouter')) {
476
+ throw new Error('invalid_desktop_bridge_unification_receipt_provider');
477
+ }
478
+ if (receipt.new_catalog_generation !== null
479
+ && typeof receipt.new_catalog_generation !== 'string') {
480
+ throw new Error('invalid_desktop_bridge_unification_receipt_catalog_generation');
481
+ }
482
+ const seenPaths = new Set();
483
+ const canonicalHomes = new Set();
484
+ for (const file of receipt.rollback_metadata.files) {
485
+ validateDesktopBridgeRollbackFile(file);
486
+ if (file.backup_path !== null) {
487
+ validateDesktopBridgeBackupPath(file.backup_path, receipt.receipt_id);
488
+ }
489
+ const resolved = path.resolve(file.path);
490
+ if (seenPaths.has(resolved)) {
491
+ throw new Error('invalid_desktop_bridge_unification_receipt_duplicate_path');
492
+ }
493
+ seenPaths.add(resolved);
494
+ canonicalHomes.add(canonicalHomeForRollbackFile(file));
495
+ }
496
+ if (canonicalHomes.size > 1) {
497
+ throw new Error('desktop_bridge_rollback_metadata_home_mismatch');
498
+ }
499
+ const metadataBackupPaths = new Set(receipt.rollback_metadata.files
500
+ .map((file) => file.backup_path)
501
+ .filter((entry) => typeof entry === 'string'));
502
+ if (receipt.backup_paths.some((entry) => typeof entry !== 'string' || !metadataBackupPaths.has(entry))
503
+ || metadataBackupPaths.size !== receipt.backup_paths.length) {
504
+ throw new Error('invalid_desktop_bridge_unification_receipt_backup_paths');
505
+ }
506
+ if (receipt.blockers.some((entry) => typeof entry !== 'string')) {
507
+ throw new Error('invalid_desktop_bridge_unification_receipt_blockers');
508
+ }
509
+ if (receipt.migration_status === 'already_migrated') {
510
+ if (receipt.rollback_supported !== false
511
+ || receipt.backup_paths.length !== 0
512
+ || receipt.rollback_metadata.files.length !== 0
513
+ || receipt.config_before_sha256 !== receipt.config_after_sha256
514
+ || receipt.auth_before_sha256 !== receipt.auth_after_sha256
515
+ || receipt.auth_semantic_identity_preserved !== true) {
516
+ throw new Error('invalid_desktop_bridge_unification_noop_receipt');
517
+ }
518
+ }
519
+ else if (receipt.rollback_supported !== true) {
520
+ throw new Error('invalid_desktop_bridge_unification_migrated_receipt');
521
+ }
522
+ }
523
+ function normalizeHistoricalSelectionCompatibility(value) {
524
+ if (!value || typeof value !== 'object')
525
+ return;
526
+ const historicalState = value.historical_state;
527
+ if (!historicalState || typeof historicalState !== 'object')
528
+ return;
529
+ const state = historicalState;
530
+ const compatibilityKey = ['provider', 'mode'].join('_');
531
+ const compatibilityValue = state[compatibilityKey];
532
+ if (compatibilityValue === undefined)
533
+ return;
534
+ const currentValue = state.historical_provider_selection;
535
+ if (currentValue !== undefined && currentValue !== compatibilityValue) {
536
+ throw new Error('invalid_desktop_bridge_unification_receipt_historical_selection_conflict');
537
+ }
538
+ if (currentValue === undefined) {
539
+ state.historical_provider_selection = compatibilityValue;
540
+ }
541
+ delete state[compatibilityKey];
542
+ }
543
+ function validateDesktopBridgeBackupPath(backupPath, receiptId) {
544
+ const resolved = path.resolve(backupPath);
545
+ const filesDir = path.dirname(resolved);
546
+ const receiptDir = path.dirname(filesDir);
547
+ if (path.basename(resolved).match(/^[a-f0-9]{16}\.before$/) === null
548
+ || path.basename(filesDir) !== 'files'
549
+ || path.basename(receiptDir) !== receiptId) {
550
+ throw new Error('desktop_bridge_rollback_backup_path_not_canonical');
551
+ }
552
+ }
553
+ function validateDesktopBridgeRollbackFile(file) {
554
+ if (!file || typeof file !== 'object') {
555
+ throw new Error('invalid_desktop_bridge_unification_rollback_file');
556
+ }
557
+ const entry = file;
558
+ const kinds = [
559
+ 'config',
560
+ 'bridge_settings',
561
+ 'provider_registry',
562
+ 'catalog_binding',
563
+ 'route_policy',
564
+ 'launchd_state'
565
+ ];
566
+ if (!entry.kind
567
+ || !kinds.includes(entry.kind)
568
+ || typeof entry.path !== 'string'
569
+ || !entry.path
570
+ || (entry.before_sha256 !== null && typeof entry.before_sha256 !== 'string')
571
+ || (entry.after_sha256 !== null && typeof entry.after_sha256 !== 'string')
572
+ || (entry.backup_path !== null && typeof entry.backup_path !== 'string')
573
+ || typeof entry.owned_by_sks !== 'boolean') {
574
+ throw new Error('invalid_desktop_bridge_unification_rollback_file');
575
+ }
576
+ const hashPattern = /^[a-f0-9]{64}$/;
577
+ if ((entry.before_sha256 !== null && !hashPattern.test(entry.before_sha256))
578
+ || (entry.after_sha256 !== null && !hashPattern.test(entry.after_sha256))
579
+ || (entry.before_sha256 === null && entry.backup_path !== null)
580
+ || (entry.before_sha256 !== null && entry.backup_path === null)
581
+ || (entry.backup_path !== null && !path.isAbsolute(entry.backup_path))) {
582
+ throw new Error('invalid_desktop_bridge_unification_rollback_file');
583
+ }
584
+ const resolved = path.resolve(entry.path);
585
+ const basename = path.basename(resolved).toLowerCase();
586
+ const segments = resolved.toLowerCase().split(path.sep);
587
+ const sensitiveBasenames = new Set([
588
+ 'auth.json',
589
+ 'sks-codex-lb.env',
590
+ 'openrouter-api-key',
591
+ 'openrouter-api-key.json'
592
+ ]);
593
+ if (sensitiveBasenames.has(basename)
594
+ || segments.includes('secrets')
595
+ || /(?:credential|api-key|secret)/i.test(basename)) {
596
+ throw new Error('desktop_bridge_rollback_secret_file_forbidden');
597
+ }
598
+ if (!path.isAbsolute(entry.path)) {
599
+ throw new Error('desktop_bridge_rollback_metadata_path_not_canonical');
600
+ }
601
+ if (entry.kind === 'config' ? entry.owned_by_sks : !entry.owned_by_sks) {
602
+ throw new Error('desktop_bridge_rollback_metadata_ownership_invalid');
603
+ }
604
+ }
605
+ function canonicalHomeForRollbackFile(file) {
606
+ const resolved = path.resolve(file.path);
607
+ const retiredArtifact = retiredDesktopBridgeRuntimeArtifact(resolved);
608
+ if (retiredArtifact !== null) {
609
+ throw new Error(`desktop_bridge_rollback_retired_runtime_target_forbidden:${retiredArtifact}`);
610
+ }
611
+ if (file.kind === 'config') {
612
+ const codexHome = path.dirname(resolved);
613
+ const home = path.dirname(codexHome);
614
+ if (path.basename(resolved) === 'config.toml'
615
+ && path.basename(codexHome) === '.codex'
616
+ && path.join(home, '.codex', 'config.toml') === resolved)
617
+ return home;
618
+ }
619
+ else if (file.kind === 'launchd_state') {
620
+ const launchAgents = path.dirname(resolved);
621
+ const library = path.dirname(launchAgents);
622
+ const home = path.dirname(library);
623
+ if (path.basename(resolved) === 'com.sneakoscope.desktop-bridge.plist'
624
+ && path.basename(launchAgents) === 'LaunchAgents'
625
+ && path.basename(library) === 'Library'
626
+ && path.join(home, 'Library', 'LaunchAgents', path.basename(resolved)) === resolved)
627
+ return home;
628
+ }
629
+ else {
630
+ const expectedBasename = {
631
+ bridge_settings: 'desktop-bridge-settings.json',
632
+ provider_registry: 'sks-bridge-provider-registry.json',
633
+ catalog_binding: 'sks-bridge-active-generation.json',
634
+ route_policy: 'sks-bridge-route-policy.json'
635
+ };
636
+ const sksDir = path.dirname(resolved);
637
+ const codexHome = path.dirname(sksDir);
638
+ const home = path.dirname(codexHome);
639
+ if (path.basename(resolved) === expectedBasename[file.kind]
640
+ && path.basename(sksDir) === 'sks'
641
+ && path.basename(codexHome) === '.codex'
642
+ && path.join(home, '.codex', 'sks', expectedBasename[file.kind]) === resolved)
643
+ return home;
644
+ }
645
+ throw new Error(`desktop_bridge_rollback_metadata_path_not_canonical:${file.kind}`);
646
+ }
647
+ function retiredDesktopBridgeRuntimeArtifact(resolvedPath) {
648
+ const basename = path.basename(resolvedPath);
649
+ const runtimeDir = path.dirname(resolvedPath);
650
+ const codexHome = path.dirname(runtimeDir);
651
+ const home = path.dirname(codexHome);
652
+ if (path.basename(runtimeDir) === 'sks'
653
+ && path.basename(codexHome) === '.codex'
654
+ && path.join(home, '.codex', 'sks', basename) === resolvedPath) {
655
+ if (basename === 'codex-lb-desktop-bridge-settings.json')
656
+ return 'settings';
657
+ if (basename === 'codex-lb-desktop-bridge.json')
658
+ return 'state';
659
+ }
660
+ const logsDir = path.dirname(resolvedPath);
661
+ const logsRuntimeDir = path.dirname(logsDir);
662
+ const logsCodexHome = path.dirname(logsRuntimeDir);
663
+ const logsHome = path.dirname(logsCodexHome);
664
+ if (path.basename(logsDir) === 'logs'
665
+ && path.basename(logsRuntimeDir) === 'sks'
666
+ && path.basename(logsCodexHome) === '.codex'
667
+ && path.join(logsHome, '.codex', 'sks', 'logs', basename) === resolvedPath) {
668
+ if (basename === 'codex-lb-desktop-bridge.out.log')
669
+ return 'stdout_log';
670
+ if (basename === 'codex-lb-desktop-bridge.err.log')
671
+ return 'stderr_log';
672
+ }
673
+ const launchAgents = path.dirname(resolvedPath);
674
+ const library = path.dirname(launchAgents);
675
+ const launchdHome = path.dirname(library);
676
+ if (basename === 'com.sneakoscope.codex-lb-desktop-bridge.plist'
677
+ && path.basename(launchAgents) === 'LaunchAgents'
678
+ && path.basename(library) === 'Library'
679
+ && path.join(launchdHome, 'Library', 'LaunchAgents', 'com.sneakoscope.codex-lb-desktop-bridge.plist') === resolvedPath)
680
+ return 'plist';
681
+ return null;
240
682
  }
241
683
  function sha256(bytes) {
242
684
  return createHash('sha256').update(bytes).digest('hex');