Module references
Warrant — authority, lifecycle & gated change
Warrant is the module that answers who vouches for this, and who is allowed to change it. Adopt it alone if you already have concepts or documents somewhere and you need a review queue, an authority tier, an owner, and a trail you can defend.
What it does
Warrant tracks four things about a concept: its authority tier (machine_extracted → asserted → curated → certified), its lifecycle (draft, pending_review, published, deprecated, superseded), its owner and stewards, and its review clock. Destructive or schema-level edits do not happen in place: they are proposed as change requests that a reviewer approves, rejects, or that the proposer withdraws. Every accepted change appends an immutable revision carrying a full snapshot and a field-level diff. Documents get a parallel, simpler treatment — a verification stamp with a re-verify due date.
Nothing in this module calls a model. All of it is transactional database work inside a workspace transaction.
Use it standalone
Issue a key with the scopes Warrant actually checks
Reads (the change-request queue, the revision trail) require
kb:read. Every mutation — propose, approve, reject, withdraw, certify, review, set authority, set owner, deprecate, verify a document — requireskb:write. There is nogovernance:*scope; the scope taxonomy isagents:run,runs:read,kb:read,kb:write,data:read,data:run,reports:read,reports:write.Delegated user keys (
eli_uk_…) are additionally narrowed by the owner's live workspace role: these routes name the capabilitygovernance:readorgovernance:write, and a 403insufficient_capabilitycomes back if the owner has lost it. Workspace keys (eli_sk_…) carry no role, so the capability check does not apply to them.Install two packages and import one entry
Headless:
@eli-ai/client/governance. React:@eli-ai/react/governance. Neither pulls in query, agents, data, reports, connectors or evals code.Call it
The client is a thin typed wrapper over
/api/v1. The transport takes an origin, a credential resolver and an optional workspace id; it makes no authorization decision of its own.
What Warrant does NOT require
update, deprecate, supersede or delete must name an entity that is live in the workspace (404 otherwise), and POST /documents/{id}/verify needs a live document. Kind create is the one path that needs no pre-existing row — it proposes the concept and creates it on approval.React components
The React package exports exactly one component for this module, plus its two types. EliGovernance is a client component ("use client") that renders the open change-request queue and, optionally, a proposal form. It uses scoped eli-* class names and ships no styling of its own — import @eli-ai/react/styles.css or provide your own rules for those classes.
EliGovernance
What it renders, top to bottom:
- a titled surface with a Propose change / Cancel toggle button (only when
canPropose); - when toggled open, a form with a kind select (
create,update,deprecate,supersede,delete), an Entity ID input (required for every kind exceptcreate), a Review note textarea, and a Proposed fields (JSON) textarea that must parse to a JSON object — an array, a scalar or invalid JSON is rejected inline before any request is sent; - one card per open request: a kind badge (critical tone for
delete, warning otherwise), the entity name — falling back to the entity id, then to "New concept" — the created date, the request note, the names of the keys inpayload, and the proposer; - when
canReview, a per-request note input plus Approve and Reject buttons; a non-empty note is sent as the request body, an empty one is omitted; - loading, error (with a retry button) and empty states.
It calls four operations only: listChangeRequests({ status: "open" }) on mount and after every successful mutation, proposeChangeRequest, approveChangeRequest and rejectChangeRequest. The status filter is fixed to open and is not a prop.
EliGovernance props
emptyLabel is deliberately absent
EliGovernanceProps is Omit<CommonSurfaceProps, "emptyLabel"> — the empty state is driven by labels.empty instead. Passing emptyLabel is a type error.EliGovernanceLabels — every key, with its default
Never ship a workspace key to the browser
eli_sk_ keys are workspace-wide. EliProvider takes a transport, not a credential, precisely so the key stays server-side — see the SDK page for the proxy pattern.What the React surface does not cover yet
EliGovernance has no UI for withdraw, for certify / review / set authority / set owner / deprecate, for the revision trail, or for document verification, and it cannot list anything other than open requests. All of those are available on the headless client (createGovernanceClient) and over HTTP — build your own markup with useEliQuery / useEliMutation from @eli-ai/react/hooks if you need them in React.HTTP API
Twelve operations. Every one authenticates with a Bearer key; the scope column below is what the route enforces, and delegated user keys additionally need the named capability. All examples use https://eli.ai with -L on non-GET calls, because the apex issues a 308 redirect that curl will not follow for a POST without it.
Change requests
/api/v1/change-requestsBearer · kb:read · governance:readThe change-request queue, newest first (created_at desc, then id desc). Rows are joined to the target entity so entityName comes back with them.
Query parameters
/api/v1/change-requestsBearer · kb:write · governance:writePropose a change. Returns 201 with the created request. The payload is validated strictly per kind, so an unknown field is a 400 rather than a silent drop.
Request body
/api/v1/change-requests/{id}/approveBearer · kb:write · governance:writeApprove and apply. The response carries both the closed request and the entity the change landed on — for kind create that id did not exist before the call.
Request body
The review-note field is `note`
{ note?: string }. Body validation strips unknown keys, so sending reviewNote is accepted with a 200 and the note is silently discarded. Send note; read it back as reviewNote on the request object.Who may approve, and what a 409 means here
If the target entity has an ownerId or a non-empty stewardIds, only those identities may approve — anyone else gets 409 conflict with "Not a reviewer". With neither set, any caller who passes the scope check may approve.
The acting identity is the user id for a delegated eli_uk_ key, and the API key id for a shared eli_sk_ workspace key. A workspace key therefore can never match an owner or steward: owned concepts must be approved with a user key belonging to a reviewer, or through the workspace UI.
A 409 also comes back when the request is no longer open — first reviewer wins, and a double apply is impossible.
/api/v1/change-requests/{id}/rejectBearer · kb:write · governance:writeClose an open request as rejected without applying it. 409 when the request is not open, 404 when it does not exist.
Request body
/api/v1/change-requests/{id}/withdrawBearer · kb:write · governance:writeClose an open request as withdrawn. Takes no request body. 409 when the request is not open.
What approval actually applies
create inserts the concept at authority curated and lifecycle published (or draft when the payload asks for it); a name collision with a live concept is a 409. update writes the fields and promotes authority to at least curated in one transaction — a promotion only, never a demotion of a certified concept. deprecate and supersede route through the same code as the deprecate endpoint below, and delete soft-deletes. Every one of them records the revision with activity change_request:<id>, which is how you trace an edit back to its proposal.Concept authority & lifecycle
These five mutations all return the same CurationMeta object and all append a revision. They are the direct path — use them for the states a change request does not cover (authority, owner, review stamps), and use change requests for edits you want reviewed.
/api/v1/entities/{id}/certifyBearer · kb:write · governance:writeOwner/admin sign-off. Sets authority to 'certified', refreshes lastReviewedAt and nextReviewAt (now + the workspace reviewIntervalDays, default 180), bumps version, and writes a revision with changeKind 'certify'. Lifecycle is untouched.
Request body
/api/v1/entities/{id}/reviewBearer · kb:write · governance:writeStamp a review without certifying. Refreshes lastReviewedAt and nextReviewAt; a machine_extracted concept is promoted to 'curated', higher tiers are left alone. Takes no request body.
/api/v1/entities/{id}/authorityBearer · kb:write · governance:writeSet the authority tier explicitly, including a demotion. Setting the tier it already has is an idempotent no-op: no version bump, no revision, no audit row.
Request body
/api/v1/entities/{id}/ownerBearer · kb:write · governance:writeAssign or clear the owner and optionally replace the whole steward list. This is what arms the reviewer gate on change requests for this concept.
Request body
/api/v1/entities/{id}/deprecateBearer · kb:write · governance:writeRetire a concept. Without supersededBy the lifecycle becomes 'deprecated'; with it the lifecycle becomes 'superseded' and supersededBy points at the successor.
Request body
Only a published concept can be retired
published. A draft or pending-review concept gets a 400 telling you to publish or delete it instead; a concept that is already deprecated or superseded gets a 409. There is no draft → deprecated edge in the state machine.Revision trail
/api/v1/entities/{id}/revisionsBearer · kb:read · governance:readThe append-only history for one concept, newest first. Returns 404 for an unknown or deleted entity, so revisions of purged ids stay private.
Query parameters
changeKind is one of create, update, review, certify, set_authority, set_owner, deprecate, supersede, delete, change_request, verify. activity records where the change came from — api for the authority and deprecate endpoints, and change_request:<id> for anything applied by an approval.
Document verification
/api/v1/documents/{id}/verifyBearer · kb:write · governance:writeStamp a document. verified and certified record who verified it, when, and a re-verify due date (now + the workspace reviewIntervalDays); unverified and deprecated clear all three stamps. Audited as document.verify.
Request body
Error envelope
{ error, message } — plus issues on a body-validation 400. Across this module: 401 unauthorized (missing, invalid or revoked key), 403 insufficient_scope or insufficient_capability, 400 invalid_body / invalid_json / invalid-params / invalid, 404 not_found, and 409 conflict. One exception worth coding around: the document-verify route's own not-found reply is { "error": "Document not found" } with no message key. That branch also applies the caller's content ACL, so a document restricted away from the key looks identical to one that does not exist. There is no pagination envelope, idempotency key or rate limiter on these routes.Data elements
Warrant owns two tables outright, owns a column group on two more, and appends to the audit log. Every one of the five carries a workspace_id and a <table>_tenant_isolation row-level-security policy whose USING and WITH CHECK clauses both require workspace_id to equal the workspace of the current transaction. There is no cross-workspace read path: a wrong-workspace id is simply invisible, which is why several routes return 404 where you might expect 403.
Two details worth knowing before you build on these rows. First, editorial_note is steward-only: it is stored on the entity but deliberately excluded from the revision snapshot and diff, so it never travels into a history view. Second, entity_revisions.version is bumped with an atomic UPDATE … RETURNING that takes the entity row lock, so two concurrent mutations can never mint the same version number.
How it composes
Warrant is usable on its own — the state it records is meaningful whether or not anything reads it. What follows is what each neighbour adds, and what genuinely breaks without it.
With Atlas — the concepts being governed
Atlas owns the entities row; Warrant owns its governance columns. This is the one real dependency: a change request of kind update, deprecate, supersede or delete, and every one of the five authority/lifecycle endpoints, needs a live entity to point at. Approving a create request is how you get one without touching Atlas's write API at all — the entity is created by the approval, at authority curated.
With Lens — where the state changes answers
This is where governance stops being paperwork. Retrieval's graph-fusion stage excludes entities whose lifecycle_status is draft or pending_review, so drafting a concept keeps it out of answer context. On the document side, documents.verification is a direct input to the answer policy's confidence features — ranked unverified 0, verified 1, certified 2, and deprecated −1, so a deprecated source counts against the answer — and when no cited document is verified or certified the policy raises a no_verified_source caveat. Entity authority participates too: the feature set tracks whether any detected concept is certified. Without Lens, all of this is recorded and simply not consumed.
With Lineage — the trail, assembled
entity_revisions and the audit rows are Warrant's, and GET /api/v1/entities/{id}/revisions reads them without any Lineage code. Lineage is what stitches them together with document revisions, extraction runs and citations into a single provenance view. Note that createGovernanceClient also exposes documentLineage(id), which calls GET /api/v1/documents/{id}/lineage— that endpoint is Lineage's, not Warrant's, and is documented there.
With Intake — verification on ingested content
Connector-ingested documents arrive at the default verification of unverified. Warrant's verify endpoint is the human stamp on top of that. No connector is required to use it — any document row will do.
With Crucible — the policy decision log
GET /api/v1/policy/decisions and GET /api/v1/policy/stats are guarded by the same governance:read capability as this module's reads (accepting either kb:read or runs:read), and their wire types PolicyDecision / PolicyStats live in @eli-ai/contracts/governance. Their client methods, however, are on the quality entry — createQualityClient(transport).listPolicyDecisions() — so they are documented with Crucible. Adopt Warrant alone and you still get the write-side governance state; you do not get the decision log unless you also take that entry.
With Ports — two approvals that are not the same thing
Agent runs have their own human-in-the-loop approval at POST /api/v1/runs/{id}/approve, gated on agents:run. It has no relationship to the change-request queue: different table, different scope, different lifecycle. Do not wire one to the other expecting shared state. Separately, the SKOS/PROV-O export at GET /api/v1/export/skos is reachable from createGovernanceClient(transport).exportSkos() but guards on graph:read — it is a graph export whose contents are lifecycle-shaped (published, deprecated and superseded concepts only; drafts never export).
With Conduit
No coupling. Live-data connectors and governed queries neither read nor write any Warrant table, and Warrant needs nothing from them.
Going deeper
Extending Warrant
policyObservers runs post-hoc on a recorded decision and can never change a verdict — and can read and write governance through the facade: staleConcepts, getCurationMeta and curationSummary under governance:read, and markReviewed, setAuthority and deprecateEntity under governance:write, each appending the same revision rows a human action would. Warrant emits entity.certified, entity.authority_changed, change_request.* and policy.decision.recorded. See the extension model for the contract and the trust boundary, and Build a plugin for a worked example.