Skip to content

Add an agent skill for EQL #422

Description

@coderdan

Why

EQL is the surface every CipherStash integration ultimately sits on — the public.eql_v3_* column domains, the eql_v3.query_* operand domains, the operators, and the term extractors — but there is no agent skill for it. An agent working in a consuming repo reconstructs that surface second-hand today: from the CipherStash Stack skills (which document it only as far as their own integration needs), from dist/sql/cipherstash-encrypt.sql, or from the Postgres catalog.

That gap has already produced re-derivation work downstream. cipherstash/stack#777 adds a stash-postgres skill for hand-written SQL against encrypted columns, and a good deal of its content — the column-domain → query-domain naming rule, which operators each domain admits, which domains carry no query terms at all — is a hand-transcription of this repo's surface. It will drift, because nothing checks it against the bundle it describes.

The skill belongs here, next to the definitions it documents.

Scope

A SKILL.md in this repo covering:

  • Purpose — what EQL is and what problem it solves, and where the boundary sits between EQL (SQL-side: domains, operators, indexes) and a client (encryption, term generation, key management).
  • Installation — the bundle and the @cipherstash/eql package, what stash eql install applies, checking the installed version with SELECT eql_v3.version(), and upgrade/reinstall semantics — notably that the install SQL opens with DROP SCHEMA IF EXISTS eql_v3 CASCADE, which takes every functional index on an extractor with it while leaving columns and data intact.
  • The domain type naming system — the public.eql_v3_<type>_<index>eql_v3.query_<type>_<index> derivation, and the irregular cases (eql_v3_json_searcheql_v3.query_json).
  • The full domain type list — every domain with its query domain, or an explicit "storage only, carries no query terms". Ideally generated from the bundle rather than hand-maintained, so it cannot drift.
  • Query forms — the operator matrix per domain (=, <>, <, <=, >, >=, @@, @>), the function twins (eql_v3.eq, eql_v3.matches, the comparison functions), JSON containment and selector extraction, and the storage-payload-vs-query-term distinction — including that the jsonb operator overloads coerce to the storage domain, so an uncast query term fails a CHECK rather than doing what the caller meant.
  • Indexing — functional indexes over the term extractors, opclass selection, and platform constraints.
  • Performance tuning — what engages an index versus what silently sequential-scans, EXPLAIN verification, and the cost of GROUP BY / DISTINCT / ORDER BY against a raw encrypted column instead of an extractor.

Cross-references

The skill should hand off rather than duplicate, in both directions.

Using EQL from an application → the CipherStash Stack skills. These ship inside the stash npm tarball and are installed into consuming repos by stash init:

  • stash-encryption — schema authoring, the client API, the rollout/cutover lifecycle
  • stash-postgres — hand-written SQL over pg / postgres-js, no ORM (feat(cli): add the stash-postgres and stash-edge skills — raw-SQL predicates and the WASM entry stack#777)
  • stash-indexing — functional indexes over the extractors, and the EXPLAIN checklist
  • stash-drizzle, stash-supabase, stash-prisma-next, stash-dynamodb — per-integration
  • stash-edge — the WASM entry for Deno / Workers / Supabase Edge Functions
  • stash-clistash eql install, stash db validate

Querying through CipherStash Proxyhttps://github.com/cipherstash/proxy. Worth an explicit section, because it inverts the model the rest of the skill describes: the application writes plaintext SQL and Proxy encrypts on write and decrypts on read, so there are no query terms to mint and no operand casts to get right. Proxy also reads its encryption config from eql_v2_configuration (populated by stash db push) rather than from the column's domain, which is an EQL v2 lifecycle — worth stating plainly alongside the v3 model, since the two are easy to conflate.

Conversely, the Stack skills should point here as the authoritative definition of the operator and domain surface. cipherstash/stack#777 adds that link, and cites this repo as where operator gaps and domain-level bugs belong.

Generate the reference tables, don't hand-maintain them

The domain list, the column-domain → query-domain mapping, and the operator matrix are the parts of this skill that will rot. They are also the parts that are already machine-readable in the published package, so they should be generated and then guarded by a test rather than transcribed.

Two sources ship inside @cipherstash/eql, and neither needs a database:

  1. The CREATE OPERATOR statements in dist/sql/cipherstash-encrypt.sql — 3,025 of them in 3.0.2. Each carries LEFTARG, RIGHTARG, and FUNCTION, which is exactly one cell of the matrix.

  2. The generated per-domain .d.ts types, whose doc comments already name the domain and its operators:

    /**
     * `eql_v3.query_text_eq` — equality domain query operand.
     *
     * Operators: `=` `<>`. Required keys: `v` `i` `hm`.
     */
    type TextEqQuery = { v: SchemaVersion; i: Identifier; hm: Hmac256 }

A ~20-line parse of the first source reproduces a matrix row exactly:

LEFTARG = public.eql_v3_text_search, RIGHTARG = a query_* domain
  =    right=eql_v3.query_text_search    impl=eql_v3.eq
  <>   right=eql_v3.query_text_search    impl=eql_v3.neq
  <    right=eql_v3.query_text_search    impl=eql_v3.lt
  <=   right=eql_v3.query_text_search    impl=eql_v3.lte
  >    right=eql_v3.query_text_search    impl=eql_v3.gt
  >=   right=eql_v3.query_text_search    impl=eql_v3.gte
  @@   right=eql_v3.query_text_search    impl=eql_v3.matches

That also recovers the operator→function mapping (eql_v3.eq, eql_v3.matches, …) for free.

Why this matters beyond tidiness. The matrix in stash-postgres (cipherstash/stack#777) was built by querying pg_operator against a live EQL 3.0.2 install. That is authoritative — pg_operator is what Postgres consults to resolve col @@ $1, so it is the runtime truth, and it is the right thing to check when debugging a live "operator does not exist". But it is unrepeatable in CI without standing up Postgres with EQL installed, so nothing catches the table drifting when the version moves. A generator over either source above closes that gap with no database at all.

Worth doing here rather than downstream: the same generated artifact can back this skill's tables and be published for consumers, so stash-postgres and the other Stack skills stop maintaining a parallel hand-written copy. Publishing the operator matrix as data (JSON alongside the existing JSON Schemas) would let every consumer assert against it.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions