Imperal Docs
Guides

Dev Portal — publishing your extension

Validation, manifest review, deploy, marketplace listing, version bumps, and scope approvals for third-party extension authors

What "publishing" means

Publishing puts your extension on Imperal Cloud where real users can install it from the marketplace. Once published:

  • The web-kernel can load your handlers and execute them in response to user chat messages.
  • Users can install the extension from the marketplace and grant it access to their data.
  • You receive install/uninstall events and can surface panels, skeletons, and scheduled actions.

Publishing is an act of trust. The review process exists to protect users from code that fabricates data, misuses scopes, or degrades platform reliability.


The publishing flow at a glance

local code
  → imperal validate .
  → imperal build .
  → version bump in app.py + imperal.json
  → upload zip / link git repo in Dev Portal
  → automated validator suite (V1-V24 + AST rules)
  → human review (new scopes, destructive actions, external network)
  → marketplace listing (title, description, icon, categories)
  → publish → users install → web-kernel runs your handlers

Prerequisites

Before you submit to the Developer Portal:

RequirementWhere to find it
Working extension with all filesLocal dev setup
All V14-V22+V24+V31 validators cleanValidators reference
Tests passing (pytest tests/ -v)Testing extensions
imperal build . regenerated imperal.jsonRun after every structural change
Stable semver version (e.g. 1.0.0 for first release)See Step 2 below
Valid icon.svg — V21: SVG with viewBox, ≤100KBManifest reference

Validators block publishing

The automated review runs the same validator suite as imperal validate .. If any V14-V22+V24+V31 rule fires at ERROR level, the submission is rejected immediately. Fix all errors locally before submitting.


Step 1: Validate locally

Run the full validator suite from inside your extension directory:

imperal validate .

A clean extension looks like this:

── Imperal Extension Validator v1.0 ────────────────────────────
Extension: my-extension v1.0.0
Tools: 3, Functions: 2, Events: 0

✅ No issues found!

If there are errors, fix them all before proceeding. The most common pre-publish failures are listed in the Common rejection reasons section below.

Also run the syntax check and regenerate the manifest:

python3 -m py_compile app.py handlers_crud.py main.py
imperal build .

imperal build . regenerates imperal.json from your code. Commit the updated file — the manifest is what the Dev Portal and marketplace read.

What the validator checks

Rule rangeCategoryLevel
V1-V2app_id format, semver versionERROR
V3-V5At least one tool, action_type enum, returns ActionResultERROR
V7No direct LLM imports (import anthropic, import openai)ERROR
V14-V15Extension description ≥40 chars, display_name ≥3 chars, neither equals app_idERROR
V16Every @chat.function description ≥20 charsERROR
V17-V18Pydantic params at module scope, no forward refsERROR
V19actions_explicit=True + chain_callable=True on write/destructiveERROR
V21SVG icon: valid XML, viewBox present, ≤100KB, no embedded rasterERROR
V22Lifecycle hook signatures match SDK contractERROR
V24ctx.skeleton.* not accessed in @chat.function bodiesERROR
V6, V9-V13Various advisory checksWARN or INFO
v1.6.0 ASTSkeleton guards, cache TTL cap, panel slot validationERROR

See Validators reference for the full table with fix guidance.


Step 2: Bump the version

Every submission must carry a version different from the previously published one. Version follows semver: MAJOR.MINOR.PATCH.

Update app.py:

app.py
from imperal_sdk import Extension

ext = Extension(
    "my-extension",
    version="1.0.0",   # bump this
    display_name="My Extension",
    description="A sample extension that greets users by name with a friendly message.",
    icon="icon.svg",
    actions_explicit=True,
)

Then rebuild:

imperal build .

This regenerates imperal.json with the new version. Commit both files.

Semver discipline

ChangeBumpReview path
Bug fix only — same tools, same scopesPATCH (e.g. 1.0.0 → 1.0.1)Auto-approved
New tools or features — no new scopesMINOR (e.g. 1.0.0 → 1.1.0)Auto-approved if no scope changes
Breaking changes — removed tools, new scopes, schema migrationMAJOR (e.g. 1.0.0 → 2.0.0)Human review required

What counts as a breaking change?

Removing a @chat.function tool, renaming an existing tool (same as removing + adding), adding a new capability scope that users must grant, or changing a Pydantic params model in a way that would reject previously valid inputs.


Step 3: Package your extension

The Dev Portal accepts either a git repository URL or a .zip file upload. If submitting as a zip, include:

my-extension.zip
  my-extension/
    app.py
    main.py
    handlers_crud.py
    panels.py          # if you have panels
    skeleton.py        # if you have skeleton sections
    icon.svg
    imperal.json
    requirements.txt
    tests/
      __init__.py
      test_handlers_crud.py

Exclude the following from your zip:

