diff --git a/src/components/pages/CorePage.astro b/src/components/pages/CorePage.astro
index ff4e54a..256dfb0 100644
--- a/src/components/pages/CorePage.astro
+++ b/src/components/pages/CorePage.astro
@@ -143,7 +143,7 @@ const fileLink = "break-all text-[var(--color-brand-1)] transition-colors hover:
{c.does.items[2].body}
- sk-ant-api03-7Hq…[CREDENTIAL_1]
+ sk-ant-api03-7Hq…{"<>"}
{c.does.items[3].title}
{c.does.items[3].body}
diff --git a/src/content/docs-core/en/crate-layers.md b/src/content/docs-core/en/crate-layers.md
index 8773f74..49f463a 100644
--- a/src/content/docs-core/en/crate-layers.md
+++ b/src/content/docs-core/en/crate-layers.md
@@ -15,7 +15,7 @@ No crate depends on a group below its own.
| Group | Crate | Role |
| --- | --- | --- |
| Shared with ThinkWatch Enterprise | `tw-dialect` | Conversion of requests, responses and streams between Anthropic Messages, OpenAI Chat Completions, OpenAI Responses and Gemini; usage parsing |
-| | `tw-guard` | Outbound redaction and restoration, inspection of the tool calls an upstream returns, hidden characters, content filtering and the output length limit |
+| | `tw-guard` | Outbound redaction and restoration, inspection of the tool calls an upstream returns, and the content filter, with the settings, built-in rules, validation, rule listings and trials of all three |
| | `tw-breaker` | The circuit-breaker state machine |
| | `tw-bedrock` | Amazon Bedrock on the wire: SigV4 signing, event-stream framing, addresses and the model catalog |
| Domain logic | `tw-types` | Messages for people: a stable code, its arguments and the English sentence |
@@ -34,7 +34,7 @@ No crate depends on a group below its own.
## Shared with ThinkWatch Enterprise
-ThinkWatch Enterprise depends on the first group and nothing else: format conversion and usage parsing (`tw-dialect`), the guards (`tw-guard`), the circuit breaker (`tw-breaker`) and Amazon Bedrock's wire protocol (`tw-bedrock`). These four depend only on each other, which a test in `tw-dialect` enforces, and CI builds ThinkWatch Enterprise against every change to them. A component that only one product uses lives in that product's repository rather than in Core.
+ThinkWatch Enterprise depends on the first group and nothing else: format conversion and usage parsing (`tw-dialect`), the guards and their rules (`tw-guard`), the circuit breaker (`tw-breaker`) and Amazon Bedrock's wire protocol (`tw-bedrock`). These four depend only on each other, which a test in `tw-dialect` enforces, and CI builds ThinkWatch Enterprise against every change to them. A component that only one product uses lives in that product's repository rather than in Core.
## Used by ThinkWatch Lite
diff --git a/src/content/docs-core/en/overview.md b/src/content/docs-core/en/overview.md
index 059cc6f..b8ae450 100644
--- a/src/content/docs-core/en/overview.md
+++ b/src/content/docs-core/en/overview.md
@@ -15,8 +15,9 @@ Clients such as Claude Code and Codex send their requests to the gateway, and Co
- **Rule-based routing.** Rules match on the model, the gateway key, the input size, the presence of tools or images and other properties of a request, and send it to an upstream or a group, rewrite its parameters or refuse it. When the client and the upstream use different API formats, the request is converted between Anthropic Messages, OpenAI Chat Completions, OpenAI Responses and Gemini.
- **Failover before the first byte.** Until the first byte reaches the client, a failing upstream is replaced by the next one without the client noticing. After that point, the failure is reported. A circuit breaker keeps requests away from an upstream that keeps failing.
- **Cost accounting.** Token usage and cache hits are priced from a public price table, which the control plane refreshes daily, or from a price sheet in the configuration. Each request records its cost and where the price came from. Estimated amounts are marked as such, and usage that cannot be priced is labelled *unknown* rather than given an invented figure.
-- **Outbound redaction.** Credentials in a request are replaced with placeholders before the request leaves, and restored when the model echoes them back.
-- **Tool-call inspection.** Tool calls returned by an upstream are checked against a rule set, and a dangerous call can be cut off mid-stream. With checks for hidden characters, content rules and an output limit, these form five guards, each set to `off`, `observe` or `enforce`.
+- **Outbound redaction.** Credentials and personal information anywhere in a request are replaced with placeholders before the request leaves, and restored when the model echoes them back.
+- **Tool-call inspection.** Tool calls returned by an upstream are checked against a rule set, and a dangerous call can be cut off mid-stream.
+- **Content filter.** The user messages and tool results a client sends are checked by keyword, regular expression or code point, and each rule refuses the request, deletes what it matched or only records it; the built-in rules also catch hidden characters that can carry instructions. Each of the three guards is set to `off`, `observe` or `enforce`, and all start in `observe`.
- **An encrypted control plane.** The desktop app and `twcore` commands reach core over a local socket (a loopback port on Windows) and, when it is enabled, a remote control port. Every control connection starts with a Noise handshake keyed by `listen.control.key`; there are no certificates.
## Further reading
diff --git a/src/content/docs-core/zh-CN/crate-layers.md b/src/content/docs-core/zh-CN/crate-layers.md
index 23c542c..20a9e3a 100644
--- a/src/content/docs-core/zh-CN/crate-layers.md
+++ b/src/content/docs-core/zh-CN/crate-layers.md
@@ -15,7 +15,7 @@ tw-gateway · tw-control ← 数据
| 分组 | crate | 作用 |
| --- | --- | --- |
| 与 ThinkWatch 企业版共用 | `tw-dialect` | 请求、响应与流在 Anthropic Messages、OpenAI Chat Completions、OpenAI Responses 与 Gemini 之间的转换;用量解析 |
-| | `tw-guard` | 出站脱敏与还原、上游返回的工具调用审查、隐藏字符、内容过滤与输出长度限制 |
+| | `tw-guard` | 出站脱敏与还原、上游返回的工具调用审查与内容过滤,以及这三项防护的设置、内置规则、校验、规则列表与测试 |
| | `tw-breaker` | 熔断器状态机 |
| | `tw-bedrock` | Amazon Bedrock 的线上协议:SigV4 签名、事件流拆帧、接口地址与模型目录 |
| 领域逻辑 | `tw-types` | 面向用户的消息:稳定的消息码、参数与英文句子 |
@@ -34,7 +34,7 @@ tw-gateway · tw-control ← 数据
## 与 ThinkWatch 企业版共用
-ThinkWatch 企业版只依赖第一组:格式转换与用量解析(`tw-dialect`)、各项防护(`tw-guard`)、熔断器(`tw-breaker`)与 Amazon Bedrock 的线上协议(`tw-bedrock`)。这四个 crate 只依赖彼此,`tw-dialect` 中的一项测试保证这一点;每次改动它们,CI 都会用 ThinkWatch 企业版编译一遍。只有一个产品使用的组件放在该产品自己的仓库中,不留在 Core。
+ThinkWatch 企业版只依赖第一组:格式转换与用量解析(`tw-dialect`)、各项防护及其规则(`tw-guard`)、熔断器(`tw-breaker`)与 Amazon Bedrock 的线上协议(`tw-bedrock`)。这四个 crate 只依赖彼此,`tw-dialect` 中的一项测试保证这一点;每次改动它们,CI 都会用 ThinkWatch 企业版编译一遍。只有一个产品使用的组件放在该产品自己的仓库中,不留在 Core。
## ThinkWatch Lite 使用的部分
diff --git a/src/content/docs-core/zh-CN/overview.md b/src/content/docs-core/zh-CN/overview.md
index effbce1..cf18ec0 100644
--- a/src/content/docs-core/zh-CN/overview.md
+++ b/src/content/docs-core/zh-CN/overview.md
@@ -15,8 +15,9 @@ Claude Code、Codex 等客户端把请求发往网关后,Core 提供以下功
- **按规则路由。** 规则按模型、网关密钥、输入规模、是否携带工具或图片等请求属性匹配,将请求发往某个上游或策略组、改写其参数,或拒绝请求。客户端与上游的接口格式不同时,请求在 Anthropic Messages、OpenAI Chat Completions、OpenAI Responses 与 Gemini 之间转换。
- **首字节前的故障转移。** 首字节到达客户端之前,出错的上游由下一个上游替换,客户端无从察觉;此后发生的故障如实报告。熔断器使持续出错的上游暂不接收请求。
- **费用核算。** token 用量与缓存命中按公开价目表计价(控制面每天刷新一次),或按配置中的价目表计价;每个请求都记录费用及价格来源。估算的金额另行标注,无法计价的用量标记为「未知」,不会填入虚构的数值。
-- **出站脱敏。** 请求发出之前,其中的凭据替换为占位符;模型回显时再恢复原值。
-- **工具调用审查。** 上游返回的工具调用按规则集审查,高危调用可在流式传输中途截断。它与隐藏字符检查、内容规则和输出长度限制合为五项防护,每项可设为 `off`、`observe` 或 `enforce`。
+- **出站脱敏。** 请求发出之前,其中任何位置的凭据和个人信息替换为占位符;模型回显时再恢复原值。
+- **工具调用审查。** 上游返回的工具调用按规则集审查,高危调用可在流式传输中途截断。
+- **内容过滤。** 客户端发送的用户消息和工具结果按关键词、正则表达式或码位检查,每条规则可拒绝请求、删除命中的内容或仅记录;内置规则也能查出可夹带指令的隐藏字符。三项防护各可设为 `off`、`observe` 或 `enforce`,出厂均为 `observe`。
- **加密的控制面。** 桌面应用与 `twcore` 命令通过本地 socket(Windows 上为回环端口)连接 core,开启后也可经远程控制端口连接。每条控制连接都先以 `listen.control.key` 完成 Noise 握手,不使用证书。
## 后续阅读
diff --git a/src/content/docs-lite/en/features.md b/src/content/docs-lite/en/features.md
index bb4dff6..a1d3fa7 100644
--- a/src/content/docs-lite/en/features.md
+++ b/src/content/docs-lite/en/features.md
@@ -14,7 +14,7 @@ Until the first request has gone to an upstream, the Overview page shows a get-s
## Traffic and sessions
-The Traffic page lists requests as they arrive: status, key, model, upstream, time to first token and total time (with the generation speed on hover), tokens and cost, with marks for a converted API format, redacted keys and a blocked or suspicious tool call. The list can be searched (path, key, upstream, model and error message), filtered by key, upstream and model, or narrowed to failed or unpriced requests. It holds the latest 2,000 requests, and a search or filter also runs over the stored history, further back on request. With content search on, it also covers what each request newly sent (the last user turn, tool results included) and its answer (tool calls included), for as long as payloads are kept; a match shows its excerpt under the request. The Sessions view groups the requests of one conversation into turns, with the input tokens and the cost of each turn. A session's Conversation tab replays it turn by turn: the messages each turn added and its answer, with text, folded thinking, tool calls beside their results, and images by type and size. Changes to the system prompt and restarts after compaction are marked, and a turn whose bodies are past retention, were too large to keep whole or cannot be read says so. Each turn opens its request.
+The Traffic page lists requests as they arrive: status, key, model, upstream, time to first token and total time (with the generation speed on hover), tokens and cost, with marks for a converted API format, redacted keys, text deleted by the content filter and a blocked or suspicious tool call. The list can be searched (path, key, upstream, model and error message), filtered by key, upstream and model, or narrowed to failed or unpriced requests. It holds the latest 2,000 requests, and a search or filter also runs over the stored history, further back on request. With content search on, it also covers what each request newly sent (the last user turn, tool results included) and its answer (tool calls included), for as long as payloads are kept; a match shows its excerpt under the request. The Sessions view groups the requests of one conversation into turns, with the input tokens and the cost of each turn. A session's Conversation tab replays it turn by turn: the messages each turn added and its answer, with text, folded thinking, tool calls beside their results, and images by type and size. Changes to the system prompt and restarts after compaction are marked, and a turn whose bodies are past retention, were too large to keep whole or cannot be read says so. Each turn opens its request.
A request opens into its timeline, its routing (the rule it matched, the group it went through and each attempt with its status and duration), the request and response bodies, and its usage and cost. A request from DeepSeek Harness also shows the size of the session log it carried, the whole conversation the client attaches to every request; the gateway removes it before a request goes to an upstream other than DeepSeek. A finished request can be sent again, unchanged, to another upstream after an estimate of its cost, and the two responses are shown side by side.
@@ -45,7 +45,7 @@ A relay or vendor can hand out an import link, `thinkwatch://import?…` or its
## Routing and failover
-Each key follows a route, and keys without one follow the default route. A route is a list of rules evaluated in order. A rule matches on the model, the key, the client's API format, input tokens, `max_tokens`, the number of tools, images, extended thinking, streaming, prompt caching or the kind of auxiliary request; it then forwards the request to an upstream or a group, or refuses it, and can rewrite the model, `max_tokens` or extended thinking. A group puts several upstreams behind one name and decides the order in which they are tried: as listed, manually selected, in turn, lowest latency first or lowest cost first. When an attempt fails, the request moves on to the next upstream, and by default a session stays on one upstream so that its prompt cache keeps hitting. A map at the top of the page traces every key through its route and groups to the upstreams.
+Each key follows a route, and keys without one follow the default route. A route is a list of rules evaluated in order. A rule matches on the model, the key, the client's API format, input tokens, `max_tokens`, the number of tools, images, extended thinking, streaming, prompt caching or the kind of auxiliary request; it then forwards the request to an upstream or a group, or refuses it, and can rewrite the model, `max_tokens` or extended thinking. Setting `max_tokens` caps the length of answers: the upstream stops at that point by itself, without an error. A group puts several upstreams behind one name and decides the order in which they are tried: as listed, manually selected, in turn, lowest latency first or lowest cost first. When an attempt fails, the request moves on to the next upstream, and by default a session stays on one upstream so that its prompt cache keeps hitting. A map at the top of the page traces every key through its route and groups to the upstreams.
Auxiliary requests that clients send on their own (health checks, warm-ups, titles, topic detection and input suggestions) can be answered locally at no cost, passed through, or routed by the rules.
@@ -53,15 +53,13 @@ Every request records the rule it matched, the group it went through and each at
## Security
-The Security page holds five protections. They apply to every upstream and every key alike, and each runs in one of three modes: Off, Observe (detect and record, change nothing) or Enforce. The output limit starts Off and the other four start in Observe, so out of the box no request is changed or blocked.
+The Security page holds three protections: outbound redaction, tool-call inspection and the content filter. They apply to every upstream and every key alike, and each runs in one of three modes: Off, Observe (detect and record, change nothing) and a third mode named for what it does: Replace for outbound redaction, Cut off for tool-call inspection and Enforce for the content filter. All three start in Observe, so out of the box no request is changed or refused.
-- **Outbound redaction** looks for credentials in a request before it leaves: API keys and tokens for Anthropic, OpenAI, GitHub, Slack, AWS, Google, GitLab, Stripe, npm, DigitalOcean and SendGrid, private keys, JWTs and passwords in connection strings, as well as Chinese resident ID numbers and bank card numbers, which count only when their structure and check digit are valid. In Enforce mode they are replaced with placeholders and restored where the response repeats them. Rules for internal IP addresses and internal domains are included and start off.
-- **Tool-call inspection** checks the tool calls a model returns for commands that download or decode code and run it, send out environment variables or credential files, read private keys or cloud credentials, or install startup items and scheduled jobs. In Enforce mode such a call cuts the response off, so the client never receives a complete call to run. Deleting the home or root directory and making files world-writable are only recorded by default.
-- **Hidden characters** looks for Unicode tag characters and bidirectional control characters in what the client sends, tool results included, and in Enforce mode refuses the request.
-- **Content filter** matches keywords or regular expressions against the messages the client sends, tool results included, and in Enforce mode refuses a request that matches a blocking rule. Of the built-in rules, the three against explicit "ignore previous instructions" phrasing are on by default; rules for jailbreaks, persona manipulation, prompt extraction and their Chinese counterparts can be switched on.
-- **Output limit** stops an answer that grows past a set number of characters, 100,000 by default: a streamed answer is cut off at that point and a non-streamed one is replaced with an error. Reasoning and tool-call arguments do not count towards the limit.
+- **Outbound redaction** searches the whole request before it leaves, including the system prompt, earlier turns and tool calls, for credentials and personal information: API keys and tokens for Anthropic, OpenAI, GitHub, Slack, AWS, Google, GitLab, Stripe, npm, DigitalOcean and SendGrid, private keys, JWTs and passwords in connection strings, as well as Chinese resident ID numbers and bank card numbers, which count only when their structure and check digit are valid. In Replace mode they are replaced with placeholders such as `<>` and restored where the response repeats them. Rules for email addresses, Chinese mainland mobile numbers, internal IP addresses and internal domains are included and start off, and a custom rule can name its own placeholder: `PROJECT` gives `<>`.
+- **Tool-call inspection** checks the tool calls a model returns for commands that download or decode code and run it, send out environment variables or credential files, send a credential to a host that is neither local nor the credential's own provider, read private keys or cloud credentials, or install startup items and scheduled jobs. In Cut off mode such a call cuts the response off, so the client never receives a complete call to run. Deleting the home or root directory, making files world-writable and uploading a local file to an outside host are only recorded by default.
+- **Content filter** checks the user messages and tool results in each request, context compaction included. A rule matches a keyword, a regular expression or code points such as `U+E0000–U+E007F`, and in Enforce mode it refuses the request, deletes the matched text and sends the rest, or only records. Built-in rules delete Unicode tag characters and bidirectional controls, which can hide instructions from people but not from a model, and refuse explicit "ignore previous instructions" phrasing. Rules for zero-width and private-use characters, which emoji, Persian text and icon fonts also use, and for jailbreaks, persona manipulation, prompt extraction and their Chinese counterparts start off and can be switched on.
-The page lists every rule. Built-in rules can be switched on or off one at a time, and those for tool calls and content can be set to act or only record in Enforce mode. Custom rules are regular expressions, or keywords for the content filter, and any rule can be tried on a sample text first. Everything the protections find is kept in the log on the first tab, together with the request it came from.
+The page lists every rule, the built-in ones grouped by kind. Built-in rules can be switched on or off one at a time; a built-in tool-call rule can be set to cut off or only record, and a built-in content rule to refuse, delete or only record. Custom rules are regular expressions, or for the content filter also keywords or code points. Any rule can be tried on a sample text first; for redaction and the content filter, the test also shows the text as it would be sent. Everything the protections find is kept in the log on the first tab, together with the request it came from; when hidden characters spell out text, the log shows that text.
## MCP
diff --git a/src/content/docs-lite/en/overview.md b/src/content/docs-lite/en/overview.md
index 59d3ec1..a3499be 100644
--- a/src/content/docs-lite/en/overview.md
+++ b/src/content/docs-lite/en/overview.md
@@ -7,7 +7,7 @@ ThinkWatch Lite is a local gateway for Claude Code, Codex and other AI clients,
## Highlights
- **Connect once, switch freely.** Twelve clients are pointed at the gateway in one step, with the change previewed and the original backed up; Cursor, Continue and Antigravity CLI come with instructions.
-- **Protection against relays.** A relay sees every request and can rewrite every answer. Outbound redaction can replace credentials, ID numbers and bank card numbers before a request leaves, and tool-call inspection can cut off an answer that carries a dangerous tool call, such as download-and-run or sending out credential files, before the client runs it. Hidden-character detection, a content filter and an output limit complete the five protections, each in Off, Observe or Enforce.
+- **Protection against relays.** A relay sees every request and can rewrite every answer. Outbound redaction can replace credentials, ID numbers and bank card numbers before a request leaves, and tool-call inspection can cut off an answer that carries a dangerous tool call, such as download-and-run or sending out credential files, before the client runs it. The content filter can delete hidden characters that smuggle instructions into what the client sends, and all three protections start out only recording.
- **MCP servers, skills and hooks, scanned.** The MCP servers of thirteen clients side by side, and a scan of client configuration for hidden characters, prompt injection, dangerous commands and overly broad permissions.
- **Plugins.** Short JavaScript plugins adjust requests and answers, such as asking for answers in a chosen language or converting file paths between WSL and Windows. They run in a sandbox, see placeholders instead of keys, and every change they make is recorded.
- **Every request traceable.** The matched rule, each attempt, any format conversion and the cost, with replay against another upstream; the whole history can be searched, including the text of requests and answers.
@@ -25,7 +25,7 @@ ThinkWatch Lite is a local gateway for Claude Code, Codex and other AI clients,
| Keys | The gateway keys clients connect with, each with its route and limits |
| Upstreams | Upstreams, outbound proxies and price sheets |
| Routing | Routes, rules and groups, auxiliary requests, and the dry run |
-| Security | The security log and the five protections with their rules |
+| Security | The security log and the three protections with their rules |
| MCP | MCP servers, skills and hooks in each client, and the configuration scan |
| Plugins | JavaScript plugins that change requests and answers, with their permissions, trial runs and logs |
| Settings | Connection, language, appearance, menu bar, notifications, listening, retention, updates and uninstall |
diff --git a/src/content/docs-lite/en/plugins.md b/src/content/docs-lite/en/plugins.md
index fffa344..2e04cc1 100644
--- a/src/content/docs-lite/en/plugins.md
+++ b/src/content/docs-lite/en/plugins.md
@@ -25,14 +25,14 @@ A request is routed first, on what the client sent: the routing rules, a rule's
1. Keys in the request are replaced with placeholders.
2. The request hooks in scope for this attempt run in list order, starting from the request as the client sent it. The placeholders are restored after them.
-3. If a plugin changed the request, the content filter and the hidden-character check look at it again and report only what the plugins added. A block refuses the whole request; it is not tried on another upstream.
+3. If a plugin changed the request, the content filter checks it again, hidden-character rules included, and reports only what the plugins added. A block refuses the whole request; it is not tried on another upstream.
4. The request is converted to the upstream's format if needed, outbound redaction applies, and it is sent.
When an attempt fails and the request moves to another upstream, the hooks run again from the request as the client sent it, so changes made for one upstream never reach another. A retry to the same upstream, such as the one after a sign-in token is refreshed, reuses what the hooks produced.
A plugin that changes the model renames only what is sent to the upstream of this attempt, like a routing rule's rename, and replaces any name a rule set. The request is not routed again and the upstream's model list is not checked again, but the models the key may use still apply: a model outside them refuses the request.
-On the answer side, the hooks run after the answer is converted to the client's format and before the tool-call inspection and the output limit.
+On the answer side, the hooks run after the answer is converted to the client's format and before the tool-call inspection.
## Adding and editing a plugin
@@ -201,7 +201,7 @@ type InputsView = {
- Messages and parts cannot be added, removed or reordered, because the answer comes back input by input.
- `params` holds the model, and for completions also `max_tokens`, `temperature`, `top_p` and `stop`. Other fields, such as `suffix` and `dimensions`, are not shown and stay as they are.
-Everything else works as for conversations: the placeholders, a run for each attempt after routing, a changed model renaming what is sent to this upstream, recording and trial runs. A changed request is checked again by the content filter and the hidden-character check, on the text of its inputs. Answer hooks do not run on these requests: an embeddings answer carries no text, and a legacy completions answer is passed through as it is.
+Everything else works as for conversations: the placeholders, a run for each attempt after routing, a changed model renaming what is sent to this upstream, recording and trial runs. A changed request is checked again by the content filter, on the text of its inputs. Answer hooks do not run on these requests: an embeddings answer carries no text, and a legacy completions answer is passed through as it is.
`messages` and `params` apply to all three kinds; `system`, `tools`, `reply.text` and `reply.tool_calls` apply to conversations only. A plugin does not load when `requests` is empty, names an unknown kind or names one twice, when a kind it declares is reached by none of its permissions (embeddings and completions need `messages` or `params`), or when it holds a permission that applies to none of its kinds, such as `system` without `conversation`.
@@ -265,7 +265,7 @@ Across the gateway, at most 32 answer-hook instances run at the same time. Each
- **A sandbox with nothing in it.** Plugins run in QuickJS compiled to WebAssembly and executed by Wasmtime inside core. Plugin code never runs in the app's window and never runs as native code. The sandbox has no network, files, environment variables or processes; even a flaw in the JavaScript engine reaches only the sandbox's own memory, not the keys and tokens in core's memory.
- **Nothing is kept.** Every request-hook call runs in a new instance. For each answer, a plugin gets one instance, shared by its hooks for that answer and discarded when the answer ends. Plugins share nothing with each other.
- **Placeholders instead of keys.** Keys that the outbound redaction rules recognize are replaced with placeholders before a plugin sees them and restored after it, on the request and on the answer, whatever mode the protection is in. The answer side matters as much as the request: answers become part of the conversation and are sent upstream again with the next request, so a plugin that could see a real key could hide it, encoded, in an answer.
-- **The protections still apply.** Request hooks run after routing, and a request they change is checked again by the content filter and the hidden-character check before outbound redaction; answer hooks run before the tool-call inspection and the output limit. Whatever a plugin writes is checked like anything else, and plugins cannot change where a request is routed or switch to a model the key may not use.
+- **The protections still apply.** Request hooks run after routing, and a request they change is checked again by the content filter before outbound redaction; answer hooks run before the tool-call inspection. Whatever a plugin writes is checked like anything else, and plugins cannot change where a request is routed or switch to a model the key may not use.
- **Approved code only.** A plugin runs only while its file matches the approved SHA-256 hash, and a save in the app updates the file and the hash together. Installing a plugin that may change tool calls, turning it on, changing its code and approving a change to its file are confirmed in a system dialog outside the page, as described in [Confirmation in a system dialog](#confirmation-in-a-system-dialog).
- **Every change is visible.** Each run is recorded on the request, a changed request is stored as it was sent to the upstream that answered (with keys replaced, like every stored request), and requests changed by plugins are marked on the Traffic page.
- **Bounded.** Every call has limits on CPU time, memory, output and log volume, and the number of answer instances alive at once is capped. Plugins run in a separate thread pool, so a slow plugin does not hold up the gateway's own work.
diff --git a/src/content/docs-lite/zh-CN/features.md b/src/content/docs-lite/zh-CN/features.md
index 65a2d85..e6b1c67 100644
--- a/src/content/docs-lite/zh-CN/features.md
+++ b/src/content/docs-lite/zh-CN/features.md
@@ -14,7 +14,7 @@
## 流量与会话
-流量页实时列出请求:状态、密钥、模型、上游、首 token 时间与总耗时(悬停时显示生成速度)、token 和费用,并标出格式转换、被脱敏的密钥,以及被拦截或可疑的工具调用。列表可以搜索(路径、密钥、上游、模型与错误信息),可以按密钥、上游和模型筛选,或只看失败、无法计价的请求。列表装有最近 2,000 条请求,搜索与筛选同时在全部请求记录中进行,可以继续向更早的记录搜索。打开「搜索内容」后,还会搜索每个请求新发送的内容(最后一轮用户消息,含工具结果)及其回答(含工具调用),范围以报文仍保留的请求为限;命中的片段显示在对应请求的下方。「会话」视图把同一段对话的请求归为若干轮次,给出每一轮的输入 token 与费用。会话的「对话」页按轮还原整段对话:每一轮新加入的消息和回答,包括文字、折叠的思考、工具调用及其结果,图片只显示类型和大小。系统提示词的变化和压缩上下文之后的重新开始会标出;报文已过保留期、过大未能完整保存或无法读取的轮次会注明原因。每一轮都可以打开对应的请求。
+流量页实时列出请求:状态、密钥、模型、上游、首 token 时间与总耗时(悬停时显示生成速度)、token 和费用,并标出格式转换、被脱敏的密钥、被内容过滤删除的文字,以及被拦截或可疑的工具调用。列表可以搜索(路径、密钥、上游、模型与错误信息),可以按密钥、上游和模型筛选,或只看失败、无法计价的请求。列表装有最近 2,000 条请求,搜索与筛选同时在全部请求记录中进行,可以继续向更早的记录搜索。打开「搜索内容」后,还会搜索每个请求新发送的内容(最后一轮用户消息,含工具结果)及其回答(含工具调用),范围以报文仍保留的请求为限;命中的片段显示在对应请求的下方。「会话」视图把同一段对话的请求归为若干轮次,给出每一轮的输入 token 与费用。会话的「对话」页按轮还原整段对话:每一轮新加入的消息和回答,包括文字、折叠的思考、工具调用及其结果,图片只显示类型和大小。系统提示词的变化和压缩上下文之后的重新开始会标出;报文已过保留期、过大未能完整保存或无法读取的轮次会注明原因。每一轮都可以打开对应的请求。
打开一个请求可以查看时间线、路由(命中的规则、经过的策略组,以及每一次尝试的状态与耗时)、请求与响应正文、用量与费用。DeepSeek Harness 发出的请求还会显示所带会话日志的大小:这是客户端随每个请求附带的整段对话记录,发往 DeepSeek 以外的上游之前由网关去除。已结束的请求可以在预估费用后原样发送到另一个上游,两次的响应并排对照。
@@ -45,7 +45,7 @@ API 密钥和请求头的值可以写成 `${变量名}`,读取系统环境变
## 路由与故障转移
-每把密钥使用一条路由,未指定的使用默认路由。路由由按顺序匹配的规则组成。规则的条件包括模型、密钥、客户端的 API 格式、输入 token 数、`max_tokens`、工具数量、图片、扩展思考、流式、提示缓存以及辅助请求的类型;命中后把请求交给某个上游或策略组,或拒绝请求,也可以改写模型、`max_tokens` 或扩展思考。策略组把多个上游放在同一个名字下,并决定尝试的先后:按顺序、手动选择、轮询、延迟最低优先或费用最低优先。一次尝试失败时,请求转到下一个上游;同一会话默认保持在同一个上游上,以便提示缓存持续命中。页面顶部的路由图显示每把密钥经过的路由、策略组和上游。
+每把密钥使用一条路由,未指定的使用默认路由。路由由按顺序匹配的规则组成。规则的条件包括模型、密钥、客户端的 API 格式、输入 token 数、`max_tokens`、工具数量、图片、扩展思考、流式、提示缓存以及辅助请求的类型;命中后把请求交给某个上游或策略组,或拒绝请求,也可以改写模型、`max_tokens` 或扩展思考。设置 `max_tokens` 可以限制回答的长度:上游到达上限时自行停止,不会报错。策略组把多个上游放在同一个名字下,并决定尝试的先后:按顺序、手动选择、轮询、延迟最低优先或费用最低优先。一次尝试失败时,请求转到下一个上游;同一会话默认保持在同一个上游上,以便提示缓存持续命中。页面顶部的路由图显示每把密钥经过的路由、策略组和上游。
客户端自行发出的辅助请求(连通性检查、预热、生成标题、话题识别、输入建议)可以由网关在本地应答而不产生费用,也可以直接转发,或交给路由规则处理。
@@ -53,15 +53,13 @@ API 密钥和请求头的值可以写成 `${变量名}`,读取系统环境变
## 安全
-安全页有五项防护,对所有上游和所有密钥统一生效,各有「关闭」「观察」「拦截」三档,其中「观察」只检测和记录,不做任何改动。输出长度出厂为「关闭」,其余四项出厂为「观察」,因此默认不会改动或拦截任何请求。
+安全页有三项防护:出站脱敏、工具调用审查和内容过滤,对所有上游和所有密钥统一生效。每项都有三档:「关闭」「观察」,以及按作用命名的第三档,出站脱敏为「替换」,工具调用审查为「切断」,内容过滤为「处置」;其中「观察」只检测和记录,不做任何改动。三项出厂均为「观察」,因此默认不会改动或拒绝任何请求。
-- **出站脱敏**:请求发出之前查找其中的凭据,包括 Anthropic、OpenAI、GitHub、Slack、AWS、Google、GitLab、Stripe、npm、DigitalOcean、SendGrid 的 API 密钥与令牌,以及私钥、JWT 和连接串中的口令,还有身份证号与银行卡号(号码结构与校验位都正确才算)。「拦截」档下把它们替换为占位符,响应中回显时再还原。内网 IP 地址和内网域名两条规则出厂为停用,可以按需启用。
-- **工具调用审查**:检查模型返回的工具调用中是否含有下载或解码后执行代码、外发环境变量或凭据文件、读取私钥或云服务凭据、写入启动项或定时任务等命令。「拦截」档下命中即切断响应,客户端收不到一个完整、可执行的调用。删除主目录或根目录、设置全员可写权限两条规则出厂只记录。
-- **隐藏字符**:检查客户端发送的内容(含工具结果)中的 Unicode 标签字符和双向控制符,「拦截」档下拒绝发出请求。
-- **内容过滤**:用关键词或正则表达式匹配客户端发送的消息(含工具结果),「拦截」档下拒绝命中拒绝类规则的请求。内置规则中,出厂只启用三条明确要求「忽略先前指令」的规则;越狱、身份操纵、套取提示词等规则及其中文版本可以按需启用。
-- **输出长度**:回答超过设定的字符数(默认 100,000)时,流式回答在超出处切断,非流式回答整份替换为错误。思考内容和工具调用的参数不计入。
+- **出站脱敏**:请求发出之前查找整个请求(含系统提示、之前的对话和工具调用)中的凭据和个人信息,包括 Anthropic、OpenAI、GitHub、Slack、AWS、Google、GitLab、Stripe、npm、DigitalOcean、SendGrid 的 API 密钥与令牌,以及私钥、JWT 和连接串中的口令,还有身份证号与银行卡号(号码结构与校验位都正确才算)。「替换」档下把它们替换为 `<>` 这样的占位符,响应中回显时再还原。邮箱地址、中国大陆手机号、内网 IP 地址和内网域名四条规则出厂为停用,可以按需启用;自定义规则可以指定占位符名称,如 `PROJECT` 替换为 `<>`。
+- **工具调用审查**:检查模型返回的工具调用中是否含有下载或解码后执行代码、外发环境变量或凭据文件、把凭据发往本机和其服务商以外的主机、读取私钥或云服务凭据、写入启动项或定时任务等命令。「切断」档下命中即切断响应,客户端收不到一个完整、可执行的调用。删除主目录或根目录、设置全员可写权限、把本地文件上传到外部主机三条规则出厂只记录。
+- **内容过滤**:检查每个请求中的用户消息和工具结果,压缩上下文的请求也在其内。规则按关键词、正则表达式或码位(如 `U+E0000–U+E007F`)匹配,「处置」档下按规则拒绝请求、删除命中的文字后发出,或仅记录。内置规则删除 Unicode 标签字符和双向控制符(人看不见、模型读得到,可用于隐藏指令),拒绝明确要求「忽略先前指令」的请求。零宽字符、私用区字符两条规则(表情符号、波斯文和图标字体也会用到这些字符),以及越狱、身份操纵、套取提示词等规则及其中文版本出厂为停用,可以按需启用。
-安全页列出全部规则:内置规则可以逐条启用或停用,工具调用审查和内容过滤的内置规则还可以设定在「拦截」档下执行处置还是仅记录;自定义规则为正则表达式,内容过滤也可以使用关键词。任何规则都可以先用一段文本测试。各项防护检出的内容都记入第一个标签页的日志,并注明所属的请求。
+安全页列出全部规则,内置规则按类别分组。内置规则可以逐条启用或停用;工具调用审查的内置规则可以设为切断或仅记录,内容过滤的内置规则可以设为拒绝、删除或仅记录。自定义规则为正则表达式,内容过滤也可以使用关键词或码位。任何规则都可以先用一段文本测试,出站脱敏和内容过滤的测试还会显示发出时的文本。各项防护检出的内容都记入第一个标签页的日志,并注明所属的请求;隐藏字符拼出文字时,日志中一并显示这段文字。
## MCP
diff --git a/src/content/docs-lite/zh-CN/overview.md b/src/content/docs-lite/zh-CN/overview.md
index 49fa2fd..a05ce62 100644
--- a/src/content/docs-lite/zh-CN/overview.md
+++ b/src/content/docs-lite/zh-CN/overview.md
@@ -7,7 +7,7 @@ ThinkWatch Lite 是 Claude Code、Codex 等 AI 客户端的本地网关,支持
## 要点
- **一次接入,随时切换。** 十二款客户端可一键指向网关,写入前预览改动并备份原文件;Cursor、Continue 与 Antigravity CLI 提供配置说明。
-- **防范中转站。** 中转站能看到每个请求,也能改写每一次回答。出站脱敏可在请求发出前替换其中的凭据、身份证号与银行卡号;回答中出现下载即执行、外发凭据文件之类的危险工具调用时,工具调用审查可在客户端执行前切断回答。另有隐藏字符检测、内容过滤与输出长度限制,共五项防护,每项可设为关闭、观察或拦截。
+- **防范中转站。** 中转站能看到每个请求,也能改写每一次回答。出站脱敏可在请求发出前替换其中的凭据、身份证号与银行卡号;回答中出现下载即执行、外发凭据文件之类的危险工具调用时,工具调用审查可在客户端执行前切断回答。内容过滤可删除客户端发送内容中夹带指令的隐藏字符;三项防护出厂均只记录。
- **扫描 MCP、技能与钩子。** 十三款客户端的 MCP 服务器并列显示,并扫描客户端配置中的隐藏字符、提示注入、危险命令与过宽权限。
- **插件。** 用简短的 JavaScript 插件调整请求与回答,例如要求用指定的语言回答、在 WSL 与 Windows 之间转换路径。插件在沙箱中运行,只看到占位符而看不到密钥,每一处改动都有记录。
- **每个请求都可追溯。** 命中的规则、每一次尝试、格式转换与费用都有记录,也可以重放到另一个上游对比;全部请求记录都可以搜索,包括请求与回答的内容。
@@ -25,7 +25,7 @@ ThinkWatch Lite 是 Claude Code、Codex 等 AI 客户端的本地网关,支持
| 密钥 | 客户端连接网关所用的密钥,及其路由与限制 |
| 上游 | 上游、出站代理与价目表 |
| 路由 | 路由、规则与策略组,辅助请求,以及试算 |
-| 安全 | 安全日志,以及五项防护及其规则 |
+| 安全 | 安全日志,以及三项防护及其规则 |
| MCP | 各客户端的 MCP 服务器、技能与钩子,以及配置扫描 |
| 插件 | 改写请求与回答的 JavaScript 插件,及其权限、试运行与日志 |
| 设置 | 连接、语言、外观、菜单栏、提醒、网关监听、日志保留、更新与卸载 |
diff --git a/src/content/docs-lite/zh-CN/plugins.md b/src/content/docs-lite/zh-CN/plugins.md
index 1220ee9..1337f19 100644
--- a/src/content/docs-lite/zh-CN/plugins.md
+++ b/src/content/docs-lite/zh-CN/plugins.md
@@ -25,14 +25,14 @@ core 按客户端自己的格式写回改动,只动改过的条目,缓存标
1. 把请求里的密钥替换为占位符;
2. 从客户端发来的原始请求开始,按列表顺序运行适用范围覆盖这一次发送的请求钩子,运行之后换回占位符;
-3. 插件改动了请求时,内容过滤和隐藏字符检测再检查一遍,只报告插件加入的内容;拦截时整个请求被拒绝,不会转到别的上游;
+3. 插件改动了请求时,内容过滤(包括隐藏字符规则)再检查一遍,只报告插件加入的内容;拦截时整个请求被拒绝,不会转到别的上游;
4. 需要时转换为上游的格式,经过出站脱敏后发出。
一次发送失败、请求转到另一个上游时,请求钩子从客户端的原始请求重新运行,为一个上游做的改动不会带到另一个上游。向同一个上游重发(例如登录令牌刷新后的重试)时,沿用请求钩子已有的结果。
插件修改模型时,只改变这一次发往上游的模型名,效果与路由规则改名相同,并取代规则改过的名字。请求不会重新路由,也不再核对上游的模型列表,但密钥的可用模型依然有效:改成密钥不可用的模型时,请求被拒绝。
-回答一侧,回答钩子在回答转换为客户端的格式之后、工具调用审查和输出长度限制之前运行。
+回答一侧,回答钩子在回答转换为客户端的格式之后、工具调用审查之前运行。
## 添加与编辑插件
@@ -201,7 +201,7 @@ type InputsView = {
- 消息和片段不能新增、删除或调换顺序,因为回答按输入逐项对应。
- `params` 中有模型,补全另有 `max_tokens`、`temperature`、`top_p` 与 `stop`。`suffix`、`dimensions` 等其他字段不提供给插件,保持原样。
-其余与对话相同:占位符、路由之后按每一次发送运行、修改模型只改变这一次发往上游的模型名、运行记录与试运行。改动过的请求按各项输入的文字,再经过一次内容过滤和隐藏字符检测。这两种请求不运行回答钩子:嵌入的回答不含文字,旧版补全的回答原样转发。
+其余与对话相同:占位符、路由之后按每一次发送运行、修改模型只改变这一次发往上游的模型名、运行记录与试运行。改动过的请求按各项输入的文字,再经过一次内容过滤。这两种请求不运行回答钩子:嵌入的回答不含文字,旧版补全的回答原样转发。
`messages` 与 `params` 适用于全部三种请求;`system`、`tools`、`reply.text` 与 `reply.tool_calls` 只适用于对话。出现以下情况时插件无法加载:`requests` 为空、含有未知的种类或同一种类写了两次;声明的某种请求没有任何一项权限用得上(嵌入和旧版补全需要 `messages` 或 `params`);或者某项权限不适用于声明的任何一种请求,例如申请了 `system` 却没有声明 `conversation`。
@@ -265,7 +265,7 @@ type Ctx = {
- **沙箱里什么都没有。** 插件在 QuickJS 中运行,QuickJS 编译成 WebAssembly,由 core 内部的 Wasmtime 执行。插件代码从不在应用窗口中运行,也从不以原生代码运行。沙箱里没有网络、文件、环境变量和进程;即使 JavaScript 引擎本身有漏洞,也只能破坏沙箱自己的内存,碰不到 core 进程中的密钥与令牌。
- **不留任何数据。** 每次调用请求钩子都使用新实例。每个回答中,一个插件使用一个实例,由它在这个回答上的各个钩子共用,回答结束即丢弃。插件之间不共享任何东西。
- **只看到占位符。** 出站脱敏规则能识别的密钥,在交给插件之前替换为占位符,插件处理之后再换回;请求与回答两个方向都是如此,与这项防护处于哪一档无关。回答一侧同样重要:回答会进入对话历史,随下一次请求再次发往上游;插件如果看得到真实的密钥,就能把它编码后藏进回答里带出去。
-- **防护照常生效。** 请求钩子在路由之后运行,被它改动的请求在出站脱敏之前再经过内容过滤和隐藏字符检测;回答钩子在工具调用审查和输出长度限制之前运行。插件写入的内容和其他内容一样受检,插件既改变不了请求的路由,也不能换用密钥不可用的模型。
+- **防护照常生效。** 请求钩子在路由之后运行,被它改动的请求在出站脱敏之前再经过内容过滤;回答钩子在工具调用审查之前运行。插件写入的内容和其他内容一样受检,插件既改变不了请求的路由,也不能换用密钥不可用的模型。
- **只运行确认过的代码。** 插件文件与确认过的 SHA-256 哈希一致才会运行,在应用中保存时,文件与哈希一同更新。能改动工具调用的插件,安装、打开、更改代码和确认文件变更,都要在页面之外的系统对话框中确认,见[在系统对话框中确认](#在系统对话框中确认)。
- **改动都有记录。** 每次运行都记录在请求上;被改动的请求另存一份发往作答上游的版本,和所有存下的请求一样替换掉了密钥;「流量」页为被插件改动的请求加上标记。
- **用量有上限。** 每次调用都有 CPU 时间、内存、输出和日志量的上限,同时存在的回答实例也有数量上限。插件在单独的线程池中运行,运行缓慢的插件不会拖住网关本身的工作。
diff --git a/src/content/docs/_meta.ts b/src/content/docs/_meta.ts
index 0b7d647..243acc2 100644
--- a/src/content/docs/_meta.ts
+++ b/src/content/docs/_meta.ts
@@ -152,8 +152,8 @@ export const products: Product[] = [
locales: both,
group: "getStarted",
summary: {
- en: "Page by page: usage and cost, traffic, client setup, keys, upstreams, routing, the five protections, MCP, settings, the menu bar and notifications.",
- "zh-CN": "逐页说明:用量与费用、流量、客户端接管、密钥、上游、路由、五项防护、MCP、设置、菜单栏与通知。",
+ en: "Page by page: usage and cost, traffic, client setup, keys, upstreams, routing, the three protections, MCP, settings, the menu bar and notifications.",
+ "zh-CN": "逐页说明:用量与费用、流量、客户端接管、密钥、上游、路由、三项防护、MCP、设置、菜单栏与通知。",
},
},
{
diff --git a/src/content/docs/en/api-reference.md b/src/content/docs/en/api-reference.md
index 84d00ad..26a4e37 100644
--- a/src/content/docs/en/api-reference.md
+++ b/src/content/docs/en/api-reference.md
@@ -2034,8 +2034,9 @@ Retrieve all settings grouped by category.
"security": {
"signature_drift_seconds": 300,
"nonce_ttl_seconds": 300,
- "content_filter_patterns": [],
- "pii_patterns": []
+ "redact": {},
+ "inspect_tools": {},
+ "content": {}
},
"budget": {
"budget_warning_threshold": 0.8,
diff --git a/src/content/docs/en/architecture.md b/src/content/docs/en/architecture.md
index a0a20de..545cb35 100644
--- a/src/content/docs/en/architecture.md
+++ b/src/content/docs/en/architecture.md
@@ -324,7 +324,7 @@ Authentication and authorization library. Contains:
Shared infrastructure used by all other crates. Contains:
- **`config.rs`** -- `AppConfig` struct loaded from environment variables.
-- **`dynamic_config.rs`** -- `DynamicConfig` system that loads settings from the `system_settings` database table. Supports multi-instance sync via Redis Pub/Sub and in-memory caching. Covers JWT TTLs, cache TTL, content filter patterns, PII patterns, budget thresholds, API key policies, and data retention settings.
+- **`dynamic_config.rs`** -- `DynamicConfig` system that loads settings from the `system_settings` database table. Supports multi-instance sync via Redis Pub/Sub and in-memory caching. Covers JWT TTLs, cache TTL, the policies of the request guards (outbound redaction, tool-call inspection and the content filter), budget thresholds, API key policies, and data retention settings.
- **`db.rs`** -- PostgreSQL connection pool setup using `sqlx`.
- **`models/`** -- Database model structs (one per domain entity): `user.rs`, `team.rs`, `api_key.rs`, `provider.rs`, `mcp_server.rs`, `usage.rs`, `audit_log.rs`.
- **`dto/`** -- Data transfer objects for API request/response serialization.
@@ -393,7 +393,7 @@ The database schema is defined across twelve migration files applied in order on
| Table | Purpose |
|---------------------|---------|
| `providers` | Upstream AI provider configuration: name, type (openai/anthropic/google/azure/bedrock/custom), base URL, AES-encrypted API key, and optional `config_json` (e.g. `api_version` for Azure). |
-| `models` | AI models registered under a provider, with input/output token pricing. |
+| `models` | AI models registered under a provider, with input/output token pricing and an optional maximum number of output tokens a request may ask for. |
| `model_permissions` | Access control rules for models, grantable by role, team, or individual user. |
### 006_init_mcp_servers -- MCP Server Registry
diff --git a/src/content/docs/en/configuration.md b/src/content/docs/en/configuration.md
index c01068f..d9be525 100644
--- a/src/content/docs/en/configuration.md
+++ b/src/content/docs/en/configuration.md
@@ -493,8 +493,26 @@ Changes to dynamic settings take effect immediately without requiring a server r
| ---------------------------- | ------- | ---------------------------------------------------- |
| `signature_drift_seconds` | `300` | Maximum allowed clock drift for signed requests |
| `nonce_ttl_seconds` | `300` | TTL for nonce values used in replay protection |
-| `content_filter_patterns` | `[]` | Content filter patterns (max 500; severity enum: `low`, `medium`, `high`, `critical`) |
-| `pii_patterns` | `[]` | PII detection regex patterns (max 100; max 1000 chars each; validated at save time) |
+| `security.redact` | `{}` | Outbound redaction policy, see [Request guards](#request-guards) |
+| `security.inspect_tools` | `{}` | Tool-call inspection policy, see [Request guards](#request-guards) |
+| `security.content` | `{}` | Content filter policy, see [Request guards](#request-guards) |
+
+#### Request guards
+
+Outbound redaction, tool-call inspection and the content filter each keep their whole policy as one JSON object under `security.redact`, `security.inspect_tools` and `security.content`. The object has the shape of the `security` section of ThinkWatch Core's `config.yaml`, which the [Core configuration reference](/docs/core/configuration#cfg-security) describes field by field, built-in rules included; `{}` is the factory setting.
+
+- `mode` is `off`, `observe` or `enforce`. `observe`, the factory mode, records matches in the audit log and changes nothing; the console calls `enforce` Replace for outbound redaction, Cut off for tool-call inspection and Enforce for the content filter.
+- `enable` and `disable` switch built-in rules on and off by id, `actions` changes what a built-in rule does under `enforce`, and `custom` holds rules of the deployment's own.
+
+```json
+{
+ "mode": "enforce",
+ "enable": ["email"],
+ "custom": [{ "name": "Employee ID", "pattern": "EMP-\\d{6}", "label": "EMPLOYEE" }]
+}
+```
+
+The Content Security page of the console edits these objects. Through `PATCH /api/admin/settings` a key is written whole and checked first: a misspelt field, an unknown built-in rule, a pattern that does not compile or a malformed code point is refused with `400`. Writing `security.redact` takes the `pii_redactor:write` permission, and the other two keys take `content_filter:write`. `GET /api/admin/security` lists each guard's mode and every rule, and `POST /api/admin/security/{guard}/test` tries a sample against them. A deployment upgraded from a version that kept `security.content_filter_patterns`, `security.pii_redactor_patterns`, `security.hidden_text` and `security.tool_inspection` has them converted into these keys at its first start.
### Budget
diff --git a/src/content/docs/en/overview.md b/src/content/docs/en/overview.md
index f14f4d5..fdafc3d 100644
--- a/src/content/docs/en/overview.md
+++ b/src/content/docs/en/overview.md
@@ -7,7 +7,7 @@ ThinkWatch Enterprise is a self-hosted AI API and MCP gateway for organizations.
## Highlights
- **MCP tool calls run as the real user.** Each user connects their own GitHub, Notion, Linear, Slack or Atlassian account, so the upstream's own audit log shows who acted. Each tool can be granted per role and per API key.
-- **Security guards on every request.** Personal information is replaced with placeholders before a request goes upstream and restored in the answer. Tool calls in model responses are checked against rules for dangerous commands, and hidden Unicode characters and prompt-injection phrases in requests are logged or refused.
+- **Security guards on every request.** Credentials and personal information can be replaced with placeholders before a request goes upstream and restored in the answer, and a dangerous tool call in a model response can be cut off before the client runs it. A content filter can refuse prompt injection or delete hidden characters from what the caller sends; every guard ships in observe mode, which only records.
- **Identity from the organization's directory.** Sign-in works through any OIDC provider, with optional TOTP. Five built-in roles and custom roles decide who may use which models, tools and admin pages.
- **One key for AI and MCP.** `tw-` virtual keys can be scoped to the AI gateway, the MCP gateway or both. Keys are stored only as hashes and rotate with a grace period.
- **Rate limits and budgets.** Sliding windows from one minute to one week limit requests or tokens, and daily, weekly or monthly budgets cap spending. Both attach to users, API keys or roles.
diff --git a/src/content/docs/zh-CN/api-reference.md b/src/content/docs/zh-CN/api-reference.md
index 17029cc..5d0a4e6 100644
--- a/src/content/docs/zh-CN/api-reference.md
+++ b/src/content/docs/zh-CN/api-reference.md
@@ -2145,8 +2145,9 @@ MCP 工具调用日志。
"security": {
"signature_drift_seconds": 300,
"nonce_ttl_seconds": 300,
- "content_filter_patterns": [],
- "pii_patterns": []
+ "redact": {},
+ "inspect_tools": {},
+ "content": {}
},
"budget": {
"budget_warning_threshold": 0.8,
diff --git a/src/content/docs/zh-CN/architecture.md b/src/content/docs/zh-CN/architecture.md
index 17c73b9..fbb51a0 100644
--- a/src/content/docs/zh-CN/architecture.md
+++ b/src/content/docs/zh-CN/architecture.md
@@ -324,7 +324,7 @@ MCP 代理引擎。包含:
所有其他 crate 使用的共享基础设施。包含:
- **`config.rs`** —— 从环境变量加载的 `AppConfig` 结构体。
-- **`dynamic_config.rs`** —— `DynamicConfig` 系统,从 `system_settings` 数据库表加载配置。支持通过 Redis Pub/Sub 的多实例同步和内存缓存。涵盖 JWT TTL、缓存 TTL、内容过滤规则、PII 模式、预算阈值、API Key 策略和数据保留设置。
+- **`dynamic_config.rs`** —— `DynamicConfig` 系统,从 `system_settings` 数据库表加载配置。支持通过 Redis Pub/Sub 的多实例同步和内存缓存。涵盖 JWT TTL、缓存 TTL、请求防护(出站脱敏、工具调用审查、内容过滤)的策略、预算阈值、API Key 策略和数据保留设置。
- **`db.rs`** —— 使用 `sqlx` 设置 PostgreSQL 连接池。
- **`models/`** —— 数据库模型结构体(每个领域实体一个):`user.rs`、`team.rs`、`api_key.rs`、`provider.rs`、`mcp_server.rs`、`usage.rs`、`audit_log.rs`。
- **`dto/`** —— 用于 API 请求/响应序列化的数据传输对象。
@@ -389,7 +389,7 @@ ThinkWatch 在 ClickHouse 中存储六种类型的日志,每种使用独立的
| 表 | 用途 |
|---------------------|------|
| `providers` | 上游 AI 提供商配置:名称、类型(openai/anthropic/google/azure/bedrock/custom)、基础 URL、AES 加密的 API 密钥以及可选的 `config_json`(如 Azure 的 `api_version`)。|
-| `models` | 注册在提供商下的 AI 模型,包含输入/输出 Token 定价。|
+| `models` | 注册在提供商下的 AI 模型,包含输入/输出 Token 定价,以及可选的最大输出 token(单个请求可要求的输出 token 上限)。|
| `model_permissions` | 模型的访问控制规则,可按角色、团队或个人用户授权。|
### 006_init_mcp_servers —— MCP 服务器注册
diff --git a/src/content/docs/zh-CN/configuration.md b/src/content/docs/zh-CN/configuration.md
index f27edca..d195c02 100644
--- a/src/content/docs/zh-CN/configuration.md
+++ b/src/content/docs/zh-CN/configuration.md
@@ -493,8 +493,26 @@ openssl rand -base64 32
| ---------------------------- | ------- | -------------------------------------------- |
| `signature_drift_seconds` | `300` | 签名请求允许的最大时钟偏差 |
| `nonce_ttl_seconds` | `300` | 用于重放保护的 nonce 值 TTL |
-| `content_filter_patterns` | `[]` | 内容过滤模式(最多 500 个;严重级别枚举:`low`、`medium`、`high`、`critical`) |
-| `pii_patterns` | `[]` | PII 检测正则表达式模式(最多 100 个;每个最长 1000 字符;保存时验证) |
+| `security.redact` | `{}` | 出站脱敏的策略,见[请求防护](#请求防护) |
+| `security.inspect_tools` | `{}` | 工具调用审查的策略,见[请求防护](#请求防护) |
+| `security.content` | `{}` | 内容过滤的策略,见[请求防护](#请求防护) |
+
+#### 请求防护
+
+出站脱敏、工具调用审查和内容过滤各以一个 JSON 对象保存完整的策略,分别位于 `security.redact`、`security.inspect_tools` 和 `security.content`。对象的结构与 ThinkWatch Core 的 `config.yaml` 中 `security` 一节相同,各字段与全部内置规则见 [Core 配置手册](/zh-CN/docs/core/configuration#cfg-security);`{}` 即出厂设置。
+
+- `mode` 为 `off`、`observe` 或 `enforce`。出厂为 `observe`:命中的内容记入审计日志,不做任何改动;`enforce` 在控制台中按作用命名,出站脱敏为「替换」,工具调用审查为「切断」,内容过滤为「处置」。
+- `enable` 和 `disable` 按 id 启用或停用内置规则,`actions` 改变内置规则在 `enforce` 下的处置,`custom` 保存部署自己的规则。
+
+```json
+{
+ "mode": "enforce",
+ "enable": ["email"],
+ "custom": [{ "name": "员工编号", "pattern": "EMP-\\d{6}", "label": "EMPLOYEE" }]
+}
+```
+
+控制台的「内容安全」页编辑这些对象。经 `PATCH /api/admin/settings` 写入时,一个键整体写入,写入前先校验:字段名拼错、内置规则 id 不存在、正则无法编译或码位写法有误时返回 `400`。写入 `security.redact` 需要 `pii_redactor:write` 权限,另外两个键需要 `content_filter:write`。`GET /api/admin/security` 列出每项防护的档位与全部规则,`POST /api/admin/security/{guard}/test` 用一段样本测试这些规则。从仍使用 `security.content_filter_patterns`、`security.pii_redactor_patterns`、`security.hidden_text` 和 `security.tool_inspection` 的版本升级时,这些设置在首次启动时转换为上述三个键。
### 预算
diff --git a/src/content/docs/zh-CN/overview.md b/src/content/docs/zh-CN/overview.md
index 9e2a209..d795528 100644
--- a/src/content/docs/zh-CN/overview.md
+++ b/src/content/docs/zh-CN/overview.md
@@ -7,7 +7,7 @@ ThinkWatch 企业版是面向组织自托管的 AI API 与 MCP 网关。组织
## 要点
- **MCP 工具调用以真实用户身份执行。** 每位用户连接自己的 GitHub、Notion、Linear、Slack、Atlassian 等账号,上游自身的审计日志因此能记录到具体操作人。每个工具可以按角色和按 API Key 授权。
-- **每个请求都经过安全防护。** 个人信息在请求发往上游前替换为占位符,并在回答中还原。模型返回的工具调用按危险命令规则检查,请求中的隐藏 Unicode 字符和提示词注入语句会被记录或拒绝。
+- **每个请求都经过安全防护。** 凭据和个人信息可在请求发往上游前替换为占位符,并在回答中还原;模型返回的危险工具调用可在客户端执行前切断。内容过滤可拒绝提示词注入,或删除调用方发送内容中的隐藏字符;各项防护出厂为观察档,只记录。
- **身份来自组织目录。** 登录可对接任意 OIDC 提供商,并可启用 TOTP 两步验证。五个内置角色与自定义角色决定每个人可用的模型、工具和管理页面。
- **AI 与 MCP 共用一把密钥。** `tw-` 虚拟密钥可限定用于 AI 网关、MCP 网关或两者。密钥只以哈希形式保存,轮换时保留宽限期。
- **限流与预算。** 一分钟到一周的滑动窗口限制请求数或 token 数,按日、周、月的预算控制总用量。两者均可设置在用户、API Key 或角色上。
diff --git a/src/data/core-docs/config.md b/src/data/core-docs/config.md
index 655d04d..c75bafd 100644
--- a/src/data/core-docs/config.md
+++ b/src/data/core-docs/config.md
@@ -150,13 +150,14 @@ means.
| `proxies` | list of [`proxies[]`](#cfg-proxies) | `[]` | Outbound proxies, declared once and referred to by name from `providers[].proxy`. |
| `pricing` | object, [`pricing`](#cfg-pricing) | — | Refreshing the default price table, and price sheets of your own. |
| `client_probes` | object, [`client_probes`](#cfg-client_probes) | — | What happens to the helper requests clients send on their own (health checks, warm-ups, titles). |
-| `security` | object, [`security`](#cfg-security) | — | The five guards. All of them start in `observe` or `off`, so out of the box nothing is changed or blocked. |
+| `security` | object, [`security`](#cfg-security) | — | The three guards. All of them start in `observe`, so out of the box nothing is changed or refused. |
| `retention` | object, [`retention`](#cfg-retention) | — | How long request logs are kept. |
| `failover` | object, [`failover`](#cfg-failover) | — | How long an upstream is set aside after it fails, and how long the start of a stream is awaited. |
| `groups` | list of [`groups[]`](#cfg-groups) | `[]` | Strategy groups: several upstreams behind one name, with a way to pick among them. |
| `routes` | list of [`routes[]`](#cfg-routes) | `[]` | Routes. Without any, requests fail over across all upstreams in the order they are declared. |
| `default_route` | string | — | The route for keys that do not name one. Unset: the route named `default`, or the built-in failover when there is none. |
| `default_key` | string | — | The gateway key for clients that were not given a key of their own. Unset: the key named `default`, or the first key. It cannot be disabled. |
+| `plugins` | list of [`plugins[]`](#cfg-plugins) | `[]` | Script plugins, in the order they run. The app installs them; each one's code and settings are a file next to this one. |
### `listen`
@@ -574,27 +575,29 @@ answered locally (`intercept`, nothing is sent upstream), passed through
### `security`
-Five guards, applied to every upstream alike. Each has a `mode`: `off`,
-`observe` (detect and record, change nothing) or `enforce` (act). They start
-in `observe`, except the output limit, which starts `off`. What `enforce`
-does differs per guard, and each says so below.
+Three guards, applied to every upstream alike. Each has a `mode`: `off`,
+`observe` (detect and record, change nothing) or `enforce` (act). All three
+start in `observe`. What `enforce` does differs per guard: redaction replaces,
+tool-call inspection cuts the response off, and the content filter does what
+each rule says (refuse, delete or record).
| Field | Type | Default | Description |
|---|---|---|---|
-| `redact` | object, [`security.redact`](#cfg-security-redact) | — | Outbound redaction: credentials found in a request are replaced before it leaves. |
+| `redact` | object, [`security.redact`](#cfg-security-redact) | — | Outbound redaction: credentials and personal information anywhere in a request are replaced before it leaves. |
| `inspect_tools` | object, [`security.inspect_tools`](#cfg-security-inspect_tools) | — | Tool-call inspection: dangerous commands in the tool calls a model returns cut the response off. |
-| `hidden_text` | object, [`security.hidden_text`](#cfg-security-hidden_text) | — | Hidden characters that people cannot see and models can read refuse the request. |
-| `content` | object, [`security.content`](#cfg-security-content) | — | Content filter: words or patterns in what the caller sends refuse the request. |
-| `output_limit` | object, [`security.output_limit`](#cfg-security-output_limit) | — | Output length: a response longer than the limit is cut off. |
+| `content` | object, [`security.content`](#cfg-security-content) | — | Content filter: words, patterns or characters (hidden ones among them) in what the caller sends; each rule refuses the request, deletes what it matched, or only records it. |
#### `security.redact`
-Before a request leaves, credentials in it are looked for. Under `enforce`
-they are replaced.
+Before a request leaves, the whole request (system prompt, earlier answers
+and tool calls included) is searched for credentials and personal
+information. Under `enforce` what is found is replaced with placeholders, and
+put back where the answer repeats them. Images, files and other base64
+payloads are not searched.
@@ -604,7 +607,7 @@ they are replaced.
| `mode` | `off` \| `observe` \| `enforce` | `observe` | `off` does nothing; `observe` detects and records only, and changes nothing; `enforce` detects and acts. |
| `enable` | list of strings | `[]` | Built-in rules to switch on that are off out of the box, by id. |
| `disable` | list of strings | `[]` | Built-in rules to switch off, by id. |
-| `custom` | list of [`security.redact.custom[]`](#cfg-security-redact-custom) | `[]` | Rules of your own: whatever a pattern matches is treated as a credential. |
+| `custom` | list of [`security.redact.custom[]`](#cfg-security-redact-custom) | `[]` | Rules of your own: whatever a pattern matches is replaced like a credential. |
@@ -614,6 +617,7 @@ they are replaced.
|---|---|---|---|
| `name` | string | **required** | Name shown in logs and in the app; it identifies the rule and has to be unique within this guard. |
| `pattern` | string | **required** | Regular expression. |
+| `label` | string | `SECRET` | Placeholder name: what the pattern matches is replaced with `<>`, numbered per name. Capital letters, digits and underscores, starting with a letter, at most 24 characters. |
| `disabled` | bool | `false` | Switches the rule off and keeps it in the file. |
@@ -646,10 +650,50 @@ Built-in rules:
| `private-key` | Private key | on |
| `jwt` | JWT | on |
| `conn-string-password` | Connection string password | on |
+| `cn-resident-id` | Chinese resident ID number | on |
+| `bank-card` | Bank card number | on |
+| `email` | Email address | off |
+| `cn-mobile-phone` | Chinese mainland mobile number | off |
| `internal-ip` | Internal IP address | off |
| `internal-domain` | Internal domain | off |
+`cn-resident-id`, `bank-card`, `email` and `cn-mobile-phone` look for
+personal information rather than credentials. The first two are on out of the
+box and match only what checks out by structure:
+
+- `cn-resident-id`: an 18-character resident ID number of the People's
+ Republic of China whose first two digits are a province-level code, whose
+ date of birth is a real date between 1900-01-01 and today, and whose last
+ character is the right check character (ISO 7064 MOD 11-2). The old
+ 15-digit numbers are not matched.
+- `bank-card`: a card number whose prefix and length belong to UnionPay,
+ Visa, Mastercard, American Express, JCB, Discover or Diners Club and which
+ passes the Luhn check, written as one run of digits or in groups of four
+ separated by single spaces or single hyphens (American Express also 4-6-5,
+ Diners Club also 4-6-4). The test card numbers published by Stripe,
+ Braintree and Adyen are not matched.
+
+`email` and `cn-mobile-phone` are off out of the box: they have no structure
+to check, and code and documents are full of things that look like them.
+
+- `email`: an address whose domain has at least two parts, the last of them
+ two or more letters. User names in URLs (`https://user@host`) and file
+ names such as `icon@2x.png` are not matched.
+- `cn-mobile-phone`: a Chinese mainland mobile number, 11 digits starting
+ with `1` and a second digit from `3` to `9`, not part of a longer run of
+ digits.
+
+A number that is part of a longer run of letters or digits is not matched,
+and neither is one written as a JSON number in the request body (in a tool
+call's arguments, for instance), since replacing it would leave the body
+invalid JSON. The placeholders of these rules say what was there
+(`<>`, `<>`, `<>`,
+`<>`), and the security log shows only the last four characters
+of a number, and the first character and the domain of an email address. A
+custom rule replaces with `<>` unless it names its own
+placeholder (`label`).
+
#### `security.inspect_tools`
Tool calls a model returns are checked against the rules. Under `enforce`,
@@ -690,37 +734,23 @@ Built-in rules:
| `exfil-credentials` | Send out a credential file | `cut` |
| `exfil-credentials-reversed` | Send out a credential file (verb first) | `cut` |
| `ssh-key-read` | Read a private key or cloud credential | `cut` |
+| `secret-to-unknown-host` | Send a credential to an unknown host | `cut` |
| `write-startup-item` | Write a startup item | `cut` |
| `crontab-install` | Install a scheduled job | `cut` |
| `rm-rf-root` | Delete home or root | `record` |
| `chmod-777` | World-writable permissions | `record` |
-
-
-#### `security.hidden_text`
-
-Characters people cannot see and models can read, in what the caller sends
-(tool results included). Under `enforce`, the request is refused.
-
-
-
-
-| Field | Type | Default | Description |
-|---|---|---|---|
-| `mode` | `off` \| `observe` \| `enforce` | `observe` | `off` does nothing; `observe` detects and records only, and changes nothing; `enforce` detects and acts. |
-| `disable` | list of strings | `[]` | Kinds not to look for: `tag`, `bidi`. |
-
-
-
-| Kind | What it is |
-|---|---|
-| `tag` | Unicode tag characters (U+E0000 to U+E007F): invisible everywhere, read by the model, able to carry a whole instruction. |
-| `bidi` | Bidirectional control characters: make the order shown differ from the order the model reads. |
+| `upload-file-to-host` | Upload a local file to an external host | `record` |
#### `security.content`
-Words or patterns in what the caller sends. Under `enforce`, a match with
-rules set to `block` refuses the request.
+Words, patterns or characters in what the caller sends: user messages and
+the tool results in them, not the system prompt or the model's own turns.
+Each rule matches a keyword (`contains`), a regular expression (`regex`) or
+code points (`codepoints`), and says what happens under `enforce`: `block`
+refuses the request, `strip` deletes every match from the caller's text and
+sends the rest, `record` only records it. After deleting, the text is checked
+again, so a keyword split by hidden characters is caught once they are gone.
@@ -730,7 +760,7 @@ rules set to `block` refuses the request.
| `mode` | `off` \| `observe` \| `enforce` | `observe` | `off` does nothing; `observe` detects and records only, and changes nothing; `enforce` detects and acts. |
| `enable` | list of strings | `[]` | Built-in rules to switch on that are off out of the box, by id. |
| `disable` | list of strings | `[]` | Built-in rules to switch off, by id. |
-| `actions` | map of built-in rule id → `block` \| `record` | `{}` | What a built-in rule does under `enforce`, written only where it differs from the factory setting. |
+| `actions` | map of built-in rule id → `block` \| `strip` \| `record` | `{}` | What a built-in rule does under `enforce`, written only where it differs from the factory setting. |
| `custom` | list of [`security.content.custom[]`](#cfg-security-content-custom) | `[]` | Rules of your own. |
@@ -740,9 +770,9 @@ rules set to `block` refuses the request.
| Field | Type | Default | Description |
|---|---|---|---|
| `name` | string | **required** | Name shown in logs and in the app; it identifies the rule and has to be unique within this guard. |
-| `pattern` | string | **required** | A keyword, or a regular expression with `match: regex`. Case-insensitive either way. |
-| `match` | `contains` \| `regex` | `contains` | `contains`: the text contains `pattern`. `regex`: `pattern` is a regular expression. |
-| `action` | `block` \| `record` | `record` | Under `enforce`: `block` the request, or only `record` the match. |
+| `pattern` | string | **required** | A keyword; a regular expression with `match: regex`; code points with `match: codepoints` (`U+200B, U+E0000–U+E007F`). Keywords and regular expressions are case-insensitive. |
+| `match` | `contains` \| `regex` \| `codepoints` | `contains` | `contains`: the text contains `pattern`. `regex`: `pattern` is a regular expression. `codepoints`: the text has a character among the code points or ranges listed in `pattern`, separated by commas. |
+| `action` | `block` \| `strip` \| `record` | `record` | Under `enforce`: `block` the request, `strip` what matched and send the rest, or only `record` the match. |
| `disabled` | bool | `false` | Switches the rule off and keeps it in the file. |
@@ -751,6 +781,10 @@ Built-in rules:
| id | Name | Group | Out of the box | Under `enforce`, out of the box |
|---|---|---|---|---|
+| `unicode-tags` | Unicode tag characters | invisible | on | `strip` |
+| `bidi-controls` | Bidirectional controls | invisible | on | `strip` |
+| `zero-width` | Zero-width characters | invisible | off | `strip` |
+| `private-use` | Private-use characters | invisible | off | `strip` |
| `ignore-previous-instructions` | Ignore previous instructions | injection | on | `block` |
| `ignore-all-previous` | Ignore all previous | injection | on | `block` |
| `disregard-your-instructions` | Disregard your instructions | injection | on | `block` |
@@ -775,16 +809,12 @@ Built-in rules:
| `zh-jailbreak` | Jailbreak (Chinese) | chinese | off | `block` |
-#### `security.output_limit`
-
-
-
-
-| Field | Type | Default | Description |
-|---|---|---|---|
-| `mode` | `off` \| `observe` \| `enforce` | `off` | Off out of the box: no single limit suits every use. `observe` records long responses; `enforce` stops the stream at the limit. |
-| `max_chars` | integer | `100000` | Limit in characters (Unicode scalar values), from 1 to 1000000. |
-
+The `invisible` group matches characters people cannot see and models can
+read. Unicode tag characters (U+E0000–U+E007F) and bidirectional controls
+(U+202A–U+202E, U+2066–U+2069) are on out of the box; zero-width characters
+(U+200B–U+200D, U+2060, U+FEFF) and private-use characters (U+E000–U+F8FF,
+U+F0000–U+FFFFD, U+100000–U+10FFFD) are off, since emoji, Persian and icon
+fonts use them too. All four delete what they match under `enforce`.
```yaml
security:
@@ -794,11 +824,16 @@ security:
custom:
- name: employee-id
pattern: 'EMP-\d{6}'
+ label: EMPLOYEE
inspect_tools:
mode: enforce
- output_limit:
+ content:
mode: enforce
- max_chars: 200000
+ enable: [zero-width]
+ custom:
+ - name: project-x
+ pattern: project-x
+ action: strip
```
### `retention`
@@ -807,6 +842,13 @@ Two limits, because the two kinds of data differ in size by three orders
of magnitude: request bodies are tens of kilobytes each, a request's record
a few hundred bytes. The byte limit covers bursts.
+Each request and response body is kept up to 4 MiB; of a longer one, the
+beginning is kept. Bodies are written with credentials and personal numbers
+already taken out. A request stored under `enforce` carries the placeholders
+the upstream received; anything else the redaction rules
+([`security.redact`](#cfg-security-redact)) recognize is masked, in every
+mode, `off` included.
+
@@ -814,7 +856,7 @@ a few hundred bytes. The byte limit covers bursts.
|---|---|---|---|
| `body_days` | integer | `7` | Days to keep request and response bodies. |
| `row_days` | integer | `90` | Days to keep the record of each request (time, model, usage, cost). |
-| `body_max_bytes` | integer | `2147483648` | Upper bound on the bytes bodies may take; beyond it the oldest days go first. The default is 2 GiB. |
+| `body_max_bytes` | integer | `5368709120` | Upper bound on the bytes bodies may take; beyond it the oldest days go first. The default is 5 GiB. |
### `failover`
@@ -963,6 +1005,93 @@ routes:
default_route: default
```
+### `plugins`
+
+Script plugins change requests before they reach an upstream and answers
+before they reach the client. They run in a sandbox inside core, without
+access to files, the network or the real values of secrets. The app installs
+them: each plugin's code goes to `plugins/.js` next to this file, a copy
+of the approved code to `plugins/.approved/.js`, and the code's SHA-256
+to `sha256`.
+
+A plugin's file holds its settings too. The `manifest` at the top of the file
+says what to do when the plugin fails (`on_error`), which requests it handles
+(`match`) and the value of each setting (`settings..value`). When the
+app changes one of these, it rewrites only the manifest in the file and
+updates `sha256` along with it. This list keeps just the plugin, its approved
+hash and whether it is on. Entries written by core 0.58 also have `on_error`,
+`scope` and `settings`: they are ignored, and removed the next time the app
+changes a plugin.
+
+A plugin runs only while its file has exactly the approved hash. When the
+file changes on disk or disappears, the plugin stops within seconds and the
+app shows the change for review. Until the change is approved, the requests
+the plugin covers are refused (`on_error: "reject"`, the default) or pass
+without it (`on_error: "skip"`), as the approved file says. A plugin that does
+not load is handled the same way. Neither keeps the rest of the configuration
+from taking effect.
+
+Plugins run in the order of this list.
+
+In `match`, `clients`, `models` and `upstreams` are lists of names or
+patterns with `*` anywhere in them, matched regardless of case; a list that
+is empty or left out matches everything. `clients` names the client app
+(`claude-code`, `codex`, …), and a request whose app is not recognised
+matches only an empty list. `models` matches the model sent to the upstream:
+when a routing rule renames the model, the new name is the one that matches.
+`upstreams` applies to requests and answers alike.
+
+A plugin changes a request after routing, each time the request is sent to an
+upstream. A request that fails over to another upstream starts again from what
+the client sent, and the plugin sees which upstream and which model name the
+request goes to. Routing, model checks and session grouping use what the
+client sent. A plugin that changes the model name only renames what is sent to
+that upstream: the request is not routed again, and the new name must still be
+one of the models the key may use ([`clients[].allow`](#cfg-clients)), or the
+request is not sent.
+
+A plugin handles the kinds of request its code declares: conversations
+(Anthropic Messages, OpenAI Chat Completions and Responses, and Gemini,
+including their token counts and compaction), embeddings (`/v1/embeddings`,
+Gemini `:embedContent` and `:batchEmbedContents`) and legacy completions
+(`/v1/completions`). A plugin that declares none handles conversations only.
+Requests of a kind a plugin does not handle pass without it, whatever its
+`on_error`. Other endpoints, such as images and audio, pass without any plugin.
+
+
+
+
+| Field | Type | Default | Description |
+|---|---|---|---|
+| `id` | string | **required** | Lowercase letters, digits and hyphens, 1 to 40 characters; unique. `order`, `inspect`, `rewrite` and `confirmed` are taken by the control plane. |
+| `file` | string | **required** | The plugin's code, relative to this file's directory. It is always `plugins/.js`; the app writes it. |
+| `sha256` | string | **required** | SHA-256 of the approved code, 64 lowercase hexadecimal characters. When the file no longer has this hash, the plugin stops running until the change is approved in the app. The approved code is kept in `plugins/.approved/.js`. |
+| `enabled` | bool | `true` | Run the plugin. `false` keeps it installed and out of every request. |
+
+
+```yaml
+plugins:
+ - id: add-date
+ file: plugins/add-date.js
+ sha256: 9f2b6c0e4a1d8f3b7c5e2a9d6f1b4c8e3a7d0f5b2c9e6a1d4f8b3c7e0a5d2f9b
+ enabled: true
+```
+
+The start of `plugins/add-date.js`:
+
+```js
+export const manifest = {
+ name: "Add date",
+ api: 1,
+ permissions: ["system"],
+ match: { clients: ["claude-code"], models: ["claude-*"], upstreams: [] },
+ on_error: "reject",
+ settings: {
+ note: { type: "string", label: "Note", value: "Answer in English." },
+ },
+};
+```
+
## Environment variables
| Variable | Effect |
diff --git a/src/data/core-docs/config.zh-CN.md b/src/data/core-docs/config.zh-CN.md
index 38093a1..13f8bee 100644
--- a/src/data/core-docs/config.zh-CN.md
+++ b/src/data/core-docs/config.zh-CN.md
@@ -99,13 +99,14 @@ twcore config set /listen/gateway/port 8790 --int
| `proxies` | 对象列表,见 [`proxies[]`](#cfg-proxies) | `[]` | 出站代理。在这里声明一次,由 `providers[].proxy` 按名字引用。 |
| `pricing` | 对象,见 [`pricing`](#cfg-pricing) | — | 默认价目表是否定期刷新,以及自定义价目表。 |
| `client_probes` | 对象,见 [`client_probes`](#cfg-client_probes) | — | 客户端自行发出的辅助请求(连通性检查、预热、起标题)如何处理。 |
-| `security` | 对象,见 [`security`](#cfg-security) | — | 五项防护。出厂时都处在 `observe` 或 `off`,不改变、不拦截任何请求。 |
+| `security` | 对象,见 [`security`](#cfg-security) | — | 三项防护。出厂时都处在 `observe`,不改变、不拒绝任何请求。 |
| `retention` | 对象,见 [`retention`](#cfg-retention) | — | 请求日志保留多久。 |
| `failover` | 对象,见 [`failover`](#cfg-failover) | — | 上游失败后停用多久,以及流式回答的开头最多等多久。 |
| `groups` | 对象列表,见 [`groups[]`](#cfg-groups) | `[]` | 策略组:多个上游合用一个名字,并规定如何在其中选择。 |
| `routes` | 对象列表,见 [`routes[]`](#cfg-routes) | `[]` | 路由。一条都不写时,请求按上游的声明顺序故障转移。 |
| `default_route` | 字符串 | — | 未指定路由的密钥走哪条路由。不写:名为 `default` 的路由;没有这条路由时走内置的故障转移。 |
| `default_key` | 字符串 | — | 没有专用密钥的客户端使用哪一把。不写:名为 `default` 的那把,没有则取第一把。这把密钥不能停用。 |
+| `plugins` | 对象列表,见 [`plugins[]`](#cfg-plugins) | `[]` | 脚本插件,按运行的顺序。由应用安装,每个插件的代码和设置是本文件旁边的一个文件。 |
### `listen`
@@ -454,23 +455,21 @@ pricing:
### `security`
-五项防护,对所有上游一视同仁。每一项都有 `mode`:`off`、`observe`(检测并记录,不改变任何行为)、`enforce`(处置)。出厂时除输出长度为 `off` 外,其余都是 `observe`。各项在 `enforce` 下的处置不同,分别见下文。
+三项防护,对所有上游一视同仁。每一项都有 `mode`:`off`、`observe`(检测并记录,不改变任何行为)、`enforce`(处置)。出厂时三项都是 `observe`。各项在 `enforce` 下的处置不同:出站脱敏替换,工具调用审查切断响应,内容过滤按每条规则的处置拒绝、删除或只记录。
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
-| `redact` | 对象,见 [`security.redact`](#cfg-security-redact) | — | 出站脱敏:请求发出前,把其中的凭据替换掉。 |
+| `redact` | 对象,见 [`security.redact`](#cfg-security-redact) | — | 出站脱敏:请求发出前,把其中任何位置的凭据和个人信息替换掉。 |
| `inspect_tools` | 对象,见 [`security.inspect_tools`](#cfg-security-inspect_tools) | — | 工具调用审查:模型返回的工具调用中出现危险命令时切断响应。 |
-| `hidden_text` | 对象,见 [`security.hidden_text`](#cfg-security-hidden_text) | — | 人看不见、模型读得到的隐藏字符,出现时拒绝请求。 |
-| `content` | 对象,见 [`security.content`](#cfg-security-content) | — | 内容过滤:调用方发送的内容中出现指定的词或写法时拒绝请求。 |
-| `output_limit` | 对象,见 [`security.output_limit`](#cfg-security-output_limit) | — | 输出长度:回答超过上限时切断。 |
+| `content` | 对象,见 [`security.content`](#cfg-security-content) | — | 内容过滤:调用方发送的内容中出现指定的词、写法或字符(包括隐藏字符)时,按规则拒绝请求、删除命中的内容或只记录。 |
#### `security.redact`
-请求发出前查找其中的凭据。`enforce` 下将其替换。
+请求发出前,在整个请求中(包括系统提示、之前的回答和工具调用)查找凭据和个人信息。`enforce` 下将其替换为占位符,回答中重复出现时再换回原值。图片、文件等 base64 内容不查。
@@ -480,7 +479,7 @@ pricing:
| `mode` | `off` \| `observe` \| `enforce` | `observe` | `off` 不检测;`observe` 检测并记录,不改变任何行为;`enforce` 检测并处置。 |
| `enable` | 字符串列表 | `[]` | 打开出厂时关着的内置规则,按 id。 |
| `disable` | 字符串列表 | `[]` | 关掉内置规则,按 id。 |
-| `custom` | 对象列表,见 [`security.redact.custom[]`](#cfg-security-redact-custom) | `[]` | 自定义规则:正则匹配到的内容按凭据处理。 |
+| `custom` | 对象列表,见 [`security.redact.custom[]`](#cfg-security-redact-custom) | `[]` | 自定义规则:正则匹配到的内容和凭据一样替换。 |
@@ -490,6 +489,7 @@ pricing:
|---|---|---|---|
| `name` | 字符串 | **必填** | 日志和应用里显示的名字,也是规则的标识;同一项防护里不能重名。 |
| `pattern` | 字符串 | **必填** | 正则表达式。 |
+| `label` | 字符串 | `SECRET` | 占位符名称:正则匹配到的内容替换为 `<>`,每个名称各自编号。只能使用大写字母、数字和下划线,以字母开头,最多 24 个字符。 |
| `disabled` | 布尔 | `false` | 停用这条规则,规则本身留在文件里。 |
@@ -522,10 +522,26 @@ pricing:
| `private-key` | Private key | 开 |
| `jwt` | JWT | 开 |
| `conn-string-password` | Connection string password | 开 |
+| `cn-resident-id` | Chinese resident ID number | 开 |
+| `bank-card` | Bank card number | 开 |
+| `email` | Email address | 关 |
+| `cn-mobile-phone` | Chinese mainland mobile number | 关 |
| `internal-ip` | Internal IP address | 关 |
| `internal-domain` | Internal domain | 关 |
+`cn-resident-id`、`bank-card`、`email`、`cn-mobile-phone` 查找的是个人信息而不是凭据。前两条出厂开启,只认结构上核对得上的:
+
+- `cn-resident-id`:18 位的中华人民共和国居民身份证号码。前两位须是省级行政区划代码,出生日期须是 1900 年 1 月 1 日至今天之间的真实日期,末位须是正确的校验码(ISO 7064 MOD 11-2)。15 位的旧号码不认。
+- `bank-card`:卡号。开头和位数须属于银联、Visa、Mastercard、American Express、JCB、Discover 或 Diners Club,并通过 Luhn 校验;连续书写,或四位一组、以单个空格或单个连字符分隔均可(American Express 另认 4-6-5,Diners Club 另认 4-6-4)。Stripe、Braintree、Adyen 公开的测试卡号不认。
+
+`email` 和 `cn-mobile-phone` 出厂关闭:它们没有可核对的结构,代码和文档中形似的内容很多。
+
+- `email`:邮箱地址,域名至少两段、最后一段为两个以上的字母。URL 中的用户名(`https://user@host`)和 `icon@2x.png` 这类文件名不认。
+- `cn-mobile-phone`:中国大陆手机号,11 位数字,以 `1` 开头、第二位为 `3` 至 `9`,前后不紧挨其他数字。
+
+夹在更长的一串字母或数字中间的号码不认;请求体中以 JSON 数值写出的号码(例如工具调用的参数)也不认,替换它会使请求体不再是合法的 JSON。占位符写明原来是什么(`<>`、`<>`、`<>`、`<>`),安全日志中号码只显示最后四位,邮箱只显示第一个字和域名。自定义规则替换为 `<>`,写了占位符名称(`label`)时用它。
+
#### `security.inspect_tools`
按规则检查模型返回的工具调用。`enforce` 下命中处置为 `cut` 的规则时切断响应,客户端拿不到可执行的完整调用。
@@ -564,35 +580,17 @@ pricing:
| `exfil-credentials` | Send out a credential file | `cut` |
| `exfil-credentials-reversed` | Send out a credential file (verb first) | `cut` |
| `ssh-key-read` | Read a private key or cloud credential | `cut` |
+| `secret-to-unknown-host` | Send a credential to an unknown host | `cut` |
| `write-startup-item` | Write a startup item | `cut` |
| `crontab-install` | Install a scheduled job | `cut` |
| `rm-rf-root` | Delete home or root | `record` |
| `chmod-777` | World-writable permissions | `record` |
-
-
-#### `security.hidden_text`
-
-调用方发送的内容中(包括工具结果)人看不见、模型读得到的字符。`enforce` 下拒绝请求。
-
-
-
-
-| 字段 | 类型 | 默认值 | 说明 |
-|---|---|---|---|
-| `mode` | `off` \| `observe` \| `enforce` | `observe` | `off` 不检测;`observe` 检测并记录,不改变任何行为;`enforce` 检测并处置。 |
-| `disable` | 字符串列表 | `[]` | 不检查的种类:`tag`、`bidi`。 |
-
-
-
-| 种类 | 说明 |
-|---|---|
-| `tag` | Unicode 标签字符(U+E0000 至 U+E007F):在任何地方都不可见,模型却能读到,足以藏下一整段指令。 |
-| `bidi` | 双向控制符:使显示顺序与模型读到的顺序不一致。 |
+| `upload-file-to-host` | Upload a local file to an external host | `record` |
#### `security.content`
-调用方发送的内容中出现的词或写法。`enforce` 下命中处置为 `block` 的规则时拒绝请求。
+调用方发送的内容中(用户消息及其中的工具结果,不含系统提示和模型自己的回答)出现的词、写法或字符。每条规则按关键词(`contains`)、正则(`regex`)或码位(`codepoints`)匹配,并写明 `enforce` 下的处置:`block` 拒绝请求,`strip` 把命中的内容从调用方的正文中全部删除后发出,`record` 只记录。删除之后会再检查一遍:被隐藏字符拆开的关键词,删掉隐藏字符后照样命中。
@@ -602,7 +600,7 @@ pricing:
| `mode` | `off` \| `observe` \| `enforce` | `observe` | `off` 不检测;`observe` 检测并记录,不改变任何行为;`enforce` 检测并处置。 |
| `enable` | 字符串列表 | `[]` | 打开出厂时关着的内置规则,按 id。 |
| `disable` | 字符串列表 | `[]` | 关掉内置规则,按 id。 |
-| `actions` | 映射: 内置规则 id → `block` \| `record` | `{}` | 内置规则在 `enforce` 下的处置,只写与出厂不同的。 |
+| `actions` | 映射: 内置规则 id → `block` \| `strip` \| `record` | `{}` | 内置规则在 `enforce` 下的处置,只写与出厂不同的。 |
| `custom` | 对象列表,见 [`security.content.custom[]`](#cfg-security-content-custom) | `[]` | 自定义规则。 |
@@ -612,9 +610,9 @@ pricing:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `name` | 字符串 | **必填** | 日志和应用里显示的名字,也是规则的标识;同一项防护里不能重名。 |
-| `pattern` | 字符串 | **必填** | 关键词;`match: regex` 时为正则表达式。均不区分大小写。 |
-| `match` | `contains` \| `regex` | `contains` | `contains`:正文包含 `pattern`。`regex`:`pattern` 是正则表达式。 |
-| `action` | `block` \| `record` | `record` | `enforce` 下拒绝请求(`block`),或只记录(`record`)。 |
+| `pattern` | 字符串 | **必填** | 关键词;`match: regex` 时为正则表达式;`match: codepoints` 时为码位(`U+200B, U+E0000–U+E007F`)。关键词和正则不区分大小写。 |
+| `match` | `contains` \| `regex` \| `codepoints` | `contains` | `contains`:正文包含 `pattern`。`regex`:`pattern` 是正则表达式。`codepoints`:正文中出现 `pattern` 所列码位或码位范围内的字符,多个之间用逗号分隔。 |
+| `action` | `block` \| `strip` \| `record` | `record` | `enforce` 下拒绝请求(`block`)、删除命中的内容后发出(`strip`),或只记录(`record`)。 |
| `disabled` | 布尔 | `false` | 停用这条规则,规则本身留在文件里。 |
@@ -623,6 +621,10 @@ pricing:
| id | 名称 | 分组 | 出厂 | `enforce` 下出厂处置 |
|---|---|---|---|---|
+| `unicode-tags` | Unicode tag characters | invisible | 开 | `strip` |
+| `bidi-controls` | Bidirectional controls | invisible | 开 | `strip` |
+| `zero-width` | Zero-width characters | invisible | 关 | `strip` |
+| `private-use` | Private-use characters | invisible | 关 | `strip` |
| `ignore-previous-instructions` | Ignore previous instructions | injection | 开 | `block` |
| `ignore-all-previous` | Ignore all previous | injection | 开 | `block` |
| `disregard-your-instructions` | Disregard your instructions | injection | 开 | `block` |
@@ -647,16 +649,7 @@ pricing:
| `zh-jailbreak` | Jailbreak (Chinese) | chinese | 关 | `block` |
-#### `security.output_limit`
-
-
-
-
-| 字段 | 类型 | 默认值 | 说明 |
-|---|---|---|---|
-| `mode` | `off` \| `observe` \| `enforce` | `off` | 出厂关闭:没有一个上限适合所有用途。`observe` 记录超长的回答;`enforce` 在超过上限处停止输出。 |
-| `max_chars` | 整数 | `100000` | 上限,按字符(Unicode 标量)计,取值 1 到 1000000。 |
-
+`invisible`(隐藏字符)一组匹配人看不见、模型读得到的字符。Unicode 标签字符(U+E0000–U+E007F)和双向控制符(U+202A–U+202E、U+2066–U+2069)出厂开启;零宽字符(U+200B–U+200D、U+2060、U+FEFF)和私用区字符(U+E000–U+F8FF、U+F0000–U+FFFFD、U+100000–U+10FFFD)出厂关闭,表情符号、波斯文和图标字体也会用到它们。四条在 `enforce` 下都删除命中的字符。
```yaml
security:
@@ -666,17 +659,24 @@ security:
custom:
- name: employee-id
pattern: 'EMP-\d{6}'
+ label: EMPLOYEE
inspect_tools:
mode: enforce
- output_limit:
+ content:
mode: enforce
- max_chars: 200000
+ enable: [zero-width]
+ custom:
+ - name: project-x
+ pattern: project-x
+ action: strip
```
### `retention`
设两个期限,是因为两类数据的体积相差三个数量级:一条请求的正文有几十 KB,一条请求记录只有几百字节。字节上限用于应对用量突增。
+每份请求和响应正文最多保存 4 MiB,更长的只保存开头。正文写入磁盘前已去掉凭据和个人号码:`enforce` 下保存的请求带着发给上游的占位符,脱敏规则([`security.redact`](#cfg-security-redact))认出的其他内容一律打码保存,`off` 时也一样。
+
@@ -684,7 +684,7 @@ security:
|---|---|---|---|
| `body_days` | 整数 | `7` | 请求和响应正文保留的天数。 |
| `row_days` | 整数 | `90` | 每条请求记录(时间、模型、用量、费用)保留的天数。 |
-| `body_max_bytes` | 整数 | `2147483648` | 正文最多占用的字节数,超出时从最早的日期开始删除。默认 2 GiB。 |
+| `body_max_bytes` | 整数 | `5368709120` | 正文最多占用的字节数,超出时从最早的日期开始删除。默认 5 GiB。 |
### `failover`
@@ -813,6 +813,56 @@ routes:
default_route: default
```
+### `plugins`
+
+脚本插件在请求发往上游之前改写请求,在回答到达客户端之前改写回答。插件运行在 core 内部的沙箱中,无法访问文件、网络,也看不到密钥的真实值。插件由应用安装:代码写入本文件旁边的 `plugins/.js`,批准过的代码另存一份在 `plugins/.approved/.js`,代码的 SHA-256 写入 `sha256`。
+
+插件的设置也在它自己的文件里。文件开头的 `manifest` 写着插件出错时怎么办(`on_error`)、处理哪些请求(`match`)和每个设置项的值(`settings.<名称>.value`)。在应用里改这几项时,应用只改写文件里的 manifest,并同时更新 `sha256`。本列表只记录插件本身、批准的哈希和是否启用。core 0.58 写下的条目里还有 `on_error`、`scope` 和 `settings`:这些字段不再生效,下一次在应用里改动插件时会被去掉。
+
+只有文件的哈希与批准时一致,插件才会运行。磁盘上的文件被改动或删除后,插件会在几秒内停止运行,应用里会列出改动供审阅。批准之前,按批准过的文件里写的,插件覆盖的请求会被拒绝(`on_error: "reject"`,默认),或者跳过这个插件照常发出(`on_error: "skip"`)。加载失败的插件按同样的方式处理。两种情况都不影响配置其余部分生效。
+
+插件按本列表的顺序运行。
+
+`match` 里的 `clients`、`models` 和 `upstreams` 都是名字或通配(`*` 可以写在任意位置)的列表,不区分大小写;列表为空或不写表示全部。`clients` 是客户端应用(`claude-code`、`codex` 等),认不出应用的请求只有空列表才算在内。`models` 按发给上游的模型匹配:路由规则改了模型名的,按改名之后的匹配。`upstreams` 对请求和回答都适用。
+
+插件在路由之后改写请求,请求每发往一个上游改写一次。故障转移到另一个上游时,从客户端发来的原样重新开始;插件看得到这一次发往哪个上游、用哪个模型名。路由、模型准入和会话归组看的都是客户端发来的原样。插件改了模型名,只是换掉发给这个上游的名字:不会重新路由,新的名字仍要在这把密钥可用的模型之内([`clients[].allow`](#cfg-clients)),否则请求不发出。
+
+插件处理它在代码里声明的那几种请求:对话(Anthropic Messages、OpenAI Chat Completions 和 Responses、Gemini,连同它们的数 token 和压缩)、嵌入(`/v1/embeddings`、Gemini 的 `:embedContent` 和 `:batchEmbedContents`)和旧版补全(`/v1/completions`)。没有声明的插件只处理对话。插件不处理的那种请求不经过它,不论 `on_error` 怎么设。其他接口(图片、音频等)不经过任何插件。
+
+
+
+
+| 字段 | 类型 | 默认值 | 说明 |
+|---|---|---|---|
+| `id` | 字符串 | **必填** | 小写字母、数字和连字符,1 到 40 个字符,不能重复。`order`、`inspect`、`rewrite` 和 `confirmed` 被控制面占用。 |
+| `file` | 字符串 | **必填** | 插件的代码,相对本文件所在的目录。只能是 `plugins/.js`,由应用写入。 |
+| `sha256` | 字符串 | **必填** | 批准过的代码的 SHA-256,64 个小写十六进制字符。文件的哈希与它不符时插件停止运行,直到在应用里批准这次改动。批准过的代码另存在 `plugins/.approved/.js`。 |
+| `enabled` | 布尔 | `true` | 是否运行这个插件。`false`:插件保留,不参与任何请求。 |
+
+
+```yaml
+plugins:
+ - id: add-date
+ file: plugins/add-date.js
+ sha256: 9f2b6c0e4a1d8f3b7c5e2a9d6f1b4c8e3a7d0f5b2c9e6a1d4f8b3c7e0a5d2f9b
+ enabled: true
+```
+
+`plugins/add-date.js` 的开头:
+
+```js
+export const manifest = {
+ name: "Add date",
+ api: 1,
+ permissions: ["system"],
+ match: { clients: ["claude-code"], models: ["claude-*"], upstreams: [] },
+ on_error: "reject",
+ settings: {
+ note: { type: "string", label: "Note", value: "用中文回答。" },
+ },
+};
+```
+
## 环境变量
| 变量 | 作用 |
diff --git a/src/data/core-docs/manifest.json b/src/data/core-docs/manifest.json
index ebfc2d4..a8229fd 100644
--- a/src/data/core-docs/manifest.json
+++ b/src/data/core-docs/manifest.json
@@ -1,9 +1,9 @@
{
"repository": "ThinkWatchProject/ThinkWatch-Core",
- "ref": "v0.56.0",
+ "ref": "v0.59.0",
"files": {
- "docs/config.md": "fb110a0f8d78a579526ae526421f47552630da0464cf01cb85b6b4b19aa706da",
- "docs/config.zh-CN.md": "ec07cc704d3093eb0ee7984159171242afff1d3164368a4b465b04ab96bef943",
+ "docs/config.md": "0122d660347a3f0e398f42dc9942901996a70e4531ba77a741248e6f97f932fc",
+ "docs/config.zh-CN.md": "61c6668435399af55e8d385ce80e3e116c1420feff97440b898ec265f65174af",
"docs/server.md": "5e1e9b901bef2b46d417aea057a1db24c78457d3c1938b8540ecb4766515b68f",
"docs/server.zh-CN.md": "1f508e7c39b8ded28653773ca4d8701e6bdc2247bcd359c3a8fd00fd1401a6ff"
}
diff --git a/src/i18n/index.ts b/src/i18n/index.ts
index d17e665..259cc7d 100644
--- a/src/i18n/index.ts
+++ b/src/i18n/index.ts
@@ -117,7 +117,7 @@ const dict = {
{ title: "Custom roles and SSO/OIDC", body: "Five built-in roles, from Super Admin to Viewer, and any number of custom roles. A new role can start from an existing one, its policy can be edited as JSON in the console, and every change is kept in the role's history. A role can also be granted for a single team. Sign-in works with Zitadel, Okta, Azure AD, or any OIDC provider, with optional TOTP." },
{ title: "AES-256-GCM at rest", body: "Provider keys, MCP tokens, and other upstream secrets are encrypted at rest with AES-256-GCM. Virtual API keys are stored as HMAC-SHA256 hashes, and the plaintext is shown only once." },
{ title: "HttpOnly cookie sessions", body: "Access and refresh tokens are stored in HttpOnly cookies, which JavaScript cannot read. Each console session also signs its requests with a key bound to the IP address it signed in from, so a stolen cookie cannot be replayed from a different network." },
- { title: "Content filtering & PII redaction", body: "Deny rules check the caller's messages, including built-in rules for common prompt-injection phrases, and each rule blocks, warns, or logs. PII such as email addresses, phone numbers, and card numbers is replaced with placeholders before a request goes upstream and restored in the answer. Rules and patterns can be tried out in the console." },
+ { title: "Request guards", body: "Credentials and personal information can be replaced with placeholders before a request goes upstream and restored in the answer, and a tool call that downloads and runs code or sends out credentials can be cut off before the client runs it. A content filter can refuse prompt injection or delete hidden characters by rule; every guard starts in observe mode, and its rules can be tried out in the console." },
{ title: "Distroless containers", body: "The server image holds one statically linked binary on a distroless base, with no shell. The server refuses to start with a JWT secret shorter than 32 characters, and deleted users, keys, and providers are purged after 30 days." },
{ title: "Model allowlists on every API", body: "A request may use only the models allowed by both the API key's allowlist and the user's roles. The same check applies to OpenAI Chat Completions, OpenAI Responses, Anthropic Messages, and Gemini requests, so no API bypasses it." },
],
@@ -133,7 +133,7 @@ const dict = {
{ title: "Health & readiness", body: "/health/live, /health/ready (which checks PostgreSQL, Redis, ClickHouse, and at least one active provider), and /api/health with per-dependency latency and connection-pool statistics." },
{ title: "Unified log explorer", body: "Search audit, gateway, MCP, access, and application logs from a single page. Each cell offers buttons to filter on or exclude its value, -key:value excludes a value in the query, and the query is kept in the URL." },
{ title: "Live dashboard", body: "The console overview receives live data over a WebSocket: requests per minute, each provider's latency, success rate, and circuit-breaker state, the most active users, and the latest requests, without a page refresh." },
- { title: "Full-body audit capture", body: "By default, request and response bodies of gateway calls, and the arguments and results of MCP tool calls, are stored in ClickHouse with each log row, compressed and kept for their own retention period. PII redaction before storage can be turned on. Bodies above the inline size limit are moved to S3-compatible storage such as MinIO or the bundled RustFS. Auditors read bodies in the log detail panel or search across them; access requires the separate logs:read_bodies permission." },
+ { title: "Full-body audit capture", body: "By default, request and response bodies of gateway calls, and the arguments and results of MCP tool calls, are stored in ClickHouse with each log row, compressed and kept for their own retention period. Bodies can be redacted with the outbound redaction rules before they are stored. Bodies above the inline size limit are moved to S3-compatible storage such as MinIO or the bundled RustFS. Auditors read bodies in the log detail panel or search across them; access requires the separate logs:read_bodies permission." },
],
},
{
@@ -349,7 +349,7 @@ const dict = {
{ title: "自定义角色 + SSO/OIDC", body: "内置从超级管理员到只读用户共五个角色,并可创建任意数量的自定义角色。新角色可从现有角色复制起步,权限策略可在控制台中以 JSON 编辑,每次修改都保留在角色的变更历史中。角色也可仅针对某一团队授予。登录支持 Zitadel、Okta、Azure AD 或任意 OIDC Provider,可选 TOTP 二次验证。" },
{ title: "AES-256-GCM 静态加密", body: "Provider 密钥、MCP 令牌及其他上游凭据以 AES-256-GCM 加密存储。虚拟 API 密钥以 HMAC-SHA256 哈希存储,明文只展示一次。" },
{ title: "HttpOnly Cookie 会话", body: "访问令牌和刷新令牌均存储在 HttpOnly Cookie 中,JavaScript 无法读取。每个控制台会话还会用一把绑定登录时 IP 地址的密钥为请求签名,被盗的 Cookie 无法在其他网络中重放。" },
- { title: "内容过滤与 PII 脱敏", body: "禁止规则检查调用方发送的消息,内置常见提示词注入短语的规则,每条规则可设为拦截、警告或仅记录。邮箱、电话号码、银行卡号等 PII 在请求发往上游前替换为占位符,并在回答中还原。规则与匹配模式可在控制台中试用。" },
+ { title: "请求防护", body: "凭据和个人信息可在请求发往上游前替换为占位符,并在回答中还原;下载即执行、外发凭据之类的工具调用可在客户端执行前切断。内容过滤可按规则拒绝提示注入或删除隐藏字符;各项防护出厂为观察档,规则可在控制台中试用。" },
{ title: "Distroless 容器", body: "服务端镜像只包含一个静态链接的二进制程序,基于 distroless 基础镜像,不含 shell。JWT 密钥短于 32 个字符时服务端拒绝启动;已删除的用户、密钥与 Provider 在 30 天后彻底清除。" },
{ title: "模型白名单覆盖全部 API", body: "请求只能使用同时被 API 密钥白名单与用户角色允许的模型。该检查对 OpenAI Chat Completions、OpenAI Responses、Anthropic Messages 与 Gemini 请求一致生效,任何 API 均无法绕过。" },
],
@@ -365,7 +365,7 @@ const dict = {
{ title: "健康与就绪", body: "/health/live、/health/ready(检查 PostgreSQL、Redis、ClickHouse 以及至少一个启用的 Provider),以及提供各依赖延迟与连接池统计的 /api/health。" },
{ title: "统一日志检索", body: "在同一页面搜索审计、网关、MCP、访问与应用日志。每个单元格提供按其值筛选或排除的按钮,查询中可用 -key:value 排除指定值,查询条件保存在 URL 中。" },
{ title: "实时看板", body: "控制台总览通过 WebSocket 接收实时数据:每分钟请求数,各 Provider 的延迟、成功率与熔断状态,最活跃的用户以及最新请求,无需刷新页面。" },
- { title: "全量请求 / 响应体审计", body: "默认情况下,网关调用的请求体与响应体、MCP 工具调用的参数与结果,随每条日志一并存入 ClickHouse,压缩存储并单独设置保留期限。可开启写入前 PII 脱敏。超过内联大小上限的内容转存到 S3 兼容存储,如 MinIO 或内置的 RustFS。审计员可在日志详情面板中查看,也可跨日志搜索;查看需要单独的 logs:read_bodies 权限。" },
+ { title: "全量请求 / 响应体审计", body: "默认情况下,网关调用的请求体与响应体、MCP 工具调用的参数与结果,随每条日志一并存入 ClickHouse,压缩存储并单独设置保留期限。可在写入前按出站脱敏的规则脱敏。超过内联大小上限的内容转存到 S3 兼容存储,如 MinIO 或内置的 RustFS。审计员可在日志详情面板中查看,也可跨日志搜索;查看需要单独的 logs:read_bodies 权限。" },
],
},
{
diff --git a/src/i18n/pages/core.ts b/src/i18n/pages/core.ts
index 9625e96..acb7cb3 100644
--- a/src/i18n/pages/core.ts
+++ b/src/i18n/pages/core.ts
@@ -58,7 +58,7 @@ export const coreCopy = {
},
{
title: "Malicious tool calls cut off",
- body: "A relay can rewrite an answer and slip in a tool call for the client to run. Tool calls in an answer are checked against rules for download-and-run, sending out credentials and similar commands, and a dangerous call can be cut off mid-stream. Together with checks for hidden characters, content rules and an output limit, these form five guards, each set to off, observe or enforce.",
+ body: "A relay can rewrite an answer and slip in a tool call for the client to run; a call that downloads and runs code, sends out credentials or does something similar can be cut off mid-stream. A content filter can also delete hidden characters that smuggle in instructions, and all three guards start by only recording.",
},
{
title: "Encrypted control plane",
@@ -181,8 +181,8 @@ export const coreCopy = {
body: "中转站能看到请求的全部内容。请求发出之前,其中的凭据可以替换为占位符,中转站拿不到原值;回答中重复出现时再恢复原值。",
},
{
- title: "拦截恶意工具调用",
- body: "中转站可以改写回答,塞入让客户端执行的工具调用。回答中的工具调用按下载即执行、外发凭据等危险命令规则审查,高危调用可在流式传输中途截断。它与隐藏字符检查、内容规则和输出长度限制合为五项防护,每项可设为关闭、观察或拦截。",
+ title: "切断恶意工具调用",
+ body: "中转站可以改写回答,塞入让客户端执行的工具调用;下载即执行、外发凭据之类的危险调用可在流式传输中途切断。内容过滤还可删除夹带指令的隐藏字符,三项防护出厂均只记录。",
},
{
title: "加密的控制面",
diff --git a/src/i18n/pages/lite.ts b/src/i18n/pages/lite.ts
index 384e2f3..cd7d62c 100644
--- a/src/i18n/pages/lite.ts
+++ b/src/i18n/pages/lite.ts
@@ -1,8 +1,8 @@
// Copy for the /lite product page.
//
// Product claims are checked against the code, not against other copy: the
-// five protections and their initial modes against ThinkWatch Core
-// (crates/tw-config/src/security.rs), which notices become system
+// three protections and their initial modes against ThinkWatch Core
+// (crates/tw-guard/src/policy.rs), which notices become system
// notifications against the app (src-tauri/src/notices/rules.rs), and what
// changes while the app is connected to a remote core against src/connection.
//
@@ -122,7 +122,7 @@ export const liteCopy = {
{
id: "security",
title: "Protection against relays: keys replaced, malicious tool calls cut off",
- body: "A relay sees every request in full and can rewrite every answer. Outbound redaction swaps API keys, private keys, JWTs, connection-string passwords, Chinese resident ID numbers and bank card numbers for placeholders before a request leaves and restores them in the response, so the relay never holds the real values. When an answer carries a tool call that downloads and runs code, sends out environment variables or credential files, reads private keys or installs a startup item or scheduled job, tool-call inspection cuts the answer off before the client can run it; hidden characters and prompt injection can be refused as well. The five protections start in Observe, recording without changing anything, and each switches to Enforce on its own.",
+ body: "API keys, private keys, connection-string passwords, ID numbers and bank card numbers can be replaced with placeholders before a request leaves, so a relay never holds the real values, and an answer whose tool call downloads and runs code or sends out credentials can be cut off before the client runs it. Hidden characters that smuggle in instructions can be deleted and prompt injection refused; the three protections start in Observe, recording without changing anything, and each switches to Replace, Cut off or Enforce on its own.",
alt: "The Security page log: credentials replaced before a request left, one of them matched by a custom rule; a download-and-run tool call cut off; and hidden characters, a delete command and an injected instruction recorded, each with the key, client, model and upstream of its request",
},
{
@@ -209,7 +209,7 @@ export const liteCopy = {
intro: "Lite holds no routing, forwarding or accounting logic. All of it is in ThinkWatch Core, which runs beside the app or on a Linux server.",
nodes: [
{ role: "Desktop app", title: "ThinkWatch Lite", items: ["Main window, menu bar and tray", "Starts and supervises the local core", "Or connects to a core on a server"] },
- { role: "Gateway", title: "ThinkWatch Core", items: ["Routing, failover and format conversion", "Cost accounting and request records", "The five security protections"] },
+ { role: "Gateway", title: "ThinkWatch Core", items: ["Routing, failover and format conversion", "Cost accounting and request records", "The three security protections"] },
{ role: "Upstreams", title: "Model services", items: ["Anthropic, OpenAI and Gemini APIs", "Amazon Bedrock and signed-in accounts", "Relays and local models"] },
],
links: ["Encrypted control channel", "Forwards requests"],
@@ -356,8 +356,8 @@ export const liteCopy = {
},
{
id: "security",
- title: "防范中转站:替换密钥,拦截恶意工具调用",
- body: "中转站能看到请求的全部内容,也能改写每一次回答。出站脱敏在请求发出前把 API 密钥、私钥、JWT、连接串口令、身份证号与银行卡号换成占位符,并在响应中还原,中转站拿不到原值。回答中若出现下载即执行、外发环境变量或凭据文件、读取私钥、写入开机启动项或定时任务之类的工具调用,工具调用审查会在客户端执行之前切断回答;隐藏字符与提示注入也可以直接拒绝。五项防护出厂只记录、不改动请求,逐项切换到拦截即可生效。",
+ title: "防范中转站:替换密钥,切断恶意工具调用",
+ body: "API 密钥、私钥、连接串口令、身份证号与银行卡号可在请求发出前换成占位符,中转站拿不到原值;回答中的工具调用若是下载即执行或外发凭据,可在客户端执行之前切断。夹带指令的隐藏字符可以删除,提示注入可以拒绝;三项防护出厂只记录、不改动请求,可逐项切换到「替换」「切断」或「处置」。",
alt: "安全页日志:请求发出前替换的凭据(其中一条由自定义规则命中)、被切断的下载即执行工具调用,以及记录在案的隐藏字符、删除命令与注入指令,每条都注明所属请求的密钥、客户端、模型与上游",
},
{
@@ -444,7 +444,7 @@ export const liteCopy = {
intro: "Lite 不含路由、转发与计费逻辑,这些都在 ThinkWatch Core 中;core 随应用在本机运行,也可以部署在 Linux 服务器上。",
nodes: [
{ role: "桌面应用", title: "ThinkWatch Lite", items: ["主窗口、菜单栏与托盘", "启动并守护本机的 core", "或连接服务器上的 core"] },
- { role: "网关", title: "ThinkWatch Core", items: ["路由、故障转移与格式转换", "费用核算与请求记录", "五项安全防护"] },
+ { role: "网关", title: "ThinkWatch Core", items: ["路由、故障转移与格式转换", "费用核算与请求记录", "三项安全防护"] },
{ role: "上游", title: "模型服务", items: ["Anthropic、OpenAI、Gemini API", "Amazon Bedrock 与登录的账号", "中转服务与本机模型"] },
],
links: ["加密控制通道", "转发请求"],