Diese Anleitung führt von „leeres Verzeichnis" bis „installierbar aus dem Hub".
Ein Plugin ist ein standalone Package, das als .zip hochgeladen wird — der
Server validiert das Manifest, prüft Peer-Dependencies und registriert es im
Katalog. Es gibt drei kinds, und sie unterscheiden sich nicht nur im
Manifest, sondern auch im Code-Contract:
| kind | Was es ist | activate() gibt zurück |
Beispiel |
|---|---|---|---|
agent |
Capability-Anbieter mit Toolkit (Zod-Tools) + System-Prompt | { toolkit, close } |
agent-seo-analyst |
integration |
reiner Credential-/HTTP-Client, kein Toolkit | (kein Toolkit; nur Secrets/Config-Container) | de.byte5.integration.odoo |
channel |
User-Surface (WhatsApp, Teams, Telegram, …) mit Transport + Adaptern | ChannelHandle über activate(ctx, core) |
@omadia/channel-whatsapp |
Quellen der Wahrheit (nicht halluzinieren):
- Agent: Referenz
middleware/packages/agent-seo-analyst/, Boilerplatemiddleware/assets/boilerplate/{agent-pure-llm,agent-integration}/, Package-Contract (10 Punkte)…/agent-pure-llm/CLAUDE.md.- Channel: SDK-Contract
middleware/packages/harness-channel-sdk/src/, öffentliches Referenz-Pluginbyte5ai/omadia-channel-whatsapp(Teams/Telegram-Quelle liegt privat). Resolvermiddleware/src/channels/dynamicChannelResolver.ts.- Runtime-Contract
middleware/packages/plugin-api/src/pluginContext.ts(das Package@omadia/plugin-api).- Manifest-Schema
docs/harness-platform/manifest-schema.v1.yaml(channel:-Block = Section 14).
- Node
>=20(das Repo pinnt die genaue Version in.nvmrc→nvm use). - Zugang zur Admin-UI (lokal
http://localhost:3333, sonst der Host der eigenen omadia-Instanz) für den lokalen Upload. - Zum Publishen auf den Hub: das
HUB_PUBLISH_TOKEN(Bearer-Token; liegt im Vercel-Env des Hub-Projekts bzw. lokal inhub/.env.local— write-only, nie im Chat/Log leaken). - Wenn dein Plugin eine Nicht-Host-Dependency braucht (z.B. ein Channel,
der
@whiskeysockets/baileysnutzt): zusätzlichesbuildals devDependency zum Bundlen (siehe §5).
Bei vagem „Plugin" zuerst klären — die drei kinds teilen Manifest-Schema + Zip-Flow, aber der Code-Contract ist verschieden:
agent— liefert Tools (Zod-Toolkit) + einen System-Prompt-Partial. Die Runtime macht SubAgent-Wrap, Tool-Bridge und Prompt-Concat. → §2–§4a.integration— nur Secrets/Config-Container, von demagents viadepends_onerben. Kein Toolkit.channel— User-Surface. Empfängt native Events, übersetzt sie in denIncomingTurn-Shape, fährt einen Orchestrator-Turn und rendert die Antwort zurück. Keinecapabilities/playbook/skills. → §4b.
Wähle agent-pure-llm (kein externes API, reines Prompting) oder
agent-integration (echter HTTP-Client + Secrets):
cp -R middleware/assets/boilerplate/agent-pure-llm \
middleware/packages/agent-<slug>
cd middleware/packages/agent-<slug>
mv skills/{{AGENT_SLUG}}-expert.md skills/<slug>-expert.mdPackage-Layout (flach, am Root):
agent-<slug>/
├── manifest.yaml # deklarative Definition (Schema v1) — Pflicht
├── package.json # name === manifest.identity.id, "type":"module", "private":true
├── tsconfig.json # NodeNext, rootDir:"./", outDir:"./dist"
├── types.ts # lokales PluginContext-Duplikat (KEIN Cross-Import!)
├── plugin.ts # activate(ctx) → { toolkit, close() }
├── toolkit.ts # Capability → ToolDescriptor[] (Zod-Input + run)
├── client.ts # externe API (LLM-frei, testbar) — nur agent-integration
├── index.ts # Barrel
├── skills/<slug>-expert.md # System-Prompt-Partial (YAML-Frontmatter)
├── assets/ # icon.png etc. (optional)
└── scripts/build-zip.mjs # tsc + stage + zip → out/<id>-<version>.zip
Ein Channel wird i.d.R. NICHT aus der Agent-Boilerplate gescaffoldet — es gibt (noch) keine Channel-Boilerplate. Nimm
byte5ai/omadia-channel-whatsappals Vorlage; das Layout steht in §4b.
Ersetze alle {{PLATZHALTER}}. Die identity-Felder + compat.core sind
das, was Hub und Katalog für die Listing-Kachel lesen — der Rest wird beim
Install voll validiert.
schema_version: "1"
identity:
id: "de.byte5.agent.<slug>" # === package.json "name". byte5-privat: reverse-DNS;
# public OSS: "@omadia/<slug>" (wie @omadia/plugin-office)
kind: "agent" # agent | integration | channel
domain: "<domain>" # z.B. coaching, m365.sharepoint, whatsapp — lowercase, dotted
name: "<Anzeigename>"
version: "0.1.0" # SemVer; === package.json "version"
description: # Sprachkarte (OM-50 / #885). Eine blanke
en: "<description EN>" # Zeichenkette wird als ENGLISCH gelesen —
de: "<Beschreibung DE>" # deutscher Text dort landet im en-Slot.
authors:
- name: "byte5 GmbH"
email: "info@omadia.ai"
license: "Proprietary" # oder MIT (public OSS)
icon: "./assets/icon.png" # PNG oder SVG
categories: ["<kategorie>"]
compat:
core: ">=1.0 <2.0"
node: ">=20"
multi_instance: true
privacy_class: "strict"
depends_on: [] # IDs von Parent-Integrations (Secret-Chain)
setup:
fields: [] # Setup-Felder → rendern automatisch im Install-Drawer
self_test: false
capabilities: [] # Tools liefert das toolkit; [] = pure-LLM (NICHT für channel)
skills:
- id: "<slug>_expert_system"
kind: "prompt_partial"
path: "skills/<slug>-expert.md"
description: "Rolle & Arbeitsweise."
permissions:
memory:
reads: ["session:*", "agent:de.byte5.agent.<slug>:*"]
writes: ["agent:de.byte5.agent.<slug>:*"]
network:
outbound: [] # erlaubte Hosts (leer = pure-LLM)Regeln, die sonst erst zur Install-/Ingest-Zeit knallen:
package.jsonname/versionmüssen exaktidentity.id/identity.versionspiegeln.- Shared/Host-Deps via
peerDependencies(nichtdependencies) — Ingest warnt sonst viapeers_missing. Eigene, nicht-Host-Deps werden gebundelt (siehe §5). setup.fieldsnur deklarieren, wenn der Parent (depends_on) sie nicht schon hat (sonst silent override).setup.fields[].type:string | url | secret | oauth | enum | boolean | integer | host_list.enumbrauchtenum: [{value,label}],oauthbrauchtprovider+scopes.string/secret-Felder könnenmultiline: truesetzen → Install-Drawer rendert eine Textarea (z.B. für PEM-Keys). Ältere Cores ignorieren das Flag (Fallback: Wert base64-encoden und im Plugin dekodieren).identity.descriptionist eine Sprachkarte (en:/de:), genau wiesetup.guideundsetup.fields[].label. Eine blanke Zeichenkette bleibt erlaubt, wird aber als englisch gelesen: deutscher Text an dieser Stelle landet imen-Slot und erscheint englischsprachigen Nutzern als Beschreibung (OM-50 / #885). Der Weg in die Oberfläche:manifestLoader.adaptManifestV1löstennachPlugin.descriptionauf (Suche, Hub, ältere Konsumenten) und hängt die vollständige Karte alsdescription_localizedan,routes/store.tsgibt sie weiter, undPluginCardbzw.store/[id]wählen daraus mitpickLocalizeddie Sprache des Nutzers. Fallback-Kette: angeforderte Sprache →en→de→ erster vorhandener Eintrag. Für die deutschen Texte gelten die Copy-Regeln des Projekts: keine Gedankenstriche, kein KI-Vokabular, ganze Sätze für Fachanwender statt Entwickler-Jargon.middleware/test/manifestDescriptionLocalized.test.tshält das für alle mitgelieferten Plugins fest.
activate(ctx)gibt fix zurück:{ toolkit: { tools: ToolDescriptor[]; close() }, close() }.ToolDescriptor = { id, description, input: ZodSchema, run(input) }.- Kein eigener LLM-Client, keine eigene Tool-Loop — die Runtime macht
SubAgent-Wrap, Tool-Bridge, Tool-Name-Derivation (
…agent.<slug>→query_<slug>) und System-Prompt-Concat ausskills/*.md. - Secrets/Config nur über
ctx:await ctx.secrets.get(k)/.require(k)(async),ctx.config.get<T>(k)(sync). - System-Prompt gehört in
skills/<slug>-expert.md(Markdown + YAML-Frontmatter), nicht in den Code.
Details + Zod-Support-Matrix + Lifecycle-Budgets (activate ≤10s, close ≤5s):
die 10 Punkte in middleware/assets/boilerplate/agent-pure-llm/CLAUDE.md.
Kanonische Vorlage: middleware/packages/agent-seo-analyst/.
npm install
npm run lint:fix && npm run typecheck # nicht bauen — nur prüfenEin Channel ist ein Plattform-Adapter: er hält Transport (WebSocket /
Webhook / Long-Poll) offen, übersetzt native Events in IncomingTurn, fährt den
Orchestrator-Turn und rendert die Antwort zurück. Quelle: @omadia/channel-sdk
(middleware/packages/harness-channel-sdk/src/). Öffentliche Referenz:
byte5ai/omadia-channel-whatsapp.
Export-Contract (≠ agent!). Der Kernel-Resolver
(middleware/src/channels/dynamicChannelResolver.ts → pickChannelPlugin)
importiert dist/plugin.js und akzeptiert drei Export-Shapes, in dieser
Priorität:
export async function activate(ctx, core)— bare function. ← bevorzugtexport default { activate(ctx, core) {…} }— Default-Objekt mit Methode.export default <ChannelPlugin-Instanz>.
Es gibt keinen Konstruktor mit Deps — alles kommt über ctx (PluginContext)
und core (CoreApi):
import {
getChatAgent, // SDK-Helper: löst den Orchestrator auf
isNoReply,
type CoreApi, type ChannelHandle, type IncomingTurn,
} from '@omadia/channel-sdk';
import type { PluginContext } from '@omadia/plugin-api';
export async function activate(ctx: PluginContext, core: CoreApi): Promise<ChannelHandle> {
// 1. Transport öffnen (NICHT awaiten bis "verbunden" — activate-Budget = 10s).
// 2. Auf inbound Events: IncomingTurn bauen → Turn fahren → rendern → zurücksenden.
// 3. Optionale Admin-/Status-UI via ctx.routes.register (siehe unten).
return { async close() { /* Sockets/Timer freigeben (≤5s) */ } };
}Einen Turn fahren — zwei Wege:
// (a) Gefaltete Antwort (ein await, kein Event-Loop) — am einfachsten:
const agent = getChatAgent(ctx); // ← SDK-Helper, Typ ChatAgent | undefined
if (!agent) throw new Error('orchestrator unavailable');
const answer = await agent.chat({ userMessage: turn.text, sessionScope, userId });
// answer: SemanticAnswer { text, interactive?, attachments?, followUps?, disclaimer? }
// (b) Live-Event-Stream (für Channels mit Tipp-/Tool-Trace-Anzeige):
for await (const ev of core.handleTurnStream(turn)) { /* ev: ChatStreamEvent */ }
core.handleTurnStream(turn)ist seit der Orchestrator-Verkabelung real an den aktivenchatAgentgebunden (middleware/src/index.ts→orchestratorDispatcher). Vorher war der Dispatcher ein Stub, der Turns still verschluckte (Logstub dispatcher: turn ignored) — wer darüber fuhr, bekam keine Antwort. Für simple Frage→Antwort genügtgetChatAgent(ctx).chat(...)(gibt direkt eineSemanticAnswer).
core (CoreApi) — was der Channel sonst auf dem Kernel aufruft:
handleTurnStream(turn): AsyncIterable<ChatStreamEvent>(siehe oben),registerRoute/registerRouter— channel-scoped Express-Routen (auto-503 bei deactivate),resolveIdentity(ref),log(level, msg, ctx?).
Inbound → Turn. IncomingTurn = { channelId, conversationId, userRef:{ kind, id, displayName? }, text, attachments?, metadata?, rawEvent? }. userRef.kind
ist ein geschlossener Union — WhatsApp = 'whatsapp-phone', Telegram =
'telegram-chat', Teams = 'teams-aad', Slack/Discord/custom. isNoReply()
filtert die NO_REPLY-Sentinel; SemanticAnswer rendert der Channel native.
Manifest-Besonderheiten (channel). Keine capabilities/playbook/skills.
Dafür der channel:-Block (Schema Section 14):
identity:
kind: "channel"
lifecycle:
entry: "dist/plugin.js" # exportiert activate(ctx, core)
admin_ui_path: "/api/<slug>/admin/index.html" # Top-Level → web-ui rendert iframe
channel:
transport:
kind: "websocket" # webhook | websocket | long-poll
routes: [] # nur für kind=webhook nötig
capabilities: ["text", "typing_indicator"] # text|attachments|interactive_cards|user_sso|…
adapters: ["text", "markdown"] # text|markdown|adaptive_card|telegram_keyboard|…Admin-UI / Auth-Surface (z.B. QR-Code). Express-Router mit
ctx.routes.register('/api/<slug>/admin', router) mounten (static index.html
- JSON-API), im Manifest
admin_ui_pathauf…/index.htmlzeigen → web-ui rendert automatisch ein<iframe src="/bot-api{admin_ui_path}">auf der Store-Detail-Page. Harte Regeln (siehemiddleware/assets/boilerplate/agent-integration/assets/admin-ui/CLAUDE.md):fetch()relativ (api/status), jede Antwort{ ok, … }, Stylesheet/bot-api/_harness/admin-ui.css, nurvar(--*)-Tokens, keine externen Scripts/Fonts.
WhatsApp/Baileys-Lehren (aus channel-whatsapp, falls relevant):
- Auth-State über
ctx.memorypersistieren (überlebt Restart) →permissions.memory.{reads,writes}deklarieren. - WhatsApp adressiert Chats teils per LID (
…@lid), nicht per Telefonnummer. Self-Chat-Erkennung gegensock.user.lid(eigene LID) UND die PN matchen. Allowlist gegensenderPn/participantPn(nicht die LID-Ziffern). fromMe:true-Nachrichten NICHT pauschal droppen (sonst antwortet ein same-account-Bot nie im Self-Chat) — nur die eigenen Replies via Sent-ID-Set ausschließen, um Loops zu vermeiden.
Der Host installiert KEINE Plugin-Dependencies. Ein hochgeladenes Package
wird unter <packagesDir>/<id>/<version>/ entpackt (kein npm install,
middleware/src/plugins/packageUploadService.ts) und zur Laufzeit per
dynamischem import() in-process geladen. Bare Specifier (import x from 'foo') werden gegen das Host-node_modules aufgelöst — ein Symlink
<packagesDir>/node_modules → <host>/node_modules
(uploadedPackageStore.ensureHostNodeModulesLink) bridged das.
Folge — zwei disjunkte Klassen von Deps:
| Klasse | Wohin in package.json |
Beim Build |
|---|---|---|
Host-bereitgestellt (@omadia/*, express, zod, …) |
peerDependencies |
external lassen (NICHT bundeln) |
Eigene, nicht-Host (@whiskeysockets/baileys, qrcode, …) |
dependencies |
in dist/ bundeln |
Ein Plugin, das eine Dep importiert, die der Host nicht schon hat, crasht
zur Laufzeit mit Cannot find package 'X' — tsc allein reicht dann nicht,
weil tsc nur transpiliert, nicht bündelt. Lösung: ein esbuild-Bundle-Step im
Build (siehe omadia-channel-whatsapp/scripts/build-zip.mjs):
import { build } from 'esbuild';
await build({
entryPoints: ['src/plugin.ts'],
outfile: 'dist/plugin.js',
bundle: true, platform: 'node', format: 'esm', target: 'node20',
// Host-Peers NICHT inlinen; alles andere (Baileys, qrcode, …) wird gebundelt:
external: ['@omadia/channel-sdk', '@omadia/plugin-api', 'express'],
// ESM-Banner, damit gebundelter CJS-Code require/__dirname hat:
banner: { js: "import{createRequire}from'node:module';const require=createRequire(import.meta.url);" },
});Hinweise:
- Type-only Imports (
import type { … }) verschwinden im Bundle von selbst —@omadia/plugin-apitaucht z.B. gar nicht als Runtime-Import auf, wenn nur Typen daraus genutzt werden. typecheckläuft separat (tsc --noEmit). Für ein standalone-Repo, das nicht neben dem Core-Checkout liegt, die@omadia/*-Typen viatsconfig.pathsauf die gebauten.d.tsmappen (siehe channel-whatsapptsconfig.json).- Peer-
@omadia/*sind nicht auf npm →npm installmitlegacy-peer-deps=true(.npmrc), sonst 404 beim Peer-Auto-Install.
node scripts/build-zip.mjs
# agent (tsc): ▶ tsc … ✓ built out/de.byte5.agent.<slug>-0.1.0.zip
# channel (esbuild): ▶ esbuild bundle … ✓ built out/omadia-channel-whatsapp-0.1.0.zipDas Script: kompiliert/bundelt nach dist/, kopiert Runtime-Artefakte
(manifest.yaml, package.json, README.md, dist/, skills/, assets/,
LICENSE) nach out/<id>-<version>-package/, verifiziert dass der dist/-Entry
existiert, und zippt nach out/<id>-<version>.zip. Im ZIP sind nur
Runtime-Artefakte — keine *.ts-Quellen außerhalb dist/, kein
node_modules, kein .env. Der safeName strippt @ und ersetzt / durch -
(@omadia/channel-whatsapp → omadia-channel-whatsapp).
Extension-Allowlist im Host-Extractor: .yaml .md .json .js .mjs .cjs .map .png .svg .jpg .txt + LICENSE / README / NOTICE. Ein gebundeltes dist/plugin.js
(auch mehrere MB) ist nur eine .js-Datei — passt.
In der Admin-UI: Store → Tab „Lokal" → Upload-Dropzone → out/<id>-<version>.zip
hineinziehen. Der Server validiert das Manifest und registriert das Package im
Katalog; es erscheint als Verfügbar im Lokal-Tab. Ein Klick auf die Kachel →
Detailseite → Jetzt installieren (Setup-Felder rendern automatisch aus
setup.fields; bei Channels öffnet sich nach Install die Admin-UI/QR-Sektion).
Äquivalent per Admin-API (alle JWT-gated):
POST /api/v1/install/plugins/:id → POST /api/v1/install/jobs/:id/configure.
Der Hub (hub.omadia.ai) ist eine dumme Registry: ein Publish schreibt nur
Artefakt + index.json um, kein Redeploy. Der nächste Index-Read enthält das
Plugin. Versionen sind immutable. Contract: hub/app/api/publish/route.ts
(Service-Seite) ↔ middleware/src/plugins/registryClient.ts (Core-Konsum).
Schritt für Schritt:
# 1. Token bereitstellen — NICHT echoen. Aus dem Hub-Env lesen:
export HUB_PUBLISH_TOKEN="$(grep -E '^HUB_PUBLISH_TOKEN=' hub/.env.local | cut -d= -f2-)"
export HUB=https://hub.omadia.ai
export ZIP=out/omadia-channel-whatsapp-0.1.0.zip
# 2. Publish (multipart):
curl -sS -X POST "$HUB/api/publish" \
-H "Authorization: Bearer $HUB_PUBLISH_TOKEN" \
-F "file=@${ZIP}"
# → 201 { "ok": true, "id": "@omadia/channel-whatsapp", "kind": "channel",
# "storage": "blob", "version": { "version": "0.1.0", "sha256": "…", … } }
# 3. Verifizieren, dass der Index das Plugin trägt:
curl -sS "$HUB/registry/index.json" | jq '.plugins[].id'- Auth:
Authorization: Bearer <HUB_PUBLISH_TOKEN>(timing-safe geprüft). Fehlt/falsch → 401publish.unauthorized; Hub ohne Token konfiguriert → 503publish.disabled. - Body: multipart
file=<zip>oder roherContent-Type: application/zip. - Max 50 MiB (= Core's Artefakt-Cap) → sonst 413
publish.too_large. - Immutable: Re-publish derselben
(id, version)→ 409publish.version_exists. Zum Überschreiben (nur dev):?overwrite=true. Sonst: Version inmanifest.yaml+package.jsonbumpen, neu bauen, neu publishen. - Der Hub extrahiert
manifest.yaml+package.json, validiert leicht (schema_version: "1",identity.*, gültigerkind), rechnet sha256 über die exakten Upload-Bytes und legt/aktualisiert den Index-Eintrag.
Prod-Token (CI/headless): der echte
HUB_PUBLISH_TOKENliegt im Vercel-Env desomadia-hub-Projekts (dashub/.env.localenthält nur den lokalen Dev-Token fürlocalhost:3100). Ziehen viacd hub && vercel env pull <tmp> --environment=production --yes, Wert rausgreifen, danach Tmp-File löschen — nie echoen.
Artefakt-URL (host-gepinnt, wird beim Read auf HUB_PUBLIC_URL umgeschrieben):
$HUB/registry/<id>/<version>/plugin.zip.
Zwei Pakete unter middleware/packages/ werden zusätzlich auf den Hub
publiziert: @omadia/plugin-office und @omadia/plugin-web-search. Ihr ZIP
entsteht ausschließlich über das gemeinsame, geguardete Script
middleware/scripts/build-plugin-zip.mjs — nie per Hand gezippt. Hintergrund
(#1075): office 0.1.2 wurde aus einem nie committeten Working-Tree publiziert,
der Hub trug danach einen Setup-Guide, den das Repo nicht hatte.
cd middleware
npm ci && npm run build # Peers (plugin-api, …) brauchen dist/
npm run package -w @omadia/plugin-office # → <repo>/out/omadia-plugin-office-<version>.zip
npm run package -w @omadia/plugin-web-searchDas Script bricht hart ab, wenn manifest.yaml identity.version/identity.id
nicht mit package.json version/name übereinstimmt, wenn unter dem
Paketordner irgendetwas uncommitted oder untracked ist — auch gitignorierte
Dateien außer Build-Output (dist/, node_modules/, *.tsbuildinfo), denn die
Root-.gitignore ignoriert tmp/, build/, logs/ überall und tsc würde
src/tmp/*.ts trotzdem kompilieren —, wenn HEAD auf keinem Remote-Tracking-Ref
liegt (Dry-Run nur mit --allow-unpushed-commit, Ausgabe dann als
„NOT PUBLISHABLE" markiert), wenn nach dem frischen Build (dist/ +
*.tsbuildinfo gelöscht, dann npm run build) lifecycle.entry fehlt oder
nicht im Archiv landet, oder wenn unter dist/ ein Symlink liegt. Das ZIP ist
flach (manifest.yaml, package.json, dist/) und byte-reproduzierbar; die
Ausgabe nennt Commit-SHA und sha256.
middleware/test/pluginPackageVersions.test.ts hält Manifest, package.json
und den Lockfile-Workspace-Eintrag jedes Pakets mit manifest.yaml im CI
synchron.
Vor dem Publish:
latest_versionaus$HUB/registry/index.jsonlesen — nur eine höhere Version publizieren. Eine höhere Nummer beweist keinen neueren Inhalt: bei Zweifel das Live-ZIP herunterladen und gegen den Build diffen.- Vom Merge-Commit auf
mainbauen, nicht von einem Feature-Branch. - Ein Plugin nach dem anderen publizieren, nie
?overwrite=true, danachindex.jsonpollen (die Blob-Liste ist eventually consistent).
Bundled-IDs: Beide IDs sind auf einem Standard-Kernel mitgeliefert. Ein Hub-Install derselben ID läuft durch
PackageUploadService.ingestund wird dort mitpackage.id_conflict_bundledabgelehnt (#789), außer die Middleware läuft mitPLUGIN_ALLOW_BUNDLED_ID_OVERRIDE=1. Das ZIP ist außerdem nicht eigenständig lauffähig: office bündelt seine Runtime-dependencies(docx,exceljs,jszip) nicht, sie müssen im Host-node_modulesliegen.Toter Update-Badge: Die Update-Erkennung im Store (
routes/store.ts, Detailseite undenrichWithRegistry) vergleicht die Hub-latest_versionmit der gespeicherteninstalled_versionund nimmt Bundled-IDs nicht aus. Jede höhere Hub-Version zeigt auf Kernels mit ältererinstalled_versionalso „Update verfügbar", der Klick endet in 422package.id_conflict_bundled. Für office besteht das schon (Hub 0.1.2 > installiert 0.1.1); ein Publish von web-search 0.2.0 erzeugt es für web-search neu. Vor diesem Publish die Update-Erkennung für Bundled-IDs abschalten oder den Badge bewusst in Kauf nehmen.
In der Admin-UI ist die Default-Registry hub.omadia.ai bereits geseedet
(verwalten unter Admin → Registries). Store → Tab „Hub" zieht
index.json und zeigt dein Plugin als Verfügbar mit dem Badge
Hub · <registry>. „Jetzt installieren" lädt das ZIP, prüft sha256, ingestet es
lokal und startet dann den normalen Install-Job.
Zwei harte Constraints (aus dem Core-Client — sonst schlägt der Install fehl):
- Die ZIP-Route streamt das Artefakt (kein 302-Redirect) — der Client
fetcht mit
redirect: 'error'. - Der
download_url-Host muss == registrierter Registry-Host sein (Host-Pinning) — nie eine Blob-/*.vercel.app-URL.
| Symptom | Ursache / Fix |
|---|---|
Channel aktiviert nicht / activate is not a function |
Falscher Export-Shape. dist/plugin.js muss export async function activate(ctx, core) (oder export default {activate}) liefern — siehe §4b. |
| Channel ackt Nachrichten, antwortet aber nie | Turn wird ins Leere gefahren. Über getChatAgent(ctx).chat(...) ODER core.handleTurnStream(turn) fahren (beides an den aktiven Orchestrator gebunden) — und sicherstellen, dass das Orchestrator-Plugin aktiv ist (anthropic_api_key gesetzt). |
Plugin crasht zur Laufzeit mit Cannot find package 'X' |
X ist nicht im Host-node_modules und wurde nicht gebundelt. Entweder X in dist/ bundeln (esbuild, §5) oder — wenn host-bereitgestellt — als peerDependencies deklarieren. |
Ingest warnt peers_missing |
Eine deklarierte Peer-Dep fehlt im Host. Host-Dep ergänzen, oder (für eigene Deps) auf dependencies + Bundle umstellen. |
npm install schlägt mit 404 auf @omadia/* fehl |
Peers sind privat/nicht-npm. .npmrc mit legacy-peer-deps=true; @omadia/*-Typen via tsconfig.paths resolven. |
| Plugin taucht nicht im Hub-Tab auf, obwohl publiziert | Eine lokale Kopie gleicher id ist installiert → Merge bevorzugt lokal (local-wins). Built-ins (z.B. @omadia/plugin-office) sind deshalb nie im Hub-Tab — zum Test ein Nicht-Built-in publishen. Ein Hub-Install einer Built-in-ID scheitert mit package.id_conflict_bundled (#789, Override nur via PLUGIN_ALLOW_BUNDLED_ID_OVERRIDE=1), siehe §8 „In-tree-Pakete". |
409 publish.version_exists |
Version existiert (immutable). Version bumpen oder ?overwrite=true (dev). |
401 publish.unauthorized / 503 publish.disabled |
HUB_PUBLISH_TOKEN falsch/fehlt bzw. im Hub-Env nicht gesetzt. |
| Admin-UI/QR lädt nicht im Store-iframe | fetch() war absolut statt relativ, oder Response ohne { ok }, oder admin_ui_path zeigt nicht auf …/index.html. Siehe §4b + admin-ui CLAUDE.md. |
Install scheitert mit install.missing_capability |
requires: im Manifest hat keinen aktiven Provider → der Install-Wizard zeigt die zu installierende Chain. |
- Package-Contract Agent (10 Punkte):
middleware/assets/boilerplate/agent-pure-llm/CLAUDE.md - Admin-UI-Constraints:
middleware/assets/boilerplate/agent-integration/assets/admin-ui/CLAUDE.md - Kanonischer Agent:
middleware/packages/agent-seo-analyst/ - Channel-SDK:
middleware/packages/harness-channel-sdk/src/(@omadia/channel-sdk) — inkl.getChatAgent(ctx) - API-Key-Auth (server-to-server Bearer statt Session-Cookie):
middleware/packages/harness-api-key-auth/src/(@omadia/api-key-auth) —requireApiKey(...)als mountbare Express-Middleware, inkl. Key-Store, Scopes, Rate-Limit, Audit-Log - Öffentliches Channel-Referenz-Plugin:
byte5ai/omadia-channel-whatsapp - Runtime-Contract:
middleware/packages/plugin-api/src/pluginContext.ts(@omadia/plugin-api) - Manifest-Schema (inkl.
channel:-Block §14):docs/harness-platform/manifest-schema.v1.yaml - Channel-Resolver (Export-Shapes):
middleware/src/channels/dynamicChannelResolver.ts - Turn-Dispatcher (CoreApi → Orchestrator):
middleware/src/channels/coreApi.ts+middleware/src/index.ts - Dep-Resolution / Symlink-Bridge:
middleware/src/plugins/{packageUploadService,uploadedPackageStore}.ts - Hub-Publish-Route (Service):
hub/app/api/publish/route.ts