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.
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/invitemade with an API key. Before, it answered 201.PATCH /v1/admin/members/{user_id}made with an API key. Before, it answered 200.
- Two refusals, in order. An API key without
members:writeis still refused 403FORBIDDENfirst. A credential that holdsmembers:writebut is not a human session is then refused 403HUMAN_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 403FORBIDDEN. - What did not change.
DELETE /v1/admin/members/{user_id}, which removes a member, andGET /v1/admin/memberscan 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/evaluateorPOST /v1/decisions/recordwith a top-level key other than the ones listed under “Decisions” below (400). - A body to
POST /v1/policies/{id}/versions/{version}/ratifywith a key other thanexpected_revision,approval_authorityandratifier_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_RESUBMITorSTOP(409).
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/evaluateacceptscontext(required),options,actionandsource.POST /v1/decisions/recordacceptscontext(required),options,action,actor,evidence_manifest_digest,evidence_manifestandsource. Any other top-level key is refused 400FST_ERR_VALIDATION, including a key that only the other route takes, such asactoron evaluate. The refused body reaches neither the policy read nor the decision write. Before, an unknown top-level key was accepted and ignored. sourceis accepted and not read. Both bodies now declare a top-levelsource, 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
contextoroptionsis still accepted on both routes. The reference declares noadditionalPropertiesforcontext,options,actionoractor, andcontext.fields,context.metadataandaction.metadataaccept any key. - The form API contract. In
GET /v1/forms/{formId}/api-contract, the evaluate request schema inopenapi_snippetis now closed (additionalProperties: false) and declaresoptionsandactionbeside the requiredcontext. It does not listsource, which the endpoint accepts. Before, it declaredcontextonly and did not close the schema.
- The body is closed. A body with any key other than
expected_revision,approval_authorityandratifier_roleis refused 400FST_ERR_VALIDATION, and nothing is ratified. Before, an unknown key was accepted and ignored.expected_revisionis still required. A request authenticated by an API key is still refused 403HUMAN_PRINCIPAL_REQUIREDbefore the body is checked, and a key withoutpolicies:writestill gets 403FORBIDDENfirst. - 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_hashthat 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 409REVIEWED_SUBJECT_STALE, withdetails.submission_id,details.review_record_id,details.sealed_active_rules_hash,details.current_active_rules_hash,details.candidate_revisionanddetails.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_RESUBMITorSTOP, the answer is 409RATIFY_REVIEW_ADVERSE, withdetails.reasonset to that disposition anddetails.submission_idanddetails.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 409RATIFY_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_codeorrule_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_revisionis stale is still refused 409STALE_DRAFT_REVISIONfirst.
- Only the hashed fields of the active rules (
- The
policy_ratifiedaudit entry cites the ACCEPT. When a ratification relied on an ACCEPT, the entry’schangescarriesreview_record_idandreview_submission_id, whether or not an authoring capability was involved. Before, only a ratification under the review gate carried them.
connection_consentedis a new auditactionvalue. No operation in this reference records it, so the reference does not say when it appears.- Where it can appear. As
actionon the events returned byGET /v1/audit-log,GET /v1/admin/audit-logandGET /v1/audit-log/entity/{type}/{id}. - As a filter.
GET /v1/audit-logaccepts it as theactionquery parameter. Before, it was refused like any other value that is not a listed audit action: 400FST_ERR_VALIDATION.
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 403HUMAN_PRINCIPAL_REQUIRED(after thepolicies:writescope check), and a tenant admin may discard a draft that is not theirs in three cases, each with areason. 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-keysandPOST /v1/admin/api-keys. Thescopesandroledescriptions now state the refusals 1.26.0 introduced (API_KEY_SCOPE_ESCALATIONandAPI_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,scopessaid only that it defaulted to “read + evaluate”, androlehad 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.canDiscardonGET /v1/policies/{id},INVALID_PARENTonPOST /v1/admin/tenants, or the duplicateunit_idrefusal on thecodificationblock.
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 acodification 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}andDELETE /v1/admin/api-keys/{id}answer 409API_KEY_ALREADY_REVOKED, withdetails.revoked_atgiving 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 404NOT_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-keysandPOST /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 indetails.exceeding_scopes. - The requested
rolemust be one the calling key’s role may create, otherwise 403API_KEY_ROLE_ESCALATION, withdetails.requested_roleanddetails.minter_role.adminmay create any role; every role may create itself andviewer;operatorandauditormay not create each other. - An omitted
scopesorroleis 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.
- Every requested scope must be one the calling key holds, otherwise
403
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, matchespolicy_idandpolicy_version_number, and is not ratified and in force, or its policy is archived, the answer is 409AGENT_LICENCE_POLICY_VERSION_NOT_IN_FORCE. It used to be a 500. Nothing is issued. Apolicy_version_idthat 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. Avalid_fromafter now, for a key whose governing licence is in force now, is refused 409AGENT_LICENCE_FUTURE_START_WHILE_LIVE, withdetails.live_licence_id,details.live_valid_untilanddetails.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_FAILEDor 500AGENT_LICENCE_READBACK_FAILED, withdetails.committedset totrue. The record exists:details.licence_idordetails.capability_idis its id. On the revoke routes that id is the revocation record, anddetails.revokes_licence_idordetails.revokes_capability_idnames 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_FOUNDor 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_idand the version id (policy_version_idorsource_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-casepolicy_idonPOST /v1/authoring-capabilitieswas 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.
- Discarding a draft is a human act.
DELETE /v1/policies/{id}/versions/{version}now refuses a request authenticated by an API key with 403HUMAN_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 withoutpolicies:writestill gets 403FORBIDDEN. A verified human session reaches the route as before.HUMAN_PRINCIPAL_REQUIREDis 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 }(400DISCARD_REASON_REQUIREDwithout 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.canDiscardisfalseandactionsdoes not includediscard.- Ratify refuses a version whose policy is archived.
POST /v1/policies/{id}/versions/{version}/ratifyanswers 409INVALID_TRANSITIONwithdetails.reasonPOLICY_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
codificationblock that declares oneunit_idon two clauses is refused. OnPOST /v1/policies,POST /v1/policies/{id}/versions,POST /v1/policies/{id}/versions/{version}/supersedeandPOST /v1/policies/{id}/versions/{version}/supersede-assigned, acodification.clauseslist with the sameunit_idon more than one clause answers 422CODIFICATION_REFUSEDwithdetails.codeDUPLICATE_UNIT_ID. Nothing is stored. Before, the later clause silently replaced the earlier in the lookup, so one outcome accounted for both.unit_idvalues are compared exactly, sou-1andU-1are two clauses. A version already stored with a duplicate is not rewritten.
POST /v1/admin/tenants: aparent_tenant_idthat names no tenant. A well-formedparent_tenant_idthat matches no tenant answers 422INVALID_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_idonPOST /v1/policies/{id}/versions/{version}/review-submissions. Send theidof the latest sealed submission of this policy version that you read before deciding to seal (the first item returned byGET /v1/policies/{id}/versions/{version}/review-submissions), ornullwhen 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, withexpected_latest_submission_idindetails. No submission is created.details.latest_is_this_packageis 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_revisionfield, and its own409 STALE_DRAFT_REVISIONrefusal, 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, andPOST /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, includingdetails.committed: false, a reason, and aRetry-Afterheader. This response is a definite non-commit and is safe to retry. - What this does not mean. A 503
AUTHORING_WRITE_CONTENDEDis 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_hashis added toGET /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 ownactive_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 theauthoring:capabilityscope, whose capability grants bothpolicy.readandreview.retrieve, this returns:items: theGET /v1/work/pendingitems (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 asanswers_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 areasonofnot_lineage_previousorhead_not_sealable. Informational, not a task list.capability_id,root_capability_id,policy_id,policy_version_id,policy_version_number, andobserved_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) andlineage_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_byis the stableuser:<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
FORBIDDENfor a broad key or a human session; the gate’s 403AUTHORING_CAPABILITY_*codes (missing, revoked or exhausted anywhere in the chain, expired, not yet valid, integrity);AUTHORING_CAPABILITY_OUT_OF_SCOPEwhen either action is missing;AUTHORING_CAPABILITY_OBJECT_MISMATCHfor a key holdingpolicies:*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
enforcestill 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 theauthoring:capabilityscope, whose capability grantsdraft.editand is bound to exactly this draft, corrects the draft by superseding it. In one transaction: the draft is closedreplaced, byte-identical; the corrected rules become a new draft (codificationis required on this route); and a new authoring capability grant is minted for the new draft, naming the admitting grant insucceeds_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 }, whereauthoring_capabilityis the new grant plus its derivedroot_capability_id. Callers should name the new grant inauthoring_capability_idfrom then on. - Refusals: 400 validation; 403
AUTHORING_CAPABILITY_ID_MISMATCH; the existingAUTHORING_CAPABILITY_*gate codes (re-checked under the row lock), includingAUTHORING_CAPABILITY_SUBJECT_NOT_LICENSED,_SUBJECT_REVOKEDand_SUBJECT_EXPIRED; 404; 409INVALID_TRANSITION,STALE_DRAFT_REVISION,DRAFT_CONFLICT, and new 409AUTHORING_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.
- Body:
- New:
GET /v1/authoring-capabilities/current. Carrier scope only, actionpolicy.read. Returns the grant that currently governs the calling key, without its signature, plusroot_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 getsAUTHORING_CAPABILITY_ALREADY_REVOKED; a grant with no ending of its own but an ended ancestor now getsCHAIN_ENDED, withdetails.ending_record_id,ending_record_typeandended_grant_id. - PATCH and the withdrawal request re-check the capability under locks. A
capability-admitted
PATCH /v1/policies/{id}/versions/{version}andPOST /v1/review-submissions/{submissionId}/withdrawal-requestsnow 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_REVOKEDrather than succeeding after it. - BREAKING — the audit action enum widens. New action
authoring_capability_carriedis a closed-enum widening onGET /v1/audit-log,GET /v1/audit-log/entity/{type}/{id}andGET /v1/admin/audit-log. An exhaustive client that switches over the audit action enum must be updated to handle it. policy_versions.closed_bymay 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:statusgainsreplaced. 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 newsuperseded_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_RECORDgainsdetails.reason(NO_SEAL_ON_HEAD,NO_ACCEPT_RECORDorLINEAGE_UNRESOLVED) anddetails.gate_submission_id/gate_policy_version_id. - Every seal is refused on a version that is not draft or submitted. New
409
INVALID_TRANSITIONwithdetails.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_observedcompares the sealed codes andactive_rules_hashwith the lineage head.unanswered_lineage_returnslists returns on an earlier draft that no later seal has answered. The same field (derived for the version) is added toGET /v1/policies/{id}/versions/{version}/review-submissions.
- Not previously changelogged (#1188):
POST /v1/review-submissions/{submissionId}/withdrawal-requestsrefuses a key that holdspolicies:readorpolicies:writebeside the carrier scope, with 403AUTHORING_CAPABILITY_OBJECT_MISMATCH.
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}/versionsrefuses a second pending version with409 DRAFT_CONFLICT.details.reasonisOPEN_DRAFT,RATIFIED_AWAITING_ACTIVATIONorPENDING_VERSION_EXISTS, anddetails.pending_versionnames the pending version when it is known.
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 asreopenedand can no longer be activated.POST /v1/policies/{id}/versions/{version}/set-aside: the pending version is closed asset_asidewith a requiredreason, 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 asreplacedand kept unchanged, including its codification record, and the corrected content becomes a new draft. It takesexpected_revision,rulesand an optionalcodificationblock processed exactly as on version creation.
- Version
statusmay now also bereopened,set_asideorreplaced. A closed version cannot be ratified or activated.supersededkeeps its existing meaning: a version that was in force and has been replaced. - Closed versions carry
closed_at,closed_byandclosed_reason. - New audit actions:
policy_version_reopened,policy_version_set_aside,policy_version_replaced.
api/openapi.json for this release (postV1PoliciesByIdVersionsByVersionReopen, postV1PoliciesByIdVersionsByVersionSetAside, postV1PoliciesByIdVersionsByVersionSupersede).
v1.17.0
Pending work for a tenant- New
GET /v1/work/pending(scopepolicies: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. reviewitems: versions whose latest sealed review submission has no review record yet (requested actreview) or whose newest record is an acceptance (requested actratify), plus versions submitted for maker-checker approval with no sealed submission (requested actratify). Returned, correct-and-resubmit and stop outcomes are not listed.activationitems: ratified versions that are not in force and are newer than the version in force, or where nothing is in force (requested actactivate). 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) andoffset.
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 aneeds_contextflag.status: "ran"with an emptyfindingsarray for a clause is a real statement — the lint ran and found nothing — distinct from an absentlint, 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
evidenceis likewise limited to the one diagnostic value a reader needs, not the full internal diagnostic bag.
- It is advisory, produced by an automated, model-assisted check. It is unsigned and unhashed — not covered by
policy_rules_hashorpolicy_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.ledgerorcodification_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 immutablepolicy_version_activationreceipt 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_FOUNDwhen the policy or version does not exist in this tenant;409 INVALID_TRANSITIONwithdetails.reasonofPOLICY_ARCHIVED,VERSION_ALREADY_IN_FORCE,VERSION_NOT_RATIFIED, orVERSION_NOT_PENDING;500 RATIFIER_KEY_REQUIREDwhen no ratifier key is configured. Nothing is written on a refusal. - New audit action
policy_version_activated.
POST /v1/policies/{id}/versions/{version}/ratify’s description is corrected: ratification records that a human approved the exact draft version, and returns a signedPolicyApprovalReceiptwhen 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.
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,countsandrefusalsare the write boundary’s own output, not values the caller supplies. Acodification_recordsent in a request body is ignored.
- It is unsigned and unhashed. It is not covered by
policy_rules_hashorpolicy_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/v1and 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 assertedPOST /v1/policiesandPOST /v1/policies/{id}/versionsaccept an optionalcodificationblock: 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_refis derived from the clause it is mapped to, and anysource_refsent in the request is ignored. A rule that names no clause cannot be stored through this path. - Refusals return
422 CODIFICATION_REFUSEDwith the specific reason indetails.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.
- 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 fromPOST /v1/decisions/evaluateandPOST /v1/decisions/recordmay now returnrule_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.resultis the signed object and itsintegrity_hashcovers the violations exactly as the evaluator produced them, so a caller joinsrule_sourcestoviolations[].rule_codeorna_rules[].rule_codeitself. Existing clients that ignore the field see unchanged bodies. - What it is evidence of, stated precisely:
source_refis author-supplied and is excluded frompolicy_rules_hash, so ratification does not attest to it. It is covered bypolicy_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.
components.schemasnow definesdef-0, the recursivewhengate union. The spec had referenced it 32 times without defining it, affecting nine operations includingPOST /v1/policiesandPOST /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 passPOST /v1/decisions/evaluateandPOST /v1/decisions/recordmay now return an optionalgovernance_coverageobject when no applicable rule was evaluated. It carriespermission_established: false, saying explicitly that anALLOWover 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_versionchanged, 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.
coveragedistinguishesNO_ACTIVE_RULESfromNO_APPLICABLE_RULES.cryptographically_establishedistrueonly forNO_ACTIVE_RULES, and only whenevaluated_rules_hashequals 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/evaluatecontinues to answer404 NO_ACTIVE_RULESwhen 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 theirwhengates.
- 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_ERRORon draft update and on ratify, and producesUNKNOWN_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 reportingrules_evaluated: 1, violations: 0, decision: "ALLOW". - A boolean temporal operator set to
false(not_expired,not_future,must_be_before,must_be_after) andmin_length: 0are likewise refused: the evaluator reads none of them, so they asked nothing. Numeric bounds of0are unaffected and remain executable —{ field, max: 0 }still refuses any positive value. - Rules already stored are unaffected until they are next edited or ratified.
PATCH /v1/policies/{id}/versions/{version}now answers409 NOT_DRAFTfor a submitted or ratified version even when the submitted rules are also malformed. It previously answered422in 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_beforeandmust_be_after. All three were already executed by the evaluator and missing from the published schema. - Policy list endpoints document the
qsearch parameter, and policy version responses documentdraft_revision. Both shipped earlier and had not been regenerated into the spec.
v1.10.0
Case-insensitive policywhen gates
whengates withequalsorinaccept an opt-incase_sensitive: false, which folds string-vs-string comparisons only. Booleans and numbers always compare strictly, and gates never coerce types — a booleantrueis 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 tofalse): gates match ratified domains, list conditions match free text.
- The published spec now also reflects earlier shipped work it had lagged behind: the signed
/v1/.well-known/trust-registryendpoint and the matchingsigning-keysdeprecation notice, boolean members inwhen.inlists, and admin member invitations by email withactor_emailin audit views.
v1.9.0
Tenant Governance Settings- New
GET /v1/settingsandPATCH /v1/settingsendpoints 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/submitreturns a decision receipt; retrieve later viaGET /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 matchesevidence_manifest_digest) andapproval_lineage(per-version approval-receipt digest, ratifier signature andpolicy_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 AttributionPOST /v1/decisions/recordnow accepts an optional top-levelactorobject:id,type,role,authority,display_name.actor.idis included in the integrity hash and cryptographically bound to every receipt.- Other actor fields are stored for audit display.
- Backward compatible:
actoris optional. If omitted andcontext.metadata.actor_idis present, the API falls back to that value. Whenactor.idis supplied, it takes precedence. - New
actoroptional parameter onclient.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_codefield — 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
ReasonCodetype exported from@meshqu/typesand re-exported from@meshqu/client. - Backwards compatible:
reason_codeis a new required field on violations. Existingreasonstrings are unchanged. - Replay note: New receipts have different integrity hashes than pre-
reason_codereceipts due to the additional field. Pre-migration receipts remain verifiable against their stored hash.
- New
POST /v1/decisions/:id/outcomeendpoint 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/:idresponse. - New
recordOutcome()method in the TypeScript SDK.
- Violations denormalized into queryable table at write time (analytics projection, not source of truth).
GET /v1/decisionsnow acceptscorrelation_idquery parameter.- Policy rules accept optional
replaces_rule_codefor advisory lineage tracking.
v1.2.0
Conditional Rule Applicability (When Gating)- Added optional
whenclause 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) andna_rules(details). - Supported
whenoperators:equals,in,exists, withall/anycombinators for nested logic. - Snapshot hash includes
whenclauses (backward compatible — existing hashes unchanged).
- Decision receipts are now signed with Ed25519 when a signing key is configured.
- Receipts include
signature,signature_kid, andsignature_algorithmfields. - Auditors can verify receipts using the public key alone — no shared secret needed.
- 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
apiKeyconfiguration is now required (throws a clear error if missing). idempotency_keyis now required when recording decisions (no longer auto-generated).- Added
dryRun()as a semantic alias forevaluate(). - Recorded decisions now capture audit attribution (
api_key_id,api_key_name). - Backend infrastructure failures now return
503 Service Unavailablewithretry_afterinstead of500. - 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.