Federal Extension Contract
The Federal Extension Contract every published Imperal extension satisfies: manifest schema v3, the federal validators, and runtime behaviour you can rely on.
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
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
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 shape from
data_model=โ ansdl.Entityor a concretesdl.EntityList[T]โ emits the SDLreturn_schema(with field roles) into the manifest. Required for reads (V23), recommended for writes (V24). chain_callabledefaults toTruefor all action types since v4.2.10 โ reads now type-dispatch in chains too, eliminating wrapper-LLM paraphrase drift onlist_*/search_*/get_*handlers
The 11 federal validators
All ERROR severity. Failing any blocks publishing.
V23 / V24 โ SDL Typed Return Contract
Two related rules govern typed SDL return shapes. V23 requires data_model= โ an sdl.Entity or a concrete sdl.EntityList[T] โ on every action_type="read" tool, so the platform can read the entity's roles, validate $REF paths, and prevent input/output field-name drift. This is the live SDL enforcement: WARN today, env-promotable to ERROR. V24 recommends the same SDL data_model= on write/destructive tools (WARN-only). Note this typed-return V24 is distinct from the skeleton-access check (V24-AST) above โ see the SDL reference and Validators reference.
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.
{
"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)" }
}
}owner_chat_tool is a manifest reference field โ a back-pointer naming the ChatExtension's tool_name that owns this tool. It is descriptive metadata, not an independently registered, callable tool: the kernel dispatches the typed tools[] entries directly and does not resolve owner_chat_tool as a separate orchestrator tool.
See the full manifest reference for every field.
Platform-side guarantees
The contract is two-sided. You satisfy the build-time validators. The platform holds these runtime guarantees:
Typed dispatch in chains
When your tool has chain_callable=True and a chain plan includes it, the platform dispatches it with Pydantic-validated args โ no extra LLM round inside your extension.
Pre-flight anti-hallucination
Fabricated IDs, ungrounded narration, and out-of-enum hints are caught before your handler runs.
Confirmation chokepoint
Destructive actions are intercepted and shown to the user; on accept, exactly what the user saw is executed โ byte-identical args, no LLM rerun.
Audit chokepoint
Every action that reaches your handler also reaches the federal [action ledger](/en/reference/glossary/). No bypass.
Runtime behaviour you can rely on
The contract isn't only build-time. The platform maintains these guarantees at runtime โ they affect how your extension is called:
| Guarantee | What you can rely on |
|---|---|
| Typed chain dispatch | When a chain step targets your chain_callable=True tool, the platform calls it with Pydantic-validated args from the prior step โ no extra LLM round in your extension. Since v4.2.10 this includes read tools (list_*/search_*/get_*). |
| Bounded validation retry | If the model passes args that fail Pydantic validation, it gets a small, bounded number of structured retries before the call fails. |
| Confirmation fidelity | On a confirmation card, accepting runs exactly what the user saw โ the same args, with no model rerun. |
| Structured chain hand-off | A downstream chain step receives the prior step's structured ActionResult.data projected into its own typed params โ not a paraphrase. |
| Read-before-write ordering | In a multi-step plan, reads are ordered before the writes that depend on them. |
System tasks get no message= | Skeleton/system-initiated calls never receive a message= kwarg, so your handler can rely on its absence to detect that path. |
| String tool-schema descriptions | Tool schema descriptions are always emitted as strings (never tuples). |
| Module isolation | Each extension's Python modules load in an isolated namespace, so you can name modules app.py, handlers.py, config.py, models.py etc. without cross-extension collisions. Keep imports at the top of the file โ a local from app import X inside a handler body can bind against another extension's module if it loaded first in the same process. |
| Capabilities are grounded | Webbee's "what can you do" answer is grounded in your installed extension's manifest display_name + description. Your manifest description IS your capabilities answer โ write it accurately and name the real user-facing capability. |
| Installed-apps only | The platform never surfaces the full platform-wide catalogue to a user's chat โ only their installed apps (plus system apps). |
| Tunable history window | Tenant operators can tune the conversational-history window. Don't assume an unbounded history when designing skeletons or chained workflows. |
| HTTP timeout cap (180s) | ctx.http(..., timeout=N) is capped at 180 seconds. Larger values raise ValueError pointing you at ctx.background_task() (v4.2.12+). |
Background coroutine returns ActionResult | A coroutine passed to ctx.background_task() MUST return ActionResult; anything else delivers a fallback error message to chat and records a critical audit row (v4.2.12+). |
| Background tasks are user-scoped | Each background task is bound to (your extension, user); cross-user cancel/status returns 403 (v4.2.12+). |
| Chat injection is user-scoped + audited | ctx.deliver_chat_message() is scoped to (your extension, user) โ cross-user inject returns 403 โ and every call writes an audit row (v4.2.12+). |
| Secrets are user- and extension-scoped | Every secret read/write is scoped to one user (cross-user access returns 403), and one extension cannot read another's secrets for the same user. |
| Secrets never logged | Secret plaintext never appears in the audit ledger, journals, error responses, chat history, workflow event history, or backups. |
| Secrets fail-closed | If the platform key service is unavailable, secret reads fail closed โ no fallback decryption, no plaintext cache. |
| Secret audit survives purges | Every secret operation writes an audit row that survives retention purges. |
| Secret plaintext is call-scoped | Secret plaintext lives only inside one handler call; the SDK keeps no cache between calls. |
| Undeclared secret reads raise | Your manifest is the single source of truth โ reading a name you didn't declare raises at runtime. |
| Uniform billing | Every billable path is metered uniformly; what you see charged matches what was deducted. |
| Wallet balance is durable | Wallet balances are never lost, and a recurring platform-managed background job re-asserts that durability. |
| BYOLLM platform fee is zero | When the user is on BYOLLM, the platform fee is zeroed at every charge point โ transparently, with no accessor your code needs to read. |
| No fabricated cost claims | The agent never tells the user a cost figure unless it came from a real charge event. |
| Placeholder args rejected pre-dispatch | A tool call whose arg values look like placeholder sentinels (e.g. <UNKNOWN>, <TODO>) is rejected before it reaches your handler โ no billing, no audit pollution (v4.2.15+). |
| Confirmation toggle respected uniformly | The user's 2-step-confirmation setting is honoured consistently across single-action and multi-step plans. |
Author-facing behaviour guarantees in plain language: Federal invariants.
What v4.1+ added on top
| Version | Added |
|---|---|
| v4.1.0 | Pydantic feedback loop โ bounded retry on ValidationError with structured prose feedback |
| v4.1.1 | emit_narration audit-mode scope clarification (BYOLLM placeholder leak fix) |
| v4.1.2 | @chat.function("my_tool", id_projection="...") for compound tool names |
| v4.2.0 | Extension(system=True) flag + V31 validator. System apps are auto-installed for every user, hidden from the marketplace, can never be uninstalled, and the system=True flag is reserved for Imperal first-party authors. See System Apps. |
| v4.2.1 | Validator 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.2 | EXT-SECRETS-V1 โ @ext.secret decorator + ctx.secrets accessor + Manifest.secrets[] optional array. Secret storage guarantees land in one release: per-user and per-extension scoping, plaintext never logged, fail-closed reads, audit rows that survive purges, call-scoped plaintext (no cross-call cache), and manifest-declared names enforced at runtime. Per-user encrypted credentials are stored in the platform's managed secret service. See @ext.secret reference for the full surface. |
| v4.2.3 | EXT-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.4 | EXT-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.5 | Validator 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.6 | New 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.7 | OAuth 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. The webhook URL returned by the helper is the single canonical URL for your extension, and your app's identity always resolves the same way across the platform. |
| v4.2.8 | SecretDecl added to the Manifest Pydantic schema. The manifest's secrets[] array is now part of the strict schema โ the emitter wrote the field since v4.2.2 but the strict schema didn't declare it, so the two are now in sync. Strict validation rejects malformed secret entries at publish time. |
| v4.2.9 | Regenerated 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("my_tool", 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.11 | ui.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.12 | LONGRUN-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. The long-running guarantees these carry (HTTP timeout cap, background coroutine return contract, user-scoped background tasks, user-scoped + audited chat injection) are the long-running rows of the runtime-behaviour table above. |
| v4.2.13 | LONGRUN-V1 Component D โ @chat.function("my_tool", 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.14 | Regenerated 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 / updatedYou 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-sdkRe-run build
imperal buildThis 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=Trueto writes - V21: provide a real SVG icon
Re-publish
imperal build .
imperal validate .
# Then upload the packaged extension via panel.imperal.io/developerThe migration is mostly mechanical โ typically 1โ2 hours per extension.
Where to next
Complete API surface
The complete Imperal SDK API on one page โ every decorator, ctx attribute, manifest field, federal validator, action type, error code and runtime guarantee.
Manifest reference
Every field in the Imperal SDK imperal.json manifest (schema v3): what each does, required vs optional, accepted values โ auto-generated by imperal build.