OXYGENHUB
# 2cd40ea1c04c oxygen / docs / stores.md
11.0 KiB · Markdown · last changed Strata: object_store 0.13, so the release cross builds link 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
## 2. What users see
32
33
Storage must be transparent: a bucket behaves like any other remote, with
34
no new concepts to learn.
35
36
```sh
37
oxy remote add backup s3://my-bucket/repos/oxygen
38
oxy push backup
39
oxy clone gs://team-bucket/metalgear
40
oxy pull
41
```
42
43
- **URLs:** `s3://BUCKET/PREFIX` and `gs://BUCKET/PREFIX`. The prefix is
44
  optional, so one bucket can hold many repositories.
45
- **Credentials come from the standard places**, with no oxy-specific
46
  configuration:
47
  - **AWS:** environment variables, `~/.aws` profiles (`AWS_PROFILE`),
48
    SSO, and instance or container roles;
49
  - **Google Cloud:** Application Default Credentials (`gcloud auth
50
    application-default login`), service account files, and workload
51
    identity.
52
  - **Overrides:** optional per-remote settings in the repository's
53
    config (`profile`, `region`, `endpoint`), for example
54
    `oxy remote add backup s3://b/p --profile work`.
55
- **Errors are written for people.** Each one says what to do next, for
56
  example: "no AWS credentials found: run `aws sso login` or set
57
  AWS_PROFILE", or "the bucket exists but this account cannot write to
58
  it (s3:PutObject denied on my-bucket/repos/oxygen)".
59
- **`oxy init --remote s3://…`** creates an empty repository in the
60
  bucket, and `oxy remote check NAME` reports access, latency and
61
  what is stored there.
62
- **Progress, colour and glyphs** follow the CLI style of every other
63
  command.
64
- **Several writers are safe.** Two people pushing to the same bucket at
65
  once is handled the way the operation log handles two processes today
66
  (§5).
67
68
## 3. Architecture
69
70
- **A `Store` trait in oxy-core**, with three implementations: the
71
  current local `.oxy/` store, S3, and GCS. It has two halves:
72
  - **objects**: `has`, `get`, `get_range`, `put` (create only), and
73
    `list` by prefix;
74
  - **references**: `read() -> (bytes, version)` and
75
    `write(bytes, expected_version) -> Ok | Conflict`.
76
- **The `object_store` crate** (Apache Arrow) for S3 and GCS: one API
77
  for both, mature credential chains, multipart uploads, and conditional
78
  puts. Azure is in the same crate, so it comes nearly free later.
79
- **Push and pull talk to the bucket directly** through the same
80
  negotiation the hub protocol uses: what does the remote have, and what
81
  is missing? The client sends packs, never loose objects.
82
- **A local cache** of fetched objects, which is always correct because
83
  objects never change.
84
- **The working repository stays local.** A bucket is a remote. Running
85
  a repository *from* a bucket (no local `.oxy/`) is out of scope here;
86
  it is what the virtual working copy (§8) is for.
87
88
## 4. Layout in the bucket (as built, 2026-10-03)
89
90
```
91
PREFIX/
92
  oxygen.toml                      format version and repository id, written once by the first push
93
  bundles/<seq>-<hash>.bundle      one per push: changes, signatures, seals, objects, policies
94
                                   (the wire protocol's bundle encoding), written once, named by content
95
  refs                             every bookmark with its heads and attestation, every tag, the policy
96
                                   and its history (the protocol's refs response), moved by compare-and-swap
97
```
98
99
- **Pushing** uploads one bundle, then updates `refs` from the version
100
  it read. A bundle named in `refs` is complete, because the bundle is
101
  written before `refs` moves.
102
- **One `refs` record** so that a push moving several bookmarks is
103
  atomic. Per-reference records would scale to more writers but lose
104
  atomic pushes; one engineer and small teams do not need them.
105
- **Bundles are whole-file transfers.** Each push is one bundle, so a
106
  pull downloads only the bundles it has not seen. Packs with indexes
107
  for range reads, as first planned, are an optimisation for later
108
  (`oxy gc` against a remote merges bundles, and could write a pack).
109
- **The client does what a hub does.** A store cannot run code, so each
110
  client keeps a **mirror**, a bare repository of its own under
111
  `.oxy/remotes/<name>/strata/`, brought level with the store before
112
  every use: the record read, the bundles it has not seen imported (each
113
  object re-hashed), the view set from the record. A push runs the hub's
114
  own checks (`receive_push`: policy, attestations, fast-forward) on the
115
  mirror, uploads the bundle, then moves `refs`. A fetch verifies each
116
  bookmark's attestation under the policy, as for any remote. The
117
  mirror costs a second copy of the objects on disk; sharing them with
118
  the repository's own store is a later optimisation.
119
- **The record moved meanwhile:** the mirror is put back to what the
120
  store says and the push is reported as a hub reports a stale push:
121
  pull, then push again. Merging two pushes to different bookmarks
122
  without a round trip is ST5's work.
123
- **A directory is a store too:** `oxy remote add usb dir:///media/usb/repo`
124
  (`dir://`, since a plain path or `file://` is another repository).
125
  Tests run the whole flow through it with no network.
126
127
## 5. Concurrency: compare-and-swap on `refs`
128
129
| Provider | Create only | Replace only if unchanged |
130
|---|---|---|
131
| S3 | `PutObject` with `If-None-Match: *` | `PutObject` with `If-Match: <ETag>` |
132
| GCS | `ifGenerationMatch=0` | `ifGenerationMatch=<generation>` |
133
134
When a push finds `refs` has moved, it re-reads `refs` and merges by the
135
same rules as the operation log's merge:
136
- a bookmark moved by only one side takes that side's value;
137
- moved differently by both, it keeps both sides' heads, as several
138
  heads;
139
- a tag keeps its first value.
140
141
It then retries the write. Uploading packs needs no locking: two writers
142
uploading the same objects is harmless.
143
144
**As built (2026-10-04):** the `object_store` crate (0.13.1: 0.14 brought in
145
`aws-lc-rs`, a C build, and `crc-fast`, whose `cdylib` the macOS cross
146
build cannot link) carries both
147
writes as `PutMode::Create` and `PutMode::Update(UpdateVersion { e_tag,
148
version })`; S3 needs `S3ConditionalPut::ETagMatch` switched on, which
149
sends `If-None-Match: *` and `If-Match: <ETag>`; GCS uses the generation
150
(`ifGenerationMatch`); Azure the ETag. A store's `Version` carries both
151
the ETag and the generation, so the same code serves all three.
152
Checked against the real services on 2026-10-03/04: every conditional
153
write refuses as required on S3, GCS and Azure.
154
155
## 6. Safety
156
157
- **The bucket is untrusted.** Every object is re-hashed on read, as
158
  local reads are now. Signatures and repository policy are checked on
159
  fetch exactly as for a hub remote.
160
- **Bound to one repository.** `oxygen.toml` names the repository id. A
161
  push to a bucket holding a different repository is refused, and so is
162
  a clone that finds packs naming another id.
163
- **Size limits.** The existing 1 GiB object cap, plus limits on index
164
  and `refs` sizes, so a hostile bucket cannot drive huge allocations.
165
- **Garbage collection never deletes in place.** `oxy gc` on a remote
166
  writes the merged pack and moves `refs` first. Packs no longer named
167
  are deleted only after a grace period (default 7 days), so a slow
168
  concurrent reader is never left with a missing pack.
169
- **Least privilege.** The documented IAM and GCS policies need only
170
  Get, Put, List and Delete under the prefix. A read-only key gives a
171
  working clone and pull.
172
- **No secrets in the repository.** Credentials stay in the providers'
173
  own chains or the OS keychain, never in `.oxy/config`.
174
175
## 7. Testing
176
177
- **One conformance suite** that every store must pass, run against
178
  local, S3 and GCS. It covers object round trips, range reads,
179
  create-only refusals, compare-and-swap conflicts, concurrent pushers
180
  merging, an interrupted push leaving the remote valid, gc with a
181
  concurrent reader, and a repository-id mismatch being refused.
182
- **In CI** (Flow jobs have no network): the in-memory and directory
183
  stores run the conformance check and the whole push/clone/pull flow.
184
- **Real buckets** for the cloud stores, run by hand from a machine that
185
  holds restricted credentials: `crates/oxy-strata/tests/cloud.rs` reads
186
  `OXY_TEST_S3`, `OXY_TEST_GCS` and `OXY_TEST_AZURE` (each a store URL
187
  under a test prefix, with `OXY_TEST_S3_PROFILE`,
188
  `OXY_TEST_GCS_CREDENTIALS` and `OXY_TEST_AZURE_CREDENTIALS` naming the
189
  identity), runs the conformance check and a repository round trip under
190
  a prefix of its own, and deletes what it wrote. Unset, the tests say so
191
  and pass.
192
- **Emulators** were tried and dropped: MinIO's images are no longer
193
  published publicly, and `fake-gcs-server` rejects the client's XML
194
  uploads. The real services are the test.
195
196
## 8. Order of work
197
198
1. **The `Store` trait in oxy-core**, with the local store moved behind
199
   it and no change in behaviour, so every existing test passes.
200
2. **S3 and GCS stores together**, with the conformance suite and the
201
   emulators.
202
3. **`oxy remote add`, `push`, `pull`, `clone` and `init --remote`**
203
   for bucket URLs, with the friendly errors and `remote check`.
204
4. **`oxy gc` against a bucket**, with the grace period.
205
5. **Documentation:** a user guide page, sample IAM and GCS policies,
206
   and a README section.
207
208
Later, on the same trait:
209
- Azure Blob storage;
210
- a Git repository used as a plain store, so `oxy push` works to
211
  GitHub or GitLab;
212
- the hub attaching a repository from a customer's bucket ("bring your
213
  own storage", see oxygenhub `docs/scaling.md`);
214
- couriers: signed, encrypted bundles shared by email, Slack, Matrix
215
  and the like (a separate design);
216
- a virtual working copy mounted with FUSE (from OxygenFlow's
217
  `flow-casfs`).
218
219
## 9. Open questions
220
221
- Should `oxy clone s3://…` set the bucket as `origin`, or prompt for a
222
  name? Proposed: `origin`, as for any URL.
223
- Does a bucket remote keep its own operation log, or only `refs`?
224
  Proposed: only `refs` at first. The local operation log already
225
  records what each push did.
226
- Should packs be encrypted on the client, so the provider only ever
227
  stores ciphertext, beyond the provider's own encryption? Proposed:
228
  later, as an option. It does not change the layout.