Source Adapters, capabilities, and Actions
Bind a Source to Profiles and optional Provider operations with compile-time consistency.
A Source Adapter owns one Source's configuration and operations. It may import exactly one Provider or operate providerlessly.
Capabilities gate operations
| Capability | Required implementation |
|---|---|
sync | cursor-driven emission of Resource upserts, removals, warnings, and checkpoints |
search-remote | provider search returning bounded normalized results and optional continuation |
retrieve | exact complete Resource retrieval by Ref |
download | streaming one resolved Artifact's bytes into managed storage |
Declaring a capability requires its operation; omitting a capability forbids that operation. The SDK preserves this relationship through inference and core validates it again before activation.
Search routing (indexed, federated, or hybrid) is not a capability. CLI routing flags override Source configuration, which overrides the Adapter's decision.
Provider access
A provider-backed Adapter declares only its additional scopes and API hosts:
defineAdapter({
id: 'acme.mailbox',
provider: acmeProvider,
access: { scopes: ['mail.read'] },
providerApiHosts: ['mail.api.example.com'],
// configSchema, profiles, routing, capabilities, operations, actions
})The host supplies scoped context.fetch; do not import stored tokens or core runtime internals.
Return portable failures
Throw syncError(...) for expected provider failures instead of relying on a package-local Error subclass. The marker is structurally recognized across separately installed SDK copies while its schema keeps diagnostics bounded:
import { syncError } from '@ctxindex/extension-sdk'
if (response.status === 403 && response.headers.get('x-ratelimit-remaining') === '0') {
throw syncError('rate_limited', {
message: 'GitHub anonymous API rate limit exhausted',
retryAfterMs: millisecondsUntilReset(response.headers),
})
}
if (!response.ok) {
throw syncError('provider_bad_response', {
message: `GitHub returned HTTP ${response.status}`,
})
}Use the narrowest stable code. Do not include response bodies, tokens, filesystem paths, or unbounded provider diagnostics in messages. Preserve cancellation instead of turning it into a provider failure.
Actions
Profiles declare Action identity, effect, input, and output Profile. Each Adapter binds only the Actions it supports and supplies the implementation:
actions: {
'acme.note.draft.create': {
profile: noteProfile,
input: createDraftInput,
output: noteProfile,
run: createDraft,
},
}An Adapter cannot implement an undeclared Profile Action. Action contexts expose the selected Source, validated input, scoped fetch/logger/cancellation, and narrow read-only Resource/Artifact resolvers—not database or arbitrary provider access.
Operation checklist
- Parse
context.source.configwith the same strict schema the Adapter declares. - Validate provider JSON before normalizing it into Profile payloads.
- Pass
context.signalto every fetch. - Keep Refs stable and treat the suffix after the Source id as Adapter-owned opaque identity.
- Bound pages, results, cursors, warning text, and downloaded bytes.
- Emit a checkpoint only after every Resource covered by that cursor has been emitted.
The external GitHub Issues Adapter is the complete checked example for sync, pagination, cursor validation, portable failures, Profile reuse, and synthetic provider tests.