Governance templates API
A governance template is a per-tenant, versioned rule template: a stable template_key + template_version, a display name, an opaque obligation_refs payload (which obligations it references), and a target_controls payload that maps each obligation onto an existing gateway configuration control. This API lets an authorized admin catalogue templates and preview / apply a template's controls against a gateway.
The apply engine always respects the tenant residency floor: a template may tighten a residency/PII control but can never downgrade it below the tenant's armed floor. A control the floor refuses is reported as blocked_by_floor and is never persisted.
Base URL: https://<your-gateway-host>/admin/v1
Required role for every endpoint: admin, tenant_admin, or ki_manager. A caller with any lower role (or none) is rejected 403/401. The client is never the authorization boundary — every call is re-gated and tenant-scoped server-side.
Tenant scope
Tenant scope is decided server-side; the client value never widens it.
| Caller | Scope |
|---|---|
admin |
Must target one tenant with ?tenant_id=<id> on the per-tenant routes. |
tenant_admin / ki_manager |
Always their own tenant. A ?tenant_id naming another tenant is ignored (pinned to the caller's own). |
The template routes are inherently per-tenant: an admin who omits ?tenant_id is rejected 400 { "error": "tenant_id required" }. A non-admin whose account carries no tenant is refused 403.
The diff / apply routes take a gateway_id in the body instead of ?tenant_id: the caller must have access to that gateway (its tenant must match the caller's, unless admin), and the template is looked up scoped to the gateway's tenant — so a cross-tenant template reads as 404.
Authoritative flag
is_authoritative is not client-settable. Every template created through this API is stored non-authoritative (0); authoritative (DPO-signed) content is provisioned out of band. An existing authoritative template is read-only to this API: PATCH and DELETE against it return 409. The management UI marks every non-authoritative template with a watermark so it is never presented as a compliance guarantee.
GET /governance/leaves
The catalogue of governable configuration leaves a template's target_controls may reference. Returns a JSON array (sorted by leaf) of { "leaf", "scope", "kind" }; scope is gateway (overlaid on the gateway, subject to the floor) or tenant (derived from the tenant — a gateway apply cannot change it). This is the single source of truth for the leaf catalogue; clients must not hardcode it.
[
{ "leaf": "eu_region_routing", "scope": "gateway", "kind": "boolean" },
{ "leaf": "pii_masking_enforced", "scope": "tenant", "kind": "boolean" },
{ "leaf": "provider_allowlist_enforced", "scope": "gateway", "kind": "boolean" }
]
GET /governance/templates
List the tenant's live templates. Returns a JSON array; each row's obligation_refs and target_controls are decoded from storage (a null column yields null / [] respectively — never an object that would break a client .map).
POST /governance/templates
Create a template. Body (JSON object):
| Field | Accepted | Rejected → 400 |
|---|---|---|
template_key |
string, 1–190 chars (required) | missing / empty / non-string / > 190 |
name |
string, 1–255 chars (required) | missing / empty / non-string / > 255 |
template_version |
integer 1 … 2147483647 (default 1) |
present-but-non-numeric / < 1 / > 2147483647 |
target_controls |
JSON array of { "leaf": <governable leaf>, "value": <boolean>, "obligation"?: <string> }; absent → stored [] |
unknown leaf / non-boolean value / duplicate leaf / not an array |
obligation_refs |
JSON array or object (opaque; never interpreted) | a scalar/string |
is_authoritative |
ignored — always stored 0 |
— |
A duplicate (tenant, template_key, template_version) returns 409 { "error": "a template with this key and version already exists" }. On success returns 201 with the created row.
PATCH /governance/templates/{id}
Update a live template's mutable fields (name, obligation_refs, target_controls), tenant-scoped. Only fields present in the body are changed — an absent field is preserved (a partial PATCH never clears stored controls). Present fields use the same validation as create. is_authoritative is ignored. An unknown / cross-tenant / soft-deleted id → 404; an authoritative template → 409 (read-only).
DELETE /governance/templates/{id}
Soft-delete a live template, tenant-scoped. Unknown / cross-tenant / already-deleted → 404; an authoritative template → 409 (read-only). On success returns 200 { "deleted": true }.
POST /governance/templates/{id}/diff
Preview what applying the template would change on a gateway. No write. Body: { "gateway_id": "<id>" } (required, string — else 400). The caller must have access to the gateway (403/404/503 otherwise). Returns:
{
"gateway_id": "…",
"tenant_id": "…",
"changes": [
{ "leaf": "eu_region_routing", "scope": "gateway",
"current": true, "requested": false, "resulting": true, "status": "blocked_by_floor" }
],
"warnings": []
}
Each changes[] entry also carries an obligation field — the governance obligation the leaf maps to — omitted from the example above.
Per-control status:
| Status | Meaning |
|---|---|
apply |
A gateway leaf whose effective value will change (tightening). |
unchanged |
Already at the requested value / a no-op. |
blocked_by_floor |
The tenant residency floor refuses this downgrade — not persisted. |
tenant_scoped |
A PII leaf a gateway apply cannot change (set it on the tenant) — not persisted. |
warnings carries a human-readable note when the apply would introduce a deny-all condition (enforcing a provider allowlist with no usable providers).
POST /governance/templates/{id}/apply
Persist the template's controls onto the gateway, floor-safe. Same body + gateway-access rules as diff. Only apply-status controls are written; blocked_by_floor / tenant_scoped / unchanged controls are skipped. Returns the same shape as diff (the applied result).
The apply refuses to introduce a deny-all condition: 409 { "error": "applying this template would deny ALL inference on the gateway (enable a usable provider allowlist first)" }, with no write. A stored template whose target_controls is malformed (e.g. a legacy row) surfaces as 422.
Error codes
| Status | Cause |
|---|---|
400 |
malformed body / failed field validation / missing gateway_id / admin omitted ?tenant_id. |
401 |
unauthenticated. |
403 |
role below ki_manager, or a non-admin without a tenant, or a gateway in another tenant. |
404 |
unknown / cross-tenant / soft-deleted template or gateway. |
409 |
duplicate (tenant, key, version); PATCH/DELETE of an authoritative template; an apply that would deny all inference. |
422 |
a stored template with an invalid target_controls payload. |
503 |
transient gateway lookup failure (retryable). |