Imperal Docs
SDK Reference

Changelog

Release notes for the imperal-sdk Python package: the current major v5.x and historical v4.x, with every new decorator, validator, and breaking change.

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.9.2 — 2026-06-30 — Fix: ext.secret(scope=, env_fallback=) reach the manifest

Patch — completes the 5.8.0 app-level secrets feature on the decorator.

  • ext.secret(scope="app") no longer raises TypeError. 5.8.0 added scope / env_fallback to SecretSpec + the manifest schema, but the ext.secret(...) decorator never accepted or forwarded them — so declaring an app-scope secret from code (the documented way) failed. The decorator now accepts scope="user"|"app" (default "user") and env_fallback=... and forwards both, so @ext.secret(scope="app") declarations emit scope/env_fallback into secrets[].

v5.9.1 — 2026-06-30 — Security: OAuth state requires a configured signing secret

Patch — hardens 5.9.0.

  • No hardcoded fallback signing key. OAuth state signing now requires IMPERAL_OAUTH_STATE_SECRET (set to the same value on the kernel and the gateway) and fails loudly if unset — the previous constant fallback was world-readable in the published package and could let an attacker forge state. Operators must set this secret before the OAuth-connect flow is used.

v5.9.0 — 2026-06-30 — Feature: unified OAuth-connect (ext.oauth + ctx.oauth_authorize_url)

Minor — additive; nothing to migrate.

Added

  • ext.oauth(provider, *, collection=None, scopes=None) + await ctx.oauth_authorize_url(provider). Hand the whole OAuth account-connect flow to the platform: declare a provider, return the authorize URL from your connect() handler, and the platform exchanges the code, fetches the email, and saves a standard account record — synchronously, with a branded result window. Client creds come from your app-scope secrets {provider}_client_id / {provider}_client_secret. Built-in providers: google, microsoft, yahoo. Replaces the hand-rolled @ext.webhook("callback") + polling-schedule pattern. See the ext.oauth reference. Emitted to the manifest oauth[] section.

v5.8.2 — 2026-06-30 — Fix: @ext.webhook path slash-normalized

Patch. A leading slash in a declared webhook path (@ext.webhook("/callback")) no longer breaks dispatch — the path is normalized so the registered tool name matches the platform's URL-derived dispatch. Both "callback" and "/callback" work; the manifest keeps a single leading slash.

v5.8.1 — 2026-06-30 — Fix: ctx.as_user(uid).secrets in system-context fan-out

Patch. ctx.as_user(uid).secrets no longer raises AttributeError — the scoped Context now carries the secrets client (rebound to the target user), so reading secrets from an @ext.schedule cron or any ctx.as_user(...) fan-out works.

v5.8.0 — 2026-06-29 — Feature: app-level (shared) secrets (scope="app")

Minor — additive @ext.secret kwargs, nothing to migrate (absent scope"user", the existing behaviour).

