← Harvester

Connectors

harvester · workzone

A connector is a plugin per source type (Jira, GitLab, Slack…), not per specific connection. It covers the source-specific surface of the contract — what's unique to each source; everything downstream is shared. Each connector declares a manifest, from which the connection form grows, and self-registers in the registry — the single list of supported types. The built-in connectors are gathered in the catalog; how to write your own is at the end. A configured connector instance is already a source.

What a connector is

Behind a single interface, a connector covers only what's unique to each source. The contract surface is four source-specific capabilities: fetch the raw material and bring it to the shared model (the pipeline core), plus two operational capabilities without which a connection can't be set up — a catalog of scope objects and a staged probe. Everything after normalize is written once and works for any source.

extract
fetch(since) → RawItem
The pipeline's inlet: connection, authentication, pagination, and the increment on since — and a stream of RawItem out. The fetch mechanics themselves are held by sources.
transform
normalize(RawItem) → Entity
The source's raw material the unified Entity model. The one transformation step written per specific source.
catalog
list_catalog() → [ScopeObject]
The source's catalog of scope objects — projects, spaces, channels. Feeds the live selection on the connection screen and the scope reconciliation on every sync (objects that appeared and were removed). The list's home is sources.
probe
check_connection() → Diagnosis
A staged probe of the setup: credentials accepted + rights sufficient, step by step, with a clear diagnosis. Reachability of the base URL is a shared layer, not the connector's concern. The step layout is held by sources.
everything after normalize is the shared pipeline (filter → classify → resolve → enrich → clean → chunk → embed → load), identical for any source
Manifest → Admin Panel

A connector doesn't just know how to reach a source — it declares what type it is and what it needs to connect. The registry serves the manifest over the API, and the add-source wizard in Admin builds a form from it: not a line of frontend per specific connector, the fields appear on their own. The backend is the source of truth, the form is its projection.

The connector declares In the UI this grows
type, name, icon a card in the type-selection step
credential kinds credential fields in the wizard
config fields (base URL, special params) the connection form
scope object kinds the Scope step — what can be selected
capabilities (incremental, webhooks) the available sync modes
toggles for collecting beyond the rules the “Content filtering” block on the card

The remaining manifest fields don't go into the form — they're declarative extensibility hooks: the connector describes its behavior with data, and the core applies a single mechanism. That's how a diverse source slots in without core edits — the limiter, webhook intake, retry/DLQ.

Registry

A connector self-registers on import — by a decorator or via discovery at startup: from the built-in package and from external packages that declare a Python entry point. The registry is the single source of truth about supported types: the type selection in the wizard, the connection form, and the dispatcher that spins up the right connector by connector_type all read the same registry.

Connector catalog

The built-in connectors. Each is a thin async client (httpx) over the source's REST API: the official SDKs are synchronous and carry their own retry layers, whereas platform reliability (AIMD, Retry-After, error_class) lives on the raw HTTP response. The scope objects define what the admin selects at the Scope step.

Jira httpx · REST API
Scope objects projects
Starting authority Medium (normal)
Starting rate 2.0 points/s
Collect beyond rules attachments · comments
Credential token · service account
Capabilities incremental · webhooks
Confluence httpx · REST API
Scope objects spaces
Starting authority High (high)
Starting rate 2.0 points/s
Collect beyond rules attachments · comments
Credential token · service account
Capabilities incremental
GitLab httpx · REST API v4
Scope objects groups · repositories
Starting authority Medium (normal)
Starting rate 1.5 req/s
Collect beyond rules attachments · comments
Credential PAT
Capabilities incremental · webhooks
Slack httpx · Web API
Scope objects channels
Starting authority Low (low)
Starting rate per-method · history ↓ 1/min
Collect beyond rules bots · attachments · private channels and DMs
Credential bot token
Capabilities incremental · webhooks
Don't confuse this is collecting history into the store; the conversational Slack bot is a separate integration
v2
S3 boto3
Scope objects buckets · prefixes
Starting authority Medium (normal)
Starting rate 50 req/s · per prefix
Collect beyond rules
Credential access key · IAM role
Capabilities polling
Custom connector

The source isn't in the catalog — write your own: one class on the same contract. Three steps — and the whole skeleton below; from there the connector slots into the type selection and the connection form on its own.

# where to put your class — two paths
✦ canonical
  pip package  entry point  achilles.connectors
  core untouched · platform upgrade stays clean

○ quick dev
  in-tree  achilles/harvester/connectors/custom/
  own folder · doesn't touch the built-ins
  1. 1 subclass · 4 methods
  2. 2 manifest
  3. 3 @register
github.py
# 3 · discovery picks it up on its own
@register
class GitHubConnector(BaseConnector):   # contract: 4 methods

    manifest = Manifest(                  # defines the form and the card
        type         = "github",
        credentials  = [PersonalToken],
        config       = [BaseURL],
        scope        = [Repository],      # the Scope step
        collect      = [Comments, Attachments],
        capabilities = [Incremental, Webhooks],
        rate_limit   = 1.0,               # starting req/sec (GitHub: 5000/hour)
        rate_limit_scope = AccountToken,  # limit per token, not per source
        request_cost = 1,                 # cost units per call (default)
        webhook      = Hmac("X-Hub-Signature-256", dedup="X-GitHub-Delivery"),
        error_class  = HttpErrors,        # 429/5xx → transient, 4xx → permanent
        default_authority = Normal,       # starting trust level: Low · Normal · High
    )

    def fetch(self, since):              # extract → stream of RawItem
        ...                               # httpx or any SDK — the contract doesn't mandate HTTP

    def normalize(self, raw):            # transform → Entity
        return Entity(...)               # then — the shared pipeline

    def list_catalog(self):              # catalog of scope objects
        ...                               # live selection + scope reconciliation

    def check_connection(self):          # staged probe: credentials + rights
        ...                               # URL reachability — shared layer
#  either way below — in-tree or as a pip package — the connector is visible
#   only after the image is rebuilt and the container redeployed:
#   discovery reads classes at process startup, not from Admin
achilles/harvester/connectors/ · in-tree
├── base.py        BaseConnector — the 4-method contract
├── registry.py    discovery: package + entry points
├── jira.py        
├── confluence.py  │ built-in
├── gitlab.py      ┤ connectors
├── slack.py       
└── custom/        ← the platform doesn't touch this folder
    └── github.py  ← your class goes here
achilles-github/ · separate pip package
# core untouched — discovery finds it via the entry point
├── connector.py   ← the same class
└── pyproject.toml [project.entry-points."achilles.connectors"]

The code is your own and trusted — the operator decides what to install: a shared process, no sandbox, a bespoke integration, not a marketplace. The < 1.0 contract isn't frozen — the package is rebuilt for the platform version.