Skip to content

Commit dc49b51

Browse files
UnbreakableMJclaude
andcommitted
feat(vault-tui): in-place unlock screen (master password / PIN)
When the agent is locked, the TUI no longer dead-ends at a "run vault unlock" banner — it shows an interactive unlock prompt for the registered account. Type the master password, or Tab to a PIN when one is enrolled, and drop straight into the browser. Reuses the readline TextInput (secret masked with bullets) and the existing Unlock/UnlockPin requests — no protocol or agent changes. Failed unlocks show the error (incl. BadPin { attempts_remaining } / PinLockedOut) and clear the field; Esc quits. A locked agent reports server/email as None, so the TUI needs the account from the registered profile. config.rs is extracted from vault-cli into a shared vault-config crate, now used by both the CLI and the TUI (single source of truth for config.toml; the agent still doesn't read config). The locked screen reads vault_config::load().account() and queries PinStatus to decide whether to offer the PIN toggle. Tests: vault-config's units moved with it; new vault-tui units for UnlockState::request (password vs PIN), toggle_pin (no-op without enrollment, clears on switch), unlock_failed, and a TestBackend smoke (account shown, secret masked, Tab hint only when a PIN is enrolled). 148 workspace tests green. No new external dependencies. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 075c35c commit dc49b51

13 files changed

Lines changed: 393 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ range may break in any release.
1010

1111
### Added
1212

13+
- **TUI in-place unlock.** When the agent is locked, `vault-tui` no longer
14+
dead-ends at a "run `vault unlock`" banner — it shows an interactive unlock
15+
prompt for the registered account: type the master password, or `Tab` to a
16+
PIN when one is enrolled, and drop straight into the browser. Reuses the
17+
readline `TextInput` (secret masked with ``) and the existing
18+
`Unlock`/`UnlockPin` requests — no protocol or agent changes; failed unlocks
19+
show the error (incl. `BadPin { attempts_remaining }` / `PinLockedOut`) and
20+
clear the field.
21+
- To read the account (server/email/device_id) — which a *locked* agent
22+
doesn't report — `config.rs` was extracted from `vault-cli` into a shared
23+
**`vault-config`** crate, now used by both the CLI and the TUI (single
24+
source of truth for `config.toml`; the agent still doesn't read config).
25+
- Tests: `vault-config`'s units moved with it; new `vault-tui` units for
26+
`UnlockState::request` (password vs PIN), `toggle_pin` (no-op without
27+
enrollment, clears on switch), `unlock_failed`, and a `TestBackend` smoke
28+
that the unlock screen shows the account, masks the secret, and offers the
29+
`Tab` hint only when a PIN is enrolled. No new external dependencies.
30+
1331
- **Token persistence + refresh.** The OAuth2 refresh token is now kept and
1432
reused, so a cache/PIN/offline session can become fully capable and a
1533
long-lived session survives access-token expiry.

Cargo.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ members = [
1111
"crates/vault-cli",
1212
"crates/vault-tui",
1313
"crates/vault-theme",
14+
"crates/vault-config",
1415
]
1516

1617
[workspace.package]

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ vault pin status # enrolled? attempts remaining?
7272
vault pin disable # forget the PIN
7373
```
7474

75+
The TUI unlocks **in place**: when the agent is locked, `vault-tui` shows an
76+
unlock prompt for your registered account (master password, or `Tab` to a PIN
77+
when one is enrolled) instead of sending you back to the CLI.
78+
7579
A PIN session reads from the encrypted cache, and — once the network is
7680
reachable — transparently goes online for `sync` and edits by refreshing a
7781
stored token (no master password needed); only a genuinely offline box stays

crates/vault-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ tokio = { workspace = true }
3939
toml = { workspace = true }
4040
uuid = { workspace = true }
4141
zeroize = { workspace = true }
42+
vault-config = { path = "../vault-config" }
4243
vault-core = { path = "../vault-core" }
4344
vault-ipc = { path = "../vault-ipc" }
4445
vault-store = { path = "../vault-store" }

crates/vault-cli/src/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
1111
#![forbid(unsafe_code)]
1212

13-
mod config;
1413
mod spawn;
1514

15+
use vault_config as config;
16+
1617
use std::io::{self, BufRead, IsTerminal, Read, Write};
1718
use std::path::{Path, PathBuf};
1819

crates/vault-cli/src/spawn.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ pub async fn spawn_and_connect(socket: &Path) -> Result<UnixStream, String> {
7777
// env/default precedence. A manually launched agent is unaffected. A
7878
// malformed config shouldn't block bringing the agent up, so fall back to
7979
// no extra args (the agent then keeps its defaults).
80-
let extra = crate::config::load().map(|c| crate::config::agent_args(&c));
80+
let extra = vault_config::load().map(|c| vault_config::agent_args(&c));
8181
if let Err(msg) = &extra {
8282
eprintln!("vault: ignoring config for auto-spawn: {msg}");
8383
}

crates/vault-config/Cargo.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# SPDX-License-Identifier: GPL-3.0-or-later
2+
3+
[package]
4+
name = "vault-config"
5+
description = "Vault user configuration — typed config.toml (account profile + tunables)"
6+
version.workspace = true
7+
publish.workspace = true
8+
edition.workspace = true
9+
rust-version.workspace = true
10+
license.workspace = true
11+
authors.workspace = true
12+
homepage.workspace = true
13+
repository.workspace = true
14+
readme.workspace = true
15+
16+
[lints]
17+
workspace = true
18+
19+
[dependencies]
20+
dirs = { workspace = true }
21+
serde = { workspace = true }
22+
tempfile = { workspace = true }
23+
toml = { workspace = true }
24+
uuid = { workspace = true }
Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
//! whatever would have consumed it. The file lives at
1010
//! `$XDG_CONFIG_HOME/vault/config.toml`; a missing file reads as defaults.
1111
//!
12-
//! Only the CLI reads this today — it sources the agent's launch flags from it
13-
//! during auto-spawn (see `crate::spawn`). The registry is shaped to grow into
14-
//! the rest of PRD §7.1's keys without disturbing callers.
12+
//! Shared by the CLI (which sources the agent's auto-spawn launch flags and
13+
//! manages the keys via `vault config`) and the TUI (which reads the
14+
//! `[account]` profile to drive in-place unlock). The registry is shaped to
15+
//! grow into the rest of PRD §7.1's keys without disturbing callers.
1516
1617
use std::ffi::OsString;
1718
use std::io::Write;
@@ -111,8 +112,11 @@ impl Config {
111112
}
112113
}
113114

114-
/// Current value of `key` as a display string, `None` when unset. Errors
115-
/// (as the offending key) when `key` is not recognised.
115+
/// Current value of `key` as a display string, `None` when unset.
116+
///
117+
/// # Errors
118+
///
119+
/// Returns the offending key when `key` is not recognised.
116120
pub fn get(&self, key: &str) -> Result<Option<String>, String> {
117121
match key {
118122
"clipboard.clear_secs" => Ok(self.clipboard.clear_secs.map(|v| v.to_string())),

crates/vault-tui/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ tokio = { workspace = true }
2828
ratatui = { workspace = true }
2929
crossterm = { workspace = true }
3030
zeroize = { workspace = true }
31+
vault-config = { path = "../vault-config" }
3132
vault-core = { path = "../vault-core" }
3233
vault-ipc = { path = "../vault-ipc" }
3334
vault-theme = { path = "../vault-theme" }

0 commit comments

Comments
 (0)