Audit
Package auditlog is your org's tamper-evident audit trail: every security-relevant event, hash-chained and readable.
Package auditlog is your org's tamper-evident audit trail: every security-relevant event, hash-chained and readable.
| Base URL | https://api.hanzo.ai |
| Operations | 1 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1103 · Audit — The Tamper-Evident Trail — Draft · read the specification →
GET /v1/audit is an org admin's read of their own organization's
security-relevant events, off the tamper-evident trail the cloud binary already
writes: every audited action is a structured record, hash-chained to its
predecessor so any later deletion or modification is detectable, appended INLINE
to a store the application can only insert into. The trail itself lives in
hanzoai/cloud's audit package; the customer surface is apps/auditlog.
Motivation
An enterprise buyer's own compliance team must be able to see what happened in
THEIR org — the audit trail is table stakes for SOC 2 and ISO review — without
being a fleet operator. The pre-existing surface was operator-only
(/v1/admin/audit, SuperAdmin-guarded), so a normal org owner had no audit
route at all. This capability adds exactly the org-scoped read and nothing else
(apps/auditlog/auditlog.go).
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
§1 The store — shared, and why
The app opens NO store of its own. Its store is the same *audit.Recorder the
request middleware writes and the operator view reads, handed through the
composition's deps (apps/auditlog/auditlog.go:61 — "this subsystem opens NO
second store"). The chain is only tamper-evident if records are appended in a
strict, gapless total order with each record's PrevHash the preceding record's
Hash, which demands a single serializing writer — so the primary is one embedded
SQLite the application only INSERTs into, opened encrypted under the process
key (audit/store.go). An OLAP mirror into the datastore exists for
long-retention query; it is best-effort, asynchronous, and a projection — losing
a mirror row is a query-completeness issue, never an integrity one.
§2 Append fails the request closed
Append is inline and its error is returned to the middleware, which fails the audited request rather than acting unlogged — the AU-5 response to audit-logging failure. A security-relevant action that cannot be recorded MUST NOT silently succeed.
§3 Addresses
GET /v1/audit, one typed operation. The whole-chain verify walk stays
operator-only because it is a fleet property that would cross tenants, but every
row an org admin reads carries its own hash and prevHash, so the chain linkage
of their events is visible. The Go package is named auditlog while the
capability is audit: the divergence parks the generic per-app liveness route
at /v1/auditlog/health so it can never shadow the trail's own address, and
the mount order binds /v1/audit before the model plane's catch-all
(apps/auditlog/auditlog.go:28-31).
§4 Tenancy
The org is principal.OrgFrom — the value the identity middleware minted from
the verified bearer (HIP-0026) — and the read's filter is PINNED server-side to
it; the request type carries no org field at all, so a caller can only ever read
their own org's events. Fail-closed: no validated principal → 401, no store →
501. The actor on every record is likewise populated only from the validated
principal, never a raw header, so an actor cannot be forged by the request being
audited (audit/record.go).
§5 Money, events, telemetry, stage, upstream
Free (plugin/audit/main.go, cloud.Free). It publishes nothing to the bus —
the trail is a store the middleware writes, not an event stream. Beyond the
request span, the capability's writes ARE the telemetry: the append per audited
request, and the best-effort mirror row into the datastore. Stage ga: the
compliance trail is observability core, table stakes for the self-service
cloud. It derives from no OSS upstream.
Rationale
The alternative primary is the OLAP store the mirror uses. It shards and scales, and that is exactly the problem: a fleet-wide, eventually-consistent sink cannot give the strict total order the hash chain needs, and a fire-and-forget write can drop the one record that mattered. The small, serialized, synchronous primary is the compliance control; the mirror buys the query surface without weakening it.
Security Considerations
An audit trail is what an attacker edits second, after the thing they did first. The chain makes in-place edits and deletions detectable; the INSERT-only surface means the application offers no verb to do either; and periodic checkpoints of the head anchor the tail, because a chain walk alone cannot notice that the last K records were truncated. The customer read adds one exposure of its own — a cross-tenant read of someone else's security events — and closes it by making the tenant a server-side constant rather than an input.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | audit at its own prefix | 1 operation |
| CLI | hanzo audit … | 1 of 1 |
| SDK | AuditApi in every published client | 1 method |
| MCP | tool audit on https://api.hanzo.ai/v1/mcp | 1 operation |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call — a read that needs nothing but the key. GET /v1/audit, operation get_audit:
hanzo audit getimport { Configuration, AuditApi } from 'hanzoai';
const api = new AuditApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getAudit();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import AuditApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = AuditApi(client).get_audit()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.AuditAPI.GetAudit(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, audit_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = audit_api::get_audit(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.AuditApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new AuditApi(client).getAudit();curl https://api.hanzo.ai/v1/audit \
-H "Authorization: Bearer $HANZO_API_KEY"Tool audit, op get_audit — POST the JSON-RPC envelope to https://api.hanzo.ai/v1/mcp.
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "audit",
"arguments": {
"op": "get_audit",
"input": {}
}
}
}'Answers 200 with object — ok.
Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/audit | List reads the caller's OWN org audit trail, newest first, with the total the filter matched so a console can page it. |
How is this guide?