Package quivrplugin implements all five Contributions
of Plugin Protocol v0. The core keeps
schedules, checkpoints, credentials and health; your source plugin fetches pages.
It uses YAML, JSON Schema and the OpenTelemetry API, never engine packages. Requires Go 1.26 or later. Both kits serve normalizers, alert rules, connectors, ingestion and retrieval.
Discovery serves the highest version the manifest admits, through Plugin API 0.19.0.
See the feature history.
The HTTP server automatically verifies engine tokens for API 0.14.0. Provision
QUIVR_PLUGIN_SIGNING_KEYS as described in the authentication contract.
Older declared API ranges remain unsigned; plugin dev and plugin test supply local keys.
go get github.com/The-Vibe-Company/quivr/sdks/goThe sample examples/static-source is a complete
plugin: a manifest, a connector, fixtures and a test.
The HTTP adapter continues incoming W3C trace context in each handler’s context.Context. Configure an OpenTelemetry SDK provider and exporter in your plugin process to export internal spans. For provider requests, call quivrplugin.InjectTrace(ctx, request.Header); SDK Blob reads already do this. See the protocol’s propagation and privacy rules.
Declare each kind in quivr-plugin.yaml with the JSON Schemas of its
configuration and credential:
id: example.feeds
version: 0.1.0
compatibility:
engine: ">=0.1.0 <0.3.0"
plugin_api: ">=0.3.0 <0.4.0"
contributions:
connector:
kinds:
feed:
config_schema: {type: object, required: [url], properties: {url: {type: string}}}
credential_schema: {type: object, required: [token], properties: {token: {type: string}}}
default_interval_seconds: 900
run:
command: [go, run, .]Implement Fetch and CheckCredential, register with plugin.Connector(kind, impl),
then call plugin.Serve(). The static-source example
shows configuration, credential decoding and checkpoint pagination.
Declare contributions.ingestion with the vector spaces the plugin owns and
register a type with two methods, SegmentAndEmbed (segments of the Version's
text Parts, each with a vector per requested space) and EmbedQuery, through
plugin.Ingestion(impl). The SDK refuses undeclared spaces and checks offsets,
dimensions and max_segments before answering; a RetryableIngestError delays
the Version, a TerminalIngestError quarantines it for later reprocessing.
examples/hash-embedder is certified in CI, and
Write an ingestion plugin
covers pinning it.
For bounded continuation, declare API 0.18.0 and contributions.ingestion.paging: true.
Implement PagedIngester.SegmentAndEmbedPage with contiguous coverage, honoring
request.Page.Start and MaxSegments; return NextStart until complete. Legacy ingesters keep using SegmentAndEmbed without a page request.
Declare contributions.retrieval profiles and register plugin.Retrieval(impl).
The deployment must map default; local profiles may be deep-only. Each round, Search
answers quivrplugin.Ask(requests...) for candidates or
quivrplugin.Rank(hits...) for the final ranking; req.Served holds every
candidate served so far. Before answering, the SDK refuses undeclared profiles
and checks what the core checks: no requests in the last round, request and k
limits, and a ranking of served candidates only. A TerminalSearchError
refuses the query. examples/fusion-retriever is
certified in CI, and
Write a retrieval plugin
covers pinning it.
| Concern | Behavior |
|---|---|
| Routes | GET /v0/discovery (identity, declared Contributions, the highest admitted Plugin API version, the sha256: digest of the exact manifest bytes), GET /v0/health, POST /v0/contributions/connector/fetch and /check_credential; /receive for push kinds and attachment routes when declared; normalizer, subscription, ingestion and retrieval routes when declared |
| Request checks | Request schema → 400 invalid_request; undeclared kind → 400 unknown_kind; installer configuration, instance config and credential against the declared schemas → 400 invalid_configuration, invalid_config or invalid_credential. All terminal; a credential never appears in the message |
| Errors | AccessError → 403, SourceError → 422 (terminal), TransientError(...).WithRetryAfter(d) → 503 with retry_after_seconds, all with error_class. ErrNotDue → a skipped run with the checkpoint unchanged. Any other error → 503 transient unexpected_error; a panic → 500 source internal_error. Details go to the log, never to the envelope |
| Response checks | Before sending: the response schema, max_items, max_response_bytes, a checkpoint of at most limits.max_checkpoint_bytes (64 KiB by default, at most 1 MiB) and diagnostics of at most 16 KiB (500 invalid_response). The engine still applies its own item validation |
| Credentials | Credential prints [redacted] in fmt, JSON and slog. Its string values are scrubbed from error messages and from req.Logger() |
| Deadline | The request context ends at the manifest's timeout_ms |
- The checkpoint is yours. Any JSON value that resumes after the returned
items: a cursor, a timestamp, a set of seen ids. The core stores it and
sends it back unchanged, and moves it forward only after your items are
accepted, so a crash never loses items. Keep it under 64 KiB, or declare
limits.max_checkpoint_bytes(at most 1 MiB). - Pages. Return at most
max_itemsitems andMore: trueto be called again at once in the same run, with the checkpoint moved. Usereq.Now, nottime.Now(), andreq.PageInRunandreq.ReadsTodayto respect source quotas.req.Connector.CorpusIDandreq.Connector.SourceNamespace(Plugin API 0.3.1) name where the instance writes, to bind Relation targets. - Items carry a stable
RecordKey, aRevisionwhen the source has one (the core skips an unchanged revision), text or a Manifest of text Parts (quivrplugin.NewManifest), extensions in namespaces your manifest declares, orWithdraw: truefor an item deleted at the source. - Attachments (Plugin API 0.4): declare
contributions.connector.attachmentsand implementOpenAttachment, which streamsreq.Attachment.Ref. The SDK reads the bytes once into a temporary file, answers their size and SHA-256 or a skip (optionalSkippedAttachmentupdates the item's extensions), then uploads them to the core's grant, which never prints. SetSizeBytesandSHA256when you already hold the exact bytes. - Errors tell the core what to show operators:
AccessErrorfor a refused credential (healthaccess_error),TransientErrorfor an outage or a rate limit (retried),SourceErrorfor data you cannot use. - Push (Plugin API 0.5): declare
modes: [pull, push]and implementReceiver. The core relays each request the source sends to the instance's publicWebhookURL(in fetch requests; register it with the source).Receiveverifies it with the credential overreq.Request.Body(), then answersAccept(items...),Respond(200, type, body)for a challenge, orRefuse(401, reason), which changes nothing. Map a delivered item exactly asFetchmaps it, so both paths converge on one Receipt.Page.Push(PushIsActive,PushIsPending,PushHasFailed) reports your setup at the source; an active one may relax polling.
For a kind with declared secure api.routes, register a typed helper for each
route:
plugin.Route("alerts", "event", func(ctx context.Context, req *quivrplugin.ReceiveRequest) (*quivrplugin.Delivery, error) {
if req.Body == nil {
return quivrplugin.Refuse(http.StatusBadRequest, "missing event"), nil
}
return quivrplugin.Accept(), nil
})The engine authenticates the request. The SDK matches the declared method and
path, validates the parsed JSON body against request_schema, and then calls
the helper. Register every declared route when the connector has no Receiver;
a Receiver can continue handling routes without helpers.
plugintest replays a fixture in process, through the same handler the core
talks to:
f, _ := plugintest.Load("fixtures/pages.json")
if err := plugintest.Verify(plugin, f); err != nil { // pages, errors, credential check
t.Fatal(err)
}A fixture (schema)
names the kind, config, credential and starting checkpoint, and what to
expect: the Record Keys of each page, an error class, the credential check,
and for a push kind the receive cases with their expected verdicts.
Use test credentials of 8 characters or more, so the leak check can see them.
Then certify the plugin with the Contract Runner:
quivr plugin inspect .
quivr plugin test --startup-timeout 120s . # go run compiles on the first startIt runs every fixture page by page, feeding each checkpoint back, resumes from
the final checkpoint, checks credentials, error classes and invalid requests,
and fails when a credential value appears in an answer or in your plugin's
output. CI certifies the sample and publishes its report as the
go-connector-contract-report artifact. To run it in Quivr, pin it as in
Run a connector plugin.
Register plugin.Normalizer(impl) with Normalize(ctx, *NormalizerRequest)
returning *NormalizerResponse, or plugin.Subscription(impl) with
Evaluate(ctx, *SubscriptionRequest) returning *SubscriptionResponse.
request.ReadInput(ctx) verifies file or signed URL bytes against size and SHA-256.
Use Match(id, explanation, partKeys...), NoMatch(id) and NotReady(id)
for decisions. The adapter checks declared expression/configuration schemas,
batch size, decision coverage, evidence and response bounds. Both handlers
return TerminalError or RetryableError to classify failures.
quivr plugin init generates Python normalizer, subscription and connector
starters, including a push source with --kind connector --push. Go starters and Python ingestion/retrieval starters are omitted:
these need a chosen vector model or search strategy, or Go module setup.
Start from the SDK examples or register the operations above; both kits can
serve every Contribution even when no starter exists.
The author of a Plugin API change owns both kits in the same PR: update the feature table, regenerate models/schema copies/version constants, adapt both servers, bump the Python package version and prepare the Go module release tag, and run SDK tests plus Contract Runner certification. Additive SDK features bump the minor package version; compatible fixes bump patch; breaking SDK interfaces bump major. Plugin API versions remain independent of kit releases.
- Import only this SDK and public packages:
make plugin-boundaryfails when code underplugins/orsdks/go/imports Quivr'sinternal/packages or requires the engine module. - The schema copies under
quivrplugin/schemascome fromcontracts/; runpython3 sdks/go/scripts/sync_schemas.pyafter a contract change (make testfails when they are stale).