VT.app: menu-bar bundle, wrap-v2 key migration, cache-hit + ui-status, lock key-wipe#6
Merged
Conversation
Route raw SSH signing, sign@vt, decrypt@vt, auth@vt, and run@vt through one platform-neutral authorization engine (src/core/authorization.rs) with a macOS adapter, replacing the duplicated prompt/cache state machines in ssh_agent.rs. - Grants are keyed Operation x SubjectId x ResourceDigest; Fresh is a first-class policy for auth@vt / run@vt / legacy decrypt. - A non-cloneable permit commits the reusable grant only after the protected operation and, for extensions, response encryption succeed; every failure path drops without writing. - Lock, idle, screen-lock, and wake invalidation advance an authorization epoch even when the grant store is empty, so an in-flight prompt cannot recreate a revoked grant. - TTL policy is stored per grant: tightening the policy requires a fresh approval; strict dual-clock expiry and non-sliding re-grant semantics are preserved. - security.rs: split load_mac_key out of load_mac_cipher and preflight key material via validate_mac_key_material so raw key bytes are not held across a human prompt. - Docs: add unified-authorization-engine.md; update diag/sign-vt/ structured-errors docs and CLAUDE.md invariants.
…n, two duration knobs, no-root=>fresh, doctor version skew, unbound-non-ssh workspace arm
…ernel workspaces, mode flags removed - raw SSH signs bind to the [email protected] destination host key (plaintext extension intercepted before the VT_AUTH cipher path); forwarding-capable, tainted, and unbound-ssh connections are never cached - local sign@vt / decrypt@vt / ssh-keygen signing bind to the caller's kernel-derived .git workspace root ((dev,ino) + canonical path from one fd) - relay traffic stays confined per connection (unchanged V1 digests) - --ssh-auth-cache-mode / --decrypt-auth-cache-mode deleted; the two duration flags remain, defaulting to 0 = engine Fresh policy - Touch ID prompts state the reuse scope (destination / workspace / relay) whenever an approval can create a grant; known_hosts name shown best-effort - diag@vt: DiagCacheReport drops mode, ContextBasis reworked to V2 scope classification; vt doctor warns on agent/client version skew - proc_info gains get_cwd (PROC_PIDVNODEPATHINFO); session-leader/app-ancestor helpers and the TTY gate are deleted with the mode classifier - docs: authorization-scopes-v2.md (codex-reviewed), README auth-caching rewrite incl. read-only-credential guidance, diag/sign-vt/ssh-vt/engine doc updates, CLAUDE.md invariants
…oling, bind refusal semantics, caller-scoped diag, prompt padding - sign@vt/decrypt@vt from an ssh peer are confined per connection (never the workspace arm): a remote host behind plain ssh -A can no longer ride local workspace grants; new ContextBasis::SshConnection - workspace roots at $HOME (dotfiles repos) or above it are rejected => Fresh; find_git_root is depth-capped - unverifiable session-binds (bad signature, cert/p521 host keys) are refused WITHOUT poisoning state; only consistency violations taint; the 16-id cap refuses without erasing an established binding (OpenSSH parity); bind intercept moved before the agent-lock check - diag live_entries counts only grants the caller's own classification could reuse; DiagRes/CLAUDE.md/diag-design wording aligned - prompt reuse line moved before client-controlled body/meta (padding defense, same rationale as the relay origin marker) - AuthCacheTtls named-field struct restores sign/decrypt TTL swap protection - destination label memoized at bind time; peer exe captured once per connection; doctor version-skew message no longer double-prefixes 'v' - stale resolve_cache_context references cleaned from ssh_sign.rs and diag-design.md; README ssh -A row corrected
- known_hosts display lookup uses ssh-key's KnownHosts parser (drops the hand-rolled field/base64 parsing; @revoked/@cert-authority now skipped; hashed entries skipped without masking a later plain-name match) - workspace resolution is lazy (OnceLock, first use): filesystem I/O on a peer-controlled cwd no longer runs on the accept loop where a hung mount would stall every client - ReusePolicy::from_ttl_secs lives on the type (0 = Fresh rule travels with the engine); GrantScope::sign_workspace/decrypt_workspace take a plain SubjectId (a resolved workspace always has one); GrantScope::is_reusable pins the reuse-line pairing invariant in tests - AuthCacheTtls carried intact through factory and session (no more bare u64 pairs); Lookup.miss_count dead field removed; Workspace.root stored as validated String (no lossy fallback into the digest) - shared helpers: nul_terminated_path, reusable_workspace, live_grants (single basis→subject mapping for diag), connection_label; raw_sign_scope matches Bound fields directly; peer_exe borrowed not cloned per sign - test coverage: ContextBasis::SshConnection wire round-trip; reuse-label iff reusable-scope invariant across all classification arms
… sentences, README scope table
…udit context, worker display Touch ID prompts: raw-sign destination/forwarding/taint truth lines, unified ssh-sign layout, exception-display caller line, unmarked home-contracted workspace reuse labels. Agent audit push gains seven agent-authoritative fields (peer_exe/key_fp/dest/scope_family/scope_label/grant_ttl_s/relayed) with NULL-preserving worker ingest and additive audit columns. Worker surfaces: verified-IP labeling, batch size on the approve page and in approval notifications, via: parent-process line, honest cache-consent copy. Display bodies shortened at the source (op: inject dropped, cmd/ppid_cmd basename+capped). Design record: docs/approval-transparency.md.
Agent-side changes supporting the VT.app bundle: - Master-key wrap v2: derivation drops the binary-path term for a fixed label so moving the binary (into VT.app) no longer locks the store. `KeychainStore.wrap_v` marks the version; v1 stores upgrade transparently at agent startup under the store flock (touches only encrypted_passphrase), and `vt secret rebind [--old-bin-path|--to-v1]` handles manual migration. - Cache-hit transparency: a grant reuse that skips Touch ID fires a native notification (op + scope + remaining TTL), thrown fire-and-forget only after permit.commit() so it never stalls revocation. NotifyKind::CacheHit, bundle-aware notify_macos (VTApp helper, osascript fallback), shared sanitize. - ui-status@vt: token-gated (spawn token over --ui-token-fd) plaintext read/revoke channel for the shell; whole-store grant snapshot with human labels, dispatched before the lock check and VT_AUTH cipher path, never resets the idle clock. - Lock/sleep now wipe decrypted SSH keys from RAM (not just grants); ensure_keys_loaded refuses to reload while the screen is locked, checked before and after the keychain read. Idle timeout default raised to 2h, floored at 60s, and reported over ui-status. - [agent] config section (timeout, cache durations, cache_hit_notify, run_allow) as spawn defaults; flag > config > default. Agent CLI flags made Option<T> for that precedence.
- app/VTShell.swift: single-file AppKit menu-bar shell. Sole agent lifecycle owner (spawns `vt ssh agent --ui-token-fd`, supervises, crash/version-skew restart, SMAppService login item). Shows agent status, live grants with remaining TTL, revoke-all, and configurable+visible cache/idle-timeout submenus (persisted to UserDefaults, applied as spawn flags). Also serves `VTApp notify` helper mode (bundle-identity native notifications). Speaks ui-status@vt over ~/.ssh/vt.sock off the main thread. Icon is the Worker-PWA hex-key redrawn as a template NSImage. - app/Info.plist: LSUIElement bundle, id dev.rustyvault.vt; executable "VTApp" (APFS case-insensitivity vs the "vt" CLI beside it). - justfile: `app` (build + swiftc + icns from cf-worker/pwa/icon-512.png + codesign, ad-hoc or VT_CODESIGN_ID) and `install-app` (to /Applications, symlink ~/.local/bin/vt into the bundle). - .gitignore: /build (bundle output).
- docs/app-bundle.md: full design (R3) — bundle layout, wrap-v2 migration + rebind, native notifications, [agent] config, ui-status@vt token channel, Swift shell, migration runbook, and §10 key-wipe-on-lock + configurable idle timeout (with the codex-review fixes folded in). - CLAUDE.md: new invariants — ui-status@vt as the one whole-store channel, wrap versioning, notify-after-commit, lock/idle key-wipe + interactive reload guard, idle floor. - authorization-scopes-v2.md: idle-timeout rationale (2h default, lock/sleep as the fast guard). - README.md: `just install-app` + rebind note. config.example.toml: [agent] section. docs/README.md: index row for app-bundle.md.
The macOS release job now also builds VT.app (via `just app`, reusing the already-built release binary through VT_APP_BIN so there's no second compile) and publishes VT-app-darwin-arm64-<tag>.tar.gz alongside the bare `vt` tarball. Linux and the bare macOS binary are unchanged. Ad-hoc signed (no Developer ID yet): tar.gz matches the binary artifacts and CLI extraction sidesteps the Gatekeeper quarantine that Finder/zip would propagate; the ad-hoc signature survives the tar round-trip. README documents the download → /Applications → CLI-symlink flow and the xattr fallback. `just app` gains VT_APP_BIN to reuse a prebuilt binary.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Turns the
vtCLI into a signed macOS.appbundle with a menu-bar shell, and adds the agent-side capabilities that make it useful — while keeping the barevtbinary as a first-class artifact (CLI / Linux / scripting).What's here
Agent (
bde579a)VT.appno longer locks the Keychain store.KeychainStore.wrap_vversions it; v1 stores upgrade transparently at agent startup under the store flock (touches onlyencrypted_passphrase), andvt secret rebind [--old-bin-path|--to-v1]covers manual migration.permit.commit()so it never stalls revocation. Bundle-awarenotify_macos(VTApp helper → osascript fallback).ui-status@vt: token-gated (spawn token over--ui-token-fd) plaintext read/revoke channel for the shell; whole-store grant snapshot with human labels; dispatched before the lock check and VT_AUTH cipher path; never resets the idle clock.ensure_keys_loadedrefuses to reload while the screen is locked (checked before and after the keychain read). Idle timeout default 2h, floored at 60s, reported over ui-status.[agent]config section (timeout, cache durations, cache_hit_notify, run_allow) as spawn defaults;flag > config > default.App (
e08c602)app/VTShell.swift: single-file AppKit menu-bar shell — sole agent lifecycle owner (spawn/supervise/restart,SMAppServicelogin item), status + live grants + revoke-all, configurable & visible cache / idle-timeout submenus, andVTApp notifyhelper mode. Icon is the Worker-PWA hex-key as a template image.just app/just install-apppackaging (swiftc + icns + codesign, ad-hoc orVT_CODESIGN_ID).Docs (
f2fc69e) —docs/app-bundle.md(full design, R3, two codex-expert reviews folded in), CLAUDE.md invariants, README, config.example, authorization-scopes-v2.Release (
1e2cfea) — macOS job also ships an ad-hocVT-app-darwin-arm64-<tag>.tar.gzalongside the barevttarball (VT_APP_BINreuses the built binary, no second compile). Linux unchanged.Verification
cargo test(296 pass), host +x86_64-unknown-linux-gnucompile,swiftccompile.just appboth paths; ad-hoc signature survives the tar.gz round-trip; CLItar xzfsidesteps Gatekeeper quarantine./simplifypass.Known limitation
Ad-hoc signed: each release re-triggers the one-time Keychain prompt on first launch (signatures differ per build). Developer ID + notarization is a drop-in
VT_CODESIGN_ID+ notarize step, documented in §1.Migration note
An existing store created by a
vtat another path is wrap v1; if the transparent upgrade can't unwrap after the move, runvt secret rebind --old-bin-path <old path>once (surfaced in the menu on failure).🤖 Generated with Claude Code