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.
- package/README.md +5 -3
- package/config/skills-hash-ledger.v1.json +3 -0
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/config/skills-manifest.json +7 -4
- package/dist/core/codex-app/menubar/config.js +4 -4
- package/dist/core/codex-app/menubar/constants.js +2 -1
- package/dist/core/codex-app/menubar/global-install.js +16 -0
- package/dist/core/codex-app/menubar/installer.js +15 -2
- package/dist/core/codex-app/menubar/launch-agent.js +23 -9
- package/dist/core/codex-app/menubar/rollback.js +12 -2
- package/dist/core/codex-lb/combined-catalog/normalize.js +37 -3
- package/dist/core/codex-lb/combined-catalog/shared.js +7 -1
- package/dist/core/codex-lb/desktop-bridge/header-policy.js +5 -3
- package/dist/core/codex-lb/desktop-bridge/http-forward.js +48 -3
- package/dist/core/codex-lb/desktop-bridge/server.js +13 -7
- package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +2 -1
- package/dist/core/codex-lb/desktop-bridge-migration/retired-runtime-cleanup.js +46 -8
- package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +12 -2
- package/dist/core/codex-lb/desktop-controller-v3/live-probes.js +34 -8
- package/dist/core/codex-lb/desktop-controller-v3/preflight-probes.js +13 -4
- package/dist/core/codex-lb/desktop-controller-v3/shared.js +2 -1
- package/dist/core/codex-lb/desktop-controller-v3/verification.js +10 -9
- package/dist/core/codex-lb/desktop-service.js +57 -4
- package/dist/core/codex-lb/route-index.js +1 -0
- package/dist/core/managed-assets/managed-assets-manifest.js +1 -0
- package/dist/core/release/canonical-test-proof.js +8 -2
- package/dist/core/routes/evidence.js +1 -1
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/CodexLifecyclePolicy.swift +18 -0
- package/dist/native/sks-menubar/Sources/ProcessClient.swift +3 -1
- package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +94 -3
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +49 -15
- package/dist/native/sks-menubar/Sources/SKSKeychainStore.swift +21 -1
- package/dist/native/sks-menubar/Sources/SettingsViewController.swift +18 -17
- package/dist/native/sks-menubar/Sources/StatusItemController.swift +14 -14
- package/dist/scripts/canonical-test-runner.js +83 -3
- 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.
|
|
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.
|
|
29
|
+
## What 8.3.1 Ships
|
|
30
30
|
|
|
31
|
-
| Problem | 8.
|
|
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
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "sks.skills-manifest.v1",
|
|
3
|
-
"package_version": "8.3.
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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.
|
|
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
|
-
|
|
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.
|
|
16
|
+
schema: 'sks.sks-menubar-config.v2',
|
|
17
17
|
codex_bundle_id: codexBundleId || previous.codex_bundle_id,
|
|
18
|
-
|
|
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
|
-
'
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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', '
|
|
10
|
-
'x-codex-turn-metadata', 'x-codex-window-id',
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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') {
|