Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/dev/llms.txt
Full corpus: https://docs.registrystack.org/dev/llms-full.txt

# Configure Evidence Gateway

> Shape an Evidence Gateway deployment project's runtime file, governed bundle, key material, immutability, and validation with evidence check.

Configuring Evidence Gateway means assembling one deployment project: a governed `bundle/` of YAML,
Rhai scripts, schemas, and fixtures, plus a process-local `runtime.yaml` that binds it to a
listener, a secret root, and audit storage. Both are trusted, startup-only artifacts. Evidence Gateway
loads them once, compiles and validates them together, and refuses to reload, merge, or
partially serve a later change. `evidence check` is the gate every edit to the project must pass
before `evidence serve` runs it.

## The project layout

`evidencectl new` starts local authoring from an OpenAPI document or from a synthetic SQLite
starter:

```sh
project_dir="<dir>"
openapi_source="<file-or-url>"
evidencectl new "$project_dir" --openapi "$openapi_source" --profile local

evidencectl new "$project_dir" --transport sqlite-extract --profile local
```

```text
<dir>/
  .gitignore
  source.openapi.yaml  retained OpenAPI document, for HTTP source authoring
  selectors/           reusable Version 1 selector-profile objects
  sources/             reusable Version 1 source objects
  adapters/            source preparation and extraction scripts
  schemas/             source parameter, response, and fact schemas
  queries/             fixed reviewed statements, for SQLite source authoring
  questions/           question-authoring documents
  derivations/         derivation scripts
  fixtures/            synthetic executable cases
  secrets/             owner-only, unbound disposable local keys
```

The OpenAPI path retains the document bytes exactly and creates empty authoring directories.
`evidencectl source suggest --project <dir>` then drafts one HTTP source and its editable artifacts.
The SQLite path creates a neutral synthetic source, statement, question, derivation, schemas, and
fixture that `evidencectl fixtures run --project <dir>` can evaluate immediately. Neither path
creates Mint configuration, a production target, a real extract, or a deployable bundle. Both
automatically create disposable local signing and HMAC material. After you add a question to an
HTTP project, `evidencectl dev`
compiles the authoring objects into a private local runtime and delegates validation to the real
Evidence Gateway loader. Each development session also scaffolds disposable P-256 keys for Registry
Mint, its caller, and the optional SD-JWT VC holder.

A compact question may set `responseFormats: [signed-jws, sd-jwt-vc]` to permit both signed
serializations during local development. Omission means signed JWS only. Signed JWS is mandatory,
and `sdJwtVc` on a reviewed structured answer also requires the explicit `sd-jwt-vc` permission.
Production format authority comes from the deployment target's bundle-wide ceiling and exact
authority grants, not from this local compiler convenience.

## The runtime file

`runtime.yaml` binds one governed bundle to one process: where the bundle lives, what the
listener binds to, where secrets and audit storage sit, and which private CA files a source may
trust. It is process-local operator configuration, not part of the reviewed bundle, and
`products/evidence/contracts/runtime.schema.yaml` is its schema of record.

Top-level required fields:

| Field | Binds |
| --- | --- |
| `version` | Fixed at `1`. |
| `bundleDirectory` | Absolute path to the governed `bundle/` directory. |
| `listener` | `bindHost` (loopback, private IPv4, or unique-local IPv6 only), `port`, `tlsTermination` (fixed at `operator-controlled-upstream`: TLS terminates ahead of Evidence Gateway), `trustProxyIdentityHeaders` (fixed `false`: proxy-supplied identity is never trusted), plus request-size, concurrency, timeout, and shutdown-grace bounds. |
| `secretProviders.file.root` | The owner-only directory the file secret provider resolves every `secret:file/<name>` reference beneath. |
| `auditStorage` | `path` for the active audit segment and `maximumFileBytes`, a per-segment rotation threshold. |
| `outboundTls` | `systemRoots` (fixed `true`) plus named `trustProfiles`, each a local id bound to one CA bundle file a source can select through `tlsTrustProfile`. |

An optional `metricsListener` (`bindHost`, `port`) serves `GET /metrics` on a second private
binding; absent by default, it is documented in `products/evidence/OPERATOR-CONTRACT.md` under
Metrics reference rather than in the public Evidence Gateway contract.

