Selbst gehosteter Sync
Sync ist die optionale Hälfte von OHarness: Sie speichert Gespräche, damit eine auf einem Rechner begonnene Sitzung auf einem anderen fortgesetzt werden kann. Der Client reserviert https://sync.oharness.ai als offiziellen Standard, doch dieser Dienst ist eine Bequemlichkeit, keine Abhängigkeit – der Server ist das npm-Paket @oharness/sync, veröffentlicht unter der MIT-Lizenz, und ein Harness auf Ihre eigene Instanz auszurichten, sind zwei Zeilen Konfiguration.
Nichts davon ist erforderlich. Ein abgemeldetes Harness ohne selbst gehostetes Token öffnet nie eine Sync-Verbindung, und der lokale Sitzungsspeicher ist für sich vollständig. Die Anmeldung schaltet Sync standardmäßig ein und verwendet das offizielle Ziel, sofern der Nutzer es nicht überschreibt oder Sync abschaltet.
Was der Server ist und was nicht
Er speichert undurchsichtige Blobs und gibt sie der Reihe nach zurück. Er führt nie ein Modell aus, führt nie etwas aus und schaut nie in das hinein, was er speichert – body bleibt auf dem Übertragungsweg vom Server unberührt (packages/engine/src/sync/protocol.ts).
Das ist die Eigenschaft, die es zu schützen gilt, nicht bloß eine Beschreibung der aktuellen Implementierung. Sie macht den Server sicher genug, um ihn laufen zu lassen, sie erlaubt Änderungen am Nachrichtenformat ohne erneutes Deployment, und sie würde einen Ende-zu-Ende-verschlüsselten Modus zu einer reinen Client-Änderung machen. Eine Funktion, für die dieser Dienst einen Verlauf parsen muss, gehört woandershin.
Zwei Dinge ist er bewusst nicht:
Er ist kein Kontosystem. Es gibt keinen Registrierungs-Endpunkt, denn ein Sync-Server, der Konten anlegen kann, ist ein Sync-Server mit einem Registrierungs-Endpunkt, den man verteidigen muss. Tokens werden auf separatem Weg ausgegeben – siehe unten.
Er ist nicht der Agent-Server. Der hält eine Shell und ist von Grund auf für einen einzigen Mandanten ausgelegt. Siehe railway.agent.toml.
Schnellstart
npx @oharness/sync token alice # printed once, stored as a hash
npx @oharness/sync --port 4483 # serve
Dann in ~/.oharness/config.json:
{
"sync": {
"url": "https://sync.example.com",
"token": "<the token printed above>"
}
}
Hat die Nutzerkonfiguration keine sync.url, verlegt OHARNESS_SYNC_URL das Ziel, ohne eine Datei zu bearbeiten. Sind beide vorhanden, gewinnt bewusst die Nutzerkonfiguration. OHARNESS_SYNC=0 schaltet Sync für einen Lauf oder für die gesamte CI ab. Starten Sie OHarness nach einer Änderung des Ziels neu.
Die URL wird auf dem Client validiert: HTTPS ist Pflicht, einfaches HTTP ist nur für Loopback-Entwicklung erlaubt. Eingebettete Anmeldedaten, eine Query oder ein Fragment werden abgelehnt statt entfernt.
Das Protokoll auf der Leitung
Der Sitzungs-Sync hat vier Endpunkte. Memory fügt drei optionale hinzu. Alles außer /v1/health erfordert Authorization: Bearer.
| Route | Zweck |
|---|---|
GET /v1/health | Erreichbarkeit. Absichtlich ohne Authentifizierung – einen Health-Check, der eine Anmeldeinformation braucht, bindet niemand ein |
GET /v1/projects | Projekt-IDs, in die bereits etwas übertragen wurde, unter einem Eigentümer |
POST /v1/push | Datensätze und Sitzungsmetadaten anhängen; gibt den neuen Cursor und die Anzahl neuer Einträge zurück |
GET /v1/pull | Ab einem Cursor vorwärts lesen; wait=<seconds> für Long Polling |
POST /v1/memory/identity | Optionale Memory-Identitäts- und Kontingentprüfung |
POST /v1/memory/snapshot | Optionales Memory-Manifest und geänderte Notizinhalte |
POST /v1/memory/commit | Optionale Memory-Aktualisierung per Compare-and-Swap |
Ein Feed ist nur erweiterbar, und ein Cursor ist eine undurchsichtige, monoton steigende, vom Server vergebene Ganzzahl. Der mitgelieferte JSONL-Speicher verwendet eine Ereignisnummer und pflegt separat Checkpoints mit Byte-Offsets; eine andere Implementierung kann eine Datenbanksequenz verwenden. Pushes werden über (session, id) dedupliziert, sodass eine Wiederholung nach einer verlorenen Antwort nichts kostet, statt einen Zug zu verdoppeln. Sitzungen werden nach Generation per Last-Writer-Wins aufgelöst, und eine Löschung reist als Tombstone – ohne ihn stellt das andere Gerät wieder her, was Sie gelöscht haben.
Der vollständige Interoperabilitätsvertrag mit den Formen von JSON-Anfragen und -Antworten, Cursor-Semantik, Fehlern, Memory-Hashing und einer Checkliste für Implementierungen steht unter Sync-Protokoll. Lesen Sie diese Seite, wenn Sie einen kompatiblen Dienst bauen, statt @oharness/sync zu deployen.
Die Limits stehen in packages/engine/src/sync/protocol.ts und werden von beiden Hälften geteilt, denn ein Client und ein Server, die jeweils eine eigene Vorstellung vom Protokoll haben, driften auseinander, und diese Drift zeigt sich als Datensätze, die stillschweigend nicht zusammengeführt werden:
MAX_PUSH_RECORDS | 500 Einträge pro Push |
MAX_BODY_BYTES | 8 MiB pro Anfrage |
DEFAULT_PULL_LIMIT | 500 Einträge pro Pull |
MAX_WAIT_SECONDS | 30 s, unter jedem sinnvollen Idle-Timeout eines Proxys |
Ein zu großer Push wird mit 413 abgelehnt statt gekürzt: Einen halben Stapel anzunehmen und Erfolg zu melden, würde Züge verlieren, die der Client dann für zugestellt hält.
Feeds werden nach <root>/feed/<kind>-<owner>/<project>.jsonl geschrieben, wobei --root standardmäßig ~/.oharness/sync ist. Auf einer Plattform, die Containern ein flüchtiges Dateisystem gibt, legen Sie das auf ein eingehängtes Volume. Geht es verloren, wird nicht bloß ein Cache geleert – jeder Client hält dann einen Cursor in einen Feed, den es nicht mehr gibt.
Zwei Arten, ein Token zu halten
Lokal ausgegeben. oharness-sync token <user> schreibt neben die Feeds eine users.json mit der Nutzer-ID und einem SHA-256 des Tokens; das Token selbst wird nie gespeichert und einmal ausgegeben. Ein Eintrag ist der Einzelnutzer-Fall und kostet nichts. Das braucht keinen weiteren Dienst und ist der Modus der Wahl, sofern Sie nicht bereits ein Kontosystem haben.
Von etwas anderem signiert. Setzen Sie OHARNESS_SYNC_JWT_SECRET (HS256) oder OHARNESS_SYNC_JWKS_URL (asymmetrisch), und der Server akzeptiert zusätzlich Tokens, die Ihr Auth-System signiert hat, verifiziert über die Signatur statt durch Nachfrage bei diesem System – nichts auf dem Anfragepfad hängt also davon ab, dass ein anderer Dienst läuft. sub wird zur Eigentümer-ID. OHARNESS_SYNC_JWT_ISSUER und OHARNESS_SYNC_JWT_AUDIENCE werden durchgesetzt, wenn sie gesetzt sind.
Die beiden Formen werden an ihrer Gestalt unterschieden: Ein lokal ausgegebenes Token besteht aus 32 zufälligen Bytes in base64url und kann nie einen Punkt enthalten. JWT zu aktivieren, deaktiviert users.json nicht; beide funktionieren weiter.
Teams
Ein Team ist ein geteilter Feed. Alle, denen ein Token mit derselben Team-ID ausgegeben wurde, lesen und schreiben einen gemeinsamen Verlauf pro Projekt – dadurch können zwei Personen die Sitzungen des jeweils anderen fortsetzen.
npx @oharness/sync token bob --team platform
Bei signierten Tokens stammen die IDs aus einem teams-Claim, umbenennbar mit OHARNESS_SYNC_TEAMS_CLAIM. Die Mitgliedschaft wird am Token festgehalten, statt bei jeder Anfrage erfragt zu werden – die Alternative stellt die Verfügbarkeit eines anderen vor jeden Schreibvorgang, und das auf genau dem Endpunkt, der Verläufe dauerhaft machen soll.
Die Angabe eines Teams, das das Token nicht enthält, wird mit 403 abgelehnt, statt stillschweigend mit dem eigenen Feed des Aufrufers beantwortet zu werden. Diese Ersetzung liest sich für den Nutzer wie „das Team enthält nichts“ – die eine falsche Antwort, auf die er hin handeln würde.
Memory braucht mehr als der Feed
/v1/memory/* wird vom selben Prozess bedient, ist aber nicht das Feed-Protokoll: ein per Compare-and-Swap aktualisiertes Manifest plus unveränderliche Markdown-Objekte in einem privaten Bucket, mit drei Aktionen – identity, snapshot und commit.
Es hat Anforderungen, die das Feed-Protokoll nicht hat, und die sollten Sie kennen, bevor Sie damit planen:
OHARNESS_SYNC_AUTH_URL Supabase project origin, bare — no path or query
OHARNESS_SYNC_AUTH_KEY publishable/anon key, never a service role
R2_ACCOUNT_ID Cloudflare account id, 32 hex characters
R2_MEMORY_BUCKET private bucket, public access disabled
R2_ACCESS_KEY_ID bucket-scoped Object Read & Write credential
R2_SECRET_ACCESS_KEY matching secret
Die Authentifizierung ist auf Supabase zugeschnitten, nicht generisch. Das Feed-Protokoll akzeptiert jedes JWKS, auf das Sie es richten; Memory validiert ein Token durch einen Aufruf von /auth/v1/user am konfigurierten Origin – das ist die API von Supabase, kein OIDC-Standard. Hosten Sie selbst mit einem anderen Identitätsanbieter, funktionieren Sitzungs-Feeds, Memory aber nicht.
Die Unterscheidung gilt auch für lokal ausgegebene Anmeldedaten: oharness-sync token <user> genügt für Sitzungs-Feeds, doch der aktuelle Memory-Client sendet sein Cloud-Kontotoken statt sync.token oder OHARNESS_SYNC_TOKEN. Ein selbst gehostetes Memory-Deployment braucht daher eine Cloud-Anmeldung und einen Identitätsprüfer, die dieses Token auf denselben Principal auflösen.
Der Speicher ist S3-kompatibel und wird als R2 angesprochen. Die Schlüssel lauten users/<verified-user-id>/projects/<project-id>/memory/ für Projektnotizen und users/<verified-user-id>/memory/ für die eigenen Notizen eines Nutzers, wobei die Nutzer-ID aus dem verifizierten Token stammt und nicht aus irgendetwas, das der Client sendet.
Aktuelle Einschränkung des Adapters. Protokoll und Memory-API unterstützen beide Präfixe, aber die Schlüssel-Allowlist des mitgelieferten createR2MemoryStore akzeptiert in dieser Quellcode-Revision noch nur das Projektpräfix. Erweitern Sie diese Allowlist und ergänzen Sie einen Test mit dem echten Adapter, bevor Sie Memory auf Nutzerebene mit R2 aktivieren; eine unabhängige Implementierung sollte beide vom Protokoll beschriebenen Präfixe unterstützen.
Fehlende Konfiguration bleibt still. Der Speicher-Client wird bei der ersten Memory-Anfrage gebaut, nicht beim Start; ein Deployment, dem eine Variable abhandengekommen ist, wirkt also völlig gesund – der Port öffnet sich, /v1/health antwortet, Feeds synchronisieren, und Memory gibt im Client des Nutzers 503 zurück, genau dort, wo niemand hinschaut, der das deployt. Der Server gibt beim Start eine Zeile aus, die die fehlenden Variablen nennt:
memory off (no Memory environment) # nothing set: the ordinary case
! memory off — missing R2_SECRET_ACCESS_KEY # some set: a mistake worth finding
memory my-bucket · auth project.supabase.co · 5000 notes · 90d tombstones # configured
Keine der sechs zu setzen, ist der gewöhnliche selbst gehostete Fall und wird entsprechend leise gemeldet. Einige davon zu setzen, ist der Fall, der eine Randnotiz verdient. Prüfen Sie das Deploy-Log; memory off dort ist die ganze Warnung.
Kontingente legen Sie fest. Jedes ist optional. Antworten nennen die vier Limits, auf die der Client reagieren kann; Byte-Limits für Anfragen und Antworten bleiben serverseitige Sicherheitsgrenzen:
OHARNESS_SYNC_MEMORY_MAX_NOTES live notes per project and per user (5000)
OHARNESS_SYNC_MEMORY_MAX_CHANGES changes per commit (500)
OHARNESS_SYNC_MEMORY_MAX_NOTE_BYTES bytes per note body (131072)
OHARNESS_SYNC_MEMORY_MAX_RESPONSE_BYTES note bytes per response (16777216)
OHARNESS_SYNC_MEMORY_MAX_REQUEST_BYTES request body bytes (4194304)
OHARNESS_SYNC_MEMORY_TOMBSTONE_DAYS days a deletion is kept (90)
Ein Kontingent blockiert Wachstum, nie Löschungen; ein gesenktes Kontingent kann einen Nutzer darüber also nicht festsetzen. Eine ratenbegrenzte Anfrage erhält 429 mit Retry-After, den verbleibenden Sekunden im Ein-Minuten-Fenster des Kontos.
Tarife schränken das Kontingent ein. Kostenlose Konten erhalten die Limits aus packages/sync/src/memory-entitlements.json, ausgelesen aus dem ent-Claim, den der Access-Token-Hook der offiziellen Cloud schreibt. Schreibt Ihr Auth-System keinen solchen Claim, sieht jedes Konto kostenlos aus; setzen Sie Folgendes, um stattdessen jedem Konto die kostenpflichtigen Limits zu geben:
OHARNESS_SYNC_MEMORY_ENTITLEMENTS=off
Ein Wert, der keine positive Ganzzahl ist, wird ignoriert und in der Startzeile genannt. Deployen Sie einen neuen Server vor neuen Clients: Ältere Server lehnen die Felder ab, die neuere Clients für Notizen auf Nutzerebene und typisierte Notizen senden.
Das clientseitige Verhalten von Memory – Local-first-Notizen, der Sync-Schalter pro Verzeichnis, Drei-Wege-Abgleich und Konfliktbehandlung – hat eine eigene Seite.
Was ein Projekt nicht entscheiden kann
sync.url, sync.token und sync.team werden beim Zusammenführen aus der Projektkonfiguration verworfen, und das ist kein Versehen: Den Sync-Server in einem Repository zu benennen hieße, dass ein Repository Ihre Verläufe anderswohin schicken kann. Sie werden nur aus der Nutzerkonfiguration (~/.oharness/config.json) und der Umgebung übernommen.
Die praktische Folge beim Selbsthosten für ein Team: Sie können die Adresse Ihres Servers nicht in einer eingecheckten oharness.config.json ausliefern und erwarten, dass alle sie übernehmen. Jede Person setzt sie einmal in ihrer Nutzerkonfiguration, oder Sie verteilen OHARNESS_SYNC_URL über das, was ohnehin ihre Shell konfiguriert.
Ein Projekt kann Sync weiterhin abschalten, exclude ergänzen und deny ergänzen – Verschärfen ist erlaubt, Lockern nicht. Die Logik steckt in projectSafe() in packages/engine/src/config.ts.
Hinweise für den Betrieb
Terminieren Sie TLS davor. Der Server spricht einfaches HTTP und authentifiziert mit einem Bearer-Token. Ein im Klartext über ein Netz gesendetes Token, das Sie nicht kontrollieren, ist ein Token, das jetzt jeder auf diesem Weg hat. Binden Sie nur hinter einem Proxy, der Ihnen gehört, breiter als Loopback (--host 0.0.0.0).
Eine Mandantengrenze ist hier ein Schlüsselpräfix, kein Container. Mandantenfähigkeit bedeutet einen Prozess, der Nutzer über das Feed-Verzeichnis trennt. Das ist solide für ein Team, das einander bereits vertraut, und nicht das Modell der Wahl, wenn Ihre Mandanten das nicht tun.
Neustarts kosten wenig. Der Server hält nichts im Arbeitsspeicher, was ein Client nicht aus seinem Cursor neu ableiten kann, und ein abgebrochener Long Poll wird wiederholt. Starten Sie ruhig neu; verlieren Sie nur --root nicht.
Er liest keine Konfigurationsdatei. Was ihn deployt – die Umgebung eines Containers, eine systemd-Unit, der Variablenspeicher einer Plattform –, ist die einzige Konfigurationsquelle, denn ein Sync-Server, der eine Datei aus dem Arbeitsverzeichnis liest, ist ein Sync-Server, dessen Ziel ein Repository ändern kann.
railway.sync.toml im Wurzelverzeichnis des Repositorys ist ein ausgearbeitetes Beispiel für all das in einem Deployment.