sneakoscope 6.0.2 → 6.1.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 (242) hide show
  1. package/README.md +5 -3
  2. package/config/codex-releases/{rust-v0.142.0.json → rust-v0.144.1.json} +6 -6
  3. package/crates/sks-core/Cargo.lock +1 -1
  4. package/crates/sks-core/Cargo.toml +1 -1
  5. package/dist/cli/install-helpers.js +255 -440
  6. package/dist/cli/router.js +53 -4
  7. package/dist/commands/codex-app.js +8 -3
  8. package/dist/commands/codex-lb.js +169 -40
  9. package/dist/commands/codex.js +6 -5
  10. package/dist/commands/doctor.js +60 -26
  11. package/dist/commands/versioning.js +7 -0
  12. package/dist/config/skills-manifest.json +57 -57
  13. package/dist/core/agents/agent-central-ledger.js +9 -0
  14. package/dist/core/agents/agent-cleanup-executor.js +334 -41
  15. package/dist/core/agents/agent-cleanup.js +20 -1
  16. package/dist/core/agents/agent-command-surface.js +2 -1
  17. package/dist/core/agents/agent-effort-policy.js +46 -33
  18. package/dist/core/agents/agent-janitor.js +9 -10
  19. package/dist/core/agents/agent-namespace-safety.js +56 -0
  20. package/dist/core/agents/agent-orchestrator.js +26 -4
  21. package/dist/core/agents/agent-output-validator.js +1 -1
  22. package/dist/core/agents/agent-proof-evidence.js +16 -2
  23. package/dist/core/agents/agent-role-config.js +3 -6
  24. package/dist/core/agents/agent-roster.js +2 -2
  25. package/dist/core/agents/agent-runner-codex-exec.js +2 -0
  26. package/dist/core/agents/agent-runner-process.js +1 -0
  27. package/dist/core/agents/fast-mode-policy.js +3 -5
  28. package/dist/core/agents/native-cli-session-swarm.js +18 -1
  29. package/dist/core/agents/native-worker-backend-router.js +180 -108
  30. package/dist/core/agents/parallel-write-fixture.js +15 -10
  31. package/dist/core/agents/runtime-proof-summary.js +21 -3
  32. package/dist/core/agents/work-partition/repo-inventory.js +68 -7
  33. package/dist/core/code-structure.js +5 -4
  34. package/dist/core/codex/codex-config-guard.js +31 -9
  35. package/dist/core/codex-app/codex-app-fast-ui-repair.js +51 -20
  36. package/dist/core/codex-app/codex-app-restart.js +38 -8
  37. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +11 -3
  38. package/dist/core/codex-app/sks-menubar.js +100 -40
  39. package/dist/core/codex-app.js +60 -17
  40. package/dist/core/codex-compat/codex-hook-warning-detector.js +10 -1
  41. package/dist/core/codex-compat/codex-release-manifest.js +6 -6
  42. package/dist/core/codex-control/codex-0139-doctor-real-probe.js +7 -1
  43. package/dist/core/codex-control/codex-0139-image-path-real-probe.js +14 -8
  44. package/dist/core/codex-control/codex-0139-probe-runner.js +103 -28
  45. package/dist/core/codex-control/codex-0139-real-probe-summary.js +7 -3
  46. package/dist/core/codex-control/codex-0139-real-probes.js +21 -6
  47. package/dist/core/codex-control/codex-0139-sandbox-real-probe.js +39 -13
  48. package/dist/core/codex-control/codex-0139-web-search-probe.js +11 -8
  49. package/dist/core/codex-control/{codex-0142-capability.js → codex-0144-capability.js} +22 -22
  50. package/dist/core/codex-control/codex-0144-collab-agent-real-probe.js +63 -0
  51. package/dist/core/codex-control/codex-model-metadata.js +8 -4
  52. package/dist/core/codex-control/codex-multi-agent-event-normalizer.js +5 -1
  53. package/dist/core/codex-control/codex-reliability-shield.js +4 -1
  54. package/dist/core/codex-control/codex-sdk-adapter.js +53 -6
  55. package/dist/core/codex-control/codex-sdk-config-policy.js +2 -1
  56. package/dist/core/codex-control/codex-sdk-sandbox-policy.js +6 -3
  57. package/dist/core/codex-lb/codex-lb-env.js +107 -0
  58. package/dist/core/codex-lb/codex-lb-tool-catalog.js +478 -0
  59. package/dist/core/codex-native/codex-native-feature-broker.js +22 -22
  60. package/dist/core/codex-native/codex-native-feature-matrix.js +2 -2
  61. package/dist/core/codex-native/native-capability-postcheck.js +16 -8
  62. package/dist/core/codex-native/native-capability-repair-matrix.js +76 -27
  63. package/dist/core/codex-plugins/codex-plugin-json.js +67 -21
  64. package/dist/core/codex-plugins/codex-plugin-repair.js +118 -0
  65. package/dist/core/codex-runtime/codex-desktop-config-policy.js +306 -0
  66. package/dist/core/commands/agent-command.js +19 -0
  67. package/dist/core/commands/basic-cli.js +45 -24
  68. package/dist/core/commands/gc-command.js +27 -4
  69. package/dist/core/commands/image-ux-review-command.js +55 -24
  70. package/dist/core/commands/mad-sks-command.js +152 -132
  71. package/dist/core/commands/naruto-command.js +41 -10
  72. package/dist/core/commands/pipeline-command.js +1 -0
  73. package/dist/core/commands/wiki-command.js +179 -83
  74. package/dist/core/computer-use-live-evidence.js +166 -3
  75. package/dist/core/computer-use-status.js +5 -2
  76. package/dist/core/daemon/sksd-hook-daemon-entrypoint.js +2 -2
  77. package/dist/core/daemon/sksd-hook-daemon.js +171 -28
  78. package/dist/core/daemon/sksd-hook-dispatch.js +2 -2
  79. package/dist/core/dfix/verification-selector.js +6 -4
  80. package/dist/core/doctor/browser-use-repair.js +54 -13
  81. package/dist/core/doctor/codex-0139-doctor.js +3 -2
  82. package/dist/core/doctor/computer-use-repair.js +28 -39
  83. package/dist/core/doctor/doctor-codex-startup-repair.js +19 -9
  84. package/dist/core/doctor/doctor-context7-repair.js +1 -1
  85. package/dist/core/doctor/doctor-dirty-planner.js +1 -1
  86. package/dist/core/doctor/doctor-native-capability-repair.js +74 -3
  87. package/dist/core/doctor/imagegen-repair.js +47 -12
  88. package/dist/core/doctor/legacy-global-hook-cleanup.js +179 -0
  89. package/dist/core/feature-registry.js +25 -31
  90. package/dist/core/fsx.js +54 -6
  91. package/dist/core/harness-conflicts.js +1 -1
  92. package/dist/core/hooks-runtime/code-pack-freshness-preflight.js +5 -4
  93. package/dist/core/hooks-runtime/hook-invocation-dedupe.js +147 -0
  94. package/dist/core/hooks-runtime/hook-io.js +6 -0
  95. package/dist/core/hooks-runtime.js +17 -2
  96. package/dist/core/image/image-artifact-path-contract.js +8 -6
  97. package/dist/core/image/image-artifact-registry.js +5 -1
  98. package/dist/core/image-ux-review/imagegen-adapter.js +5 -2
  99. package/dist/core/imagegen/imagegen-capability.js +10 -13
  100. package/dist/core/imagegen/require-imagegen.js +37 -11
  101. package/dist/core/managed-assets/managed-assets-manifest.js +1 -5
  102. package/dist/core/naruto/naruto-work-graph.js +70 -4
  103. package/dist/core/naruto/naruto-write-e2e.js +3 -4
  104. package/dist/core/ops/reporting.js +3 -0
  105. package/dist/core/pipeline-internals/runtime-core.js +12 -7
  106. package/dist/core/pipeline-internals/runtime-gates.js +17 -1
  107. package/dist/core/ppt/style-tokens.js +1 -1
  108. package/dist/core/ppt-review/index.js +1 -1
  109. package/dist/core/ppt-review/slide-exporter.js +23 -17
  110. package/dist/core/ppt-review/slide-imagegen-review.js +1 -1
  111. package/dist/core/ppt-review/slide-issue-extraction.js +4 -2
  112. package/dist/core/product-design-plugin.js +2 -2
  113. package/dist/core/proof/route-finalizer.js +1 -1
  114. package/dist/core/proof-field.js +3 -3
  115. package/dist/core/provider/model-router.js +68 -3
  116. package/dist/core/release/gate-manifest.js +18 -12
  117. package/dist/core/release/gate-pack-manifest.js +1 -1
  118. package/dist/core/release/package-size-budget.js +2 -0
  119. package/dist/core/release/release-gate-affected-selector.js +1 -1
  120. package/dist/core/release/release-gate-dag.js +43 -3
  121. package/dist/core/release/release-gate-hermetic-env.js +28 -17
  122. package/dist/core/release/release-real-contract.js +416 -0
  123. package/dist/core/retention/retention-budget.js +25 -6
  124. package/dist/core/retention.js +980 -101
  125. package/dist/core/routes/constants.js +1 -1
  126. package/dist/core/routes/evidence.js +4 -4
  127. package/dist/core/routes/model-mode-router.js +3 -0
  128. package/dist/core/routes/ppt-policy.js +1 -1
  129. package/dist/core/routes.js +11 -12
  130. package/dist/core/secret-redaction.js +4 -0
  131. package/dist/core/triwiki/code-pack.js +8 -2
  132. package/dist/core/triwiki/triwiki-cache-key.js +108 -29
  133. package/dist/core/triwiki-attention.js +13 -6
  134. package/dist/core/triwiki-provenance.js +274 -0
  135. package/dist/core/triwiki-runtime.js +21 -7
  136. package/dist/core/triwiki-wrongness/wrongness-ledger.js +178 -59
  137. package/dist/core/triwiki-wrongness/wrongness-retrieval.js +11 -2
  138. package/dist/core/triwiki-wrongness/wrongness-trust-policy.js +2 -0
  139. package/dist/core/update/update-migration-state.js +1 -1
  140. package/dist/core/update/update-notice.js +23 -0
  141. package/dist/core/update-check.js +69 -25
  142. package/dist/core/version.js +1 -1
  143. package/dist/core/wiki-coordinate.js +128 -12
  144. package/dist/core/work-order-ledger.js +168 -9
  145. package/dist/core/zellij/zellij-command.js +12 -1
  146. package/dist/core/zellij/zellij-dashboard-pane.js +3 -3
  147. package/dist/core/zellij/zellij-launcher.js +15 -14
  148. package/dist/core/zellij/zellij-pane-proof.js +32 -6
  149. package/dist/core/zellij/zellij-worker-pane-manager.js +5 -3
  150. package/dist/scripts/agent-fast-mode-default-check.js +48 -29
  151. package/dist/scripts/agent-role-config-repair-check.js +25 -4
  152. package/dist/scripts/agent-wiki-context-proof-check.js +16 -2
  153. package/dist/scripts/all-feature-deep-completion-check.js +14 -4
  154. package/dist/scripts/build-dist.js +17 -5
  155. package/dist/scripts/canonical-test-runner.js +188 -0
  156. package/dist/scripts/check-command-module-budget.js +38 -16
  157. package/dist/scripts/check-legacy-free.js +1 -1
  158. package/dist/scripts/codex-0139-interrupt-agent-check.js +5 -1
  159. package/dist/scripts/{codex-0142-app-server-v2-check.js → codex-0144-app-server-v2-check.js} +11 -10
  160. package/dist/scripts/{codex-0142-binary-identity-check.js → codex-0144-binary-identity-check.js} +4 -4
  161. package/dist/scripts/codex-0144-capability-check.js +21 -0
  162. package/dist/scripts/{codex-0139-real-probes-check.js → codex-0144-core-real-probes-check.js} +19 -5
  163. package/dist/scripts/{codex-0142-doctor-wiring-check.js → codex-0144-doctor-wiring-check.js} +6 -6
  164. package/dist/scripts/{codex-0142-manifest-check.js → codex-0144-manifest-check.js} +11 -3
  165. package/dist/scripts/{codex-0142-policy-check.js → codex-0144-policy-check.js} +2 -2
  166. package/dist/scripts/{codex-0142-thread-store-check.js → codex-0144-thread-store-check.js} +3 -3
  167. package/dist/scripts/codex-app-provider-model-ui-check.js +57 -8
  168. package/dist/scripts/codex-app-ui-preservation-check.js +35 -4
  169. package/dist/scripts/codex-control-side-effect-scope-check.js +2 -2
  170. package/dist/scripts/codex-effort-auto-discovery-check.js +1 -1
  171. package/dist/scripts/codex-lb-fast-mode-truth-check.js +1 -0
  172. package/dist/scripts/{codex-lb-gpt55-fast-profile-check.js → codex-lb-gpt56-fast-profile-check.js} +56 -5
  173. package/dist/scripts/codex-model-metadata-check.js +1 -1
  174. package/dist/scripts/codex-sdk-sandbox-policy-check.js +5 -1
  175. package/dist/scripts/codex-sdk-version-compat-check.js +2 -1
  176. package/dist/scripts/dfix-fixture-check.js +1 -1
  177. package/dist/scripts/docs-truthfulness-check.js +1 -1
  178. package/dist/scripts/doctor-codex-startup-repair-check.js +35 -10
  179. package/dist/scripts/doctor-context7-repair-check.js +2 -1
  180. package/dist/scripts/doctor-fix-proves-codex-read-check.js +1 -1
  181. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +34 -7
  182. package/dist/scripts/doctor-imagegen-repair-check.js +14 -4
  183. package/dist/scripts/doctor-startup-config-repair-blackbox.js +1 -1
  184. package/dist/scripts/doctor-startup-config-repair-check.js +1 -1
  185. package/dist/scripts/gate-policy-audit-check.js +2 -2
  186. package/dist/scripts/install-update-preserves-config-check.js +1 -1
  187. package/dist/scripts/legacy-gate-inventory-check.js +7 -5
  188. package/dist/scripts/legacy-strong-inventory-check.js +7 -5
  189. package/dist/scripts/legacy-upgrade-matrix-check.js +5 -5
  190. package/dist/scripts/lib/native-cli-session-swarm-check-lib.js +175 -14
  191. package/dist/scripts/mad-sks-app-ui-no-mutation-check.js +166 -6
  192. package/dist/scripts/mutation-callsite-coverage-check.js +0 -11
  193. package/dist/scripts/naruto-shadow-clone-swarm-check.js +88 -50
  194. package/dist/scripts/npm-publish-performance-check.js +2 -1
  195. package/dist/scripts/package-published-contract-check.js +9 -0
  196. package/dist/scripts/packlist-performance-check.js +5 -2
  197. package/dist/scripts/postinstall-safe-side-effects-check.js +8 -3
  198. package/dist/scripts/ppt-no-mock-as-real-check.js +3 -1
  199. package/dist/scripts/product-design-plugin-routing-check.js +1 -1
  200. package/dist/scripts/release-check-stamp.js +182 -2
  201. package/dist/scripts/release-dag-full-coverage-check.js +7 -7
  202. package/dist/scripts/release-dynamic-presets-check.js +8 -0
  203. package/dist/scripts/release-gate-dag-runner-check.js +3 -2
  204. package/dist/scripts/release-gate-dag-runner.js +24 -11
  205. package/dist/scripts/release-gate-existence-audit.js +8 -9
  206. package/dist/scripts/release-gate-script-parity-check.js +1 -1
  207. package/dist/scripts/release-real-check.js +368 -157
  208. package/dist/scripts/release-runtime-truth-matrix-check.js +22 -0
  209. package/dist/scripts/route-proof-artifact-structure-check.js +1 -0
  210. package/dist/scripts/scheduler-batch-dispatch-check.js +1 -1
  211. package/dist/scripts/search-visibility-gate-lib.js +1 -1
  212. package/dist/scripts/seo-geo-route-identity-check.js +30 -4
  213. package/dist/scripts/sizecheck.js +7 -2
  214. package/dist/scripts/sks-1-11-gate-lib.js +53 -6
  215. package/dist/scripts/sks-menubar-install-check.js +7 -0
  216. package/dist/scripts/sks-uninstall-regression-check.js +1 -1
  217. package/dist/scripts/sksd-daemon-check.js +16 -6
  218. package/dist/scripts/update-default-command-check.js +4 -2
  219. package/dist/scripts/write-build-manifest.js +2 -1
  220. package/dist/scripts/zellij-layout-valid-check.js +31 -6
  221. package/dist/scripts/zellij-pane-proof-check.js +33 -2
  222. package/dist/scripts/zellij-real-session-cleanup-check.js +122 -2
  223. package/dist/scripts/zellij-real-session-launch-check.js +76 -5
  224. package/dist/scripts/zellij-slot-pane-renderer-check.js +4 -4
  225. package/package.json +27 -30
  226. package/schemas/codex/{app-server-0.142 → app-server-0.144}/codex_app_server_protocol.v2.schemas.json +1146 -781
  227. package/schemas/codex/codex-0139-real-probe-result.schema.json +39 -5
  228. package/schemas/codex/ppt-slide-issue-ledger.schema.json +2 -1
  229. package/dist/core/codex-control/codex-0139-multi-agent-real-probe.js +0 -107
  230. package/dist/scripts/agent-native-release-gate.js +0 -274
  231. package/dist/scripts/codex-0142-capability-check.js +0 -21
  232. package/dist/scripts/command-performance-scorecard-check.js +0 -204
  233. package/dist/scripts/config-managed-merge-callsite-coverage-check.js +0 -212
  234. package/dist/scripts/dollar-performance-scorecard-check.js +0 -257
  235. package/dist/scripts/gate-timing-check.js +0 -16
  236. package/dist/scripts/ops-maturity-scorecard-check.js +0 -12
  237. package/dist/scripts/performance-baseline-check.js +0 -12
  238. package/dist/scripts/performance-improvement-report-check.js +0 -58
  239. package/dist/scripts/release-parallel-check.js +0 -389
  240. package/dist/scripts/release-runner-efficiency-check.js +0 -15
  241. package/dist/scripts/retention-long-run-smoke-check.js +0 -299
  242. package/docs/assets/sneakoscope-architecture-pipeline.jpg +0 -0
