Skip to content

Commit 9d5b6a6

Browse files
committed
fix(mcode-trajectory-studio): contain file reads, redact stored credential forms, gate the panel on a capability
Addresses the review on MiniMax-AI#56. Every finding was reproduced before the change and re-checked after it, and the new suites are written so that "refuse everything" cannot pass them. **Symlink root escapes** (`tasks.mjs`, `jsonl.mjs`). Every file read now goes through `containedRealPath` / `openContainedRead` in `fsutil.mjs` and has to canonicalize inside the data directory, with a non-symlink final component opened under `O_NOFOLLOW` so the containment decision and the read describe one inode. Three separate defects were reproduced: a symlinked task directory, a symlinked task root, and a symlinked session directory. The session level of the JSONL walk had no `isDirectory()` guard at all, and the `stat()` before `isSymbolicLink()` was dead code, because `stat` resolves the link it was asked to detect. **Redaction missed the encodings credentials take on disk.** The key/value rule required the key and its separator to be adjacent, so `{"api_key":"…"}` never matched, and the Authorization rule ran after the key/value rule, which consumed `Authorization` and left the token of `Authorization: Bearer …` in the clear. The rules are now ordered most-specific-first and shape-preserving, task descriptions and commands are redacted at the data source, a session title has only its credential substrings replaced, and every outbound payload is swept once more at the boundary so a field added later cannot escape. One defect the browser run caught and no unit test would have: key-name redaction matched `sessionId`, so `/api/sessions` returned `"[redacted]"` and every following request 404'd. **The panel's fences were CSRF defences, not authorization.** Any local process could send the fixed header and read every session's tool arguments. A per-process 256-bit capability is now minted on each start, carried in the URL fragment (so it is never sent to the server, logged, or leaked through `Referer`) and required, constant-time, on every API route. mcode spawns one MCP server per session, so two sessions get two panels with two capabilities and neither URL opens the other's panel. **`trajectory_studio` declared `readOnlyHint: true` while opening a listener.** The annotation is corrected, and the port file is gone rather than hardened: once the capability is per process, reusing a previous process's port is semantically wrong (the old token is gone and the new one was never sent to the old page), and removing the write removes the symlink hazard. The Plugin now creates no files. **The Node range was overstated.** Measured across nine releases: `node:sqlite` needs 22.13.0, and FTS5 is not monotonic in the Node version — absent in 22.13.0 through 22.18.x and throughout 23.x, present from 22.19.0 and 24.0.0. The floor is enforced with a readable message instead of a module-resolution crash, the verified range mirrors mcode's own `engines` (`>=22.19 <23 || >=24 <27`), FTS5 is probed rather than inferred, and search degrades with a warning where it is absent. Found while measuring: SQLite needs a writable *directory* even for a read-only open, because WAL needs its `-shm` file, now documented as a limitation. **Render layer, unchanged but now guarded.** The client already built every node with `createElement` and wrote only `textContent`; a mutation-checked scan now fails the suite if that ever stops being true, and the panel binds `127.0.0.1` as a non-overridable constant and refuses a non-loopback peer address. **New evidence.** `test/protocol.test.mjs` spawns the real MCP server over stdio the way mcode does and drives it as a client: version negotiation, the seven declared tools and their annotations, a call against a fixture projection, full detail redacted on the wire while the payload survives, an unknown tool and an unknown method, a notification left unanswered, `--doctor`, and the lifecycle — closing stdin has to end the process *with the panel running*, because a listener left behind leaks a port on every session. Every wait is bounded, so a server that stops answering fails the suite instead of hanging it. `tools/fixture.mjs` now owns the runtime-shaped projection that the domain tests, the protocol suite and the E2E harness share, and `tools/compat-matrix.mjs` asserts the compatibility table rather than printing it (fail === 0; skip count matching whether this runtime's SQLite has FTS5; every skip being the FTS5 search test). It is mutation-checked: an injected failure and an injected unrelated skip both make it exit 1. **Cross-platform.** This suite had never run off Linux. Its first run on Windows and macOS found three platform assumptions, all of them in the tests rather than the Plugin: a temporary path compared before canonicalization (`/var` on macOS, a short `RUNNER~1` path on Windows), a POSIX-only assertion that a resolved data directory comes back verbatim, and a temporary directory removed while SQLite still held the file open (`EBUSY` on Windows, harmless on POSIX — it cascaded through 24 tests). All three are fixed; both platforms now report 115 tests, 0 failures. No workflow is added to this repository: what runs on the project's runners is the maintainers' call. The job definitions are written out in the pull request instead. Tests: 115 in the Plugin suite, 472 repo-wide, 0 failures. Verified on Node 22.12.0 (refused with a reason), 22.13.0, 22.15.0, 22.19.0, 22.21.1, 22.23.2, 23.4.0, 23.11.0, 24.0.0, 24.16.0, 24.19.0 and 24.20.0, with the outcome matching the documented range on each. Version 0.1.1.
1 parent 8b7cd94 commit 9d5b6a6

