Imperal Docs
SDK Reference

Changelog

Release notes for the imperal-sdk Python package — current major v5.x, historical v4.x

Release history for the imperal-sdk Python package. Migration notes are inline. Older minor-release entries are condensed; full per-tag detail lives in the package's CHANGELOG.md.

v5.0.1 — 2026-05-17

Typed return contract for @chat.function (additive, no breaking changes).

Declare the shape of your tool's ActionResult.data and the platform validates it for you — at emit time, in chain $REF paths, and in the LLM tool catalog. Extensions built against v5.0.0 keep working unchanged.

Added

  • @chat.function(data_model=YourModel) — pass a Pydantic BaseModel subclass describing the shape of ActionResult.data. The platform validates returned data against your model at emit time (warn-only in v5.0.1), surfaces the schema to the LLM tool catalog, and lets other extensions reference your fields safely via $REF in multi-step chains.
  • ActionResult[T] generic auto-detection. -> ActionResult[NoteRecord] return annotations now infer the data model automatically. Resolution order: explicit data_model= kwarg → direct -> SomeBaseModel return annotation → -> ActionResult[T] generic extraction.
  • ActionResult.validate_against(model_class) — opt into validation inline when you cannot declare the model on the decorator.

Validator changes

  • V23 (WARN) — flags chain-callable write/destructive tools without data_model=. Recommendation, not a publish blocker. Will promote to ERROR in a future minor once adoption stabilises.

Why this matters for you

If your tool's data is consumed downstream via a chain reference like $REF:<your-app>[0].some_field, declaring data_model= catches broken references at publish time rather than at runtime. For single-step tools, it is purely an opt-in safety net.

v5.0.0 — 2026-05-15 — ChatExtension simplification

ChatExtension is now a thinner wrapper around your @chat.function handlers — the manifest no longer emits an umbrella orchestrator tool, and one constructor kwarg style is deprecated. Your handlers themselves keep working without changes.

What changed

  • Manifest no longer emits tool_<your-ext>_chat umbrella tools. The platform refuses any manifest that still contains such entries at publish time (new validator V25, ERROR severity). Most existing extensions get this for free — a clean rebuild against v5.0.0 drops the entry automatically.
  • ChatExtension(tool_name=..., ...) kwarg-form deprecated. Pass tool_name positionally instead. The kwarg-form still works at runtime but emits a DeprecationWarning log line on each instantiation.

How to migrate

  1. Rebuild against imperal-sdk>=5.0.0 and redeploy via the Developer Portal. The manifest emitter drops the tool_<your-ext>_chat entry automatically.
  2. Switch ChatExtension(ext=..., tool_name="...", ...) to positional form: ChatExtension(ext, "tool_<your-ext>_chat", ...). Silences the DeprecationWarning. The description=, system_prompt=, max_rounds= kwargs continue to work as before.

That's the whole migration. Your @chat.function handlers keep working unchanged — only the ChatExtension constructor call style changes.

Why this matters for you

Cleaner separation of responsibilities: your SDK code declares tools and writes business logic, the platform routes between them. Multi-extension chains that consume your tool's output preserve full structured data per step (no more aggregated .data from sibling tool calls leaking into yours).

v4.2.16 — 2026-05-15

Operator log marker for hallucinated tool names.

When the LLM tries to call a function that does not exist on your extension, the rejection log line now carries the UNKNOWN_FUNCTION(will-reject) marker. The call was already rejected before — this is purely a log-format change so operators can grep for the marker and track hallucination rates per extension.

No SDK API change. No behaviour change for your code. If you do not read worker journals, this release is invisible to you.

v4.2.15 — 2026-05-14

Feat: federal placeholder-args guard (I-PARAMS-NO-PLACEHOLDER-VALUES)

New ChatExtension guard that rejects any tool call whose argument values look like LLM-emitted placeholder sentinels — e.g. <UNKNOWN>, <TODO>, <MISSING>, <EMAIL>, <PASSWORD>, <USER_ID>. Runs before write-arg-bleed, target-scope, and 2-step confirmation guards, so a dispatch carrying a placeholder is short-circuited before any billing-charged work or audit-ledger pollution. The rejection text is shaped as an instruction back to the LLM so the chat loop feeds it through as a synthetic tool_result and the LLM self-corrects by asking the user a clarifying question.

Motivation. When the user's message omits a required field, the wrapper LLM occasionally substitutes a placeholder token (<UNKNOWN>) instead of asking. Before this guard, the dispatch went through; the downstream anti-fab layer correctly caught the drift on the response side (server did not reflect 'email': requested '<UNKNOWN>', got None), but by then billing tokens had been spent, action_ledger had a target=<UNKNOWN> row, and the end user saw an opaque failure. The new guard fails fast on the request side.