`runtime.yaml` cannot override anything the bundle governs. The schema's `ownership` block closes
the allowed set to the bundle directory, listener binding and process limits, the optional
metrics listener binding, the file-secret root, audit path and rotation bound, and logical
private-CA file bindings. Every other field, including service identity, authentication,
authority, sources, disclosure, and signing policy, belongs to the bundle alone.

## The bundle

`bundle/evidence.yaml` is the single governed contract Evidence Gateway compiles and validates as one
atomic revision (`products/evidence/contracts/bundle.schema.yaml`). Its top-level sections:

| Section | Declares |
| --- | --- |
| `service`, `issuer` | The technical provider and the legal issuing authority, both URIs. |
| `authentication` | The one trusted OIDC access-token profile: issuer, audiences, token type, algorithms, JWKS URI, maximum token lifetime, revoked key identifiers, and the claim names for principal, requester tags, evidence audience, grant id, and grant authority. |
| `audit` | Keyed JSONL audit: the hash secret reference, key version, and fixed fail-closed behavior. |
| `subjectBinding` | The secret reference and key version behind the audience-scoped entity-reference HMAC. |
| `rateLimits` | Per-principal request, burst, and failed-selector-attempt bounds. |
| `signing` | Fixed ES256 signing, governed active and published public JWK files, revoked service key identifiers, the fixed JWKS path, and assertion validity and clock-skew bounds. |
| `responseFormats` (optional) | The bundle-wide ceiling on releasable serializations; defaults to signed JWS alone and must always include it. |
| `selectorProfiles` | Named, bounded scalar field sets a subject role may be looked up by. |
| `sources` | Named fixed HTTP JSON sources: transport, base URL, acquisition posture, authentication, the fixed request shape, and the response, extraction, and fact schemas bound to it. |
| `authorityProfiles` | Named requester-tag gates: which grants (requirement, purpose, audience, subject roles, selector profiles, value origins) a caller carrying those tags holds. |
| `requirements` | The assertion definitions themselves: id, closed acquisition, purposes, subject roles, reference frameworks, evidence type, validity, derivation script and parameters, output concepts, fixtures, and disclosure-guard family. |

Everything under `bundle/` is mounted read-only and covered by one revision digest computed over
the exact bundle bytes; there is no partial edit, hot reload, or fallback
(`bundle.schema.yaml`, `atomic_revision`). Allowed bundle directories are `adapters/`,
`derivations/`, `schemas/`, `codelists/`, `fixtures/`, and `public-keys/`; symlinks and any file
outside them fail startup (`bundle.schema.yaml`, `bundle_layout`).

Evidence Gateway's four coequal acceptance definitions are adult status, residence region, professional
licence status, and legal-parent relationship (`products/evidence/README.md`,
`products/evidence/OPERATOR-CONTRACT.md`). You shape each as an ordinary `requirements` entry
with its own acquisition, derivation script, and fixtures. The four are presented as coequal, and none
of them is a privileged domain type, a Rust built-in operation, or a special route.

Each requirement declares one closed acquisition kind. `single` names one fixed source.
`search-then-fetch` names a fixed search source and a distinct fixed fetch source:

```yaml
requirements:
  - id: urn:example:requirement:registered-status:v1
    kind: criterion
    acquisition:
      kind: search-then-fetch
      search: subject-search
      fetch: subject-record
```

The fetch runs only after the search returns one schema-valid match. Rust passes the validated search
FactSet to the fetch as transient `prior_facts`; a fetch source may bind one declared scalar fact to a
complete path segment. Neither a response nor a script can select a source, origin, method, credential,
or additional call. The requirement's effective acquisition posture is the weaker posture of its two
sources (`products/evidence/contracts/source-contract.yaml`, invariant `V1-I40`).

## What Rhai scripts may and may not do

A deployment project supplies three kinds of Rhai script, one per source or requirement, and Rust
compiles and reviews all of them at startup (`products/evidence/contracts/rhai-abi.yaml`).
Scripts are trusted, reviewed, immutable bundle artifacts; that trust extends to provider and
requirement semantics only, and it never grants transport, credential, authorization, signing,
audit, or evidence-construction authority.

### Preparation, extraction, derivation

| Script | Signature | Owns | Never touches |
| --- | --- | --- | --- |
| `prepare` (adapters) | `(source_required_selectors, adapter_context) -> RequestParts` | Rendering an ordered query and a bounded JSON body from already authorized selector values, trusted parameters, and any validated prior facts supplied to a fixed fetch. | Source origin, method, path, headers, credentials, TLS, proxy, timeout, redirects, or request count; those stay Rust-owned. |
| `extract` (adapters) | `(projected_source_response, adapter_context) -> LookupResult` | Reading one already-projected, schema-validated response and returning `match` with facts, `no_match`, or `ambiguous`. | Choosing a candidate, scoring, counting, or emitting any diagnostic beyond the closed outcome. |
| `derive` (derivations) | `(facts, declared_authorized_selectors, evaluation_context) -> ConceptValueSet` | Comparing facts against declared authorized selectors and producing at most 16 typed concept values. | Requester, purpose, audience, authority, grant, token, credential, or a logging, audit, or signing handle. |

Every stage runs on fresh invocation state. The exact `adapter_context` contains only `parameters`
and `prior_facts`. Prior facts are empty for single and search stages; a fetch receives only the
schema-validated search FactSet. Only final fetch facts reach derivation. Failures map to one closed,
value-free class per stage (`adapter_input_error`,
`source_protocol_error`, `derivation_input_error`), and raw Rhai errors never reach a public
response.

### Capability boundary

Scripts get fresh local variables, bounded same-file helper functions, pure expressions, and
bounded array, map, and string construction. They cannot reach the filesystem, environment,
network, process, or a module system; they get no ambient clock, timezone, randomness, UUID,
logging, or printing; and they cannot define anonymous functions, dispatch dynamically, or index
an array with a computed negative offset (`products/evidence/contracts/rhai-abi.yaml`,
`capabilities`).

Everything a script computes with runs through a fixed primitive library: date and instant
parsing, exact-decimal arithmetic, calendar addition, bucket lookup, codelist lookup, list and set
membership, and a handful of bounded string and array operations, each pure, deterministic, and
domain-neutral (`products/evidence/contracts/primitive-library.yaml`). Ordinary floating-point
values stay usable inside preparation and extraction, but a derived concept value can never be an
ordinary Rhai float: exact numbers use the declared integer or the Rust-owned `Decimal` type.
`entity_reference_seed` wraps a source-derived value in a protected type Rhai can construct but
never read back, print, or serialize; only Rust projects it into an audience-scoped reference
after output validation.

## Key material

Generate the audit and subject-binding secrets independently and keep the private files inside the
configured secret root. `evidencectl keygen` writes private material as owner-only files and never
prints its contents (`crates/registry-evidencectl/src/keygen.rs`):

```sh
evidencectl keygen secret --out "<secrets-dir>/audit-hmac-key"
evidencectl keygen secret --out "<secrets-dir>/subject-binding-hmac-key"
```

### Local P-256 signing JWKs

For local assurance, Evidence Gateway signs with a P-256 private JSON Web Key (JWK) referenced by
`signer.privateKeyRef`. The governed public half lives under `bundle/public-keys/`; its service
`kid` is the RFC 7638 thumbprint that the runtime derives, not an operator-chosen value.

Generate a disposable local pair with:

```sh
evidencectl keygen signing --out-dir "<owner-only-local-directory>"
```

Do not use this command to provision a production signer.

Production and evidence-grade deployments use a workload-local Vault or OpenBao Transit proxy.
The runtime names its Unix socket, mount, key name, pinned nonzero version, and bounded timeout.
The private key stays in Transit. Startup validates the configured public P-256 JWK, pinned version,
custody controls, and a sign-and-verify operation before the deployment is ready.
Follow
[Configure Transit signing for Evidence Gateway and Registry Mint](../../tutorials/move-evidence-to-production-signing/)
for provider key, public JWK, proxy, and policy setup.

### `audit-hmac-key`

Evidence Gateway uses this random secret to authenticate the audit chain. A changed, removed, or
reordered audit entry then fails chain verification.

