Skip to main content

P4.7 P1 收尾(内部信任认证 + name:action + manifest 填充 + Task 8)Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 让 /chat 的 read-class 业务 tool 第一次真正打通到 Hermes——通过内部信任认证绕过 runner-token(/chat 是可信容器,runner-token 是错配),补齐 name:action / manifest / MCP spawn 三个剩余阻塞。

Architecture: /chat 的 Hermes 跑在我们自己的可信 agent 容器里(agent 进程本地 spawn 的子进程),不是不可信的 remote runner。因此 tool-gateway 的 runner-token 认证对这条路是错配。方案 = gateway 加一条内部信任分支:agent 容器用它已有的 AGENT_SERVICE_SECRET(agent↔NestJS 既有对称互信密钥)作 bearer,gateway 用 timingSafeEqual 校验后从 body orgId 合成 req.runner 放行,限 read-class,下游 tenant-safety(AgentRun{id,orgId} 二元查找 / manifest 校验 / ActionPolicy / feature flag)全保留。remote-runner 路径走原 runner-JWT,零影响,两条入口各匹配自己的信任模型。

Tech Stack: NestJS 11(RunnerTokenGuard / ConfigService / crypto.timingSafeEqual)+ Python 3.11 agent(hermes_client secret-fence / tool_gateway_client / humanwork_mcp_server)+ Hermes v0.17.0 cli-config mcp_servers


背景与权威事实(动工前必读,全部 file:line 亲验于本 worktree HEAD 643a9028)

已完成(Task 1-7,长期分支 feat/specialist-value-output-p2plus:见 docs/superpowers/plans/2026-07-09-p4.7-phase1-read-tool-dispatch-implementation.md + 深挖文档 docs/superpowers/specs/2026-07-09-p4.7-p1-auth-gap-deepdive.md。T7 已产 slug 交集 resolveSpecialistToolsets(flag tool_dispatch_enabled 默认 OFF)。

本计划前的 4 个剩余阻塞(深挖确认,次序)

  1. 认证(本计划核心):MCP 子进程发的 bearer 是 runner-token(dev 容器落到无效 OpenRouter key),过不了 RunnerTokenGuard。→ 内部信任绕过。
  2. name:action:交集现只产 slug(["order_lookup"]),MCP server 要 name:actionorder_lookup:lookup)。权威源 = api/src/agent-api/tool-registry.ts AGENT_TOOL_REGISTRY[slug].actions
  3. manifest 填充:gateway 校验 run.metadata.runtimeManifest.allowedToolstool-gateway.service.ts:300-316),/chat draft 的 AgentRun 只写 inboundMessageRole/channelconversations.service.ts:3207)→ NOT_IN_MANIFEST 403。AgentRun 已存在conversations.service.ts:3183 createRun→save),只需往 metadata 填交集。
  4. Task 8 (MCP spawn)humanwork_mcp_server.py 从没被 Hermes spawn(无 cli-config mcp_servers 注册),HUMANWORK_MCP_TOOLS 无 producer。

关键接线点(亲验)

  • RunnerTokenGuardapi/src/runtime-control-plane/runner-token.guard.tscanActivate :33-57extractBearerToken :59-63,只注入 JwtService:31)。req.runner=完整 claims,req.user={platformRole:'runner',orgMemberships:[{orgId}]}
  • AGENT_SERVICE_SECRET config key = 字面量 "AGENT_SERVICE_SECRET"agent.client.ts:283 先例)。
  • timingSafeEqual 先例:api/src/tavus/webhook-verifier.service.ts:68(先长度检查再比较)。
  • ExecuteToolDto.orgIdtool-execution.controller.ts:18-20,body 可取(guard 先于 ValidationPipe 但 express.json 已解析 raw body)。
  • module runtime-control-plane.module.ts 已 import ConfigModule:102)→ guard 可加注入 ConfigService 无需改 module。
  • agent secret-fence hermes_client._runner_subprocess_env :250-343:allowlist 明文 :252-256、前缀 passthrough HERMES_/HUMANWORK_ :258、denied markers SECRET/TOKEN/KEY/... :259PLATFORM_API_URL 显式转发 :307-309AGENT_SERVICE_SECRET 双重被拦(无前缀 + 含 SECRET),必须显式转发
  • tool_gateway_client.py:token 取自 platform_api_token():72),header 设于 :102-1048 个 caller 共用 platform_api_token()——不动它,只在 tool_gateway_client 单独读 AGENT_SERVICE_SECRET。
  • humanwork_mcp_server.py:50 import execute_tool_via_gateway,自身不读 token,无需改代码(但子进程 env 要有 AGENT_SERVICE_SECRET)。