Added

  • check_placeholder_args(tu, action_type) -> str | None in imperal_sdk.chat.guards — recursive scan of tu.input (dict / list / tuple / str) for values matching the tight regex ^<[A-Z][A-Z0-9_]*>$. Full-anchored — substrings inside prose (e.g. an error message body containing <UNKNOWN>) do not trip the guard. Whitespace-tolerant via .strip(). Lowercase HTML/XML tags such as <html> or <unknown> are explicitly ignored.

  • _PLACEHOLDER_RE and _scan_for_placeholders(value) module-private helpers. The recursive scan walks dict values and list/tuple items; non-string values are skipped.

  • Integration into the check_guards() orchestrator as the first layer — placeholders are rejected before any other guard runs.

Federal invariants (new)

  • I-PARAMS-NO-PLACEHOLDER-VALUES — registered in the web-kernel federal invariant table (imperal_kernel/tests/federal/_invariant_assertions.py). The standalone source-inspection test at tests/test_i_params_no_placeholder_values.py asserts the regex contract, dict/list recursion, full-anchor (no substring), empty-input no-op, integration ordering inside check_guards(), and SDK signature shape (tu, action_type).

Migration

Zero migration. Strictly additive — default-allow surface, no manifest schema change, no API break. Existing extensions emit no placeholder sentinels in legitimate flows, so the guard is a no-op for every real call. PATCH bump per the project's conservative semver convention (additive features ship as PATCH; MINOR is reserved for "existing ext must rebuild to stay valid"; MAJOR for runtime API breakage).

Pin >= 4.2.15 to get the new guard.

v4.2.14 — 2026-05-14

Fix: regenerated imperal.schema.json static mirror to match runtime Manifest model

v4.2.13 added background + long_running to the runtime Tool Pydantic model but forgot to regenerate the static imperal.schema.json ship-alongside file. The CI gate caught the drift on the release commit (same pattern as the v4.2.8 → v4.2.9 fix). No public API change.

Pin >= 4.2.14 directly to avoid pulling the temporarily-drifted v4.2.13 sdist.

v4.2.13 — 2026-05-14

Feat: @chat.function(background=True) declarative flag

Sugar wrapper over ctx.background_task() from v4.2.12. Author writes a single handler body; the SDK auto-wraps the call in ctx.background_task() when the flag is set.

Added

  • @chat.function(..., background=True, long_running=False) — when background=True, the SDK chat handler wraps the function call in ctx.background_task() instead of running it synchronously. The LLM receives an immediate ack envelope carrying task_id; the platform delivers the handler's returned ActionResult as a fresh bot turn when the work finishes. long_running=True raises the federal 180-second cap to 1800 seconds.

    @chat.function(
        "refine_output",
        description="Refine the given text via AI completion.",
        action_type="write",
        event="text_refined",
        background=True,        # auto-wrap in ctx.background_task
        long_running=False,     # default cap 180s; True → 1800s
    )
    async def refine_output(ctx, params: RefineParams) -> ActionResult:
        # Body runs detached. No inner _work() wrapper, no manual task_id.
        await ctx.progress(50, "Generating with AI")
        resp = await ctx.http.post(api_url, json={...}, timeout=120)
        return ActionResult.success(
            summary="Refined output ready!",
            data={"text": resp.body["text"]},
        )
  • Manifest emissiontools[] entries in imperal.json now carry background and long_running booleans. Strict Manifest.Tool Pydantic schema gains matching optional fields.

When to use sugar vs. explicit ctx.background_task(coro)

  • Sugar (background=True) — handler body is entirely the long work, you're happy with the platform's auto-ack summary.
  • Explicit (ctx.background_task(coro)) — you need a custom acknowledgement summary, want to choose background_task() conditionally at runtime, or run mixed sync + background work in the same handler.

Migration

None required — background defaults to False. Existing @chat.function handlers work unchanged.

v4.2.12 — 2026-05-14

Feat: long-running operations primitives

Three new SDK surfaces for ops that exceed the 30-second ctx.http ceiling.

Added

  • ctx.http.{get,post,put,patch,delete}(..., timeout=N) per-call kwarg. Federal cap 180 seconds (I-LONGRUN-HTTP-CAP-180S). Anything larger raises ValueError("ctx.http timeout {N}s exceeds federal cap (180s)...") — use ctx.background_task() for longer ops.

    resp = await ctx.http.post("https://api.example.com/v1/...",
                               json={...}, timeout=120)
  • ctx.background_task(coro, *, long_running=False, name="") -> str — explicit opt-in for the web-kernel's auto-promote path. The coroutine runs detached; the platform auto-delivers its returned ActionResult as a fresh bot message to the user's chat when done. long_running=True raises the 180s cap to 1800s. Returns a task_id immediately so the caller can return an acknowledgement to chat without blocking.

    @chat.function(action_type="write")
    async def start_refinement(ctx, params: StartParams) -> ActionResult:
        async def _work():
            await ctx.progress(20, "Fetching context")
            ctx_data = await ctx.http.post(retrieval_url, json={...}, timeout=15)
            await ctx.progress(50, "Generating with AI")
            out = await ctx.http.post(openai_url, json={...}, timeout=120)
            await ctx.progress(90, "Saving")
            return ActionResult.success(
                summary="Refined output ready! 🎉",
                data={"text": out.body["text"]},
            )
    
        task_id = await ctx.background_task(_work(), long_running=False,
                                            name="AI refinement")
        return ActionResult.success(
            summary="Got it — refining (≈90s). I'll send the result here.",
            data={"task_id": task_id},
        )
  • ctx.deliver_chat_message(text, *, msg_type="response", refresh_panels=None) — public API for extension-initiated bot-turn injection at any time (not tied to task completion). Text truncated to 64KB with marker. msg_type{response, system, tool_result}.

    @ext.webhook("/callback", method="GET")
    async def oauth_callback(ctx, params):
        # ... exchange code, store refresh token via ctx.secrets ...
        await ctx.deliver_chat_message("Spotify connected! 🎵 You can now ask "
                                       "what's playing or search your library.")
        return ActionResult.success(summary="OAuth complete")

