fix: 落地 REVIEW-2026-06-10 P0–P2 — 数据不丢、本地 API 不被劫持、任务不再卡死#41
Conversation
Fine-grained review of the entire DevLog repo across five subsystems (core data layer, runtime/orchestration, API security, CLI/build/CI, frontend), produced by parallel specialist reviewers. Several findings were empirically reproduced against better-sqlite3 and the shipped dist/cli.js. Catalogs 9 Critical, 30 Important, and a suggestions set spanning data loss in SQLite migrations, a watchdog that re-executes agent turns without a retry cap, broken control-plane gate delivery, CSRF/path- traversal/arbitrary-binary execution on unauthenticated localhost API routes, a non-functional published binary shipping a destructive test-data script, UTC-vs-local cost/date bucketing, and unbounded caches/SSE/poll leaks. Closes with a phased technology roadmap. Baseline at time of review: tsc --noEmit clean, 224/224 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…via sqlite_master CR-1: DROP TABLE during the tasks rebuild fired sessions' ON DELETE SET NULL action, silently nulling every session->task link on legacy DBs. Rebuilds now follow the documented SQLite procedure: foreign_keys OFF, column-intersection copy, rename, foreign_key_check, foreign_keys ON. CR-2: the sessions/tasks CHECK-widening probes (UPDATE ... WHERE status='idle') could never throw on a legacy DB, so the constraint was never widened and the catch path would have cascade-deleted session logs and dropped columns. Detection now inspects sqlite_master SQL; the dead tasks probe block is removed and the canonical DDL is single-sourced from db-schema.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CR-9: the test-data generator ran rm -rf ~/.claude/projects at module load — unrecoverably deleting every real Claude Code session log — and shipped in the npm tarball. It now refuses to touch the real history dir without --force, supports DEVLOG_TEST_DATA_DIR for scratch output, only cleans the specific project dirs it generates, and the test harness scripts are excluded from the published package. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CR-8: tsup bundles the CLI to a single dist/cli.js, so the fixed '../../..' hops from import.meta.dirname resolved above the repo root — 'devlog serve' exited with 'requires running from the DevLog source repo' even when run from it, and setup-statusline/setup-tmux never found the shipped scripts/. Resolution now walks up to the @moose-lab/devlog package.json. The stale force-added dist/ artifacts leave version control and prepublishOnly rebuilds them on publish, so a new version can no longer ship weeks-old code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oxy guard CR-6: no API route validated Origin/Sec-Fetch-Site/Host, so every mutation (task execute, worktree create, gh pr create with the user's credentials) was reachable from any webpage via CSRF or DNS rebinding to 127.0.0.1. A Next.js proxy (Next 16's middleware convention) now rejects requests whose Host is not loopback, and mutating requests carrying cross-origin browser markers. Headerless CLI clients (curl, scripts) remain allowed. This closes the remote trigger for CR-5/CR-7 and IM-19/20/21. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…em and git use CR-5/IM-22: POST /api/worktrees joined the request-supplied name into a path with only a falsy check, so '../../../../tmp/pwn' materialized a working tree (with repo files) at an attacker-chosen location, and branch values starting with '-' were argument injection into git. Names and refs are now validated at the route (400) and again in createWorktree as defense in depth, the resolved path is asserted to stay inside .worktrees/, and '--' separates git options from paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Ls in local-CLI env
CR-7: a request body could set CLAUDE_BIN (or any agent's *_BIN) to
any existing path — POST {"CLAUDE_BIN":"/bin/sh"} to the
unauthenticated connection-test route spawned a shell. *_BIN overrides
are now only honored when the file is actually named like the agent's
binary, so custom install locations keep working while arbitrary
binaries don't.
IM-21: request-supplied ANTHROPIC_BASE_URL/OPENAI_BASE_URL values were
spread into the child CLI env without validateAgentConnectionBaseUrl,
redirecting the agent's provider traffic (prompt exfiltration, SSRF to
private hosts). *_BASE_URL env values now pass the same SSRF
validation as direct provider base URLs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IM-19: claude_session_id is parsed from agent stdout (attacker- influenced under the malicious-JSONL threat model) and flowed unvalidated into getJsonlPath + python3 argv + safeRead, letting a traversal id read arbitrary .txt/.jsonl files into the API response. The id is now validated at persist time (process-manager) and at use time (compileSession), with a path-containment assertion against the Claude projects dir. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…starts CR-3: writeMessage never bumped lastActivityAt, so any turn with >3min of stdout silence (slow model, long tool call) was SIGKILLed and its user message silently re-executed — with no restart limit, a deterministically slow turn was killed and re-run every 3 minutes forever, duplicating side effects and growing session_messages unboundedly. The watchdog also churned idle persistent sessions. The activity clock now starts at send time and stderr output counts as activity; idle (non-processing) sessions are exempt; and after MAX_WATCHDOG_RESTARTS (2) the turn is failed and its linked task released instead of being re-executed again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eliverable CR-4: for plain-stdin agents (codex, gemini, opencode, ...) the original send closes stdin, so when the SIGSTOPped process later hit a gate, resolveGate cleared the persisted gate, emitted gate_resolved + running, and then writeGateResponse silently dropped the answer on the writableEnded guard — the human's response was permanently lost while the UI showed success. resolveGate now peeks the gate first, replaces processes whose stdin was consumed with a fresh one, delivers through writeMessage (protocol-correct stdin.end for EOF-reading runners), and only clears the gate once delivery is possible. IM-9: when ensureProcess fails, resolveGate now returns an actionable error and leaves the gate persisted instead of reporting success on a failed session. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… tasks on orphan recovery
IM-5: the close handler deleted the session registry entry without
checking it still owned it, so after a kill->respawn the stale close
evicted the freshly respawned process — leaving a live agent invisible
to kill() and the watchdog. Close handlers now verify identity and
skip status/exit mutations when a newer process owns the session.
IM-6: proc.on('close', code) never wrote the exit code, so
sessions.exit_code stayed NULL and task routing read NULL !== 0 as
failure — every clean no-change exit was routed to 'blocked'. The exit
code is now persisted and NULL is treated explicitly as 'not recorded'.
IM-8: recoverOrphanedSessions failed sessions directly, stranding
linked tasks 'in_progress' against a dead session forever; it now goes
through markSessionFailedAndReleaseLinkedTask so they can be retried.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @moose-lab's task in 4m 12s —— View job Code Review
|
…exit routing Addresses two review follow-ups on #41: the second migrateControlPlaneColumns call in getDb() was redundant (idempotent but confusing), and the clean-exit/no-changes case in onSessionExit is intentionally left to the human — now stated in a comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @moose-lab's task in 2m 19s —— View job Code Review
|
|
Re: review findings — verified each claim before acting. Summary: [must-fix] "src/proxy.ts is never mounted" — incorrect, verified empirically.
Adding a [must-fix] "rebuildTable BEGIN/COMMIT — postSql runs after COMMIT" — misread. [should-fix] resolveGate clears gate before writeMessage returns. Acknowledged residual: the window is between the writability check and the write a few lines later, and a failure there now surfaces as a visible error + idle status (vs. the original bug: silent loss with a success UI). Making Fixed in 537f785: duplicate Nits: 🤖 Generated with Claude Code |

用户视角:这次更新你会感受到什么
🗄️ 你的数据不会再无声丢失
generate-test-data一执行就rm -rf ~/.claude/projects(不可恢复)。现在它默认拒绝触碰真实目录,需要显式--force,且只清理它自己生成的演示项目。📦 npm 安装版终于能用了
devlog serve不再报"需要源码仓库"直接退出——之前发布的二进制里这个命令是完全坏的。devlog setup-statusline/setup-tmux能找到随包发布的脚本了。dist/。🔒 浏览网页时,你的本地 DevLog 不再是攻击面
DevLog 跑在 localhost 且无鉴权,以前任何你访问的网页都能悄悄调用它的 API:替你创建并执行任务、用你的 gh 凭证推送 PR、在任意目录写出 worktree、甚至拉起
/bin/sh。现在:claude/codex等就照常生效。⚙️ 长任务、人工确认和任务看板更可靠
blocked;DevLog 重启后,卡在"进行中"的孤儿任务会被释放为可重试,而不是永远占着状态。范围与验证
quality:build(Next + tsup)本地通过;中间件行为已起真实服务用 curl 验证。🤖 Generated with Claude Code