Public spec · no login required

Evidence Pack Format Spec

The exact pack_manifest.json schema and SHA-256 hashing rule behind every Protokol Compliance Evidence Pack — published here so any third-party tool, not just our own /verify page, can independently check that a pack hasn't been tampered with.

Current format version

Every pack exported today declares "format_version": "1.1" in its manifest, plus a spec_url pointing at this page — both the single-meeting pack_manifest.json AND the whole-client client_manifest.json carry the same stamp. A manifest with no format_version field predates this spec (any pack exported before this page existed) — it is the legacy case, still fully verifiable, because the hashing rule below has never changed since the pack format was introduced.

An unrecognized/future format_version (e.g. a pack made by a newer Protokol than your verifier knows about) should be reported as "unrecognized format version" rather than silently compared against a rule it doesn't actually implement.

pack_manifest.json schema

Every field in a single-meeting evidence/audit pack's manifest, in the order they appear at export time.

FieldTypeDescription
pack_versionstringOn-disk pack SHAPE version. Unchanged at "1.0" since the pack format was introduced — describes the ZIP layout, not this spec.
pack_kind"evidence" | "audit""evidence" = curated regulator deliverable (selected questions only). "audit" = the complete bundle.
generated_atstring (ISO 8601)UTC timestamp the pack was generated at export time.
meeting_idstringThe Protokol meeting id this pack was exported from.
form_idstring | nullThe compliance form attached to the meeting, if any.
form_versionstring | nullThe version of that form at export time.
ai_modelstringThe AI model used to produce the dossier (e.g. "gemini-2.5-flash").
reviewed_atstring | nullWhen an advisor reviewed/approved the dossier, if at all.
reviewed_bystring | nullThe reviewing user’s id.
acknowledged_incompletebooleanTrue if the advisor explicitly acknowledged missing-critical items before export.
redactedboolean (optional)Present and true only when the pack was exported with export-time redaction applied.
sha256string (64-char hex)The integrity hash of every OTHER file in the pack. See "The hashing rule" below.
format_versionstring (optional)The version of THIS published spec the pack was built against. Absent = legacy, pre-spec pack (still verifiable — see below).
spec_urlstring (optional)The URL of this page at export time.
warningstring (optional)Present only when the dossier had not been reviewed by an advisor at export time.

The hashing rule

sha256 is computed over every OTHER file in the pack — pack_manifest.json itself is excluded (it is only added to the ZIP after this hash is taken) — in ascending filename order, feeding each file's name then its UTF-8 content bytes into one running SHA-256 digest:

const sha256 = createHash('sha256')
for (const name of Object.keys(fileContents).sort()) {
  sha256.update(name)
  sha256.update(fileContents[name])
}
const packHash = sha256.digest('hex')

To verify a pack independently: unzip it, read every file except pack_manifest.json, sort the remaining filenames ascending, and for each one (in that order) feed its filename bytes then its content bytes into a single SHA-256 digest. Compare the resulting hex digest to the sha256 field declared in the pack's own pack_manifest.json — a match proves the pack has not been altered since export (tamper-evidence only; it does not by itself prove Protokol issued it — for that, pair it with /verify's provenance check against our audit log).

client_manifest.json schema

A whole-CLIENT evidence bundle (every "done" meeting for one client, exported as one client-pack-*.zip for FCA SYSC 9.1 "show me everything on client X" requests) declares its own root-level manifest, client_manifest.json, alongside one pack_manifest.json per included meeting subfolder (schema above, unchanged).

FieldTypeDescription
client_idstringThe Protokol client id this whole-client bundle covers.
client_namestringThe client’s display name at export time.
exported_atstring (ISO 8601)UTC timestamp the bundle was generated at export time.
exported_bystring | nullThe exporting advisor’s email.
meeting_countnumberCount of "done" meetings actually included in the bundle.
included_meeting_idsstring[]The meeting ids included (status = "done") — one subfolder per id.
excluded_meetingsArray<{ id, reason }>Meetings for this client NOT included (not yet "done"), each with the reason.
sha256_per_meetingRecord<meeting_id, string>Each included meeting’s own subfolder hash, keyed by meeting_id — computed the exact same way as that meeting’s own pack_manifest.json sha256 field (see "The hashing rule" above).
root_sha256string (64-char hex)Hash-of-hashes over every value in sha256_per_meeting. See "The client (root) hashing rule" below.
format_versionstring (optional)The SAME published spec version as pack_manifest.json’s format_version. Absent = legacy, pre-slice-0194 bundle (still verifiable).
spec_urlstring (optional)The URL of this page at export time.

The client (root) hashing rule

root_sha256 is a hash-of-hashes, one level up from a single meeting's sha256: each included meeting's subfolder is hashed exactly like a single-meeting pack (the same rule above, excluding that folder's own pack_manifest.json), keyed into sha256_per_meeting by meeting_id. Those per-meeting hash STRINGS (not raw file bytes) are then sorted by meeting_id and concatenated into one more SHA-256 digest:

const sortedHashes = Object.keys(sha256PerMeeting).sort().map(k => sha256PerMeeting[k])
const rootSha256Hash = createHash('sha256').update(sortedHashes.join('')).digest('hex')

To verify a client bundle independently: recompute each meeting subfolder's hash the same way as a single-meeting pack, sort those hashes by meeting_id, concatenate the hash strings (in that sorted order — no separator), and SHA-256 the concatenation. Compare the resulting hex digest to the root_sha256 field declared in the bundle's own client_manifest.json — same tamper-evidence guarantee as a single-meeting pack's sha256, one level up.

Version history

1.1Current. Adds the format_version and spec_url fields to the manifest and publishes this page. Slice 0194 extends the same stamp to the whole-client client_manifest.json. The hashing rules are unchanged from the original pack format.
(none)Legacy — every pack (or client bundle) exported before this spec existed. No format_version field; verified using the same hashing rules documented above.