OXYGENHUB

Strata: the user guide, a README section, and the design marked as built 8e1194860efa

docs/strata.md: setting up a bucket on S3, Google Cloud Storage and
Azure with least-privilege policies, the credential chains, using a
store remote (remote add, remote check, push, clone, pull, gc --remote),
what a store holds, and what each error means.

refs #63
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-10-03 23:10 UTC
Signature
✔ signed by Andy McCall
Hash
8e1194860efaf059e0fa9081944a9b36ada868976604745858f0debc3b3e0049
Depends on
648724d8e0de a70dce536476 d02e545dc265 ee06a6c557b7 ff0af1ee299d
Depended on by
771661aa50f4 8b3c9b2ba502 d069701a4997 648189388f1a
In bookmarks
agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
In tags
v0.10.0 v0.11.0 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
Edits
8 atoms, 1 tree operation, 3 files

Browse the tree at this change

added docs/strata.md +162 −0
1 +
# Oxygen Strata: keeping a repository on your own storage
2 +
3 +
A repository can be pushed to, cloned from and pulled from a bucket in
4 +
your own cloud account, or a directory on a shared drive, with no server
5 +
in between. The bucket is just another remote:
6 +
7 +
```
8 +
oxy remote add cloud s3://my-bucket/repos/project --profile work
9 +
oxy remote check cloud
10 +
oxy push cloud
11 +
oxy clone s3://my-bucket/repos/project?profile=work
12 +
oxy pull
13 +
```
14 +
15 +
It works because Oxygen's objects never change and are named by their
16 +
hash, so any store that can keep write-once objects and update one small
17 +
record by compare-and-swap can hold a complete repository. Nothing in the
18 +
store is trusted: every object is re-hashed as it is read, changes are
19 +
signed, and a bookmark's attestation is verified under your policy, as
20 +
for any remote. The data stays in your account, in your region, under
21 +
your keys; an operator of a hub need never see it.
22 +
23 +
Supported stores:
24 +
25 +
| URL | Store |
26 +
|---|---|
27 +
| `s3://BUCKET/PREFIX` | Amazon S3, and S3-compatible services with `--endpoint` |
28 +
| `gs://BUCKET/PREFIX` | Google Cloud Storage |
29 +
| `az://ACCOUNT/CONTAINER/PREFIX` | Azure Blob Storage |
30 +
| `dir://PATH` | a directory: a USB stick, a network share, or for trying things out |
31 +
32 +
A store holds one repository per prefix. The first push binds the prefix
33 +
to the repository; a push of another repository to the same prefix is
34 +
refused. Use a prefix per repository, as `repos/project` above.
35 +
36 +
## Setting up a bucket
37 +
38 +
### Amazon S3
39 +
40 +
1. Create a bucket in the region you want the data kept in (for example
41 +
   `eu-west-2`, London), with public access blocked.
42 +
2. Give the identity oxy will use these permissions, and no more:
43 +
44 +
   ```json
45 +
   {
46 +
     "Version": "2012-10-17",
47 +
     "Statement": [
48 +
       { "Effect": "Allow", "Action": "s3:ListBucket",
49 +
         "Resource": "arn:aws:s3:::my-bucket" },
50 +
       { "Effect": "Allow",
51 +
         "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
52 +
         "Resource": "arn:aws:s3:::my-bucket/repos/*" }
53 +
     ]
54 +
   }
55 +
   ```
56 +
57 +
   A read-only identity (`s3:ListBucket` and `s3:GetObject`) gives a
58 +
   working clone and pull.
59 +
3. Credentials come from the places the AWS tools use, in this order:
60 +
   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in the environment; the
61 +
   profile named by `--profile` (or `AWS_PROFILE`, else `default`) in
62 +
   `~/.aws/credentials`; what the AWS CLI can produce for that profile
63 +
   (`aws sso login`, `aws login`, assumed roles, credential processes), so
64 +
   anything that works for `aws` works for `oxy`; and the instance's or
65 +
   container's role.
