sneakoscope 8.3.0 → 8.3.2

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 (38) hide show
  1. package/README.md +5 -3
  2. package/config/skills-hash-ledger.v1.json +3 -0
  3. package/crates/sks-core/Cargo.lock +1 -1
  4. package/crates/sks-core/Cargo.toml +1 -1
  5. package/dist/config/skills-manifest.json +7 -4
  6. package/dist/core/codex-app/menubar/config.js +4 -4
  7. package/dist/core/codex-app/menubar/constants.js +2 -1
  8. package/dist/core/codex-app/menubar/global-install.js +16 -0
  9. package/dist/core/codex-app/menubar/installer.js +15 -2
  10. package/dist/core/codex-app/menubar/launch-agent.js +23 -9
  11. package/dist/core/codex-app/menubar/rollback.js +12 -2
  12. package/dist/core/codex-lb/combined-catalog/normalize.js +37 -3
  13. package/dist/core/codex-lb/combined-catalog/shared.js +7 -1
  14. package/dist/core/codex-lb/desktop-bridge/header-policy.js +5 -3
  15. package/dist/core/codex-lb/desktop-bridge/http-forward.js +48 -3
  16. package/dist/core/codex-lb/desktop-bridge/server.js +13 -7
  17. package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +2 -1
  18. package/dist/core/codex-lb/desktop-bridge-migration/retired-runtime-cleanup.js +46 -8
  19. package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +12 -2
  20. package/dist/core/codex-lb/desktop-controller-v3/live-probes.js +34 -8
  21. package/dist/core/codex-lb/desktop-controller-v3/preflight-probes.js +13 -4
  22. package/dist/core/codex-lb/desktop-controller-v3/shared.js +2 -1
  23. package/dist/core/codex-lb/desktop-controller-v3/verification.js +10 -9
  24. package/dist/core/codex-lb/desktop-service.js +57 -4
  25. package/dist/core/codex-lb/route-index.js +1 -0
  26. package/dist/core/managed-assets/managed-assets-manifest.js +1 -0
  27. package/dist/core/release/canonical-test-proof.js +8 -2
  28. package/dist/core/routes/evidence.js +1 -1
  29. package/dist/core/version.js +1 -1
  30. package/dist/native/sks-menubar/Sources/CodexLifecyclePolicy.swift +18 -0
  31. package/dist/native/sks-menubar/Sources/ProcessClient.swift +3 -1
  32. package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +94 -3
  33. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +49 -15
  34. package/dist/native/sks-menubar/Sources/SKSKeychainStore.swift +21 -1
  35. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +18 -17
  36. package/dist/native/sks-menubar/Sources/StatusItemController.swift +14 -14
  37. package/dist/scripts/canonical-test-runner.js +83 -3
  38. package/package.json +1 -1
package/README.md CHANGED
@@ -22,13 +22,13 @@ Proof-first orchestration for Codex CLI, ChatGPT Desktop, AI coding agents, mult
22
22
  Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.
23
23
  <!-- END SKS SEARCH VISIBILITY MARKETING -->
24
24
 
25
- This README documents package **SKS 8.3.0** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
25
+ This README documents package **SKS 8.3.2** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
26
26
 
27
27
  Use the official latest stable SKS and Codex CLI releases. The Codex compatibility SSOT is always the **current latest stable** host; capability probes measure what that host can actually do. Product docs do not crown a fixed `0.x.y` string as SSOT (release pins and schema directories are measured artifacts for the current package, not a permanent product version claim). Menu Bar / Center induce updates to the latest stable build. Run `sks update-check` for what is installed and read the capability report for what is supported. Install SSOT is npm `sneakoscope@latest`; PATH `sks` and Menu Bar stamped generation must match that version or gates fail. It resolves managed SKS skills from the authoritative global install, preserves a runnable Naruto child slot when `max_threads=2`, and keeps Menu Bar repair transactional so stamped generations remain verifiable. Naruto uses stable opt-in multi-agent V2 when the host exposes it (Codex official multi-agent wrap-only; SKS does not reimplement a parallel runtime). Local code search is mode-separated (`sks search files|text|structure|symbol|context`); `context` is answered by the compiled TriWiki Context Graph (`context-graph.json` is exhaustive authority; `context-pack.json` and managed `AGENTS.md` are bounded projections) — see [docs/architecture/context-graph.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/architecture/context-graph.md) and [docs/PRODUCT-CONTRACT.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/PRODUCT-CONTRACT.md). See [CHANGELOG.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/CHANGELOG.md).
28
28
 
29
- ## What 8.2.0 Ships
29
+ ## What 8.3.1 Ships
30
30
 
31
- | Problem | 8.2.0 behavior |
31
+ | Problem | 8.3.1 behavior |
32
32
  | --- | --- |
