Skip to content

Feat/python sdk cipher - #1453

Merged
yujiyokoo merged 3 commits into
feat/python-sdk-declaration-meaningfrom
feat/python-sdk-cipher
Nov 13, 2026
Merged

yujiyokoo merged 3 commits into
feat/python-sdk-declaration-meaningfrom
feat/python-sdk-cipher

Conversation

@yujiyokoo

Copy link
Copy Markdown
Contributor

Summary

A declared class no longer prints plaintext: repr, str and rich's __rich_repr__ show <sealed> for every sealed field. Cipher(User) now exists. It checks that the class is declared and unchanged, then finds the credentials the way every SDK does, with no request. encrypt, decrypt and query come in later tickets.

Changes

Redaction (_redact.py). @stackencrypt.model replaces __repr__, __str__ and __rich_repr__. A passthrough field shows its value, a sealed field shows <sealed>, and an Ignore field is left out. A value is read from the instance's __dict__ or a storage slot only, so a SQLAlchemy column that is not loaded runs no SELECT. A generated method is replaced. A hand-written one is refused with declaration_repr_present, unless the decorator is given redact=False.

Cipher (_cipher.py). Cipher(cls) refuses an undeclared class, a subclass of a declared class, or a non-class with declaration_not_decorated. A class changed after decoration is declaration_class_changed, with reason methods, fields or constructor. Then it calls the engine for the credentials. A refusal is an AuthenticationError with the engine's code, such as stack_auth_not_authenticated.

Credentials (crates/core/src/credentials.rs). stack-auth's AutoStrategy reads CS_CLIENT_ACCESS_KEY and CS_WORKSPACE_CRN, then the profile stash auth login writes. It is given a transport that sends nothing, so no HTTP client is built and the http feature stays off. Each refusal names the variable or file to fix.

Placeholder. Python prints <sealed>, not Go's [sealed]. [sealed] is a valid tag in rich's markup, so rich.print(repr(user)) would show the field as empty. The ADR and the spec record it.

Verification

  • pytest: 228 passed and 2 skipped, on Python 3.14 and 3.9. The redaction tests run on a dataclass, attrs, Pydantic, NamedTuple and SQLAlchemy.
  • cargo test --locked, clippy, cargo fmt --check, mypy strict, and pyright are clean.
  • pnpm run test:scripts: 83 of 83 files pass.
  • The six platform wheels are built by CI only. This is the first branch with stack-auth in them.

Related

Closes #1388. Part of #1341.

Review notes

  • Start with _redact.hand_written and _value, then check_unchanged in _declaration.py.
  • Serialisation is not printing: dataclasses.asdict, model_dump and vars still return plaintext, by design.
  • A SQLAlchemy Ignore field under Annotated loads as one object, not a list. That is a spec problem, noted on Python SDK: core call over psycopg, on EQL v4 and Stack Encrypt #1341.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 11ee027

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 Oct 30, 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: 0813c145-87d3-43fe-865d-78a9ebdd3875

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

@yujiyokoo
yujiyokoo added this pull request to stack #1520 November 4, 2026 02:58
yujiyokoo and others added 3 commits November 5, 2026 00:19
repr and str show [sealed] for sealed fields. Cipher(cls) checks the
class again and finds the credentials, with no request and no HTTP
client.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Credentials are found without building an HTTP client, and each
refusal names the variable or file to fix. Redaction reads the class's
own dict, covers rich, and runs no descriptor that may do work.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
repr and str show [sealed] for sealed fields. Cipher(cls) checks the
class again and finds the credentials, with no request.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@yujiyokoo
yujiyokoo force-pushed the feat/python-sdk-cipher branch from 5b792d5 to 11ee027 Compare November 4, 2026 15:24
@yujiyokoo
yujiyokoo marked this pull request as ready for review November 10, 2026 00:40
@yujiyokoo
yujiyokoo requested a review from a team as a code owner November 10, 2026 00:40

@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 — commit reviewed: 11ee027aa1b1c5fbce339a6a0c2a23c1017d0a23. Host: Claude Code (scheduled routine). Model: claude-sonnet-5-5. Skills: code-review (medium, performed inline), review-cross-check (done manually: no existing review threads; only changeset-bot and CodeRabbit-skipped notices), review-post.

Blocking issues: none.
Findings: 1 inline comment (non-blocking, medium confidence) on hand_written in _redact.py.
Not reviewed in depth: Rust engine/refusal/plan changes and test files were skimmed from the file list only; tests were not run.


Generated by Claude Code

@cipherstash-bot cipherstash-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Recommendation: 🟡 merge after changes (1 of 8 review job(s) failed)

infracode — claude (claude-opus-5-5)

Nothing in the infrastructure part of this change needs to be fixed before merge. This PR changes no Terraform, CloudFormation, mise tasks, CI workflows or observability configs. Its only build-config changes are a new stack-auth path dependency in languages/python/Cargo.toml, crates/core/Cargo.toml and one line in languages/python/Cargo.lock.

