Skip to content

Remote sync

knomit is a distributed, multi-machine knowledge base. Each machine writes only its own agent branch (agent/<hostname>-<key-fingerprint8>); main is consensus. Syncing with a git remote (“origin”) is how agent branches reach and absorb that consensus. Configure it from the web UI (the gear icon in the top bar) or with environment variables.

  • This machine is the sole writer of its agent branch.
  • main (or whatever the upstream branch is named) is never written directly by the MCP server.
  • Reconciliation is merge-based, not replay-based: in steady state the agent branch merges main with a local-wins strategy (the local agent’s edits win overlapping paths), producing a fast-forward, a no-op, or a single merge commit — O(1) regardless of how many commits main absorbed.
  • A zero-diff merge is reported as a no-op and never creates a merge commit, which is what makes squash-merged upstreams fast-forward correctly.
MethodURL formatCredentials
SSHgit@github.com:user/repo.gitknomit’s SSH key (~/.knomit/id_ed25519); auto-detected for git@… / ssh:// URLs
Token (GitHub PAT, etc.)https://github.com/user/repo.gita PAT, sent as HTTP basic auth with username x-token; auto-detected for https:// URLs with a token present
Basichttps://github.com/user/repo.gituser:password, stored in one field and split on the first colon
None / anonymousany public URLno credentials (public repos)

The auth_method is inferred from the URL and fields when left empty.

SSH remotes verify the server’s host key against an OpenSSH known_hosts file, on a trust-on-first-use model:

SituationBehaviour
Host not yet listedAccepted, and its key is appended to known_hosts
Host listed under the same keyAccepted
Host listed under a different keyRejected — the sync fails
known_hosts unusable (bad path, unparseable)Hard error; there is no insecure fallback

The file is set by [remote] known_hosts / KNOMIT_REMOTE_KNOWN_HOSTS and defaults to <KNOMIT_HOME>/known_hosts; point it at ~/.ssh/known_hosts to share pins with your shell’s git. It is re-read on every connection, so an edit takes effect without a restart. Each first-use pin is logged with the key’s SHA-256 fingerprint — verify that fingerprint out-of-band if the remote is untrusted. See Configuration.

Auth tokens are never stored in plaintext. They are encrypted with AES-256-GCM using a key derived (HKDF-SHA256) from the agent’s SSH private key (id_ed25519), and stored base64-encoded in control.db’s repo_origins table — one machine-local control plane for every repo, not a copy per repo database. If the SSH key is unreadable at boot, the encryptor is unavailable and setting a remote refuses to persist any auth token rather than writing it in the clear. (Legacy plaintext values are still read, with a warning.)

This is one of several guarantees that hold at the sync boundary — the local origin gate and host-key pinning above are the others. See Security & privacy for how they fit together.

An origin is attached at runtime — when the repo is created from a remote (below), through the origin REST endpoints, or through the web UI’s “Connect Remote” flow. There is no config key or environment variable for the URL: config holds only the credentials used to reach it ([remote] in knomit.toml, or the KNOMIT_REMOTE_* variables). The stored record carries:

FieldMeaning
URLGit remote URL
BranchUpstream consensus branch — default main, configurable (e.g. master); changeable later via PATCH /origin/upstream
Interval / PushIntervalPull / push cadence (seconds); a single reconcile loop ticks at the shorter of the two and does both
AuthMethodtoken · basic · ssh · none (inferred if empty)
AuthTokenEncrypted at rest (see above)
LastSyncAt / LastStatus / LastError (+ push variants)Last-outcome bookkeeping, surfaced through the gear-icon indicator

On clone/init, knomit prefers main over a remote agent-branch HEAD as the consensus upstream, and syncs both the agent branch and the upstream.

