mindspace: close authority boundaries

This commit is contained in:
john
2026-07-27 15:34:35 +08:00
parent dfab78c75a
commit e94052ff24
78 changed files with 11962 additions and 2162 deletions
+31
View File
@@ -102,6 +102,37 @@ launchctl kickstart -k "gui/$(id -u)/cn.tkmind.mindspace-service"
pnpm dev
```
如需验证 Goose 的逻辑 workspace MCP,再让 Portal 与 MindSpace Service
使用相同的本地签名 secret;该值至少 16 个字符,只放本机 `.env`
不得提交:
```bash
MINDSPACE_MCP_BASE_URL=http://127.0.0.1:8082
MINDSPACE_MCP_TOKEN_SECRET=replace-with-a-local-secret
# 可选;默认 12 MiB,必须大于等于 1024
MINDSPACE_MCP_MAX_BODY_BYTES=12582912
```
Portal 用 secret 签发绑定 user/session/package/workspace/tool allowlist
的短期 tokenMindSpace Service 用同一 secret 验证;Goose extension
只收到 scoped token,不会收到签名 secret。该 MCP 配置目前用于
`split-service` 联调。配置完整时 DOCX 目标写入和长图渲染也会经过
MindSpace Service;未配置时不会开放 `publish_page`,其它工具保留
本地兼容行为。
验证当前源码的本地 split-service 链路可运行:
```bash
npm run smoke:mindspace-split-service
```
该 smoke 会构建本地 `.runtime/mindspace-service`、启动临时独立
MindSpace RPC 服务,并通过 remote adapter 验证 health/contract、
workspace 写读、微信 HTML 交付与 fresh thumbnail;不会发布或上传
runtime artifact。它会读取本机配置的开发数据库并创建唯一的
`split-service-smoke-*` session/package 记录,退出时会清理对应
package/artifact,并把临时文件根删除。
启动后 Portal 日志应出现:`[Portal] Runtime profile: MEMIND_RUNTIME_PROFILE=local, MINDSPACE_SERVER_ADAPTER=local, ...`
**禁止:** 把 103 生产 `.env`、RDS 连接串、`MINDSPACE_REMOTE_AUTH_TOKEN` 生产值复制进本机 Git 仓库。
@@ -339,6 +339,51 @@ MindSpace Domain
- 现有 H5、公开页、页面发布、文件下载行为不变。
- 代码搜索可以证明新增业务不再直接依赖物理路径。
2026-07-27 增量进展:
- Chat Save 的错误资源引用修复、私有图片物化和 HTML 写回已收进
`chatSaveService.materializeWorkspaceHtml`
- local adapter 与 remote RPC 使用同一 contractPortal 不再在
`resolveChatSaveBundle` 中读取 `storageRoot` 或直接 `writeFile`
- 已增加源码门禁,防止这条已完成的写路径重新泄漏回 `server.mjs`
- 公开页交付、Finish public HTML 同步和其它 Portal 路径仍需按后续
P1 切片继续收口;本增量不代表 P1 整体完成。
- 新增 `conversationArtifactService`local 与 remote adapter/RPC
共用同一契约。
- Finish 的 public HTML、聊天导出的 DOCX、发布长图不再由 Portal
直接调用 conversation package registry;物理目录解析、backing file
校验、artifact/package 登记与 manifest 刷新由 MindSpace 负责。
- Portal 的 Finish 保留 SSE/锁/页面同步时序,仅向 artifact 服务传
user/session/relativePath/messageId 等逻辑信息。
- 新增 `publicFinishService`SSE `write_file/edit_file` 兼容落盘、
Finish HTML/私有资源物化、DOCX 同步、workspace asset sync、
artifact 登记和 canonical URL 生成均在 MindSpace 内完成。
- Quick Share 的私有资源内联、`public/shared/*.html` 落盘和 public
URL 生成也已收进 `chatSaveService.createSharedHtml`
- `workspacePublicationDeliveryService` 已成为 `/MindSpace`
`/u/:slug/public`、workspace 长图、recent HTML discovery 和
Agent Run deliverable validation 的统一读交付入口;返回 HTML 或
Base64 body 等逻辑交付对象,禁止把绝对路径返回 Portal。
- publication resolve 由 MindSpace 返回 `workspacePublicUrl`
Portal 不再拼接 canonical workspace URL。
- Finish 的 HTML 完整性检查、Page Data 自动绑定/验收,以及
`workspacePageDeliveryService.syncAndDeliver` 已迁入 local/remote
adapter`portal-session-routes` 不再接收 `publishDir`
`storageRoot``h5Root`
- 微信 Page Data 验收、HTML 链接交付和 fresh thumbnail 校验/修复
均改为调用 `publicFinishService.prepareWechatPageDataDelivery`
`prepareWechatHtmlDelivery``ensureWechatFreshPageThumbnails`
Portal bootstrap 只注入这些逻辑能力,不再向微信模块注入 MindSpace
pool、storage root、publishDir 或 H5 root。
- 资产下载、from-asset 读取和 Agent Job 输入资产交付改为
`bodyBase64` 契约;远程 adapter 不再暴露会泄漏物理路径的
`readAsset` / `readPublicAsset`
- Portal 仅保留鉴权、HTTP 响应头、HTML 展示层注入、Finish 锁、
修复触发与 analytics 转发,并等待流式写入完成后才标记页面 ready。
- P1 的 H5/Portal/微信 HTML/Page Data 主读写与交付链路已收拢;P4
仍需补齐真实 split-service 端到端 smoke,并继续收敛剩余
`workingDir` 兼容上下文。
### P2: Conversation Package 首先落地
目标: 用户每次对话生成的图片、文件、页面、公开链接都能在一个包里看到。
@@ -409,6 +454,39 @@ MindSpace Domain
- Agent 生成文件后,MindSpace package manifest 自动更新。
- Policy 泄漏时也只能访问当前 package 范围。
2026-07-27 第一批实现进展:
- 新增 `workspaceToolService`,以
`workspaceRef + sessionId + packageId + relativePath` 承载
`read_file``write_file``edit_file`、目录操作和
`publish_page`local/remote adapter 使用同一契约,返回值不暴露绝对路径。
- MindSpace Service 新增专用 `/mindspace/v1/mcp/:tool` 入口。
HMAC scoped token 同时绑定 user、session、package、workspace 和
tool allowlist;请求不能覆盖 token 中的作用域。
- Agent policy 在取得真实 session 后重新生成 scoped extension 配置;
签名 secret 只留在 Portal/MindSpace ServiceGoose sandbox 只接收
短期 token。
- sandbox MCP 在配置 scoped endpoint 时通过 MindSpace Service 完成
逻辑读写;`publish_page` 只在该配置完整时开放。
- `write_file` / `edit_file` 对公开 HTML 使用专用 public artifact
登记,对其它文件使用通用 workspace artifact 登记;每次写入均刷新
package manifest,不依赖 asset sync 是否因相同 checksum 跳过。
- 本批已完成核心工具的逻辑写入与 package 自动登记,但 P4 尚未整体
关闭:当前 scoped token 能防止跨 user/session/package 身份写入,
`read_file` 的内容范围仍是该用户的逻辑 workspace,并非仅限当前
package 已登记对象。要满足“token 泄漏也只能读取当前 package”
的完整验收,还需增加 package artifact read capability。
- 第二批已新增受 scoped token 保护的 `write_binary_file` 内部操作:
`generate_docx` 只在 sandbox 临时目录生成文件,随后把二进制交给
MindSpace Service 写入目标 workspace 并登记 packageremote 模式
不再把 DOCX 直接写到 Goose 的 workspace path。
- `generate_long_image` 已改为由 MindSpace Service 校验 backing HTML、
生成 canonical source URL、渲染 PNG、写入 workspace 并登记 artifact
sandbox 不再读取目标 HTML 的物理路径或写目标 PNG。
- 微信 HTML 交付与 fresh thumbnail 已改为 MindSpace Service 内部解析
backing file、生成 canonical URL 并返回逻辑 artifact;真实
split-service 端到端 smoke 仍需后续切片收口。
### P5: NAS/S3 Adapter
目标: 存储后端可替换。
@@ -132,6 +132,21 @@ Expected result:
- final read parity still reports no drift after write-path validation
- any service started by the verifier is stopped before exit
For validating the current Memind checkout before publishing a standalone
runtime, use the repo-local split-service smoke:
```bash
cd /Users/john/Project/Memind
npm run smoke:mindspace-split-service
```
It builds `.runtime/mindspace-service`, starts a temporary standalone RPC
service on loopback, and verifies remote health/contract, workspace
write/read, WeChat HTML delivery, and fresh thumbnail generation.
The smoke uses the configured local development database to create a unique
`split-service-smoke-*` session/package and removes those package/artifact
rows plus its temporary filesystem root before exit.
Current verified local user:
- `1c99b83b-0454-474f-a5d2-129d34506a32`
@@ -16,7 +16,8 @@
| 位置 | 行为 |
|------|------|
| `mindspace-public-finish-sync.mjs` | `edit_file` 读取 baseline + `old_str`/`new_str` patch,再 `writeFileSync` 覆盖 |
| `server.mjs` | Finish / SSE 流式事件调用 `materializePublicHtmlWritesFromSessionEvent` |
| `mindspace-public-finish-service.mjs` | 在 MindSpace 内调用流式物化与最终 Finish sync,并生成 canonical URL |
| `server/portal-session-routes.mjs` | Finish / SSE 流式事件只调用 `publicFinishService`,等待流式写入完成后再标记交付 ready |
### 守卫
@@ -25,6 +26,136 @@
---
## 1.1 Chat SaveHTML 修复与落盘权威必须在 MindSpace
### 必须保留
| 位置 | 行为 |
|------|------|
| `mindspace-chat-save-service.mjs` | 修复错误私有图片引用、物化私有图片并受限写回 `public/*.html`;预览缩略图生成与 sidecar 写入 |
| `mindspace-chat-save-service.mjs` | quick share 的私有资源内联、`public/shared/*.html` 写入和 public URL 生成 |
| `mindspace-server-adapter-contract.mjs` | `chatSaveService.materializeWorkspaceHtml``ensurePreviewThumbnail``renderPreviewThumbnailSvg``createSharedHtml``readWorkspaceHtml` 同时暴露给 local / remote adapter |
| `server.mjs` | 只调用 `mindSpaceChatSave.materializeWorkspaceHtml`,禁止重新读取 `storageRoot` 或直接 `writeFile` |
| `server/portal-mindspace-chat-save-routes.mjs` | 预览缩略图与分析/保存流程只调用 `getMindSpaceChatSave()`,禁止直接解析 `publishDir` 或调用 `ensureWorkspaceHtmlThumbnail` |
| `server/portal-mindspace-chat-share-routes.mjs` | quick share 只调用 `createSharedHtml`quick Plaza 的 workspace HTML 读取只调用 `readWorkspaceHtml`;禁止直接解析 publishDir、写文件、读取 storage root 或生成 public URL |
这条边界用于保证 `split-service` 下 Portal 不再成为 Chat Save
的物理文件写权威。修复与图片物化继续保持幂等和 best-effort
真正的工作区写失败仍须明确返回错误,禁止静默回退到 Portal 本地写入。
### 守卫
- 单测:`mindspace-chat-save-service.test.mjs`
- local / remote / RPC`mindspace-local-runtime-services.test.mjs`
`mindspace-remote-server-adapter.test.mjs`
`mindspace-service/mindspace-rpc-server.test.mjs`
- 源码门禁:`scripts/verify-mindspace-authority-boundary.mjs`
- 综合验证:`npm run verify:mindspace-publish-guards`
- runtime 双边一致性:构建 Portal 与 MindSpace service runtime 后执行
`npm run verify:mindspace-publish-guards:full`
---
## 1.2 Conversation Artifact:登记与 manifest 写入权威必须在 MindSpace
### 必须保留
| 位置 | 行为 |
|------|------|
| `mindspace-conversation-package-artifact-service.mjs` | 从逻辑 user/session/artifact ref 解析工作区文件,登记 public HTML、聊天 DOCX、发布长图和通用 workspace 写入,并刷新 package manifest |
| `mindspace-server-adapter-contract.mjs` | `conversationArtifactService` 的四类登记方法同时暴露给 local / remote adapter`conversationPackageRegistry` 的远程契约只保留读取方法 |
| `server/portal-session-routes.mjs` | Finish 只传 user/session/relativePath/messageId,禁止把 `publishDir` 传入 artifact 登记 |
| `server/portal-mindspace-chat-save-routes.mjs` | DOCX 生成后调用 `registerChatDocxArtifact`,禁止直接取得 conversation package registry |
| `server.mjs` | public HTML 与发布长图登记只调用 `mindSpaceConversationArtifacts`;禁止直接 `putObjectForSession``recordArtifact``writeManifestForSession` |
该边界保证 local 与 split-service 使用同一个产物登记入口。MindSpace
负责 backing file 校验、确定性 artifact ID、对象写入和 manifest
刷新;Portal 仍只负责鉴权、Finish 时序与生成结果转发。
Finish 中受第 1 节保护的 `edit_file` 落盘兼容逻辑已迁入
`publicFinishService`,但仍复用同一纯实现和回归用例;禁止在 Portal
增加本地写回 fallback,也不得破坏已验证的覆盖语义。
### 守卫
- 单测:`mindspace-conversation-package-artifact-service.test.mjs`
- local / remote / RPC`mindspace-local-runtime-services.test.mjs`
`mindspace-remote-server-adapter.test.mjs`
`mindspace-service/mindspace-rpc-server.test.mjs`
- Portal 路由:`server/portal-session-routes.test.mjs`
`server/portal-mindspace-chat-save-routes.test.mjs`
- 源码门禁:`scripts/verify-mindspace-authority-boundary.mjs`
- 综合验证:`npm run verify:mindspace-publish-guards`
---
## 1.3 Workspace 读交付:物理路径与 canonical URL 必须在 MindSpace
### 必须保留
| 位置 | 行为 |
|------|------|
| `mindspace-workspace-publication-delivery-service.mjs` | 解析 `/MindSpace` 请求、校验 backing file 与 delivery contract、读取 HTML/二进制、补私有资源、生成缩略图/长图、扫描最近 HTML,并验证 Agent Run deliverables |
| `mindspace-publications.mjs` | `resolvePublic` 返回 `workspacePublicUrl`canonical workspace URL 不由 Portal 拼接 |
| `mindspace-public-finish-service.mjs` | Finish 内完成 HTML 交付完整性检查、H5/微信 Page Data preparation、微信 HTML 链接交付与 fresh thumbnail 校验/修复;跨 RPC 返回的结果必须移除绝对路径与 HTML 原文 |
| `mindspace-local-runtime-services.mjs` | 构造 `workspacePublicationDeliveryService``workspacePageDeliveryService`local/remote 使用同一 adapter contract |
| `server/portal-workspace-publication-delivery.mjs` | 只把逻辑 request path 交给 MindSpace,并代理 HTML/Base64 body;禁止读取、校验或返回本地绝对路径 |
| `server/portal-publication-routes.mjs` | `/u/:slug/public` 调用 `readOwnerPublicAsset`publication redirect 使用 `workspacePublicUrl` |
| `server/portal-session-routes.mjs` | 先同步页面记录,再调用 `preparePageDataAfterFinish` 并消费逻辑结果;禁止解析 `publishDir``storageRoot``h5Root` |
| `server/portal-integration-services-bootstrap.mjs` + `wechat-mp.mjs` | 微信只接收 `prepareWechatPageDataDelivery``prepareWechatHtmlDelivery``ensureWechatFreshPageThumbnails` 逻辑能力;禁止注入或读取 MindSpace 的 pool/storage path / publishDir / backing file path |
| `server/portal-mindspace-asset-routes.mjs` | 下载和 from-asset 只消费 `readAssetContent` / `readPublicAssetContent` 的 Base64 body |
Portal 可以继续负责鉴权、viewer-specific HTML 注入、CSP/缓存响应头和
repair turn 的触发,但不能获得 backing file 路径,也不能在 remote
模式下回退读取 Portal 本机目录。
### 守卫
- 核心服务:`mindspace-workspace-publication-delivery-service.test.mjs`
- Portal edge`server/portal-workspace-publication-delivery.test.mjs`
`server/portal-publication-routes.test.mjs`
- local / remote / RPC`mindspace-local-runtime-services.test.mjs`
`mindspace-remote-server-adapter.test.mjs`
`mindspace-service/mindspace-rpc-server.test.mjs`
- 源码门禁:`scripts/verify-mindspace-authority-boundary.mjs`
- 综合验证:`npm run verify:mindspace-publish-guards`
- runtime 双边一致性:构建两侧 runtime 后执行
`npm run verify:mindspace-publish-guards:full`
---
## 1.4 Agent MCP:逻辑 workspace/package 与 scoped token 必须成套
### 必须保留
| 位置 | 行为 |
|------|------|
| `mindspace-workspace-tool-service.mjs` | 只接受逻辑 `workspaceRef`、当前 `sessionId/packageId` 和相对路径;阻止绝对路径、路径穿越、symlink escape 与浏览器持久存储 |
| `mindspace-mcp-scoped-token.mjs` | token 同时绑定 user、session、package、workspace 与工具 allowlist,并校验签名和有效期 |
| `mindspace-service/mindspace-rpc-server.mjs` | `/mindspace/v1/mcp/:tool` 从 token 注入作用域,拒绝请求体覆盖 user/session/package/workspace |
| `mindspace-sandbox-mcp.mjs` | scoped 模式下通用 workspace 工具和 `generate_long_image` 只经 MindSpace ServiceDOCX 仅在临时目录生成,再通过内部 `write_binary_file` 上传 |
| `capabilities.mjs` + `user-auth.mjs` | 取得真实 session 后才生成 scoped token;签名 secret 不得进入 Goose extension env |
| `mindspace-conversation-package-artifact-service.mjs` | 非公开 workspace 写入也必须直接登记 artifact 并刷新 manifest,不能把 asset sync 的结果当作唯一登记保证 |
| `mindspace-workspace-tool-service.mjs` | 二进制写入和长图目标写入必须落盘、登记 artifact、刷新 manifest,并且响应不得暴露绝对路径 |
`publish_page` 在 scoped endpoint/token/workspace/session/package 任一项
缺失时不得暴露。保留旧本地文件工具只是未配置 scoped split-service
时的兼容路径,不得让它成为 remote 模式的静默 fallback。
scoped 模式下 `generate_docx` / `generate_long_image` 也不得在 RPC
失败时回退写 Goose 本地目标路径。
### 守卫
- 单测:`mindspace-workspace-tool-service.test.mjs`
`mindspace-mcp-scoped-token.test.mjs`
`mindspace-sandbox-mcp.test.mjs``capabilities.test.mjs`
- RPC / policy`mindspace-service/mindspace-rpc-server.test.mjs`
`user-auth.test.mjs``tkmind-proxy.test.mjs`
- 源码门禁:`scripts/verify-mindspace-authority-boundary.mjs`
- 综合验证:`npm run verify:mindspace-publish-guards`
---
## 2. 聊天 Finish:禁止清空对话 / 禁止暴露内部前缀
### 症状
@@ -123,6 +254,7 @@ npm run verify:mindspace-publish-guards:full
- [ ] 是否仍 merge 本地与服务器消息(Finish / UpdateConversation)?
- [ ] 是否仍过滤/剥离用户消息中的 agent-only 前缀?
- [ ] `edit_file` 是否仍会 materialize 到 `MindSpace/<userId>/public/`
- [ ] Chat Save HTML 修复与落盘是否仍只经 `chatSaveService`
- [ ] 是否已跑 `npm run verify:mindspace-publish-guards`
- [ ] 是否更新了本页或相关单测?