PathWhy
.git/Internal VCS history
__pycache__/, *.pycCompiled bytecode — regenerated at deploy time
.venv/, venv/Virtualenv — incompatible with platform runtime
.env, *.env.*Never ship secrets in extension packages
.imperal/credentialsAPI credentials — never included
*.bakBackup files

A minimal .gitignore (and zip exclusion) list:

.venv/
__pycache__/
*.pyc
.env
.imperal/

If you use git URL submission, the platform clones the repository at review time and ignores the above paths automatically via the .gitignore.


Step 4: Upload via Dev Portal

Subject to refinement

The Dev Portal UI is at developer.imperal.io (subject to refinement when Dev Portal documentation lands). The steps below describe the canonical flow based on the SDK's deploy command contract.

Sign in to the Developer Portal

Navigate to the Developer Portal and sign in with your Imperal account. If you are a first-time publisher, complete the developer profile (organization name, contact email, terms acceptance).

Create an extension entry

Select New Extension. You will be prompted for:

  • App ID — must match the app_id in your Extension(...) constructor exactly. Once set, this is permanent.
  • Display name — the human-readable name shown in the marketplace. Must match display_name in Extension(...).
  • Category — primary category for marketplace discovery (e.g. Productivity, Communication, Analytics).

The platform checks that the app_id is not already claimed by another developer.

Upload your extension

Choose one of:

  • Git URL — paste your repository URL. The platform clones at review time. Tag a commit or specify a branch.
  • ZIP upload — drag and drop the package from Step 3.

The platform runs the automated validator suite immediately after upload. If any V14-V22+V24+V31 rule fires at ERROR, the upload is rejected with a specific error list. Fix and re-upload.


Step 5: Manifest review

Once the automated validators pass, the manifest enters review.

Automated review (instant)

The same checks as imperal validate ., plus:

  • Manifest schema v3 conformance (V14)
  • Tool description quality for embeddings (tools with empty descriptions fail embedding indexing even if V16 passes — imperal validate checks this locally too)
  • Scope format validation against the platform's capability registry

Human review

Human review is triggered when any of the following are present:

TriggerReason
New capability scope not previously approvedScope grant requires explicit user consent — reviewers verify the scope is necessary
Any action_type="destructive" toolIrreversible actions (deletes, sends, charges) require confirmation by design
External network calls (ctx.http.*) to domains not previously declaredNetwork egress to new endpoints is audited
Major version bumpEnsures breaking changes are intentional and documented
First-ever submission from a new developer accountAll first submissions receive manual review

Review SLA: Standard reviews complete within 2 business days. First-time developer reviews may take up to 5 business days. Submissions with new external-network scopes or financial action types may take additional time.

During review you can:

  • Monitor status on the Dev Portal dashboard.
  • Respond to reviewer questions via the portal messaging thread.
  • Withdraw and resubmit a corrected version (this restarts the queue).

Step 6: Test in staging

After automated validation passes (before or during human review), a staging install link is available on your Dev Portal dashboard. This allows you to:

  • Install the extension into your own Imperal account for end-to-end testing.
  • Invite specific users (by email) to test the staging version.
  • Verify panels render correctly, skeleton sections surface in the intent classifier, and confirmation cards appear for destructive actions.

Subject to refinement

Staging environment details (URL, invite flow) are subject to refinement when Dev Portal documentation lands. The staging install link is only accessible to the developer account and explicitly invited testers.


Step 7: Marketplace listing

Once review passes, configure your marketplace listing before going live:

FieldRequirementNotes
TitleRequiredShown in search results and install dialogs
Short descriptionRequired, ≤160 charsAppears in search result cards
Full descriptionRequired, ≤2000 charsShown on the extension detail page
IconRequiredThe icon.svg from your manifest; also used in Panel tab headers
ScreenshotsOptional (strongly recommended)Panel screenshots, confirmation card examples
CategoriesAt least one requiredDrives marketplace discovery filters
Supported languagesOptionalLanguages your extension's responses support
Privacy policy URLRequired if you collect or process user dataStandard marketplace requirement
Support contactRequiredEmail or URL for user support requests

Write descriptions that are accurate and specific. The marketplace review will reject listings with boilerplate or misleading descriptions.


Step 8: Publish

After setting up the marketplace listing, click Publish. The extension becomes live in the marketplace.

Users who discover or navigate to your extension's marketplace page can click Install. The install flow:

  1. The platform displays the scopes your extension requires.
  2. The user reviews and grants consent.
  3. The extension is added to the user's workspace.
  4. The web-kernel calls your @ext.on_install handler (if defined) to provision any per-user resources (e.g. database tables).
  5. The extension's panels, skeletons, and chat tools become active for that user.

Your extension's install URL follows the pattern: panel.imperal.io/marketplace/ext/<app_id> (subject to refinement).

