Skip to content

Commit 7bbf8d0

Browse files
authored
Merge pull request #1095 from cipherstash/feat/eql-plan-targets
feat(stack-encrypt)!: EQL types as plan field targets, from Go through the guest
2 parents 4a20345 + c8b498e commit 7bbf8d0

70 files changed

Lines changed: 9662 additions & 225 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.cargo/mutants.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ exclude_re = [
7373
'stack-encrypt/src/sem/mod\.rs:\d+:\d+: replace <impl MatchConfig for DefaultMatch>::options -> MatchOptions with Default::default\(\)$',
7474
# Both unit-context conversions explicitly return Self::default().
7575
'stack-encrypt/src/target/context\.rs:\d+:\d+: replace <impl From<\(\)> for (DeclaredContext|ExpectedContext<T>)>::from -> Self with Default::default\(\)$',
76+
# The resolver of a build without EQL types holds none: its `targets` IS
77+
# the empty list (`dynamic::target::NoTargets`), which a test asserts on
78+
# and `resolve` turns into `TargetError::NoTargets`; `vec![]` is the body.
79+
'stack-encrypt/src/dynamic/target\.rs:\d+:\d+: replace <impl TargetResolver for NoTargets>::targets -> Vec<TargetDescriptor> with vec!\[\]$',
7680
]
7781

7882
# Headroom over the measured baseline before a slow-but-correct mutant is
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cipherstash/eql': minor
3+
---
4+
5+
**The `eql-bindings` crate resolves an EQL type named as a string to its own Stack Encrypt plan** (`stack-encrypt` feature). `eql_bindings::encryption::targets` carries a catalog-generated table of every EQL type a data plan may name as a field target — its name across languages, family and suffix, the plaintext `ValueKind` it takes, the indexes it carries, its query twin, and whether the engine can produce it today with the reason when not — and `encrypt` / `decrypt` / `query` entry points that dispatch on the name and run the type's derived `EncryptFrom` / `DecryptInto`, resolving to the EQL value's JSON bytes through the engine's `Pending` so a guest batches it with the rest of a plan. This is the EQL half of EQL types as plan field targets (cipherstash/stack#1062): a Go data plan names `TextEq` and the guest returns the finished EQL value instead of assembling one. `TextEq` is the only producible type; every other name is refused with the plan's reason. The SQL surface and the TypeScript package are unchanged.

