Plugin extension guide¶
A plugin owns its operations, interface, assets, tests and release. The host renders typed data contributions and versioned local views through public contracts, without imports or named branches for concrete plugins. ADR 0094 defines the 0.9 boundary and independent delivery.
Follow Create and ship a custom plugin for the complete standalone public/private or
collection workflow. Start with hz system plugin new --help, then scaffold a plugin with the CLI. Use the generated
repository's make setup, make preview, make check and make acceptance. These use an installed compatible core artifact. A distribution is named heinzel-<name>, its Python package
heinzel_<name>, and its entry point belongs to heinzel.modules. See the architecture.
Choose the smallest extension¶
| You need | Use today | Rich-view plan |
|---|---|---|
| A command or a tool | @module.operation, with typed parameters and a serializable result |
Same operation remains the source of data |
| A footer indicator | @module.status, a StatusItem with detail and actions |
Keep the generic accessible icon and detail pane |
| A header action | module.action and a checked effect |
Keep the generic button for ordinary actions |
| Settings or sidebar navigation | module.nav, checked icons and existing effects |
Rich custom views remain separate |
| A Today card | @module.widget and a supported widget shape |
Keep data shapes for common views |
| A colour palette, logo or fonts | module.theme |
Optional theme figure sets, with core fallbacks |
| Background work | @module.job |
Persisted triggers and wake scheduling need their own implementation |
| A specialist interactive page | module.view(..., abi=1, slot="page", route="/plugins/<module>/<view>") |
Local versioned bundle with fallback |
| An interactive answer in chat | Text or structured tool output | A declared renderer, with text fallback and the same approval gate |
The public Python API is exported from heinzel and heinzel.kernel. Plugins do not import private kernel files,
heads, built-in modules or another plugin implementation. Shared interfaces belong in the public SDK or an implementation-free contract package. Import contracts enforce this.
Navigation and Settings¶
A plugin declares its title and semantic icon, and groups only its own Settings sections. Sidebar links use the existing checked effects; a declaration does not add a renderer or make an unknown route exist.
from heinzel import NavSettings, Navigate
module.nav(
title="Harbor",
icon="cloud-sun",
settings=NavSettings(sections=("harbor",), group="customize"),
sidebar=Navigate(path="/?settings=harbor"),
)
hz system ui navigation --json lists the same metadata the authenticated app uses. Disabled/uninstalled providers
are absent. An invalid declaration has a diagnostic and no executable target. Explicit valid entries replace the
generic Settings compatibility entry, so the app needs no plugin-name map. ADR 0072
records the ownership rules. Declare the core range that supplies this additive API when publishing a plugin.
Repository badges describe the installed source: the official core and plugins repositories are First-party, the contrib repository is
Community, and other repositories are Custom when public or Private when private. Package names and
catalog routes do not establish provenance. When visibility was not reported, the app says so rather than guessing;
repository visibility is recorded during installation or returned by the catalog, without a network request for each
badge. Catalog metadata discovers configured repository directories and external repositories with the heinzel-plugin topic. No central plugin list is required.
Python, CLI, MCP and REST¶
| Surface | Status | Declaration and limits |
|---|---|---|
| CLI command, argument, option, help and JSON | Available | @module.operation; public parameter types and Field(description=...); audience controls discovery, not authorization |
| CLI presentation | Available | render= supplies text presentation; operations return results and never print |
| MCP tools | Available | The head exposes operations using their declarations and heads= restrictions |
| MCP prompts and resources for skills | Available | module.skill; stdio MCP serves checked content outside chat |
| Arbitrary MCP prompts and resources | Proposed | No general plugin declaration; do not claim every MCP feature is an extension point |
| REST endpoints | Available | Operation exposure and read/write rules apply; inspect the generated reference for each operation's heads |
| Setup and onboarding steps | Available | @module.setup, Prompter, ConfigEditor and Credentials; web/chat onboarding adapters remain proposed |
| Health and doctor | Available | @module.health, including explicit live checks; failures say what to do next |
| System information | Available | @module.info; publish useful metadata, never credential values |
| Settings, defaults, schema and environment variables | Available | @module.settings and ModuleSettings; follow SETTINGS_UX |
| Credentials | Available | module.credential; injected Credentials; never put a secret into a setting or result |
| Capabilities and adapters | Available | @module.provides and declared requirements; bindings select adapters and mode-gated handles restrict writes |
| Agent runners and AI access | Available | Public capability/service contracts; see ADRs 0029, 0050 and 0057 |
| Services and dependency injection | Available | module.service; dependencies supplied by type, without imports between implementations |
| Company, organisation, site and team layers | Available | module.layer; general-to-specific recommendations, accepted by the person |
| Playbooks and artifact types | Available | module.playbook; types, discovery hints and contract templates; PLAYBOOKS |
| Artifact document templates | Proposed | A document starting point is distinct from a contract template; no public template API yet |
An operation is not automatically safe to run from every surface. Declare its heads, capabilities and write status; test the refusal paths as well as the successful call. Never put a private operation behind an unauthenticated head.
App layout and interaction¶
| Surface | Status | What a plugin can contribute |
|---|---|---|
| Left sidebar top icons | Proposed | No general plugin slot; Home and Search are host controls |
| Left navigation pages | Available | module.nav links to an owned page route or registered panel |
| Left sidebar bottom stack | Available, limited | Status contributions may name the sidebar slot; player and account layout remain host-owned |
| Left sidebar account row | Host-owned | No plugin injection; identity and account actions stay consistent |
| Header actions | Available | ActionLet, checked icons and effects; no independent left/right placement contract |
| Header left and right placement | Proposed | Explicit slots, with responsive priorities and overflow; never position with arbitrary CSS |
| Footer status icons and panes | Available | StatusLet; state uses colour plus shape, details are keyboard reachable, actions use the host gate |
| Footer left and right placement | Proposed | Current ordering is not a stable left/right extension API |
| Right sidebar panels | Available | module.view(..., slot="panel"); open /?panel=<module>:<view> |
| Full pages | Available | slot="page" with the owned /plugins/<module>/<view> route |
| Today | Available | Card, list, metric, progress and graphic widget shapes; ids identify contributions and avoid duplicates |
| Settings fields | Available | JSON Schema chooses the control; slot switches belong with the contributing module |
| Settings and sidebar metadata | Available | module.nav, NavSettings, checked Icon and existing effects; only active owned Settings sections |
| Custom Settings panes | Available | slot="settings", claiming only the owning module section |
| Connectors | Available, limited | Capability providers appear through the host's connector management; no arbitrary connector-card renderer |
| Search | Available, limited | Host navigation, settings, contracts and artifact types are searched; generic operation search and plugin result providers are proposed |
| Share and export | Proposed | Pages declare export data; design must exclude tokens, device links, private paths and secrets |
| Chat slash commands and mentions | Proposed | Data declarations name an operation or skill; no command string evaluated as code |
| Interactive chat results | Proposed | Tool result selects a registered view with validated data and a plain-text fallback |
Existing UI contribution models, limits and exposure rules are specified in ADR 0056. A richer view must not silently replace the generic footer, hide a confirmation or make a read operation write. Registered bundles load through the versioned SDK and the owning declaration; plugin browser code shares the trusted host realm.
Theme, sound and character¶
| Surface | Status | Contract |
|---|---|---|
| Themes, variants, backdrop, fonts and logo | Available | module.theme; validated assets and a checked palette; THEMES |
| Time-of-day and weather signals | Available | @module.signals; closed signal vocabulary; plugins supply weather |
| Soundscapes and earcons | Available | module.soundscape; checked assets or an allowed stream provider; ADR 0058 |
| Avatar accessories in graphic widgets | Available | Checked accessory data; the host draws the character |
| Dressing the user's own avatar | Proposed | A policy for selecting accessories across plugins, separate from a widget's own avatar |
| Avatar palette and seasonal decorations | Available | Optional checked theme data, for example snow, retaining the person's look choices |
| Original avatar SVG layers | Available | Optional per-age hat/outfit files, checked like logos; missing slots fall back to core; ADR 0073 |
| Full avatar and face icon sets | Proposed | Per-state assets need an explicit supported-look/fixed-choice contract |
| App, PWA and notification icon sets | Proposed | Validated source assets and generated required sizes; no remote asset URLs |
| Splash artwork and dance | Proposed | A declared theme asset with reduced-motion fallback; no arbitrary startup JavaScript |
| Terminal artwork | Host-owned today | Generated core art; a plugin artwork API needs a separate contract |
A fictional winter theme can add a snow cap, falling snow and a scarf. It must preserve a readable face and accessible state labels. Supported personal choices remain age, beard, hat colour, shirt colour and build; a fixed artwork set must say which choices it cannot represent. Themes cannot supply executable SVG, scripts or uncontrolled CSS. The full asset-set contract remains proposed in ADR 0065. Semantic avatar layers are available through ADR 0073.
Background work, observability and content¶
| Surface | Status | Contract |
|---|---|---|
| Scheduled jobs | Available | @module.job; daemon bounds, receipts and catch-up rules apply |
| Compiled contract jobs | Available | CompiledJobs; contracts are recompiled as jobs are listed |
| Event and polling triggers | Proposed | Persistent trigger data, staging and a bounded wake loop; not arbitrary background threads |
| Notifications | Available | Notifications policy decides delivery, deduplication and history; a plugin never bypasses it |
| Telemetry records | Available, limited | Read checked entries through TelemetryLog; writes use report operations; consent and sink policies still apply |
| Arbitrary telemetry streams | Proposed | A versioned metadata schema, retention, redaction and explicit consent before a team sink |
| Graph nodes and relationships | Available, limited | Team promises and recorded reads/writes feed the host graph; arbitrary plugin node renderers are proposed |
| Artifact discovery | Available | Playbook hints and declared capabilities; reads obey the contract's permissions |
| Skills and skill files | Available | Checked module.skill content; explicit exports and MCP representations; engine use is being built |
| Skill scripts and coding handoffs | Proposed | Bounded runner handoff, separate from loading reference text |
| Translations | Proposed | No general plugin translation-catalog API yet; supply plain labels and accessible fallback text |
| Generated plugin documentation pages | Available | hz system plugin docs build; the starter carries scoped page/site tooling |
| Documentation snippets | Available | Own scenes and recordings, built with the generated project’s documentation commands |
Local view packaging and independent work¶
The --preset contributions starter has a small static view, SDK types, status, navigation, a Today widget and an explicit
notification operation. It needs no JavaScript bundler. A richer plugin supplies its own frontend build and packages
the resulting ES module and assets in its wheel. Export current SDK definitions with
hz system plugin sdk <new-directory>. The host supplies one React runtime; bundles use the exported React bridges
and must not bundle another copy.
from pathlib import Path
module.view(
Path(__file__).parent / "views",
abi=1,
name="inspector",
source="read",
title="Harbor",
fallback="Run hz harbor read.",
slot="panel",
order=500,
)
The source is an owned read-only operation requiring no user arguments. Slots include panel, page, sidebar,
settings and background; a page owns /plugins/harbor/inspector, and a Settings view claims only its own module.
Common representations use status, action and widget declarations with explicit priorities. Views receive source
data, loading/visibility state and declared named actions through the host's ordinary confirmation gate. Missing or
incompatible bundles show the declared fallback while Python operations remain usable. No remote bundles or eval
are permitted. This is an interface boundary; installed JavaScript is trusted code in the host realm.
make preview uses isolated configuration/state. Restart its backend after rebuilding already-loaded view assets.
make check tests this plugin against an installed compatible core; make build packages only this plugin. The
public plugins repository adds per-plugin
hash-enforced locks, selected consumer checks, installed-wheel browser tests and independent release tags/provenance.
Its release command previews artifacts; publishing is explicit through the protected release workflow.
Shared interfaces without provider dependencies¶
A required injectable interface subclasses the public ServiceContract marker in the SDK or a neutral contract
package. A provider implements it and registers module.service(ReadingContract, Reading). A consumer imports only
ReadingContract, then declares a parameter of that type. If no implementation is installed or it is disabled, only
operations requiring it become unavailable, including dependencies through service factories. CLI/MCP/API still
build and unrelated operations work. Ordinary user data models are never guessed to be injected services.
For optional integration, inject Kernel, test kernel.provides(ReadingContract) and resolve it only when available.
Existing capability protocols use module.provides and declared requirements/bindings. Declare versioned
requires/provides identifiers in the owning project metadata to select affected consumer compatibility checks.
Quality checklist for a change¶
- Verify the declaration and public imports against the installed core version.
- Give every setting one obvious home, with plain labels and help; keep CLI, MCP, REST and app wording consistent.
- Test capability refusal, missing credentials, offline operation and a failing contribution without exposing secrets.
- Test keyboard access, reduced motion, narrow layouts and meaningful loading and empty states.
- Run the generated references and required checks; review the actual output in every affected head.
- Keep private plugins and their names out of public examples, documentation, commits and reviews.
The generated module catalog, CLI reference and configuration reference are the authority for the operations and fields present in a particular version. A proposed extension in this guide does not make an unavailable operation callable.