# AGENTS.md — AI Agent Project Guide
## Project Overview
Authy is a CLI secrets store & dispatch tool for AI agents. Built in Rust. Single binary, no server, no accounts. It stores encrypted secrets locally and dispatches them to agents with policy-based scoping, short-lived session tokens, and audit logging.
**Tech Stack:** Rust 2021 edition, clap (CLI), age (encryption), MessagePack (serialization), HMAC-SHA256 (session tokens).
## Build & Test Commands
```bash
cargo build # dev build
cargo build --release # release build (binary at target/release/authy)
cargo test # run all tests (unit + integration)
cargo test --test integration # run integration tests only
cargo test <test_name> # run a single test
cargo clippy -- -D warnings # lint (must pass clean)
```
## Project Structure
```
src/
main.rs Entry point — parse CLI args, dispatch to handlers
error.rs AuthyError enum (thiserror), Result type alias
types.rs Common re-exports (serde, chrono, BTreeMap, PathBuf)
cli/ clap command definitions (one file per command)
mod.rs CLI struct with Subcommand enum
init.rs authy init — create vault, generate keyfile or passphrase
store.rs authy store — decrypt vault, insert secret, re-encrypt
get.rs authy get — decrypt vault, policy check, output to stdout
list.rs authy list — list secret names with optional scope filter
remove.rs authy remove — delete secret from vault
rotate.rs authy rotate — update secret value, bump version
policy.rs authy policy * — CRUD for scope policies
session.rs authy session * — create/list/revoke tokens
run.rs authy run — subprocess injection with scoped secrets
audit.rs authy audit * — show/verify/export audit log
config.rs authy config — show configuration
vault/ Encrypted vault storage
mod.rs Vault struct, VaultKey enum, load_vault(), save_vault()
crypto.rs age encrypt/decrypt (passphrase + keyfile), HKDF derivation
secret.rs SecretEntry, SecretMetadata (with Zeroize)
auth/ Authentication dispatcher
mod.rs resolve_auth() — resolve credentials to VaultKey + AuthContext
context.rs AuthContext — carries resolved identity and permission level
policy/ Access control
mod.rs Policy struct, can_read() with globset matching
session/ Session token management
mod.rs SessionRecord, generate_token(), validate_token()
audit/ Audit logging
mod.rs AuditEntry, append_entry(), verify_chain()
subprocess/ Child process spawning
mod.rs Spawn child process with env var injection
config/ Configuration parsing
mod.rs authy.toml parsing
tests/integration/ Integration tests using assert_cmd + tempfile
```
## Code Style Guidelines
### Imports
- Group imports: std first, external crates second, crate modules third
- Use `crate::types::*` for common re-exports (Serialize, Deserialize, DateTime, BTreeMap, PathBuf)
- Example from `src/vault/mod.rs`:
```rust
use std::fs;
use crate::error::{AuthyError, Result};
use crate::policy::Policy;
use crate::types::*;
```
### Error Handling
- Use `AuthyError` enum from `src/error.rs` for all errors
- Return `Result<T>` type alias (maps to `std::result::Result<T, AuthyError>`)
- Use `?` operator with `map_err()` for external errors:
```rust
.map_err(|e| AuthyError::Serialization(e.to_string()))?
```
- User-facing error messages should be descriptive and actionable
### Types & Serialization
- Use `BTreeMap<String, T>` for ordered key-value storage (not HashMap)
- All persistent types must derive `Serialize, Deserialize` from serde
- DateTime fields use `DateTime<Utc>` from chrono
- Use `#[serde(default)]` for optional/empty Vec fields
### Naming Conventions
- Functions: `snake_case` (e.g., `load_vault`, `derive_key`)
- Types: `PascalCase` (e.g., `VaultKey`, `SecretEntry`)
- Module names: `snake_case` (one word preferred)
- CLI commands: kebab-case in help text, PascalCase in enum variants
### Documentation
- Use `///` doc comments on public items
- Brief one-line descriptions preferred
## Key Conventions
1. **Secret values flow through stdin/stdout**, never CLI arguments. This prevents secrets from appearing in shell history or process listings.
2. **Diagnostics go to stderr**, secret values to stdout. This enables safe piping.
3. **All secret-holding types must derive `Zeroize` and `ZeroizeOnDrop`**. Memory is wiped when the value is dropped.
4. **Session tokens are read-only** — no mutation commands accept token auth.
5. **Policy evaluation: deny overrides allow, default deny**.
6. **Vault writes use atomic rename** (write to .tmp, then rename) to prevent corruption.
7. **Policies are stored inside the encrypted vault** — cannot be tampered with without the master key.
## Testing
### Integration Tests
- Located in `tests/integration/`
- Use `assert_cmd` for CLI testing, `tempfile` for isolated home directories
- Each test creates a `TempDir` and sets `HOME` env var to isolate tests
- Helper pattern:
```rust
fn authy_cmd(home: &TempDir) -> Command {
let mut cmd = Command::cargo_bin("authy").unwrap();
cmd.env("HOME", home.path());
cmd.env_remove("AUTHY_PASSPHRASE");
cmd
}
```
### Test Naming
- `test_<command>_<scenario>` pattern (e.g., `test_store_and_get`, `test_init_twice_fails`)
## Security Considerations
### Crypto Stack
- `age` crate for vault encryption (passphrase via scrypt, keyfile via X25519)
- `hmac` + `sha2` for session token HMAC-SHA256
- `hkdf` for deriving session and audit keys from master key
- `subtle` for constant-time token comparison
- `rand::OsRng` for cryptographic random generation
### Secret Handling
- Secrets never appear in CLI arguments, shell history, or process argv
- Session tokens have a scannable `authy_v1.` prefix for leak detection
- Audit log uses HMAC chain — any modification breaks the chain
- All secret-holding types are zeroized on drop
### File Layout
```
~/.authy/
vault.age Encrypted vault (secrets + policies + sessions)
audit.log Append-only audit log (JSONL)
authy.toml Configuration (optional)
keys/
master.key age identity (private key)
```
## Data Flow Examples
### Store a secret (admin, master key)
```
authy store db-url
→ auth: resolve passphrase/keyfile → VaultKey
→ vault: load_vault(key) → decrypt vault.age → Vault in memory
→ read secret value from stdin
→ insert into vault.secrets["db-url"]
→ vault: save_vault(vault, key) → serialize → encrypt → atomic write
→ audit: append SecretWrite entry
```
### Get a secret (agent, session token)
```
authy get db-url (with AUTHY_TOKEN + AUTHY_KEYFILE)
→ auth: resolve token + keyfile → VaultKey + session scope
→ vault: load_vault(key) → decrypt vault.age
→ session: validate_token() → find matching session → get scope
→ policy: scope.can_read("db-url") → allow/deny
→ if allowed: write secret value to stdout
→ audit: append SecretRead entry (GRANTED or DENIED)
```