Skip to content

fix(python): address the review findings across the Python SDK stack - #1635

Open
yujiyokoo wants to merge 21 commits into
feat/python-sqlalchemyfrom
fix/python-review-findings
Open

yujiyokoo wants to merge 21 commits into
feat/python-sqlalchemyfrom
fix/python-review-findings

Conversation

@yujiyokoo

@yujiyokoo yujiyokoo commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Reviewers left 113 review threads across the 17 pull requests of the Python SDK stack. This pull request fixes the ones that were still open, on top of #1622, including most of the smaller ones tagged "low". The fixes cover wrong error codes and declaration checks, a hang in a forked worker process, CI secrets that build code could read, and tests that could pass while checking nothing.

Changes

Declarations

  • Refuse a hand-written __repr__, __str__ or __rich_repr__ in any form, not only a plain function.
  • Fix five faults in the field reader: generic value types, dunder names, uncaught hint errors, a wrong field name after a string ClassVar, and a required Ignore field with an alias or a private name.
  • On Python 3.9, a bare ClassVar written as a string now gets an error that says how to fix it.

Redaction and messages

  • On a Pydantic model, __repr_args__ also shows <sealed>. Pydantic's __repr_str__ and devtools' debug() print through it.
  • An engine message and its help read as two sentences.
  • The "no credentials" message also covers a profile that was found but is incomplete.
  • Core's query_unsupported names the marker in reason, as the Python check does.

Encrypt and decrypt

  • encrypt refuses a model of another class. A datetime with no UTC offset is naive.
  • decrypt keeps KeyboardInterrupt and MemoryError, refuses one row passed on its own, and refuses a str that is not valid UTF-8.
  • The README and the Cipher.decrypt docstring say which two engine errors do not name the row.

Processes

  • A process forked after the engine client was built is refused with stack_forked. Before, its next call never returned. A new runtime in the child is not safe either: on Linux it blocked in 8 of 15 runs.

Engine crates

  • stack-kms exports endpoint_from_env, and stack-encrypt exports client_key_provider. The Python client calls them, in place of its own copies.

Build and CI

  • A release build with the deterministic-kms test feature fails to compile, and the wheel check refuses a wheel that has it.
  • The live job gives the CS_* credentials to the test step only. The build runs before it, without them.
  • maturin is locked in uv.lock, and mise run build uses that copy.
  • The Python Cargo.lock resolves the root workspace's vitaminc again. A new test compares the crates that decide stored values in the two locks.
  • Path filters, the engine commit of a pull request's wheel, and the wheel summary job are fixed.
  • The live job caches both Cargo workspaces in one rust-cache step. Two steps deleted each other's cached crates.
  • New checks that the workflow runs the _eql.py parity test, and that the build installs from uv.lock with no tool from outside it.

Tests

  • New tests for the paths reviewers found without one, such as the engine commit against git, an expired SQLAlchemy row, and decrypt from 8 threads.
  • The fuzz seeds now reach the value frame read check.
  • Dates, timestamps and decimals are checked against known engine numbers. The engine-commit override rules, and the _eql.py generator's guards, have tests.
  • An offline test is skipped when a live test has already built the shared client in the same process. A combined run had 51 failures.
  • The declaration cases are compiled with pytest's assertion rewrite, so a failure shows its values.

Docs

  • The customer directory example's FINDINGS.md. Each of its 8 items has a status line.
  • The README covers the input that Pydantic and FastAPI errors show, a key the database generates, and why a unique index on eq_term works.
  • The README and the Cipher.decrypt docstring say that decrypt keeps the order of the rows. A deterministic test checks it.

Verification

Run locally on macOS, on the last commit unless stated:

  • Offline pytest with no profile: 566 passed on Python 3.14. On 3.9, all pass except one smoke test that needs installed package metadata.
  • mise run test:live against ZeroKMS and PostgreSQL 17: 98 passed.
  • mise run test:deterministic: 24 passed.
  • mise run test:cargo, clippy with and without features, and cargo fmt --check passed.
  • stack-kms and stack-encrypt: nextest 1128 passed, rustdoc with warnings as errors passed.
  • All 86 script test files passed.

Run on Linux in Docker:

  • The fork test, 15 runs of each case: all children forked after the first call were refused, and all forked before it worked.
  • The fuzz replay on current nightly Rust: all 7 seeds reached the read check, with no crash.

CI passed at bf84840af, the last commit: 63 checks passed. 2 were skipped, fuzz campaign and review, which do not run on a pull request. The live job ran the fork test on Linux, and both cases passed.

Not run:

  • The SIGINT gate tests, which need the gate-probe build. CI builds no such wheel.
  • The customer directory example's tests, which need its own database.

Related

Refs #1341

Review notes

  • Each commit names the review threads it addresses.
  • c7d6eb1b7 rebuilt the client in a forked child. 3c2b14ea9 replaces that with the refusal, after the Linux runs showed the rebuild can block. The two could be squashed.
  • Not fixed here: 1509-F3, because it contradicts F4/F9; 1499-F7, a speed change with no measured cost; and 1621-F3, an example test that needs the example's own database. The engine fix for the missing row number is recorded in FINDINGS.md item 8.
  • No changeset: the Python SDK is not published. The two engine crates release through release-plz.

🤖 Generated with Claude Code

yujiyokoo and others added 15 commits November 12, 2026 14:52
A release build with the deterministic-kms feature now fails to
compile. That feature exports _use_deterministic_keys. That function
sets every key in the process from a fixed seed. Only the dev-profile
test build uses it. The wheel import step also fails if a wheel
exports it.

tests-python.yml:
- The live job skips Dependabot's pull requests. They get no Actions
  secrets, so require-cs-secrets failed on every one.
- An example-only change no longer starts the workflow. CI does not
  run the example's tests.
