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 documents API and SDK functionality shipped into those deployments. Console UI changes are not tracked 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 (this changelog’s latest entry, the OpenAPI spec info.version, and the API package version) is 1.30.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.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. Before, neither operation declared a 403 or said who may call it. The reference now declares the 403 on both and describes the rule.
  • 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 checks what was reviewed, and the audit actions gain a value. No operation is added or removed and no response status changes. The reference descriptions of draft discard and key creation are rewritten to state behaviour that 1.26.0 introduced. 1.27.0 was not released. Its change, a new audit action value, is documented here. 1.28.0 has no entry of its own: its changes, the ratify checks, the closed ratify body and the rewritten discard and key-creation descriptions, are documented here too. This release follows 1.26.0 directly in this changelog. 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 of a version whose active rules changed since its latest sealed submission was sealed, when the newest review record on that submission is an ACCEPT (409).
  • A ratify, when no seal of the version or of a draft it supersedes was made under an authoring capability, of a version whose newest review record on its latest sealed submission is CORRECT_AND_RESUBMIT or STOP (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.
  • A version whose reviewed rules changed is refused. When the newest review record on the version’s latest sealed submission is an ACCEPT, the version’s current active rules must still hash to the active_rules_hash that submission sealed. A change to the active rules since the submission was sealed is refused, including reordering them: the hash is taken over the active rules in stored order. The answer is 409 REVIEWED_SUBJECT_STALE, with details.submission_id, details.review_record_id, details.sealed_active_rules_hash, details.current_active_rules_hash, details.candidate_revision and details.draft_revision. Nothing is ratified. Seal the current rules and have them reviewed again, or restore the rules that were accepted. Before, a change to the active rules after the seal did not stop the ratify. This applies whether or not an authoring capability sealed the version.
  • A version whose latest review is adverse is refused. When no seal of the version or of a draft it supersedes was made under an authoring capability, and the newest review record on its latest sealed submission is CORRECT_AND_RESUBMIT or STOP, the answer is 409 RATIFY_REVIEW_ADVERSE, with details.reason set to that disposition and details.submission_id and details.review_record_id. Nothing is ratified. Before, it was ratified. When a seal was made under an authoring capability, the same records are still refused 409 RATIFY_NO_ACCEPT_RECORD, as before.
  • What these checks do not cover.
    • Only the hashed fields of the active rules (code, condition, severity, when) are compared. A change to a rule’s name, description, source_ref, replaces_rule_code or rule_type, or to the content of a rule that is inactive before and after, is not refused. Making a rule active or inactive does change what is compared.
    • Neither refusal applies to a version with no sealed submission, or whose latest sealed submission has no review record. Where an authoring capability sealed the version or a draft it supersedes, such a version is still refused 409 RATIFY_NO_ACCEPT_RECORD, as before.
    • A version that can no longer be ratified, such as one already ratified, is still refused 409 INVALID_TRANSITION.
    • A request whose expected_revision is stale is still refused 409 STALE_DRAFT_REVISION first.
  • The policy_ratified audit entry cites the ACCEPT. When a ratification relied on an ACCEPT, the entry’s changes carries review_record_id and review_submission_id, whether or not an authoring capability was involved. Before, only a ratification under the review gate carried them.
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.
Reference descriptions These change what the reference says, not what the API does.
  • DELETE /v1/policies/{id}/versions/{version}. The description now states the 1.26.0 behaviour: only a verified human session may discard, an API key is refused 403 HUMAN_PRINCIPAL_REQUIRED (after the policies:write scope check), and a tenant admin may discard a draft that is not theirs in three cases, each with a reason. Before, it said an API key never takes the admin path and that a draft whose author key is still live is refused.
  • POST /v1/api-keys and POST /v1/admin/api-keys. The scopes and role descriptions now state the refusals 1.26.0 introduced (API_KEY_SCOPE_ESCALATION and API_KEY_ROLE_ESCALATION), the default scopes and the default role, and that a human session caller is not bounded. The default role depends on a deployment setting. Before, scopes said only that it defaulted to “read + evaluate”, and role had no description.
  • Still described only in the v1.26.0 entry. The reference does not yet describe the refusal of a ratify for an archived policy’s version, permissions.canDiscard on GET /v1/policies/{id}, INVALID_PARENT on POST /v1/admin/tenants, or the duplicate unit_id refusal on the codification block.

v1.26.0

Clearer refusals across key, licence, capability, discard, ratify, tenant 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. 1.25.0 was not released. This release follows 1.24.0 directly. 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). This entry is the current statement of some of these behaviours. The OpenAPI reference is regenerated for this release. It carries the revoke-key 409 and the licence and capability changes below, apart from UUID letter case. The reference descriptions of the other routes named here (key creation, draft discard, GET /v1/policies/{id}, ratify, tenant provisioning and the codification block) are unchanged in this release, so they do not yet describe the changes below. Key creation and draft discard are described in the reference from v1.29.0. 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. The reference now declares the 409 on both routes.
  • 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.
