Doc 02 sketched the hierarchy; this is the machinery. A versioned manifest describes the desired kernel state; an idempotent service reconciles the reader to it — set, verify, check-value — on first boot and every update alike. Processor-independent, deployment-specific by data, never by code.
Two concerns kept apart. The Configuration Source says what the reader should hold — a signed, versioned manifest. The Provisioning Service makes it so — the only code that touches kernel set* / retrieve* calls. Decoupling them means the same service applies any manifest, and the same manifest can come from any source.
Desired-state document: terminal data, config groups, AIDs, CAPKs, CRL. Signed & version-stamped. Delivered however the deployment needs — host pull, MDM push, or local profile. Pure data.
Reads current kernel state, diffs against the manifest, applies only what changed, verifies by check-value. The only component that drives the SDK config surface. Identical on boot and update.
A shape, not a final schema — the point is that every parameter the L2 kernel enforces is declared here, version-stamped, and nothing about a processor appears. Card-brand & cert requirements shape the content; the structure is universal.
// desired kernel state — signed, version-stamped, processor-agnostic { "manifestVersion": "7", "profile": "transit-kiosk-mx", "terminal": { "countryCode": 484, // MX "defaultCurrency": 484, // MXN "language": ["es", "en"], "terminalType": "22" }, "configGroups": [ { "id": 1, "floorLimit": 0, "cvmRequiredLimit": 75000, // minor units "contactlessTxnLimit": 200000 } ], "aids": [ { "aid": "A0000000031010", "group": 1, // Visa "tacDefault": "…", "tacOnline": "…" }, { "aid": "A0000000041010", "group": 1 } // Mastercard ], "capks": [ { "rid": "A000000003", "index": "95", "checksum": "…" } ], "crl": [ /* revoked cert entries */ ], "signature": "ed25519:…" }
ctls_setCAPK. Secret key material (P2PE / DUKPT) never travels in a config manifest — it's injected separately through device_startRKI against device_setSymmetric_RKI_URL, or loaded as CA certs via device_loadCertCA. Config feed and key injection are separate channels by design.
The service doesn't blindly push — it converges current state to desired state and proves it. Same loop whether the reader is blank (first boot) or already provisioned (update). Diff-driven, so an unchanged reader does almost nothing.
add, what to update, what to remove. Unchanged entries are left alone. A blank reader diffs to "add everything"; an up-to-date reader diffs to "nothing."check-value against the manifest's expected value. Provisioning is not "done" until they match. A mismatch fails the apply and rolls forward to retry, never silently proceeds.v7 with its check-value. Next update compares against this and converges only the difference. The audit trail logs what changed, not card data.Because the loop is diff-driven and verified, applying a manifest is safe to run any number of times. The outcome depends on the gap between current and desired — not on how many times you press go.
| Scenario | What the diff finds | Result |
|---|---|---|
| First boot · blank reader | Everything in the manifest is missing | apply all |
| Re-run · already current | No delta — kernel matches manifest v7 |
no-op |
| Update · new floor limit | One config group changed; AIDs & keys unchanged | apply Δ only |
| AID retired | Manifest dropped an AID the reader still holds | remove stale |
| Partial failure last run | Some entries applied, check-value mismatched | re-converge |
| Verify passes | Check-value matches expected | done |
set* call is not a reader that holds the right config — only the check-value proves that. Skipping verification is how a terminal reaches certification with subtly wrong parameters. Foundry treats an unverified apply as a failed apply.
What keeps provisioning trustworthy across deployments and reruns.
check-value. Unverified = failed, never "probably fine."