‎.github/workflows/tests-golang.yml‎

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ name: Tests (Go)
88
# wasi-check the stack crates build for wasm32-wasip1 with no JS-host or
99
# native-HTTP dependencies, the no-http shape passes its tests
1010
# and docs, both guests pass lint and tests and are built with
11-
# their import surfaces checked (the stack-encrypt guest twice:
12-
# the real build and the deterministic-kms test build), their
11+
# their import surfaces checked (the stack-encrypt guest four
12+
# times: the real build, the build with the EQL types, and the
13+
# deterministic-kms test build of each), their
1314
# sha256 is recorded, `go:test` runs against them, and
1415
# `go generate` leaves the tree unchanged.
1516
# go-lint golangci-lint, Linux only.
@@ -32,6 +33,9 @@ on:
3233
- packages/stack-encrypt/**
3334
- packages/stack-encrypt-derive/**
3435
- packages/stack-guest-abi/**
36+
# The eql guest build links eql-bindings.
37+
- packages/eql/crates/eql-bindings/**
38+
- packages/eql/crates/eql-domains/**
3539
- languages/golang/**
3640
- scripts/check-wasm-imports.py
3741
- scripts/go-binding-test.sh
@@ -54,6 +58,9 @@ on:
5458
- packages/stack-encrypt/**
5559
- packages/stack-encrypt-derive/**
5660
- packages/stack-guest-abi/**
61+
# The eql guest build links eql-bindings.
62+
- packages/eql/crates/eql-bindings/**
63+
- packages/eql/crates/eql-domains/**
5764
- languages/golang/**
5865
- scripts/check-wasm-imports.py
5966
- scripts/go-binding-test.sh
@@ -156,6 +163,18 @@ jobs:
156163
- name: stack-encrypt guest deterministic test build
157164
run: mise run wasm:guest:build:deterministic
158165

166+
# The build with the EQL types (ADR-0007, amended 2026-10-06), which
167+
# package encrypt/eql embeds and registers on import, and its
168+
# deterministic test build, which the hermetic tests of a TextEq field
169+
# load. Same import-surface gate. The build task prints both builds'
170+
# sizes: the plan asks for that measurement before the SDK settles on
171+
# one build or two.
172+
- name: stack-encrypt guest eql build and import-surface gate
173+
run: mise run wasm:guest:build:eql
174+
175+
- name: stack-encrypt guest eql deterministic test build
176+
run: mise run wasm:guest:build:eql:deterministic
177+
159178
- name: Credential guest lint and tests
160179
run: mise run wasm:auth-guest:test
161180

@@ -167,7 +186,7 @@ jobs:
167186
# same bytes rather than a stale or rebuilt guest.
168187
- name: Record the guests' checksums
169188
run: |
170-
for guest in encrypt/wasm/stack_encrypt_guest.wasm encrypt/testdata/stack_encrypt_guest_deterministic.wasm auth/wasm/stack_auth_guest.wasm; do
189+
for guest in encrypt/wasm/stack_encrypt_guest.wasm encrypt/testdata/stack_encrypt_guest_deterministic.wasm encrypt/eql/wasm/stack_encrypt_guest_eql.wasm encrypt/testdata/stack_encrypt_guest_eql_deterministic.wasm auth/wasm/stack_auth_guest.wasm; do
171190
(cd languages/golang && openssl dgst -sha256 "$guest" | awk '{print $NF}' > "$guest.sha256" && echo "$guest sha256 $(cat "$guest.sha256")")
172191
done
173192
@@ -180,6 +199,10 @@ jobs:
180199
languages/golang/encrypt/wasm/stack_encrypt_guest.wasm.sha256
181200
languages/golang/encrypt/testdata/stack_encrypt_guest_deterministic.wasm
182201
languages/golang/encrypt/testdata/stack_encrypt_guest_deterministic.wasm.sha256
202+
languages/golang/encrypt/eql/wasm/stack_encrypt_guest_eql.wasm
203+
languages/golang/encrypt/eql/wasm/stack_encrypt_guest_eql.wasm.sha256
204+
languages/golang/encrypt/testdata/stack_encrypt_guest_eql_deterministic.wasm
205+
languages/golang/encrypt/testdata/stack_encrypt_guest_eql_deterministic.wasm.sha256
183206
languages/golang/auth/wasm/stack_auth_guest.wasm
184207
languages/golang/auth/wasm/stack_auth_guest.wasm.sha256
185208
if-no-files-found: error
@@ -290,7 +313,7 @@ jobs:
290313
- name: The guests are the ones Linux built and checked
291314
run: |
292315
cd languages/golang
293-
for guest in encrypt/wasm/stack_encrypt_guest.wasm encrypt/testdata/stack_encrypt_guest_deterministic.wasm auth/wasm/stack_auth_guest.wasm; do
316+
for guest in encrypt/wasm/stack_encrypt_guest.wasm encrypt/testdata/stack_encrypt_guest_deterministic.wasm encrypt/eql/wasm/stack_encrypt_guest_eql.wasm encrypt/testdata/stack_encrypt_guest_eql_deterministic.wasm auth/wasm/stack_auth_guest.wasm; do
294317
want=$(cat "$guest.sha256")
295318
got=$(openssl dgst -sha256 "$guest" | awk '{print $NF}')
296319
if [ "$want" != "$got" ]; then
@@ -356,7 +379,7 @@ jobs:
356379
- name: The guests are the ones the wasi-check job built and checked
357380
run: |
358381
cd languages/golang
359-
for guest in encrypt/wasm/stack_encrypt_guest.wasm encrypt/testdata/stack_encrypt_guest_deterministic.wasm auth/wasm/stack_auth_guest.wasm; do
382+
for guest in encrypt/wasm/stack_encrypt_guest.wasm encrypt/testdata/stack_encrypt_guest_deterministic.wasm encrypt/eql/wasm/stack_encrypt_guest_eql.wasm encrypt/testdata/stack_encrypt_guest_eql_deterministic.wasm auth/wasm/stack_auth_guest.wasm; do
360383
want=$(cat "$guest.sha256")
361384
got=$(openssl dgst -sha256 "$guest" | awk '{print $NF}')
362385
if [ "$want" != "$got" ]; then

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,10 @@ mutants.out/
109109
# The Go module's embedded WASI guests: build outputs of `mise run
110110
# wasm:guest:build` and `mise run wasm:auth-guest:build`.
111111
languages/golang/encrypt/wasm/*.wasm
112+
languages/golang/encrypt/eql/wasm/*.wasm
112113
languages/golang/auth/wasm/*.wasm
113114
languages/golang/encrypt/wasm/*.sha256
115+
languages/golang/encrypt/eql/wasm/*.sha256
114116
languages/golang/auth/wasm/*.sha256
115117
# The deterministic-kms TEST build of the stack-encrypt guest, from `mise run
116118
# wasm:guest:build:deterministic`; under testdata so no binary embeds it.

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ Every npm package except EQL lives under `languages/typescript/`: packages in `l
9292
links are provenance only.
9393
- `packages/stack-auth`, `packages/stack-profile`, `packages/stack-kms`, `packages/stack-encrypt`, `packages/stack-encrypt-derive`, `packages/stack-guest-abi`: The Rust crates imported from `cipherstash/cipherstash-suite` with their history — `stack-auth` and `stack-profile` (published to crates.io, one version group), `stack-kms` (published to crates.io from 0.1.0, its own version group, re-exported by `stack-encrypt` as `stack_encrypt::kms`), `stack-encrypt` and `stack-encrypt-derive` (published to crates.io from 0.1.0, one version group; `eql-bindings`' `stack-encrypt` feature depends on them from the registry), and `stack-guest-abi` (`publish = false`). They are the members of the **root Cargo workspace**, with the three node binding crates below. See "Working on the Rust crates".
9494
- `languages/typescript/packages/auth`, `languages/typescript/packages/profile`, `languages/typescript/packages/stack-auth-wasm`: The node bindings of those crates. `@cipherstash/auth` (napi-rs v2) and its six `platforms/*` packages are published to npm from this repository by `release.yml` (`auth-artifacts`, `publish-auth`); a change to what it ships, the `stack-auth` crate included, needs an `@cipherstash/auth` changeset (`require-auth-npm-changeset.yml`). `@cipherstash/profile` and its platforms are private and never published; `@cipherstash/stack-auth-wasm` is private and builds the wasm that `@cipherstash/auth` ships. Their `build` and `test` scripts never invoke cargo; `build:native`, `build:debug` and `test:cargo` do.
95-
- `languages/golang`: The Go module — the SDK `encrypt` with its generated-code support `encrypt/gensupport` and the policy packages `encrypt/policy` and `encrypt/policy/protosource`; the credential package `auth`; the generator `stashgen` and its command `cmd/stashgen`; and `internal` (the shared guest plumbing and the `record` wire model). A wazero host with no cgo. Its two WASI guests (`encrypt/guest`, `auth/guest`) are detached Cargo workspaces built by `mise run wasm:guest:build` and `mise run wasm:auth-guest:build`; `mise run wasm:guest:build:deterministic` builds the seeded TEST build the hermetic Go tests use; the `.wasm` files they embed are gitignored. Generated `*_stash.go` files are committed and CI fails when `go generate ./...` changes one. There is no Go release process yet.
95+
- `languages/golang`: The Go module — the SDK `encrypt` with its generated-code support `encrypt/gensupport` and the policy packages `encrypt/policy` and `encrypt/policy/protosource`; the credential package `auth`; the generator `stashgen` and its command `cmd/stashgen`; and `internal` (the shared guest plumbing and the `record` wire model). A wazero host with no cgo. Its two WASI guests (`encrypt/guest`, `auth/guest`) are detached Cargo workspaces built by `mise run wasm:guest:build` and `mise run wasm:auth-guest:build`; `mise run wasm:guest:build:deterministic` builds the seeded TEST build the hermetic Go tests use; the `.wasm` files they embed are gitignored. The stack-encrypt guest has a second build with the EQL types (`mise run wasm:guest:build:eql`, cargo feature `eql`, linking `eql-bindings` by path; `wasm:guest:build:eql:deterministic` is its test build), embedded by `encrypt/eql` and registered on import, so a program whose generated code names an EQL type runs it (ADR-0007, amended). `encrypt/eql`'s types and `Types` table are generated from the EQL catalog by `eql-codegen` (`eql_gen.go`, written by `mise run types:generate` in `packages/eql` and drift-gated by `types:check` and `cargo test -p eql-codegen`). Generated `*_stash.go` files are committed and CI fails when `go generate ./...` changes one. There is no Go release process yet.
9696
- `e2e/*`: Cross-package end-to-end tests (package managers, supply chain, Prisma example README)
9797
- `languages/typescript/examples/*`: Working apps (basic, prisma, supabase-worker)
9898
- `docs/plans/*`: Internal design plans. User-facing documentation lives at https://cipherstash.com/docs (not in this repo).

‎docs/plans/2026-10-04-plan-builder.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,9 @@ The ciphertext inside it is a Stack Encrypt ciphertext, which starts with `stack
667667
"EQL v4" in this plan is the name of that form, and not a new envelope.
668668

669669
The engine produces one EQL type today: `TextEq`.
670+
Status (2026-10-06, #1062): `TextEq` is producible through the data plan's target form.
671+
`stashgen` accepts `encrypt_into=TextEq`, the guest build with the EQL types returns the finished value, and `encrypt/eql` holds the generated Go types; every other type is listed by `se_targets` with the reason it is not producible, and `stashgen` refuses it.
672+
Open question for Dan: an EQL value is stored under a table and a column, so a target field's label must be exactly `<table>/<column>`; a cipher extended with a tenant part has no column for the extended label, and a plan with a target field refuses the extension rather than dropping it.
670673
The other types wait for work in the engine:
671674

672675
- **Every family but `Text`:** how the family encodes a plaintext is not specified.

‎languages/golang/cmd/stashgen/README.md‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ Your program calls those functions, and it never builds or names a plan.
2121
type User struct {
2222
_ struct{} `stash:"context=users"`
2323
ID int64 `stash:"id,passthrough"`
24-
Email string `stash:"email,encrypt,index=equality;match"`
25-
Name string `stash:"name,encrypt"`
24+
Email string `stash:"email,encrypt_into=TextEq"`
25+
Name string `stash:"name,encrypt_into=TextEq"`
2626
}
2727
```
2828

@@ -40,12 +40,11 @@ Your program calls those functions, and it never builds or names a plan.
4040
```go
4141
encrypted, err := users.Encrypt(ctx, cipher, people)
4242
opened, err := users.Decrypt(ctx, cipher, encrypted)
43-
term, err := users.Fields.Email.Equality(ctx, cipher, "bob@example.com")
43+
query, err := users.Fields.Email.Query(ctx, cipher, "bob@example.com")
4444
```
4545

4646
6. Store the encrypted type.
47-
Each sealed field is one or more byte columns: `Email.Ciphertext`, `Email.Equality`, `Email.Match`.
48-
`encrypt.Ciphertext` and each term type implement `driver.Valuer` and `sql.Scanner`, so a database library binds and scans each one as bytes; map each one to its own column.
47+
Each `encrypt_into` field is one EQL column: `eql.TextEq` implements `driver.Valuer` and `sql.Scanner`, so a database library binds and scans it as the JSON a `public.eql_v3_text_eq` column holds.
4948

5049
7. Run the generator again after each change to the struct or to a tag.
5150
A change to the fields of the struct stops the build until you do.
@@ -148,10 +147,14 @@ It ignores its own output file when it loads the package, so a stale file does n
148147
The same input always gives the same file: fields keep their declared order, and the file carries no version and no time.
149148

150149
`stashgen` checks each declaration with the engine, and holds no copy of the engine's rules: it runs the WASI guest the SDK embeds and asks it, one field at a time, so the error names the field.
151-
It asks the engine for the EQL types it holds: each name, its plaintext type, its indexes and its query form.
152-
This build of the engine produces no EQL type, so `encrypt_into` is refused with "EQL types are not available yet"; the next release adds `TextEq` and `encrypt/eql`.
150+
It asks the engine for the EQL types it holds: each name, its plaintext type, its indexes, its query form, and whether the engine produces it today.
151+
The command links the build of the engine that holds the EQL types (`encrypt/eql`), so it can answer for every type; a generated file imports `encrypt/eql` only when it names one.
152+
The engine produces `TextEq` today; `encrypt_into` with any other type is refused with the type's name.
153153
Separate columns work today for four indexes: `equality`, `match`, `ore` and `ope`.
154154

155+
A field with `encrypt_into` is stored under its table and column, which is what an EQL value records in its `i`.
156+
A cipher extended with a tenant part (`cipher.Extend(...)`) has no column for the extended label, so it refuses a struct with an `encrypt_into` field; use `index=` columns for a tenant-extended struct until that rule is settled.
157+
155158
## When stashgen stops
156159

157160
`stashgen` stops with an error, and writes no file, for each of these.
@@ -225,5 +228,5 @@ A protobuf message with a `oneof` cannot be generated from a policy: protoc-gen-
225228

226229
## Status
227230

228-
The command runs the WASI guest the SDK embeds, so it needs the guest built: `mise run wasm:guest:build`.
231+
The command runs the WASI guest the SDK embeds, so it needs the guests built: `mise run wasm:guest:build wasm:guest:build:eql`.
229232
The library, `github.com/cipherstash/stack/languages/golang/stashgen`, takes any `Engine`; `stashgen.Generate` takes one with `WithEngine`, and `stashgen/enginetest` has a static one for tests.

‎languages/golang/cmd/stashgen/main.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ import (
1515
"os"
1616
"strings"
1717

18+
// The build of the engine that holds the EQL types: the generator
19+
// asks the embedded guest which EQL types it produces, so it links the
20+
// build that has them. Generated code imports encrypt/eql only when it
21+
// names one.
22+
_ "github.com/cipherstash/stack/languages/golang/encrypt/eql"
1823
"github.com/cipherstash/stack/languages/golang/stashgen"
1924
)
2025

0 commit comments

Comments
 (0)