Audit & security
How every action gets recorded, what tenant isolation guarantees you depend on, and what your extension is responsible for
Imperal Cloud takes audit and tenant isolation seriously. Most of what you need is automatic — your extension gets it for free. This page documents what you get, what you owe, and what to expect from the platform.
What you get for free
Audit chokepoint
Every @chat.function call lands in the federal action_ledger. Tool, args (sanitized), result_status, retention class, timestamp, user_id, tenant_id.
Tenant isolation
ctx.user.[imperal_id](/en/reference/glossary/) and ctx.tenant_id are [web-kernel](/en/reference/glossary/)-authoritative. Spoofing them downstream is detected by audit.
Anti-hallucination guards
Pre-flight checks (I-AH-1..4) catch fabricated IDs and ungrounded args before your handler runs.
Action authorization
RBAC + scope checks gate every action. Failures don't reach your code.
Observability
ctx.http calls + ctx.log entries flow into structured observability with tags. Alerts wired for federal violations.
The action ledger
Every @chat.function invocation produces a row:
-- Simplified shape
CREATE TABLE action_ledger (
id UUID PRIMARY KEY,
user_id TEXT NOT NULL, -- ctx.user.imperal_id
tenant_id TEXT NOT NULL, -- ctx.tenant_id
source TEXT NOT NULL, -- 'chat', 'panel', 'mcp', 'webhook'
ext_name TEXT NOT NULL,
tool_name TEXT NOT NULL,
action_type TEXT NOT NULL, -- 'read' | 'write' | 'destructive'
args JSONB, -- with PII masking applied
status TEXT NOT NULL, -- 'success', 'failed', 'validation_rejected', 'cancelled'
error JSONB,
retention_class TEXT NOT NULL, -- see below
received_at TIMESTAMPTZ NOT NULL,
llm_per_purpose JSONB -- which LLM ran, tokens, cost
);Read access is restricted — admins via Panel, security review via federal protocols.
Retention classes
| Class | Duration | When |
|---|---|---|
federal_7y | 7 years | Default for all action rows. CJIS-equivalent retention. |
standard_90d | 90 days | Read-only tools that don't touch sensitive data (rare; opt-in). |
minimal_30d | 30 days | Internal probes, health checks. |
security_forever | NEVER deleted | Security-relevant events (login, role change, federation). DBA cannot delete these. |
You don't pick this manually — action_type + your extension's compliance class drive it. action_type="destructive" always lands in federal_7y.
What your extension is responsible for
Federal can't enforce this from outside
These are author-discipline rules. The platform can't read your code's intent. You're trusted but reviewed.
Always scope state by ctx.user.imperal_id
# ✅ Federal-clean
db.set(f"user:{ctx.user.imperal_id}:notes:{note_id}", note)
# ❌ Cross-tenant leak
db.set(f"notes:{note_id}", note) # global key — can be overwritten by another userTag external calls with the tenant context
# ctx.http does this for free
await ctx.http.post("/external/api", json=...)
# If you have to use a custom client (rare — ctx.http is preferred by
# convention but not enforced by a federal validator):
# pass user_id / tenant_id headers explicitlyDon't log secrets or PII
# ❌ Don't
ctx.log.info("Auth", token=user_token)
# ✅ Do
ctx.log.info("Auth attempt", user_id=ctx.user.imperal_id, success=True)ctx.log auto-masks api_key, token, password, secret patterns when EXPOSE_PII_TO_LLM=False (default). But don't rely on it — write the right thing in the first place.
Honour cancellation
async def long_running(ctx, params):
for batch in batches:
if ctx.cancel_event.is_set():
ctx.log.info("Cancelled", processed=processed_count)
return {"text": "Cancelled.", "partial": True}
await process(batch)Audit rows are written even on cancellation — with status='cancelled'. Don't silently ignore.
Anti-hallucination — what runs before your code
Before your handler is dispatched, the web-kernel runs:
| Guard | What it catches |
|---|---|
I-AH-1 | Args containing fabricated ID-shaped strings (note_id="abc123" not seen anywhere) |
I-AH-2v2 | Narration claiming data the tools didn't return (rendered, not yours) |
I-AH-3 | Classifier hint outside the closed enum |
I-AH-4 | Narrator factual claims without backing |
I-MAGIC-UX-1/2 | Conversational chat errors — no leaked Pydantic class names, no stack traces |
You don't have to write any of these. They're enforced web-kernel-side. What you can do is make them less likely to fire — by writing tight Pydantic descriptions and not returning ungrounded text.
When something goes wrong
The user sees a conversational error template:
"I had trouble understanding the 'title' field — could you re-state what title you want?"
Audit row: status='validation_rejected', error.code='VALIDATION_MISSING_FIELD', retention federal_7y.
Your handler is not called.
If the request asks for resource X but X belongs to a different tenant:
"That's not yours — this account doesn't have access."
Audit row: status='failed', error.code='TENANT_SCOPE_VIOLATION', retention security_forever. PagerDuty fires above threshold (federal incident response).
If the LLM picked an id that wasn't seen in conversation:
"Hmm, I don't have a record of that — could you try rephrasing?"
Audit row: status='failed', error.code='FABRICATED_ID_SHAPE', retention federal_7y. Counter increments toward fabricated_id_on_retry SigNoz alert.
User pressed "stop" or navigated away mid-call:
Audit row: status='cancelled', error=null, retention per action_type.
Your handler should:
- Detect via
ctx.cancel_event - Roll back partial work if reasonable
- Return a
cancelledstatus return shape
What you can do for stronger compliance
Tag with effects
@chat.function(effects=['email.send', 'pii.read']) — flows into compliance dashboards alongside the action_ledger row.
Use ctx.log structurally
Every log line gets shipped to SigNoz with user_id, tenant_id, ext_name. Your custom keys persist alongside.
Implement health endpoints
Skeletons can health-check your own backend. Failed health propagates to admin dashboards.