OXYGENHUB
# 750251b21a96 oxygen / docs / spec.md
60.5 KiB · Markdown · last changed Import the Oxygen specification and the first Rust implementation (root change) 1 month ago renderedsource blame history raw
1
# Oxygen (`oxy`) — Version Control System Specification
2
3
| | |
4
|---|---|
5
| **Status** | Draft 0.5 |
6
| **Format version** | 1 |
7
| **Date** | 2026-09-26 |
8
| **Audience** | Implementers, security reviewers, and contributors |
9
10
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as described in RFC 2119.
11
12
---
13
14
## Table of contents
15
16
1. [Overview]#1-overview
17
2. [Goals, non-goals, and lineage]#2-goals-non-goals-and-lineage
18
3. [Threat model]#3-threat-model
19
4. [Terminology]#4-terminology
20
5. [Object model and hashing]#5-object-model-and-hashing
21
6. [The change graph]#6-the-change-graph
22
7. [Files, trees, and atoms]#7-files-trees-and-atoms
23
8. [Dependencies and commutation]#8-dependencies-and-commutation
24
9. [Conflicts]#9-conflicts
25
10. [States, bookmarks, and tags]#10-states-bookmarks-and-tags
26
11. [Checkpoints]#11-checkpoints
27
12. [Operation log, workspaces, and the working copy]#12-operation-log-workspaces-and-the-working-copy
28
13. [Storage engine]#13-storage-engine
29
14. [Identity, signatures, and trust policy]#14-identity-signatures-and-trust-policy
30
15. [Transport]#15-transport
31
16. [Wire protocol]#16-wire-protocol
32
17. [Partial, sparse, and shallow repositories]#17-partial-sparse-and-shallow-repositories
33
18. [Locks for unmergeable files]#18-locks-for-unmergeable-files
34
19. [Repository configuration files]#19-repository-configuration-files
35
20. [Hooks and server policy]#20-hooks-and-server-policy
36
21. [Garbage collection]#21-garbage-collection
37
22. [Client profiles and retro hardware]#22-client-profiles-and-retro-hardware
38
23. [Git interoperability]#23-git-interoperability
39
24. [Command-line interface]#24-command-line-interface
40
25. [Performance targets]#25-performance-targets
41
26. [Conformance and test vectors]#26-conformance-and-test-vectors
42
27. [Open questions]#27-open-questions
43
44
---
45
46
## 1. Overview
47
48
Oxygen (`oxy`) is an open-source, distributed version control system written in Rust. It is built for multi-gigabyte repositories, monorepos, and high-frequency development loops, and it defines a reduced client profile so that older and low-specification machines, which current version control systems exclude, can still take part.
49
50
Its distinguishing design choices are:
51
52
- **History is a set of changes, not a sequence of snapshots.** A change records what it did to identified lines and tree nodes, and which earlier changes it depends on. Changes that do not depend on each other commute: applying them in any order yields the same result.
53
- **Every change has two identifiers.** A random, stable *change ID* that survives rewriting, and a content-addressed *change hash* that makes history tamper-evident. Neither is optional.
54
- **Conflicts are data, not errors.** A conflicted state can be committed, pushed, fetched, and built on. Resolution is itself a change.
55
- **Every mutation of repository state is logged and undoable** through an operation log.
56
- **Content is stored in a content-defined-chunked, BLAKE3-addressed store**, so large files and large histories deduplicate naturally and can be fetched lazily.
57
- **Every change and every reference update is authenticated** with composite Ed25519 + ML-DSA-65 signatures, either on the change itself or on a push-time seal covering it, and verification is driven by an in-repository trust policy.
58
- **Transport is pluggable.** HTTPS is mandatory for every server, Noise is available for peer-to-peer use, and a plaintext transport exists for constrained clients on trusted networks, with integrity provided end to end by the object model rather than by the channel.
59
60
## 2. Goals, non-goals, and lineage
61
62
### 2.1 Goals
63
64
- Correct, deterministic merges of concurrent work without merge commits for independent changes.
65
- Stable identifiers for human and tooling use across rebase, amend, and reorder.
66
- Tamper-evident history and verifiable authorship, robust against future large quantum computers.
67
- Repositories of tens of gigabytes with millions of files, with `status` and `commit` latency independent of repository size.
68
- Partial clone by path, depth, and blob size as a first-class mode, not an afterthought.
69
- Interoperability with Git repositories in both directions.
70
- A documented Thin client profile for machines with a 68000-class CPU and 1 MiB of RAM.
71
72
### 2.2 Non-goals
73
74
- Running on machines with less than 512 KiB of RAM.
75
- Replacing code review, CI, or issue tracking.
76
- Semantic (language-aware) merging. Merging is line-based for text and replace-based for binary.
77
- Confidentiality of repository contents at rest in version 1 (see §27).
78
79
### 2.3 Lineage
80
81
The change model is the patch theory of Darcs and Pijul. The stable-ID-plus-hash split, the first-class working-copy revision, and the operation log come from Jujutsu. The chunked content store follows bup, restic, and Xet. The signed reference model follows The Update Framework (TUF) and gittuf. Oxygen's contribution is combining these with a partial-fetch-first protocol, composite post-quantum signatures, and a constrained-client profile in a single coherent design.
82
83
## 3. Threat model
84
85
### 3.1 Adversaries
86
87
| Adversary | Capabilities |
88
|---|---|
89
| **Network attacker** | Observe, modify, drop, delay, and replay traffic between client and server. |
90
| **Malicious or compromised server** | Serve arbitrary objects and references, withhold objects, and serve stale references. |
91
| **Malicious collaborator** | Hold a valid signing key authorized for some references and attempt to exceed that authorization, forge others' authorship, or introduce colliding identifiers. |
92
| **Compromised client host** | Full control of one contributor's machine, including their private keys. |
93
94
### 3.2 Security goals
95
96
- **G1 Integrity.** A client that has verified a state's heads can detect any modification to any object reachable from them.
97
- **G2 Authenticity.** Every change and every reference update carries a signature verifiable against an identity, and policy decides whether that identity is authorized.
98
- **G3 Freshness.** A client can detect when a server presents a reference older than one it has already seen.
99
- **G4 Non-downgrade.** An attacker cannot strip the post-quantum half of a composite signature and have it verify.
100
- **G5 Availability under adversarial input.** Pathological file contents cannot cause super-linear chunking, hashing, or graph work.
101
102
### 3.3 Explicitly out of scope
103
104
- Confidentiality against a malicious server (repository contents are visible to whoever hosts them).
105
- Recovery from a compromised signing key beyond revocation and policy update.
106
- Protection against a compromised client host altering its own working copy.
107
108
Every mechanism in §§5, 14, 15, and 16 is annotated with the goal it serves.
109
110
## 4. Terminology
111
112
| Term | Meaning |
113
|---|---|
114
| **Object** | An immutable, content-addressed byte string with a type. |
115
| **ObjectRef** | The 32-byte BLAKE3 hash of an object, including its domain-separation header. |
116
| **Change** | The unit of history: tree operations, edits, dependencies, and metadata. |
117
| **ChangeId** | A 128-bit random identifier that is stable across rewrites of a change. |
118
| **ChangeHash** | The ObjectRef of a change. Changes with different content have different hashes. |
119
| **Atom** | An identified unit of file content: one line of a text file. |
120
| **AtomId** | `(ChangeHash of introducing change, u32 index)`. |
121
| **Node** | A file, directory, or symlink, identified by a 128-bit random **NodeId** stable across renames. |
122
| **Placement** | The `(parent NodeId, name)` pair that positions a node in the tree. |
123
| **State** | A dependency-closed set of changes. Fully determined by its maximal elements (**heads**), and named by a `state` object that lists them. |
124
| **Bookmark** | A named, mutable pointer to a state. Equivalent to a Git branch. |
125
| **Tag** | A named, immutable, signed pointer to a state. |
126
| **Checkpoint** | A materialized snapshot of a state, used as a cache. |
127
| **Operation** | A recorded mutation of local repository state (bookmarks, workspaces). |
128
| **Workspace** | A checkout directory associated with a repository. |
129
| **Attestation** | A signed statement about a reference, tag, batch of changes, or key. |
130
| **Identity** | An object holding a contributor's public keys and metadata. |
131
132
## 5. Object model and hashing
133
134
### 5.1 Encoding
135
136
All objects are encoded as deterministic CBOR (RFC 8949 §4.2.1): definite lengths, shortest integer encodings, map keys sorted bytewise, no floating-point values, no indefinite-length items. Map keys are small unsigned integers; the tables in this document give the key number in brackets, e.g. `deps [2]`.
137
138
Every object begins with the field `version [0]`, an unsigned integer. This document defines version 1. Readers MUST reject objects whose version they do not understand.
139
140
### 5.2 Hashing (G1)
141
142
```
143
ObjectRef = BLAKE3-256( header || body )
144
header    = "oxy1 " || type-name || " " || decimal(len(body)) || 0x00
145
```
146
147
`type-name` is one of `chunk`, `blob`, `change`, `checkpoint`, `op`, `attest`, `identity`, `policy`, `state`. The header prevents an object of one type from being presented as another. BLAKE3 with a 256-bit output retains 128-bit collision resistance against quantum attackers.
148
149
The all-zero ObjectRef is reserved and means "self" inside a change (§7.4).
150
151
### 5.3 Object types
152
153
| Type | Purpose | Section |
154
|---|---|---|
155
| `chunk` | Raw bytes, at most 1 MiB. | §13.2 |
156
| `blob` | Ordered list of chunk refs plus total length; represents file content. | §13.2 |
157
| `change` | A history entry. | §6 |
158
| `checkpoint` | Materialized state. | §11 |
159
| `op` | Operation log entry. | §12 |
160
| `attest` | A signed statement. | §14.4 |
161
| `identity` | A contributor's keys. | §14.2 |
162
| `policy` | The repository's trust policy. | §14.5 |
163
| `state` | A sorted list of heads naming a state. | §10.1 |
164
165
### 5.4 Signatures are outside the hash
166
167
Signatures never appear inside a hashed object. A signed object is transported and stored as:
168
169
```
170
Signed = { object [0]: bytes, sigs [1]: [Signature] }
171
```
172
173
Adding a signature, re-signing, or attaching a seal (§14.4) therefore never changes an ObjectRef.
174
175
### 5.5 Encoding conventions
176
177
The field tables in this document give each map key in brackets. Where a structure is a choice between variants, or a compact reference, the following encodings apply:
178
179
| Item | Encoding |
180
|---|---|
181
| `TreeOp` | Map with key 0 = variant tag: 0 `AddNode`, 1 `RemoveNode`, 2 `Move`, 3 `SetMode`; key 1 = node id; `AddNode` adds 2 kind, 3 mode, 4 parent, 5 name; `RemoveNode` adds 2 placement ref; `Move` adds 2 old placement ref, 4 parent, 5 name; `SetMode` adds 2 old mode ref, 3 mode. |
182
| `Edit` | Map with key 0 = variant tag: 0 `Insert`, 1 `Delete`, 2 `Replace`; key 1 = node id; `Insert` adds 2 after, 3 before, 4 lines (array of byte strings) or 5 `lines_blob`; `Delete` adds 2 atom ranges; `Replace` adds 2 old (bytes or null), 3 new (bytes or null). |
183
| `NodeKind` | Unsigned integer: 0 file, 1 dir, 2 symlink. |
184
| `Mode` | Unsigned integer bit field: bit 0 executable, bit 1 opaque content mode. Directories carry 0. |
185
| `OpRef` (placement or mode reference) | `[bytes(32) change hash, uint index]`; the all-zero hash means this change. |
186
| `AtomRef` | `0` for `START`, `1` for `END`, otherwise `[bytes(32) change hash, uint index]` with the all-zero hash meaning this change. |
187
| `AtomRange` | `[bytes(32) change hash, uint from, uint to]`, half-open, non-empty. |
188
| `View` (in `Op`) | Key 0 bookmarks: array of `[text name, bytes(32) state ref]` sorted by name; key 1 workspaces: array of `[bytes(16) id, bytes(32) change]` sorted by id. Remote and tag namespaces are added in later drafts. Repository format 1 stored `[name, [heads]]`; format 2 stores the state ref. |
189
| `Signature` | Map: 0 key id, 1 alg (1 = Ed25519 + ML-DSA-65), 2 Ed25519 bytes(64), 3 ML-DSA-65 bytes(3309). A signature list is a CBOR array of these. |
190
| Secret key file | Map: 0 version, 1 key id, 2 Ed25519 seed bytes(32), 3 ML-DSA-65 seed bytes(32). Kept only in the user's key directory. |
191
192
Atom indices count the atoms one change introduces for one node, in edit order, across all of that change's `Insert` edits on the node.
193
194
## 6. The change graph
195
196
### 6.1 Change object
197
198
```
199
Change {
200
  version   [0]: 1
201
  change_id [1]: bytes(16)            // random, CSPRNG; see §6.3
202
  deps      [2]: [ChangeHash]         // sorted, unique; see §8
203
  author    [3]: KeyId                // ObjectRef of an identity object
204
  hlc       [4]: { wall_ms: u64, logical: u32 }
205
  message   [5]: text
206
  tree_ops  [6]: [TreeOp]             // §7.2
207
  edits     [7]: [Edit]               // §7.4
208
  kind      [8]: 0 = normal, 1 = resolution, 2 = working-copy
209
  extra     [9]: map                  // reserved; MUST be empty in v1
210
}
211
```
212
213
### 6.2 Two identifiers
214
215
- **ChangeHash** is the integrity handle (G1). All references between objects use it. Dependencies, bookmark heads, tags, and checkpoints reference changes by hash only.
216
- **ChangeId** is the stable handle. It is what users type, what tooling tracks, and what survives amend, rebase, and reorder. It carries no integrity meaning and is never used alone as a reference in any object.
217
218
A change may be *rewritten*: a new change object is created with the same ChangeId and a different hash. Changes that depended on the old hash are *orphaned* and must be rewritten in turn (§8.4). A repository may hold several changes with the same ChangeId; when more than one is reachable from the same state the ChangeId is *divergent*, and commands MUST report divergence rather than pick silently.
219
220
### 6.3 ChangeId generation
221
222
ChangeIds MUST be produced by a cryptographically secure random generator with 128 bits of entropy on Full and Lite profiles. On the Thin profile they are generated by the proxy (§22). Two changes with equal ChangeId and unequal hash are not an error; they are divergence (§6.2). An implementation MUST NOT infer anything about content from a ChangeId.
223
224
### 6.4 Timestamps
225
226
`hlc` is a hybrid logical clock: `wall_ms` is the author's wall clock in milliseconds since the Unix epoch, and `logical` is a counter. On observing a change with a later HLC, a client advances its own clock per the Kulkarni et al. HLC rules. `wall_ms` is untrusted metadata and MUST NOT be used for ordering, security, or freshness decisions. Sorting for display uses `(hlc, ChangeHash)`.
227
228
**Drift bound.** A change with a far-future `wall_ms` must not be able to drag every peer's clock forward. On observing a change, a client MUST NOT advance its own HLC more than `core.hlc-max-drift` (default 5 minutes) ahead of its local wall clock; the change itself is still accepted and stored, so whether a state is well-formed never depends on the observer's clock. A server MAY reject a push containing a change whose `wall_ms` is more than `core.hlc-max-drift` ahead of the server clock, since the author is present and can correct their clock. `logical` is a `u32`; a client whose counter would overflow MUST refuse to create the change and report clock skew.
229
230
## 7. Files, trees, and atoms
231
232
### 7.1 Tree model
233
234
The repository tree is a set of nodes. Each node has a **NodeId**, a **kind** (`file`, `dir`, `symlink`), a **mode**, and zero or more **placements**. The mode of a file is its executable bit and its **content mode**, `text` or `opaque` (§7.5); a symlink's content is its target path and its content mode is always `opaque`; a directory has no mode. The root directory has the fixed NodeId of 16 zero bytes and no placement.
235
236
A placement is `(parent NodeId, name)`. Names are byte strings that MUST be valid UTF-8, MUST NOT be empty, `.`, or `..`, and MUST NOT contain `/` or NUL. Placements that a particular file system cannot represent distinctly, such as names differing only by case, are workspace conflicts (§9.7), not errors in the state.
237
238
Renames and moves are placement changes, so rename tracking is exact and never heuristic.
239
240
### 7.2 Tree operations
241
242
Placements are atoms in the sense of §7.3: each is introduced by exactly one change and may be deleted by later changes. Because a placement is defined relative to the parent's NodeId rather than a path, moving a directory moves its whole subtree with a single placement change; no operations are generated for descendants. The cost of this is that two concurrent moves can create a cycle, handled in §9.2.
243
244
```
245
TreeOp = AddNode  { node: NodeId, kind, mode, place: (parent, name) }   // introduces node + placement
246
       | RemoveNode { node: NodeId, placement: PlacementRef }            // deletes the placement; node dead when no live placements
247
       | Move     { node: NodeId, old: PlacementRef, place: (parent, name) } // deletes old placement, introduces new one; implicitly moves all descendants
248
       | SetMode  { node: NodeId, old: ModeRef, mode }                  // mode is an atom too
249
PlacementRef = (ChangeHash, u32 index into that change's tree_ops)
250
```
251
252
A node is **live** in a state when it has at least one live placement and its parent is live. A node with more than one live placement, or two live nodes with the same placement, is a conflict (§9.2).
253
254
### 7.3 Atoms and the file graph
255
256
The content of a text file is a directed graph whose vertices are atoms. Each atom is one line, including its terminator (bytes up to and including `\n`, or to end of file). Edges express "immediately after". An atom is introduced by exactly one change and has an `AtomId = (ChangeHash, index)`, where `index` counts atoms introduced by that change for that node, starting at 0.
257
258
Every file graph contains two virtual atoms, `START` and `END`. An atom is **live** unless a change in the state deletes it. Deleted atoms remain in the graph as tombstones; they may still be referenced as anchors.
259
260
The graph of a file in a state is the union of the atoms and edges introduced by every change in the state, minus nothing: deletion sets a flag, it does not remove vertices. **The graph is a function of the set of changes alone and not of the order they were applied.** Implementations MUST preserve this invariant; the conformance suite (§26) tests it by applying random permutations.
261
262
### 7.4 Edits
263
264
```
265
Edit = Insert  { node: NodeId, after: AtomRef, before: AtomRef, lines: [bytes] | lines_blob: ObjectRef }
266
     | Delete  { node: NodeId, atoms: [AtomRange] }
267
     | Replace { node: NodeId, old: ObjectRef | null, new: ObjectRef }     // opaque nodes only
268
AtomRef   = START | END | AtomId
269
AtomRange = { change: ChangeHash, from: u32, to: u32 }   // half-open
270
```
271
272
- `Insert` introduces `len(lines)` new atoms, indexed consecutively, and edges `after → first`, `last → before`. `after` and `before` MUST be adjacent in the author's view at authoring time; the pair is the insert's **anchor**.
273
- Atoms introduced by the same change are referenced with the all-zero "self" hash. This is how a change can insert a line and then insert another after it without knowing its own hash.
274
- `lines_blob` MAY replace inline `lines` when the inserted content exceeds 64 KiB; the blob's content is split into atoms by the same line rule.
275
- `Replace` is the only edit for `opaque` nodes. `old` is the blob currently expected; two concurrent `Replace` operations with the same `old` and different `new` conflict (§9.3).
276
277
### 7.5 Opaque nodes
278
279
A node's content mode is `opaque` if its content is not line-mergeable. A node MUST be opaque if it matches a `binary` pattern in `.oxyattributes`, if it contains a NUL byte in its first 8 KiB, if it exceeds `core.max-text-size` (default 4 MiB), or if it is a symlink. Symlink targets are stored as small blobs and edited only by `Replace`, bypassing the line graph. Opaque content is stored as a blob and versioned by `Replace`.
280
281
Content mode is part of the node's mode atom, so a switch is a `SetMode`. A change switching a node to `opaque` MUST also `Delete` every live atom of the node and `Replace` with `old = null`; a change switching to `text` MUST also `Replace` with `new = null` and `Insert` the content between `START` and `END`. Concurrent edits made under the other content mode then conflict through the mode atom (§9.2) rather than silently combining.
282
283
### 7.6 Line endings and encoding
284
285
Oxygen stores bytes verbatim and never rewrites line endings unless `.oxyattributes` requests `eol=lf` or `eol=crlf`, in which case normalization happens on snapshot and denormalization on checkout, exactly as Git does with `text eol=`. Implementations MUST NOT guess.
286
287
### 7.7 Checkout safety
288
289
Checkout writes only inside the workspace directory, whatever the tree contains.
290
291
- Checkout MUST create a symlink without following it and MUST NOT write any file through a path that contains a symlink, whether that symlink came from the repository or already existed in the workspace. Implementations SHOULD open parent directories with `O_NOFOLLOW` or the platform equivalent and write relative to that handle.
292
- A placement named `.oxy` under the root, or any placement that resolves to a path inside `.oxy/`, MUST be refused at snapshot and at checkout.
293
- `.oxyignore` and `.oxyattributes` files that are symlinks MUST be treated as absent, with a warning from `oxy status`.
294
- On file systems without symlink support, a symlink node is written as a plain file containing the target path, and `oxy status` reports it; snapshot MUST NOT convert such a file back into a content change.
295
296
## 8. Dependencies and commutation
297
298
### 8.1 Definition
299
300
The **scope** of a change `C` is the set of atoms and placements it references: every atom it deletes, every atom used as an anchor, every placement it deletes or moves, and every node it operates on.
301
302
`deps(C)` MUST contain the ChangeHash of the introducing change of every element of `scope(C)`, excluding `C` itself, START, END, and the root node. 
303
304
**Explicit (semantic) dependencies.** Changes that share no atoms commute even when they are semantically coupled, for example an API change in one file and the update of its caller in another. To prevent such changes from being separated, `deps(C)` MAY contain additional hashes that the author or tooling chose to require. `oxy commit --depends-on <change>` adds one; the argument is resolved to a ChangeHash as in §24, and a divergent ChangeId is an error. Implementations SHOULD support the `semantic-deps` hook (§20) so that build tools or language servers can inject cross-file dependencies at commit time.
305
306
An explicit dependency is a hard dependency with every consequence of §8.2 and §8.4: the change no longer commutes with its target, and rewriting the target orphans it. Tooling that injects dependencies MUST inject the minimal set and MUST be enabled per repository by the user, because injecting dependencies liberally turns history back into a linear sequence and forfeits the model's benefits. `deps(C)` MUST NOT be transitively reduced by the writer; readers MAY reduce it.
307
308
A change with `kind = resolution` MUST additionally depend on every change that contributed an atom or placement to the conflict it resolves.
309
310
### 8.2 Commutation
311
312
Two changes `A` and `B` **commute** if neither is in the transitive dependency closure of the other. By §7.3 applying `{A, B}` in either order produces the same graph, so no rewriting, re-hashing, or merge change is needed to reorder them. Commutation is a consequence of the model, not a separate rule to implement; the implementer's obligation is the invariant in §7.3.
313
314
### 8.3 Well-formedness
315
316
A state `S` is well-formed iff for every `C ∈ S`, `deps(C) ⊆ S`. Every reference to a state (bookmark, tag, checkpoint) names it by its heads, and the state is the dependency closure of the heads. Implementations MUST refuse to construct a state that is not well-formed and MUST refuse to store a change whose dependencies are unavailable unless the repository is shallow (§17).
317
318
### 8.4 Rewriting
319
320
Rewriting a change `C` to `C'` (amend, rebase, split, squash) produces a new object with the same ChangeId. Any change `D` with `hash(C) ∈ deps(D)` is orphaned relative to `C'`. `oxy` MUST automatically rewrite orphaned descendants by translating their AtomRefs and PlacementRefs from `C` to `C'` where a correspondence exists, and MUST record a conflict (§9) where it does not. Changes that do not depend on `C` are untouched. Rewriting is recorded in the operation log (§12) and is undoable.
321
322
## 9. Conflicts
323
324
### 9.1 Text conflicts
325
326
The live atoms of a file graph form a partial order. The file is **conflicted** if that order is not total. Two shapes arise:
327
328
- **Order conflict.** Two or more chains of live atoms are incomparable (concurrent inserts at the same anchor, or concurrent edits of overlapping regions). The conflict region is the minimal interval of the graph containing all incomparable chains.
329
- **Delete/insert conflict.** An atom `x` is deleted by change `D`, and a live atom `y` is anchored after `x` by a change `I`, where `D` and `I` commute. The inserted content has no live neighbour to attach to.
330
331
### 9.2 Tree conflicts
332
333
- **Name conflict.** Two live nodes share a placement.
334
- **Multi-placement.** One node has two live placements (concurrent renames).
335
- **Orphan.** A live node's parent directory is not live (concurrent delete of a directory and modification within it).
336
- **Cycle.** A directory is placed inside its own descendant, which arises when two commuting changes each move a directory under the other. Both placements are live and the tree is conflicted. For materialization the placement whose introducing change has the greater `(hlc, ChangeHash)` is treated as absent; resolution deletes one of the two placements.
337
- **Kind conflict.** Concurrent `AddNode` of the same placement with different kinds is a name conflict; concurrent `SetMode` with different modes is an order conflict on the mode atom.
338
339
### 9.3 Opaque conflicts
340
341
Two live `Replace` edits with the same `old` and different `new` are a **replace conflict**. The state exposes both candidate blobs.
342
343
### 9.4 Properties
344
345
- A conflicted state is well-formed and valid. It MAY be committed, pushed, fetched, tagged, and built upon.
346
- Conflicts nest and compose automatically because they are graph shapes, not special objects. A resolution that is itself concurrent with another resolution yields a new order conflict between the two resolutions.
347
- Servers MAY refuse conflicted heads on protected bookmarks through policy (§20). Clients MUST be able to push conflicted heads to unprotected bookmarks.
348
349
### 9.5 Materialization in the working copy
350
351
Conflicted text files are written with markers that identify the conflict and each side so that tools can parse them unambiguously:
352
353
```
354
<<<<<<< oxy conflict 1 of 2, side 1: change k7m2p9 "Add retry loop"
355
... lines ...
356
======= oxy conflict 1 of 2, side 2: change q3x8w1 "Refactor client"
357
... lines ...
358
>>>>>>> oxy conflict 1 of 2 end
359
```
360
361
The short identifiers are ChangeId prefixes. Delete/insert conflicts render the deleted context as a `-------` section. Tree conflicts are materialized by writing each candidate with a suffix `.oxy-conflict-<n>` and are listed by `oxy status`. Replace conflicts materialize as `<name>.oxy-conflict-<n>` files.
362
363
`oxy status` MUST list every conflict, MUST exit with status 2 when any conflict exists in the working-copy state, MUST exit with status 3 when only workspace conflicts (§9.7) exist, and MUST support `--json` output enumerating conflicts by node, kind, and contributing ChangeIds.
364
365
### 9.6 Resolution
366
367
A **resolution** is a change with `kind = resolution` whose deletions and insertions leave the affected graphs totally ordered and the tree conflict-free. `oxy resolve` computes it from the edited working copy by parsing the markers; a resolution MAY also be authored by any tool that can produce a change. Because a resolution depends on all contributing changes (§8.1), any peer that fetches it necessarily has the conflict it resolves.
368
369
### 9.7 Workspace conflicts
370
371
A state can be conflict-free and still not be representable on a particular file system. These are **workspace conflicts**: the state is valid, peers on other file systems see nothing, and no resolution change is involved.
372
373
- Two live placements in one directory whose names are equal under the file system's case folding (`Foo.c` and `foo.c` on default Windows and macOS volumes).
374
- Two live placements whose names are equal after the file system's Unicode normalization (NFC and NFD forms of the same name on macOS).
375
- A name the file system reserves or alters: Windows device names such as `CON` and `NUL`, names ending in a dot or a space, and names containing `:` or other characters the platform forbids.
376
- A path longer than the platform limit.
377
378
Each colliding node MUST be materialized with a `.oxy-conflict-<n>` suffix so that no content is lost, and `oxy status` MUST list the conflict with its kind. A workspace conflict is cleared by any ordinary change that renames one of the nodes; it is never counted by `allow_conflicts` (§14.5) or by server rules that forbid conflicted heads (§20).
379
380
## 10. States, bookmarks, and tags
381
382
### 10.1 References
383
384
References live in a namespace:
385
386
```
387
bookmarks/<name>                 local bookmarks
388
remotes/<remote>/bookmarks/<name>  last-seen remote bookmarks
389
tags/<name>                      tags (immutable once created)
390
policy                           the trust policy head (§14.5)
391
```
392
393
`<name>` is UTF-8, 1–255 bytes, without control characters, leading `-`, or the sequences `..`, `//`, `@{`.
394
395
A bookmark or tag points at a **state object**:
396
397
```
398
State { version [0]: 1, heads [1]: [ChangeHash] }   // sorted, unique; may be empty
399
```
400
401
Every reference, attestation, view, and checkpoint therefore names a state by one ObjectRef however many heads it has. Multiple heads are normal in this model: every independent change is a head until something depends on it, so a busy bookmark may have dozens. That is not a conflict in the §9 sense; `oxy log` shows the heads as parallel, and a state object keeps the cost of naming them constant for everything that refers to the bookmark. The empty state, `State { heads: [] }`, is what a new bookmark points at.
402
403
### 10.2 Reference attestations (G2, G3)
404
405
Every published bookmark update is accompanied by an attestation (§14.4) of kind `ref` that binds the repository ID, the reference name, the previous state, the new state, and a per-reference monotonically increasing `serial`. Servers MUST store the latest attestation per reference and serve it with the reference. Clients MUST verify the attestation against policy before updating a `remotes/` reference and MUST reject an attestation whose `serial` is lower than one they have already recorded for that reference (rollback protection). Clients SHOULD warn when the newest attestation's `hlc.wall_ms` is older than `remote.max-age` (default 30 days) and the remote claims no update.
406
407
Tags are attestations of kind `tag` and are never updated; a server MUST reject a second attestation for an existing tag name.
408
409
## 11. Checkpoints
410
411
Materializing a state by replaying every change is linear in history length. Checkpoints make checkout proportional to state size instead.
412
413
```
414
Checkpoint {
415
  version [0]: 1
416
  heads   [1]: [ChangeHash]
417
  tree    [2]: [ { node, kind, mode, placements: [(parent, name)], content: ObjectRef /* blob */, graph: ObjectRef /* blob */ } ]
418
  conflicted [3]: bool
419
}
420
```
421
422
`content` is the live content of the node as a blob (with markers if conflicted). `graph` is a blob containing the file graph in run-length form: a sequence of `(ChangeHash, from, to, live: bool)` runs plus explicit edges wherever the graph departs from a single chain. The graph blob is sufficient to apply any further change without consulting history. Tombstones are retained because future changes may anchor to them.
423
424
A checkpoint is valid only if it is bit-for-bit reproducible from its heads. Implementations MUST create a checkpoint at every tag, MUST create one at least every `core.checkpoint-interval` changes (default 256) along each bookmark, and MAY discard any checkpoint at any time. Clients fetching a state SHOULD request the nearest checkpoint and replay only the difference.
425
426
## 12. Operation log, workspaces, and the working copy
427
428
### 12.1 Operations
429
430
Every command that mutates local repository state (bookmarks, workspaces, the working-copy change, fetched remote references) records an operation:
431
432
```
433
Op {
434
  version [0]: 1
435
  parents [1]: [ObjectRef]            // usually one; two when merging concurrent operations
436
  hlc     [2]: HLC
437
  command [3]: text                   // e.g. "oxy rebase -s k7m2p9 -d main"
438
  view    [4]: { bookmarks: map<name, state ObjectRef>, remotes: map, tags: map, workspaces: map<WorkspaceId, ChangeHash> }
439
}
440
```
441
442
`view` is the complete state of all references and workspaces after the operation. The operation log is a DAG; concurrent processes each append to their own head, and the next command merges heads by three-way merge of views, with both sides kept when they disagree on a bookmark (the bookmark then has multiple heads).
443
444
`oxy undo` moves the current operation to its parent's view. `oxy redo` and `oxy op log` expose the DAG. Operations are retained for `core.op-retention` (default 90 days) and then eligible for garbage collection.
445
446
Because all mutation goes through the operation log and object writes are idempotent, no global repository lock is needed. A short-lived lock is held only while snapshotting a single workspace's working copy.
447
448
### 12.2 Workspaces
449
450
A repository has one or more workspaces, each with a `WorkspaceId` (128-bit random) and a checkout directory. Each workspace has exactly one **working-copy change** at any time.
451
452
### 12.3 The working-copy change
453
454
The working copy is a real change with `kind = working-copy`. Before running any command that reads repository state, `oxy` snapshots the working copy: it diffs the file system against the working-copy change's materialized state and rewrites the working-copy change to include the difference. The change therefore always describes the working directory. To prevent operation log bloat, a command that snapshots the working copy MUST NOT record a new Operation (§12.1) if the filesystem diff was empty (i.e., the working-copy change's hash did not change).
455
456
- The working-copy change is unsigned and never pushed while `kind = working-copy`.
457
- `oxy commit` (alias `oxy describe`) sets the message, sets `kind = normal`, and starts a fresh working-copy change on top of it. The change is signed immediately if `core.sign = "change"`; with the default `core.sign = "seal"` it is left unsigned locally and covered by a seal at push time (§14.4).
458
- `oxy new`, `oxy edit <change>`, and `oxy abandon` move which change the workspace edits.
459
- There is no staging area and no stash. Partial commits are made with `oxy split`.
460
461
### 12.4 Snapshot performance
462
463
Implementations MUST support a file-system monitor (Watchman or a native inotify/FSEvents/ReadDirectoryChangesW watcher) and MUST fall back to a stat-based crawl with a persistent cache keyed by `(path, size, mtime, inode, ctime)`. Snapshot latency targets are in §25.
464
465
## 13. Storage engine
466
467
### 13.1 Layout
468
469
```
470
.oxy/
471
  format              "oxy repository format 2\n"
472
  config              repository-local configuration (TOML)
473
  objects/
474
    pack-<id>.pack    append-only pack file
475
    pack-<id>.idx     sorted (ObjectRef → offset, length, type) index
476
    loose/            objects written since the last pack, as header || body
477
    sigs/             signature lists keyed by the ObjectRef they cover (§5.4)
478
  ops/                operation objects and op heads
479
  refs/               current view cache (derived from ops; rebuildable)
480
  workspaces/<id>/    snapshot cache, fsmonitor cookie
481
  policy/             cached policy and identities
482
```
483
484
Object writes go to a temporary file, are `fsync`ed, and are renamed into place. Pack files are `fsync`ed before their index is written. An interrupted write can never corrupt an existing object. The refs cache is derivable from the operation log and is rebuilt if missing.
485
486
### 13.2 Chunking and blobs (G5)
487
488
File content is split with FastCDC (Xia et al., 2016) using a 32 KiB minimum, 64 KiB target, and 256 KiB maximum chunk size, with normalized chunking level 2 and cut-point skipping from the minimum size. Files of 32 KiB or less are a single chunk, and the final chunk of a file may be shorter than the minimum. The minimum and maximum bounds guarantee that adversarial content produces at most `len / 32 KiB` chunks and at least `len / 256 KiB` chunks, bounding both index growth and dedup loss.
489
490
So that every implementation produces identical boundaries, the parameters are fixed:
491
492
- **Gear table.** `GEAR[i]` for `i` in `0..256` is the little-endian `u64` at bytes `8i..8i+8` of the BLAKE3 extendable output of the ASCII string `oxy1 fastcdc gear`.
493
- **Rolling hash.** `h = (h << 1) + GEAR[byte]` in 64-bit wrapping arithmetic, starting from zero at the minimum size.
494
- **Masks.** A mask with `k` one-bits has them at bit positions `63 - floor(i * 64 / k)` for `i` in `0..k`. The small mask has 18 one-bits (used until the target size) and the large mask has 14 (used until the maximum). A cut occurs at the first position where `h & mask == 0`.
495
496
```
497
Blob { version [0]: 1, length [1]: u64, chunks [2]: [ObjectRef] }
498
```
499
500
Chunks and blobs are the only object types that can be large. Every other object type MUST be under 1 MiB after encoding; a change exceeding that MUST use `lines_blob`.
501
502
### 13.3 Compression
503
504
Each object in a pack is individually compressed with zstd (level 3 by default). Objects whose compressed size is not smaller are stored raw, with a flag. Chunks matching a `nocompress` attribute pattern (already compressed media) are stored raw.
505
506
### 13.4 Packing
507
508
`oxy gc` (and an automatic trigger when loose object count exceeds 4096) writes loose objects into a new pack and merges packs when there are more than 16. Pack indexes MUST support lookup by ObjectRef prefix and MUST be memory-mappable. A repository with two million objects MUST be openable with under 64 MiB of resident index memory on the Full profile.
509
510
### 13.5 Lazy objects
511
512
A blob or chunk may be **absent** in a partial repository (§17). The object store records absent refs it has been told about with the remote that promised them, and fetches on first read. Changes and checkpoints are never lazy: a repository that has a change has all of its inline data.
513
514
## 14. Identity, signatures, and trust policy
515
516
### 14.1 Algorithms
517
518
| Purpose | Algorithm |
519
|---|---|
520
| Hash | BLAKE3-256 |
521
| Signature (composite) | Ed25519 **and** ML-DSA-65 (FIPS 204) |
522
| Transport AEAD | ChaCha20-Poly1305 |
523
| Transport key agreement | X25519, with ML-KEM-768 hybrid where the Noise pattern supports it |
524
| Random | OS CSPRNG |
525
526
ML-DSA-65 is chosen over ML-DSA-44 for NIST security category 3, at the cost of signatures of 3309 bytes.
527
528
### 14.2 Identity object
529
530
```
531
Identity {
532
  version    [0]: 1
533
  ed25519_pk [1]: bytes(32)
534
  mldsa65_pk [2]: bytes(1952)
535
  name       [3]: text
536
  email      [4]: text
537
  created    [5]: u64 (wall ms)
538
  expires    [6]: u64 | null
539
  previous   [7]: KeyId | null        // rotation chain
540
  delegates  [8]: [KeyId]             // keys allowed to sign or seal on this identity's behalf
541
}
542
KeyId = ObjectRef of the Identity
543
```
544
545
The two public keys live in one object, so a `KeyId` names the pair. `delegates` lists keys that may authenticate changes whose `author` is this identity: a proxy acting for a Thin client (§22), or a CI signer. Delegation is one level deep and is not transitive. Rotation is a new identity with `previous` set, plus an attestation of kind `rotate` signed by the previous key. Revocation is an attestation of kind `revoke` signed by the revoked key or by a policy root key (§14.5).
546
547
### 14.3 Composite signature (G2, G4)
548
549
```
550
Signature {
551
  key  [0]: KeyId
552
  alg  [1]: 1                          // ed25519+mldsa65
553
  ed   [2]: bytes(64)
554
  ml   [3]: bytes(3309)
555
}
556
message = "oxy1 sig " || domain || 0x00 || KeyId || ObjectRef-being-signed
557
domain  = "change" | "attest"
558
```
559
560
Both signatures are computed over the same `message`, which includes the `KeyId` of the composite pair. Verification MUST require both signatures to verify with the keys of the identity named by `key`. A signature with either half missing, empty, or failing is invalid. This is the same construction as the IETF LAMPS composite signature drafts and prevents stripping the post-quantum half.
561
562
Profiles that cannot compute ML-DSA (§22) do not sign; they produce changes that a proxy signs.
563
564
### 14.4 Attestations
565
566
```
567
Attest {
568
  version [0]: 1
569
  kind    [1]: "ref" | "tag" | "seal" | "rotate" | "revoke" | "lock"
570
  repo    [2]: bytes(16)               // repository ID, chosen at init
571
  hlc     [3]: HLC
572
  body    [4]: map                     // per kind
573
}
574
ref:    { name, old_state: ObjectRef, new_state: ObjectRef, serial: u64 }
575
tag:    { name, state: ObjectRef, message }
576
seal:   { hashes: [ChangeHash] }        // sorted, unique, 1..4096 entries
577
rotate: { new: KeyId }
578
revoke: { key: KeyId, reason }
579
lock:   { node: NodeId, path, acquire: bool, expires }
580
```
581
582
A **seal** is one signature over a batch of change hashes. A composite signature is 3373 bytes, which is larger than a typical change, so signing every change individually roughly doubles the size of history in high-frequency workflows. A seal replaces that with one signature per push.
583
584
**Cadence.** With the default `core.sign = "seal"`, changes are left unsigned locally and `oxy push` creates one seal covering every change in the push that carries no individual signature, immediately before the reference attestation. Seals are created only at push: a seal made earlier would be invalidated by any amend, since rewriting produces a new hash. With `core.sign = "change"`, every change is signed at commit and no seal is needed. Policy (§14.5) decides which the receiving repository accepts.
585
586
**Verification.** A sealed change `C` is authenticated iff a seal `S` exists such that `hash(C) ∈ S.body.hashes`, `S` carries a valid signature from key `K`, and `K` equals `C.author` or is listed in `C.author`'s identity as a delegate (§14.2). The same author-or-delegate rule applies to individual change signatures. This rule is what stops a collaborator from sealing a change whose `author` field names someone else (§3.1). A seal's signer must additionally be authorized by policy for the reference being pushed.
587
588
**Handling.** A seal is stored and transported with the changes it covers: `fetch` (§16.1) streams the seals covering every change it returns, `push` (§16.3) MUST include a seal or signature for every change, and a seal covering any reachable change is itself reachable (§21). Seals from different keys MAY overlap; a change needs at least one valid authenticator, and policy thresholds count distinct authenticating keys.
589
590
### 14.5 Trust policy (G2)
591
592
The `policy` reference points to a signed `Policy` object:
593
594
```
595
Policy {
596
  version   [0]: 1
597
  repo      [1]: bytes(16)
598
  serial    [2]: u64
599
  roots     [3]: { keys: [KeyId], threshold: u32 }
600
  rules     [4]: [ { pattern: glob over reference names, keys: [KeyId], threshold: u32,
601
                     require: "signed" | "sealed" | "any" /* default */, allow_conflicts: bool } ]
602
  identities[5]: [Identity]           // embedded so verification needs no fetch
603
  revoked   [6]: [KeyId]
604
  expires   [7]: u64
605
}
606
```
607
608
- The initial policy is signed by the root keys; every update MUST be signed by `roots.threshold` root keys and have a higher `serial`.
609
- A change or attestation is **authorized** for reference `r` if its signer is listed in the first rule whose pattern matches `r`, and the rule's threshold of distinct authorized signers is met across the attestation and each change in the closure of `new_state` not previously in the closure of `old_state`. `require` selects which authenticators count for changes: individual signatures only, seals only, or either. Every change MUST additionally satisfy the author-or-delegate rule of §14.4 regardless of `require`.
610
- Clients MUST verify policy on fetch, MUST refuse to update a `remotes/` reference whose attestation is unauthorized, and MUST warn loudly on an expired policy. Servers MUST enforce the same rules on push.
611
- A repository without a `policy` reference is **unverified**; every command that talks to a remote MUST print a one-line warning until one is created. `oxy init` creates one by default.
612
613
## 15. Transport
614
615
Every transport carries the same wire protocol (§16). The transport provides the channel; integrity and authenticity of history come from the object model regardless of channel.
616
617
### 15.1 HTTPS (mandatory)
618
619
Every server MUST offer HTTPS with TLS 1.3 at `https://<host>/<repo>/oxy/v1/`. Authentication is by bearer token, mutual TLS, or HTTP signature over a server-issued challenge with the client's identity key. HTTPS is required because it traverses corporate proxies, load balancers, and CDNs; it is the interoperability baseline.
620
621
### 15.2 Noise (optional)
622
623
Peer-to-peer and self-hosted deployments MAY use Noise over TCP or QUIC.
624
625
- Pattern `Noise_XXhfs_25519+kyber768_ChaChaPoly_BLAKE2s` (hybrid post-quantum handshake) for first contact, or `IKhfs` when the server's static key is known.
626
- The client's Noise static key MUST be the X25519 key derived from its Ed25519 identity key (RFC 8032 key clamping via the standard birational map), so the transport identity and the signing identity are the same KeyId.
627
- Servers publish their static key fingerprint out of band. Clients pin it on first use and MUST refuse a changed key without explicit `oxy remote trust` action.
628
629
### 15.3 Zero-Crypto Transport (ZCT, restricted)
630
631
ZCT is a plaintext, length-prefixed framing of the wire protocol over TCP or serial, for Thin clients that cannot run ChaCha20 or X25519 at usable speed.
632
633
- ZCT is disabled by default on servers and on clients. A remote MUST be explicitly configured with `transport = "zct"` and clients MUST print a warning on every use.
634
- ZCT provides no confidentiality. Over ZCT the client MUST verify every change signature or seal, every reference attestation, and every ObjectRef it receives; a client incapable of verification (Thin profile without Ed25519) MUST only use ZCT through a trusted proxy on a network it trusts, and this MUST be a documented deployment decision.
635
- Push over ZCT is permitted only with a reference attestation signed by an authorized key; there is no other authentication. Servers MAY refuse push over ZCT entirely.
636
- ZCT MUST NOT be offered on a public network interface by default.
637
638
### 15.4 Proxy
639
640
A **proxy** is a Full-profile `oxy` server that fronts one or more remotes for Thin clients, performing verification, chunking, delta computation, and signing on their behalf. Its security properties are those of the Thin client's trust in it.
641
642
## 16. Wire protocol
643
644
Messages are deterministic CBOR. Over HTTPS each message is a request body with `Content-Type: application/oxy-cbor` and responses are CBOR sequences (RFC 8742) with `Content-Encoding: zstd` when supported. Over Noise and ZCT, messages are framed as `u32 big-endian length || bytes`.
645
646
### 16.1 Endpoints
647
648
| Endpoint | Request | Response |
649
|---|---|---|
650
| `GET capabilities` | — | `{ versions: [1], transports, features: ["lazy", "checkpoint", "seal", "lock"], max_frame }` |
651
| `GET refs` | `{ patterns }` | `[ { name, state: ObjectRef, heads, attestation: Signed<Attest> } ]` plus the current `Signed<Policy>` |
652
| `POST negotiate` | `{ have: [ChangeHash], want: [ChangeHash], filter }` | stream of `ChangeHash` the client lacks, in dependency order |
653
| `POST fetch` | `{ changes: [ChangeHash], include_blobs: bool, filter }` | stream of `Signed<Change>`, then every `Signed<Attest kind=seal>` covering them, then referenced `blob`/`chunk` objects |
654
| `POST objects` | `{ refs: [ObjectRef] }` | stream of objects |
655
| `GET checkpoint` | `{ heads, filter }` | nearest `Checkpoint` and its objects, and the list of changes to replay |
656
| `POST push` | stream of `Signed<Change>`, objects, seals, then `Signed<Attest kind=ref>` per reference | `{ results: [ { name, ok, reason } ] }` |
657
| `POST lock` | `Signed<Attest kind=lock>` | `{ ok, holder, expires }` |
658
659
### 16.2 Negotiation
660
661
The client sends the heads it has for the requested references and the heads it wants. Because dependencies form a DAG and every state is its heads' closure, the server computes `closure(want) \ closure(have)` by a reverse traversal, stopping at any hash in `have`. For very large histories a server MAY ask the client for the Merkle root of its known-hash set in 2^16-hash-prefix buckets and skip buckets whose roots match. The response is ordered so that no change precedes any of its dependencies.
662
663
### 16.3 Push validation
664
665
On push, the server MUST, in order: verify that every change has a valid signature or seal from a key embedded in policy and satisfies the author-or-delegate rule (§14.4), verify the attestation for each reference, check that `old_state` matches its current state (compare-and-swap), check policy rules for the reference including `allow_conflicts`, check that every dependency of every change is either included or already stored, and only then update references atomically. A failure rejects the whole push.
666
667
## 17. Partial, sparse, and shallow repositories
668
669
A repository records a **filter** in `.oxy/config`:
670
671
```
672
[filter]
673
paths    = ["src/", "docs/"]      # sparse: only these subtrees are materialized and blobs fetched
674
max-blob = "1MiB"                 # blobs larger than this are lazy
675
depth    = 100                    # shallow: history beyond this many changes from heads is not stored
676
```
677
678
- **Sparse** repositories hold the full change graph but only materialize and fetch blobs under the listed paths. Changes touching other paths are stored and applied to the graph; their content is lazy.
679
- **Lazy blobs** are fetched via `objects` on first read. `oxy status` and `oxy commit` MUST NOT need them.
680
- **Shallow** repositories store a *shallow boundary*: a set of ChangeHashes whose dependencies are known to be absent. A shallow repository MUST hold a checkpoint at the boundary so that graphs can be reconstructed. Deps into the boundary are accepted as well-formed. Deepening fetches more history and moves the boundary.
681
- Thin clients (§22) always use `depth = 0`: a checkpoint plus zero history.
682
683
Servers MUST honour `filter` on `negotiate`, `fetch`, and `checkpoint`.
684
685
## 18. Locks for unmergeable files
686
687
Opaque files (models, textures, binary documents) cannot be merged. Paths matching a `lockable` attribute in `.oxyattributes` are read-only in the working copy until locked.
688
689
- `oxy lock <path>` sends a `lock` attestation to the remote configured as `lock-server`. The server grants it if unlocked or expired, and records the holder.
690
- A push that replaces a lockable node's blob MUST be rejected by the server unless the pushing key holds the lock.
691
- Locks expire (default 7 days) and can be broken by a policy root key.
692
- Locks are advisory in repositories with no `lock-server`.
693
694
## 19. Repository configuration files
695
696
| File | Purpose |
697
|---|---|
698
| `.oxyignore` | Ignore rules. Gitignore syntax and semantics, per directory, plus `.oxy/info/ignore` and the global ignore file. |
699
| `.oxyattributes` | Per-path attributes: `binary`, `eol=lf|crlf`, `nocompress`, `lockable`, `filter=<name>`. Gitattributes syntax. |
700
| `.oxy/config` | Repository-local settings, TOML. |
701
| `~/.config/oxy/config.toml` | User settings: identity key path, aliases, default profile. |
702
703
Filters (`filter=<name>`) are clean/smudge programs configured in user or repo config, as in Git. Implementations MUST refuse to run filters defined in `.oxy/config` of a freshly cloned repository until the user approves them.
704
705
## 20. Hooks and server policy
706
707
Client hooks live in `.oxy/hooks/` and are never cloned. Events: `pre-commit`, `semantic-deps`, `post-commit`, `pre-push`, `post-fetch`, `post-checkout`. A hook receives its event as JSON on stdin and blocks the operation by non-zero exit for `pre-*` events.
708
709
The `semantic-deps` hook runs during `oxy commit` after the working copy is snapshotted and before the change is finalized. It receives `{ change_id, touched_paths, candidate_deps: [ { change_id, hash, paths } ] }`, where `candidate_deps` are the changes in the working-copy state that touch any file the new change touches or that it references by symbol if the hook has such information, and it returns `{ deps: [ChangeHash] }` to add to `deps(C)`. It runs only when `hooks.semantic-deps = true` is set in `.oxy/config`, which is never set by clone.
710
711
Server policy is the trust policy (§14.5) plus server configuration for per-reference rules: protected patterns that additionally require CI status, forbid conflicted heads, forbid multiple heads, or forbid force (non-fast-forward) updates. A force update is a `ref` attestation whose `old_state` heads are not all in the closure of `new_state`.
712
713
## 21. Garbage collection
714
715
An object is **reachable** if it is referenced, directly or transitively, from any bookmark, tag, policy, the current operation heads, any operation newer than `core.op-retention`, any workspace's working-copy change, or any checkpoint of a reachable state. `oxy gc` deletes unreachable objects that are older than `core.gc-grace` (default 14 days, to protect concurrent writers) and repacks. Tombstone atoms are never collected independently of their change. Divergent changes are reachable if any of their variants is. A seal is reachable if any change it covers is reachable, and reference attestations are reachable while they are the latest for their reference or are within `core.op-retention`.
716
717
## 22. Client profiles and retro hardware
718
719
| | Full | Lite | Thin |
720
|---|---|---|---|
721
| Minimum hardware | 1 GiB RAM, current desktop or server | 16 MiB RAM (486, 68030, ARMv5, RISC-V RV32) | 68000 @ 8 MHz, 1 MiB RAM, block storage (Amiga 500+, Atari ST, Macintosh Plus/SE, early 386 DOS) |
722
| Local history | Full or partial | Partial (shallow + lazy) | None: one checkpoint |
723
| Change graph operations | Yes | Yes | No |
724
| BLAKE3 verification | Yes | Yes | Yes (chunks and objects received; ~150 KiB/s on 68000) |
725
| Ed25519 verification | Yes | Yes | MAY (~2 s per signature on 68000; used for attestations only) |
726
| ML-DSA-65 | Sign + verify | Verify; sign MAY | No |
727
| Chunking, delta computation, signing | Local | Local | Proxy |
728
| Transports | HTTPS, Noise, ZCT | HTTPS, Noise, ZCT | ZCT to proxy; serial or TCP |
729
| Working copy | Full | Sparse | Sparse subtree, whole-file edits |
730
731
A Thin client fetches a checkpoint for a sparse subtree, verifies each object's ObjectRef, and writes files. To commit, it sends the full content of modified files with their previous blob refs to the proxy; the proxy computes edits, assigns ChangeIds, signs or seals with its own key, which the user has listed in `delegates` on their identity (§14.2), and pushes. The threat model for Thin clients is therefore "the proxy is trusted", and `oxy` documentation MUST state this without qualification.
732
733
Machines under 512 KiB of RAM are out of scope; a serial terminal to a Lite or Full machine is the recommended path.
734
735
## 23. Git interoperability
736
737
`oxy git import <repo>` and `oxy git export <repo>` provide a lossless-enough bridge; `oxy git fetch/push` operate against a Git remote directly.
738
739
- **Import.** Each Git commit becomes one change whose edits are the diff against its first parent; merge commits become a change whose edits are the diff against the merged graph state. Atom identities are assigned by first appearance and cached in `.oxy/git/` so re-import is stable. Renames are detected heuristically (Git similarity ≥ 50%) on import only, then tracked exactly. The Git commit's `Change-Id` trailer, if present and 128 bits, becomes the ChangeId; otherwise one is derived as BLAKE3-128 of the Git commit hash, so re-import is idempotent.
740
- **Export.** Each state along a bookmark becomes a Git commit whose tree is the materialized state. The ChangeId is written as a `Change-Id:` trailer and the ChangeHash as `Oxy-Hash:`. Conflicted states are exported with §9.5 markers and an `Oxy-Conflicted: true` trailer. Signatures are not exported; Git commits MAY be additionally signed with the Ed25519 key via SSH signature format.
741
- **Round trip.** Import followed by export MUST reproduce every tree hash. Export followed by import MUST reproduce every ChangeId.
742
743
## 24. Command-line interface
744
745
Commands mirror Jujutsu where semantics match, so that muscle memory transfers.
746
747
```
748
oxy init [--no-policy]              oxy clone <url> [--sparse PATH..] [--depth N] [--max-blob SIZE]
749
oxy status [--json]                 oxy diff [-r CHANGE] [PATH..]
750
oxy commit [-m MSG] [--depends-on CHANGE].. [--sign]
751
                                    oxy new [PARENT..]           oxy edit CHANGE
752
oxy split                           oxy squash                   oxy abandon CHANGE
753
oxy rebase -s CHANGE -d DEST        oxy resolve [PATH..]
754
oxy log [-r REVSET]                 oxy show CHANGE
755
oxy bookmark create|set|delete|list oxy tag create NAME -r CHANGE
756
oxy fetch [REMOTE]                  oxy push [REMOTE] [-b BOOKMARK]
757
oxy undo                            oxy redo                     oxy op log
758
oxy lock PATH                       oxy unlock PATH
759
oxy identity create|rotate|revoke   oxy policy edit|sign|show
760
oxy remote add|trust|list           oxy gc
761
oxy git import|export|fetch|push
762
```
763
764
Change arguments accept a ChangeId prefix, a ChangeHash prefix (with `#`), a bookmark, `@` for the working-copy change, and `@-` for its parent. Ambiguous prefixes are an error.
765
766
## 25. Performance targets
767
768
Measured on the Full profile, warm cache, NVMe storage, 8-core x86-64, with a 20 GiB repository of 2 million files and 500 000 changes.
769
770
| Operation | Target |
771
|---|---|
772
| `oxy status` (no changes, fsmonitor) | < 50 ms |
773
| `oxy status` (no changes, stat crawl) | < 2 s |
774
| `oxy commit` touching 10 files | < 100 ms |
775
| `oxy log -r main` first page | < 100 ms |
776
| Checkout between adjacent checkpoints | proportional to changed bytes, ≥ 500 MiB/s |
777
| Full clone over 1 Gbit/s | line rate; chunk verification ≥ 1 GiB/s per core |
778
| Sparse clone of a 50 MiB subtree | < 5 s |
779
| Rebase of 100 commuting changes | < 200 ms, zero object rewrites |
780
| Resident memory, `status` | < 200 MiB |
781
782
Implementations SHOULD ship a benchmark harness that reproduces this table.
783
784
## 26. Conformance and test vectors
785
786
A conforming implementation MUST pass the `oxy-conformance` suite, which includes:
787
788
- Encoding vectors: canonical CBOR and ObjectRef for every object type.
789
- Signature vectors: composite signatures, including stripped-half and wrong-KeyId negatives.
790
- Graph invariance: for random change sets, all permutations of application yield identical graphs and checkpoints.
791
- Conflict shapes: each shape in §9 with expected materialization and expected `status --json`.
792
- Rewrite propagation: amend of a change with dependents, with and without correspondence.
793
- Protocol: negotiate/fetch/push against a reference server, including rollback and unauthorized-attestation rejection.
794
- Adversarial chunking: inputs with no gear boundaries and inputs with boundaries at every byte.
795
- Git round trip on a corpus of public repositories.
796
797
## 27. Open questions
798
799
1. **Encryption at rest.** Per-repository XChaCha20-Poly1305 with a key hierarchy is reserved for format version 2; object headers already leave room. Convergent encryption is rejected because it leaks equality.
800
2. **Word- or character-granularity atoms** for prose and minified files. Line atoms are fixed for v1.
801
3. **Set reconciliation** at the scale of tens of millions of changes may need an IBLT-based negotiator; the Merkle bucket scheme in §16.2 is the v1 answer.
802
803
---
804
805
*Changelog.* Draft 0.5 (2026-09-26): bookmarks and tags point at a `state` object naming the heads, so attestations, views, and references carry one ObjectRef per bookmark however many heads it has; repository format 2 records this, and format-1 repositories migrate on open.
806
807
Draft 0.4b (2026-09-26): §5.5 records the encoding conventions for variants, references, views, signatures, and secret key files; §13.1 adds the signature directory.
808
809
Draft 0.4a (2026-09-26): §13.2 pins the FastCDC gear table, rolling hash, and masks so boundaries are identical across implementations.
810
811
Draft 0.4 (2026-09-26): overview no longer describes the constrained profile by word size; HLC drift bound replaces timestamp-based rejection; node kinds reduced to file, dir, symlink with a separate content mode and defined mode switching; symlinks always opaque; checkout safety rules (§7.7); workspace conflicts (§9.7) replace the case-folding tree conflict and cover normalization, reserved names, and path length; operation log entries suppressed for empty snapshots.
812
813
Draft 0.3 (2026-09-26): recursive directory moves with cycle handling; explicit semantic dependencies with the `semantic-deps` hook; seals redefined as push-time batch signatures carrying their hash list, with an author-or-delegate authentication rule and `delegates` on identities; `core.sign` setting; fetch, push, policy, and garbage collection updated for seals.
814
815
Draft 0.2 (2026-09-26): rewrote from the 0.1 outline (the original `spec.txt`, since removed). Added threat model, two-identifier model, atom coordinate system, tree operations as atoms, conflict taxonomy and materialization, operation log, checkpoints, storage layout, composite signature construction, trust policy, reference attestations, HTTPS-mandatory transport, ZCT restrictions, wire protocol, partial repositories, locks, client profiles with a named hardware floor, Git bridge, CLI, performance targets, and conformance suite.