Federal invariants (new)

  • I-LONGRUN-HTTP-CAP-180S — per-call timeout federal cap. Anything larger must use ctx.background_task().
  • I-LONGRUN-BG-CORO-RETURNS-ACTIONRESULT — the 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.
  • I-LONGRUN-BG-USER-SCOPED — every background task is bound to (ext_id, user_id) at creation. Cross-user cancel/status returns 403.
  • I-LONGRUN-CHAT-INJECT-USER-SCOPED — chat inject scoped to (ext_id, user_id). Cross-user inject returns 403.
  • I-LONGRUN-CHAT-INJECT-AUDIT-EVERY — every chat inject writes an audit row.

Migration

None required — all additions are additive opt-in. Existing extensions work unchanged.

v4.2.11 — 2026-05-13

Fix: ui.Link(text=...) no longer breaks panel render

Before v4.2.11, ui.Link only accepted the visible text as label=. Passing the natural-looking text= kwarg raised TypeError: Link() got an unexpected keyword argument 'text' at panel-render time, which killed the right panel for any extension that used the alias.

Changed

  • ui.Link now accepts the visible text via either label= (canonical) or text= (alias). The two are interchangeable — label wins if both are passed.
  • Calling ui.Link() with neither raises a clear TypeError("ui.Link requires a 'label' (or 'text' alias)") instead of silently rendering an empty anchor.
# All three are equivalent
ui.Link("Read docs", href="https://docs.imperal.io")           # positional
ui.Link(label="Read docs", href="https://docs.imperal.io")     # canonical kwarg
ui.Link(text="Read docs",  href="https://docs.imperal.io")     # alias kwarg

Migration

None required — existing label= callers are unchanged.

v4.2.10 — 2026-05-13

Federal: chain_callable=True is now the default for ALL action_type values

Before v4.2.10 the auto-default for chain_callable was True for write/destructive and False for read. That meant typed read handlers (list_*, search_*, get_*) wouldn't participate as first-step data providers in multi-step chains unless authors set the flag explicitly — most didn't, so chains routinely fell back to wrapper-LLM dispatch for reads and lost structured-data fidelity between steps.

Changed

  • @chat.function(chain_callable=...) now defaults to True regardless of action_type. Typed read handlers join chains automatically.
  • Behavior is fully backward-compatible: authors who set chain_callable=False explicitly keep their override.

Why this matters

In a chain like "find my pending tasks and email them to my manager", step 1 (list_tasks) is a read. With the new default, the web-kernel issues a typed tasks/list_tasks(args) dispatch — Pydantic-validated, no wrapper-LLM round in your extension — and the structured output flows verbatim into step 2's send_email params. The classifier still routes open conversational reads ("what's in my inbox today?") to the wrapper-LLM chat path; the difference is intent classification, not the flag.

Migration

None required for first-party reads. If your extension has a catch-all conversational handler that depends on the wrapper-LLM seeing raw user prose (a case_chat-style tool), set chain_callable=False explicitly so the typed-dispatch path is skipped.

Rebuild with v4.2.10 (imperal build .) to refresh imperal.json against the new auto-default.

v4.2.9 — 2026-05-13

Fix: regenerated static imperal.schema.json to match runtime Manifest model

v4.2.8 added SecretDecl + Manifest.secrets but forgot to regenerate the committed src/imperal_sdk/schemas/imperal.schema.json. The CI gate test_static_schema_matches_runtime_export[imperal] (federal — pins the runtime ↔ static contract) caught the drift on the v4.2.8 release commit.

Fixed

  • Regenerated src/imperal_sdk/schemas/imperal.schema.json from runtime manifest_schema.get_schema(). Static artifact now equals runtime model again.

No public API change. Single-file fix.

v4.2.8 — 2026-05-13

Federal: SecretDecl finally in Manifest Pydantic schema

EXT-SECRETS-V1 manifest emitter has been writing manifest["secrets"] = [...] since v4.2.2, but the Manifest Pydantic model in manifest_schema.py had no matching field. With model_config = ConfigDict(extra="forbid"), this should have caused validate_manifest_dict() to reject every manifest that declared secrets — but publish-time validators didn't gate through this schema, so the drift lived silently for six PATCH releases.

