# === Signet Vault - Encrypted Local State with Tiered Access (src_vault.rs) v1 ===
#  Dependencies: aes_gcm, argon2, base64, hkdf, hmac, rand, rusqlite, serde, sha2, std::path, std::time
# Encrypted local state vault with tiered access control for AI agent tool call enforcement. Implements three security tiers: Tier 1 (unencrypted ledger/action log), Tier 2 (session-key encrypted session state), Tier 3 (compartment-key encrypted credentials requiring passphrase). Security model includes Argon2id-derived master key, device-encrypted session key file, brute-force protection via lockout after failed attempts, and policy file integrity via HMAC. Provides credential storage with metadata constraints, spending ledger, preflight task management, and violation tracking.

# Module invariants:
#   - SALT_LEN = 32 bytes
#   - KEY_LEN = 32 bytes (AES-256)
#   - NONCE_LEN = 12 bytes (AES-GCM)
#   - SESSION_TTL_SECS = 1800.0 (30 minutes)
#   - MAX_FAILED_ATTEMPTS = 5
#   - LOCKOUT_SECS = 300.0 (5 minutes)
#   - MAX_PREFLIGHT_CONSTRAINTS = 20
#   - DEFAULT_VIOLATION_THRESHOLD = 5
#   - Master key derived via Argon2id (memory-hard KDF)
#   - Session key and compartment key derived from master via HKDF-SHA256
#   - Tier 2 credentials encrypted with session_key
#   - Tier 3 credentials encrypted with compartment_key
#   - All encryption uses AES-256-GCM with random nonce prepended
#   - Policy HMAC uses session_key with 'signet-vault-check' domain separation
#   - Preflight HMAC uses session_key with 'signet-preflight:' prefix
#   - Session key cache encrypted with device-specific key (HKDF from machine-id + username)
#   - Ledger (Tier 1) stored unencrypted in SQLite
#   - Session spend tracking filtered to decision='ALLOW'
#   - Preflight constraints must have non-empty alternatives
#   - Constraint action must be 'DENY' or 'ASK'
#   - One-time credentials deleted after successful request_capability
#   - Expired credentials return None from get_credential
#   - Failed unlock attempts increment counter and trigger lockout at threshold
#   - Lockout auto-expires after LOCKOUT_SECS
#   - HMAC mismatch on preflight triggers warning and returns None
#   - Preflight lockout_until check auto-deactivates expired preflights

class VaultMeta:
    """Metadata for vault initialization and lockout tracking"""
    salt: String                             # required, Base64-encoded salt for key derivation
    key_check: String                        # required, Base64-encoded HMAC check value for passphrase verification
    created_at: f64                          # required, Unix epoch timestamp of vault creation
    failed_attempts: u32                     # required, Count of failed unlock attempts
    locked_until: f64                        # required, Unix epoch timestamp until which vault is locked

class CredentialMeta:
    """Metadata constraints for stored credentials"""
    domain: Option<String> = None            # optional, Domain scope restriction (e.g. 'amazon.com')
    purpose: Option<String> = None           # optional, Purpose restriction (e.g. 'purchase', 'api_access')
    max_amount: Option<f64> = None           # optional, Per-use amount cap
    one_time: bool                           # required, Invalidate after first use
    label: Option<String> = None             # optional, Human-readable label

class Preflight:
    """Preflight task with constraints and violation tracking"""
    id: String                               # required, Unique preflight identifier
    task: String                             # required, Task description
    risks: Vec<String>                       # required, List of identified risks
    constraints: Vec<SoftConstraint>         # required, Soft constraints to enforce
    submitted_at: u64                        # required, Unix timestamp of submission
    lockout_until: u64                       # required, Unix timestamp until which preflight is active
    violation_count: u32                     # required, Count of constraint violations
    escalated: bool                          # required, Whether violation threshold exceeded

