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
| Property | Value |
|---|---|
| Append-only | yes; the SDK exposes no delete or amend method |
| Hash chain | SHA256 over canonical payload, locked field order |
| Retention | 7 years |
| Storage class | WORM (write-once, read-many) |
| Cross-clinic read | no; tokens are clinic-scoped |
Rate limits
The marketing spec sets the buckets. All limits are per-agent unless noted.
| Bucket | Limit | Burst |
|---|---|---|
| Evidence map reads | 100 / minute | 5× |
| Simulation runs | 10 / minute | 5× |
| Commits (clinician principal) | 1 / minute | 5× |
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.