Síťový protokol
Dokumentace/Nativní architektura a formáty
Síťový protokol42 min čtení

Wire format specification: TCP v2 and secured v4

Zdrojová dokumentace je zatím dostupná v angličtině. Ovládání čtečky odpovídá zvolenému jazyku.

Na této stránce

Normative description of the bytes exchanged between scribe and scribe-server, and of the auth protocol SAU2 that storage servers, gateways, and clients use to reach scribe-auth. Reference implementation: crates/scribe-protocol-tcp (frame.rs, wire/, auth.rs, client.rs, server/) and crates/scribe-auth/src/wire.rs. Logical semantics live in crates/scribe-api; another protocol may encode the same operations differently without changing storage or operation identity. Scribe is unreleased; protocol layouts may change between development revisions. Use matching client and server revisions.

1. Transport

TCP, either plaintext or inside TLS 1.3. One control connection per client; up to four data connections bound to the same session. Exactly one request is in flight per connection; the response carries the request id back. No multiplexing. Default listen tcp://127.0.0.1:7447; non-loopback binding in plaintext mode requires explicit configuration.

An authenticated server (see auth.md) accepts only TLS and only protocol 4: the same frames, codecs, and opcodes as protocol 2, with an extended hello (section 4). It never negotiates plaintext or protocol 2. Plaintext servers speak only protocol 2.

Every TLS endpoint (storage, gateway, auth, and trust discovery) uses TLS 1.3 only and offers and accepts only the hybrid post-quantum key exchange X25519MLKEM768; a peer without it cannot connect. Callers prove their identity with a TLS client certificate: a storage server or gateway accepts only access certificates issued by auth's access CA, and auth accepts only credential certificates issued by its credential CA. TLS 1.3 CertificateVerify binds the proof to the connection, so no bearer secret crosses any Scribe wire. A client without a certificate may still connect anonymously. See identity, device keys, and certificates.

Timeouts: the server waits 10 s for the first frame (handshake) and then applies an inactivity timeout (default 300 s) between frames; there is no total limit on a long transfer. The timeout covers a whole session: a control connection and the data connections bound to it are closed for silence only once none of them has had a request in progress for that long, so a control connection waiting while its data connections transfer stays open. The client applies a 300 s read/write timeout and a 10 s connect timeout. A client that keeps an idle connection for later requests, as a gateway does with its upstream connections, replaces it once it has been silent for half the inactivity timeout the server announced in its handshake reply, before sending anything on it.

2. Frame

text
offset size field
0      4    magic "SCR1"
4      2    opcode                u16 LE
6      2    flags                 u16 LE   bit0 STREAM: object data follows the payload
                                           bit1 ERROR: response payload is an error
8      8    request id            u64 LE   client-assigned; echoed unchanged
16     4    payload length        u32 LE   0 ..= 67,108,864
20     4    reserved              must be 0
24     n    payload

The header is always little-endian, whatever the negotiated codec. A receiver that sees bad magic, unknown flag bits, nonzero reserved bytes, or a length over the maximum closes the connection: its position in the stream is unknown.

3. Codecs and primitives

Payload fields are encoded by the negotiated codec. Codec 1 is little-endian and is the only production codec. Codec 2 encodes the same layouts big-endian; it exists so contract tests prove semantics and operation identity are independent of byte order, and servers accept it only when built for tests (allow_test_codec), never through configuration.

primitiveencoding
u8, boolone byte; bool is 0 or 1, anything else is malformed
u16, u32, u64codec byte order
str16 / bytes16u16 length, then bytes; strings must be UTF-8
str32 / bytes32u32 length, then bytes
opt_str16u8 present, then str16 if 1
digest32 raw bytes
id1616 raw bytes
object idu8 kind (1 chunk, 2 manifest, 3 change set, 4 commit), digest
pathstr16 whose bytes must form a canonical path (else invalid_path)
prefixstr16, empty or a canonical path
file statedigest manifest, digest content, u64 size, u8 mode, u64 change
opt file stateu8 present, then file state
semantic erroru16 code, str16 detail (≤ 4096 bytes)
lock infopath, u64 generation, id16 workspace, id16 branch, str16 author, u64 acquired ms
submit outcomeid16 operation, u8 ok; if ok: u64 change, digest commit id; else semantic error; then u64 file flushes, u64 flush nanos

Every count is validated against its limit and against the remaining payload before allocation. Trailing bytes after a payload are malformed.

4. Handshake

Request opcode 1 hello, payload always little-endian:

text
u16 protocol version (2, or 4 over TLS) | u8 codec (1 or 2) | u32 capabilities (0 = raw object transfers, 1 = LZ4 object transfers) | u64 bind session (0 = new) | str16 client string (≤ 256)

Protocol 4 appends a caller envelope and a binding to the same hello:

text
u8 caller | [bytes16 delegated certificate, only for caller 2] | [32] binding
callermeaningidentity
0anonymousnone; any TLS client certificate is ignored and only public repositories are readable
1directthe verified TLS client certificate, which must be an access certificate
2delegatedthe TLS client certificate is a gateway's access certificate; the user's access certificate follows as DER, 1..16384 bytes

The hello carries no credential. For caller 2 the server verifies the forwarded certificate against auth's access CAs itself and refuses one whose trust domain differs from the gateway's. A storage server's own identity cannot call storage. Other caller tags, an empty or oversized delegated certificate, and trailing bytes are malformed.

The binding is zero for a control connection and, for a data connection, the 256-bit secret the server returned when the control session was established. A data connection must present the same caller mode, the same subject and delegate credentials certified for the same keys, and a live control session. The maximum handshake payload is 16,692 bytes (see request-frame memory admission). Anonymous TLS sessions receive the same random binding protection as authenticated sessions.

The server resolves the caller from the certificate first, then confirms it with auth: the credential must be live, the certificate's key must be the key auth registered for it, and its account must be enabled. For a delegated caller the gateway's service account must also hold a delegate grant for the server's namespace. The decision is cached for at most 30 seconds (the authorization lease), so a revocation takes effect within that time whatever the certificate's lifetime. See gateways and delegation.

Response opcode 1: u16 protocol version | u8 codec | u64 session id | u32 inactivity timeout seconds | u8 compression (0 none, 1 LZ4). A client refuses an inactivity timeout of 0. A protocol-4 control session additionally receives its 32-byte binding secret.

