Skip to main content
Version: v0.25.0 (Latest)

Command Contracts

Every service operation in the EDK, issuing a credential, verifying a presentation, generating a key, signing a JWT, is expressed as a command. Commands are how the platform moves work between modules, tenants, transports, and runtimes. That much is foundational to the IDK.

What the EDK adds on top is a contract for each of those commands: an app-scoped object that describes, in a single place, everything that is statically true about the operation, who is allowed to run it, what regulatory obligations touch it, what data it accepts and returns, what configuration it reads, and how sensitive its inputs and outputs are.

A command without a contract is still executable. A command with a contract becomes something the rest of the platform can reason about: authorization can decide before invocation instead of inside it, compliance reporting can be generated from the running system rather than a spreadsheet, UIs and MCP agents can render inputs safely without copying the schema, and operators can audit which features a new build actually activates.

Why Not Just Put This in Code?

The information a contract captures has traditionally been scattered across five or six different places:

  • Regulatory obligations live in compliance documents maintained by a different team.
  • Minimum authentication levels live in Keycloak configurations or route-level annotations.
  • Input validation lives in the command's implementation, mixed with business logic.
  • UI labels and help text live in a frontend translation file that nobody keeps in sync.
  • Configuration keys are documented in a README, if at all.

This scatter is the reason compliance audits take weeks, security reviews miss requirements, and "what config does this service actually consume?" is a question no one can answer confidently. It is also the reason most platforms quietly accumulate implicit knowledge that exists only in the heads of the engineers who wrote the code.

Contracts collapse this scatter into one declarative object per operation, owned by the module that owns the command. The command implementation no longer carries metadata the runtime would like to know, it just does its work. Everything else, policy, compliance, rate limiting, schema, config, becomes introspectable, queryable, and tooling-friendly.

What a Contract Describes

Each contract attaches five categories of metadata to a single command.

Regulatory relationship

The most distinctive part of the EDK contract is the compliance profile: an explicit, typed list of the regulatory clauses that apply to the operation, the frameworks they come from (GDPR, eIDAS 2, the EU Digital Identity Wallet regulation, NIS2, DORA, NIST SP 800-63, ISO 27001, FIPS 140), and whether each clause applies, may apply, or is implemented by the code.

The distinction matters. Issuing a credential that carries a date of birth is a clear Article 25 ("data protection by design") and Article 32 ("security of processing") activity under the GDPR. Issuing a credential that carries a biometric template additionally triggers Article 9 ("special categories") and Article 35 ("data protection impact assessment"). A policy engine, an audit report, or a deployment gate can read those references and treat the two commands differently without the application code needing to know anything about regulations. When auditors ask which operations in production process special-category data, the answer is a registry query, not a code search.

Alongside the references, a contract may declare that its execution constitutes a data-processing activity for Article 30 record-keeping, or that the operation requires a DPIA. These are not magic strings, they are typed enum values that the registry can filter on and that IDE autocomplete helps developers get right.

Concretely, the compliance profile of an OID4VCI issuance command looks something like this:

override val complianceProfile = complianceProfile {
frameworks(RegulatoryFramework.EIDAS2, RegulatoryFramework.EUDIW)
reference(Eidas.ART_25_SIGNATURES)
reference(Gdpr.ART_6) // lawful basis required
reference(Gdpr.ART_25_DATA_PROTECTION_BY_DESIGN)
reference(Gdpr.ART_30_RECORDS_OF_PROCESSING)
reference(Gdpr.ART_32_SECURITY_OF_PROCESSING)
potentiallyApplies(Gdpr.ART_35_DPIA,
note = "DPIA required for large-scale issuance")
dataProcessingActivity()
}

A verifier handling a presentation that contains biometric data flips two of those from "applies" to "applies with higher bar" and adds Article 9:

reference(Gdpr.ART_9)        // special categories (biometrics, health)
reference(Gdpr.ART_35_DPIA) // no longer just "potentially applies"
requiresDpia()

Nothing in the command's implementation changes. What changes is what the platform can tell you about the running system. An internal audit can enumerate every operation that processes special-category data without anyone reading source code. A risk assessment can filter the registry for commands that trigger Article 35 and check whether the corresponding DPIAs on file actually cover what is deployed. A GDPR Article 30 record-of-processing can be generated from the dataProcessingActivity() flag rather than maintained by hand. When a new regulation lands, a new eIDAS implementing act or a new NIS2 control, finding the affected operations is a query, not a workshop.

Risk and assurance requirements

A contract declares the authentication assurance level required to execute the command, following NIST SP 800-63B (AAL1 / AAL2 / AAL3) and mapped 1:1 to the eIDAS levels of assurance (Low / Substantial / High). It can also require a maximum authentication age (a command that signs a qualified signature may refuse a session older than five minutes), a specific authentication method reference (multi-factor, hardware-bound), or dual control for destructive ceremonies.

Requiring AAL2 for a credential issuance and AAL3 for a key deletion is not something the business logic should decide every time. It is a property of the operation itself, and the contract is where it lives. When the transport layer or the policy engine sees a command come in, it can reject or step-up the session before the implementation runs, using nothing but the contract.

Inputs, outputs, and schema overlays

Every contract declares typed tokens for its input and output. This alone gives the runtime what it needs for serialization, for REST/gRPC binding, and for generating OpenAPI and gRPC descriptors without reflection. But the more interesting piece is the schema overlay.

