Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
a890511
chore: pin the core dependency to a tag instead of tracking main (#25)
fylorn Sep 23, 2026
a90e9f3
feat!: forward what can be forwarded, convert only what must be (#26)
fylorn Sep 23, 2026
ca7647c
refactor: take the circuit-breaker registry and metric labels back fr…
fylorn Sep 23, 2026
a6678cf
refactor: redact PII with core's guard engine, and restore tool argum…
fylorn Sep 23, 2026
437a671
feat: inspect the tool calls an upstream returns (#29)
fylorn Sep 23, 2026
038796b
build: keep only line tables in dev and test builds (#30)
fylorn Sep 24, 2026
05f52e8
feat: one circuit-breaker state machine, and hidden characters in req…
fylorn Sep 24, 2026
8a76395
fix: make the integration suite pass again (#32)
fylorn Sep 24, 2026
6482690
test: fix the two flaky integration tests (#33)
fylorn Sep 24, 2026
9f46116
fix: bill a Chat stream whose caller did not ask for usage (#34)
fylorn Sep 24, 2026
3c35ab8
fix: a request the upstream refuses no longer fails over or trips bre…
fylorn Sep 24, 2026
234fcce
Merge main (v1.1.0) into dev
fylorn Sep 24, 2026
82f8061
docs: cut releases from a release branch, and keep the tag's headings…
fylorn Sep 24, 2026
bd57b5d
ci: run checks on pull requests into dev, including the integration s…
fylorn Sep 24, 2026
c1b2085
fix: bill cached input at cache prices, estimate missing usage, strip…
fylorn Sep 24, 2026
ad5b75b
refactor: take back what only this side used from core (#41)
fylorn Sep 24, 2026
3a617f3
feat(gateway): take official hosts, output fallback and error shapes …
fylorn Sep 24, 2026
ed1c8b5
feat(gateway): accept Gemini clients, and the Responses API over a We…
fylorn Sep 24, 2026
28d4387
test: count the probes that miss before the streaming cache hit (#44)
fylorn Sep 24, 2026
d3f36fc
refactor(server): move the catalog's SQL into repositories (#45)
fylorn Sep 24, 2026
83e9bcc
refactor(server): move the dashboard, limits and log-forwarding handl…
fylorn Sep 24, 2026
9b5b326
refactor(server): move the MCP handlers' SQL into repositories (#50)
fylorn Sep 24, 2026
86beb82
refactor(gateway): run the request guards on core's tw-guard engines …
fylorn Sep 24, 2026
1c80b83
refactor(server): move the identity handlers' SQL into repositories (…
fylorn Sep 24, 2026
c25b44f
fix(mcp): revoking a default connection no longer fails with a 500 (#51)
fylorn Sep 24, 2026
dbce845
test: leave the cancelled stream after it has started, not on a timer…
fylorn Sep 24, 2026
c3d2d57
refactor(server): move the access handlers' SQL into repositories (#48)
fylorn Sep 24, 2026
d92c2d1
fix(settings): read security.totp_required as a boolean, and seed aut…
fylorn Sep 24, 2026
7e95183
feat(gateway): record a client that leaves before its response exists…
fylorn Sep 24, 2026
367df3c
feat(auth): enforce the TOTP requirement (#55)
fylorn Sep 24, 2026
443de8d
feat(gateway): keep the last response on a Responses WebSocket (#56)
fylorn Sep 24, 2026
6890c72
Merge main (v2.0.0) into dev
fylorn Sep 24, 2026
d4f6d5a
docs(release): CI runs the integration suite on the release PR (#58)
fylorn Sep 24, 2026
ef3de87
docs(contributing): point vulnerability reports at the organization's…
fylorn Sep 25, 2026
c4f8b1c
docs(readme): current description of ThinkWatch Lite and ThinkWatch C…
fylorn Sep 25, 2026
f259962
feat(providers): authenticate Bedrock with an API key (#61)
fylorn Sep 28, 2026
78f9ab5
test: hold the early-cancel client once its key is known, not on a ti…
fylorn Sep 28, 2026
1b752f2
feat(providers): list Bedrock's models, and let the route editor take…
fylorn Sep 28, 2026
1f08421
fix(bedrock): refuse keys that will not decrypt, and keep instance-ro…
fylorn Sep 28, 2026
e480bfd
refactor(bedrock): use core's shared tw-bedrock, and core v0.55.0 (#65)
fylorn Sep 29, 2026
e1e7999
docs(readme): rewrite both READMEs to be short and accurate (#66)
fylorn Sep 30, 2026
af9eb81
Merge main (v2.1.0) into dev
fylorn Sep 30, 2026
ed6dd39
docs(readme): 37 MCP templates, what the setup wizard does, body reda…
fylorn Sep 30, 2026
9afcc08
fix(rbac): make the seeded team_manager work at team scope (#69)
fylorn Sep 30, 2026
fa3a5b9
fix(rbac): require gateway use, and count only grants that give it (#70)
fylorn Sep 30, 2026
a1eed6f
Merge main (v2.2.0) into dev
fylorn Sep 30, 2026
da88b09
Merge main (sponsor line) into dev
fylorn Oct 2, 2026
1a399e7
Allow clippy::double_must_use on the async_trait BlobStore (#73)
fylorn Oct 2, 2026
8d8d96f
Unify the request guards with thinkwatch-core's rule model (#72)
fylorn Oct 3, 2026
2baa123
Fix what the pre-release review of the guard unification found (#75)
fylorn Oct 3, 2026
33c22dc
Merge main (v3.0.0) into dev
fylorn Oct 3, 2026
dcc8616
Merge main (README: ThinkWatch Lite for individual developers) into dev
fylorn Oct 4, 2026
6b73344
Merge main (v3.1.0) into dev
fylorn Oct 5, 2026
03c393c
Usage limits: token limits refuse, key limits count on the key, Retry…
fylorn Oct 5, 2026
4075855
fix(redis): connect over TLS for rediss:// URLs (#82)
fylorn Oct 5, 2026
33234ee
Concurrent startup, body retention on restart, and Helm network polic…
fylorn Oct 5, 2026
b7a34e1
Merge main (v3.2.0) into dev
fylorn Oct 5, 2026
2ce6471
chore(deps): update rust crate rand to v0.10.1 [security] (#77)
renovate[bot] Oct 6, 2026
0682864
chore(deps): update rust crate xxhash-rust to v0.8.16 [security] (#78)
renovate[bot] Oct 6, 2026
1fbe7bd
chore(deps): take the Renovate dashboard upgrades (#85)
fylorn Oct 6, 2026
2e23fd9
chore(deps): pnpm 12, and ClickHouse row validation on (#86)
fylorn Oct 6, 2026
977cab0
ci: save Rust caches only on pushes, and let releases read CI's image…
fylorn Oct 8, 2026
a19ed65
Merge main (v3.2.1) into dev
fylorn Oct 8, 2026
ea2a81f
Merge main (v3.3.0) into dev
fylorn Oct 9, 2026
e8ae1f1
Stream endings and in-stream errors logged like answers; core v0.68.0…
fylorn Oct 10, 2026
0e070a9
Merge main (v3.3.1) into dev
fylorn Oct 10, 2026
633ad90
Merge main (v3.4.0) into dev
fylorn Oct 10, 2026
235e38b
fix(limits): enforce route caps and close the places limits leaked (#94)
fylorn Oct 10, 2026
31cbfc8
feat: GET /v1/usage answers for the key or its owner; API key limits …
fylorn Oct 10, 2026
8151cee
Merge main (v3.5.0) into dev
fylorn Oct 10, 2026
721b5ee
Merge main (README zh) into dev
fylorn Oct 10, 2026
4f87839
feat(console): Add to ThinkWatch Lite; gateway address for every user…
fylorn Oct 11, 2026
042e605
Merge main (v3.6.0) into dev
fylorn Oct 11, 2026
ebdb9dc
chore: add a contributor license agreement signed by PR comment (#102)
fylorn Oct 11, 2026
133d610
ci: name the CLA job cla (#103)
fylorn Oct 12, 2026
4e28df8
Redaction rules: match text as written or by regex, case-sensitive on…
fylorn Oct 12, 2026
247915a
chore(release): tag 3.7.0
fylorn Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .github/workflows/cla.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Contributor License Agreement (CLA.md).
#
# On a contributor's first pull request the bot comments with a link to
# CLA.md and the phrase to sign with. Commenting the phrase once records
# the signature in signatures/cla.json on the `cla-signatures` branch of
# this repository, and it covers every later pull request here.
#
# `pull_request_target` rather than `pull_request` because a PR from a
# fork gets a read-only token under the latter, and this workflow has to
# comment and write the signature file. That trigger runs with the base
# repository's token, so this workflow **must never check out or execute
# PR code**. It doesn't: no actions/checkout, no build; the action reads
# PR metadata and comments through the API.
#
# Two consequences of the triggers worth knowing:
# - `issue_comment` always runs the copy of this file on the default
# branch (`main`), so signing by comment takes effect immediately only
# once this file is on `main`. Until then a signature comment is picked
# up by the next run that `pull_request_target` starts (a push to the PR).
# - Only `GITHUB_TOKEN` is used: no personal access token, no
# cross-repository token. That is why signatures live in this repository.
name: CLA

on:
pull_request_target:
types: [opened, synchronize, reopened, closed]
issue_comment:
types: [created]

# What the action calls: comments and the post-merge lock (pull-requests),
# the signature file (contents), and re-running the failed check after a
# signature (actions). It never uses the commit status API, so no
# `statuses` here.
permissions:
actions: write
contents: write
pull-requests: write

jobs:
cla:
runs-on: ubuntu-latest
# Pull request events, except a close without a merge (nothing to lock),
# and comments on pull requests that are exactly the signature or
# `recheck`. Expression comparisons ignore case.
if: >-
(github.event_name == 'pull_request_target' &&
(github.event.action != 'closed' || github.event.pull_request.merged)) ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request &&
(github.event.comment.body == 'recheck' ||
github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA'))
steps:
# The action cannot start the record itself: it compares the 404 it
# gets for a missing file with the string "404", so a missing branch
# or file fails every run. Create the branch once, as an orphan that
# holds only the signature file. No workflow lives on it, and writes
# made with GITHUB_TOKEN start no workflow runs anyway, so recording
# a signature triggers no CI.
- name: Create the signature branch if missing
if: github.event.action != 'closed'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: cla-signatures
run: |
set -euo pipefail
if gh api "repos/$REPO/branches/$BRANCH" --silent 2>/dev/null; then
exit 0
fi
tree=$(jq -n '{tree: [{path: "signatures/cla.json", mode: "100644", type: "blob",
content: "{\n \"signedContributors\": []\n}\n"}]}' |
gh api "repos/$REPO/git/trees" --input - --jq .sha)
commit=$(jq -n --arg tree "$tree" \
'{message: "Start the CLA signature record", tree: $tree, parents: []}' |
gh api "repos/$REPO/git/commits" --input - --jq .sha)
# A concurrent run may have created it in the meantime; that is fine.
gh api "repos/$REPO/git/refs" -f ref="refs/heads/$BRANCH" -f sha="$commit" --silent ||
gh api "repos/$REPO/branches/$BRANCH" --silent

- name: Check signatures
# v2.6.1
uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
path-to-document: https://github.com/ThinkWatchProject/ThinkWatch/blob/main/CLA.md
branch: cla-signatures
path-to-signatures: signatures/cla.json
custom-pr-sign-comment: I have read the CLA Document and I hereby sign the CLA
# The maintainer, and bots, which cannot sign. `*[bot]` is a
# wildcard the action supports; the named bots are listed anyway.
allowlist: fylorn,dependabot[bot],renovate[bot],github-actions[bot],*[bot]
# Lock the conversation after a merge, as the action recommends,
# so the signature comments on it stay as they were.
lock-pullrequest-aftermerge: true
100 changes: 99 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,103 @@ target.

## [Unreleased]

## [3.7.0] — 2026-10-12

Outbound redaction keeps away from the structure of a request: it no longer
looks inside field names, and custom rules leave alone the identifiers a
protocol needs as they are, so a rule can no longer break the ids a client
sends back with its history. A custom redaction rule now says whether it
looks for its text as written or for a regular expression, and whether case
matters. The thinkwatch-core crates move from v0.70.0 to v0.71.0.

### Read before upgrading

- **Custom redaction rules saved before 3.7.0 now match their text as
written, ignoring case.** A custom rule in `security.redact` takes two new
optional keys, `match` (`contains` or `regex`) and `case_sensitive`. A
rule without `match` is looked for as plain text, in which `.`, `*`, `\d`
and the like have no special meaning; a rule without `case_sensitive`
ignores case. Until now every custom redaction rule was a case-sensitive
regular expression, and no rule saved by 3.0 to 3.6 carries either key,
including the rules an earlier upgrade converted from the old
`security.pii_redactor_patterns` setting. A rule written as a regular
expression still loads and nothing reports it, but it stops matching:
what it used to replace is sent upstream as it is and, with
`audit.body_redact_pii` on, stored unmasked. Nothing is converted
automatically. **Once every replica runs 3.7.0, open each custom
redaction rule on the console's security page, set Match by to Regex
for one written as a regular expression, and tick Case-sensitive where
case mattered.** A policy written by a script or through
`PATCH /api/admin/settings` needs `"match": "regex"` and
`"case_sensitive": true` on each such rule. Settings still in the format
from before 3.0.0, converted on the first start of 3.7.0, become
case-sensitive regex rules and keep matching as before.
- **Upgrade every replica before editing redaction rules, and do not roll
back afterwards.** 3.6 refuses a redaction policy with keys it does not
know. A 3.6 instance that reads a policy in which any custom rule has
`match` or `case_sensitive` logs `Unreadable guard policy` and runs
redaction on the factory policy: observe mode, the built-in rules as
shipped, no custom rules, so nothing is replaced on the wire. Any change
to outbound redaction saved from the 3.7.0 console, even a mode switch,
writes `match` on every custom rule. During a rolling upgrade,
edit redaction rules only once no 3.6 instance is left; to roll back to
3.6 after editing them, first remove `match` and `case_sensitive` from
every custom rule in `security.redact`.
- **Redaction no longer searches field names, and custom rules skip
protocol identifiers.** No rule looks inside a JSON key any more. Custom
rules also leave alone the values of `id`, `call_id`, `item_id`,
`tool_use_id`, `tool_call_id`, `toolUseId`, `previous_response_id`,
`response_id`, `type`, `role`, `name`, `namespace`, `model`, `status`,
`object` and `phase`. Built-in rules still search those values, and every
other value is searched by every rule as before, including fields such as
`user_id` or `aws_access_key_id` inside tool arguments. A custom rule
meant to catch a value under one of those keys, such as a tool name or a
model name, no longer does. The same holds for masking stored bodies
(`audit.body_redact_pii`).
- **The private-key rule replaces less.** The header of a key block and its
end line are now looked for within the same text (see Fixed). A header
in one message and an end line in another are no longer replaced, along
with everything between them, as one key.

### Added

- **Match by and Case-sensitive for custom redaction rules.** The custom
rule dialog for outbound redaction offers Contains and Regex, as the
content filter's does, and a Case-sensitive checkbox. Contains looks for
the text exactly as written; leading and trailing spaces count. The rule
list says how each rule matches, such as "Contains `x`, ignoring case" or
"Regex `x`, case-sensitive". Trying a sample in the dialog uses the same
settings: `POST /api/admin/security/redact/test` takes `match` and
`case_sensitive`. Code points remain a content filter matcher and are
refused for redaction.

### Fixed

- **Custom redaction rules no longer break the ids a client sends back.**
Rules ran over the whole request body, field names and identifiers
included, so a custom rule could match inside an id: Codex sends ids such
as `fco_` followed by a hexadecimal UUID, and a rule whose text can be
spelled in hexadecimal turned one into `fco_…<<TW_SECRET_1>>…`. The
upstream rejected the request, and since the history is sent again on
every turn, every later turn of that conversation as well. See Read
before upgrading for what is no longer searched.
- **Private keys stay within one text.** The private-key rule looked for
the end of a key block anywhere after its header. A header without an
end in one message, such as a file read halfway or code that spells the
marker, joined up with an end line in a later message, and everything in
between, earlier turns included, was replaced by a single placeholder.

### Changed

- thinkwatch-core crates (tw-bedrock, tw-breaker, tw-dialect, tw-guard)
v0.70.0 → v0.71.0.
- The Licensor in `LICENSE` is now the project's owner, named in the file,
instead of "ThinkWatch Contributors"; the Business Source License 1.1
terms are unchanged.
- A first pull request to this repository asks its author to sign the
Contributor License Agreement (`CLA.md`) once, by a comment on the pull
request; `CONTRIBUTING.md` explains how.

## [3.6.0] — 2026-10-12

A key shown once can go straight into ThinkWatch Lite: the console's key
Expand Down Expand Up @@ -2022,7 +2119,8 @@ unreleased builds should: stop the gateway, run `db/schema.sql`
against PostgreSQL, restart against this tag. The schema is
idempotent end-to-end, so the apply is safe to repeat.

[Unreleased]: https://github.com/ThinkWatchProject/ThinkWatch/compare/v3.6.0...HEAD
[Unreleased]: https://github.com/ThinkWatchProject/ThinkWatch/compare/v3.7.0...HEAD
[3.7.0]: https://github.com/ThinkWatchProject/ThinkWatch/releases/tag/v3.7.0
[3.6.0]: https://github.com/ThinkWatchProject/ThinkWatch/releases/tag/v3.6.0
[3.5.0]: https://github.com/ThinkWatchProject/ThinkWatch/releases/tag/v3.5.0
[3.4.0]: https://github.com/ThinkWatchProject/ThinkWatch/releases/tag/v3.4.0
Expand Down
140 changes: 140 additions & 0 deletions CLA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# ThinkWatch Contributor License Agreement

Version 1.0

This agreement (the "Agreement") sets out the terms on which You contribute to
the ThinkWatch Project. Please read it before You sign. A Chinese translation is
in [CLA.zh-CN.md](CLA.zh-CN.md); the English text governs (section 12).

## 1. Definitions

- **"Licensor"** means the holder of the GitHub account @fylorn
(https://github.com/fylorn), and that holder's heirs, successors and assigns.
- **"You"** means the individual who accepts this Agreement.
- **"Covered Repositories"** means these repositories of the ThinkWatch Project,
including any repository one of them is later moved or renamed to:
- ThinkWatch: https://github.com/ThinkWatchProject/ThinkWatch
- ThinkWatch Lite: https://github.com/ThinkWatchProject/ThinkWatch-Lite
- ThinkWatch Core: https://github.com/ThinkWatchProject/ThinkWatch-Core
- **"Project"** means the software, documentation and other material in the
Covered Repositories, in every edition and version, and any product or
service the Licensor makes from them.
- **"Submit"** means to send material to a Covered Repository or to the
Licensor for inclusion in the Project, for example by opening a pull
request, pushing commits to a pull request, or posting a patch or proposed
text in an issue or review comment.
- **"Contribution"** means any work of authorship that You Submit, such as
code, documentation, images or translations, including changes to existing
material. Material that You clearly mark as "Not a Contribution" when You
Submit it is not a Contribution.

## 2. You keep Your copyright

You keep the copyright and all other rights in Your Contributions, except for
the licences You grant in this Agreement. Nothing in this Agreement transfers
ownership of Your copyright to the Licensor.

## 3. Copyright licence

You grant the Licensor a perpetual, worldwide, non-exclusive, royalty-free,
irrevocable licence to use, reproduce, modify, prepare derivative works of,
publicly display, publicly perform, distribute and sublicense Your
Contributions and derivative works of them.

This licence includes the right to license and relicense Your Contributions,
on their own or as part of the Project, under any terms the Licensor chooses.
Those terms may be open source licences such as the MIT License,
source-available licences such as the Business Source License, or proprietary
and commercial licences. The Licensor may move Your Contributions between
editions of the Project, may charge for licences to the Project, and does not
owe You any part of the income.

## 4. Patent licence

You grant the Licensor, and everyone who receives the Project or any part of it
from the Licensor or under a licence granted by the Licensor, a perpetual,
worldwide, non-exclusive, royalty-free patent licence to make, have made, use,
offer to sell, sell, import and otherwise transfer Your Contributions and the
Project. This licence is irrevocable, except as stated in the last paragraph of
this section.

The patent licence covers only those patent claims that You are able to
license and that are necessarily infringed by Your Contribution on its own, or
by Your Contribution combined with the Project as it stood when You Submitted
it.

If anyone starts patent litigation, including a cross-claim or counterclaim,
alleging that Your Contribution or the Project directly or indirectly infringes
a patent, every patent licence granted to that party under this Agreement for
that Contribution or the Project ends on the date the litigation is filed.

## 5. What You confirm

By accepting this Agreement, You confirm that:

1. **You are entitled to grant these licences.** If Your employer, a client or
anyone else has rights in work You create, including Your Contributions,
You have their permission to make Your Contributions under this Agreement,
or they have waived those rights in Your Contributions.
2. **Each Contribution is Your original work,** apart from material You
identify as coming from someone else under point 3.
3. **You identify material from others.** If You Submit anything that is not
entirely Your original work, You say so when You Submit it (for example in
the pull request description), and You name its source, its licence and
any other restriction on it that You know of, including related patents
and trademarks.

## 6. The Licensor may transfer these rights

The Licensor may assign or transfer the rights granted under this Agreement, in
whole or in part, to any person or entity, for example a company formed later
to run the Project. The grant then benefits the assignee in the same way it
benefited the Licensor. You do not need to be asked or notified.

## 7. No obligation to use Your Contribution

The Licensor is not required to use, merge, distribute or keep any
Contribution, and may change or remove it at any time.

## 8. No warranty

Except for what You confirm in section 5, and unless applicable law requires
otherwise or You agree otherwise in writing, You provide Your Contributions
"as is", without warranties or conditions of any kind, express or implied,
including warranties or conditions of title, non-infringement, merchantability
or fitness for a particular purpose. You are not expected to provide support
for Your Contributions.

## 9. Telling the Licensor about inaccuracies

If You learn of any fact or circumstance that would make anything You confirm
in section 5 untrue, You agree to tell the Licensor promptly, for example by
commenting on the pull request concerned or by opening an issue in the Covered
Repository.

## 10. How You accept this Agreement

You accept this Agreement by posting this comment, exactly, on a pull request
to a Covered Repository:

> I have read the CLA Document and I hereby sign the CLA

Your acceptance covers the Contributions in that pull request, including those
Submitted before You posted the comment, and everything You Submit to any
Covered Repository afterwards. Each Covered Repository keeps its own record of
acceptances, so its automated check may ask You to accept again on Your first
pull request there; accepting again does not change these terms.

To record Your acceptance, the Covered Repository stores Your GitHub username,
Your GitHub user ID, the pull request number, the comment ID and the time of
the comment in a public file in that repository.

## 11. Later versions

The Licensor may publish a new version of this Agreement. A new version applies
only to Contributions You Submit after You accept it.

## 12. Language

This Agreement is written in English. The Chinese translation is provided for
convenience only; if the two differ, the English text governs.
Loading
Loading