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
|
@@ -2,13 +2,26 @@ import fsp from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { runProcess } from '../../fsx.js';
|
|
4
4
|
const RETIRED_LAUNCHD_LABEL = 'com.sneakoscope.codex-lb-desktop-bridge';
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
5
|
+
const RETIRED_SETTINGS_V1_SCHEMA = 'sks.codex-lb-desktop-bridge-settings.v1';
|
|
6
|
+
const RETIRED_SETTINGS_V2_SCHEMA = 'sks.codex-lb-desktop-bridge-settings.v2';
|
|
7
|
+
const RETIRED_STATE_SCHEMAS = new Set([
|
|
8
|
+
'sks.codex-lb-desktop-bridge.v1',
|
|
9
|
+
'sks.codex-lb-desktop-bridge.v2',
|
|
10
|
+
]);
|
|
11
|
+
const SETTINGS_V1_KEYS = new Set([
|
|
12
|
+
'schema', 'listen_host', 'listen_port', 'provider_mode', 'allowed_models',
|
|
13
|
+
'gateway_auth_transport', 'allowed_origins', 'connect_timeout_ms',
|
|
14
|
+
'idle_timeout_ms', 'catalog_version', 'registered_child_models',
|
|
15
|
+
'session_pins', 'require_session_pin',
|
|
16
|
+
]);
|
|
17
|
+
const SETTINGS_V2_KEYS = new Set([
|
|
8
18
|
'schema', 'listen_host', 'listen_port', 'provider_registry', 'route_policy',
|
|
9
19
|
'provider_session_pins', 'client_capability_sha256', 'allowed_origins',
|
|
10
20
|
'connect_timeout_ms', 'idle_timeout_ms',
|
|
11
21
|
]);
|
|
22
|
+
const TRANSFERABLE_V1_KEYS = [
|
|
23
|
+
'listen_host', 'listen_port', 'allowed_origins', 'connect_timeout_ms', 'idle_timeout_ms',
|
|
24
|
+
];
|
|
12
25
|
function retiredPaths(home) {
|
|
13
26
|
const resolvedHome = path.resolve(home);
|
|
14
27
|
const runtime = path.join(resolvedHome, '.codex', 'sks');
|
|
@@ -51,14 +64,37 @@ async function readRetiredSettings(file) {
|
|
|
51
64
|
throw new Error('desktop_bridge_retired_settings_invalid');
|
|
52
65
|
}
|
|
53
66
|
const row = value;
|
|
54
|
-
|
|
67
|
+
const allowedKeys = row.schema === RETIRED_SETTINGS_V1_SCHEMA
|
|
68
|
+
? SETTINGS_V1_KEYS
|
|
69
|
+
: row.schema === RETIRED_SETTINGS_V2_SCHEMA
|
|
70
|
+
? SETTINGS_V2_KEYS
|
|
71
|
+
: null;
|
|
72
|
+
if (!allowedKeys || Object.keys(row).some((key) => !allowedKeys.has(key))) {
|
|
55
73
|
throw new Error('desktop_bridge_retired_settings_invalid');
|
|
56
74
|
}
|
|
57
|
-
if (/"(?:api_?key|secret|authorization|cookie|access_token|refresh_token)"\s*:/i.test(JSON.stringify(row))) {
|
|
75
|
+
if (/"(?:api_?key|secret|authorization|cookie|access_token|refresh_token|gatewayKey)"\s*:/i.test(JSON.stringify(row))) {
|
|
58
76
|
throw new Error('desktop_bridge_retired_settings_secret_forbidden');
|
|
59
77
|
}
|
|
60
78
|
const { schema: _schema, ...settings } = row;
|
|
61
|
-
|
|
79
|
+
if (row.schema === RETIRED_SETTINGS_V2_SCHEMA)
|
|
80
|
+
return settings;
|
|
81
|
+
validateTransferableSettings(settings);
|
|
82
|
+
return Object.fromEntries(TRANSFERABLE_V1_KEYS.map((key) => [key, settings[key]]));
|
|
83
|
+
}
|
|
84
|
+
function validateTransferableSettings(settings) {
|
|
85
|
+
const host = settings.listen_host;
|
|
86
|
+
const port = Number(settings.listen_port);
|
|
87
|
+
const origins = settings.allowed_origins;
|
|
88
|
+
const connectTimeout = Number(settings.connect_timeout_ms);
|
|
89
|
+
const idleTimeout = Number(settings.idle_timeout_ms);
|
|
90
|
+
if ((host !== '127.0.0.1' && host !== '::1')
|
|
91
|
+
|| !Number.isInteger(port) || port < 49_152 || port > 65_535
|
|
92
|
+
|| !Array.isArray(origins) || origins.length === 0
|
|
93
|
+
|| origins.some((origin) => typeof origin !== 'string' || !origin.trim())
|
|
94
|
+
|| !Number.isFinite(connectTimeout) || connectTimeout < 100 || connectTimeout > 120_000
|
|
95
|
+
|| !Number.isFinite(idleTimeout) || idleTimeout < 1_000 || idleTimeout > 86_400_000) {
|
|
96
|
+
throw new Error('desktop_bridge_retired_settings_invalid');
|
|
97
|
+
}
|
|
62
98
|
}
|
|
63
99
|
function launchDomain(uid = typeof process.getuid === 'function' ? process.getuid() : 0) {
|
|
64
100
|
return `gui/${uid}`;
|
|
@@ -91,13 +127,15 @@ export async function cleanupRetiredDesktopBridgeRuntime(preparation) {
|
|
|
91
127
|
return;
|
|
92
128
|
if (await regularFileExists(preparation.paths.settings)) {
|
|
93
129
|
const raw = JSON.parse(await fsp.readFile(preparation.paths.settings, 'utf8'));
|
|
94
|
-
if (raw.schema !==
|
|
130
|
+
if (raw.schema !== RETIRED_SETTINGS_V1_SCHEMA && raw.schema !== RETIRED_SETTINGS_V2_SCHEMA) {
|
|
95
131
|
throw new Error('desktop_bridge_retired_settings_changed');
|
|
132
|
+
}
|
|
96
133
|
}
|
|
97
134
|
if (await regularFileExists(preparation.paths.state)) {
|
|
98
135
|
const raw = JSON.parse(await fsp.readFile(preparation.paths.state, 'utf8'));
|
|
99
|
-
if (raw.schema
|
|
136
|
+
if (!RETIRED_STATE_SCHEMAS.has(String(raw.schema || ''))) {
|
|
100
137
|
throw new Error('desktop_bridge_retired_state_changed');
|
|
138
|
+
}
|
|
101
139
|
}
|
|
102
140
|
for (const file of Object.values(preparation.paths)) {
|
|
103
141
|
if (await regularFileExists(file))
|
|
@@ -27,7 +27,8 @@ export async function ensureDesktopBridge(options, operation) {
|
|
|
27
27
|
if (service.running)
|
|
28
28
|
report = await verifyDesktopBridgeV3('shallow', options);
|
|
29
29
|
const status = await desktopBridgeStatusV3(options);
|
|
30
|
-
|
|
30
|
+
const outcome = desktopBridgeServiceCommandOutcome(service);
|
|
31
|
+
return commandResult(operation, outcome.ok, status, { service, catalog_sync: sync, capabilities: report }, outcome.blockers, options);
|
|
31
32
|
}
|
|
32
33
|
export async function repairDesktopBridge(options) {
|
|
33
34
|
let core = await loadCore(options);
|
|
@@ -43,7 +44,16 @@ export async function repairDesktopBridge(options) {
|
|
|
43
44
|
core = await loadCore(options);
|
|
44
45
|
const report = service.running ? await verifyDesktopBridgeV3('shallow', options) : null;
|
|
45
46
|
const status = await desktopBridgeStatusV3(options);
|
|
46
|
-
|
|
47
|
+
const outcome = desktopBridgeServiceCommandOutcome(service);
|
|
48
|
+
return commandResult('repair', outcome.ok, status, { service, capabilities: report }, outcome.blockers, options);
|
|
49
|
+
}
|
|
50
|
+
export function desktopBridgeServiceCommandOutcome(service) {
|
|
51
|
+
const blockers = [...new Set(stringArray(service.blockers))];
|
|
52
|
+
const ok = service.ok && service.running && blockers.length === 0;
|
|
53
|
+
return {
|
|
54
|
+
ok,
|
|
55
|
+
blockers: ok ? [] : blockers.length > 0 ? blockers : ['desktop_bridge_service_not_running']
|
|
56
|
+
};
|
|
47
57
|
}
|
|
48
58
|
export async function setDefaultProvider(providerId, options) {
|
|
49
59
|
const core = await loadCore(options);
|
|
@@ -6,6 +6,39 @@ import { runAuxiliarySurfacesProbeV3 } from '../probes/auxiliary-surfaces-probe.
|
|
|
6
6
|
import { capabilityProbeResultV3 } from '../probes/probe-evidence.js';
|
|
7
7
|
import { validateCapabilityDeepEvidenceV2 } from '../trusted-deep-evidence.js';
|
|
8
8
|
import { bridgeClientUrl, providerCode, safeCode, timeoutMs, unique } from './shared.js';
|
|
9
|
+
export function textResponsePayloadValid(contentType, text) {
|
|
10
|
+
if (/text\/event-stream/i.test(contentType || '')) {
|
|
11
|
+
let completed = false;
|
|
12
|
+
for (const line of text.split('\n')) {
|
|
13
|
+
if (!line.startsWith('data:'))
|
|
14
|
+
continue;
|
|
15
|
+
const data = line.slice(5).trim();
|
|
16
|
+
if (!data || data === '[DONE]')
|
|
17
|
+
continue;
|
|
18
|
+
let event;
|
|
19
|
+
try {
|
|
20
|
+
event = JSON.parse(data);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const type = event && typeof event === 'object' ? event.type : null;
|
|
26
|
+
if (type === 'response.failed' || type === 'error')
|
|
27
|
+
return false;
|
|
28
|
+
if (type === 'response.completed')
|
|
29
|
+
completed = true;
|
|
30
|
+
}
|
|
31
|
+
return completed;
|
|
32
|
+
}
|
|
33
|
+
let payload = null;
|
|
34
|
+
try {
|
|
35
|
+
payload = text ? JSON.parse(text) : null;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
payload = null;
|
|
39
|
+
}
|
|
40
|
+
return payload !== null && typeof payload === 'object';
|
|
41
|
+
}
|
|
9
42
|
export async function probeProviderText(core, providerId, loopbackOrigin, context, options) {
|
|
10
43
|
const route = core.policy
|
|
11
44
|
? Object.entries(core.policy.model_routes).find(([, target]) => target.provider_id === providerId)
|
|
@@ -69,14 +102,7 @@ export async function probeProviderText(core, providerId, loopbackOrigin, contex
|
|
|
69
102
|
const text = await response.text();
|
|
70
103
|
if (Buffer.byteLength(text) > 4 * 1024 * 1024)
|
|
71
104
|
throw new Error('provider_text_response_too_large');
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
payload = text ? JSON.parse(text) : null;
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
payload = null;
|
|
78
|
-
}
|
|
79
|
-
const valid = response.ok && payload !== null && typeof payload === 'object';
|
|
105
|
+
const valid = response.ok && textResponsePayloadValid(response.headers.get('content-type'), text);
|
|
80
106
|
const root = valid ? null : `${providerCode(providerId)}_text_response_failed`;
|
|
81
107
|
return capabilityProbeResultV3({
|
|
82
108
|
...context,
|
|
@@ -44,9 +44,11 @@ export function combinedRoutePolicyProbe(core, context) {
|
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
|
-
export function combinedModelRouteProbe(core, status, context) {
|
|
47
|
+
export function combinedModelRouteProbe(core, status, context, liveTextResults = []) {
|
|
48
48
|
const active = activeProviderIds(core);
|
|
49
49
|
const verified = active.length > 0 && active.every((providerId) => Boolean(core.policy && Object.values(core.policy.model_routes).some((route) => route.provider_id === providerId)));
|
|
50
|
+
const liveRouteProven = verified && active.every((providerId) => liveTextResults.some((result) => result.scope === `provider:${providerId}`
|
|
51
|
+
&& result.capability === 'text_responses' && result.state === 'verified'));
|
|
50
52
|
return capabilityProbeResultV3({
|
|
51
53
|
...context,
|
|
52
54
|
capability: 'model_route',
|
|
@@ -58,10 +60,11 @@ export function combinedModelRouteProbe(core, status, context) {
|
|
|
58
60
|
blockers: verified ? [] : ['catalog_model_route_missing'],
|
|
59
61
|
retryable: !verified,
|
|
60
62
|
recoveryAction: verified ? null : 'refresh_catalog_or_select_supported_model',
|
|
61
|
-
source: 'config',
|
|
63
|
+
source: liveRouteProven ? 'transport' : 'config',
|
|
62
64
|
evidence: {
|
|
63
65
|
active_provider_ids: active,
|
|
64
66
|
route_count: status.catalog_sync.route_count,
|
|
67
|
+
live_route_proven: liveRouteProven,
|
|
65
68
|
fallback: 'none'
|
|
66
69
|
}
|
|
67
70
|
});
|
|
@@ -119,11 +122,16 @@ export function providerAuthProbe(core, providerId, context) {
|
|
|
119
122
|
}
|
|
120
123
|
});
|
|
121
124
|
}
|
|
122
|
-
export function providerModelRouteProbe(core, providerId, context) {
|
|
125
|
+
export function providerModelRouteProbe(core, providerId, context, liveText = null) {
|
|
123
126
|
const route = core.policy
|
|
124
127
|
? Object.entries(core.policy.model_routes).find(([, target]) => target.provider_id === providerId)
|
|
125
128
|
: null;
|
|
126
129
|
const verified = Boolean(route && core.catalogSync.providers[providerId].state === 'verified');
|
|
130
|
+
const liveRouteProven = Boolean(verified && route && liveText
|
|
131
|
+
&& liveText.capability === 'text_responses'
|
|
132
|
+
&& liveText.scope === `provider:${providerId}`
|
|
133
|
+
&& liveText.state === 'verified'
|
|
134
|
+
&& liveText.evidence.public_model === route[0]);
|
|
127
135
|
return capabilityProbeResultV3({
|
|
128
136
|
...context,
|
|
129
137
|
capability: 'model_route',
|
|
@@ -132,12 +140,13 @@ export function providerModelRouteProbe(core, providerId, context) {
|
|
|
132
140
|
state: verified ? 'verified' : 'not_attempted',
|
|
133
141
|
retryable: !verified,
|
|
134
142
|
recoveryAction: verified ? null : 'refresh_catalog_or_select_supported_model',
|
|
135
|
-
source: 'config',
|
|
143
|
+
source: liveRouteProven ? 'transport' : 'config',
|
|
136
144
|
evidence: {
|
|
137
145
|
provider_id: providerId,
|
|
138
146
|
public_model: route?.[0] || null,
|
|
139
147
|
upstream_model: route?.[1].upstream_model || null,
|
|
140
148
|
catalog_generation: core.policy?.catalog_generation || null,
|
|
149
|
+
live_route_proven: liveRouteProven,
|
|
141
150
|
fallback: 'none'
|
|
142
151
|
}
|
|
143
152
|
});
|
|
@@ -118,7 +118,8 @@ export async function persistRuntimeSettings(core, options, behavior = {}) {
|
|
|
118
118
|
}
|
|
119
119
|
if (!restarted.ok || !restarted.running) {
|
|
120
120
|
await stopAfterFailedRestart(restartOptions, options);
|
|
121
|
-
|
|
121
|
+
const rootCause = restarted.blockers.find((blocker) => blocker === 'desktop_bridge_entry_macos_protected_folder');
|
|
122
|
+
throw new Error(rootCause || restarted.blockers[0] || 'desktop_bridge_restart_failed');
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
export async function quiesceRunningBridge(core, options) {
|
|
@@ -30,6 +30,12 @@ export async function verifyDesktopBridgeV3(requestedLevel, options = {}) {
|
|
|
30
30
|
sessionId,
|
|
31
31
|
attemptId: 1
|
|
32
32
|
};
|
|
33
|
+
const activeProviders = activeProviderIds(core);
|
|
34
|
+
const enabledProviders = ['codex-lb', 'openrouter']
|
|
35
|
+
.filter((providerId) => core.registry.profiles[providerId].enabled);
|
|
36
|
+
const textResults = requestedLevel !== 'shallow'
|
|
37
|
+
? await Promise.all(activeProviders.map((providerId) => probeProviderText(core, providerId, status.service.loopback_origin, probeContext, options)))
|
|
38
|
+
: [];
|
|
33
39
|
const results = [
|
|
34
40
|
...runBridgeProbeV3({
|
|
35
41
|
...probeContext,
|
|
@@ -40,18 +46,13 @@ export async function verifyDesktopBridgeV3(requestedLevel, options = {}) {
|
|
|
40
46
|
}),
|
|
41
47
|
nativeIdentityProbe(core, probeContext),
|
|
42
48
|
combinedRoutePolicyProbe(core, probeContext),
|
|
43
|
-
combinedModelRouteProbe(core, status, probeContext)
|
|
49
|
+
combinedModelRouteProbe(core, status, probeContext, textResults)
|
|
44
50
|
];
|
|
45
|
-
const activeProviders = activeProviderIds(core);
|
|
46
|
-
const enabledProviders = ['codex-lb', 'openrouter']
|
|
47
|
-
.filter((providerId) => core.registry.profiles[providerId].enabled);
|
|
48
51
|
for (const providerId of ['codex-lb', 'openrouter']) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (requestedLevel !== 'shallow') {
|
|
52
|
-
const textResults = await Promise.all(activeProviders.map((providerId) => probeProviderText(core, providerId, status.service.loopback_origin, probeContext, options)));
|
|
53
|
-
results.push(...textResults);
|
|
52
|
+
const liveText = textResults.find((result) => result.scope === `provider:${providerId}`) || null;
|
|
53
|
+
results.push(providerCredentialProbe(core, providerId, probeContext), providerAuthProbe(core, providerId, probeContext), providerModelRouteProbe(core, providerId, probeContext, liveText));
|
|
54
54
|
}
|
|
55
|
+
results.push(...textResults);
|
|
55
56
|
if (requestedLevel === 'deep') {
|
|
56
57
|
const deepResults = await Promise.all(activeProviders.map((providerId) => probeProviderDeep(core, providerId, probeContext, options)));
|
|
57
58
|
results.push(...deepResults.flat());
|
|
@@ -354,13 +354,16 @@ export async function installAndStartDesktopBridgeService(options = {}) {
|
|
|
354
354
|
const domain = launchDomain(options.uid);
|
|
355
355
|
await run(ctl, ['bootout', service], { timeoutMs: 5_000, maxOutputBytes: 16 * 1024 }).catch(() => undefined);
|
|
356
356
|
await removeStaleState(paths.state_path, options.processExists);
|
|
357
|
-
const bootstrap = await
|
|
357
|
+
const bootstrap = await bootstrapLaunchdWithRetry(options, domain, service, paths.launch_agent_path);
|
|
358
358
|
if (bootstrap.code !== 0 && !bootstrap.timedOut)
|
|
359
359
|
return failedStatus(paths, settings, service, 'missing', 'desktop_bridge_launchd_bootstrap_failed');
|
|
360
360
|
await run(ctl, ['kickstart', '-k', service], { timeoutMs: 10_000, maxOutputBytes: 32 * 1024 }).catch(() => undefined);
|
|
361
361
|
const status = await waitForBridge({ ...options, home });
|
|
362
362
|
if (!status.ok) {
|
|
363
363
|
await run(ctl, ['bootout', service], { timeoutMs: 5_000, maxOutputBytes: 16 * 1024 }).catch(() => undefined);
|
|
364
|
+
if (await launchTargetsProtectedFolder([command.executable, ...command.arguments], home)) {
|
|
365
|
+
return withProtectedFolderBlocker(status);
|
|
366
|
+
}
|
|
364
367
|
return status;
|
|
365
368
|
}
|
|
366
369
|
try {
|
|
@@ -382,10 +385,14 @@ export async function bootstrapExistingDesktopBridgeService(options = {}) {
|
|
|
382
385
|
const ctl = options.launchctl || '/bin/launchctl';
|
|
383
386
|
const service = launchService(options.uid);
|
|
384
387
|
await run(ctl, ['bootout', service], { timeoutMs: 5_000, maxOutputBytes: 16 * 1024 }).catch(() => undefined);
|
|
385
|
-
const result = await
|
|
388
|
+
const result = await bootstrapLaunchdWithRetry(options, launchDomain(options.uid), service, paths.launch_agent_path);
|
|
386
389
|
if (result.code === 0 || result.timedOut)
|
|
387
390
|
await run(ctl, ['kickstart', '-k', service], { timeoutMs: 10_000, maxOutputBytes: 32 * 1024 }).catch(() => undefined);
|
|
388
|
-
|
|
391
|
+
const status = await waitForBridge({ ...options, home });
|
|
392
|
+
if (!status.ok && await launchAgentTargetsProtectedFolder(paths.launch_agent_path, home)) {
|
|
393
|
+
return withProtectedFolderBlocker(status);
|
|
394
|
+
}
|
|
395
|
+
return status;
|
|
389
396
|
}
|
|
390
397
|
export async function stopDesktopBridgeService(options = {}) {
|
|
391
398
|
const home = options.home || options.env?.HOME || process.env.HOME || os.homedir();
|
|
@@ -569,10 +576,56 @@ function launchService(uid) { return `${launchDomain(uid)}/${DESKTOP_BRIDGE_LAUN
|
|
|
569
576
|
async function resolveLaunchCommand(options) { if (options.executablePath)
|
|
570
577
|
return await exists(path.resolve(options.executablePath)) ? { executable: path.resolve(options.executablePath), arguments: [...(options.executableArguments || [])] } : null; const entry = String(process.argv[1] || ''); if (entry && ['sks', 'sneakoscope'].includes(path.basename(entry).replace(/\.js$/i, '')) && await exists(entry))
|
|
571
578
|
return { executable: path.resolve(process.execPath), arguments: [path.resolve(entry)] }; const sks = await which('sks').catch(() => null); return sks ? { executable: path.resolve(sks), arguments: [] } : null; }
|
|
579
|
+
function macosProtectedUserPath(target, home) {
|
|
580
|
+
if (!target)
|
|
581
|
+
return false;
|
|
582
|
+
return ['Desktop', 'Documents', 'Downloads']
|
|
583
|
+
.some((dir) => target.startsWith(`${path.resolve(home, dir)}${path.sep}`));
|
|
584
|
+
}
|
|
585
|
+
async function launchTargetsProtectedFolder(targets, home) {
|
|
586
|
+
const resolved = await Promise.all(targets.map(async (target) => {
|
|
587
|
+
try {
|
|
588
|
+
return await fsp.realpath(target);
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
return target;
|
|
592
|
+
}
|
|
593
|
+
}));
|
|
594
|
+
return resolved.some((target) => macosProtectedUserPath(target, home));
|
|
595
|
+
}
|
|
596
|
+
async function launchAgentTargetsProtectedFolder(plistPath, home) {
|
|
597
|
+
let text = '';
|
|
598
|
+
try {
|
|
599
|
+
text = await fsp.readFile(plistPath, 'utf8');
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
const targets = [...text.matchAll(/<string>([^<]+)<\/string>/g)].map((match) => match[1]);
|
|
605
|
+
return launchTargetsProtectedFolder(targets, home);
|
|
606
|
+
}
|
|
607
|
+
function withProtectedFolderBlocker(status) {
|
|
608
|
+
return { ...status, blockers: [...new Set([...status.blockers, 'desktop_bridge_entry_macos_protected_folder'])] };
|
|
609
|
+
}
|
|
610
|
+
export async function bootstrapLaunchdWithRetry(options, domain, service, plistPath) {
|
|
611
|
+
const run = options.run || runProcess;
|
|
612
|
+
const ctl = options.launchctl || '/bin/launchctl';
|
|
613
|
+
let result = { code: 1, timedOut: false };
|
|
614
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
615
|
+
if (attempt > 0)
|
|
616
|
+
await run(ctl, ['bootout', service], { timeoutMs: 5_000, maxOutputBytes: 16 * 1024 }).catch(() => undefined);
|
|
617
|
+
for (let poll = 0; poll < 20 && (await inspectLaunchd(options, service)).loaded; poll += 1)
|
|
618
|
+
await delay(100);
|
|
619
|
+
result = await run(ctl, ['bootstrap', domain, plistPath], { timeoutMs: 10_000, maxOutputBytes: 32 * 1024 }).catch((error) => failedProcess(error));
|
|
620
|
+
if (result.code === 0 || result.timedOut)
|
|
621
|
+
return result;
|
|
622
|
+
}
|
|
623
|
+
return result;
|
|
624
|
+
}
|
|
572
625
|
async function inspectLaunchd(options, service) { if ((options.platform || process.platform) !== 'darwin')
|
|
573
626
|
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)
|
|
574
627
|
return { loaded: false, running: false }; const text = `${result.stdout}\n${result.stderr}`; return { loaded: true, running: /state = running/.test(text) && /pid = \d+/.test(text) }; }
|
|
575
|
-
async function waitForBridge(options) { let status = await desktopBridgeServiceStatus(options); for (let i = 0; i <
|
|
628
|
+
async function waitForBridge(options) { let status = await desktopBridgeServiceStatus(options); for (let i = 0; i < 150 && !status.ok; i += 1) {
|
|
576
629
|
await delay(100);
|
|
577
630
|
status = await desktopBridgeServiceStatus(options);
|
|
578
631
|
} return status; }
|
|
@@ -35,6 +35,7 @@ export function buildBridgeRouteIndex(input) {
|
|
|
35
35
|
const model = {
|
|
36
36
|
...source,
|
|
37
37
|
public_id: publicId,
|
|
38
|
+
slug: source.slug || publicId,
|
|
38
39
|
upstream_model: upstream,
|
|
39
40
|
capabilities: unique(source.capabilities).sort(),
|
|
40
41
|
route_key: providerRouteKey(source.provider_id, publicId)
|
|
@@ -192,6 +192,7 @@ Verify with the narrowest compile, deterministic template, or live native check
|
|
|
192
192
|
instructions: `You are the scoped Computer Use operator.
|
|
193
193
|
|
|
194
194
|
Use Codex Computer Use only for the explicit native macOS, desktop-app, OS-settings, or non-web visual slice assigned by the parent.
|
|
195
|
+
Do not target the hosting Codex Desktop app (com.openai.codex). For Codex-linked checks, observe Codex through structured host/process evidence and operate only the external native target.
|
|
195
196
|
Do not replace judgment, debugging, planning, or security review; return captured evidence to the appropriate Sol Max specialist.
|
|
196
197
|
Honor the parent permission scope, avoid destructive or irreversible UI actions, do not edit source files, and report exactly what was observed or changed.`
|
|
197
198
|
}),
|
|
@@ -25,7 +25,7 @@ export function canonicalTestProofPath(root) {
|
|
|
25
25
|
}
|
|
26
26
|
export function canonicalTestFiles(root) {
|
|
27
27
|
const compiled = discover(path.join(root, 'dist'), (file) => {
|
|
28
|
-
if (!
|
|
28
|
+
if (!isCurrentCompiledTest(root, file))
|
|
29
29
|
return false;
|
|
30
30
|
const relative = repoRelative(root, file);
|
|
31
31
|
return relative.startsWith('dist/core/release/__tests__/') || CURRENT_COMPILED_TESTS.has(relative);
|
|
@@ -43,7 +43,7 @@ export function allCanonicalTestFiles(root) {
|
|
|
43
43
|
unit.push(...discover(path.join(root, 'test', 'regression'), (file) => file.endsWith('.test.mjs')));
|
|
44
44
|
unit.sort();
|
|
45
45
|
return {
|
|
46
|
-
compiled: discover(path.join(root, 'dist'), (file) =>
|
|
46
|
+
compiled: discover(path.join(root, 'dist'), (file) => isCurrentCompiledTest(root, file)),
|
|
47
47
|
unit
|
|
48
48
|
};
|
|
49
49
|
}
|
|
@@ -150,6 +150,12 @@ function validCorpus(value) {
|
|
|
150
150
|
function readPackage(root) {
|
|
151
151
|
return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
152
152
|
}
|
|
153
|
+
function isCurrentCompiledTest(root, file) {
|
|
154
|
+
if (!file.endsWith('.test.js') || !file.includes(`${path.sep}__tests__${path.sep}`))
|
|
155
|
+
return false;
|
|
156
|
+
const relative = path.relative(path.join(root, 'dist'), file);
|
|
157
|
+
return fs.existsSync(path.join(root, 'src', `${relative.slice(0, -'.js'.length)}.ts`));
|
|
158
|
+
}
|
|
153
159
|
function discover(dir, accept) {
|
|
154
160
|
const out = [];
|
|
155
161
|
if (!fs.existsSync(dir))
|
|
@@ -22,7 +22,7 @@ export const QA_INTERACTION_SURFACES = Object.freeze([
|
|
|
22
22
|
]);
|
|
23
23
|
export const CODEX_QA_SURFACE_ROUTING_POLICY = `Codex QA surface routing follows the official Codex App split: use @Browser / in-app Browser (${CODEX_IN_APP_BROWSER_DOC_URL}) first for localhost, local development servers, file-backed previews, and public pages that do not require sign-in; use @Chrome / Codex Chrome Extension (${CODEX_CHROME_EXTENSION_DOC_URL}) for signed-in websites, cookies, browser profiles, extensions, existing tabs, or internal tools; use @Computer or @AppName (${CODEX_COMPUTER_USE_DOC_URL}) for native macOS/Windows apps, OS settings, cross-app workflows, and GUI-only bugs. Prefer structured Plugins/MCPs for repeatable data operations, then verify rendered user-visible results with Browser, Chrome, or Computer Use. Playwright, Selenium, Puppeteer, Chrome MCP, static screenshots, plugin cache, and final-agent prose are not Codex App live action proof. App Server evidence (${CODEX_APP_SERVER_DOC_URL}) must correlate thread, turn, item/tool events, approvals, diffs, actions, observations, findings, fixes, and same-flow replay before a real QA pass is claimed.`;
|
|
24
24
|
export const CODEX_WEB_VERIFICATION_POLICY = CODEX_QA_SURFACE_ROUTING_POLICY;
|
|
25
|
-
export const CODEX_COMPUTER_USE_ONLY_POLICY = `Codex Computer Use is a live GUI surface for supported macOS and Windows environments, invoked with @Computer or @AppName for native apps, OS settings, browser contexts that truly require GUI-level operation, and cross-app workflows. Do not replace @Browser localhost/public-page checks or @Chrome signed-in checks with Computer Use unless the surface router records a specific GUI-only/cross-app reason. If live Computer Use tools, permissions, or app access are unavailable, mark the affected native/GUI evidence blocked or unverified instead of fabricating screenshots or actions. Codex App readiness/config checks are capability evidence only, not target interaction proof.`;
|
|
25
|
+
export const CODEX_COMPUTER_USE_ONLY_POLICY = `Codex Computer Use is a live GUI surface for supported macOS and Windows environments, invoked with @Computer or @AppName for native apps, OS settings, browser contexts that truly require GUI-level operation, and cross-app workflows. Computer Use must never target the hosting Codex Desktop app itself (com.openai.codex), because host self-control is outside the allowed safety boundary. For Codex-linked native QA, use structured App Server, process, or NSWorkspace evidence for the Codex side and direct Computer Use only at the external native target. Do not replace @Browser localhost/public-page checks or @Chrome signed-in checks with Computer Use unless the surface router records a specific GUI-only/cross-app reason. If live Computer Use tools, permissions, or app access are unavailable, mark the affected native/GUI evidence blocked or unverified instead of fabricating screenshots or actions. Codex App readiness/config checks are capability evidence only, not target interaction proof.`;
|
|
26
26
|
export const IMAGEGEN_SOCIAL_SOURCE_POLICY = 'Use public X/social/community reports only as prompt-quality and workflow-sentiment hints after official OpenAI/Codex docs. Social posts are not capability specs, evidence of tool availability, or proof that a generated asset was created.';
|
|
27
27
|
export const CODEX_IMAGEGEN_REQUIRED_POLICY = 'Pipeline image generation, raster asset creation/editing, and generated image-review evidence must use gpt-image-2 through the user-selected Codex provider when that evidence is required for full verification: Codex App imagegen/$imagegen, or the selected and ready codex-lb Responses provider. Only completed response output or a final response.output_item.done image result qualifies; streamed partial preview frames never count as generated-image evidence. For newest-model image requests, prompt explicitly for "ChatGPT Images 2.0 / GPT Image 2.0 with gpt-image-2" instead of relying on generic image-generation wording. Do not substitute placeholder SVG/HTML/CSS, prose-only critique, stock-like stand-ins, manually fabricated files, or missing-output ledgers for requested/generated raster assets or required generated review images. If imagegen/gpt-image-2 is unavailable or generated annotated images cannot be created/linked, record the blocker and cap any closeout at verified_partial/reference-only instead of claiming generated-image evidence or full route verification; that partial closeout requires source screenshots plus hashes, docs evidence, source Image Voxel anchors, and Honest Mode evidence. In Codex App prompts, invoke $imagegen when live image generation is needed; SKS hooks and skills can require the policy but cannot attach missing host image-generation tools to an already-started turn. Official OpenAI/Codex docs are authoritative for capabilities, surfaces, limits, and evidence rules; X/social/community reports may inform prompt style only.';
|
|
28
28
|
export const DEFAULT_CODEX_APP_PLUGINS = Object.freeze([
|
package/dist/core/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '8.3.
|
|
1
|
+
export const PACKAGE_VERSION = '8.3.2';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum CodexLifecyclePolicy {
|
|
4
|
+
static func followsCodex(from config: [String: Any]?) -> Bool {
|
|
5
|
+
if let current = config?["follow_codex_lifecycle"] as? Bool { return current }
|
|
6
|
+
return config?["quit_with_codex"] as? Bool == true
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
static func initialVisibility(followCodex: Bool, codexRunning: Bool) -> Bool {
|
|
10
|
+
!followCodex || codexRunning
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static func visibilityAfterCodexLaunch() -> Bool { true }
|
|
14
|
+
|
|
15
|
+
static func visibilityAfterCodexTermination(followCodex: Bool) -> Bool {
|
|
16
|
+
!followCodex
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -16,7 +16,9 @@ final class ProcessClient {
|
|
|
16
16
|
private let actionScript: String
|
|
17
17
|
private let logPath: String
|
|
18
18
|
private let projectRoot: String
|
|
19
|
-
|
|
19
|
+
// Bridge status/repair JSON scales with the combined catalog (395+ models
|
|
20
|
+
// ≈ 120KB today); 64KB truncated every routing-aware command result.
|
|
21
|
+
private let outputLimit = 1024 * 1024
|
|
20
22
|
private let processIdentityGuard: ProcessIdentityGuard
|
|
21
23
|
private let ownedProcessRegistry: OwnedProcessRegistry
|
|
22
24
|
|
|
@@ -247,15 +247,22 @@ struct DesktopBridgeStatusV3Truth {
|
|
|
247
247
|
private static let keys: Set<String> = [
|
|
248
248
|
"schema", "checked_at", "correlation_id", "management", "service",
|
|
249
249
|
"http_probe", "websocket_probe", "native_identity", "providers", "routing",
|
|
250
|
-
"catalog_sync", "capabilities", "readiness", "recovery_actions"
|
|
250
|
+
"catalog_sync", "capabilities", "readiness", "recovery_actions",
|
|
251
|
+
"ok", "execution_ok", "command_summary"
|
|
251
252
|
]
|
|
252
253
|
let raw: [String: Any]
|
|
253
254
|
let checkedAt: String
|
|
254
255
|
let correlationId: String
|
|
255
256
|
let capabilities: DesktopCapabilityReportV3?
|
|
256
257
|
|
|
258
|
+
// The envelope trio (ok/execution_ok/command_summary) exists on top-level
|
|
259
|
+
// `bridge status` output but NOT on the status object nested inside a
|
|
260
|
+
// command result, so it is allowed and type-checked — never required.
|
|
261
|
+
private static let envelopeKeys: Set<String> = ["ok", "execution_ok", "command_summary"]
|
|
262
|
+
|
|
257
263
|
static func decode(from json: [String: Any]) throws -> DesktopBridgeStatusV3Truth {
|
|
258
|
-
|
|
264
|
+
let required = keys.subtracting(envelopeKeys)
|
|
265
|
+
guard required.isSubset(of: Set(json.keys)), Set(json.keys).isSubset(of: keys),
|
|
259
266
|
json["schema"] as? String == "sks.desktop-bridge-status.v3",
|
|
260
267
|
let checkedAt = nonempty(json["checked_at"]), let correlationId = nonempty(json["correlation_id"]),
|
|
261
268
|
let management = json["management"] as? [String: Any],
|
|
@@ -265,7 +272,10 @@ struct DesktopBridgeStatusV3Truth {
|
|
|
265
272
|
let routing = json["routing"] as? [String: Any],
|
|
266
273
|
let catalog = json["catalog_sync"] as? [String: Any],
|
|
267
274
|
let readiness = json["readiness"] as? [String: Any],
|
|
268
|
-
json["recovery_actions"] is [String]
|
|
275
|
+
json["recovery_actions"] is [String],
|
|
276
|
+
json["ok"] == nil || json["ok"] is Bool,
|
|
277
|
+
json["execution_ok"] == nil || json["execution_ok"] is Bool,
|
|
278
|
+
json["command_summary"] == nil || nonempty(json["command_summary"]) != nil else {
|
|
269
279
|
throw ProviderFacadeError.schemaInvalid("desktop_bridge_status_schema_invalid")
|
|
270
280
|
}
|
|
271
281
|
let managed = management["managed"] as? Bool == true
|
|
@@ -356,6 +366,70 @@ struct DesktopBridgeStatusV3Truth {
|
|
|
356
366
|
}
|
|
357
367
|
}
|
|
358
368
|
|
|
369
|
+
struct DesktopBridgeCommandResultTruth: Equatable {
|
|
370
|
+
private static let keys: Set<String> = [
|
|
371
|
+
"schema", "operation", "operation_id", "correlation_id", "checked_at", "ok",
|
|
372
|
+
"execution", "readiness", "status", "result", "recovery_action",
|
|
373
|
+
"execution_ok", "command_summary"
|
|
374
|
+
]
|
|
375
|
+
let completed: Bool
|
|
376
|
+
let blockers: [String]
|
|
377
|
+
let recoveryAction: String?
|
|
378
|
+
|
|
379
|
+
static func decode(from json: [String: Any], expectedOperation: String) throws -> DesktopBridgeCommandResultTruth {
|
|
380
|
+
guard Set(json.keys) == keys,
|
|
381
|
+
json["schema"] as? String == "sks.desktop-bridge-command-result.v1",
|
|
382
|
+
json["operation"] as? String == expectedOperation,
|
|
383
|
+
nonempty(json["operation_id"]) != nil,
|
|
384
|
+
nonempty(json["correlation_id"]) != nil,
|
|
385
|
+
nonempty(json["checked_at"]) != nil,
|
|
386
|
+
let topLevelOK = json["ok"] as? Bool,
|
|
387
|
+
let execution = json["execution"] as? [String: Any],
|
|
388
|
+
Set(execution.keys) == ["ok", "status", "blockers"],
|
|
389
|
+
let executionOK = execution["ok"] as? Bool,
|
|
390
|
+
let executionStatus = execution["status"] as? String,
|
|
391
|
+
["completed", "partial", "failed"].contains(executionStatus),
|
|
392
|
+
let blockers = execution["blockers"] as? [String],
|
|
393
|
+
let readiness = json["readiness"] as? [String: Any],
|
|
394
|
+
Set(readiness.keys) == ["ready", "blockers", "warnings"],
|
|
395
|
+
readiness["ready"] is Bool,
|
|
396
|
+
readiness["blockers"] is [String],
|
|
397
|
+
readiness["warnings"] is [String],
|
|
398
|
+
(json["status"] is NSNull || json["status"] is [String: Any]),
|
|
399
|
+
let result = json["result"] as? [String: Any],
|
|
400
|
+
(json["recovery_action"] is NSNull || json["recovery_action"] is String),
|
|
401
|
+
json["execution_ok"] as? Bool == executionOK,
|
|
402
|
+
nonempty(json["command_summary"]) != nil,
|
|
403
|
+
topLevelOK == executionOK else {
|
|
404
|
+
throw ProviderFacadeError.schemaInvalid("desktop_bridge_command_result_schema_invalid")
|
|
405
|
+
}
|
|
406
|
+
let completed = topLevelOK && executionStatus == "completed" && blockers.isEmpty
|
|
407
|
+
let partial = topLevelOK && executionStatus == "partial" && !blockers.isEmpty
|
|
408
|
+
let failed = !topLevelOK && executionStatus == "failed"
|
|
409
|
+
guard completed || partial || failed else {
|
|
410
|
+
throw ProviderFacadeError.schemaInvalid("desktop_bridge_command_result_execution_invalid")
|
|
411
|
+
}
|
|
412
|
+
if completed && expectedOperation == "repair" {
|
|
413
|
+
guard let service = result["service"] as? [String: Any],
|
|
414
|
+
service["ok"] as? Bool == true,
|
|
415
|
+
service["running"] as? Bool == true else {
|
|
416
|
+
throw ProviderFacadeError.schemaInvalid("desktop_bridge_command_result_service_invalid")
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return DesktopBridgeCommandResultTruth(
|
|
420
|
+
completed: completed,
|
|
421
|
+
blockers: blockers,
|
|
422
|
+
recoveryAction: json["recovery_action"] as? String
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private static func nonempty(_ value: Any?) -> String? {
|
|
427
|
+
guard let string = value as? String else { return nil }
|
|
428
|
+
let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
429
|
+
return trimmed.isEmpty ? nil : trimmed
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
359
433
|
struct CapabilityDisplayRow: Equatable {
|
|
360
434
|
let scope: CapabilityScope
|
|
361
435
|
let capability: String
|
|
@@ -385,3 +459,20 @@ struct CapabilityDisplayRow: Equatable {
|
|
|
385
459
|
return nil
|
|
386
460
|
}
|
|
387
461
|
}
|
|
462
|
+
|
|
463
|
+
enum CapabilityDisplayFilter {
|
|
464
|
+
static func rows(_ rows: [CapabilityDisplayRow], showAll: Bool) -> [CapabilityDisplayRow] {
|
|
465
|
+
showAll ? rows : rows.filter { isIssue($0.state) }
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
static func issueCount(_ rows: [CapabilityDisplayRow]) -> Int {
|
|
469
|
+
rows.filter { isIssue($0.state) }.count
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private static func isIssue(_ state: CapabilityProbeState) -> Bool {
|
|
473
|
+
switch state {
|
|
474
|
+
case .degraded, .blocked, .failed, .stale: return true
|
|
475
|
+
case .notAttempted, .running, .verified, .unsupported: return false
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|