OXYGENHUB
# 67a4fe1aa2d5 oxygen / docs / protocol.md
11.7 KiB · Markdown · last changed oxy-server: a host may refuse a push by what it adds (Backend::vet_push) 16 days ago renderedsource blame history raw
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; one a host
56
  refuses by its content (`vet_push`, spec §16.4) is an HTTP error with
57
  the host's reason as its text, and nothing of it is stored.
58
- TLS is ordinary HTTPS. The reference client trusts the system web roots
59
  plus an optional PEM file (`remote.<name>.ca` or `OXY_CA`); servers with
60
  self-signed certificates are expected to hand that file out.
61
- Pushes to one repository are serialised by the server; reads are not.
62
- A hosted repository may be **unbound**: created empty with no id, so an
63
  existing repository can be pushed to it and keep its identity. Its
64
  `capabilities` reports an all-zero id. A client refuses to clone or fetch
65
  from an unbound remote and accepts it for a push, sending its own id in
66
  the `Oxy-Repo-Id` request header (32 hex characters); the server binds
67
  the repository to that id under the push lock before validating the
68
  push, and refuses a push to an unbound repository that carries no header
69
  with `400`. Clients send the header on every request when they have an
70
  id; servers ignore it on bound repositories.
71
72
Embedding: `oxy_server::Server` answers any endpoint for a repository name
73
a host's `Backend` resolves and authorises (`open`, `authorize`, and an
74
`after_push` hook, and `vet_push`, with which a host refuses a push by
75
what it adds before anything is stored: an HTTP error, `422` say, whose
76
text, several lines perhaps, the client shows as it is; spec §16.4), so a hub with `owner/name` paths and its own accounts
77
routes `/{owner}/{name}/oxy/v1/{endpoint}` to `Server::handle`; `oxy serve`
78
is `DirBackend` on that same server.
79
80
The spec (§16) also defines framing for Noise and ZCT transports (`u32
81
big-endian length || bytes`); the reference implementation speaks HTTP only.
82
83
## 3. Messages
84
85
Keys are CBOR unsigned integers. `[x]` is an array of x; `bytes(32)` an
86
ObjectRef; "signed X" is `[X bytes, [Signature]]`.
87
88
| Message | Encoding |
89
|---|---|
90
| Signature | `{0: key id bytes(32), 1: alg (1 = Ed25519 + ML-DSA-65), 2: Ed25519 bytes(64), 3: ML-DSA-65 bytes(3309)}` |
91
| Signed attestation | `[attest bytes, [Signature]]` |
92
| Signed policy | `[policy bytes, [Signature]]` (also the policy proposal file format) |
93
| Capabilities | `{0: [protocol versions], 1: repo id bytes(16), 2: [feature names]}`; version `1`; features currently `["seal"]` |
94
| Refs response | `{0: [ref], 1: repo id, 2: signed policy or null, 3: [tag], 4: [signed policy]}` |
95
| ref | `{0: name, 1: state ref, 2: [heads], 3: signed attestation or null, 4: last serial or null, 5: signed acceptance}`; the attestation is present only when it describes the current state; `last serial` is the highest attestation serial ever recorded for the name; key 5 (optional, only when there is one) is the host's `accepted` attestation of that attestation (spec §14.7) |
96
| tag | `{0: name, 1: state ref, 2: [heads], 3: signed attestation or null}` |
97
| Negotiate request | `{0: [have], 1: [want]}`; response `{0: [hashes]}` in dependency order (no change before its dependencies) |
98
| Fetch request | `{0: [hashes], 1: max blob size or null}`; response a Bundle whose blobs above the size (and their chunks) are omitted |
99
| Objects request | `{0: [refs]}`; response a Bundle carrying only objects; a blob is accompanied by its chunks |
100
| 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 |
101
| Shallow request | `{0: [heads], 1: depth}`; response `{0: [hashes]}`: the changes within `depth` dependency steps of the heads, dependencies first |
102
| Bundle | `{0: [[change bytes, [Signature]]], 1: [signed attestation], 2: [[type name, object bytes]], 3: [signed policy]}` |
103
| Push request | `{0: Bundle, 1: [update], 2: signed policy or null, 3: [tag]}` |
104
| update | `{0: name, 1: old state ref, 2: new state ref, 3: force (bool), 4: signed attestation or null}` |
105
| Push response | `{0: [{0: name, 1: ok (bool), 2: reason (text)}]}`; a result named `policy` reports the policy update, `tags/<name>` each tag |
106
107
Bundle details:
108
109
- Changes are in dependency order, each with its individual signatures
110
  (possibly none).
111
- Key 1 carries seals (`kind = seal`) covering the changes.
112
- Key 2 objects are only `chunk`, `blob`, `identity`, `state`, `checkpoint`;
113
  a receiver rejects any other type.
114
- Key 3 (optional) carries the policies between the receiver's serial and
115
  the one in the push request, ascending, so the receiver can verify each
