Conversation
…ld is never handed the caller's context
Review of the context-parameter commit found the derive's where clause
and the docs disagreeing in both directions. A row whose fields all carry
literals left `__Ctx` unbounded, so `encrypt_into_with_context(&cipher,
tenant)` compiled against it and silently dropped `tenant` — while the
docs said the compiler turns that form away. And a `from` field with no
literal made the impl demand `SuppliedContext` regardless of the field's
type, so a row nesting another (all-literal) row lost the context-free
forms it had before, and the context the caller was then forced to pass
never reached a leaf. The same bound landed on `DecryptInto` for one-way
term fields that decryption never opens.
One rule replaces all of that: a `from` field is derived under its own
`context`, or under `()` if it has none — never under the caller's. Its
type decides whether `()` will do: a nested row accepts it, a leaf refuses
it at the field (the obligation is checked in the body, spanned at the
field type) until it is given a literal. A row therefore never passes the
caller's context anywhere and is implemented for `Ctx = ()` exactly, which
makes `encrypt_into` / `decrypt_from` the only forms that compile, as
documented. Handing every column of a row one shared context was the
cross-column transplant the per-field contexts exist to prevent, so
nothing legitimate is lost; `#[stash(row = ..)]` fills the literal in.
The derive's context bound is now `__Ctx: Clone` — the per-field bounds
already imply `EncryptContext` / `DecryptContext`, and the extra impl
lifetime went with it. `Record::derived()` replaces three inlined filters
and the by-field candidate list is computed once.
Also from the review:
- `EncryptFrom` / `DecryptInto` `on_unimplemented` notes said "if `{Ctx}`
is `()` … use `encrypt_into_with_context`" unconditionally, which never
fired for `()` (the `SuppliedContext` note wins) and gave false advice
on a source-type mismatch. Both now state the rule; `SuppliedContext`'s
note covers `from` fields and how a context type of your own opts in.
- The "extend with your own SEM type" recipe guarded with
`context.as_bytes().is_empty()` on an encoded `PrfContext`, which is
never true. `is_degenerate_aad` / `is_degenerate_prf_context` are
public; the recipe and the `PrefixTerm` example use them.
- `DecryptFrom<S, C>`'s blanket impl had dropped its `S: DecryptInto`
clause, so the trait held for every triple and meant nothing as a
bound. It mirrors `EncryptInto` now: no trait parameters.
- Decrypt leaves bound `Ctx: SuppliedContext<'c>` alone; it implies
`DecryptContext`.
- Stale spellings from the rename: `encrypt_into::<EqualityTerm>(..)`,
two-parameter `EncryptFrom<P, _>` / `DecryptInto<P, _>`, and the design
doc's `#[encrypted(source = ..)]` / "omit `source`".
Pinned by `tests/ui/row_with_context.rs` (`_with_context` against a row),
`tests/ui/from_leaf_without_context.rs` (a `from` leaf with no literal,
reported at the field) and `a_row_nests_in_a_row_without_a_context`.
Claude-Session: https://claude.ai/code/session_01HU1Bbw4eQp9kEEneXxDcKr
…_context sugar Review fixes on the context-parameter change: - A derived `DecryptInto` generic over the caller's context bounds it by `DecryptContext` again. A term field's `DecryptField` accepts any context (it opens nothing), so field bounds alone let a record whose ciphertext field carries a literal accept — and silently discard — any `Clone` value as its decrypt context. Pinned by an expansion test and a `compile_fail` doctest on `DecryptContext`. - `encrypt_into_with_context` / `decrypt_from_with_context` bound `Ctx: SuppliedContext`, so `()` is refused on the sugar and misusing a row now gets the guided E0277 on the decrypt side too, not a bare E0308. - `SuppliedContext: DecryptContext` is declared, not hand-mirrored, so the documented implication holds by construction; the transitional `is_degenerate_*` predicates now say in rustdoc that they are deleted when vitaminc#291 lands. - The empty-`context` derive error no longer advises a `from` field to drop the attribute — a dead end, since a `from` field is never handed the record's context — and the check runs after parsing so attribute order cannot change the advice. - Derive internals: one `FieldContext` classification replaces the four parallel projections of literal/unit/caller; the impl scaffolding is shared between the two derives; encrypt-side field bounds are spanned at the field type as decrypt's already were; the stray raw derived- field filter uses `Record::derived`, computed once per expansion. - The hand-written composite examples inherit the leaves' context policy through per-field bounds — the clauses the derive emits — instead of restating it, and RFC 0002 no longer claims a row "leaves the context unbounded" where the implementation is `()` alone. Claude-Session: https://claude.ai/code/session_01P5YHK3w6Kj9ajTnmkaXCHW
The Naming section still recorded `#[derive(Encrypted)]` as the macro name, and the implementation notes still called the derive unbuilt and the row snippet "future". `stack-encrypt-derive` ships `#[derive(EncryptFrom)]` / `#[derive(DecryptInto)]` under `#[stash(..)]`, each named after the trait it emits — which is also why the sketched noun could not stand: it names neither trait, and one noun cannot cover both directions. Claude-Session: https://claude.ai/code/session_01VEKAfJiDDhSxEZRAVJQJPX
Review findings from cipherstash/cipherstash-suite#2163: - The derived encrypt impl now bounds its caller context by `EncryptContext`, mirroring the decrypt side. Without it, a third-party leaf generic over its context let the raw `EncryptFrom::encrypt_from` accept — and silently discard — any `Clone` value as its context. Pinned by `a_caller_context_must_be_an_encrypt_context`. - The `encrypt_into_with_context` sketch in the design doc now shows the `Ctx: SuppliedContext<'c>` bound the shipped signature has. - The `seal_pending` doc no longer names `encrypt_into::<StackCipherText>`, a call that no longer compiles. - The `SuppliedContext` doc no longer claims coverage of composites containing `()`, and the crate re-exports `IntoPrfContext` / `PrfContext` so a context newtype needs no direct `vitaminc-prf` dependency. Claude-Session: https://claude.ai/code/session_01P5YHK3w6Kj9ajTnmkaXCHW
…s `from` and context A row no longer needs an attribute on any field. `row = User` says the record is a row of the struct `User`: every derived field is derived from the plaintext field of its own name, under the context `"<snake_case type>/<plaintext field>"` — `age` from `user.age` under `"user/age"`, a tuple row's `.0` under `"user/0"`. Both halves name the column, not the encrypted struct, so `#[stash(from = email_address)]` (the override for a name that differs) is derived under `"user/email_address"`; `#[stash(context = "..")]` is taken verbatim. Nothing is pluralised or otherwise guessed. `row` is exclusive with `plaintext`, and must name a struct directly. Because the inferred context is the AAD of every stored ciphertext in the column, renaming the plaintext type or a field is a data migration; the docs say to pin the old literal with `context = ".."` first. A field the plaintext does not have is reported by rustc at the field (`tests/ui/row_field_missing.rs`); `row` + `plaintext` at the attribute (`tests/ui/row_with_plaintext.rs`). Claude-Session: https://claude.ai/code/session_01HU1Bbw4eQp9kEEneXxDcKr
…nested` hands a field `()` `row = User` now requires `context = "users"` beside it; each field is derived under `"<context>/<field>"`. The prefix is part of the stored data's identity — the AAD of every ciphertext in the row and the domain of every term — so it is never inferred from the Rust type's name: two plaintext types with the same name in different modules can no longer silently share every column context (byte-identical index terms across their tables, ciphertexts transplantable between them), and renaming a struct can no longer silently change the AAD of every stored row. `#[stash(nested)]` opts a row field out of the inferred context: it is handed `()`, which a nested row accepts and a leaf refuses — the ()-handoff the docs promised now exists in row mode, not only under `plaintext = ..`. Also addressed from the same review: - `supplied_aad` / `supplied_prf_context` are the one public choke point for validating-and-encoding a supplied context; the `is_degenerate_*` predicates return to crate-private, and the third-party-leaf recipe goes through the choke point, whose signature survives the vitaminc#291 migration. - `DecryptField` carries a `#[diagnostic::on_unimplemented]` pointing a term-only bundle inside an auto-mode record at `#[stash(decrypt)]`. - The deferred exactly-one-decryptable check for generic records is pinned by a `compile_fail` doctest on `Decryptable` (trybuild runs `cargo check`, which never evaluates post-monomorphization consts, so a ui test cannot reach it). - The empty-container fail-fast loss and the caller context a literal-carrying record discards on decrypt are documented where they bite (the container impls, `DecryptContext`, the attributes guide). - The `SuppliedContext` roster notes its coupling to vitaminc's `IntoAad` implementor list and the orphan-rule consequence. - Both derives build field bounds through one shared `push_field_bounds`, and the generic-source vs listed-plaintexts emission fork collapses to one loop per derive. Claude-Session: https://claude.ai/code/session_01P5YHK3w6Kj9ajTnmkaXCHW
Missed in bd3b61824: docs/target-directed-encryption.md and RFC 0002 §7 still described the row prefix as inferred from the snake-cased type name. Claude-Session: https://claude.ai/code/session_01P5YHK3w6Kj9ajTnmkaXCHW
…ke point `is_degenerate_aad` went back to crate-private when `supplied_aad` became the one public way to validate and encode a supplied context, so the design doc's `StackCipherText` bridge no longer matched the impl it quotes. Claude-Session: https://claude.ai/code/session_01VEKAfJiDDhSxEZRAVJQJPX
Review finding on cipherstash/cipherstash-suite#2164: the row decrypt now runs before the field-alone plan decrypt, so the counter reads exactly 1 where the other counter assertions in the file are exact too, instead of the `>= 1` that hid the total. Claude-Session: https://claude.ai/code/session_01P5YHK3w6Kj9ajTnmkaXCHW
…h/claude/stack-encrypt-derive-row feat(stack-encrypt-derive): `#[stash(row = User, context = "users")]` infers each field's `from` and context
…ASI without reqwest Phase 1 of the stack-encrypt Go bindings (docs/stack-encrypt-go-bindings.md). On wasm32-wasip1 reqwest 0.13.4 selects a native backend (tokio-full and the aws-lc-sys TLS provider) that does not build for WASI, and it was the only thing standing between the three stack crates and the target. HTTP is host-provided under wazero, so it has to be out of the WASI build by construction: - `http` feature, default on, in all three crates: stack-encrypt/http -> stack-kms/http -> stack-auth/http -> dep:reqwest. stack-auth keeps the token model, `AuthStrategy`, `AuthStrategyFn` and `StaticTokenStrategy` unconditionally; every HTTP-speaking strategy, the refresh engine, device binding and `Token::refresh` sit behind the feature. stack-kms keeps `Client<C>`, key derivation and the key-source traits; `HttpConnection`, its options and `StackKmsBuilder` sit behind it. stack-encrypt keeps `StackCipher::builder().kms(..)`; `StackCipher::new()` and the from-environment `init` sit behind it. stack-kms and stack-encrypt take stack-auth as a path dep with `default-features = false` (a workspace dep's defaults cannot be turned off by a member). - `StackKms<C, Conn = HttpConnection>` with `StackKms::connect(opts, credentials, client_key)`: the transport-injecting constructor a host with its own `ZeroKMSConnection` builds through, and the only one without `http`. `ZeroKMSConnection` gains `ensure_base_url` / `has_base_url` so endpoint discovery from the token's `services` claim works over any connection (previously inherent to `HttpConnection`). - `Error::ConnectionInit` boxes the connection's own init error. - `StackCipher::builder()` lives on `impl StackCipher<FromEnv>` so it resolves without a type annotation whether or not `http` is on. - `wasm:wasi-check` now gates stack-auth, stack-kms and stack-encrypt (`--no-default-features`) alongside the core crates; the CI workflow's paths cover them. - A unit test drives `StackKms` end to end over the in-memory `TestConnection`. Verified: `mise run wasm:wasi-check` passes for all eight crates with no reqwest/hyper/aws-lc-sys in any wasip1 tree; `mise run lint` clean; 451 tests pass with `--all-features` and with `--no-default-features`; doc tests and rustdoc (`-D warnings`) pass. BREAKING CHANGE: stack-auth's `RequestError` tuple payload is now `Box<dyn std::error::Error + Send + Sync + 'static>` instead of `reqwest::Error`. Construct it via `RequestError::from(reqwest_error)` (or box the error yourself) instead of `RequestError(reqwest_error)`, and recover the concrete error with `.0.downcast_ref::<reqwest::Error>()` instead of using `.0` as a `reqwest::Error` directly. `source()` behaviour is unchanged; the `Display` message is now "Request to the auth server failed" (previously "HTTP request failed"). Claude-Session: https://claude.ai/code/session_01HU1Bbw4eQp9kEEneXxDcKr
…h item-level http gates Without the http feature the crate previously silenced dead-code analysis entirely via #![cfg_attr(not(feature = "http"), allow(dead_code))]. Gate the eleven affected helpers (URL massaging, clock sharing, refusal classification, token setters, AccessKey secret access) individually with #[cfg(feature = "http")] instead, so the compiler verifies the feature partition in both directions: no-http code reaching an http helper fails to compile, and newly dead code warns instead of being swallowed. Test fallout handled so no coverage regresses in the no-http run: - workspace_crn tests assign Token.region directly instead of the now http-only set_region, keeping workspace_crn covered without http - test_refresh_debug_does_not_leak_tokens loses its http gate (it never needed HTTP) so the Debug secret-leak regression test runs in the no-default-features shape the WASI guest ships - http-only test helpers (TestClock, crn_with_workspace, jwt_with_workspace, classify_issuance_failure_tests) are gated with their consumers The one honest residual is AccessKey's inner field: parsing is unconditional token-model API, but only the http-gated AccessKeyStrategy consumes the secret, so that single field keeps a scoped cfg_attr(not(http), allow(dead_code)). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDes81qQM2ConLobY5G5JD
Feature additivity (the two API-shape bugs): - stack-auth: RequestError has one definition with an always-boxed payload; the http-gated From<reqwest::Error> does the boxing. Its public field's type no longer changes under feature unification, which would have broken a no-http host the moment anything else in its graph enabled http. - stack-encrypt: Error::Config is unconditional with a boxed source (the http-gated From<StackKmsBuilderError> boxes), so the enum's variant set no longer tracks the feature. Build config: - stack-auth's workspace entry is now default-features = false (the same pattern as cllw-ore/cts-common); stack-kms and stack-encrypt use the workspace dep again instead of hand-written path+version pins, and the consumers that rely on the default transport re-enable it with features = ["http"] (cipherstash-client, cipherstash-cli, cts-web dev-dep, stack-auth node/wasm bindings). One version pin remains, at the root. Cargo.lock is unchanged. Docs and doctests: - cargo test --no-default-features now passes including doctests in all three crates: http-only examples (README via include_str, token_store, StackKmsBuilder quick-start, StackCipher::new) are doc-gated on the feature with short no-http fallbacks, and the no-http rustdoc's broken intra-doc links are fixed. RUSTDOCFLAGS=-D warnings is clean in both shapes; default-features doctest coverage is unchanged. API cleanups: - ZeroKMSConnection::ensure_base_url / has_base_url get default impls (no-op / true) with the first-value-wins contract documented, so transports that don't do endpoint discovery (TestConnection, and hosts that pin at init) no longer stub them; HttpConnection keeps its overrides. - The unused From<ConnectionInitError> for Error impl is deleted; StackKms::connect is the one construction path for Error::ConnectionInit. - StackCipherBuilder::new() (+ Default) is the canonical builder entry point; StackCipher::builder() stays as a thin alias on the phantom FromEnv impl for annotation-free inference. - token.rs's scattered per-test http gates collapse into one gated refresh_tests module; make_token and the Debug secret-leak test stay ungated so they keep running without http. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDes81qQM2ConLobY5G5JD
…-web dev-dep wasm:wasi-check only runs cargo check, so the no-default-features shape's unit tests, doctests, and rustdoc had no gate — the 7 doctest failures and 21 broken intra-doc links fixed in the previous commit merged green. Add wasm:no-http-test (per-crate cargo test + RUSTDOCFLAGS=-D warnings cargo doc, default features off, one crate per invocation so dev-dep feature unification can't switch http back on) and run it from test-wasi.yml, whose path filters already cover the three crates. cts-web's stack-auth dev-dep only uses StaticTokenStrategy, which is part of the unconditional token model, so it doesn't need the http feature. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDes81qQM2ConLobY5G5JD
Keep HttpConnection failure Display to the status line — full body and headers can carry sensitive response content into logs and are unbounded; they remain available via Debug. Align RequestError's message with its transport-agnostic shape: the payload is no longer necessarily an HTTP error, so say 'request to the auth server failed' instead. Claude-Session: https://claude.ai/code/session_01BpqczxAVwUsTYdCRWh9dYb
…x Phase 1 contract docs Review follow-ups from cipherstash/cipherstash-suite#2158: 1. `cargo test -p stack-encrypt --no-default-features` still resolved reqwest: the stack-kms dev-dependency didn't set `default-features = false`, so dev-dep feature unification pulled stack-kms/http -> stack-auth/http -> reqwest into the graph `wasm:no-http-test` claimed was HTTP-free. Disable defaults on the dev-dep (the tests only need `test-support` for FakeDataKeySource; none are HTTP-dependent). Verified: `cargo tree --no-default-features -e normal,dev,build -i reqwest` matches nothing, and the full no-default-features test/doctest run passes. 2. The plan doc named StaticTokenStrategy as part of the unconditional no-http surface, but it is (correctly) gated behind `cfg(any(test, feature = "test-utils"))` and has no production users. Revise the Phase 1 contract to name AuthStrategyFn (and the guest's HostTokenStrategy) as the supported production path; StaticTokenStrategy stays a test double. Claude-Session: https://claude.ai/code/session_01BpqczxAVwUsTYdCRWh9dYb
… manifests Review follow-up on cipherstash/cipherstash-suite#2158: two manifest comments still described StaticTokenStrategy as part of the unconditional no-`http` surface, contradicting the corrected plan and the actual `cfg(any(test, feature = "test-utils"))` gate. Name AuthStrategyFn as the production path and state where the test double lives. Comments only; no dependency or feature change. Claude-Session: https://claude.ai/code/session_01BpqczxAVwUsTYdCRWh9dYb
The variant had {0} in the message and #[source] on the same field, so
chain printers (anyhow {:#}, tracing) showed the inner error twice —
against this file's own convention that Display stays static and the
source chain carries the detail. Flagged by Toby on cipherstash/cipherstash-suite#2158.
Claude-Session: https://claude.ai/code/session_01P5YHK3w6Kj9ajTnmkaXCHW
The Go/wazero binding needs byte formats both languages agree on before the
guest is written. This freezes the two commitments stack-encrypt makes:
SealedValue leaf: to_bytes/from_bytes with the canonical layout
`version(1) ‖ iv(16) ‖ tag_len(u16 LE) ‖ tag ‖ local_ciphertext`. The
version byte is bound into every leaf's AAD through a new labelled
derivation — PAE("stack-encrypt/leaf", version, derived_aad, tag), replacing
the unlabelled (aad, tag) tuple — mirroring how vitaminc binds its inner
wire version, so bytes relabelled with a future version byte fail
authentication instead of parsing under the wrong rules. The derivation
bytes are pinned by a unit test; decode failures surface as the new
LeafBytesError.
Index terms: every term type now exposes its frozen encoding.
EqualityTerm is the 32 PRF bytes as-is and OreTerm/OpeTerm are the raw CLLW
ciphertext bytes — byte-identical to what the EQL layer hex-encodes into
hm/oc/op, so rows written through a binding compare against rows the
Rust/EQL path wrote. MatchTerm encodes its sorted positions as
little-endian u16s (EQL sends bf as a JSON integer array, so the
byte-string form is this crate's own). cllw-ore's variable-width
ciphertext types gain length-validating TryFrom<&[u8]> for the decode path
(the direction the existing From<Vec<u8>> FIXME asks for).
tests/frozen_bytes.rs carries the golden vectors (fixed hex the Go decoder
tests against) plus structural-rejection and real-leaf round-trip coverage;
tests/term_bytes.rs continues to pin the derivations these encodings wrap.
Part of CIP-3553 (stack-encrypt Go bindings), phase 2 of
docs/plans/stack-encrypt-go-bindings.md.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDes81qQM2ConLobY5G5JD
…te layout Expand SealedValue's frozen-encoding rustdoc with an offset table for the v1 envelope and a diagram of the two nested framings (this crate's envelope around vitaminc's LocalCipherText), spelling out where each version byte lives and which AAD derivation binds it. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDes81qQM2ConLobY5G5JD
…e formats - cllw-ore variable-width types: one private length predicate per type, applied by both `from_bytes(Vec<u8>) -> Result` and `TryFrom<&[u8]>`. The unchecked path is now `from_bytes_unchecked` (replacing the FIXME'd `From<Vec<u8>>`); it stays `pub` and `FromHex` stays permissive because cipherstash-client's ste_vec terms carry a tagged bit stream (65/66 bytes) that the byte-aligned rule would reject. Callers renamed. - `SealedValue` is valid-by-construction: `from_parts` and serde deserialisation reject tags longer than the u16 length field, so `to_bytes()` is infallible. `TryFrom<&[u8]>` rustdoc no longer claims a generic-codec justification. - Document that the labelled, versioned `leaf_aad` cannot open leaves sealed under the phase-1 AAD (plain AEAD failure, no version signal). - Reword the "Byte encodings" docs: same shape as v1 / cipherstash-client terms, values are not comparable across the two. Claude-Session: https://claude.ai/code/session_01AhxMmV52dSYwjAT8KENHRV
… byte APIs - `MatchTerm::from_positions`/`from_bytes` reject positions outside the filter size fixed by `O: MatchConfig`, so a mis-decoded position list fails loudly instead of producing a term that never matches. - Replace the stringly `TermError::MalformedTermBytes(&str)` with a `TermBytesError` enum (`PartialEq`), mirroring `LeafBytesError`; CLLW decode failures report the offending length rather than the deliberately opaque `cllw_ore::Error`. - Every term type now exposes `to_bytes()` and `TryFrom<&[u8]>`; `as_bytes()` only where a contiguous buffer exists. Plan doc states the per-type surface instead of "on every term type". - Reframe the match term's LE-u16 byte string as the frozen FFI transport encoding; the stored and queried contract is the position list. - Scope the `SealedValue` byte-format commitment to ciphertext and link the index-term encodings. Claude-Session: https://claude.ai/code/session_01AhxMmV52dSYwjAT8KENHRV
- Move the sealed-leaf AAD breaking change out of `SealedValue`'s rustdoc and into a new packages/stack-encrypt/CHANGELOG.md. Release history does not belong on the type. - Drop the cross-reference from `SealedValue`'s public docs to the `cipher` module docs. `mod cipher` is private, so its module-level `//!` docs render nowhere in `cargo doc` output except the source listing — the pointer sent readers to documentation the HTML does not contain. The leaf-AAD derivation it referred to is already stated inline. - Write `LocalCipherText` and `Aes256Cipher` as code spans rather than intra-doc links. Both are private imports of vitaminc types, so rustdoc resolves them only when dependency docs are built; under `--no-deps` (what CI and `wasm:no-http-test` run) the links silently degrade to literal `[Name]` text, with no warning. Code spans render the same either way. - Mark `LeafBytesError`, `TermBytesError` and `TermError` `#[non_exhaustive]` so the versioned decoders can gain variants without a source break. Claude-Session: https://claude.ai/code/session_01BpqczxAVwUsTYdCRWh9dYb
The seal path constructed SealedValue directly from the DataKeyWithTag a DataKeySource returned, without the tag_fits_length_field check every other construction site applies. A custom source returning a tag longer than the u16 length field would (in release builds) produce a leaf whose to_bytes saturates the length field but appends the whole tag, so from_bytes no longer inverts the encoding. seal_leaf now validates the generated tag before building the leaf, and a seal-path boundary test drives encrypt through a DataKeySource that inflates its tags past u16::MAX, asserting the seal fails rather than mis-encodes. Claude-Session: https://claude.ai/code/session_01P5YHK3w6Kj9ajTnmkaXCHW
The cipher module was private with its items re-exported at the crate root, so the 68 lines of module-level internals documentation (batching, AAD derivation, wire format) never appeared in cargo doc output — the crate docs pointed readers at src/cipher.rs source instead. The module is now pub (matching sem and target) and the crate docs link to it. The rustdoc lints this surfaces (a link to the private leaf_aad, two redundant explicit StackKms link targets) were fixed in the previous commit; cargo doc is warning-free with and without default features. Claude-Session: https://claude.ai/code/session_01P5YHK3w6Kj9ajTnmkaXCHW
…h/claude/stack-encrypt-wasi-phase2 feat(stack-encrypt): freeze the byte formats the crate owns (WASI phase 2)
…raits Two additive API changes the WASI guest (next commit) needs: - PendingStackCipherText::into_pending — turn a pending tree into a Pending request carrier without settling it, so several independently built trees (e.g. one per record field, decoded from FFI values that are not Clone) merge with Pending::zip/all and seal in one batched generate_keys call. seal() is now expressed through it; same sealing path either way. - sem re-exports CllwOreEncrypt/CllwOpeEncrypt: they already appear in the module's public bounds (ore_term, OreTerm, ...), so a caller writing a generic wrapper over the term APIs has to be able to name them without depending on cllw-ore directly. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDes81qQM2ConLobY5G5JD
…se 3) The wasm32-wasip1 guest at bindings/go/stackencrypt/guest (a detached workspace, like the fuzz crates), per phase 3 of docs/plans/stack-encrypt-go-bindings.md. Control stays in Rust: request assembly, key derivation, batching and AAD/PRF context binding run unmodified inside the guest; the host provides exactly two imports. - Exports (vitaminc guest ABI conventions: buffer registry with zeroizing dealloc, packed-u64 results, hostile-input validation, no handle-id reuse): se_alloc/se_dealloc, se_cipher_init/se_cipher_free, se_encrypt/se_decrypt (+_element), se_encrypt_record/se_decrypt_record, se_term. Status codes 1-4 match vitaminc's; 5-11 map the ZeroKMS request outcomes and term failures so the Go caller can tell a bad token from a tampered ciphertext. - Host imports (module cipherstash_transport): transport_send — cipherstash/cipherstash-suite#2099's import generalised to (method, url, headers, body) with 'name: value' line headers — and token_get (phase-1 auth: the host owns minting and refresh). WasiHostConnection implements stack_kms::ZeroKMSConnection over it, with the endpoint pinned from the init config or discovered from the token's services claim; HostTokenStrategy implements stack_auth::AuthStrategy over token_get. - Values cross in the vitaminc FFI codec (one codec, shared with the vitaminc guest); ciphertext-tree leaves are the frozen phase-2 SealedValue byte encoding, so a leaf lifted out of a tree is exactly what a database column holds. - Records: a plan {field -> {context, outputs: [c|eq|match|ore|ope]}} drives per-field ciphertexts and locally derived terms; all rows of a batch seal in one generate_keys via the new PendingStackCipherText::into_pending. Terms ride the result tree as passthrough bytes nodes. - Native tests (26) run the same ops the ABI drives against FakeDataKeySource: codec round trips, native-decryptable leaves, term bytes equal to the native sem derivations, a counting key source pinning the one-call batching, and status mapping for hostile inputs. The release .wasm's import surface is exactly WASI + cipherstash_transport. - mise tasks: wasm:guest:build, wasm:guest:test. Extracting the shared ABI modules into a common vitaminc crate is a follow-up in the vitaminc repository; the registry/session modules here are copies with pointers back. bridge.go and the integration harness land with the Go module (phases 4-5). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDes81qQM2ConLobY5G5JD
…nsports `HttpConnection` and the WASI guest each carried their own copy of the same table: 2xx must be JSON and deserialize, and 404/401/403/409 map to specific `ViturRequestErrorKind`s so callers can tell a bad token from a missing keyset without parsing strings. Two copies of a protocol contract drift. Move it to `connection::classify`, outside the `http` feature gate, so a guest built without `http` reaches the same verdicts as the default transport. `HttpConnection` now reads the response once and delegates; `BaseUrlUnresolved`, `FailureResponse` and `UnexpectedContentType` are public so a host bringing its own transport can return the same errors. `Display` on the two response errors stays the concise form landed in f3c87fbcc for cipherstash/cipherstash-suite#2158 — status and expectation only. Body and headers are unbounded, attacker-influenced text and these types' `Display` reaches logs; both remain available through `Debug`. Claude-Session: https://claude.ai/code/session_01BpqczxAVwUsTYdCRWh9dYb
`ClientKey::from_hex_v1` is strict lowercase hex, but the encodings a user actually holds are not: `secretkey.json` serialises standard padded base64, and hex pasted from elsewhere may be upper case. Front-ends that take key material from an untyped boundary — an environment variable, a config file, the WASI guest's FFI config object — were rejecting valid keys for their encoding alone. Add `from_encoded_v1`, the lenient counterpart, matching what `SecretKey::from_hex` and `EnvKeyProvider` already accept. Decoding stays constant-time (`base16ct` / `base64ct`) and the intermediate bytes are wiped on every path. Claude-Session: https://claude.ai/code/session_01BpqczxAVwUsTYdCRWh9dYb
The struct-tag source's search for facts tags inside embedded structs
keeps the structs on its path, so a recursive embedding (type Node struct
{ *Node; ... }) is searched once rather than forever; a struct's own
self-embedding is not searched at all, since its fields are the struct's
own, already read.
Plan.Validate refuses a nil type with the error PlanFromTags gives,
instead of dereferencing it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJc73YNcTeGaLDhP2P3LNM
…facts tags All() with no matchers matched every field, so a rule built from a slice that came back empty decided every field below it and could leave a classified field out of the plan with no error. Any() with none never fired. Both now panic where the rule is written, as a nil matcher does. Also pin the fail-closed refusal of a facts tag reached through an embedded pointer, which no test covered. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019YjmHJEM12fRq8XKwg7eaH
The embedded *list and *node fields exist only to make the types recursive; nothing reads them, so golangci-lint's unused check fails. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019YjmHJEM12fRq8XKwg7eaH
…h/dan/cip-4160-plan-policy
|
This was referenced Oct 2, 2026
The ORE and OPE string encodings run cllw-ore's orderize_string first: it decomposes each character canonically and drops anything that is not alphanumeric, whitespace or ASCII punctuation. The Go property test compared random Unicode strings by their raw UTF-8 bytes, so it failed whenever collation changed a string. For example, a string of private-use characters collates to "" and orders before any string with a letter in it. The fixed cases had the same problem: "\x7f" collates to "" too. Suite CI never ran this live test, because it needs credentials, so stack's CI port was the first to run it. Generate the random strings from characters that collation leaves unchanged, including multi-byte letters with no decomposition. Move "\x7f" out of the byte-order cases, and check that pairs differing only in dropped or decomposed characters give equal terms. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
auxesis
added a commit
that referenced
this pull request
Oct 2, 2026
mise merges config down the directory tree, so every tool in the root mise.toml is also part of the toolset in packages/eql and in languages/typescript/packages/protect-ffi. Their CI jobs run `mise install` (and `mise run`) in those folders, so each one now built cargo-nextest, cargo-llvm-cov, cargo-crap, cargo-mutants, cargo-fuzz and cargo-udeps from source, none of which they use. mise's cargo backend compiles a `cargo:` tool with the runner's default rustc (1.92 on the Ubuntu images), not the root's pinned 1.94.1. So a `latest` that raises its minimum Rust fails the install: cargo-udeps 0.1.61 needs cargo@0.96, which needs rustc 1.93. That broke the protect-ffi integration suite on #1001 and the new udeps job on #1003. The cargo tools move to mise.test.toml, which mise loads only for the `test` environment (`mise x --env test`, or MISE_ENV=test). Nested workspaces no longer inherit them. Each is pinned to an exact version that builds on the runners' rustc; cargo-udeps goes back to 0.1.60. The tasks that call them (crap:*, mutants:*, fuzz:*, and nextest in wasm:guest:test and wasm:auth-guest:test) now run them through `mise x --env test --`, as test:doc and the crap coverage step already did, so `mise run <task>` keeps working locally without MISE_ENV. Rust, Go, golangci-lint and wasm-pack stay in the root mise.toml. They are prebuilt downloads, and plan section 3.5 has protect-ffi inherit the Rust pin. That section's intent holds: the repository pins one version of each cargo tool, reached through mise. The pins now sit in the test environment rather than the default one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
…stack-crates Import the stack-* crates, the node bindings and the Go module from cipherstash-suite with their history (plan §7.2). The export is suite main e059b8ed5a5c8bd6c2f13ec621de8efbc4e2f433 run through git filter-repo; its head is 82a85b4. This merge is left untouched. Every fix is a separate commit on top. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
Plan §7.3, the first commit on top of the untouched import merge.
- Delete .github/imported-workflows/. The 13 suite workflows travelled for
history only; PR C ports what they did.
- Delete packages/stack-auth/package-lock.json, an empty stray npm lock.
- Delete the nested biome.json files in the auth and profile bindings, so
the root Biome config covers them.
- Mark @cipherstash/profile and its six platform packages private. It was
never published, and without the flag the release gate and changesets
would treat 0.35.0 as a JS package to publish with no binaries.
- Point the Go guest crates' cts-common, zerokms-protocol and recipher
path dependencies at the Phase 0 crates.io releases (=0.43.0, =0.12.31,
=0.3.1). Those paths do not exist in stack. The guests' Cargo.lock
files are refreshed in the workspace commit, once the root workspace
they reach through the stack-* path dependencies exists.
- Make the imported docs pass `lint:package-paths`, which fails on the
merge with seven references to crates that stayed in the suite:
- docs/fuzzing.md: trim to the set's six targets, dropping the three
cts-common ones. The suite keeps its side of the shared file.
- docs/auth-strategy-handover.md and docs/wasm-analysis.md: the export
moved them from the root into docs/, so their relative links gain
`../` or lose `docs/`.
- Name suite crates such as cts-common, cllw-ore and cipherstash-client
and the vitaminc crates without a path.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
Plan §7.3, before the reformat, so `code:fix` leaves these files alone. - target/ and **/target: Cargo build output at the new root workspace. - The auth binding's wasm/, built by build:wasm. - Every file @cipherstash/auth publishes from the tree: index.js, stack-auth-node.js, the four .mjs modules, the seven .d.ts files, README.md and LICENSE (plan §13.6). The release gate freezes the wrapper by comparing these bytes with @cipherstash/auth@0.44.0 on npm. The suite formatted them with double quotes and semicolons, so without the ignores the reformat rewrites ten of them and the gate refuses the tree. native.d.ts and index.d.ts are also generated by napi. - The profile binding's index.d.ts, which napi generates. - languages/golang/, which has no JavaScript Biome should own. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
Plan §7.3: `pnpm run code:fix` after the Biome ignores, in its own commit. The suite formatted these files with a nested Biome config (double quotes, semicolons, 80 columns); the root config uses single quotes and no semicolons. Formatting only, plus Biome's safe fixes. The files the frozen @cipherstash/auth publishes are ignored, so they keep the bytes that are on npm. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
The root Biome config lints the imported tests, which the suite's nested config did not (its linter was off). `pnpm run code:check` then failed on two `lint/correctness/noUnsafeOptionalChaining` errors: a cast of `failure?.error` followed by a plain `.help`. Carry the `undefined` into the cast and chain the property access, so a missing failure fails the assertion instead of throwing a TypeError. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
Plan §7.4, the workspace commit. The freeze and Dependabot follow in their own commit. Cargo: - Root Cargo.toml: resolver 2; the six stack-* crates and the three node binding crates as members; EQL, protect-ffi, the three fuzz crates and the two Go guests excluded, each its own workspace. - [workspace.package] with repository = cipherstash/stack, and version 0.35.0 for the three node binding crates that use version.workspace. - [workspace.dependencies]: the Phase 0 crates.io releases of the suite crates, pinned exactly (cts-common =0.43.0, cllw-ore =0.5.0, recipher =0.3.1, zerokms-protocol =0.12.31), the external crates with the suite's feature lists, and vitaminc* at 0.5.0. stack-auth and stack-profile are also listed, by path: members take them with `workspace = true`. - [profile.release] and [profile.dev] incremental, from the suite. - Cargo.lock seeded from the suite lock at f161a447f. The four suite crates were path packages there, so `cargo update -p` cannot name them; `cargo metadata` re-resolved only what changed. It added the four registry crates and cipherstash-config 0.42.3, which zerokms-protocol pulls, and changed nothing else. - Cargo.lock for all three fuzz crates, seeded from the root lock and pruned by `cargo metadata`; the suite gitignored them. The two guest locks are refreshed for the registry dependencies of the cleanup commit. `cargo metadata --locked` passes in all six workspaces. - .cargo/config.toml with the wasm32 getrandom rustflags, and .config/nextest.toml with profile.ci. - .gitignore: the fuzz crates' artifacts, coverage and grown corpus, mutants.out/, and the Go guests' built .wasm and .sha256 files. mise: root mise.toml (rust 1.94.1 with the wasm targets, the cargo tools, go 1.26 and golangci-lint for go:lint, wasm-pack for build:wasm, the five tasks.toml includes, and the wasm:* and go:* tasks repathed to languages/golang) and mise.test.toml. The node binding integration tasks now run in their new folders, copy from ../../../../target/debug, drop their `npm install` (which fails on the workspace:* peers; the root `pnpm install` covers it) and run `pnpm exec vitest run`. JavaScript: - pnpm-workspace.yaml lists the auth and profile platform folders. The bindings themselves are already selected by the languages/typescript/packages/* glob. - @cipherstash/auth: platform peers at workspace:*, and each platform package at 0.44.0, the version on npm; the suite's publish workflow rewrote both at publish. Scripts split so `build` and `test` never run cargo: build:native, build:debug, test (vitest and Biome) and test:cargo. build:wasm repathed to the stack-auth-wasm folder. The npx Biome 2.3.4 format scripts, tied to the deleted nested config, go. - @cipherstash/profile and @cipherstash/stack-auth-wasm split the same way; profile's platform optionalDependencies move to workspace:*. - turbo.json declares @cipherstash/auth#build:native outputs. - pnpm-lock.yaml gains the new importers and their dependencies. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
Plan §7.4 (Freeze, Gate change, Platform packages, Dependabot) and §13.6. The seven @cipherstash/auth packages now live here but keep publishing from cipherstash-suite until the arming PR (PR E). Release gate: - release-gate.mjs learns two more artefact shapes. A `files` entry lists tracked files: the gate hashes each with sha256, from disk and from the `npm pack` tarball it already fetches, and a mismatch names the file. A listed file missing on either side throws. A `noTreeBytes` entry declares, with a reason, that the tarball holds nothing the tree has; check C skips it. EQL's `field` entry is unchanged. Node's built-in crypto only, so the gate still needs nothing installed. - @cipherstash/auth and its six platform packages join FROZEN_PUBLISHERS and FROZEN_ARTEFACT_DIGESTS: the wrapper with the 15 tracked files it publishes, the platforms with noTreeBytes. Check A still blocks a version npm does not carry. Tests in release-gate.test.mjs. - lint-no-auth-changeset.mjs, modelled on the retired protect-ffi guard, with its self-test, a lint:auth-changeset script and a tests.yml step. It can only refuse: no-parked-changesets forbids the old .md.deferred escape hatch. - frozen-publisher-docs covers the auth freeze, so AGENTS.md and SECURITY.md describe it here, beside the map. Dependabot: a cargo entry for the root, the three fuzz crates and the two Go guests, each of which has a Cargo.lock, and gomod for /languages/golang. The cargo entry ignores cts-common, zerokms-protocol, recipher, cllw-ore and vitaminc*, which move in lockstep with the suite. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
Plan §7.6, the registrations that belong to PR B. - cargo-publish-opt-out: the root workspace, with stack-auth and stack-profile publishable, and the three fuzz crates and two Go guests with nothing publishable. Their `[workspace]` has no members, so the test reads a single-package workspace as one member, the root. - cargo-lock-freshness discovers locks by itself, and now asserts it sees the root lock and the five detached ones. It reads the version of a `version.workspace = true` crate from the workspace root, and expects eql-bindings in the two locks that build it rather than in every lock. - workflow-mise-setup: the root now has a mise config, so the comment and failure message stop saying it has none. The check still requires an explicit working_directory on every mise-action step, so an EQL job that forgets packages/eql still fails instead of reading the root config. - lint-typecheck-scope: the auth and profile platform folders as roots. The auth and profile packages themselves are already found through languages/typescript/packages. workflow-paths-filter-parity needs nothing here: PR B adds no workflow. eql-suite-ci and crates-ci wait for PR C, and lint-no-workflow-caching for the publish workflows of PR D. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
Plan §7.8, the meta commit. - AGENTS.md: the layout says packages/ holds Rust crates only, with bullets for the six stack-* crates, the node bindings and languages/golang; "two Cargo workspaces" becomes three; a new "Working on the Rust crates" section carries the suite's Fuzzing, Miri and Mutation testing notes, repathed. The CI those notes mention arrives with the CI port. The crate paths are spelled out, because lint:package-paths reads a `packages/stack-*` glob as packages/stack. - SECURITY.md: rows for @cipherstash/auth and its six platform packages, and a paragraph for the stack-auth and stack-profile crates and the Go module. - docs/agents/issue-tracker.md: the imported paths are tracked here, not in cipherstash-suite. - CODEOWNERS: packages/stack-*, languages/golang and the three node binding folders. - CONTRIBUTING.md: the auth packages' freeze and future fixed group, and the crates.io line for stack-auth and stack-profile. No changeset: nothing published changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
mise merges config down the directory tree, so every tool in the root mise.toml is also part of the toolset in packages/eql and in languages/typescript/packages/protect-ffi. Their CI jobs run `mise install` (and `mise run`) in those folders, so each one now built cargo-nextest, cargo-llvm-cov, cargo-crap, cargo-mutants, cargo-fuzz and cargo-udeps from source, none of which they use. mise's cargo backend compiles a `cargo:` tool with the runner's default rustc (1.92 on the Ubuntu images), not the root's pinned 1.94.1. So a `latest` that raises its minimum Rust fails the install: cargo-udeps 0.1.61 needs cargo@0.96, which needs rustc 1.93. That broke the protect-ffi integration suite on #1001 and the new udeps job on #1003. The cargo tools move to mise.test.toml, which mise loads only for the `test` environment (`mise x --env test`, or MISE_ENV=test). Nested workspaces no longer inherit them. Each is pinned to an exact version that builds on the runners' rustc; cargo-udeps goes back to 0.1.60. The tasks that call them (crap:*, mutants:*, fuzz:*, and nextest in wasm:guest:test and wasm:auth-guest:test) now run them through `mise x --env test --`, as test:doc and the crap coverage step already did, so `mise run <task>` keeps working locally without MISE_ENV. Rust, Go, golangci-lint and wasm-pack stay in the root mise.toml. They are prebuilt downloads, and plan section 3.5 has protect-ffi inherit the Rust pin. That section's intent holds: the repository pins one version of each cargo tool, reached through mise. The pins now sit in the test environment rather than the default one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
@cipherstash/profile's build:debug and build:native ran `napi build` with no `--dts`, so napi wrote its generated typings over the hand-written index.d.ts. Every job that builds the binding (tests.yml run-tests and tests-crates.yml, from the CI port) would leave the tree dirty and the package's types wrong. Both now pass `--dts native.d.ts`, as @cipherstash/auth does. The generated file is committed so a drift guard can diff it, and Biome leaves it alone like auth's. index.d.ts does not import it, and the package is private, so nothing it ships changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
`pnpm run test` now reaches the @cipherstash/auth and @cipherstash/profile vitest suites, which load the napi module. Their `test` scripts do not build it, and nothing else in run-tests does, so profile-store.test.ts failed with "Failed to load native binding for linux-x64" on this PR (Run Tests, Node 22 and 24). run-tests now builds both bindings with `build:debug` after the protect-ffi binding, before the test steps. The previous commit has `build:debug` write its typings to the committed native.d.ts, so the build leaves the tree clean. This step and that fix came from the CI port (#1003); they land here because this PR merges first and has to pass on its own. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
The root [workspace.dependencies] pinned both exactly, and stack-auth inherits the requirement when it is packaged, so stack-auth 0.43.0 would require cts-common =0.43.0 and zerokms-protocol =0.12.31. The suite then could not unify registry stack-auth with a later compatible cts-common patch: its [patch.crates-io] entry would go unused, and a second cts-common would break the Crn, Region and WorkspaceId types that stack-auth exposes. That defeats the cts-common release decision in plan section 13.7. Use caret requirements for the two crates. Cargo.lock still holds the exact versions, so this changes no resolved version: every lock passes cargo metadata --locked unchanged. recipher and cllw-ore stay exact, because only unpublished crates use them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
Rename the module from github.com/cipherstash/cipherstash-suite/bindings/go to github.com/cipherstash/stack/languages/golang, as plan section 3.4 decides. The old path names a private repository whose bindings/go folder the suite removal deletes, so a go get of it could never resolve. Nothing outside the module imports the old path, and no version of it was ever tagged or fetchable through the Go proxy. The change is generated by stack-migration/go-rename.sh (go mod edit -module, plus a text replace in the module's .go and .md files): 36 files, 53 lines. go.sum is unchanged, and go vet, go build and go test pass under the new path. At the freeze, re-run the script rather than replaying this commit, because the cutover re-export can bring the old path back in new files. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
go vet catches an import of the old suite module path, but not a stale go get line or pkg.go.dev link in a README. Check that go.mod declares the stack path, and that no tracked file outside docs/plans/ names the suite path, so the cutover re-export cannot bring it back unnoticed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
wasm:wasi-check greps cargo tree output for crates that must never link into a WASI guest (wasm-bindgen, web-sys, js-sys, reqwest, hyper, aws-lc-sys). CI sets CARGO_TERM_COLOR=always, so cargo wraps each line's tree prefix in ANSI colour codes, and the grep, which anchors on that prefix, can never match. A deliberate-break run on 2 October 2026 added wasm-bindgen to stack-kms, and the CI step still printed "all WASI crates compile with no JS-host or native-HTTP deps". Only the cargo check half of the step was working. Ask cargo tree for --color never. With CARGO_TERM_COLOR=always, the gate now fails on that break and passes on the clean tree. A script test checks every cargo tree call in the task asks for uncoloured output, and records why with a coloured sample the patterns cannot match. cipherstash-suite has the same task and the same CI setting, so its gate is dead too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
auxesis
force-pushed
the
refactor/typescript-to-languages
branch
from
October 2, 2026 08:07
229f473 to
b3ffccd
Compare
auxesis
force-pushed
the
build/import-stack-crates
branch
from
October 2, 2026 08:07
bceb0bc to
1ad1055
Compare
This was referenced Oct 2, 2026
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.
This PR brings six Rust crates, three bindings and the Go module into this repository from
cipherstash/cipherstash-suite, with 807 commits of their history. It then makes them build and test here. The crates arestack-auth,stack-profile,stack-kms,stack-encrypt,stack-encrypt-deriveandstack-guest-abi. The bindings, which let JavaScript call the Rust code, are@cipherstash/auth,@cipherstash/profileandstack-auth-wasm. The Go module lands inlanguages/golang.This is PR B of 6 in the stack crates import, which moves this code here from the private suite repository. The stack crates import plan in Linear lists every step, and Linear issue CIP-4274 tracks the work.
This PR is stacked on PR A, #1000. Its base branch is PR A's branch, so the diff shows only this PR's own changes. It stays a draft until the freeze on Friday 2 October 2026, Pacific time. The freeze is the window in which the six PRs merge in order: #1000, #1001, #1003, #1002, #1009, then #1010. The steps for that day are in §9.1 of the plan.
Check the import merge by its file list, and read the 15 later commits
The first commit,
d0cea7cc, merges the suite's history into this repository. It adds 408 files, so check its shape rather than reading it:git diff --name-status b3ffccd4 d0cea7ccprints 408 lines, and every one is an added file (A). No existing file changes.git rev-list --count d0cea7cc^2prints 807, the number of suite commits it brings in.Read the 15 commits after the merge line by line. You can skim three parts of them:
2371f731adds fourCargo.lockfiles, which hold 15,526 of its 16,410 added lines.fa05f243reformats 21 files and changes nothing else.a981590arenames the Go module in 36 files, 53 lines. A script on the migration machine,stack-migration/go-rename.sh, generated it.The import merge keeps the suite's history and changes no file
The merge brings in an export: a copy of the suite's history that keeps only the files that move.
git filter-repo, a tool that rewrites history to keep chosen paths, made it from suite commite059b8ed. The export's head is82a85b4d, with 807 commits and 408 files, and its history starts on 11 February 2026.The two histories share no commit, and the merge changes nothing inside the files. The suite's 13 workflows land in
.github/imported-workflows/, where GitHub never runs them, and the next commit deletes them. PR C ports the CI.Commit messages name suite PRs as
cipherstash/cipherstash-suite#NNNN. That way, GitHub does not link them to this repository's PRs with the same numbers.Each commit after the merge makes one change
359a6f18cleans up the imported files. It deletes the imported workflows and stray files, and fixes links in the moved docs. It marks@cipherstash/profileprivate, because it was never published. It also points the Go guests' dependencies at crates.io, because the suite folders they named do not exist here. The guests are the Rust crates that the Go module runs as WebAssembly.6bb5775dstops Biome, the formatter and linter, from changing two kinds of file. The first is the 15 files that@cipherstash/authpublishes. The release gate,scripts/release-gate.mjs, runs on every push tomainbefore anything publishes to npm. From commit 6, it compares those 15 files byte for byte with version 0.44.0 on npm, so a reformat would make it fail. The second is generated files.fa05f243reformats the imported bindings with this repository's Biome config. The suite used double quotes and semicolons, and this repository does not. The commit holds formatting changes and Biome's safe fixes only.d94d6a77fixes two lint errors that the reformat brings to light in the auth binding's tests. The suite's linter was off for these files. A missing failure now fails the assertion, instead of throwing aTypeError.2371f731makes the repository root a Rust and Go workspace. It adds the rootCargo.tomlandCargo.lock, and lockfiles for the three crates that hold the fuzz tests. It refreshes the two Go guests' lockfiles, and those five crates each stay a separate Cargo workspace. It also adds entries for mise, pnpm and turbo, and splits the binding scripts sobuildandtestnever run cargo. mise is the tool that pins this repository's tool versions and runs its tasks.8c86f4bffreezes@cipherstash/authand its six platform packages in the release gate. Each platform package holds the native binary for one platform, such asdarwin-arm64. A frozen package fails the gate if its version changes, or if its published files differ from npm's copy. The commit also adds Dependabot entries, and a temporary check,lint-no-auth-changeset, that fails if a pending changeset names an auth package. A changeset is a file in.changeset/that says which packages a change releases.542fa511adds the new workspaces to the repository checks that list every workspace, such as the checks on Cargo publish settings and lockfile freshness.1c52c8f6describes the crates, bindings and Go module inAGENTS.md,SECURITY.md,CODEOWNERSandCONTRIBUTING.md.f7b00319moves the six cargo test tools intomise.test.toml, at exact versions. mise passes the root config down to every subfolder, so the EQL and protect-ffi CI jobs built these tools too. They built them with the runner's Rust 1.92 and failed, becausecargo-udeps0.1.61 needs Rust 1.93. mise readsmise.test.tomlonly for test runs, so those jobs no longer get the tools.8e8dd7abmakes the profile binding write its generated typings tonative.d.ts, as the auth binding does. Before, its build overwrote the hand-writtenindex.d.ts, which left the tree changed and the package's types wrong.0dc76af1builds the auth and profile bindings intests.ymlbefore the test run. Without the build,pnpm run testfailed to load the profile binding. This step started in PR C and moved here, so PR B passes CI on its own.5147dd00givescts-commonandzerokms-protocolcaret version requirements, such as^0.43.0. A caret requirement accepts any later compatible version. The publishedstack-authinherits these requirements, so an exact pin would stop the suite sharing one copy ofcts-commonwith it. The lockfiles still hold the exact versions, so no resolved version changes.a981590arenames the Go module fromgithub.com/cipherstash/cipherstash-suite/bindings/gotogithub.com/cipherstash/stack/languages/golang. The old path names a folder in a private repository, sogo getcould never fetch it. Nothing outside the module imported the old path, and no version of it was ever tagged.d3524b87adds a test thatgo.moddeclares the new path, and that no file outsidedocs/plans/names the old one. A later export from the suite could bring the old path back, and this test would catch it.1ad1055dfixeswasm:wasi-check, which could never fail in CI. The check searchescargo treeoutput for crates such aswasm-bindgen, which must never be in a build for WASI. WASI is the WebAssembly System Interface that the guests target. CI turns on coloured output, and the colour codes stopped the search matching. The check now asks for--color never, and fails when a deliberate break addswasm-bindgentostack-kms.The shared suite crates come from crates.io
The imported crates depend on four crates that stay in the suite:
cts-common0.43.0,cllw-ore0.5.0,recipher0.3.1 andzerokms-protocol0.12.31. The root workspace takes them from crates.io. Phase 0 of the plan released them from suite main, and their source matches suite main.Publishing stays off until PR E
This PR brings
@cipherstash/authand its six platform packages here, but the release gate freezes them. A version bump of any of them makes the gate fail, so nothing publishes. PR E removes the freeze.The checks pass locally and in CI
cargo metadata --lockedpasses in all 8 Cargo workspaces. It fails if a lockfile is out of date.cargo tree -dlists crates that appear in more than one version. It shows onects-commonand one set of vitaminc crates.go:testandgo:lintpass.pnpm install --frozen-lockfile,code:checkandtest:scriptspass, with 1,020 script tests.@cipherstash/auth.Plain cargo test fails 3 tests, so CI uses nextest
Plain
cargo test --workspacefails 3stack-kmstests. A test helper sets environment variables while other tests run in parallel, so the tests race. nextest runs each test in its own process, so CI passes. PR C adds a test that stops any workflow running plaincargo testover the workspace.A Developers team member must merge this PR
mainrequires signed commits.git filter-reporewrote the 807 suite commits, so they carry no signature. A member of the GitHub Developers team must merge this PR, and bypass only the signature rule.Merge it with a merge commit, which keeps every commit, and never with a squash or a rebase. A squash would replace the 807 suite commits with one, and lose the history that this PR exists to keep.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a