Installs are per-user

Each user who installs your extension gets an isolated data namespace. Your ctx.store.* calls are always scoped to ctx.user.imperal_id. Never store cross-user data in a per-user collection.


Versioning in detail

Patch releases (bug fixes)

Patch releases (1.0.0 → 1.0.1) that do not add new tools, remove existing tools, or add new scopes are auto-approved. Deploy the new zip or push to git, select Update Version in the Dev Portal, and the update rolls out automatically.

Minor releases (new features, no new scopes)

Minor releases (1.0.0 → 1.1.0) that add new @chat.function tools or panels — but do not add new capability scopes — are auto-approved if they pass the automated validator suite.

Major releases (breaking changes)

Major releases (1.0.0 → 2.0.0) require a full human review cycle. A major release is appropriate when:

  • You are removing a @chat.function tool that users may rely on.
  • You are renaming a tool (functionally equivalent to remove + add).
  • You are adding a new capability scope.
  • You are changing a Pydantic params model in a way that would reject previously valid inputs.
  • You are migrating users' stored data (requires @ext.on_upgrade + migration logic).

For major releases, document the breaking changes in your full description. Reviewers check that the changelog is truthful.


Scope approvals

Scopes are declared in Extension(capabilities=[...]). Each scope must be explicitly granted by the user at install time.

Read-only scopes

app.py
from imperal_sdk import Extension

ext = Extension(
    "my-extension",
    version="1.0.0",
    capabilities=["my-extension:read"],
    display_name="My Extension",
    description="A sample extension that greets users by name with a friendly message.",
    icon="icon.svg",
    actions_explicit=True,
)

Read-only scopes are auto-approved in review. Users see the scope list at install time and can decline.

Write scopes

app.py
from imperal_sdk import Extension

ext = Extension(
    "my-extension",
    version="1.0.0",
    capabilities=["my-extension:read", "my-extension:write"],
    display_name="My Extension",
    description="A sample extension that greets users by name with a friendly message.",
    icon="icon.svg",
    actions_explicit=True,
)

Write scopes are reviewed to confirm the extension actually requires write access for its stated purpose. Every @chat.function with action_type="write" must have chain_callable=True (V19).

External-network scopes

If your extension calls external APIs via ctx.http.*, reviewers check the destination domains. Declare outbound domains clearly in your full description. Extensions that call undeclared endpoints may be rejected or suspended.

Destructive actions

action_type="destructive" functions trigger the platform's confirmation card — the user must explicitly approve the action in the Panel UI before the web-kernel executes the handler. This applies to irreversible operations: deletes, bulk operations, sends, and any action that charges the user.

handlers_crud.py
from __future__ import annotations

from pydantic import BaseModel, Field
from imperal_sdk import Extension, ActionResult
from imperal_sdk.chat import ChatExtension

ext = Extension(
    "my-extension",
    version="1.0.0",
    display_name="My Extension",
    description="A sample extension that greets users by name with a friendly message.",
    icon="icon.svg",
    actions_explicit=True,
)
chat = ChatExtension(ext, tool_name="my_extension_chat", description="My Extension chat assistant")


class DeleteItemParams(BaseModel):
    item_id: str = Field(description="ID of the item to permanently delete")


@chat.function(
    "delete_item",
    description="Permanently delete an item from the user's collection — cannot be undone",
    action_type="destructive",
    chain_callable=True,
    effects=["delete:item"],
)
async def delete_item(ctx: object, params: DeleteItemParams) -> ActionResult:
    """Delete an item permanently."""
    # ctx.store.delete is called only after user confirms via confirmation card
    return ActionResult.success(
        data={"deleted_id": params.item_id},
        summary=f"Deleted item {params.item_id}",
    )

Do not attempt to bypass the confirmation gate. Extensions that trigger confirmation cards for non-destructive actions (to appear safer) or skip them for destructive actions violate the federal action contract and will be rejected.


Updating an installed extension

Patch and minor updates (no new scopes)

Existing users receive the update automatically when you publish a new patch or minor version. The web-kernel restarts the extension worker and loads the new code. No user action required.

Major updates and new scopes

When a new version adds scopes not previously granted, or bumps the major version, existing users are notified in their workspace. They see a prompt explaining what changed and what new scopes are requested. They must opt-in to continue using the extension. Users who decline stay on the previous version until they opt-in or uninstall.

Data migrations

If your new version requires migrating stored data, implement @ext.on_upgrade:

app.py
from imperal_sdk import Extension

ext = Extension(
    "my-extension",
    version="2.0.0",
    display_name="My Extension",
    description="A sample extension that greets users by name with a friendly message.",
    icon="icon.svg",
    actions_explicit=True,
)