安全 rationale(记入代码注释):转发 AGENT_SERVICE_SECRET 给 Hermes 子进程不扩大暴露面——Hermes 跑在我们自己的可信容器(这是绕过前提),该 secret 本就是容器级 agent↔NestJS 共享密钥、主进程 env 本来就有,同一信任域内转发无新增风险。信任边界完全落在 timingSafeEqual(X-Agent-Secret, AGENT_SERVICE_SECRET) + read-class 限制 + 下游 tenant-safety。


File Structure

文件责任动作
api/src/runtime-control-plane/runner-token.guard.ts加内部信任分支Modify
api/src/runtime-control-plane/runner-token.guard.spec.tsguard 内部分支测试Create/Modify
api/src/conversations/tool-resolution.util.ts交集产 name:actionModify
api/src/conversations/tool-resolution.util.spec.tsname:action 测试Modify
api/src/conversations/conversations.service.tsmanifest 填充(createRun metadata)Modify
agent/hermes_client.pysecret-fence 转发 AGENT_SERVICE_SECRETModify
agent/tool_gateway_client.pybearer 优先 AGENT_SERVICE_SECRETModify
agent/main.py (_hermes_env)HUMANWORK_MCP_TOOLSModify
agent/Dockerfile 或 boot 脚本cli-config 注入 mcp_servers:{humanwork}Modify
api/src/runtime-control-plane/CLAUDE.md / agent/CLAUDE.md契约文档Modify

Task 1: NestJS gateway 内部信任分支(read-class only)

Files:

  • Modify: api/src/runtime-control-plane/runner-token.guard.ts
  • Test: api/src/runtime-control-plane/runner-token.guard.spec.ts

先 Read 真实文件对齐接口(别照抄本计划的行号,可能已漂移):runner-token.guard.ts 全文 + 一处 timingSafeEqual 先例 api/src/tavus/webhook-verifier.service.ts:60-70

  • Step 1: 写失败测试 —— 内部信任分支 3 个用例:有效 X-Agent-Secret+body.orgId → 合成 req.runner 放行;无 X-Agent-Secret → 落原 runner-JWT 路径(无 bearer 抛 UnauthorizedException);X-Agent-Secret 错误 → 不放行落原路径。
// runner-token.guard.spec.ts 增补
describe("internal-trust bypass (X-Agent-Secret)", () => {
const AGENT_SECRET = "test-agent-secret-123";
function makeGuard() {
const jwt = { verify: jest.fn() } as unknown as JwtService;
const config = { get: jest.fn((k: string) => (k === "AGENT_SERVICE_SECRET" ? AGENT_SECRET : undefined)) } as unknown as ConfigService;
return new RunnerTokenGuard(jwt, config);
}
function ctx(headers: Record<string, string>, body: Record<string, unknown>) {
const req: any = { headers, body, header: (n: string) => headers[n.toLowerCase()] };
return { switchToHttp: () => ({ getRequest: () => req }), _req: req } as any;
}

it("放行有效 X-Agent-Secret 并从 body.orgId 合成 req.runner", () => {
const guard = makeGuard();
const c = ctx({ "x-agent-secret": AGENT_SECRET }, { orgId: "org-1" });
expect(guard.canActivate(c)).toBe(true);
expect(c._req.runner).toMatchObject({ org_id: "org-1", role: "runner", aud: "runner" });
expect(c._req.user.orgMemberships[0].orgId).toBe("org-1");
});

it("X-Agent-Secret 错误 → 落原 runner-JWT 路径(无 bearer 抛 Unauthorized)", () => {
const guard = makeGuard();
const c = ctx({ "x-agent-secret": "wrong" }, { orgId: "org-1" });
expect(() => guard.canActivate(c)).toThrow(UnauthorizedException);
});

it("无 X-Agent-Secret 且无 body.orgId → 落原路径", () => {
const guard = makeGuard();
const c = ctx({}, {});
expect(() => guard.canActivate(c)).toThrow(UnauthorizedException);
});

it("有 X-Agent-Secret 但缺 body.orgId → 不放行(落原路径)", () => {
const guard = makeGuard();
const c = ctx({ "x-agent-secret": AGENT_SECRET }, {});
expect(() => guard.canActivate(c)).toThrow(UnauthorizedException);
});
});
  • Step 2: 跑测试确认失败

