Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/bench-eql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ on:
- "packages/eql/docker/**"
- "packages/eql/README.md"
- "packages/eql/SUPABASE.md"
- "conformance/stack-encrypt/**"
- "packages/stack-encrypt/**"
- "packages/stack-encrypt-derive/**"
- "packages/stack-kms/**"
Expand Down
16 changes: 12 additions & 4 deletions .github/workflows/crap-crates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ env:
CARGO_NET_GIT_FETCH_WITH_CLI: true
NEXTEST_PROFILE: ci

# cargo-crap 0.6.1's prebuilt GNU/Linux binary needs GLIBC 2.39 (Ubuntu 24.04).
# Keep the runner and its mise cache on that image. The gate itself is unchanged.
jobs:
crap-stack-auth:
name: CRAP (stack-auth)
runs-on: blacksmith-16vcpu-ubuntu-2204
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
Expand All @@ -74,16 +76,19 @@ jobs:
working_directory: .
cache: true
# Must name runs-on's image; see scripts/__tests__/workflow-mise-cache-image.test.mjs
cache_key_prefix: mise-v1-ubuntu-2204
cache_key_prefix: mise-v1-ubuntu-2404

- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2

- name: Check the CRAP executable before collecting coverage
run: cargo crap --version

- name: Run the CRAP gate
run: mise run crap:stack-auth -- --format github

crap-stack-encrypt:
name: CRAP (stack-encrypt)
runs-on: blacksmith-16vcpu-ubuntu-2204
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
Expand All @@ -97,9 +102,12 @@ jobs:
working_directory: .
cache: true
# Must name runs-on's image; see scripts/__tests__/workflow-mise-cache-image.test.mjs
cache_key_prefix: mise-v1-ubuntu-2204
cache_key_prefix: mise-v1-ubuntu-2404

- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2

- name: Check the CRAP executable before collecting coverage
run: cargo crap --version

