OXYGENHUB
# 82504d09aefe oxygen / docs / stores.md
10.2 KiB · Markdown · last changed Strata design: the data sovereignty case (refs #63) 1 month ago renderedsource blame history raw
1
# Oxygen Anywhere: stores
2
3
Status: design, 2026-10-03. Nothing here is built yet. Next to implement.
4
5
## 1. What and why
6
7
Today an Oxygen repository lives in `.oxy/` on a disk, and a remote is
8
either another directory or an OxygenHub. This feature makes storage
9
pluggable. Any **store** that can keep write-once objects and update a
10
small reference record by compare-and-swap can hold a complete
11
repository, with no server in between.
12
13
The first stores are **Amazon S3** and **Google Cloud Storage**, built
14
side by side so the code stays portable. Azure Blob and a Git repository
15
used as a plain store follow on the same interface.
16
17
Why Oxygen can do this safely when Git cannot:
18
19
- **Objects never change.** Changes, states, blobs, attestations and
20
  receipts are named by their BLAKE3 hash and written once.
21
- **Only references are mutable.** Bookmarks, tags and the operation
22
  head are small, and object stores now offer compare-and-swap on a
23
  single object.
24
- **Every read is verified.** Objects are re-hashed on read and changes
25
  are signed, so a bucket is untrusted storage. Tampering is detected,
26
  and nobody can forge history, not even the bucket's owner.
27
28
Performance is "good enough" for one engineer today. This work is about
29
where a repository can live, not about speed.
30
31
## 1a. The driving case: data sovereignty
32
33
UK Government work requires data to be kept on UK shores. On GitLab SaaS
34
that meant waiting for GitLab to build UK data centres, and it was still
35
unclear where runners kept their data, so the runners were self-hosted.
36
37
Strata answers that for repositories: they live in the customer's own
38
bucket, in their region, encrypted with their own KMS key. A sovereign
39
forge needs the same for everything else:
40
41
- **Flow artifacts, logs and caches** in the customer's bucket. Today
42
  artifacts are kept in the hub's store and logs in its database. This is
43
  the next gap after Strata.
44
- **Forge records** (issues, proposals, reviews) in a database in the
45
  customer's region.
46
- **Keys held by the customer:** KMS for storage, developers' own keys
47
  for signing.
48
49
The model to aim for is **control plane anywhere, data plane in the
50
customer's account**:
51
- **Content is never trusted to the operator.** It is hash-verified and
52
  signed, so it stays tamper-evident whoever runs the hub.
53
- **Crypto-shredding.** Revoking the customer's KMS key makes the data
54
  unreadable everywhere, including backups. That is a clean exit and
55
  deletion guarantee.
56
- **The customer audits the vendor.** Every read the hub makes appears in
57
  the customer's own CloudTrail or GCS audit logs.
58
- **Rendering stays in-region.** Rendering code for the browser needs
59
  plaintext, so a store-and-render node runs inside the customer's
60
  account, and only orchestration and the UI shell run outside it.
61
62
## 2. What users see
63
64
Storage must be transparent: a bucket behaves like any other remote, with
65
no new concepts to learn.
66
67
```sh
68
oxy remote add backup s3://my-bucket/repos/oxygen
69
oxy push backup
70
oxy clone gs://team-bucket/metalgear
71
oxy pull
72
```
73
74
- **URLs:** `s3://BUCKET/PREFIX` and `gs://BUCKET/PREFIX`. The prefix is
75
  optional, so one bucket can hold many repositories.
76
- **Credentials come from the standard places**, with no oxy-specific
77
  configuration:
78
  - **AWS:** environment variables, `~/.aws` profiles (`AWS_PROFILE`),
79
    SSO, and instance or container roles;
80
  - **Google Cloud:** Application Default Credentials (`gcloud auth
81
    application-default login`), service account files, and workload
82
    identity.
83
  - **Overrides:** optional per-remote settings in the repository's
84
    config (`profile`, `region`, `endpoint`), for example
85
    `oxy remote add backup s3://b/p --profile work`.
86
- **Errors are written for people.** Each one says what to do next, for
87
  example: "no AWS credentials found: run `aws sso login` or set
88
  AWS_PROFILE", or "the bucket exists but this account cannot write to
89
  it (s3:PutObject denied on my-bucket/repos/oxygen)".
90
- **`oxy init --remote s3://…`** creates an empty repository in the
91
  bucket, and `oxy remote check NAME` reports access, latency and
92
  what is stored there.
93
- **Progress, colour and glyphs** follow the CLI style of every other
94
  command.
95
- **Several writers are safe.** Two people pushing to the same bucket at
96
  once is handled the way the operation log handles two processes today
97
  (§5).
98
99
## 3. Architecture
100
101
- **A `Store` trait in oxy-core**, with three implementations: the
102
  current local `.oxy/` store, S3, and GCS. It has two halves:
103
  - **objects**: `has`, `get`, `get_range`, `put` (create only), and
104
    `list` by prefix;
105
  - **references**: `read() -> (bytes, version)` and
106
    `write(bytes, expected_version) -> Ok | Conflict`.
107
- **The `object_store` crate** (Apache Arrow) for S3 and GCS: one API
108
  for both, mature credential chains, multipart uploads, and conditional
109
  puts. Azure is in the same crate, so it comes nearly free later.
110
- **Push and pull talk to the bucket directly** through the same
111
  negotiation the hub protocol uses: what does the remote have, and what
112
  is missing? The client sends packs, never loose objects.
113
- **A local cache** of fetched objects, which is always correct because
114
  objects never change.
115
- **The working repository stays local.** A bucket is a remote. Running
116
  a repository *from* a bucket (no local `.oxy/`) is out of scope here;
117
  it is what the virtual working copy (§8) is for.
118
119
## 4. Layout in the bucket
120
121
```
122
PREFIX/
123
  oxygen.toml          format version and repository id, written once
124
  packs/<id>.pack      immutable, zstd-compressed objects
125
  packs/<id>.idx       sorted index of the pack (read with range GETs)
126
  refs                 bookmarks, tags and the remote's operation head: one object, updated by compare-and-swap
127
  receipts/, attest/   only if they are kept outside packs
128
```
129
130
- **Pushing** uploads one pack and its index, then updates `refs`.
131
- **Packs never change.** A pack named in `refs` is complete, because
132
  the pack is written before `refs` moves.
133
- **One `refs` object** so that a push moving several bookmarks is
134
  atomic. Per-reference objects would scale to more writers but lose
135
  atomic pushes; one engineer and small teams do not need them.
136
- **Many small packs from many pushes** are merged by `oxy gc` against
137
  the remote, as `merge_packs` does locally.
138
139
## 5. Concurrency: compare-and-swap on `refs`
140
141
| Provider | Create only | Replace only if unchanged |
142
|---|---|---|
143
| S3 | `PutObject` with `If-None-Match: *` | `PutObject` with `If-Match: <ETag>` |
144
| GCS | `ifGenerationMatch=0` | `ifGenerationMatch=<generation>` |
145
146
When a push finds `refs` has moved, it re-reads `refs` and merges by the
147
same rules as the operation log's merge:
148
- a bookmark moved by only one side takes that side's value;
149
- moved differently by both, it keeps both sides' heads, as several
150
  heads;
151
- a tag keeps its first value.
152
153
It then retries the write. Uploading packs needs no locking: two writers
154
uploading the same objects is harmless.
155
156
Before building on it, check each provider's current documentation for
157
conditional writes: S3 added `If-Match` on `PutObject` in 2024, and this
158
design depends on it.
159
160
## 6. Safety
161
162
- **The bucket is untrusted.** Every object is re-hashed on read, as
163
  local reads are now. Signatures and repository policy are checked on
164
  fetch exactly as for a hub remote.
165
- **Bound to one repository.** `oxygen.toml` names the repository id. A
166
  push to a bucket holding a different repository is refused, and so is
167
  a clone that finds packs naming another id.
168
- **Size limits.** The existing 1 GiB object cap, plus limits on index
169
  and `refs` sizes, so a hostile bucket cannot drive huge allocations.
170
- **Garbage collection never deletes in place.** `oxy gc` on a remote
171
  writes the merged pack and moves `refs` first. Packs no longer named
172
  are deleted only after a grace period (default 7 days), so a slow
173
  concurrent reader is never left with a missing pack.
174
- **Least privilege.** The documented IAM and GCS policies need only
175
  Get, Put, List and Delete under the prefix. A read-only key gives a
176
  working clone and pull.
177
- **No secrets in the repository.** Credentials stay in the providers'
178
  own chains or the OS keychain, never in `.oxy/config`.
179
180
## 7. Testing
181
182
- **One conformance suite** that every store must pass, run against
183
  local, S3 and GCS. It covers object round trips, range reads,
184
  create-only refusals, compare-and-swap conflicts, concurrent pushers
185
  merging, an interrupted push leaving the remote valid, gc with a
186
  concurrent reader, and a repository-id mismatch being refused.
187
- **Emulators in CI** (Flow jobs have no network):
188
  - MinIO for S3, if its conditional writes match S3's, otherwise
189
    LocalStack;
190
  - `fake-gcs-server` for GCS.
191
- **Real buckets** (Andy's AWS and GCP accounts) for a nightly or
192
  manual test run. It needs network access, so it runs outside the
193
  isolated tier, with credentials scoped to a test prefix.
194
195
## 8. Order of work
196
197
1. **The `Store` trait in oxy-core**, with the local store moved behind
198
   it and no change in behaviour, so every existing test passes.
199
2. **S3 and GCS stores together**, with the conformance suite and the
200
   emulators.
201
3. **`oxy remote add`, `push`, `pull`, `clone` and `init --remote`**
202
   for bucket URLs, with the friendly errors and `remote check`.
203
4. **`oxy gc` against a bucket**, with the grace period.
204
5. **Documentation:** a user guide page, sample IAM and GCS policies,
205
   and a README section.
206
207
Later, on the same trait:
208
- Azure Blob storage;
209
- a Git repository used as a plain store, so `oxy push` works to
210
  GitHub or GitLab;
211
- the hub attaching a repository from a customer's bucket ("bring your
212
  own storage", see oxygenhub `docs/scaling.md`);
213
- couriers: signed, encrypted bundles shared by email, Slack, Matrix
214
  and the like (a separate design);
215
- a virtual working copy mounted with FUSE (from OxygenFlow's
216
  `flow-casfs`).
217
218
## 9. Open questions
219
220
- Should `oxy clone s3://…` set the bucket as `origin`, or prompt for a
221
  name? Proposed: `origin`, as for any URL.
222
- Does a bucket remote keep its own operation log, or only `refs`?
223
  Proposed: only `refs` at first. The local operation log already
224
  records what each push did.
225
- Should packs be encrypted on the client, so the provider only ever
226
  stores ciphertext, beyond the provider's own encryption? Proposed:
227
  later, as an option. It does not change the layout.