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 +153 -0
- package/CHANGELOG.md +100 -0
- package/README.en.md +3 -2
- package/README.md +3 -2
- package/dist/agents/traework/ui/session.js +4 -31
- package/dist/agents/zcode/run.js +3 -3
- package/dist/version.generated.js +1 -1
- package/dist/visual/manage.js +3 -0
- package/dist/visual/runtime.js +8 -1
- package/dist/visual/schema.js +27 -2
- package/docs/visual-acceptance.en.md +11 -7
- package/docs/visual-acceptance.md +15 -9
- package/package.json +1 -1
- package/skills/tianshu-mcp/SKILL.md +1 -1
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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) {
|
package/dist/agents/zcode/run.js
CHANGED
|
@@ -348,7 +348,8 @@ export async function runZcodeTask(args) {
|
|
|
348
348
|
error: "ZCode 原会话回选后身份回读不一致,不发送任务",
|
|
349
349
|
});
|
|
350
350
|
let session = activeSession;
|
|
351
|
-
|
|
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
|
package/dist/visual/manage.js
CHANGED
|
@@ -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));
|
package/dist/visual/runtime.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
package/dist/visual/schema.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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`
|
|
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
|
@@ -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
|
|
277
|
+
- **数据外发**:`allowRemote` 默认 `false`,**只**禁止受约束的内联字节通道 `<image:base64:file>`(未放行即 schema 拒绝);`<image:path>` / `<expect:file>` 是**不受约束**的路径通道(命令在项目内执行、本就能自读文件,门控无收益)。契约层不是对图片外发的完备拦截——图片是否离开本机取决于用户命令的行为,MCP 无法在系统层拦截。`visual doctor` 逐规则标出实际使用的通道与是否 `GATED`。
|
|
278
278
|
|
|
279
279
|
---
|
|
280
280
|
|