I checked three things:

  • CI path filter. tests-python.yml already lists packages/stack-auth/**, because stack-kms's profile feature pulls in that crate. A change to stack-auth already reruns the Python suite.
  • http feature stays off. The new dependency is default-features = false. stack-encrypt is built with default-features = false, features = ["dynamic"], and dynamic does not turn on stack-auth/http or stack-kms/http. Cargo's feature unification does not compile reqwest or TLS into the wheel, so the comments in Cargo.toml and credentials.rs are correct.
  • Lock file. stack-auth was already in the workspace's Cargo.lock, through stack-kms. The diff adds only the new edge from stackencrypt-core, so cargo ... --locked still resolves.

I found no infrastructure issues.


python — claude (claude-opus-5-5)

Nothing must change before merging. Two small follow-ups can wait: hand_written can replace some hand-written methods with no error, and Pydantic's devtools hook still prints plaintext.

I did not repeat the open thread on reprlib.recursive_repr. The first finding below is a different input with a related cause.

Severity Finding Location
Follow-up A hand-written method with no __code__, or from another module, is replaced with no error languages/python/python/stackencrypt/_redact.py:73
Optional Pydantic's __pretty__ (devtools) still prints sealed fields languages/python/python/stackencrypt/_redact.py:60

I could not run the toolchain: this checkout has no .venv, and Pydantic and SQLAlchemy are not installed. I ran hand_written and install alone on Python 3.12, outside the package. They gave the results below. Rust files were out of scope.


rust — claude (claude-opus-5-5)

No Rust change has to be fixed before merging. The two follow-ups are both about the messages a Python user reads. The new Rust code does not use unsafe, has no panic sites, and holds no lock across an .await. The no-request transport keeps stack-auth's http feature off, as the PR description says. The one gap is that cargo clippy and cargo test were not run, because this environment has no Rust toolchain.

Severity Finding Location
Fix in a follow-up Refusal::of joins the message and the help with a space only, so the text reads as one broken sentence. This now applies to plan refusals too. languages/python/crates/core/src/refusal.rs:24
Fix in a follow-up When a profile exists but its token has no region or client id, the user sees "no credentials were found" languages/python/crates/core/src/credentials.rs:47
Optional The default message is cloned on every refusal, even when a mapped message replaces it languages/python/crates/core/src/credentials.rs:37

test-gap — claude (claude-opus-5-5)

Two coverage gaps should be fixed before merging: repr on a real SQLAlchemy instance with a column that is not loaded, and a Cipher built from a valid stash auth login profile. Both are the main promises of this PR, and today only a stand-in or the refusal path is tested. The other four comments are optional and can wait.

The tests that exist are clear and follow the repo's pytest style. The sketches below use the same helpers (declared, refusal, the credentials fixture, SECRET). I could not run the sketches here, because no .venv is built in this checkout.

Not repeated here: the existing review thread on hand_written and reprlib.recursive_repr already asks for a test of a hand-written @reprlib.recursive_repr() __repr__.

Out of scope, noted only: hand_written returns False for a method whose __module__ differs from the class's module. So a __repr__ defined in a helper module and assigned in the class body (__repr__ = helpers.show) is replaced with no error. This is the same kind of misclassification as the recursive_repr thread.


infracode — codex (gpt-5.6-terra)

This pull request contains no infrastructure-as-code changes. No infrastructure review findings.


python — codex (gpt-5.6-terra)

Redaction must avoid custom attribute access before merging. The remaining issue can wait only after this access is made storage-only.

Severity Finding Location
request-changes Reading __dict__ can run user code and trigger I/O. languages/python/python/stackencrypt/_redact.py:122

Focused tests were not run because this checkout has no Python or pytest executable.


rust — codex (gpt-5.6-terra)

I found no Rust issues that require changes before merging. The Rust credential detection builds a no-request transport and only performs local credential and profile validation.

Severity Findings
— None

Other findings not posted as comments

  • languages/python/crates/core/src/credentials.rs:56 — Optional: the Python messages for InvalidCrn, Region and InvalidAccessKey are not checked by any test.
  • languages/python/crates/core/src/credentials.rs:29 — Change before merge: no test builds a Cipher from a valid stash auth login profile.
  • languages/python/tests/test_cipher.py:151 — Optional: this test is named "names the name", but it does not check which field is named.
  • languages/python/python/stackencrypt/_redact.py:122 — Change before merge: getattr(instance, "__dict__") runs a custom __getattribute__ method.
How this review was made
Agent Model Review type Result
claude claude-opus-5-5 test-gap 6 found, 3 posted
claude claude-opus-5-5 rust 3 found, 3 posted
claude claude-opus-5-5 python 2 found, 2 posted
claude claude-opus-5-5 infracode 0 found, 0 posted
codex gpt-5.6-terra test-gap failed
codex gpt-5.6-terra rust 0 found, 0 posted
codex gpt-5.6-terra python 1 found, 0 posted
codex gpt-5.6-terra infracode 0 found, 0 posted

Plain language: the reader test did not run; only the word-and-shape check was applied.

Stack: position 7 of 15 (#1405, #1406, #1414, #1415, #1433, #1437, *️⃣ #1453, #1499, #1509, #1510, #1511, #1513, #1516, #1517, #1582). *️⃣ marks this pull request.

Context loaded: the description, 2 linked issue(s) and 4 discussion entries.

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.

3 participants