OXYGENHUB

Docs: user guide by task (docs/guide.md) and wire protocol for implementers (docs/protocol.md); README links ee06a6c557b7

Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-27 00:16 UTC
Signature
✔ signed by Andy McCall
Hash
ee06a6c557b7207430d0e0ddb398c8f4bfbbefc691cce83d4ba98d4fece2f58d
Depends on
648724d8e0de 96f7610fa666 b7f102fcbb17 d02e545dc265 ff0af1ee299d
Depended on by
91334b7fa574 642fa213de80 2452b4407ab7 4afe288b7946 9bd98cfb4f7e 157d02e95586 b63fc7a77fc6 ee130dd9eef2 1ec6fdbc6bcb 67a4fe1aa2d5 ad2496f7e608 51d4862a16d8 e59512f2f075 0ed150d62994 c895bc52e4e0 f53cbe7a204e 5f661562b3a4 a39754d02c58 68a924846379 b9bf7eec1f2d f6dccc4533c8 42ccf504aa50 54e2e6309aa3 4afab467e35f 72484195899a bf3a67819b59 b0a0c9afae2d 7993d465675b da58bbbbc4d9 614f55e14eee 445ce913ba56 ff1a64f81c1c 47ce8b8e2d4b 7ba155f20aa0 8e1194860efa 33472ccc08cc 98a1620a3840 2bf22305bc48 14edbaaf5291 ff1f62fee654 d4dbab97e948 e266b7d4065c 06d16dc2bc13 da652d6392d8 d443639bcf35 4fe7d6045860 314db9b4f3f8 fc1a466a1e66 0bdc3f91a39f d42369abf180 3e5772ccf9da 387335ff7f4d 75167672ebcd e02bf0cd0533 c6aa5bd9d1c8 728da0f92161 957fee30826e 4dee277c327a fbda43e7d8ed 9ac1fbe211b4 50a046e439e9
In bookmarks
agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
In tags
v0.1.0 v0.10.0 v0.11.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
Edits
8 atoms, 2 tree operations, 4 files

Browse the tree at this change

added docs/guide.md +342 −0
1 +
# Oxygen user guide
2 +
3 +
This guide is organised by task. It assumes the `oxy` binary is on your
4 +
path (`cargo build --release` puts it in `target/release/`). The design
5 +
behind every command is in [spec.md](spec.md); this guide only tells you
6 +
what to type and what to expect.
7 +
8 +
Oxygen tracks *changes* rather than snapshots: a change is the set of
9 +
edits you made, it commutes with unrelated changes, and conflicts are
10 +
recorded in history instead of blocking you. Two ideas recur below:
11 +
12 +
- A **bookmark** (default `main`) names a *state*: the set of changes it
13 +
  contains. A state can have several **heads** when independent changes
14 +
  have not been related to each other; that is normal, not a problem.
15 +
- The **working copy** is itself a change that Oxygen rewrites every time
16 +
  it looks at your files. `oxy commit` turns it into a real change.
