Imperal Docs
Guides

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

ClassDurationWhen
federal_7y7 yearsDefault for all action rows. CJIS-equivalent retention.
standard_90d90 daysRead-only tools that don't touch sensitive data (rare; opt-in).
minimal_30d30 daysInternal probes, health checks.
security_foreverNEVER deletedSecurity-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 user

Tag 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 explicitly

Don'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:

GuardWhat it catches
I-AH-1Args containing fabricated ID-shaped strings (note_id="abc123" not seen anywhere)
I-AH-2v2Narration claiming data the tools didn't return (rendered, not yours)
I-AH-3Classifier hint outside the closed enum
I-AH-4Narrator factual claims without backing
I-MAGIC-UX-1/2Conversational 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:

  1. Detect via ctx.cancel_event
  2. Roll back partial work if reasonable
  3. Return a cancelled status 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.

Where to read more

On this page