33
33
  | Overview mixed Menu Bar, installed SKS, and cached registry versions | Each value is labeled by authority, stale or unavailable probes remain explicit, and Refresh forces a bounded update-status refresh. |
34
34
  | Naruto stopped creating children after its first wave | The root parent records settled waves, recovers open-thread capacity, rescans the ready DAG, and can launch later direct-child waves under the same workflow run. |
@@ -39,6 +39,8 @@ Use the official latest stable SKS and Codex CLI releases. The Codex compatibili
39
39
  | Codex routing was split across legacy provider paths | One local Desktop Bridge is the managed routing runtime; Codex-LB and OpenRouter are simultaneous credential profiles. |
40
40
  | Requests could be coupled to a provider mode or weak model heuristic | The combined catalog's explicit route index resolves provider/model pairs with `fallback: none`; missing or ambiguous routes block. |
41
41
  | Transport verification was confused with deep evidence | Capability v3 records scope, stage, requested level, execution, and readiness separately; unattempted deep work is not a transport failure. |
42
+ | The Menu Bar could terminate with Codex and miss the next Codex launch | A launchd-resident observer remains alive, follows Codex visibility without terminating, and migrates the legacy quit preference. |
43
+ | A partial Provider repair could look successful because the CLI exited zero | The Center validates the structured command status, records partial repair as needing action, and shows capability issues before verified diagnostic rows. |
42
44
 
43
45
  ## Install In One Command
44
46
 