17 +
18 +
Output uses colour and glyphs on a terminal; pipe it, pass `--plain`, or set
19 +
`NO_COLOR` for plain ASCII.
20 +
21 +
## 1. Set yourself up
22 +
23 +
```
24 +
oxy identity create --name "Ada Lovelace" --email [email protected]
25 +
oxy config --user core.sign change
26 +
```
27 +
28 +
`identity create` generates a composite Ed25519 + ML-DSA-65 key pair, keeps
29 +
the secret in `~/.config/oxy/keys/`, and records you as the default author
30 +
in `~/.config/oxy/config.toml`. With `core.sign = change` every commit is
31 +
signed; without it, changes are sealed in batches when you push (also
32 +
authenticated, just later). Use `--local` to record the identity in one
33 +
repository only.
34 +
35 +
`oxy identity show` prints the key, its expiry, delegates, and whether the
36 +
secret is available. `oxy identity export` writes the public identity to a
37 +
file to hand to whoever manages a trust policy (section 8).
38 +
39 +
## 2. Start a repository
40 +
41 +
```
42 +
mkdir project && cd project
43 +
oxy init
44 +
oxy status
45 +
```
46 +
47 +
`oxy init` creates `.oxy/`. Add `.oxyignore` (gitignore syntax) for files
48 +
that should never be tracked; `/target` and `*.tmp` are typical entries.
49 +
50 +
To take over an existing Git project, see section 12.
51 +
52 +
## 3. Everyday work
53 +
54 +
```
55 +
oxy status                 # what changed since the last commit
56 +
oxy diff                   # the edits, file by file
57 +
oxy commit -m "Add retry loop"
58 +
oxy log                    # newest first; -n 10, -v for full messages
59 +
oxy show 658990aa          # one change by ChangeId prefix, #hash prefix, or @
60 +
```
61 +
62 +
`status` lists each path with a mark: `✚ added`, `✎ modified`, `✖ removed`,
63 +
`➜ renamed`, `⚙ mode`. Renames are detected: a removed file and an added
64 +
file of the same kind whose contents are at least half alike are recorded
65 +
as a move of the same file, so identity survives a rename.
66 +
67 +
Files containing NUL bytes are stored whole ("opaque"); text files are
68 +
stored line by line and merge line by line. `.oxyattributes` overrides the
69 +
guess (section 14).
70 +
71 +
Every change has two names: a **ChangeId** (`658990aa…`) that survives
72 +
rewrites, and a content hash (`#7469d16c…`) that changes when the change is
73 +
edited. Use whichever you have; prefixes work.
74 +
75 +
## 4. Fix up history
76 +
77 +
Oxygen keeps its own record of every operation, so all of these are safe to
78 +
try and to undo.
79 +
80 +
```
81 +
oxy describe -m "Better message"        # reword the head the working copy sits on
82 +
oxy describe 658990aa -m "..."          # reword any change; dependents are rewritten
83 +
oxy amend                               # fold the working copy into the head
84 +
oxy amend --into 658990aa               # fold it into an older change
85 +
oxy split 658990aa src/parser.rs        # split a head: those paths become a separate first change
86 +
oxy abandon 658990aa                    # drop a change; its content stays on disk
87 +
oxy undo                                # undo the last operation, whatever it was
88 +
oxy op log                              # the operation log
89 +
```
90 +
91 +
Rewriting a change that others depend on rewrites them too, keeping their
92 +
ChangeIds. Where a dependent referred to a line that no longer exists, the
93 +
reference is re-anchored to the nearest surviving line and `oxy abandon`
94 +
prints a note about it.
95 +
96 +
## 5. Move around
97 +
98 +
```
99 +
oxy checkout                    # the bookmark's heads
100 +
oxy checkout 658990aa           # the state of one change and its dependencies
101 +
oxy checkout v1.2               # a tag
102 +
oxy checkout --discard ...      # throw away uncommitted work first
103 +
```
104 +
105 +
Checking out a single older change gives you the files that change and its
106 +
dependencies touch, not a full snapshot from that date: in Oxygen a change
107 +
depends only on what it actually builds on.
108 +
109 +
## 6. Conflicts
110 +
111 +
A conflict never stops a commit, fetch, or pull. It is recorded in the
112 +
state and shown by `oxy status`, which exits with status 2 while any
113 +
conflict remains. Conflicted text files are written with markers:
114 +
115 +
```
116 +
<<<<<<< oxy conflict 1 of 2, side 1: change k7m2p9 "Add retry loop"
117 +
... lines ...
118 +
======= oxy conflict 1 of 2, side 2: change q3x8w1 "Refactor client"
119 +
... lines ...
120 +
>>>>>>> oxy conflict 1 of 2 end
121 +
```
122 +
123 +
Edit the file to what it should be (keep one side, both, or neither) and
124 +
remove the markers. Tree conflicts, such as two files wanting the same
125 +
name, appear as `<name>.oxy-conflict-<n>` candidates: keep the one you
126 +
want under the right name and delete the rest.
127 +
128 +
```
129 +
oxy resolve      # which conflicts your edits resolve, which still have markers
130 +
oxy commit -m "Resolve retry vs refactor"
131 +
```
132 +
133 +
The commit is recorded as a *resolution*, a change that depends on every
134 +
side of the conflict; anyone who fetches it gets the conflict resolved.
135 +
136 +
## 7. Work with others
137 +
138 +
Run a server (any machine with the repositories under one directory):
139 +
140 +
```
141 +
oxy serve --root /srv/oxy --bind 0.0.0.0:8890 --token SECRET \
142 +
          --tls-cert cert.pem --tls-key key.pem
143 +
```
144 +
145 +
Without `--tls-*` the server speaks plain HTTP; keep that on a loopback
146 +
address or behind a TLS terminator. `--token` makes pushes require a bearer
147 +
token; reads stay open.
148 +
149 +
On your machine:
150 +
151 +
```
152 +
oxy clone https://host:8890/project
153 +
cd project
154 +
oxy config remote.origin.token SECRET   # or export OXY_TOKEN
155 +
oxy config remote.origin.ca my-ca.pem   # self-signed server: extra roots to trust (or OXY_CA)
156 +
oxy push
157 +
oxy pull
158 +
```
159 +
160 +
- `oxy fetch` updates `origin/main` without touching your bookmark.
161 +
- `oxy pull` fetches, then makes your bookmark the union of both sides.
162 +
  There is no merge commit; unrelated changes simply coexist, and a real
