Skip to main content

P4.7 P1 认证缺口深挖 —— 修正后的完整图景(2026-07-09)

交接文档把 P4.7 P1 收尾窄化成"定一个认证方案 + 做 Task 8"。用户要求"先深挖再定"。深挖的净结果:原路线(方向 A + 甲,复用 gateway)没有被推翻,但认证缺口的精确形态与真实阻塞次序需要修正。本文档记录三个 Explore 的核实结论 + 一处我一度相信但被证伪的错误判断,供决策认证方案时参考。全部只读核实,未改代码。

一处必须诚实记下的错误判断(已被同一 Explore 自我修正)

深挖过程中我一度相信:「/chat draft 流程不创建 AgentRun,runId=conversationId,gateway 第一步 agentRunRepo.findOne({id:runId,orgId}) 就会 404 —— 这比认证更前置」。这是错的。

真相(conversations.service.ts 亲验):

  • conversations.service.ts:3183 this.runLifecycle.createRun({ runKind:"conversation_draft", ... })run-lifecycle.service.ts:102 runRepo.save(run) 真的持久化一行 AgentRun
  • guard 是 if (dispatchOrgId && this.runLifecycle)(:3177)。runLifecycle 声明为 @Optional()(:265), RuntimeControlPlaneModuleconversations.module.ts:95 被 import → 生产环境实际被 provide → guard 通过 → AgentRun 真实存在。
  • draftRunId = run.id(:3212)→ runId: draftRunId.chat()(:3348)→ wire run_idagent.client.ts:478)→ agent req.run_idmain.py:321)→ _hermes_envHUMANWORK_RUN_IDmain.py:508)。

所以 gateway 的 AgentRun{id:runId, orgId:dispatchOrgId} 查找会通过,不会 404。内存里"M2 createRun 无活 caller"只对 M2 runner surface 成立,对 /chat draft 不成立(draft 路径有自己的 createRun/startRun)。

教训(Map≠Territory)@Optional() 的依赖不等于"运行时缺失"——要看它所在 module 的 import 图。我因为看到 @Optional() + 内存里"createRun 无活 caller"就误判 AgentRun 不存在,差点据此把路线推向重构。

例外(不影响 P4.7 P1):Expert↔AI consultation 路径创建 AgentRun(runId:null/randomUUID() fallback,expert-agent-thread.service.ts:478-482),但该路径明确不在 tool dispatch 范围内(T7 只接 main + regen 两 dispatch site,不接 chatStream consultation)。

复用 runner gateway 跑 /chat read tool —— 真实阻塞次序(修正后)

三层阻塞,但次序和严重度与第一直觉不同:

阻塞 0(最前,就是 Task 8 本体):MCP server 进程今天根本不启动

  • agent/humanwork_mcp_server.py 是完整的 stdio MCP server(stdio_server()_call_tooldispatch_toolexecute_tool_via_gateway),但没有任何代码把它注册进 Hermes 的 cli-config mcp_servers:(grep 全 agent/ 只有自引用 + CLAUDE.md prose)。
  • HUMANWORK_MCP_TOOLS env 无 producer(只有 consumer humanwork_mcp_server.py)。_hermes_envmain.py:504-516)不 emit 它。
  • 后果:Hermes 从不 spawn 进程 C;即便 spawn,也 advertise 零 tool,每个 dispatch_toolTOOL_NOT_CONFIGURED
  • 这就是未做的 Task 8agent/CLAUDE.md:46 明写)。不是新发现的架构问题。

阻塞 1:manifest allowedTools 无来源(数据填充,非架构分叉)

  • gateway ToolGatewayService.execute 校验 run.metadata.runtimeManifest.allowedToolstoolName.action,否则 NOT_IN_MANIFEST → 403(tool-gateway.service.ts:300-316)。
  • allowedTools 只在 runner 路径写runner-control-plane.service.ts:245);/chat draft 的 AgentRun metadata 只有 inboundMessageRole/channelconversations.service.ts:3207)。
  • 关键:AgentRun 已存在(见上),这只是往它的 metadata 填数据(binding∩skill 交集的 name:action),不是造新的 AgentRun 生命周期。是 Task 8 的自然组成部分(NestJS 在 createRun 时把交集写进 metadata)。

