Connectors
Three connectors carry the pilot: an ELN connector (category eln), a QMS/SOP connector (category
documents) and the open literature and knowledge connector (literature_search). All three are read-only:
agents never write into a notebook or a QMS, drafts stay in Trovensa as proposals, and scientists carry them
over themselves with the provenance footer. All fetching runs in the API or the worker; the agent container’s
only configured endpoint is the tool server. Every connector is designed until the R1 gates pass.
Adapter shape
IntegrationAdapter (with IntegrationContext) keeps its phase-1 methods (describe, begin_connect,
complete_connect, health_check, sync, handle_webhook, disconnect) and gains verify_webhook,
list_changes, fetch_object, search, render_launch and budget; defaults raise NotSupported.
ProviderSpec gains event_verification, write_policy, budget_scope, launchable, credential_class,
poll_interval_seconds and api_version_default; IntegrationContext gains budget: BudgetGate and
documents: DocumentIngestor.
| Provider id | Category | Credential class | Events | Launchable | Default API version | Write policy |
|---|---|---|---|---|---|---|
benchling | eln | service | ecdsa_jwks signed webhooks | yes (notebook canvas) | v2 | read_only |
veeva_qualitydocs | documents | service | none; 15-minute delta poll and nightly reconciliation | no | v26.2 | read_only |
literature_search | literature | platform keys | none | no | per source | read only |
Connections
Connections stay program-scoped (ADR 0018): a partner registers one app or integration user per program and
scope_rules (project ids, document types) are enforced in Trovensa even where the vendor token is
tenant-wide. ConnectionOut gains scope_rules, poll_interval_seconds, credential_class,
write_policy, hook_url, api_version, consented_by and consented_at; health is written on every
sync. A bump of api_version is a controlled change that writes an audit row. Reads run under one integration
identity per connection, so a console user could see content their own vendor permissions deny; the
integration user is provisioned with a minimal profile (effective SOPs of the mapped document types, read-only
access to the mapped notebook projects), and every surfaced record names its source system.
connector_objects (ConnectorObjectOut) mirror what a connection has seen: external_type,
external_id, external_version, payload_hash, external_modified_at, first_seen_at, last_seen_at,
deleted_at, mapped_type, mapped_id, state; unique per connection, type and id.
ELN connector
Provider benchling, read-only. Verified vendor facts (Benchling developer documentation: app authentication,
webhooks, App Canvas and rate limits; verified 2026-09-14 and recorded in PRODUCT-CONTRACT.md 10.10):
POST /api/v2/token with client credentials issues tokens with expires_in 900 s; webhooks are signed with ECDSA
P-256 over id.timestamp.body and carry the headers Webhook-Id, Webhook-Timestamp and Webhook-Signature;
the verifying keys come from a JWKS per app definition, refreshed at most every 6 h, and a delivery whose timestamp
is outside a 5-minute tolerance is rejected; canvas events v2.canvas.created, v2.canvas.initialized and
v2.canvas.userInteracted expect an answer within 3 s; rate limits are 300 requests per 30 s per app and 1000 per
30 s per tenant across all apps, reported in x-rate-limit-* headers; v2 entries are days[].notes[] with
modifiedAt and no version counter (v3 uses parts).
- Token: cached in worker memory, refreshed at 12 minutes (the vendor lifetime is 900 s).
- Events:
POST /api/v1/hooks/benchling/{connection_id}verifies the signature before parsing (JWKS cached and refreshed at most every 6 h, 5-minute timestamp tolerance), answers canvas events with202inside the 3 s window and does the work afterwards, writeswebhook_deliveries(signature_ok,replay,outcomeinenqueued,ignored,rejected), de-duplicates on delivery id inside the replay window, treats payloads as pointers and re-fetches the canonical object. - Versions:
external_version = <payload_hash>@<modifiedAt>, where the hash is sha256 of canonical JSON of the entry content (days in order, notes in order, table cells row-major, mentions expanded totype:id, attachments asid:sha256);target_ref = benchling:entry:<id>@<hash prefix>. - Synced: entries (metadata,
modifiedAt, review state mirrored toconnector_objects.stateand shown as “recorded in the notebook”), entry content flattened intodocumentsascustomer_internalwith thedaysstructure kept incontent_json, attachment metadata (bytes on demand under 25 MB), registry entities linked by id only. Mapping: entry ->sources(eln_entry),experiments.eln_source_id. - Budgets: an organisation
rate_budgetsrow of 900 per 30 s; per-connection rows of 250 per 30 s draw from it; vendorx-rate-limit-*headers drive adaptive throttling. - Launch: the app canvas renders “Check this entry against the effective SOP”; a click mints a one-time
launch_idsrow (10-minute expiry) and the canvas gains Open in Trovensa (how-to). - Onboarding order: create the connection, copy
hook_url, the partner admin creates the app from the manifest with the canvas feature, store client id and secret, run the health check (how-to). - Fake server:
api/scripts/fake_benchling.py(port 8190) implements signed webhooks, the canvas lifecycle and entries withdays[].notes[]for CI and the local demo; results from it are labelled illustrative.
QMS/SOP connector
Provider veeva_qualitydocs, read-only ingestion of controlled SOP versions. Verified vendor facts (Vault REST API
reference v26.2: authentication, documents, document versions, renditions, VQL and API limits; verified 2026-09-14
and recorded in PRODUCT-CONTRACT.md 10.10): REST base https://{vaultDNS}/api/v{YY.N} with three frozen versions a
year and v26.2 current; POST /api/{version}/auth returns sessionId and vaultId; documents carry
major.minor versions and per-Vault lifecycle labels; VQL supports FROM ALLVERSIONS documents with
version_modified_date__v, major_version_number__v, minor_version_number__v, status__v and
document_number__v; the version text endpoint GET .../versions/{major}/{minor}/text returns text/plain and
GET .../renditions/{rendition_type} streams the viewable file; the only throttling headers are
X-VaultAPI-BurstLimitRemaining (5-minute window) and X-VaultAPI-ResponseDelay, because the
X-VaultAPI-DailyLimit* headers were removed in v21.1, so there is no daily-limit budget; /auth has its own
one-minute window; the client header X-VaultAPI-ClientID (at most 100 characters) is sent as
trovensa-review-connector-client-vault.
- Health check (R1c exit gate): validates each field of the per-connection VQL template, the lifecycle map and
ALLVERSIONSavailability, verifiesvaultId, and reports gaps before the first sync. - Ingestion: 15-minute delta poll filtered
version_modified_date__v > watermark(poll_qms); nightly reconciliation overALLVERSIONSand the document events endpoint (reconcile_qms); the version text endpoint is preferred over rendition parsing for requirement extraction; renditions are downloaded only when(major, minor, rendition_hash)changed; a connection-scopedrate_budgetsrow follows the burst header and honoursResponseDelay;connection.healthis written on every sync. - Versions (
sop_versions):major,minor,lifecycle_statemapped per connection (draft,in_review,effective,superseded,obsolete,unmapped),effective_atfrom the effective-date field when present, else first observation witheffective_on_observed = true,superseded_atfrom the source or the successor’seffective_at,rendition_hash,document_id,external_version_label. Onlyeffectiveversions bind checks; unmapped states are ineligible and surfaced in health. - Authority: the QMS is authoritative for content and lifecycle state;
POST .../review/sops/{sop_id}/approveapplies to local SOPs only (409 sop_externalotherwise); the binding decision for connector SOPs is the requirement-set acceptance (how-to). - Pilot reads: the partner’s sandbox in R1c, production QualityDocs through a dedicated read-only integration user at R2 after partner sign-off; the connection records which and the pinned
api_version. - Fake server:
api/scripts/fake_vault.py(port 8191) implements the template query,ALLVERSIONS, renditions and lifecycle labels; its output is labelled illustrative.
Open literature and knowledge connector
Provider literature_search. Platform keys live in trovensa/<env>/providers/literature_search; budgets are
platform-scoped with per-organisation fair shares. Licence-gated sources are excluded in phase 2 and, when
added, take tenant-supplied keys through connection secrets, never a shared platform key.
| Source | Key | Provenance schemes | Budget rule | Snapshot stamp |
|---|---|---|---|---|
| Europe PMC | none | PMID, PMCID, DOI, PPR | self-cap 5 requests per second (no numeric limit published; bulk automated download forbidden, so bulk full text comes from the PMC Open Data bucket); the licence is read from the license field of resultType=core results (for example cc by); full text from /{PMCID}/fullTextXML | retrieved_at |
| Crossref | mailto (polite pool; User-Agent also accepted) | DOI | header-driven (x-rate-limit-limit 3 per x-rate-limit-interval 1 s observed live); mailto is always sent | none |
| OpenAlex | key (api_key parameter or bearer) | W-id, DOI | header-driven on X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Credits-Used and X-RateLimit-Reset; 429 above 100 requests per second; daily credit budget, stop at 90 % of it (the polite pool is no longer documented); the licence is read from primary_location.license and best_oa_location.license, not from open_access | none |
| PubMed E-utilities and PMC | key | PMID, PMCID | 8 requests per second; full text read anonymously (--no-sign-request) from the public PMC Open Data bucket pmc-oa-opendata in us-east-1, which is not split by licence directory, so the worker filters per article version on the license_code, is_pmc_openaccess and is_manuscript fields of the metadata prefix | none |
| ClinicalTrials.gov v2 | none | NCT | 3 requests per second | dataTimestamp |
| openFDA | key | report id, NDC | 120 000 requests per day | meta.last_updated |
| Open Targets | none | EFO, Ensembl | 3 requests per second | meta.dataVersion |
| ChEMBL | none | CHEMBL id, InChIKey | 3 requests per second | chembl_db_version |
Query policy (visible in the run rail): Europe PMC search first, Crossref for DOI metadata, OpenAlex for the
citation graph and open-access location, PMC full text only for cc0, cc_by and cc_by_sa; 25 results per
query and three queries per turn by default (programs.settings.evidence_query_budget overrides). Sources are
de-duplicated per cluster (cluster_key is the DOI when known; PMID, PMCID and W-id are aliases) with
per-provider snapshot stamps in provenance. Excluded with reason: Semantic Scholar, DrugBank, Lens, bioRxiv
(preprint successors are resolved from Europe PMC PPR records). Licence handling per field is on the
licence codes page. Numbers in this table are vendor statements verified on
2026-09-14 in the Europe PMC RESTful API documentation, the OpenAlex API documentation (works, rate limits and
authentication), the Crossref REST API documentation (etiquette and rate limits) and the PMC Open Data bucket
registry entry, and recorded in PRODUCT-CONTRACT.md 10.10; live smoke tests run behind LIVE=1.
Budgets and events in common
Agent tool calls and syncs draw from the same rate_budgets rows (scope in platform, organization,
connection; limit_per_window, window_seconds, used, remote_remaining from vendor headers);
exhaustion returns a typed RateLimited that the rail shows as “budget exhausted for source X, retry after
N s”. The inbound gateway POST /hooks/{provider_id}/{connection_id} takes no bearer, caps bodies at 1 MB,
verifies before parsing, stores rejected deliveries without payload and rate-limits them per source address,
and answers 202 in under 2 s. Retention: webhook_deliveries 90 days (rejected 30), launch_ids swept 24 h
after expiry.
Source: api/app/integrations/base.py, api/app/integrations/registry.py, api/app/integrations/secret_store.py, api/app/db/models/connection.py, api/app/db/models/connectors.py, api/app/db/models/review.py, api/app/worker.py, web/lib/api/types.ts, shared/knowledge-attributions.json, PRODUCT-CONTRACT.md sections 10.7 and 10.10, PRODUCT-PLAN.md Part 4 section 6, decisions/0015-read-only-connectors-and-agent-service-identity.md, decisions/0018-program-scoped-connections-and-pinned-api-versions.md; vendor documentation pages as named in PRODUCT-CONTRACT.md section 10.10 (Vault REST API reference v26.2, Benchling developer documentation, Europe PMC RESTful API, OpenAlex API, Crossref REST API, PMC Open Data bucket registry entry), read on 2026-09-14