KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

05 · 模型接入、渠道接入与安全边界 — keel 龙骨

前四章讲的是内核。这一章讲内核的「两个接口面」——往外的模型侧、往人的渠道侧——以及把它们兜住的安全姿态。

前四章讲的是内核。这一章讲内核的「两个接口面」——往外的模型侧、往人的渠道侧——以及把它们兜住的安全姿态。

这一章回答:

  1. ProviderProfile 为什么被刻意设计成「只有数据、没有构造逻辑」;
  2. 36 个 provider 插件与 6 个协议适配器如何分工;
  3. 渠道接入为什么也走插件,BasePlatformAdapter 抽象了什么;
  4. ACP 适配器如何把同一个 agent 塞进编辑器,且不污染 stdout;
  5. SECURITY.md 那句「唯一边界是操作系统」到底否定了什么、保留了什么。

一、声明式 ProviderProfile

模型接入的抽象是 providers/base.py:39: class ProviderProfile,一个 @dataclass(装饰器在 :38)。它的第一条注释(providers/base.py:7)就是设计宣言:

Provider profiles are DECLARATIVE — they describe the provider's behavior.

字段可以按四组理解:

name: str
api_mode: str = "chat_completions"
auth_type: str = "api_key"   # api_key|oauth_device_code|oauth_external|copilot|aws_sdk
fixed_temperature: Any = None
default_aux_model: str = (...)
def fetch_models(...): ...   # providers/base.py:197

「DECLARATIVE」这句强调的其实是否定:profile 不拥有客户端构造。它只描述行为,真正的 HTTP 客户端与协议转换由 agent/ 下的适配器承担。这是把「配置」与「实现」分开的一次刻意切分。

注册表在 providers/__init__.py:_REGISTRY(:45)、register_provider()(:56,写入 _REGISTRY[profile.name])、get_provider_profile()(:70),以及懒发现的 _discover_providers()(:271)——它扫 plugins/model-providers/ 与 entry-points,所以「有哪些 provider」是运行时才知道的。

二、36 个 provider 插件与 6 个协议适配器

plugins/model-providers/ 下共 36 个 含 plugin.yaml 的插件目录,覆盖聚合器与直连两类:openrouter、anthropic、openai-codex、gemini、vertex、bedrock、deepseek、zai、kimi-coding、ollama-cloud、copilot、copilot-acp、xai、qwen-oauth、minimax、nvidia、novita、fireworks、deepinfra、huggingface、upstage、stepfun、xiaomi、arcee、gmi、kilocode、commandcode、meta-ai、opencode-zen、ai-gateway、azure-foundry、alibaba、alibaba-coding-plan、nous、custom、actual。插件清单极简,plugins/model-providers/openrouter/plugin.yaml 全文只有五行 name / kind / version / description / author。

协议转换的实现在 agent/ 下,六个适配器模块:anthropic_adapter.py、gemini_native_adapter.py、bedrock_adapter.py、vertex_adapter.py、azure_identity_adapter.py、codex_responses_adapter.py。另有 agent/transports/ 目录(chat_completions.py、codex.py、anthropic.py、bedrock.py 等)承担发送侧。

这个分工的好处很直接:新增一家「OpenAI 兼容」的供应商,只需一个 plugin.yaml + 一个 ProviderProfile 实例;只有协议真的不同(Anthropic Messages、Bedrock Converse、Codex Responses)才需要写适配器。

三、渠道:一个抽象基类 + 22 个插件

渠道侧的抽象是 gateway/platforms/base.py:2890: class BasePlatformAdapter(ABC)。仓库内直接实现的适配器包括 api_server、signal、weixin、whatsapp_cloud、yuanbao(带 yuanbao_proto / yuanbao_media / yuanbao_sticker 三个配套模块)、qqbot(独立子包)、bluebubbles、msgraph_webhook、webhook;另有一个「中转」实现 gateway/relay/adapter.py:65: class RelayAdapter(BasePlatformAdapter)。

