Imperal Docs
SDK Reference

Federal Extension Contract

The complete contract every published extension must satisfy — manifest schema v3, validators V14-V22 + V24 + V31, runtime invariants

The Federal Extension Contract is the binding agreement between your code and Imperal Cloud. It defines what a published extension MUST satisfy to ship and run. This page is the canonical reference.

The current contract is v4.0 (with v4.0.1 patches). Every published extension passes through it at build time, validate time, and publish time.

Why this contract exists

Before v4.0, the platform had a class of silent-failure bugs:

🔴

Silent write failures

Extension says 'deleted' in chat, but nothing was actually deleted. Caused by chain-planner [BYOLLM](/en/reference/glossary/)-router gap.

🔴

Hallucinated IDs

LLM fabricates id-shaped slugs (webhostmost-outlook-1) that look right but don't exist.

🔴

Confirmation bypass

2-step destructive actions slipping through without user accept.

🟠

Lifecycle TypeError flood

Extensions on stale SDK signatures crashing skeleton refresh ~1166×/h.

v4.0 closed ~75% of these classes. The contract details follow.

The Extension declaration

app.py
from imperal_sdk import Extension

ext = Extension(
    "my-app",                                          # internal name
    version="1.0.0",
    display_name="My App",                             # V15: ≥3 chars, ≠ app_id
    description=(                                      # V14: ≥40 chars
        "Sentence describing what the extension does, "
        "shown in Webbee's 'what I can do' capability list."
    ),
    icon="icon.svg",                                   # V21: required SVG
    actions_explicit=True,                             # V19: federal default
    capabilities=["my-app:read", "my-app:write"],
)

Prop

Type

The @chat.function contract

handlers.py
from imperal_sdk import ChatExtension, ActionResult
from .schemas import NoteIdParams

@chat.function(
    "delete_note",                                    # explicit name
    action_type="destructive",
    description="Delete a note (moves to trash, restorable for 30 days).",  # V16: ≥20 chars
    chain_callable=True,                              # V19: required for write/destructive
    effects=["delete:note"],                          # V20: required for write/destructive
)
async def fn_delete_note(ctx, params: NoteIdParams) -> ActionResult:  # V17 + V18
    ...

The decorator auto-detects from your function annotations:

  • Params model from params: NoteIdParams → emits full JSON Schema in manifest (params_schema)
  • Return type from -> ActionResult → emits return_schema
  • chain_callable defaults to True for all action types since v4.2.10 — reads now type-dispatch in chains too, eliminating wrapper-LLM paraphrase drift on list_*/search_*/get_* handlers

The 11 federal validators

All ERROR severity. Failing any blocks publishing.

V23 was dropped

v4.0.0 had V23 (scope pattern check). v4.0.1 dropped it as redundant — manifest_schema.SCOPE_PATTERN Pydantic validator catches the same thing. The current set is V14-V22 + V24 + V31.

Manifest schema v3

generate_manifest(ext) walks every typed @chat.function and emits a tool entry. Pre-v3 manifests only contained the BYOLLM router — chain planner was blind to typed surfaces. Now the web-kernel sees every tool.

imperal.json
{
  "manifest_schema_version": 3,
  "name": "notes",
  "display_name": "Notes",
  "description": "Personal notes assistant — create, read, update, delete...",
  "icon": "icon.svg",
  "icon_size_bytes": 510,
  "actions_explicit": true,
  "capabilities": ["notes:read", "notes:write"],
  "tools": [
    {
      "name": "delete_note",
      "description": "Delete a note (moves to trash).",
      "action_type": "destructive",
      "chain_callable": true,
      "effects": ["delete:note"],
      "params_schema": { "type": "object", "properties": { "note_id": { "type": "string" } }, "required": ["note_id"] },
      "return_schema": { ... },
      "event": "deleted",
      "owner_chat_tool": "tool_notes_chat"
    }
  ],
  "lifecycle_hooks": {
    "on_install":  { "signature": "(ctx, message=None)" },
    "on_refresh":  { "signature": "(ctx, message=None)" }
  }
}

See the full manifest reference for every field.

Web-kernel-side guarantees (Phase 2)

The contract is two-sided. You satisfy V14-V22+V24+V31 at build time. The web-kernel holds these runtime guarantees:

🎯

Typed dispatch in chains

When the classifier emits an action_plan and your tool has chain_callable=True, the web-kernel dispatches via Pydantic — no second LLM round in your extension.

🛡️

Pre-flight anti-hallucination

I-AH-1..4 fire before your handler runs. Fabricated IDs, ungrounded narration, out-of-enum hints — caught upstream.

🔐

Confirmation chokepoint

Destructive actions intercepted, shown to user, run typed-iterate on accept. Federal I-CONFIRMATION-EXECUTES-WHAT-USER-SAW.

