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. A contract is an app-scoped object that describes, in one 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. Operators can audit which features a new build actually activates.

Why this is not buried 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 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. It is 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. 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 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 the input and 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").

License protection

Contracts can also declare licenseProtection, the static license metadata for a command. The descriptor carries a canonical entitlement key in product.module.service.command form, the deployment shapes where enforcement applies, the protection mode, optional quota keys, and an optional domain policy reference. This is the bridge between the command registry, the license runtime, OpenAPI documentation, and operational inventory.

For HTTP-exposed commands, the canonical source is the OpenAPI operation. The operation carries x-license-protection beside its operationId:

x-license-protection:
entitlementKey: vdx.semantic.binding.create-party-type-binding
deploymentShapePredicates: [packaging=container, environment=production]
protectionMode: licensed

The build reads that metadata, validates the descriptor shape, and attaches it to the command's contract. If an annotation and OpenAPI both describe the same operation, the OpenAPI descriptor is authoritative. Duplicate operation IDs with conflicting license descriptors are rejected because they make enforcement ambiguous.

Missing license descriptors are treated as an inventory gap, not as a malformed descriptor. That distinction helps during migration. Existing commands can keep running while coverage reports identify unclassified operations. A malformed descriptor is different. If a command claims license protection but the entitlement key, product, module, service, command, protection mode, or deployment predicates are inconsistent, validation reports it as a contract error.

The entitlement key

The entitlement key is the stable address of a licensed operation. It always has exactly four dot-separated segments, product.module.service.command, and it is the join key between the command contract, the signed grant, the OpenAPI operation, and the operational inventory. A descriptor's product, module, service, and command fields must agree with the four segments of its entitlement key. Validation rejects any descriptor where they drift apart, so a single canonical string identifies the operation everywhere it is referenced.

The four-segment shape is what lets a license grant address operations at any granularity. A grant can name an exact command, or it can use a wildcard pattern such as kms.** or vdx.semantic.binding.* to cover a whole module or service. The same pattern vocabulary the authorization layer uses for command IDs applies here, so a coarse feature grant and a fine command rule live in the same namespace.

Command overrides

A coarse feature grant is convenient but blunt. It turns a whole capability on or off. Command overrides are the definitive, per-command layer above it. A grant carries two override sets, allow and deny, each a set of command patterns:

  • An allow override grants a command even when its feature is not present. It is how a license enables a single operation from a capability the customer otherwise does not hold.
  • A deny override withholds a command even when its feature is present. It is how a license excludes one operation from a capability the customer otherwise holds.

A deny always wins. When a command matches both an allow and a deny pattern, whether from the platform baseline or a per-tenant subscription, the deny is definitive and the command is refused with COMMAND_DENIED. This deny-overrides rule makes overrides safe to compose. A tenant can be granted extra operations on top of the platform baseline, but no addition can re-enable something the platform or the signed ceiling has denied. The full evaluation order, deny first, then the signed ceiling, then allow overrides, then feature gates, is described in Entitlement Resolution.

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, for example 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 contract framework lives in the EDK. The concrete contract for each domain lives next to the public command interface it describes, so the issuer contracts sit with the issuer command interfaces, the KMS contracts with the KMS command interfaces, and so on. The module that owns the command owns its contract.

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. The IDK stays an open foundation, and the contract infrastructure, along with the compliance, policy, and operational tooling that depends on it, is an enterprise capability.

Pure EDK and VDX command surfaces may keep the contract in the public module when there is no separate contract module. The VDX semantic-binding surface does this. Its service command contracts live next to the public command interfaces and expose license descriptors that match the per-operation x-license-protection entries in the canonical OpenAPI specification. The HTTP adapter commands are not licensed separately. The protected unit is the service command that performs the business operation.

A service that wants contract-driven behavior pulls in the relevant contract modules. 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 one 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.