@ext.on_upgrade("2.0.0")
async def upgrade_to_v2(ctx: object, from_version: str = "") -> None:
    """Migrate per-user data from v1 schema to v2 schema."""
    # from_version is the user's current installed version string
    # This handler runs once per user when they opt-in to v2.0.0
    pass

The web-kernel passes from_version as the user's currently installed version string. The handler MUST accept from_version as a kwarg (V22).


Common rejection reasons

V14-V22+V24+V31 validator errors

Run imperal validate . locally and fix all ERROR-level issues before submitting. The automated review runs the same suite. See Validators reference.

Missing or boilerplate descriptions

The marketplace review rejects listings where:

  • Extension(description=...) is fewer than 40 chars or equals the app_id.
  • @chat.function(description=...) is fewer than 20 chars or a placeholder like "TODO".
  • The marketplace Full description is copy-pasted boilerplate.

Good descriptions tell the LLM intent classifier when to call each tool and explain to users what the extension does. Write them as you would write them for a colleague who has never seen your extension.

Sensitive data in display_name or description

Do not include API keys, secrets, internal service URLs, or personally identifiable information in any manifest field. These fields are public.

Unverified webhook secret handling

If your extension uses @ext.webhook, the handler must verify the incoming request's HMAC signature before processing the payload. The platform cannot enforce this statically, but the human reviewer checks for it. The pattern is:

handlers_webhook.py
import hashlib
import hmac
import os
from imperal_sdk import ActionResult


async def handle_my_webhook(ctx: object, headers: dict, body: str, query_params: dict) -> ActionResult:
    """Handle incoming webhook from external service."""
    secret = os.environ["MY_WEBHOOK_SECRET"]
    sig_header = headers.get("X-Hub-Signature-256", "")
    expected = "sha256=" + hmac.new(
        secret.encode(), body.encode(), hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(sig_header, expected):
        return ActionResult.error("Invalid webhook signature")
    # process verified payload
    return ActionResult.success(data={}, summary="Webhook processed")

Extensions that process webhook payloads without signature verification are rejected.

ctx.skeleton.* access in handler body (V24)

The skeleton is the LLM-facts snapshot — not a live data source. Only @ext.skeleton refresh tools may read it. Accessing ctx.skeleton from a @chat.function body raises SkeletonAccessForbidden at runtime and is caught by V24 at validate time.

from __future__ import annotations

from pydantic import BaseModel
from imperal_sdk import Extension, ActionResult
from imperal_sdk.chat import ChatExtension

ext = Extension(
    "my-extension",
    version="1.0.0",
    display_name="My Extension",
    description="A sample extension that greets users by name with a friendly message.",
    icon="icon.svg",
    actions_explicit=True,
)
chat = ChatExtension(ext, tool_name="my_extension_chat", description="My Extension chat assistant")


class ListParams(BaseModel):
    status: str = "active"


# V24 violation — do NOT do this:
# @chat.function("list_monitors", description="List active monitors by status and name")
# async def list_monitors(ctx, params: ListParams) -> ActionResult:
#     data = await ctx.skeleton.get("monitors_summary")  # V24 ERROR — skeleton is classifier input only


# Correct — query the real store
@chat.function("list_monitors", description="List active monitors by status and name")
async def list_monitors(ctx: object, params: ListParams) -> ActionResult:
    """List monitors from the real store, not from skeleton."""
    return ActionResult.success(data={"status": params.status}, summary="Listed monitors")

Insufficient test coverage

The validator does not enforce test coverage, but the human reviewer checks for it. Extensions with no test files are flagged. The minimum expectation is at least one test per @chat.function handler, using MockContext from imperal_sdk.testing. See Testing extensions.


Marketplace policies (high level)

All published extensions must comply with the Imperal Marketplace Terms of Service. Key expectations:

Security: Extensions must not bypass the platform's auth, audit, or confirmation mechanisms. Secrets must be stored via environment variables or ctx.config, never in manifest fields or source code.

Privacy: Extensions must declare in their privacy policy how they handle user data. Data stored in ctx.store is isolated per-user — extensions must not write data from one user into another user's namespace.

Accuracy: Extension descriptions and tool descriptions must accurately reflect what the extension does. Misleading descriptions are grounds for suspension.

Stability: Extensions should implement @ext.health_check and return accurate health status. Repeated crashes or health-check failures may result in automatic suspension with notification to the developer.

Updates: Developers are expected to maintain extensions in response to platform API changes. Extensions that break after a platform SDK upgrade and are not updated within 30 days may be delisted.


How to publish

For third-party extension authors, the standard path is the Dev Portal upload/git flow described above. Run imperal build . + imperal validate . locally, then drag-and-drop your packaged extension at panel.imperal.io/developer. Direct Registry write access is reserved for first-party Imperal authors.


Cross-references

On this page