AgentHub 架构与模块深潜
Pilot v2 · 开发者架构导读 · 更新于 2026-07-12

AgentHub 架构与模块深潜

面向 Pilot 开发者的架构介绍:先用系统总览、五元关系和数据模型搭骨架,再用五个模块深潜 + 十六条跨模块机制链讲清每个部件怎么工作、怎么发版、坑在哪。全部结论 file·符号级读码实证,基于 main @ 042761fd1。文中 INV-* 不变式的全文定义见 docs/architecture/agenthub-system-model.md §4.4;模块深潜正文沉淀在 docs/architecture/agenthub-module-deep-dives.md

入口 / 前端壳
控制面(含环境资产线)
Sandbox 执行面
出站依赖
强引用 / 同步调用
弱引用 / 异步
解析产生 / 跨库

系统架构总览

七个进程 + 三路出站。鼠标悬停任意组件高亮其直接关系;横向可滚动。关键心智: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 × Stage × Environment × Session

整个平台的产品语义骨架,一句话:Agent 定义"是什么",Revision 是它的不可变版本"哪一版",Environment 定义"跑在什么环境上"(自有版本线),Stage 是部署槽位——把「哪一版 × 哪个环境的哪个版本」这对选择钉住,Session 是运行实例——创建时从槽位解析并永久冻结这份选择。晋级、回滚、灰度,全部 = 拨槽位上的指针,从不修改已存在的版本或会话。

K8s 类比(团队已有的心智模型)

Agent(workload) ≈ Deployment 定义;Revision ≈ 镜像/ReplicaSet 版本(不可变制品);Environment ≈ ConfigMap+资源规格(部署配置层,不属制品——E-DEC6 拍板的核心:同一制品必须能跨 stage 配不同资源,冻进制品就破坏晋级语义);Stage ≈ 环境的部署指针;Session ≈ Pod(用完即弃的运行实例)。

两条版本线 + 一个配对点

Revision 和 EnvironmentVersion 是两条独立的不可变版本线(制品线 × 环境线),唯一的交汇点是 Stage 槽位上的配对:(revisionId, environmentId, environmentVersion)。这就是 E-DEC6 的本质——不是"一个大盒子装所有配置",而是两条线 + 槽位记配对,配对变更全程有账(repoint 账本)。

Session 的三条解析路径

stage 路径(主线):按槽位解析 revision 快照 ⊕ 钉住的环境版本配置 → 冻结进 session;② pin 路径:显式指定 revisionId,不经槽位、不带环境形态;③ legacy 路径:两者皆缺,回退 default_revision_id。冻结后永不回读(INV-9)——晋级/回滚只影响之后新建的 Session。

核心数据模型(E-DEC6 后)

packages/db/src/schema/pilot-v2-sessions.ts 逐字段整理;全库共 54 表,此图只画主链三层,其余(凭据 11 表 / 租户 12 表 / v1 遗留 11 表等)见 packages/db/src/schema/AGENTS.md

读图要点(数据模型的六条隐含约定)

runtime_templates 就是 environments

E-DEC1 拍板:environments 资源在 runtime_templates 表原地演化,没有另开新表、没有 kind 判别字段。图中"环境资产线"那个高亮框,实体上跟老的 Runtime Template 是同一张表。

版本配对靠数据库触发器

workload_stages.environment_idenvironment_version 必须成对移动,由触发器 workload_stages_environment_version_pairing(migration 0104)强制——不在 Drizzle schema 里,裸 SQL 改绑会被自动钉到目标环境 tip。

environment_versions 只有一列可以改

config/version/source/created_by/created_at 全部 write-once(POLICY LOCK 锁死)。唯一可变列是 provider_template_name——物化指针挪动不改变版本语义。

账本表故意不建外键

environment_version_repoints 的槽位引用是弱引用——审计记录要比槽位活得久,槽位级联删除后账本仍可读。

sessions 上的环境字段只是溯源

pilot_v2_sessions.environment_id / environment_version 创建时写入、之后只读——真解析结果冻结在 runtime_template_snapshot_json,历史读取不回源。

环境解析两条并行路径

老路径:revisions.runtime_template_id 发布时拍平快照。新路径:槽位绑定 environment,session 创建时解析。槽位 environment_id 为空 = 老路径,两者共存(OQ-10 强制化 P2 不做)。

模块深潜 ①:Sandbox 执行面(pod-agent × agent-worker)

数据模型之下,单个模块的具体工作方式。四个问题:怎么工作、怎么发版、怎么请求 gateway、env 怎么注入。全部落点已读码验证。

A · 两进程怎么分工 + 启动时序

pod-agent = OCI sidecar(:8790,监管+代理);agent-worker = 跑 agent 的业务进程(:8787)

  1. provision:control-worker 消费 session_startup_jobs,按槽位钉住的环境版本解析 Kanyun 模板名(E-DEC6 物化指针)+ TTL,调 Kanyun createSandbox(或 warm pool claim)
  2. start:control-worker → pod-agent POST /pilot/worker/start,请求体带 podAgentToken、sessionResources、workspaceSnapshot、agentBootstrap 和 PILOT_* env
  3. spawn:pod-agent buildWorkerEnv(process-manager.ts)组装子进程 env → 直接 spawn 镜像内 baked 的 agent-worker dist/server.js
  4. gates:worker 把 5 个启动 gate(worker / ingress / resource_mount / workspace_restore / agent_bootstrap)挂在自己的 /health
  5. 健康两跳:CP 只打 pod-agent 的 /pilot/worker/health(聚合端点);pod-agent 内部再打 worker 的 /health 并经 mapWorkerHealth 透传——透传丢失=卡 gate(历史真实 bug 点)
  6. ready 之后:keepalive(sdk-worker,60s 心跳、连续 3 失败标 unhealthy)+ health-watcher(30s probe,DB 共享计数 5 次熔断)双向监护

