Skip to content

VT.app: menu-bar bundle, wrap-v2 key migration, cache-hit + ui-status, lock key-wipe#6

Merged
timqi merged 12 commits into
mainfrom
feat/vt-app-bundle
Jul 22, 2026
Merged

VT.app: menu-bar bundle, wrap-v2 key migration, cache-hit + ui-status, lock key-wipe#6
timqi merged 12 commits into
mainfrom
feat/vt-app-bundle

Conversation

@timqi

@timqi timqi commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Turns the vt CLI into a signed macOS .app bundle with a menu-bar shell, and adds the agent-side capabilities that make it useful — while keeping the bare vt binary as a first-class artifact (CLI / Linux / scripting).

What's here

Agent (bde579a)

  • 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 Keychain store. KeychainStore.wrap_v versions it; v1 stores upgrade transparently at agent startup under the store flock (touches only encrypted_passphrase), and vt secret rebind [--old-bin-path|--to-v1] covers manual migration.
  • Cache-hit transparency: a grant reuse that skips Touch ID fires a native notification (op + scope + remaining TTL), fire-and-forget after permit.commit() so it never stalls revocation. Bundle-aware notify_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.
  • Lock/sleep now wipe 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 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, SMAppService login item), status + live grants + revoke-all, configurable & visible cache / idle-timeout submenus, and VTApp notify helper mode. Icon is the Worker-PWA hex-key as a template image.
  • just app / just install-app packaging (swiftc + icns + codesign, ad-hoc or VT_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-hoc VT-app-darwin-arm64-<tag>.tar.gz alongside the bare vt tarball (VT_APP_BIN reuses the built binary, no second compile). Linux unchanged.

Verification

  • cargo test (296 pass), host + x86_64-unknown-linux-gnu compile, swiftc compile.
  • just app both paths; ad-hoc signature survives the tar.gz round-trip; CLI tar xzf sidesteps Gatekeeper quarantine.
  • Two codex-expert design reviews (wrap-v2/ui-status/notify; lock key-wipe + idle menu) applied, and a 4-agent /simplify pass.

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 vt at another path is wrap v1; if the transparent upgrade can't unwrap after the move, run vt secret rebind --old-bin-path <old path> once (surfaced in the menu on failure).

🤖 Generated with Claude Code

timqi added 12 commits July 21, 2026 16:47
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
…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.
@timqi
timqi merged commit 8a3b18e into main Jul 22, 2026
4 checks passed
@timqi
timqi deleted the feat/vt-app-bundle branch July 22, 2026 08:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant