Resources
A resource is a place items live. It is an instance — one bucket, one mailbox — and its type is the STI subclass it belongs to.
app/models/resource.rb, app/models/resource/
Identity is (tenant_id, type, key); key is validated unique within that pair. sti_name
dasherizes the class name, so Resource::OauthGoogle stores and reports as oauth-google.
The types
Section titled “The types”| Type | Class | Capabilities | Syncs |
|---|---|---|---|
s3 |
Resource::S3 |
storage | ✓ |
filesystem |
Resource::Filesystem |
storage | ✓ |
database |
Resource::Database |
storage | ✓ |
webdav |
Resource::Webdav |
storage | ✓ |
imap |
Resource::Imap |
— | ✓ |
rss |
Resource::Rss |
— | ✓ |
caldav |
Resource::Caldav |
— | ✓ |
carddav |
Resource::Carddav |
— | ✓ |
oauth-google |
Resource::OauthGoogle |
integration | — |
openai-compatible |
Resource::OpenaiCompatible |
inference | — |
Caldav and Carddav subclass Webdav and inherit its each_page, narrowing what they walk with
a wanted? predicate.
Resource::Brokered is a concern, not a type. Resource::OauthGoogle is the only type that
includes it — see Enrollment below.
One class serves every vendor speaking its dialect: s3 covers AWS, R2, B2, Wasabi, MinIO and
Garage.
Capability
Section titled “Capability”self.capabilities returns an array of symbols. storage, integration and inference are in
use; transport is defined and unimplemented.
Reaching a resource
Section titled “Reaching a resource”via_id is a nullable self-reference: this resource is reached through that one. Null means
reached directly. A resource named as via must declare the transport capability and implement
reach!(url), which rewrites the URL its dependent would otherwise dial.
The edge is validated both ways. A via must be this tenant’s, must not be the resource itself,
must not close a loop, and must not sit more than MAX_HOPS away. A transport that something still
rides on cannot be archived, and the database refuses to delete it or to point a via at another
tenant — the foreign key carries tenant_id, so a cross-tenant transport is unrepresentable rather
than merely refused.
storage? gates two items: being an export destination (Resource#storage!) and being the
tenant’s default_storage, which Resource#make_default_storage! sets exclusively.
Syncable
Section titled “Syncable”Resource#syncable? is respond_to?(:each_page). There is no flag and no registry — a type either
implements the enumerator or it cannot sync.
Only a syncable resource may hold a sync_interval, enforced by the
only_a_syncable_resource_keeps_a_schedule validation.
MINIMUM_SYNC_INTERVAL |
1 minute |
SYNC_ABANDONED_AFTER |
6 hours — after which sync_started_at no longer counts as a lock |
claim_sync! takes the lock with a conditional UPDATE, so a resource already syncing is left
alone rather than started twice. release_sync! clears it and computes next_sync_at.
Commands
Section titled “Commands”self.command_schema maps a command name to its parameter types; ? marks a parameter optional.
Resource#command validates arguments against the schema and dispatches to command_<name>.
{ list: { prefix: "string?", continuation_token: "string?" }, get: { key: "string", version_id: "string?" }, put: { key: "string", body: "bytes" }}describe_resource returns this schema, so a caller discovers a type’s vocabulary rather than
having it written down. See MCP tools.
Checking
Section titled “Checking”check! is implemented per type and is cheap — a HEAD rather than a listing. check wraps it,
writes checked_at and check_error, and returns a boolean. healthy? reads those columns.
Failure
Section titled “Failure”Adapters raise Resource::Failed (or a subclass — Resource::Filesystem::Escaped,
Resource::Rss::Gone) rather than a vendor error. Jobs retry it with backoff; see
Jobs.
Enrollment
Section titled “Enrollment”Types that include Resource::Brokered hold a connection id, not a credential. Broker.release
exchanges it for an access token at the issuer’s /connections/token endpoint on each use.
Enrollment.open! builds a signed, 30-minute (Enrollment::WINDOW) token; /enroll/:token is
where the browser completes it. Nothing is created until the link is followed.
Credentials never travel through an MCP tool call.