主流 IM 渠道则以插件形式提供:plugins/platforms/ 下共 22 个 频道目录——telegram、discord、slack、feishu、dingtalk、wecom、matrix、mattermost、irc、line、sms、teams、email、google_chat、homeassistant、a2a、buzz、ntfy、raft、simplex、photon、whatsapp。

以 Telegram 为例:plugins/platforms/telegram/adapter.py:638: class TelegramAdapter(BasePlatformAdapter),配套 telegram_ids.py、telegram_network.py(文件头提到 direct-IP failover)。清单 plugins/platforms/telegram/plugin.yaml 里用 requires_env / optional_env 声明依赖:

name: telegram-platform
kind: platform
requires_env:
  - name: TELEGRAM_BOT_TOKEN
    description: "Telegram bot token from @BotFather"
    password: true
optional_env:
  - name: TELEGRAM_ALLOWED_USERS
  - name: TELEGRAM_HOME_CHANNEL

这份清单同时是「首次配置怎么问用户」的规格:prompt 是提问文案、url 是去哪拿、password: true 表示回显要遮蔽。注册表是 gateway/platform_registry.py:232: class PlatformRegistry。

渠道层的另一个设计是「命令的单一来源」。hermes_cli/commands.py:139 的注释写着「Central registry -- single source of truth」,文件头(:4)声明这套定义同时扇出到 CLI dispatch、Telegram BotCommands、Slack 子命令映射与自动补全。对应函数如 telegram_bot_commands()(:650)、gateway_help_lines()(:599)、resolve_command()(:412)。注释 :684 还记了一条平台硬约束:「Telegram allows up to 100 BotCommands. Hermes ships ~50 built-in commands」。

四、ACP:把 agent 塞进编辑器

ACP 适配器的两个工程细节值得单独看:

  1. stdout 专供传输。acp_adapter/entry.py 的文件 docstring 与 _setup_logging()(:81)把 root logger 的 handler 挂到 sys.stderr,并套 RedactingFormatter(来自 agent/redact)。任何往 stdout 打印的东西都会破坏 JSON-RPC 帧。
  2. 噪音过滤。客户端会定期发 ping / health 这类不在 ACP schema 里的探活方法,acp 路由会正确返回 -32601,但监督任务会把异常打成 traceback。_BenignProbeMethodFilter(acp_adapter/entry.py:52)专门只压掉这一类噪音(_BENIGN_PROBE_METHODS,:49),其它 background-task 错误照常打印。

Agent 本体是 acp_adapter/server.py:566: class HermesACPAgent(acp.Agent)。宿主能力通过回调桥接:acp_adapter/events.py:114: make_tool_progress_cb、:189: make_thinking_cb、:209: make_step_cb、:266: make_message_cb——正是第 02 章那 19 个回调在 ACP 侧的具体接法。

审批在 ACP 里有两条路径:权限用 acp_adapter/permissions.py:110: make_approval_callback(...);文件编辑另有专门通道 acp_adapter/edit_approval.py:233: maybe_require_edit_approval(tool_name, arguments),它支持 write_file、patch-replace、v4a patch 三种提案形状(_proposal_for_write_file :82、_proposal_for_patch_replace :98、_proposal_for_patch_v4a :155)。文档明确:requester 抛异常时默认拒绝(edit_approval.py:237: "Requester exceptions deny by default")。

五、安全边界:唯一边界是操作系统

SECURITY.md 第 58 节标题是「The Boundary: OS-Level Isolation」,第一句是全书最重的判词(SECURITY.md:60):

The only security boundary against an adversarial LLM is the operating system.

紧接着一段把话说满(:61-65):「Nothing inside the agent process constitutes containment — not the approval gate, not output redaction, not any pattern scanner, not any tool allowlist.」理由是:任何在进程内筛查 LLM 输出的组件,本质上都是「在一个受攻击者影响的字符串上跑的启发式」。