📝

Audit chokepoint

Every action that reaches your handler also reaches the federal [action ledger](/en/reference/glossary/). No bypass.

Runtime invariants you can rely on

The contract isn't only build-time. Several invariants the web-kernel maintains at runtime affect how your extension is called:

InvariantStatement
I-AH-1Tool args must not contain id-shaped values not seen in conversation history
I-AH-2v2Narration must not claim data the tools didn't return
I-AH-3Classifier hint must be in the closed enum
I-AH-4Narrator factual claims require backing
I-PYDANTIC-RETRY-BUDGETBounded retry — max 2 per tool_use on ValidationError
I-CONFIRMATION-EXECUTES-WHAT-USER-SAWConfirmation card args = byte-identical args on accept
I-CHAIN-TYPED-PIPEChain step outputs flow as typed ctx.prior namespaces
I-EXT-SYSTEM-TASK-NO-MESSAGE-KWARGSkeleton/system tasks never receive message= kwarg
I-EXT-SCHEMA-DESCRIPTION-STRING-DRAFT-2020-12Tool schemas use string descriptions (never tuples — typo killer)
I-EXT-MODULE-ISOLATIONEach extension's Python modules load in an isolated namespace. You can name your modules app.py, handlers.py, config.py, models.py etc. without worrying about cross-extension collisions. Keep imports at the top of the file (no local from app import X inside handler bodies — the binding can freeze against another extension's module if it loaded first in the same worker)
I-CHAT-FUNCTION-VERBATIM-PARAMSLegacy ChatExtension(tool_name=...) wrappers MUST instruct the LLM in their system_prompt to pass the user's message verbatim. Otherwise the wrapper-LLM paraphrases and corrupts the typed-params contract. Prefer migrating to direct typed @chat.function dispatch — chain_callable=True is now the default for every action_type (v4.2.10+) so reads also typed-dispatch in chains
I-CAPABILITY-LIST-MUST-BE-GROUNDEDWebbee's "what can you do" answer is grounded in installed-extension manifest display_name + description. As an extension author, your manifest description IS your capabilities answer — write it accurately, name the actual user-facing capability, and the platform paraphrases verbatim
I-CAPABILITIES-USER-INSTALLED-ONLYThe platform never surfaces the platform-wide catalogue to a user's chat — only their installed apps (plus system apps). Public privacy/isolation contract
I-CONTEXT-WINDOW-ADMIN-CONFIGURABLETenant operators can tune the chat conversational-history window via Admin > LLM Config. Extension authors don't address this directly, but should not assume an unbounded history when designing skeletons or chained workflows
I-LONGRUN-HTTP-CAP-180Sctx.http(..., timeout=N) per-call kwarg is capped at 180 seconds federally. Larger timeouts raise ValueError pointing to ctx.background_task(). For ops legitimately exceeding 3 minutes, use the background-task primitive (v4.2.12+)
I-LONGRUN-BG-CORO-RETURNS-ACTIONRESULTThe coroutine passed to ctx.background_task() MUST return ActionResult. Non-ActionResult return triggers a critical audit row and delivers a fallback error message to chat (v4.2.12+)
I-LONGRUN-BG-USER-SCOPEDEvery background task is bound to (ext_id, user_id) at creation. Cross-user cancel/status returns 403. Federal task-lifecycle invariant inherited from the web-kernel task manager (v4.2.12+)
I-LONGRUN-CHAT-INJECT-USER-SCOPEDctx.deliver_chat_message() is scoped to (ext_id, user_id); cross-user inject returns 403. Enforced server-side via the acting-user header check (v4.2.12+)
I-LONGRUN-CHAT-INJECT-AUDIT-EVERYEvery ctx.deliver_chat_message() call writes an audit row. Enforced at the chokepoint, not by extension policy (v4.2.12+)
I-SECRETS-USER-SCOPEDEvery secret read/write is scoped to one user; cross-user access returns 403
I-SECRETS-NEVER-LOGGEDPlaintext never appears in action_ledger, journals, error responses, Temporal events, or backups
I-SECRETS-EXT-SCOPEDExtension A cannot read extension B's secrets for the same user
I-SECRETS-VAULT-DEPENDENCYPlatform KMS downtime → fail-closed; no fallback decryption, no plaintext cache
I-SECRETS-AUDIT-FOREVEREvery secret operation writes retention_class='security_forever' (survives retention purges)
I-SECRETS-HANDLER-SCOPE-MEMORYPlaintext lives only inside one handler call's stack; SDK has no cache between calls
I-SECRETS-CONTRACT-DECLAREDManifest is the single source of truth; reading an undeclared name raises at runtime
I-BILLING-ENFORCE-UNIFIEDEvery billable code path dispatches through a single deduct_for_action() chokepoint; legacy direct-wallet writes are forbidden. BILLING_ENFORCE=true in production
I-WALLET-PERSISTENTAll four wallet Lua scripts (CHECK_AND_DEDUCT, RESERVE, SETTLE, CREDIT) call PERSIST KEYS[1] atomically; wallet keys are TTL-free by construction
I-WALLET-SWEEP-ACTIVEHourly Temporal WalletPersistSweepWorkflow (namespace imperal-billing) re-runs PERSIST on any wallet keys that regained a TTL. Defence-in-depth; no cron — Temporal only
I-BYOLLM-PLATFORM-FEE-ZEROWhen ctx.user.is_byollm == True, platform_fee → 0 at every deduct site. Resolved at deduct time via async user_llm_config lookup with 60s Redis cache
I-NARRATOR-NO-FABRICATED-TOKEN-CLAIMSNarrator MUST NOT emit cost claims to the user unless the claim is sourced from an actual deduct event. Regex+whitelist detector at delivery chokepoint strips fabricated phrases
I-KAV-CONF-EXPIRES-AT-INT-SECONDSKAV writer emits expires_at as int seconds (mirrors Redis expires_at_ts). ISO string forbidden
I-CHAIN-PRIOR-RESULTS-STRUCTUREDChain executor plumbs structured prior_step_results: list[dict] alongside the text summary; downstream LLMs receive verbatim ActionResult.data
I-CHAIN-READ-BEFORE-WRITE-DEPENDENCIESClassifier action_plans[] schema requires depends_on: list[str]; web-kernel applies stable Kahn's topological sort before iteration so reads precede dependent writes
I-PANEL-RENDERING-CONTRACTPANEL_SLOT_RENDERING_STATUS is the single source of truth for which slots the Imperal Panel host actually renders. CI gate tests/test_panel_rendering_contract.py enforces symmetry with ALLOWED_PANEL_SLOTS
I-MANIFEST-EMITTER-SCHEMA-SYMMETRICtests/test_manifest_roundtrip_gate.py builds a canary Extension exercising every emitter code path and asserts every field round-trips through validate_manifest_dict with zero issues
I-USER-ROLE-AUTHORITATIVE_resolve_user_info cache TTL is bounded at 30s; never caches results with empty role; WARN-logs on auth-gw 200 with empty role. Closes silent admin-demote-for-5-minutes class
I-PARAMS-NO-PLACEHOLDER-VALUESSDK check_placeholder_args guard rejects any tool call whose arg values match LLM-placeholder pattern (<UNKNOWN>, <TODO>, <MISSING>, <EMAIL>, <PASSWORD>, <USER_ID>, ...) before write-arg-bleed + target-scope + 2-step confirmation. Tight regex ^<[A-Z][A-Z0-9_]*>$, full-anchored — substrings inside prose and lowercase HTML/XML tags do not trip the guard (v4.2.15+)
I-CHAIN-PREFLIGHT-RESPECTS-USER-TOGGLEPre-flight confirmation cards consult kctx.confirmation_enabled + kctx.confirmation_actions on both the single-action hub.handle_hub_chat path and the multi-step session_workflow.py Phase 2B-2 path. Users with 2-step OFF see uniform behaviour across plan shapes