class SoftConstraint:
    """Single constraint rule for preflight enforcement"""
    name: String                             # required, Constraint name
    tool_pattern: String                     # required, Tool name pattern to match
    conditions: Vec<String>                  # required, Conditions for constraint activation
    action: String                           # required, Action to take: 'DENY' or 'ASK'
    reason: String                           # required, Human-readable reason
    alternative: String                      # required, Suggested alternative (plan B)

class PreflightViolation:
    """Record of a constraint violation"""
    preflight_id: String                     # required, ID of violated preflight
    constraint_name: String                  # required, Name of violated constraint
    tool_name: String                        # required, Tool that triggered violation
    parameters_summary: String               # required, Summary of tool parameters
    alternative: String                      # required, Suggested alternative
    timestamp: u64                           # required, Unix timestamp of violation

class Vault:
    """Main vault instance with derived keys and database connection"""
    master_key: [u8; 32]                     # required, Argon2id-derived master key
    session_key: [u8; 32]                    # required, HKDF-derived session key for Tier 2
    compartment_key: [u8; 32]                # required, HKDF-derived compartment key for Tier 3
    db_path: PathBuf                         # required, Path to SQLite database
    session_start: f64                       # required, Unix epoch timestamp of session start

def now_epoch() -> f64:
    """
    Get current Unix epoch time as f64 seconds

    Postconditions:
      - Returns monotonically increasing timestamp

    Errors:
      - system_time_error (panic): SystemTime before UNIX_EPOCH
          message: unwrap on duration_since failed

    Side effects: reads system clock
    Idempotent: no
    """
    ...

def random_hex_id() -> String:
    """
    Generate a 32-character hex string from 16 random bytes

    Postconditions:
      - Returns 32-character lowercase hex string
      - Each call produces unique value with high probability

    Side effects: reads from system RNG
    Idempotent: no
    """
    ...

def derive_master_key(
    passphrase: &str,
    salt: &[u8],
) -> [u8; 32]:
    """
    Derive 32-byte master key from passphrase and salt using Argon2id

    Postconditions:
      - Returns deterministic key for same passphrase+salt
      - Key is cryptographically derived via Argon2id

    Errors:
      - argon2_failure (panic): Argon2 derivation fails
          message: Argon2 derivation failed

    Side effects: CPU-intensive computation (memory-hard)
    Idempotent: no
    """
    ...

def derive_subkey(
    master: &[u8; 32],
    purpose: &str,
) -> [u8; 32]:
    """
    Derive purpose-specific subkey from master key using HKDF-SHA256

    Postconditions:
      - Returns deterministic subkey for same master+purpose
      - Subkey cryptographically independent from other purposes

    Errors:
      - hkdf_expand_failure (panic): HKDF expand fails
          message: HKDF expand failed

    Side effects: none
    Idempotent: no
    """
    ...

def make_key_check(
    master: &[u8; 32],
) -> Vec<u8>:
    """
    Generate HMAC-SHA256 check value for master key verification

    Postconditions:
      - Returns deterministic check value for same master key
      - Check value can verify passphrase without exposing key

    Side effects: none
    Idempotent: no
    """
    ...

def encrypt(
    key: &[u8; 32],
    plaintext: &[u8],
) -> Vec<u8>:
    """
    Encrypt plaintext using AES-256-GCM with random nonce

    Postconditions:
      - Returns nonce (12 bytes) concatenated with ciphertext
      - Ciphertext is authenticated

    Errors:
      - encryption_failure (panic): AES-GCM encryption fails
          message: encryption failed

    Side effects: reads from system RNG for nonce generation
    Idempotent: no
    """
    ...

def decrypt(
    key: &[u8; 32],
    data: &[u8],
) -> Result<Vec<u8>, String>:
    """
    Decrypt AES-256-GCM ciphertext (nonce prepended)

    Postconditions:
      - On success, returns plaintext
      - On failure, returns error string

    Errors:
      - data_too_short (Result::Err): data.len() < 12
          message: data too short
      - decryption_failure (Result::Err): AES-GCM decryption or authentication fails
          message: decryption failed

    Side effects: none
    Idempotent: no
    """
    ...

def policy_hmac(
    session_key: &[u8; 32],
    policy_content: &str,
) -> String:
    """
    Compute HMAC-SHA256 of policy file content using session key

    Postconditions:
      - Returns base64-encoded HMAC
      - Deterministic for same key+content

    Side effects: none
    Idempotent: no
    """
    ...

def verify_policy_integrity(
    session_key: &[u8; 32],
    policy_path: &std::path::Path,
) -> bool:
    """
    Verify policy file HMAC signature. Fail-closed: missing HMAC with existing vault returns false.

    Postconditions:
      - Returns true if policy valid or absent
      - Returns false if HMAC missing (vault exists) or mismatched

    Side effects: reads policy file, reads .hmac file
    Idempotent: no
    """
    ...

def sign_policy(
    session_key: &[u8; 32],
    policy_path: &std::path::Path,
) -> Result<(), String>:
    """
    Sign policy file by writing HMAC to adjacent .hmac file

    Postconditions:
      - On success, .hmac file created/updated alongside policy

    Errors:
      - read_policy_error (Result::Err): Cannot read policy file
          message: read policy: {error}
      - write_hmac_error (Result::Err): Cannot write HMAC file
          message: write hmac: {error}

    Side effects: reads policy file, writes .hmac file
    Idempotent: no
    """
    ...

def new(
    master_key: [u8; 32],
    db_path: PathBuf,
) -> Vault:
    """
    Create new Vault instance from master key and database path. Initializes database schema and session.

    Postconditions:
      - Database schema initialized
      - Session keys derived
      - Session start timestamp loaded or created

    Errors:
      - db_open_failure (panic): Cannot open database
          message: open db
      - db_init_failure (panic): Cannot create schema
          message: init db

    Side effects: creates/opens SQLite database, creates tables if not exist, reads/writes session_state table
    Idempotent: no
    """
    ...

def session_key(
    self: &Vault,
) -> &[u8; 32]:
    """
    Get reference to session key

    Postconditions:
      - Returns immutable reference to session key

    Side effects: none
    Idempotent: no
    """
    ...

def init_db(
    self: &Vault,
) -> ():
    """
    Initialize SQLite database schema with tables for ledger, credentials, session_state, preflights, preflight_violations

    Postconditions:
      - All tables and indexes created if not exist

    Errors:
      - db_open_failure (panic): Cannot open database
          message: open db
      - schema_creation_failure (panic): Cannot execute schema SQL
          message: init db

    Side effects: creates/opens database, creates tables and indexes
    Idempotent: no
    """
    ...

def load_or_create_session_start(
    self: &Vault,
) -> f64:
    """
    Load session start timestamp from database or create new if expired/missing

    Postconditions:
      - Returns valid session start timestamp
      - Timestamp is within SESSION_TTL_SECS or newly created

    Side effects: reads session_state table, writes new session_start if expired/missing
    Idempotent: no
    """
    ...

def reset_session(
    self: &mut Vault,
) -> ():
    """
    Reset session start to current time, invalidating previous session spending totals

    Postconditions:
      - session_start set to current epoch
      - Database updated with new session_start

    Side effects: writes session_state table
    Idempotent: no
    """
    ...

def log_action(
    self: &Vault,
    tool: &str,
    decision: &str,
    category: &str,
    amount: f64,
    detail: &str,
) -> ():
    """
    Log tool action to ledger table (Tier 1: unencrypted)

    Postconditions:
      - Action logged to ledger table with current timestamp

    Side effects: writes ledger table
    Idempotent: no
    """
    ...

def session_spend(
    self: &Vault,
    category: &str,
) -> f64:
    """
    Get total allowed spending in category since session start

    Postconditions:
      - Returns sum of amounts where decision='ALLOW' and timestamp >= session_start

    Side effects: reads ledger table
    Idempotent: no
    """
    ...

def total_spend(
    self: &Vault,
    category: &str,
    since: f64,
) -> f64:
    """
    Get total allowed spending in category since given timestamp

    Postconditions:
      - Returns sum of allowed amounts since timestamp
      - Returns 0.0 if database unopenable

    Side effects: reads ledger table
    Idempotent: no
    """
    ...