- name: Run the CRAP gate
run: mise run crap:stack-encrypt -- --format github
2 changes: 2 additions & 0 deletions .github/workflows/test-eql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ on:
# cannot see them (a task body names a cargo package, not a path); the
# manifest walk in eql-suite-ci.test.mjs does, and fails when one of them
# is missing from this list.
- "conformance/stack-encrypt/**"
- "packages/stack-encrypt/**"
- "packages/stack-encrypt-derive/**"
- "packages/stack-kms/**"
Expand Down Expand Up @@ -208,6 +209,7 @@ jobs:
- "packages/eql/docker/**"
- "packages/eql/README.md"
- "packages/eql/SUPABASE.md"
- "conformance/stack-encrypt/**"
- "packages/stack-encrypt/**"
- "packages/stack-encrypt-derive/**"
- "packages/stack-kms/**"
Expand Down
46 changes: 45 additions & 1 deletion .github/workflows/tests-golang.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ on:
- scripts/go-binding-test.sh
- scripts/stack-conformance.sh
- scripts/stack-conformance-selection.mjs
- scripts/stack-eql-conformance.mjs
- Cargo.toml
- Cargo.lock
- .cargo/**
Expand Down Expand Up @@ -99,6 +100,7 @@ on:
- scripts/go-binding-test.sh
- scripts/stack-conformance.sh
- scripts/stack-conformance-selection.mjs
- scripts/stack-eql-conformance.mjs
- Cargo.toml
- Cargo.lock
- .cargo/**
Expand Down Expand Up @@ -144,6 +146,18 @@ jobs:
with:
persist-credentials: false

# Native EQL conformance uses the EQL workspace's toolchain. Install it
# before root setup, so ordinary guest commands retain the root pin.
# Its local DATABASE_URL must not enable database tests in this job.
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4
with:
version: 2026.4.0
install: true
working_directory: packages/eql
env: false
cache: true
cache_key_prefix: mise-v1-ubuntu-2204

- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4
with:
version: 2026.4.0
Expand Down Expand Up @@ -258,6 +272,17 @@ jobs:
echo "RLIMIT_MEMLOCK: $(ulimit -l) KiB"
mise run go:test

- name: Required EQL and JSON conformance
env:
STACK_CONFORMANCE_REPORT_DIR: ${{ runner.temp }}/eql-conformance-fast
run: mise run test:conformance:eql

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: eql-conformance-fast
path: ${{ runner.temp }}/eql-conformance-fast/*.json
if-no-files-found: error

- name: Fresh Go scalar artifacts open in Rust
env:
STACK_CONFORMANCE_INPUT: ${{ runner.temp }}/stack-conformance-go.json
Expand Down Expand Up @@ -392,6 +417,8 @@ jobs:
# v4 bundle under packages/eql's mise config, which pins its own Rust; the
# guests are the ones wasi-check built and checked.
eql-postgres:
env:
MISE_ENV: test
name: Go rows and queries in PostgreSQL with EQL v4
needs: wasi-check
runs-on: blacksmith-16vcpu-ubuntu-2204
Expand Down Expand Up @@ -434,6 +461,16 @@ jobs:
# Must name runs-on's image; see scripts/__tests__/workflow-mise-cache-image.test.mjs
cache_key_prefix: mise-v1-ubuntu-2204

# mise caches Rust symlinks, not ~/.rustup. A cache hit can leave a
# toolchain directory without cargo and still report "all tools installed".
# Reconcile both workspaces' configured toolchains before cargo is used.
- name: Restore Rust toolchains behind the cached mise links
run: |
mise install --force rust
mise -C packages/eql install --force rust
mise x -- cargo --version
mise -C packages/eql x -- cargo --version

# `mise run build` compiles eql-codegen to write the bundle.
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
Expand All @@ -459,11 +496,18 @@ jobs:

# Builds the bundle from the tree, installs it, and runs the tests; a
# skip fails the task, so a missing guest or database cannot pass.
- name: Go rows and queries through EQL v4
- name: Go rows, queries and required JSON conformance through EQL v4
env:
EQL_TEST_DATABASE_URL: "host=localhost port=${{ job.services.postgres.ports['5432'] }} user=postgres password=postgres dbname=postgres sslmode=disable"
STACK_CONFORMANCE_REPORT_DIR: ${{ runner.temp }}/eql-conformance-postgres
run: mise run go:test:postgres

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: eql-conformance-postgres
path: ${{ runner.temp }}/eql-conformance-postgres/*.json
if-no-files-found: error

# Round trips through real ZeroKMS. The Go live tests skip without their
# STACK_ENCRYPT_TEST_* variables, so the secrets pre-flight runs first and
# turns a missing credential into a failure rather than a silent skip. The
Expand Down
13 changes: 11 additions & 2 deletions conformance/stack-encrypt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ is `languages/golang/encrypt/conformance_test.go`. Go calls the record interface
used by generated code. Testing public SDK declarations is a separate layer.
Python and TypeScript runners are pending; their absence is not a passing result.

## EQL and JSON operations

The [EQL/JSON contract](eql-json-contract.md) enrolls resolver capabilities,
document writes, queries, extraction/subtree reads, element-match and predicate
matching. It reuses the native, Go and independent PostgreSQL suites and shares
write/predicate case data. Run `mise run test:conformance:eql` and the separate
required `test:conformance:eql:postgres` task; see the contract for guest builds,
disposable database setup, intentional SQL differences and version reports.

## Context composition and expectations

The [context contract](context-contract.md) covers ordered extensions, canonical
Expand All @@ -52,8 +61,8 @@ checks. The task runs the existing tests directly, and a missing guest fails
when conformance is required. The ordinary CI suites already execute them.

This is representative fixture coverage, not an exhaustive type/operator
matrix. Wider fresh artifact exchange, shared query refusals and EQL/JSON
operations remain separate work.
matrix. Wider fresh artifact exchange and shared scalar query refusals remain
separate work. The EQL/JSON profiles are described below.

## `record-reads-v1` contract

Expand Down
16 changes: 16 additions & 0 deletions conformance/stack-encrypt/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,19 @@ Go proves canonical AAD indirectly through its actual guest plan frame and nativ
ciphertext opening; provider-call instrumentation remains a separate gap.
Nested composite extensions remain a native API capability rather than an
unsupported feature silently advertised by the Go/dynamic record profile.

## EQL/JSON checkpoint

The [EQL/JSON contract](eql-json-contract.md) and its enrollment index cover
resolver/catalog capabilities, JSON writes and queries, request-bound reads,
element-match and predicate-match. Existing Rust, Go and PostgreSQL suites stay
in place. The common document and predicate-refusal tables now live in
`v1/eql-json.json`: Rust and Go execute them, retaining native key-call checks,
Go diagnostic mapping and all extra cases from the original suites.

The database lane preserves independent plaintext oracles and generated
properties. Required test inventory/no-skip checks and engine/guest/SQL identity
reports make enrollment observable. This consolidates data and requirements;
it does not replace adapter tests or claim whole-database authentication.
Fresh JSON artifact exchange, released-version matrices and Python/TypeScript
runners remain open.
134 changes: 134 additions & 0 deletions conformance/stack-encrypt/eql-json-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# EQL and JSON operation profiles

These experimental profiles enroll the existing EQL v4 JSON tree operations.
They do not describe EQL v3 SteVec, or certify an entire SDK. The
[enrollment index](v1/eql-json-profiles.json) names mandatory compiled tests;
[shared cases](v1/eql-json.json) supply document text and predicate refusals to
native Rust and Go. Python and TypeScript have no enrolled runner yet.

## Requirements and evidence

| Requirement | Contract | Existing evidence retained |
|---|---|---|
| `eql-resolver-catalog-v1` | The resolver names catalog domains, their producibility and operators. Unknown names, unavailable EQL builds and unsupported capabilities are refused. JSON options require a JSON target. | Native `targets` and `json_tree_data_plan`; Go catalog and plain-guest refusal tests |
| `json-write-v1` | JSON documents, including scalar roots, empty containers, JSON null and integer boundaries, round-trip as values. SQL null stores no document. Invalid text is refused without exposing it; native tests observe no key request. Typed and dynamic plans agree on search terms and can open each other's output. | Native typed/data-plan suites, Go raw and generated typed document APIs; existing seeded derivation fixture |
| `json-query-v1` | Containment, reverse containment, equality at a path, ordered bounds, selectors and child operations derive their documented operand shapes. Unsupported requests fail. | Native engine/EQL operand comparisons, Go generated queries, independent PostgreSQL `jsonb` containment oracles |
| `json-request-bound-reads-v1` | Extraction and subtree/wildcard reads open against the prepared path, column, position and bounds. Wrong requests and detectable tampering fail. | Native adversarial read suite, EQL request/open tests, Go selector reads, PostgreSQL extraction/subtree and hostile-row suites |
| `json-extraction-v1` | Single-node extraction opens the requested scalar. | Native scalar extraction, Go selectors and PostgreSQL plaintext extraction oracle |
| `json-subtree-v1` | Complete bounded subtrees and wildcard reads match plaintext queries. | Native/EQL prepared reads and Go/PostgreSQL read paths |
| `json-tamper-v1` | Detectable mixed-document, truncated-chain and substituted-column answers are refused. | Native and PostgreSQL adversarial suites; Go observes wrong-column rejection |
| `json-element-match-v1` | Every condition must hold on one element. Ordering paths and resolver capability are required; a probe beyond the declared bound raises rather than silently truncating. | Native operator/capability tests, Go generated member APIs, SQL domain and independent plaintext predicate tests |
| `json-predicate-match-v1` | Supported text compiles to existing operators; refusals preserve reason, construct and byte offset, with no caller text in diagnostics. Missing and null paths obey the documented truth rules. | Shared negative corpus, native compiler/property tests, Go generated `Match`, SQL `jsonb_path_match` and handwritten-operator oracles |

The index is an evidence inventory, not a second copy of every existing test.
Native execution retains whole binaries, including their unindexed properties
and regressions. Go selects the exact enrolled tests and rejects missing passes,
failures and skips, including skipped subtests. Native inventory checks reject
missing names, filtered tests and a wrong `#[ignore]` state before execution.
Only the PostgreSQL lane executes the database tests marked `#[ignore]`.

## Predicate grammar

The current grammar in `packages/stack-encrypt/src/dynamic/json_query.rs` and
`json_predicate.rs` is the executable specification. This profile pins:

```text
predicate := comparison | exists(path) | exists(path ? (filter))
| (predicate) | !(predicate)
| predicate && predicate | predicate || predicate
comparison := path op value | value op path
op := == | != | <> | < | <= | > | >=
path := $ followed by .key, ."quoted key", or [*] steps
value := JSON scalar literal | $variable | $"quoted variable"
filter := comparison on @ or @.member, optionally joined with &&
```

Negation binds before `&&`, which binds before `||`. Variables are a JSON
object; absence of `vars` means an empty object. Scalar literals are strings,
numbers, booleans and null; objects/arrays cannot be comparison values.
Orderings and multi-condition element filters require numbers or strings and
configured ordered paths. A filter requires a wildcard in its outer path;
its member cannot contain a wildcard. Predicate nesting is limited to 32.
A wildcard bound must be positive; its probe prevents an undersized bound from
silently producing a partial answer.

Arithmetic, regex, `starts with`, `is unknown`, mode words, methods, `.*`, `.**`,
slices, `last`, path-to-path/value-to-value comparisons, array positions,
filters outside `exists`, and `||`, `!`, `!=` or nested `exists` inside filters
are unsupported. `!=` at a wildcard is refused. The corpus pins representative
syntax, unsupported-construct, variable, type, depth and path diagnostics.
Existing parser tests retain the wider grammar matrix and adapter-specific
invalid-input checks; the shared corpus is not an exhaustive grammar fuzzer.
Offsets are UTF-8 byte offsets, not character indices. Each refusal also names
`at_text`, the suffix beginning at that offset. Omitted `vars` exercises absent
variables rather than an explicit empty object.

## Nulls, oracles and intentional differences

SQL null (no encrypted document), JSON null (a document/node), and an absent
path are distinct. The SQL predicate suites compare both selected rows and
three-valued answers. An absent path does not satisfy a comparison. Ordering a
JSON null can produce SQL unknown where plaintext jsonpath returns false;
the compiled predicate adds the null test under negation where it affects
selected rows. This exception is checked explicitly, not normalized away.

The independent oracles use PostgreSQL `jsonb @>`, `<@`, `jsonb_path_query` and
`jsonb_path_match`, alongside handcrafted plaintext conditions. They do not
calculate expected results from encrypted operands. Their supported-domain
assumptions and exceptions remain explicit:

- Paths are literal. There is no lax implicit array unwrapping or scalar-to-array
wrapping. Use `[*]` and keep array shapes consistent.
- Keep one scalar kind at an ordered path. Terms do not preserve the plaintext
type distinctions used by jsonpath's cross-kind comparisons.
- Order terms fold text case/accents and compare numbers as float64. Equality in
a multi-condition element filter uses those terms; standalone `==` is exact.
- Reverse containment excludes the plaintext root-scalar-in-root-array special
case; the generated oracle logs how many such comparisons it excludes.
- Storage and projection list order is randomized. Compare projections as sets
and stored selector collections as multisets. The existing deterministic
`packages/stack-encrypt/tests/fixtures/json_tree_vectors.json` pins a seeded
permutation, tags and tokens, not a production ordering promise. Fresh
ciphertext bytes are not equality fixtures.
- Read authentication detects substituted paths, wrong columns, mixed-document
subtrees, forged tags and incomplete returned chains. It does not establish
database completeness or row identity: withholding a whole answer can look
like no node, and another row's node at the same path can open. Those limits
have named native evidence in the index.

## Required execution and identity

From the repository root:

```sh
mise run wasm:guest:build
mise run wasm:guest:build:deterministic
mise run wasm:guest:build:eql
mise run wasm:guest:build:eql:deterministic
mise run test:conformance:eql

# A disposable database: this command installs/replaces EQL v4 there.
EQL_TEST_DATABASE_URL='host=localhost port=5432 user=postgres password=postgres dbname=postgres' \
mise run test:conformance:eql:postgres
```

No CipherStash credentials are required. Both lanes are required in
`tests-golang.yml` (the PostgreSQL lane is called by `go:test:postgres`); conformance changes trigger them. The database lane rebuilds
SQL from this tree, installs exactly the bundle whose SHA-256 it records, and
passes the same connection to both languages. It fails without a database or
required guests. Required mode deliberately applies to every Go test using
`deterministicEQLClient`, including tests outside the enrollment index. A fast-lane pass alone is not SQL conformance.

Reports include the native engine and EQL binding package versions, root/EQL
lockfile hashes, corpus/enrollment hashes, deterministic guest hashes, executed
requirement test names, and (for PostgreSQL) the installed EQL version and schema name, the expected schema name, PostgreSQL version and actual SQL bundle hash. Development bundles can
report `DEV`; their hash distinguishes them. CI uploads reports and fails if
none were produced. Locally set `STACK_CONFORMANCE_REPORT_DIR` to retain them;
the same report is always printed after success. These describe this checkout
and its guest artifacts, not a claim of compatibility across released versions.

Required checks reject all fixture-update environment variables. Extending a
language binding means running the applicable shared cases and providing its
own adapter evidence for each claimed requirement. Fresh cross-language JSON
artifact exchange, a release compatibility matrix, and Python/TypeScript
runners remain separate work.
Loading
Loading