116
  step (§14.5). The refs response carries the same history in its key 4.
117
118
Sizes: a seal lists at most 4096 hashes, sorted and unique. Chunks are
119
32–256 KiB (FastCDC, average 64 KiB, gear table from the BLAKE3 XOF of
120
`"oxy1 fastcdc gear"`, spec §13.2); a receiver re-hashes every object.
121
122
## 4. Sequences
123
124
### 4.1 Clone or fetch
125
126
1. `capabilities`: check protocol version 1 and record the repository id.
127
2. `refs`: for each wanted bookmark note `state`, `heads`, `attestation`;
128
   take the policy and history.
129
3. Policy (spec §14.5): with no local policy, accept the remote's if it is
130
   for this repository and carries a valid signature by one of its own roots
131
   (trust on first use). With a local policy, refuse a lower serial or a
132
   different policy at the same serial; otherwise verify each policy in the
133
   chain against the one before it (signatures by the previous roots meeting
134
   the previous threshold, serial strictly increasing) and adopt them in
135
   order. Record the highest serial this remote has ever presented and
136
   refuse a remote that later presents none or a lower one.
137
4. `negotiate` with `have` = local heads of every bookmark, `want` = the
138
   remote heads. The response lists the missing changes.
139
5. `fetch` those hashes (with `max blob size` for a partial clone). Store
140
   objects, changes and seals; verify every change's signatures and seals;
141
   under a policy, authenticate each new change (§14.4: signer is the author
142
   or a delegate of the author) and check it is authorised for the bookmark
143
   by the first matching rule (a rule key covers its rotation predecessors,
144
   and a delegate's signature counts for the author).
145
6. Verify the bookmark's attestation (`kind = ref`: repo id, name, old
146
   state, new state, serial) against the policy and against the serial you
147
   last recorded for that reference (rollback protection), then update
148
   `remotes/<name>/<bookmark>`.
149
7. Tags: adopt those not yet held, checking their attestations the same way.
150
151
A shallow clone replaces steps 4–5 by `shallow` (the hashes within the
152
depth) plus `checkpoint` for the heads, and fetches only the listed changes;
153
their missing dependencies form the boundary. Lazy blobs are fetched later
154
with `objects`.
155
156
### 4.2 Push
157
158
1. `capabilities` and `refs` as above; refuse if the remote's policy is
159
   newer than yours (fetch first).
160
2. Compute the changes the remote lacks (`negotiate` with the roles
161
   reversed, or locally from the remote heads), bundle them in dependency
162
   order with their signatures, seal your unsigned changes with one `seal`
163
   attestation, and attach identities, blobs, chunks and the new state
164
   object.
165
3. Attest the reference: `kind = ref` with `serial` = the remote's `last
166
   serial + 1`, signed by your key.
167
4. Send `push` with `old state` = the state the remote reported (empty
168
   state ref when the bookmark is new), the new state, and your policy with
169
   its intermediate history when the remote's serial is behind.
170
171
### 4.3 What a server checks on push
172
173
In this order, spec §16.4: adopt the policy chain if present (each step
174
verified against the previous policy); import the bundle, verifying every
175
object's hash and, under a policy, every change's authentication and
176
authorisation; verify the reference attestation (signer authorised for the
177
name, serial above the last recorded); check `old state` equals the current
178
state (compare-and-swap; `force` skips this check only); check the rule's
179
`allow_conflicts` and `require` (signed, sealed, any); check every
180
dependency of every change is present; then update the references
181
atomically and record the attestation. Any failure rejects the whole push
182
with `ok = false` and a reason.
183
184
## 4.4 Editors and tools on the same machine
185
186
A tool that drives a local repository does not speak this protocol; it runs
187
`oxy` with `--json` (spec §24) and reads one JSON document per command. The
188
VS Code extension in `oxygen-vscode/docs/spec.md` §4 lists the commands it
189
relies on.
190
191
## 5. Minimal implementations
192
193
A **read-only client** needs: CBOR decode, BLAKE3, the change and state
194
decoders, `capabilities`/`refs`/`negotiate`/`fetch`, and the ability to
195
materialise a state (spec §7–§9). Signature verification (Ed25519 and
196
ML-DSA-65) is required to honour a policy; without it, treat every
197
repository as unverified and say so.
198
199
A **server** needs the above plus storage, the push checks of §4.3, per-
200
repository write serialisation, and the policy chain (`refs` key 4, bundle
201
key 3). It never needs a private key of its own: servers store and forward
202
signatures, they do not make them.
203
204
Test material: the encoders and decoders in `crates/oxy-core/src/protocol.rs`
205
(`wire` module) and the round-trip and corruption tests beside them; the
206
signature vectors in `crates/oxy-core/src/identity.rs`; and the chunking
207
bounds test in `crates/oxy-core/src/chunk.rs`.