Skip to content
Book a demoSign in
All docs
Agent SDK

Audit + rate limits

Audit-log shape, hash-chain verification, append-only / 7-year / WORM semantics, and the 429 backoff contract.

Updated May 28, 2026

Every read, draft, simulation, and commit lands in the patient's audit log. The log is append-only, hash-chained, and retained for 7 years under WORM semantics. Agents read it the same way clinicians do.

The shape

type AuditLogEntry = {
  id: string;
  timestamp: string;     // ISO 8601
  actorId: string;       // your agent ID
  role: "patient" | "clinician" | "agent";
  clinicId: string;
  patientId?: string;    // empty string in the canonical hash payload when absent
  action: string;        // e.g. "evidence_map.read", "sandbox.fork", "draft.create"
  summary: string;       // one-line, plain-language
  previousHash: string;  // links to the prior entry; "genesis" for the first
  hash: string;          // SHA256 over the canonical payload
};

The canonical hash payload uses a fixed field order: id, timestamp, actorId, role, clinicId, patientId, action, summary, previousHash. Both SDK ports emit byte-identical canonical JSON over that ordered tuple. A golden SHA256 is pinned in both test suites so cross-language drift fails fast in CI.

Verify the chain

import { verifyAuditLogIntegrity } from "@humyn/nyra";

const page = await client.getAuditLog(patientId);
const status = verifyAuditLogIntegrity(page.entries);
if (status.status === "broken") {
  throw new Error(`Audit chain broken at ${status.brokenAtAuditId}`);
}
from humyn_nyra import verify_audit_log_integrity

page = await client.get_audit_log(patientId)
status = verify_audit_log_integrity(page.entries)
if status.status == "broken":
    raise RuntimeError(f"Audit chain broken at {status.broken_at_audit_id}")

The verifier walks newest-to-oldest, recomputing every hash and checking that each entry's previousHash matches the next entry's hash. The oldest entry's previousHash must equal the genesis sentinel "genesis".

Retention

PropertyValue
Append-onlyyes; the SDK exposes no delete or amend method
Hash chainSHA256 over canonical payload, locked field order
Retention7 years
Storage classWORM (write-once, read-many)
Cross-clinic readno; tokens are clinic-scoped

Rate limits

The marketing spec sets the buckets. All limits are per-agent unless noted.

BucketLimitBurst
Evidence map reads100 / minute
Simulation runs10 / minute
Commits (clinician principal)1 / minute

When you exceed a bucket, the response is a 429 with a Retry-After header in seconds. The SDK surfaces this as a typed error:

import { RateLimitError } from "@humyn/nyra";

try {
  await client.runSimulation(input);
} catch (err) {
  if (err instanceof RateLimitError) {
    await new Promise((r) => setTimeout(r, err.retryAfterSeconds * 1000));
    // retry the call
  } else {
    throw err;
  }
}
from humyn_nyra import RateLimitError

try:
    await client.run_simulation(input_)
except RateLimitError as err:
    await asyncio.sleep(err.retry_after_seconds)
    # retry

The error carries the bucket name verbatim so multi-bucket agents can sleep on the right clock.

What lands when

A successful call writes one audit entry per logical action. A sandbox.fork writes one entry. A simulation.run writes one entry. A draft.create writes one entry. A read writes one entry. There is no batching. Your agent identity, the clinician principal (if any), and a one-line plain-language summary all land in the entry. The body of the call (the LLM's prompt, your scratch tokens) does not.

That is the contract. Audit explains what happened, not how the model thought about it.

Next: Errors and recovery.