Knowledge and memory sync protocol, version 1
Status: normative. This page specifies version 1 of the personal knowledge and memory sync protocol so that any
client and any server can implement it without reading Hub code. circle-hub is one server implementation; it serves
the protocol under /api/sync/v1. A short overview in Chinese is in knowledge-sync-v1.zh-CN.md.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be read as in RFC 2119 and RFC 8174 when, and only when, they appear in capitals.
Machine-readable shapes are generated from the contracts and are part of this specification: every request and answer
body named below has a JSON Schema (draft 2020-12) in
contracts/generated/schemas/
named <schema>.v1.schema.json (for example personal-sync-commit.v1.schema.json), generated from
contracts/src/sync.ts. Where the
prose and a schema disagree, the schema wins. The frozen shared vectors in
contracts/test-fixtures/sync/v1/
fix exact canonical bytes, object identities and expected outcomes; an implementation that reproduces them is
interoperable on encoding. A portable reference of the encoding is
contracts/src/sync-canonical.ts
and of the ancestry merge contracts/src/sync-merge.ts;
both depend only on JSON, UTF-8 and SHA-256.
1. Scope and roles
The protocol moves one account’s own knowledge, originals, folders, memory and pack manifests between that account’s clients and a server, and keeps a revision history with explicit conflicts. It does not publish anything to a team, grant access, install or execute packs, or read business systems. Team content and content shared by others are not exported through it.
- A client is any program the account holder runs (a desktop or terminal client, a script). Several clients of one account sync the same personal space.
- A server keeps the account’s sync state per account and epoch (section 8) and re-verifies everything it receives before it becomes native knowledge or memory.
- The sync API is an ordinary HTTP API. It is not an MCP tool and a server MUST NOT expose it to models as one.
2. Transport and endpoints
All routes are relative to the sync base URL. A client finds it in the discovery document (section 15); on
circle-hub the base is <issuer>/api/sync/v1, where <issuer> is the origin the client reached the server on
(scheme, host and port, no path). Route templates are fixed relative to
the base: a client MUST NOT rewrite them, and a server MUST serve exactly these paths. {id} is a session UUID for the
/sessions/{id}/… routes and an operation key for /operations/{id}.
| Route | Method | Request body | Answer body (200) |
|---|---|---|---|
/state |
GET | none | personal-sync-state@1 |
/clients |
POST | personal-sync-client@1 |
personal-sync-client@1 |
/sessions |
POST | personal-sync-session@1 |
personal-sync-session-reply@1 |
/sessions/{id}/inventory?epoch=…[&cursor=…] |
GET | none | personal-sync-inventory@1 |
/sessions/{id}/missing |
POST | personal-sync-missing@1 |
{"missing": [id…]} |
/sessions/{id}/upload |
POST | personal-sync-upload@1 |
personal-sync-receipt@1 |
/sessions/{id}/download |
POST | personal-sync-download@1 |
personal-sync-transfer@1 |
/sessions/{id}/commit |
POST | personal-sync-commit@1 |
personal-sync-result@1 |
/sessions/{id}/abort |
POST | personal-sync-abort@1 |
personal-sync-receipt@1 |
/operations/{id}?epoch=…[&cursor=…] |
GET | none | personal-sync-mode-result@1, personal-sync-result@1 or personal-sync-receipt@1 |
/mode/check |
POST | personal-sync-mode-check@1 |
personal-sync-mode-impact@1 |
/mode |
POST | personal-sync-mode-change@1 |
personal-sync-mode-result@1 |
The same table is published as data in contracts/test-fixtures/sync/v1/http.json (personal-sync-http@1). Routes a
capability adds (section 15) are listed with that capability and are not part of this frozen table:
| Route | Method | Request body | Answer body (200) | Capability |
|---|---|---|---|---|
/share |
POST | personal-sync-share@1 |
personal-sync-share-result@1 |
share-single-item |
Message rules:
- Request bodies MUST be
Content-Type: application/jsonin UTF-8 without a byte order mark, withoutContent-Encodingother thanidentity, and MUST be one JSON object. Duplicate object members are refused (I-JSON, RFC 7493). A POST carries no query string. - Every request and answer shape is closed: a server MUST refuse a request member or query parameter it does not know
with
SYNC_OBJECT_INVALID. Clients MUST ignore answer members they do not know (section 13). - Answers are
application/json; charset=utf-8withCache-Control: no-store. - Browser-style cross-origin use is not supported: a request carrying an
Originheader that is not the server’s own origin is refused with 403. There is no CORS. - The sync API never takes a source URL, a local path or an arbitrary record address; all scope comes from the authenticated account.
3. Object kinds
Every synchronized thing is an immutable object: a JSON value with the envelope
{"schema": "personal-sync-object@1", "kind": "<kind>", "body": { … }}
and a closed body per kind (personal-sync-object.v1.schema.json). The kinds are the sync_object_kind vocabulary:
| Kind | Body | Meaning |
|---|---|---|
bytes |
bytes_base64 |
Verbatim bytes (at most 1 MiB decoded): a chunk of an original, or a verbatim quotation. Never normalized. |
source_revision |
item_id, parents, directory_id, name, media_type, raw_sha256, size, chunks |
An original document: its bytes chunks in order, the SHA-256 of the raw bytes and their size. |
entry_revision |
item_id, parents, directory_id, content, grounds |
A knowledge entry and its grounds; each ground names a quotation bytes object and either a source revision with a parser and selector, an existing reference, or another entry revision. |
directory_revision |
item_id, parents, parent_directory, name |
A folder. The root has parent_directory: null and an empty name. Empty folders sync too. |
memory_revision |
item_id, parents, directory_id, title, markdown, state |
A memory document (enabled or disabled). Memory is never an input or a ground. |
pack_manifest |
item_id, parents, manifest |
A personal workflow or skill manifest. Holding a manifest does not install, publish or run anything. |
deletion |
item_id, parents (non-empty), target_kind |
An explicit deletion of an item. A missing item is never read as a deletion. |
inventory_page |
items |
Up to 500 index rows {kind, item_id, heads}, sorted by kind + ":" + item_id, unique. |
snapshot |
parents, pages |
One state of the whole personal space: the sorted set of its inventory pages and its parent snapshots. |
A client’s verification flags are not part of any object: the server computes verification (section 7).
4. Identity and revisions
- The object ID is the lowercase hexadecimal SHA-256 of the object’s canonical bytes (section 5). Two objects with
equal canonical bytes are one object. The object ID of a
source_revisionand itsraw_sha256are different values and MUST NOT be substituted for each other. - An item is a folder, entry, original, memory document or pack with a stable random
item_id(a UUID). Renaming, moving or editing an item keeps itsitem_id; every change is a new revision whoseparentslists the revisions it replaces. An item has exactly one kind. - The heads of an item are its revisions that no other listed revision of the same item descends from
(
mergePersonalSyncHeads). One head is the normal case; more than one head is a conflict. - A snapshot names the heads of every item through its pages. Its
parentsare the snapshots it continues. - Sets are sorted and unique:
parents,heads,pagesand the hashes in a batch are sorted by their hexadecimal text (which equals byte order);itemsis sorted bykind:item_id. Ordered lists (chunks,grounds, Markdown text) keep their order. The caller supplies sorted sets; the server does not sort them.
5. Canonical form
The canonical bytes of an object are the UTF-8 encoding of the JSON Canonicalization Scheme (JCS, RFC 8785) applied to the object after every string, member names included, has been normalized as editable text:
- The string MUST be well-formed Unicode (no lone surrogates); otherwise the object is invalid.
- One leading U+FEFF (byte order mark) is removed. A BOM elsewhere is kept.
- CR LF and lone CR become LF.
- The result is put in Unicode Normalization Form C (NFC). No other change: other whitespace, trailing spaces, case and final newlines are kept; NFKC is not applied.
Further rules:
- Member names are compared after normalization; two names that become equal make the object invalid.
- Numbers are finite (no NaN or infinity); JSON nesting is at most 64 levels.
- An object MUST match its closed schema both before and after normalization.
- Verbatim bytes (original chunks and quotations) travel only as canonical padded standard base64 (RFC 4648 section 4)
in a
bytesobject, so normalization never touches them. A source with a BOM or CR LF keeps it. - A canonical object is at most 8 MiB (the server may announce less in
limits.object_bytes).
A server MUST decode uploaded bytes strictly (fatal UTF-8, no BOM, no duplicate members), re-encode them and refuse
bytes that are not already canonical with SYNC_OBJECT_INVALID, and refuse bytes whose SHA-256 is not the claimed ID
with SYNC_OBJECT_HASH_MISMATCH. The server never guesses a client’s hash from non-canonical input. The vectors in
encoding.json and native-links.json pin exact bytes for these rules.
6. The sync flow
A complete exchange is: state → client → session → inventory → missing → upload → download → commit.
- State.
GET /statereturns the account’sepoch,mode,status(sync_status:idle,syncing,complete,conflicted),state_version(an opaque hash of the current personal content, used for CAS),snapshot(the current snapshot ID or null),last_synced_at,used_bytes,capacity_bytesand the effectivelimits(section 11). Reading state never creates content. A server advertisingfirst-sync-noticealso reportsnever_synced(section 15). - Client.
POST /clientsregisters the client’s randomclient_id(UUID) and a displayname(1–100 characters). It is idempotent; the same ID with another name isIDEMPOTENCY_MISMATCH. The client ID is a device declaration, not an identity: it does not authenticate anything. - Session.
POST /sessionswithclient_id,epoch,known_state(the laststate_versionthe client saw, or null) andbase_snapshot(the last snapshot it committed or downloaded in this epoch, or null) opens a resumable session. The answer hassession_id,expires_at(at mostlimits.session_secondslater), the currentstateandinventory_cursor. The session freezes the server’s inventory at that moment. - Inventory.
GET /sessions/{id}/inventory?epoch=…pages through the frozen inventory as descriptors{id, kind, bytes}, ordered by ID; follownextascursoruntil it is null. A cursor is opaque and bound to the account, epoch, session and snapshot. - Missing.
POST /sessions/{id}/missingwith up tolimits.batch_objectsobject IDs returns the subset the server does not hold for this account and epoch. Found IDs are leased to the session so they stay downloadable and committable. The answer never reveals whether another account holds equal bytes. - Upload.
POST /sessions/{id}/uploadsends a batch{schema: "personal-sync-upload@1", epoch, operation_id, objects: [{id, bytes_base64}]}. The batch is all-or-nothing: every object is decoded, checked for shape, size, identity and credentials before any is stored. Uploaded objects are staged only: no model, search, memory session or grant can read them before a commit. The receipt countsaccepted,existingand decodedbytes. - Download.
POST /sessions/{id}/downloadwith object IDs returns{objects: [{id, bytes_base64}], missing}. Only objects in the session’s frozen inventory or leased to it are returned; anything else is listed inmissing. - Commit.
POST /sessions/{id}/commitwith{schema: "personal-sync-commit@1", epoch, operation_id, expected_state, base_snapshot, snapshot}asks the server to makesnapshotcurrent (section 7). On success the answerpersonal-sync-result@1carries the newstate, thereceipt, per-objectverification,conflicts, optionalprojections(where accepted objects landed natively) andnext; whennextis not null the client reads the remaining pages withGET /operations/{operation_id}?epoch=…&cursor=<next>. - Abort.
POST /sessions/{id}/abortends a session and releases objects only it leased. Sessions also expire; an expired session answersSYNC_SESSION_EXPIREDand the client keeps its local revisions and opens a new one.
A client builds a candidate snapshot locally: new revisions continuing the current heads, inventory pages listing
every item, and a snapshot whose parents include the current snapshot. It uploads whatever missing reports,
then commits. On an empty personal space a client uploads the root directory, its revisions, the page and the
snapshot and commits with the state the server returned.
7. Commit, CAS and conflicts
A commit is a compare-and-swap on the whole personal space, inside one server transaction:
epochMUST be the current epoch (SYNC_EPOCH_CHANGED) and the mode MUST behub(SYNC_MODE_LOCAL).expected_stateMUST equal the currentstate_version; otherwise the server answers 409SYNC_VERSION_CONFLICTwith the currentstatein the error body and changes nothing. Native edits made in the server’s console or through MCP tools changestate_versiontoo, so they take part in the same CAS. A client MUST NOT retry by substituting the new state blindly: it downloads, merges and commits a new candidate.base_snapshotis null or a snapshot this account committed earlier in this epoch (SOURCE_UNAVAILABLEotherwise).- The candidate graph MUST be closed (every referenced object present:
SYNC_OBJECT_MISSING), acyclic, typed (a revision’s parents are revisions of the same item and kind; chunks and quotations arebytes; pages hold revisions whose kind and item match the row) and bounded (SYNC_BATCH_LIMITbeyond 10,000 items or 4,096 levels). - The candidate MUST advance every current item: each current item appears in the candidate, and each current head is
equal to or an ancestor of a candidate head of that item. Dropping a competing head is
SYNC_VERSION_CONFLICT. Heads listed for one item MUST NOT descend from each other.
Merging is deterministic and the same on both sides: changes on one side are taken; changes to different items are
united; identical revisions are one. When both sides changed one item, the candidate keeps both heads and the
item is conflicted. No clock, arrival order or semantic merge chooses a winner. A conflict is resolved by a new
revision that lists all competing heads as its parents. Conflicted memory is not loaded into new client sessions, and
conflicted knowledge is not used as a current entry, until it is resolved. The server may also report a structural
conflict with a single head (structural: true), for example a folder cycle or two siblings whose names collide after
normalization.
The server re-verifies every entry revision against what the account may read now: folder resolution, ground
readability, parser eligibility, selector position and the verbatim quotation. A client’s claim is never evidence.
verification[] reports per object status (verified or unverified), the rule version, scanner, parser locks,
bounded positions and registered codes. A wrong or missing quotation leaves the revision stored as unverified; it is
not an error of the commit. last_synced_at changes only on a successful commit without conflicts.
8. Epochs, modes and purge
The sync_mode of an account is hub (sync to the server, the default for every account) or local (keep personal
content on the client only). The wire value hub names the mode, not a product; clients choose their own display text.
The default stays hub; a client tells the account holder so on first connection (capability first-sync-notice,
section 15).
- Every return from
localtohubstarts a new opaqueepoch. Sessions, objects, heads, cursors and operation keys are scoped to account and epoch; an old epoch’s requests are refused withSYNC_EPOCH_CHANGED. A new epoch never restores grants or content from before a purge; the client uploads its local copy again as new content. POST /mode/checkwith{mode}returns the impact of a change without writing anything:check(an opaque hash of the impact and its inputs),expected_state,personal_bytes,directory_grantsthat would end,pending_shares(null when the server cannot count them) andteam_copies_preserved. It may also carryrun_held_originalsandaffected_runs(below); a client that does not know them ignores them.POST /modewith{epoch, operation_id, mode, expected_state, check, confirm_purge}changes the mode. Switching tolocalpurges the server’s copy of the epoch: personal knowledge, originals, folders, memory, staged objects and their projections are removed and the account’s folder grants end. It requiresconfirm_purge: trueand thecheckof a fresh/mode/check; a staleexpected_stateorcheckisSYNC_VERSION_CONFLICT. Team copies made by sharing stay, local copies are untouched, and a server backup taken before the purge still holds the old content.- The purge also removes every copy of the epoch’s originals that the server held independently for runs (the run, its
claims, submissions and results), except copies a team knowledge source holds. Those runs’ grounds become
unavailable for the registered reason
GROUNDS_PURGED; run records, capture records and their digests are not rewritten, and the removal is recorded append-only./mode/checkreports how many such copies the purge removes (run_held_originals) and how many runs hold them (affected_runs), and both are part of thecheckhash. - A server that advertises
purge-confirmation(section 15) does not purge on a Bearer token alone: it records the request and answers 403SYNC_PURGE_CONFIRMATION_REQUIREDwithconfirmation; the purge runs only after the account holder confirms that exact request outside the sync API, and the client’s identical retry then returns the receipt. - Switching from
localtohubcreates the new epoch;checkis null andconfirm_purgefalse. - Requesting the current mode is a no-op with a receipt.
9. Receipts and idempotency
Upload, commit, abort and mode change carry an operation_id (1–128 characters of A–Z a–z 0–9 _ -), chosen by the
client and unique per account and epoch. The server binds the key to a hash of the route template, the session (for
session routes) and the complete request. Sending the same key with the same request again returns the original
receipt (or commit result) instead of performing the write again, also after a server restart; the same key with a
different request is IDEMPOTENCY_MISMATCH.
A receipt personal-sync-receipt@1 has operation_id, epoch, request (the request hash), previous_state,
state_version, counts and at. When an answer is lost, the client either repeats the identical request or reads
GET /operations/{operation_id}?epoch=…, which returns the mode result, the first page of the commit result, or the
receipt of an upload or abort; an unknown or foreign key is SOURCE_UNAVAILABLE. Mode receipts stay readable after
the epoch they ended.
10. Authentication and authorization
- Every request carries
Authorization: Bearer <access token>. A missing, expired, revoked or otherwise unusable token, or one of a disabled account, is answered with 401, the headerWWW-Authenticate: Bearerand the body{"code": "SOURCE_UNAVAILABLE"}, before any body is read. - Access tokens are the server’s ordinary member access tokens. On circle-hub a client obtains one with the MCP
authorization flow, OAuth 2.1 with PKCE (RFC 7636), dynamic client registration (RFC 7591) and a resource indicator
(RFC 8707) naming the protected resource
<issuer>/mcp; the resource and its authorization server are discovered from<issuer>/.well-known/oauth-protected-resource/mcp(RFC 9728) and<issuer>/.well-known/oauth-authorization-server(RFC 8414). The same token authenticates the sync API. Access tokens an administrator issues for an account are accepted too. Console sign-in sessions, cookies and setup or invitation codes are not. - Version 1 tokens carry no per-route scopes: whoever holds an account’s access token may sync that account. Purging
the server’s copy additionally needs the account holder’s confirmation where the server advertises
purge-confirmation(section 15), so a token held by a model-facing MCP client cannot purge alone. - The scope is always the authenticated account. There is no owner or space parameter; administrators have no right to sync another account. Another account’s sessions, operations and objects answer exactly like absent ones.
- The server rechecks the token and the epoch before it sends a private answer; a token revoked or an epoch purged while a request ran gets no private bytes.
11. Limits
GET /state and every session answer report the limits actually in force; a client MUST NOT exceed them and MUST NOT
assume larger values.
| Limit | Field | circle-hub default |
|---|---|---|
| Canonical object | limits.object_bytes |
8 MiB, never above batch_bytes |
| Decoded bytes per batch | limits.batch_bytes |
8 MiB (CIRCLE_HUB_SYNC_MAX_BATCH_BYTES), bounded by the request size limit |
| Objects per batch | limits.batch_objects |
64 |
| Inventory and result page | limits.page_items |
500 |
| Session lifetime | limits.session_seconds |
86,400 |
| Byte object | fixed | 1 MiB decoded |
| Requests per account | not reported | 120 per minute (CIRCLE_HUB_SYNC_REQUESTS_PER_MINUTE), shared by all tokens and clients; at most 2 concurrent writes |
Personal capacity (capacity_bytes, 1 GiB by default) counts committed and staged objects, all history and conflict
candidates and memory, by original bytes rather than base64 length; exceeding it is SPACE_CAPACITY_EXCEEDED.
Memory documents keep their own limits (MEMORY_DOCUMENT_LIMIT). A request over the request size limit is 413
SYNC_BATCH_LIMIT. Over the rate limit the answer is 429 SYNC_RATE_LIMITED with Retry-After and
retry_after_seconds.
12. Errors
An error answer is personal-sync-error@1: {"code": "<registered code>"}, plus the current state when a CAS or
epoch refusal can tell it and retry_after_seconds on 429. With the capability error-detail it also carries
detail, and the purge refusal carries confirmation (section 15). Codes are the protocol codes registered in the generic
model (contracts/model.json, generated into contracts/generated/protocol.ts); the registered texts are the server’s
Chinese user-facing texts.
Transport statuses: 401 is the Bearer challenge above; 403 is a refused Origin; 404 SOURCE_UNAVAILABLE is an
unknown route, session, operation or object (indistinguishable from a foreign one); 405 SYNC_OBJECT_INVALID (with
Allow) is a wrong method; 415 SYNC_OBJECT_INVALID is a wrong media type or content encoding; 503
SOURCE_RETRYABLE with Retry-After is a temporary condition and the request may be repeated with the same
operation_id. Every other registered code a route returns answers 422. The table lists the codes the sync routes
return and their status:
| Code | HTTP status | Registered text (when) |
Registered next step (fix) |
|---|---|---|---|
SOURCE_UNAVAILABLE |
404 | 别人个人空间里的内容(不区分有没有),或凭据与登录类读取失败:Hub 登录来源失败或过期、来源系统拒绝 Hub 的账号、应答里有疑似凭据被扣下、读取者不是有效账号、个人凭据来源;调用者面前同形(D05 R[3]) | 核对引用;需要时请内容的主人分享,或请管理员按管理员记录恢复读取后按原引用重读;原因只记在管理员记录里 |
SOURCE_RETRYABLE |
503 | 有界重试仍未读到来源,附缺失事实与下次重读时刻 | 按给出的下次时刻重读同一来源;仍失败时由资源管理员恢复读取 |
SYNC_RATE_LIMITED |
429 | 同步请求过于频繁 | 等待答复中的重试时间后重试 |
SYNC_BATCH_LIMIT |
413 | 同步批量超过上限 | 按服务器给出的大小分批重试 |
SYNC_VERSION_CONFLICT |
409 | 同步期间内容已变化 | 取得最新内容并合并后重试 |
SYNC_EPOCH_CHANGED |
409 | 同步方式已变化 | 重新读取同步设置,不自动上传旧内容 |
SYNC_MODE_LOCAL |
409 | 当前仅保存在本机 | 在客户端设置同步方式后重试 |
SYNC_PURGE_CONFIRMATION_REQUIRED |
403 | 清除 Hub 上的私人空间需要本人在控制台确认 | 10 分钟内在 Hub 控制台的同步方式中确认这次清除,再用同一请求重试 |
SYNC_OBJECT_INVALID |
422 | 同步对象不符合结构 | 核对对象格式后重试 |
SYNC_OBJECT_HASH_MISMATCH |
422 | 同步对象摘要不符 | 按规范形式重新计算并上传 |
SYNC_OBJECT_MISSING |
422 | 同步对象尚未齐全 | 补齐本人缺少的对象后重试 |
SYNC_SESSION_EXPIRED |
422 | 同步会话已到期 | 保留本机修改并重新开始同步 |
IDEMPOTENCY_MISMATCH |
422 | 同一步骤键或意图键内容不同(D05、B06) | 改了内容就不要带原键 |
SPACE_CAPACITY_EXCEEDED |
422 | 本次写入计入版本记录后超过空间容量上限 | 请管理员调整容量后重试;撤回或停用内容不会释放版本记录占用 |
SPACE_CREDENTIALS_DETECTED |
422 | 个人条目或授权内容命中凭据扫描 | 移除凭据后重试;原文不向其他账户开放 |
SPACE_CONTENT_UNINSPECTABLE |
422 | 来源不是可完整检查的文本,当前不能开放给其他账户 | 改用可完整检查的文本原文;ZIP、PDF、图片等文件仍可仅保存在个人空间 |
MEMORY_CREDENTIALS_DETECTED |
422 | 记忆含有疑似凭据 | 移除命中位置的凭据后重新提交 |
MEMORY_DOCUMENT_LIMIT |
422 | 记忆正文超过单篇上限 | 缩短正文或由管理员调整单篇限额 |
13. Versioning and compatibility
- The protocol version is the
v1path segment and the@1suffix of every schema name. Version 1 changes only additively: existing routes, members, values (including the mode valuehub), canonical bytes and the frozen vectors stay as they are; no member is renamed or removed and no new required request member is added to an existing operation. An incompatible change is a new version under a new path with new schema names and a new vector directory. - Clients MUST ignore answer members they do not know, and MUST treat an error code they do not know as a generic
failure of that request, showing the error’s
detailtext when one is present (a later addition) and otherwise their own generic message. - Servers keep refusing request members and query parameters they do not know (
SYNC_OBJECT_INVALID), so a client learns early when it sends something a server does not support. - New behaviour is offered only behind a named capability that a server advertises in its discovery document (section 15); a client uses a capability only when the server advertises it and keeps working without it. A server without a discovery document offers plain version 1 with no capabilities.
14. Security considerations
The sync API carries an account’s private content. Servers keep personal payloads per account and epoch and purge them
physically; equal bytes are never deduplicated across accounts. Uploaded text and bytes are scanned for credentials
before they are stored (SPACE_CREDENTIALS_DETECTED, MEMORY_CREDENTIALS_DETECTED; content that cannot be inspected
is SPACE_CONTENT_UNINSPECTABLE). Audit metadata records actor, token, the verified OAuth client, the declared
client ID, route template, times, counts, state hashes and the status, never titles, paths, device names or content.
On circle-hub an entry version committed through sync names its audited request as its session, so the console can
show which token and OAuth client wrote it. An entry written through MCP keeps the clientInfo the client reported in
initialize as a declaration only when the write carries the Mcp-Session-Id the server issued in that initialize
answer (MCP Streamable HTTP) with the same token; a write without such a session, or with a session that declared
nothing, names no client and is shown as 客户端. One token used by two clients therefore never mislabels either.
Circle-Session-Id, when sent, is kept alongside. Names are shown as recorded and are never treated as verified
identities.
Bytes already sent to a client cannot be recalled by a purge, and a purge does not erase offline backups.
15. Discovery and capabilities
A server MAY publish a discovery document at /.well-known/knowledge-sync on its origin (RFC 8615). It is answered to
GET and HEAD without authentication, as application/json, and names only addresses and capabilities of the server,
nothing of an account. Its shape is knowledge-sync-discovery@1 (knowledge-sync-discovery.v1.schema.json); the
vector contracts/test-fixtures/sync/v1-capabilities/discovery.json is circle-hub’s document for the origin
https://192.0.2.10.
| Member | Meaning |
|---|---|
issuer |
The origin the document was requested on. |
sync.versions[] |
Per supported version: version (v1), base_url (the sync base URL) and the sorted capabilities it offers. Route templates stay fixed relative to base_url. |
authorization.resource |
The OAuth protected resource to request tokens for (RFC 8707); on circle-hub <issuer>/mcp. |
authorization.resource_metadata |
Its RFC 9728 metadata URL, which names the authorization server. |
local_tools.base_url |
Base of the read-only local-tools distribution (circle-hub: <issuer>/local-tools, the MCP endpoint with /mcp replaced). |
mcp.endpoint, mcp.capabilities |
Optional. The MCP endpoint and, per named operation, which tool and input argument serve it, with fixed argument members, so clients need not hard-code them. |
MCP capabilities defined for version 1:
| Name | Use |
|---|---|
team-memory-read |
List or read team memory: the named tool with the named argument and the fixed members (circle-hub: get_nodes with memory, scope: "team"). |
entry-share |
Share one verified own entry to team knowledge (circle-hub: upload_file with share_entry). |
memory-share |
Share own memory to the team, first with check_only: true (circle-hub: upload_file with memory_share). |
Sync capabilities defined for version 1:
error-detail. Error answers carrydetail: a non-empty text (at most 2,000 characters) for people, made of the code’s registered explanation and next step. It adds no fact beyond the code, so a foreign and an absent object still answer identically. The 401 Bearer challenge is unchanged and carries nodetail. Clients showdetailfor codes they do not know.-
purge-confirmation.POST /modetolocalwith a validexpected_state,checkandconfirm_purgeis not performed. The server records the exact request, bound to the account, epoch and request hash, for 10 minutes, and answers 403 with{"code": "SYNC_PURGE_CONFIRMATION_REQUIRED", "detail": …, "confirmation": {"url": …, "expires_at": …}}.urlis where the account holder confirms (circle-hub: the console’s sync settings, behind a password session and CSRF check, which a sync or MCP token cannot open). Repeating the request before confirmation answers the same refusal and does not record a second request. After confirmation the server performs the recorded request as is: the client’s identical retry, orGET /operations/{operation_id}, returns the mode result. If the content changed before confirmation, the confirmation fails withSYNC_VERSION_CONFLICTand the client starts again with/mode/check. An expired request is never performed. Purging from the server’s own console is unaffected, and so is switching back tohub. first-sync-notice.personal-sync-state@1carriesnever_synced:truewhile no client of the account has ever uploaded, committed, aborted a session or changed the mode on this server, in any epoch; native edits in the server’s console or through MCP do not count. A client connecting to a server SHOULD show the account holder, before its first upload, that personal content will be synced to the server and that the mode can be changed tolocalat any time, whennever_syncedistrueand the mode ishub. The server only reports the fact; it never changes the mode on the client’s behalf, and the default mode stayshub. Clients of servers without the capability decide from their own records.share-single-item. Share one entry and only its own sources to team knowledge without a server copy of the personal space, in either mode, includinglocal.- The team mark is a client-local flag on one entry item (
item_id), set only by the account holder’s explicit action (for example 分享到团队). It is not part of any object, does not change object IDs and never syncs as content. A client in modelocalthat is connected to a server advertising this capability sends a single-item share only for an item carrying the mark; otherwise the item stays on the client. POST /sharewithpersonal-sync-share@1:{schema, epoch, operation_id, team_mark: true, item, path, team_path?, objects: [{id, bytes_base64}]}.itemis the object ID of the entry revision to share,paththe folder the item has on the client (the default team folder) andteam_paththe team folder to use instead.team_markMUST betrue.objectsholds exactly the entry revision, its quotationbytes, and thesource_revisionobjects itssourcegrounds name with their chunks; any other object isSYNC_OBJECT_INVALID, a missing oneSYNC_OBJECT_MISSING. The objects follow sections 5 and 11 (canonical bytes, claimed IDs,limits.batch_objects,limits.object_bytes,limits.batch_bytes). A ground on another entry revision is refused (only the item and its sources travel); areferenceground names something the server already holds.- The server re-verifies exactly as for normal sharing, in the same order: the entry has grounds, each ground
resolves with team parsers only, every quotation is verbatim, every source may be shared (an uploaded source is the
account’s own declaration through
team_mark), the team has no equal entry, and entry, folder name and sources pass the credential scan. A refusal is not an error: the answer hasstatus: "rejected"and a registeredcode(SHARE_UNVERIFIED,SHARE_GROUND_UNRESOLVED,TEAM_PARSER_REQUIRED,SHARE_QUOTE_MISMATCH,SHARE_GROUND_UNREADABLE,SHARE_DUPLICATE,SHARE_CREDENTIALS,GROUNDS_WITHDRAWN,SPACE_CONTENT_UNINSPECTABLE). Onmergedthe server appends a team entry and team-held copies of the sources; nothing personal is staged, projected or kept, and the account’s mode, state and capacity are unchanged. Team capacity applies (SPACE_CAPACITY_EXCEEDED). - Either way the server records a share receipt naming the item, the entry revision, the team mark and the raw
SHA-256 of the uploaded sources (never their bytes or names).
personal-sync-share-result@1returns{operation_id, epoch, request, item_id, object, status, code, message, team_root, reference, record, at};referencenames the team entry when merged. A later single-item share of the sameitem_idby the same account replaces only that account’s own earlier team entry. - The operation key follows section 9: the identical request returns the original answer, the same key with another
request is
IDEMPOTENCY_MISMATCH, and a changed epoch isSYNC_EPOCH_CHANGED.
- The team mark is a client-local flag on one entry item (
The request shapes of existing operations are unchanged by these capabilities, and so are the frozen vectors in
contracts/test-fixtures/sync/v1/. The additive vectors are in contracts/test-fixtures/sync/v1-capabilities/.