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