371 lines
13 KiB
TypeScript
371 lines
13 KiB
TypeScript
/**
|
|
* 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/<name>/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/<id> (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[<channel>] 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.<id>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[] = [];
|