feat: add Channel System documentation (CN/EN)

This commit is contained in:
程序员阿江(Relakkes) 2026-04-05 09:23:50 +08:00
parent cf55c55d00
commit 4c9a77f903
13 changed files with 1752 additions and 0 deletions

View File

@ -39,6 +39,14 @@ const zhSidebar = [
{ text: '实现原理', link: '/skills/02-implementation' },
],
},
{
text: 'Channel 系统',
collapsed: false,
items: [
{ text: '概览', link: '/channel/' },
{ text: '架构解析', link: '/channel/01-channel-system' },
],
},
{
text: 'Computer Use',
collapsed: false,
@ -96,6 +104,14 @@ const enSidebar = [
{ text: 'Implementation', link: '/en/skills/02-implementation' },
],
},
{
text: 'Channel System',
collapsed: false,
items: [
{ text: 'Overview', link: '/en/channel/' },
{ text: 'Architecture', link: '/en/channel/01-channel-system' },
],
},
{
text: 'Computer Use',
collapsed: false,

View File

@ -0,0 +1,785 @@
# Channel 系统架构解析
> 从源码视角深度剖析 Claude Code 如何通过 IM 平台远程控制 Agent
<p align="center">
<a href="#一、什么是-channel">概念</a> ·
<a href="#二、整体架构">架构</a> ·
<a href="#三、消息协议">协议</a> ·
<a href="#四、六层访问控制">访问控制</a> ·
<a href="#五、权限中继系统">权限中继</a> ·
<a href="#六、ui-组件">UI</a> ·
<a href="#七、插件-channel-架构">插件</a> ·
<a href="#八、安全设计">安全</a> ·
<a href="#九、命令行接口">CLI</a> ·
<a href="#十、特性开关与分析">特性开关</a>
</p>
![Channel System Overview](./images/01-channel-overview.png)
---
## 一、什么是 Channel
Channel 是 Claude Code 的 **IM 集成系统**,它允许用户通过 Telegram、Feishu飞书、Discord、Slack 等即时通讯平台远程控制正在运行的 Claude Code Agent。
### 核心理念
传统的 AI 编程助手只能在终端中交互。Channel 系统打破了这一限制——你可以在手机上通过 Telegram 给 Claude Code 发消息,它会像在终端中一样理解并执行你的请求,并将结果回复到你的聊天窗口。
### Channel 的本质
从技术角度看,一个 Channel 就是一个特殊的 **MCPModel Context ProtocolServer**,它需要:
1. **声明能力**:在 MCP 握手时声明 `experimental['claude/channel']` 能力
2. **推送消息**:通过 `notifications/claude/channel` 通知将 IM 消息推入 Agent 对话
3. **暴露工具**:提供 `reply``react``edit_message` 等 MCP 工具,让 Agent 回复到 IM 平台
```typescript
// Channel 的两种形态
type ChannelEntry =
| { kind: 'plugin'; name: string; marketplace: string; dev?: boolean }
| { kind: 'server'; name: string; dev?: boolean }
```
**plugin 类型**:来自 marketplace 的验证插件(如 `plugin:telegram@anthropic`
**server 类型**:直接指定的 MCP 服务器名称(始终需要 dev 旁路)
---
## 二、整体架构
![Message Flow](./images/02-message-flow.png)
### 消息流转全链路
Channel 系统的消息流转遵循一个清晰的双向路径:
```
┌─────────────────────────────────────────────────────────────┐
│ 入站IM → Agent
│ │
│ Telegram/Feishu/Discord │
│ ↓ │
│ Channel PluginMCP Server
│ ↓ │
│ notifications/claude/channel { content, meta } │
│ ↓ │
│ useManageMCPConnections → registerNotificationHandler │
│ ↓ │
│ wrapChannelMessage() → <channel source="..." user="...">
│ ↓ │
│ enqueue({ priority: 'next', isMeta: true }) │
│ ↓ │
│ SleepTool 每 ~1s 轮询 hasCommandsInQueue() │
│ ↓ │
│ Model 看到 <channel> 标签,理解消息来源 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 出站Agent → IM
│ │
│ Model 决定使用哪个工具回复 │
│ ↓ │
│ callTool() → Channel 的 MCP 工具 │
reply / react / edit_message / download_attachment
│ ↓ │
│ MCP 协议调用 Channel Server │
│ ↓ │
│ Channel Server 发送消息到 IM 平台 │
│ ↓ │
│ Telegram/Feishu/Discord 用户收到回复 │
└─────────────────────────────────────────────────────────────┘
```
### 核心组件关系
| 组件 | 文件 | 职责 |
|------|------|------|
| **Channel Gate** | `channelNotification.ts` | 六层访问控制门 |
| **Message Wrapper** | `channelNotification.ts` | XML 消息封装 |
| **Permission Relay** | `channelPermissions.ts` | 远程权限审批 |
| **Allowlist** | `channelAllowlist.ts` | GrowthBook 白名单管理 |
| **MCP Connection** | `useManageMCPConnections.ts` | 连接管理与通知注册 |
| **Channel Message UI** | `UserChannelMessage.tsx` | 终端渲染 Channel 消息 |
| **Dev Dialog** | `DevChannelsDialog.tsx` | 开发模式确认对话框 |
| **Channels Notice** | `ChannelsNotice.tsx` | 启动时 Channel 状态通知 |
| **Plugin Integration** | `mcpPluginIntegration.ts` | 插件作用域命名 |
| **State** | `bootstrap/state.ts` | 全局 Channel 白名单状态 |
---
## 三、消息协议
### 3.1 入站通知 Schema
Channel Server 推送到 Claude Code 的通知格式:
```typescript
// channelNotification.ts
const ChannelMessageNotificationSchema = z.object({
method: z.literal('notifications/claude/channel'),
params: z.object({
content: z.string(),
// 透传元数据 — thread_id、user 等
// 渲染为 <channel> 标签的属性
meta: z.record(z.string(), z.string()).optional(),
}),
})
```
### 3.2 XML 封装
收到通知后,系统将其封装为 `<channel>` XML 标签:
```typescript
// channelNotification.ts:106-116
function wrapChannelMessage(
serverName: string,
content: string,
meta?: Record<string, string>,
): string {
const attrs = Object.entries(meta ?? {})
.filter(([k]) => SAFE_META_KEY.test(k)) // 防 XML 注入
.map(([k, v]) => ` ${k}="${escapeXmlAttr(v)}"`)
.join('')
return `<channel source="${escapeXmlAttr(serverName)}"${attrs}>
${content}
</channel>`
}
```
**封装结果示例**
```xml
<channel source="plugin:telegram:tg" user="alice" chat_id="123456">
帮我看看 main.ts 有什么问题
</channel>
```
Model 看到这个标签后,就知道消息来自 Telegram 的用户 alice并会使用 Telegram 的 `reply` 工具回复。
### 3.3 安全的元数据过滤
Meta 键名会成为 XML 属性名,恶意构造的键(如 `x="" injected="y`)可能导致 XML 注入。系统使用严格的正则过滤:
```typescript
// 只允许纯标识符格式的键名
const SAFE_META_KEY = /^[a-zA-Z_][a-zA-Z0-9_]*$/
```
实际场景中Channel 服务器只发送 `chat_id``user``thread_ts``message_id` 这类安全键名。
### 3.4 消息入队
封装后的消息通过 enqueue 推入消息队列:
```typescript
enqueue({
mode: 'prompt',
value: wrapChannelMessage(serverName, content, meta),
priority: 'next', // 高优先级
isMeta: true, // 元数据消息
origin: { kind: 'channel', server: serverName },
skipSlashCommands: true // 不解释为斜杠命令
})
```
SleepTool 每约 1 秒轮询一次 `hasCommandsInQueue()`,发现新消息后唤醒 Agent。
---
## 四、六层访问控制
![Access Control](./images/03-access-control.png)
Channel 系统采用**六层递进式访问控制**,每一层都可以独立阻断 Channel 注册。这是整个系统安全性的基石。
### Gate 函数签名
```typescript
// channelNotification.ts:191-316
function gateChannelServer(
serverName: string,
capabilities: ServerCapabilities | undefined,
pluginSource: string | undefined,
): ChannelGateResult // { action: 'register' } | { action: 'skip', kind, reason }
```
### 4.1 第一层能力声明Capability
```typescript
if (!capabilities?.experimental?.['claude/channel']) {
return { action: 'skip', kind: 'capability',
reason: 'server did not declare claude/channel capability' }
}
```
MCP Server 必须在握手时声明 `experimental['claude/channel']: {}` 能力。这是 MCP 的"存在信号"惯例(类似 `tools: {}`),将普通 MCP Server 与 Channel Server 区分开来。
### 4.2 第二层运行时开关Runtime Gate
```typescript
if (!isChannelsEnabled()) {
return { action: 'skip', kind: 'disabled',
reason: 'channels feature is not currently available' }
}
```
`isChannelsEnabled()` 检查 GrowthBook 特性开关 `tengu_harbor`(默认 false5 分钟刷新)。这是全局的"紧急制动"——关闭此开关立即禁用所有 Channel不需要发版。
### 4.3 第三层OAuth 认证Auth
```typescript
if (!getClaudeAIOAuthTokens()?.accessToken) {
return { action: 'skip', kind: 'auth',
reason: 'channels requires claude.ai authentication (run /login)' }
}
```
Channel 仅限 OAuth 认证用户。API Key 用户被阻止,因为 Console 端目前还没有 `channelsEnabled` 的管理界面。
### 4.4 第四层组织策略Policy
```typescript
const sub = getSubscriptionType()
const managed = sub === 'team' || sub === 'enterprise'
const policy = managed ? getSettingsForSource('policySettings') : undefined
if (managed && policy?.channelsEnabled !== true) {
return { action: 'skip', kind: 'policy',
reason: 'channels not enabled by org policy' }
}
```
Teams/Enterprise 组织必须在托管设置中显式启用 `channelsEnabled: true`。默认关闭——即使没有配置任何策略的团队组织也不会回退到非托管路径。
### 4.5 第五层会话白名单Session
```typescript
const entry = findChannelEntry(serverName, getAllowedChannels())
if (!entry) {
return { action: 'skip', kind: 'session',
reason: `server ${serverName} not in --channels list for this session` }
}
```
MCP Server 必须在本次会话的 `--channels` 参数列表中。即使一个受信任的 Server 动态添加了 `claude/channel` 能力,也无法绕过——必须用户在启动时显式列出。
### 4.6 第六层Marketplace 验证 + 白名单Allowlist
对于 plugin 类型的 Channel
```typescript
// Marketplace 验证:确保安装的插件来自声称的市场
const actual = pluginSource
? parsePluginIdentifier(pluginSource).marketplace
: undefined
if (actual !== entry.marketplace) {
return { action: 'skip', kind: 'marketplace',
reason: `tag mismatch: asked for @${entry.marketplace}, installed from ${actual}` }
}
// 白名单检查:插件必须在 GrowthBook 审批列表中
if (!entry.dev) {
const { entries } = getEffectiveChannelAllowlist(sub, policy?.allowedChannelPlugins)
if (!entries.some(e => e.plugin === entry.name && e.marketplace === entry.marketplace)) {
return { action: 'skip', kind: 'allowlist', reason: 'not on approved list' }
}
}
```
**双重验证**:先验证 `--channels plugin:slack@anthropic` 中的 `@anthropic` 标签与实际安装的插件来源是否匹配(防止 `slack@evil` 冒充 `slack@anthropic`),再检查是否在审批白名单中。
**白名单来源优先级**Team/Enterprise 组织可以设置 `allowedChannelPlugins`,此时它替代 GrowthBook ledger 白名单(管理员拥有信任决策权)。
对于 server 类型白名单始终失败schema 是 `{marketplace, plugin}` 格式server 类型无法匹配),除非使用 `--dangerously-load-development-channels`(设置 `entry.dev = true` 旁路白名单)。
### Gate 结果类型
```typescript
type ChannelGateResult =
| { action: 'register' } // 通过所有检查,注册通知处理器
| { action: 'skip'; kind: string; reason: string } // 某层拦截
// kind 枚举capability | disabled | auth | policy | session | marketplace | allowlist
```
---
## 五、权限中继系统
![Permission Relay](./images/04-permission-relay.png)
### 5.1 为什么需要权限中继
当 Claude Code 需要执行敏感操作(如运行 Bash 命令),会弹出权限确认对话框。但如果用户通过 Telegram 远程控制 Agent他看不到本地终端的对话框。
权限中继系统解决了这个问题:**将权限提示转发到 IM 平台,让用户在手机上也能审批或拒绝操作**。
### 5.2 出站CC → Channel权限请求
当 Agent 触发权限对话框且 Channel 声明了 `experimental['claude/channel/permission']` 能力时:
```typescript
// 通知 Schema
const CHANNEL_PERMISSION_REQUEST_METHOD =
'notifications/claude/channel/permission_request'
type ChannelPermissionRequestParams = {
request_id: string // 5 字母标识符(如 "tbxkq"
tool_name: string // 工具名(如 "Bash"
description: string // 人类可读描述
input_preview: string // JSON 输入预览,截断到 200 字符
}
```
Channel Server 收到后按各平台格式化Telegram markdown、Discord embed 等)发送给用户。
### 5.3 Short Request ID 生成
5 字母标识符的设计充满巧思:
```typescript
// channelPermissions.ts:140-152
function shortRequestId(toolUseID: string): string {
let candidate = hashToId(toolUseID)
for (let salt = 0; salt < 10; salt++) {
if (!ID_AVOID_SUBSTRINGS.some(bad => candidate.includes(bad))) {
return candidate
}
candidate = hashToId(`${toolUseID}:${salt}`)
}
return candidate
}
```
**设计决策**
- **25 字母表**a-z 去掉 `l`(与 1/I 混淆25^5 约 980 万种组合
- **FNV-1a 哈希**:不是加密级别,但足够稳定且快速
- **脏话过滤**5 个随机字母可能拼出不雅词汇(如发短信给老板的场景),内置屏蔽词表
- **纯字母**:手机用户不需要切换键盘模式(十六进制需要在字母和数字间切换)
- **大小写不敏感**:适配手机自动更正
### 5.4 入站Channel → CC权限响应
用户在 IM 中回复格式:`yes tbxkq``no tbxkq`
```typescript
// 服务端解析正则
const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i
// 结构化通知(服务端解析后发出,而非 CC 端正则匹配文本)
const ChannelPermissionNotificationSchema = z.object({
method: z.literal('notifications/claude/channel/permission'),
params: z.object({
request_id: z.string(),
behavior: z.enum(['allow', 'deny']),
}),
})
```
**关键设计**Channel Server 负责解析用户回复并发出结构化事件CC 端不做文本正则匹配。这意味着普通聊天中的文字永远不会意外触发权限审批。
### 5.5 多源竞争
权限响应来自四个来源,先到先得:
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 本地终端 │ │ Bridge │ │ Channels │ │ Hooks │
│ Local UI │ │ 远程控制 │ │ Telegram etc │ │ Permission │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │ │
└───────────────────┴───────────────────┴──────────────────┘
claim() — 先到先得
┌─────┴─────┐
│ resolve │
│ allow/deny│
└───────────┘
```
`createChannelPermissionCallbacks()` 使用闭包维护 pending Map不在模块级别也不在 AppState 中——函数引用在状态中会导致序列化问题),构造一次后存入 AppState。
### 5.6 过滤权限中继客户端
```typescript
// channelPermissions.ts:177-194
function filterPermissionRelayClients(clients, isInAllowlist) {
return clients.filter(c =>
c.type === 'connected' &&
isInAllowlist(c.name) &&
c.capabilities?.experimental?.['claude/channel'] !== undefined &&
c.capabilities?.experimental?.['claude/channel/permission'] !== undefined
)
}
```
**三个条件全部必须**:已连接 + 在白名单中 + 同时声明了两个能力(`claude/channel``claude/channel/permission`)。第二个能力是显式 opt-in——只推消息的 Channel 永远不会意外成为权限审批入口。
---
## 六、UI 组件
### 6.1 终端消息渲染UserChannelMessage
```typescript
// UserChannelMessage.tsx
// 解析 <channel> XML 标签并在终端中渲染
const CHANNEL_RE = new RegExp(
`<${CHANNEL_TAG}\\s+source="([^"]+)"([^>]*)>\\n?([\\s\\S]*?)\\n?</${CHANNEL_TAG}>`
)
// 插件服务器名称显示plugin:slack-channel:slack → slack
function displayServerName(name: string): string {
const i = name.lastIndexOf(':')
return i === -1 ? name : name.slice(i + 1)
}
const TRUNCATE_AT = 60 // 消息体截断长度
```
**渲染效果**
```
◁ tg · alice: 帮我看看 main.ts 有什么问题
```
其中 `◁` 是 Channel 箭头符号(`CHANNEL_ARROW`),显示服务器叶子名称和可选的用户名。
### 6.2 状态通知ChannelsNotice
启动时显示 `--channels` 条目的状态,报告阻断原因:
| 阻断类型 | 含义 |
|----------|------|
| `disabled` | Channel 功能未启用tengu_harbor 关闭) |
| `noAuth` | 未通过 OAuth 认证 |
| `policyBlocked` | 组织策略未启用 channels |
| `unmatched` | 未在 `--channels` 列表中匹配到 |
### 6.3 开发者确认对话框DevChannelsDialog
使用 `--dangerously-load-development-channels` 时显示的警告对话框:
```
┌─ WARNING: Loading development channels ──────────────────┐
│ │
│ --dangerously-load-development-channels is for local │
│ channel development only. Do not use this option to │
│ run channels you have downloaded off the internet. │
│ │
│ Please use --channels to run a list of approved channels│
│ │
│ Channels: plugin:my-channel@local
│ │
│ > I am using this for local development │
│ Exit │
└──────────────────────────────────────────────────────────┘
```
选择 "Exit" 会调用 `gracefulShutdownSync(1)` 立即退出。
---
## 七、插件 Channel 架构
### 7.1 Plugin Manifest 中的 Channel 声明
插件通过 `plugin.json` 中的 `channels` 数组声明 Channel
```typescript
// schemas.ts:670-703
const PluginManifestChannelsSchema = z.object({
channels: z.array(z.object({
server: z.string().min(1), // MCP 服务器名,必须匹配 mcpServers 中的 key
displayName: z.string().optional(), // 配置对话框标题(如 "Telegram"
userConfig: z.record( // 安装时提示用户配置的字段
z.string(),
PluginUserConfigOptionSchema()
).optional(),
}).strict()),
})
```
**示例 plugin.json**
```json
{
"name": "telegram",
"version": "1.0.0",
"mcpServers": {
"tg": {
"command": "node",
"args": ["./server.js"],
"env": {
"BOT_TOKEN": "${user_config.bot_token}",
"OWNER_ID": "${user_config.owner_id}"
}
}
},
"channels": [
{
"server": "tg",
"displayName": "Telegram",
"userConfig": {
"bot_token": {
"type": "string",
"description": "Telegram Bot API Token",
"required": true,
"secret": true
},
"owner_id": {
"type": "string",
"description": "Your Telegram User ID",
"required": true
}
}
}
]
}
```
### 7.2 配置流PluginOptionsFlow
启用插件后,如果 Channel 有未配置的 `userConfig` 字段:
1. `getUnconfiguredChannels()` 检测未满足验证的字段
2. `PluginOptionsFlow` 组件逐个提示用户输入
3. 敏感值(如 bot_token存入 Keychain
4. 普通值存入 `~/.claude/plugins/options/{pluginId}.json`
```typescript
// mcpPluginIntegration.ts:290-318
function getUnconfiguredChannels(plugin: LoadedPlugin): UnconfiguredChannel[] {
const channels = plugin.manifest.channels
if (!channels || channels.length === 0) return []
const unconfigured: UnconfiguredChannel[] = []
for (const channel of channels) {
if (!channel.userConfig) continue
const saved = loadMcpServerUserConfig(pluginId, channel.server) ?? {}
const validation = validateUserConfig(saved, channel.userConfig)
if (!validation.valid) {
unconfigured.push({
server: channel.server,
displayName: channel.displayName ?? channel.server,
configSchema: channel.userConfig,
})
}
}
return unconfigured
}
```
### 7.3 作用域命名Scoped Naming
插件提供的 MCP Server 会被添加作用域前缀以避免冲突:
```typescript
// mcpPluginIntegration.ts:341-360
function addPluginScopeToServers(
servers: Record<string, McpServerConfig>,
pluginName: string,
pluginSource: string, // e.g., "telegram@anthropic"
): Record<string, ScopedMcpServerConfig> {
const scopedServers = {}
for (const [name, config] of Object.entries(servers)) {
const scopedName = `plugin:${pluginName}:${name}`
scopedServers[scopedName] = {
...config,
scope: 'dynamic',
pluginSource,
}
}
return scopedServers
}
```
**命名转换**
- 输入:`{ "tg": { ... } }` from `telegram@anthropic`
- 输出:`{ "plugin:telegram:tg": { scope: 'dynamic', pluginSource: 'telegram@anthropic', ... } }`
`pluginSource` 被保存在配置上,后续 Channel Gate 的 marketplace 验证步骤会用到它。
### 7.4 白名单有效来源
```typescript
// channelNotification.ts:127-138
function getEffectiveChannelAllowlist(sub, orgList) {
// Team/Enterprise 组织自定义白名单 → 替代 GrowthBook ledger
if ((sub === 'team' || sub === 'enterprise') && orgList) {
return { entries: orgList, source: 'org' }
}
// 默认使用 GrowthBook ledger
return { entries: getChannelAllowlist(), source: 'ledger' }
}
```
组织管理员可以通过 `allowedChannelPlugins` 完全控制哪些 Channel 插件被信任,而不依赖全局 ledger。
---
## 八、安全设计
### 8.1 XML 注入防护
Channel 消息中的元数据会成为 XML 属性。两道防线:
1. **键名过滤**`SAFE_META_KEY = /^[a-zA-Z_][a-zA-Z0-9_]*$/` 只允许纯标识符
2. **值转义**`escapeXmlAttr()` 对属性值进行 XML 转义
### 8.2 Marketplace 验证
`--channels plugin:slack@anthropic` 只是用户的"意图声明"。运行时名称 `plugin:slack:X` 可能来自 `slack@anthropic``slack@evil`。Gate 验证两者必须匹配:
```typescript
const actual = pluginSource
? parsePluginIdentifier(pluginSource).marketplace
: undefined
if (actual !== entry.marketplace) {
return { action: 'skip', kind: 'marketplace', reason: '...' }
}
```
### 8.3 权限中继的信任边界
来自代码注释中 Kenneth 的分析PR 讨论 #2956440848
> "Claude 会自我审批吗?" 答:审批方是通过 Channel 的人类,不是 Claude。但信任边界不是终端——而是白名单tengu_harbor_ledger。一个被妥协的 Channel Server 可以在人类没看到提示的情况下伪造 "yes \<id\>"。这是被接受的风险:一个已妥协的 Channel 本来就有无限的对话注入轮次(可以社会工程攻击、等待 acceptEdits 模式等);注入后自动审批更快,但能力上并不更强。权限对话框减缓了已妥协 Channel 的攻击速度,但不能完全阻止。
### 8.4 skipSlashCommands
Channel 消息入队时设置 `skipSlashCommands: true`,确保 IM 用户发送的 `/help` 等文本不会被解释为 Claude Code 的斜杠命令。
### 8.5 dev 旁路的粒度
`--dangerously-load-development-channels``dev` 标记是**每个条目**级别的,不是全局的。接受开发对话框后,只有显式标记为 dev 的条目才旁路白名单,`--channels` 中的正常条目仍然需要通过完整的白名单检查。
---
## 九、命令行接口
### 9.1 启动参数
```bash
# 使用已审批的 Channel 插件
claude --channels plugin:telegram@anthropic plugin:feishu@anthropic
# 本地开发模式(旁路白名单)
claude --dangerously-load-development-channels plugin:my-channel@local
# 两者可以同时使用
claude --channels plugin:telegram@anthropic \
--dangerously-load-development-channels plugin:dev-channel@local
```
### 9.2 参数解析
```typescript
// main.tsx
const parseChannelEntries = (raw: string[], flag: string): ChannelEntry[] => {
// 解析 "plugin:slack@anthropic"、"server:slack" 等格式
// 验证格式正确性
// 返回 ChannelEntry 数组
}
// 存入全局状态
setAllowedChannels(channelEntries)
```
### 9.3 特性门控
这两个 CLI 选项只在特性开关启用时可用:
```typescript
// main.tsx:3850-3852
if (feature('KAIROS') || feature('KAIROS_CHANNELS')) {
program.addOption(new Option('--channels <servers...>', '...').hideHelp())
program.addOption(new Option('--dangerously-load-development-channels <servers...>', '...').hideHelp())
}
```
`hideHelp()` 表示这些选项不会出现在 `--help` 输出中——Channel 功能目前处于隐藏特性阶段。
---
## 十、特性开关与分析
### 10.1 特性开关
| 开关 | 来源 | 用途 |
|------|------|------|
| `KAIROS` / `KAIROS_CHANNELS` | 编译时 | 控制 CLI 参数注册和代码路径 |
| `tengu_harbor` | GrowthBook 运行时 | Channel 功能总开关(默认 false |
| `tengu_harbor_ledger` | GrowthBook 运行时 | 审批插件白名单 |
| `tengu_harbor_permissions` | GrowthBook 运行时 | 权限中继功能开关 |
### 10.2 分析事件
```typescript
// Channel Gate 结果
tengu_mcp_channel_gate: {
gate_kind: 'disabled' | 'auth' | 'policy' | 'session' | 'marketplace' | 'allowlist'
plugin: string
is_dev: boolean
}
// Channel 消息
tengu_mcp_channel_message: {
content_length: number
meta_key_count: number
entry_kind: 'plugin' | 'server'
is_dev: boolean
plugin: string
}
// 启动标志
tengu_mcp_channel_flags: {
channels_count: number
dev_count: number
plugins: string[]
dev_plugins: string[]
}
```
---
## 十一、总结
Channel 系统是 Claude Code 的 **IM 集成框架**,它的设计体现了几个核心原则:
### 1. 安全优先
六层访问控制门确保只有经过审批、用户明确选择的 Channel 才能推送消息。从编译时特性开关到运行时白名单,每一层都可以独立中断。
### 2. 协议驱动
Channel 不是特殊代码——它们就是普通的 MCP Server只是多了一个通知协议。这意味着任何能实现 MCP 的语言都可以编写 Channel 插件。
### 3. 松耦合
Channel 失败不会阻断本地工作流。权限中继是多源竞争机制,任一来源的响应都有效。
### 4. 渐进式信任
从全局开关 → 认证 → 组织策略 → 会话白名单 → Marketplace 验证 → 白名单,信任级别逐级递增,每一步都有明确的安全目的。
### 5. 插件友好
通过 `plugin.json` 的声明式 Channel 配置、自动的用户配置提示流和作用域命名,第三方开发者可以轻松构建自己的 Channel 插件。
### 源码文件索引
| 文件 | 行数 | 职责 |
|------|------|------|
| `src/services/mcp/channelNotification.ts` | ~320 | 门控、消息封装、白名单集成 |
| `src/services/mcp/channelPermissions.ts` | ~240 | 权限中继、请求 ID 生成 |
| `src/services/mcp/channelAllowlist.ts` | ~80 | GrowthBook 白名单查询 |
| `src/services/mcp/useManageMCPConnections.ts` | - | 连接管理、通知处理器注册 |
| `src/components/messages/UserChannelMessage.tsx` | ~140 | 终端渲染 Channel 消息 |
| `src/components/DevChannelsDialog.tsx` | ~105 | 开发模式确认对话框 |
| `src/components/LogoV2/ChannelsNotice.tsx` | - | 启动时状态通知 |
| `src/utils/plugins/mcpPluginIntegration.ts` | - | 插件 MCP 集成、作用域命名 |
| `src/utils/plugins/schemas.ts` | ~700 | 插件清单 Schema含 Channel 声明) |
| `src/bootstrap/state.ts` | - | 全局 Channel 白名单状态 |
| `src/main.tsx` | ~3850 | CLI 参数注册和解析 |

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

83
docs/channel/index.md Normal file
View File

@ -0,0 +1,83 @@
# Claude Code Channel 系统文档
> 通过 IM 平台远程控制 Claude Code Agent 的完整技术解析
---
## 文档目录
### [01-channel-system.md](./01-channel-system.md) — Channel 系统架构解析
从源码视角深度剖析 Claude Code Channel 系统的设计与实现,涵盖:
- **什么是 Channel**IM 集成的核心概念、MCP 协议基础
- **整体架构**:消息流转全链路、组件关系
- **消息协议**入站通知、XML 封装、出站工具调用
- **六层访问控制**:能力声明 → 运行时开关 → OAuth 认证 → 组织策略 → 会话白名单 → 插件审批
- **权限中继系统**远程审批工具执行、5 字母请求 ID、多源竞争
- **插件架构**Channel 插件清单声明、用户配置流、作用域命名
- **UI 组件**:终端消息渲染、状态通知、开发者警告对话框
- **安全设计**:防 XML 注入、marketplace 验证、信任边界分析
**适合人群**:想了解 AI Agent IM 集成架构的开发者、架构师、插件作者
---
## 配图说明
所有配图采用深色背景(#1a1a2e+ Anthropic 品牌橙铜色(#D97757)风格。
| 图片 | 说明 | 所属文档 |
|------|------|----------|
| `01-channel-overview.png` | Channel 系统架构总览 — 组件关系全景 | 架构解析 |
| `02-message-flow.png` | 消息流转全链路 — IM → Agent → IM | 架构解析 |
| `03-access-control.png` | 六层访问控制 — 层层递进的安全门 | 架构解析 |
| `04-permission-relay.png` | 权限中继系统 — 远程审批流程 | 架构解析 |
---
## 快速开始
### 用户
1. 阅读 [Channel 系统架构解析](./01-channel-system.md)
2. 了解如何通过 `--channels` 启动 IM 集成
3. 理解不同平台Telegram、Feishu、Discord的接入方式
### 插件开发者
1. 阅读架构解析中的 [插件架构](./01-channel-system.md#七、插件-channel-架构) 章节
2. 了解 `plugin.json` 中 Channel 声明格式
3. 实现 MCP Server 的 `notifications/claude/channel` 协议
4. 查看源码位置:
- `src/services/mcp/channelNotification.ts` — 核心门控与消息封装
- `src/services/mcp/channelPermissions.ts` — 权限中继系统
- `src/services/mcp/channelAllowlist.ts` — 白名单管理
- `src/utils/plugins/mcpPluginIntegration.ts` — 插件 MCP 集成
- `src/utils/plugins/schemas.ts` — 插件清单 Channel 声明 Schema
---
## 核心概念速查
| 概念 | 说明 |
|------|------|
| **Channel** | 一个声明了 `claude/channel` 能力的 MCP Server可推送 IM 消息到 Agent |
| **Channel Entry** | `--channels` 参数解析后的条目,分 plugin 和 server 两种 |
| **Channel Gate** | 六层访问控制门,决定是否注册通知处理器 |
| **Permission Relay** | 将工具执行审批提示转发到 IM 平台的机制 |
| **Channel Plugin** | 在 `plugin.json` 中声明 `channels` 字段的插件 |
| **Scoped Name** | 插件服务器的作用域名称,格式 `plugin:{pluginName}:{serverName}` |
| **Short Request ID** | 5 字母权限请求标识符,基于 FNV-1a 哈希生成 |
| **Channel Tag** | `<channel>` XML 标签,封装来自 IM 的消息内容和元数据 |
| **Dev Channels** | 通过 `--dangerously-load-development-channels` 加载的本地开发频道 |
| **tengu_harbor** | GrowthBook 运行时特性开关,控制 Channel 功能总开关 |
---
## 相关资源
- [Claude Code Haha 主页](/)
- [Agent 框架解析](/agent/03-agent-framework)
- [Skills 系统文档](/skills/01-usage-guide)
- [GitHub Issues](https://github.com/NanmiCoder/cc-haha/issues)

View File

@ -0,0 +1,785 @@
# Channel System Architecture
> A deep dive into how Claude Code enables remote Agent control via IM platforms
<p align="center">
<a href="#_1-what-is-a-channel">Concepts</a> ·
<a href="#_2-architecture-overview">Architecture</a> ·
<a href="#_3-message-protocol">Protocol</a> ·
<a href="#_4-six-layer-access-control">Access Control</a> ·
<a href="#_5-permission-relay-system">Permission Relay</a> ·
<a href="#_6-ui-components">UI</a> ·
<a href="#_7-plugin-channel-architecture">Plugins</a> ·
<a href="#_8-security-design">Security</a> ·
<a href="#_9-command-line-interface">CLI</a> ·
<a href="#_10-feature-flags-and-analytics">Feature Flags</a>
</p>
![Channel System Overview](./images/01-channel-overview.png)
---
## 1. What is a Channel
A Channel is Claude Code's **IM integration system** that allows users to remotely control a running Claude Code Agent through instant messaging platforms such as Telegram, Feishu (Lark), Discord, and Slack.
### Core Idea
Traditional AI coding assistants can only interact through the terminal. The Channel system breaks this limitation — you can send messages to Claude Code from your phone via Telegram, and it will understand and execute your requests just as it would in the terminal, replying directly to your chat window.
### The Essence of a Channel
From a technical perspective, a Channel is simply a special **MCP (Model Context Protocol) Server** that must:
1. **Declare capability**: Announce `experimental['claude/channel']` during MCP handshake
2. **Push messages**: Send inbound messages via `notifications/claude/channel` notifications
3. **Expose tools**: Provide MCP tools like `reply`, `react`, `edit_message` for the Agent to respond through
```typescript
// Two forms of Channel entries
type ChannelEntry =
| { kind: 'plugin'; name: string; marketplace: string; dev?: boolean }
| { kind: 'server'; name: string; dev?: boolean }
```
**Plugin kind**: Verified plugins from a marketplace (e.g., `plugin:telegram@anthropic`)
**Server kind**: Directly specified MCP server names (always requires dev bypass)
---
## 2. Architecture Overview
![Message Flow](./images/02-message-flow.png)
### End-to-End Message Flow
The Channel system follows a clear bidirectional message path:
```
┌─────────────────────────────────────────────────────────────┐
│ Inbound (IM → Agent) │
│ │
│ Telegram/Feishu/Discord │
│ ↓ │
│ Channel Plugin (MCP Server) │
│ ↓ │
│ notifications/claude/channel { content, meta } │
│ ↓ │
│ useManageMCPConnections → registerNotificationHandler │
│ ↓ │
│ wrapChannelMessage() → <channel source="..." user="...">
│ ↓ │
│ enqueue({ priority: 'next', isMeta: true }) │
│ ↓ │
│ SleepTool polls hasCommandsInQueue() every ~1s │
│ ↓ │
│ Model sees <channel> tag, understands message source │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Outbound (Agent → IM) │
│ │
│ Model decides which tool to use for reply │
│ ↓ │
│ callTool() → Channel's MCP tools │
│ (reply / react / edit_message / download_attachment) │
│ ↓ │
│ MCP protocol calls Channel Server │
│ ↓ │
│ Channel Server sends message to IM platform │
│ ↓ │
│ Telegram/Feishu/Discord user receives reply │
└─────────────────────────────────────────────────────────────┘
```
### Core Component Map
| Component | File | Responsibility |
|-----------|------|---------------|
| **Channel Gate** | `channelNotification.ts` | Six-layer access control |
| **Message Wrapper** | `channelNotification.ts` | XML message wrapping |
| **Permission Relay** | `channelPermissions.ts` | Remote permission approval |
| **Allowlist** | `channelAllowlist.ts` | GrowthBook allowlist management |
| **MCP Connection** | `useManageMCPConnections.ts` | Connection mgmt & notification registration |
| **Channel Message UI** | `UserChannelMessage.tsx` | Terminal rendering of channel messages |
| **Dev Dialog** | `DevChannelsDialog.tsx` | Development mode confirmation dialog |
| **Channels Notice** | `ChannelsNotice.tsx` | Startup channel status notifications |
| **Plugin Integration** | `mcpPluginIntegration.ts` | Plugin scoped naming |
| **State** | `bootstrap/state.ts` | Global channel allowlist state |
---
## 3. Message Protocol
### 3.1 Inbound Notification Schema
The notification format Channel Servers push to Claude Code:
```typescript
// channelNotification.ts
const ChannelMessageNotificationSchema = z.object({
method: z.literal('notifications/claude/channel'),
params: z.object({
content: z.string(),
// Opaque passthrough — thread_id, user, etc.
// Rendered as attributes on the <channel> tag
meta: z.record(z.string(), z.string()).optional(),
}),
})
```
### 3.2 XML Wrapping
After receiving the notification, the system wraps it in a `<channel>` XML tag:
```typescript
// channelNotification.ts:106-116
function wrapChannelMessage(
serverName: string,
content: string,
meta?: Record<string, string>,
): string {
const attrs = Object.entries(meta ?? {})
.filter(([k]) => SAFE_META_KEY.test(k)) // Prevent XML injection
.map(([k, v]) => ` ${k}="${escapeXmlAttr(v)}"`)
.join('')
return `<channel source="${escapeXmlAttr(serverName)}"${attrs}>
${content}
</channel>`
}
```
**Example output**:
```xml
<channel source="plugin:telegram:tg" user="alice" chat_id="123456">
Can you check what's wrong with main.ts?
</channel>
```
When the model sees this tag, it knows the message came from Telegram user "alice" and will use Telegram's `reply` tool to respond.
### 3.3 Safe Metadata Filtering
Meta keys become XML attribute names. A crafted key like `x="" injected="y` could break out of the attribute structure. The system uses strict regex filtering:
```typescript
// Only allow plain identifier-format key names
const SAFE_META_KEY = /^[a-zA-Z_][a-zA-Z0-9_]*$/
```
In practice, channel servers only send safe keys like `chat_id`, `user`, `thread_ts`, `message_id`.
### 3.4 Message Enqueuing
Wrapped messages are pushed into the message queue:
```typescript
enqueue({
mode: 'prompt',
value: wrapChannelMessage(serverName, content, meta),
priority: 'next', // High priority
isMeta: true, // Metadata message
origin: { kind: 'channel', server: serverName },
skipSlashCommands: true // Don't interpret as slash commands
})
```
SleepTool polls `hasCommandsInQueue()` every ~1 second, waking the Agent when new messages arrive.
---
## 4. Six-Layer Access Control
![Access Control](./images/03-access-control.png)
The Channel system employs **six progressive access control layers**, each capable of independently blocking Channel registration. This is the cornerstone of the system's security.
### Gate Function Signature
```typescript
// channelNotification.ts:191-316
function gateChannelServer(
serverName: string,
capabilities: ServerCapabilities | undefined,
pluginSource: string | undefined,
): ChannelGateResult // { action: 'register' } | { action: 'skip', kind, reason }
```
### 4.1 Layer 1: Capability Declaration
```typescript
if (!capabilities?.experimental?.['claude/channel']) {
return { action: 'skip', kind: 'capability',
reason: 'server did not declare claude/channel capability' }
}
```
The MCP Server must declare `experimental['claude/channel']: {}` during handshake. This is MCP's "presence signal" idiom (similar to `tools: {}`), separating Channel Servers from ordinary MCP Servers.
### 4.2 Layer 2: Runtime Gate
```typescript
if (!isChannelsEnabled()) {
return { action: 'skip', kind: 'disabled',
reason: 'channels feature is not currently available' }
}
```
`isChannelsEnabled()` checks GrowthBook feature flag `tengu_harbor` (default false, 5-minute refresh). This is the global "emergency brake" — flipping this switch immediately disables all Channels without a release.
### 4.3 Layer 3: OAuth Authentication
```typescript
if (!getClaudeAIOAuthTokens()?.accessToken) {
return { action: 'skip', kind: 'auth',
reason: 'channels requires claude.ai authentication (run /login)' }
}
```
Channels are restricted to OAuth-authenticated users. API key users are blocked because Console doesn't have a `channelsEnabled` admin surface yet.
### 4.4 Layer 4: Organization Policy
```typescript
const sub = getSubscriptionType()
const managed = sub === 'team' || sub === 'enterprise'
const policy = managed ? getSettingsForSource('policySettings') : undefined
if (managed && policy?.channelsEnabled !== true) {
return { action: 'skip', kind: 'policy',
reason: 'channels not enabled by org policy' }
}
```
Teams/Enterprise organizations must explicitly enable `channelsEnabled: true` in managed settings. Default is OFF — even a team org with zero configured policy keys is still considered managed and does not fall through to the unmanaged path.
### 4.5 Layer 5: Session Allowlist
```typescript
const entry = findChannelEntry(serverName, getAllowedChannels())
if (!entry) {
return { action: 'skip', kind: 'session',
reason: `server ${serverName} not in --channels list for this session` }
}
```
The MCP Server must be in the current session's `--channels` parameter list. Even if a trusted server dynamically adds the `claude/channel` capability, it cannot bypass this — the user must explicitly list it at startup.
### 4.6 Layer 6: Marketplace Verification + Allowlist
For plugin-kind channels:
```typescript
// Marketplace verification: ensure installed plugin matches claimed source
const actual = pluginSource
? parsePluginIdentifier(pluginSource).marketplace
: undefined
if (actual !== entry.marketplace) {
return { action: 'skip', kind: 'marketplace',
reason: `tag mismatch: asked for @${entry.marketplace}, installed from ${actual}` }
}
// Allowlist check: plugin must be on GrowthBook approved list
if (!entry.dev) {
const { entries } = getEffectiveChannelAllowlist(sub, policy?.allowedChannelPlugins)
if (!entries.some(e => e.plugin === entry.name && e.marketplace === entry.marketplace)) {
return { action: 'skip', kind: 'allowlist', reason: 'not on approved list' }
}
}
```
**Dual verification**: First verify that the `@anthropic` tag in `--channels plugin:slack@anthropic` matches the actual installed plugin source (preventing `slack@evil` from impersonating `slack@anthropic`), then check against the approved allowlist.
**Allowlist source priority**: Team/Enterprise orgs can set `allowedChannelPlugins`, which replaces the GrowthBook ledger allowlist (admin owns the trust decision).
For server-kind entries, the allowlist always fails (schema is `{marketplace, plugin}` format, server kind cannot match), unless `--dangerously-load-development-channels` is used (setting `entry.dev = true` to bypass).
### Gate Result Type
```typescript
type ChannelGateResult =
| { action: 'register' } // Passed all checks, register notification handler
| { action: 'skip'; kind: string; reason: string } // Blocked at some layer
// kind enum: capability | disabled | auth | policy | session | marketplace | allowlist
```
---
## 5. Permission Relay System
![Permission Relay](./images/04-permission-relay.png)
### 5.1 Why Permission Relay Exists
When Claude Code needs to execute sensitive operations (like running a Bash command), it shows a permission confirmation dialog. But if the user is controlling the Agent remotely via Telegram, they can't see the local terminal dialog.
The permission relay system solves this: **forward permission prompts to the IM platform so users can approve or deny operations from their phone**.
### 5.2 Outbound: CC → Channel (Permission Request)
When the Agent triggers a permission dialog and a Channel has declared `experimental['claude/channel/permission']` capability:
```typescript
// Notification schema
const CHANNEL_PERMISSION_REQUEST_METHOD =
'notifications/claude/channel/permission_request'
type ChannelPermissionRequestParams = {
request_id: string // 5-letter identifier (e.g., "tbxkq")
tool_name: string // Tool name (e.g., "Bash")
description: string // Human-readable description
input_preview: string // JSON input preview, truncated to 200 chars
}
```
The Channel Server formats this for its platform (Telegram markdown, Discord embed, etc.) and sends it to the user.
### 5.3 Short Request ID Generation
The 5-letter identifier design is thoughtfully crafted:
```typescript
// channelPermissions.ts:140-152
function shortRequestId(toolUseID: string): string {
let candidate = hashToId(toolUseID)
for (let salt = 0; salt < 10; salt++) {
if (!ID_AVOID_SUBSTRINGS.some(bad => candidate.includes(bad))) {
return candidate
}
candidate = hashToId(`${toolUseID}:${salt}`)
}
return candidate
}
```
**Design decisions**:
- **25-letter alphabet**: a-z minus `l` (confusion with 1/I), 25^5 ≈ 9.8M combinations
- **FNV-1a hash**: Not cryptographic, but stable and fast
- **Profanity filter**: 5 random letters can spell offensive words (imagine texting your boss), built-in blocklist
- **Letters only**: Phone users don't need to switch keyboard modes (hex alternates between letters and digits)
- **Case insensitive**: Accommodates phone autocorrect
### 5.4 Inbound: Channel → CC (Permission Response)
Users reply in IM with format: `yes tbxkq` or `no tbxkq`
```typescript
// Server-side parsing regex
const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i
// Structured notification (server parses and emits, CC doesn't regex-match text)
const ChannelPermissionNotificationSchema = z.object({
method: z.literal('notifications/claude/channel/permission'),
params: z.object({
request_id: z.string(),
behavior: z.enum(['allow', 'deny']),
}),
})
```
**Key design**: The Channel Server is responsible for parsing the user's reply and emitting a structured event — CC never does text regex matching. This means casual conversation text can never accidentally trigger permission approval.
### 5.5 Multi-Source Racing
Permission responses come from four sources, first to resolve wins:
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Local UI │ │ Bridge │ │ Channels │ │ Hooks │
│ Terminal │ │ Remote │ │ Telegram etc │ │ Permission │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │ │
└───────────────────┴───────────────────┴──────────────────┘
claim() — first to resolve wins
┌─────┴─────┐
│ resolve │
│ allow/deny │
└───────────┘
```
`createChannelPermissionCallbacks()` uses a closure to maintain a pending Map (not at module level, not in AppState — function references in state cause serialization issues), constructed once and stored in AppState.
### 5.6 Filtering Permission Relay Clients
```typescript
// channelPermissions.ts:177-194
function filterPermissionRelayClients(clients, isInAllowlist) {
return clients.filter(c =>
c.type === 'connected' &&
isInAllowlist(c.name) &&
c.capabilities?.experimental?.['claude/channel'] !== undefined &&
c.capabilities?.experimental?.['claude/channel/permission'] !== undefined
)
}
```
**All three conditions required**: connected + in allowlist + declares BOTH capabilities (`claude/channel` AND `claude/channel/permission`). The second capability is explicit opt-in — a relay-only Channel never accidentally becomes a permission approval surface.
---
## 6. UI Components
### 6.1 Terminal Message Rendering (UserChannelMessage)
```typescript
// UserChannelMessage.tsx
// Parses <channel> XML tags and renders them in terminal
const CHANNEL_RE = new RegExp(
`<${CHANNEL_TAG}\\s+source="([^"]+)"([^>]*)>\\n?([\\s\\S]*?)\\n?</${CHANNEL_TAG}>`
)
// Plugin server name display: plugin:slack-channel:slack → slack
function displayServerName(name: string): string {
const i = name.lastIndexOf(':')
return i === -1 ? name : name.slice(i + 1)
}
const TRUNCATE_AT = 60 // Message body truncation length
```
**Rendered output**:
```
◁ tg · alice: Can you check what's wrong with main.ts?
```
Where `◁` is the Channel arrow symbol (`CHANNEL_ARROW`), showing the server leaf name and optional username.
### 6.2 Status Notice (ChannelsNotice)
Shows the status of `--channels` entries at startup, reporting blockers:
| Block Type | Meaning |
|------------|---------|
| `disabled` | Channel feature not enabled (tengu_harbor off) |
| `noAuth` | Not OAuth authenticated |
| `policyBlocked` | Org policy hasn't enabled channels |
| `unmatched` | Not matched in `--channels` list |
### 6.3 Developer Confirmation Dialog (DevChannelsDialog)
Warning dialog shown when using `--dangerously-load-development-channels`:
```
┌─ WARNING: Loading development channels ──────────────────┐
│ │
│ --dangerously-load-development-channels is for local │
│ channel development only. Do not use this option to │
│ run channels you have downloaded off the internet. │
│ │
│ Please use --channels to run a list of approved channels│
│ │
│ Channels: plugin:my-channel@local
│ │
│ > I am using this for local development │
│ Exit │
└──────────────────────────────────────────────────────────┘
```
Selecting "Exit" calls `gracefulShutdownSync(1)` for immediate exit.
---
## 7. Plugin Channel Architecture
### 7.1 Channel Declaration in Plugin Manifest
Plugins declare channels via the `channels` array in `plugin.json`:
```typescript
// schemas.ts:670-703
const PluginManifestChannelsSchema = z.object({
channels: z.array(z.object({
server: z.string().min(1), // MCP server name, must match key in mcpServers
displayName: z.string().optional(), // Config dialog title (e.g., "Telegram")
userConfig: z.record( // Fields to prompt user for at install time
z.string(),
PluginUserConfigOptionSchema()
).optional(),
}).strict()),
})
```
**Example plugin.json**:
```json
{
"name": "telegram",
"version": "1.0.0",
"mcpServers": {
"tg": {
"command": "node",
"args": ["./server.js"],
"env": {
"BOT_TOKEN": "${user_config.bot_token}",
"OWNER_ID": "${user_config.owner_id}"
}
}
},
"channels": [
{
"server": "tg",
"displayName": "Telegram",
"userConfig": {
"bot_token": {
"type": "string",
"description": "Telegram Bot API Token",
"required": true,
"secret": true
},
"owner_id": {
"type": "string",
"description": "Your Telegram User ID",
"required": true
}
}
}
]
}
```
### 7.2 Configuration Flow (PluginOptionsFlow)
After enabling a plugin, if a Channel has unconfigured `userConfig` fields:
1. `getUnconfiguredChannels()` detects fields that haven't passed validation
2. `PluginOptionsFlow` component prompts the user for each field
3. Sensitive values (like bot_token) stored in Keychain
4. Regular values stored in `~/.claude/plugins/options/{pluginId}.json`
```typescript
// mcpPluginIntegration.ts:290-318
function getUnconfiguredChannels(plugin: LoadedPlugin): UnconfiguredChannel[] {
const channels = plugin.manifest.channels
if (!channels || channels.length === 0) return []
const unconfigured: UnconfiguredChannel[] = []
for (const channel of channels) {
if (!channel.userConfig) continue
const saved = loadMcpServerUserConfig(pluginId, channel.server) ?? {}
const validation = validateUserConfig(saved, channel.userConfig)
if (!validation.valid) {
unconfigured.push({
server: channel.server,
displayName: channel.displayName ?? channel.server,
configSchema: channel.userConfig,
})
}
}
return unconfigured
}
```
### 7.3 Scoped Naming
Plugin-provided MCP Servers get a scope prefix to avoid naming conflicts:
```typescript
// mcpPluginIntegration.ts:341-360
function addPluginScopeToServers(
servers: Record<string, McpServerConfig>,
pluginName: string,
pluginSource: string, // e.g., "telegram@anthropic"
): Record<string, ScopedMcpServerConfig> {
const scopedServers = {}
for (const [name, config] of Object.entries(servers)) {
const scopedName = `plugin:${pluginName}:${name}`
scopedServers[scopedName] = {
...config,
scope: 'dynamic',
pluginSource,
}
}
return scopedServers
}
```
**Naming transformation**:
- Input: `{ "tg": { ... } }` from `telegram@anthropic`
- Output: `{ "plugin:telegram:tg": { scope: 'dynamic', pluginSource: 'telegram@anthropic', ... } }`
`pluginSource` is preserved on the config for later marketplace verification in the Channel Gate.
### 7.4 Effective Allowlist Source
```typescript
// channelNotification.ts:127-138
function getEffectiveChannelAllowlist(sub, orgList) {
// Team/Enterprise custom allowlist → replaces GrowthBook ledger
if ((sub === 'team' || sub === 'enterprise') && orgList) {
return { entries: orgList, source: 'org' }
}
// Default to GrowthBook ledger
return { entries: getChannelAllowlist(), source: 'ledger' }
}
```
Organization admins can fully control which Channel plugins are trusted via `allowedChannelPlugins`, independent of the global ledger.
---
## 8. Security Design
### 8.1 XML Injection Prevention
Channel message metadata becomes XML attributes. Two lines of defense:
1. **Key name filtering**: `SAFE_META_KEY = /^[a-zA-Z_][a-zA-Z0-9_]*$/` — only plain identifiers
2. **Value escaping**: `escapeXmlAttr()` applies XML escaping to attribute values
### 8.2 Marketplace Verification
`--channels plugin:slack@anthropic` is merely the user's "intent declaration." The runtime name `plugin:slack:X` could come from `slack@anthropic` or `slack@evil`. The gate verifies they must match:
```typescript
const actual = pluginSource
? parsePluginIdentifier(pluginSource).marketplace
: undefined
if (actual !== entry.marketplace) {
return { action: 'skip', kind: 'marketplace', reason: '...' }
}
```
### 8.3 Trust Boundary of Permission Relay
From Kenneth's analysis in code comments (PR discussion #2956440848):
> "Would this let Claude self-approve?" Answer: the approving party is the human via the channel, not Claude. But the trust boundary isn't the terminal — it's the allowlist (tengu_harbor_ledger). A compromised channel server CAN fabricate "yes \<id\>" without the human seeing the prompt. Accepted risk: a compromised channel already has unlimited conversation-injection turns (social-engineer over time, wait for acceptEdits, etc.); inject-then-self-approve is faster, not more capable. The dialog slows a compromised channel; it doesn't stop one.
### 8.4 skipSlashCommands
Channel messages are enqueued with `skipSlashCommands: true`, ensuring text like `/help` sent by IM users is not interpreted as Claude Code slash commands.
### 8.5 Dev Bypass Granularity
The `dev` flag from `--dangerously-load-development-channels` is **per-entry**, not global. After accepting the dev dialog, only explicitly dev-flagged entries bypass the allowlist — normal `--channels` entries still go through full allowlist verification.
---
## 9. Command-Line Interface
### 9.1 Startup Parameters
```bash
# Use approved Channel plugins
claude --channels plugin:telegram@anthropic plugin:feishu@anthropic
# Local development mode (bypass allowlist)
claude --dangerously-load-development-channels plugin:my-channel@local
# Both can be used simultaneously
claude --channels plugin:telegram@anthropic \
--dangerously-load-development-channels plugin:dev-channel@local
```
### 9.2 Argument Parsing
```typescript
// main.tsx
const parseChannelEntries = (raw: string[], flag: string): ChannelEntry[] => {
// Parse "plugin:slack@anthropic", "server:slack", etc.
// Validate format correctness
// Return ChannelEntry array
}
// Store in global state
setAllowedChannels(channelEntries)
```
### 9.3 Feature Gating
These CLI options are only available when feature flags are enabled:
```typescript
// main.tsx:3850-3852
if (feature('KAIROS') || feature('KAIROS_CHANNELS')) {
program.addOption(new Option('--channels <servers...>', '...').hideHelp())
program.addOption(new Option('--dangerously-load-development-channels <servers...>', '...').hideHelp())
}
```
`hideHelp()` means these options don't appear in `--help` output — the Channel feature is currently in hidden feature stage.
---
## 10. Feature Flags and Analytics
### 10.1 Feature Flags
| Flag | Source | Purpose |
|------|--------|---------|
| `KAIROS` / `KAIROS_CHANNELS` | Build-time | Controls CLI argument registration and code paths |
| `tengu_harbor` | GrowthBook runtime | Channel system master switch (default false) |
| `tengu_harbor_ledger` | GrowthBook runtime | Approved plugin allowlist |
| `tengu_harbor_permissions` | GrowthBook runtime | Permission relay feature switch |
### 10.2 Analytics Events
```typescript
// Channel Gate result
tengu_mcp_channel_gate: {
gate_kind: 'disabled' | 'auth' | 'policy' | 'session' | 'marketplace' | 'allowlist'
plugin: string
is_dev: boolean
}
// Channel message
tengu_mcp_channel_message: {
content_length: number
meta_key_count: number
entry_kind: 'plugin' | 'server'
is_dev: boolean
plugin: string
}
// Startup flags
tengu_mcp_channel_flags: {
channels_count: number
dev_count: number
plugins: string[]
dev_plugins: string[]
}
```
---
## 11. Summary
The Channel system is Claude Code's **IM integration framework**, and its design embodies several core principles:
### 1. Security First
Six access control layers ensure only approved, user-explicitly-chosen Channels can push messages. From build-time feature flags to runtime allowlists, each layer can independently interrupt the flow.
### 2. Protocol-Driven
Channels aren't special code — they're ordinary MCP Servers with an additional notification protocol. This means any language that can implement MCP can write Channel plugins.
### 3. Loose Coupling
Channel failures don't block local workflows. Permission relay is a multi-source racing mechanism where any source's response is valid.
### 4. Progressive Trust
From global switch → authentication → org policy → session allowlist → marketplace verification → allowlist, trust levels increase progressively, with each step serving a clear security purpose.
### 5. Plugin-Friendly
Through declarative Channel configuration in `plugin.json`, automatic user config prompting, and scoped naming, third-party developers can easily build their own Channel plugins.
### Source File Index
| File | Lines | Responsibility |
|------|-------|---------------|
| `src/services/mcp/channelNotification.ts` | ~320 | Gating, message wrapping, allowlist integration |
| `src/services/mcp/channelPermissions.ts` | ~240 | Permission relay, request ID generation |
| `src/services/mcp/channelAllowlist.ts` | ~80 | GrowthBook allowlist queries |
| `src/services/mcp/useManageMCPConnections.ts` | — | Connection management, notification handler registration |
| `src/components/messages/UserChannelMessage.tsx` | ~140 | Terminal rendering of Channel messages |
| `src/components/DevChannelsDialog.tsx` | ~105 | Development mode confirmation dialog |
| `src/components/LogoV2/ChannelsNotice.tsx` | — | Startup status notifications |
| `src/utils/plugins/mcpPluginIntegration.ts` | — | Plugin MCP integration, scoped naming |
| `src/utils/plugins/schemas.ts` | ~700 | Plugin manifest schema (incl. Channel declarations) |
| `src/bootstrap/state.ts` | — | Global Channel allowlist state |
| `src/main.tsx` | ~3850 | CLI argument registration and parsing |

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

83
docs/en/channel/index.md Normal file
View File

@ -0,0 +1,83 @@
# Claude Code Channel System Documentation
> Complete technical analysis of remote Agent control via IM platforms
---
## Documentation
### [01-channel-system.md](./01-channel-system.md) — Channel System Architecture
A deep dive into the design and implementation of Claude Code's Channel system from a source code perspective, covering:
- **What is a Channel**: Core concepts of IM integration, MCP protocol foundations
- **Architecture Overview**: End-to-end message flow, component relationships
- **Message Protocol**: Inbound notifications, XML wrapping, outbound tool calls
- **Six-Layer Access Control**: Capability → Runtime gate → OAuth → Org policy → Session allowlist → Plugin approval
- **Permission Relay System**: Remote tool execution approval, 5-letter request IDs, multi-source racing
- **Plugin Architecture**: Channel manifest declarations, user config flow, scoped naming
- **UI Components**: Terminal message rendering, status notices, developer warning dialogs
- **Security Design**: XML injection prevention, marketplace verification, trust boundary analysis
**Target audience**: Developers, architects, and plugin authors interested in AI Agent IM integration architecture
---
## Illustrations
All illustrations use dark background (#1a1a2e) with Anthropic brand copper-orange (#D97757) accent color.
| Image | Description | Document |
|-------|-------------|----------|
| `01-channel-overview.png` | Channel system architecture overview | Architecture |
| `02-message-flow.png` | End-to-end message flow: IM → Agent → IM | Architecture |
| `03-access-control.png` | Six-layer access control gates | Architecture |
| `04-permission-relay.png` | Permission relay system flow | Architecture |
---
## Quick Start
### Users
1. Read the [Channel System Architecture](./01-channel-system.md)
2. Learn how to start IM integration with `--channels`
3. Understand how different platforms (Telegram, Feishu, Discord) connect
### Plugin Developers
1. Read the [Plugin Architecture](./01-channel-system.md#_7-plugin-channel-architecture) section
2. Understand the `plugin.json` Channel declaration format
3. Implement the MCP Server `notifications/claude/channel` protocol
4. Key source files:
- `src/services/mcp/channelNotification.ts` — Core gating and message wrapping
- `src/services/mcp/channelPermissions.ts` — Permission relay system
- `src/services/mcp/channelAllowlist.ts` — Allowlist management
- `src/utils/plugins/mcpPluginIntegration.ts` — Plugin MCP integration
- `src/utils/plugins/schemas.ts` — Plugin manifest Channel schema
---
## Core Concepts Reference
| Concept | Description |
|---------|-------------|
| **Channel** | An MCP Server declaring `claude/channel` capability that can push IM messages to the Agent |
| **Channel Entry** | Parsed `--channels` argument entry, either plugin or server kind |
| **Channel Gate** | Six-layer access control gate deciding whether to register notification handlers |
| **Permission Relay** | Mechanism for forwarding tool execution approval prompts to IM platforms |
| **Channel Plugin** | A plugin declaring `channels` field in its `plugin.json` |
| **Scoped Name** | Plugin server name with scope prefix: `plugin:{pluginName}:{serverName}` |
| **Short Request ID** | 5-letter permission request identifier generated via FNV-1a hash |
| **Channel Tag** | `<channel>` XML tag wrapping IM message content and metadata |
| **Dev Channels** | Channels loaded via `--dangerously-load-development-channels` for local development |
| **tengu_harbor** | GrowthBook runtime feature flag controlling the Channel system master switch |
---
## Related Resources
- [Claude Code Haha Home](/en/)
- [Agent Framework Deep Dive](/en/agent/03-agent-framework)
- [Skills System Documentation](/en/skills/01-usage-guide)
- [GitHub Issues](https://github.com/NanmiCoder/cc-haha/issues)