Proposal G, PR1: zero-knowledge SavedDeck schema + deck cap setting - #85
Merged
Conversation
Adds SavedDeckKind/SavedDeck (docs/proposals/proposal-g-user-accounts-saved-decks.md §3) as a fresh model rather than resurrecting the dead Project/ProjectMember pair - see that section's "note on prior art" for why. One additive migration (0065), admin registration, and SAVED_DECK_MAX_PER_USER (env-driven, default 100, same idiom as MODERATORS_GROUP_NAME/CARD_REPORT_RATE) as the abuse-guard cap decision 4 calls for. The 5-per-user snapshot FIFO ring (decision 7) is deliberately NOT a setting - it's a fixed implementation safety valve per the spec, enforced in the view layer landing in the next PR. Scope note: this PR is schema-only. §7's authed-vote-tier machinery (AUTHED_VOTE_GATE_MODE, authed_vote_weight(), AbstractWeightedVote.account_tier) is explicitly scoped by the spec itself as "a separate, later build ... own migration and consensus-math change, own PR, own review - not bundled with this HOLD's core scope" and "does not block Proposal G building at its queued slot" - so it isn't included here, read as the spec's own deferral rather than an omission. Incidentally fixed a pre-existing bug found while editing admin.py: AdminTagAliasSuggestion declares actions=["accept_suggestions", "reject_suggestions"] but those two methods were indented under a different, unrelated ModelAdmin class further down the file (AdminCardScanLog), not under AdminTagAliasSuggestion itself - moved them back to the class that actually references them. Verified against a real local Postgres instance (this sandbox has no Django/DB by default - installed it plus enough of requirements.txt to import cardpicker.models cleanly): `makemigrations --check --dry-run` confirms 0065 exactly matches the model state (no drift), a full `migrate` applies cleanly end to end, and a manual smoke test confirms the conditional unique constraint (blocks duplicate deck names per owner, allows duplicate snapshot names, allows a snapshot to share a deck's name) and CASCADE delete-on-owner-removal all behave exactly as specified. `ruff`/`isort`/`black==22.8.0`/`mypy --config-file mypy.ini` all clean. Added test_saved_deck_model.py + a SavedDeckFactory, but could not execute them via pytest in this sandbox: cardpicker/tests/conftest.py's ES/Postgres fixtures are testcontainers-based (real Docker required) and one is session-autouse, so no test in this suite can run without Docker - the same nested-Docker limitation already documented in docs/troubleshooting.md. The tests are written to run in CI/local dev where Docker is available; not a substitute for that real pytest run, flagging the gap explicitly.
5 tasks
Adds §8 - the server must be cryptographically unable to read saved deck contents. Client-side WebCrypto only: PBKDF2-SHA256 >=600k iterations + per-user salt derives a master key; per-deck AES-256-GCM DEKs wrapped by it; the entire deck payload including the title is ciphertext server-side. A user-held recovery key (generated at passphrase creation, wraps the same master key) is the ZK-preserving recovery path; a Discord-gated, data-destroying account reset is the last resort if both are lost. No admin-side or Discord-derived decryption/escrow path exists or will be added - recorded as explicitly rejected. Supersedes §2/§3's originally-specified plaintext JSONField storage and its server-side name-uniqueness constraint (no longer enforceable once titles are encrypted - becomes a client-side-only check, a real behavior change documented in the new Consequences section). §2/§3's original text is left as historical record per this doc's existing convention of appending amendments rather than rewriting approved spec text (see proposal-b.md for the same pattern) - decision 10 and forward-pointer notes mark what's superseded. Adds decision 10, updates the header/status line, and the legal data-inventory paragraph for the owner's PIPEDA review. Next: revise PR1 (#85, still open/unmerged) to match this schema before it merges, then PR3 (opaque-blob API) and PR4 (crypto module + UX).
…intext name) Per §8's ZK amendment: SavedDeck no longer has name/state/is_public. It now stores ciphertext + ciphertext_nonce + wrapped_dek + wrapped_dek_nonce - the entire frontend Project shape, including the deck's own title, lives inside client-encrypted ciphertext the backend never inspects. The saveddeck_owner_name_unique_for_decks constraint is gone - it can't be enforced server-side once titles are encrypted (name-uniqueness becomes a client-side-only check, per §8's Consequences section). New UserCryptoProfile model (one per user, created at first save): PBKDF2 salt + iteration count (public-safe, not secret) plus two independently- wrapped copies of the same master key - one wrapped by the passphrase- derived key, one by the user-held recovery key. Both CASCADE-delete with the owner, same as SavedDeck. Regenerated migration 0065 (renamed 0065_usercryptoprofile_saveddeck.py) rather than adding a second one, since this branch/PR is still unmerged - verified via makemigrations --check --dry-run (no drift) and a full migrate against real Postgres. Updated SavedDeckFactory (now produces random opaque bytes instead of a plaintext name/state, exactly as valid here as a real client-produced blob since nothing server-side inspects it) and added UserCryptoProfileFactory. Rewrote test_saved_deck_model.py: confirms the name/state/is_public fields are genuinely gone, confirms two decks for the same owner never collide regardless of content, confirms the two wrapped-key slots are independently stored, confirms one-profile-per- owner and cascade delete for both models. Verified: ruff/isort/black==22.8.0/mypy all clean. Manual Django-shell smoke test against real Postgres covers every assertion in the rewritten test file (same testcontainers-Docker limitation as before prevents running pytest directly in this sandbox - CI has real Docker and will run it for real, as it already did for PR1's original test file).
6 tasks
Caught while designing PR4a's crypto module: the "Key design" bullet said
"a passphrase change re-wraps every deck's DEK" (echoing the owner's
original terse phrasing), but the "Recovery key" section - written from
the addendum's own explicit clarification ("a recovery key generated
before a later passphrase change still works... it wraps the master key,
which never changed") - only makes sense if the master key is a separate,
randomly-generated value that's wrapped (not derived) by the passphrase
key, and never regenerated. Under that model, DEKs (wrapped by the never-
changing master key) never need re-wrapping on a passphrase change either
- only the ONE master key (stored on UserCryptoProfile) does. The two
sections contradicted each other; fixed "Key design" and "Tests required"
to state the single, self-consistent model, and fixed a same-root error
in the legal data-inventory paragraph ("two wrapped copies of that deck's
encryption key" -> two wrapped copies of the user's one master key).
No schema/code impact - UserCryptoProfile (already merged into this PR)
only ever stored one master key's two wrapped copies per user, never
anything per-deck, so this was purely a documentation self-consistency
bug, not a shipped design flaw. Fixing it now, before PR4a's crypto
module gets built against the wrong mental model.
5 tasks
Expands the ZK amendment's "deck sharing" future-work bullet into a full design per the owner's addendum: key-in-URL-fragment share creation, unauthenticated recipient decrypt flow, revocation with an optional DEK-rotation-on-revoke option, and the tests required when PR-5 is eventually built. Nothing built in this commit — confirms the new SavedDeckShare table is additive to PR-1's schema and doesn't require changing SavedDeck/UserCryptoProfile.
This was referenced Jul 18, 2026
Closed
WilfordGrimley
added a commit
that referenced
this pull request
Jul 18, 2026
* Proposal G, PR1: SavedDeck model, migration, per-user deck cap setting Adds SavedDeckKind/SavedDeck (docs/proposals/proposal-g-user-accounts-saved-decks.md §3) as a fresh model rather than resurrecting the dead Project/ProjectMember pair - see that section's "note on prior art" for why. One additive migration (0065), admin registration, and SAVED_DECK_MAX_PER_USER (env-driven, default 100, same idiom as MODERATORS_GROUP_NAME/CARD_REPORT_RATE) as the abuse-guard cap decision 4 calls for. The 5-per-user snapshot FIFO ring (decision 7) is deliberately NOT a setting - it's a fixed implementation safety valve per the spec, enforced in the view layer landing in the next PR. Scope note: this PR is schema-only. §7's authed-vote-tier machinery (AUTHED_VOTE_GATE_MODE, authed_vote_weight(), AbstractWeightedVote.account_tier) is explicitly scoped by the spec itself as "a separate, later build ... own migration and consensus-math change, own PR, own review - not bundled with this HOLD's core scope" and "does not block Proposal G building at its queued slot" - so it isn't included here, read as the spec's own deferral rather than an omission. Incidentally fixed a pre-existing bug found while editing admin.py: AdminTagAliasSuggestion declares actions=["accept_suggestions", "reject_suggestions"] but those two methods were indented under a different, unrelated ModelAdmin class further down the file (AdminCardScanLog), not under AdminTagAliasSuggestion itself - moved them back to the class that actually references them. Verified against a real local Postgres instance (this sandbox has no Django/DB by default - installed it plus enough of requirements.txt to import cardpicker.models cleanly): `makemigrations --check --dry-run` confirms 0065 exactly matches the model state (no drift), a full `migrate` applies cleanly end to end, and a manual smoke test confirms the conditional unique constraint (blocks duplicate deck names per owner, allows duplicate snapshot names, allows a snapshot to share a deck's name) and CASCADE delete-on-owner-removal all behave exactly as specified. `ruff`/`isort`/`black==22.8.0`/`mypy --config-file mypy.ini` all clean. Added test_saved_deck_model.py + a SavedDeckFactory, but could not execute them via pytest in this sandbox: cardpicker/tests/conftest.py's ES/Postgres fixtures are testcontainers-based (real Docker required) and one is session-autouse, so no test in this suite can run without Docker - the same nested-Docker limitation already documented in docs/troubleshooting.md. The tests are written to run in CI/local dev where Docker is available; not a substitute for that real pytest run, flagging the gap explicitly. * Proposal G: zero-knowledge encryption amendment (owner-directed) Adds §8 - the server must be cryptographically unable to read saved deck contents. Client-side WebCrypto only: PBKDF2-SHA256 >=600k iterations + per-user salt derives a master key; per-deck AES-256-GCM DEKs wrapped by it; the entire deck payload including the title is ciphertext server-side. A user-held recovery key (generated at passphrase creation, wraps the same master key) is the ZK-preserving recovery path; a Discord-gated, data-destroying account reset is the last resort if both are lost. No admin-side or Discord-derived decryption/escrow path exists or will be added - recorded as explicitly rejected. Supersedes §2/§3's originally-specified plaintext JSONField storage and its server-side name-uniqueness constraint (no longer enforceable once titles are encrypted - becomes a client-side-only check, a real behavior change documented in the new Consequences section). §2/§3's original text is left as historical record per this doc's existing convention of appending amendments rather than rewriting approved spec text (see proposal-b.md for the same pattern) - decision 10 and forward-pointer notes mark what's superseded. Adds decision 10, updates the header/status line, and the legal data-inventory paragraph for the owner's PIPEDA review. Next: revise PR1 (#85, still open/unmerged) to match this schema before it merges, then PR3 (opaque-blob API) and PR4 (crypto module + UX). * Proposal G, PR1 revision: zero-knowledge schema (opaque blobs, no plaintext name) Per §8's ZK amendment: SavedDeck no longer has name/state/is_public. It now stores ciphertext + ciphertext_nonce + wrapped_dek + wrapped_dek_nonce - the entire frontend Project shape, including the deck's own title, lives inside client-encrypted ciphertext the backend never inspects. The saveddeck_owner_name_unique_for_decks constraint is gone - it can't be enforced server-side once titles are encrypted (name-uniqueness becomes a client-side-only check, per §8's Consequences section). New UserCryptoProfile model (one per user, created at first save): PBKDF2 salt + iteration count (public-safe, not secret) plus two independently- wrapped copies of the same master key - one wrapped by the passphrase- derived key, one by the user-held recovery key. Both CASCADE-delete with the owner, same as SavedDeck. Regenerated migration 0065 (renamed 0065_usercryptoprofile_saveddeck.py) rather than adding a second one, since this branch/PR is still unmerged - verified via makemigrations --check --dry-run (no drift) and a full migrate against real Postgres. Updated SavedDeckFactory (now produces random opaque bytes instead of a plaintext name/state, exactly as valid here as a real client-produced blob since nothing server-side inspects it) and added UserCryptoProfileFactory. Rewrote test_saved_deck_model.py: confirms the name/state/is_public fields are genuinely gone, confirms two decks for the same owner never collide regardless of content, confirms the two wrapped-key slots are independently stored, confirms one-profile-per- owner and cascade delete for both models. Verified: ruff/isort/black==22.8.0/mypy all clean. Manual Django-shell smoke test against real Postgres covers every assertion in the rewritten test file (same testcontainers-Docker limitation as before prevents running pytest directly in this sandbox - CI has real Docker and will run it for real, as it already did for PR1's original test file). * Proposal G, PR3: opaque-blob saved-decks API Per §3/§8: require_authenticated decorator (cardpicker/security.py) plus 7 endpoints - 2/savedDecks/ (list), 2/saveDeck/ (upsert), 2/loadDeck/, 2/deleteDeck/, 2/cryptoProfile/ (GET), 2/saveCryptoProfile/ (upsert), 2/resetSavedDecks/ (the destructive last-resort reset). Every ciphertext/nonce/wrapped-key field is base64 in transit, opaque to the backend - stored and returned faithfully, never decrypted or inspected. - saveDeck upserts by key (null creates, existing key updates in place if owned else 403); kind defaults to "deck"; a "snapshot" create skips the SAVED_DECK_MAX_PER_USER cap entirely and prunes the owner's snapshot rows to the newest SAVED_DECK_SNAPSHOT_RING_SIZE (5, a fixed constant per decision 7, not a setting) afterwards. - No server-side name-uniqueness check - can't exist once titles are encrypted (§8's Consequences). The old spec's renameDeck endpoint is gone too: renaming is now just a normal saveDeck update-by-key call, since there's no server-visible name to rename. - get_saved_decks returns full per-deck ciphertext, not just metadata - the deck's title lives inside it, so the client must decrypt each row to render "My Decks"; there's no lighter-weight name to return instead. Documented as a real, closed-eyes tradeoff, not solved differently, since §8 enumerates the stored fields as an exhaustive list ("nothing else"). - saveCryptoProfile is an upsert covering both first-save creation and a passphrase change (which only ever replaces this one row via update_or_create - deck ciphertext/wrapped-DEKs are never touched). kdfIterations is checked against SAVED_DECK_MIN_KDF_ITERATIONS (default 600,000, matching §8's own floor) as a defensive floor against a buggy/malicious client persisting a weak key derivation. - resetSavedDecks requires an explicit confirm:true and deletes every SavedDeck + the crypto profile for the requesting user - the "lost both keys" last resort from §8's Account reset design. No admin-side or Discord-derived decryption path exists anywhere in this stack, by design (§8's "Explicitly rejected"). New JSON schemas under schemas/schemas/endpoints/ for all 7 request/response shapes, regenerated schema_types.py/schema_types.ts via quicktype. Caught and fixed a real quicktype naming collision: adding schemas with a "kind" enum property caused quicktype's disambiguation to rename the pre-existing generic `Kind` type (used only by VoteQueueRequest before now) to `VoteQueueRequestKind` - fixed the two import sites (views.py, store/api.ts) that referenced the old name. Also had to isort+black the raw quicktype Python output and prettier the raw TypeScript output - the committed files are always post-processed, not raw generator output, and diffing against the unprocessed version produces a huge spurious diff. Verified against real Postgres via Django's test client (not pytest - same testcontainers/Docker limitation as before): a 17-assertion smoke script covering every endpoint - anonymous rejection, crypto-profile creation with the iteration floor enforced, deck create/update-in-place, list scoping, ownership 403s on load/save/delete across two different owners, cap enforcement with the friendly message, snapshot-ring pruning to exactly 5 after creating 8, delete, and the destructive reset flow's confirm requirement and full cleanup - all passed. Added test_saved_deck_views.py (pytest, mirrors test_moderation_views.py's ownership/403 pattern) covering the same ground for real CI to run, which has actual Docker (as it already did for PR1's model tests). ruff/isort/black==22.8.0/mypy clean on the backend; tsc/eslint/prettier clean and the full 304-test jest suite passes on the frontend. * CI: fix isort drift (local isort 8.0.1 vs pinned 5.12.0 collapsed an import differently) Ran the correctly-pinned isort==5.12.0 (matching .pre-commit-config.yaml) locally after CI caught a real drift my newer local isort didn't - it collapses a 3-line wrapped import into one line where newer isort leaves it wrapped. No semantic change, purely the import statement's formatting. --------- Co-authored-by: Claude <noreply@anthropic.com>
4 tasks
WilfordGrimley
pushed a commit
that referenced
this pull request
Jul 18, 2026
Task-end wiki/docs check (CLAUDE.md): this changed what a USER sees (My Decks page, editor Save/breadcrumb, navbar sign-in) - all 5 sequenced PRs (#85, #86, #94, #89, #93) are now merged, so there's real behavior to document. Covers the zero-knowledge crypto mental model, backend endpoints/constants, frontend file map, the still-design-only PR-5/PR-6 addenda, and the owner-only Discord-credentials/legal-review pointers. Added to docs/README.md's flat index. Wiki note (cloud session, per CLAUDE.md convention): the project's GitHub wiki itself (a separate, generated-view target from docs/) likely wants a new "Saved Decks" user-facing page once this feature is visible in production - flagging here rather than editing it directly, since that's the documented cloud-session convention.
WilfordGrimley
added a commit
that referenced
this pull request
Jul 19, 2026
…gns, docs (design/docs only) (#99) * Proposal G spec: PR-6 design for deck portability (design only) Formalizes what the zero-knowledge, server-unbound crypto design already implies: export/import of the complete encrypted bundle (no unlock required for export - it's the same ciphertext the server already holds), a versioned public format as the actual portability contract, a standalone decrypt tool as the trust anchor ("if this site vanishes tomorrow, your decks are still yours"), honest offline-attackability limits, and an explicit rejection of any server-bound key material. Nothing built in this commit - the owner's addendum was explicit that this lands with a later PR-6. Also updates the doc's stale header status line (still said "BUILDING... PR1/PR2 opened" from before any of the 5 sequenced PRs had merged) now that schema+backend (#85), sign-in relocation (#86), the saved-decks API (#94, recreated after #88's base-deletion auto-close), the crypto module (#89), and the frontend UI wiring (#93) have all landed on master - and adds the portability sentence to the legal data-inventory paragraph, per the addendum's explicit instruction. * docs: add docs/features/saved-decks.md now that Proposal G has merged Task-end wiki/docs check (CLAUDE.md): this changed what a USER sees (My Decks page, editor Save/breadcrumb, navbar sign-in) - all 5 sequenced PRs (#85, #86, #94, #89, #93) are now merged, so there's real behavior to document. Covers the zero-knowledge crypto mental model, backend endpoints/constants, frontend file map, the still-design-only PR-5/PR-6 addenda, and the owner-only Discord-credentials/legal-review pointers. Added to docs/README.md's flat index. Wiki note (cloud session, per CLAUDE.md convention): the project's GitHub wiki itself (a separate, generated-view target from docs/) likely wants a new "Saved Decks" user-facing page once this feature is visible in production - flagging here rather than editing it directly, since that's the documented cloud-session convention. * Proposal G spec: PR-7 design for art provenance (design only) Per-slot provenance (driveId, sourceName, sourceType, optional contentPhash, indexedBy) in a future deckPayload version (bumps formatVersion per PR-6's own versioning rule), so an un-indexed slot renders a direct-from-drive thumbnail with a "not in this catalog" badge + origin link instead of breaking. States the moderation-bypass rationale explicitly (user's own private data, client-side fetch, never served/cached by this server) rather than leaving it implicit. XML 2.0 gains three optional, backwards-compatible attributes for third-party phash->federation-verdict joins. Hard line: provenance never enters the federation verdict export, which stays conclusions-only. Addendum clarifications folded in: importing any XML version with un-indexed drive IDs still leaves those slots viewable via the same direct-drive rendering (the badge/link only appear with 2.0+ provenance present); web PDF export of un-indexed slots is explicitly out of scope for PR-7 (v1 answer is view + print-via-desktop-tool guidance, not a foregone-conclusion export fallback). Nothing built - per the owner's explicit instruction, this is spec-only. Also updates docs/features/saved-decks.md's "not yet built" list and the proposal doc's Future-work/header pointers to include PR-7 alongside PR-5/PR-6. * docs/README.md: fix stale Proposal G status + PR-5/6/7 addenda references The Plans & proposals status table still said HOLD for Proposal G even though the core build has fully shipped (only the PR-5/6/7 addenda remain HOLD) - matches proposal-c's existing PARTIAL precedent for the same shape (some shipped, some still HOLD). Also fixed the features/saved-decks.md summary bullet, which still said "PR-5/PR-6" before PR-7 was added. * Proposal G spec: PR-6 revision/modifiedAt fields + deck roaming note Two small fields added to PR-6's encrypted-payload envelope: revision (int, incremented per save) and modifiedAt (timestamp) - private inside the payload like everything else, bumping formatVersion per PR-6/PR-7's shared versioning rule. Purpose: makes an export/import round-trip self-describing (a bundle can be compared against the server's current copy without any server-side plaintext comparison) and seeds any future cross-instance sync with conflict-detection for free. Also adds a "Deck roaming" future-work paragraph after "Deck sharing": cross-instance blob sync is ZK-compatible in principle (only ciphertext would travel, never keys) but is a full protocol in its own right (discovery, consent, conflict surfacing, deletion propagation) - explicitly out of scope until federation has real peers to sync between. Manual export/import (PR-6) is the supported path today; the new revision/modifiedAt fields exist in part to make that manual path safe without committing to automatic sync's unsolved questions. Nothing built - design-only, per the owner's instruction. --------- Co-authored-by: Claude <noreply@anthropic.com>
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.
Build per
docs/proposals/proposal-g-user-accounts-saved-decks.md. First of 4 sequenced PRs (schema+backend → Discord OAuth UX [#86] → saved-decks API → frontend).Revised mid-review: the owner directed a zero-knowledge encryption design (§8, added to the spec in this same PR) partway through review — the server must be cryptographically unable to read saved deck contents. This PR's schema was revised in place (still unmerged, so no follow-up migration needed) to match.
Description
SavedDeck(revised): no morename/state/is_public— nowciphertext+ciphertext_nonce+wrapped_dek+wrapped_dek_nonce, opaque to the backend by design. The oldUniqueConstrainton(owner, name)is gone — it can no longer be enforced server-side once titles are encrypted (this becomes a client-side-only check, a real documented behavior change from the original decision 4/7).UserCryptoProfile(new): one per user, created at first save — PBKDF2 salt + iteration count (public-safe, not secret) plus two independently-wrapped copies of the same master key (passphrase slot, recovery slot). Both models CASCADE-delete with the owner.SAVED_DECK_MAX_PER_USERsetting (default 100): unaffected by the ZK revision — still a plain row-count abuse guard onkind=deckrows, same idiom asMODERATORS_GROUP_NAME/CARD_REPORT_RATE.SavedDeck— ciphertext isn't inspectable).Incidental fix (unchanged from the original PR): found and fixed a pre-existing bug in
admin.py—AdminTagAliasSuggestion'sactionsreferenced two methods that were indented under an unrelatedModelAdminclass further down the file. Moved them back to the class that actually references them.Checklist
pre-commitand installed the hooks withpre-commit installbefore creating any commits.requirements.txtlocally against a real local Postgres instance (not present in this sandbox by default).python manage.py makemigrations --check --dry-run cardpickerconfirms migration 0065 exactly matches the model state — no drift, both before and after the ZK revision.python manage.py migrateapplies cleanly end to end.SavedDeckgenuinely has noname/state/is_publicattributes; two decks for the same owner never collide regardless of content;UserCryptoProfile's two wrapped-key slots are independently stored; one profile per owner is enforced;owner.delete()cascades to bothSavedDeckandUserCryptoProfile.ruff check,isort --check-only,black==22.8.0 --check,mypy --config-file mypy.ini MPCAutofill/all clean.test_saved_deck_model.pyrewritten for the new schema, but still could not execute it viapytestin this sandbox —conftest.py's ES/Postgres fixtures are testcontainers-based (real Docker required) and one is session-autouse. CI has real Docker and ran the original version of this file successfully on the first push; the rewritten version is written to run the same way there.Merge-time checklist
Generated by Claude Code