Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion module/DATABASE_AUTHORITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ executable inventory and this table in the same change.
| Scope | Relations | Required database boundary |
|---|---|---|
| `global` | `audit_event_types`, `bootstrap_state`, `data_retention_policies`, `datasource_credential_budgets`, `email_templates`, `feature_flags`, `identity_providers`, `module_envelopes`, `plan_entitlements`, `plans`, `platform_admins`, `policy_log_commits`, `policy_log_cursor`, `solution_delivery_documents`, `solution_generation_history`, `solution_host_bindings`, `solution_registrations`, `solution_targets` | No RLS; exact grants |
| `tenant` | `actor_chain_journal`, `actor_chain_revocations`, `api_keys`, `approval_decisions`, `approval_requests`, `audit_event_idempotency`, `audit_events`, `connector_credentials`, `dashboards`, `datasource_account_links`, `datasource_domains`, `datasource_group_bindings`, `codefly_effect_receipts`, `datasource_operation_attempts`, `datasource_oauth_consumed_states`, `datasource_source_operations`, `datasource_sources`, `delegation_grants`, `domain_events`, `entitlement_overrides`, `executable_artifact_approvals`, `github_app_installations`, `github_app_setups`, `installations`, `invitations`, `membership_integrity_findings`, `org_generic_settings`, `org_identity_providers`, `org_key_bindings`, `org_settings`, `organization_activations`, `organization_authorization_revisions`, `organization_members`, `organizations`, `principal_authorization_revisions`, `principals`, `record_shares`, `role_assignments`, `role_permissions`, `roles`, `scope_grants`, `scope_nodes`, `source_delegations`, `source_read_revisions`, `subscriptions`, `team_members`, `team_membership_quarantine`, `teams`, `usage_events`, `usage_totals`, `webhook_deliveries`, `webhook_subscriptions`, `work_context_replay` | Enabled and forced RLS with at least one policy |
| `tenant` | `actor_chain_journal`, `actor_chain_revocations`, `api_keys`, `approval_decisions`, `approval_requests`, `audit_event_idempotency`, `audit_events`, `connector_credentials`, `dashboards`, `datasource_account_links`, `datasource_domains`, `datasource_group_bindings`, `datasource_object_manifests`, `codefly_effect_receipts`, `datasource_operation_attempts`, `datasource_oauth_consumed_states`, `datasource_source_operations`, `datasource_sources`, `delegation_grants`, `domain_events`, `entitlement_overrides`, `executable_artifact_approvals`, `github_app_installations`, `github_app_setups`, `installations`, `invitations`, `membership_integrity_findings`, `org_generic_settings`, `org_identity_providers`, `org_key_bindings`, `org_settings`, `organization_activations`, `organization_authorization_revisions`, `organization_members`, `organizations`, `principal_authorization_revisions`, `principals`, `record_shares`, `role_assignments`, `role_permissions`, `roles`, `scope_grants`, `scope_nodes`, `source_delegations`, `source_read_revisions`, `subscriptions`, `team_members`, `team_membership_quarantine`, `teams`, `usage_events`, `usage_totals`, `webhook_deliveries`, `webhook_subscriptions`, `work_context_replay` | Enabled and forced RLS with at least one policy |
| `user` | `client_authorization_codes`, `gdpr_requests`, `mfa_backup_codes`, `mfa_devices`, `mfa_login_transactions`, `notifications`, `onboarding_progress`, `resource_follows`, `sessions`, `user_consent_events`, `user_consent_preferences`, `user_identities`, `users`, `webauthn_ceremonies`, `webauthn_credentials` | Enabled and forced RLS with at least one policy |
| `pre_auth` | `magic_links`, `waitlist_entries` | Enabled and forced RLS; fail-closed request policy, accessed only by the control-plane role |
| `job` | `job_messages` | Enabled and forced RLS with at least one policy; no request relation grant — function-only scoped enqueue plus exact job-worker grants |
Expand Down
34 changes: 34 additions & 0 deletions module/services/accounts/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1173,3 +1173,37 @@ cause (v2 of that type; `datasourceAccessLostCodes` is the whole vocabulary a
registry test holds the declaration to). Recovery is the ordinary one — a later
snapshot clearing the degrade — which a repository made public again, or
reconnected with a PAT or through the App, all reach.

## Object storage is a client of the object-storage service, diffed against a listing the host retains

