Repository navigation
Conversation
A design-level model of how Proxy maps, forwards or refuses each statement, checked with TLC: plaintext must never reach an encrypted column except with UNSAFE_DISABLE_MAPPING. Covers the refuse-by-default route from #484, simple Query batches, Parse and Bind, transactions and concurrent DDL. TLC confirms the safety properties for clients that never disable mapping, and records three findings as expected counterexamples: stale routes under concurrent DDL, statement kinds Proxy never type checks, and a Parse under UNSAFE_DISABLE_MAPPING bound after mapping is enabled again. Three injected faults are each caught. Adds a non-required CI workflow. No production code changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
S3 tracks writes into columns that are encrypted when the write happens; values already stored before DDL makes a column encrypted are out of scope. The README and verification record no longer imply that a change to Proxy's Rust code alone changes the TLC results. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
tobyhede
force-pushed
the
claude/project-thread-p8sn3b
branch
from
October 22, 2026 23:51
a2c955f to
dd25d54
Compare
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
Proxy refuses a Bind with PreparedSettingsChanged when the mapping setting differs from the one at Parse, for any statement it type checks. The model left that check out, so it reported a plaintext path (F3) that Proxy built from main does not have. Model the check, drop the candidate-fix instances, and add mutation M4, which removes it and is caught by S1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
tobyhede
marked this pull request as ready for review
October 25, 2026 22:39
cipherstash-agent
left a comment
There was a problem hiding this comment.
Review attestation
Source: Claude Code, /code-review medium plus company-skills:cross-check-review-findings (loaded from cipherstash/skills). Reviewed head 1d3997f.
Result: No critical or blocking issues. 3 minor findings, inline.
Cross-check: The PR had no prior reviews, review threads or comments, so all 3 findings are new.
What I checked
- The model against
requirements.md: properties S1–S4, assumptions A1–A5 and findings F1–F3. - The four mutation patterns in
check.sh. Eachfromstring occurs once inrouting.qnt. - The scenario tests' expectations traced against the actions.
- The workflow and tooling files.
Not done: I did not run Quint or TLC. I did not compare the model with Frontend::route or the other Rust sources it cites (#484 and related). It is a design-level model with no Rust adapter, so that comparison is the open risk, as F3 showed.
The lockfile was skimmed, not audited.
Generated by Claude Code
- check.sh: the TLC summary grep no longer fails a passing check if Quint changes its output wording. - routing.qnt: S4 comment says D1 is decided. - quint.yml: restrict GITHUB_TOKEN to contents: read. Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
check.sh reported witness counts without enforcing them, so a dead action would pass. Each simulated instance now fails when any witness is reached in 0 traces. Add witnesses for Parse storing a route, an open transaction, a transaction snapshot made stale by concurrent DDL, and a stale prepared route. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add a source-file correspondence table to the routing model's requirements, so a change to one of those functions prompts a model check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Address the skill-driven review of the routing model. - Split the unchecked statement kind into CopyFrom, NestedWrite and Opaque, and route them as Frontend::route does since #488: check_copy_into refuses COPY ... FROM into an encrypted or unresolvable column, and a nested write is refused while encrypted columns are configured. The Bind settings check is gated by requires_schema. - A3 and F2 now cover only DO, CALL and SQL EXECUTE. New assumption A6: a nested write takes no parameters. A5 is documented as an over-approximation, which widens F1. Decision D4 keeps Bind and Execute as one step. - Parse is only of an unused name, a refused Parse leaves it unused, and an unknown Bind under UNSAFE_DISABLE_MAPPING is forwarded, as in Proxy. - New witnesses for Bind, Close, transaction end and the mapping setting, so every action has one only it can reach. New mutations M5 to M8. - Two-statement batches hold type-checked statements only, which keeps check.sh all at about 6 minutes. - Fill in the source-file correspondence table and the AGENTS.md pointer. No property changes. TLC: asBuilt 56,994 states, concurrentDdl 445,094, safety holds in both; all expected violations and M1 to M8 reproduce. Signed-off-by: Claude <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Toby
Stacked on #490, which adds the Quint agent skills, the mise tasks and their docs. Merge #490 first.
This is a proof-of-concept formal model, in Quint, of how Proxy decides to map, forward or refuse each statement.
It checks that plaintext never reaches an encrypted column, except when the client uses
UNSAFE_DISABLE_MAPPING.The model is design-level only: it changes no production code, and no Rust code is tested against it yet.
Start with
specs/quint/routing/requirements.md, which covers the scope, assumptions, properties, findings and open decisions, and maps each model element to its Rust code.verification.mdhas the exact results.What it models
Frontend::routefrom refactor(proxy): route each statement through one refuse-by-default decision #484, includingmay_fall_back, the unresolvable-table refusal (fix(proxy): refuse writes to a table Proxy cannot resolve #486), the simple Query batch rule, and theCOPY ... FROMand nested-write refusals from fix(proxy): refuse COPY FROM into encrypted columns, check statements after COPY FROM STDIN #488.PreparedSettingsChanged), gated byrequires_schema.UNSAFE_DISABLE_MAPPING.mapping_errors_enabled.Results (Quint 0.33.0, TLC, exhaustive for each finite instance)
UNSAFE_DISABLE_MAPPINGand concurrent DDL.With a stable schema, Proxy's routing alone keeps them true, even with PostgreSQL's domain check switched off.
main's behaviour before refactor(proxy): route each statement through one refuse-by-default decision #484), no Bind settings check, and noCOPY ... FROMrefusal.Findings to confirm against real Proxy before acting
These are recorded as expected counterexamples, so CI fails if one stops reproducing.
F3(retracted): the first version of the model left out the Bind settings check (PreparedSettingsChanged), so it reported a plaintext path that Proxy does not have. The model now includes the check, and mutation M4 covers it.DO,CALLand SQLEXECUTEare opaque to Proxy, so nothing in routing stops them writing plaintext.COPY ... FROMis no longer part of F2: fix(proxy): refuse COPY FROM into encrypted columns, check statements after COPY FROM STDIN #488 refuses it.PostgreSQL's domain check should reject the value in every case, so none of these is a confirmed leak. Each would need a failing test first.
Requirements changes for review
No property (S1 to S4) changes. These assumptions change:
DO,CALLand SQLEXECUTE;COPY ... FROMand nested writes are modelled.Tooling
specs/quinthas a pinnedpackage-lock.json.check.shcovers typecheck, scenario tests, simulation with every witness required to be reached, TLC and mutations..github/workflows/quint.ymlruns only on changes underspecs/quint. It isn't a required check, and a full run takes about 7 minutes.npm auditflagsadm-zip, which Quint uses only to unpack its own Apalache download, as the README notes.Acknowledgment
By submitting this pull request, I confirm that CipherStash can use, modify, copy, and redistribute this contribution, under the terms of CipherStash's choice.
Generated by Claude Code