163 +
  conflict is recorded as in section 6. The working copy follows when it was
164 +
  clean.
165 +
- `oxy push` sends your changes and moves the remote bookmark. If the remote
166 +
  moved since you last fetched, the push is refused: pull and push again.
167 +
  `--force` overrides the compare-and-swap.
168 +
- `oxy remote add|remove|list` manages remotes; the clone source is `origin`.
169 +
170 +
A repository can also be cloned from a local path, which is handy for tests
171 +
and for a bare "hub" on a shared disk.
172 +
173 +
## 8. Decide who may publish: the trust policy
174 +
175 +
Without a policy a repository is *unverified*: everything is accepted and
176 +
every remote command says so. A policy names root keys and the keys allowed
177 +
to update each bookmark; servers and clients enforce it.
178 +
179 +
```
180 +
oxy policy init                    # your key becomes the only root; push publishes it
181 +
oxy policy show
182 +
oxy policy grant alice.identity    # alice.identity came from her `oxy identity export`
183 +
oxy policy grant ci.identity --pattern 'release/*'
184 +
oxy policy revoke 9458b656         # by KeyId prefix
185 +
oxy verify                         # signature status of every change in the log
186 +
```
187 +
188 +
Several people can hold root keys:
189 +
190 +
```
191 +
oxy policy root add bob.identity
192 +
oxy policy root threshold 2        # future updates need two root signatures
193 +
```
194 +
195 +
Once the threshold is above one, an update you make alone becomes a
196 +
*proposal file* (`policy-<serial>.proposal`, or `--out`). Send it to another
197 +
root holder, who runs `oxy policy sign policy-7.proposal`; whoever completes
198 +
the threshold applies it, and the next push publishes it. Everyone else
199 +
picks it up on fetch, verifying each step from the policy they already
200 +
trust.
201 +
202 +
## 9. Keys over time
203 +
204 +
```
205 +
oxy identity rotate                   # fresh key pair; old signatures stay valid
206 +
oxy identity renew --expires 365      # same keys, new expiry in days (0 = never)
207 +
oxy identity delegate add ci.identity # let another key sign on your behalf
208 +
oxy identity delegate remove 3f2a…
209 +
oxy identity revoke                   # retire your own key
210 +
oxy identity revoke 3f2a… --reason "laptop stolen"   # any key, if you hold a root
211 +
```
212 +
213 +
Rotation, renewal, and delegation all issue a successor identity that names
214 +
the previous one. If a policy names your key, it is updated to the successor
215 +
in the same step (as a proposal when more root signatures are needed).
216 +
Changes signed before the rotation remain authorised; a revoked key
217 +
authorises nothing. `oxy verify` shows signatures by revoked keys as valid
218 +
but revoked.
219 +
220 +
## 10. Tags and workspaces
221 +
222 +
```
223 +
oxy tag create v1.2 -m "First release"   # the current heads; or name changes
224 +
oxy tag list
225 +
oxy workspace add ../project-hotfix      # a second checkout sharing the repository
226 +
oxy workspace list
227 +
oxy workspace forget ../project-hotfix
228 +
```
229 +
230 +
A tag is an immutable state, signed when you have a key, and travels with
231 +
push and fetch. Workspaces have their own working copy and bookmark
232 +
(`workspace.bookmark`) but share objects and history.
233 +
234 +
## 11. Big repositories: partial, sparse, shallow
235 +
236 +
```
237 +
oxy clone URL --max-blob 1M          # files above 1 MiB are fetched on first read
238 +
oxy clone URL --sparse src --sparse docs   # materialise only these subtrees
239 +
oxy clone URL --depth 50             # only the last 50 levels of history
240 +
oxy sparse src docs tests            # change the filter later
241 +
oxy deepen 100                       # fetch more history below the boundary
242 +
```
243 +
244 +
Lazy blobs and sparse trees do not affect commits: what you do not have is
245 +
left exactly as it was. A shallow clone starts from a *checkpoint* of the
246 +
state, so status and commit work at full speed from the first minute.
247 +
248 +
## 12. Git interoperability
249 +
250 +
```
251 +
oxy git import ../project.git ./project-oxy --branch main   # convert a Git history
252 +
oxy git fetch https://github.com/you/project.git            # bring new Git commits in
253 +
oxy git push  https://github.com/you/project.git            # publish your changes to Git
254 +
oxy git export ../mirror.git                                # a plain Git mirror of the bookmark
255 +
```
256 +
257 +
Import turns each commit into one change (merge commits become
258 +
resolutions), keeps authors and hashes as `Git-Author`/`Git-Commit`
259 +
trailers, detects renames, and skips submodules. `git fetch`/`git push` keep
260 +
a bare mirror under `.oxy/git/`; a push is refused until the remote's tip
261 +
has been fetched, so it always fast-forwards. Exported commits carry
262 +
`Change-Id` and `Oxy-Hash` trailers.
263 +
264 +
## 13. Automate: hooks
265 +
266 +
Executables in `.oxy/hooks/<event>` (never cloned) receive one JSON object
267 +
on stdin. `pre-commit`, `pre-push` and other `pre-*` hooks veto by exiting
268 +
non-zero, and their stderr becomes the error message. Events:
269 +
`pre-commit`, `post-commit`, `pre-push`, `post-fetch`, `post-checkout`, and
270 +
`semantic-deps`, which runs at commit when `hooks.semantic-deps = true` and
271 +
answers `{"deps": ["<hash>", …]}` to make the change depend on more than it
272 +
textually references.
273 +
274 +
## 14. Per-path attributes, ignores, locks
275 +
276 +
`.oxyattributes` uses gitattributes syntax:
277 +
278 +
```
279 +
*.png     binary
280 +
*.txt     text eol=lf
281 +
*.psd     lockable nocompress
282 +
```
283 +
284 +
`binary`/`text` override content sniffing, `eol=lf|crlf` normalises line
285 +
endings in the store and on checkout, `nocompress` stores chunks raw, and
286 +
`lockable` files are checked out read-only until you take the local lock:
287 +
288 +
```
289 +
oxy lock assets/logo.psd
290 +
oxy unlock assets/logo.psd
291 +
```
292 +
293 +
Locks are advisory and local; server-side locks are planned.
294 +
295 +
## 15. Configuration
296 +
297 +
Two files, both `key = "value"` lines: `~/.config/oxy/config.toml`
298 +
(`oxy config --user KEY VALUE`) and `.oxy/config` (`oxy config KEY VALUE`).
299 +
The repository file wins. `OXY_HOME` relocates the user directory.
300 +
301 +
| Key | Meaning |
302 +
|---|---|
303 +
| `author.name`, `author.email`, `author.key` | who you are; `key` is set by `identity create` |
304 +
| `core.sign` | `change` to sign every commit; otherwise seals at push |
305 +
| `core.op-retention` | days for which old operations keep their states alive (90) |
306 +
| `core.gc-grace` | days an unreachable object survives (14) |
307 +
| `core.checkpoint-interval` | changes between automatic checkpoints (256) |
308 +
| `filter.max-blob`, `filter.paths` | lazy blobs and sparse subtrees (section 11) |
309 +
| `hooks.semantic-deps` | run the semantic-deps hook at commit |
310 +
| `workspace.bookmark` | this workspace's bookmark (`main`) |
311 +
| `remote.<name>.url`, `.token`, `.ca` | remotes; token and CA fall back to `OXY_TOKEN` and `OXY_CA` |
312 +
313 +
## 16. Shell prompt
314 +
315 +
`oxy prompt` prints `main 658990aa*+2` (bookmark, working copy, `*` dirty,
316 +
`!` conflicted, ahead/behind origin) in a few milliseconds and records
317 +
nothing. See [prompt.md](prompt.md) for bash, oh-my-bash, zsh, and fish
318 +
snippets and the `--format` placeholders.
319 +
320 +
## 17. Housekeeping
321 +
322 +
```
323 +
oxy gc              # pack loose objects, prune unreachable ones past the grace period
324 +
oxy gc --now        # ignore the grace period (only when nothing else is writing)
325 +
oxy checkpoint      # serialise the bookmark's state so large histories load instantly
326 +
```
327 +
328 +
Both run automatically: packing when loose objects pile up, checkpoints
329 +
every `core.checkpoint-interval` changes.
330 +
331 +
## 18. If something looks wrong
332 +
333 +
- **"the remote has change … which you do not; pull first"**: someone
334 +
  pushed since your last fetch. `oxy pull`, then push.