def recent_actions(
    self: &Vault,
    limit: u32,
) -> Vec<serde_json::Value>:
    """
    Get last N actions from ledger in reverse chronological order

    Postconditions:
      - Returns up to limit actions as JSON objects
      - Ordered by id DESC (most recent first)

    Side effects: reads ledger table
    Idempotent: no
    """
    ...

def has_recent_allowed_action(
    self: &Vault,
    search: &str,
    within: u32,
) -> bool:
    """
    Check if any of the last N allowed actions match search term(s) in tool name or detail. Supports pipe-delimited OR.

    Postconditions:
      - Returns true if any term found in tool or detail columns
      - Returns false if no match or empty search

    Side effects: reads ledger table
    Idempotent: no
    """
    ...

def store_credential(
    self: &Vault,
    name: &str,
    value: &str,
    tier: u8,
) -> ():
    """
    Store credential with tier-based encryption (no expiry or metadata)

    Postconditions:
      - Credential encrypted and stored
      - Replaces existing credential with same name

    Side effects: writes credentials table, generates random nonce
    Idempotent: no
    """
    ...

def store_credential_with_expiry(
    self: &Vault,
    name: &str,
    value: &str,
    tier: u8,
    expires_at: Option<f64>,
) -> ():
    """
    Store credential with tier-based encryption and optional expiry

    Postconditions:
      - Credential stored with expiry timestamp

    Side effects: writes credentials table
    Idempotent: no
    """
    ...

def store_credential_full(
    self: &Vault,
    name: &str,
    value: &str,
    tier: u8,
    expires_at: Option<f64>,
    metadata: Option<&CredentialMeta>,
) -> ():
    """
    Store credential with full metadata and optional expiry

    Postconditions:
      - Credential stored with metadata serialized as JSON

    Errors:
      - db_write_failure (panic): Database write fails
          message: unwrap on execute failed

    Side effects: writes credentials table
    Idempotent: no
    """
    ...

def get_credential(
    self: &Vault,
    name: &str,
) -> Option<String>:
    """
    Retrieve and decrypt credential by name. Returns None if expired or not found.

    Postconditions:
      - Returns Some(value) if found, valid, and decrypts successfully
      - Returns None if not found, expired, or decryption fails

    Side effects: reads credentials table
    Idempotent: no
    """
    ...

def get_credential_meta(
    self: &Vault,
    name: &str,
) -> Option<CredentialMeta>:
    """
    Get credential metadata without retrieving value

    Postconditions:
      - Returns deserialized metadata if found and valid JSON

    Side effects: reads credentials table
    Idempotent: no
    """
    ...

def request_capability(
    self: &Vault,
    name: &str,
    domain: &str,
    amount: f64,
    purpose: &str,
) -> Result<String, String>:
    """
    Request scoped capability token for credential. Enforces domain, purpose, and amount constraints. Invalidates one-time credentials after use.

    Postconditions:
      - On success: returns credential value, logs action, deletes if one_time
      - On failure: returns error message

    Errors:
      - domain_mismatch (Result::Err): domain constraint violated
          message: Credential scoped to different domain
      - purpose_mismatch (Result::Err): purpose constraint violated
          message: Credential scoped to different purpose
      - amount_exceeded (Result::Err): amount > max_amount
          message: Amount exceeds cap
      - not_found_or_expired (Result::Err): Credential not found or expired
          message: Credential not found or expired

    Side effects: reads credentials table, writes ledger table, deletes credential if one_time
    Idempotent: no
    """
    ...

def delete_credential(
    self: &Vault,
    name: &str,
) -> bool:
    """
    Delete credential by name

    Postconditions:
      - Returns true if credential existed and was deleted
      - Returns false if not found or error

    Side effects: writes credentials table
    Idempotent: no
    """
    ...

def credential_exists(
    self: &Vault,
    name: &str,
) -> bool:
    """
    Check if credential exists by name

    Postconditions:
      - Returns true if credential found in database

    Side effects: reads credentials table
    Idempotent: no
    """
    ...