Run: cd api && npx jest runner-token.guard --silent Expected: FAIL(现 guard 构造只接一个参数 / 无内部分支)

  • Step 3: 实现内部信任分支

RunnerTokenGuard 构造函数加 ConfigServicecanActivate 顶部(取 req 后、extractBearerToken 前)加分支。用常量时间比较,先长度检查(照 webhook-verifier 模式)。缺 orgId 或 secret 不匹配则 fall through 到原 runner-JWT 路径(不 return false,让原逻辑决定)。

import { timingSafeEqual } from "node:crypto";
import { ConfigService } from "@nestjs/config";
// ...
constructor(
private readonly jwtService: JwtService,
private readonly config: ConfigService,
) {}

canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest<RunnerRequest & { body?: any; header?: (n: string) => string | undefined; headers?: Record<string, string> }>();

// 内部信任分支:可信 /chat agent 容器用 AGENT_SERVICE_SECRET 直连(read-class tool 执行)。
// Hermes 跑在我们自己的可信容器(非 remote runner),runner-token 对这条路是错配。
// 信任边界 = timingSafeEqual(X-Agent-Secret, AGENT_SERVICE_SECRET);orgId 从 body 来,
// 下游 tenant-safety(AgentRun{id,orgId} 查找 / manifest / ActionPolicy / flag)全保留。
const agentSecret = this.config.get<string>("AGENT_SERVICE_SECRET");
const provided = req.headers?.["x-agent-secret"] ?? req.header?.("x-agent-secret");
const bodyOrgId = typeof req.body?.orgId === "string" ? req.body.orgId : undefined;
if (agentSecret && provided && bodyOrgId && this.secretMatches(provided, agentSecret)) {
const claims: RunnerTokenClaims = {
sub: `internal-agent:${bodyOrgId}`,
org_id: bodyOrgId,
role: "runner",
aud: "runner",
};
req.runner = claims;
req.user = { platformRole: "runner", orgMemberships: [{ orgId: bodyOrgId }] };
return true;
}

// 原 runner-JWT 路径(remote-runner,零影响)
const token = this.extractBearerToken(req);
// ... 保留现有逻辑不变 ...
}

private secretMatches(provided: string, expected: string): boolean {
const a = Buffer.from(provided);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}
  • Step 4: 跑测试确认通过 + 原有 runner-JWT 测试不回归

Run: cd api && npx jest runner-token.guard --silent Expected: PASS(新 4 用例 + 原有全部)

  • Step 5: tsc + lint

Run: cd api && npx tsc --noEmit && npx eslint src/runtime-control-plane/runner-token.guard.ts Expected: EXIT 0

  • Step 6: Commit
git add api/src/runtime-control-plane/runner-token.guard.ts api/src/runtime-control-plane/runner-token.guard.spec.ts
git commit -m "feat(runtime): trusted internal-call bypass on RunnerTokenGuard for /chat read-tool dispatch (P4.7 P1)"

Task 2: 交集产 name:action(回改 T2)

Files:

  • Modify: api/src/conversations/tool-resolution.util.ts
  • Test: api/src/conversations/tool-resolution.util.spec.ts

先 Readtool-resolution.util.ts 全文(现 intersectReadTools 产 slug)+ api/src/agent-api/tool-registry.ts 确认 AGENT_TOOL_REGISTRY 形状({[slug]: {actions: string[], accessClass?, kind, ...}} —— 亲验字段名,深挖记 registry 有 actions,read/write 在 tool_binding schema 不在 registry)。

  • Step 1: 写失败测试 —— 交集结果从 slug 扩为 slug:action(每个 slug 的每个 read action 一条),未知 slug(registry 无)跳过,确定性排序。