66 +
4. The region is taken from `--region`, `AWS_REGION`, the profile, or
67 +
   asked of S3. `--endpoint http://…` points at an S3-compatible service.
68 +
69 +
### Google Cloud Storage
70 +
71 +
1. Create a bucket in the region you want (for example `europe-west2`,
72 +
   London), with uniform access and public access prevention.
73 +
2. Create a service account and grant it **Storage Object Admin** on that
74 +
   bucket only (**Storage Object Viewer** for a read-only identity), or
75 +
   use your own account.
76 +
3. Credentials: a service account key file named by `--credentials` or
77 +
   `GOOGLE_APPLICATION_CREDENTIALS`; else the application default
78 +
   credentials that `gcloud auth application-default login` writes; else
79 +
   the instance's service account.
80 +
81 +
### Azure Blob Storage
82 +
83 +
1. Create a storage account in the region you want (for example
84 +
   `UK South`), with public blob access off, and a container in it.
85 +
2. Create a service principal (`az ad sp create-for-rbac`) with
86 +
   **Storage Blob Data Contributor** scoped to that container (**Storage
87 +
   Blob Data Reader** for read-only), or use your own sign-in.
88 +
3. Credentials: `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` and
89 +
   `AZURE_TENANT_ID` for a service principal; `AZURE_STORAGE_ACCOUNT_KEY`
90 +
   or `AZURE_STORAGE_SAS_TOKEN`; else what `az login` left behind; else
91 +
   the machine's managed identity.
92 +
93 +
### A directory
94 +
95 +
`dir:///media/usb/project` needs nothing. Several people on one machine,
96 +
or one person from several checkouts, can share it; a reference is
97 +
updated under a lock file beside it.
98 +
99 +
## Using a store remote
100 +
101 +
`oxy remote add NAME URL` stores the URL; `--profile`, `--region`,
102 +
`--endpoint` and `--credentials` are kept on it as query parameters, so a
103 +
clone can name them too: `oxy clone "gs://bucket/project?credentials=key.json"`.
104 +
105 +
`oxy remote check NAME` reaches the store with the credentials in hand
106 +
and says how long that took, what the prefix holds (this repository,
107 +
another one, or nothing yet), and then puts the store through the checks
108 +
a repository depends on: object round trips, create-only writes, listing,
109 +
deletion and compare-and-swap on a reference. It writes under a probe
110 +
prefix of its own and leaves nothing behind.
111 +
112 +
Then `push`, `pull`, `fetch` and `clone` work as for any remote. Every
113 +
push uploads one bundle holding what the store lacks, and moves the
114 +
record of bookmarks and tags from the version it read. Two people
115 +
pushing at once are safe: whoever lands second is applied over what
116 +
landed, or told to pull first when the same bookmark moved.
117 +
118 +
`oxy gc --remote NAME` folds the bundles into one, so a fresh clone
119 +
downloads one file. The superseded bundles are removed only seven days
120 +
later (`--now` removes them at once), so a clone that was listing them at
121 +
the time still completes.
122 +
123 +
## What a store holds
124 +
125 +
```
126 +
PREFIX/
127 +
  oxygen.toml                     format version and repository id, written once
128 +
  bundles/00000001-<hash>.bundle  one per push: changes, signatures, seals, objects
129 +
  bundles/00000002-<hash>.bundle
130 +
  refs                            bookmarks with heads and attestations, tags, policy
131 +
  gc                              bundles a gc superseded, with when
132 +
```
133 +
134 +
Each client keeps a mirror of the store under `.oxy/remotes/NAME/strata/`,
135 +
brought level before every use, and runs the same checks on a push that a
136 +
hub runs. The mirror is a cache: delete it and the next use rebuilds it.
137 +
138 +
## Troubleshooting
139 +
140 +
Every error says what to do next. The common ones:
141 +
142 +
- **access denied** — the identity cannot do this on the bucket: check
143 +
  its policy covers `ListBucket` on the bucket and `GetObject`,
