From 2077975c4b8556be07fbf9be89089fbc9a59e32a Mon Sep 17 00:00:00 2001 From: fylorn <249551762+fylorn@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:06:07 +0800 Subject: [PATCH] v0.73.0 Per-rule actions for every guard, content rules for answers (strip and replace), record-only redaction. Protocol 48. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 38 +++++++++++++++++++------------------- Cargo.toml | 2 +- release-notes/0.73.0.md | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 20 deletions(-) create mode 100644 release-notes/0.73.0.md diff --git a/Cargo.lock b/Cargo.lock index 75259ae..05cb3b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3143,7 +3143,7 @@ dependencies = [ [[package]] name = "tw-api" -version = "0.72.0" +version = "0.73.0" dependencies = [ "serde", "serde_json", @@ -3154,7 +3154,7 @@ dependencies = [ [[package]] name = "tw-bedrock" -version = "0.72.0" +version = "0.73.0" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -3175,7 +3175,7 @@ dependencies = [ [[package]] name = "tw-breaker" -version = "0.72.0" +version = "0.73.0" dependencies = [ "serde", "serde_json", @@ -3183,7 +3183,7 @@ dependencies = [ [[package]] name = "tw-config" -version = "0.72.0" +version = "0.73.0" dependencies = [ "blake3", "libc", @@ -3208,7 +3208,7 @@ dependencies = [ [[package]] name = "tw-control" -version = "0.72.0" +version = "0.73.0" dependencies = [ "axum", "base64", @@ -3251,7 +3251,7 @@ dependencies = [ [[package]] name = "tw-dialect" -version = "0.72.0" +version = "0.73.0" dependencies = [ "memchr", "serde", @@ -3260,7 +3260,7 @@ dependencies = [ [[package]] name = "tw-engine" -version = "0.72.0" +version = "0.73.0" dependencies = [ "chrono", "serde", @@ -3274,7 +3274,7 @@ dependencies = [ [[package]] name = "tw-gateway" -version = "0.72.0" +version = "0.73.0" dependencies = [ "arc-swap", "async-stream", @@ -3325,7 +3325,7 @@ dependencies = [ [[package]] name = "tw-guard" -version = "0.72.0" +version = "0.73.0" dependencies = [ "aho-corasick", "base64", @@ -3343,7 +3343,7 @@ dependencies = [ [[package]] name = "tw-link" -version = "0.72.0" +version = "0.73.0" dependencies = [ "serde", "serde_json", @@ -3357,7 +3357,7 @@ dependencies = [ [[package]] name = "tw-observe" -version = "0.72.0" +version = "0.73.0" dependencies = [ "tokio", "tracing", @@ -3366,7 +3366,7 @@ dependencies = [ [[package]] name = "tw-plugin" -version = "0.72.0" +version = "0.73.0" dependencies = [ "libc", "rand 0.10.2", @@ -3381,7 +3381,7 @@ dependencies = [ [[package]] name = "tw-pricing" -version = "0.72.0" +version = "0.73.0" dependencies = [ "arc-swap", "flate2", @@ -3394,14 +3394,14 @@ dependencies = [ [[package]] name = "tw-secret" -version = "0.72.0" +version = "0.73.0" dependencies = [ "thiserror", ] [[package]] name = "tw-store" -version = "0.72.0" +version = "0.73.0" dependencies = [ "blake3", "bytes", @@ -3421,7 +3421,7 @@ dependencies = [ [[package]] name = "tw-types" -version = "0.72.0" +version = "0.73.0" dependencies = [ "serde", "serde_json", @@ -3430,7 +3430,7 @@ dependencies = [ [[package]] name = "tw-watch" -version = "0.72.0" +version = "0.73.0" dependencies = [ "libc", "notify", @@ -3441,7 +3441,7 @@ dependencies = [ [[package]] name = "tw-yaml" -version = "0.72.0" +version = "0.73.0" dependencies = [ "saphyr-parser", "serde_yaml_ng", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "twcore" -version = "0.72.0" +version = "0.73.0" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index 93c2776..6a343e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ categories = ["network-programming", "web-programming::http-server"] # 二进制走 CalVer,crate 走 SemVer —— 两者是两回事,卖给不同的人。 # 这里是 crate 的版本。 -version = "0.72.0" +version = "0.73.0" [workspace.dependencies] # ── 内部 crate ─────────────────────────────────────────── diff --git a/release-notes/0.73.0.md b/release-notes/0.73.0.md new file mode 100644 index 0000000..007e23f --- /dev/null +++ b/release-notes/0.73.0.md @@ -0,0 +1,37 @@ +This release gives every rule of every guard its own action, lets content rules check the text of the model's answers as well as requests, and adds a record-only action that reports a match without changing anything. + +**Upgrade notes** + +- The control-plane protocol version (`CONTROL_API_VERSION`) goes from 47 to 48. ThinkWatch Lite connects only to a core with the same protocol version: ThinkWatch Lite 2026.10.18 includes 0.72.0 (protocol 47) and does not connect to 0.73.0. A server used with it stays on 0.72.0 until the app is updated to a release that includes 0.73.0; `sudo twcore upgrade --version 0.72.0 --restart` switches a server back. +- The request store's schema is unchanged; nothing is cleared. +- Configuration: existing configurations load unchanged and keep their behaviour. New optional keys: `actions` and a per-rule `action` under `security.redact`; `scope`, `case_sensitive` and `replacement` on content rules; the content action `replace`. A configuration that uses any of them does not load in 0.72.x. +- A content rule whose `scope` includes `response` cannot use `action: block` or `match: codepoints`; such a configuration is refused with its own message. +- New message codes: `config.rule_scope_block`, `config.rule_scope_codepoints`, `config.rule_scope_empty`, `security.rule_scope_block`, `security.rule_scope_codepoints`, `security.rule_scope_empty`, `security.redact_action_unknown`, `security.unknown_custom_rule`. `security.content_action_unknown` is renamed `security.content_action_bad` (its sentence now lists `replace`); `security.no_action_of_its_own` is removed. +- Changed in the protocol: + - `RuleAction` gains `placeholder` and `replace`; new type `ContentScope` (`request` or `response`). + - Rule views carry `scope` and `replacement` for content rules, the real `case_sensitive` of content matchers, and `action` / `default_action` for redaction rules. + - `CustomRuleSave` gains `scope` and `replacement`; `case_sensitive` applies to content rules and `action` to redaction rules. `SetBuiltinRuleAction` accepts redaction rules. + - New endpoint `PUT /security/{guard}/rules/enabled` (`SetRulesEnabled`) switches several built-in and custom rules on or off with one configuration write; unknown ids or names are refused and nothing is written. + - `SecurityTestRequest` gains `scope` (try a sample as a request or as an answer) and `replacement`. + - `ContentMatched` gains `direction`; its outcome can be `replaced`. `SecretItem` gains `replaced`. `OutcomeDetail` for a content replacement carries `segments` and `replacement`. `SecurityCounts` gains `content_replaced`. + +**Actions per rule.** Every rule now has its own action, and a guard's mode decides whether the actions apply: under `enforce` each rule does what it says, under `observe` every rule only records, under `off` nothing is checked. +- Outbound redaction: `placeholder` (replace with a placeholder and put the value back in the answer, as before) or `record` (report the value and send it unchanged; it never enters the ledger). Where a record-only match overlaps a placeholder match, the placeholder match still applies. +- Tool-call inspection: `cut` or `record`, as before. +- Content filter: `block` (requests only), `strip`, `replace` or `record`. `replace` writes the rule's `replacement` text (default `***`) in place of the match, on requests as well as answers, and is not reversed in the answer. + +**Content rules for answers.** A custom content rule's `scope` lists `request`, `response` or both. A rule on `response` checks the text the client receives: streamed answers in every supported format (including Codex freeform tool input), whole answers and the Responses WebSocket. Tool calls are left to tool-call inspection and thinking is not checked. Matching is the same as for requests, with `case_sensitive` (off by default). +- Streamed answers are not buffered. Each text holds back only the characters that could still grow into a match: the longest keyword less one, or a regular expression's longest possible match, at most 256 characters; deleting rules hold back as many again. A match longer than that may have its start sent already. +- Deleting repeats until no match remains, so deleting one match cannot form another. Whole answers always come out clean. A stream gives the same result where a match is nested one level deep; with deeper nesting the start of a match may already have been sent, and the rest is deleted. +- Regular expressions anchored to the start or end of a text or line are checked once, on the model's own text. +- Copies of the answer text in the same stream (closing events that repeat the whole text) are rewritten to match what was sent. When an answer rule can delete or replace, logprobs are removed from streamed answers and from whole answers whose text changed, since they repeat the text word by word. +- Held text is sent when the stream ends, breaks off or carries an upstream error. +- The stored answer is the one the client received. + +**Requests.** Deleting and replacing on requests repeat until no match remains; a part still changing after 64 rounds is emptied. Case-insensitive matching uses the same method for requests and answers, so characters whose lowercase form has a different length are matched correctly. + +**Redaction fixes.** Overlapping placeholder matches merge into one placeholder, so no part of either is sent in clear. Custom redaction patterns match within one JSON string at a time and can no longer make a request body invalid JSON. Matches on placeholders already in the text are ignored before overlapping matches merge. + +**Stored bodies and security log.** Stored bodies and log excerpts put the placeholder back wherever a request value appears as written, so a value the model repeats without its context (a connection-string password, for example) is not stored in clear. In valid JSON only string contents are changed; text that is not valid JSON, such as an event stream, is masked wherever a value appears, numbers included. Excerpts never show part of a value. + +**WebSocket.** A frame from an answer that has already ended is filtered by that answer's own filter and never changes the answer in progress, also when a relay rewrites answer ids.