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
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 payloadThe 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.
| primitive | encoding |
|---|---|
| u8, bool | one byte; bool is 0 or 1, anything else is malformed |
| u16, u32, u64 | codec byte order |
| str16 / bytes16 | u16 length, then bytes; strings must be UTF-8 |
| str32 / bytes32 | u32 length, then bytes |
| opt_str16 | u8 present, then str16 if 1 |
| digest | 32 raw bytes |
| id16 | 16 raw bytes |
| object id | u8 kind (1 chunk, 2 manifest, 3 change set, 4 commit), digest |
| path | str16 whose bytes must form a canonical path (else invalid_path) |
| prefix | str16, empty or a canonical path |
| file state | digest manifest, digest content, u64 size, u8 mode, u64 change |
| opt file state | u8 present, then file state |
| semantic error | u16 code, str16 detail (≤ 4096 bytes) |
| lock info | path, u64 generation, id16 workspace, id16 branch, str16 author, u64 acquired ms |
| submit outcome | id16 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:
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:
u8 caller | [bytes16 delegated certificate, only for caller 2] | [32] binding| caller | meaning | identity |
|---|---|---|
| 0 | anonymous | none; any TLS client certificate is ignored and only public repositories are readable |
| 1 | direct | the verified TLS client certificate, which must be an access certificate |
| 2 | delegated | the 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:
| range | codes |
|---|---|
| 1..99 request | 1 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 repository | 100 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 submission | 200 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 locks | 300 lock_held, 301 lock_not_held, 302 lock_generation_stale |
| 500..599 storage | 500 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.
| op | name | request | response |
|---|---|---|---|
| 37 | discover | (empty) | u8 role (1 storage, 2 gateway), bool authentication configured, u32 configured storage servers, str16 namespace × servers (one per server; empty on plaintext) |
| 2 | server_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 |
| 3 | repo_create | str16 name (≤193; <namespace>/<name> through a gateway), u8 storage compression (0 none, 1 LZ4; permanent) | repo info |
| 4 | repo_list | u8 has_cursor, optional [16] opaque cursor | u8 has_next, optional [16] cursor, u32 count (≤128), repo info × count |
| 5 | repo_info | u8 1 + id16, or u8 2 + str16 name (≤193) | repo info |
| 6 | workspace_register | repo, workspace, branch, u16 view count, prefix × count, str16 label, u16 tag count (≤ 16), str16 tag (≤ 64 bytes) × count | (empty) |
| 42 | pin_objects | workspace id, intent id, repo, u32 count (≤ 8192), object id × count | u32 count, bool × count |
| 43 | tree_proof | bytes32 tree_page request, selector, u32 path count, str16 paths | content root[32], revision root[32], u32 node count, bytes32 portable revision node × count |
| 7 | presence | repo, u32 count (≤ 8192), object id × count | u32 count, bool × count |
| 8 | put_objects | repo, 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 |
| 9 | submit | repo, id16 expected incarnation, branch, workspace, operation, digest change set, str16 author (≤ 256), str32 message (≤ 65536), bool keep locks | submit outcome |
| 10 | operation_outcome | repo, operation | u8 present, then submit outcome |
| 11 | head | repo, branch | u64 head, id16 incarnation |
| 12 | tree_page | repo, branch, u64 change, prefix, opt_str16 after, u32 limit | u32 count, (path, file state) × count, bool more, content root[32], revision root[32] |
| 13 | changed_paths | repo, branch, u64 from, u64 to (target), prefix, u8 token present (+ bytes32 token), u32 limit | u32 count, (u64 change, path, opt file state as of to, bytes32 token) × count, bool more |
| 14 | get_objects | repo, u32 count (≤ 1024), object id × count | STREAM 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 |
| 15 | log | repo, branch, bool follow merges, bool all branches, bool snapshot present (+ u64), opt_str16 path, u8 before present (+ u64), opt_str16 after path, u32 limit | u32 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) |
| 16 | lock | repo, path, workspace, branch, str16 author | lock info |
| 17 | unlock | repo, path, workspace, u64 generation, bool force | (empty) |
| 18 | locks | repo, prefix, opt_str16 after, u32 limit | u32 count, lock info × count, bool more |
| 19 | ping | (empty) | (empty) |
| 38 | repo_usage | repo | u64 apparent bytes |
| 39 | describe_change | repo, u64 change, u32 expected revision, str16 author (≤ 256), str32 text (≤ 65536) | change note |
| 40 | change_notes | repo, u32 count (≤ 4096), u64 change × count | u32 count, (bool present, change note if present) × count, positional |
| 41 | change_note_history | repo, u64 change | u32 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_objectsagain 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_foundnames an unknown id. - A mutating response is sent only after the durable commit. A lost response
leaves the outcome unknown;
operation_outcomeresolves 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 throughoperation_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.
| Opcode | Operation |
|---|---|
| 20 | branch list |
| 21 | branch create/rename with persisted operation id |
| 22 | branch operation outcome |
| 23 | pinned merge parents and merge-base discovery |
| 24 | submit 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:
- Exact 16-byte data-root incarnation.
u64 total_repositories,u64 used_bytes(apparent whole-root bytes, populated on the first page only).u8 has_available_bytes(0 or 1), optionalu64 available_bytes.u32 repository_count, at most 128; each entry is the existingRepoInfoencoding followed byu64 apparent_repository_bytes.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.
| opcode | bound (bytes) | derivation |
|---|---|---|
hello (first frame only) | 16,692 | hello fields (273) + u8 caller + bytes16 delegated access certificate (16,384) + 32-byte binding |
ping, server_info, discover | 0 | empty |
authorize_repo_create | 67 | u8 + str16 namespace (64) |
head, operation_outcome, BranchOutcome, workspace_remove_outcome | 32 | two id16 |
branch_list, authorize_upload, lock_policy, repo_usage | 16 | id16 |
change_note_history | 24 | id16 + u64 |
repo_list, instance_status | 17 | u8 + optional id16 |
repo_create | 196 | str16 name (193) + u8 compression |
repo_info | 196 | u8 kind + str16 name (193) |
workspace_list | 38 | id16 + two u8 + optional id16 + u32 |
repository_mounts | 40 | two id16 + u64 |
cherry_pick_info | 56 | three id16 + u64 |
workspace_remove | 64 | four id16 |
MergeInfo | 66 | three id16 + two optional u64 |
BranchMutate | 581 | four id16 + str16 name (256) + u8 + str16 expected name (256) |
lock | 8,500 | id16 + path (2 + 8,192) + two id16 + str16 author (256) |
unlock | 8,235 | id16 + path + id16 + u64 + u8 |
tree_page | 16,433 | two id16 + u64 + prefix + optional after + u32 |
tree_proof | 1,048,576 | bounded aggregate tree request, selector and optional point paths |
locks | 16,409 | id16 + prefix + optional after + u32 |
log | 16,446 | two id16 + three flags + optional u64 + optional path + optional u64 + optional path + u32 |
changed_paths | 16,507 | two id16 + two u64 + prefix + optional bytes32 token (8,256) + u32 |
change_notes | 32,788 | id16 + u32 + 4,096 × u64 |
get_objects | 33,812 | id16 + u32 + 1,024 × (u8 + digest) |
put_objects | 42,004 | id16 + u32 + 1,024 × (u8 + digest + u64); bodies are streamed, never framed |
describe_change | 65,826 | id16 + u64 + u32 + str16 author (256) + str32 text (65,536) |
submit | 65,910 | five id16 + digest + str16 author (256) + str32 message (65,536) |
MergeSubmit | 65,970 | bytes32 submit + u64 + id16 + u64 + id16 + u64 |
cherry_pick_submit | 65,946 | bytes32 submit + u64 + id16 + u64 |
get_chunks | 65,556 | id16 + u32 + 1,024 × two digests |
pin_objects | 270,388 | workspace id16 + intent id16 + repo id16 + u32 + 8,192 × (u8 + digest) |
presence | 270,356 | id16 + u32 + 8,192 × (u8 + digest) |
workspace_register | 2,099,030 | three id16 + u16 + 256 × path + str16 label (256) + u16 + 16 × str16 tag (64) |
MergeRead | 8,390,725 | id16 + 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_sourcepending 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_principalconnections (default 64); the next hello is answeredbusy(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.
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.
| verb | caller | arguments after the verb → rows |
|---|---|---|
trust-bundle | anyone | none → [trust domain], then one [credential or access, CA certificate PEM] row per CA |
login, login-outcome | anonymous, with the password | username, 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-outcome | anonymous, with the password of an enabled person auth administrator | as 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-certificate | any account credential (not a storage server) | none → [access certificate PEM, lifetime ms] for the caller's own key; issuing writes nothing |
identity | a session or automation credential | none → [user id, username] |
logout, session-outcome | the session or automation credential being revoked | none → [ok]; an already revoked, expired, or pruned credential answers ok without writing; session-outcome of a live credential → no rows |
automation-create, automation-outcome | a password session | credential id, public key, label, expires ms, role → [ok, expires ms, credential certificate PEM] |
automation-derive, derive-outcome | an automation credential that is not itself derived | credential id, public key, label → [ok, expires ms, credential certificate PEM] |
authorize | a storage server credential | credential, key id, repository, actor credential, actor key id → [principal id, name, role, namespace admin, lease ms, public read, namespace reader] |
authorize-batch | a storage server credential | credential, key id, actor credential, actor key id, repository… (1..125 repositories) → [can read, lease ms] per repository |
public-policy | a storage server credential | repository → [public or private, visibility revision, lease ms] |
| administrative verbs | an administrator credential | listed 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-createrolesreader,writer,maintainer, andnamespace-admincreate credential kindsautomation,automation-write,automation-maintain, andautomation-admin. The expiry must be within one year. Only a password session may create one, over its own connection.automation-deriveissues 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:
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 idcredentials-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.
| Opcode | Request | Response |
|---|---|---|
34 workspace_list | [16] repo, u8 everyone, u8 has-after, [16] after?, u32 limit | u32 count, registration rows, u8 more |
35 workspace_remove | [16] repo, [16] incarnation, [16] workspace, [16] operation | removal outcome |
36 workspace_remove_outcome | [16] repo, [16] operation | empty 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.