Added

  • SecretDecl Pydantic model in manifest_schema.py — mirrors imperal_sdk.secrets.spec.SecretSpec.to_manifest_dict(). Validates name regex (^[a-z][a-z0-9_]{0,62}$), write_mode in {user, extension, both}, max_bytes in [1, 65536], rotation_hint_days >= 1 when present, non-empty description.
  • Manifest.secrets: Optional[List[SecretDecl]] field — additive, back-compatible with manifests that don't declare any secrets.

Federal invariants

InvariantWhat it pins
I-MANIFEST-EMITTER-SCHEMA-SYMMETRICNow actually holds for secrets[] — emitter and schema agree.

Migration notes

  • No code change required in extensions. Existing manifests with secrets[] (emitted since v4.2.2) now pass strict Pydantic validation instead of relying on validators that didn't gate through the schema.
  • Manifests with malformed secret entries (e.g. name with uppercase, invalid write_mode, max_bytes outside [1, 65536]) will now fail validate_manifest_dict() at publish time. Previously they slipped through. See Manifest reference — secrets[].

v4.2.7 — 2026-05-13

OAuth callback infrastructure end-to-end + ctx.webhook_url() helper

Closes the architectural gap that made @ext.webhook("/callback", method="GET") non-functional for OAuth providers. Before this release: hardcoded redirect URIs landed users on a 404 (no public route for OAuth callbacks), and the request path internally was POST-only.

