面向 Pilot 开发者的架构介绍:先用系统总览、五元关系和数据模型搭骨架,再用五个模块深潜 + 十六条跨模块机制链讲清每个部件怎么工作、怎么发版、坑在哪。全部结论 file·符号级读码实证,基于 main @ 042761fd1。文中 INV-* 不变式的全文定义见 docs/architecture/agenthub-system-model.md §4.4;模块深潜正文沉淀在 docs/architecture/agenthub-module-deep-dives.md。
七个进程 + 三路出站。鼠标悬停任意组件高亮其直接关系;横向可滚动。关键心智:packages/control-plane 是没有独立进程的 shared package——三个壳和 control-worker 都进程内 import 它,写库动作发生在各引用方自己的进程里,但写入代码只有这一份("唯一 canonical writer"是代码层概念);apps/control-plane 只是给外部 SDK/admin 的第三个对等壳,前端主流量不经过它。前端↔后台只经 DB+pg-boss,绝不直接 RPC;apps/web 已冻结(2026-07-04 拍板,fork 永不合并)。
整个平台的产品语义骨架,一句话:Agent 定义"是什么",Revision 是它的不可变版本"哪一版",Environment 定义"跑在什么环境上"(自有版本线),Stage 是部署槽位——把「哪一版 × 哪个环境的哪个版本」这对选择钉住,Session 是运行实例——创建时从槽位解析并永久冻结这份选择。晋级、回滚、灰度,全部 = 拨槽位上的指针,从不修改已存在的版本或会话。
Agent(workload) ≈ Deployment 定义;Revision ≈ 镜像/ReplicaSet 版本(不可变制品);Environment ≈ ConfigMap+资源规格(部署配置层,不属制品——E-DEC6 拍板的核心:同一制品必须能跨 stage 配不同资源,冻进制品就破坏晋级语义);Stage ≈ 环境的部署指针;Session ≈ Pod(用完即弃的运行实例)。
Revision 和 EnvironmentVersion 是两条独立的不可变版本线(制品线 × 环境线),唯一的交汇点是 Stage 槽位上的配对:(revisionId, environmentId, environmentVersion)。这就是 E-DEC6 的本质——不是"一个大盒子装所有配置",而是两条线 + 槽位记配对,配对变更全程有账(repoint 账本)。
① stage 路径(主线):按槽位解析 revision 快照 ⊕ 钉住的环境版本配置 → 冻结进 session;② pin 路径:显式指定 revisionId,不经槽位、不带环境形态;③ legacy 路径:两者皆缺,回退 default_revision_id。冻结后永不回读(INV-9)——晋级/回滚只影响之后新建的 Session。
按 packages/db/src/schema/pilot-v2-sessions.ts 逐字段整理;全库共 54 表,此图只画主链三层,其余(凭据 11 表 / 租户 12 表 / v1 遗留 11 表等)见 packages/db/src/schema/AGENTS.md。
E-DEC1 拍板:environments 资源在 runtime_templates 表原地演化,没有另开新表、没有 kind 判别字段。图中"环境资产线"那个高亮框,实体上跟老的 Runtime Template 是同一张表。
workload_stages.environment_id 和 environment_version 必须成对移动,由触发器 workload_stages_environment_version_pairing(migration 0104)强制——不在 Drizzle schema 里,裸 SQL 改绑会被自动钉到目标环境 tip。
config/version/source/created_by/created_at 全部 write-once(POLICY LOCK 锁死)。唯一可变列是 provider_template_name——物化指针挪动不改变版本语义。
environment_version_repoints 的槽位引用是弱引用——审计记录要比槽位活得久,槽位级联删除后账本仍可读。
pilot_v2_sessions.environment_id / environment_version 创建时写入、之后只读——真解析结果冻结在 runtime_template_snapshot_json,历史读取不回源。
老路径:revisions.runtime_template_id 发布时拍平快照。新路径:槽位绑定 environment,session 创建时解析。槽位 environment_id 为空 = 老路径,两者共存(OQ-10 强制化 P2 不做)。
数据模型之下,单个模块的具体工作方式。四个问题:怎么工作、怎么发版、怎么请求 gateway、env 怎么注入。全部落点已读码验证。
pod-agent = OCI sidecar(:8790,监管+代理);agent-worker = 跑 agent 的业务进程(:8787)
session_startup_jobs,按槽位钉住的环境版本解析 Kanyun 模板名(E-DEC6 物化指针)+ TTL,调 Kanyun createSandbox(或 warm pool claim)POST /pilot/worker/start,请求体带 podAgentToken、sessionResources、workspaceSnapshot、agentBootstrap 和 PILOT_* envbuildWorkerEnv(process-manager.ts)组装子进程 env → 直接 spawn 镜像内 baked 的 agent-worker dist/server.js/health 上/pilot/worker/health(聚合端点);pod-agent 内部再打 worker 的 /health 并经 mapWorkerHealth 透传——透传丢失=卡 gate(历史真实 bug 点)单一部署通道(#1070 起),OSS 热更已退役
apps/pod-agent/Dockerfile.managed-agent-base 一起 bake 进同一 OCI 镜像(/opt/pilot/* 保留命名空间)——改任一进程都必须重烤镜像build-sandbox-base-image.yml 是 workflow_dispatch-only;GitHub 侧只做 mirror 到 GitLab + 触发 Kanyun ImageBuild(真正 build 在 Kanyun 侧 clone+docker build)。#1070 分层让"只改代码"的发版复用 ~320MB 依赖层缓存(含 claude/codex CLI 二进制)platform_release 版本(releaseRef 幂等)→ CAS repoint 只拨跟随 tip 的槽位;已 fork 环境刷新专属模板内容不退回共享底座(E5b/E-DEC6,07-12 test 实机验证过)RUNTIME_VERSION 文件单点 + PILOT_RUNTIME_VERSION env 注入(两进程各自 runtime-version.ts 读取)+ CI 门禁gateway 是 sandbox 出站的「可信通道」——只管 MCP + OTLP relay,不碰 git/llm/oss
mcpFetch()(packages/sdk-worker/src/mcp-fetch.ts)带 PILOT_SESSION_TOKEN 打 gateway POST /mcp/v1/proxy——不经 pod-agent(它没有 /proxy/mcp 路由)allowedMcpTargets 快照(INV-2.1)→ Vault 解密 → sanitize+注入转发上游/otlp/v1/traces relay 上报(#1106);Claude Code CLI 的 OTel 导出同样钉死走这条 relay(#1111)pod-agent 对 agent-worker 的看护边界(process-manager.ts,939 行)——所有"聪明"都刻意留在 control-worker 侧
POST /pilot/worker/start 才被 spawn(baked dist)detached:true 进程组五层来源,注意各层的保护机制不同
| 层 | 内容 / 写点 | 边界与保护 |
|---|---|---|
| ① 环境 envVars | 用户在环境编辑面配的 infra env;物化进环境专属 Kanyun 模板(environment-materialization.ts),容器启动即有 | 写面拒绝平台保留前缀(POD_AGENT_* / PILOT_* / LD_* 等 PROVIDER_CONTROL_ENV_PREFIXES);E-DEC6 后有版本历史 |
| ② PILOT_* 运行时 | session token / endpoints / WORKLOAD·REVISION_ID 等;startWorker 请求 → pod-agent buildWorkerEnv | 原名 podAgentToken 在 PROTECTED denylist 被挡,但 buildWorkerEnv 又以 PILOT_WORKSPACE_LEASE_TOKEN 名义注入 worker env(R-SWS-14,gitClone 需要)——与 contracts 注释"MUST NOT forward via env"冲突,⚠ 待拍板(spec 与实现二选一归一) |
| ③ ModelGate 凭据 | ANTHROPIC_BASE_URL / API_KEY;control-plane prepareWorkspace 写入(modelgate-env-overrides.ts · injectModelGateEnv) | 设计内明文例外:worker 与 CLI 共用同一凭据直连 ModelGate,不经 pod-agent |
| ④ env_jit secret | config values 中 value_kind=secret_ref 的键;每个 turn JIT:worker → pod-agent /internal/resolve-secret(loopback)→ broker 双因子 → 解密注入 CLI 子进程 | 明文例外③(#1140);rotate 立即生效且不可回滚——宽限窗已立 issue 待安全侧 |
| ⑤ config.env 用户层 | workload/run 级用户可配 env,进 CLI 子进程前经 filterProviderEnv 清洗(packages/agent-runtime · claude-code-provider.ts) | OTEL_* 全 scrub + traces endpoint 钉死 gateway relay(#1111,防端点劫持/prompt 泄露);AWS/Bedrock denylist |
| ⚠ 验证注意 | Claude Code Bash 工具对子进程 env 有白名单过滤——实机验证注入要读 /proc/*/environ,不能靠 echo $VAR(E5 实测经验) | |
最该重写、也最有资格重写的模块:护栏(CAS 乐观锁 / 事故提炼的 guard / 接线锁定测试)比看起来扎实,但两个入口文件已是事实上的上帝对象——worker.ts 的 start() 单方法 3714 行、orchestration/index.ts 5361 行 49 方法。诊断:两代代码风格叠加——干净的 register-with-deps 模式和 200-400 行的内联 handler 并存在同一个方法里,拆分范式已存在且被验证,缺的只是把老代码回填进去。下图是三条主链(startup / prompt 投递 / health 熔断)。
start() 占 3714 行——新 job(pool/revival/vault/ttl/schedule/env-propagation)全走干净的 register 函数,老代码(run:execute/finalize/abort 三个 handler 本体、MCP bootstrap、SSE reconnect、WeCom wiring)还内联在方法体里41,227 行源码 / 17 个域目录 / 1917 个测试用例的"上帝包"(拆包目标已解冻)。诊断结论:域拆分本身是干净的(域间是无环 DAG),真正焊死中心的是根级文件 session-startup.ts(4478 行 / 66 个 async 方法)——它与 session-lifecycle 域存在全包唯一的值级循环依赖。下图是拆包依据的域依赖骨架。
session-startup.ts:117 值导入 SessionEventAppendBatcher ⇄ session-event-append-batch.ts:53 值导入 SessionStartupError(class,非 type-only,import type 消解不掉)——拆包第一个必解的结四个后端模块里 spec / 代码 / 测试三者最对齐的一个:9 个源文件职责边界干净、3318 行测试把 5 步锁定逐步覆盖、坑点全部写进 AGENTS.md 历史事故记录。唯一真实功能债 = spec §4 业务审计 span(spec 自己诚实标注 NOT IMPLEMENTED);其余都是"部署验证债"而非设计债。可作为其它模块重写的参照基线。
session-auth.ts · verifySessionRequest 被 MCP proxy 和 OTLP relay 共用——两个入口零鉴权漂移,重写时新入口必须继续复用不许各写各的@pilot/control-plane 的 VaultClientImpl(与 web/agent-hub/control-plane 四处同一个类,961 行单源)pilot_gateway NOLOGIN 角色,仅 5 表 SELECT + session_events INSERT + vault_credentials 7 列 UPDATEservice=pilot-gateway 到底有没有 http.server span?DATABASE_URL_GATEWAY 真切到低权限用户了吗(代码只有回退链,PGlite 测不了真实 GRANT)?gateway_credential_refresh_* 因 Vault silent fallthrough 永不触发(#543)——实现 throw 路径或从 enum 退休GP1 四规则(route 零逻辑 / 端点数=调用方数 / HTTP 不跑长任务 / 双端点补契约测试)是这层的宪法。实测:agent-hub 有 84 个非测试文件直接 import 数据库层(对照 control-plane 仅 5 个),权限引擎本体(cas-guard.ts 448 行 + token-cache ACL)活在壳层而非共享层;部分违规发生在规则落地之后——现有 CI 门禁对这一切零覆盖。下图是分层应然路径与规则映射。
projects/[id]/settings/route.ts 1367 行(webhook secret 轮换/credential CRUD 全内联);sessions/route.ts 有 ~180 行 prewarm-claim 编排比模块更贴用户视角的机制链——"我配的东西如何/何时生效 / 我的产物去哪了 / 沙箱没了会怎样 / 怎么紧急关功能 / 什么能安全重试 / 挂了怎么查 / 什么时候被杀 / push 为什么 403 / 点停止发生什么"。全部 file·符号级实证,未验证处均已标注。
补深潜① D 表的"如何生效"——关键是每层的生效时机不同
/proc/*/environ 不能 echo从 agent 写文件到用户点下载
.agent/artifacts/manifest.json(显式声明)→ .agent/artifacts/ 目录 → 全工作区扫描(深度 6、白名单扩展名);.mcp.json 永远剔除(内含真凭据)general-bundle.zip(50 文件/50MB 预算,超了 partial 静默跳过——阶段性妥协,因 OSS proxy 单 key 限制)"session 没过期但 sandbox 没了"——revival 机制
self_managed 把 sandbox 端口暴露公网
POST /api/runtime-templates/auto-create(API/SDK)带 workerPort + agentPorts[] + ingresses[](name/backendPort/hostPrefix/path)——普通业务方无自助 UI,admin 才有 platform-templates 面板{url}/api/health 2xx 才放行——gate 是 PLATFORM_RESERVED,worker 伪造同名上报会被过滤(有整套防伪造回归)agentPublicEndpoints[],URL=https://{hostPrefix}.{基础域名}{path};想跨 sandbox 重建不漂移要传稳定 runtimeClaimNameCAS cookie → platform token → session token → pod-agent token
requirePlatformAuth——有 Bearer 走 platform token(scope+aud+effectiveProjects 现查),无 Bearer 走 CAS cookie(scopes='*');CAS debug 后门生产无条件 fail-closed(配置说什么都不听)TOKEN_AUDIENCE_MISMATCH=拿错产品面的 token(pilot↔agenthub),不是 token 坏了;PROJECT_NOT_IN_TOKEN_SCOPE 刻意 403 非 404/空表,且 CAS 路径也复用这个 code——别被名字骗成"一定是 token 问题""怎么紧急关一个功能"——关键是分清哪类 30s 生效、哪类要重启
conso-cli fdc,没有 config 表哪些请求能安全重试、哪些会翻倍
"一个 run 挂了怎么查"——好消息:链已缝合
"会话什么时候会被杀"
"这个会话用什么模型、怎么换"
"改了提示词为什么没生效"的准确答案
"我刚装了个 MCP 为什么会话里没有"的准确答案
.mcp.json 每轮 turn 现场重写——v2 没有"下一次 provision 才生效"的概念只读输入文件的完整生命周期
PILOT_SESSION_RESOURCES env 进 worker → 经 pod-agent 代理逐个下载到 .session/inputs/ 只读 0444 → resource_mount gate 把关(单文件失败=整体 fail).session/inputs/ 每次 provision rm -rf 重建(agent 写进去的东西必丢,Model B 快照也刻意排除它);envelope 含 ossUrl 明文进 env(spec 声称"不走 env"只对了一半)"push 为什么 403"——v1/v2 是两套独立实现,行为不对称
"聊太长怎么办"——机制齐全,但生产实测全关
"点停止到底发生什么"——v1 先翻库再通知,v2 靠被打断者自报
session 拉起 / prompt 投递 / health 熔断三条已在深潜②成图;这里补齐其余五条会被问到的路径。
agent.delivery_* 业务事件——交付失败绝不炸 runlifecycle_phase 轴接管:active → idle → terminating → terminated(+reviving)POST /api/sessions/:id/events(Bearer + W3C traceparent)→ 落 session_eventsevents/stream:50ms DB 轮询(不是 Redis;Redis push 是默认关的可选加速)+ 10s heartbeat__seq 游标;v2 SSE 与 v1 的 Redis StreamRegistry 是两套,别混schedule_tick(默认 1min,env 可配)→ ScheduleDispatcher 四分支:workload / agent / inline_v1 / inline_v2UNIQUE(scheduleId, firedAt) 防 double-fire;幂等键透传下游agent_memories;D-6 writer 分裂(落库在 control-worker);默认关、零测试、无正式 spec——开 flag 前三项必补