Contracts¶
The wire types shared between Primer's services. They are strict by default: unknown fields are rejected rather than ignored, and every model is immutable once constructed.
That strictness is the point. A field silently dropped between two services is a bug that surfaces as missing data much later, somewhere else.
The web app does not restate them. Each service's OpenAPI schema is written
to schemas/ by scripts/dump_openapi.py, and the TypeScript types are
generated from those files into apps/web/src/lib/api/generated/. Both are
checked in, and CI regenerates and diffs both — so a field renamed here is a
failed build rather than a runtime surprise in a browser.
uv run python scripts/dump_openapi.py # contracts -> schemas/
pnpm run generate:api # schemas/ -> TypeScript
What is not generated is the part that decides who a request is from: the
identity headers this server forwards to Primer are hand-written and
reviewed, in apps/web/src/lib/server/. A tool emitting that from a
document describing something else is not a saving worth making.
Identity¶
primer_contracts.identity ¶
The internal principal that services authorize against.
Principal is derived by the Control API from trusted edge identity. It is
never populated directly from a browser-supplied header by any other service.
Principal ¶
Bases: WireModel
An authenticated Primer identity acting on a request.
DeploymentCapabilities ¶
Bases: WireModel
What this deployment can actually do, as the browser needs to know it.
Sent to the web app so it can hide what will not work rather than offering it and failing. Every field is a checked fact or an operator's declaration - none is inferred from a model's name.
is_admin ¶
Whether this principal may see and change how the deployment is wired.
One rule, shared, because two services enforce it and a deployment where they disagreed about who is an administrator would be a deployment whose answer depends on which door you knocked on.
Group membership is the whole test. Primer implements no authentication
and no roles of its own; the operator names a group in their identity
provider, the proxy asserts it, and this is where that assertion is read.
It stays consistent with groups never granting access to a resource -
no library becomes readable because of this, only the deployment's own
configuration.
Fails closed. Authentication on with no group named means nobody is an administrator, which is the safe reading of an operator who has not made the decision yet: the alternative is that every user of a shared deployment can repoint it at an endpoint of their own.
With authentication off there is exactly one identity and it is whoever is running Primer - a single-user Compose stack, by construction. There is nobody to withhold this from, and withholding it would leave that deployment with no way to reach its own settings at all.
Errors¶
primer_contracts.errors ¶
Libraries¶
primer_contracts.libraries ¶
Library contracts.
A library has one owner, and owner_user_id is reported alongside the
acting principal rather than standing in for authorization: a caller
compares the two to tell its own libraries from ones shared with it, and
never to decide what it may do with either. Control decides that.
LibrarySummary ¶
Bases: WireModel
A library as returned by the Control API.
LibraryShare ¶
Bases: WireModel
One person a library has been shared with.
Identified by email as well as by id, because an id is not something the owner can check their intent against. Sharing is a decision about a person, and the owner has to be able to see that the person on the list is the one they meant.
Deliberately carries no role. Read access is the only thing a share
grants today, and a field named role that always says the same word
would be a promise the authorization model does not keep.
Documents¶
primer_contracts.documents ¶
Document and ingestion-status contracts.
IngestionStatus ¶
Bases: StrEnum
Deterministic states a user can observe for a document version.
DocumentSummary ¶
Bases: WireModel
A document and the state of its current immutable version.
ReindexSummary ¶
Bases: WireModel
What a library-wide rebuild actually started.
Two numbers rather than one, because they differ for a reason worth showing. A document already being rebuilt is not restarted - two workers writing different generations of one version, only one of which is ever activated - so pressing the button twice queues nothing the second time. Reporting only "queued" would make that look like a failure.
Chunks¶
primer_contracts.chunks ¶
The unit of retrievable content, passed from ingestion to retrieval.
Chunks cross a service boundary as Primer contracts rather than as Haystack documents. Retrieval owns the document store exclusively, so the shape Haystack wants is its business alone; putting a Haystack type on the wire would spread that ownership across two services and make an upgrade there a coordinated release here.
DocumentChunk ¶
Bases: WireModel
One passage, with everything needed to authorize and cite it.
Scope is carried on every chunk, not attached at index time. A chunk that could not name its library could not be filtered by one, and retrieval's isolation guarantee is exactly that filter.
Retrieval¶
primer_contracts.retrieval ¶
Retrieval contracts.
Every request carries both the acting principal and an explicit library scope. The Retrieval service rejects unscoped queries, so the scope is required here rather than inferred from ownership.
SourceLocator ¶
Bases: WireModel
Where a passage sits inside its source document.
Locators are user-meaningful positions only. Filesystem or object-store paths are deliberately absent from citation-facing contracts.
RetrievalRequest ¶
Bases: WireModel
A user- and library-scoped search.
RetrievedChunk ¶
Bases: WireModel
A ranked passage with the scope needed to authorize and cite it.
Ingestion jobs¶
primer_contracts.ingestion ¶
Contracts for the cluster-internal job transition protocol.
Workers own no ingestion state. Every transition goes through the Control API, which is the only writer of job rows, so two workers handed the same message cannot both decide they are the one doing the work.
Messages on the broker carry a job id and nothing else. Everything a worker needs arrives in the claim response, so a message that sat in a queue across a reindex cannot act on a stale copy of the document it was published with.
StageName ¶
Bases: StrEnum
The units of work a message can ask for.
ClaimOutcome ¶
Bases: StrEnum
Why a worker may or may not proceed.
Only CLAIMED authorizes work. The rest are ordinary, expected results
of at-least-once delivery, not errors: a worker that sees them
acknowledges the message and stops.
FailureDisposition ¶
Bases: StrEnum
What a failure means for the job, decided by the worker.
The worker is the only party that knows whether a failure was transient, so it says so explicitly rather than leaving Control to infer it from an error code.
JobClaim ¶
Bases: WireModel
Everything a worker needs, delivered at claim time.
The scope fields are not conveniences: every chunk this job produces carries them, and retrieval filters on them, so a claim that could not name its library could not produce retrievable content.
StageClaim ¶
Bases: WireModel
Ask to enter a stage. The job is named in the path, not the body.
StageCompletion ¶
Bases: WireModel
Report that a claimed stage finished.
The generation is echoed back so Control can refuse a completion for work that a reindex has already superseded.
StageFailure ¶
Bases: WireModel
Report that a claimed stage failed.
code and detail are shown to users, so they must stay sanitized;
exception traces belong in worker logs, correlated by job id.
TransitionResult ¶
Bases: WireModel
The outcome of a completion, failure, or heartbeat.
Indexing¶
primer_contracts.indexing ¶
The Retrieval service's cluster-internal API.
Retrieval owns the vector store exclusively. Nothing outside it names a collection, a table, or a filter syntax; callers name a library and a generation, and Retrieval decides what that means for the backend it was configured with.
IndexRequest ¶
Bases: WireModel
Write one generation's chunks.
Chunks are written into a pending generation, which nothing searches until it is activated. A half-finished rebuild is therefore invisible rather than partially answering questions.
GenerationQuery ¶
Bases: WireModel
Address one generation of one version.
GenerationCount ¶
Bases: WireModel
How many chunks a generation actually holds.
Activation compares this against what ingestion produced. A generation that is short of its expected count is an incomplete index, and activating it would silently drop the missing passages from every future answer.
LibraryAccessRequest ¶
Bases: WireModel
Ask Control whether a principal may read a library, and what to search.
LibraryScope ¶
Bases: WireModel
Permission and search scope in one answer.
They travel together because they are decided from the same rows: asking separately would leave a window where a library became readable, or stopped being, between the two calls.
SearchRequest ¶
Bases: WireModel
Search one library, within a known set of active generations.
Both scope fields are required and neither has a default. A search that could omit them would, on the day someone forgot, quietly return another user's documents - so omission is a validation error, not an empty filter.
DeleteRequest ¶
Bases: WireModel
Remove one generation's chunks.
Deletion is addressed by generation, so retiring a superseded index and erasing a deleted document are the same operation with the same idempotency.
PurgeRequest ¶
Bases: WireModel
Remove a version's chunks, optionally sparing one generation.
Retiring a superseded build and erasing a deleted document are the same operation with one parameter different, so they share a code path rather than two that must stay in agreement about what "gone" means.