Imperal Docs
SDK Reference

Experimental decorators reference

@ext.tray / @ext.widget / @ext.expose / @ext.oauth_provider — under-specified surfaces with prominent warnings

These decorators exist in the SDK but have one or more of: zero production deployment, incomplete manifest emission, undocumented host-side contract, or routing paths not yet committed to a public API. Each section below documents what is in the SDK source, what is known to work, and what is not yet reliable.

Experimental surface — contract subject to change

The four decorators on this page are not part of the stable v4.1 contract. No production extensions on Imperal Cloud currently use them at scale. The signatures may shift, manifest emission may change, and web-kernel-side routing is not documented as a public API. Build against them only with explicit awareness that you may need to migrate when the contract solidifies.


@ext.tray

System tray item: an icon in the platform's top-bar area with an optional badge count and dropdown panel. The handler returns a UINode tree (typically ui.TrayResponse) that the platform renders as a compact overlay.

Experimental — zero production deployments

@ext.tray is SDK-defined and manifest-emitting, but no production extension currently uses it. Frontend rendering behaviour of TrayResponse (badge positioning, dropdown lifecycle) is not formally documented. Do not ship tray items to users without verifying that your platform version renders them.

Signature

def tray(
    self,
    tray_id: str,
    icon: str = "Circle",
    tooltip: str = "",
) -> Callable:
    ...

Kwargs reference

Prop

Type

Handler signature

async def handler(ctx, **kwargs) -> UINode:
    ...

The decorator wraps your handler: if the return value has a .to_dict() method, the wrapper serializes it to {"ui": ..., "tray_id": tray_id, "icon": icon} before returning to the caller. If the return value does not have .to_dict(), it is passed through as-is.

How it registers

Internally, @ext.tray does two things:

  1. Adds a TrayDef to ext._tray[tray_id] with tray_id, icon, and tooltip.
  2. Adds a synthetic ToolDef named __tray__{tray_id} to ext._tools, which is what the web-kernel dispatches via the /call endpoint.

The generate_manifest() function emits a tray[] section in the manifest for every registered TrayDef:

{
  "tray": [
    {"tray_id": "unread", "icon": "Mail", "tooltip": "Unread messages"}
  ]
}

Use ui.TrayResponse for a badge + optional dropdown panel:

from imperal_sdk import Extension, ui

ext = Extension(
    "my-mail",
    version="1.0.0",
    display_name="My Mail",
    description="My Mail extension — read and send email with AI assistance.",
    icon="icon.svg",
    actions_explicit=True,
)


@ext.tray("unread", icon="📧", tooltip="Unread messages")
async def tray_unread(ctx, **kwargs):
    count = await ctx.store.count("messages", where={"read": False})
    badge = ui.Badge(str(count), color="red" if count > 0 else "gray")
    return ui.TrayResponse(badge=badge)

ui.TrayResponse accepts two optional kwargs:

KwargTypeDescription
badgeUINode | NoneOverlay node — typically ui.Badge
panelUINode | NoneDropdown panel node — any UINode tree

Known constraints

  • There is no slot validation at decoration time — unlike @ext.panel, @ext.tray does not enforce known tray IDs.
  • The platform-side rendering contract for TrayResponse.panel (dropdown positioning, dismiss behaviour, scroll containment) is not documented.
  • Zero production extensions use @ext.tray. If you build one, verify rendering end-to-end on your platform version before shipping to users.

Production usage

None found in any reviewed Imperal extension at time of audit (SDK v4.2.0).


@ext.widget

Widget for platform injection points: a small UI fragment (stat tile, toolbar button, contextual action) rendered at a named slot in the platform host. The handler returns a UINode tree.

Manifest emission gap — web-kernel cannot discover widgets

@ext.widget stores only a synthetic ToolDef (__widget__{widget_id}) in ext._tools. There is no widgets[] section emitted by generate_manifest() — the manifest builder skips all __*-prefixed tools. This means the web-kernel cannot discover registered widgets from the manifest using the standard manifest-driven flow. Side-channel discovery would be required. Until this gap is closed, @ext.widget is not production-ready for manifest-driven deployments.

Signature

def widget(
    self,
    widget_id: str,
    slot: str = "dashboard.stats",
    label: str = "",
    icon: str = "",
    **kwargs,
) -> Callable:
    ...

Kwargs reference

Prop

Type

Additional **kwargs are silently accepted and ignored — they are not stored anywhere.

What actually gets stored

The decorator wraps your handler (serializing .to_dict() results to {"ui": ..., "widget_id": widget_id}) and registers only a synthetic ToolDef:

ext._tools["__widget__my_stat"] = ToolDef(
    name="__widget__my_stat",
    func=<wrapper>,
    description="Widget: My Stat",
)

The widget metadata (slot, label, icon) is not stored in any dedicated registry. There is no ext._widgets attribute. The manifest emitter skips __widget__* entries along with all other __* entries. The platform has no way to read widget slot assignments from the manifest.

Illustrative usage

The following shows the decorator API as written. It will not produce a discoverable manifest entry:

from imperal_sdk import Extension, ui

ext = Extension(
    "my-app",
    version="1.0.0",
    display_name="My App",
    description="My App extension — manage resources with AI assistance.",
    icon="icon.svg",
    actions_explicit=True,
)


@ext.widget("active_count", slot="dashboard.stats", label="Active Items", icon="🌀")
async def widget_active_count(ctx, **kwargs):
    count = await ctx.store.count("items", where={"status": "active"})
    return ui.Badge(str(count), color="blue")

