diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 8c73c631..1325929f 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,6 +16,14 @@ --- +## 2026-06-13 `/editor/agent` AI Web 工程编辑器采用静态沙箱预览 MVP + +- 背景:`/editor/agent` 需要承载浏览器内类似 IDE 的 AI Web 工程编辑和实时预览能力,但 AI 生成工程的构建和运行不能进入 Genarrative 主站 JS 上下文、当前仓库源码目录或 api-server 进程。 +- 决策:第一版采用“平台编辑器壳 `/editor/agent` + api-server 控制面 + 独立 `web-project-runner` worker + 独立 preview origin”的四层结构。MVP 只支持固定 React / Vite / TypeScript 静态模板、虚拟文件系统、结构化 AI patch、平台固定构建命令、独立 runner 静态构建和独立域 iframe 预览;明确不做 HMR、终端 shell、后端服务、任意端口代理、任意 npm 安装、AI 自定义 shell script 或主站同源预览。 +- 影响范围:`/editor/agent` 前端入口、api-server Web project 控制面、Web project runtime job、runner 部署、preview gateway、artifact store、安全验收和后续作品化发布链路。 +- 验证方式:Phase 0 必须先完成技术方案、威胁模型和验收清单;Phase 1 只能在路径校验、runner 资源限制、网络隔离、preview token、iframe/CSP、失败保留上一版预览和刷新恢复验收口径明确后进入编码。 +- 关联文档:`docs/technical/【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md`、`docs/technical/【安全模型】AIWeb工程Runner与预览隔离威胁模型-2026-06-13.md`、`docs/technical/【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md`。 + ## 2026-06-12 外部生成 worker 扩展到跳一跳、拼消消和敲木鱼 - 背景:外部图片生成已从 HTTP 长请求迁到 `external_generation_job` 队列;跳一跳、拼消消和敲木鱼继续扩展时需要统一 job 粒度、前端等待展示和本地 / 生产验证口径。 diff --git a/docs/README.md b/docs/README.md index 7aa90c09..a2501862 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,6 +21,8 @@ 微信小程序虚拟支付接入、`wechat_mp_virtual` 渠道、`wx.requestVirtualPayment` 承接页和后端签名配置见 [【技术方案】微信虚拟支付接入-2026-05-26.md](./%E3%80%90%E6%8A%80%E6%9C%AF%E6%96%B9%E6%A1%88%E3%80%91%E5%BE%AE%E4%BF%A1%E8%99%9A%E6%8B%9F%E6%94%AF%E4%BB%98%E6%8E%A5%E5%85%A5-2026-05-26.md)。 +`/editor/agent` 浏览器内 AI Web 工程编辑器的静态 SPA 沙箱预览 MVP,采用“平台编辑器壳 + api-server 控制面 + 独立 runner worker + 独立预览域”四层结构;技术方案、威胁模型和验收清单见 [【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md](./technical/【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md)、[【安全模型】AIWeb工程Runner与预览隔离威胁模型-2026-06-13.md](./technical/【安全模型】AIWeb工程Runner与预览隔离威胁模型-2026-06-13.md) 和 [【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md](./technical/【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md)。 + 本地通过 SSH alias 管理多台服务器、查看硬件 / systemd / HTTP 健康状态并执行受控服务启停的 egui 桌面工具见 [【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md](./technical/【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md)。 生产部署切换到 systemd + Nginx + SpacetimeDB 自托管的总方案见 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),该文档也是当前生产 Jenkinsfile 的唯一入口。SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md);private 表迁移 JSON 导入导出、HTTP 413 分片导入和旧数据库迁移流水线经验见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md)。 diff --git a/docs/technical/【安全模型】AIWeb工程Runner与预览隔离威胁模型-2026-06-13.md b/docs/technical/【安全模型】AIWeb工程Runner与预览隔离威胁模型-2026-06-13.md new file mode 100644 index 00000000..90311ef0 --- /dev/null +++ b/docs/technical/【安全模型】AIWeb工程Runner与预览隔离威胁模型-2026-06-13.md @@ -0,0 +1,241 @@ +# AI Web 工程 Runner 与预览隔离威胁模型 + +更新时间:`2026-06-13` + +## 范围 + +本文约束 `/editor/agent` 浏览器内 AI Web 工程编辑器的 MVP:固定 React / Vite / TypeScript 静态模板、虚拟文件系统、独立 runner 构建、独立 origin iframe 预览。 + +不覆盖 HMR、终端 shell、后端服务、任意依赖安装、任意端口代理和正式作品发布。这些能力进入后续阶段前必须单独补威胁模型。 + +## 信任边界 + +```text +用户浏览器主站 origin + | /editor/agent 编辑器壳 +api-server 控制面 + | 最小任务能力 +web-project-runner 执行面 + | immutable artifact +preview gateway / 独立 preview origin +``` + +关键原则: + +- 主站不执行 AI 工程代码。 +- api-server 不执行 AI 工程代码。 +- runner 无平台密钥、无宿主源码挂载、无 Docker socket。 +- 预览域和主站不同 origin。 +- 预览页拿不到平台 cookie、access token、SpacetimeDB、OSS 写权限或 LLM provider 密钥。 + +## 资产与威胁 + +| 资产 | 主要威胁 | MVP 缓解 | +| --- | --- | --- | +| 主站 access token / cookie | 预览代码同源读取、XSS 窃取 | 独立 preview origin;iframe 不带主站 cookie;预览页不能访问主站 storage | +| 用户 Web 工程源码 | 跨租户读取、snapshot 枚举 | project / owner 校验;snapshotId 不可枚举;preview token 绑定 owner / project / snapshot | +| preview artifact | 路径穿越、MIME 错误、旧 token 访问 | preview gateway 校验 token;禁止 `..`;按白名单 MIME 服务;短期 token 可撤销 | +| runner 临时工作区 | 逃逸到宿主源码、读取密钥 | 独立临时目录或容器;非 root;无宿主源码挂载;任务结束销毁 | +| 依赖缓存 | 缓存污染、恶意 postinstall | MVP 固定依赖;禁用 scripts;缓存 key 包含模板、Node 版本和 lock digest | +| api-server / SpacetimeDB | runner 横向访问内部服务 | runner 默认无内网访问;阻断 api-server 管理端口、SpacetimeDB 和生产数据库 | +| OSS / artifact store | 越权读写、签名 URL 泄露 | runner 只拿短期只读资产签名或受控写 artifact 能力;日志脱敏 | +| 构建日志 | 泄露环境变量、宿主路径、签名 URL | 日志限长、脱敏、错误摘要化;不回显平台密钥 | +| 用户浏览器 | 弹窗逃逸、下载、剪贴板、摄像头、Service Worker 常驻 | iframe sandbox;CSP;禁用 Service Worker;不授权敏感能力 | + +## Runner 限制 + +runner 必须 fail-closed。每个 job 至少限制: + +- CPU。 +- 内存。 +- 磁盘。 +- 进程数。 +- 打开文件数。 +- 任务时长。 +- 日志大小。 +- artifact 大小。 +- 单文件大小。 + +执行环境要求: + +- 非 root 用户。 +- 只读基础镜像。 +- 无 Docker socket。 +- 无宿主源码目录挂载。 +- 无平台 `.env`。 +- 无平台 token。 +- 临时工作区任务结束销毁。 + +构建超时或资源超限时直接 kill,job 进入 `failed` 或 `expired`,保留可展示错误摘要,不推进 active preview。 + +## 网络限制 + +MVP 构建期默认无外网。若模板构建确实需要网络,只允许: + +- 平台受控 npm registry mirror。 +- 平台资产只读签名域。 + +必须阻断: + +- RFC1918 内网网段。 +- 云 metadata 地址。 +- api-server 管理端口。 +- SpacetimeDB。 +- 生产数据库。 +- Docker daemon。 +- 任意用户配置 registry。 + +需要覆盖 DNS rebinding 和 HTTP redirect 到内网的情况;不能只按初始 URL 字符串判断。 + +## 预览 token + +preview token 必须满足: + +- 绑定 `ownerUserId`。 +- 绑定 `projectId`。 +- 绑定 `snapshotId`。 +- 绑定 `artifactId`。 +- 短期有效。 +- 可撤销。 +- 不可枚举。 +- 不能跨租户复用。 + +访问无权限 artifact 应返回 `403` 或不可区分的 `404`,不得泄露其它项目是否存在。 + +## Preview Gateway + +gateway 只读服务 immutable artifact。 + +必须校验: + +- token 有效且未撤销。 +- artifact 属于 token 绑定 snapshot。 +- 请求路径规范化后仍在 artifact 根目录。 +- `index.html` fallback 只在 SPA 路由范围内生效。 +- MIME 类型来自白名单映射,不信任上传文件名。 + +必须测试: + +- path traversal。 +- 错误 MIME。 +- HTML 注入。 +- 缓存串租户。 +- 旧 token 访问。 +- artifact 删除后访问。 +- token 过期后访问。 + +## 浏览器隔离 + +iframe sandbox MVP 建议最小化: + +```text +sandbox="allow-scripts" +``` + +如果后续需要表单或同源能力,必须单独评审。MVP 不允许: + +- `allow-same-origin` +- `allow-top-navigation` +- `allow-downloads` +- `allow-popups` +- `allow-modals` +- 摄像头。 +- 麦克风。 +- 剪贴板。 +- 地理位置。 + +CSP 默认收紧: + +```text +default-src 'none'; +script-src 'self'; +style-src 'self' 'unsafe-inline'; +img-src 'self' data: blob: https://assets.genarrative.world; +font-src 'self' data:; +connect-src 'none'; +frame-ancestors https://www.genarrative.world; +worker-src 'none'; +``` + +MVP 禁用 Service Worker 和持久缓存,避免恶意预览代码在用户浏览器里长期驻留。 + +## Patch 校验 + +AI 只能提交结构化 patch: + +- create file +- update file +- delete file +- rename file +- package manifest request + +api-server 必须拒绝: + +- 绝对路径。 +- `..`。 +- 符号链接。 +- `.env`、`.npmrc`、`.git`、`.ssh` 等隐藏敏感文件。 +- 超深目录。 +- 超大文件。 +- 超大总 snapshot。 +- 二进制膨胀。 +- 大 Data URL。 +- 可执行权限。 + +前端编辑器可以显示校验错误,但正式准入以 api-server 为准。 + +## 依赖供应链 + +MVP 固定模板依赖,不开放任意 npm。即便用户或 AI 修改 `package.json`,也必须拒绝或忽略: + +- `preinstall`。 +- `install`。 +- `postinstall`。 +- `prepare`。 +- `git:` 依赖。 +- `file:` 依赖。 +- `http:` / `https:` tarball 依赖。 +- 私有 registry。 +- lockfile 篡改。 +- native build。 +- 二进制下载型包。 + +后续开放白名单依赖时,必须加入依赖 resolver、registry mirror、封禁列表、许可证 / 安全扫描和缓存污染防护。 + +## 审计与熔断 + +必须记录: + +- `ownerUserId` +- `projectId` +- `snapshotId` +- `jobId` +- `artifactId` +- `templateKey` +- 依赖 lock digest +- 构建耗时 +- 资源用量 +- 失败原因摘要 +- runner id +- preview token 签发和撤销事件 + +必须提供运维开关: + +- kill job。 +- 禁用项目。 +- 禁用 owner。 +- 禁用模板。 +- 禁用依赖。 +- 清理 artifact。 +- 暂停 runner。 +- 全局关闭 `/editor/agent` preview build。 + +## 发布门禁 + +临时 preview artifact 不能直接升为线上作品。作品化发布必须: + +- 重新构建。 +- 重新校验 snapshot。 +- 重新签发发布 artifact。 +- 使用 immutable artifact。 +- 走平台作品身份、公开详情、统计和权限链路。 diff --git a/docs/technical/【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md b/docs/technical/【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md new file mode 100644 index 00000000..236780ed --- /dev/null +++ b/docs/technical/【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md @@ -0,0 +1,266 @@ +# 浏览器内 AI Web 工程沙箱预览方案 + +更新时间:`2026-06-13` + +## 背景 + +`/editor/agent` 需要承载一个类似 IDE 的 AI Web 工程编辑器:用户在浏览器里看到文件树、代码编辑、AI 指令输入、构建日志和实时预览,AI 生成的是一个完整 Web 工程。这个工程的构建、依赖安装和运行必须与 Genarrative 主站、当前仓库源码目录、平台密钥和正式业务数据隔离。 + +第一版不追求“浏览器里完整云 IDE + 任意 npm + HMR + 后端服务”。目标是先跑通一个安全、可恢复、可验收的静态 SPA 预览闭环。 + +## 结论 + +MVP 采用四层结构: + +```text +平台编辑器壳 /editor/agent + -> api-server 控制面 + -> 独立 web-project-runner worker + -> 独立 preview origin / preview gateway +``` + +第一版只支持固定 React / Vite / TypeScript 静态模板、虚拟文件系统、结构化 AI patch、平台固定构建命令、独立 runner 静态构建和独立域 iframe 预览。 + +MVP 明确不做: + +- 终端 shell。 +- 任意后端服务。 +- 任意端口代理。 +- HMR / 长驻 dev server。 +- 任意 npm 依赖安装。 +- AI 自定义 build shell script。 +- 与主站同源的预览 iframe。 +- 将 AI 生成工程写入当前 Genarrative 仓库源码目录。 + +## 分层职责 + +### 平台编辑器壳 + +入口路由固定为 `/editor/agent`。这一层只负责前端体验: + +- 文件树、代码编辑器、AI 指令输入。 +- 当前项目版本、diff、构建日志和预览 iframe。 +- 保存用户编辑和 AI patch。 +- 订阅构建 job SSE 状态。 +- 在构建成功后切换 iframe 的 `previewUrl`。 +- 构建失败时保留上一版可用预览。 + +平台编辑器壳不得直接执行用户工程代码,不持有 runner 临时工作区路径,不把平台 access token、用户 cookie、SpacetimeDB 连接信息或 OSS 写权限暴露给预览页。 + +如果后续接入现有创作链路,入口仍应走 `play_flow` 主干和 `shared-contracts` DTO,不在前端壳或 `app.rs` 中新建平行业务流程。 + +### api-server 控制面 + +控制面只管理权限、快照、任务和状态,不执行 AI 工程代码: + +- 鉴权和项目归属校验。 +- 校验 AI patch 和用户编辑。 +- 保存虚拟文件系统 snapshot。 +- 创建 preview build job。 +- 给 runner 发放最小任务能力。 +- 输出构建日志和状态 SSE。 +- 签发短期 preview URL。 +- 将业务状态通过受控 procedure 或 BFF 更新到正式数据层。 + +api-server 不把 SpacetimeDB、OSS 写权限、LLM provider、支付、用户 cookie 或平台内部 token 传给 runner。runner 需要读取资产时,只拿只读短期签名 URL 或受控 fixture。 + +### web-project-runner worker + +新增独立进程角色,例如 `web-project-runner`。它可以复用外部生成 worker 的“任务表 + lease + controller + worker”模式,但不应把 Web 工程执行塞进 `external_generation_job`,避免语义污染。更合适的落点是新增 `web_project_runtime_job` 或抽象出通用 job 模块。 + +runner 只做执行面: + +- 拉取指定 snapshot。 +- 展开到独立临时工作区或容器。 +- 使用平台白名单模板依赖。 +- 执行平台固定构建命令。 +- 采集构建日志、资源用量和结果。 +- 上传静态 artifact。 + +runner 不能反向写平台业务表。业务状态回写必须经过 api-server / SpacetimeDB 的受控入口。 + +### preview gateway 与独立预览域 + +预览页使用独立 origin,例如: + +```text +https://preview-.sandbox.genarrative.world +``` + +或者统一域名加不可枚举 token path: + +```text +https://sandbox.genarrative.world/p// +``` + +预览域不得与主站同源,不带平台 cookie,不共享主站 `localStorage` / `sessionStorage`。主站只通过 sandbox iframe 嵌入预览。 + +MVP 只服务静态构建产物。HMR、WebSocket 代理和长驻 dev server 后置到单独阶段评审。 + +## MVP 流程 + +```text +用户在 /editor/agent 输入需求或编辑文件 + -> AI 返回结构化 patch + -> api-server 校验 patch + -> 生成 workspace snapshot + -> 前端 debounce 后创建 preview build job + -> runner claim job 并构建静态 dist + -> artifact 上传 artifact store + -> preview gateway 只读服务 artifact + -> 前端 SSE 收到 succeeded 后 iframe reload +``` + +失败链路: + +```text +构建失败 / 超时 / runner 崩溃 + -> job failed / expired + -> 前端展示错误日志摘要 + -> active preview 保持上一版 succeeded artifact +``` + +## 接口草案 + +接口归属分成项目控制面和运行时控制面。 + +```text +POST /api/creation/web-project/projects +GET /api/creation/web-project/projects/{projectId}/snapshot +PATCH /api/creation/web-project/projects/{projectId}/files +POST /api/runtime/web-project/projects/{projectId}/preview-builds +GET /api/runtime/web-project/preview-builds/{jobId} +GET /api/runtime/web-project/preview-builds/{jobId}/events +``` + +`/events` 复用现有 `src/services/sseStream.ts` 的 SSE 传输层口径。事件类型建议包括: + +- `queued` +- `running` +- `log` +- `succeeded` +- `failed` +- `cancelled` +- `expired` +- `stale` + +前端刷新恢复时,先读取项目 active snapshot 和 active preview,再按未终态 job 继续订阅 SSE。 + +## 虚拟文件系统 + +平台内的“文件系统”是虚拟工作区,不是真实目录长期挂载。 + +snapshot manifest 建议包含: + +- `projectId` +- `snapshotId` +- `parentSnapshotId` +- `ownerUserId` +- `templateKey` +- `files[]` +- `createdAt` +- `createdBy` +- `patchSummary` + +文件项建议包含: + +- `path` +- `contentDigest` +- `sizeBytes` +- `mediaType` +- `encoding` +- `executable=false` + +路径必须 fail-closed: + +- 只允许相对路径。 +- 拒绝绝对路径。 +- 拒绝 `..`。 +- 拒绝符号链接。 +- 拒绝超深目录。 +- 拒绝超大文件。 +- 拒绝隐藏敏感文件名,例如 `.env`、`.npmrc`、`.ssh`、`.git`。 +- MVP 只支持文本源码和少量静态资源。 + +文件内容小文本可以按 digest 存对象;大一点的 snapshot 可打包成 tar / zip artifact 放受控 artifact store。图片、音频等大资产继续走现有资产链路,不让 AI 工程随意内嵌大 Data URL。 + +## 依赖和构建 + +MVP 只支持平台预置模板依赖: + +- React +- Vite +- TypeScript +- 平台已审核的 UI / 工具依赖子集 + +`package.json` 在 MVP 中不是事实源。AI 可以提出 manifest patch,但 api-server 会重写或忽略危险字段。构建命令由平台固定,例如: + +```text +npm ci --ignore-scripts +npm exec vite build +``` + +第一版应禁止: + +- `preinstall` / `install` / `postinstall` +- `git:` / `file:` / `http:` 依赖。 +- 私有 registry。 +- native build。 +- 二进制下载型包。 +- AI 自定义 shell script。 + +后续若要开放依赖,必须进入“受控依赖白名单”和“隔离依赖解析服务”阶段,不混入 MVP。 + +## 预览状态机 + +```text +draft snapshot + -> build queued + -> build running + -> build succeeded + -> build failed + -> build cancelled + -> build expired + -> build stale +``` + +状态规则: + +- 新 snapshot 触发新 build 时,旧 running build 应取消或标记 stale。 +- 只有当前 active snapshot 的 `succeeded` job 可以成为 active preview。 +- failed / cancelled / expired / stale 不能覆盖 active preview。 +- 回滚是切回历史 snapshot 并复用对应 immutable artifact,或重新构建该 snapshot。 +- 不允许复用 runner 临时目录作为回滚来源。 + +## 后续阶段 + +### Phase 0:文档和威胁模型 + +补齐技术方案、威胁模型和验收清单,明确 MVP 不可做能力、安全门禁、状态机和 QA 口径。 + +### Phase 1:静态模板预览纵切 + +完成 `/editor/agent` 编辑器壳、固定模板、虚拟文件系统、AI patch 保存、runner 静态构建、artifact 服务和 iframe reload。 + +### Phase 2:持久任务和恢复 + +引入 `web_project_runtime_job`,按 lease / controller / worker 模式实现任务队列、取消、stale、刷新恢复和日志 SSE。 + +### Phase 3:受控依赖安装 + +支持白名单依赖、lockfile 固化、依赖层缓存、安装失败可读错误、依赖封禁和审计。 + +### Phase 4:实时体验增强 + +在安全评审后再做长驻 dev server、HMR、WebSocket 代理、端口租约和空闲回收。 + +### Phase 5:作品化发布 + +将通过安全门禁的 Web project 接入平台作品类型。发布必须重新构建 immutable artifact,不直接提升临时预览 artifact。 + +## 与现有系统的关系 + +- 前端 SSE 读取复用 `src/services/sseStream.ts`。 +- 后端 job 模式参考外部生成 worker 的 lease / controller 经验,但使用新的 Web 工程 runtime 语义。 +- `/editor/agent` 是第一版用户入口,不新增重复 IDE 页面。 +- 业务真相仍通过 server-rs + SpacetimeDB 受控写入,前端和 runner 都不能绕过控制面。 diff --git a/docs/technical/【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md b/docs/technical/【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md new file mode 100644 index 00000000..1d97a0ff --- /dev/null +++ b/docs/technical/【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md @@ -0,0 +1,205 @@ +# AI Web 工程静态预览 MVP 验收清单 + +更新时间:`2026-06-13` + +## 范围 + +本清单用于验收 `/editor/agent` 浏览器内 AI Web 工程编辑器 MVP。 + +MVP 必须只支持: + +- 固定 React / Vite / TypeScript 静态模板。 +- 虚拟文件系统。 +- 结构化 AI patch。 +- 独立 runner 静态构建。 +- 独立 preview origin iframe 预览。 +- 失败保留上一版可用预览。 + +MVP 不支持: + +- 终端 shell。 +- 后端服务。 +- HMR。 +- 任意端口代理。 +- 任意 npm 安装。 +- AI 自定义 package scripts。 +- Service Worker。 +- 主站同源预览。 + +## Happy Path + +- [ ] 用户打开 `/editor/agent` 能看到文件树、编辑器、AI 输入区、构建日志区和预览 iframe。 +- [ ] 创建新 Web project 后得到固定模板文件树。 +- [ ] AI patch 新增或修改一个 React 组件后,api-server 生成新 snapshot。 +- [ ] 前端 debounce 后创建 preview build job。 +- [ ] runner 构建成功并产出 immutable artifact。 +- [ ] SSE 返回 `queued -> running -> succeeded`。 +- [ ] iframe 切换到新 preview URL。 +- [ ] 刷新 `/editor/agent` 后能恢复当前 project、active snapshot、active preview 和未完成 job 状态。 + +## Patch 与路径校验 + +- [ ] 相对路径普通源码修改通过。 +- [ ] 绝对路径被拒绝。 +- [ ] `..` 路径被拒绝。 +- [ ] 符号链接被拒绝。 +- [ ] `.env` 被拒绝。 +- [ ] `.npmrc` 被拒绝。 +- [ ] `.git/` 被拒绝。 +- [ ] `.ssh/` 被拒绝。 +- [ ] 超深目录被拒绝。 +- [ ] 超大单文件被拒绝。 +- [ ] 超大 snapshot 被拒绝。 +- [ ] 大 Data URL 被拒绝或转资产流程。 +- [ ] 二进制膨胀被拒绝。 +- [ ] rename 后目标路径仍需重新校验。 + +## 构建与状态机 + +- [ ] job 状态覆盖 `queued/running/succeeded/failed/cancelled/expired/stale`。 +- [ ] 新 snapshot 创建后,旧 running job 被取消或标记 stale。 +- [ ] 只有当前 active snapshot 的 `succeeded` job 能推进 active preview。 +- [ ] failed job 不覆盖上一版 active preview。 +- [ ] cancelled job 不覆盖上一版 active preview。 +- [ ] expired job 不覆盖上一版 active preview。 +- [ ] stale job 不覆盖上一版 active preview。 +- [ ] 构建失败时展示可读错误摘要和日志片段。 +- [ ] 构建超时后 runner 被 kill,job 进入 failed 或 expired。 +- [ ] runner 崩溃后 job 能恢复为可重领或 expired。 +- [ ] 页面刷新后能通过 projectId / jobId 恢复日志和状态。 +- [ ] snapshot 回滚会复用对应 immutable artifact 或重新构建,不复用临时目录。 + +## Runner 隔离 + +- [ ] runner 进程无平台密钥环境变量。 +- [ ] runner 无宿主源码目录挂载。 +- [ ] runner 无 Docker socket。 +- [ ] runner 使用非 root 用户。 +- [ ] runner 工作区为任务级临时目录。 +- [ ] 任务结束后临时目录被销毁。 +- [ ] CPU 限制生效。 +- [ ] 内存限制生效。 +- [ ] 磁盘限制生效。 +- [ ] 进程数限制生效。 +- [ ] 打开文件数限制生效。 +- [ ] 日志大小限制生效。 +- [ ] artifact 大小限制生效。 +- [ ] 任务超时限制生效。 + +## 网络隔离 + +- [ ] 构建期默认不能访问公网。 +- [ ] 若允许 registry mirror,只能访问白名单域名。 +- [ ] 不能访问 RFC1918 内网地址。 +- [ ] 不能访问云 metadata 地址。 +- [ ] 不能访问 api-server 管理端口。 +- [ ] 不能访问 SpacetimeDB。 +- [ ] 不能访问生产数据库。 +- [ ] HTTP redirect 到内网时被阻断。 +- [ ] DNS rebinding 到内网时被阻断。 + +## 依赖供应链 + +- [ ] AI 修改 `package.json` 新增普通依赖时,MVP 拒绝或忽略。 +- [ ] `preinstall` 被拒绝或忽略。 +- [ ] `install` 被拒绝或忽略。 +- [ ] `postinstall` 被拒绝或忽略。 +- [ ] `prepare` 被拒绝或忽略。 +- [ ] `git:` 依赖被拒绝。 +- [ ] `file:` 依赖被拒绝。 +- [ ] `http:` / `https:` tarball 依赖被拒绝。 +- [ ] 私有 registry 被拒绝。 +- [ ] lockfile 篡改被拒绝或重写。 +- [ ] 构建命令由平台固定,不执行 AI 写入的 shell script。 + +## Preview Token 与 Gateway + +- [ ] preview token 绑定 owner。 +- [ ] preview token 绑定 project。 +- [ ] preview token 绑定 snapshot。 +- [ ] preview token 绑定 artifact。 +- [ ] preview token 短期有效。 +- [ ] preview token 可撤销。 +- [ ] preview token 不可枚举。 +- [ ] 跨租户访问返回 403 或 404。 +- [ ] path traversal 被拒绝。 +- [ ] 错误 MIME 不会按可执行脚本服务。 +- [ ] SPA fallback 只在 artifact 根内生效。 +- [ ] artifact 删除后旧 URL 不能继续读取。 +- [ ] token 过期后旧 URL 不能继续读取。 +- [ ] preview cache 不串租户。 + +## 浏览器隔离 + +- [ ] 预览 origin 与主站 origin 不同。 +- [ ] 预览 iframe 不带主站 cookie。 +- [ ] 预览代码不能读取主站 `localStorage`。 +- [ ] 预览代码不能读取主站 `sessionStorage`。 +- [ ] 预览代码不能调用主站认证 API。 +- [ ] iframe sandbox 不允许 top navigation。 +- [ ] iframe sandbox 不允许 downloads。 +- [ ] iframe sandbox 不允许 popups。 +- [ ] iframe sandbox 不允许 clipboard。 +- [ ] iframe sandbox 不允许 camera / mic。 +- [ ] CSP 禁止未白名单 `connect-src`。 +- [ ] Service Worker 被禁用。 + +## 日志与错误 + +- [ ] 构建日志按 jobId 分段展示。 +- [ ] 日志限长。 +- [ ] 错误摘要可读。 +- [ ] 日志不包含平台 token。 +- [ ] 日志不包含 OSS 写签名。 +- [ ] 日志不包含完整宿主路径。 +- [ ] 日志不包含环境变量 dump。 +- [ ] SSE 断开后前端可重新拉取 job 状态。 + +## 取消、回滚与并发 + +- [ ] 用户连续编辑触发多个 snapshot 时,只保留最新 snapshot 的 active build 候选。 +- [ ] 用户手动取消当前 build 后,runner 停止或 job 被标记 cancelled。 +- [ ] 两个标签页同时编辑同一项目时,有明确版本冲突或 last-write 策略提示。 +- [ ] 回滚到历史 snapshot 后,active preview 对应历史 snapshot。 +- [ ] 历史 artifact 缺失时可重新构建。 +- [ ] 构建队列积压时 `/editor/agent` 显示确定状态,不假装实时完成。 + +## Artifact GC + +- [ ] 未引用的 failed artifact 会清理。 +- [ ] 过期 preview artifact 会清理。 +- [ ] active preview artifact 不会被误删。 +- [ ] 历史 snapshot 的可回滚 artifact 按保留策略保留或可重建。 +- [ ] GC 后 preview gateway 对已删除 artifact 返回确定错误。 + +## 最小自动化验证建议 + +后端 / runner: + +```bash +cargo test -p api-server web_project --manifest-path server-rs/Cargo.toml +cargo test -p spacetime-module web_project --manifest-path server-rs/Cargo.toml +``` + +前端: + +```bash +npm run test -- src/services/sseStream.test.ts +npm run test -- src/components/editor/agent +npm run typecheck +npm run check:encoding +git diff --check +``` + +浏览器 smoke: + +```text +打开 /editor/agent +创建模板项目 +提交一次 AI patch +等待静态构建成功 +确认 iframe 展示新预览 +提交一次故意破坏构建的 patch +确认错误出现且上一版预览仍保留 +刷新页面确认项目、日志和 active preview 可恢复 +```