11 unstable releases (3 breaking)
| new 0.9.0 | May 16, 2026 |
|---|---|
| 0.8.1 | May 14, 2026 |
| 0.7.2 | May 13, 2026 |
| 0.6.6 | May 9, 2026 |
| 0.0.1 |
|
#754 in Database interfaces
Used in 2 crates
185KB
4K
SLoC
gaze-audit
Passive audit sinks for Gaze metadata-only redaction logs
Part of the Gaze workspace — a reversible PII pseudonymization runtime for agentic LLM workflows.
Provides SqliteLogger - the concrete RedactionLogger implementation that writes
session-scoped redaction metadata to a local SQLite database. The audit log is
metadata-only: it records class, action, source, field metadata, and timestamp,
but never the original PII values or the pseudonymous tokens.
Safety-net leak suspects are written through the canonical
LeakSuspectLogger::log_leak_suspect trait method.
When to use this crate
Add gaze-audit when you need to:
- Query which PII classes were detected in a session
- Export audit records for compliance review
- Detect suspected misses via the SafetyNet log table (
SqliteLogger::query_safety_net)
Do not use the audit log to restore PII. The restore contract lives in SensitiveSnapshot
(owned by your application). Only the snapshot can reconstruct original values.
Usage
[dependencies]
gaze-pii = "0.9.0"
gaze-audit = "0.9.0"
Wire the logger when building the pipeline. Note: SqliteLogger is not Clone;
construct it where the pipeline is built and pass it directly.
use std::path::Path;
use gaze::Pipeline;
use gaze_audit::SqliteLogger;
let logger = SqliteLogger::new(Path::new("audit.db"))?;
let pipeline = Pipeline::builder()
// ... recognizers and rules ...
.redaction_logger(logger)
.build()?;
# Ok::<(), Box<dyn std::error::Error>>(())
Query metadata after running redactions. query is a static function on SqliteLogger
that takes a path; the logger may already be moved into the pipeline:
use std::path::Path;
use gaze_audit::{AuditFilter, SqliteLogger};
let rows = SqliteLogger::query(Path::new("audit.db"), &AuditFilter::default())?;
for row in &rows {
// metadata only - class, action, session_id, field metadata, timestamp; no raw PII
println!("{:?} {:?}", row.class, row.action);
}
# Ok::<(), Box<dyn std::error::Error>>(())
Every column emitted by SqliteLogger, the full closed-enum value sets for
decided_by / validator_fail_reason / fallback_triggered, and the
AuditFilter query dimensions are cataloged in
docs/metrics.md — that
doc is the SSOT for observable surfaces and includes stability guarantees
and the version each column landed.
Audit-query API surface
Adopters building dashboards, exports, or compliance views interact with four
public items beyond SqliteLogger:
| Item | Role |
|---|---|
AuditFilter |
Plain-struct filter builder. All fields are Option<_> and default to None, so AuditFilter::default() returns every row. Narrow by class, source, action, document_kind, field_path, session_id, from_epoch_ms / to_epoch_ms, snapshot scheme / alg / key version, plus the v0.7.x ambiguity columns described below. |
AuditLogRow |
Shape returned by SqliteLogger::query. Metadata only — class, action, field_name, document_kind, conflict_loser, decided_by, created_at, session_id, snapshot metadata, and the four v0.7.x ambiguity columns. No raw PII, no token values, no restore material. |
build_audit_query_sql |
Lower-level helper that constructs the (SQL, params) pair used by SqliteLogger::query. Takes column-presence booleans so callers querying older databases project NULL AS <missing_column> rather than failing. Exposed so external readers can run the same projection logic against a read replica without re-implementing the filter compiler. |
AUDIT_RESTRICTED_COLUMNS |
Canonical allowlist of columns the audit-query path may project. audit export and SqliteLogger::query select only from this set. redaction_log may grow columns over time (e.g. snapshot locators, replay hashes); restricting projection here is defense in depth so future schema additions never accidentally leak raw PII, token bytes, or document content. The clean path is forbidden from touching audit storage by the gaze_module_isolation Dylint lint; this constant is the matching read-side guard. |
Safety-net writes use the LeakSuspectLogger trait:
use std::path::Path;
use gaze_audit::{LeakSuspectLogEntry, LeakSuspectLogger, SqliteLogger};
# fn log(entry: &LeakSuspectLogEntry) -> gaze_audit::Result<()> {
let logger = SqliteLogger::new(Path::new("audit.db"))?;
logger.log_leak_suspect(entry)?;
# Ok(())
# }
Ambiguity side-channel columns (v0.7.2)
SqliteLogger's redaction_log migration adds four nullable columns and
AuditLogRow mirrors them as Option<String>:
validator_fail_reason— JSON-encoded closed enum (LuhnFailed,IbanMod97Failed,EmailRfcFailed,E164PhoneFailed) for validator-veto losers. Populated only on rows wheredecided_by = ValidatorVeto.ambiguity_record— JSON-encodedAmbiguityRecord(family-level class, losing candidate list, closedAmbiguityReason). Populated when the resolver fell back to a family-level token instead of a precise variant.collision_family— plain string identifier for the[recognizers.collision]family this row belongs to.NULLfor rows outside collision-family policy.collision_variant— plain string variant identifier within the family.NULLwhen the family-level fallback fired (no specific variant was emitted).
Migration is lazy and idempotent: SqliteLogger::new(path) runs
CREATE TABLE IF NOT EXISTS followed by PRAGMA table_info and
ALTER TABLE ADD COLUMN for any missing column. There is no schema-version
table; reopening an up-to-date database is a no-op.
AuditFilter exposes four matching filter fields (has_ambiguity,
ambiguity_reason, collision_family, collision_variant) and the CLI
surfaces them as --has-ambiguity, --ambiguity-reason <variant> (kebab
case, e.g. no-anchor), --collision-family <id>, and
--collision-variant <id> on gaze audit query and gaze audit export.
Full contract:
docs/architecture/ambiguity-side-channel.md.
Isolation gate
gaze core has no compile-time dependency on gaze-audit. The gaze_module_isolation
Dylint lint enforces this - the clean/tokenize path cannot accidentally import the audit
path. Wire SqliteLogger only in your application layer, never in library crates that
compose pipelines.
Feature flags
This crate has no optional features. It always depends on rusqlite.
MSRV
rust-version = "1.89" (matches the workspace).
Dependencies
~31MB
~512K SLoC