@@ -65,6 +65,7 @@
65
65
  {
66
66
  "canonical_name": "sks-computer-use-fast",
67
67
  "trusted_sha256": [
68
+ "57e4c1d04ae785b0a53711e53210f2ddabd6131041b97edb4b6d99f18b24155e",
68
69
  "b270d82e4057934d7f92ed4241d4ae86e22d0a9a08bceba1aa179d7535bda042",
69
70
  "b8736b2621398dfc85f7a2ff35d7a2fc728db0dfc0e964de69aebff93b5ed897"
70
71
  ]
@@ -79,6 +80,7 @@
79
80
  {
80
81
  "canonical_name": "sks-cu",
81
82
  "trusted_sha256": [
83
+ "be9a3eb7197483693f354e5f7e5a8eb2c0dc0209597cea53479e2e803ad39479",
82
84
  "fc6ed807d0299dd56441738d9388787b1dd857dab9bb4c31a11f7d9b90b424d0",
83
85
  "da480ba03208d4888df1e97bbec6d80d608dafad0e33c2b1c72266e1860476a8"
84
86
  ]
@@ -148,6 +150,7 @@
148
150
  {
149
151
  "canonical_name": "sks-from-chat-img",
150
152
  "trusted_sha256": [
153
+ "1cf6b9494829216c27c0778cecd825698c37a6a6dbde67a2a79dd957a32c24cc",
151
154
  "be2f0ec42839c0c8ec09c4bfbb99e3c57d6ffe1b8253c2ebbf894200cf8dbf40",
152
155
  "583c41f46a7355d8c0c87472ea83573695d04341822eb9adcea822ed3311809f"
153
156
  ]
@@ -259,7 +259,7 @@ dependencies = [
259
259
 
260
260
  [[package]]
261
261
  name = "sks-core"
262
- version = "8.3.0"
262
+ version = "8.3.2"
263
263
  dependencies = [
264
264
  "globset",
265
265
  "grep-matcher",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "8.3.0"
3
+ version = "8.3.2"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "sks.skills-manifest.v1",
3
- "package_version": "8.3.0",
3
+ "package_version": "8.3.2",
4
4
  "skills": [
5
5
  {
6
6
  "canonical_name": "sks",
@@ -82,8 +82,9 @@
82
82
  {
83
83
  "canonical_name": "sks-computer-use-fast",
84
84
  "type": "official",
85
- "content_sha256": "b270d82e4057934d7f92ed4241d4ae86e22d0a9a08bceba1aa179d7535bda042",
85
+ "content_sha256": "57e4c1d04ae785b0a53711e53210f2ddabd6131041b97edb4b6d99f18b24155e",
86
86
  "hash_history": [
87
+ "b270d82e4057934d7f92ed4241d4ae86e22d0a9a08bceba1aa179d7535bda042",
87
88
  "b8736b2621398dfc85f7a2ff35d7a2fc728db0dfc0e964de69aebff93b5ed897"
88
89
  ],
89
90
  "deprecated_aliases": []
@@ -100,8 +101,9 @@
100
101
  {
101
102
  "canonical_name": "sks-cu",
102
103
  "type": "official",
103
- "content_sha256": "fc6ed807d0299dd56441738d9388787b1dd857dab9bb4c31a11f7d9b90b424d0",
104
+ "content_sha256": "be9a3eb7197483693f354e5f7e5a8eb2c0dc0209597cea53479e2e803ad39479",
104
105
  "hash_history": [
106
+ "fc6ed807d0299dd56441738d9388787b1dd857dab9bb4c31a11f7d9b90b424d0",
105
107
  "da480ba03208d4888df1e97bbec6d80d608dafad0e33c2b1c72266e1860476a8"
106
108
  ],
107
109
  "deprecated_aliases": []
@@ -188,8 +190,9 @@
188
190
  {
189
191
  "canonical_name": "sks-from-chat-img",
190
192
  "type": "official",
191
- "content_sha256": "be2f0ec42839c0c8ec09c4bfbb99e3c57d6ffe1b8253c2ebbf894200cf8dbf40",
193
+ "content_sha256": "1cf6b9494829216c27c0778cecd825698c37a6a6dbde67a2a79dd957a32c24cc",
192
194
  "hash_history": [
195
+ "be2f0ec42839c0c8ec09c4bfbb99e3c57d6ffe1b8253c2ebbf894200cf8dbf40",
193
196
  "583c41f46a7355d8c0c87472ea83573695d04341822eb9adcea822ed3311809f"
194
197
  ],
195
198
  "deprecated_aliases": []
@@ -4,18 +4,18 @@ import { readJson, runProcess, which, writeJsonAtomic } from '../../fsx.js';
4
4
  export async function readMenuBarConfig(configPath) {
5
5
  const value = await readJson(configPath, null);
6
6
  return {
7
- schema: 'sks.sks-menubar-config.v1',
7
+ schema: 'sks.sks-menubar-config.v2',
8
8
  codex_bundle_id: typeof value?.codex_bundle_id === 'string' && value.codex_bundle_id.trim()
9
9
  ? value.codex_bundle_id.trim() : null,
10
- quit_with_codex: value?.quit_with_codex === true
10
+ follow_codex_lifecycle: value?.follow_codex_lifecycle === true || value?.quit_with_codex === true
11
11
  };
12
12
  }
13
13
  export async function writeDefaultMenuBarConfig(configPath, codexBundleId) {
14
14
  const previous = await readMenuBarConfig(configPath);
15
15
  const value = {
16
- schema: 'sks.sks-menubar-config.v1',
16
+ schema: 'sks.sks-menubar-config.v2',
17
17
  codex_bundle_id: codexBundleId || previous.codex_bundle_id,
18
- quit_with_codex: previous.quit_with_codex
18
+ follow_codex_lifecycle: previous.follow_codex_lifecycle
19
19
  };
20
20
  await writeJsonAtomic(configPath, value);
21
21
  return value;
@@ -6,12 +6,13 @@ export const MENU_ITEMS = [
6
6
  'Pending approvals',
7
7
  'Check for Updates',
8
8
  'View Last Operation',
9
- 'Quit SKS Menu'
9
+ 'Hide Until Codex Opens'
10
10
  ];
11
11
  export const NATIVE_SOURCE_FILES = [
12
12
  'main.swift',
13
13
  'AppDelegate.swift',
14
14
  'StatusItemController.swift',
15
+ 'CodexLifecyclePolicy.swift',
15
16
  'ControlCenterWindowController.swift',
16
17
  'SidebarItem.swift',
17
18
  'ControlKit.swift',
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { exists, nowIso, readJson, readText, runProcess, writeJsonAtomic } from '../../fsx.js';
3
3
  import { ensureConfinedDirectory, inspectConfinedPath, removeManagedPathVerified } from '../../managed-path-safety.js';
4
+ import { realUserHome } from './installer/runtime.js';
4
5
  const MENU_BAR_EXECUTABLE_SUFFIX = path.join('SKSMenuBar.app', 'Contents', 'MacOS', 'SKSMenuBar');
5
6
  const MENU_BAR_PROCESS_DISCOVERY_TIMEOUT_MS = 5_000;
6
7
  export function projectMenuBarDuplicateInstallDirs(input) {
@@ -21,6 +22,8 @@ export async function verifiedProjectMenuBarDuplicateExecutablePaths(input) {
21
22
  const executables = [];
22
23
  const candidates = await discoverProjectMenuBarDuplicateCandidates(input);
23
24
  for (const candidate of candidates.inspected) {
25
+ if (isOutsideScopeReapingRefused(candidate, root, home))
26
+ continue;
24
27
  const confinementRoot = confinementRootForCandidate(root, home, candidate);
25
28
  const state = await inspectConfinedPath(confinementRoot, candidate).catch(() => null);
26
29
  if (!state?.exists || state.leafSymlink || !state.stat?.isDirectory())
@@ -96,6 +99,11 @@ export async function cleanupProjectMenuBarDuplicates(input) {
96
99
  const blockers = [];
97
100
  const warnings = [];
98
101
  for (const candidate of inspected) {
102
+ if (isOutsideScopeReapingRefused(candidate, root, input.paths.home)) {
103
+ preserved.push(candidate);
104
+ warnings.push(`menubar_project_duplicate_outside_scope_preserved:${candidate}`);
105
+ continue;
106
+ }
99
107
  const confinementRoot = confinementRootForCandidate(root, input.paths.home, candidate);
100
108
  const state = await inspectConfinedPath(confinementRoot, candidate).catch(() => null);
101
109
  if (!state) {
@@ -254,6 +262,14 @@ function duplicateInstallDirForExecutable(executablePath, canonicalInstallDir) {
254
262
  return null;
255
263
  return executable.slice(0, -suffix.length);
256
264
  }
265
+ function isOutsideScopeReapingRefused(candidateInput, rootInput, homeInput) {
266
+ const candidate = path.resolve(candidateInput);
267
+ const root = path.resolve(rootInput);
268
+ const home = path.resolve(homeInput);
269
+ if (isWithin(root, candidate) || isWithin(home, candidate))
270
+ return false;
271
+ return home !== realUserHome();
272
+ }
257
273
  function confinementRootForCandidate(rootInput, homeInput, candidate) {
258
274
  const root = path.resolve(rootInput);
259
275
  const home = path.resolve(homeInput);
@@ -6,7 +6,7 @@ import { aggregateFileHashes, createSksMenuBarBuildStamp, menuBarBuildStampsEqua
6
6
  import { SKS_MENUBAR_LABEL } from './constants.js';
7
7
  import { resolveCodexBundleId, writeDefaultMenuBarConfig } from './config.js';
8
8
  import { applyMenuBarGenerationTransaction, commitMenuBarGenerationTransaction, installGenerationPairs, MenuBarGenerationTransactionError, recoverMenuBarGenerationTransaction, rollbackGenerationPairs } from './generation-transaction.js';
9
- import { launchAgentSource, launchMenuBar, inspectRunningMenuBarProcess, seedMenuBarPreferredPosition, stopMenuBarForReplacement, terminateMenuBarProcesses } from './launch-agent.js';
9
+ import { launchAgentSource, launchMenuBar, inspectRunningMenuBarProcess, menuBarLaunchdTouchAllowed, seedMenuBarPreferredPosition, stopMenuBarForReplacement, terminateMenuBarProcesses } from './launch-agent.js';
10
10
  import { cleanupProjectMenuBarDuplicates, verifiedProjectMenuBarDuplicateExecutablePaths } from './global-install.js';
11
11
  import { cleanupRetiredRemoteBridgeLaunchAgent, quarantineRetiredRemoteBridgeBindings } from './migration.js';
12
12
  import { sksMenuBarPaths } from './paths.js';
@@ -114,6 +114,11 @@ export async function installSksMenuBar(opts = {}) {
114
114
  return blocked('sks_entry_unresolved');
115
115
  if (targetCheck.project_local)
116
116
  warnings.push('sks_entry_project_local');
117
+ const macosProtectedRoots = ['Desktop', 'Documents', 'Downloads']
118
+ .map((dir) => `${path.join(paths.home, dir)}${path.sep}`);
119
+ if (targetCheck.resolved && macosProtectedRoots.some((root) => targetCheck.resolved.startsWith(root))) {
120
+ warnings.push('sks_entry_macos_protected_folder');
121
+ }
117
122
  const previousActionScript = await readText(paths.action_script_path, '');
118
123
  const previousLaunchAgent = await readText(paths.launch_agent_path, '');
119
124
  const previousRaw = await readJson(paths.build_stamp_path, null);
@@ -256,7 +261,15 @@ export async function installSksMenuBar(opts = {}) {
256
261
  });
257
262
  const mustStopCanonical = !upToDate || (launchWanted && launchAllowed);
258
263
  let canonicalStopped = false;
259
- if (mustStopCanonical) {
264
+ if (mustStopCanonical && !menuBarLaunchdTouchAllowed(paths, env)) {
265
+ const stopped = await terminateMenuBarProcesses([paths.executable_path, ...duplicateExecutablePaths], env);
266
+ if (!stopped.ok) {
267
+ return blocked('menubar_stop_before_replace_failed', `Remaining PIDs: ${stopped.remaining_pids.join(',')}`);
268
+ }
269
+ warnings.push('launchd_stop_skipped_non_user_home');
270
+ actions.push('verified canonical/project duplicate processes stopped without touching the user launchd service');
271
+ }
272
+ else if (mustStopCanonical) {
260
273
  const stopped = await stopMenuBarForReplacement({
261
274
  launchctl,
262
275
  paths,
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { exists, readJson, runProcess, which, writeJsonAtomic } from '../../fsx.js';
4
4
  import { CONTROL_CENTER_DOMAIN, CONTROL_CENTER_PREFERRED_POSITION, SKS_MENUBAR_LABEL } from './constants.js';
5
+ import { realUserHome } from './installer/runtime.js';
5
6
  export function launchAgentSource(executablePath, installDir) {
6
7
  return `<?xml version="1.0" encoding="UTF-8"?>
7
8
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -9,11 +10,15 @@ export function launchAgentSource(executablePath, installDir) {
9
10
  <key>Label</key><string>${SKS_MENUBAR_LABEL}</string>
10
11
  <key>ProgramArguments</key><array><string>${xml(executablePath)}</string></array>
11
12
  <key>RunAtLoad</key><true/>
13
+ <key>KeepAlive</key><true/>
12
14
  <key>ProcessType</key><string>Interactive</string>
13
15
  <key>StandardOutPath</key><string>${xml(path.join(installDir, 'menubar.out.log'))}</string>
14
16
  <key>StandardErrorPath</key><string>${xml(path.join(installDir, 'menubar.err.log'))}</string>
15
17
  </dict></plist>\n`;
16
18
  }
19
+ export function menuBarLaunchdTouchAllowed(paths, env) {
20
+ return Boolean(env.SKS_MENUBAR_LAUNCHCTL) || path.resolve(paths.home) === realUserHome();
21
+ }
17
22
  export async function seedMenuBarPreferredPosition(env) {
18
23
  const defaults = env.SKS_MENUBAR_DEFAULTS || await which('defaults').catch(() => null) || '/usr/bin/defaults';
19
24
  const writes = [
@@ -304,15 +309,24 @@ export async function removeLaunchAgent(paths, env) {
304
309
  const actions = [];
305
310
  const warnings = [];
306
311
  const blockers = [];
307
- const launchctl = env.SKS_MENUBAR_LAUNCHCTL || await which('launchctl').catch(() => null) || '/bin/launchctl';
308
- const stopped = await stopMenuBarForReplacement({
309
- launchctl,
310
- paths,
311
- executablePaths: [paths.executable_path],
312
- env
313
- });
314
- if (!stopped.ok)
315
- blockers.push(stopped.error || 'menubar_process_stop_failed');
312
+ if (menuBarLaunchdTouchAllowed(paths, env)) {
313
+ const launchctl = env.SKS_MENUBAR_LAUNCHCTL || await which('launchctl').catch(() => null) || '/bin/launchctl';
314
+ const stopped = await stopMenuBarForReplacement({
315
+ launchctl,
316
+ paths,
317
+ executablePaths: [paths.executable_path],
318
+ env
319
+ });
320
+ if (!stopped.ok)
321
+ blockers.push(stopped.error || 'menubar_process_stop_failed');
322
+ }
323
+ else {
324
+ const terminated = await terminateMenuBarProcesses([paths.executable_path], env);
325
+ if (!terminated.ok)
326
+ blockers.push('menubar_process_stop_failed');
327
+ else
328
+ warnings.push('launchd_stop_skipped_non_user_home');
329
+ }
316
330
  if (blockers.length > 0)
317
331
  return { actions, warnings, blockers };
318
332
  await fs.rm(paths.launch_agent_path, { force: true }).catch((error) => blockers.push(`remove_launch_agent_failed:${String(error)}`));
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { readJson, sha256, which } from '../../fsx.js';
4
4
  import { applyMenuBarGenerationTransaction, commitMenuBarGenerationTransaction, installGenerationPairs, MenuBarGenerationTransactionError, recoverMenuBarGenerationTransaction, rollbackGenerationPairs } from './generation-transaction.js';
5
- import { launchMenuBar, stopMenuBarForReplacement } from './launch-agent.js';
5
+ import { launchMenuBar, menuBarLaunchdTouchAllowed, stopMenuBarForReplacement, terminateMenuBarProcesses } from './launch-agent.js';
6
6
  import { sksMenuBarPaths } from './paths.js';
7
7
  import { aggregateFileHashes } from './build-stamp.js';
8
8
  import { inspectInstalledResources } from './resources.js';
@@ -286,7 +286,7 @@ export async function rollbackSksMenuBar(opts = {}) {
286
286
  else {
287
287
  transaction = committed;
288
288
  }
289
- if (opts.launch === false || env.SKS_SKIP_SKS_MENUBAR_LAUNCH === '1') {
289
+ if (opts.launch === false || env.SKS_SKIP_SKS_MENUBAR_LAUNCH === '1' || !menuBarLaunchdTouchAllowed(paths, env)) {
290
290
  return result('rolled_back_launch_skipped', true, verificationBefore.package_version, replacedStamp?.package_version || null, verificationBefore, verificationAfter, { requested: false, method: 'skipped', ok: true }, [], warnings);
291
291
  }
292
292
  const launch = await launchMenuBar({
@@ -307,6 +307,16 @@ export async function rollbackSksMenuBar(opts = {}) {
307
307
  async function stopBeforeReplacement() {
308
308
  if (replacementStopped)
309
309
  return null;
310
+ if (!menuBarLaunchdTouchAllowed(paths, env)) {
311
+ const terminated = await terminateMenuBarProcesses([paths.executable_path], env);
312
+ if (!terminated.ok) {
313
+ return result('failed', false, null, null, null, null, { requested: false, method: 'none', ok: false, error: 'menubar_rollback_stop_failed' }, ['menubar_rollback_stop_failed'], warnings);
314
+ }
315
+ replacementStopped = true;
316
+ warnings.push('launchd_stop_skipped_non_user_home');
317
+ actions.push('verified process termination without touching the user launchd service before journaled rollback replacement');
318
+ return null;
319
+ }
310
320
  const stopped = await stopMenuBarForReplacement({
311
321
  launchctl,
312
322
  paths,
@@ -44,7 +44,8 @@ export function normalizeProviderCatalog(input) {
44
44
  supported_in_api: row?.supported_in_api !== false,
45
45
  capabilities,
46
46
  source_catalog_generation: input.generation || 'unknown',
47
- route_key: `openrouter:${publicId}`
47
+ route_key: `openrouter:${publicId}`,
48
+ ...codexModelInfoFields(publicId, null, { tools: capabilities.includes('tools') })
48
49
  });
49
50
  }).filter(isModel);
50
51
  if (models.length === 0)
@@ -119,7 +120,11 @@ function normalizeCodexLbBridgeCatalogModels(payload, sourceCatalogGeneration) {
119
120
  supported_in_api: row.supported_in_api !== false,
120
121
  capabilities: unique(capabilities).sort(),
121
122
  source_catalog_generation: sourceCatalogGeneration,
122
- route_key: `codex-lb:${publicId.toLowerCase()}`
123
+ route_key: `codex-lb:${publicId.toLowerCase()}`,
124
+ ...codexModelInfoFields(publicId, row, {
125
+ tools: row.supports_tools === true || row.supports_tool_choice === true
126
+ }),
127
+ ...(typeof row.priority === 'number' ? {} : { priority: 100 })
123
128
  });
124
129
  }
125
130
  catch (error) {
@@ -146,6 +151,32 @@ function sanitizeCodexLbModelRow(row, index) {
146
151
  function isPlainObject(value) {
147
152
  return Boolean(value && typeof value === 'object' && !Array.isArray(value));
148
153
  }
154
+ export function codexModelInfoFields(slug, row, fallback) {
155
+ const summaries = typeof row?.supports_reasoning_summary_parameter === 'boolean'
156
+ ? row.supports_reasoning_summary_parameter
157
+ : row?.supports_reasoning_summaries !== false;
158
+ return {
159
+ slug,
160
+ supported_reasoning_levels: Array.isArray(row?.supported_reasoning_levels)
161
+ ? row.supported_reasoning_levels.map(String)
162
+ : [],
163
+ shell_type: typeof row?.shell_type === 'string' && row.shell_type ? row.shell_type : 'shell_command',
164
+ visibility: typeof row?.visibility === 'string' && row.visibility ? row.visibility : 'list',
165
+ priority: typeof row?.priority === 'number' && Number.isFinite(row.priority) ? row.priority : 1,
166
+ base_instructions: typeof row?.base_instructions === 'string' ? row.base_instructions : '',
167
+ supports_reasoning_summary_parameter: summaries,
168
+ support_verbosity: row?.support_verbosity === true,
169
+ truncation_policy: isPlainObject(row?.truncation_policy)
170
+ ? row.truncation_policy
171
+ : { mode: 'tokens', limit: 10_000 },
172
+ supports_parallel_tool_calls: typeof row?.supports_parallel_tool_calls === 'boolean'
173
+ ? row.supports_parallel_tool_calls
174
+ : fallback.tools,
175
+ experimental_supported_tools: Array.isArray(row?.experimental_supported_tools)
176
+ ? row.experimental_supported_tools.map(String)
177
+ : []
178
+ };
179
+ }
149
180
  function providerCode(providerId) {
150
181
  return providerId === 'codex-lb' ? 'codex_lb' : 'openrouter';
151
182
  }
@@ -162,7 +193,10 @@ function canonicalModel(model) {
162
193
  supported_in_api: model.supported_in_api !== false,
163
194
  capabilities: unique(model.capabilities).sort(),
164
195
  source_catalog_generation: String(model.source_catalog_generation || 'unknown'),
165
- route_key: `${model.provider_id}:${publicId}`
196
+ route_key: `${model.provider_id}:${publicId}`,
197
+ ...codexModelInfoFields(publicId, model, {
198
+ tools: model.capabilities.includes('tools')
199
+ })
166
200
  };
167
201
  }
168
202
  function isModel(value) {
@@ -3,8 +3,14 @@ export function safeErrorCode(error, fallback) {
3
3
  return /^[a-z0-9_:-]{1,160}$/i.test(message) ? message : fallback;
4
4
  }
5
5
  export function compareModels(left, right) {
6
+ if (left.provider_id !== right.provider_id) {
7
+ if (left.provider_id === 'codex-lb')
8
+ return -1;
9
+ if (right.provider_id === 'codex-lb')
10
+ return 1;
11
+ return left.provider_id.localeCompare(right.provider_id);
12
+ }
6
13
  return left.public_id.localeCompare(right.public_id)
7
- || left.provider_id.localeCompare(right.provider_id)
8
14
  || left.upstream_model.localeCompare(right.upstream_model);
9
15
  }
10
16
  export function unique(values) {
@@ -6,8 +6,9 @@ const REQUEST_HEADER_ALLOWLIST = new Set([
6
6
  'accept', 'accept-encoding', 'cache-control', 'content-encoding', 'content-length', 'content-type',
7
7
  ]);
8
8
  const CODEX_LOCAL_HEADERS = new Set([
9
- 'thread-id', 'session-id', 'x-client-request-id', 'x-codex-installation-id',
10
- 'x-codex-turn-metadata', 'x-codex-window-id', 'originator',
9
+ 'thread-id', 'thread_id', 'session-id', 'session_id', 'x-client-request-id',
10
+ 'x-codex-installation-id', 'x-codex-turn-metadata', 'x-codex-window-id',
11
+ 'originator', 'openai-beta',
11
12
  ]);
12
13
  const NEVER_FORWARD_FROM_CLIENT = new Set([
13
14
  'authorization', 'cookie', 'forwarded', 'proxy-authorization', 'x-api-key', 'x-codex-lb-api-key',
@@ -43,7 +44,8 @@ export function buildProviderUpstreamHeaders(inbound, context, upstreamHost) {
43
44
  if (rawValue === undefined)
44
45
  continue;
45
46
  const name = rawName.toLowerCase();
46
- const providerAllowed = context.providerId === 'openrouter' && OPENROUTER_CLIENT_HEADERS.has(name);
47
+ const providerAllowed = (context.providerId === 'openrouter' && OPENROUTER_CLIENT_HEADERS.has(name))
48
+ || (context.providerId === 'codex-lb' && CODEX_LOCAL_HEADERS.has(name));
47
49
  if (!REQUEST_HEADER_ALLOWLIST.has(name) && !providerAllowed)
48
50
  continue;
49
51
  result[name] = rawValue;
@@ -1,11 +1,44 @@
1
1
  import http, {} from 'node:http';
2
2
  import https from 'node:https';
3
+ import zlib from 'node:zlib';
3
4
  import { pipeline } from 'node:stream/promises';
4
5
  import { buildProviderUpstreamHeaders, rewriteResponseHeaders } from './header-policy.js';
5
6
  import { resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader } from './security.js';
6
7
  import { desktopBridgeListenOrigin } from './state.js';
7
8
  import { DesktopBridgeError } from './types.js';
8
9
  const MAX_UPSTREAM_ERROR_BODY_BYTES = 1024 * 1024;
10
+ function decodeBridgeRequestBody(body, encoding, maximum) {
11
+ const options = { maxOutputLength: maximum };
12
+ try {
13
+ switch (encoding) {
14
+ case 'gzip':
15
+ case 'x-gzip':
16
+ return zlib.gunzipSync(body, options);
17
+ case 'deflate':
18
+ try {
19
+ return zlib.inflateSync(body, options);
20
+ }
21
+ catch {
22
+ return zlib.inflateRawSync(body, options);
23
+ }
24
+ case 'br':
25
+ return zlib.brotliDecompressSync(body, options);
26
+ case 'zstd': {
27
+ const zstd = zlib.zstdDecompressSync;
28
+ if (!zstd)
29
+ throw new DesktopBridgeError('bridge_request_encoding_unsupported');
30
+ return zstd(body, options);
31
+ }
32
+ default:
33
+ throw new DesktopBridgeError('bridge_request_encoding_unsupported');
34
+ }
35
+ }
36
+ catch (error) {
37
+ if (error instanceof DesktopBridgeError)
38
+ throw error;
39
+ throw new DesktopBridgeError('bridge_responses_body_invalid_json');
40
+ }
41
+ }
9
42
  function bodyCarriesModel(rawUrl) {
10
43
  const pathname = new URL(String(rawUrl || '/'), 'http://bridge.invalid').pathname;
11
44
  return pathname === '/backend-api/codex/responses' || pathname === '/api/v1/responses' || pathname === '/v1/responses';
@@ -40,10 +73,18 @@ export async function prepareDesktopBridgeRequest(req, config) {
40
73
  const pathname = new URL(String(req.url || '/'), 'http://bridge.invalid').pathname;
41
74
  let body = null;
42
75
  let payload = null;
76
+ let contentEncodingStripped = false;
43
77
  if (bodyCarriesModel(req.url)) {
44
- body = await readBoundedBody(req, config.maxRequestBodyBytes ?? 16 * 1024 * 1024);
78
+ const maximum = config.maxRequestBodyBytes ?? 16 * 1024 * 1024;
79
+ body = await readBoundedBody(req, maximum);
80
+ const encoding = String(req.headers['content-encoding'] || '').trim().toLowerCase();
81
+ let decoded = body;
82
+ if (encoding && encoding !== 'identity') {
83
+ decoded = decodeBridgeRequestBody(body, encoding, maximum);
84
+ contentEncodingStripped = true;
85
+ }
45
86
  try {
46
- const parsed = JSON.parse(body.toString('utf8'));
87
+ const parsed = JSON.parse(decoded.toString('utf8'));
47
88
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
48
89
  throw new Error();
49
90
  payload = parsed;
@@ -51,6 +92,8 @@ export async function prepareDesktopBridgeRequest(req, config) {
51
92
  catch {
52
93
  throw new DesktopBridgeError('bridge_responses_body_invalid_json');
53
94
  }
95
+ if (contentEncodingStripped)
96
+ body = decoded;
54
97
  }
55
98
  const headerModel = singleBridgeHeader(req.headers, 'x-sks-model');
56
99
  const model = typeof payload?.model === 'string' ? payload.model : headerModel;
@@ -64,7 +107,7 @@ export async function prepareDesktopBridgeRequest(req, config) {
64
107
  body = Buffer.from(JSON.stringify(payload));
65
108
  }
66
109
  const credential = await resolveCredential(config, route);
67
- return { body, route, credential };
110
+ return { body, route, credential, contentEncodingStripped };
68
111
  }
69
112
  function connectTimeout(request, config) {
70
113
  request.once('socket', (socket) => {
@@ -121,6 +164,8 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
121
164
  headers['content-length'] = String(request.body.length);
122
165
  else
123
166
  delete headers['content-length'];
167
+ if (request.contentEncodingStripped)
168
+ delete headers['content-encoding'];
124
169
  await new Promise((resolve, reject) => {
125
170
  let responseStarted = false;
126
171
  let settled = false;
@@ -117,16 +117,22 @@ function writeBridgeRejection(res, error) {
117
117
  res.end(JSON.stringify({ error: { type: 'sks_bridge_rejection', code, message: code } }));
118
118
  }
119
119
  function writeUpgradeRejection(socket, error) {
120
- if (socket.destroyed)
120
+ socket.on('error', () => undefined);
121
+ if (socket.destroyed || socket.writableEnded)
121
122
  return;
122
123
  const code = safeBridgeErrorCode(error);
123
124
  const status = rejectionStatus(code);
124
- socket.end(`HTTP/1.1 ${status} ${rejectionStatusText(status)}\r\n`
125
- + 'Content-Type: application/json\r\n'
126
- + 'Cache-Control: no-store\r\n'
127
- + 'Connection: close\r\n'
128
- + '\r\n'
129
- + JSON.stringify({ error: { type: 'sks_bridge_rejection', code, message: code } }));
125
+ try {
126
+ socket.end(`HTTP/1.1 ${status} ${rejectionStatusText(status)}\r\n`
127
+ + 'Content-Type: application/json\r\n'
128
+ + 'Cache-Control: no-store\r\n'
129
+ + 'Connection: close\r\n'
130
+ + '\r\n'
131
+ + JSON.stringify({ error: { type: 'sks_bridge_rejection', code, message: code } }));
132
+ }
133
+ catch {
134
+ socket.destroy();
135
+ }
130
136
  }
131
137
  function writeDiagnosticHealth(req, res, input) {
132
138
  if (req.method !== 'GET')
@@ -28,8 +28,9 @@ export function inspectHistoricalDesktopBridgeIntent(configText) {
28
28
  blockers.push('historical_provider_marker_invalid');
29
29
  if (nativeBridge && compatBridge)
30
30
  blockers.push('historical_desktop_marker_conflict');
31
- if (nativeBridge && selectedProvider !== 'openai')
31
+ if (nativeBridge && selectedProvider !== null && selectedProvider !== 'openai') {
32
32
  blockers.push('historical_native_bridge_selection_mismatch');
33
+ }
33
34
  if (compatBridge && selectedProvider !== 'codex-lb')
34
35
  blockers.push('historical_compat_bridge_selection_mismatch');
35
36
  if (managedCodexLbSelection && selectedProvider !== 'codex-lb') {