// tool-resolution.util.spec.ts 增补(对齐真实导出名,先 Read)
describe("name:action expansion", () => {
it("把交集 slug 扩成 registry 里的 name:action,确定性排序", () => {
// 假设 registry: order_lookup.actions=['lookup'], shopify.actions=['getOrder','getProduct']
const out = expandToNameAction(["shopify", "order_lookup"]);
expect(out).toEqual(["order_lookup:lookup", "shopify:getOrder", "shopify:getProduct"]);
});
it("registry 无此 slug → 跳过(never throw)", () => {
expect(expandToNameAction(["nonexistent_tool"])).toEqual([]);
});
it("空输入 → 空", () => {
expect(expandToNameAction([])).toEqual([]);
});
});
  • Step 2: 跑测试确认失败

Run: cd api && npx jest tool-resolution.util --silent Expected: FAIL(expandToNameAction 未定义)

  • Step 3: 实现 expandToNameAction(从 AGENT_TOOL_REGISTRY 查 actions,纯函数,never-throws,确定性 sort)。决定:是新增 expandToNameActionresolveSpecialistToolsets(T7)调用,还是直接改 intersectReadTools 返回。推荐新增独立纯函数,保持 intersect 与 expand 单一职责分离。
import { AGENT_TOOL_REGISTRY } from "../agent-api/tool-registry";

/** 把 read-class 交集 slug 扩成 MCP server 要的 `name:action` 列表。
* 权威源 = AGENT_TOOL_REGISTRY.actions。registry 无此 slug → 跳过。纯函数,never-throws。 */
export function expandToNameAction(slugs: string[]): string[] {
const out: string[] = [];
for (const slug of slugs) {
const def = AGENT_TOOL_REGISTRY[slug];
if (!def?.actions) continue;
for (const action of def.actions) out.push(`${slug}:${action}`);
}
return out.sort();
}
  • Step 4: 跑测试确认通过(若 registry 真实 actions 与假设不符,改测试期望值对齐真实 registry,不改实现)

Run: cd api && npx jest tool-resolution.util --silent Expected: PASS

  • Step 5: tsc + lint

Run: cd api && npx tsc --noEmit && npx eslint src/conversations/tool-resolution.util.ts Expected: EXIT 0

  • Step 6: Commit
git add api/src/conversations/tool-resolution.util.ts api/src/conversations/tool-resolution.util.spec.ts
git commit -m "feat(conversations): expand read-tool intersection to name:action from tool registry (P4.7 P1)"

Task 3: manifest 填充 + name:action 穿到 agent(回改 T7 dispatch site)