335 +
- **"policy needs N root signature(s)"** on fetch: the server's policy was
336 +
  updated in a way your copy cannot verify. Ask a root holder whether the
337 +
  chain of updates was published; a fresh clone trusts the current policy.
338 +
- **`oxy status` exits 2**: conflicts remain; see section 6.
339 +
- **Many heads on `main`**: independent changes that do not reference one
340 +
  another. Nothing to fix; the state is their union.
341 +
- **Undo anything**: `oxy undo` reverts the last operation, including
342 +
  pulls, checkouts, and rewrites.
added docs/protocol.md +181 −0
1 +
# Oxygen wire protocol for implementers
2 +
3 +
This document describes what a second implementation needs to talk to an
4 +
Oxygen server or client: the transport, the message encodings, the order
5 +
of requests, and the checks each side performs. It is derived from
6 +
[spec.md](spec.md) §5.5, §14, §16 and §17; where the two disagree the
7 +
specification wins, and this document says which section to read.
8 +
9 +
Everything on the wire is deterministic CBOR (§5.1): maps have unsigned
10 +
integer keys in increasing order, integers use the shortest form, no
11 +
floats, no indefinite lengths, no tags. Objects travel as their canonical
12 +
bytes, so the receiver hashes exactly what the sender signed.
13 +
14 +
## 1. Identifiers and hashing
15 +
16 +
- **ObjectRef** (32 bytes) = BLAKE3 of `"oxy1 <type> <len>\0" || body`, where
17 +
  `<type>` is one of `chunk blob change checkpoint op attest identity policy
18 +
  state` and `<len>` is the decimal body length. A ChangeHash is the
19 +
  ObjectRef of a change; a KeyId is the ObjectRef of an identity.
20 +
- **ChangeId** (16 random bytes) survives rewrites of a change.
21 +
- **Repository id** (16 random bytes) is chosen at `init` and returned by
22 +
  `capabilities`; a client refuses to push to a repository whose id differs
23 +
  from its own.
24 +
25 +
Object encodings are in spec §5.5 (conventions), §6.1 (change), §11
26 +
(checkpoint), §12.1 (op), §14.2 (identity), §14.4 (attestation), §14.5
27 +
(policy) and §10.1 (state).
28 +
29 +
## 2. Transport
30 +
31 +
The reference server (`oxy serve`) exposes one repository per subdirectory
32 +
of its root at `<base>/<repo>/oxy/v1/<endpoint>`:
33 +
34 +
| Method and path | Body | Response |
35 +
|---|---|---|
36 +
| `GET  …/capabilities` | — | capabilities |
37 +
| `GET  …/refs` | — | refs response |
38 +
| `POST …/negotiate` | negotiate request | `{0: [hashes]}` |
39 +
| `POST …/fetch` | fetch request | Bundle |
40 +
| `POST …/objects` | objects request | Bundle |
41 +
| `POST …/checkpoint` | checkpoint request | Bundle |
42 +
| `POST …/shallow` | shallow request | `{0: [hashes]}` |
43 +
| `POST …/push` | push request | push response |
44 +
45 +
Rules:
46 +
47 +
- Request and response bodies are CBOR with `Content-Type:
48 +
  application/oxy-cbor`. A body is a single CBOR item (not a sequence).
49 +
- Repository names are `[A-Za-z0-9._-]+` and may not start with `.`; anything
50 +
  else, and any directory without `.oxy/`, is `404`.
51 +
- Reads are unauthenticated. `push` requires `Authorization: Bearer <token>`
52 +
  when the server was started with tokens; a missing or wrong token is `401`.
