feat(dashboard): add writable web terminal action - #642
Conversation
084fdd6 to
84cc01b
Compare
首次 Review(Claude)— ✅ 无阻塞白话:这个 PR 改了什么Dashboard 会话的终端入口原来是一个图标,语义随登录态变化:
一个图标承担两种含义,已登录用户「只读看一眼」和「进去操作」无法区分。本 PR 拆成两个明确入口:
覆盖三个面:会话卡片( 验证
安全性核查(本 PR 的核心前提)「只读入口常驻 + 钥匙按钮仅登录可见」不是提权面,已逐层核过:
发现(均非阻塞)
结论:改动干净、边界收敛(三面 + 嵌入壳 + 公共只读 gating 一致),无 correctness / 安全问题。已请 @codex 复审;合码待申晗确认。 |
|
To use Codex here, create a Codex account and connect to github. |
deepcoldy
left a comment
There was a problem hiding this comment.
Codex 复审 — ❌ 1 个阻塞问题
[P1] Riff 的“只读终端”实际仍是可写 AIO Sandbox
本 PR 在 TerminalControls 中把 terminalHref(row) 固定渲染成 sessions.openReadonlyTerminal,并另外增加钥匙按钮走 /write-link。但 src/dashboard/web/session-terminal.ts:14 仍然优先原样返回 row.riffAccessUrl。
riffAccessUrl 不是只读日志 URL:src/core/terminal-url.ts、worker-pool.ts 和 dashboard-ipc-server.ts 都明确把它定义为 Riff 的 writable/operate capability。实际结果:
- 已登录 Riff 会话的 🖥️ 和 🔑 两个按钮会打开同一个可操作沙箱;新加的“只读”按钮标签与权限不符,也没有真正完成入口分离。
- 匿名公开只读面板更危险:
/api/sessions的redactSessionForPublic()只移除gitBranch,仍会下发riffAccessUrl;session.updateSSE 也保留它。publicReadOnly默认开启,因此“无 token 直达 URL 服务端本就只读”这一安全前提对 Riff 不成立。
我用当前 head 84cc01be 直接复现:
sessionTerminalHref({ webPort: 3001, riffAccessUrl: '.../write-capability' })
→ .../write-capability
redactSessionsForPublic([sameRow])[0]
→ 仍含 riffAccessUrl
其中匿名暴露 riffAccessUrl 在基线已经存在,但本 PR 新增的“双入口”会在已登录 Riff 会话中把同一个写能力同时标成只读和可写;而且 PR 的核心只读安全不变量仍未成立,所以本次需要挡住。
建议修复
- 只读入口对 Riff 使用本地 worker Web Terminal(日志页),不要让
sessionTerminalHref()优先返回riffAccessUrl;Riff AIO 只通过鉴权后的/write-link暴露。 - 匿名
/api/sessions与 session SSE patch 脱掉riffAccessUrl,避免写 capability 进入公开只读 payload。 - 补三类回归:Riff 只读 href 指向本地日志、匿名 REST/SSE 不含
riffAccessUrl、Riff 两个按钮指向不同权限入口。
独立验证
pnpm build:通过(domain audit、tsc、dashboard bundle、dist audit 均通过)pnpm test:722 files passed / 11096 tests passed(1 file、5 tests skipped)- 额外聚焦 5 files:86 tests passed(sessions UI、terminal auth、public redact、monitor room、embedded shell)
git diff --check:通过- 与最新
origin/mastermerge-tree:无冲突;GitHub 显示 mergeable
其余已核对:普通 PTY/Tmux 的 tokenless terminal URL 服务端确实只读;/write-link 仍受 dashboard auth + daemon IPC HMAC 双门保护;Desktop/Mobile embedded shell 两个入口均隐藏。sessions.openTerminal 死字符串与 modal 固定只读属于非阻塞项,modal 行为仍待申晗确认产品取舍。
更正首审结论:认同 @codex 的 CHANGES_REQUESTED —— 我首审对 Riff 的安全清算是错的我独立复核了 codex 提出的 Riff blocker,三条子结论逐一在源码层证实,且这正是我首审漏掉的分支。首审说「只读入口常驻不是提权面」——该结论对 Riff 不成立,是我的错:我只追到 源码证据
新增 vs 基线(供定级)
认同的修复方向(codex 已提,我复核可行)
验证
结论:认同阻塞,首审「无阻塞」作废。合码待作者修复 + 申晗确认;modal 固定只读的产品取舍仍需申晗拍板。 —— 教训:审「常驻只读入口是否提权」不能只追到通用鉴权层,必须把每个后端(尤其 Riff 这种 URL 即写能力的旁路)在 href 构造处的短路都走一遍。 |
|
To use Codex here, create a Codex account and connect to github. |
复审发现 dashboard 侧 Riff 会话没真正拆成只读/可写:
- `sessionTerminalHref` 对 Riff 无条件短路返回 `riffAccessUrl`,而该 URL 是 AIO
Sandbox 的**可写** bearer capability(唯一子域名本身即写凭证,见
riff-backend.ts hashUrlForLog 注释)。结果标「只读终端」的入口打开的是可写沙箱,
且默认 publicReadOnly=true 下匿名访客也能拿到写能力。
按申晗决策「dashboard 与卡片对齐」修复(卡片侧本就是 Web终端=日志页 / 操作链接=AIO):
1. `session-terminal.ts`:去掉 riffAccessUrl 短路,只读入口一律走本地 worker
webPort 日志视图(与卡片 buildTerminalUrl(ds) 一致);写入口仍经鉴权
/write-link → riffAccessUrl(与卡片 buildTerminalUrl(ds,{write:true}) 一致)。
Riff 无本地 workerPort 时返回 null,不再回退到写沙箱。
2. `public-redact.ts`:redactSessionForPublic + session.update SSE patch 剥离
riffAccessUrl,匿名只读 payload 不再泄漏写沙箱 bearer URL(消除基线匿名提权面)。
影响面:仅 dashboard 前端 href 构造 + 匿名脱敏两处;本地 worker 终端/其它 CLI
读链接语义不变(它们本就走 webPort,不含 riffAccessUrl 短路)。
测试:
- 新增 test/dashboard-monitor-room.ts:Riff 只读 href 走 localhost:webPort、
不含 sandbox URL;无 webPort 返回 null。
- 新增 test/dashboard-public-redact.ts:匿名 REST 行 + spawned/update SSE body
均脱掉 riffAccessUrl 且不 mutate 原对象。
- pnpm build 绿;dashboard-public-redact/monitor-room/sessions-ui/terminal-write-auth/
terminal-url 共 103 测试全绿。
Co-Authored-By: Riff <riff@riff.cc>
已按「dashboard 与卡片对齐」修复 Riff 双入口问题(申晗拍板方向)申晗决策:dashboard 直接对齐飞书卡片语义——卡片的「Web终端=日志页」就是 dashboard 的只读、「操作链接=AIO」就是 dashboard 的可写。这正是核心层 已 push 1.
|
|
To use Codex here, create a Codex account and connect to github. |
deepcoldy
left a comment
There was a problem hiding this comment.
Codex 复审(head 864b74dd)— ✅ blocker 已收敛,批准
复核结论:修复与申晗“dashboard 跟卡片对齐”的决策一致,原先的 Riff 阻塞问题已完整关闭。
路径核对
- Riff 只读入口:
sessionTerminalHref()不再优先返回riffAccessUrl,统一按webPort/proxyPort生成本地 worker 日志终端 URL;无webPort时返回null,不会降级到可写沙箱。 - Riff 可写入口:🔑 仍经 dashboard 鉴权后的
/api/sessions/:id/write-link,daemon IPC HMAC 验证后返回riffAccessUrl;读写能力重新与卡片侧buildTerminalUrl(ds)/buildTerminalUrl(ds, { write: true })一一对应。 - 匿名 REST:
redactSessionForPublic()同时移除gitBranch与riffAccessUrl,且不修改已认证调用方持有的原对象。 - 匿名 SSE:
session.spawned复用同一 redactor;session.update.patch显式移除riffAccessUrl,无法从/events绕过 REST 脱敏。 - 其它 CLI / 壳层:PTY/Tmux/Herdr 等原本没有
riffAccessUrl短路,仍走既有webPort逻辑;Desktop/Mobile 两个 Web terminal 入口继续隐藏。
独立验证
pnpm build:✅- 聚焦 5 files:✅ 103 tests passed
- dashboard public redact
- monitor room / session terminal href
- sessions UI
- terminal write auth
- terminal URL
pnpm test:首轮 721 files / 11094 tests 通过,仅group-join-shared-routing.test.ts的无关beforeAll在并发全量运行中超时;该文件独立重跑 ✅ 5/5,通过,判定为环境/并发抖动而非本 PR 回归。git diff --check:✅- 与最新
origin/mastermerge-tree:✅ 无冲突;GitHubMERGEABLE
TerminalModal 固定只读也符合本话题中申晗已确认的“只读=日志、可操作=单独入口”口径。无剩余阻塞项;sessions.openTerminal 死字符串仍仅是 P3 清理项,不影响合入。
已合入 master ✅(申晗授权)
未发版(未打 tag)。live 生效需 |
Summary
Validation
test/dashboard-sessions-ui.test.ts: 22 passedtest/dashboard-i18n-c5.test.ts: 430 passedtest/terminal-write-auth.test.ts: 31 passedScope
This PR contains only the Dashboard terminal-control UI, i18n, style, and focused regression-test changes. No production deployment or remote configuration change is included.