def list_credentials(
    self: &Vault,
) -> Vec<serde_json::Value>:
    """
    List all credentials with metadata (values not included)

    Postconditions:
      - Returns array of credential info objects

    Side effects: reads credentials table
    Idempotent: no
    """
    ...

def store_preflight(
    self: &Vault,
    preflight: &Preflight,
) -> Result<(), String>:
    """
    Store new preflight task with HMAC signature. Deactivates previous active preflight. Validates constraints.

    Postconditions:
      - On success: preflight stored as active, previous deactivated
      - HMAC signature computed and stored

    Errors:
      - too_many_constraints (Result::Err): constraints.len() > MAX_PREFLIGHT_CONSTRAINTS
          message: Too many constraints
      - empty_alternative (Result::Err): Any constraint has empty alternative
          message: Constraint has empty alternative
      - invalid_action (Result::Err): action not DENY or ASK
          message: action must be DENY or ASK
      - serialize_error (Result::Err): Cannot serialize preflight
          message: serialize: {error}
      - db_error (Result::Err): Database operation fails
          message: db operation: {error}

    Side effects: writes preflights table, updates active flag
    Idempotent: no
    """
    ...

def active_preflight(
    self: &Vault,
) -> Option<Preflight>:
    """
    Get currently active preflight with HMAC verification. Returns None if expired, tampered, or not found.

    Postconditions:
      - Returns Some(preflight) if active, valid HMAC, not expired
      - Returns None and deactivates if lockout expired
      - Logs warning if HMAC mismatch

    Side effects: reads preflights table, writes preflights table if lockout expired
    Idempotent: no
    """
    ...

def is_preflight_locked(
    self: &Vault,
) -> bool:
    """
    Check if active preflight is locked (cannot be replaced)

    Postconditions:
      - Returns true if lockout_until > now

    Side effects: reads preflights table
    Idempotent: no
    """
    ...

def log_preflight_violation(
    self: &Vault,
    violation: &PreflightViolation,
) -> Result<(), String>:
    """
    Record preflight violation. Increments violation_count. Sets escalated flag if threshold exceeded.

    Postconditions:
      - Violation logged
      - violation_count incremented
      - escalated set to true if count >= DEFAULT_VIOLATION_THRESHOLD

    Errors:
      - db_error (Result::Err): Database operation fails
          message: db operation: {error}

    Side effects: writes preflight_violations table, updates preflights table
    Idempotent: no
    """
    ...

def preflight_violations(
    self: &Vault,
    preflight_id: &str,
) -> Vec<PreflightViolation>:
    """
    Get violations for a preflight in reverse chronological order

    Postconditions:
      - Returns violations ordered by timestamp DESC

    Side effects: reads preflight_violations table
    Idempotent: no
    """
    ...

def preflight_history(
    self: &Vault,
    limit: u32,
) -> Vec<serde_json::Value>:
    """
    Get preflight history (all preflights) in reverse chronological order

    Postconditions:
      - Returns up to limit preflights as JSON objects

    Side effects: reads preflights table
    Idempotent: no
    """
    ...

def override_preflight(
    self: &Vault,
) -> Result<(), String>:
    """
    Deactivate active preflight (human override)

    Postconditions:
      - Active preflight deactivated

    Errors:
      - no_active_preflight (Result::Err): No active preflight found
          message: No active preflight to override.
      - db_error (Result::Err): Database operation fails
          message: deactivate: {error}

    Side effects: writes preflights table
    Idempotent: no
    """
    ...

def preflight_hmac(
    self: &Vault,
    payload: &str,
) -> String:
    """
    Compute HMAC-SHA256 for preflight payload using session key

    Postconditions:
      - Returns base64-encoded HMAC with 'signet-preflight:' prefix

    Side effects: none
    Idempotent: no
    """
    ...

def signet_dir() -> PathBuf:
    """
    Get signet directory path. Uses SIGNET_DIR env var if set, else ~/.signet

    Postconditions:
      - Returns path to signet directory

    Side effects: none
    Idempotent: no
    """
    ...

