第八部:证明与守卫——怎样知道系统真的成立
写完了所有运行时模块,最后要回答一个工程师终极问题:你怎么知道它真的成立?从credentials不进日志到telemetry侧车定位,从trajectory replay到mock snapshot测试,从per-package invariant守卫到分层安全模型,最后这本书本身的更新方法论——这一部讲"证明"这件事。
![[图片占位:一座堡垒,护城河(sandbox)、城墙(permission)、瞭望塔(telemetry)、守卫(invariant)、城门检查(credentials)各层防御。工程师拿着检查清单巡逻。顶部有旗帜写着VERIFIED。色调:堡垒灰色+金色守卫+蓝绿色护城河光。]](/static/images/handbook/deepseek-harness-internals/parts/08-runtime-proof.png)
展开阅读路线与实验入口
你读完了前七部,知道CLI怎么启动、Cordis怎么组装、Turn怎么跑、工具怎么调度、事件怎么落盘。你甚至能画出整个系统的调用图。但有个问题你没法回答:
你怎么知道它真的成立?
密钥会不会泄漏到日志里?Telemetry采集会不会反过来污染会话?崩溃后能不能重放验证?测试是不是真的覆盖了真实路径?每个包自己的守卫是什么?安全是不是一层纸糊的墙?还有——这本书本身,当代码commit前进时,怎么保证你读到的不是过期的谎言?
这一部不引入新功能。这一部讲”证明”。
这一部解决什么
读完这七章,你能回答七个证明相关的问题:
- 为什么credentials用引用而非值传递,空字符串为什么永远不被当作已配置密钥
- 为什么telemetry/feedback只是观测侧车,它永远不能创建或恢复会话
- SessionQueryEngine怎样做重放验证,traceEvent怎样追踪事件替换链
- ACP Snapshot Harness怎样启动真实子进程做record/replay,22种LLM mock行为覆盖哪些故障
- CI gate怎样强制每个包必须有自己的invariant.ts companion
- 安全为什么是Trust→Permission→Sandbox→Network四层,每层都不假设其他层完美
- 这本书本身的更新方法论:为什么锁commit不是为了停在旧版,而是让每个”当前”有可复核对象
你可能以为”证明”就是写测试。Harness不这么认为。测试是证明的一部分,但不是全部。证明还包括:密钥不泄漏的设计约束、观测不写入的边界、重放验证的能力、每个包自带守卫的CI强制、分层防御不把鸡蛋放一个篮子里、以及文档本身的可验证性。
accTitle: 第八部阅读路径
accDescr: 凭证不出日志、Telemetry 全链路捕获、Trajectory 可查询回放、Mock Snapshot 测试、Invariant 目录文档化、Trust/Permission/Sandbox/Network 四层安全、Update 固定快照保证可复现
accDescription: 第八部阅读路径流程图,从 Credentials(引用非值+0600权限+不进日志)开始,经过 Telemetry(侧车定位:只观测不创建)、Trajectory/Query/Replay(readSession做重放验证)、Mock Snapshot(真实子进程+22种LLM故障注入)、Invariant Catalog(每个包有自己的守卫,CI强制)、安全分层(Trust→Permission→Sandbox→Network),最后到更新方法论(锁commit不是停在旧版)。
flowchart TD
A["Credentials\n引用非值 + 0600权限 + 不进日志"] --> B["Telemetry\n侧车定位:只观测不创建"]
B --> C["Trajectory/Query/Replay\nreadSession做重放验证"]
C --> D["Mock Snapshot\n真实子进程 + 22种LLM故障注入"]
D --> E["Invariant Catalog\n每个包有自己的守卫,CI强制"]
E --> F["安全分层\nTrust→Permission→Sandbox→Network"]
F --> G["更新方法论\n锁commit不是停在旧版"]
style A fill:#1a3a5c,stroke:#d4af37,color:#fff
style G fill:#1a3a5c,stroke:#d4af37,color:#fff
所有证据来自固定commit的源码。实验命令只读不写,用$DSH_SOURCE_DIR指向官方checkout。
从最敏感的credentials开始。你会发现,防止密钥泄漏不是靠”小心打印日志”,而是从数据结构设计上就让密钥值根本不可能出现在错误消息里。
凭证隔离、Telemetry 与 Mock 测试
三项运维级关切——凭证为什么永远不会出现在日志里、Telemetry 如何无需定义 session 就完成采集、MockLlm 与快照测试如何让你确定性地重放 LLM 行为——在源码中究竟怎样实现。
引言:三项隐形守卫
你在 Harness 中调试 LLM 调用时,从未在日志或报错中看到过 API Key 的明文——这不是偶然的”别打印就好”,而是 结构性不可能。你打开 telemetry 开关后,会话事件几乎零延迟地到达 OTLP 收集器——但你从未在 agent 循环的任何位置声明过 session 或配置过 span。你写回归测试时发现不需要真正的 DeepSeek endpoint——一个种子可控的 mock 服务器逐请求消费脚本,确定性地注入所有失败模式。
下面按这三条线直接看源码:凭证怎样被挡在日志外,Telemetry 怎样只观察不介入,Mock Server 又怎样把失败路径做成可复现的测试输入。
第一节:凭证隔离——永远触碰不到日志的秘密
1.1 Schema 级 role(‘secret’) 声明
Harness 的 Settings 系统基于 schemastery 类型 schema。任何字段标记为 .role('secret') 后,在描述符序列化(describe() 带 redactSecrets: true)或跨 wire 边界传输时,都会被 结构化剥离。
function walk(node, value, path, secrets) {
if (node.meta?.role === 'secret') {
secrets.push({ path, set: value !== undefined })
return undefined
}
// ...递归 object / dict / array
}
关键点:
- 剥离发生在序列化时刻,而不是”打印前字符串替换”——secret 字段的值从未进入输出结构。
- sidecar 记录:每个被剥离的位置记入
RedactedSecret[](包含path和set两个字段),让 UI 表单知道该字段的 slot 存在、是否已填写,但永远不接收实际值。 - 不可变输入:
redactSecrets()从不修改原始对象——它构建一个新的 detached copy。
1.2 递归容器的完整覆盖
walker 处理三种容器类型:
object:按 schemadict中声明的属性递归,未声明的属性原样保留。dict:对值中每个 key 都用node.inner递归。array:对每个元素用node.inner递归。- default(fail-open TODO):union、intersection、transform 中埋藏的 secret 会被原样返回——源码标注了
TODO(settings-wire-redaction)承诺将来 fail-closed。
这意味着:你只要用标准 z.object() / z.dict() / z.array() 建模,secret 就不可能穿越到 wire 描述符。
1.3 assertUsableApiKey:错误消息绝不回显凭证
在 LLM 包的 API Key 校验路径中,assertUsableApiKey() 拒绝非法 key 时 从不把原始值放进错误消息:
throw new LlmError(
checked.reason === 'empty'
? `${pkg}: the API key resolved from ${ref} is blank; set ${ref} to the raw key`
: `${pkg}: the API key resolved from ${ref} contains characters no HTTP header can carry;`
+ ` set ${ref} to the raw key alone`,
INVALID_CREDENTIAL_CODE,
)
你可以看到:
- 错误消息引用 环境变量名(
ref)和 包名(pkg),不是 key 本身。 - 测试显式验证这一点:
expect((error as Error).message).not.toContain('supersecret')。
1.4 normalizeApiKey:printable-ASCII 传输不变式
const LEGAL_API_KEY = /^[\x21-\x7E]+$/
export function normalizeApiKey(raw: string): ApiKeyCheck {
const value = raw.trim()
if (value.length === 0) return { ok: false, reason: 'empty' }
if (!LEGAL_API_KEY.test(value)) return { ok: false, reason: 'illegalCharacters' }
return { ok: true, value }
}
这是一个 传输不变式(transport invariant):任何不在 !~ 范围内的字符都无法被 HTTP header 承载——所以提前拒绝是帮用户避免后续 opaque 401,而不是某个 provider 的特殊政策。
1.5 环境变量即唯一存储
cordis.patch.yml 中的凭证配置通过 !!js process.env.DSH_TELEMETRY_OTLP_URL 式环境变量表达式注入。Settings schema 中 role('credential-ref') 标记的字段存的是 引用名(如 DEEPSEEK_API_KEY),不是值本身。解析到值只发生在运行时内存中——文件系统上永远只有名字。
第二节:Telemetry——无需声明 Session 的事件采集
2.1 架构分层
Telemetry 系统分为三层:
| 层 | 包 | 职责 |
|---|---|---|
| Service Definition | dsh-session-telemetry | 定义 SessionTelemetryBackend 抽象类、SessionTelemetryRecord 结构、SessionTelemetrySink 接口 |
| Coordinator | dsh-session-telemetry/coordinator | 采集逻辑——live/on-demand 两种模式、chunk projection、redaction waterfall |
| Backend | dsh-session-telemetry-otel | OTel SDK 集成——LoggerProvider + BatchLogRecordProcessor + OTLP/HTTP exporter |
2.2 Coordinator:live 模式的零配置采集
当 backend 以 FULL 模式加载时,Coordinator 在构造函数里直接订阅四个事件:
ctx.on('session/created', (session) => this.adopt(session))
ctx.on('session/disposed', (session) => { /* shutdown marker + retire */ })
ctx.on('session/event', (session, event) => this.captureEvent(session, event))
ctx.on('session/flush', (session) => this.hintFlush(session))
ctx.on('agent/error', ({ agent, turn, step, error }) => this.relayAgentError(...))
你从未在 agent 循环中调用过任何 telemetry API——session/event bus 在 Session.append() 时自动 emit,Coordinator 作为 subscriber 自动捕获。
2.3 Chunk Projection:只运第一块
对于 assistant/chunk 事件,Coordinator 只交付每个 (turn, step) 的 第一块——“stream-started” 信号。后续 chunk 的内容在步骤结束时会通过 assembled assistant/message 完整交付,所以重复 chunk 既浪费带宽也无信息增益。
if (event.type === 'assistant/chunk') {
const key = `${event.data.turn}:${event.data.step}`
if (seen.has(key)) return // 丢弃后续 chunk
seen.add(key)
}
2.4 Redaction Waterfall:导出前最后一道防线
session-telemetry/record 是一个 cordis waterfall 事件。部署方可以挂载任意数量的 listener 来脱敏导出记录:
- innermost
next()原样返回 record。 - 每层 listener 可以变换
next()的返回值。 - 跳过
next()= 完全替换下层所有处理。 - 抛异常 = fail-closed,该条 record 被扣留,永远不到达 backend。
核心安全属性:canonical session log 永远不会被改写——redaction 只作用于导出副本。
2.5 OTel Backend:从 record 到 OTLP/HTTP
Backend 把每条 SessionTelemetryRecord 映射为一次 OTel logger.emit() 调用:
const enqueue: SessionTelemetrySink['emit'] = (record) => {
const logger = record.channel === 'ops' ? ops : ledger
logger.emit({
timestamp: record.time,
observedTimestamp: record.time,
...SEVERITY[record.severity],
body: record.body as AnyValue,
attributes: record.attributes,
})
}
之后的 batching、retry、queueing 全部由 OTel SDK 的 BatchLogRecordProcessor 管理——Harness 代码不重新实现任何导出逻辑。
2.6 DSH_TELEMETRY_MODE 与 DISABLED 默认
cordis.patch.yml 配置:
mode: !!js process.env.DSH_TELEMETRY_MODE || 'DISABLED'
三种模式:
DISABLED(默认):不创建 SDK 状态,只在有人提交 feedback 时 warn “stays local”。FEEDBACK_ONLY:只有当 session 中出现feedback/record事件时,才回溯该 session 的 canonical log 做一次 on-demand capture。FULL:实时 firehose 采集所有 session 事件。
DSH_TELEMETRY_DISABLED(任意非空值)由 launcher 注入来强制 patch 行禁用——这是进程级 opt-out。
2.7 Shutdown 与 drain 时限
async shutdown(): Promise<void> {
if (this.provider === undefined) return // DISABLED 无 SDK 状态
const providerShutdown = this.provider.shutdown()
let timer: ReturnType<typeof setTimeout> | undefined
const deadline = new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => {
reject(new Error(`provider shutdown exceeded ${this.shutdownTimeoutMillis}ms`))
}, this.shutdownTimeoutMillis)
})
try {
await Promise.race([providerShutdown, deadline])
} finally {
if (timer !== undefined) clearTimeout(timer)
}
}
默认 3000ms。OTel SDK 的 forceFlush() 可能在 transport 无法获取 socket 时永远 pending——所以 backend 用外层 deadline 做 load-bearing 的超时守卫。finally 块确保 timer 总被清理,避免 Node 进程因悬挂 timer 无法退出。Provider promise 在 deadline reject 后仍被 observed(不 .catch()),确保潜在的后续 rejection 不变为 unhandled。
第三节:Mock LLM 与确定性测试
3.1 llm-mock-server:脚本化行为序列
startMockLlmServer() 启动一个本地 HTTP/SSE 服务器,接受一个有序 sequence:每次 /chat/completions 请求消费序列中的下一个行为。26 种行为覆盖:
- 传输层失败:
connection_reset、stream_disconnect、partial_disconnect、stall - 协议层异常:
malformed_json、malformed_event、wrong_content_type、empty_body、stream_eof、partial_eof - HTTP 错误:
rate_limit(429)、server_error(500)、service_unavailable(503)、auth_error(401)、invalid_request(400)、context_overflow(400)、quota_exceeded(429) - 成功路径:
success、slow_success、reasoning_success、tool_call_success、max_tokens、empty - 随机:
random——按配置的权重表在上述具体行为中选择。
3.2 种子随机与确定性重放
function seededRandom(seed: number): () => number {
let state = seed
return () => {
state = (state + 0x6d2b_79f5) >>> 0
let mixed = state
mixed = Math.imul(mixed ^ mixed >>> 15, mixed | 1)
mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, mixed | 61)
return ((mixed ^ mixed >>> 14) >>> 0) / 0x1_0000_0000
}
}
randomSeed 可以显式指定或自动生成后暴露在 MockLlmServer.randomSeed 上——测试失败时,用记录的种子精确重现同一行为序列。权重通过 DEFAULT_MOCK_LLM_RANDOM_WEIGHTS 配置,以非均匀概率模拟生产压力分布。
3.3 请求录制:完整 wire 证据
每个接受的请求都记录为 MockLlmRequestRecord:
interface MockLlmRequestRecord {
readonly attempt: number
readonly scriptBehavior: MockLlmBehavior | 'script_exhausted'
readonly behavior: ConcreteMockLlmBehavior | 'script_exhausted'
readonly path: string
readonly headers: Readonly<IncomingHttpHeaders>
readonly body: unknown
chunksSent: number
outcome?: MockLlmRequestOutcome
}
测试可以对 server.requests 做断言——验证重试次数、header 内容、request body 格式,以及每次请求的最终 outcome(completed、reset、stalled、client_closed、server_error)。
3.4 repeatLast 与 script_exhausted
当 sequence 耗尽时:
repeatLast: true:无限重复最后一个行为(适合 stress test)。repeatLast: false(默认):返回 500 +MOCK_SCRIPT_EXHAUSTED——loud failure,让你立刻发现测试消耗了比预期更多的请求。
3.5 Bearer Token 验证
if (resolved.apiKey !== undefined &&
request.headers.authorization !== `Bearer ${resolved.apiKey}`) {
response.writeHead(401, { 'content-type': 'application/json' })
response.end(JSON.stringify({
error: { message: 'invalid mock bearer token', code: 'invalid_api_key' }
}))
return
}
如果 mock server 配置了 apiKey,则每个请求必须携带匹配的 Bearer token——这让你验证 adapter 是否正确把凭证放进了 Authorization header,而不需要连接真正的 provider。省略 apiKey 配置则接受任何 authorization header,适合只关心 response 行为的测试场景。
3.6 SSE 流模拟与 chunk 切割
splitText() 按 Unicode code-point(不是 byte)切割 response text,每个 chunk 通过 writeSse() 发送为一个 data: 事件。slow_success 在 chunk 间注入 chunkDelayMs 延迟,模拟慢速流。partial_disconnect 在发送部分 chunk 后主动 response.destroy(),触发客户端的 stream 恢复逻辑。
3.7 Telemetry 观察者(onEvent)
function emit(options, event) {
try { options.onEvent?.(Object.freeze(event)) }
catch (_) { /* observer failure never affects wire behavior */ }
}
测试可以通过 onEvent 回调收集 server 端的 telemetry 事件——但 observer 异常被 严格隔离,永远不会影响 mock server 对客户端的行为。这是观测性与行为正确性的分离原则。
第四节:三者如何协作
4.1 凭证 + Telemetry
Settings 的 describe({ redactSecrets: true }) 确保 wire 描述符中不含 secret;Telemetry 的 session-telemetry/record waterfall 是导出前的第二道脱敏屏障。两道防线独立运作:即使没有人挂 waterfall listener,secret 字段也不会通过 settings 描述符泄漏;即使 settings 不标记 role(‘secret’),部署方仍可通过 waterfall rule 过滤任何敏感内容。
4.2 凭证 + Mock 测试
mock server 的 apiKey 配置让你 不用真正凭证 就能完整测试 authorization 路径。assertUsableApiKey 的错误消息不回显凭证这一属性也被单元测试显式守卫——回归保证。
4.3 Telemetry + Mock 测试
OTel backend 的单元测试使用一个本地 node:http mock collector(不是 llm-mock-server,而是更简单的 request-capture server)验证导出格式。Session telemetry 的 coordinator 测试则用 CollectingBackend(一个 3 行的 in-memory sink),完全不依赖 OTel SDK——关注点隔离。
第五节:不变式与防御姿态
| 不变式 | 守卫者 | 违反时行为 |
|---|---|---|
| secret 值不跨 wire | redactSecrets() walker | 返回 undefined,记录 path |
| 错误消息不含凭证原文 | assertUsableApiKey | 只引用 ref 名 |
| telemetry 异常不逃逸到 agent loop | Coordinator contain() | catch + warn |
| waterfall 异常 = fail-closed | Coordinator captureEvent | record 扣留不送 backend |
| shutdown 有确定性上界 | Backend Promise.race | 3s deadline reject |
| mock script 耗尽 = loud 500 | selectBehavior() | 返回 script_exhausted |
| observer 异常不影响 mock wire | emit() try/catch | 静默吞错 |
第六节:配置速查
Telemetry 环境变量
| 变量 | 作用 | 默认 |
|---|---|---|
DSH_TELEMETRY_MODE | FULL / FEEDBACK_ONLY / DISABLED | DISABLED |
DSH_TELEMETRY_OTLP_URL | OTLP/HTTP logs endpoint | https://harness-telemetry.deepseeksvc.com/v1/logs |
DSH_TELEMETRY_DISABLED | 任意非空值 = 进程级 opt-out | 未设置 |
Mock Server 关键参数
| 参数 | 作用 | 默认 |
|---|---|---|
sequence | 行为脚本(必填) | — |
repeatLast | 耗尽后循环最后一个 | false |
randomSeed | 确定性种子 | OS 随机 |
chunkSize | 每 SSE delta 的 code-point 数 | 8 |
chunkDelayMs | slow_success 的 inter-chunk 延迟 | 25ms |
disconnectDelayMs | 断连前等待 | 10ms |
收口
到这里,这章想守住的三条边界就很清楚了:
- 凭证隔离不是约定,是结构——schema walker 在序列化那一刻就把 secret 扔掉,error 构造函数只引用名字,测试里还会显式断言”不含原文”。
- Telemetry 是被动采集,不是主动声明——Coordinator 订阅 session bus,chunk projection 负责降噪,waterfall 负责可扩展脱敏,OTel SDK 负责真正把数据送出去。
- Mock 测试要的是确定性回放——26 种行为覆盖失败模式,seed 保证可重现,wire 录制让断言精确到 header。
有了这三道护栏,你就能把三件事分开做:开发期不泄漏凭证,生产期采集但不侵入 agent 循环,CI 里不连真实 endpoint 也能把恢复路径跑全。