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:
| Field | What it records |
|---|---|
user / tenant | who ran the action (ctx.user.imperal_id, ctx.user.tenant_id) |
source | where it came from — chat, panel, mcp, or webhook |
| extension + tool | which extension and which tool ran |
action_type | read, write, or destructive |
args | the call arguments, with PII masking applied |
status | success, failed, validation_rejected, or cancelled |
error | structured error detail when the call failed |
| retention class | how long the record is kept (see below) |
| timestamp | when the action was received |
| usage | which model ran, plus tokens and 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
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:
| Guard | What it catches |
|---|---|
| Fabricated IDs | Args containing ID-shaped strings (note_id="abc123") that were never seen in the conversation |
| Ungrounded narration | Text claiming data the tools didn't actually return |
| Out-of-enum hints | A routing hint that falls outside the allowed set |
| Unbacked claims | Factual statements with nothing behind them |
| Clean error UX | Conversational 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:
- Catch
TaskCancelled(raised by the nextawait ctx.progress(...)call) - Roll back partial work if reasonable
- 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
Chains
Chains sequence multiple tools from one user message, dependency-ordered, threading each step's output into the next and rendering one combined Webbee response.
Build a panel layout
Build panel layouts for the Imperal Panel: four React surface patterns, from master-detail to hub-only, covering the vast majority of extension UI shapes.