aidevops 3.32.5 → 3.32.7

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.
@@ -0,0 +1,160 @@
1
+ import { type RefObject, useCallback, useEffect, useRef, useState } from "react";
2
+ import type { VaultDialogIntent } from "./VaultBadges";
3
+ import { type NativeVaultAction, postNativeVaultCommand, vaultCommandText } from "./vault-command-bridge";
4
+
5
+ export type VaultLaunchStatus = "idle" | "requesting" | "opened" | "copied" | "failed";
6
+
7
+ const terminalActions: Record<VaultDialogIntent, NativeVaultAction | null> = {
8
+ lock: "lock",
9
+ recover: "lost-passphrase",
10
+ setup: "init",
11
+ unavailable: null,
12
+ unlock: "unlock",
13
+ };
14
+
15
+ export function terminalActionForIntent(intent: VaultDialogIntent): NativeVaultAction | null {
16
+ return terminalActions[intent];
17
+ }
18
+
19
+ export function useVaultDialogFocus({ dialogRef, intent, onClose, primaryActionRef }: {
20
+ dialogRef: RefObject<HTMLElement | null>;
21
+ intent: VaultDialogIntent;
22
+ onClose: () => void;
23
+ primaryActionRef: RefObject<HTMLButtonElement | null>;
24
+ }): void {
25
+ useEffect(() => {
26
+ const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
27
+ return () => previousFocus?.focus();
28
+ }, []);
29
+
30
+ useEffect(() => {
31
+ const primaryAction = primaryActionRef.current;
32
+ if (primaryAction?.dataset.vaultIntent === intent) {
33
+ primaryAction.focus();
34
+ }
35
+ }, [intent, primaryActionRef]);
36
+
37
+ useEffect(() => {
38
+ const handleDialogKeys = (event: KeyboardEvent) => {
39
+ if (event.key === "Escape") {
40
+ onClose();
41
+ }
42
+ trapDialogFocus(event, dialogRef.current);
43
+ };
44
+ window.addEventListener("keydown", handleDialogKeys);
45
+ return () => window.removeEventListener("keydown", handleDialogKeys);
46
+ }, [dialogRef, onClose]);
47
+ }
48
+
49
+ export function useVaultCommandLaunch({ intent, onRefresh, onTerminalLaunch }: {
50
+ intent: VaultDialogIntent;
51
+ onRefresh: () => Promise<void> | void;
52
+ onTerminalLaunch: () => void;
53
+ }): { launchStatus: VaultLaunchStatus; launchTerminal: () => Promise<void> } {
54
+ const [launchState, setLaunchState] = useState<{ intent: VaultDialogIntent; status: VaultLaunchStatus }>({ intent, status: "idle" });
55
+ const nativeResultTimeout = useRef<number | null>(null);
56
+ const timeoutIntent = useRef(intent);
57
+ const launchStatus = launchState.intent === intent ? launchState.status : "idle";
58
+ const setLaunchStatus = useCallback((status: VaultLaunchStatus) => setLaunchState({ intent, status }), [intent]);
59
+ const clearNativeResultTimeout = useCallback(() => clearScheduledTimeout(nativeResultTimeout), []);
60
+
61
+ useEffect(() => {
62
+ const handleNativeResult = (event: Event) => {
63
+ clearNativeResultTimeout();
64
+ const result = (event as CustomEvent<unknown>).detail;
65
+ setLaunchStatus(result === "opened" ? "opened" : "failed");
66
+ };
67
+ window.addEventListener("aidevops:vault-command-result", handleNativeResult);
68
+ return () => window.removeEventListener("aidevops:vault-command-result", handleNativeResult);
69
+ }, [clearNativeResultTimeout, setLaunchStatus]);
70
+
71
+ useEffect(() => {
72
+ if (timeoutIntent.current !== intent) {
73
+ clearNativeResultTimeout();
74
+ timeoutIntent.current = intent;
75
+ }
76
+ return clearNativeResultTimeout;
77
+ }, [clearNativeResultTimeout, intent]);
78
+
79
+ const launchTerminal = async () => {
80
+ const terminalAction = terminalActionForIntent(intent);
81
+ if (terminalAction === null) {
82
+ await onRefresh();
83
+ return;
84
+ }
85
+
86
+ const status = await requestVaultCommand(terminalAction);
87
+ setLaunchStatus(status);
88
+ if (status !== "failed") {
89
+ onTerminalLaunch();
90
+ }
91
+ if (status === "requesting") {
92
+ clearNativeResultTimeout();
93
+ nativeResultTimeout.current = scheduleNativeResultTimeout(intent, setLaunchState);
94
+ }
95
+ };
96
+
97
+ return { launchStatus, launchTerminal };
98
+ }
99
+
100
+ async function requestVaultCommand(action: NativeVaultAction): Promise<"requesting" | "copied" | "failed"> {
101
+ if (postNativeVaultCommand(action)) {
102
+ return "requesting";
103
+ }
104
+
105
+ const copied = await copyVaultCommand(action);
106
+ return copied ? "copied" : "failed";
107
+ }
108
+
109
+ async function copyVaultCommand(action: NativeVaultAction): Promise<boolean> {
110
+ let copied = false;
111
+ if (typeof navigator !== "undefined" && navigator.clipboard !== undefined) {
112
+ try {
113
+ await navigator.clipboard.writeText(vaultCommandText(action));
114
+ copied = true;
115
+ } catch {
116
+ copied = false;
117
+ }
118
+ }
119
+ return copied;
120
+ }
121
+
122
+ function scheduleNativeResultTimeout(intent: VaultDialogIntent, setLaunchState: (update: (current: { intent: VaultDialogIntent; status: VaultLaunchStatus }) => { intent: VaultDialogIntent; status: VaultLaunchStatus }) => void): number {
123
+ return window.setTimeout(() => {
124
+ setLaunchState((current) => current.intent === intent && current.status === "requesting" ? { ...current, status: "failed" } : current);
125
+ }, 3000);
126
+ }
127
+
128
+ function clearScheduledTimeout(timeoutRef: { current: number | null }): void {
129
+ if (timeoutRef.current !== null) {
130
+ window.clearTimeout(timeoutRef.current);
131
+ timeoutRef.current = null;
132
+ }
133
+ }
134
+
135
+ function trapDialogFocus(event: KeyboardEvent, dialog: HTMLElement | null): void {
136
+ if (event.key !== "Tab" || dialog === null) {
137
+ return;
138
+ }
139
+
140
+ const focusable = [...dialog.querySelectorAll<HTMLButtonElement>("button:not([disabled])")];
141
+ const first = focusable[0];
142
+ const last = focusable.at(-1);
143
+ if (first === undefined || last === undefined) {
144
+ return;
145
+ }
146
+ const activeIndex = focusable.indexOf(document.activeElement as HTMLButtonElement);
147
+ const targetIndex = dialogFocusTargetIndex(activeIndex, focusable.length, event.shiftKey);
148
+ if (targetIndex !== null) {
149
+ event.preventDefault();
150
+ focusable[targetIndex]?.focus();
151
+ }
152
+ }
153
+
154
+ export function dialogFocusTargetIndex(activeIndex: number, focusableCount: number, shiftKey: boolean): number | null {
155
+ if (focusableCount === 0) return null;
156
+ if (activeIndex < 0) return shiftKey ? focusableCount - 1 : 0;
157
+ if (shiftKey && activeIndex === 0) return focusableCount - 1;
158
+ if (!shiftKey && activeIndex === focusableCount - 1) return 0;
159
+ return null;
160
+ }
@@ -0,0 +1,27 @@
1
+ export type NativeVaultAction = "init" | "unlock" | "lock" | "status" | "lost-passphrase";
2
+
3
+ interface WebKitVaultCommandWindow extends Window {
4
+ webkit?: {
5
+ messageHandlers?: {
6
+ vaultCommand?: {
7
+ postMessage: (action: NativeVaultAction) => void;
8
+ };
9
+ };
10
+ };
11
+ }
12
+
13
+ export function postNativeVaultCommand(action: NativeVaultAction): boolean {
14
+ if (typeof window === "undefined") return false;
15
+ const handler = (window as WebKitVaultCommandWindow).webkit?.messageHandlers?.vaultCommand;
16
+ if (handler === undefined) return false;
17
+ try {
18
+ handler.postMessage(action);
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ export function vaultCommandText(action: NativeVaultAction): string {
26
+ return `aidevops vault ${action}`;
27
+ }
package/setup.sh CHANGED
@@ -12,7 +12,7 @@ shopt -s inherit_errexit 2>/dev/null || true
12
12
  # AI Assistant Server Access Framework Setup Script
13
13
  # Helps developers set up the framework for their infrastructure
14
14
  #
15
- # Version: 3.32.5
15
+ # Version: 3.32.7
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)
@@ -1435,6 +1435,7 @@ _setup_run_non_interactive() {
1435
1435
  # (GH#21102 / t2926: missing setsid kills workers on every pulse restart).
1436
1436
  _time_step "setup_setsid_advisory" setup_setsid_advisory
1437
1437
  _time_step "check_python_upgrade_available" check_python_upgrade_available
1438
+ _time_step "setup_vault_python_env" setup_vault_python_env || print_warning "Vault crypto runtime setup encountered issues; Vault status remains metadata-only"
1438
1439
  _time_step "set_permissions" set_permissions
1439
1440
  _time_step "migrate_old_backups" migrate_old_backups
1440
1441
  _time_step "migrate_loop_state_directories" migrate_loop_state_directories
@@ -1544,6 +1545,7 @@ _setup_run_non_interactive() {
1544
1545
  # prompt order around runtime-specific installers and config updates.
1545
1546
  _setup_run_interactive_runtime_tools() {
1546
1547
  confirm_step "Deploy aidevops agents to runtime agent directories" && deploy_agents_to_runtimes
1548
+ confirm_step "Setup isolated Vault crypto runtime" && setup_vault_python_env
1547
1549
  confirm_step "Setup Python environment (DSPy, crawl4ai)" && setup_python_env
1548
1550
  confirm_step "Setup Node.js environment" && setup_nodejs_env
1549
1551
  confirm_step "Install MCP packages globally (fast startup)" && install_mcp_packages
@@ -1,147 +0,0 @@
1
- import { useEffect, useMemo, useState, type ReactElement } from "react";
2
- import { FiAlertTriangle, FiCheckCircle, FiLock, FiShield, FiUnlock } from "react-icons/fi";
3
- import type { GuiVaultStatusData } from "@aidevops/gui-shared";
4
- import type { VaultDialogIntent } from "./VaultBadges";
5
-
6
- export function VaultPassphraseModal({ intent, onClose, vault }: {
7
- intent: VaultDialogIntent;
8
- onClose: () => void;
9
- vault: GuiVaultStatusData;
10
- }): ReactElement {
11
- const [step, setStep] = useState(0);
12
- const [passphrase, setPassphrase] = useState("");
13
- const [passphraseConfirm, setPassphraseConfirm] = useState("");
14
- const [unlockPassphrase, setUnlockPassphrase] = useState("");
15
- const [savedWarningAccepted, setSavedWarningAccepted] = useState(false);
16
- const setupMode = intent === "setup";
17
- const title = setupMode ? "Set Vault encryption passphrase" : intent === "unlock" ? "Unlock Vault" : "Lock Vault";
18
- const actionLabel = setupMode ? "Use passphrase and continue" : intent === "unlock" ? "Unlock Vault" : "Lock Vault";
19
- const setupPassphrasesMatch = passphrase.length > 0 && passphrase === passphraseConfirm;
20
- const finalPassphraseMatches = unlockPassphrase.length > 0 && unlockPassphrase === passphrase;
21
- const canContinue = useMemo(() => {
22
- if (!setupMode) {
23
- return intent === "lock" || unlockPassphrase.length > 0;
24
- }
25
-
26
- if (step === 0) {
27
- return setupPassphrasesMatch;
28
- }
29
- if (step === 1) {
30
- return savedWarningAccepted;
31
- }
32
- return finalPassphraseMatches;
33
- }, [finalPassphraseMatches, intent, savedWarningAccepted, setupMode, setupPassphrasesMatch, step, unlockPassphrase.length]);
34
-
35
- useEffect(() => {
36
- setStep(0);
37
- setPassphrase("");
38
- setPassphraseConfirm("");
39
- setUnlockPassphrase("");
40
- setSavedWarningAccepted(false);
41
- }, [intent]);
42
-
43
- const continueFlow = () => {
44
- if (setupMode && step < 2) {
45
- setStep((current) => current + 1);
46
- return;
47
- }
48
-
49
- onClose();
50
- };
51
-
52
- return (
53
- <div className="vault-modal-backdrop" role="presentation">
54
- <section
55
- aria-label={title}
56
- aria-modal="true"
57
- className="vault-modal"
58
- role="dialog"
59
- >
60
- <form
61
- onSubmit={(event) => {
62
- event.preventDefault();
63
- if (canContinue) {
64
- continueFlow();
65
- }
66
- }}
67
- style={{ display: "contents" }}
68
- >
69
- <header className="vault-modal-header">
70
- <span className="vault-modal-icon" aria-hidden="true">{intent === "lock" ? <FiLock /> : <FiUnlock />}</span>
71
- <div>
72
- <p className="eyebrow">aidevops Vault</p>
73
- <h2>{title}</h2>
74
- </div>
75
- </header>
76
- {setupMode ? <SetupStep accepted={savedWarningAccepted} confirm={passphraseConfirm} match={setupPassphrasesMatch} onAcceptChange={setSavedWarningAccepted} onConfirmChange={setPassphraseConfirm} onPassphraseChange={setPassphrase} onUnlockPassphraseChange={setUnlockPassphrase} passphrase={passphrase} step={step} unlockMatch={finalPassphraseMatches} unlockPassphrase={unlockPassphrase} /> : <UnlockStep intent={intent} onUnlockPassphraseChange={setUnlockPassphrase} unlockPassphrase={unlockPassphrase} vault={vault} />}
77
- <footer className="vault-modal-actions">
78
- <button className="secondary-action" onClick={onClose} type="button">Cancel</button>
79
- <button className="primary-action" disabled={!canContinue} type="submit">{setupMode && step < 2 ? "Continue" : actionLabel}</button>
80
- </footer>
81
- <p className="vault-modal-footnote">Passphrases stay in this local dialog state only and are never shown in logs, issues, command arguments, or AI chat.</p>
82
- </form>
83
- </section>
84
- </div>
85
- );
86
- }
87
-
88
- function SetupStep({ accepted, confirm, match, onAcceptChange, onConfirmChange, onPassphraseChange, onUnlockPassphraseChange, passphrase, step, unlockMatch, unlockPassphrase }: {
89
- accepted: boolean;
90
- confirm: string;
91
- match: boolean;
92
- onAcceptChange: (accepted: boolean) => void;
93
- onConfirmChange: (value: string) => void;
94
- onPassphraseChange: (value: string) => void;
95
- onUnlockPassphraseChange: (value: string) => void;
96
- passphrase: string;
97
- step: number;
98
- unlockMatch: boolean;
99
- unlockPassphrase: string;
100
- }): ReactElement {
101
- if (step === 1) {
102
- return (
103
- <div className="vault-modal-body">
104
- <div className="notice warning-notice" role="alert"><FiAlertTriangle aria-hidden="true" /> Save this passphrase in a password manager now. aidevops cannot recover encrypted data if it is lost.</div>
105
- <label className="vault-confirm-check"><input checked={accepted} onChange={(event) => onAcceptChange(event.currentTarget.checked)} type="checkbox" /> I have saved the Vault passphrase in a password manager and understand recovery is not possible without it.</label>
106
- </div>
107
- );
108
- }
109
-
110
- if (step === 2) {
111
- return (
112
- <div className="vault-modal-body">
113
- <p>Final safety check: enter the newly saved passphrase once more before any confidential app data is encrypted.</p>
114
- <label className="vault-field"><span>Use saved passphrase</span><input autoComplete="current-password" onChange={(event) => onUnlockPassphraseChange(event.currentTarget.value)} type="password" value={unlockPassphrase} /></label>
115
- {unlockPassphrase.length > 0 ? <p className={unlockMatch ? "vault-valid" : "vault-invalid"}>{unlockMatch ? "Passphrase matches." : "Passphrase does not match the saved setup value."}</p> : null}
116
- </div>
117
- );
118
- }
119
-
120
- return (
121
- <div className="vault-modal-body">
122
- <p>Create the encryption passphrase for this local Vault. Use a long unique value generated by your password manager.</p>
123
- <label className="vault-field"><span>New passphrase</span><input autoComplete="new-password" autoFocus onChange={(event) => onPassphraseChange(event.currentTarget.value)} type="password" value={passphrase} /></label>
124
- <label className="vault-field"><span>Repeat passphrase</span><input autoComplete="new-password" onChange={(event) => onConfirmChange(event.currentTarget.value)} type="password" value={confirm} /></label>
125
- {confirm.length > 0 ? <p className={match ? "vault-valid" : "vault-invalid"}>{match ? "Passphrases match." : "Passphrases must match before continuing."}</p> : null}
126
- </div>
127
- );
128
- }
129
-
130
- function UnlockStep({ intent, onUnlockPassphraseChange, unlockPassphrase, vault }: {
131
- intent: VaultDialogIntent;
132
- onUnlockPassphraseChange: (value: string) => void;
133
- unlockPassphrase: string;
134
- vault: GuiVaultStatusData;
135
- }): ReactElement {
136
- if (intent === "lock") {
137
- return <div className="vault-modal-body"><p><FiLock aria-hidden="true" /> Locking forgets in-memory keys for this local session. Protected previews will be hidden again.</p></div>;
138
- }
139
-
140
- return (
141
- <div className="vault-modal-body">
142
- <p><FiShield aria-hidden="true" /> Enter your Vault passphrase to unlock protected previews and write actions for this local session.</p>
143
- <label className="vault-field"><span>Vault passphrase</span><input autoComplete="current-password" autoFocus onChange={(event) => onUnlockPassphraseChange(event.currentTarget.value)} type="password" value={unlockPassphrase} /></label>
144
- <div className="notice compact-notice" role="note"><FiCheckCircle aria-hidden="true" /> {vault.unlock_hint}</div>
145
- </div>
146
- );
147
- }