sneakoscope 10.1.1 → 10.1.3
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 +9 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/cli/router.js +1 -1
- package/dist/commands/codex-app.js +12 -1
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/codex/context-management.js +55 -0
- package/dist/core/codex-app/context-management-command.js +38 -0
- package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +286 -0
- package/dist/core/codex-lb/desktop-bridge/security.js +4 -0
- package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +10 -0
- package/dist/core/codex-lb/desktop-controller-v3/catalog.js +2 -2
- package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +2 -2
- package/dist/core/codex-lb/desktop-controller-v3/shared.js +3 -1
- package/dist/core/codex-lb/desktop-controller-v3/status.js +2 -1
- package/dist/core/codex-lb/desktop-service.js +22 -15
- package/dist/core/codex-runtime/codex-desktop-config-policy.js +2 -0
- package/dist/core/routes.js +1 -1
- package/dist/core/update/update-migration-state/desktop-bridge-restage.js +48 -52
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/AuthPriorityState.swift +1 -1
- package/dist/native/sks-menubar/Sources/SettingsViewController.swift +65 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
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.
|
|
17
17
|
<!-- END SKS SEARCH VISIBILITY MARKETING -->
|
|
18
18
|
|
|
19
|
-
Current package: **SKS 10.1.
|
|
19
|
+
Current package: **SKS 10.1.3**. Install the latest stable release from npm.
|
|
20
20
|
|
|
21
21
|
[Quick start](#install-in-one-command) · [Commands](#everyday-commands) · [SKS Center](#sks-center-macos) · [Documentation](#documentation) · [Changelog](CHANGELOG.md)
|
|
22
22
|
|
|
@@ -56,6 +56,13 @@ for setup boundaries and reported execution evidence.
|
|
|
56
56
|
|
|
57
57
|
## Everyday commands
|
|
58
58
|
|
|
59
|
+
SKS enables experimental Astra context management by default during setup and
|
|
60
|
+
repair. Turn it off in **SKS Center → Settings → Astra context management**, or
|
|
61
|
+
use `sks codex-app context-management off`. Updates preserve an explicit opt-out.
|
|
62
|
+
Start a new task after changing the setting. Availability depends on a supported
|
|
63
|
+
Codex client and eligible ChatGPT sign-in; API-key and custom-provider sessions
|
|
64
|
+
may not activate it. See [OpenAI's context management guidance](https://learn.chatgpt.com/docs/models#experimental-context-management).
|
|
65
|
+
|
|
59
66
|
Use these inside a Codex conversation:
|
|
60
67
|
|
|
61
68
|
| Command | Purpose |
|
|
@@ -169,6 +176,7 @@ freshness semantics.
|
|
|
169
176
|
- [Essential Trust](docs/essential-trust.md) — verification profiles and safety boundaries.
|
|
170
177
|
- [Astra guidance](docs/astra-guidance.md) — how SKS applies the official model recommendations.
|
|
171
178
|
- [Agent Bridge](docs/AGENT-BRIDGE.md) — integrate through the CLI or MCP interface.
|
|
179
|
+
- [Codex-LB priority](docs/codex-lb-priority.md) — how Codex App WebSockets follow the Center priority setting.
|
|
172
180
|
- [Release readiness](docs/release-readiness.md) — build, verify, and publish a release.
|
|
173
181
|
- [Release evidence](docs/release-proof-truth.md) — what each verification result proves.
|
|
174
182
|
- [Changelog](CHANGELOG.md) — changes by version.
|
package/dist/cli/router.js
CHANGED
|
@@ -302,7 +302,7 @@ export function safeReadOnlySubcommand(command, args) {
|
|
|
302
302
|
if (command === 'mcp' && sub === 'config' && ['list', 'test', 'backups', 'show'].includes(nested)) {
|
|
303
303
|
return !args.some((arg) => ['--fix', '--yes', '-y', '--write', '--apply', '--execute', '--force', '--real'].includes(String(arg)));
|
|
304
304
|
}
|
|
305
|
-
if (command === 'codex-app' &&
|
|
305
|
+
if (command === 'codex-app' && ['context-1m', 'context-management'].includes(sub) && (nested === 'status' || nested === '' || nested.startsWith('--'))) {
|
|
306
306
|
return !args.some((arg) => ['--fix', '--yes', '-y', '--write', '--apply', '--execute', '--force', '--real'].includes(String(arg)));
|
|
307
307
|
}
|
|
308
308
|
if (command === 'remote' && ['readiness', 'status', 'show'].includes(sub)) {
|
|
@@ -14,6 +14,17 @@ import { restartCodexApp } from '../core/codex-app/codex-app-restart.js';
|
|
|
14
14
|
import { resetRoleModelPreference, roleModelPreferencesStatus, setRoleModelPreference } from '../core/subagents/role-model-preferences.js';
|
|
15
15
|
export async function run(_command, args = []) {
|
|
16
16
|
const action = args[0] || 'check';
|
|
17
|
+
if (action === 'context-management') {
|
|
18
|
+
const { contextManagementCommand } = await import('../core/codex-app/context-management-command.js');
|
|
19
|
+
const result = await contextManagementCommand(args.slice(1));
|
|
20
|
+
if (flag(args, '--json'))
|
|
21
|
+
printJson(result);
|
|
22
|
+
else
|
|
23
|
+
console.log(`${result.ok ? (result.enabled ? 'Enabled' : 'Disabled') : 'Unavailable'}: ${result.message}`);
|
|
24
|
+
if (!result.ok)
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
17
28
|
if (action === 'restart')
|
|
18
29
|
return printCodexAppResult(args, await restartCodexApp());
|
|
19
30
|
if (action === 'context-1m') {
|
|
@@ -137,7 +148,7 @@ export async function run(_command, args = []) {
|
|
|
137
148
|
process.exitCode = 1;
|
|
138
149
|
return;
|
|
139
150
|
}
|
|
140
|
-
console.error('Usage: sks codex-app check|status|restart|context-1m [status|on|off] [--no-restart]|harness-matrix|skill-sync|agent-role-sync|init-deep|hook-lifecycle|execution-profile|role-models|set-role-model --role <name> [--provider <id>] --model <catalog-slug> --reasoning <effort>|reset-role-model --role <name>|product-design [--check-only]|ensure-product-design|chrome-extension|pat status|remote-control [--json]');
|
|
151
|
+
console.error('Usage: sks codex-app check|status|restart|context-management [status|on|off]|context-1m [status|on|off] [--no-restart]|harness-matrix|skill-sync|agent-role-sync|init-deep|hook-lifecycle|execution-profile|role-models|set-role-model --role <name> [--provider <id>] --model <catalog-slug> --reasoning <effort>|reset-role-model --role <name>|product-design [--check-only]|ensure-product-design|chrome-extension|pat status|remote-control [--json]');
|
|
141
152
|
console.error('Provider routing moved to: sks bridge provider configure|validate|enable; sks bridge catalog sync; sks bridge route set-default.');
|
|
142
153
|
process.exitCode = 1;
|
|
143
154
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { parse } from 'smol-toml';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
export function contextManagementValue(text) {
|
|
4
|
+
const value = parse(text).features?.context_management?.experimental_mode;
|
|
5
|
+
if (value !== undefined && typeof value !== 'boolean')
|
|
6
|
+
throw new Error('context_management_invalid_boolean');
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
export function setContextManagement(text, enabled, onlyIfAbsent = false) {
|
|
10
|
+
const before = parse(text);
|
|
11
|
+
const current = contextManagementValue(text);
|
|
12
|
+
if (current === enabled || (onlyIfAbsent && current !== undefined))
|
|
13
|
+
return text;
|
|
14
|
+
const expected = structuredClone(before);
|
|
15
|
+
expected.features ??= {};
|
|
16
|
+
expected.features.context_management ??= {};
|
|
17
|
+
expected.features.context_management.experimental_mode = enabled;
|
|
18
|
+
const valid = (candidate) => {
|
|
19
|
+
try {
|
|
20
|
+
return isDeepStrictEqual(parse(candidate), expected);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
if (current !== undefined) {
|
|
27
|
+
for (const match of text.matchAll(/\b(?:true|false)\b/g)) {
|
|
28
|
+
const candidate = text.slice(0, match.index) + String(enabled) + text.slice(match.index + match[0].length);
|
|
29
|
+
if (valid(candidate))
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const suffix = `\n[features.context_management]\nexperimental_mode = ${enabled}\n`;
|
|
35
|
+
if (valid(text + suffix))
|
|
36
|
+
return text + suffix;
|
|
37
|
+
const candidates = [
|
|
38
|
+
`features.context_management.experimental_mode = ${enabled}\n${text}`,
|
|
39
|
+
...[...text.matchAll(/\{/g)].flatMap(match => {
|
|
40
|
+
const offset = match.index + 1;
|
|
41
|
+
const separator = /^\s*\}/.test(text.slice(offset)) ? '' : ', ';
|
|
42
|
+
return [`experimental_mode = ${enabled}`, `context_management = { experimental_mode = ${enabled} }`]
|
|
43
|
+
.map(value => text.slice(0, offset) + value + separator + text.slice(offset));
|
|
44
|
+
}),
|
|
45
|
+
...[...text.matchAll(/^\s*\[[^\n]+\][^\n]*(?:\n|$)/gm)].flatMap(match => {
|
|
46
|
+
const offset = match.index + match[0].length;
|
|
47
|
+
return ['experimental_mode', 'context_management.experimental_mode'].map(key => text.slice(0, offset) + `\n${key} = ${enabled}\n` + text.slice(offset));
|
|
48
|
+
}),
|
|
49
|
+
];
|
|
50
|
+
for (const candidate of candidates)
|
|
51
|
+
if (valid(candidate))
|
|
52
|
+
return candidate;
|
|
53
|
+
}
|
|
54
|
+
throw new Error('context_management_config_edit_unsupported');
|
|
55
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { codexUserConfigPath } from './codex-model-catalog.js';
|
|
3
|
+
import { writeCodexConfigGuarded } from '../codex/codex-config-guard.js';
|
|
4
|
+
import { contextManagementValue, setContextManagement } from '../codex/context-management.js';
|
|
5
|
+
export async function contextManagementCommand(args, options = {}) {
|
|
6
|
+
const configPath = codexUserConfigPath(options);
|
|
7
|
+
const action = args[0] || 'status';
|
|
8
|
+
try {
|
|
9
|
+
if (!['status', 'on', 'off'].includes(action) || args.slice(1).some(arg => arg !== '--json'))
|
|
10
|
+
throw new Error('context_management_invalid_arguments');
|
|
11
|
+
let exists = true;
|
|
12
|
+
const before = await fs.readFile(configPath, 'utf8').catch(error => { if (error.code !== 'ENOENT')
|
|
13
|
+
throw error; exists = false; return ''; });
|
|
14
|
+
let after = before;
|
|
15
|
+
let changed = false;
|
|
16
|
+
if (action !== 'status') {
|
|
17
|
+
const next = setContextManagement(before, action === 'on');
|
|
18
|
+
const write = await writeCodexConfigGuarded({
|
|
19
|
+
configPath, before, cause: 'context-management', removeTopLevelModeLocks: false,
|
|
20
|
+
verifyUnchangedBeforeWrite: true, expectedBeforeExists: exists, mutate: () => next,
|
|
21
|
+
});
|
|
22
|
+
if (!write.ok)
|
|
23
|
+
throw new Error(`context_management_write_${write.status}`);
|
|
24
|
+
after = await fs.readFile(configPath, 'utf8');
|
|
25
|
+
if (contextManagementValue(after) !== (action === 'on'))
|
|
26
|
+
throw new Error('context_management_readback_mismatch');
|
|
27
|
+
changed = write.changed;
|
|
28
|
+
}
|
|
29
|
+
const value = contextManagementValue(after);
|
|
30
|
+
return { schema: 'sks.context-management.v1', ok: true, enabled: value === true, configured: value !== undefined,
|
|
31
|
+
default_enabled: true, changed, config_path: configPath,
|
|
32
|
+
message: 'Applies to new tasks on supported Codex clients with eligible ChatGPT sign-in. API-key and custom-provider sessions may not activate it.' };
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { schema: 'sks.context-management.v1', ok: false, enabled: null, config_path: configPath,
|
|
36
|
+
message: 'Could not read or update the setting. Check the Codex configuration; existing content was not replaced without validation.' };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import tls from 'node:tls';
|
|
3
|
+
import WebSocket, { WebSocketServer } from 'ws';
|
|
4
|
+
import { BRIDGE_OFFICIAL_ROUTE_ID } from '../bridge-contracts.js';
|
|
5
|
+
import { buildOfficialPassthroughWebSocketHeaders, buildProviderWebSocketHeaders } from './header-policy.js';
|
|
6
|
+
import { createDesktopBridgeRejectionLogger } from './rejection-log.js';
|
|
7
|
+
import { assertDesktopBridgeRouteContext, ensureDesktopBridgeRemoteTarget, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode } from './security.js';
|
|
8
|
+
import { DesktopBridgeError } from './types.js';
|
|
9
|
+
const MAX_PENDING_MESSAGES = 256;
|
|
10
|
+
const CLOSE_GRACE_MS = 1_000;
|
|
11
|
+
const INITIAL_CREATE_TIMEOUT_MS = 60 * 60_000;
|
|
12
|
+
const logRejection = createDesktopBridgeRejectionLogger();
|
|
13
|
+
export function isResponsesWebSocketRequest(req) {
|
|
14
|
+
const pathname = new URL(req.url || '/', 'http://bridge.invalid').pathname;
|
|
15
|
+
return ['/backend-api/codex/responses', '/api/v1/responses', '/v1/responses'].includes(pathname);
|
|
16
|
+
}
|
|
17
|
+
function bytes(data) {
|
|
18
|
+
return Buffer.isBuffer(data) ? data : Array.isArray(data) ? Buffer.concat(data) : Buffer.from(data);
|
|
19
|
+
}
|
|
20
|
+
export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
21
|
+
const upgradeIdentity = resolveCodexSessionIdentity(req.headers);
|
|
22
|
+
const maxBytes = config.maxRequestBodyBytes ?? 16 * 1024 * 1024;
|
|
23
|
+
const server = new WebSocketServer({ noServer: true, clientTracking: false, perMessageDeflate: false, maxPayload: maxBytes });
|
|
24
|
+
try {
|
|
25
|
+
server.handleUpgrade(req, socket, head, (client) => {
|
|
26
|
+
bridge(client);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
server.close();
|
|
31
|
+
}
|
|
32
|
+
function bridge(client) {
|
|
33
|
+
let identity = upgradeIdentity;
|
|
34
|
+
let upstream = null;
|
|
35
|
+
let upstreamSocket = null;
|
|
36
|
+
let bound = null;
|
|
37
|
+
let boundBaseUrl = null;
|
|
38
|
+
let boundCredentialGeneration = null;
|
|
39
|
+
let boundCredentialFingerprint = null;
|
|
40
|
+
let stopped = false;
|
|
41
|
+
let processing = false;
|
|
42
|
+
let pendingBytes = 0;
|
|
43
|
+
const pending = [];
|
|
44
|
+
let closeTimer = null;
|
|
45
|
+
let handshakeTimer = null;
|
|
46
|
+
const initialTimer = setTimeout(closeUnbound, config.websocketInitialCreateTimeoutMs ?? INITIAL_CREATE_TIMEOUT_MS);
|
|
47
|
+
initialTimer.unref();
|
|
48
|
+
const localSocket = socket;
|
|
49
|
+
localSocket.setKeepAlive?.(true, 30_000);
|
|
50
|
+
function stop() {
|
|
51
|
+
if (stopped)
|
|
52
|
+
return;
|
|
53
|
+
stopped = true;
|
|
54
|
+
clearTimeout(initialTimer);
|
|
55
|
+
if (handshakeTimer)
|
|
56
|
+
clearTimeout(handshakeTimer);
|
|
57
|
+
pending.length = 0;
|
|
58
|
+
pendingBytes = 0;
|
|
59
|
+
closeTimer = setTimeout(() => { client.terminate(); upstream?.terminate(); upstreamSocket?.destroy(); }, CLOSE_GRACE_MS);
|
|
60
|
+
closeTimer.unref();
|
|
61
|
+
}
|
|
62
|
+
function fail(error) {
|
|
63
|
+
if (stopped)
|
|
64
|
+
return;
|
|
65
|
+
const code = safeBridgeErrorCode(error);
|
|
66
|
+
logRejection({ code, transport: 'websocket', ...(req.method ? { method: req.method } : {}), ...(req.url ? { url: req.url } : {}) });
|
|
67
|
+
stop();
|
|
68
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
69
|
+
if (client.bufferedAmount < maxBytes)
|
|
70
|
+
client.send(JSON.stringify({ type: 'error', error: { type: 'sks_bridge_error', code, message: code } }), () => undefined);
|
|
71
|
+
client.close(1011, code.slice(0, 123));
|
|
72
|
+
}
|
|
73
|
+
upstream?.terminate();
|
|
74
|
+
upstreamSocket?.destroy();
|
|
75
|
+
}
|
|
76
|
+
function closeUnbound() {
|
|
77
|
+
if (stopped || bound)
|
|
78
|
+
return;
|
|
79
|
+
logRejection({ code: 'bridge_websocket_initial_create_timeout', transport: 'websocket', ...(req.method ? { method: req.method } : {}), ...(req.url ? { url: req.url } : {}) });
|
|
80
|
+
stop();
|
|
81
|
+
if (client.readyState === WebSocket.OPEN)
|
|
82
|
+
client.close(1000, 'bridge_websocket_initial_create_timeout');
|
|
83
|
+
}
|
|
84
|
+
function relay(destination, data, binary) {
|
|
85
|
+
if (destination.readyState !== WebSocket.OPEN)
|
|
86
|
+
throw new DesktopBridgeError('bridge_websocket_upstream_unavailable');
|
|
87
|
+
if (destination.bufferedAmount + data.length > maxBytes)
|
|
88
|
+
throw new DesktopBridgeError('bridge_websocket_backpressure_exceeded');
|
|
89
|
+
destination.send(data, { binary }, (error) => { if (error)
|
|
90
|
+
fail(error); });
|
|
91
|
+
}
|
|
92
|
+
function routeRequest(model) {
|
|
93
|
+
return { public_model: model, session_id: identity.thread_id, pathname: new URL(req.url || '/', 'http://bridge.invalid').pathname, transport: 'websocket', headers: req.headers };
|
|
94
|
+
}
|
|
95
|
+
function assertBinding(route) {
|
|
96
|
+
if (!bound)
|
|
97
|
+
return;
|
|
98
|
+
const official = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID;
|
|
99
|
+
const provider = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID ? null : config.providers[route.provider_id];
|
|
100
|
+
const baseUrl = official ? config.officialRemote?.baseUrl : provider?.base_url;
|
|
101
|
+
if (route.provider_id !== bound.provider_id || baseUrl !== boundBaseUrl
|
|
102
|
+
|| (provider && (provider.credential_generation !== boundCredentialGeneration || provider.credential_fingerprint !== boundCredentialFingerprint))) {
|
|
103
|
+
throw new DesktopBridgeError('bridge_websocket_route_change_forbidden');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function resolveCreate(message) {
|
|
107
|
+
const model = message.create?.model;
|
|
108
|
+
if (typeof model !== 'string' || !model.trim())
|
|
109
|
+
throw new DesktopBridgeError('bridge_websocket_model_required');
|
|
110
|
+
const nextIdentity = resolveCodexSessionIdentity(req.headers, message.create);
|
|
111
|
+
if (bound && ((nextIdentity.thread_id && nextIdentity.thread_id !== identity.thread_id)
|
|
112
|
+
|| (nextIdentity.session_id && nextIdentity.session_id !== identity.session_id))) {
|
|
113
|
+
throw new DesktopBridgeError('bridge_codex_session_identity_conflict');
|
|
114
|
+
}
|
|
115
|
+
if (!bound)
|
|
116
|
+
identity = nextIdentity;
|
|
117
|
+
const request = routeRequest(model);
|
|
118
|
+
assertBinding(assertDesktopBridgeRouteContext(request, config));
|
|
119
|
+
const route = await resolveAndBindDesktopBridgeRouteContext(request, config);
|
|
120
|
+
if (stopped || client.readyState !== WebSocket.OPEN)
|
|
121
|
+
return;
|
|
122
|
+
assertBinding(route);
|
|
123
|
+
if (!bound) {
|
|
124
|
+
bound = route;
|
|
125
|
+
await connect(route);
|
|
126
|
+
}
|
|
127
|
+
if (route.upstream_model !== model)
|
|
128
|
+
message.data = Buffer.from(JSON.stringify({ ...message.create, model: route.upstream_model }));
|
|
129
|
+
}
|
|
130
|
+
async function connect(route) {
|
|
131
|
+
const official = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID;
|
|
132
|
+
const provider = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID ? null : config.providers[route.provider_id];
|
|
133
|
+
const remote = official ? config.officialRemote : provider?.remote;
|
|
134
|
+
if (!remote)
|
|
135
|
+
throw new DesktopBridgeError('bridge_websocket_upstream_unavailable');
|
|
136
|
+
boundBaseUrl = official ? remote.baseUrl : provider.base_url;
|
|
137
|
+
boundCredentialGeneration = provider?.credential_generation ?? null;
|
|
138
|
+
boundCredentialFingerprint = provider?.credential_fingerprint ?? null;
|
|
139
|
+
const credential = provider ? await config.resolveProviderCredential(provider.provider_id, provider.credential_generation) : null;
|
|
140
|
+
if (credential && (credential.provider_id !== provider.provider_id || credential.generation !== boundCredentialGeneration
|
|
141
|
+
|| (boundCredentialFingerprint && credential.fingerprint !== boundCredentialFingerprint)))
|
|
142
|
+
throw new DesktopBridgeError('bridge_provider_credential_generation_mismatch');
|
|
143
|
+
await ensureDesktopBridgeRemoteTarget(remote, config.remoteLookup);
|
|
144
|
+
if (stopped || client.readyState !== WebSocket.OPEN)
|
|
145
|
+
return;
|
|
146
|
+
const target = resolveDesktopBridgeTarget(req.url, remote);
|
|
147
|
+
const outgoing = official ? buildOfficialPassthroughWebSocketHeaders(req.headers, target.host)
|
|
148
|
+
: buildProviderWebSocketHeaders(req.headers, { providerId: provider.provider_id, authTransport: provider.auth_transport, credential: credential }, target.host);
|
|
149
|
+
const headers = {};
|
|
150
|
+
for (const [name, value] of Object.entries(outgoing)) {
|
|
151
|
+
if (name.startsWith('sec-websocket-') || name === 'connection' || name === 'upgrade' || name === 'content-length')
|
|
152
|
+
continue;
|
|
153
|
+
if (value !== undefined)
|
|
154
|
+
headers[name] = Array.isArray(value) ? value.join(', ') : String(value);
|
|
155
|
+
}
|
|
156
|
+
target.protocol = remote.secure ? 'wss:' : 'ws:';
|
|
157
|
+
let connected = false;
|
|
158
|
+
handshakeTimer = setTimeout(() => {
|
|
159
|
+
if (!connected)
|
|
160
|
+
void refreshDesktopBridgeRemoteTarget(remote, config.remoteLookup);
|
|
161
|
+
fail(new DesktopBridgeError('bridge_websocket_upstream_handshake_timeout'));
|
|
162
|
+
}, config.connectTimeoutMs);
|
|
163
|
+
handshakeTimer.unref();
|
|
164
|
+
upstream = new WebSocket(target, client.protocol ? [client.protocol] : [], {
|
|
165
|
+
headers, perMessageDeflate: false, maxPayload: maxBytes,
|
|
166
|
+
handshakeTimeout: config.connectTimeoutMs, followRedirects: false,
|
|
167
|
+
createConnection: () => {
|
|
168
|
+
const connection = remote.secure
|
|
169
|
+
? tls.connect({ host: remote.address, port: remote.port, ...(remote.tlsServername ? { servername: remote.tlsServername } : {}) })
|
|
170
|
+
: net.connect({ host: remote.address, port: remote.port, family: remote.family });
|
|
171
|
+
upstreamSocket = connection;
|
|
172
|
+
connection.setKeepAlive(true, 30_000);
|
|
173
|
+
connection.once(remote.secure ? 'secureConnect' : 'connect', () => { connected = true; });
|
|
174
|
+
return connection;
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
const peer = upstream;
|
|
178
|
+
peer.on('error', (error) => {
|
|
179
|
+
if (isUnreachableUpstreamError(error))
|
|
180
|
+
void refreshDesktopBridgeRemoteTarget(remote, config.remoteLookup);
|
|
181
|
+
fail(new DesktopBridgeError('bridge_websocket_upstream_unavailable'));
|
|
182
|
+
});
|
|
183
|
+
peer.on('unexpected-response', (_request, response) => {
|
|
184
|
+
response.destroy();
|
|
185
|
+
fail(new DesktopBridgeError(`bridge_websocket_upgrade_failed_${response.statusCode || 502}`));
|
|
186
|
+
});
|
|
187
|
+
peer.on('message', (data, binary) => {
|
|
188
|
+
if (stopped)
|
|
189
|
+
return;
|
|
190
|
+
try {
|
|
191
|
+
relay(client, bytes(data), binary);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
fail(error);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
peer.on('close', (code, reason) => {
|
|
198
|
+
upstreamSocket?.destroy();
|
|
199
|
+
stop();
|
|
200
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
201
|
+
if (code === 1005)
|
|
202
|
+
client.close();
|
|
203
|
+
else
|
|
204
|
+
client.close(code === 1006 ? 1011 : code, code === 1006 ? 'bridge_websocket_upstream_closed' : reason);
|
|
205
|
+
}
|
|
206
|
+
clearCloseTimer();
|
|
207
|
+
});
|
|
208
|
+
await new Promise((resolve, reject) => {
|
|
209
|
+
peer.once('open', resolve);
|
|
210
|
+
peer.once('error', reject);
|
|
211
|
+
peer.once('close', () => reject(new DesktopBridgeError('bridge_websocket_upstream_closed')));
|
|
212
|
+
});
|
|
213
|
+
if (handshakeTimer)
|
|
214
|
+
clearTimeout(handshakeTimer);
|
|
215
|
+
clearTimeout(initialTimer);
|
|
216
|
+
}
|
|
217
|
+
async function drain() {
|
|
218
|
+
if (processing || stopped)
|
|
219
|
+
return;
|
|
220
|
+
processing = true;
|
|
221
|
+
try {
|
|
222
|
+
if (!bound) {
|
|
223
|
+
const firstCreate = pending.find((message) => message.create);
|
|
224
|
+
if (!firstCreate)
|
|
225
|
+
return;
|
|
226
|
+
await resolveCreate(firstCreate);
|
|
227
|
+
firstCreate.create = null;
|
|
228
|
+
}
|
|
229
|
+
while (!stopped && client.readyState === WebSocket.OPEN && pending.length) {
|
|
230
|
+
const message = pending[0];
|
|
231
|
+
if (message.create)
|
|
232
|
+
await resolveCreate(message);
|
|
233
|
+
if (stopped || client.readyState !== WebSocket.OPEN)
|
|
234
|
+
return;
|
|
235
|
+
relay(upstream, message.data, message.binary);
|
|
236
|
+
pending.shift();
|
|
237
|
+
pendingBytes -= message.originalBytes;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
fail(error);
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
processing = false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function clearCloseTimer() {
|
|
248
|
+
if (client.readyState === WebSocket.CLOSED && (!upstream || upstream.readyState === WebSocket.CLOSED) && closeTimer)
|
|
249
|
+
clearTimeout(closeTimer);
|
|
250
|
+
}
|
|
251
|
+
client.on('message', (data, binary) => {
|
|
252
|
+
if (stopped)
|
|
253
|
+
return;
|
|
254
|
+
const payload = bytes(data);
|
|
255
|
+
let create = null;
|
|
256
|
+
try {
|
|
257
|
+
const parsed = JSON.parse(payload.toString('utf8'));
|
|
258
|
+
if (parsed && typeof parsed === 'object' && parsed.type === 'response.create')
|
|
259
|
+
create = parsed;
|
|
260
|
+
}
|
|
261
|
+
catch { }
|
|
262
|
+
if (pending.length >= MAX_PENDING_MESSAGES || pendingBytes + payload.length > maxBytes) {
|
|
263
|
+
fail(new DesktopBridgeError('bridge_websocket_pending_limit_exceeded'));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
pending.push({ data: payload, binary, create, originalBytes: payload.length });
|
|
267
|
+
pendingBytes += payload.length;
|
|
268
|
+
void drain();
|
|
269
|
+
});
|
|
270
|
+
client.on('error', () => { stop(); upstream?.terminate(); upstreamSocket?.destroy(); client.terminate(); });
|
|
271
|
+
client.once('close', (code, reason) => {
|
|
272
|
+
stop();
|
|
273
|
+
if (upstream?.readyState === WebSocket.OPEN) {
|
|
274
|
+
if (code === 1005)
|
|
275
|
+
upstream.close();
|
|
276
|
+
else
|
|
277
|
+
upstream.close(code === 1006 ? 1011 : code, reason);
|
|
278
|
+
}
|
|
279
|
+
else if (upstream?.readyState === WebSocket.CONNECTING) {
|
|
280
|
+
upstream.terminate();
|
|
281
|
+
upstreamSocket?.destroy();
|
|
282
|
+
}
|
|
283
|
+
clearCloseTimer();
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -581,6 +581,10 @@ export function validateDesktopBridgeConfig(config) {
|
|
|
581
581
|
&& (!Number.isInteger(config.maxConnections) || config.maxConnections < 1 || config.maxConnections > 2_048)) {
|
|
582
582
|
throw new DesktopBridgeError('bridge_connection_limit_invalid');
|
|
583
583
|
}
|
|
584
|
+
if (config.websocketInitialCreateTimeoutMs !== undefined
|
|
585
|
+
&& (!Number.isFinite(config.websocketInitialCreateTimeoutMs) || config.websocketInitialCreateTimeoutMs < 1_000 || config.websocketInitialCreateTimeoutMs > 86_400_000)) {
|
|
586
|
+
throw new DesktopBridgeError('bridge_websocket_initial_create_timeout_invalid');
|
|
587
|
+
}
|
|
584
588
|
for (const origin of config.allowedOrigins)
|
|
585
589
|
normalizeAllowedOrigin(origin);
|
|
586
590
|
if (!config.providerRegistry)
|
|
@@ -7,6 +7,7 @@ import { createDesktopBridgeRejectionLogger } from './rejection-log.js';
|
|
|
7
7
|
import { rewriteLocationHeader } from './location-rewrite.js';
|
|
8
8
|
import { desktopBridgeOfficialPassthroughEnabled, ensureDesktopBridgeRemoteTarget, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader, canonicalSessionId } from './security.js';
|
|
9
9
|
import { desktopBridgeListenOrigin } from './state.js';
|
|
10
|
+
import { forwardResponsesWebSocket, isResponsesWebSocketRequest } from './responses-websocket.js';
|
|
10
11
|
import { DESKTOP_BRIDGE_DIAGNOSTIC_PROTOCOL, DesktopBridgeError, } from './types.js';
|
|
11
12
|
const MAX_HEAD = 64 * 1024;
|
|
12
13
|
const REDACTED_RESPONSE_HEADERS = new Set(['authorization', 'cookie', 'proxy-authenticate', 'proxy-authorization', 'set-cookie', 'x-api-key', 'x-codex-lb-api-key']);
|
|
@@ -137,6 +138,15 @@ function writeUpgradeFailure(client, error, req) {
|
|
|
137
138
|
+ JSON.stringify({ error: { type: 'sks_bridge_error', code, retryable: !permanent } }));
|
|
138
139
|
}
|
|
139
140
|
export async function forwardWebSocket(req, client, head, config, authenticatedLocalBaseUrl = desktopBridgeListenOrigin(config)) {
|
|
141
|
+
if (isResponsesWebSocketRequest(req)) {
|
|
142
|
+
try {
|
|
143
|
+
forwardResponsesWebSocket(req, client, head, config);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
writeUpgradeFailure(client, error, req);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
140
150
|
let prepared;
|
|
141
151
|
try {
|
|
142
152
|
prepared = await prepareDesktopBridgeWebSocketRequest(req, config);
|
|
@@ -22,7 +22,7 @@ export async function syncCatalog(options) {
|
|
|
22
22
|
: {};
|
|
23
23
|
return commandResult('catalog.sync', result.ok === true, status, { catalog_sync: result }, result.ok === true ? [] : stringArray(activation.blockers), options);
|
|
24
24
|
}
|
|
25
|
-
export async function syncCatalogInternal(options) {
|
|
25
|
+
export async function syncCatalogInternal(options, behavior = {}) {
|
|
26
26
|
const paths = controllerPaths(options);
|
|
27
27
|
const historicalIntent = inspectHistoricalDesktopBridgeIntent(await readText(paths.configPath, ''));
|
|
28
28
|
if (historicalIntent.blockers.length > 0)
|
|
@@ -155,7 +155,7 @@ export async function syncCatalogInternal(options) {
|
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
157
|
const serviceBefore = await (options.serviceStatusImpl || desktopBridgeServiceStatus)({ ...options, home: paths.home });
|
|
158
|
-
if (serviceBefore.installed || serviceBefore.running) {
|
|
158
|
+
if (behavior.restartService !== false && (serviceBefore.installed || serviceBefore.running)) {
|
|
159
159
|
await (options.bootstrapServiceImpl || bootstrapExistingDesktopBridgeService)({
|
|
160
160
|
...options,
|
|
161
161
|
home: paths.home,
|
|
@@ -12,7 +12,7 @@ import { commandResult, controllerPaths, nowIso, persistRuntimeSettings, provide
|
|
|
12
12
|
import { desktopBridgeStatusV3, loadCore, statusFromCore } from './status.js';
|
|
13
13
|
import { verifyDesktopBridgeV3 } from './verification.js';
|
|
14
14
|
export async function ensureDesktopBridge(options, operation) {
|
|
15
|
-
const sync = await syncCatalogInternal(options);
|
|
15
|
+
const sync = await syncCatalogInternal(options, { restartService: false });
|
|
16
16
|
let core = await loadCore(options);
|
|
17
17
|
if (!core.activeCatalog.ok || !core.policy) {
|
|
18
18
|
return commandResult(operation, true, statusFromCore(core, options), { catalog_sync: sync }, syncResultBlockers(sync), options);
|
|
@@ -35,7 +35,7 @@ export async function repairDesktopBridge(options) {
|
|
|
35
35
|
let core = await loadCore(options);
|
|
36
36
|
if (!core.activeCatalog.ok || !core.policy)
|
|
37
37
|
return ensureDesktopBridge(options, 'repair');
|
|
38
|
-
await persistRuntimeSettings(core, options);
|
|
38
|
+
await persistRuntimeSettings(core, options, { restartService: false });
|
|
39
39
|
const service = await (options.installServiceImpl || installAndStartDesktopBridgeService)({
|
|
40
40
|
...options,
|
|
41
41
|
home: core.paths.home,
|
|
@@ -116,11 +116,13 @@ export async function persistRuntimeSettings(core, options, behavior = {}) {
|
|
|
116
116
|
await stopAfterFailedRestart(restartOptions, options);
|
|
117
117
|
throw error;
|
|
118
118
|
}
|
|
119
|
-
if (!restarted.
|
|
119
|
+
if (!restarted.running) {
|
|
120
120
|
await stopAfterFailedRestart(restartOptions, options);
|
|
121
121
|
const rootCause = restarted.blockers.find((blocker) => blocker === 'desktop_bridge_entry_macos_protected_folder');
|
|
122
122
|
throw new Error(rootCause || restarted.blockers[0] || 'desktop_bridge_restart_failed');
|
|
123
123
|
}
|
|
124
|
+
if (!restarted.ok)
|
|
125
|
+
throw new Error(restarted.blockers[0] || 'desktop_bridge_restart_failed');
|
|
124
126
|
}
|
|
125
127
|
export async function quiesceRunningBridge(core, options) {
|
|
126
128
|
if (!core.service.running)
|
|
@@ -287,6 +287,7 @@ export function authPriorityStatusFromCore(core) {
|
|
|
287
287
|
: profile.state !== 'ready' ? 'codex_lb_route_not_ready'
|
|
288
288
|
: !core.activeCatalog.ok || core.activeCatalog.route_index.providers['codex-lb'].state !== 'ready' || !core.policy || !Object.entries(core.policy.model_routes).some(([model, route]) => /^(?:gpt-[0-9]|o[0-9]|codex-mini)/.test(model) && !model.includes(':') && route.provider_id === 'codex-lb')
|
|
289
289
|
? 'codex_lb_eligible_route_missing'
|
|
290
|
-
: !core.service.running ? 'desktop_bridge_not_running'
|
|
290
|
+
: !core.service.running ? 'desktop_bridge_not_running'
|
|
291
|
+
: !core.service.ok ? 'desktop_bridge_runtime_not_ready' : null;
|
|
291
292
|
return { enabled: true, state: error ? 'unavailable' : 'active', error };
|
|
292
293
|
}
|
|
@@ -373,22 +373,24 @@ export async function installAndStartDesktopBridgeService(options = {}) {
|
|
|
373
373
|
return failedStatus(paths, settings, service, 'missing', 'desktop_bridge_launchd_bootstrap_failed');
|
|
374
374
|
await run(ctl, ['kickstart', '-k', service], { timeoutMs: 10_000, maxOutputBytes: 32 * 1024 }).catch(() => undefined);
|
|
375
375
|
const status = await waitForBridge({ ...options, home });
|
|
376
|
-
if (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
376
|
+
if (status.running) {
|
|
377
|
+
if (!status.ok)
|
|
378
|
+
return status;
|
|
379
|
+
try {
|
|
380
|
+
await cleanupRetiredDesktopBridgeRuntime(retired);
|
|
381
|
+
return status;
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
await run(ctl, ['bootout', service], { timeoutMs: 5_000, maxOutputBytes: 16 * 1024 }).catch(() => undefined);
|
|
385
|
+
await removeStaleState(paths.state_path, options.processExists);
|
|
386
|
+
return failedStatus(paths, settings, service, 'missing', safeServiceError(error));
|
|
380
387
|
}
|
|
381
|
-
return status;
|
|
382
|
-
}
|
|
383
|
-
try {
|
|
384
|
-
await cleanupRetiredDesktopBridgeRuntime(retired);
|
|
385
|
-
return status;
|
|
386
388
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
return failedStatus(paths, settings, service, 'missing', safeServiceError(error));
|
|
389
|
+
await run(ctl, ['bootout', service], { timeoutMs: 5_000, maxOutputBytes: 16 * 1024 }).catch(() => undefined);
|
|
390
|
+
if (await launchTargetsProtectedFolder([command.executable, ...command.arguments], home)) {
|
|
391
|
+
return withProtectedFolderBlocker(status);
|
|
391
392
|
}
|
|
393
|
+
return status;
|
|
392
394
|
}
|
|
393
395
|
export async function bootstrapExistingDesktopBridgeService(options = {}) {
|
|
394
396
|
const home = options.home || options.env?.HOME || process.env.HOME || os.homedir();
|
|
@@ -710,6 +712,11 @@ function normalizeProviderRegistrySnapshot(value) {
|
|
|
710
712
|
function overridePaths(base, options) { return { settings_path: options.settingsPath || base.settings_path, state_path: options.statePath || base.state_path, client_capability_path: options.clientCapabilityPath || base.client_capability_path, launch_agent_path: options.launchAgentPath || base.launch_agent_path, stdout_log_path: options.stdoutLogPath || base.stdout_log_path, stderr_log_path: options.stderrLogPath || base.stderr_log_path }; }
|
|
711
713
|
function launchDomain(uid = typeof process.getuid === 'function' ? process.getuid() : 0) { return `gui/${uid}`; }
|
|
712
714
|
function launchService(uid) { return `${launchDomain(uid)}/${DESKTOP_BRIDGE_LAUNCHD_LABEL}`; }
|
|
715
|
+
export function launchCommandForExecutable(executable, resolved, execPath = process.execPath) {
|
|
716
|
+
return /\.[cm]?js$/i.test(resolved)
|
|
717
|
+
? { executable: path.resolve(execPath), arguments: [path.resolve(resolved)] }
|
|
718
|
+
: { executable: path.resolve(executable), arguments: [] };
|
|
719
|
+
}
|
|
713
720
|
async function resolveLaunchCommand(options, home) {
|
|
714
721
|
if (options.executablePath) {
|
|
715
722
|
return await exists(path.resolve(options.executablePath))
|
|
@@ -723,7 +730,7 @@ async function resolveLaunchCommand(options, home) {
|
|
|
723
730
|
}
|
|
724
731
|
const sks = await which('sks').catch(() => null);
|
|
725
732
|
if (sks)
|
|
726
|
-
candidates.push(
|
|
733
|
+
candidates.push(launchCommandForExecutable(sks, await fsp.realpath(sks).catch(() => sks)));
|
|
727
734
|
let rejectedProtected = false;
|
|
728
735
|
for (const candidate of candidates) {
|
|
729
736
|
if (await launchTargetsProtectedFolder([candidate.executable, ...candidate.arguments], home)) {
|
|
@@ -783,7 +790,7 @@ export async function bootstrapLaunchdWithRetry(options, domain, service, plistP
|
|
|
783
790
|
async function inspectLaunchd(options, service) { if ((options.platform || process.platform) !== 'darwin')
|
|
784
791
|
return { loaded: false, running: false }; const result = await (options.run || runProcess)(options.launchctl || '/bin/launchctl', ['print', service], { timeoutMs: 3_000, maxOutputBytes: 32 * 1024 }).catch(() => null); if (!result || result.code !== 0)
|
|
785
792
|
return { loaded: false, running: false }; const text = `${result.stdout}\n${result.stderr}`; return { loaded: true, running: /state = running/.test(text) && /pid = \d+/.test(text) }; }
|
|
786
|
-
async function waitForBridge(options) { let status = await desktopBridgeServiceStatus(options); for (let i = 0; i < 150 && !status.
|
|
793
|
+
async function waitForBridge(options) { let status = await desktopBridgeServiceStatus(options); for (let i = 0; i < 150 && !status.running; i += 1) {
|
|
787
794
|
await delay(100);
|
|
788
795
|
status = await desktopBridgeServiceStatus(options);
|
|
789
796
|
} return status; }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
|
+
import { setContextManagement } from '../codex/context-management.js';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { DEFAULT_CODEX_APP_PLUGINS } from '../routes.js';
|
|
4
5
|
import { ensureDir, PACKAGE_VERSION, readText, writeTextAtomic } from '../fsx.js';
|
|
@@ -95,6 +96,7 @@ function normalizeCodexFastModeUiConfigOnce(text = '', opts = {}) {
|
|
|
95
96
|
next = upsertTomlTable(next, table, `[${table}]\nenabled = true`);
|
|
96
97
|
}
|
|
97
98
|
}
|
|
99
|
+
next = setContextManagement(next, true, true);
|
|
98
100
|
return ensureTrailingNewline(next);
|
|
99
101
|
}
|
|
100
102
|
function removeTopLevelTomlKey(text = '', key = '') {
|
package/dist/core/routes.js
CHANGED
|
@@ -644,7 +644,7 @@ export const COMMAND_CATALOG = [
|
|
|
644
644
|
{ name: 'uninstall', usage: 'sks uninstall [--dry-run] [--yes] [--keep-config] [--keep-data] [--purge-projects] [--json]', description: 'Remove SKS global skills, hooks, menu bar, state, temp files, and optional project residue while preserving user-owned content by default.' },
|
|
645
645
|
{ name: 'deps', usage: 'sks deps check [--json] [--yes]', description: 'Check Node/npm and Codex CLI readiness; pass --yes to repair missing Codex CLI tooling when supported.' },
|
|
646
646
|
{ name: 'codex', usage: 'sks codex compatibility|version|update-status [--refresh]|update|doctor|schema|current [--json]', description: 'Check Codex CLI compatibility/version/update status, run the official `codex update`, and inspect current manifest, capability, and hook-schema evidence.' },
|
|
647
|
-
{ name: 'codex-app', usage: 'sks codex-app [check|status|restart|context-1m [status|on|off]|product-design|chrome-extension|pat status|remote-control]', description: 'Check Codex App integration, Desktop Bridge readiness, Product Design plugin readiness, Codex Chrome Extension web verification readiness, PAT-safe status, first-party MCP/plugin readiness, Codex CLI remote-control availability, and the opt-in GPT-5.6 Sol 1M context window toggle with automatic Codex restart. Provider routing is managed only by sks bridge.' },
|
|
647
|
+
{ name: 'codex-app', usage: 'sks codex-app [check|status|restart|context-management [status|on|off]|context-1m [status|on|off]|product-design|chrome-extension|pat status|remote-control]', description: 'Check Codex App integration, Desktop Bridge readiness, Product Design plugin readiness, Codex Chrome Extension web verification readiness, PAT-safe status, first-party MCP/plugin readiness, Codex CLI remote-control availability, and the opt-in GPT-5.6 Sol 1M context window toggle with automatic Codex restart. Provider routing is managed only by sks bridge.' },
|
|
648
648
|
{ name: 'codex-native', usage: 'sks codex-native status|feature-broker|invocation-plan|init-deep [--json]', description: 'Inspect Codex Native feature broker readiness, invocation routing, pattern evidence, and managed memory setup.' },
|
|
649
649
|
{ name: 'hooks', usage: 'sks hooks explain|status|trust-report|replay|codex-validate|warning-check ... [--json]', description: 'Explain Codex hook events, validate current vendored event output schemas, replay fixtures, and enforce warning-zero SKS hook policies.' },
|
|
650
650
|
{ name: 'remote', usage: 'sks remote readiness|machines|worker ... [--json]', description: 'Inspect official Codex Remote readiness and the allowlisted proof-aware SSH stdio worker surface.' },
|
|
@@ -1,73 +1,69 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { exists, PACKAGE_VERSION
|
|
4
|
-
import {
|
|
3
|
+
import { exists, PACKAGE_VERSION } from '../../fsx.js';
|
|
4
|
+
import { executeDesktopBridgeCommandV3 } from '../../codex-lb/desktop-controller-v3.js';
|
|
5
|
+
import { desktopBridgeRuntimeVersion } from '../../codex-lb/desktop-bridge/state.js';
|
|
6
|
+
import { desktopBridgeServicePaths, desktopBridgeServiceStatus } from '../../codex-lb/desktop-service.js';
|
|
5
7
|
export async function desktopBridgeRestage(options = {}) {
|
|
6
8
|
const skip = (reason) => ({ ok: true, status: 'ok', actions: [reason], blockers: [], warnings: [] });
|
|
7
9
|
const warn = (warnings) => ({ ok: true, status: 'ok', actions: [], blockers: [], warnings });
|
|
8
10
|
const env = options.env || process.env;
|
|
11
|
+
const platform = options.platform || process.platform;
|
|
9
12
|
const version = options.packageVersion || PACKAGE_VERSION;
|
|
10
|
-
|
|
11
|
-
if ((options.platform || process.platform) !== 'darwin')
|
|
13
|
+
if (platform !== 'darwin')
|
|
12
14
|
return skip('desktop_bridge_restage_not_macos');
|
|
13
|
-
|
|
15
|
+
const reachesRealLaunchd = !(options.serviceStatus && options.executeCommand);
|
|
16
|
+
const isolated = [process.env, env].some((candidate) => candidate.NODE_TEST_CONTEXT !== undefined
|
|
17
|
+
|| candidate.SKS_TEST_ISOLATION === '1'
|
|
18
|
+
|| candidate.SKS_RELEASE_UPGRADE_SMOKE === '1');
|
|
19
|
+
if (reachesRealLaunchd && isolated)
|
|
14
20
|
return skip('desktop_bridge_restage_skipped_under_tests');
|
|
15
|
-
}
|
|
16
21
|
if (env.SKS_SKIP_BRIDGE_RESTAGE === '1')
|
|
17
22
|
return skip('desktop_bridge_restage_disabled');
|
|
18
23
|
const home = options.home || path.resolve(env.HOME || os.homedir());
|
|
19
24
|
const paths = desktopBridgeServicePaths(home);
|
|
20
25
|
if (!(await exists(paths.launch_agent_path)))
|
|
21
26
|
return skip('desktop_bridge_restage_no_launch_agent');
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.catch(() => null);
|
|
30
|
-
if (bootstrap?.running) {
|
|
31
|
-
return {
|
|
32
|
-
ok: true,
|
|
33
|
-
status: 'ok',
|
|
34
|
-
actions: [`desktop_bridge_bootstrapped:${version}`],
|
|
35
|
-
blockers: [],
|
|
36
|
-
warnings: bootstrap.blockers.map((blocker) => `desktop_bridge_restage_bootstrap_incomplete:${blocker}`)
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
return warn([
|
|
40
|
-
...(bootstrap?.blockers || ['desktop_bridge_restage_bootstrap_failed'])
|
|
41
|
-
.map((blocker) => `desktop_bridge_restage_bootstrap_incomplete:${blocker}`),
|
|
42
|
-
'Desktop Bridge is installed but not running: run `sks bridge repair` from your home directory'
|
|
43
|
-
]);
|
|
44
|
-
}
|
|
45
|
-
if (runningVersion === version)
|
|
27
|
+
if (!(await exists(paths.settings_path)))
|
|
28
|
+
return skip('desktop_bridge_restage_no_managed_bridge');
|
|
29
|
+
const serviceOptions = { home, env, platform };
|
|
30
|
+
const statusImpl = options.serviceStatus || desktopBridgeServiceStatus;
|
|
31
|
+
const before = await statusImpl(serviceOptions).catch(() => null);
|
|
32
|
+
const runningVersion = desktopBridgeRuntimeVersion(before?.state);
|
|
33
|
+
if (before?.ok && before.running && before.loaded && runningVersion === version) {
|
|
46
34
|
return skip('desktop_bridge_restage_already_current');
|
|
47
|
-
const uid = options.uid === undefined
|
|
48
|
-
? (typeof process.getuid === 'function' ? process.getuid() : null)
|
|
49
|
-
: options.uid;
|
|
50
|
-
if (uid === null)
|
|
51
|
-
return skip('desktop_bridge_restage_no_uid');
|
|
52
|
-
const kick = await (options.run || runProcess)('/bin/launchctl', ['kickstart', '-k', `gui/${uid}/com.sneakoscope.desktop-bridge`], { timeoutMs: 10_000, maxOutputBytes: 16 * 1024 }).catch(() => null);
|
|
53
|
-
if (!kick || kick.code !== 0) {
|
|
54
|
-
return warn([`desktop_bridge_restage_kickstart_failed:${runningVersion || 'pre-8.6.2'}`]);
|
|
55
35
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
36
|
+
const repair = await (options.executeCommand || executeDesktopBridgeCommandV3)({ operation: 'repair' }, serviceOptions).catch(() => null);
|
|
37
|
+
const after = await statusImpl(serviceOptions).catch(() => null);
|
|
38
|
+
const repairedVersion = desktopBridgeRuntimeVersion(after?.state);
|
|
39
|
+
const command = repair?.schema === 'sks.desktop-bridge-command-result.v1' ? repair : null;
|
|
40
|
+
const commandBlockers = command?.execution.blockers || [];
|
|
41
|
+
if (command?.ok && command.execution.ok && commandBlockers.length === 0
|
|
42
|
+
&& after?.ok && after.running && after.loaded && repairedVersion === version) {
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
status: 'ok',
|
|
46
|
+
actions: [before?.running
|
|
47
|
+
? `desktop_bridge_restarted:${runningVersion || 'pre-8.6.2'}:${version}`
|
|
48
|
+
: `desktop_bridge_bootstrapped:${version}`],
|
|
49
|
+
blockers: [],
|
|
50
|
+
warnings: []
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const incomplete = [...new Set([
|
|
54
|
+
...commandBlockers,
|
|
55
|
+
...(after?.blockers || []),
|
|
56
|
+
...(!command?.ok || !command.execution.ok ? ['desktop_bridge_repair_failed'] : []),
|
|
57
|
+
...(!after?.running || !after.loaded ? ['desktop_bridge_service_not_running'] : []),
|
|
58
|
+
...(repairedVersion !== version
|
|
59
|
+
? [`desktop_bridge_runtime_version_unverified:${repairedVersion || 'unknown'}:${version}`]
|
|
60
|
+
: [])
|
|
61
|
+
])];
|
|
62
|
+
return warn([
|
|
63
|
+
...incomplete.map((blocker) => `desktop_bridge_restage_incomplete:${blocker}`),
|
|
64
|
+
'Desktop Bridge update repair incomplete: run `sks bridge repair` from your home directory'
|
|
65
|
+
]);
|
|
61
66
|
}
|
|
62
67
|
export function runDesktopBridgeRestageStage() {
|
|
63
68
|
return desktopBridgeRestage();
|
|
64
69
|
}
|
|
65
|
-
function processAlive(pid) {
|
|
66
|
-
try {
|
|
67
|
-
process.kill(pid, 0);
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
}
|
package/dist/core/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '10.1.
|
|
1
|
+
export const PACKAGE_VERSION = '10.1.3';
|
|
@@ -26,7 +26,7 @@ struct AuthPriorityState: Equatable {
|
|
|
26
26
|
switch error {
|
|
27
27
|
case "codex_lb_provider_disabled": return "On, unavailable · enable Codex-LB below"
|
|
28
28
|
case "codex_lb_credential_missing": return "On, unavailable · connect your Codex-LB account below"
|
|
29
|
-
case "desktop_bridge_not_running": return "On, unavailable · open Bridge diagnostics and repair the bridge service"
|
|
29
|
+
case "desktop_bridge_not_running", "desktop_bridge_runtime_not_ready": return "On, unavailable · open Bridge diagnostics and repair the bridge service"
|
|
30
30
|
case "codex_lb_route_not_ready", "codex_lb_eligible_route_missing": return "On, unavailable · validate Codex-LB and refresh its model catalog"
|
|
31
31
|
default: return "On, unavailable · check your Codex-LB connection below"
|
|
32
32
|
}
|
|
@@ -19,6 +19,11 @@ final class SettingsViewController: NSViewController, ControlCenterPage {
|
|
|
19
19
|
private var contextEnabled: Bool?
|
|
20
20
|
private var contextBusy = false
|
|
21
21
|
private var contextGeneration = 0
|
|
22
|
+
private let memoryToggle = NSSwitch()
|
|
23
|
+
private let memoryStatus = NativeView.detail("Checking saved preference…")
|
|
24
|
+
private var memoryEnabled: Bool?
|
|
25
|
+
private var memoryBusy = false
|
|
26
|
+
private var memoryGeneration = 0
|
|
22
27
|
init(processClient: ProcessClient, operations: OperationCoordinator, notifications: NotificationCoordinator) {
|
|
23
28
|
self.processClient = processClient
|
|
24
29
|
self.operations = operations
|
|
@@ -28,6 +33,17 @@ final class SettingsViewController: NSViewController, ControlCenterPage {
|
|
|
28
33
|
required init?(coder: NSCoder) { nil }
|
|
29
34
|
|
|
30
35
|
override func loadView() {
|
|
36
|
+
memoryToggle.target = self
|
|
37
|
+
memoryToggle.action = #selector(toggleContextManagement)
|
|
38
|
+
memoryToggle.isEnabled = false
|
|
39
|
+
memoryToggle.setAccessibilityLabel("Experimental context management")
|
|
40
|
+
memoryToggle.setAccessibilityIdentifier("sks-context-management-toggle")
|
|
41
|
+
memoryStatus.setAccessibilityIdentifier("sks-context-management-status")
|
|
42
|
+
let memoryCard = NativeView.card(
|
|
43
|
+
title: "Astra context management",
|
|
44
|
+
subtitle: "Experimental · Keep notes and retrieve earlier messages and tool results. Enabled by default in SKS. Applies to new tasks with supported Codex and eligible ChatGPT sign-in; API-key and custom-provider sessions may not activate it.",
|
|
45
|
+
views: [NativeView.row([memoryToggle, NativeView.detail("Enable experimental context management")]), memoryStatus]
|
|
46
|
+
)
|
|
31
47
|
followCodexLifecycle.target = self; followCodexLifecycle.action = #selector(save)
|
|
32
48
|
followCodexLifecycle.setAccessibilityLabel("Show SKS Menu only while Codex is running")
|
|
33
49
|
notificationButton = NativeView.button("Enable Notifications", target: self, action: #selector(enableNotifications))
|
|
@@ -50,11 +66,12 @@ final class SettingsViewController: NSViewController, ControlCenterPage {
|
|
|
50
66
|
)
|
|
51
67
|
view = NativeView.page([
|
|
52
68
|
ControlKit.header("Settings", "Choose how SKS works on this Mac."),
|
|
53
|
-
lifecycleCard, notificationsCard, NativeDisclosure("Advanced", views: [contextCard])
|
|
69
|
+
memoryCard, lifecycleCard, notificationsCard, NativeDisclosure("Advanced", views: [contextCard])
|
|
54
70
|
])
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
func refreshOnAppear() {
|
|
74
|
+
refreshContextManagement()
|
|
58
75
|
refreshContext1m()
|
|
59
76
|
let configResult = readConfig()
|
|
60
77
|
switch configResult {
|
|
@@ -87,6 +104,53 @@ final class SettingsViewController: NSViewController, ControlCenterPage {
|
|
|
87
104
|
}
|
|
88
105
|
}
|
|
89
106
|
|
|
107
|
+
private func refreshContextManagement() {
|
|
108
|
+
guard !memoryBusy else { return }
|
|
109
|
+
memoryGeneration += 1
|
|
110
|
+
let generation = memoryGeneration
|
|
111
|
+
processClient.run(["codex-app", "context-management", "status", "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
|
|
112
|
+
guard let self = self, !self.memoryBusy, generation == self.memoryGeneration else { return }
|
|
113
|
+
guard result.code == 0, let payload = self.json(result.output),
|
|
114
|
+
payload["schema"] as? String == "sks.context-management.v1",
|
|
115
|
+
payload["ok"] as? Bool == true, let enabled = payload["enabled"] as? Bool else {
|
|
116
|
+
self.memoryEnabled = nil
|
|
117
|
+
self.memoryToggle.isEnabled = false
|
|
118
|
+
self.memoryStatus.stringValue = "Saved setting unavailable. Check Codex configuration, then reopen Settings."
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
self.memoryEnabled = enabled
|
|
122
|
+
self.memoryToggle.state = enabled ? .on : .off
|
|
123
|
+
self.memoryToggle.isEnabled = true
|
|
124
|
+
self.memoryStatus.stringValue = enabled
|
|
125
|
+
? "Setting enabled · start a new task to apply. Availability depends on Codex and your sign-in."
|
|
126
|
+
: "Setting disabled · your choice is preserved during updates."
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@objc private func toggleContextManagement() {
|
|
131
|
+
guard !memoryBusy, let previous = memoryEnabled else { return }
|
|
132
|
+
let desired = memoryToggle.state == .on
|
|
133
|
+
guard let operation = operations.begin(kind: "context-management", mutationGroup: "codex-config", summary: "Change experimental context management") else {
|
|
134
|
+
memoryToggle.state = previous ? .on : .off
|
|
135
|
+
memoryStatus.stringValue = "Another configuration change is running. Try again when it finishes."
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
memoryBusy = true
|
|
139
|
+
memoryGeneration += 1
|
|
140
|
+
memoryToggle.isEnabled = false
|
|
141
|
+
memoryStatus.stringValue = "Saving preference…"
|
|
142
|
+
processClient.run(["codex-app", "context-management", desired ? "on" : "off", "--json"], timeout: NativeView.mutationTimeout) { [weak self] result in
|
|
143
|
+
guard let self = self else { return }
|
|
144
|
+
self.memoryBusy = false
|
|
145
|
+
let payload = self.json(result.output)
|
|
146
|
+
let ok = result.code == 0 && payload?["schema"] as? String == "sks.context-management.v1"
|
|
147
|
+
&& payload?["ok"] as? Bool == true && payload?["enabled"] as? Bool == desired
|
|
148
|
+
_ = self.operations.update(operation, state: ok ? .succeeded : .failed, stage: "complete", progress: 1,
|
|
149
|
+
summary: ok ? "Preference saved. Start a new Codex task to apply." : "Save could not be confirmed. Rechecking the setting.")
|
|
150
|
+
self.refreshContextManagement()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
90
154
|
private func refreshContext1m(preserveStatusText: Bool = false) {
|
|
91
155
|
contextGeneration += 1
|
|
92
156
|
let requestGeneration = contextGeneration
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "10.1.
|
|
4
|
+
"version": "10.1.3",
|
|
5
5
|
"description": "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.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|
|
@@ -196,7 +196,7 @@
|
|
|
196
196
|
"dependencies": {
|
|
197
197
|
"@modelcontextprotocol/client": "2.0.0",
|
|
198
198
|
"@modelcontextprotocol/server": "2.0.0",
|
|
199
|
-
"@openai/codex-sdk": "0.
|
|
199
|
+
"@openai/codex-sdk": "0.153.4",
|
|
200
200
|
"smol-toml": "^1.7.0",
|
|
201
201
|
"typescript": "^5.9.3",
|
|
202
202
|
"ws": "^8.21.3"
|