32 files changed

Lines changed: 2829 additions & 330 deletions

‎plugins/weekbin/mcode-trajectory-studio/.claude-plugin/plugin.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "mcode-trajectory-studio",
3-
"version": "0.1.0",
4-
"description": "只读查看本机 MiniMax Code 会话轨迹:轮次、步骤、LLM 与工具的墙钟耗时、token 用量、工具入参与结果、上下文拆分、压缩与子代理任务,并附带一个本地交互式面板。数据取自本地运行时自身的 SQLite 投影;插件不写入会话存储、不联网、不收集遥测。需要宿主运行时提供 Node.js 22 及以上(使用内置 node:sqlite)。 / Read-only inspection of local MiniMax Code session trajectories: turns, steps, LLM and tool wall-clock time, token usage, tool arguments and results, context breakdown, compactions and sub-agent tasks, plus a local interactive panel. Data comes from the local runtime's own SQLite projection; the Plugin never writes to session storage, never touches the network and collects no telemetry. Requires Node.js 22 or newer from the host runtime (uses the built-in node:sqlite).",
3+
"version": "0.1.1",
4+
"description": "只读查看本机 MiniMax Code 会话轨迹:轮次、步骤、LLM 与工具的墙钟耗时、token 用量、工具入参与结果、上下文拆分、压缩与子代理任务,并附带一个本地交互式面板。数据取自本地运行时自身的 SQLite 投影;插件不写入任何文件、不联网、不收集遥测。面板仅监听 127.0.0.1,每个 API 路由都要求一份随进程新生成的访问凭据。需要 Node.js 22.13.0+(内置 node:sqlite 的下限),已验证范围与 mcode engines 一致:>=22.19 <23 或 >=24 <27;全文检索另需内置 SQLite 带 FTS5,22.13.0-22.18.x 与 23.x 缺失时搜索降级并告警。 / Read-only inspection of local MiniMax Code session trajectories: turns, steps, LLM and tool wall-clock time, token usage, tool arguments/results, context breakdown, compactions, sub-agent tasks, plus a local panel. Data comes from the local runtime SQLite projection; the Plugin writes no files, no network, no telemetry. The panel listens on 127.0.0.1 only and requires a per-process capability token on every API route. Requires Node.js 22.13.0+ (the built-in node:sqlite floor); the verified range matches mcode engines: >=22.19 <23 or >=24 <27. Full-text search also needs FTS5 in that bundled SQLite; absent in 22.13.0-22.18.x and 23.x, where search degrades with a warning.",
55
"author": {
66
"name": "weekbin",
77
"url": "https://github.com/weekbin"

‎plugins/weekbin/mcode-trajectory-studio/DESIGN.md‎

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -322,15 +322,59 @@ plugins/weekbin/mcode-trajectory-studio/
322322
7. **搜索** — 走 FTS5
323323
8. 长历史分页 + 只渲染可见行
324324

325-
### 6.5 安全基线(对齐先例)
326-
327-
- 绑定 `127.0.0.1`,仅本机
328-
- `Host` 头必须等于监听 authority;带 `Origin` 时校验同源
329-
- API 要求自定义头,强制跨站请求走被拒的 CORS preflight
330-
- CSP `default-src 'self'`;无远程字体/脚本/样式;无外网请求
331-
- `X-Content-Type-Options: nosniff`、`Cache-Control: no-store`、`Referrer-Policy: no-referrer`
332-
- SQLite 一律只读打开;不写入任何会话产物
333-
- 拒绝 symlink 的输出目录;单行/单响应大小上限
325+
### 6.5 安全基线(对齐先例,并按 review 修正)
326+
327+
监听与网络隔离:
328+
329+
- **仅绑定 `127.0.0.1`**,且地址是常量,无任何覆盖途径(环境变量/参数/配置都改不动)
330+
- 请求级再挡一道:`req.socket.remoteAddress` 不是回环(含 `::1`、`::ffff:127.0.0.1`、`127.0.0.0/8`)
331+
直接 403 `forbidden_remote`
332+
- `Host` 头必须等于监听 authority;带 `Origin` 时校验同源;`Sec-Fetch-Site` 为 cross-site / same-site 时拒绝
333+
- 测试断言「所有非回环本地地址上都连不通」
334+
335+
授权(**不是** CSRF 栅栏):
336+
337+
- 每个 MCP 进程启动面板时用 CSPRNG 生成 256 位 capability token,放在 URL fragment(`#t=…`)
338+
- 每个 `/api/*` 都要求 `x-trajectory-token`,用 `timingSafeEqual` 等长比较;重复头/非字符串一律拒绝
339+
- token 只在内存、不落盘、`stop()` 即失效;重启面板换新 token
340+
- fragment 不发往服务端 → 不进请求日志、不进静态资源 Referer
341+
- 进程隔离:mcode 每会话一个 MCP 进程 → 每会话一个面板一份 token,A 的 token 打不开 B 的面板
342+
- 保留 Host/Origin/Sec-Fetch 校验作为纵深,但不把它们当授权
343+
344+
文件读取的包含性(唯一不变式):
345+
346+
- 任何读取的 canonical(realpath)结果必须落在 canonical `dataDir` 之内
347+
- 末段组件不得是 symlink;用 `O_NOFOLLOW` 关闭 realpath→open 之间的窗口
348+
- 用已打开 fd 做 fstat 取 size、做 stream 读 → 检查与读取是同一个 inode(无 TOCTOU)
349+
- 每次目录遍历逐层用 `isDirectory()` 过滤(dirent 对 symlink 目录返回既非文件也非目录)
350+
- canary 测试覆盖:目录 symlink / 两级跳转 / 相对 symlink / 末段文件 symlink / 根自身是 symlink,
351+
且每条都配正向对照
352+
353+
脱敏(覆盖凭据在磁盘上的真实形态):
354+
355+
- 规则**有序**:私钥块 → 连接串内联凭据 → 整个 Authorization 头(含 scheme)→ 裸 scheme+token
356+
→ provider key → AWS key id → 键值对
357+
- 键值对允许 key 与分隔符之间夹引号(`{"api_key":"…"}`),替换时保留引号形状 → 结果仍是合法 JSON
358+
- 规则幂等(源头发过一次、出口再 sweep 一次不会二次破坏)
359+
- 结构化键用**精确名**判定(保住 `inputTokens` 等计数),自由文本用**带分隔符的包含**判定
360+
- 任务 description/command 在数据源头脱敏;会话 title 就地替换凭据子串、保留其余文本
361+
- 每个出口(MCP tool result、HTTP API response)整体 sweep 一次,上限取各面已有的最大上限
362+
(只脱敏、不额外截断),因此日后新增字段不会漏
363+
364+
渲染层:
365+
366+
- 客户端零 `innerHTML` / `insertAdjacentHTML` / `outerHTML` / `document.write` / `srcdoc` / `eval` /
367+
`new Function` / `javascript:`,且 `setAttribute` 的名字必须是字面量
368+
- 全部节点用 `createElement` 构建、用 `textContent` 写入 → 会话正文永远是文本
369+
- CSP `default-src 'none'` + `script-src 'self'`(无内联脚本、无远程源)
370+
- 上方 sink 清单由**带变异自检**的静态扫描守住(扫描器先证明自己抓得到,再扫真实资源)
371+
372+
其余:
373+
374+
- SQLite 一律只读打开(`readOnly: true`);不写入任何会话产物
375+
- 插件不创建任何文件:不写 PLUGIN_DATA、不写端口文件(面板端口只出现在工具返回值与 stdout)
376+
- 单行 / 单响应大小上限;JSONL 单行 2 MiB 上限
377+
- 错误响应只回闭集内的固定码,非预期错误只写 stderr
334378

335379
### 6.6 校验器合规清单(`scripts/validate.mjs`)
336380

0 commit comments

Comments
 (0)