Files:

  • Modify: api/src/conversations/conversations.service.ts(createRun metadata + toolsets wire)
  • Test: api/src/conversations/*.spec.ts(conversations dispatch 相关 spec)

先 Readconversations.service.ts 的 createRun 调用点(约 :3183,metadata 现只 inboundMessageRole/channel:3207)+ resolveSpecialistToolsets(T7)+ .chat() dispatch site(约 :3348 main / :6852 regen)+ run-lifecycle.service.ts createRun 的 metadata 类型。注意 runtimeManifest.allowedTools 的确切结构(Read runner-control-plane.service.ts:245 的写法作模板——它是 runner 路径的权威写法)。

设计resolveSpecialistToolsets 现返 ["humanwork"](toolset 名,flag ON 时)。现需两个衍生值:(a) -t 用的 toolset 名 ["humanwork"](不变,穿 wire toolsets);(b) manifest 的 allowedTools = expandToNameAction 的结果(写进 createRun metadata);(c) HUMANWORK_MCP_TOOLS 也要 = name:action 列表,需穿到 agent(T6 的 toolsets wire 字段可能不够,需评估加字段 or 复用)。决策点:name:action 列表既要进 NestJS 本地的 manifest metadata,又要传到 agent 设 HUMANWORK_MCP_TOOLS。最简 = 扩 wire 加一个 mcpTools: string[](name:action),或复用把 toolsets 语义改成 name:action + 另传 toolset 名。先 Read T6 的 wire 字段(AgentChatRequest/buildV1ChatPayload/agent ChatRequest)再定,保持 paired PR(agent+api 同步)。

  • Step 1: 写失败测试 —— flag ON 且有交集时:createRun 的 metadata.runtimeManifest.allowedTools = name:action 列表;wire payload 带 name:action(供 agent 设 HUMANWORK_MCP_TOOLS)。flag OFF:metadata 无 runtimeManifest(字节等同)。
// 对齐真实 spec 结构,先 Read 现有 conversations dispatch spec
it("flag ON + 交集非空 → createRun metadata 写 runtimeManifest.allowedTools (name:action)", async () => {
// arrange: tool_dispatch_enabled ON, published binding∩skill = order_lookup
// act: 驱动 main dispatch
// assert: createRun 调用参数 metadata.runtimeManifest.allowedTools 含 "order_lookup:lookup"
});
it("flag OFF → createRun metadata 无 runtimeManifest(字节等同)", async () => {
// assert: metadata 只有 inboundMessageRole/channel
});
  • Step 2: 跑测试确认失败

Run: cd api && npx jest conversations --silent -t "runtimeManifest" Expected: FAIL

  • Step 3: 实现 —— (a) resolveSpecialistToolsets 或新 helper 同时返 { toolsetNames: ["humanwork"], mcpTools: string[] }(mcpTools=expandToNameAction);(b) createRun 时若 mcpTools 非空则 metadata.runtimeManifest = { allowedTools: mcpTools }(照 runner-control-plane 模板);(c) wire payload 传 mcpTools。严格 flag-gated:flag OFF 时 mcpTools=[],不写 runtimeManifest,wire 字段缺省 → 字节等同。两 dispatch site(main + regen)都改。

  • Step 4: 跑测试确认通过 + conversations 全 suite 不回归

Run: cd api && npx jest conversations config-assets feature-flags --silent Expected: PASS(含 flag-OFF 字节等同)

  • Step 5: tsc + lint

Run: cd api && npx tsc --noEmit && npx eslint src/conversations/conversations.service.ts Expected: EXIT 0

  • Step 6: Commit
git add api/src/conversations/conversations.service.ts api/src/conversations/*.spec.ts api/src/common/agent.client.ts
git commit -m "feat(conversations): populate AgentRun manifest allowedTools + wire name:action to agent, flag-gated (P4.7 P1)"

Task 4: agent wire 收 name:action + 设 HUMANWORK_MCP_TOOLS(paired with Task 3)

Files:

  • Modify: agent/main.py(ChatRequest 加 mcpTools 字段 + _hermes_envHUMANWORK_MCP_TOOLS
  • Modify: agent/hermes_client.py(secret-fence 转发 AGENT_SERVICE_SECRET + HUMANWORK_MCP_TOOLS 若需)
  • Test: agent/evals/test_hermes_client_subprocess.py 或对应 subprocess env 测试

先 Readagent/main.py ChatRequest(T6 加的 toolsets)+ _hermes_env(约 :504-516)+ hermes_client._runner_subprocess_env:250-343,PLATFORM_API_URL 转发 :307-309 作模板)+ 现有 subprocess env 测试。

  • Step 1: 写失败测试 —— _runner_subprocess_env 转发 AGENT_SERVICE_SECRET 到子进程 env;_hermes_env 在 mcpTools 非空时设 HUMANWORK_MCP_TOOLS
def test_subprocess_env_forwards_agent_service_secret(monkeypatch):
monkeypatch.setenv("AGENT_SERVICE_SECRET", "sekret")
env = _runner_subprocess_env(base_env={}, ...) # 对齐真实签名
assert env["AGENT_SERVICE_SECRET"] == "sekret"

def test_hermes_env_sets_mcp_tools_when_present():
env = _hermes_env(req_with_mcp_tools=["order_lookup:lookup"], ...)
assert env["HUMANWORK_MCP_TOOLS"] == "order_lookup:lookup"

def test_hermes_env_omits_mcp_tools_when_empty():
env = _hermes_env(req_with_mcp_tools=[], ...)
assert "HUMANWORK_MCP_TOOLS" not in env
  • Step 2: 跑测试确认失败

Run: cd agent && python -m pytest evals/test_hermes_client_subprocess.py -v (对齐真实测试文件名) Expected: FAIL

  • Step 3: 实现 —— (a) _runner_subprocess_env 显式转发 AGENT_SERVICE_SECRET(照 PLATFORM_API_URL 模式,加安全 rationale 注释);(b) ChatRequest 加 mcp_tools: list[str] = [](snake_case wire,对齐 Task 3 的 camelCase mcpTools);(c) _hermes_envreq.mcp_tools 非空时 env["HUMANWORK_MCP_TOOLS"] = ",".join(req.mcp_tools)

  • Step 4: 跑测试确认通过 + echo-guard 回归

Run: cd agent && python -m pytest evals/ -k "hermes or echo or subprocess" -v Expected: PASS(echo-guard 54 绿不回归)

  • Step 5: Commit
git add agent/main.py agent/hermes_client.py agent/evals/test_*.py
git commit -m "feat(agent): forward AGENT_SERVICE_SECRET to hermes subprocess + set HUMANWORK_MCP_TOOLS (P4.7 P1)"

Task 5: tool_gateway_client bearer 优先 AGENT_SERVICE_SECRET

Files:

  • Modify: agent/tool_gateway_client.py(bearer 取值,:72 + :102-104
  • Test: agent/ 下 tool_gateway_client 测试(先 Read 有无)

先 Readagent/tool_gateway_client.py 全文 + agent/platform_auth.py(确认不动它)。

  • Step 1: 写失败测试 —— tool_gateway_client 有 AGENT_SERVICE_SECRET 时用它作 bearer,缺失时回退 platform_api_token()。
def test_gateway_uses_agent_service_secret_first(monkeypatch):
monkeypatch.setenv("AGENT_SERVICE_SECRET", "agent-sekret")
monkeypatch.delenv("HUMANWORK_RUNNER_TOKEN", raising=False)
# 断言发出的 Authorization header == "Bearer agent-sekret"

def test_gateway_falls_back_to_platform_token(monkeypatch):
monkeypatch.delenv("AGENT_SERVICE_SECRET", raising=False)
monkeypatch.setenv("PLATFORM_API_TOKEN", "plat-tok")
# 断言 Authorization header == "Bearer plat-tok"
  • Step 2: 跑测试确认失败

Run: cd agent && python -m pytest -k tool_gateway -v Expected: FAIL

  • Step 3: 实现 —— 在 tool_gateway_client 取 token 处:token = os.getenv("AGENT_SERVICE_SECRET") or platform_api_token()只改这一个文件,platform_auth 不动(保护其他 7 caller)。

  • Step 4: 跑测试确认通过

Run: cd agent && python -m pytest -k tool_gateway -v Expected: PASS

  • Step 5: Commit
git add agent/tool_gateway_client.py agent/test_*.py
git commit -m "feat(agent): tool_gateway_client prefers AGENT_SERVICE_SECRET bearer for trusted /chat dispatch (P4.7 P1)"

Task 6: Task 8 —— cli-config 注入 mcp_servers:{humanwork}

Files:

  • Modify: agent/Dockerfile 或 boot/entrypoint 脚本(cli-config 注入)
  • 参考:内存记的 langfuse plugin re-assert 模式

先 Readagent/Dockerfile + 任何 boot 脚本(找 langfuse plugin enable 的 re-assert 模式作模板,|| true 幂等)+ Hermes cli-config 路径(HERMES_HOME / /opt/data/config.yaml)+ spike 文档 docs/superpowers/specs/2026-07-09-p4.7-hermes-runtime-spike-results.md(#2 确认 mcp_servers key 成 -t-selectable)。

关键约束:boot 时 re-assert 写 mcp_servers:{humanwork:{command,args,env}} 进 cli-config,别 clobber operator 可能已有的 github/notion/slack(merge 不 overwrite)。command = spawn humanwork_mcp_server.py,env 传 PLATFORM_API_URL(stdio 子进程会继承 Hermes env,但显式列更稳)。

  • Step 1: 确认 humanwork_mcp_server.py 的 stdio 启动命令(Read 它的 __main__ + docstring 确认调用形态:python humanwork_mcp_server.py 或模块形式)。

  • Step 2: 写 boot 注入逻辑(idempotent merge 进 cli-config mcp_servers)。若有 boot 脚本照 langfuse re-assert 模式;env 契约:HUMANWORK_ORG_ID/HUMANWORK_RUN_ID/HUMANWORK_MCP_TOOLS/PLATFORM_API_URL/AGENT_SERVICE_SECRET_hermes_env+secret-fence 在 per-request 提供,cli-config 的 server env 段只需静态项(如 command/args)。

# 注入进 cli-config(示意,实际 merge 别 clobber 已有 servers)
mcp_servers:
humanwork:
command: python
args: ["/app/agent/humanwork_mcp_server.py"]
# env 由 Hermes 子进程从其自身 env 继承(_runner_subprocess_env 已转发所需项)
  • Step 3: 验证注入幂等(boot 跑两次 cli-config 不重复、不丢 operator 已有 server)。若有本地 hermes 二进制:hermes chat -Q -t humanwork 看是否 list 到 tool(无 Unknown warning)。

  • Step 4: Commit

git add agent/Dockerfile agent/*.sh
git commit -m "feat(agent): register humanwork MCP server in hermes cli-config at boot, idempotent (P4.7 P1 Task 8)"

Task 7: 契约文档更新

Files:

  • Modify: api/src/runtime-control-plane/CLAUDE.md(若存在;否则 api/CLAUDE.md 相关段)

  • Modify: agent/CLAUDE.md

  • Step 1: 文档写实(grep 核实所有 symbol 存在再写):

    • RunnerTokenGuard 内部信任分支(AGENT_SERVICE_SECRET → read-class /chat dispatch,下游 tenant-safety 保留,两条入口各匹配信任模型)。
    • agent secret-fence 转发 AGENT_SERVICE_SECRET 的 rationale。
    • tool_gateway_client bearer 优先级。
    • Task 8 cli-config 注入契约(别 clobber operator servers)。
    • agent/CLAUDE.md:46「Task 8 still wires...」改为已完成。
  • Step 2: Commit

git add api/**/CLAUDE.md agent/CLAUDE.md
git commit -m "docs(p4.7): document internal-trust auth + MCP registration contracts (P4.7 P1)"