B · 怎么发版

单一部署通道(#1070 起),OSS 热更已退役

  1. 同一镜像:两进程由 apps/pod-agent/Dockerfile.managed-agent-base 一起 bake 进同一 OCI 镜像(/opt/pilot/* 保留命名空间)——改任一进程都必须重烤镜像
  2. 手动构建,实测 19–30 分钟build-sandbox-base-image.yml 是 workflow_dispatch-only;GitHub 侧只做 mirror 到 GitLab + 触发 Kanyun ImageBuild(真正 build 在 Kanyun 侧 clone+docker build)。#1070 分层让"只改代码"的发版复用 ~320MB 依赖层缓存(含 claude/codex CLI 二进制)
  3. 发版=环境版本线管道:platform-templates 页人工发版 → 每个跟随/fork 底座的环境异步产出 platform_release 版本(releaseRef 幂等)→ CAS repoint 只拨跟随 tip 的槽位;已 fork 环境刷新专属模板内容不退回共享底座(E5b/E-DEC6,07-12 test 实机验证过)
  4. 版本纪律 R1:repo 根 RUNTIME_VERSION 文件单点 + PILOT_RUNTIME_VERSION env 注入(两进程各自 runtime-version.ts 读取)+ CI 门禁
  5. 生效边界:运行镜像永远取 live Kanyun 模板;在跑会话/已 ready 池槽不受发版影响(快照冻结),新会话才吃新镜像

C · 怎么请求 gateway(MCP 出站)

gateway 是 sandbox 出站的「可信通道」——只管 MCP + OTLP relay,不碰 git/llm/oss

  1. worker 侧 mcpFetch()packages/sdk-worker/src/mcp-fetch.ts)带 PILOT_SESSION_TOKEN 打 gateway POST /mcp/v1/proxy——不经 pod-agent(它没有 /proxy/mcp 路由)
  2. gateway 5 步锁定:验 token(HMAC 共享 secret,两边不同源=全 401)→ 校验 path/target/method → 命中 session 创建时冻结的 allowedMcpTargets 快照(INV-2.1)→ Vault 解密 → sanitize+注入转发上游
  3. MCP 凭据在 gateway 注入,worker 全程不持有明文
  4. 顺带:pod-agent 手拼的 OTLP span 也经 gateway /otlp/v1/traces relay 上报(#1106);Claude Code CLI 的 OTel 导出同样钉死走这条 relay(#1111)

E · 监管关系:只管生、不管救

pod-agent 对 agent-worker 的看护边界(process-manager.ts,939 行)——所有"聪明"都刻意留在 control-worker 侧

  1. worker 不随容器启动:entrypoint 只起 pod-agent(root 写 /etc/hosts 劫持后 runuser 降权);worker 等 POST /pilot/worker/start 才被 spawn(baked dist)
  2. 单 worker 制:同 session 重复 start 幂等返回 reused;不同 session 拒绝"already supervising another worker";并发请求走 startChain 串行化防双 spawn;detached:true 进程组
  3. 预热两模式:REUSE(不杀,绑 session + init,健康掩码 WORKER_BOOTING,失败自动 fallback 冷启动)/ REPLACE 默认(杀了重拉)
  4. 崩溃语义=无自动重启:exit 监听只记录 exitCode + 立刻擦除 podAgentToken(内存副本);健康如实报 WORKER_PROCESS_EXITED → 恢复权在 health-watcher(5 连败熔断)/ revival——哲学同 R-ASB-08a:坏了换新沙箱比原地救更安全。⚠ 注意 token 有一份改名副本进了 worker env(见 D 表②层,待拍板)
  5. 健康四态:没 start 过→START_FAILED;init 中→掩码 BOOTING(防过早 ready);已退出→PROCESS_EXITED;活着→3s probe worker /health 透传 conditions(5 个 gate 全靠这条透传);probe 不通→报 BOOTING 不误判 failed
  6. 杀法:SIGTERM 整个进程组 → grace 窗口 → SIGKILL(连 CLI 子进程一锅端,不留孤儿)。重写提示:这个"薄"是资产——镜像重烤 20-30 分钟,逻辑越少的进程越不需要发版,别把智能下放到 pod-agent

D · env 变量注入分层

五层来源,注意各层的保护机制不同

内容 / 写点边界与保护
① 环境 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 secretconfig 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 实测经验)

模块深潜 ②:后台编排面(apps/control-worker)

最该重写、也最有资格重写的模块:护栏(CAS 乐观锁 / 事故提炼的 guard / 接线锁定测试)比看起来扎实,但两个入口文件已是事实上的上帝对象——worker.tsstart() 单方法 3714 行、orchestration/index.ts 5361 行 49 方法。诊断:两代代码风格叠加——干净的 register-with-deps 模式和 200-400 行的内联 handler 并存在同一个方法里,拆分范式已存在且被验证,缺的只是把老代码回填进去。下图是三条主链(startup / prompt 投递 / health 熔断)。

组织诊断与边界修正(证据)

  1. worker.ts 4503 行:ControlWorker 类只有 3 个方法,start() 占 3714 行——新 job(pool/revival/vault/ttl/schedule/env-propagation)全走干净的 register 函数,老代码(run:execute/finalize/abort 三个 handler 本体、MCP bootstrap、SSE reconnect、WeCom wiring)还内联在方法体里
  2. orchestration/index.ts 5361 行:同目录 8 个已拆出的小文件证明"按 phase 一个文件"模式可行;剩 finalize()(~500 行)、prepareWorkspace()(~434 行)等 6 个核心 phase 方法没拆
  3. "不写 sessions"说法被证伪(但可控):2 处窄写例外——worker.ts:974(清一个 jsonb key)、session-revival-worker.ts:452(带 lifecycleTransitionId CAS 守卫)。不是散乱越权,但重写时要承认
  4. 对领域包是"双通道"依赖:worker.ts 引 25 个 service 类之外,13 个文件直接 drizzle 摸表(runs 302 处 / sessions 139 处引用)——表结构变更会同时炸两层
  5. AGENTS.md 是全仓最好的 app 级文档之一(职责/队列清单/GP2 扩展指南/坑位),可直接当模板推广

重写就绪度:护栏与补课顺序

  1. 先补 webhook-delivery 测试:唯一"默认开着跑 prod + 有 spec + 零测试"的循环(114 行,mock 基建现成,成本最低优先级最高)
  2. 三个 run:* 内联 handler 的 guard 顺序抽成可测函数——全系统最核心的状态转移入口,目前只有拼装单测反推;套用 dispatcher-call-sites.test.ts 的"接线锁定"范式
  3. compaction + agent-memory 开 flag 前必须补课:默认关所以暂可接受零测试,但灰度前要补测试 + proposal 升格正式 spec(唯一硬性阻塞项)
  4. orchestration 按 phase 拆,从 finalize() 开刀(最大但已有 5 个测试文件打底,回归成本最低);然后 start() 收敛成"构造 deps → 一串 register"
  5. 表访问收敛放最后——先把行为拆干净再决定直接摸表要不要收进 service 层,顺序反了范围会失控
  6. 可直接依赖的护栏:advanceStatus CAS 范式(已复用到 sessions)、run-execute-guards(2026-05-26 真实事故提炼:一个 run 64 分钟被执行 95 次)、health-watcher 文件头注释(多副本无中心协调的范例,重写原样保留)、统一熔断 helper + control_loop 结构化日志两通用件(#1172,schedule/webhook 已迁移)

模块深潜 ③:领域核心(packages/control-plane)

41,227 行源码 / 17 个域目录 / 1917 个测试用例的"上帝包"(拆包目标已解冻)。诊断结论:域拆分本身是干净的(域间是无环 DAG),真正焊死中心的是根级文件 session-startup.ts(4478 行 / 66 个 async 方法)——它与 session-lifecycle 域存在全包唯一的值级循环依赖。下图是拆包依据的域依赖骨架。

上帝文件的真实形态(证据)

  1. 唯一的环session-startup.ts:117 值导入 SessionEventAppendBatchersession-event-append-batch.ts:53 值导入 SessionStartupError(class,非 type-only,import type 消解不掉)——拆包第一个必解的结
  2. 拆分线索是现成的:根 __tests__/ 下 19 个测试文件已按关注点命名切片(modelgate-injection / ingress-gate / stage-resolution / delivery-override / config-env-denylist…)——测试套件已经想清楚了内部结构,代码还没跟上
  3. 文档滞后:AGENTS.md 记录 ~3700 行/65 方法,实测 4478/66(E-DEC6 刚又加了 ~800 行;environment-service.ts 1906 行是第二大文件、当前增长最快)
  4. 写者纪律复核通过:sessions.status 写点仍然只有 2 个文件(6+1 处),delivery/termination 的 update 都不碰 status 列——INV-1 未被新代码破坏
  5. 包内类型边界靠约定不靠强制:4 个域直接 import 兄弟域源文件里的 type(CreateSessionInput 等),没有包内 contracts 层

重写就绪度:切法与第一刀

  1. 切法①保守(推荐先做):包内重组不拆包——下沉底座三件套 → 解值循环(SessionStartupError 挪独立文件)→ 按 19 个测试切片把 session-startup.ts 拆成 src/session-startup/ 子目录。零功能变更,1917 个用例直接回归,不动任何消费方 import
  2. 切法②激进(后置):物理拆包 @pilot/session-runtime + @pilot/workload-runtime + 叶子域留守——能用 exports 强制边界,但三个消费方 import 全要改,且需先做全量依赖二次验证
  3. 第一刀已明确:切法①前两步(下沉底座 + 解环)是任何后续方案的公共前置,零风险可立即动手
  4. 前置补课 4 条:profile-registry 把提案升格正式 spec(17 域里唯一没 spec 的);webhook(222 行)/vault/tenant 测试补厚;POLICY LOCK 模式从 environment 推广到 run 状态机 + worker-protocol 单写者;包内共享类型收敛 contracts.ts
  5. 测试分布不均是重写风险图:session-lifecycle 最厚(5689 行)可放心动;webhook/vault/tenant 薄,动之前先补

模块深潜 ④:出站网关(apps/gateway)

四个后端模块里 spec / 代码 / 测试三者最对齐的一个:9 个源文件职责边界干净、3318 行测试把 5 步锁定逐步覆盖、坑点全部写进 AGENTS.md 历史事故记录。唯一真实功能债 = spec §4 业务审计 span(spec 自己诚实标注 NOT IMPLEMENTED);其余都是"部署验证债"而非设计债。可作为其它模块重写的参照基线。

边界与组织(重写可直接照抄的部分)

  1. 只有 3 条路由:/healthz(不碰 DB)、/mcp/v1/proxy、/otlp/v1/traces;明确不碰 git/llm/oss、不 enqueue、不与 control-plane 通信(自己验 token 自己查 Vault)
  2. 鉴权单一实现session-auth.ts · verifySessionRequest 被 MCP proxy 和 OTLP relay 共用——两个入口零鉴权漂移,重写时新入口必须继续复用不许各写各的
  3. Vault 不自带实现:直接 import @pilot/control-planeVaultClientImpl(与 web/agent-hub/control-plane 四处同一个类,961 行单源)
  4. DB 最小权限:migration 0046 的 pilot_gateway NOLOGIN 角色,仅 5 表 SELECT + session_events INSERT + vault_credentials 7 列 UPDATE
  5. 可选优化 flag-off 默认:credential-cache(TTL+singleflight)与 undici 连接池,关闭时字节级等价旧行为

重写就绪度:缺什么、按什么顺序补

  1. 先问部署事实(不是代码活):线上 service=pilot-gateway 到底有没有 http.server span?DATABASE_URL_GATEWAY 真切到低权限用户了吗(代码只有回退链,PGlite 测不了真实 GRANT)?
  2. 冻结硬契约:wire 协议(隧道 method header)、15 个错误码 stable enum、流式不 buffer 语义、共享鉴权模式——这些 spec+测试双锁,直接照抄
  3. 决策 §4 审计债:12 个 MUST 字段 + 业务 span 一个都没实现(成功路径零审计,403 是唯一落 session_events 的分支)。要么补上,要么显式改 spec 降级——不许继续放"目标态"吃灰
  4. 清理 2 个 dead 错误码gateway_credential_refresh_* 因 Vault silent fallthrough 永不触发(#543)——实现 throw 路径或从 enum 退休
  5. 测试覆盖唯一空白 = §4 本身(功能没做所以没测试目标);其余 5 步各有独立测试、OTLP relay 连 gzip 不串味都有专项

模块深潜 ⑤:前端壳层(apps/agent-hub × apps/control-plane)

GP1 四规则(route 零逻辑 / 端点数=调用方数 / HTTP 不跑长任务 / 双端点补契约测试)是这层的宪法。实测:agent-hub 有 84 个非测试文件直接 import 数据库层(对照 control-plane 仅 5 个),权限引擎本体(cas-guard.ts 448 行 + token-cache ACL)活在壳层而非共享层;部分违规发生在规则落地之后——现有 CI 门禁对这一切零覆盖。下图是分层应然路径与规则映射。

越界地形图(5 档分级,全部 file:line 已建档)

  1. Tier1 · route.ts 内联写路径 13+ 处:最大单文件 projects/[id]/settings/route.ts 1367 行(webhook secret 轮换/credential CRUD 全内联);sessions/route.ts 有 ~180 行 prewarm-claim 编排
  2. Tier2 · lib 里的领域服务:platform-templates.ts(909 行 Kanyun 同步引擎)、v2-agents-dispatcher.ts(1012 行)、observability ×2(1266 行原生跨表 join)——admin 域 4320 行里约 1400 行是真领域逻辑
  3. 权限引擎在壳层:cas-guard.ts + token-cache.ts 的 ACL 矩阵——按规则 2 这该是共享层按 caller profile 判的东西,被几乎每条路由间接依赖,杠杆最高的下沉项
  4. 违规仍在发生:settings/route.ts、sessions/route.ts 的最近实质修改都晚于 GP1 落地(#1016)——规则在热点文件上没有被稳定执行,靠自觉不靠门禁
  5. 文档缺口:83 个 /api/v2 路由 REST 参考文档覆盖为 0(geistdocs 7 篇 + 2178 行 user-guide 全是 v1 面);15 对 web↔agent-hub 字节级拷贝只有 warn-only 检查

重写就绪度:先止血再搬家

  1. 第一步是 CI 门禁不是搬代码:扩展 check-layering-rules.sh——app/api/**/route.ts 与 lib/** 出现 db.insert/update/delete 或跨表 join 直接 FAIL(与已有"包根冻结"同一模式,成本低、立刻止血)
  2. 第二步下沉权限引擎(cas-guard + token-cache ACL + project-visibility → packages/web-platform)——规则 2 已指明目标包
  3. 第三步按体量×隔离度迁移:先读多写少调用面窄的分析引擎(platform-templates / observability),settings/route.ts 和 v2-agents-dispatcher.ts 最后动、各开专门 PR、先补规则 4 漂移测试
  4. 测试资产可依赖:75 个 route.test 契约测试精确锁住鉴权/校验/错误映射形状(薄壳收敛的护栏);但 Tier1/2 内嵌逻辑没有同等覆盖——有些不变式测试被迫写在了 BFF 层 PGlite 测试里
  5. 两份 AGENTS.md 都是活文档(control-plane 的 route→test 覆盖矩阵值得推广);无 .skip/.only、无 retry 配置——测试卫生良好

机制深潜:跨模块关键链路(十六条)

比模块更贴用户视角的机制链——"我配的东西如何/何时生效 / 我的产物去哪了 / 沙箱没了会怎样 / 怎么紧急关功能 / 什么能安全重试 / 挂了怎么查 / 什么时候被杀 / push 为什么 403 / 点停止发生什么"。全部 file·符号级实证,未验证处均已标注。

机制① env 变量五跳生效链

补深潜① D 表的"如何生效"——关键是每层的生效时机不同

  1. 跳 1:环境 envVars 保存 → 版本行冻结 → 物化进(必要时 lazy fork 的)专属 Kanyun 模板 container env(environment-materialization.ts · buildTemplatePatch)——生效=下一个新 sandbox
  2. 跳 2:startWorker 请求带 PILOT_*/凭据 → pod-agent buildWorkerEnv sanitize+显式 pick → worker 进程 env——生效=会话创建时(含 ModelGate 凭据;lease token 改名注入=待拍板冲突)
  3. 跳 3:worker spawn CLI 子进程 → filterProviderEnv 清洗 config.env(OTEL_* 全 scrub + AWS denylist)
  4. 跳 4:env_jit secret_ref 每 turn JIT 解析注入 CLI——全链唯一"turn 级生效"的层(rotate 立即被吃到)
  5. 验证铁律:Claude Code Bash 工具对子进程 env 有白名单,实机验证读 /proc/*/environ 不能 echo

机制② AI 产物管道

从 agent 写文件到用户点下载

  1. turn 开始先建基线:把已存在的"像交付物"的文件哈希存档——防止仓库自带文件被误报成 agent 产物
  2. 发现三级.agent/artifacts/manifest.json(显式声明)→ .agent/artifacts/ 目录 → 全工作区扫描(深度 6、白名单扩展名);.mcp.json 永远剔除(内含真凭据)
  3. 增量+打包:内容哈希对比、未变不重传;全部打进单个 general-bundle.zip(50 文件/50MB 预算,超了 partial 静默跳过——阶段性妥协,因 OSS proxy 单 key 限制)
  4. 上传:pod-agent OSS proxy per-object 预签名(凭据不进 worker),key 每次新版本号永不覆盖;100MB 硬顶
  5. 落账+下载:artifacts 表 run⊕session 双键;v2 下载=服务端解 zip 代理流式(CSP sandbox 响应头)
  6. 坑 ×4:413 不可重试(超大产物=该步永久失败,无分片兜底);v2 UI 只列 zip 类(diff 产物 v1 能下 v2 不能);"checkpoint"一词两义(delivery 步骤 ≠ run_checkpoints 表);零保留策略——OSS 每 turn 新 key 无限增长

机制③ Sandbox 过期重建与状态恢复

"session 没过期但 sandbox 没了"——revival 机制

  1. 触发是无感的:用户在 sandbox 已回收的会话发新 turn → turns route 自动 reviveSession,这条 pending prompt 暂存进 job payload,恢复后自动重放——用户只觉得"这条慢了点"
  2. revival worker:CAS lifecycleTransitionId 守卫(phase=reviving 防并发)→ 新 provision → 新 binding 行,旧行标 superseded_at 留审计(REV-INV-4)
  3. 状态来源=Model B 工作区快照:每 turn 结束 agent-worker 产出(git bundle + 外部文件 tar)存 session 行 → 新沙箱启动恢复,workspace_restore gate 把关
  4. 上限 3 次;conversation 会话专属路径(run_scoped 挂了走重投不走 revival)
  5. 活 bug 线索(已双向实证代码矛盾,待实机验证):Model B 上传/下载仍走直连 OSS 老路(workspace-snapshot.ts → getOssConfig),而 Model A 的注释明说这条路在 v2 凭据隔离沙箱里会挂死(Model A 因此已改走 pod-agent proxy,Model B 从未迁移)——revival 的状态恢复可能在真实 v2 沙箱里跑不通。另:Model B 快照每 turn 新 OSS key、旧的永久孤儿(spec 自认保留策略未决)

机制④ Ingress 公开端点(用户自配)

self_managed 把 sandbox 端口暴露公网

  1. 声明POST /api/runtime-templates/auto-create(API/SDK)带 workerPort + agentPorts[] + ingresses[](name/backendPort/hostPrefix/path)——普通业务方无自助 UI,admin 才有 platform-templates 面板
  2. 生效:auto-create 当场调 Kanyun 建路由;Revision 发布把 ingresses 冻结进快照;改模板不影响已发布 Revision
  3. ready 把关:有 ingresses 时平台自动注入 ingress gate,控制面主动探测 {url}/api/health 2xx 才放行——gate 是 PLATFORM_RESERVED,worker 伪造同名上报会被过滤(有整套防伪造回归)
  4. 取用:ready 后 session 详情 agentPublicEndpoints[],URL=https://{hostPrefix}.{基础域名}{path};想跨 sandbox 重建不漂移要传稳定 runtimeClaimName
  5. 坑 ×3:公开端点零平台鉴权(spec 责任边界划给业务方 worker 自己做);spec 说"与 sandbox 真实 URL reconcile 是 deferred"但代码已部分实现(真实 URL 优先、缺了降级公式)——spec 待回写;UDP deferred

机制⑤ 鉴权身份链(四凭证接力)

CAS cookie → platform token → session token → pod-agent token

  1. 分流点单一requirePlatformAuth——有 Bearer 走 platform token(scope+aud+effectiveProjects 现查),无 Bearer 走 CAS cookie(scopes='*');CAS debug 后门生产无条件 fail-closed(配置说什么都不听)
  2. platform token 的聪明设计:JWT 里不含 scope/项目——每请求 DB 现查(Redis 60s 兜底+显式失效),所以撤销/降权最迟 60s 生效、不用换 token;单用户上限 10 个活跃 token(创建闸门,非逐请求限流——后者未找到实现)
  3. 一枚 session token 打三份工:events 回写 / gateway MCP+OTLP / broker 兑换 factor 1——三处验的是同一把 secret(gateway 显式复用 resolveSessionTokenSecret,排查时可排除"两套密钥"假设)
  4. 双因子边界:pod-agent token 是 factor 2(timingSafeEqual),只活在 pod-agent 内存,worker 永远拿不到——"worker 能自证 session 身份,但不能自证是可信 pod-agent"
  5. 安全观察:session token 无主动 revoke——全仓 session_tokens 只有 INSERT 无 UPDATE 路径,仅靠 7 天 TTL 被动过期;泄漏场景只能等过期或换 session
  6. 403 排查两大误导项TOKEN_AUDIENCE_MISMATCH=拿错产品面的 token(pilot↔agenthub),不是 token 坏了;PROJECT_NOT_IN_TOKEN_SCOPE 刻意 403 非 404/空表,且 CAS 路径也复用这个 code——别被名字骗成"一定是 token 问题"
  7. 文档缺口:session token/双因子无独立 spec(权威=代码注释),platform token 有 pilot-sdk-auth.md

机制⑥ 紧急开关与灰度面

"怎么紧急关一个功能"——关键是分清哪类 30s 生效、哪类要重启

  1. FDC 动态 key ×6(30s 轮询自动生效):kill_switch_enabled、traffic_split_v2_percentage、canary_template_ids、project/agent 两个 allow-list、v2_delivery_enabled。决策优先级:kill_switch → 读取失败 fail-safe → allow-list(project>agent>template)→ 百分比边界 → hash bucket。改用 conso-cli fdc,没有 config 表
  2. env flag 要重启:bootstrap 读一次(如 WORKER_PROTOCOL_JOBS_POLLER_ENABLED)。⚠ 假热更新陷阱:PILOT_STARTUP_SPLIT_ENABLED 每次调用重读 process.env,形式像热更但 K8s 下 env 是启动常量
  3. 唯一反例:v2_delivery_enabled 是"env 压过 FDC",与其它 key 优先级方向相反——回退时容易搞反
  4. 前端 flag 真 bug:ENABLE_GENERAL_PROJECTS 类前端 flag 模块级缓存一次性读取,注释承诺"FDC 改值下次请求生效"实际不重启不生效——与 kill switch 是两套机制别混
  5. 轮询进程实测 3+1:web/agent-hub/control-worker 启动即轮询,control-plane 惰性(首次被打到才开始)

机制⑦ 幂等语义面(SDK 消费者视角)

哪些请求能安全重试、哪些会翻倍

  1. session 创建=opt-in 幂等:(project, scope, key) 部分唯一索引——任一为 NULL 完全不受保护(设计如此);命中返回 200+旧 session,字段不匹配 409
  2. prompt/followup 三层幂等:outbox event_id → 队列唯一索引 → worker 内存 cache(5min TTL)。已知 hole(spec 自认):L3 纯内存,worker 重启后同一 dispatch 重派可能重跑 side-effect
  3. 明确不幂等:admin retry(AR-INV-4,两次点击=两条 run,靠 metric 计入不拦截)、schedule run-now;大量创建类 POST(projects/vaults/secrets…)无幂等参数——未逐个验证有无唯一索引兜底
  4. schedule 触发:(scheduleId, firedAt) 全量唯一索引防 double-fire

机制⑧ 排障 trace 链

"一个 run 挂了怎么查"——好消息:链已缝合

  1. 两个字段分工:sessions.root_trace_id(创建时 1:1,查"会话怎么建的")vs session_events.trace_id(每轮 dispatch 1:N,查"某一轮")
  2. 重要更正:全链已缝合——浏览器 RUM(/api/*) → 前端 server → control-worker(pgboss CONSUMER span 靠 otel carrier 缝合)→ agent-worker → pod-agent,OTLP 经 gateway relay 出沙箱,test 已实证;"sandbox 未上报"是旧结论。prod 部署状态未验证
  3. UI 深链已存在:会话头"启动 Trace"+ 每条消息"查看 Trace/日志"(octopus-deeplink.ts 预填 trace-explorer)
  4. 改进项:故障三信号源互不 fallback——failure API 查不到直接 404 不会降级查 gate conditions;排查顺序(is_primary → gates → worker_protocol.failure)纯靠运维经验,spec 与代码一致地都没定义优先级

机制⑨ 会话回收配置面

"会话什么时候会被杀"

  1. 三参数(PUT /api/workloads/[id]/lifecycle-config + UI 对话框):idleAfterSeconds 默认 30min / idleGraceSeconds 5min / maxSessionLifetimeMs 24h;idleEnabled:managed_agent 默认开、self_managed 默认关
  2. 两条独立赛道:idleEnabled=false 只关 idle 判定,不影响 24h 硬顶回收——排查"为什么还是被杀了"先看这条
  3. R-ASB-05 五维度是懒检测:(a)ready (b)binding 活着 (c)TTL buffer 30s (d)keepalive 健康 (e)超龄——在 follow-up dispatch 时检查,不是后台 poller
  4. 孤儿窗口:sandbox TTL 到期不通知 session 状态机——"DB 说 ready、sandbox 已物理回收"的窗口靠 leak-reconciler 兜底(默认关、30min 一跑)
  5. prod 现状:idle/termination/leak 三 poller 代码默认全关(运行时值未用 conso-cli 独立核实);另记:24h 常量在两处独立定义=漂移点

机制⑩ 模型选择与覆盖

"这个会话用什么模型、怎么换"

  1. 创建时定:caller 显式 metadata.model > revision profile 快照 > workload 默认,写进 session 行
  2. 运行中改=唯一豁免通道:PATCH /api/sessions/:id/model → jsonb_set 单键更新(幂等短路、failed 409)
  3. 疑似断链(待单独排查):turns 组装派发配置的文件里 grep "model" 零命中——没找到任何代码把改后的 metadata.model 塞进下一 turn 的运行配置;前端 toast 承诺"下一条消息生效",静态证据不支持(v1/wecom 路径正常,仅 v2 session 路径存疑)
  4. ModelGate key 是按人的:按触发用户 LDAP 四级回落(缓存→表→revealKey→createKey),不支持项目共享 key;仅 claude-code 且无显式 override 才有部署环境静态 key 兜底,其余 vendor fail-fast
  5. 坑:改模型不打断在途 turn;PATCH 不校验 model id 有效性

机制⑪ systemPrompt 冻结

"改了提示词为什么没生效"的准确答案

  1. 优先级:revision profile 快照 > workload 默认(活读)> null——无 project 级回落(MAP-D8)
  2. first-write-wins 零破例:创建事务是唯一写入点(全仓 UPDATE 零命中)——运行中 session 永远不变,唯一解法=新建会话
  3. "有的会话变有的不变"不是随机:取决于各自绑定的 Revision 是否已把该字段冻结进快照——冻结过的,改 workload 默认对新会话也无效,要重新 publish Revision
  4. ⚠ 坑:GET session API 主动剔除 systemPrompt 字段——用户/排障者查不到当前生效值,两眼一抹黑
  5. v1(agents 表)与 v2(sessions 表)是两条同构独立链,排障先分清

机制⑫ MCP / Skills 安装→生效

"我刚装了个 MCP 为什么会话里没有"的准确答案

  1. session 列默认 NULL=跟随 project 活读——从没打开过 MCP popover 的会话,新装的项目级 MCP 下一轮 turn 就能看到
  2. 打开过 popover 并保存=永久冻结:两列被 PUT 成显式值后,project 层后续变更永远不可见——唯一解法=重开 popover 勾选或新建会话
  3. 按触发用户解析(不是创建者):共享会话里 A 发消息能用的 MCP,B 发可能被静默丢弃(个人凭据在 user_mcp_installs,解析失败只 warn 不阻断)
  4. .mcp.json 每轮 turn 现场重写——v2 没有"下一次 provision 才生效"的概念
  5. 坑:NULL(跟随)vs [](显式全禁)语义天差地别;Revision 快照冻结过该字段(哪怕空数组)则 project 变更对绑定它的会话全不可见

机制⑬ 用户带文件进会话(Model A)

只读输入文件的完整生命周期

  1. 上传(image 5MB / 其余 20MB,session 上限 20 个文件)→ 创建请求携带 → 创建事务冻结(唯一写入点,之后无任何 API 能追加)
  2. 启动挂载:envelope 经 PILOT_SESSION_RESOURCES env 进 worker → 经 pod-agent 代理逐个下载到 .session/inputs/ 只读 0444 → resource_mount gate 把关(单文件失败=整体 fail)
  3. 生效=下一次完整 provision:运行中会话加不进文件;reprovision/revival 会自动延续(重新下载,R-ASB-06)——但延续读取失败只 warn,新沙箱静默零挂载无提示
  4. ⚠ 坑:格式白名单形同虚设(ALLOWED_EXTENSIONS 定义了但 validateUpload 从未调用);.session/inputs/ 每次 provision rm -rf 重建(agent 写进去的东西必丢,Model B 快照也刻意排除它);envelope 含 ossUrl 明文进 env(spec 声称"不走 env"只对了一半)
  5. 别与旧 per-run 附件混淆:那条链每 turn 现场下载、上限 5 个,是另一套

机制⑭ Git 凭据选择与注入

"push 为什么 403"——v1/v2 是两套独立实现,行为不对称

  1. 两套模型:v1=control-worker 持 token 直塞 HTTP body(4 层 fallback:payload→runs 列→agents 列→user_git_credentials);v2=worker 不持 token,push 委托 pod-agent 每次双因子现取(不缓存)
  2. 403 自愈只有一种场景有:v1+GitLab OAuth(刷新重放一次);v1 PAT 和 v2 全系(GitHub+GitLab)一次 403 即终态,无刷新重试
  3. 真隐患(待修复):user_git_credentials 在 5 个查询点都不按 instance_url 过滤且无 orderBy(注释声称"字典序第一个"但代码没有对应实现)——同时注册 gitlab.com 和自建实例的用户可能拿错 host 的 token,403 后误判"broker 坏了"
  4. 不对称债:GitHub issuer 支持 preferUserGitToken(自动化触发时用户 PAT 提前),GitLab issuer 完全没实现该字段——spec 未标注
  5. 集体故障模式:server 级 GITLAB_TOKEN 静默兜底(wecom/旧 webhook 入口),轮换它=这批 run 同时 403,现象像"一堆不相关 run 集体失败"
  6. 另记:runs.encrypted_gitlab_token 不是遗留字段——三通道活跃写入

机制⑮ 长上下文管理

"聊太长怎么办"——机制齐全,但生产实测全关

  1. 三件套分工:L1 压缩(2min sweep 扫活跃会话、8 轮阈值触发、Haiku 60s、失败旧摘要续用)/ L2 跨会话记忆(压缩尾部顺带抽取、删除=墓碑 suppression、新会话首轮 recall 4000 字预算)/ checkpoint 是 v1 独立崩溃恢复系统(v2 revival 完全不碰它)
  2. 实测全关(octo-cli 实证非推测):双 kill-switch 在 online+test 都关着(prod 日志 07-10、test 07-12 都在打 disabled)——生产长会话完全不经过这套机制
  3. 更大的疑问(待确认):flag 关闭时 followup 轮 contextMessages 为空、仓内未找到 CLI resume 调用——轮间对话记忆疑似完全依赖 sandbox 内 Claude Code CLI 的本地会话持久化(未验证);这个假设成立与否决定长会话的真实风险等级
  4. 细节债:压缩阈值硬编码(方案说的 per-workload 可调未接入);召回 relevance 恒为 1(排序只剩 salience×时间衰减);room scope 写死 null;L2 metric 埋点缺失

机制⑯ Abort / 停止链

"点停止到底发生什么"——v1 先翻库再通知,v2 靠被打断者自报

  1. v1:abort 入队异步 → advanceStatus(failed) 翻库 best-effort 通知 worker——终态不依赖 sandbox 配合
  2. v2:同步写 outbox+队列 → poller 转发 POST /v2/abort → 按 turnId 精确打断 AbortController(检查点=进 provider 前 + 每个 stream chunk 之间;能否杀死执行中的单个长工具调用未验证)
  3. v2 终态靠被打断的 turn 自己上报 wp_aborted 事件——事件没发出来 run 就卡 running,兜底=worker 内 30s 硬超时合成 + health-watcher ~2.5min 巡检(且只覆盖 ready 态)
  4. 假成功隐患:abort 下发依赖与 prompt 同一个默认关的 poller flag——flag 关时 abort 只落库永不下发,前端却拿到 200 accepted(prod 已显式开;新环境检查清单+1)
  5. 不可打断点:delivery 进 create_mr 后内部 HTTP 没传 signal——PR 可能已创建成功才检测到 abort,整体报 failed 且无回滚;finalizing_* 状态 409 拒绝 abort
  6. abort 与 sandbox 回收完全解耦(回收走 TTL/watcher 另一条线)

关键路径速查(深潜图之外的五条)

session 拉起 / prompt 投递 / health 熔断三条已在深潜②成图;这里补齐其余五条会被问到的路径。

Delivery 交付管道(9 步,永不 throw)

  1. collect_diff → commit → push(经 pod-agent proxy,凭据不出 sidecar)
  2. create_mr → upload_artifact(manifest 驱动 + 内容哈希增量,缺清单才回退 convention scan)
  3. checkpoint → jira_reverse
  4. DP-INV-1:任一步失败 → classifyDeliveryError 转三元组,只 emit agent.delivery_* 业务事件——交付失败绝不炸 run

Post-ready 生命周期(四件套 poller)

  1. ready 后 lifecycle_phase 轴接管:active → idle → terminating → terminated(+reviving)
  2. idle-watcher(30s,默认关+ENFORCE 双闸)判定空闲 → termination-runner(10s,指数退避 10s×2ⁿ 封顶 300s,5 次后强制 confirm)执行终止
  3. leak-reconciler(30min,默认关)兜底找漏网 sandbox;revival 走独立队列(CAS transitionId 守卫)
  4. 只终止 run_scoped——conversation/prewarmed 不受影响

事件回流 SSE(读路径)

  1. worker POST /api/sessions/:id/events(Bearer + W3C traceparent)→ 落 session_events
  2. 前端 events/stream50ms DB 轮询(不是 Redis;Redis push 是默认关的可选加速)+ 10s heartbeat
  3. 断线续传靠 __seq 游标;v2 SSE 与 v1 的 Redis StreamRegistry 是两套,别混

Schedule 派发(用户级 cron)

  1. schedule_tick(默认 1min,env 可配)→ ScheduleDispatcher 四分支:workload / agent / inline_v1 / inline_v2
  2. UNIQUE(scheduleId, firedAt) 防 double-fire;幂等键透传下游
  3. 连续失败 5 次 auto-disable(#1172 起与 webhook 共用统一熔断 helper);已知洞:触发时活读配置、trigger 行不存快照(issue #1169 先行子项)

预热池(性能关键路径)

  1. replenish(1min cron)算 deficit → warm-slot job 暖槽;stage 池 warm 会话走完整 stage 解析(#1113),池指纹含槽位钉住的 (environmentId, environmentVersion) 对(#1185)——repoint 即 key-miss 换代;环境绑定槽位已恢复池化,仅 legacy NULL 钉维持 carve-out(钉上读不出解析将用的版本,入指纹会静默投毒)
  2. acquire 领取:指纹匹配 ready 槽 → 秒级交付;miss 则冷启动(20s~分钟级)
  3. 熔断:连续失败 5 → paused,需人工 resumeWarming;generation 指纹是缓存失效标记不是审计版本线(别与 E-DEC6 混)
  4. prod 现状:4 池 54 ready 槽真实在用(2026-07-12 实测)

未深潜的独立模块速查

  1. SDK 三包:pilot(legacy web client)/ agenthub(v2 Platform client 直连 control-plane)/ pilot-worker-sdk(sandbox 内 substrate,工厂零 IO)——wire 类型全 re-export 自 contracts,禁本地替身
  2. WeCom 子系统:全仓唯一 Redis leader-lease 并发模型(WSS 长连接),测试最厚的子系统之一
  3. Profile Registry(#931):不可变版本+stable_version 单指针(类 npm dist-tag)——缺 repoint 账表、17 域唯一没正式 spec、yank 竞态 follow-up 未关
  4. Memory 子系统(#963):L1 压缩事件 + L2 agent_memories;D-6 writer 分裂(落库在 control-worker);默认关、零测试、无正式 spec——开 flag 前三项必补
  5. Webhook 出站:outbox + deliveries 完整投递史(值得抄的账表范式);签名密钥 immediate-switch 是刻意语义