Full list with statements and enforcement sites: Federal invariants.

What v4.1+ added on top

VersionAdded
v4.1.0Pydantic feedback loop — bounded retry on ValidationError with structured prose feedback
v4.1.1emit_narration audit-mode scope clarification (BYOLLM placeholder leak fix)
v4.1.2@chat.function(id_projection="...") for compound tool names
v4.2.0Extension(system=True) flag + V31 validator. Three new invariants: I-SYSTEM-APPS-NEVER-UNINSTALLABLE, I-MARKETPLACE-HIDES-SYSTEM, I-SYSTEM-FLAG-RESERVED-FOR-IMPERAL. See System Apps.
v4.2.1Validator MANIFEST-SKELETON-1 false-positive fix — no longer flags @ext.tool("skeleton_alert_*") (the documented, web-kernel-supported pattern). See @ext.skeleton reference for the canonical refresh + alert pairing.
v4.2.2EXT-SECRETS-V1 — @ext.secret decorator + ctx.secrets accessor + Manifest.secrets[] optional array. Seven new federal invariants (I-SECRETS-USER-SCOPED / NEVER-LOGGED / EXT-SCOPED / VAULT-DEPENDENCY / AUDIT-FOREVER / HANDLER-SCOPE-MEMORY / CONTRACT-DECLARED). Closes ARCH-D1 (Vault integration for per-user encrypted credentials). See @ext.secret reference for the full surface.
v4.2.3EXT-SECRETS-V1 UX polish — synthetic secrets panel auto-injected on first @ext.secret(...) declaration (slot right, title Secrets, icon KeyRound). Superseded by v4.2.4.
v4.2.4EXT-SECRETS-V1 — synthetic secrets panel registration becomes unconditional in Extension.__init__. Every extension gets the Secrets tab regardless of whether it declares any secrets.
v4.2.5Validator tool_count (V3 "at least one tool" + marketplace display counts) excludes synthetic-prefix tools: __panel__*, __widget__*, __tray__*, __webhook__*. Fixes V3 detection masked by the always-present __panel__secrets from v4.2.4.
v4.2.6New ui.Password primitive + ui.Input(type=) kwarg — browser-blind credential-entry input (<input type="password" autocomplete="new-password" spellcheck="false">). Required for EXT-SECRETS-V1 write surfaces. See ui.Password.
v4.2.7OAuth callback infrastructure end-to-end: panel.imperal.io/v1/ext/* accepts both GET (OAuth 302 redirects, verification challenges) and POST (server-to-server hooks) on the same path; new Context.webhook_url(path) SDK helper; public marketplace endpoint for a tenant's webhook URLs; Panel Secrets-tab redirect-URI hint; Dev Portal Webhooks tab. New invariants: I-EXT-CACHE-APP-ID-WEB-KERNEL-AUTHORITATIVE, I-WEBHOOK-URL-CANONICAL.
v4.2.8SecretDecl added to Manifest Pydantic schema (manifest_schema.py). Closes I-MANIFEST-EMITTER-SCHEMA-SYMMETRIC for secrets[] — emitter wrote the field since v4.2.2 but the strict schema didn't declare it. Strict validation now rejects malformed secret entries at publish time.
v4.2.9Regenerated static imperal.schema.json to match the v4.2.8 Pydantic schema; v4.2.8 missed the regen step and the CI gate test_static_schema_matches_runtime_export[imperal] caught the drift on the release commit.
v4.2.10@chat.function(chain_callable=) default flipped to True for all action_type values (was write/destructive only). Closes wrapper-LLM paraphrase risk on typed read handlers (list_*/search_*/get_*); reads now typed-dispatch in chains automatically. Authors needing the catch-all wrapper-LLM loop set chain_callable=False explicitly. See @chat.function reference.
v4.2.11ui.Link(label=, text=) accepts either kwarg (alias) — earlier signature accepted only label= and text= raised TypeError mid-render, breaking the right panel. See ui.Link.
v4.2.12LONGRUN-V1 Session 1 — three SDK primitives for ops longer than the 30s ctx.http default: ctx.http(..., timeout=N) per-call kwarg (federal cap 180s); ctx.background_task(coro, long_running=) explicit fire-and-detached (cap 180/1800s); ctx.deliver_chat_message(text) extension-initiated bot-turn injection. Five new federal invariants (see I-LONGRUN-* rows in the runtime-invariants table above).
v4.2.13LONGRUN-V1 Component D — @chat.function(background=True, long_running=False) declarative sugar. SDK auto-wraps the call in ctx.background_task() under the hood; LLM receives an immediate ack with task_id; platform auto-delivers the handler's returned ActionResult as a fresh bot turn when done. Manifest emission adds tools[].background + tools[].long_running booleans. See @chat.function — background sugar.
v4.2.14Regenerated imperal.schema.json static mirror to match v4.2.13 Pydantic schema; v4.2.13 missed the regen step and the same CI gate that caught v4.2.8 caught the drift on the v4.2.13 release commit. Pin >= 4.2.14 to skip the temporarily-drifted v4.2.13 sdist.

See the changelog for migration notes.

How publishing enforces this

You run: imperal build .   →   imperal validate .

SDK runs all 11 validators locally

   Any ERROR? → publish refused, list shown
   All pass? → upload the packaged extension at
              panel.imperal.io/developer (drag-and-drop)

Dev Portal re-runs validators server-side (defense in depth)

Marketplace listing created / updated

You cannot ship a non-conformant extension. The contract is enforced at three layers — your local CLI, the upload server, and the web-kernel reload step.

Migrating from v3.x

If you have an extension on SDK v3.x:

Bump the SDK pin

pip install --upgrade imperal-sdk

Re-run build

imperal build

This regenerates the manifest as v3. It will likely fail validators — that's expected.

Fix validator failures one by one

Read the error list. Most common:

  • V14: lengthen description
  • V17: add Pydantic params model (module-scope)
  • V18: add return annotation
  • V19: add chain_callable=True to writes
  • V21: provide a real SVG icon

Re-publish

imperal build .
imperal validate .
# Then upload the packaged extension via panel.imperal.io/developer

The migration is mostly mechanical — typically 1–2 hours per extension.

Where to next

On this page