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 |