验证策略(P4.7 P1 是"第一次打通",非回归)

  • 每个 seam 有单测(guard 分支 / name:action / manifest 填充 / secret-fence 转发 / bearer 优先 / cli-config 幂等)。
  • 端到端(env 允许):tool_dispatch_enabled ON + 一个 org 有 published read tool_binding(toolSlug 在某 matched skill 的 tools[] 里)→ 驱动 /chat → 观察 Hermes 选 -t humanwork → MCP handler → gateway(内部信任放行)→ 执行。这条链路从没通过,是第一次通。
  • 本地全栈受 schema-drift 阻 → 更轻验证:humanwork_mcp_server.py standalone stdio + mock gateway + hermes chat -Q -t humanwork 看 list+call。

final review(本计划完成后)

P4.7 P1 级 final code-reviewer,连同 Task 4-7(原计划 agent+wire+dispatch 批未做 final review)。动 /chat wire + gateway 认证 → 必派 code-reviewer 二次审。重点验证:flag-OFF 字节等同、内部信任分支只 read-class 且下游 tenant-safety 未削弱、remote-runner 原路径零回归、secret 转发无扩大暴露面、name:action 权威源正确(registry 非 tool_specs.py)。

follow-up(非 P4.7 P1)

  • write 审批半场 = P4.7 P2(ExpertQueueItem messageId 缺口)。
  • tool_permissions 退役、tool_exec.py/tool_specs.py 死代码清理、skill 编辑器 tools[] 编辑期约束。
  • 内部信任分支的更强收口(如将来加 mTLS / 专用 scoped token 替代对称密钥)——当前对称密钥对可信容器足够,记为可选加固。