Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "thinkwatch-lite",
"private": true,
"description": "Desktop app for a local AI API gateway on macOS, Windows and Linux",
"version": "2026.10.16",
"version": "2026.10.17",
"type": "module",
"packageManager": "pnpm@11.13.0",
"scripts": {
Expand Down
9 changes: 9 additions & 0 deletions release-notes/2026.10.17.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
**Upgrade notes:**
- The bundled core is now 0.71.0, with control-plane protocol 47. A remote server must run core 0.71.0; servers on 0.70.x do not connect until they are upgraded.
- Nothing is cleared; the request history, stored payloads and settings carry over.
- **Check your custom outbound redaction rules.** A rule written before this release was a case-sensitive regular expression; it now looks for its text as written and ignores case. A rule that relies on a regular expression keeps loading and simply stops matching, with nothing reported, so what it used to replace is sent as it is. Open each such rule and choose **Regex** under **Match by**, and **Match case** to keep telling capital letters from small ones.

**Outbound redaction:**
- Field names in a request are no longer searched, and custom rules leave alone the identifiers a protocol needs exactly as they are: item and call ids, types, roles, tool names and model names. A custom rule whose text appeared inside an id that Codex sends with its history used to replace part of the id; the upstream then rejected that request and every later turn of the conversation. Built-in rules still search those values.
- The private-key rule now looks for the end of a key block only within the same text. A key header without an end in one message no longer joins up with an end line in a later message, which used to replace everything in between with one placeholder.
- A custom redaction rule has **Match by**: **Contains** looks for the text exactly as written, so `.` and `*` have no special meaning; **Regex** takes a regular expression. **Match case** next to it tells capital letters from small ones; it is off by default. The test box tries the chosen settings, and the rule list and the security log detail say how each rule matches.
30 changes: 15 additions & 15 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "thinkwatch-lite"
version = "2026.10.16"
version = "2026.10.17"
edition = "2024"
# **这个数决定依赖能升到哪一版。**edition 2024 的解析器只挑声明的 Rust
# 版本编得动的依赖:写 1.85 的时候,`cargo update` 一直停在旧的 time 和
Expand All @@ -26,12 +26,12 @@ license = "MIT"
# twcore 二进制必须和这里编译进去的协议镜像来自同一个 core 版本 —— 打包脚本正是
# 从 tw-api 锁到的 tag 去取二进制的(见 scripts/fetch-core.sh)。
[workspace.dependencies]
tw-api = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.70.1" }
tw-types = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.70.1" }
tw-yaml = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.70.1" }
tw-guard = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.70.1" }
tw-watch = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.70.1" }
tw-link = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.70.1" }
tw-api = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.71.0" }
tw-types = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.71.0" }
tw-yaml = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.71.0" }
tw-guard = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.71.0" }
tw-watch = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.71.0" }
tw-link = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.71.0" }

[lib]
name = "thinkwatch_lite_lib"
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ThinkWatch Lite",
"version": "2026.10.16",
"version": "2026.10.17",
"identifier": "app.thinkwatch.lite",
"build": {
"beforeDevCommand": "pnpm dev",
Expand Down
40 changes: 32 additions & 8 deletions src/generated/tw-api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Generated by tw-api (`tw_api::ts::export_all`). Do not edit by hand.

export const CONTROL_API_VERSION = 46;
export const CONTROL_API_VERSION = 47;

/**
* 手动中止(`POST /request/{id}/abort`、`POST /sessions/{id}/abort`)叫停了哪几个请求。
Expand Down Expand Up @@ -1056,18 +1056,23 @@ no_usage_requests: number, };
*/
export type CustomRuleSave = { name: string,
/**
* 正则;内容过滤按 `match`:要找的那段文字、正则,或者码位(`U+200B, U+E0000–U+E007F`)
* 工具调用审查是正则;出站脱敏和内容过滤按 `match`:要找的那段文字、正则,或者码位
* (`U+200B, U+E0000–U+E007F`,只有内容过滤)
*/
pattern: string,
/**
* 工具调用审查:`cut` / `record`;内容过滤:`block` / `strip` / `record`。不给按 `record`
*/
action?: RuleAction | null,
/**
* 内容过滤才有:`contains`(不分大小写的子串)/ `regex` / `codepoints`。不给按
* `contains`。别的防护的自定义规则都是正则
* 怎么认。内容过滤:`contains`(不分大小写的子串)/ `regex` / `codepoints`;出站脱敏:
* `contains`(按原样找这段文字)/ `regex`。不给按 `contains`。工具调用审查的都是正则
*/
match?: ContentMatch | null,
/**
* 出站脱敏才有:分大小写。不给是不分
*/
case_sensitive?: boolean | null,
/**
* 出站脱敏才有:占位符名称,`PROJECT` 换成 `<<TW_PROJECT_1>>`。大写字母开头,其余是
* 大写字母、数字、下划线,最多 24 个字符。不给是 `SECRET`
Expand Down Expand Up @@ -2586,7 +2591,16 @@ base_version?: string | null, };
/**
* 一条规则按什么认。**给界面说明用**,界面按类型写成自己的话。
*/
export type Matcher = { "kind": "prefix", prefix: string, min_tail: number, } | { "kind": "openai-legacy", min_len: number, } | { "kind": "pem" } | { "kind": "jwt" } | { "kind": "conn-string" } | { "kind": "private-ip" } | { "kind": "domain-suffix", suffixes: Array<string>, } | { "kind": "cn-resident-id", born_since: number, } | { "kind": "bank-card", networks: Array<CardNetwork>, } | { "kind": "email" } | { "kind": "cn-mobile-phone" } | { "kind": "regex", pattern: string, } | { "kind": "contains", text: string, } | { "kind": "codepoints", ranges: Array<string>, } | { "kind": "builtin", check: string, };
export type Matcher = { "kind": "prefix", prefix: string, min_tail: number, } | { "kind": "openai-legacy", min_len: number, } | { "kind": "pem" } | { "kind": "jwt" } | { "kind": "conn-string" } | { "kind": "private-ip" } | { "kind": "domain-suffix", suffixes: Array<string>, } | { "kind": "cn-resident-id", born_since: number, } | { "kind": "bank-card", networks: Array<CardNetwork>, } | { "kind": "email" } | { "kind": "cn-mobile-phone" } | { "kind": "regex", pattern: string,
/**
* 分大小写:工具调用审查的规则(开头写了 `(?i)` 的除外)、写明了要分的出站脱敏
* 自定义规则。内容过滤的不分
*/
case_sensitive?: boolean, } | { "kind": "contains", text: string,
/**
* 分大小写:写明了要分的出站脱敏自定义规则。内容过滤的不分
*/
case_sensitive?: boolean, } | { "kind": "codepoints", ranges: Array<string>, } | { "kind": "builtin", check: string, };

/**
* 中位数和样本数。
Expand Down Expand Up @@ -4292,9 +4306,14 @@ name: string,
*/
pattern?: string | null,
/**
* 内容过滤:这条规则怎么认(`contains` / `regex` / `codepoints`)。别的防护没有
* 自定义规则怎么认:内容过滤是 `contains` / `regex` / `codepoints`,出站脱敏是
* `contains` / `regex`。内置规则和工具调用审查没有
*/
matching?: ContentMatch | null,
/**
* 出站脱敏的自定义规则:分大小写
*/
case_sensitive?: boolean,
/**
* 认出它的 core 的版本
*/
Expand Down Expand Up @@ -4739,13 +4758,18 @@ action?: RuleAction | null, };
*/
export type SecurityTestRequest = { sample: string,
/**
* 只试这一条(正在编辑的规则):出站脱敏和工具调用审查是正则,内容过滤按 `match`
* 只试这一条(正在编辑的规则):工具调用审查是正则,出站脱敏和内容过滤按 `match`
*/
pattern?: string | null,
/**
* 内容过滤试 `pattern` 时怎么认:`contains` / `regex` / `codepoints`,不给按 `contains`
* 试 `pattern` 时怎么认:内容过滤是 `contains` / `regex` / `codepoints`,出站脱敏是
* `contains` / `regex`。不给按 `contains`
*/
match?: ContentMatch | null,
/**
* 出站脱敏试 `pattern` 时分不分大小写。不给是不分
*/
case_sensitive?: boolean | null,
/**
* 只试这一条内置规则(停用着的也能试)
*/
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/core.zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@
"security.bad_content_pattern": "匹配内容无法使用:{detail}",
"security.bad_codepoints": "码位写法有误:{detail}",
"security.bad_label": "占位符名称「{label}」不可用:须以大写字母开头,由 1 到 24 个大写字母、数字或下划线组成。",
"security.redact_match_bad": "出站脱敏规则只能按「包含」或「正则」匹配,不支持「{matching}」。",
"security.pattern_empty": "匹配内容为空。",
"security.no_action_of_its_own": "{guard:guard}的规则不单独设处置,命中后的处理由档位决定。",
"control.session_not_found": "未找到会话 {id}。",
Expand Down Expand Up @@ -683,6 +684,7 @@
"config.rule_pattern_bad": "自定义{what:rule_line}规则「{name}」的{what:pattern_of}有误:{detail}",
"config.rule_codepoints_bad": "自定义内容过滤规则「{name}」的码位写法有误:{detail}",
"config.rule_label_bad": "自定义出站脱敏规则「{name}」的占位符名称为「{label}」,须以大写字母开头,由 1 到 24 个大写字母、数字或下划线组成。",
"config.rule_match_bad": "自定义出站脱敏规则「{name}」的匹配方式为「{matching}」,出站脱敏规则只能为 contains 或 regex。",
"config.unknown_rule": "security.{guard} 中的「{rule}」不是内置规则。",
"config.failover_range": "failover.{field} 为 {value},须在 {min} 到 {max} 之间。",
"config.plugin.bad_id": "插件 ID「{plugin}」写法有误:只能使用小写字母、数字和连字符,1 到 {max} 个字符。",
Expand Down
1 change: 1 addition & 0 deletions src/security/HitDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ function RuleText({ e }: { e: SecurityEventView }) {
<>
<Badge variant="outline">{lt.custom}</Badge>
{mode && <span className="text-muted-foreground">· {lt.matching[mode]}</span>}
{s.case_sensitive && <span className="text-muted-foreground">· {lt.caseSensitive}</span>}
</>
)}
</span>
Expand Down
22 changes: 15 additions & 7 deletions src/security/RuleDialog.i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,14 @@ export const ruleDialogText = messages(
regex: "匹配(正则表达式)",
codepoints: "码位",
},
patternHint: {
redact: "匹配到的整段内容按凭据处理。",
inspect_tools: "按工具调用的参数匹配。",
},
toolHint: "按工具调用的参数匹配。",
/** 出站脱敏的自定义规则:怎么找、分不分大小写 */
redactHint: {
contains: (cased: boolean) =>
`在整个请求中按原样查找这段文字,. * 等字符没有特殊含义;${cased ? "区分" : "不区分"}大小写。找到的内容按凭据替换。`,
regex: (cased: boolean) => `按正则表达式匹配,${cased ? "区分" : "不区分"}大小写。匹配到的整段内容按凭据替换。`,
},
caseSensitive: "区分大小写",
contentHint: {
contains: "在用户消息和工具结果中查找这段文字,不区分大小写,首尾空格也参与匹配。",
regex: "在用户消息和工具结果中匹配,不区分大小写。",
Expand Down Expand Up @@ -146,10 +150,14 @@ export const ruleDialogText = messages(
regex: "Match (regular expression)",
codepoints: "Code points",
},
patternHint: {
redact: "Everything the pattern matches is treated as a credential.",
inspect_tools: "Matched against the arguments of each tool call.",
toolHint: "Matched against the arguments of each tool call.",
redactHint: {
contains: (cased: boolean) =>
`Looked for as written anywhere in the request; characters such as . and * have no special meaning. ${cased ? "Case must match" : "Case is ignored"}. What is found is replaced like a credential.`,
regex: (cased: boolean) =>
`A regular expression; ${cased ? "case must match" : "case is ignored"}. Everything it matches is replaced like a credential.`,
},
caseSensitive: "Match case",
contentHint: {
contains:
"Searched for in user messages and tool results, ignoring case. Leading and trailing spaces count.",
Expand Down
Loading
Loading