53 +
- Errors are plain-text bodies with a 4xx or 5xx status; the client shows the
54 +
  text. A push that is *refused* (policy, compare-and-swap) is a `200` with
55 +
  `ok = false` in the push response, not an HTTP error.
56 +
- TLS is ordinary HTTPS. The reference client trusts the system web roots
57 +
  plus an optional PEM file (`remote.<name>.ca` or `OXY_CA`); servers with
58 +
  self-signed certificates are expected to hand that file out.
59 +
- Pushes to one repository are serialised by the server; reads are not.
60 +
61 +
The spec (§16) also defines framing for Noise and ZCT transports (`u32
62 +
big-endian length || bytes`); the reference implementation speaks HTTP only.
63 +
64 +
## 3. Messages
65 +
66 +
Keys are CBOR unsigned integers. `[x]` is an array of x; `bytes(32)` an
67 +
ObjectRef; "signed X" is `[X bytes, [Signature]]`.
68 +
69 +
| Message | Encoding |
70 +
|---|---|
71 +
| Signature | `{0: key id bytes(32), 1: alg (1 = Ed25519 + ML-DSA-65), 2: Ed25519 bytes(64), 3: ML-DSA-65 bytes(3309)}` |
72 +
| Signed attestation | `[attest bytes, [Signature]]` |
73 +
| Signed policy | `[policy bytes, [Signature]]` (also the policy proposal file format) |
74 +
| Capabilities | `{0: [protocol versions], 1: repo id bytes(16), 2: [feature names]}`; version `1`; features currently `["seal"]` |
75 +
| Refs response | `{0: [ref], 1: repo id, 2: signed policy or null, 3: [tag], 4: [signed policy]}` |
76 +
| ref | `{0: name, 1: state ref, 2: [heads], 3: signed attestation or null, 4: last serial or null}`; the attestation is present only when it describes the current state; `last serial` is the highest attestation serial ever recorded for the name |
77 +
| tag | `{0: name, 1: state ref, 2: [heads], 3: signed attestation or null}` |
78 +
| Negotiate request | `{0: [have], 1: [want]}`; response `{0: [hashes]}` in dependency order (no change before its dependencies) |
79 +
| Fetch request | `{0: [hashes], 1: max blob size or null}`; response a Bundle whose blobs above the size (and their chunks) are omitted |
80 +
| Objects request | `{0: [refs]}`; response a Bundle carrying only objects; a blob is accompanied by its chunks |
81 +
| Checkpoint request | `{0: [heads]}`; response a Bundle of objects: the checkpoint of exactly those heads, its payload blob, and the blob's chunks; the server computes one when it has none for those heads |
82 +
| Shallow request | `{0: [heads], 1: depth}`; response `{0: [hashes]}`: the changes within `depth` dependency steps of the heads, dependencies first |
83 +
| Bundle | `{0: [[change bytes, [Signature]]], 1: [signed attestation], 2: [[type name, object bytes]], 3: [signed policy]}` |
84 +
| Push request | `{0: Bundle, 1: [update], 2: signed policy or null, 3: [tag]}` |
85 +
| update | `{0: name, 1: old state ref, 2: new state ref, 3: force (bool), 4: signed attestation or null}` |
86 +
| Push response | `{0: [{0: name, 1: ok (bool), 2: reason (text)}]}`; a result named `policy` reports the policy update, `tags/<name>` each tag |
87 +
88 +
Bundle details:
89 +
90 +
- Changes are in dependency order, each with its individual signatures
91 +
  (possibly none).
92 +
- Key 1 carries seals (`kind = seal`) covering the changes.
93 +
- Key 2 objects are only `chunk`, `blob`, `identity`, `state`, `checkpoint`;
94 +
  a receiver rejects any other type.
95 +
- Key 3 (optional) carries the policies between the receiver's serial and
96 +
  the one in the push request, ascending, so the receiver can verify each
97 +
  step (§14.5). The refs response carries the same history in its key 4.
