gloves 0.5.11

seamless secret manager and handoff
Documentation
use std::os::unix::fs::PermissionsExt;

use chrono::{Duration, Utc};
use gloves::{
    audit::{AuditEvent, AuditLog},
    types::{AgentId, SecretId},
};

#[test]
fn log_writes_jsonl() {
    let temp_dir = tempfile::tempdir().unwrap();
    let log = AuditLog::new(temp_dir.path().join("audit.jsonl")).unwrap();

    log.log(AuditEvent::SecretExpired {
        secret_id: SecretId::new("a").unwrap(),
    })
    .unwrap();

    let text = std::fs::read_to_string(log.path()).unwrap();
    let line = text.lines().next().unwrap();
    let value: serde_json::Value = serde_json::from_str(line).unwrap();
    assert_eq!(value["event"], "secret_expired");
}

#[test]
fn log_appends() {
    let temp_dir = tempfile::tempdir().unwrap();
    let log = AuditLog::new(temp_dir.path().join("audit.jsonl")).unwrap();

    log.log(AuditEvent::SecretExpired {
        secret_id: SecretId::new("a").unwrap(),
    })
    .unwrap();
    log.log(AuditEvent::SecretRevoked {
        secret_id: SecretId::new("b").unwrap(),
        by: AgentId::new("agent-a").unwrap(),
    })
    .unwrap();

    let text = std::fs::read_to_string(log.path()).unwrap();
    assert_eq!(text.lines().count(), 2);
}

#[test]
fn all_events_serialize() {
    let events = vec![
        AuditEvent::SecretAccessed {
            secret_id: SecretId::new("a").unwrap(),
            by: AgentId::new("agent-a").unwrap(),
        },
        AuditEvent::SecretExpired {
            secret_id: SecretId::new("b").unwrap(),
        },
        AuditEvent::SecretCreated {
            secret_id: SecretId::new("c").unwrap(),
            by: AgentId::new("agent-b").unwrap(),
        },
        AuditEvent::SecretRevoked {
            secret_id: SecretId::new("d").unwrap(),
            by: AgentId::new("agent-c").unwrap(),
        },
        AuditEvent::RequestCreated {
            request_id: uuid::Uuid::new_v4(),
            secret_id: SecretId::new("human/token").unwrap(),
            requested_by: AgentId::new("agent-a").unwrap(),
            reason: "need deploy".to_owned(),
            expires_at: Utc::now() + Duration::minutes(10),
        },
        AuditEvent::RequestApproved {
            request_id: uuid::Uuid::new_v4(),
            secret_id: SecretId::new("human/token").unwrap(),
            requested_by: AgentId::new("agent-a").unwrap(),
            approved_by: AgentId::new("reviewer-a").unwrap(),
        },
        AuditEvent::RequestDenied {
            request_id: uuid::Uuid::new_v4(),
            secret_id: SecretId::new("human/token").unwrap(),
            requested_by: AgentId::new("agent-a").unwrap(),
            denied_by: AgentId::new("reviewer-b").unwrap(),
        },
        AuditEvent::VaultCreated {
            vault: "agent_data".to_owned(),
            owner: gloves::types::Owner::Agent,
        },
        AuditEvent::VaultMounted {
            vault: "agent_data".to_owned(),
            agent: AgentId::new("agent-a").unwrap(),
            ttl_minutes: 60,
        },
        AuditEvent::VaultUnmounted {
            vault: "agent_data".to_owned(),
            reason: "explicit".to_owned(),
            agent: AgentId::new("agent-a").unwrap(),
        },
        AuditEvent::VaultSessionExpired {
            vault: "agent_data".to_owned(),
        },
        AuditEvent::VaultHandoffPromptIssued {
            vault: "agent_data".to_owned(),
            requester: AgentId::new("agent-a").unwrap(),
            trusted_agent: AgentId::new("agent-b").unwrap(),
            requested_file: "docs/notes.txt".to_owned(),
        },
        AuditEvent::GpgKeyCreated {
            agent: AgentId::new("agent-c").unwrap(),
            fingerprint: "ABCDEF0123456789".to_owned(),
        },
        AuditEvent::CommandExecuted {
            by: AgentId::new("agent-z").unwrap(),
            interface: "cli".to_owned(),
            command: "list".to_owned(),
            target: Some("pending".to_owned()),
        },
    ];

    for event in events {
        let json = serde_json::to_string(&event).unwrap();
        assert!(serde_json::from_str::<serde_json::Value>(&json).is_ok());
    }
}

#[test]
fn log_file_permissions() {
    let temp_dir = tempfile::tempdir().unwrap();
    let log = AuditLog::new(temp_dir.path().join("audit.jsonl")).unwrap();

    let mode = std::fs::metadata(log.path()).unwrap().permissions().mode() & 0o777;
    assert_eq!(mode, 0o600);
}

#[test]
fn log_includes_timestamp() {
    let temp_dir = tempfile::tempdir().unwrap();
    let log = AuditLog::new(temp_dir.path().join("audit.jsonl")).unwrap();

    log.log(AuditEvent::SecretExpired {
        secret_id: SecretId::new("a").unwrap(),
    })
    .unwrap();

    let text = std::fs::read_to_string(log.path()).unwrap();
    let line = text.lines().next().unwrap();
    let value: serde_json::Value = serde_json::from_str(line).unwrap();
    assert!(value["timestamp"].as_str().unwrap().contains('T'));
}