{"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."}
`workspaceId` 是 Mind 页面请求声明的空间选择,不是身份。Center 只在 Mind 页面边界接收它,将其附带 Cookie 和 TM `channelAccountId` 转交固定 Mind session-authorization endpoint;Center 不查询 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,统一拥有:
-> 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` |
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.
{"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."}
- 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.
- 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.
| 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 |
- Contract/unit matrix:mock records same account/different workspace;requested workspace A only matches A;missing/blank/unknown returns `scope_mismatch`; plugin same account/different device remains allowed when binding matches; revoked/unavailable/permission errors retain old codes。
- HTTP unit matrix:valid 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 matrix:pause/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.
- 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/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/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/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。
"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":{}
}
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.