approval-engine/schemas/approval_claim.schema.json
tegwick 7e756773de Implement GH-DEC-2026-008: declared PDP-path intent, enforced at issue
Gate House ruled binding.pdp_digest is the binding correspondence on the
GH-DEC-2026-003 path and is required there, having rejected a vocabulary
mapping for the reasons we gave. It asked this engine to record the PDP
digest at issue for approvals intended for that path, and to have the
claim state which approvals those are rather than leaving it to the
requester's memory.

Schema v3 adds approvals.pdp_path. create() refuses pdp_path true without
a pdp_digest, so an approval that would be unusable on the path fails at
issue rather than at the protected side effect. The claim exposes
binding.pdp_path, which makes it a guarantee rather than a hint: pdp_path
true implies pdp_digest is non-null.

Intent is declared and never inferred. A pdp_digest that happens to be
present is not a declaration anybody made, so a recorded digest alone
leaves pdp_path false, legacy rows migrate to false rather than being
back-filled from their digests, and a successor inherits its
predecessor's declaration. Approvals issued before the ruling stay usable
by consumers in this engine's own vocabulary and are simply not usable on
the PDP path -- the ruling's intended cost, stated as such.

Schema, both published examples, a v2-to-v3 migration test asserting
survivors keep their digest while declaring no path intent, and tests for
refusal at issue, claim exposure, non-inference, and successor
inheritance. 102 tests pass (8 new).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvyJPAaVCGsVheVhcCwNND

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 411227@bnt-lap001
Assistant-Session: d566f6d3-bcaf-43c3-bc5e-3ddd0f64b535
2026-09-06 14:51:23 +02:00

200 lines
5.2 KiB
JSON

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://approval-engine.netkingdom/schemas/approval_claim.schema.json",
"title": "ApprovalClaim",
"description": "Input claim that access-engine consumes. This is a fact about an approval object, not a decision. Yields to the Taxonomy request-claim schema (statute \u00a717) when that artifact exists and is assented; do not treat this local shape as permanent.",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"kind",
"issuer",
"approval_id",
"state",
"valid_now",
"consumed",
"binding",
"freshness",
"validity",
"reason_code"
],
"properties": {
"schema_version": {
"const": "0.1"
},
"kind": {
"const": "approval-claim"
},
"yields_to": {
"type": "string",
"description": "Taxonomy artifact this contract yields to. Informational; consumers must not branch on it."
},
"issuer": {
"const": "approval-engine"
},
"approval_id": {
"type": "string",
"format": "uuid"
},
"state": {
"type": "string",
"enum": [
"requested",
"approved",
"valid",
"consumed",
"superseded",
"revoked",
"expired"
]
},
"valid_now": {
"type": "boolean",
"description": "True only when the object is approved, inside its validity window, and not consumed, superseded, revoked, or expired. Not a permission."
},
"consumed": {
"type": "boolean"
},
"binding": {
"$ref": "#/$defs/binding"
},
"freshness": {
"$ref": "#/$defs/freshness"
},
"validity": {
"$ref": "#/$defs/validity"
},
"reason_code": {
"type": "string",
"enum": [
"ok",
"requested",
"not_yet_valid",
"insufficient_approvers",
"expired",
"revoked",
"superseded",
"consumed"
]
}
},
"not": {
"anyOf": [
{
"required": [
"effect"
]
},
{
"required": [
"decision"
]
},
{
"required": [
"allow"
]
},
{
"required": [
"deny"
]
}
]
},
"$defs": {
"binding": {
"type": "object",
"additionalProperties": false,
"required": [
"action",
"target",
"actor",
"principal",
"purpose",
"digest",
"pdp_digest",
"pdp_path"
],
"properties": {
"action": {
"type": "string",
"minLength": 1
},
"target": {
"type": "object"
},
"actor": {
"type": "string",
"minLength": 1
},
"principal": {
"type": "string",
"minLength": 1
},
"purpose": {
"type": "string",
"minLength": 1
},
"digest": {
"type": "string",
"pattern": "^sha256:[0-9a-f]{64}$",
"description": "SHA-256 over the canonical JSON of action, actor, principal, purpose, target (sorted keys, RFC 8259). Distinguishes approved from approved-for-this-exact-request."
},
"pdp_digest": {
"type": [
"string",
"null"
],
"pattern": "^sha256:[0-9a-f]{64}$",
"description": "The flex-auth NewDecisionBinding request_digest recorded at issue time, or null when the approval was not issued against a PDP decision. Always present so its absence is a stated fact rather than a missing key. When non-null, access-engine MUST compare this to the digest it already computes and MUST NOT re-derive the native digest as a substitute. A PEP on a privileged lane MUST refuse a claim whose pdp_digest is null."
},
"pdp_path": {
"type": "boolean",
"description": "Whether this approval was declared at issue for the GH-DEC-2026-003 PDP consumption path. GH-DEC-2026-008 requires pdp_digest on that path, so this engine refuses to create a pdp_path approval without one; a true value therefore guarantees pdp_digest is non-null. A consumer on that path MUST require pdp_path true and MUST NOT infer path intent from a pdp_digest that merely happens to be present."
}
}
},
"freshness": {
"type": "object",
"additionalProperties": false,
"required": [
"observed_at",
"ttl_seconds",
"not_after"
],
"properties": {
"observed_at": {
"type": "string",
"format": "date-time"
},
"ttl_seconds": {
"type": "integer",
"minimum": 1
},
"not_after": {
"type": "string",
"format": "date-time"
}
}
},
"validity": {
"type": "object",
"additionalProperties": false,
"required": [
"not_before",
"expires_at"
],
"properties": {
"not_before": {
"type": "string",
"format": "date-time"
},
"expires_at": {
"type": "string",
"format": "date-time"
}
}
}
}
}