chore(task): archive 09-14-workspace-authorization-rebinding

This commit is contained in:
YBF
2026-09-14 21:04:57 +08:00
parent c9417f4469
commit 20a54a3362
7 changed files with 479 additions and 0 deletions
@@ -0,0 +1,4 @@
{"file":".trellis/spec/server/backend/quality-guidelines.md","reason":"Server test, type-check, build, and formatting verification requirements."}
{"file":".trellis/spec/server/backend/mind-authorization.md","reason":"Authorization security and regression matrix used to review the implementation."}
{"file":".trellis/spec/project/async-state-boundaries.md","reason":"Review authorization fences and late-result invalidation behavior."}
{"file": ".trellis/tasks/09-14-workspace-authorization-rebinding/research/auth-context-research.md", "reason": "Independent check matrix, source anchors, and high-risk publish fallback evidence."}
@@ -0,0 +1,93 @@
# Center 空间授权上下文设计
## Boundary and Invariant
`workspaceId` 是 Mind 页面请求声明的空间选择,不是身份。Center 只在 Mind 页面边界接收它,将其附带 Cookie 和 TM `channelAccountId` 转交固定 Mind session-authorization endpointCenter 不查询 Mind 数据库、不从任何用户 ID/header 推导身份,也不向插件连接增加该字段。
一个 Mind 页面授权判定只能对同时满足以下条件的空间有效:
```text
HTTP header / WS query workspaceId
== Mind authorization response.mindScope.workspaceId
== HTTP response / authenticated WS state workspaceId
```
任一缺失或不相等均失败关闭;不会选择默认空间、复用旧 decision 或降级为只按账号授权。
## Canonical Workspace Context
`apps/server/src/` 创建一个 Center-only 的 Mind workspace context owner,统一拥有:
- 对外发送 header 的精确名称 `Sinan-Pilot-Workspace-Id` 与小写入站 header key
- HTTP header 的单值、非空校验;
- `/ws/mind` URL 中 `workspaceId` 的单值、非空校验;
- 从请求边界取得值后供授权 reader 使用的窄 `workspaceId` 值。
它不处理 Cookie、Mind user、channel account 或授权结果。这些分别仍由 HTTP/WS ingress、Mind authorization adapter 和 protocol session owner 负责。重复 query/header、空白值或多值合并被视为无效;不会 trim/重写后再向 Mind 转发。
## Contract Change
`OneTalkMindAuthorizationRequest.scope` 从仅 `channelAccountId` 改为 `{ channelAccountId, workspaceId }`。这是 Center 与现有 mock authorization reader 共用的授权调用契约;响应 contract 和请求 JSON body 都不改变。
Mind authorization adapter 对 `mind_page` request
```text
POST /internal/bright/onetalk/authorize-session
Cookie: <original request cookie>
Sinan-Pilot-Workspace-Id: <received workspaceId>
Content-Type: application/json
{ "channelAccountId": "..." }
```
只允许固定 session URL`workspaceId` 不参与 URL、日志、诊断或持久化。成功 response 必须同时匹配请求的 `channelAccountId``workspaceId`,否则 reader 返回 `scope_mismatch`。Plugin binding authorization 不获得该字段或 header。
## HTTP Data Flow
```text
Mind HTTP read
-> public context reads exactly one workspace header
-> rejects invalid context with 403 scope_mismatch
-> calls Mind session authorization with cookie + account + workspace
-> adapter validates response account/workspace equality
-> route calls existing Bright read service with Mind-returned scope
```
会话列表、会话详情、消息历史与现有 public rebuild route 共用此 flowinternal `7777` summary route 不属于 Mind page,不变。CORS preflight 把精确 workspace header 加入 allow-list,仍要求精确 Mind Origin 和 credentialed CORS。
## WebSocket Data Flow and Fences
```text
GET /ws/mind?workspaceId=<id>
-> upgrade hook requires exactly one nonblank query value
-> request context pins it to this socket
-> hello scope must equal query workspaceId
-> session authorization captures cookie + pinned workspaceId
-> every hello/heartbeat/send/rebuild/publish authorization forwards pinned value
-> each existing authorization/version/binding/scope fence stays in force
```
Invalid query is rejected before endpoint creation with HTTP 403. A valid query but mismatching `ws.hello.scope.workspaceId` produces the existing WebSocket `scope_mismatch` close. `OneTalkMindSessionAuthorization` owns the per-socket captured workspace value; it rejects an invocation whose full Mind scope does not match that value before asking Mind.
Existing registry and pending-send fallback authorization calls (used when a manually registered/test connection has no session capability) must pass both account and workspace values from the connection. This preserves the same invariant for publish, rebuild and send fences rather than relying on a normally-present capability.
There is no new revocation push. As today, HTTP authorizes each request and WebSocket fences reauthorize before heartbeat, send, rebuild and publish. When Mind denies/changes binding/version/scope after a rebind, the next fence sends only its stable failure, unregisters/closes the old socket where existing flow requires, and performs no read, send, rebuild status publish or message publish.
## Error Mapping
| Boundary | Invalid condition | Center result |
| --- | --- | --- |
| Public HTTP | missing, blank or ambiguous workspace header | `403 { error: { code: "scope_mismatch" } }`, no Mind/Bright call |
| CORS preflight | workspace header with valid Mind Origin/method | `204`, includes `Sinan-Pilot-Workspace-Id` in allowed headers |
| WS upgrade | missing, blank or repeated `workspaceId` | HTTP `403`, no socket/session |
| WS hello | query workspace differs from hello scope | `ws.error scope_mismatch`, close `1008` |
| Mind response | returned workspace/account differs from requested values | existing authorization rejection `scope_mismatch` |
| Mind denial/unavailability | denied, timeout, malformed payload | existing stable failure/close behavior |
## Compatibility and Rollout
This is intentionally a breaking requirement for Mind page callers: header/query omission is rejected. Mind UI must deploy its HTTP header and WebSocket query changes before or with Center. During space switching it must close old socket, cancel old reads, clear state, establish the new connection, and reject late callbacks by its own epoch; those are Mind-owned changes and are not implemented here.
Center's message tables, composite fact identity, deduplication, history records and plugin protocol remain byte-for-byte outside this change. `/api/oss/harness-upload` is explicitly excluded.
Rollback is a code rollback of this Center contract only; no Center migration or data repair occurs.
@@ -0,0 +1,5 @@
{"file":".trellis/spec/server/backend/mind-authorization.md","reason":"Mind session authorization boundary, cookie forwarding, fail-closed scope validation, and existing WS fences."}
{"file":".trellis/spec/server/backend/error-handling.md","reason":"Stable HTTP and WebSocket error code and response-shape requirements."}
{"file":".trellis/spec/project/async-state-boundaries.md","reason":"Reauthorization and post-await invalidation rules for send, rebuild, and publish fences."}
{"file":".trellis/spec/project/module-organization.md","reason":"Ownership of the workspace input contract shared by HTTP, WS, and authorization adapter."}
{"file": ".trellis/tasks/09-14-workspace-authorization-rebinding/research/auth-context-research.md", "reason": "Current-worktree authorization chain audit and high-risk publish fallback evidence."}
@@ -0,0 +1,40 @@
# Implementation Plan
## 1. Define the Center authorization contract
- Extend the Mind-page authorization request scope with `workspaceId` in `packages/onetalk-contract/src/authorization.ts`.
- Make the mock authorization reader match Mind page records by both `channelAccountId` and `workspaceId`, retaining existing plugin behavior.
- Add a Center server workspace-context module as the single owner for the canonical header name and HTTP/WS boundary parsing.
## 2. Forward and validate Mind session workspace context
- Change `apps/server/src/mind-authorization.ts` so only session authorization adds `Sinan-Pilot-Workspace-Id`; preserve Cookie forwarding and JSON body shape.
- Require the returned Mind scope to match both requested account and workspace before returning an allowed decision.
- Propagate the workspace field through `readAuthorizationVersion` and all fallback Mind-page authorization calls.
## 3. Secure public HTTP reads and CORS
- In `apps/server/src/http/onetalk/public-context.ts`, parse workspace context before calling the authorization reader and return existing `scope_mismatch` on invalid input.
- Expand only the Bright public-read CORS allow-list/response to include `Sinan-Pilot-Workspace-Id`.
- Exercise list, detail, history and public rebuild authorization paths. Leave internal summary and OSS harness paths unchanged.
## 4. Pin WebSocket workspace context
- In `apps/server/src/websocket/index.ts`, validate the Mind WS query during upgrade and put the accepted value in request context.
- In `apps/server/src/websocket/handler.ts` and `apps/server/src/websocket/mind/*`, require hello/query equality and construct `OneTalkMindSessionAuthorization` with the pinned workspace.
- Preserve existing single-socket FIFO, registry generation, canonical-connection and post-authorization fences; update only their authorization request shape so heartbeat, send, rebuild and publish cannot omit workspace.
## 5. Tests and review
- Update contract/mock tests for the expanded Mind-page request scope.
- Extend `apps/server/test/mind-authorization.test.ts` for outgoing header, unchanged body, response workspace mismatch, and no plugin header.
- Extend `apps/server/test/onetalk-http.test.ts` for header-required reads, preflight and no reader/service call on invalid header.
- Extend `apps/server/test/websocket.test.ts` plus the focused WS fence tests for query-required connection, hello mismatch, query forwarding across heartbeat, and rebind failure blocking publish/send/rebuild.
- Run focused tests (each no more than 60 seconds), then `pnpm format:check`, `pnpm typecheck`, `pnpm build`, `pnpm test`, and `git diff --check`. Before any commit, run the required GitNexus change detection and inspect only task-owned diffs.
## Risk and Rollback Gates
- Before modifying each source symbol, run GitNexus upstream impact analysis and stop for HIGH/CRITICAL findings until reviewed.
- Do not alter authorization response schemas, error-code schemas, request JSON bodies, message storage, deduplication, history content, plugin handshake, or OSS harness upload.
- If Mind has not deployed the required header/query before Center rollout, all Mind-page reads and connections will fail closed by design; rollout requires coordinated deployment, not a fallback.
- A rollback restores the preceding Center code; no migration or data cleanup is involved.
@@ -0,0 +1,49 @@
# Center 空间授权 Header 透传与会话失效
## Goal
Center 从 Mind 页面 HTTP 请求的 `Sinan-Pilot-Workspace-Id` header 与 Mind 页面 WebSocket URL 的 `workspaceId` query 取得当前空间,并在每次向 Mind 请求授权时将其原样置入同名 header。这样 Mind 能以可信 Session、空间与 TM 账号信息作最终校验,Center 不会跨空间复用授权结果。
## Confirmed Facts
- Mind 页面三条 Bright 读取路由(会话列表、会话详情、消息历史)均经过 `createPublicReadContext` 的逐请求 `authorizeRead`;当前只从请求转发 Cookie,授权 request body 仅含 `channelAccountId``apps/server/src/http/onetalk/public-context.ts:74-102``apps/server/src/mind-authorization.ts:173-194`)。
- 读取 API 的 CORS 预检当前仅允许 `content-type`,因此浏览器携带空间 header 会被预检拒绝(`apps/server/src/http/onetalk/public-context.ts:17,136-164`)。
- Mind 页面 WebSocket 仅将 Cookie 与 Origin 保存到 request context`/ws/mind` 的 URL query 尚未被解析或传至授权 reader(`apps/server/src/websocket/index.ts:64-131`)。
- Mind 页面 WS 建连后保留 session-only authorization capability,心跳、发送和发布前重授信均会调用它;它当前未携带请求空间(`apps/server/src/websocket/mind/session-authorization.ts:11-42`)。
- 授权 reader 只允许两个固定 Mind endpoint,且以 Cookie 调用 session endpoint、失败关闭;这是扩展 header 的唯一对外授权调用点(`apps/server/src/mind-authorization.ts:12-13,101-148,151-229`)。
## In Scope
- [R1] 定义一个 Center 内部唯一 owner,用于解析并校验非空 `Sinan-Pilot-Workspace-Id` / `workspaceId`,并以精确名称把值转交 Mind 授权调用。
- [R2] 对会话列表、会话详情、消息历史以及已有的会话重建 HTTP 路径:缺少、空白或重复的空间 header 一律以稳定拒绝响应失败关闭;可用 header 必须在 CORS 预检中获准。
- [R3] 对 `/ws/mind?workspaceId=...`:缺少、空白或重复 query 一律在建连前拒绝;有效值绑定至该连接,并在 hello、心跳、发送、重建和推送前的重授权请求中持续转发给 Mind。
- [R4] 保持现有 HTTP 每次读取重新授权,以及 WS 关键副作用前重新授权的 fail-closed 行为;Mind 使旧空间/binding 失效后,Center 不得把旧连接缓存的授权用于推送、发送或读取/重建。
- [R5] 添加针对授权 reader、HTTP CORS/读取路由和 Mind WS 的回归,证明 header/query 被精确透传、缺失或伪造值被拒绝、以及 Mind 返回空间不匹配或旧授权失效时无数据/副作用泄漏。
## Out of Scope
- Mind 前端的 HTTP header 注入、WebSocket 重连、取消旧请求、清理状态和 callback epoch 防串写;Center 只定义并接收其接口契约。
- Mind 后端的可信 Session 用户读取、成员/角色/账号归属检查、binding 换绑事务、唯一约束、存量重复清理,以及后台纪要任务停止逻辑。
- Center 消息表、消息去重键、历史事实与新空间完整历史的读取语义。
- Chrome extension/plugin 的授权 payload、binding 存储或协议;它不接收 Mind 页面 workspace header/query。
- 仅供本地联调的 `/api/oss/harness-upload`;它没有 TM 账号或 binding 上下文,用户明确排除。
## Interface Contract for Mind
- HTTP reads use `Sinan-Pilot-Workspace-Id: <current workspace ID>` and credentials. On browser CORS preflight, the requested header must be allowed by Center.
- Mind page WebSocket connects to `/ws/mind?workspaceId=<current workspace ID>` and credentials; query parameter names are case-sensitive.
- On switching spaces, Mind UI must close the old socket, abort old reads, clear old data, then create requests/connection for the new space. It must ignore late callbacks from an old request or socket epoch.
- Center forwards the received value only to the fixed Mind session-authorization endpoint as `Sinan-Pilot-Workspace-Id`; it never trusts the value as a user identity or changes the request body contract.
## Acceptance Criteria
- [AC1] Each of the three HTTP read routes accepts a single nonblank workspace header, passes it unchanged to Mind authorization, and allows the browser CORS preflight for that exact header.
- [AC2] HTTP routes reject a missing, blank, or ambiguous workspace header before any Bright read or Mind authorization request; no other workspace fallback exists.
- [AC3] `/ws/mind` accepts only one nonblank `workspaceId`; it passes that value unchanged to every session authorization request associated with that socket.
- [AC4] A differing/denied result from Mind blocks the HTTP request and prevents old Mind WS connections from publishing, sending, or rebuilding after their next authorization fence; no old successful decision is reused.
- [AC5] Existing plugin WebSocket behavior, Center message storage, deduplication, and historical read projections remain unchanged.
- [AC6] Focused tests cover successful forwarding, HTTP preflight, rejected header/query cases, Mind-denied cross-space access, and post-invalidation WS fences.
## Open Questions
- None. The actual Mind UI/backend rollout and its persistence migration are explicitly owned outside this task.
@@ -0,0 +1,262 @@
# 研究:workspace authorization context 与 rebinding 边界
- 查询:审计 `OneTalkMindAuthorizationRequest`、内存 mock reader、Mind session authorization adapter、public HTTP/CORS、Mind WebSocket upgrade/session/registry fallback、发布/发送/rebuild fences 及现有测试夹具;使用 `rg`/带行号源码阅读/GitNexus `query``context``impact`
- 范围:internal
- 日期:2026-09-14
- 协调类别:cross-cutting
- 阻塞:`auth-context-research` 依赖的 contract/mock、Mind adapter、public HTTP context/CORS、Mind WS ingress/session/fallback、回归测试实现 scope;这些 scope 需要先共享同一 workspace request contract 和 boundary parser 结论。插件授权、消息存储/去重和 internal summary 路径不被本研究阻塞。
- 共享边界:`packages/onetalk-contract/src/authorization.ts` 的 Mind-page request contractCenter 内计划新增的 workspace context owner`apps/server/src/`,当前不存在);`apps/server/src/mind-authorization.ts` 的固定 session endpoint header`apps/server/src/http/onetalk/public-context.ts` 的 public HTTP/CORS context`apps/server/src/websocket/index.ts``handler.ts``websocket/mind/*` 的 request context`registry.ts`/`mind/publisher.ts`/`pending-send-coordinator.ts` 的 fallback authorization。
- 证据基线:当前 checkout `/Users/ybf/work/trade-message-center``HEAD=854e9c46eb64ac3ddc4d81a8cc1039cfb2f4d9a0``dev...origin/dev`;相关生产/测试文件无 dirty path,只有任务目录未跟踪。GitNexus 当前索引为同名 repo 的旧快照(本 checkout 索引记录 `c9468f91d30e511ff40715f62a7ca9b79c751f06`,落后 11 commits;另有 `/Users/ybf/code/trade-message-center-worktree` 快照落后 1 commit),故图谱仅作调用关系补充,当前源码/测试为最终事实。
- 复用证据与缺口:任务现有 `prd.md``design.md``implement.md` 已覆盖目标契约与 rollout;本报告刷新当前 source anchors。没有既有 `research/` 报告可复用。GitNexus 对匿名 type `OneTalkMindAuthorizationRequest` 未建节点,对许多箭头函数产生 Function/Const 双候选;下述静态搜索补足这些缺口。
## 发现
### 1. 当前 contract 与 mock reader
- `OneTalkMindAuthorizationRequest` 当前是 `packages/onetalk-contract/src/authorization.ts:86-92` 的 type,不是 interface。它的 `scope` 仅是 `Pick<OneTalkMindScope, "channelAccountId">``cookie?: string`;因此实现里虽然有些调用传入完整 `OneTalkMindScope`,类型契约仍只表达 account。Plugin request 在 `:78-84` 维持 `{ channelAccountId, deviceId } + binding`,不能把 workspace 字段或 header 扩展到 plugin contract。
- `OneTalkAuthorizationReader``:123-130` 是唯一 reader facade`readAuthorizationVersion` 仍按 `scope, binding?, cookie?` 传递,workspace 若要参与 Mind-page recheck,必须通过 Mind request scope 传入,而不能另建并行参数/缓存。
- `MockAuthorizationRecord``:142-149` 同时保存 plugin `scope` 与完整 `mindScope``recordKey` `:158-160` 只按 `[channelAccountId,binding]` 建 key,符合当前 plugin binding 语义;不能把它改成页面 workspace header 的事实源。
- `findRecord` `:177-195` 的 plugin 分支按 account/binding 匹配;Mind 分支 `:191-194` 只按 `record.mindScope.channelAccountId === request.scope.channelAccountId`,再优先 active。它目前会在同 account 的不同 workspace 间错误复用 active record;需要改成 account + workspace 精确匹配,且没有 workspace fallback。plugin 分支必须保持现状。
- `decisionForRecord` `:213-235` 只在 plugin binding 上做额外核对;对 Mind request 需要显式检查 requested workspace 与 record/decision workspace 一致,否则 mock 会成为第二个不完整事实源。
- `createMockAuthorizationReader` `:250-328` 由 package root `packages/onetalk-contract/src/index.ts:3-6` re-export;真实调用者来自 `apps/server/test/{websocket,onetalk-websocket,onetalk-http,onetalk-profile-websocket,onetalk-buyer-websocket}.test.ts``packages/onetalk-contract/test/contract.test.ts`,均通过 root import。`readAuthorizationVersion` `:268-293` 对 Mind 分支传完整 scope 对象,但当前类型只要求 account;扩展后应由该同一 request shape 承载 workspace。
### 2. Mind adapter 的真实外部调用点
- `apps/server/src/mind-authorization.ts:49-90``decisionFromResponse` 当前只校验返回 `mindScope.channelAccountId` 与 expected accountbinding endpoint 另校验 binding;成功 response 的完整 `mindScope` 会被返回。实现 workspace rebinding 时,session expected 值必须同时带 channel account 与 workspace,并在这里把返回 workspace 不匹配映射为 `scope_mismatch`
- `requestAuthorization` `:92-131` 是唯一实际 `fetch` helper;固定 POST、`content-type: application/json``redirect: "error"``AbortSignal.timeout`、Cookie 单次内存转发均应保留。当前 headers `:110-115` 只有 content-type 与可选 lowercase `cookie`,没有 `Sinan-Pilot-Workspace-Id`
- `createMindAuthorizationReader` `:150-231` 只允许固定 binding/session URL (`:12-13`)。plugin 分支 `:158-182` 不应获得 workspace headerMind session 分支 `:184-208` 才应把 request scope.workspaceId 原样加入精确 header,同时 body 仍只含 `{ channelAccountId }`。缺 Cookie 仍在 `:184-196` 先返回 `auth_required`,不可因 workspace 改动而发上游请求。
- `readAuthorizationVersion` `:212-229` 重新构造 heartbeat request,是 WS/其他版本 fence 的间接入口;Mind scope 的 workspace 必须随该 request 进入 adapter。当前 `decisionFromResponse`/reader 捕获异常的 unavailable 行为、safe diagnostics(不含 Cookie/workspace)应保留。
- 现有 adapter 测试 `apps/server/test/mind-authorization.test.ts:26-54,136-163,189-358` 已锁住 plugin body 无 Cookie、session Cookie/body、严格 status/JSON/transport failure;缺少 session workspace header、body 不增加 workspace、返回 workspace mismatch 和 plugin 不发该 header 的断言。
### 3. Public HTTP/CORS 链路
- `apps/server/src/http/onetalk/public-context.ts:17-18` 的 CORS allow-list 只有 `content-type``allowsCorsRequestHeaders` `:125-135` 按逗号拆分、lowercase、严格 allow-list 判断;`installPublicPreflight` `:142-165` 只返回 `access-control-allow-headers: content-type`。有效 Mind Origin/method 的预检在当前实现不会允许 workspace header。
- `authorizeRead` `:74-104` 是 public read/rebuild 共享授权 owneraccount 空值先 `403 scope_mismatch`Cookie 从 `request.headers.cookie` 读取,调用 reader 的 request scope 仅 `{ channelAccountId }`,授权成功后只复核返回 account 与 permission。workspace boundary 应在调用它之前解析并注入 scope;无效/缺失/重复值必须在任何 reader/read service 前返回 `403 scope_mismatch`
- `createPublicReadContext` `:168-240` 保存每请求 cutover epoch`mindOriginGuard` `:179-185` 先 admission、capture epoch、Origin 检查、CORS headers;其后 `authorizeRequestScope`/`authorizeRebuildScope` 都调用同一个 `authorizeRead`。这说明 workspace parser 应位于该 context 或其唯一 boundary owner,不应在 list/detail/messages/rebuild 四路由各自复制。
- `apps/server/src/http/onetalk/public.ts:17-27` 组合 preflight、列表、详情、消息、rebuild;四个路由分别在 `conversation-list.ts:20-60``conversation.ts:21-53``messages.ts:17-50``rebuild.ts:22-49` 使用同一 `mindOriginGuard` 与 context authorization。列表/详情在 service await 后有 `requestIsAdmitted`messages/rebuild 也有共享 read/rebuild fence;这些 post-await checks 不能被 workspace patch 删除。
- `apps/server/src/http/onetalk/summary.ts:37-84` 是 internal 7777 summary route,使用 account-only scope、时间窗和 cutover admission,无 Cookie/CORS;任务明确排除它,不能把 public workspace header 扩展到此路径。
- `apps/server/test/onetalk-http.test.ts:141-144``headers()` 只提供 Cookie + Origin,被大量 public route 测试复用;`627-697` 的预检测试目前断言只允许 `content-type` 并拒绝 retired summary headers`232-265``267-337``339-393` 覆盖 list/detail/history 的 service scope/错误边界。加入强制 header 后,测试 fixture 必须统一提供一个有效 header,另补缺失、空白、重复/ambiguous header 在 reader/service 前零调用的探针。
### 4. Mind WebSocket upgrade、session capability 与 fallback
- `apps/server/src/websocket/index.ts:52-132``registerWebsocketRoutes``onRequest` 只根据 `request.url.startsWith` 判 route type、检查 cutover 与精确 Origin`:66-99`);`endpointOptions` `:101-121` 只把 Cookie/Origin/origin allow-list 放入 `requestContext`。Mind query 未解析,`/ws/mind` 当前缺少 workspace 仍能进入 socket handler。
- `installWebsocket` `:135-180` 构造 shared registry/router 并注册 `/ws/plugin``/ws/mind`;直接安装未注入 reader 时保持 `createUnavailableAuthorizationReader`,不可因新增 query 解析改变 plugin/default fail-closed 语义。
- `apps/server/src/websocket/handler.ts:53-65``OneTalkWebSocketHandlerOptions.requestContext` 没有 workspace;通用 handler `:217-251` 只把 authorization/requestContext 交给 endpoint`dispose` `:307-326` 清理 session capability、unregister。workspace 一旦通过 upgrade 接受,应放入同一 request context,并在 unregister/close 的代际边界保持不可复用。
- `apps/server/src/websocket/mind/index.ts:35-173` 是 Mind endpoint owner。`onHello` `:93-169` 先创建 `OneTalkMindSessionAuthorization`(当前只传 reader + cookie`:97-100`),检查 Mind Origin/shape,调用 connect authorization `:112-117`,再检查 hello frame scope 与 Mind 返回 scope 完整相等 `:130-134`,最后把 capability 写入 state/registry `:145-168`。query workspace 应在 upgrade 前验证并与 hello scope workspace 比较;有效 workspace 应绑定 capability,而不是只相信 hello。
- heartbeat `:59-89` 优先走 `state.sessionAuthorization.authorize(frame.scope, "heartbeat")`,无 capability 时 fallback 到 `context.authorize`,并在授权 await 后检查 policy、`sessionAuthorizationFailure`、recordHeartbeat;这些既有顺序必须保持。hello/query mismatch 应稳定 `scope_mismatch`/1008,且不能调用 reader/Bright service。
- `apps/server/src/websocket/mind/session-authorization.ts:11-43``OneTalkMindSessionAuthorization` 是不可序列化 session capability owner:当前私有状态只有 reader、Cookie、revoked`authorize` `:25-39` 将 operation/scope/cookie 转给 reader`clear` `:41-43` 清 Cookie 并禁止后续调用。它是最窄的 workspace 持有点:应捕获 pinned workspace,拒绝调用 scope.workspaceId 与 pinned 值不一致,并保持 clear 后 `auth_required`、不泄漏 Cookie 的行为。
- `apps/server/src/websocket/registry.ts:167-224``publishRebuildStatus` 对每个 Mind connection 在 publish 前重新授权。若有 session capability,走 `authorize(connection.scope, "rebuild")`;否则 `:186-190` fallback 只传 `{ channelAccountId }`。手工注册/测试 connection 可能无 capability,这条 fallback 也必须带 workspace;授权后仍需复核 full scope、binding、version、permission、canonical connection、socket open、epoch/policy`:191-203`),最终 `socket.send` 前不能插入 await。
- `apps/server/src/websocket/mind/publisher.ts:101-221``publishToConnections` 覆盖 `message.created``sync.status``conversation.updated` 与 plugin status。它在 `:128-135` 对 Mind connection 优先 session capability,否则同样只传 account 的 fallback。授权失败会发送稳定 error、unregister/close`:165-197`);最后一次授权后的 canonical/generation/open/epoch fence `:200-210` 必须保留。GitNexus 对该 symbol 的 upstream impact 为 `HIGH`4 个 direct callers3 个 publish execution flowsmessage/sync/conversation),所以改动前必须由主会话再次核对当前索引/源码并显式审查。
- `apps/server/src/websocket/pending-send-coordinator.ts:123-287``requestSend` 在第一次 authorization await 前同步 reserve request IDMind authorization `:211-224` 优先 session capability,否则 fallback `:216-220` 只传 account。workspace 必须进入 capability/fallback,但必须保留双端 auth、canonical/generation/lease/epoch/open 的 final no-await fence `:239-257`,不可因为字段变更改变 `rejected_before_send`/`delivery_unknown` 映射。
- confirmation path `:289-365` 只重新授权 plugin,故不应新增 Mind workspace header;它的 claim-before-await、commit guard、terminal/no-op 语义是回归基线。
- `apps/server/src/websocket/connection-store.ts:19-43,80-101,142-159,166-243` 已将完整 `mindScope`(包含 workspace)存于 registered connectionplugin owner key `:80-82` 已按 mindUser/workspace/account 区分。该事实来源应继续由 connection/session owner 保存,不能在 publisher/pending/registry 各自复制 pinned workspace state。
### 5. Mock/harness 与现有 fixture 边界
- `apps/mind-test-harness/src/authorization/session.ts:13-21,38-71` 的 mock session request 当前只严格接受 body `{ channelAccountId }`,用 Cookie 做 Session 校验,随后按 account 与 fixture scope 返回 authorization;它不读取 workspace header。该 harness 是 Mind-side mock,不是 Center adapter,若本任务只改 Center,不能把 header 解析职责倒灌到 harness;但联调回归必须明确它尚未模拟 workspace header/mismatch。
- `apps/mind-test-harness/src/authorization/binding.ts:13-25,38-70` 的 binding request 仅 account+binding,必须保持不带 Cookie/workspace`src/response.ts:37-47` 返回完整 fixture `mindScope`
- `apps/mind-test-harness/src/config.ts:115-153` fixture 默认 workspace 为 `workspace-1``harness/reading.ts:50-65``readResponse` 支持传 headers 但当前 list/history 调用 `:161-163``:206-211` 未注入 workspace header`harness/websocket.ts:150-154,189-221` 当前构造 `/ws/mind` URL 无 query。根据 PRD,这些是 Mind UI/rollout owner 的接口缺口,不应成为 Center 内部 fallback;真实浏览器联调仍未执行。
- Server WS fixture `apps/server/test/websocket.test.ts:187-195``apps/server/test/onetalk-websocket.test.ts:319-325``openSocket` 当前所有 Mind path 都不带 query`connectMindPage` helpers `:344-378`/`onetalk-websocket.test.ts:344-378` 的 hello 仍是 scope-only。测试改造需先统一 helper 的 valid workspace URL,再专门覆盖 missing/blank/repeated query 与 hello mismatch。
## 候选 Scope 与依赖
### Scope ACenter workspace contract + parser ownercross-cutting,先行)
- 语义职责:定义精确 outgoing header 名 `Sinan-Pilot-Workspace-Id`、入站 canonical lowercase lookup、HTTP header/WS query 单值非空/重复检测和窄 workspace value;不处理 Cookie、Mind user、channel account、authorization response 或业务错误之外的流程。
- 交付物/拥有路径:`packages/onetalk-contract/src/authorization.ts` 的 Mind scope request contract`apps/server/src/` 下新的 Center-only context/model 模块(具体文件名由实现设计决定)。
- 排除路径:plugin request/authorization、Mind DB、harness UI、internal summary、message facts。
- 接口依赖:A 必须先于 B/C/D/E;所有 Mind HTTP/WS writer 都等待 parser 名称和值语义稳定。workspace 原样透传意味着 parser 应验证空白但不 trim 后重写 outgoing value;设计需明确是否允许内部检查 trim、但传输保持原字节。
- 并行性:A 与任何修改依赖其类型/解析结果的 writer 不可并发;A 稳定后,Badapter)与 C(HTTP)可在代码写入上并行,但两者都必须使用同一 parser/contract,不得各自定义字段名。
### Scope BMind session authorization adapter + mock readerpackage/adapter-local,依赖 A
- 语义职责:让 `OneTalkMindAuthorizationRequest.scope` 表达 account+workspacemock 按双键匹配 Mind recordsession adapter 原样发 header、body 保持 account-only、响应双 scope equalityplugin adapter byte-for-byte 保持现状。
- 拥有路径:`packages/onetalk-contract/src/authorization.ts``apps/server/src/mind-authorization.ts`;相关 contract/adapter tests。
- 排除路径:HTTP route parsing、WS query ingress、Mind backend/migration。
- 实际共享写入:contract type 会触发所有手写 `OneTalkAuthorizationRequest` fixture/reader wrapper 的类型更新;必须搜索并更新 server tests,不能只改 production adapter。
- 依赖/可并行:等待 A;B 完成后 C/D/E 才能验证 successful forwarding。B 内 mock 与 adapter 可独立实现,但共享 request shape,推荐先 contract type/mock,再 adapter。
### Scope CPublic HTTP reads + CORSpackage-local,依赖 A+B
- 语义职责:在 `createPublicReadContext` 统一解析 workspace,缺失/空白/重复在 reader/read service 前返回 `403 scope_mismatch`;将 workspace 传给 reader;只把精确 header 加入 public read preflight;保留 list/detail/messages/rebuild 的 epoch/post-await/error semantics。
- 拥有路径:`apps/server/src/http/onetalk/public-context.ts``public.ts`、四个 public route 文件与 `apps/server/test/onetalk-http.test.ts`
- 排除路径:`summary.ts` internal route、OSS harness upload CORS、Mind UI header injection。
- 依赖:B 的 reader request typeC 不能直接修改 adapter response logic。A+B 稳定后可与 D 并行(write-disjoint),但 C 的 test fixture 不应和 D 的 WS helpers 共享可变 state。
### Scope DMind WebSocket ingress + pinned session capabilitypackage-local,依赖 A+B
- 语义职责:升级前严格接受 exactly-one nonblank `workspaceId` query;把 pinned value 放 request contexthello/query/full-Mind-scope equality`OneTalkMindSessionAuthorization` 捕获 workspace 并贯穿 hello/heartbeat/send/rebuild/publish authorization;保留 Origin、Cookie、FIFO、close and policy fences。
- 拥有路径:`apps/server/src/websocket/index.ts``handler.ts` request context type、`websocket/mind/index.ts``mind/session-authorization.ts` 与 WS upgrade/hello tests。
- 排除路径:plugin query/header、Mind UI reconnect/epoch clearing、protocol wire scope schema。
- 依赖:B 的 request shapeD 先于 E 的 fallback/publish writer,因为 E 需知道 connection capability/pinned workspace 如何表达。D 与 C 可并行,均是 B-ready/write-disjoint;两者不具备 shared-write-free 的测试 fixture(各自改 helpers),测试文件冲突需主会话协调。
### Scope ERegistry/publisher/pending fallback fencescross-cutting downstream,依赖 A+B+D
- 语义职责:补齐无 session capability 的 manually registered/test Mind connection fallback;每个 fallback request 带 account+workspace;保留 reauth-before-side-effect、canonical/generation/epoch/socket fences;确保 revoked/mismatch 后 no publish/no send/no rebuild/read。
- 拥有路径:`apps/server/src/websocket/registry.ts``mind/publisher.ts``pending-send-coordinator.ts`,必要时 `history-rebuild-coordinator.ts` 仅确认 plugin branch 不受影响;对应 `onetalk-websocket.test.ts``onetalk-history-rebuild-coordinator.test.ts`
- 排除路径:plugin authorization semantics、confirmation plugin-only reauth、message repository/DB schema。
- 依赖:必须等待 D 对 capability pinned workspace 的最终形状;否则容易添加第二个 fallback truth。E 可在 D 完成后与 C 的测试补充并行,但必须单独处理 GitNexus 标记的 `publishToConnections` HIGH blast radius。
### Scope F:回归矩阵(跨层验证,依赖 A-E)
- 语义职责:证明 HTTP header/query exact forwarding、invalid ingress zero-call、Mind response mismatch, old connection post-invalidation fences;保持 existing client-frame wire matrix。
- 拥有路径:`packages/onetalk-contract/test/contract.test.ts``apps/server/test/mind-authorization.test.ts``apps/server/test/onetalk-http.test.ts``apps/server/test/websocket.test.ts``apps/server/test/onetalk-websocket.test.ts`,必要时 history rebuild focused test。
- 排除路径:harness/real Mind rollout acceptance;后者可作为 runtime gap 记录,不应在 Center 测试中伪造成功。
- 可并行:各 focused test file 在代码 contract 完成后基本 write-disjoint;不得在 contract type 未稳定时并行修改几十个 fixture,避免重复修复和第二套测试 helper。
## Invariant 与验收探针
### 结构性 invariant / owner / facts source
1. 一个 Mind-page authorization decision 只有在 `requested workspace == Mind returned mindScope.workspaceId == response/registered WS state.workspaceId` 时有效;`mindScope.channelAccountId` 同理。workspace 不是身份,不从 user/header/account 推导。唯一 owner 分层:boundary context owner 负责取得/校验值,Mind adapter 负责 returned-scope equalityWS session/connection owner 负责 pinned lifetimepublisher/registry/pending 只消费窄 capability/connection,不复制 parser/decision。
2. HTTP fact source 是每次请求精确一个 `Sinan-Pilot-Workspace-Id` headerWS fact source 是 upgrade URL 精确一个 case-sensitive `workspaceId` query。缺失、空白、重复/ambiguity 均 fail closed;不得默认 workspace、复用旧 decision、从 hello-only scope fallback。
3. Mind adapter 是唯一 Center 对外授权 HTTP writer。session only 发 `Cookie` + `Sinan-Pilot-Workspace-Id` + JSON body `{ channelAccountId }`plugin binding 不得发 Cookie/workspace header。Mind 返回值是 scope/permission/version 的事实源,Center 不连 Mind DB。
4. 任何旧 WS cached successful decision 不能跨过下一次 authorization fenceheartbeat、send、rebuild、publish(以及 session capability 的 read)都必须 reauthorizedenied/mismatch/unavailable 后不得继续 read/send/rebuild/status/message publish。HTTP 每次 request 仍授权一次,且 read service await 后复核 epoch/admission。
### Snapshot、await、mutation、副作用和 rollback 映射
| 边界 | 当前 anchor | 需要保持/新增的控制点 | 不可逆副作用与失败语义 |
| --- | --- | --- | --- |
| HTTP ingress | `mindOriginGuard` captured epoch + request header | parser 在 `authorizeRead` 前;invalid 不触 reader/service | 无读取/授权请求;稳定 `403 scope_mismatch` |
| HTTP adapter | `authorizeRead` account/Cookie + adapter fetch | request scope 带 workspaceresponse account/workspace equality | Mind call 是外部副作用,但失败映射现有 auth codes/unavailable;不缓存 |
| HTTP read | list/detail/messages `await readService` | `requestIsAdmitted` after await;返回 scope 是 Mind returned scope | 失效后不返回数据,`503 authorization_unavailable`;不把 partial result 发出 |
| HTTP rebuild | `authorizeRebuildScope` → registry await | authorization/rebuild permission + request admission before/after | reset 是 commit boundarypost-commit resync 失败不回滚 committed reset,但不发失效 status |
| WS upgrade | `onRequest` Origin/cutover | parse query before endpoint/socket session; exactly-one workspace | invalid query HTTP 403;不创建 authenticated session/registry entry |
| WS hello | `onHello` connect authorization await | query workspace == hello scope == returned scopepolicy check after await | accepted frame/registry register only after all checksmismatch 1008/`scope_mismatch` |
| WS capability | session auth `authorize` | capture Cookie + pinned workspace; `clear` invalidates both | clear/unregister/pause 后 future call `auth_required`Cookie 不出诊断/frames |
| WS heartbeat/send/rebuild/publish | each capability/fallback authorization await | post-await policy/canonical/generation/open/full-scope fence; no await before send | no heartbeat ACK/send.command/status/message.created/rebuild reset when fence fails |
| send confirmation | pending claim before authorization, guarded process | plugin-only reauth remains plugin-only; no workspace addition | DB commit → ACK/publish ordering and delivery_unknown semantics unchanged |
### Static/unit probes(本研究未运行 runtime probe
- `rg -n "scope: \{ channelAccountId:|connectionType: \"mind_page\"|authorize\(connection\.scope|authorize\(connection\.mindScope" apps/server/src packages/onetalk-contract/src`:实现后预期所有 Mind-page authorization request(含 publisher/registry/pending fallback)都能看见 workspace-bearing scopeplugin branch 仍 account/device/binding-only。
- `rg -n "Sinan-Pilot-Workspace-Id|workspaceId" apps/server/src/mind-authorization.ts apps/server/src/http/onetalk/public-context.ts apps/server/src/websocket`header 名只由 context/adapter owner 使用;public CORS allow-list 和 WS query parser 位置明确,无第二个常量。
- `rg -n "authorize-session|headers:|body:" apps/server/src/mind-authorization.ts` + adapter testssession outgoing header exact、body 精确等于 `{ channelAccountId }`、plugin request header 无 Cookie/workspace。
- `rg -n "MIND_SESSION_AUTHORIZATION_PATH|MIND_BINDING_AUTHORIZATION_PATH|scope.*channelAccountId" packages/onetalk-contract apps/server apps/mind-test-harness`:固定 endpoint、contract shape、harness body contract 无意外漂移。
- Contract/unit matrixmock records same account/different workspacerequested workspace A only matches Amissing/blank/unknown returns `scope_mismatch`; plugin same account/different device remains allowed when binding matches; revoked/unavailable/permission errors retain old codes。
- HTTP unit matrixvalid single header → one reader call with unchanged workspace; missing/empty/duplicate → `403` and zero reader/read-service/registry calls; OPTIONS with exact header → `204` and allow-headers includes exact header; retired identity headers remain irrelevant; internal summary unchanged.
- WS unit/wire matrix via `injectWS`missing/blank/repeated query rejected before socket endpoint; valid query + hello same workspace accepts; valid query + different hello closes 1008 with `scope_mismatch`; captured outgoing Mind auth requests all preserve workspace across connect/heartbeat/send/rebuild/publish; manually registered no-capability fallback also includes workspace.
- Fence/latch matrixpause/revoke/rebind during each authorization `await` yields no subsequent `socket.send`, reset, ACK, publish or read response; ensure `publishToConnections` HIGH-risk flows each assert no `message.created`, `sync.status`, `conversation.updated` after mismatch.
- `git diff --check`, focused contract/server tests, `pnpm format:check`, `pnpm typecheck`, `pnpm build`, `pnpm test` are implement/check gates; use <=60s timeout for each backend unit invocation as project instructions require.
### Runtime probes(未执行;需要独立端口/真实或本地 Mind/mock/浏览器状态)
- Start isolated Mind mock + Center, make credentialed browser CORS OPTIONS/GET with one workspace header; expected 204 allow exact header and read request observed by Mind with exact header/body/Cookie. Do not treat process startup alone as evidence.
- Open real `/ws/mind?workspaceId=A` with Mind Origin/Cookie, observe connect/heartbeat/send/rebuild/publish authorization request headers; switch mock to workspace B/revoke old scope and assert next fence closes/blocks old socket. Use isolated ports and no production credentials.
- Run harness browser flow only after its own UI has been updated to send HTTP header and WS query; current harness source does not do so (`harness/reading.ts:50-52`, `harness/websocket.ts:150-154`), so a current run would be an expected integration failure, not a Center regression proof.
- Real Mind backend Session/member/binding checks, persistence rebinding transaction, revocation push absence, and browser callback epoch behavior remain external/unverified boundaries explicitly outside this scope.
## 影响与 GitNexus upstream impact 结果
以下是对拟修改 symbol 的当前索引 upstream 检查;因索引落后,已用当前源码 `rg`/行号复核 direct callers
- `createMockAuthorizationReader` (`packages/onetalk-contract/src/authorization.ts:250`)GitNexus `LOW`, direct `0`type/function dual representation and package-root import edges undercount); current search shows five server fixture files plus contract tests.
- `createMindAuthorizationReader` (`apps/server/src/mind-authorization.ts:150`)`LOW`, direct `2` (`apps/server/test/mind-authorization.test.ts`, `apps/server/test/websocket.test.ts`); `app.ts` imports it through `authorizationFor` as an additional source-level caller.
- `authorizeRead` (`apps/server/src/http/onetalk/public-context.ts:74`)`LOW`, direct `2` (`authorizeRequestScope`, `authorizeRebuildScope`); both are exposed through one public context used by four public routes.
- `createPublicReadContext` (`apps/server/src/http/onetalk/public-context.ts:168`)`LOW`, direct `1` (`installOneTalkReadRoutes`), one `Onetalk` process and eight transitive test/app symbols.
- `installPublicPreflight` (`apps/server/src/http/onetalk/public-context.ts:142`)`LOW`, direct `1` (`installOneTalkReadRoutes`); its returned allow-list controls all public read/rebuild OPTIONS routes.
- `registerWebsocketRoutes` (`apps/server/src/websocket/index.ts:52`)`LOW`, direct `1` (`installWebsocket`), with `createOneTalkMindWebSocketHandler` and plugin handler downstream.
- `OneTalkMindSessionAuthorization` (`apps/server/src/websocket/mind/session-authorization.ts:11`)`LOW`, direct `2` (`onHello` and `mind/index.ts` import), but transitive app/index/test callers; current index confirms direct `onHello` construction.
- `publishToConnections` (`apps/server/src/websocket/mind/publisher.ts:101`)`HIGH`, direct `4` (`publishPluginStatus`, `publishMessageCreated`, `publishSyncStatus`, `publishConversationUpdated`), 3 affected processes and Mind module. Main session must warn/review before editing this symbol.
GitNexus additionally reports `createOneTalkConnectionRegistry` (`registry.ts:99`) `LOW`, direct test callers 3, and `authorizePlugin` (`history-rebuild-coordinator.ts:97`) `LOW`, direct request/handleCompletion 2. The latter is plugin-only and should remain unchanged except compile-time request-shape fallout.
## 找到的文件
- `packages/onetalk-contract/src/authorization.ts` — OneTalk authorization request/decision types, mock record lookup and reader implementations。
- `packages/onetalk-contract/src/index.ts` — contract package root re-exports authorization API consumed by server/tests。
- `apps/server/src/mind-authorization.ts` — fixed Mind binding/session HTTP adapter, response decoder integration, Cookie/header construction and diagnostics。
- `apps/server/src/http/onetalk/public-context.ts` — public read/rebuild authorization, CORS preflight and cutover request context。
- `apps/server/src/http/onetalk/public.ts` — public route composition。
- `apps/server/src/http/onetalk/conversation-list.ts` — public list route using shared context。
- `apps/server/src/http/onetalk/conversation.ts` — public detail route using shared context。
- `apps/server/src/http/onetalk/messages.ts` — public history route using shared context。
- `apps/server/src/http/onetalk/rebuild.ts` — public destructive rebuild route using rebuild authorization/context。
- `apps/server/src/http/onetalk/summary.ts` — internal account-only summary route; explicitly out of scope。
- `apps/server/src/websocket/index.ts` — WS Origin/cutover upgrade hook, requestContext assembly and endpoint registration。
- `apps/server/src/websocket/handler.ts` — shared handler requestContext/state/dispose and policy/session fences。
- `apps/server/src/websocket/mind/index.ts` — Mind hello/heartbeat/send endpoint and session capability construction。
- `apps/server/src/websocket/mind/session-authorization.ts` — per-socket Cookie/session capability。
- `apps/server/src/websocket/mind/publisher.ts` — Mind event reauthorization, full-scope/generation fence and socket publish。
- `apps/server/src/websocket/registry.ts` — canonical connection registry, rebuild-status fallback authorization and commit guards。
- `apps/server/src/websocket/pending-send-coordinator.ts` — pending send state, Mind/plugin authorization, final dispatch fence and confirmation path。
- `apps/server/src/websocket/connection-store.ts` — registered connection full `mindScope`, canonical/generation/presence state and commit guard owner。
- `apps/server/src/websocket/history-rebuild-coordinator.ts` — plugin-side rebuild authorization/reset flow; current workspace use is source context, not Mind-page adapter。
- `apps/server/test/mind-authorization.test.ts` — adapter request/response/status/Cookie/diagnostic fixtures。
- `apps/server/test/onetalk-http.test.ts` — public read/rebuild/CORS/internal summary fixtures and request helper。
- `apps/server/test/websocket.test.ts` — WS handshake, Origin, Cookie, fallback/default authorization fixtures and `openSocket` helper。
- `apps/server/test/onetalk-websocket.test.ts` — wire matrix, publisher/send/rebuild fence fixtures and `openSocket`/Mind helpers。
- `apps/server/test/onetalk-history-rebuild-coordinator.test.ts` — rebuild clear/reset/post-commit and revoked Mind status fixture。
- `apps/server/test/app.test.ts` — app/route registration and safe Mind adapter diagnostics fixture。
- `apps/server/test/onetalk-profile-websocket.test.ts` — plugin profile authorization and connection fixtures; preserve plugin behavior。
- `apps/server/test/onetalk-buyer-websocket.test.ts` — plugin buyer-fact authorization and connection fixtures; preserve plugin behavior。
- `packages/onetalk-contract/test/contract.test.ts` — canonical decoder/mock authorization/revoke/permission contract tests。
- `apps/mind-test-harness/src/authorization/session.ts` — local Mind mock session body/Cookie contract; does not yet model workspace header。
- `apps/mind-test-harness/src/authorization/binding.ts` — local Mind mock binding body contract; must remain account+binding only。
- `apps/mind-test-harness/src/config.ts` — local fixture defaults including `workspace-1`
- `apps/mind-test-harness/src/harness/reading.ts` — current browser read fetches with credentials but no workspace header。
- `apps/mind-test-harness/src/harness/websocket.ts` — current browser Mind WS URL has no `workspaceId` query。
## 代码模式
- `apps/server/src/http/onetalk/public-context.ts:179-185` — capture cutover epoch before external authorization/read; Origin/CORS is boundary-only。
- `apps/server/src/http/onetalk/public-context.ts:74-104` — shared authorization result maps stable status/code, then checks returned account and required permission。
- `apps/server/src/http/onetalk/read.ts:97-120` — after read await, re-check admission before exposing data; database/unknown errors map to stable codes。
- `apps/server/src/mind-authorization.ts:92-131` — one fixed fetch helper, timeout/redirect policy, no raw error payload/diagnostic secret。
- `apps/server/src/mind-authorization.ts:184-208` — session Cookie required before fixed session endpoint call; plugin branch must stay separate。
- `apps/server/src/websocket/mind/index.ts:112-168` — hello auth → policy fence → full returned scope comparison → state/registry registration → accepted frame。
- `apps/server/src/websocket/mind/session-authorization.ts:25-43` — capability captures opaque Cookie, reauths per operation, `clear()` revokes and removes Cookie。
- `apps/server/src/websocket/mind/publisher.ts:126-210` — auth await, returned binding/version/full-scope/permission comparison, canonical/generation/open/epoch fence, immediate socket send。
- `apps/server/src/websocket/pending-send-coordinator.ts:172-205,211-285` — reserve before first await; no-await final fence before wire send; terminal cleanup owns pending maps。
- `apps/server/src/websocket/registry.ts:174-224` — rebuild status reauth per candidate and skips stale/mismatched connections without changing committed reset semantics。
- `apps/server/src/websocket/connection-store.ts:142-159,166-243` — canonical connection/generation/epoch commit guard and plugin owner key including workspace。
- `apps/server/test/onetalk-websocket.test.ts:42-56,422-560` — client frame matrix derived from `ONETALK_CLIENT_FRAME_TYPES`; wire assertions, not private handler calls。
## 已验证事实、假设与外部/未验证边界
### 已验证事实
- 当前 checkout/HEAD/dirty state 如证据基线所述。
- Center 当前 Mind request contract 只表达 accountadapter session header 只转发 Cookiepublic preflight 只允许 content-typeWS upgrade/session request context 不含 query workspace。
- Mind connection state/registry 已保存完整 returned `mindScope`,但 fallback authorization 在 publisher/registry/pending 中把它缩成 account-only request。
- HTTP 路由和 WS 关键副作用已经有 post-await policy/canonical/generation/permission fences;插件 binding/device/owner key 行为已有测试覆盖。
- GitNexus upstream impact 对拟改高层 symbol 已执行;最高风险为 `publishToConnections=HIGH`,其余已列 symbol 返回 LOW/类型节点未找到。
### 假设/实现前待确认
- 新 workspace context owner 的具体文件名和导出 facade 未在当前树中存在;由实现设计决定,但必须只有一个 parser/header constant owner。
- “重复 header/query”需要在 Fastify/Node request 表示层验证为 ambiguous;不能依赖框架是否把多值合并成逗号字符串而误放行。实现应针对实际 `string|string[]` 与 raw URL 行为补 unit probe。
- parser 可以内部使用 `trim()` 判断 blank,但设计目标要求向 Mind 转发接收值原样;是否允许周围空格作为非空值必须由实现/测试锁定,不能隐式 trim 后转发。
- `OneTalkMindSessionCapability.authorize` 扩展 workspace 的最终签名尚未实现;建议 capability 捕获 pinned workspace,并继续要求每次调用传完整 Mind scope 以支持 frame scope equality。
### 外部/未验证边界
- 真实 Mind Session 是否会读取该 header、如何校验用户/member/binding、换绑事务/唯一约束、远端 revocation push 均未从外部系统验证,且在 PRD 中排除。
- 当前没有启动 Center/Mind/mock、没有使用 ports/database/container/测试账号、没有进行浏览器 CORS/WS smoke;上述 runtime probes 仅是可执行计划,不是运行时证明。
- `apps/mind-test-harness` 当前不会发送 header/queryCenter contract rollout 需要 Mind UI/harness 先后或同步部署,不能在 Center 中加入 fallback。
## 外部参考(文档、版本)
- 未进行互联网/外部文档检索;本报告依据当前 checkout 与仓库规范。
- 相关 repo specs`.trellis/spec/server/backend/mind-authorization.md`Mind fixed endpoints/Cookie/CORS/WS/fail-closed)、`.trellis/spec/server/backend/error-handling.md`stable HTTP/WS errors)、`.trellis/spec/server/backend/service-foundation.md`Fastify/WS lifecycle)、`.trellis/spec/project/async-state-boundaries.md`(await 后重新确认与副作用顺序)、`.trellis/spec/project/module-ownership.md``module-organization.md`(唯一 contract/parser owner)、`.trellis/spec/guides/cross-layer-thinking-guide.md`
- 工具链事实:Node `v22.22.2`、pnpm `11.7.0`、workspace package version `0.8.27`;不得把这些本地版本信息当作真实 Mind 生产版本证明。
## 相关 spec
- `.trellis/spec/server/backend/index.md`
- `.trellis/spec/server/backend/mind-authorization.md`
- `.trellis/spec/server/backend/error-handling.md`
- `.trellis/spec/server/backend/service-foundation.md`
- `.trellis/spec/project/architecture.md`
- `.trellis/spec/project/async-state-boundaries.md`
- `.trellis/spec/project/module-ownership.md`
- `.trellis/spec/project/module-organization.md`
- `.trellis/spec/guides/cross-layer-thinking-guide.md`
## 注意事项 / 未找到内容
- 未找到现成 Center workspace context/parser 模块;不能让四个 public route、WS upgrade、adapter 各自复制 header/query 解析。
- 未找到 `OneTalkMindAuthorizationRequest` 的 GitNexus 节点;这是索引能力缺口,不是源码中缺少定义。
- 未找到现有 session authorization workspace header/query 回归;当前 tests 证明的是 Cookie/account-only 行为,需要显式扩展。
- 未找到真实 Mind/浏览器运行时证据;不要把本地 mock 或启动成功描述为跨空间授权已验证。
- 高风险提醒:修改 `publishToConnections` 前主会话必须确认 GitNexus HIGH blast radius,并检查 message/sync/conversation publish 三条 execution flows;任何失败都必须保持 no publish/no stale socket。
@@ -0,0 +1,26 @@
{
"id": "workspace-authorization-rebinding",
"name": "workspace-authorization-rebinding",
"title": "空间授权边界与换绑失效",
"description": "统一 Center 与 Mind 的空间授权、换绑和旧会话失效边界",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "ybf",
"assignee": "ybf",
"createdAt": "2026-09-14",
"completedAt": "2026-09-14",
"branch": "dev",
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}