据此它给出两种姿态:

沙箱后端的实现目录是 tools/environments/,基类 tools/environments/base.py:595: class BaseEnvironment(ABC),后端模块为 local / docker / ssh / singularity / modal / managed_modal / daytona / vercel_sandbox(另有 file_sync.py、modal_utils.py 两个辅助模块)。

六、进程内的纵深(以及它自认不是边界)

把 in-process 的防护说成「不是边界」不等于它们无用。SECURITY.md:137-153 的定位是「useful… not boundaries」——纵深防御,用于降低事故概率而非承担对抗性威胁。这一层由这些组件构成:

代码地图

机制 位置 要点
Profile 声明 providers/base.py: ProviderProfile 第 39 行;@dataclass(:38),api_mode 默认 chat_completions(:44)
声明式宣言 providers/base.py 第 7 行 "profiles are DECLARATIVE";不拥有客户端构造
鉴权五值 providers/base.py: auth_type 第 56 行;api_key / oauth_device_code / oauth_external / copilot / aws_sdk
辅助模型默认值 providers/base.py: default_aux_model 第 97 行;注释 :107 承认会腐烂,:116 给掉落回策略
模型列表查询 providers/base.py: fetch_models 第 197 行
provider 注册表 providers/__init__.py: _REGISTRY 第 45 行;register_provider(:56)、get_provider_profile(:70)
provider 懒发现 providers/__init__.py: _discover_providers 第 271 行;扫 plugins/model-providers/ + entry-points
provider 插件 plugins/model-providers/openrouter/plugin.yaml 36 个插件目录;清单仅 5 行(name/kind/version/description/author)
协议适配器 agent/codex_responses_adapter.py 六件套之一;另有 anthropic / gemini_native / bedrock / vertex / azure_identity
渠道抽象基类 gateway/platforms/base.py: BasePlatformAdapter 第 2890 行;ABC
中转适配器 gateway/relay/adapter.py: RelayAdapter 第 65 行;继承 BasePlatformAdapter
渠道插件 plugins/platforms/telegram/adapter.py: TelegramAdapter 第 638 行;plugins/platforms/ 共 22 个渠道目录
渠道依赖声明 plugins/platforms/telegram/plugin.yaml: requires_env 第 13 行 TELEGRAM_BOT_TOKEN;optional_env 在 :19
渠道注册表 gateway/platform_registry.py: PlatformRegistry 第 232 行
命令单一来源 hermes_cli/commands.py 第 139 行 注释「Central registry -- single source of truth」;telegram_bot_commands(:650)、resolve_command(:412)
ACP 入口 acp_adapter/entry.py: main 第 220 行;_setup_logging(:81)把日志锁到 stderr
ACP 探活降噪 acp_adapter/entry.py: _BenignProbeMethodFilter 第 52 行;_BENIGN_PROBE_METHODS 在 :49
ACP Agent acp_adapter/server.py: HermesACPAgent 第 566 行;继承 acp.Agent
ACP 事件桥 acp_adapter/events.py: make_tool_progress_cb 第 114 行;另有 make_thinking_cb(:189)、make_step_cb(:209)、make_message_cb(:266)
ACP 权限审批 acp_adapter/permissions.py: make_approval_callback 第 110 行
ACP 编辑审批 acp_adapter/edit_approval.py: maybe_require_edit_approval 第 233 行;requester 抛异常默认拒绝
安全判词 SECURITY.md 第 60 行 「The only security boundary … is the operating system」
两种隔离姿态 SECURITY.md 第 70 / 90 行 terminal-backend isolation;whole-process wrapping(含 NVIDIA OpenShell)
沙箱基类 tools/environments/base.py: BaseEnvironment 第 595 行;八个后端模块
Tirith 扫描 tools/tirith_security.py 第 7 行 退出码 0 = allow, 1 = block, 2 = warn
全局急停 agent/estop.py: is_engaged 第 59 行;engage(reason) 在 :74
子进程环境裁剪 tools/env_passthrough.py 第 6 / 17 行 会话级 allowlist;剥变量前必须过 is_env_passthrough
插件安装扫描 tools/plugin_guard.py 第 3 / 11 行 针对插件形态取舍,避免误伤全部合法 provider 插件

