/** * Portal-side package catalog. Hardcoded mirror of the operator-side * catalog ConfigMap (deploy/helm/pieced-operator/templates/catalog-cm.yaml). * * The two have to stay in sync: * - `id` here must match the catalog key in the ConfigMap. * - `secrets[].key` here must match the catalog's env_vars[].secret_key * so that POST /api/tenants/:name/secrets writes to the same OpenBao * path the operator's ExternalSecret reads from. * - `requiresSecrets` is true when the catalog declares any env_var * that is a secret (vault_path_suffix set, no default value). * * Category model (Phase A rework): * - core — platform-behaviour toggles (heartbeat, cron, * active-memory, voice). Mostly no secrets. core-voice is * fully wired (Phase B): toggling installs the STT / TTS / * Talk surface via the operator's config_patch, routed * through LiteLLM (pieced-stt, pieced-tts-inbound, * pieced-tts-talk). * - channel — messaging integration. * - skill — ClawHub skill install. * * Custom provisioning (Threema): * The `threema` channel sets `requiresSecrets: false` because its * credentials are platform-issued, not customer-entered. Enabling * threema goes through a dedicated endpoint * (/api/tenants/:name/threema) that mints token + HMAC secret from * the central pieced-threema-gateway relay and writes them to OpenBao * at secret/data/tenants//threema-relay before the package is * added to spec.packages. Disabling reverses both steps. The * `customProvisioning` flag here tells the package-card UI to use * that endpoint instead of the standard /secrets+PATCH dance. */ export interface PackageSecretField { key: string; labelKey: string; placeholderKey: string; } export type PackageCategory = "core" | "channel" | "skill"; export interface PackageDef { id: string; name: string; descriptionKey: string; requiresSecrets: boolean; secrets?: PackageSecretField[]; instructionsKey?: string; disclaimerKey?: string; category: PackageCategory; /** * When true, enabling/disabling this package goes through * /api/tenants/:name/ (POST/DELETE) instead of the generic * /secrets+PATCH flow. The handler at that path does platform-side * provisioning (mint credentials, register with sibling services, etc.) * that the customer is not aware of. */ customProvisioning?: boolean; /** * When true, customer-initiated enable requests are routed through * an admin approval queue (skill_activation_requests) instead of * being applied immediately. Platform-side manual work (hardware * provisioning, third-party account setup, DNS, etc.) happens * between request and approval, so we keep the tenant out of the * spec until that work is done and the operator would otherwise * fail to reconcile. * * Platform admins bypass the gate (direct PATCH from /admin still * applies immediately). Disable is always direct — there's no * gate on turning a skill off. * * Orthogonal to `requiresSecrets` and `customProvisioning`. A skill * can have all three: customer provides credentials, the secrets * are stored, the activation request lands in the admin queue, * admin does the manual work, then approves. */ requiresManualSetup?: boolean; /** * Phase 9b: when true, the wizard visually highlights this package * as recommended (a badge + accent border) without pre-selecting * it. Used for the Threema channel — we want customers to choose * Threema as their messaging surface when possible, but the choice * stays opt-in. */ recommended?: boolean; /** * Phase 9b: when true, the onboarding wizard collects the * customer's own user id for this channel (e.g. their Telegram * numeric id, their Threema ID) at request time. The collected * id is forwarded with the tenant request, stored on the row, * and applied on admin approval: * - spec.channelUsers[] gets the id seeded so the * operator's first reconcile already has it * - for Threema specifically, the approve handler additionally * calls the relay's createRoute() so inbound messages from * that id reach the new tenant * Customers can add more ids later via the channel-users page. * Help copy and label come from channelUsers.IdHelp. */ collectsChannelUserId?: boolean; } export const PACKAGE_CATALOG: PackageDef[] = [ // ------------------------------------------------------------------------- // CORE // ------------------------------------------------------------------------- { id: "core-heartbeat", name: "Heartbeat (Proactive Checks)", descriptionKey: "packages.coreHeartbeat.description", requiresSecrets: false, category: "core", }, { id: "core-cron", name: "Scheduled Tasks (Cron)", descriptionKey: "packages.coreCron.description", requiresSecrets: false, category: "core", }, { id: "core-active-memory", name: "Active Memory", descriptionKey: "packages.coreActiveMemory.description", requiresSecrets: false, category: "core", }, { id: "core-voice", name: "Voice Interaction", descriptionKey: "packages.coreVoice.description", requiresSecrets: false, category: "core", }, // ------------------------------------------------------------------------- // CHANNELS // ------------------------------------------------------------------------- { id: "telegram", name: "Telegram", descriptionKey: "packages.telegram.description", requiresSecrets: true, secrets: [ { key: "bot-token", labelKey: "packages.telegram.botTokenLabel", placeholderKey: "packages.telegram.botTokenPlaceholder", }, ], instructionsKey: "packages.telegram.instructions", disclaimerKey: "packages.telegram.disclaimer", category: "channel", collectsChannelUserId: true, }, { id: "discord", name: "Discord", descriptionKey: "packages.discord.description", requiresSecrets: true, secrets: [ { key: "bot-token", labelKey: "packages.discord.botTokenLabel", placeholderKey: "packages.discord.botTokenPlaceholder", }, // app-id was missing from the portal catalog historically while the // operator catalog declared DISCORD_APP_ID as a required env var. // Tenants who enabled Discord ended up with the env var blank // because the secrets POST never wrote an `app-id` key to OpenBao // and the operator's ExternalSecret couldn't populate it. Added // here as part of the Phase A rework to close the alignment gap; // not strictly secret (the application ID is visible in the bot's // profile URL) but stored alongside the bot token for convenience. { key: "app-id", labelKey: "packages.discord.appIdLabel", placeholderKey: "packages.discord.appIdPlaceholder", }, ], instructionsKey: "packages.discord.instructions", disclaimerKey: "packages.discord.disclaimer", category: "channel", collectsChannelUserId: true, }, { id: "threema", name: "Threema", descriptionKey: "packages.threema.description", // No customer-entered secrets. The token + hmac secret are minted // server-side by the relay's /admin/tokens endpoint when the // package is enabled, and stored in OpenBao by the portal. The // `customProvisioning` flag steers the PackageCard UI through the // dedicated /api/tenants/:name/threema endpoint instead. requiresSecrets: false, customProvisioning: true, instructionsKey: "packages.threema.instructions", disclaimerKey: "packages.threema.disclaimer", category: "channel", recommended: true, collectsChannelUserId: true, }, // ------------------------------------------------------------------------- // SKILLS // ------------------------------------------------------------------------- { id: "git-cli", name: "Git CLI", descriptionKey: "packages.gitCli.description", requiresSecrets: false, category: "skill", }, { id: "github", name: "GitHub (gh CLI)", descriptionKey: "packages.github.description", requiresSecrets: true, secrets: [ { key: "token", labelKey: "packages.github.tokenLabel", placeholderKey: "packages.github.tokenPlaceholder", }, ], instructionsKey: "packages.github.instructions", category: "skill", }, { id: "gitea", name: "Gitea", descriptionKey: "packages.gitea.description", requiresSecrets: true, secrets: [ { key: "token", labelKey: "packages.gitea.tokenLabel", placeholderKey: "packages.gitea.tokenPlaceholder", }, ], instructionsKey: "packages.gitea.instructions", category: "skill", }, { id: "whisper-self-hosted", name: "Whisper (Self-Hosted Transcription)", descriptionKey: "packages.whisperSelfHosted.description", requiresSecrets: false, category: "skill", }, { id: "searxng-local-search", name: "Web Search (SearXNG)", descriptionKey: "packages.searxngLocalSearch.description", requiresSecrets: false, category: "skill", }, { id: "gog", name: "Google Workspace (Gog)", descriptionKey: "packages.gog.description", requiresSecrets: true, secrets: [ { key: "client-id", labelKey: "packages.gog.clientIdLabel", placeholderKey: "packages.gog.clientIdPlaceholder", }, { key: "client-secret", labelKey: "packages.gog.clientSecretLabel", placeholderKey: "packages.gog.clientSecretPlaceholder", }, { key: "refresh-token", labelKey: "packages.gog.refreshTokenLabel", placeholderKey: "packages.gog.refreshTokenPlaceholder", }, ], instructionsKey: "packages.gog.instructions", disclaimerKey: "packages.gog.disclaimer", category: "skill", }, { id: "mail", name: "Email (IMAP / SMTP)", descriptionKey: "packages.mail.description", requiresSecrets: true, secrets: [ { key: "imap-host", labelKey: "packages.mail.imapHostLabel", placeholderKey: "packages.mail.imapHostPlaceholder", }, { key: "imap-user", labelKey: "packages.mail.imapUserLabel", placeholderKey: "packages.mail.imapUserPlaceholder", }, { key: "imap-pass", labelKey: "packages.mail.imapPassLabel", placeholderKey: "packages.mail.imapPassPlaceholder", }, { key: "smtp-host", labelKey: "packages.mail.smtpHostLabel", placeholderKey: "packages.mail.smtpHostPlaceholder", }, { key: "smtp-user", labelKey: "packages.mail.smtpUserLabel", placeholderKey: "packages.mail.smtpUserPlaceholder", }, { key: "smtp-pass", labelKey: "packages.mail.smtpPassLabel", placeholderKey: "packages.mail.smtpPassPlaceholder", }, ], instructionsKey: "packages.mail.instructions", disclaimerKey: "packages.mail.disclaimer", category: "skill", }, ]; export function getPackageDef(id: string): PackageDef | undefined { return PACKAGE_CATALOG.find((p) => p.id === id); } /** * IDs of channel-category packages. Derived from the catalog so it * cannot drift from the source of truth (previously hardcoded as * `["telegram", "discord", "email"]` in tenants/[name]/page.tsx — * removed as part of the Phase A package-model rework). * * Consumers: tenant detail page (filter spec.packages to channel set * before rendering the channel-users panel). */ export const CHANNEL_PACKAGE_IDS: string[] = PACKAGE_CATALOG .filter((p) => p.category === "channel") .map((p) => p.id); /** * Default packages selected when the wizard opens a fresh onboarding * request. The three CORE behaviours that make the assistant feel * "smart out of the box": * - heartbeat: proactive checks (otherwise the assistant is purely * reactive). * - cron: scheduled tasks (daily briefings, reminders). * - active-memory: long-term recall of stable preferences and habits. * * Each adds some token cost — active-memory the most (one extra * sub-agent turn per inbound message) — so customers can untoggle any * of them before submitting. * * core-voice is intentionally NOT a default. It is fully wired (Phase B) * and customers can enable it from the wizard, but it incurs separate * audio spend on every inbound voice note (Whisper STT) and every * outbound reply (kani-tts / kokoro-fastapi via LiteLLM). Opt-in keeps * cost predictable for tenants who don't intend to use voice channels. * * Phase 9b revision: nothing is pre-enabled. New tenants start with a * blank slate — the customer opts into exactly what they want. The * Threema channel is flagged `recommended` (see PACKAGE_CATALOG) so * the wizard highlights it, since we want customers to use Threema as * their channel when possible — but it's still opt-in, not auto-on. */ export const DEFAULT_PACKAGE_IDS: string[] = [];