Errors during the handshake are little-endian error frames: unsupported_version for a protocol or codec the server does not accept (there is no fallback to an older protocol; an older client is simply refused), unauthenticated for a missing, invalid, or revoked certificate or credential (for example an access certificate is required, invalid delegated access certificate), busy when the identity already holds its connection limit, auth_unavailable when auth cannot be reached or answers from a revision older than one the server already saw, malformed otherwise, then the connection closes. A data connection passes the control session's id in bind session and receives the same id back.

5. Errors

A response with the ERROR flag carries a semantic error. Codes are the stable values from scribe_core::ErrorCode, also persisted in operation records:

rangecodes
1..99 request1 malformed, 2 unsupported_version, 3 limit_exceeded, 4 unauthorized_network, 5 busy, 6 shutting_down, 7 cancelled, 8 unauthenticated, 9 forbidden, 10 auth_unavailable
100..199 repository100 repository_not_found, 101 repository_exists, 102 branch_not_found, 103 branch_not_main, 104 workspace_not_found, 105 change_not_found, 106 path_not_found, 107 invalid_path, 108 incarnation_mismatch, 109 workspace_retired
200..299 submission200 base_mismatch, 201 already_exists, 202 not_present, 203 locked_by_other, 204 path_collision, 205 object_missing, 206 object_invalid, 207 operation_intent_mismatch, 208 empty_submission, 209 lock_required, 210 invalid_repository_config, 211 merge_incomplete, 212 note_revision_stale
300..399 locks300 lock_held, 301 lock_not_held, 302 lock_generation_stale
500..599 storage500 storage_failure, 501 integrity_failure, 599 internal

Codes 1–4, 8–9, and 100–499 are client outcomes and are not server errors. forbidden is returned for any operation the caller's role does not allow, and repositories the user cannot read are absent from repo_list.

6. Messages

Request payload → response payload. repo, branch, workspace, operation are id16; change fields are u64.

discover reports the logical deployment visible through the connected endpoint. It is available to anonymous and authenticated sessions after the normal hello; TLS trust and all existing connection, frame, and query admission limits apply. The request has no payload or stream. The reply is u8 role | bool authentication | u32 storage count | str16 namespace × count: one entry per configured storage server, in configuration order, naming the auth namespace that server serves. Role and boolean tags are strict; unknown tags, truncated or trailing bytes, and invalid counts are malformed. A direct storage endpoint reports one storage server. A gateway reports its configured 1..64 storage servers, including offline nodes; this is configuration, not a health sample. authentication describes configured native authentication, not whether this caller logged in.

On authenticated endpoints every label is a namespace (1..64 ASCII letters, digits, -, _ or .); on plaintext endpoints every label is empty. The distinct labels are the namespaces reachable through the endpoint, and the repeats say how many servers back each. When a gateway serves several namespaces, repositories are addressed by name as <namespace>/<name> (see repo_create and repo_info) and repo_list returns names in that form.

A direct endpoint cannot enumerate gateways above it or unrelated storage servers. Discovery performs no inventory scan and exposes no endpoints, identities, repositories, paths, or usage. The existing session establishment requirements still apply, including gateway backend availability. Errors are returned, never replaced with inferred topology.

Canonical little-endian reply for an authenticated two-storage gateway whose instances serve studio and publishing: 02 01 02 00 00 00 06 00 "studio" 0a 00 "publishing"; unauthenticated direct storage: 01 00 01 00 00 00 00 00.

Repository names on the wire are at most 193 bytes: a 128-byte storage name, optionally prefixed by a 64-byte namespace and /. Storage names never contain /; the prefix is understood by gateways only. A gateway with one namespace or a configured default resolves bare names in it; otherwise a bare name is malformed and the error names the accepted form.

The table below lists the core opcodes. Opcodes 20–36 are specified in their own sections: 20–25 in section 9 (Branch and merge opcodes), 26–28 in Gateway inventory and authorization preflights, 29 in Repository lock policy, 30 in Public repository reads, 31 in Versioned repository mounts, 32–33 in Selective integration, and 34–36 in Workspace lifecycle.

opnamerequestresponse
37discover(empty)u8 role (1 storage, 2 gateway), bool authentication configured, u32 configured storage servers, str16 namespace × servers (one per server; empty on plaintext)
2server_info(empty)u16 protocol, str16 server version, u64 session id, u32 max batch objects, u64 max batch bytes, u32 max page entries, u64 max submit entries
3repo_createstr16 name (≤193; <namespace>/<name> through a gateway), u8 storage compression (0 none, 1 LZ4; permanent)repo info
4repo_listu8 has_cursor, optional [16] opaque cursoru8 has_next, optional [16] cursor, u32 count (≤128), repo info × count
5repo_infou8 1 + id16, or u8 2 + str16 name (≤193)repo info
6workspace_registerrepo, workspace, branch, u16 view count, prefix × count, str16 label, u16 tag count (≤ 16), str16 tag (≤ 64 bytes) × count(empty)
42pin_objectsworkspace id, intent id, repo, u32 count (≤ 8192), object id × countu32 count, bool × count
43tree_proofbytes32 tree_page request, selector, u32 path count, str16 pathscontent root[32], revision root[32], u32 node count, bytes32 portable revision node × count
7presencerepo, u32 count (≤ 8192), object id × countu32 count, bool × count
8put_objectsrepo, u32 count (≤ 1024), (u8 kind, digest, u64 length) × count; STREAM flag set; then the concatenated payloads in order (total ≤ 256 MiB)u32 count, per object: u8 1 stored / 2 already present / 3 rejected + semantic error; then u64 file flushes, u64 dir flushes, u64 flush nanos
9submitrepo, id16 expected incarnation, branch, workspace, operation, digest change set, str16 author (≤ 256), str32 message (≤ 65536), bool keep lockssubmit outcome
10operation_outcomerepo, operationu8 present, then submit outcome
11headrepo, branchu64 head, id16 incarnation
12tree_pagerepo, branch, u64 change, prefix, opt_str16 after, u32 limitu32 count, (path, file state) × count, bool more, content root[32], revision root[32]
13changed_pathsrepo, branch, u64 from, u64 to (target), prefix, u8 token present (+ bytes32 token), u32 limitu32 count, (u64 change, path, opt file state as of to, bytes32 token) × count, bool more
14get_objectsrepo, u32 count (≤ 1024), object id × countSTREAM flag set; payload u32 count; then per requested id in order: u8 present, u64 original length (zero if absent), followed by object data when present
15logrepo, branch, bool follow merges, bool all branches, bool snapshot present (+ u64), opt_str16 path, u8 before present (+ u64), opt_str16 after path, u32 limitu32 count, per entry: bool ancestry present (+ bool in ancestry), bool history snapshot present (+ u64), u64 change, id16 branch, str16 branch name (≤ 128 bytes), digest commit id, u64 parent, bool merge parent present (+ id16 source branch, u64 source change), bool cherry-pick present (+ id16 source branch, u64 source change), str16 author, str32 message, u64 timestamp ms, u32 entry count, id16 workspace, id16 operation, u8 action present (+ u8 action), opt file state, opt_str16 historical path, bool move present (+ str16 source path, u64 source change)
16lockrepo, path, workspace, branch, str16 authorlock info
17unlockrepo, path, workspace, u64 generation, bool force(empty)
18locksrepo, prefix, opt_str16 after, u32 limitu32 count, lock info × count, bool more
19ping(empty)(empty)
38repo_usagerepou64 apparent bytes
39describe_changerepo, u64 change, u32 expected revision, str16 author (≤ 256), str32 text (≤ 65536)change note
40change_notesrepo, u32 count (≤ 4096), u64 change × countu32 count, (bool present, change note if present) × count, positional
41change_note_historyrepo, u64 changeu32 count (≤ 1024), change note × count, oldest first

Repo info = id16 id, str16 name, id16 main branch, u64 head, id16 incarnation, u64 created ms, u8 storage compression. Change note = u64 change, u32 revision, str16 author (≤ 256), u64 edited ms, str32 text (≤ 65536). Page limits are clamped to 1..=4096 entries. after and token are opaque continuation values taken from the previous page's last entry.

Log follow merges requires an exact path; otherwise the server returns malformed. With it set, path history follows both branch origins and merge sources up to their recorded revisions. Without it, only native and inherited first-parent path history is returned. Both modes follow explicit move sources, sort descending by change then ascending by historical path, and deduplicate by (change, path). Without after path, before is exclusive. With it, the same change's lexicographically later paths are included as well as older changes. after path requires both a path query and before, otherwise malformed. Pass the last row's change and historical path together to continue without skipping tied rows. Pagination filters results, not newer merge or move edges needed to discover older revisions. Branch names are current display names; branch IDs identify where revisions were accepted. Merge parents describe ancestry, not per-file resolution choices. History exceeding its 100,000 metadata-visit or 32 MiB lineage-path budget returns limit_exceeded.

All-branch log requests require a path and follow merges = true. An optional snapshot ceiling is allowed only in this mode; it must be within the selected branch's lifetime and no later than the repository head. Otherwise the server returns malformed. Without a ceiling, the server pins the repository head in the query's single metadata read transaction. Every returned row carries that ceiling and a commit-ancestry membership boolean; ordinary logs carry neither. The same (before, after path) cursor applies; clients repeat the snapshot ceiling to keep file selection and membership stable across pages. Branch names remain current display metadata. Empty results need no continuation.

This mode selects the latest stored revision (including deletion) at the query path and snapshot, then traverses the file_lineage adjacency index in both directions. Ordinary adds start new incarnations; names and content hashes are not identity links. Fork snapshots retain original revisions. Per-file merge entries connect their predecessors, while ancestry-only merges affect membership without inventing file identity links. Traversal excludes nodes beyond the snapshot, independently of the pagination cursor, and fails explicitly on its read/path budget rather than returning an incomplete page.

7. Streaming rules

The client selects transfer compression in Hello; the server echoes the accepted choice in HelloOk. Unknown capability bits are rejected and the client rejects a different echoed choice. Control frames and other metadata are not compressed. Bound data connections negotiate their own transfer policy (the client carries its existing preference forward).

With compression 0, object data is the original byte stream. With compression 1, each object is a sequence of bounded blocks with the same layout specified in the object-envelope section of on-disk-format.md, without the stored object's u64 original length prefix. Announced object lengths and admission budgets always count original bytes. Block headers are little endian even with the big-endian test metadata codec. Decoder allocation is bounded to 256 KiB per block; malformed blocks close the connection. Empty objects have no blocks. Receivers drain unfinished objects before reading the next item.

Transfer compression is independent of repository storage. Stored compressed objects are decoded before being passed to the adapter; the adapter recompresses them when transfer compression is enabled. Raw storage and raw transfers retain the direct file-to-socket path.

Per-operation rules:

  • put_objects: the sender announces every length up front and then writes object data that decodes to exactly those lengths. The server hashes the original bytes while receiving, validates manifests (all chunks present, in the store or earlier in the same batch) and change sets (all manifests present), publishes the batch durably, and only then answers. If a request-level failure occurs before all announced bytes were consumed, the server sends the error and closes the connection; the client reconnects and retries (uploads are idempotent).
  • get_objects: the response header commits to a count; each item is self-describing so absent objects are reported inline. A receiver that stops consuming an item must drain the remaining bytes to stay in sync.
  • Resume is at chunk granularity: an uploading client asks pin_objects again and re-sends only absent chunks.
  • Byte accounting: the harness reports "protocol bytes" as header + payload + stream bytes as seen by the client (TcpRemote::bytes_sent/received).

8. Semantics every adapter must preserve

  • Every request naming a branch is checked against the repository's branch table before any mutation; branch_not_found names an unknown id.
  • A mutating response is sent only after the durable commit. A lost response leaves the outcome unknown; operation_outcome resolves it.
  • The operation fingerprint is computed from the semantic request, so a submit replayed through another adapter or codec resolves to the same accepted change; a replay with different intent is operation_intent_mismatch. Attribution is not intent: the author is excluded, because an authenticated server replaces it with the principal's display name before publication.
  • Submit (including merge submit) carries the expected repository incarnation, included in its semantic fingerprint immediately after the repository ID. A new operation from an older incarnation is refused with incarnation_mismatch, even if its numeric expected bases match reused change numbers after restore. An existing operation still resolves to its original result through operation_outcome.
  • Locks, budgets, and publication gates are shared server state, identical across concurrently connected adapters.

9. Branch and merge opcodes

Protocol 2 includes the branch and merge operations below. Protocol 1 is unsupported: its hello receives unsupported_version, with no client fallback.

OpcodeOperation
20branch list
21branch create/rename with persisted operation id
22branch operation outcome
23pinned merge parents and merge-base discovery
24submit with expected target head, pinned source parent, and pinned merge base

Opcode 21 is [16] repository ‖ [16] incarnation ‖ [16] operation ‖ [16] branch ‖ str16 name ‖ u8 action. Create (1) appends [16] source branch ‖ u64 source change; rename (2) appends str16 expected name. The incarnation is included in the semantic fingerprint after the repository ID. A new intent from an old incarnation receives incarnation_mismatch before any branch reservation or outcome is written. Exact recorded outcomes still replay; an exact snapshot intent retained in the backup can finish because its pinned source ancestry is also retained. Changed intent with a reused operation ID is always refused, including a changed incarnation.

Opcode 24 is bytes32 submit request ‖ u64 target head ‖ [16] source branch ‖ u64 source head ‖ [16] base branch ‖ u64 base. The base is the merge base the client planned against; it is part of the semantic fingerprint. Before the publication gate the server recomputes the merge base from its own history, refuses a base that disagrees with it (base_mismatch), and verifies that the change set covers every path the source changed since that base, returning merge_incomplete otherwise (see docs/branches.md). Neither rejection is recorded as an operation outcome. cherry_pick_submit carries no base: the picked commit's parent is derived server-side.

The canonical codecs are in scribe-protocol-tcp/src/branches.rs. Names, counts, input lengths, and trailing bytes are checked. A merge submission uses the existing accepted/rejected outcome encoding and operation_outcome for retries. Data connections negotiate the same version as their bound session.

Opcode 13 (changed_paths) returns each selected path at most once, ordered by exact canonical path. Its change is the last native event in (min(from, to), max(from, to)], and its state is pinned at to (absent when the target has no file). Reverse ranges are supported; the event change can exceed to. Fork-inherited events are excluded. Continuation tokens are opaque; repeat the same branch, interval, and prefix on subsequent pages. The field encodings remain unchanged.

Opcode 25 (MergeRead) is read-only and requires repository schema 3. Its request begins with RepositoryId and a u8 query tag:

  • 1: u64 left revision, u64 right revision, optional str16 path cursor. A page traverses the larger first-parent revision until the chains meet, at most 128 commits or 1,024 changed paths. Reply: tag, updated left/right revisions, optional cursor, u32 path count, str16 paths. Equal revisions signal completion.

  • 2: BranchId, u64 revision, u32 count, str16 canonical paths (maximum 1,024). Reply: tag, u32 count, optional FileStateAt for each input in the same order.

  • 3: BranchId, u64 revision, str16 canonical path, optional str16 folded cursor. Reply: tag, optional next cursor, u32 count, (str16 path, FileStateAt) entries. The first page includes exact/ancestor matches. Each page examines at most 1,024 historical descendant paths, including deleted paths. An empty page can still carry a continuation cursor. Replies are bounded to 5,120 entries, allowing the ancestors of a maximum-length canonical path.

  • 4: BranchId, u64 revision, base BranchId, u64 base revision, u32 count, str16 canonical paths (maximum 1,024). Reply: tag, u32 count, one origin per input: u8 0 (no recorded base file), u8 1 + str16 base path (unique origin), or u8 2 (multiple base-file origins; automatic matching is unsafe). Both snapshots must lie within their branches' lifetimes. The entire batch shares the 100,000-visit / 32 MiB lineage-path budget and fails rather than truncating. The query follows selected-side predecessors. Forward edges are allowed only within the pinned base's commit ancestry, to recognize bases reached by merges that kept different bytes or names. Other branches and later descendants do not establish a merge origin. Results use recorded revision relationships, never content equality or filename similarity.

  • 5: BranchId, u64 from revision, u64 to revision, u32 count, str16 canonical paths (maximum 1,024). Reply: tag and boolean. True means the earlier revision is in the later target ancestry and none of these paths, their folded aliases, ancestors or descendants changed between the revisions. Tombstones count as changes. This uses snapshot change summaries, without scanning unrelated commits. Both revisions must be within the branch lifetime. Publication still checks the dependencies again under the repository acceptance rules.

Optional strings are a boolean presence byte followed by str16 when present. Numeric fields follow the negotiated codec; the deployed codec is little endian. Malformed, oversized and trailing payloads are rejected. All reads are repository-scoped and enforce branch/revision bounds.

10. Upload admission under protocol 4

On an authenticated connection put_objects is answered with an admission response before the client sends the streamed payload, so a reader cannot force the server to consume an upload it will reject. Object payloads pass through TLS; the zero-copy sendfile path is used only on plaintext connections.

Gateway inventory and authorization preflights (opcodes 26–28)

These additive operations use the existing TCP v2/plaintext and v4/TLS handshake, framing and negotiated codec. Existing operation payloads and canonical object bytes are unchanged. A storage server remains usable without a gateway.

InstanceStatus (26) request: u8 has_after (0 or 1), followed by an exact 16-byte repository ID when present. Response, in order:

  1. Exact 16-byte data-root incarnation.
  2. u64 total_repositories, u64 used_bytes (apparent whole-root bytes, populated on the first page only).
  3. u8 has_available_bytes (0 or 1), optional u64 available_bytes.
  4. u32 repository_count, at most 128; each entry is the existing RepoInfo encoding followed by u64 apparent_repository_bytes.
  5. u8 has_next (0 or 1), optional exact 16-byte exclusive repository-ID cursor.

Entries are ordered by repository ID. The gateway retries inventory if the identity or total count changes between pages. On authenticated storage the operation requires a namespace reader or namespace administrator grant for the server's namespace; a gateway polls it with its own access certificate as a direct caller. It is read-only and performs metadata work in the existing bounded blocking storage pool. Gateways use it for placement and discovery, not to acknowledge repository mutations.

AuthorizeUpload (27) request: exact 16-byte repository ID. Successful response: empty payload. Storage checks the same writer authorization as upload admission. A gateway uses this before granting its own v4 upload admission. The storage server repeats admission for the actual upload; this preflight grants no lease.

AuthorizeRepoCreate (28) request: u8 has_namespace, then str16 namespace when set. Successful response: empty payload. No namespace means the endpoint's default; an explicit namespace must be one the endpoint serves (storage refuses any other with malformed, a gateway refuses unknown labels the same way and uses the label to pick the placement pool). Authenticated storage requires namespace-administrator authorization; plaintext storage permits it and accepts no namespace. A gateway checks this at the chosen pool before persisting a new placement. Creation itself repeats authorization at storage.