PATCH /origin/upstream takes a {"branch": "…"} body and changes only the upstream branch — auth is untouched and the connect flow is not re-run. The branch is validated against a conservative subset of git’s ref-name rules (control characters, spaces, ~ ^ : ? * [ \, a leading - or /, a trailing /, and the .. / @{ sequences are rejected with 400). The reconcile loop re-reads the record each tick, so the change takes effect on the next cycle.

Cloning from a local filesystem path is disabled by default. To allow it, set local_origin_root / KNOMIT_LOCAL_ORIGIN_ROOT to an absolute directory; a local origin is then accepted only if it resolves (symlinks evaluated) to a path inside that root. Network origins (https://, ssh://, git://, git@host:path) always pass. Every clone/fetch path is gated — there is no trusted exemption.

The “Connect Remote” flow is session-based and ephemeral (≈10-min idle expiry):

  1. POST /repos/{repo}/origin-sessions with the URL + credentials → a session.
  2. GET …/test checks connectivity; GET …/preview shows one merge summary — counts of local-only / remote-only / shared-path / dead-ref facts. The conflict strategy is not a preview input; it is an argument to apply.
  3. POST …/apply then …/commit performs the merge, taking conflict_strategy in the apply body.

…/test, …/preview, …/apply and …/commit are SSE streams (text/event-stream), each ending in a done event carrying its result. The session endpoints below serve application/hal+json, and errors as application/problem+json:

EndpointResponse shape
GET /repos/{repo}/origin-sessionsHAL collection — a count plus the sessions under _embedded.sessions, not a bare JSON array
POST /repos/{repo}/origin-sessionssession_id plus _links
GET …/origin-sessions/{sessionID}Session detail (state, url, and any history / last_preview / last_apply) plus _links

Disjoint histories (e.g. reconnecting after a token refresh against a repo with no shared history) are reconciled via a cursor replay with one-level dead-ref resolution.

A per-repo reconcile loop re-reads the remote config fresh every tick, runs an initial synchronous reconcile first (so credential errors surface immediately), and escalates its log level WARN→ERROR after five consecutive failures. Sync uses a compare-and-swap watermark on last_commit.

Pull and push share one loop rather than running on separate timers: every tick syncs, then pushes (push is skipped in read-only mode). The loop interval is the shorter of Interval and PushInterval — a non-positive PushInterval is ignored, and if neither yields a positive value the interval falls back to 300s.

Set KNOMIT_GIT_SERVE (default on, optionally with KNOMIT_GIT_PORT) to expose a git smart-HTTP endpoint at /git, so other knomit instances or git clients can clone and fetch directly from this server.

This endpoint is also what knomit-okf reads: the KB URL you hand clone is <server>/git/<repo>, and it is unauthenticated, so an export against your own server needs no credentials.

Create a repo directly from a remote through the REST API, in one of two modes. They are the two halves of a single question about the branch you name — does it already carry .knomit/ontology.yaml? — which POST /api/v1/repos:probe-initialized answers.

clone joins a branch that is already a knowledge base. Its ontology governs, and passing ontology_preset or ontology_yaml is refused rather than silently dropped:

Terminal window
curl -N -X POST http://localhost:19278/api/v1/repos \
-H 'Content-Type: application/json' \
-d '{"name":"work","mode":"clone",
"origin":{"url":"git@github.com:you/kb.git","branch":"main"}}'

initialize turns a branch that is not a knowledge base into one. It clones the branch, cuts agent/<host> from its tip, commits the chosen ontology there, and pushes that branch alone. An ontology_preset or ontology_yaml is required — writing that file is the act that makes the branch a knowledge base, so without one there is nothing to write:

Terminal window
curl -N -X POST http://localhost:19278/api/v1/repos \
-H 'Content-Type: application/json' \
-d '{"name":"work","mode":"initialize","ontology_preset":"code",
"origin":{"url":"git@github.com:you/project.git","branch":"main"}}'

Neither mode mints a new identity: the repo id is the remote’s existing root commit, so two machines pointed at the same remote converge on one identity instead of splitting into two.

See the REST API reference for the full repo-management surface.