tianshu-mcp 0.7.9 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.en.md CHANGED
@@ -8,6 +8,71 @@ Chinese version: [CHANGELOG.md](CHANGELOG.md)
8
8
 
9
9
  ---
10
10
 
11
+ ## [0.1.1-beta.4] — 2026-10-06 — mcp-gui independent line
12
+
13
+ > This section records the **security-hardening release** of the GUI's independent `0.1.1` line (issue #32); **the MCP
14
+ > package is untouched**. It moves the task-id character allowlist from frontend deep-link parsing down into the
15
+ > **Rust command and module layers**, closing the path-joining in four commands. **No new features, no UI changes**:
16
+ > user-visible behaviour matches `0.1.1-beta.3`.
17
+
18
+ ### Fixed
19
+
20
+ - **Escape validation for `task_id` in four Log Viewer commands (issue #32)**: `read_events` / `read_baseline` /
21
+ `export_task_zip` joined task ids **directly** into `tasks/<id>/…` without going through the module's own escape
22
+ guard `resolve_rel`, contradicting the `ARCHITECTURE.md` §16.10 statement that ids go through the `[A-Za-z0-9_-]`
23
+ allowlist. The same defence line had different conventions per command, so **integrity depended on each caller
24
+ remembering** and a newly added command would not inherit the protection. The fix is **one central choke point**:
25
+ `data_home.rs` gains `validate_task_id` (the allowlist, matching the frontend `TASK_ID_RE` in `core/deeplink.ts` and
26
+ §16.10) plus `task_dir` (validates, then joins `tasks/<id>`) as the single entry point from a bare task id to a path;
27
+ the three modules switch to `task_dir` (**defence in depth**); all four commands gain a first-line check in the
28
+ command layer (`lib.rs`) — besides the three named in the issue, **`read_report` is included too** (it used
29
+ `resolve_rel` and did not escape, but likewise lacked the character allowlist). **An assumption disproved while
30
+ verifying**: the existing `if !task_dir.is_dir()` check in `export_task_zip` was treated as an effective guard, but
31
+ measurement showed `is_dir` evaluates to true with `..` in the task id, piercing the gate (**a guard existing is not
32
+ the same as a guard working**); the allowlist now rejects the input **before any path is built**.
33
+ - **Attacked surface covered**: path traversal (`../../outside/evil` / `..` / `../..`), backslash traversal
34
+ (`..\..\evil`, **Windows only**), absolute paths (`/etc/passwd` / `C:/Windows`), NTFS alternate data streams
35
+ (`tsk_1:secret`), Windows-illegal characters (`tsk*1` / `tsk?1` / `tsk|1`), whitespace and dots (`tsk 1` / `tsk.1` /
36
+ empty), and Unicode homoglyphs (fullwidth `tsk_1` / Cyrillic `tаsk_1`) — five vectors escaped before the fix and all
37
+ are rejected after it, with **zero false rejections of legitimate ids**. The check is **byte-by-byte** rather than a
38
+ regex: no new `regex` dependency, and ASCII-only naturally excludes homoglyphs, Windows-illegal path characters and
39
+ alternate data streams.
40
+
41
+ ### Changed
42
+
43
+ - **`read_baseline` now returns an error instead of a default value for an illegal id**: the command **already** used
44
+ `Err` to report a missing task id (`lib.rs`), and an illegal character is the same class of caller error that silent
45
+ degradation would hide. A normal UI path **cannot** supply an illegal id (`selectedTaskId` comes from real directory
46
+ names returned by `list_tasks`; deep links have their own frontend allowlist), so **user-visible behaviour is
47
+ effectively unchanged**; every frontend consumer already catches errors (all six call sites verified — the
48
+ `readBaseline` sites use `try/catch` + `setError`, and `exportTaskZip`'s `Err` is caught by `doExport()` in
49
+ `WorkspacePage.vue`).
50
+
51
+ ### Tests
52
+
53
+ - Frontend **169 passed** (15 files); `check:schema` (including `GUI version consistent (0.1.1-beta.4)`) / `typecheck` /
54
+ `lint` all green.
55
+ - **This machine has no MSVC linker** (`link.exe` is shadowed by Git Bash coreutils; the Windows SDK ships no `Lib/`),
56
+ so `cargo test` / `clippy` still go to `gui.yml`. Instead this change was verified by **really compiling and running
57
+ the Rust code via `rustc --target wasm32-unknown-unknown`** (`std::path` is pure logic and the wasm32 target ships
58
+ `rust-lld`, so no MSVC is needed): **46 passed / 0 failed**, with the verified function bodies **extracted from the
59
+ on-disk source** by the harness; a **mutation test** (removing the allowlist turned 17 assertions red) proves the
60
+ suite has discriminating power.
61
+ - **`cargo fmt --check` does work locally** (rustfmt needs no linker; only `clippy` / `test` do) — the first tag push
62
+ failed the `Rust format / clippy / tests` step on the Windows leg because one new `assert_eq!` exceeded the 100-column
63
+ limit; the same diff was reproducible locally with `cargo fmt --check`, and `cargo fmt` fixed it. **Lesson**: do not
64
+ skip every Rust gate just because the linker is missing — `fmt` and `--emit=metadata` type checking are unaffected.
65
+ - New Rust unit tests: `data_home.rs` (allowlist and `task_dir` path-boundary cases), plus escape RED cases, absolute
66
+ path rejection and legitimate-id counter-proofs in `export.rs` / `baseline.rs` / `event_stream.rs`.
67
+
68
+ ### Docs
69
+
70
+ - Added `docs/release-gui-v0.1.1-beta.4.md` + `.en.md`; `ARCHITECTURE.md` / `.en.md` §16.3 records the new
71
+ `data_home.rs` responsibility and §16.10 notes that the same allowlist is now enforced in the Rust command layer and
72
+ does **not** rely on the frontend `TASK_ID_RE`.
73
+
74
+ ---
75
+
11
76
  ## [0.1.1-beta.3] — 2026-10-02 — mcp-gui independent line
12
77
 
13
78
  > This section records the **third and final pre-release batch** of the GUI's independent `0.1.1` line; **the MCP package
@@ -60,6 +125,94 @@ Chinese version: [CHANGELOG.md](CHANGELOG.md)
60
125
 
61
126
  ---
62
127
 
128
+ ## [0.8.0] - 2026-10-06
129
+
130
+ A cumulative release of **two agent resume-semantics fixes**: TraeWork's cross-mode project-binding
131
+ fallback is removed (issue #35), and ZCode resume rounds no longer silently rewrite the session's
132
+ permission (issue #30). Neither adds features or UI changes — both correct distorted behaviour when
133
+ *resuming an existing session*, and both belong to the same family: **resuming must stay faithful to
134
+ the original session**.
135
+
136
+ ### Fixed
137
+
138
+ - **TraeWork: removed the cross-mode project-binding fallback; failures keep the target mode
139
+ (issue #35, #36)**: when binding in a non-Work mode failed, `bindProject()` would **fall back to
140
+ Work mode** to complete the binding and then switch back. Under "each mode binds independently",
141
+ that fallback is **structurally unreachable**: a project bound in Work mode is not inherited by
142
+ Code / Design, so switching back loses it — the round still ends in failure, while the user's
143
+ requested mode was **silently rewritten** to Work. The whole fallback branch is now removed; a
144
+ non-Work bind failure returns an honest failure and **never changes the requested mode**. Before
145
+ the fix, two behaviour tests in `test/integration/traework-bind-fallback.test.ts` failed faithfully
146
+ (`expected 'Work' to be 'Code'` / `expected 'Work' to be 'Design'`); they pass after it.
147
+
148
+ - **ZCode: resume rounds preserve the original session permission (issue #30, #37)**: when
149
+ `continue_task` / `rework_task` resumed an existing session, `runZcodeTask` first derived the
150
+ permission from `ctx.resume.permissionMode` but then **unconditionally overwrote** it with
151
+ `gui.defaultPermissionMode` before dispatch — silently reverting the session permission to the
152
+ profile default, then forcing that value onto the UI and reading it back, so the reported
153
+ `session.permissionMode` was distorted too. `permission` is now `const` (the only assignment site in
154
+ the file) and falls back to the profile default **only when no record exists**; on a read-back
155
+ mismatch the error now carries the **actual target permission** (it previously always said
156
+ "完全访问", which misled diagnosis).
157
+
158
+ ### Verification
159
+
160
+ - TraeWork: `vitest run traework` — 15 files / 139 tests pass; reverting `session.ts` reproduces the
161
+ two failures.
162
+ - ZCode: three related test files — 85 tests pass; reverting only `src/agents/zcode/run.ts` yields
163
+ 5 failed / 3 passed on the new cases, failing exactly at the layer under test (`fake.permission`
164
+ expected "受限访问", got "完全访问").
165
+ - `tsc --noEmit` / ESLint `--max-warnings 0` / `git diff --check` pass.
166
+ - **The ZCode side uses a fake-CDP integration test and has not been verified on a real machine**
167
+ (the boundary declared in PR #37; carried over here).
168
+
169
+ ### Known limitations
170
+
171
+ - The ZCode permission-preservation fix is covered only by fake-CDP integration tests; no real-machine
172
+ (actual ZCode 3.14.x) reproduction.
173
+ - In the full `vitest run`, `test/integration/zcode-rework-loop.test.ts` has a **timing flake waiting
174
+ for a terminal state**, reproduced on base master too and unrelated to these two fixes.
175
+
176
+ ---
177
+
178
+ ## [0.7.10] - 2026-10-06
179
+
180
+ ### Fixed
181
+
182
+ - **Precise semantics for the visual content-egress gate (issue #29)**: with `allowRemote` defaulting to
183
+ `false`, `<image:base64:file>` is rejected by the schema, while `<image:path>` / `<expect:file>` are
184
+ unconstrained — and the docs implied the contract layer blocked *every* way of handing an image to a
185
+ command, leading readers to believe the channels were sealed. Probes and counter-evidence show that
186
+ **gating the path channel yields zero security**: the judgement command runs with `shell:false` inside the
187
+ **project directory** and can already read project files itself; forcing an opt-in would also make users
188
+ set `allowRemote` just to pass the schema, which **opens up the inline base64 channel too**. This release
189
+ therefore does not change the gate — it states the boundary precisely and makes the channels visible.
190
+
191
+ ### Changed
192
+
193
+ - **New `contentChannelUsage()` (`src/visual/schema.ts`) as the single source of channel semantics**: the
194
+ gate check and the doctor/probe display share the same logic, so the two cannot drift apart.
195
+ - **`visual doctor` now reports each rule's actually-used placeholder channels and whether they are
196
+ constrained** (`GATED` / `NOT gated`) — surfacing what the command really receives (absence never raises
197
+ an alarm by itself).
198
+ - **`visual content probe` output gains `egressConstrained` / `pathChannels`** for programmatic checks.
199
+ - Wording synced across `SECURITY.md` / `SECURITY.en.md`, `docs/visual-acceptance.md` / `.en.md`, and
200
+ `skills/tianshu-mcp/SKILL.md`: `allowRemote` constrains **only** the inline-byte shape, and the contract
201
+ layer is **not** a complete block on image egress.
202
+ - `<expect:file>` nature clarified: it delivers a temp file holding the **expectation text** and is **not**
203
+ an image-egress channel (the issue conflated the two).
204
+
205
+ ### Verification
206
+
207
+ - 5 new channel/admission contract tests (`test/unit/visual-content-schema.test.ts`), 2 doctor
208
+ channel-semantics tests (`test/unit/visual-runtime.test.ts`), plus the probe field assertion.
209
+ - Counter-evidence probe: the command read a project file with **zero placeholders**
210
+ (`READ 4187 bytes with zero placeholders`), proving gating the path channel has no benefit.
211
+ - Full `npm test` **1582 passed / 12 skipped, 0 failed**; `typecheck` / `lint` / `build` / `pack:check` /
212
+ `check:stdio` all green.
213
+
214
+ ---
215
+
63
216
  ## [0.7.9] - 2026-10-06
64
217
 
65
218
  ### Fixed
package/CHANGELOG.md CHANGED
@@ -7,6 +7,34 @@
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.1.1-beta.4] — 2026-10-06 — mcp-gui 独立版本线
11
+
12
+ > 本段记录 GUI 独立版本线 `0.1.1` 的**安全加固版**(issue #32);**MCP 主包零改动**。
13
+ > 把任务 ID 的字符白名单从前端深链下沉到 **Rust 命令层与模块层**,收口四条命令的路径拼装。
14
+ > **无新增功能、无界面变化**,用户可见行为与 `0.1.1-beta.3` 一致。
15
+
16
+ ### 修复
17
+
18
+ - **日志台四条命令的 `task_id` 补齐越界校验(issue #32)**:`read_events` / `read_baseline` / `export_task_zip` 在拼接 `tasks/<任务>/…` 时对任务 ID **直接 `join`**,未走同模块的越界校验 `resolve_rel`,与 `ARCHITECTURE.md` §16.10「ID 走字符白名单 `[A-Za-z0-9_-]`」的自述矛盾——同一条防线在各命令上口径不一,**完整性依赖调用方自觉**,新增命令不会自动继承防护。修法为**单点收口**:`data_home.rs` 新增 `validate_task_id`(白名单,与前端 `core/deeplink.ts` 的 `TASK_ID_RE` 及 §16.10 同口径)与 `task_dir`(先过白名单再拼 `tasks/<id>`),作为「裸任务 ID → 路径」的唯一入口;三个模块改用 `task_dir`(**防御深度**);命令层(`lib.rs`)四条命令加第一道校验——除 issue 点名的三条外,**一并纳入 `read_report`**(它走 `resolve_rel` 本不逃逸,但同样缺字符白名单,同属口径不一致)。**验证中推翻的假设**:`export_task_zip` 原有的 `if !task_dir.is_dir()` 曾被当作有效防线,实测 `task_id` 含 `..` 时 `is_dir` 为真、闸门被穿透(**存在闸门 ≠ 闸门有效**),现由白名单在**拼路径之前**拦下。
19
+ - **覆盖的攻击面**:路径穿越(`../../outside/evil` / `..` / `../..`)、反斜杠穿越(`..\..\evil`,**仅 Windows 生效**)、绝对路径(`/etc/passwd` / `C:/Windows`)、NTFS 备用数据流(`tsk_1:secret`)、Windows 非法字符(`tsk*1` / `tsk?1` / `tsk|1`)、空白与点号(`tsk 1` / `tsk.1` / 空串)、Unicode 同形字(全角下划线 `tsk_1` / 西里尔 `tаsk_1`)——修复前 5 条确凿逃逸,修复后全部拒绝;**合法 ID 零误伤**。用**逐字节判定**而非正则:不引入 `regex` 依赖,ASCII-only 天然排除同形字、Windows 非法路径字符与备用数据流。
20
+
21
+ ### 行为变更
22
+
23
+ - **`read_baseline` 对非法 ID 由「回默认值」改为「报错」**:该命令**本就用 `Err` 表达「任务 ID 缺失」**(`lib.rs`),非法字符属同类调用方错误,静默降级会掩盖 bug。正常界面路径**不可能**传入非法 ID(`selectedTaskId` 来自 `list_tasks` 的真实目录名,深链另有前端白名单),故**用户可见行为 ≈ 0**;前端 `readBaseline` 调用点已有 `try/catch` + `setError` 兜底,`exportTaskZip` 的 `Err` 由 `WorkspacePage.vue` 的 `doExport()` 捕获(已核对全部 6 个调用点)。
24
+
25
+ ### 测试
26
+
27
+ - 前端 **169 passed**(15 文件),`check:schema`(含 `GUI 版本号一致(0.1.1-beta.4)`)/ `typecheck` / `lint` 全绿。
28
+ - **Rust 侧本机无 MSVC 链接器**(`link.exe` 被 Git Bash coreutils 遮蔽,Windows SDK 无 `Lib/`),故 `cargo test` / `clippy` 仍交 `gui.yml`;本次改用 **`rustc --target wasm32-unknown-unknown` 真实编译并执行**(`std::path` 为纯逻辑,wasm32 目标自带 `rust-lld`,无需 MSVC):**46 passed / 0 failed**,被验证函数体由脚本从磁盘源码**现场抽取**;另做**变异测试**(移除白名单 → 17 项断言转红)证明测试有辨别力。
29
+ - **`cargo fmt --check` 本机可用**(rustfmt 不需要链接器,只有 `clippy` / `test` 需要)——首次推 tag 时 CI 的 `Rust format / clippy / tests` 在 windows 腿失败,根因是新增单测里一处 `assert_eq!` 超 100 字符宽度;本机 `cargo fmt --check` 可复现同一 diff,`cargo fmt` 修正后重跑通过。**教训**:不要因为链接器缺失就跳过全部 Rust 门禁,`fmt` 与 `--emit=metadata` 类型检查都不受影响。
30
+ - 新增 Rust 单测:`data_home.rs`(`validate_task_id` 白名单 / `task_dir` 路径边界 4 项),`export.rs` / `baseline.rs` / `event_stream.rs` 各补越界 RED 用例 + 绝对路径拒绝 + 合法 ID 反证。
31
+
32
+ ### 文档
33
+
34
+ - 新增 `docs/release-gui-v0.1.1-beta.4.md` + `.en.md`;`ARCHITECTURE.md` / `.en.md` §16.3 补 `data_home.rs` 的新增职责、§16.10 补「同一白名单已下沉到 Rust 命令层,不依赖前端 `TASK_ID_RE`」。
35
+
36
+ ---
37
+
10
38
  ## [0.1.1-beta.3] — 2026-10-02 — mcp-gui 独立版本线
11
39
 
12
40
  > 本段记录 GUI 独立版本线 `0.1.1` 的**第三个(最后一个)预发布批次**;**MCP 主包零改动**。
@@ -58,6 +86,78 @@
58
86
 
59
87
  ---
60
88
 
89
+ ## [0.8.0] - 2026-10-06
90
+
91
+ 本版为**两个 agent 恢复语义修复的累积发布**:TraeWork 的跨模式项目绑定兜底被移除(issue #35),
92
+ ZCode 恢复轮不再静默改写会话权限(issue #30)。两者都没有新增功能或界面变化,只纠正「恢复既有
93
+ 会话时」的行为失真——都属**恢复语义必须忠于原会话**这条不变量的同一族问题。
94
+
95
+ ### 修复
96
+
97
+ - **TraeWork 移除跨模式项目绑定兜底,失败保留目标模式(issue #35,#36)**:
98
+ `bindProject()` 在非 Work 模式绑定失败时,会**回落 Work 模式**完成绑定再切回目标模式。
99
+ 该兜底在「各模式独立绑定」的语义下**结构性不可达**:Work 模式绑定的项目不继承给 Code / Design
100
+ 模式,切回后项目即丢失,最终仍以失败收场,且把用户的原目标模式**静默改写**为 Work。
101
+ 现移除整个兜底分支,非 Work 模式绑定失败即如实返回失败,**不改变用户请求的模式**。
102
+ 修复前 `test/integration/traework-bind-fallback.test.ts` 两条行为用例如实失败
103
+ (`expected 'Work' to be 'Code'` / `expected 'Work' to be 'Design'`),恢复实现后通过。
104
+
105
+ - **ZCode 恢复轮保留原会话权限(issue #30,#37)**:`continue_task` / `rework_task` 恢复原会话时,
106
+ `runZcodeTask` 先按 `ctx.resume.permissionMode` 求得权限,但发送前又用
107
+ `gui.defaultPermissionMode` **无条件覆盖**它——原会话权限被静默改回 profile 默认值,
108
+ 且随后以该值强制切换界面、回读,`session.permissionMode` 回执随之失真。
109
+ 现 `permission` 改为 `const`(全文件仅此一处取值),**仅在记录缺失时回落 profile 默认值**;
110
+ 权限回读失败时报错文本携带**实际目标权限**(原先固定显示「完全访问」,会误导排查)。
111
+
112
+ ### 验证
113
+
114
+ - TraeWork:`vitest run traework` 15 文件 / 139 用例通过;回退 `session.ts` 到基线可复现两条失败。
115
+ - ZCode:三个相关测试文件 85 用例通过;仅回退 `src/agents/zcode/run.ts` 到基线再跑,
116
+ 新增用例 5 failed / 3 passed,失败点即被测层(`fake.permission` 期望「受限访问」实得「完全访问」)。
117
+ - `tsc --noEmit` / ESLint `--max-warnings 0` / `git diff --check` 通过。
118
+ - **ZCode 侧使用假 CDP 集成测试,未做真机验证**(PR #37 声明的边界,本版沿用)。
119
+
120
+ ### 已知限制
121
+
122
+ - ZCode 权限保留修复仅有假 CDP 集成测试覆盖,缺真机(真实 ZCode 3.14.x)复现。
123
+ - 全量 `vitest run` 中 `test/integration/zcode-rework-loop.test.ts` 存在**等待终态超时的时序 flake**,
124
+ 在 base master 上同样复现,与本版两个修复无关。
125
+
126
+ ---
127
+
128
+ ## [0.7.10] - 2026-10-06
129
+
130
+ ### 修复
131
+
132
+ - **视觉内容判定的外发闸门语义精确化(issue #29)**:`allowRemote` 默认 `false` 时,
133
+ `<image:base64:file>` 被 schema 拒绝,但 `<image:path>` / `<expect:file>` 不受任何约束——
134
+ 文档措辞又暗示「契约层封死了所有把图交给命令的形态」,读者会以为通道已被堵死。
135
+ 经探针实测与反证(见下),**门控路径通道零安全收益**:判定命令以 `shell:false` 在**项目目录内**
136
+ 执行,本就能自读项目内文件;且强制放行会因「必须开 `allowRemote` 才能过 schema」而**反向连带
137
+ 放行 base64 内联**。故本版不改门控、只把边界说清并把通道可见化。
138
+
139
+ ### 变更
140
+
141
+ - **新增 `contentChannelUsage()`(`src/visual/schema.ts`)作为通道语义的单一来源**:
142
+ 门控判定与 doctor/probe 展示共用同一段逻辑,避免两处实现分叉。
143
+ - **`visual doctor` 逐规则输出实际使用的占位符通道与是否受约束**(`GATED` / `NOT gated`)——
144
+ 把「命令实际拿到什么」摆到台面(缺席不会自己报警)。
145
+ - **`visual content probe` 输出新增 `egressConstrained` / `pathChannels` 字段**,便于调用方可编程核对。
146
+ - `SECURITY.md` / `SECURITY.en.md`、`docs/visual-acceptance.md` / `.en.md`、`skills/tianshu-mcp/SKILL.md`
147
+ 同步措辞:明确 `allowRemote` **只**约束内联字节形态,契约层**不是**对图片外发的完备拦截。
148
+ - `<expect:file>` 性质澄清:它交付的是**期望文本**临时文件,**不是图片外发通道**(issue 原文在此处有误归并)。
149
+
150
+ ### 验证
151
+
152
+ - 新增 5 个 `contentChannelUsage` / 准入边界契约用例(`test/unit/visual-content-schema.test.ts`),
153
+ 2 个 doctor 通道语义用例(`test/unit/visual-runtime.test.ts`),probe 字段断言同步。
154
+ - 反证探针实测:命令在**零占位符**下即读到项目内文件字节(`READ 4187 bytes with zero placeholders`),
155
+ 证明门控路径通道无收益。
156
+ - 全量 `npm test` **1582 passed / 12 skipped,0 失败**;`typecheck` / `lint` / `build` / `pack:check` /
157
+ `check:stdio` 全绿。
158
+
159
+ ---
160
+
61
161
  ## [0.7.9] - 2026-10-06
62
162
 
63
163
  ### 修复
package/README.en.md CHANGED
@@ -251,8 +251,8 @@ run_task(projectPath=D:/xxx/my-app, task="…task brief…", agentId=codex,
251
251
  | agentId | driver / adapter | status | Notes |
252
252
  |---|---|---|---|
253
253
  | `codex` | `gui` / `codex-gui` | **ready** (`research` on macOS) | Codex desktop GUI (Windows: MSIX COM activation + CDP; macOS: spawn .app + CDP); supports `model` / `reasoningLevel` / `planDoc` / `designSystem`; user-confirmation wait, cancellation and re-dispatch guards are all machine-verified |
254
- | `zcode` | `gui` / `zcode-gui` | **ready** (closed-loop verified on real Windows; macOS unverified) | CDP GUI adapter; supports project-less dispatch, `allowCreateProject` and `reasoningLevel` (the tier set **varies per model**; out-of-range tiers fail **before sending**); **v0.7.4 adapted to the missing 3.14.x path contracts** (the binding verdict became "path first, display-name when no path is available + global name disambiguation", failing closed on duplicates); **v0.7.6 fixes the binding deadlock** (sidebar `workspace-item-*` nodes scrolled out of view were still collected, short-circuiting the only trustworthy menu channel), **adds a recovery entry point for runtime CDP disconnects** (reconnect once for observation, never resend; a failed reconnect lands on `needs_user(setup_recovery)`) and **fixes the two-level model menu** (provider groups render their submenu only on hover) |
255
- | `traework` | `gui` / `traework-gui` | **ready** | CDP-driven TRAE SOLO CN desktop UI; supports `mode` (Work / Code / Design, each of the three modes maintaining its own project binding); all three modes machine-verified |
254
+ | `zcode` | `gui` / `zcode-gui` | **ready** (closed-loop verified on real Windows; macOS unverified) | CDP GUI adapter; supports project-less dispatch, `allowCreateProject` and `reasoningLevel` (the tier set **varies per model**; out-of-range tiers fail **before sending**); **v0.7.4 adapted to the missing 3.14.x path contracts** (the binding verdict became "path first, display-name when no path is available + global name disambiguation", failing closed on duplicates); **v0.7.6 fixes the binding deadlock** (sidebar `workspace-item-*` nodes scrolled out of view were still collected, short-circuiting the only trustworthy menu channel), **adds a recovery entry point for runtime CDP disconnects** (reconnect once for observation, never resend; a failed reconnect lands on `needs_user(setup_recovery)`) and **fixes the two-level model menu** (provider groups render their submenu only on hover); **v0.8.0 keeps the original session permission on resume rounds** (issue #30: `continue_task` / `rework_task` are no longer silently overwritten by the profile default) |
255
+ | `traework` | `gui` / `traework-gui` | **ready** | CDP-driven TRAE SOLO CN desktop UI; supports `mode` (Work / Code / Design, each of the three modes maintaining its own project binding); all three modes machine-verified; **v0.8.0 removes the cross-mode project-binding fallback** (issue #35: a failed non-Work bind no longer falls back to Work and silently rewrites the target mode — it now fails honestly) |
256
256
  | `kimicode` | `gui` / `kimicode-gui` | **ready** (`research` on macOS) | Kimi Code desktop (Electron); **dual renderer processes** (main window plus a `Kimi Browser Overlay` that hosts the model / reasoning / mode menus); workspaces bind by full path; supports `model` / `reasoningLevel`, **not `mode`**, and **not project-less dispatch** |
257
257
  | `qoder` | `gui` / `qoder-gui` | closed-loop verified on Windows; **research** on macOS | Qoder CN only; requires an existing `projectPath` and a readable `planDoc`; `modelSource=default\|custom` disambiguates same-named models, and the reasoning level is saved as a global preference via "Model management" and read back |
258
258
  | `opendesign` | `gui` / `opendesign-gui` | **ready** (`research` on macOS) | Open Design desktop GUI; selectors are taken from the product's own web-frontend `data-testid` hooks, the full 12-step execution chain is wired, and the acceptance → auto-rework → re-acceptance loop is connected; it is the only driver with an "artifact signal" (file mtime / size fingerprint) |
@@ -372,6 +372,7 @@ Allowing and disabling (a CLI flag or its equivalent environment variable; `--no
372
372
  | Hardening and observability | 0.6.x | Skill self-install hardening (0.6.0), GUI selector drift fixes (0.6.2), fine-grained event stream, structured repair directives, dryRun, three-level acceptance config inheritance, terminal-state webhook |
373
373
  | Open Design | 0.7.x | Open Design desktop adapter (0.7.1), ZCode 3.14.x binding-contract fix (0.7.4) |
374
374
  | MiniMax Code | 0.7.8 | Seventh GUI agent (0.7.8); real-machine evidence corrected three structural assumptions (second-level submenu / per-model candidate sets / two-step project creation), plus the `contextWindow` parameter and a read-only diagnostic probe |
375
+ | Resume-semantics fixes | 0.8.0 | TraeWork: removed the cross-mode project-binding fallback (#35 — structurally unreachable and it silently rewrote the target mode); ZCode: resume rounds keep the original session permission (#30 — the unconditional pre-dispatch overwrite is gone) |
375
376
  | Log viewer GUI | `gui-v*` (separate line) | `mcp-gui/` local read-only log viewer (Tauri 2.x + Vue 3), independent version and tag, **not released with the MCP main package** |
376
377
 
377
378
  > The complete per-version record is in [CHANGELOG.en.md](CHANGELOG.en.md); handoff status and the troubleshooting handbook are in [HANDOFF.md](HANDOFF.md); engineering-metric definitions are in [ARCHITECTURE.en.md](ARCHITECTURE.en.md).
package/README.md CHANGED
@@ -251,8 +251,8 @@ run_task(projectPath=D:/xxx/my-app, task="…任务书…", agentId=codex,
251
251
  | agentId | driver / adapter | status | 说明 |
252
252
  |---|---|---|---|
253
253
  | `codex` | `gui` / `codex-gui` | **ready**(macOS 为 `research`) | Codex 桌面端 GUI(Windows:MSIX COM 激活 + CDP;macOS:spawn .app + CDP);支持 `model` / `reasoningLevel` / `planDoc` / `designSystem`;等待用户确认、取消与重派护栏均已真机验证 |
254
- | `zcode` | `gui` / `zcode-gui` | **ready**(Windows 真机闭环;macOS 未验证) | CDP GUI adapter;支持无项目派发、`allowCreateProject` 与 `reasoningLevel`(档位集合**随模型变化**,越权在**发送前**报错);**v0.7.4 适配 3.14.x 的路径契约缺席**(绑定判据改为「路径优先、无路径渠道时按显示名 + 全局同名消歧」,同名即 fail-closed);**v0.7.6 修掉绑定死锁**(侧边栏 `workspace-item-*` 滚出视口仍被采集 → 唯一可信的菜单渠道被短路)、**运行期 CDP 断连的恢复入口**(重连观察一次、绝不重发,失败落 `needs_user(setup_recovery)`)与**两级模型菜单**(provider 分组须 hover 才渲染子项) |
255
- | `traework` | `gui` / `traework-gui` | **ready** | CDP 驱动 TRAE SOLO CN 桌面 UI;支持 `mode`(Work / Code / Design,三种模式各自维护独立项目绑定);三种面板模式真机验证通过 |
254
+ | `zcode` | `gui` / `zcode-gui` | **ready**(Windows 真机闭环;macOS 未验证) | CDP GUI adapter;支持无项目派发、`allowCreateProject` 与 `reasoningLevel`(档位集合**随模型变化**,越权在**发送前**报错);**v0.7.4 适配 3.14.x 的路径契约缺席**(绑定判据改为「路径优先、无路径渠道时按显示名 + 全局同名消歧」,同名即 fail-closed);**v0.7.6 修掉绑定死锁**(侧边栏 `workspace-item-*` 滚出视口仍被采集 → 唯一可信的菜单渠道被短路)、**运行期 CDP 断连的恢复入口**(重连观察一次、绝不重发,失败落 `needs_user(setup_recovery)`)与**两级模型菜单**(provider 分组须 hover 才渲染子项);**v0.8.0 恢复轮保留原会话权限**(issue #30:`continue_task` / `rework_task` 不再被 profile 默认值静默覆盖) |
255
+ | `traework` | `gui` / `traework-gui` | **ready** | CDP 驱动 TRAE SOLO CN 桌面 UI;支持 `mode`(Work / Code / Design,三种模式各自维护独立项目绑定);三种面板模式真机验证通过;**v0.8.0 移除跨模式项目绑定兜底**(issue #35:非 Work 模式绑定失败不再回落 Work 并静默改写目标模式,失败即如实返回) |
256
256
  | `kimicode` | `gui` / `kimicode-gui` | **ready**(macOS 为 `research`) | Kimi Code 桌面端(Electron);**双渲染进程**(主窗口 + `Kimi Browser Overlay` 浮层承载模型 / 档位 / 模式菜单);工作区以完整路径绑定;支持 `model` / `reasoningLevel`,**不支持 `mode`**,且**不支持无项目派发** |
257
257
  | `qoder` | `gui` / `qoder-gui` | Windows 真机闭环通过;macOS **research** | 仅 Qoder CN;必须提供已有 `projectPath` 与可读 `planDoc`;`modelSource=default\|custom` 消除同名模型歧义,思考等级经「模型管理」保存为全局偏好并回读 |
258
258
  | `opendesign` | `gui` / `opendesign-gui` | **ready**(macOS 为 `research`) | Open Design 桌面端 GUI;选择器取自产品自身 Web 前端的 `data-testid` 钩子,12 步执行链全部接线,并接入验收 → 自动返修 → 再验收闭环;它是唯一带「产物信号」(文件 mtime / 大小指纹)的 driver |
@@ -372,6 +372,7 @@ run_task(projectPath=D:/xxx/my-app, task="…任务书…", agentId=codex,
372
372
  | 加固与可观测 | 0.6.x | 技能自装加固(0.6.0)、GUI 选择器漂移修复(0.6.2)、细粒度事件流、结构化修复指令、dryRun、验收配置三级继承、终态通知 |
373
373
  | Open Design | 0.7.x | Open Design 桌面端适配(0.7.1)、ZCode 3.14.x 绑定契约修复(0.7.4) |
374
374
  | MiniMax Code | 0.7.8 | 第七个 GUI agent 接入(0.7.8);真机取证修正三处结构假设(二级子菜单 / 集合随模型变化 / 项目创建两步),新增 `contextWindow` 参数与只读诊断探针 |
375
+ | 恢复语义修正 | 0.8.0 | TraeWork 移除跨模式项目绑定兜底(#35:兜底结构性不可达且静默改写目标模式);ZCode 恢复轮保留原会话权限(#30:发送前无条件覆盖默认值已移除) |
375
376
  | 日志台 GUI | `gui-v*`(独立线) | `mcp-gui/` 本地只读日志台(Tauri 2.x + Vue 3),独立版本与 tag,**不随 MCP 主包发布** |
376
377
 
377
378
  > 完整逐版记录见 [CHANGELOG.md](CHANGELOG.md),交接状态与排障手册见 [HANDOFF.md](HANDOFF.md),工程质量口径见 [ARCHITECTURE.md](ARCHITECTURE.md)。
@@ -316,38 +316,11 @@ async function waitDialogAppeared(timeoutMs, sleep) {
316
316
  * @param projectPath 项目绝对路径
317
317
  */
318
318
  export async function bindProject(cdp, projectPath, opts) {
319
- const { selectors, logger } = opts;
320
319
  const wantMode = opts.mode ?? "Work";
321
- const first = await bindProjectOnce(cdp, projectPath, { ...opts, mode: wantMode });
322
- if (first.bound || wantMode === "Work")
323
- return first;
324
- // 兜底(实测 2026-09-08):「选择文件夹」相关 UI 在非 Work 模式下可能不出现/不稳定
325
- // (失败任务 mode=Code 时下拉底部按钮点击后原生对话框未弹出)。回落 Work 完成绑定,
326
- // 再切回目标模式;仅重试一次,避免无限循环。
327
- logger.warn(`[traework] 在 ${wantMode} 模式绑定失败(${first.message}),回落 Work 模式重试一次`);
328
- const inWork = await bindProjectOnce(cdp, projectPath, { ...opts, mode: "Work" });
329
- if (!inWork.bound) {
330
- // 把两次失败信息都带出来,便于定位
331
- return {
332
- bound: false,
333
- method: inWork.method,
334
- message: `${wantMode} 模式失败(${first.message});Work 模式亦失败(${inWork.message})`,
335
- };
336
- }
337
- // 切回目标模式
338
- const backOk = await ensureMode(cdp, wantMode, { selectors, logger, sleep: opts.sleep });
339
- if (!backOk) {
340
- logger.warn(`[traework] Work 模式绑定成功,但切回 ${wantMode} 模式失败`);
341
- }
342
- const stillBound = await readBoundProject(cdp, selectors);
343
- if (!stillBound || !matchProjectItem({ name: stillBound, subtitle: "" }, projectPath)) {
344
- return {
345
- bound: false,
346
- method: "failed",
347
- message: `Work 模式绑定成功但切回 ${wantMode} 后项目丢失(当前:${stillBound || "空"})`,
348
- };
349
- }
350
- return { bound: true, method: inWork.method, message: `${inWork.message}(经 Work 模式兜底,已切回 ${wantMode})` };
320
+ // Work/Code/Design 各自维护独立的项目绑定(run.ts 的模式切换约束)。
321
+ // Work 绑定不会建立目标模式的绑定,跨模式重试还会改变当前模式与 Work 侧项目。
322
+ // 因此仅在目标模式内尝试,失败直接返回,不以 Work 绑定作为补救(issue #35)。
323
+ return bindProjectOnce(cdp, projectPath, { ...opts, mode: wantMode });
351
324
  }
352
325
  /** 单次绑定尝试(在指定模式下) */
353
326
  async function bindProjectOnce(cdp, projectPath, opts) {
@@ -348,7 +348,8 @@ export async function runZcodeTask(args) {
348
348
  error: "ZCode 原会话回选后身份回读不一致,不发送任务",
349
349
  });
350
350
  let session = activeSession;
351
- let permission = ctx.resume?.permissionMode ?? gui.defaultPermissionMode ?? "完全访问";
351
+ // 恢复轮沿用已记录的会话权限;仅在缺失记录时回落 profile 默认值。
352
+ const permission = ctx.resume?.permissionMode ?? gui.defaultPermissionMode ?? "完全访问";
352
353
  let answeredQuestion = false;
353
354
  /**
354
355
  * 运行期 CDP 断连的恢复位(issue #27):单次 evaluate 超时或端点抖动不等于 CDP 已死。
@@ -1006,7 +1007,6 @@ export async function runZcodeTask(args) {
1006
1007
  logger.info(`[zcode] 思考档位已切换为「${spec.level}」`);
1007
1008
  }
1008
1009
  }
1009
- permission = gui.defaultPermissionMode ?? "完全访问";
1010
1010
  if (!exactUiName(await cdp.text("permissionValue"), permission)) {
1011
1011
  if (!(await cdp.click("permissionTrigger")))
1012
1012
  return result({
@@ -1028,7 +1028,7 @@ export async function runZcodeTask(args) {
1028
1028
  if (!exactUiName(await cdp.text("permissionValue"), permission))
1029
1029
  return result({
1030
1030
  hardFailure: true,
1031
- error: "权限模式回读不是完全访问",
1031
+ error: `权限模式回读与目标权限不一致:${permission}`,
1032
1032
  endReason: "permission_unknown",
1033
1033
  });
1034
1034
  // A fresh-task click can leave the previous session pane mounted and visible
@@ -4,4 +4,4 @@
4
4
  * 本文件由 scripts/sync-version.mjs 在每次 build 前重新生成。
5
5
  */
6
6
  // generated: 勿手改 —— 运行 `npm run build` 自动同步
7
- export const MCP_SERVER_VERSION = "0.7.9";
7
+ export const MCP_SERVER_VERSION = "0.8.0";
@@ -15,6 +15,7 @@ import { VisualBrowser } from "./capture.js";
15
15
  import { VisualServices } from "./services.js";
16
16
  import { clearContentCache } from "./content-cache.js";
17
17
  import { resolveCommandPath } from "./content-command.js";
18
+ import { contentChannelUsage } from "./schema.js";
18
19
  import { contentCommandParts, hasContentRules, probeContent, } from "./content.js";
19
20
  import { isDefaultWorkspace } from "../tasks/task.js";
20
21
  function taskDirectory(home, taskId) {
@@ -159,6 +160,7 @@ export async function probeContentRules(home, projectPath, ruleId) {
159
160
  command: effective.command,
160
161
  resolved: (await resolveCommandPath(effective.command, await projectFile(projectPath, effective.cwd))) !== null,
161
162
  allowRemote: effective.allowRemote,
163
+ ...contentChannelUsage(effective.argsTemplate),
162
164
  });
163
165
  for (const [fileIndex, file] of rule.files.entries())
164
166
  results.push(await probeContent({ config, project: projectPath, budget, tempDir: tempRoot }, {
@@ -175,6 +177,7 @@ export async function probeContentRules(home, projectPath, ruleId) {
175
177
  command: effective.command,
176
178
  resolved: (await resolveCommandPath(effective.command, await projectFile(projectPath, effective.cwd))) !== null,
177
179
  allowRemote: effective.allowRemote,
180
+ ...contentChannelUsage(effective.argsTemplate),
178
181
  });
179
182
  for (const viewport of config.viewports.filter((v) => page.viewports === undefined || page.viewports.includes(v.id))) {
180
183
  const captured = await browser.capture(projectPath, page, viewport, await services.get(page.source));
@@ -5,6 +5,7 @@ import { VisualError } from "./errors.js";
5
5
  import { projectFile } from "./paths.js";
6
6
  import { contentCommandParts, hasContentRules, contentRulesOf } from "./content.js";
7
7
  import { resolveCommandPath } from "./content-command.js";
8
+ import { contentChannelUsage } from "./schema.js";
8
9
  export function assertVisualRuntime(version = process.versions.node) {
9
10
  const [major = 0, minor = 0] = version.split(".").map(Number);
10
11
  if (major < 20 || (major === 20 && minor < 3))
@@ -117,6 +118,8 @@ export async function doctor(projectPath, home) {
117
118
  await check("browser", async () => resolveBrowser((await readAcceptanceConfig(projectPath))?.visual?.browser ?? { mode: "managed" }, home));
118
119
  // 内容校验诊断(issue #13 F 组):逐条有效命令的解析结果 + allowRemote 声明清单、
119
120
  // 规则数 × samples × timeoutMs 与 roundTimeoutMs 的预算对比(超预算给出建议值,不自动改配置)
121
+ // issue #29:额外显性化**通道语义**——命令实际使用哪些占位符、哪些受 allowRemote 约束,
122
+ // 避免「allowRemote=false 即封死一切外发」的错误安全感(缺席不会自己报警)。
120
123
  await check("content command", async () => {
121
124
  const visual = (await readAcceptanceConfig(projectPath))?.visual;
122
125
  if (!visual?.content.enabled || !hasContentRules(visual))
@@ -129,7 +132,11 @@ export async function doctor(projectPath, home) {
129
132
  const resolved = await resolveCommandPath(effective.command, cwd);
130
133
  if (!resolved)
131
134
  unresolved.push(rule.label);
132
- lines.push(`${rule.label}: ${effective.command} -> ${resolved ?? "UNRESOLVED (will block the whole round)"}; allowRemote=${effective.allowRemote}`);
135
+ // allowRemote 的实际约束范围只有内联字节通道;路径通道不受约束(issue #29)
136
+ const channels = contentChannelUsage(effective.argsTemplate);
137
+ const gated = channels.egressConstrained ? "GATED" : "NOT gated";
138
+ const used = [...(channels.egressConstrained ? ["<image:base64:file>"] : []), ...channels.pathChannels];
139
+ lines.push(`${rule.label}: ${effective.command} -> ${resolved ?? "UNRESOLVED (will block the whole round)"}; allowRemote=${effective.allowRemote} (constrains only <image:base64:file>); channels used: ${used.length ? used.join(", ") : "(none)"} — ${gated} by allowRemote`);
133
140
  }
134
141
  // 有效命令不可解析会让整轮配置错误(assertContentReady 抛错),诊断必须据实报失败
135
142
  if (unresolved.length)
@@ -132,6 +132,29 @@ export const ContentVerdictSchema = z
132
132
  const envReference = z.record(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), z.string().min(1));
133
133
  /** 占位符白名单:argsTemplate 中形如 <...> 的 token 只允许这三个 */
134
134
  export const CONTENT_PLACEHOLDERS = ["<image:path>", "<expect:file>", "<image:base64:file>"];
135
+ /**
136
+ * 外发闸门的通道边界(issue #29)——**唯一的语义源**,schema 校验与 doctor/probe 展示共用:
137
+ * - `<image:base64:file>`:把图片字节**内联**进临时文件。这是「明确要把图片交给命令」的强信号,
138
+ * 受 `allowRemote` 约束(未放行即拒绝配置)。定位是**防无意/防误配**,不是防有意外发。
139
+ * - `<image:path>` / `<expect:file>`:交付路径/期望文本,**不受** `allowRemote` 约束。
140
+ * 命令的 cwd 本就在项目内、可自读文件,门控 path 无安全收益(见计划 §2.1 探针);
141
+ * 且强制放行会反向扩大外发面(用户为过 schema 而开 allowRemote 会连带放行 base64)。
142
+ */
143
+ export const EGRESS_CONSTRAINED_PLACEHOLDER = "<image:base64:file>";
144
+ /** 按 argsTemplate 统计通道使用情况:doctor/probe 据此把「命令实际拿到什么」显性化 */
145
+ export function contentChannelUsage(argsTemplate) {
146
+ const pathChannels = [];
147
+ let egressConstrained = false;
148
+ for (const token of argsTemplate)
149
+ for (const match of token.matchAll(/<[^<>\s]*>/g)) {
150
+ const channel = match[0];
151
+ if (channel === EGRESS_CONSTRAINED_PLACEHOLDER)
152
+ egressConstrained = true;
153
+ else if (!pathChannels.includes(channel))
154
+ pathChannels.push(channel);
155
+ }
156
+ return { egressConstrained, pathChannels };
157
+ }
135
158
  /** 页面/规则共用的内容检查声明(pages[].content 与 contents[] 条目) */
136
159
  export const ContentCheckSchema = z
137
160
  .object({
@@ -296,9 +319,11 @@ export const VisualConfigSchema = z
296
319
  const argsTemplate = check.argsTemplate ?? v.content.argsTemplate;
297
320
  if (!command || !argsTemplate)
298
321
  issue(`${label}: content checks require command and argsTemplate`);
299
- // 外发闸门:字节外传占位符必须逐规则显式放行
322
+ // 外发闸门:仅内联字节通道(<image:base64:file>)需逐规则显式放行;
323
+ // 路径通道(<image:path>/<expect:file>)不受约束——边界见 EGRESS_CONSTRAINED_PLACEHOLDER 注释
300
324
  const allowRemote = check.allowRemote ?? v.content.allowRemote;
301
- if (argsTemplate?.some((token) => token.includes("<image:base64:file>")) && allowRemote !== true)
325
+ const channels = contentChannelUsage(argsTemplate ?? []);
326
+ if (channels.egressConstrained && allowRemote !== true)
302
327
  issue(`${label}: <image:base64:file> requires allowRemote = true`);
303
328
  // 预算自洽:roundTimeoutMs 是硬总闸,超预算配置必然整轮 ROUND_TIMEOUT,必须在配置期拦截
304
329
  const samples = check.samples ?? v.content.samples;
@@ -166,13 +166,15 @@ client. Judgement is fully delegated to a local command you supply, which uses i
166
166
 
167
167
  - **Placeholders** (a placeholder absent from the template produces no temporary file):
168
168
 
169
- | Placeholder | Expands to | Extra condition |
170
- |---|---|---|
171
- | `<image:path>` | absolute path of the inspected image (through the project path gate) | — |
172
- | `<expect:file>` | absolute path of a temporary file holding the expectation as UTF-8 | — |
173
- | `<image:base64:file>` | absolute path of a temporary file holding the image's base64 | **requires** the rule's effective `allowRemote === true`, else the schema rejects it |
169
+ | Placeholder | Expands to | Constrained by `allowRemote`? | Extra condition |
170
+ |---|---|---|---|
171
+ | `<image:path>` | absolute path of the inspected image (through the project path gate) | **No** | — |
172
+ | `<expect:file>` | absolute path of a temporary file holding the expectation as UTF-8 (**not** an image-egress channel; it carries expectation text only) | **No** | — |
173
+ | `<image:base64:file>` | absolute path of a temporary file holding the image's base64 (inlines the image **bytes**) | **Yes** | **requires** the rule's effective `allowRemote === true`, else the schema rejects it |
174
174
 
175
- Any other `<...>` token is rejected at configuration time.
175
+ Any other `<...>` token is rejected at configuration time. `allowRemote` constrains **only** the
176
+ inline-byte shape `<image:base64:file>`; the two path channels are unconstrained (see “Egress statement”
177
+ below), and `visual doctor` marks each rule's actually-used channels.
176
178
 
177
179
  - **stdout**: the **last non-empty line** is parsed as JSON: `{ "passed": boolean, "confidence"?: 0..1, "reason": string }` (strict mode; unknown fields rejected).
178
180
  - **Exit code**: `0` means the command ran normally (**not** that the judgement passed — read the JSON); non-`0` means the command failed.
@@ -221,7 +223,9 @@ There is no separate cap on judgement count or spend. Cost is bounded entirely b
221
223
 
222
224
  ### Egress statement
223
225
 
224
- Whether images leave the machine **depends on the behaviour of your command**; the MCP cannot block that at the system level. Its enforcement is contract-level only: a rule that has not explicitly opted into `allowRemote` may not use `<image:base64:file>` (the schema rejects it). `visual doctor` lists each rule's `allowRemote` declaration. Confirm your command's actual behaviour yourself.
226
+ Whether images leave the machine **depends on the behaviour of your command**; the MCP cannot block that at the system level. Its enforcement is contract-level only, and **covers just the `allowRemote` constraint on `<image:base64:file>`** (inlining the image bytes into a temp file; the schema rejects it unless the rule explicitly opts in).
227
+
228
+ **The contract layer is not a complete block on image egress** (issue #29): `<image:path>` / `<expect:file>` are **not** constrained by `allowRemote`. The judgement command runs with `shell:false` inside the **project directory** and can read the inspected project image by itself, so gating the path channel adds no security while forcing users to set `allowRemote` just to pass the schema — which would also open up the inline base64 channel. `allowRemote` is about **preventing accidental/misconfigured inline egress**, not “blocking every way of handing an image to a command”. `visual doctor` lists each rule's `allowRemote` declaration and the placeholder channels it **actually uses** (marking which are constrained). Confirm your command's actual behaviour yourself.
225
229
 
226
230
  ### Testing your command
227
231
 
@@ -168,13 +168,14 @@ tianshu-mcp visual rules approve TASK_ID REVIEW_ID DIGEST "用户确认的批准
168
168
 
169
169
  - **占位符**(未在模板中出现的占位符不会生成对应临时文件):
170
170
 
171
- | 占位符 | 展开为 | 附加条件 |
172
- |---|---|---|
173
- | `<image:path>` | 被检图片的绝对路径(经项目路径闸门) | — |
174
- | `<expect:file>` | 写入 UTF-8 期望原文的临时文件绝对路径 | — |
175
- | `<image:base64:file>` | 写入该图片 base64 的临时文件绝对路径 | **必须**该规则有效 `allowRemote === true`,否则 schema 拒绝 |
171
+ | 占位符 | 展开为 | 是否受 `allowRemote` 约束 | 附加条件 |
172
+ |---|---|---|---|
173
+ | `<image:path>` | 被检图片的绝对路径(经项目路径闸门) | **否** | — |
174
+ | `<expect:file>` | 写入 UTF-8 期望原文的临时文件绝对路径(**不是图片外发通道**,只交付期望文本) | **否** | — |
175
+ | `<image:base64:file>` | 写入该图片 base64 的临时文件绝对路径(把图片**字节**内联进文件) | **是** | **必须**该规则有效 `allowRemote === true`,否则 schema 拒绝 |
176
176
 
177
- 出现任何其他 `<...>` token 直接拒绝配置。
177
+ 出现任何其他 `<...>` token 直接拒绝配置。`allowRemote` **只**约束 `<image:base64:file>` 这一内联字节
178
+ 形态;两个路径通道不受约束(原因见下方「数据外发声明」),`visual doctor` 会逐规则标出实际使用的通道。
178
179
 
179
180
  - **stdout**:取**最后一行非空文本**解析 JSON:`{ "passed": boolean, "confidence"?: 0..1, "reason": string }`(严格模式,未知字段拒绝)。
180
181
  - **退出码**:`0` 表示命令正常执行(**不代表判定通过**,通过与否看 JSON);非 `0` 表示命令执行失败。
@@ -230,9 +231,14 @@ tianshu-mcp visual rules approve TASK_ID REVIEW_ID DIGEST "用户确认的批准
230
231
 
231
232
  ### 数据外发声明
232
233
 
233
- 图片是否离开本机取决于**用户自备命令的行为**,MCP 无法在系统层拦截。MCP 的强制力仅在契约层:未显式放行
234
- `allowRemote` 的规则禁止使用 `<image:base64:file>`(schema 拒绝)。`visual doctor` 列出各规则的 `allowRemote`
235
- 声明。请自行确认命令的实际行为。
234
+ 图片是否离开本机取决于**用户自备命令的行为**,MCP 无法在系统层拦截。MCP 的强制力仅在契约层,且
235
+ **只覆盖 `allowRemote` 对 `<image:base64:file>` 的约束**(把图片字节内联进临时文件;未显式放行即 schema 拒绝)。
236
+
237
+ **契约层不是对图片外发的完备拦截**(issue #29):`<image:path>` / `<expect:file>` 不受 `allowRemote`
238
+ 约束。判定命令以 `shell:false` 在**项目目录内**执行,本身就能读取项目内的被检图片,因此门控路径通道
239
+ 既无安全收益,又会因「必须开 `allowRemote` 才能过 schema」而反向连带放行 base64 内联。`allowRemote`
240
+ 的定位是**防无意/防误配的内联外发**,不是「封死一切把图交给命令的形态」。`visual doctor` 列出各规则的
241
+ `allowRemote` 声明与**实际使用的占位符通道**(标明哪些通道受约束)。请自行确认命令的实际行为。
236
242
 
237
243
  ### 验证自备命令
238
244
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tianshu-mcp",
3
- "version": "0.7.9",
3
+ "version": "0.8.0",
4
4
  "description": "天枢 × AI-Agent 编排 MCP server —— 驱动 Codex、TraeWork、ZCode、Kimi Code、Qoder CN 与 Open Design 完成项目开发、验收、失败返修与再验收闭环。",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -274,7 +274,7 @@ meta 的 `needsUserKind` 给出等待类型,`pendingQuestion` 给出问题原
274
274
  - **整轮阻塞**:任一规则的**有效**命令不可解析(`CONTENT_COMMAND_MISSING`)或宿主环境变量缺失(`CONTENT_ENV_MISSING`)会让整轮进 `needs_attention` 且**不产出任何视觉结果行**。这是 fail-closed,不是告警。
275
275
  - **`uncertain` 不是失败**:票不集中或低于 `minConfidence` 时判 `uncertain`,永不阻塞、不触发返修;命令不报 confidence 时 `minConfidence` 不生效。
276
276
  - 排查:`tianshu-mcp visual doctor <project>`、`visual content probe <project> [ruleId]`、`visual content cache clear <taskId>`。
277
- - **数据外发**:`allowRemote` 默认 `false`,未放行的规则禁止使用字节外传占位符;图片是否离开本机取决于用户命令的行为,MCP 无法在系统层拦截。
277
+ - **数据外发**:`allowRemote` 默认 `false`,**只**禁止受约束的内联字节通道 `<image:base64:file>`(未放行即 schema 拒绝);`<image:path>` / `<expect:file>` 是**不受约束**的路径通道(命令在项目内执行、本就能自读文件,门控无收益)。契约层不是对图片外发的完备拦截——图片是否离开本机取决于用户命令的行为,MCP 无法在系统层拦截。`visual doctor` 逐规则标出实际使用的通道与是否 `GATED`。
278
278
 
279
279
  ---
280
280