The inventory byte fixture is in wire/tests.rs. Gateways forward all existing repository operations and stream object bodies; failed upstream transports close the client connection so unknown mutation outcomes are not presented as definitive failures.

Repository lock policy (opcode 29)

LockPolicy is read-only and requires Reader permission. Request: exact id16 repository ID. Response: u64 config change, str32 UTF-8 TOML configuration text (at most 65,536 bytes). The response uses the negotiated numeric codec. The change is the last accepted revision of .scribe/config.toml on main; when absent, change is zero and text is exactly version = 1\n. Existing configs must be valid TOML; there is no empty-text fallback. The response contains the complete config, including profiles, although only its locks section governs locking. Profile-only edits also advance this config-file revision. No workspace branch, local policy text, or client-supplied matching result can change this authority. Gate/admission routing treats this as a control read; object reads and rule compilation run on admitted blocking workers.

An edit fetches this snapshot and uses existing Lock requests for matching files. The server independently evaluates the current policy for every submit, including merge submissions, and validates ownership in the acceptance transaction. Missing mandatory ownership returns a durably recorded lock_required outcome; invalid submitted configuration returns invalid_repository_config. Changing the decoded ordered locks.rules on main requires Maintainer permission, including through merges, section removal, or file moves/deletion. Profile-only and formatting changes that preserve those values require Writer permission. The shared schema is in repository configuration; the updated UTF-8 snapshot golden fixture is in scribe-protocol-tcp/src/wire/tests.rs.

Request-frame memory admission

Every request opcode has an exact payload bound, checked against the frame header before its body is allocated or received. The bound is the largest encoding the opcode's decoder accepts, derived from its field layout and the limits in scribe_core::limits and scribe_api::limits; the reference table lives in server/admission.rs and is verified against maximal encodings in both codecs. A header announcing more than the bound closes the connection without reading the body. The general 64 MiB frame ceiling is only a header sanity bound; no request opcode admits a payload that large.

opcodebound (bytes)derivation
hello (first frame only)16,692hello fields (273) + u8 caller + bytes16 delegated access certificate (16,384) + 32-byte binding
ping, server_info, discover0empty
authorize_repo_create67u8 + str16 namespace (64)
head, operation_outcome, BranchOutcome, workspace_remove_outcome32two id16
branch_list, authorize_upload, lock_policy, repo_usage16id16
change_note_history24id16 + u64
repo_list, instance_status17u8 + optional id16
repo_create196str16 name (193) + u8 compression
repo_info196u8 kind + str16 name (193)
workspace_list38id16 + two u8 + optional id16 + u32
repository_mounts40two id16 + u64
cherry_pick_info56three id16 + u64
workspace_remove64four id16
MergeInfo66three id16 + two optional u64
BranchMutate581four id16 + str16 name (256) + u8 + str16 expected name (256)
lock8,500id16 + path (2 + 8,192) + two id16 + str16 author (256)
unlock8,235id16 + path + id16 + u64 + u8
tree_page16,433two id16 + u64 + prefix + optional after + u32
tree_proof1,048,576bounded aggregate tree request, selector and optional point paths
locks16,409id16 + prefix + optional after + u32
log16,446two id16 + three flags + optional u64 + optional path + optional u64 + optional path + u32
changed_paths16,507two id16 + two u64 + prefix + optional bytes32 token (8,256) + u32
change_notes32,788id16 + u32 + 4,096 × u64
get_objects33,812id16 + u32 + 1,024 × (u8 + digest)
put_objects42,004id16 + u32 + 1,024 × (u8 + digest + u64); bodies are streamed, never framed
describe_change65,826id16 + u64 + u32 + str16 author (256) + str32 text (65,536)
submit65,910five id16 + digest + str16 author (256) + str32 message (65,536)
MergeSubmit65,970bytes32 submit + u64 + id16 + u64 + id16 + u64
cherry_pick_submit65,946bytes32 submit + u64 + id16 + u64
get_chunks65,556id16 + u32 + 1,024 × two digests
pin_objects270,388workspace id16 + intent id16 + repo id16 + u32 + 8,192 × (u8 + digest)
presence270,356id16 + u32 + 8,192 × (u8 + digest)
workspace_register2,099,030three id16 + u16 + 256 × path + str16 label (256) + u16 + 16 × str16 tag (64)
MergeRead8,390,725id16 + u8 + origins query: two id16 + two u64 + u32 + 1,024 × path

All incoming request bodies share a 128 MiB budget per listener, including handshakes, incomplete bodies, queued requests, and requests executing on workers; anonymous TLS connections together hold at most 64 MiB of it. The byte reservation is acquired before allocation and released when the request finishes or its read is cancelled. One request is in flight per connection, so a connection holds at most one payload, and that payload is capped at an eighth of the shared budget (16 MiB; a quarter of the anonymous budget). Every opcode bound is below this cap, so no legal request is refused by it, and at least eight connections (four anonymous) must stall bodies simultaneously to exhaust the budget for everyone else. All connections of one principal share a quarter of the listener budget (32 MiB), so at least four accounts must stall bodies at once to starve everyone else. The principal is the user named by the access certificate; for a gateway's delegated traffic it is the user, not the gateway.

If a reservation cannot be acquired immediately, the server answers the frame with a busy error carrying its request id, then closes the connection because the body was never read. The client may reconnect and retry. This budget is separate from object-transfer, storage-worker, query-worker, and handshake admission; no started durable commit is cancelled. These rules apply to both plaintext and TLS connections.

Handshake admission

Handshake authentication may make a network round trip to the auth service or, on a gateway, to a backend. It runs on a dedicated bounded handshake pool (handshake_workers, default 8), never on the query or storage pools, so a peer that presents a syntactically valid certificate cannot occupy request workers before it is authenticated. Anonymous TLS handshakes additionally take an anonymous-request permit before authentication work begins.

Cheaper limits are decided before that work, from the TCP source and the certificate alone:

  • A connection is pending until it has completed its handshake and delivered one complete request frame. One source address (an IPv6 /64 counts as one source) may hold at most max_pending_per_source pending connections (default 16). When the connection table is full, the oldest pending connection is closed to admit a newcomer, so unauthenticated peers can delay a legitimate client by at most one handshake deadline. A connection is refused at accept when its source already holds its share, or when every slot carries a request in progress.
  • One principal may hold at most max_connections_per_principal connections (default 64); the next hello is answered busy (too many concurrent connections for this identity) before any auth round trip.

Auth, storage, and gateway listeners share the same pending-connection bookkeeping (scribe_core::admission).

