OXYGENHUB

Oxygenhub specification Draft 0.1 and Earth deployment notes 93be9ce4653e

Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-27 16:28 UTC
Signature
✔ signed by Andy McCall
Hash
93be9ce4653e32704c445475b3b3df34b72e8ca28723cb602770522bb7f71bf6
Depends on
nothing: a root change
Depended on by
40a43921ef42 f2d2776b2a14 0bc95135954d 50928fab5ffc 306aec1839aa a22e4aec2e28 8f1d76f4169a 1c52d940b4a0 04f9c984439e fac6853c4e46 3d086bc3d69b 851a26a2a6d1 4dfa4599b3f7 dd0788b756f2 17dc8de4fca8 946fe7a09faa 439ecd8708a1 ecac6e8d9b41 456f4197ade3 ba0a55bb5b3c 8ac2aa4ca6d1 ca0c443c30cf c419455a82dd adbc38e7ea09 5588bb3631d2 3b3a51afde59 737de0fac722 367f34b8e590 166f6e9bf4f1 c7067c7bda56 cb7b3cd45dc0 33d98147511b e57765dee61a 0ee5c3fb32eb 6b195ed3b637 a9f1c0bf403a 58cbf42447bc 86a69b53aff0 2a8b621676b4 7c06f5b28ade e97500b70f9a 67523e06ed86 bb018142cca7 16ee5d19d220 85ec74fd82d8 2c47eecfeab6 205d8f863e5c a6d824c5ebf5 b9b74d997cec 8a48dd830178 8dcaeeb848c6 4f40eed0588f e5b97ac399e7 965b950f9b96 559c39204234 8aa5ef384ef1 2c31915f5c89 026a76d251ce 53a0fb2e4ac6 119fd17d213f d2c1d6a7d12e 9aee52d392a6 97fc47fee9cf f157bcea0e32 681db69eba17 52ae6422e5a1 6e006eaf1da4 076c7a7c86b1 c08cec372c81 107eaba52c9d c92a99490cba c8684d44d549 3b1bc27fe805 2cbf6f348595 3ff52534f69b 0024ccb1b050 3dd6582de060 d74ce22fa80e aa0bfbd11f02 9a777fec4ed9 9a8e3b4419e0 c0e39717895d a2b29af1ce88 bff9106edcba cd46e56b1709 d9b3b84605a4 4a4400c5812d e8fa53694f68 c2be148cfe83 f4d6ef418a96 93501da2a162 35fea4237cc9 d7d182467382 6dbd999ad794 17ca6cddc534 2f14c5f39e81 4fa0da126b70 eb2a3502601d d943160dafeb acc85ac0f70b 73ef9f767132 180dc43ad0e0 757195ad7a5d c4e432da4bc9 71e65fcf95ab 9aaa88b2d9c8 7a14bdaeb857 3776b70d9b17 a7e6e8e6094c 1d634f698ecd e71583501b1d f677d6270298 8620925e5c53 6ca0b971ff07 39442b000e6b 6adcd68b90b1 a46d15e72101 1c8276b90314 501d8b268dd4 44a3d2ebaddd 2fb70d019851 40dd3f86379a 92a797b20912 a0dd06b0094e 67040b158ae2 0650d94ede82 50d93fa443dc b1e92a6233ee efb51b39db1c 3d5c825aebba 617c2840446c 7fff30c4f5e2 36e5a9c15a84 0b203bbf7cc3 17a3a84b00d1 7edaff01d506 c987c86513df 554ccacfc123 8ddebaa7e856 1f78655a0466 62ceb2eec18c 155cab142b63 efd29a0033e8 83940d53664f 66fd9d6b1452 d0fbea2eb46e d1727c573f30 4ac01a732552 2a83f5303458 7d2eb590051f e71846aa07b9 d4792b4a4629 3556d08e17f9 8933faff2043 f28389bd6aab 5ea4872f23e8 b43a9ab0498f 8a3334b7a364 745d225de344 2291f8cc9c96 739a763a558c d98a93c10a0d f6346d875ee5 c4c87f481d05 7974adb0be66 b66e70b7b6c7 00cec4e18792 cc1cd197df1d 74be97962ed8 df71c7892d27 c96b32865000 129c23effb73 d3e75fb41e61 ff6569530173 d336629ffa90 462f88d161ce 0b20f1ec4007 d91780ee20de a1b9869565aa 38833ed78707 b6b7e4cf6b24 1f1eb06a9009 5c6e92fd8d71 df940d75cd3d 4f6925337848 10708ab8f54d a3c8288b0141 cc19697f8871 2bfe5ef66b0c 92574625ee9a 65be9f9c3074 5e6d6484c28e 9e5ef6c23006 16cb2b3aa947 fd958fbc06fb c5f3bde466d1 bf3d07541368 760a7dd30d6f 90326c9601f9 1edac4b68c6d e271a37b629a a0d4bbfbedd1 9008bf6c2b8c 961c10bee174 ef20fdc14e8b 9756c67d3aec c79a1111dd56 e1ec9fbf538e 78001759604c 76f262e1a666 8d194f035eb6 f5c443378333 8fb35df7d66e 756fe44a0f4d a160b98fb922 eb0c44a9ad37 34beb1a7e376 43e8ca33bc5d 3d1e74cdac76 736f6813543d 42fa95540468 8d2caf122517 6ae41081ccd3 1ac84c62ba53 1553bfc9ae53 07b1a5bbf481 9df855f29948 101740454617 42a0ba4bac4e c40c2d3edb51 3f02e5ea4f2d 4edcf24025cf f160cbc57756 145306044896 5c6eb1feaa71 baebdd18aaa2 8f3d2ccfe6fc d23464c0ac60
In bookmarks
api-unknown-fields cancel-landed-runs clear-waiting close-proposal-140 diffusion-pin-bookmark draft-ready image-check job-waits main oh-comment-body-file proposal-list-fast run-one-job wiki-says-so
In tags
v0.1.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.5.0 v0.5.1 v0.5.2 v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
Edits
3 atoms, 4 tree operations, 3 files

Browse the tree at this change

