Connector sync¶
connector-sync is one supervised scheduler for every connector. For each
connector it opens one MCP session through the Transport port and uses it for
everything:
- Provisioning. It reads the server's tools, skills, prompts and resources as a content pack. The EG sink reads the current committed head; when its canonical digest matches, nothing happens. Otherwise EG imports the pack and returns the sole durable receipt.
- Sync. It runs the manifest
syncpresets through themcp_toolsource adapter. Every page goes to the sink as a batch. EG records the checkpoint only after its receipt acknowledges that exact batch and checkpoint, and the next page is requested from that committed checkpoint. After a crash, the stream resumes from the last committed page. - Changes. It subscribes with
subscriptions/listento the list changes and to updates of the pack's resources and the descriptor's data resources. A list change, or an update to a pack resource, re-provisions. An update to a mapped data resource syncs its presets. Without listen support, the notifications the server sends on its own still count, and the connector'sinterval_secondsschedule runs a full cycle.
Composition and running¶
The SDK does not discover epistemic-graph or mint its request identity. GraphOS constructs the verified generated client from its authenticated runtime context and injects it at the public composition seam:
services = default_services(
settings,
state_dir=state_dir,
sink_name="epistemic_graph",
sink_client=verified_epistemic_graph_client,
pack_import_authority=live_pack_import_authority,
)
The authority resolver is called with the canonical connector id on every pack
import and returns the current generated McpCatalogSnapshotBinding and
AgentLibraryMutationContext. It is never cached. Omitting either injected
dependency for the epistemic_graph sink fails before a runner is built;
supplying either to another sink also fails. The standalone CLI consequently
cannot manufacture a usable EG composition; GraphOS owns that authenticated
composition boundary.
Provisioning reads the current EG pack head before upload. A matching canonical
EG digest is an acknowledged no-op. If the head moves during import, the typed
PACK_HEAD_CONFLICT causes one fresh status read and retry; the generated EG
client preserves the same deterministic import key for a lost-response retry.
Any non-null result digest that differs from the submitted pack fails closed.
connector-sync --config runner.yml # serve until stopped
connector-sync --config runner.yml --once # one cycle per connector
| Option | Default | Meaning |
|---|---|---|
--config |
required | runner configuration (YAML or JSON) |
--state-dir |
$XDG_STATE_HOME/connector-sync |
reserved process-state directory; ingestion checkpoints remain authoritative in EG |
--sink |
epistemic_graph |
sink extension name |
--once |
off | exit 0 only when every connector succeeded, otherwise 1 |
--log-format |
json |
json (one object per line) or text, on stderr |
--health-addr |
unset | PORT or HOST:PORT serving /health and /health/ready (env RUNNER_HEALTH_ADDR); disabled unless set, and never started for --once |
--health-allow-non-loopback |
off | required to bind --health-addr to a non-loopback host |
Exit 2 means the runner could not start: an invalid configuration, an
uncertified extension, a malformed credential setting, or a malformed or
disallowed --health-addr.
Record pages use EG's generated SourceIngest request, receipt, and client call.
Provisioning uses EG's generated ConnectorPack archive builder, digest helper,
and typed import result. A rejected pack remains a typed result and fails the
cycle; an unchanged result is an acknowledged no-op.
Health¶
--health-addr starts a small dependency-free HTTP listener (hand-rolled over
http.server, no framework) reporting the supervisor's own state -- not a
separate timer, so it can only ever say what the scheduler loop itself proved:
| Route | Meaning | 200 when | 503 body |
|---|---|---|---|
/health |
liveness: is the scheduler loop still ticking? | the loop completed a full reconcile pass within the liveness window (derived from registry_refresh_seconds, floor 30s) |
{"status": "error", "component": "scheduler_loop", "age_seconds": ..., "max_age_seconds": ...} |
/health/ready |
readiness: can the runner do useful work right now? | the registry has loaded, every currently-registered connector's endpoint credentials resolved, and sink.readiness() reports ready |
{"status": "not_ready", "reasons": [...], "connectors": {...}} |
/health/ready asks the configured Sink directly (its readiness() method
-- Extension ports), bounded by a short timeout so
a sink whose readiness check hangs (a stalled network call, say) reports not
ready instead of hanging the probe request; the timeout itself becomes a
"sink readiness timed out after ...s" reason.
The heartbeat backing /health is updated by ConnectorSyncRunner.run_forever
itself immediately after _reconcile() returns -- there is no separate
timer thread, so a wedged registry read, a stuck worker start, or any other
hang inside that loop stops the heartbeat and /health goes 503 naming
scheduler_loop, exactly the signal a pgrep-based liveness probe cannot give.
/health/ready's body always includes a connectors map, one entry per
connector the registry has ever listed, with no secret values:
{
"status": "not_ready",
"reasons": ["credentials unresolved: freshrss-agent"],
"connectors": {
"freshrss-agent": {
"last_success_seconds_ago": null,
"consecutive_failures": 0,
"next_retry_seconds": null,
"credentials_ok": false,
"credential_error": "connector 'freshrss-agent' has a credential reference that could not be resolved"
}
}
}
The listener binds loopback (127.0.0.1) by default -- --health-addr 8765
binds 127.0.0.1:8765 -- and refuses a non-loopback --health-addr (for
example 0.0.0.0:8765, the bind a Kubernetes httpGet probe needs, since it
reaches the pod IP rather than the container's loopback interface) unless
--health-allow-non-loopback is also given.
Configuration¶
settings:
max_concurrency: 4 # cycles running at once, runner-wide
backoff_initial_seconds: 5 # per connector, doubling after each failure
backoff_max_seconds: 600
registry_refresh_seconds: 60 # how often the registry is re-read
connectors:
- connector: freshrss-agent
package_root: ../agents/freshrss-agent # holds connector_manifest.yml
connectors_dir: ../agents/freshrss-agent/freshrss_agent/connectors
endpoint:
url: https://freshrss-mcp.example.invalid/mcp
bearer_token: openbao://apps/freshrss-agent#MCP_TOKEN
interval_seconds: 900
data_resources:
data://freshrss-agent/reading-list: [freshrss]
| Connector field | Meaning |
|---|---|
endpoint.url or endpoint.command + args |
streamable HTTP or stdio |
endpoint.bearer_token, endpoint.env |
credential references only (env://, openbao://); a literal value is rejected |
endpoint.client_credentials |
instead of bearer_token: issuer or token_url, client_id, client_secret_ref, audience, scope; the token is cached per connector, refreshed before expiry and re-minted once on a 401 (see HTTP clients) |
presets |
presets to run; empty means every sync preset of the manifest |
provision |
whether the content pack is provisioned |
data_resources |
resource URI to the presets synced when it is updated |
mapping_reference |
exact mapping the sink applies, normally manifest:<connector>#schema_mappings/<key>; manifest:<connector> is allowed only when the manifest declares exactly one mapping |
max_pages_per_cycle |
pages one cycle reads per preset; the next cycle resumes |
Relative paths are resolved against the configuration file. The package is validated before any session opens (manifest, presets and pinned fingerprints must agree), and credential references are resolved before the session opens. Any failure fails that connector closed and retries it with backoff.
Ports¶
| Port | Purpose | Implementations |
|---|---|---|
ConnectorRegistry |
the connectors to serve, re-read periodically | StaticConfigRegistry |
ChangeSource |
change events of a session | the MCP transport's sessions |
Transport, Sink, ArtifactKind, SourceAdapter |
see Extension ports |
Log events¶
event |
Fields |
|---|---|
connector_started, connector_stopped |
connector |
pack_imported, pack_unchanged |
pack_digest, imported |
stream_synced |
stream, pages, records, accepted, exhausted |
change_subscription |
listening, resources |
connector_failed |
error, and retry_in when serving |
registry_unreadable |
error |