98 +
99 +
Sizes: a seal lists at most 4096 hashes, sorted and unique. Chunks are
100 +
32–256 KiB (FastCDC, average 64 KiB, gear table from the BLAKE3 XOF of
101 +
`"oxy1 fastcdc gear"`, spec §13.2); a receiver re-hashes every object.
102 +
103 +
## 4. Sequences
104 +
105 +
### 4.1 Clone or fetch
106 +
107 +
1. `capabilities`: check protocol version 1 and record the repository id.
108 +
2. `refs`: for each wanted bookmark note `state`, `heads`, `attestation`;
109 +
   take the policy and history.
110 +
3. Policy (spec §14.5): with no local policy, accept the remote's if it is
111 +
   for this repository and carries a valid signature by one of its own roots
112 +
   (trust on first use). With a local policy, refuse a lower serial or a
113 +
   different policy at the same serial; otherwise verify each policy in the
114 +
   chain against the one before it (signatures by the previous roots meeting
115 +
   the previous threshold, serial strictly increasing) and adopt them in
116 +
   order. Record the highest serial this remote has ever presented and
117 +
   refuse a remote that later presents none or a lower one.
118 +
4. `negotiate` with `have` = local heads of every bookmark, `want` = the
119 +
   remote heads. The response lists the missing changes.
120 +
5. `fetch` those hashes (with `max blob size` for a partial clone). Store
121 +
   objects, changes and seals; verify every change's signatures and seals;
122 +
   under a policy, authenticate each new change (§14.4: signer is the author
123 +
   or a delegate of the author) and check it is authorised for the bookmark
124 +
   by the first matching rule (a rule key covers its rotation predecessors,
125 +
   and a delegate's signature counts for the author).
126 +
6. Verify the bookmark's attestation (`kind = ref`: repo id, name, old
127 +
   state, new state, serial) against the policy and against the serial you
128 +
   last recorded for that reference (rollback protection), then update
129 +
   `remotes/<name>/<bookmark>`.
130 +
7. Tags: adopt those not yet held, checking their attestations the same way.
131 +
132 +
A shallow clone replaces steps 4–5 by `shallow` (the hashes within the
133 +
depth) plus `checkpoint` for the heads, and fetches only the listed changes;
134 +
their missing dependencies form the boundary. Lazy blobs are fetched later
135 +
with `objects`.
136 +
137 +
### 4.2 Push
138 +
139 +
1. `capabilities` and `refs` as above; refuse if the remote's policy is
140 +
   newer than yours (fetch first).
141 +
2. Compute the changes the remote lacks (`negotiate` with the roles
142 +
   reversed, or locally from the remote heads), bundle them in dependency
143 +
   order with their signatures, seal your unsigned changes with one `seal`
144 +
   attestation, and attach identities, blobs, chunks and the new state
145 +
   object.
