sneakoscope 8.3.0 → 8.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/config/skills-hash-ledger.v1.json +3 -0
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/config/skills-manifest.json +7 -4
- package/dist/core/codex-app/menubar/config.js +4 -4
- package/dist/core/codex-app/menubar/constants.js +2 -1
- package/dist/core/codex-app/menubar/global-install.js +16 -0
- package/dist/core/codex-app/menubar/installer.js +15 -2
- package/dist/core/codex-app/menubar/launch-agent.js +23 -9
- package/dist/core/codex-app/menubar/rollback.js +12 -2
- package/dist/core/codex-lb/combined-catalog/normalize.js +37 -3
- package/dist/core/codex-lb/combined-catalog/shared.js +7 -1
- package/dist/core/codex-lb/desktop-bridge/header-policy.js +5 -3
- package/dist/core/codex-lb/desktop-bridge/http-forward.js +48 -3
- package/dist/core/codex-lb/desktop-bridge/server.js +13 -7
- package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +2 -1
- package/dist/core/codex-lb/desktop-bridge-migration/retired-runtime-cleanup.js +46 -8
- package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +12 -2
- package/dist/core/codex-lb/desktop-controller-v3/live-probes.js +34 -8
- package/dist/core/codex-lb/desktop-controller-v3/preflight-probes.js +13 -4
- package/dist/core/codex-lb/desktop-controller-v3/shared.js +2 -1
- package/dist/core/codex-lb/desktop-controller-v3/verification.js +10 -9
- package/dist/core/codex-lb/desktop-service.js +57 -4
- package/dist/core/codex-lb/route-index.js +1 -0
- package/dist/core/managed-assets/managed-assets-manifest.js +1 -0
- package/dist/core/release/canonical-test-proof.js +8 -2
- package/dist/core/routes/evidence.js +1 -1
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/CodexLifecyclePolicy.swift +18 -0
- package/dist/native/sks-menubar/Sources/ProcessClient.swift +3 -1
- package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +94 -3
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +49 -15
- package/dist/native/sks-menubar/Sources/SKSKeychainStore.swift +21 -1
- package/dist/native/sks-menubar/Sources/SettingsViewController.swift +18 -17
- package/dist/native/sks-menubar/Sources/StatusItemController.swift +14 -14
- package/dist/scripts/canonical-test-runner.js +83 -3
- package/package.json +1 -1
|
@@ -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-"
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
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: "
|
|
25
|
-
views: [
|
|
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
|
-
|
|
43
|
-
|
|
42
|
+
followCodexLifecycle.isEnabled = true
|
|
43
|
+
followCodexLifecycle.state = CodexLifecyclePolicy.followsCodex(from: config) ? .on : .off
|
|
44
44
|
case .missing:
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
followCodexLifecycle.isEnabled = true
|
|
46
|
+
followCodexLifecycle.state = .off
|
|
47
47
|
case .unreadable, .malformed:
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
100
|
+
config["schema"] = "sks.sks-menubar-config.v2"
|
|
101
101
|
config["codex_bundle_id"] = AppRuntime.codexBundleId as Any
|
|
102
|
-
config["
|
|
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 =
|
|
120
|
-
? "Saved.
|
|
121
|
-
: "Saved. SKS Menu stays
|
|
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("
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
statusItem.isVisible =
|
|
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 =
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
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.
|
|
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",
|