Why a package
An agent that works a regulated case - onboarding a business, dispositioning a screening hit - leaves its trail across several systems: the grant that authorised it, the provider calls it made, the policy that scored the result, the person who approved the decision. An auditor asking “why was this business referred, and who approved the decline?” should not have to query all of them, or trust any of them. An evidence package is one JSON document per case that answers those questions on its own:
The format is specified in
spec/evidence-package.md, with a JSON Schema in
spec/evidence-package-1.0.schema.json.
How tamper evidence works
- Canonical bytes. A package is only valid as its own RFC 8785 canonical JSON, so every byte matters.
- A hash chain. Each entry carries the hash of the entry before it and of itself. Change an entry and verification names that entry and shows the expected and actual hash.
- An anchor. On export the auth service writes the package root into the
tenant’s audit chain as a platform-only entry and embeds that entry in the
package. Tenants cannot write such entries through
POST /v1/audit/log. - A signature. The service signs the root and the anchor with its platform key, so anyone with the service JWKS can check the package came from the service.
What verification proves
Entries marked tenant-asserted are statements the tenant’s platform made to
the service when they were recorded; the service’s signature proves it
received them then, not that they are true. Grants, decisions, consumptions
and revocations come from the service’s own records.
Privacy by default
A default package does not let a reader recover or confirm who was involved, what the case is about, which provider records were fetched or what the tool inputs were, and two packages cannot be joined. Five classes are keyed per case with HMAC unless disclosed:principalandapproveridentifiers, and the casesubject: pseudonyms (pz:...);- upstream
recordidentifiers: pseudonyms, stable within the case so citations still line up; - tool input and output
contentdigests: keyed digests (hmac-sha256:...), so equal inputs in two cases do not match.
action_hash is an unkeyed hash over the clear
subject; putting it next to a pseudonymised subject would let anyone confirm a
guess, so such packages carry a keyed action_ref instead. Memos, rationales
and rule reasons appear only as digests.
Disclosure (for example "disclose": ["record"] so an auditor can look up the
provider records) is allowed only for developers an operator lists in
EVIDENCE_DISCLOSURE_DEVELOPER_IDS. The case owner, holding the tenant key,
can always recompute a keyed value to confirm it.
Verifying a package
Get the root and anchor from somewhere other than the package - the export response headers you stored, or the tenant audit log (GET /v1/audit/entries?action=evidence.package_exported) - and the service
JWKS from /.well-known/jwks.json. Then:
grantex-evidence verify ... with the Python SDK.) It exits 0 only if every
check passes and prints the trust basis:
--root, exit 2):
Producing evidence
A platform records evidence as the case runs withPOST /v1/evidence/cases/{caseId}/records: run context, tool calls with their
upstream record identifiers, policy evaluations, dispositions and
recommendations. Each record is checked when it is written - including its
references to earlier records and its grant - so an accepted case can always
be exported. Records are idempotent on their own ids, and mistakes are voided
with POST /v1/evidence/cases/{caseId}/void, never deleted. Decisions come
from the auth service’s decision grants. When the case is decided,
POST /v1/evidence/cases/{caseId}/export returns the package. Everything is
behind the EVIDENCE_EXPORT_ENABLED flag, off by default.