The `upload` provider — "Object storage" to a person — reads a bucket under a
key prefix into a `collection` scope node. Its readers are source-scoped: a
bucket has no per-object access list, so the model to offer is one collection
per prefix, granted like any scope. It is registered non-conformant, so
`AdmitNewSource` refuses a new one; existing sources sync on `runUploadSync`
over the host's own S3 client (`pkg/datasource/objectstore/client.go`), daily
and on "Sync now", re-sending every object and closing a whole-folder sync with
the listing the store tombstones the rest against. `AddSource` refuses a
webhook secret for it: there is no push path (#1211 b).

The owner's direction (2026-10-21) is that the host **re-uses the composed
object-storage service** rather than keep a store client of its own: the
service takes a per-call bucket binding with the credential handed per call and
owns the public-destination guard and the size caps
(codefly-dev/service-object-storage#108). What lives here is what is per-source
state:

- **The retained listing is the cursor.** `objectstore.FilesConnector` versions
a source by the digest of its listing under its binding and diffs the current
listing against the one a version names. The host keeps those listings in
`datasource_object_manifests` (migration 46), tenant-owned — the control plane
has no grant — and pruned to the newest four per source by
`PutDatasourceObjectManifest`; a pruned or rebound version resyncs. Clause by
clause, in [CONFORMANCE.md](./code/pkg/datasource/connector/CONFORMANCE.md).
- **The connector reaches the store through `objectstore.Bucket`.** It lists the
prefix whole or fails (`ErrListingIncomplete` — a partial listing would read
every unlisted object as deleted) and reads a batch in one call at the listed
ETags. The implementation over the service is not written yet; until it is,
the connector is exercised only by the conformance suite over a scripted
bucket, the provider stays registered non-conformant, and `client.go` stays
the running engine. It is deleted when the service client lands, not extended.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ func TestDatasourceCatalog_ProjectsTheRegistry(t *testing.T) {
t.Fatalf("%s entry = %v, want listed, non-conformant, with a gap, accepting no new source", key, p)
}
}
// A bucket has no per-object access list; the catalog says so before a
// person connects one.
if r := byKey[business.DatasourceProviderUpload].GetReadersModel(); r != gen.DatasourceReadersModel_DATASOURCE_READERS_MODEL_SOURCE_SCOPED {
t.Fatalf("object storage readers model = %v, want source-scoped", r)
}
if datasourceCatalog(nil).GetProviders() != nil {
t.Fatal("an unconfigured connector offers nothing")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ func (s *Service) newDatasourceConnectorRegistry() *connector.Registry {
Description: "An S3-compatible bucket; objects under a prefix are pulled on sync.",
Interface: connector.InterfaceFiles,
CredentialModes: []connector.CredentialMode{connector.CredentialStaticSecret},
Gap: gapUploadDatasource,
// A bucket has no per-object access list, so the catalog says before a
// person asks that every object is readable by the collection's readers.
Readers: connector.ReadersSourceScoped,
Gap: gapUploadDatasource,
}))
return r
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,41 @@ closes a whole-folder sync.
| 6 budget and backpressure | No declared `Budget`, and the connector is not metered by `datasource_credential_budgets`, so an object-storage sync cannot yield to a person's sync the way a GitHub one does. |
| 8 bulk only | One `GetObject` per object. The listing is one call; the fetches are not. |

**What closing it takes.** A `FilesConnector` over S3 whose version is a digest
of the listing (key → ETag) and whose `Changes(from)` diffs the current listing
against the one `from` names — which needs the previous listing to be retained,
since S3 has no "what changed since" API. That is a store-shaped change, not
just a connector: the host would have to keep the last manifest per source. Not
done here because it is a design question about where that manifest lives, not a
gap in a flow that already exists.
**What closing it takes, and how far it has got.** The connector that closes
clause 4 exists and passes `connectortest.RunFiles`
(`pkg/datasource/objectstore/connector_conformance_test.go`); it is not yet what
the host syncs through, so the registration above is unchanged and every
object-storage source still runs on `runUploadSync`.

- **The cursor is a retained listing.** An object store answers only "what is
there now", so `objectstore.FilesConnector` versions a source by a digest of
its listing (key, ETag, size) under its binding (endpoint, region, bucket,
prefix), and the host keeps the listing each version names in
`datasource_object_manifests` (migration 46; tenant-owned, the newest four
per source). `Changes(from)` diffs the current listing against the retained
one; a version whose listing was pruned, or that was listed under another
bucket or prefix, is `ErrResyncRequired`. A key that left and a key that
arrived with the same ETag and size are reported as one MOVED change — a
store has no rename, and the same bytes at a new key is what a move is.
- **A fetch is at a version.** `FetchFiles` serves an object only at the ETag
the version's listing names; an object rewritten or deleted since is
`ErrItemNotFound`, never content the version did not have.
- **The store is reached through a `Bucket`, not spoken to.** The connector
signs, dials and guards nothing: listing and reading are the composed
object-storage service's, under a per-call binding with the credential the
host decrypts (codefly-dev/service-object-storage#108). `client.go` in this
package is the engine `runUploadSync` still uses; it is deleted when the
`Bucket` over the service lands, not finished.
- **Clause 8 rests on the service.** The suite's scripted bucket serves a batch
in one read, which is the contract `Bucket.Read` states. Whether the
service's binding serves a batch in one call or one call per object decides
whether that holds against a real store; until the client exists it is the
stated requirement, not a measured fact.

What remains before the provider is registered conformant: the `Bucket` over the
service, `runUploadSync` replaced by the envelope path, the connector registered
through `connector.Budgeted` so it is metered by `datasource_credential_budgets`
(clause 6), and the gap sentence above deleted.

## crawler — documentation website

Expand Down Expand Up @@ -146,6 +174,7 @@ not qualify database commit/rollback or cross-process serialization.
| api | no | yes (records) | no | no | no |

Object storage is the nearest: it is already on an admissible interface and
already applies deletions, and one cursor stands between it and a second
conformant implementation. That is what would demonstrate the model generalises,
and it is the one worth doing next.
already applies deletions, and its cursor is built and passes the suite against
a scripted store. What stands between it and a second conformant implementation
is the client of the object-storage service the host syncs through. That is
what would demonstrate the model generalises.
Loading
Loading