No manifest entry emitted

The code above registers the handler callable and a synthetic tool for direct dispatch, but generate_manifest() will not include a widgets[] section. The web-kernel relies on the manifest for discovery. Without a widgets[] section, the slot assignment is invisible to the platform.

Production usage

None found in any reviewed Imperal extension at time of audit (SDK v4.2.0).


@ext.expose

Cross-extension IPC: expose a named method that other extensions can call via ctx.extensions.call(app_id, method_name, **kwargs). The Auth Gateway routes the call to the target extension's exposed handler.

Experimental — IPC contract not fully documented

@ext.expose stores the handler in ext._exposed and emits it to the manifest exposed[] section. ExtensionsClient.call() routes via the Auth Gateway to the target. However, the following are not documented as a public contract: the exact request shape the target handler receives, error semantics (what happens on timeout, auth failure, or handler exception), and cross-tenant IPC limitations. No production extension has deployed @ext.expose at time of audit. The imperal-drive extension design document uses this pattern as a planned interface — it is not yet deployed.

Signature

def expose(
    self,
    name: str,
    action_type: str = "read",
) -> Callable:
    ...

Kwargs reference

Prop

Type

Handler signature

async def handler(ctx, **kwargs) -> Any:
    ...

The handler receives the caller's Context object (with the caller's identity, not the calling extension's identity — verify this with a live test) and the keyword arguments passed to ctx.extensions.call().

What gets stored and emitted

# Internal storage:
ext._exposed["my_method"] = ExposedMethod(
    name="my_method",
    func=<handler>,
    action_type="read",
)

generate_manifest() emits:

{
  "exposed": [
    {"name": "my_method", "action_type": "read"}
  ]
}

Calling side

From the consuming extension:

result = await ctx.extensions.call("target-app", "method_name", param1="value")

ctx.extensions is an ExtensionsClient. It raises CircularCallError if a circular dependency is detected.

Illustrative usage

from imperal_sdk import Extension

ext = Extension(
    "my-storage",
    version="1.0.0",
    display_name="My Storage",
    description="My Storage extension — manage files with AI assistance.",
    icon="icon.svg",
    actions_explicit=True,
)


@ext.expose("get_file_url", action_type="read")
async def expose_get_file_url(ctx, file_id: str = "", ttl_s: int = 3600):
    doc = await ctx.store.get("files", file_id)
    if doc is None:
        return {"error": "not_found"}
    return {"url": doc.data.get("url"), "expires_in_s": ttl_s}


@ext.expose("register_file", action_type="write")
async def expose_register_file(ctx, path: str = "", mime_type: str = ""):
    doc = await ctx.store.create("files", {"path": path, "mime_type": mime_type})
    return {"file_id": doc.id}

Consuming extension:

# In another extension's @chat.function handler:
url_result = await ctx.extensions.call(
    "my-storage", "get_file_url", file_id="abc123", ttl_s=7200
)

Known gaps

  • The Auth Gateway routing path for cross-extension calls is not documented as a stable public contract.
  • Whether the target handler receives the caller's user context, the target's service context, or a merged context is not specified in public documentation.
  • Error handling on the receiving side (what the caller sees on handler exception, timeout, or auth failure) is not specified.
  • Cross-tenant calls (caller in tenant A, target in tenant B) may be blocked — the isolation contract is not documented.

Production usage

No deployed production extension uses @ext.expose at time of audit (SDK v4.2.0). The imperal-drive extension has @ext.expose in its design specification (handlers_cross_ext.py section) but this extension had not been deployed.


@ext.oauth_provider

@ext.oauth_provider does not exist in the SDK at v4.2.0. A search of the SDK source finds no oauth_provider method, class, or decorator anywhere in imperal_sdk.

This decorator does not exist in SDK v4.2.10

There is no @ext.oauth_provider in the current SDK. If you encountered this name in older documentation, a design document, or a community post, it refers to a planned future surface that has not shipped. Do not write code that references it.

OAuth flows in current SDK extensions are handled via @ext.webhook (OAuth callback endpoint) combined with secret storage via ctx.secrets. The imperal-drive design document references an ctx.oauth.complete_flow() call in its @ext.webhook("oauth_callback") handler — that method is not part of the public Context surface documented in v4.2.12.

If you need OAuth token management, use the pattern from deployed extensions:

  1. Register a webhook to receive the OAuth callback: @ext.webhook("oauth_callback")
  2. Inside the handler, extract the authorization code from query_params
  3. Exchange it for tokens via ctx.http.post(...) to the provider's token endpoint
  4. Persist tokens in ctx.store

When to use experimental decorators

Short answer: only if you are prepared to refactor when the contract changes, and only after verifying the feature works in a staging environment on your exact platform version.

Specifically:

  • @ext.tray — use in staging only; verify badge rendering and dropdown lifecycle before shipping to users. The manifest does emit a tray[] section, so the web-kernel can find tray items, but frontend rendering is unverified in production.
  • @ext.widget — do not use in production until the manifest emission gap is resolved. The web-kernel cannot discover widget slots from the manifest.
  • @ext.expose — use carefully in non-critical paths; the IPC error contract is not yet specified. Start with action_type="read" methods where failure is recoverable.
  • @ext.oauth_provider — does not exist; do not reference it.

Filing feedback

If you build against an experimental decorator and discover routing behaviour, error semantics, or rendering constraints that are not documented here, file a ticket with the Imperal Developer Portal. Experimental contracts solidify faster when concrete usage findings are reported.


Cross-references

On this page