A schema overlay describes each input field at a semantic level: what to label it in UIs ("Credential Configuration"), what help text to show, whether the field is confidential (redact in logs, don't echo to clients), whether it is an identifier that policy rules can target ("this is the client_id"), and which policy constraints apply to it. Overlay entries carry stable i18n keys derived from the command ID so translators can translate them without touching code, and MCP agents or admin UIs can render the same form consistently across locales.

The overlay is intentionally separate from the input type itself. The same String can be a public issuer identifier in one command and a confidential access token in another; the overlay tells the platform which one, without forcing the argument type into a custom wrapper.

Resource targets and policy identifiers

Authorization engines, AuthZEN, Cedar, OPA, evaluate rules against structured requests: subject, action, resource, context. A contract's resource target describes the resource side of that tuple: what type of thing the command operates on ("oid4vci.credential", "kms.key"), which arguments identify the instance, and which arguments are constraints that policy can read. Once this is declared, the policy layer can build correct requests for every command automatically; it never needs a hand-written mapping between "here is the input, here are the fields that matter for authorization."

This is what makes deep policy evaluation possible. Without a contract, a policy engine receives an opaque command invocation: it knows the command ran, it may know who called it, but it cannot reason about what the command is actually doing. Rules degrade to coarse "this role can call this endpoint" checks, because the engine has no structured view of the inputs. With a contract, the engine sees the command's exact shape: the resource type, the identifiers that pin down which instance is being touched, the typed constraints drawn from the arguments, the confidential fields to treat carefully in decision logs, the assurance level that was declared, the compliance clauses that apply. A Cedar policy can now say "this tenant may issue credentials only for configurations whose credential_configuration_id starts with employee_, and only at AAL2 or higher, and only during business hours", and the engine has every field it needs to evaluate that rule, because the contract surfaced them as typed resource attributes rather than hidden inside the argument object.

The same declarations fuel rate limiting keyed by tenant and resource, usage metering for billing, and pattern-based feature gating ("all oid4vci.* operations are gated behind the issuance license").

Configuration inventory

Production services read dozens of configuration keys. In practice, nobody has an authoritative list of which keys a given build actually touches, which are required, which have defaults, and which are read as secrets versus plain values.

A contract's config options inventory fixes that. Each key is declared with a role, the command either defines it (authoritative source, controls the default), consumes it (reads but doesn't own), or references it (mentions, e.g., for redirection). Keys are typed, and secret-bearing keys are marked. The registry can answer: "which command defines sphereon.oid4vci.issuer.nonce.ttl?" "which commands read it?" "what is the default?" That is a very different operational posture from grepping a codebase for string literals.

How the Platform Uses Contracts

Contracts are app-scoped, which means they are available before any session is created and before any request arrives. The EDK exposes them through a single registry with pattern-based lookup. From that point on:

  • Authorization builds its AuthZEN/Cedar/OPA requests from the resource target and identifiers, without the command ever knowing a policy engine exists.
  • Step-up authentication reads the assurance requirements to decide whether to challenge for AAL3 before invocation.
  • Schema delivery serves overlay-enriched input schemas to UI clients and MCP agents, with i18n resolved per locale.
  • Transport binding generates REST routes, gRPC method descriptors, and OpenAPI documents from the type tokens and resource identifiers.
  • Compliance reporting walks the registry to enumerate which running commands touch personal data, which trigger DPIAs, which define retention periods, which process special-category data.
  • Operational introspection lets operators ask which commands read a given config key, which capabilities a deployment has enabled, or which commands are ready for async execution.

None of this is framework magic. It is a straightforward consequence of deciding that a command's static description is data, and data should be queryable.

Where Contracts Live

The framework types (ServiceCommandContract, ComplianceProfile, SchemaOverlay, ResourceTargetDescriptor, ConfigOptionDescriptor, AssuranceRequirements, and the DSL that builds them) live in the EDK module lib-core-contract-public. The concrete contracts for each domain live in sibling -contract modules next to the -public command interface modules they describe, so the issuer contracts sit in lib-openid-oid4vci-issuer-contract, the KMS contracts in lib-crypto-kms-contract, and so on.

Deliberately, nothing in the open-source IDK depends on the contract framework. Commands declare themselves in IDK modules without knowing contracts exist. The EDK layers the metadata over them. This preserves the IDK as a clean, open foundation while making the contract infrastructure, and the compliance, policy, and operational tooling that depends on it, an enterprise capability.

A service that wants contract-driven behavior pulls in the relevant -contract modules. Metro DI discovers them automatically, and the registry is populated at application startup. A service that does not need them, a stripped-down testing harness, for instance, can omit them with no runtime changes anywhere else.

When to Write One

Any new service command in the EDK should have a contract. The minimum bar is the command's action type, operation type, resource target, assurance requirements, and compliance profile. The schema overlay and config inventory are worth filling in for anything exposed over a transport or consumed by a UI; for internal orchestration helpers that are never externally addressable, they can be deferred.

The contract is authored once, next to the command, by the module that owns both. It is reviewed as part of the same PR. It is the single artifact a reviewer needs to confirm that a new operation has thought about regulation, risk, input handling, and configuration, before the implementation is merged, not six months later during an audit.