Capabilities · 10. Bringing your own systems

Every capability in this family moves data towards the daemon, or between two daemons you control, and none of them sends anything to a service you do not run. That is the shape of the whole family and it is the reason the connectors are built the way they are: a connector pulls a source you already own into a corpus on your box, and a sync path reconciles two boxes directly rather than through a hub.

This chapter is explanation. It says why each connector exists, what it is gated on, and where it stops. Route tables, manifest fields and scheduler contracts are reference and already exist.

10.0 Eight capabilities, and the two questions to ask of each

Two questions separate these eight cleanly, and both are worth asking before you enable anything: what leaves the box, and what has to be true before this runs at all. The second question matters more than usual here, because several of these are double-gated: a flag alone does not start them.

#CapabilityStatusWhat leaves the box
10.1Peer-to-peer tenant syncSHIPPED; mutual auth and delegation both behind default-off flagsA tenant's collections, to a peer you name
10.2Background fact sync and the sharing postureSync runs when a remote and key are configured; posture and backfill SHIPPEDNon-private facts, to the configured remote. The posture endpoint tells you exactly which
10.3Scheduled connector jobsSHIPPED, always spawnedNothing. It is the timer the others register against
10.4Markdown vault watcherSHIPPED, double-gatedNothing. It reads local notes into a local corpus
10.5GitHub connector and indexed corpusSHIPPED; CORECRUXD_INTEGRATIONS_ENABLED, default onAuthenticated requests to GitHub, on your credential
10.6OpenAI-compatible connector and shimConnector SHIPPED; shim behind CORECRUXD_OPENAI_SHIM, default offWhatever you point the connector at
10.7Repository and workspace intelligenceSHIPPED; active watching feature-gatedNothing. Local trees, scanned locally
10.8Daemon-to-daemon embedding providerFLAG CORECRUXD_COMPUTE_PROVIDER, default offNothing outbound. It serves embeddings to a peer you authenticate

Grounding for each capability is in that capability's closing line rather than in a chapter-level list.

10.1 Two daemons reconciling directly, with no hub in the middle

