Connectors

How external services reach the box filesystem: the framework every connector shares, and one page per connector.

How connectors work

External service → Connector.sync() → Writes/reads card files → Git commit

A connector's sync() method:

  1. Reads its config from _config/connectors/<name>.json and its credentials from the machine-level secret store (docs/secrets.md). Some box-authored integrations (capture, dropbox, raindrop, kie, omdb, tmdb) still keep their own _config/connectors/<name>.secret.json; no built-in connector does
  2. Pulls new data from the external service
  3. Creates/updates card files in the box
  4. Stages and commits changes with structured trailers
  5. Optionally pushes local changes back to the service (two-way sync)
  6. Creates job cards for downstream processing (e.g., chat jobs for new messages)

Returns a SyncResult with { success, created, updated, pushed?, jobs?, procedures?, error? }. Procedure requests are run by wakeup orchestration after connector writes finish.

Sync rebuilds a connector-managed card's content wholesale from its template; any agent-added field the template doesn't know about is lost unless it's one of the few fields preserve-agent-fields.ts explicitly carries forward (currently just contains).

Members

Every built-in connector is service-injected (see below). Shared Google authorization for the three Google connectors: Google auth.

ConnectorFileCard typesDirectionPage
Google Calendargoogle-calendar.ts.ics filesTwo-wayCalendar
Gmailgmail.tsemail-thread, email-message, email-outboundTwo-way (pull + draft upload)Gmail
Google Drivegoogle-drive.tsgsheet, gdoc, gfolder, glinkTwo-wayDrive

Lifecycle

Connectors are called during bbx wakeup:

  1. wakeup.ts loads connector configs for the box
  2. Creates connector instances (with optional service injection)
  3. Calls syncConnector(connector, { boxRoot }) on each, which runs sync() and records the attempt (see Activity record below)
  4. Reports results

Every caller that syncs a connector (wakeup, bbx finalize, the sync command, bbx drive sync) goes through syncConnector, never sync() directly, so each attempt is recorded once.

Activity record

connectors/activity.ts keeps, per connector and box-local day, the number of sync attempts and how many succeeded, errored or skipped, plus the counts of new items, raw created paths and updated paths. It lives in machine-local transient state (_bookkeeping/connectors/connector-activity.state.json) and keeps 60 days.

A new item is a created path outside any .attach/ scope, that is, a new top-level card. Files inside a card's attachment scope belong to a card that already counted: Gmail lists every new message file in created, including replies on threads the box already tracks, so raw created would count a connector that only refreshes old threads as productive.

A connector reports what it did through SyncResult as before. To be counted correctly it must put new top-level cards in created, and set error when part of the sync failed even if the rest succeeded (Gmail does this for a draft that fails to upload).

connectors/activity-verdict.ts reads the record and decides whether a connector has gone quiet or keeps failing; the scheduler records each episode once, notifies the boxholder once for a failing one, and the dashboard shows a warning. See health-checks.md.

Configuration

A connector with non-credential config reads it from _config/connectors/<name>.json; the shape, and whether one exists at all, is on the connector's page (Drive's configuration is its cards). Credentials come from the machine secret store. Transient state (sync cursors, mappings) lives in _bookkeeping/connectors/<name>.state.json or <name>-state.json, machine-owned and gitignored.

Freshness metadata

A convention for any file that caches an external source of truth, applied wherever a cache is designed.

Any file that's a cache of an external source of truth (Gmail/Calendar/Drive snapshots, web fetches, synthesized briefings) should carry freshness metadata, but only where staleness would cause confidently-wrong output. Recipe categories: don't bother. Calendar snapshots: definitely.

Two timestamps, not one: last_sync (when we last checked) and data_through (cutoff of the actual data). They diverge when a check found nothing new — without both, "May 11" is ambiguous between "stopped checking" and "checked, nothing new." Sidecar <file>.sync.json is usually cleaner than an inline header (no diff noise in the human-readable file).

Hardest case: synthesized caches. A briefing built from 30 cards has data_through = min(inputs.data_through), not its own generation timestamp. If the pipeline doesn't propagate this, the briefing looks fresh while resting on stale inputs — this is where silent-stale bugs actually live.

When and how an agent tells the user about staleness is a prompt lens.

Service injection

Connectors that call external APIs accept an optional service parameter:

export function createGmailConnector(
  boxRoot: string,
  service?: GoogleGmailService,
): Connector

When no service is provided, connectors create real implementations from config files. When a fake is injected (in tests), all API calls go through the fake.

See src/services/AGENTS.md for the full service layer documentation.

Writing a new connector

  1. Implement the Connector interface (name, sync())
  2. Declare and resolve credentials via the machine secret store (docs/secrets.md) rather than a new <name>.secret.json file
  3. Use transient state for sync cursors/offsets
  4. Stage and commit all file changes with descriptive messages and trailers
  5. Create job cards when new items need processing
  6. Register via registerConnector() in the connector registry
  7. Add a service interface if the connector calls external APIs (see src/services/AGENTS.md)

Shared utilities