docs(agent-specs): handoff 跨空间统一接入评审版(B 类头号架构缺口)

三方并行调研(后端契约/design-docs SSOT/studio 现状)证实 handoff 红线五域零落地、
后端兑现侧断链(token 不验真、Market 无 consume、agent/content 无兑现入口)。评审版定性
B+C 混合(前端四环统一层 + 后端兑现侧 token 核验),MVP 纵切市场→知识库,分 P1-P4 演进。
含拍板点表/Mermaid 数据流(标注前端缺/后端断链)/blast radius/验收/Open Items。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-22 08:25:28 -07:00
parent f3967da5f3
commit cb14b40223

View File

@ -0,0 +1,187 @@
# 跨空间 Handoff 统一接入 —— 评审版
> 版本:v0.1(评审版) · 日期:2026-06-22 · 目标读者:架构 review + 实现 agent · 类型:评审版(结论先行,无代码细节)
> 上游:[admin+studio 全功能域缺口盘点](../mvp/进度总账.md)横切共性缺口 ①(handoff 跨空间安全交接=架构级缺失,五域全卡)。A 类(接已就绪后端)三切片收官后,转 B 类头号架构缺口。
> 触发:三方并行调研(后端契约 / design-docs SSOT / studio 现状)证实——handoff 红线在五域**零落地**,且后端**兑现侧本身断链**,纯前端接线无法让红线生效。
---
## 一、结论先行
1. **目标**:把 design-docs 已定义、后端发起侧已就绪、但端到端从未闭合的"跨空间 Handoff 安全交接",落地为**一套统一前端消费层 + 一条真实闭合的兑现链**,使"不信任客户端 URL 参数"红线首次真正生效。先打通**一条端到端纵切**(市场→知识库绑定),再按统一层向其余域铺开。
2. **推荐方案(一句话)**:按 SSOT 已给的 `HandoffStore`(前端-05 §25.3)+ token 生命周期(前端-01 §9)实现**统一前端 handoff 层**(创建 token→跳 targetPage→落地 replaceState 清 URL→目标 owner 消费换 session/precheck→确认原子写→返回刷新);**同时补强后端兑现侧 token 核验**(MVP 先 knowledge:兑现时回验 token 属主/未过期/一次性 + Market 加 token 核销使状态推进 completed),否则红线只是"形式落地、实质虚设"。
3. **关键依据(design-docs SSOT 已定义,实现须遵守,不得重新发明)**:
- **四模型**:Handoff Token(来源签发一次性凭据)/ Handoff Session(目标消费 token 后的更窄会话)/ Precheck Result(目标 owner 原子消费)/ Return Context(返回来源,只刷状态不补写)——架构-02 §7。
- **红线**:"不能信任 URL 或客户端参数直接执行高影响动作"——产品-03 §9.2、流程-02B §13、各域 02C §10 / 02D §8 / 02E §3.7 / 02F §9 / 前端-05 反复声明。
- **固定生命周期**(措辞跨产品-03 §9.3 / 流程-02B §13 一致):发起→落地→消费 token 生成 session→owner 预检→用户确认→原子消费→写 owner 事实→返回刷新。
- **状态机**(架构-04 §10):Token `issued→consumed/expired/canceled`;Session `active→prechecked→confirmed→consumed`(旁路 rejected/failed/canceled/expired)。
- **统一性**(强制):各域复用同一机制,由固定生命周期 + 统一不变式集 + 统一 API 契约(后端-05)三重锁定;ADR-014/015 把 Handoff 定为不可被实现层(物理模块/页面入口)改变的逻辑 owner。前端统一抽象 SSOT = 前端-05 §25.3 `HandoffStore` + 前端-01 §9。
4. **核心矛盾(本评审的 load-bearing 前提,§二详述)**:红线要求"目标 owner 服务端消费 token 换 session + 重新校验",但**后端兑现侧断链**——Market 无 consume 端点(token 永停 pending)、knowledge 兑现只存 token hash 从不回验、agent/content 兑现入口根本不存在。**因此这不是纯 B 类前端接线,而是 B(前端四环)+ C(后端兑现侧核验补强)混合,且 C 是 B 真实生效的前置依赖。**
5. **拍板点(§四/§十)**:① 后端兑现侧补强范围(MVP knowledge 一条 vs 一次补全三 owner);② token 核销机制(目标兑现时回调 Market 标 consumed,还是 Market 加显式 consume 端点);③ `targetOwner='content'` 与后端 `/works/` 命名对齐;④ MVP 纵切确认(knowledge 优先)。
---
## 二、背景:三方现状与核心矛盾
### 2.1 后端(发起侧完整、兑现侧断链)
- **发起侧 4 端点就绪、P1r 验证**(`AppMuseMarketHandoffController`):`POST /assets/{id}/bind-precheck`(拿 authorizationSummaryId,不发 token)→ `POST /handoffs`(拿明文 `handoffToken` + `targetPage`)→ `GET /handoffs/{token}`(只读状态)→ `POST /handoffs/{token}/cancel`(CAS 需 expectedStatus)。token 安全扎实:确定性派生幂等、明文仅创建响应返回、DB 只存 sha256 hash、属主隔离(`MARKET_RESOURCE_FORBIDDEN`)、`targetPage` URL **不带 token**(符合红线)。
- **兑现侧断链(关键)**:
- Market **无 consume/核销端点**——token 状态永停 `pending`,`completed` 常量定义了但无任何写入路径,`getHandoffStatus` 不反映"目标已绑定"。
- token 实际预期在**目标空间**兑现:knowledge `POST /works/{id}/knowledge-bindings/prechecks``PrecheckReqVO``handoffToken`(market_kb 来源时必填),但 knowledge **只对 token 取 hash 存档、从不回 Market 校验**(属主/一次性/过期);真正落绑定靠 knowledge 自己的 `kbBindPrecheckId`,不是 token。
- **agent / content owner 兑现入口根本不存在**(白名单含 agent/content,但只有 knowledge 的 market_kb 分支有兑现代码)。
- `targetPage` 白名单:owner `{agent,knowledge,content}` × action `{slot_bind,bind,asset_use,governance_handle}`,组合不受后端约束。
### 2.2 design-docs(SSOT 已完整定义统一机制)
- handoff **无单一 owner**,分层归属:模型=架构-02 §7、边界=架构-01 §7.3、状态机=架构-04 §10、产品语义/旅程=产品-03 §9、API=后端-05、**前端统一消费=前端-01 §9 + 前端-04 §2.4 + 前端-05 §25.3**。
- 五域来源→目标映射(产品-03 §9.4 + 前端-05 §25 跳转矩阵):
| 域 | 角色 | 交接载荷 | 目标侧预检凭据 |
|---|---|---|---|
| 02C 作品台 | **目标**为主 + 返回来源 | agent 槽位绑定 / knowledge 绑定 | 由目标 owner 生成 |
| 02D 智能体 | 目标(接 02C) + 来源(发布到市场) | agent 槽位绑定 | `agentSlotPrecheckId` |
| 02E 知识库 | 目标(接 02C) + 来源(发布到市场) | knowledge 绑定 | `kbBindPrecheckId` |
| 02F 市场 | **纯来源侧**(只发起、不写目标事实) | listing/license/install/source status/return point | 市场不生成预检,只携摘要 |
| 02G 个人中心 | **记录跳转(中转)**,不写业务事实 | 按记录类型携带对象 + 返回点 | 目标空间自建 |
- 命名提醒:前端-04 用 `targetOwner='content'` 指代作品台,后端绑定走 `/works/{workId}/...`——文档未显式统一,实现侧须对齐(content = 作品/Work 空间)。
### 2.3 studio(前端断在 bind-precheck 响应之后)
- 唯一真实运行代码 = 02F `useBindPrecheck`(MarketAssetDetailPage),打 `bind-precheck`(targetOwner/targetAction **硬编码** 'knowledge'/'bind'),拿 authorizationSummaryId 后**只渲染只读 dl 就停了**。
- token 消费四环(创建 `POST /handoffs`→跳 targetPage→目标消费→owner 确认)**全缺**;路由层 `useSearchParams`/`replaceState`/`?token` **全仓零命中**(无"凭 token 进目标空间"入口)。
- 02D slot 绑定是**同空间流**(已安装 agent→槽位,请求体不含 handoffToken),**非**跨空间 handoff;02C/02E/02G 连来源侧入口都没有。
- 技术债:`openapi.ts` 不导出任何 Handoff 类型;useMarket 手写 `MarketBindPrecheckResult`(字段比 market.ts `BindPrecheckResult` 少、id 用 number 而非 uuid string)——统一时收口。
### 2.4 核心矛盾一句话
> design-docs 红线("目标 owner 服务端消费 token 换 session + 重新校验")在后端兑现侧**根本没实现**(token 不验真、永停 pending、agent/content 无入口),前端则连发起后四环都缺。**纯前端接线会做出"看似有 handoff、实则 token 形同虚设"的假安全**——这正是 anti-false-green 要拦截的。故必须 B+C 同步。
---
## 三、范围与非目标
**范围(MVP 纵切:市场→知识库一条端到端真闭合)**
- 前端统一 handoff 层:`HandoffStore`(Zustand,对齐前端-05 §25.3)+ 通用创建/落地/消费/确认/取消 hooks + 目标落地路由(凭 token query 进入、`replaceState` 立即清 URL)。
- `openapi.ts` 导出 Handoff 类型簇(BindPrecheckResult/HandoffCreateResult/HandoffStatusResult),useMarket 收口手写类型漂移。
- 02F 来源侧:bind-precheck 后续接 `POST /handoffs` → 跳 targetPage。
- 02E 目标侧:落地页消费 token → `kbBindPrecheck`(回验 token)→ 确认 `createKnowledgeBinding` → 返回来源刷新。
- **后端兑现侧补强(C)**:knowledge 兑现时回验 token(属主/未过期/一次性)+ token 核销(状态推进 consumed/completed)。
- 真后端 playwright e2e:市场资产 → 发起 → 跳转 → 知识库落地消费 → 绑定 → 返回,全链 MSW-off 活体。
**非目标(本轮)**
- 不一次铺开五域(02C/02D/02G 与 agent/content owner 留演进,见 §六)。
- 不改后端 token 派生/属主/CAS 既有安全机制(已 P1r 验证,只**补**兑现侧)。
- 不改 02D 现有同空间 slot bind(保留;新增的是"市场 agent→槽位"跨空间路径,本轮不做)。
- 不做 handoff 的离线/多端同步(token 短期有效,单端落地)。
---
## 四、关键决策(拍板点)
| # | 决策 | 选项 | 推荐 | 理由 |
|---|---|---|---|---|
| D-A | 后端兑现侧补强范围 | (a) MVP 仅 knowledge 一条 + 回验/核销;(b) 一次补全 agent+knowledge+content 三 owner 兑现;(c) 前端先做、后端暂不验真 | **(a)** | (c) 是假安全(违 anti-false-green)直接排除;(b) 工程量大且 agent/content 目标侧 UI 本就缺;(a) 先证红线可端到端落地,knowledge 后端骨架最全(已有 kbBindPrecheck/createKnowledgeBinding) |
| D-B | token 核销机制 | (a) 目标兑现成功后回调 Market 标 token consumed;(b) Market 新增显式 consume 端点供目标调用;(c) 不核销、靠 knowledge precheck 一次性兜底 | **(a) 或 (b),本轮拍一个** | 红线要求 token 一次性 + 状态可反映完成;(c) 留下 token 可重放窗口(虽 knowledge precheck 一次性,但 token 本身未失效)。(a)/(b) 都需后端改动,(b) 契约更清晰、(a) 耦合更松——属人类拍板 |
| D-C | targetOwner 命名 | (a) 前端枚举对齐后端白名单 {agent,knowledge,content},content→作品空间;(b) 前端用 work、转换层映射 | **(a)** | 后端白名单是事实源(`LocalMarketTargetOwnerFacade`),前端对齐避免双向映射;content→作品路由在落地层解析 |
| D-D | 前端统一层抽象边界 | (a) 通用 HandoffStore + 各域提供"目标消费适配器"(precheck 接口 + 确认接口 + 返回路由);(b) 每域独立实现 handoff | **(a)** | SSOT §5 强制统一;各域差异仅"目标 owner 的 precheck/确认端点 + 落地路由",抽象为适配器即可全域复用,一次修受益五域 |
| D-E | MVP 纵切方向 | (a) 市场→knowledge bind;(b) 市场→agent slot;(c) 市场→work asset_use | **(a)** | knowledge 兑现侧后端最接近完整(只差 token 回验),最快证闭环;agent/content 兑现入口为零,做纵切要先补目标侧整套 |
| D-F | URL token 卫生 | (固定,非选项)落地后立即 `replaceState` 移除 token query | **遵守 SSOT** | 前端-01 §9 / 前端-05 §11.3 硬要求,防刷新重复消费;属实现要求不是决策 |
---
## 五、推荐方案数据流
```mermaid
flowchart TD
subgraph SRC[来源空间 02F 市场资产详情]
A1[用户:获取授权后发起绑定] --> A2["POST /assets/id/bind-precheck<br/>✅前端已有(止于此)"]
A2 --> A3["POST /handoffs<br/>❌前端缺:拿 handoffToken + targetPage"]
end
A3 -->|"跳转 targetPage?token=...(query)"| B1
subgraph TGT[目标空间 02E 知识库落地页 ❌前端整段缺]
B1["落地路由解析 token<br/>❌缺:replaceState 立即清 URL"] --> B2["消费 token 换 session<br/>POST .../knowledge-bindings/prechecks(带 handoffToken)"]
B2 --> B3["owner 预检 → kbBindPrecheckId<br/>⚠️后端缺:回验 token 属主/过期/一次性"]
B3 --> B4[用户确认]
B4 --> B5["原子写:POST .../knowledge-bindings(凭 precheckId)<br/>+ ⚠️后端缺:核销 token→consumed"]
end
B5 -->|Return Context 只刷状态| C1["返回来源刷新<br/>GET /handoffs/token 反映 completed"]
A3 -. 失败/取消 .-> X1["POST /handoffs/token/cancel<br/>CAS expectedStatus ❌前端缺"]
HS["统一 HandoffStore (Zustand)<br/>前端-05 §25.3:session/precheckId/expiry/returnUrl<br/>+ consumeToken/clearSession/isSessionValid<br/>❌前端缺,本轮新建,五域复用"]
HS -.贯穿.-> B1
HS -.贯穿.-> B4
classDef have fill:#d4f8d4,stroke:#2a2;
classDef femiss fill:#ffe0e0,stroke:#c33;
classDef bemiss fill:#fff0c0,stroke:#e90;
class A2 have;
class A3,B1,X1,HS femiss;
class B3,B5 bemiss;
```
**图例**:🟢=前端已有;🔴=前端缺(B 类四环 + 统一 Store);🟡=后端兑现侧断链需补强(C 类)。**职责切分(合 SSOT 边界)**:来源(02F)只签发 token,不写目标事实;目标 owner(02E)消费 token 换 session + 自建 precheck + 原子写;统一 HandoffStore 只承载客户端会话态,不承载授权事实。
---
## 六、演进路径(分阶段,各阶段独立可验证)
| 阶段 | 内容 | 验证 | 受益域 |
|---|---|---|---|
| **P1(本轮 MVP)** | 统一 HandoffStore + 类型收口 + 02F 发起 `POST /handoffs` + 02E 落地消费 + **knowledge 后端回验/核销** | 真后端 e2e:市场→知识库绑定端到端闭合;红线首次真生效 | 02E + 02F |
| **P2** | agent owner 兑现侧补全(后端 agent precheck 回验 token + 前端落地页)+ 市场→agent 槽位跨空间 handoff | e2e:市场 agent→作品槽位 | 02D + 02F |
| **P3** | 02C 作为目标的统一落地(作品台内 agent 关联页 / 知识来源页发起)+ content owner asset_use | e2e:作品台发起→目标绑定 | 02C |
| **P4** | 02G 记录跳转(中转空间,生成跳转授权→目标重算 gate)+ governance_handle 治理处理 | e2e:个人中心记录→目标空间 | 02G |
> 统一层(P1 的 HandoffStore + 适配器抽象)一次建成,P2-P4 各域只新增"目标消费适配器"(precheck/确认/返回路由)+ 后端对应 owner 的 token 回验,不重写主链。
---
## 七、Blast Radius / 兼容 / 回滚
- **前端 muse-studio**:新增 `HandoffStore` + 通用 hooks + 目标落地路由(`/...?token=` 解析 + replaceState);02F MarketAssetDetailPage 扩"发起绑定"动作;02E 新增落地页 + 消费 hooks;`openapi.ts` 导出 Handoff 类型 + useMarket 类型收口。**纯增量,不改既有 bind-precheck 只读展示路径**(可并存)。
- **后端 muse-cloud**:knowledge 兑现侧补 token 回验(调 Market 校验或 Market 暴露核验)+ token 核销(Market `completed`/`consumed` 写入,D-B 拍板后定具体形态)。**触及 module-knowledge + module-market 跨 BC 协作**,需过 `BcBoundaryArchTest`(0 违例硬条件)。
- **兼容**:现有 02F bind-precheck 行为不变(仅在其后追加发起);knowledge 现有同空间绑定不受影响。token 安全机制(派生/属主/CAS)不动,只**补**兑现侧。
- **回滚**:前端统一层是新增模块,02F"发起绑定"按钮可 feature-gate;后端 token 回验若出问题,回退到"仅存 hash"(即现状)——但那等于红线不生效,故回退=已知降级而非安全态,需在发布决策里标注。
---
## 八、风险与缓解
| 风险 | 说明 | 缓解 |
|---|---|---|
| **假安全(头号)** | 只做前端、后端兑现侧不验真 → token 形同虚设,红线"形式落地实质虚设" | D-A 排除纯前端选项;P1 验收硬条件 = 后端回验 token(伪造/过期/跨属主 token 必须被拒,e2e 负路覆盖) |
| 跨 BC 边界违规 | knowledge 回验 token 需读 market 态 | 走 market-api 端口(非直连 market.dal);`BcBoundaryArchTest` 0 违例 |
| token 核销机制未定 | D-B 两方案都需后端改 | 本评审拍板 D-B 后再出执行版;P1 不动既有 CAS |
| content/work 命名漂移 | 前端 targetOwner 枚举 vs 后端 /works/ | D-C 对齐 + 落地路由集中解析,单点映射 |
| 02D 同空间/跨空间混淆 | 现有 slot bind 易被误改 | 范围明确隔离:本轮不碰 02D 同空间流,跨空间留 P2 |
| 真后端 fixture 缺资产/授权 | e2e 需已购 active 授权 + 可绑定知识库 | globalSetup 种子补齐(市场资产 + active license + 目标作品/知识库) |
| targetPage 白名单不匹配 | 前端 targetOwner 组合不在后端白名单 → MARKET_TARGET_OWNER_UNAVAILABLE | 前端枚举严格取自后端白名单 {agent,knowledge,content}×{slot_bind,bind,asset_use,governance_handle} |
---
## 九、验收标准
- **统一层**:`HandoffStore` 单测(session 存取 / isSessionValid 过期判定 / consumeToken 一次性 / clearSession);类型收口后 `openapi.ts` 导出 Handoff 簇、useMarket 不再手写漂移类型(tsc 绿)。
- **前端四环**:02F 发起 `POST /handoffs` 拿 token+targetPage;落地页解析 token 后 URL 立即无 token(replaceState 验证);消费换 precheckId;确认写绑定;取消走 CAS。
- **后端兑现(C,反假绿核心)**:knowledge 兑现**回验 token**——合法 token 通过、过期/跨属主/已消费 token **必须被拒**(对应错误码);token 核销后 `GET /handoffs/{token}` 反映终态。
- **端到端(真后端活体 playwright,MSW-off)**:市场资产详情 → 发起绑定 → 跳知识库落地 → 消费 token → 预检 → 确认 → `muse_knowledge_binding` 落库 + 返回来源 → 状态刷新为完成。**负路**:伪造/过期 token 落地 → 被拒、不写任何绑定事实。
- **边界**:`BcBoundaryArchTest` 0 违例;knowledge↔market 仅经端口协作。
- **回归**:现有全量 e2e(当前 47/0)+ vitest(当前 82)不退;02F 既有 bind-precheck 只读展示与 market-* 测试不破。
---
## 十、Open Items(须人类拍板)
1. **(拍板·决定工程量)后端兑现侧补强范围**:D-A——MVP 仅 knowledge 一条 + 回验/核销(推荐),还是本轮即补全 agent+knowledge+content 三 owner?后两者目标侧 UI 本就缺,纵切成本高。
2. **(拍板·安全机制)token 核销形态**:D-B——目标兑现成功后回调 Market 标 consumed,还是 Market 新增显式 consume 端点供目标 owner 调用?涉及后端跨 BC 协作形态与谁持有"核销 authority"(倾向 Market 持有,因 token 是 Market 签发)。
3. **(拍板·命名)** `targetOwner='content'` ↔ 后端 `/works/` 对齐确认(content=作品/Work 空间),落地路由单点映射。
4. **(确认)MVP 纵切 = 市场→知识库绑定**(D-E 推荐),是否同意作为 P1?
5. (实现期验证)knowledge 回验 token 时,Market 提供的核验端口形态(已有 `getHandoffStatus` 是否够用,还是需新增"按 tokenHash 核验属主+有效性"的内部端口)——依赖 D-B 结论。
> 下一步:本评审走查确认 §十.1/§十.2/§十.4 三个拍板点后,出**执行版 spec**(统一层模块结构 + 前端四环 hook/路由契约 + 后端兑现侧端口契约 + 边界验收 + 分步落地 + 回滚),再实现 P1 纵切。