Added

  • Context.webhook_url(path) — builds the canonical public callback URL from the web-kernel-authoritative app identifier (the manifest's app_id, not the drift-prone Python Extension("X", ...) value). Returns https://{IMPERAL_PUBLIC_HOST}/v1/ext/{app_id}/webhook/{path} — default host panel.imperal.io. See @ext.webhook reference.
  • OAuth callback class fully supportedpanel.imperal.io/v1/ext/* now accepts both GET (OAuth 302 redirects, verification challenges) and POST (server-to-server hooks) on the same path.
  • Public GET /v1/marketplace/apps/{app_id}/webhooks endpoint — returns [{path, method}] for each declared @ext.webhook in the app's manifest. Used by the Panel Secrets tab UI and Dev Portal Webhooks tab.
  • Panel Secrets tab — blue info card lists every webhook URL with a Copy button so end-users know what to paste into OAuth provider developer consoles (Spotify Dashboard → Redirect URIs, etc.).
  • Dev Portal App Details → Webhooks tab — per-app catalogue of every declared @ext.webhook with method badge, canonical URL, and OAuth/server-to-server hints. imperal-ext-developer v1.3.0.
  • ctx.cache + ctx.secrets now available in webhook handlers — both moved to ContextFactory._build_context so every dispatch path (chat tool, panel, skeleton, schedule, webhook, lifecycle, health check) gets the same surface uniformly. _HealthCheckCtx gained _StubSecrets graceful no-op so @ext.health_check handlers that legitimately read ctx.secrets don't crash with AttributeError.

Migration notes

  • Replace hardcoded redirect URIs with ctx.webhook_url("/callback") at runtime. Hardcoded URLs are the #1 cause of OAuth drift bugs (Python Extension("spotify-extension", ...) ≠ deployed folder spotify ≠ auth-gw DB row). No code change is strictly required for existing extensions — the platform now accepts both POST and GET on the existing URL shape — but new extensions should prefer the helper.
  • Existing webhook handlers continue to work — nothing in the request envelope changed. POST handlers still receive the same (ctx, headers, body, query_params) quartet.
  • End-user setup flow becomes self-documenting — Panel Secrets tab and Dev Portal Webhooks tab both render the canonical URL with a Copy button; no more "where do I paste this?" support tickets.

v4.2.6 — 2026-05-13

New: ui.Password primitive + ui.Input(type=) kwarg

Adds a browser-blind credential-entry primitive for EXT-SECRETS-V1 Panel UIs. Renders as <input type="password" autocomplete="new-password" spellcheck="false"> so values are visually masked while the user types and don't get saved into the browser's autofill database.

Added

  • ui.Password(placeholder=, on_submit=, value=, param_name=) — canonical credential-entry component. EXT-SECRETS-V1 entry surfaces (Dev Portal Secrets tab, Panel SecretManagerCard equivalents) MUST use this instead of ui.Input for write_mode='user'/'both' secrets. Full reference: UI primitives reference.
  • ui.Input(type=) kwarg — accepts "text" (default), "password", "email", "number", "url". Backward-compatible: existing ui.Input(...) calls without type= continue rendering as text. The type prop is only emitted into the manifest when it differs from "text".

Panel rendering

DInput.tsx now reads type from props and applies it to the native <input type={...}> element. When type === "password" it also sets autoComplete="new-password" (suppresses browser autofill/save prompts) and spellCheck={false} (no red squiggle on opaque base64 / hex values).

Federal note

type="password" is a defence against shoulder-surfing, not a security control. The plaintext still travels in the POST body to the server, which is the only correctness boundary. The platform's audit chokepoint + KMS encryption are what make this federal-grade — see @ext.secret reference and the EXT-SECRETS-V1 contract in Federal contract.

v4.2.5 — 2026-05-13

Fix: synthetic __panel__* tools excluded from validator tool_count

v4.2.4 introduced an unconditional synthetic secrets panel via Extension.__init__, which auto-registers a __panel__secrets tool internally. The validator's tool_count logic was counting this synthetic tool as a user-authored tool, masking V3 ("at least one tool") error detection for extensions with zero user tools and inflating marketplace tool counts.

Fixed

  • validator.tool_count now excludes any tool whose name matches the synthetic-prefix allowlist (__panel__, __widget__, __tray__, __webhook__). These are platform-provided, not author-authored, and shouldn't count. See Validators reference for the updated semantics of V3.
  • V3 "at least one tool" check now correctly fires for extensions with only synthetic auto-registered tools.
  • tests/test_panels.py::test_multiple_panels updated to assert +1 for the always-present synthetic secrets panel.

Notes

  • No behavior change for extensions with at least one real @ext.tool or ChatExtension action.
  • Marketplace tool counts shown to users no longer count synthetic panels.

v4.2.4 — 2026-05-13

EXT-SECRETS-V1 — unconditional synthetic Secrets panel

In v4.2.3 the synthetic Secrets panel was registered conditionally on the first @ext.secret(...) call, which meant extensions that did not declare secrets had no menu entry — leaving end-users without a discoverable place to manage credentials when developers later add declarations.

This release flips the registration to unconditional: every Extension instance auto-registers the synthetic secrets panel in __init__ (slot right, title Secrets, icon KeyRound). When the manifest has zero declared secrets, the panel renders an empty state with developer guidance (@ext.secret(...) code example + link to docs). When declarations exist, it renders one card per secret with is_set status + Manage button.

Migration notes

  • No code changes required. Bump your ext's SDK pin to >= 4.2.4 and redeploy via Dev Portal — the Secrets tab appears automatically alongside any tabs you've declared yourself.
  • Extensions that genuinely never need credentials still get the tab; this is intentional for UX consistency. Federal V32 contract still requires @ext.secret for any real credential access at runtime — see @ext.secret reference.

v4.2.3 — 2026-05-13

EXT-SECRETS-V1 UX polish — synthetic secrets panel auto-injected on first declaration

When an extension declares one or more @ext.secret(...) entries, the SDK now auto-registers a synthetic secrets panel (slot right, title Secrets, icon KeyRound) so the user-facing Secrets manager appears alongside the extension's own tabs without the author writing any panel code.

Added

  • Auto-injected secrets panel (conditional on at least one @ext.secret(...) call). Superseded by v4.2.4 which makes registration unconditional — prefer pinning >= 4.2.4 directly.
  • The synthetic panel uses slot right defensively — most extensions use left (sidebar nav) and center (main content); right is rarely used so the panel-sync logic in imperal-ext-developer won't overwrite it. If your extension already declares a right-slot panel, your panel wins; users still reach the Secrets UI via the direct /ext/{ext_id}/secrets route.
  • Panel idempotent — multiple @ext.secret(...) calls register the panel only once.

Migration notes

  • This release is a stepping stone; prefer v4.2.4 for the canonical unconditional behaviour.

v4.2.2 — 2026-05-13

EXT-SECRETS-V1 — federal @ext.secret API + ctx.secrets accessor

Closes per-user encrypted credentials in the platform's compliance posture.

Extensions can now declare credentials the user supplies — third-party API keys, OAuth refresh tokens, webhook signing secrets — and read them in handlers via ctx.secrets.get(). Plaintext is encrypted by the platform KMS (AES-256-GCM, non-exportable key), stored as ciphertext in the encrypted-secrets store, and is never present in the audit ledger, journals, error responses, chat history, workflow event history, or backups.

Added

  • Extension.secret(name, description, *, required, write_mode, max_bytes, rotation_hint_days) declarative decorator. Full reference: @ext.secret reference.
  • ctx.secrets accessor with five methods: get(name), set(name, value), delete(name), is_set(name), list(). See API surface for full signatures.
  • Manifest.secrets[] optional array — additive field, manifest schema v3 stays. See Manifest reference.
  • imperal_sdk.testing.MockSecretStore for pytest fixtures with optional declared set to mirror SecretNotDeclaredError semantics.
  • Dev mode env-var fallback: IMPERAL_DEV_MODE=true + IMPERAL_SECRET_<UPPER_NAME> env vars feed ctx.secrets.get() without hitting the platform secrets endpoint.
  • Seven federal invariants enforced across SDK + auth-gateway:
    • I-SECRETS-USER-SCOPED, I-SECRETS-NEVER-LOGGED, I-SECRETS-EXT-SCOPED, I-SECRETS-VAULT-DEPENDENCY, I-SECRETS-AUDIT-FOREVER (auth-gateway-side)
    • I-SECRETS-HANDLER-SCOPE-MEMORY, I-SECRETS-CONTRACT-DECLARED (SDK-side) See Federal contract for definitions.
  • Five new exceptions at top-level imperal_sdk: SecretNotDeclaredError, SecretWriteForbidden, SecretVaultUnavailable, SecretValueTooLarge, SecretDeclarationConflict.
  • Panel UI — Imperal Panel now ships /ext/[extId]/secrets page where users set, rotate, and delete extension secrets. Input is type="password", browser-blind, with state cleared on submit. Federal contract — no echo, no clipboard, no show-toggle.

Migration notes

  • No breaking changes. Extensions without @ext.secret declarations continue to work unchanged. The secrets[] manifest field is optional.
  • Existing plaintext-stored credentials (BYOLLM keys in Redis hashes, OAuth refresh tokens in plain DB rows, etc.) are not automatically migrated. Extension authors migrate at their own pace by adding @ext.secret declarations and switching read sites from os.environ.get() / redis.hget() to await ctx.secrets.get().
  • V32 publish-time validator (IMPERAL_SECRET_DECLARED) will reject new extensions submitted to Dev Portal that read credential-like fields (os.environ.get("*KEY*"|"*TOKEN*"|"*SECRET*"|...), similar Redis hash reads) without a matching @ext.secret declaration. Use the bypass marker # imperal-allow-plaintext-credential: <reason> for legitimate cases (test fixtures, legacy bootstrap migrating).
  • Imperal-SDK version policy is conservative — additive features ship as PATCH per federal rule. v4.2.2 is a PATCH bump from v4.2.1 because all surface is additive and back-compatible.

v4.2.1 — 2026-05-11

Validator MANIFEST-SKELETON-1 false-positive fix

The local AST validator (validator_v1_6_0.py) was flagging @ext.tool("skeleton_alert_<section>") as a MANIFEST-SKELETON-1 ERROR with the suggestion "Replace with @ext.skeleton(<section>)".

That suggestion is wrong. @ext.skeleton(section, alert=True) registers only skeleton_refresh_<section>; the paired skeleton_alert_<section> handler must be registered separately with @ext.tool — and the web-kernel discovers it by tool-name presence in tools[]. There is no @ext.skeleton sugar for the alert handler.

The validator now flags only skeleton_refresh_* tools, leaving skeleton_alert_* as the documented, web-kernel-supported pattern. See @ext.skeleton reference and Skeletons concept for the canonical refresh + alert pairing.

Migration notes

  • If your CI was previously skipping MANIFEST-SKELETON-1 to work around the false positive on skeleton_alert_* tools, you can now enable it again. imperal build/imperal validate no longer fails on the documented canonical pattern.
  • No code changes required in any extension.

v4.2.0 — 2026-05-11

Federal system=True flag for platform-managed extensions

The four first-party Imperal extensions (admin, billing, developer, automations) are now declared as system apps via a new manifest field. System apps:

  • Auto-install for every user on registration — they appear in the sidebar bottom block without anyone clicking "Install".
  • Hidden from the marketplace — listing, featured, categories, and developer-profile queries all filter system = FALSE at the SQL layer.
  • Cannot be uninstalled — the auth-gw /v1/marketplace/apps/<id>/install DELETE endpoint returns 403 if the app is system=True.

Extension(system: bool = False) kwarg

ext = Extension(
    "billing",
    version="2.0.0",
    display_name="Billing",
    description="Imperal Cloud billing — usage meter, invoices, prepayments.",
    icon="icon.svg",
    system=True,   # ← new
)

The build emits "system": true at the top of imperal.json. Validator V31 fails locally if a non-Imperal author tries to set the flag; Dev Portal enforces the author allowlist server-side at publish time.

Federal invariants added

InvariantWhat it pins
I-SYSTEM-APPS-NEVER-UNINSTALLABLEAuth-gw refuses uninstall on system=True rows.
I-MARKETPLACE-HIDES-SYSTEMEvery /v1/marketplace/* SELECT has AND system = FALSE in WHERE.
I-SYSTEM-FLAG-RESERVED-FOR-IMPERALOnly authors in the Imperal allowlist may set the flag.

Migration notes

For the four first-party extensions: add system=True to your Extension(...) and re-publish through the Dev Portal. The live DB column was already backfilled during the Sprint B deploy, so marketplace already hides them — this just keeps manifest and DB consistent going forward.

For everyone else: nothing changes. system defaults to False. Third-party apps continue to publish, list, and install through the normal marketplace flow.

See /en/concepts/system-apps for the full lifecycle picture.

v4.1.9 — 2026-05-10

imperal init template now passes federal validators clean

Fixes the v4 onboarding cliff: imperal init my-ext && imperal validate used to surface 4 ERROR-severity validators on the very first run (V14 description ≥40 chars, V15 missing display_name, V16 description ≥20 chars, V21 missing icon.svg). New developers followed the docs, scaffolded, and got rejected — making the SDK look broken before they wrote a line of their own code.

New scaffold writes:

  • Extension(...) with all v4 required kwargs (display_name, description ≥40 chars, icon="icon.svg", actions_explicit=True, capabilities).
  • ChatExtension(...) properly declared (chat template only).
  • @chat.function with description ≥20 chars + Pydantic-typed param.
  • icon.svg placeholder file (V21-compliant — XML root + viewBox + ≤100 KB).
  • requirements.txt: imperal-sdk>=4.0.0 (was >=1.0.0).
  • tests/test_main.py exercises Pydantic param validation + MockContext async path.

Next-steps message updated to the canonical workflow:

pip install 'imperal-sdk>=4.0.0'
imperal build       # generates imperal.json
imperal validate    # runs V14-V22+V24+V31 federal validators
imperal test        # smoke-test handlers via MockContext
# Then upload the packaged extension at panel.imperal.io/developer

Migration: none required. Existing extensions are unaffected; only the template generated by imperal init changed.

v4.1.8 — 2026-05-10

Declarative center-overlay flag on @ext.panel

Replaces the legacy hardcoded TypeScript isCenterOverlay allowlist in the Imperal Panel host (usePanelDiscovery.ts) with a per-extension manifest field. Extensions that want the modal-style center surface (chat collapses to a 380 px right rail) declare it in code:

@ext.panel(
    "workshop",
    slot="center",
    title="Automation Workshop",
    icon="🔀",
    center_overlay=True,   # ← v4.1.8 — declarative; replaces hardcoded TS allowlist
)
async def workshop_panel(ctx, **kwargs):
    return ui.Stack([...])

The web-kernel publishes center_overlay: true into the panel's unified_config entry; the frontend reads the flag declaratively instead of consulting a hardcoded list of panel_id literals.

Backward compatibility: the legacy hardcoded allowlist (compose, email_viewer+message_id, editor+note_id, workshop) remains as a fallback for extensions that haven't redeployed since v4.1.7. It will be removed once those panel_ids migrate to declarative center_overlay=True.

Migration: add center_overlay=True to your @ext.panel(slot="center", ...) declaration and redeploy via panel.imperal.io/developer. No frontend code change required.

v4.1.7 — 2026-05-10

PANEL_SLOT_RENDERING_STATUS federal contract + I-PANEL-RENDERING-CONTRACT CI gate

Single source of truth in imperal_sdk.types.contributions for what the Imperal Panel host actually does with each declared slot. Three federal categories:

StatusWhat it meansSlots
permanentAlways-fetched at session-init, persistent columnleft, right
center-overlayOn-demand via __panel__<id> action when panel_id matches the host's isCenterOverlay allowlist; chat collapses to 380 px right railcenter
reservedAccepted by SDK validator but frontend has no render pathoverlay, bottom, chat-sidebar

tests/test_panel_rendering_contract.py enforces the symmetry — when a contributor adds a slot to ALLOWED_PANEL_SLOTS they MUST declare its rendering status. Closes the v4.1.x class of bug where extensions decorated with slot="overlay" etc. and the SDK accepted the registration but the frontend silently dropped them.

Companion: docs.imperal.io/concepts/panels.mdx rewrite — accurate slot table + ### How slot="center" actually activates walk-through (auto_action wire-up + isCenterOverlay allowlist + the chat-collapses-to-380-px layout transition).

v4.1.6 — 2026-05-10

I-MANIFEST-EMITTER-SCHEMA-SYMMETRIC CI gate

Closes the schema/emitter drift class of bug surfaced in v4.1.4 → v4.1.5: chat/extension.py started emitting id_projection in tool entries and manifest.py started emitting sdk_version at top level, but manifest_schema.Tool/Manifest had model_config = ConfigDict(extra='forbid') without those fields. Production extensions shipped manifests that local imperal validate rejected — drift was only visible at manual CLI run, not PR time.

New tests/test_manifest_roundtrip_gate.py builds a canary Extension exercising every emitter code path (@ext.tool, @ext.signal, @ext.schedule, @ext.webhook, @ext.on_event, @ext.on_install, @ext.health_check, @ext.panel, @chat.function with effects=, id_projection=) and asserts:

  1. generate_manifest output round-trips through validate_manifest_dict with zero issues — extras = drift signal.
  2. v4 required top-level fields all present (manifest_schema_version, sdk_version, app_id, version, name, description, icon, actions_explicit, capabilities, tools).
  3. Every @chat.function tool entry carries the v4 contract fields (action_type, chain_callable, effects, params_schema, event, id_projection).

Federal invariant I-MANIFEST-EMITTER-SCHEMA-SYMMETRIC documented in the test docstring. Future schema/emitter drift fails CI at PR time, not at production deploy time.

Migration: None required. CI gate only.

v4.1.5 — 2026-05-09

Manifest-schema gaps closed + sdk_version auto-emission

Two long-standing schema issues were both blocking imperal validate for extensions that already deployed cleanly through the Dev Portal:

  • Tool.id_projection accepted in the manifest schema. The chat-extension emitter has been writing this field since v4.1.2, but the manifest validator's Tool model used extra="forbid" and rejected it. Production extensions (e.g. Imperal-owned notes, sql-db, tasks) shipped with id_projection in their manifests via the Dev Portal pipeline; only the local CLI validator erred.
  • Manifest.sdk_version accepted at top level. The SDK-VERSION-1 validator checked for this field, but generate_manifest() never wrote it. Engineers had to hand-edit imperal.json after every imperal build.

Changes:

  • manifest_schema.Tool — added id_projection: Optional[str] = None.
  • manifest_schema.Manifest — added sdk_version: Optional[str] = None.
  • manifest.generate_manifest(ext) — now emits sdk_version at the top level, sourced from imperal_sdk.__version__.
  • Static src/imperal_sdk/schemas/imperal.schema.json regenerated.

Migration: None required. Existing extensions just stop seeing the warning on imperal build. Extensions using id_projection no longer need a --no-validate workaround.

v4.1.4 — 2026-05-09

Repository hygiene + tests/fixtures/openapi/ relocation

Bookkeeping release. Public PyPI surface unchanged.

  • Removed the stale local docs/ tree from the GitHub repo. The full, version-locked, code-validated documentation lives canonically at docs.imperal.io.
  • Removed examples/hello_extension/ (pre-v4 scaffold that would have failed validators V14, V15, V19, V21, V24 — see Federal contract below). Recipes at docs.imperal.io/en/recipes replace it with code-validated equivalents.
  • OpenAPI specs relocated from docs/openapi/ to tests/fixtures/openapi/ — semantically they were always test fixtures, never user-facing documentation.
  • README.md rewritten under canonical positioning ("Imperal Cloud is the world's first AI Cloud OS. Webbee 🐝 is its agent.") and trimmed of inline release notes (CHANGELOG.md is the single source of release history).
  • GitHub repo description and homepage updated.

v4.1.3 — 2026-05-06

chat/handler.py split into chat/handler.py + chat/retry.py

Module hygiene. Public API unchanged — every previously-exported symbol still re-imports cleanly from imperal_sdk.chat.handler.

  • chat/handler.py had grown to 807 LOC after the v4.1.0 Pydantic feedback-loop landed (+231 LOC vs v4.0.1), violating the workspace's 300-LOC ceiling.
  • Extracted into chat/retry.py: format_pydantic_for_llm, _emit_retry_outcome, _RETRY_BUDGET, _validation_missing_field_response, retry sub-loop helpers.
  • Logger name for validation_retry_outcome lines pinned to imperal_sdk.chat.handler (NOT __name__) so the SigNoz scrape pipeline contract and existing caplog test scoping survived the move.

Migration: None required.

v4.1.2 — 2026-05-05

@chat.function(id_projection="...") for compound tool names

When a tool name has multiple nouns (delete_notes_from_folder), the web-kernel's heuristic for inferring the ID field was wrong. New id_projection kwarg lets authors declare it explicitly — federal-clean.

@chat.function(
    description="Delete all notes inside a folder.",
    pydantic_param=DeleteNotesFromFolderParams,
    action_type="destructive",
    id_projection="folder_id",   # ← NEW
)

Migration: Optional. Tools with single-noun names continue to auto-derive correctly.

v4.1.1 — 2026-05-05

emit_narration audit-mode scope clarification

Tightened the schema description on the mode field of EMIT_NARRATION_TOOL so BYOLLM LLMs don't generalise audit-mode brevity globally across other tool calls in the same turn.

Symptom (pre-fix): "create a note with a 200-word essay" would produce content_text='<essay 200 words>' placeholder when narration_mode was audit.

Fix: Schema description now explicitly says mode controls only the prose field's interpretation — never a global brevity directive.

Migration: None. Pure schema description change. Effective on next worker restart with v4.1.1+.

v4.1.0 — 2026-05-02

Pydantic feedback loop — bounded retry on ValidationError

The runtime quality release. When the LLM emits arguments that fail Pydantic validation, the SDK auto-retries (max 2) with structured prose feedback derived from e.errors().

Federal invariants added (5):

  • I-PYDANTIC-RETRY-BUDGET
  • I-PYDANTIC-RETRY-SCOPE
  • I-PYDANTIC-FEEDBACK-STRUCTURED
  • I-PYDANTIC-FC-SINGLE-APPEND
  • I-PYDANTIC-WIRE-FROZEN

Closed ~75% of arg-quality hallucinations in production traffic.

Migration: Automatic for any @chat.function that declares pydantic_param=.... Legacy **kwargs handlers unchanged.

Full reference →

v4.0.1 — 2026-05-01

Manifest schema v3 release line

The federal extension contract v4.0 lands.

  • Manifest schema_version: 3
  • Validators V14-V22+V24 all promoted to ERROR severity
  • Extension(display_name=, description=, icon=, actions_explicit=) kwargs become the canonical surface
  • @chat.function(chain_callable=, effects=) kwargs added
  • ChatExtension(model=) legacy wrapper deprecated (still works; stop using)
  • New federal invariants: web-kernel-side I-EXT-SYSTEM-TASK-NO-MESSAGE-KWARG, SDK-side I-CHAT-FUNCTION-VERBATIM-PARAMS, I-EXT-SCHEMA-DESCRIPTION-STRING-DRAFT-2020-12

Migration: Re-run python -m imperal_sdk.cli build to regenerate v3 manifest. Existing v2 manifests fail V14 — must rebuild.

Pre-v4 history

Earlier releases (v3.x and below) used schema v2 with weaker validators. Migration paths are documented in the federal extension contract record on GitHub.

Where to next

On this page