@@ -64,6 +64,7 @@ export async function writeCodexConfigGuarded(input) {
64
64
  return result;
65
65
  }
66
66
  if (next === ensureTrailingNewline(before)) {
67
+ await writeTextAtomic(configPath, next, { mode: 0o600 });
67
68
  const result = { ok: true, status: 'present', config_path: configPath, backup_path: null, changed: false, repaired_keys: preserved.keys, forbidden_top_level: forbiddenTopLevel };
68
69
  if (preserved.keys.length || forbiddenTopLevel.length) {
69
70
  await recordCodexConfigGuard(root, input.reportPath, {
@@ -80,7 +81,7 @@ export async function writeCodexConfigGuarded(input) {
80
81
  }
81
82
  const backupPath = before.trim() ? await backupCodexConfig(configPath, before, input.backupTag || cause) : null;
82
83
  await ensureDir(path.dirname(configPath));
83
- await writeTextAtomic(configPath, next);
84
+ await writeTextAtomic(configPath, next, { mode: 0o600 });
84
85
  const afterSnapshot = await snapshotForConfig(root, configPath).catch(() => null);
85
86
  const diff = beforeSnapshot && afterSnapshot ? diffCodexAppUiSnapshots(beforeSnapshot, afterSnapshot) : null;
86
87
  const result = {
@@ -161,15 +162,36 @@ export function removeLegacyTopLevelCodexModeLocks(text = '') {
161
162
  return true;
162
163
  if (!/^\s*(?:model|model_reasoning_effort)\s*=/.test(line))
163
164
  return true;
164
- return !isSksProvenanceLine(line, lines[index - 1] || '');
165
+ return !hasSksModeLockProvenance(lines, index);
165
166
  }).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n'));
166
167
  }
167
- function isSksProvenanceLine(...lines) {
168
- return lines.some((line) => {
169
- const trimmed = String(line || '').trim();
170
- const comment = trimmed.startsWith('#') ? trimmed : trimmed.includes('#') ? trimmed.slice(trimmed.indexOf('#')) : '';
171
- return /(?:SKS|Sneakoscope|codex-lb|sks fast)/i.test(comment);
172
- });
168
+ function hasSksModeLockProvenance(lines, index) {
169
+ const current = String(lines[index] || '');
170
+ const inlineComment = current.includes('#') ? current.slice(current.indexOf('#')) : '';
171
+ if (isSksModeLockMarker(inlineComment))
172
+ return true;
173
+ const allowedManagedKeys = new Set([
174
+ 'service_tier', 'model', 'model_reasoning_effort', 'model_provider',
175
+ 'approval_policy', 'sandbox_mode', 'web_search', 'notify', 'preferred_auth_method'
176
+ ]);
177
+ const lowerBound = Math.max(0, index - 16);
178
+ for (let cursor = index - 1; cursor >= lowerBound; cursor -= 1) {
179
+ const candidate = String(lines[cursor] || '').trim();
180
+ if (!candidate)
181
+ continue;
182
+ if (candidate.startsWith('#')) {
183
+ if (isSksModeLockMarker(candidate))
184
+ return true;
185
+ continue;
186
+ }
187
+ const key = candidate.match(/^([A-Za-z0-9_-]+)\s*=/)?.[1] || '';
188
+ if (!allowedManagedKeys.has(key))
189
+ return false;
190
+ }
191
+ return false;
192
+ }
193
+ function isSksModeLockMarker(value = '') {
194
+ return /^#\s*(?:SKS|Sneakoscope)\b.*(?:moved machine-local Codex config|forced fast UI|managed (?:Codex )?(?:model|reasoning)|codex-lb (?:model|reasoning))/i.test(String(value || '').trim());
173
195
  }
174
196
  function mergeLostFastUiKeys(before, nextInput) {
175
197
  let next = String(nextInput || '');
@@ -301,7 +323,7 @@ async function snapshotForConfig(root, configPath) {
301
323
  async function backupCodexConfig(configPath, text, tag) {
302
324
  try {
303
325
  const backupPath = `${configPath}.sks-${tag}-${Date.now().toString(36)}.bak`;
304
- await writeTextAtomic(backupPath, text);
326
+ await writeTextAtomic(backupPath, text, { mode: 0o600 });
305
327
  await cleanupCodexConfigBackups(configPath, { keepPerTag: 3, maxAgeMs: 30 * 24 * 60 * 60 * 1000 }).catch(() => undefined);
306
328
  return backupPath;
307
329
  }
@@ -4,8 +4,12 @@ import { ensureDir, nowIso, readText, sha256, writeJsonAtomic, writeTextAtomic }
4
4
  import { codexHome, scanProjectLocalForbiddenKeys, snapshotCodexAppUiState } from './codex-app-ui-state-snapshot.js';
5
5
  import { assertCodexAppUiMutationAllowed } from './codex-app-ui-clobber-guard.js';
6
6
  import { codexProviderModelUiStatus } from '../codex-app.js';
7
+ import { cleanupCodexConfigBackups } from '../codex/codex-config-toml.js';
7
8
  export const CODEX_APP_FAST_UI_REPAIR_SCHEMA = 'sks.codex-app-fast-ui-repair.v1';
8
- const FAST_UI_TOP_LEVEL_RE = /^\s*(?:model|model_reasoning_effort|service_tier)\s*=/;
9
+ // `service_tier = "fast"` is the current Codex Desktop default-selection
10
+ // contract. Removing it would silently turn Fast mode off, not restore the
11
+ // selector. Only legacy SKS model/effort pins lock the native picker.
12
+ const FAST_UI_TOP_LEVEL_RE = /^\s*(?:model|model_reasoning_effort)\s*=/;
9
13
  const FAST_UI_FEATURE_LINE_RE = /^\s*fast_mode\s*=/;
10
14
  const FAST_UI_LEGACY_TABLES = new Set(['user.fast_mode', 'profiles.sks-fast-high']);
11
15
  const SKS_CAUSED_RE = /(?:SKS|Sneakoscope|codex-lb|sks-mad|sks fast)/i;
@@ -20,6 +24,7 @@ export async function repairCodexAppFastUi(root = process.cwd(), input = {}) {
20
24
  const actions = [];
21
25
  const detectedProjectLocalForbiddenKeys = [];
22
26
  const unsafeReasons = [];
27
+ let permissionsHardened = 0;
23
28
  let detectedSksCausedMutation = false;
24
29
  for (const candidate of candidates) {
25
30
  const text = await readText(candidate.file, null);
@@ -27,34 +32,39 @@ export async function repairCodexAppFastUi(root = process.cwd(), input = {}) {
27
32
  actions.push({ scope: candidate.scope, file: displayPath(candidate.file), status: 'missing', changed: false });
28
33
  continue;
29
34
  }
35
+ if (input.apply === true)
36
+ permissionsHardened += await hardenCodexConfigPermissions(candidate.file);
30
37
  const repaired = candidate.mode === 'project_forbidden_keys'
31
38
  ? stripProjectLocalForbiddenKeys(text)
32
39
  : stripSksCausedHostOwnedLines(text);
40
+ const candidateUnsafeReasons = detectUnsafeFastUiRepair(text);
41
+ unsafeReasons.push(...candidateUnsafeReasons);
33
42
  if (candidate.mode === 'project_forbidden_keys')
34
43
  detectedProjectLocalForbiddenKeys.push(...repaired.removedKeys);
35
44
  if (candidate.mode === 'sks_caused_host_owned_keys') {
36
- const unsafe = detectUnsafeFastUiRepair(text);
37
- unsafeReasons.push(...unsafe);
38
45
  if (repaired.text !== text)
39
46
  detectedSksCausedMutation = true;
40
47
  }
41
48
  if (repaired.text === text) {
42
- actions.push({ scope: candidate.scope, file: displayPath(candidate.file), status: unsafeReasons.length && candidate.mode === 'sks_caused_host_owned_keys' ? 'requires_confirmation' : 'ok', changed: false, removed_keys: repaired.removedKeys });
49
+ actions.push({ scope: candidate.scope, file: displayPath(candidate.file), status: candidateUnsafeReasons.length ? 'requires_confirmation' : 'ok', changed: false, removed_keys: repaired.removedKeys });
43
50
  continue;
44
51
  }
45
52
  const backupPath = `${candidate.file}.codex-app-ui-repair-${Date.now().toString(36)}.bak`;
46
- if (input.apply) {
53
+ const applyAllowed = input.apply === true && (candidateUnsafeReasons.length === 0 || input.force === true);
54
+ if (applyAllowed) {
47
55
  await ensureDir(path.dirname(candidate.file));
48
- await fs.writeFile(backupPath, text, 'utf8');
56
+ await fs.writeFile(backupPath, text, { encoding: 'utf8', mode: 0o600 });
57
+ await fs.chmod(backupPath, 0o600);
49
58
  assertCodexAppUiMutationAllowed({ kind: 'codex_app_ui_state', scope: 'codex-app-ui-repair', backupPath });
50
- await writeTextAtomic(candidate.file, repaired.text);
59
+ await writeTextAtomic(candidate.file, repaired.text, { mode: 0o600 });
60
+ await cleanupCodexConfigBackups(candidate.file, { keepPerTag: 3, maxAgeMs: 30 * 24 * 60 * 60 * 1000 }).catch(() => undefined);
51
61
  }
52
62
  actions.push({
53
63
  scope: candidate.scope,
54
64
  file: displayPath(candidate.file),
55
- status: input.apply ? 'repaired' : 'planned',
65
+ status: applyAllowed ? 'repaired' : input.apply ? 'requires_confirmation' : 'planned',
56
66
  changed: true,
57
- backup_path: input.apply ? displayPath(backupPath) : null,
67
+ backup_path: applyAllowed ? displayPath(backupPath) : null,
58
68
  before_hash: sha256(text),
59
69
  after_hash: sha256(repaired.text),
60
70
  removed_keys: repaired.removedKeys
@@ -63,17 +73,24 @@ export async function repairCodexAppFastUi(root = process.cwd(), input = {}) {
63
73
  const after = await snapshotCodexAppUiState(resolvedRoot, { codexHome: home });
64
74
  const providerModelUi = await codexProviderModelUiStatus({
65
75
  cwd: resolvedRoot,
76
+ env: input.env,
66
77
  home: path.dirname(home),
67
78
  configPath: path.join(home, 'config.toml'),
68
79
  codexLbEnvPath: path.join(home, 'sks-codex-lb.env')
69
80
  });
70
81
  const changed = actions.some((action) => action.changed);
82
+ const applied = actions.some((action) => action.status === 'repaired');
83
+ const pending = actions.some((action) => action.changed && action.status !== 'repaired');
71
84
  const requiresConfirmation = unsafeReasons.length > 0 && input.force !== true;
85
+ const selectedProviderBlockers = Array.isArray(providerModelUi.selected_provider_blockers)
86
+ ? providerModelUi.selected_provider_blockers
87
+ : [];
72
88
  const safeAutoApply = changed && !requiresConfirmation;
73
89
  const manual = changed && !input.apply;
74
90
  const blockers = [
75
91
  ...(requiresConfirmation ? ['codex_app_fast_ui_repair_requires_confirmation'] : []),
76
92
  ...(manual && !safeAutoApply ? ['codex_app_fast_ui_repair_requires_explicit_apply'] : []),
93
+ ...selectedProviderBlockers.map((blocker) => `selected_provider:${blocker}`),
77
94
  ...(after.indicators.secret_leak_suspected ? ['codex_app_ui_repair_secret_leak_suspected'] : [])
78
95
  ];
79
96
  const report = {
@@ -86,12 +103,15 @@ export async function repairCodexAppFastUi(root = process.cwd(), input = {}) {
86
103
  detected_sks_caused_mutation: detectedSksCausedMutation,
87
104
  detected_project_local_forbidden_keys: [...new Set(detectedProjectLocalForbiddenKeys)],
88
105
  unsafe_repair_reasons: [...new Set(unsafeReasons)],
89
- fast_selector: changed ? (input.apply ? 'repaired' : 'manual_action_required') : before.indicators.fast_selector === 'maybe_hidden_or_locked' ? 'manual_action_required' : 'ok',
90
- provider_selector: providerModelUi.ok ? 'ok' : 'manual_action_required',
106
+ permissions_hardened: permissionsHardened,
107
+ fast_selector: pending ? 'manual_action_required' : applied ? 'repaired' : before.indicators.fast_selector === 'maybe_hidden_or_locked' ? 'manual_action_required' : 'ok',
108
+ provider_selector: selectedProviderBlockers.length ? 'manual_action_required' : 'selected_provider_ready',
91
109
  provider_model_ui: providerModelUi,
92
110
  provider_actions: providerModelUi.ui_actions || [],
93
111
  provider_blockers: providerModelUi.blockers || [],
94
- host_owned_config: input.apply && changed ? 'repaired_with_backup' : changed ? 'preserved_until_explicit_apply' : 'preserved',
112
+ selected_provider_blockers: selectedProviderBlockers,
113
+ optional_provider_blockers: providerModelUi.optional_provider_blockers || [],
114
+ host_owned_config: applied && !pending ? 'repaired_with_backup' : changed ? 'preserved_until_explicit_apply' : 'preserved',
95
115
  actions,
96
116
  before_fast_selector: before.indicators.fast_selector,
97
117
  after_fast_selector: after.indicators.fast_selector,
@@ -102,16 +122,27 @@ export async function repairCodexAppFastUi(root = process.cwd(), input = {}) {
102
122
  await writeJsonAtomic(input.reportPath, report);
103
123
  return report;
104
124
  }
125
+ async function hardenCodexConfigPermissions(configFile) {
126
+ const dir = path.dirname(configFile);
127
+ const base = path.basename(configFile);
128
+ const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
129
+ const targets = [configFile, ...entries
130
+ .filter((entry) => entry.isFile() && entry.name.startsWith(`${base}.`) && entry.name.endsWith('.bak'))
131
+ .map((entry) => path.join(dir, entry.name))];
132
+ let hardened = 0;
133
+ for (const target of targets) {
134
+ const stat = await fs.lstat(target).catch(() => null);
135
+ if (!stat?.isFile() || stat.isSymbolicLink())
136
+ continue;
137
+ if ((stat.mode & 0o777) !== 0o600) {
138
+ await fs.chmod(target, 0o600);
139
+ hardened += 1;
140
+ }
141
+ }
142
+ return hardened;
143
+ }
105
144
  function detectUnsafeFastUiRepair(text) {
106
145
  const reasons = [];
107
- const lines = text.split(/\r?\n/);
108
- for (let i = 0; i < lines.length; i += 1) {
109
- const line = lines[i] || '';
110
- const serviceTier = line.match(/^\s*service_tier\s*=\s*"(standard|flex)"\s*(?:#.*)?$/i)?.[1];
111
- const sksMarked = SKS_CAUSED_RE.test(line) || SKS_CAUSED_RE.test(lines[i - 1] || '') || SKS_CAUSED_RE.test(lines[i + 1] || '');
112
- if (serviceTier && !sksMarked)
113
- reasons.push(`user_selected_service_tier_${serviceTier.toLowerCase()}`);
114
- }
115
146
  if (hasOddUnescapedQuotes(text))
116
147
  reasons.push('unparseable_config_requires_manual_review');
117
148
  return [...new Set(reasons)];
@@ -1,15 +1,16 @@
1
1
  import { exists, runProcess, which } from '../fsx.js';
2
2
  export async function restartCodexApp(opts = {}) {
3
3
  const env = opts.env || process.env;
4
- const appName = String(opts.appName || env.SKS_CODEX_APP_NAME || 'Codex');
4
+ const appName = String(opts.appName || env.SKS_CODEX_APP_NAME || 'ChatGPT');
5
+ const bundleId = String(opts.bundleId || env.SKS_CODEX_APP_BUNDLE_ID || 'com.openai.codex');
5
6
  if (opts.enabled === false || env.SKS_SKIP_CODEX_APP_RESTART === '1') {
6
7
  return skipped(appName, 'disabled');
7
8
  }
8
- if (process.platform !== 'darwin')
9
+ if ((opts.platform || process.platform) !== 'darwin')
9
10
  return skipped(appName, 'not_macos');
10
11
  const run = opts.runProcessImpl || runProcess;
11
- const osascript = await which('osascript').catch(() => null) || await exists('/usr/bin/osascript').then((ok) => ok ? '/usr/bin/osascript' : null).catch(() => null);
12
- const open = await which('open').catch(() => null) || await exists('/usr/bin/open').then((ok) => ok ? '/usr/bin/open' : null).catch(() => null);
12
+ const osascript = opts.osascriptPath || await which('osascript').catch(() => null) || await exists('/usr/bin/osascript').then((ok) => ok ? '/usr/bin/osascript' : null).catch(() => null);
13
+ const open = opts.openPath || await which('open').catch(() => null) || await exists('/usr/bin/open').then((ok) => ok ? '/usr/bin/open' : null).catch(() => null);
13
14
  if (!osascript || !open) {
14
15
  return {
15
16
  schema: 'sks.codex-app-restart.v1',
@@ -22,25 +23,54 @@ export async function restartCodexApp(opts = {}) {
22
23
  ]
23
24
  };
24
25
  }
25
- const quit = await run(osascript, ['-e', `tell application ${JSON.stringify(appName)} to quit`], { timeoutMs: 5000, maxOutputBytes: 8192 }).catch((err) => ({
26
+ const appTarget = bundleId ? `application id ${JSON.stringify(bundleId)}` : `application ${JSON.stringify(appName)}`;
27
+ const quit = await run(osascript, ['-e', `tell ${appTarget} to quit`], { timeoutMs: 5000, maxOutputBytes: 8192 }).catch((err) => ({
26
28
  code: 1,
27
29
  stdout: '',
28
30
  stderr: err?.message || String(err)
29
31
  }));
30
- await sleep(Number(opts.delayMs ?? env.SKS_CODEX_APP_RESTART_DELAY_MS ?? 1200));
31
- const launched = await run(open, ['-a', appName], { timeoutMs: 10000, maxOutputBytes: 8192 }).catch((err) => ({
32
+ const quitOk = quit.code === 0 || /not running|Can't get application|application isn't running/i.test(String(quit.stderr || quit.stdout || ''));
33
+ const exitTimeoutMs = Math.max(250, Number(opts.exitTimeoutMs ?? env.SKS_CODEX_APP_EXIT_TIMEOUT_MS ?? 5000));
34
+ const pollMs = Math.max(50, Number(opts.pollMs ?? 100));
35
+ let exited = !quitOk;
36
+ let attempts = 0;
37
+ const deadline = Date.now() + exitTimeoutMs;
38
+ while (quitOk && Date.now() < deadline) {
39
+ attempts += 1;
40
+ const probe = await run(osascript, ['-e', `${appTarget} is running`], { timeoutMs: 1000, maxOutputBytes: 1024 }).catch(() => null);
41
+ if (probe?.code === 0 && String(probe.stdout || '').trim().toLowerCase() === 'false') {
42
+ exited = true;
43
+ break;
44
+ }
45
+ await sleep(pollMs);
46
+ }
47
+ if (quitOk && !exited) {
48
+ return {
49
+ schema: 'sks.codex-app-restart.v1',
50
+ ok: false,
51
+ status: 'blocked',
52
+ app_name: appName,
53
+ bundle_id: bundleId,
54
+ quit: { ok: true, code: quit.code, error: null },
55
+ exit_wait: { ok: false, attempts, error: 'codex_app_exit_timeout' },
56
+ blockers: ['codex_app_exit_timeout']
57
+ };
58
+ }
59
+ await sleep(Number(opts.delayMs ?? env.SKS_CODEX_APP_RESTART_DELAY_MS ?? 150));
60
+ const launched = await run(open, bundleId ? ['-b', bundleId] : ['-a', appName], { timeoutMs: 10000, maxOutputBytes: 8192 }).catch((err) => ({
32
61
  code: 1,
33
62
  stdout: '',
34
63
  stderr: err?.message || String(err)
35
64
  }));
36
- const quitOk = quit.code === 0 || /not running|Can't get application|application isn't running/i.test(String(quit.stderr || quit.stdout || ''));
37
65
  const openOk = launched.code === 0;
38
66
  return {
39
67
  schema: 'sks.codex-app-restart.v1',
40
68
  ok: quitOk && openOk,
41
69
  status: quitOk && openOk ? 'restarted' : 'blocked',
42
70
  app_name: appName,
71
+ bundle_id: bundleId,
43
72
  quit: { ok: quitOk, code: quit.code, error: quitOk ? null : String(quit.stderr || quit.stdout || '').trim() },
73
+ exit_wait: { ok: exited, attempts, error: exited ? null : 'codex_app_exit_timeout' },
44
74
  open: { ok: openOk, code: launched.code, error: openOk ? null : String(launched.stderr || launched.stdout || '').trim() },
45
75
  blockers: [
46
76
  ...(quitOk ? [] : ['codex_app_quit_failed']),
@@ -12,7 +12,7 @@ export const PROJECT_LOCAL_FORBIDDEN_CODEX_KEYS = [
12
12
  'otel'
13
13
  ];
14
14
  const HOST_OWNED_KEY_RE = /^(?:model|model_reasoning_effort|openai_base_url|chatgpt_base_url|apps_mcp_product_sku|model_provider|model_providers(?:\.|$)|notify|profile|profiles(?:\.|$)|experimental_realtime_ws_base_url|otel(?:\.|$)|features\.fast_mode|service_tier|user\.fast_mode(?:\.|$))/;
15
- const SECRET_KEY_RE = /(?:key|token|secret|password|credential|cookie|authorization|bearer|refresh|access)/i;
15
+ const SECRET_KEY_RE = /(?:key|token|secret|password|credential|cookie|authorization|auth|bearer|refresh|access|headers?|env)/i;
16
16
  export function codexHome(input) {
17
17
  return path.resolve(String(input?.codexHome || process.env.CODEX_HOME || path.join(os.homedir(), '.codex')));
18
18
  }
@@ -41,8 +41,6 @@ export async function snapshotCodexAppUiState(root = process.cwd(), input = {})
41
41
  return true;
42
42
  if ((signal.key_path === 'model' || signal.key_path === 'model_reasoning_effort') && signal.sks_related)
43
43
  return true;
44
- if (signal.key_path === 'service_tier' && signal.sks_related)
45
- return true;
46
44
  return false;
47
45
  });
48
46
  return {
@@ -98,6 +96,9 @@ export function scanTomlSignals(text) {
98
96
  const tables = [];
99
97
  let table = null;
100
98
  const lines = text.split(/\r?\n/);
99
+ const firstTableIndex = lines.findIndex((line) => /^\s*\[/.test(line));
100
+ const topLevelLines = firstTableIndex === -1 ? lines : lines.slice(0, firstTableIndex);
101
+ const sksManagedTopLevel = topLevelLines.some((line) => /(?:SKS|Sneakoscope|codex-lb|sks fast)/i.test(line));
101
102
  lines.forEach((lineText, index) => {
102
103
  const tableMatch = lineText.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
103
104
  if (tableMatch?.[1]) {
@@ -124,6 +125,7 @@ export function scanTomlSignals(text) {
124
125
  fast_ui_related: /(?:fast_mode|service_tier|model_reasoning_effort|^model$)/i.test(keyPath) || /(?:fast|priority|default)/i.test(value),
125
126
  provider_related: /(?:provider|base_url|auth|profile|openai|chatgpt|codex-lb)/i.test(lowerPath),
126
127
  sks_related: /(?:sks|sneakoscope|codex-lb)/i.test(lineText)
128
+ || (table == null && sksManagedTopLevel && (key === 'model' || key === 'model_reasoning_effort'))
127
129
  });
128
130
  });
129
131
  return { signals, tables: [...new Set(tables)] };
@@ -252,7 +254,13 @@ function hostOwnedFingerprint(snapshot) {
252
254
  function redactValuePreview(keyPath, value) {
253
255
  if (SECRET_KEY_RE.test(keyPath))
254
256
  return '<redacted>';
257
+ if (/^\s*\{/.test(value))
258
+ return '<redacted-object>';
259
+ if (/^\s*\[/.test(value))
260
+ return '<redacted-array>';
255
261
  const normalized = value.replace(/^['"]|['"]$/g, '');
262
+ if (/^[a-z][a-z0-9+.-]*:\/\/[^/@\s]+@/i.test(normalized))
263
+ return '<redacted-url-credentials>';
256
264
  if (SECRET_KEY_RE.test(normalized))
257
265
  return '<redacted>';
258
266
  return normalized.length > 80 ? `${normalized.slice(0, 77)}...` : normalized;
@@ -444,7 +444,7 @@ async function resolveCodexBundleId(input) {
444
444
  }
445
445
  export async function smokeSksMenuBarAction(actionScriptPath) {
446
446
  if (!(await exists(actionScriptPath)))
447
- return { ok: false, code: null, output: null, versionDetected: false, executable: false };
447
+ return { ok: false, code: null, output: null, versionDetected: false, detectedVersion: null, executable: false };
448
448
  // The Swift app runs the script directly (which requires the executable bit), so the smoke
449
449
  // check must invoke it the same way. Running it via `/bin/zsh <script>` — as this check used
450
450
  // to — succeeds even when +x is missing, which let doctor/status report a healthy action
@@ -456,6 +456,7 @@ export async function smokeSksMenuBarAction(actionScriptPath) {
456
456
  code: null,
457
457
  output: 'action script is not executable (missing +x); the menu bar app cannot run it',
458
458
  versionDetected: false,
459
+ detectedVersion: null,
459
460
  executable: false
460
461
  };
461
462
  }
@@ -464,12 +465,14 @@ export async function smokeSksMenuBarAction(actionScriptPath) {
464
465
  maxOutputBytes: 16 * 1024
465
466
  }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
466
467
  const output = String(`${result.stdout || ''}\n${result.stderr || ''}`).trim();
467
- const versionDetected = /\b(?:sks|sneakoscope)?\s*v?\d+\.\d+\.\d+\b/i.test(output);
468
+ const detectedVersion = output.match(/\b(?:sks|sneakoscope)?\s*v?(\d+\.\d+\.\d+)\b/i)?.[1] || null;
469
+ const versionDetected = Boolean(detectedVersion);
468
470
  return {
469
471
  ok: result.code === 0 && versionDetected,
470
472
  code: result.code,
471
473
  output: output ? output.slice(0, 700) : null,
472
474
  versionDetected,
475
+ detectedVersion,
473
476
  executable: true
474
477
  };
475
478
  }
@@ -505,6 +508,10 @@ export async function inspectSksMenuBarStatus(opts = {}) {
505
508
  warning: config.codex_bundle_id ? null : 'codex_sync_disabled'
506
509
  };
507
510
  const buildStamp = await readJson(paths.build_stamp_path, null);
511
+ const actionIntegrity = evaluateActionScriptIntegrity(actionText, buildStamp);
512
+ const expectedActionVersion = buildStamp?.package_version || PACKAGE_VERSION;
513
+ const actionVersionMatchesExpected = actionSmoke.detectedVersion === expectedActionVersion
514
+ && actionSmoke.detectedVersion === PACKAGE_VERSION;
508
515
  const launchd = await inspectLaunchdService(opts.env);
509
516
  const signature = await inspectSignature(paths.app_path, opts.env);
510
517
  const blockers = [];
@@ -517,6 +524,12 @@ export async function inspectSksMenuBarStatus(opts = {}) {
517
524
  blockers.push('action_script_not_executable');
518
525
  if (installed && !actionSmoke.ok)
519
526
  blockers.push('action_script_smoke_failed');
527
+ if (installed && actionSmoke.ok && !actionVersionMatchesExpected)
528
+ blockers.push('action_target_version_mismatch');
529
+ if (installed && !buildStamp)
530
+ blockers.push('build_stamp_missing');
531
+ if (installed && buildStamp && !actionIntegrity.script_hash_matches_stamp)
532
+ blockers.push('action_script_hash_mismatch');
520
533
  if (installed && signature.checked && !signature.ok)
521
534
  warnings.push('codesign_identifier_unexpected');
522
535
  if (!codexSync.ok)
@@ -539,8 +552,12 @@ export async function inspectSksMenuBarStatus(opts = {}) {
539
552
  smoke_code: actionSmoke.code,
540
553
  smoke_output: actionSmoke.output,
541
554
  version_detected: actionSmoke.versionDetected,
555
+ detected_version: actionSmoke.detectedVersion,
556
+ expected_version: expectedActionVersion,
557
+ version_matches_expected: actionVersionMatchesExpected,
558
+ ...actionIntegrity,
542
559
  executable: actionSmoke.executable,
543
- ok: actionSmoke.ok
560
+ ok: actionSmoke.ok && actionVersionMatchesExpected && actionIntegrity.script_hash_matches_stamp
544
561
  },
545
562
  codex_sync: codexSync,
546
563
  build_stamp: buildStamp,
@@ -551,6 +568,15 @@ export async function inspectSksMenuBarStatus(opts = {}) {
551
568
  next_actions: blockers.length || warnings.length ? defaultNextActions() : ['sks menubar status --json']
552
569
  };
553
570
  }
571
+ export function evaluateActionScriptIntegrity(actionText, buildStamp) {
572
+ const scriptSha256 = actionText ? sha256(actionText) : null;
573
+ const expectedScriptSha256 = buildStamp?.action_script_sha256 || null;
574
+ return {
575
+ script_sha256: scriptSha256,
576
+ expected_script_sha256: expectedScriptSha256,
577
+ script_hash_matches_stamp: Boolean(scriptSha256 && expectedScriptSha256 && scriptSha256 === expectedScriptSha256)
578
+ };
579
+ }
554
580
  export async function restartSksMenuBar(opts = {}) {
555
581
  const env = opts.env || process.env;
556
582
  const paths = sksMenuBarPaths(opts.home || env.HOME, opts.root);
@@ -757,6 +783,14 @@ run_node_entry() {
757
783
  exec "$node_bin" "$entry" "$@"
758
784
  }
759
785
 
786
+ # The installer pins the entry that produced this menu build. Prefer it over
787
+ # PATH/global copies so a newly updated 6.0.3 companion cannot execute stale
788
+ # 6.0.2 auth/restart logic. Fall back only when the pinned file or Node runtime
789
+ # is unavailable.
790
+ if [ -f "$SKS_ENTRY" ]; then
791
+ run_node_entry "$SKS_ENTRY" "$@" || true
792
+ fi
793
+
760
794
  SKS_BIN="$(/bin/zsh -lc 'command -v sks' 2>/dev/null | /usr/bin/head -n 1 || true)"
761
795
  if [ -n "$SKS_BIN" ] && [ -x "$SKS_BIN" ]; then
762
796
  exec "$SKS_BIN" "$@"
@@ -772,7 +806,6 @@ for entry in "$HOME"/.nvm/versions/node/*/lib/node_modules/sneakoscope/dist/bin/
772
806
  run_node_entry "$entry" "$@" || true
773
807
  fi
774
808
  done
775
- run_node_entry "$SKS_ENTRY" "$@" || true
776
809
  notify_sks_missing
777
810
  echo "SKS command not found. Run npm install -g sneakoscope or sks doctor --fix, then try again." >&2
778
811
  exit 127
@@ -1176,9 +1209,10 @@ ${codexLifecycleSource}
1176
1209
  let cachePath = NSString(string: "~/.sneakoscope/cache/update-notice.json").expandingTildeInPath
1177
1210
  guard let data = FileManager.default.contents(atPath: cachePath) else { return false }
1178
1211
  guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return false }
1179
- if let available = json["update_available"] as? Bool { return available }
1180
- guard let latest = json["latest_version"] as? String else { return false }
1181
- return latest != packageVersion && !latest.isEmpty
1212
+ if let latest = json["latest_version"] as? String, !latest.isEmpty {
1213
+ return latest.compare(packageVersion, options: .numeric) == .orderedDescending
1214
+ }
1215
+ return json["update_available"] as? Bool == true
1182
1216
  }
1183
1217
 
1184
1218
  func runSksCapture(_ args: [String], title: String, stdinText: String? = nil, notify: Bool = true, completion: ((Int32, String) -> Void)? = nil) {
@@ -1238,12 +1272,18 @@ ${codexLifecycleSource}
1238
1272
  runSksSilent(["codex-lb", "status", "--json"]) { [weak self] code, output in
1239
1273
  guard let self = self else { return }
1240
1274
  let json = parseJsonObject(output)
1241
- let selected = json?["selected"] as? Bool == true
1242
- let provider = (json?["model_provider"] as? String)?.lowercased() == "codex-lb"
1243
- let mode = (json?["mode"] as? String)?.lowercased() == "codex-lb"
1244
- let codexLbActive = code == 0 && (selected || provider || mode)
1275
+ guard code == 0, let json = json else {
1276
+ self.codexLbItem.state = .off
1277
+ self.oauthItem.state = .off
1278
+ return
1279
+ }
1280
+ let selected = json["selected"] as? Bool == true
1281
+ let providerContract = json["provider_contract_ok"] as? Bool == true
1282
+ let authMode = (json["auth_mode"] as? String)?.lowercased() ?? ""
1283
+ let codexLbActive = selected && providerContract && authMode == "apikey"
1284
+ let oauthActive = !selected && (authMode.contains("chatgpt") || authMode.contains("oauth") || authMode.contains("browser"))
1245
1285
  self.codexLbItem.state = codexLbActive ? .on : .off
1246
- self.oauthItem.state = codexLbActive ? .off : .on
1286
+ self.oauthItem.state = oauthActive ? .on : .off
1247
1287
  }
1248
1288
  }
1249
1289
 
@@ -1251,22 +1291,32 @@ ${codexLifecycleSource}
1251
1291
  runSksSilent(["fast-mode", "status", "--json"]) { [weak self] code, output in
1252
1292
  guard let self = self else { return }
1253
1293
  let json = parseJsonObject(output)
1254
- let fastMode = json?["fast_mode"] as? Bool == true
1255
- let serviceTier = (json?["service_tier"] as? String)?.lowercased()
1256
- let fastActive = code == 0 && (fastMode || serviceTier == "fast" || serviceTier == "priority")
1257
- self.fastModeOnItem.state = fastActive ? .on : .off
1258
- self.fastModeOffItem.state = fastActive ? .off : .on
1294
+ guard code == 0, let json = json else {
1295
+ self.fastModeOnItem.state = .off
1296
+ self.fastModeOffItem.state = .off
1297
+ return
1298
+ }
1299
+ guard let global = json["global"] as? [String: Any], let desktopFast = global["on"] as? Bool else {
1300
+ self.fastModeOnItem.state = .off
1301
+ self.fastModeOffItem.state = .off
1302
+ return
1303
+ }
1304
+ let fastMode = json["fast_mode"] as? Bool == true
1305
+ let serviceTier = (json["service_tier"] as? String)?.lowercased()
1306
+ let projectFast = fastMode || serviceTier == "fast" || serviceTier == "priority"
1307
+ self.fastModeOnItem.state = projectFast && desktopFast ? .on : .off
1308
+ self.fastModeOffItem.state = !projectFast && !desktopFast ? .on : .off
1259
1309
  }
1260
1310
  }
1261
1311
 
1262
1312
  @objc func useCodexLb() {
1263
- runSksBackground(["codex-lb", "use-codex-lb", "--json"], title: "Use codex-lb") { [weak self] code, _ in
1313
+ runSksBackground(["codex-lb", "use-codex-lb", "--restart-app", "--json"], title: "Use codex-lb") { [weak self] code, _ in
1264
1314
  if code == 0 { self?.updateAuthModeChecks() }
1265
1315
  }
1266
1316
  }
1267
1317
 
1268
1318
  @objc func useChatGptOAuth() {
1269
- runSksBackground(["codex-lb", "use-oauth", "--json"], title: "Use ChatGPT OAuth") { [weak self] code, _ in
1319
+ runSksBackground(["codex-lb", "use-oauth", "--restart-app", "--json"], title: "Use ChatGPT OAuth") { [weak self] code, _ in
1270
1320
  if code == 0 { self?.updateAuthModeChecks() }
1271
1321
  }
1272
1322
  }
@@ -1274,14 +1324,14 @@ ${codexLifecycleSource}
1274
1324
  @objc func setCodexLbDomainAndKey() {
1275
1325
  guard let domain = promptText(title: "Set codex-lb Domain", message: "Enter just your codex-lb domain or base URL — the /backend-api/codex path is added automatically.", placeholder: "lb.example.com") else { return }
1276
1326
  guard let key = promptText(title: "Set codex-lb Key", message: "Enter your codex-lb API key.", placeholder: "sk-clb-...", secure: true) else { return }
1277
- runSksBackground(["codex-lb", "setup", "--host", domain, "--api-key-stdin", "--yes", "--json"], title: "Set codex-lb", stdinText: key + "\\n") { [weak self] code, _ in
1327
+ runSksBackground(["codex-lb", "setup", "--host", domain, "--api-key-stdin", "--yes", "--restart-app", "--json"], title: "Set codex-lb", stdinText: key + "\\n") { [weak self] code, _ in
1278
1328
  if code == 0 { self?.updateAuthModeChecks() }
1279
1329
  }
1280
1330
  }
1281
1331
 
1282
1332
  @objc func setOpenRouterKey() {
1283
1333
  guard let key = promptText(title: "Set OpenRouter Key", message: "Enter your OpenRouter API key.", placeholder: "sk-or-v1-...", secure: true) else { return }
1284
- runSksBackground(["codex-app", "set-openrouter-key", "--api-key-stdin", "--json"], title: "Set OpenRouter Key", stdinText: key + "\\n")
1334
+ runSksBackground(["codex-app", "set-openrouter-key", "--api-key-stdin", "--restart-app", "--json"], title: "Set OpenRouter Key", stdinText: key + "\\n")
1285
1335
  }
1286
1336
 
1287
1337
  @objc func fastModeOn() {
@@ -1312,36 +1362,46 @@ ${codexLifecycleSource}
1312
1362
  let urlString = "http://127.0.0.1:4477"
1313
1363
  runProcess("/usr/bin/curl", ["-fsS", "--max-time", "1", urlString]) { [weak self] code, _ in
1314
1364
  DispatchQueue.main.async {
1315
- if code != 0 {
1365
+ if code == 0 {
1366
+ if let url = URL(string: urlString) { NSWorkspace.shared.open(url) }
1367
+ } else {
1316
1368
  self?.startSksDetached(["ui"], title: "SKS Dashboard")
1369
+ self?.waitForDashboard(urlString, attempts: 20)
1317
1370
  }
1318
- if let url = URL(string: urlString) {
1319
- NSWorkspace.shared.open(url)
1371
+ }
1372
+ }
1373
+ }
1374
+
1375
+ func waitForDashboard(_ urlString: String, attempts: Int) {
1376
+ runProcess("/usr/bin/curl", ["-fsS", "--max-time", "1", urlString]) { [weak self] code, _ in
1377
+ if code == 0 {
1378
+ DispatchQueue.main.async {
1379
+ if let url = URL(string: urlString) { NSWorkspace.shared.open(url) }
1380
+ }
1381
+ } else if attempts > 1 {
1382
+ DispatchQueue.global().asyncAfter(deadline: .now() + 0.25) {
1383
+ self?.waitForDashboard(urlString, attempts: attempts - 1)
1384
+ }
1385
+ } else {
1386
+ DispatchQueue.main.async {
1387
+ showAlert("SKS Dashboard failed", informative: "Dashboard did not become ready at \(urlString). See View Last Log.")
1320
1388
  }
1321
1389
  }
1322
1390
  }
1323
1391
  }
1324
1392
 
1325
1393
  @objc func openCodexSettings() {
1326
- runProcess("/usr/bin/open", ["codex://settings"])
1394
+ runProcess("/usr/bin/open", ["codex://settings"]) { code, output in
1395
+ if code != 0 {
1396
+ DispatchQueue.main.async {
1397
+ showAlert("Open Codex Settings failed", informative: humanizeSksFailure(redactSecrets(output)))
1398
+ }
1399
+ }
1400
+ }
1327
1401
  }
1328
1402
 
1329
1403
  @objc func restartCodex() {
1330
- let running = NSWorkspace.shared.runningApplications.filter { app in
1331
- if let bundle = codexBundleId, app.bundleIdentifier == bundle { return true }
1332
- return app.localizedName == "Codex"
1333
- }
1334
- for app in running {
1335
- app.terminate()
1336
- }
1337
- DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
1338
- if let bundle = codexBundleId, let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundle) {
1339
- NSWorkspace.shared.open(url)
1340
- showNotification("Restart Codex", "requested")
1341
- } else {
1342
- showAlert("Restart Codex failed", informative: "Codex app bundle could not be resolved.")
1343
- }
1344
- }
1404
+ runSksBackground(["codex-app", "restart", "--json"], title: "Restart Codex")
1345
1405
  }
1346
1406
 
1347
1407
  @objc func viewLastLog() {