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 handlersPrerequisites
Before you submit to the Developer Portal:
| Requirement | Where to find it |
|---|---|
| Working extension with all files | Local dev setup |
| All V14-V22+V24+V31 validators clean | Validators reference |
Tests passing (pytest tests/ -v) | Testing extensions |
imperal build . regenerated imperal.json | Run 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, ≤100KB | Manifest 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 range | Category | Level |
|---|---|---|
| V1-V2 | app_id format, semver version | ERROR |
| V3-V5 | At least one tool, action_type enum, returns ActionResult | ERROR |
| V7 | No direct LLM imports (import anthropic, import openai) | ERROR |
| V14-V15 | Extension description ≥40 chars, display_name ≥3 chars, neither equals app_id | ERROR |
| V16 | Every @chat.function description ≥20 chars | ERROR |
| V17-V18 | Pydantic params at module scope, no forward refs | ERROR |
| V19 | actions_explicit=True + chain_callable=True on write/destructive | ERROR |
| V21 | SVG icon: valid XML, viewBox present, ≤100KB, no embedded raster | ERROR |
| V22 | Lifecycle hook signatures match SDK contract | ERROR |
| V24 | ctx.skeleton.* not accessed in @chat.function bodies | ERROR |
| V6, V9-V13 | Various advisory checks | WARN or INFO |
| v1.6.0 AST | Skeleton guards, cache TTL cap, panel slot validation | ERROR |
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:
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
| Change | Bump | Review path |
|---|---|---|
| Bug fix only — same tools, same scopes | PATCH (e.g. 1.0.0 → 1.0.1) | Auto-approved |
| New tools or features — no new scopes | MINOR (e.g. 1.0.0 → 1.1.0) | Auto-approved if no scope changes |
| Breaking changes — removed tools, new scopes, schema migration | MAJOR (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.pyExclude the following from your zip:
| Path | Why |
|---|---|
.git/ | Internal VCS history |
__pycache__/, *.pyc | Compiled bytecode — regenerated at deploy time |
.venv/, venv/ | Virtualenv — incompatible with platform runtime |
.env, *.env.* | Never ship secrets in extension packages |
.imperal/credentials | API credentials — never included |
*.bak | Backup 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_idin yourExtension(...)constructor exactly. Once set, this is permanent. - Display name — the human-readable name shown in the marketplace. Must match
display_nameinExtension(...). - 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 validatechecks 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:
| Trigger | Reason |
|---|---|
| New capability scope not previously approved | Scope grant requires explicit user consent — reviewers verify the scope is necessary |
Any action_type="destructive" tool | Irreversible actions (deletes, sends, charges) require confirmation by design |
External network calls (ctx.http.*) to domains not previously declared | Network egress to new endpoints is audited |
| Major version bump | Ensures breaking changes are intentional and documented |
| First-ever submission from a new developer account | All 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:
| Field | Requirement | Notes |
|---|---|---|
| Title | Required | Shown in search results and install dialogs |
| Short description | Required, ≤160 chars | Appears in search result cards |
| Full description | Required, ≤2000 chars | Shown on the extension detail page |
| Icon | Required | The icon.svg from your manifest; also used in Panel tab headers |
| Screenshots | Optional (strongly recommended) | Panel screenshots, confirmation card examples |
| Categories | At least one required | Drives marketplace discovery filters |
| Supported languages | Optional | Languages your extension's responses support |
| Privacy policy URL | Required if you collect or process user data | Standard marketplace requirement |
| Support contact | Required | Email 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:
- The platform displays the scopes your extension requires.
- The user reviews and grants consent.
- The extension is added to the user's workspace.
- The web-kernel calls your
@ext.on_installhandler (if defined) to provision any per-user resources (e.g. database tables). - 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.functiontool 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
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
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.
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:
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
passThe 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 theapp_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:
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
Local dev setup
From zero to a validated extension in 10 minutes
Testing extensions
MockContext, MockStore, and assertion patterns
Validators reference
All V14-V22+V24+V31 rules, levels, and fixes
Manifest reference
Every field in imperal.json explained
Audit and security
ctx scoping, secrets, OWASP patterns
Error handling
ActionResult.error patterns, Magic UX, retryable