Public repository reads

Opcode 30 get_chunks requests RepositoryId | u32 count (≤1024) | ([32] ChunkId | [32] ManifestId) × count. The streamed response is identical to get_objects, in request order. The secured adapter requires accepted manifest membership and a matching chunk reference; an invalid witness returns an absent object. Public-only get_objects and presence expose accepted manifests, never arbitrary staged chunks or changesets. Explicit repository grants retain existing raw-object access.

repo_list uses bounded candidate pages. A secured cursor is opaque and scoped to the connection; finish pagination without interleaving other operations. Empty pages can still carry continuations. Private and absent repository names produce the same access error. The gateway applies these rules through backend authorization.

Auth protocol SAU2

Storage servers, gateways, clients, and the administration commands reach scribe-auth over SAU2, a separate TLS 1.3 protocol (same key-exchange rule as section 1). It does not change storage TCP framing or canonical objects. Reference: crates/scribe-auth/src/wire.rs. All integers are little-endian.

text
frame   = "SAU2" | u32 payload length (≤ 1 MiB) | payload
request = [16] operation | u16 argc | argc × str16 | u8 has_secret | [str16 secret]
reply   = u16 0 | [16] incarnation | u64 revision | u16 rows | rows × (u16 n | n × str16)
        | u16 code | str16 message (≤ 2048 bytes)

The first request string is the verb; the rest are its arguments. An argument vector or row holds at most 130 strings of at most 4,096 bytes each (certificates travel as PEM text), and a reply holds at most 256 rows. Only login, login-outcome, admin-login and admin-login-outcome (the password, 1..1024 bytes) and users-password (the new password, 12..1024 bytes) carry a secret; any other verb with a secret, or one of these without it, is malformed. A secret is never part of an intent digest, an outcome, or an audit row. Error codes are the storage codes of section 5.

Every successful reply names the auth incarnation (random 128-bit, new for each init, migrate, and restore) and revision (u64, advanced by every applied administrative change and every logout). A storage server refuses an answer from a lower revision of the incarnation it last saw; see rollback protection.

The caller is identified only by the TLS client certificate, which must be a credential certificate from auth's credential CA; no credential travels in a request. A connection without a certificate may call only trust-bundle, login, login-outcome, admin-login, and admin-login-outcome. A connection carries any number of sequential requests. The first must arrive within 3 seconds of accepting the connection (one deadline for the TLS handshake and the first frame); afterwards the service closes a connection idle for 60 seconds. An idle connection counts as pending again until its next complete request, so it can be evicted when the service is full (see handshake admission). Clients pool up to four connections, reuse one for at most 50 seconds, and retry a request once on a fresh connection when the service closed a pooled one.

verbcallerarguments after the verb → rows
trust-bundleanyonenone → [trust domain], then one [credential or access, CA certificate PEM] row per CA
login, login-outcomeanonymous, with the passwordusername, credential id, public key (SPKI hex), label → [ok, expires ms, credential certificate PEM]; login-outcome of an unknown operation → no rows
admin-login, admin-login-outcomeanonymous, with the password of an enabled person auth administratoras login; registers an administrator credential (kind user, bound to the password generation) that expires after the auth-configured 1–24 hours; a person who is no administrator → forbidden after the password check; admin-login-outcome of an unknown operation → no rows
access-certificateany account credential (not a storage server)none → [access certificate PEM, lifetime ms] for the caller's own key; issuing writes nothing
identitya session or automation credentialnone → [user id, username]
logout, session-outcomethe session or automation credential being revokednone → [ok]; an already revoked, expired, or pruned credential answers ok without writing; session-outcome of a live credential → no rows
automation-create, automation-outcomea password sessioncredential id, public key, label, expires ms, role → [ok, expires ms, credential certificate PEM]
automation-derive, derive-outcomean automation credential that is not itself derivedcredential id, public key, label → [ok, expires ms, credential certificate PEM]
authorizea storage server credentialcredential, key id, repository, actor credential, actor key id → [principal id, name, role, namespace admin, lease ms, public read, namespace reader]
authorize-batcha storage server credentialcredential, key id, actor credential, actor key id, repository… (1..125 repositories) → [can read, lease ms] per repository
public-policya storage server credentialrepository → [public or private, visibility revision, lease ms]
administrative verbsan administrator credentiallisted below

