Skip to content

Redaction exception values and scan cache (core v0.86.0) - #130

Merged
fylorn merged 1 commit into
devfrom
feat/redact-exceptions
Oct 30, 2026
Merged

fylorn merged 1 commit into
devfrom
feat/redact-exceptions

Conversation

@fylorn

@fylorn fylorn commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

What this changes

thinkwatch-core moves to v0.86.0. Secrets and privacy gains exception values: a value recognised by mistake can be listed so that it is neither replaced nor recorded by any rule, while the rule keeps working for every other value. The gateway also stops rescanning unchanged conversation history every turn.

Why

Until now a false positive (a fixed test value, say) could only be silenced by switching the whole rule off, which also switched off its protection for real values. And a client sends the whole conversation every turn, so a long session's requests were scanned from the start each time.

Details

Storage. Exceptions live in the redaction policy as core defines it: security.redact.exceptions, each { sha256, masked, rule?, added_at }, at most 500. Only the SHA-256 and the masked form are stored, never the value. Saved provider keys cannot be exceptions.

API (OpenAPI updated):

  • GET /api/admin/security/redact/exceptions → { items, max }; settings:read.
  • POST /api/admin/security/redact/exceptions { value } → the exception as stored (201). The value is hashed on arrival by core's RedactPolicy::exception_of, which also picks the masked form and the rule; the value is never stored, logged or answered. 409 when already listed; 400 when empty, a saved provider key, or the list is full.
  • DELETE /api/admin/security/redact/exceptions/{sha256} → the exception removed; 404 when absent.
  • Adding and removing take settings:write and pii_redactor:write, the permissions of editing redaction rules. Each is audited (security.redact_exception_added / _removed) with the rule and the masked value only.
  • The settings row is locked from read to write, so concurrent edits on different instances both land; guards are reloaded locally and on other instances through config:changed.
  • PATCH /api/admin/settings with security.redact keeps the stored exceptions whatever it carries: the console rebuilds the policy from the rule view, which does not carry them.

Console. An "Exceptions · N" section under the redaction rule table: masked value, the rule that first recognised it (named from the rule view), when it was added, and Remove (with confirmation). "Add exception" opens a dialog to paste a value, and is unavailable at the cap with the reason shown. With no exceptions the section is one line and the button. The trial marks excepted values ("Exception, left as is") and does not count them as hits.

Scan cache. Redaction holds one core ScanCache per compiled rule set and passes it to look_hits_with / replace_with. The cache keeps hit positions and rule ids in process memory, never values, and is dropped with the rules whenever a policy, the providers' keys or the exceptions change. Results are identical to an uncached scan.

Left out: "List as exception" from the security log. The audit log stores neither the value nor its hash, and should not. Offering the button would need each finding's hash kept somewhere until an admin clicks. Kept in one gateway instance's memory, it would be unreachable from the console instance handling the click (any of several replicas) and lost on restart; kept in Redis or the database, it would put value hashes at rest outside the policy. Exceptions are added by pasting the value instead.

How it was verified

  • cargo fmt --check, cargo clippy --workspace --all-targets: clean.
  • Unit tests: 904 passed. New: settings writes keep stored exceptions; an excepted value is neither found nor replaced; over a growing 30-turn conversation, the cached look and replace match the uncached ones turn after turn.
  • Integration suite against fresh Postgres, Redis and ClickHouse containers: 484 passed. New: add by hash (surrounding whitespace ignored), 409 on a duplicate, 400 when empty, list with the cap, nothing of the value in the stored policy, kept across a settings write, the gateway sending the excepted value as it is, the trial reporting excepted, audit rows with the rule and masked value only, remove then 404; and a saved provider key refused as an exception.
  • Web: check-i18n, eslint, vitest (205 passed, new tests for the section, the cap, the empty state and the trial), tsc -b, vite build.

🤖 Generated with Claude Code

….86.0)

- Core crates pinned to v0.86.0.
- Exception values: stored in the redaction policy (`security.redact.
  exceptions`, SHA-256 and masked form only). New endpoints list, add
  (the pasted value is hashed on arrival and never stored, logged or
  answered) and remove them, with the permissions of editing the
  redaction policy; each add and remove is audited with the rule and the
  masked value. The row is locked from read to write. A policy written
  through the settings endpoint keeps the stored exceptions, since the
  console writes the policy back from the rule view, which does not
  carry them.
- Console: an Exceptions section under the redaction rules (masked
  value, rule, added time, remove; add dialog; adding unavailable at the
  cap); the trial marks excepted values as left as is.
- Gateway: one thinkwatch-core ScanCache per compiled redaction rule
  set, used when looking at and replacing a request, so unchanged
  conversation history is taken from the previous turn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fylorn
fylorn merged commit c54f7fd into dev Oct 30, 2026
7 checks passed
@fylorn
fylorn deleted the feat/redact-exceptions branch October 30, 2026 01:41
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant