Skip to main content

Overview

The audit client provides a tamper-evident audit trail for agent actions. Every entry is hash-chained to the previous entry, making the log append-only and tamper-detectable. Access the audit client via client.audit.

Log

Record an audit entry for an agent action:
from grantex import Grantex

with Grantex(api_key="gx_live_...") as client:
    entry = client.audit.log(
        agent_id="agt_abc123",
        grant_id="grnt_xyz789",
        action="file.read",
        metadata={"path": "/documents/report.pdf", "size_bytes": 102400},
        status="success",
    )

    print(f"Entry ID: {entry.entry_id}")
    print(f"Hash: {entry.hash}")
    print(f"Previous hash: {entry.prev_hash}")
    print(f"Timestamp: {entry.timestamp}")

Parameters

All parameters are keyword-only.
ParameterTypeRequiredDefaultDescription
agent_idstrYesThe agent that performed the action.
grant_idstrYesThe grant under which the action was performed.
actionstrYesA label for the action (e.g. "file.read").
metadatadict[str, Any] | NoneNoNoneAdditional context about the action.
statusstrNo"success"The outcome ("success", "failure", "blocked").

List

Query audit entries with optional filters:
from grantex import Grantex, ListAuditParams

with Grantex(api_key="gx_live_...") as client:
    # List all entries
    result = client.audit.list()
    print(f"Total entries: {result.total}")

    # List with filters
    result = client.audit.list(ListAuditParams(
        agent_id="agt_abc123",
        action="file.read",
        since="2026-01-01T00:00:00Z",
        until="2026-02-01T00:00:00Z",
        page=1,
        page_size=50,
    ))

    for entry in result.entries:
        print(f"  [{entry.timestamp}] {entry.action} - {entry.status}")

ListAuditParams

FieldTypeRequiredDescription
agent_idstr | NoneNoFilter by agent ID.
grant_idstr | NoneNoFilter by grant ID.
principal_idstr | NoneNoFilter by principal (user) ID.
actionstr | NoneNoFilter by action label.
sincestr | NoneNoISO 8601 start timestamp (inclusive).
untilstr | NoneNoISO 8601 end timestamp (exclusive).
pageint | NoneNoPage number for pagination.
page_sizeint | NoneNoNumber of results per page.

ListAuditResponse

FieldTypeDescription
entriestuple[AuditEntry, ...]The list of audit entries.
totalintTotal number of matching entries.
pageintCurrent page number.
page_sizeintNumber of entries per page.

Get

Retrieve a single audit entry by its ID:
entry = client.audit.get("aud_abc123")

print(f"Action: {entry.action}")
print(f"Agent: {entry.agent_id} ({entry.agent_did})")
print(f"Grant: {entry.grant_id}")
print(f"Principal: {entry.principal_id}")
print(f"Hash: {entry.hash}")
print(f"Previous hash: {entry.prev_hash}")
print(f"Metadata: {entry.metadata}")

AuditEntry Type

The AuditEntry frozen dataclass has the following fields:
FieldTypeDescription
entry_idstrUnique entry identifier.
agent_idstrThe agent that performed the action.
agent_didstrThe agent’s DID.
grant_idstrThe grant under which the action occurred.
principal_idstrThe authorizing user/principal.
actionstrThe action label.
metadatadict[str, Any]Additional context.
hashstrSHA-256 hash of this entry.
prev_hashstr | NoneHash of the previous entry (chain link).
timestampstrISO 8601 timestamp of the action.
statusstrOutcome status.

Hash Chain Integrity

Each audit entry contains a hash and a prev_hash field. The hash is computed over the entry’s contents, and prev_hash references the previous entry’s hash. This creates a tamper-evident chain: modifying or deleting any entry breaks the chain for all subsequent entries.
# Verify chain integrity by iterating entries
result = client.audit.list()
for i, entry in enumerate(result.entries):
    if i == 0:
        print(f"First entry: {entry.hash}")
    else:
        expected_prev = result.entries[i - 1].hash
        if entry.prev_hash == expected_prev:
            print(f"Entry {entry.entry_id}: chain valid")
        else:
            print(f"Entry {entry.entry_id}: CHAIN BROKEN")
For automated chain integrity verification, use the compliance evidence pack.