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 raisesTypeError. 5.8.0 addedscope/env_fallbacktoSecretSpec+ the manifest schema, but theext.secret(...)decorator never accepted or forwarded them — so declaring an app-scope secret from code (the documented way) failed. The decorator now acceptsscope="user"|"app"(default"user") andenv_fallback=...and forwards both, so@ext.secret(scope="app")declarations emitscope/env_fallbackintosecrets[].
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
statesigning now requiresIMPERAL_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 forgestate. 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 yourconnect()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 manifestoauth[]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 sameawait ctx.secrets.get(name)— no code change. Use it for the credentials you own: your OAuthclient_id/client_secret, a shared API key you pay for. Per-user credentials (the user's own key, their OAuth token after they authorize) stayscope="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.secretreference.@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 ownIMPERAL_APPSECRET_namespace —SecretSpecrejects 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_stepnow 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 asstr). 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.createstep schema now requiresdata(notset), matching what the interpreter reads — a declarativestore.createcould previously passvalidate_stepXOR 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_storelistnow readsPage.data(was a non-existentPage.items), sostore.listreturned empty against the realPage/MockStore— breaking every declarative app that lists. Test fakes corrected to the realPagecontract.
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; nestedidentity/meter/attribution+ opendimensions). Carries WHAT was consumed, never the price (price resolution stays platform-side). Vendored freshness-gated JSON schema; a validator forbids price keys anywhere indimensions.
Changed
UserContext.imperal_id/tenant_idnow requiremin_length=1— an empty identity raisesValidationErrorat construction (agency_idstays 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), theimpldiscriminator (code|declarative),validate_ir_dict(),generate_ir(ext), and a versionedmigrate_ir()registry. - Engine SPI + minimal declarative executor (
imperal_sdk.runtime) — abstractKernelEngine,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· staticconditional) over the{{event/steps/prev}}binding-DSL with a step budget. Real logic staysimpl=code. - Bounded SDL projection, 3-tier UI (static | data-bound
template| code) + first-classskeletonslot in the IR, and an enriched symbol catalog (sdk-reference.jsonper-symbol type graph +declarative_capable/action_vocab_safeflags; 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.jsonso 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__secretspanel calledui.Heading(...)(which doesn't exist) — the element isui.Header. Every secrets-panel render raisedAttributeError. Switched toui.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 (402no card / SCA required,409not expired); returns{status, plan, expires_at, payment_intent_id}.BillingProtocol18 → 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(defaultFalse) —get_subscription()now maps it from the gateway response so extensions can show whether an active subscription is set to cancel at period end.BillingProtocol17 → 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 (forui.Open), so extensions let users manage cards + view invoices on Stripe's hosted page (PAN never touches our backend). Surfaces errors.- Five
ctx.billingparity 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), andupdate_billing_profile(profile)(writes name/company/vat/country). Writes surface errors. - New dataclasses
PlanInfoandAutoTopupSettingsinimperal_sdk.types.models.
v5.3.0 — 2026-06-16 — BillingClient write/payment methods
Additive — nothing to migrate.
Added
ctx.billingwrite/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.BillingClientnow sendsX-Acting-Useron the service-token path soget_user_or_servicegateway 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_sdkis now import-light. The package root resolves its public surface lazily, so importing the package — or lightweight helper modules such asimperal_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_nameis now optional. Omit it and it defaults totool_<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. Subclasssdl.Entityand the platform readsid/title/kinddirectly.- 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 totrack_usage(meter: str, quantity: int = 1, user=None) -> bool(previouslytrack_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_versionbelow5.0.0as an error — caught before deploy instead of being rejected at load.
Removed (unused — never wired by the platform)
ctx.dbandctx.tools— usectx.extensionsfor inter-extension calls.- The
event_schema=parameter on@chat.function. ctx.config.require()— usectx.config.get(...).
Documentation
effects,background, andlong_runningare now documented as advisory, declared-intent metadata. Declare them for convention; the long-running runtime path remainsctx.background_task(long_running=...).- Corrected the documented behavior of
data_model,chain_callable, andvalidate_manifest_dict(it raises on duplicate webhook paths, cross-namespace event types, and duplicate exposed names).MockSkeletonis now read-only, matching the real skeleton client.
How to migrate
- Rebuild against
imperal-sdk>=5.1.0and redeploy via the Developer Portal. - If you called
track_usage(tokens, resource), switch totrack_usage(meter, quantity). - If you used
ctx.db,ctx.tools,ctx.config.require(), orevent_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: truemanifest field. Honoured only whensystem: trueis also set — third-party extensions cannot hide themselves from the user-facing sidebar, and a manifest that pairshidden_in_sidebar: truewith 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 PydanticBaseModelsubclass describing the shape ofActionResult.data. The platform validates returneddataagainst 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$REFin multi-step chains.ActionResult[T]generic auto-detection.-> ActionResult[NoteRecord]return annotations now infer the data model automatically. Resolution order: explicitdata_model=kwarg → direct-> SomeBaseModelreturn 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— flagsaction_type="read"tools withoutdata_model=. Read tools must declare a typed return shape so the platform can validate$REFpaths 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) — flagsaction_type="write"/"destructive"tools withoutdata_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>_chatumbrella tools. The platform refuses any manifest that still contains such entries at publish time (new validatorV25, 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 aDeprecationWarningon each instantiation; that warning was removed in v5.2.1.
How to migrate
- Rebuild against
imperal-sdk>=5.0.0and redeploy via the Developer Portal. The manifest emitter drops thetool_<your-ext>_chatentry automatically. - No
ChatExtensioncall-style change is needed. The v5.0.0 advice to movetool_name=to positional form was based on a deprecation that was reverted in v5.2.1 —ChatExtension(ext, tool_name="...", ...)is fully supported and canonical. Thedescription=,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)— whenbackground=True, the SDK chat handler wraps the function call inctx.background_task()instead of running it synchronously. The LLM receives an immediate ack envelope carryingtask_id; the platform delivers the handler's returnedActionResultas a fresh bot turn when the work finishes.long_running=Trueraises 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 emission —
tools[]entries inimperal.jsonnow carrybackgroundandlong_runningbooleans. StrictManifest.ToolPydantic 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 choosebackground_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 raisesValueError("ctx.http timeout {N}s exceeds federal cap (180s)...")— usectx.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 returnedActionResultas a fresh bot message to the user's chat when done.long_running=Trueraises the 180s cap to 1800s. Returns atask_idimmediately 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.httpcall is capped; anything larger must usectx.background_task(). - Background coroutine return shape — the coroutine passed to
ctx.background_task()must returnActionResult. A non-ActionResultreturn 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.Linknow accepts the visible text via eitherlabel=(canonical) ortext=(alias). The two are interchangeable —labelwins if both are passed.- Calling
ui.Link()with neither raises a clearTypeError("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 kwargMigration
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 toTrueregardless ofaction_type. Typed read handlers join chains automatically.- Behavior is fully backward-compatible: authors who set
chain_callable=Falseexplicitly 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.jsonfrom runtimemanifest_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
SecretDeclPydantic model inmanifest_schema.py— mirrorsimperal_sdk.secrets.spec.SecretSpec.to_manifest_dict(). Validatesnameregex (^[a-z][a-z0-9_]{0,62}$),write_modein{user, extension, both},max_bytesin[1, 65536],rotation_hint_days >= 1when present, non-emptydescription.Manifest.secrets: Optional[List[SecretDecl]]field — additive, back-compatible with manifests that don't declare any secrets.
Guarantees the platform enforces
| Guarantee | What it pins |
|---|---|
| Manifest builder and validator stay in sync | Now 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.
namewith uppercase, invalidwrite_mode,max_bytesoutside[1, 65536]) will now failvalidate_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'sapp_id, not the drift-prone PythonExtension("X", ...)value). Returnshttps://{IMPERAL_PUBLIC_HOST}/v1/ext/{app_id}/webhook/{path}— default hostpanel.imperal.io. See @ext.webhook reference.- OAuth callback class fully supported —
panel.imperal.io/v1/ext/*now accepts bothGET(OAuth 302 redirects, verification challenges) andPOST(server-to-server hooks) on the same path. - Public
GET /v1/marketplace/apps/{app_id}/webhooksendpoint — returns[{path, method}]for each declared@ext.webhookin 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.webhookwith method badge, canonical URL, and OAuth/server-to-server hints.imperal-ext-developerv1.3.0. ctx.cache+ctx.secretsnow available in webhook handlers — both moved toContextFactory._build_contextso every dispatch path (chat tool, panel, skeleton, schedule, webhook, lifecycle, health check) gets the same surface uniformly._HealthCheckCtxgained_StubSecretsgraceful no-op so@ext.health_checkhandlers that legitimately readctx.secretsdon't crash withAttributeError.
Migration notes
- Replace hardcoded redirect URIs with
ctx.webhook_url("/callback")at runtime. Hardcoded URLs are the #1 cause of OAuth drift bugs (PythonExtension("spotify-extension", ...)≠ deployed folderspotify≠ 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 ofui.Inputforwrite_mode='user'/'both'secrets. Full reference: UI primitives reference.ui.Input(type=)kwarg — accepts"text"(default),"password","email","number","url". Backward-compatible: existingui.Input(...)calls withouttype=continue rendering as text. Thetypeprop 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_countnow 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_panelsupdated to assert +1 for the always-present syntheticsecretspanel.
Notes
- No behavior change for extensions with at least one real
@ext.toolorChatExtensionaction. - 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.4and 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.secretfor 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
secretspanel (conditional on at least one@ext.secret(...)call). Superseded by v4.2.4 which makes registration unconditional — prefer pinning>= 4.2.4directly. - The synthetic panel uses slot
rightdefensively — most extensions useleft(sidebar nav) andcenter(main content);rightis rarely used so the panel-sync logic inimperal-ext-developerwon't overwrite it. If your extension already declares aright-slot panel, your panel wins; users still reach the Secrets UI via the direct/ext/{ext_id}/secretsroute. - 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.secretsaccessor 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.MockSecretStorefor pytest fixtures with optionaldeclaredset to mirrorSecretNotDeclaredErrorsemantics.- Dev mode env-var fallback:
IMPERAL_DEV_MODE=true+IMPERAL_SECRET_<UPPER_NAME>env vars feedctx.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]/secretspage where users set, rotate, and delete extension secrets. Input istype="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.secretdeclarations continue to work unchanged. Thesecrets[]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.secretdeclarations and switching read sites fromos.environ.get()/redis.hget()toawait 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.secretdeclaration. 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-1to work around the false positive onskeleton_alert_*tools, you can now enable it again.imperal build/imperal validateno 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 = FALSEat the SQL layer. - Cannot be uninstalled — the auth-gw
/v1/marketplace/apps/<id>/installDELETE endpoint returns 403 if the app issystem=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
| Guarantee | What it pins |
|---|---|
| System apps can never be uninstalled | The platform refuses uninstall requests for system=True apps. |
| The marketplace hides system apps | Every marketplace listing query excludes system apps. |
| The system flag is reserved for Imperal | Only 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.functionwithdescription≥20 chars + Pydantic-typed param.icon.svgplaceholder file (V21-compliant — XML root +viewBox+ ≤100 KB).requirements.txt: imperal-sdk>=4.0.0(was>=1.0.0).tests/test_main.pyexercises Pydantic param validation +MockContextasync 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/developerMigration: 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:
| Status | What it means | Slots |
|---|---|---|
permanent | Always-fetched at session-init, persistent column | left, right |
center-overlay | On-demand via __panel__<id> action when panel_id matches the host's isCenterOverlay allowlist; chat collapses to 380 px right rail | center |
reserved | Accepted by SDK validator but frontend has no render path | overlay, 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:
generate_manifestoutput round-trips throughvalidate_manifest_dictwith zero issues — extras = drift signal.- v4 required top-level fields all present (
manifest_schema_version,sdk_version,app_id,version,name,description,icon,actions_explicit,capabilities,tools). - Every
@chat.functiontool 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_projectionaccepted in the manifest schema. The chat-extension emitter has been writing this field since v4.1.2, but the manifest validator'sToolmodel usedextra="forbid"and rejected it. Production extensions (e.g. Imperal-ownednotes,sql-db,tasks) shipped withid_projectionin their manifests via the Dev Portal pipeline; only the local CLI validator erred.Manifest.sdk_versionaccepted at top level. TheSDK-VERSION-1validator checked for this field, butgenerate_manifest()never wrote it. Engineers had to hand-editimperal.jsonafter everyimperal build.
Changes:
manifest_schema.Tool— addedid_projection: Optional[str] = None.manifest_schema.Manifest— addedsdk_version: Optional[str] = None.manifest.generate_manifest(ext)— now emitssdk_versionat the top level, sourced fromimperal_sdk.__version__.- Static
src/imperal_sdk/schemas/imperal.schema.jsonregenerated.
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/totests/fixtures/openapi/— semantically they were always test fixtures, never user-facing documentation. README.mdrewritten 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
descriptionandhomepageupdated.
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.pyhad 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_outcomelines pinned toimperal_sdk.chat.handler(NOT__name__) so the platform log-scrape pipeline contract and existingcaplogtest 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.
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 addedChatExtension(model=)legacy wrapper deprecated (still works; stop using)- New platform-enforced guarantees: system tasks no longer accept a
messagekeyword argument,@chat.functionparameters 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
Pydantic feedback loop
Pydantic feedback loop — the runtime retry layer that fixes malformed LLM tool args, closing about 75% of arg-quality hallucinations with bounded retries.
Marketplace
Marketplace is the Imperal Cloud app store for Webbee extensions, letting you discover, search, install, and manage every add-on entirely from chat with Webbee.