sneakoscope 8.3.0 → 8.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +5 -3
  2. package/config/skills-hash-ledger.v1.json +3 -0
  3. package/crates/sks-core/Cargo.lock +1 -1
  4. package/crates/sks-core/Cargo.toml +1 -1
  5. package/dist/config/skills-manifest.json +7 -4
  6. package/dist/core/codex-app/menubar/config.js +4 -4
  7. package/dist/core/codex-app/menubar/constants.js +2 -1
  8. package/dist/core/codex-app/menubar/global-install.js +16 -0
  9. package/dist/core/codex-app/menubar/installer.js +15 -2
  10. package/dist/core/codex-app/menubar/launch-agent.js +23 -9
  11. package/dist/core/codex-app/menubar/rollback.js +12 -2
  12. package/dist/core/codex-lb/combined-catalog/normalize.js +37 -3
  13. package/dist/core/codex-lb/combined-catalog/shared.js +7 -1
  14. package/dist/core/codex-lb/desktop-bridge/header-policy.js +5 -3
  15. package/dist/core/codex-lb/desktop-bridge/http-forward.js +48 -3
  16. package/dist/core/codex-lb/desktop-bridge/server.js +13 -7
  17. package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +2 -1
  18. package/dist/core/codex-lb/desktop-bridge-migration/retired-runtime-cleanup.js +46 -8
  19. package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +12 -2
  20. package/dist/core/codex-lb/desktop-controller-v3/live-probes.js +34 -8
  21. package/dist/core/codex-lb/desktop-controller-v3/preflight-probes.js +13 -4
  22. package/dist/core/codex-lb/desktop-controller-v3/shared.js +2 -1
  23. package/dist/core/codex-lb/desktop-controller-v3/verification.js +10 -9
  24. package/dist/core/codex-lb/desktop-service.js +57 -4
  25. package/dist/core/codex-lb/route-index.js +1 -0
  26. package/dist/core/managed-assets/managed-assets-manifest.js +1 -0
  27. package/dist/core/release/canonical-test-proof.js +8 -2
  28. package/dist/core/routes/evidence.js +1 -1
  29. package/dist/core/version.js +1 -1
  30. package/dist/native/sks-menubar/Sources/CodexLifecyclePolicy.swift +18 -0
  31. package/dist/native/sks-menubar/Sources/ProcessClient.swift +3 -1
  32. package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +94 -3
  33. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +49 -15
  34. package/dist/native/sks-menubar/Sources/SKSKeychainStore.swift +21 -1
  35. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +18 -17
  36. package/dist/native/sks-menubar/Sources/StatusItemController.swift +14 -14
  37. package/dist/scripts/canonical-test-runner.js +83 -3
  38. package/package.json +1 -1
@@ -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 RETIRED_SETTINGS_SCHEMA = 'sks.codex-lb-desktop-bridge-settings.v2';
6
- const RETIRED_STATE_SCHEMA = 'sks.codex-lb-desktop-bridge.v2';
7
- const SETTINGS_KEYS = new Set([
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
- if (row.schema !== RETIRED_SETTINGS_SCHEMA || Object.keys(row).some((key) => !SETTINGS_KEYS.has(key))) {
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
- return settings;
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 !== RETIRED_SETTINGS_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 !== RETIRED_STATE_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
- return commandResult(operation, true, status, { service, catalog_sync: sync, capabilities: report }, [], options);
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
- return commandResult('repair', true, status, { service, capabilities: report }, [], options);
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
- let payload = null;
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
- throw new Error(restarted.blockers[0] || 'desktop_bridge_restart_failed');
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
- results.push(providerCredentialProbe(core, providerId, probeContext), providerAuthProbe(core, providerId, probeContext), providerModelRouteProbe(core, providerId, probeContext));
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 run(ctl, ['bootstrap', domain, paths.launch_agent_path], { timeoutMs: 10_000, maxOutputBytes: 32 * 1024 }).catch((error) => failedProcess(error));
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 run(ctl, ['bootstrap', launchDomain(options.uid), paths.launch_agent_path], { timeoutMs: 10_000, maxOutputBytes: 32 * 1024 }).catch((error) => failedProcess(error));
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
- return waitForBridge({ ...options, home });
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 < 40 && !status.ok; i += 1) {
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 (!file.endsWith('.test.js') || !file.includes(`${path.sep}__tests__${path.sep}`))
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) => file.endsWith('.test.js') && file.includes(`${path.sep}__tests__${path.sep}`)),
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([
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = '8.3.0';
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
- private let outputLimit = 64 * 1024
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
- guard Set(json.keys) == keys,
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] else {
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
+ }