/ services / mcp / channelAllowlist.ts
channelAllowlist.ts
 1  /**
 2   * Approved channel plugins allowlist. --channels plugin:name@marketplace
 3   * entries only register if {marketplace, plugin} is on this list. server:
 4   * entries always fail (schema is plugin-only). The
 5   * --dangerously-load-development-channels flag bypasses for both kinds.
 6   * Lives in GrowthBook so it can be updated without a release.
 7   *
 8   * Plugin-level granularity: if a plugin is approved, all its channel
 9   * servers are. Per-server gating was overengineering — a plugin that
10   * sprouts a malicious second server is already compromised, and per-server
11   * entries would break on harmless plugin refactors.
12   *
13   * The allowlist check is a pure {marketplace, plugin} comparison against
14   * the user's typed tag. The gate's separate 'marketplace' step verifies
15   * the tag matches what's actually installed before this check runs.
16   */
17  
18  import { z } from 'zod/v4'
19  import { lazySchema } from '../../utils/lazySchema.js'
20  import { parsePluginIdentifier } from '../../utils/plugins/pluginIdentifier.js'
21  import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics/growthbook.js'
22  
23  export type ChannelAllowlistEntry = {
24    marketplace: string
25    plugin: string
26  }
27  
28  const ChannelAllowlistSchema = lazySchema(() =>
29    z.array(
30      z.object({
31        marketplace: z.string(),
32        plugin: z.string(),
33      }),
34    ),
35  )
36  
37  export function getChannelAllowlist(): ChannelAllowlistEntry[] {
38    const raw = getFeatureValue_CACHED_MAY_BE_STALE<unknown>(
39      'tengu_harbor_ledger',
40      [],
41    )
42    const parsed = ChannelAllowlistSchema().safeParse(raw)
43    return parsed.success ? parsed.data : []
44  }
45  
46  /**
47   * Overall channels on/off. Checked before any per-server gating —
48   * when false, --channels is a no-op and no handlers register.
49   * Default false; GrowthBook 5-min refresh.
50   */
51  export function isChannelsEnabled(): boolean {
52    return getFeatureValue_CACHED_MAY_BE_STALE('tengu_harbor', false)
53  }
54  
55  /**
56   * Pure allowlist check keyed off the connection's pluginSource — for UI
57   * pre-filtering so the IDE only shows "Enable channel?" for servers that will
58   * actually pass the gate. Not a security boundary: channel_enable still runs
59   * the full gate. Matches the allowlist comparison inside gateChannelServer()
60   * but standalone (no session/marketplace coupling — those are tautologies
61   * when the entry is derived from pluginSource).
62   *
63   * Returns false for undefined pluginSource (non-plugin server — can never
64   * match the {marketplace, plugin}-keyed ledger) and for @-less sources
65   * (builtin/inline — same reason).
66   */
67  export function isChannelAllowlisted(
68    pluginSource: string | undefined,
69  ): boolean {
70    if (!pluginSource) return false
71    const { name, marketplace } = parsePluginIdentifier(pluginSource)
72    if (!marketplace) return false
73    return getChannelAllowlist().some(
74      e => e.plugin === name && e.marketplace === marketplace,
75    )
76  }