Imperal Docs
Guides

Audit & security

Audit and security in Imperal Cloud: the action ledger, retention classes, tenant isolation, and exactly what every Webbee extension action records for free.

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.user.tenant_id are [web-kernel](/en/reference/glossary/)-authoritative. Spoofing them downstream is detected by audit.

🚫

Anti-hallucination guards

Pre-flight checks 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 is recorded with a consistent set of fields. Each record captures:

FieldWhat it records
user / tenantwho ran the action (ctx.user.imperal_id, ctx.user.tenant_id)
sourcewhere it came from — chat, panel, mcp, or webhook
extension + toolwhich extension and which tool ran
action_typeread, write, or destructive
argsthe call arguments, with PII masking applied
statussuccess, failed, validation_rejected, or cancelled
errorstructured error detail when the call failed
retention classhow long the record is kept (see below)
timestampwhen the action was received
usagewhich model ran, plus tokens and 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
await ctx.log(f"Auth token={user_token}", level="info")

# ✅ Do
await ctx.log(f"Auth attempt user={ctx.user.imperal_id} success=True", level="info")

ctx.log is an async def coroutine — await ctx.log("message", level="info") — not a logger object with .info/.warn/.error attributes, and it takes only message + level (no structured keyword fields). Build the message string yourself, and keep secrets and PII out of it — write the right thing in the first place.

Honour cancellation

from imperal_sdk import ActionResult
from imperal_sdk.chat import TaskCancelled

async def long_running(ctx, params):
    try:
        for i, batch in enumerate(batches):
            # ctx.progress raises TaskCancelled if the user cancelled
            await ctx.progress(int(100 * i / max(len(batches), 1)), "Processing…")
            await process(batch)
    except TaskCancelled:
        await ctx.log(f"Cancelled after {processed_count} items", level="info")
        return ActionResult.error("Cancelled")
    return ActionResult.success(data={}, summary="Done")

Cancellation is surfaced through ctx.progress(): when the user cancels, the next await ctx.progress(...) call raises TaskCancelled. There is no separate cancellation flag or event object to poll — watch for the TaskCancelled exception instead. 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 platform pre-screens the request:

GuardWhat it catches
Fabricated IDsArgs containing ID-shaped strings (note_id="abc123") that were never seen in the conversation
Ungrounded narrationText claiming data the tools didn't actually return
Out-of-enum hintsA routing hint that falls outside the allowed set
Unbacked claimsFactual statements with nothing behind them
Clean error UXConversational errors with no leaked Pydantic class names or stack traces

You don't have to write any of these — they're enforced by the platform. 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. The platform escalates these to incident response above threshold.

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. The platform's observability pipeline tracks repeated occurrences.

User pressed "stop" or navigated away mid-call:

Audit row: status='cancelled', error=null, retention per action_type.

Your handler should:

  1. Catch TaskCancelled (raised by the next await ctx.progress(...) call)
  2. Roll back partial work if reasonable
  3. Return ActionResult.error("Cancelled")

What you can do for stronger compliance

🔍

Tag with effects

@chat.function('send_email', effects=['email.send', 'pii.read']) — a declarative side-effect tag emitted into your manifest. It is metadata only today (no compliance/dashboard consumption yet); reserved for future use.

📊

Use ctx.log

await ctx.log('message', level='info') ships a structured log line, visible in the extension dashboard.

🚦

Implement health endpoints

Skeletons can health-check your own backend. Failed health propagates to admin dashboards.

Where to read more

On this page