Skip to main content
MeshQu is currently available for research, evaluation and design-partner deployments. Production availability and support commitments are agreed per engagement. This changelog covers changes to the publicly documented API and SDK contracts. Releases with no change to those contracts have no entry here.
MeshQu follows Semantic Versioning. The API contract version is v1 — the /v1 URL prefix, which is stable and only changes on a major release. The current release is 1.31.0.

Versioning policy

  • Major (v2.x): Breaking changes to request/response contracts or authentication. Announced with a migration guide and deprecation period.
  • Minor (v1.2): New endpoints, fields, or features. Backwards-compatible.
  • Patch (v1.1.1): Bug fixes and performance improvements. No contract changes.

Stability guarantees

  • Existing response fields are never removed or renamed within a major version.
  • New optional fields may be added to responses at any time — clients should ignore unknown fields.
  • Webhook payload schemas follow the same policy.

API version header

All endpoints are prefixed with /v1. When a new major version is released, the previous version will remain available for a documented deprecation period.

v1.31.0

The audit action list gains a value, connection_permission_outcome. The three audit-log reads can return it, and GET /v1/audit-log accepts it as a filter. No operation is added or removed and no response status changes. The only schema change is the new value in the action list. A client that treats the list of audit actions as closed, for example by validating action against the values the reference listed before, will reject an event that carries the new value. Audit actions
  • connection_permission_outcome is a new audit action value. No operation in this reference records it, so the reference does not say when it appears.
  • Where it can appear. As action on the events returned by GET /v1/audit-log, GET /v1/admin/audit-log and GET /v1/audit-log/entity/{type}/{id}.
  • As a filter. GET /v1/audit-log accepts it as the action query parameter. Before, it was refused like any other value that is not a listed audit action: 400 FST_ERR_VALIDATION.

v1.30.0

Breaking for API keys: inviting a member and changing a member’s role are now human acts. POST /v1/admin/members/invite and PATCH /v1/admin/members/{user_id} answer 403 HUMAN_PRINCIPAL_REQUIRED to any credential that is not a verified human session, which includes every API key. Before, an API key that held members:write and the admin role could do both. No operation is added or removed, no request schema changes, and no other response changes. Callers that relied on the old behaviour. These requests used to succeed and are now refused (403):
  • POST /v1/admin/members/invite made with an API key. Before, it answered 201.
  • PATCH /v1/admin/members/{user_id} made with an API key. Before, it answered 200.
Both are done by a signed-in person instead. An API key that invites a user who is already a member now gets 403 as well; before, it got 409. Members
  • Two refusals, in order. An API key without members:write is still refused 403 FORBIDDEN first. A credential that holds members:write but is not a human session is then refused 403 HUMAN_PRINCIPAL_REQUIRED, and nothing is written.
  • A human session is unaffected. Only the admin role holds members:write, so a verified admin session reaches both operations as before, and anyone else was already refused 403 FORBIDDEN.
  • What did not change. DELETE /v1/admin/members/{user_id}, which removes a member, and GET /v1/admin/members can still be called with an API key. This release makes only inviting and role changes human acts.

v1.29.0

Breaking: a request body with a key the reference does not declare is now refused. POST /v1/decisions/evaluate, POST /v1/decisions/record and POST /v1/policies/{id}/versions/{version}/ratify answer 400 FST_ERR_VALIDATION to a body that carries an unknown top-level key. Before, such a key was accepted and ignored. Callers that send extra keys now get 400. Ratify also adds review-state checks where they apply, and the audit actions gain a value. No operation is added or removed. Callers that relied on the old behaviour. These requests used to succeed and are now refused:
  • A body to POST /v1/decisions/evaluate or POST /v1/decisions/record with a top-level key other than the ones listed under “Decisions” below (400).
  • A body to POST /v1/policies/{id}/versions/{version}/ratify with a key other than expected_revision, approval_authority and ratifier_role (400).
  • A ratify whose reviewed active rules are stale, or whose latest applicable review is adverse (409).
Separately, a client that treats the list of audit actions as closed, for example by validating action against the values the reference listed before, will reject an event that carries the new value. Decisions: evaluate and record
  • Unknown top-level keys are refused. POST /v1/decisions/evaluate accepts context (required), options, action and source. POST /v1/decisions/record accepts context (required), options, action, actor, evidence_manifest_digest, evidence_manifest and source. Any other top-level key is refused 400 FST_ERR_VALIDATION, including a key that only the other route takes, such as actor on evaluate. The refused body reaches neither the policy read nor the decision write. Before, an unknown top-level key was accepted and ignored.
  • source is accepted and not read. Both bodies now declare a top-level source, described as “Accepted and not read.” It accepts any value, and nothing reads it. Before, it was accepted and ignored as an unknown key.
  • Only the top level is closed. An unknown key inside context or options is still accepted on both routes. The reference declares no additionalProperties for context, options, action or actor, and context.fields, context.metadata and action.metadata accept any key.
  • The form API contract. In GET /v1/forms/{formId}/api-contract, the evaluate request schema in openapi_snippet is now closed (additionalProperties: false) and declares options and action beside the required context. It does not list source, which the endpoint accepts. Before, it declared context only and did not close the schema.
Ratify
  • The body is closed. A body with any key other than expected_revision, approval_authority and ratifier_role is refused 400 FST_ERR_VALIDATION, and nothing is ratified. Before, an unknown key was accepted and ignored. expected_revision is still required. A request authenticated by an API key is still refused 403 HUMAN_PRINCIPAL_REQUIRED before the body is checked, and a key without policies:write still gets 403 FORBIDDEN first.
  • Reviewed rules must still match. When ratification relies on an accepted review, a change to the active evaluated rules is refused with 409 REVIEWED_SUBJECT_STALE. Nothing is ratified; review the current rules again or restore the accepted rules. This check covers the active evaluation fields, not descriptive metadata or inactive rules.
  • An adverse review blocks ratification. A latest applicable review of CORRECT_AND_RESUBMIT or STOP is refused with 409 RATIFY_REVIEW_ADVERSE or, where the review gate requires an acceptance, 409 RATIFY_NO_ACCEPT_RECORD. Nothing is ratified.
  • A stale expected_revision remains 409 STALE_DRAFT_REVISION, and a version that cannot transition remains 409 INVALID_TRANSITION.
  • When an accepted review supports ratification, the policy_ratified audit entry includes the review record and submission identifiers.
Audit actions
  • connection_consented is a new audit action value. No operation in this reference records it, so the reference does not say when it appears.
  • Where it can appear. As action on the events returned by GET /v1/audit-log, GET /v1/admin/audit-log and GET /v1/audit-log/entity/{type}/{id}.
  • As a filter. GET /v1/audit-log accepts it as the action query parameter. Before, it was refused like any other value that is not a listed audit action: 400 FST_ERR_VALIDATION.

v1.26.0

Clearer refusals across key, licence, discard, ratify and codification writes. Most changes below replace a status a caller already got on the same input, whether a success, a bare 500 or a misleading 404, with a named refusal. One widens who may discard a draft. The set of routes and their request schemas are unchanged. Callers that relied on the old behaviour. Six requests that used to succeed are now refused: revoking a key that is already revoked (409), creating a key that asks for more than the calling API key holds (403), discarding a draft with an API key (403), ratifying a version of an archived policy (409), issuing a licence with a future start while the key’s governing licence is in force (409), and a codification block that repeats a unit_id (422). API keys
  • Revoking a key that is already revoked is refused. DELETE /v1/api-keys/{id} and DELETE /v1/admin/api-keys/{id} answer 409 API_KEY_ALREADY_REVOKED, with details.revoked_at giving the revocation that stands. Nothing is written: the original revocation time and actor are kept and no second audit entry is appended. Before, the second call answered 200 or 204 and overwrote the revocation time and actor. An unknown key is still 404 NOT_FOUND.
  • Creating a key: an API key cannot ask for more than it holds. POST /v1/api-keys and POST /v1/admin/api-keys, when the caller is an API key:
    • Every requested scope must be one the calling key holds, otherwise 403 API_KEY_SCOPE_ESCALATION, with the scopes it lacks in details.exceeding_scopes.
    • The requested role must be one the calling key’s role may create, otherwise 403 API_KEY_ROLE_ESCALATION, with details.requested_role and details.minter_role. admin may create any role; every role may create itself and viewer; operator and auditor may not create each other.
    • An omitted scopes or role is checked as the value it would default to. A request whose default exceeds the calling key is refused, not narrowed.
    • What this does not cover. A request from a verified human session is not bounded this way. The expiry of the new key is not bounded by the calling key’s. And the check applies to these two routes only: it does not change what any other route lets the holder of a key grant.
Agent licences These changes are on POST /v1/agent-licences and POST /v1/agent-licences/{id}/revoke.
  • POST /v1/agent-licences: a version that is not in force. When the named version exists, matches policy_id and policy_version_number, and is not ratified and in force, or its policy is archived, the answer is 409 AGENT_LICENCE_POLICY_VERSION_NOT_IN_FORCE. It used to be a 500. Nothing is issued. A policy_version_id that does not exist, or does not match the policy and number named, still answers 500.
  • POST /v1/agent-licences: a future start while a licence is live. A valid_from after now, for a key whose governing licence is in force now, is refused 409 AGENT_LICENCE_FUTURE_START_WHILE_LIVE, with details.live_licence_id, details.live_valid_until and details.requested_valid_from. It used to be a 201 that would govern at once and leave the key with no valid licence until its start. Nothing is issued. The governing licence is the most recently issued one for the key. A licence that starts now is not refused for this reason, and neither is a future start for a key whose governing licence is not in force.
  • A committed write whose read-back failed. If the read after an issue or revoke commits but fails, the response is 500 AGENT_LICENCE_READBACK_FAILED with details.committed: true and an identifier for the committed record. Check that record before retrying; repeating an issue could replace the governing licence. A 404 means nothing was written.
  • UUID letter case. Licence issue and revoke now treat upper-case UUIDs as the same identifiers as their lower-case spellings.
Policies
  • Discarding a draft is a human act. DELETE /v1/policies/{id}/versions/{version} now refuses a request authenticated by an API key with 403 HUMAN_PRINCIPAL_REQUIRED, before the route looks at the draft. That includes a key that authored the draft, which could discard it before. The scope check runs first: a key without policies:write still gets 403 FORBIDDEN. A verified human session reaches the route as before. HUMAN_PRINCIPAL_REQUIRED is the refusal ratify, reject and activation already give an API key.
  • A tenant admin may discard an API-key-created draft. A verified tenant admin may discard an eligible draft created by one of the tenant’s API keys, even if that key remains active. The request must include a reason, or it is refused with 400 DISCARD_REASON_REQUIRED. The reason is recorded in the audit trail.
  • GET /v1/policies/{id} no longer offers a key a discard it would be refused. For a request authenticated by an API key, permissions.canDiscard is false and actions does not include discard.
  • Ratify refuses a version whose policy is archived. POST /v1/policies/{id}/versions/{version}/ratify answers 409 INVALID_TRANSITION with details.reason POLICY_ARCHIVED, the code and reason activation already gives for an archived policy. The version stays as it was and nothing is recorded. Before, the ratify succeeded, leaving a ratified version that activation then refuses.
  • A codification block that declares one unit_id on two clauses is refused. On POST /v1/policies, POST /v1/policies/{id}/versions, POST /v1/policies/{id}/versions/{version}/supersede and a codification.clauses list with the same unit_id on more than one clause answers 422 CODIFICATION_REFUSED with details.code DUPLICATE_UNIT_ID. Nothing is stored. Before, the later clause silently replaced the earlier in the lookup, so one outcome accounted for both. unit_id values are compared exactly, so u-1 and U-1 are two clauses. A version already stored with a duplicate is not rewritten.

v1.23.0

A server-side bound on policy draft writes.
  • Affected public route: PATCH /v1/policies/{id}/versions/{version}.
  • The response. A write that exceeds the bound is rolled back — nothing is written — and the request fails with 503 AUTHORING_WRITE_CONTENDED, including details.committed: false, a reason, and a Retry-After header. This response is a definite non-commit and is safe to retry.
  • Client timeouts remain ambiguous. A client-side timeout alone does not tell the caller whether the write committed; read the draft before retrying.

v1.22.0

Active-rules hash on the policy version read. An additive field: every response that carries a policy version now includes a hash of that version’s current active rules.
  • active_rules_hash is added to GET /v1/policies/{id}/versions/{version}, the versions list, and every other response that embeds a policy version (create, patch, reopen, set-aside, supersede, submit, reject, recall and ratify). It identifies the version’s active evaluated rules.
  • What it covers. The hash is derived from each active rule’s code, condition, severity and when-clause, in stored order. Editing a rule’s name, description or other non-evaluated metadata does not change it, and adding a new inactive rule does not change it either — only a change to an active rule’s code, condition, severity or when-clause, or a change to which rules are active, moves the hash.
  • Always present on 1.22.0 responses. Every policy version has this field from this release on. Servers older than 1.22.0 omit it — clients should treat a missing field as unknown, not as a specific value.

v1.20.0

Audit and policy response compatibility. Audit log responses may contain the new authoring_capability_carried action. Clients that treat audit action values as a closed enum should handle unknown values. Policy version responses may return a key’s display identity in closed_by when that key superseded a draft.

v1.19.0

Ratification review gate. Where a policy version is subject to the review gate, ratification requires an accepted review of the current draft. A request without one is refused with 409 RATIFY_NO_ACCEPT_RECORD. Clients should use details.reason to distinguish an absent seal, absent acceptance, or unresolved lineage.

v1.18.0

One pending version per policy
  • A policy now has its version in force plus at most one pending version: a draft, a version submitted for review, or a ratified version that is not yet in force.
  • POST /v1/policies/{id}/versions refuses a second pending version with 409 DRAFT_CONFLICT. details.reason is OPEN_DRAFT, RATIFIED_AWAITING_ACTIVATION or PENDING_VERSION_EXISTS, and details.pending_version names the pending version when it is known.
Reopen, set aside and supersede. Each is a human-only act: a verified human session is required, and an API key receives 403 HUMAN_PRINCIPAL_REQUIRED.
  • POST /v1/policies/{id}/versions/{version}/reopen: a ratified version that is not in force continues as a new draft, which must be ratified again. The ratified version is closed as reopened and can no longer be activated.
  • POST /v1/policies/{id}/versions/{version}/set-aside: the pending version is closed as set_aside with a required reason, so that a new draft can start from the version in force.
  • POST /v1/policies/{id}/versions/{version}/supersede: a correction to a draft. The draft is closed as replaced and kept unchanged, including its codification record, and the corrected content becomes a new draft. It takes expected_revision, rules and an optional codification block processed exactly as on version creation.
New version statuses and fields
  • Version status may now also be reopened, set_aside or replaced. A closed version cannot be ratified or activated. superseded keeps its existing meaning: a version that was in force and has been replaced.
  • Closed versions carry closed_at, closed_by and closed_reason.
  • New audit actions: policy_version_reopened, policy_version_set_aside, policy_version_replaced.

v1.16.0

The codification lint’s findings, on the wire
  • Policy version responses carry an optional codification_record.lint. Present only on a version whose write ran the model-assisted codification lint; absent — not null — on every other version, including one written before the lint existed and one written with the lint disabled.
  • Each linted clause carries its findings (a code, a message, and diagnostic evidence) and a needs_context flag. status: "ran" with an empty findings array for a clause is a real statement — the lint ran and found nothing — distinct from an absent lint, which means the lint never ran at all.
  • The response contains derived findings and the diagnostic evidence listed in the API schema.
What it is not
  • It is advisory, produced by an automated, model-assisted check. It is unsigned and unhashed — not covered by policy_rules_hash or policy_snapshot_digest, and it enters no receipt, digest, snapshot, export or evidence manifest.
  • It is not part of review or ratification. A finding is a prompt to whoever is codifying or ratifying the version, never a disposition — it does not change codification_record.ledger or codification_record.counts.

v1.15.0

Policy version activation
  • New POST /v1/policies/{id}/versions/{version}/activate — human-only. Puts this exact ratified version in force: the version currently in force (if any) is superseded, this version governs evaluations from now on, and a signed immutable policy_version_activation receipt is returned.
  • A policy’s first activation starts it in shadow mode. A later activation keeps the policy’s current mode — moving a policy from shadow to enforcing stays its own act, POST /v1/policies/{id}/activate.
  • Refusals: 404 NOT_FOUND when the policy or version does not exist in this tenant; 409 INVALID_TRANSITION with details.reason of POLICY_ARCHIVED, VERSION_ALREADY_IN_FORCE, VERSION_NOT_RATIFIED, or VERSION_NOT_PENDING; 500 RATIFIER_KEY_REQUIRED when no ratifier key is configured. Nothing is written on a refusal.
  • New audit action policy_version_activated.
Ratification no longer puts a version in force
  • POST /v1/policies/{id}/versions/{version}/ratify’s description is corrected: ratification records that a human approved the exact draft version, and returns a signed PolicyApprovalReceipt when a ratifier key is configured, but it does not put the version in force. The version currently in force keeps governing evaluations until the ratified version is activated with the new endpoint above.

v1.14.0

The codification record: what a version’s clauses were accounted for
  • Policy version responses carry an optional codification_record. It is present only on versions written through the codification path introduced in 1.13.0, and absent — not null — on every other version.
  • It records one disposition per declared clause, the clause text a reviewer needs in order to judge it, the counts, and any proposal the rule grammar refused.
  • The declines are the point. Before this, a clause the codifier read and chose not to express as a rule left no trace once the write completed, so a reviewer could see the rules that were produced but not the judgements behind the ones that were not.
  • ledger, counts and refusals are the write boundary’s own output, not values the caller supplies. A codification_record sent in a request body is ignored.
What it is not
  • It is unsigned and unhashed. It is not covered by policy_rules_hash or policy_snapshot_digest, and it enters no receipt, snapshot, export or evidence manifest.
  • It does not establish that the declared clauses are the complete source document or that each rule expresses its cited clause correctly.

v1.13.0

Codification: rule citations are derived, not asserted
  • POST /v1/policies and POST /v1/policies/{id}/versions accept an optional codification block: the clauses the caller read, a map from rule code to clause, and a disposition (ambiguous, unsupported, non_normative) with a reason for every clause that produced no rule.
  • When it is supplied, each stored rule’s source_ref is derived from the clause it is mapped to, and any source_ref sent in the request is ignored. A rule that names no clause cannot be stored through this path.
  • Refusals return 422 CODIFICATION_REFUSED with the specific reason in details.code — a clause with no outcome, a disposition with no reason, or a rule whose condition the evaluator cannot execute.
  • Omitting the block leaves behaviour exactly as before. Existing callers are unaffected.
What this establishes, precisely
  • That every clause the caller declared has exactly one accounted-for outcome, and that every stored rule is executable and attributed to a declared clause with a citation the API derived rather than accepted.
  • It does not establish that the declared clauses are the whole source document. Nothing server-side reads the document, so a caller that under-reports both its clauses and their outcomes cannot be distinguished from one that does not. That remains a question for human review before ratification.

v1.12.0

Evaluation results say where a rule came from
  • POST /v1/decisions/evaluate and POST /v1/decisions/record may now return rule_sources: a map from rule code to the source reference its author recorded (document, section, url). Present only when at least one rule in the governing snapshot carries one.
  • It sits beside result, not inside it. result is the signed object and its integrity_hash covers the violations exactly as the evaluator produced them, so a caller joins rule_sources to violations[].rule_code or na_rules[].rule_code itself. Existing clients that ignore the field see unchanged bodies.
  • What it is evidence of, stated precisely: source_ref is author-supplied and is excluded from policy_rules_hash, so ratification does not attest to it. It is covered by policy_snapshot_digest, which hashes the snapshot rules verbatim — so a v2 receipt binds that this snapshot carried this reference. That is a narrower claim than “the rule faithfully encodes that clause”, which no digest can make.
Corrections to the published contract
  • The OpenAPI specification now defines the recursive when gate union. Earlier published versions had an unresolved schema reference that could break validators and generated clients for policy operations.

v1.11.0

Ungoverned evaluations are distinguishable from a governed pass
  • POST /v1/decisions/evaluate and POST /v1/decisions/record may now return an optional governance_coverage object when no applicable rule was evaluated. It carries permission_established: false, saying explicitly that an ALLOW over zero evaluated rules records the absence of an applicable ratified rule rather than a determination that the action is permitted.
  • The object is unsigned and is not a receipt field. No hash payload, signing envelope, canonical profile or receipt_schema_version changed, and stripping the object does not affect the signed payload. Existing receipts verify exactly as before.
  • It is absent on a governed pass, so a client that ignores it sees byte-identical response bodies to v1.10.0. Treating its absence as permission is a client error.
  • coverage distinguishes NO_ACTIVE_RULES from NO_APPLICABLE_RULES. cryptographically_established is true only for NO_ACTIVE_RULES, and only when evaluated_rules_hash equals the empty-active-set hash and the result is signed — it means the claim is bound into a signed payload, not that the signature has been verified.
  • Note POST /v1/decisions/evaluate continues to answer 404 NO_ACTIVE_RULES when a decision type resolves to no active rules at all; it refuses the evaluation rather than returning an advisory. The advisory’s reachable case is active rules that are all excluded by their when gates.
Malformed rule conditions are refused rather than silently passing
  • A rule whose condition the evaluator cannot execute is now rejected with VALIDATION_ERROR on draft update and ratify, and with UNKNOWN_CONDITION_SHAPE (400) at evaluation time. Previously, an invalid condition could be skipped and reported as a passing rule.
  • A boolean temporal operator set to false (not_expired, not_future, must_be_before, must_be_after) and min_length: 0 are likewise refused: the evaluator reads none of them, so they asked nothing. Numeric bounds of 0 are unaffected and remain executable — { field, max: 0 } still refuses any positive value.
  • Rules already stored are unaffected until they are next edited or ratified.
Corrections to the published contract
  • PATCH /v1/policies/{id}/versions/{version} now answers 409 NOT_DRAFT for a submitted or ratified version even when the submitted rules are also malformed. It previously answered 422 in that case, reporting a rule problem where the version was not editable at all.
  • The temporal rule condition schema documents min_days_after, must_be_before and must_be_after. All three were already executed by the evaluator and missing from the published schema.
  • Policy list endpoints document the q search parameter, and policy version responses document draft_revision. Both shipped earlier and had not been regenerated into the spec.

v1.10.0

Case-insensitive policy when gates
  • when gates with equals or in accept an opt-in case_sensitive: false, which folds string-vs-string comparisons only. Booleans and numbers always compare strictly, and gates never coerce types — a boolean true is never matched by the string "true".
  • An absent key means strict comparison, exactly as before: every previously authored rule and every ratified policy version evaluates byte-identically. There is no schema default — absence is the canonical strict spelling.
  • Note the default is deliberately the opposite of the list-condition case_sensitive (which defaults to false): gates match ratified domains, list conditions match free text.
OpenAPI spec refresh
  • The published spec now also reflects earlier shipped work it had lagged behind: boolean members in when.in lists and admin member invitations by email with actor_email in audit views.

v1.9.0

Tenant Governance Settings
  • New GET /v1/settings and PATCH /v1/settings endpoints for tenant-level governance configuration.
  • Includes maker-checker toggle controlling whether policy versions require an independent ratifier before becoming active.

v1.8.0

Human Decision Capture (Forms)
  • New Forms surface for capturing structured human decisions that produce receipts identical in shape to programmatic decisions.
  • Endpoints: POST /v1/forms, GET /v1/forms, GET /v1/forms/:formId, PATCH /v1/forms/:formId, DELETE /v1/forms/:formId.
  • Form schemas and API contracts are introspectable: GET /v1/forms/:formId/schema, GET /v1/forms/:formId/api-contract.
  • Submissions: POST /v1/forms/:formId/submit returns a decision receipt; retrieve later via GET /v1/forms/:formId/receipts/:decisionId.

v1.7.0

Policy v2 Lifecycle (Submit / Reject / Recall)
  • Draft policy versions now move through an explicit lifecycle:
    • POST /v1/policies/:id/versions/:version/submit — submit a draft for ratification.
    • POST /v1/policies/:id/versions/:version/reject — reject a submitted draft.
    • POST /v1/policies/:id/versions/:version/recall — recall a submitted draft back to draft state.
  • Additional draft-version endpoints: GET /v1/policies/:id/versions/:version, PATCH /v1/policies/:id/versions/:version, DELETE /v1/policies/:id/versions/:version.
  • New GET /v1/policies/:id/decisions — list all decisions evaluated against a given policy.

v1.6.0

Receipt Schema v2 + Approval Lineage
  • New top-level fields on decision receipts:
    • receipt_schema_version — explicit schema version marker.
    • policy_snapshot_digest — canonical SHA-256 of the bound policy snapshot.
    • evidence_manifest_digest — canonical SHA-256 of the evidence manifest, when present.
  • Policy Approval Receipts — cryptographic ratifier signatures on the policy version that governed a decision. The offline verifier now accepts ratifier and custodian public-key sets as additional trust roots.
  • Two new bundle verifier sub-claims — evidence (bundled evidence manifest matches evidence_manifest_digest) and approval_lineage (per-version approval-receipt digest, ratifier signature and policy_rules_hash) — taking the sub-claim set from eight to ten.
  • All v1 receipts remain verifiable indefinitely — schema v2 fields are additive.

v1.5.0

Public Verification Surface
  • New JWKS endpoint: GET /v1/.well-known/signing-keys — versioned public signing keys for offline receipt verification.
  • New public, unauthenticated endpoints (callers do not need an API key):
    • GET /v1/receipts/:decisionId — fetch a signed decision receipt.
    • GET /v1/receipts/:decisionId/bundle — download the full Verification Bundle.
    • GET /v1/chains/:chainId/verify — verify a chain proof.
    • GET /v1/chains/:chainId/seal — read a chain seal.
    • GET /v1/chains/:chainId/bundle — download a chain Verification Bundle.
  • Verification Bundles — self-contained, offline-verifiable archives under a digest-of-digests manifest, with a sub-claim reported per artefact (receipt, policy snapshot, chain proofs, transparency proof, trust roots, canonicalisation). Two further sub-claims (evidence, approval_lineage) arrived in v1.6.0, bringing the set to ten.

v1.4.0

Actor Attribution
  • POST /v1/decisions/record now accepts an optional top-level actor object: id, type, role, authority, display_name.
  • actor.id is included in the integrity hash and cryptographically bound to every receipt.
  • Other actor fields are stored for audit display.
  • Backward compatible: actor is optional. If omitted and context.metadata.actor_id is present, the API falls back to that value. When actor.id is supplied, it takes precedence.
  • New actor optional parameter on client.record() in the TypeScript SDK.
  • Decision chain steps can now carry per-step actor attribution, enabling multi-actor chain audit trails.

v1.3.0

Structured Violation Reason Codes
  • Every violation now includes a reason_code field — a structured code for programmatic aggregation without text parsing.
  • 17 reason codes across all rule types: presence (FIELD_MISSING, FIELD_FORBIDDEN, FIELD_TOO_SHORT), temporal (DATE_EXPIRED, DATE_INVALID, etc.), threshold (VALUE_ABOVE_MAX, VALUE_BELOW_MIN, etc.), and list (VALUE_NOT_ALLOWED, VALUE_FORBIDDEN).
  • New ReasonCode type exported from @meshqu/types and re-exported from @meshqu/client.
  • Backwards compatible: reason_code is a new required field on violations. Existing reason strings are unchanged.
  • Replay note: New receipts have different integrity hashes than pre-reason_code receipts due to the additional field. Pre-migration receipts remain verifiable against their stored hash.
Decision Outcomes
  • New POST /v1/decisions/:id/outcome endpoint to capture what happened after a verdict.
  • Five outcome statuses: accepted, overridden, escalated, expired, abandoned.
  • One outcome per decision (409 on duplicate). Immutable after insert.
  • Outcomes included on GET /v1/decisions/:id response.
  • New recordOutcome() method in the TypeScript SDK.
Violation Analytics & Identifier Stability
  • GET /v1/decisions now accepts correlation_id query parameter.
  • Policy rules accept optional replaces_rule_code for advisory lineage tracking.

v1.2.0

Conditional Rule Applicability (When Gating)
  • Added optional when clause on policy rules — rules are only evaluated when the clause matches the decision context.
  • Non-matching rules return NA (not applicable) with a human-readable reason on the decision receipt.
  • New optional fields on EvaluationResult: rules_na (count) and na_rules (details).
  • Supported when operators: equals, in, exists, with all/any combinators for nested logic.
  • Snapshot hash includes when clauses (backward compatible — existing hashes unchanged).
Receipt Signing
  • Decision receipts are now signed with Ed25519 when a signing key is configured.
  • Receipts include signature, signature_kid, and signature_algorithm fields.
  • Auditors can verify receipts using the public key alone — no shared secret needed.
Security & Infrastructure
  • Encryption key versioning with rotation support and legacy fallback.
  • Rate limiting upgraded to shared state across API replicas.

v1.1.0

  • Added policy groups (CRUD + listing policies within a group).
  • Added alert webhook subscriptions with HMAC-signed delivery.
  • Added webhook delivery history and statistics endpoints.
  • Added decision SSE stream (GET /v1/decisions/stream).
  • Added audit log verification (POST /v1/audit-log/verify).
  • Added API key naming (key_name) for audit attribution.
  • Added advisory mode for policies.
  • Added two-tier rate limiting (pre-auth + post-auth).

v1.0.1

Production hardening release. All fixes are backwards-compatible at the HTTP layer.
  • SDK apiKey configuration is now required (throws a clear error if missing).
  • idempotency_key is now required when recording decisions (no longer auto-generated).
  • Added dryRun() as a semantic alias for evaluate().
  • Recorded decisions now capture audit attribution (api_key_id, api_key_name).
  • Backend infrastructure failures now return 503 Service Unavailable with retry_after instead of 500.
  • Production authentication hardening improvements.

v1.0.0

  • Initial release.
  • Policy CRUD with immutable versioning.
  • Decision evaluation (dry-run) and recording (persisted).
  • Decision replay for audit verification.
  • Alert listing and acknowledgement.
  • API key management with scoped access.
  • Audit event listing.
  • OpenAPI 3.0 spec and Swagger UI.