Repository navigation
Remote Sync
Remote transport for the §4 sync model, and wire protocol v1. Issue #42. Inputs: D-037, D-038, D-039, D-040, D-041, D-042, D-043, D-044, Repository-Semantics §4 (messages §4.2, delta §4.3, resume §4.4, validation §4.5, checkpoints §4.6, refs §4.11), Appendix A (A.1 barrier order, A.2 format v1), CLI-JSON, and the code on develop a28c722 (hub-core/src/sync.rs: Have, Ops, Need, CheckpointsMsg, answer_ops + lacking_blobs = the #34 NeedBlobs step, receive_blob; hub-cli/src/sync.rs: sync_with, persist, dup_warning, the canonical-path lock order).
This page is Repository-Semantics §5 Remote transport (it extends §4 and does not change it); Repository-Semantics §4 links here. Labels: [decided] (with a D-link), [decision-proposal] (this spec's proposal; Review/Mentor may accept), [open question], [escalation] (a new product choice for the human; not decided here). Implementation is #43; the background service is #44. Repaired after Review (#42 REPAIR R1–R6).
-
[decided] (D-037) The message exchange is transport-independent; SSH is implemented first (
barocss-hub remote add origin ssh://host/path,barocss-hub sync origin, remote endbarocss-hub serve --stdio); HTTP is designed here but implemented later. - [decided] (D-038) Topology is a central, always-on replica on a server; every client syncs with it. P2P is deferred.
-
[decision-proposal] The protocol is still symmetric: both ends are ordinary replicas running the same §4.5 validation; "client" only means "the side that opened the connection and sends the first
Have". Nothing in v1 depends on the server being central, so P2P later needs no new messages. - Refined by Online-First (D-048, #47): the protocol stays symmetric between full replicas, but a partial replica only talks to a full replica that serves it (6.1.1). "Partial clone" below is superseded: thin clients are specified in 6.2.
- Out of scope (explicit): the HTTP implementation (only its design, §5.3.2); P2P discovery/relay topology; ACL enforcement (§5.9.6 is an open question); blob packs (D-035); partial clone; in-protocol repo selection (forbidden, §5.9.1).
- A connection is a pair of ordered, reliable byte streams (stdin/stdout of
serve --stdio, an in-memory pipe in tests, later an HTTP body). No other assumption is made about the transport. -
Preamble: each side first writes the 8 bytes
BHUBWIRE(magic), then frames. A peer that reads anything else closes with no reply (catches a login banner or a shellmotdleaking into stdout, and a non-barocss-hub program answering). -
Frame:
len: u32 big-endian(length oftype+payload, 1 ≤ len ≤max_frame) ·type: u8·payload: len-1 bytes. No padding, no compression in v1. -
Payload encoding: UTF-8 text, fields separated by one space, list items by
,, using exactly the Appendix A token forms (s<escaped>strings,<counter>@<replica32hex>op ids, 64-hex blob ids, frontier lists), and op bodies are theops.logrecord bodies (O <id> <deps> <payload>, without the tab-crc; the frame length replaces the newline). Reason: one parser for disk and wire, already fuzzed by the log tests, and a byte-exact deterministic encoding (a frame sequence is a pure function of the two replicas' states and the batch size), which is what makes the in-memory transcript tests of §5.10 possible. Blob chunk payloads (Chunk) are the only binary payloads. -
Determinism: senders emit ops in
<order, blob ids sorted ascending, checkpoints inpublished.logorder; nothing depends on timing, hash-map order or wall clock except the optionalProgressframe, which tests ignore.
| type | name | direction | payload | maps to |
|---|---|---|---|---|
| 0x01 | Hello |
both, first frame | see 5.2.3 | — |
| 0x02 | Have |
requester |
frontier vv_hint|-
|
§4.2 Have
|
| 0x03 | Ops |
answerer |
sender_frontier done(0|1) then \n-separated op bodies (≤ max_ops_per_frame) |
§4.2 Ops (without blob bytes, as answer_ops) |
| 0x04 | Need |
requester | op id list | §4.2 Need
|
| 0x05 | NeedBlobs |
requester | sorted, deduplicated blob id list | #34 step (lacking_blobs) |
| 0x06 | BlobBegin |
answerer |
blob_id size offset
|
streamed blob, 5.2.4 |
| 0x07 | Chunk |
answerer | raw bytes (≤ max_chunk) followed by 32 bytes SHA-256 of those bytes |
5.2.4 |
| 0x08 | BlobEnd |
answerer | blob_id |
5.2.4 |
| 0x09 | NoBlob |
answerer | blob_id |
answerer lacks it (it is pending there too) |
| 0x0A | Checkpoints |
both |
\n-separated K … records (published only) |
§4.2/§4.6 |
| 0x0B | RefMoves |
both |
\n-separated move records |
§4.2/§4.11; only if feature refs negotiated |
| 0x0C | Done |
both | phase name (ops, blobs, meta) |
end of a phase |
| 0x0D | Committed |
receiver→sender (the side that requested and persisted) | frontier that the receiver made durable; with feature rejected-ops (#45) followed by \nR <op-id> <reason> lines for the ops of this half it rejected (equivocation, lamport-violation) |
durability ack, 5.4; rejected-op feedback, 5.2.7 |
| 0x0E | Error |
both |
code s<message> fatal(0|1)
|
5.2.6 |
| 0x0F | Progress |
both |
phase done total|-
|
UI only, optional |
| 0x10 | Bye |
both | — | clean close |
| 0x11 | Sums |
answerer |
* or resolved ids, then <replica> <count> <checksum> lines |
#46 divergence digest (5.2.8); only if feature digest negotiated |
- Unknown frame types: a fatal
Error protocolunless the type is in a feature the peer announced (5.2.3). So adding types needs a feature flag or a protocol bump.
-
Hello { proto_min, proto_max, format, format_minor_max, replica, features, limits }, text:proto 1 1 format 1 0 replica <32hex> features blobs-chunked,checkpoints,resume limits frame=<n> chunk=<n> ops=<n>. - Both sides send
Helloimmediately (the client does not wait), then read the peer's. The session protocol ismin(proto_max_a, proto_max_b)if ≥ both mins, elseError version-mismatch(fatal) naming both ranges, and close. v1 defines only protocol1. -
formatis the on-disk major format (D-029, A.2). The wire carries logical records, not files, so different on-disk formats can sync as long as each side can encode/decode the v1 record bodies; a server whose repo has a newer major format than its binary understands refuses to open it (corrupt, as locally today) beforeHello. -
features: the intersection is in force. v1 features:blobs-chunked(mandatory in v1),checkpoints(mandatory),resume(blob offset resume, 5.2.4),refs(RefMoves; not implementable yet: refs are specified in §3.8/§4.11 but not in the code on develop, so #43 omits it),progress,rejected-ops(#45, 5.2.7),digest(#46, 5.2.8).text(#54, Text-CRDT 7.9, D-053 E-T6): the side can encode and holdTextEdit(Trecords, Text-CRDT 7.4.1). In force only when bothHellos carry it. The answerer never sends aTextEditto a peer without it: if anOpsbatch it is about to send (the delta or aNeedanswer) contains one, it ends the session withError upgrade-required(fatal). For a pull from a text server the old client has persisted nothing of that half; for a push to an old server (mixed remotes: a client with atextremote and an old one), the push ends the session the same way.file://uses the same session code. ATextEditreceived withouttextnegotiated isunsupported(fatal). Old peers ignore the unknown feature name. Record size (#54): a replica never authors an op record larger thanTEXT_OP_MAX= 192 KiB (FRAME_FLOORminus 64 KiB ofOpsheader room), so its records reach every conforming peer; larger saves are chained (Text-CRDT 7.4.1). Tests:crates/hub-cli/tests/text_t2.rs, also with the pre-#54 05a1be0 binary as client and as server. -
limits: each side announces what it accepts; a sender must stay within the peer's limits (5.2.5). Floors (R4):frame ≥ 256 KiB + 64,chunk ≥ 16 KiBandchunk ≤ frame - 33,ops ≥ 1; aHellobelow a floor or above the hard caps (frame 4 MiB, chunk 1 MiB, ops 65 536) is fatalprotocol. Withframe ≥ 256 KiBevery legal op record fits in one frame (5.2.5 name limit). -
Replica identity check (D-022), repaired R5: if the peer's
replicaequals our own, sendError duplicate-replicaand end this session only. Nothing is persisted: theHelloreplica is an unauthenticated claim, so a lying client (e.g. announcing the server's own id) must not be able to write warnings into the central server. A DuplicateReplica warning is persisted only from op-level detection (§4.5: an op authored under our id that we never authored), which is evidence, not a claim. The client side, which chose the URL, may show the refusal but also persists nothing fromHello. (Differs from localdup_warning, which is safe locally because both repos are on the user's disk.) -
No repo id in
Hello: the repo is fixed by the SSH command (§5.9.1). [open question] A repo identity (e.g. the root op id) to refuse syncing two unrelated repos; today nothing prevents it locally either.
-
Problem: a pushed op that the receiver rejects (equivocation: the id is already held with another payload; Lamport violation: a dep not
<the op) was only a D-008 warning on the receiver. The sender exited 0 and never learned it. -
Rule: after the persist step of the half it requested, the receiver computes the rejected ops of that half from the replay's verdict (R1b): every received op that is neither in the applied set nor in the pending buffer;
equivocationif the id is held with another payload, elselamport-violation. Ops still pending (missing deps) are not rejections. If bothHellos carryrejected-ops, it appends them to itsCommittedframe as linesR <op-id> <reason>(reason[a-z-]{1,64}, at mostmax_listlines; anything else is fatalprotocol). -
Compatibility: the feature gates the extra lines. A pre-#45 peer's decoder would refuse them, so they are never sent to it; a pre-#45 server never sends them, and the client reports
"rejected_reported": false(it cannot know). No protocol bump. -
Client: each rejection is a
warning:line naming the op, the reason and what to do (reidfor equivocation, upgrade/redo for Lamport);sync --jsonhas"rejected": [{op, reason, message}]and"rejected_reported";watchemits arejectedevent; the VS Code extension shows a warning notification. [decided] (D-045)syncthen exits 3 (EXIT_REJECTED, distinct from 1 = error and 2 = no cwd) with its normal report: everything else of the session was committed, so stdout is the full human report or--jsondocument (rejectednon-empty) and stderr has the warnings, never an error document.watch, the D-044 service and VS Code are not affected:watchemits therejectedevent, keeps running and does not back off; VS Code shows a warning and treats asyncexit 3 with a valid document as a result (no background service exists in the repo yet; when it is added it must runwatchor treat 3 as a warning). Against a pre-#45 server (rejected_reported: false) the exit is 0. -
Hint (#49, D-049): the
duplicate-replicaerror and the equivocation/rejection hints now say "runbarocss-hub reid --rescue" (re-authors the copy's edits under a fresh id, Repository-Semantics 4.13) instead of "runbarocss-hub reid, then redo the change".reid --rescueforgets the reported-rejection entries (5.2.7.1) of the ops it re-authors.
Source: user decision D-050 (2026-10-14; first given in the Hub Compute session, reconciled in the Planner session), split from #49 into #50. Local only: no wire change, no feature, no protocol bump.
-
Memory:
.hub/reported-rejections(headerbarocss-hub/reported-rejections/v0, CRC-framed records like the other.hublogs, never replicated):R <local> <url> <exec> <op> <payload-sha256> <reason>.(local, url, exec)is the remote's #45 trust key (canonical local.hub/, normalized URL, exec word; forfile://the canonical target repository root). It's keyed by the trust key, not by the peer replica id fromHello: the replica id changes on the server'sreidand can be shared by a copy (D-022), while the trust key names what the user configured and confirmed. Since the local.hub/is part of the key, a copied or moved repository has no memory and reports again. -
Match: a rejection is already reported iff a record has the same key, op id, SHA-256 of this repository's text encoding of that op (
store::encode_op, not the binary 4.12 hash), and reason. Anything else is new: another op, another payload under the same id (a new equivocation), another reason, another remote.duplicate-replicais never remembered or suppressed. An op not held locally isn't remembered. -
Behavior:
syncwarns and exits 3 (D-045) only for new rejections. If every rejection is already reported, it exits 0 and prints nothing about them (human mode);--jsonstill lists all of them inrejected, each withalready_reported.watchemitsrejectedevents (and VS Code notifications) only for new ones. The memory only dedups the rejected-op report: the D-008 equivocation warnings, the D-022 DuplicateReplica refusal and the #46 localization (which stops the session on a new equivocation) are unchanged. -
Durability: the memory is written only after the report was printed or emitted, under the exclusive
.hub/lock: temp, flush, barrier, rename, directory sync (crash stagesreported-tmp,reported-renamed). A crash before the rename loses only the memory, so the report repeats; it never suppresses a report that wasn't shown. A corrupt or torn file is ignored, which also just means repeats. A write failure is a warning, not a sync failure. -
Listing and clearing:
barocss-hub warningslists them (reported rejection (suppressed): …;--json:reported_rejections).barocss-hub warnings --clear-reported [<remote>]forgets all of them, or one remote's, so they're reported again.reported::forget(dir, locked, pred)is the hook #49'sreid --rescueuses to drop the entries it resolves. - Old binaries and peers: a pre-#50 binary never reads or writes the file (verified: c8be24d ran status, warnings, log, record, sync and reid on a repository with the file, and the file's hash was unchanged). A pre-#45 peer reports no rejections, so nothing changes.
- Feature
digest. Only when bothHellos carry it. - Frame
0x11 Sums, answerer → requester, sent right after receivingHaveand before the firstOps. Payload: first line*(every id of the requester's frontier resolves here) or the resolved id list (F_A ∩ V_B,-if empty); then one line per author replica with ops inK:<replica 32hex> <count> <checksum 64hex>(sorted by replica, ≤max_listlines).checksum= the per-author sequence digestS_r(K)ofK = downclose(resolved)(SHA-256 over 16 384-op position chunks in<order; Repository-Semantics 4.12). Review R2 replaced the earlier plain 256-bit sum (vulnerable to Wagner's attack; never released). The additive LtHash16 is reserved for partial replicas behind a future feature. The canonical op hash input is length-prefixed binary (id, sorteddeps, payload tag and fields), independent of the text encoding. - Requester: compares with its own sums over
downclose(resolved). Mismatch → localization with ordinaryNeedframes (newest first, ≤ 1024 ids per round, stop at the first new equivocation), thenDone ops; at persist, a new equivocation stops the session withduplicate-replicaafter persisting only the warnings. No new frame is needed for the evidence: the peer's ownOpsanswer is the evidence (R5). - Review R1: when one side stops with a fatal
Errorand closes while the other is still writing, the writer reads the bufferedErrorframe and reports the peer's code (for exampleduplicate-replicawith thereidhint), notio;file://also prefers the far end's error. - Both halves use it, so the server also detects divergence in the push half when the pull half did not stop first.
- Old peers: no
Sums, no detection (as pre-#46). An old peer ignores the unknown feature name inHello; it never sees frame0x11. - Cost: hashing every op once per side per session (derived at load, nothing stored), one
Sumsframe per half.
A sync is pull then push over one connection, each half exactly the #34 in-memory pull:
-
Hello↔Hello. - Pull half (client requests): client
Have→ serverOps…(done=1), frames in<order. After eachOpsframe the client computeslacking_blobsand sendsNeedBlobs; the server streams each requested blob; the client sendsNeedfor missing deps (4.4) if any. Client sendsDone ops. - The client persists (5.4) and sends
Committed <frontier>. - Push half (server requests): the same with roles swapped — server
Have→ clientOps…→ serverNeedBlobs/Need→ server persists →Committed. - Meta: both send
Checkpoints(andRefMovesif negotiated), thenDone meta; each side persists its received records and repliesCommitted. -
Bye↔Bye.
-
Blob streaming (tightened, R4):
BlobBegin id size offsetis accepted only ifidis in the receiver's outstandingNeedBlobsset (unsolicited id → fatalprotocol),offset ≤ sizeandoffsetequals the requested offset (elseprotocol),size ≤ max_blob_size(elselimit); aChunkthat would carry the total pastsizeis fatalprotocol, andBlobEndbeforesizebytes is fatalprotocol. One blob in flight at a time;BlobBegin id size offset·Chunk* ·BlobEnd id. The receiver writes chunks straight toblobs/tmp-*(never buffers a whole blob; this is the #34 RSS note), checks every chunk's SHA-256 trailer (a bad chunk = transport damage: fatalError corrupt, temp discarded), and atBlobEndchecks the whole-file SHA-256 against the id; a mismatch is a CorruptBlob warning for that peer (D-021), the file is dropped, and the ops that need it stay pending, exactly asreceive_blobtoday. -
Resume inside a blob (feature
resume): a receiver keeps a partially received temp asblobs/part-<blobid>(flushed, not durable-promised). In the next session'sNeedBlobsit may writeid@offset; the sender starts at that offset and the receiver re-hashes the kept prefix before appending. A kept part may have come from a different peer, so when the whole-file hash fails after a resumed transfer (offset > 0), the receiver deletes the part and re-requests the blob once from offset 0 in the same session, with no warning (R6); only a mismatch of a transfer that started at 0 yields CorruptBlob. Parts are swept after 7 days or when the blob exists ([open question] the age). - Chunk size default 256 KiB; the per-chunk hash costs ~0.1% of bandwidth and lets a resume trust a prefix.
| limit | default | on violation |
|---|---|---|
max_frame (len field) |
1 MiB (4 MiB hard cap in v1) | fatal Error limit, close, before reading the payload |
max_chunk |
256 KiB | fatal limit
|
max_ops_per_frame |
1024 | fatal limit
|
max_list (ids in Need/NeedBlobs/frontier) |
65 536 | fatal limit
|
max_blob_size (server config) |
2 GiB |
Error limit non-fatal for that blob; ops needing it stay pending on the receiver |
| step timeout (no complete frame) | 60 s; Hello 15 s |
fatal timeout
|
| session wall time (server) | 1 h | fatal timeout
|
| concurrent sessions per repo (server) | 8; the repo lock already serializes persist steps |
Error locked (non-fatal, retry later) |
| rate (server) | none in v1; [open question] per-key rate limits are left to sshd/MaxStartups
|
— |
-
Record size limit (R4) [decision-proposal]: a name is at most 4096 bytes (after the §1.3 rules; checked on the raw bytes), and a checkpoint message/note at most 64 KiB each. So an escaped op record (≤ 3 bytes per name byte + ~300 bytes of ids) and a
Krecord stay far below the 256 KiB frame floor, and one record always fits in one frame. Enforced at authoring:record/watch auto-record (a working-directory name over the limit is skipped with a warning, like an unreadable file;recordreports it),mkdir/touch/mvandcommit -m/publishrefuse withinvalid-path(names) orusage(messages). On receipt, an op with an over-long name is accepted into the log as a §2.2 invalid op (conflict, never projected), as for other bad names. Already existing oversized records (possible only from pre-#43 binaries): the sender never splits a record across frames; if a single record exceeds the peer'smax_frame, it sends fatalError limitnaming the op id, and the session ends with a clear message ("op X is larger than the remote accepts; run with a larger frame limit"). Nothing is silently skipped, because skipping an op would make every later op pending forever. - Limits are enforced before allocation (the length prefix is checked before reading the payload), so a hostile length cannot make the server allocate.
-
Error { code, message, fatal }. Codes reuse the CLI-JSON error codes where the meaning is the same:not-a-repository,corrupt,io,locked,duplicate-replica,bad-blob-id,invalid-path,error.upgrade-required(#54; also a CLI-JSON code): the peer lacks featuretextand the ops it would receive include text edits (5.2.3). New wire-only codes:protocol(malformed frame, unknown type, frame out of order),version-mismatch,limit,timeout,unsupported(feature used but not negotiated),forbidden(path confinement, §5.9). - A fatal error closes the connection after the frame; the peer keeps everything it has already made durable (5.4). The CLI maps a remote
Errorto the same code in its own--jsonerror ({"error":{"code":…,"remote":true}}) and exit code 1; transport failure (ssh exits non-zero, EOF) isiowith the ssh stderr tail in the message. - Op-level problems are not errors: equivocation, Lamport violation, corrupt blob and invalid op go through §4.5 (5.8) and become warnings/conflicts; the session continues.
-
barocss-hub serve --stdio <path>: open the repo at<path>(after confinement, §5.9.2), speak the protocol on stdin/stdout, log to stderr, exit 0 afterBye, 1 after a fatal error. It never writes a working directory and never reads user input. - It serves exactly one repo per process (§5.9.1) and holds the repo lock only during persist steps (A.1), so a local
recordon a non-bare server repo is not blocked for a whole session. -
--root <dir>(else envBAROCSS_HUB_SERVE_ROOT, elseserve.rootin a server config; env ignored under a forced command, 5.9.6) sets the confinement root.
-
Extended by Online-First 6.5:
serve --httpbecomes the single-writer daemon with live sessions and the JSON API; auth is required (E-B1 supersedes E-R3). - One
POST /<repo>/syncper session, request body and response body each a stream of v1 frames (HTTP/1.1 chunked or HTTP/2), after the same preamble; frame semantics identical. Because v1 is request/response per half, a non-streaming fallback (one POST per phase carrying asessiontoken) is possible later. Auth (TLS plus a bearer token, reverse-proxy auth or mTLS) is an [open question] (E-R3). Not implemented in #43.
-
file:///pathruns the same protocol over an in-process pipe with a server thread on the other repo. Per D-041 it is the successor ofpeer; the existing directsync_withpath may remain the implementation forfile://until the protocol version lands, and the two must produce identical results (property P1).
-
Rule: each side persists received data with the A.1 order before acknowledging it;
Committed <frontier>is sent only after the persist step's final barrier/log sync. The sender treats nothing as delivered untilCommitted, and nothing depends on it anyway: resume is stateless (§4.4),Kis recomputed from the nextHave. -
What each side persists, per half, as the local sync does today (A.1 "sync" and the "Sync persist order"): 1. blobs (direct writes of verified files, flush,
flush(blobs/), one barrier; [blobs-written], [blobs-barriered]); 2. new applied ops appended in<order, one log sync ([ops-appended], [ops-fsynced]); 3.pending.logandwarnings.log(write_atomic); 4. published checkpoints appended (and ref moves later). Under format v1 (A.2) step 2 is one commit-marked batch, so no partial batch is visible. -
When: the receiver persists at the end of its half (after
Done opsand all requested blobs), not per frame. [decision-proposal] Additionally, for large deltas it persists everypersist_every= 64 MiB of received blob bytes or 100k ops, emittingCommittedeach time, so a drop late in a big transfer loses at most one window. -
Drop mid-ops: nothing of the unpersisted window is on disk (it was only in memory), so the repo is the previous version; ops already
Committedare durable. The rerun'sHavecarries the new frontier; the answer isV_B \ Kagain (4.3), so persisted ops are skipped and nothing is missed. -
Drop mid-blob: complete verified blobs of the window were not yet flushed-and-barriered, so they may or may not survive; either way no op names them yet (orphan blobs are harmless, reused by the rerun, A.1). The partial blob is kept as
part-<id>for resume (5.2.4) or discarded. -
Drop between persist and
Committed: the data is durable but the peer did not see the ack; harmless, the nextHaveshows it. -
Superseded for a daemon-served repo by Online-First 6.1.2 (single-writer daemon, #47); the R1 model below still applies to
serve --stdiowithout a daemon. -
Concurrency model (R1) [decision-proposal]: persist-only lock plus reload-and-merge. develop's
sync_withholds both repos' exclusive locks for the whole session; a remote session cannot, because it would block the server (and every other client) for as long as the slowest network. Soservetakes the exclusive repo lock only for each persist step, and each persist step, under the lock: (1) reopens the on-disk state (ops, blobs,pending.log,warnings.log,published.log); (2) replays every op and blob the session received (in arrival order) throughreceive_ops/receive_blobagainst that reloaded state (R1b), so equivocation, Lamport, duplicate-replica, corrupt blob and pending promotion are evaluated against the merged state, not the snapshot the session loaded at start; a same-id op with a different payload that another session already persisted is therefore an equivocation with a D-008 warning, never a silent id-dedup drop. Then pending = reloaded pending ∪ replay's pending, minus ops now applied; warnings = reloaded ∪ replay's warnings (dedup by key, earliestfirst_seen); published = reloaded ∪ received (checkpoints re-filtered as §4.6). The session's earlier in-memory validation is only advisory (for earlyNeed/NeedBlobs); only the replay's verdict is written; (3) writes with the A.1 order;pending.log/warnings.logare rewritten from the merged set, never from the session's stale snapshot. Consequences: concurrent sessions (up to 8) never lose each other's pending ops or warnings (P16); a session'sHavemay be stale by the time it persists, which is harmless (redundant ops dedup; a laterHavefetches the rest); theCommittedfrontier is the merged frontier. The client keeps the develop model (session-long exclusive lock on its own repo), since only the local user is affected. -
Lock model as built (#43, recorded by #45) [decision-proposal]:
-
file://sync runs the v1 protocol in-process (client and serverSideover an in-memory pipe) and holds session-long exclusive locks on both repositories, taken in canonical-path order (the smaller canonical.hub/path first). Everyfile://sync, from either end, takes the pair in the same order, so two concurrenta→bandb→asyncs cannot deadlock; one waits. Both repos are on the user's disk, with no network latency, so holding both for the whole session is cheap and keeps the develop semantics (no stale state, nothing to replay). -
serve --stdio(the served end of ssh) uses persist-only locks with reload and replay (R1, R1b above): a shared lock to load, the exclusive lock only for each persist step. A session can be held open by a slow or hostile network client for up to the session wall clock; holding the server's lock that long would block every other client and a localrecordon a non-bare server. - The ssh client holds its own repo's exclusive lock for the session (as
file://'s client end).
-
- Server side is symmetric: in the push half the server is the receiver and follows the same order; a drop never leaves the central replica with an op whose deps or blob is missing (A.1 clause 2). Pending ops and warnings from a client are persisted server-side exactly as on a client.
-
No acknowledged data lost: the only acknowledgements are
Committedframes and the command's exit 0; both happen after the barrier.barocss-hub sync originexits 0 only after both halves and meta areCommittedon both sides.
-
[decided] (D-037) git-like commands:
barocss-hub remote add <name> <url>,barocss-hub sync <name>. -
[decision-proposal] Also
remote list [--json],remote rm <name>,remote set-url <name> <url>.barocss-hub syncwith no argument syncs the default remote. -
[decision-proposal] Stored in
.hub/remotes, local config, never synced, write_atomic under the repo lock, likepeers: headerbarocss-hub/remotes/v0, one line per remoteR s<name> s<url> <key=value>*(keys:exec=<remote binary path>,default=1, laterhttp.token-file=). Remote names follow the §1.3 name rules plus no/. -
[decided] (D-041)
peersmerges intoremotes: thepeercommand goes away; a same-machine folder isremote add <name> file:///abs/path. Migration [decision-proposal]: the first command that takes the exclusive repo lock and finds.hub/peers(and nopeersentries already inremotes) converts each peer root to a remote namedlocal-<n>(n = 1.. in file order; if the basename is a valid unused name, that name instead), URLfile://<canonical root>, merged with any existingremotes; writesremoteswith write_atomic (fsync, rename, dsync); only then removespeersand dsyncs.hub/. Crash before the rename:peersintact,remotesunchanged, rerun repeats. Crash after the rename, before removingpeers: the rerun seespeersagain and merges idempotently (dedup by URL), then removes it. A read-only command (shared lock) reads both files without migrating.peer add/rmprint a usage error pointing toremote. The watchstartedevent'speerscount becomesremotes(CLI-JSON change for #43). -
[decision-proposal] Default name
originfor the firstremote addwhen no name is given, and the first remote added becomes the default;remote add --defaultchanges it. -
URL forms [decision-proposal]:
-
ssh://[user@]host[:port]/abs/path— path absolute on the server;ssh://host/~/repois expanded by the server relative to the login home (then confined). - scp-like
[user@]host:path— relative paths are relative to the login home (as git); recognized only if there is no://and the part before:contains no/. -
file:///abs/pathand a bare local path. -
https://host/path— reserved, refused withunsupporteduntilserve --httpexists. - Anything else:
usageerror. Userinfo passwords (user:pass@) are refused (§5.6).
-
-
Command line:
ssh [-p <port>] [-l <user>] -o BatchMode=<yes if non-interactive> -- <host> '<exec> serve --stdio <quoted path>', with<exec>=exec=of the remote (defaultbarocss-hub).sshis found on PATH;BAROCSS_HUB_SSH(likeGIT_SSH_COMMAND) overrides the program and fixed args. The remote path is single-quoted with'escaped as'\''; a path containing a newline or NUL is refused locally. -
Injection hardening (Issue #45) [decided by the #45 security fix; details decision-proposal]. The remote command is interpreted by the remote login shell, so every component is restricted, and re-checked when the ssh argv is built (defense in depth):
-
exec: one word of[A-Za-z0-9._/~+-], non-empty, ≤ 1024 bytes, not starting with-. No space, quote,;,&,|,$, backquote,(),<>,\, glob or brace character can appear, so it is safe unquoted as the first word (~is allowed for~/bin/barocss-huband is inert elsewhere in a word). The #43 check (printable, no quote) allowedbarocss-hub;id, which ranidon the remote. -
user:[A-Za-z0-9._-], not starting with-.host: a DNS name/IPv4 of[A-Za-z0-9._-]not starting with-or., or a bracketed IPv6 literal[hex:…[%zone]](brackets stripped for ssh). Nothing in them can be syntax for aProxyCommandshell after ssh's%h/%rexpansion (cf. CVE-2023-51385).port: a u16. Host and user are separate argv entries after--/-l; no local shell is ever used. -
path: absolute or~/…(so it can never be read as an option ofserve; a relative scp path is sent as~/<path>,~useris refused), printable ASCII (the forced-commandpchar), and none of\or!(special inside single quotes in fish, resp. csh history expansion). It is always single-quoted. - All of this is validated at
remote add, atremote set-url, and when.hub/remotesis loaded: a hand-edited or planted file with an unsafe name, url or exec makes every command that reads remotes (sync,watch,remote list) fail withcorrupt("refusing .hub/remotes: remote X is unsafe: …; nothing was run"), before ssh is started;remote rm <name>still works to repair it.
-
-
Binary discovery: the remote login shell's PATH, unless
exec=(per remote) or--execgives an absolute path (for installs in~/.local/binor~/.cargo/binnot on a non-interactive PATH). A missing binary shows up as ssh exit 127 before the preamble; the CLI reportsiowith a hint to setexec. -
Host keys, keys, agents, ProxyJump, ControlMaster: entirely delegated to
sshand the user's~/.ssh/config. barocss-hub never parses known_hosts, never accepts a host key, never reads or asks for a password or passphrase. stdin of ssh is the protocol stream, so interactive prompts go to the tty (sshuses/dev/tty); the background service (§5.11) runs withBatchMode=yes, so a missing key fails instead of hanging. - stderr of ssh is passed through (human mode) or captured into the error message (
--json).
-
[decided] (D-042)
barocss-hub init --bare <dir>creates<dir>/.hub/, laid out exactly as a normal repo's, plus an empty marker file.hub/bare;<dir>itself holds nothing else and is never used as a working directory.serveaccepts the path of<dir>. - No working directory, no
index, nopinned, nocheckout-in-progress, no.hubignoreevaluation, no watch. - Allowed on a bare repo:
serve,status(head, counts, warnings; no working-directory section),log,show,cat,cat-blob,ls,warnings,commit/publishof an existing version id (metadata only),reid,remote/sync(a server may also pull from another server). Refused with a new codebare([decision-proposal], added to CLI-JSON):record,checkout,mkdir/touch/mv/rm,watch. - A non-bare repo can also be served (P2P-ready); sync is log-only (§4.4), so serving never touches its working directory.
-
[decided] (D-006) Only published checkpoints are sent: the sender reads
published.logonly;checkpoints.log(private) is never opened by the serve/sync send path (property P9). A received record identical to a private one marks it published (§4.6). -
[decided] (D-007, D-009, D-010, D-011, D-012, D-013, D-015–D-018) Refs: only published ref move records travel in
RefMoves, evaluated by the receipt-side validity of §4.11 (FF against all heads,published/prefix reserved, local wins on clash). Local refs never leave the replica. Because refs are not implemented on develop,RefMovesis gated behind featurerefsand absent from #43. -
[decision-proposal] Sender filter: only checkpoints/moves whose frontier is in the sender's applied
V(§4.6); the receiver keeps unresolvable ones pending. -
[decided] (D-008, D-021, D-022) Equivocation, Lamport violation, CorruptBlob, DuplicateReplica are recorded as local warnings naming the peer = the remote's replica id from
Hello(for display, also the remote name). A persisted DuplicateReplica (op-level only, 5.2.3) stops later syncs with that peer (fatalduplicate-replica) untilreid(D-025). -
[decision-proposal] Warnings are local and not sent over the wire in v1. On a central server, the server's warnings are visible to the server admin via
status/warnings; [open question] whether clients should be able to fetch the server's warnings (aWarningsframe behind a feature) so a user learns that the server rejected their op. - [decision-proposal] Relay: the server relays everyone's ops (they are in its log), and relays published checkpoints it holds; an op it rejected (equivocation/Lamport) is not in its log and so is never relayed; a pending op is not relayed (only the applied frontier is advertised, §4.4).
-
serve --stdio <path>serves only the repo named on its command line. No frame names a repo or a path; there is no frame that could switch repos. (Property P11.)
- The server canonicalizes
<path>(realpath, resolving every symlink) and requires it to be inside the canonical--rootwhen a root is configured (default in forced-command setups; without a root, the OS account's permissions are the boundary, as with git). Rejected withforbidden: a path with a..component (even before canonicalization), a path whose canonical form leaves the root, a repo whose.hub/or any store file is a symlink pointing outside the repo, a path that is not a repository (not-a-repository). Checks run before any file is opened for writing. (P12.) - Blob files are addressed only by strict 64-hex ids (
bad-blob-idotherwise), so no received id can form a path;part-<id>names likewise. Op names obey the §1.3 name rules (no/, no NUL, not./.., non-empty) and are validated by the existing decoder; an op breaking them is a §2.2 invalid op (recorded as aninvalid-opconflict, never projected), exactly as locally. Checkpoint messages and ref names are data, never paths. Remote names in.hub/remotesare validated the same way. (P13.)
-
Threat:
.hub/remotestravels with a folder. A repository someone hands you (archive, shared or synced drive, copied disk) may name an attacker's host, user orexec, or, withfile://, another of your own repositories (Review reproduced a plantedfile:///…/victim default=1pulling the victim's content into the planted folder and pushing planted ops into the victim and on to its team server). With D-044 auto-sync this would happen without any act of yours. -
Rule: every remote read from
.hub/remotes(ssh andfile://) is contacted only if this user trusts it from this repository. Trust lives in a per-user store outside every repository ([decided] D-046): on macOS~/Library/Application Support/barocss-hub/trusted-remotes(XDG_CONFIG_HOMEis not consulted on macOS, like other macOS applications; one rule per platform keeps the location predictable); on Linux and other Unixes$XDG_CONFIG_HOME/barocss-hub/trusted-remoteswhenXDG_CONFIG_HOMEis absolute, else$HOME/.config/barocss-hub/trusted-remotes.BAROCSS_HUB_CONFIG_DIR(absolute; the directory holdingtrusted-remotes) overrides it on every platform; the tests always set it (with a temporary dir), so they never touch the real~/Libraryor~/.config. macOS migration: a store written by the first #45 rounds at$XDG_CONFIG_HOMEor~/.config/barocss-hub/trusted-remotesis still read (its entries count) and is moved on the next write: the write merges its entries into the new store (temp + fsync + rename, under the store lock), then deletes the old file. Not done whenBAROCSS_HUB_CONFIG_DIRis set. Headerbarocss-hub/trusted-remotes/v1, linesT <local> <url> <exec>(escaped as.hub/remotes). The key is:-
local: the canonical.hub/of the repository syncing (R2), so a planted folder carrying the exact URL of an already-trusted team server is a new, untrusted remote; -
url:ssh://[user@]host[:port]/pathnormalized, orfile://<canonical target>(realpath, symlinks resolved; a missing target keeps the literal path, and is refused as untrusted without being created); -
exec: the exec word (defaultbarocss-hub),-forfile://. Changing host, user, port, path, target or exec by hand, or copying the folder elsewhere, makes it untrusted again.
-
-
Old entries: the first #45 round wrote
…/v0entries withoutlocal. They no longer match: each such remote needs one re-confirmation (remote trust,--yesor the prompt), and the next write replaces the file with v1. -
Becoming trusted:
remote addandremote set-url(the user's own act; a localremote add ../bstays painless),remote trust <name>, or a first contact confirmed on a terminal or withsync --yes. Thepeers→remotesmigration grants no trust [decision-proposal]: a.hub/peersfile can be planted just likeremotes, and the migration cannot tell who wrote it; each migrated remote needs one confirmation. (Argument for carrying trust: the user created those peers withpeer addon a pre-#43 binary. It is rejected because the file is indistinguishable from a planted one, and the cost is one confirmation per remote per repository.) -
CLI: first contact asks
Trust it and connect now? [y/N]when stdin and stderr are terminals and--jsonis not given; otherwise it refuses withuntrusted-remote(exit 1; the message names the remote, URL, exec, the local repository and the two ways to trust it) unless--yes. Without a usable location (no absoluteHOME, nor the override or, off macOS,XDG_CONFIG_HOME) it refuses unless--yes(which syncs without recording). A URL or path typed on thesynccommand line is the user's own act and is not gated. -
watch / service (D-044): an untrusted remote is skipped with one
untrusted-remoteevent per remote per run; not an error, no backoff, re-checked every cycle (aremote trusttakes effect without a restart).watch --no-synccontacts no remote. -
VS Code: declares
capabilities.untrustedWorkspaces: limited. In Restricted Mode it startswatch --no-syncand says so once ("Manage Workspace Trust"); granting trust restarts running watchers with sync.untrusted-remoteandrejectedwatch events become warning notifications.
- The server executes nothing a client sends; the only process started is
sshon the client, with a command built from local config. The server never runs hooks in v1. [open question] server-side hooks (notify on push) later, admin-configured only.
- Ops and blobs from the wire go through exactly
SyncReplica::receive_ops/receive_bloband the §4.5 hooks: equivocation and Lamport violation rejected with a D-008 warning, corrupt blob dropped with a CorruptBlob warning, invalid ops recorded as conflicts, unknown deps buffered. There is no wire-specific fast path that skips validation. (P14.)
- Frame/chunk/list/blob limits, timeouts and session caps of 5.2.5; limits checked before allocation. [open question] Bound on the pending buffer a peer can grow with ops whose deps never arrive (already open in §4.4); v1 proposes a cap of 100k pending ops per peer, after which further such ops are dropped with a warning.
- [decided] (D-037) Authentication is SSH's.
-
[decided] (D-043)
serve --read-onlyis in the alpha. The server announces featureread-onlyin itsHello(R7); a client seeing it runs the pull half only, skips the push half and the meta phase except receiving the server'sCheckpoints(it sends an emptyDone metaand noCheckpoints), and exits 0 with the noticeremote is read-only; pulled only(--json:"read_only": truein the sync document; watchsyncedevent getsread_only: true), so the D-044 service does not error every cycle. Otherwise (a client ignoring the flag): the server answers the pull half and sends itsCheckpoints, but never sends aHavefor the push half, and anyOps,BlobBegin/ChunkorCheckpoints/RefMovesframe from the client is refused with fatalforbiddenbefore anything is written (it never takes the exclusive lock). Use it in a forced command for read-only keys. -
[open question] Finer ACLs (per-repo, per-user). Without
--read-only, anyone who can runserveon the repo can read and write it. - Forced-command example (server
~/.ssh/authorized_keys), confining one key to serving under one root:
restrict,command="barocss-hub serve --stdio --root /srv/hub --from-ssh-original-command" ssh-ed25519 AAAA... alice@laptop
restrict,command="barocss-hub serve --stdio --read-only --root /srv/hub --from-ssh-original-command" ssh-ed25519 AAAA... ci@build
restrict (OpenSSH ≥ 7.2) disables port/agent/X11 forwarding and pty, and future forwarding features; recommended over listing no-* options.
-
--from-ssh-original-command[decision-proposal] (R3) parsesSSH_ORIGINAL_COMMANDwith this exact grammar and nothing else (no shell is ever invoked):
cmd = word SP "serve" SP "--stdio" [SP "--read-only"] SP path
word = 1*( %x21-7E except "'" ) ; the client's binary name, ignored
path = "'" 1*( pchar / "'\''" ) "'" ; exactly what §5.6 emits
pchar = %x20-7E except "'" ; printable ASCII, no quote
SP = %x20 ; exactly one space
Anything else (extra or missing spaces, other options, unquoted path, " quoting, backslashes other than inside '\'', non-ASCII, control bytes, trailing text) exits with forbidden. A path produced this way is then confined: it must be absolute or start with ~/ (expanded to the login home); ~user is refused. A --read-only in the authorized_keys command cannot be removed by the client; one in SSH_ORIGINAL_COMMAND can only add it. (Non-ASCII repo paths are therefore not servable through a forced command in v1; [open question] follow-up: allow valid non-control UTF-8 in pchar.) The scp form host:path and ssh://host/path map onto this grammar only as an absolute path or ~/…: the client sends a relative scp path as ~/<path>. exec= values are restricted to [A-Za-z0-9._/~+-] (§5.6, #45; a subset of word), at remote add, set-url and load.
-
Root precedence:
--rooton the command line wins overBAROCSS_HUB_SERVE_ROOTand the server config. Under--from-ssh-original-commandthe environment is ignored entirely for root and exec (a client could inject variables where sshd'sAcceptEnv/PermitUserEnvironmentallow it).
- Old client / new server and vice versa: protocol ranges intersect → the older protocol is used; otherwise both sides print
version-mismatchwith both ranges and exit 1 without writing anything. - A pre-#43 binary on the server (no
servecommand): ssh returns the binary's usage error before the preamble; the client reportsio: "remote barocss-hub has no serve (too old?)". A non-barocss-hub program: no magic →protocol. - Format-v1 minor revisions (A.2): a minor revision may add record kinds only behind a wire feature; a peer never sends a record kind the other did not announce (
format_minor_max). A received unknown record kind despite that isprotocol(fatal), not silently dropped, so no data is lost unnoticed. - Protocol 2 would be required for: changing frame layout, changing an existing frame's meaning, or making a feature mandatory.
-
D-039: the service runs
watchplus remote sync for registered repos. [decided] (D-044) remote sync is automatic whenever a repo has a remote; adding a remote is the opt-in. [decision-proposal]watchsyncs with every remote (localfile://ones included, which replace peers) after each auto-record and on a timer (e.g. 60 s, backoff on failure); each result is asynced/errorevent in CLI-JSON §3 withpeer= the remote name. ssh runs withBatchMode=yes. Details (registration, scheduling, backoff, logs) belong to #44. -
D-040 + D-044 ([decided]): with
autoStartWatchtrue, opening a folder starts watch, which syncs with the repo's remotes by default.
-
P1 Equivalence: for random replica pairs, sync over an in-memory pipe transport yields the same ops, blobs, pending, warnings and published sets on both sides as the #22/#34 local
pullboth ways (comparesync_prop). -
P2 Determinism: the frame transcript of a session is a pure function of the two states and limits (byte-identical across runs;
Progressexcluded). -
P3 Disconnect at every frame: for each k, cutting the pipe after frame k (in either direction) leaves both repos opening as valid, causally closed versions (A.1 clause 2), and a rerun converges to the P1 result.
-
P4 No acknowledged loss: with the A.1 crash hooks, a crash after a
Committedfor frontier F never loses any op ≤ F on that side. -
P5 Blob resume: a drop inside a blob and a rerun with
resumetransfers only the remaining bytes (+ one chunk), and the result equals P1; a tampered kept prefix is detected and re-fetched. -
P6 Per-chunk integrity: flipping one byte of a
Chunkgives fatalcorruptand no blob file; a correctly chunked blob whose content does not hash to its id gives a CorruptBlob warning and pending ops (asreceive_blob). -
P7 Limits: a frame with
len>max_frame, a chunk >max_chunk, a list >max_listeach giveError limitwithout allocating the payload (measured by a counting allocator or a fixed cap on read buffer). -
P8 Version refusal: disjoint protocol ranges → both sides exit 1 with
version-mismatch, no file written; overlapping ranges → the lower max is used. -
P9 Privacy: a private checkpoint (and, later, a local ref) never appears in any frame, for any history (property test over transcripts).
-
P10 Duplicate replica (reworded by #45 to match R5): two repos sharing a replica id → the
Hellocheck gives fatalduplicate-replicafor that session only, and nothing is persisted fromHelloon either side (an unauthenticated claim, R5). A DuplicateReplica warning is persisted only from op-level detection (an op authored under our id that we never authored), and a persisted one stops syncs with that replica untilreid; afterreidsync works. -
P11 One repo: no byte sequence sent by a client causes the server to open any file outside the repo it was started on (fuzz the frame decoder with a filesystem-access log).
-
P12 Confinement:
serve --root Rrefuses.., absolute paths outside R, symlinks (repo dir,.hub/, or a store file) escaping R, withforbidden, before opening anything for write. -
P13 Names and ids: a blob id that is not 64 lowercase hex, an op with a name violating §1.3, and checkpoint messages containing
/,..or NUL never create or touch a path outsideblobs/(ids refusedbad-blob-id; bad names becomeinvalid-opconflicts, as locally). -
P14 Same validation: a scripted malicious peer (equivocation, Lamport violation, corrupt blob, invalid op, op authored under the server's own id) produces the same warnings/conflicts over the wire as with the local sync.
-
P15 Forced command: with
--from-ssh-original-command, anySSH_ORIGINAL_COMMANDother than the §5.6 form exitsforbidden, and shell metacharacters in it are never interpreted. -
P16 Concurrent sessions (R1): N ≥ 2 concurrent
servesessions on one repo, each delivering ops with missing deps and ops that raise warnings, persist in any interleaving; afterwardspending.log∪ log contains every pending op of every session andwarnings.logevery warning (no lost update), and the result equals running the sessions sequentially. Equivocation across sessions: two concurrent sessions sending different payloads under the same OpId → exactly one payload is in the log, a D-008 equivocation warning naming the later-persisting session's peer is persisted, and nothing is dropped silently (R1b). -
P17 Forced-command grammar (R3): a fuzzed
SSH_ORIGINAL_COMMANDis accepted iff it matches the 5.9.6 grammar;~user/…, env-provided roots under a forced command, and--rootoverridden by env are all refused/ignored. -
P18 Limit floors and blob framing (R4):
Hellowithchunk=0,frame=1or below the floors is fatalprotocol;BlobBeginwith an unsolicited id,offset > size, or chunks pastsizeis fatalprotocoland leaves no blob file. -
P19 Record size (R4):
mkdir/touch/mvof a 4097-byte name refuses withinvalid-path; every op any command can author encodes to ≤ 256 KiB; an existing record larger than the peer'smax_framegivesError limitnaming the op, never a hang or a skipped op. -
P20 Hello id cannot poison (R5): a client announcing the server's replica id gets
duplicate-replicaand the server'swarnings.logis byte-identical before and after; an op authored under the server's id still gives a persisted DuplicateReplica warning. -
P21 Part from another peer (R6): a
part-<id>whose prefix came from peer X, resumed from peer Y, is retried from 0 once and gives no CorruptBlob warning when Y's full blob is correct; if the from-0 transfer also mismatches, exactly one CorruptBlob naming Y. -
P22 Read-only (D-043): under
serve --read-only, anyOps,BlobBegin,Chunk,CheckpointsorRefMovesfrom the client givesforbidden, and the repo (all files, mtimes) is unchanged; pulls still equal P1; a normal client syncing with a read-only server exits 0 withread_only: true, sends no push orCheckpointsframe, and its pull result equals P1 (R7). -
P24 No injection (#45): with a fake ssh that evaluates the remote command with
sh -c, no shell metacharacter (; & | \$ ( ) < > \ " ' space tab newline CR * ? [ ] { } ! # ~ % ^ = ,) in any position of exec, user, host, port or path, given atremote add,remote set-url, in a planted.hub/remotes, or on thesynccommand line, runs a command; unsafe values are refused before ssh starts, and every command that reaches the shell is exactlyserve --stdio ''(tests/injection45.rs`, with a positive control proving the fake executes unquoted commands). -
P25 First contact (#45, repaired): a planted ssh or
file://remote (also via a symlink), or a planted copy of a trusted URL in another folder, is never contacted (both repos byte-identical) and a migrated peer starts untrusted; a planted ssh remote is never contacted bysyncwithout a terminal or--yes(untrusted-remote), is asked about on a terminal, is skipped bywatchwith exactly one event and no error;--yes/remote trust/remote add/set-urlrecord it outside the repo; changing url or exec by hand makes it untrusted again (tests/trust45.rs). -
P26 Rejected-op feedback (#45): a push containing an equivocating op and a Lamport-violating op gets both listed in
Committediffrejected-opswas negotiated; a client without the feature gets a plainCommitted; against a server without it the client reportsrejected_reported: false(tests/wire.rs). -
P27 Divergence (#46): two copies sharing a replica id that record different content under one OpId and meet only through a server: the copy's next sync (ssh and
file://) fails withduplicate-replica, persists an equivocation and a DuplicateReplica warning naming the server, and appends no op; with an old peer (nodigest) the session behaves as pre-#46; random same-id copies and sync schedules never diverge silently and honest histories never report divergence (tests/remote.rsdiverged_copy_detected_over_{ssh,file},session::diverge46_tests, mutation-checked). -
P23 Peers migration (D-041): crashing at each step of the
peers→remotesmigration and rerunning yields exactly onefile://remote per former peer and nopeersfile.
Server srv (bare, /srv/hub/proj, replica S); laptop L (replica L) with remote add origin ssh://alice@srv/srv/hub/proj. Server holds ops s1..s3 and published checkpoint v1 at {s3}; L holds {s1} plus new l1 (touch a.png, 5 MiB blob b1) and l2, and a private checkpoint wip.
-
barocss-hub sync origin→ssh -l alice -- srv 'barocss-hub serve --stdio '\''/srv/hub/proj'\'''. Forced command confines to/srv/hub. - Both:
BHUBWIRE,Hello proto 1 1 … replica L|S features blobs-chunked,checkpoints,resume. Protocol 1, features intersected. - Pull: L
Have {l2}. l2 ∉ V_S, so S computesK = downclose({l2} ∩ V_S) = ∅and Δ = {s1,s2,s3} (s1 redundant, §4.3). SOps {s3} done=1 [s1 s2 s3]. L: s3 names no missing blob → emptyNeedBlobs,Done ops. L persists (append s2, s3; log sync) →Committed {l2,s3}. - Push: S
Have {s3}. LOps {l2,s3} done=1 [l1 l2]. SNeedBlobs b1. LBlobBegin b1 5242880 0,Chunk×20 (256 KiB each)… the Wi-Fi drops after chunk 12. ssh exits 255; L reportsioand exits 1.- S: l1/l2 were only in memory; its log is still {s1..s3}. The 3 MiB prefix is in
part-b1. L: {s1,l1,l2,s2,s3} durable since step 3'sCommitted. Nothing acknowledged is lost; both open as valid versions.
- S: l1/l2 were only in memory; its log is still {s1..s3}. The 3 MiB prefix is in
- Rerun: handshake; pull: L
Have {l2,s3}, S answers only what L lacks (nothing new, possibly redundant s1..s3),Committed. Push: SHave {s3}, LOps [l1 l2], SNeedBlobs b1@3145728, L streams chunks 13..20, S verifies prefix + chunks, whole SHA-256 = b1, persists b1 (flush + barrier), appends l1, l2 (log sync),Committed {l2,s3}. - Meta: S
Checkpoints [K … {s3} v1]; LCheckpoints [](wipis private, never sent). Both persist,Committed,Bye. L exits 0. L'sstatusthen reports the working directory behind head (log-only sync, §4.4) untilcheckout.
- Resolved by user decisions: E-R1 → D-041, E-R2 → D-042, E-R4 → D-043, E-R5 → D-044.
- E-R3 HTTP auth mechanism: superseded by Online-First E-B1 (D-048 makes auth required; options and proposal in 6.5.4).
- Allow valid non-control UTF-8 in the forced-command grammar (follow-up; ASCII-only accepted for v1, 5.9.6). HTTP auth (E-R3). Repo identity in
Helloto refuse unrelated repos (5.2.3). Server warnings visible to clients (5.8). Rate limits beyond sshd (5.2.5). Pending-buffer cap (5.9.5, §4.4).part-*sweep age (5.2.4). ACLs (5.9.6). Server hooks (5.9.3). Reducing redundant Δ via ancestor negotiation (§4.3) now that a wire exists.