144 +
  `PutObject` and `DeleteObject` under the prefix (or the equivalents on
145 +
  GCS and Azure).
146 +
- **no usable credentials** — nothing in the chain above produced any:
147 +
  set a profile, sign in with the provider's tool, or set the variables.
148 +
- **profile "x" is not in ~/.aws/config or ~/.aws/credentials** — the
149 +
  name on the URL is wrong, or the files live elsewhere
150 +
  (`AWS_CONFIG_FILE`, `AWS_SHARED_CREDENTIALS_FILE`).
151 +
- **no region was given and S3 did not say** — add `--region`, set
152 +
  `AWS_REGION`, or put `region` in the profile.
153 +
- **holds a different repository** — the prefix is taken: use another.
154 +
- **a push may be in progress, so try again** — a bundle another push
155 +
  uploaded is not yet named in the record; a moment later it is. If it
156 +
  never is, the store is missing changes: `oxy remote check` says what
157 +
  it holds.
158 +
- **the remote kept moving while pushing** — several pushes landed while
159 +
  yours was retried; `oxy pull`, then push again.
160 +
161 +
`oxy remote check NAME` is the first thing to run when something is
162 +
wrong: it separates reachability, permissions and behaviour.
modified README.md +13 −0
…
51 unchanged lines
52 52
oxy pull
53 53
```
54 54
55 +
A repository kept on your own storage, with no server in between (Oxygen
56 +
Strata, see `docs/strata.md`): a bucket in your cloud account, in your
57 +
region, under your keys, or a directory on a shared drive.
58 +
59 +
```
60 +
oxy remote add cloud s3://my-bucket/repos/project --profile work   # or gs://, az://, dir://
61 +
oxy remote check cloud                   # reachable, what it holds, and that it behaves
62 +
oxy push cloud
63 +
oxy clone s3://my-bucket/repos/project?profile=work
64 +
```
65 +
55 66
Trust policy, so a server accepts only changes from keys you have granted:
56 67
57 68
```
…
19 unchanged lines
77 88
|---|---|
78 89
| `crates/oxy-core` | Library: encoding, hashing, objects, the change graph, storage, identities, policy, protocol, repository operations |
79 90
| `crates/oxy-server` | Reference HTTP(S) server for the wire protocol |
91 +
| `crates/oxy-strata` | The S3, Google Cloud Storage and Azure stores a repository can be kept on |
80 92
| `src/cli` | The `oxy` command-line interface |
81 93
| `docs/spec.md` | The specification |
82 94
| `docs/guide.md` | User guide, organised by task |
95 +
| `docs/strata.md` | Keeping a repository on your own storage: S3, GCS, Azure, a directory |
83 96
| `docs/protocol.md` | Wire protocol for other implementers |
84 97
| `docs/prompt.md` | Shell prompt integration |
85 98
…
3 unchanged lines
modified docs/stores.md +18 −11
1 1
# Oxygen Anywhere: stores
2 2
3 -
Status: design, 2026-10-03. Nothing here is built yet. Next to implement.
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.
4 8
5 9
## 1. What and why
6 10
…
154 unchanged lines
161 165
  manual test run. It needs network access, so it runs outside the
162 166
  isolated tier, with credentials scoped to a test prefix.
163 167
164 -
## 8. Order of work
168 +
## 8. Order of work (done)
165 169
166 -
1. **The `Store` trait in oxy-core**, with the local store moved behind
167 -
   it and no change in behaviour, so every existing test passes.
168 -
2. **S3 and GCS stores together**, with the conformance suite and the
169 -
   emulators.
170 -
3. **`oxy remote add`, `push`, `pull`, `clone` and `init --remote`**
171 -
   for bucket URLs, with the friendly errors and `remote check`.
172 -
4. **`oxy gc` against a bucket**, with the grace period.
173 -
5. **Documentation:** a user guide page, sample IAM and GCS policies,
174 -
   and a README section.
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.
175 182
176 183
Later, on the same trait:
177 184
- Azure Blob storage;
…
19 unchanged lines