def dirs() -> PathBuf:
    """
    Get user home directory path. Falls back to '.' if HOME not set.

    Postconditions:
      - Returns home directory or current directory

    Side effects: none
    Idempotent: no
    """
    ...

def meta_path() -> PathBuf:
    """
    Get path to vault.meta file

    Postconditions:
      - Returns {signet_dir}/vault.meta

    Side effects: none
    Idempotent: no
    """
    ...

def db_path() -> PathBuf:
    """
    Get path to SQLite database file

    Postconditions:
      - Returns {signet_dir}/state.db

    Side effects: none
    Idempotent: no
    """
    ...

def session_key_path() -> PathBuf:
    """
    Get path to encrypted session key cache file

    Postconditions:
      - Returns {signet_dir}/.session_key

    Side effects: none
    Idempotent: no
    """
    ...

def vault_exists() -> bool:
    """
    Check if vault has been initialized

    Postconditions:
      - Returns true if vault.meta file exists

    Side effects: none
    Idempotent: no
    """
    ...

def pause_path() -> PathBuf:
    """
    Get path to pause_until file

    Postconditions:
      - Returns {signet_dir}/pause_until

    Side effects: none
    Idempotent: no
    """
    ...

def set_pause_file(
    pause_until: u64,
) -> ():
    """
    Set pause-until timestamp in file (no vault required)

    Postconditions:
      - pause_until file created/updated with timestamp

    Side effects: creates signet directory if needed, writes pause_until file
    Idempotent: no
    """
    ...

def is_paused_file() -> bool:
    """
    Check if paused via file. Auto-expires and cleans up expired pause.

    Postconditions:
      - Returns true if current time < pause_until
      - Deletes pause file if expired

    Side effects: reads pause_until file, may delete pause_until file
    Idempotent: no
    """
    ...

def clear_pause_file() -> ():
    """
    Remove pause file (resume)

    Postconditions:
      - pause_until file deleted if exists

    Side effects: deletes pause_until file
    Idempotent: no
    """
    ...

def pause_until_file() -> u64:
    """
    Get pause expiry timestamp from file (0 if not paused)

    Postconditions:
      - Returns pause_until timestamp or 0

    Side effects: reads pause_until file
    Idempotent: no
    """
    ...

def disabled_path() -> PathBuf:
    """
    Get path to disabled flag file

    Postconditions:
      - Returns {signet_dir}/disabled

    Side effects: none
    Idempotent: no
    """
    ...

def set_disabled_file() -> ():
    """
    Set disabled flag (bypass all rules including self-protection)

    Postconditions:
      - disabled file created

    Side effects: creates signet directory if needed, writes disabled file
    Idempotent: no
    """
    ...

def is_disabled_file() -> bool:
    """
    Check if fully disabled

    Postconditions:
      - Returns true if disabled file exists

    Side effects: none
    Idempotent: no
    """
    ...

def clear_disabled_file() -> ():
    """
    Remove disabled flag (re-enable)

    Postconditions:
      - disabled file deleted if exists

    Side effects: deletes disabled file
    Idempotent: no
    """
    ...

def device_key() -> [u8; 32]:
    """
    Derive device-specific key from machine ID and username using HKDF. Not a passphrase—prevents trivial session key file copying.

    Postconditions:
      - Returns deterministic key for this device+user
      - Falls back to default if machine ID unavailable

    Side effects: reads /etc/machine-id or /var/lib/dbus/machine-id, on macOS: runs ioreg command, reads USER env var
    Idempotent: no
    """
    ...

def encrypt_session_key(
    master_key: &[u8; 32],
) -> Vec<u8>:
    """
    Encrypt master key with device key for disk storage

    Postconditions:
      - Returns AES-256-GCM encrypted master key

    Side effects: reads device key, generates random nonce
    Idempotent: no
    """
    ...

