Repository navigation
Conversation
Require a dedicated preview hostname and explicit studio frame origins. Keep platform routes off the preview host and reject studio backends. Cover authenticated browser isolation, module loading and SPA navigation.
fx-kurumi
left a comment
There was a problem hiding this comment.
审查 head 81c53b7:Request changes,工作台内嵌预览被平台自己的 CSP 拦截
隔离思路本身没问题:预览固定到独立主机名,不再读取 X-Forwarded-*;预览主机上的页面和 API 一律返回 404;已持久化的会话也会重新校验后端主机名。这些我都在本机确认过。问题出在这次没有改到的另一半:工作台页面本身的 CSP。
[P1] 预览移到独立主机名后,工作台页面的 frame-src 没有放行它,「前端预览」标签页里的 iframe 会被浏览器拦截
internal/httpapi/gin_helpers.go:127(不在本 PR diff 内):工作台页面的 CSP 是frame-src 'self' blob:。以前预览和工作台同源,所以'self'足够。本 PR 把预览强制挪到AGENTFLOW_PREVIEW_PUBLIC_URL这个不同源的主机,web/app/runs.js:738的<iframe data-preview-frame src=…>就会被工作台自己的 CSP 拦截。Chromium 报:Framing 'http://localhost:18187/' violates the following Content Security Policy directive: "frame-src 'self' blob:",iframe 内显示的是错误页。- 结果是按 PR 说明配好两个变量后,运行详情 →「前端预览」里看不到任何内容,只有「独立窗口」能用。这是这个功能的主要入口。
- 测试没有发现这个问题:
internal/httpapi/preview_isolation_browser_test.go:113的/viewer父页面是测试自己写的 handler,响应里没有平台的securityHeaders(),所以 iframe 能加载。真实工作台页面要经过securityHeaders()。
建议:工作台 CSP 的 frame-src 追加配置好的预览 origin。例如在 securityHeaders() 里,当 preview.LoadConfig() 得到合法的独立 origin 时,在 frame-src 'self' blob: 后面追加 scheme://host。只需改 / 和 /index.html 等工作台页面,/chatbot/ 与 vscode 页面不嵌入预览,可以不改。同时请把浏览器回归测试的父页面改成真实的工作台页面(或者至少套上 securityHeaders()),让这个问题能被测试覆盖。
我用实验分支验证过:只在 securityHeaders() 的 frame-src 后追加预览 origin,其他不变,内嵌预览就能正常显示和交互。同时 parent.document 读取仍然被拦截(跨源),fetch 工作台 API 也仍然被拦截,隔离效果不受影响。
本机真实服务验证
环境:本机构建的平台 handler(httpapi.New 的完整中间件链)+ 真实预览服务(ArchiveLocal 归档、会话、网关),内存 sqlite,访客账号,虚构 run「评审用预览 SOP」;不连模型,不连外部服务。预览包是自己写的计数器页面:点 +1,并探测 parent.document、带 cookie 的 fetch 工作台 API、同主机的 /api。
| 内嵌「前端预览」标签页 | 独立窗口 | parent.document | fetch 工作台 API | |
|---|---|---|---|---|
base bdf8723(同源) |
可显示,计数器可点 | 可用 | 可读(这正是 #93 的问题) | 被拦截(connect-src 'none') |
本 PR 81c53b7 |
被 frame-src 拦截,显示错误页 | 可用 | —(iframe 未加载) | 被拦截 |
| 本 PR + 上面的 frame-src 修改(实验) | 可显示,计数器可点 | 可用 | 被拦截(跨源) | 被拦截 |
| 本 PR:内嵌预览被拦截 | base:内嵌预览正常(但同源) | 本 PR + frame-src 修改 |
|---|---|---|
![]() |
![]() |
![]() |
本 PR 的独立窗口(隔离生效,studioApi/sameHostApi 都被拦截):
交互录屏(打开运行 → 前端预览 → 点 3 次 +1 → 独立窗口):本 PR · base 对照 · 加上 frame-src 修改后
其他验证
go vet ./internal/preview ./internal/httpapi通过;go test ./internal/preview通过;go test ./internal/httpapi -run Preview通过,包括TestPreviewIsolationBrowser(本机非 root,需要给 Chromium 加--no-sandbox)。- 预览主机上请求
/api/health返回 404,符合预期。 go test ./internal/engine有 2 个失败(TestPushConversationWorkspace…、TestPrepareRepositoryApplies…),原因是本机全局 git 身份干扰了测试中的提交作者,和本 PR 无关。- 非阻塞:
web/app/runs.js:689会对 http 预览地址用 HEAD 探测 https 版本,这个请求现在跨源,会被工作台的connect-src 'self'拦截,控制台多一条错误。它会回退到原地址,不影响功能。可以考虑只在工作台本身是 https 时才探测,或者直接信任服务端返回的地址。 - 限制:没有跑 MySQL 版 Playwright 套件、Firefox/WebKit,也没有真实 DNS/TLS 反代部署。
| var currentPreviewURL atomic.Value | ||
| var tokenRequests atomic.Int32 | ||
| server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| if r.Host == server.Listener.Addr().String() && r.URL.Path == "/viewer" { |
There was a problem hiding this comment.
这里的 /viewer 父页面由测试自己的 handler 返回,不带平台 securityHeaders() 的 CSP,所以测不出真实工作台 frame-src 'self' blob:(internal/httpapi/gin_helpers.go:127)会拦截跨源预览 iframe 的问题。建议让父页面走 handler(真实工作台页面),或者至少套上 securityHeaders()。
| ancestors = "'self'" | ||
| } else if !strings.Contains(ancestors, "'self'") { | ||
| ancestors = "'self' " + ancestors | ||
| ancestors := "'none'" |
There was a problem hiding this comment.
预览端的 frame-ancestors 已经放行工作台 origin,但嵌入方工作台页面的 frame-src 还是 'self' blob:,两边要同时放开。建议在 securityHeaders() 里,当 AGENTFLOW_PREVIEW_PUBLIC_URL 是合法的独立 origin 时,在 frame-src 后追加这个 origin(我在本机实验过,内嵌预览能恢复,parent.document 仍然被拦截)。
|
LoopForge PR Review — head Verdict:
@AlexXPZhu local non-browser evidence is clean; the decisive |



What changed
Fixes #93.
An archived preview could share the studio origin and use
parent.fetchwith the viewer's signed-in session. Previews now require a dedicated hostname and explicit studio frame origins.Scope
Compatibility
Deployment configuration is required to reopen online previews:
The second value accepts space-separated exact studio origins. Route both hostnames to the platform while preserving Host; deployment docs include proxy and local-development examples. Until configured, session creation returns an explicit error. Archiving and the authenticated ZIP download API remain available. Existing studio-origin preview links return 404.
Bundle external scripts/styles/fonts locally. Nested frames and workers/service workers are blocked. Configured non-studio backends still need their own CORS support. Preview producers share the dedicated preview origin; this change does not provide isolation between previews.
No SQL migration, schema change or runner protocol change. Classic and Portable are unchanged.
Validation
Passed on the submitted tree:
bash scripts/build-chatbot.sh: 299 tests and production build.go test -p=2 ./... -count=1 -timeout=180s: all packages passed.go vet ./...python3 scripts/check-public.pybash scripts/scan-secrets.shgit diff --cached --checkbefore committing.Go tests used Go 1.26.11 and Chromium with its required system libraries. The Chromium regression passes both with and without a configured backend and in embedded/standalone modes.
Negative control: overlaying the four original production files from
bdf8723makes both API and Chromium regressions fail: studio APIs are exposed on the preview host, studio backends are accepted, and four image requests reach the studio token endpoint.Not run: the full MySQL-backed public Playwright suite, Firefox/WebKit, or a live DNS/TLS proxy deployment. Local-edition checks are not applicable.
Safety
Prepared with Codex assistance.