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
@@ -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
- let card = NativeView.card(title: "Capability Matrix", subtitle: "Each row keeps its own scope, route, OAuth requirement, stage, root cause, and allowlisted recovery. Deep-only work not run is not a failure.", views: [capabilityStatus, capabilityLastCheckedStatus, capabilityStack])
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
- for row in CapabilityDisplayRow.rows(from: report) {
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 repairDesktopBridge() { run(["bridge", "repair", "--json"], title: "Repair Desktop Bridge", kind: "bridge-repair", group: "codex-config") { [weak self] in self?.refresh() } }
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-"
@@ -153,8 +153,18 @@ final class SKSKeychainStore {
153
153
  return SKSKeychainReadResult(state: .available, secret: data)
154
154
  }
155
155
 
156
+ /// Status checks query item attributes only. The classic Keychain ACL
157
+ /// password dialog protects item *data*, not attributes, and it is NOT
158
+ /// suppressed by LAContext.interactionNotAllowed — so a status check that
159
+ /// requested kSecReturnData would beach-ball the app behind a system
160
+ /// password prompt whenever a rebuilt/resigned binary no longer matches
161
+ /// the stored item's ACL. Attributes-only presence can never prompt.
156
162
  func statusNonInteractive(_ credential: SKSKeychainCredential) -> SKSKeychainCredentialState {
157
- readNonInteractive(credential).state
163
+ let result = client.copyMatching(statusQuery(for: credential))
164
+ guard result.status == errSecSuccess else {
165
+ return state(for: result.status)
166
+ }
167
+ return .available
158
168
  }
159
169
 
160
170
  /// Writing is permitted only after an explicit Center action. Callers cannot
@@ -301,6 +311,16 @@ final class SKSKeychainStore {
301
311
  ]
302
312
  }
303
313
 
