| 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 |
|
| 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. |