- .cargo/** and EQL's Cargo.toml and Cargo.lock now start it. Cargo
  reads the first for this workspace. The EQL build uses the other two.
- A pull request's wheel records the PR's head commit. GitHub's test
  merge commit is on no branch, so a bug report could not name it.
- maturin is fixed at v1.16.0, inside pyproject.toml's range.
- wheels-summary runs when a leg fails, so the passing legs still get
  their build-time row.

Addresses review threads 1513-F5, 1499-F2, 1499-F12, 1621-F5, and
1406-F1, F2, F3, F5.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The decorator replaced a hand-written __repr__ or __str__ with no error
in four cases: a method wrapped by reprlib.recursive_repr, a
staticmethod, a partialmethod, or a function from another module. Now
the decorator replaces only a method that dataclasses, attrs, or
NamedTuple generated. It refuses anything else on the class with
declaration_repr_present.

The field reader had five faults:
- It refused a correctly placed marker around a generic value type,
  such as re.Pattern[str].
- It read dunder annotations, such as __tablename__, as fields.
- An AttributeError or a SyntaxError from a hint escaped with no field
  name.
- Before Python 3.14, it named the wrong field after a string ClassVar
  hint.
- It did not refuse a required Ignore field with a Pydantic alias or
  an attrs private name.

The fixes:
- Only a SQLAlchemy wrapper is refused inside the marker's Annotated.
- Dunder names are skipped.
- The four hint errors give declaration_hints_unresolved.
- Each hint is resolved on a probe class, not a probe function.
- An Ignore field is looked up by its constructor name.
- A Pydantic dataclass's config is read from __pydantic_config__.

Addresses review threads 1453-F1, F2, 1433-F1, F2, F3, F5, and
1437-F1, F2, F3.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
encrypt read each field by name, so a model of another class with the
same field names was encrypted under this table. It now refuses a model
that is not an instance of the declared class, with value_wrong_type.

A datetime whose tzinfo gives no UTC offset is naive in Python. The
value reader now checks utcoffset(), so such a value gives
value_naive_datetime, not a TypeError.

decrypt turned every error from json.dumps into decrypt_malformed. That
included KeyboardInterrupt and MemoryError. Now only TypeError,
ValueError, and RecursionError do so. An error raised while a
passthrough value is read now propagates, as it does on encrypt.

One row passed on its own, for example from fetchone(), was read as a
list of column names. decrypt, decrypt_rows, and encrypt_rows now refuse
it and say to pass [row].

A str with a lone surrogate now gives decrypt_malformed, the same as
bytes that are not UTF-8. encrypt now has the type Sequence[T].

Addresses review threads 1499-F5, 1499-F6, and 1509-F4, F5, F6, F8,
F9.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewers marked ten paths as having no test. Each one could break with
no test failing.

- The engine commit is checked against git: the SHA must equal HEAD,
  with -dirty exactly when a stack-* crate has changed outside fuzz/.
  build.rs now records where the commit came from. cargo sets the
  commit variable when it runs the tests, so the test cannot read the
  environment to find an override.
- The frame reader walks a typed null, a sequence, and a nested map.
  A frame writer writes a typed null as the plain null, so the test
  expects no type there.
- The SIGINT gate tests cancel and join their timer in finally.
- A class read from its hints leaves out ClassVar and InitVar, and rule
  A holds inside Mapped on a real mapped class.
- repr of an expired or detached SQLAlchemy row sends no SQL statement.
- After the switch to the deterministic client, the live client and a
  second switch are refused. test:cargo runs this test binary with the
  deterministic-kms feature. A seed that is not 32 bytes is refused.
- decrypt runs from 8 threads at once, live and deterministic.
- The keyset test fails if the load-keyset counter is missing.

Addresses review threads 1405-F2, 1414-F4, F6, 1433-F6, F7, 1453-F7,
1513-F2, F3, and 1516-F1, F2.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each change is small. Together they close the review threads tagged as
quick wins.

Build and engine:
- build.rs fails when git status fails. Before, it recorded a clean
  commit. -dirty now also covers the root Cargo.toml and the Python
  Cargo.lock, which choose the engine's dependency versions.
- run! gives its value in both builds. Before, the test build returned
  from the calling function.
- The allocator spy uses a poisoned lock as it is. A panic inside the
  allocator would abort the test binary with no message.

Tests:
- The value tests refuse both sides of each integer range, a float too
  large for float32, and a decimal mantissa one past 96 bits. The
  edge-value test builds the cipher outside pytest.raises.
- Refusals of a hand-written or replaced __str__ and __rich_repr__, and
  of a changed or removed hint.
- A constructor's own error reaches the caller of decrypt unchanged. A
  dict with no JSON form gives decrypt_malformed.
- The thread helpers time out and name the failed thread. Other helpers
  give clearer failure messages, and the Go-row tests check lengths
  before zip.
- The conformance test compares vector names, not counts.
- The CI script tests check that their scans find something, test the
  triggers() helper, and hold the deterministic build's features to
  pyproject.toml's.
- The codegen test shows that a refused override writes no file.
- The ClassVar test from the last commit keeps its bare ClassVar case
  for Python 3.10 and later. Python 3.9 cannot resolve one written as a
  string.

Docs and example:
- AGENTS.md says which job builds the wheels, where extension-module is
  named, and what test:cargo runs.
- G6 on the scorecard is strained: it lists two gaps with no test.
- The example builds EQL before it deletes its database volume, says to
  trust EQL's mise file, and accepts a postgres:// URL.

Addresses review threads 1405-F1, F4, 1406-F6, F7, F8, F9, 1414-F3,
F5, 1415-F4, 1453-F8, F9, 1499-F17, 1509-F10, F11, 1511-F1, F3, F4,
F5, 1513-F1, F4, F7, F8, 1516-F3, 1517-F1, F2, F7, F8, and 1621-F1,
F2.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under from __future__ import annotations, Python 3.9 cannot read a bare
ClassVar hint: get_type_hints raises TypeError. The decorator reported
it as an unresolved name, and told the user to define or import it.
Nothing was missing, so that advice did not help.

The decorator now evaluates the hint that failed. If it is ClassVar with
no type, the error says to write ClassVar[int], with the reason
bare_class_var. The evaluation is Python's own, so typing.ClassVar and
an alias are found too. Python versions that read the bare form accept
it as before.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engine client includes a tokio runtime with its own threads. fork()
copies only the thread that called it, so a child that Gunicorn or
multiprocessing forked after the first call held a client whose runtime
had no threads. Its next call never returned.

The client is now stored with the ID of the process that built it. A
child builds its own client on its first call, and loads the keyset
again. The parent's copy is never dropped in the child: dropping a
runtime whose threads are gone could block.

macOS does not allow its TLS certificate service in a child that fork
made, so on macOS the child crashes when it builds its client. The
test skips on macOS, and test:live leaves it out there. CI runs it on
Linux.

run! also had a label that only the test build used. clippy with
-D warnings refused it in the normal build. It is allowed now.

Addresses review thread 1499-F4.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engine client includes a tokio runtime with its own threads. fork()
copies only the thread that called it, so a child forked after the first
call held a client whose runtime had no threads. Its next call never
returned.

Building a new runtime in the child is not safe either. tokio's locks
keep a process-wide table of waiting threads, and the child's copy lists
the parent's threads. On Linux, a new runtime in the child blocked in 8
of 15 runs.

So a child that fork made after the client was built is now refused,
with StashError code stack_forked. The message says to make the first
call after the fork. The check reads a OnceLock and an atomic process
ID, and takes no lock, so it cannot block. A child forked before any call
builds its own client, as before.

The fork test covers both cases, and runs on macOS and Linux. On Linux,
each case passed 15 of 15 runs.

Addresses review thread 1499-F4.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stays so

AGENTS.md said the Python Cargo.lock is seeded from the root one, so the
crates the two workspaces share resolve to the versions the root tests.
That was no longer true: the Python lock had vitaminc 0.8.1, and the root
lock had 0.8.0. No check compared the two locks.

The Python lock is seeded from the root lock again. The difference was
small: 0.8.1 adds a storage profile to vitaminc-aead-frame. The SDK does
not use it. The encryption and order crates have the same source in both
versions. The documented steps were not enough to re-seed: cargo
metadata moved vitaminc back to 0.8.1, and cargo update --precise moved
each crate to 0.8.0. AGENTS.md now gives that step.

python-lock-parity.test.mjs compares the crates that decide what a
stored value is, by version, source and checksum: every vitaminc crate,
recipher, ore-rs, clww-ore, cts-common and zerokms-protocol. Other
shared crates may differ. The lock parser moves to lib/cargo-lock.mjs,
so this test and the cipherstash-client one share it.

The deterministic suite still opens the Go SDK's frozen rows and seals
the same values, so vitaminc 0.8.0 gives the same results here.

Addresses review thread 1406-F4.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The live job set the four CS_* credentials for the whole job. So every
step could read them, the builds included. A build runs third-party
code: each dependency's build.rs and proc-macros, and maturin.

The credentials now go to the Live tests step only. A step before it
builds the extension and the EQL bundle, with no credentials. The live
tests run through test:live:run, a new task that builds nothing.
build:eql-bundle is the other new task. test:live still builds and runs
everything, for local use.

python-ci.test.mjs requires CI to run every Python task. test:live is
exempt, with its reason, because CI runs it in parts.

maturin is still installed unlocked (review thread 1582-F1). It now runs
without the credentials.

Addresses review thread 1499-F1, and in part 1582-F1.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The value_frame target read its input as an arbitrary Input enum, and
the committed seeds are plain value frames. arbitrary reads the first
four bytes as the enum tag. So six seeds decoded as Write, and the
seventh as a Read of one byte. No seed reached the read check, and CI's
replay never ran it.

Putting the Read tag in front of each seed is not enough. arbitrary
1.5.0 puts a byte of its own before each Vec<u8> element, so an
arbitrary input never contains a frame byte for byte.

The target now takes raw bytes, and the first byte chooses the branch.
0x00 is Read, and the rest of the input is the frame, byte for byte.
Any other byte is Write, decoded through arbitrary as before. Each seed
now starts with 0x00. On current nightly, all seven seeds reach the read
check, and the replay passes.

Addresses review thread 1511-F2.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The example found gaps in the SDK and its documentation while it was
built. This file records them, with the timings it measured and the
README claims that held.

Items 5, 6 and 7 were found before the SQLAlchemy layer existed, and
the layer addresses each one. Each now has a status line that says how.

Item 8 is new: when the engine refuses a value, decrypt_malformed and
decrypt_kind_mismatch name the field but not the row. The Go SDK has the
same gap. The suggested fix is in the engine, so that both SDKs get the
row (review thread 1509-F7).

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Most decrypt errors name the row and the field. decrypt_malformed and
decrypt_kind_mismatch from the engine name the field only, because the
engine does not report the row. The README and the Cipher.decrypt
docstring now say so, and say where to look for the bad value.

The fix belongs in the engine, so that the Python and Go SDKs both get
the row. FINDINGS.md item 8 records it.

Addresses review thread 1509-F7.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…own code

The Python SDK builds its engine client over a connection that counts
its requests, so it cannot use StackCipherBuilder. It copied two private
pieces of that builder: the reader of CS_ZEROKMS_HOST in stack-kms, and
the client key provider in stack-encrypt. A change to either would not
reach Python, and no test would report it.

stack-kms now exports endpoint_from_env, the reader the builder uses.
stack-encrypt now exports client_key_provider, the provider init uses:
the environment first, then the stash profile. The Python client calls
both, and its copies are gone.

The two copies had already drifted: with no client key and no profile,
Python now gives the engine's message, which says npx stash auth login.

The wasm32 variant of client_key_provider named KeyProvider, which is
imported only for other targets. It now uses the full path. Nothing in
CI builds stack-encrypt's http feature for wasm32.

Addresses review thread 1499-F15.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
uv.lock fixed the test tools, but not maturin, the build backend. Each
build downloaded maturin from PyPI at any version in [build-system]'s
range, with no hash check and no Dependabot cooldown.

maturin is now in a build dependency group, so uv.lock records its
version and hash. Dependabot updates it with the test extra. mise run
build installs the group into .venv, and builds the package with
--no-build-isolation, so uv downloads no maturin of its own. Without
maturin in .venv, that build fails.

[build-system] keeps its range, for an install by any other route. The
wheels job still uses maturin-action, with maturin's version fixed in
the workflow.

Addresses review thread 1582-F1.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yujiyokoo
yujiyokoo requested a review from a team as a code owner November 12, 2026 11:01
@changeset-bot

changeset-bot Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: bf84840

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI; Enforced overrides: Organization
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2503e11d-9d3b-4b89-80c7-187e42934cd2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review

@github-actions

Copy link
Copy Markdown
Contributor

Mutation testing (cargo-mutants, --in-diff, stack-auth + stack-encrypt)

No mutants were generated for the changed lines.

@cipherstash-agent cipherstash-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review attestation

Reviewed 75c78105 · posted from Claude Code (scheduled routine) · claude-opus-5-5 · effort medium
Sources: code-review · claude-opus-5-5
Skills: code-review, rust-security, review-cross-check

Findings (9): 0 critical, 3 major, 6 minor. All 9 are inline, 0 below.

Every finding was checked against 75c78105 by separate verification agents. Most were reproduced, and the fork hang and attrs __str__ findings among them. One finding was dropped as refuted: the not(debug_assertions) guard on deterministic-kms (crates/core/src/lib.rs:32) is reachable only through a build that names the feature on purpose. The rust-security pass over the changed Rust found nothing new. That pass covered endpoint_from_env, client_key_provider, the decrypt error paths and the deterministic-kms gating. The PR had no earlier review findings to cross-check against.


Generated by Claude Code

if let Some(client) = current()? {
return Ok(client);
}
let _building = locked(&BUILDING);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

major The fork refusal only fires once CLIENT is set. If a child is forked while another thread holds BUILDING, from this line until shared() returns (including the network keyset load in build()), then the child's first call gets Ok(None) from current() and blocks here forever, on a mutex whose owner does not exist in the child. A trigger is Gunicorn --preload or multiprocessing fork while a warm-up thread is making the first call. This was reproduced with a copy of this lock order: the child was still blocked 6s after the parent's build finished. One fix is to store the builder's PID in an AtomicU32 right after taking BUILDING, have waiters loop on try_lock, and refuse with stack_forked when that PID is set and is not std::process::id(). use_deterministic (line 200) uses the same lock pattern.

Source: code-review · claude-opus-5-5


Generated by Claude Code

"""``rows`` as a list. Raises ``error`` with ``code`` for one row on its
own, such as ``cursor.fetchone()`` returns: ``list()`` of a mapping is its
keys, and each key would be refused as a row that is not a mapping."""
if isinstance(rows, Mapping):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

major Only a Mapping is refused as a lone row. A SQLAlchemy Row or a psycopg tuple row is a sequence, so list(rows) splits it into its column values. When every selected column is sealed, each value is an EQL dict. _row_fields then finds no declared field and decrypt_rows returns the ciphertext dicts unchanged, with no error. This was reproduced: decrypt_rows(row) with row = ({"c": …}, {"c": …}) returns the two payloads as they were. Fix: also refuse an object with _mapping (point to row._mapping or .mappings()). encrypt(models) at line 98 needs a guard too: refuse isinstance(models, self._cls) before list(), because a Pydantic model or a NamedTuple model is iterated into its fields.

Source: code-review · claude-opus-5-5


Generated by Claude Code

return True
if filename == _DATACLASS_FILE:
return inner.__qualname__.startswith(_DATACLASS_QUALNAME)
return _COLLECTIONS_FILE is not None and os.path.realpath(filename) == _COLLECTIONS_FILE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

major @attrs.define(str=True) is now refused with declaration_repr_present. attrs builds that __str__ as a plain closure in attr/_make.py, not under <attrs generated, so none of the three cases here matches it. This was reproduced with attrs 26.1.0. The only way past it is redact=False, which turns redaction off. Fix: also accept code whose file is sys.modules["attr._make"].__file__ and whose co_name is "__str__", and add a str=True test case.

Source: code-review · claude-opus-5-5


Generated by Claude Code


# Builds the wheel and the bundle, installs the bundle, and runs the
# live tests; a skip fails the task.
# The builds, with no credentials: the extension into .venv, and the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor The build step and the Live tests step run on the same runner and filesystem. Anything the build step leaves behind therefore runs with CS_CLIENT_KEY and CS_CLIENT_ACCESS_KEY set: a .pth in .venv, a $GITHUB_ENV or $GITHUB_PATH line, an edited mise.toml, or a changed shim. Dependency code compiled into _engine.so does too. This is still better than the job-level secrets it replaces, but this comment, mise.toml:108-111 and the AGENTS.md line ("no dependency's build script runs with the credentials set") claim more than the change delivers. Either reword them, or build the wheel in a job with no secrets and install only that wheel into a fresh venv in the live job.

Source: code-review · claude-opus-5-5


Generated by Claude Code

id: engine
run: echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
env:
COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor The wheels checkout (line 387) has no ref:, so on a pull request it compiles refs/pull/N/merge, while this records head.sha. build.rs trusts the override and skips the -dirty check. So when the base branch has moved under packages/stack-*, __engine__ names a commit the wheel was not built from. Fix: add ref: ${{ github.event.pull_request.head.sha || github.sha }} to that checkout.

Source: code-review · claude-opus-5-5


Generated by Claude Code

rust-toolchain: ${{ steps.rust.outputs.version }}
# A fixed maturin, inside pyproject.toml's range. Without it the
# action downloads the newest release on every run.
maturin-version: v1.16.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor Nothing keeps this pin equal to maturin in uv.lock (1.16.0). Dependabot's uv group will bump the lock alone, and then mise run build and the live job build with the new maturin while the shipped wheels stay on 1.16.0, with no failure. Add an assertion in scripts/__tests__/python-ci.test.mjs that this value, without the v, equals the uv.lock version. The dependabot.yml comment that says uv.lock "holds only the test extra" is now stale as well.

Source: code-review · claude-opus-5-5


Generated by Claude Code

'pyproject.toml has a [tool.maturin] features list',
).not.toBeNull()
const normal = [...declared[1].matchAll(/"([^"]+)"/g)].map((m) => m[1])
const task = read(`${PYTHON_ROOT}/mise.toml`).match(/--features ([^\s"]+)/)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor This takes the first --features match anywhere in mise.toml. This PR adds a second one (test:cargo, line 90), so the check now depends on block order. Moving test:cargo above test:deterministic makes it capture deterministic-kms alone and fail on a correct file. This was reproduced. Slice the [tasks."test:deterministic"] body first, as the localOnly check does.

Source: code-review · claude-opus-5-5


Generated by Claude Code

/// (`int8`, the unsigned and 128-bit integers, `uuid`), it is not a scalar
/// (`array`, `object`), or it is a timestamp in a unit other than the
/// nanoseconds the SDK writes.
/// The vectors the SDK writes, in corpus order: every scalar kind it has,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor WRITTEN was inserted between NOT_WRITTEN's doc comment and NOT_WRITTEN, so all seven /// lines now document WRITTEN. It is described as the vectors "the SDK never writes", and NOT_WRITTEN (line 49) has no doc. Move lines 20-23 down to just above const NOT_WRITTEN.

Source: code-review · claude-opus-5-5


Generated by Claude Code

a ``ClassVar``, and before 3.14 a string ``ClassVar`` hint raises there.
"""
module = sys.modules.get(base.__module__)
global_names = dict(vars(module)) if module is not None else {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor dict(vars(module)) and dict(vars(base)) are now rebuilt for every hint, not once per base. This runs on the relationship path of _hints, at decoration and at each Cipher(...), and again in _unresolved_field and _is_bare_class_var. Compute global_names and base_locals once per base and pass them in.

Source: code-review · claude-opus-5-5


Generated by Claude Code

yujiyokoo and others added 5 commits November 12, 2026 20:32
The comment on the uv entry said uv.lock contains only the test extra.
It now also locks maturin in the build dependency group. Dependabot's uv
parser reads [dependency-groups] (uv/helpers/lib/parser.py in
dependabot-core), so maturin gets the same updates and cooldown.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t hooks

- Pydantic builds __repr_str__ and devtools' __pretty__ from
  __repr_args__, which still showed plaintext. It is now replaced on a
  Pydantic model with the sealed fields.
- Refusal::of joined an engine message and its help with a space, so
  the two read as one broken sentence. A full stop now ends the message.
- NotAuthenticated is also raised for a profile that lacks its region or
  client ID. Its message no longer says that nothing was found.
- Core's query_unsupported now names the marker in reason, as the
  Python check does.

Addresses review threads 1453-F3, F4, F5, and 1510-F1.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tor guards

- A date and a datetime read as known engine numbers, before the epoch
  and with a non-UTC offset, and back. A decimal reads as its exact
  mantissa and scale, signs and positive exponents included.
- The STACKENCRYPT_ENGINE_COMMIT rules moved from build.rs into
  engine_commit_rules.rs, which build.rs and a unit test both include.
- The frame reader reads a batch written the way dynamic::decrypt
  writes one.
- The _eql.py generator's guards have tests. A new py_ident check
  refuses a catalog name that is not a usable Python class name.

Addresses review threads 1405-F3, 1414-F5, 1415-F2, F3, and 1499-F13,
F14.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An offline test is skipped once a live test has built the shared
  engine client in the same process. A combined run had 51 failures.
- refused() checks that the refused value is not in the error text.
- The same value encrypts to three different ciphertexts.
- The declaration cases are compiled with pytest's assertion rewrite,
  so a failure shows its values.
- New tests for four README claims: a redact=False class may replace
  its printing methods, decrypt refuses a uuid.UUID for a str field,
  and bound_names keeps only top-level names, now with except-as and a
  walrus in a comprehension.
- The gate test says that CI does not run it, and what its timing
  assertion can show.

Addresses review threads 1414-F1, F2, 1433-F4, 1499-F8, F10, F11, and
1517-F3, F4, F5, F6.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The live job had one rust-cache step per workspace. Each pruned the
shared cargo registry to its own crates before it saved, so each save
lost the other's. One step now lists both. eql-suite-ci.test.mjs reads
workspaces: as a list.

python-ci.test.mjs now checks that the workflow runs the _eql.py parity
test, and that the build task installs from uv.lock with --locked,
--no-deps and --no-build-isolation.

Addresses review threads 1415-F1, 1499-F16, and 1582-F2.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yujiyokoo yujiyokoo changed the title Fix/python review findings fix(python): address the review findings across the Python SDK stack Nov 12, 2026
The customer directory example recorded four gaps in the README:

- Pydantic's ValidationError and FastAPI's 422 response show the
  rejected input. The README's list of what shows plaintext now names
  both. hide_input_in_errors hides the input from the error's text, but
  not from errors(), which FastAPI's response is built from.
- A key the database generates cannot be a required field. The UUID
  key section now covers it, for psycopg and for the SQLAlchemy layer.
- decrypt keeps the order of the rows. The README and the docstring now
  say so, and a deterministic test checks it with the rows reversed.
- A unique index on eq_term works because equal values have equal eq
  terms. The README now says so.

Each finding in FINDINGS.md now has a status line.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants