How ai-memory Audit Log Attributes Writes to Specific DB Users vs Root Operators

The ai-memory audit log distinguishes root operators from database users by storing NULL in the author_id column for root-level actions and the user's 16-byte UserId for authenticated operations.

The ai-memory project implements a comprehensive audit trail that tracks every mutating operation through a centralized audit_log table. Understanding how this system attributes writes to either anonymous root operators or specific database users is essential for security auditing and compliance in multi-user deployments. This article examines the implementation details in the Rust source code, including the audit helper functions, writer interfaces, and reader resolution mechanisms.

The Audit Log Schema and author_id Design

The foundation of user attribution lies in the nullable author_id column defined in crates/ai-memory-store/src/lib.rs【/cache/repos/github.com/akitaonrails/ai-memory/main/crates/ai-memory-store/src/lib.rs#L216-L224】.

This column serves as a foreign key to the users table but is explicitly optional, allowing the database to store NULL values. The schema design enables a simple boolean distinguisher: NULL means root operator, NOT NULL means authenticated database user.

Root Operator Operations: Passing author_id: None

When the ai-memory CLI or other root-level tools execute commands without an authenticated user context, the audit system records these as anonymous operations.

In crates/ai-memory-store/src/ops.rs【/cache/repos/github.com/akitaonrails/ai-memory/main/crates/ai-memory-store/src/ops.rs#L77-L95】, the audit helper function accepts author_id: Option<UserId> and inserts it directly into the audit_log table:

// Example: Deleting a page as the root operator (no user)
store.writer.delete_page(
    &workspace_id,
    &project_id,
    &page_path,
    None,                        // <-- author_id = NULL
).await?;                       // audit row will have a NULL author

The None value propagates through the call chain and results in a database NULL. When querying these records, left-joining against users yields no match, which calling code conventionally displays as "root" or "system".

Database User Operations: Passing Some(user_id)

Authenticated API requests supply a valid 16-byte UserId that flows through the entire write path.

All mutating functions in crates/ai-memory-store/src/writer.rs【/cache/repos/github.com/akitaonrails/ai-memory/main/crates/ai-memory-store/src/writer.rs#L88-L100】accept author_id: Option<UserId> as a parameter:

// Example: Deleting a page as a specific DB user
let user_id = ai_memory_core::UserId::new(); // obtained from auth layer
store.writer.delete_page(
    &workspace_id,
    &project_id,
    &page_path,
    Some(user_id),               // <-- author_id supplied
).await?;                       // audit row will contain this user's ID

The Some(user_id) variant ensures precise attribution. The UserId type is a fixed-size 16-byte identifier (likely a UUID), stored efficiently without additional serialization overhead.

Resolving Author Names in Audit Queries

The attribution system completes its cycle in crates/ai-memory-store/src/reader.rs【/cache/repos/github.com/akitaonrails/ai-memory/main/crates/ai-memory-store/src/reader.rs#L6700-L6720】, where read operations left-join the users table to resolve human-readable names:

// Listing audit events with resolved author names
let events = store.reader.list_audit_events(AuditLogFilter::default()).await?;
for ev in events {
    println!(
        "{} – {} – author: {}",
        ev.at,
        ev.op,
        ev.author_name.unwrap_or_else(|| "root".into())
    );
}

The left join preserves audit records with NULL author_id (root operations) while enriching user-attributed records with usernames or display names. This pattern ensures backward-compatible queries that handle both attribution types uniformly.

Key Implementation Files

File Responsibility Critical Lines
crates/ai-memory-store/src/ops.rs Core audit and audit_with_detail helper functions 77-95
crates/ai-memory-store/src/writer.rs Writer methods accepting author_id: Option<UserId> 88-100
crates/ai-memory-store/src/reader.rs Audit query resolution with user table joins 6700-6720
crates/ai-memory-store/src/lib.rs Schema definition ensuring nullable author_id 216-224

Summary

  • Root operator attribution: Pass author_id: None → stored as SQL NULL → displayed as "root".
  • Database user attribution: Pass author_id: Some(UserId) → stored as 16-byte foreign key → resolved via left join to username.
  • Schema design: The nullable author_id column in audit_log provides a single, efficient discriminator.
  • Implementation path: Writer functions → audit helper in ops.rs → database insert → reader resolution with user join.

Frequently Asked Questions

How does ai-memory distinguish between system actions and user actions in the audit log?

The system uses the nullability of the author_id column as the sole discriminator. Root operators pass None, resulting in NULL database values. Authenticated users pass Some(user_id), populating the foreign key. This design avoids additional boolean columns and leverages standard SQL join semantics for resolution.

What happens if a user is deleted after performing audited actions?

The left-join pattern in crates/ai-memory-store/src/reader.rs【/cache/repos/github.com/akitaonrails/ai-memory/main/crates/ai-memory-store/src/reader.rs#L6700-L6720】uses a left outer join, which preserves audit records even when the referenced user no longer exists. The author_name field will be None in these cases, similar to root operations, but the original author_id remains stored in the audit row for forensic reference.

Can the audit log track which specific root operator performed an action?

Based on the current implementation, no. Root operations are indistinguishable from one another—all map to NULL author_id. Organizations requiring per-operator attribution for administrative actions must implement authentication layers that supply distinct UserId values even for service accounts or elevated operators.

Is the UserId type a standard UUID or a custom format?

The UserId type referenced throughout ai-memory is a 16-byte identifier, consistent with UUID v4 storage. The code treats it as an opaque type from ai_memory_core, ensuring type safety across the audit boundary without exposing internal byte layout to callers.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →