aidevops 3.32.44 → 3.32.47

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 CHANGED
@@ -60,7 +60,7 @@ The result: an AI operations platform that manages projects across every busines
60
60
  [![Copyright](https://img.shields.io/badge/Copyright-Marcus%20Quinn%202025--2026-blue.svg)](https://github.com/marcusquinn)
61
61
 
62
62
  <!-- Release & Version Info -->
63
- [![Version](https://img.shields.io/badge/Version-3.32.44-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
63
+ [![Version](https://img.shields.io/badge/Version-3.32.47-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
64
64
  [![npm version](https://img.shields.io/npm/v/aidevops)](https://www.npmjs.com/package/aidevops)
65
65
  [![Homebrew](https://img.shields.io/badge/homebrew-marcusquinn%2Ftap-orange)](https://github.com/marcusquinn/homebrew-tap)
66
66
  [![GitHub repository](https://img.shields.io/badge/github-repository-181717.svg?logo=github)](https://github.com/marcusquinn/aidevops)
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.32.44
1
+ 3.32.47
package/aidevops.sh CHANGED
@@ -5,7 +5,7 @@
5
5
  # AI DevOps Framework CLI
6
6
  # Usage: aidevops <command> [options]
7
7
  #
8
- # Version: 3.32.44
8
+ # Version: 3.32.47
9
9
 
10
10
  set -euo pipefail
11
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidevops",
3
- "version": "3.32.44",
3
+ "version": "3.32.47",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -42,6 +42,7 @@ import {
42
42
  import { readLocalReposSetupSummary } from "./status-local-repos";
43
43
  import { readManagedApps } from "./status-managed-apps";
44
44
  import { readPulseWorkersSummary } from "./status-pulse-workers";
45
+ import { readSecretInventory } from "./status-inventory";
45
46
  import { readVaultSummary } from "./status-vault";
46
47
 
47
48
  export { readVaultSummary } from "./status-vault";
@@ -82,6 +83,7 @@ export function readStatus(
82
83
  const opencodeSessions = readOpenCodeSessions(opencodeDbPath, opencodeDbPathRef, localRepos.repos);
83
84
  const oauthPool = readOAuthPoolSummary(oauthPoolPath, oauthPoolPathRef);
84
85
  const vault = readVaultSummary(repoRoot);
86
+ const secretInventory = readSecretInventory(repoRoot, vault);
85
87
  const pulseWorkers = readPulseWorkersSummary({ observedAt: options.observedAt, oauthPoolPath, ...options.pulseWorkers });
86
88
  const notifications = buildStatusNotifications({
87
89
  aiApps,
@@ -153,7 +155,8 @@ export function readStatus(
153
155
  notifications,
154
156
  vault,
155
157
  pulse_workers: pulseWorkers.summary,
156
- secrets: vault.unlocked ? statusFixture.secrets : [],
158
+ secrets: vault.unlocked ? secretInventory.secrets : [],
159
+ secret_backends: vault.unlocked ? secretInventory.backends : statusFixture.secret_backends,
157
160
  };
158
161
 
159
162
  const envelope = createEnvelope({
@@ -0,0 +1,86 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { lstatSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import type { GuiSecretReference, GuiVaultStatusData } from "../../gui-shared/src";
5
+ import { readVaultSummary } from "./status-vault";
6
+
7
+ type BackendState = "available" | "missing" | "error";
8
+ type SecretInventory = {
9
+ version: 1;
10
+ backends: { gopass: BackendState; credentials: BackendState };
11
+ secrets: GuiSecretReference[];
12
+ };
13
+
14
+ const EMPTY_INVENTORY: Omit<SecretInventory, "version"> = {
15
+ backends: { gopass: "missing", credentials: "missing" },
16
+ secrets: [],
17
+ };
18
+ const BACKEND_STATES = new Set<unknown>(["available", "missing", "error"]);
19
+
20
+ export function readSecretInventory(repoRoot: string, vault: GuiVaultStatusData): Omit<SecretInventory, "version"> {
21
+ if (!isAuthoritativelyUnlocked(vault)) return EMPTY_INVENTORY;
22
+ const helperPath = join(repoRoot, ".agents", "scripts", "secret-helper.sh");
23
+ if (!isTrustedHelper(helperPath)) return EMPTY_INVENTORY;
24
+ const result = spawnSync("/bin/bash", [helperPath, "inventory"], {
25
+ encoding: "utf8",
26
+ env: inventoryEnvironment(),
27
+ maxBuffer: 65_536,
28
+ stdio: ["ignore", "pipe", "ignore"],
29
+ timeout: 300,
30
+ });
31
+ if (result.error !== undefined || result.signal !== null || result.status !== 0 || result.stdout.length > 65_535) return EMPTY_INVENTORY;
32
+ return parseUnlockedInventory(result.stdout, repoRoot);
33
+ }
34
+
35
+ function parseUnlockedInventory(output: string, repoRoot: string): Omit<SecretInventory, "version"> {
36
+ try {
37
+ const inventory: unknown = JSON.parse(output);
38
+ const confirmed = readVaultSummary(repoRoot);
39
+ return isInventory(inventory) && isAuthoritativelyUnlocked(confirmed)
40
+ ? { backends: inventory.backends, secrets: inventory.secrets }
41
+ : EMPTY_INVENTORY;
42
+ } catch {
43
+ return EMPTY_INVENTORY;
44
+ }
45
+ }
46
+
47
+ function isAuthoritativelyUnlocked(vault: GuiVaultStatusData): boolean {
48
+ return vault.status === "unlocked" && vault.unlocked && !vault.locked && vault.helper_status === "available";
49
+ }
50
+
51
+ function isTrustedHelper(path: string): boolean {
52
+ try {
53
+ const stat = lstatSync(path);
54
+ return stat.isFile() && !stat.isSymbolicLink() && stat.uid === process.getuid?.() && (stat.mode & 0o022) === 0;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ function isInventory(value: unknown): value is SecretInventory {
61
+ if (!isRecord(value) || value.version !== 1 || !Array.isArray(value.secrets) || value.secrets.length > 512) return false;
62
+ if (!isRecord(value.backends) || Object.keys(value).length !== 3 || Object.keys(value.backends).length !== 2) return false;
63
+ if (!BACKEND_STATES.has(value.backends.gopass) || !BACKEND_STATES.has(value.backends.credentials)) return false;
64
+ return value.secrets.every((secret, index) => isSecretReference(secret, index === 0 ? "" : value.secrets[index - 1]?.name));
65
+ }
66
+
67
+ function isSecretReference(value: unknown, previousName: string | undefined): value is GuiSecretReference {
68
+ if (!isRecord(value) || typeof value.name !== "string") return false;
69
+ return /^[A-Z][A-Z0-9_]{0,127}$/.test(value.name)
70
+ && value.status === "configured"
71
+ && value.name > (previousName ?? "")
72
+ && Object.keys(value).every((key) => key === "name" || key === "status");
73
+ }
74
+
75
+ function isRecord(value: unknown): value is Record<string, unknown> {
76
+ return typeof value === "object" && value !== null;
77
+ }
78
+
79
+ function inventoryEnvironment(): NodeJS.ProcessEnv {
80
+ const environment: NodeJS.ProcessEnv = { PATH: "/usr/bin:/bin:/usr/sbin:/sbin" };
81
+ for (const name of ["HOME", "XDG_CONFIG_HOME", "XDG_RUNTIME_DIR", "AIDEVOPS_VAULT_DIR", "AIDEVOPS_VAULT_RUNTIME_DIR", "AIDEVOPS_VAULT_PYTHON"]) {
82
+ const value = process.env[name];
83
+ if (value !== undefined) environment[name] = value;
84
+ }
85
+ return environment;
86
+ }
@@ -202,9 +202,27 @@ run_vault_command() {
202
202
  local action="\$1"
203
203
  local command_name=""
204
204
  case "\$action" in
205
- init|unlock|lock|status|lost-passphrase) command_name="\$action" ;;
205
+ init|unlock|lock) command_name="\$action" ;;
206
206
  *) printf 'Unsupported Vault action\n' >&2; return 2 ;;
207
207
  esac
208
+ local trusted_path=""
209
+ for trusted_path in \
210
+ "\${REPO_ROOT}/aidevops.sh" \
211
+ "\${REPO_ROOT}/.agents/scripts/vault-helper.sh" \
212
+ "\${REPO_ROOT}/.agents/scripts/vault_crypto_core.py"; do
213
+ if [[ ! -f "\${trusted_path}" || -L "\${trusted_path}" ]]; then
214
+ printf 'Untrusted Vault helper chain\n' >&2
215
+ return 2
216
+ fi
217
+ local owner_mode=""
218
+ owner_mode="\$(/usr/bin/stat -f '%u %Lp' "\${trusted_path}" 2>/dev/null)" || return 2
219
+ local owner="\${owner_mode%% *}"
220
+ local mode="\${owner_mode##* }"
221
+ if [[ "\${owner}" != "\$(/usr/bin/id -u)" || \$((8#\${mode} & 8#022)) -ne 0 ]]; then
222
+ printf 'Untrusted Vault helper chain\n' >&2
223
+ return 2
224
+ fi
225
+ done
208
226
  AIDEVOPS_SESSION_ID="gui-desktop" exec /bin/bash "\${REPO_ROOT}/aidevops.sh" vault "\$command_name"
209
227
  return 1
210
228
  }
@@ -451,6 +469,7 @@ write_webview_source() {
451
469
 
452
470
  cat > "$swift_source" <<'SWIFT'
453
471
  import AppKit
472
+ import Darwin
454
473
  import WebKit
455
474
 
456
475
  final class DraggableTitlebarView: NSView {
@@ -477,10 +496,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
477
496
  private let serviceQueue = DispatchQueue(label: "sh.aidevops.gui.services")
478
497
  private var hasLoadedDashboard = false
479
498
  private var servicesStopped = false
499
+ private var vaultProcess: Process?
500
+ private var vaultMaster: FileHandle?
501
+ private var vaultSlaveFD: Int32 = -1
502
+ private var vaultOverlay: NSView?
503
+ private var vaultOutput: NSTextView?
504
+ private var vaultInput: NSSecureTextField?
505
+ private var vaultSubmit: NSButton?
506
+ private var vaultAllowsVisibleAcknowledgement = false
480
507
 
481
508
  func applicationDidFinishLaunching(_ notification: Notification) {
482
509
  configureMenu()
483
510
  configureWindow()
511
+ NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(cancelVaultForSessionChange(_:)), name: NSWorkspace.sessionDidResignActiveNotification, object: nil)
512
+ NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(cancelVaultForSessionChange(_:)), name: NSWorkspace.screensDidSleepNotification, object: nil)
484
513
  startServicesAndLoadApp()
485
514
  }
486
515
 
@@ -489,6 +518,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
489
518
  }
490
519
 
491
520
  func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
521
+ closeVaultTerminal(result: "cancelled")
492
522
  saveMainWindowFrame()
493
523
  stopServices()
494
524
  return .terminateNow
@@ -551,57 +581,198 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
551
581
  // action. The bundled launcher independently allowlists it and resolves
552
582
  // the repository CLI without PATH lookup.
553
583
  switch action {
554
- case "init", "unlock", "lock", "status", "lost-passphrase": break
584
+ case "init", "unlock", "lock": break
555
585
  default: return
556
586
  }
587
+ presentVaultTerminal(action: action)
588
+ }
557
589
 
558
- guard let helperPath = Bundle.main.path(forResource: "aidevops-gui-services", ofType: "sh") else {
559
- NSSound.beep()
590
+ private func isTrustedVaultMessage(_ message: WKScriptMessage) -> Bool {
591
+ let origin = message.frameInfo.securityOrigin
592
+ return message.frameInfo.isMainFrame && origin.protocol == "http" && origin.host == "127.0.0.1" && origin.port == Int(webPort)
593
+ }
594
+
595
+ private func notifyVaultCommandResult(_ result: String) {
596
+ guard ["presented", "running", "succeeded", "failed", "cancelled"].contains(result) else { return }
597
+ let script = "window.dispatchEvent(new CustomEvent('aidevops:vault-command-result', { detail: '\(result)' }))"
598
+ webView.evaluateJavaScript(script, completionHandler: nil)
599
+ }
600
+
601
+ private func presentVaultTerminal(action: String) {
602
+ guard vaultProcess == nil, let contentView = window.contentView else {
560
603
  notifyVaultCommandResult("failed")
561
604
  return
562
605
  }
563
- guard helperPath.rangeOfCharacter(from: .newlines) == nil else {
606
+ let helperPath = Bundle.main.path(forResource: "aidevops-gui-services", ofType: "sh") ?? ""
607
+ guard trustedVaultExecutable(path: helperPath) else {
564
608
  NSSound.beep()
565
609
  notifyVaultCommandResult("failed")
566
610
  return
567
611
  }
568
612
 
569
- let command = "/bin/bash \(shellQuote(helperPath)) vault-command \(shellQuote(action))"
570
- let source = "tell application \"Terminal\"\nactivate\ndo script \(appleScriptQuote(command))\nend tell"
571
- guard let script = NSAppleScript(source: source) else {
572
- NSSound.beep()
573
- notifyVaultCommandResult("failed")
613
+ let overlay = NSVisualEffectView()
614
+ overlay.material = .hudWindow
615
+ overlay.blendingMode = .withinWindow
616
+ overlay.translatesAutoresizingMaskIntoConstraints = false
617
+ let title = NSTextField(labelWithString: "Secure Vault \(action)")
618
+ title.font = .boldSystemFont(ofSize: 18)
619
+ title.translatesAutoresizingMaskIntoConstraints = false
620
+ let scroll = NSScrollView()
621
+ scroll.hasVerticalScroller = true
622
+ scroll.translatesAutoresizingMaskIntoConstraints = false
623
+ let output = NSTextView()
624
+ output.isEditable = false
625
+ output.isSelectable = true
626
+ output.font = .monospacedSystemFont(ofSize: 13, weight: .regular)
627
+ output.string = "Starting fixed Vault action…\n"
628
+ scroll.documentView = output
629
+ let input = NSSecureTextField()
630
+ input.placeholderString = "Secure input becomes available when requested"
631
+ input.isEnabled = false
632
+ input.translatesAutoresizingMaskIntoConstraints = false
633
+ let submit = NSButton(title: "Submit", target: self, action: #selector(submitVaultInput(_:)))
634
+ submit.isEnabled = false
635
+ submit.translatesAutoresizingMaskIntoConstraints = false
636
+ let cancel = NSButton(title: "Cancel", target: self, action: #selector(cancelVaultTerminal(_:)))
637
+ cancel.keyEquivalent = "\u{1b}"
638
+ cancel.translatesAutoresizingMaskIntoConstraints = false
639
+ for view in [title, scroll, input, submit, cancel] { overlay.addSubview(view) }
640
+ contentView.addSubview(overlay)
641
+ NSLayoutConstraint.activate([
642
+ overlay.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), overlay.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
643
+ overlay.topAnchor.constraint(equalTo: contentView.topAnchor), overlay.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
644
+ title.leadingAnchor.constraint(equalTo: overlay.leadingAnchor, constant: 32), title.topAnchor.constraint(equalTo: overlay.topAnchor, constant: 48),
645
+ scroll.leadingAnchor.constraint(equalTo: title.leadingAnchor), scroll.trailingAnchor.constraint(equalTo: overlay.trailingAnchor, constant: -32),
646
+ scroll.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 18), scroll.bottomAnchor.constraint(equalTo: input.topAnchor, constant: -16),
647
+ input.leadingAnchor.constraint(equalTo: title.leadingAnchor), input.trailingAnchor.constraint(equalTo: submit.leadingAnchor, constant: -10), input.bottomAnchor.constraint(equalTo: overlay.bottomAnchor, constant: -32),
648
+ submit.trailingAnchor.constraint(equalTo: cancel.leadingAnchor, constant: -10), submit.centerYAnchor.constraint(equalTo: input.centerYAnchor),
649
+ cancel.trailingAnchor.constraint(equalTo: scroll.trailingAnchor), cancel.centerYAnchor.constraint(equalTo: input.centerYAnchor)
650
+ ])
651
+ vaultOverlay = overlay
652
+ vaultOutput = output
653
+ vaultInput = input
654
+ vaultSubmit = submit
655
+ window.sharingType = .none
656
+ titlebarCameraButton?.isEnabled = false
657
+ notifyVaultCommandResult("presented")
658
+ startVaultPTY(helperPath: helperPath, action: action)
659
+ }
660
+
661
+ private func trustedVaultExecutable(path: String) -> Bool {
662
+ guard !path.isEmpty, let attributes = try? FileManager.default.attributesOfItem(atPath: path),
663
+ let type = attributes[.type] as? FileAttributeType, type == .typeRegular,
664
+ let owner = attributes[.ownerAccountID] as? NSNumber, owner.uint32Value == getuid(),
665
+ let permissions = attributes[.posixPermissions] as? NSNumber, permissions.intValue & 0o022 == 0 else { return false }
666
+ let values = try? URL(fileURLWithPath: path).resourceValues(forKeys: [.isSymbolicLinkKey])
667
+ return values?.isSymbolicLink == false
668
+ }
669
+
670
+ private func startVaultPTY(helperPath: String, action: String) {
671
+ var master: Int32 = -1
672
+ var slave: Int32 = -1
673
+ guard openpty(&master, &slave, nil, nil, nil) == 0 else {
674
+ closeVaultTerminal(result: "failed")
574
675
  return
575
676
  }
576
- var error: NSDictionary?
577
- script.executeAndReturnError(&error)
578
- if error != nil {
579
- NSSound.beep()
580
- notifyVaultCommandResult("failed")
581
- return
677
+ vaultSlaveFD = slave
678
+ let masterHandle = FileHandle(fileDescriptor: master, closeOnDealloc: true)
679
+ let slaveHandle = FileHandle(fileDescriptor: slave, closeOnDealloc: false)
680
+ let process = Process()
681
+ process.executableURL = URL(fileURLWithPath: "/bin/bash")
682
+ process.arguments = [helperPath, "vault-command", action]
683
+ process.environment = ["HOME": FileManager.default.homeDirectoryForCurrentUser.path, "PATH": "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin", "AIDEVOPS_SESSION_ID": "gui-desktop-secure-pty", "TERM": "dumb"]
684
+ process.standardInput = slaveHandle
685
+ process.standardOutput = slaveHandle
686
+ process.standardError = slaveHandle
687
+ masterHandle.readabilityHandler = { [weak self] handle in
688
+ let data = handle.availableData
689
+ guard !data.isEmpty else { return }
690
+ DispatchQueue.main.async { self?.appendVaultOutput(data) }
691
+ }
692
+ process.terminationHandler = { [weak self] process in
693
+ DispatchQueue.main.async { self?.finishVaultTerminal(succeeded: process.terminationStatus == 0) }
694
+ }
695
+ vaultMaster = masterHandle
696
+ vaultProcess = process
697
+ do {
698
+ try process.run()
699
+ close(slave)
700
+ vaultSlaveFD = -1
701
+ notifyVaultCommandResult("running")
702
+ } catch {
703
+ closeVaultTerminal(result: "failed")
582
704
  }
583
- notifyVaultCommandResult("opened")
584
- }
585
-
586
- private func isTrustedVaultMessage(_ message: WKScriptMessage) -> Bool {
587
- let origin = message.frameInfo.securityOrigin
588
- return message.frameInfo.isMainFrame && origin.protocol == "http" && origin.host == "127.0.0.1" && origin.port == Int(webPort)
589
- }
590
-
591
- private func shellQuote(_ value: String) -> String {
592
- return "'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'"
593
705
  }
594
706
 
595
- private func appleScriptQuote(_ value: String) -> String {
596
- let escaped = value
597
- .replacingOccurrences(of: "\\", with: "\\\\")
598
- .replacingOccurrences(of: "\"", with: "\\\"")
599
- return "\"\(escaped)\""
707
+ private func appendVaultOutput(_ data: Data) {
708
+ guard let text = String(data: data, encoding: .utf8) else { return }
709
+ let sanitized = text.unicodeScalars.filter { scalar in scalar == "\n" || scalar == "\r" || scalar == "\t" || scalar.value >= 0x20 && scalar.value != 0x7f }.map(String.init).joined()
710
+ vaultOutput?.string.append(String(sanitized.suffix(16_384)))
711
+ vaultOutput?.scrollToEndOfDocument(nil)
712
+ var attributes = termios()
713
+ let echoDisabled = vaultMaster.map { tcgetattr($0.fileDescriptor, &attributes) == 0 && (attributes.c_lflag & tcflag_t(ECHO)) == 0 } ?? false
714
+ vaultAllowsVisibleAcknowledgement = !echoDisabled && sanitized.contains("I UNDERSTAND")
715
+ let acceptsInput = echoDisabled || vaultAllowsVisibleAcknowledgement
716
+ vaultInput?.isEnabled = acceptsInput
717
+ vaultSubmit?.isEnabled = acceptsInput
718
+ if acceptsInput { window.makeFirstResponder(vaultInput) }
719
+ }
720
+
721
+ @objc private func submitVaultInput(_ sender: Any?) {
722
+ guard let input = vaultInput, input.isEnabled, let master = vaultMaster else { return }
723
+ var attributes = termios()
724
+ let echoDisabled = tcgetattr(master.fileDescriptor, &attributes) == 0 && (attributes.c_lflag & tcflag_t(ECHO)) == 0
725
+ if !echoDisabled && (!vaultAllowsVisibleAcknowledgement || input.stringValue != "I UNDERSTAND") {
726
+ NSSound.beep()
727
+ return
728
+ }
729
+ var bytes = Array(input.stringValue.utf8)
730
+ input.stringValue = ""
731
+ vaultAllowsVisibleAcknowledgement = false
732
+ input.isEnabled = false
733
+ vaultSubmit?.isEnabled = false
734
+ guard !bytes.isEmpty else { return }
735
+ bytes.append(0x0a)
736
+ let data = Data(bytes)
737
+ do { try master.write(contentsOf: data) } catch { closeVaultTerminal(result: "failed") }
738
+ bytes.withUnsafeMutableBytes { buffer in
739
+ buffer.initializeMemory(as: UInt8.self, repeating: 0)
740
+ }
600
741
  }
601
742
 
602
- private func notifyVaultCommandResult(_ result: String) {
603
- let script = "window.dispatchEvent(new CustomEvent('aidevops:vault-command-result', { detail: '\(result)' }))"
604
- webView.evaluateJavaScript(script, completionHandler: nil)
743
+ @objc private func cancelVaultTerminal(_ sender: Any?) {
744
+ closeVaultTerminal(result: "cancelled")
745
+ }
746
+
747
+ private func finishVaultTerminal(succeeded: Bool) {
748
+ let result = succeeded ? "succeeded" : "failed"
749
+ appendVaultOutput(Data("\nVault action \(result). Close this secure surface.\n".utf8))
750
+ vaultInput?.isEnabled = false
751
+ vaultSubmit?.isEnabled = false
752
+ notifyVaultCommandResult(result)
753
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in self?.closeVaultTerminal(result: nil) }
754
+ }
755
+
756
+ private func closeVaultTerminal(result: String?) {
757
+ vaultProcess?.terminationHandler = nil
758
+ if vaultProcess?.isRunning == true { vaultProcess?.terminate() }
759
+ vaultMaster?.readabilityHandler = nil
760
+ try? vaultMaster?.close()
761
+ if vaultSlaveFD >= 0 { close(vaultSlaveFD) }
762
+ vaultInput?.stringValue = ""
763
+ vaultOutput?.string = ""
764
+ vaultOverlay?.removeFromSuperview()
765
+ vaultProcess = nil
766
+ vaultMaster = nil
767
+ vaultSlaveFD = -1
768
+ vaultInput = nil
769
+ vaultOutput = nil
770
+ vaultSubmit = nil
771
+ vaultOverlay = nil
772
+ vaultAllowsVisibleAcknowledgement = false
773
+ window?.sharingType = .readOnly
774
+ titlebarCameraButton?.isEnabled = true
775
+ if let result = result { notifyVaultCommandResult(result) }
605
776
  }
606
777
 
607
778
  private func revealFileURL(_ url: URL) -> Bool {
@@ -874,9 +1045,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
874
1045
  }
875
1046
 
876
1047
  func windowWillClose(_ notification: Notification) {
1048
+ closeVaultTerminal(result: "cancelled")
877
1049
  saveMainWindowFrame()
878
1050
  }
879
1051
 
1052
+ @objc private func cancelVaultForSessionChange(_ notification: Notification) {
1053
+ closeVaultTerminal(result: "cancelled")
1054
+ }
1055
+
880
1056
  @objc private func reloadPage(_ sender: Any?) {
881
1057
  webView.reload()
882
1058
  }
@@ -1131,6 +1307,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
1131
1307
  }
1132
1308
 
1133
1309
  @objc private func captureAppScreenshot(_ sender: Any?) {
1310
+ guard vaultOverlay == nil else { NSSound.beep(); return }
1134
1311
  guard let contentView = window.contentView else {
1135
1312
  return
1136
1313
  }
@@ -1147,6 +1324,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
1147
1324
  }
1148
1325
 
1149
1326
  @objc private func capturePageScreenshot(_ sender: Any?) {
1327
+ guard vaultOverlay == nil else { NSSound.beep(); return }
1150
1328
  let script = """
1151
1329
  (() => {
1152
1330
  const el = document.querySelector('.workspace-scroll');
@@ -648,6 +648,10 @@ export interface GuiStatusData {
648
648
  pulse_workers: GuiPulseWorkerSummary;
649
649
  capabilities: GuiCapabilitySummary[];
650
650
  secrets: GuiSecretReference[];
651
+ secret_backends: {
652
+ gopass: "available" | "missing" | "error";
653
+ credentials: "available" | "missing" | "error";
654
+ };
651
655
  placeholders: string[];
652
656
  }
653
657
 
@@ -495,6 +495,7 @@ export const statusFixture: GuiStatusData = {
495
495
  },
496
496
  ],
497
497
  secrets: [],
498
+ secret_backends: { gopass: "missing", credentials: "missing" },
498
499
  placeholders: [
499
500
  "Settings, repos, routines, OpenCode sessions, and capabilities will be added as read-only adapters.",
500
501
  ],
@@ -47,6 +47,7 @@ export function SecuritySurface({ onVaultRequest, status }: {
47
47
  <SecretMetric detail={vault.unlocked ? "ready for dependent tools" : "visible after local unlock"} icon={<FiCheckCircle />} label="Configured" value={hiddenCount ?? String(configured)} />
48
48
  <SecretMetric detail={vault.unlocked ? "missing or not yet checked" : "visible after local unlock"} icon={<FiAlertTriangle />} label="Needs attention" value={hiddenCount ?? String(needsAttention)} />
49
49
  <SecretMetric detail="hidden local prompt only" icon={<FiTerminal />} label="Value custody" value="Write-only" />
50
+ <SecretMetric detail={`credentials: ${status.secret_backends.credentials}`} icon={<FiActivity />} label="gopass backend" value={vault.unlocked ? status.secret_backends.gopass : "Hidden"} />
50
51
  </div>
51
52
 
52
53
  {vault.unlocked ? (
@@ -63,11 +63,11 @@ interface DialogContent {
63
63
  }
64
64
 
65
65
  const dialogContentFactories: Record<VaultDialogIntent, (vault: GuiVaultStatusData) => DialogContent> = {
66
- lock: () => ({ action: "Open lock terminal", detail: "Locking forgets in-memory keys and hides protected previews again.", notice: "The fixed local command does not receive browser data or secret material.", title: "Lock local Vault" }),
67
- recover: () => ({ action: "Open recovery guidance", detail: "Vault metadata appears damaged. Preserve existing encrypted data and review conservative recovery options.", notice: "Do not initialise with --force or overwrite the existing Vault.", title: "Review Vault recovery" }),
68
- setup: () => ({ action: "Open setup terminal", detail: "Create this device's Vault once through the secure local helper.", notice: "Save the new passphrase in a trusted password manager. aidevops cannot recover it.", title: "Set up Vault" }),
66
+ lock: () => ({ action: "Lock Vault", detail: "Locking forgets in-memory keys and hides protected previews again.", notice: "The fixed native action does not receive browser data or secret material.", title: "Lock local Vault" }),
67
+ recover: () => ({ action: "Refresh status", detail: "Vault metadata appears damaged. Preserve existing encrypted data and use direct CLI recovery guidance.", notice: "Do not initialise with --force or overwrite the existing Vault.", title: "Review Vault recovery" }),
68
+ setup: () => ({ action: "Set up securely", detail: "Create this device's Vault in the native secure surface.", notice: "Save the new passphrase in a trusted password manager. aidevops cannot recover it.", title: "Set up Vault" }),
69
69
  unavailable: () => ({ action: "Retry status", detail: "Vault readiness is not authoritative, so setup and passphrase actions are disabled.", notice: "Check the local helper and crypto runtime, then retry. Existing encrypted data will not be reinitialised.", title: "Vault status unavailable" }),
70
- unlock: (vault) => ({ action: "Open secure terminal", detail: "Unlock the existing Vault with the passphrase you already saved.", notice: vault.unlock_hint, title: "Unlock existing Vault" }),
70
+ unlock: () => ({ action: "Unlock securely", detail: "Unlock the existing Vault with the passphrase you already saved.", notice: "Passphrase input remains in the native secure surface and never enters the web view.", title: "Unlock existing Vault" }),
71
71
  };
72
72
 
73
73
  const dialogIcons: Record<VaultDialogIntent, IconType> = {
@@ -109,6 +109,8 @@ const launchStatusPresentation: Partial<Record<VaultLaunchStatus, { Icon: IconTy
109
109
  copied: { Icon: FiClipboard, className: "vault-valid", role: "status", text: "Command copied. Run it in your local terminal." },
110
110
  failed: { Icon: FiAlertTriangle, className: "vault-invalid", role: "alert", text: "Open a local terminal and run the displayed command." },
111
111
  opened: { Icon: FiCheckCircle, className: "vault-valid", role: "status", text: "Secure terminal opened. Return here after the command completes; status refreshes on focus." },
112
+ succeeded: { Icon: FiCheckCircle, className: "vault-valid", role: "status", text: "Vault action completed securely." },
113
+ cancelled: { Icon: FiAlertTriangle, className: "vault-invalid", role: "status", text: "Vault action cancelled and native buffers cleared." },
112
114
  requesting: { Icon: FiTerminal, className: "vault-valid", role: "status", text: "Requesting the secure local terminal…" },
113
115
  };
114
116
 
@@ -99,6 +99,7 @@ function normalizeStatusEnvelope(envelope: GuiResponseEnvelope<Partial<GuiStatus
99
99
  pulse_workers: normalizePulseWorkers(data.pulse_workers),
100
100
  capabilities: data.capabilities ?? statusFixture.capabilities,
101
101
  secrets: visibleSecrets(vault, data.secrets),
102
+ secret_backends: vault.status === "unlocked" && vault.unlocked ? data.secret_backends ?? statusFixture.secret_backends : statusFixture.secret_backends,
102
103
  placeholders: data.placeholders ?? statusFixture.placeholders,
103
104
  },
104
105
  };
@@ -1,16 +1,23 @@
1
1
  import { type RefObject, useCallback, useEffect, useRef, useState } from "react";
2
2
  import type { VaultDialogIntent } from "./VaultBadges";
3
- import { type NativeVaultAction, postNativeVaultCommand, vaultCommandText } from "./vault-command-bridge";
3
+ import { isNativeVaultResult, type NativeVaultAction, type NativeVaultResult, postNativeVaultCommand, vaultCommandText } from "./vault-command-bridge";
4
4
 
5
- export type VaultLaunchStatus = "idle" | "requesting" | "opened" | "copied" | "failed";
5
+ export type VaultLaunchStatus = "idle" | "requesting" | "opened" | "copied" | "failed" | "succeeded" | "cancelled";
6
6
 
7
7
  const terminalActions: Record<VaultDialogIntent, NativeVaultAction | null> = {
8
8
  lock: "lock",
9
- recover: "lost-passphrase",
9
+ recover: null,
10
10
  setup: "init",
11
11
  unavailable: null,
12
12
  unlock: "unlock",
13
13
  };
14
+ const nativeResultStatuses: Record<NativeVaultResult, VaultLaunchStatus> = {
15
+ cancelled: "cancelled",
16
+ failed: "failed",
17
+ presented: "opened",
18
+ running: "opened",
19
+ succeeded: "succeeded",
20
+ };
14
21
 
15
22
  export function terminalActionForIntent(intent: VaultDialogIntent): NativeVaultAction | null {
16
23
  return terminalActions[intent];
@@ -62,11 +69,12 @@ export function useVaultCommandLaunch({ intent, onRefresh, onTerminalLaunch }: {
62
69
  const handleNativeResult = (event: Event) => {
63
70
  clearNativeResultTimeout();
64
71
  const result = (event as CustomEvent<unknown>).detail;
65
- setLaunchStatus(result === "opened" ? "opened" : "failed");
72
+ setLaunchStatus(isNativeVaultResult(result) ? nativeResultStatuses[result] : "failed");
73
+ if (result === "succeeded") void onRefresh();
66
74
  };
67
75
  window.addEventListener("aidevops:vault-command-result", handleNativeResult);
68
76
  return () => window.removeEventListener("aidevops:vault-command-result", handleNativeResult);
69
- }, [clearNativeResultTimeout, setLaunchStatus]);
77
+ }, [clearNativeResultTimeout, onRefresh, setLaunchStatus]);
70
78
 
71
79
  useEffect(() => {
72
80
  if (timeoutIntent.current !== intent) {
@@ -1,4 +1,10 @@
1
- export type NativeVaultAction = "init" | "unlock" | "lock" | "status" | "lost-passphrase";
1
+ export type NativeVaultAction = "init" | "unlock" | "lock";
2
+ export type NativeVaultResult = "presented" | "running" | "succeeded" | "failed" | "cancelled";
3
+ const NATIVE_VAULT_RESULTS = new Set<unknown>(["presented", "running", "succeeded", "failed", "cancelled"]);
4
+
5
+ export function isNativeVaultResult(value: unknown): value is NativeVaultResult {
6
+ return NATIVE_VAULT_RESULTS.has(value);
7
+ }
2
8
 
3
9
  interface WebKitVaultCommandWindow extends Window {
4
10
  webkit?: {
package/setup.sh CHANGED
@@ -17,7 +17,7 @@ fi
17
17
  # AI Assistant Server Access Framework Setup Script
18
18
  # Helps developers set up the framework for their infrastructure
19
19
  #
20
- # Version: 3.32.44
20
+ # Version: 3.32.47
21
21
  #
22
22
  # Quick Install:
23
23
  # npm install -g aidevops && aidevops update (recommended)