Audit API Reference
Structured governance events emitted by the gateway. See Governance for the conceptual overview.
AuditEvent Schema
Audit events are frozen Pydantic v2 models serialized to JSON by each sink. The schema is versioned and follows additive-only evolution within a version.
{
"schema_version": "1",
"event_id": "bc8b6a823ffb4209bdc12697761ef676",
"timestamp": "2026-04-17T17:09:23.259153Z",
"action": "http.request",
"outcome": "success",
"actor_id": "alice",
"actor_type": "user",
"actor_groups": ["admins"],
"resource_type": "http",
"resource_id": "/api/v1/agents",
"request_id": "6828fe0619354661ae478d994588eb87",
"correlation_id": "trace-create-1",
"source_ip": "127.0.0.1",
"user_agent": "curl/8.7.1",
"http_method": "POST",
"http_path": "/api/v1/agents",
"http_status": 201,
"duration_ms": 17.242,
"reason": null,
"metadata": {}
}
Fields
| Field |
Type |
Description |
schema_version |
string |
Stable schema version. Breaking changes bump to "2". |
event_id |
string |
Unique per event (hex UUID). |
timestamp |
string (ISO-8601 UTC) |
When the event was produced. |
action |
string |
Stable action identifier from the taxonomy below. |
outcome |
enum |
allow, deny, success, failure, error. |
actor_id |
string or null |
Principal ID (hashed when redact_principal_id: true). |
actor_type |
string or null |
user, service, anonymous. |
actor_groups |
list[string] |
Principal groups. |
resource_type |
enum or null |
agent, team, policy, policy_engine, credential, session, tool, http, memory, user, llm. |
resource_id |
string or null |
Resource name / path / ID. |
request_id |
string or null |
Per-HTTP-request identifier (x-request-id). |
correlation_id |
string or null |
Cross-request chain identifier (x-correlation-id). |
source_ip |
string or null |
Client IP (set by AuditMiddleware). |
user_agent |
string or null |
Client UA (set by AuditMiddleware). |
http_method |
string or null |
HTTP method when applicable. |
http_path |
string or null |
HTTP path when applicable. |
http_status |
int or null |
Final response status code. |
duration_ms |
float or null |
Measured duration in milliseconds. |
reason |
string or null |
Human-readable reason, especially for denials / failures. |
metadata |
object |
Action-specific fields. Redacted before emit. |
Action Taxonomy
All action constants live in agentic_primitives_gateway.audit.models.AuditAction. The first segment (before .) is the category and forms the Prometheus label on gateway_audit_events_total.
Authentication
| Action |
Outcome |
Emitted by |
Key metadata |
auth.success |
success |
AuthenticationMiddleware |
backend |
auth.failure |
failure |
AuthenticationMiddleware |
backend, reason |
auth.logout |
success |
Reserved (UI-initiated logout) |
|
Policy Enforcement
| Action |
Outcome |
Emitted by |
Key metadata |
policy.allow |
allow |
PolicyEnforcementMiddleware |
cedar_principal, cedar_action |
policy.deny |
deny |
PolicyEnforcementMiddleware |
cedar_principal, cedar_action, reason |
policy.create/update/delete |
success |
routes/policy.py |
engine_id |
policy.engine.create/delete |
success |
routes/policy.py |
name |
policy.load |
success |
Reserved (Cedar refresh) |
|
Credentials
| Action |
Outcome |
Emitted by |
Key metadata |
credential.resolve |
success |
CredentialResolutionMiddleware |
services (names only), source |
credential.read |
success |
routes/credentials.py |
|
credential.write |
success/failure/error |
routes/credentials.py |
keys (names only), reason |
credential.delete |
success/failure/error |
routes/credentials.py |
keys, reason |
Credential events never log values — only attribute names.
Agents
| Action |
Outcome |
Emitted by |
Key metadata |
agent.create |
success |
routes/agents.py |
model |
agent.update |
success |
routes/agents.py |
fields |
agent.delete |
success |
routes/agents.py |
|
agent.run.start |
success |
AgentRunner |
session_id, depth |
agent.run.complete |
success |
AgentRunner |
session_id, depth, turns_used, tools_called |
agent.run.failed |
error |
AgentRunner |
session_id, depth |
agent.run.cancelled |
deny |
AgentRunner |
session_id, depth |
agent.delegate |
success |
Reserved (sub-agent call) |
|
Teams
| Action |
Outcome |
Emitted by |
Key metadata |
team.create |
success |
routes/teams.py |
workers, planner, synthesizer |
team.update |
success |
routes/teams.py |
fields |
team.delete |
success |
routes/teams.py |
|
team.run.start |
success |
TeamRunner |
team_run_id |
team.run.complete |
success |
TeamRunner |
team_run_id |
team.run.failed |
error |
TeamRunner |
team_run_id |
team.run.cancelled |
deny |
TeamRunner |
team_run_id |
| Action |
Outcome |
Emitted by |
Key metadata |
tool.call |
success/failure |
agents/tools/catalog.execute_tool |
tool_name, primitive, duration_ms, error_type |
tool.register |
success/failure |
POST /api/v1/tools |
duration_ms |
tool.delete |
success/failure |
DELETE /api/v1/tools/{name} |
duration_ms |
tool.server.register |
success/failure |
POST /api/v1/tools/servers |
duration_ms |
llm.generate |
success/error |
LLMProvider ABC (automatic) |
model, input_tokens, output_tokens, error_type |
Tool events never log tool input (the LLM may pass credentials through tool arguments).
Memory
| Action |
Outcome |
Emitted by |
Key metadata |
memory.resource.create |
success/failure |
POST /api/v1/memory/resources |
name, memory_id |
memory.resource.delete |
success/failure |
DELETE /api/v1/memory/resources/{memory_id} |
memory_id |
memory.strategy.create |
success/failure |
POST /api/v1/memory/resources/{memory_id}/strategies |
strategy_id |
memory.strategy.delete |
success/failure |
DELETE /api/v1/memory/resources/{memory_id}/strategies/{strategy_id} |
strategy_id |
memory.event.append |
success/failure |
POST /api/v1/memory/sessions/{actor}/{session}/events |
event_id, message_count |
memory.event.delete |
success/failure |
DELETE /api/v1/memory/sessions/{actor}/{session}/events/{event_id} |
|
memory.branch.create |
success/failure |
POST /api/v1/memory/sessions/{actor}/{session}/branches |
branch_name, root_event_id |
memory.record.write |
success/failure |
POST /api/v1/memory/{namespace} |
|
memory.record.delete |
success/failure |
DELETE /api/v1/memory/{namespace}/{key} |
|
Evaluators
| Action |
Outcome |
Emitted by |
Key metadata |
evaluator.create |
success/failure |
POST /api/v1/evaluations/evaluators |
name, evaluator_type, evaluator_id |
evaluator.update |
success/failure |
PUT /api/v1/evaluations/evaluators/{id} |
|
evaluator.delete |
success/failure |
DELETE /api/v1/evaluations/evaluators/{id} |
|
evaluator.score.create |
success/failure |
POST /api/v1/evaluations/scores |
name, trace_id, score_id |
evaluator.score.delete |
success/failure |
DELETE /api/v1/evaluations/scores/{id} |
|
evaluator.online_config.create |
success/failure |
POST /api/v1/evaluations/online-configs |
name, config_id |
evaluator.online_config.delete |
success/failure |
DELETE /api/v1/evaluations/online-configs/{id} |
|
Identity
| Action |
Outcome |
Emitted by |
Key metadata |
credential.read |
success/failure |
POST /api/v1/identity/{token,api-key} |
kind, credential_provider |
identity.credential_provider.create |
success/failure |
POST /api/v1/identity/credential-providers |
provider_type |
identity.credential_provider.update |
success/failure |
PUT /api/v1/identity/credential-providers/{name} |
|
identity.credential_provider.delete |
success/failure |
DELETE /api/v1/identity/credential-providers/{name} |
|
identity.workload.create |
success/failure |
POST /api/v1/identity/workload-identities |
|
identity.workload.update |
success/failure |
PUT /api/v1/identity/workload-identities/{name} |
|
identity.workload.delete |
success/failure |
DELETE /api/v1/identity/workload-identities/{name} |
|
Observability
| Action |
Outcome |
Emitted by |
Key metadata |
observability.trace.ingest |
success/failure |
POST /api/v1/observability/traces |
|
observability.trace.update |
success/failure |
PUT /api/v1/observability/traces/{id} |
|
observability.trace.generation.log |
success/failure |
POST /api/v1/observability/traces/{id}/generations |
name, model |
observability.trace.score.create |
success/failure |
POST /api/v1/observability/traces/{id}/scores |
name |
observability.log.ingest |
success/failure |
POST /api/v1/observability/logs |
|
observability.flush |
success/failure |
POST /api/v1/observability/flush |
|
Browser
| Action |
Outcome |
Emitted by |
Key metadata |
browser.navigate |
success/failure |
POST /api/v1/browser/sessions/{id}/navigate |
url |
browser.click |
success/failure |
POST /api/v1/browser/sessions/{id}/click |
selector |
browser.type |
success/failure |
POST /api/v1/browser/sessions/{id}/type |
selector, text_length (never text itself) |
browser.evaluate |
success/failure |
POST /api/v1/browser/sessions/{id}/evaluate |
expression_length (never the expression itself) |
Code Interpreter
| Action |
Outcome |
Emitted by |
Key metadata |
code_interpreter.execute |
success/failure |
POST /api/v1/code-interpreter/sessions/{id}/execute |
language, code_length (never the code itself) |
code_interpreter.file.upload |
success/failure |
POST /api/v1/code-interpreter/sessions/{id}/files |
size_bytes |
code_interpreter.file.download |
success/failure |
GET /api/v1/code-interpreter/sessions/{id}/files/{name} |
size_bytes |
Tasks (team-run task board)
Emitted when an agent invokes the task tool inside a team run. Direct
registry.tasks.* calls from TeamRunner are covered by
provider.call.
| Action |
Outcome |
Emitted by |
Key metadata |
task.create |
success |
agents/tools/handlers.py::task_create |
created_by, depends_on, priority, suggested_worker |
task.claim |
success/failure |
agents/tools/handlers.py::task_claim |
claimed_by |
task.update |
success/failure |
agents/tools/handlers.py::task_update |
status, has_result |
task.note |
success/failure |
agents/tools/handlers.py::task_add_note |
author |
Resource Access
| Action |
Outcome |
Emitted by |
Key metadata |
resource.access.denied |
deny |
auth/access.py |
resource_owner, resource_type_hint, reason |
HTTP + Provider Call + Sessions
| Action |
Outcome |
Emitted by |
Key metadata |
http.request |
success/failure |
AuditMiddleware |
method, path, status, duration, source_ip, user_agent |
provider.call |
success/failure |
MetricsProxy (every wrapped primitive method) |
primitive, provider, method, duration_ms |
session.create |
success/failure |
POST /api/v1/{browser,code-interpreter}/sessions |
primitive, language |
session.terminate |
success/failure |
DELETE /api/v1/{browser,code-interpreter}/sessions/{id} |
primitive |
agent.delegate |
success/failure |
agents/tools/delegation.py (agent-as-tool) |
parent_owner_id, depth, error_type |
policy.load |
success/failure |
CedarPolicyEnforcer.load_policies() (only when policy set changes) |
policy_count, previous_count |
Outcome Values
class AuditOutcome(StrEnum):
ALLOW = "allow"
DENY = "deny"
SUCCESS = "success"
FAILURE = "failure"
ERROR = "error"
NOT_IMPLEMENTED = "not_implemented"
| Value |
Use for |
allow |
Policy permit |
deny |
Policy deny, access denial, cancelled run |
success |
Successful mutation or HTTP 2xx |
failure |
Expected failure (auth rejected, 4xx, validation) |
error |
Unexpected failure (exception, 5xx) |
not_implemented |
Provider deliberately doesn't implement the requested operation — emitted by MetricsProxy so compliance dashboards don't count optional-method absence as a real failure |
Resource Types
class ResourceType(StrEnum):
AGENT = "agent"
TEAM = "team"
POLICY = "policy"
POLICY_ENGINE = "policy_engine"
CREDENTIAL = "credential"
SESSION = "session"
TOOL = "tool"
HTTP = "http"
MEMORY = "memory"
USER = "user"
LLM = "llm"
EVALUATOR = "evaluator"
IDENTITY = "identity"
TASK = "task"
TRACE = "trace"
CODE_EXECUTION = "code_execution"
FILE = "file"
PAGE = "page"
The user type is reserved for future emits that target a specific user
record (today, credential operations use credential since the operation
is on the credential, not the user). Every other type is emitted by at
least one action above.
Redaction
Three layers keep secrets out of audit output:
- Per-emit deny-list.
redact_mapping() in audit/redaction.py walks the metadata dict and replaces values for known-sensitive keys (authorization, cookie, password, token, secret, api_key, x-aws-secret-access-key, x-aws-session-token, any key listed in audit.redact_keys) with "***".
- Principal ID hashing. Set
audit.redact_principal_id: true to replace actor_id with a 16-char SHA-256 prefix before emit. Useful for multi-tenant k8s deployments.
- Log sanitization. Separate from audit events,
LogSanitizationFilter scrubs Bearer tokens, AWS access keys, JWT three-part tokens, and apg.* key=value pairs from rendered log messages.
Schema Evolution
- Additive changes within
schema_version: "1" are allowed: new optional fields, new actions, new metadata keys.
- Breaking changes (rename / remove / type change) bump to
schema_version: "2".
- Consumers must tolerate unknown fields — don't reject events because of a new key.
- Emitting code is on one version at a time; consumers decide their compatibility window.
Sink Configuration
Sinks are configured in the audit: block of the server YAML. Each entry has a stable name (used as the sink metric label) and a backend (short alias or dotted class path).
audit:
enabled: true
stdout_json: true # always-on unless explicitly disabled
sinks:
- name: local_file
backend: file
config:
path: /var/log/apg/audit.log
max_bytes: 10485760
backup_count: 5
- name: durable
backend: redis_stream
config:
redis_url: "${REDIS_URL:=redis://localhost:6379/0}"
stream: "gateway:audit"
maxlen: 100000
- name: traces
backend: observability
redact_keys: [] # extra keys to scrub in metadata
redact_principal_id: false # hash actor_id before emit
queue_size: 2048 # per-sink queue bound
sink_timeout_seconds: 2.0 # per-emit timeout per sink
filter: # drop noisy events before fan-out
exclude_actions: [] # exact action drops (e.g. "provider.call")
exclude_action_categories: [] # prefix drops (e.g. "memory")
sample_rates: {} # per-action keep rate in [0.0, 1.0]
logging:
format: json # text | json
sanitize: true # install LogSanitizationFilter
Filtering Noisy Events
The router drops events that match any filter rule before fan-out, so
filtered events never hit a sink queue. Each rule is independent and
compose logically with AND: an event must pass every rule to
survive.
exclude_actions: exact action string match (drop every provider.call).
exclude_action_categories: first .-segment match (drop every memory.*).
sample_rates: per-action keep fraction in [0.0, 1.0]. 0.0 drops
every event, 1.0 keeps every event. Sampling is independent per-event.
Dropped events increment
gateway_audit_events_dropped_total{sink="__router__",reason="filtered"}.
Keep compliance-relevant events unfiltered — auth, policy, resource
access, credential, version, and fork events are emitted at low volume
but carry high audit value. Filter the high-volume ones:
provider.call (one per primitive RPC), tool.call, and the
memory.record.* family.
Built-in Sink Aliases
| Alias |
Class |
Use for |
noop |
NoopAuditSink |
Tests; disabled audit |
stdout_json |
StdoutJsonSink |
Default; k8s log shipping |
file |
RotatingFileAuditSink |
Local or sidecar tail |
redis_stream |
RedisStreamAuditSink |
Durable cross-replica bus |
observability |
ObservabilityProviderSink |
Route into Langfuse / AgentCore via registry.observability.ingest_log |
Custom Sinks
Implement AuditSink and reference by dotted path:
from agentic_primitives_gateway.audit.base import AuditSink
from agentic_primitives_gateway.audit.models import AuditEvent
class MySink(AuditSink):
def __init__(self, *, name: str = "mine", **config) -> None:
self.name = name
async def emit(self, event: AuditEvent) -> None:
...
async def close(self) -> None:
...
audit:
sinks:
- name: mine
backend: myapp.audit.MySink
config: {...}
Read-back via AuditReader
The UI audit viewer (/ui/audit) and the GET /api/v1/audit/{status,events,events/stream}
endpoints are backend-agnostic — they iterate router.sinks and dispatch
against the first sink that implements the AuditReader protocol:
from collections.abc import AsyncIterator
from typing import Any
from agentic_primitives_gateway.audit.base import AuditReader, AuditSink
from agentic_primitives_gateway.audit.models import AuditEvent
class MyDurableSink(AuditSink):
"""Write-only. UI viewer will ignore this sink."""
async def emit(self, event: AuditEvent) -> None: ...
class MyQueryableSink(AuditSink):
"""Implements AuditReader too — UI viewer can query it."""
async def emit(self, event: AuditEvent) -> None: ...
# AuditReader protocol — surfaces in /api/v1/audit/*.
def describe(self) -> dict[str, Any]:
return {"backend": "my_queryable", "retention_days": 90}
async def count(self) -> int | None: ...
async def list_events(
self, *, start: str, end: str, count: int,
) -> tuple[list[AuditEvent], str | None]: ...
async def tail(self) -> AsyncIterator[AuditEvent | None]:
"""Yield new events or ``None`` as a keepalive tick."""
...
Today only RedisStreamAuditSink implements AuditReader. A custom
Postgres, SQLite, or S3-index sink can plug in without changing the
route layer or UI. Write-only sinks (stdout_json, file,
observability, noop) stay silent — their audit data is consumed
externally (SIEM, Langfuse trace explorer, etc.).
See Also