Sync-Protokoll
Diese Seite ist der Interoperabilitätsvertrag für alle, die einen Sync-Dienst für OHarness implementieren. Sie beschreibt, was der Client sendet und auf welches Verhalten er sich verlässt; den JSONL-Speicher von @oharness/sync setzt sie nicht voraus.
Unter einem Origin laufen zwei unabhängige Protokolle:
- Sitzungs-Sync spiegelt Verläufe, Artefakt-Revisionen und Sitzungsmetadaten über einen nur erweiterbaren Feed.
- Memory-Sync gleicht Markdown-Notizen über ein Manifest und Compare-and-Swap ab. Er ist optional. Ein Server kann Sitzungs-Sync implementieren, ohne Memory zu implementieren.
Die Base-URL enthält keinen Pfad, keine Query, keine Anmeldedaten und kein Fragment. Produktive Clients akzeptieren nur HTTPS; einfaches HTTP wird für die Entwicklung mit localhost, 127.0.0.1 und [::1] akzeptiert. Die Beispiele unten verwenden https://sync.example.com.
Auf dieser Seite
- [Kompatibilitätsziel](#compatibility-target)
- [Authentifizierung und Mandanten](#authentication-and-tenancy)
- [Datenmodell des Sitzungs-Syncs](#session-sync-data-model)
- [Sitzungs-Endpunkte](#session-endpoints)
- [Fehlerantworten des Sitzungs-Syncs](#session-error-responses)
- [Memory-Sync](#memory-sync)
- [Einen OHarness-Client konfigurieren](#configuring-an-oharness-client)
- [Checkliste für Implementierungen](#implementation-checklist)
- [Keine Anforderungen](#non-requirements)
Kompatibilitätsziel
Dieses Dokument beschreibt Protokollversion 1, wie sie OHarness 0.5.x verwendet. Derzeit gibt es weder einen Capabilities-Endpunkt noch eine Content-Negotiation. Halten Sie /v1/ abwärtskompatibel: Fügen Sie optionale Felder hinzu, ändern Sie nicht die Bedeutung eines bestehenden Feldes und machen Sie ein bisher optionales Feld nicht zur Pflicht.
Unbekannte Anfragefelder sollten beim Sitzungs-Sync ignoriert werden. Memory ist strenger und lehnt sie mit 400 {"code":"unknown_field"} ab; neue Clients nutzen diese Antwort, um einen älteren Memory-Server zu erkennen und einen Projekt-Snapshot ohne die have-Optimierung zu wiederholen.
Alle Zeitstempel sind Unix-Millisekunden. Alle Inhalte sind JSON. Erfolgreiche Antworten verwenden Content-Type: application/json; Clients hängen nicht von bestimmtem JSON-Whitespace oder einer bestimmten Reihenfolge der Eigenschaften ab.
Authentifizierung und Mandanten
Jede Route außer GET /v1/health erfordert:
Authorization: Bearer <token>
Ein gültiges Token wird auf genau eine stabile Principal-ID aufgelöst. Der Server muss den Eigentümer aus dem verifizierten Token ableiten, nie aus einer in einer Anfrage mitgeschickten Nutzer-ID. Ein Sitzungstoken kann zusätzlich Team-IDs enthalten.
Ohne team richten sich Sitzungsanfragen an den persönlichen Feed des Principals. Mit team richten sie sich erst nach Prüfung der Mitgliedschaft an den Feed dieses Teams. Ein unbekanntes oder nicht berechtigtes Team gibt 403 zurück; es darf nicht stillschweigend auf den persönlichen Feed ausweichen.
Die Authentifizierung ist über den Bearer-Transport hinaus bewusst nicht standardisiert. Der mitgelieferte Server akzeptiert lokal ausgegebene undurchsichtige Tokens, HS256-JWTs und über JWKS verifizierte JWTs. Eine andere Implementierung kann eine Datenbank oder einen anderen Token-Verifier verwenden, solange die beobachtbare Semantik von Principal und Team dieselbe ist.
Memory hat eine strengere Identitätsprüfung, die unten beschrieben wird. Insbesondere genügt ein von oharness-sync token ausgegebenes undurchsichtiges Token für den Sitzungs-Sync, doch der aktuelle Memory-Client von OHarness verwendet sein Cloud-Kontotoken.
Datenmodell des Sitzungs-Syncs
Datensatz
Ein Datensatz ist ein unveränderliches Element einer Sitzung. Heute enthält er normalerweise einen Verlaufsdatensatz oder einen Umschlag für eine Artefakt-Revision, doch der Server muss body als undurchsichtiges JSON behandeln.
{
"session": "20260914_abcd",
"id": "stable-record-id",
"seq": 12,
"device": "device-id",
"at": 1789344000000,
"body": { "opaque": "to the server" }
}
| Feld | Typ | Bedeutung |
|---|---|---|
session | string | Sitzung, die den Datensatz enthält |
id | string | Stabile ID für die Deduplizierung bei Wiederholungen |
seq | number | Lamport-Sequenz innerhalb der Sitzung |
device | string | Gerät, das den Datensatz erzeugt hat |
at | number | Erstellungszeit in Unix-Millisekunden |
body | beliebiges JSON | Nutzlast, unverändert gespeichert und zurückgegeben |
Der Deduplizierungsschlüssel ist (session, id). Ein erneuter Push dieses Schlüssels darf kein zweites Ereignis anhängen, auch wenn eine vorherige Antwort verloren ging. Parsen, normalisieren oder umschreiben Sie body nicht.
Sitzungsmetadaten
{
"id": "20260914_abcd",
"gen": 1,
"updatedAt": 1789344000000,
"body": { "opaque": "session metadata" }
}
body ist undurchsichtig. deleted: true ist ein Tombstone und hat normalerweise keinen Inhalt. Metadaten folgen Last-Writer-Wins anhand dieses Tupels, in absteigender Priorität:
- Größeres
gengewinnt. Eine Verdichtung oder Neuschreibung erhöht die Generation. - Innerhalb einer Generation gewinnt das größere
updatedAt. - Ein gleiches Tupel ist bereits vorhanden und fügt kein Ereignis hinzu.
Die gewinnende Aktualisierung, auch ein Tombstone, wird an den Feed angehängt, damit Geräte, die bereits einen älteren Wert gelesen haben, die Änderung bemerken.
Cursor
Ein Cursor ist eine nicht negative Ganzzahl, die der Server vergibt. Er bezeichnet eine Position in einem Feed (owner, project). Der Client behandelt ihn als undurchsichtig: Er speichert ihn, sendet ihn als since zurück und erwartet nur, dass er nie rückwärts läuft.
Die mitgelieferte JSONL-Implementierung verwendet eine fortlaufende Ereignisnummer. Eine Datenbankimplementierung kann eine Sequenz verwenden. Ein Cursor muss kein Byte-Offset, Zeitstempel oder seq eines Datensatzes sein.
Cursor gehören zu einem Feed und dürfen nicht über Nutzer, Teams oder Projekte hinweg wiederverwendet werden. Der Feed-Speicher ist dauerhafter Zustand. Verliert ein Server einen Feed, während ein Client dessen Cursor behält, kann der Client dem neuen Feed dauerhaft voraus sein; Protokoll v1 hat keinen Handshake zum Zurücksetzen des Cursors.
Sitzungs-Endpunkte
GET /v1/health
Erreichbarkeits-Endpunkt ohne Authentifizierung.
HTTP/1.1 200 OK
Content-Type: application/json
{"ok":true}
Das belegt, dass Prozess und Route erreichbar sind. Es belegt nicht, dass ein Token funktioniert, dass persistenter Speicher eingehängt ist oder dass Memory konfiguriert ist.
GET /v1/projects
Listet die Projekte auf, die für den gewählten Eigentümer einen Feed haben.
GET /v1/projects
GET /v1/projects?team=platform
Antwort:
{"projects":["project-a","project-b"]}
Geben Sie ein leeres Array zurück, wenn der Eigentümer keine Feeds hat. IDs, die aktuelle OHarness-Clients erzeugen, passen auf [A-Za-z0-9_-]{1,128} und sollten unverändert zurückgegeben werden. Ein Server kann andere Eingaben ablehnen oder umkehrbar kodieren; hängen Sie eine nicht vertrauenswürdige ID nie an einen Dateisystempfad an.
Der aktuelle Spiegel pro Projekt ruft diese Route in seiner normalen Push/Pull-Schleife nicht auf, sie gehört aber zum öffentlichen Client und ist für eine vollständige Implementierung und die geräteübergreifende Erkennung von Projekten erforderlich.
POST /v1/push
Hängt neue Datensätze und gewinnende Sitzungsmetadaten atomar an.
{
"project": "project-a",
"team": "platform",
"records": [
{
"session": "20260914_abcd",
"id": "record-12",
"seq": 12,
"device": "laptop",
"at": 1789344000000,
"body": { "role": "user", "content": "hello" }
}
],
"sessions": [
{
"id": "20260914_abcd",
"gen": 0,
"updatedAt": 1789344000000,
"body": { "id": "20260914_abcd", "title": "Example" }
}
]
}
team, records und sessions sind optional. Fehlende Arrays gelten als leere Arrays. Erforderlich ist mindestens project. Ältere Typdeklarationen enthielten ein device auf oberster Ebene; der OHarness-Client sendet es nicht, und kompatible Server dürfen es nicht verlangen. Die Geräteidentität steht in jedem Datensatz.
Antwort:
{"cursor":42,"accepted":2}
cursor ist das aktuelle Ende des Feeds nach diesem Push. accepted zählt neue Datensatz-Ereignisse plus gewinnende Ereignisse für Sitzungsmetadaten. Doppelte Datensätze und veraltete oder gleiche Sitzungsmetadaten zählen nicht.
Die gesamte Anfrage ist wiederholbar. Validieren Sie sie, bevor Sie einen Eintrag festschreiben. Nehmen Sie keinen Teilstapel an: Geben Sie für die ganze Anfrage einen Fehler zurück, wenn ein Eintrag fehlerhaft ist oder ein Limit überschritten wird.
Limits des aktuellen Clients und des mitgelieferten Servers:
| Limit | Wert |
|---|---|
| Datensätze pro Push | 500 |
| Einträge für Sitzungsmetadaten pro Push | 500 |
| Serialisierter Anfrageinhalt | 8 MiB |
Die beiden Array-Limits sind unabhängig voneinander: Eine Anfrage kann 500 Datensätze und 500 Sitzungseinträge enthalten.
GET /v1/pull
Liest Feed-Ereignisse nach einem Cursor.
GET /v1/pull?project=project-a&since=20&limit=500&wait=30&team=platform
| Query | Erforderlich | Bedeutung |
|---|---|---|
project | ja | Undurchsichtige Projekt-ID |
since | ja, im normalen Gebrauch | Ereignisse strikt nach diesem Cursor zurückgeben; 0 beginnt am Anfang |
limit | nein | Seitengröße, begrenzt auf 1..500; Standard 500 |
wait | nein | Sekunden für Long Polling, begrenzt auf 0..30; Standard 0 |
team | nein | Team-Feed statt des Feeds des Principals |
Antwort:
{
"cursor": 22,
"records": [],
"sessions": [],
"more": false
}
Geben Sie Ereignisse in Feed-Reihenfolge und ohne rein serverseitige Felder zurück. Der Cursor ist das zuletzt zurückgegebene Ereignis. Gibt es keine neuen Ereignisse, bleibt er gleich since. Setzen Sie more: true nur, wenn bereits eine weitere Seite verfügbar ist; der Client fragt dann sofort erneut ab.
Ist wait > 0 und der Feed auf dem neuesten Stand, halten Sie die Anfrage, bis ein Ereignis angehängt wird oder die Wartezeit abläuft. Ein Timeout ist eine erfolgreiche leere Antwort, kein HTTP-Fehler. Das Trennen eines Long Polls darf den Feed-Zustand nicht ändern.
Fehlerantworten des Sitzungs-Syncs
Sitzungsfehler sind JSON-Objekte mit einem kurzen, menschenlesbaren error-String:
{"error":"missing or bad token"}
Verwenden Sie übliche Statuscodes:
| Status | Wann |
|---|---|
400 | Ungültiges JSON, fehlendes Projekt, ungültiger Cursor oder fehlerhafter Eintrag |
401 | Fehlendes, abgelaufenes oder ungültiges Bearer-Token; WWW-Authenticate: Bearer mitsenden |
403 | Der authentifizierte Principal ist kein Mitglied des angefragten Teams |
413 | Anfragebytes oder Anzahl der Einträge überschreiten das Limit |
404 | Unbekannte Route |
500 | Unerwarteter Speicherfehler; behaupten Sie nicht, ein Teil-Push sei erfolgreich gewesen |
Der Spiegel wiederholt Netzwerkfehler und gewöhnliche Serverfehler mit Backoff. Ein 401 stoppt diesen Spiegel, bis er mit einer brauchbaren Anmeldeinformation neu gestartet wird.
Memory-Sync
Memory-Sync ist optional und vom nur erweiterbaren Sitzungs-Feed getrennt. Alle drei Routen sind authentifizierte POST-Anfragen mit Content-Type: application/json:
/v1/memory/identity/v1/memory/snapshot/v1/memory/commit
Der aktuelle Client ist rein nativ. Eine Anfrage mit Origin-Header wird mit 403 {"code":"native_client_required"} abgelehnt.
Memory-Identität
Jede Anfrage enthält expectedPrincipal, entnommen aus dem lokal dekodierten Cloud-Konto als Zustimmungsbindung. Der Server muss das Bearer-Token unabhängig validieren und verlangen, dass der verifizierte Principal damit übereinstimmt. Autorisieren Sie nie ein Objektpräfix allein anhand von expectedPrincipal.
Anfrage:
{"expectedPrincipal":"user-id"}
Antwort:
{
"principal": "user-id",
"quota": {
"maxNotes": 5000,
"maxChanges": 500,
"maxNoteBytes": 131072,
"tombstoneDays": 90
}
}
Die mitgelieferte Implementierung validiert Tokens über Supabase GET /auth/v1/user, eine kompatible Implementierung kann aber einen anderen Kontodienst verwenden. Sie muss anonyme, gelöschte, abgelaufene oder anderweitig ungültige Konten ablehnen und dieselbe stabile Principal-ID zurückgeben, die der Client erwartet.
Geltungsbereich und Notizformat
Der Projektbereich wird durch project ohne Eigenschaft scope ausgedrückt:
{"project":"project-a"}
Der Nutzerbereich hat scope: "user" und darf kein project enthalten:
{"scope":"user"}
Das Protokoll erfordert beide Bereiche. Der aktuelle mitgelieferte R2-Adapter hat eine bekannte Lücke in der Schlüssel-Allowlist für das Nutzerpräfix; am Protokollvertrag für unabhängige Implementierungen ändert das nichts.
Eine Notiz sieht so aus:
{
"id": "borderless-cards-a91",
"summary": "Pricing cards use fills, not borders",
"body": "Use whitespace and background fills.",
"tags": ["design"],
"type": "feedback",
"createdAt": 1789344000000,
"updatedAt": 1789345000000
}
type ist optional und, falls vorhanden, einer von user, feedback, project oder reference. updatedAt ist optionale Metadaten und bewusst vom Inhalts-Hash ausgenommen.
Validierungslimits sind Teil des Protokollvertrags:
| Feld | Einschränkung |
|---|---|
| Principal-, Projekt- und Notiz-IDs | [A-Za-z0-9_-]{1,128}; außerdem __proto__, constructor und prototype ablehnen |
summary | Getrimmt, eine Zeile, 1–500 Zeichen |
body | Getrimmter String, höchstens das angekündigte maxNoteBytes in UTF-8 |
tags | Höchstens 30; jeder 1–64 Zeichen ohne Leerraum, Komma oder Zeilenumbruch |
createdAt | Nicht negative sichere Ganzzahl |
updatedAt | Optionale nicht negative sichere Ganzzahl |
IDs aktiver Notizen sind ohne Berücksichtigung der Groß-/Kleinschreibung eindeutig. Das ist erforderlich, weil ein Server keinen Zustand erzeugen darf, den verbreitete Client-Dateisysteme ohne Unterscheidung der Groß-/Kleinschreibung nicht abbilden können.
Der SHA-256-Hash einer Notiz in hexadezimaler Kleinschreibung wird über UTF-8-JSON mit genau diesem positionsbasierten Array berechnet:
JSON.stringify([id, summary, body, tags, createdAt])
Genau dann, wenn type vorhanden ist, wird es angehängt:
JSON.stringify([id, summary, body, tags, createdAt, type])
Implementierungen müssen für diese Werte JavaScripts JSON.stringify exakt nachbilden. Die Kompatibilitätsausnahme bewahrt Hashes, die vor der Einführung von Notiztypen entstanden sind.
POST /v1/memory/snapshot
Projektanfrage:
{
"expectedPrincipal": "user-id",
"project": "project-a",
"knownEtag": "optional-previous-etag",
"have": {
"borderless-cards-a91": "64-lowercase-hex-characters"
}
}
Für den Nutzerbereich ersetzen Sie project durch "scope":"user".
knownEtag ermöglicht einen schnellen Pfad für den unveränderten Fall. have ordnet Notiz-IDs den Hashes zu, die der Client bereits hat; der Server kann diese Notizinhalte weglassen, ohne sie aus dem Objektspeicher zu lesen. Lehnen Sie eine have-Zuordnung mit mehr als maxNotes * 2 + 1000 Schlüsseln ab.
Antwort bei unverändertem Stand:
{
"principal": "user-id",
"etag": "same-etag",
"unchanged": true,
"quota": {
"maxNotes": 5000,
"maxChanges": 500,
"maxNoteBytes": 131072,
"tombstoneDays": 90
}
}
Vollständige oder inkrementelle Antwort:
{
"principal": "user-id",
"etag": "current-etag-or-null",
"entries": [
{
"id": "borderless-cards-a91",
"hash": "64-lowercase-hex-characters",
"note": { "id": "borderless-cards-a91", "summary": "...", "body": "...", "tags": [], "createdAt": 1789344000000 }
},
{ "id": "already-held", "hash": "64-lowercase-hex-characters" },
{ "id": "deleted-note", "hash": null }
],
"quota": {
"maxNotes": 5000,
"maxChanges": 500,
"maxNoteBytes": 131072,
"tombstoneDays": 90
}
}
Ein aktiver Eintrag darf note nur weglassen, wenn have[id] seinem Hash entspricht. Ein Tombstone hat hash: null und keine Notiz. etag ist null, wenn kein Manifest existiert.
POST /v1/memory/commit
{
"expectedPrincipal": "user-id",
"project": "project-a",
"etag": "etag-from-snapshot-or-null",
"changes": [
{
"id": "borderless-cards-a91",
"hash": "64-lowercase-hex-characters",
"note": { "id": "borderless-cards-a91", "summary": "...", "body": "...", "tags": [], "createdAt": 1789344000000 }
},
{ "id": "deleted-note", "hash": null }
],
"have": {}
}
Für den Nutzerbereich ersetzen Sie project durch "scope":"user". Ein Anlegen oder Aktualisieren muss eine Notiz mitführen, deren ID und berechneter Hash zur Änderung passen. Eine Löschung hat hash: null und kein note. changes muss zwischen 1 und dem angekündigten maxChanges Einträge enthalten, ohne doppelte IDs.
Wenden Sie den Commit nur an, wenn etag noch das aktuelle Manifest bezeichnet. Bei einem veralteten ETag geben Sie 409 {"code":"revision_conflict"} zurück und lassen das veröffentlichte Manifest unverändert. Schreiben Sie neue unveränderliche Notizrevisionen, bevor Sie das neue Manifest veröffentlichen, und veröffentlichen Sie das Manifest atomar. Ein unterlegener gleichzeitiger Schreiber kann unerreichbare unveränderliche Objekte für eine spätere Garbage Collection hinterlassen, aber kein Teilzustand darf über das Manifest sichtbar werden. Die Antwort hat dieselbe Form wie ein erfolgreicher Snapshot, mit dem neuen ETag und den aktuellen Einträgen.
Verwerfen Sie Löschungen nicht sofort. Bewahren Sie Tombstones für das angekündigte Zeitfenster auf, damit Offline-Clients sie bemerken können. Ein gesenktes Kontingent darf einen Principal darüber nicht festsetzen: Lehnen Sie Wachstum über maxNotes ab, erlauben Sie Löschungen aber weiterhin.
Memory-Fehler und -Limits
Memory-Fehler verwenden einen stabilen, maschinenlesbaren code:
{"code":"revision_conflict"}
Die aktuelle Implementierung verwendet unter anderem diese Codes:
| Status | Codes |
|---|---|
400 | invalid_body, unknown_field, invalid_project, invalid_scope, invalid_have, invalid_changes, invalid_note, hash_mismatch, id_case_conflict |
401 | sign_in_required |
403 | native_client_required, account_mismatch |
409 | revision_conflict |
413 | memory_quota_exceeded |
415 | json_required |
429 | rate_limited |
502 | auth_unavailable, invalid_manifest, missing_memory_object, memory_integrity_error, memory_sync_failed |
503 | auth_not_configured, r2_not_configured |
Die mitgelieferten Standardwerte sind 5000 aktive Notizen pro Bereich, 500 Änderungen pro Commit, 128 KiB pro Notizinhalt, 4 MiB pro Anfrage, 16 MiB serialisierte Eintragsdaten pro Antwort und 90 Tage für Tombstones. Nur die vier für den Client relevanten Werte in quota werden über das Protokoll angekündigt. Ein Client wendet zusätzlich eigene harte Sicherheitsobergrenzen an; höhere Serverkontingente garantieren also nicht, dass ein Client eine beliebig große Antwort annimmt.
Einen OHarness-Client konfigurieren
Legen Sie das Ziel in die Konfiguration des Nutzers, nie in die eines Repositorys:
{
"sync": {
"url": "https://sync.example.com",
"token": "opaque-session-token",
"team": "optional-team",
"enabled": true
}
}
Die Datei ist $OHARNESS_HOME/config.json, wenn OHARNESS_HOME gesetzt ist, $XDG_CONFIG_HOME/oharness/config.json unter Linux, wenn XDG gesetzt ist, und sonst ~/.oharness/config.json. Starten Sie OHarness nach einer Änderung des Ziels neu.
Die aktuelle Rangfolge lautet:
sync.url in user config > OHARNESS_SYNC_URL > https://sync.oharness.ai
sync.token in user config > OHARNESS_SYNC_TOKEN > Cloud account token
Folglich ersetzt OHARNESS_SYNC_URL den fest eingebauten Standard nur, wenn die Nutzerkonfiguration keine sync.url hat. OHARNESS_SYNC=0 deaktiviert den Sitzungs-Sync unabhängig von Ziel oder Anmeldeinformation.
Die Dateien oharness.config.json und .oharness.json eines Repositorys können sync.url, sync.token oder sync.team nicht setzen; OHarness verwirft diese Werte. Ein Repository kann Sync nur abschalten oder Ausschlüsse ergänzen. So kann ein geklontes Repository die Verläufe eines Nutzers nicht umleiten.
Für Memory wird dieselbe aufgelöste Base-URL verwendet, doch der aktuelle Memory-Client nutzt die Anmeldeinformation des Cloud-Kontos, nicht sync.token oder OHARNESS_SYNC_TOKEN. Um Memory selbst zu hosten, müssen sich die Client-Anmeldung und der Identitätsprüfer des Memory-Servers daher auf denselben Principal einigen.
Checkliste für Implementierungen
Ein kompatibler Dienst sollte all diese Verhaltensweisen erfüllen:
- Health funktioniert ohne Token; jede Datenroute schlägt ohne Token fail-closed fehl.
- Zwei Principals mit derselben Projekt-ID können die Daten des jeweils anderen nicht sehen.
- Persönliche Feeds und Team-Feeds sind getrennt; nicht berechtigte Teams geben
403zurück. - Die Wiederholung eines Pushs verdoppelt weder Datensätze noch veraltete Sitzungsmetadaten.
- Pull ist geordnet, paginiert, monoton im Cursor und sicher für Long Polling.
- Sitzungsinhalte überstehen Push und Pull auf Ebene der JSON-Werte unverändert.
- Löschungen von Sitzungen und Memory-Notizen erreichen ein zweites Gerät, das offline war.
- Ein veraltetes Memory-ETag veröffentlicht keine Änderung am Manifest.
- Memory-Objektschlüssel verwenden immer den serverseitig verifizierten Principal.
- Hash-Abweichungen, Kollisionen von Notiz-IDs, die sich nur in der Groß-/Kleinschreibung unterscheiden, und zu große Anfragen werden vor dem Veröffentlichen eines Manifests abgelehnt.
- Persistenter Feed- und Manifest-Zustand übersteht Prozessneustarts und Deployments.
Die Sitzungstests des Repositorys in test/sync.test.ts und die Memory-Tests in script/sync-memory.test.ts sind ausführbare Beispiele für den Vertrag. Sie instanziieren derzeit direkt den mitgelieferten Server, statt eine entfernte URL anzunehmen; eine andere Implementierung sollte also dieselben Fälle portieren oder einen kleinen Adapter bereitstellen, bevor sie Kompatibilität beansprucht.
Für einen ersten Smoke-Test gegen einen entfernten Server geben Sie ein Wegwerf-Token aus und setzen dessen Wert unten ein. Verwenden Sie kein produktives Kontotoken in einer Shell-Historie:
export SYNC_URL=https://sync.example.com
export SYNC_TOKEN=test-token
curl "$SYNC_URL/v1/health"
curl -X POST "$SYNC_URL/v1/push" \
-H "Authorization: Bearer $SYNC_TOKEN" \
-H "Content-Type: application/json" \
--data '{"project":"conformance","records":[{"session":"s1","id":"r1","seq":1,"device":"test","at":1,"body":{"value":"unchanged"}}],"sessions":[]}'
curl "$SYNC_URL/v1/pull?project=conformance&since=0&limit=500" \
-H "Authorization: Bearer $SYNC_TOKEN"
Pushen Sie denselben Inhalt ein zweites Mal und prüfen Sie, dass accepted 0 ist; pullen Sie dann ab dem zurückgegebenen Cursor und prüfen Sie, dass die Arrays leer sind und der Cursor unverändert ist. Diese Prüfungen belegen Erreichbarkeit und grundlegende Idempotenz, nicht Mandantentrennung oder Sicherheit von CAS bei Nebenläufigkeit.
Keine Anforderungen
Ein kompatibler Server muss nicht:
- ein Modell ausführen, Tools ausführen oder Nachrichtenformate von Verläufen verstehen;
- JSONL, R2, Supabase oder eine bestimmte Datenbank verwenden;
- Registrierung, Anmeldung, Token-Ausgabe oder eine Browser-Anwendung bereitstellen;
- Memory implementieren, um Sitzungs-Sync zu implementieren;
- CORS bereitstellen. Die aktuellen Clients sind native Node-/Electron-Clients.
Dass diese Belange außerhalb des Protokolls bleiben, ermöglicht es einem kleinen selbst gehosteten Dienst und einem mandantenfähigen Deployment, dasselbe Protokollformat zu sprechen.