Added

  • @ext.secret(scope="user" | "app"). A secret can now be developer-owned and shared by every user (scope="app") instead of per-user (scope="user", the default). An app-scope secret is stored once for the extension, set by the app owner in the Developer Portal, and read transparently by your handlers for every user through the same await ctx.secrets.get(name) — no code change. Use it for the credentials you own: your OAuth client_id / client_secret, a shared API key you pay for. Per-user credentials (the user's own key, their OAuth token after they authorize) stay scope="user". App-scope writes are owner-only (Developer Portal), and end users can never read or list them. See the two-scope guide and the @ext.secret reference.
  • @ext.secret(env_fallback="IMPERAL_APPSECRET_<EXT>_<NAME>") — optional, scope="app" only: a temporary migration bridge so an app-scope read falls back to an env var until you save the value in the Dev Portal. The name must live in your app's own IMPERAL_APPSECRET_ namespace — SecretSpec rejects anything else at build time, so a fallback can never point at another app's or a platform secret.

v5.7.3 — 2026-06-21 — Fix: validate_step is binding-DSL-aware

Patch — nothing to migrate.

Fixed

  • validate_step now accepts a whole-match binding ({{ path }}) in any typed field — a binding resolves at runtime to the referenced object of unknown static type, so whole-match {{...}} skips the static type-check (interpolated "...{{x}}..." still checks as str). Unblocks declarative flows that bind a prior step's list into an array field (ir/actions.py).

v5.7.2 — 2026-06-21 — Fix: store.create schema ↔ interpreter alignment

Patch — nothing to migrate.

Fixed

  • store.create step schema now requires data (not set), matching what the interpreter reads — a declarative store.create could previously pass validate_step XOR run, never both. A full 11-verb audit confirmed this was the only hard schema↔interpreter mismatch; pinned by a new test.

v5.7.1 — 2026-06-20 — Fix: declarative store.list returned count=0

Patch — nothing to migrate.

Fixed

  • run_store list now reads Page.data (was a non-existent Page.items), so store.list returned empty against the real Page/MockStore — breaking every declarative app that lists. Test fakes corrected to the real Page contract.

v5.7.0 — 2026-06-20 — Metering rails (L0-4 core)

Minor — additive metering contract + identity hardening. One tightening: an empty imperal_id/tenant_id on UserContext is now rejected (was always invalid).

Added

  • MeteredEvent — sealed, dimension-only usage-metering DTO (frozen Pydantic; nested identity/meter/attribution + open dimensions). Carries WHAT was consumed, never the price (price resolution stays platform-side). Vendored freshness-gated JSON schema; a validator forbids price keys anywhere in dimensions.

Changed

  • UserContext.imperal_id / tenant_id now require min_length=1 — an empty identity raises ValidationError at construction (agency_id stays nullable).

v5.6.1 — 2026-06-20 — Engine-seal completion (L0-3)

Patch — nothing to migrate (internal/cosmetic).

Changed

  • Optional platform-runtime imports are owned by a single substrate-neutral shim; fallback warnings no longer name internal engine modules.

v5.6.0 — 2026-06-20 — IR envelope + minimal declarative executor (L0-2)

Minor — purely additive; nothing to migrate.

Added

  • IR envelope (imperal_sdk.ir) — versioned, schema'd definition of what an app is: IREnvelope/IRApp (+ schemas/ir.schema.json), the impl discriminator (code | declarative), validate_ir_dict(), generate_ir(ext), and a versioned migrate_ir() registry.
  • Engine SPI + minimal declarative executor (imperal_sdk.runtime) — abstract KernelEngine, LocalDevEngine, HostedClient (all interchangeable via the engine-parity test). A non-Turing step interpreter runs the declarative vocabulary (call/navigate/send/open · store.{get,list,create,update,delete} · ai.complete · static conditional) over the {{event/steps/prev}} binding-DSL with a step budget. Real logic stays impl=code.
  • Bounded SDL projection, 3-tier UI (static | data-bound template | code) + first-class skeleton slot in the IR, and an enriched symbol catalog (sdk-reference.json per-symbol type graph + declarative_capable/action_vocab_safe flags; per-verb action schemas).

v5.5.1 — 2026-06-19 — Sync chat_result schema artifact (Seal-B)

Patch — nothing to migrate.

Fixed

  • Regenerated schemas/chat_result.schema.json so its embedded description matches the runtime model after the engine-neutral pass — completes Seal-B (no engine-implementation names anywhere in the shipped package).

v5.5.0 — 2026-06-19 — Apache-2.0 relicense + engine-neutral docs

Licensing + documentation — nothing to migrate.

Changed

  • Relicensed AGPL-3.0 → Apache-2.0 — the SDK you build Imperal apps in is now permissively licensed (no copyleft friction for commercial adopters). No runtime/API change.
  • Engine-neutral public docs — docstrings/comments describe behavior in substrate-neutral terms ("the platform", "the platform state/event store") with no internal invariant IDs. Contract suite green.

v5.4.3 — 2026-06-18 — Fix secrets-panel render crash

Bugfix — nothing to migrate.

Fixed

  • The auto-generated __panel__secrets panel called ui.Heading(...) (which doesn't exist) — the element is ui.Header. Every secrets-panel render raised AttributeError. Switched to ui.Header.

v5.4.2 — 2026-06-16 — BillingClient renew_subscription

Additive — nothing to migrate.

Added

  • ctx.billing.renew_subscription() — renews an expired subscription for the same plan: charges the saved default card for one fresh period and restores access immediately (POST /v1/billing/renew). Surfaces errors (402 no card / SCA required, 409 not expired); returns {status, plan, expires_at, payment_intent_id}. BillingProtocol 18 → 19 methods.

v5.4.1 — 2026-06-16 — BillingClient resume + cancel_at_period_end

Additive — nothing to migrate.

Added

  • ctx.billing.resume_subscription() — undoes a pending cancel-at-period-end (POST /v1/billing/resume); returns {status, plan, expires_at, cancel_at_period_end}. Surfaces errors.
  • SubscriptionInfo.cancel_at_period_end: bool (default False) — get_subscription() now maps it from the gateway response so extensions can show whether an active subscription is set to cancel at period end. BillingProtocol 17 → 18 methods.

v5.4.0 — 2026-06-16 — BillingClient portal + full Webbee parity

Additive — nothing to migrate.

Added

  • ctx.billing.create_billing_portal_session() — mints a Stripe Customer Portal session and returns its hosted URL (for ui.Open), so extensions let users manage cards + view invoices on Stripe's hosted page (PAN never touches our backend). Surfaces errors.
  • Five ctx.billing parity methods so Webbee can fully drive billing via chat: list_plans() (public plan catalog → list[PlanInfo], safe-degrades to []), get_auto_topup() (→ AutoTopupSettings, safe-degrades to disabled defaults), set_auto_topup(enabled, threshold_pct=10, recharge_tokens=20000, payment_method_id=""), cancel_subscription() (cancel-at-period-end → result dict), and update_billing_profile(profile) (writes name/company/vat/country). Writes surface errors.
  • New dataclasses PlanInfo and AutoTopupSettings in imperal_sdk.types.models.

v5.3.0 — 2026-06-16 — BillingClient write/payment methods

Additive — nothing to migrate.

Added

  • ctx.billing write/payment methods: list_payment_methods, list_payments, create_setup_intent, set_default_payment_method, remove_payment_method, change_plan, topup. Reads degrade safely; writes surface errors so the caller can render Stripe failures / drive the Payment Element.
  • BillingClient now sends X-Acting-User on the service-token path so get_user_or_service gateway endpoints resolve the acting user.

v5.2.2 — 2026-06-11 — Import-light package root

A performance and robustness release. Zero API changes — every public name, submodule attribute, star-import and dir() entry resolves exactly as before.

Changed

  • import imperal_sdk is now import-light. The package root resolves its public surface lazily, so importing the package — or lightweight helper modules such as imperal_sdk.chat.filters — no longer loads the HTTP client stack. Heavy dependencies load on first use of the names that need them (Context, the service clients, get_llm_provider, …). You get faster cold imports, and helper modules are safe to import from restricted execution contexts.

Nothing to migrate — rebuild against imperal-sdk>=5.2.2 at your convenience.

v5.2.1 — 2026-06-01 — ChatExtension ergonomics

A small, fully backward-compatible cleanup of ChatExtension. No API removals; existing extensions are unchanged.

Changed

  • ChatExtension(tool_name=...) no longer emits a deprecation warning. The keyword argument is the supported, canonical way to register your chat functions and it is not going away — the previous "will be removed" warning was incorrect and has been removed.

Added

  • tool_name is now optional. Omit it and it defaults to tool_<app_id>_chat; pass it explicitly to pin a stable name (recommended for production extensions). description= is optional too.

Nothing to migrate — rebuild against imperal-sdk>=5.2.1 at your convenience.

v5.2.0 — 2026-05-31 — Structured Data Layer (SDL) foundation

Adds the SDL (imperal_sdk.sdl) — a typed, semantic vocabulary for the data your @chat.function returns, so the platform can read an entity's id / title / kind and its facets directly instead of inferring them from field names. Fully additive and opt-in — the existing API and working extensions are unchanged; you adopt SDL via data_model=.

Added

  • sdl.Entity / sdl.Ref / sdl.EntityList[T] — canonical typed entity, lightweight reference, and typed list. Subclass sdl.Entity and the platform reads id / title / kind directly.
  • Standard facet library — 123 composable facet mixins across 17 families (Identity, Time, People, Content, Communication, Media, Quantities, Money, Catalog, Tasks, Location, Tech/Network, Analytics, Events, Ratings, Security, Devices/Health). Compose only the facets your entity needs; every field carries a standard semantic role.
  • sdl.field(role="yourapp.x") — declare a custom semantic role (non-reserved namespace) for anything the standard facets don't cover.
  • sdl.roles_of(model) — introspect a model's field→role map.

See the SDL reference for the full guide. Live in production — the platform reads the SDL entities your extensions return today; adopting the types is safe and forward-compatible.

v5.1.0 — 2026-05-30 — Accuracy & correctness pass

This release makes the SDK faithful to current platform behavior: a billing fix, removal of unused surface, a corrected limit, and documentation that now matches what the platform actually does. Everything removed was unused (never wired by the platform), and working extensions keep working — the only signature change is on a method that previously could not record usage correctly.

Fixed

  • ctx.billing.track_usage(...) now records the amount you pass. Previously every call was recorded as a single unit regardless of the amount, and one path could not reach the platform at all. Signature changed to track_usage(meter: str, quantity: int = 1, user=None) -> bool (previously track_usage(tokens, resource)).
  • Inter-extension call-depth limit now matches the platform's real nesting allowance, so a legitimate chain of nested ctx.extensions.call(...) hops is no longer rejected one level too early.
  • Manifest pre-flight validation now flags an sdk_version below 5.0.0 as an error — caught before deploy instead of being rejected at load.

Removed (unused — never wired by the platform)

  • ctx.db and ctx.tools — use ctx.extensions for inter-extension calls.
  • The event_schema= parameter on @chat.function.
  • ctx.config.require() — use ctx.config.get(...).

Documentation

  • effects, background, and long_running are now documented as advisory, declared-intent metadata. Declare them for convention; the long-running runtime path remains ctx.background_task(long_running=...).
  • Corrected the documented behavior of data_model, chain_callable, and validate_manifest_dict (it raises on duplicate webhook paths, cross-namespace event types, and duplicate exposed names). MockSkeleton is now read-only, matching the real skeleton client.

How to migrate

  1. Rebuild against imperal-sdk>=5.1.0 and redeploy via the Developer Portal.
  2. If you called track_usage(tokens, resource), switch to track_usage(meter, quantity).
  3. If you used ctx.db, ctx.tools, ctx.config.require(), or event_schema=, switch to the real equivalents above. (These were never wired, so most extensions need no change.)

v5.0.3 — 2026-05-27 — Manifest hidden_in_sidebar field (system-only)

System apps may opt out of the Imperal Panel sidebar tile by declaring hidden_in_sidebar: true in their imperal.json. Chat tools, lifecycle hooks, skeleton refreshes, and the audit ledger all continue to work — only the visual sidebar icon is suppressed.

Added

  • hidden_in_sidebar: true manifest field. Honoured only when system: true is also set — third-party extensions cannot hide themselves from the user-facing sidebar, and a manifest that pairs hidden_in_sidebar: true with a non-system app is rejected at validation time.

No schema-version bump (still v3); no other public API changes.

v5.0.2 — 2026-05-26 — Internal correctness

Docs-only release. No behavior change, no new APIs. Internal source-citation hygiene so the platform's correctness checks stay durable across SDK reinstalls. Nothing to migrate.

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 — flags action_type="read" tools without data_model=. Read tools must declare a typed return shape so the platform can validate $REF paths and prevent input/output naming drift. WARN today (a recommendation, not a publish blocker); promotable to ERROR in a future release once adoption stabilises.
  • V24 (WARN-only) — flags action_type="write"/"destructive" tools without data_model=. Recommended so the chain narrator and audit ledger see the resulting entity shape; never a publish blocker.

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.

The tool_name deprecation was REVERTED in v5.2.1

The "ChatExtension(tool_name=...) is deprecated / will be removed" warning announced below was incorrect and has been reverted in v5.2.1. tool_name= is the canonical, non-deprecated keyword for registering chat functions and is not going away. (tool_name is now also optional — it defaults to tool_<app_id>_chat.) Only ChatExtension(model=...) remains deprecated. Disregard the deprecation note in the bullet below.

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. (Reverted in v5.2.1 — see the callout above; tool_name= is canonical and not going away.) The kwarg-form was, at v5.0.0, said to emit a DeprecationWarning on each instantiation; that warning was removed in v5.2.1.

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. No ChatExtension call-style change is needed. The v5.0.0 advice to move tool_name= to positional form was based on a deprecation that was reverted in v5.2.1ChatExtension(ext, tool_name="...", ...) is fully supported and canonical. The description=, system_prompt=, max_rounds= kwargs continue to work as before.

That's the whole migration. Your @chat.function handlers keep working unchanged — and on imperal-sdk>=5.2.1 the tool_name= keyword is canonical (and optional, defaulting to tool_<app_id>_chat).

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: placeholder-args guard

The SDK now rejects any tool call whose argument values look like model-emitted placeholder sentinels — e.g. <UNKNOWN>, <TODO>, <MISSING>, <EMAIL>, <PASSWORD>, <USER_ID>. The rejection happens before the call is dispatched, so a placeholder argument never reaches your handler, never spends billing tokens, and never lands in your audit trail. The rejection is shaped as an instruction back to the model, which self-corrects by asking the user a clarifying question.

Motivation. When the user's message omits a required field, the model occasionally substitutes a placeholder token instead of asking. The guard fails fast on the request side so the user gets a clarifying question instead of an opaque downstream failure.

Behaviour

  • Only fully-bracketed uppercase sentinels (e.g. <UNKNOWN>) are treated as placeholders. Substrings inside prose (an error-message body that happens to contain <UNKNOWN>) and lowercase HTML/XML tags such as <html> do not trip the guard. Leading/trailing whitespace is tolerated. The check recurses through dict, list, tuple, and string argument values.

Behaviour

  • A tool call whose argument values look like placeholder sentinels is rejected before dispatch, so no billing-charged work runs and no audit row is written for the bad call. The rejection is shaped as an instruction back to the model, which then asks the user a clarifying question instead.

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. The platform caps a single HTTP call at 180 seconds. 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("start_refinement", 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")

Guarantees the platform enforces

  • Per-call HTTP timeout cap — a single ctx.http call is capped; anything larger must use ctx.background_task().
  • Background coroutine return shape — the coroutine passed to ctx.background_task() must return ActionResult. A non-ActionResult return is recorded in the audit trail and delivers a fallback error message to chat.
  • Background tasks are owner-scoped — every background task is bound to the extension and the user that created it. Another user cannot cancel it or read its status (returns 403).
  • Chat injection is owner-scoped — extension-initiated chat messages are scoped to the extension and the user. Cross-user injection returns 403.
  • Every chat injection is audited — each extension-initiated chat message is recorded in the audit trail.

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.

Guarantees the platform enforces

GuaranteeWhat it pins
Manifest builder and validator stay in syncNow actually holds for secrets[] — what the build emits and what validation accepts 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.
  • Platform-enforced guarantees for secrets:
    • Each secret is scoped to a single user, never written to logs, scoped to the declaring extension, backed by the encrypted-secrets store, and retained in the audit trail.
    • Secret values exist only for the lifetime of the handler call, and a secret can only be read after it has been declared. See Federal contract for the full behaviour.
  • 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.

Guarantees the platform enforces

GuaranteeWhat it pins
System apps can never be uninstalledThe platform refuses uninstall requests for system=True apps.
The marketplace hides system appsEvery marketplace listing query excludes system apps.
The system flag is reserved for ImperalOnly authors on 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 rendering config; 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 + panel-rendering 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

Manifest builder/validator symmetry 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).

The guarantee that the manifest builder and validator stay in sync is 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 first ICNLI 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 platform log-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(
    "delete_notes_from_folder",
    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().

Platform-enforced guarantees for the feedback loop:

  • The retry count is bounded, so a tool never loops indefinitely on bad arguments.
  • Retries are scoped to the failing call only.
  • The feedback handed back to the model is structured prose derived from the validation error.
  • The corrective feedback is appended to the conversation exactly once per attempt.
  • The arguments sent on the wire are frozen once validated.

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 platform-enforced guarantees: system tasks no longer accept a message keyword argument, @chat.function parameters are passed to your handler verbatim, and every parameter schema description is a Draft 2020-12 JSON Schema string

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. To migrate, re-run python -m imperal_sdk.cli build against the current SDK and fix what the validators flag — every check prints an actionable message. See the validators reference for the full rule set.

Where to next

On this page

v5.9.2 — 2026-06-30 — Fix: ext.secret(scope=, env_fallback=) reach the manifestv5.9.1 — 2026-06-30 — Security: OAuth state requires a configured signing secretv5.9.0 — 2026-06-30 — Feature: unified OAuth-connect (ext.oauth + ctx.oauth_authorize_url)Addedv5.8.2 — 2026-06-30 — Fix: @ext.webhook path slash-normalizedv5.8.1 — 2026-06-30 — Fix: ctx.as_user(uid).secrets in system-context fan-outv5.8.0 — 2026-06-29 — Feature: app-level (shared) secrets (scope="app")Addedv5.7.3 — 2026-06-21 — Fix: validate_step is binding-DSL-awareFixedv5.7.2 — 2026-06-21 — Fix: store.create schema ↔ interpreter alignmentFixedv5.7.1 — 2026-06-20 — Fix: declarative store.list returned count=0Fixedv5.7.0 — 2026-06-20 — Metering rails (L0-4 core)AddedChangedv5.6.1 — 2026-06-20 — Engine-seal completion (L0-3)Changedv5.6.0 — 2026-06-20 — IR envelope + minimal declarative executor (L0-2)Addedv5.5.1 — 2026-06-19 — Sync chat_result schema artifact (Seal-B)Fixedv5.5.0 — 2026-06-19 — Apache-2.0 relicense + engine-neutral docsChangedv5.4.3 — 2026-06-18 — Fix secrets-panel render crashFixedv5.4.2 — 2026-06-16 — BillingClient renew_subscriptionAddedv5.4.1 — 2026-06-16 — BillingClient resume + cancel_at_period_endAddedv5.4.0 — 2026-06-16 — BillingClient portal + full Webbee parityAddedv5.3.0 — 2026-06-16 — BillingClient write/payment methodsAddedv5.2.2 — 2026-06-11 — Import-light package rootChangedv5.2.1 — 2026-06-01 — ChatExtension ergonomicsChangedAddedv5.2.0 — 2026-05-31 — Structured Data Layer (SDL) foundationAddedv5.1.0 — 2026-05-30 — Accuracy & correctness passFixedRemoved (unused — never wired by the platform)DocumentationHow to migratev5.0.3 — 2026-05-27 — Manifest hidden_in_sidebar field (system-only)Addedv5.0.2 — 2026-05-26 — Internal correctnessv5.0.1 — 2026-05-17AddedValidator changesWhy this matters for youv5.0.0 — 2026-05-15 — ChatExtension simplificationWhat changedHow to migrateWhy this matters for youv4.2.16 — 2026-05-15v4.2.15 — 2026-05-14BehaviourBehaviourMigrationv4.2.14 — 2026-05-14v4.2.13 — 2026-05-14AddedWhen to use sugar vs. explicit ctx.background_task(coro)Migrationv4.2.12 — 2026-05-14AddedGuarantees the platform enforcesMigrationv4.2.11 — 2026-05-13ChangedMigrationv4.2.10 — 2026-05-13ChangedWhy this mattersMigrationv4.2.9 — 2026-05-13Fixedv4.2.8 — 2026-05-13AddedGuarantees the platform enforcesMigration notesv4.2.7 — 2026-05-13AddedMigration notesv4.2.6 — 2026-05-13AddedPanel renderingFederal notev4.2.5 — 2026-05-13FixedNotesv4.2.4 — 2026-05-13Migration notesv4.2.3 — 2026-05-13AddedMigration notesv4.2.2 — 2026-05-13AddedMigration notesv4.2.1 — 2026-05-11Migration notesv4.2.0 — 2026-05-11Extension(system: bool = False) kwargGuarantees the platform enforcesMigration notesv4.1.9 — 2026-05-10v4.1.8 — 2026-05-10v4.1.7 — 2026-05-10v4.1.6 — 2026-05-10v4.1.5 — 2026-05-09v4.1.4 — 2026-05-09v4.1.3 — 2026-05-06v4.1.2 — 2026-05-05v4.1.1 — 2026-05-05v4.1.0 — 2026-05-02v4.0.1 — 2026-05-01Pre-v4 historyWhere to next