def decrypt_session_key(
    encrypted: &[u8],
) -> Option<[u8; 32]>:
    """
    Decrypt session key file with device key

    Postconditions:
      - Returns Some(key) if decryption succeeds and length correct
      - Returns None on failure

    Side effects: reads device key
    Idempotent: no
    """
    ...

def setup_vault(
    passphrase: &str,
) -> Result<Vault, String>:
    """
    Initialize new vault with passphrase. Creates directory, generates salt, derives keys, writes metadata and encrypted session key cache.

    Postconditions:
      - On success: vault initialized, metadata written, session key cached, database initialized
      - Session key file has 0600 permissions on Unix

    Errors:
      - mkdir_error (Result::Err): Cannot create directory
          message: mkdir: {error}
      - write_meta_error (Result::Err): Cannot write vault.meta
          message: write meta: {error}
      - write_key_error (Result::Err): Cannot write session key cache
          message: write key: {error}

    Side effects: creates signet directory, writes vault.meta, writes .session_key, creates database
    Idempotent: no
    """
    ...

def unlock_vault(
    passphrase: &str,
) -> Result<Vault, String>:
    """
    Unlock existing vault with passphrase. Enforces brute-force lockout. Updates session key cache on success.

    Postconditions:
      - On success: returns vault, resets failed_attempts, updates session key cache
      - On failure: increments failed_attempts, sets lockout if threshold reached

    Errors:
      - no_vault (Result::Err): vault.meta not found
          message: No vault found. Run 'signet-eval setup' first.
      - locked_out (Result::Err): failed_attempts >= MAX_FAILED_ATTEMPTS and lockout active
          message: Vault locked for N more seconds
      - wrong_passphrase (Result::Err): key_check mismatch
          message: Wrong passphrase (N/5 attempts)

    Side effects: reads vault.meta, writes vault.meta on failed attempts, writes .session_key on success
    Idempotent: no
    """
    ...

def try_load_vault() -> Option<Vault>:
    """
    Attempt to load vault from cached session key (hook mode—no passphrase prompt). Verifies key against metadata.

    Postconditions:
      - Returns Some(vault) if session key valid and verified
      - Returns None if cache missing, invalid, or verification fails

    Side effects: reads .session_key, reads vault.meta
    Idempotent: no
    """
    ...

def make_test_vault(
    dir: &std::path::Path,
    passphrase: &str,
) -> Vault:
    """
    Test helper: create vault in temp directory

    Postconditions:
      - Returns initialized vault for testing

    Side effects: none
    Idempotent: no
    """
    ...

# ── REQUIRED EXPORTS ──────────────────────────────────
# Your implementation module MUST export ALL of these names
# with EXACTLY these spellings. Tests import them by name.
# __all__ = ['VaultMeta', 'CredentialMeta', 'Preflight', 'SoftConstraint', 'PreflightViolation', 'Vault', 'now_epoch', 'panic', 'random_hex_id', 'derive_master_key', 'derive_subkey', 'make_key_check', 'encrypt', 'decrypt', 'Result::Err', 'policy_hmac', 'verify_policy_integrity', 'sign_policy', 'new', 'session_key', 'init_db', 'load_or_create_session_start', 'reset_session', 'log_action', 'session_spend', 'total_spend', 'recent_actions', 'has_recent_allowed_action', 'store_credential', 'store_credential_with_expiry', 'store_credential_full', 'get_credential', 'get_credential_meta', 'request_capability', 'delete_credential', 'credential_exists', 'list_credentials', 'store_preflight', 'active_preflight', 'is_preflight_locked', 'log_preflight_violation', 'preflight_violations', 'preflight_history', 'override_preflight', 'preflight_hmac', 'signet_dir', 'dirs', 'meta_path', 'db_path', 'session_key_path', 'vault_exists', 'pause_path', 'set_pause_file', 'is_paused_file', 'clear_pause_file', 'pause_until_file', 'disabled_path', 'set_disabled_file', 'is_disabled_file', 'clear_disabled_file', 'device_key', 'encrypt_session_key', 'decrypt_session_key', 'setup_vault', 'unlock_vault', 'try_load_vault', 'make_test_vault']