The client generates the device key and the random credential id; a request carries only the public key as hexadecimal SubjectPublicKeyInfo DER, and auth records its key id (the BLAKE3 digest of that DER). A public key is registered at most once. login, admin-login, automation-create, and automation-derive need a nonzero persisted operation id; their intent is the BLAKE3 digest of the canonical encoding of a zero operation id and the arguments after the verb (for admin-login, the string admin-login and then those arguments, so an id used for one sign-in is refused for the other), so the matching -outcome verb replays the recorded certificate for the same intent and a reused operation id with a different intent is operation_intent_mismatch. Replay never extends an expiry.

  • automation-create roles reader, writer, maintainer, and namespace-admin create credential kinds automation, automation-write, automation-maintain, and automation-admin. The expiry must be within one year. Only a password session may create one, over its own connection.
  • automation-derive issues a child for another key (a runner's) with the parent's kind, account, role ceiling, and expiry; revoking or expiring the parent revokes the child. A derived credential cannot derive another.
  • An access certificate lives one hour and never beyond its credential's expiry. Clients renew it at half its lifetime measured by local elapsed time.

authorize, authorize-batch, and public-policy refuse repositories outside the calling server's namespace. The caller columns identify the subject of an access certificate the server verified: its credential id and key id, and for a delegated connection the gateway's credential id and key id (empty when the caller connected directly). Auth checks that each credential is live, that the key id is the registered key, and that its account is enabled; a delegate must be a live service-reader or service-writer credential whose account holds a delegate grant for the server's namespace, otherwise the answer is delegate may not act for users in this namespace. In authorize-batch all four caller columns empty ask for an anonymous caller. In authorize an empty repository asks only for the identity. The role is empty when the caller has none; it is the lesser of the credential kind's ceiling and the account's current grants, and only user, session, and automation-admin credentials keep namespace administration. The lease is at most 30 seconds and never beyond the caller's, the delegate's, or the server credential's expiry.

Administrative verbs need a credential of kind user, held by an enabled person who is an auth administrator:

text
users-add name id administrator kind       users-password name generation (+ secret)
users-disable name                          users-enable name
users-list cursor                           servers-add name namespace id
servers-disable name                        servers-enable name
servers-list cursor                         credentials-add kind subject credential public-key expires-ms label
credentials-certificate credential          credentials-revoke credential
credentials-list cursor [username]          grants-set | grants-revoke | grants-list
namespace-admins-set | -revoke | -list      namespace-readers-set | -revoke | -list
delegates-set user namespace                delegates-revoke user namespace
delegates-list cursor [namespace]           visibility-get | visibility-set
audit-list cursor                           operation-outcome id

credentials-add kinds are user, server, service-reader, and service-writer; it and credentials-certificate answer [credential certificate PEM], so a lost answer is recovered by asking again. A mutation needs a nonzero persisted operation id; the same id with another actor or intent is operation_intent_mismatch, and another administrator's operation id answers like an unknown one. users-password must name a new password generation. Every administrative request, applied or rejected, is audited. See credentials and roles.

Versioned repository mounts (opcode 31)

RepositoryMounts is a read-only operation available through the same native protocol and gateway. Request: exact id16 repository, id16 branch, u64 change (40 bytes). Response: u32 count (at most 64), then each declaration in mount-name order as str16 name (64 bytes maximum), str16 path (canonical path bound), str16 repository (32 lowercase hex digits), str16 branch (32 lowercase hex digits), str16 revision (at most 20 digits, canonical u64 decimal). Counts and lengths use the negotiated codec; production remains little-endian. Duplicate names, malformed identities/pins, excess counts, truncation and trailing bytes are rejected. Golden bytes and both numeric codecs are tested.

Only the parent repository's Reader permission is checked. The operation reads its accepted config at the exact selected branch revision and never contacts children. Missing configuration returns an empty declaration list. Target resolution uses separate normal repository/branch/tree reads under the caller’s credential; it grants no inherited access. No structured-object or commit format changes are introduced by mount declarations, which remain ordinary TOML bytes.

Selective integration (opcodes 32 and 33)

cherry_pick_info (32) takes id16 repo, id16 target, id16 source, u64 change (56 bytes). It returns two branch-info records (target with live pinned head, source with selected change), id16 parent_branch, u64 parent, bool already_applied. Only ordinary source commits on the selected branch's first-parent history qualify. Reader permission is required. Provenance is not merge ancestry.

cherry_pick_submit (33) takes bytes32 ordinary_submit_request, u64 target_head, id16 source, u64 source_change. It returns the ordinary SubmitOutcome. Writer, workspace ownership, lock policy and configuration authorization match submit. The operation fingerprint is BLAKE3 of "scribe:cherry-pick-operation:v1\0", the 32-byte ordinary submit fingerprint, LE target head, source branch bytes and LE source change. Changing any of those fields cannot reuse an operation ID. Target advancement rejects only when integration path dependencies changed; unrelated commits preserve the planned result. Competing duplicate picks still produce recorded rejections.

The log result carries optional cherry-pick provenance immediately after its optional merge parent. There is no wire fallback for a log layout without it, so native binaries and the SDK/API are deployed together. Object formats remain explicitly versioned to retain accepted history.

Repository usage (opcode 38)

RepoUsage (38) request: exact 16-byte repository ID. Response: u64 apparent bytes — the sum of file sizes under the repository's storage directory, sampled from metadata without reading object contents or following links, the same measure InstanceStatus reports per entry. Unlike InstanceStatus it is a single-repository read for anyone who can read that repository (Reader on authenticated storage, public-read for anonymous callers, everyone on plaintext storage); an unknown ID is repository_not_found. It runs in the bounded blocking storage pool and is measured on request, so a gateway forwards it to the owning node and never answers from its own inventory sample. discover still exposes no usage: it is public and describes configuration, not repositories. The byte fixture is in wire/tests.rs.

Change descriptions (opcodes 39–41)

A commit's message is part of its identity and never changes. A change note overlays it: the server keeps an append-only journal of descriptions per accepted change, clients display the latest revision in place of the message, and every earlier revision stays readable. log (15) is unchanged and keeps returning the submitted message, so a client that predates these opcodes sees exactly what it always did; a client that knows them fetches the overlay for the page it displays with one change_notes request.

describe_change (39) appends a revision. expected revision is the revision the caller last displayed, zero for a change that has none; when the journal is already past it the server answers note_revision_stale (212) and records nothing, which is also what makes a retry after a lost reply safe — a duplicate revision is never written. An empty or blank text is malformed; a change that does not exist is change_not_found; the per-change journal is bounded by scribe_core::limits::MAX_CHANGE_NOTE_REVISIONS (1,024), after which the server answers limit_exceeded. On authenticated storage the author field is ignored and the revision is attributed to the principal; a Writer may describe changes whose commit author is that principal, anyone else's need a Maintainer, and the response is the recorded note. The request runs in the storage pool; its write transaction is one bounded read of the change's last revision and one insert.

change_notes (40) returns the current note of each requested change in request order, present = 0 where a change was never described, from one read snapshot. change_note_history (41) returns one change's revisions oldest first. Both need only a Reader and run in the query pool. Byte fixtures are in wire/tests.rs.

Workspace lifecycle (opcodes 34–36)

All production integers use the negotiated little-endian codec. These operations use the ordinary bounded blocking admission and work through gateway forwarding. Storage enforces authorization through AuthProvider.

OpcodeRequestResponse
34 workspace_list[16] repo, u8 everyone, u8 has-after, [16] after?, u32 limitu32 count, registration rows, u8 more
35 workspace_remove[16] repo, [16] incarnation, [16] workspace, [16] operationremoval outcome
36 workspace_remove_outcome[16] repo, [16] operationempty when unknown, otherwise removal outcome

A registration row is [16] workspace, [16] branch, u8 has-owner, [16] owner?, str16 label, u16 tag count, str16 tag × count, u64 created-ms, u8 has-locks. At most 128 rows are returned; label length is at most 256 UTF-8 bytes; at most 16 tags of at most 64 bytes each. A tag is name or name=value: the name is lowercase ASCII letters, digits, -, _ or . starting with a letter or digit, the value printable ASCII without spaces, names unique within one registration (scribe_core::workspace_tags). Tags are fixed by the first registration of a workspace; a re-registration naming different ones is refused with malformed. The cursor is an exclusive workspace ID within the requested repository and owner selection. Authenticated own listings seek only that principal's index; everyone requires Maintainer. Anonymous clients cannot list or remove registrations. Unauthenticated storage treats all callers as trusted and lists all registrations.

A removal outcome is [16] operation, u16 error-code, followed by u64 retired-ms when the code is zero or str16 detail (at most 4096 bytes) otherwise. Removal fingerprints hash scribe:workspace-remove:v1\0, repository ID, incarnation, and workspace ID in that order. Operation IDs share the mutation namespace with submits and branch operations. Authenticated outcome reads are restricted to the principal that issued the removal. The new semantic code 109, workspace_retired, refuses reuse of a retired identity. Request payload bounds are 38, 64, and 32 bytes respectively. The server records successful and rejected removals durably before replying; held locks produce a recorded lock_held rejection.

Durable upload pins (opcode 42)

pin_objects writes workspace-and-intent-scoped pins durably before returning object presence bits. Both present and absent identities are pinned. Uploading clients call it before put_objects and before reusing existing chunks/manifests/change sets. Repeating it is idempotent. Raw put_objects alone creates unleased, unaccepted objects eligible for explicit offline collection; it does not extend an upload lease implicitly. Accepted history is always retained independently.

The shared client combines chunk, manifest, and change-set identities into mixed-kind requests of at most 8,192 identities. All pin requests finish before any object upload begins; missing objects are then sent in dependency order (chunks, manifests, change sets). A small submit uses one pin request. Presence responses must contain exactly one bit per requested identity.

Pins remain active until that submission intent has a final outcome, workspace retirement, or explicit offline operator abandonment. The client persists the intent ID before sending it. A finalized ID or an ID belonging to another workspace is refused. Finalizing one intent does not release another’s pins. There is no clock expiry or renewal interval. Retired workspaces are refused. Authenticated adapters require Writer permission and ownership of the workspace; no-auth adapters use the same durable metadata without an auth dependency. The request uses storage admission, not reserved read/query capacity. presence remains read-only. The shared client and all adapters must be upgraded together; there is no fallback that silently uploads without a pin.

Portable tree pages and proofs (opcode 43)

tree_page responses append the full tree’s content root[32] and revision root[32] after the existing more byte. Roots describe the requested historical snapshot, regardless of prefix or pagination. They are bound into accepted v5 commit envelopes; roots returned by the server alone are not a trust anchor.

tree_proof takes bytes32 of the tree-page request, a selector u8, then u32 path count and that many str16 canonical paths, followed by a continuation bool. When true it carries bytes32 edge prefix (1–8,192 bytes) and u32 remaining selected values (1 through original limit + 1). The prefix is raw radix bytes and may end inside UTF-8; it is not a canonical path string. Selectors are 0 (live range), 1 (changed range, followed by lower change u64), 2 (exact path), and 3 (explicit paths). A changed range proves all revisions newer than the lower change at the requested snapshot, including tombstones. Exact requires a nonempty prefix, limit 1 and no cursor. Explicit paths require sorted unique paths, an empty prefix, no cursor and a limit equal to their count. Other selectors carry zero explicit paths. Requests are capped at 1 MiB and 4,096 paths. Unknown selectors, truncation, trailing bytes and oversized counts are refused in both codecs.

The response is content root[32], revision root[32], u32 node count, followed by bytes32 canonical portable revision nodes in depth-first query visitation order. Each response contains at most 8,192 nodes and 8 MiB of canonical node bytes. Payload framing adds 68 bytes plus 4 bytes per node. The query worker pool is shared with tree pages. Node-internal integers are always little endian.

A logical proof spans as many bounded fragments as needed. The initial fragment starts at the root. The client maintains its authenticated depth-first frontier; when the response ends, it derives the next edge prefix and remaining result count (including one lookahead value) itself. The next request retains the exact repository, branch, revision, selector, paths and page arguments. The producer seeks that node from the historical root, reconstructs ancestor iterators, and continues through later siblings. It stops at a response budget, traversal end, or the remaining selected-value count. There is no server session, client-supplied local page ID, or server-provided completion flag. A seek follows at most 8,193 ancestor nodes; producing a fragment visits at most 8,193 nodes including the one that would overflow the response. A valid node fits alone.

The verifier checks every returned node against its retained parent commitment and prefix. The frontier stores one prefix and lazy child iterator per ancestor; strictly growing prefixes bound depth by the 8,192-byte path limit, independently of total repository size. Frontier memory depends on depth and fanout, and can exceed a single response budget. Fragment bytes are released as they are consumed. An empty, replayed, omitted, reordered or surplus fragment cannot establish completion. The client returns page values only after verifying the complete page and lookahead. A transport failure discards that in-memory page; retry starts its proof again without advancing the sync watermark. Oversized point requests still split to fit the 1-MiB request budget, reserving room for a continuation. No supported tree requires a larger response budget or an unverified fallback.

The shared Rust scribe_client::tree::verified_page checks the proof against caller-supplied trusted roots, reconstructs results and more, and rejects missing/extra nodes, changed digests, invalid summaries, and invalid ordering. Authenticated child counts and prefix intervals justify skipped subtrees. A limit-plus-one live witness proves continuation. Empty and all-deleted trees have explicit verifiable representations. Permission is repository Reader, including public-read/no-auth configurations, matching tree enumeration. Proofs can reveal ancestor values and sibling metadata within that repository; a future path-scoped authorization model must restrict this operation accordingly.

Normal workspace sync uses these proofs against persisted per-branch history anchors, including backward deltas and deferred point states. It binds applied state atomically to journal completion. First contact trusts the connected server; later heads must reach the pinned commit through authenticated parents. This detects rollback and inconsistency against that client's anchor, but cannot prove global freshness or detect a server maintaining separate consistent forks for different clients. Restores require explicit re-anchoring. Update shared clients, server and gateways together for this unreleased request change.

Path-scoped access is not supported by this proof format. Introducing it requires reviewing ancestor/sibling disclosure, direct object downloads, historical objects and mount repositories together; filtering returned entries alone is insufficient.

Submit lock disposition

Ordinary, merge, and cherry-pick submissions release the submitting workspace's locks on their submitted paths at durable acceptance, including submitted keep entries and move endpoints. Locks on other paths or owned by other workspaces are untouched. keep locks = true retains acquisitions; retained moves may acquire a free destination from an owned source lock. Rejections retain locks. This flag is part of the operation fingerprint. Replaying a recorded operation returns its original result and never changes a newer acquisition.

Zdroj docs/wire-format.mdVerze 2b8dcccb