314
+ private func statusQuery(for credential: SKSKeychainCredential) -> [String: Any] {
315
+ var query = identityQuery(for: credential)
316
+ let authenticationContext = LAContext()
317
+ authenticationContext.interactionNotAllowed = true
318
+ query[kSecReturnAttributes as String] = true
319
+ query[kSecMatchLimit as String] = kSecMatchLimitOne
320
+ query[kSecUseAuthenticationContext as String] = authenticationContext
321
+ return query
322
+ }
323
+
304
324
  private func readQuery(for credential: SKSKeychainCredential) -> [String: Any] {
305
325
  var query = identityQuery(for: credential)
306
326
  let authenticationContext = LAContext()
@@ -9,20 +9,20 @@ final class SettingsViewController: NSViewController, ControlCenterPage {
9
9
  }
10
10
 
11
11
  private let notifications: NotificationCoordinator
12
- private let quitWithCodex = NSButton(checkboxWithTitle: "Quit SKS Menu when Codex quits", target: nil, action: nil)
12
+ private let followCodexLifecycle = NSButton(checkboxWithTitle: "Show SKS Menu only while Codex is running", target: nil, action: nil)
13
13
  private let status = NativeView.detail("Settings use the native app configuration file.")
14
14
  private var notificationButton: NSButton!
15
15
  init(notifications: NotificationCoordinator) { self.notifications = notifications; super.init(nibName: nil, bundle: nil) }
16
16
  required init?(coder: NSCoder) { nil }
17
17
 
18
18
  override func loadView() {
19
- quitWithCodex.target = self; quitWithCodex.action = #selector(save)
20
- quitWithCodex.setAccessibilityLabel("Quit SKS Menu when Codex quits")
19
+ followCodexLifecycle.target = self; followCodexLifecycle.action = #selector(save)
20
+ followCodexLifecycle.setAccessibilityLabel("Show SKS Menu only while Codex is running")
21
21
  notificationButton = NativeView.button("Enable Notifications", target: self, action: #selector(enableNotifications))
22
22
  let lifecycleCard = NativeView.card(
23
23
  title: "Codex lifecycle",
24
- subtitle: "Off keeps the menu icon available on cold start and hides it only after a Codex session ends.",
25
- views: [quitWithCodex]
24
+ subtitle: "On keeps a lightweight observer running, hides the icon when Codex is closed, and shows it automatically when Codex opens.",
25
+ views: [followCodexLifecycle]
26
26
  )
27
27
  let notificationsCard = NativeView.card(
28
28
  title: "Notifications",
@@ -39,13 +39,13 @@ final class SettingsViewController: NSViewController, ControlCenterPage {
39
39
  let configResult = readConfig()
40
40
  switch configResult {
41
41
  case .loaded(let config):
42
- quitWithCodex.isEnabled = true
43
- quitWithCodex.state = config["quit_with_codex"] as? Bool == true ? .on : .off
42
+ followCodexLifecycle.isEnabled = true
43
+ followCodexLifecycle.state = CodexLifecyclePolicy.followsCodex(from: config) ? .on : .off
44
44
  case .missing:
45
- quitWithCodex.isEnabled = true
46
- quitWithCodex.state = .off
45
+ followCodexLifecycle.isEnabled = true
46
+ followCodexLifecycle.state = .off
47
47
  case .unreadable, .malformed:
48
- quitWithCodex.isEnabled = false
48
+ followCodexLifecycle.isEnabled = false
49
49
  }
50
50
  if notifications.authorizationDenied {
51
51
  notificationButton.title = "Open Notification Settings…"
@@ -89,17 +89,18 @@ final class SettingsViewController: NSViewController, ControlCenterPage {
89
89
  case .missing:
90
90
  config = [:]
91
91
  case .unreadable:
92
- quitWithCodex.isEnabled = false
92
+ followCodexLifecycle.isEnabled = false
93
93
  status.stringValue = "Settings were not saved because the existing file cannot be read. No file was overwritten."
94
94
  return
95
95
  case .malformed:
96
- quitWithCodex.isEnabled = false
96
+ followCodexLifecycle.isEnabled = false
97
97
  status.stringValue = "Settings were not saved because the existing file is malformed. No file was overwritten."
98
98
  return
99
99
  }
100
- config["schema"] = "sks.sks-menubar-config.v1"
100
+ config["schema"] = "sks.sks-menubar-config.v2"
101
101
  config["codex_bundle_id"] = AppRuntime.codexBundleId as Any
102
- config["quit_with_codex"] = quitWithCodex.state == .on
102
+ config["follow_codex_lifecycle"] = followCodexLifecycle.state == .on
103
+ config.removeValue(forKey: "quit_with_codex")
103
104
  guard JSONSerialization.isValidJSONObject(config), let data = try? JSONSerialization.data(withJSONObject: config, options: [.prettyPrinted]) else {
104
105
  status.stringValue = "Settings could not be encoded."
105
106
  return
@@ -116,9 +117,9 @@ final class SettingsViewController: NSViewController, ControlCenterPage {
116
117
  try FileManager.default.moveItem(at: temporary, to: target)
117
118
  }
118
119
  try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: target.path)
119
- status.stringValue = quitWithCodex.state == .on
120
- ? "Saved. SKS Menu will quit when Codex quits."
121
- : "Saved. SKS Menu stays available on cold start; after a Codex session ends it hides until Codex returns."
120
+ status.stringValue = followCodexLifecycle.state == .on
121
+ ? "Saved. The observer stays running; the icon follows Codex without quitting."
122
+ : "Saved. SKS Menu stays visible whether Codex is open or closed."
122
123
  } catch {
123
124
  try? FileManager.default.removeItem(at: temporary)
124
125
  status.stringValue = "Settings could not be saved. Confirm \(directory.path) is writable."
@@ -74,7 +74,7 @@ final class StatusItemController: NSObject, NSMenuDelegate {
74
74
  menu.addItem(item("View Last Operation", #selector(viewLastOperation)))
75
75
  menu.addItem(item("Retry Last Operation", #selector(retryLastOperation)))
76
76
  menu.addItem(NSMenuItem.separator())
77
- menu.addItem(item("Quit SKS Menu", #selector(quit)))
77
+ menu.addItem(item("Hide Until Codex Opens", #selector(hideUntilCodexOpens)))
78
78
  statusItem.menu = menu
79
79
  configureCodexLifecycle()
80
80
  refreshLocalState()
@@ -297,25 +297,25 @@ final class StatusItemController: NSObject, NSMenuDelegate {
297
297
 
298
298
  private func configureCodexLifecycle() {
299
299
  guard let bundle = AppRuntime.codexBundleId else { return }
300
- // Keep the status item visible on cold start even when Codex is not
301
- // running. Accessory apps have no Dock icon, so hiding before the first
302
- // Codex session made Control Center unreachable after install/restart.
303
- statusItem.isVisible = true
300
+ let config = readJson(path: AppRuntime.configPath)
301
+ let followCodex = CodexLifecyclePolicy.followsCodex(from: config)
302
+ let codexRunning = NSWorkspace.shared.runningApplications.contains { $0.bundleIdentifier == bundle }
303
+ statusItem.isVisible = CodexLifecyclePolicy.initialVisibility(
304
+ followCodex: followCodex,
305
+ codexRunning: codexRunning
306
+ )
304
307
  let center = NSWorkspace.shared.notificationCenter
305
308
  center.addObserver(forName: NSWorkspace.didLaunchApplicationNotification, object: nil, queue: .main) { [weak self] note in
306
309
  guard let app = note.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, app.bundleIdentifier == bundle else { return }
307
- self?.statusItem.isVisible = true
310
+ self?.statusItem.isVisible = CodexLifecyclePolicy.visibilityAfterCodexLaunch()
311
+ self?.refreshLocalState()
308
312
  }
309
313
  center.addObserver(forName: NSWorkspace.didTerminateApplicationNotification, object: nil, queue: .main) { [weak self] note in
310
314
  guard let app = note.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, app.bundleIdentifier == bundle else { return }
311
315
  let config = self?.readJson(path: AppRuntime.configPath)
312
- if config?["quit_with_codex"] as? Bool == true { NSApplication.shared.terminate(nil) }
313
- else {
314
- // Provider/proxy/network failure must not strand recovery behind
315
- // a hidden accessory app. Keep settings and retry actions alive.
316
- self?.statusItem.isVisible = true
317
- self?.refreshLocalState()
318
- }
316
+ let followCodex = CodexLifecyclePolicy.followsCodex(from: config)
317
+ self?.statusItem.isVisible = CodexLifecyclePolicy.visibilityAfterCodexTermination(followCodex: followCodex)
318
+ self?.refreshLocalState()
319
319
  }
320
320
  }
321
321
 
@@ -373,5 +373,5 @@ final class StatusItemController: NSObject, NSMenuDelegate {
373
373
  if FileManager.default.fileExists(atPath: AppRuntime.lastActionLogPath) { NSWorkspace.shared.open(URL(fileURLWithPath: AppRuntime.lastActionLogPath)) }
374
374
  else { openControlCenter(.overview) }
375
375
  }
376
- @objc private func quit() { NSApplication.shared.terminate(nil) }
376
+ @objc private func hideUntilCodexOpens() { statusItem.isVisible = false }
377
377
  }
@@ -3,7 +3,7 @@ import crypto from 'node:crypto';
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
- import { spawn } from 'node:child_process';
6
+ import { execFileSync, spawn } from 'node:child_process';
7
7
  import { SKS_TEMP_LEASE_FILE, tmpdir, writeJsonAtomic } from '../core/fsx.js';
8
8
  import { allCanonicalTestFiles, canonicalTestCorpus, canonicalTestFiles, canonicalTestProofPath, RELEASE_HARNESS_REGRESSION_TESTS, sameCanonicalTestCorpus, writeCanonicalTestProof } from '../core/release/canonical-test-proof.js';
9
9
  import { releaseAuthorizationSnapshot, sameReleaseAuthorizationSnapshot } from '../core/release/release-authorization-snapshot.js';
@@ -148,6 +148,74 @@ function diffIsolatedCodexHome(before, after) {
148
148
  return breaches.sort();
149
149
  }
150
150
  const isolatedHomeBaseline = snapshotIsolatedCodexHome();
151
+ const realMenuBarInstallDir = path.join(realHome, '.codex', 'sks-menubar');
152
+ const realMenuBarLaunchAgentPath = path.join(realHome, 'Library', 'LaunchAgents', 'com.sneakoscope.sks-menubar.plist');
153
+ const realMenuBarStableEntries = ['SKSMenuBar.app', 'build-stamp.json', 'sks-menubar-action.sh'];
154
+ function snapshotRealMenuBarInstall() {
155
+ const out = new Map();
156
+ const record = (file) => {
157
+ try {
158
+ const stat = fs.lstatSync(file);
159
+ if (stat.isSymbolicLink()) {
160
+ out.set(path.relative(realHome, file), 'symlink');
161
+ return;
162
+ }
163
+ if (!stat.isFile())
164
+ return;
165
+ const hash = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
166
+ out.set(path.relative(realHome, file), `${(stat.mode & 0o777).toString(8)}:${hash}`);
167
+ }
168
+ catch { }
169
+ };
170
+ const walk = (dir) => {
171
+ let entries = [];
172
+ try {
173
+ entries = fs.readdirSync(dir, { withFileTypes: true });
174
+ }
175
+ catch {
176
+ return;
177
+ }
178
+ for (const entry of entries) {
179
+ const file = path.join(dir, entry.name);
180
+ if (entry.isDirectory())
181
+ walk(file);
182
+ else
183
+ record(file);
184
+ }
185
+ };
186
+ for (const name of realMenuBarStableEntries) {
187
+ const target = path.join(realMenuBarInstallDir, name);
188
+ let directory = false;
189
+ try {
190
+ directory = fs.lstatSync(target).isDirectory();
191
+ }
192
+ catch {
193
+ continue;
194
+ }
195
+ if (directory)
196
+ walk(target);
197
+ else
198
+ record(target);
199
+ }
200
+ record(realMenuBarLaunchAgentPath);
201
+ return out;
202
+ }
203
+ function realMenuBarLaunchdLoaded() {
204
+ if (process.platform !== 'darwin' || typeof process.getuid !== 'function')
205
+ return null;
206
+ try {
207
+ execFileSync('/bin/launchctl', ['print', `gui/${process.getuid()}/com.sneakoscope.sks-menubar`], {
208
+ stdio: 'ignore',
209
+ timeout: 5_000
210
+ });
211
+ return true;
212
+ }
213
+ catch {
214
+ return false;
215
+ }
216
+ }
217
+ const realMenuBarBaseline = snapshotRealMenuBarInstall();
218
+ const realMenuBarLaunchdBaseline = realMenuBarLaunchdLoaded();
151
219
  const isolatedProcessGroup = process.platform !== 'win32';
152
220
  const childEnv = {
153
221
  ...process.env,
@@ -229,6 +297,13 @@ async function finalize(code, signal, spawnError) {
229
297
  clearTimeout(signalTimer);
230
298
  await settleChildTree();
231
299
  const homeBreaches = diffIsolatedCodexHome(isolatedHomeBaseline, snapshotIsolatedCodexHome());
300
+ const menubarBreaches = diffIsolatedCodexHome(realMenuBarBaseline, snapshotRealMenuBarInstall());
301
+ const realMenuBarLaunchdAfter = realMenuBarLaunchdLoaded();
302
+ if (realMenuBarLaunchdBaseline !== null && realMenuBarLaunchdAfter !== null && realMenuBarLaunchdBaseline !== realMenuBarLaunchdAfter) {
303
+ menubarBreaches.push(realMenuBarLaunchdBaseline
304
+ ? 'launchd_service_booted_out:com.sneakoscope.sks-menubar'
305
+ : 'launchd_service_bootstrapped:com.sneakoscope.sks-menubar');
306
+ }
232
307
  const cleanupError = await cleanup();
233
308
  removeSignalHandlers();
234
309
  if (spawnError)
@@ -240,8 +315,13 @@ async function finalize(code, signal, spawnError) {
240
315
  : null;
241
316
  if (breachError)
242
317
  console.error(breachError.message);
318
+ const menubarBreachError = menubarBreaches.length
319
+ ? new Error(`canonical_test_menubar_isolation_breach: a test mutated the real SKS Menu Bar install (${menubarBreaches.join(', ')})`)
320
+ : null;
321
+ if (menubarBreachError)
322
+ console.error(menubarBreachError.message);
243
323
  let proofError = null;
244
- const successfulRun = code === 0 && !signal && !forwardedSignal && !spawnError && !cleanupError && !breachError;
324
+ const successfulRun = code === 0 && !signal && !forwardedSignal && !spawnError && !cleanupError && !breachError && !menubarBreachError;
245
325
  if (successfulRun && !allTestsRequested) {
246
326
  try {
247
327
  const finalAuthorization = releaseAuthorizationSnapshot(root, pkg);
@@ -272,7 +352,7 @@ async function finalize(code, signal, spawnError) {
272
352
  else if (signal)
273
353
  process.kill(process.pid, signal);
274
354
  else
275
- process.exitCode = spawnError || cleanupError || breachError || proofError ? 1 : code;
355
+ process.exitCode = spawnError || cleanupError || breachError || menubarBreachError || proofError ? 1 : code;
276
356
  }
277
357
  function signalChildTree(signal) {
278
358
  const child = activeChild;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "8.3.0",
4
+ "version": "8.3.2",
5
5
  "description": "Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",