Adding a Source Adapter
August 3, 2026 · View on GitHub
A source adapter turns a ResolvedSource into SourceDocument values without
bypassing the shared pipeline. This guide describes the real pattern used by
axon-adapters (crates/axon-adapters/src/) — the source-family matrix,
the SourceAdapter trait, and the onboarding checklist a new family must
satisfy.
See also: crate guide crates/axon-adapters/src/CLAUDE.md, behavior contract
docs/pipeline-unification/sources/adapter-scopes.md, onboarding contract
docs/pipeline-unification/sources/new-source-contract.md.
Where a new family fits in the pipeline
SourceRequest -> SourceResolver -> SourceRouter -> SourceAdapter
-> SourceLedger -> SourceDocument -> SourceParseFacts / GraphCandidate
-> DocumentPreparer -> EmbeddingProvider -> VectorStore -> DocumentStatus
Adapters emit SourceDocument only — they never write prepared
documents, vectors, graph rows, jobs, or transport responses directly. Ledger
persistence, chunking, embedding, and vector writes happen downstream in the
shared pipeline (axon-services::source::index_source_with_auth and
friends), not inside the adapter.
Step 1: Add a SourceFamily variant
crates/axon-adapters/src/spec.rs defines SourceFamily (the enum every
family variant belongs to — Local, Git, Web, Feed, Youtube,
Reddit, Sessions, Registry, CliTool, McpTool,
MemoryIntegration) and SourceAdapterSpec, the struct that fully describes
one family's declared capabilities:
pub struct SourceAdapterSpec {
pub family: SourceFamily,
pub adapter: &'static str,
pub version: &'static str,
pub source_kinds: &'static [SourceKind],
pub vector_namespace: &'static str,
pub supported_schemes: &'static [&'static str],
pub shorthand_patterns: &'static [&'static str],
pub default_scope: SourceScope,
pub scopes: &'static [SourceScopeCapability],
pub credential_requirements: &'static [CredentialRequirement],
pub option_schema: &'static str,
pub parser_families: &'static [ParserFamily],
pub metadata_families: &'static [&'static str],
pub watch_supported: bool,
pub refresh_supported: bool,
pub may_access_local_paths: bool,
pub may_perform_network_fetches: bool,
pub may_call_render_provider: bool,
pub may_execute_tools: bool,
pub is_source_adapter: bool,
pub degraded_modes: &'static [&'static str],
pub required_graph_fact_kinds: &'static [&'static str],
pub optional_graph_fact_kinds: &'static [&'static str],
}
Add your new family to SourceFamily, then declare a const <FAMILY>_SCOPES: &[SourceScopeCapability] list (each scope marked required: bool with a
notes string) and add a new SourceAdapterSpec entry to the MATRIX const
in crates/axon-adapters/src/family_matrix.rs, returned by
source_family_matrix(). Model the security-relevant booleans honestly —
may_access_local_paths, may_perform_network_fetches,
may_call_render_provider, and may_execute_tools gate real security
policy elsewhere (SSRF checks, local-path trust, tool-exec allowlists), so
under-declaring them is a security bug, not just a docs gap.
Step 2: Implement SourceAdapter
crates/axon-adapters/src/adapter.rs defines the trait:
#[async_trait]
pub trait SourceAdapter: Send + Sync {
fn name(&self) -> &str;
fn version(&self) -> &str;
async fn capabilities(&self) -> Result<AdapterCapability>;
async fn discover(&self, plan: &SourcePlan) -> Result<SourceManifest>;
async fn acquire(
&self,
plan: &SourcePlan,
diff: &SourceManifestDiff,
) -> Result<SourceAcquisition>;
async fn normalize(
&self,
plan: &SourcePlan,
acquisition: SourceAcquisition,
) -> Result<StageExecutionResult<Vec<SourceDocument>>>;
}
discoverbuilds the fullSourceManifestfor the current state of the source (every item the source currently contains, keyed bysource_item_key) — this is what the ledger diffs against the previously committed generation.acquirefetches only the items the manifest diff says changed, given the diff computed fromdiscover's output.normalizeturns acquired raw content intoSourceDocumentvalues — the shared shape every downstream stage (parsing, chunking, embedding) consumes regardless of source family.
Look at an existing family's implementation for the real pattern before
writing a new one — crates/axon-adapters/src/local.rs (filesystem),
crates/axon-adapters/src/git.rs (repository), crates/axon-adapters/src/ feed.rs (RSS/Atom/JSON) are all live, non-stub implementations with sidecar
test files (local_tests.rs, git_tests.rs, feed_tests.rs).
Step 3: Register the adapter
crates/axon-adapters/src/registry.rs (AdapterRegistry) is the
registration/lookup boundary the router uses to find the right adapter for a
resolved source. Register your new adapter implementation there.
Step 4: Satisfy the onboarding checklist
crates/axon-adapters/src/onboarding.rs derives a SourceOnboardingStatus
from a SourceAdapterSpec — 15 rows (identity, resolver, router,
adapter, scopes, ledger, parsing, graph, chunking, metadata,
auth_secrets, observability, error_handling, tests, docs), each
either complete or not based on whether the spec declares the corresponding
fields non-empty. This is a derived, mechanical check — it doesn't
validate correctness, only that the spec isn't obviously incomplete
(source_kinds non-empty, parser_families non-empty, credential
requirements carry a reason, etc.). Run
onboarding_status(spec)/onboarding_rows(status) against your new spec
before considering a family "onboarded"; a family with any incomplete row is
not ready for the family-matrix contract tests.
Step 5: Add fixtures and tests
Add a sidecar <family>_tests.rs per the repo's test convention (declared
via #[path] in the family's source file). The Phase 9 family-matrix
contract expects, per family: resolver/adapter/parser/graph/metadata/vector
payload/source-job/degraded/auth/provider-failure fixtures where applicable.
Existing families' fixture trees under crates/axon-adapters/fixtures/ <family>/ are the concrete pattern to copy.
Step 6: Update generated docs/schemas
Adapter capability docs and schemas are generated, not hand-written — run
cargo xtask generated-contracts refresh after adding or changing a
SourceAdapterSpec so the generated capability docs and
schemas/provider-capability-schema.md/schemas/vector-payload-schema.md
stay in sync with the matrix. Finish with cargo xtask generated-contracts check.
Boundary reminders
- Source id / canonical URI construction belongs to
axon-route, not the adapter. - Ledger persistence, generation publishing, final chunking, embedding,
vector writes, and search/RAG do not belong in
axon-adapters. - No direct Qdrant upserts, embedding-provider calls, or job-store ownership from inside an adapter.
- No CLI/MCP/REST rendering from inside an adapter.
- Allowed dependencies:
axon-api,axon-error,axon-core,axon-route,axon-authz,axon-observe, and acquisition libraries (HTTP/git/feed/ transcript/archive/tool clients) hidden behind the adapter implementation. Forbidden:axon-vectors/axon-embedding/axon-retrieval/axon-services, direct job store, transport crates — enforced bycargo xtask check-layering.