Repository navigation
Description
Background
Stack Encrypt (packages/stack-encrypt) is the Rust engine that every CipherStash language SDK runs on. A Plan is a record's declaration as data: its name scope, its fields, and each field's indexes. It says how each field is stored, and which queries each field supports. Writes and queries must use the same plan. Otherwise a query derives terms that do not match what was stored, and finds nothing.
Stack Encrypt has two layers. The typed Rust API is checked by the compiler. The dynamic interface is behind the dynamic feature. It is for language bindings such as the Go SDK. Their plans and values arrive as data.
The Stack Encrypt API plan, merged in pull request #1365, decided how both layers declare and query a record.
Problem
On next, a Rust program writes a query plan by hand: cipher.query(op, value).on(&plan). Nothing ties that plan to the plan the record was written with. Some operators, such as element match, exist only in the dynamic interface. Each binding also builds its own requests, so two bindings can come to disagree.
Proposal
The plan items "Field markers and the encryption plan" and "Planned and its serialised form" describe the work.
- Field markers. The derive emits one marker per field, as an associated constant such as
EncryptedMember::EMAIL. The marker has the field's name, plaintext type, accessor, and index list. It offers one method per operator the field's indexes answer, checked at compile time. encryption_plan(). The derive emits it. It returns&'static Plan, built once through the plan builder.- A hand-built plan gets the same markers.
Plan::<Member>::scope("members").fields()followed by the field verbs.build()returns the plan and one marker per field. - One validator for options. Typed Rust option factories and options read from data go through the same validator, before any key is fetched.
- Two wire shapes for bindings. A serialised
Planalone serves a check, an operator list, or a query plan. A serialisedPlannedserves encrypt, query, and decrypt. It contains the plan, the context parts in declared order, and the value. It is a transport form only. It is never logged or stored, because its value is plaintext. - Query plans.
plan.query(field, op)is refused where it is made if no index answers the operator. - The dynamic field handle.
plan.field(name), with every operator checked at run time. contexts()on aPlanand on aPlanned, before either runs.
Typed JSON queries are #1459. Shared query preparation and refusals are #1279 and its child issues.
Acceptance
- The plan's marker example compiles.
-
tests/uicompile-fail cases cover an operator the field's indexes do not answer. - Tests show that a query cannot run without its plan or its required context.
- Every case in the conformance corpus is a serialised
PlanorPlanned, with the engine's expected answer beside it. - A Go program generated for rc.3 runs against a module that sends serialised
Plannedvalues. The only change allowed is the one regeneration the handle rename in stack-encrypt refactor: finalize crate ownership, naming and coordinated migration #1489 needs. - Custom Rust stages that are not registered stay explicitly non-portable, and tests cover the refusal.
- Do not reimplement the configured JSON target support that Go already has.
Relationship to other work
- Coordinate with stack-encrypt refactor: migrate record derives and plans to owned declarations #1483 (record derives), stack-encrypt refactor: separate ciphertext binding from search scope #1487 (scopes and bindings), and stack-encrypt conformance: enroll EQL and JSON/SQL operation profiles #1476 (EQL and JSON profiles).
- On the Go side, Go SDK: methods on Cipher and Client take a *record.Plan and their docs and errors say "plan" — rename and hide them #1116 hides or renames
record.Plan, and Go: public plan-building API for types stashgen can't tag (reconcile with #1116) #1307 adds a public plan builder. - The chain in
plan/chain.rsis removed in stack-encrypt refactor: remove transitional traits and redundant execution APIs #1488.
Plans: the Stack Encrypt API plan and the conformance roadmap.
Activity
coderdan commentedon Nov 9, 2026
For the API plan's "EQL types as fields" item, which this issue covers in part: its prerequisite is now tracked in #1585. That issue adds
EncryptTargetandDecryptSourceto the EQL v4 types on their own. Fields of a derived record also need the record's context handed to an EQL field as anIdentifier, which #1585 leaves out.#1586 is related: it lets a target take its context from the plaintext, which the EQL migration and compatibility shims use.