sneakoscope 8.3.0 → 8.3.1
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 +36 -3
- 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 +86 -2
- 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
|
@@ -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.1';
|
|
@@ -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,7 +247,8 @@ 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
|
|
@@ -265,7 +266,9 @@ struct DesktopBridgeStatusV3Truth {
|
|
|
265
266
|
let routing = json["routing"] as? [String: Any],
|
|
266
267
|
let catalog = json["catalog_sync"] as? [String: Any],
|
|
267
268
|
let readiness = json["readiness"] as? [String: Any],
|
|
268
|
-
json["recovery_actions"] is [String]
|
|
269
|
+
json["recovery_actions"] is [String],
|
|
270
|
+
json["ok"] is Bool, json["execution_ok"] is Bool,
|
|
271
|
+
nonempty(json["command_summary"]) != nil else {
|
|
269
272
|
throw ProviderFacadeError.schemaInvalid("desktop_bridge_status_schema_invalid")
|
|
270
273
|
}
|
|
271
274
|
let managed = management["managed"] as? Bool == true
|
|
@@ -356,6 +359,70 @@ struct DesktopBridgeStatusV3Truth {
|
|
|
356
359
|
}
|
|
357
360
|
}
|
|
358
361
|
|
|
362
|
+
struct DesktopBridgeCommandResultTruth: Equatable {
|
|
363
|
+
private static let keys: Set<String> = [
|
|
364
|
+
"schema", "operation", "operation_id", "correlation_id", "checked_at", "ok",
|
|
365
|
+
"execution", "readiness", "status", "result", "recovery_action",
|
|
366
|
+
"execution_ok", "command_summary"
|
|
367
|
+
]
|
|
368
|
+
let completed: Bool
|
|
369
|
+
let blockers: [String]
|
|
370
|
+
let recoveryAction: String?
|
|
371
|
+
|
|
372
|
+
static func decode(from json: [String: Any], expectedOperation: String) throws -> DesktopBridgeCommandResultTruth {
|
|
373
|
+
guard Set(json.keys) == keys,
|
|
374
|
+
json["schema"] as? String == "sks.desktop-bridge-command-result.v1",
|
|
375
|
+
json["operation"] as? String == expectedOperation,
|
|
376
|
+
nonempty(json["operation_id"]) != nil,
|
|
377
|
+
nonempty(json["correlation_id"]) != nil,
|
|
378
|
+
nonempty(json["checked_at"]) != nil,
|
|
379
|
+
let topLevelOK = json["ok"] as? Bool,
|
|
380
|
+
let execution = json["execution"] as? [String: Any],
|
|
381
|
+
Set(execution.keys) == ["ok", "status", "blockers"],
|
|
382
|
+
let executionOK = execution["ok"] as? Bool,
|
|
383
|
+
let executionStatus = execution["status"] as? String,
|
|
384
|
+
["completed", "partial", "failed"].contains(executionStatus),
|
|
385
|
+
let blockers = execution["blockers"] as? [String],
|
|
386
|
+
let readiness = json["readiness"] as? [String: Any],
|
|
387
|
+
Set(readiness.keys) == ["ready", "blockers", "warnings"],
|
|
388
|
+
readiness["ready"] is Bool,
|
|
389
|
+
readiness["blockers"] is [String],
|
|
390
|
+
readiness["warnings"] is [String],
|
|
391
|
+
(json["status"] is NSNull || json["status"] is [String: Any]),
|
|
392
|
+
let result = json["result"] as? [String: Any],
|
|
393
|
+
(json["recovery_action"] is NSNull || json["recovery_action"] is String),
|
|
394
|
+
json["execution_ok"] as? Bool == executionOK,
|
|
395
|
+
nonempty(json["command_summary"]) != nil,
|
|
396
|
+
topLevelOK == executionOK else {
|
|
397
|
+
throw ProviderFacadeError.schemaInvalid("desktop_bridge_command_result_schema_invalid")
|
|
398
|
+
}
|
|
399
|
+
let completed = topLevelOK && executionStatus == "completed" && blockers.isEmpty
|
|
400
|
+
let partial = topLevelOK && executionStatus == "partial" && !blockers.isEmpty
|
|
401
|
+
let failed = !topLevelOK && executionStatus == "failed"
|
|
402
|
+
guard completed || partial || failed else {
|
|
403
|
+
throw ProviderFacadeError.schemaInvalid("desktop_bridge_command_result_execution_invalid")
|
|
404
|
+
}
|
|
405
|
+
if completed && expectedOperation == "repair" {
|
|
406
|
+
guard let service = result["service"] as? [String: Any],
|
|
407
|
+
service["ok"] as? Bool == true,
|
|
408
|
+
service["running"] as? Bool == true else {
|
|
409
|
+
throw ProviderFacadeError.schemaInvalid("desktop_bridge_command_result_service_invalid")
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return DesktopBridgeCommandResultTruth(
|
|
413
|
+
completed: completed,
|
|
414
|
+
blockers: blockers,
|
|
415
|
+
recoveryAction: json["recovery_action"] as? String
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private static func nonempty(_ value: Any?) -> String? {
|
|
420
|
+
guard let string = value as? String else { return nil }
|
|
421
|
+
let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
422
|
+
return trimmed.isEmpty ? nil : trimmed
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
359
426
|
struct CapabilityDisplayRow: Equatable {
|
|
360
427
|
let scope: CapabilityScope
|
|
361
428
|
let capability: String
|
|
@@ -385,3 +452,20 @@ struct CapabilityDisplayRow: Equatable {
|
|
|
385
452
|
return nil
|
|
386
453
|
}
|
|
387
454
|
}
|
|
455
|
+
|
|
456
|
+
enum CapabilityDisplayFilter {
|
|
457
|
+
static func rows(_ rows: [CapabilityDisplayRow], showAll: Bool) -> [CapabilityDisplayRow] {
|
|
458
|
+
showAll ? rows : rows.filter { isIssue($0.state) }
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
static func issueCount(_ rows: [CapabilityDisplayRow]) -> Int {
|
|
462
|
+
rows.filter { isIssue($0.state) }.count
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private static func isIssue(_ state: CapabilityProbeState) -> Bool {
|
|
466
|
+
switch state {
|
|
467
|
+
case .degraded, .blocked, .failed, .stale: return true
|
|
468
|
+
case .notAttempted, .running, .verified, .unsupported: return false
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
@@ -29,6 +29,7 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
29
29
|
let capabilityStatus = NativeView.detail("Capabilities have not been verified.")
|
|
30
30
|
let capabilityLastCheckedStatus = NativeView.detail("Last feature check: never")
|
|
31
31
|
let capabilityStack = NSStackView()
|
|
32
|
+
var capabilityFilterButton: NSButton!
|
|
32
33
|
let routeModelField: NSTextField = {
|
|
33
34
|
let field = NSTextField(frame: NSRect(x: 0, y: 0, width: 250, height: 24))
|
|
34
35
|
field.placeholderString = "Select or enter an exact catalog model"
|
|
@@ -55,6 +56,7 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
55
56
|
var lastStatusCheckedAt: Date?
|
|
56
57
|
var lastStatusCorrelationId: String?
|
|
57
58
|
var providerEnabled = ["codex-lb": false, "openrouter": false]
|
|
59
|
+
var showAllCapabilities = false
|
|
58
60
|
|
|
59
61
|
init(processClient: ProcessClient, operations: OperationCoordinator) {
|
|
60
62
|
self.processClient = processClient; self.operations = operations
|
|
@@ -103,7 +105,9 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
private func makeCapabilityMatrixCard() -> NSBox {
|
|
106
|
-
|
|
108
|
+
capabilityFilterButton = NativeView.button("Show All Capabilities", target: self, action: #selector(toggleCapabilityFilter))
|
|
109
|
+
capabilityFilterButton.setAccessibilityIdentifier("sks-provider-capability-filter")
|
|
110
|
+
let card = NativeView.card(title: "Capability Matrix", subtitle: "Issues are shown first. Expand the verified and not-attempted rows only when you need the full diagnostic trace.", views: [capabilityStatus, capabilityLastCheckedStatus, ControlKit.actionRow([capabilityFilterButton]), capabilityStack])
|
|
107
111
|
card.setAccessibilityIdentifier("sks-provider-card-capability-matrix")
|
|
108
112
|
return card
|
|
109
113
|
}
|
|
@@ -120,17 +124,6 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
120
124
|
if providerActionInFlight.isEmpty { globalSpinner.stopAnimation(nil) } else { globalSpinner.startAnimation(nil) }
|
|
121
125
|
}
|
|
122
126
|
|
|
123
|
-
private func run(_ args: [String], title: String, kind: String, group: String?, timeout: TimeInterval = NativeView.mutationTimeout, completion: (() -> Void)? = nil) {
|
|
124
|
-
guard let snapshot = operations.begin(kind: kind, mutationGroup: group, summary: title) else { providerStatus.stringValue = "Another guarded mutation is running."; return }
|
|
125
|
-
setBusy(true)
|
|
126
|
-
_ = operations.update(snapshot, state: .running, stage: "running", progress: nil, summary: title)
|
|
127
|
-
processClient.run(args, timeout: timeout) { [weak self] result in
|
|
128
|
-
guard let self = self else { return }; self.setBusy(false)
|
|
129
|
-
_ = self.operations.update(snapshot, state: result.code == 0 ? .succeeded : .failed, stage: "complete", progress: 1, summary: result.code == 0 ? "\(title) completed" : "\(title) failed")
|
|
130
|
-
completion?()
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
127
|
func refresh() {
|
|
135
128
|
refreshCredentialHealth()
|
|
136
129
|
processClient.run(["bridge", "status", "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
|
|
@@ -234,7 +227,9 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
234
227
|
private func renderCapabilityReport(_ report: DesktopCapabilityReportV3) {
|
|
235
228
|
lastCapabilityReport = report
|
|
236
229
|
capabilityStack.arrangedSubviews.forEach { capabilityStack.removeArrangedSubview($0); $0.removeFromSuperview() }
|
|
237
|
-
|
|
230
|
+
let allRows = CapabilityDisplayRow.rows(from: report)
|
|
231
|
+
let visibleRows = CapabilityDisplayFilter.rows(allRows, showAll: showAllCapabilities)
|
|
232
|
+
for row in visibleRows {
|
|
238
233
|
let cause = row.rootCause.map { " · root cause \(ProviderSecretRedactor.redact($0))" } ?? ""
|
|
239
234
|
let recoveryAction = row.recoveryAction.flatMap(ProviderRecoveryAction.init(rawValue:))
|
|
240
235
|
let recovery = recoveryAction.map { " · recovery \($0.buttonTitle)" } ?? ""
|
|
@@ -250,8 +245,14 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
250
245
|
capabilityStack.addArrangedSubview(value)
|
|
251
246
|
}
|
|
252
247
|
}
|
|
248
|
+
if visibleRows.isEmpty {
|
|
249
|
+
capabilityStack.addArrangedSubview(NativeView.detail("No capability issues in this report. Show all capabilities to inspect the complete trace."))
|
|
250
|
+
}
|
|
251
|
+
capabilityFilterButton.title = showAllCapabilities ? "Show Issues Only" : "Show All Capabilities"
|
|
252
|
+
capabilityFilterButton.setAccessibilityLabel(capabilityFilterButton.title)
|
|
253
|
+
let issueCount = CapabilityDisplayFilter.issueCount(allRows)
|
|
253
254
|
let satisfied = report.summary.levelSatisfied
|
|
254
|
-
capabilityStatus.stringValue = "\(report.requestedLevel.capitalized) diagnostic completed · readiness \(satisfied ? "satisfied" : "needs action") · full deep \(report.summary.fullFeatureVerified ? "verified" : "not verified")"
|
|
255
|
+
capabilityStatus.stringValue = "\(report.requestedLevel.capitalized) diagnostic completed · \(issueCount) issue\(issueCount == 1 ? "" : "s") · readiness \(satisfied ? "satisfied" : "needs action") · full deep \(report.summary.fullFeatureVerified ? "verified" : "not verified")"
|
|
255
256
|
capabilityStatus.textColor = satisfied ? .systemGreen : .systemOrange
|
|
256
257
|
capabilityLastCheckedStatus.stringValue = "Last feature check: \(report.checkedAt) · report \(report.reportId)"
|
|
257
258
|
renderCatalogStatus(report.catalogSync)
|
|
@@ -261,7 +262,40 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
261
262
|
ProviderStatusColor.forState(state.rawValue)
|
|
262
263
|
}
|
|
263
264
|
|
|
264
|
-
@objc private func
|
|
265
|
+
@objc private func toggleCapabilityFilter() {
|
|
266
|
+
showAllCapabilities.toggle()
|
|
267
|
+
if let report = lastCapabilityReport { renderCapabilityReport(report) }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@objc private func repairDesktopBridge() {
|
|
271
|
+
guard let snapshot = operations.begin(kind: "bridge-repair", mutationGroup: "codex-config", summary: "Repair Desktop Bridge") else {
|
|
272
|
+
providerStatus.stringValue = "Another guarded mutation is running."
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
setBusy(true)
|
|
276
|
+
providerStatus.stringValue = "Desktop Bridge repair running…"
|
|
277
|
+
_ = operations.update(snapshot, state: .running, stage: "repairing", progress: nil, summary: "Repair Desktop Bridge")
|
|
278
|
+
processClient.run(["bridge", "repair", "--json"], timeout: NativeView.mutationTimeout) { [weak self] result in
|
|
279
|
+
guard let self = self else { return }
|
|
280
|
+
self.setBusy(false)
|
|
281
|
+
let parsed = self.json(result.output)
|
|
282
|
+
let truth = parsed.flatMap {
|
|
283
|
+
try? DesktopBridgeCommandResultTruth.decode(from: $0, expectedOperation: "repair")
|
|
284
|
+
}
|
|
285
|
+
let nativeError = parsed.flatMap { row -> String? in
|
|
286
|
+
row["schema"] as? String == "sks.native-process-error.v1" ? row["error"] as? String : nil
|
|
287
|
+
}
|
|
288
|
+
let completed = result.code == 0 && truth?.completed == true
|
|
289
|
+
let blocker = (truth?.blockers.first ?? nativeError).map(ProviderSecretRedactor.redact)
|
|
290
|
+
let summary = completed
|
|
291
|
+
? "Repair Desktop Bridge completed"
|
|
292
|
+
: blocker.map { "Repair Desktop Bridge needs action · \($0)" } ?? "Repair Desktop Bridge result schema invalid"
|
|
293
|
+
_ = self.operations.update(snapshot, state: completed ? .succeeded : .failed, stage: "complete", progress: 1, summary: summary)
|
|
294
|
+
self.providerStatus.stringValue = summary
|
|
295
|
+
self.providerStatus.textColor = completed ? .systemGreen : .systemRed
|
|
296
|
+
self.refresh()
|
|
297
|
+
}
|
|
298
|
+
}
|
|
265
299
|
|
|
266
300
|
@objc private func performRecoveryAction(_ sender: NSButton) {
|
|
267
301
|
let prefix = "sks-provider-recovery-"
|