BodySense StreamEvent Trust Boundary
解释 BodySense Consultation 如何把 SSE / durable replay 的网络 JSON 视为 unknown,并通过共享 parseStreamEvent runtime parser 升级为可信 StreamEvent 后再进入 reducer 和 UI。
[!info] related notes
BodySense StreamEvent Trust Boundary
一句话定义
StreamEvent Trust Boundary 是把来自 Live SSE / durable event API 的不可信 JSON,从 unknown 经过运行时解析与校验升级为可信 StreamEvent 的边界;只有越过这道边界的数据,才应该进入 reducer、selector 和 UI。
Network Bytes
→ Text Framing
→ JSON.parse
→ unknown
→ parseStreamEvent
→ trusted StreamEvent
→ domain projection
为什么这是架构边界,而不只是 TypeScript 最佳实践
一旦代码把网络对象叫作:
StreamEvent
下游就会开始相信:
type 合法
payload 与 type 匹配
seq 具有协议语义
ids 结构合法
control/safety event 不缺关键字段
因此 unknown → StreamEvent 实际上是一次信任升级。
如果只写:
const event = data as StreamEvent;
并没有产生 runtime evidence,只是要求 TypeScript 编译器相信开发者。
TypeScript Union 解决了什么
共享 StreamEvent discriminated union 对已经可信的数据非常有价值:
switch (event.type) {
case "message.text.delta":
// payload 可以按该 variant 收窄
}
它帮助:
exhaustiveness
payload narrowing
IDE / refactor safety
reducer contract
但 TypeScript 类型在编译后消失,所以:
Static type safety
≠ runtime input validation
当前 BodySense 已经闭合这条主要边界
旧实现曾经存在:
JSON.parse
→ as StreamEvent
→ reducer
这是一条真实历史缺口,但不再是当前实现状态。
现在 Live SSE 入口:
SSE data line
→ JSON.parse → unknown
→ parseStreamEvent(data)
→ trusted StreamEvent
→ handler/reducer
Durable Replay API 入口:
HTTP JSON
→ ids/payload decode
→ construct unknown event candidate
→ parseStreamEvent(...)
→ trusted StreamEvent
→ replay handler/reducer
对应代码:
packages/contracts/src/stream-event-parser.ts
packages/contracts/src/stream-events.ts
apps/web/src/features/consultation/hooks/useSSEProcessor.ts
apps/web/src/features/consultation/services/consultationService.ts
这使 Live 与 Replay 使用同一个当前 consumer contract,而不是“来自数据库就默认可信”。
Network Bytes 到 Domain Event 有多个 Boundary
完整路径:
Uint8Array
→ TextDecoder(stream=true)
→ text buffer
→ SSE framing
→ JSON.parse
→ unknown JSON value
→ StreamEvent runtime parser
→ trusted event
每一步都可能失败:
UTF-8 字符跨 chunk
一条 SSE frame 被拆成多个 chunk
一 chunk 包含多条 frame
JSON syntax invalid
JSON valid but event schema invalid
event/data type mismatch
unsupported/unknown protocol shape
所以:
ReadableStream chunk ≠ protocol event
JSON parse success ≠ valid StreamEvent
unknown 为什么优于 any
const data: unknown = JSON.parse(text);
unknown 会强迫调用方先 narrowing/validation 才能使用。
而 any 会让:
data.payload.foo.bar
直接穿过静态类型系统。
因此 unknown 是很好的“尚未获得信任”静态表示。
Runtime Parser 应验证什么
至少要覆盖:
envelope
├─ version
├─ seq
├─ channel
├─ type
├─ ids
└─ payload
然后按 discriminant 验证 variant-specific payload:
message.text.delta
→ delta/text contract
tool.result
→ tool_call identity + result contract
state.interaction.required
→ interaction/tool/question contract
safety.red_flag.detected
→ safety payload contract
如果某些字段之间存在关系,还需要 refinement/cross-field invariant,而不是只检查 primitive types。
event: 与 data.type 也应该一致
SSE 本身可能携带:
event: message.text.delta
data: { "type": "tool.call", ... }
即使 data 对象单独能通过某种 schema,这种 transport envelope 与 domain discriminant 不一致也属于 protocol error。
当前 useSSEProcessor.ts 会检查当前 event: 行和 parseStreamEvent 得到的 event.type 是否匹配。
Validation 失败后的原则
不要:
invalid event
→ unchecked reducer
也不要对 authority/safety relevant event:
malformed
→ silently treat as “nothing happened”
更可靠的是:
protocol validation fails
→ explicit error/diagnostic
→ stop or recover according to policy
→ never promote malformed payload into business state
尤其 safety/control event 更接近 fail-closed 边界。
Parser 不应该偷偷“修成合法”
例如:
missing seq → 0
unknown enum → normal
missing red flag → false
这些默认值可能把 UNKNOWN 变成错误业务事实。
对于 authority/safety/control 字段,通常宁可 explicit reject,也不要为了 parser 永不报错而创造语义。
Live 与 Replay 必须经过同一个 Trust Model
错误:
Live SSE → strict parser
Replay DB → unchecked cast
数据库可能包含:
旧 schema
migration 前 payload
历史 bug 生成的数据
corrupted row
所以 trust boundary 取决于当前 consumer contract,不是数据来自公网还是内网。
理想结构:
Live SSE ───┐
├→ parseStreamEvent → trusted union → reducer
Replay API ─┘
Trust Boundary 之后 Reducer 才能保持 Pure/Simple
如果 parser 已经建立可信边界,reducer 就不用每个 branch 都重新做:
if (!payload || typeof payload.delta !== 'string') ...
架构边界可以保持:
Boundary layer
= defensive validation
Domain reducer
= trusted deterministic projection
这对 replay correctness 很重要。
Protocol Version 与兼容性
如果 envelope 有:
version = 1
它应该参与 parser/compatibility policy,而不只是展示字段:
supported version → parse
legacy supported → explicit adapter
unknown critical version → explicit incompatibility
不要依赖历史数据“碰巧还能被 TypeScript cast”。
Contract Drift 怎么防
最危险的情况是:
TS union 一份
runtime parser 一份
Go DTO 一份
长期靠人工同步。
更稳的方向包括:
shared schema/source generation
or
cross-language contract fixtures/tests
至少应该证明:
Go-produced valid fixture
→ frontend parser accepts
malformed fixture
→ frontend parser rejects
与 Chapter 7 Streaming Recovery 的关系
只有 runtime trust boundary 正确,after_seq replay 才真正有意义。
否则:
Durable Event Log
→ replay malformed event
→ unchecked cast
→ deterministic reducer corrupts state
并不是“持久化了就可靠”。完整链路必须是:
Durable event
→ current runtime validation
→ run-aware ordering/dedup
→ pure projection
见 durable-sse-recovery-with-after-seq。
当前重点测试
应该覆盖:
valid variants accepted
unknown/malformed type rejected
wrong payload type rejected
invalid seq rejected
SSE event/data type mismatch rejected
Live and Replay both use parser
new/legacy version compatibility explicit
safety/control malformed input fails explicitly
代码审查问题
看到网络数据进入 domain 时问:
JSON.parse后变量是unknown还是直接any/as T?- 哪一行第一次有资格把它叫
StreamEvent? - Live 与 Replay 是否走同一个 parser?
- Parser 是否验证 discriminant-specific payload?
- malformed safety/control event 会不会 fail open?
- Protocol version/schema drift 是否有自动化证据?
最终记忆
TypeScript protects code from developers;runtime validation protects the application from external data.
更适合 BodySense 的一句话:
unknown network data
→ runtime evidence
→ trusted StreamEvent
→ reducer
当前 BodySense 已经通过共享 parseStreamEvent 把这条主要信任边界真正落到了 Live SSE 与 durable replay 两个入口。