added .oxyignore +2 −0
1 +
/target
2 +
/.claude
added docs/deploy.md +27 −0
1 +
# Deploying the developer edition on Earth
2 +
3 +
Companion to `spec.md` §17.5. Status: planned, nothing installed yet (2026-09-27).
4 +
5 +
## The host
6 +
7 +
| | |
8 +
|---|---|
9 +
| Name | `earth` (`earth.andymccall.xyz`, IPv6 ULA; `192.168.1.6` on the LAN) |
10 +
| OS | Ubuntu 26.04.1 LTS, x86_64, 4 cores, 31 GiB RAM |
11 +
| Disk | `/` 251 GB (95 GB free), `/home` 1 TB (944 GB free) |
12 +
| Already running | sshd :22, a Python service :6400, `wren-nest` :7842, retroplay-manager in Docker (:8080, :6670) with Postgres and a tetriblox server, two GitHub Actions runners (`doomsdayonecom`), Docker, containerd |
13 +
| Not installed | Rust toolchain, sqlite3 CLI, nginx or caddy, certbot |
14 +
| Access | `ssh -i ~/.ssh/earth_claude claude@earth`; `claude` has passwordless sudo |
15 +
16 +
Ports 7300 (HTTP) and 7343 (HTTPS) are free and are the hub's.
17 +
18 +
## Steps
19 +
20 +
1. **Toolchain** (as `claude`): `curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal`, then `sudo apt-get install -y build-essential pkg-config git`.
21 +
2. **Source**: from the development machine, `rsync -a --delete --exclude target --exclude node_modules ~/development/oxygen ~/development/oxygenhub claude@earth:~/src/`. Oxygen is never pushed to Git; rsync is the bootstrap, and after the hub runs, `oxy push` to the hub itself is the ongoing path.
22 +
3. **Build**: `cd ~/src/oxygenhub && cargo build --release`.
23 +
4. **System user and data**: `sudo useradd --system --home /home/oxygenhub --create-home oxygenhub`, `sudo install -o oxygenhub -g oxygenhub -m 0755 target/release/oxygenhub /usr/local/bin/`, `sudo -u oxygenhub oxygenhub init --data /home/oxygenhub/data --admin andymccall` (prints the first admin's one-time password).
24 +
5. **Service**: `/etc/systemd/system/oxygenhub.service` running `oxygenhub serve --data /home/oxygenhub/data` as `oxygenhub`, `Restart=on-failure`, `ProtectSystem=strict`, `ReadWritePaths=/home/oxygenhub/data`, `NoNewPrivileges=yes`; `sudo systemctl enable --now oxygenhub`.
25 +
6. **TLS**: the developer edition generates a self-signed certificate at `init`. Download the CA from `https://earth.andymccall.xyz:7343/trust/ca.pem` and point clients at it with `oxy config remote.<name>.ca <file>` or `OXY_CA`.
26 +
7. **First repositories**: on the development machine, `oxy remote add earth https://earth.andymccall.xyz:7343/andymccall/oxygen` in each of `oxygen`, `oxygen-vscode`, `oxygenhub`, then `oxy push earth`.
27 +
8. **Updates**: rsync, build, `sudo install`, `sudo systemctl restart oxygenhub`. A `make deploy` target in the hub wraps steps 2, 3 and 8.
added docs/spec.md +542 −0
1 +
# Oxygenhub — Specification
2 +
3 +
| | |
4 +
|---|---|
5 +
| **Status** | Draft 0.1 |
6 +
| **Scope** | A self-hosted forge for Oxygen repositories: hosting, browsing, review, issues, releases, an HTTP API and webhooks, and the integration point for a runner. It works like GitHub where GitHub is right and differs where the commutative model makes something better possible. |
7 +
| **Depends on** | `oxy-core` and `oxy-server` from the Oxygen workspace (spec Draft 0.8b); `oxy` clients speaking wire protocol version 1 |
8 +
| **Companion documents** | `docs/runner.md` (Oxygen Runner, later), `docs/deploy.md` (developer edition on Earth) |
9 +
| **Date** | 2026-09-27 |
10 +
| **Audience** | Implementers, security reviewers, and the person who runs it |
11 +
12 +
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as described in RFC 2119.
13 +
14 +
---
15 +
16 +
## Table of contents
17 +
18 +
1. [Goals and non-goals](#1-goals-and-non-goals)
19 +
2. [How it compares with GitHub](#2-how-it-compares-with-github)
20 +
3. [Architecture](#3-architecture)
21 +
4. [Repositories on the server](#4-repositories-on-the-server)
22 +
5. [Accounts, identities and authorization](#5-accounts-identities-and-authorization)
23 +
6. [Web interface](#6-web-interface)
24 +
7. [Proposals and review](#7-proposals-and-review)
25 +
8. [Issues](#8-issues)
26 +
9. [Releases and tags](#9-releases-and-tags)
27 +
10. [Notifications](#10-notifications)
28 +
11. [Search](#11-search)
29 +
12. [HTTP API and webhooks](#12-http-api-and-webhooks)
30 +
13. [Checks and the runner](#13-checks-and-the-runner)
31 +
14. [Git interoperability](#14-git-interoperability)
32 +
15. [Security](#15-security)
33 +
16. [Performance and caching](#16-performance-and-caching)
34 +
17. [Operations: configuration, storage, backup](#17-operations-configuration-storage-backup)
35 +
18. [Required changes to Oxygen](#18-required-changes-to-oxygen)
36 +
19. [Milestones](#19-milestones)
37 +
20. [Open questions](#20-open-questions)
38 +
21. [Changelog](#21-changelog)
39 +
40 +
---
41 +
42 +
## 1. Goals and non-goals
43 +
44 +
1. **Work like GitHub.** A developer who knows GitHub must find repositories, history, diffs, review, issues, releases, tokens and webhooks where they expect them, with the same words where the words still apply. The learning curve is Oxygen's model, not the forge.
45 +
2. **Show what the model makes possible.** Multiple heads, reified conflicts, exact blame, signatures and trust policy, proposals that never go stale, review comments that survive rebases, and approvals that are portable signed objects. Each of these is a first-class page, not a footnote.
46 +
3. **One binary, one directory.** `oxygenhub serve` links `oxy-core`, embeds the wire protocol server, renders its own pages, and keeps everything under one data directory with SQLite for hub state. A backup is a copy of that directory. There is no separate database server, job queue, cache server or front-end build.
47 +
4. **The repository is the source of truth.** Everything about code, history, references, signatures and policy lives in the Oxygen object store and is readable with `oxy` alone. Hub tables hold only what Oxygen has no object for: accounts, sessions, issues, review threads, webhooks and check results. Deleting the hub database loses discussion, never history.
48 +
5. **Off the shelf where it is not the product.** Markdown editing uses EasyMDE, in-browser code editing uses CodeMirror 6, syntax highlighting uses syntect, Markdown rendering uses pulldown-cmark with ammonia sanitising. The hub's own code is the forge logic and the pages.
49 +
6. **Honest state.** A page never shows something the object store does not say. A signature is shown as verified only when the hub verified it against policy; an approval is shown only when the approving seal exists.
50 +
51 +
Non-goals for 1.0: hosting Git repositories natively (§14 covers mirrors), a marketplace, wikis, project boards, discussions, GitHub Pages, and federation between hubs. The runner is specified separately (§13, `docs/runner.md`) and is not required for 1.0.
52 +
53 +
## 2. How it compares with GitHub
54 +
55 +
| GitHub | Oxygenhub | Why it differs |
56 +
|---|---|---|
57 +
| Repository, owner, organisation | Same | — |
58 +
| Branch | **Bookmark** | An Oxygen bookmark names a set of heads, not one commit |
59 +
| Commit | **Change** | Changes commute; a change has dependencies, not a parent |
60 +
| Commit page shows one parent diff | Change page shows the change's own edits, its dependencies and its dependents | The diff of a change is intrinsic; it is the same on every bookmark that contains it |
61 +
| Merge, merge commit, rebase before merge | **Landing**: add the proposal's changes to the target bookmark | Union of heads; no merge commit; the result is computed before landing, so CI runs on exactly what lands |
62 +
| Pull request "out of date with base branch" | Never happens; a proposal stays valid as the target moves | Changes commute unless they conflict; a new conflict is shown, not a stale flag |
63 +
| Merge conflict blocks the PR until the author rebases | Conflict is a reified part of the resulting state; the proposal page shows the exact sides, and any collaborator may push a resolution change | Conflicts are values in the model (spec §9) |
64 +
| Review comment on a line, goes "outdated" after a force-push | Review comment anchored to an **atom** `(ChangeHash, index)`, follows the line through rebase, squash and split | Atoms are stable identities; rewrites record correspondences |
65 +
| Approval is a database row | Approval is a **seal** attestation signed by the reviewer's key, stored in the repository | Verifiable offline and after export; survives hub loss |
66 +
| Required status checks on a commit SHA | Required checks on a **state hash** | A state is content addressed, so a green check on the pre-landing state is exactly the check on the post-landing state |
67 +
| Branch protection rules | The repository's **trust policy** (spec §14.5), edited through signed proposals | One authority for who may update a reference, enforced identically by every client and server |
68 +
| Signed commits as a badge | Signature and seal status on every change, with the policy rule that admitted it | Signatures are the protocol, not an add-on |
69 +
| Tags and releases | Tags are signed attestations; releases attach notes and assets | — |
70 +
| Actions | Runner (§13), later | Designed from the failures of Actions and GitLab CI |
71 +
| Fork and PR from a fork | Fork, and proposals from a fork or from a bookmark in the same repository | Same |
72 +
| Issues, labels, milestones | Same, smaller | — |
73 +
| Personal access tokens | Same, scoped | Push also accepts the client's identity signature (§5.4) |
74 +
| Webhooks with HMAC | Same | — |
75 +
| REST and GraphQL APIs | JSON REST only | — |
76 +
77 +
## 3. Architecture
78 +
79 +
### 3.1 One process
80 +
81 +
`oxygenhub` is a Rust binary in its own Cargo workspace at `../oxygenhub`, depending on `oxy-core` and `oxy-server` by path (developer edition) or by version (release). It runs one tokio runtime with:
82 +
83 +
- an axum router serving pages, static assets, the JSON API, and the wire protocol under every repository's path;
84 +
- askama templates compiled into the binary; one hand-written stylesheet, also compiled in and served at `/style.css`;
85 +
- SQLite through `sqlx` (WAL mode, `synchronous=NORMAL`, foreign keys on) for hub state, opened from `<data>/hub.db`;
86 +
- a small background worker pool (tokio tasks) for webhooks delivery, notification email, search indexing, Git mirror export and cache warming;
87 +
- an in-process cache (§16).
88 +
89 +
There is no separate front-end build. JavaScript is limited to: the theme cookie, EasyMDE (Markdown editing), CodeMirror 6 (file editing), and small progressive enhancements (history graph hover, keyboard shortcuts, live check status). Every page MUST render and every form MUST work with JavaScript disabled, except the two editors, which degrade to a plain `<textarea>`.
90 +
91 +
### 3.2 Layout on disk
92 +
93 +
```
94 +
<data>/
95 +
  hub.db                      SQLite database (and -wal, -shm)
96 +
  hub.toml                    configuration (§17.1), optional; environment overrides
97 +
  keys/                       the hub's own identity (delegate key, §5.5) and TLS material
98 +
  repos/<owner>/<name>/       one Oxygen repository per directory, bare (§4.1)
99 +
  mirrors/<owner>/<name>.git  optional Git mirror (§14)
100 +
  assets/<owner>/<name>/      release assets, content addressed by BLAKE3
101 +
  cache/                      rebuildable: rendered pages, graph layouts, search index
102 +
```
103 +
104 +
`<owner>` and `<name>` MUST match `[A-Za-z0-9][A-Za-z0-9._-]{0,63}` and MUST NOT be `.`, `..`, or end in `.git` (reserved for the mirror URL). The reserved top-level path segments are `api`, `static`, `login`, `logout`, `register`, `settings`, `explore`, `search`, `notifications`, `new`, `orgs`, `admin`, `style.css`, `favicon.ico`, `favicon.svg`, `health`; no owner may take those names.
105 +
106 +
### 3.3 Request path
107 +
108 +
1. Middleware: request id, tracing span, body limits, session cookie decode, CSRF check on unsafe methods (§15.2), rate limits (§15.6).
109 +
2. Wire protocol routes `/{owner}/{name}/oxy/v1/*` go to the embedded `oxy-server` handlers with the hub's repository resolver and authorizer (§18).
110 +
3. Page and API handlers resolve `{owner}/{name}` to a `RepoHandle`, check the viewer's role, and run every `oxy-core` call inside `spawn_blocking` under the repository's read-write lock (§16.2).
111 +
4. Templates receive plain structs; no template touches `oxy-core` directly.
112 +
113 +
### 3.4 Crate layout
114 +
115 +
```
116 +
oxygenhub/
117 +
  Cargo.toml            workspace: crates/hub (binary), crates/hub-api (types shared with clients)
118 +
  crates/hub/src/
119 +
    main.rs             CLI: serve, init, admin (§17.3)
120 +
    config.rs           hub.toml and environment
121 +
    db/                 sqlx migrations and queries, one module per table group
122 +
    repos/              RepoHandle, locks, cache, bare repository operations
123 +
    auth/               sessions, passwords, TOTP, tokens, identity linking
124 +
    web/                page handlers, one module per area (repo, change, proposal, issue, …)
125 +
    api/                JSON handlers
126 +
    protocol.rs         embedding oxy-server
127 +
    render/             markdown, syntax highlighting, diff rendering, graph layout
128 +
    jobs/               background workers
129 +
  templates/            askama; base.html, style.css, one file per page
130 +
  assets/               wordmark, favicons, vendored EasyMDE and CodeMirror bundles
131 +
  migrations/           sqlx migrations
132 +
  docs/                 this file, runner.md, deploy.md
133 +
```
134 +
135 +
## 4. Repositories on the server
136 +
137 +
### 4.1 Bare repositories
138 +
139 +
A hosted repository is an Oxygen repository directory with no working copy checked out and no user workspace. `oxy-core` today always keeps one working-copy change per workspace; the hub never snapshots, commits from a checkout, or touches files outside `.oxy/`. The directory contains only `.oxy/`. §18 lists the small change that lets `Repo::open` skip working-copy bookkeeping for such a directory.
140 +
141 +
Creation: `Repo::init` under `<data>/repos/<owner>/<name>` with a fresh repository id, then an initial row in `repositories`. Import: a client `oxy push` into an empty hub repository; or, for Git, §14.
142 +
143 +
Deletion moves the directory to `<data>/trash/<owner>/<name>-<timestamp>` and a job removes it after `trash_days` (default 30). Renames are a directory move under the repository's write lock plus a `redirects` row so old URLs resolve for 90 days.
144 +
145 +
### 4.2 Forks
146 +
147 +
A fork copies the object store (hard links where the file system allows, otherwise a copy) and records `forked_from`. Because objects are content addressed and repository ids differ, a proposal from a fork carries the changes across repositories by hash; the hub fetches missing objects from the fork's store directly, never over the network.
148 +
149 +
### 4.3 The default bookmark
150 +
151 +
Each repository has a default bookmark (`main` unless changed). It is what the repository page, tree, file views and search show, and the target every proposal picks first.
152 +
153 +
### 4.4 Visibility
154 +
155 +
`public` (readable by anyone including anonymous clients), `internal` (readable by every signed-in account), `private` (readable by collaborators). Wire protocol reads obey the same rule: a private repository answers `404` to an unauthorized client, not `401`, so its existence is not disclosed.
156 +
157 +
## 5. Accounts, identities and authorization
158 +
159 +
### 5.1 Accounts
160 +
161 +
An account has a username (same grammar as an owner name), a display name, an email (verified before it is used for anything), an Argon2id password hash, an optional TOTP secret, a session salt (rotating it signs out every session), and a set of linked Oxygen identities (§5.3). The first account created becomes the site administrator. Registration is open, invitation only, or closed, per configuration.
162 +
163 +
Sessions are a signed cookie `oxyhub=<username>.<mac>` where `mac = HMAC-SHA256(cookie_key, "session:" || username || ":" || session_salt)`, `HttpOnly`, `Secure` when served over HTTPS, `SameSite=Lax`. Password changes and "sign out everywhere" rotate the salt. Passkeys (WebAuthn) are planned for 1.0 as a second factor or password replacement.
164 +
165 +
### 5.2 Organisations and teams
166 +
167 +
An organisation is an owner with members and teams. Roles on an organisation: `owner`, `member`. Roles on a repository, whether personal or organisational: `read`, `triage` (manage issues and proposals without writing), `write`, `maintain` (settings except deletion and secrets), `admin`. Teams hold members and receive repository roles as a unit. An organisation's default repository role for members is configurable.
168 +
169 +
### 5.3 Linked identities
170 +
171 +
An Oxygen identity is a signed object holding an Ed25519 + ML-DSA-65 key pair's public halves (spec §14.2). An account links an identity by uploading the identity object (`oxy identity export`) and proving possession: the hub issues a 32-byte challenge and the client signs it with `oxy identity sign-challenge` (§18); the hub verifies both signatures. A linked key is what makes a pushed change *this account's* change: author attribution in every page comes from the change's signing key, not from who held the token that pushed it. A change signed by an unlinked key shows its KeyId and "unlinked key" rather than a user name.
172 +
173 +
An identity can be linked to at most one account. Rotation (`oxy identity rotate`) produces a new identity whose object names the old key; the hub accepts the rotated identity for the same account without a new challenge when the rotation is signed by the old key.
174 +
175 +
### 5.4 Authentication for the wire protocol
176 +
177 +
`push` requires one of:
178 +
179 +
1. `Authorization: Bearer <token>` with a personal access token holding `repo:write` on that repository;
180 +
2. an HTTP signature (spec §15.1): the client signs a server challenge with its identity key and the key is linked to an account with `write`. Reference implementation support is a §18 item; until then tokens are the only method.
181 +
182 +
Reads on `public` repositories are anonymous. Reads on `internal` and `private` repositories require the same credentials with `repo:read`.
183 +
184 +
### 5.5 Two authorities, one answer
185 +
186 +
The hub's roles decide who may reach a repository, open issues, create proposals, edit settings, and *attempt* a push. The repository's Oxygen trust policy (spec §14.5) decides which keys may update which references and whether conflicts may land on them. Both must agree for a push to succeed; the hub MUST NOT bypass or reimplement policy, and it MUST run the same `receive_push` validation `oxy-core` runs everywhere else.
187 +
188 +
A repository with no policy accepts any reference update from any account with `write`. The settings page says so plainly and offers to create a policy. Policy editing in the browser produces a **policy proposal**: a signed policy object that roots sign with `oxy policy sign` from their own clients; the hub collects signatures until the threshold is met, then applies the update as an ordinary push. The hub never holds a root key.
189 +
190 +
The hub has one identity of its own, the **hub delegate key**, generated at `init` and published at `/.well-known/oxyhub-key`. It is used only to sign changes the hub itself creates on behalf of a signed-in user (web edits, §6.9; proposal landings, §7.6). Such a change carries the user's linked key as `author` and the hub key as signer, which the author-or-delegate rule (spec §14.4) accepts only when the user's identity object lists the hub key as a delegate: the user opts in once with `oxy identity delegate add <hub key>` and re-links the rotated identity (§5.3). Policy is then checked exactly as for the user's own signature, since a rule that names the author covers the author's delegates (spec §14.5). If the user has not delegated, or the policy does not authorise them for the reference, the hub refuses the action and shows the client recipe. Delegation is per user and revocable by the user alone (`oxy identity delegate remove`); the hub never needs a policy entry of its own.
191 +
192 +
### 5.6 Personal access tokens
193 +
194 +
Tokens are 32 random bytes, shown once, stored as BLAKE3 hashes, with a name, an optional expiry, and scopes: `repo:read`, `repo:write`, `repo:admin`, `issues:write`, `proposals:write`, `user:read`, `hooks:admin`, optionally restricted to listed repositories. Tokens authenticate the API and the wire protocol; they never authenticate the pages.
195 +
196 +
## 6. Web interface
197 +
198 +
### 6.1 Theme
199 +
200 +
The visual language is the RetroPlay Network's: dark by default, the Commander X16 palette, `Barlow` for text and `Press Start 2P` for the wordmark, ranks and first columns, square corners, thin `--line` rules, a `--panel` header and footer, and a full-width `--band` navigation bar with the active tab underlined in `--acc`. The stylesheet starts from `retroplay-manager/templates/style.css` lines 1–171 (tokens, shell, band, tabs, main, tables, forms, footer) and adds forge components; the store, showcase and game components are not carried over.
201 +
202 +
Tokens (dark default, light under `:root[data-theme="light"]`):
203 +
204 +
```
205 +
--bg  --panel  --soft  --ink  --mute  --line  --dim
206 +
--acc  --band  --band-ink  --band-line  --now  --now-ink
207 +
--ok  --bad  --warn (new: #ffb347 dark / #b8651b light)
208 +
--add  --del  --add-ink  --del-ink   (new: diff backgrounds and text)
209 +
--m1 … --m5   (kept for signature status colours: root, signed, sealed, unsigned, invalid)
210 +
```
211 +
212 +
Theme selection: cookie `theme=light|dark|auto`; `auto` follows `prefers-color-scheme`. The `<head>` sets `data-theme` before paint exactly as retroplay-manager does. Fonts are vendored under `/static/fonts/` rather than loaded from Google, so a hub on a private network paints without internet access.
213 +
214 +
The wordmark is `OXY<span>◆</span>GEN` in the pixel font with the diamond in `--acc`. Favicon: a 16×16 pixel-art "O" in amber on `--panel`, matching `retroplay.svg`'s style.
215 +
216 +
### 6.2 Page shell
217 +
218 +
`header` (wordmark, global search box, "+" menu for new repository/issue/proposal, notifications bell with unread count, avatar menu) · `.band` (Repositories · Explore · Proposals · Issues · Notifications, with admin links pushed right) · repository `.tabs` (Code · Changes · Proposals · Issues · Releases · Trust · Settings, each with a count pill) · `main` at 1180px max (wider than retroplay's 980 because diffs need it; tree and file pages use 100% with 22px gutters) · `footer`.
219 +
220 +
Every page has a `<title>` of the form `<thing> · <owner>/<name> · Oxygenhub`.
221 +
222 +
### 6.3 Pages
223 +
224 +
| Path | Page |
225 +
|---|---|
226 +
| `/` | Signed in: dashboard (your repositories, proposals awaiting your review, assigned issues, recent activity). Anonymous: explore. |
227 +
| `/explore` | Public repositories, sorted by activity; language and topic filters |
228 +
| `/{owner}` | Profile or organisation: repositories, linked keys with fingerprints, activity |
229 +
| `/{owner}/{name}` | Repository overview (§6.4) |
230 +
| `/{owner}/{name}/tree/{ref}/{path}` | Directory listing at a bookmark, tag or change (§6.5) |
231 +
| `/{owner}/{name}/file/{ref}/{path}` | File view, `?plain=1` raw, `?blame=1` blame (§6.6) |
232 +
| `/{owner}/{name}/changes/{ref}` | History (§6.7) |
233 +
| `/{owner}/{name}/change/{hash}` | Change page (§6.8) |
234 +
| `/{owner}/{name}/compare/{a}...{b}` | Changes in `b` not in `a`, and the diff between the two states |
235 +
| `/{owner}/{name}/bookmarks` | Bookmarks with heads, ahead/behind the default, last update, attestation status |
236 +
| `/{owner}/{name}/conflicts` | Every bookmark whose state carries conflicts, each with its regions (§6.10) |
237 +
| `/{owner}/{name}/proposals`, `/proposal/{n}` | §7 |
238 +
| `/{owner}/{name}/issues`, `/issue/{n}` | §8 |
239 +
| `/{owner}/{name}/releases`, `/tags` | §9 |
240 +
| `/{owner}/{name}/trust` | Policy, roots, rules, hub delegate status, verification summary (§6.11) |
241 +
| `/{owner}/{name}/pushes` | Reference update log: who pushed what, when, with which key, result (§6.12) |
242 +
| `/{owner}/{name}/settings/*` | General, collaborators, bookmarks (default, delete), tokens, webhooks, mirror, danger zone |
243 +
| `/{owner}/{name}/edit/{ref}/{path}`, `/new/{ref}/{path}` | Web edit (§6.9) |
244 +
| `/settings/*` | Profile, security (password, TOTP, sessions), keys (linked identities), tokens, notifications, theme |
245 +
| `/notifications` | Inbox (§10) |
246 +
| `/search` | §11 |
247 +
| `/admin/*` | Site: accounts, registration mode, storage, jobs, audit log |
248 +
249 +
### 6.4 Repository overview
250 +
251 +
The default bookmark's tree at the root, the rendered `README.md` (or `README`, `README.txt`) below it, and a right column: description and topics, clone URL with copy button (`oxy clone https://…/{owner}/{name}`), heads count with a link when more than one, bookmarks, tags, latest release, collaborators, license detected from `LICENSE`, and the trust badge: **policy**, **no policy**, or **policy expired**.
252 +
253 +
A repository whose default bookmark has more than one head shows a `--warn` strip: "main has 3 heads; the tree below is their union" with a link to the bookmarks page. Conflicts in that state show a `--bad` strip linking to the conflicts page.
254 +
255 +
### 6.5 Tree
256 +
257 +
Directory rows show name, the last change touching the entry (message and relative time, from `log_for_path`, computed lazily per row over the API so a large directory paints immediately), size, and mode glyphs (executable, symlink, subrepository). A subrepository row links to its pin: another hub repository at a state, or a Git URL at a SHA.
258 +
259 +
### 6.6 File view and blame
260 +
261 +
Text files render with syntect line highlighting, line numbers as anchors (`#L12`, `#L12-L20` selects a range), a "raw", "history", "blame", "edit" toolbar, and the file's last change. Markdown renders by default with a toggle to source. Images and PDFs render inline; other binaries offer download. Files above `render_limit` (default 2 MiB) show the first part and a download link.
262 +
263 +
Blame uses `Repo::blame`: every line is exact, because each line is an atom with one authoring change. The gutter shows the change's short hash, author and age, grouped into runs, with the change's signature glyph. Blame is exact through rebase and squash, so there is no "ignore revs" file.
264 +
265 +
### 6.7 History
266 +
267 +
A dependency graph, not a parent chain. The default view lists the bookmark's changes newest first with lanes drawn between a change and its dependencies (the same layout algorithm as the VS Code extension's webview, ported to Rust and rendered server side as SVG). Heads are marked. Filters: author, path, date, signed/unsigned, message text. Each row: short hash, message first line, author (name, or KeyId when unlinked), relative time, signature glyph, bookmark and tag chips, a conflict chip when the change introduces one.
268 +
269 +
`?path=` gives the history of one path.
270 +
271 +
### 6.8 Change page
272 +
273 +
Header: full hash, message, author identity (linked account or KeyId), timestamp, signature status with the policy rule that admitted it (or "unsigned", or "invalid"), seals covering it, the bookmarks and tags containing it. Body: dependencies and dependents as chips, the change's edits as a diff (unified or split, per-user preference), atom counts, and the raw change (`oxy show --json`) behind a details block.
274 +
275 +
Actions for `write`: open a proposal containing this change, tag it, copy hash. Describing, abandoning, squashing and rebasing are client operations and are not offered here.
276 +
277 +
### 6.9 Web edit
278 +
279 +
`CodeMirror 6` with the file's language mode. Saving creates a change with the given message signed by the hub delegate on behalf of the user (§5.5) and adds it to the chosen bookmark, or to a new bookmark plus a proposal, exactly as GitHub's "propose changes" flow. The hub refuses with a clear message when the user has not delegated to the hub key (§5.5) or the policy does not authorise them for the target. Binary files and files over `render_limit` cannot be edited in the browser.
280 +
281 +
### 6.10 Conflicts page
282 +
283 +
For each bookmark whose state has conflicts: the file (or tree entry), the conflict kind, and each side with the change that produced it and its content, rendered with the same marker structure `oxy checkout` writes. A "resolve in browser" action opens the file's conflict markers in the editor (§6.9) and commits a resolution change through the delegate. Otherwise the page gives the two-line client recipe.
284 +
285 +
### 6.11 Trust page
286 +
287 +
The current policy (serial, expiry, roots, threshold, rules with their patterns, thresholds and `allow_conflicts`), the policy history, the hub delegate key with its fingerprint and which collaborators have delegated to it, every linked account's keys that the policy names, revoked keys, and a verification summary computed from `oxy verify`: counts of signed, sealed, unsigned and invalid changes, with a list of invalid ones. Policy proposals awaiting root signatures are listed with who has signed and a download of the proposal file.
288 +
289 +
### 6.12 Pushes page
290 +
291 +
The hub keeps its own log of every reference update it accepted or refused: time, account, key, reference, old and new state, force flag, the result and reason. This is the answer to "who moved main and when" and is the hub's audit trail for history; it is not the repository's operation log, which is per workspace.
292 +
293 +
### 6.13 Keyboard and accessibility
294 +
295 +
`/` focuses search, `g c` / `g p` / `g i` jump to Code, Proposals, Issues, `?` shows the sheet. Every interactive element is reachable by keyboard; colour is never the only carrier of meaning (glyphs accompany every status colour); contrast meets WCAG AA in both themes.
296 +
297 +
## 7. Proposals and review
298 +
299 +
### 7.1 Definition
300 +
301 +
A **proposal** is a request to add a set of changes to a target bookmark of a repository. It is identified by a per-repository number, like a pull request. Its source is either a bookmark of the same repository or of a fork, in which case the proposal's changes are those in the source bookmark's state not in the target's; or an explicit list of change hashes (from the change page or the API).
302 +
303 +
A proposal records: number, title, description (Markdown), author, target bookmark, source (bookmark and repository, or explicit hashes), state (`draft`, `open`, `landed`, `closed`), reviewers requested, labels, and the **projected state** (§7.2).
304 +
305 +
### 7.2 Projected state
306 +
307 +
Whenever the proposal or its target changes, a job computes the projected state: the union of the target's heads and the proposal's changes, loaded through `oxy-core` exactly as a client's `oxy merge` would. From it the page shows:
308 +
309 +
- the changes to be added, in dependency order, with signature status;
310 +
- the diff between the target's state and the projected state;
311 +
- **conflicts** the landing would reify, each with its sides, in the same rendering as §6.10;
312 +
- files touched, and check results for the projected state hash (§13).
313 +
314 +
Because changes commute, a projected state is stale only when the target has moved; the job then recomputes it. There is no "update branch" button because there is nothing to update. If the recomputation shows a new conflict, the proposal is marked `conflicted` and the author and reviewers are notified.
315 +
316 +
### 7.3 Review comments
317 +
318 +
A comment is attached to the proposal (general), to a file, or to an **atom**: `(ChangeHash, index)` plus the change that introduced the line. Rendering places it at the atom's current position in the projected state; if the atom is gone (deleted by a later change in the proposal), the comment shows in the file's "resolved lines" section rather than disappearing. When the author squashes or rebases, the correspondence the rewrite recorded maps the old atom to the new one, and the comment follows. Comments are Markdown (EasyMDE), threaded, and resolvable.
319 +
320 +
### 7.4 Reviews and approvals
321 +
322 +
A review is `comment`, `request changes`, or `approve`. An approval MUST be backed by a **seal attestation** (spec §14.4, `kind = seal`) over the proposal's change hashes, signed with a key linked to the reviewer's account. Private keys never enter the browser, so the reviewer signs from a client: `oxy seal --proposal <url>` or `oxy seal <hashes>` (§18, a new command; the seal object already exists in the model) signs and pushes the seal; the hub matches the seal to the proposal and records the approval. The page shows the command ready to copy. An approval not yet backed by a seal is shown as "approval pending signature" and does not count towards requirements.
323 +
324 +
Landing requirements per repository (settings): N approvals, approvals from code owners (`.oxyowners` in the target state, GitHub `CODEOWNERS` syntax), required checks green on the projected state, no conflicts (or conflicts allowed, matching the policy rule's `allow_conflicts`), and no outstanding "request changes".
325 +
326 +
### 7.5 Stacked proposals
327 +
328 +
A change may depend on changes in another open proposal. The page shows "depends on #12" and lands only after #12 has landed, or lands both when the user chooses "land with dependencies" and holds the rights for each.
329 +
330 +
### 7.6 Landing
331 +
332 +
Landing sets the target bookmark to the projected state's heads: a reference update with a fresh ref attestation. It is performed by:
333 +
334 +
1. the hub delegate key on behalf of the user landing, when that user has delegated to it (§5.5), the policy authorises them for the target, and they hold `write`; or
335 +
2. a client: `oxy proposal land <url>` (§18) fetches the proposal's changes, sets the bookmark locally and pushes with the user's own key. The hub notices the push, sees the proposal's changes are now in the target, and marks it `landed`.
336 +
337 +
The landed state hash is recorded. Nothing is rewritten: the changes keep their hashes, so links to them stay valid and check results carry over. "Squash on land" is offered as a client-side option only, since it rewrites signed changes.
338 +
339 +
### 7.7 Closing and reopening
340 +
341 +
`closed` keeps everything; the source bookmark may be deleted on close if the user asks. A proposal whose changes all appear in the target through another route is marked `landed` automatically with a note.
342 +
343 +
## 8. Issues
344 +
345 +
Per-repository numbered issues sharing the number space with proposals (as GitHub does), with title, Markdown body, author, assignees, labels (name, colour, description), a milestone (title, due date, description, open/closed), state (`open`, `closed` with reason `completed` or `not planned`), comments, lock, and pin. Issue templates come from `.oxyhub/ISSUE_TEMPLATE/*.md` in the default state.
346 +
347 +
Cross references: `#12` links an issue or proposal; `owner/name#12` across repositories; a 12-hex-or-longer token links a change when it resolves; `closes #12` in a change message closes the issue when the change lands on the default bookmark. Every reference creates a timeline entry on the target.
348 +
349 +
Import from GitHub or GitLab is a 1.x item.
350 +
351 +
## 9. Releases and tags
352 +
353 +
Tags come from the repository (spec §10): a name, the state, and a signed attestation when the tagger signed. The tags page lists them with signature status. A release attaches to a tag: notes (Markdown, generated draft from the changes since the previous release), assets (uploaded files, content addressed, size limited by `asset_limit`), a pre-release flag, and a latest marker. Source archives (`.tar.zst`, `.zip`) of the tagged state are generated on demand and cached.
354 +
355 +
## 10. Notifications
356 +
357 +
Events: proposal opened, review requested, review submitted, comment on a thread you are in, mention (`@name`), landing, check failed on your proposal, issue assigned, policy proposal awaiting your signature, push refused. Each becomes an inbox item (`/notifications`, with unread count in the header) and, when the account chooses, an email through `lettre` with a per-account SMTP-independent unsubscribe link. Watching a repository subscribes to all its events; participating in a thread subscribes to it.
358 +
359 +
## 11. Search
360 +
361 +
Global search box with scopes: repositories (name, description, topics), issues and proposals (FTS5 over title, body and comments), changes (message FTS5 over the default bookmark's state, indexed on push), accounts, and code. Code search in 1.0 is a per-repository substring and regular-expression search over the default state's text files, run with a time budget and a result cap, cached per state hash. A trigram index is a 1.x item.
362 +
363 +
## 12. HTTP API and webhooks
364 +
365 +
### 12.1 API
366 +
367 +
JSON under `/api/v1`, bearer tokens, `application/json`, RFC 7807 problem responses, cursor pagination (`?after=`), ETags on cacheable resources, and rate limits per token. Resources mirror the pages: `/repos/{owner}/{name}`, `/bookmarks`, `/changes/{hash}`, `/tree/{ref}/{path}`, `/file/{ref}/{path}`, `/blame`, `/history`, `/compare`, `/conflicts`, `/proposals`, `/proposals/{n}/comments|reviews|checks|land`, `/issues`, `/labels`, `/milestones`, `/releases`, `/tags`, `/trust`, `/pushes`, `/hooks`, `/checks`, `/user`, `/users/{name}`, `/orgs/{name}`, `/search`. Shapes reuse `oxy --json` shapes wherever one exists (change, status, blame, diff) so tools written against the CLI work against the hub. `crates/hub-api` holds the types with `serde` derives and is what the runner and the VS Code extension will link.
368 +
369 +
### 12.2 Webhooks
370 +
371 +
Per repository and per organisation: a URL, a secret, an event selection, active flag, and delivery log with redelivery. Payloads are JSON with `X-Oxyhub-Event`, `X-Oxyhub-Delivery`, and `X-Oxyhub-Signature: blake3-keyed=<hex>` computed as keyed BLAKE3 over the body. Events: `push` (reference updated: old and new state, heads, changes), `proposal` (opened, edited, review requested, landed, closed, conflicted), `review`, `comment`, `issue`, `release`, `check`, `policy`. Deliveries retry with backoff up to 24 hours.
372 +
373 +
## 13. Checks and the runner
374 +
375 +
### 13.1 Checks
376 +
377 +
A **check** is a result attached to a state hash: name, status (`queued`, `running`, `passed`, `failed`, `cancelled`, `skipped`), a summary, a URL, timing, and an optional signed **seal** from the checker's key over the changes in that state, which makes a passed check a verifiable object. Checks are created through the API (`POST /repos/{o}/{n}/checks`) by any token with `checks:write`, so any CI system can report, and required checks for landing (§7.4) are matched by name against the projected state's hash. The proposal page streams check status updates over server-sent events.
378 +
379 +
Because a state is content addressed, the hub deduplicates: a check that passed for a state hash is shown for every proposal and bookmark that reaches that same state.
380 +
381 +
### 13.2 Oxygen Runner (later; `docs/runner.md`)
382 +
383 +
The runner is a separate Rust program and a separate specification, not required for hub 1.0. The hub side is only §13.1 plus a `runner` scope for tokens and a job-claim API. The runner's design principles, recorded here so the hub does not close doors:
384 +
385 +
1. **Pipelines are typed, validated documents, not templated YAML.** The definition lives in the repository (`.oxy/ci/*.kdl`), has a schema, and is validated by `oxyrun check` and by the hub on push before anything runs. Errors point at a line. There is no string templating stage; parameters are typed values.
386 +
2. **Same execution locally and remotely.** `oxyrun` runs the pipeline on a laptop in the same OCI container image with the same inputs and produces the same result. "Works on CI but not here" is a bug in the runner.
387 +
3. **Content-addressed steps and images.** A step references an action by an Oxygen state hash or a Git SHA, never a mutable tag; images by digest. The hub refuses a definition that pins nothing. Actions are ordinary Oxygen repositories with signed tags, so `uses` is verifiable.
388 +
4. **Proposals from outsiders run with no secrets.** There is no `pull_request_target`. Secrets are bound to named environments; an environment lists who may deploy to it and whether a proposal may use it, and using one from a proposal needs an explicit approval by a maintainer on that projected state.
389 +
5. **Results are seals.** A green job is a seal over the state's changes signed by the runner's key, admitted by policy as a checker. It is the same object §13.1 stores, verifiable by any client offline.
390 +
6. **The state is the unit, not the commit.** A job runs on a state hash and is cached by it. Two proposals reaching the same state run once.
391 +
7. **Structured logs, streamed and kept.** Log lines carry step, timestamp and level; the UI groups and folds them; retention is configurable; artefacts are content addressed.
392 +
8. **Caches keyed by content.** Cache entries are keyed by the BLAKE3 of the declared inputs, never by a hand-written key string.
393 +
9. **No implicit shell.** Each step declares its program and arguments; a `sh` step is explicit about the shell and `set -eu` is on by default.
394 +
10. **Runners are cattle.** A runner registers with a token, claims jobs over a long-poll API, runs each in a fresh container or VM, and reports; no state survives a job. Runner groups and labels select where jobs run.
395 +
396 +
## 14. Git interoperability
397 +
398 +
A repository may enable a **Git mirror**: on every landing on selected bookmarks the hub runs `oxy git export` into `<data>/mirrors/<owner>/<name>.git` and serves it read-only over Git's smart HTTP protocol at `/{owner}/{name}.git` (implemented with the `git` binary's `http-backend` behind the hub's authorization, since reimplementing pack negotiation is not the product). This lets GitHub Actions runners, IDEs and tooling that only speak Git read hosted Oxygen repositories. Pushing to a mirror is refused with a message pointing at `oxy`.
399 +
400 +
Import from Git: `oxy git import` at repository creation from an uploaded bundle or a URL, keeping the mapping so later `oxy git fetch` keeps the imported bookmark following the Git branch.
401 +
402 +
## 15. Security
403 +
404 +
### 15.1 Threat model
405 +
406 +
An anonymous internet user, a signed-in user attempting to exceed their role, a collaborator with `write` attempting to move a reference the policy forbids, a malicious repository content author (Markdown, file names, huge objects), and a compromised hub host. The last is bounded by design: the hub holds no root keys, approvals are user-signed seals, history is signed by authors, so a compromised hub can deny service and forge hub-only data (issues, comments) but cannot forge history, approvals or policy without detection.
407 +
408 +
### 15.2 Web
409 +
410 +
CSRF: every unsafe form carries a token bound to the session (`HMAC(cookie_key, "csrf:" || session mac)`), and the `Origin` header is checked when present. Cookies are `HttpOnly`, `SameSite=Lax`, `Secure` over HTTPS. Content Security Policy: `default-src 'self'`, no inline scripts except the theme snippet by hash, `img-src 'self' data:`; rendered Markdown is sanitised by ammonia with an allow-list; rendered HTML files are never served inline (raw file responses use `Content-Disposition: attachment` and `X-Content-Type-Options: nosniff`) except images with sniffed, allow-listed types. Path parameters are validated before touching the file system; paths inside repositories are resolved against the object store, never the disk.
411 +
412 +
### 15.3 Passwords, sessions, tokens
413 +
414 +
Argon2id with the defaults of the `argon2` crate, per-password salt; TOTP with a 30-second step and one-step drift; recovery codes; sessions as in §5.1; tokens stored as BLAKE3 hashes and compared in constant time; login rate limited per account and per address; all session-changing actions logged in the audit log.
415 +
416 +
### 15.4 Repository content
417 +
418 +
Object sizes are bounded by the wire protocol server's body limits (per push and per object); rendering is bounded by `render_limit` and by time budgets; archives are streamed; symlinks in trees are shown, never followed on the server; subrepository pins are displayed, never fetched by the hub.
419 +
420 +
### 15.5 Keys and TLS
421 +
422 +
The hub delegate key lives in `<data>/keys/` with mode `0600`. Its identity object is published at `/.well-known/oxyhub-key` so policy holders can admit it. TLS: the hub serves HTTPS itself with the configured certificate, or plain HTTP behind a terminating proxy, or on the developer edition a self-signed certificate it generates at `init` (with `rcgen`) whose CA is downloadable from the trust page and usable with `OXY_CA`.
423 +
424 +
### 15.6 Rate limits and abuse
425 +
426 +
Per address for anonymous requests, per account and per token otherwise, with separate budgets for pages, API, search and the wire protocol; registration and login have their own. Administrators can ban accounts and block addresses.
427 +
428 +
## 16. Performance and caching
429 +
430 +
### 16.1 Targets
431 +
432 +
Repository overview and tree pages in under 100 ms on a warm hub for a repository of 100 000 changes; a change page in under 200 ms; a projected state for a 50-change proposal in under 2 s; wire protocol throughput limited by disk and network, not the hub.
433 +
434 +
### 16.2 Locks and blocking
435 +
436 +
Every `oxy-core` call runs on the blocking pool. Each repository has a `tokio::sync::RwLock`: pushes, landings, web edits and mirror exports take the write lock; page reads take the read lock. `oxy-core` itself is single-threaded per `Repo`, so a `Repo` is opened per request and dropped after; store handles are cheap to open because packs are memory mapped.
437 +
438 +
### 16.3 Caches
439 +
440 +
Keyed by state hash, so they never go stale: loaded `State` objects (LRU by memory), tree listings, rendered file views, blame, history graph layouts per bookmark head set, projected states per proposal, archives, code search results. Keyed by object hash: rendered Markdown, syntax highlighted files. Keyed by repository: bookmark and tag lists, invalidated by the push path. `ETag` and `Cache-Control: private, max-age=0, must-revalidate` on pages; `immutable` on content-addressed responses (raw files by hash, archives by state).
441 +
442 +
### 16.4 Large repositories
443 +
444 +
History pages paginate by cursor; the graph renders a window of 100 changes with lazy extension; `log_for_path` for tree rows is fetched per row after the page paints and cached per (state, path). The projected state job uses `load_state` on the target and applies only the proposal's changes on top.
445 +
446 +
## 17. Operations: configuration, storage, backup
447 +
448 +
### 17.1 Configuration
449 +
450 +
`hub.toml` in the data directory; every key overridable by `OXYHUB_<SECTION>_<KEY>`:
451 +
452 +
```toml
453 +
[server]
454 +
bind = "0.0.0.0:7300"          # plain HTTP
455 +
tls_bind = "0.0.0.0:7343"      # HTTPS, when tls.cert and tls.key (or self_signed) are set
456 +
base_url = "https://earth.andymccall.xyz:7343"
457 +
behind_proxy = false            # trust X-Forwarded-* when true
458 +
459 +
[tls]
460 +
self_signed = true              # developer edition; otherwise cert = "...", key = "..."
461 +
462 +
[registration]
463 +
mode = "invite"                 # open | invite | closed
464 +
465 +
[limits]
466 +
render_limit = "2MiB"
467 +
asset_limit = "512MiB"
468 +
push_body_limit = "4GiB"
469 +
470 +
[email]
471 +
smtp = "smtp://…"               # optional
472 +
from = "Oxygenhub <hub@example>"
473 +
474 +
[mirror]
475 +
git = "/usr/bin/git"
476 +
477 +
[retention]
478 +
trash_days = 30
479 +
webhook_log_days = 30
480 +
```
481 +
482 +
### 17.2 Storage
483 +
484 +
SQLite in WAL mode; migrations embedded and applied at start; `PRAGMA user_version` guards downgrades. Repositories are ordinary Oxygen repositories; `oxy gc` runs per repository as a nightly job with the retention the repository settings choose.
485 +
486 +
### 17.3 Command line
487 +
488 +
```
489 +
oxygenhub init  --data DIR [--admin NAME]      create the data directory, keys, database, first admin
490 +
oxygenhub serve --data DIR                     run
491 +
oxygenhub admin --data DIR user|repo|token …   offline administration
492 +
oxygenhub backup --data DIR --to FILE          consistent snapshot: sqlite backup API plus repos and assets as tar.zst
493 +
oxygenhub check --data DIR                     verify every repository's objects and signatures, report
494 +
```
495 +
496 +
### 17.4 Backup and restore
497 +
498 +
`backup` uses SQLite's online backup API for the database and copies `repos/`, `assets/`, `keys/` and `hub.toml`; `cache/` is excluded. Restore is extraction plus `serve`. Because objects are content addressed, a repository directory copied while a push is in flight is still valid up to its last committed reference update.
499 +
500 +
### 17.5 Developer edition on Earth
501 +
502 +
Earth (`earth.andymccall.xyz`, Ubuntu 26.04, x86_64, 4 cores, 31 GiB, Docker, ports 22, 6400, 6670, 7842, 8080 in use) runs the developer edition as a systemd service under a dedicated `oxygenhub` system user with `<data> = /home/oxygenhub/data`, HTTP on 7300 and HTTPS on 7343 with a self-signed certificate. Source is synchronised from the development machine with `rsync` (the Oxygen workspace and the hub) and built on Earth with a rustup toolchain installed for the `claude` account; `docs/deploy.md` gives the exact steps. Once the hub runs, `oxygen`, `oxygen-vscode` and `oxygenhub` are pushed to it as its first repositories and become their own remotes; that fulfils the self-hosting rule without any Git involvement.
503 +
504 +
## 18. Required changes to Oxygen
505 +
506 +
Each is small and lives in the Oxygen workspace, tracked in its `docs/plan.md`:
507 +
508 +
1. **Bare repositories.** `Repo::open_bare(dir)`: open a `.oxy/` with no workspace; refuse working-copy operations with a clear error. `oxy init --bare`.
509 +
2. **Server embedding.** `oxy-server` exposes `router_with(resolver, authorizer)` where the resolver maps a path (possibly `owner/name`) to a repository directory and the authorizer decides read and push per request, so the hub embeds it instead of copying handlers. Repository names with one `/` allowed when the resolver permits.
510 +
3. **Challenge signing.** `oxy identity sign-challenge <bytes>` and `oxy_core::identity::verify_challenge`, for linking keys (§5.3) and HTTP signature authentication (§5.4).
511 +
4. **Seal command.** `oxy seal <hash>..` signs a seal attestation over the given changes with the identity key and pushes it on the next push (the seal object and its verification exist, spec §14.4; today only the sync path creates seals). `oxy seal --proposal <url>` fetches the hash list from the hub API first.
512 +
5. **Landing from a client.** `oxy proposal land <url>` and `oxy proposal show <url>` as thin API clients.
513 +
6. **Delegate signing of hub-created changes.** A library entry point to build and sign a change from an edit set with `author = user key, signer = hub key`, without a workspace (used by web edits, browser conflict resolution and landings). `oxy identity delegate add|remove` already exists.
514 +
7. **History for the hub.** `Repo::log_window(heads, cursor, limit)` for paginated history, and a `graph::layout` module shared with the CLI so both draw the same lanes.
515 +
8. **Push hook.** `receive_push` reports, per accepted reference, the old and new state and the list of newly stored changes, for the pushes log and webhooks.
516 +
9. **Machine interface.** `oxy verify --json` already exists; add `oxy policy propose` output as a downloadable proposal file (it exists as a file format, spec §14.5) and `oxy tag list --json` is already present.
517 +
518 +
## 19. Milestones
519 +
520 +
| | Delivers | Done when |
521 +
|---|---|---|
522 +
| **0.1 Browse** | Data directory, config, SQLite, embedded wire server, repository creation by `oxy push`, theme and shell, overview, tree, file, raw, Markdown, history, change page, compare, bookmarks, tags, blame, conflicts page, trust page (read-only) | `oxygen`, `oxygen-vscode` and `oxygenhub` are hosted on Earth and browsable; `oxy clone` and `oxy push` work against it |
523 +
| **0.2 Accounts and access** | Registration, sessions, TOTP, profile, organisations and teams, roles, visibility, tokens, linked identities with challenge, pushes log, settings pages, audit log | Pushes are attributed to linked keys; a private repository is invisible to outsiders |
524 +
| **0.3 Proposals** | Proposals from bookmarks and forks, projected state, diff, conflicts, atom-anchored comments, reviews with seal-backed approvals, requirements, landing by delegate or client, stacking | A change proposed, reviewed with a comment that survives a rebase, sealed, and landed, all from the pages plus `oxy seal` |
525 +
| **0.4 Issues and notifications** | Issues, labels, milestones, cross references, templates, inbox, email, watching, mentions, search over issues and proposals | — |
526 +
| **0.5 API, webhooks, checks** | `/api/v1` complete for every page, `hub-api` crate, webhooks with delivery log, checks with seals, required checks, SSE status | The VS Code extension can list proposals and checks through the API |
527 +
| **0.6 Web edits, releases, mirror** | CodeMirror editing and new file with delegate signing, browser conflict resolution, releases with assets and archives, Git mirror over smart HTTP, code search | A GitHub Actions job can clone a hosted repository over the mirror |
528 +
| **0.7 Runner** | `docs/runner.md`, `oxyrun`, job-claim API, seals from the runner | A pipeline in `oxygen` builds and tests on Earth and its result appears as a required check |
529 +
| **1.0** | Passkeys, backup and restore verified, load test at the §16.1 targets, security review of §15, documentation, packaged release (binary, container image, systemd unit) | — |
530 +
531 +
## 20. Open questions
532 +
533 +
1. **Seal signing UX.** Requiring `oxy seal` from a client for every approval is honest but slower than a button. A browser-held signing key (WebAuthn `prf` extension deriving an Ed25519 key) could sign approvals in the browser with hardware backing; ML-DSA-65 in the browser is heavier. Decide after 0.3 ships with the client flow.
534 +
2. **Delegate key custody.** One hub key delegated to by many users means one key whose compromise lets an attacker sign as any of them (still subject to policy and to each user's revocation). Per-user delegate keys held by the hub would narrow that blast radius at the cost of a key per user. Decide before 0.6.
535 +
3. **Postgres.** SQLite is enough for one host; an organisation-scale hub may want Postgres. `sqlx` keeps the option open, but every query would need to be dialect-clean. Decide when someone needs it.
536 +
4. **Comment anchoring for lines that are not atoms.** Comments on deleted lines and on binary files fall back to file anchors; is that good enough?
537 +
5. **Pipeline language.** KDL is the current choice for `.oxy/ci`; Starlark would give loops and functions at the cost of a real evaluator. Settle in `docs/runner.md`.
538 +
6. **Federation.** Proposals across hubs (a fork on another hub) are natural in a content-addressed model and out of scope for 1.0.
539 +
540 +
## 21. Changelog
541 +
542 +
- Draft 0.1 (2026-09-27): initial specification.