Changelog
Release notes for the imperal-sdk Python package — current major v5.x, historical v4.x
Release history for the imperal-sdk Python package. Migration notes are inline. Older minor-release entries are condensed; full per-tag detail lives in the package's CHANGELOG.md.
v5.0.1 — 2026-05-17
Typed return contract for @chat.function (additive, no breaking changes).
Declare the shape of your tool's ActionResult.data and the platform validates it for you — at emit time, in chain $REF paths, and in the LLM tool catalog. Extensions built against v5.0.0 keep working unchanged.
Added
@chat.function(data_model=YourModel)— pass a 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(WARN) — flags chain-callable write/destructive tools withoutdata_model=. Recommendation, not a publish blocker. Will promote to ERROR in a future minor once adoption stabilises.
Why this matters for you
If your tool's data is consumed downstream via a chain reference like $REF:<your-app>[0].some_field, declaring data_model= catches broken references at publish time rather than at runtime. For single-step tools, it is purely an opt-in safety net.
v5.0.0 — 2026-05-15 — ChatExtension simplification
ChatExtension is now a thinner wrapper around your @chat.function handlers — the manifest no longer emits an umbrella orchestrator tool, and one constructor kwarg style is deprecated. Your handlers themselves keep working without changes.
What changed
- Manifest no longer emits
tool_<your-ext>_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. Passtool_namepositionally instead. The kwarg-form still works at runtime but emits aDeprecationWarninglog line on each instantiation.
How to migrate
- Rebuild against
imperal-sdk>=5.0.0and redeploy via the Developer Portal. The manifest emitter drops thetool_<your-ext>_chatentry automatically. - Switch
ChatExtension(ext=..., tool_name="...", ...)to positional form:ChatExtension(ext, "tool_<your-ext>_chat", ...). Silences theDeprecationWarning. Thedescription=,system_prompt=,max_rounds=kwargs continue to work as before.
That's the whole migration. Your @chat.function handlers keep working unchanged — only the ChatExtension constructor call style changes.
Why this matters for you
Cleaner separation of responsibilities: your SDK code declares tools and writes business logic, the platform routes between them. Multi-extension chains that consume your tool's output preserve full structured data per step (no more aggregated .data from sibling tool calls leaking into yours).
v4.2.16 — 2026-05-15
Operator log marker for hallucinated tool names.
When the LLM tries to call a function that does not exist on your extension, the rejection log line now carries the UNKNOWN_FUNCTION(will-reject) marker. The call was already rejected before — this is purely a log-format change so operators can grep for the marker and track hallucination rates per extension.
No SDK API change. No behaviour change for your code. If you do not read worker journals, this release is invisible to you.
v4.2.15 — 2026-05-14
Feat: federal placeholder-args guard (I-PARAMS-NO-PLACEHOLDER-VALUES)
New ChatExtension guard that rejects any tool call whose argument values look like LLM-emitted placeholder sentinels — e.g. <UNKNOWN>, <TODO>, <MISSING>, <EMAIL>, <PASSWORD>, <USER_ID>. Runs before write-arg-bleed, target-scope, and 2-step confirmation guards, so a dispatch carrying a placeholder is short-circuited before any billing-charged work or audit-ledger pollution. The rejection text is shaped as an instruction back to the LLM so the chat loop feeds it through as a synthetic tool_result and the LLM self-corrects by asking the user a clarifying question.
Motivation. When the user's message omits a required field, the wrapper LLM occasionally substitutes a placeholder token (<UNKNOWN>) instead of asking. Before this guard, the dispatch went through; the downstream anti-fab layer correctly caught the drift on the response side (server did not reflect 'email': requested '<UNKNOWN>', got None), but by then billing tokens had been spent, action_ledger had a target=<UNKNOWN> row, and the end user saw an opaque failure. The new guard fails fast on the request side.
Added
-
check_placeholder_args(tu, action_type) -> str | Noneinimperal_sdk.chat.guards— recursive scan oftu.input(dict / list / tuple / str) for values matching the tight regex^<[A-Z][A-Z0-9_]*>$. Full-anchored — substrings inside prose (e.g. an error message body containing<UNKNOWN>) do not trip the guard. Whitespace-tolerant via.strip(). Lowercase HTML/XML tags such as<html>or<unknown>are explicitly ignored. -
_PLACEHOLDER_REand_scan_for_placeholders(value)module-private helpers. The recursive scan walks dict values and list/tuple items; non-string values are skipped. -
Integration into the
check_guards()orchestrator as the first layer — placeholders are rejected before any other guard runs.
Federal invariants (new)
I-PARAMS-NO-PLACEHOLDER-VALUES— registered in the web-kernel federal invariant table (imperal_kernel/tests/federal/_invariant_assertions.py). The standalone source-inspection test attests/test_i_params_no_placeholder_values.pyasserts the regex contract, dict/list recursion, full-anchor (no substring), empty-input no-op, integration ordering insidecheck_guards(), and SDK signature shape(tu, action_type).
Migration
Zero migration. Strictly additive — default-allow surface, no manifest schema change, no API break. Existing extensions emit no placeholder sentinels in legitimate flows, so the guard is a no-op for every real call. PATCH bump per the project's conservative semver convention (additive features ship as PATCH; MINOR is reserved for "existing ext must rebuild to stay valid"; MAJOR for runtime API breakage).
Pin >= 4.2.15 to get the new guard.
v4.2.14 — 2026-05-14
Fix: regenerated imperal.schema.json static mirror to match runtime Manifest model
v4.2.13 added background + long_running to the runtime Tool Pydantic model but forgot to regenerate the static imperal.schema.json ship-alongside file. The CI gate caught the drift on the release commit (same pattern as the v4.2.8 → v4.2.9 fix). No public API change.
Pin >= 4.2.14 directly to avoid pulling the temporarily-drifted v4.2.13 sdist.
v4.2.13 — 2026-05-14
Feat: @chat.function(background=True) declarative flag
Sugar wrapper over ctx.background_task() from v4.2.12. Author writes a single handler body; the SDK auto-wraps the call in ctx.background_task() when the flag is set.
Added
-
@chat.function(..., background=True, long_running=False)— 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. Federal cap 180 seconds (I-LONGRUN-HTTP-CAP-180S). 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(action_type="write") async def start_refinement(ctx, params: StartParams) -> ActionResult: async def _work(): await ctx.progress(20, "Fetching context") ctx_data = await ctx.http.post(retrieval_url, json={...}, timeout=15) await ctx.progress(50, "Generating with AI") out = await ctx.http.post(openai_url, json={...}, timeout=120) await ctx.progress(90, "Saving") return ActionResult.success( summary="Refined output ready! 🎉", data={"text": out.body["text"]}, ) task_id = await ctx.background_task(_work(), long_running=False, name="AI refinement") return ActionResult.success( summary="Got it — refining (≈90s). I'll send the result here.", data={"task_id": task_id}, ) -
ctx.deliver_chat_message(text, *, msg_type="response", refresh_panels=None)— public API for extension-initiated bot-turn injection at any time (not tied to task completion). Text truncated to 64KB with marker.msg_type∈{response, system, tool_result}.@ext.webhook("/callback", method="GET") async def oauth_callback(ctx, params): # ... exchange code, store refresh token via ctx.secrets ... await ctx.deliver_chat_message("Spotify connected! 🎵 You can now ask " "what's playing or search your library.") return ActionResult.success(summary="OAuth complete")
Federal invariants (new)
I-LONGRUN-HTTP-CAP-180S— per-call timeout federal cap. Anything larger must usectx.background_task().I-LONGRUN-BG-CORO-RETURNS-ACTIONRESULT— the coroutine passed toctx.background_task()MUST returnActionResult. Non-ActionResult return triggers a critical audit row and delivers a fallback error message to chat.I-LONGRUN-BG-USER-SCOPED— every background task is bound to(ext_id, user_id)at creation. Cross-user cancel/status returns 403.I-LONGRUN-CHAT-INJECT-USER-SCOPED— chat inject scoped to(ext_id, user_id). Cross-user inject returns 403.I-LONGRUN-CHAT-INJECT-AUDIT-EVERY— every chat inject writes an audit row.
Migration
None required — all additions are additive opt-in. Existing extensions work unchanged.
v4.2.11 — 2026-05-13
Fix: ui.Link(text=...) no longer breaks panel render
Before v4.2.11, ui.Link only accepted the visible text as label=. Passing the natural-looking text= kwarg raised TypeError: Link() got an unexpected keyword argument 'text' at panel-render time, which killed the right panel for any extension that used the alias.
Changed
ui.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.
Federal invariants
| Invariant | What it pins |
|---|---|
I-MANIFEST-EMITTER-SCHEMA-SYMMETRIC | Now actually holds for secrets[] — emitter and schema agree. |
Migration notes
- No code change required in extensions. Existing manifests with
secrets[](emitted since v4.2.2) now pass strict Pydantic validation instead of relying on validators that didn't gate through the schema. - Manifests with malformed secret entries (e.g.
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. - Seven federal invariants enforced across SDK + auth-gateway:
I-SECRETS-USER-SCOPED,I-SECRETS-NEVER-LOGGED,I-SECRETS-EXT-SCOPED,I-SECRETS-VAULT-DEPENDENCY,I-SECRETS-AUDIT-FOREVER(auth-gateway-side)I-SECRETS-HANDLER-SCOPE-MEMORY,I-SECRETS-CONTRACT-DECLARED(SDK-side) See Federal contract for definitions.
- Five new exceptions at top-level
imperal_sdk:SecretNotDeclaredError,SecretWriteForbidden,SecretVaultUnavailable,SecretValueTooLarge,SecretDeclarationConflict. - Panel UI — Imperal Panel now ships
/ext/[extId]/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.
Federal invariants added
| Invariant | What it pins |
|---|---|
I-SYSTEM-APPS-NEVER-UNINSTALLABLE | Auth-gw refuses uninstall on system=True rows. |
I-MARKETPLACE-HIDES-SYSTEM | Every /v1/marketplace/* SELECT has AND system = FALSE in WHERE. |
I-SYSTEM-FLAG-RESERVED-FOR-IMPERAL | Only authors in the Imperal allowlist may set the flag. |
Migration notes
For the four first-party extensions: add system=True to your Extension(...) and re-publish through the Dev Portal. The live DB column was already backfilled during the Sprint B deploy, so marketplace already hides them — this just keeps manifest and DB consistent going forward.
For everyone else: nothing changes. system defaults to False. Third-party apps continue to publish, list, and install through the normal marketplace flow.
See /en/concepts/system-apps for the full lifecycle picture.
v4.1.9 — 2026-05-10
imperal init template now passes federal validators clean
Fixes the v4 onboarding cliff: imperal init my-ext && imperal validate used to surface 4 ERROR-severity validators on the very first run (V14 description ≥40 chars, V15 missing display_name, V16 description ≥20 chars, V21 missing icon.svg). New developers followed the docs, scaffolded, and got rejected — making the SDK look broken before they wrote a line of their own code.
New scaffold writes:
Extension(...)with all v4 required kwargs (display_name,description≥40 chars,icon="icon.svg",actions_explicit=True,capabilities).ChatExtension(...)properly declared (chat template only).@chat.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 unified_config entry; the frontend reads the flag declaratively instead of consulting a hardcoded list of panel_id literals.
Backward compatibility: the legacy hardcoded allowlist (compose, email_viewer+message_id, editor+note_id, workshop) remains as a fallback for extensions that haven't redeployed since v4.1.7. It will be removed once those panel_ids migrate to declarative center_overlay=True.
Migration: add center_overlay=True to your @ext.panel(slot="center", ...) declaration and redeploy via panel.imperal.io/developer. No frontend code change required.
v4.1.7 — 2026-05-10
PANEL_SLOT_RENDERING_STATUS federal contract + I-PANEL-RENDERING-CONTRACT CI gate
Single source of truth in imperal_sdk.types.contributions for what the Imperal Panel host actually does with each declared slot. Three federal categories:
| 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
I-MANIFEST-EMITTER-SCHEMA-SYMMETRIC CI gate
Closes the schema/emitter drift class of bug surfaced in v4.1.4 → v4.1.5: chat/extension.py started emitting id_projection in tool entries and manifest.py started emitting sdk_version at top level, but manifest_schema.Tool/Manifest had model_config = ConfigDict(extra='forbid') without those fields. Production extensions shipped manifests that local imperal validate rejected — drift was only visible at manual CLI run, not PR time.
New tests/test_manifest_roundtrip_gate.py builds a canary Extension exercising every emitter code path (@ext.tool, @ext.signal, @ext.schedule, @ext.webhook, @ext.on_event, @ext.on_install, @ext.health_check, @ext.panel, @chat.function with effects=, id_projection=) and asserts:
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).
Federal invariant I-MANIFEST-EMITTER-SCHEMA-SYMMETRIC documented in the test docstring. Future schema/emitter drift fails CI at PR time, not at production deploy time.
Migration: None required. CI gate only.
v4.1.5 — 2026-05-09
Manifest-schema gaps closed + sdk_version auto-emission
Two long-standing schema issues were both blocking imperal validate for extensions that already deployed cleanly through the Dev Portal:
Tool.id_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 world's first AI Cloud OS. Webbee 🐝 is its agent.") and trimmed of inline release notes (CHANGELOG.md is the single source of release history).- GitHub repo
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 SigNoz 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(
description="Delete all notes inside a folder.",
pydantic_param=DeleteNotesFromFolderParams,
action_type="destructive",
id_projection="folder_id", # ← NEW
)Migration: Optional. Tools with single-noun names continue to auto-derive correctly.
v4.1.1 — 2026-05-05
emit_narration audit-mode scope clarification
Tightened the schema description on the mode field of EMIT_NARRATION_TOOL so BYOLLM LLMs don't generalise audit-mode brevity globally across other tool calls in the same turn.
Symptom (pre-fix): "create a note with a 200-word essay" would produce content_text='<essay 200 words>' placeholder when narration_mode was audit.
Fix: Schema description now explicitly says mode controls only the prose field's interpretation — never a global brevity directive.
Migration: None. Pure schema description change. Effective on next worker restart with v4.1.1+.
v4.1.0 — 2026-05-02
Pydantic feedback loop — bounded retry on ValidationError
The runtime quality release. When the LLM emits arguments that fail Pydantic validation, the SDK auto-retries (max 2) with structured prose feedback derived from e.errors().
Federal invariants added (5):
I-PYDANTIC-RETRY-BUDGETI-PYDANTIC-RETRY-SCOPEI-PYDANTIC-FEEDBACK-STRUCTUREDI-PYDANTIC-FC-SINGLE-APPENDI-PYDANTIC-WIRE-FROZEN
Closed ~75% of arg-quality hallucinations in production traffic.
Migration: Automatic for any @chat.function that declares pydantic_param=.... Legacy **kwargs handlers unchanged.
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 federal invariants: web-kernel-side
I-EXT-SYSTEM-TASK-NO-MESSAGE-KWARG, SDK-sideI-CHAT-FUNCTION-VERBATIM-PARAMS,I-EXT-SCHEMA-DESCRIPTION-STRING-DRAFT-2020-12
Migration: Re-run python -m imperal_sdk.cli build to regenerate v3 manifest. Existing v2 manifests fail V14 — must rebuild.
Pre-v4 history
Earlier releases (v3.x and below) used schema v2 with weaker validators. Migration paths are documented in the federal extension contract record on GitHub.