关键取舍

把 profile 做成纯数据、不给它构造客户端的权力,代价是「配置在哪生效」要跨文件追。
ProviderProfile 只有字段和 fetch_models(),客户端构造留在 agent/transports/ 与各适配器。收益是新增商家几乎零代码、profile 可以被插件系统安全地装载;代价是排查「这个 provider 的温度为什么没生效」要同时读 fixed_temperature、api_mode 分派点、以及对应 transport——三处都在不同文件。

渠道也走插件、渠道依赖写在 plugin.yaml 里,代价是配置流程变成「数据驱动」。
requires_env 的每一项带 prompt / url / password,配置向导可以完全由清单生成。收益是新增渠道不用写配置 UI;代价是「为什么没让我填某个变量」这类问题只能去读 optional_env 与 check_fn 的组合,而不是读一段命令式代码。

ACP 把 stdout 完全让给协议,代价是所有打印都必须走 logger。
_setup_logging() 强制 root handler 指向 stderr,且统一套 RedactingFormatter。收益是 JSON-RPC 帧不会被任何 print 污染;代价是调试时不能随意 print,且客户端探活产生的良性异常必须用过滤器(_BenignProbeMethodFilter)单独识别,否则会周期性刷屏。

明确宣布 in-process 防护「不是边界」,代价是用户可能低估它们的作用。
SECURITY.md 用整段否定了审批门、脱敏、模式扫描、工具白名单的边界地位,只承认终端后端隔离与整进程包裹两种 OS 级姿态。收益是责任划分清晰:对抗性威胁由 OS 层承担,进程内只做纵深;代价是那些防护(hardline 命令、Tirith 扫描、凭据池、环境裁剪、安装前扫描)真实的实用价值容易被「反正不算边界」一句话带过——它们防的是事故,不是对手。

保守的安装前扫描,代价是规则只能覆盖插件/技能形态的窄集。
plugin_guard.py 的注释直说通用危险模式会「flag every legitimate provider plugin」,因此扫描器必须收窄。收益是安装体验不被误报摧毁;代价是这条防线本质上只能拦住「形态明显异常」的包,真正的恶意插件仍需靠 OS 级隔离兜底。

自测题

  1. ProviderProfile 的注释说 profile 「do NOT own client construction」。请指出如果让 profile 自己构造客户端,会破坏哪些现有机制(至少两个),并说明 api_mode 与 auth_type 分别属于「描述」还是「实现」。
  2. auth_type 有五个值,其中 oauth_external 与 oauth_device_code 的差别是什么?为什么这两者不能合并成一个「OAuth」值,而必须区分?
  3. Telegram 的 plugin.yaml 用 requires_env / optional_env 声明依赖,包括 password: true 与 url。请说明这套声明在「首次配置向导」与「运行时可用性判断」两处各自被谁消费,以及 TELEGRAM_ALLOW_ALL_USERS 被列为 optional_env 而非 requires_env 的安全含义。
  4. ACP 里权限审批(permissions.py)与编辑审批(edit_approval.py)是两条独立路径。请说明为什么文件编辑需要一个单独的提案机制,以及「requester 抛异常默认拒绝」这一选择的代价。
  5. SECURITY.md 说 terminal-backend isolation 不覆盖 code-execution、MCP 子进程、插件/ hook /技能加载。请针对其中任意两项,说明为什么 whole-process wrapping 才能覆盖它们,以及选择 terminal-backend isolation 的运营者应当接受什么残余风险。

进入 keel 阅读