Status SHIPPED. Mutual peer authentication is behind CORECRUXD_SYNC_MUTUAL_AUTH, default off. Recipient-bound delegation is behind CORECRUXD_SYNC_DELEGATION_ENFORCE, default off. Reached through /v1/sync/handshake/nonce and /v1/sync/tenants/{id}/*. Who it is for operators.

What it does. Two daemons reconcile a tenant's collections between themselves: a manifest exchange, a collection pull, a promotion preview, a promotion confirm and an offboard. Behind CORECRUXD_SYNC_MUTUAL_AUTH, default off, every one of those endpoints additionally requires an Ed25519 peer handshake, and ordinary or admin scopes cannot bypass it; the authenticated peer's tenant must exactly match the tenant in the path.

Why it works this way. Sync between two boxes you own does not need a third party, and introducing one would mean your memory transits a service you do not control in order to reach a machine you do. Direct reconciliation removes that hop. The mutual-authentication design goes further than the usual "the peer presents a token" arrangement by making the handshake replace scope authorisation on these routes rather than supplement it, so a peer with valid admin scope but no handshake is refused. That is deliberate: the risk on a sync route is not an under-privileged caller, it is a correctly-privileged one talking to the wrong tenant.

The delegation path is the same fail-closed pattern as capability tokens elsewhere in this documentation set. A delegated token is contextual, and with CORECRUXD_SYNC_DELEGATION_ENFORCE off, which is the default, such a token is rejected outright rather than having its caveats dropped to grant the broader underlying authority. The ordering constraint that follows is stated in the source as mint-before-verify: deploy verifiers with the flag on before any delegated token is minted, because an old verifier refuses rather than accepts.

What changes for you.

  • As an operator: you can run a second daemon and keep a tenant in step with it without either box depending on a hosted service, and promotion is previewable before it is confirmed.
  • As an agent: nothing. This is a between-daemons plane.

What it does not do.

  • Mutual authentication is off by default. Without it, these routes fall back to ordinary scope authorisation, which under a permissive auth mode is no authorisation at all.
  • It syncs collections for a named tenant. It is not a whole-daemon replication and it does not carry receipts.
  • Offboarding produces a receipt. It does not by itself erase anything on the peer.

Turn it on. Set CORECRUXD_SYNC_MUTUAL_AUTH=1 on both peers before using these routes across a network.

Where the detail lives. The five endpoints, the handshake headers and the tenant-match rule: API 1.9. The fail-closed delegation contract: sync.rs:36.

10.2 What would actually leave this box, answered before you find out

Status background sync runs when CORECRUXD_SYNC_REMOTE_URL and its API key are both set. The posture report and the backfill are SHIPPED and unconditional. Reached through MCP sync_pull, sync_push, sync_status; GET /v1/admin/sharing/posture, POST /v1/admin/sharing/backfill. Who it is for operators and agents.

What it does. When a remote URL and key are configured, the daemon pulls and then pushes facts against that remote on an interval, and private facts are excluded by policy on both directions. Alongside that, and independent of whether sync is configured at all, a posture endpoint reports exactly which prefixes would be pushed, how many facts are private, how many are pushable, and how many would become private if you ran the backfill. The backfill itself re-privatises history that should have been private, and it is dry-runnable: run it with confirmation off first and compare what it would re-store against the total.

Why it works this way. This is the most useful capability in the family and it is the one most likely to be missed, because it is documented elsewhere as two route rows in an admin table rather than as the privacy control it is. The design insight is that "is my private data safe from sync" is a question you want answered before you enable sync, not inferred afterwards from what appeared on the remote. Making the posture report unconditional, so it works on a daemon with no remote configured, is what makes it a pre-flight rather than a post-mortem. The backfill exists because privacy policy changes over time: prefixes get added to the born-private list, and history written before that change is not retroactively covered. Re-storing rather than rewriting keeps the append-only guarantee, so the backfill creates new fact versions rather than editing old ones.

What changes for you.

  • As an operator: before you configure a remote, read the posture. It enumerates by prefix. Afterwards, run the backfill dry and read would_re_store against the total before confirming.
  • As an agent: sync_status tells you whether this node is in a healthy sync mode; treat a degraded or local-only answer as a reason to stay on the local store rather than to attempt a cross-daemon handoff.

What it does not do.

  • Sync does not run because a flag is on; it runs because a remote and a key are both configured. There is no separate enable switch, which means checking your environment is the only way to know.
  • The backfill requires two scopes, not one, and takes an explicit confirmation. It is append-only safe but it is not free: it creates a new version of every fact it moves.
  • Private-fact exclusion is a policy applied by the sync path. It is not an encryption boundary.

Turn it on. Set CORECRUXD_SYNC_REMOTE_URL and its API key to enable background sync. The posture endpoint needs neither.

Where the detail lives. The posture and backfill routes, their scope requirements and the dry-run field to read: API 8.3. The sync tools: API 12.26. Exclusion accounting: sync.rs:91.

10.3 One timer for every recurring job, and a status you can query

Status SHIPPED. The scheduler is always spawned; individual jobs decide whether to run. Reached through the status fact each job writes, readable at GET /v1/facts?entity_prefix=__sync__::. Who it is for operators.

What it does. One driver task runs every recurring integration job, sleeping to the earliest deadline rather than ticking. Each job reports one of three outcomes rather than two, gets backoff on failure, has its consecutive failures counted, and writes a status fact readable over the ordinary fact API. The scheduler is always spawned; a job whose configuration is absent skips itself rather than the scheduler not existing.

Why it works this way. Before this existed, every recurring job open-coded its own interval loop, and none of them could answer when it last ran, whether it failed, or how many times in a row. Rather than copy that loop a third time, the jobs register against one driver. The genuinely useful design choice is the third outcome: a job that ran and correctly did nothing is distinguishable from a job that ran and succeeded, and both are distinguishable from a failure. Two-outcome schedulers force "nothing to do" to masquerade as success, which is exactly the state that hides a misconfiguration for weeks.

Writing status as a fact rather than to a log is the second choice worth naming. It means "did the connector run" is answerable with the same query you use for everything else, by a person or by an agent, without log access.

What changes for you.

  • As an operator: connector health is a fact query, not a log dive, and a job that is quietly skipping is visibly skipping.
  • As an agent: you can read connector status yourself under the __sync__:: prefix and decide whether a corpus is current before you rely on it.

What it does not do.

  • The first attempt is one full interval after start, never at boot. A restart therefore does not trigger an immediate run, which surprises people debugging a connector by restarting the daemon.
  • It schedules. It does not retry indefinitely or alert; consecutive-failure counting is a number you read, not a page you receive.

Where the detail lives. The registration contract, the three outcomes, the boot-delay rule and the status-fact shape: Extending 7.1. Module contract: sync_scheduler.rs:6.

10.4 Your notes, searchable, without leaving your disk

Status SHIPPED, double-gated. It runs only when a file-watcher pack is installed and granted, and watch roots are configured. Reached through the CORECRUXD_VAULT_WATCH_* environment variables; status fact __sync__::vault-watcher. Who it is for operators.

What it does. Point the daemon at a directory of markdown notes and each cycle it scans the configured roots, diffs against a persisted cursor, and pushes changed notes through the same local prose ingest path used by direct ingestion, so notes become searchable on the node. Deletions are recorded rather than applied.

Why it works this way. This is the first runtime behind a manifest entry kind that had been declared with nothing behind it, and the choice to make it double-gated rather than flag-gated is the interesting part. A single flag would mean one environment variable stands between a daemon and reading arbitrary directories on its host. Requiring both a granted file-watcher capability and an explicit root list means the operator has said yes twice, in two different places, and neither yes on its own does anything. That is more friction than a flag and it is the right amount for a capability whose blast radius is "reads files on this machine".

Recording deletions rather than applying them follows the same instinct as everywhere else in this product: removing a note from your vault should not silently remove evidence from a corpus an agent may have cited. What was removed is recorded so you can decide.

What changes for you.

  • As an operator: your existing notes become a retrievable corpus with no upload, no external service and no copy leaving the machine.
  • As an agent: the vault corpus behaves like any other locally ingested corpus, and its freshness is readable from the watcher's status fact.

What it does not do.

  • Neither gate alone starts it. A granted pack with no roots does nothing; roots with no granted pack do nothing.
  • Deletions are recorded, not applied. A note you delete from disk does not vanish from the corpus as a consequence of this watcher.
  • It watches markdown notes. It is not a general file-system indexer.

Turn it on. Install and grant a file-watcher pack, then set CORECRUXD_VAULT_WATCH_ROOTS. Both are required.

Where the detail lives. The double gate, the cursor, the cycle behaviour and the deletion policy: Extending 7.2. Runtime contract: vault_watcher.rs:18.

Status SHIPPED, behind CORECRUXD_INTEGRATIONS_ENABLED, which defaults on. The sync interval defaults to 900 seconds. Reached through MCP github_search, github_recent_commits, github_open_prs, github_open_issues, github_comments_since; /v1/integrations/github/*; Console → Studio › Integrations. Who it is for operators and agents.

What it does. Connect a GitHub account, select repositories, and the daemon syncs them on a fifteen-minute cycle into a local corpus where commits, pull requests, issues and comments are all first-class searchable objects. The connector's credentials are sealed under a key derived from the passport key. The MCP tools are thin filtering wrappers over the resulting facts, so what an agent searches is the same store everything else uses.

Why it works this way. The alternative is for the agent to call the GitHub API live on every question, which is slower, rate-limited, and gives the agent a general-purpose network capability it does not need. Indexing into the ordinary fact store means retrieval, budgets and privacy scoping all apply for free rather than being reimplemented per connector, and it means a repository's history is queryable at the same cost as anything else in memory. The cost is staleness bounded by the sync interval, which is a much easier property to reason about than rate limits.

Sealing credentials under a passport-derived key rather than storing them in configuration is the other choice worth naming: the token is at rest under a key tied to this daemon's identity, so lifting the file alone does not lift the credential.

What changes for you.

  • As an operator: the integration is on by default at the feature level and still does nothing until you connect an account and select repositories. Connecting is the real gate.
  • As an agent: you can ask what changed in a repository, which pull requests are open, and what was commented since a point in time, without network access and without a GitHub token of your own.

What it does not do.

  • It indexes what you selected. Unselected repositories are not synced, and there is no implicit organisation-wide crawl.
  • Freshness is bounded by the sync interval. A commit pushed a minute ago is not there yet, and the scheduler's first run is one interval after start.
  • It reads GitHub on your credential. That is an outbound call, on your token, to a service you already use, and it is the one connector in this family that talks to a third party by design.

Turn it on. Connect an account, then select repositories. CORECRUXD_INTEGRATIONS_ENABLED is already on; CORECRUXD_GITHUB_SYNC_INTERVAL_SECS tunes the cycle.

Where the detail lives. Connect, select and sync behaviour: Extending 7.3. Credential sealing: Extending 7.5. The tools and their storage shape: github.rs:6.

10.6 Driving the daemon from a harness that does not speak MCP

Status the OpenAI-compatible connector is SHIPPED. The function-calling shim is behind CORECRUXD_OPENAI_SHIM, default off. Reached through /v1/integrations/openai/*; GET /v1/openai/tools.json and POST /v1/openai/invoke. Who it is for agents.

What it does. Two separate things share a name. The connector lets you register an OpenAI-compatible endpoint as a configured provider. The shim, behind CORECRUXD_OPENAI_SHIM and off by default, re-emits the daemon's own tool surface as OpenAI function-calling schemas and executes one tool per invoke call, so a harness that has never heard of MCP can drive the daemon.

Why it works this way. MCP is the daemon's native surface and most harnesses do not speak it, which would normally mean either maintaining a second hand-written tool catalogue or telling those users no. The shim generates its manifest from the live tool registry at request time rather than from a checked-in file, which is the whole design: there is one source of truth for what tools exist, and the OpenAI view is a projection of it. A hand-maintained parallel catalogue would drift on its first change, and drift here means an agent calling a tool that no longer exists.

The manifest is also token-filtered through the same shaping path as the native tool list, so a caller does not see a wider surface by arriving over the compatibility route than it would have over MCP. That closes the obvious hole.

What changes for you.

  • As an agent: if your harness speaks OpenAI function calling, you can use the daemon without an MCP client, and the tools you are offered are the tools you would have been offered natively.
  • As an operator: enabling the shim widens the ways in. It does not widen what any given caller may do.

What it does not do.

  • The shim is off by default. The connector and the shim are different things behind different gates; enabling one does not enable the other.
  • One invoke call executes one tool. It is not a conversational endpoint and it does not run a model.
  • The connector registers a provider. It does not route your traffic through it on its own.

Turn it on. Set CORECRUXD_OPENAI_SHIM=1 for the function-calling routes.

Where the detail lives. The connector: Extending 7.4. The shim's two routes and their shapes: API 7.4. Generation-at-request-time contract: openai_shim.rs:10.

10.7 Letting an agent orient itself in an unfamiliar tree

Status SHIPPED. Active repository watching is feature-gated. Reached through MCP register_repo, list_repos, get_workspace_storyline; /v1/repos*, /v1/workspace/scan, /v1/workspace/storyline. Who it is for operators and agents.

What it does. Register a local repository and the daemon scans it into a code map with dependants, optionally watching it for changes. Separately, a workspace scan walks a tree and emits structured facts about crates, modules, files, symbols, dependencies, stubs and dead code, and the latest scan is stored as a single fact so the context-graph endpoint can fold it in. A storyline composes that material into something an agent can read to work out where it is.

Why it works this way. An agent dropped into an unfamiliar repository spends its first several thousand tokens rediscovering structure that is entirely derivable and entirely stable between runs. Deriving it once, storing it as facts, and handing back a storyline converts that recurring cost into a one-off. Storing the scan as a fact rather than as a special-purpose artefact is the choice that matters: it means the scan participates in the ordinary graph, budgets and retrieval rather than being a side channel.

What changes for you.

  • As an agent: you can ask what this workspace is before you read any of it, and the answer costs a fact read rather than a directory walk.
  • As an operator: a registered repository has a code map with dependants, which is the view that answers "what breaks if I change this".

What it does not do.

  • Registration and scanning are separate concerns; a repository can be registered with its scan deferred or failed, and it stays registered either way. Check the scan status rather than assuming registration implies a code map.
  • Active watching is feature-gated. Without it, a scan is a point-in-time artefact that goes stale silently.
  • A storyline is composed from a scan. It is not a substitute for reading the code, and it is only as current as the scan behind it.

Where the detail lives. Repository registry and code map: API 8.4. Workspace scan and storyline: API 8.5. Scan storage contract: workspace.rs:6.

10.8 One box with a model, backing several without sharing a database

Status FLAG CORECRUXD_COMPUTE_PROVIDER, default off. The route stays mounted when disabled and returns an explicit capability-disabled response. Reached through POST /v1/compute/embed, scope compute:embed; consumers set CORECRUXD_EMBED_DELEGATE_URL. Who it is for operators.

What it does. Behind CORECRUXD_COMPUTE_PROVIDER, default off, a daemon serves embeddings to another daemon over an authenticated endpoint capped at 512 KiB per request. Successful calls are bound to a durable signed observation receipt containing hashes and profile metadata only; the caller's text is never copied into the audit log. On the consumer side, a delegate URL points one daemon's embedder at another's.

Why it works this way. Running an embedding model is the one part of this system with a real hardware appetite, and the obvious workaround, pointing several daemons at one shared database, couples their storage and their failure modes together for the sake of sharing a GPU. Delegating just the embedding call keeps each daemon's data on its own disk and shares only the compute. Leaving the route mounted when the provider is disabled is a small but deliberate choice: a caller gets an explicit runtime capability response rather than a 404 or a 501, so "not enabled here" is distinguishable from "wrong URL" and from "not built".

The receipt contents follow the same rule as the rest of the trust plane. Recording hashes and profile metadata rather than text means the audit trail proves a call happened and what shape it had, without the audit trail becoming a second copy of the data.

What changes for you.

  • As an operator: one machine with a model can back several without any of them sharing storage, and the provider is off until you say otherwise.
  • As an agent: nothing. Delegation is invisible above the embedder interface.

What it does not do.

  • It is off by default, and a disabled provider answers with a capability response rather than an error you can mistake for a routing problem.
  • Requests are capped at 512 KiB. Batch accordingly.
  • It shares compute, not memory. Two daemons using one provider still have entirely separate stores.

Turn it on. Set CORECRUXD_COMPUTE_PROVIDER=1 on the serving daemon and CORECRUXD_EMBED_DELEGATE_URL on each consumer.

Where the detail lives. The route, its scope and its response shapes: API 3.5. Provider contract and the receipt policy: compute.rs:6.

Sources

Grounding for each capability is carried in that capability's closing line, next to the claim it supports, rather than gathered here. Every source link resolves against origin/main of the Crux repository.