datasources: a provider is resolved from a composed service, not a Go import — object storage first #1268
Description
What we are trying to do
Adding a datasource should not mean editing, compiling and releasing accounts.
pkg/business/datasources.go imports every provider package directly:
"accounts/pkg/datasource/apisource"
"accounts/pkg/datasource/connector"
"accounts/pkg/datasource/crawler"
"accounts/pkg/datasource/github"
"accounts/pkg/datasource/objectstore"and declares the providers as four constants — github, api, crawler, upload. The connector model itself is already generic: pkg/datasource/connector/ states an eight-clause envelope, Interface is a closed set of five (files, pages, records, messages, events) with the comment "The set is closed: a new interface is a contract change. The set of providers is open", Descriptor carries config fields, credential modes, a readers model, a budget and Conformant with a mandatory Gap, and connectortest/ holds a connector to all of it.
So the model's open set is closed by its host's build. That is the whole of what this issue fixes.
Scope — one PR
Populate the connector registry from composed service endpoints rather than Go imports, and register object storage that way as the first provider.
- The registry resolves a provider from a declared service dependency, not from a compiled-in package. A composition without that service has no such provider in its catalog, and a source of it refuses by name — the behaviour feat(accounts): object-storage sources read through the composed object-storage service; the host's store client is deleted #1235 already establishes for object storage.
objectstoreregisters through its composed service. feat(accounts): object-storage sources read through the composed object-storage service; the host's store client is deleted #1235 makes it a client of the composedobject-storageservice and deletes the host's ownclient.go, so the seam is half-built there and the proof costs one provider, not five.github,apiandcrawlerkeep working exactly as they do, still compiled in. Converting them is not in this PR.- The import block above is deleted for
objectstoreonly; the four provider constants stay until a provider contract is published. - Credentials stay per call and are never persisted by a provider service (envelope clause 3, decisions/callable-sources.md). The per-credential budget stays host-side: it meters across every source and replica (
datasource_credential_budgets), so it is shared state with a tenancy. - The conformance suite runs against the provider as registered, not against a package.
Size: ~400 hand-written lines. Past ~500, stop and ask rather than finishing.
Ordering
Gated on #1235, which must merge first: it is the proof that a provider can be a composed service, and the content of the three PRs closed as contained in it (#1213, #1216, #1214's successor) lives only on its branch today.
What follows this issue, each its own record when it is reachable, not now:
- publish a provider contract so a provider can be written in any language — this is where
DatasourceItemReadersandDatasourceProvenancestop beingaccountsv1types; githubmoves out as a service — the honest test, 4 RPCs but ~1,700 lines of Go behind an app-setup handshake, a push receiver, an OAuth account link and a git client;- only then is "extract a datasource core, keep saas as a thin host" answerable with evidence rather than preference.
Why this shape
The surface is the largest in accounts: datasource.proto answers 34 RPCs, against 26 for authorization.proto, 25 for platform_admin.proto and 13 for organizations.proto. Classified at 74e35f0b:
| Group | RPCs |
|---|---|
| Source lifecycle and catalog — generic | 10 |
| Callable source operations — generic | 6 |
| Directory, domains and groups — organisation identity, not source work | 6 |
| GitHub-specific | 4 |
| Provider account links | 4 |
| Delegation and triggers | 4 |
A host-independent core would be the first 16, under half the surface. Six are not datasource work at all.
Two templates for the rest already run in this platform, so neither is invented here:
- a provider as a composed service —
obin-ai/module-document-storecomposesxlsx,docx,pdf,libreoffice,parseandobject-storageas six separately published agents, each pinned by release inservice.codefly.yamland dialed through declared dependencies with no address written down; - one core, thin hosts —
codefly-dev/authority'slibraries/authority-core/gopluslibraries/conformance/go, which feat(accounts): the host interfaces are decided by the shared authority core #1257 made accounts a second host of, re-implementing no rule, with the shared suite a required CI context.
Docs, in the same PR
module/services/accounts/AGENTS.md— the datasource section, on how a provider is resolved.pkg/datasource/connector/CONFORMANCE.md— registration through a service rather than a package.module/services/README.mdandDEPLOYMENT_TOPOLOGY.mdif the service graph changes.
Reuse, and what is deliberately not reused
Reused: the connector envelope, Descriptor, the registry's validation, connectortest/, connector.Budgeted, and #1235's composed-service client. Not reused: nothing new is written that one of these already does — if this PR grows a second mechanism for provider resolution, it is wrong.
Fix or hack
A fix at the owner: the host stops owning the set of providers. No stopgap is expected; label one if it appears.
Context
- Investigation:
obin-ai/handbookdesign/datasource-module.md(branchbuckets, not yet merged) — the measurements above, both templates with their commits, the four host-coupled concerns, and the open question of whether the capability stays saas-only. - Owner's framing (2026-10-27): "saas knows way too many datasources, so we should create a new module: datasource … would be great if this module is generic and different implementations can add custom behavior." The owner chose to prove the seam in saas first and leave the new-repo question open until after step 1 above.
- Related, not superseded: Object-storage datasource: the contract says a client offers and flags providers by the catalog, no client reads it, and the connector that exists cannot take a new source (no cursor, no push, no admission) #1211 (the bucket provider), A connected source rotates its credential in place: same source, same collection, verified by a read before it replaces the old — today a rotated key is a different source and a full re-ingest #1225 (credential rotation in place), A receiver for bucket notifications, so a change in a customer bucket the service does not front enqueues a change set of exactly the delivered keys instead of waiting for the daily reconcile #1226 (a bucket notification receiver), Callable sources: connecting a source into a new collection grants the connector datasource:invoke at its boundary #1102 (grants at connect).
🤖 Generated with Claude Code
Activity
antoinetoussaint-byte commentedon Oct 27, 2026
Scope correction from the owner (2026-10-27)
Three constraints landed after this issue was filed. They do not cancel it — the seam below is still exactly right — but they change what it is a step towards, and they rule out the step this issue named as next.
1. Each deployment defines its own datasources. The provider set is a property of the composition, not of any host's build. So the catalog is derived:
GetDatasourceCataloganswers from the provider services this deployment composed and their declared endpoints, the way a cell derives reach fromendpointConsumersrather than a written-down list. That is precisely what this issue builds, so its scope stands.2. The datasource API cannot be turned into one interface — "some of them stream".
Interfaceis a closed set of five, all bulk-only by clause 8, and its own comment says a new interface is a contract change. Closing the data plane into five shapes is what breaks. So the next step is not "publish a provider contract with the five interfaces and the descriptor", as the Ordering section of this issue says. Strike that. What gets published is the vocabulary —DatasourceItemReaders,DatasourceProvenance, source identity, the refusal types — so a provider written elsewhere can speak about readers and provenance without importing a host's generated package. Nothing about how it serves data.3. The implementation does not live in this repository, and common providers here are convenience, not the exhaustive set. saas's job is access control: authorising a source, custody of the credential handed per call, the boundary node and its grants, the per-credential budget across sources and replicas, one audit vocabulary, and the catalog of what this deployment composed. The data plane — listing, diffing, fetching, streaming, watching — belongs to the datasource's own service.
A source whose API does not fit the envelope does not need a sixth envelope interface. It declares its operations, and this service authorises each invocation, spends the budget, stamps the credential and keeps a receipt without modelling what the operation does — the callable-sources seam that is already 6 of this service's 34 RPCs (
DeclareSourceOperations,ListSourceOperations,InvokeSourceOperation,LookupInvokeSourceOperationand the two receipt-pruning operations).What this means for the PR
Unchanged: resolve a provider from a declared, composed service; object storage first;
github/api/crawlerstay compiled in; credentials per call; budget host-side; ~400 hand-written lines.Added: the catalog must be derived from what is composed, not from a list — a deployment that composes no bucket provider has no bucket entry, and a stored source of an absent provider refuses by name.
Removed from the Ordering section: "publish a provider contract" as step 1 after this. Replaced by publishing the readers/provenance vocabulary. And nothing here should be built so that only envelope-shaped providers can be described — a
Descriptorwith no way to say "this one is called, not synced", or anAddSourcethat refuses what it cannot type, re-closes the set through the back door.The investigation is updated to match:
obin-ai/handbookdesign/datasource-module.mdon branchbuckets, which now carries the control-plane / data-plane split, why the core-and-thin-hosts template may be the wrong one here, and the revised order.🤖 Generated with Claude Code
antoinetoussaint-byte commentedon Oct 29, 2026
A lazybox agent is working on this.
claude(Opus 5.5) started at 2026-10-27 15:09 UTC, last renewed the claim at 2026-10-29 19:25 UTC, and the claim lapses at 2026-10-29 20:25 UTC unless it is renewed again. A claim is an assertion with an expiry, not proof that a process is alive.claim detail