阻塞 2:认证(精确形态,最后才轮到)

  • tool 执行请求从进程 Chumanwork_mcp_server.py,agent 容器内 Hermes 的孙进程)发出,Authorization: Bearer {platform_api_token()}
  • token 来源:_runner_subprocess_envhermes_client.py:319-342)从 agent 容器 ambient envHUMANWORK_RUNNER_TOKEN/HUMANWORK_MODEL_GATEWAY_TOKEN,过 secret-fence allowlist 传给子进程。不是 per-request 注入
  • gateway 的 RunnerTokenGuard 要求 JWT aud==='runner' && role==='runner' && org_idrunner-token.guard.ts:42-49)。
  • dev 容器现状(Railway 亲验):agent 容器 HUMANWORK_RUNNER_TOKEN PLATFORM_API_TOKENplatform_api_token() 落到 HUMANWORK_MODEL_GATEWAY_TOKEN(一个 OpenRouter key)——不是有效 platform bearer,过不了 RunnerTokenGuard。
  • 所以认证缺口的精确形态不是"要不要建 runner token 签发基础设施"这种架构大问题,而是"agent 容器需要一个能过 gateway 的 bearer"这个界定清楚的子问题。

认证方案选项(次要于确认路线,供决策)

前提:tenant-safety 逻辑(manifest 校验 / ActionPolicy / ToolCall 账本 / feature flag)全在 ToolGatewayService.execute 里、与 runner 无关(executor 本身只查 registry + 凭据完整性,无这些前置)。runner-surface 真正特有的只有认证入口。所以选项都是"用什么身份进 gateway 的 tenant-safety 逻辑"。

  • (A) 给 agent 容器配一个能过 RunnerTokenGuard 的 bearer:改动最小(agent 已从 ambient env 取 token 过 secret-fence,只是 dev 里那个无效)。但共享多租户 agent 用单个容器级 runner token;org_id 从 body(ExecuteToolDto.orgId)来而非 token claim。gateway 隔离仍靠 AgentRun{id,orgId} 二元查找 + manifest + policy 兜底。需一条运行时签发路径(现只有 dev-only CLI mint-runner-token.ts,且硬依赖 RunnerManifest+Registration 重资产——语义错配共享 agent)。
  • (ii) gateway 加"可信内部调用"分支:给 RunnerTokenGuard 加一条——带可信内部 header(如 AGENT_SERVICE_SECRET)时从 body orgId 合成 req.runner,限 read-class + 保留 manifest/policy/ledger/flag。有 AgentTokenGuard dev-bypass(agent-api.guard.ts:38-62,共享密钥→合成 payload)作同构模板。代价:gateway 第一次接受非 runner 调用方,信任面扩大一点。
  • (iii) 走 agent-api surface:在 AgentApiControllerv1/agent-api,已受 AgentTokenGuard)下新增 POST tools/execute 调已 export 的 ToolGatewayService.execute。agent 用它今天已在用的凭证(过 AgentTokenGuard)。:AgentTokenGuard 让共享 agent 过关也只靠 dev-bypass(IS_DEV 且 token==PLATFORM_API_TOKEN)——staging/prod 下共享 agent 既过不了 RunnerTokenGuard 也过不了 AgentTokenGuard,无任何 scoped JWT,仓库无给共享 agent 铸 agent JWT 的路径。所以 (iii) 与 (A) 撞同一堵墙(共享 agent 在非 dev 如何证明身份),只是换了个 guard。

共同的深层前提问题:无论哪个方案,"共享多租户 /chat agent 容器在 staging/prod 如何向 NestJS 证明身份"是普遍缺失,不是 tool 执行独有(agent 现在调 v1/agent-api 也是靠 dev-bypass 过的)。这个问题值得单独想清楚,可能影响方案选择。

建议的收尾次序(认证方案定后)

  1. 回改 T2/T6/T7 补 name:action:交集现只产 slug(["order_lookup"]),MCP server 要 name:actionorder_lookup:lookup)。权威源 = api/src/agent-api/tool-registry.ts AGENT_TOOL_REGISTRY.actions别用 agent 侧 tool_specs.py 死路径)。
  2. manifest 填充(阻塞 1):NestJS 在 createRun 时把交集写进 metadata.runtimeManifest.allowedTools。纯数据、无架构分叉、无认证依赖 —— 可最先做
  3. Task 8(阻塞 0):cli-config 注入 mcp_servers:{humanwork}(boot re-assert,|| true 幂等,别 clobber operator 的 github/notion/slack)+ _hermes_envHUMANWORK_MCP_TOOLS
  4. 认证(阻塞 2):按拍板的方案落地。
  5. P4.7 P1 级 final code-reviewer(连同 Task 4-7)。

端到端验证策略(不变)

本地全栈受 schema-drift 阻。更轻的验证:humanwork_mcp_server.py standalone stdio + mock gateway + hermes chat -Q -t humanwork 看 list+call。这条链路从没跑通过(第一次通,非回归)。