{
  "name": "Aiopta Addon Marketplace",
  "generatedAt": "2026-08-05T14:15:16.379Z",
  "registry": "Aiopta/aiopta-addons",
  "pricingTiersSchemaVersion": 1,
  "count": 71,
  "addons": [
    {
      "id": "accounting",
      "uuid": "4891152c-ab52-479f-aea5-68fd61faf29d",
      "name": "Accounting (Einnahmen/Ausgaben/Belege/USt/DATEV)",
      "version": "1.2.0",
      "description": "Per-User Buchhaltung: Einnahmen & Ausgaben, Belege, USt-Voranmeldung und DATEV-CSV-Export. Integriert lesend mit dem payments-Addon (bezahlte Rechnungen fließen als steuerpflichtige Erlöse in die USt ein, ohne payments zu verändern). Lokale SQLite ist die Quelle der Wahrheit (ADR-054).",
      "author": "Aiopta",
      "icon": "🧾",
      "capabilities": [
        "accounting",
        "income",
        "expenses",
        "receipts",
        "vat",
        "reports",
        "datev-export"
      ],
      "tags": [],
      "download": "/artifacts/accounting-1.2.0.zip",
      "sha256": "2aea54f69c10f8465821a7b56a1dbec2db08a8d2a19381d3934f4861747bc0d3",
      "size": 150557,
      "page": {
        "id": "accounting-page",
        "path": "/addons/accounting",
        "name": "Accounting",
        "icon": "🧾",
        "section": "Money",
        "component": "@addons/accounting/src/components/AccountingPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/accounting/transactions",
        "/api/v1/addons/accounting/transactions/:id",
        "/api/v1/addons/accounting/receipts",
        "/api/v1/addons/accounting/reports/summary",
        "/api/v1/addons/accounting/vat",
        "/api/v1/addons/accounting/datev"
      ],
      "skills": [
        {
          "name": "accounting-read",
          "displayName": "Accounting: Buchungen, Belege & USt lesen",
          "description": "Listet Einnahmen/Ausgaben-Buchungen, Belege, die Monatsübersicht und die USt-Voranmeldung für den Nutzer. Reiner Lesezugriff — bucht nichts.",
          "permissions": [
            "addons/accounting/transactions",
            "addons/accounting/receipts",
            "addons/accounting/reports",
            "addons/accounting/vat"
          ],
          "endpoint": "/api/v1/addons/accounting/reports/summary",
          "scripts": []
        },
        {
          "name": "accounting-book",
          "displayName": "Accounting: Buchungen & Belege erfassen",
          "description": "Erfasst Einnahmen-/Ausgaben-Buchungen und Belege und löscht Buchungen für den Nutzer (per-User gebucht).",
          "permissions": [
            "addons/accounting/transactions",
            "addons/accounting/receipts"
          ],
          "endpoint": "/api/v1/addons/accounting/transactions",
          "scripts": []
        },
        {
          "name": "accounting-export",
          "displayName": "Accounting: DATEV-CSV-Export",
          "description": "Erzeugt einen DATEV-CSV-Buchungsstapel (Jahr) aus den Buchungen des Nutzers.",
          "permissions": [
            "addons/accounting/datev"
          ],
          "endpoint": "/api/v1/addons/accounting/datev",
          "scripts": []
        }
      ],
      "mcpServers": [
        {
          "id": "accounting-tools",
          "displayName": "Accounting tools",
          "description": "Read-only accounting math for agents: German VAT (USt/Vorsteuer) report aggregation, gross/net conversion and money formatting — the same pure functions the accounting addon uses for its VAT report and DATEV export (issue #400, epic #399). Books nothing.",
          "transport": "stdio",
          "runtime": "node",
          "entry": "accounting-tools/server.mjs",
          "permissions": [
            "addons/accounting/reports"
          ],
          "toolPolicy": {
            "mode": "read-only",
            "allowDestructive": false
          },
          "configRefs": [],
          "credentialRefs": [],
          "archiveEntry": "accounting/mcp/servers/accounting-tools/server.mjs",
          "sha256": "75405d9c7ab616072de7de7abe8c08132361ad8e31039872d2df5573def66134",
          "size": 18789,
          "descriptorVersion": 1
        }
      ],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>🧾 Accounting (Einnahmen/Ausgaben/Belege/USt/DATEV)</h1>\n<p>Per-User Buchhaltung: Einnahmen &amp; Ausgaben, Belege, USt-Voranmeldung und DATEV-CSV-Export. Integriert lesend mit dem payments-Addon (bezahlte Rechnungen fließen als steuerpflichtige Erlöse in die USt ein, ohne payments zu verändern). Lokale SQLite ist die Quelle der Wahrheit (ADR-054).</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>accounting</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> accounting, income, expenses, receipts, vat, reports, datev-export</li><li><strong>UI-Seite:</strong> <code>/addons/accounting</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| POST | <code>/api/v1/addons/accounting/datev</code> |<br />| GET, POST | <code>/api/v1/addons/accounting/receipts</code> |<br />| GET | <code>/api/v1/addons/accounting/reports</code> |<br />| GET, POST | <code>/api/v1/addons/accounting/transactions</code> |<br />| DELETE | <code>/api/v1/addons/accounting/transactions/[id]</code> |<br />| GET | <code>/api/v1/addons/accounting/vat</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<ul><li>Keine bekannten Einschränkungen.</li></ul>\n<blockquote><p>README aus <code>addon.json</code> + vorhandenen Routen generiert (<code>scripts/generate-addon-readmes.js</code>). Bei manuellen Ergänzungen den Generator-Hinweis entfernen.</p></blockquote>",
          "byteSize": 1197
        }
      }
    },
    {
      "id": "aiopta-base-skill",
      "uuid": "ebbb02af-670b-49a4-811b-9990e9da2420",
      "name": "Aiopta Basisskill",
      "version": "1.2.1",
      "description": "Versioned, secret-free meta-skill that every newly created agent receives across UI, API, Template, Onboarding and Import/Seed creation paths. Single canonical source of truth for auth API, error handling, permission request and MCP tool usage — never re-implemented per controller (issues #105, #398).",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "agent-skills",
        "base-skill",
        "meta-skill"
      ],
      "tags": [],
      "download": "/artifacts/aiopta-base-skill-1.2.1.zip",
      "sha256": "9dd2785c1142937759e7823758ec8ec20277db3d45c420d65e8f5ecf90e29178",
      "size": 25355,
      "page": null,
      "deprecated": false,
      "endpoints": [],
      "skills": [
        {
          "name": "aiopta-base",
          "displayName": "Aiopta Base Skill (meta)",
          "description": "Versioned, secret-free meta-skill: describes how the agent authenticates against the Aiopta API, how it reports errors, how it requests permissions and how it discovers and uses MCP tools — both addon-delivered servers and the three core-MCP surfaces (read-only, mutating, and the dedicated Tier-3 surface for sensitive/destructive tools) — including the permission-denied contract. Reused across UI / API / Template / Onboarding / Import / Seed creation paths; existing agents are migrated idempotently to the latest version (issues #105, #398). Meta-skill — declares no executable scripts and no addon-specific endpoint.",
          "permissions": [],
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Aiopta Base Skill (issues #105/#398)</h1>\n<p>This addon is the <strong>single versioned source of truth</strong> for the<br /><em>Aiopta Basisskill</em> — the secret-free meta-skill that every newly created<br />agent receives across <strong>UI / API / Template / Onboarding / Import / Seed</strong><br />creation paths.</p>\n<p>| Property | Value |<br />|----------|-------|<br />| Addon id | <code>aiopta-base-skill</code> |<br />| Skill name | <code>aiopta-base</code> |<br />| Current version | 1.2.0 |<br />| Skill scripts | none (meta-skill, not a capability) |<br />| Endpoint | none (no runtime, doc-only descriptor) |<br />| Secrets / tokens | <strong>none</strong> — explicitly forbidden by the validator |<br />| Hard-coded URLs | <strong>none</strong> — agent fills the instance URL from its own config |</p>\n<h2>Why this addon exists</h2>\n<p>Without a canonical base skill, each agent-creation controller (UI form,<br /><code>POST /api/v1/agents</code>, template clone, onboarding wizard, seed import)<br />would have to hand-roll its own auth/error/permission boilerplate. That<br />guarantees drift — one controller forgets to forward the bearer token,<br />another forgets to translate a <code>429</code> into a retry, and the agents stop<br />behaving consistently.</p>\n<p>The base skill ships the <strong>description</strong> of how an agent authenticates,<br />handles errors, asks for permissions and uses MCP tools. The agent (a<br />separate runtime,<br />out of scope for this addon) consumes that description and renders it<br />into the same prompt / system-message across every creation path.</p>\n<p>Existing agents are migrated <strong>idempotently</strong>: the migration script<br />(<code>scripts/migrate-base-skill.cjs</code>) reads the addon catalog, compares<br />the on-file version against the version each agent was provisioned<br />with, and either upgrades the reference or leaves the agent untouched<br />(user customisations are never overwritten — see the<br />Migration section below).</p>\n<h2>What is in the skill</h2>\n<p>The single <code>skills[]</code> entry on this addon (<code>name: aiopta-base</code>) carries<br />the documented behaviour. It is a <strong>descriptor only</strong> — no scripts, no<br />endpoint, no runtime. The descriptor is consumed by core&#39;s<br /><code>src/lib/agents/provision.ts</code> (linked Core-PR, out of scope for this<br />addon repo).</p>\n<p>Four things the descriptor specifies (see <code>SKILL.md</code> for the full text<br />the agent actually receives):</p>\n<p>1. <strong>Auth API</strong> — every agent calls the Aiopta API with<br />   <code>Authorization: Bearer &lt;its own key&gt;</code>, never with an addon secret.<br />   The key is per-agent and provisioned by the canonical<br />   <code>createAgent(...)</code> helper on the core side, not by this addon.<br />2. <strong>Error handling</strong> — <code>4xx</code> is reported verbatim (status + body);<br />   <code>429</code> triggers a single retry with exponential backoff (max 3);<br />   <code>5xx</code> is surfaced to the user with the request trace id; no silent<br />   fallback that hides a permanent failure.<br />3. <strong>Permission request</strong> — the agent enumerates the permissions it<br />   needs <em>before</em> the first call, asks the user once, and stores the<br />   answer in the agent&#39;s permission record. Per-call <code>403</code> is treated<br />   as &quot;permission missing → request it&quot;, never as a fatal error.<br />4. <strong>MCP tools</strong> (issue #398) — the agent uses only the MCP tools that<br />   core projects into its tool registry (capability-filtered,<br />   aiopta-core#417). This covers both addon-delivered servers and the<br />   three shipped <strong>core-MCP</strong> surfaces (aiopta-core#467/#468/#480,<br />   docs/CORE-MCP.md): <code>mcp:core:read:*</code> (15 read-only tools),<br />   <code>mcp:core:write:*</code> (11 mutating tools) and the dedicated Tier-3<br />   surface <code>mcp:core:tier3:*</code> (4 sensitive/destructive tools). §4.4 of<br />   <code>SKILL.md</code> documents the surface, that destructive/Tier-3 tools need an<br />   exact <code>core/…</code> grant (no wildcard), and that the permission-denied<br />   contract for core tools equals §4.3 — a <code>denied</code> core call is never<br />   retried, escalated, or side-stepped via the raw route; the agent<br />   informs the user once and stops.</p>\n<h2>Migration</h2>\n<pre><code class=\"lang-bash\"># Verify (default — read-only, exits non-zero if invariants violated)\n./scripts/migrate-base-skill.sh --check</code></pre>\n<p># Apply (idempotent — second run reports &quot;no change&quot;)<br />./scripts/migrate-base-skill.sh --apply<br />```</p>\n<p>The script:</p>\n<p>1. Reads <code>addons/aiopta-base-skill/addon.json</code> and confirms the skill<br />   shape (one skill, no scripts, no endpoint, secret-free).<br />2. Reads <code>SKILL.md</code> and runs the secret / URL / host literal guard.<br />3. Reads <code>dist/marketplace/catalog.json</code> (if present) and confirms the<br />   base skill is discoverable under id <code>aiopta-base-skill</code>.<br />4. Writes the current &quot;applied&quot; state to<br />   <code>dist/migrate-base-skill.json</code>. A subsequent run with the same<br />   inputs is a no-op.</p>\n<p>On the <strong>addons repo</strong> the migration is purely catalog-side: the source<br />of truth for &quot;which base-skill version is current&quot; is this addon&#39;s<br /><code>addon.json</code>. On the <strong>core repo</strong> (out of scope) the same script is<br />invoked from <code>src/lib/agents/provision.ts</code> to backfill existing agents.</p>\n<h2>Provenance / version visibility</h2>\n<p>The catalog entry for this addon (built by <code>npm run build:catalog</code>)<br />exposes the version under <code>addons[].version</code>. The core API surfaces<br />the same version per agent under<br /><code>GET /api/v1/agents/:id → skills[][].addonVersion</code>, so an operator can<br />always tell which base-skill a given agent is running.</p>\n<h2>Hard negatives</h2>\n<p>The base skill must NEVER contain:</p>\n<p>- API keys, bearer tokens, OAuth refresh tokens, basic-auth credentials<br />  of any shape (the validator scans the descriptor body for these).<br />- Hard-coded instance URLs (the agent fills the URL from its own<br />  config; baking one in would tie every agent to a single deployment).<br />- Hard-coded host names or IPs (same reason).<br />- Addon-specific skills (the base skill is meta — it points the agent<br />  at the per-addon skill, it does not embed one).<br />- Duplicates — there is exactly one base skill version active at any<br />  time; older versions are upgraded, not stacked.</p>",
          "byteSize": 5686
        },
        "changelog": {
          "filename": "CHANGELOG.md",
          "sourcePath": "CHANGELOG.md",
          "rendered": "<h1>Changelog — aiopta-base-skill</h1>\n<p>All notable changes to the Aiopta Basisskill are documented here. The base<br />skill follows semantic versioning: a <code>MAJOR</code> bump means every agent must be<br />re-provisioned (auth API or permission model changed), a <code>MINOR</code> bump means<br />advisable but non-breaking (a new §3 rule, a new error class), a <code>PATCH</code> bump<br />means documentation / typo fixes only.</p>\n<h2>1.2.0 — 2026-07-07 (issue #398)</h2>\n<p>MINOR — advisable but non-breaking. Existing agents are upgraded idempotently<br />via <code>scripts/migrate-base-skill.sh --apply</code> (the #105 hook); user<br />customisations are never overwritten.</p>\n<p>- §4 now documents the <strong>concrete</strong> core-MCP tool surface (the generic §4<br />  from 1.1.0 said &quot;once it ships&quot; — it has now shipped: aiopta-core<br />  #467/#468/#480 are merged, docs/CORE-MCP.md + ADR-075). New §4.4<br />  &quot;Core-MCP tools (Aiopta itself, shipped)&quot;:<br />  - The three core surfaces and their tool-id namespaces —<br />    <code>mcp:core:read:*</code> (15 read-only tools, #467), <code>mcp:core:write:*</code><br />    (11 mutating tools, #468) and the dedicated Tier-3 surface<br />    <code>mcp:core:tier3:*</code> (4 sensitive/destructive tools, #480). Reserved<br />    addon id <code>core</code>; each tool is a thin wrapper over one existing Aiopta<br />    route.<br />  - How the agent discovers/uses them: identical to any addon MCP tool<br />    (§4.1–§4.3) — projected into the registry capability-filtered, called by<br />    id, never probed/spawned, inputs carry no credential/URL/identity.<br />  - Why some core tools are harder to obtain: destructive write tools need<br />    the exact <code>core/…</code> grant verbatim (no <code>*</code>/<code>core/*</code>), and every Tier-3<br />    tool needs its own exact capability (a <code>mcp:core:write</code> grant unlocks<br />    none) and is owner-only to delegate.<br />  - The permission-denied contract for core tools is the §4.3 contract,<br />    stated explicitly for the core surface: a <code>denied</code>/<code>not_available</code> core<br />    result is never retried, never escalated, never side-stepped via the raw<br />    <code>/api/v1/...</code> route, and never worked around by obtaining a<br />    destructive/Tier-3 grant — the agent informs the user once and stops.<br />    Core enforces server-side on every call and audits denials + every<br />    mutation (<code>core.mcp.tool.denied</code> / <code>core.mcp.tool.ok</code>).<br />- Unchanged: auth API (§1), error handling (§2), permission request (§3),<br />  the addon-MCP rules (§4.1–§4.3) and the domain rule — no hard-coded<br />  URLs/hosts; the agent fills the instance URL from its own runtime config.<br />- <code>EXPECTED_BASESKILL_VERSION</code> bumped to <code>1.2.0</code> in<br />  <code>scripts/migrate-base-skill.cjs</code>.</p>\n<h2>1.1.0 — 2026-07-06 (issue #398)</h2>\n<p>MINOR — advisable but non-breaking. Existing agents are upgraded idempotently<br />via <code>scripts/migrate-base-skill.sh --apply</code> (the #105 hook); user<br />customisations are never overwritten.</p>\n<p>- New §4 &quot;MCP tools (addon-delivered and core)&quot;: how the agent discovers<br />  addon-delivered MCP tools (capability-filtered projection into the<br />  canonical tool registry, aiopta-core#417), how it uses them (server-side<br />  re-authorization per call, fail-closed, no credentials/URLs in tool<br />  arguments), and that the core-MCP (aiopta-core#466/#467) follows exactly<br />  the same rules once it ships.<br />- New §4.3 permission-denied contract: a denied MCP tool call is never<br />  retried, never escalated or worked around via another tool; the agent<br />  informs the user once and stops attempting the call.<br />- Renamed the legacy &quot;Aiopta API&quot; wording to &quot;Aiopta API&quot; (naming only, no<br />  semantic change).<br />- Sections renumbered: Provenance is now §5, Hard-negative scope is now §6.<br />- Unchanged: auth API (§1), error handling (§2), permission request (§3)<br />  and the domain rule — no hard-coded URLs/hosts; the agent fills the<br />  instance URL from its own runtime config.<br />- <code>EXPECTED_BASESKILL_VERSION</code> bumped to <code>1.1.0</code> in<br />  <code>scripts/migrate-base-skill.cjs</code>.</p>\n<h2>1.0.0 — 2026-06-23 (issue #105)</h2>\n<p>Initial release. Defines the single, versioned, secret-free Aiopta Basisskill:</p>\n<p>- One skill descriptor: <code>name: aiopta-base</code>, no scripts, no endpoint, no<br />  permissions of its own (meta-skill).<br />- Three documented behaviours: auth API, error handling, permission request.<br />- Hard-negative scope (validator-enforced): no secrets, no hard-coded URLs,<br />  no hard-coded hosts, no addon-specific skills, no scripts.<br />- Migration script <code>scripts/migrate-base-skill.cjs</code> (+ <code>.sh</code> wrapper + tests)<br />  for idempotent backfill of existing agents and upgrade on version bumps.<br />- Catalog entry under id <code>aiopta-base-skill</code> for marketplace discovery.</p>",
          "byteSize": 4457
        }
      }
    },
    {
      "id": "analytics",
      "uuid": "689f3fef-a773-47da-905f-5e037954ad69",
      "name": "Analytics",
      "version": "1.0.5",
      "description": "Usage analytics, cost tracking, performance metrics, and report generation.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "analytics",
        "reports",
        "usage-events",
        "cost-tracking",
        "observability"
      ],
      "tags": [],
      "download": "/artifacts/analytics-1.0.5.zip",
      "sha256": "5d35be6e99b057c6b3e3e35e190cd7f2fdd5542640dace861e70c06c5f3401e6",
      "size": 253027,
      "page": {
        "id": "analytics",
        "path": "/addons/analytics",
        "name": "Analytics",
        "icon": "📈",
        "section": "Overview",
        "component": "@addons/analytics/src/components/AnalyticsPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/analytics/activity",
        "/api/v1/addons/analytics/costs",
        "/api/v1/addons/analytics/dashboard",
        "/api/v1/addons/analytics/events",
        "/api/v1/addons/analytics/observability",
        "/api/v1/addons/analytics/performance",
        "/api/v1/addons/analytics/reports",
        "/api/v1/addons/analytics/reports/{id}",
        "/api/v1/addons/analytics/reports/export"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Analytics</h1>\n<p>Usage analytics, cost tracking, performance metrics, report generation, and a central<br /><strong>Observability-Dashboard</strong> (C5, ADR-033).</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>analytics</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> analytics, reports, usage-events, cost-tracking, observability</li><li><strong>UI-Seite:</strong> <code>/analytics</code> (Tab <strong>Observability</strong> für Nutzung/Kosten/Fehler/Kontingent)</li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET | <code>/api/v1/addons/analytics/activity</code> |<br />| GET | <code>/api/v1/addons/analytics/costs</code> |<br />| GET | <code>/api/v1/addons/analytics/dashboard</code> |<br />| GET, POST | <code>/api/v1/addons/analytics/events</code> |<br />| GET | <code>/api/v1/addons/analytics/observability</code> |<br />| GET | <code>/api/v1/addons/analytics/performance</code> |<br />| GET, POST | <code>/api/v1/addons/analytics/reports</code> |<br />| GET, PUT, DELETE | <code>/api/v1/addons/analytics/reports/[id]</code> |<br />| GET | <code>/api/v1/addons/analytics/reports/export</code> |</p>\n<h2>Observability-Dashboard (C5, ADR-033)</h2>\n<p>Zentrale, <strong>read-only</strong> Aggregation bereits vorhandener Daten über einen wählbaren Zeitraum<br />(24 h / 7 / 30 / 90 Tage):</p>\n<p>- <strong>Agenten-/LLM-Nutzung</strong> — Anzahl + Token-Schätzung aus Phase-Q <code>usage_counters</code>.<br />- <strong>Kosten</strong> — aus den erfassten <code>cost_entries</code> (klar als <strong>Schätzung</strong> markiert, keine Abrechnung).<br />- <strong>Fehler/Aktivität</strong> — aus dem <code>audit_log</code>, gruppiert nach <code>action</code> (die Tabelle hat <strong>keine</strong><br />  Severity-Spalte → Fehler werden anhand des Aktionsnamens erkannt, z. B. <code>*.failed</code>/<code>*.denied</code>).<br />- <strong>Kontingent-Status</strong> — Verbrauch vs. Tarif-Limit aus den Entitlements/Quotas.</p>\n<p>Scope ist rollenbewusst: ein normaler Benutzer sieht <strong>nur eigene</strong> Daten, ein Admin standardmäßig die<br /><strong>ganze Instanz</strong> (mit Opt-down auf <code>?scope=user</code>). Die Route ist die Autorität — ein Benutzer kann den<br />Scope nicht erweitern. KPIs, Charts (hand-gezeichnetes SVG/CSS — <strong>keine</strong> neue Charting-Dependency) und<br />Tabellen. Server liest <code>getDb</code>; die Tab-Komponente nutzt ausschließlich <code>apiFetch</code> (kein <code>getDb</code>/<code>fs</code> im<br />Client). SQL ist durchgehend parametrisiert; es werden keine Secrets gelesen oder geloggt.</p>\n<pre><code class=\"lang-bash\"># Instanzweit (Admin), 30-Tage-Fenster\ncurl -s -H &quot;Authorization: Bearer $AIOPTA_API_KEY&quot; \\\n  &quot;https://&lt;instance&gt;/api/v1/addons/analytics/observability?scope=instance&amp;windowDays=30&quot;</code></pre>\n<p># Nur eigene Daten<br />curl -s -H &quot;Authorization: Bearer $AIOPTA_API_KEY&quot; \\<br />  &quot;<a href=\"https://&amp;lt;instance&amp;gt;/api/v1/addons/analytics/observability?scope=user&amp;amp;windowDays=7&amp;quot;\" rel=\"noopener noreferrer\">https://&amp;lt;instance&amp;gt;/api/v1/addons/analytics/observability?scope=user&amp;amp;windowDays=7&amp;quot;</a><br />```</p>\n<h2>Event-Bus-Contributor-Feed (#142 / INBOX-65, ADR-053 + ADR-057-Addendum)</h2>\n<p>Zusätzlich zum direkten Schreibpfad (<code>POST /events</code> → <code>recordUsageEvent</code>) speist analytics<br />Agent-Aktivitäts-Metriken <strong>passiv über den in-process Event-Bus</strong>: der Collector<br />(<code>src/lib/activity-collector.ts</code>) abonniert <strong>lazy + idempotent</strong> im Addon-<code>install()</code> (globalThis-<br />Singleton, <strong>kein</strong> Boot-Hook — spiegelt den artefakt-workspace-Collector) und schreibt für jedes<br />Agent-Output-Event eine per-User-<code>usage_events</code>-Zeile (<code>event_type=&#39;agent.activity&#39;</code>), sobald es<br />passiert. So füllen sich die bestehenden Dashboard-Metriken (Top-Agenten, Trends) aus echter<br />Agent-Aktivität — additiv, die Bestandspfade bleiben unverändert.</p>\n<p>- <strong>Abonnierte Events:</strong> <code>agent.output.produced</code> (kanonisches generisches Signal) und<br />  <code>development.session.completed</code> (trägt <code>userId</code> + <code>status</code>). Ein <strong>fehlgeschlagener</strong> Run<br />  wird als Error markiert (<code>metadata.isError</code>).<br />- <strong>Ownership (ADR-054):</strong> der Besitzer kommt IMMER aus dem Event-Payload (<code>userId</code>/<code>user_id</code>/<br />  <code>ownerId</code>), NIE aus einem Client-Wert; ein Event ohne erkennbaren User wird <strong>übersprungen</strong><br />  (geloggt), nicht geraten. Der Write läuft über <code>scopedStore(userId)</code> ⇒ <code>user_id</code> server-seitig<br />  erzwungen.<br />- <strong>Per-User-Feed:</strong> <code>GET /api/v1/addons/analytics/activity</code> (<code>userRoute</code>) liefert die <strong>eigenen</strong><br />  <code>agent.activity</code>-Zeilen (Filter <code>event_type</code>/<code>agent_id</code> + Pagination); B&#39;s Aktivität erscheint nie<br />  in A&#39;s Feed. Die instanzweiten Aggregate (dashboard/observability) bleiben admin-gegated.<br />- <strong>Gap (Park-Kandidat):</strong> <code>business-operator.action.queued</code> und <code>payments.invoice.paid</code> tragen keinen<br />  User-Owner und werden <strong>nicht</strong> abonniert (per-User-Recording würde einen Owner erfinden — ADR-054-<br />  verboten). Per-User-Attribution erfordert eine additive Emit-Anreicherung an der Quelle (Owner-<br />  Entscheidung). <code>agent.output.produced</code> hat noch keinen in-tree-Emitter (Vorwärts-Signal).</p>\n<pre><code class=\"lang-bash\"># Eigener Agent-Aktivitäts-Feed (event-getrieben)\ncurl -s -H &quot;Authorization: Bearer $AIOPTA_API_KEY&quot; \\\n  &quot;https://&lt;instance&gt;/api/v1/addons/analytics/activity?page=1&amp;page_size=60&quot;</code></pre>\n<h2>Bekannte Grenzen</h2>\n<p>- <strong>Kosten = Schätzung</strong> aus erfassten Nutzungsdaten, keine abgerechnete Summe (präzises Pricing geparkt,<br />  OPEN-DECISIONS #52).<br />- <strong>Fehler</strong> werden heuristisch aus dem Aktionsnamen im <code>audit_log</code> abgeleitet (keine Severity-Spalte).<br />- <strong>Nutzungszähler</strong> sind Fenster-Buckets (Tag/Monat/lifetime) → die Nutzung spiegelt die <strong>aktuellen</strong><br />  Zähler, nicht einen beliebigen historischen Bereich.<br />- Kein Export zu externen Observability-/APM-Systemen (Prometheus/Grafana/Datadog) — geparkt<br />  (OPEN-DECISIONS #51).</p>",
          "byteSize": 5160
        }
      }
    },
    {
      "id": "artefakt-workspace",
      "uuid": "d8bda831-16ba-4dac-b545-1747a02e313c",
      "name": "Artefakt-Workspace",
      "version": "1.0.4",
      "description": "A typed, auto-collected gallery of ALL agent outputs — subscribes to the in-process event-bus and turns every produced agent output (image/file/code/text/link/data) into a per-user artefact, then presents a filterable grid to view, open, and delete.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "artefacts",
        "agent-output-gallery",
        "event-bus-consumer"
      ],
      "tags": [],
      "download": "/artifacts/artefakt-workspace-1.0.4.zip",
      "sha256": "bea3501639172f77798bc81d81d1e9d42e1bd544e691aa8c81cf2b03dd3bd859",
      "size": 85578,
      "page": {
        "id": "artefakt-workspace",
        "path": "/addons/artefakt-workspace",
        "name": "Artefakt-Workspace",
        "icon": "🗂️",
        "section": "Data",
        "component": "@addons/artefakt-workspace/src/components/ArtefaktWorkspacePage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/artefakt-workspace",
        "/api/v1/addons/artefakt-workspace/facets",
        "/api/v1/addons/artefakt-workspace/{id}"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Artefakt-Workspace</h1>\n<p>A typed, <strong>auto-collected</strong> gallery of ALL agent outputs. The addon subscribes to the in-process<br />event-bus (ADR-053) and turns every produced agent output into a per-user artefact — passively, with<br />zero manual upload — then presents a filterable grid to view, open, and delete.</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>artefakt-workspace</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> artefacts, agent-output-gallery, event-bus-consumer</li><li><strong>UI-Seite:</strong> <code>/addons/artefakt-workspace</code> (Bereich: Data)</li><li><strong>ADR:</strong> ADR-057 · Audit-Zeile <strong>#152</strong> · Issue <strong>#148 / INBOX-71</strong></li></ul>\n<h2>Wie es funktioniert (passiv, event-getrieben)</h2>\n<p>1. Ein Agent erzeugt einen Output (z. B. eine Coding-Session läuft fertig).<br />2. Der Emitter publiziert ein Event auf dem in-process Event-Bus, das die <strong>erzeugende <code>userId</code></strong><br />   (server-seitig gestempelt) und den Output trägt.<br />3. Der <strong>Collector</strong> (<code>src/lib/collector.ts</code>) abonniert die Agent-Output-Events, klassifiziert den<br />   Output nach Typ und legt eine Artefakt-Zeile an — <strong>gestempelt auf den erzeugenden User</strong>.<br />4. Die Galerie zeigt jedem User <strong>nur seine eigenen</strong> Artefakte.</p>\n<p>Abonnierte Events:</p>\n<p>| Event | Quelle | Trägt |<br />|---|---|---|<br />| <code>agent.output.produced</code> | beliebig (kanonisches Signal) | <code>userId</code> + Output (<code>content</code>/<code>url</code>/<code>path</code>/<code>kind</code>/<code>mime</code>/<code>title</code>) |<br />| <code>development.session.completed</code> | development | <code>userId</code> + <code>result</code>/<code>text</code> (additiv ergänzt, #148) |</p>\n<blockquote><p><strong>Ownership-Regel (ADR-054):</strong> Der Besitzer kommt IMMER aus dem Event-Payload-User-Context</p><p>(<code>userId</code>/<code>user_id</code>/<code>ownerId</code>) — nie aus einem Client-Wert. Ein Event ohne erkennbaren User wird</p><p><strong>übersprungen</strong> (geloggt), nicht geraten.</p></blockquote>\n<h2>Typ-Klassifikation</h2>\n<p>Sechs Typen, first-match-wins (am spezifischsten → am allgemeinsten):</p>\n<p>| Typ | Erkannt an |<br />|---|---|<br />| <code>image</code> 🖼️ | Image-MIME, oder url/path mit Bild-Endung |<br />| <code>file</code> 📄 | ein <code>path</code>-Verweis, eine Quelldatei-url, oder ein nicht-Bild-Binär-MIME |<br />| <code>code</code> 💻 | ein <code>language</code>-Hinweis oder ein ```<code> </code>`<code> </code>```-Codeblock |<br />| <code>text</code> 📝 | Fallback für Prosa |<br />| <code>link</code> 🔗 | eine nackte http(s)-url |<br />| <code>data</code> 📊 | Inhalt, der als JSON-Objekt/Array parst |</p>\n<p>Ein expliziter, gültiger <code>kind</code>-Hinweis im Payload gewinnt immer über die Inferenz.</p>\n<h2>API-Endpunkte</h2>\n<p>Basis: <code>/api/v1/addons/artefakt-workspace</code>. Auth: Aiopta-API-Key<br />(<code>-H &quot;Authorization: Bearer $AIOPTA_API_KEY&quot;</code>). Antworten: <code>{ success, data | error }</code>.</p>\n<p>| Methode(n) | Pfad | Zweck |<br />|---|---|---|<br />| GET | <code>/api/v1/addons/artefakt-workspace</code> | Eigene Artefakte listen (Filter: <code>type</code>, <code>source</code>, <code>since</code>, <code>until</code>, <code>page</code>, <code>page_size</code>) |<br />| GET | <code>/api/v1/addons/artefakt-workspace/facets</code> | Pro-Typ-Zähler + Quellen-Liste für die Filterleiste |<br />| GET | <code>/api/v1/addons/artefakt-workspace/[id]</code> | Ein eigenes Artefakt holen (fremde/unbekannte id → 404) |<br />| DELETE | <code>/api/v1/addons/artefakt-workspace/[id]</code> | Ein eigenes Artefakt löschen (owner-scoped; fremd → 404) |</p>\n<h3>Beispiel</h3>\n<pre><code class=\"lang-bash\"># Eigene Code-Artefakte der letzten Woche listen\ncurl -s -H &quot;Authorization: Bearer $AIOPTA_API_KEY&quot; \\\n  &quot;https://&lt;instance&gt;/api/v1/addons/artefakt-workspace?type=code&amp;since=2026-06-09&quot;</code></pre>\n<p># Facetten (für die Filter-Chips)<br />curl -s -H &quot;Authorization: Bearer $AIOPTA_API_KEY&quot; \\<br />  &quot;<a href=\"https://&amp;lt;instance&amp;gt;/api/v1/addons/artefakt-workspace/facets&amp;quot;\" rel=\"noopener noreferrer\">https://&amp;lt;instance&amp;gt;/api/v1/addons/artefakt-workspace/facets&amp;quot;</a></p>\n<p># Ein Artefakt löschen<br />curl -s -X DELETE -H &quot;Authorization: Bearer $AIOPTA_API_KEY&quot; \\<br />  &quot;<a href=\"https://&amp;lt;instance&amp;gt;/api/v1/addons/artefakt-workspace/&amp;lt;id&amp;gt;&amp;quot;\" rel=\"noopener noreferrer\">https://&amp;lt;instance&amp;gt;/api/v1/addons/artefakt-workspace/&amp;lt;id&amp;gt;&amp;quot;</a><br />```</p>\n<h2>Sicherheit</h2>\n<p>- <strong>Per-User isoliert (ADR-054):</strong> Jedes Artefakt gehört dem erzeugenden User; <code>addon_artefacts</code><br />  trägt <code>user_id TEXT NOT NULL</code>, jeder Zugriff läuft über <code>scopedStore(userId)</code>. Man sieht und<br />  löscht nur die EIGENEN Artefakte; eine fremde/unbekannte <code>{id}</code> liefert <strong>404</strong> (nie 403, nie die<br />  fremde Zeile).<br />- <strong>Route-Auth (ADR-056):</strong> Alle Routen sind <code>userRoute</code>/<code>ownedResourceRoute</code>-gewrappt; die<br />  Lösch-Mutation ist owner-scoped.<br />- <strong>Keine Blobs / kein FS-Write:</strong> Es werden nur Metadaten + eine Referenz (url/path) und ein<br />  gekappter Inhalts-Vorschautext (max. 20 000 Zeichen) gespeichert.</p>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — das Addon sammelt automatisch, sobald es installiert/aktiviert ist<br />(die Event-Bus-Subskription wird im <code>install()</code> lazy + idempotent aufgesetzt).</p>",
          "byteSize": 4314
        }
      }
    },
    {
      "id": "backups",
      "uuid": "0816ec0d-e322-4517-99ac-28c6b8ed5e7d",
      "name": "Backups",
      "version": "1.4.0",
      "description": "Full-instance backup and restore: create a single archive containing a logical dump of every database table (incl. app_settings/license/config) plus the files under DATA_DIR plus a manifest with a SHA-256 checksum, download it, and restore from a stored, uploaded, or remote-pulled archive. Archives are ENCRYPTED AT REST (AES-256-GCM, self-managed key) and can be copied off-site to an S3, SFTP, or NAS (WebDAV) remote target (owner creds; gracefully inactive when unset); a restore can pull the archive back from that target. A retention prune keeps the last N / N days (never the last copy). Backup schedules auto-fire (create + prune) on the durable in-process scheduler. Restore is destructive, admin-gated, requires an explicit confirm, takes a pre-restore safety snapshot first, and is never auto-run; extraction is zip-slip/zip-bomb-safe.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "backups",
        "restore",
        "schedules"
      ],
      "tags": [],
      "download": "/artifacts/backups-1.4.0.zip",
      "sha256": "2c116e9d4fd41bc6edf8be42c34e483a34611622c27f2b41469f3428e605c6a6",
      "size": 359413,
      "page": {
        "id": "backups",
        "path": "/addons/backups",
        "name": "Backups",
        "icon": "💾",
        "section": "Admin",
        "component": "@addons/backups/src/components/BackupPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/backups",
        "/api/v1/addons/backups/{id}",
        "/api/v1/addons/backups/{id}/download",
        "/api/v1/addons/backups/{id}/restore",
        "/api/v1/addons/backups/restore",
        "/api/v1/addons/backups/config",
        "/api/v1/addons/backups/schedules",
        "/api/v1/addons/backups/schedules/{id}"
      ],
      "skills": [
        {
          "name": "backup-create",
          "displayName": "Backups: Create & list full-instance backups",
          "description": "Create a full-instance backup (a single ZIP with a logical dump of every DB table incl. app_settings/license/config, the files under DATA_DIR, and a manifest with a SHA-256 checksum) and list existing backups with their size/date/contents. A backup contains secret settings by nature, so this requires the addon.manage capability. Pass excludeSecrets:true for a sanitized dump.",
          "permissions": [
            "addons/backups/backups"
          ],
          "endpoint": "/api/v1/addons/backups",
          "scripts": []
        },
        {
          "name": "backup-restore",
          "displayName": "Backups: Restore the instance (destructive)",
          "description": "Restore the instance from a stored backup (POST /api/v1/addons/backups/:id/restore), an uploaded archive (POST /api/v1/addons/backups/restore, multipart), or an archive pulled from the configured remote/NAS target (POST /api/v1/addons/backups/restore, JSON { remoteFile, confirm }). DESTRUCTIVE: it overwrites the DB rows + data files. It requires the addon.manage capability AND an explicit confirm flag (never auto-restores), validates the manifest+checksum+version first, and takes a pre-restore safety snapshot so a bad restore is recoverable. Extraction is zip-slip/zip-bomb-safe.",
          "permissions": [
            "addons/backups/backups",
            "addons/backups/restore"
          ],
          "endpoint": "/api/v1/addons/backups/restore",
          "scripts": []
        },
        {
          "name": "backup-schedules",
          "displayName": "Backups: Manage backup schedules",
          "description": "List/create/update/delete scheduled-backup definitions (name, type, cron or interval_seconds, retention). Requires the addon.manage capability. Enabled schedules AUTO-FIRE on the durable in-process scheduler (#124): each due fire creates an encrypted backup and applies the retention prune restart-persistently. Auto-fire only ever creates/prunes — it never restores.",
          "permissions": [
            "addons/backups/schedules"
          ],
          "endpoint": "/api/v1/addons/backups/schedules",
          "scripts": []
        },
        {
          "name": "backup-config",
          "displayName": "Backups: Configure encryption / remote target / retention",
          "description": "Read (masked) and update the backups configuration: the off-site remote target (none|s3|sftp|nas, endpoint/bucket/region/path or WebDAV URL + owner-secret creds), and the retention policy (keep N / N days). The nas target uses WebDAV (Synology/QNAP/TrueNAS/Nextcloud) over plain HTTP[S] with no extra dependency. Trigger a non-destructive remote connectivity test (POST ?action=test-remote) or an immediate retention prune (POST ?action=prune). At-rest AES-256-GCM encryption uses a self-managed key (auto-generated, masked, never returned). Requires the addon.manage capability; secret values are never echoed.",
          "permissions": [
            "addons/backups/backups"
          ],
          "endpoint": "/api/v1/addons/backups/config",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Backups</h1>\n<p>Full-instance <strong>backup + restore</strong> (C4, ADR-032).</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>backups</code></li><li><strong>Version:</strong> 1.2.0</li><li><strong>Fähigkeiten:</strong> backups, restore, schedules</li><li><strong>UI-Seite:</strong> <code>/backups</code> (Admin-Sektion)</li></ul>\n<p>An admin can create a full-instance backup, download it, and restore from a stored or uploaded archive.<br />A backup is a single ZIP containing a <strong>logical dump of every database table</strong> (including<br /><code>app_settings</code> with its secret/license/config rows and all addon-local tables), the **files under<br /><code>DATA_DIR</code><strong>, and a </strong>manifest with a SHA-256 checksum**. The archive is built with a dependency-free<br />ZIP writer (Node <code>zlib</code>) and is read back by Aiopta&#39;s hardened, zip-slip/zip-bomb-safe extractor.</p>\n<p>Backups are <strong>encrypted at rest</strong> (AES-256-GCM), can be copied <strong>off-site</strong> to an **S3, SFTP, or NAS<br />(WebDAV)<strong> remote target — and a restore can </strong>pull the archive back** from that target — are pruned by a<br /><strong>retention policy</strong>, and enabled <strong>schedules auto-fire</strong> on the durable in-process scheduler<br />(#124 + #138, ADR-032 addendum).</p>\n<h2>Einrichtung</h2>\n<p>Funktioniert sofort ohne Credentials: das lokale Backup wird <strong>at-rest verschlüsselt</strong> mit einem<br />selbst-verwalteten 32-Byte-Schlüssel, der beim ersten Backup automatisch erzeugt und in der Addon-Config<br />als Secret (<code>BACKUP_ENC_KEY</code>, <code>is_secret=1</code>, maskiert, nie geloggt) abgelegt wird — kein Owner nötig.</p>\n<p>Optional (Off-site, Owner-Creds, OPEN-DECISIONS #48) über <code>PUT /api/v1/addons/backups/config</code> bzw. die<br />Addon-Config:</p>\n<p>| Key | Zweck |<br />|---|---|<br />| <code>BACKUP_REMOTE_TYPE</code> | <code>none</code> (Default) · <code>s3</code> · <code>sftp</code> · <code>nas</code> |<br />| <code>BACKUP_REMOTE_PATH</code> | Key-/Pfad-Präfix im Ziel (Unterordner) |<br />| <code>BACKUP_S3_BUCKET</code> / <code>BACKUP_S3_REGION</code> / <code>BACKUP_S3_ENDPOINT</code> | S3-Ziel (Endpoint optional für S3-kompatibel) |<br />| <code>BACKUP_S3_ACCESS_KEY_ID</code> / <code>BACKUP_S3_SECRET_ACCESS_KEY</code> | S3-Creds (<strong>Secret</strong>) |<br />| <code>BACKUP_SFTP_HOST</code> / <code>BACKUP_SFTP_PORT</code> / <code>BACKUP_SFTP_USERNAME</code> | SFTP-Ziel |<br />| <code>BACKUP_SFTP_PASSWORD</code> / <code>BACKUP_SFTP_PRIVATE_KEY</code> | SFTP-Creds (<strong>Secret</strong>) |<br />| <code>BACKUP_NAS_URL</code> | <strong>NAS via WebDAV</strong> — Collection-Basis-URL (z. B. <code>https://nas.local:5006/remote.php/dav/files/mc</code>); Synology/QNAP/TrueNAS/Nextcloud, reines HTTP[S], keine neue Dependency |<br />| <code>BACKUP_NAS_USERNAME</code> / <code>BACKUP_NAS_PASSWORD</code> | NAS/WebDAV-Creds (Passwort <strong>Secret</strong>) |<br />| <code>BACKUP_RETENTION_KEEP</code> / <code>BACKUP_RETENTION_DAYS</code> | Retention (Default 10 / 30) |</p>\n<p>Solange Creds fehlen ist das Remote-Ziel <strong>graceful inaktiv</strong> (&quot;remote target not configured&quot;) — das<br />lokale (verschlüsselte) Backup läuft trotzdem, es gibt keinen Fake-Upload und keinen Crash. Alle Routen<br />sind <strong>admin-only</strong> (<code>addon.manage</code>), weil ein Backup naturgemäß Secret-Einstellungen enthält.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad | Zweck |<br />|---|---|---|<br />| GET | <code>/api/v1/addons/backups</code> | Backups auflisten (admin) |<br />| POST | <code>/api/v1/addons/backups</code> | Voll-Backup erstellen — Body <code>{ name?, excludeSecrets? }</code> (admin) |<br />| GET, DELETE | <code>/api/v1/addons/backups/[id]</code> | Backup-Metadaten / löschen (das letzte Backup ist geschützt) |<br />| GET | <code>/api/v1/addons/backups/[id]/download</code> | Archiv als Attachment streamen (admin) |<br />| POST | <code>/api/v1/addons/backups/[id]/restore</code> | Aus gespeichertem Backup wiederherstellen — Body <code>{ confirm: true }</code> (admin, destruktiv) |<br />| POST | <code>/api/v1/addons/backups/restore</code> | Aus <strong>hochgeladenem</strong> Archiv wiederherstellen — multipart <code>file</code> + <code>confirm=true</code> (admin, destruktiv); <code>dryRun=true</code> validiert nur. <strong>Oder</strong> aus dem <strong>Remote-/NAS-Ziel ziehen</strong> — JSON-Body <code>{ remoteFile, confirm: true }</code> (zieht zuerst, dann identischer Safe-Restore). Entschlüsselt verschlüsselte Archive automatisch |<br />| GET, PUT, POST | <code>/api/v1/addons/backups/config</code> | Config lesen (maskiert) / speichern; <code>POST ?action=test-remote</code> (Verbindungstest) · <code>POST ?action=prune</code> (Retention jetzt anwenden) (admin) |<br />| GET, POST | <code>/api/v1/addons/backups/schedules</code> | Backup-Zeitpläne auflisten/erstellen — Body akzeptiert <code>interval_seconds?</code> (admin) |<br />| GET, PATCH, DELETE | <code>/api/v1/addons/backups/schedules/[id]</code> | Zeitplan lesen/ändern/löschen (admin) |</p>\n<h2>Sicherheit</h2>\n<p>- <strong>Destruktiv &amp; abgesichert:</strong> Restore läuft nie implizit — explizites <code>confirm</code> erforderlich; das Manifest<br />  + die Prüfsumme + die Format-Version werden <strong>vor</strong> jedem Schreiben validiert; vorab wird ein<br />  <strong>Pre-Restore-Safety-Snapshot</strong> des aktuellen Stands erstellt; die DB wird in <strong>einer Transaktion</strong><br />  (atomar) wiederhergestellt; die Extraktion ist <strong>zip-slip/zip-bomb-sicher</strong> (geteilter Extractor).<br />- <strong>Encryption-at-rest:</strong> Archive werden mit <strong>AES-256-GCM</strong> in einem authentifizierten Envelope<br />  (<code>MCBK1</code>-Magic + IV + Auth-Tag) verschlüsselt; der 32-Byte-Schlüssel wird lazy erzeugt und als Secret<br />  in der Addon-Config gehalten (nie geloggt). <strong>Rückwärtskompatibel:</strong> alte Plaintext-ZIPs werden weiter<br />  restauriert; der Download entschlüsselt on-the-fly zu einem portablen ZIP.<br />- <strong>Secrets:</strong> Ein Backup enthält Secret-<code>app_settings</code> (Lizenz/Auth/Config) — deshalb sind alle Routen<br />  admin-only und Secret-<strong>Werte</strong> (inkl. Backup-Key und Remote-Creds) werden nie geloggt.<br />  <code>excludeSecrets: true</code> erzeugt einen bereinigten Dump (ohne <code>is_secret</code>-Zeilen). **Ein Restore<br />  überschreibt auch die Secrets.**<br />- <strong>Retention:</strong> löscht nur Archive, die <strong>beide</strong> Fenster (Anzahl <strong>und</strong> Alter) verletzen, und **nie das<br />  letzte verbleibende Backup**.<br />- <strong>Auto-Fire-Sicherheit:</strong> der Scheduler erstellt + pruned nur — ein **destruktiver Restore wird nie<br />  automatisch ausgeführt**.</p>\n<h2>Bekannte Grenzen / geparkt</h2>\n<p>- <strong>Remote-Aktivierung</strong> wartet auf Owner-Creds (OPEN-DECISIONS #48 / Block H; das NAS-Ziel ist #138) —<br />  Mechanismus + UI sind gebaut, das Ziel ist ohne Creds graceful inaktiv. <code>ssh2</code> (SFTP) wird dynamisch<br />  geladen; fehlt es, meldet der Upload &quot;dependency missing&quot; statt zu crashen. <strong>NAS</strong> nutzt WebDAV über den<br />  globalen <code>fetch</code> (keine neue Dependency); Creds gehen nur in den <code>Authorization: Basic …</code>-Header und<br />  werden nie geloggt/zurückgegeben.<br />- <strong>Cron mit fixer Uhrzeit:</strong> der durable Scheduler arbeitet intervallbasiert; Cron-Ausdrücke werden auf<br />  eine feste Kadenz gemappt (stündlich/täglich/wöchentlich/alle-N-Min/Std), eine exakte Wall-Clock-Zeit<br />  ist eine Folgearbeit (OPEN-DECISIONS #50, &quot;simpel jetzt&quot; wie der restliche Scheduler).</p>\n<blockquote><p>Siehe <code>docs/architecture/ADR-032-backup-restore.md</code> (Addendum #124) und <code>SKILL.md</code>.</p></blockquote>",
          "byteSize": 6388
        }
      }
    },
    {
      "id": "bahn-bet",
      "uuid": "cba4f478-4516-457d-986b-4ded28cf8e4a",
      "name": "BahnBet",
      "version": "1.0.6",
      "description": "Live BahnBet monitor for public train-delay markets, platform stats, and leaderboard highlights.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "train-delay-markets",
        "leaderboard",
        "web-scraping"
      ],
      "tags": [],
      "download": "/artifacts/bahn-bet-1.0.6.zip",
      "sha256": "c6421aa842fe1063e363a9563661e1f7934b978911d2c5da450adf8e84b68b62",
      "size": 77639,
      "page": {
        "path": "/addons/bahn-bet",
        "name": "BahnBet",
        "icon": "🚆",
        "section": "Experiments",
        "component": "@addons/bahn-bet/src/components/BahnBetDashboard"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/bahn-bet/summary",
        "/api/v1/addons/bahn-bet/leaderboard"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>BahnBet</h1>\n<p>Live BahnBet monitor for public train-delay markets, platform stats, and leaderboard highlights.</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>bahn-bet</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> train-delay-markets, leaderboard, web-scraping</li><li><strong>UI-Seite:</strong> <code>/addons/bahn-bet</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials.</p>\n<p>npm-Abhängigkeiten (werden bei Installation aufgelöst): <code>cheerio</code>.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET | <code>/api/v1/addons/bahn-bet/leaderboard</code> |<br />| GET | <code>/api/v1/addons/bahn-bet/status</code> |<br />| GET | <code>/api/v1/addons/bahn-bet/summary</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<ul><li>Keine bekannten Einschränkungen.</li></ul>\n<blockquote><p>README aus <code>addon.json</code> + vorhandenen Routen generiert (<code>scripts/generate-addon-readmes.js</code>). Bei manuellen Ergänzungen den Generator-Hinweis entfernen.</p></blockquote>",
          "byteSize": 837
        }
      }
    },
    {
      "id": "blender",
      "uuid": "3c63ddcb-87fa-4b10-b516-8f7926ef8a9e",
      "name": "Blender",
      "version": "0.3.1",
      "description": "Agent-driven Blender via a BlenderMCP-style socket backend, fronted by a capability-enforcing Aiopta proxy (addons#815, core#1078 F2). Curated semantic tools (scene inspect/op, material, render, asset import/export) map to elevated dotted capabilities (addons.blender.scene.*/.asset.*); a separately gated raw-Python hatch (execute_blender_code) maps to the highest-tier addons.blender.python.exec. No raw passthrough: every tool call is gated against server-resolved grants and audited. Two backends: local-first (native-app bridge to the user's Blender on loopback :9876) and a headless sandbox container (workspace-only, no host network, render caps) for the safe autonomous path. Future/backlog — not dispatched until after the Enterprise-Clean programme (#1020).",
      "author": "Aiopta",
      "icon": "🧊",
      "capabilities": [
        "blender",
        "3d",
        "rendering",
        "mcp",
        "agent-tools"
      ],
      "tags": [],
      "download": "/artifacts/blender-0.3.1.zip",
      "sha256": "d77c747420192ec3b2a98696095d6f6e0c1fff961bd22b7e6881ff4fac6fd8fb",
      "size": 157163,
      "page": null,
      "deprecated": false,
      "endpoints": [],
      "skills": [],
      "mcpServers": [
        {
          "id": "blender-tools",
          "displayName": "Blender tools",
          "description": "Capability-enforcing MCP proxy in front of a BlenderMCP-style socket backend (addons#815 / core#1078 F2). The agent never speaks the Blender socket protocol directly: it sees curated semantic tools (scene inspect/op, material, render, asset import/export) plus a separately gated raw-Python hatch. Each tool maps to a dotted capability (scene/asset = elevated, python/exec = highest); calls are gated against server-resolved grants (core#871 gateway seam, stubbed) and audited. Credentials for asset providers travel only via scoped tickets (addons#813), never as tool arguments. Local backend talks loopback-only; sandbox backend has no host network.",
          "transport": "stdio",
          "runtime": "node",
          "entry": "blender-tools/server.mjs",
          "permissions": [
            "addons/blender/asset/export",
            "addons/blender/asset/import",
            "addons/blender/scene/op",
            "addons/blender/scene/read",
            "addons/blender/scene/render"
          ],
          "toolPolicy": {
            "mode": "write",
            "allowDestructive": false
          },
          "configRefs": [
            "mode"
          ],
          "credentialRefs": [
            "sketchfabApiKey"
          ],
          "archiveEntry": "blender/mcp/servers/blender-tools/server.mjs",
          "sha256": "b53146b7547d898f8841c598860f7610cd159983de4f9b887ff7dadd4e221f10",
          "size": 30392,
          "descriptorVersion": 1
        }
      ],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": null
    },
    {
      "id": "booking",
      "uuid": "603d9ba4-21d6-4cdb-b8e5-ef3c0536548d",
      "name": "Booking",
      "version": "1.0.1",
      "description": "Per-user booking addon: owner-configured bookable resources, weekly availability and time slots, a calendar of bookings, plus a public booking page where visitors pick an open slot and book — with confirmation/reminder emails.",
      "author": "Aiopta",
      "icon": "📆",
      "capabilities": [
        "booking",
        "scheduling",
        "calendar"
      ],
      "tags": [],
      "download": "/artifacts/booking-1.0.1.zip",
      "sha256": "d0809fe605e15113c877853face9f97a04c4729afb78ad9a5fa5269e7d90d404",
      "size": 117233,
      "page": null,
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/booking"
      ],
      "skills": [
        {
          "name": "booking-manage",
          "displayName": "Manage bookings",
          "description": "List, create and update the user's own bookable resources, availability and bookings.",
          "permissions": [
            "addons/booking/read",
            "addons/booking/write"
          ],
          "endpoint": "/api/v1/addons/booking",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Booking Addon</h1>\n<p>Per-user booking for Aiopta (issue #485 / INBOX-5). An owner publishes<br />bookable resources with weekly availability; the addon computes concrete open<br />time slots and exposes a <strong>public booking page</strong> where external visitors pick a<br />slot and book. Confirmation (and reminder) emails are sent best-effort.</p>\n<p>Built user-scoped from day one on the ADR-054 foundation: every durable record<br />carries <code>user_id</code> and is reached only through <code>scopedStore(ownerId)</code>.</p>\n<h2>Capabilities (acceptance criteria)</h2>\n<p>- <strong>Bookable resources</strong> (event types): name, slug, duration, buffer, active.<br />- <strong>Availability</strong>: weekly recurring windows per resource (weekday + start/end).<br />- <strong>Slots / calendar</strong>: open slots are computed from availability minus existing<br />  bookings (buffer-aware), over a configurable horizon, with a minimum lead time.<br />- <strong>Public booking page</strong>: read-only listing of an owner&#39;s published resources +<br />  open slots, and a constrained &quot;create a pending booking&quot; action.<br />- <strong>Confirmation / reminder emails</strong>: pure renderers + an SMTP transport (lazy<br />  <code>nodemailer</code>, instance addon-config); reminder timing is computed (<code>reminderDueAt</code>)<br />  for a cron/worker — the addon holds no live timers.</p>\n<h2>Public-vs-owner security model</h2>\n<p>This is the security-critical boundary. Two distinct surfaces:</p>\n<p>| Surface | Auth | Wrapper | Owner identity | What it can do |<br />| --- | --- | --- | --- | --- |<br />| Owner CRUD (<code>resources</code>, <code>availability</code>, <code>bookings</code>, <code>settings</code>) | authenticated | <code>userRoute</code> | <code>auth.id</code> (server) | full read/write of <strong>own</strong> data only |<br />| Public page (<code>public/[owner]</code>, <code>…/availability</code>, <code>…/book</code>) | <strong>none</strong> | <code>route</code> | resolved from the <strong>public slug</strong> | read published slots; create <strong>one pending</strong> booking for that owner |</p>\n<p>Key invariants:</p>\n<p>1. <strong>The owner id is never taken from the request body.</strong> Owner CRUD stamps the<br />   owner from <code>auth.id</code>; the public path resolves it from the public slug via<br />   <code>resolveOwnerByPublicSlug</code> and forces it into <code>createBooking</code>. There is no<br />   parameter by which a caller can write to a <em>different</em> owner.<br />2. <strong>The public path is not a cross-user write primitive.</strong> The only mutation an<br />   anonymous visitor can perform is appending a <code>pending</code> booking to the single<br />   owner the slug names, for a slot that owner <em>published</em>, validated against that<br />   owner&#39;s availability + existing bookings, and bounded by a per-key sliding<br />   rate limit. Owners review/confirm pending bookings via the authed CRUD.<br />3. <strong>No cross-user reads.</strong> The public projection exposes only <code>active</code> resources<br />   and computed slots for the resolved owner — never another owner&#39;s data, never<br />   private fields (resource ids, visitor PII of other bookings, owner id).<br />4. <strong>Unauthenticated reachability.</strong> The addon dispatch defers unauthenticated<br />   callers to the route&#39;s own auth, so the public <code>route()</code>-wrapped endpoints are<br />   reachable without a session while the owner endpoints require one.<br />5. <strong><code>resolveOwnerByPublicSlug</code> is the only cross-user read</strong> — it returns just an<br />   owner id from a public slug (no private data); everything after it is normal<br />   owner-scoped access for that one resolved owner.</p>\n<h2>Data model (all <code>user_id</code>-scoped, FK-free)</h2>\n<ul><li><code>booking_resources(id, user_id, slug, name, description, duration_minutes, buffer_minutes, active, …)</code> — <code>UNIQUE(user_id, slug)</code></li><li><code>booking_availability(id, user_id, resource_id, weekday, start_minute, end_minute, …)</code></li><li><code>booking_bookings(id, user_id, resource_id, booking_date, start_minute, end_minute, visitor_name, visitor_email, notes, status, source, …)</code></li><li><code>booking_settings(user_id PK, public_slug UNIQUE, display_name, timezone, min_lead_minutes, horizon_days, accepting_bookings, …)</code></li></ul>\n<p>The <code>user_id</code> column is intentionally <strong>FK-free</strong> (<code>TEXT NOT NULL</code>, no<br /><code>REFERENCES users(id)</code>): a REFERENCES FK resolves at prepare time and throws on a<br />bare addon-only test DB. Ownership is enforced in SQL by the user-scope layer<br />regardless, and <code>registerScopedTable</code> is best-effort (try/catch).</p>\n<h2>API</h2>\n<p>Owner (authenticated):<br />- <code>GET/POST /api/v1/addons/booking/resources</code>, <code>GET/PUT/DELETE …/resources/[id]</code><br />- <code>GET/POST /api/v1/addons/booking/availability</code>, <code>DELETE …/availability/[id]</code><br />- <code>GET/POST /api/v1/addons/booking/bookings</code>, <code>GET/PATCH/DELETE …/bookings/[id]</code><br />- <code>GET/PUT /api/v1/addons/booking/settings</code> (mail status is masked — no secret)<br />- <code>GET /api/v1/addons/booking/status</code></p>\n<p>Public (unauthenticated, keyed to the owner&#39;s public slug):<br />- <code>GET /api/v1/addons/booking/public/[owner]</code> — published resources + page meta<br />- <code>GET /api/v1/addons/booking/public/[owner]/availability?resource=&lt;slug&gt;</code> — open slots<br />- <code>POST /api/v1/addons/booking/public/[owner]/book</code> — create a pending booking</p>\n<h2>Email config</h2>\n<p>SMTP is read from instance addon-config (<code>SMTP_HOST/PORT/USER/PASS/SECURE/FROM</code>)<br />via <code>getAddonConfiguration(&quot;booking&quot;)</code>, never the manifest, never logged. The<br />settings/status endpoints expose only a <strong>masked</strong> view (presence flags + the<br />public From address).</p>",
          "byteSize": 4991
        }
      }
    },
    {
      "id": "browser",
      "uuid": "999f26ee-fc80-49a1-bbd8-8a3110ecb2a9",
      "name": "Browser",
      "version": "1.4.0",
      "description": "Container-local browser automation addon with screenshot and page snapshot APIs.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "browser-automation",
        "screenshots",
        "page-snapshots"
      ],
      "tags": [],
      "download": "/artifacts/browser-1.4.0.zip",
      "sha256": "1edbd69d54d2c18be8d6148a2e6599882b9c08f9c19409884d1a35435eecac3d",
      "size": 119739,
      "page": {
        "path": "/addons/browser",
        "name": "Browser",
        "icon": "🌐",
        "section": "Tools",
        "component": "@addons/browser/src/ui/BrowserSettings"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/browser/health",
        "/api/v1/addons/browser/status",
        "/api/v1/addons/browser/start",
        "/api/v1/addons/browser/stop",
        "/api/v1/addons/browser/snapshot",
        "/api/v1/addons/browser/screenshot",
        "/api/v1/addons/browser/document"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Browser</h1>\n<p>Container-local browser automation addon with screenshot and page snapshot APIs.</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>browser</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> browser-automation, screenshots, page-snapshots</li><li><strong>UI-Seite:</strong> <code>/addons/browser</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials.</p>\n<p>npm-Abhängigkeiten (werden bei Installation aufgelöst): <code>puppeteer</code>.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET | <code>/api/v1/addons/browser/config</code> |<br />| GET | <code>/api/v1/addons/browser/health</code> |<br />| POST | <code>/api/v1/addons/browser/install-runtime</code> |<br />| POST | <code>/api/v1/addons/browser/screenshot</code> |<br />| POST | <code>/api/v1/addons/browser/snapshot</code> |<br />| POST | <code>/api/v1/addons/browser/start</code> |<br />| GET | <code>/api/v1/addons/browser/status</code> |<br />| POST | <code>/api/v1/addons/browser/stop</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<ul><li>Keine bekannten Einschränkungen.</li></ul>\n<blockquote><p>README aus <code>addon.json</code> + vorhandenen Routen generiert (<code>scripts/generate-addon-readmes.js</code>). Bei manuellen Ergänzungen den Generator-Hinweis entfernen.</p></blockquote>",
          "byteSize": 1040
        }
      }
    },
    {
      "id": "budget",
      "uuid": "1f4a5e3f-8f9a-4f8a-8c3c-24a07f957465",
      "name": "Budget (Konten/Kategorien/Budgets/wiederkehrend/Charts)",
      "version": "1.2.0",
      "description": "Per-User Budgeting: Konten, Kategorien, Budgets, wiederkehrende Transaktionen und Charts (Ausgaben über Zeit, Kategorie-Aufschlüsselung, Budget vs Ist). Integriert lesend mit den trading- + payments-Addons (realisierte PnL / bezahlte Rechnungen fließen in die Reports), ohne diese zu verändern. Lokale SQLite ist die Quelle der Wahrheit (ADR-054).",
      "author": "Aiopta",
      "icon": "💰",
      "capabilities": [
        "budget",
        "accounts",
        "categories",
        "budgets",
        "transactions",
        "recurring",
        "reports",
        "charts"
      ],
      "tags": [],
      "download": "/artifacts/budget-1.2.0.zip",
      "sha256": "fa5b12d76d218c60cec6bf77b72848732470309b822f37392b2622dd3b71ff87",
      "size": 196651,
      "page": {
        "id": "budget-page",
        "path": "/addons/budget",
        "name": "Budget",
        "icon": "💰",
        "section": "Money",
        "component": "@addons/budget/src/components/BudgetPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/budget/accounts",
        "/api/v1/addons/budget/accounts/:id",
        "/api/v1/addons/budget/categories",
        "/api/v1/addons/budget/categories/:id",
        "/api/v1/addons/budget/budgets",
        "/api/v1/addons/budget/budgets/:id",
        "/api/v1/addons/budget/transactions",
        "/api/v1/addons/budget/transactions/:id",
        "/api/v1/addons/budget/recurring",
        "/api/v1/addons/budget/recurring/:id",
        "/api/v1/addons/budget/reports/bundle",
        "/api/v1/addons/budget/reports/summary"
      ],
      "skills": [
        {
          "name": "budget-read",
          "displayName": "Budget: Konten, Budgets & Reports lesen",
          "description": "Listet Konten, Kategorien, Budgets, Transaktionen und wiederkehrende Regeln des Nutzers sowie die Charts (Ausgaben über Zeit, Kategorie-Aufschlüsselung, Budget vs Ist). Reiner Lesezugriff — bucht nichts.",
          "permissions": [
            "addons/budget/accounts",
            "addons/budget/categories",
            "addons/budget/budgets",
            "addons/budget/transactions",
            "addons/budget/recurring",
            "addons/budget/reports"
          ],
          "endpoint": "/api/v1/addons/budget/reports/bundle",
          "scripts": []
        },
        {
          "name": "budget-write",
          "displayName": "Budget: Transaktionen, Budgets & Regeln erfassen",
          "description": "Erfasst Konten, Kategorien, Budgets, Transaktionen und wiederkehrende Regeln für den Nutzer (per-User gebucht) und löscht diese.",
          "permissions": [
            "addons/budget/accounts",
            "addons/budget/categories",
            "addons/budget/budgets",
            "addons/budget/transactions",
            "addons/budget/recurring"
          ],
          "endpoint": "/api/v1/addons/budget/transactions",
          "scripts": []
        }
      ],
      "mcpServers": [
        {
          "id": "budget-tools",
          "displayName": "Budget tools",
          "description": "Read-only budget report math for agents: spending over time, category breakdown, budget vs actual, account balances, recurring-rule expansion and money formatting — the same pure functions the budget addon's charts use (issue #405, epic #399). Books nothing.",
          "transport": "stdio",
          "runtime": "node",
          "entry": "budget-tools/server.mjs",
          "permissions": [
            "addons/budget/reports"
          ],
          "toolPolicy": {
            "mode": "read-only",
            "allowDestructive": false
          },
          "configRefs": [],
          "credentialRefs": [],
          "archiveEntry": "budget/mcp/servers/budget-tools/server.mjs",
          "sha256": "e4c8f779bb4dc1a7a813d6fbd2785cc3b15ba211c84679a9229c8d0e683e8c6a",
          "size": 27143,
          "descriptorVersion": 1
        }
      ],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>💰 Budget (Konten/Kategorien/Budgets/wiederkehrend/Charts)</h1>\n<p>Per-User Budgeting: Konten, Kategorien, Budgets, wiederkehrende Transaktionen und<br />Charts (Ausgaben über Zeit, Kategorie-Aufschlüsselung, Budget vs Ist). Integriert<br />lesend mit den trading- + payments-Addons (realisierte PnL / bezahlte Rechnungen<br />fließen in die Reports), ohne diese zu verändern. Lokale SQLite ist die Quelle<br />der Wahrheit (ADR-054).</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>budget</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> budget, accounts, categories, budgets, transactions, recurring, reports, charts</li><li><strong>UI-Seite:</strong> <code>/addons/budget</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials. Die<br />trading/payments-Integration ist optional und aktiviert sich automatisch, sobald<br />die Addons installiert sind.</p>\n<h2>Datenmodell</h2>\n<ul><li><code>budget_accounts</code> — Konten (Name, Typ, Eröffnungsbilanz, Währung)</li><li><code>budget_categories</code> — Kategorien (Name, Gruppe)</li><li><code>budget_budgets</code> — Ausgabenlimits (Kategorie + Periode <code>YYYY-MM</code>, Betrag)</li><li><code>budget_transactions</code> — Einnahmen/Ausgaben (Konto, Kategorie, Betrag)</li><li><code>budget_recurring</code> — Wiederkehrende Regeln (Frequenz, Zeitraum, Betrag)</li></ul>\n<p>Alle Tabellen sind per <code>user_id</code> scoped (ADR-054).</p>",
          "byteSize": 1238
        }
      }
    },
    {
      "id": "business-operator",
      "uuid": "ad6479f7-69ab-4e97-ac02-8c9da48c6e00",
      "name": "Autonomous Business-Operator",
      "version": "1.2.0",
      "description": "An autonomous business operator that pursues configured business goals by running agent-driven cycles which orchestrate the existing money/business addons (dropshipping, seo-content, lead-gen, wordpress) and the workflow-builder through the MC API/gateway. Mandatory guardrails: dry-run by default (propose, don't execute) until explicitly armed; a per-operator addon allow-list; approval-required for any spend/publish/outreach action (a pending-actions queue + approve/reject); optional budget + per-day caps; optional per-user voting/quorum snapshot (#95) so a queued action only proceeds after N eligible voters have approved. It drives the tools — it never re-implements them, never calls external services (ADR-009), and never auto-runs an approval-required action.",
      "author": "Aiopta",
      "icon": "🧭",
      "capabilities": [
        "business-goals",
        "operator-cycles",
        "action-allow-list",
        "approval-queue",
        "spend-day-caps",
        "voting-quorum"
      ],
      "tags": [],
      "download": "/artifacts/business-operator-1.2.0.zip",
      "sha256": "17130cce91c889c4007735dcd6d2765a82e64cf7e749cea75ecc8d60c1a6796e",
      "size": 435620,
      "page": {
        "id": "business-operator-page",
        "path": "/addons/business-operator",
        "name": "Business-Operator",
        "icon": "🧭",
        "section": "Automation",
        "component": "@addons/business-operator/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/business-operator/status",
        "/api/v1/addons/business-operator/operators",
        "/api/v1/addons/business-operator/operators/:id",
        "/api/v1/addons/business-operator/operators/:id/run",
        "/api/v1/addons/business-operator/operators/:id/runs",
        "/api/v1/addons/business-operator/actions",
        "/api/v1/addons/business-operator/actions/:id/approve",
        "/api/v1/addons/business-operator/actions/:id/reject",
        "/api/v1/addons/business-operator/actions/:id/votes",
        "/api/v1/addons/business-operator/actions/:id/quorum",
        "/api/v1/addons/business-operator/widget"
      ],
      "skills": [
        {
          "name": "operator-manage",
          "displayName": "Operator: Define & manage business operators",
          "description": "List/create/edit/delete autonomous business operators (goal + KPI target, addon allow-list, guardrails, dry-run/armed mode). Arming an operator and editing its guardrails requires the addon.manage capability. Pure config — it does not run a cycle.",
          "permissions": [
            "addons/business-operator/operators",
            "addons/business-operator/status"
          ],
          "endpoint": "/api/v1/addons/business-operator/operators",
          "scripts": []
        },
        {
          "name": "operator-run",
          "displayName": "Operator: Run a cycle & inspect history",
          "description": "Run one operator cycle (gather addon state, ask the configured MC agent via the gateway for the next best action constrained to the allow-list, then execute ONLY safe actions when armed and QUEUE every spend/publish/outreach action for human approval) and read the cycle history. A dry-run operator only proposes. Never auto-runs an approval-required action; never touches an addon outside the allow-list.",
          "permissions": [
            "addons/business-operator/operators",
            "addons/business-operator/status"
          ],
          "endpoint": "/api/v1/addons/business-operator/operators/:id/run",
          "scripts": []
        },
        {
          "name": "operator-approvals",
          "displayName": "Operator: Approval inbox (approve/reject)",
          "description": "List the side-effecting actions an operator's cycles have queued and approve or reject them. Approve is the ONLY path that carries out a spend/publish/outreach action — it can never happen automatically — and still runs under the approver's RBAC + the underlying addon's own guardrails. Requires the addon.manage capability.",
          "permissions": [
            "addons/business-operator/actions",
            "addons/business-operator/status"
          ],
          "endpoint": "/api/v1/addons/business-operator/actions",
          "scripts": [
            {
              "id": "operator-approvals-cli",
              "name": "operator-approvals-cli",
              "path": "operator-approvals/cli.sh",
              "sha256": "226fa65bd655e00dce342c98a8f18aa59577cbdf51247532de2215bcfde2279f",
              "interpreter": "bash",
              "timeoutMs": 10000,
              "permissions": [
                "addons/business-operator/actions",
                "addons/business-operator/status"
              ],
              "inputSchema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 20
                  },
                  "timeoutMs": {
                    "type": "integer",
                    "minimum": 100,
                    "maximum": 300000
                  },
                  "input": {
                    "type": "object",
                    "description": "Request-Body als JSON — niemals Secrets"
                  },
                  "dryRun": {
                    "type": "boolean",
                    "default": false,
                    "description": "Request validieren ohne Mutation — kein API-Call"
                  }
                }
              },
              "outputSchema": {
                "type": "object",
                "required": [
                  "ok"
                ],
                "properties": {
                  "ok": {
                    "type": "boolean"
                  },
                  "status": {
                    "type": "integer"
                  },
                  "data": {},
                  "error": {
                    "type": "string"
                  },
                  "truncated": {
                    "type": "boolean"
                  }
                }
              },
              "size": 12147
            }
          ]
        },
        {
          "name": "operator-voting",
          "displayName": "Operator: Vote on a pending action (quorum)",
          "description": "Cast an `approve` or `reject` vote on a queued pending action; the action transitions to `approved`/`rejected` atomically the moment the running tally crosses the queued snapshot's threshold (#95). Each voter may cast at most one vote per action; only the eligibility snapshot (operator owner + the explicit `eligibleVoterIds` from the operator's `guardrails.quorum` block) may vote. Requires the addon.manage capability.",
          "permissions": [
            "addons/business-operator/actions",
            "addons/business-operator/status"
          ],
          "endpoint": "/api/v1/addons/business-operator/actions/:id/votes",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Autonomous Business-Operator</h1>\n<p>The higher-level <strong>brain</strong> that pursues configured business <strong>goals</strong> by running agent-driven **operator<br />cycles*<em> which orchestrate the </em>existing* money/business addons (dropshipping, seo-content, lead-gen,<br />wordpress) and the workflow-builder (#45) through the Aiopta API/gateway.</p>\n<p>It <strong>drives</strong> those tools — it never re-implements them and <strong>never calls external services directly</strong><br />(ADR-009). Every action is an Aiopta-internal <code>/api/v1/addons/...</code> call (same-origin guarded); planning goes<br />through the Aiopta gateway.</p>\n<h2>Model</h2>\n<ul><li><strong>Operator</strong> = <code>{ id, name, goal, target, status, mode, addons[], guardrails, agentId?, schedule? }</code>.</li><li><strong>goal</strong> is free-text; <strong>target</strong> is a small KPI <code>{ kpi?, target?, current? }</code>.</li><li><strong>mode</strong> ∈ <code>dry-run</code> (default) | <code>armed</code>; <strong>status</strong> ∈ <code>idle</code> | <code>running</code> | <code>paused</code>.</li><li><strong>addons[]</strong> is the <strong>allow-list</strong> — the operator may orchestrate ONLY these.</li><li><strong>guardrails</strong> = <code>{ maxActionsPerCycle (≤10), maxActionsPerDay?, budgetCapCents? }</code>.</li></ul>\n<p>A <strong>cycle</strong> (<code>POST …/operators/{id}/run</code>) = gather state from the allow-listed addons → ask the<br />configured Aiopta agent for the next best action constrained to the allow-list → apply guardrails → record.</p>\n<h2>Safety (the point of this addon)</h2>\n<p>- <strong>Dry-run by default</strong> — a new operator only <strong>proposes</strong>; it runs NOTHING until explicitly <strong>armed</strong>.<br />- <strong>Per-operator allow-list</strong> — an action for an addon outside <code>addons[]</code> is <strong>refused</strong>, never run.<br />- <strong>Approval-required for side-effects</strong> — every <strong>spend / publish / outreach</strong> action is <strong>queued</strong> as a<br />  pending action and is <strong>never auto-run, even when armed</strong>. Only a human <strong>approve</strong> carries it out — and<br />  it still runs under the approver&#39;s RBAC + the target addon&#39;s own guardrails (e.g. lead-gen&#39;s anti-spam<br />  <code>confirm</code>/suppression).<br />- <strong>Budget + per-day + per-cycle caps</strong> bound an armed operator.<br />- <strong>Same-origin only / no eval</strong> — <code>assertSameOrigin</code> re-checks every call; nothing is ever <code>eval</code>&#39;d.<br />- <strong>Capability-gated</strong> — create/edit/<strong>arm</strong>/run/approve require the <code>addon.manage</code> RBAC capability.</p>\n<h2>Approval notifications (C7, ADR-035)</h2>\n<p>Optionally, every <strong>newly-queued</strong> approval can ALSO be <strong>pushed</strong> through the <code>notifications</code> addon<br />(Discord/email/push/webhook) — <strong>in addition to</strong> the in-UI approvals inbox, never replacing it.</p>\n<p>- <strong>Opt-in, default OFF.</strong> Per operator: <code>notify = { enabled, channelId? }</code>. Disabled/unconfigured ⇒<br />  behaviour is unchanged (only the UI queue). When enabled, an optional <code>channelId</code> targets one channel;<br />  omitted ⇒ broadcast to all enabled channels.<br />- <strong>Aiopta-API only (ADR-009).</strong> The bridge (<code>src/lib/notify-bridge.ts</code>) POSTs to the Aiopta-internal<br />  <code>POST /api/v1/addons/notifications/send</code> (<code>assertSameOrigin</code> + a canonical-origin check, forwarded auth)<br />  — it <strong>never</strong> calls Discord/email/an external host directly; the notifications addon owns the channel<br />  credentials.<br />- <strong>Best-effort &amp; idempotent.</strong> Fired <strong>once</strong> per pending action at queue time (not every cycle); a<br />  notify failure <strong>never blocks/breaks the cycle</strong> and <strong>never weakens the approval gate</strong> (approval still<br />  needs the human <code>approve</code>). The message carries the operator name, the action + risk, and a <strong>deep-link</strong><br />  to the approvals inbox. No secret/webhook URL is ever built or logged.<br />- <strong>UI.</strong> A <strong>&quot;Freigaben auch per Benachrichtigung&quot;</strong> toggle + channel picker in the operator editor.</p>\n<h2>Persistence</h2>\n<p>Three addon-owned tables (created in <code>install()</code>, dropped in <code>uninstall()</code> via Aiopta core&#39;s <code>getDb()</code>):<br /><code>business_operator_operators</code>, <code>business_operator_runs</code>, <code>business_operator_pending_actions</code> (the approval<br />queue). The addon never touches the core schema; no secret is stored.</p>\n<h2>UI</h2>\n<p>A control panel: operator list, a per-operator editor (goal/KPI, <strong>allow-list checkboxes</strong> with per-action<br />risk, <strong>guardrails</strong>, <strong>Arm/Pause</strong> toggle, <strong>Run cycle</strong>), a <strong>plan/activity log</strong>, and a<br /><strong>pending-approvals inbox</strong> (approve/reject). The dry-run/armed state and approval gates are made visually<br />unmistakable.</p>\n<p>Durable auto-scheduling of cycles and a fully-autonomous (no-approval) spend mode are parked owner/risk<br />decisions — see <code>docs/OPEN-DECISIONS.md</code>.</p>\n<p>See <code>SKILL.md</code> for the agent-facing endpoint cheat-sheet and<br /><code>docs/architecture/ADR-025-business-operator.md</code> for the design.</p>",
          "byteSize": 4321
        }
      }
    },
    {
      "id": "calendar",
      "uuid": "fc94a478-e5d9-46c8-93ef-d7dd856e8fbb",
      "name": "Calendar",
      "version": "1.1.3",
      "description": "Calendar UI and calendar integration API as an installable addon.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "calendar",
        "scheduling"
      ],
      "tags": [],
      "download": "/artifacts/calendar-1.1.3.zip",
      "sha256": "7a0a44b0fc261413901dd9ad0383596511d0fd5a4607ea37cb52db9828cbea49",
      "size": 111941,
      "page": {
        "id": "calendar",
        "path": "/addons/calendar",
        "name": "Calendar",
        "icon": "📅",
        "section": "Work",
        "component": "@addons/calendar/src/components/calendar/CalendarView"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/calendar"
      ],
      "skills": [],
      "mcpServers": [
        {
          "id": "calendar-tools",
          "displayName": "Calendar tools",
          "description": "Read-only calendar domain tools for agents: filter events by date/range, expand cron schedules into concrete dates, month bounds and event summaries — the same semantics as the calendar addon's own store/route logic (issue #407, epic #399).",
          "transport": "stdio",
          "runtime": "node",
          "entry": "calendar-tools/server.mjs",
          "permissions": [
            "addons/calendar/read"
          ],
          "toolPolicy": {
            "mode": "read-only",
            "allowDestructive": false
          },
          "configRefs": [],
          "credentialRefs": [],
          "archiveEntry": "calendar/mcp/servers/calendar-tools/server.mjs",
          "sha256": "20c71786b79e4057ea92f7fd1525c248d8ee96aab632fdaf350c81ae5cbc6710",
          "size": 15864,
          "descriptorVersion": 1
        }
      ],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Calendar</h1>\n<p>Calendar UI and calendar integration API as an installable addon.</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>calendar</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> calendar, scheduling</li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET, POST | <code>/api/v1/addons/calendar</code> |<br />| GET | <code>/api/v1/addons/calendar/status</code> |</p>\n<h2>Sicherheit — Pro-Benutzer-Isolation (#142 / INBOX-59, ADR-054)</h2>\n<p>Manuell erstellte Kalender-Events sind <strong>pro Benutzer</strong> isoliert: sie liegen in der Tabelle<br /><code>calendar_events</code> mit <code>user_id NOT NULL</code> und werden ausschließlich über <code>scopedStore(auth.id)</code> gelesen/<br />geschrieben (Owner server-seitig aus dem authentifizierten Aufrufer gestempelt, ein Client-Owner wird<br />ignoriert). Ein Benutzer sieht und erstellt nur seine <strong>eigenen</strong> Events. Die daneben angezeigten<br />Aufgaben- (<code>project_tasks</code>), Backup-Zeitplan- (<code>backup_schedules</code>) und verbundenen Kalender-Einträge<br />(<code>integrations</code>) sind <strong>schreibgeschützte Projektionen</strong> der Daten anderer Addons und werden hier nicht<br />neu partitioniert. Eine einmalige idempotente Migration hebt Alt-Events aus dem <code>addon_settings</code>-JSON-Blob<br />in die neue Tabelle (Owner = der Benutzer der Settings-Zeile).</p>\n<h2>Bekannte Grenzen</h2>\n<ul><li>Keine bekannten Einschränkungen.</li></ul>",
          "byteSize": 1314
        }
      }
    },
    {
      "id": "chat",
      "uuid": "539ae329-7bed-4281-a007-1b1ca5c5c3f7",
      "name": "Chat",
      "version": "1.17.1",
      "description": "Agent chat sessions, messages, streaming responses, agent selection, group chat, plus chat I/O: file upload (+drag&drop), voice-to-text and agent file-returns (download + preview, mirrored to artefakt-workspace), a responsive owner-scoped Kontext-Sidebar for session Ausgaben (artifacts) and Quellen (sources), and a versioned server-authoritative subagent/task status stream (snapshot + monotone deltas) reconciled by an idempotent client reducer.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "chat",
        "sessions",
        "messages",
        "streaming",
        "groups",
        "attachments",
        "voice-input",
        "artifacts"
      ],
      "tags": [
        "onboarding"
      ],
      "download": "/artifacts/chat-1.17.1.zip",
      "sha256": "b8b201340a3b5d694ef72f5b995c03a858d7b1b1bff06be06c60474d8f6dac80",
      "size": 1306079,
      "page": {
        "id": "chat",
        "path": "/addons/chat",
        "name": "Chat",
        "icon": "💬",
        "section": "Agents",
        "component": "@addons/chat/src/components/chat/ChatPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/chat/agents",
        "/api/v1/addons/chat/sessions",
        "/api/v1/addons/chat/sessions/{id}",
        "/api/v1/addons/chat/sessions/{id}/messages",
        "/api/v1/addons/chat/sessions/{id}/stream",
        "/api/v1/addons/chat/sessions/{id}/compact",
        "/api/v1/addons/chat/sessions/{id}/attachments",
        "/api/v1/addons/chat/sessions/{id}/attachments/{attachmentId}/serve",
        "/api/v1/addons/chat/sessions/{id}/artifacts",
        "/api/v1/addons/chat/sessions/{id}/artifacts/{artifactId}/serve",
        "/api/v1/addons/chat/sessions/{id}/sources",
        "/api/v1/addons/chat/stt-status",
        "/api/v1/addons/chat/plan-limits",
        "/api/v1/addons/chat/groups",
        "/api/v1/addons/chat/groups/{id}/members",
        "/api/v1/addons/chat/groups/{id}/members/{memberId}",
        "/api/v1/addons/chat/groups/{id}/messages",
        "/api/v1/addons/chat/sessions/{id}/browser",
        "/api/v1/addons/chat/sessions/{id}/browser/navigate"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Chat</h1>\n<p>Agent chat sessions, messages, streaming responses, chat agent selection — plus a<br /><strong>model-independent slash-command system</strong>, a full-width layout with a context sidebar, and in-app<br />deep-links (ADR-011, Phase U Block 4 / G10).</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>chat</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> chat, sessions, messages, streaming</li><li><strong>UI-Seite:</strong> <code>/chat</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials.</p>\n<h2>Slash-Commands</h2>\n<p>Eine Nachricht, die mit <code>/</code> beginnt, ist ein <strong>Command</strong> und <strong>kein Prompt</strong>. Commands werden<br />client-seitig abgefangen und ausgeführt — sie erreichen <strong>nie</strong> das Modell und funktionieren auch<br />dann, wenn kein Modell/Backend erreichbar ist (modellunabhängige Interception, ADR-011).</p>\n<p>Beim Tippen von <code>/</code> öffnet sich ein Autocomplete-Popover (↑/↓ wählen, Enter/Tab übernehmen, Esc<br />schließen). Unbekannte <code>/x</code>-Eingaben zeigen einen Inline-Hinweis mit Vorschlägen — ebenfalls ohne<br />Modell-Aufruf.</p>\n<p>| Command | Beschreibung | Offline |<br />|---|---|---|<br />| <code>/help</code> (<code>/commands</code>) | Alle Commands auflisten | ✅ |<br />| <code>/model [provider/model]</code> | Modell anzeigen / für die Session wechseln (Provider, Status, Fallbacks) | — |<br />| <code>/skills [toggle &lt;name&gt;]</code> | Skills des aktiven Agenten auflisten / (de)aktivieren | — |<br />| <code>/mcp</code> | MCP-Server &amp; Tools auflisten | — |<br />| <code>/tools</code> | Für den Agent-Key freigegebene Tools/Endpunkte | — |<br />| <code>/permissions</code> (<code>/perms</code>) | Allow/Deny-Patterns des Agenten | — |<br />| <code>/agent [name]</code> | Aktiven Agenten anzeigen / (neue Session mit) Agent wechseln | ✅ |<br />| <code>/system</code> (<code>/soul</code>) | System-Prompt / Soul des Agenten anzeigen | — |<br />| <code>/compact</code> (<code>/summarize</code>) | Konversation zusammenfassen/komprimieren | — |<br />| <code>/memory</code> | Memory-Status anzeigen | — |<br />| <code>/clear</code> (<code>/new</code>, <code>/reset</code>) | Session zurücksetzen / neuen Chat | ✅ |<br />| <code>/retry</code> (<code>/regenerate</code>) | Letzte Nachricht erneut senden | ✅ |<br />| <code>/stop</code> (<code>/abort</code>, <code>/cancel</code>) | Laufende Antwort abbrechen | ✅ |<br />| <code>/export</code> | Chat als Markdown exportieren | ✅ |</p>\n<p>Die Registry ist erweiterbar: <code>registerSlashCommand(spec)</code> in<br /><code>addons/chat/src/lib/slash-commands.ts</code>. Der Parser <code>parseSlashCommand()</code> ist rein und unit-getestet<br />(<code>addons/chat/tests/unit/slash-commands.test.ts</code>).</p>\n<h2>Layout &amp; Deep-Links</h2>\n<p>- <strong>Volle Breite</strong>: Die Nachrichtenspalte hat eine großzügige Lese-Maximalbreite, der Chat nutzt die<br />  volle verfügbare Breite. Auf Desktop gibt es eine <strong>rechte Kontext-Sidebar</strong> (Agent,<br />  Workspace-Status, Modell/Backend, Token/Kontext/Kosten, Quota, freigegebene Skills, laufende<br />  Commands — DEVELOPMENT.md §7), auf Mobile ein <strong>Drawer</strong>.<br />- <strong>Deep-Links</strong>: Interne Links in Chat-/Fehler-/Empty-States öffnen <strong>keinen neuen Tab</strong> und laden<br />  die App <strong>nicht neu</strong>, sondern springen über den Client-Router direkt zur Zielsektion (z. B.<br />  <code>/admin?section=providers</code>). Helfer: <code>addons/chat/src/lib/deep-links.ts</code>.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET | <code>/api/v1/addons/chat/agents</code> |<br />| GET | <code>/api/v1/addons/chat/plan-limits</code> |<br />| GET, POST | <code>/api/v1/addons/chat/sessions</code> |<br />| DELETE | <code>/api/v1/addons/chat/sessions/[id]</code> |<br />| GET, POST | <code>/api/v1/addons/chat/sessions/[id]/messages</code> |<br />| POST | <code>/api/v1/addons/chat/sessions/[id]/stream</code> |<br />| POST | <code>/api/v1/addons/chat/sessions/[id]/compact</code> |<br />| GET, POST | <code>/api/v1/addons/chat/groups</code> |<br />| GET, POST | <code>/api/v1/addons/chat/groups/[id]/members</code> |<br />| DELETE | <code>/api/v1/addons/chat/groups/[id]/members/[memberId]</code> |<br />| GET, POST | <code>/api/v1/addons/chat/groups/[id]/messages</code> |</p>\n<blockquote><p>Die Core-Routen unter <code>/api/v1/chat/...</code> sind dünne Proxies, die diese Addon-Handler</p><p>re-exportieren; die UI ruft die Core-Pfade auf.</p></blockquote>\n<h2>Gruppen-Chats (#147 / INBOX-70, ADR-057)</h2>\n<p>Ein <strong>Gruppen-Chat</strong> ist eine Session mit einer <strong>Mitglieder-Liste</strong>: mehrere<br /><code>user_id</code>s + mehrere <code>agent_id</code>s teilen eine Konversation. Additiv zum 1:1-Chat — die<br />strikte 1:1-Isolation (#148 / ADR-054) bleibt <strong>unverändert</strong>.</p>\n<p>- <strong>Datenmodell:</strong> neue Addon-Tabelle <code>chat_session_members</code><br />  (<code>session_id</code>, <code>member_type</code> user|agent, <code>member_id</code>, <code>role</code> owner|member, <code>added_by</code>).<br />  Die Session bleibt im Besitz von <code>user_sessions.user_id</code> (dem Ersteller). Nachrichten<br />  bleiben per <code>session_id</code>-FK an der Session — kein neues Owner-Feld pro Nachricht.<br />- <strong>Zugriff = Eigentümer ODER Nutzer-Mitglied</strong> (spiegelt das task-board-Muster exakt):<br />  <code>getAccessibleSession(userId, id)</code> liefert die Session nur für Eigentümer <strong>oder</strong> ein<br />  eingetragenes Nutzer-Mitglied; ein Nicht-Mitglied bekommt <strong>404</strong> (nicht 403, kein<br />  Existenz-Leak, <strong>kein Admin-Bypass</strong>). Eine 1:1-Session hat <strong>keine</strong> Mitglieder-Zeilen,<br />  daher reduziert sich <code>getAccessibleSession</code> dort auf den Eigentümer-Check<br />  (<code>getOwnedSession</code>) — Gruppen können eine 1:1-Session <strong>nie</strong> aufweiten. Die bestehenden<br />  <code>sessions/[id]/*</code>-Routen nutzen weiter <code>getOwnedSession</code>.<br />- <strong>@-Mention-Routing:</strong> In einer Gruppe antwortet ein Agent <strong>nur</strong>, wenn er per<br />  <code>@Name</code> erwähnt wird (mehrere erwähnte Agenten antworten je einmal, in Roster-Reihenfolge).<br />  <strong>Keine Erwähnung ⇒ kein Agent antwortet</strong> (verhindert N-Agenten-Sturm). Quoting für<br />  Mehrwort-Namen: <code>@&quot;Ada Lovelace&quot;</code>. Der mentionierte Agent wird wie heute über das Gateway<br />  invoziert. Ein zwischenzeitlich entferntes Agent-Mitglied antwortet auch bei Erwähnung<br />  nicht (Mitgliedschaft wird vor dem Senden erneut geprüft).<br />- <strong>Permission-gated:</strong> <strong>Nur der Eigentümer</strong> darf Mitglieder hinzufügen/entfernen<br />  (Mitglied-aber-nicht-Eigentümer ⇒ 403, Fremder ⇒ 404). Einen <strong>Agenten</strong> einzuladen<br />  erfordert, dass der Eigentümer diesen Agenten <strong>besitzt</strong> (cross-user Agent-ID<br />  machtlos — derselbe Check wie <code>createSession</code>). Agent-Antworten respektieren die<br />  Rechte-Obergrenze Eigentümer ∩ Agent (<code>effectiveAgentCapabilities</code>, ADR-055): der Agent<br />  wurde vom Eigentümer eingeladen, der ihn besitzt, also ist seine Cap-Obergrenze durch den<br />  Eigentümer begrenzt.<br />- <strong>Evil-User-Tests:</strong> <code>addons/chat/tests/unit/group-chat-isolation.test.ts</code> (reale<br />  in-memory better-sqlite3, User A/B/C, Agenten/Gateway gemockt) + die reine<br />  <code>mentions.test.ts</code>.</p>\n<h2>Per-User-Isolation (#148 / ADR-054)</h2>\n<p>Chat-Sessions + Nachrichten sind <strong>strikt pro Nutzer isoliert</strong>. Die „Session&quot; ist<br />die Core-Tabelle <code>user_sessions</code> (<code>user_id NOT NULL</code>); Nachrichten (<code>chat_messages</code>)<br />hängen per <code>session_id</code>-FK an der Session. Sämtlicher Zugriff läuft über<br /><code>addons/chat/src/lib/chat-store.ts</code>, das <code>user_sessions</code> über die Scope-Layer<br />(<code>scopedStore</code>/<code>registerScopedTable</code>) führt.</p>\n<p>- <strong>Server-stamped Owner:</strong> Beim Anlegen wird <code>user_id = auth.id</code> gesetzt; ein vom<br />  Client mitgeschickter Owner wird ignoriert. Eine Session kann nur an <strong>eigene</strong><br />  Agenten gebunden werden.<br />- <strong>404 statt 403, kein Admin-Bypass:</strong> Jede <code>sessions/[id]/*</code>-Route<br />  (route/messages/compact/stream) lädt die Session via <code>getOwnedSession(auth.id, id)</code>.<br />  Gehört sie einem anderen Nutzer (oder existiert nicht), kommt <strong>404</strong> zurück —<br />  nicht der Inhalt, nicht 403, und <strong>auch ein Admin</strong> liest/streamt/komprimiert die<br />  fremde Konversation nicht. Bei <code>stream</code> greift die 404 <strong>bevor</strong> der SSE-Stream<br />  öffnet, bei <code>messages</code>/<code>compact</code> <strong>bevor</strong> irgendein Modell-Call passiert.<br />- <strong>gateway/LLM-Routing unberührt:</strong> Nur die Session/Message-Persistenz ist<br />  isoliert; Streaming + Compaction bleiben für den Eigentümer voll funktional.<br />- <code>agents</code> ist auf die eigenen Agenten gescoped; <code>plan-limits</code> liest<br />  instanzweite Provider-Limits (kein Per-User-Geheimnis).</p>\n<h2>Backend-Agent-Pfad, kein Passthrough (#550)</h2>\n<p>Jede Chat-Nachricht läuft <strong>durch den echten Backend-Agent-Pfad</strong><br />(hermes/openclaw) — niemals als roher Passthrough an ein LLM. Die Stream-Route<br />(<code>sessions/[id]/stream/route.ts</code>) ruft den Agenten-Gateway<br /><code>chatCompletionWithFallbacks(modelCandidates, opts, { backendId })</code> auf: Sie löst<br />die Modell-Fallback-Kette des Agenten auf (Primary zuerst), baut den SOUL-Prompt<br />und übergibt den Turn an den aktiven Backend-Driver. Selbst ohne explizit<br />aktiven Backend fällt das Gateway auf den verwalteten Default-Driver (openclaw)<br />zurück — ein Turn wird <strong>nie</strong> silent zu einem raw Provider-Call.</p>\n<p>- <strong>Verifizierbare Evidenz pro Turn:</strong> <code>addons/chat/src/lib/chat-agent-trace.ts</code><br />  baut einen reinen Trace-Record (backend, agent, model, candidates, tools, via)<br />  und schreibt <strong>eine</strong> grep-bare <code>[chat:agent-path]</code>-Zeile pro Turn ins Core-Log<br />  (<code>tail</code> zeigt backend + agent + model + tools + via für jede Nachricht). Zusätzlich<br />  emittiert die Route ein <code>trace</code>-SSE-Event mit demselben Record.<br />- <strong><code>via</code> ist das Evidence-Flag:</strong> <code>&quot;backend&quot;</code> = der gebundene Backend-Driver<br />  hat geantwortet (echter Backend-Agent); <code>&quot;provider&quot;</code> = ein explizit gewählter<br />  roher Provider (aufgezeichnet, nicht silent); <code>&quot;fallback&quot;</code>/<code>&quot;unknown&quot;</code> = Degradation.<br />- <strong>UI-Transparenz:</strong> Der Chat-Header zeigt <code>via &lt;backend&gt; · &lt;model&gt; · &lt;via&gt; · 🛠N</code><br />  (Tools); der Standalone-Hook <code>useChatStream</code> exponiert <code>activeBackend</code>,<br />  <code>activeModel</code> und <code>agentPath</code>.<br />- <strong>Per-User-Isolation unberührt:</strong> Der Trace trägt die <code>userId</code> des Eigentümers;<br />  der gesamte Pfad bleibt durch <code>getOwnedSession</code> scope-gate-geschützt.</p>\n<p>Tests: <code>addons/chat/tests/unit/chat-agent-trace.test.ts</code> (reine Trace-Shape- &amp;<br />Log-Evidenz) + <code>addons/chat/tests/unit/chat-agent-path.test.ts</code> (Routing durch<br />den Agenten-Gateway mit Backend-Binding, Trace-Emission, Per-User-404).</p>\n<h2>Bekannte Grenzen</h2>\n<p>- <code>/compact</code> ist modellabhängig (Zusammenfassung): bei nicht erreichbarem Modell kommt ein 503 und<br />  der Command zeigt einen Hinweis.<br />- Der Markdown-Renderer ist absichtlich minimal (Code-Blöcke, Inline-Code, Fett/Kursiv, Listen,<br />  Links); kein vollständiger Markdown-Parser.</p>",
          "byteSize": 9669
        }
      }
    },
    {
      "id": "comfyui-web-app",
      "uuid": "402c8853-030a-40b4-b78d-f98a9a51e7db",
      "name": "ComfyUI Web App",
      "version": "1.0.1",
      "description": "Aiopta addon wrapper for the ComfyUI Web App with embedded UI, connection config, and passthrough API endpoints.",
      "author": "Aiopta",
      "icon": "🎛️",
      "capabilities": [
        "comfyui-embed",
        "workflow-management",
        "job-execution",
        "api-proxy"
      ],
      "tags": [],
      "download": "/artifacts/comfyui-web-app-1.0.1.zip",
      "sha256": "4fbcf898b845f3eddca86ce63a66f478b2a9534201cd941c27c237b3b72ca904",
      "size": 38424,
      "page": {
        "id": "comfyui-web-app",
        "name": "ComfyUI Web App",
        "path": "/addons/comfyui-web-app",
        "icon": "🎛️",
        "section": "Media",
        "component": "@addons/comfyui-web-app/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/comfyui-web-app/config",
        "/api/v1/addons/comfyui-web-app/status",
        "/api/v1/addons/comfyui-web-app/workflows",
        "/api/v1/addons/comfyui-web-app/jobs",
        "/api/v1/addons/comfyui-web-app/execute"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>🎛️ ComfyUI Web App</h1>\n<p>Aiopta addon wrapper for the ComfyUI Web App with embedded UI, connection config, and passthrough API endpoints.</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>comfyui-web-app</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> comfyui-embed, workflow-management, job-execution, api-proxy</li><li><strong>UI-Seite:</strong> <code>/addons/comfyui-web-app</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET, POST | <code>/api/v1/addons/comfyui-web-app/config</code> |<br />| POST | <code>/api/v1/addons/comfyui-web-app/execute</code> |<br />| GET | <code>/api/v1/addons/comfyui-web-app/jobs</code> |<br />| GET | <code>/api/v1/addons/comfyui-web-app/status</code> |<br />| GET | <code>/api/v1/addons/comfyui-web-app/workflows</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<ul><li>Keine bekannten Einschränkungen.</li></ul>\n<blockquote><p>README aus <code>addon.json</code> + vorhandenen Routen generiert (<code>scripts/generate-addon-readmes.js</code>). Bei manuellen Ergänzungen den Generator-Hinweis entfernen.</p></blockquote>",
          "byteSize": 951
        }
      }
    },
    {
      "id": "commerce",
      "uuid": "5ba1c6f2-d305-4007-a736-3e5d6595ef70",
      "name": "Commerce",
      "version": "0.6.1",
      "description": "Commerce consolidation hub (Slices 2–5): the canonical connector framework + shared Order/Inventory model that the channel addons migrate into. Shopify (Slice 2, #588), Etsy (Slice 3, #589), Dropshipping/POD (Slice 4, #590) and Marketplace-Automation (Slice 5, #591) are integrated as connectors. Each connector is per-user BYOK (ADR-054) with its own credentials; sync/fulfillment is always an explicit call. External SaaS APIs — no container.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "commerce",
        "commerce-connector-framework",
        "commerce-connectors",
        "commerce-sync",
        "commerce-shopify",
        "commerce-etsy",
        "commerce-dropshipping",
        "commerce-dropshipping-margins",
        "commerce-pod-printful",
        "commerce-pod-printify",
        "commerce-marketplace",
        "commerce-marketplace-accounts",
        "commerce-marketplace-listings",
        "commerce-marketplace-publish"
      ],
      "tags": [],
      "download": "/artifacts/commerce-0.6.1.zip",
      "sha256": "821b2ff470f1118558e1453dcc4a23f8a7b6dc8db71473e020d230582b962eba",
      "size": 380429,
      "page": {
        "id": "commerce-page",
        "path": "/addons/commerce",
        "name": "Commerce",
        "icon": "🛒",
        "section": "Money",
        "component": "@addons/commerce/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/commerce/status",
        "/api/v1/addons/commerce/connectors",
        "/api/v1/addons/commerce/shopify/status",
        "/api/v1/addons/commerce/shopify/config",
        "/api/v1/addons/commerce/shopify/products",
        "/api/v1/addons/commerce/shopify/stock",
        "/api/v1/addons/commerce/shopify/orders",
        "/api/v1/addons/commerce/shopify/sync",
        "/api/v1/addons/commerce/etsy/status",
        "/api/v1/addons/commerce/etsy/config",
        "/api/v1/addons/commerce/etsy/products",
        "/api/v1/addons/commerce/etsy/stock",
        "/api/v1/addons/commerce/etsy/orders",
        "/api/v1/addons/commerce/etsy/sync",
        "/api/v1/addons/commerce/dropshipping/status",
        "/api/v1/addons/commerce/dropshipping/config",
        "/api/v1/addons/commerce/dropshipping/products",
        "/api/v1/addons/commerce/dropshipping/orders",
        "/api/v1/addons/commerce/dropshipping/orders/:id",
        "/api/v1/addons/commerce/dropshipping/orders/:id/fulfill",
        "/api/v1/addons/commerce/dropshipping/fulfillments",
        "/api/v1/addons/commerce/dropshipping/margins",
        "/api/v1/addons/commerce/dropshipping/sync",
        "/api/v1/addons/commerce/marketplace/status",
        "/api/v1/addons/commerce/marketplace/accounts",
        "/api/v1/addons/commerce/marketplace/accounts/:id",
        "/api/v1/addons/commerce/marketplace/listings",
        "/api/v1/addons/commerce/marketplace/listings/:id",
        "/api/v1/addons/commerce/marketplace/listings/:id/publish",
        "/api/v1/addons/commerce/marketplace/jobs"
      ],
      "skills": [
        {
          "name": "commerce-status",
          "displayName": "Commerce: Connector overview",
          "description": "List the registered commerce connectors and which the caller has configured (BYOK).",
          "permissions": [
            "addons/commerce/read"
          ],
          "endpoint": "/api/v1/addons/commerce/status",
          "scripts": []
        },
        {
          "name": "commerce-shopify-sync",
          "displayName": "Commerce/Shopify: Sync products / stock / orders",
          "description": "Trigger an explicit sync of the caller's Shopify shop (products, stock and orders) via the Admin API using the user's BYOK access token. Migrated from standalone shopify addon (Slice 2, #588).",
          "permissions": [
            "addons/commerce/shopify/sync",
            "addons/commerce/shopify/products",
            "addons/commerce/shopify/orders"
          ],
          "endpoint": "/api/v1/addons/commerce/shopify/sync",
          "scripts": []
        },
        {
          "name": "commerce-shopify-products",
          "displayName": "Commerce/Shopify: Products & Stock",
          "description": "List the caller's cached Shopify products and stock levels, or write back a stock level to Shopify. Migrated from standalone shopify addon (Slice 2, #588).",
          "permissions": [
            "addons/commerce/shopify/products",
            "addons/commerce/shopify/stock"
          ],
          "endpoint": "/api/v1/addons/commerce/shopify/products",
          "scripts": []
        },
        {
          "name": "commerce-shopify-orders",
          "displayName": "Commerce/Shopify: Orders",
          "description": "List the caller's cached Shopify orders. Migrated from standalone shopify addon (Slice 2, #588).",
          "permissions": [
            "addons/commerce/shopify/orders"
          ],
          "endpoint": "/api/v1/addons/commerce/shopify/orders",
          "scripts": []
        },
        {
          "name": "commerce-etsy-sync",
          "displayName": "Commerce/Etsy: Sync listings / stock / orders",
          "description": "Trigger an explicit sync of the caller's Etsy shop (listings, stock and orders/receipts) via the OpenAPI v3 API. Migrated from standalone etsy addon (Slice 3, #589).",
          "permissions": [
            "addons/commerce/etsy/sync",
            "addons/commerce/etsy/products",
            "addons/commerce/etsy/orders"
          ],
          "endpoint": "/api/v1/addons/commerce/etsy/sync",
          "scripts": []
        },
        {
          "name": "commerce-etsy-products",
          "displayName": "Commerce/Etsy: Listings & Stock",
          "description": "List the caller's cached Etsy listings and stock levels, or write back a stock level to Etsy. Migrated from standalone etsy addon (Slice 3, #589).",
          "permissions": [
            "addons/commerce/etsy/products",
            "addons/commerce/etsy/stock"
          ],
          "endpoint": "/api/v1/addons/commerce/etsy/products",
          "scripts": []
        },
        {
          "name": "commerce-etsy-orders",
          "displayName": "Commerce/Etsy: Orders",
          "description": "List the caller's cached Etsy orders/receipts. Migrated from standalone etsy addon (Slice 3, #589).",
          "permissions": [
            "addons/commerce/etsy/orders"
          ],
          "endpoint": "/api/v1/addons/commerce/etsy/orders",
          "scripts": []
        },
        {
          "name": "commerce-dropshipping-catalog",
          "displayName": "Commerce/Dropshipping: Catalog & Import",
          "description": "List Printful/Printify catalog products (with margin-applied retail prices) and import one into the shop. Migrated from standalone dropshipping addon (Slice 4, #590).",
          "permissions": [
            "addons/commerce/dropshipping/products",
            "addons/commerce/dropshipping/status"
          ],
          "endpoint": "/api/v1/addons/commerce/dropshipping/products",
          "scripts": []
        },
        {
          "name": "commerce-dropshipping-orders",
          "displayName": "Commerce/Dropshipping: Orders & Fulfillment",
          "description": "List/create draft orders and explicitly fulfill them (forward to the supplier for production). Fulfillment moves money and is never implicit. Migrated from standalone dropshipping addon (Slice 4, #590).",
          "permissions": [
            "addons/commerce/dropshipping/orders",
            "addons/commerce/dropshipping/fulfillment"
          ],
          "endpoint": "/api/v1/addons/commerce/dropshipping/orders",
          "scripts": []
        },
        {
          "name": "commerce-dropshipping-margins",
          "displayName": "Commerce/Dropshipping: Margin Rules",
          "description": "Read or set per-user margin rules (percent + fixed, charm-rounding) that turn supplier base cost into retail price. Migrated from standalone dropshipping addon (Slice 4, #590).",
          "permissions": [
            "addons/commerce/dropshipping/margins"
          ],
          "endpoint": "/api/v1/addons/commerce/dropshipping/margins",
          "scripts": []
        },
        {
          "name": "commerce-marketplace-accounts",
          "displayName": "Commerce/Marketplace: Accounts",
          "description": "List, create, update and delete the caller's connected marketplace accounts (Kleinanzeigen, eBay, Vinted). Migrated from standalone marketplace-automation addon (Slice 5, #591).",
          "permissions": [
            "addons/commerce/marketplace/accounts"
          ],
          "endpoint": "/api/v1/addons/commerce/marketplace/accounts",
          "scripts": []
        },
        {
          "name": "commerce-marketplace-listings",
          "displayName": "Commerce/Marketplace: Listings",
          "description": "List, create and update listing drafts for marketplace publication. Migrated from standalone marketplace-automation addon (Slice 5, #591).",
          "permissions": [
            "addons/commerce/marketplace/listings"
          ],
          "endpoint": "/api/v1/addons/commerce/marketplace/listings",
          "scripts": []
        },
        {
          "name": "commerce-marketplace-publish",
          "displayName": "Commerce/Marketplace: Publish & Jobs",
          "description": "Trigger an explicit publish of a listing to its target providers and track publish jobs. Publish is always an explicit call (ADR-013). Migrated from standalone marketplace-automation addon (Slice 5, #591).",
          "permissions": [
            "addons/commerce/marketplace/publish",
            "addons/commerce/marketplace/jobs"
          ],
          "endpoint": "/api/v1/addons/commerce/marketplace/jobs",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": null
    },
    {
      "id": "container-orchestrator",
      "uuid": "d4e8ceba-d31e-4b2f-aa0b-f7ae3f23b557",
      "name": "Container Orchestrator (Lifecycle + Resource Limits + Health + Ephemeral Leases + Prometheus Metrics)",
      "version": "0.8.3",
      "description": "Aiopta Container Orchestrator — slices 1+2+3+4 of issues #354/#355/#356/#357. Slice 1/4 establishes the public API schema (ContainerRequest, ContainerStatus, ContainerAction), the pure-function action handlers (start/stop/restart with drift protection + state-transition matrix), and the request-shape validator. Slice 2/4 wires the docker-manager adapter behind the same seam (fake adapter for tests, real adapter behind feature flag) and adds the scoped operation store, drift detection, audit log, and preview. Slice 3/4 adds resource-limits (CPU/RAM/PIDs/Disk) + health monitoring (HTTP/TCP/exec probes) + cursor-paginated metrics + integration glue. Slice 4/4 adds the ephemeral-agent-container surface: lease-model + GC + crash-recovery + idempotent renew/release. Cross-user lookups map to 404 (no enumeration leak). The orchestrator does NOT expose delete/prune/exec/image-pull — those remain in the underlying docker-manager addon.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "container-lifecycle",
        "container-start",
        "container-stop",
        "container-restart",
        "container-status",
        "container-pure-handler",
        "container-request-validation",
        "container-drift-protection",
        "container-state-transition-matrix",
        "container-noop-on-redundant",
        "container-secret-shape-rejection",
        "container-log-injection-guard",
        "container-destroy",
        "container-state-machine",
        "container-drift-detection",
        "container-idempotency",
        "container-audit",
        "container-health-probe-stub",
        "fake-adapter-seam",
        "container-resource-limits",
        "container-resource-limits-validation",
        "container-resource-limits-host-capacity-check",
        "container-resource-limits-idempotent",
        "container-health-probe",
        "container-health-probe-http",
        "container-health-probe-tcp",
        "container-health-probe-exec",
        "container-health-probe-timeout",
        "container-health-probe-job-cancellation",
        "container-health-event-log",
        "container-health-event-severity-warning",
        "container-metrics-aggregation",
        "container-metrics-cursor-pagination",
        "container-metrics-user-isolation",
        "container-ephemeral-create",
        "container-ephemeral-renew",
        "container-ephemeral-release",
        "container-ephemeral-lease-model",
        "container-ephemeral-lease-gc",
        "container-ephemeral-crash-recovery",
        "container-ephemeral-default-quotas",
        "container-ephemeral-idempotent-replay",
        "container-ephemeral-per-user-isolation",
        "container-resource-sample",
        "container-resource-sample-quota-relative",
        "container-resource-sample-cursor-pagination",
        "container-prometheus-metrics-export",
        "container-prometheus-metrics-label-scoping",
        "container-prometheus-metrics-per-user-isolation",
        "container-standard-dashboard"
      ],
      "tags": [],
      "download": "/artifacts/container-orchestrator-0.8.3.zip",
      "sha256": "ab8cc2bfc98acc8908788eb39f57abd1a334479eb10e54361f665e0933c07f76",
      "size": 579314,
      "page": null,
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/container-orchestrator/health",
        "/api/v1/addons/container-orchestrator/containers/:id/status",
        "/api/v1/addons/container-orchestrator/containers/:id/preview",
        "/api/v1/addons/container-orchestrator/containers/:id/start",
        "/api/v1/addons/container-orchestrator/containers/:id/stop",
        "/api/v1/addons/container-orchestrator/containers/:id/restart",
        "/api/v1/addons/container-orchestrator/containers/:id/resource-limits",
        "/api/v1/addons/container-orchestrator/containers/:id/health",
        "/api/v1/addons/container-orchestrator/containers/:id/metrics",
        "/api/v1/addons/container-orchestrator/metrics",
        "/api/v1/addons/container-orchestrator/containers/:id/resource-samples",
        "/api/v1/addons/container-orchestrator/ephemeral",
        "/api/v1/addons/container-orchestrator/ephemeral/lease",
        "/api/v1/addons/container-orchestrator/ephemeral/release"
      ],
      "skills": [
        {
          "name": "container-orchestrator-view",
          "displayName": "Container Orchestrator: Container-Status lesen",
          "description": "Liest den aktuellen Lifecycle-State eines Containers (running | stopped | restarting | exited | paused | unknown). Read-only, idempotent.",
          "permissions": [
            "addons/container-orchestrator/containers/status"
          ],
          "endpoint": "/api/v1/addons/container-orchestrator/containers/:id/status",
          "scripts": []
        },
        {
          "name": "container-orchestrator-manage",
          "displayName": "Container Orchestrator: Container-Actions (start/stop/restart)",
          "description": "Wendet start | stop | restart auf einen Container an. Drift-Schutz via snapshotHash, idempotencyKey erforderlich, explizite noop/drift/timeout/rejected Results. Audit-Log ohne Secrets.",
          "permissions": [
            "addons/container-orchestrator/containers/start",
            "addons/container-orchestrator/containers/stop",
            "addons/container-orchestrator/containers/restart"
          ],
          "endpoint": "/api/v1/addons/container-orchestrator/containers/:id/start",
          "scripts": []
        },
        {
          "name": "container-orchestrator-resource-limits",
          "displayName": "Container Orchestrator: Resource-Limits setzen",
          "description": "Setzt CPU/RAM/PIDs/Disk-Quotas für einen Container. Validiert gegen Host-Kapazität, idempotent (zweiter Aufruf mit gleichem Payload ist noop). QUOTA_EXCEEDS_HOST → 400. Slice 3/4 von #356.",
          "permissions": [
            "addons/container-orchestrator/containers/resource-limits"
          ],
          "endpoint": "/api/v1/addons/container-orchestrator/containers/:id/resource-limits",
          "scripts": []
        },
        {
          "name": "container-orchestrator-health",
          "displayName": "Container Orchestrator: Health-Probe",
          "description": "Führt eine einzelne Health-Probe (HTTP/TCP/exec) gegen einen Container aus. Status healthy | unhealthy | unknown, Latenz in ms. Slice 3/4 von #356.",
          "permissions": [
            "addons/container-orchestrator/containers/health"
          ],
          "endpoint": "/api/v1/addons/container-orchestrator/containers/:id/health",
          "scripts": [
            {
              "id": "container-orchestrator-health-cli",
              "name": "container-orchestrator-health-cli",
              "path": "container-orchestrator-health/cli.sh",
              "sha256": "11f51a07d8253c8c52b63c79273ef051846210f6aaa011919bde774ff5c57703",
              "interpreter": "bash",
              "timeoutMs": 10000,
              "permissions": [
                "addons/container-orchestrator/containers/health"
              ],
              "inputSchema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 20
                  },
                  "timeoutMs": {
                    "type": "integer",
                    "minimum": 100,
                    "maximum": 300000
                  },
                  "id": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9._-]+$",
                    "description": "Resource-Id — ersetzt :id im Endpoint"
                  }
                },
                "required": [
                  "id"
                ]
              },
              "outputSchema": {
                "type": "object",
                "required": [
                  "ok"
                ],
                "properties": {
                  "ok": {
                    "type": "boolean"
                  },
                  "status": {
                    "type": "integer"
                  },
                  "data": {},
                  "error": {
                    "type": "string"
                  },
                  "truncated": {
                    "type": "boolean"
                  }
                }
              },
              "size": 12248
            }
          ]
        },
        {
          "name": "container-orchestrator-metrics",
          "displayName": "Container Orchestrator: Health-Metriken",
          "description": "Cursor-paginierte Health-Samples pro Container (default limit=20, max=100), inkl. successRate + p50/p95 Latency. Pro-User isoliert. Slice 3/4 von #356.",
          "permissions": [
            "addons/container-orchestrator/containers/metrics"
          ],
          "endpoint": "/api/v1/addons/container-orchestrator/containers/:id/metrics",
          "scripts": []
        },
        {
          "name": "container-orchestrator-resource-samples",
          "displayName": "Container Orchestrator: Resource-Samples (CPU/RAM/Netz/IO)",
          "description": "Cursor-paginierte ResourceSample-Rows pro Container (CPU/RAM/Netz/IO, default limit=20, max=100), quota-relativiert gegen memoryLimitBytes (ADR-CO-07). Pro-User isoliert. ADR-CO-06 (#573).",
          "permissions": [
            "addons/container-orchestrator/containers/resource-samples"
          ],
          "endpoint": "/api/v1/addons/container-orchestrator/containers/:id/resource-samples",
          "scripts": []
        },
        {
          "name": "container-orchestrator-prometheus-metrics",
          "displayName": "Container Orchestrator: Prometheus /metrics Export",
          "description": "Pull-basierter Prometheus-Textformat-Export (/metrics) über Health- + Resource-Samples. container_id-Label immer, owner-Label nur für Admin (per-User gefiltert, Isolation erhalten). ADR-CO-06 Option D (#573).",
          "permissions": [
            "addons/container-orchestrator/metrics"
          ],
          "endpoint": "/api/v1/addons/container-orchestrator/metrics",
          "scripts": []
        },
        {
          "name": "container-orchestrator-ephemeral-create",
          "displayName": "Container Orchestrator: Ephemeral-Container erstellen (lease-modell)",
          "description": "Erstellt einen ephemeren Container mit definiertem Lease (TTL 1..86400s). Container wird automatisch gestoppt nach Lease-Ablauf, GC sweep alle 60s. Idempotent auf leaseKey. Slice 4/4 von #357.",
          "permissions": [
            "addons/container-orchestrator/ephemeral"
          ],
          "endpoint": "/api/v1/addons/container-orchestrator/ephemeral",
          "scripts": []
        },
        {
          "name": "container-orchestrator-ephemeral-renew",
          "displayName": "Container Orchestrator: Ephemeral-Lease erneuern",
          "description": "Verlängert das Lease eines ephemeren Containers um leaseTtlSeconds (absolut, nicht additiv). Idempotent: gleicher leaseKey mit gleichem TTL gibt dieselbe Response ohne doppelte Verlängerung. Slice 4/4 von #357.",
          "permissions": [
            "addons/container-orchestrator/ephemeral/lease"
          ],
          "endpoint": "/api/v1/addons/container-orchestrator/ephemeral/lease",
          "scripts": []
        },
        {
          "name": "container-orchestrator-ephemeral-release",
          "displayName": "Container Orchestrator: Ephemeral-Container explizit freigeben",
          "description": "Setzt Lease-Status auf `released` und stoppt den Container sofort. Idempotent: replay nach erfolgreichem Release gibt noop zurück. Slice 4/4 von #357.",
          "permissions": [
            "addons/container-orchestrator/ephemeral/release"
          ],
          "endpoint": "/api/v1/addons/container-orchestrator/ephemeral/release",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Container Orchestrator — Slice 2/4 (Issue #355)</h1>\n<p>Container lifecycle management for the Aiopta platform. Slice 2 ships the<br /><strong>start / stop / restart / destroy</strong> mutating surface with idempotent<br />semantics, drift detection, scoped operation store, and an audit log.</p>\n<h2>Design-Entscheidungen (ADR)</h2>\n<p>Die offenen Architektur-/Produktentscheidungen der Container-Orchestrator-API<br />(Epic #207, gesammelte Owner-Runde #390) sind als ADR-Vorlagen (Kontext,<br />Optionen mit Trade-offs, begründete Empfehlung) in<br /><code>docs/adr/</code> dokumentiert (Serie <code>ADR-CO-01</code> … <code>ADR-CO-08</code>,<br />Status <code>Proposed</code> — Owner-Entscheidung ausstehend).</p>\n<h2>Slices</h2>\n<p>| # | Slice | Status |<br />|---|---|---|<br />| 1 | Manifest schema (slice #354) | prerequisite |<br />| 2 | Container lifecycle (this slice) | ✅ |<br />| 3 | Health probes + resource quotas | upcoming |<br />| 4 | Ephemeral agent containers | upcoming |</p>\n<h2>Architecture</h2>\n<p>The slice is a <strong>bounded context</strong>. It does NOT import from <code>docker-manager</code><br />or <code>development</code> — the patterns from those addons are mirrored<br />shape-for-shape but kept independent (see issue #355 Architektur-Vorgabe:<br />&quot;Pattern, kein Import&quot;).</p>\n<pre><code>src/lib/\n├── types.ts            Domain types (Container, ContainerState, ...)\n├── state-machine.ts    Pure transition table (start|stop|restart|destroy)\n├── store.ts            In-memory scoped container registry\n├── adapter.ts          FakeContainerAdapter (deterministic, hermetic)\n├── operation-store.ts  Idempotency-key store (7-day TTL)\n├── drift.ts            Snapshot hash + drift detection + user-scope guard\n├── preview.ts          Preview pipeline + dispatch-log audit\n├── handlers.ts         runStart / runStop / runRestart / runDestroy\n├── health.ts           Addon-level health probe (slice-3 will add per-container)\n└── manifest.ts         Public export surface</code></pre>\n<h2>Acceptance Criteria → Test mapping</h2>\n<p>| AC | Test |<br />|---|---|<br />| <code>container_stop</code> on stopped → 200 idempotentReplay (no double-persist) | <code>tests/unit/idempotency.test.ts</code> |<br />| Cross-user → 404 (no enumeration leak) | <code>tests/unit/user-isolation.test.ts</code> |<br />| Drift detected → 409 DRIFT_DETECTED | <code>tests/unit/drift.test.ts</code>, <code>tests/unit/stop.test.ts</code> |<br />| Adapter timeout &gt; 30s → pending, retry re-attaches | <code>tests/unit/timeout.test.ts</code> |<br />| Container orphan during operation → state: orphaned | <code>tests/unit/orphan-recovery.test.ts</code> |<br />| <code>restart</code> on exited is equivalent to <code>start</code> | <code>tests/unit/restart.test.ts</code> |<br />| <code>start</code> on running → 200 idempotentReplay | <code>tests/unit/start.test.ts</code> |<br />| 7-day retention (no auto-delete &lt; 7d) | <code>tests/unit/operation-store.test.ts</code> |<br />| Audit log with latencyMs | <code>tests/unit/audit.test.ts</code> |<br />| State machine correctness (32 transitions) | <code>tests/unit/state-machine.test.ts</code> |</p>\n<h2>Tests</h2>\n<pre><code class=\"lang-bash\">npm test</code></pre>\n<p>59 unit tests across 11 suites. All passing as of slice 2/4.</p>\n<h2>Dependencies</h2>\n<ul><li><code>node &gt;= 20</code> (uses <code>node --experimental-strip-types</code> for tests).</li><li>No external runtime deps (CLAUDE.md: dependency-free addons).</li><li><code>crypto</code> (built-in) for snapshot hashing.</li></ul>\n<h2>Out of scope (slice 2)</h2>\n<ul><li>Real adapter integration (<code>RemoteAgentAdapter</code>) — lands with parent #39.</li><li>Health probes (slice 3).</li><li>Resource quotas (slice 3).</li><li>Ephemeral agent containers (slice 4).</li><li>Image updates (separate slice, parent #159 pattern).</li></ul>",
          "byteSize": 3356
        }
      }
    },
    {
      "id": "copilot",
      "uuid": "67928067-1c5c-4754-bd23-facb873afd88",
      "name": "Aiopta Copilot",
      "version": "1.1.3",
      "description": "The admin/setup copilot: an agent that controls, manages, sets up and configures Aiopta itself via natural language. It drives the platform's own management operations through the agent-capable Live-OpenAPI (diagnose, config, addons, agents, build dashboards/workflows). Every tool is gated by the owner/admin-only mc.manage capability (default-deny), audited via logAudit (secrets masked), and — when sensitive/irreversible — queued for the owner's approval, never auto-run. No container: it reuses the platform's own agent/backend infrastructure (connector/internal pattern). Idempotently seeded as a system-agent after onboarding.",
      "author": "Aiopta",
      "icon": "🛠️",
      "capabilities": [
        "mc-management",
        "copilot-console",
        "management-toolset",
        "approval-queue",
        "audited-tools"
      ],
      "tags": [
        "onboarding"
      ],
      "download": "/artifacts/copilot-1.1.3.zip",
      "sha256": "4d02452f10fbbcc2e7b458dd7e76770fd264f64f24698d50b4e29f5973401974",
      "size": 248343,
      "page": {
        "id": "copilot-page",
        "path": "/addons/copilot",
        "name": "Aiopta Copilot",
        "icon": "🛠️",
        "section": "Automation",
        "component": "@addons/copilot/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/copilot/status",
        "/api/v1/addons/copilot/sessions",
        "/api/v1/addons/copilot/sessions/:id",
        "/api/v1/addons/copilot/sessions/:id/message",
        "/api/v1/addons/copilot/actions",
        "/api/v1/addons/copilot/actions/:id/approve",
        "/api/v1/addons/copilot/actions/:id/reject",
        "/api/v1/addons/copilot/seed",
        "/api/v1/addons/copilot/widget"
      ],
      "skills": [
        {
          "name": "mc-diagnose",
          "displayName": "Diagnose (read-only)",
          "description": "Read platform health/status, list configured providers and the active backend, list addons and agents, and surface problems with proposed fixes. Read-only diagnostics. Requires the owner/admin-only mc.manage capability; every call is audited.",
          "permissions": [
            "addons/copilot/status",
            "addons/copilot/sessions"
          ],
          "endpoint": "/api/v1/addons/copilot/sessions",
          "scripts": []
        },
        {
          "name": "mc-configure",
          "displayName": "Configure (settings, providers, backend, addons, agents)",
          "description": "Get/set Aiopta settings, set or rotate LLM/provider keys, select the active backend, install/enable/disable/configure addons, and create/edit/delete agents — all through the platform API. Requires the owner/admin-only mc.manage capability. Sensitive/irreversible actions (secret rotation, backend selection, addon enable/disable, agent delete) are NEVER auto-run: they are queued for the owner's approval. Every call is audited and secret values are masked.",
          "permissions": [
            "addons/copilot/sessions",
            "addons/copilot/actions"
          ],
          "endpoint": "/api/v1/addons/copilot/sessions",
          "scripts": []
        },
        {
          "name": "mc-build",
          "displayName": "Build (dashboards & workflows)",
          "description": "Create dashboards/widgets and workflows (via the workflow-builder) through the platform API. Requires the owner/admin-only mc.manage capability; every call is audited.",
          "permissions": [
            "addons/copilot/sessions"
          ],
          "endpoint": "/api/v1/addons/copilot/sessions",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Aiopta Copilot</h1>\n<p>The <strong>flagship</strong> admin/setup copilot: an agent that controls, manages, sets up and configures Aiopta<br /><em>itself</em> via natural language — available right after onboarding (the core of the &quot;2-minute<br />aha&quot;). It <strong>drives the platform&#39;s own management operations</strong> through the agent-capable<br />Live-OpenAPI (ADR-022); there is <strong>no container</strong> — it reuses the platform&#39;s own agent/backend<br />infrastructure (connector/internal pattern, ADR-009).</p>\n<p>This is powerful and dangerous, so the <strong>security model is the most important part</strong>.</p>\n<h2>What it can do (the management toolset)</h2>\n<p>- <strong>Diagnose (read-only):</strong> read health/status, list configured providers + the active backend, list<br />  addons + agents, and surface problems with proposed fixes.<br />- <strong>Config:</strong> get/set settings (public URL, mail, connections), set/rotate LLM/provider keys, select the<br />  active backend.<br />- <strong>Addons:</strong> list, enable/disable, configure.<br />- <strong>Agents:</strong> list, create, edit, delete.<br />- <strong>Build:</strong> create a dashboard/widget, create a workflow (via the workflow-builder).</p>\n<p>Each tool wraps an <strong>existing platform-API endpoint</strong> — the copilot never re-implements core and never<br />calls an external service.</p>\n<h2>Security model (the point of this addon)</h2>\n<p>- <strong><code>mc.manage</code> capability, default-deny.</strong> Every management tool is gated by the owner/admin-only<br />  <code>mc.manage</code> RBAC capability (ADR-005, Phase S). A plain user — and a normal user-scoped agent key<br />  (which resolves to role <code>user</code>) — is <strong>denied 403 with no side effect</strong>. The route enforces it via<br />  <code>requireCapability(request, &quot;mc.manage&quot;)</code>, and the engine re-asserts <code>hasCapability</code> (defence in depth).<br />- <strong>Sensitive ⇒ approval, never direct.</strong> Delete / grant-root / move-money / external-post / secret-<br />  rotate / addon-enable are <strong>never executed directly</strong> — they are <strong>queued</strong> as a pending action and run<br />  ONLY after a human approves (delivered to the approvals panel + optionally a notification). Read/diagnose<br />  + reversible config run directly.<br />- <strong>Audited + masked.</strong> Every tool invocation writes a <code>logAudit</code> row (actor, action, target, params);<br />  secret-bearing fields are masked — a secret value is never logged in clear.<br />- <strong>Same-origin only.</strong> Every tool path is a literal <code>/api/v1/...</code> path, re-checked at request time —<br />  the copilot can never reach an external host. The owner&#39;s auth is forwarded to each internal call so<br />  platform RBAC re-applies server-side.<br />- <strong>Agent-key scoping (S1).</strong> The copilot&#39;s own agent key is provisioned least-privilege; it does not by<br />  itself carry <code>mc.manage</code>. The copilot acts on the <strong>owner&#39;s</strong> forwarded auth — the audited trust<br />  boundary — so an out-of-scope or capabilityless caller is denied.</p>\n<h2>Approval queue</h2>\n<p>Reuses the existing approval+notify mechanism established by business-operator (ADR-025) and the<br />notifications-approval delivery (C7 / ADR-035). A sensitive action creates a pending row the owner<br />confirms in the console&#39;s <strong>Freigaben</strong> panel (and optionally via the notifications addon). On approval<br />the action runs (and is audited as <code>copilot.action.approved</code>); on rejection it is dropped (audited as<br /><code>copilot.action.rejected</code>).</p>\n<h2>Surfaces</h2>\n<p>- <strong>Page</strong> <code>/addons/copilot</code> — the <strong>Aiopta Copilot</strong> console (chat + quick-action<br />  suggestions + a tool catalog with approval gates + the approvals panel). Renders a graceful<br />  &quot;connect a backend / seed the copilot&quot; state when not configured.<br />- <strong>Widget</strong> <code>copilot.ask</code> — <strong>&quot;Frag Aiopta&quot;</strong>: a prompt box that opens the console with the<br />  question prefilled, plus readiness + open-approval count.</p>\n<h2>Seeding</h2>\n<p><code>seedCopilotAgent(owner)</code> (in <code>src/lib/seed.ts</code>) provisions the copilot as an idempotent<br /><strong>system-agent</strong> (soul, model from the active backend, the management skills) via the shared<br /><code>provisionAgentFromSpec</code>. The onboarding-completion hook (#96) calls it at onboarding end; an admin can<br />also seed it now via <code>POST /api/v1/addons/copilot/seed</code>.</p>\n<h2>Backend-native</h2>\n<p>The conversation turn runs through <code>gatewayChatCompletion</code> with <code>{ needsTools: true }</code> (ADR-038): when<br />the active backend supports native tool-calling it runs there, otherwise the gateway falls back to the<br />default path. Memory/Tools/Orchestration are reused via the gateway, never reimplemented.</p>\n<p>See ADR-040 and <code>SKILL.md</code> for the full contract.</p>",
          "byteSize": 4333
        }
      }
    },
    {
      "id": "crm",
      "uuid": "d60d52fa-dfbc-4ebe-bdcf-061003d13e14",
      "name": "CRM (Kontakte, Firmen, Pipeline, Deals, Aktivitäten)",
      "version": "1.4.1",
      "description": "Kanonisches CRM-Addon und Ziel der CRM-Konsolidierung (crm ⊃ lead-gen, ADR-008 / Issue #491 / Epic #490): Vollständiges CRM auf der lead-gen-CRM-lite (ADR-015): Firmen (Organisationen), Kontakte (Personen, optional einer Firma zugeordnet), eine Deal-Pipeline (Stages → Deals), Aktivitäten (Anrufe/Notizen/Aufgaben/Meetings, an Kontakte/Deals gehängt) und eine E-Mail-/Touchpoint-Timeline. Lokale SQLite ist die Quelle der Wahrheit; reine Daten — KEIN externer Container, KEIN Provider, KEIN Secret. Jede Zeile ist pro Nutzer isoliert (user_id, ADR-054): Nutzer B sieht/ändert nie die Daten von Nutzer A. Das (jetzt als deprecated markierte) lead-gen-Addon wird hierher konsolidiert: seine erfassten Leads werden per Import verlustfrei zu CRM-Kontakten (lose Verknüpfung über die E-Mail, kein Hard-Coupling; die lead_gen_*-Tabellen bleiben unangetastet). ADR-054.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "crm-companies",
        "crm-contacts",
        "crm-deals",
        "crm-pipeline",
        "crm-activities",
        "crm-touchpoints",
        "lead-gen-import",
        "lead-gen-consolidation",
        "lead-scraping",
        "lead-enrichment",
        "outreach-sequences",
        "outreach-sending",
        "outreach-suppression"
      ],
      "tags": [],
      "download": "/artifacts/crm-1.4.1.zip",
      "sha256": "4584c4619a9c1518ec6456488f640cf2b1577767eb284cce234766c5ec64ef19",
      "size": 359962,
      "page": {
        "id": "crm-page",
        "path": "/addons/crm",
        "name": "CRM",
        "icon": "🤝",
        "section": "Money",
        "component": "@addons/crm/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/crm/status",
        "/api/v1/addons/crm/companies",
        "/api/v1/addons/crm/companies/:id",
        "/api/v1/addons/crm/contacts",
        "/api/v1/addons/crm/contacts/:id",
        "/api/v1/addons/crm/contacts/:id/detail",
        "/api/v1/addons/crm/contacts/:id/touchpoints",
        "/api/v1/addons/crm/deals",
        "/api/v1/addons/crm/deals/:id",
        "/api/v1/addons/crm/deals/:id/stage",
        "/api/v1/addons/crm/activities",
        "/api/v1/addons/crm/activities/:id",
        "/api/v1/addons/crm/import/leads",
        "/api/v1/addons/crm/widget",
        "/api/v1/addons/crm/outreach/status",
        "/api/v1/addons/crm/outreach/contacts",
        "/api/v1/addons/crm/outreach/contacts/:id",
        "/api/v1/addons/crm/outreach/pipeline",
        "/api/v1/addons/crm/outreach/enrich",
        "/api/v1/addons/crm/outreach/sequences",
        "/api/v1/addons/crm/outreach/sequences/:id/generate",
        "/api/v1/addons/crm/outreach/sequences/:id/send",
        "/api/v1/addons/crm/outreach/suppression",
        "/api/v1/addons/crm/outreach/widget"
      ],
      "skills": [
        {
          "name": "crm-overview",
          "displayName": "CRM: Status & Übersicht",
          "description": "Liest die CRM-Übersicht des Nutzers (Firmen, Kontakte, offene/gewonnene Deals, offene Pipeline-Summe, offene Aufgaben, Deals je Stage). Reiner Lesezugriff — ändert nichts.",
          "permissions": [
            "addons/crm/status"
          ],
          "endpoint": "/api/v1/addons/crm/status",
          "scripts": [
            {
              "id": "crm-overview-cli",
              "name": "crm-overview-cli",
              "path": "crm-overview/cli.sh",
              "sha256": "1c9b2db4177e316cbc978045891f2899a6b913baee09a2d27d4bebfdc42e085a",
              "interpreter": "bash",
              "timeoutMs": 10000,
              "permissions": [
                "addons/crm/status"
              ],
              "inputSchema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 20
                  },
                  "timeoutMs": {
                    "type": "integer",
                    "minimum": 100,
                    "maximum": 300000
                  }
                }
              },
              "outputSchema": {
                "type": "object",
                "required": [
                  "ok"
                ],
                "properties": {
                  "ok": {
                    "type": "boolean"
                  },
                  "status": {
                    "type": "integer"
                  },
                  "data": {},
                  "error": {
                    "type": "string"
                  },
                  "truncated": {
                    "type": "boolean"
                  }
                }
              },
              "size": 12045
            }
          ]
        },
        {
          "name": "crm-companies",
          "displayName": "CRM: Firmen",
          "description": "Listet/erstellt/bearbeitet Firmen (Organisationen) mit Domain/Branche/Website. Kontakte und Deals können einer Firma zugeordnet werden.",
          "permissions": [
            "addons/crm/companies"
          ],
          "endpoint": "/api/v1/addons/crm/companies",
          "scripts": []
        },
        {
          "name": "crm-contacts",
          "displayName": "CRM: Kontakte",
          "description": "Listet/erstellt/bearbeitet Kontakte (Personen, optional einer Firma zugeordnet) und liest die Kontakt-Detailansicht mit Deals, Aktivitäten und der Touchpoint-Timeline. De-dupliziert per E-Mail.",
          "permissions": [
            "addons/crm/contacts",
            "addons/crm/companies"
          ],
          "endpoint": "/api/v1/addons/crm/contacts",
          "scripts": []
        },
        {
          "name": "crm-deals",
          "displayName": "CRM: Deals & Pipeline",
          "description": "Listet/erstellt/bearbeitet Deals und bewegt sie durch die Pipeline (lead→qualified→proposal→negotiation→won/lost). Die Stage-Maschine ist erzwungen; won/lost sind terminal.",
          "permissions": [
            "addons/crm/deals",
            "addons/crm/contacts"
          ],
          "endpoint": "/api/v1/addons/crm/deals",
          "scripts": []
        },
        {
          "name": "crm-activities",
          "displayName": "CRM: Aktivitäten",
          "description": "Protokolliert Aktivitäten (Anruf/Notiz/Aufgabe/Meeting/E-Mail) gegen einen Kontakt und/oder Deal und schließt Aufgaben ab. Reine Daten — versendet nichts.",
          "permissions": [
            "addons/crm/activities",
            "addons/crm/contacts",
            "addons/crm/deals"
          ],
          "endpoint": "/api/v1/addons/crm/activities",
          "scripts": []
        },
        {
          "name": "crm-import-leads",
          "displayName": "CRM: lead-gen-Leads importieren",
          "description": "Importiert die EIGENEN lead-gen-Kontakte des Nutzers als CRM-Kontakte (lose Verknüpfung über die E-Mail). Idempotent; verändert die lead-gen-Daten nicht.",
          "permissions": [
            "addons/crm/import",
            "addons/crm/contacts"
          ],
          "endpoint": "/api/v1/addons/crm/import/leads",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>CRM (Kontakte, Firmen, Pipeline, Deals, Aktivitäten)</h1>\n<p>Vollständiges CRM auf der lead-gen-CRM-lite (ADR-015): Firmen (Organisationen), Kontakte (Personen, optional einer Firma zugeordnet), eine Deal-Pipeline (Stages → Deals), Aktivitäten (Anrufe/Notizen/Aufgaben/Meetings, an Kontakte/Deals gehängt) und eine E-Mail-/Touchpoint-Timeline. Lokale SQLite ist die Quelle der Wahrheit; reine Daten — KEIN externer Container, KEIN Provider, KEIN Secret. Jede Zeile ist pro Nutzer isoliert (user_id, ADR-054): Nutzer B sieht/ändert nie die Daten von Nutzer A. Ein erfasster lead-gen-Lead kann per Import zu einem CRM-Kontakt werden (lose Verknüpfung über die E-Mail, kein Hard-Coupling). ADR-054.</p>\n<h2>CRM-Konsolidierung (crm ⊃ lead-gen, ADR-008 / #491, Epic #490)</h2>\n<p><code>crm</code> ist das <strong>kanonische Ziel-Addon</strong> der CRM-Konsolidierung. Das <code>lead-gen</code>-Addon (Lead-Capture +<br />Cold Outreach) ist als <strong>deprecated</strong> markiert (<code>replacedBy: crm</code>, Sunset <strong>2026-12-31</strong>) und wird<br />hierher zusammengeführt. Die gewählte Umsetzung ist <strong>datenerhaltend und rückwärtskompatibel</strong>:</p>\n<p>- <strong>Ein Ziel-Addon:</strong> <code>crm</code> ist die durable Relationship-Ebene, auf der lead-gen laut ADR-015 ohnehin<br />  aufsetzt. lead-gens Fähigkeiten bleiben im Alias-Fenster erreichbar; die Nachfolge zeigt auf <code>crm</code>.<br />- <strong>Verlustfreie Migration:</strong> die per-Nutzer-Brücke <code>POST /api/v1/addons/crm/import/leads</code><br />  (<code>importLeads</code>) liest lead-gens <strong>eigenen, bereits nutzer-isolierten</strong> Store und legt jeden Lead als<br />  CRM-Kontakt an (idempotent, per E-Mail verknüpft, <code>leadEmail</code>-Rückreferenz). Es wird **keine Zeile<br />  kopiert oder gelöscht** — die <code>lead_gen_*</code>-Tabellen bleiben unverändert bestehen (loss-free „by<br />  construction“, analog zum Core-Boot-Reconcile).<br />- <strong>Alt-ID-Redirect:</strong> auf <strong>Addon-Ebene</strong> über lead-gens Metadaten (<code>replacedBy: crm</code>, <code>sunset</code>,<br />  <code>settingsPagePath: /addons/crm</code>) — Marketplace/Settings-Hub zeigen den Nachfolger; auf<br />  <strong>Datensatz-Ebene</strong> löst der kanonische Resolver <code>getContactByLeadEmail(userId, email)</code><br />  (<code>src/lib/lead-redirect.ts</code>) eine alte lead-gen-Lead-Identität (per E-Mail) auf den migrierten<br />  CRM-Kontakt auf.<br />- <strong>Bewusst NICHT (Owner-Entscheidung):</strong> das schwere Outreach-Maschinenwerk (Scraper-Container,<br />  AI-Sequenzen, garantierter Versand, Suppression) wird <strong>nicht</strong> in einem Schritt physisch nach <code>crm</code><br />  verschoben — das würde eine riskante Zusammenführung zweier unterschiedlicher Kontakt-/Pipeline-<br />  Datenmodelle bedeuten. lead-gens Routen bleiben unverändert (deprecated, aber funktional); die<br />  Code-Relocation zu reinen Re-Export-Shims samt HTTP-Redirect-Headern ist der Folge-Schritt vor dem<br />  Entfernen (ADR-008 „one PR per cluster“).</p>\n<h3>Lizenz-/Pricing- und Permission-Review (AC3)</h3>\n<p>- <strong>Lizenz:</strong> beide Addons sind <code>UNLICENSED</code>/first-party (author <code>Aiopta</code>); die Konsolidierung ändert<br />  keine Drittlizenz. Keine neue Abhängigkeit.<br />- <strong>Pricing:</strong> lead-gen ist „Money-Stufe-1“; <code>crm</code> ist reine Daten ohne Container/Secret. Es wird **kein<br />  neuer bezahlter Flow** eingeführt. Der garantierte E-Mail-Versand (der einzige kostenwirksame Pfad)<br />  bleibt im deprecated lead-gen hinter seinen Guardrails; <code>crm</code> selbst versendet nichts.<br />- <strong>Permissions:</strong> <code>crm</code> bleibt least-privilege und rein nutzer-isoliert (ADR-054, jede Zeile<br />  <code>user_id</code>-gestempelt). Die Brücke liest ausschließlich die <strong>eigenen</strong> lead-gen-Daten des Aufrufers<br />  über dessen bereits gescopten Store — kein Cross-User-Zugriff, keine neue Berechtigung. lead-gens<br />  Skills (inkl. des separaten Sende-Skills) bleiben unverändert.</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>crm</code></li><li><strong>Version:</strong> 1.2.0</li><li><strong>Fähigkeiten:</strong> crm-companies, crm-contacts, crm-deals, crm-pipeline, crm-activities, crm-touchpoints, lead-gen-import, lead-gen-consolidation</li><li><strong>UI-Seite:</strong> <code>/addons/crm</code></li><li><strong>Dashboard-Widgets:</strong> <code>crm.summary</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET, POST | <code>/api/v1/addons/crm/activities</code> |<br />| GET, PUT, DELETE | <code>/api/v1/addons/crm/activities/[id]</code> |<br />| GET, POST | <code>/api/v1/addons/crm/companies</code> |<br />| GET, PUT, DELETE | <code>/api/v1/addons/crm/companies/[id]</code> |<br />| GET, POST | <code>/api/v1/addons/crm/contacts</code> |<br />| GET, PUT, DELETE | <code>/api/v1/addons/crm/contacts/[id]</code> |<br />| GET | <code>/api/v1/addons/crm/contacts/[id]/detail</code> |<br />| GET, POST | <code>/api/v1/addons/crm/contacts/[id]/touchpoints</code> |<br />| GET, POST | <code>/api/v1/addons/crm/deals</code> |<br />| GET, PUT, DELETE | <code>/api/v1/addons/crm/deals/[id]</code> |<br />| PUT | <code>/api/v1/addons/crm/deals/[id]/stage</code> |<br />| POST | <code>/api/v1/addons/crm/import/leads</code> |<br />| GET | <code>/api/v1/addons/crm/status</code> |<br />| GET | <code>/api/v1/addons/crm/widget</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<ul><li>Keine bekannten Einschränkungen.</li></ul>\n<blockquote><p>README aus <code>addon.json</code> + vorhandenen Routen generiert (<code>scripts/generate-addon-readmes.js</code>). Bei manuellen Ergänzungen den Generator-Hinweis entfernen.</p></blockquote>",
          "byteSize": 4882
        }
      }
    },
    {
      "id": "cubee3d-hive",
      "uuid": "3ebf6c8a-6232-4d6b-b6bc-9fb4b41bf0a2",
      "name": "Cubee3D Hive",
      "version": "1.1.0",
      "description": "Cubee3D Hive catalog reader — lists catalogs, products and 3D-designer assets from the Wix-hosted storefront using an authenticated session or BYOK API key. Automated Wix credential login remains separately tracked until Cubee3D provides a stable supported contract. Requires the WordPress & WooCommerce addon for downstream product sync.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "3d-products",
        "hive-catalog"
      ],
      "tags": [],
      "download": "/artifacts/cubee3d-hive-1.1.0.zip",
      "sha256": "daf1901c9f46e76e97f6ef011044a74011a397a38da8d8b39f589f50fd0bc510",
      "size": 126389,
      "page": {
        "id": "cubee3d-hive-page",
        "path": "/addons/cubee3d-hive",
        "name": "Cubee3D Hive",
        "icon": "🎲",
        "section": "Tools",
        "component": "@addons/cubee3d-hive/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/cubee3d-hive/catalogs",
        "/api/v1/addons/cubee3d-hive/products",
        "/api/v1/addons/cubee3d-hive/credentials"
      ],
      "skills": [],
      "mcpServers": [
        {
          "id": "cubeehive-tools",
          "displayName": "Cubee3D Hive tools",
          "description": "Read-only Cubee3D/Hive catalog tools for agents: list catalogs, list products, get a single product. Read-only — no sync/mutation tools (those stay in the addon's authenticated write API).",
          "transport": "stdio",
          "runtime": "node",
          "entry": "cubeehive-tools/server.mjs",
          "permissions": [
            "addons/cubee3d-hive/read"
          ],
          "toolPolicy": {
            "mode": "read-only",
            "allowDestructive": false
          },
          "archiveEntry": "cubee3d-hive/mcp/servers/cubeehive-tools/server.mjs",
          "sha256": "5d53de6517b5676094a698efce633b08a844e5a410cf5e2a9ba6501e559bcd32",
          "size": 12085,
          "descriptorVersion": 1
        }
      ],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": null
    },
    {
      "id": "dataannotation-bot",
      "uuid": "303118cb-e901-4b42-9f80-29e7ae9df28f",
      "name": "DataAnnotation Bot",
      "version": "1.0.1",
      "description": "Human-like task automation for DataAnnotation.tech — browser automation (Puppeteer), Bezier-curve mouse movements, scheduling, and reporting. Runs as a Aiopta extension.",
      "author": "Aiopta",
      "icon": "🤖",
      "capabilities": [
        "automation",
        "browser",
        "puppeteer",
        "scheduling",
        "human-like-input"
      ],
      "tags": [],
      "download": "/artifacts/dataannotation-bot-1.0.1.zip",
      "sha256": "3be9c27dd7ba149f18a72e5c799aff0489d2903d7c5f797f8ee7f44dccf278fb",
      "size": 53366,
      "page": null,
      "deprecated": false,
      "endpoints": [
        "/api/dataannotation-bot/status"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>DataAnnotation Bot — Aiopta Addon</h1>\n<p>Human-like task automation for DataAnnotation.tech, built as an Aiopta extension.</p>\n<h2>Features</h2>\n<p>### 🖱️ Browser Automation (Puppeteer)<br />- Full headless/headed browser control via Puppeteer<br />- Realistic viewport (1920×1080) and user agent<br />- Anti-detection: removes <code>navigator.webdriver</code>, fakes plugins/languages<br />- Disables <code>AutomationControlled</code> Chrome feature</p>\n<p>### 🎯 Human-like Mouse Movements (Bezier Curves)<br />- <strong>Quadratic Bezier curves</strong> with random control points for natural cursor paths<br />- <strong>Eased interpolation</strong> — slower at start/end, faster in middle (mimics real hand acceleration)<br />- <strong>Micro-jitter</strong> — ±1-2px random offset on each step for realism<br />- <strong>Variable step count</strong> based on distance (more steps for longer moves)<br />- <strong>Pre/post-click pauses</strong> (100-300ms and 50-150ms)<br />- <strong>Hover dwell time</strong> — random 200-800ms hover before action</p>\n<p>### ⌨️ Human-like Typing<br />- Variable keystroke delay (50-200ms per character)<br />- <strong>3% typo rate</strong> — occasional double-char with backspace correction<br />- Natural backspace timing (100-300ms)</p>\n<p>### 📅 Human Scheduling<br />- <strong>Weekday evenings</strong>: 19:00–23:00<br />- <strong>Early mornings</strong>: 06:00–08:00<br />- <strong>Weekends</strong>: 10:00–22:00<br />- <strong>Never during business hours</strong> (9-17 weekdays)<br />- <strong>15% random day-off chance</strong></p>\n<p>### 📊 Reporting<br />- Session logging (tasks, earnings, duration)<br />- Daily reports with effective hourly rate<br />- 7-day rolling history</p>\n<h2>Architecture</h2>\n<pre><code>addons/dataannotation-bot/\n├── src/\n│   ├── index.ts                    # Addon registration\n│   ├── bot/\n│   │   ├── human-bot.ts            # Main bot orchestrator\n│   │   ├── browser-automation.ts   # Puppeteer wrapper\n│   │   ├── mouse-movements.ts      # Bezier curve mouse simulation\n│   │   ├── schedule.ts             # Work-hour scheduler\n│   │   └── report.ts               # Session reporting\n│   ├── api/\n│   │   ├── bot-status.route.ts     # GET /api/dataannotation-bot/status\n│   │   ├── bot-control.route.ts    # POST /api/dataannotation-bot/control\n│   │   └── bot-reports.route.ts    # GET /api/dataannotation-bot/reports\n│   ├── components/\n│   │   ├── BotControlPanel.tsx     # Start/stop/pause UI\n│   │   ├── BotStatusCard.tsx       # Compact status display\n│   │   └── HumanPatternConfig.tsx  # Config sliders\n│   ├── pages/dataannotation/\n│   │   └── page.tsx                # Full bot page\n│   └── config/\n│       └── addon.config.ts         # Addon metadata\n├── package.json\n├── tsconfig.json\n└── README.md</code></pre>\n<h2>API</h2>\n<p>| Endpoint | Method | Description |<br />|----------|--------|-------------|<br />| <code>/api/dataannotation-bot/status</code> | GET | Current bot status |<br />| <code>/api/dataannotation-bot/control</code> | POST | Send action (<code>start</code>, <code>stop</code>, <code>pause</code>, <code>resume</code>) |<br />| <code>/api/dataannotation-bot/reports?days=7</code> | GET | Daily reports |</p>\n<h2>Environment Variables</h2>\n<p>| Variable | Required | Description |<br />|----------|----------|-------------|<br />| <code>DATAANNOTATION_EMAIL</code> | Yes | Login email |<br />| <code>DATAANNOTATION_PASSWORD</code> | Yes | Login password |</p>\n<h2>Review of Original Scripts</h2>\n<p>The original <code>dataannotation-bot/</code> scripts were analyzed:<br />- <strong><code>human-bot.js</code></strong>: Uses Puppeteer with human-like typing, random delays, and anti-detection. <strong>Missing</strong>: mouse movement simulation.<br />- <strong><code>schedule.js</code></strong>: Human work-hour scheduling. Fully ported.<br />- <strong><code>report.js</code></strong>: Simple daily report generator. Enhanced with session logging.<br />- <strong><code>package.json</code></strong>: Dependencies: <code>puppeteer</code> ^24.41.0, <code>node-cron</code> ^4.2.1.</p>\n<p>### Key Improvements in Addon Version<br />1. <strong>Bezier curve mouse movements</strong> (original had none)<br />2. <strong>Eased interpolation</strong> for natural acceleration<br />3. <strong>Micro-jitter</strong> on cursor path<br />4. <strong>TypeScript</strong> throughout<br />5. <strong>React control panel</strong> with live status<br />6. <strong>Aiopta addon architecture</strong> (no core changes)</p>",
          "byteSize": 3959
        }
      }
    },
    {
      "id": "development",
      "uuid": "1c7801ec-ead1-4468-9b32-369f57a83904",
      "name": "Development",
      "version": "2.2.1",
      "description": "Lets agents start, steer, inspect, and terminate tmux-backed coding sessions (Claude Code, opencode, codex) via a pluggable engine-adapter registry, with per-session git-worktree isolation so parallel sessions on one repo never corrupt each other, plus per-session engine config (model, permissions, tools, system prompt) and custom provider environment overrides, plus per-user isolated workspaces with idempotent workspace + worktree cleanup (#151) and orphan recovery on startup, plus deterministic per-session project-instructions (AGENTS.md + global + explicit) load with full provenance (#131), plus server-side per-(user, session) session-start validation of engine, model id, provider, and credential ownership with adapter-scoped env allowlists (#111), plus a portable Skills + Commands catalog with provenance, hash, permissions and per-workspace materialisation to engine-native paths (#132), plus owner-scoped, versioned role profiles for architect / planner / coder / qa / research / docs with explicit permission presets, hardwired refusal of admin patterns, and immutable per-run snapshot/audit binding (#135), plus a multi-engine extension adapter (#236) with engine × kind support matrix, per-engine deterministic materializers (opencode / claude-code / codex), dry-run diff, fail-closed on unsupported entries, user-file preservation, partial-rollback on materialise failure, npm coordinate pinning and secret-stripping on markdown, plus a CodingProject domain (#139) that bundles repositories (code / tooling / docs), host, and lifecycle (draft / provisioning / ready / degraded / detached / deleting / failed) so sessions can OPTIONALLY anchor on a project (legacy sessions without projectId keep their row-only behaviour), plus a Git-Host-Adapter layer (#140) that registers per-user forge-host profiles (forgejo, generic-git), probes them with a token-redacted, SSRF-guarded connectivity check, and exposes typed argv builders for clone/fetch/push/ls-remote, plus versioned installer recipes with license, terms, hash and supply-chain policy for the future per-project Toolchain-Runtime (#145), plus capability-driven session-supervision for child-sessions, todos, diffs, forks, summaries and permission responses with per-operation support tags, autonomy-policy gating and audited decisions (#130), plus curated MCP-Server-Source-Management (local-binary / npm / docker / remote) with per-kind allowlist, secret-redacted health-check + tools/list probe, owner-defaults via adminRoute, and per-(user, owner) scoped persistence (#235), plus trusted plugin profiles with hook/tool boundaries, hash-pinned npm or project-local sources, owner-bound allowlist, role intersection, secret-redacting audit log, session binding, and OpenCode materialisation that preserves user files (#134).",
      "author": "Aiopta",
      "icon": "🧠",
      "capabilities": [
        "coding-sessions",
        "tmux-control",
        "agent-steering",
        "custom-llm-routing",
        "terminal-automation",
        "git-worktree-isolation",
        "engine-adapters",
        "parallel-orchestration",
        "per-session-engine-config",
        "per-user-workspace-isolation",
        "workspace-cleanup",
        "orphan-recovery",
        "project-instructions",
        "session-start-validation",
        "env-allowlist",
        "coding-resources",
        "skills-catalog",
        "commands-catalog",
        "engine-native-materialisation",
        "role-profiles",
        "permission-presets",
        "admin-pattern-refusal",
        "snapshot-audit-binding",
        "git-host-adapters",
        "forge-host-probe",
        "ssrf-guard",
        "coding-projects",
        "project-profiles",
        "repository-bundle",
        "project-lifecycle",
        "toolchain-installer-recipes",
        "recipe-schema",
        "recipe-validator",
        "license-policy",
        "supply-chain-policy",
        "owner-acknowledgement",
        "sbom-provenance",
        "session-supervision",
        "supervision-children",
        "supervision-todos",
        "supervision-diff",
        "supervision-fork",
        "supervision-summarize",
        "supervision-permissions",
        "permission-policy-gating",
        "supervision-audit",
        "extension-multi-engine-adapter",
        "extension-engine-materializer",
        "extension-engine-support-matrix",
        "extension-dry-run-diff",
        "extension-npm-pinning",
        "lsp-profile-management",
        "lsp-sources",
        "lsp-allowlist",
        "lsp-engine-intersection",
        "lsp-materializers",
        "lsp-health-check",
        "lsp-per-user-isolation",
        "lsp-workspace-cascade",
        "mcp-source-management",
        "mcp-source-local-binary",
        "mcp-source-npm",
        "mcp-source-docker",
        "mcp-source-remote",
        "mcp-source-allowlist",
        "mcp-source-probe",
        "mcp-source-health-check",
        "mcp-source-owner-defaults",
        "parallel-session-ui",
        "cursor-stream-reconnect",
        "output-buffer-dedupe",
        "plugin-profile-management",
        "plugin-profile-allowlist",
        "plugin-profile-versioning",
        "plugin-profile-hash-pinning",
        "plugin-hook-boundaries",
        "plugin-tool-collisions",
        "plugin-secret-redaction",
        "plugin-session-binding",
        "plugin-engine-intersection",
        "plugin-opencode-materialization",
        "plugin-uninstall-cleanup",
        "plugin-audit-log",
        "tool-binding-profiles",
        "tool-binding-custom-tools",
        "tool-binding-session-snapshot",
        "tool-binding-permission-intersection",
        "tool-binding-collision-resolution",
        "tool-binding-engine-materialisation",
        "tool-binding-redacted-status-view",
        "task-graph",
        "task-graph-validator",
        "task-graph-scheduler",
        "task-graph-completion-gate",
        "task-graph-handoff",
        "task-graph-restart-safe",
        "task-graph-role-handoff",
        "project-provisioning",
        "provision-dry-run",
        "provision-apply",
        "provision-rollback",
        "provision-policy",
        "provision-capability-check",
        "agent-tool-api",
        "agent-tool-session-create",
        "agent-tool-session-list",
        "agent-tool-session-get",
        "agent-tool-session-prompt-async",
        "agent-tool-session-events",
        "agent-tool-session-abort",
        "agent-tool-least-privilege",
        "agent-tool-cursor-pagination",
        "agent-tool-secret-redaction",
        "agent-tool-idempotency",
        "agent-tool-audit",
        "supervision-ui",
        "orchestration-overview",
        "orchestration-actions",
        "orchestration-graph-detail",
        "orchestration-cursor-reconnect",
        "orchestration-stale-banner",
        "orchestration-unsupported-explain",
        "toolchain-runtime",
        "toolchain-runtime-policy",
        "toolchain-lock",
        "toolchain-lock-hash",
        "toolchain-base-image-digest",
        "toolchain-snapshot",
        "toolchain-mount-allowlist",
        "toolchain-cpu-ram-pid-policy",
        "toolchain-network-policy",
        "toolchain-shared-snapshot",
        "toolchain-session-binding",
        "toolchain-fake-core-runtime",
        "toolchain-engine-probe-in-runtime",
        "toolchain-upgrade-rollback",
        "toolchain-audit-log",
        "toolchain-gc",
        "toolchain-no-host-path-fallback",
        "toolchain-no-shared-writable-home",
        "toolchain-revision-bumping",
        "project-setup-wizard",
        "project-health-dashboard",
        "project-dashboard-resource-graph",
        "project-dashboard-version-drift",
        "project-dashboard-typed-confirmation",
        "project-dashboard-rbac",
        "proprietary-coding-tools",
        "proprietary-coding-tools-claude-code",
        "proprietary-coding-tools-owner-rbac",
        "proprietary-coding-tools-acknowledgement",
        "proprietary-coding-tools-session-gate",
        "proprietary-coding-tools-cache-eviction",
        "proprietary-coding-tools-redirect-resolver",
        "proprietary-coding-tools-no-redistribute",
        "proprietary-coding-tools-no-auto-accept",
        "coding-control",
        "plan-orchestration",
        "agent-bridge"
      ],
      "tags": [],
      "download": "/artifacts/development-2.2.1.zip",
      "sha256": "832802becc62cca6018da9b13f8885345aa41c79e308260c2405996d5ef95a1f",
      "size": 4414264,
      "page": {
        "id": "development",
        "name": "Development",
        "path": "/addons/development",
        "icon": "🧠",
        "section": "Tools",
        "component": "@addons/development/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/development/status",
        "/api/v1/addons/development/engines",
        "/api/v1/addons/development/providers",
        "/api/v1/addons/development/auth",
        "/api/v1/addons/development/sessions",
        "/api/v1/addons/development/sessions/:id/output",
        "/api/v1/addons/development/sessions/:id/input",
        "/api/v1/addons/development/sessions/:id/run",
        "/api/v1/addons/development/sessions/:id/terminate",
        "/api/v1/addons/development/sessions/:id/delete",
        "/api/v1/addons/development/role-profiles",
        "/api/v1/addons/development/role-profiles/:role",
        "/api/v1/addons/development/role-profiles/:role/fallback",
        "/api/v1/addons/development/projects",
        "/api/v1/addons/development/projects/:id",
        "/api/v1/addons/development/projects/:id/adopt",
        "/api/v1/addons/development/projects/:id/detach",
        "/api/v1/addons/development/sessions/:id/supervision/children",
        "/api/v1/addons/development/sessions/:id/supervision/todos",
        "/api/v1/addons/development/sessions/:id/supervision/diff",
        "/api/v1/addons/development/sessions/:id/supervision/fork",
        "/api/v1/addons/development/sessions/:id/supervision/summarize",
        "/api/v1/addons/development/sessions/:id/supervision/permissions",
        "/api/v1/addons/development/sessions/:id/supervision/permissions/:permissionId",
        "/api/v1/addons/development/extensions",
        "/api/v1/addons/development/extensions/:id/support",
        "/api/v1/addons/development/extensions/:id/diff",
        "/api/v1/addons/development/extensions/:id/materialize",
        "/api/v1/addons/development/workspaces/:ws/lsp",
        "/api/v1/addons/development/workspaces/:ws/lsp/:id",
        "/api/v1/addons/development/workspaces/:ws/lsp/:id/health",
        "/api/v1/addons/development/mcp-sources",
        "/api/v1/addons/development/mcp-sources/:id",
        "/api/v1/addons/development/mcp-sources/:id/health",
        "/api/v1/addons/development/mcp-sources/:id/probe",
        "/api/v1/addons/development/admin/mcp-sources",
        "/api/v1/addons/development/plugin-profiles",
        "/api/v1/addons/development/plugin-profiles/:id",
        "/api/v1/addons/development/plugin-profiles/:id/bindings/uninstall",
        "/api/v1/addons/development/sessions/:id/plugins",
        "/api/v1/addons/development/task-graphs",
        "/api/v1/addons/development/task-graphs/:id",
        "/api/v1/addons/development/task-graphs/:id/handoffs",
        "/api/v1/addons/development/projects/:id/provision",
        "/api/v1/addons/development/projects/:id/provision/plan",
        "/api/v1/addons/development/projects/:id/provision/apply",
        "/api/v1/addons/development/projects/:id/provision/rollback",
        "/api/v1/addons/development/projects/:id/health",
        "/api/v1/addons/development/projects/:id/upgrade",
        "/api/v1/addons/development/agent-tools",
        "/api/v1/addons/development/orchestration/overview",
        "/api/v1/addons/development/orchestration/graphs/:planId",
        "/api/v1/addons/development/orchestration/actions",
        "/api/v1/addons/development/admin/proprietary-tools",
        "/api/v1/addons/development/admin/proprietary-tools/:id",
        "/api/v1/addons/development/admin/proprietary-tools/:id/preview",
        "/api/v1/addons/development/admin/proprietary-tools/:id/acknowledge",
        "/api/v1/addons/development/admin/proprietary-tools/:id/acknowledgements"
      ],
      "skills": [
        {
          "name": "coding-session",
          "displayName": "Coding: Start & Steer Sessions",
          "description": "Start, inspect, steer and terminate isolated coding sessions (e.g. Claude Code) with provider/model env overrides",
          "permissions": [
            "addons/development/sessions",
            "addons/development/status"
          ],
          "endpoint": "/api/v1/addons/development/sessions",
          "scripts": []
        },
        {
          "name": "ci-check",
          "displayName": "Coding: CI Check (token-sparing)",
          "description": "Run the repo lint/type-check/test gate via ci-check.sh inside a session before committing — a cheap deterministic quality gate that avoids burning tokens on trial-and-error",
          "permissions": [
            "addons/development/sessions"
          ],
          "endpoint": "/api/v1/addons/development/sessions",
          "scripts": []
        },
        {
          "name": "coding-resources",
          "displayName": "Coding: Skills & Commands Catalog (#132)",
          "description": "Discover, validate and materialise portable Skills and re-usable Commands from project, addon and global sources into engine-native per-workspace paths (OpenCode: .opencode/skills + .agents/skills + .claude/commands). Other engines report unsupported. No network install, no auto-execute, no overwrite of existing project files.",
          "permissions": [
            "addons/development/status"
          ],
          "scripts": []
        },
        {
          "name": "coding-projects",
          "displayName": "Coding: Projects (#139) — Workspace Profiles & Repository Bundles",
          "description": "Manage CodingProjects — the stable bundle of repositories (code / tooling / docs), git host reference, and lifecycle (draft / provisioning / ready / degraded / detached / deleting / failed). Sessions can OPTIONALLY anchor on a project (legacy sessions without projectId keep their row-only behaviour). Adopt is idempotent on identical metadata; detach removes Aiopta-side references only (no provider-side deletes in this slice).",
          "permissions": [
            "addons/development/projects"
          ],
          "endpoint": "/api/v1/addons/development/projects",
          "scripts": []
        },
        {
          "name": "toolchain-installer-recipes",
          "displayName": "Coding: Toolchain Installer Recipes (#145)",
          "description": "Inspect the typed installer-recipe catalog (bundled / package-manager / upstream-reference / owner-url / manual), validate a recipe fail-closed (HTTPS-only, allowlist, size, timeout, redirect, SSRF, hash) and emit an audit-able InstallPlan (download source, version, hash/signature, license/terms, target runtime, network policy, argv). Owner acknowledgements are bound to recipe hash + terms URL/version + license SPDX id; any change invalidates the existing ack. No curl|bash, no silent license defaults, no execution on the Core host.",
          "permissions": [
            "addons/development/status"
          ],
          "scripts": []
        },
        {
          "name": "coding-supervision",
          "displayName": "Coding: Session-Supervision (#130)",
          "description": "Observe and steer long-lived coding sessions: list child sessions, read the in-flight todo list with revision cursors, read paginated file diffs, fork a child session, summarise session state, list pending permission requests and answer them with autonomy-policy gating + audit. Read-only callers can observe but cannot fork or answer permissions. Engines without a supervision surface (Claude Code, Codex today) report unsupported for every operation.",
          "permissions": [
            "addons/development/sessions"
          ],
          "endpoint": "/api/v1/addons/development/sessions/:id/supervision",
          "scripts": []
        },
        {
          "name": "coding-lsp-profiles",
          "displayName": "Coding: LSP-Server-Verwaltung (#234)",
          "description": "Manage LSP server profiles per workspace with three source kinds (local-binary, npm, docker). Owner-allowlist is mandatory (fail-closed); profiles not allowlisted stay in 'draft' state. Materializers write engine-native manifests (OpenCode `.opencode/config.json`, Claude Code `.lsp.json`, Codex `config.toml` lsp_servers block) idempotently; the effective LSP set is the intersection of workspace profiles, owner allowlist, and engine support. A 3 s health check aborts session start with `LSP_UNHEALTHY`; per-user isolation enforced via the scoped store (foreign profiles return 404).",
          "permissions": [
            "addons/development/workspaces"
          ],
          "endpoint": "/api/v1/addons/development/workspaces/:ws/lsp",
          "scripts": []
        },
        {
          "name": "coding-plugin-profiles",
          "displayName": "Coding: Plugin-Profile mit Hook-/Tool-Grenzen (#134)",
          "description": "Manage trusted plugin profiles (npm-pinned or project-local). Owner-allowlist is mandatory (fail-closed); profiles not allowlisted stay in 'draft' state. Each profile is hash-pinned (SHA-256), declares the allowed hook events + tool names + secrets + roles, and binds to a session via a frozen snapshot. OpenCode loads TS/npm wrappers from `.opencode/plugins/`; existing user files at the target path are preserved (Aiopta writes to a sibling `.aiopta-<id>.ts` fallback). Hash drift, tool-collision, host-path escape and undeclared secret access all fail closed. Hook/tool calls are audited with redacted secrets; non-declared env-var access marks the session degraded.",
          "permissions": [
            "addons/development/plugin-profiles",
            "addons/development/sessions"
          ],
          "endpoint": "/api/v1/addons/development/plugin-profiles",
          "scripts": []
        },
        {
          "name": "coding-tool-bindings",
          "displayName": "Coding: Tool-Bindings (MCP + Custom Tools) per Role/Session (#133)",
          "description": "Provider-neutral tool-binding contract: owner-curated profiles reference MCP-Server sources (#235) and Custom Tools (exec/http/function), the session-start gate resolves a frozen per-session snapshot = intersection of profile × role × engine capability × agent permission, tool-name collisions are namespaced deterministically, OAuth/header secrets are emitted as ${REF:...} placeholders (never literal), the redacted status view surfaces effective tool names + health + engine support without credentials, and the OpenCode adapter materialises engine-native config (Claude Code + Codex report unsupported until a follow-up).",
          "permissions": [
            "addons/development/tool-bindings"
          ],
          "endpoint": "/api/v1/addons/development/tool-bindings",
          "scripts": []
        },
        {
          "name": "coding-task-graph",
          "displayName": "Coding: Persistent Task-Graph with Parallel Child Sessions (#136)",
          "description": "Accept a DAG-shaped plan (tasks with role + doneDefinition + scope + tests + dependsOn), validate it (no cycles, deps resolve, bounded size), schedule ready tasks in isolated child sessions per role, route structured handoff artifacts between roles, and gate run completion on an independent mandatory QA step that depends on every mandatory coder task. Restart-safe: persisted state survives crashes; the scheduler refuses to start a second attempt for the same task while one is active; orphaned attempts are reaped on the next tick. Reuses the leases module (race-safe single-writer), the worktree module (per-session isolation), and the green-gate (merge-back is green-gated).",
          "permissions": [
            "addons/development/task-graphs"
          ],
          "endpoint": "/api/v1/addons/development/task-graphs",
          "scripts": []
        },
        {
          "name": "coding-project-provisioning",
          "displayName": "Coding: Project Provisioning (#141) — Idempotent Repo Bundle",
          "description": "Provision a CodingProject's repository bundle (code + tooling + optional docs) via the Git-Host-Adapter layer (#140). Build a versioned policy (default branch, label taxonomy, branch protection), dry-run the plan (per-resource capability + rollback classification), then apply with persisted operation ledger keyed on (project, resource_key, desired_hash). Idempotent re-runs are no-ops; partial failures are retryable per resource; rollback deletes only newly-created empty resources on explicit confirmation. No provider-side deletes of pre-existing repos, no merge/branch-protection override, no Aiopta constants in the policy.",
          "permissions": [
            "addons/development/projects"
          ],
          "endpoint": "/api/v1/addons/development/projects/:id/provision",
          "scripts": []
        },
        {
          "name": "agent-tools",
          "displayName": "Coding: Agent-Tool-API für Sessionsteuerung (#113, Least Privilege)",
          "description": "Provider-neutral tool surface for authorised agents to manage coding sessions end-to-end: session_create / session_list / session_get / session_prompt_async / session_events / session_abort. Same AuthZ + status machine as the UI; every mutating call is idempotent (replay-safe under the same idempotency key) and audited. Owner-scoped — foreign session ids return 404 (no existence leak). Responses are paginated/cursor-based (nextCursor/hasMore) and secret-redacted; engine verdicts (`supported` / `unsupported` / `unavailable`) are reported honestly (no fake-green). No tool for arbitrary host shell, free host paths, credential values, merge or foreign session ids. Supervision (children/todos/diff/fork/summarize/permissions) stays in #130.",
          "permissions": [
            "addons/development/sessions"
          ],
          "endpoint": "/api/v1/addons/development/agent-tools",
          "scripts": []
        },
        {
          "name": "coding-orchestration-ui",
          "displayName": "Coding: Orchestration-UI für Rollen, Task-Graph, Child-Sessions und QS-Evidenz (#137)",
          "description": "Observe and steer active runs: render the canonical task-graph (role, status, dependencies, attempt, session, runtime, blocker), the parent/child session tree, and a per-task supervision detail (handoffs, todos, diff summary, QS evidence, pending permissions, audit timeline). Actions (pause / resume / cancel / approve / deny) are validated server-side; a stale action returns 409 and refreshes the view (no optimistic mutation, no fake-green unsupported panel). Snapshots are revision-tagged so a reconnect resumes from the last cursor without duplication.",
          "permissions": [
            "addons/development/task-graphs",
            "addons/development/sessions"
          ],
          "endpoint": "/api/v1/addons/development/orchestration/overview",
          "scripts": [
            {
              "id": "coding-orchestration-ui-cli",
              "name": "coding-orchestration-ui-cli",
              "path": "coding-orchestration-ui/cli.sh",
              "sha256": "2a5478bdac7e2f91b336c807e6611593874890006b44624690d9bbfc1d924416",
              "interpreter": "bash",
              "timeoutMs": 10000,
              "permissions": [
                "addons/development/sessions",
                "addons/development/task-graphs"
              ],
              "inputSchema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 20
                  },
                  "timeoutMs": {
                    "type": "integer",
                    "minimum": 100,
                    "maximum": 300000
                  },
                  "input": {
                    "type": "object",
                    "description": "Request-Body als JSON — niemals Secrets"
                  },
                  "dryRun": {
                    "type": "boolean",
                    "default": false,
                    "description": "Request validieren ohne Mutation — kein API-Call"
                  }
                }
              },
              "outputSchema": {
                "type": "object",
                "required": [
                  "ok"
                ],
                "properties": {
                  "ok": {
                    "type": "boolean"
                  },
                  "status": {
                    "type": "integer"
                  },
                  "data": {},
                  "error": {
                    "type": "string"
                  },
                  "truncated": {
                    "type": "boolean"
                  }
                }
              },
              "size": 12176
            }
          ]
        },
        {
          "name": "coding-project-wizard-dashboard",
          "displayName": "Coding: Project Setup-Wizard + Health-Dashboard (#149)",
          "description": "Guided Setup-Wizard (profile lite/managed/hosted → source existing/existing-forgejo/new-hosted-forgejo → repo → review with capability/mutation plan + license + typed-name confirmation → apply) plus a Health-Dashboard that surfaces the resource graph, image/tool versions + drift, credentials (configured boolean only, never the value), and the configured/installed/healthy/degraded/drifted health lens with the last-probe timestamp. Detach/Delete stay behind the typed-name gate; the server re-validates the confirm flag. All state lives server-side; reload/tab-switch never re-fires a half-applied create.",
          "permissions": [
            "addons/development/projects"
          ],
          "endpoint": "/api/v1/addons/development/projects/:id/health",
          "scripts": []
        },
        {
          "name": "proprietary-coding-tools",
          "displayName": "Coding: Proprietäre & benutzerdefinierte Coding-Tools (#147)",
          "description": "Manage proprietary / closed-source coding-tool install references (Claude Code as first curated upstream example). Owner-only flow: instance owner pins official upstream URL OR uploads artefact hash, sees terms text + license URL + hash + cache policy + install-plan preview (resolved redirect chain, argv, mounts, env names) and approves explicitly. Reference-only mode forbids rehost/mirror/cache; cachePolicy is locked to ephemeral|none; bytes are deleted after the isolated build (no registry/backup/tooling-repo copy); session-start is gated on probe + credential/subscription + terms-drift success. Owner acknowledgement binds to recipe hash + terms URL/version + license SPDX id; any drift invalidates the ack.",
          "permissions": [
            "addons/development/admin/proprietary-tools"
          ],
          "endpoint": "/api/v1/addons/development/admin/proprietary-tools",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>🧠 Development</h1>\n<p>Lets agents start, steer, inspect, and terminate tmux-backed coding sessions (Claude Code, opencode, codex) via a pluggable engine-adapter registry, with per-session git-worktree isolation so parallel sessions on one repo never corrupt each other, plus custom model and provider environment overrides, plus deterministic per-session project-instructions (AGENTS.md + global + explicit) load with full provenance, plus a portable Skills + Commands catalog with provenance, hash, permissions and per-workspace materialisation to engine-native paths (#132), plus versioned installer recipes with license, terms, hash and supply-chain policy for the future per-project Toolchain-Runtime (#145).</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>development</code></li><li><strong>Version:</strong> 1.6.0</li><li><strong>Fähigkeiten:</strong> coding-sessions, tmux-control, agent-steering, custom-llm-routing, terminal-automation, git-worktree-isolation, engine-adapters, parallel-orchestration, project-instructions, coding-resources (#132), skills-catalog, commands-catalog, engine-native-materialisation, <strong>toolchain-installer-recipes</strong> (#145), recipe-schema, recipe-validator, license-policy, supply-chain-policy, owner-acknowledgement, sbom-provenance</li><li><strong>UI-Seite:</strong> <code>/addons/development</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET | <code>/api/v1/addons/development/engines</code> |<br />| GET | <code>/api/v1/addons/development/providers</code> |<br />| GET, POST | <code>/api/v1/addons/development/auth</code> |<br />| GET, POST | <code>/api/v1/addons/development/sessions</code> |<br />| POST | <code>/api/v1/addons/development/sessions/[id]/input</code> |<br />| GET | <code>/api/v1/addons/development/sessions/[id]/output</code> |<br />| POST | <code>/api/v1/addons/development/sessions/[id]/run</code> |<br />| POST | <code>/api/v1/addons/development/sessions/[id]/terminate</code> |<br />| GET | <code>/api/v1/addons/development/status</code> |<br />| GET, POST | <code>/api/v1/addons/development/role-profiles</code> |<br />| GET, DELETE | <code>/api/v1/addons/development/role-profiles/[role]</code> |<br />| GET, POST, DELETE | <code>/api/v1/addons/development/role-profiles/[role]/fallback</code> |</p>\n<h2>Rollenprofile (Issue #135)</h2>\n<p>Add-on bindet sechs Rollen an separate Engine-/Modell-/Instructions-/<br />Skill-/Tool-/Permissions-Bundles, versioniert pro Owner:</p>\n<ul><li><code>architect</code> · <code>planner</code> · <code>coder</code> · <code>qa</code> · <code>research</code> · <code>docs</code>.</li></ul>\n<p>Jedes Profil deklariert explizit:</p>\n<p>- <strong>Engine + Modell</strong> — kein impliziter Default, jede Konfigurationslücke<br />  schlägt fehl (INBOX-86).<br />- <strong>Permissions</strong> — `canRead / canEdit / canBash / canTask / canSkill /<br />  canTool / canCommit / canMerge`; die ISSUE verbietet mehrere Kombinationen<br />  implizit (siehe „Negative-Scope&quot;, unten).<br />- <strong>Limits</strong> — <code>MaxSteps / TimeoutSeconds / Budget ( + WarnAt)</code>.<br />- <strong>Handoffs</strong> — gerichtete Kanten zu anderen Rollen + erlaubte Artefakte<br />  (<code>validated-plan</code>, <code>released-task-bundle</code>, <code>diff-bundle</code>, <code>test-evidence</code>,<br />  <code>acceptance-rationale</code>, <code>run-audit</code>).</p>\n<p>Der <strong>Resolver</strong> materialisiert pro Rollen-Snapshot das konkrete<br />Engine-Setup, prüft Skills/Tools gegen die Registry und friert das<br />Ergebnis als unveränderlichen <strong>Snapshot</strong> ein (SHA-256 über Profil,<br />Instructions, Modell). Läuft der Agent, hält er seinen Snapshot für die<br />gesamte Laufzeit — eine Profiländerung mitten im Lauf ändert nichts am<br />laufenden Run, sondern erst am nächsten.</p>\n<h3>Negative-Scope (verbindliche Defaults)</h3>\n<p>- <code>qa</code> darf <strong>nicht edit / commit / merge</strong> und <strong>nicht</strong> Skills aufrufen —<br />  der Owner-Muss-Override (<code>permissions.ownerGranted=true</code>) wird<br />  auditiert.<br />- <code>coder</code> darf <strong>nicht</strong> Acceptance-Kriterien als bestanden markieren<br />  (<code>canTask:false</code>).<br />- Kein Modell/Engine-Silent-Swap: ein nicht verfügbares Modell / eine nicht<br />  verfügbare Engine wird <strong>entweder</strong> über den explizit registrierten<br />  Fallback (<code>POST /role-profiles/{role}/fallback</code>) ersetzt <strong>oder</strong> der Run<br />  schlägt fehl (<code>RoleProfileUnavailableError</code>).<br />- Skill-/Tool-IDs, die nicht im Add-on-Registry existieren, schlagen mit<br />  einem konkreten <code>resource_not_found</code>-Fehler fehl — kein stilles Ignorieren.</p>\n<h3>Beispiel</h3>\n<pre><code class=\"lang-bash\"># Aktive Profile aller Rollen lesen\ncurl -s -H &quot;Authorization: Bearer $AIOPTA_API_KEY&quot; \\\n  https://&lt;instance&gt;/api/v1/addons/development/role-profiles</code></pre>\n<p># Neuen Planner-Snapshot speichern (Version 2)<br />curl -s -X POST -H &quot;Authorization: Bearer $AIOPTA_API_KEY&quot; -H &#39;Content-Type: application/json&#39; \\<br />  -d &#39;{<br />    &quot;role&quot;: &quot;planner&quot;,<br />    &quot;version&quot;: 2,<br />    &quot;engine&quot;: &quot;claude-code&quot;,<br />    &quot;model&quot;: &quot;anthropic/claude-opus-4-7&quot;,<br />    &quot;instructions&quot;: &quot;You are the PLANNER agent ...&quot;,<br />    &quot;skills&quot;: [],<br />    &quot;tools&quot;: [],<br />    &quot;permissions&quot;: {<br />      &quot;canRead&quot;: true, &quot;canEdit&quot;: true, &quot;canSkill&quot;: true,<br />      &quot;canBash&quot;: false, &quot;canTask&quot;: false, &quot;canTool&quot;: true,<br />      &quot;canCommit&quot;: false, &quot;canMerge&quot;: false<br />    },<br />    &quot;limits&quot;: { &quot;maxSteps&quot;: 6, &quot;timeoutSeconds&quot;: 1500, &quot;budget&quot;: 4000 },<br />    &quot;handoffsOut&quot;: [<br />      { &quot;from&quot;: &quot;planner&quot;, &quot;to&quot;: &quot;coder&quot;, &quot;artifacts&quot;: [<br />          { &quot;kind&quot;: &quot;validated-plan&quot; }, { &quot;kind&quot;: &quot;released-task-bundle&quot; } ] }<br />    ]<br />  }&#39; \\<br />  <a href=\"https://&amp;lt;instance&amp;gt;/api/v1/addons/development/role-profiles\" rel=\"noopener noreferrer\">https://&amp;lt;instance&amp;gt;/api/v1/addons/development/role-profiles</a><br />```</p>\n<h2>Session-Erstellung (INBOX-58)</h2>\n<p>- <strong>Arbeitsverzeichnis (BUG1):</strong> <code>cwd</code> weglassen ⇒ auto-erzeugter Default-Workspace<br />  (<code>.aiopta-coding-worktrees</code>). Fehlender Pfad ⇒ klarer Fehler mit „create it?&quot;-Angebot<br />  (<code>createCwdIfMissing</code>); Isolation braucht ein existierendes Git-Repo.<br />- <strong>Provider/Modell-Dropdowns (GAP3):</strong> <code>GET /providers</code> liefert konfigurierte<br />  Provider + Modelle für Engine→Provider→Modell-Auswahl (kein Env-Tippen); Roh-Env<br />  bleibt als „Advanced&quot;.<br />- <strong>Auth pro Engine (GAP2):</strong> <code>GET/POST /auth</code> — je Engine „API-Key | Login&quot;<br />  (Claude setup-token/OAuth, codex ChatGPT-OAuth); Token sicher pro (User,Engine),<br />  nie geloggt; nicht-konfiguriert ⇒ graziöser Hinweis (Owner-Aktivierung Block H).<br />- <strong>Engines + Verfügbarkeit (#130):</strong> <code>GET /engines</code> liefert je Engine die<br />  Capability-Flags <strong>und</strong> ein <code>available</code>-Flag. Claude Code + opencode gaten über<br />  ihre CLI auf PATH; <strong>Codex (OpenAI)</strong> ist die 3. Engine und ist <strong>key-aware</strong> —<br />  <code>available:false</code> bis der Owner einen Codex/OpenAI-Key setzt (<code>CODEX_API_KEY</code> /<br />  <code>OPENAI_API_KEY</code> in der Addon-Config oder einen konfigurierten<br />  <code>openai</code>/<code>openai-codex-oauth</code> Provider) oder ein ChatGPT-Login verbindet. Ein<br />  Headless-<code>run</code> auf nicht-konfiguriertem Codex liefert ein sauberes<br />  „nicht konfiguriert&quot;-Ergebnis (kein Crash, kein Spawn). Key/Endpoint +<br />  CLI-Container-vs-API: OPEN-DECISIONS Block H #76 (ADR-061, Addendum zu ADR-046).</p>\n<h2>Skills + Commands Katalog (#132)</h2>\n<p>Der Coding-Orchestrator verwaltet einen <strong>kanonischen Resource-Katalog</strong> aus<br />drei Quellen, in fester Präzedenz (<code>project &gt; addon &gt; global</code>):</p>\n<p>- <strong>Skills</strong> liegen unter <code>&lt;root&gt;/skills/&lt;name&gt;/SKILL.md</code> (Markdown mit<br />  YAML-Frontmatter: <code>name</code>, <code>description</code>, optional <code>version</code>, <code>permissions</code>).<br />- <strong>Commands</strong> liegen unter <code>&lt;root&gt;/commands/&lt;name&gt;.md</code> (Frontmatter: <code>name</code>,<br />  <code>description</code>, <code>prompt</code>, optional <code>role</code>, <code>engine</code>, <code>subtask</code>,<br />  <code>permissions</code>).</p>\n<p>Bestehende Projektdateien werden bevorzugt gelesen statt kopiert — der<br /><code>project</code>-Source-Lookup reicht in <code>.opencode/skills/</code> und nutzt OpenCode-<br />konforme Layouts ohne Migration.</p>\n<h3>Validierung (Sicherheits-Gate)</h3>\n<p>Die Validierung lehnt ab:</p>\n<p>- Unbekannte Frontmatter-Felder (jede <code>script:</code>/<code>command:</code>/<code>exec</code>-Field auf<br />  einer Skill wird abgelehnt, <strong>nicht</strong> automatisch ausgeführt).<br />- Path-Traversal in <code>originPath</code> (kein <code>..</code>, kein Backslash auf POSIX).<br />- Dateien &gt; 64 KiB oder Prompt mit NUL/Control-Bytes.<br />- Permissions ausserhalb des bekannten Vokabulars (<code>files:read</code>, <code>git:write</code>,<br />  <code>mcp:invoke</code>, …) — keine impliziten Grants.</p>\n<h3>Materialisierung</h3>\n<p>Pro Session materialisiert der Orchestrator freigegebene Resources in<br /><strong>engine-native Pfade innerhalb des Workspace</strong> (nie in den Projektbaum):</p>\n<p>| Engine | Skills | Commands |<br />|---|---|---|<br />| <code>opencode</code> | <code>.opencode/skills/&lt;name&gt;/SKILL.md</code> + <code>.agents/skills/&lt;name&gt;/SKILL.md</code> (Mirror) | <code>.aiopta-resources/opencode/commands/&lt;name&gt;.md</code> |<br />| <code>claude-code</code> | <code>unsupported</code> (Mapping noch zu spezifizieren) | <code>unsupported</code> |<br />| <code>codex</code> | <code>unsupported</code> (Mapping noch zu spezifizieren) | <code>unsupported</code> |</p>\n<p>Regeln:</p>\n<p>- Materialisierung <strong>überschreibt nie</strong> eine existierende Projektdatei.<br />- Skill-Inhalte werden <strong>nie ausgeführt</strong> — nur als opaque Markdown geschrieben.<br />- Cleanup entfernt <strong>nur</strong> die im Manifest gelisteten Dateien.<br />- Engine-Support-Verdikte werden in <code>MaterializeResult.refused</code> mit Reason<br />  protokolliert; <code>describeActiveResources()</code> macht die Teilfähigkeit im<br />  Session-Status sichtbar.</p>\n<h3>Bekannte Grenzen (Coding-Resources)</h3>\n<p>- <strong>Claude Code + Codex:</strong> Materialisierung ist als <code>unsupported</code> markiert,<br />  bis ein getestetes Mapping vorliegt. Der <code>unsupported</code>-Verdikt ist<br />  dokumentiert und niemals ein stilles Degradieren.</p>\n<h2>Bekannte Grenzen</h2>\n<p>- <strong>Per-User-Workspace-FS-Isolation (GAP4):</strong> zurückgestellt auf die<br />  #136-Isolation-Foundation. Sessions sind bereits user-scoped (<code>user_id</code>); nur die<br />  per-User-Dateisystem-Sandbox wird dort gebaut.</p>\n<h2>Project-Instructions (#131)</h2>\n<p>Beim Session-Start wird deterministisch ein Project-Context aus der<br /><code>AGENTS.md</code>-Hierarchie des Workspaces, dem globalen Aiopta-Context<br />(<code>CODING_ORCHESTRATOR_GLOBAL_AGENTS_FILE</code> / <code>~/.config/aiopta/AGENTS.md</code>) &lt;!-- security-allow: env-var name (CODING_ORCHESTRATOR_*), not a secret --&gt;<br />und einer optionalen expliziten Datei-Liste<br />(<code>CODING_ORCHESTRATOR_INSTRUCTION_FILES</code>, addon-config) geladen. Die &lt;!-- security-allow: env-var name (CODING_ORCHESTRATOR_*), not a secret --&gt;<br />kanonische, deterministisch geordnete Inhaltsmenge wird in den<br /><code>appendSystemPrompt</code> der Engine gespleißt — **identisch für Claude Code,<br />opencode und Codex**, die jeweilige Engine entscheidet nur, ob sie<br /><code>systemPrompt</code> als Flag/Env unterstützt.</p>\n<p>| Eigenschaft | Verhalten |<br />|---|---|<br />| Präzedenz (niedrig → hoch) | global → repo-root → tieferes <code>AGENTS.md</code> → explizit |<br />| Hash je Datei | SHA-256, im Provenance-Datensatz persistiert |<br />| Per-File-Limit | 64 KiB (Default) — größer ⇒ Reject + Warnung |<br />| Total-Budget | 256 KiB (Default) — Überschreitung ⇒ <code>truncated</code> |<br />| Binary-Erkennung | NUL-Byte in den ersten 8 KiB ⇒ Reject |<br />| Symlink-Escape | <code>realpath</code> muss innerhalb des Workspace-Roots bleiben |<br />| Traversal | <code>..</code>-Komponenten und nicht-absolute explizite Pfade ⇒ Reject |<br />| Request-Kontrolle | HTTP-Body kann KEINE zusätzlichen Instructions-Dateien benennen |<br />| Provenance-Endpoint | <code>GET /sessions/:id</code> ⇒ <code>instructions</code> (paths, hashes, status, warnings) |</p>\n<h2>Toolchain Installer Recipes (#145)</h2>\n<p>Der Coding-Orchestrator definiert ein versioniertes Rezept-Schema für Coding-Tools<br />(Claude Code, opencode, ripgrep, …), das die zukünftige per-Project Toolchain-Runtime<br />(#144) mit einer prüfbaren Installationsbasis versorgt. Der Slice in #145 liefert<br />Schema + Validator + Planner + Provenance; der Runtime-Executor bleibt #144.</p>\n<h3>Fünf Quell-Kinds</h3>\n<p>| Kind                        | Download                       | Owner-Approval | Hash    | Netzwerk-Policy | Auto-Exec |<br />|-----------------------------|--------------------------------|----------------|---------|-----------------|-----------|<br />| <code>bundled</code>                   | aus Image-Layer                | nein           | ja      | nein            | n/a       |<br />| <code>package-manager</code>           | aus System-Paket-Manager       | nein           | n/a     | nein            | ja        |<br />| <code>upstream-reference</code>        | aus Upstream-URL               | nein           | <strong>ja</strong>  | ja              | ja        |<br />| <code>owner-url</code>                 | aus Owner-freigegebener URL    | <strong>ja</strong>         | <strong>ja</strong>  | ja              | ja        |<br />| <code>manual</code>                    | kein Download (Runbook-Text)   | <strong>ja</strong>         | n/a     | nein            | <strong>nein</strong>  |</p>\n<h3>Hard Rules (Negativanforderungen)</h3>\n<p>- Keine unversionierten Pakete / Images / URLs — <code>version</code> ist Pflichtfeld.<br />- Kein Hash-Bypass bei URL-Artefakten — <code>digest.sha256</code> ist Pflicht für<br />  <code>upstream-reference</code> und <code>owner-url</code>.<br />- Kein <code>curl|bash</code> / <code>irm|iex</code> — <code>manual</code>-Steps, die wie eine piped-payload aussehen,<br />  werden abgelehnt.<br />- Keine automatische Terms-Akzeptanz — <code>owner-confirmation</code>-Lizenzen und<br />  <code>owner-url</code> / <code>manual</code>-Rezepte benötigen ein <code>Acknowledgement</code>, das an<br />  Rezept-Hash, Terms-URL+Version und Lizenz-SPDX-Id gebunden ist. Änderungen an<br />  einem dieser Felder invalidieren die vorhandene Zustimmung.<br />- Keine Ausführung auf dem Core-Host — der Planner emittiert nur den<br />  <code>InstallPlan</code>; die Runtime (#144) führt ihn aus.</p>\n<h3>Public API (issue #145)</h3>\n<p>Unter <code>addons/development/src/lib/toolchains/installers/</code>:</p>\n<p>- <code>types.ts</code> — <code>Recipe</code> (diskriminierte Union über 5 kinds), <code>InstallPlan</code>,<br />  <code>Acknowledgement</code>, <code>NetworkPolicy</code>, <code>ProvenanceAttestation</code>, <code>SbomEntry</code>,<br />  <code>ToolchainLockSlice</code>.<br />- <code>validator.ts</code> — <code>validateRecipe</code> (fail-closed, sammelt alle Issues),<br />  <code>isHostAllowedByPolicy</code>, <code>isAcknowledgementApplicable</code>.<br />- <code>network-guard.ts</code> — <code>assertAllowedDownload</code> (HTTPS-only, SSRF-Guard gegen<br />  private/loopback/link-local/multicast-IPv4 + IPv6, Host-Allowlist,<br />  Größe/Timeout-Oberflächen) und <code>assertRedirectAllowed</code><br />  (none/same-host/same-allowlist).<br />- <code>acknowledgement.ts</code> — <code>recordAcknowledgement</code>, <code>revokeAcknowledgement</code>,<br />  <code>isAcknowledgementFresh</code>.<br />- <code>catalog.ts</code> — <code>createRecipeCatalog</code>, <code>addRecipe</code> (Validator-fail-closed),<br />  <code>listRecipes</code>, <code>findRecipesByTool</code>, <code>findRecipeFor</code>, <code>listHashDriftEntries</code>.<br />- <code>plan.ts</code> — <code>buildPlan</code> (emittiert den InstallPlan inkl. Quell-URL,<br />  Hash/Signatur, Lizenz/Terms, Zielruntime, Netzwerk-Policy und argv),<br />  <code>deriveCachePlan</code> (Cache-/Redistribution-Policy für Uninstall/Upgrade).<br />- <code>provenance.ts</code> — <code>buildProvenance</code> (SLSA-Provenanz-Attestation),<br />  <code>buildSbom</code>, <code>buildToolchainLockSlice</code>, <code>verifyToolchainLockSlice</code>.</p>\n<h3>Tests</h3>\n<p><code>addons/development/tests/unit/toolchain-installers-*.test.ts</code>:</p>\n<p>- <code>types</code> — Konstruktion + Recipe-Hash-Stabilität (Approval-Metadaten zählt<br />  nicht zum Hash).<br />- <code>validator</code> — fail-closed für jeden Kind; Security-Fixtures für bösartige<br />  URL/redirect/private-IP/Oversize/Hash-Mismatch/Shell-Injection.<br />- <code>network</code> — SSRF-Guard (IPv4 reservierte Bereiche + IPv6 reserved),<br />  Allowlist-Wildcards inkl. Suffix-Angriff, Redirect-Policies.<br />- <code>acknowledgement</code> — Terms-/Hash-/License-Änderungen invalidieren<br />  die Zustimmung (Akzeptanzkriterium &quot;Quelle/Hash/Terms ändern sich -&gt;<br />  bisherige Zustimmung ungültig&quot;).<br />- <code>plan</code> — golden Plans für <code>package-manager</code>, <code>upstream-reference</code> und<br />  <code>manual</code>; Cache-Policy-Surface.<br />- <code>provenance</code> — SLSA-Attestation + SBOM + Lock-Slice-Verifikation<br />  (inkl. Reject bei Recipe-Drift).</p>\n<p>Ausführen: <code>npx vitest run addons/development/tests/unit/toolchain-installers-*.test.ts</code>.</p>",
          "byteSize": 14725
        }
      }
    },
    {
      "id": "dive-log",
      "uuid": "7e0cbe39-e48e-4ffe-9158-d64336f386eb",
      "name": "Dive Log (Tauchgänge erfassen, importieren, auswerten)",
      "version": "1.2.1",
      "description": "Per-User Tauchgangslogbuch: CRUD für eigene Dives, CSV- + Subsurface-XML-Import mit Vorschau, Deduplizierung, expliziter Zeitzonen-/Einheiten-Normalisierung sowie Statistiken (Anzahl, Gesamtzeit, Max-/Durchschnittstiefe, Monatsverlauf) und Karte mit geclusterten Punkten. Lokale SQLite ist die Quelle der Wahrheit (ADR-054). SI-Einheiten intern (Meter/Sekunde/UTC).",
      "author": "Aiopta",
      "icon": "🤿",
      "capabilities": [
        "dive-log",
        "dives",
        "import",
        "csv",
        "subsurface",
        "stats",
        "map"
      ],
      "tags": [],
      "download": "/artifacts/dive-log-1.2.1.zip",
      "sha256": "d626da3f6f0aad75e524761a042ab4e7cc02224a201c1051ed3fe336f6554b59",
      "size": 223886,
      "page": {
        "id": "dive-log-page",
        "path": "/addons/dive-log",
        "name": "Dive Log",
        "icon": "🤿",
        "section": "Lifestyle",
        "component": "@addons/dive-log/src/components/DiveLogPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/dive-log/dives",
        "/api/v1/addons/dive-log/dives/:id",
        "/api/v1/addons/dive-log/imports/preview",
        "/api/v1/addons/dive-log/imports/:id",
        "/api/v1/addons/dive-log/imports/:id/confirm",
        "/api/v1/addons/dive-log/stats"
      ],
      "skills": [
        {
          "name": "dive-log-read",
          "displayName": "Dive Log: Tauchgänge lesen",
          "description": "Listet die Tauchgänge des Nutzers und liest einzelne Einträge. Reiner Lesezugriff — schreibt nichts.",
          "permissions": [
            "addons/dive-log/dives"
          ],
          "endpoint": "/api/v1/addons/dive-log/dives",
          "scripts": []
        },
        {
          "name": "dive-log-write",
          "displayName": "Dive Log: Tauchgänge erfassen & importieren",
          "description": "Erfasst, löscht und importiert Tauchgänge für den Nutzer (per-User gebucht) inkl. CSV-/Subsurface-Preview + Commit.",
          "permissions": [
            "addons/dive-log/dives",
            "addons/dive-log/imports"
          ],
          "endpoint": "/api/v1/addons/dive-log/imports/preview",
          "scripts": []
        },
        {
          "name": "dive-log-stats",
          "displayName": "Dive Log: Statistiken & Karte",
          "description": "Berechnet Kennzahlen (Anzahl, Gesamtzeit, Max-/Ø-Tiefe, Monatsverlauf) und projiziert Dives mit gültigen Koordinaten auf eine Karte. Reiner Lesezugriff — verändert weder Dives noch gespeicherte SI-Werte.",
          "permissions": [
            "addons/dive-log/dives",
            "addons/dive-log/stats"
          ],
          "endpoint": "/api/v1/addons/dive-log/stats",
          "scripts": []
        }
      ],
      "mcpServers": [
        {
          "id": "dive-log-tools",
          "displayName": "Dive Log tools",
          "description": "Read-only dive-log statistics and map projection for agents: headline + monthly stats, coordinate filtering and map-point/bounds derivation — the same pure functions the dive-log stats page uses (issue #413, epic #399). Reads nothing external.",
          "transport": "stdio",
          "runtime": "node",
          "entry": "dive-log-tools/server.mjs",
          "permissions": [
            "addons/dive-log/stats"
          ],
          "toolPolicy": {
            "mode": "read-only",
            "allowDestructive": false
          },
          "configRefs": [],
          "credentialRefs": [],
          "archiveEntry": "dive-log/mcp/servers/dive-log-tools/server.mjs",
          "sha256": "97056877b4e9902ae462565ddbc33f8446f4c3ad1225ca877c824303b28eef8f",
          "size": 18942,
          "descriptorVersion": 1
        }
      ],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Dive Log (aiopta-addons)</h1>\n<p>Per-User Tauchgangslogbuch. Erfasst eigene Tauchgänge, importiert CSV- und<br />Subsurface-XML-Bestände mit Vorschau und Deduplizierung, berechnet<br />Kennzahlen (Anzahl, Gesamtzeit, Max-/Durchschnittstiefe, Monatsverlauf) und<br />zeigt Tauchgänge mit gültigen Koordinaten auf einer Karte (geclustert, mit<br />Inline-SVG-Fallback wenn kein Map-Provider konfiguriert ist). Alle Daten<br />in einem per-User scoped Schema (ADR-054). SI-Einheiten intern (Meter,<br />Sekunde, UTC); Zeitzonen + Einheiten werden beim Import explizit normalisiert<br />und nicht stillschweigend umgerechnet.</p>\n<p>Issue #35 liefert das CRUD-Fundament, #123 den Import-Layer, #124 die<br />Statistiken + Karte.</p>\n<h2>Endpoints</h2>\n<p>Alle Routen sind per-User scoped (<code>auth.id</code> aus <code>mc-session</code>). Eine fremde<br />ID liefert 404 ohne Existenz-Leak.</p>\n<p>| Methode | Pfad | Beschreibung |<br />|---|---|---|<br />| <code>GET</code>    | <code>/api/v1/addons/dive-log/dives</code> | Eigene Dives (Filter: <code>from</code>, <code>to</code>, <code>site</code>) |<br />| <code>POST</code>   | <code>/api/v1/addons/dive-log/dives</code> | Neuen Dive erfassen |<br />| <code>GET</code>    | <code>/api/v1/addons/dive-log/dives/[id]</code> | Einen Dive lesen |<br />| <code>DELETE</code> | <code>/api/v1/addons/dive-log/dives/[id]</code> | Einen Dive löschen |<br />| <code>POST</code>   | <code>/api/v1/addons/dive-log/imports/preview</code> | CSV / Subsurface-XML einlesen + Vorschau |<br />| <code>GET</code>    | <code>/api/v1/addons/dive-log/imports/[id]</code> | Vorschau-Status abrufen |<br />| <code>DELETE</code> | <code>/api/v1/addons/dive-log/imports/[id]</code> | Vorschau verwerfen |<br />| <code>POST</code>   | <code>/api/v1/addons/dive-log/imports/[id]/confirm</code> | Vorschau übernehmen |<br />| <code>GET</code>    | <code>/api/v1/addons/dive-log/stats</code> | Statistiken + Map-Punkte (Filter: <code>from</code>, <code>to</code>, <code>site</code>) |</p>\n<h2>Statistiken + Karte (<code>GET /stats</code>)</h2>\n<p>Liefert für die per-User gefilterte Dive-Menge:</p>\n<p>- <strong>Stat-Kennzahlen</strong>: <code>totalDives</code>, <code>totalDurationS</code>, <code>maxDepthM</code>,<br />  <code>avgDepthM</code>, <code>longestDiveS</code>, <code>avgDurationS</code>, <code>deepestSite</code>,<br />  <code>divesWithCoordinates</code>, <code>earliestDiveAt</code>, <code>latestDiveAt</code>.<br />- <strong>Monatsverlauf</strong>: <code>monthly[]</code> — Aggregate pro <code>YYYY-MM</code> (UTC, aufsteigend)<br />  mit <code>diveCount</code>, <code>totalDurationS</code>, <code>avgDepthM</code>, <code>maxDepthM</code>.<br />- <strong>Karte</strong>: <code>mapPoints[]</code> (nur Dives mit gültigen Koordinaten) plus<br />  <code>bounds</code> (Bounding-Box). Filter wirkt IDENTISCH auf Stats und Karte<br />  (<code>buildStatsResponse</code> leitet beide aus derselben <code>filterDives()</code>-Menge ab).<br />- <strong>Map-Provider</strong>: <code>provider.name</code> ist <code>&quot;fallback&quot;</code> (Inline-SVG, keine<br />  externe Verbindung) oder <code>&quot;iframe&quot;</code> (Operator hat <code>DIVE_LOG_MAP_URL_TEMPLATE</code><br />  gesetzt). <code>providerUrl</code> ist <code>null</code> im Fallback-Pfad → UI rendert die SVG.</p>\n<h3>Formeln (in <code>src/lib/stats.ts</code> + golden-fixture-getestet)</h3>\n<pre><code>totalDives        = filtered.length\ntotalDurationS    = Σ d.durationS\nmaxDepthM         = max(d.maxDepthM)\navgDepthM         = mean(d.maxDepthM)\nlongestDiveS      = max(d.durationS)\navgDurationS      = round(mean(d.durationS))\ndeepestSite       = site of the dive with max depth\ndivesWithCoordinates = count of dives with valid lat/lon\nmonthly[i]        = aggregate of dives in YYYY-MM (UTC), ascending\nmapPoints         = dives with finite lat ∈ [-90,90] AND finite lon ∈ [-180,180]\nbounds            = min/max of mapPoints (or null when empty)</code></pre>\n<p>Display-Unit (metrisch/imperial) ist <strong>Anzeige</strong>-Sache. Gespeicherte SI-Werte<br />werden NICHT verändert (per AC: &quot;metric/imperial Anzeige ändert nicht<br />gespeicherte SI-Werte&quot;). Die <code>StatsPanel</code>-Komponente konvertiert beim<br />Rendern, nicht beim Schreiben.</p>\n<h2>Map-Provider</h2>\n<p>Standardpfad: <strong>Inline-SVG mit geclusterten Punkten</strong>. Kein externer Call,<br />kein Key im Browserbundle, keine Drittanbieter-Verbindung. Die SVG-Fallback<br />zeichnet ein Equator-/Meridian-Raster und projiziert die Punkte über die<br />ermittelten Bounds. Cluster werden mit einem 1.5°-Grid gebildet (gleicher<br />Algorithmus wie <code>src/lib/map-provider.ts</code>).</p>\n<p>Operator-Knopf: <code>DIVE_LOG_MAP_URL_TEMPLATE</code> (env). Akzeptiert die<br />Platzhalter <code>{lat}</code>, <code>{lon}</code>, <code>{zoom}</code>. Wird er gesetzt, liefert der<br /><code>/stats</code>-Endpoint eine voll aufgelöste <code>providerUrl</code>, die das UI in einem<br />sandboxed Iframe rendert. Der Placeholder-Substitution ist opt-in:<br />unbekannte Platzhalter wie <code>{apiKey}</code> werden NICHT stillschweigend entfernt<br />(defensiv — der Operator steuert das Template bewusst).</p>\n<h2>CSV-Vorlage</h2>\n<p>Komma-getrennt, UTF-8, Header in der ersten Zeile. Spaltennamen sind<br />case-insensitive. Die Spalten <code>started_at</code>, <code>duration</code>, <code>depth</code> sind<br />Pflicht; alle anderen sind optional.</p>\n<pre><code>started_at,timezone,duration,duration_unit,depth,depth_unit,site,lat,lon,buddy,gas_mix,notes\n2024-06-15T10:30,Europe/Berlin,45,min,18,m,Lake Zurich,47.123,8.456,Hans,Air,Great viz\n2024-06-15T11:25,Europe/Berlin,52,min,22.4,m,Lake Zurich,47.123,8.456,Hans,EAN32,</code></pre>\n<p>Einheiten:</p>\n<p>- <code>duration</code> / <code>duration_unit</code>: <code>s</code> (Sekunden) oder <code>min</code> (Minuten). Default <code>min</code>.<br />- <code>depth</code> / <code>depth_unit</code>: <code>m</code> (Meter) oder <code>ft</code> (Fuß). Default <code>m</code>.<br />- <code>started_at</code> darf ISO-8601 mit Z, mit <code>±HH:MM</code> Offset, oder naive Wand-Uhr<br />  in der gewählten <code>timezone</code> sein.</p>\n<h2>Subsurface XML</h2>\n<p><code>&lt;divelog&gt;</code> mit einem oder mehreren <code>&lt;dive&gt;</code>-Blöcken. Gelesen werden<br /><code>&lt;date&gt;</code> (YYYY-MM-DD), <code>&lt;time&gt;</code> (HH:MM), <code>&lt;duration&gt;</code> (Sekunden), <code>&lt;depth&gt;</code><br />(Meter), <code>&lt;location&gt;</code>, <code>&lt;gps&gt;</code> (<code>lat,lon</code>), <code>&lt;buddy&gt;</code>, <code>&lt;cylinder&gt;&lt;description&gt;</code><br />(Gas Mix) und <code>&lt;notes&gt;</code>. Namespaces, CDATA, HTML-Entities ausser <code>&amp;amp;</code><br /><code>&amp;lt;</code> <code>&amp;gt;</code> <code>&amp;quot;</code> <code>&amp;apos;</code> werden nicht unterstützt — ein<br /><code>&lt;malformed&gt;</code>-Block im Test-Fixture dokumentiert das Negativ-Verhalten.</p>\n<h2>Idempotenz</h2>\n<p>Jede Datei wird über ihren SHA-256-Hash identifiziert. Ein zweiter Import<br />derselben Datei ergibt 100 % Duplikate (Re-Import ist sicher). Innerhalb<br />eines Imports gilt: die Zeilen-Fingerprints (<code>started_at</code> UTC × <code>site</code> ×<br /><code>max_depth_m</code> × <code>duration_s</code>) werden aufsteigend geprüft, sodass ein<br />Duplikat innerhalb derselben Datei nicht zweimal geschrieben wird.</p>\n<h2>Limits</h2>\n<p>- Maximal 5 MB pro Upload<br />- Maximal 5 000 Zeilen pro Datei<br />- <code>max_depth_m</code> &lt; 0, <code>duration_s</code> &lt; 0 und out-of-range Koordinaten liefern<br />  einen Zeilen-Fehler, nicht einen 400 für die ganze Datei<br />- Map-Punkte: nur Dives mit endlichen Koordinaten in<br />  <code>lat ∈ [-90, 90]</code> / <code>lon ∈ [-180, 180]</code> werden projiziert</p>\n<h2>Negativ-Scope</h2>\n<p>- Keine Cross-Repo-Refactors (Repository bleibt aiopta-addons)<br />- Keine Änderung am Auth/Login-Layer<br />- Keine DB-Schema-Änderung (Stats lesen nur, keine zweite Aggregationstabelle)<br />- Keine neuen externen Dependencies<br />- Keine Map-Keys im Browserbundle ohne vorhandene Credential-Verwaltung<br />  (Inline-SVG ist der Default; ein Provider ist Operator-Opt-in via env)<br />- Keine medizinischen Deko-Berechnungen</p>",
          "byteSize": 6447
        }
      }
    },
    {
      "id": "docker-manager",
      "uuid": "6790723a-a456-4adf-b0a7-53f3f8f46c33",
      "name": "Docker Manager (Multi-Host: Inventory, Trust, Mutations, Image Updates, Compose Orchestration + Preview)",
      "version": "1.4.3",
      "description": "Aiopta Docker Manager — read-only multi-host inventory + logs (#126), secure multi-host trust contract (#39: single-use enrollment, mTLS-CA, SSRF gate), controlled start/stop/restart with drift protection (#157), digest-pinned image updates with health rollback (#159), and compose orchestration with apply + partial-failure reconcile + rollback (#158; slice #329 adds the declarative compose-schema + versioned repo loader, slice #330 adds declarative Compose-Deploy preview with policy gate — render + 4 standard policy rules + secret-redacted preview output). Cross-user lookups map to 404 (no enumeration leak).",
      "author": "Aiopta",
      "icon": "🐳",
      "capabilities": [
        "docker-inventory",
        "docker-logs",
        "docker-readonly",
        "docker-multihost",
        "log-redaction",
        "container-start",
        "container-stop",
        "container-restart",
        "preview-with-drift",
        "idempotent-apply",
        "audit-without-secrets",
        "digest-pinned-image-update",
        "image-pull-before-mutation",
        "auto-rollback-on-unhealthy",
        "registry-allowlist",
        "credential-redaction",
        "docker-trust",
        "docker-enrollment",
        "docker-mtls-ca",
        "docker-ssrf-guard",
        "docker-audit",
        "compose-schema",
        "compose-versioned-loader",
        "compose-project",
        "compose-preview",
        "compose-apply-idempotent",
        "compose-partial-failure-reconcile",
        "compose-rollback",
        "compose-render",
        "compose-policy-gate",
        "compose-secret-redaction",
        "compose-drift-detection"
      ],
      "tags": [],
      "download": "/artifacts/docker-manager-1.4.3.zip",
      "sha256": "91fe638406a857756a4b16575e5be2e54d6f4705c117508e95d222003f9c3c1f",
      "size": 892910,
      "page": {
        "id": "docker-manager-page",
        "path": "/addons/docker-manager",
        "name": "Docker Manager",
        "icon": "🐳",
        "section": "Operations",
        "component": "@addons/docker-manager/src/components/DockerManagerPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/docker-manager/health",
        "/api/v1/addons/docker-manager/hosts",
        "/api/v1/addons/docker-manager/inventory",
        "/api/v1/addons/docker-manager/inventory/:host",
        "/api/v1/addons/docker-manager/logs/:host/:container",
        "/api/v1/addons/docker-manager/status/:host",
        "/api/v1/addons/docker-manager/redaction-patterns",
        "/api/v1/addons/docker-manager/preview",
        "/api/v1/addons/docker-manager/containers/:id/preview",
        "/api/v1/addons/docker-manager/containers/:id/start",
        "/api/v1/addons/docker-manager/containers/:id/stop",
        "/api/v1/addons/docker-manager/containers/:id/restart",
        "/api/v1/addons/docker-manager/containers/:id/update/preview",
        "/api/v1/addons/docker-manager/containers/:id/update/apply",
        "/api/v1/addons/docker-manager/containers/:id/update/rollback",
        "/api/v1/addons/docker-manager/audit",
        "/api/v1/addons/docker-manager/ca/status",
        "/api/v1/addons/docker-manager/enroll",
        "/api/v1/addons/docker-manager/enroll/:token",
        "/api/v1/addons/docker-manager/hosts/:id",
        "/api/v1/addons/docker-manager/hosts/:id/test",
        "/api/v1/addons/docker-manager/hosts/:id/revoke",
        "/api/v1/addons/docker-manager/hosts/:id/rotate-cert",
        "/api/v1/addons/docker-manager/snippet/:host_label",
        "/api/v1/addons/docker-manager/compose/preview",
        "/api/v1/addons/docker-manager/compose/apply",
        "/api/v1/addons/docker-manager/compose/rollback"
      ],
      "skills": [
        {
          "name": "docker-manager-view",
          "displayName": "Docker Manager: Multi-Host Inventar und Logs lesen",
          "description": "Listet Container, Images, Netzwerke und Volumes mehrerer Docker-Hosts read-only und liefert begrenzte, redacted Logs. Reiner Lesezugriff — keine Mutationen.",
          "permissions": [
            "addons/docker-manager/inventory",
            "addons/docker-manager/logs",
            "addons/docker-manager/status"
          ],
          "endpoint": "/api/v1/addons/docker-manager/inventory",
          "scripts": [
            {
              "id": "docker-manager-view-cli",
              "name": "docker-manager-view-cli",
              "path": "docker-manager-view/cli.sh",
              "sha256": "c50553b6ef2f34e341ea6ce1f9c9b32ab9549058f3d6e537a2264dd26bdc4a88",
              "interpreter": "bash",
              "timeoutMs": 10000,
              "permissions": [
                "addons/docker-manager/inventory",
                "addons/docker-manager/logs",
                "addons/docker-manager/status"
              ],
              "inputSchema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 20
                  },
                  "timeoutMs": {
                    "type": "integer",
                    "minimum": 100,
                    "maximum": 300000
                  }
                }
              },
              "outputSchema": {
                "type": "object",
                "required": [
                  "ok"
                ],
                "properties": {
                  "ok": {
                    "type": "boolean"
                  },
                  "status": {
                    "type": "integer"
                  },
                  "data": {},
                  "error": {
                    "type": "string"
                  },
                  "truncated": {
                    "type": "boolean"
                  }
                }
              },
              "size": 12130
            }
          ]
        },
        {
          "name": "docker-manager-manage",
          "displayName": "Docker Manager: Reaktions-Pattern verwalten",
          "description": "Verwaltet die pro User konfigurierbaren Secret-Reaktions-Pattern (Regex), die auf alle Log-Read-Pull-Aufrufe angewendet werden.",
          "permissions": [
            "addons/docker-manager/redaction-patterns"
          ],
          "endpoint": "/api/v1/addons/docker-manager/redaction-patterns",
          "scripts": []
        },
        {
          "name": "docker-manager-preview",
          "displayName": "Docker Manager: Container-Preview mit Drift-Schutz",
          "description": "Liest einen Container-Snapshot vom registrierten Docker-Host und liefert den snapshotHash, den Apply-Aufrufe zur Drift-Prüfung zitieren müssen. Keine Mutation; idempotent. Der Snapshot enthält keinen Secret-Material.",
          "permissions": [
            "addons/docker-manager/containers/preview"
          ],
          "endpoint": "/api/v1/addons/docker-manager/containers/:id/preview",
          "scripts": []
        },
        {
          "name": "docker-manager-apply",
          "displayName": "Docker Manager: Start/Stop/Restart mit Drift-Schutz",
          "description": "Wendet start | stop | restart auf einen Container an. Erfordert idempotencyKey + snapshotHash vom vorherigen Preview. Drift -> 409, Timeout -> 'unknown' (nie success), doppelte Submission -> 409 in_progress. Audit-Log ohne Secrets.",
          "permissions": [
            "addons/docker-manager/containers/start",
            "addons/docker-manager/containers/stop",
            "addons/docker-manager/containers/restart"
          ],
          "endpoint": "/api/v1/addons/docker-manager/containers/:id/start",
          "scripts": []
        },
        {
          "name": "docker-manager-update",
          "displayName": "Docker Manager: digest-gepinnte Image-Updates mit Health-Rollback",
          "description": "Pullt einen Image-Digest (kein Tag) von einer allowlisteten Registry, verifiziert Signatur/Policy, stoppt den Container und startet ihn mit dem neuen Digest. Health-Check entscheidet; bei unhealthy/timeout wird automatisch auf den alten Digest zurückgerollt. Kein prune/delete fremder Images, Credentials werden nicht im Audit-Log abgelegt (issue #159).",
          "permissions": [
            "addons/docker-manager/containers/update/preview",
            "addons/docker-manager/containers/update/apply",
            "addons/docker-manager/containers/update/rollback"
          ],
          "endpoint": "/api/v1/addons/docker-manager/containers/:id/update/preview",
          "scripts": []
        },
        {
          "name": "docker-manager-trust",
          "displayName": "Docker Manager: Trust-Vertrag verwalten (Enrollment, mTLS-CA, SSRF-Gate)",
          "description": "Verwaltet Remote-Docker-Host-Enrollment (Single-Use-Tokens), die Aiopta-interne mTLS-CA mit Rotation, Endpoint-Validierung (SSRF/DNS-Pinning) und den pro-User Audit-Log.",
          "permissions": [
            "addons/docker-manager/hosts",
            "addons/docker-manager/enroll",
            "addons/docker-manager/ca.status",
            "addons/docker-manager/audit"
          ],
          "endpoint": "/api/v1/addons/docker-manager/hosts",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Docker Manager (addons/docker-manager)</h1>\n<p>**Read-only Multi-Host-Inventar und Logs (#126) + controlled start/stop/restart (#157)<br />+ digest-pinned image updates with health-rollback (#159) + secure multi-host<br />trust contract (#39: single-use enrollment, mTLS-CA, SSRF gate).**<br />A self-contained Aiopta addon for the homelab operator who wants to see<br />container, image, network, and volume state across multiple Docker hosts,<br />perform bounded, audited start/stop/restart actions, roll out<br />digest-pinned image updates that auto-rollback on health failure, <strong>and</strong><br />enroll remote hosts via single-use tokens backed by an Aiopta-internal<br />mTLS-CA with SSRF/DNS-pinning endpoint validation — without<br />exposing any delete/prune/exec/compose surface. Host definitions are per-user<br />(ADR-054 <code>scopedStore</code>), and every log read + audit entry passes<br />through a configurable secret-redaction pipeline.</p>\n<h2>#39 — Trust contract (layered)</h2>\n<p>The trust anchor for talking to remote Docker hosts:</p>\n<p>- <strong>Single-use enrollment tokens</strong> (24h TTL), SHA-256-hashed at rest,<br />  plaintext returned ONCE in the <code>POST /hosts</code> response + the compose<br />  snippet. Audit row on every consume.<br />- <strong>Aiopta-internal mTLS-CA</strong> (Ed25519, AES-256-GCM-encrypted private key<br />  at rest). Operator CLI <code>node scripts/aiopta-docker.cjs {status,rotate,help}</code>.<br />- <strong>SSRF gate</strong>: scheme MUST be https, server-side DNS resolve, IP pinned<br />  for the session, static CIDR blocklist (private/loopback/link-local/cloud<br />  metadata incl. AWS <code>169.254.169.254</code> + <code>fd00:ec2::254</code>).<br />- <strong>Cross-user → 404</strong>, TLS hostname/CA failure → 404 + audit.<br />- Tables: <code>docker_trust_hosts</code>, <code>docker_enrollments</code>, <code>docker_audit</code>,<br />  <code>docker_ca</code>, <code>docker_ca_revocations</code> (separate from #126&#39;s<br />  <code>docker_hosts</code> — see the integration PR body §Documented compromise).<br />- Endpoints: <code>POST hosts</code>, <code>audit</code>, <code>ca/status</code>, <code>enroll</code>, <code>enroll/:token</code>,<br />  <code>hosts/:id</code> (DELETE), <code>hosts/:id/{test,revoke,rotate-cert}</code>,<br />  <code>snippet/:host_label</code>.</p>\n<blockquote><p><strong>Slice against the parent agent-trust model from #39.</strong></p><p>This addon ships standalone with a clean adapter seam. Parent #39 will drop</p><p>in the <code>RemoteAgentDockerAdapter</code> (mTLS-enrolled agent transport). Until then</p><p>the in-memory / fake adapters serve a seeded surface so the addon is usable</p><p>end-to-end and fully testable.</p></blockquote>\n<h2>Scope (this slice)</h2>\n<p>### #126 — Read-only inventory + logs<br />- <strong>Per-user host list and inventory</strong> — the <code>docker_hosts</code> and<br />  <code>docker_inventory_cache</code> scoped tables (ADR-054) hold host definitions +<br />  the most recent inventory snapshot for the caller only. Foreign lookups<br />  return 0 rows; the route layer maps that to a 404 with no existence leak.<br />- <strong>Paged multi-host inventory</strong> — <code>GET /inventory</code> returns one normalized<br />  envelope per host. Partial-success: a down host yields <code>reachable=false</code><br />  and does not poison the rest of the payload.<br />- <strong>Bounded, redacted log reads</strong> — <code>GET /logs/:host/:container</code> enforces byte<br />  + line limits and returns an opaque cursor. Every line is run through the<br />  resolved redaction-pattern set BEFORE the response leaves the server.<br />- <strong>Configurable redaction</strong> — <code>GET|PUT|DELETE|PATCH /redaction-patterns</code> is<br />  the per-user meta-config knob for the secret-redaction pipeline.<br />- <strong>Status</strong> — every status response carries <code>host</code> + <code>fetchedAt</code>.<br />- <strong>Polling</strong> — a singleton in-process poller refreshes the inventory cache<br />  for every enrolled user; when the addon is disabled the poller exits cleanly.</p>\n<p>### #157 — Controlled mutations<br />- ✅ Start / stop / restart of allowlisted containers<br />- ✅ Digest-pinned image updates with health-rollback (#159) — pull + verify an<br />  image BEFORE mutation; on unhealthy/timeout/start-failure the container rolls<br />  back to the old digest automatically (old image preserved; no prune/delete)<br />- ✅ Preview snapshot + drift detection between preview and apply<br />- ✅ Idempotency key replay (single mutation, repeated requests are no-ops)<br />- ✅ Audit log with actor + host alias + container id, secrets redacted<br />- ✅ Per-user registry allowlist + credential redaction (no plaintext in audit)<br />- ❌ <strong>No</strong> delete / prune / exec / compose / image-tag mutation<br />- ❌ <strong>No</strong> prune/delete of foreign images (the old digest stays for rollback)</p>\n<p>### #158 (slice 1/3, #329) — Declarative compose-schema + versioned repo loader<br />- ✅ JSON-Schema for Compose-project manifests at<br />  <code>addons/docker-manager/schema/compose-project.schema.json</code> — single source<br />  of truth for what a valid <code>project.json</code> looks like (<code>name</code>, <code>version</code>,<br />  <code>services[]</code>, <code>volumes[]</code>, <code>networks[]</code>, <code>secrets[]</code>).<br />- ✅ Versioned repo loader (<code>loadProject(repoUrl, commitSha)</code>) — clones the<br />  pinned commit, validates the manifest against the schema, and verifies<br />  the post-checkout HEAD matches the pinned SHA. Pure function for tests<br />  via injected <code>LoaderDeps</code>; production deps spawn <code>git</code> via<br />  <code>child_process.execFile</code> (no shell, argv-only).<br />- ✅ Typed errors (<code>DriftError</code> → 409, <code>ProjectSchemaError</code> → 422,<br />  <code>EmptyRepoError</code> / <code>MissingManifestError</code> → 404, <code>InvalidJsonError</code> →<br />  400, <code>IoError</code> → 502, <code>InvalidArgsError</code> → 400). The route layer maps<br />  each <code>ProjectErrorCode</code> to its HTTP status via <code>PROJECT_ERROR_TO_HTTP</code><br />  in <code>project-errors.ts</code>.<br />- ✅ No privileged / host-network / arbitrary bind mounts — the schema<br />  refuses them structurally (slice #330 will enforce them semantically).<br />- ✅ No silent overwrite of an existing project path — the loader refuses<br />  to clobber a leftover clone and surfaces <code>InvalidArgsError</code>.<br />- ⏭ Render / Policy / Preview → slice #330.<br />- ⏭ Apply + Rollback → slice #331.<br />- ❌ <strong>No</strong> direct <code>/var/run/docker.sock</code> from the addon — adapter seam only<br />- ❌ <strong>No</strong> cross-user enumeration — foreign container ids map to 404</p>\n<h2>Routes</h2>\n<p>| Method | Path | Description |<br />| --- | --- | --- |<br />| <code>GET</code> | <code>/api/v1/addons/docker-manager/health</code> | Addon enabled + hosts count |<br />| <code>GET</code> | <code>/api/v1/addons/docker-manager/hosts</code> | List caller&#39;s enrolled hosts |<br />| <code>GET</code> | <code>/api/v1/addons/docker-manager/inventory</code> | Paginated multi-host inventory |<br />| <code>GET</code> | <code>/api/v1/addons/docker-manager/inventory/:host</code> | Single-host drill-down |<br />| <code>GET</code> | <code>/api/v1/addons/docker-manager/logs/:host/:container</code> | Bounded, redacted logs |<br />| <code>GET</code> | <code>/api/v1/addons/docker-manager/status/:host</code> | Per-host status (+ fetchedAt) |<br />| <code>GET\\|PUT\\|DELETE\\|PATCH</code> | <code>/api/v1/addons/docker-manager/redaction-patterns</code> | Per-user redaction config |<br />| <code>POST</code> | <code>/api/v1/addons/docker-manager/containers/:id/preview</code> | Read-only snapshot → <code>snapshotHash</code> |<br />| <code>POST</code> | <code>/api/v1/addons/docker-manager/containers/:id/start</code> | Idempotent start |<br />| <code>POST</code> | <code>/api/v1/addons/docker-manager/containers/:id/stop</code> | Idempotent stop |<br />| <code>POST</code> | <code>/api/v1/addons/docker-manager/containers/:id/restart</code> | Idempotent restart |<br />| <code>POST</code> | <code>/api/v1/addons/docker-manager/containers/:id/update/preview</code> | Pull + verify a digest-pinned image; return update plan |<br />| <code>POST</code> | <code>/api/v1/addons/docker-manager/containers/:id/update/apply</code> | Stop → start-new → health-check; auto-rollback on unhealthy |<br />| <code>POST</code> | <code>/api/v1/addons/docker-manager/containers/:id/update/rollback</code> | Operator-initiated rollback to old digest |</p>\n<p>Every mutation route is wrapped in <code>userRoute</code> (issue #157 user-scope). The<br />user scope is enforced in <code>src/lib/allowlist.ts</code> via <code>assertContainerForUser</code>,<br />which maps a foreign id to <strong>404</strong> (no enumeration leak across users).</p>\n<h2>Workflow (mutations)</h2>\n<pre><code class=\"lang-text\">client                docker-manager           adapter         audit\n  │  POST /preview      │                       │              │\n  │ ───────────────────►│ assertContainerForUser│              │\n  │                     │ ────────────────────►│ snapshot     │\n  │                     │                       │ ───────────► │\n  │ {snapshotHash,K}    │ audit row (success)   │              │\n  │ ◄───────────────────│                       │              │\n  │  POST /start        │                       │              │\n  │  {snapshotHash,K}   │ reserve(K)            │              │\n  │                     │ ────────────────────►│ apply        │\n  │                     │ ◄────────────────────│ ok / drift   │\n  │ {status:success}    │ audit row             │              │\n  │ ◄───────────────────│                       │              │</code></pre>\n<p>Drift between preview and apply surfaces as <strong>HTTP 409</strong> with<br /><code>status: &quot;drift&quot;</code>. Timeout surfaces as <strong>HTTP 504</strong> with <code>status: &quot;timeout&quot;</code><br />— never <code>success</code> (issue #157 acceptance #2).</p>\n<h2>Trust contract (parent #39)</h2>\n<p>This addon does NOT ship the agent enrollment / mTLS surface — that lives in<br />parent #39 / PR #261. The adapter seam is the contract between this addon and<br />the trust layer. Implementations shipped today:</p>\n<p>- <code>FakeDockerAdapter</code> (#126) — pure in-memory, hermetic. Used by the #126<br />  inventory unit suite. NEVER opens a socket.<br />- <code>createInMemoryDockerAdapter</code> (#157) — fully deterministic mutation adapter<br />  for the #157 unit suite; no real network, no real Docker.<br />- <code>LocalDockerAdapter</code> / <code>RemoteAgentAdapter</code> — land with parent #39.</p>\n<h2>Out of scope (parent #39 + child stories)</h2>\n<ul><li>Compose / Image <strong>mutations</strong> — #158 / #159.</li><li>Host enrollment UI + mTLS key generation — parent #39 / PR #261.</li><li>Live Docker API integration — bundled with the agent contract.</li></ul>\n<h2>Security posture</h2>\n<p>- The only Docker-mutating routes are start/stop/restart (#157); the<br />  <code>redaction-patterns</code> non-GET routes are per-user meta-config, not Docker<br />  mutations.<br />- <code>credentialRef</code> (the pointer to the Core Credential Store) is masked before<br />  any host is sent to a client; the raw reference NEVER leaves the server.<br />- Every Docker-host operation is per-user via <code>scopedStore(userId)</code>; foreign<br />  users get 0 rows + 404.<br />- Per-host failure isolation — a down host does not poison the rest of the<br />  dashboard.<br />- Audit entries carry no secret material (opaque-token + secret-key patterns<br />  are redacted before logging).</p>\n<h2>Commands</h2>\n<pre><code class=\"lang-bash\"># Local validation\n./scripts/validate-addons.sh</code></pre>\n<p># Unit tests<br />npx vitest run addons/docker-manager/tests/unit<br />```</p>\n<h2>Parent / Blocker</h2>\n<ul><li>#39 (Docker Manager Multi-Host-Trust) — supplies the agent transport</li><li>#126 (Read-only Multi-Host-Inventar) — supplies the inventory + host CRUD</li><li>#157 (this slice) — the mutation surface against that trust model</li></ul>\n<p>The adapter seam lets #157 ship standalone; the agent transport drops in once<br />#39 lands without touching this addon&#39;s route handlers.</p>",
          "byteSize": 10807
        },
        "changelog": {
          "filename": "CHANGELOG.md",
          "sourcePath": "CHANGELOG.md",
          "rendered": "<h1>Changelog</h1>\n<h2>0.1.0 — Issue #39 (Multi-Host-Docker-Trust contract)</h2>\n<p><strong>Initial release</strong> — ships the trust anchor for multi-host Docker<br />management. Children #126–#159 build the read/mutate/compose/img<br />surfaces on top.</p>\n<h3>Added</h3>\n<p>- <code>addon.json</code> manifest + <code>src/manifest.ts</code> runtime extension (scope: <code>list</code>, <code>test</code>, <code>enroll</code>, <code>revoke</code>, <code>rotate-cert</code>, <code>audit</code>, <code>ca/status</code>, <code>snippet/{label}</code>).<br />- Per-user <code>scopedStore(&#39;docker_hosts&#39;)</code>, <code>scopedStore(&#39;docker_enrollments&#39;)</code>, <code>scopedStore(&#39;docker_audit&#39;)</code> (ADR-054).<br />- Instance-level <code>docker_ca</code> + <code>docker_ca_revocations</code> (CRL).<br />- Single-use enrollment tokens: SHA-256(token) stored; plaintext<br />  returned ONCE in <code>POST /hosts</code> response and embedded in the<br />  <code>docker-compose.yml</code> snippet.<br />- AES-256-GCM envelope encryption (<code>src/lib/crypto.ts</code>) for the CA<br />  private key + optional cached agent private key.<br />- mTLS CA primitive (<code>src/lib/mtls-ca.ts</code>) — generation, agent cert<br />  issuance, signature verify (with Ed25519-aware <code>crypto.sign</code> /<br />  <code>crypto.verify</code>), rotation, CRL pinning.<br />- Endpoint validator (<code>src/lib/endpoint-validator.ts</code>) — SSRF gate<br />  (private / loopback / link-local / cloud-metadata blocklist +<br />  DNS-pinning) + <code>assertSameUser</code> cross-user guard.<br />- Adapter seam (<code>src/lib/adapter.ts</code>) — typed <code>Success</code> / <code>Timeout</code> /<br />  <code>Refuse</code> / <code>Revoke</code> fakes + <code>liveAdapter</code> (TLS-validated) +<br />  <code>outcomeFromAdapter()</code> audit-log mapper.<br />- <code>docker-compose.yml</code> snippet renderer (<code>src/lib/compose-snippet.ts</code>)<br />  with security invariants (no <code>ports:</code>, no <code>privileged: true</code>, no<br />  <code>network_mode: host</code>).<br />- 8 API route handlers under <code>src/app/api/v1/addons/docker-manager/</code>.<br />- UI dashboard (<code>src/components/DockerManagerDashboard.tsx</code>) with<br />  enroll form, host list, compose snippet viewer, audit log, and<br />  per-host actions.<br />- Operator CLI (<code>scripts/aiopta-docker.cjs {status, rotate, help}</code>)<br />  for CA rotation.</p>\n<h3>Security</h3>\n<p>- No <code>/var/run/docker.sock</code> mount from Aiopta to remote hosts.<br />- Cross-user → 404 (no enumeration leak).<br />- TLS hostname / CA failure → 404 + audit row.<br />- DNS-pinning (server-side resolve, IP pinned for the session).<br />- Agent private key (when cached for re-rotate) encrypted at rest<br />  via AES-256-GCM.<br />- <code>redactEnvelopes()</code> strips <code>enc:v1:...</code> blobs from any error path.</p>\n<h3>Tests</h3>\n<p>- 6 test files, 77 passing tests.<br />- <code>compose-snippet.test.ts</code> (12) — render + security invariants + sha256.<br />- <code>endpoint-validator.test.ts</code> (19) — SSRF, DNS-pinning, cross-user,<br />  CIDR membership for v4/v6.<br />- <code>crypto.test.ts</code> (15) — round-trip + tamper detection + nonce<br />  uniqueness + weak-key guard + envelope redaction.<br />- <code>mtls-ca.test.ts</code> (9) — CA gen, agent cert, rotation, verify<br />  round-trip, cross-CA defense, expiry.<br />- <code>adapter.test.ts</code> (11) — fake outcomes + audit-log mapping.<br />- <code>store.test.ts</code> (10) — schema idempotency + per-user scoping +<br />  single-use tokens + CRL pinning + CA rotation.</p>\n<h3>Validation</h3>\n<ul><li><code>npm run validate:addons</code> ✅</li><li><code>node --check scripts/aiopta-docker.cjs</code> ✅</li></ul>\n<h2>1.3.0 — Issue #158 slice 2 (Render / Policy / Preview vor Apply)</h2>\n<p>Slice 2 of the Docker Manager declarative Compose-Deploy epic. Ships<br />the deterministic render pipeline + 4 standard policy rules + the<br />preview endpoint. Slice 1 (#329) owns the JSON-Schema + repo-loader;<br />slice 3 (#331) will wire the preview output into the apply path.</p>\n<h3>Added</h3>\n<p>- <strong><code>src/lib/compose-project.ts</code></strong> — <code>ComposeProject</code> types (services,<br />  volumes, networks, secrets, mounts) + <code>validateProjectShape()</code><br />  (pure schema validator that collects ALL issues in one pass) +<br />  <code>loadProject({project, expectedCommitSha})</code> (deep-frozen validator<br />  with drift detection via <code>DriftError</code>) + <code>projectContentHash()</code><br />  (canonical JSON SHA-256, deterministic across key-order changes).<br />  This file is the shared contract between slice 1 (loader) and slice<br />  2 (renderer).<br />- <strong><code>src/lib/render.ts</code></strong> — deterministic <code>renderProject(project, vars)</code><br />  → canonical docker-compose.yml + <code>configHash</code> + used placeholder<br />  list. Pure function: equal inputs always yield equal output.<br />  <code>PLACEHOLDER_RE = /\\$\\{([A-Z][A-Z0-9_]{0,63})\\}/g</code> is the only<br />  recognised placeholder shape; suspicious placeholders are passed<br />  verbatim so the policy engine can flag them. <code>redactRenderedYaml()</code><br />  + <code>previewRenderedConfig()</code> strip secret-shaped values<br />  (<code>password=...</code>, <code>token=...</code>, <code>api_key=...</code>, <code>secret=...</code>, URL with<br />  userinfo, opaque ≥32-char tokens) before returning to the caller.<br />- <strong><code>src/lib/policy.ts</code></strong> — <code>checkPolicy(project)</code> runs the four<br />  standard rules and returns typed <code>PolicyViolation[]</code>:<br />  - R1 — <code>privileged: true</code> → block<br />  - R2 — <code>network_mode: host</code> → block<br />  - R3 — dangerous bind mounts (<code>/</code>, <code>/etc</code>, <code>/proc</code>, <code>/sys</code>,<br />    <code>/dev</code>, <code>/boot</code>, <code>/root</code>, <code>/home</code>, <code>/var/run/docker.sock</code>,<br />    <code>/var/lib/docker</code>); catches <code>/etc/../var/run/docker.sock</code><br />    traversal evasion<br />  - R4 — inlined secrets in env / command / labels; suspicious<br />    placeholders (<code>${../etc/passwd}</code>, <code>${$(rm -rf /)}</code>) → block,<br />    non-standard placeholder shape → warn<br />- <strong><code>src/lib/preview-pipeline.ts</code></strong> — <code>runPreview(input)</code> glues<br />  load → render → policy → redact into a typed <code>PreviewResult</code>.<br />  The route layer uses it directly; slice 3 (#331) calls the same<br />  helper before apply.<br />- <strong><code>POST /api/v1/addons/docker-manager/preview</code></strong> — preview endpoint<br />  (200 on clean / 403 on block-severity violations / 409 on drift /<br />  400 on bad input / 401 on no-auth). Writes one audit entry per<br />  accepted request: <code>success</code> / <code>rejected</code> (with violation summary)<br />  / <code>drift</code> (with both SHAs). No secrets in the audit <code>reason</code><br />  field — redaction runs before the entry is appended.<br />- <strong>Tests</strong> — 4 new test files (52 new assertions total):<br />  <code>compose-project.test.ts</code> (19), <code>render.test.ts</code> (15),<br />  <code>policy.test.ts</code> (16), <code>preview-pipeline.test.ts</code> (13),<br />  <code>preview-route.test.ts</code> (15). All pass via vitest<br />  (<code>npx vitest run addons/docker-manager/tests/unit/</code>).</p>\n<h3>Out-of-scope (handled by sibling slices)</h3>\n<p>- Slice 1 (#329): Git-clone repo loader + JSON-Schema file. Slice 2<br />  consumes the typed <code>ComposeProject</code> slice 1 produces.<br />- Slice 3 (#331): <code>POST /api/v1/addons/docker-manager/apply</code> with<br />  partial-failure reconcile + rollback plan. Slice 3 reads the<br />  preview&#39;s <code>configHash</code> + <code>violations[]</code> and refuses apply when<br />  <code>blocked === true</code>.</p>\n<h3>Notes</h3>\n<p>- The ComposeProject type lives in slice 2 even though slice 1 owns<br />  the loader — the render + policy modules need a typed shape and<br />  putting it here means slice 1 imports this module. Slice 1&#39;s<br />  JSON-Schema becomes a hand-written mirror of the runtime shape,<br />  so a drift fails fast in <code>validateProjectShape</code>.</p>",
          "byteSize": 6639
        }
      }
    },
    {
      "id": "dropshipping",
      "uuid": "ee37a5f1-96bb-4aa8-aa90-d809df1cb2ed",
      "name": "Dropshipping & POD",
      "version": "2.0.1",
      "description": "DEPRECATED — zusammengeführt in das kanonische Commerce-Addon `commerce` als Dropshipping-Connector (ADR-008 Addon-Konsolidierung, Issue #590 / Epic #492). Der Dropshipping-Channel (Printful/Printify POD, Supplier-Abstraktion, Margins/Pricing, Woo-Bridge) ist als Feature-Modul unter `commerce/src/connectors/dropshipping/` migriert: BYOK-Credentials in der gemeinsamen `commerce_connector_config`-Tabelle, Sync-Client auf dem kanonischen `CommerceChannel`-Vertrag, Alt-ID-Redirect `/addons/dropshipping` → `/addons/commerce?connector=dropshipping`. dropshipping bleibt bis zum Sunset (2026-12-31) voll funktionsfähig und datenerhaltend — kein Endpoint entfällt, keine Zeile wird kopiert/gelöscht. Ursprüngliche Fähigkeit: Import print-on-demand products, set margins, place and fulfill orders via Printful / Printify. The suppliers are external SaaS APIs reached with the owner's API key from Aiopta-config (no container) — auto-fulfillment is always an explicit call (ADR-013).",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "commerce-dropshipping",
        "dropshipping-products",
        "dropshipping-orders",
        "dropshipping-fulfillment",
        "dropshipping-margins",
        "pod-printful",
        "pod-printify"
      ],
      "tags": [],
      "download": "/artifacts/dropshipping-2.0.1.zip",
      "sha256": "07f9a9b697fd208b70e990dd39d4c2fec0e6da0bbc2ce3379742feb888dc21bb",
      "size": 114975,
      "page": {
        "id": "dropshipping-page",
        "path": "/addons/dropshipping",
        "name": "Dropshipping & POD",
        "icon": "📦",
        "section": "Money",
        "component": "@addons/dropshipping/src/page"
      },
      "deprecated": true,
      "replacedBy": "commerce",
      "deprecation": {
        "deprecated": true,
        "replacedBy": "commerce",
        "sunset": "2026-12-31",
        "successorPath": "/addons/commerce",
        "successorLabel": "Commerce (Nachfolger)"
      },
      "endpoints": [
        "/api/v1/addons/dropshipping/status",
        "/api/v1/addons/dropshipping/products",
        "/api/v1/addons/dropshipping/orders",
        "/api/v1/addons/dropshipping/orders/:id",
        "/api/v1/addons/dropshipping/orders/:id/fulfill",
        "/api/v1/addons/dropshipping/fulfillments",
        "/api/v1/addons/dropshipping/margins"
      ],
      "skills": [
        {
          "name": "dropshipping-catalog",
          "displayName": "Dropshipping: Browse & Import Products",
          "description": "List Printful/Printify catalog products (with margin-applied retail prices) and import one into the shop.",
          "permissions": [
            "addons/dropshipping/products",
            "addons/dropshipping/status"
          ],
          "endpoint": "/api/v1/addons/dropshipping/products",
          "scripts": []
        },
        {
          "name": "dropshipping-orders",
          "displayName": "Dropshipping: Orders & Fulfillment",
          "description": "List/create draft orders and explicitly fulfill them (forward to the supplier for production). Fulfillment moves money and is never implicit.",
          "permissions": [
            "addons/dropshipping/orders",
            "addons/dropshipping/fulfillment"
          ],
          "endpoint": "/api/v1/addons/dropshipping/orders",
          "scripts": []
        },
        {
          "name": "dropshipping-margins",
          "displayName": "Dropshipping: Margin Rules",
          "description": "Read or set margin rules (percent + fixed, charm-rounding) that turn supplier base cost into retail price.",
          "permissions": [
            "addons/dropshipping/margins"
          ],
          "endpoint": "/api/v1/addons/dropshipping/margins",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Dropshipping &amp; POD Addon</h1>\n<p>Run a <strong>print-on-demand / dropshipping</strong> business from inside Aiopta — browse a supplier<br />catalog, apply <strong>margin rules</strong> to turn supplier cost into retail price, place <strong>draft orders</strong>, and<br /><strong>explicitly fulfill</strong> them. Usable by agents (via the addon API) and by humans (via the management<br />page). Suppliers: <strong>Printful</strong> and <strong>Printify</strong>.</p>\n<h2>Architecture (ADR-013)</h2>\n<p>- <strong>The suppliers are external SaaS APIs — there is NO container.</strong> Printful (<code>https://api.printful.com</code>)<br />  and Printify (<code>https://api.printify.com/v1</code>) are third-party SaaS, not self-hostable tools, so —<br />  unlike the WordPress addon (ADR-009/010) — this addon declares <strong>no <code>container</code></strong>. Each supplier<br />  client (<code>src/lib/printful-client.ts</code>, <code>src/lib/printify-client.ts</code>) is a thin HTTPS client behind a<br />  small common interface (<code>src/lib/supplier.ts</code>: <code>SupplierClient</code>). This is the same pattern as the<br />  LLM providers (a client to a third-party API using the owner&#39;s key).<br />- <strong>Owner&#39;s key from Aiopta-config, never hard-coded, never logged.</strong> Each client reads its API key via<br />  <code>getAddonConfiguration(&quot;dropshipping&quot;)</code> (<code>PRINTFUL_API_KEY</code> / <code>PRINTIFY_API_KEY</code>). The vendor base<br />  URL is a compile-time constant in the client — <strong>never a user-entered host</strong>. The key is sent as a<br />  <code>Bearer</code> token and is never written to logs. When a key is missing, the client returns a clear<br />  <strong>400</strong> <code>SupplierError</code> (it never throws uncaught and never calls a default host).<br />- <strong>No money moves without an explicit fulfill call.</strong> Creating an order produces a <strong>draft</strong> only.<br />  Fulfillment is a separate, explicit endpoint (<code>POST /orders/{id}/fulfill</code>) that forwards the order<br />  to the supplier (<code>/orders/{id}/confirm</code> for Printful, <code>/send-to-production.json</code> for Printify). Real<br />  fulfillment requires the owner&#39;s <strong>funded</strong> supplier account; until funded the supplier rejects<br />  confirmation and the upstream error is surfaced.<br />- <strong>Talks to other addons only via the Aiopta API.</strong> The optional WooCommerce bridge<br />  (<code>src/lib/woo-bridge.ts</code>) calls the <strong>wordpress addon&#39;s</strong> Woo endpoints<br />  (<code>/api/v1/addons/wordpress/products</code>, <code>.../orders/{id}</code>) through the Aiopta API — never WooCommerce or<br />  the supplier directly — and fails soft if the wordpress addon is absent.</p>\n<h2>Configuration (Aiopta addon-config — no secret committed)</h2>\n<p>| Key | Required | Purpose |<br />|-----|----------|---------|<br />| <code>PRINTFUL_API_KEY</code> | for Printful | Printful API token (Bearer). Stored <code>is_secret=1</code>. |<br />| <code>PRINTFUL_STORE_ID</code> | optional | Store id for account-level Printful tokens (<code>X-PF-Store-Id</code>). |<br />| <code>PRINTIFY_API_KEY</code> | for Printify | Printify personal access token (Bearer). Stored <code>is_secret=1</code>. |<br />| <code>PRINTIFY_SHOP_ID</code> | optional | Shop id; auto-resolved from <code>/shops.json</code> when absent. |<br />| <code>MARGIN_RULES</code> | optional | JSON margin rules; defaults to +40% charm-rounded to <code>.99</code>. |</p>\n<p>Set them with <code>setAddonConfiguration(&quot;dropshipping&quot;, { PRINTFUL_API_KEY: &quot;…&quot; })</code>. Until at least one<br />key is set, <code>/status</code> reports the supplier as not-configured and product/order calls return a clear<br /><strong>400</strong>.</p>\n<h2>API</h2>\n<p>All routes are namespaced under <code>/api/v1/addons/dropshipping/</code> and require auth.</p>\n<pre><code>GET  /status                          → configured suppliers + connection probe\nGET  /products?supplier=printful      → list catalog products (with margin-applied retailPrice)\nPOST /products    { productId, supplier?, syncWoo? }   → import a product (optional Woo publish)\nGET  /orders?supplier=&amp;status=        → list orders\nPOST /orders      { items[], recipient{…}, supplier? } → create a DRAFT order (no charge)\nGET  /orders/{id}?supplier=           → order status + tracking\nPOST /orders/{id}/fulfill { supplier? }→ EXPLICITLY fulfill (forward to supplier; moves money)\nGET  /fulfillments                    → open/in-progress orders across suppliers + revenue\nPOST /fulfillments { orderId, supplier? } → re-sync one order&#39;s status + tracking\nGET  /margins                         → active margin rules + worked example\nPUT  /margins     { default:{percent,fixed,minPrice?,roundTo?}, perSupplier?{…} } → set rules</code></pre>\n<p>Responses use the <code>{ &quot;success&quot;: true, &quot;data&quot;: … }</code> envelope. On failure read <code>error</code> + the HTTP<br />status (a <code>400</code> means the supplier key is not set; the upstream vendor status is preserved otherwise).</p>\n<h3>Margins / pricing</h3>\n<p><code>src/lib/pricing.ts</code> is pure and unit-tested. A <code>MarginRule</code> turns a supplier <strong>base cost</strong> into a<br /><strong>retail price</strong>: <code>retail = max(cost*(1+percent/100) + fixed, minPrice)</code>, then optional charm-rounding<br />(<code>roundTo</code>, e.g. <code>.99</code>). Per-supplier overrides are supported via <code>perSupplier</code>. Default rule: <code>+40%</code>,<br />charm-rounded to <code>.99</code> (so cost <code>20</code> → retail <code>28.99</code>).</p>\n<h2>Layout</h2>\n<pre><code>addons/dropshipping/\n├── addon.json                 # Manifest (page + widget + endpoints + skills; NO container)\n├── README.md\n├── SKILL.md                   # Agent skill\n├── src/\n│   ├── manifest.ts            # Extension (dual-registration: routes + page; string-literal handlers)\n│   ├── page.tsx               # Management UI (Products / Orders / Fulfillments / Margins)\n│   ├── widget.tsx             # Dashboard widget (open fulfillments + revenue, declarative)\n│   ├── lib/\n│   │   ├── supplier.ts        # Common SupplierClient interface + SupplierError\n│   │   ├── printful-client.ts # Printful SaaS client (key from Aiopta-config)\n│   │   ├── printify-client.ts # Printify SaaS client (key from Aiopta-config)\n│   │   ├── http.ts            # Shared Bearer fetch (constant base URL; key never logged)\n│   │   ├── suppliers.ts       # Registry + /status probe\n│   │   ├── pricing.ts         # Pure margin / retail-price math\n│   │   ├── margins-store.ts   # Margin rules persisted via Aiopta addon-config\n│   │   └── woo-bridge.ts      # OPTIONAL WooCommerce bridge via the wordpress addon&#39;s Aiopta-API\n│   └── app/api/v1/addons/dropshipping/{status,products,orders,orders/[id],orders/[id]/fulfill,fulfillments,margins}/route.ts\n└── tests/unit/                # manifest validity + pure pricing + supplier clients (fetch+config mocked)</code></pre>\n<h2>Owner-parked items (non-blocking)</h2>\n<p>The addon code + routes work the moment the owner stores a supplier key. Tracked in<br /><code>docs/OPEN-DECISIONS.md</code>:</p>\n<blockquote><p><strong>TODO(owner): <code>dropshipping-supplier-keys</code></strong> — create a Printful and/or Printify API key and store</p><p>it in Aiopta addon-config (<code>PRINTFUL_API_KEY</code> / <code>PRINTIFY_API_KEY</code>). No secret is committed. Until set,</p><p><code>/status</code> reports not-configured and calls return a clear 400. Real <strong>fulfillment</strong> additionally</p><p>requires a <strong>funded</strong> supplier account.</p></blockquote>\n<h2>License</h2>\n<p>MIT</p>",
          "byteSize": 6745
        }
      }
    },
    {
      "id": "email-marketing",
      "uuid": "c9f6a2c1-0716-4f47-91f4-dbc7c437c654",
      "name": "E-Mail-Marketing (Listen, Kampagnen, Templates, Sequenzen)",
      "version": "1.0.3",
      "description": "E-Mail-Marketing über die in Aiopta konfigurierte E-Mail (SMTP/ESP, wie lead-gen): Listen & Abonnenten mit Double-Opt-in, wiederverwendbare Templates, Kampagnen mit Segmentierung, Drip-Sequenzen sowie Öffnungs-/Klick-/Bounce-Tracking. Lokale SQLite ist die Quelle der Wahrheit; ohne konfigurierten Versand läuft das Addon graceful (Listen/Abonnenten/Templates anlegbar, Versand meldet 'zuerst E-Mail konfigurieren', nie ein 500). Jede Marketing-Mail trägt einen Abmeldelink; unbestätigte Adressen werden nie angeschrieben. Confirm/Unsubscribe/Open/Click sind öffentlich, aber tokengeschützt; der Klick-Redirect leitet nur auf die gespeicherte URL um (kein Open-Redirect). ADR-042.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "lists",
        "subscribers",
        "double-opt-in",
        "templates",
        "campaigns",
        "sequences",
        "segmentation",
        "open-tracking",
        "click-tracking",
        "bounce-tracking",
        "unsubscribe"
      ],
      "tags": [],
      "download": "/artifacts/email-marketing-1.0.3.zip",
      "sha256": "b8e84e1d012d6df17b592bc29a23a9f65d45281600524347e859c785cce4f1bc",
      "size": 262544,
      "page": {
        "id": "email-marketing-page",
        "path": "/addons/email-marketing",
        "name": "E-Mail-Marketing",
        "icon": "📣",
        "section": "Money",
        "component": "@addons/email-marketing/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/email-marketing/status",
        "/api/v1/addons/email-marketing/lists",
        "/api/v1/addons/email-marketing/lists/:id",
        "/api/v1/addons/email-marketing/lists/:id/subscribers",
        "/api/v1/addons/email-marketing/subscribers/:id",
        "/api/v1/addons/email-marketing/templates",
        "/api/v1/addons/email-marketing/templates/:id",
        "/api/v1/addons/email-marketing/campaigns",
        "/api/v1/addons/email-marketing/campaigns/:id",
        "/api/v1/addons/email-marketing/campaigns/:id/send",
        "/api/v1/addons/email-marketing/sequences",
        "/api/v1/addons/email-marketing/sequences/:id",
        "/api/v1/addons/email-marketing/sends/:id/bounce",
        "/api/v1/addons/email-marketing/public/confirm",
        "/api/v1/addons/email-marketing/public/unsubscribe",
        "/api/v1/addons/email-marketing/public/open",
        "/api/v1/addons/email-marketing/public/click",
        "/api/v1/addons/email-marketing/widget"
      ],
      "skills": [
        {
          "name": "email-marketing-overview",
          "displayName": "E-Mail-Marketing: Status & Übersicht",
          "description": "Liest den Versand-Status (konfiguriert? Provider/From-Domain, ohne Secrets) und die Marketing-Übersicht (Listen, Abonnenten nach Opt-in-Status, Templates/Kampagnen/Sequenzen, letzte Öffnungsrate). Reiner Lesezugriff — versendet nichts.",
          "permissions": [
            "addons/email-marketing/status"
          ],
          "endpoint": "/api/v1/addons/email-marketing/status",
          "scripts": []
        },
        {
          "name": "email-marketing-lists",
          "displayName": "E-Mail-Marketing: Listen & Abonnenten",
          "description": "Listet/erstellt Listen und fügt Abonnenten hinzu (mit Double-Opt-in: neue Adressen sind 'pending' und bekommen eine Bestätigungs-Mail). Unbestätigte Adressen werden nie angeschrieben. Versand der Bestätigung braucht konfigurierte E-Mail (sonst bleibt der Abonnent 'pending', kein 500).",
          "permissions": [
            "addons/email-marketing/lists",
            "addons/email-marketing/status"
          ],
          "endpoint": "/api/v1/addons/email-marketing/lists",
          "scripts": []
        },
        {
          "name": "email-marketing-templates",
          "displayName": "E-Mail-Marketing: Templates",
          "description": "Listet/erstellt/bearbeitet wiederverwendbare E-Mail-Templates (Betreff + HTML/Text-Body mit Merge-Feldern wie {{name}}).",
          "permissions": [
            "addons/email-marketing/templates"
          ],
          "endpoint": "/api/v1/addons/email-marketing/templates",
          "scripts": []
        },
        {
          "name": "email-marketing-campaigns",
          "displayName": "E-Mail-Marketing: Kampagnen",
          "description": "Erstellt Kampagnen (Liste/Segment + Template) und versendet sie. Der Versand iteriert NUR bestätigte, segment-passende Abonnenten, respektiert eine Sendegrenze, hängt an jede Mail einen Abmeldelink und fügt Öffnungs-/Klick-Tracking hinzu. Ohne konfigurierte E-Mail liefert der Versand ein klares 400 ('zuerst E-Mail konfigurieren'), nie ein 500.",
          "permissions": [
            "addons/email-marketing/campaigns",
            "addons/email-marketing/status"
          ],
          "endpoint": "/api/v1/addons/email-marketing/campaigns",
          "scripts": []
        },
        {
          "name": "email-marketing-sequences",
          "displayName": "E-Mail-Marketing: Sequenzen",
          "description": "Listet/erstellt Drip-Sequenzen (mehrere Mails zeitversetzt, Trigger: nach Bestätigung) und aktiviert/deaktiviert sie.",
          "permissions": [
            "addons/email-marketing/sequences"
          ],
          "endpoint": "/api/v1/addons/email-marketing/sequences",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>E-Mail-Marketing Addon</h1>\n<p>Listen &amp; Abonnenten (mit <strong>Double-Opt-in</strong>), wiederverwendbare <strong>Templates</strong>, <strong>Kampagnen</strong> mit<br /><strong>Segmentierung</strong>, Drip-<strong>Sequenzen</strong> sowie <strong>Öffnungs-/Klick-/Bounce-Tracking</strong> — versendet über die in<br />Aiopta konfigurierte E-Mail (SMTP/ESP, <strong>derselbe Mail-Pfad wie lead-gen</strong>).</p>\n<blockquote><p>Money-Stufe-1 · ADR-042 · Connector-Pattern (<strong>kein</strong> Container — nutzt Aiopta instances Mail) · #69 / INBOX-2.</p></blockquote>\n<h2>Was es tut</h2>\n<p>- <strong>Listen &amp; Abonnenten</strong> — Listen anlegen; Abonnenten hinzufügen/importieren (Name/E-Mail/Attribute/<br />  Tags). Opt-in-Status <code>pending → confirmed → unsubscribed | bounced</code>.<br />- <strong>Double-Opt-in</strong> — neue Abonnenten sind <code>pending</code> und erhalten eine <strong>Bestätigungs-Mail</strong> mit einem<br />  tokengeschützten Confirm-Link; eine <strong>öffentliche</strong> Confirm-Route setzt sie auf <code>confirmed</code>. Unbestätigte<br />  Adressen werden <strong>nie</strong> in Kampagnen angeschrieben.<br />- <strong>Templates</strong> — Betreff + HTML/Text-Body mit Merge-Feldern (<code>{{name}}</code>, <code>{{email}}</code>, beliebige<br />  Attribute).<br />- <strong>Kampagnen</strong> — Liste/Segment + Template wählen, anlegen, senden (oder planen). Der Versand iteriert<br />  <strong>nur bestätigte, segment-passende</strong> Abonnenten, respektiert eine Sendegrenze (<code>sendCap</code> + Tageslimit),<br />  hängt an <strong>jede</strong> Mail einen Abmeldelink und fügt Öffnungs-/Klick-Tracking hinzu.<br />- <strong>Sequenzen</strong> — geordnete Mail-Serie mit Zeitversatz; Trigger: nach Bestätigung (on-confirm).<br />- <strong>Tracking</strong> — <strong>Öffnung</strong> (1×1-Pixel über eine öffentliche Route, token = Send-ID), <strong>Klick</strong> (Links<br />  werden auf eine öffentliche Redirect-Route umgeschrieben, die den Klick erfasst und dann auf die<br />  <strong>gespeicherte</strong> Ziel-URL 302-weiterleitet — kein Open-Redirect), <strong>Bounce</strong> (Bounce-Zustand +<br />  Route/Hook zum Markieren).<br />- <strong>Segmentierung</strong> — filtere Abonnenten nach Tag/Attribut/Aktivität (geöffnet/geklickt) für die<br />  Kampagnen-Zielgruppe.</p>\n<h2>Sicherheit / Compliance (#78)</h2>\n<p>- <strong>Abmeldelink in jeder Marketing-Mail</strong> (tokengeschützte öffentliche Route, ein-Klick, ohne Login) —<br />  CAN-SPAM/GDPR, nicht verhandelbar.<br />- <strong>Double-Opt-in</strong> ist Standard; unbestätigte Adressen werden nie massenhaft angeschrieben.<br />- Tracking-Pixel / Klick-Redirect / Confirm / Unsubscribe sind <strong>öffentlich, aber tokengeschützt</strong><br />  (unrater Token → erfassen → handeln). Das Redirect-Ziel ist <strong>ausschließlich die gespeicherte URL</strong><br />  (kein Open-Redirect / SSRF über ein vom Empfänger geliefertes Ziel — es gibt keinen Ziel-Parameter im<br />  Request).<br />- Secrets (SMTP/ESP) kommen aus <strong>Aiopta-Config</strong>, nie geloggt. In Logs/URLs steht keine PII außer dem<br />  opaken Token.<br />- Ohne konfigurierte E-Mail liefert der Versand ein klares <strong>400</strong> (&quot;zuerst E-Mail konfigurieren&quot;); die<br />  Seite zeigt den <code>not-configured</code>-EmptyState (CTA → <code>/settings/connections</code>), nie ein 500.<br />- <strong>Pro-Benutzer-Isolation (#142 / INBOX-59, ADR-054):</strong> jede Liste, jeder Abonnent, jedes Template,<br />  jede Kampagne/Sequenz/Send/Tracking-Zeile trägt <code>user_id NOT NULL</code> und gehört dem **erstellenden<br />  Benutzer** (server-seitig aus <code>auth.id</code> gestempelt, ein Client-Owner wird ignoriert). Lesen/Ändern/<br />  Senden/Löschen ist auf die <strong>eigenen</strong> Daten gescoped; eine fremde id ist ein 404 (nie 403). Das<br />  Tageslimit ist pro Benutzer. Die <strong>öffentlichen, tokengeschützten</strong> Routen sind die einzige Ausnahme<br />  (login-frei, nur durch den unraten Token autorisiert).</p>\n<h2>Versand-Konfiguration (Aiopta addon-config, wie lead-gen)</h2>\n<p>SMTP: <code>SMTP_HOST</code>, <code>SMTP_PORT</code>, <code>SMTP_USER</code>, <code>SMTP_PASS</code>, <code>SMTP_SECURE</code>, <code>SMTP_FROM</code> — <strong>oder</strong> ein ESP:<br /><code>ESP_PROVIDER</code> + <code>ESP_API_KEY</code> + <code>ESP_FROM</code>. Optional: <code>SEND_DAILY_LIMIT</code> (Standard 2000). Secrets werden<br />mit <code>is_secret=1</code> gespeichert, maskiert, nie geloggt. (Siehe OPEN-DECISIONS <code>email-marketing-mail-config</code>.)</p>\n<h2>Architektur</h2>\n<p>- <strong>Quelle der Wahrheit:</strong> lokale SQLite (<code>email_marketing_*</code>), erstellt in <code>install()</code>, entfernt in<br />  <code>uninstall()</code> über Aiopta instances <code>getDb()</code>.<br />- <strong>Mail-Pfad:</strong> <code>src/lib/sender.ts</code> liest die Config wie lead-gen (<code>getAddonConfiguration</code>) und versendet<br />  per <strong>nodemailer</strong> (lazy import) — kein neuer SMTP-Layer.<br />- <strong>Pure Kerne (unit-getestet, ohne DB):</strong> Opt-in-Statusmaschine, Merge-Felder, Segment-Prädikat,<br />  Abmelde-Footer, Klick-Link-Rewrite (gespeicherte Ziel-URL).<br />- <strong>Öffentliche Routen:</strong> <code>src/proxy.ts</code> (<code>isEmailMarketingPublic</code>) lässt GETs unter <code>/public/</code> ohne<br />  Credential durch — derselbe Mechanismus wie der file-hosting-Serve-Read.</p>\n<h2>Owner-abhängig (OPEN-DECISIONS)</h2>\n<p>- <strong>Zustellbarkeit</strong> (Sending-Domain + SPF/DKIM/DMARC) — der Owner stellt die Mail-Infrastruktur; das<br />  Addon versendet über die konfigurierte SMTP/ESP.<br />- <strong>Bounce-Ingestion</strong> (IMAP-Postfach oder Provider-Webhook) — der Bounce-<strong>Zustand</strong> + die<br />  <code>POST /sends/:id/bounce</code>-Route/Hook shippen heute; der Ingestion-Transport ist Owner-Infra.</p>\n<p>Beides ist <strong>nicht blockierend</strong>: das Addon läuft graceful ohne sie.</p>\n<h2>Tests</h2>\n<p><code>addons/email-marketing/tests/unit</code> — Store (Opt-in-Statusmaschine + Double-Opt-in-Confirm-Flip,<br />confirmed-only-Audience, Tracking, Bounce-Flip), Sender (gemockte SMTP: confirmed-only, Abmeldelink in<br />jeder Mail, Cap, Klick-Rewrite gespeicherte URL, ohne SMTP → graceful 400), öffentliche Routen<br />(Confirm/Unsubscribe-Flip, Open-Pixel, Klick-Redirect nur auf gespeicherte URL + fremdes Ziel abgelehnt),<br /><strong>Evil-User-Isolation</strong> (<code>isolation.test.ts</code> — Benutzer A/B, B sieht/ändert/sendet/löscht A&#39;s Listen/<br />Abonnenten/Templates/Kampagnen/Sequenzen nicht; Owner aus <code>auth.id</code> gestempelt; Route-Layer 401 unauth +<br />404 für fremde id), Proxy-Allow-Liste, Manifest-Audit.</p>",
          "byteSize": 5556
        }
      }
    },
    {
      "id": "error-monitoring",
      "uuid": "ebe1c125-189b-4270-9209-c6979d5c242a",
      "name": "Error Monitoring",
      "version": "1.0.2",
      "description": "Self-hosted, Sentry-like in-app error capture: unhandled client exceptions, API 500s and server errors are captured per-user, grouped by fingerprint with occurrence counts, and surfaced for acknowledgement/resolution. No external service.",
      "author": "Aiopta",
      "icon": "🐞",
      "capabilities": [
        "error-monitoring",
        "observability",
        "errors"
      ],
      "tags": [],
      "download": "/artifacts/error-monitoring-1.0.2.zip",
      "sha256": "6cd7357c27b34693bd2c0a87f808863dd1b51e57679104693b9a27cf140214c3",
      "size": 48735,
      "page": null,
      "deprecated": true,
      "replacedBy": "monitoring",
      "deprecation": {
        "deprecated": true,
        "replacedBy": "monitoring",
        "sunset": "2026-12-31",
        "successorPath": "/addons/monitoring",
        "successorLabel": "Observability (Nachfolger)"
      },
      "endpoints": [
        "/api/v1/addons/error-monitoring/errors",
        "/api/v1/addons/error-monitoring/resolve",
        "/api/v1/addons/error-monitoring/stats"
      ],
      "skills": [
        {
          "name": "error-monitoring-read",
          "displayName": "Error Monitoring: read captured errors",
          "description": "List recent captured errors and aggregated error stats for the user.",
          "permissions": [
            "addons/error-monitoring/errors",
            "addons/error-monitoring/stats"
          ],
          "endpoint": "/api/v1/addons/error-monitoring/errors",
          "scripts": []
        },
        {
          "name": "error-monitoring-manage",
          "displayName": "Error Monitoring: capture & resolve errors",
          "description": "Capture an in-app error and acknowledge or resolve captured errors for the user.",
          "permissions": [
            "addons/error-monitoring/errors",
            "addons/error-monitoring/resolve"
          ],
          "endpoint": "/api/v1/addons/error-monitoring/errors",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": null
    },
    {
      "id": "etsy",
      "uuid": "27b03d6b-6e37-4f5b-a06d-0c4f14f83967",
      "name": "Etsy",
      "version": "2.0.1",
      "description": "DEPRECATED — zusammengeführt in das kanonische Commerce-Addon `commerce` als Etsy-Connector (ADR-008 Addon-Konsolidierung, Issue #589 / Epic #492). Der Etsy-Channel ist als Feature-Modul unter `commerce/src/connectors/etsy/` migriert: BYOK-Credentials in der gemeinsamen `commerce_connector_config`-Tabelle, Sync-Client auf dem kanonischen `CommerceChannel`-Vertrag, Alt-ID-Redirect `/addons/etsy` → `/addons/commerce?connector=etsy`. etsy bleibt bis zum Sunset (2026-12-31) voll funktionsfähig und datenerhaltend — kein Endpoint entfällt, keine Zeile wird kopiert/gelöscht. Ursprüngliche Fähigkeit: Commerce channel: sync listings, stock & orders from your Etsy shop via the OpenAPI v3 REST API (external SaaS). Each user brings their own API key + OAuth2 access token (BYOK) stored per-user through the scoped data-access layer (ADR-054); no secret is hard-coded or logged and there is no container. Sync is always an explicit call.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "commerce-etsy",
        "etsy-products",
        "etsy-stock",
        "etsy-orders",
        "etsy-sync"
      ],
      "tags": [],
      "download": "/artifacts/etsy-2.0.1.zip",
      "sha256": "41062aa27a6b8bf4fd933dd1b12516d244883c0baf5c297b1154efb9d611d419",
      "size": 70618,
      "page": {
        "id": "etsy-page",
        "path": "/addons/etsy",
        "name": "Etsy",
        "icon": "🎨",
        "section": "Money",
        "component": "@addons/etsy/src/page"
      },
      "deprecated": true,
      "replacedBy": "commerce",
      "deprecation": {
        "deprecated": true,
        "replacedBy": "commerce",
        "sunset": "2026-12-31",
        "successorPath": "/addons/commerce",
        "successorLabel": "Commerce (Nachfolger)"
      },
      "endpoints": [
        "/api/v1/addons/etsy/status",
        "/api/v1/addons/etsy/config",
        "/api/v1/addons/etsy/products",
        "/api/v1/addons/etsy/stock",
        "/api/v1/addons/etsy/orders",
        "/api/v1/addons/etsy/sync"
      ],
      "skills": [
        {
          "name": "etsy-sync",
          "displayName": "Etsy: Sync listings / stock / orders",
          "description": "Trigger an explicit sync of the caller's Etsy shop (listings, stock and orders/receipts) via the OpenAPI v3 API using the user's BYOK credentials.",
          "permissions": [
            "addons/etsy/sync",
            "addons/etsy/products",
            "addons/etsy/orders"
          ],
          "endpoint": "/api/v1/addons/etsy/sync",
          "scripts": []
        },
        {
          "name": "etsy-products",
          "displayName": "Etsy: Listings & Stock",
          "description": "List the caller's cached Etsy listings and stock levels, or write back a stock level to Etsy.",
          "permissions": [
            "addons/etsy/products",
            "addons/etsy/stock"
          ],
          "endpoint": "/api/v1/addons/etsy/products",
          "scripts": []
        },
        {
          "name": "etsy-orders",
          "displayName": "Etsy: Orders",
          "description": "List the caller's cached Etsy orders/receipts.",
          "permissions": [
            "addons/etsy/orders"
          ],
          "endpoint": "/api/v1/addons/etsy/orders",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": null
    },
    {
      "id": "file-hosting",
      "uuid": "94c186f9-93e5-4e38-90c0-672fa0940142",
      "name": "Files",
      "version": "2.0.1",
      "description": "Files: library (documents) + hosting/upload/public-URL. Canonical Files addon (ADR-008) consolidating the former documents addon, with upload, download, and public URL generation using Aiopta's Next.js API routes.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "file-upload",
        "file-download",
        "url-generation",
        "token-based-access",
        "documents",
        "workspace",
        "agent-files"
      ],
      "tags": [],
      "download": "/artifacts/file-hosting-2.0.1.zip",
      "sha256": "fbd6c8299be6375ea31cf22f7ba5147b6892b4b07409b1a6e12c65238dd94fcb",
      "size": 124216,
      "page": {
        "path": "/addons/file-hosting",
        "name": "Files",
        "icon": "📁",
        "section": "Tools",
        "component": "@addons/file-hosting/src/components/FileHostingPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/file-hosting/upload",
        "/api/v1/addons/file-hosting/files",
        "/api/v1/addons/file-hosting/list",
        "/api/v1/addons/file-hosting/file/:id",
        "/api/v1/addons/file-hosting/serve/:id",
        "/api/v1/addons/file-hosting/config",
        "/api/v1/addons/file-hosting/agent-files"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>File Hosting Addon</h1>\n<p>A file hosting service addon for Aiopta that provides file upload, download, and public URL generation capabilities using Aiopta&#39;s Next.js API routes.</p>\n<h2>Features</h2>\n<ul><li>📤 <strong>File Upload</strong>: Upload files with automatic validation (size, type)</li><li>📥 <strong>File Download</strong>: Access files via unique IDs through Next.js API routes</li><li>🔗 <strong>Public URL Generation</strong>: Generate public URLs with optional token-based access</li><li>🔒 <strong>Token-based Access</strong>: Optional token protection for file downloads using Aiopta&#39;s API key system</li><li>📊 <strong>Metadata Tracking</strong>: Store file metadata (name, size, MIME type, checksum)</li><li>⚙️ <strong>Configurable</strong>: Customize upload directory, file size limits, and allowed types</li><li>🌐 <strong>Next.js Integration</strong>: Uses Aiopta&#39;s Next.js web server (no standalone server)</li><li>🔒 <strong>Type Safety</strong>: Full TypeScript support</li></ul>\n<h2>Configuration</h2>\n<h3>Default Configuration</h3>\n<pre><code class=\"lang-json\">{\n  &quot;publicUrl&quot;: &quot;http://localhost:3000&quot;,\n  &quot;requireToken&quot;: true,\n  &quot;uploadDir&quot;: &quot;data/uploads&quot;,\n  &quot;maxFileSize&quot;: 104857600,\n  &quot;allowedMimeTypes&quot;: [\n    &quot;image/jpeg&quot;,\n    &quot;image/png&quot;,\n    &quot;image/gif&quot;,\n    &quot;image/webp&quot;,\n    &quot;application/pdf&quot;,\n    &quot;text/plain&quot;,\n    &quot;text/markdown&quot;,\n    &quot;application/json&quot;,\n    &quot;application/zip&quot;,\n    &quot;application/x-zip-compressed&quot;\n  ]\n}</code></pre>\n<h3>Configuration Options</h3>\n<p>| Option | Type | Default | Description |<br />|--------|------|---------|-------------|<br />| <code>publicUrl</code> | string | <code>&quot;http://localhost:3000&quot;</code> | The public base URL for file access |<br />| <code>requireToken</code> | boolean | <code>true</code> | Whether authentication is required |<br />| <code>uploadDir</code> | string | <code>&quot;data/uploads&quot;</code> | Directory to store uploaded files |<br />| <code>maxFileSize</code> | number | <code>104857600</code> (100MB) | Maximum file size in bytes |<br />| <code>allowedMimeTypes</code> | string[] | See default | List of allowed MIME types |</p>\n<h2>API Endpoints</h2>\n<p>### Upload File<br /><strong>POST</strong> <code>/api/v1/addons/file-hosting/upload</code><br />Upload a file and get its ID and public URL.</p>\n<p>### Get File Info<br /><strong>GET</strong> <code>/api/v1/addons/file-hosting/file/:id</code><br />Get file metadata and download URL.</p>\n<p>### Download File<br /><strong>GET</strong> <code>/api/v1/addons/file-hosting/serve/:id?token={apiKey}</code><br />Download the file. Token can be provided via query param, Authorization header, or X-API-Key header.</p>\n<p>### Delete File<br /><strong>DELETE</strong> <code>/api/v1/addons/file-hosting/file/:id</code><br />Delete a file by ID.</p>\n<p>### Get Configuration<br /><strong>GET</strong> <code>/api/v1/addons/file-hosting/config</code><br />Get the current addon configuration.</p>\n<p>### Update Configuration<br /><strong>POST</strong> <code>/api/v1/addons/file-hosting/config</code><br />Update the addon configuration.</p>\n<h2>Examples</h2>\n<p><strong>Upload:</strong><br />```bash<br />curl -X POST http://localhost:3000/api/v1/addons/file-hosting/upload \\<br />  -F &quot;file=@document.pdf&quot;<br />```</p>\n<p><strong>Download with token:</strong><br />```bash<br />curl &quot;http://localhost:3000/api/v1/addons/file-hosting/serve/{id}?token={your_api_key}&quot; -o file.pdf<br />```</p>\n<h2>Architecture</h2>\n<p>This addon uses Aiopta&#39;s Next.js web server for all file serving operations. No standalone server is required.</p>\n<h3>Token-based Access</h3>\n<p>When <code>requireToken: true</code>, files can only be accessed with valid authentication:<br />1. Query Parameter: <code>?token={apiKey}</code><br />2. Authorization Header: <code>Authorization: Bearer {apiKey}</code><br />3. X-API-Key Header: <code>X-API-Key: {apiKey}</code></p>\n<p>The token must be a valid Aiopta API key.</p>",
          "byteSize": 3187
        }
      }
    },
    {
      "id": "forgejo",
      "uuid": "1e4f363b-6556-4e35-b5e4-9500c03082ea",
      "name": "Forgejo",
      "version": "1.2.2",
      "description": "Manage Forgejo / Gitea instances from Aiopta: list repositories, issues, pull requests, projects, milestones, releases, and labels through a single dashboard, plus a per-user instance picker for admins and end-users. Slice #227 ships the skeleton (manifest + page shell + SKILL.md placeholder). Slice #228 ships the typed HTTP client with token-redaction and retry semantics. Slice #229 ships the instance picker (admin + user, AES-GCM at-rest token encryption, per-user isolation, connectivity test, empty-state CTA — no defaults). Slice #230 ships the read-dashboard (filter engine, priority mapper, LRU cache with stampede protection, and the API routes + UI components for repos/issues/PRs tabs). Slice #231 ships the Issue/PR write API on top of the read dashboard (CRUD, comments, labels, reviews, merge, approval gate, idempotency, job pattern). Projects CRUD in #232, SKILL.md freeze in #233.",
      "author": "Aiopta",
      "icon": "🦊",
      "capabilities": [
        "forgejo",
        "gitea",
        "repositories",
        "issues",
        "pull-requests",
        "projects",
        "milestones",
        "releases",
        "labels"
      ],
      "tags": [],
      "download": "/artifacts/forgejo-1.2.2.zip",
      "sha256": "e0eeb7f4eca10cf74465009b41ead6f56b5fc17ee51a7d88d26e71328331b7f2",
      "size": 896356,
      "page": [
        {
          "id": "forgejo",
          "path": "/addons/forgejo",
          "name": "Forgejo",
          "icon": "🦊",
          "section": "Dev",
          "component": "@addons/forgejo/src/components/ForgejoDashboard"
        }
      ],
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/forgejo/repos",
        "/api/v1/addons/forgejo/issues",
        "/api/v1/addons/forgejo/pulls",
        "/api/v1/addons/forgejo/projects",
        "/api/v1/addons/forgejo/labels",
        "/api/v1/addons/forgejo/members",
        "/api/v1/addons/forgejo/cache/stats",
        "/api/v1/addons/forgejo/issues/{instance}/{repo}",
        "/api/v1/addons/forgejo/issues/{instance}/{repo}/{index}",
        "/api/v1/addons/forgejo/issues/{instance}/{repo}/{index}/state",
        "/api/v1/addons/forgejo/issues/{instance}/{repo}/{index}/comments",
        "/api/v1/addons/forgejo/issues/{instance}/{repo}/{index}/labels",
        "/api/v1/addons/forgejo/issues/{instance}/{repo}/{index}/assignees",
        "/api/v1/addons/forgejo/issues/bulk/label",
        "/api/v1/addons/forgejo/pulls/{instance}/{repo}/{index}",
        "/api/v1/addons/forgejo/pulls/{instance}/{repo}/{index}/files",
        "/api/v1/addons/forgejo/pulls/{instance}/{repo}/{index}/commits",
        "/api/v1/addons/forgejo/pulls/{instance}/{repo}/{index}/reviews",
        "/api/v1/addons/forgejo/pulls/{instance}/{repo}/{index}/merge",
        "/api/v1/addons/forgejo/pulls/{instance}/{repo}/{index}/state",
        "/api/v1/addons/forgejo/jobs/{id}",
        "/api/v1/addons/forgejo/instances",
        "/api/v1/addons/forgejo/instances/{id}",
        "/api/v1/addons/forgejo/instances/{id}/test",
        "/api/v1/addons/forgejo/admin/instances",
        "/api/v1/addons/forgejo/admin/instances/{id}"
      ],
      "skills": [],
      "mcpServers": [
        {
          "id": "forgejo-tools",
          "displayName": "Forgejo tools",
          "description": "Read-only Forgejo/Gitea companion tools for agents: decode/encode the dashboard issue/PR filter wire format, map priorities to label names, detect issue priority, redact credential patterns from text, and validate instance base URLs and labels - faithful ports of the addon's src/lib modules (issue #420, epic #399). Mutates nothing; live per-user Forgejo access is resolved core-side via the declared config/credential slots.",
          "transport": "stdio",
          "runtime": "node",
          "entry": "forgejo-tools/server.mjs",
          "permissions": [
            "addons/forgejo/read"
          ],
          "toolPolicy": {
            "mode": "read-only",
            "allowDestructive": false
          },
          "configRefs": [
            "baseUrl"
          ],
          "credentialRefs": [
            "token"
          ],
          "archiveEntry": "forgejo/mcp/servers/forgejo-tools/server.mjs",
          "sha256": "8f479e5a96d349cd12582a4f026721f1e5c27941808fe59971a1d3097500cbc4",
          "size": 29917,
          "descriptorVersion": 1
        }
      ],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Forgejo Addon</h1>\n<p>Aiopta integration for <strong>Forgejo</strong> and <strong>Gitea</strong>. Manage repositories,<br />issues, pull requests, projects, milestones, releases, and labels<br />across one or more Forgejo / Gitea instances — both from the dashboard<br />UI (<code>/addons/forgejo</code>) and through the Aiopta agent skill<br />(<code>SKILL.md</code>).</p>\n<blockquote><p><strong>Status: 1.0.0.</strong> Slices #227 (skeleton), #228 (typed client +</p><p>token-redaction + retry), and #230 (read-dashboard: filter engine,</p><p>priority mapper, LRU cache with stampede protection, API routes, and</p><p>UI components) are landed. Slice #233 (SKILL.md + agent discovery</p><p>smoke + README polish) is finalising the agent-surface + the</p><p>1.0.0 manifest. Instance picker (#229), write API (#231), and</p><p>projects / milestones / releases / labels admin (#232) are tracked</p><p>as separate issues — see the Roadmap below.</p></blockquote>\n<h2>Overview</h2>\n<p>- <strong>Addon ID:</strong> <code>forgejo</code><br />- <strong>Version:</strong> <code>1.0.0</code> (slice #228 client + slice #230 read-dashboard<br />  + slice #233 SKILL.md agent surface)<br />- <strong>Capabilities:</strong> <code>forgejo</code>, <code>gitea</code>, <code>repositories</code>, <code>issues</code>,<br />  <code>pull-requests</code>, <code>projects</code>, <code>milestones</code>, <code>releases</code>, <code>labels</code><br />- <strong>UI Page:</strong> <code>/addons/forgejo</code><br />- <strong>Author:</strong> Aiopta<br />- <strong>License:</strong> Per the surrounding Aiopta addon marketplace terms</p>\n<h2>Why Forgejo + Gitea share one client</h2>\n<p>Forgejo&#39;s HTTP API is API-compatible with Gitea&#39;s. A single client<br />serves both — no parallel code path. The addon assumes upstream<br />versions <code>&gt;= Forgejo 1.20 / Gitea 1.20</code>; older instances will receive<br />a <code>426 Upgrade Required</code> response from the concrete endpoints (see<br /><code>forgejo-client.ts</code>).</p>\n<h2>Roadmap</h2>\n<p>| Issue | Title | Status | Slice |<br />|---|---|---|---|<br />| #227 | Forgejo-Addon skeleton | ✅ | manifest, page shell, docs |<br />| #228 | TypeScript client + auth wrapper (token redaction, retry) | ✅ | core client |<br />| #229 | Instance management (admin + user, scoped storage, RBAC, encrypted tokens) | ⏳ | instance picker |<br />| #230 | Read dashboard with repos/issues/PRs + filter engine | ✅ | dashboard backend + UI |<br />| #231 | Issue/PR detail + write API (CRUD, comments, labels, reviews, merge, approval gate) | ⏳ | write surface |<br />| #232 | Projects + milestones + releases + labels admin | ⏳ | board surface |<br />| #233 | SKILL.md + agent discovery smoke + README polish | ✅ | agent surface (this slice) |</p>\n<h2>Setup</h2>\n<p>The Forgejo addon does <strong>not</strong> require any environment variables at<br />install time. The HTTP client is instantiated lazily when the user<br />configures a per-user instance (slice #229 lands the instance picker<br />and the encrypted-token storage). Until then, an env-var-backed<br />default instance store lets the dashboard run against a single<br />configured instance.</p>\n<p>Runtime data that the addon needs to operate:</p>\n<p>| Source | Where it lives | Notes |<br />|--------|----------------|-------|<br />| Per-user instance URL | caller&#39;s per-user data root (<code>&lt;DATA_DIR&gt;/addons/forgejo/&lt;userId&gt;/instances.json</code>) | Configured via the dashboard. No hardcoded default. |<br />| Per-user access token | same root, encrypted at rest | Slice #229. |<br />| Upstream Forgejo / Gitea version | n/a | Required <code>&gt;= 1.20</code>. Older versions emit <code>426 Upgrade Required</code>. |</p>\n<h3>Env-var fallback (slice #230 — interim)</h3>\n<p>For the env-var-backed default instance store, set:</p>\n<pre><code class=\"lang-bash\">export FORGEJO_INSTANCE_&lt;ID&gt;_URL=&quot;https://your-forgejo-host.example&quot;\nexport FORGEJO_INSTANCE_&lt;ID&gt;_TOKEN=&quot;ghp_…&quot;\n# Optional: override the priority label prefix (default: &quot;priority:&quot;)\nexport FORGEJO_PRIORITY_PREFIX=&quot;prio&quot;\n# Optional: override the read-cache TTL (default: 60s)\nexport FORGEJO_READ_CACHE_TTL_SECONDS=120</code></pre>\n<p>Slice #229 will replace the env-var backend with a DB-backed store<br />without changing the <code>instance-store.ts</code> interface.</p>\n<h2>Endpoint overview</h2>\n<p>The agent skill (<code>SKILL.md</code>) is the canonical endpoint cheat-sheet<br />for the agent. The manifest&#39;s <code>endpoints</code> array mirrors the slice<br />#228 baseline of the typed client — every endpoint listed there is<br />reachable from <code>@addons/forgejo/src/lib/forgejo-client.ts</code> today.<br />The slice #230 read-dashboard exposes an additional Aiopta-side<br />aggregation surface under <code>/api/v1/addons/forgejo/*</code>.</p>\n<h3>Upstream Forgejo REST API (slice #228 typed client)</h3>\n<p>| Resource | Paths |<br />|----------|-------|<br />| User | <code>/user</code>, <code>/user/repos</code>, <code>/user/projects</code> |<br />| Repos | <code>/repos/{owner}/{name}</code> |<br />| Issues | <code>/repos/{repo}/issues</code>, <code>/repos/{repo}/issues/{number}</code>, <code>/repos/{repo}/issues/{number}/comments</code> |<br />| Pulls | <code>/repos/{repo}/pulls</code>, <code>/repos/{repo}/pulls/{number}</code> |<br />| Projects | <code>/repos/{repo}/projects</code>, <code>/user/projects</code> |<br />| Milestones | <code>/repos/{repo}/milestones</code> |<br />| Releases | <code>/repos/{repo}/releases</code> |<br />| Labels | <code>/repos/{repo}/labels</code> |</p>\n<h3>Aiopta-side dashboard routes (slice #230 read-dashboard)</h3>\n<p>| Method | Path | Purpose |<br />| ------ | ---- | ------- |<br />| GET | <code>/api/v1/addons/forgejo/repos?instance=&amp;search=</code> | List repos for the active instance |<br />| GET | <code>/api/v1/addons/forgejo/issues?&lt;filter&gt;</code> | List issues for a repo |<br />| GET | <code>/api/v1/addons/forgejo/pulls?&lt;filter&gt;</code> | List PRs for a repo |<br />| GET | <code>/api/v1/addons/forgejo/projects?instance=&amp;repo=</code> | Project boards |<br />| GET | <code>/api/v1/addons/forgejo/labels?instance=&amp;repo=</code> | Repo labels (filter dropdown source) |<br />| GET | <code>/api/v1/addons/forgejo/members?instance=&amp;org=</code> | Org members (author / assignee source) |<br />| GET | <code>/api/v1/addons/forgejo/cache/stats</code> | Cache diagnostic counts |</p>\n<p>Future slices extend this surface:</p>\n<p>- <strong>#229</strong> — per-user instance picker (local-only routes, no new<br />  upstream endpoints)<br />- <strong>#231</strong> — write API + approval gate (consumes the slice #228<br />  <code>create</code> / <code>update</code> / <code>addComment</code> endpoints; adds<br />  <code>POST /repos/{repo}/pulls/{number}/merge</code>)<br />- <strong>#232</strong> — projects / milestones / releases / labels admin (adds<br />  <code>POST /user/projects</code>, <code>PATCH /projects/{id}</code>, and label CRUD<br />  against <code>/repos/{repo}/labels</code>)</p>\n<h2>Slice #230 — what ships here</h2>\n<h3>Library (<code>addons/forgejo/src/lib/</code>)</h3>\n<p>- <strong><code>issue-filters.ts</code></strong> — typed <code>IssueFilter</code> + URL codec.<br />  Multi-value groups use repeated keys (<code>?labels=bug&amp;labels=urgent</code>)<br />  so labels containing commas, colons, or Unicode roundtrip<br />  unambiguously. AND across groups, OR within each group.<br />  Defaults: <code>state=open</code>, <code>limit=50</code> (hard cap 200).<br />- <strong><code>priority-mapper.ts</code></strong> — maps priority values to label names with<br />  a configurable prefix (default <code>priority:</code>). Accepts compact<br />  <code>P0..P3</code> aliases. Detects the highest priority present on an<br />  issue&#39;s labels via <code>detect()</code>.<br />- <strong><code>read-cache.ts</code></strong> — per-user LRU cache with TTL (default 60s,<br />  overridable via <code>FORGEJO_READ_CACHE_TTL_SECONDS</code>) + stale-while-<br />  revalidate + stampede protection (100 concurrent identical<br />  requests coalesce into one upstream call). Cache keys NEVER<br />  contain the configured token; user-supplied free-text <code>q</code> is<br />  FNV-1a-hashed before key assembly.<br />- <strong><code>instance-store.ts</code></strong> — read-side contract for the per-user<br />  instance + token lookup. Ships an env-var-backed default so the<br />  dashboard runs before slice #229&#39;s DB schema lands; the<br />  implementation in #229 swaps the loader while keeping the<br />  interface intact.<br />- <strong><code>read-dashboard-api.ts</code></strong> — server-side helpers used by every<br />  read route: instance resolution, typed client construction, the<br />  shared read cache, and the <code>DashboardError</code> wire shape (<code>code</code> /<br />  <code>lastSuccessAt</code> / <code>retryAfterMs</code>).</p>\n<h3>API routes (<code>addons/forgejo/src/app/api/v1/addons/forgejo/</code>)</h3>\n<p>- <code>GET /repos?instance=&amp;search=&amp;page=&amp;limit=</code> — list repos for the<br />  active instance.<br />- <code>GET /issues?&lt;filter&gt;</code> — list issues for a repo, filtered.<br />- <code>GET /pulls?&lt;filter&gt;</code> — list PRs for a repo, filtered.<br />- <code>GET /projects?instance=&amp;repo=</code> — project boards.<br />- <code>GET /labels?instance=&amp;repo=</code> — repo labels (filter dropdown<br />  source).<br />- <code>GET /members?instance=&amp;org=</code> — org members (author / assignee<br />  typeahead).<br />- <code>GET /cache/stats</code> — diagnostic cache stats (size / hits / misses<br />  / stampedeHits / evictions / hit-ratio).</p>\n<p>All routes are <code>userRoute</code>-wrapped (auth required) and flow through<br />the shared cache + stampede helper.</p>\n<h3>UI (<code>addons/forgejo/src/components/</code> + <code>hooks/</code>)</h3>\n<p>- <strong><code>ForgejoDashboard.tsx</code></strong> — three-tab page (Repos / Issues / PRs)<br />  with instance + repo pickers. Filter state is mirrored to the URL<br />  via <code>useForgejoQuery</code> so refresh restores the exact view and the<br />  browser back/forward buttons navigate between filter histories.<br />- <strong><code>IssueFilterBar.tsx</code></strong> / <strong><code>PRFilterBar.tsx</code></strong> — combined filter<br />  bars (labels, priority, state, author, assignee, updated-range,<br />  full-text <code>q</code>).<br />- <strong><code>IssueTable.tsx</code></strong> / <strong><code>PRTable.tsx</code></strong> / <strong><code>ReposTable.tsx</code></strong> —<br />  tables with cursor-style pagination (50 default, 200 hard cap, no<br />  endless scroll in v1).<br />- <strong><code>LabelPill.tsx</code></strong> / <strong><code>PriorityPill.tsx</code></strong> — coloured chips with<br />  stable priority colour mapping (critical=red, high=orange,<br />  medium=yellow, low=gray).<br />- <strong><code>EmptyState.tsx</code></strong> — three distinguished copy variants:<br />  <code>no_matches</code>, <code>repo_empty</code>, <code>no_instance</code> — each with its own<br />  <code>data-i18n-key</code> for the v1 localisation wire.<br />- <strong><code>ErrorState.tsx</code></strong> — stable <code>code</code> / <code>lastSuccessAt</code> /<br />  <code>retryAfterMs</code> UX with a Retry button on transient errors and a<br />  &quot;Renew token&quot; CTA on <code>UNAUTHORIZED</code>.<br />- <strong><code>StaleDataBanner.tsx</code></strong> — &quot;Data from X — stale by Ys&quot; banner<br />  when the cache TTL has elapsed (stale-while-revalidate).<br />- <strong><code>useForgejoQuery</code></strong> — data-fetching hook with URL sync,<br />  background revalidation, and typed error surface.</p>\n<h3>Tests</h3>\n<p>- <code>tests/unit/issue-filters.test.ts</code> (52 tests) — Encode/Decode<br />  roundtrip + 20 random vectors + special-character handling +<br />  pagination caps.<br />- <code>tests/unit/priority-mapper.test.ts</code> (21 tests) — default prefix<br />  + custom prefix (<code>prio</code> → <code>prio:high</code>) + aliases + detection.<br />- <code>tests/unit/read-cache.test.ts</code> (20 tests) — TTL, stampede<br />  protection (100 calls → 1 upstream), token redaction invariant,<br />  LRU eviction, per-user isolation.<br />- <code>tests/unit/read-api.test.ts</code> (12 tests) — integration coverage<br />  for every read endpoint against the mock Forgejo server +<br />  <code>DashboardError</code> shape + stampede.<br />- <code>tests/e2e/dashboard.spec.ts</code> — Playwright spec covering tab<br />  navigation, three-filter combination + URL roundtrip + back-button<br />  restoration, empty-state rendering, and upstream error UX.</p>\n<h2>Architecture notes</h2>\n<p>- <strong>Typed HTTP client.</strong> <code>src/lib/forgejo-client.ts</code> wraps the<br />  upstream REST API in a small typed surface (<code>repos</code>, <code>issues</code>,<br />  <code>pulls</code>, <code>projects</code>, <code>milestones</code>, <code>releases</code>, <code>labels</code>,<br />  <code>users</code>). It uses Node 20+ built-in <code>fetch</code> + <code>AbortController</code><br />  — no <code>gitea-js</code> or <code>undici</code> dependency (see<br />  <code>docs/LIBRARY-EVALUATION.md</code> for the rationale).<br />- <strong>Token redaction.</strong> <code>src/lib/token-redaction.ts</code> rewrites every<br />  <code>token &lt;value&gt;</code> and <code>Bearer &lt;value&gt;</code> literal to <code>***REDACTED***</code><br />  in response bodies, error messages, and stack traces before they<br />  leave the client. The unit test<br />  <code>tests/unit/token-redaction.test.ts</code> pins this contract.<br />- <strong>Retry semantics.</strong> <code>src/lib/retry.ts</code> implements exponential<br />  backoff (<code>baseMs</code>, doubling per attempt), honours the upstream<br />  <code>Retry-After</code> header on <code>429</code>, and stops at <code>maxRetries</code> (default<br />  3). Retryable codes are <code>429</code> and any <code>5xx</code>; non-retryable <code>4xx</code><br />  propagate immediately.<br />- <strong>Per-user isolation.</strong> Every persisted artifact is registered via<br />  <code>registerScopedTable()</code> per ADR-054. The addon never reads or<br />  writes another user&#39;s instance configuration or cached state.<br />- <strong>No hardcoded default instance.</strong> No demo URL, no demo token, no<br />  silent default admin. When the user has not configured an<br />  instance, the dashboard shows an empty state with a CTA and every<br />  API call returns a clear <code>instance_not_configured</code> error (slice<br />  #229 will codify that error code).<br />- <strong>Approval gate.</strong> Write operations (issue close, PR merge, label<br />  changes) flow through the existing autonomy/approval pipeline —<br />  never silently executed. The read-dashboard does not perform any<br />  writes.</p>\n<h2>Slice #233 — what ships here</h2>\n<p>- <code>addons/forgejo/SKILL.md</code> — the agent skill, with the 5 mandatory<br />  sections (Purpose, When to use, Capability / security, Endpoint<br />  cheat-sheet, Example curl) modelled on<br />  <code>addons/task-board/SKILL.md</code>.<br />- <code>addons/forgejo/README.md</code> — this file (Setup, Env-Vars, Endpoint<br />  overview, Architecture notes).<br />- <code>addons/forgejo/addon.json</code> — finalised manifest with<br />  <code>version: 1.0.0</code>, the complete <code>endpoints</code> list (slice #228 typed<br />  client baseline + slice #230 read-dashboard routes),<br />  <code>capabilities</code>, and <code>pages</code> array.<br />- <code>addons/forgejo/tests/integration/skill-discovery.test.ts</code> —<br />  agent-discovery smoke test: stub loader finds <code>SKILL.md</code> and<br />  parses the 5 sections successfully.<br />- <code>addons/forgejo/tests/integration/marketplace-build.test.ts</code> —<br />  confirms <code>npm run build:artifacts</code> produces a ZIP and<br />  <code>npm run build:catalog</code> includes the entry.</p>\n<h2>Non-goals (slice #233)</h2>\n<p>- <strong>No new HTTP endpoints</strong> — this slice is docs + tests only.<br />- <strong>No new client methods</strong> — slice #228 owns the client surface.<br />- <strong>No instance picker</strong> — slice #229.<br />- <strong>No filter engine</strong> — slice #230.<br />- <strong>No write operations</strong> — slices #231 / #232.<br />- <strong>No agent-loader changes</strong> — the loader lives in<br />  <code>aiopta-core/src/lib/addons/addon-skills.ts</code> and is out of scope<br />  for this slice.</p>\n<h2>License</h2>\n<p>Per the Aiopta addon marketplace repository terms.</p>",
          "byteSize": 13228
        }
      }
    },
    {
      "id": "hermes-desktop",
      "uuid": "332ed156-aa4b-4dd4-92b7-fd173b48d71f",
      "name": "Hermes Desktop",
      "version": "0.6.1",
      "description": "Commit-pinned feature matrix for NousResearch hermes-agent desktop capabilities plus a secret-free capability adapter (issues #101 + #169), a per-user Hermes sessions UI (sessions list, SSE stream with chunk dedupe, Stop, Retry — issue #120), a per-user workspace file browser + sandboxed preview envelope (issue #167), a secret-free settings panel for model / personality / skills / memory / automation-status (issue #121), an optional voice input / output panel with explicit opt-in, retention hints and secret-free verdict (issue #168), and a pinned upstream + image-compatibility contract (issue #169). The shell surfaces only capabilities and settings backed by an Aiopta contract — no optimistic flags. Status: chat-streaming/tool-activity/sessions/provider/personality/skills/memory/automation/tool-settings are partial, file-browser/side-by-side-previews are partial (issue #167 — capability-gated, per-user workspace root, sandboxed preview, no core-origin rights for active content), voice-input + voice-output are partial (issue #168 — explicit opt-in, secret-free, retention hint, no audio bytes logged, permission revoke collapses to unavailable), image-pin is partial, upstream-update is unsupported-by-design, shell-cache-invalidation/shell-no-optimistic-flags/preview-sandbox/no-file-url/voice-secret-free are full. Pin JSON (hermes-desktop.pin.json) hard-binds the source commit + the OCI image + sha256 imageDigest + API schema version + license + compatibility issue per issue #169; any drift is a hard error in scripts/validate-addons.cjs. The browser talks only to Aiopta APIs — no Hermes-domain code is duplicated here.",
      "author": "Aiopta",
      "icon": "📜",
      "capabilities": [
        "hermes-desktop",
        "feature-matrix",
        "capability-adapter",
        "secret-free-status",
        "commit-pinned-contract",
        "image-digest-pinned",
        "api-schema-version-pinned",
        "supply-chain-checked",
        "hermes-sessions",
        "hermes-streaming",
        "hermes-stream-stop",
        "hermes-stream-retry",
        "hermes-workspace-files",
        "hermes-workspace-preview",
        "preview-sandboxed",
        "no-file-url",
        "settings-panel",
        "settings-shell-state",
        "voice-input",
        "voice-output",
        "voice-opt-in",
        "voice-secret-free",
        "voice-audio-size-limit"
      ],
      "tags": [],
      "download": "/artifacts/hermes-desktop-0.6.1.zip",
      "sha256": "dc9a5d6297897e591e2b5589a5d706f9fd113649d9934ec47d5fa82973c97757",
      "size": 922214,
      "page": {
        "id": "hermes-desktop",
        "name": "Hermes Desktop",
        "path": "/addons/hermes-desktop",
        "icon": "📜",
        "section": "Tools",
        "component": "@addons/hermes-desktop/src/components/HermesDesktopPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/hermes-desktop/capabilities",
        "/api/v1/addons/hermes-desktop/sessions",
        "/api/v1/addons/hermes-desktop/sessions/{id}/stream",
        "/api/v1/addons/hermes-desktop/sessions/{id}/stop",
        "/api/v1/addons/hermes-desktop/sessions/{id}/retry",
        "/api/v1/addons/hermes-desktop/files",
        "/api/v1/addons/hermes-desktop/preview",
        "/api/v1/addons/hermes-desktop/settings",
        "/api/v1/addons/hermes-desktop/voice"
      ],
      "skills": [
        {
          "name": "hermes-desktop-capabilities",
          "displayName": "Hermes Desktop: Capability Matrix",
          "description": "Read the commit-pinned Hermes-desktop feature matrix and the live capability-adapter verdict for the current backend driver. Secret-free: the route returns status + a reference to the upstream row, never credentials. Use this skill to answer 'which Hermes features work in Aiopta right now?' — no live Hermes call required.",
          "permissions": [
            "addons/hermes-desktop/capabilities"
          ],
          "endpoint": "/api/v1/addons/hermes-desktop/capabilities",
          "scripts": [
            {
              "id": "hermes-desktop-capabilities-cli",
              "name": "hermes-desktop-capabilities-cli",
              "path": "hermes-desktop-capabilities/cli.sh",
              "sha256": "a377bf88125a933c465d28ef61d631c5c7bc8742a7466ea7af7dedfe3e4844fb",
              "interpreter": "bash",
              "timeoutMs": 10000,
              "permissions": [
                "addons/hermes-desktop/capabilities"
              ],
              "inputSchema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 20
                  },
                  "timeoutMs": {
                    "type": "integer",
                    "minimum": 100,
                    "maximum": 300000
                  }
                }
              },
              "outputSchema": {
                "type": "object",
                "required": [
                  "ok"
                ],
                "properties": {
                  "ok": {
                    "type": "boolean"
                  },
                  "status": {
                    "type": "integer"
                  },
                  "data": {},
                  "error": {
                    "type": "string"
                  },
                  "truncated": {
                    "type": "boolean"
                  }
                }
              },
              "size": 12176
            }
          ]
        },
        {
          "name": "hermes-desktop-sessions",
          "displayName": "Hermes Desktop: Per-User Sessions",
          "description": "Read the per-user Hermes sessions list, stream chunks, and Stop/Retry an in-flight session (issue #120). The route is per-user, capability-flagged, and secret-free. No live Hermes call is made — the page renders the cached chat-addon session list and surfaces the HermesDesktop capability verdict for the page to gate Stop/Retry buttons.",
          "permissions": [
            "addons/hermes-desktop/sessions",
            "addons/hermes-desktop/sessions/{id}/stream",
            "addons/hermes-desktop/sessions/{id}/stop",
            "addons/hermes-desktop/sessions/{id}/retry"
          ],
          "endpoint": "/api/v1/addons/hermes-desktop/sessions",
          "scripts": []
        },
        {
          "name": "hermes-desktop-workspace",
          "displayName": "Hermes Desktop: Workspace Files & Preview",
          "description": "Read a per-user workspace file listing and a sandboxed preview of a single file (issue #167). The endpoints are per-user (each user sees their own workspace root, never another user's), capability-gated, and secret-free. Previews are returned as envelopes (mime + body + sandbox + CSP) — never as `file://` URLs or raw paths. Active content in HTML / PDF previews is sandboxed (no `allow-same-origin`) so it cannot reach the core origin.",
          "permissions": [
            "addons/hermes-desktop/files",
            "addons/hermes-desktop/preview"
          ],
          "endpoint": "/api/v1/addons/hermes-desktop/files",
          "scripts": []
        },
        {
          "name": "hermes-desktop-settings",
          "displayName": "Hermes Desktop: Settings Panel",
          "description": "Read the per-row settings verdict for the matrix rows whose group is 'settings' (model / personality / skills / memory / automation-status). Secret-free: the route returns supported/unsupported/unreachable + the Aiopta route that owns the value + the recency of the last successful observation + a stable error code, never credentials or setting values. Use this skill to answer 'which Hermes settings can the user act on right now?' — no live Hermes call required.",
          "permissions": [
            "addons/hermes-desktop/settings"
          ],
          "endpoint": "/api/v1/addons/hermes-desktop/settings",
          "scripts": []
        },
        {
          "name": "hermes-desktop-voice",
          "displayName": "Hermes Desktop: Voice Input / Output",
          "description": "Read the per-row voice verdict (voice-input / voice-output) for the matrix rows whose id starts with 'feat-voice-'. Secret-free: the route returns the per-row shell state (ready | degraded | unavailable | unsupported) + the provider id (NEVER the URL or credential) + the stable error code + the retention hint (max audio bytes, transcript preview limit) — never audio bytes, transcripts, or backend credentials. The verdict is consent-aware: until the user explicitly opts in, every row reports CONSENT_REQUIRED. The agent must surface the verdict verbatim and never recommend a default opt-in.",
          "permissions": [
            "addons/hermes-desktop/voice"
          ],
          "endpoint": "/api/v1/addons/hermes-desktop/voice",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Hermes Desktop (issues #101 + #120 + #121 + #167 + #168 + #169)</h1>\n<p>This addon ships the <strong>pinned-upstream + image-compatibility contract</strong><br />for the <a href=\"https://github.com/NousResearch/hermes-agent\" rel=\"noopener noreferrer\">NousResearch <code>hermes-agent</code></a><br />desktop capabilities together with a <strong>secret-free capability adapter</strong><br />that tells the Aiopta shell which Hermes features are usable <em>right now</em><br />on the active backend driver, a <strong>per-user Hermes sessions UI</strong><br />(session list, SSE stream with chunk dedupe, Stop, Retry — issue #120),<br />a <strong>per-user workspace file browser + sandboxed preview envelope</strong><br />(issue #167), a <strong>secret-free settings panel</strong> that surfaces the<br />5 settings-group rows of the matrix (model / personality / skills /<br />memory / automation-status — issue #121) with a dedicated<br />shell-state vocabulary (<code>supported | unsupported | unreachable</code>) plus<br />the recency of the last successful observation and a stable error<br />code, and an <strong>optional voice input/output panel</strong> with explicit<br />opt-in, secret-free verdict, retention hints and a 10 MiB audio size<br />limit (issue #168). The browser talks only to Aiopta APIs — no<br />Hermes-domain code is duplicated here.</p>\n<p>| Property | Value |<br />|----------|-------|<br />| Addon id | <code>hermes-desktop</code> |<br />| Matrix source | <code>hermes-desktop-parity.json</code> (16 rows, pinned) |<br />| Pin file | <code>hermes-desktop.pin.json</code> |<br />| Pin commit | <code>ea8a8b4af8612b655a5bbfc74eba21e1e806758d</code> |<br />| Pin image | <code>nousresearch/hermes-agent</code> |<br />| Pin image digest | <code>sha256:dabcf559…a7cb</code> — <strong>PLACEHOLDER</strong> (sha256 sentinel, no <code>latest</code>); real immutable digest pending, see issue #169 caveat below |<br />| Pin API schema version | <code>1.0.0</code> (Semver) |<br />| Pin license | <code>MIT</code> |<br />| Spec issue | #101 |<br />| Compatibility issue | #169 (pinned upstream + image-compatibility contract) |<br />| Follow-ups | #120 (sessions UI, done in 0.2.0), #121 (settings panel, done in 0.2.0), #167, #168 |<br />| Endpoints | <code>GET /api/v1/addons/hermes-desktop/capabilities</code> (issue #101)&lt;br&gt;<code>GET /api/v1/addons/hermes-desktop/sessions</code> (issue #120, per-user slice)&lt;br&gt;<code>GET /api/v1/addons/hermes-desktop/sessions/{id}/stream</code> (SSE with chunk dedupe)&lt;br&gt;<code>POST /api/v1/addons/hermes-desktop/sessions/{id}/stop</code> (capability-gated)&lt;br&gt;<code>POST /api/v1/addons/hermes-desktop/sessions/{id}/retry</code> (capability-gated)&lt;br&gt;<code>GET /api/v1/addons/hermes-desktop/settings</code> (issue #121, secret-free per-row verdict) |<br />| Pages | <code>/addons/hermes-desktop</code> (matrix + settings panel, #101 + #121)&lt;br&gt;<code>/addons/hermes-desktop/sessions</code> (sessions list + stream pane, #120) |<br />| Secrets / tokens | <strong>none</strong> — adapter + sessions routes + settings route return status + references, never credentials or setting values |<br />| Hard-coded URLs | <strong>none</strong> — paths come from the matrix JSON + the settings source-bindings map |<br />| <code>unsupported</code> | upstream self-update is intentionally <code>unsupported</code> (by design) |</p>\n<h2>Pinned upstream + image-compatibility contract (issue #169)</h2>\n<p><code>hermes-desktop.pin.json</code> is the single source of truth for the contract<br />that binds Hermes-desktop to upstream. The contract is <strong>explicit</strong>,<br /><strong>rollback-capable</strong>, and <strong>supply-chain-checked</strong>:</p>\n<p>| Field | Purpose |<br />|-------|---------|<br />| <code>commit</code> | Pinned git source commit (issue #101) |<br />| <code>image</code> | OCI registry/repository reference (e.g. <code>nousresearch/hermes-agent</code>) |<br />| <code>imageDigest</code> | sha256-pinned image digest — <code>^sha256:[a-f0-9]{64}$</code>, <strong>no <code>latest</code></strong> |<br />| <code>apiSchemaVersion</code> | Pinned Semver of the upstream API contract |<br />| <code>license</code> | Upstream license string, stored alongside the rest of the contract |<br />| <code>compatibilityIssue</code> | Reference to the contract that hardens the pin (issue #169) |</p>\n<p>The matrix JSON (<code>hermes-desktop-parity.json</code>) mirrors every field<br />above in its <code>pin</code> block. Drift between the two files is a hard error<br />from <code>scripts/validate-addons.cjs</code> (issue #101 + #169) and from the<br />runtime matrix loader (<code>src/lib/matrix.ts:validateMatrix</code>).</p>\n<p>The adapter (<code>src/lib/capability-adapter.ts</code>) surfaces the full contract<br />in the verdict and degrades rows to <code>down</code> / <code>unsupported-version</code><br />when the running image&#39;s sha256 digest or the backend&#39;s API schema<br />version drifts from the pin — <strong>never</strong> an optimistic <code>full</code> flag.</p>\n<p><strong>Update procedure</strong> (explicit, rollbackable, supply-chain checked):<br />1. Operator decides to bump: change all four pinned fields in<br />   <code>hermes-desktop.pin.json</code> AND in <code>hermes-desktop-parity.json</code><br />   (pin block) in a single PR.<br />2. <code>scripts/validate-addons.sh</code> hard-fails on any drift between the<br />   two JSONs.<br />3. <code>node --test addons/hermes-desktop/tests/unit/pin.test.cjs</code><br />   covers the schema contract for new fields.<br />4. Rollback: <code>git revert</code> the PR — pin, matrix, and matrix rows move<br />   together, so a single revert restores the prior contract.<br />5. Supply-chain check: the imageDigest field is sha256-pinned and the<br />   adapter reports <code>imageDigestOk=false</code> if the running image&#39;s sha256<br />   does NOT match. No <code>latest</code> tag, no silent fallback.</p>\n<h3>Caveat — imageDigest is a placeholder (issue #169, partial)</h3>\n<p>The real immutable sha256 digest of <code>nousresearch/hermes-agent</code> at the<br />pinned commit is <strong>not yet available</strong> in issue #169. To keep the<br />contract <em>shape</em> enforced end-to-end (drift detection between<br /><code>hermes-desktop.pin.json</code>, the matrix pin block, and the<br /><code>HERMES_DESKTOP_PIN_IMAGE_DIGEST</code> constant in <code>scripts/validate-addons.cjs</code><br />all work) without faking a real digest, the three mirrors currently carry<br />a clearly-nonzero <strong>placeholder</strong> digest — the sha256 of a descriptive<br />sentinel string. It passes the <code>^sha256:[a-f0-9]{64}$</code> shape check but<br />is <strong>not</strong> a real image digest.</p>\n<p><strong>Before this addon is trusted for production supply-chain claims</strong>, an<br />operator must replace the placeholder in all three mirrors with the<br />actual <code>docker digest</code> (or registry manifest digest) of the pinned image.<br />Until then the <code>feat-image-pin</code> matrix row is <code>partial</code>, not <code>full</code>.</p>\n<h2>Why this addon exists</h2>\n<p><code>hermes-agent</code> ships a desktop client with a broad feature surface<br />(streaming, sessions, side-by-side previews, file browser, voice, settings,<br />updates). Aiopta already covers the chat / streaming / sessions / settings<br />paths through <em>other</em> addons (chat, development, memories, skills).<br />The risk is that the Aiopta browser ends up duplicating Hermes-domain code,<br />or worse, advertising features that are not actually wired up.</p>\n<p>This addon fixes that:</p>\n<p>1. The <strong>matrix</strong> (<code>hermes-desktop-parity.json</code>) is the <em>only</em> source of<br />   truth for &quot;which Hermes feature exists in Aiopta, where, and at what<br />   coverage level&quot;. Each row binds an upstream feature to an Aiopta<br />   contract (capability + route + skill + UI + test) and tags it<br />   <code>full | partial | missing | unsupported | different</code>. Rows tagged<br />   with <code>group: &quot;settings&quot;</code> (issue #121) are rendered together in the<br />   settings panel.<br />2. The <strong>capability adapter</strong> (<code>src/lib/capability-adapter.ts</code>) reads<br />   the matrix and returns the <em>shell-verified</em> state (`not-selected |<br />   down | old | unsupported | full`) for every row, keyed by the<br />   active backend driver. The cache invalidates on backend switch —<br />   there are no optimistic &quot;ready&quot; badges.<br />3. The <strong>settings adapter</strong> (<code>src/lib/settings-adapter.ts</code>) reads the<br />   matrix rows whose <code>group === &quot;settings&quot;</code> and returns a per-row<br />   settings verdict (<code>supported | unsupported | unreachable</code>) plus the<br />   last-refreshed timestamp and the stable error code. The verdict is<br />   keyed by <code>(backend, live-state-hash)</code> so a backend switch OR a<br />   change in the live Aiopta configuration source forces a fresh<br />   evaluation.<br />4. The <strong>shell</strong> (<code>src/components/HermesDesktopPage.tsx</code>) renders only<br />   what the capability adapter actually returned. The settings panel<br />   (<code>src/components/SettingsPanel.tsx</code>) renders only what the settings<br />   adapter returned. Missing/unsupported/down/unreachable rows are<br />   labelled honestly. The user never sees &quot;voice input: ready&quot; when<br />   the upstream contract is <code>missing</code>, nor &quot;memory: supported&quot; when<br />   the memories addon is unreachable.</p>\n<h2>Files</h2>\n<p>| File | Purpose |<br />|------|---------|<br />| <code>hermes-desktop.pin.json</code> | Frozen commit SHA + spec/parity issue refs |<br />| <code>hermes-desktop-parity.json</code> | 20-row feature matrix (validated by <code>validate-addons.cjs</code>) |<br />| <code>hermes-desktop-parity.schema.json</code> | JSON Schema for the matrix (draft-07) |<br />| <code>addon.json</code> | Aiopta manifest (capability list, endpoints, page) |<br />| <code>src/manifest.ts</code> | Runtime extension manifest (ADR-001 dual-registration) |<br />| <code>src/page.ts</code> | Page descriptor consumed by the registry generator |<br />| <code>src/components/HermesDesktopPage.tsx</code> | Shell — renders the capability matrix + the settings panel + the voice panel |<br />| <code>src/components/SettingsPanel.tsx</code> | Settings panel — model / personality / skills / memory / automation (issue #121) |<br />| <code>src/components/VoicePanel.tsx</code> | Voice input/output panel — explicit opt-in, secret-free, retention hint, audio size limit (issue #168) |<br />| <code>src/app/api/v1/addons/hermes-desktop/capabilities/route.ts</code> | Capability-adapter endpoint (secret-free, authenticated) |<br />| <code>src/app/api/v1/addons/hermes-desktop/settings/route.ts</code> | Settings-adapter endpoint (secret-free, authenticated, issue #121) |<br />| <code>src/app/api/v1/addons/hermes-desktop/voice/route.ts</code> | Voice-panel endpoint (secret-free, authenticated, issue #168): per-row voice verdict + retention hint |<br />| <code>src/lib/matrix.ts</code> | Matrix loader + validator (pure) — accepts the optional <code>group</code> field (issue #121) |<br />| <code>src/lib/capability-adapter.ts</code> | Capability-adapter logic — backend-aware, cache-aware, secret-free (pure) |<br />| <code>src/lib/settings-adapter.ts</code> | Settings-adapter logic — backend- + live-state-aware, cache-aware, secret-free (pure, issue #121) |<br />| <code>src/lib/voice-adapter.ts</code> | Voice-adapter logic — backend- + live-snapshot-aware, consent-aware, cache-aware, secret-free (pure, issue #168) |<br />| <code>src/lib/voice-policy.ts</code> | Voice policy — consent state machine, audio size limit, log redaction, provider status (pure, issue #168) |<br />| <code>src/lib/shell-state.ts</code> | Capability shell-state computation: backend state × matrix row → <code>not-selected|down|old|unsupported|full</code> |<br />| <code>src/lib/settings-shell-state.ts</code> | Settings shell-state computation: backend state × matrix row → <code>supported|unsupported|unreachable</code> (pure, issue #121) |<br />| <code>src/lib/session-source.ts</code> | Sessions + stream contract (issue #120): per-user row, SSE chunk shape, id validator, capability-driven <code>canStop</code> / <code>canShowRetry</code> |<br />| <code>src/lib/stream-buffer.ts</code> | SSE chunk dedupe + reconnect helper (issue #120): <code>StreamBuffer</code> (hard-floor dedupe at <code>highestSeq</code>), bounded exponential backoff with jitter, terminal-flag-aware reconnect |<br />| <code>src/lib/session-page-state.ts</code> | Page state machine (issue #120): <code>loading | down | ready | stream | closed</code>, capability-driven gates, backend-switch invalidation |<br />| <code>src/lib/sessions-fixture.ts</code> | Per-user sessions fixture + cursor helper (issue #120): shape validator + <code>selectUserSessions</code> / <code>findSessionForUser</code> / <code>nextChunk</code> |<br />| <code>src/app/api/v1/addons/hermes-desktop/sessions/route.ts</code> | <code>GET /sessions</code> (issue #120): per-user slice + capability-flagged <code>canStop</code> / <code>canRetry</code> |<br />| <code>src/app/api/v1/addons/hermes-desktop/sessions/[id]/stream/route.ts</code> | <code>GET /sessions/{id}/stream</code> (issue #120): SSE responder, advances cursor based on <code>Last-Event-ID</code> |<br />| <code>src/app/api/v1/addons/hermes-desktop/sessions/[id]/stop/route.ts</code> | <code>POST /sessions/{id}/stop</code> (issue #120): capability-gated, idempotent on already-stopped |<br />| <code>src/app/api/v1/addons/hermes-desktop/sessions/[id]/retry/route.ts</code> | <code>POST /sessions/{id}/retry</code> (issue #120): capability-gated, idempotent on already-streaming |<br />| <code>src/components/HermesSessionsPage.tsx</code> | Sessions UI (issue #120): per-user list + stream pane + capability-gated Stop / Retry + backend-switch cleanup |<br />| <code>src/page-sessions.ts</code> | Page descriptor for the sessions UI (ADR-001 dual-registration) |<br />| <code>src/lib/workspace-fs.ts</code> | Per-user workspace file browser + sandboxed preview envelope (issue #167): <code>FsAdapter</code>, <code>normalizeRelativePath</code>, <code>resolveSafePath</code>, <code>listDirectory</code>, <code>readPreview</code>, <code>sandboxFor</code>, <code>cspFor</code>. Pure — every fs call goes through the caller-supplied adapter so the unit tests pass an in-memory <code>Map</code> |<br />| <code>src/app/api/v1/addons/hermes-desktop/files/route.ts</code> | <code>GET /files</code> (issue #167): per-user workspace listing, capability-gated, secret-free, no <code>file://</code> URL |<br />| <code>src/app/api/v1/addons/hermes-desktop/preview/route.ts</code> | <code>GET /preview</code> (issue #167): per-user sandboxed preview envelope, capability-gated, secret-free, no <code>file://</code> URL, active content sandboxed (no <code>allow-same-origin</code>) |<br />| <code>src/components/HermesWorkspacePage.tsx</code> | Workspace UI (issue #167): file tree + side-by-side preview pane, capability-gated, sandboxed iframe for active content |<br />| <code>src/page-workspace.ts</code> | Page descriptor for the workspace UI (ADR-001 dual-registration) |<br />| <code>src/lib/auth-fixture.ts</code> | Auth test fixtures (4 scenarios: authenticated-anon, missing-token, bad-signature, backend-switch) |<br />| <code>src/lib/timeout-fixture.ts</code> | Timeout test fixtures (DB / network &gt; 30s → degraded) |<br />| <code>src/lib/version-fixture.ts</code> | Version test fixtures (down / old / pinned-commit match) |<br />| <code>tests/unit/*.test.cjs</code> | Legacy <code>node --test</code> suite covering the acceptance criteria |<br />| <code>tests/unit/*.test.ts</code> | Vitest suite covering the acceptance criteria (mirrors the <code>.test.cjs</code> files) |</p>\n<h2>Acceptance criteria → evidence</h2>\n<p>| AC | Test |<br />|----|------|<br />| Matrix pro Upstreamfeature: Commitpfad, Aiopta-Vertrag, <code>full|partial|missing|unsupported</code>, Test/Gap | <code>tests/unit/matrix.test.cjs</code> validates every row&#39;s shape; <code>tests/unit/manifest.test.cjs</code> asserts the matrix rows match the manifest contract |<br />| Adapter liefert authenticated, secret-free status und invalidiert bei Backendwechsel | <code>tests/unit/capability-adapter.test.cjs</code> — happy path + 4 auth-fixture scenarios + backend-switch invalidation |<br />| Shell zeigt not-selected/down/old/unsupported; keine optimistischen Flags | <code>tests/unit/shell-state.test.cjs</code> — every state transition is asserted against the matrix + backend state tuple; no test ever passes when a <code>down</code> row renders as <code>full</code> |<br />| Manifest-/Adapter-/Auth-/Timeout-/Versionfixtures und <code>validate:addons</code> grün | <code>scripts/validate-addons.sh</code> (exits 0) + the four fixture files plus their dedicated unit tests |<br />| <strong>Issue #120:</strong> Nur eigene Sessions sichtbar | <code>tests/unit/sessions-route.test.cjs</code> (Alice sees her sessions, Bob sees his, cross-user probe returns same shape as missing) + <code>tests/unit/sessions-fixture.test.cjs</code> (<code>selectUserSessions</code> + <code>findSessionForUser</code>) |<br />| <strong>Issue #120:</strong> Stop/Retry folgt Capability-Flags | <code>tests/unit/sessions-route.test.cjs</code> (capability flags threaded per row) + <code>tests/unit/stop-route.test.cjs</code> + <code>tests/unit/retry-route.test.cjs</code> (501 on unsupported/missing, 200 on full/partial, 409 on invalid state) + <code>tests/unit/session-page-state.test.cjs</code> (<code>canStop</code>/<code>canRetry</code> gated by verdict) |<br />| <strong>Issue #120:</strong> Streaming dupliziert keine Chunks | <code>tests/unit/stream-buffer.test.cjs</code> (hard-floor dedupe at <code>highestSeq</code> + recent-set dedupe + bounded eviction) + <code>tests/unit/stream-route.test.cjs</code> (<code>Last-Event-ID</code> cursor + SSE <code>id:</code> line) |<br />| <strong>Issue #120:</strong> Addon deaktivieren entfernt Navigation und beendet Polling/Streams | <code>src/components/HermesSessionsPage.tsx</code> <code>useEffect</code> cleanup closes the <code>EventSource</code> + clears the reconnect timer on unmount |<br />| <strong>Issue #120:</strong> Hermes down → retrybarer degraded state | <code>tests/unit/session-page-state.test.cjs</code> (<code>down</code> state + <code>RETRY</code> transition) + <code>tests/unit/sessions-route.test.cjs</code> (BACKEND_DOWN → 503 typed error) |<br />| <strong>Issue #120:</strong> Streamabbruch → reconnect oder klar beendet | <code>tests/unit/stream-buffer.test.cjs</code> (<code>shouldReconnect</code> honours terminal flag + attempt cap; <code>selectReconnectBackoff</code> is bounded at 8s with half-jitter) + <code>HermesSessionsPage</code> reconnects with <code>?resume=&lt;nextSeq&gt;</code> |<br />| <strong>Issue #120:</strong> Backendwechsel beendet Subscriptions und leert stale Sessions | <code>tests/unit/session-page-state.test.cjs</code> (<code>BACKEND_SWITCH</code> transitions from ready/stream/down to closed) + <code>HermesSessionsPage</code> backend-switch watcher |<br />| <strong>Issue #167:</strong> Capability-gated Files/Previews mit per-User Workspace-root | <code>tests/unit/workspace-fs.test.cjs</code> (per-user root, workspace-root cap, <code>scopedStore</code>-ready) + <code>tests/unit/files-route.test.cjs</code> (per-user <code>resolveWorkspaceRootForUser</code> + 401 on missing/bad signature) + <code>tests/unit/preview-route.test.cjs</code> (per-user envelope) |<br />| <strong>Issue #167:</strong> Path traversal, symlink escape, oversized/binary/unsupported sauber behandelt | <code>tests/unit/workspace-fs.test.cjs</code> (TRAVERSAL via <code>..</code>, <code>\\\\</code>, NUL, symlink escape via <code>resolveSafePath</code> realpath check, OVERSIZED via <code>maxBytes + 1</code> round-trip, BINARY via extension + NUL-byte sniff, UNSUPPORTED via non-whitelisted mime) + <code>tests/unit/files-route.test.cjs</code> (every code → status mapping) |<br />| <strong>Issue #167:</strong> Preview ist sandboxed; aktiver Inhalt erhält keine Core-Origin-Rechte | <code>tests/unit/workspace-fs.test.cjs</code> (<code>sandboxFor</code> returns <code>allow-popups</code> for text/html + application/pdf, NEVER <code>allow-same-origin</code>; <code>cspFor</code> returns <code>default-src &#39;none&#39;</code>, NEVER <code>script-src</code>) + <code>tests/unit/preview-route.test.cjs</code> (envelope carries <code>sandbox</code> + <code>csp</code> for every mime, response is secret-free) |<br />| <strong>Issue #167:</strong> Adapter verwendet ausschließlich Aiopta Workspace-/Artefakt-APIs; keine freien Hostpfade oder <code>file://</code>-URLs | <code>tests/unit/workspace-fs.test.cjs</code> (response shape is secret-free, listing entries are RELATIVE paths, envelope never carries <code>file://</code>) + <code>tests/unit/preview-route.test.cjs</code> (response body never contains <code>file://</code> or the absolute path) |<br />| <strong>Issue #167:</strong> FS/AuthZ/preview sandbox Matrix und zwei User | <code>tests/unit/two-user-isolation.test.cjs</code> (user-a + user-b see different content, path-traversal blocked, symlink to other user blocked as <code>SYMLINK_ESCAPE</code>, error body never carries another user&#39;s file content) |<br />| Issue #121: Jedes Matrixfeature hat UI-Zustand supported/unsupported/unreachable | <code>tests/unit/settings-shell-state.test.cjs</code> — full truth table + &quot;no optimistic flags&quot; guard |<br />| Issue #121: Modelloptionen stammen aus Aiopta-Konfiguration | <code>tests/unit/settings-adapter.test.cjs</code> — source reference binds to Aiopta routes (<code>SETTINGS_SOURCE_BINDINGS</code>) |<br />| Issue #121: Memory/Automation zeigen Aktualität und Fehlerzustand | <code>tests/unit/settings-adapter.test.cjs</code> — <code>lastRefreshedAt</code> + <code>errorCode</code> surfaces, plus <code>tests/unit/settings-route.test.cjs</code> |<br />| Issue #121: Keine internen Hosts/Secrets im Browserpayload | <code>tests/unit/settings-adapter.test.cjs</code> — JSON-stringified verdict scan forbids <code>token</code>, <code>password</code>, <code>secret</code>, <code>apikey</code>, <code>bearer</code>, <code>credential</code>, <code>http://</code>, <code>https://</code>, <code>localhost</code>, <code>127.0.0.1</code>, <code>0.0.0.0</code> |<br />| Issue #121: Capability-Matrix-Tests | <code>tests/unit/matrix.test.cjs</code> §&quot;issue #121 — settings-group rows&quot; pins every settings-group row |<br />| Issue #121: Komponententests für supported/unsupported/down | <code>tests/unit/settings-shell-state.test.cjs</code> (every state × every backend) |<br />| Issue #121: E2E Backendwechsel Hermes → anderer Provider | <code>tests/unit/settings-backend-switch.test.cjs</code> — hermes → openclaw with mixed live-state per row |</p>\n<h2>What is explicitly out of scope</h2>\n<p>- <strong>Upstream self-update</strong> is <code>unsupported</code> (by design). The addon never<br />  advertises &quot;update available&quot; — there is no Hermes-desktop self-update<br />  surface. Updates flow through the normal Aiopta Marketplace ZIP-artifact<br />  release pipeline.<br />- <strong>Hermes desktop app</strong> itself is not in this repo. The matrix binds<br />  upstream features to Aiopta contracts; it does not re-implement the<br />  Hermes client.<br />- <strong>DB schema</strong> — this addon ships no <code>migrations/</code> directory. All state<br />  lives in the matrix JSON + the request-scoped adapter cache + the<br />  per-user sessions fixture (issue #120). No <code>scopedStore</code>, no per-user<br />  table, no FK.<br />- <strong>Browser → Hermes direct</strong> (issue #120 neg-scope). The browser never<br />  talks to Hermes directly; every session / stream / stop / retry call<br />  flows through the Aiopta <code>userRoute</code>-wrapped handler in this addon.<br />- <strong>Cross-user session access</strong> (issue #120 neg-scope). The sessions<br />  list is filtered by <code>auth.id</code>. A cross-user probe returns the same<br />  answer as a missing id — no enumeration leak.<br />- <strong>Setting values</strong> (issue #121 neg-scope). The settings verdict<br />  surfaces the Aiopta route that owns the value + recency + error code.<br />  It never returns the value of a setting (model id, API key,<br />  personality content, memory content, automation payload). The user<br />  clicks &quot;Manage in Aiopta&quot; to reach the surface that owns the value.</p>\n<h2>Run it locally</h2>\n<pre><code class=\"lang-bash\">npm run validate:addons        # addon schema + matrix pin guard\nnode --test addons/hermes-desktop/tests/unit/*.test.cjs</code></pre>\n<p>The Jenkins PR-check pipeline runs both as part of <code>ci:build</code>.</p>",
          "byteSize": 20743
        },
        "changelog": {
          "filename": "CHANGELOG.md",
          "sourcePath": "CHANGELOG.md",
          "rendered": "<h1>Hermes Desktop Changelog (issues #101 + #120 + #169 + #121 + #167)</h1>\n<p>All notable changes to the <code>hermes-desktop</code> addon are documented here.<br />This addon follows <a href=\"https://semver.org/\" rel=\"noopener noreferrer\">Semantic Versioning</a>.</p>\n<h2>0.3.0 — 2026-06-30 (issue #167)</h2>\n<h3>Added</h3>\n<p>- <strong>Per-user workspace file browser</strong> — `GET<br />  /api/v1/addons/hermes-desktop/files?path=&lt;relative&gt;` returns a<br />  listing of the calling user&#39;s workspace root only. The endpoint<br />  is per-user (scopedStore(userId).workspaces), capability-gated<br />  (<code>addons/hermes-desktop/files</code>), and secret-free (the response<br />  never carries a token, bearer, or credential; the listing<br />  entries are RELATIVE paths only).<br />- <strong>Per-user sandboxed preview</strong> — `GET<br />  /api/v1/addons/hermes-desktop/preview?path=&lt;relative&gt;` returns<br />  a <code>PreviewEnvelope</code> (<code>mime</code>, <code>content</code>, <code>encoding</code>, <code>size</code>,<br />  <code>sandbox</code>, <code>csp</code>, <code>truncated</code>). HTML / PDF previews carry<br />  <code>sandbox=&quot;allow-popups&quot;</code> (NEVER <code>allow-same-origin</code>, NEVER<br />  <code>allow-scripts</code>) and a CSP that defaults to <code>default-src &#39;none&#39;</code><br />  — active content cannot reach the core origin. PDF bodies are<br />  base64-encoded; text / markdown / JSON / HTML bodies are<br />  UTF-8. The shell renders the envelope via `&lt;iframe sandbox<br />  srcdoc=...&gt;<code> or </code>&lt;pre&gt;<code> for text. There is no </code>file://` URL<br />  and no path the shell could follow.<br />- <strong>Path-traversal / symlink-escape / binary / oversize guards</strong><br />  — implemented in <code>src/lib/workspace-fs.ts</code>. Every filesystem<br />  call goes through a caller-supplied <code>FsAdapter</code> so the unit<br />  tests pass an in-memory <code>Map</code>-backed mock; the route handler<br />  passes <code>node:fs/promises</code>. The route handler maps the<br />  <code>WorkspaceFsError.code</code> to a stable HTTP status: 400 (TRAVERSAL<br />  / ABSOLUTE_PATH / NOT_A_DIRECTORY), 404 (NOT_FOUND /<br />  SYMLINK_ESCAPE), 413 (OVERSIZED), 415 (BINARY / UNSUPPORTED).<br />- <strong>Matrix rows</strong> — <code>feat-side-by-side-previews</code> and<br />  <code>feat-file-browser</code> move from <code>missing</code> to <code>partial</code> (issue<br />  #167 closes the upstream follow-up; the Aiopta side is<br />  capability-gated and secret-free). Two new contract rows are<br />  added: <code>feat-preview-sandbox</code> (full) and <code>feat-no-file-url</code><br />  (full).<br />- <strong>Workspace page + capability flags</strong> — <code>HermesWorkspacePage</code><br />  composes the file tree + a side-by-side preview pane. New<br />  capabilities: <code>hermes-workspace-files</code>, <code>hermes-workspace-preview</code>,<br />  <code>preview-sandboxed</code>, <code>no-file-url</code>. New skill:<br />  <code>hermes-desktop-workspace</code> (permissions for both routes; the<br />  agent must have both before calling <code>/preview</code>).</p>\n<h3>Tests</h3>\n<p>- <code>tests/unit/workspace-fs.test.cjs</code> — 69 tests covering the pure<br />  module: path normalisation, symlink escape, binary detection,<br />  mime classification, sandbox/CSP contract, secret-free response<br />  shape.<br />- <code>tests/unit/files-route.test.cjs</code> — 24 tests covering the route<br />  handler: auth gate, happy path, capability gating, error code<br />  → status mapping.<br />- <code>tests/unit/preview-route.test.cjs</code> — 22 tests covering the<br />  preview route: envelope shape, sandbox contract, no-file-url<br />  contract, per-user workspace, security guards.<br />- <code>tests/unit/two-user-isolation.test.cjs</code> — 8 tests covering<br />  the Pflicht-Test &quot;Cross-User-Zugriff: 404 statt 403&quot; for both<br />  routes (file browser + preview).</p>\n<h2>0.2.0 — 2026-06-30 (issues #120 + #121 + #169)</h2>\n<h3>Added</h3>\n<p>- <strong>Per-user Hermes sessions UI</strong> — a new page at<br />  <code>/addons/hermes-desktop/sessions</code> that lists the calling user&#39;s<br />  Hermes sessions, opens an SSE stream, and offers Stop / Retry<br />  only when the capability adapter reports <code>feat-chat-streaming</code><br />  as <code>full</code> or <code>partial</code>.<br />- <strong>Per-user sessions API</strong> — `GET<br />  /api/v1/addons/hermes-desktop/sessions` returns the user&#39;s slice<br />  only. Cross-user probes are merged with missing-id responses so a<br />  caller cannot enumerate session ids by HTTP status (issue #120<br />  AC #1: &quot;Nur eigene Sessions sichtbar&quot;).<br />- <strong>Capability-gated SSE stream</strong> — `GET<br />  /api/v1/addons/hermes-desktop/sessions/{id}/stream` emits one<br />  chunk per request. The browser dedupes by sequence id; the route<br />  advances the cursor based on <code>Last-Event-ID</code> so a reconnect<br />  resumes from the next chunk (issue #120 AC #3: &quot;Streaming<br />  dupliziert keine Chunks&quot;). The browser&#39;s <code>StreamBuffer</code> adds a<br />  hard floor at <code>highestSeq</code> so a buggy backend cannot trick the UI<br />  into rendering an old chunk.<br />- <strong>Capability-gated Stop / Retry</strong> — `POST<br />  /api/v1/addons/hermes-desktop/sessions/{id}/stop<code> and </code>POST<br />  /api/v1/addons/hermes-desktop/sessions/{id}/retry` are gated by<br />  the capability verdict for <code>feat-chat-streaming</code> (issue #120<br />  AC #2: &quot;Stop/Retry folgt Capability-Flags&quot;). <code>unsupported</code> /<br />  missing row → 501. Already-stopped → 200 (idempotent). Invalid<br />  state → 409.<br />- <strong>Pinned upstream + image-compatibility contract</strong> (issue #169):<br />  <code>hermes-desktop.pin.json</code> now stores, displays and validates<br />  together with the source commit:<br />  - <code>image</code>              — OCI registry/repository reference<br />  - <code>imageDigest</code>        — sha256-pinned image digest<br />                          (<code>^sha256:[a-f0-9]{64}$</code>, no <code>latest</code>)<br />  - <code>apiSchemaVersion</code>   — pinned Semver of the upstream API contract<br />  - <code>license</code>            — upstream license string (now mandatory +<br />                          hard-validated against the canonical value)<br />  - <code>compatibilityIssue</code> — reference to issue #169 (this entry)<br />- <code>hermes-desktop-parity.json</code> mirrors every contract field in its<br />  <code>pin</code> block; the matrix JSON Schema (<code>hermes-desktop-parity.schema.json</code>)<br />  requires the new fields.<br />- <code>src/lib/pin.ts</code> — pure pin loader/validator + <code>compareSemver</code> +<br />  <code>classifyApiSchemaCompatibility</code> for the new API-schema drift<br />  verdict (<code>compatible | older | newer | unknown</code>).<br />- Adapter verdict (<code>src/lib/capability-adapter.ts</code>) carries the<br />  full pinned contract: <code>pinImage</code>, <code>pinImageDigest</code>,<br />  <code>pinApiSchemaVersion</code>, <code>pinLicense</code>, <code>compatibilityIssue</code>,<br />  <code>apiSchemaCompatible</code>, <code>backendApiSchemaVersion</code>. Each<br />  <code>VerdictRow</code> carries the per-evaluation <code>imageDigestOk</code> and<br />  <code>apiSchemaVersionOk</code> columns so the shell can render a precise<br />  reason without re-running the adapter.<br />- New shell state <code>&#39;unsupported-version&#39;</code> (issue #169 AC #2): API<br />  schema drift on a <code>full</code> row surfaces <code>unsupported-version</code>,<br />  distinct from <code>down</code> (image-digest mismatch / backend unreach-<br />  able) and <code>old</code> (commit drift).<br />- <code>BackendState</code> carries new mandatory fields <code>imageDigestOk</code> and<br />  <code>apiSchemaVersionOk</code> (defaults to <code>false</code> — no optimistic<br />  default).<br />- <code>src/app/api/v1/addons/hermes-desktop/capabilities/route.ts</code><br />  reads the backend&#39;s reported API schema version from a<br />  deterministic file (<code>.backend-api-schema.json</code>) and surfaces the<br />  full pinned contract under <code>data.pinContract</code> in the response.<br />- Validator (<code>scripts/validate-addons.cjs</code>):<br />  <code>validateHermesDesktopPin</code> hard-validates every contract field<br />  (image + imageDigest + apiSchemaVersion + license +<br />  compatibilityIssue). <code>validateHermesMatrix</code> hard-fails on any<br />  drift between the matrix pin block and the pin JSON.<br />- New unit test <code>tests/unit/pin.test.cjs</code> (issue #169 contract):<br />  every required field is present + drift surfaces a typed<br />  <code>PinLoadError</code> with a precise <code>field</code> pointer.<br />- Existing tests extended:<br />  <code>tests/unit/capability-adapter.test.cjs</code> covers the new<br />  <code>imageDigestOk</code> / <code>apiSchemaVersionOk</code> semantics + the<br />  <code>apiSchemaCompatible</code> verdict.<br />  <code>scripts/validate-addons.test.cjs</code> covers every drift scenario<br />  for the new pin fields.<br />- <strong>Backend-switch cleanup</strong> — when the verdict&#39;s <code>backend</code> id<br />  changes between refreshes the page tears down every open<br />  EventSource + clears the session list (issue #120 edge-case:<br />  &quot;Backendwechsel beendet Subscriptions und leert stale<br />  Sessions&quot;).<br />- <strong>Page state machine</strong> — <code>src/lib/session-page-state.ts</code><br />  implements a typed reducer over `loading | down | ready | stream<br />  | closed<code> with transitions for </code>LOADED<code>, </code>LOAD_FAILED`,<br />  <code>SELECT_SESSION</code>, <code>CHUNK_RENDERED</code>, <code>STREAM_TERMINAL</code>, <code>STOP</code>,<br />  <code>RETRY</code>, <code>BACKEND_SWITCH</code>, <code>AUTH_LOST</code>. Pure, fully unit-tested.<br />- <strong>Sessions fixture</strong> — a git-ignored<br />  <code>addons/hermes-desktop/.sessions-fixture.json</code> (local test seam)<br />  holds the per-user session slices + stream chunks the route<br />  handlers consult. The shape is validated by <code>loadSessionsFixture</code><br />  so a malformed fixture fails the route handler with 503, never<br />  silently downgrades to an empty list.<br />- <strong>New skill</strong> — <code>hermes-desktop-sessions</code> exposes the per-user<br />  sessions endpoint to Aiopta&#39;s agent layer. Secret-free by<br />  construction (issue #101 invariant preserved).<br />- <strong>Settings panel</strong> (model / personality / skills / memory /<br />  automation-status) backed by a new endpoint and a new shell-state<br />  vocabulary (issue #121). The matrix is extended with an optional<br />  <code>group</code> field (currently the only allowed value is <code>&quot;settings&quot;</code>);<br />  the five settings-group rows are tagged accordingly.<br />- <code>src/lib/settings-shell-state.ts</code> — pure shell-state computation<br />  that maps a backend state × matrix row to a discrete state<br />  (<code>supported | unsupported | unreachable</code>). The verdict is <em>never</em><br />  optimistic: a <code>missing</code> matrix row stays <code>unsupported</code> regardless of<br />  live-source health, and a <code>full</code>/<code>partial</code>/<code>different</code> row on an<br />  unreachable / erroring / never-observed source renders as<br />  <code>unreachable</code>.<br />- <code>src/lib/settings-adapter.ts</code> — pure, secret-free adapter. Reads the<br />  matrix once at construction, filters rows whose `group ===<br />  &quot;settings&quot;`, and returns a per-row settings verdict keyed by<br />  <code>(backend, live-state-hash)</code>. The verdict is whitelist-serialized<br />  by <code>serializeSettingsSecretFree</code> so a future contributor adding a<br />  new field cannot leak a credential, an internal host, a base URL,<br />  or a setting value. Stable error code namespace:<br />  <code>SOURCE_UNREACHABLE</code>, <code>SOURCE_ERRORED</code>, <code>SOURCE_NEVER_OBSERVED</code>.<br />- <code>src/components/SettingsPanel.tsx</code> — shell page that renders the<br />  per-row settings verdict with the discrete shell state + last-<br />  refreshed timestamp + stable error code + Aiopta route reference.<br />- <code>src/app/api/v1/addons/hermes-desktop/settings/route.ts</code> —<br />  authenticated, secret-free settings endpoint. Reads the live per-row<br />  state from <code>addons/hermes-desktop/.settings-state.json</code> and falls<br />  back to <code>emptySettingsBackendState()</code> when the file is missing.<br />  Hard 30s timeout on the adapter evaluation.<br />- Updated <code>src/manifest.ts</code> to register the new <code>settings</code> route + the<br />  new skill (alongside the existing sessions routes from issue #120)<br />  and the bumped version (<code>0.2.0</code>).<br />- Updated <code>addon.json</code> to register the new endpoint + skill +<br />  capabilities (<code>settings-panel</code>, <code>settings-shell-state</code>) in addition<br />  to the existing sessions endpoints/capabilities (issue #120).<br />- Updated <code>hermes-desktop-parity.schema.json</code> to accept the optional<br />  <code>group</code> field.<br />- New unit tests (issue #121):<br />  <code>settings-shell-state.test.cjs</code> (full truth table + no-optimistic-<br />  flags guard), <code>settings-adapter.test.cjs</code> (happy path + secret-free<br />  invariant + cache invalidation on backend switch + cache<br />  invalidation on live-state change + cache TTL + error code mapping),<br />  <code>settings-route.test.cjs</code> (auth gate + happy path + secret-free<br />  response + per-row state mapping), <code>settings-backend-switch.test.cjs</code><br />  (E2E backend-switch test, hermes → openclaw).</p>\n<h3>Changed</h3>\n<p>- <code>hermes-desktop.pin.json</code> schema extended additively — every<br />  existing field is preserved; the four new fields are mandatory.<br />- <code>hermes-desktop-parity.json</code> <code>pin</code> block extended additively with<br />  the four contract fields + <code>compatibilityIssue</code>.<br />- <code>src/lib/shell-state.ts</code> truth-table now distinguishes image-<br />  digest mismatch (<code>down</code>) from API-schema drift on <code>full</code> rows<br />  (<code>unsupported-version</code>).<br />- <code>src/lib/matrix.ts</code> <code>validateMatrix</code> accepts the optional<br />  <code>expectedPin</code> argument and hard-fails on drift between matrix<br />  and pin JSON for every contract field.<br />- The 16-row matrix keeps all rows; the <code>image-pin</code> row status is<br />  unchanged (still <code>missing</code>) because the surface-level pin UI is<br />  out of scope for this issue. The data model + validator hardening<br />  IS the contract surface this issue ships.<br />- <code>addon.json</code> — version 0.2.0; capabilities extended with<br />  <code>image-digest-pinned</code>, <code>api-schema-version-pinned</code>,<br />  <code>supply-chain-checked</code> (issue #169) and<br />  <code>hermes-sessions</code>, <code>hermes-streaming</code>, <code>hermes-stream-stop</code>,<br />  <code>hermes-stream-retry</code> (issue #120); endpoint list extended with<br />  the four new routes; <code>pages[]</code> now also carries<br />  <code>hermes-desktop-sessions-page</code>.<br />- <code>src/manifest.ts</code> — registers the four new routes + the new<br />  page descriptor; <code>pages: [hermesDesktopPage, hermesSessionsPage]</code>.<br />- <code>hermes-desktop-parity.json</code> — <code>feat-chat-streaming</code> and<br />  <code>feat-sessions</code> rows now have <code>followUpIssue: null</code> (the UI<br />  surface shipped in #120). Status remains <code>partial</code> because the<br />  page is wired against a local fixture rather than a live chat<br />  backend driver — that live integration is a future follow-up.<br />- <code>feat-tool-activity</code> row still references #120 as the<br />  follow-up (the timeline is still routed through chat UI; this<br />  slice did not address it).</p>\n<h3>Internal</h3>\n<p>- New lib modules: <code>src/lib/session-source.ts</code> (typed contract),<br />  <code>src/lib/stream-buffer.ts</code> (dedupe + reconnect helper),<br />  <code>src/lib/session-page-state.ts</code> (page state reducer),<br />  <code>src/lib/sessions-fixture.ts</code> (fixture validator + helpers).<br />- New routes: <code>src/app/api/v1/addons/hermes-desktop/sessions/route.ts</code><br />  + <code>sessions/[id]/{stream,stop,retry}/route.ts</code>.<br />- New tests: <code>tests/unit/{stream-buffer,session-page-state,sessions-fixture,sessions-route,stream-route,stop-route,retry-route}.test.cjs</code><br />  + <code>tests/unit/_sessions-fixture-shared.cjs</code> (pure helper module<br />  shared by the route tests).<br />- <code>tests/unit/manifest.test.cjs</code> extended to assert the new<br />  capabilities, endpoints, skills, and pages[] entries.<br />- Path-param fix for <code>sessions/[id]/{stream,stop,retry}</code> route<br />  handlers: read <code>id</code> from <code>context.params.id</code> (awaited per Next.js<br />  15) instead of <code>req.nextUrl.searchParams</code> (the old code returned<br />  400 for every stream/stop/retry call).</p>\n<h3>Acceptance criteria (issue #121)</h3>\n<p>- ✅ Jedes Matrixfeature hat UI-Zustand supported/unsupported/unreachable —<br />  <code>settings-shell-state.test.cjs</code> pins every state × backend combination.<br />- ✅ Modelloptionen stammen aus Aiopta-Konfiguration — <code>SETTINGS_SOURCE_BINDINGS</code><br />  binds each matrix row to its Aiopta route; the verdict surfaces the<br />  reference, never the value.<br />- ✅ Memory/Automation zeigen Aktualität und Fehlerzustand — <code>lastRefreshedAt</code><br />  + <code>errorCode</code> are first-class verdict fields.<br />- ✅ Keine internen Hosts/Secrets im Browserpayload — <code>serializeSettingsSecretFree</code><br />  whitelist + JSON-stringified scan forbids <code>token</code>/<code>password</code>/<code>secret</code>/<br />  <code>apikey</code>/<code>bearer</code>/<code>credential</code>/<code>http://</code>/<code>https://</code>/<code>localhost</code>/<code>127.0.0.1</code>/<code>0.0.0.0</code>.<br />- ✅ Capability-Matrix-Tests — <code>matrix.test.cjs</code> §&quot;issue #121 — settings-group<br />  rows&quot; pins every settings-group row.<br />- ✅ Komponententests für supported/unsupported/down — <code>settings-shell-state.test.cjs</code>.<br />- ✅ E2E Backendwechsel Hermes → anderer Provider — <code>settings-backend-switch.test.cjs</code>.</p>\n<h2>0.1.0 — 2026-06-29 (issue #101)</h2>\n<h3>Added</h3>\n<p>- <code>hermes-desktop.pin.json</code> — commit-pinned reference to<br />  NousResearch <code>hermes-agent</code> <code>@ea8a8b4af8612b655a5bbfc74eba21e1e806758d</code>.<br />- <code>hermes-desktop-parity.json</code> — 16-row feature matrix binding every<br />  upstream Hermes desktop capability to an Aiopta contract (route +<br />  skill + UI + test) with a status of<br />  <code>full|partial|missing|unsupported|different</code>.<br />- <code>hermes-desktop-parity.schema.json</code> — JSON Schema (draft-07) for the<br />  matrix shape; the validator enforces the same shape at PR-check time.<br />- Capability adapter (<code>src/lib/capability-adapter.ts</code>) — pure, backend-<br />  aware, cache-aware, secret-free. Cache invalidates on backend switch.<br />- Shell state computation (<code>src/lib/shell-state.ts</code>) — maps a backend<br />  state × matrix row to a discrete shell state<br />  (<code>not-selected|down|old|unsupported|full</code>). No optimistic flags.<br />- Adapter endpoint (<code>src/app/api/v1/addons/hermes-desktop/capabilities</code>)<br />  — authenticated, secret-free. Returns status + references, never<br />  credentials.<br />- Shell page (<code>src/components/HermesDesktopPage.tsx</code>) — renders the<br />  matrix + the live verdict. Every cell carries the upstream commit<br />  ref + the follow-up issue (when status ≠ <code>full</code>).<br />- Auth / timeout / version fixtures (<code>src/lib/auth-fixture.ts</code>,<br />  <code>timeout-fixture.ts</code>, <code>version-fixture.ts</code>) — 4 + 3 + 3 scenarios<br />  each, driven by unit tests so a future change cannot silently weaken<br />  the contract.<br />- Validator hooks in <code>scripts/validate-addons.cjs</code>:<br />  <code>validateHermesDesktopPin</code> (commit pin equals canonical SHA) and<br />  <code>validateHermesMatrix</code> (16 unique rows, all upstream<br />  commits equal the pin, every <code>missing|different</code> row carries a<br />  numeric <code>followUpIssue</code>).<br />- Unit tests:<br />  <code>matrix.test.cjs</code>, <code>capability-adapter.test.cjs</code>,<br />  <code>shell-state.test.cjs</code>, <code>auth-fixture.test.cjs</code>,<br />  <code>timeout-fixture.test.cjs</code>, <code>version-fixture.test.cjs</code>,<br />  <code>capabilities-route.test.cjs</code>, <code>manifest.test.cjs</code>.</p>",
          "byteSize": 16961
        }
      }
    },
    {
      "id": "homeassistant",
      "uuid": "cba7bb26-7bbc-4718-b303-d7edfff5500d",
      "name": "Home Assistant",
      "version": "1.6.1",
      "description": "Monitor and control Home Assistant entities — lights, climate, sensors, switches, locks, automations, scenes and scripts via REST API. Curated automation & scene packs (formerly the standalone `smart-home` addon, consolidated per ADR-008 / #493) are a feature within Home Assistant. Includes an agent-skill-driven onboarding for the upstream `homeassistant-ai/ha-mcp` server (read-only by default).",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "entity-browser",
        "entity-control",
        "dashboard",
        "automation-trigger",
        "yaml-config-editor",
        "lovelace-dashboards",
        "helpers-management",
        "areas-management",
        "voice-assistant",
        "connection-config",
        "mcp-onboarding",
        "automation-scene-packs"
      ],
      "tags": [],
      "download": "/artifacts/homeassistant-1.6.1.zip",
      "sha256": "64bbde355d7f6a9d2ff39fffcd258fc3242c6c7ff208ebcd25b1f7cdc824f2a2",
      "size": 624214,
      "page": {
        "id": "homeassistant-page",
        "path": "/addons/homeassistant",
        "name": "Home Assistant",
        "icon": "🏠",
        "section": "Smart Home",
        "component": "@addons/homeassistant/src/components/HomeAssistantPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/homeassistant/status",
        "/api/v1/addons/homeassistant/entities",
        "/api/v1/addons/homeassistant/entities/:id",
        "/api/v1/addons/homeassistant/entities/:id/toggle",
        "/api/v1/addons/homeassistant/entities/:id/service",
        "/api/v1/addons/homeassistant/automations",
        "/api/v1/addons/homeassistant/automations/:id/trigger",
        "/api/v1/addons/homeassistant/automations/:id/yaml",
        "/api/v1/addons/homeassistant/automations/:id/update",
        "/api/v1/addons/homeassistant/scenes",
        "/api/v1/addons/homeassistant/scenes/:id",
        "/api/v1/addons/homeassistant/scenes/:id/yaml",
        "/api/v1/addons/homeassistant/scenes/:id/update",
        "/api/v1/addons/homeassistant/scenes/reload",
        "/api/v1/addons/homeassistant/scripts",
        "/api/v1/addons/homeassistant/scripts/:id",
        "/api/v1/addons/homeassistant/scripts/:id/yaml",
        "/api/v1/addons/homeassistant/scripts/:id/update",
        "/api/v1/addons/homeassistant/scripts/reload",
        "/api/v1/addons/homeassistant/lovelace/dashboards",
        "/api/v1/addons/homeassistant/lovelace/dashboards/:id",
        "/api/v1/addons/homeassistant/lovelace/dashboards/:id/config",
        "/api/v1/addons/homeassistant/helpers/:domain",
        "/api/v1/addons/homeassistant/helpers/:domain/:id",
        "/api/v1/addons/homeassistant/areas",
        "/api/v1/addons/homeassistant/areas/:id",
        "/api/v1/addons/homeassistant/assist",
        "/api/v1/addons/homeassistant/assist-config",
        "/api/v1/addons/homeassistant/config",
        "/api/v1/addons/homeassistant/test",
        "/api/v1/addons/homeassistant/services",
        "/api/v1/addons/homeassistant/services/:domain/:service",
        "/api/v1/addons/homeassistant/template",
        "/api/v1/addons/homeassistant/registry/entities",
        "/api/v1/addons/homeassistant/history"
      ],
      "skills": [],
      "mcpServers": [
        {
          "id": "homeassistant-tools",
          "displayName": "Home Assistant tools",
          "description": "Capability-gegateter MCP-Server fuer Home Assistant: Zustaende lesen, Dienste aufrufen, Automationen lesen/anlegen/aendern/loeschen. Jedes mutierende Tool laeuft ueber Preview/Confirm (addons#930); destruktives Loeschen verlangt einen wortgleichen Grant. Zugangsdaten kommen ueber die Slots url/token, nie als Tool-Argument.",
          "transport": "stdio",
          "runtime": "node",
          "entry": "homeassistant-tools/server.mjs",
          "permissions": [
            "addons/homeassistant/automation/delete",
            "addons/homeassistant/automation/read",
            "addons/homeassistant/automation/write",
            "addons/homeassistant/entity/call",
            "addons/homeassistant/entity/read"
          ],
          "toolPolicy": {
            "mode": "write",
            "allowDestructive": false
          },
          "configRefs": [
            "url"
          ],
          "credentialRefs": [
            "token"
          ],
          "archiveEntry": "homeassistant/mcp/servers/homeassistant-tools/server.mjs",
          "sha256": "52e4de460e205d7c7f1c80b2f8ac2cc2a668ea2e5840c9048fd9f88686da42da",
          "size": 14107,
          "descriptorVersion": 1
        }
      ],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Home Assistant</h1>\n<p>Monitor and control Home Assistant entities — lights, climate, sensors, switches, locks,<br />automations, scenes and scripts — via the HA REST API. Edit automation/scene/script **code as<br />YAML<strong>, manage </strong>Lovelace dashboards (Views + Cards), Helpers (<code>input_*</code>) and Areas**, and expose a<br />Aiopta agent as a Home Assistant <strong>voice assistant</strong>. Onboard the upstream<br /><code>homeassistant-ai/ha-mcp</code> MCP server so Aiopta agents can read HA over MCP.</p>\n<h2>Überblick</h2>\n<p>- <strong>Addon-ID:</strong> <code>homeassistant</code><br />- <strong>Version:</strong> 1.0.0<br />- <strong>Fähigkeiten:</strong> entity-browser, entity-control, dashboard, automation-trigger,<br />  yaml-config-editor (automation/scene/script), lovelace-dashboards, helpers-management,<br />  areas-management, voice-assistant (Aiopta agent as HA conversation agent), connection-config,<br />  mcp-onboarding (ha-mcp setup flow, addons#179)</p>\n<h2>Einrichtung</h2>\n<p>1. <strong>HA-Verbindung</strong> (Settings-Zahnrad oben rechts): Home-Assistant-URL +<br />   Long-Lived Access Token. Wird unter <code>data/homeassistant-config.json</code> gespeichert; **kein<br />   hartkodierter Host**, kein Secret im Repo.<br />2. <strong>Voice Assistant</strong> (optional, Tab „Voice&quot;): Aiopta-Agent auswählen, der HA-Assist beantwortet.<br />3. <strong>HA-MCP für Agenten</strong> (Abschnitt oben auf der HA-Seite, ab addons#179): das Upstream-Add-on<br />   <code>homeassistant-ai/ha-mcp</code> einbinden, damit MCP-fähige Agenten Home Assistant lesen können. Wird<br />   unter dem Per-User-Slug <code>home-assistant</code> in der MCP-Addon-Datenbank gespeichert (kein zweites<br />   Secret in <code>homeassistant-config.json</code>). Feature-gated auf das <code>mcp</code>-Addon (ADR-047) — ist das<br />   MCP-Addon nicht installiert oder deaktiviert, erscheint eine klare Installieren-/Aktivieren-<br />   Handlung; die klassische REST/WS-Verbindung bleibt unabhängig nutzbar.</p>\n<h2>YAML-Editor (Automationen / Szenen / Skripte) — QA #31</h2>\n<p>Für Automationen, Szenen und Skripte lässt sich der <strong>tatsächliche Code (YAML)</strong> bearbeiten — nicht<br />nur der Name.</p>\n<p>- Tab <strong>Automations</strong> → Button „Edit YAML&quot; (oder Tabs <strong>Scenes</strong> / <strong>Scripts</strong> → „YAML&quot;).<br />- Der Editor lädt das HA-Config-Objekt als YAML (<code>GET …/&lt;domain&gt;s/[id]/yaml</code>), prüft die Eingabe<br />  <strong>live mit <code>js-yaml</code></strong> (gültig/ungültig-Status) und speichert über <code>PUT …/&lt;domain&gt;s/[id]/update</code><br />  mit <code>{ yaml }</code>.<br />- <strong>Server-seitig</strong> wird das YAML vor dem Schreiben geparst + validiert (<code>validateYaml</code> in<br />  <code>src/lib/yaml.ts</code>); nur ein gültiges <strong>Config-Objekt</strong> geht an HA<br />  (<code>POST /api/config/&lt;domain&gt;/config/&lt;object_id&gt;</code>) — nie roher YAML-Text, nie ein leeres/<code>null</code>/<br />  Listen-/Skalar-Dokument (das würde den Eintrag zerstören).<br />- Domains sind auf <code>automation</code> / <code>scene</code> / <code>script</code> whitelisted; alle drei sind voll editierbar.</p>\n<h2>Automationen/Routinen lesen+schreiben (Edit-Routine) — INBOX-60</h2>\n<p>Die HA-Config-API (<code>/api/config/automation/config/&lt;id&gt;</code>) ist <strong>nicht</strong> über den Entity-Slug<br />(<code>automation.wohnzimmer_indirekt_ein</code>) ansprechbar, sondern nur über die <strong>echte <code>attributes.id</code></strong><br />(numerische/uuid <code>unique_id</code>) — und sie bedient <strong>nur <code>.storage</code>/UI-definierte</strong> Automationen.</p>\n<p>- <strong>id-Auflösung:</strong> Beim Lesen/Schreiben einer Automation wird zuerst die echte <code>attributes.id</code> aus<br />  dem Entity-State (<code>GET /api/states/automation.&lt;slug&gt;</code> → <code>attributes.id</code>) aufgelöst und <strong>diese</strong> an<br />  die Config-API gegeben (<code>src/lib/client.ts → resolveAutomationConfigId</code>). Das war die Ursache des<br />  404 / der leeren Trigger/Action im „Edit Routine&quot;-Dialog.<br />- <strong>YAML-Automationen (kein Config-Eintrag):</strong> Schlägt die Config-API mit <strong>404</strong> fehl (Automation<br />  liegt in <code>automations.yaml</code>), wird <strong>best-effort</strong> die <strong>WebSocket-API</strong> versucht; bietet HA die<br />  Config dort nicht an, kommt eine <strong>klare, typisierte Meldung</strong> zurück (<code>HaYamlOnlyError</code>, HTTP<br />  <strong>422</strong>, <code>{ readOnly:true, reason:&quot;yaml_defined&quot; }</code>) — „YAML-definiert, hier nur lesbar; in HA<br />  bearbeiten&quot;. <strong>Nie ein nacktes 404 in der UI</strong>; der Edit-Dialog zeigt entweder echte Trigger/Action<br />  oder einen klaren Grund.<br />- Lesen/Schreiben/Löschen einer Routine laufen über <code>src/lib/automations.ts</code><br />  (<code>readAutomationConfig</code> / <code>writeAutomationConfig</code> / <code>deleteAutomationConfig</code>); Enable/Disable/Trigger<br />  über <code>automation.turn_on|turn_off|trigger</code>.</p>\n<h2>Voll-Kontrolle (Agent-Fähigkeiten: lesen + schreiben)</h2>\n<p>Das Addon (und ein Aiopta-Agent als HA-Voice-/Routinen-Schreiber) deckt über die Aiopta-API **lesend und<br />schreibend** ab:</p>\n<p>- <strong>Entitäten/Sensoren:</strong> lesen (<code>GET /entities</code>, <code>/entities/[id]</code>), schalten/setzen<br />  (<code>POST /entities/[id]/toggle</code>, <code>/entities/[id]/service</code> → <code>/api/services/&lt;domain&gt;/&lt;service&gt;</code>).<br />- <strong>Automationen/Routinen:</strong> lesen + anlegen + ändern + aktivieren/deaktivieren + auslösen + löschen<br />  (id-Auflösung + WS-Fallback wie oben).<br />- <strong>Szenen / Skripte:</strong> lesen/anlegen/löschen/reload + YAML-Edit.<br />- <strong>Helpers (<code>input_*</code>) / Areas:</strong> lesen/anlegen/ändern/löschen (WS).<br />- <strong>Lovelace-Dashboards:</strong> Views/Karten lesen + speichern (No-Wipe-Garantie).</p>\n<h2>Dashboards / Helpers / Areas / Szenen+Skripte-Verwaltung — ADR-028</h2>\n<p>Aufbauend auf der YAML/Entity/Config-Basis (ADR-018) lassen sich weitere HA-Konfigurationsflächen<br />direkt aus Aiopta verwalten. <strong>Aiopta-API only</strong>; HA über den Addon-Client (URL+Token aus der<br />Addon-Config, <strong>Token wird nie geloggt</strong>).</p>\n<p>WS-only-Flächen laufen über einen kleinen <strong>global-<code>WebSocket</code>-Helper</strong> (<code>src/lib/ha-ws.ts</code>) —<br /><strong>keine neue Dependency</strong> (Node-22-Built-in): eine authentifizierte WS wird geöffnet<br />(<code>auth_required</code>→<code>auth</code>→<code>auth_ok</code>), <strong>ein</strong> Kommando gesendet, das Ergebnis abgewartet, dann<br />geschlossen. Wo HA REST anbietet (Szenen/Skripte-Config) wird REST bevorzugt.</p>\n<p>- <strong>Tab Dashboards</strong> — Lovelace-Dashboards auflisten/anlegen/löschen und die <strong>Views + Karten</strong> eines<br />  Dashboards als YAML bearbeiten (<code>lovelace/config</code> get, <code>lovelace/config/save</code> set,<br />  <code>lovelace/dashboards/list|create|delete</code>). Vor dem Speichern wird die <strong>Form geprüft</strong> (Objekt mit<br />  <code>views[]</code>) — eine ungültige/leere Konfiguration wird <strong>abgelehnt</strong>, damit ein Dashboard nie<br />  überschrieben/geleert wird (<code>validateLovelaceConfig</code> in <code>src/lib/yaml.ts</code>, client <strong>und</strong> server).<br />- <strong>Tab Helpers</strong> — <code>input_boolean</code>, <code>input_number</code>, <code>input_text</code>, <code>input_select</code>, <code>input_datetime</code>,<br />  <code>input_button</code>: auflisten/anlegen/ändern/löschen über die WS-Collections<br />  (<code>&lt;domain&gt;/list|create|update|delete</code>); nur diese Domains sind erlaubt.<br />- <strong>Tab Areas</strong> — Bereiche auflisten/anlegen/umbenennen/löschen über die Area-Registry<br />  (<code>config/area_registry/list|create|update|delete</code>).<br />- <strong>Szenen &amp; Skripte</strong> — der YAML-Editor (oben) deckt die Config ab; zusätzlich gibt es<br />  list/create/delete/reload über die HA-Config-API (<code>/api/config/&lt;domain&gt;/config/...</code>,<br />  <code>&lt;domain&gt;.reload</code>).</p>\n<p><strong>Geparkt</strong> (Owner/Folge-Arbeit, <code>docs/OPEN-DECISIONS.md</code>): Entitäten/Geräte einem Bereich<br />zuweisen (<code>ha-area-entity-assignment</code>), HA-Version-abhängige WS-Edge-Cases<br />(<code>ha-ws-auth-edge-cases</code>).</p>\n<p><code>ha-lovelace-visual-designer</code> ist <strong>nicht mehr geparkt</strong> — die Kartenbibliothek im<br />Dashboard-Reiter ist mit addons#932 gebaut.</p>\n<h2>HA-MCP Onboarding (ha-mcp für Agenten einrichten) — addons#179</h2>\n<p>Ein zweiter, <strong>vom klassischen REST-Pfad unabhängiger</strong> Onboarding-Weg bindet das Upstream-Add-on<br /><code>homeassistant-ai/ha-mcp</code> ein, damit MCP-fähige Agenten die HA-Instanz über MCP lesen können.<br />Standardmäßig <strong>read-only</strong> — destruktive Tools (<code>ha_config_set_yaml</code>, <code>ha_write_file</code>,<br /><code>ha_delete_file</code>, <code>ha_rename_entity</code>, rohe YAML / Filesystem / Delete) bleiben blockiert und<br />brauchen eine separate explizite Freigabe (siehe #178, <code>mcp-tool-policy.ts</code>).</p>\n<p>- <strong>UI:</strong> Abschnitt „Home Assistant MCP for agents&quot; oben auf der HA-Seite (<code>HaMcpSetupSection.tsx</code>).<br />  URL + optionales Bearer-Token + Default-<code>read-only</code>-Toggle, plus eine sichtbare State-Machine<br />  (<code>not configured → saved/disabled → connecting → tools discovered → ready</code> | konkreter Fehler).<br />  Der Bereich erscheint auch wenn die klassische REST-Verbindung nicht konfiguriert ist, damit<br />  reine ha-mcp-Nutzer ohne REST-Token onboarden können.<br />- <strong>Chat:</strong> Agent-Skill <code>homeassistant-ha-mcp-setup</code> (siehe <code>addon.json</code>). Deterministischer Dialog<br />  (URL → Token → read-only → Setup → Resultat), identische Validierung und Idempotenz wie die UI.<br />- <strong>Feature-Gating:</strong> der gesamte Bereich ist in <code>&lt;FeatureGate addonId=&quot;mcp&quot;&gt;</code> eingebettet (ADR-047).<br />  Ohne / mit deaktiviertem MCP-Addon erscheint der Standard-„needs the MCP addon — enable it&quot;-<br />  Panel mit Link auf den Addon-Manager; die REST-Verbindung bleibt nutzbar.<br />- <strong>Server-Storage:</strong> Per-User-Slug <code>home-assistant</code> in der <code>mcp_servers</code>-Tabelle des MCP-Addons<br />  (<code>addons/mcp/src/lib/mcp-server-store.ts</code>). <strong>Kein</strong> zweites HA-MCP-Secret in<br />  <code>homeassistant-config.json</code> — die direkte HA-Config bleibt unverändert.<br />- <strong>Secret-Handling:</strong> die zufällige Secret-Path-Komponente der ha-mcp-URL ist ein Credential. Sie<br />  wird per User gespeichert, aber <code>maskMcpServer</code> maskiert sie auf jeder GET-Antwort, sobald das<br />  Feld <code>metadata.credentialInUrl: true</code> gesetzt ist (Antwort zeigt nur Origin +<br />  <code>••••••</code>). Der Bearer-Token folgt dem bestehenden <code>auth.token → &quot;••••••&quot;</code>-Vertrag. Beide werden<br />  in UI / Chat / Logs / Fehlern nie im Klartext ausgegeben — <code>sanitiseHaMcpError</code> entfernt Bearer-<br />  Header und <code>/private_&lt;token&gt;</code>-Pfade aus jeder Fehlermeldung, bevor sie die Komponente oder den<br />  Chat erreicht.<br />- <strong>Idempotenz:</strong> Mehrfaches Speichern mit unveränderter URL/Token ist ein No-op auf dem Wire; ein<br />  PATCH mit leerem Token löscht kein bestehendes Token; eine vorhandene strengere Policy<br />  (z. B. manuelle <code>deniedTools</code>-Liste unter <code>read-only</code>) wird nicht gelockert; eine vorhandene<br />  weitere Policy (<code>write</code>/<code>full</code>) wird auf <code>read-only</code> zurückgesetzt.<br />- <strong>Identity-Check:</strong> nach <code>discover</code> und <code>sync-tools</code> prüft der Flow, dass mindestens einer der<br />  ha-mcp-Identitätsmarker (<code>ha_get_overview</code> ODER <code>ha_get_skill_guide</code>) im entdeckten Toolset<br />  vorhanden ist. Ein beliebiger fremder MCP-Server wird <strong>nicht</strong> still als Home Assistant<br />  aktiviert; bei fehlendem Marker bleibt der Server deaktiviert und der Retry ist möglich.<br />- <strong>Kein Fake-Green:</strong> der Server wird nur aktiviert, wenn Handshake + Identity-Check grün sind.<br />  Bei jedem Fehlschlag bleibt der Datensatz deaktiviert und retry-fähig.<br />- <strong>OAuth:</strong> im ersten Schritt nicht unterstützt — statischer Bearer-Token (bzw. der vom Upstream<br />  auf HA OS / Supervised vererbte Supervisor-Token) ist der einzige aktuelle Auth-Flow, bis #177<br />  explizit einen OAuth-Flow nachliefert.<br />- <strong>Video + Upstream:</strong> siehe Video <a href=\"https://youtu.be/AL391nkWGIc\" rel=\"noopener noreferrer\">https://youtu.be/AL391nkWGIc</a> und Upstream-Repository<br />  <a href=\"https://github.com/homeassistant-ai/ha-mcp.\" rel=\"noopener noreferrer\">https://github.com/homeassistant-ai/ha-mcp.</a> <strong>Kein</strong> Forken / Vendoren / eigenes Container-<br />  Image; Aiopta greift ausschließlich über die HA-Add-on-Logs auf die vom Nutzer bereitgestellte<br />  URL zu.</p>\n<p>Installationsschritte HA-Seitig (Owner-Aktion, automatisierbar in einer Folge-Story):</p>\n<p>1. Repository <code>https://github.com/homeassistant-ai/ha-mcp</code> im HA Add-on Store hinzufügen.<br />2. <strong>Home Assistant MCP Server</strong> installieren und starten.<br />3. Vollständige Endpoint-URL (inkl. Secret-Path) aus dem Add-on-Log kopieren.<br />4. In Aiopta im HA-Add-on-Bereich „Home Assistant MCP for agents&quot; einfügen und Setup starten.<br />5. Auf HA OS / Supervised ist <strong>kein</strong> zusätzlicher Long-Lived Token nötig — das Upstream-Add-on<br />   erkennt die Supervisor-Verbindung selbst.</p>\n<h2>Voice Assistant (Aiopta-Agent als HA-Conversation-Agent) — QA #32</h2>\n<p>Ein Mission-Control-Agent kann HA-Assist beantworten. Aiopta stellt den Endpunkt; HA zeigt<br />seinen Conversation-Agent darauf.</p>\n<p>- Tab <strong>Voice</strong>: Aiopta-Agent auswählen (aus <code>/api/v1/agents/my-agents</code>), Endpunkt-URL kopieren,<br />  Setup-Schritte ansehen. Auswahl wird als <code>HA_ASSIST_AGENT_ID</code> in der Addon-Config gespeichert.<br />- <strong>Endpunkt:</strong> <code>POST /api/v1/addons/homeassistant/assist</code> mit<br />  <code>{ text, conversation_id?, agentId? }</code>. Der Text wird über <code>@/lib/gateway</code> an den gewählten Agenten<br />  geroutet (dessen Soul + Modell-Kette + Fallbacks, Backend-Binding) — <strong>kein</strong> direkter Provider-Call.<br />- <strong>Antwort:</strong> HA-<code>conversation.process</code>-Form<br />  <code>{ response: { speech: { plain: { speech } }, … }, conversation_id }</code>. Fehler werden als<br />  HA-Error-Response zurückgegeben (Assist bleibt am Leben), nicht als HTTP-Fehler.<br />- <strong>HA-Seite = Owner-Konfiguration</strong> (parked, <code>ha-assist-agent-setup</code> in<br />  <code>docs/OPEN-DECISIONS.md</code>): in HA einen REST/Conversation-Agent auf den Endpunkt zeigen, mit einem<br />  <strong>Aiopta-Agent-API-Key</strong> als Bearer-Token authentifizieren, und in eine <strong>Assist-Pipeline</strong> einhängen.<br />  Bis dahin funktioniert die Aiopta-Seite vollständig; HA muss den Endpunkt nur erreichen.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET | <code>/api/v1/addons/homeassistant/automations</code> |<br />| GET | <code>/api/v1/addons/homeassistant/automations/[id]</code> |<br />| POST | <code>/api/v1/addons/homeassistant/automations/[id]/toggle</code> |<br />| POST | <code>/api/v1/addons/homeassistant/automations/[id]/trigger</code> |<br />| PUT | <code>/api/v1/addons/homeassistant/automations/[id]/update</code> |<br />| GET | <code>/api/v1/addons/homeassistant/automations/[id]/yaml</code> |<br />| GET | <code>/api/v1/addons/homeassistant/scenes/[id]/yaml</code> |<br />| PUT | <code>/api/v1/addons/homeassistant/scenes/[id]/update</code> |<br />| GET | <code>/api/v1/addons/homeassistant/scripts/[id]/yaml</code> |<br />| PUT | <code>/api/v1/addons/homeassistant/scripts/[id]/update</code> |<br />| POST | <code>/api/v1/addons/homeassistant/assist</code> |<br />| GET, PUT | <code>/api/v1/addons/homeassistant/assist-config</code> |<br />| GET, PUT | <code>/api/v1/addons/homeassistant/config</code> |<br />| GET | <code>/api/v1/addons/homeassistant/entities</code> |<br />| GET | <code>/api/v1/addons/homeassistant/entities/[id]</code> |<br />| POST | <code>/api/v1/addons/homeassistant/entities/[id]/service</code> |<br />| POST | <code>/api/v1/addons/homeassistant/entities/[id]/toggle</code> |<br />| GET, POST | <code>/api/v1/addons/homeassistant/routines</code> |<br />| GET, PUT, DELETE | <code>/api/v1/addons/homeassistant/routines/[id]</code> |<br />| GET, POST | <code>/api/v1/addons/homeassistant/lovelace/dashboards</code> |<br />| DELETE | <code>/api/v1/addons/homeassistant/lovelace/dashboards/[id]</code> |<br />| GET, PUT | <code>/api/v1/addons/homeassistant/lovelace/dashboards/[id]/config</code> |<br />| GET, POST | <code>/api/v1/addons/homeassistant/helpers/[domain]</code> |<br />| PUT, DELETE | <code>/api/v1/addons/homeassistant/helpers/[domain]/[id]</code> |<br />| GET, POST | <code>/api/v1/addons/homeassistant/areas</code> |<br />| PUT, DELETE | <code>/api/v1/addons/homeassistant/areas/[id]</code> |<br />| GET, POST | <code>/api/v1/addons/homeassistant/scenes</code> |<br />| DELETE | <code>/api/v1/addons/homeassistant/scenes/[id]</code> |<br />| POST | <code>/api/v1/addons/homeassistant/scenes/reload</code> |<br />| GET, POST | <code>/api/v1/addons/homeassistant/scripts</code> |<br />| DELETE | <code>/api/v1/addons/homeassistant/scripts/[id]</code> |<br />| POST | <code>/api/v1/addons/homeassistant/scripts/reload</code> |<br />| GET | <code>/api/v1/addons/homeassistant/status</code> |<br />| POST | <code>/api/v1/addons/homeassistant/test</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<p>- Alle Verwaltungsfunktionen (YAML-Editor, Dashboards, Helpers, Areas) verlangen eine konfigurierte<br />  HA-Verbindung (URL + Token); ist HA nicht erreichbar, antworten die Routen mit einem klaren<br />  Nicht-200-Status statt eines Absturzes.<br />- Dashboards: <strong>Kartenbibliothek</strong> (sechs Lovelace-Kartentypen: Kachel, Verlauf, Thermostat,<br />  Energie, Kamera, Sensor-Gruppe) <strong>und</strong> YAML-Editor für Views/Karten. Beide arbeiten auf<br />  derselben Konfiguration und speichern über denselben Weg — es gibt genau einen Speicherpfad<br />  (<code>saveDashboardConfig</code>), damit kein Builder ein Dashboard leeren kann.<br />- Areas: Bereiche-CRUD; <strong>Entitäten/Geräte einem Bereich zuweisen</strong> ist geparkt<br />  (<code>ha-area-entity-assignment</code>).<br />- Die <strong>HA-seitige</strong> Voice-Assistant-Verkabelung ist Owner-Konfiguration (siehe oben /<br />  <code>docs/OPEN-DECISIONS.md</code> → <code>ha-assist-agent-setup</code>).<br />- <strong>HA-MCP</strong>-Onboarding (addons#179): Standard-Policy ist <code>read-only</code>. Der explizite<br />  „Switch zu <code>write</code> / <code>full</code>&quot;-Flow ist <strong>eine separate Folge-Story</strong> (nicht in #179 enthalten);<br />  destruktive Tools bleiben so lange gesperrt, wie der Nutzer nicht ausdrücklich zustimmt.<br />  OAuth-Auth ist im aktuellen Schritt nicht unterstützt.</p>\n<p>&gt; Architektur-Entscheidungen: <code>docs/architecture/ADR-018-homeassistant-yaml-voice.md</code> (YAML+Voice),<br />&gt; <code>docs/architecture/ADR-028-ha-dashboards-helpers-areas.md</code> (Dashboards/Helpers/Areas/Szenen+Skripte),<br />&gt; <code>docs/architecture/ADR-047-addon-dependencies-feature-gating.md</code> (Feature-Gating über das<br />  MCP-Addon), <code>docs/architecture/ADR-054-user-scoped-data-access.md</code> (Per-User-Isolation für die<br />  <code>home-assistant</code>-MCP-Zeile). Voraussetzungen für die HA-MCP-Sicherheit: #177 (Streamable-HTTP-<br />  Transport im MCP-Addon) + #178 (Least-Privilege-Tool-Policy im MCP-Addon).</p>",
          "byteSize": 16419
        }
      }
    },
    {
      "id": "jarvis",
      "uuid": "610e4b31-2e5a-436a-8f83-723035f05fbf",
      "name": "Jarvis",
      "version": "1.0.4",
      "description": "Voice-first personal operations console with morning briefing, live weather, agenda, Tesla controls, and Home Assistant visibility.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "voice-assistant",
        "briefing",
        "calendar",
        "weather",
        "tesla",
        "home-assistant"
      ],
      "tags": [],
      "download": "/artifacts/jarvis-1.0.4.zip",
      "sha256": "6a25fa490fb2da3e2dc148ccdc2297439411bbbdca7a17df2f65212bc26f9433",
      "size": 102893,
      "page": {
        "path": "/addons/jarvis",
        "name": "Jarvis",
        "icon": "🤖",
        "section": "Tools",
        "component": "@addons/jarvis/src/components/dashboard/JarvisDashboard"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/jarvis",
        "/api/v1/addons/jarvis/briefing",
        "/api/v1/addons/jarvis/weather",
        "/api/v1/addons/jarvis/calendar",
        "/api/v1/addons/jarvis/tesla",
        "/api/v1/addons/jarvis/home-assistant"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Jarvis AI Assistant — Aiopta Addon</h1>\n<p>Iron Man-style AI assistant with weather, calendar, Tesla, and home automation.</p>\n<h2>Installation</h2>\n<ol><li>Copy this addon into <code>aiopta/addons/jarvis/</code></li><li>Copy <code>.env.example</code> to <code>.env.local</code> and configure</li><li>Install dependencies: <code>npm install</code></li><li>Rebuild Aiopta</li></ol>\n<h2>Features</h2>\n<p>| Feature | API | Auth Required |<br />|---------|-----|--------------|<br />| Weather | Open-Meteo | No |<br />| Calendar | Google Calendar | API Key |<br />| Tesla | Fleet API | OAuth Token |<br />| Home Assistant | REST API | Long-Lived Token |<br />| Morning Briefing | Aggregates all | Per-service |</p>\n<h2>API Endpoints</h2>\n<p>### Weather<br />```<br />GET /api/jarvis/weather?lat=52.52&amp;lon=13.40&amp;forecast=true<br />```</p>\n<p>### Calendar<br />```<br />GET /api/jarvis/calendar?today=true<br />GET /api/jarvis/calendar?start=2026-01-01&amp;end=2026-01-07<br />```</p>\n<p>### Tesla<br />```<br />GET  /api/jarvis/tesla                    # List vehicles<br />GET  /api/jarvis/tesla?vehicleId=123      # Vehicle status<br />POST /api/jarvis/tesla                    # Send command<br />     { &quot;vehicleId&quot;: &quot;123&quot;, &quot;action&quot;: &quot;wake&quot; }<br />```</p>\n<p>Actions: <code>wake</code>, <code>lock</code>, <code>unlock</code>, <code>honk</code>, <code>flash</code>, <code>climate_on</code>, <code>climate_off</code>, <code>charge_start</code>, <code>charge_stop</code></p>\n<p>### Home Assistant<br />```<br />GET  /api/jarvis/home-assistant                  # All entities<br />GET  /api/jarvis/home-assistant?entityId=light.living_room<br />POST /api/jarvis/home-assistant<br />     { &quot;entityId&quot;: &quot;light.living_room&quot;, &quot;service&quot;: &quot;toggle&quot; }<br />```</p>\n<p>### Morning Briefing<br />```<br />GET /api/jarvis/briefing<br />```</p>\n<h2>Environment Variables</h2>\n<p>| Variable | Default | Description |<br />|----------|---------|-------------|<br />| <code>WEATHER_DEFAULT_LAT</code> | 52.5200 | Default latitude (Berlin) |<br />| <code>WEATHER_DEFAULT_LON</code> | 13.4050 | Default longitude (Berlin) |<br />| <code>GOOGLE_CALENDAR_API_KEY</code> | — | Google API key |<br />| <code>GOOGLE_CALENDAR_ID</code> | — | Calendar ID |<br />| <code>TESLA_ACCESS_TOKEN</code> | — | Tesla OAuth token |<br />| <code>HOMEASSISTANT_URL</code> | — | HA instance URL |<br />| <code>HOMEASSISTANT_TOKEN</code> | — | HA long-lived token |</p>\n<h2>Architecture</h2>\n<p>This is a <strong>pure addon</strong> — zero changes to Aiopta core. Each service gracefully degrades when unconfigured (returns empty data).</p>\n<h2>Troubleshooting</h2>\n<ul><li><strong>Weather not loading</strong>: No config needed. Check network connectivity.</li><li><strong>Calendar empty</strong>: Set <code>GOOGLE_CALENDAR_API_KEY</code> and <code>GOOGLE_CALENDAR_ID</code>.</li><li><strong>Tesla not connected</strong>: Generate a token via Tesla developer portal.</li><li><strong>Home Assistant offline</strong>: Verify URL is reachable and token is valid.</li></ul>",
          "byteSize": 2396
        }
      }
    },
    {
      "id": "lead-gen",
      "uuid": "7a769d72-3697-43e2-93b5-7a9a947712d4",
      "name": "Lead-Gen & Outreach (CRM-lite)",
      "version": "2.0.3",
      "description": "DEPRECATED — zusammengeführt in das kanonische CRM-Addon `crm` (ADR-008 Addon-Konsolidierung, Issue #491 / Epic #490). `crm` ist das durchgängige CRM, das laut ADR-015 direkt auf dieser lead-gen-CRM-lite aufbaut. lead-gen bleibt bis zum Sunset (2026-12-31) voll funktionsfähig und datenerhaltend — kein Endpoint entfällt, keine Zeile wird kopiert/gelöscht. Der Redirect läuft über die Addon-Metadaten (`replacedBy: crm`, `settingsPagePath: /addons/crm`) und den Nachfolger-Link im Marketplace/Settings-Hub. Ursprüngliche Fähigkeit: CRM-lite (contacts, pipeline, suppression, sequences) + Cold Outreach; Scrape/Enrich via Aiopta-managed Container (ADR-009, nur interne URL), Sequenz-Entwürfe via Aiopta-Gateway, Versand unter zwingenden Anti-Spam-Guardrails (Suppression + per-run/daily Rate-Limit + explizites confirm). SMTP/ESP- und Enrichment-Keys kommen aus Aiopta addon-config, nie aus dem Manifest. Money-Stufe-1 (ADR-015).",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "crm-contacts",
        "crm-pipeline",
        "lead-scraping",
        "lead-enrichment",
        "outreach-sequences",
        "outreach-sending",
        "outreach-suppression"
      ],
      "tags": [],
      "download": "/artifacts/lead-gen-2.0.3.zip",
      "sha256": "609acf52034328f56445449ed4d9c1b7266f3f92114971a2e5b9a0ebadc9fec5",
      "size": 111764,
      "page": {
        "id": "lead-gen-page",
        "path": "/addons/lead-gen",
        "name": "Lead-Gen & Outreach",
        "icon": "🎯",
        "section": "Money",
        "component": "@addons/lead-gen/src/page"
      },
      "deprecated": true,
      "replacedBy": "crm",
      "deprecation": {
        "deprecated": true,
        "replacedBy": "crm",
        "sunset": "2026-12-31",
        "successorPath": "/addons/crm",
        "successorLabel": "CRM (Nachfolger)"
      },
      "endpoints": [
        "/api/v1/addons/lead-gen/status",
        "/api/v1/addons/lead-gen/contacts",
        "/api/v1/addons/lead-gen/contacts/:id",
        "/api/v1/addons/lead-gen/pipeline",
        "/api/v1/addons/lead-gen/sequences",
        "/api/v1/addons/lead-gen/sequences/:id/generate",
        "/api/v1/addons/lead-gen/sequences/:id/send",
        "/api/v1/addons/lead-gen/suppression",
        "/api/v1/addons/lead-gen/enrich",
        "/api/v1/addons/lead-gen/widget"
      ],
      "skills": [
        {
          "name": "lead-crm",
          "displayName": "Lead-Gen: CRM contacts & pipeline",
          "description": "List/create/edit contacts and move them through the pipeline (new→contacted→replied→qualified→won/lost). Pure stage-machine enforced; no sending.",
          "permissions": [
            "addons/lead-gen/contacts",
            "addons/lead-gen/pipeline",
            "addons/lead-gen/status"
          ],
          "endpoint": "/api/v1/addons/lead-gen/contacts",
          "scripts": [
            {
              "id": "lead-crm-cli",
              "name": "lead-crm-cli",
              "path": "lead-crm/cli.sh",
              "sha256": "a1bcdfec934ada02e9669497c1ad1b1f94f76b5a505788b1ca9f793266834761",
              "interpreter": "bash",
              "timeoutMs": 10000,
              "permissions": [
                "addons/lead-gen/contacts",
                "addons/lead-gen/pipeline",
                "addons/lead-gen/status"
              ],
              "inputSchema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 20
                  },
                  "timeoutMs": {
                    "type": "integer",
                    "minimum": 100,
                    "maximum": 300000
                  },
                  "input": {
                    "type": "object",
                    "description": "Request-Body als JSON — niemals Secrets"
                  },
                  "dryRun": {
                    "type": "boolean",
                    "default": false,
                    "description": "Request validieren ohne Mutation — kein API-Call"
                  }
                }
              },
              "outputSchema": {
                "type": "object",
                "required": [
                  "ok"
                ],
                "properties": {
                  "ok": {
                    "type": "boolean"
                  },
                  "status": {
                    "type": "integer"
                  },
                  "data": {},
                  "error": {
                    "type": "string"
                  },
                  "truncated": {
                    "type": "boolean"
                  }
                }
              },
              "size": 12062
            }
          ]
        },
        {
          "name": "lead-enrich",
          "displayName": "Lead-Gen: Scrape & enrich",
          "description": "Scrape lead candidates and enrich contacts via the MC-managed scraper container. If the container is not provisioned the call returns a clear 503; never a hard-coded host.",
          "permissions": [
            "addons/lead-gen/enrich",
            "addons/lead-gen/contacts",
            "addons/lead-gen/status"
          ],
          "endpoint": "/api/v1/addons/lead-gen/enrich",
          "scripts": []
        },
        {
          "name": "lead-sequences",
          "displayName": "Lead-Gen: Draft outreach sequences",
          "description": "Create sequences and generate personalized message DRAFTS per contact via the MC gateway (never a direct LLM call). Drafting only — this skill cannot send.",
          "permissions": [
            "addons/lead-gen/sequences",
            "addons/lead-gen/contacts"
          ],
          "endpoint": "/api/v1/addons/lead-gen/sequences",
          "scripts": []
        },
        {
          "name": "lead-suppression",
          "displayName": "Lead-Gen: Suppression / opt-out",
          "description": "List and add opt-out / suppressed addresses. Suppressed contacts are NEVER sent to — this is the anti-spam source of truth.",
          "permissions": [
            "addons/lead-gen/suppression",
            "addons/lead-gen/status"
          ],
          "endpoint": "/api/v1/addons/lead-gen/suppression",
          "scripts": []
        },
        {
          "name": "lead-send",
          "displayName": "Lead-Gen: Send outreach (guarded)",
          "description": "Send drafted messages. REQUIRES confirm:true, respects the suppression list and the per-run/daily rate limits, and needs SMTP/ESP config in MC addon-config (missing → 400). This is the only skill that can send; grant it deliberately.",
          "permissions": [
            "addons/lead-gen/sequences",
            "addons/lead-gen/suppression",
            "addons/lead-gen/status"
          ],
          "endpoint": "/api/v1/addons/lead-gen/sequences/:id/send",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Lead-Gen &amp; Outreach (CRM-lite)</h1>\n<blockquote><p>⚠️ **DEPRECATED — merged into the canonical <code>crm</code> addon (ADR-008 addon-consolidation,</p><p>issue #491 / epic #490).** <code>crm</code> is the durable, full CRM that ADR-015 already builds on top of this</p><p>CRM-lite. lead-gen remains functional and data-preserving until its <strong>sunset on 2026-12-31</strong> — no</p><p>endpoint is removed and no row is copied or deleted. The Alt-ID redirect is expressed through the</p><p>addon metadata (<code>replacedBy: crm</code>, <code>sunset</code>, <code>settingsPagePath: /addons/crm</code>) so the Marketplace and</p><p>Settings-Hub point at the successor, and — at the record level — through <code>crm</code>&#39;s</p><p><code>getContactByLeadEmail</code>, which resolves an old lead-gen lead identity (by email) to the migrated CRM</p><p>contact. The lossless consolidation path is <code>crm</code>&#39;s per-user <code>importLeads</code> reconcile</p><p>(<code>POST /api/v1/addons/crm/import/leads</code>), which reads this addon&#39;s own scoped store and never mutates</p><p>the <code>lead_gen_*</code> tables. See the PR for #491 for the license/pricing/permission review.</p></blockquote>\n<p>A <strong>Money-Stufe-1</strong> addon: a lightweight CRM plus cold-outreach engine. Capture and qualify leads,<br />scrape/enrich them via an Aiopta-managed container, draft personalized messages through the Aiopta<br />AI gateway, and send them under <strong>mandatory anti-spam guardrails</strong>.</p>\n<p>This addon <strong>orchestrates</strong> existing Aiopta building blocks the same way <code>seo-content</code> and<br /><code>dropshipping</code> do (ADR-015):</p>\n<p>- <strong>AI message generation → the Aiopta gateway</strong> (<code>@/lib/gateway</code>, <code>chatCompletionWithFallbacks</code>). There is<br />  <strong>no direct LLM/provider call</strong> anywhere in the addon.<br />- <strong>Scraping / enrichment → an Aiopta-managed container</strong> (ADR-009), reached <strong>only</strong> via<br />  <code>getContainerAddonInternalUrl(&quot;lead-gen&quot;)</code> — never a hard-coded host.<br />- <strong>Sending → an SMTP/ESP interface</strong> that reads config from Aiopta addon-config — **no secret in the repo<br />  or logs**.</p>\n<h2>⚠️ Anti-spam guardrails (mandatory — read this first)</h2>\n<p>Cold outreach is dangerous if it can blast. This addon makes a careless mass-send **structurally<br />impossible<strong>. Three guardrails are enforced in <code>src/lib/sender.ts</code> and are </strong>not optional**:</p>\n<p>1. <strong>Explicit confirmation.</strong> <code>POST /sequences/:id/send</code> refuses to send unless the request body<br />   contains <code>confirm: true</code>. Without it the call returns <strong>400 before any transport is opened</strong> — no<br />   accidental blast.<br />2. <strong>Suppression / opt-out check.</strong> Every recipient is checked against the suppression list<br />   (<code>/suppression</code>). A suppressed / opted-out address is <strong>never</strong> sent to — it is skipped and reported.<br />   This is the source of truth for honoring unsubscribes; add an address there and it is dead to the<br />   sender forever.<br />3. <strong>Rate limits.</strong> A small <strong>per-run</strong> cap and a <strong>daily</strong> cap (both configurable in addon-config,<br />   with conservative defaults — <code>25/run</code>, <code>200/day</code>) bound how many messages can go out. Anything over<br />   the remaining quota is deferred (<code>rate-limited</code>), not sent.</p>\n<p>The ordering is strict and <strong>pure</strong> (<code>planSend</code>): drop invalid emails → drop suppressed → apply the<br />remaining-quota cap. It is unit-tested without any SMTP/DB so the guarantee can&#39;t silently regress<br />(<code>tests/unit/sender.test.ts</code>).</p>\n<blockquote><p><strong>Mass-send / bulk outreach without explicit owner approval is not supported.</strong> These guardrails are</p><p>a guardrail, not a setting to switch off. Sending also requires SMTP/ESP config to be present</p><p>(missing → 400), so the addon can never email through an unconfigured transport.</p></blockquote>\n<h2>Per-user isolation (#141 / ADR-054)</h2>\n<p>The CRM is <strong>per-user</strong>, not a shared catalog. Contacts, sequences (+ their steps), the<br />suppression / opt-out list and the daily send-log all carry <code>user_id</code> and are read/written only<br />through <code>scopedStore(auth.id)</code> / <code>WHERE user_id = ?</code>. The server stamps the owner from the session<br />(a client-supplied <code>user_id</code> is ignored), and a foreign contact/sequence id returns <strong>404</strong> (not the<br />row, not 403). Two users may track the <strong>same email</strong> independently (<code>UNIQUE(user_id, email)</code>).<br />Critically, <strong>suppression and the rate-limit are per user</strong>: one user&#39;s opt-out list cannot gag<br />another user&#39;s outreach, and one user&#39;s sent volume cannot consume another&#39;s daily quota.</p>\n<h2>Configuration (Aiopta addon-config — never the manifest, never logged)</h2>\n<p>Stored via Aiopta addon-config for the <code>lead-gen</code> addon; secret-looking keys (<code>*_PASS</code>, <code>*_API_KEY</code>) are<br />persisted with <code>is_secret = 1</code>. Two owner decisions are parked in <code>docs/OPEN-DECISIONS.md</code><br />(<code>lead-scraper-image</code>, <code>lead-gen-email-config</code>).</p>\n<h3>Email sending (<code>lead-gen-email-config</code>)</h3>\n<p>| Key | Meaning | Default |<br />|-----|---------|---------|<br />| <code>ESP_PROVIDER</code> | <code>smtp</code> (wired) or <code>sendgrid</code>/<code>mailgun</code>/<code>postmark</code>/<code>resend</code> (declared) | <code>smtp</code> |<br />| <code>SMTP_HOST</code> / <code>SMTP_PORT</code> / <code>SMTP_USER</code> / <code>SMTP_PASS</code> | SMTP connection (provider = <code>smtp</code>) | — / <code>587</code> / — / — |<br />| <code>SMTP_SECURE</code> | <code>&quot;true&quot;</code> for implicit TLS | <code>false</code> |<br />| <code>SMTP_FROM</code> (or <code>ESP_FROM</code> / <code>FROM</code>) | From address (<strong>required for any send</strong>) | — |<br />| <code>ESP_API_KEY</code> | ESP API key (provider ≠ <code>smtp</code>) | — |<br />| <code>SEND_PER_RUN_LIMIT</code> | per-run rate cap | <code>25</code> |<br />| <code>SEND_DAILY_LIMIT</code> | rolling-24h rate cap | <code>200</code> |</p>\n<p>Until email is configured, <code>/status</code> reports <code>email.configured: false</code> and any send returns a clear<br /><strong>400</strong>. No secret is ever written to the manifest or surfaced by <code>/status</code> (only booleans, the provider<br />id, and the from-<strong>domain</strong>).</p>\n<h3>Scraper / enrichment container (<code>lead-scraper-image</code>)</h3>\n<p>Declared as a <code>container</code> in <code>addon.json</code> / <code>src/manifest.ts</code>:</p>\n<pre><code>image:        registry.langkau.info/aiopta/lead-scraper:latest\ninternalPort: 8080\nhealthPath:   /health\nenvSchema:    ENRICHMENT_PROVIDER, ENRICHMENT_API_KEY, SCRAPER_USER_AGENT, SCRAPER_RATE_LIMIT</code></pre>\n<p><code>envSchema</code> declares only the <strong>names</strong> of the keys the <strong>container</strong> needs (enrichment-provider<br />credentials); the values come from Aiopta addon-config, never the manifest. The image is <strong>owner-built</strong> —<br />until it is published, <code>/status</code> reports <code>scraper.provisioned: false</code> and <code>/enrich</code> (scrape/enrich)<br />returns a clear <strong>503</strong>. The addon never falls back to a default host.</p>\n<h2>API</h2>\n<p>Base: <code>/api/v1/addons/lead-gen</code> (all routes auth-gated; agent capability <code>lead-gen/*</code>).</p>\n<p>| Goal | Method + path | Body |<br />|------|---------------|------|<br />| Status (scraper + email + gateway + pipeline + limits) | <code>GET /status</code> | — |<br />| List contacts | <code>GET /contacts?stage=new</code> | — |<br />| Create / upsert a contact | <code>POST /contacts</code> | <code>{ &quot;email&quot;: &quot;…&quot;, &quot;name&quot;: &quot;…&quot;, &quot;company&quot;: &quot;…&quot; }</code> |<br />| Read / edit / delete a contact | <code>GET|PUT|DELETE /contacts/:id</code> | edit: <code>{ &quot;stage&quot;: &quot;…&quot;, &quot;tags&quot;: […] }</code> |<br />| Pipeline board | <code>GET /pipeline</code> | — |<br />| Move a contact&#39;s stage | <code>PUT /pipeline</code> | <code>{ &quot;contactId&quot;: 1, &quot;stage&quot;: &quot;contacted&quot; }</code> |<br />| List sequences | <code>GET /sequences</code> | — |<br />| Create a sequence | <code>POST /sequences</code> | <code>{ &quot;name&quot;: &quot;…&quot;, &quot;goal&quot;: &quot;book a demo&quot; }</code> |<br />| Draft messages (via gateway) | <code>POST /sequences/:id/generate</code> | <code>{ &quot;stage&quot;: &quot;new&quot; }</code> or <code>{ &quot;contactId&quot;: 1 }</code> |<br />| <strong>Send drafts (guarded)</strong> | <code>POST /sequences/:id/send</code> | <code>{ &quot;confirm&quot;: true, &quot;messages&quot;: [{ &quot;contactId&quot;: 1, &quot;subject&quot;: &quot;…&quot;, &quot;body&quot;: &quot;…&quot; }] }</code> |<br />| Suppression list | <code>GET /suppression</code> | — |<br />| Add an opt-out | <code>POST /suppression</code> | <code>{ &quot;email&quot;: &quot;…&quot;, &quot;reason&quot;: &quot;unsubscribe&quot; }</code> |<br />| Scrape / enrich (container) | <code>POST /enrich</code> | <code>{ &quot;query&quot;: &quot;…&quot; }</code> · <code>{ &quot;contactId&quot;: 1 }</code> · <code>{ &quot;email&quot;: &quot;…&quot; }</code> |<br />| Dashboard widget (flat) | <code>GET /widget</code> | — |</p>\n<h3>Pipeline stages</h3>\n<p><code>new → contacted → replied → qualified → won/lost</code>. <code>won</code>/<code>lost</code> are terminal (a closed deal can&#39;t be<br />re-opened by a move — delete + recreate instead). Moving to <code>replied</code> stamps the reply timestamp, which<br />drives the <strong>reply-rate</strong> shown on the page and widget. The stage-machine (<code>canTransition</code>) is pure and<br />unit-tested.</p>\n<h2>Page &amp; widget</h2>\n<p>- <strong>Page</strong> (<code>Money</code> section, <code>/addons/lead-gen</code>): tabs <strong>Leads</strong> (table + add/scrape), <strong>Pipeline</strong><br />  (per-stage columns with move buttons), <strong>Sequences</strong> (create → generate drafts → edit → **send with a<br />  required confirm checkbox<strong>), </strong>Suppression** (opt-out list). A status banner shows gateway/scraper/<br />  email readiness and the active rate limits.<br />- <strong>Widget</strong> (<code>lead-gen.pipeline</code>): open leads, reply-rate, won, suppression size, scraper + email<br />  readiness.</p>\n<h2>Architecture notes (ADR-015)</h2>\n<p>- <strong>AI via gateway only.</strong> <code>src/lib/sequencer.ts</code> builds the prompt and calls<br />  <code>chatCompletionWithFallbacks</code> — no provider <code>fetch</code>. A test stubs global <code>fetch</code> and asserts it is<br />  never called during generation.<br />- <strong>Scraper via the container-service.</strong> <code>src/lib/scraper.ts</code> resolves the base URL with<br />  <code>getContainerAddonInternalUrl(&quot;lead-gen&quot;)</code>; absent → 503 (tested), present → calls the internal<br />  <code>/scrape</code> and <code>/enrich</code> endpoints.<br />- <strong>Sender is an interface with guardrails.</strong> <code>src/lib/sender.ts</code> reads SMTP/ESP from addon-config,<br />  enforces confirm + suppression + rate-limit, and records each send for the daily cap.<br />- <strong>CRM owns its tables.</strong> <code>src/lib/crm-store.ts</code> creates <code>lead_gen_contacts</code>, <code>lead_gen_suppression</code>,<br />  <code>lead_gen_sequences</code>, <code>lead_gen_sequence_steps</code> (and the sender&#39;s <code>lead_gen_send_log</code>) in <code>install()</code><br />  and drops them in <code>uninstall()</code> via Aiopta core&#39;s <code>getDb()</code>.</p>\n<h2>Tests</h2>\n<p><code>addons/lead-gen/tests/unit/</code> (run with the manifest-route-sync test):</p>\n<p>- <code>manifest.test.ts</code> — manifest validity, namespaced endpoints, page + widget, <strong>container present</strong>,<br />  least-privilege skills (only <code>lead-send</code> carries <code>/send</code>), no secret/host in the manifest.<br />- <code>crm-store.test.ts</code> — the pure stage-machine + email/contact validation.<br />- <code>sender.test.ts</code> — <strong>the anti-spam guarantees</strong>: <code>planSend</code> never includes a suppressed contact and<br />  enforces per-run/daily caps; <code>sendBatch</code> rejects without <code>confirm</code>, rejects when unconfigured, and<br />  with confirm never delivers to a suppressed contact and caps the batch.<br />- <code>sequencer.test.ts</code> — generation routes through <code>@/lib/gateway</code>, never a direct provider <code>fetch</code>.<br />- <code>scraper.test.ts</code> — reached only via <code>getContainerAddonInternalUrl</code>; container-absent → 503.</p>\n<pre><code class=\"lang-bash\">wsl -e bash -lc &#39;cd /mnt/c/Users/Patrick/IdeaProjects/Antigravity/aiopta &amp;&amp; \\\n  docker run --rm -v &quot;$PWD&quot;:/app -w /app node:22-slim \\\n  node node_modules/vitest/vitest.mjs run \\\n  tests/unit/extensions/manifest-route-sync.test.ts addons/lead-gen/tests/unit&#39;</code></pre>",
          "byteSize": 10290
        }
      }
    },
    {
      "id": "manifest-routing",
      "uuid": "170ef3fc-b8d2-4760-a92e-98ffce3fc31e",
      "name": "Manifest Routing",
      "version": "1.1.0",
      "description": "Routing over the Core provider catalog, subscriptions and monitoring as an installable addon.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "llm-routing",
        "subscription-management",
        "usage-monitoring",
        "zai"
      ],
      "tags": [],
      "download": "/artifacts/manifest-routing-1.1.0.zip",
      "sha256": "4320c8d41c170a4244548d0d26de9833ed06fb606f99f663be80705f708b33ce",
      "size": 169626,
      "page": {
        "path": "/addons/manifest-routing",
        "name": "Manifest Routing",
        "icon": "🧭",
        "section": "Admin",
        "component": "@addons/manifest-routing/src/components/ManifestRoutingSettings"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/manifest-routing",
        "/api/v1/addons/manifest-routing/subscriptions",
        "/api/v1/addons/manifest-routing/zai",
        "/api/v1/addons/manifest-routing/notifications",
        "/api/v1/addons/manifest-routing/failed-requests",
        "/api/v1/addons/manifest-routing/health-check",
        "/api/v1/addons/manifest-routing/stats",
        "/api/v1/addons/manifest-routing/rule/{id}"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Manifest Routing</h1>\n<p>Routing over the Core provider catalog, subscriptions and monitoring as an installable addon.</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>manifest-routing</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> llm-routing, subscription-management, usage-monitoring, zai</li><li><strong>UI-Seite:</strong> <code>/addons/manifest-routing</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Dieses Addon benötigt folgende Secrets/Provider-Konfiguration (Settings → Credentials bzw. Provider-Store, <strong>nie</strong> in addon.json):</p>\n<ul><li><code>OPENAI_CLIENT_SECRET</code></li></ul>\n<p>Provider, Modelle und API-Keys werden ausschließlich vom Core unter<br /><code>/settings#llm-providers</code> beziehungsweise <code>/api/v1/llm-providers</code> verwaltet.<br />Manifest Routing liest den Core-Provider-Katalog und referenziert dessen Provider- und Modell-IDs<br />in Routing-Regeln; das Addon besitzt keine eigene Provider-CRUD-Oberfläche.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET, POST | <code>/api/v1/addons/manifest-routing</code> |<br />| GET, POST, DELETE | <code>/api/v1/addons/manifest-routing/failed-requests</code> |<br />| GET | <code>/api/v1/addons/manifest-routing/health-check</code> |<br />| GET, POST, DELETE | <code>/api/v1/addons/manifest-routing/notifications</code> |<br />| POST | <code>/api/v1/addons/manifest-routing/oauth</code> |<br />| DELETE | <code>/api/v1/addons/manifest-routing/rule/[id]</code> |<br />| GET | <code>/api/v1/addons/manifest-routing/stats</code> |<br />| GET | <code>/api/v1/addons/manifest-routing/status</code> |<br />| GET, POST, PUT, DELETE | <code>/api/v1/addons/manifest-routing/subscriptions</code> |<br />| GET, POST | <code>/api/v1/addons/manifest-routing/zai</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<ul><li>Keine bekannten Einschränkungen.</li></ul>\n<blockquote><p>README aus <code>addon.json</code> + vorhandenen Routen generiert (<code>scripts/generate-addon-readmes.js</code>). Bei manuellen Ergänzungen den Generator-Hinweis entfernen.</p></blockquote>",
          "byteSize": 1667
        }
      }
    },
    {
      "id": "marketing-ads",
      "uuid": "f1030d66-e21a-4546-8085-1ea9840bdf6d",
      "name": "Marketing & Ads (Affiliate-Tracking + Google/Meta Kampagnen + KI-Creatives)",
      "version": "1.0.1",
      "description": "Per-User Marketing: Ad-Kampagnen (Google/Meta) mit Budget & ROAS, Affiliate-Tracking mit Klick-/Conversion-Zählern und KI-Creative-Prompts. Plattform-Spend/Revenue fließen über einen injizierbaren, per-User BYOK Fetch-Seam (nie hartcodiert); KI-Creatives hinter einem LLM-Gateway-Seam mit graceful Degradation. Lokale SQLite ist die Quelle der Wahrheit (ADR-054).",
      "author": "Aiopta",
      "icon": "📣",
      "capabilities": [
        "marketing",
        "ads",
        "campaigns",
        "google",
        "meta",
        "affiliates",
        "creatives",
        "budget",
        "roas"
      ],
      "tags": [],
      "download": "/artifacts/marketing-ads-1.0.1.zip",
      "sha256": "7555ecff0b3141718dacfc184e849ca1fdab91254fc984e0418f026991df054c",
      "size": 105490,
      "page": {
        "id": "marketing-ads-page",
        "path": "/addons/marketing-ads",
        "name": "Marketing & Ads",
        "icon": "📣",
        "section": "Money",
        "component": "@addons/marketing-ads/src/components/MarketingAdsPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/marketing-ads/campaigns",
        "/api/v1/addons/marketing-ads/campaigns/:id",
        "/api/v1/addons/marketing-ads/affiliates",
        "/api/v1/addons/marketing-ads/affiliates/:id",
        "/api/v1/addons/marketing-ads/affiliates/:id/click",
        "/api/v1/addons/marketing-ads/affiliates/:id/convert",
        "/api/v1/addons/marketing-ads/creatives",
        "/api/v1/addons/marketing-ads/creatives/:id",
        "/api/v1/addons/marketing-ads/stats"
      ],
      "skills": [
        {
          "name": "marketing-ads-read",
          "displayName": "Marketing & Ads: Kampagnen, ROAS & Affiliates lesen",
          "description": "Listet Ad-Kampagnen (Google/Meta) mit Budget/Spend/Revenue/ROAS, Affiliate-Links mit Klick-/Conversion-Zählern und KI-Creatives für den Nutzer. Reiner Lesezugriff — legt nichts an.",
          "permissions": [
            "addons/marketing-ads/campaigns",
            "addons/marketing-ads/affiliates",
            "addons/marketing-ads/creatives",
            "addons/marketing-ads/stats"
          ],
          "endpoint": "/api/v1/addons/marketing-ads/stats",
          "scripts": []
        },
        {
          "name": "marketing-ads-manage",
          "displayName": "Marketing & Ads: Kampagnen & Budget verwalten",
          "description": "Legt Ad-Kampagnen (Google/Meta) an oder entfernt diese und setzt Status/Budget/Spend/Revenue (per-User gebucht). ROAS wird aus lokaler Spend/Revenue berechnet.",
          "permissions": [
            "addons/marketing-ads/campaigns"
          ],
          "endpoint": "/api/v1/addons/marketing-ads/campaigns",
          "scripts": []
        },
        {
          "name": "marketing-ads-affiliates",
          "displayName": "Marketing & Ads: Affiliate-Links verwalten",
          "description": "Legt Affiliate-/Partner-Links an, zeichnet Klicks/Conversions auf (per-User gebucht) und entfernt diese.",
          "permissions": [
            "addons/marketing-ads/affiliates"
          ],
          "endpoint": "/api/v1/addons/marketing-ads/affiliates",
          "scripts": []
        },
        {
          "name": "marketing-ads-creatives",
          "displayName": "Marketing & Ads: KI-Creatives generieren",
          "description": "Erstellt KI-Creative-Prompts (Google/Meta) und generiert Copy über das LLM-Gateway (graceful Degradation auf einen Stub, falls das Gateway nicht konfiguriert ist).",
          "permissions": [
            "addons/marketing-ads/creatives"
          ],
          "endpoint": "/api/v1/addons/marketing-ads/creatives",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>📣 Marketing &amp; Ads (Affiliate-Tracking + Google/Meta Kampagnen + KI-Creatives)</h1>\n<p>Per-User Marketing: Ad-Kampagnen (Google/Meta) mit Budget &amp; ROAS, Affiliate-Tracking mit Klick-/Conversion-Zählern und KI-Creative-Prompts. Plattform-Spend/Revenue fließen über einen injizierbaren, per-User BYOK Fetch-Seam (nie hartcodiert); KI-Creatives hinter einem LLM-Gateway-Seam mit graceful Degradation. Lokale SQLite ist die Quelle der Wahrheit (ADR-054).</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>marketing-ads</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> marketing, ads, campaigns, google, meta, affiliates, creatives, budget, roas</li><li><strong>UI-Seite:</strong> <code>/addons/marketing-ads</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials. Die<br />Google/Meta-Plattform-Integration (Spend/Revenue) ist OPTIONAL und per-User<br />BYOK (Bring-Your-Own-Key): Tokens liegen im Nutzer-Secret-Store und fließen über<br />den injizierbaren Fetch-Seam (<code>src/lib/marketing-ads-fetch.ts</code>). Bis eine echte<br />Plattform-Anbindung konfiguriert ist, läuft das Addon auf lokalen Zahlen<br />(graceful Degradation) — ROAS bleibt deterministisch offline. Die KI-Creative-<br />Generierung (<code>src/lib/marketing-ads-creative.ts</code>) verhält sich analog: fehlt das<br />LLM-Gateway, wird ein deterministischer Stub gespeichert.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />| --- | --- |<br />| GET, POST | <code>/api/v1/addons/marketing-ads/campaigns</code> |<br />| PATCH, DELETE | <code>/api/v1/addons/marketing-ads/campaigns/:id</code> |<br />| GET, POST | <code>/api/v1/addons/marketing-ads/affiliates</code> |<br />| DELETE | <code>/api/v1/addons/marketing-ads/affiliates/:id</code> |<br />| POST | <code>/api/v1/addons/marketing-ads/affiliates/:id/click</code> |<br />| POST | <code>/api/v1/addons/marketing-ads/affiliates/:id/convert</code> |<br />| GET, POST | <code>/api/v1/addons/marketing-ads/creatives</code> |<br />| DELETE | <code>/api/v1/addons/marketing-ads/creatives/:id</code> |<br />| GET | <code>/api/v1/addons/marketing-ads/stats</code> |</p>\n<p>Alle Routen sind <code>userRoute</code> + <code>auth.id</code>-scoped (ADR-054). Eine fremde ID ergibt<br />404 ohne Existenz-Leak.</p>\n<h2>Architektur</h2>\n<p>- <strong>Per-User Store</strong> (<code>src/lib/marketing-ads-store.ts</code>): <code>scopedStore(userId)</code> +<br />  <code>registerScopedTable</code> für <code>marketing_ads_campaigns</code>, <code>marketing_ads_affiliates</code>,<br />  <code>marketing_ads_creatives</code>. ROAS = revenue / spend (0 bei Spend 0, nie Infinity).<br />- <strong>Affiliate-Resolver</strong> (<code>src/lib/affiliate-link.ts</code>): reine URL-Builder +<br />  Resolver für <code>/go/&lt;code&gt;</code> → Ziel-URL.<br />- <strong>Fetch-Seam</strong> (<code>src/lib/marketing-ads-fetch.ts</code>): injizierbarer Google/Meta-<br />  Adapter, BYOK-Tokens per User, graceful Degradation.<br />- <strong>Creative-Seam</strong> (<code>src/lib/marketing-ads-creative.ts</code>): injizierbares LLM-<br />  Gateway, deterministischer Stub-Fallback.</p>",
          "byteSize": 2618
        }
      }
    },
    {
      "id": "marketplace",
      "uuid": "685a4b98-3401-4207-bf8c-4259c6d5d32b",
      "name": "Agent-/Addon-Marketplace",
      "version": "1.5.0",
      "description": "An in-instance marketplace: a browsable catalog to discover, install and publish agents (as shared templates) and addons. A thin curation surface over existing primitives — agent listings are shared agent-templates (instantiate to install, Phase A), addon listings are installable addons from the registry (enable to install, Phase K). Listings may be PAID (one-time and/or subscription price + currency); a configurable platform revenue-share % splits each paid transaction (seller payout vs platform cut). Purchases create a payment via the existing payments addon — when payments is unconfigured the paid flow is gracefully inactive while free listings + browsing keep working. It never re-implements sharing/install/Stripe-PayPal, never copies a secret, is visibility-aware (a private template is never exposed), and never calls external services (ADR-009, ADR-026). Cross-instance/remote-registry marketplace is out of scope.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "marketplace-catalog",
        "marketplace-agent-listings",
        "marketplace-addon-listings",
        "marketplace-install",
        "marketplace-publish",
        "marketplace-paid-listings",
        "marketplace-revenue-share",
        "marketplace-seeds",
        "marketplace-subscriptions"
      ],
      "tags": [],
      "download": "/artifacts/marketplace-1.5.0.zip",
      "sha256": "aeeccc44601a30cff7c4fb21dc84588298288c18d8b0662482f1c105a9c467c4",
      "size": 505755,
      "page": {
        "id": "marketplace-page",
        "path": "/addons/marketplace",
        "name": "Marketplace",
        "icon": "🛍️",
        "section": "Addons",
        "component": "@addons/marketplace/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/marketplace/status",
        "/api/v1/addons/marketplace/listings",
        "/api/v1/addons/marketplace/listings/:id",
        "/api/v1/addons/marketplace/listings/:id/install",
        "/api/v1/addons/marketplace/listings/:id/purchase",
        "/api/v1/addons/marketplace/listings/:id/pricing",
        "/api/v1/addons/marketplace/listings/:id/tiers",
        "/api/v1/addons/marketplace/listings/:id/subscribe",
        "/api/v1/addons/marketplace/listings/:id/cancel",
        "/api/v1/addons/marketplace/revenue-share",
        "/api/v1/addons/marketplace/publish",
        "/api/v1/addons/marketplace/widget",
        "/api/v1/addons/marketplace/seeds",
        "/api/v1/addons/marketplace/seeds/:id/install"
      ],
      "skills": [
        {
          "name": "marketplace-browse",
          "displayName": "Marketplace: Katalog durchsuchen",
          "description": "List/search the in-instance catalog of agents (shared templates) and addons. Visibility-aware: never returns another user's private template. Read-only; supports kind/category/q filters and a detail view per listing.",
          "permissions": [
            "addons/marketplace/listings",
            "addons/marketplace/status"
          ],
          "endpoint": "/api/v1/addons/marketplace/listings",
          "scripts": [
            {
              "id": "marketplace-browse-cli",
              "name": "marketplace-browse-cli",
              "path": "marketplace-browse/cli.sh",
              "sha256": "80a5e529eeafe322234457b593d1b8265cd27c3426ab9577deedf4584f5ba19b",
              "interpreter": "bash",
              "timeoutMs": 10000,
              "permissions": [
                "addons/marketplace/listings",
                "addons/marketplace/status"
              ],
              "inputSchema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 20
                  },
                  "timeoutMs": {
                    "type": "integer",
                    "minimum": 100,
                    "maximum": 300000
                  }
                }
              },
              "outputSchema": {
                "type": "object",
                "required": [
                  "ok"
                ],
                "properties": {
                  "ok": {
                    "type": "boolean"
                  },
                  "status": {
                    "type": "integer"
                  },
                  "data": {},
                  "error": {
                    "type": "string"
                  },
                  "truncated": {
                    "type": "boolean"
                  }
                }
              },
              "size": 12111
            }
          ]
        },
        {
          "name": "marketplace-install",
          "displayName": "Marketplace: Eintrag installieren/verwenden",
          "description": "Install a listing: kind=agent instantiates the shared template for the caller via the Phase-A path (own key, no secrets); kind=addon enables the available addon (Phase K). Increments the install counter. Reuses existing primitives — never re-implements provisioning or ZIP install.",
          "permissions": [
            "addons/marketplace/listings",
            "addons/marketplace/status"
          ],
          "endpoint": "/api/v1/addons/marketplace/listings/:id/install",
          "scripts": []
        },
        {
          "name": "marketplace-publish",
          "displayName": "Marketplace: Agenten veröffentlichen",
          "description": "Publish an agent the caller owns as a SHARED template + curated listing (owner/admin-gated, same capability as Phase-A shared-publish). Captures only declarative config (model/soul/skills/permission-preset) — never secrets. Addons are listed from the registry, not published here.",
          "permissions": [
            "addons/marketplace/publish",
            "addons/marketplace/status"
          ],
          "endpoint": "/api/v1/addons/marketplace/publish",
          "scripts": []
        },
        {
          "name": "marketplace-seeds",
          "displayName": "Marketplace: Seed-Katalog (kuratiert)",
          "description": "Read the curated marketplace seed catalog (issue #321, Slice C): agents / addons / workflows shipped as versioned fixtures, with slice-B import state overlaid (installable / installed / upgrade-available / missing-dependency). Install a seed by id; the slice-B idempotent importer is the canonical writer.",
          "permissions": [
            "addons/marketplace/listings",
            "addons/marketplace/status"
          ],
          "endpoint": "/api/v1/addons/marketplace/seeds",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Agent-/Addon-Marketplace</h1>\n<p>An <strong>in-instance</strong> marketplace — one browsable catalog to <strong>discover, install, and publish</strong> agents (as<br />shared templates) and addons inside this Aiopta instance.</p>\n<p>It is a thin <strong>curation layer over primitives that already exist</strong> and re-implements <strong>nothing</strong><br />(ADR-026):</p>\n<p>- <strong>Agent listings</strong> ARE shared <code>agent_templates</code> (Phase A / ADR-021). Install = instantiate the template<br />  for the caller (a new agent with its own key, <strong>no secrets copied</strong>). Publish = share an agent you own.<br />- <strong>Addon listings</strong> are installable addons from the registry (Phase K / ADR-022). Install = enable the<br />  available addon. A ZIP upload is a separate flow (<code>POST /api/v1/addons/install</code>).</p>\n<h2>Model</h2>\n<p>- <strong>Listing</strong> = `{ id, kind:&#39;agent&#39;|&#39;addon&#39;, refId, title, description, category, authorUserId,<br />  visibility:&#39;shared&#39;, installs, featured, installed, pricing, purchased, createdAt, source }`.<br />  - <code>refId</code> is an <code>agent_templates.id</code> (agent) or an addon id (addon). <code>id</code> is synthetic<br />    (<code>agent:&lt;id&gt;</code> / <code>addon:&lt;id&gt;</code>), and a curated <code>mkt-&lt;uuid&gt;</code> id also resolves.<br />  - <code>installed</code> is computed <strong>live per caller</strong> (own shared template / enabled addon).<br />  - <code>pricing</code> = `{ pricingModel:&#39;free&#39;|&#39;one_time&#39;|&#39;subscription&#39;|&#39;both&#39;, priceMinor, subscriptionPriceMinor,<br />    subscriptionInterval, currency }<code> (money in **integer minor units**; </code>free` by default — no regression).<br />  - <code>purchased</code> = whether the <strong>caller</strong> holds a completed entitlement (buyer-scoped; always false for free).<br />- The addon owns ONE thin table <code>marketplace_listings</code> (curation metadata + install counter +<br />  monetization columns, <code>UNIQUE(kind, ref_id)</code>) and a per-buyer <code>marketplace_purchases</code> ledger<br />  (<code>user_id NOT NULL</code>, <code>UNIQUE(user_id, listing_id)</code>). It never copies the template payload or addon code —<br />  those stay in their Phase-A / Phase-K homes and are read live by <code>src/lib/catalog.ts</code>.</p>\n<h2>Monetization (#122, ADR-026 addendum)</h2>\n<p>- A listing can be <strong>paid</strong>: a one-time and/or subscription <strong>price</strong> + currency (<code>monetization.ts</code>,<br />  pure). Free stays the default. Money mirrors the <strong>payments</strong> addon convention (minor units + ISO-4217).<br />- A configurable <strong>platform revenue-share %</strong> (instance-wide admin setting, Aiopta addon-config) splits each<br />  paid transaction into a platform cut + seller payout (<code>computeRevenueSplit</code>, rounding-safe: cut + payout<br />  == gross exactly). The split is recorded per transaction on the purchase row.<br />- A <strong>purchase</strong> reuses the <strong>payments</strong> addon read-only (provider registry + payment-link store) — no<br />  Stripe/PayPal reimplemented. <strong>Owner creds are owner-secret (Block H):</strong> when payments is unconfigured<br />  the paid flow is <strong>gracefully inactive</strong> (<code>409</code> &quot;payments not configured&quot;), free listings + browsing<br />  always work. On success the buyer gets a (pending) entitlement; a paid listing must be purchased before<br />  it can be installed (<code>402</code> otherwise).<br />- <strong>Policy:</strong> publishing (free or paid) uses the existing <code>agent.create</code> gate; editing a listing&#39;s price is<br />  <strong>owner-scoped</strong> (seller or admin); changing the revenue-share % is <strong><code>mc.manage</code>-admin-gated</strong>.</p>\n<h2>Aggregation</h2>\n<p><code>listMarketplace(user, query)</code> merges <code>listVisibleTemplates(user)</code> (filtered to <code>visibility===&#39;shared&#39;</code>)<br />with <code>readInstalledAddonCatalog()</code> (excluding the marketplace itself), applies <code>q</code>/<code>category</code>/<code>kind</code><br />filters and <code>sort</code> (<code>featured|installs|recent|title</code>), and lets curated rows win over derived rows.</p>\n<h2>Safety</h2>\n<p>- <strong>Visibility-aware</strong>: only <code>shared</code> agent-templates are surfaced. **Another user&#39;s PRIVATE template is<br />  never exposed** — not in browse, and not by a guessed <code>agent:&lt;id&gt;</code> (detail/install resolve through the<br />  same function → 404).<br />- <strong>No secrets</strong>: publish reuses the Phase-A allow-list projection (<code>buildTemplateFromAgent</code>); install<br />  mints a new own key via the Phase-A instantiate route.<br />- <strong>No external calls</strong>: install dispatches to the existing primitive over the <strong>same-origin</strong> Aiopta API<br />  (<code>assertSameOrigin</code>); there is no container and no outbound request (ADR-009).<br />- <strong>RBAC</strong>: browsing is for any authed user; publishing a shared listing is <strong>owner/admin-gated</strong> (the<br />  same <code>agent.create</code> capability Phase-A uses for instance-wide sharing). Editing a listing&#39;s price is<br />  owner-scoped; the revenue-share % is <code>mc.manage</code>-admin-gated.<br />- <strong>Isolation</strong>: catalog <strong>reads are instance-wide by design</strong> (public catalog, not a leak); **listing<br />  writes<strong> (incl. pricing) are </strong>owner-scoped** (seller <code>user_id</code> stamped from <code>auth.id</code>);<br />  <strong>purchases/entitlements</strong> are <strong>buyer-scoped</strong> (<code>WHERE user_id</code>). All new raw-<code>getDb</code> access lives in the<br />  already-allowlisted <code>src/lib/store.ts</code>; no allowlist addition was needed.</p>\n<h2>Routes</h2>\n<p>Dual-registered in <code>addon.json</code> + <code>src/manifest.ts</code>:</p>\n<ul><li><code>GET /api/v1/addons/marketplace/status</code></li><li><code>GET /api/v1/addons/marketplace/listings?kind=&amp;category=&amp;q=&amp;sort=</code></li><li><code>GET /api/v1/addons/marketplace/listings/[id]</code></li><li><code>POST /api/v1/addons/marketplace/listings/[id]/install</code></li><li><code>POST /api/v1/addons/marketplace/listings/[id]/purchase</code> — buy a paid listing (#122)</li><li><code>PUT /api/v1/addons/marketplace/listings/[id]/pricing</code> — set a listing&#39;s price (owner/admin, #122)</li><li><code>GET</code> + <code>PUT /api/v1/addons/marketplace/revenue-share</code> — read / set the platform % (PUT admin-only, #122)</li><li><code>POST /api/v1/addons/marketplace/publish</code></li><li><code>GET /api/v1/addons/marketplace/widget</code></li></ul>\n<p>See <code>SKILL.md</code> for the agent-facing endpoint cheat-sheet, and <code>docs/architecture/ADR-026-marketplace.md</code><br />(incl. the monetization addendum) for the full decision. Paid listings are now supported (OPEN-DECISIONS<br />#33 resolved); cross-instance/remote-registry marketplace remains parked (OPEN-DECISIONS #32).</p>",
          "byteSize": 5640
        }
      }
    },
    {
      "id": "marketplace-automation",
      "uuid": "777c8b5f-044c-4a2a-a55c-1e256cb91f1a",
      "name": "Marketplace Automation",
      "version": "2.0.1",
      "description": "DEPRECATED — zusammengeführt in das kanonische Commerce-Addon `commerce` als Marketplace-Connector (ADR-008 Addon-Konsolidierung, Issue #591 / Epic #492). Die Publish-Pipeline + der Store sind als Feature-Modul unter `commerce/src/connectors/marketplace/` migriert: BYOK-Credentials in der gemeinsamen scopedStore-Basis, Alt-ID-Redirect `/addons/marketplace-automation` → `/addons/commerce?connector=marketplace`. Die Section-Kollision (Operations → Money) ist final aufgelöst: marketplace-automation ist als Connector in der einheitlichen commerce-Section. marketplace-automation bleibt bis zum Sunset (2026-12-31) voll funktionsfähig und datenerhaltend — kein Endpoint entfällt, keine Zeile wird kopiert/gelöscht. Ursprüngliche Fähigkeit: Manage marketplace accounts, create listing drafts, publish to supported portals, and track publish jobs from inside Aiopta.",
      "author": "Aiopta",
      "icon": "🛍️",
      "capabilities": [
        "commerce-marketplace",
        "marketplace-accounts",
        "listing-drafts",
        "listing-publish",
        "listing-tracking"
      ],
      "tags": [],
      "download": "/artifacts/marketplace-automation-2.0.1.zip",
      "sha256": "039681f3fe7490dec98188ac2d8958379f46d5bd52e2eac8912f78d8ed201402",
      "size": 72908,
      "page": {
        "id": "marketplace-automation",
        "name": "Marketplace Automation",
        "path": "/addons/marketplace-automation",
        "icon": "🛍️",
        "section": "Operations",
        "component": "@addons/marketplace-automation/src/page"
      },
      "deprecated": true,
      "replacedBy": "commerce",
      "deprecation": {
        "deprecated": true,
        "replacedBy": "commerce",
        "sunset": "2026-12-31",
        "successorPath": "/addons/commerce",
        "successorLabel": "Commerce (Nachfolger)"
      },
      "endpoints": [
        "/api/v1/addons/marketplace-automation/status",
        "/api/v1/addons/marketplace-automation/accounts",
        "/api/v1/addons/marketplace-automation/accounts/:id",
        "/api/v1/addons/marketplace-automation/listings",
        "/api/v1/addons/marketplace-automation/listings/:id",
        "/api/v1/addons/marketplace-automation/listings/:id/publish",
        "/api/v1/addons/marketplace-automation/jobs"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>🛍️ Marketplace Automation</h1>\n<p>Manage marketplace accounts, create listing drafts, publish to supported portals, and track publish jobs from inside Aiopta.</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>marketplace-automation</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> marketplace-accounts, listing-drafts, listing-publish, listing-tracking</li><li><strong>UI-Seite:</strong> <code>/addons/marketplace-automation</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET, POST, DELETE | <code>/api/v1/addons/marketplace-automation/accounts</code> |<br />| GET, PUT | <code>/api/v1/addons/marketplace-automation/accounts/[id]</code> |<br />| GET | <code>/api/v1/addons/marketplace-automation/jobs</code> |<br />| GET, POST | <code>/api/v1/addons/marketplace-automation/listings</code> |<br />| GET, PUT | <code>/api/v1/addons/marketplace-automation/listings/[id]</code> |<br />| POST | <code>/api/v1/addons/marketplace-automation/listings/[id]/publish</code> |<br />| GET | <code>/api/v1/addons/marketplace-automation/status</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<ul><li>Keine bekannten Einschränkungen.</li></ul>\n<blockquote><p>README aus <code>addon.json</code> + vorhandenen Routen generiert (<code>scripts/generate-addon-readmes.js</code>). Bei manuellen Ergänzungen den Generator-Hinweis entfernen.</p></blockquote>",
          "byteSize": 1197
        }
      }
    },
    {
      "id": "mcp",
      "uuid": "c0f71398-0eeb-4cf5-b840-a6b83c5879ac",
      "name": "MCP Servers",
      "version": "1.3.1",
      "description": "Model Context Protocol server management, runtime installation, tool discovery, agent tool execution, and per-user least-privilege tool policies (addons#178).",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "mcp",
        "tools",
        "servers",
        "agent-tools"
      ],
      "tags": [],
      "download": "/artifacts/mcp-1.3.1.zip",
      "sha256": "986a1426f93ba552b35b4c33d18968a35962c87c1d3ef69d9d5f53941e638cfc",
      "size": 381803,
      "page": {
        "id": "mcp",
        "path": "/addons/mcp",
        "name": "MCP Servers",
        "icon": "🔌",
        "section": "Admin",
        "component": "@addons/mcp/src/components/MCPPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/mcp/agent",
        "/api/v1/addons/mcp/deploy",
        "/api/v1/addons/mcp/servers",
        "/api/v1/addons/mcp/servers/{id}",
        "/api/v1/addons/mcp/servers/{id}/discover",
        "/api/v1/addons/mcp/servers/{id}/test-auth",
        "/api/v1/addons/mcp/servers/{id}/tool-policy",
        "/api/v1/addons/mcp/servers/{id}/tools",
        "/api/v1/addons/mcp/tools"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>MCP Servers</h1>\n<p>Model Context Protocol server management, runtime installation, tool discovery, and agent tool execution.</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>mcp</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> mcp, tools, servers, agent-tools</li><li><strong>UI-Seite:</strong> <code>/mcp</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Optionale BYOK-Credentials pro MCP-Server (<code>auth.token</code>). Diese sind <strong>pro Benutzer</strong> privat.</p>\n<h2>Remote-Transporte (#177)</h2>\n<p><code>type: &quot;http&quot;</code> bedeutet standardkonformes <strong>MCP Streamable HTTP</strong> (offizielles<br /><code>@modelcontextprotocol/sdk</code>). <code>type: &quot;sse&quot;</code> ist Legacy-SSE und nutzt<br /><code>SSEClientTransport</code> aus demselben SDK.</p>\n<p><code>source.url</code> ist <strong>der vollständige MCP-Endpunkt</strong> — inklusive einer etwaigen<br />Secret-Path-Komponente wie <code>http://HA-IP:9583/private_&lt;secret&gt;</code>. Es wird **kein<br />Pfadsegment** angehängt (<code>/health</code>, <code>/tools</code>, <code>/mcp</code> o.ä.). Der Endpunkt wird<br />nur getrimmt und als URL validiert.</p>\n<p>Unterstützte Auth-Modi:</p>\n<p>| <code>auth.type</code> | Verhalten |<br />|---|---|<br />| <code>none</code> | Kein <code>Authorization</code>-Header. |<br />| <code>token</code> (statischer Bearer) | <code>requestInit.headers.Authorization: Bearer &lt;token&gt;</code> — Token passiert ausschließlich diesen Pfad. |<br />| <code>oauth</code> (Authorization-Code-Flow) | <strong>Nicht unterstützt</strong> — saubere <code>not supported</code>-Fehlermeldung ohne Fallback. |</p>\n<p>Health = erfolgreicher MCP-<code>initialize</code>-Handshake am konfigurierten Endpunkt<br />(optional gefolgt von <code>tools/list</code>). Es gibt <strong>keinen</strong> proprietären <code>/health</code><br />Endpunkt mehr.</p>\n<p>Sicherheits-Disziplin:</p>\n<p>- Das Bearer-Token und der Secret-Path-Anteil der URL werden <strong>nie</strong> geloggt,<br />  <strong>nie</strong> in Fehlermeldungen gespiegelt und <strong>nie</strong> über die API zurückgegeben.<br />- Per <code>userId::serverId</code> existiert <strong>genau ein</strong> verbundener SDK-Client +<br />  Transport; zwei Nutzer mit demselben Server-Slug bleiben vollständig<br />  isoliert (ADR-054).</p>\n<h2>Daten-Isolation (#144 / ADR-054)</h2>\n<p>MCP-Server sind <strong>pro Benutzer</strong> isoliert (BYOK). Jeder registrierte Server — inklusive seines<br /><code>auth.token</code> — gehört ausschließlich dem anlegenden Benutzer und liegt in der user-gescopten<br />Tabelle <code>mcp_servers</code> (über <code>scopedStore(auth.id)</code>), <strong>nicht</strong> mehr in einer geteilten Datei.</p>\n<p>- Reads/Updates/Deletes sehen nur die <strong>eigenen</strong> Server; eine fremde Server-ID liefert <strong>404</strong>.<br />- Der rohe <code>auth.token</code> wird über die API <strong>nie</strong> zurückgegeben (maskiert als <code>••••••</code>) — auch nicht<br />  an den Besitzer.<br />- Die Agent-Tool-Ausführung (<code>POST /agent</code>) löst nur die Server des <strong>Agent-Besitzers</strong> auf; ein<br />  fremder Agent kann die BYOK-Server eines Benutzers nicht betreiben.<br />- Der mitgelieferte <code>config/mcp-servers.json</code> ist nur noch ein <strong>read-only Instanz-Katalog</strong> von<br />  Default-Server-Definitionen (ohne Secrets), aus dem die privaten Zeilen eines Benutzers beim<br />  ersten Zugriff geseedet werden (alle deaktiviert, ohne Token).</p>\n<h2>Least-Privilege-Tool-Profile (addons#178)</h2>\n<p><code>homeassistant-ai/ha-mcp</code> veröffentlicht 84 Werkzeuge; ohne Profile könnte ein Agent nach Eingabe<br />einer Secret-URL unmittelbar den gesamten schreibenden und teils destruktiven HA-Katalog<br />bedienen. Die MCP-Integration filtert daher jede Tool-Sicht und jeden <code>tools/call</code> durch die<br /><strong>Least-Privilege-Policy</strong> des Server-Owners, <strong>bevor</strong> der Aufruf an den MCP-Transport geht<br />(zweistufig: Discovery-Filter + Execution-Gate).</p>\n<h3>Policy-Modell</h3>\n<p>Pro <code>mcp_servers</code>-Zeile existiert eine Policy in der Spalte <code>tool_policy_json</code><br />(Default: <code>custom</code> + leere Listen = <strong>Default-Deny</strong>). Optional überschreibt eine zweite,<br />per-User Override in <code>mcp_tool_policies</code> (<code>scopedStore(userId)</code>) die Server-Konfig — der<br />Override <strong>ersetzt</strong> die Server-Policy (kein Merge, ADR-054).</p>\n<p>| Feld | Mögliche Werte | Bedeutung |<br />|---|---|---|<br />| <code>mode</code> | <code>custom</code> \\| <code>read-only</code> \\| <code>write</code> \\| <code>full</code> | Steuert die annotations-getriebene Auswahl. <code>custom</code> = nur Listen zählen. |<br />| <code>allowDestructive</code> | <code>boolean</code> | Nur unter <code>mode: &quot;full&quot;</code> wirksam. Öffnet destructive Tools. |<br />| <code>allowedTools</code> | <code>string[]</code> | Optionale Allowlist (zusätzlich zur Mode-Grenze; kann sie <strong>nicht</strong> erweitern). |<br />| <code>deniedTools</code> | <code>string[]</code> | Denylist — gewinnt immer gegen die Allowlist. |</p>\n<h3>Semantik pro Mode</h3>\n<p>| Mode | Sichtbar sind … | <code>destructiveHint: true</code> … |<br />|---|---|---|<br />| <code>custom</code> (Default) | nur <code>allowedTools</code> (ohne <code>deniedTools</code>) | wie der Mode bzw. die Liste es hergibt |<br />| <code>read-only</code> | Tools mit <code>annotations.readOnlyHint === true</code> | blockiert |<br />| <code>write</code> | Read-only + nicht-destruktive Write-Tools | blockiert |<br />| <code>full</code> | alle nicht-destruktiven Tools | blockiert — <code>allowDestructive: true</code> hebt es auf |<br />| _kein Mode / fehlende Annotation_ | _nichts_ (Default-Deny) | blockiert |</p>\n<h3>Vertrag (Edge-Cases)</h3>\n<p>- <strong>Default-Deny</strong>: leere / fehlende Policy → kein Tool ist sichtbar. Es gibt <strong>keinen</strong> stillen<br />  Default-Allow.<br />- <strong>Fehlende oder widersprüchliche Annotationen</strong> bleiben in jedem annotation-getriebenen Mode<br />  unsichtbar — ein Tool ohne <code>annotations</code> ist im <code>read-only</code>-, <code>write</code>- und <code>full</code>-Mode blockiert.<br />- <strong>Cache-Stale-State</strong>: ein bereits entdeckter Tool-Aufruf wird nach Policy-Verschärfung<br />  sofort blockiert — der Execution-Gate liest die Policy unmittelbar vor <code>tools/call</code> neu.<br />- <strong>Direkter Execute-Request</strong> auf ein blockiertes Tool ergibt 403 mit <code>MCP_TOOL_DENIED</code>,<br />  <strong>ohne</strong> Argumente, Token oder Secret-Path-URL in der Fehlermeldung.<br />- <strong>Per-User-Isolation</strong>: <code>mcp_tool_policies</code> ist <code>UNIQUE(user_id, server_id)</code> — zwei Nutzer<br />  mit demselben Slug haben distinkte Overrides. Eine Policy-Änderung von A berührt B nie.<br />- <strong>Legacy-Fallback</strong> (bestehende Server ohne <code>tool_policy_json</code>): die Spalte wird mit der<br />  &quot;legacy full&quot; Policy (<code>mode: &quot;full&quot;</code>, <code>allowDestructive: false</code>) back-gefüllt. Das<br />  HA-MCP-Onboarding MUSS dagegen explizit <code>read-only</code> schreiben.<br />- <strong>Race-Safety</strong>: das Lesen der effektiven Policy läuft in einer Transaktion; ein gleichzeitiger<br />  PATCH + callTool reißt den Zustand nicht.</p>\n<h3>HA-spezifische Sicherheitsregeln (Vorgabe für Onboarding)</h3>\n<p>- <code>ha_config_set_yaml</code>, <code>ha_write_file</code>, <code>ha_delete_file</code> sind <strong>immer</strong> Default-Deny.<br />  Sie tauchen ohne explizite Allowlist in keiner Mode-Sicht auf und werden am Gate<br />  durch <code>McpToolDeniedError</code> (kein Token, keine Argumente, kein Secret-Path) abgewiesen.<br />- <code>full + allowDestructive: true</code> erfordert in der UI eine zweite ausdrückliche Bestätigung<br />  (Tippen der Server-ID); die UI schaltet <code>allowDestructive</code> niemals stillschweigend frei.<br />- Der Status zeigt, ob <code>ha_manage_backup</code> bzw. die Upstream-Safety-Tools entdeckt wurden.</p>\n<h3>Per-User-Override (API)</h3>\n<p>| Methode | Pfad | Effekt |<br />|---|---|---|<br />| <code>GET</code> | <code>/api/v1/addons/mcp/servers/{id}/tool-policy</code> | Effective Policy + <code>source</code> (<code>user-override</code> \\| <code>server-config</code> \\| <code>default</code>) |<br />| <code>PUT</code> | <code>/api/v1/addons/mcp/servers/{id}/tool-policy</code> | Installiert / ersetzt den per-User Override (validiert). |<br />| <code>DELETE</code> | <code>/api/v1/addons/mcp/servers/{id}/tool-policy</code> | Entfernt den Override; Server-Konfig greift wieder. |</p>\n<p>Fremde Server-IDs liefern 404 (kein Existenz-Leak). <code>PUT</code> validiert <code>mode</code>, <code>allowDestructive</code><br />und die Listen — ein leerer Body ist 400, niemals Default-Deny-Fallback.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET, POST | <code>/api/v1/addons/mcp/agent</code> |<br />| POST | <code>/api/v1/addons/mcp/deploy</code> |<br />| GET, POST, PATCH | <code>/api/v1/addons/mcp/servers</code> |<br />| GET, DELETE, PATCH | <code>/api/v1/addons/mcp/servers/[id]</code> |<br />| GET, POST | <code>/api/v1/addons/mcp/servers/[id]/discover</code> |<br />| POST | <code>/api/v1/addons/mcp/servers/[id]/test-auth</code> |<br />| GET, PUT, DELETE | <code>/api/v1/addons/mcp/servers/[id]/tool-policy</code> |<br />| GET | <code>/api/v1/addons/mcp/servers/[id]/tools</code> |<br />| GET, POST | <code>/api/v1/addons/mcp/tools</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<p>- Der MCP-Tool-Schema-Cache (<code>config/mcp-tools-registry.json</code>) und der npm-Runtime-Installer sind<br />  instanzweit (geteilt) — sie enthalten keine Secrets; der Per-User-Zugriff wird in den Routen<br />  erzwungen (jeder Read/Sync ist auf die eigenen Server beschränkt).<br />- OAuth Authorization-Code-Flow ist nicht Bestandteil dieses Addons — ein entsprechender Server<br />  erhält eine <code>MCP_UNSUPPORTED_AUTH</code>-Fehlermeldung ohne Fallback.<br />- Das <code>config/mcp-servers.json</code> ist <strong>read-only</strong>; eine Änderung am dort hinterlegten<br />  <code>toolPolicy</code> wird beim nächsten Seeden in die per-User-Zeile übernommen. Das HA-MCP-<br />  Onboarding wird in einem separaten Issue ausgeliefert und schreibt <code>read-only</code> explizit.</p>",
          "byteSize": 8262
        }
      }
    },
    {
      "id": "media",
      "uuid": "5d497847-829e-4ae6-afb6-502c11699879",
      "name": "Media Server",
      "version": "1.0.1",
      "description": "Home media server management — Plex, Jellyfin, Sonarr, Radarr, qBittorrent, and more. Monitor what's playing, manage libraries, request new content, and control downloads.",
      "author": "Aiopta",
      "icon": "🎬",
      "capabilities": [
        "media",
        "streaming",
        "downloads",
        "plex",
        "jellyfin"
      ],
      "tags": [],
      "download": "/artifacts/media-1.0.1.zip",
      "sha256": "fbf251cb592deea03706e7d0a9f4b1e9f3827f6935d5f2ce65695c0718806499",
      "size": 331935,
      "page": {
        "path": "/addons/media",
        "name": "Media Server",
        "icon": "🎬",
        "section": "Media",
        "component": "@addons/media/src/components/MediaDashboard"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/media/status",
        "/api/v1/addons/media/sessions",
        "/api/v1/addons/media/libraries",
        "/api/v1/addons/media/search",
        "/api/v1/addons/media/request",
        "/api/v1/addons/media/blacklist",
        "/api/v1/addons/media/releases",
        "/api/v1/addons/media/download-queue",
        "/api/v1/addons/media/config",
        "/api/v1/addons/media/health",
        "/api/v1/addons/media/history"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>🎬 Media Server</h1>\n<p>Home media server management — Plex, Jellyfin, Sonarr, Radarr, qBittorrent, and more. Monitor what&#39;s playing, manage libraries, request new content, and control downloads.</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>media</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> media, streaming, downloads, plex, jellyfin</li><li><strong>UI-Seite:</strong> <code>/addons/media</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Dieses Addon benötigt folgende Secrets/Provider-Konfiguration (Settings → Credentials bzw. Provider-Store, <strong>nie</strong> in addon.json):</p>\n<ul><li><code>MEDIA_JELLYFIN_KEY</code></li><li><code>MEDIA_LIDARR_KEY</code></li><li><code>MEDIA_OVERSEERR_KEY</code></li><li><code>MEDIA_PLEX_TOKEN</code></li><li><code>MEDIA_PROWLARR_KEY</code></li><li><code>MEDIA_QBIT_PASSWORD</code></li><li><code>MEDIA_RADARR_KEY</code></li><li><code>MEDIA_READARR_KEY</code></li><li><code>MEDIA_SABNZBD_KEY</code></li><li><code>MEDIA_SONARR_KEY</code></li><li><code>MEDIA_TAUTULLI_KEY</code></li></ul>\n<p>Die ENV-Werte dienen nur als gemeinsame Fallback-Baseline. Die eigentliche Konfiguration (Service-URLs + BYOK-Secrets) wird <strong>pro Nutzer</strong> über <code>GET/POST /config</code> gepflegt.</p>\n<h2>Sicherheit / Per-User-Isolation (#136 / ADR-054)</h2>\n<p>Die Media-Konfiguration ist <strong>pro authentifiziertem Aiopta-Nutzer isoliert</strong>: Sie liegt in einer<br />user-gescopten Tabelle (<code>media_config</code>, <code>UNIQUE(user_id)</code>) über <code>scopedStore(auth.id)</code>, und jede<br /><code>MediaRegistry</code> (samt der daraus gebauten Service-Clients) wird <strong>pro Nutzer</strong> aufgebaut — kein<br />prozessweiter Singleton mehr. Konsequenzen:</p>\n<p>- <code>GET /config</code> maskiert <code>apiKey</code>/<code>token</code>/<code>password</code> standardmäßig. <code>GET /config?edit=true</code> liefert<br />  die Klartext-Secrets <strong>ausschließlich für die eigene Konfiguration</strong> des Aufrufers — niemals die<br />  eines anderen Nutzers.<br />- <code>POST /config</code> schreibt nur die eigene Zeile; ein Nutzer kann die Credentials eines anderen weder<br />  lesen noch überschreiben.<br />- Der frühere globale <code>data/media-config.json</code>-Blob wird beim ersten Zugriff einmalig in den<br />  Namespace des Seeding-Admins migriert und anschließend gelöscht.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| POST | <code>/api/v1/addons/media/auto-confirm-import</code> |<br />| GET, POST, DELETE | <code>/api/v1/addons/media/blacklist</code> |<br />| GET, POST | <code>/api/v1/addons/media/config</code> |<br />| PUT | <code>/api/v1/addons/media/config/auto-request</code> |<br />| GET | <code>/api/v1/addons/media/diagnose</code> |<br />| GET, DELETE | <code>/api/v1/addons/media/download-queue</code> |<br />| POST | <code>/api/v1/addons/media/download-queue/bulk-delete</code> |<br />| POST | <code>/api/v1/addons/media/fix</code> |<br />| GET | <code>/api/v1/addons/media/health</code> |<br />| GET | <code>/api/v1/addons/media/history</code> |<br />| GET | <code>/api/v1/addons/media/libraries</code> |<br />| POST | <code>/api/v1/addons/media/libraries/bulk-delete</code> |<br />| DELETE | <code>/api/v1/addons/media/libraries/items</code> |<br />| POST | <code>/api/v1/addons/media/libraries/refresh</code> |<br />| POST | <code>/api/v1/addons/media/libraries/scan</code> |<br />| PUT | <code>/api/v1/addons/media/monitor</code> |<br />| GET | <code>/api/v1/addons/media/popular</code> |<br />| GET | <code>/api/v1/addons/media/propose-import</code> |<br />| GET | <code>/api/v1/addons/media/releases</code> |<br />| POST | <code>/api/v1/addons/media/releases/add-language-priority</code> |<br />| GET | <code>/api/v1/addons/media/releases/detail</code> |<br />| POST | <code>/api/v1/addons/media/releases/grab</code> |<br />| GET, POST, DELETE | <code>/api/v1/addons/media/releases/language-profile</code> |<br />| POST | <code>/api/v1/addons/media/releases/search</code> |<br />| GET, POST | <code>/api/v1/addons/media/releases/search-custom</code> |<br />| POST | <code>/api/v1/addons/media/releases/upgrade-language</code> |<br />| POST | <code>/api/v1/addons/media/request</code> |<br />| GET, PUT, DELETE | <code>/api/v1/addons/media/requests</code> |<br />| POST | <code>/api/v1/addons/media/retry-import</code> |<br />| GET | <code>/api/v1/addons/media/search</code> |<br />| GET | <code>/api/v1/addons/media/search/advanced</code> |<br />| GET | <code>/api/v1/addons/media/sessions</code> |<br />| POST | <code>/api/v1/addons/media/sessions/terminate</code> |<br />| GET | <code>/api/v1/addons/media/stats/downloads</code> |<br />| GET | <code>/api/v1/addons/media/stats/requests-stats</code> |<br />| GET | <code>/api/v1/addons/media/stats/storage</code> |<br />| GET | <code>/api/v1/addons/media/stats/watch</code> |<br />| GET | <code>/api/v1/addons/media/status</code> |<br />| GET | <code>/api/v1/addons/media/trending</code> |<br />| GET | <code>/api/v1/addons/media/upcoming</code> |<br />| POST, DELETE | <code>/api/v1/addons/media/users/ban</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<ul><li>Keine bekannten Einschränkungen.</li></ul>\n<blockquote><p>README aus <code>addon.json</code> + vorhandenen Routen generiert (<code>scripts/generate-addon-readmes.js</code>). Bei manuellen Ergänzungen den Generator-Hinweis entfernen.</p></blockquote>",
          "byteSize": 4122
        }
      }
    },
    {
      "id": "membership",
      "uuid": "56d99555-98da-45fe-be6c-7040dd69466c",
      "name": "Membership (Mitglieder/Abos/Paywall/Kurse/E-Books)",
      "version": "1.0.1",
      "description": "Per-User Mitgliedschaften: Mitglieder, Abo-Stufen mit Entitlement-Bündeln, Subscriptions (Paywall-Autorität), Kurse und E-Books. Integriert lesend mit dem payments-Addon (ein Abo darf die billing-Subscription referenzieren), ohne payments zu verändern. Lokale SQLite ist die Quelle der Wahrheit (ADR-054).",
      "author": "Aiopta",
      "icon": "🎟️",
      "capabilities": [
        "membership",
        "members",
        "plans",
        "subscriptions",
        "paywall",
        "entitlements",
        "courses",
        "ebooks"
      ],
      "tags": [],
      "download": "/artifacts/membership-1.0.1.zip",
      "sha256": "2c3cc345192ad87505a2f08ac234cb5f8679d05d709d526a248a41bb72b1872f",
      "size": 114755,
      "page": {
        "id": "membership-page",
        "path": "/addons/membership",
        "name": "Membership",
        "icon": "🎟️",
        "section": "Money",
        "component": "@addons/membership/src/components/MembershipPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/membership/members",
        "/api/v1/addons/membership/members/:id",
        "/api/v1/addons/membership/plans",
        "/api/v1/addons/membership/plans/:id",
        "/api/v1/addons/membership/subscriptions",
        "/api/v1/addons/membership/subscriptions/:id",
        "/api/v1/addons/membership/courses",
        "/api/v1/addons/membership/courses/:id",
        "/api/v1/addons/membership/ebooks",
        "/api/v1/addons/membership/ebooks/:id",
        "/api/v1/addons/membership/entitlements"
      ],
      "skills": [
        {
          "name": "membership-read",
          "displayName": "Membership: Mitglieder, Abos & Inhalte lesen",
          "description": "Listet Mitglieder, Abo-Stufen, Subscriptions, Kurse und E-Books für den Nutzer und prüft den Entitlement-/Paywall-Status. Reiner Lesezugriff — legt nichts an.",
          "permissions": [
            "addons/membership/members",
            "addons/membership/plans",
            "addons/membership/subscriptions",
            "addons/membership/courses",
            "addons/membership/ebooks",
            "addons/membership/entitlements"
          ],
          "endpoint": "/api/v1/addons/membership/entitlements",
          "scripts": []
        },
        {
          "name": "membership-manage",
          "displayName": "Membership: Mitglieder & Abos verwalten",
          "description": "Legt Mitglieder, Abo-Stufen und Subscriptions an oder entfernt diese (per-User gebucht). Setzt den Subscription-Status, auf dem die Paywall entscheidet.",
          "permissions": [
            "addons/membership/members",
            "addons/membership/plans",
            "addons/membership/subscriptions"
          ],
          "endpoint": "/api/v1/addons/membership/subscriptions",
          "scripts": []
        },
        {
          "name": "membership-content",
          "displayName": "Membership: Kurse & E-Books verwalten",
          "description": "Legt Kurse und E-Books im geschützten Inhaltskatalog des Nutzers an oder entfernt diese (mit erforderlichem Entitlement).",
          "permissions": [
            "addons/membership/courses",
            "addons/membership/ebooks"
          ],
          "endpoint": "/api/v1/addons/membership/courses",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>🎟️ Membership (Mitglieder/Abos/Paywall/Kurse/E-Books)</h1>\n<p>Per-User Mitgliedschaften: Mitglieder, Abo-Stufen mit Entitlement-Bündeln, Subscriptions (Paywall-Autorität), Kurse und E-Books. Integriert lesend mit dem payments-Addon (ein Abo darf die billing-Subscription referenzieren, ohne payments zu verändern). Lokale SQLite ist die Quelle der Wahrheit (ADR-054).</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>membership</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> membership, members, plans, subscriptions, paywall, entitlements, courses, ebooks</li><li><strong>UI-Seite:</strong> <code>/addons/membership</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials. Die<br />payments-Integration ist optional und degradiert graceful, falls payments nicht<br />installiert/verfügbar ist.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET, POST | <code>/api/v1/addons/membership/members</code> |<br />| DELETE | <code>/api/v1/addons/membership/members/[id]</code> |<br />| GET, POST | <code>/api/v1/addons/membership/plans</code> |<br />| DELETE | <code>/api/v1/addons/membership/plans/[id]</code> |<br />| GET, POST | <code>/api/v1/addons/membership/subscriptions</code> |<br />| PATCH, DELETE | <code>/api/v1/addons/membership/subscriptions/[id]</code> |<br />| GET, POST | <code>/api/v1/addons/membership/courses</code> |<br />| DELETE | <code>/api/v1/addons/membership/courses/[id]</code> |<br />| GET, POST | <code>/api/v1/addons/membership/ebooks</code> |<br />| DELETE | <code>/api/v1/addons/membership/ebooks/[id]</code> |<br />| GET | <code>/api/v1/addons/membership/entitlements</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<ul><li>Der Paywall-Entscheid basiert rein auf dem lokalen Subscription-Status; payments wird nur zur Billing-Anreicherung gelesen.</li></ul>\n<blockquote><p>README aus <code>addon.json</code> + vorhandenen Routen generiert (<code>scripts/generate-addon-readmes.js</code>). Bei manuellen Ergänzungen den Generator-Hinweis entfernen.</p></blockquote>",
          "byteSize": 1730
        }
      }
    },
    {
      "id": "memories",
      "uuid": "477f94c5-8620-46fc-be34-c9ce4de3d8ba",
      "name": "Memories",
      "version": "1.5.0",
      "description": "Long-term memories, memory search, memory group sharing, and a per-user knowledge-map graph.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "memories",
        "memory-groups",
        "search",
        "sharing",
        "knowledge-map",
        "memory-loop"
      ],
      "tags": [],
      "download": "/artifacts/memories-1.5.0.zip",
      "sha256": "28a3c882fda3625d75b8d4441e439269e42490a79ddfb1bf4bf1deb162ad4d26",
      "size": 357397,
      "page": [
        {
          "id": "memories",
          "path": "/addons/memories",
          "name": "Memories",
          "icon": "🧠",
          "section": "Data",
          "component": "@addons/memories/src/components/memories/MemoryList"
        },
        {
          "id": "memories-graph",
          "path": "/addons/memories/graph",
          "name": "Knowledge Map",
          "icon": "🕸️",
          "section": "Data",
          "component": "@addons/memories/src/components/memories/MemoryGraph"
        },
        {
          "id": "memories-loop",
          "path": "/addons/memories/agent-loop",
          "name": "Memory-Loop",
          "icon": "♻️",
          "section": "Data",
          "component": "@addons/memories/src/components/memories/MemoryLoopSettings"
        }
      ],
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/memories",
        "/api/v1/addons/memories/{id}",
        "/api/v1/addons/memories/search",
        "/api/v1/addons/memories/graph",
        "/api/v1/addons/memories/memory-groups",
        "/api/v1/addons/memories/memory-groups/{id}",
        "/api/v1/addons/memories/memory-groups/{id}/members",
        "/api/v1/addons/memories/memory-groups/{id}/memories",
        "/api/v1/addons/memories/memory-groups/share",
        "/api/v1/addons/memories/memory-groups/visible",
        "/api/v1/addons/memories/agent-loop/settings",
        "/api/v1/addons/memories/agent-loop/sync"
      ],
      "skills": [],
      "mcpServers": [
        {
          "id": "memories-tools",
          "displayName": "Memories tools",
          "description": "Read-only knowledge-map math for agents: extract [[name]] cross-links and assemble the memory/group knowledge graph (nodes, dedup edges, membership) — the same pure builder the memories knowledge-map uses (issue #429, epic #399). Reads nothing external.",
          "transport": "stdio",
          "runtime": "node",
          "entry": "memories-tools/server.mjs",
          "permissions": [
            "addons/memories/read"
          ],
          "toolPolicy": {
            "mode": "read-only",
            "allowDestructive": false
          },
          "configRefs": [],
          "credentialRefs": [],
          "archiveEntry": "memories/mcp/servers/memories-tools/server.mjs",
          "sha256": "a2eaa5b2d66fba6274d77e1fdcb8cf3167f7750308c2ce6a0d508d8540812c9b",
          "size": 16130,
          "descriptorVersion": 1
        }
      ],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Memories</h1>\n<p>Long-term memories, memory search, memory group sharing, and a per-user knowledge-map graph.</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>memories</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> memories, memory-groups, search, sharing, knowledge-map, memory-loop</li><li><strong>UI-Seiten:</strong> <code>/memories</code> (Liste), <code>/memories/graph</code> (Wissenskarte), <code>/memories/agent-loop</code> (Memory-Loop-Einstellungen)</li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET, POST | <code>/api/v1/addons/memories</code> |<br />| GET, PUT, DELETE | <code>/api/v1/addons/memories/[id]</code> |<br />| GET, POST | <code>/api/v1/addons/memories/memory-groups</code> |<br />| GET, PUT, DELETE | <code>/api/v1/addons/memories/memory-groups/[id]</code> |<br />| GET, POST, DELETE | <code>/api/v1/addons/memories/memory-groups/[id]/members</code> |<br />| GET, POST, DELETE | <code>/api/v1/addons/memories/memory-groups/[id]/memories</code> |<br />| POST | <code>/api/v1/addons/memories/memory-groups/share</code> |<br />| GET | <code>/api/v1/addons/memories/memory-groups/visible</code> |<br />| GET | <code>/api/v1/addons/memories/search</code> |<br />| GET | <code>/api/v1/addons/memories/graph</code> |<br />| GET, POST | <code>/api/v1/addons/memories/agent-loop/settings</code> |<br />| POST | <code>/api/v1/addons/memories/agent-loop/sync</code> |</p>\n<h2>Wissenskarte (Knowledge-Map, #146 / INBOX-69)</h2>\n<p>Die UI-Seite <code>/memories/graph</code> rendert die eigenen Erinnerungen + Memory-Groups als interaktiven<br />Graphen (SVG, keine zusätzliche Abhängigkeit — force-directed Layout):</p>\n<p>- <strong>Knoten</strong> = Erinnerungen (violett, Größe ∝ <code>importance</code>) und Memory-Groups (bernstein).<br />- <strong>Kanten</strong> = <code>[[name]]</code>-Querverweise zwischen Erinnerungen (durchgezogen, titel-aufgelöst,<br />  case-insensitiv, ungerichtet entdoppelt) sowie Memory↔Group-Zugehörigkeit aus<br />  <code>memory_group_memories</code> (gestrichelt).<br />- <strong>Interaktion</strong> = Zoom (Scrollen), Pan (Hintergrund ziehen), Klick auf einen Knoten öffnet die<br />  Erinnerung (Detail-Panel via scoped <code>[id]</code>-Route) bzw. fokussiert auf eine Gruppe; Filter nach Gruppe.</p>\n<p>Die Graph-Daten kommen ausschließlich aus den <strong>bereits pro-Nutzer-isolierten</strong> Stores<br />(<code>listMemories</code>/<code>listGroups</code>/<code>getGroupMemories</code>, alle auf <code>auth.id</code> gescoped) — <code>GET …/graph</code> ist<br /><code>userRoute</code>-gewrappt. Ein <code>[[name]]</code> kann daher nur auf eine <strong>eigene</strong> Erinnerung auflösen, nie auf die<br />eines anderen Tenants.</p>\n<h2>Memory-Loop (#149 / INBOX-72, ADR-058)</h2>\n<p>Agenten schreiben ihre Outputs/Insights <strong>automatisch</strong> als Erinnerungen zurück (Contributor-Pattern)<br />— <strong>opt-in, standardmäßig AUS</strong>, mit optionalem Obsidian-Export. Die UI-Seite <code>/memories/agent-loop</code><br />steuert die per-Nutzer-Einstellung.</p>\n<p>- <strong>Event-getrieben, zweiter Subscriber:</strong> Das Addon abonniert das kanonische Event<br />  <code>agent.output.produced</code> (ADR-053, eingeführt in #148) ein <strong>zweites Mal, unabhängig</strong> vom<br />  artefakt-workspace — derselbe Event erzeugt sowohl ein Artefakt (#148) als auch eine Erinnerung,<br />  ohne Kopplung. Abo lazy+idempotent im <code>install()</code> (globalThis-Singleton, kein Boot-Hook).<br />- <strong>Ownership (ADR-054):</strong> Der Besitzer kommt IMMER aus dem Event-Payload (<code>userId</code>/<code>user_id</code>/<code>ownerId</code>),<br />  NIE aus einem Client-Wert; ein Event ohne erkennbaren Nutzer wird <strong>übersprungen</strong> (geloggt), nicht<br />  geraten. Der Write läuft über den per-Nutzer-<code>createMemory</code>-Store ⇒ <code>user_id</code> server-seitig gestempelt.<br />- <strong>Contributor-Provenance:</strong> Jede agent-erzeugte Erinnerung ist von menschlichen unterscheidbar —<br />  Kategorie <code>agent-contributed</code>, Tags <code>agent-contributed</code> + <code>agent:&lt;id&gt;</code> + <code>event:&lt;id&gt;</code>, plus<br />  Provenienz-Footer im Body (Quell-Agent + Source-Event).<br />- <strong>Opt-in (default OFF):</strong> per-Nutzer-Einstellung in <code>addon_settings</code> (<code>addon_id=&#39;memories-loop&#39;</code>,<br />  über <code>scopedStore</code>); ohne explizites <code>enabled: true</code> wird <strong>nichts</strong> geschrieben. Optionaler<br />  per-Agent-Allowlist (leer = alle Agenten).<br />- <strong>Optionaler Obsidian-Sync (default OFF):</strong> exportiert jede Erinnerung als eine Markdown-Datei<br />  (Frontmatter + Body + native <code>[[links]]</code>) unter <code>resolveUserPath(user,&#39;memories&#39;,&#39;obsidian&#39;,...)</code><br />  ⇒ <code>&lt;DATA_DIR&gt;/addons/memories/&lt;userId&gt;/obsidian</code> (containment-checked; ein <code>../</code>-Escape wird mit<br />  403 abgewiesen). Manueller Re-Export via <code>POST …/agent-loop/sync</code> (nur wenn aktiviert).</p>\n<h2>Sicherheit / Isolation (#137, ADR-054)</h2>\n<p>Alle Daten sind <strong>pro Nutzer isoliert</strong> (default-deny über die <code>scopedStore</code>-Foundation):</p>\n<p>- <code>agent_memories</code>, <code>memory_groups</code>, <code>memory_group_members</code> und <code>memory_group_memories</code> tragen je eine<br />  <code>user_id</code>-Spalte (<code>NOT NULL</code>). Der Server stempelt <code>user_id = auth.id</code> bei jedem Insert — ein<br />  client-gelieferter <code>agent_id</code>/<code>created_by</code>/<code>shared_by</code> ist <strong>nur ein gruppierender Schlüssel</strong>, nie die<br />  Sicherheitsgrenze.<br />- Jeder Read/Update/Delete ist auf den aufrufenden Nutzer gescoped. Eine fremde oder unbekannte <code>{id}</code><br />  liefert <strong>404</strong> (nie die Zeile eines anderen Nutzers, nie 403). Die <code>[id]</code>-Routen laufen über<br />  <code>userRoute</code>/<code>ownedResourceRoute</code> — eine unauthentifizierte Anfrage wird mit 401 abgewiesen (der<br />  frühere no-auth-IDOR ist geschlossen).<br />- Teilen funktioniert nur innerhalb des eigenen Besitzes: man kann ausschließlich eine <strong>eigene</strong> Memory<br />  in eine <strong>eigene</strong> Gruppe teilen.</p>\n<h2>Bekannte Grenzen</h2>\n<ul><li>Memory-Groups sind pro Nutzer; ein nutzerübergreifendes Teilen existiert bewusst nicht.</li></ul>",
          "byteSize": 5199
        }
      }
    },
    {
      "id": "monitoring",
      "uuid": "008f1300-57c6-495e-9b1a-c6d79ac14c2c",
      "name": "Observability (Host-Metriken Glances + Uptime-Kuma + Alerts + Error-Tracking)",
      "version": "1.0.4",
      "description": "Per-User Observability: verbindet sich BYOK mit Glances (Host-Metriken CPU/RAM/Disk/Net) und Uptime-Kuma (Uptime-Checks up/down/Latenz). Dashboard für Host-Metriken, Uptime-Status und konfigurierbare Alert-Regeln (Schwellwert → Vorfall/Notification). Jeder Nutzer verwaltet seine eigenen Connections, Alert-Regeln und Incident-Historie (ADR-054 scopedStore). Die Clients sind graziös, wenn nichts konfiguriert ist. Kanonisches Observability-Addon (ADR-008 / #495, Epic #490): Client-/Server-Error-Tracking (vormals `error-monitoring`) ist als Errors-Domänen-Connector desselben geteilten Observability-Datenmodells hier konsolidiert.",
      "author": "Aiopta",
      "icon": "📡",
      "capabilities": [
        "monitoring",
        "metrics",
        "uptime",
        "alerts",
        "glances",
        "uptime-kuma",
        "incident-log",
        "observability",
        "observability-hub",
        "error-connector"
      ],
      "tags": [],
      "download": "/artifacts/monitoring-1.0.4.zip",
      "sha256": "e7454857dfc43d50233b1f696633b688f19035fc4bcb7c12c6ad36ea4ca00c2e",
      "size": 108277,
      "page": {
        "id": "monitoring-page",
        "path": "/addons/monitoring",
        "name": "Monitoring",
        "icon": "📡",
        "section": "Operations",
        "component": "@addons/monitoring/src/components/MonitoringPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/monitoring/config",
        "/api/v1/addons/monitoring/metrics",
        "/api/v1/addons/monitoring/uptime",
        "/api/v1/addons/monitoring/alerts",
        "/api/v1/addons/monitoring/alerts/:id",
        "/api/v1/addons/monitoring/uptime-checks",
        "/api/v1/addons/monitoring/uptime-checks/:id"
      ],
      "skills": [
        {
          "name": "monitoring-view",
          "displayName": "Monitoring: Metriken & Uptime-Status lesen",
          "description": "Zeigt Host-Metriken (CPU/RAM/Disk/Netz aus Glances) und den Uptime-Status (up/down/Latenz aus Uptime-Kuma) für die konfigurierten Connections des Nutzers. Reiner Lesezugriff — legt nichts an.",
          "permissions": [
            "addons/monitoring/metrics",
            "addons/monitoring/uptime",
            "addons/monitoring/incident-log"
          ],
          "endpoint": "/api/v1/addons/monitoring/metrics",
          "scripts": []
        },
        {
          "name": "monitoring-manage",
          "displayName": "Monitoring: Connections & Alert-Regeln verwalten",
          "description": "Legt BYOK-Connections (Glances/Uptime-Kuma) und Alert-Regeln (Schwellwert → Vorfall) für den Nutzer an oder entfernt diese (per-User gebucht). Konfiguration wird im scopedStore abgelegt.",
          "permissions": [
            "addons/monitoring/config",
            "addons/monitoring/alerts",
            "addons/monitoring/uptime-checks"
          ],
          "endpoint": "/api/v1/addons/monitoring/config",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": null
    },
    {
      "id": "moto-routeplanner",
      "uuid": "4de4b514-2723-455c-9b53-c37b2953f16e",
      "name": "Moto Route Planner (GPX)",
      "version": "1.1.1",
      "description": "Plan motorcycle tours — round-trip or one-way, with waypoints, by target distance or duration, with a curviness level, avoid-highway (or highway-until-X then curvy/passes), prefer-passes — and export a valid GPX 1.1 for your navi (Garmin/TomTom/Calimoto). v1.1 adds POIs along the route (fuel/food/viewpoint via OpenStreetMap/Overpass), multi-day tours split into day stages with one GPX per stage, and an elevation profile (Höhenprofil). Routing runs on an MC-managed BRouter container (ADR-009, reached only via the internal URL — no hard-coded host) with an owner-keyed GraphHopper/ORS Directions API fallback; when neither is configured, plan calls return a clear 503. The fallback + Overpass API keys + BRouter rd5 region/disk are owner-config (OPEN-DECISIONS), never the manifest. ADR-037.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "route-planning",
        "gpx-export",
        "roundtrip-generation",
        "curvy-routing",
        "pois-along-route",
        "multi-day-stages",
        "elevation-profile"
      ],
      "tags": [],
      "download": "/artifacts/moto-routeplanner-1.1.1.zip",
      "sha256": "042f7452387e8eaa80994a76c532b8d668fc7ec7e7dfe0da6d1641d457e0e1ee",
      "size": 249864,
      "page": {
        "id": "moto-routeplanner-page",
        "path": "/addons/moto-routeplanner",
        "name": "Moto Route Planner",
        "icon": "🏍️",
        "section": "Tools",
        "component": "@addons/moto-routeplanner/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/moto-routeplanner/status",
        "/api/v1/addons/moto-routeplanner/plan",
        "/api/v1/addons/moto-routeplanner/tours",
        "/api/v1/addons/moto-routeplanner/tours/:id",
        "/api/v1/addons/moto-routeplanner/tours/:id/gpx",
        "/api/v1/addons/moto-routeplanner/tours/:id/stages",
        "/api/v1/addons/moto-routeplanner/tours/:id/stages/:stage/gpx",
        "/api/v1/addons/moto-routeplanner/widget"
      ],
      "skills": [
        {
          "name": "moto-plan-tour",
          "displayName": "Moto: Plan a tour → GPX",
          "description": "Plan a motorcycle tour from parameters (start/destination/waypoints, round-trip vs one-way, curviness, avoid-highway or highway-until-X, target distance/duration, prefer-passes) and get the routed geometry + a valid GPX 1.1. Routing uses the MC-managed BRouter container with an owner-keyed fallback; if neither is configured the call returns a clear 503 (never a hard-coded host).",
          "permissions": [
            "addons/moto-routeplanner/plan",
            "addons/moto-routeplanner/status"
          ],
          "endpoint": "/api/v1/addons/moto-routeplanner/plan",
          "scripts": []
        },
        {
          "name": "moto-tours",
          "displayName": "Moto: Save & list tours",
          "description": "Plan-and-save tours, list saved tours, fetch one (params + GPX), and download the GPX. Saved tours live in an addon-local table; the GPX download is a pure read (no re-routing).",
          "permissions": [
            "addons/moto-routeplanner/tours",
            "addons/moto-routeplanner/status"
          ],
          "endpoint": "/api/v1/addons/moto-routeplanner/tours",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Moto Route Planner (GPX) — Aiopta addon</h1>\n<p>Plan motorcycle tours and export a <strong>GPX 1.1</strong> file for your navi (Garmin /<br />TomTom / Calimoto). Round-trip or one-way, with waypoints, sized by target<br />distance or duration, with a <strong>curviness</strong> level, <strong>avoid-highway</strong> (or<br />&quot;highway-until-X, then curvy/passes&quot;), and <strong>prefer-passes</strong>. Built on ADR-037.</p>\n<p><strong>v1.1</strong> adds: <strong>POIs along the route</strong> (fuel/food/viewpoint… from<br />OpenStreetMap/Overpass, kept only inside a corridor around the planned line and<br />emitted as GPX waypoints with navi symbols), <strong>multi-day tours</strong> split into day<br /><strong>stages</strong> with <strong>one GPX per stage</strong> (download a single day), and an **elevation<br />profile** (Höhenprofil — ascent/descent/min/max + a distance↔elevation series).</p>\n<h2>How it works</h2>\n<p>- <strong>Routing engine = BRouter container (ADR-009).</strong> BRouter is an open-source<br />  router ideal for curvy moto routes (it avoids motorways and biases toward<br />  winding roads / passes via <em>profiles</em>). The addon declares it as an Aiopta-managed<br />  container, booted at install, and reaches it <strong>only</strong> via the Aiopta-internal URL —<br />  never a hard-coded host (<code>src/lib/brouter.ts</code> →<br />  <code>getContainerAddonInternalUrl(&quot;moto-routeplanner&quot;)</code>).<br />- <strong>Fallback.</strong> When the container is absent, an optional owner-keyed<br />  <strong>GraphHopper / OpenRouteService</strong> Directions API serves the route<br />  (<code>ROUTING_FALLBACK_API_KEY</code> in Aiopta addon-config — sent server-side, never<br />  logged). If <strong>neither</strong> is available, plan calls return a clear <strong>503</strong> and<br />  <code>/status</code> reports it; the addon never crashes and never calls a default host.<br />- <strong>Planning is pure + tested.</strong> Parameters → a BRouter profile (<code>profile.ts</code>),<br />  round-trip loops generated by a documented waypoint-ring heuristic<br />  (<code>roundtrip.ts</code>), and a hand-rolled GPX 1.1 serializer (<code>gpx.ts</code>). No routing or<br />  GPX npm dependency is added.<br />- <strong>v1.1 POIs along the route.</strong> A keyless <strong>Overpass</strong> (OpenStreetMap) query<br />  (<code>overpass.ts</code>, the only POI I/O — server-side, owner-overridable endpoint, any<br />  private-mirror key sent as a header and never logged) fetches candidates in the<br />  track&#39;s bounding box; a <strong>pure corridor filter</strong> (<code>poi.ts</code>) keeps only those<br />  within <code>poiCorridorM</code> of the line, annotates offset + along-route distance, and<br />  emits them as GPX <code>&lt;wpt&gt;</code>s with category symbols. A POI-source outage is<br />  best-effort: the route + GPX still come back (with a <code>poiError</code> note).<br />- <strong>v1.1 multi-day stages + elevation.</strong> A <strong>pure</strong> splitter (<code>stages.ts</code>) cuts<br />  the routed track into day stages near a <code>perDayKm</code> target (or a fixed<br />  <code>stageCount</code>) — contiguous slices sharing each boundary point — and each stage is<br />  serialized to <strong>its own GPX</strong>. A <strong>pure</strong> profiler (<code>elevation.ts</code>) turns the<br />  track&#39;s elevations into a Höhenprofil (jitter-suppressed ascent/descent, min/max,<br />  max grade, a down-sampled distance↔elevation series). Saved tours re-derive<br />  stages/elevation from the stored GPX (a tolerant <code>parseGpxTrackPoints</code>) with no<br />  re-routing.</p>\n<h2>Parameters</h2>\n<p><code>start</code> (required), optional <code>destination</code> (required for one-way; defaults to the<br />start for a round-trip) and <code>waypoints</code>; <code>tourType</code> (<code>round-trip</code> / <code>one-way</code>);<br /><code>curviness</code> (<code>straight</code> / <code>balanced</code> / <code>curvy</code> / <code>very-curvy</code>); <code>highwayMode</code><br />(<code>avoid</code> / <code>allow</code> / <code>until-waypoint</code> + <code>highwayUntilIndex</code>); <code>targetDistanceKm</code><br /><strong>or</strong> <code>targetDurationMin</code> (round-trip sizing); <code>preferPasses</code>; <code>name</code>, <code>save</code>.</p>\n<p><strong>v1.1:</strong> <code>includePois</code> + <code>poiCategories</code><br />(<code>fuel</code>/<code>food</code>/<code>cafe</code>/<code>viewpoint</code>/<code>attraction</code>/<code>hotel</code>/<code>repair</code>) + <code>poiCorridorM</code>;<br /><code>multiDay</code> + <code>perDayKm</code> <strong>or</strong> <code>stageCount</code>. <code>POST /plan</code> then also returns <code>pois</code>,<br /><code>poiError</code>, <code>elevation</code> (<code>samples</code> + <code>stats</code>) and, when <code>multiDay</code>, <code>stages</code> (with<br />a per-stage <code>gpx</code>).</p>\n<p>The acceptance case — <strong>a curvy round-trip by target distance, no highway</strong> —<br />produces a valid, downloadable GPX. Curviness and highway mode demonstrably change<br />the BRouter profile that reaches the engine.</p>\n<h2>API (agent-capable)</h2>\n<p>Base: <code>/api/v1/addons/moto-routeplanner</code>. All routes are <code>userRoute</code>-auth-gated;<br />the per-agent capability is <code>addons/moto-routeplanner/*</code>.</p>\n<p>| Method + path | Purpose |<br />|---------------|---------|<br />| <code>GET /status</code> | Engine/container availability + saved-tour count |<br />| <code>POST /plan</code> | Plan a tour → route + GPX (optional <code>save:true</code>) |<br />| <code>GET /tours</code> | List saved tours |<br />| <code>POST /tours</code> | Plan <strong>and save</strong> a tour |<br />| <code>GET /tours/{id}</code> | Fetch one saved tour (params + GPX + elevation profile) |<br />| <code>GET /tours/{id}/gpx</code> | Download the GPX (<code>application/gpx+xml</code>) |<br />| <code>GET /tours/{id}/stages</code> | <strong>v1.1</strong> List the saved tour&#39;s day stages |<br />| <code>GET /tours/{id}/stages/{n}/gpx</code> | <strong>v1.1</strong> Download one stage&#39;s GPX (<code>application/gpx+xml</code>) |<br />| <code>DELETE /tours/{id}</code> | Delete a saved tour |</p>\n<p>Saved tours live in <strong>one addon-local table</strong> (<code>moto_routeplanner_tours</code>), created<br />in <code>install()</code> and dropped in <code>uninstall()</code> — no core-schema impact.</p>\n<p><strong>Per-user isolation (#147 / ADR-054):</strong> <code>moto_routeplanner_tours</code> carries <code>user_id</code>;<br />the server stamps the owner from the authenticated caller on save (a client-supplied<br />owner is ignored), and every list/read/GPX-download/delete is scoped to the caller via<br /><code>scopedStore</code>. A tour id owned by another user returns <strong>404</strong> (no existence leak) — you<br />only ever see and act on your own saved tours, and the widget&#39;s &quot;latest&quot; is your own.</p>\n<h2>Page + widget</h2>\n<p>A management page (<code>Tools</code> section) sets start/destination/waypoints + curviness,<br />highway and target-distance controls + a round-trip toggle and prefer-passes,<br />computes a route, shows a summary (distance / duration / ascent / waypoint list)<br />and downloads the GPX, plus a saved-tours list. A dashboard widget shows the<br />saved-tour count, the latest tour and engine readiness. Light + Dark, mobile,<br /><code>var(--color-*)</code>.</p>\n<blockquote><p><strong>No interactive map yet.</strong> No map library is currently an Aiopta</p><p>dependency, and the project&#39;s no-new-dep rule forbids adding one, so the page</p><p>ships a functional coordinate-input + route-summary UI. An interactive map is</p><p>parked in <code>docs/OPEN-DECISIONS.md</code> as a follow-up.</p></blockquote>\n<h2>Owner setup (OPEN-DECISIONS)</h2>\n<p>1. <strong>BRouter image + <code>rd5</code> region data</strong> (<code>brouter-routing-image</code> /<br />   <code>brouter-rd5-region</code>): build/publish the BRouter image and provision the<br />   region-selectable OSM segment data (disk).<br />2. <strong>Fallback Directions key</strong> (<code>routing-fallback-api-key</code>): optionally set<br />   <code>ROUTING_FALLBACK_API_KEY</code> (+ <code>ROUTING_FALLBACK_PROVIDER</code>) for the<br />   container-absent fallback.<br />3. <strong>Overpass endpoint (v1.1, optional)</strong>: POIs use the public Overpass API by<br />   default (keyless). To point at a self-hosted/rate-limited mirror, set<br />   <code>OVERPASS_API_URL</code> (+ optional <code>OVERPASS_API_KEY</code>, sent server-side as a header,<br />   never logged) in Aiopta addon-config.<br />4. <strong>Map library</strong> (<code>moto-routeplanner-map-lib</code>): parked until/unless a map lib is<br />   a dependency.</p>\n<p>Until (1) or (2) is provisioned, <code>/status</code> reports the engine not-configured and<br />plan calls return a clear 503.</p>\n<h2>Tests</h2>\n<p><code>tests/unit/</code> (WSL node22): GPX 1.1 validity (<code>gpx.test.ts</code>), param→profile<br />mapping (<code>profile.test.ts</code>), round-trip distance bounds + geo helpers<br />(<code>roundtrip.test.ts</code>), the engine client incl. the no-engine 503 + container URL +<br />fallback (<code>brouter.test.ts</code>), the planner incl. the acceptance GPX + highway-until<br />two-leg routing (<code>planner.test.ts</code>), manifest/dual-registration (<code>manifest.test.ts</code>),<br />and the saved-tour per-user isolation (<code>isolation.test.ts</code>).</p>\n<p><strong>v1.1 tests:</strong> the elevation profile (<code>elevation.test.ts</code>), multi-day staging<br />(<code>stages.test.ts</code>), the POI corridor filter + category model (<code>poi.test.ts</code>), the<br />Overpass transport incl. key-never-leaked + 502 paths (<code>overpass.test.ts</code>), the<br />GPX POI waypoints + trackpoint parser (<code>gpx-v11.test.ts</code>), the planner<br />orchestration of POIs/elevation/stages with the engine + Overpass mocked<br />(<code>planner-v11.test.ts</code>), and per-user isolation of the new stage routes<br />(<code>isolation-v11.test.ts</code>).</p>",
          "byteSize": 7876
        }
      }
    },
    {
      "id": "network",
      "uuid": "4079e196-7af4-47b8-9665-5ec933516698",
      "name": "Network (Pi-hole / AdGuard Home DNS & Adblock)",
      "version": "1.0.3",
      "description": "Per-User BYOK DNS/Adblock-Integration: verbindet Pi-hole oder AdGuard Home mit eigenen API-Token/Passwörtern (ein Connection-Set pro Nutzer). DNS-Query-Statistiken, Block-Raten/Top-Blocked-Domains, Allow/Deny-Listen-Verwaltung und optionaler UniFi/Router-Client-Status. Alles per-user über scopedStore isoliert (ADR-054). Kanonisches Homelab-Netzwerk-Addon (ADR-008 / #494, Epic #490): VPN (WireGuard/Tailscale) und Reverse-Proxy (NPM/Traefik/Caddy) sind als Domänen-Connectoren desselben geteilten BYOK-Host-Modells hier konsolidiert.",
      "author": "Aiopta",
      "icon": "🛡️",
      "capabilities": [
        "dns-stats",
        "blocked-metrics",
        "allow-deny-lists",
        "client-status",
        "connection-config",
        "homelab-hub",
        "vpn-connector",
        "reverse-proxy-connector"
      ],
      "tags": [],
      "download": "/artifacts/network-1.0.3.zip",
      "sha256": "c1e98c3e0d3d057432ee8e6c5c16b5d9da7c28896bcbec8d7ec942204d7f59d6",
      "size": 126910,
      "page": {
        "id": "network-page",
        "path": "/addons/network",
        "name": "Network",
        "icon": "🛡️",
        "section": "Tools",
        "component": "@addons/network/src/components/NetworkPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/network/connections",
        "/api/v1/addons/network/connections/:id",
        "/api/v1/addons/network/config",
        "/api/v1/addons/network/stats",
        "/api/v1/addons/network/lists",
        "/api/v1/addons/network/lists/:id",
        "/api/v1/addons/network/clients"
      ],
      "skills": [
        {
          "name": "network-read",
          "displayName": "Network: DNS-Stats & Block-Metriken lesen",
          "description": "Liest DNS-Query-Statistiken, Block-Raten und Top-Blocked-Domains aus der per-User BYOK-Verbindung (Pi-hole/AdGuard). Reiner Lesezugriff — ändert nichts.",
          "permissions": [
            "addons/network/stats",
            "addons/network/connections",
            "addons/network/clients"
          ],
          "endpoint": "/api/v1/addons/network/stats",
          "scripts": []
        },
        {
          "name": "network-lists",
          "displayName": "Network: Allow/Deny-Listen verwalten",
          "description": "Legt Allow- oder Deny-Listen-Einträge für den Nutzer an oder entfernt diese (per-User gebucht). Diese werden gegen die verbundene Pi-hole/AdGuard-Instanz synchronisiert.",
          "permissions": [
            "addons/network/lists",
            "addons/network/connections"
          ],
          "endpoint": "/api/v1/addons/network/lists",
          "scripts": []
        },
        {
          "name": "network-manage",
          "displayName": "Network: Verbindungen (BYOK) verwalten",
          "description": "Legt Pi-hole/AdGuard-Verbindungen mit Host + API-Token/Passwort für den Nutzer an oder entfernt diese. Die Secrets werden niemals zurückgegeben (maskiert).",
          "permissions": [
            "addons/network/connections",
            "addons/network/config"
          ],
          "endpoint": "/api/v1/addons/network/connections",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": null
    },
    {
      "id": "notifications",
      "uuid": "2f00f42e-406c-41c2-90cc-d126c3a2a92f",
      "name": "Notifications",
      "version": "1.0.4",
      "description": "Universal notifications: agent-backend dispatch (OpenClaw, Pi Agent, Hermes) + external channels (Discord, Telegram, Slack, WhatsApp, Signal, Email). Reads from Hermes gateway config when available.",
      "author": "Aiopta",
      "icon": "🔔",
      "capabilities": [
        "notifications",
        "discord",
        "telegram",
        "slack",
        "whatsapp",
        "signal",
        "email",
        "push",
        "webhook"
      ],
      "tags": [],
      "download": "/artifacts/notifications-1.0.4.zip",
      "sha256": "1c9a6ae149a57af34e89e0174a98a7cdbeb09f1ac30cbecee61e8c4bdf035436",
      "size": 334046,
      "page": {
        "path": "/addons/notifications",
        "name": "Notifications",
        "icon": "🔔",
        "section": "Settings",
        "component": "@addons/notifications/src/components/ExternalNotificationsSettings"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/notifications/send",
        "/api/v1/addons/notifications/config",
        "/api/v1/addons/notifications/test",
        "/api/v1/addons/notifications/channels",
        "/api/v1/addons/notifications/history",
        "/api/v1/addons/notifications/status",
        "/api/v1/addons/notifications/platforms",
        "/api/v1/addons/notifications/platforms/{id}",
        "/api/v1/addons/notifications/platforms/{id}/test",
        "/api/v1/addons/notifications/external-config",
        "/api/v1/addons/notifications/external-channels",
        "/api/v1/addons/notifications/external-test",
        "/api/v1/addons/notifications/ntfy/settings",
        "/api/v1/addons/notifications/ntfy/test",
        "/api/v1/addons/notifications/push/health",
        "/api/v1/addons/notifications/push/vapid"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>🔔 Notification Dispatch</h1>\n<p>Universal notification dispatch to AI agent backends</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>notifications</code></li><li><strong>Version:</strong> 1.0.0</li></ul>\n<h2>Einrichtung</h2>\n<p>Dieses Addon benötigt folgende Secrets/Provider-Konfiguration (Settings → Credentials bzw. Provider-Store, <strong>nie</strong> in addon.json):</p>\n<ul><li><code>HERMES_API_KEY</code></li><li><code>OPENCLAW_API_KEY</code></li><li><code>PI_AGENT_API_KEY</code></li></ul>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET | <code>/api/v1/addons/notifications/channels</code> |<br />| GET, POST | <code>/api/v1/addons/notifications/config</code> |<br />| GET | <code>/api/v1/addons/notifications/history</code> |<br />| POST | <code>/api/v1/addons/notifications/send</code> |<br />| POST | <code>/api/v1/addons/notifications/test</code> |<br />| GET, PUT | <code>/api/v1/addons/notifications/ntfy/settings</code> |<br />| POST | <code>/api/v1/addons/notifications/ntfy/test</code> |</p>\n<h2>Push via self-hosted ntfy / UnifiedPush (#129 / INBOX-52)</h2>\n<p>Der <strong>bevorzugte self-hosted Push-Pfad</strong> (statt FCM). Push geht per HTTP-POST an ein <strong>pro-User-Topic</strong>, das <strong>serverseitig aus der <code>userId</code></strong> abgeleitet wird (<code>&lt;prefix&gt;-sha256(userId)[:24]</code>) — <strong>nie ein Client-Wert</strong>. Ein Push für User A kann niemals an B&#39;s Topic adressiert werden. Der Mobile-Wrapper abonniert via UnifiedPush dasselbe Topic — ein POST erreicht Web <strong>und</strong> Android ohne Firebase. FCM bleibt optional/deprecated, aber funktionsfähig.</p>\n<p><strong>Konfiguration (instance-global, Aiopta addon-config):</strong></p>\n<p>| Key | Zweck | Default |<br />|---|---|---|<br />| <code>NTFY_SERVER_URL</code> | self-hosted ntfy-Server (oder ntfy.sh). <strong>Owner-Entscheidung</strong> (OPEN-DECISIONS Block H). | <code>https://ntfy.sh</code> |<br />| <code>NTFY_AUTH_TOKEN</code> | optionaler Bearer-Token für geschützten Server (Secret). | — |<br />| <code>NTFY_TOPIC_PREFIX</code> | Namespace-Prefix für pro-User-Topics. | <code>mc</code> |</p>\n<p>Solange kein Server gesetzt ist, <strong>degradiert</strong> der Kanal graziös (<code>ntfy not configured</code>, kein Crash).</p>\n<p><strong>Per-User:</strong> Jeder User aktiviert ntfy in seinen Notification-Settings (<code>GET/PUT …/ntfy/settings</code>, opt-in + Min-Priorität); der State liegt user-scoped (<code>scopedStore</code>, ADR-054). <code>POST …/ntfy/test</code> sendet einen Test-Push <strong>nur</strong> an das eigene Topic des Callers.</p>\n<h2>Bekannte Grenzen</h2>\n<ul><li>ntfy: Server-URL + ob FCM ganz fallengelassen wird = <strong>Owner-Entscheidungen</strong> (OPEN-DECISIONS Block H, #2); Mechanismus gebaut, Aktivierung/Server-Wahl wartet auf Owner.</li></ul>",
          "byteSize": 2257
        }
      }
    },
    {
      "id": "octoprint",
      "uuid": "2f1044e4-5bd6-44f5-9eaf-98f6c91186dc",
      "name": "OctoPrint (3D-Druck: Status/Aufträge/Temp/Webcam/G-Code-Queue/Multi)",
      "version": "1.0.1",
      "description": "Per-User OctoPrint-Anbindung: BYOK URL+API-Key pro Drucker, Live-Status/Jobs/Temperatur/Webcam, G-Code-Queue und Multi-Drucker. Brückt lesend zum openscad-generator-Addon (generierte Modelle in die Queue), ohne openscad-generator zu verändern. Lokale SQLite ist die Quelle der Wahrheit (ADR-054).",
      "author": "Aiopta",
      "icon": "🖨️",
      "capabilities": [
        "octoprint",
        "printers",
        "jobs",
        "temperature",
        "webcam",
        "queue",
        "gcode",
        "3d-printing"
      ],
      "tags": [],
      "download": "/artifacts/octoprint-1.0.1.zip",
      "sha256": "3aa764d0cd7e420761761cf63f6b5bd2acbd0a26c029aac01d5880ab2b038b84",
      "size": 104899,
      "page": {
        "id": "octoprint-page",
        "path": "/addons/octoprint",
        "name": "OctoPrint",
        "icon": "🖨️",
        "section": "Tools",
        "component": "@addons/octoprint/src/components/OctoprintPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/octoprint/printers",
        "/api/v1/addons/octoprint/printers/:id",
        "/api/v1/addons/octoprint/jobs",
        "/api/v1/addons/octoprint/temperature",
        "/api/v1/addons/octoprint/webcam",
        "/api/v1/addons/octoprint/queue",
        "/api/v1/addons/octoprint/queue/:id",
        "/api/v1/addons/octoprint/bridge/openscad"
      ],
      "skills": [
        {
          "name": "octoprint-read",
          "displayName": "OctoPrint: Status, Jobs, Temperatur & Webcam lesen",
          "description": "Listet die konfigurierten Drucker des Nutzers und liest Live-Status, Druckaufträge, Temperaturen und Webcam-URLs von seiner OctoPrint-Instanz (BYOK URL+API-Key). Reiner Lesezugriff — legt nichts an.",
          "permissions": [
            "addons/octoprint/printers",
            "addons/octoprint/jobs",
            "addons/octoprint/temperature",
            "addons/octoprint/webcam",
            "addons/octoprint/queue"
          ],
          "endpoint": "/api/v1/addons/octoprint/printers",
          "scripts": []
        },
        {
          "name": "octoprint-manage",
          "displayName": "OctoPrint: Drucker & G-Code-Queue verwalten",
          "description": "Legt BYOK Drucker-Verbindungen an oder entfernt diese und verwaltet die G-Code-Queue (stagen, hochladen, Status setzen) per-User gebucht.",
          "permissions": [
            "addons/octoprint/printers",
            "addons/octoprint/queue"
          ],
          "endpoint": "/api/v1/addons/octoprint/queue",
          "scripts": []
        },
        {
          "name": "octoprint-bridge-openscad",
          "displayName": "OctoPrint: openscad-Modell in die Queue schreiben",
          "description": "Schreibt ein generiertes Modell aus dem openscad-generator-Addon lesend in die G-Code-Queue eines Druckers (ohne openscad-generator zu verändern).",
          "permissions": [
            "addons/octoprint/queue",
            "addons/octoprint/bridge/openscad"
          ],
          "endpoint": "/api/v1/addons/octoprint/bridge/openscad",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>🖨️ OctoPrint (3D-Druck: Status/Aufträge/Temp/Webcam/G-Code-Queue/Multi)</h1>\n<p>Per-User OctoPrint-Anbindung: BYOK URL+API-Key pro Drucker, Live-Status/Jobs/<br />Temperatur/Webcam, G-Code-Queue und Multi-Drucker. Brückt lesend zum<br />openscad-generator-Addon (generierte Modelle in die Queue), ohne<br />openscad-generator zu verändern. Lokale SQLite ist die Quelle der Wahrheit<br />(ADR-054).</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>octoprint</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> octoprint, printers, jobs, temperature, webcam, queue, gcode, 3d-printing</li><li><strong>UI-Seite:</strong> <code>/addons/octoprint</code></li></ul>\n<h2>Einrichtung</h2>\n<p>BYOK: pro Drucker werden eine OctoPrint-Basis-URL und ein Application API-Key<br /> benötigt (im Frontend oder via POST <code>/printers</code>). Die Credentials liegen<br /> per-User scoped in der lokalen SQLite (ADR-054) — sie werden nie hardcodiert<br /> und nur für den jeweiligen Caller gelesen. Live-Daten (Status/Jobs/Temp/Webcam)<br /> werden on-demand von der Instanz des Nutzers geholt und nicht persistiert.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET, POST | <code>/api/v1/addons/octoprint/printers</code> |<br />| GET, PATCH, DELETE | <code>/api/v1/addons/octoprint/printers/[id]</code> |<br />| GET | <code>/api/v1/addons/octoprint/jobs?printerId=</code> |<br />| GET | <code>/api/v1/addons/octoprint/temperature?printerId=</code> |<br />| GET | <code>/api/v1/addons/octoprint/webcam?printerId=</code> |<br />| GET, POST | <code>/api/v1/addons/octoprint/queue</code> |<br />| PATCH, DELETE | <code>/api/v1/addons/octoprint/queue/[id]</code> |<br />| POST | <code>/api/v1/addons/octoprint/bridge/openscad</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<ul><li>OctoPrint-Status/Jobs/Temperatur sind Live-Daten der Nutzer-Instanz; nur die Drucker-Konfiguration und die G-Code-Queue sind lokal gespeichert.</li><li>Die openscad-Brücke schreibt generierte Modelle lesend in die Queue; openscad-generator wird nie modifiziert.</li></ul>\n<blockquote><p>README manuell gepflegt (addon.json + Routen als Quelle).</p></blockquote>",
          "byteSize": 1824
        }
      }
    },
    {
      "id": "octoprint-proxy",
      "uuid": "25f29dbd-f4f1-4b2b-82d6-907de0ccb930",
      "name": "OctoPrint Sync Proxy",
      "version": "1.2.0",
      "description": "Kapselt den extern laufenden OctoPrint Sync Proxy (Python-FastAPI, LAN-intern) in Aiopta: natives Dashboard (Status-Karten OctoPrint/Git/USB, Job-Liste mit Fortschritt, Retry- und USB-Sync-Aktionen), API-Routen mit zentraler Auth und HMAC-Grant-Minting, OctoPrint-kompatible Catch-all-API für OrcaSlicer (Upload/Download via Aiopta Agent-API-Key) sowie ein MCP-Server (proxy-tools).",
      "author": "Aiopta",
      "icon": "🖨️",
      "capabilities": [
        "octoprint",
        "sync-proxy",
        "jobs",
        "usb-sync",
        "orcaslicer",
        "3d-printing"
      ],
      "tags": [],
      "download": "/artifacts/octoprint-proxy-1.2.0.zip",
      "sha256": "fb5fdd93303b2104fb9d03268594c702e6dd09f45978338c9c9a3014edc56cab",
      "size": 158735,
      "page": {
        "id": "octoprint-proxy",
        "path": "/addons/octoprint-proxy",
        "name": "OctoPrint Sync",
        "icon": "🖨️",
        "section": "Tools",
        "component": "@addons/octoprint-proxy/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/octoprint-proxy/status",
        "/api/v1/addons/octoprint-proxy/jobs",
        "/api/v1/addons/octoprint-proxy/jobs/{id}",
        "/api/v1/addons/octoprint-proxy/jobs/{id}/retry",
        "/api/v1/addons/octoprint-proxy/usb/sync",
        "/api/v1/addons/octoprint-proxy/api/version",
        "/api/v1/addons/octoprint-proxy/api/files/local",
        "/api/v1/addons/octoprint-proxy/api/files/local/{name}"
      ],
      "skills": [
        {
          "name": "octoprint-proxy-read",
          "displayName": "OctoPrint Sync Proxy: Status & Jobs lesen",
          "description": "Liest Service-Status (OctoPrint/Git/USB) und die Sync-Job-Liste des externen OctoPrint Sync Proxy über die Addon-API. Reiner Lesezugriff.",
          "permissions": [
            "addons/octoprint-proxy/read"
          ],
          "endpoint": "/api/v1/addons/octoprint-proxy/status",
          "scripts": []
        },
        {
          "name": "octoprint-proxy-write",
          "displayName": "OctoPrint Sync Proxy: Retry & USB-Sync auslösen",
          "description": "Setzt fehlgeschlagene Sync-Targets eines Jobs zurück (Retry) und startet manuelle USB-Syncs über die Addon-API.",
          "permissions": [
            "addons/octoprint-proxy/read",
            "addons/octoprint-proxy/write"
          ],
          "endpoint": "/api/v1/addons/octoprint-proxy/usb/sync",
          "scripts": []
        }
      ],
      "mcpServers": [
        {
          "id": "proxy-tools",
          "displayName": "OctoPrint Sync Proxy tools",
          "description": "MCP-Tools für den externen OctoPrint Sync Proxy: Status und Job-Liste lesen, Job-Details abrufen und als einziges schreibendes Tool einen Job-Retry auslösen. Authentifiziert sich per HMAC-Grant (grantSecret) oder Fallback per proxyApiKey; beides wird core-seitig aus den deklarierten Settings-Slots aufgelöst.",
          "transport": "stdio",
          "runtime": "node",
          "entry": "proxy-tools/server.mjs",
          "permissions": [
            "addons/octoprint-proxy/job/retry",
            "addons/octoprint-proxy/read"
          ],
          "toolPolicy": {
            "mode": "write",
            "allowDestructive": false
          },
          "configRefs": [
            "baseUrl"
          ],
          "credentialRefs": [
            "grantSecret",
            "proxyApiKey"
          ],
          "archiveEntry": "octoprint-proxy/mcp/servers/proxy-tools/server.mjs",
          "sha256": "8c2fe0bf4ac062a01a8ec90a2c37b36f6e3eafa9067d3ebf6d12b30c4dc18975",
          "size": 13729,
          "descriptorVersion": 1
        }
      ],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>OctoPrint Sync Proxy (octoprint-proxy)</h1>\n<p>Kapselt den <strong>extern laufenden OctoPrint Sync Proxy</strong> (Python/FastAPI,<br />LAN-intern unter <code>http://192.168.0.30:8000</code>) in Aiopta — ohne den Dienst<br />selbst zu verändern und ohne ihn nach außen zu exponieren.</p>\n<h2>Architektur</h2>\n<pre><code>Browser/OrcaSlicer ──HTTPS──▶ Aiopta (dieses Addon) ──LAN──▶ Sync Proxy (192.168.0.30:8000)\n                              ├─ Dashboard (native DS-Seite)      /status, /api/proxy/*\n                              ├─ API-Routen (userRoute + Grant)   /api/proxy/*\n                              ├─ Orca-API (X-Api-Key, statisch)   /api/version, /api/files/local*\n                              └─ MCP-Server (proxy-tools)         /status, /api/proxy/jobs*</code></pre>\n<p>- <strong>Natives Dashboard</strong> (<code>src/page.tsx</code>, <code>ui.mode: &quot;aiopta&quot;</code>): Status-Karten<br />  (OctoPrint/Git/USB), Job-Liste (neueste oben) mit Fortschrittsbalken,<br />  Retry- und USB-Sync-Aktionen. Ausschließlich <code>@aiopta/ui</code>-Komponenten.<br />  Der Browser kontaktiert den Proxy nie direkt.<br />- <strong>API-Routen</strong> (<code>src/app/api/v1/addons/octoprint-proxy/*</code>): zentrale Auth<br />  via <code>userRoute</code> (Session-Cookie oder Bearer/X-API-Key Agent-Key). Danach<br />  mintet das Addon einen <strong>HMAC-Grant</strong> (`X-Aiopta-Grant:<br />  v1.&lt;base64url(payload)&gt;.&lt;base64url(hmac)&gt;<code>; Payload </code>{aud, caps, exp,<br />  iat, sub}<code>; HMAC-SHA256 mit </code>grantSecret`; TTL 110 s) — Implementierung in<br />  <code>src/lib/grant.ts</code>, byte-kompatibel zur Proxy-Referenz <code>app/grant.py</code>.<br />  Fallback: statischer <code>X-Api-Key</code> (<code>proxyApiKey</code>).<br />- <strong>OrcaSlicer</strong> nutzt den Proxy nicht mehr direkt: als „OctoPrint&quot;-Host<br />  wird <code>https://&lt;aiopta&gt;/api/v1/addons/octoprint-proxy</code> eingetragen (Orca<br />  hängt <code>/api/...</code> an). Die Endpunkte (<code>api/version</code>, <code>api/files/local</code>,<br />  <code>api/files/local/[name]</code> — bewusst <strong>statische Routen</strong>, siehe<br />  Runtime-Kompatibilität) validieren den<br />  Orca-<code>X-Api-Key</code> als <strong>Aiopta Agent-/User-API-Key</strong> (<code>mc_…</code>, via<br />  Core-<code>getAuthUser</code>) und reichen den Request mit <strong>HMAC-Grant</strong> weiter —<br />  der Grant ist DER Auth-Weg auch auf den OctoPrint-Endpunkten<br />  (<code>/api/version</code>, <code>/api/files/*</code>; write-caps bei POST). <code>proxyApiKey</code><br />  (<code>X-Api-Key</code>) ist nur noch optionaler Legacy-Fallback für Installationen<br />  ohne grantSecret. Uploads gestreamt (<code>duplex: &quot;half&quot;</code>, kein Vollpuffer),<br />  Responses 1:1 im OctoPrint-JSON-Format. Die gemeinsame<br />  Auth/Mapping/Forwarding-Logik liegt zentral in <code>src/lib/orca-api.ts</code>.<br />- <strong>MCP-Server</strong> (<code>mcp/servers/proxy-tools/server.mjs</code>, Node-STDIO): vier<br />  Tools, davon drei read-only und eines mutierend.</p>\n<h2>Settings (settingsSchema → WebUI-Formular)</h2>\n<p>| Key | Typ | Default | Beschreibung |<br />| --- | --- | --- | --- |<br />| <code>baseUrl</code> | text | <code>http://192.168.0.30:8000</code> | Interne Basis-URL des Proxy (nur LAN). |<br />| <code>grantSecret</code> | password (secret) | — | Geteiltes HMAC-Secret; muss mit <code>AIOPTA_GRANT_SECRET</code> des Proxy übereinstimmen. |<br />| <code>proxyApiKey</code> | password (secret) | — | Statischer Proxy-Key — <strong>optional</strong>, nur Legacy-Fallback für Installationen ohne <code>grantSecret</code>. |<br />| <code>pollIntervalSeconds</code> | number | <code>10</code> (2–300) | Aktualisierungs-Intervall des Dashboards. |<br />| <code>allowUsbMirrorDelete</code> | boolean (adminOnly) | <code>false</code> | Erlaubt USB-Sync mit <code>delete=true</code> (zusätzlich <code>confirm=DELETE</code> nötig). |</p>\n<h2>Permissions</h2>\n<ul><li><code>addons/octoprint-proxy/read</code> — Status/Jobs lesen (Dashboard-GETs, MCP).</li><li><code>addons/octoprint-proxy/write</code> — Retry + USB-Sync (POST-Routen).</li></ul>\n<p>Die Durchsetzung für Agent-API-Keys erfolgt core-seitig (Agent-Scope-<br />Enforcement anhand der deklarierten Permissions); der Grant trägt die<br />Achse zusätzlich Richtung Proxy (<code>caps</code>: lesend <code>[&quot;read&quot;]</code>, mutierend<br /><code>[&quot;read&quot;,&quot;write&quot;]</code>).</p>\n<h2>API-Routen</h2>\n<p>| Route | Methode | Proxy-Ziel |<br />| --- | --- | --- |<br />| <code>.../status</code> | GET | <code>GET /status</code> (+ <code>pollIntervalSeconds</code> im Envelope) |<br />| <code>.../jobs</code> | GET | <code>GET /api/proxy/jobs</code> |<br />| <code>.../jobs/{id}</code> | GET | <code>GET /api/proxy/jobs/{id}</code> (404 passthrough) |<br />| <code>.../jobs/{id}/retry</code> | POST | <code>POST /api/proxy/jobs/{id}/retry</code> |<br />| <code>.../usb/sync</code> | POST | <code>POST /api/proxy/usb/sync</code> (Delete-Guard s. Settings) |<br />| <code>.../api/version</code> | GET | <code>GET /api/version</code> (OrcaSlicer) |<br />| <code>.../api/files/local</code> | GET | <code>GET /api/files/local</code> (Query durchgereicht) |<br />| <code>.../api/files/local/{name}</code> | GET | <code>GET /api/files/local/&lt;name&gt;</code> (einstufige Namen) |<br />| <code>.../api/files/local</code> | POST | <code>POST /api/files/local</code> (Multipart-Upload, gestreamt) |</p>\n<h2>Runtime-Kompatibilität (runtime-installiert vs. gebündelt)</h2>\n<p>Die OrcaSlicer-Endpunkte sind bewusst <strong>statische Routen</strong> (plus einstufige<br /><code>[name]</code>-Dynamik), keine <code>[[...path]]</code>-Catch-all: der Core-Runtime-Router<br />(jiti, runtime-installierte ZIPs) resolvt dynamische Verzeichnisse nur mit<br />einem Segment pro Ebene — eine mehrstufige Catch-all liefert dort 404<br />(„No handler found&quot;). Das ist ein bekannter Core-Bug, verfolgt als<br /><a href=\"https://git.langkau.info/Aiopta/aiopta-core/issues/1355\" rel=\"noopener noreferrer\">Aiopta/aiopta-core#1355</a>;<br />das Addon umgeht ihn konventionsnah statt auf den Fix zu warten.</p>\n<p>Konsequenzen:</p>\n<p>- <code>api/version</code>, <code>api/files/local</code> (GET+POST) und<br />  <code>api/files/local/[name]</code> (GET) funktionieren in BEIDEN Welten<br />  (runtime-installiertes ZIP und gebündelter Next.js-Weg). Params kommen<br />  runtime-seitig als Record mit Bracket-Key — <code>routeParam</code> in<br />  <code>src/lib/orca-api.ts</code> akzeptiert beide Formen.<br />- <strong>Verschachtelte Datei-Detail-Pfade</strong> (<code>files/local/&lt;ordner&gt;/&lt;datei&gt;</code>)<br />  sind runtime-seitig bis zum Core-Fix #1355 nicht erreichbar (404);<br />  einstufige Namen decken den OrcaSlicer-Hauptweg ab. Der gebündelte Weg<br />  (Zielweg nach Core-Repair<br />  <a href=\"https://git.langkau.info/Aiopta/aiopta-core/issues/1356\" rel=\"noopener noreferrer\">Aiopta/aiopta-core#1356</a>)<br />  kann tiefe Pfade nachziehen, sobald der Router sie in beiden Welten<br />  kann.</p>\n<h2>MCP-Tools (<code>proxy-tools</code>)</h2>\n<p>| Tool | Wirkung |<br />| --- | --- |<br />| <code>proxy_status</code> | <code>GET /status</code> (read-only) |<br />| <code>proxy_list_jobs</code> | <code>GET /api/proxy/jobs</code> (read-only) |<br />| <code>proxy_job_detail</code> | <code>GET /api/proxy/jobs/{id}</code> (read-only) |<br />| <code>proxy_retry_job</code> | <strong>MUTIEREND</strong>: <code>POST /api/proxy/jobs/{id}/retry</code> — einziges schreibendes Tool |</p>\n<p>Der Server minted den Grant selbst (gleiche Implementierung wie<br /><code>src/lib/grant.ts</code>). Die deklarierten Slots (<code>configRefs: [&quot;baseUrl&quot;]</code>,<br /><code>credentialRefs: [&quot;grantSecret&quot;, &quot;proxyApiKey&quot;]</code>) werden core-seitig<br />aufgelöst und als <code>AIOPTA_CONFIG_BASEURL</code> /<br /><code>AIOPTA_CREDENTIAL_GRANTSECRET</code> / <code>AIOPTA_CREDENTIAL_PROXYAPIKEY</code><br />injiziert. Ohne aufgelöste Credentials schlägt jeder Tool-Call sauber<br />fehl („nicht konfiguriert&quot;) — Handshake und <code>tools/list</code> funktionieren<br />immer.</p>\n<h2>Betrieb</h2>\n<p>- Der Sync Proxy muss unter <code>http://192.168.0.30:8000</code> laufen (nur LAN;<br />  kein öffentlicher Zugriff nötig/möglich).<br />- <code>AIOPTA_GRANT_SECRET</code> auf der Proxy-Seite == <code>grantSecret</code> hier — der<br />  Grant wird auf <code>/api/proxy/*</code> UND den OctoPrint-Endpunkten (<code>/api/*</code>)<br />  akzeptiert und ist der empfohlene Weg.<br />- <code>PROXY_API_KEY</code> / <code>proxyApiKey</code>: optionaler Legacy-Fallback, wenn kein<br />  <code>grantSecret</code> gesetzt ist. Ohne beides antwortet das Addon mit<br />  „grantSecret nicht konfiguriert&quot;.<br />- OrcaSlicer: Host <code>https://&lt;aiopta&gt;/api/v1/addons/octoprint-proxy</code>,<br />  API-Key = Aiopta Agent-API-Key (<code>mc_…</code>) mit Scope<br />  <code>addons/octoprint-proxy/*</code>.</p>\n<h2>Ausblick: mount-manager-Dependency (Epic #881)</h2>\n<p>Die USB-Mount-Verwaltung liegt aktuell außerhalb von Aiopta (fstab/udev auf<br />dem Proxy-Host, manuell eingerichtet). Sobald das Epic<br /><a href=\"https://git.langkau.info/Aiopta/aiopta-addons/issues/881\" rel=\"noopener noreferrer\">#881</a><br />(<code>mount-manager</code> — zentrale Verwaltung von Block-Devices, fstab/udev,<br />UI mit Permissions, Freigabe von Mounts an Addons/Agenten) umgesetzt ist,<br />wird dieses Addon <code>mount-manager</code> als <code>addonDependency</code> deklarieren und den<br />USB-Pfad aus dessen Freigabe-Modell beziehen. Bis dahin bleibt die<br />host-seitige Einrichtung wie im Proxy-Repo dokumentiert<br />(<code>docs/PROXMOX-USB-PASSTHROUGH.md</code>, <code>docs/USB-SETUP.md</code>).</p>",
          "byteSize": 7816
        }
      }
    },
    {
      "id": "openscad-generator",
      "uuid": "e7f613c4-7492-443e-9661-b8902f8f7103",
      "name": "OpenSCAD Generator",
      "version": "1.1.1",
      "description": "Generate OpenSCAD 3D models from text prompts (via the LLM gateway) and images, with containerized OpenSCAD rendering and STL export",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "text-to-openscad",
        "image-to-openscad",
        "stl-export",
        "preview-rendering"
      ],
      "tags": [],
      "download": "/artifacts/openscad-generator-1.1.1.zip",
      "sha256": "d68b0b44a7eb4ad6ac673fc98aeb5c8230ea34b5330ed870cd100d615db0d149",
      "size": 63415,
      "page": {
        "path": "/addons/openscad-generator",
        "name": "OpenSCAD Generator",
        "icon": "🧱",
        "section": "Tools",
        "component": "@addons/openscad-generator/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/openscad-generator/generate",
        "/api/v1/addons/openscad-generator/render",
        "/api/v1/addons/openscad-generator/status"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>OpenSCAD Generator Addon</h1>\n<p>Generate 3D-printable OpenSCAD models from text prompts and images, render previews, and export<br />STL — all from inside Aiopta.</p>\n<h2>Features</h2>\n<p>- <strong>Text to OpenSCAD</strong>: Describe your model in natural language. Generation is an LLM task and<br />  runs through the Aiopta LLM gateway (<code>@/lib/gateway</code>) — never a hard-coded model host<br />  (ADR-009). A deterministic keyword-based generator is used as an offline fallback when no model<br />  is reachable.<br />- <strong>Image to OpenSCAD</strong>: Convert 2D images to extruded 3D models (via Trace2SCAD or OpenCV<br />  contours, server-side).<br />- <strong>Preview Rendering</strong> &amp; <strong>STL Export</strong>: The <code>openscad</code> binary is an external tool, so it runs in<br />  the addon&#39;s <strong>render container</strong> (declared in the manifest <code>container</code> block). The render route<br />  reaches it via Aiopta&#39;s internal container URL; a local <code>openscad</code> CLI is used as a dev<br />  fallback.</p>\n<h2>Architecture (ADR-009)</h2>\n<p>- <strong>LLM via gateway only.</strong> <code>text-to-openscad.ts</code> calls <code>chatCompletionWithFallbacks</code> from<br />  <code>@/lib/gateway</code>; it does <strong>not</strong> talk to Ollama or any model endpoint directly.<br />- <strong>External tool as a container.</strong> The OpenSCAD renderer is declared as a <code>container</code> in<br />  <code>addon.json</code> / <code>manifest.ts</code> (<code>registry.langkau.info/aiopta/openscad-render</code>). It is<br />  booted at install by the container-service, started/stopped by enable/disable (G6 lifecycle),<br />  and reached only via <code>getContainerAddonInternalUrl(&quot;openscad-generator&quot;)</code>.<br />- <strong>No secrets in the manifest.</strong> The render image is stateless (<code>envSchema: []</code>); any future<br />  secret must go through <code>envSchema</code> + Aiopta config.</p>\n<h2>API</h2>\n<p>All routes are namespaced under <code>/api/v1/addons/openscad-generator/</code> and require auth.</p>\n<p>### Generate (text or image → OpenSCAD)<br />```<br />POST /api/v1/addons/openscad-generator/generate<br />{ &quot;type&quot;: &quot;text&quot;,  &quot;input&quot;: &quot;I need a watering can&quot; }<br />{ &quot;type&quot;: &quot;image&quot;, &quot;input&quot;: &quot;/tmp/source.png&quot;, &quot;options&quot;: { &quot;height&quot;: 5, &quot;layers&quot;: 10 } }<br />```</p>\n<p>### Render / export (OpenSCAD code → PNG preview or STL)<br />```<br />POST /api/v1/addons/openscad-generator/render<br />{ &quot;format&quot;: &quot;png&quot;, &quot;code&quot;: &quot;&lt;openscad code&gt;&quot;, &quot;options&quot;: { &quot;width&quot;: 800, &quot;height&quot;: 600 } }<br />{ &quot;format&quot;: &quot;stl&quot;, &quot;code&quot;: &quot;&lt;openscad code&gt;&quot; }<br />```<br />The response includes <code>source</code> (<code>&quot;container&quot;</code> or <code>&quot;cli&quot;</code>) and, for the container path, a base64<br /><code>dataBase64</code> payload.</p>\n<p>### Status<br />```<br />GET /api/v1/addons/openscad-generator/status<br />→ { ok, capabilities, textGeneration: &quot;gateway&quot;, renderContainerAvailable }<br />```</p>\n<h2>Render container contract</h2>\n<p>The render image is expected to expose:</p>\n<p>- <code>POST /render</code> — body <code>{ code, format: &quot;png&quot;|&quot;stl&quot;, width?, height? }</code>, returns<br />  <code>{ dataBase64 }</code> (base64 PNG or STL).<br />- <code>GET /api/v1/health</code> — health probe used by the container-service.</p>\n<blockquote><p><strong>TODO(owner): <code>openscad-render-image</code></strong> — building and publishing the</p><p><code>registry.langkau.info/aiopta/openscad-render</code> image is owner-run (registry currently</p><p>in outage). Until it is published, render falls back to a local <code>openscad</code> CLI in dev. See</p><p><code>docs/OPEN-DECISIONS.md</code>.</p></blockquote>\n<h2>Layout</h2>\n<pre><code>addons/openscad-generator/\n├── addon.json                 # Manifest (page + container + endpoints)\n├── README.md\n├── SKILL.md                   # Agent skill\n├── src/\n│   ├── manifest.ts            # Extension (dual-registration: routes + page + container)\n│   ├── page.tsx               # Addon UI (registered page)\n│   ├── text-to-openscad.ts    # Text→SCAD via @/lib/gateway (+ offline fallback)\n│   ├── image-to-openscad.ts   # Image→SCAD via Trace2SCAD/contours\n│   ├── render-openscad.ts     # Render PNG + export STL (container-first, CLI fallback)\n│   └── app/api/v1/addons/openscad-generator/{generate,render,status}/route.ts\n└── tests/unit/                # manifest validity + gateway-backed generation (gateway mocked)</code></pre>\n<h2>License</h2>\n<p>MIT</p>",
          "byteSize": 3839
        }
      }
    },
    {
      "id": "paid-sample",
      "uuid": "7cc803ea-f53f-4dd4-b12a-b77b576c6961",
      "name": "Pro Pack (Paid-Addon Sample)",
      "version": "1.0.2",
      "description": "Beispiel-Addon für kostenpflichtige Marketplace-Eintraege (issue #89). Demonstriert das neue `pricing`-Feld in addon.json + Catalog: nur nach Registrierung + Login + 'Kaufen' + Mock-Payment downloadbar. Hat selbst keinen UI-Surface — der Wert liegt im演示ieren der Bezahl-Flow.",
      "author": "Aiopta",
      "icon": "💎",
      "capabilities": [
        "paid-listing-sample"
      ],
      "tags": [],
      "download": "/api/v1/store/download?addon=paid-sample",
      "sha256": "56787cd133916aff8b9fc9a1ad4d8a0ad7fe6211e6f662f37dffd0d3934adf53",
      "size": 2784,
      "page": null,
      "deprecated": false,
      "endpoints": [],
      "skills": [
        {
          "name": "paid-sample-info",
          "displayName": "Pro Pack (Paid Sample): Info",
          "description": "Liest die Pricing-Metadaten dieses Addons und gibt sie zurück. Kostet nichts aufzurufen — der Kauf ist eine演示 des Paywalls, nicht ein API-Gate für die Info selbst.",
          "permissions": [
            "addons/paid-sample/status"
          ],
          "endpoint": "/api/v1/addons/paid-sample/status",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": {
        "amount": 499,
        "currency": "EUR",
        "kind": "one-time"
      },
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": "EUR",
        "tiers": [
          {
            "id": "standard",
            "name": "Standard",
            "priceMinor": 499,
            "interval": "one_time",
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Pro Pack — Paid-Addon Sample (issue #89)</h1>\n<p>Demonstrations-Addon für kostenpflichtige Marketplace-Eintraege. Der<br />eigentliche Wert liegt nicht in irgendwelchen Features, sondern darin,<br />den kompletten Auth + Paywall + Download-Flow einmal real durch den<br />Store zu schicken:</p>\n<p>1. Im Store-Browse als 💎 Pro Pack sichtbar mit Preis-Tag <strong>4,99 €</strong>.<br />2. Beim Klick auf &quot;Kaufen&quot; oeffnet sich der Login/Register-Modal<br />   (User-Story: Store-Betreiber moechte, dass kostenpflichtige Addons<br />   nur nach Registrierung + Login + Kauf heruntergeladen werden<br />   koennen).<br />3. Nach Registrierung + Klick auf &quot;Kaufen&quot; erstellt das Backend einen<br />   Checkout (Mock-Stripe-Signatur, status=pending) und das SPA ruft<br />   <code>/api/v1/store/payment/simulate</code> mit status=success auf.<br />4. Sobald die Purchase auf <code>paid</code> steht, schaltet der Download-Button<br />   scharf. Das Artifact wird vom Backend mit<br />   <code>Content-Disposition: attachment</code> gestreamt.</p>\n<p>Dieses Addon hat bewusst keine <code>src/</code>-UI — der UI-Surface waere nur<br />Rauschen. Das Manifest selbst ist die Demonstration.</p>\n<h2>Felder</h2>\n<ul><li><code>pricing.amount</code>: 499 (Cents, nicht Euro — Integer, ADR-konform)</li><li><code>pricing.currency</code>: EUR (ISO 4217, Whitelist: EUR/USD/GBP/CHF)</li><li><code>pricing.kind</code>: <code>one-time</code> (vs. <code>subscription</code>)</li></ul>\n<p>Aenderungen an <code>pricing.amount</code> &lt;= 0 oder fehlendes <code>pricing</code>-Feld<br />bedeuten automatisch &quot;free&quot; — dann erscheint der Eintrag im Store<br />ohne Preis-Tag und der Download ist weiterhin oeffentlich via nginx.</p>",
          "byteSize": 1447
        }
      }
    },
    {
      "id": "payments",
      "uuid": "5eea08c0-a898-4ba7-b55d-5d356bde7d09",
      "name": "Payments (Rechnungen, Abos, Payment-Links, Mahnwesen)",
      "version": "1.4.0",
      "description": "Rechnungen, Abos, Payment-Links und Mahnwesen über Stripe + PayPal — providerseitig konfigurierbar (Keys/Webhook-Secret/Mode via Aiopta addon-config, nie im Manifest) und auch für Aioptas eigene Abrechnung (Phase Q, ADR-023) nutzbar. Lokale SQLite ist die Quelle der Wahrheit; ohne konfigurierten Provider läuft das Addon graceful im Lesemodus — KEIN Geld ohne Owner-Keys. Webhooks sind signaturverifiziert. Money-Stufe-1 (ADR-039).",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "invoices",
        "subscriptions",
        "payment-links",
        "dunning",
        "payment-providers",
        "webhooks",
        "aiopta-billing"
      ],
      "tags": [],
      "download": "/artifacts/payments-1.4.0.zip",
      "sha256": "dcec9680568325e6a5d396af6d055945c51fd25cca3ee2971f632ab4f2617645",
      "size": 328273,
      "page": {
        "id": "payments-page",
        "path": "/addons/payments",
        "name": "Payments",
        "icon": "💳",
        "section": "Money",
        "component": "@addons/payments/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/payments/status",
        "/api/v1/addons/payments/invoices",
        "/api/v1/addons/payments/invoices/:id",
        "/api/v1/addons/payments/subscriptions",
        "/api/v1/addons/payments/subscriptions/:id",
        "/api/v1/addons/payments/payment-links",
        "/api/v1/addons/payments/payment-links/:id",
        "/api/v1/addons/payments/dunning",
        "/api/v1/addons/payments/webhooks/:provider",
        "/api/v1/addons/payments/widget"
      ],
      "skills": [
        {
          "name": "payments-overview",
          "displayName": "Payments: Status & Übersicht",
          "description": "Liest Provider-Status (konfiguriert/erreichbar, ohne Secrets) und die Payments-Übersicht (offene Rechnungen, Umsatz/Monat, überfällig, aktive Abos). Reiner Lesezugriff — bewegt kein Geld.",
          "permissions": [
            "addons/payments/status"
          ],
          "endpoint": "/api/v1/addons/payments/status",
          "scripts": [
            {
              "id": "payments-overview-cli",
              "name": "payments-overview-cli",
              "path": "payments-overview/cli.sh",
              "sha256": "1a648111d619787c2d55d52043ce9283dad0e6dc5355bc402226d63cfe5b64d8",
              "interpreter": "bash",
              "timeoutMs": 10000,
              "permissions": [
                "addons/payments/status"
              ],
              "inputSchema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 20
                  },
                  "timeoutMs": {
                    "type": "integer",
                    "minimum": 100,
                    "maximum": 300000
                  }
                }
              },
              "outputSchema": {
                "type": "object",
                "required": [
                  "ok"
                ],
                "properties": {
                  "ok": {
                    "type": "boolean"
                  },
                  "status": {
                    "type": "integer"
                  },
                  "data": {},
                  "error": {
                    "type": "string"
                  },
                  "truncated": {
                    "type": "boolean"
                  }
                }
              },
              "size": 12090
            }
          ]
        },
        {
          "name": "payments-invoices",
          "displayName": "Payments: Rechnungen",
          "description": "Listet/erstellt Rechnungs-Entwürfe und führt Aktionen aus (senden=beim Provider hosten+öffnen, als bezahlt markieren, stornieren). Senden braucht einen konfigurierten Provider (sonst 400 — kein stilles No-Op); Statusmaschine wird erzwungen.",
          "permissions": [
            "addons/payments/invoices",
            "addons/payments/status"
          ],
          "endpoint": "/api/v1/addons/payments/invoices",
          "scripts": []
        },
        {
          "name": "payments-subscriptions",
          "displayName": "Payments: Abos",
          "description": "Listet/erstellt/kündigt Abos gegen eine vom Owner bereitgestellte Price-/Plan-ID. Erstellen braucht einen konfigurierten Provider (sonst 400).",
          "permissions": [
            "addons/payments/subscriptions",
            "addons/payments/status"
          ],
          "endpoint": "/api/v1/addons/payments/subscriptions",
          "scripts": []
        },
        {
          "name": "payments-links",
          "displayName": "Payments: Payment-Links",
          "description": "Erstellt teilbare Zahl-URLs für einen Einmalbetrag und deaktiviert sie. Erstellen braucht einen konfigurierten Provider (sonst 400); Betrag>0 und Währungs-Whitelist werden geprüft.",
          "permissions": [
            "addons/payments/payment-links",
            "addons/payments/status"
          ],
          "endpoint": "/api/v1/addons/payments/payment-links",
          "scripts": []
        },
        {
          "name": "payments-dunning",
          "displayName": "Payments: Mahnwesen",
          "description": "Startet Mahnläufe für überfällige Rechnungen (Stufen 1–3 + zuletzt-versendet) und versendet Erinnerungen über die Notifications, falls aktiv — sonst wird der Zustand nur erfasst. Versendet keine Rohdaten/PII in Logs.",
          "permissions": [
            "addons/payments/dunning",
            "addons/payments/status"
          ],
          "endpoint": "/api/v1/addons/payments/dunning",
          "scripts": []
        }
      ],
      "mcpServers": [
        {
          "id": "payments-tools",
          "displayName": "Payments tools",
          "description": "Read-only payments math + projections for agents: invoice status machine, invoice totals (subtotal/tax/total), payments summary (invoice/subscription/payment-link counts, revenue-this-month, overdue), dunning projection and money formatting — the same pure functions the payments addon uses for its status, summary and widget (issue #436, epic #399). Moves no money.",
          "transport": "stdio",
          "runtime": "node",
          "entry": "payments-tools/server.mjs",
          "permissions": [
            "addons/payments/read"
          ],
          "toolPolicy": {
            "mode": "read-only",
            "allowDestructive": false
          },
          "configRefs": [],
          "credentialRefs": [],
          "archiveEntry": "payments/mcp/servers/payments-tools/server.mjs",
          "sha256": "6c7279e9dbecdabcc5fcca04a92ccaac3aac5d9cc23777cd97abffe46fa7ca21",
          "size": 39541,
          "descriptorVersion": 1
        }
      ],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Payments Addon</h1>\n<p>Rechnungen (Invoices), Abos (Subscriptions), Payment-Links und Mahnwesen (Dunning) über <strong>Stripe + PayPal</strong><br />— providerseitig konfigurierbar und auch für Aioptas <strong>eigene Abrechnung (Phase Q)</strong> nutzbar.<br />Foundation-Addon für spätere <code>accounting</code> / <code>membership</code> / <code>marketing-ads</code>.</p>\n<blockquote><p>Money-Stufe-1 · ADR-039 · Connector-Pattern (Stripe/PayPal sind SaaS — <strong>kein</strong> Container).</p></blockquote>\n<h2>Was es tut</h2>\n<p>- <strong>Rechnungen</strong> — anlegen/listen/lesen/senden/als-bezahlt-markieren/stornieren. Positionen, Währung,<br />  USt; Statusmaschine <code>draft → open → paid | void | uncollectible</code>. Lokale SQLite ist die Quelle der<br />  Wahrheit; der Provider hostet die Rechnung / erzeugt die Zahl-URL.<br />- <strong>Abos</strong> — anlegen/listen/kündigen gegen eine <strong>vom Owner bereitgestellte</strong> Price-/Plan-ID; Status<br />  <code>active | past_due | canceled</code>.<br />- <strong>Payment-Links</strong> — teilbare Zahl-URL für einen Einmalbetrag; listen/deaktivieren.<br />- <strong>Mahnwesen</strong> — Eskalationsstufen (1–3) + zuletzt-versendet für überfällige Rechnungen; Versand über die<br />  <strong>Notifications</strong> (falls installiert), sonst wird der Zustand nur erfasst und angezeigt.<br />- <strong>Webhooks</strong> — <code>POST /webhooks/{provider}</code> empfängt Stripe/PayPal-Events, <strong>verifiziert die Signatur</strong><br />  und aktualisiert lokalen Status <strong>idempotent</strong>.<br />- <strong>Aiopta-Billing-Tie-in</strong> — ein dünner interner Helper (<code>src/lib/aiopta-billing.ts</code>), damit Aiopta instances Phase-Q-Metering<br />  eine Rechnung/ein Abo über dieses Addon ausstellen kann — <strong>ohne</strong> Metering zu duplizieren (liest<br />  <code>@/lib/quotas</code>), und einen bezahlten Tarif über dieselbe Naht wie der Invite-Pfad<br />  (<code>applyInviteAssignment</code>) setzen kann.</p>\n<h2>Provider-Konfiguration (Aiopta-Config, nie im Manifest)</h2>\n<p>Beide Provider sind <strong>optional</strong> — ohne Keys läuft das Addon graceful im Lesemodus (kein Geld ohne<br />Owner-Keys). Secrets liegen ausschließlich in Aiopta addon-config (<code>addon.config.payments.*</code>, <code>is_secret=1</code>);<br />der einzige Echo ist ein maskierter <code>set(len=…)</code>-Hinweis.</p>\n<p>| Schlüssel | Provider | Zweck |<br />|-----------|----------|-------|<br />| <code>STRIPE_SECRET_KEY</code> | Stripe | API-Key (Bearer) |<br />| <code>STRIPE_WEBHOOK_SECRET</code> | Stripe | Webhook-HMAC-Secret (<code>Stripe-Signature</code>) |<br />| <code>STRIPE_MODE</code> | Stripe | <code>test</code> \\| <code>live</code> |<br />| <code>PAYPAL_CLIENT_ID</code> / <code>PAYPAL_CLIENT_SECRET</code> | PayPal | OAuth2 Client-Credentials |<br />| <code>PAYPAL_WEBHOOK_ID</code> | PayPal | Webhook-ID für die verify-API |<br />| <code>PAYPAL_MODE</code> | PayPal | <code>sandbox</code> (Default) \\| <code>live</code> |</p>\n<p>Konkrete Keys/Webhook-Secrets, live-vs-test und die Price-/Plan-IDs sind <strong>Owner-Entscheidungen</strong> — siehe<br /><code>docs/OPEN-DECISIONS.md</code> (<code>billing-payment-provider</code> / <code>payments-stripe</code>, <code>payments-paypal</code>,<br /><code>payments-price-ids</code>).</p>\n<h2>Sicherheit (Review-relevant, #72/INBOX-11)</h2>\n<p>- <strong>Secrets nur via Aiopta-Config</strong>, nie geloggt, nur maskiert echo&#39;d (<code>maskSecret</code>).<br />- <strong>Webhook-Signaturprüfung ist Pflicht</strong>: Stripe HMAC-SHA256 über den Rohbody (<code>Stripe-Signature</code>,<br />  Replay-Toleranz 5 min, konstantzeitiger Vergleich); PayPal über die verify-webhook-signature-API gegen<br />  die konfigurierte <code>PAYPAL_WEBHOOK_ID</code>. Fehlende/ungültige Signatur → <strong>400</strong>, <strong>keine</strong> State-Mutation.<br />  Webhook-Events sind über <code>markWebhookSeen</code> <strong>idempotent</strong>.<br />- <strong>Geldbewegende Routen</strong> sind POST/PUT/DELETE mit mc-session-Auth und <strong>erfordern</strong> einen konfigurierten<br />  Provider (sonst 400 — kein stilles No-Op). GET/Daten-Routen degradieren graceful (200, leere/<code>configured:false</code>).<br />- <strong>Idempotency</strong> bei jedem Provider-Create (Stripe <code>Idempotency-Key</code>, PayPal <code>PayPal-Request-Id</code>).<br />- <strong>Validierung</strong>: Betrag &gt; 0 (Minor-Units), Währungs-Whitelist (EUR/USD/GBP/CHF/CAD/AUD), USt 0–100.<br />  Kein PII in Logs/URLs (Kunden-E-Mail wird in der Mahnliste maskiert).</p>\n<h2>Architektur</h2>\n<pre><code>addons/payments/\n  addon.json                       # Manifest (Endpoints, Page, Widget, Skills; kein Container)\n  src/manifest.ts                  # Dual-Registration (ADR-001), String-Literal route()-Handler\n  src/page.tsx                     # PaymentsPage (Tabs: Rechnungen/Abos/Links/Mahnwesen + Provider-Panel)\n  src/widget.tsx                   # Dashboard-Widget (key-values, graceful)\n  src/lib/\n    money.ts                       # pure: Währungs-Whitelist, Beträge, Summen, Formatierung\n    errors.ts                      # PaymentsError (+ HTTP-Status)\n    provider-config.ts             # liest Provider-Secrets aus Aiopta-Config, maskierte Hints\n    providers/types.ts             # PaymentProvider-Interface (wie BackendDriver) + Money/LineItem\n    providers/stripe.ts            # Stripe (raw REST, HMAC-Webhook-Verify) — vollständig\n    providers/paypal.ts            # PayPal (OAuth2 REST, verify-API) — gleiche Schnittstelle\n    providers/index.ts             # Registry/Factory: resolveWriteProvider, defaultProvider\n    payments-store.ts              # SQLite-Quelle-der-Wahrheit + Statusmaschinen (pure + DB)\n    dunning.ts                     # Mahnlauf, Reminder-Versand (Notifications-Reuse, graceful)\n    status.ts                      # /status-Aggregat (Provider + Summary), graceful\n    aiopta-billing.ts              # Phase-Q-Tie-in (Rechnung/Abo ausstellen, Tarif-Naht)\n  src/app/api/v1/addons/payments/**/route.ts   # thin Handler (apiSuccess/apiError)\n  tests/unit/                      # money, store (Statusmaschine+DB), providers (Signatur), manifest, routes-graceful\n  SKILL.md, README.md</code></pre>\n<h2>Tests</h2>\n<pre><code class=\"lang-bash\">npx vitest run addons/payments</code></pre>\n<p>- <code>money.test.ts</code> — pure Validierung/Math.<br />- <code>payments-store.test.ts</code> — Statusmaschine (pure) + DB (In-Memory SQLite): Transitions, Abos,<br />  Payment-Links, Mahn-Cap, Webhook-Idempotenz + <code>applyWebhook</code>.<br />- <code>providers.test.ts</code> — Stripe-Signaturprüfung (gültig/manipuliert/abgelaufen), Klassifizierer,<br />  Happy-Path (mock fetch) + not-configured-Refusal, Registry.<br />- <code>manifest.test.ts</code> — Manifest gültig, namespaced, kein Container, keine Secrets/Hosts.<br />- <code>routes-graceful.test.ts</code> — Daten-Routen 200/leer ohne Provider; Webhook lehnt unkonfiguriert/unbekannt ab.</p>",
          "byteSize": 5962
        }
      }
    },
    {
      "id": "polymarket",
      "uuid": "b5b72cdb-8f3b-4815-910e-2e3c544c091d",
      "name": "Polymarket",
      "version": "1.0.1",
      "description": "Prediction market addon with market discovery, positions, open orders, and direct order placement via the official Polymarket CLOB client.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "prediction-markets",
        "market-discovery",
        "positions",
        "orders",
        "trading"
      ],
      "tags": [],
      "download": "/artifacts/polymarket-1.0.1.zip",
      "sha256": "8d9ec6178be7800c5ed1197ad268b66757fd356e6148690e493184eb2aaeb27d",
      "size": 101060,
      "page": {
        "path": "/addons/polymarket",
        "name": "Polymarket",
        "icon": "🎯",
        "section": "Finance",
        "component": "@addons/polymarket/src/components/PolymarketPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/polymarket",
        "/api/v1/addons/polymarket/status",
        "/api/v1/addons/polymarket/config",
        "/api/v1/addons/polymarket/markets",
        "/api/v1/addons/polymarket/book",
        "/api/v1/addons/polymarket/positions",
        "/api/v1/addons/polymarket/orders"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Polymarket</h1>\n<p>Prediction market addon with market discovery, positions, open orders, and direct order placement via the official Polymarket CLOB client.</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>polymarket</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> prediction-markets, market-discovery, positions, orders, trading</li><li><strong>UI-Seite:</strong> <code>/addons/polymarket</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials.</p>\n<p>npm-Abhängigkeiten (werden bei Installation aufgelöst): <code>@polymarket/clob-client-v2</code>, <code>viem</code>.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET | <code>/api/v1/addons/polymarket</code> |<br />| GET | <code>/api/v1/addons/polymarket/book</code> |<br />| GET, POST, DELETE | <code>/api/v1/addons/polymarket/config</code> |<br />| GET | <code>/api/v1/addons/polymarket/markets</code> |<br />| GET, POST, DELETE | <code>/api/v1/addons/polymarket/orders</code> |<br />| GET | <code>/api/v1/addons/polymarket/positions</code> |<br />| GET | <code>/api/v1/addons/polymarket/status</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<ul><li>Keine bekannten Einschränkungen.</li></ul>\n<blockquote><p>README aus <code>addon.json</code> + vorhandenen Routen generiert (<code>scripts/generate-addon-readmes.js</code>). Bei manuellen Ergänzungen den Generator-Hinweis entfernen.</p></blockquote>",
          "byteSize": 1134
        }
      }
    },
    {
      "id": "proxmox",
      "uuid": "13e9fe70-e0df-4254-9de4-295e89b75d1c",
      "name": "Proxmox VE",
      "version": "1.0.3",
      "description": "Control Proxmox VE — start/stop/shutdown/reboot VMs and LXC containers, node overview and cluster resources via the Proxmox API.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "node-overview",
        "vm-control",
        "lxc-control",
        "cluster-resources",
        "connection-config"
      ],
      "tags": [],
      "download": "/artifacts/proxmox-1.0.3.zip",
      "sha256": "f09479dec077c757fe223817ce359889a802396846b082b2beda641a15aee5f8",
      "size": 104272,
      "page": {
        "id": "proxmox-page",
        "path": "/addons/proxmox",
        "name": "Proxmox VE",
        "icon": "🖥️",
        "section": "Tools",
        "component": "@addons/proxmox/src/components/ProxmoxPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/proxmox/status",
        "/api/v1/addons/proxmox/nodes",
        "/api/v1/addons/proxmox/resources",
        "/api/v1/addons/proxmox/nodes/:node/qemu",
        "/api/v1/addons/proxmox/nodes/:node/qemu/:vmid/status",
        "/api/v1/addons/proxmox/nodes/:node/qemu/:vmid/start",
        "/api/v1/addons/proxmox/nodes/:node/qemu/:vmid/stop",
        "/api/v1/addons/proxmox/nodes/:node/qemu/:vmid/shutdown",
        "/api/v1/addons/proxmox/nodes/:node/qemu/:vmid/reboot",
        "/api/v1/addons/proxmox/nodes/:node/lxc",
        "/api/v1/addons/proxmox/nodes/:node/lxc/:vmid/status",
        "/api/v1/addons/proxmox/nodes/:node/lxc/:vmid/start",
        "/api/v1/addons/proxmox/nodes/:node/lxc/:vmid/stop",
        "/api/v1/addons/proxmox/nodes/:node/lxc/:vmid/shutdown",
        "/api/v1/addons/proxmox/nodes/:node/lxc/:vmid/reboot",
        "/api/v1/addons/proxmox/config",
        "/api/v1/addons/proxmox/test"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Proxmox VE Addon</h1>\n<p>Control a <a href=\"https://www.proxmox.com/\" rel=\"noopener noreferrer\">Proxmox VE</a> cluster from Aiopta:<br />node overview, cluster-wide VM/LXC list, per-guest status, and power actions<br />(start / shutdown / stop / reboot) — via the Proxmox API token.</p>\n<h2>Features</h2>\n<p>- <strong>Node overview</strong> — list cluster nodes and their state.<br />- <strong>Cluster resources</strong> — all QEMU VMs and LXC containers in one call (<code>/resources</code>).<br />- <strong>Guest control</strong> — start, clean-shutdown, hard-stop, reboot for VMs and containers.<br />- <strong>Connection config</strong> — host/port + API token stored on the server; the token<br />  secret is never returned to the client.<br />- <strong>Dashboard widget</strong> <code>proxmox.status</code> — running vs. total guests.<br />- <strong>Agent skill</strong> (<code>SKILL.md</code>) — token-sparing single-REST-call control for agents.</p>\n<h2>Setup</h2>\n<p>1. <strong>Create an API token in Proxmox</strong> (Datacenter → Permissions → API Tokens):<br />   - Pick a user/realm (e.g. <code>root@pam</code>) and a token id (e.g. <code>mc</code>).<br />   - Give the token the permissions it needs (e.g. <code>PVEVMAdmin</code> on <code>/</code> or per-VM).<br />   - Copy the secret — Proxmox shows it <strong>once</strong>.<br />   - The full token id is <code>USER@REALM!TOKENID</code> (e.g. <code>root@pam!mc</code>).<br />2. <strong>Configure in Aiopta</strong> (Proxmox addon → Settings, or <code>PUT /config</code>):<br />   - <code>host</code> (e.g. <code>pve.example.com</code>), <code>port</code> (default <code>8006</code>),<br />   - <code>tokenId</code> (<code>root@pam!mc</code>), <code>tokenSecret</code>,<br />   - <code>verifyTls</code> — <strong>uncheck</strong> if your Proxmox uses the default self-signed cert.<br />3. <strong>Test</strong> the connection (Test button, or <code>POST /test</code>).</p>\n<h2>TLS</h2>\n<p>Proxmox ships a <strong>self-signed</strong> certificate by default. Set <code>verifyTls: false</code><br />to connect anyway (the client then disables certificate verification for the<br />Proxmox host only). Use a proper certificate + <code>verifyTls: true</code> in production.</p>\n<h2>Endpoints</h2>\n<p>Base: <code>/api/v1/addons/proxmox</code></p>\n<p>| Method | Path | Description |<br />|--------|------|-------------|<br />| GET | <code>/status</code> | Reachability + Proxmox version |<br />| GET | <code>/nodes</code> | Cluster node list |<br />| GET | <code>/resources</code> | All VMs + LXC (vmid, node, type, status, name) |<br />| GET | <code>/nodes/{node}/qemu</code> | VMs on a node |<br />| GET | <code>/nodes/{node}/qemu/{vmid}/status</code> | VM status |<br />| POST | <code>/nodes/{node}/qemu/{vmid}/start</code> | Start VM |<br />| POST | <code>/nodes/{node}/qemu/{vmid}/shutdown</code> | Clean shutdown |<br />| POST | <code>/nodes/{node}/qemu/{vmid}/stop</code> | Hard power-off |<br />| POST | <code>/nodes/{node}/qemu/{vmid}/reboot</code> | Reboot |<br />| GET | <code>/nodes/{node}/lxc</code> | Containers on a node |<br />| GET | <code>/nodes/{node}/lxc/{vmid}/status</code> | Container status |<br />| POST | <code>/nodes/{node}/lxc/{vmid}/start\\|shutdown\\|stop\\|reboot</code> | Container power actions |<br />| GET | <code>/config</code> | Current config (token secret masked) |<br />| PUT | <code>/config</code> | Set connection + token (auth-gated) |<br />| POST | <code>/test</code> | Test the configured connection (auth-gated) |</p>\n<p><code>shutdown</code> is a clean shutdown (guest agent / ACPI); <code>stop</code> is a hard power-off.<br />Control routes (POST) and <code>PUT /config</code> run under the per-agent key scoping from<br />Phase S — <code>proxmox/*</code> is its own capability.</p>\n<h2>Architecture</h2>\n<pre><code>src/\n├── lib/\n│   ├── config.ts     # data/proxmox-config.json (load/save/mask)\n│   ├── client.ts     # Proxmox API client (PVEAPIToken auth, TLS opt-out, timeouts)\n│   ├── types.ts      # ProxmoxConfig, PveNode, PveResource, PveGuestStatus, …\n│   └── response.ts    # { success, data } envelope helpers\n├── manifest.ts       # Extension + route registration\n├── page.ts           # Sidebar page (section &quot;Tools&quot;)\n├── components/ProxmoxPage.tsx\n└── app/api/v1/addons/proxmox/...  # route handlers</code></pre>",
          "byteSize": 3509
        }
      }
    },
    {
      "id": "rag",
      "uuid": "c4d66679-decf-4ac0-85ea-76473c76a572",
      "name": "RAG",
      "version": "1.7.0",
      "description": "Retrieval-Augmented Generation: chunk → embed → store → retrieve over a self-hosted Qdrant sidecar, with HYBRID search (semantic dense-vector + lexical BM25/SQLite-FTS5 fused via Reciprocal Rank Fusion, #727), an optional CROSS-ENCODER RERANK stage as the final quality gate (coarse hybrid pool → cross-encoder → top-K; off by default, disableable per call for low-latency voice paths, #728), FILE INGESTION via an OCR-to-markdown pipeline (PDF/Office/scans → normalised markdown → the same chunk+embed+store path; local converter by default, optional Mistral-OCR cloud provider, #729), and CONFIGURABLE CHUNK STRATEGIES with an ENFORCED INDEX-INVARIANT (recursive char-splitter default vs semantic 0-overlap structure-boundary splitter; per-collection {embeddingModelId,dimensions,chunkStrategy} metadata; a model/dimension mismatch on ingest or retrieval is a clear code:\"index_model_mismatch\" error instead of silently broken search; document-wise reindex from the persisted markdown source for model/strategy switches, #730). Standalone search surface; other addons (memories, knowledge-base) detect this capability at runtime via isAddonAvailable(\"rag\").",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "rag",
        "knowledge-management"
      ],
      "tags": [],
      "download": "/artifacts/rag-1.7.0.zip",
      "sha256": "8607a3e25c53d489a5fddf1809b98f7d13d633778d6d49646132325919dd72cb",
      "size": 411491,
      "page": [
        {
          "id": "rag",
          "path": "/addons/rag",
          "name": "RAG",
          "icon": "🔎",
          "section": "Workspace",
          "component": "@addons/rag/src/components/rag/RagConsole"
        }
      ],
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/rag",
        "/api/v1/addons/rag/status",
        "/api/v1/addons/rag/ingest",
        "/api/v1/addons/rag/ingest/file",
        "/api/v1/addons/rag/reindex",
        "/api/v1/addons/rag/search",
        "/api/v1/addons/rag/documents",
        "/api/v1/addons/rag/documents/{id}"
      ],
      "skills": [],
      "mcpServers": [
        {
          "id": "rag-tools",
          "displayName": "RAG tools",
          "description": "Read-only semantic search over the RAG vector index for agents: rag_search (scoped to the calling user + agent, Qdrant payload filter enforces isolation) and rag_status (pipeline health). Read-only Policy — no ingest/delete/write tools (issue #384 contract, #584).",
          "transport": "stdio",
          "runtime": "node",
          "entry": "rag-tools/server.mjs",
          "permissions": [
            "addons/rag/read"
          ],
          "toolPolicy": {
            "mode": "read-only",
            "allowDestructive": false
          },
          "configRefs": [],
          "credentialRefs": [],
          "archiveEntry": "rag/mcp/servers/rag-tools/server.mjs",
          "sha256": "8701beb71660cf7151614067f8a1cd9b70cf3d81c49e23603771735b96ff5c30",
          "size": 14952,
          "descriptorVersion": 1
        }
      ],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>RAG</h1>\n<p>Retrieval-Augmented Generation: a standalone semantic-search addon that chunks,<br />embeds, stores and retrieves documents over a self-hosted Qdrant sidecar. Other<br />addons (memories, knowledge-base) detect this capability at runtime via<br /><code>isAddonAvailable(&quot;rag&quot;)</code> and use it <strong>optionally</strong> — falling back to their own<br />(SQL <code>LIKE</code>) search when RAG is not installed or the Qdrant sidecar is absent.</p>\n<p>This README covers the addon surface (Issue #587, <strong>Slice 1</strong>). The pipeline<br />itself lives in the core library <code>src/lib/rag/</code> (see<br /><code>docs/rag.md</code> in aiopta-core); this addon only <em>consumes</em><br />that library.</p>\n<h2>Überblick</h2>\n<p>- <strong>Addon-ID:</strong> <code>rag</code><br />- <strong>Version:</strong> 1.7.0<br />- <strong>Fähigkeiten (capabilities):</strong> <code>rag</code>, <code>knowledge-management</code><br />- <strong>UI-Seite:</strong> <code>/addons/rag</code> (RAG-Konsole: Status, Ingest, Search)<br />- <strong>Abhängigkeiten:</strong> keine (standalone). Optionaler Qdrant-Sidecar<br />  (<code>docker compose up -d qdrant</code>).</p>\n<h2>Knowledge-Anbieter 1 „vector&quot; (core#816/#818)</h2>\n<p>Seit dem Knowledge-Epic (core#816, Slice 2 core#818) ist RAG <strong>Anbieter 1</strong><br />(<code>kind: &quot;vector&quot;</code>) der generischen knowledge-management-Fähigkeit: Der<br /><code>RagService</code> wird über <code>src/lib/knowledge-adapter.ts</code> auf den Vertrag<br /><code>KnowledgeManagementCapability</code> aus <code>@aiopta/sdk</code> abgebildet und in<br /><code>src/lib/capability-register.ts</code> unter <code>core.knowledge.management</code><br />registriert (Provider-Liste der Core-Registry, <code>registerCapabilityProvider</code>).<br />Verbraucher codieren gegen die Fähigkeit, nie gegen dieses Addon; welcher<br />Anbieter für wen aktiv ist, entscheiden Resolver + Zustimmung (core#820/#821).<br />Wichtig: Wissen ≠ Gedächtnis (Owner-Entscheidung 2026-08-03) — was der Agent<br />über den Nutzer behält, bleibt allein im <code>memories</code>-Addon.</p>\n<h2>Architektur (Owner-Entscheidung D5, 2026-07-08)</h2>\n<p>RAG ist ein <strong>eigenständiges Addon</strong> — kein direkter Einbau in <code>memories</code>. Das<br />Pattern ist <strong>Capability-Detection</strong>:</p>\n<pre><code class=\"lang-ts\">import { isAddonAvailable } from &quot;@/lib/addons/availability&quot;;</code></pre>\n<p>if (await isAddonAvailable(&quot;rag&quot;)) {<br />  // use src/lib/rag vector search (chunk + embed + retrieve, Qdrant)<br />} else {<br />  // existing SQL LIKE search (fallback, no regression)<br />  // + store-link in the UI: &quot;Install the RAG addon for semantic search&quot;<br />}<br />```</p>\n<p>- <strong>NICHT</strong> <code>addonDependencies</code> (hard deps) — RAG ist optional, kein 409-Block.<br />- <strong><code>isAddonAvailable(&quot;rag&quot;)</code></strong> (<code>availability.ts:154</code>) ist die kanonische<br />  Prüfung. Keine neue <code>hasCapability</code> erfinden.<br />- <strong>Registry:</strong> <code>addonRegistry.getByCapability(&quot;rag&quot;)</code> findet dieses Addon.</p>\n<h2>Pipeline (konsumiert <code>src/lib/rag/</code>)</h2>\n<pre><code>document text ──► chunk (rag/chunk.ts)\n              ──► embed (rag/embedding.ts)   ← injectable provider\n              ──► store (rag/vector-store.ts) ← Qdrant sidecar, degrades gracefully\nquery text    ──► embed ──► search (store) ──► rank (rag/retrieve.ts)</code></pre>\n<p>| Stage | Core module | Notes |<br />|---|---|---|<br />| Chunking | <code>rag/chunk.ts</code> | Greedy word packing with overlap, exact char offsets. |<br />| Embedding | <code>rag/embedding.ts</code> | Injectable <code>EmbeddingProvider</code>; deterministic hash fallback (network-free) for tests/CI. |<br />| Store | <code>rag/vector-store.ts</code> | <code>QdrantVectorStore</code> over the REST API. Never throws on connectivity. |<br />| Retrieve | <code>rag/retrieve.ts</code> | Pure cosine ranking + merge/dedupe. |</p>\n<h2>Graceful degradation</h2>\n<p>Das Addon <strong>crasht nie</strong>, weil der optionale Qdrant-Sidecar fehlt:</p>\n<p>- <code>createVectorStore()</code> liefert <code>null</code>, wenn <code>RAG_VECTOR_STORE_ENABLED=false</code>.<br />- Jede <code>QdrantVectorStore</code>-Methode fängt Connectivity-Fehler ab, loggt eine<br />  Warnung und resolve zu einem No-op / leeren Ergebnis. <code>store.available</code><br />  reflektiert den letzten Kontakt.<br />- Der deterministische Embedding-Fallback braucht kein Netzwerk/keinen API-Key,<br />  sodass die volle Pipeline (chunk → embed → rank) in CI ohne Container läuft.<br />- <code>GET /status</code> meldet <code>storeAvailable:false</code> — die UI rendert einen klaren<br />  &quot;nicht bereit&quot;-Zustand, kein Crash.</p>\n<h2>User-Isolation</h2>\n<p>Jeder Vektor-Punkt trägt ein <code>user_id</code>-Payload-Feld (vom Server aus der Session<br />gestanzt, niemals ein Client-Wert). Die Suche filtert standardmäßig auf<br /><code>user_id = &lt;caller&gt;</code> — User A sieht <strong>niemals</strong> User B&#39;s Dokumente. Der<br /><code>includeOthers</code>-Parameter (Admin-Affordanz) lockert den Filter bewusst.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode | Pfad | Beschreibung |<br />|---|---|---|<br />| GET | <code>/api/v1/addons/rag</code> | Addon-Deskriptor (id, capabilities, endpoints). |<br />| GET | <code>/api/v1/addons/rag/status</code> | Pipeline-Health + Konfiguration (Provider, Dimensions, Qdrant-Verfügbarkeit). Wirft nie. |<br />| POST | <code>/api/v1/addons/rag/ingest</code> | Dokument chunken + embedden + speichern. <code>user_id</code> vom Server gestanzt. Degradiert zu <code>stored:false</code>, wenn Qdrant nicht erreichbar. |<br />| POST | <code>/api/v1/addons/rag/ingest/file</code> | Multipart-Datei-Ingestion (#729): PDF/Office/Scan/Text → OCR-zu-Markdown-Konvertierung → dieselbe chunk+embed+store-Pipeline. 400 bei unsupported/oversize, 409 <code>code:&quot;unconfigured&quot;</code> wenn der benötigte Konverter fehlt (nie 5xx). |<br />| POST | <code>/api/v1/addons/rag/search</code> | Semantische Suche, gescoped auf den Caller (<code>user_id</code>-Payload-Filter). Leeres Resultat (kein Fehler), wenn der Store nicht verfügbar ist. |<br />| GET | <code>/api/v1/addons/rag/documents</code> | Distinct ingested document ids des Callers (read-only). |<br />| DELETE | <code>/api/v1/addons/rag/documents/{id}</code> | Löscht ein Dokument + alle seine Chunks (idempotent, gescoped). |</p>\n<h3>Beispiel</h3>\n<pre><code class=\"lang-bash\"># Ingest a document\ncurl -s -X POST -H &quot;Authorization: Bearer $AIOPTA_API_KEY&quot; -H &quot;Content-Type: application/json&quot; \\\n  -d &#39;{&quot;id&quot;:&quot;readme-v1&quot;,&quot;text&quot;:&quot;The quick brown fox jumps over the lazy dog.&quot;}&#39; \\\n  https://&lt;instance&gt;/api/v1/addons/rag/ingest</code></pre>\n<p># Semantic search<br />curl -s -X POST -H &quot;Authorization: Bearer $AIOPTA_API_KEY&quot; -H &quot;Content-Type: application/json&quot; \\<br />  -d &#39;{&quot;query&quot;:&quot;quick fox&quot;,&quot;topK&quot;:5}&#39; \\<br />  <a href=\"https://&amp;lt;instance&amp;gt;/api/v1/addons/rag/search\" rel=\"noopener noreferrer\">https://&amp;lt;instance&amp;gt;/api/v1/addons/rag/search</a><br />```</p>\n<p>Responses use <code>{ success, data | error }</code>.</p>\n<h2>Datei-Ingestion: OCR-zu-Markdown-Pipeline (Issue #729)</h2>\n<p>Praxis-Dokumente liegen selten als sauberer Text vor. Am Anfang der<br />Indexierungs-Pipeline steht deshalb ein Konvertierungs-Schritt, der **jede<br />Datei in normalisiertes Markdown<strong> umwandelt — es wird </strong>immer bereinigtes<br />Markdown<strong> in den Wissensspeicher geladen, nie rohe PDFs. Erst </strong>nach** der<br />Markdown-Normalisierung wird gechunkt und embedded (dieselbe Pipeline wie<br /><code>POST /ingest</code> — keine Parallel-Pipeline).</p>\n<p><strong>Konverter-Provider</strong> (<code>src/lib/markdown-converter.ts</code>):</p>\n<p>| Provider | Pfad | Kann | Kann NICHT |<br />|---|---|---|---|<br />| <code>local-text</code> (<strong>Default</strong>) | lokal, kein Datenabfluss | Text/Markdown-Passthrough; Text-PDFs via <code>pdftotext</code> (poppler, falls installiert) bzw. eingebauter Extraktor für einfache/unkomprimierte PDFs | <strong>Scans/Bild-PDFs, Bilder, Office</strong> (dokumentierte Grenze — dafür ist OCR nötig) |<br />| <code>mistral-ocr</code> (optional) | <strong>Cloud</strong> — Inhalt verlässt die Instanz | Scans, Bilder, Layout-PDFs, Office-Dokumente → strukturiertes Markdown | läuft nur mit konfiguriertem API-Key |</p>\n<p><strong>DSGVO / Datenabfluss (Betriebs-Hinweis):</strong> Der lokale Pfad ist der Default<br />und verlässt die Instanz nie. Cloud-OCR (<code>mistral-ocr</code>) wird <strong>nur</strong> genutzt,<br />wenn (a) ein Key konfiguriert ist <strong>und</strong> (b) der lokale Konverter den Inhalt<br />nicht extrahieren kann (Scan/Bild/Office). Jede Nutzung ist gekennzeichnet:<br /><code>GET /status</code> meldet pro Provider <code>cloud: true/false</code>, die Antwort von<br /><code>POST /ingest/file</code> trägt ein <code>cloud</code>-Flag und die RAG-Konsole zeigt den<br />Egress-Marker (&quot;content leaves this instance&quot;) an. Betreiber, die keinen<br />Datenabfluss wollen, konfigurieren schlicht keinen <code>RAG_MISTRAL_OCR_API_KEY</code><br />— Scan-Uploads antworten dann sauber mit <code>409 code:&quot;unconfigured&quot;</code> (#515),<br />nie mit einem 5xx.</p>\n<p><strong>Persistierte Markdown-Quelle / Reindex ohne OCR:</strong> Das normalisierte<br />Markdown wird pro <code>(user_id, document_id)</code> in der Tabelle<br /><code>rag_markdown_sources</code> gespeichert (user-scoped, ADR-054). Re-Chunking/<br />Reindex (<code>reindexDocument()</code> in <code>src/lib/file-ingest.ts</code>) liest diese Quelle<br />und läuft <strong>ohne erneutes OCR</strong>. Re-Ingest derselben Dokument-Id ersetzt<br />Chunks <strong>und</strong> Markdown-Quelle vollständig (id-stabil, kein<br />Duplikat-Wachstum).</p>\n<p><strong>Konfiguration:</strong></p>\n<pre><code class=\"lang-bash\">RAG_INGEST_FILE_MAX_BYTES=20971520          # Upload-Limit (Default 20 MiB)\nRAG_MISTRAL_OCR_API_KEY=&lt;key&gt;               # aktiviert mistral-ocr (Fallback: MISTRAL_API_KEY)\nRAG_MISTRAL_OCR_BASE_URL=https://api.mistral.ai\nRAG_MISTRAL_OCR_MODEL=mistral-ocr-latest\n# Optional fuer bessere lokale Text-PDF-Extraktion: poppler-utils installieren\n# (apt-get install poppler-utils) — ohne poppler greift der eingebaute\n# Extraktor fuer einfache PDFs; komprimierte Text-PDFs brauchen dann OCR.</code></pre>\n<h2>Qdrant-Sidecar (Operator)</h2>\n<pre><code class=\"lang-bash\"># Start the sidecar\ndocker compose up -d qdrant</code></pre>\n<p># Smoke-check<br />curl -s http://localhost:6333/readyz</p>\n<p># Defaults already match; override only if needed<br /># RAG_VECTOR_STORE_URL=http://qdrant:6333   # inside the compose network<br /># RAG_VECTOR_STORE_ENABLED=false            # disable RAG entirely<br />```</p>\n<p>Env vars: <code>RAG_VECTOR_STORE_URL</code>, <code>RAG_VECTOR_STORE_API_KEY</code>,<br /><code>RAG_VECTOR_STORE_ENABLED</code>, <code>RAG_EMBEDDING_PROVIDER</code>,<br /><code>RAG_EMBEDDING_DIMENSIONS</code>. Siehe aiopta-core <code>.env.example</code> + <code>docs/rag.md</code>.</p>\n<h2>Slice-Plan (Issue #587)</h2>\n<p>- <strong>Slice 1 (dieser PR):</strong> Standalone Addon-Surface + Manifest. Konsumiert<br />  <code>src/lib/rag/</code> + Qdrant-Sidecar. <strong>Keine</strong> Consumer-Integration.<br />- <strong>Slice 2 (Folge-Issue):</strong> memories-Integration (<code>searchMemories()</code><br />  Vektor-Search-Fallback + Isolation + Store-Link).<br />- <strong>Slice 3 (Folge-Issue):</strong> MCP-Server für RAG (<code>rag-tools</code>, read-only search)<br />  + realer Embedding-Provider (OpenAI/Ollama).</p>",
          "byteSize": 9546
        }
      }
    },
    {
      "id": "real-estate-investor",
      "uuid": "d5cb732a-73d8-478a-8f77-519df4ca5086",
      "name": "Real Estate Investor",
      "version": "1.0.2",
      "description": "Tracks investment property leads from IMAP and external market sources, calculates deal metrics, and ranks opportunities by configurable scoring.",
      "author": "Aiopta",
      "icon": "🏘️",
      "capabilities": [
        "real-estate-tracking",
        "imap-ingest",
        "market-comparison",
        "deal-scoring",
        "investment-analysis"
      ],
      "tags": [],
      "download": "/artifacts/real-estate-investor-1.0.2.zip",
      "sha256": "18e7de527214eab30af30ef056205c877fcda034dc2fc85a4d72bb5655cd80df",
      "size": 123693,
      "page": {
        "id": "real-estate-investor",
        "name": "Real Estate Investor",
        "path": "/addons/real-estate-investor",
        "icon": "🏘️",
        "section": "Finance",
        "component": "@addons/real-estate-investor/src/components/RealEstateInvestorPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/real-estate-investor/status",
        "/api/v1/addons/real-estate-investor/config",
        "/api/v1/addons/real-estate-investor/regions",
        "/api/v1/addons/real-estate-investor/sources",
        "/api/v1/addons/real-estate-investor/sync/mail",
        "/api/v1/addons/real-estate-investor/sync/market",
        "/api/v1/addons/real-estate-investor/candidates",
        "/api/v1/addons/real-estate-investor/candidates/:id/promote",
        "/api/v1/addons/real-estate-investor/objects",
        "/api/v1/addons/real-estate-investor/objects/:id"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>🏘️ Real Estate Investor</h1>\n<p>Tracks investment property leads from IMAP and external market sources, calculates deal metrics, and ranks opportunities by configurable scoring.</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>real-estate-investor</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> real-estate-tracking, imap-ingest, market-comparison, deal-scoring, investment-analysis</li><li><strong>UI-Seite:</strong> <code>/addons/real-estate-investor</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET, POST | <code>/api/v1/addons/real-estate-investor/candidates</code> |<br />| POST | <code>/api/v1/addons/real-estate-investor/candidates/[id]/promote</code> |<br />| GET, POST | <code>/api/v1/addons/real-estate-investor/config</code> |<br />| GET, POST | <code>/api/v1/addons/real-estate-investor/objects</code> |<br />| GET, PUT | <code>/api/v1/addons/real-estate-investor/objects/[id]</code> |<br />| GET, POST | <code>/api/v1/addons/real-estate-investor/regions</code> |<br />| GET, POST | <code>/api/v1/addons/real-estate-investor/sources</code> |<br />| GET | <code>/api/v1/addons/real-estate-investor/status</code> |<br />| POST | <code>/api/v1/addons/real-estate-investor/sync/mail</code> |<br />| POST | <code>/api/v1/addons/real-estate-investor/sync/market</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<ul><li>Im Manifest <strong>deaktiviert</strong> (<code>enabled: false</code>).</li></ul>\n<blockquote><p>README aus <code>addon.json</code> + vorhandenen Routen generiert (<code>scripts/generate-addon-readmes.js</code>). Bei manuellen Ergänzungen den Generator-Hinweis entfernen.</p></blockquote>",
          "byteSize": 1407
        }
      }
    },
    {
      "id": "revenue-cat",
      "uuid": "41e76065-d882-4591-b1c6-51d51cf47553",
      "name": "Revenue Cat (Subscription-Management + Entitlements)",
      "version": "1.0.1",
      "description": "Subscription-Management über Revenue Cat — per-user BYOK (eigenes API-Projekt + Secret-API-Key via scopedStore(userId), ADR-054). Holt aktive Subscriptions, Entitlements, Customer-Info und Offerings von Revenue Cats REST API (fetch, kein SDK) und brückt Revenue-Cat-Entitlements ins Core-Entitlement-System (hasEntitlement, core#583). Ein read-only MCP-Server (revenue-cat-tools) exponiert die Daten für Agenten (#384). Ohne konfigurierten API-Key läuft das Addon graceful im Lesemodus — KEIN Default/Anonymous-Zugriff (INBOX-86-Analogie). Keys werden NIE geloggt. Money-Stufe-1 (ADR-039).",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "subscriptions",
        "entitlements",
        "customers",
        "offerings"
      ],
      "tags": [],
      "download": "/artifacts/revenue-cat-1.0.1.zip",
      "sha256": "84b03e21939ceac8a51496749e51f35652400d9f7e0617867394fa34fb4cd01d",
      "size": 98622,
      "page": {
        "id": "revenue-cat-page",
        "path": "/addons/revenue-cat",
        "name": "Revenue Cat",
        "icon": "🐱",
        "section": "Money",
        "component": "@addons/revenue-cat/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/revenue-cat/status",
        "/api/v1/addons/revenue-cat/config",
        "/api/v1/addons/revenue-cat/subscriptions",
        "/api/v1/addons/revenue-cat/entitlements",
        "/api/v1/addons/revenue-cat/customer",
        "/api/v1/addons/revenue-cat/offerings",
        "/api/v1/addons/revenue-cat/widget"
      ],
      "skills": [],
      "mcpServers": [
        {
          "id": "revenue-cat-tools",
          "displayName": "Revenue Cat tools",
          "description": "Read-only Revenue Cat companion tools for agents: normalize/filter active subscriptions, project a customer's entitlements, summarize the customer info (active subs + non-subscription purchases) and project an offerings/package tree — faithful ports of the revenue-cat addon's src/lib projection helpers (issue #545, epic #399). Mutates nothing; live per-user Revenue Cat access is resolved core-side via the declared config/credential slots.",
          "transport": "stdio",
          "runtime": "node",
          "entry": "revenue-cat-tools/server.mjs",
          "permissions": [
            "addons/revenue-cat/read"
          ],
          "toolPolicy": {
            "mode": "read-only",
            "allowDestructive": false
          },
          "configRefs": [
            "projectId"
          ],
          "credentialRefs": [
            "apiKey"
          ],
          "archiveEntry": "revenue-cat/mcp/servers/revenue-cat-tools/server.mjs",
          "sha256": "f5f50508a918f474995661bab9f6f54c9cd5ee1f8397445d59ebe554133f3eea",
          "size": 19924,
          "descriptorVersion": 1
        }
      ],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Revenue Cat Addon — Subscription-Management + Entitlements + MCP (#545)</h1>\n<p>Subscription-Management über <a href=\"https://www.revenuecat.com/\" rel=\"noopener noreferrer\">Revenue Cat</a> — per-user<br />BYOK (Bring-Your-Own-Key), an Aiopta angebunden. Holt aktive Subscriptions,<br />Entitlements, Customer-Info und Offerings von Revenue Cats REST API und brückt<br />Revenue-Cat-Entitlements ins Core-Entitlement-System (<code>hasEntitlement</code>, core#583).<br />Ein read-only MCP-Server exponiert die Daten für Agenten.</p>\n<blockquote><p>Owner-Entscheidung D5 (2026-07-08): neue Commerce/Payment-Addons. Revenue Cat</p><p>ist <strong>neu</strong> — kein bestehendes Aiopta-Addon deckt es ab (<code>payments</code> → Stripe/</p><p>PayPal, <code>membership</code> → Abo-Modellierung, aber keines spricht Revenue Cat an).</p></blockquote>\n<h2>Eigenschaften</h2>\n<p>- <strong>BYOK per-user (ADR-054):</strong> jeder User hinterlegt seinen eigenen Revenue Cat<br />  <code>projectId</code> + Secret-API-Key über <code>scopedStore(userId)</code>. Der Key wird <strong>nie</strong><br />  geloggt und <strong>nie</strong> über die API zurückgegeben (nur ein boolean <code>hasApiKey</code>).<br />- <strong>Graceful Read-Mode:</strong> ohne konfigurierten Key läuft das Addon im Lesemodus —<br />  <strong>kein</strong> Default/Anonymous-Zugriff (INBOX-86-Analogie, wie das <code>payments</code>-Addon).<br />- <strong>User-Isolation:</strong> User A sieht <strong>nur</strong> seine eigenen Revenue-Cat-Daten<br />  (<code>user_id</code>-Filter via <code>scopedStore</code>).<br />- <strong>Entitlement-Bridge:</strong> Revenue-Cat-Entitlements → Aiopta-Entitlement-Keys<br />  (<code>addon.&lt;id&gt;</code> / <code>rc.&lt;id&gt;</code>), die mit dem Core-Gate (<code>hasEntitlement</code>,<br />  <code>hasAddonEntitlement</code>, core#583) komponieren.<br />- <strong>MCP-Server (read-only):</strong> <code>revenue-cat-tools</code> exponiert Projektions-Tools für<br />  Agenten (<code>list_subscriptions</code>, <code>get_entitlements</code>, <code>get_customer</code>,<br />  <code>list_offerings</code>) — validiert gegen den <code>#384</code>-Contract.</p>\n<h2>API-Endpunkte</h2>\n<p>Alle Endpunkte sind namespaced unter <code>/api/v1/addons/revenue-cat</code>. Daten-Endpunkte<br />benötigen <code>customer_id</code> (den Revenue Cat app user id) und einen konfigurierten Key.</p>\n<p>| Methode | Pfad | Beschreibung |<br />|---|---|---|<br />| GET | <code>/status</code> | Konfiguriert? (boolean, kein Secret-Leak) |<br />| GET/POST | <code>/config</code> | BYOK-Config lesen/schreiben (Key nie zurückgegeben) |<br />| GET | <code>/subscriptions?customer_id=&lt;id&gt;&amp;active=true</code> | Subscriptions eines Kunden |<br />| GET | <code>/entitlements?customer_id=&lt;id&gt;</code> | Entitlements + Aiopta-Keys |<br />| GET | <code>/customer?customer_id=&lt;id&gt;</code> | Customer-Info + Summary |<br />| GET | <code>/offerings</code> | Project-Offerings |<br />| GET | <code>/widget</code> | Dashboard-Key-Values (wirft nie) |</p>\n<h2>BYOK konfigurieren</h2>\n<pre><code class=\"lang-bash\">curl -X POST /api/v1/addons/revenue-cat/config \\\n  -H &#39;Content-Type: application/json&#39; \\\n  -d &#39;{&quot;projectId&quot;:&quot;abc123def&quot;,&quot;apiKey&quot;:&quot;sk_xxx_…&quot;}&#39;</code></pre>\n<p>Der <code>apiKey</code> (Secret-API-Key aus dem Revenue Cat Dashboard) wird per-user gespeichert<br />und nie zurückgegeben. <code>projectId</code> ist die Projekt-ID aus der Dashboard-URL.</p>\n<h2>MCP-Server <code>revenue-cat-tools</code></h2>\n<p>Ein Node-STDIO-Server (Contract <code>#384</code>), deklariert in <code>addon.json</code> → <code>mcpServers[]</code>.<br />Permissions: <code>[&quot;addons/revenue-cat/read&quot;]</code>, <code>toolPolicy: read-only</code>. BYOK-Slots:<br /><code>configRefs: [&quot;projectId&quot;]</code>, <code>credentialRefs: [&quot;apiKey&quot;]</code> (per Namen, nie Werte —<br />die Core-Runtime löst sie pro aufrufendem User auf, core#454).</p>\n<p>Tools (alle read-only, annotiert <code>readOnlyHint:true</code>/<code>destructiveHint:false</code>):</p>\n<ul><li><code>list_subscriptions</code> — aktive Subscriptions filtern</li><li><code>get_entitlements</code> — aktive Entitlements → Aiopta-Keys (core#583)</li><li><code>get_customer</code> — Customer-Summary (aktive Abos, Entitlements, Käufe)</li><li><code>list_offerings</code> — Offerings/Packages projizieren</li></ul>\n<p>Validierung: <code>npm run test:mcp-servers</code> (Konformanz-Suite entdeckt das Addon<br />automatisch) + <code>scripts/addon-mcp-pilot-revenue-cat.test.cjs</code> (Golden Values).</p>\n<h2>Architektur</h2>\n<pre><code>addons/revenue-cat/\n  addon.json                         # Manifest + mcpServers[]-Deskriptor\n  README.md\n  src/\n    lib/\n      credential-store.ts            # BYOK per-user via scopedStore (ADR-054)\n      revenue-cat-client.ts          # REST-API-Client (fetch, kein SDK), BYOK\n      projections.ts                 # Pure Projektions-Helper (aktiv-Filter etc.)\n      entitlement-bridge.ts          # RC-Entitlements → Core hasEntitlement (core#583)\n    app/api/v1/addons/revenue-cat/   # Routen (status, config, subscriptions, …)\n    page.tsx                         # Read-only Management-Seite (Money-Section)\n  mcp/servers/revenue-cat-tools/\n    server.mjs                       # Read-only MCP-Server (faithful ports)\n  tests/unit/\n    manifest.test.ts\n    revenue-cat-isolation.test.ts\n    revenue-cat-graceful.test.ts\n    revenue-cat-projections.test.ts</code></pre>\n<p><strong>NICHT anfassen:</strong> Core <code>src/lib/entitlements.ts</code> (nur konsumieren),<br /><code>src/lib/license/*</code>, <code>payments</code>/<code>membership</code>-Addon-Internals, Core-Routen.</p>\n<h2>Money-Stufe &amp; Sicherheit</h2>\n<p>- Money-Stufe-1 (ADR-039). Webhook-Signature-Verifizierung folgt dem<br />  <code>payments</code>-Muster (falls Webhooks implementiert werden).<br />- Rate-Limit von Revenue Cat → Backoff/Retry (client-seitig via Timeout).<br />- Ungültiger API-Key → HTTP 401, Fehleranzeige, keine stille Recovery.</p>\n<h2>Tests</h2>\n<pre><code class=\"lang-bash\">npx vitest run addons/revenue-cat/tests/unit          # Addon-Unit (BYOK, Isolation, Graceful, Projektionen)\nnpm run test:mcp-servers                              # MCP-Konformanz + Golden Values</code></pre>",
          "byteSize": 5120
        }
      }
    },
    {
      "id": "reverse-proxy",
      "uuid": "3d3c832b-4b84-440c-8b9e-21ec883809f5",
      "name": "Reverse Proxy (NPM/Traefik/Caddy — Domains, Routen, SSL + Ablauf-Warnungen)",
      "version": "1.0.2",
      "description": "Per-User Reverse-Proxy-Monitoring: Verbindet sich BYOK mit Nginx Proxy Manager, Traefik oder Caddy. Listet Domains, Proxy-Routen und SSL-Zertifikate; warnt vor bald auslaufenden Zertifikaten (warn/critical) und prüft die Health der Routen. Read-only gegenüber dem Proxy, vollständig per-User über scopedStore isoliert (ADR-054).",
      "author": "Aiopta",
      "icon": "🛡️",
      "capabilities": [
        "reverse-proxy",
        "domains",
        "routes",
        "certs",
        "ssl",
        "health",
        "cert-expiry"
      ],
      "tags": [],
      "download": "/artifacts/reverse-proxy-1.0.2.zip",
      "sha256": "1bfd9040c802ded9c85d9e65bf758c6cf93233d2c2377ddcece0752c9d33ce29",
      "size": 103399,
      "page": {
        "id": "reverse-proxy-page",
        "path": "/addons/reverse-proxy",
        "name": "Reverse Proxy",
        "icon": "🛡️",
        "section": "Infra",
        "component": "@addons/reverse-proxy/src/components/ReverseProxyPage"
      },
      "deprecated": true,
      "replacedBy": "network",
      "deprecation": {
        "deprecated": true,
        "replacedBy": "network",
        "sunset": "2026-12-31",
        "successorPath": "/addons/network",
        "successorLabel": "Network/Homelab (Nachfolger)"
      },
      "endpoints": [
        "/api/v1/addons/reverse-proxy/config",
        "/api/v1/addons/reverse-proxy/routes",
        "/api/v1/addons/reverse-proxy/certs",
        "/api/v1/addons/reverse-proxy/health"
      ],
      "skills": [
        {
          "name": "reverse-proxy-read",
          "displayName": "Reverse Proxy: Domains, Routen & SSL-Zertifikate lesen",
          "description": "Listet Domains, Proxy-Routen und SSL-Zertifikate des konfigurierten Reverse Proxies (NPM/Traefik/Caddy) für den Nutzer und zeigt Zertifikats-Ablauf-Warnungen. Reiner Lesezugriff — legt nichts an.",
          "permissions": [
            "addons/reverse-proxy/config",
            "addons/reverse-proxy/routes",
            "addons/reverse-proxy/certs",
            "addons/reverse-proxy/health"
          ],
          "endpoint": "/api/v1/addons/reverse-proxy/routes",
          "scripts": []
        },
        {
          "name": "reverse-proxy-config",
          "displayName": "Reverse Proxy: Verbindung verwalten",
          "description": "Legt die BYOK-Verbindung zum Reverse Proxy an oder entfernt diese (per-User gebucht: provider, base URL, Token — niemals das Passwort im Klartext zurückgegeben).",
          "permissions": [
            "addons/reverse-proxy/config"
          ],
          "endpoint": "/api/v1/addons/reverse-proxy/config",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Reverse Proxy (NPM / Traefik / Caddy)</h1>\n<p>Per-User Reverse-Proxy-Monitoring (#515). BYOK-Verbindung zu Nginx Proxy<br />Manager, Traefik oder Caddy; listet Domains, Proxy-Routen und SSL-Zertifikate;<br />warnt vor bald auslaufenden Zertifikaten (<code>warn</code> / <code>critical</code> / <code>expired</code>) und<br />prüft die Health überwachter Routen.</p>\n<h2>Eigenschaften</h2>\n<p>- <strong>BYOK per User</strong> — jeder Nutzer hinterlegt seine eigene Proxy-Verbindung<br />  (provider, base URL, token). Token wird gespeichert, aber niemals im<br />  Klartext zurückgegeben (maskiert).<br />- <strong>Read-only gegenüber dem Proxy</strong> — das Addon mutiert nie eine Upstream-Route<br />  oder ein Zertifikat. Es spiegelt und klassifiziert nur.<br />- <strong>Per-User isoliert</strong> — alle Daten via <code>scopedStore</code> + <code>registerScopedTable</code><br />  (ADR-054). Eine fremde id → 404 ohne Existenz-Leak.<br />- <strong>Cert-Expiry-Evaluator (pure)</strong> — `notAfter → days-remaining →<br />  ok/warn/critical/expired/unknown`. Schwellen default 30/7 Tage.<br />- <strong>Gracious Degradation</strong> — ein nicht erreichbarer Proxy skippt die Verbindung,<br />  statt die gesamte Anzeige scheitern zu lassen.</p>\n<h2>Struktur</h2>\n<pre><code>src/\n  manifest.ts            # Extension-Manifest (Routes + Page + install/uninstall)\n  page.ts                # Page-Deskriptor\n  components/\n    ReverseProxyPage.tsx # Dashboard (Domains / SSL / Health / Verbindungen)\n  lib/\n    rp-store.ts               # scopedStore: connections, routes, certs\n    cert-expiry-evaluator.ts  # PURE: notAfter → days-remaining → level\n    proxy-types.ts            # neutrale Shapes + safeFetch + injectable fetch\n    proxy-clients.ts          # provider dispatch\n    npm-client.ts             # Nginx Proxy Manager (read-only)\n    traefik-client.ts         # Traefik (read-only)\n    caddy-client.ts           # Caddy admin API (read-only)\n  app/api/v1/addons/reverse-proxy/\n    config/route.ts + [id]/route.ts\n    routes/route.ts + [id]/route.ts\n    certs/route.ts\n    health/route.ts\ntests/unit/\n    cert-expiry.test.ts       # reiner Evaluator (kein DB)\n    proxy-clients.test.ts     # fetch seam gemockt, provider-mapping\n    reverse-proxy-isolation.test.ts  # store-level per-user isolation</code></pre>\n<h2>Routen</h2>\n<p>| Method | Path | Beschreibung |<br />|--------|------|--------------|<br />| GET    | <code>/api/v1/addons/reverse-proxy/config</code> | Verbindungen (maskiert) |<br />| POST   | <code>/api/v1/addons/reverse-proxy/config</code> | Verbindung anlegen |<br />| DELETE | <code>/api/v1/addons/reverse-proxy/config/:id</code> | Verbindung entfernen |<br />| GET    | <code>/api/v1/addons/reverse-proxy/routes</code> | Monitored domains (<code>?sync=1</code> spiegelt Zertifikate) |<br />| POST   | <code>/api/v1/addons/reverse-proxy/routes</code> | Domain überwachen |<br />| PATCH  | <code>/api/v1/addons/reverse-proxy/routes/:id</code> | aktiv/inaktiv |<br />| DELETE | <code>/api/v1/addons/reverse-proxy/routes/:id</code> | entfernen |<br />| GET    | <code>/api/v1/addons/reverse-proxy/certs</code> | Zertifikate + Expiry-Level + worst-Level |<br />| GET    | <code>/api/v1/addons/reverse-proxy/health</code> | Probed aktiver Routen (up/down/unknown) |</p>",
          "byteSize": 2929
        }
      }
    },
    {
      "id": "seo-content",
      "uuid": "f877087a-5162-4d62-97d3-861752855653",
      "name": "SEO Content Engine",
      "version": "1.0.1",
      "description": "Keyword → AI draft → on-page optimize → publish pipeline plus rank tracking. Research goes through the websearch addon, AI drafting through the MC gateway, publishing through the wordpress addon (all via MC-API); the rank-tracker SEO-data provider is an external SaaS reached with the owner's API key from MC-config (no container, ADR-014).",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "seo-keyword-research",
        "seo-article-generation",
        "seo-on-page-analysis",
        "seo-publishing",
        "seo-rank-tracking"
      ],
      "tags": [],
      "download": "/artifacts/seo-content-1.0.1.zip",
      "sha256": "4b2d1dbdaa57c11a18a74e58d90a35459c79374216b053fb1f57145963f07cf0",
      "size": 151244,
      "page": {
        "id": "seo-content-page",
        "path": "/addons/seo-content",
        "name": "SEO Content Engine",
        "icon": "📈",
        "section": "Money",
        "component": "@addons/seo-content/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/seo-content/status",
        "/api/v1/addons/seo-content/keywords",
        "/api/v1/addons/seo-content/articles",
        "/api/v1/addons/seo-content/articles/:id",
        "/api/v1/addons/seo-content/articles/:id/optimize",
        "/api/v1/addons/seo-content/articles/:id/publish",
        "/api/v1/addons/seo-content/rankings",
        "/api/v1/addons/seo-content/widget"
      ],
      "skills": [
        {
          "name": "seo-keyword-research",
          "displayName": "SEO: Keyword Research",
          "description": "Research a query (via the websearch addon) into scored keyword suggestions, and save target keywords into the content pipeline.",
          "permissions": [
            "addons/seo-content/keywords",
            "addons/seo-content/status"
          ],
          "endpoint": "/api/v1/addons/seo-content/keywords",
          "scripts": []
        },
        {
          "name": "seo-article-pipeline",
          "displayName": "SEO: Draft & Optimize Articles",
          "description": "Generate an AI article draft (via the MC gateway), edit it, and run pure on-page analysis (title/meta/density/links). Drafting is never a direct LLM call.",
          "permissions": [
            "addons/seo-content/articles"
          ],
          "endpoint": "/api/v1/addons/seo-content/articles",
          "scripts": []
        },
        {
          "name": "seo-publish",
          "displayName": "SEO: Publish to WordPress",
          "description": "Publish a finished draft to WordPress via the wordpress addon's MC-API. Fails soft if WordPress is not installed; never talks to WordPress directly.",
          "permissions": [
            "addons/seo-content/articles"
          ],
          "endpoint": "/api/v1/addons/seo-content/articles/:id/publish",
          "scripts": []
        },
        {
          "name": "seo-rank-tracking",
          "displayName": "SEO: Rank Tracking",
          "description": "Read live SERP positions for tracked keywords via the owner-keyed SEO-data SaaS. Missing key returns a clear 400; no provider call without a key.",
          "permissions": [
            "addons/seo-content/rankings",
            "addons/seo-content/status"
          ],
          "endpoint": "/api/v1/addons/seo-content/rankings",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>SEO Content Engine Addon</h1>\n<p>Run an <strong>SEO content pipeline</strong> from inside Aiopta: research keywords, generate an AI article<br /><strong>draft</strong>, run <strong>on-page</strong> optimization, <strong>publish</strong> to WordPress, and track <strong>rankings</strong>. Usable by<br />agents (via the addon API) and by humans (via the management page). This is the second money tool in<br />the series (after dropshipping, ADR-013), covering REQUIREMENTS-AUDIT row 26 &quot;SEO/Content&quot;.</p>\n<pre><code>keyword research → AI draft → on-page optimize → publish → rank-track\n   (websearch)      (gateway)   (pure analyzer)   (wordpress)  (owner SaaS)</code></pre>\n<h2>Architecture (ADR-014)</h2>\n<p>The engine is deliberately a <strong>thin orchestrator</strong> over capabilities Aiopta already owns — it<br />adds no new direct integration except the owner-keyed rank tracker:</p>\n<p>- <strong>AI generation goes through <code>@/lib/gateway</code> — never a direct LLM/provider call.</strong><br />  <code>src/lib/article-generator.ts</code> builds the prompt and calls <code>chatCompletionWithFallbacks(...)</code>; the<br />  gateway decides provider/backend routing. There is <strong>no provider <code>fetch</code></strong> in this addon (a unit test<br />  stubs global <code>fetch</code> and asserts it is never called during generation). The model returns a fenced<br />  ``<code>json block which we strip + parse into </code>{ title, slug, markdown, metaDescription }`.<br />- <strong>Research builds on the consolidated research suite (websearch addon) via the Aiopta API.</strong><br />  <code>src/lib/keyword-research.ts</code> calls <code>POST /api/v1/addons/websearch/search</code> through the Aiopta API,<br />  <strong>forwarding the caller&#39;s auth</strong> (RBAC preserved). It never re-implements search and never imports<br />  the websearch addon directly. The raw SERP hits feed a <strong>pure</strong> scoring helper<br />  (difficulty/opportunity) that is fully unit-tested.<br />- <strong>Publishing goes through the wordpress addon&#39;s Aiopta API — never WordPress directly.</strong><br />  <code>src/lib/wordpress-bridge.ts</code> calls <code>POST /api/v1/addons/wordpress/posts</code> (or <code>/pages</code>), forwarding<br />  auth, and <strong>fails soft</strong> with a clear error if the wordpress addon is absent / not configured.<br />- <strong>The rank-tracker is an external SEO-data SaaS reached with the owner&#39;s key — NO container.</strong><br />  <code>src/lib/rank-tracker.ts</code> reads <code>SEO_API_KEY</code> (+ <code>SEO_API_PROVIDER</code>) from<br />  <code>getAddonConfiguration(&quot;seo-content&quot;)</code> — the same owner-key pattern as the LLM providers and the<br />  dropshipping suppliers. Provider base URLs (DataForSEO / SerpApi) are <strong>compile-time constants</strong>,<br />  never a user-entered host; the key is sent as a Bearer token and <strong>never logged</strong>. A **missing key<br />  returns a clean 400 before any fetch** (no unauthenticated call ever goes out).<br />- <strong>The pipeline owns a table.</strong> Unlike the dropshipping margins (a single config blob), the pipeline<br />  has real state, so the addon owns <code>seo_content_items</code>, created in the extension <code>install()</code> and<br />  dropped in <code>uninstall()</code> via Aiopta core&#39;s <code>getDb()</code>. On-page analysis (<code>src/lib/on-page.ts</code>) is <strong>pure</strong><br />  (no I/O) and unit-tested.<br />- <strong>Per-user isolation (#146 / ADR-054).</strong> <code>seo_content_items</code> carries <code>user_id NOT NULL</code>; all CRUD<br />  goes through <code>scopedStore(auth.id)</code>, the server stamps the owner from <code>auth.id</code>, and a foreign id<br />  returns 404. A user only ever sees/edits/publishes/ranks their own articles, and on-page<br />  optimization draws internal-link candidates only from the caller&#39;s own pipeline (never another<br />  user&#39;s slugs). DDL runs via <code>db.prepare(sql).run()</code> (never <code>db.exec</code>); a legacy table is backfilled<br />  to the first admin (logged) or its un-attributable rows dropped.</p>\n<h2>Configuration (Aiopta addon-config — no secret committed)</h2>\n<p>| Key | Required | Purpose |<br />|-----|----------|---------|<br />| <code>SEO_API_KEY</code> | for rank tracking | SEO-data SaaS API token (Bearer). Stored <code>is_secret=1</code>. |<br />| <code>SEO_API_PROVIDER</code> | optional | <code>dataforseo</code> (default) or <code>serpapi</code>. |<br />| <code>SEO_API_LOCATION</code> | optional | Default location/country code passed to the provider. |</p>\n<p>Set them with <code>setAddonConfiguration(&quot;seo-content&quot;, { SEO_API_KEY: &quot;…&quot; })</code>. Keyword research and AI<br />drafting work <strong>without</strong> this key (they use the websearch addon + the gateway). Only <strong>rank tracking</strong><br />needs it; until it is set, <code>/status</code> reports <code>rankTracker.configured: false</code> and <code>/rankings</code> returns a<br />clear <strong>400</strong>.</p>\n<h2>API</h2>\n<p>All routes are namespaced under <code>/api/v1/addons/seo-content/</code> and require auth.</p>\n<pre><code>GET  /status                              → rank-tracker config + AI-gateway reachability + pipeline summary\nGET  /keywords?q=…                        → research a query (via websearch) → scored suggestions\nGET  /keywords                            → saved target keywords (stage = keyword)\nPOST /keywords    { keyword }             → save a target keyword into the pipeline\nGET  /articles?stage=                     → list the pipeline (optionally by stage)\nPOST /articles    { keyword, itemId?, … } → generate an AI DRAFT (via the gateway), persisted\nGET  /articles/{id}                       → one pipeline item\nPUT  /articles/{id} { title?, markdown?, metaDescription?, slug?, keyword? } → edit the draft\nDELETE /articles/{id}                     → remove a pipeline item\nPOST /articles/{id}/optimize              → pure on-page analysis → score + recommendations (stage→optimized)\nPOST /articles/{id}/publish { type?, status? } → publish to WordPress via the wordpress addon (stage→published)\nGET  /rankings?keywords=a,b&amp;domain=&amp;location= → live SERP positions (owner SEO key)\nGET  /widget                              → flat key-values for the dashboard widget</code></pre>\n<p>Responses use the <code>{ &quot;success&quot;: true, &quot;data&quot;: … }</code> envelope. On failure read <code>error</code> + the HTTP status<br />(a <code>400</code> on <code>/rankings</code> means <code>SEO_API_KEY</code> is not set; a publish/research failure forwards the<br />dependent addon&#39;s status).</p>\n<h3>On-page scoring (pure)</h3>\n<p><code>src/lib/on-page.ts</code> is pure + unit-tested. It scores a draft 0–100 from title/meta length, keyword<br />presence + density (healthy window 0.5–2.5%), word count, heading usage, and suggests internal links to<br />sibling pipeline items that share a meaningful term. No AI is involved — the model <em>writes</em> the draft;<br />this module <em>measures</em> it deterministically.</p>\n<h2>Layout</h2>\n<pre><code>addons/seo-content/\n├── addon.json                  # Manifest (page + widget + endpoints + skills; NO container)\n├── README.md\n├── SKILL.md                    # Agent skill\n├── src/\n│   ├── manifest.ts             # Extension (dual-registration; string-literal handlers; install/uninstall table)\n│   ├── page.tsx                # Pipeline UI (Pipeline / Keyword Research / Rankings)\n│   ├── widget.tsx              # Dashboard widget (pipeline counts + rank-tracker availability, declarative)\n│   ├── lib/\n│   │   ├── errors.ts           # SeoContentError (HTTP-status-carrying)\n│   │   ├── keyword-research.ts # websearch via Aiopta-API + pure scoring (difficulty/opportunity)\n│   │   ├── article-generator.ts# prompt → @/lib/gateway → draft {title,slug,markdown,meta}; fence-strip\n│   │   ├── on-page.ts          # PURE on-page analyzer (score + recommendations + internal links)\n│   │   ├── rank-tracker.ts     # SEO-data SaaS client (SEO_API_KEY from Aiopta-config; missing key → 400)\n│   │   ├── wordpress-bridge.ts # publish via the wordpress addon&#39;s Aiopta-API (fail-soft)\n│   │   ├── content-store.ts    # pipeline persistence (seo_content_items table via getDb)\n│   │   └── status.ts           # /status payload (providers + gateway reachability + summary)\n│   └── app/api/v1/addons/seo-content/{status,keywords,articles,articles/[id],articles/[id]/optimize,articles/[id]/publish,rankings,widget}/route.ts\n└── tests/unit/                 # manifest validity + pure on-page + pure scoring + gateway-routing + rank-tracker 400</code></pre>\n<h2>Owner-parked items (non-blocking)</h2>\n<p>The addon code + routes work today: research uses the websearch addon, drafting uses the gateway,<br />publishing uses the wordpress addon. Only <strong>rank tracking</strong> needs an owner SEO key. Tracked in<br /><code>docs/OPEN-DECISIONS.md</code>:</p>\n<blockquote><p><strong>TODO(owner): <code>seo-data-provider-keys</code></strong> — create a DataForSEO or SerpApi key and store it in Aiopta</p><p>addon-config (<code>SEO_API_KEY</code>, optional <code>SEO_API_PROVIDER</code>). No secret is committed. Until set,</p><p><code>/status</code> reports rank tracking not-configured and <code>/rankings</code> returns a clear 400.</p></blockquote>\n<h2>License</h2>\n<p>MIT</p>",
          "byteSize": 8332
        }
      }
    },
    {
      "id": "shopify",
      "uuid": "093be941-2064-484f-aba4-51b9e232ea08",
      "name": "Shopify",
      "version": "2.0.1",
      "description": "DEPRECATED — zusammengeführt in das kanonische Commerce-Addon `commerce` als Shopify-Connector (ADR-008 Addon-Konsolidierung, Issue #588 / Epic #492). Der Shopify-Channel ist als Feature-Modul unter `commerce/src/connectors/shopify/` migriert: BYOK-Credentials in der gemeinsamen `commerce_connector_config`-Tabelle, Sync-Client auf dem kanonischen `CommerceChannel`-Vertrag, Alt-ID-Redirect `/addons/shopify` → `/addons/commerce?connector=shopify`. shopify bleibt bis zum Sunset (2026-12-31) voll funktionsfähig und datenerhaltend — kein Endpoint entfällt, keine Zeile wird kopiert/gelöscht. Ursprüngliche Fähigkeit: Commerce channel: sync products, stock & orders from your Shopify shop via the Admin API (external SaaS). Each user brings their own access token (BYOK) stored per-user through the scoped data-access layer (ADR-054); no secret is hard-coded or logged and there is no container. Sync is always an explicit call.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "commerce-shopify",
        "shopify-products",
        "shopify-stock",
        "shopify-orders",
        "shopify-sync"
      ],
      "tags": [],
      "download": "/artifacts/shopify-2.0.1.zip",
      "sha256": "7e1ceb36c01bda13d42e982ffc1ac9be6f2fdbdefe936c3328e4d49fe4147f38",
      "size": 102441,
      "page": {
        "id": "shopify-page",
        "path": "/addons/shopify",
        "name": "Shopify",
        "icon": "🛍️",
        "section": "Money",
        "component": "@addons/shopify/src/page"
      },
      "deprecated": true,
      "replacedBy": "commerce",
      "deprecation": {
        "deprecated": true,
        "replacedBy": "commerce",
        "sunset": "2026-12-31",
        "successorPath": "/addons/commerce",
        "successorLabel": "Commerce (Nachfolger)"
      },
      "endpoints": [
        "/api/v1/addons/shopify/status",
        "/api/v1/addons/shopify/config",
        "/api/v1/addons/shopify/products",
        "/api/v1/addons/shopify/stock",
        "/api/v1/addons/shopify/orders",
        "/api/v1/addons/shopify/sync"
      ],
      "skills": [
        {
          "name": "shopify-sync",
          "displayName": "Shopify: Sync products / stock / orders",
          "description": "Trigger an explicit sync of the caller's Shopify shop (products, stock and orders) via the Admin API using the user's BYOK access token.",
          "permissions": [
            "addons/shopify/sync",
            "addons/shopify/products",
            "addons/shopify/orders"
          ],
          "endpoint": "/api/v1/addons/shopify/sync",
          "scripts": []
        },
        {
          "name": "shopify-products",
          "displayName": "Shopify: Products & Stock",
          "description": "List the caller's cached Shopify products and stock levels, or write back a stock level to Shopify.",
          "permissions": [
            "addons/shopify/products",
            "addons/shopify/stock"
          ],
          "endpoint": "/api/v1/addons/shopify/products",
          "scripts": []
        },
        {
          "name": "shopify-orders",
          "displayName": "Shopify: Orders",
          "description": "List the caller's cached Shopify orders.",
          "permissions": [
            "addons/shopify/orders"
          ],
          "endpoint": "/api/v1/addons/shopify/orders",
          "scripts": []
        }
      ],
      "mcpServers": [
        {
          "id": "shopify-tools",
          "displayName": "Shopify tools",
          "description": "Read-only Shopify commerce-channel companion tools for agents: build the Admin API base URL from a shop domain, normalise product/variant payloads into the channel's CommerceProduct shape, roll up variant inventory, normalise order payloads, and summarise a sync result - faithful ports of the addon's src/lib modules (issue #442, epic #399). Mutates nothing; live per-user Shopify access is resolved core-side via the declared config/credential slots.",
          "transport": "stdio",
          "runtime": "node",
          "entry": "shopify-tools/server.mjs",
          "permissions": [
            "addons/shopify/read"
          ],
          "toolPolicy": {
            "mode": "read-only",
            "allowDestructive": false
          },
          "configRefs": [
            "shopDomain",
            "apiVersion",
            "locationId"
          ],
          "credentialRefs": [
            "accessToken"
          ],
          "archiveEntry": "shopify/mcp/servers/shopify-tools/server.mjs",
          "sha256": "cce740612ed7dd55be79f90bacf452f6170849e79c4fcb860ece95bef9676c18",
          "size": 20421,
          "descriptorVersion": 1
        }
      ],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": null
    },
    {
      "id": "skills",
      "uuid": "24b7741d-1b5c-4885-8fbc-9a2a90f0f4a4",
      "name": "Skills",
      "version": "1.0.12",
      "description": "Skill catalog, local skill management, ClawHub integration, and dynamic skill generation helpers.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "skills",
        "clawhub",
        "skill-repository",
        "dynamic-skills"
      ],
      "tags": [],
      "download": "/artifacts/skills-1.0.12.zip",
      "sha256": "6bf2693ea4253b994b5403468c59329ef1b50e13e73787abe6bafc0370539aca",
      "size": 277438,
      "page": {
        "id": "skills",
        "path": "/addons/skills",
        "name": "Skills",
        "icon": "🧩",
        "section": "Agents",
        "component": "@addons/skills/src/components/skills/SkillsPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/skills",
        "/api/v1/addons/skills/clawhub",
        "/api/v1/addons/skills/repository"
      ],
      "skills": [
        {
          "name": "skills-list",
          "displayName": "Skills: lokal auflisten",
          "description": "Listet die lokal installierten Skills aus dem Skills-Catalog. Reine Lese-Operation, kein Netzwerk-Call.",
          "permissions": [
            "addons/skills/list"
          ],
          "endpoint": "/api/v1/addons/skills",
          "scripts": [
            {
              "id": "skills-list-local",
              "name": "skills-list-local",
              "path": "skills-list/list.sh",
              "sha256": "fe3d0b5e133713889db65772d238994030924ce556ed1de662fd11ad90d8287e",
              "interpreter": "bash",
              "permissions": [
                "addons/skills/list"
              ],
              "timeoutMs": 5000,
              "size": 864
            }
          ]
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Skills</h1>\n<p>Skill catalog, local skill management, ClawHub integration, and dynamic skill generation helpers.</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>skills</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> skills, clawhub, skill-repository, dynamic-skills</li><li><strong>UI-Seite:</strong> <code>/skills</code></li></ul>\n<h2>Lokale vs. Remote-Skills</h2>\n<p>Das Addon hat <strong>zwei voneinander unabhängige Pfade</strong>:</p>\n<h3>Lokale Skills (immer verfügbar, ohne Secrets) — <strong>per User isoliert</strong></h3>\n<p>Lokale Skills liegen im Dateisystem. Seit der per-User-Isolation (#145 / INBOX-59 / ADR-054) liegen<br />die <strong>eigenen erstellten/installierten</strong> Skills eines Users unter dessen <strong>per-User-FS-Root</strong><br />(<code>resolveUserPath(user, &quot;skills&quot;, &quot;local&quot;, …)</code>, containment-checked) statt im früheren<br />prozessweiten <code>…/skills/local</code>. Diese funktionieren <strong>vollständig ohne externen Dienst</strong>:<br />Durchsuchen, <code>SKILL.md</code> ansehen/bearbeiten, neuen Skill anlegen, in einen Agent-Workspace deployen<br />(<code>installSkillToWorkspace</code>), aktivieren/deaktivieren — alles auf <code>auth.id</code> gescoped, sodass ein User<br />nie fremde Local-Skills sieht/ändert/löscht. Der lokale Browse-Pfad (<code>GET /api/v1/skills</code>) macht<br /><strong>keinen</strong> Netzwerk-Aufruf — eine fehlende/falsch konfigurierte Remote-Registry blockiert die lokalen<br />Skills nie.</p>\n<p>Der <strong>geteilte Katalog</strong> (builtin-/Projekt-Skill-Verzeichnisse + die remote ClawHub-Registry) bleibt<br /><strong>instanzweit, read-only</strong>: er wird jedem User gelistet, aber nie per-User geschrieben, und ein<br />Katalog-Skill ist nicht als eigener Local-Skill löschbar. Nur die EIGENEN Local-Instanzen sind<br />gescoped.</p>\n<h3>Remote-Registry (ClawHub — optional, Owner-Infra)</h3>\n<p>Browse/Install/Sync <strong>entfernter</strong> Skills läuft entweder über das <code>clawhub</code>-CLI<br />(falls installiert) <strong>oder</strong> über einen HTTP-<code>skill-repo</code>-Dienst. Die Basis-URL<br />dieses Dienstes kommt aus der <strong>Aiopta-Addon-Konfiguration</strong> (<code>SKILL_REPO_URL</code>) bzw. dem<br />Env-Fallback — <strong>kein</strong> Host ist fest verdrahtet. Ist nichts konfiguriert, ist der<br />Remote-Pfad in einem klaren <em>not-configured</em>-Zustand (keine stillen Fehler): die UI<br />zeigt einen informativen Leerzustand mit Konfigurationshinweis statt einer leeren<br />Liste oder eines Dauer-Spinners.</p>\n<blockquote><p>Solange weder das CLI installiert noch eine Registry konfiguriert ist, sind die</p><p>Remote-Funktionen erwartungsgemäß deaktiviert. Das ist Owner-Infrastruktur und in</p><p><code>docs/OPEN-DECISIONS.md</code> (<code>clawhub-skill-registry</code>) geparkt. **Lokale Skills sind</p><p>davon unberührt.**</p></blockquote>\n<h2>Einrichtung (Remote-Registry)</h2>\n<p>Lokale Skills brauchen <strong>keine</strong> Konfiguration. Um die Remote-Registry zu aktivieren:</p>\n<p>1. Entweder das <code>clawhub</code>-CLI im Aiopta-Container installieren (<code>which clawhub</code> muss<br />   erfolgreich sein), <strong>oder</strong><br />2. einen <code>skill-repo</code>-Dienst betreiben und dessen Basis-URL setzen — in der<br />   Aiopta-Addon-Konfiguration des <code>skills</code>-Addons unter dem Schlüssel<br />   <strong><code>SKILL_REPO_URL</code></strong> (z. B. <code>https://skill-repo.example.com</code>), oder ersatzweise<br />   über die Umgebungsvariable <code>SKILL_REPO_URL</code>. Die Addon-Konfiguration hat Vorrang<br />   vor der Env-Variable.</p>\n<p>Erwartete HTTP-API des <code>skill-repo</code>-Dienstes (siehe <code>SkillRepositoryClient</code> in<br /><code>src/lib/skills-repo.ts</code>): <code>GET /health</code>, <code>GET /api/stats</code>, <code>GET /api/skills</code>,<br /><code>GET /api/skills/:name</code>, <code>…/versions</code>, <code>…/dependencies</code>, <code>…/dependents</code>,<br /><code>GET /api/search</code>, <code>GET /api/categories</code>, <code>POST /api/sync</code>,<br /><code>POST /api/skills/:name/{install,uninstall,update}</code>. Ein leichtgewichtiger<br />Mock-Server für die Entwicklung liegt in <code>src/lib/mock-skill-repo.ts</code>.</p>\n<h2>Status / Diagnose</h2>\n<p><code>GET /api/v1/skills/clawhub?action=status</code> (oder <code>…/repository?action=status</code>)<br />liefert den echten Zustand:</p>\n<pre><code class=\"lang-json\">{ &quot;cliAvailable&quot;: false, &quot;repoConfigured&quot;: false, &quot;repoReachable&quot;: false, &quot;repoUrl&quot;: &quot;&quot;, &quot;remoteAvailable&quot;: false }</code></pre>\n<p>So weiß die UI, ob die Remote-Registry <strong>nicht konfiguriert</strong>, **konfiguriert aber<br />nicht erreichbar**, oder voll funktionsfähig ist.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad | Zweck |<br />|---|---|---|<br />| GET, POST, PUT, DELETE | <code>/api/v1/addons/skills</code> | Lokale Skills (Filesystem) — browse, <code>SKILL.md</code>, create, deploy, toggle, delete |<br />| GET, POST | <code>/api/v1/addons/skills/clawhub</code> | ClawHub (CLI <strong>oder</strong> skill-repo); <code>?action=status</code> |<br />| GET, POST | <code>/api/v1/addons/skills/repository</code> | Remote-Registry-Browse/Install/Sync; <code>?action=status</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<p>- <strong>Remote-Registry</strong> (Browse/Install/Sync entfernter Skills) erfordert entweder das<br />  <code>clawhub</code>-CLI oder einen konfigurierten <code>skill-repo</code>-Dienst (<code>SKILL_REPO_URL</code>).<br />  Ohne beides ist der Remote-Pfad deaktiviert und zeigt einen klaren Hinweis —<br />  lokale Skills funktionieren unabhängig davon. Geparkt in <code>docs/OPEN-DECISIONS.md</code><br />  (<code>clawhub-skill-registry</code>).</p>",
          "byteSize": 4613
        }
      }
    },
    {
      "id": "smart-home",
      "uuid": "0dcae4ae-196e-4c95-aec8-512901ec4129",
      "name": "Smart Home (Home Assistant Automation & Scene Packs)",
      "version": "1.0.5",
      "description": "Per-User Smart-Home-Packs: kuratierte Home Assistant Automations- und Szenen-Vorlagen (BYOK HA URL + Token pro Nutzer), Pack-Browser mit Apply-Flow, Live-Gerätestatus (device overview) und per-User Szenen-State. Lokale SQLite ist die Quelle der Wahrheit für angewandte Packs + Szenen-State (ADR-054).",
      "author": "Aiopta",
      "icon": "🏡",
      "capabilities": [
        "smart-home",
        "automation-packs",
        "scene-packs",
        "pack-apply",
        "device-overview",
        "ha-connection-config"
      ],
      "tags": [],
      "download": "/artifacts/smart-home-1.0.5.zip",
      "sha256": "f0b191e3c316887135039e3eea33a103320ed448637fb87ce512df1e8275aa58",
      "size": 120207,
      "page": null,
      "deprecated": true,
      "replacedBy": "homeassistant",
      "deprecation": {
        "deprecated": true,
        "replacedBy": "homeassistant",
        "sunset": "2026-12-31",
        "successorPath": "/addons/homeassistant",
        "successorLabel": "Home Assistant (Nachfolger)"
      },
      "endpoints": [
        "/api/v1/addons/smart-home/packs",
        "/api/v1/addons/smart-home/packs/:id",
        "/api/v1/addons/smart-home/scenes",
        "/api/v1/addons/smart-home/devices",
        "/api/v1/addons/smart-home/config",
        "/api/v1/addons/smart-home/config/:id",
        "/api/v1/addons/smart-home/config/test",
        "/api/v1/addons/smart-home/apply"
      ],
      "skills": [
        {
          "name": "smart-home-read",
          "displayName": "Smart Home: Packs & Gerätestatus lesen",
          "description": "Listet die kuratierten Home Assistant Automations- und Szenen-Vorlagen, den Live-Gerätestatus (device overview) und die gespeicherte HA-Verbindung des Nutzers. Reiner Lesezugriff — wendet nichts an.",
          "permissions": [
            "addons/smart-home/packs",
            "addons/smart-home/scenes",
            "addons/smart-home/devices",
            "addons/smart-home/config"
          ],
          "endpoint": "/api/v1/addons/smart-home/packs",
          "scripts": []
        },
        {
          "name": "smart-home-apply",
          "displayName": "Smart Home: Packs anwenden & Szenen steuern",
          "description": "Wendet eine kuratierte Automations- oder Szenen-Vorlage auf die Home Assistant Instanz des Nutzers an (BYOK URL+Token) und steuert den per-User Szenen-State.",
          "permissions": [
            "addons/smart-home/packs",
            "addons/smart-home/scenes",
            "addons/smart-home/apply"
          ],
          "endpoint": "/api/v1/addons/smart-home/apply",
          "scripts": []
        },
        {
          "name": "smart-home-config",
          "displayName": "Smart Home: HA-Verbindung verwalten",
          "description": "Speichert, testet oder entfernt die per-User Home Assistant Verbindung (BYOK URL + Long-Lived-Token). Das Token wird serverseitig gespeichert und nie an Clients zurückgegeben.",
          "permissions": [
            "addons/smart-home/config"
          ],
          "endpoint": "/api/v1/addons/smart-home/config",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>🏡 Smart Home (Home Assistant Automation &amp; Scene Packs)</h1>\n<p>Per-User Smart-Home-Packs: kuratierte Home Assistant Automations- und Szenen-Vorlagen<br />(BYOK HA URL + Long-Lived-Token pro Nutzer), ein Pack-Browser mit Apply-Flow, ein<br />Live-Gerätestatus (device overview) sowie ein per-User Szenen-State. Lokale SQLite ist<br />die Quelle der Wahrheit für angewandte Packs + Szenen-State (ADR-054).</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>smart-home</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> smart-home, automation-packs, scene-packs, pack-apply, device-overview, ha-connection-config</li><li><strong>UI-Seite:</strong> <code>/addons/smart-home</code></li></ul>\n<h2>Konzept</h2>\n<p>Jeder Nutzer bringt sein eigenes Home Assistant mit (BYOK): URL + Long-Lived-Token<br />werden per-User gespeichert (das Token verlässt niemals den Server — Clients sehen nur<br /><code>hasToken: boolean</code>). Aus dem kuratierten Katalog wählt der Nutzer eine Automation- oder<br />Szenen-Vorlage, füllt die Platzhalter (z. B. <code>motion_entity</code>, <code>light_entity</code>) und wendet<br />sie an. Der reine <code>pack-applier</code> baut daraus das HA-Config-Payload, der Server POSTet es<br />an <code>/api/config/&lt;domain&gt;/config/&lt;id&gt;</code> der Nutzer-Instanz und lädt die Domain neu.<br />Angewandte Packs + Szenen-State werden per-User in SQLite gespeichert.</p>\n<p>Der <code>device-overview</code> ist ein Best-Effort-Lesezugriff auf <code>GET /api/states</code> der<br />Nutzer-Instanz und degradiert graceful (<code>connected: false</code>) — ohne Verbindung oder bei<br />Netz-/Auth-Fehlern rendert die UI den &quot;Verbinde dein HA&quot;-Prompt statt zu crashen.</p>\n<h2>Setup</h2>\n<p>Keine Secrets im Addon selbst. Der Nutzer trägt seine eigenen HA-Credentials über den<br />&quot;HA Connection&quot;-Tab (oder <code>POST /api/v1/addons/smart-home/config</code>) ein. Ohne Verbindung<br />sind Katalog + Szenen nutzbar; das <code>apply</code> und der <code>device-overview</code> benötigen eine<br />aktive Verbindung.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET | <code>/api/v1/addons/smart-home/packs</code> |<br />| GET | <code>/api/v1/addons/smart-home/packs/:id</code> |<br />| GET, PATCH | <code>/api/v1/addons/smart-home/scenes</code> |<br />| GET | <code>/api/v1/addons/smart-home/devices</code> |<br />| GET, POST | <code>/api/v1/addons/smart-home/config</code> |<br />| DELETE | <code>/api/v1/addons/smart-home/config/:id</code> |<br />| POST | <code>/api/v1/addons/smart-home/config/test</code> |<br />| POST | <code>/api/v1/addons/smart-home/apply</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<p>- Das HA-Config-Payload wird bei jedem Apply neu aus der Vorlage + den gespeicherten<br />  Platzhalter-Werten generiert (nie roh persistiert) — Vorlagen-Updates wirken beim<br />  erneuten Anwenden.<br />- Der Reload der Domain nach dem Apply ist Best-Effort (<code>reloaded: false</code> möglich, wenn<br />  HA den <code>reload</code>-Service einschränkt); der Config-Eintrag wird dennoch geschrieben.<br />- Szenen-State ist eine reine UI/Memory-Funktion; das tatsächliche HA-Szenen-Activation<br />  passiert über das <code>apply</code>.</p>",
          "byteSize": 2705
        }
      }
    },
    {
      "id": "social-media",
      "uuid": "ed389eea-f473-42a6-8a49-0911455c2fc9",
      "name": "Social Media (Planung & Posting)",
      "version": "1.0.1",
      "description": "Multi-Plattform-Planung und -Posting (Mastodon voll implementiert; X/Instagram/Facebook/LinkedIn über die gleiche Connector-Schnittstelle, sobald die Owner-OAuth-Apps konfiguriert sind), KI-gestützte Captions über den MC-Gateway, Redaktionskalender und Basis-Analytics. Lokale SQLite ist die Quelle der Wahrheit; ohne verbundene Plattform meldet ein Post graceful ein 400 ('zuerst eine Plattform verbinden'), nie ein 500 und nie ein stilles No-op. Plattform-Tokens nur via MC-Config, maskiert, nie geloggt. KI-Vorschläge werden dem Nutzer angezeigt, nie automatisch gepostet. ADR-043.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "multi-platform-posting",
        "scheduling",
        "editorial-calendar",
        "ai-captions",
        "basic-analytics",
        "mastodon"
      ],
      "tags": [],
      "download": "/artifacts/social-media-1.0.1.zip",
      "sha256": "83c04a6990e12321cd7cc75da5c23165d0bdbc4d9cd8f6e4da2e651d44f32304",
      "size": 88426,
      "page": {
        "id": "social-media-page",
        "path": "/addons/social-media",
        "name": "Social Media",
        "icon": "📱",
        "section": "Money",
        "component": "@addons/social-media/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/social-media/status",
        "/api/v1/addons/social-media/accounts",
        "/api/v1/addons/social-media/accounts/:id",
        "/api/v1/addons/social-media/posts",
        "/api/v1/addons/social-media/posts/:id",
        "/api/v1/addons/social-media/posts/:id/publish",
        "/api/v1/addons/social-media/scheduled/run-due",
        "/api/v1/addons/social-media/ai/caption",
        "/api/v1/addons/social-media/widget"
      ],
      "skills": [
        {
          "name": "social-media-overview",
          "displayName": "Social Media: Status & Übersicht",
          "description": "Liest, welche Plattformen verbunden sind (nur Booleans, keine Tokens) und die Inhalts-Übersicht (Konten, Posts, geplant, veröffentlicht). Reiner Lesezugriff — postet nichts.",
          "permissions": [
            "addons/social-media/status"
          ],
          "endpoint": "/api/v1/addons/social-media/status",
          "scripts": []
        },
        {
          "name": "social-media-compose",
          "displayName": "Social Media: Beitrag erstellen & planen",
          "description": "Erstellt einen Beitrag (Inhalt + Zielplattformen, optional Medien-Referenz und Planzeitpunkt). Veröffentlichen ist eine separate, bestätigte Aktion; ohne verbundene Plattform liefert das Veröffentlichen ein klares 400 ('zuerst verbinden'), nie ein 500.",
          "permissions": [
            "addons/social-media/posts",
            "addons/social-media/status"
          ],
          "endpoint": "/api/v1/addons/social-media/posts",
          "scripts": []
        },
        {
          "name": "social-media-ai-caption",
          "displayName": "Social Media: KI-Caption",
          "description": "Erzeugt einen Caption-Vorschlag über den MC-Gateway (Thema/Plattform/Tonalität). Der Vorschlag wird dem Nutzer angezeigt, nie automatisch gepostet. Ohne erreichbares Modell graceful (klare Meldung, kein 500).",
          "permissions": [
            "addons/social-media/ai"
          ],
          "endpoint": "/api/v1/addons/social-media/ai/caption",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>social-media</h1>\n<p>Multi-Plattform-Planung &amp; -Posting für Aiopta (ADR-043).</p>\n<p>## Was es kann<br />- <strong>Verfassen</strong> eines Beitrags für mehrere Plattformen, <strong>sofort posten</strong> oder<br />  <strong>planen</strong> (<code>scheduled_at</code>).<br />- <strong>Redaktionskalender</strong> der geplanten Beiträge.<br />- <strong>KI-Caption</strong> über den Aiopta-Gateway (Thema → Vorschlag; wird nie automatisch gepostet).<br />- <strong>Basis-Analytics</strong> je Beitrag/Plattform (was die Plattform-API liefert).</p>\n<p>## Plattformen<br />- <strong>Mastodon — voll implementiert.</strong> Konfiguration in Aiopta-Config:<br />  <code>MASTODON_INSTANCE</code> (z. B. <code>https://mastodon.social</code>) + <code>MASTODON_ACCESS_TOKEN</code>.<br />- <strong>X / Instagram / Facebook / LinkedIn</strong> — an dieselbe Connector-Schnittstelle<br />  modelliert; aktiv, sobald die Owner-OAuth-Apps konfiguriert sind<br />  (siehe <code>docs/OPEN-DECISIONS.md</code> #75). Bis dahin meldet ein Post für diese<br />  Plattformen graceful „nicht verbunden&quot; (kein Fake-Erfolg).</p>\n<p>## Sicherheit<br />- Posten ist eine <strong>authentifizierte</strong> Aktion; ohne verbundene Plattform → klares<br />  <strong>400</strong> („zuerst verbinden&quot;), nie ein 500, <strong>nie ein stilles No-op</strong>.<br />- Tokens nur via Aiopta-Config, maskiert, nie geloggt.<br />- KI-Captions werden dem Nutzer zur Prüfung angezeigt, nicht automatisch versendet.<br />- <strong>Per-User-isoliert (#141 / ADR-054):</strong> Accounts, Beiträge und deren Plattform-<br />  Targets tragen <code>user_id</code> und werden ausschließlich über <code>scopedStore(auth.id)</code><br />  gelesen/geschrieben. Der Server stempelt den Owner aus der Session (ein vom<br />  Client geliefertes <code>user_id</code> wird ignoriert); eine fremde Beitrags-/Account-id<br />  liefert <strong>404</strong> (nicht die Zeile). <code>POST /scheduled/run-due</code> ist ein<br />  <strong>Per-User-Scheduler</strong> — jeder Aufruf veröffentlicht nur die EIGENEN fälligen<br />  Beiträge.</p>\n<p>## Endpoints<br /><code>/status</code>, <code>/accounts(+/:id)</code>, <code>/posts(+/:id, /:id/publish)</code>, <code>/scheduled/run-due</code>,<br /><code>/ai/caption</code>, <code>/widget</code> — alle unter <code>/api/v1/addons/social-media/</code>.</p>\n<p>## Planung<br /><code>POST /scheduled/run-due</code> veröffentlicht fällige geplante Beiträge — von einem<br />Cron/Scheduler (owner-konfiguriert) getriggert.</p>",
          "byteSize": 2002
        }
      }
    },
    {
      "id": "task-board",
      "uuid": "66755468-1c64-4061-a1c0-2e542d134e15",
      "name": "Projects & Tasks",
      "version": "2.2.0",
      "description": "Canonical Projects & Tasks addon (ADR-008): task board + kanban plus the project-management surface (projects, members, tags, templates, stats) absorbed under task-board/project-management/*. Old /api/v1/addons/project-management/* paths keep working via re-export shims.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "tasks",
        "kanban",
        "projects",
        "project-management"
      ],
      "tags": [],
      "download": "/artifacts/task-board-2.2.0.zip",
      "sha256": "d09ecf1d5fb0c5c07b2cea1a17a042a296f539e3a4a780e7e78ff1fa44127098",
      "size": 442516,
      "page": [
        {
          "id": "tasks",
          "path": "/addons/task-board",
          "name": "Tasks",
          "icon": "📋",
          "section": "Work",
          "component": "@addons/task-board/src/components/tasks/TasksPage"
        },
        {
          "id": "projects",
          "path": "/addons/task-board/projects",
          "name": "Projects",
          "icon": "📁",
          "section": "Work",
          "component": "@addons/task-board/src/components/project-management/projects/CoreProjectsPage"
        }
      ],
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/task-board",
        "/api/v1/addons/task-board/search",
        "/api/v1/addons/task-board/templates",
        "/api/v1/addons/task-board/{id}",
        "/api/v1/addons/task-board/{id}/attachments",
        "/api/v1/addons/task-board/{id}/comments",
        "/api/v1/addons/task-board/{id}/dependencies",
        "/api/v1/addons/task-board/{id}/subtasks",
        "/api/v1/addons/task-board/{id}/time",
        "/api/v1/addons/task-board/tasks",
        "/api/v1/addons/task-board/tasks/{id}",
        "/api/v1/addons/task-board/tasks/search",
        "/api/v1/addons/task-board/kanban/boards",
        "/api/v1/addons/task-board/kanban/{boardId}/analytics",
        "/api/v1/addons/task-board/kanban/{boardId}/columns",
        "/api/v1/addons/task-board/kanban/{boardId}/export",
        "/api/v1/addons/task-board/kanban/{boardId}/swimlanes",
        "/api/v1/addons/task-board/comments/destinations",
        "/api/v1/addons/task-board/project-management",
        "/api/v1/addons/task-board/project-management/categories",
        "/api/v1/addons/task-board/project-management/create",
        "/api/v1/addons/task-board/project-management/import",
        "/api/v1/addons/task-board/project-management/sync",
        "/api/v1/addons/task-board/project-management/tags",
        "/api/v1/addons/task-board/project-management/templates",
        "/api/v1/addons/task-board/project-management/{id}",
        "/api/v1/addons/task-board/project-management/{id}/activity",
        "/api/v1/addons/task-board/project-management/{id}/archive",
        "/api/v1/addons/task-board/project-management/{id}/export",
        "/api/v1/addons/task-board/project-management/{id}/members",
        "/api/v1/addons/task-board/project-management/{id}/settings",
        "/api/v1/addons/task-board/project-management/{id}/stats",
        "/api/v1/addons/task-board/project-management/{id}/tags",
        "/api/v1/addons/task-board/project-management/{id}/tasks",
        "/api/v1/addons/task-board/project-management/projects",
        "/api/v1/addons/task-board/project-management/projects/{id}"
      ],
      "skills": [],
      "mcpServers": [
        {
          "id": "task-board-tools",
          "displayName": "Projects & Tasks tools",
          "description": "Read-only kanban board projection for agents: group tasks into the canonical board columns, count work-in-progress and surface approval-needed items — the same board model the Projects & Tasks addon renders (issue #445, epic #399). Mutates nothing.",
          "transport": "stdio",
          "runtime": "node",
          "entry": "task-board-tools/server.mjs",
          "permissions": [
            "addons/task-board/read"
          ],
          "toolPolicy": {
            "mode": "read-only",
            "allowDestructive": false
          },
          "configRefs": [],
          "credentialRefs": [],
          "archiveEntry": "task-board/mcp/servers/task-board-tools/server.mjs",
          "sha256": "7b3fc5ba601d8739c96cdd9cdf9a1b0c64d597449ab7ab509790a2fc5b106b50",
          "size": 13124,
          "descriptorVersion": 1
        }
      ],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Task Board</h1>\n<p>Task board UI and task APIs as an installable addon.</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>task-board</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> tasks, kanban</li><li><strong>UI-Seite:</strong> <code>/tasks</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET, POST | <code>/api/v1/addons/task-board</code> |<br />| GET, PUT, DELETE | <code>/api/v1/addons/task-board/[id]</code> |<br />| GET, POST, DELETE | <code>/api/v1/addons/task-board/[id]/attachments</code> |<br />| GET, POST | <code>/api/v1/addons/task-board/[id]/comments</code> |<br />| GET, POST, DELETE | <code>/api/v1/addons/task-board/[id]/dependencies</code> |<br />| GET, POST | <code>/api/v1/addons/task-board/[id]/subtasks</code> |<br />| GET, POST, DELETE | <code>/api/v1/addons/task-board/[id]/time</code> |<br />| GET, POST | <code>/api/v1/addons/task-board/comments/destinations</code> |<br />| GET | <code>/api/v1/addons/task-board/kanban/[boardId]/analytics</code> |<br />| GET, POST, PUT, DELETE | <code>/api/v1/addons/task-board/kanban/[boardId]/columns</code> |<br />| GET | <code>/api/v1/addons/task-board/kanban/[boardId]/export</code> |<br />| GET, POST | <code>/api/v1/addons/task-board/kanban/[boardId]/swimlanes</code> |<br />| GET, POST | <code>/api/v1/addons/task-board/kanban/boards</code> |<br />| GET | <code>/api/v1/addons/task-board/search</code> |<br />| GET | <code>/api/v1/addons/task-board/status</code> |<br />| GET, POST | <code>/api/v1/addons/task-board/tasks</code> |<br />| GET, PUT, DELETE | <code>/api/v1/addons/task-board/tasks/[id]</code> |<br />| GET | <code>/api/v1/addons/task-board/tasks/search</code> |<br />| GET, POST | <code>/api/v1/addons/task-board/templates</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<ul><li>Keine bekannten Einschränkungen.</li></ul>\n<blockquote><p>README aus <code>addon.json</code> + vorhandenen Routen generiert (<code>scripts/generate-addon-readmes.js</code>). Bei manuellen Ergänzungen den Generator-Hinweis entfernen.</p></blockquote>",
          "byteSize": 1678
        }
      }
    },
    {
      "id": "trading",
      "uuid": "97c0b487-8e82-46ee-abb1-1a335c48ea5e",
      "name": "trading",
      "version": "1.53.2",
      "description": "Hyperliquid trading extension with real-time orders, positions, journal, workflow automation, self-contained market research tooling, DST-safe pre-market/weekly-review scheduler (issue #153), market-open/midday-scan safe-execute dispatcher (issue #154), and a deterministic, per-user lesson tracker (issue #116)",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "trading",
        "orders",
        "positions",
        "journal",
        "autotrading",
        "strategy-automation",
        "risk-management",
        "research",
        "scheduler",
        "planner",
        "executor",
        "workflow",
        "gov-scanner"
      ],
      "tags": [],
      "download": "/artifacts/trading-1.53.2.zip",
      "sha256": "6047b974eed36e9e57f34ed96672fcc31a88be84668d8985ffcb54443e192139",
      "size": 2335295,
      "page": {
        "path": "/addons/trading",
        "name": "Trading",
        "icon": "📊",
        "section": "Finance",
        "component": "@addons/trading/src/components/TradingPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/trading/account",
        "/api/v1/addons/trading/analyze",
        "/api/v1/addons/trading/autotrading",
        "/api/v1/addons/trading/buy",
        "/api/v1/addons/trading/charts",
        "/api/v1/addons/trading/close",
        "/api/v1/addons/trading/config",
        "/api/v1/addons/trading/gov-config",
        "/api/v1/addons/trading/gov-config/test-webhook",
        "/api/v1/addons/trading/gov-proposals",
        "/api/v1/addons/trading/gov-proposals/[id]/decide",
        "/api/v1/addons/trading/gov-scan",
        "/api/v1/addons/trading/gov-signals",
        "/api/v1/addons/trading/gov-signals/[id]",
        "/api/v1/addons/trading/gov-sources",
        "/api/v1/addons/trading/gov-issuers/refresh",
        "/api/v1/addons/trading/history",
        "/api/v1/addons/trading/hyperliquid",
        "/api/v1/addons/trading/journal",
        "/api/v1/addons/trading/lessons",
        "/api/v1/addons/trading/market/{category}",
        "/api/v1/addons/trading/orders",
        "/api/v1/addons/trading/positions",
        "/api/v1/addons/trading/report",
        "/api/v1/addons/trading/research/fetch",
        "/api/v1/addons/trading/research/search",
        "/api/v1/addons/trading/risk",
        "/api/v1/addons/trading/sell",
        "/api/v1/addons/trading/settings",
        "/api/v1/addons/trading/sl",
        "/api/v1/addons/trading/stats",
        "/api/v1/addons/trading/status",
        "/api/v1/addons/trading/stop-loss",
        "/api/v1/addons/trading/strategies",
        "/api/v1/addons/trading/strategies/ladder",
        "/api/v1/addons/trading/strategies/preview",
        "/api/v1/addons/trading/workflow",
        "/api/v1/addons/trading/workflow-config",
        "/api/v1/addons/trading/workflow/run"
      ],
      "skills": [
        {
          "name": "trading-status",
          "displayName": "Trading: Account & Positions",
          "description": "Read Hyperliquid account value, open positions and live status",
          "permissions": [
            "addons/trading/status",
            "addons/trading/positions",
            "addons/trading/account"
          ],
          "endpoint": "/api/v1/addons/trading/status",
          "scripts": [
            {
              "id": "trading-status-cli",
              "name": "trading-status-cli",
              "path": "trading-status/cli.sh",
              "sha256": "6da8c8486514f36922ec398642274a0c57528fbdd70b4f58716ebd91b977d145",
              "interpreter": "bash",
              "timeoutMs": 10000,
              "permissions": [
                "addons/trading/account",
                "addons/trading/positions",
                "addons/trading/status"
              ],
              "inputSchema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 20
                  },
                  "timeoutMs": {
                    "type": "integer",
                    "minimum": 100,
                    "maximum": 300000
                  }
                }
              },
              "outputSchema": {
                "type": "object",
                "required": [
                  "ok"
                ],
                "properties": {
                  "ok": {
                    "type": "boolean"
                  },
                  "status": {
                    "type": "integer"
                  },
                  "data": {},
                  "error": {
                    "type": "string"
                  },
                  "truncated": {
                    "type": "boolean"
                  }
                }
              },
              "size": 12083
            }
          ]
        },
        {
          "name": "trading-orders",
          "displayName": "Trading: Place & Manage Orders",
          "description": "Open/close positions and place limit orders on Hyperliquid perps",
          "permissions": [
            "addons/trading/buy",
            "addons/trading/sell",
            "addons/trading/close",
            "addons/trading/orders"
          ],
          "endpoint": "/api/v1/addons/trading/orders",
          "scripts": []
        },
        {
          "name": "trading-risk",
          "displayName": "Trading: Risk & Stop-Loss",
          "description": "Set stop-loss orders and read risk metrics for open positions",
          "permissions": [
            "addons/trading/stop-loss",
            "addons/trading/sl",
            "addons/trading/risk"
          ],
          "endpoint": "/api/v1/addons/trading/risk",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Trading Extension</h1>\n<p>Crypto trading integration for Aiopta, powered by Hyperliquid.</p>\n<h2>Features</h2>\n<ul><li><strong>Portfolio Status</strong> — Real-time account value, margin, positions across core and builder-deployed perp DEXs</li><li><strong>Market &amp; Limit Orders</strong> — Buy/sell with instant or price-targeted execution</li><li><strong>Risk Management</strong> — Auto stop-loss on buy, manual stop-loss/take-profit</li><li><strong>Position Tracking</strong> — Open positions with unrealized P&amp;L</li><li><strong>Trade History</strong> — Paginated history with performance stats (Sharpe, drawdown, win rate)</li><li><strong>Daily Snapshots</strong> — Automatic capital snapshots for trend analysis</li><li><strong>Journal &amp; Lessons</strong> — Trading journal with AI-generated lessons</li><li><strong>Auto-trading</strong> — Configurable automated trading strategies</li><li><strong>Mr Krabs Strategy Automation</strong> — Percentage trailing stops, DCA ladders, options wheel planning, and copy-trading policy</li><li><strong>Workflow Automation</strong> — Pre-market, market-open, midday, EOD and weekly workflow runs with stored reports</li><li><strong>Multi-channel Delivery</strong> — Aiopta chat, Discord, Slack, email, webhook, and in-app storage</li><li><strong>Research Delegation</strong> — Trading research is delegated to the <code>research-tools</code> addon instead of duplicating search logic</li><li><strong>KRABCLI Parity Matrix</strong> — Owner-verified, evidence-backed parity view of the eleven atomic KRABCLI commands and five workflow phase bundles (issue #104). Pin-enforced sub-module reference (<code>krabcli.pin.json</code> at commit <code>487b7895a65d53bfa698a17583fbf6d61342e325</code>); matrix loader at <code>src/lib/krabcli/parity.ts</code> and sub-process wrapper at <code>src/lib/krabcli/runner.ts</code>. See <code>docs/KRABCLI-PARITY.md</code> for the 17-row evidence document.</li></ul>\n<h2>Automated Strategies</h2>\n<p>Rule-based strategy engines (no LLM needed) under <code>src/lib/</code>. Each is a pure,<br />unit-tested planner; execution goes through the normal order path with the<br />strategy guard-rails (<code>dryRun</code>, daily-trade and exposure caps).</p>\n<p>| Strategy | Module | Execution on Hyperliquid | Notes |<br />|----------|--------|--------------------------|-------|<br />| <strong>Percentage trailing stop</strong> | <code>stop-manager.ts</code> + <code>strategy-config.ts</code> | ✅ full | Price-based floor that only ratchets up (longs) / down (shorts); places the SL via <code>engine.setStopLoss</code>. |<br />| <strong>Ladder buys / DCA</strong> | <code>ladder-executor.ts</code> | ✅ full | Staged limit buys at dip thresholds, double-execution-guarded in DB. |<br />| <strong>Wheel strategy (#236)</strong> | <code>wheel-strategy.ts</code> | ⚠️ advisory only | The cash-secured-put → assignment → covered-call <strong>cycle state machine</strong> and per-cycle premium accounting are implemented and tested. <strong>Limitation:</strong> Hyperliquid has no options, so there is no execution backend — <code>planWheelCycle</code> returns <code>advisoryOnly</code> plans (paper). Strike selection uses a delta-as-moneyness heuristic; wiring an options-capable provider (Tradier/Alpaca/IBKR) through a <code>WheelExecutionAdapter</code> + a live options chain would make it executable. |<br />| <strong>Copy trading / smart money (#237)</strong> | <code>copy-trading.ts</code> | ✅ executable, ⚠️ needs data source | The <strong>mirroring engine</strong> (conviction sizing, USD cap, confidence/lag filters, allow/deny lists, manual-approval gate) is implemented and tested; mirrored orders are normal perp orders. <strong>Limitation:</strong> the <em>data source</em> — which trades to copy (CapitolTrades/Quiver politician filings, whale wallets) — is external and not wired. Feed trades via the <code>ObservedTrade</code> shape; a future provider implements the <code>CopyTradeProvider</code> interface. |</p>\n<p>Strategy config lives in <code>strategy-config.ts</code> (<code>StrategyAutomationConfig</code>) and is<br />editable through the Trading settings UI.</p>\n<h2>API Endpoints</h2>\n<p>All endpoints require API key authentication (<code>Authorization: Bearer &lt;key&gt;</code>).<br />Addon runtime routes are exposed under <code>/api/v1/addons/trading/...</code>.</p>\n<p>| Method | Endpoint | Description |<br />|--------|----------|-------------|<br />| GET | <code>/api/v1/trading/status</code> | Full portfolio status with positions |<br />| GET | <code>/api/v1/trading/account</code> | Account balance, margin, capital summary |<br />| GET | <code>/api/v1/trading/positions</code> | Open positions only |<br />| GET | <code>/api/v1/trading/orders</code> | Open orders |<br />| POST | <code>/api/v1/trading/orders</code> | Place limit/stop/TP order |<br />| POST | <code>/api/v1/trading/buy</code> | Open long position |<br />| POST | <code>/api/v1/trading/sell</code> | Open short position |<br />| POST | <code>/api/v1/trading/close</code> | Close position by coin |<br />| POST | <code>/api/v1/trading/stop-loss</code> | Set stop-loss for position |<br />| GET | <code>/api/v1/trading/history</code> | Closed-trade history (canonical, KRABCLI <code>history</code>, max 30, per-user) |<br />| GET | <code>/api/v1/trading/stats</code> | Performance statistics (canonical, KRABCLI <code>stats</code>, per-user) |<br />| GET | <code>/api/v1/trading/report</code> | Daily report (template-versioned, idempotent, KRABCLI <code>report</code>) |<br />| GET | <code>/api/v1/trading/charts</code> | Chart data |<br />| GET | <code>/api/v1/trading/risk</code> | Risk metrics |<br />| GET | <code>/api/v1/trading/journal</code> | Trading journal entries |<br />| GET | <code>/api/v1/trading/lessons</code> | Learned lessons |<br />| GET | <code>/api/v1/trading/analyze</code> | AI trade analysis |<br />| GET | <code>/api/v1/trading/research/search</code> | Search web research sources |<br />| GET | <code>/api/v1/trading/research/fetch</code> | Extract readable source content |<br />| GET | <code>/api/v1/trading/autotrading</code> | Auto-trading config |<br />| GET | <code>/api/v1/addons/trading/strategies</code> | Mr Krabs strategy automation settings |<br />| PATCH | <code>/api/v1/addons/trading/strategies</code> | Update percentage trailing, DCA, wheel, copy-trading and guard-rail settings |<br />| POST | <code>/api/v1/addons/trading/strategies/preview</code> | Preview strategy effects for a supplied market snapshot |<br />| GET | <code>/api/v1/trading/workflow</code> | Recent workflow runs |<br />| POST | <code>/api/v1/trading/workflow/run</code> | Run a workflow phase manually |<br />| POST | <code>/api/v1/trading/hyperliquid</code> | Webhook for trade fills/cancels/liquidations |</p>\n<h2>Canonical history &amp; stats (issue #115 — KRABCLI parity)</h2>\n<p><code>GET /api/v1/addons/trading/history</code>, <code>GET /api/v1/addons/trading/stats</code><br />and <code>GET /api/v1/addons/trading/report</code> are the <strong>canonical</strong> surfaces<br />that mirror the KRABCLI commands <code>history</code>, <code>stats</code>, <code>report</code>. They<br />read from the per-user <code>trades</code> table (<code>scopedStore(userId)</code>, ADR-054)<br />so a user can never see another user&#39;s positions or PnL — the row-level<br />isolation is enforced in the SQL layer, not in JS.</p>\n<h3>History (<code>/history</code>)</h3>\n<p>- Closed trades only (<code>status = &#39;closed&#39;</code>). Open / cancelled rows never<br />  leak through.<br />- Sort key: <code>closed_at DESC, id DESC</code> (deterministic tie-break).<br />- Hard cap: <strong>30 rows</strong> (the KRABCLI parity contract). Bumping this<br />  constant is a contract change — update both this file and<br />  <code>addons/trading/src/lib/history.ts: CANONICAL_HISTORY_LIMIT</code>.<br />- Each row carries its <strong>own</strong> <code>fxRate</code>, <code>fxFetchedAt</code>, <code>fxStale</code><br />  triple so a downstream consumer can render `USD (1.10 EUR, fetched<br />  2 min ago)`-style labels without re-resolving the FX snapshot.</p>\n<h3>Stats formulas (<code>/stats</code>)</h3>\n<p>All formulas are pinned by the golden-fixture tests in<br /><code>tests/unit/performance-canonical.test.ts</code>. Sample size, all rates and<br />the Sharpe are computed from the <strong>per-user closed-trade ledger only</strong><br />(ADR-054). When <code>sampleSize = 0</code> the response returns neutral zeros<br />plus <code>fx</code>/<code>degraded</code> metadata.</p>\n<p>| Metric | Formula |<br />|---|---|<br />| <code>winRate</code> | <code>wins / sampleSize</code> (rounded to 4 dp) |<br />| <code>lossRate</code> | <code>losses / sampleSize</code> (rounded to 4 dp) |<br />| <code>sharpe</code> | <code>(mean(pnlUsd) / sampleStd(pnlUsd)) * sqrt(252)</code> (annualised; <code>sqrt(252)</code> is the documented trading-day convention for the KRABCLI parity matrix). Undefined for <code>sampleSize ≤ 1</code> → returns <code>0</code>. |<br />| <code>maxDrawdownUsd</code> | <code>max(peak_cumulative - cumPnL_i)</code> over the chronological pnl series (USD, rounded to cents) |<br />| <code>avgDurationHours</code> | <code>mean(closed_at - opened_at)</code> across rows where both timestamps parse |<br />| <code>medianDurationHours</code> | <code>median</code> of the same duration array (rounded to 4 dp) |<br />| <code>totalPnlUsd</code> / <code>totalPnlEur</code> | <code>sum(pnl_usd)</code> / <code>sum(pnl_eur)</code> (USD rounded to cents, EUR to cents) |<br />| <code>totalFeesUsd</code> | <code>sum(fees_usd)</code> (column added by issue #115 migration) |<br />| <code>bestTradeUsd</code> / <code>worstTradeUsd</code> / <code>avgPnlUsd</code> | <code>max</code> / <code>min</code> / <code>mean</code> of per-trade <code>pnl_usd</code> |</p>\n<p>Changing any of these is a contract change — update the README, the<br />golden-fixture test, and <code>performance.ts</code> together.</p>\n<h3>FX service (<code>fx.ts</code>)</h3>\n<p>- TTL: <strong>1 hour</strong> (<code>FX_TTL_MS = 3 600 000</code> ms). A cache entry older<br />  than the TTL is served as <code>stale: true</code>, never as fresh.<br />- Provider: Frankfurter API (<code>https://api.frankfurter.app/latest</code>),<br />  with a 5 s per-request timeout (<code>FX_REQUEST_TIMEOUT_MS</code>).<br />- <strong>No invented rates.</strong> When the provider fails AND no cache entry<br />  exists, the snapshot returns <code>rate: 1</code> with <code>source: &#39;provider-failed&#39;</code><br />  and <code>stale: true</code>. The route layer surfaces <code>degraded: true</code> so the<br />  UI tells the user &quot;we don&#39;t know the rate right now&quot; instead of<br />  silently picking a number.<br />- Per-trade FX pricing is the canonical source — each closed-trade<br />  row stores its own <code>eur_usd_rate</code> (USD → EUR) plus <code>fx_rate_at</code><br />  timestamp (ISO), written at close time. Legacy rows that pre-date<br />  the column fall back to the live snapshot and get tagged<br />  <code>fxStale: true</code>. The route payload exposes both the per-row FX and<br />  the batch-level FX snapshot so the consumer can choose which to<br />  surface.</p>\n<h3><code>report</code> (<code>/report</code>)</h3>\n<p>The <code>report</code> command is the <strong>MISSING-CONTRACT</strong> from the KRABCLI<br />parity matrix (issue #156, row <code>cmd-report</code>). The Aiopta route is:</p>\n<p>- Read-only, template-versioned (<code>templateVersion: &quot;1.0.0&quot;</code>), no order<br />  mutation.<br />- Idempotent per <code>(userId, date)</code> — the response carries an<br />  <code>idempotencyKey</code> (FNV-1a over <code>(userId, date, stats)</code>) so a cron<br />  retry on the same day produces the same key and the same body.<br />- Date defaults to today (UTC); <code>?date=YYYY-MM-DD</code> for back-fills.</p>\n<h2>Workflow Notes</h2>\n<ul><li>Broad scan covers crypto, equities, and macro research.</li><li>Live execution remains limited to connected backends, currently Hyperliquid.</li><li>Trading research inside this addon calls into the <code>research-tools</code> addon.</li><li>Delivery targets are configurable in the trading settings panel.</li></ul>\n<h2>Mr. Crabs Usage Examples</h2>\n<p>### Check Account Balance<br />```bash<br />curl -H &quot;Authorization: Bearer $API_KEY&quot; \\<br />  <a href=\"https://aiopta.example.com/api/v1/trading/account\" rel=\"noopener noreferrer\">https://aiopta.example.com/api/v1/trading/account</a><br />```</p>\n<p>### Open a Long Position (BTC, $100 margin, 20x leverage, 5% stop-loss)<br />```bash<br />curl -X POST -H &quot;Authorization: Bearer $API_KEY&quot; \\<br />  -H &quot;Content-Type: application/json&quot; \\<br />  -d &#39;{&quot;coin&quot;:&quot;BTC&quot;,&quot;size&quot;:100,&quot;leverage&quot;:20}&#39; \\<br />  <a href=\"https://aiopta.example.com/api/v1/trading/buy\" rel=\"noopener noreferrer\">https://aiopta.example.com/api/v1/trading/buy</a><br />```</p>\n<p>### Open a Short Position<br />```bash<br />curl -X POST -H &quot;Authorization: Bearer $API_KEY&quot; \\<br />  -H &quot;Content-Type: application/json&quot; \\<br />  -d &#39;{&quot;coin&quot;:&quot;ETH&quot;,&quot;size&quot;:200,&quot;leverage&quot;:10}&#39; \\<br />  <a href=\"https://aiopta.example.com/api/v1/trading/sell\" rel=\"noopener noreferrer\">https://aiopta.example.com/api/v1/trading/sell</a><br />```</p>\n<p>### Place a Limit Order<br />```bash<br />curl -X POST -H &quot;Authorization: Bearer $API_KEY&quot; \\<br />  -H &quot;Content-Type: application/json&quot; \\<br />  -d &#39;{&quot;coin&quot;:&quot;BTC&quot;,&quot;side&quot;:&quot;BUY&quot;,&quot;type&quot;:&quot;LIMIT&quot;,&quot;size&quot;:0.1,&quot;price&quot;:80000}&#39; \\<br />  <a href=\"https://aiopta.example.com/api/v1/trading/orders\" rel=\"noopener noreferrer\">https://aiopta.example.com/api/v1/trading/orders</a><br />```</p>\n<p>### Trade a Builder-Deployed Perp<br />```bash<br />curl -X POST -H &quot;Authorization: Bearer $API_KEY&quot; \\<br />  -H &quot;Content-Type: application/json&quot; \\<br />  -d &#39;{&quot;coin&quot;:&quot;xyz:NVDA&quot;,&quot;side&quot;:&quot;BUY&quot;,&quot;type&quot;:&quot;MARKET&quot;,&quot;size&quot;:1}&#39; \\<br />  <a href=\"https://aiopta.example.com/api/v1/trading/orders\" rel=\"noopener noreferrer\">https://aiopta.example.com/api/v1/trading/orders</a><br />```</p>\n<p>### Set Stop-Loss<br />```bash<br />curl -X POST -H &quot;Authorization: Bearer $API_KEY&quot; \\<br />  -H &quot;Content-Type: application/json&quot; \\<br />  -d &#39;{&quot;coin&quot;:&quot;BTC&quot;,&quot;price&quot;:79000}&#39; \\<br />  <a href=\"https://aiopta.example.com/api/v1/trading/stop-loss\" rel=\"noopener noreferrer\">https://aiopta.example.com/api/v1/trading/stop-loss</a><br />```</p>\n<p>### Close Position<br />```bash<br />curl -X POST -H &quot;Authorization: Bearer $API_KEY&quot; \\<br />  -H &quot;Content-Type: application/json&quot; \\<br />  -d &#39;{&quot;coin&quot;:&quot;BTC&quot;}&#39; \\<br />  <a href=\"https://aiopta.example.com/api/v1/trading/close\" rel=\"noopener noreferrer\">https://aiopta.example.com/api/v1/trading/close</a><br />```</p>\n<p>### Get Daily P&amp;L Summary<br />```bash<br />curl -H &quot;Authorization: Bearer $API_KEY&quot; \\<br />  <a href=\"https://aiopta.example.com/api/v1/trading/stats\" rel=\"noopener noreferrer\">https://aiopta.example.com/api/v1/trading/stats</a><br />```</p>\n<p>### Run A Pre-Market Workflow<br />```bash<br />curl -X POST -H &quot;Authorization: Bearer $API_KEY&quot; \\<br />  -H &quot;Content-Type: application/json&quot; \\<br />  -d &#39;{&quot;phase&quot;:&quot;pre_market&quot;}&#39; \\<br />  <a href=\"https://aiopta.example.com/api/v1/trading/workflow/run\" rel=\"noopener noreferrer\">https://aiopta.example.com/api/v1/trading/workflow/run</a><br />```</p>\n<h2>Configuration</h2>\n<p>Set these environment variables:</p>\n<pre><code class=\"lang-env\">HYPERLIQUID_ADDRESS=0x...\nHYPERLIQUID_PRIVATE_KEY=0x...</code></pre>\n<h2>Extension Management</h2>\n<p>Enable/disable via Admin API:<br />```bash<br /># Check status<br />curl -H &quot;Authorization: Bearer $API_KEY&quot; \\<br />  <a href=\"https://aiopta.example.com/api/v1/admin/extensions\" rel=\"noopener noreferrer\">https://aiopta.example.com/api/v1/admin/extensions</a></p>\n<p># Disable trading<br />curl -X POST -H &quot;Authorization: Bearer $API_KEY&quot; \\<br />  -H &quot;Content-Type: application/json&quot; \\<br />  -d &#39;{&quot;id&quot;:&quot;trading&quot;,&quot;enabled&quot;:false}&#39; \\<br />  <a href=\"https://aiopta.example.com/api/v1/admin/extensions\" rel=\"noopener noreferrer\">https://aiopta.example.com/api/v1/admin/extensions</a><br />```</p>\n<h2>Database Tables</h2>\n<p>Created on install:<br />- <code>trades</code> — All trade records (open/closed/cancelled)<br />- <code>orders</code> — Limit/stop/TP orders<br />- <code>lessons</code> — Trading lessons learned<br />- <code>daily_snapshots</code> — Capital snapshots per day</p>\n<h2>Gov-Scanner (addons#933)</h2>\n<p>Wertet öffentliche Pflichtmeldungen und Vergabebekanntmachungen aus. **Keine<br />Insider-Tipps** — ausschliesslich Quellen, deren Veröffentlichung gesetzlich<br />vorgeschrieben ist.</p>\n<h3>Die sechs Quellen</h3>\n<p>| Kennung | Art | liefert | Zugangsdaten |<br />|---|---|---|---|<br />| <code>usaspending</code> | Vergabe | US-Bundesvergaben | keine |<br />| <code>sec_form4</code> | Meldung | Insidergeschäfte (SEC Form 4) | <code>gov.sec_user_agent</code> |<br />| <code>stock_act</code> | Meldung | Offenlegungen des US-Kongresses | <code>gov.stock_act_user_agent</code> |<br />| <code>ted</code> | Vergabe | EU-Vergabebekanntmachungen | <code>gov.ted_user_agent</code> |<br />| <code>eu_transparency</code> | Meldung | Stimmrechtsmitteilungen (EU) | <code>gov.eu_transparency_user_agent</code> |<br />| <code>bundesanzeiger</code> | Meldung | Directors&#39; Dealings (Art. 19 MAR) | <code>gov.bundesanzeiger_api_key</code> |</p>\n<p>Alle sechs stehen nach <code>install()</code> in <code>gov_sources</code> — auch die ohne<br />Zugangsdaten. Eine leere Liste wäre von „sechs Quellen, alle unkonfiguriert&quot;<br />nicht zu unterscheiden, obwohl das eine ein Zustand und das andere eine<br />Aufgabenliste ist.</p>\n<p><strong>Vergabe- und Meldequellen unterscheiden sich in der Richtung.</strong> Eine Vergabe<br />ist ein Zufluss: sie kann <code>BUY</code> oder <code>NEUTRAL</code> begründen, nie <code>SELL</code>. Nur<br />Meldequellen nennen den Geschäftsvorfall und damit auch einen Verkauf.</p>\n<h3>Zustand je Quelle</h3>\n<p><code>gov_sources.last_status</code> trägt einen von vier Werten:</p>\n<p>| Wert | heisst |<br />|---|---|<br />| <code>NULL</code> | noch nie gelaufen |<br />| <code>ok</code> | die Quelle hat geantwortet |<br />| <code>not_configured</code> | es fehlt eine Angabe, die nur der Betreiber liefern kann |<br />| <code>unreachable</code> | die Angabe ist da, die Quelle antwortet nicht |</p>\n<p>Die letzten beiden sehen an der Oberfläche gleich aus („keine Daten&quot;), sind<br />aber verschiedene Dinge: das eine ist eine Aufgabe für den Betreiber, das<br />andere eine Störung, die von selbst vergeht. Eine gestörte Quelle bricht den<br />Gesamtscan <strong>nicht</strong> ab — sonst sähen die übrigen aus wie „nichts Neues<br />gefunden&quot;, obwohl sie nie gefragt wurden.</p>\n<h3>Rechte</h3>\n<p>| Recht | erlaubt |<br />|---|---|<br />| <code>addons.trading.gov.read</code> | Quellenliste, Treffer, Beleg, Backtest, Konfiguration lesen |<br />| <code>addons.trading.gov.configure</code> | Quellen an/aus, Schwellen, Webhook, Scan auslösen |</p>\n<p>Die Gov-Daten sind <strong>instanzweit</strong> (ADR-054), nicht nutzergebunden — es gibt<br />kein Besitzerprädikat. Die Vorschlagsliste (<code>/gov-proposals</code>) ist die Ausnahme:<br />sie gehört dem Nutzer und wird über <code>user_id</code> gebunden.</p>\n<h3>Emittenten-Auflösung</h3>\n<p><code>resolveIssuer</code> löst einen gemeldeten Namen gegen <code>gov_issuers</code> auf und liefert<br />einen von drei Zuständen:</p>\n<p>| Zustand | heisst |<br />|---|---|<br />| <code>resolved</code> | genau ein Treffer, <code>ticker</code> ist gesetzt |<br />| <code>ambiguous</code> | mehrere Treffer, <code>candidates</code> nennt sie |<br />| <code>unresolved</code> | kein Treffer |</p>\n<p><strong><code>unresolved</code> lässt die Meldung nicht verschwinden.</strong> Der Entwurf trägt dann<br /><code>ticker: null</code> und bleibt sichtbar; nur zum Handelssignal wird er nicht. Eine<br />frühere Fassung liess solche Meldungen fallen — damit war die Zahl der<br />gefundenen Vorgänge systematisch zu klein, ohne dass es jemand sah.</p>\n<p>Die Auflösung vergleicht <strong>exakt</strong> auf der normalisierten Form. Eine frühere<br />beidseitige Teilstring-Heuristik erzeugte belegbare Falschtreffer<br />(<code>&quot;AMAZONIA CONSTRUCTORA LTDA&quot;</code> → <code>AMZN</code>, <code>&quot;CO&quot;</code> → <code>AIR</code>) — und aus einem<br />Falschtreffer wird kein Anzeigefehler, sondern ein Handelssignal auf das<br />falsche Papier.</p>\n<h3>Der Leerzustand des Backtests</h3>\n<p>Ohne konfigurierte Kursquelle gibt es <strong>keine Trefferquote</strong> — und <code>0 %</code> wäre<br />die schlechteste aller Antworten: sie sieht aus wie eine Messung und heisst<br />„alles verloren&quot;. Jedes Ergebnis trägt deshalb entweder Zahlen <strong>oder</strong> einen<br />Grund:</p>\n<p>| Grund | wann |<br />|---|---|<br />| <code>no_price_source</code> | keine Kursreihe vorhanden |<br />| <code>insufficient_sample</code> | weniger als 8 auswertbare Signale |<br />| <code>no_price_at_signal</code> | dieses Signal hat keinen Kurs am Signaltag |<br />| <code>horizon_incomplete</code> | der Horizont reicht über das Ende der Reihe hinaus |</p>\n<p>Die letzten beiden gelten je Signal und schliessen es aus der Stichprobe aus,<br />ohne die ganze Rechnung zu verwerfen. Fenster: 24 Monate Rückblick, 20<br />Handelstage Horizont.</p>\n<h2>Architecture</h2>\n<pre><code>lib/trading/\n├── client.ts       # Hyperliquid SDK wrapper\n├── engine.ts       # Core trading: buy, sell, close, stop-loss\n├── orders.ts       # Order types: market, limit, stop, take-profit\n├── performance.ts  # Stats, history, snapshots\n├── fx.ts           # EUR/USD rate\n├── charts.ts       # Chart data\n├── risk.ts         # Risk calculations\n├── journal.ts      # Trading journal\n├── lessons.ts      # AI lessons\n├── autotrading.ts  # Auto-trading config\n└── websocket.ts    # Real-time feeds</code></pre>\n<p>app/api/v1/trading/  # API routes (16 endpoints)<br />extensions/trading/   # Extension manifest &amp; registration<br />```</p>",
          "byteSize": 17348
        }
      }
    },
    {
      "id": "travel-booking",
      "uuid": "8ad3de7d-5aff-441f-9498-9073b3d16a9a",
      "name": "Travel-Booking (read-only Flug- + Hotelsuche)",
      "version": "0.2.0",
      "description": "Read-only Flug- + Hotelsuche: sucht und vergleicht normalisierte Flug- und Hotelangebote aus serverkonfigurierten Aggregatoren (Fake + echter Adaptervertrag). Keine Buchung, keine Zahlung, keine Browserautomation. User-scoped Suchprofil/-historie (ADR-054).",
      "author": "Aiopta",
      "icon": "✈️",
      "capabilities": [
        "travel-booking",
        "flights",
        "flight-search",
        "hotels",
        "hotel-search"
      ],
      "tags": [],
      "download": "/artifacts/travel-booking-0.2.0.zip",
      "sha256": "f2d1f9be2fab27ec9f1e0450933f970384b1e2a7424b1132eac761b66e7b3818",
      "size": 189840,
      "page": null,
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/travel-booking/flights",
        "/api/v1/addons/travel-booking/hotels"
      ],
      "skills": [
        {
          "name": "travel-booking-search-flights",
          "displayName": "Travel-Booking: Flüge suchen (read-only)",
          "description": "Sucht normalisierte Flugangebote (Preis minor + Währung, Zeiten, Segmente, Quote-Ablauf) aus konfigurierten Aggregatoren. Streng read-only — löst keine Buchung aus und exponiert keine Credentials.",
          "permissions": [
            "addons/travel-booking/flights"
          ],
          "endpoint": "/api/v1/addons/travel-booking/flights",
          "scripts": []
        },
        {
          "name": "travel-booking-search-hotels",
          "displayName": "Travel-Booking: Hotels suchen (read-only)",
          "description": "Sucht normalisierte Hotelangebote (Preis minor + Währung, Steuern/Gebühren, Meal-Plan, Storno/Refundability, Quote-Ablauf) aus konfigurierten Aggregatoren. Streng read-only — löst keine Buchung aus, synthetisiert keine Bewertungen und exponiert keine Credentials.",
          "permissions": [
            "addons/travel-booking/hotels"
          ],
          "endpoint": "/api/v1/addons/travel-booking/hotels",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": null
    },
    {
      "id": "trip-planner",
      "uuid": "821a1fcc-69ff-4b9d-a9e5-a235d66dd260",
      "name": "Trip-Planner (Reisen: Etappen/Unterkünfte/POIs/Packliste/Budget/Karte)",
      "version": "1.0.1",
      "description": "Per-User Reisen: Trips mit Etappen (Timeline), Unterkünften, POIs, Packliste, pro-Trip Budget (Rollup aus Etappen + Unterkünften) und einer Karte. Lokale SQLite ist die Quelle der Wahrheit (ADR-054).",
      "author": "Aiopta",
      "icon": "🧳",
      "capabilities": [
        "trip-planner",
        "trips",
        "stages",
        "accommodations",
        "pois",
        "packing",
        "budget",
        "map"
      ],
      "tags": [],
      "download": "/artifacts/trip-planner-1.0.1.zip",
      "sha256": "dcc5a4ed8d9bdf68e572124795e82b7d09a6f72581cd2840f2370b92d02cec12",
      "size": 120574,
      "page": {
        "id": "trip-planner-page",
        "path": "/addons/trip-planner",
        "name": "Trip-Planner",
        "icon": "🧳",
        "section": "Tools",
        "component": "@addons/trip-planner/src/components/TripPlannerPage"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/trip-planner/trips",
        "/api/v1/addons/trip-planner/trips/:id",
        "/api/v1/addons/trip-planner/stages",
        "/api/v1/addons/trip-planner/stages/:id",
        "/api/v1/addons/trip-planner/accommodations",
        "/api/v1/addons/trip-planner/accommodations/:id",
        "/api/v1/addons/trip-planner/pois",
        "/api/v1/addons/trip-planner/pois/:id",
        "/api/v1/addons/trip-planner/packing",
        "/api/v1/addons/trip-planner/packing/:id",
        "/api/v1/addons/trip-planner/budget"
      ],
      "skills": [
        {
          "name": "trip-planner-read",
          "displayName": "Trip-Planner: Reisen, Etappen & Orte lesen",
          "description": "Listet Reisen, Etappen, Unterkünfte, POIs und die Packliste für den Nutzer und zeigt das pro-Trip Budget (Rollup). Reiner Lesezugriff — legt nichts an.",
          "permissions": [
            "addons/trip-planner/trips",
            "addons/trip-planner/stages",
            "addons/trip-planner/accommodations",
            "addons/trip-planner/pois",
            "addons/trip-planner/packing",
            "addons/trip-planner/budget"
          ],
          "endpoint": "/api/v1/addons/trip-planner/budget",
          "scripts": []
        },
        {
          "name": "trip-planner-manage",
          "displayName": "Trip-Planner: Reisen & Etappen verwalten",
          "description": "Legt Reisen und Etappen an oder entfernt diese, setzt Etappen-Reihenfolge und Budget (per-User gebucht).",
          "permissions": [
            "addons/trip-planner/trips",
            "addons/trip-planner/stages"
          ],
          "endpoint": "/api/v1/addons/trip-planner/trips",
          "scripts": []
        },
        {
          "name": "trip-planner-details",
          "displayName": "Trip-Planner: Unterkünfte, POIs & Packliste verwalten",
          "description": "Legt Unterkünfte, POIs und Packlisten-Einträge an oder entfernt diese und setzt den Pack-Status (per-User gebucht).",
          "permissions": [
            "addons/trip-planner/accommodations",
            "addons/trip-planner/pois",
            "addons/trip-planner/packing"
          ],
          "endpoint": "/api/v1/addons/trip-planner/packing",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>🧳 Trip-Planner (Reisen: Etappen/Unterkünfte/POIs/Packliste/Budget/Karte)</h1>\n<p>Per-User Reisen: Trips mit Etappen (Timeline), Unterkünften, POIs, Packliste, pro-Trip Budget (Rollup aus Etappen + Unterkünften) und einer Karte. Lokale SQLite ist die Quelle der Wahrheit (ADR-054).</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>trip-planner</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> trip-planner, trips, stages, accommodations, pois, packing, budget, map</li><li><strong>UI-Seite:</strong> <code>/addons/trip-planner</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Keine Secrets erforderlich — funktioniert ohne zusätzliche Credentials. Die Karte<br />ist eingebettet (Equirectangular-Projektion über Lat/Lng der Etappen/Unterkünfte/POIs)<br />und benötigt keine externen Tile-Provider.</p>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET, POST | <code>/api/v1/addons/trip-planner/trips</code> |<br />| PATCH, DELETE | <code>/api/v1/addons/trip-planner/trips/[id]</code> |<br />| GET, POST | <code>/api/v1/addons/trip-planner/stages</code> |<br />| PATCH, DELETE | <code>/api/v1/addons/trip-planner/stages/[id]</code> |<br />| GET, POST | <code>/api/v1/addons/trip-planner/accommodations</code> |<br />| DELETE | <code>/api/v1/addons/trip-planner/accommodations/[id]</code> |<br />| GET, POST | <code>/api/v1/addons/trip-planner/pois</code> |<br />| DELETE | <code>/api/v1/addons/trip-planner/pois/[id]</code> |<br />| GET, POST | <code>/api/v1/addons/trip-planner/packing</code> |<br />| PATCH, DELETE | <code>/api/v1/addons/trip-planner/packing/[id]</code> |<br />| GET | <code>/api/v1/addons/trip-planner/budget</code> |</p>\n<p>Alle Endpunkte sind per <code>userRoute</code> authentifiziert und per <code>scopedStore(auth.id)</code><br />per-User gebucht (ADR-054). Ein fremdes/fehlendes <code>id</code> -&gt; 404 ohne Existenz-Leak.<br />Das Löschen einer Reise kaskadiert auf deren Etappen/Unterkünfte/POIs/Packliste.</p>\n<h2>Budget</h2>\n<p><code>GET /budget?tripId=</code> liefert den Rollup: geplante Ausgaben (Summe aus Etappen-<br />+ Unterkünfte-Kosten) gegen das Reise-Budget, mit <code>remaining</code>, <code>percentUsed</code> und<br /><code>overBudget</code>. Zusätzlich wird der Pack-Fortschritt (<code>packed/total</code>, <code>percent</code>)<br />zurückgegeben.</p>\n<blockquote><p>README aus <code>addon.json</code> + vorhandenen Routen generiert (<code>scripts/generate-addon-readmes.js</code>). Bei manuellen Ergänzungen den Generator-Hinweis entfernen.</p></blockquote>",
          "byteSize": 2063
        }
      }
    },
    {
      "id": "voice",
      "uuid": "9777b5eb-54ad-488b-b622-1f7724eb6007",
      "name": "Voice",
      "version": "1.1.2",
      "description": "Unified voice addon combining Text-to-Speech and Speech-to-Text with multiple backend support.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "tts",
        "stt",
        "voice",
        "audio"
      ],
      "tags": [],
      "download": "/artifacts/voice-1.1.2.zip",
      "sha256": "a8302ed165af93282fc5d8946e72c9088e09fd0fb63cf297970db77fb8834082",
      "size": 147817,
      "page": {
        "path": "/addons/voice",
        "name": "Voice",
        "icon": "🎤",
        "section": "Settings",
        "component": "@addons/voice/src/ui/VoiceSettings"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/voice/health",
        "/api/v1/addons/voice/status",
        "/api/v1/addons/voice/start",
        "/api/v1/addons/voice/stop",
        "/api/v1/addons/voice/synthesize",
        "/api/v1/addons/voice/transcribe",
        "/api/v1/addons/voice/voices",
        "/api/v1/addons/voice/backends",
        "/api/v1/addons/voice/backend/switch",
        "/api/v1/addons/voice/settings",
        "/api/v1/addons/voice/tools/synthesize",
        "/api/v1/addons/voice/tools/transcribe",
        "/api/v1/addons/voice/tools/voice-proxy",
        "/api/v1/addons/voice/tools/voice-reply",
        "/api/v1/addons/voice/tools/voice-reply-external"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Voice</h1>\n<p>Unified voice addon combining Text-to-Speech and Speech-to-Text with multiple backend support.</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>voice</code></li><li><strong>Version:</strong> 1.0.0</li><li><strong>Fähigkeiten:</strong> tts, stt, voice, audio</li><li><strong>UI-Seite:</strong> <code>/addons/voice</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Dieses Addon benötigt folgende Secrets/Provider-Konfiguration (Settings → Credentials bzw. Provider-Store, <strong>nie</strong> in addon.json):</p>\n<ul><li><code>AWS_ACCESS_KEY_ID</code></li><li><code>AWS_SECRET_ACCESS_KEY</code></li><li><code>AWS_STT_ACCESS_KEY_ID</code></li><li><code>AWS_STT_SECRET_ACCESS_KEY</code></li><li><code>AZURE_STT_SUBSCRIPTION_KEY</code></li><li><code>ELEVENLABS_API_KEY</code></li><li><code>GOOGLE_STT_API_KEY</code></li><li><code>GOOGLE_TTS_API_KEY</code></li></ul>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| POST | <code>/api/v1/addons/voice/backend/switch</code> |<br />| GET | <code>/api/v1/addons/voice/backends</code> |<br />| GET | <code>/api/v1/addons/voice/health</code> |<br />| GET, POST | <code>/api/v1/addons/voice/settings</code> |<br />| POST | <code>/api/v1/addons/voice/start</code> |<br />| GET | <code>/api/v1/addons/voice/status</code> |<br />| POST | <code>/api/v1/addons/voice/stop</code> |<br />| POST | <code>/api/v1/addons/voice/synthesize</code> |<br />| GET, POST | <code>/api/v1/addons/voice/tools/synthesize</code> |<br />| GET, POST | <code>/api/v1/addons/voice/tools/transcribe</code> |<br />| GET, POST | <code>/api/v1/addons/voice/tools/voice-proxy</code> |<br />| GET, POST | <code>/api/v1/addons/voice/tools/voice-reply</code> |<br />| GET, POST | <code>/api/v1/addons/voice/tools/voice-reply-external</code> |<br />| POST | <code>/api/v1/addons/voice/transcribe</code> |<br />| GET | <code>/api/v1/addons/voice/voices</code> |</p>\n<h2>Bekannte Grenzen</h2>\n<ul><li>Keine bekannten Einschränkungen.</li></ul>\n<blockquote><p>README aus <code>addon.json</code> + vorhandenen Routen generiert (<code>scripts/generate-addon-readmes.js</code>). Bei manuellen Ergänzungen den Generator-Hinweis entfernen.</p></blockquote>",
          "byteSize": 1582
        }
      }
    },
    {
      "id": "vpn",
      "uuid": "9cdda8a9-9ac1-4548-b49d-fb20573ec8f0",
      "name": "VPN (WireGuard / Tailscale — Peers, Status & Konfig-QR)",
      "version": "1.0.2",
      "description": "Per-User VPN-Verwaltung: BYOK-Verbindung zu WireGuard (wg-easy API) oder Tailscale (API). Listet Peers (connected/disconnected), zeigt Peer-Status (Handshake/Transfer), generiert Konfig-QR (WireGuard-Peer-Config → QR-Data-URI) und legt Peers an/entfernt. Read/Write gegenüber dem eigenen VPN-Server, vollständig per-User über scopedStore isoliert (ADR-054).",
      "author": "Aiopta",
      "icon": "🔒",
      "capabilities": [
        "vpn",
        "wireguard",
        "tailscale",
        "peers",
        "peer-status",
        "config-qr",
        "connections"
      ],
      "tags": [],
      "download": "/artifacts/vpn-1.0.2.zip",
      "sha256": "97e15c8eb104de13a3cf5c43162a4d04e6718c531d52216d2b9c908acac0fc44",
      "size": 109229,
      "page": {
        "id": "vpn-page",
        "path": "/addons/vpn",
        "name": "VPN",
        "icon": "🔒",
        "section": "Infra",
        "component": "@addons/vpn/src/components/VpnPage"
      },
      "deprecated": true,
      "replacedBy": "network",
      "deprecation": {
        "deprecated": true,
        "replacedBy": "network",
        "sunset": "2026-12-31",
        "successorPath": "/addons/network",
        "successorLabel": "Network/Homelab (Nachfolger)"
      },
      "endpoints": [
        "/api/v1/addons/vpn/config",
        "/api/v1/addons/vpn/config/:id",
        "/api/v1/addons/vpn/peers",
        "/api/v1/addons/vpn/peers/:id",
        "/api/v1/addons/vpn/status",
        "/api/v1/addons/vpn/config-string",
        "/api/v1/addons/vpn/qr"
      ],
      "skills": [
        {
          "name": "vpn-read",
          "displayName": "VPN: Peers & Status lesen",
          "description": "Listet VPN-Peers (WireGuard/Tailscale) für den Nutzer mit Verbindungsstatus (connected/disconnected) und Peer-Status (Handshake/Transfer). Reiner Lesezugriff — legt nichts an.",
          "permissions": [
            "addons/vpn/peers",
            "addons/vpn/status",
            "addons/vpn/config"
          ],
          "endpoint": "/api/v1/addons/vpn/peers",
          "scripts": []
        },
        {
          "name": "vpn-manage",
          "displayName": "VPN: Peers & Verbindung verwalten",
          "description": "Legt die BYOK-Verbindung zum VPN-Server an/entfernt diese und fügt Peers hinzu oder entfernt sie (per-User gebucht: provider, base URL, Token — niemals das Passwort im Klartext zurückgegeben). Stellt die WireGuard-Peer-Konfiguration und den Konfig-QR bereit.",
          "permissions": [
            "addons/vpn/peers",
            "addons/vpn/config",
            "addons/vpn/config-string",
            "addons/vpn/qr"
          ],
          "endpoint": "/api/v1/addons/vpn/peers",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": null
    },
    {
      "id": "websearch",
      "uuid": "c39538aa-2123-46d4-b973-b1ffc6b3b2e4",
      "name": "Research",
      "version": "2.1.1",
      "description": "Unified research capability: web search (Brave, ZAI, Google, SearXNG) for chat agents, plus trend radar, briefing studio, YouTube intelligence, web fetch (URL extraction), image analysis, and PDF analysis. Consolidates websearch + trend-radar + briefing-studio + youtube-intelligence + research-tools (ADR-008, issue #374).",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "search",
        "web-search",
        "multi-provider",
        "brave-search",
        "zai-search",
        "google-search",
        "searxng",
        "trend-scanning",
        "watchlists",
        "news-monitoring",
        "briefing-generation",
        "script-framing",
        "youtube-analysis",
        "transcript-analysis",
        "content-ideation",
        "title-patterns",
        "research",
        "web-fetch",
        "image-analysis",
        "pdf-analysis"
      ],
      "tags": [],
      "download": "/artifacts/websearch-2.1.1.zip",
      "sha256": "630369dce1228ceb5738540576d6855e230fc791e8d738b0854ad6c60010a01f",
      "size": 171555,
      "page": {
        "path": "/addons/websearch",
        "name": "Research",
        "icon": "🔍",
        "section": "Tools",
        "component": "@addons/websearch/src/components/WebSearchSettings"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/websearch/search",
        "/api/v1/addons/websearch/zread",
        "/api/v1/addons/websearch/settings",
        "/api/v1/addons/websearch/status",
        "/api/v1/addons/websearch/providers",
        "/api/v1/addons/websearch/web-fetch",
        "/api/v1/addons/websearch/image-analysis",
        "/api/v1/addons/websearch/pdf-analysis",
        "/api/v1/addons/websearch/trend-radar/status",
        "/api/v1/addons/websearch/trend-radar/news",
        "/api/v1/addons/websearch/trend-radar/scan",
        "/api/v1/addons/websearch/trend-radar/watchlists",
        "/api/v1/addons/websearch/trend-radar/watchlists/:id/run",
        "/api/v1/addons/websearch/briefing-studio/status",
        "/api/v1/addons/websearch/briefing-studio/research",
        "/api/v1/addons/websearch/briefing-studio/script-frame",
        "/api/v1/addons/websearch/youtube-intelligence/status",
        "/api/v1/addons/websearch/youtube-intelligence/channel/analyze",
        "/api/v1/addons/websearch/youtube-intelligence/video/transcript",
        "/api/v1/addons/websearch/youtube-intelligence/patterns",
        "/api/v1/addons/websearch/youtube-intelligence/ideas"
      ],
      "skills": [],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Web Search</h1>\n<p>Enable web search capabilities for chat agents with multi-provider support (Brave, ZAI, Google, SearXNG). Allows agents to search the web and read URLs for current information. Since issue #374 the addon also carries the consolidated research-tools capabilities: web fetch (URL content extraction), image analysis (ZAI Vision) and PDF analysis (ZAI PDF).</p>\n<h2>Überblick</h2>\n<ul><li><strong>Addon-ID:</strong> <code>websearch</code></li><li><strong>Version:</strong> 2.1.0</li><li><strong>Fähigkeiten:</strong> search, web-search, multi-provider, brave-search, zai-search, google-search, searxng, research, web-fetch, image-analysis, pdf-analysis (+ trend-radar / briefing-studio / youtube-intelligence Namespaces)</li><li><strong>UI-Seite:</strong> <code>/addons/websearch</code></li></ul>\n<h2>Einrichtung</h2>\n<p>Dieses Addon benötigt folgende Secrets/Provider-Konfiguration (Settings → Credentials bzw. Provider-Store, <strong>nie</strong> in addon.json):</p>\n<ul><li><code>BRAVE_API_KEY</code></li><li><code>GOOGLE_SEARCH_API_KEY</code></li><li><code>SEARXNG_API_KEY</code></li><li><code>ZAI_API_KEY</code></li></ul>\n<h2>API-Endpunkte</h2>\n<p>| Methode(n) | Pfad |<br />|---|---|<br />| GET, POST | <code>/api/v1/addons/websearch/config</code> |<br />| GET | <code>/api/v1/addons/websearch/providers</code> |<br />| POST | <code>/api/v1/addons/websearch/search</code> |<br />| GET, POST | <code>/api/v1/addons/websearch/settings</code> |<br />| GET | <code>/api/v1/addons/websearch/status</code> |<br />| POST | <code>/api/v1/addons/websearch/zread</code> |<br />| GET | <code>/api/v1/addons/websearch/web-fetch</code> |<br />| POST | <code>/api/v1/addons/websearch/image-analysis</code> |<br />| POST | <code>/api/v1/addons/websearch/pdf-analysis</code> |</p>\n<h3>Web Fetch (issue #374, ehemals research-tools)</h3>\n<pre><code>GET /api/v1/addons/websearch/web-fetch?url=&lt;url&gt;</code></pre>\n<p>Authentifiziert. Liefert <code>{ success, url, title, content, extractedAt }</code>. Der Abruf läuft<br />hinter einem SSRF-Guard (nur http/https, DNS-geprüfte öffentliche Adressen, Redirect-Hops<br />werden einzeln re-validiert, Größen-/Zeitlimit) — private/loopback/link-local/metadata-Ziele<br />werden mit <code>403</code> abgelehnt, ohne dass ein ausgehender Request stattfindet. Siehe<br /><code>src/lib/research-tools/web-fetch.ts</code> und <code>addons/research-tools/SUNSET.md</code>.</p>\n<h3>Image / PDF Analysis (issue #374, ehemals research-tools)</h3>\n<pre><code>POST /api/v1/addons/websearch/image-analysis\nBody: { &quot;imagePath&quot;: &quot;uploads/image.jpg&quot;, &quot;prompt&quot;: &quot;Describe this image&quot; }</code></pre>\n<p>POST /api/v1/addons/websearch/pdf-analysis<br />Body: { &quot;pdfPath&quot;: &quot;docs/file.pdf&quot;, &quot;prompt&quot;: &quot;Summarize this document&quot; }<br />```</p>\n<blockquote><p><strong>Per-User-Isolation (#147 / ADR-054, 1:1 aus research-tools übernommen):</strong> beide</p><p>Endpoints sind authentifiziert und lesen ausschließlich Dateien unter dem per-User-Datenroot</p><p>(<code>&lt;DATA_DIR&gt;/addons/websearch/&lt;userId&gt;</code>). <code>imagePath</code> / <code>pdfPath</code> sind <strong>relativ</strong> zu diesem</p><p>Root — absolute Pfade und <code>../</code>-Traversal werden mit <code>403</code> abgelehnt.</p></blockquote>\n<h2>Bekannte Grenzen</h2>\n<ul><li>Keine bekannten Einschränkungen.</li></ul>",
          "byteSize": 2679
        }
      }
    },
    {
      "id": "white-label",
      "uuid": "d480f9bd-1afa-463a-aa87-5cac2726bef0",
      "name": "White-Label & Business-Packs",
      "version": "1.0.2",
      "description": "Rebrand the instance (app name, logo, accent color, login tagline, powered-by toggle) with SAFE DEFAULTS so an unconfigured instance is visually unchanged, and apply business-packs — named bundles of addons + agent templates that set up a business vertical in a few clicks by reusing the existing enable/instantiate primitives (Phase K / Phase A). FULL white-label (OD #34/#35/#36): upload a brand LOGO (magic-byte validated, size-capped, registry-free storage under DATA_DIR, served via the addon and wired into the core logo URL); the custom-branding capability is TIER-GATED via the #119 licensing scaffold (graceful fallback to default MC branding + upgrade hint on a non-entitled tier); and a per-customer CUSTOM-DOMAIN config + status is stored (actual TLS cert issuance + DNS remain owner/infra at the reverse-proxy). Branding lives in core app_settings (a sibling of provisioning, ADR-020); the shell + login read it via the public GET /api/v1/branding and the accent threads through var(--color-accent*). Admin-gated; writes entitlement-gated; pack apply safe + idempotent. No container, no external calls (ADR-009, ADR-027).",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "white-label-branding",
        "white-label-business-packs"
      ],
      "tags": [],
      "download": "/artifacts/white-label-1.0.2.zip",
      "sha256": "be67db3c4c1742ec5b91535e37654a05d2da00f54e6ed07e03c85a9160a46ac6",
      "size": 220962,
      "page": {
        "id": "white-label-page",
        "path": "/addons/white-label",
        "name": "White-Label",
        "icon": "🏷️",
        "section": "Admin",
        "component": "@addons/white-label/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/white-label/status",
        "/api/v1/addons/white-label/branding",
        "/api/v1/addons/white-label/packs",
        "/api/v1/addons/white-label/packs/:id/apply",
        "/api/v1/addons/white-label/widget",
        "/api/v1/addons/white-label/logo",
        "/api/v1/addons/white-label/config"
      ],
      "skills": [
        {
          "name": "white-label-branding",
          "displayName": "White-Label: Branding lesen/setzen",
          "description": "Read the effective instance branding (app name, logo URL, accent color, login tagline, powered-by) and update it (admin). Allow-listed keys only; color/url shape-validated; safe MC defaults when unset. No secrets — branding is public-by-nature.",
          "permissions": [
            "addons/white-label/branding",
            "addons/white-label/status"
          ],
          "endpoint": "/api/v1/addons/white-label/branding",
          "scripts": []
        },
        {
          "name": "white-label-packs-apply",
          "displayName": "White-Label: Business-Pack anwenden",
          "description": "List the built-in business-packs and apply one (admin): it enables the pack's addons (Phase K) and instantiates its agent templates for the caller (Phase A, own key, no secrets). Safe + idempotent — re-apply never duplicates. Returns a summary of what was applied/skipped.",
          "permissions": [
            "addons/white-label/packs",
            "addons/white-label/status"
          ],
          "endpoint": "/api/v1/addons/white-label/packs",
          "scripts": []
        },
        {
          "name": "white-label-full",
          "displayName": "White-Label: Logo-Upload, Entitlement, Custom-Domain",
          "description": "Upload/replace/remove the brand logo (admin; magic-byte validated image, size-capped, served via the addon and wired into the core logo URL), read the tier-gating entitlement (is full white-label unlocked on the active tier, + upgrade hint — builds on the #119 tier scaffold), and configure the per-customer custom domain + status (TLS/DNS handled at the reverse-proxy — owner/infra). Admin-gated; writes are entitlement-gated and degrade gracefully on a non-entitled tier. No secret — branding + hostname are public-by-nature.",
          "permissions": [
            "addons/white-label/logo",
            "addons/white-label/config",
            "addons/white-label/status"
          ],
          "endpoint": "/api/v1/addons/white-label/config",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>White-Label &amp; Business-Packs</h1>\n<p>Two admin capabilities for resellers/operators (ADR-027):</p>\n<p>- <strong>White-label branding</strong> — rebrand this Aiopta instance (app name, logo URL, accent color,<br />  login tagline, &quot;powered by Aiopta&quot; toggle), applied across the shell + login. **SAFE<br />  DEFAULTS**: an unconfigured instance is visually identical to stock Aiopta.<br />- <strong>Business-packs</strong> — named bundles <code>{ addons[], agentTemplates[], settings }</code> that set up a business<br />  vertical in a few clicks by <strong>reusing existing primitives</strong> — enable + instantiate. Apply is **safe +<br />  idempotent** (re-apply never duplicates).<br />- <strong>Full white-label (#121 / INBOX-52)</strong> — logo <strong>upload</strong> + asset-hosting, a <strong>tier-gated</strong> branding<br />  entitlement (built on the #119 licensing scaffold), and per-customer <strong>custom-domain</strong> config. See<br />  &quot;Full white-label&quot; below.</p>\n<p>It <strong>extends + orchestrates</strong> — it never forks a store or reimplements install/provisioning.</p>\n<h2>Branding</h2>\n<p>- <code>Branding = { appName, logoUrl, accentColor, loginTagline, poweredBy }</code> lives in core <code>app_settings</code><br />  via <code>src/lib/branding.ts</code> — a <strong>sibling of provisioning.ts</strong> (ADR-020) that SHARES <code>INSTANCE_NAME</code> +<br />  <code>INSTANCE_BRAND_COLOR</code> and adds <code>BRANDING_LOGO_URL</code>/<code>_LOGIN_TAGLINE</code>/<code>_POWERED_BY</code>. <strong>No new table.</strong><br />- <code>getBranding()</code> returns the exact current MC values when unset (<code>AIOPTA_DEFAULT_BRANDING</code>, accent<br />  <code>#7c3aed</code>). <code>setBranding()</code> is an <strong>allow-list</strong> + <strong>shape-validates</strong> color (hex) and logo (empty /<br />  <code>/…</code> / <code>http(s)</code>; <code>javascript:</code>/<code>data:</code>/protocol-relative rejected).<br />- The shell + login read the <strong>public</strong> <code>GET /api/v1/branding</code>; a core <code>BrandingProvider</code><br />  (<code>src/lib/branding-client.tsx</code>, in <code>app/layout.tsx</code>) renders defaults immediately then threads a custom<br />  accent through <code>var(--color-accent*)</code> via a CSS variable on <code>&lt;html&gt;</code> — <strong>no per-component edits</strong>.</p>\n<h2>Business-packs</h2>\n<p>- <code>BusinessPack = { id, name, description, vertical, addons[], agentTemplates[], settings{} }</code>.<br />- 3 built-ins (repo-bundled addons + seeded templates): <code>ecommerce</code>, <code>agency-leadgen</code>, <code>content-saas</code>.<br />- <strong>Apply</strong> reuses primitives and is <strong>idempotent</strong>: addons → enabled via the shared <code>extensions</code> table<br />  (Addon-Manager, ADR-022; already-enabled → skipped); templates → instantiated for the caller via the<br />  Phase-A route (own key, no secrets; owned template / 409 → skipped); settings → allow-listed keys only.<br />- Apply-state is <strong>derived</strong> from <code>extensions</code> + <code>user_agents</code> (the addon owns no table).</p>\n<h2>Full white-label (OD #34/#35/#36, RESOLVED → built)</h2>\n<p>- <strong>Logo upload + asset-hosting (OD #35)</strong> — <code>src/lib/logo-storage.ts</code> stores an uploaded image<br />  registry-free under <code>DATA_DIR/white-label/</code> (a JSON sidecar — <strong>no table</strong>). The format is determined<br />  by <strong>magic-byte sniffing</strong> (PNG/JPEG/GIF/WEBP/SVG); the <strong>client MIME is never trusted</strong>, and a 2 MiB<br />  cap applies. <code>PUT …/logo</code> accepts a multipart <code>file</code> or JSON <code>{ dataUrl | base64 }</code>; the <strong>public</strong><br />  <code>GET …/logo</code> serves the bytes (with <code>X-Content-Type-Options: nosniff</code> + a strict CSP so an uploaded SVG<br />  stays inert), and the root-relative serve path is written into the core <code>Branding.logoUrl</code> (which the<br />  existing <code>isValidLogoUrl</code> already accepts — <strong>no core change</strong>). <code>DELETE …/logo</code> resets to the default.<br />- <strong>Branding entitlement, tier-gated (OD #36, builds on #119/ADR-050)</strong> — <code>src/lib/branding-entitlement.ts</code><br />  is a <strong>read-only</strong> consumer of <code>src/lib/entitlements.ts</code> (<code>hasEntitlement</code>) + <code>src/lib/tier-config.ts</code><br />  (<code>TIER_REGISTRY</code>); <strong>neither is modified</strong>. The shipped <code>Feature</code> union has no dedicated <code>whiteLabel</code><br />  flag yet, so it maps to the closest existing one, <code>addons.private-registry</code> (enterprise-only) — one line<br />  in <code>BRANDING_FEATURE</code> to change later. A non-entitled tier gets a <strong>graceful 402</strong> on the logo/domain<br />  writes (branding falls back to the Aiopta default, with an &quot;upgrade&quot; hint) — never a crash.<br />- <strong>Custom-domain config (OD #34)</strong> — <code>src/lib/custom-domain.ts</code> stores a hostname + status<br />  (<code>unconfigured</code>/<code>pending</code>/<code>active</code>) + a note as a JSON blob in <code>app_settings</code> (registry-free, `getDb()<br />  .prepare().run()`). <strong>Actual TLS certs + DNS are owner/infra at the reverse-proxy — no cert code ships.</strong><br />  An unconfigured instance uses the default domain (the public URL stays owned by the existing ingress).</p>\n<h2>Safety</h2>\n<p>- <strong>Admin-gated</strong>: branding by <code>user.manage</code>, pack apply by <code>addon.manage</code> (<code>requireCapability</code>).<br />- <strong>No secrets</strong>: branding is public-by-nature; pack apply mints a new own key via the Phase-A route and<br />  copies no secret.<br />- <strong>No external calls</strong>: pack apply dispatches over the <strong>same-origin</strong> Aiopta API (<code>assertSameOrigin</code>); no<br />  container, no outbound request (ADR-009).<br />- <strong>Safe defaults</strong>: zero visual change when unconfigured.</p>\n<h2>Routes</h2>\n<p>Dual-registered in <code>addon.json</code> + <code>src/manifest.ts</code> (plus the core public read):</p>\n<ul><li><code>GET /api/v1/branding</code> (core, <strong>public</strong> — shell/login read this)</li><li><code>GET /api/v1/addons/white-label/branding</code> · <code>PUT /api/v1/addons/white-label/branding</code> (<code>user.manage</code>)</li><li><code>GET /api/v1/addons/white-label/packs</code> (<code>addon.manage</code>)</li><li><code>POST /api/v1/addons/white-label/packs/[id]/apply</code> (<code>addon.manage</code>)</li><li><code>GET /api/v1/addons/white-label/status</code> · <code>GET /api/v1/addons/white-label/widget</code> (<code>user.manage</code>)</li><li><code>GET /api/v1/addons/white-label/logo</code> (<strong>public</strong> serve) · <code>PUT</code> · <code>DELETE</code> (<code>user.manage</code>, entitlement-gated write)</li><li><code>GET /api/v1/addons/white-label/config</code> (<code>user.manage</code>) · <code>PUT</code> (<code>user.manage</code>, entitlement-gated)</li></ul>\n<p>See <code>SKILL.md</code> for the agent-facing cheat-sheet and ADR-027 (white-label business<br />packs, in the core repo under <code>docs/architecture/</code>) for the full decision. **Logo upload, tier-gated branding entitlement, and custom-domain config are now<br />built (OD #34/#35/#36).** Still owner/infra: actual TLS cert issuance + DNS (handled at the reverse-proxy,<br />OD #34), and a dedicated <code>whiteLabel</code> tier-feature flag (additive in #119 — today mapped to<br /><code>addons.private-registry</code>).</p>",
          "byteSize": 5938
        }
      }
    },
    {
      "id": "wordpress",
      "uuid": "9a02657b-a931-43fa-977f-2c91562caca7",
      "name": "WordPress & WooCommerce",
      "version": "1.2.1",
      "description": "Create and manage WordPress sites and WooCommerce shops — pages, posts, themes, menus, products, orders, coupons and customers — agent- and web-usable. Dual-mode (INBOX-63): connect an EXISTING WordPress+WooCommerce site, or run the Aiopta-managed container (ADR-009/ADR-010).",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "wordpress-sites",
        "wordpress-pages",
        "wordpress-posts",
        "wordpress-themes",
        "wordpress-menus",
        "woocommerce-products",
        "woocommerce-orders",
        "woocommerce-coupons",
        "woocommerce-customers"
      ],
      "tags": [],
      "download": "/artifacts/wordpress-1.2.1.zip",
      "sha256": "0d914982404d5d61193111f02ad5e56e22a27810b398ccbf1b169cb29eaaa736",
      "size": 161108,
      "page": {
        "id": "wordpress-page",
        "path": "/addons/wordpress",
        "name": "WordPress & WooCommerce",
        "icon": "🛍️",
        "section": "Tools",
        "component": "@addons/wordpress/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/wordpress/status",
        "/api/v1/addons/wordpress/config",
        "/api/v1/addons/wordpress/test",
        "/api/v1/addons/wordpress/sites",
        "/api/v1/addons/wordpress/pages",
        "/api/v1/addons/wordpress/posts",
        "/api/v1/addons/wordpress/themes",
        "/api/v1/addons/wordpress/menus",
        "/api/v1/addons/wordpress/products",
        "/api/v1/addons/wordpress/products/:id",
        "/api/v1/addons/wordpress/orders",
        "/api/v1/addons/wordpress/orders/:id",
        "/api/v1/addons/wordpress/coupons",
        "/api/v1/addons/wordpress/customers"
      ],
      "skills": [],
      "mcpServers": [
        {
          "id": "wordpress-tools",
          "displayName": "WordPress & WooCommerce tools",
          "description": "Read-only WordPress + WooCommerce companion tools for agents: normalize a site URL, resolve a WP core (/wp-json/wp/v2) or WooCommerce (/wp-json/wc/v3) REST URL, normalize WooCommerce product/order/customer payloads into the addon's stable shapes, and summarize a connection status - faithful ports of the addon's src/lib/wp-client.ts modules (issue #451, epic #399). Mutates nothing; all 12 mutating route handlers (create/update/delete product, update/delete order, create coupon, create page/post/menu, activate theme) stay in the addon's authenticated write API with its approval/audit pipeline. Live per-user WordPress + WooCommerce access is resolved core-side via the declared config/credential slots.",
          "transport": "stdio",
          "runtime": "node",
          "entry": "wordpress-tools/server.mjs",
          "permissions": [
            "addons/wordpress/read"
          ],
          "toolPolicy": {
            "mode": "read-only",
            "allowDestructive": false
          },
          "configRefs": [
            "WORDPRESS_MODE",
            "WORDPRESS_SITE_URL"
          ],
          "credentialRefs": [
            "AIOPTA_WP_APP_PASSWORD",
            "AIOPTA_WC_CONSUMER_SECRET"
          ],
          "archiveEntry": "wordpress/mcp/servers/wordpress-tools/server.mjs",
          "sha256": "698842925328ba80afead09b60a74133159d2b03dd0c97f64484925fa120c61f",
          "size": 25291,
          "descriptorVersion": 1
        }
      ],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>WordPress &amp; WooCommerce Addon</h1>\n<p>Create and manage <strong>WordPress sites</strong> and <strong>WooCommerce shops</strong> from inside Aiopta —<br />pages, posts, themes, menus (WordPress) and products, orders, coupons, customers (WooCommerce) —<br />usable both by agents (via the addon API) and by humans (via the management page).</p>\n<h2>Dual mode (INBOX-63): connect an existing site OR run a container</h2>\n<p>The addon works in <strong>two modes</strong>, chosen on the management page and persisted in Aiopta addon-config<br />(<code>WORDPRESS_MODE</code>, default <code>container</code>):</p>\n<p>- <strong>Aiopta-Container hochfahren (<code>container</code>, default)</strong> — the Aiopta-managed WordPress + WooCommerce<br />  container (ADR-010). Full container self-provisioning is still pending (<strong>TODO #133</strong>); until then<br />  the status reports &quot;Container nicht provisioniert&quot;.<br />- <strong>Bestehende Seite verbinden (<code>external</code>)</strong> — the owner connects an <strong>existing</strong> WordPress +<br />  WooCommerce site. They enter the <strong>site URL</strong>, a WordPress <strong>application password</strong> (user +<br />  app-password) and the <strong>WooCommerce consumer key + secret</strong>, hit <strong>&quot;Verbindung testen&quot;</strong>, and<br />  then every WP/Woo call runs against that real site. This mode is fully functional today.</p>\n<p><code>src/lib/wp-client.ts</code> resolves the base URL via <code>resolveBaseUrl()</code>: in <code>external</code> mode it is the<br />owner&#39;s <code>WORDPRESS_SITE_URL</code>; in <code>container</code> mode it is <code>getContainerAddonInternalUrl(&quot;wordpress&quot;)</code>.<br />Either way there is <strong>no hard-coded <code>localhost</code>/host</strong> anywhere — when no base URL is resolvable the<br />client throws a clear, mode-aware &quot;not configured / not provisioned&quot; error.</p>\n<h2>Architecture (ADR-009 / ADR-010)</h2>\n<p>- <strong>The container path is a container, not a host.</strong> WordPress with the WooCommerce plugin is an<br />  external tool, so it is declared as a <code>container</code> in <code>addon.json</code> / <code>manifest.ts</code><br />  (<code>registry.langkau.info/aiopta/wordpress-woo:latest</code>, <code>internalPort 80</code>,<br />  <code>healthPath /wp-json/</code>). It is booted at install by the container-service, started/stopped by the<br />  enable/disable lifecycle (G6), and removed on uninstall. The addon writes no lifecycle code.<br />- <strong>Reached only via the resolved base URL.</strong> <code>src/lib/wp-client.ts</code> talks to the<br />  <strong>WordPress REST API</strong> (<code>/wp-json/wp/v2/...</code>) and <strong>WooCommerce REST API</strong> (<code>/wp-json/wc/v3/...</code>)<br />  using the mode-aware base URL above.<br />- <strong>Secrets via Aiopta-config, never the manifest.</strong> The manifest declares only the <em>names</em> of the<br />  config keys (<code>envSchema</code>). Values — the WordPress <strong>application password</strong> and the WooCommerce<br />  <strong>consumer key + secret</strong> — are stored in Aiopta addon-config (<code>setAddonConfiguration</code>, secret-looking<br />  keys flagged <code>is_secret = 1</code>). The masked <code>GET /config</code> response only reports <em>whether</em> each<br />  secret is set, never its value. No secret value is ever written into the manifest or logged.</p>\n<h2>Authentication</h2>\n<p>| Surface | Mechanism | Config keys (Aiopta addon-config / <code>envSchema</code>) |<br />|---------|-----------|---------------------------------------------|<br />| Mode + site URL | non-secret connection config | <code>WORDPRESS_MODE</code> (<code>container</code>\\|<code>external</code>), <code>WORDPRESS_SITE_URL</code> |<br />| WordPress core (<code>/wp-json/wp/v2</code>) | Application password (Basic auth) | <code>AIOPTA_WP_USER</code>, <code>AIOPTA_WP_APP_PASSWORD</code> |<br />| WooCommerce (<code>/wp-json/wc/v3</code>) | Consumer key + secret (Basic auth) | <code>AIOPTA_WC_CONSUMER_KEY</code>, <code>AIOPTA_WC_CONSUMER_SECRET</code> |<br />| Container / DB | non-secret connection config | <code>WORDPRESS_DB_HOST</code>, <code>WORDPRESS_DB_NAME</code>, <code>WORDPRESS_DB_USER</code>, <code>WORDPRESS_TABLE_PREFIX</code> |</p>\n<h2>API</h2>\n<p>All routes are namespaced under <code>/api/v1/addons/wordpress/</code> and require auth.</p>\n<h3>WordPress</h3>\n<pre><code>GET  /status                      → mode + reachability + connection + config status\nGET  /config       PUT /config    → read (masked) / save mode + connect-existing creds\nPOST /test                        → &quot;Verbindung testen&quot;: probe WP + Woo on the configured site\nGET  /sites        POST /sites    → list / create managed site(s)\nGET  /pages        POST /pages    → list / create pages       { &quot;title&quot;, &quot;content?&quot;, &quot;status?&quot; }\nGET  /posts        POST /posts    → list / create posts        { &quot;title&quot;, &quot;content?&quot;, &quot;status?&quot; }\nGET  /themes       POST /themes   → list / activate theme      { &quot;stylesheet&quot; }\nGET  /menus        POST /menus    → list / create menu         { &quot;name&quot; }</code></pre>\n<h3>WooCommerce</h3>\n<pre><code>GET  /products       POST /products          → list / create product  { &quot;name&quot;, &quot;regular_price?&quot;, ... }\nGET  /products/[id]  PUT /products/[id]  DELETE /products/[id]\nGET  /orders                                 → list orders   (?status=, ?per_page=)\nGET  /orders/[id]    PUT /orders/[id]         → fetch / update an order (e.g. status)\nGET  /coupons        POST /coupons           → list / create coupon   { &quot;code&quot;, &quot;amount?&quot;, &quot;discount_type?&quot; }\nGET  /customers                              → list customers</code></pre>\n<p>Responses use the <code>{ &quot;success&quot;: true, &quot;data&quot;: … }</code> envelope. On failure read <code>error</code> and the HTTP<br />status (a <code>503</code> means the container is not provisioned; a <code>400</code> means credentials are not set).</p>\n<h2>Layout</h2>\n<pre><code>addons/wordpress/\n├── addon.json                 # Manifest (page + widget + container + endpoints)\n├── README.md\n├── SKILL.md                   # Agent skill\n├── src/\n│   ├── manifest.ts            # Extension (dual-registration: routes + page + container)\n│   ├── page.tsx               # Management UI (Sites/Pages/Posts + Products/Orders/Coupons)\n│   ├── widget.tsx             # Dashboard status widget (declarative key-values)\n│   ├── lib/wp-client.ts       # WP + Woo REST client — mode-aware base URL (external site OR container) + Aiopta-config\n│   └── app/api/v1/addons/wordpress/{status,config,test,sites,pages,posts,themes,menus,products,products/[id],orders,orders/[id],coupons,customers}/route.ts\n└── tests/unit/                # manifest validity + wp-client logic (dual-mode + testConnection) + config/test routes (mocked)</code></pre>\n<h2>Owner-parked items (non-blocking)</h2>\n<p><strong>External mode works today</strong> — the owner connects an existing site (URL + app-password + Woo<br />keys), tests it, and uses the full CRUD surface immediately.</p>\n<p>The <strong>container</strong> path needs its image + self-provisioning before it is usable:</p>\n<blockquote><p><strong>TODO(#133): container self-provisioning</strong> — the <code>container</code> mode declares the manifest</p><p><code>container</code> block but cannot yet actually provision/boot the WP+Woo stack from inside the addon.</p><p>Tracked as #133 (<code>addons/wordpress</code> is a concrete instance of #123/#133). Until then <code>container</code></p><p>mode reports &quot;Container nicht provisioniert&quot;; use <code>external</code> mode to connect an existing site.</p><p><strong>TODO(owner): <code>wordpress-woo-image</code></strong> — build + publish the</p><p><code>registry.langkau.info/aiopta/wordpress-woo</code> image (WordPress + WooCommerce behind its</p><p>HTTP server, exposing <code>/wp-json/</code>).</p><p><strong>TODO(owner): credentials</strong> — for either mode, the WordPress <strong>application password</strong></p><p>(<code>AIOPTA_WP_USER</code> / <code>AIOPTA_WP_APP_PASSWORD</code>) and the WooCommerce <strong>consumer key + secret</strong></p><p>(<code>AIOPTA_WC_CONSUMER_KEY</code> / <code>AIOPTA_WC_CONSUMER_SECRET</code>) are stored in Aiopta addon-config (via the addon page</p><p>or <code>PUT /config</code>). No secret is committed. The VALUES are owner-secret; the addon builds the</p><p>mechanism/UI.</p></blockquote>\n<h2>License</h2>\n<p>MIT</p>",
          "byteSize": 7176
        }
      }
    },
    {
      "id": "workflow-builder",
      "uuid": "83b7fda8-ea5c-4a92-a98f-60887f0936ed",
      "name": "Workflow / Automation Builder",
      "version": "1.1.8",
      "description": "Define automations (a trigger + ordered steps) that Aiopta runs server-side — an n8n-lite scoped to this platform. Triggers: manual, schedule (interval), webhook (tokened inbound URL), event. Step types: http (same-origin Aiopta API call), agent (run an Aiopta agent via the gateway), condition, delay. Steps thread prior-step output via {{stepN.field}}; the engine enforces a same-origin allow-list (never external services, never eval) and persists run history. No container (ADR-024). Canonical home of the former integrations addon's trigger surface (issue #375): the per-user webhook-trigger registry (/webhooks) and the admin-gated schedule-trigger view (/cron) live here.",
      "author": "Aiopta",
      "icon": "🧩",
      "capabilities": [
        "workflow-definitions",
        "workflow-execution",
        "workflow-triggers",
        "workflow-run-history",
        "workflow-webhooks",
        "webhooks",
        "cron",
        "integrations"
      ],
      "tags": [
        "onboarding"
      ],
      "download": "/artifacts/workflow-builder-1.1.8.zip",
      "sha256": "2e5192450461836ce741fda21684fef5c0d592e5eead8a405e364f2248f11f53",
      "size": 279722,
      "page": {
        "id": "workflow-builder-page",
        "path": "/addons/workflow-builder",
        "name": "Workflow Builder",
        "icon": "🔀",
        "section": "Automation",
        "component": "@addons/workflow-builder/src/page"
      },
      "deprecated": false,
      "endpoints": [
        "/api/v1/addons/workflow-builder/status",
        "/api/v1/addons/workflow-builder/webhooks",
        "/api/v1/addons/workflow-builder/cron",
        "/api/v1/addons/workflow-builder/workflows",
        "/api/v1/addons/workflow-builder/workflows/:id",
        "/api/v1/addons/workflow-builder/workflows/:id/run",
        "/api/v1/addons/workflow-builder/workflows/:id/runs",
        "/api/v1/addons/workflow-builder/workflows/:id/webhook",
        "/api/v1/addons/workflow-builder/widget",
        "/api/v1/addons/workflow-builder/widget-runs",
        "/api/v1/addons/workflow-builder/widget-link"
      ],
      "skills": [
        {
          "name": "workflow-manage",
          "displayName": "Workflow: Define & manage automations",
          "description": "List/create/edit/delete workflows (trigger + ordered steps). Validates the trigger and every step shape; a webhook trigger gets a server-minted token. Pure CRUD — does not execute anything.",
          "permissions": [
            "addons/workflow-builder/workflows",
            "addons/workflow-builder/status"
          ],
          "endpoint": "/api/v1/addons/workflow-builder/workflows",
          "scripts": []
        },
        {
          "name": "workflow-run",
          "displayName": "Workflow: Run & inspect history",
          "description": "Execute a workflow now (manual trigger) and read its run history. The engine orchestrates ONLY the Aiopta API (same-origin allow-list) and Aiopta agents via the gateway — never external services, never eval. The caller's auth is forwarded to internal steps so RBAC still applies.",
          "permissions": [
            "addons/workflow-builder/workflows",
            "addons/workflow-builder/status"
          ],
          "endpoint": "/api/v1/addons/workflow-builder/workflows/:id/run",
          "scripts": []
        }
      ],
      "mcpServers": [],
      "pricing": null,
      "pricingTiers": {
        "schemaVersion": 1,
        "currency": null,
        "tiers": [
          {
            "id": "free",
            "name": "Free",
            "priceMinor": 0,
            "interval": null,
            "features": [],
            "contactOnly": false
          }
        ]
      },
      "meta": {
        "readme": {
          "filename": "README.md",
          "sourcePath": "README.md",
          "rendered": "<h1>Workflow / Automation Builder</h1>\n<p>An in-Aiopta automation builder — a Zapier/n8n-lite <strong>scoped to this platform</strong>. A workflow is<br />a <strong>trigger</strong> plus an <strong>ordered list of steps</strong> that Aiopta runs server-side, threading each<br />step&#39;s output into the next via <code>{{stepN.field}}</code> interpolation.</p>\n<p>It orchestrates <em>existing</em> Aiopta capabilities and <strong>never calls external services directly</strong> (ADR-009):<br /><code>http</code> steps hit only the Aiopta API (same-origin allow-list), <code>agent</code> steps go through the Aiopta gateway.</p>\n<h2>Model</h2>\n<p>- <strong>Workflow</strong> = <code>{ id, name, enabled, trigger, steps[] }</code>.<br />- <strong>Trigger</strong> ∈ <code>manual</code> · <code>schedule</code> (interval seconds) · <code>webhook</code> (per-workflow token) · <code>event</code> (Aiopta<br />  event name).<br />- <strong>Step</strong> = <code>{ id, type, name?, config, onError }</code>, type ∈:<br />  - <code>http</code> — an Aiopta-internal API call (<code>method</code> + same-origin <code>/api/...</code> <code>path</code> + optional <code>body</code>).<br />  - <code>agent</code> — run an Aiopta agent via the gateway with a <code>prompt</code> (built from prior step output).<br />  - <code>condition</code> — <code>left &lt;op&gt; right</code>; on <strong>false</strong> the run stops cleanly (skip-rest). No <code>eval</code>.<br />  - <code>delay</code> — wait <code>ms</code> (capped at 60s).</p>\n<h2>Trigger surface absorbed from <code>integrations</code> (issue #375)</h2>\n<p>Workflow-builder is the canonical home of the former integrations addon&#39;s two<br />endpoints (parent #312, cluster <code>automation-orchestration</code>,<br /><code>docs/ADDON-CLUSTERS.md</code>):</p>\n<p>- <strong><code>/webhooks</code></strong> (GET/POST/DELETE) — the per-user <strong>webhook-trigger registry</strong><br />  (register/list/delete webhook entries; <code>user_id</code>-scoped, secrets masked).<br />  Auth: <code>userRoute</code> — 401 without a session; every read/write is isolated to<br />  the caller (#139).<br />- <strong><code>/cron</code></strong> (GET) — the read-only <strong>schedule-trigger view</strong> over the<br />  instance scheduler config (<code>&lt;DATA_DIR&gt;/cron/jobs.json</code>). Auth: <code>adminRoute</code><br />  — 401 unauthenticated / 403 non-admin (#586, ADR-054); only schedule<br />  metadata is exposed, never a job payload/secret.</p>\n<p>The legacy <code>/api/v1/addons/integrations/*</code> routes stay alive as alias shims<br />during the sunset window (until <strong>2026-12-31</strong>, RFC 8594 headers) and answer<br /><strong>410 Gone</strong> afterwards — see <code>addons/integrations/README.md</code> for the mapping<br />and the rollback recipe. The data migration is zero-copy: the <code>webhooks</code> table<br />and <code>jobs.json</code> are unchanged; only the serving endpoints moved.</p>\n<h2>Safety</h2>\n<p>- <strong>Same-origin allow-list</strong>: an <code>http</code> step&#39;s path must begin with <code>/api/</code> and resolve to the Aiopta<br />  origin; absolute/protocol-relative/traversal paths are refused both at validation time and at request<br />  time in the engine. A workflow can never make an arbitrary outbound request.<br />- <strong>No eval / no code execution</strong>: conditions are a fixed operator switch; interpolation is plain string<br />  substitution.<br />- <strong>Auth forwarding</strong>: manual runs forward the caller&#39;s session/agent auth to internal steps, so Aiopta<br />  RBAC still applies — a workflow can do no more than its initiator.<br />- <strong>Webhook</strong>: token-gated (constant-time compare), no Aiopta session; the token is never logged or echoed.</p>\n<h2>Persistence</h2>\n<p>Two addon-owned tables (created in <code>install()</code>, dropped in <code>uninstall()</code> via Aiopta core&#39;s <code>getDb()</code>):<br /><code>workflow_builder_workflows</code> and <code>workflow_builder_runs</code> (run history). The addon never touches the core<br />schema. The webhook-trigger registry (<code>/webhooks</code>, #375) reads/writes the <strong>core-owned</strong> <code>webhooks</code><br />table through the user-scope layer (<code>scopedStore</code>), exactly as its integrations predecessor did.</p>\n<h2>UI</h2>\n<p>A workflow <strong>list</strong> + a structured <strong>step editor</strong>: pick a trigger, add/reorder/remove steps with<br />type-specific config, <strong>Run now</strong>, and a run-history timeline. A full visual drag-drop canvas is a<br />future enhancement (see <code>docs/OPEN-DECISIONS.md</code>).</p>\n<p>See <code>SKILL.md</code> for the agent-facing endpoint cheat-sheet and <code>docs/architecture/ADR-024-workflow-builder.md</code><br />for the design.</p>",
          "byteSize": 3755
        }
      }
    }
  ]
}