### `subject-binding-hmac-key`

Evidence Gateway uses this random secret to derive audience-scoped subject references from authorized
selector values. The references let an assertion identify its subject without disclosing the
selector values or creating one identifier that every audience can reuse.

### Client assertion keys

A source that names `authentication.kind: oauth2-client-credentials` authenticates to its token
endpoint with either a client secret or a private key. The private-key form is RFC 7523 section 2.2
`private_key_jwt`: Evidence Gateway signs a short-lived assertion with the key that
`clientAssertionKeyRef` names and sends that assertion in place of a secret, so no shared secret
reaches the token endpoint. SMART on FHIR Backend Services sources authenticate this way.

Generate the pair with:

```sh
evidencectl keygen client-assertion --out-dir "<secrets-dir>"
```

The default writes `client-assertion-p384-private-jwk` and `client-assertion-p384-public.jwk.json`.
Passing `--algorithm rs384` writes `client-assertion-rsa2048-private-jwk` and
`client-assertion-rsa2048-public.jwk.json` instead. SMART App Launch v2.2.0 requires a token endpoint
to validate only one of ES384 and RS384, so the source's authorization server decides which key you
need. The two pairs use different filenames, so a deployment facing two sources that disagree keeps
both in one secret root.

A deployment reaching two authorization servers needs a key for each, and both live in the same
secret root, so name them:

```sh
evidencectl keygen client-assertion --out-dir "<secrets-dir>" --private-name records-authority-key
```

`--private-name` names the private file itself, so the name you pass is the one
`clientAssertionKeyRef` points at, as `secret:file/records-authority-key`. The public half is written
beside it as `records-authority-key-public.jwk.json`. A name follows the `secret:file/` grammar: a
lowercase ASCII letter, then lowercase letters, digits, `.`, `_`, or `-`. `evidencectl` refuses
anything else rather than write key material no bundle could reference.

Point `clientAssertionKeyRef` at the private file and register the public file with the source's
authorization server. The `kid` is the RFC 7638 thumbprint of the public half, and Evidence Gateway
sends it in the assertion header so the authorization server selects the registered key without an
operator-chosen identifier. Each assertion carries a fresh `jti` and expires within five minutes, so
an authorization server that refuses replays has an identifier to record
(`crates/registry-platform-authcommon/src/client_assertion.rs`).

Evidence Gateway sets the assertion's `aud` to `tokenEndpoint` exactly as the bundle spells it, which
is the value SMART App Launch requires. RFC 7523 section 3 asks only that the audience identify the
authorization server and says the token endpoint URL MAY be used, so a server that expects a
different identifier states it in `clientAssertionAudience`:

```yaml
authentication:
  kind: oauth2-client-credentials
  tokenEndpoint: https://gateway.example.gov/oauth2/token
  clientIdRef: secret:file/source-client-id
  clientAssertionKeyRef: secret:file/source-client-key
  clientAssertionAudience: https://auth.example.gov/realms/main
  maximumCacheSeconds: 300
```

Two kinds of server need it: one reached through a proxy or load balancer, where the URL Evidence
Gateway dials is not the one the authorization server knows itself by, and one that expects its
issuer identifier. RFC 8414 does not require an issuer identifier to share an origin with the token
endpoint, so Evidence Gateway imposes no relationship between the two. RFC 7523 section 3 has the
authorization server compare the value by Simple String Comparison, so what you write is signed byte
for byte: a default port or a trailing slash survives into the claim rather than being normalized
away. The key is accepted only beside `clientAssertionKeyRef`, since a client secret carries no
assertion to hold the claim, and it changes only who may accept the assertion, never where the token
request goes. Register one assertion key per authorization server: a key registered at two servers is
what would let an assertion audienced for one be presented to the other.

## Immutability

Evidence Gateway refuses to start from a deployment input it could write to, and reports a
non-immutable-input error rather than starting on it (`products/evidence/OPERATOR-CONTRACT.md`).
Version 1 permits no reload, merge, mutation, governed-field override, or fallback bundle or
runtime file: a project is either the exact bytes Evidence Gateway loaded at startup, or it is a different
revision that requires a restart.