Licences and capabilities These changes are on POST /v1/agent-licences, POST /v1/agent-licences/{id}/revoke, POST /v1/authoring-capabilities and POST /v1/authoring-capabilities/{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.
  • The four routes: a committed write whose read-back failed. When the read after the commit returns nothing, or itself fails, the answer is 500 AUTHORING_CAPABILITY_READBACK_FAILED or 500 AGENT_LICENCE_READBACK_FAILED, with details.committed set to true. The record exists: details.licence_id or details.capability_id is its id. On the revoke routes that id is the revocation record, and details.revokes_licence_id or details.revokes_capability_id names the grant being revoked. Do not repeat the request: a second licence issue would govern in place of the first, and a repeated revoke is refused 409. Before, these answered a 404 *_NOT_FOUND or a bare 500. A 404 on these routes means nothing was written.
  • The four routes: UUID letter case. The UUIDs these routes take, that is key_id, policy_id and the version id (policy_version_id or source_policy_version_id) in the body, and the grant id in the revoke path, are lower-cased before use, so an upper-case spelling names the same record as the lower-case one. Before, an upper-case policy_id on POST /v1/authoring-capabilities was refused as not found for a version that exists, and a licence issued with upper-case ids was accepted but then failed integrity verification when read.
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 unbound draft that an API key created. With discard closed to keys, a tenant admin’s human session may discard a draft that is bound to no authoring capability and whose creation record in the audit chain names one of the tenant’s API keys as the actor, whether or not that key can still act. Before, this needed a key that could no longer act (revoked, inactive, or failing capability verification), and a draft whose author key was still live was refused. The body must carry { "reason": string } (400 DISCARD_REASON_REQUIRED without it), and the reason is recorded in the audit chain. Nothing is exhausted, because nothing is bound. A draft whose creation record names a person, names no key of the tenant, or cannot be resolved to a single record is still refused this way.
  • 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 POST /v1/policies/{id}/versions/{version}/supersede-assigned, 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.
Tenants
  • POST /v1/admin/tenants: a parent_tenant_id that names no tenant. A well-formed parent_tenant_id that matches no tenant answers 422 INVALID_PARENT. It used to be a 500. The existing cycle and depth refusal is unchanged.

v1.24.0

A seal precondition against a late or duplicated request. The review-submission seal route now accepts an optional-but-often-required field that lets a caller tell the server what it already knew, so a stale or repeated seal attempt is refused instead of creating a second sealed submission.
  • expected_latest_submission_id on POST /v1/policies/{id}/versions/{version}/review-submissions. Send the id of the latest sealed submission of this policy version that you read before deciding to seal (the first item returned by GET /v1/policies/{id}/versions/{version}/review-submissions), or null when that list was empty. The server compares it, inside the seal’s own locks, with its own record of the version’s latest submission.
  • A mismatch is refused, and nothing is written. A disagreement is 409 STALE_LATEST_SUBMISSION, with expected_latest_submission_id in details. No submission is created. details.latest_is_this_package is also included: it tells you whether the latest sealed submission — the one that won — holds the same package this request would have sealed, so you can tell an intended-but-overtaken seal from a genuinely different one. A legitimate further seal (after a return, or a deliberate second package) simply names whichever submission is latest at that point and seals as usual.
  • A safe re-send. Because the field is compared under the same locks the seal itself takes, two requests carrying the same value can never both succeed — the first to commit moves the latest, and the second is refused. So re-sending the exact same request after a timeout or a dropped response, without re-reading anything first, cannot seal the same package twice.
  • Required for capability-admitted requests. When the request was admitted by an authoring capability, the field is required: omitting it is refused 400 EXPECTED_LATEST_SUBMISSION_REQUIRED. For every other caller it is optional — checked when sent, and the route behaves exactly as before when it is not.
  • What this does not cover. This precondition is about the latest submission, not the draft’s rule content — that is what the existing optional expected_draft_revision field, and its own 409 STALE_DRAFT_REVISION refusal, continues to guard. Sending both narrows what a seal can silently overtake; sending neither leaves the route’s pre-1.24.0 behavior unchanged.

v1.23.0

A server-side bound on authoring writes. Four write routes now run under a server-side time bound, and a write that cannot finish inside it is rolled back and reported, rather than left to run indefinitely behind a lock.
  • Affected routes: PATCH /v1/policies/{id}/versions/{version}, POST /v1/policies/{id}/versions/{version}/review-submissions, POST /v1/policies/{id}/versions/{version}/supersede-assigned, and POST /v1/review-submissions/{submissionId}/withdrawal-requests.
  • The bound. Each of these routes now waits at most 2 seconds for any database lock it needs, and the server abandons the write if it is not ready to commit within 4 seconds of the server beginning to handle the request. A write that hits either limit 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.
  • What this does not mean. A 503 AUTHORING_WRITE_CONTENDED is the only response that tells a caller its write did not commit. A client-side timeout on any of these routes does not: the bound above is measured from when the server began handling the request, which a client cannot observe, and nothing bounds how long a request takes to reach the server. If a request to one of these routes times out on the client, the client cannot conclude — from the timeout alone — that the write did or did not commit.
  • Scope. Only these four write routes carry the bound. Other write routes are unaffected by this change.

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 is computed the same way the review-submission seal computes its own active_rules_hash, so the two values can be compared directly.
  • 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.21.0

The capability-scoped pending-work read. A read release: an authoring-capability key can now read its own pending work directly, without a human session or a broader key.
  • New: GET /v1/authoring-capabilities/current/pending-work. For an API key holding only the authoring:capability scope, whose capability grants both policy.read and review.retrieve, this returns:
    • items: the GET /v1/work/pending items (the same item schema) for the governing grant’s policy version, plus — only when a grant of the same capability chain, on the same policy, names it — the items of at most one earlier draft: the lineage-previous version the head’s next seal is bound to.
    • next_seal_answers (optional): the one return the next seal of this version must name as answers_record_id. Present only while the version is draft or submitted.
    • returns_not_answerable: unanswered returns in view that no seal can currently answer, each with a reason of not_lineage_previous or head_not_sealable. Informational, not a task list.
    • capability_id, root_capability_id, policy_id, policy_version_id, policy_version_number, and observed_at (server time at the start of the read — the response is not a snapshot).
    • Optional flags lineage_unresolved (same meaning as on the review-submission list read) and lineage_not_backed_by_grant (new: the lineage-previous version is not named by any grant of the chain, so its items are not listed; a seal is not refused for this reason). Both flags can be present together.
    • Returns carry identifiers only: returned_by is the stable user:<uuid>, never the returner’s display name.
    • The request takes no path, body or query parameters — the scope comes only from the key’s signed grant chain.
    • Refusals reuse existing gate codes: 403 FORBIDDEN for a broad key or a human session; the gate’s 403 AUTHORING_CAPABILITY_* codes (missing, revoked or exhausted anywhere in the chain, expired, not yet valid, integrity); AUTHORING_CAPABILITY_OUT_OF_SCOPE when either action is missing; AUTHORING_CAPABILITY_OBJECT_MISMATCH for a key holding policies:* beside the carrier scope, or when the capability was carried while the read ran.
    • Not in the agent-openapi (ChatGPT Actions) subset.
  • Known limit, unchanged: no capability read joins the agent licence — it is joined only at issuance and at carry — so a revoked licence under enforce still admits all five capability reads, with one verdict.

v1.20.0

Capability-bound draft supersession; the capability carry. A writer release: correcting a capability-bound draft now mints a new authoring capability grant for the correction, and a chain of such grants can be followed and ended.
  • New: POST /v1/policies/{id}/versions/{version}/supersede-assigned. An API key holding only the authoring:capability scope, whose capability grants draft.edit and is bound to exactly this draft, corrects the draft by superseding it. In one transaction: the draft is closed replaced, byte-identical; the corrected rules become a new draft (codification is required on this route); and a new authoring capability grant is minted for the new draft, naming the admitting grant in succeeds_capability_id. Every other field on the new grant (key, policy, granted_by, grant_authority, actions, source items, window, agent declaration) is copied from the admitting grant.
    • Body: expected_revision, rules, codification, change_reason?, authoring_capability_id (must match the admitting capability), additionalProperties: false.
    • Response 200 { replaced, draft, authoring_capability }, where authoring_capability is the new grant plus its derived root_capability_id. Callers should name the new grant in authoring_capability_id from then on.
    • Refusals: 400 validation; 403 AUTHORING_CAPABILITY_ID_MISMATCH; the existing AUTHORING_CAPABILITY_* gate codes (re-checked under the row lock), including AUTHORING_CAPABILITY_SUBJECT_NOT_LICENSED, _SUBJECT_REVOKED and _SUBJECT_EXPIRED; 404; 409 INVALID_TRANSITION, STALE_DRAFT_REVISION, DRAFT_CONFLICT, and new 409 AUTHORING_CAPABILITY_CHAIN_LIMIT — a capability a human issued may be carried at most 20 times; 422; 500. Nothing is written on any refusal.
    • Not in the agent-openapi (ChatGPT Actions) subset.
  • New: GET /v1/authoring-capabilities/current. Carrier scope only, action policy.read. Returns the grant that currently governs the calling key, without its signature, plus root_capability_id — so a caller whose supersession response was lost can learn which grant to name next.
  • Revoke: new 409 AUTHORING_CAPABILITY_CHAIN_ENDED. Revoking any grant in a chain ends that grant and every grant carried from it, so a grant that was itself carried on may still be revoked. A grant whose own ending already exists still gets AUTHORING_CAPABILITY_ALREADY_REVOKED; a grant with no ending of its own but an ended ancestor now gets CHAIN_ENDED, with details.ending_record_id, ending_record_type and ended_grant_id.
  • PATCH and the withdrawal request re-check the capability under locks. A capability-admitted PATCH /v1/policies/{id}/versions/{version} and POST /v1/review-submissions/{submissionId}/withdrawal-requests now lock the policy row, then the version row, and re-resolve the admitting capability in their own transaction. If it was revoked or exhausted anywhere in its chain (including a revocation still in flight when the request arrived), expired, or no longer governs because a supersession carried it, the write is refused with the same gate 403 code and nothing is written. Before this release, a write admitted a moment before a revocation could still land after it. Human and broad-key PATCHes are unchanged.
  • Seals take the policy row lock before the version row lock. A seal racing a supersession of the same draft no longer risks a 500 from a deadlock — it waits and then answers normally. A capability seal also waits for an in-flight revocation of its root, and is refused AUTHORING_CAPABILITY_REVOKED rather than succeeding after it.
  • BREAKING — the audit action enum widens. New action authoring_capability_carried is a closed-enum widening on GET /v1/audit-log, GET /v1/audit-log/entity/{type}/{id} and GET /v1/admin/audit-log. An exhaustive client that switches over the audit action enum must be updated to handle it.
  • policy_versions.closed_by may now hold a key’s display identity (the key that superseded the draft), not only a human’s.

v1.19.0

Review loop across draft supersession; capability chain reader. A reader release: it mints no successor capability and adds no route, and changes these existing behaviours instead.
  • BREAKING — GET /v1/work/pending: status gains replaced. The closed union widens, and an exhaustive client must handle the new value. A replaced version’s pending review is listed while a successor links to it, the lineage head (at most 64 corrections ahead) is draft or submitted, and no later version in its lineage has a seal. Such items carry the new superseded_by_version (the head’s number).
  • The review gate is once on, always on. Once any seal in a version’s lineage was made under an authoring capability, ratify needs an ACCEPT on the head’s own latest seal, whoever made it — a human or broad-key re-seal no longer lifts the gate. RATIFY_NO_ACCEPT_RECORD gains details.reason (NO_SEAL_ON_HEAD, NO_ACCEPT_RECORD or LINEAGE_UNRESOLVED) and details.gate_submission_id / gate_policy_version_id.
  • Every seal is refused on a version that is not draft or submitted. New 409 INVALID_TRANSITION with details.reason: VERSION_NOT_SEALABLE, checked under the version row lock. Before, the seal route never read the status.
  • GET /v1/review-submissions/{submissionId} gains two optional, never-blocking observations, each omitted when empty:
    • newer_draft_observed compares the sealed codes and active_rules_hash with the lineage head.
    • unanswered_lineage_returns lists returns on an earlier draft that no later seal has answered. The same field (derived for the version) is added to GET /v1/policies/{id}/versions/{version}/review-submissions.
  • Not previously changelogged (#1188): POST /v1/review-submissions/{submissionId}/withdrawal-requests refuses a key that holds policies:read or policies:write beside the carrier scope, with 403 AUTHORING_CAPABILITY_OBJECT_MISMATCH.
Operation names, codes and fields above are taken from the regenerated api/openapi.json for this release and from the API’s own changelog (apps/meshqu-api/CHANGELOG.md, MCP-020 R1) in tradequ.

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.
Operation names, codes and fields above are taken from the regenerated api/openapi.json for this release (postV1PoliciesByIdVersionsByVersionReopen, postV1PoliciesByIdVersionsByVersionSetAside, postV1PoliciesByIdVersionsByVersionSupersede).

v1.17.0

Pending work for a tenant
  • New GET /v1/work/pending (scope policies:read). Lists the tenant’s pending governance work, oldest waiting first. It performs no act; people and API keys holding the scope can read it, and a key holding only an authoring capability cannot.
  • review items: versions whose latest sealed review submission has no review record yet (requested act review) or whose newest record is an acceptance (requested act ratify), plus versions submitted for maker-checker approval with no sealed submission (requested act ratify). Returned, correct-and-resubmit and stop outcomes are not listed.
  • activation items: ratified versions that are not in force and are newer than the version in force, or where nothing is in force (requested act activate). Archived policies are not listed.
  • Each item names the policy and version, the version’s status, the requested act, and when it entered that state together with the stored field that time came from (entered_state_source). There is no field for who acts next: nothing in the data names one.
  • Paged with limit (1–100, default 50) and offset.
Field names and values above are taken from the regenerated api/openapi.json for this release (operation getV1WorkPending, GET /v1/work/pending).

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 codification lint (an internal, model-assisted check comparing a declared clause’s text against the codifier’s stored rule); 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.
  • This is a projection, not a mirror, of what is stored internally: it does not include the model’s raw per-question answers (only the derived findings), and it does not include token-usage accounting. A finding’s evidence is likewise limited to the one diagnostic value a reader needs, not the full internal diagnostic bag.
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.
Field names, error codes and descriptions above are taken directly from the regenerated api/openapi.json for this release (operations postV1PoliciesByIdVersionsByVersionActivate and postV1PoliciesByIdVersionsByVersionRatify). This entry does not change concepts/receipt-reference.mdx or any guide — see PAL-007.

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 is not a sealed review package. A review package proves byte equality under meshqu-review-package/v1 and binds source custody; this proves neither. Where a version has both, the review package is the stronger artefact.
  • As in 1.13.0, it does not establish that the declared clauses are the whole document. Nothing server-side reads the document.

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
  • components.schemas now defines def-0, the recursive when gate union. The spec had referenced it 32 times without defining it, affecting nine operations including POST /v1/policies and POST /v1/policies/validate. Anything that resolves references — an OpenAPI validator, a client generator, an agent importing the schema — previously had nothing to resolve it to. The generator now hoists recursive definitions and refuses to emit a document containing an unresolvable reference.

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 — an unrecognised operator key, no subject field, or no executable constraint — is now rejected at the write boundary with VALIDATION_ERROR on draft update and on ratify, and produces UNKNOWN_CONDITION_SHAPE (400) at evaluation time.
  • Previously such a condition was skipped and counted as a rule that had passed. A threshold rule written { field, operator: "gt", value: 150 } — not the canonical { field, min | max | greater_than | less_than } — permitted any amount while reporting rules_evaluated: 1, violations: 0, decision: "ALLOW".
  • 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: the signed /v1/.well-known/trust-registry endpoint and the matching signing-keys deprecation notice, 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
  • Violations denormalized into queryable table at write time (analytics projection, not source of truth).
  • 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.