146 +
3. Attest the reference: `kind = ref` with `serial` = the remote's `last
147 +
   serial + 1`, signed by your key.
148 +
4. Send `push` with `old state` = the state the remote reported (empty
149 +
   state ref when the bookmark is new), the new state, and your policy with
150 +
   its intermediate history when the remote's serial is behind.
151 +
152 +
### 4.3 What a server checks on push
153 +
154 +
In this order, spec §16.4: adopt the policy chain if present (each step
155 +
verified against the previous policy); import the bundle, verifying every
156 +
object's hash and, under a policy, every change's authentication and
157 +
authorisation; verify the reference attestation (signer authorised for the
158 +
name, serial above the last recorded); check `old state` equals the current
159 +
state (compare-and-swap; `force` skips this check only); check the rule's
160 +
`allow_conflicts` and `require` (signed, sealed, any); check every
161 +
dependency of every change is present; then update the references
162 +
atomically and record the attestation. Any failure rejects the whole push
163 +
with `ok = false` and a reason.
164 +
165 +
## 5. Minimal implementations
166 +
167 +
A **read-only client** needs: CBOR decode, BLAKE3, the change and state
168 +
decoders, `capabilities`/`refs`/`negotiate`/`fetch`, and the ability to
169 +
materialise a state (spec §7–§9). Signature verification (Ed25519 and
170 +
ML-DSA-65) is required to honour a policy; without it, treat every
171 +
repository as unverified and say so.
172 +
173 +
A **server** needs the above plus storage, the push checks of §4.3, per-
174 +
repository write serialisation, and the policy chain (`refs` key 4, bundle
175 +
key 3). It never needs a private key of its own: servers store and forward
176 +
signatures, they do not make them.
177 +
178 +
Test material: the encoders and decoders in `crates/oxy-core/src/protocol.rs`
179 +
(`wire` module) and the round-trip and corruption tests beside them; the
180 +
signature vectors in `crates/oxy-core/src/identity.rs`; and the chunking
181 +
bounds test in `crates/oxy-core/src/chunk.rs`.
modified README.md +4 −1
1 1
# Oxygen
2 2
3 -
Oxygen (`oxy`) is a distributed, commutative version control system written in Rust for large repositories. See [docs/spec.md](docs/spec.md) for the specification.
3 +
Oxygen (`oxy`) is a distributed, commutative version control system written in Rust for large repositories. See [docs/spec.md](docs/spec.md) for the specification, [docs/guide.md](docs/guide.md) for a user guide by task, and [docs/protocol.md](docs/protocol.md) if you are implementing a client or server.
4 4
5 5
It is self-hosting: this directory is an Oxygen repository, and the project's own history is kept with `oxy`, not with Git.
6 6
…
72 unchanged lines
79 79
| `crates/oxy-server` | Reference HTTP(S) server for the wire protocol |
80 80
| `src/cli` | The `oxy` command-line interface |
81 81
| `docs/spec.md` | The specification |
82 +
| `docs/guide.md` | User guide, organised by task |
83 +
| `docs/protocol.md` | Wire protocol for other implementers |
84 +
| `docs/prompt.md` | Shell prompt integration |
82 85
83 86
## Licence
84 87
…
1 unchanged lines
modified docs/plan.md +2 −1
…
32 unchanged lines
33 33
- [x] Hardening: property tests for the CBOR decoder and pack reader against random and mutated inputs; a concurrent-push test against the server; a 1 GB single-file benchmark for chunking and blobs. (Done: random/mutated CBOR never panics and accepted input is canonical; 300 rounds of pack/index corruption fail cleanly, with new index bounds checks; 8 concurrent HTTP pushes serialise with pull-and-retry; `bench_one_gibibyte_file` is an ignored release-mode test, run with `OXY_BENCH_DIR` on a real disk.)
34 34
- [x] Performance follow-up from the 1 GiB benchmark: commit takes 5.6 s on tmpfs but 63 s on the SSD, and clone 49 s, because every object is written with its own fsync (13,715 chunks). Write committed and fetched objects straight into a pack with one fsync, or batch loose writes. (Done: store write batches; small batches stay loose, large ones spill to one pack. SSD: commit 63 s → 8.8 s, clone 49 s → 15.9 s; spec 0.6r.)
35 35
- [x] Performance follow-up: `oxy status` re-hashes a clean 1 GiB working copy (2.4 s). Record size and mtime in the manifest and skip hashing files whose stat is unchanged (spec allows a stat cache). (Done: manifest records blob refs for opaque files; unchanged files are not read; two-second racy window; status on a clean 1 GiB copy 2.4 s → 4 ms; spec 0.6s.)
36 -
- [ ] Documentation: `docs/guide.md` (user guide by task) and `docs/protocol.md` (for other implementers, derived from spec §16 and the wire encodings).
36 +
- [x] Documentation: `docs/guide.md` (user guide by task) and `docs/protocol.md` (for other implementers, derived from spec §16 and the wire encodings). (Done; every command and flag in the guide was checked against `--help`.)
37 37
- [ ] Spec review pass: resolve or re-word every "MAY ... until" allowance introduced during implementation, and close what can be closed in §27.
38 38
39 39
## Log
40 40
41 41
- 2026-09-26 23:01 plan created.
42 +
- 2026-09-27 09:35 iteration 17: user guide and protocol document. 112 tests.
42 43
- 2026-09-27 09:10 iteration 16: manifest stat cache (status 2.4 s → 4 ms on 1 GiB). 112 tests.
43 44
- 2026-09-27 08:40 iteration 15: batched object writes (commit 8.8 s, clone 15.9 s for 1 GiB on SSD). 111 tests.
44 45
- 2026-09-27 08:05 iteration 14: hardening (CBOR and pack fuzz-style tests, concurrent pushes, 1 GiB benchmark: write 0.7 s, commit 63 s on SSD / 5.6 s on tmpfs, status 2.4 s, clone 49 s, read back 1.0 s, store 1025.6 MiB, 13,715 objects). 110 tests, 1 ignored benchmark.
…
13 unchanged lines