Freeze the project before validating or serving it:

```sh
chmod -R a-w bundle && chmod 444 runtime.yaml
```

The bundle directory and every file beneath it carry no write bits, `runtime.yaml` is mode `444`,
and the secret root stays owner-only at mode `0700` with mode `0600` secret files. Evidence Gateway
Version 1 supports Unix targets only because these invariants rely on owner, mode, no-follow,
link-count, and open-file-identity checks the platform provides
(`products/evidence/contracts/runtime.schema.yaml`, `platform`). To edit the project again,
restore write permission, make the change, and freeze it again before the next `evidence check`.

## Validating a project

Two offline commands prove a project before it ever binds a port:

```sh
evidence --runtime runtime.yaml check
evidence --runtime runtime.yaml evaluate --fixture "bundle/fixtures/<cases>.yaml"
```

`evidence check` loads, compiles, and validates the complete bundle and runtime file together:
every selector, role, profile, authority, and source binding resolves, every script compiles
against the frozen ABI, and mounted signing material passes its provider self-test, all without
opening the audit chain or contacting a source. `evidence evaluate` replays one fixture file's synthetic
cases through the reviewed adapter, derivation, and output gate, with no source and no network
involved.

A fixture failure names the contract that broke and nothing else, so it says a case failed but not
which stage it stopped at. Add `--explain` to print, for every case, the stages it reached and how
each one ended, and `--explain-format json` to render the same trace as one document for a CI job
(`crates/registry-evidence/src/trace.rs`). The trace reports member names, counts, and identifiers,
never a response, fact, derived, or selector value, and each fixture's own
`diagnostics_exclude` canaries are checked against it before it is printed. The flag is offline
only: `evidence serve` has no equivalent.

`evidencectl fixtures run --project <dir>` drives both commands across a complete deployment
project: it runs `check`, then `evaluate` against every fixture path the bundle's requirements
reference, and reports `PASS` or `FAIL` per step
(`crates/registry-evidencectl/src/fixtures.rs`). Add `--explain` to relay each fixture's trace with
its step, or as that fixture's `trace` field under `--json`. The incomplete workspace created by
`evidencectl new` has no bundle, runtime, or fixture cases, so it cannot pass this gate. After an
operator supplies those deployment artifacts, both `check` and `fixtures run` must pass before
`evidence serve` runs the revision.

## Build a production candidate

An editable project is not a deployment input. Add complete named targets when the reviewed source,
questions, and fixtures are ready. Keep them in the deployment-operator Git repository with public
keys and nonsecret Transit settings, but never private JWKs, audit masters, provider tokens, or live
identifiers. There are no overlays, environment branches, substitutions, or symlinks:

```text
environments/
  local/evidence/{governance.yaml,runtime.yaml,public-keys/}
  staging/evidence/{governance.yaml,runtime.yaml,public-keys/}
  production/evidence/{governance.yaml,runtime.yaml,public-keys/}
```

`governance.yaml` supplies the production-owned bundle fields, including service, issuer,
authentication, audit, subject binding, rate limits, signing, response formats, and authority
profiles. It cannot override selectors, sources, or requirements. Each production question adds
stable concept identifiers and a `governance` block with its requirement, frameworks, Evidence Type,
validity, observation timezone, fixture, and disclosure families.

`runtime.yaml` remains the ordinary runtime document. It names the final absolute bundle,
private listener, secret root, audit path, and any private CA files for one target. It is copied
without transformation.

Build a new candidate directory with explicit target and output paths:

```sh
evidencectl build \
  --project "<editable-project>" \
  --target "<editable-project>/environments/production/evidence" \
  --output "<new-candidate-directory>"
```

The output is create-only and contains `runtime.yaml` and a closed `bundle/`. It contains no
secret values, source responses, local request state, or audit records. The compiler validates
the resulting bundle with the `evidence` binary and every referenced fixture before publishing.
It does not contact an identity provider, Mint, or a source endpoint.

Use [Build and deploy an Evidence Gateway project](../../tutorials/build-and-deploy-evidence-project/)
for the full handoff, and [Evidencectl command reference](../../reference/evidencectl/) for the
command contract.