OXYGENHUB

The site: a static product page, built by OxygenFlow, pulled by its host (refs #1) d239f0850b00

One page and a 404 in hand-written HTML and one stylesheet (the Oxygen
palette, dark by default, light on request; system fonts; no scripts, no
third-party requests, which the build refuses). scripts/build.sh fills in
the shared header and footer with sh and awk and packs site.tar.gz; the
pipeline keeps it as an artifact with signed provenance. deploy/ holds the
pull agent (newest passing run of main, checked against its provenance,
swapped in by one rename, five kept) and the user services that serve it
with busybox httpd on 127.0.0.1:7410 behind the tunnel.
Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key a411fb3e5163
When
· 2026-10-26 12:36 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
d239f0850b0050dc98b75a4e8e09ae0419de5f59eec2aae52822fb709179f4a4
Depends on
d789eb005ce5
Depended on by
nothing yet
In bookmarks
main
Receipts
✔ site passed in 718 ms · signed by earth-1 job 11068 · download
Edits
16 atoms, 20 tree operations, 15 files

Browse the tree at this change

added .flow/pipeline.kdl +25 −0
1 +
// oxygenvcs.com's pipeline (OxygenFlow): every push builds the static
2 +
// site and keeps it as an artifact, with a signed provenance statement.
3 +
// Nothing deploys from here: the host serving the site pulls the newest
4 +
// passing build of main itself (deploy/pull.sh, oxygenvcs-web#1).
5 +
schema-version 1
6 +
7 +
// The OxygenFlow build image (oxygenflow/images/rust); the build needs
8 +
// only sh, awk and tar, which it has. No network.
9 +
sandbox "site" {
10 +
    runtime "microvm"
11 +
    rootfs "cas://blob/836461b613019f62399a960c61a25c482eb74e30054ff2cd44d5594c3305872b"
12 +
    memory-limit "1GiB"
13 +
    cpu-limit 1
14 +
    network "isolated"
15 +
}
16 +
17 +
job "site" {
18 +
    sandbox "site"
19 +
    exec "sh" "scripts/build.sh"
20 +
    output {
21 +
        artifact "site.tar.gz" {
22 +
            path "/src/site.tar.gz"
23 +
        }
24 +
    }
25 +
}
added .oxyignore +2 −0
1 +
public/
2 +
site.tar.gz
added deploy/httpd.conf +9 −0
1 +
# busybox httpd for oxygenvcs.com: the site's own 404 page, and types it
2 +
# serves (paths are relative to the live release).
3 +
E404:404.html
4 +
.html:text/html; charset=utf-8
5 +
.css:text/css; charset=utf-8
6 +
.svg:image/svg+xml
7 +
.png:image/png
8 +
.ico:image/x-icon
9 +
.txt:text/plain; charset=utf-8
added deploy/oxygenvcs-pull.service +9 −0
1 +
# Pull the newest passing build of oxygenvcs-web's main and make it live.
2 +
[Unit]
3 +
Description=oxygenvcs.com: pull the newest passing build
4 +
5 +
[Service]
6 +
Type=oneshot
7 +
Environment=ROOT=%h/oxygenvcs
8 +
Environment="RELOAD=systemctl --user restart oxygenvcs-web.service"
9 +
ExecStart=%h/oxygenvcs/pull.sh
added deploy/oxygenvcs-pull.timer +9 −0
1 +
[Unit]
2 +
Description=oxygenvcs.com: look for a new build every two minutes
3 +
4 +
[Timer]
5 +
OnBootSec=1min
6 +
OnUnitActiveSec=2min
7 +
8 +
[Install]
9 +
WantedBy=timers.target
added deploy/oxygenvcs-web.service +11 −0
1 +
# The static server for oxygenvcs.com: busybox httpd on loopback, behind
2 +
# the Cloudflare tunnel. A user service (systemctl --user), with lingering.
3 +
[Unit]
4 +
Description=oxygenvcs.com static site (busybox httpd)
5 +
6 +
[Service]
7 +
ExecStart=/usr/bin/busybox httpd -f -p 127.0.0.1:7410 -h %h/oxygenvcs/current -c %h/oxygenvcs/httpd.conf
8 +
Restart=on-failure
9 +
10 +
[Install]
11 +
WantedBy=default.target
added deploy/pull.sh +65 −0
1 +
#!/bin/sh
2 +
# Pull the newest passing build of the site from OxygenHub and make it
3 +
# live (oxygenvcs-web#1). Run by a timer on the host that serves the site
4 +
# (Earth now, a VisionFive 2 Lite later); the host needs no inbound
5 +
# access and no credentials, since the repository is public.
6 +
#
7 +
# Each run: find the newest passing run of main, and if it is not the one
8 +
# live already, download its site.tar.gz, check it against the run's
9 +
# provenance statement (the artifact's SHA-256, this repository, main),
10 +
# unpack it into releases/RUN and point `current` at it in one rename.
11 +
# The last five releases stay for a rollback: `ln -sfn releases/N current`.
12 +
#
13 +
# Needs: sh, curl, python3 (reading JSON), sha256sum, tar.
14 +
set -eu
15 +
HUB=${HUB:-https://oxygenhub.io}
16 +
REPO=${REPO:-andymccall/oxygenvcs-web}
17 +
BOOKMARK=${BOOKMARK:-main}
18 +
ROOT=${ROOT:-/srv/oxygenvcs}
19 +
KEEP=${KEEP:-5}
20 +
# Run after a new release goes live: busybox httpd resolves its document
21 +
# root when it starts, so the service restarts it (see README.md).
22 +
RELOAD=${RELOAD:-}
23 +
24 +
mkdir -p "$ROOT/releases"
25 +
cd "$ROOT"
26 +
json() { python3 -I -c "import json,sys; $1" ; }
27 +
28 +
run=$(curl -fsS "$HUB/api/v1/repos/$REPO/flow/runs?limit=20" | json "
29 +
runs=[r for r in json.load(sys.stdin) if r['bookmark']=='$BOOKMARK' and r['status']=='passed']
30 +
print(runs[0]['number'] if runs else '')")
31 +
[ -n "$run" ] || { echo "no passing run of $BOOKMARK yet"; exit 0; }
32 +
live=$(readlink current 2>/dev/null | sed 's|releases/||' || true)
33 +
[ "$run" = "$live" ] && exit 0
34 +
35 +
art="$HUB/$REPO/flow/$run/artifacts/site/site.tar.gz"
36 +
tmp=$(mktemp -d "$ROOT/.pull.XXXXXX")
37 +
trap 'rm -rf "$tmp"' EXIT
38 +
curl -fsS "$art" -o "$tmp/site.tar.gz"
39 +
curl -fsS "$art/provenance" -o "$tmp/provenance.json"
40 +
got=$(sha256sum "$tmp/site.tar.gz" | cut -d' ' -f1)
41 +
# The statement must name this file's digest, this repository and the
42 +
# bookmark. (Its DSSE signature is the hub's; checking that here is for
43 +
# when oh can verify provenance offline.)
44 +
json "
45 +
import base64
46 +
env=json.load(open('$tmp/provenance.json'))
47 +
st=json.loads(base64.b64decode(env['payload']))
48 +
digests=[s['digest'].get('sha256') for s in st.get('subject',[])]
49 +
p=st['predicate']['buildDefinition']['externalParameters']
50 +
assert '$got' in digests, 'the artifact does not match its provenance'
51 +
assert p['repository']=='$HUB/$REPO', 'provenance names another repository: '+p['repository']
52 +
assert p['bookmark']=='$BOOKMARK', 'provenance names another bookmark: '+p['bookmark']
53 +
" <"$tmp/provenance.json"
54 +
55 +
mkdir "$tmp/site"
56 +
tar -C "$tmp/site" -xzf "$tmp/site.tar.gz"
57 +
[ -f "$tmp/site/index.html" ] || { echo "run $run: no index.html in the site"; exit 1; }
58 +
rm -rf "releases/$run"
59 +
mv "$tmp/site" "releases/$run"
60 +
ln -sfn "releases/$run" current.new
61 +
mv -T current.new current
62 +
echo "live: run $run ($got)"
63 +
[ -z "$RELOAD" ] || sh -c "$RELOAD"
64 +
# Keep the newest few.
65 +
ls -1 releases | sort -n | head -n -"$KEEP" | while read -r old; do rm -rf "releases/$old"; done
added partials/footer.html +14 −0
1 +
<footer class="site-foot">
2 +
  <div class="wrap foot-row">
3 +
    <div class="foot-brand">
4 +
      <img src="/assets/oxygen.svg" width="28" height="28" alt="">
5 +
      <span>Oxygen version control</span>
6 +
    </div>
7 +
    <nav aria-label="Elsewhere">
8 +
      <a href="https://oxygenhub.io">OxygenHub</a>
9 +
      <a href="https://oxygenhub.io/andymccall/oxygen">Source</a>
10 +
      <a href="https://oxygenhub.io/andymccall/oxygen/releases">Releases</a>
11 +
    </nav>
12 +
    <p class="fine">This site is static: no scripts, no cookies, no trackers, no third-party requests. Built by OxygenFlow from <a href="https://oxygenhub.io/andymccall/oxygenvcs-web">its repository</a>.</p>
13 +
  </div>
14 +
</footer>
added partials/header.html +16 −0
1 +
<a class="skip" href="#main">Skip to content</a>
2 +
<header class="site-head">
3 +
  <div class="wrap head-row">
4 +
    <a class="brand" href="/" aria-label="Oxygen, home">
5 +
      <img src="/assets/oxygen.svg" width="36" height="36" alt="">
6 +
      <span>Oxygen</span>
7 +
    </a>
8 +
    <nav aria-label="Site">
9 +
      <a href="/#oxygen">Oxygen</a>
10 +
      <a href="/#hub">OxygenHub</a>
11 +
      <a href="/#flow">OxygenFlow</a>
12 +
      <a href="/#sovereign">Sovereign</a>
13 +
      <a class="nav-cta" href="https://oxygenhub.io">Try OxygenHub</a>
14 +
    </nav>
15 +
  </div>
16 +
</header>
added scripts/build.sh +40 −0
1 +
#!/bin/sh
2 +
# Build the site into public/ and pack it as site.tar.gz: each page in
3 +
# src/ has its `<!-- include header -->` and `<!-- include footer -->`
4 +
# lines replaced by partials/header.html and partials/footer.html;
5 +
# everything else in src/ is copied as it is. Plain sh, awk and tar, so
6 +
# it runs in any image (and on the VisionFive 2 Lite) with nothing to
7 +
# install.
8 +
set -eu
9 +
cd "$(dirname "$0")/.."
10 +
rm -rf public
11 +
mkdir -p public
12 +
(cd src && find . -type f) | while read -r f; do
13 +
    mkdir -p "public/$(dirname "$f")"
14 +
    case "$f" in
15 +
        *.html)
16 +
            awk '
17 +
                /<!-- include header -->/ { while ((getline l < "partials/header.html") > 0) print l; close("partials/header.html"); next }
18 +
                /<!-- include footer -->/ { while ((getline l < "partials/footer.html") > 0) print l; close("partials/footer.html"); next }
19 +
                { print }
20 +
            ' "src/$f" > "public/$f"
21 +
            if grep -q '<!-- include ' "public/$f"; then
22 +
                echo "build: $f names a partial that does not exist" >&2
23 +
                exit 1
24 +
            fi
25 +
            ;;
26 +
        *) cp "src/$f" "public/$f" ;;
27 +
    esac
28 +
done
29 +
# Nothing in the site may load from elsewhere (the footer promises it).
30 +
# Links out are fine; nothing a page loads (src=, <link href=) may be.
31 +
if grep -rniE 'src="(https?:)?//|<link[^>]*href="(https?:)?//' public --include='*.html'; then
32 +
    echo "build: a page loads a resource from another site (above)" >&2
33 +
    exit 1
34 +
fi
35 +
if grep -rniE 'url\((https?:)?//|@import' public --include='*.css'; then
36 +
    echo "build: the stylesheet loads something from another site (above)" >&2
37 +
    exit 1
38 +
fi
39 +
tar -C public -czf site.tar.gz .
40 +
echo "built $(find public -type f | wc -l) files into public/ and site.tar.gz"
added src/404.html +23 −0
1 +
<!doctype html>
2 +
<html lang="en-GB">
3 +
<head>
4 +
<meta charset="utf-8">
5 +
<meta name="viewport" content="width=device-width, initial-scale=1">
6 +
<title>Not found · Oxygen</title>
7 +
<link rel="icon" href="/assets/oxygen.svg" type="image/svg+xml">
8 +
<link rel="stylesheet" href="/assets/site.css">
9 +
</head>
10 +
<body>
11 +
<!-- include header -->
12 +
<main id="main">
13 +
<section class="hero short">
14 +
  <div class="wrap">
15 +
    <p class="eyebrow">404</p>
16 +
    <h1>Nothing here.</h1>
17 +
    <p class="lede">That page does not exist. The <a href="/">home page</a> has everything this site holds; the documentation will live at docs.oxygenvcs.com.</p>
18 +
  </div>
19 +
</section>
20 +
</main>
21 +
<!-- include footer -->
22 +
</body>
23 +
</html>
added src/assets/oxygen.svg +20 −0
1 +
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="100%" height="100%">
2 +
  <defs>
3 +
    <linearGradient id="oxyGrad" gradientUnits="userSpaceOnUse" x1="-22" y1="-11" x2="22" y2="11">
4 +
      <stop offset="0%" stop-color="#00F0FF" />
5 +
      <stop offset="100%" stop-color="#0072FF" />
6 +
    </linearGradient>
7 +
8 +
    <g id="oxy-mark-mini">
9 +
      <path d="M -11 -6 L 11 -6" stroke="url(#oxyGrad)" stroke-width="3" stroke-linecap="round" />
10 +
      <path d="M -11 6 L 11 6" stroke="url(#oxyGrad)" stroke-width="3" stroke-linecap="round" />
11 +
      <circle cx="-11" cy="0" r="10" fill="none" stroke="url(#oxyGrad)" stroke-width="3" />
12 +
      <circle cx="-11" cy="0" r="3.5" fill="#00F0FF" />
13 +
      <circle cx="11" cy="0" r="10" fill="none" stroke="url(#oxyGrad)" stroke-width="3" />
14 +
      <circle cx="11" cy="0" r="3.5" fill="#FFFFFF" />
15 +
    </g>
16 +
  </defs>
17 +
18 +
  <rect width="64" height="64" rx="14" fill="#070c18" />
19 +
  <use href="#oxy-mark-mini" x="32" y="32" />
20 +
</svg>
added src/assets/site.css +131 −0
1 +
/* oxygenvcs.com: one stylesheet, the Oxygen palette (the mark's cyan to
2 +
   blue on near-black), system fonts only, no external requests. Dark by
3 +
   default, light when the visitor's system asks for it. */
4 +
5 +
:root {
6 +
  --bg: #070c18;
7 +
  --bg-2: #0c1426;
8 +
  --panel: #101a30;
9 +
  --line: #1f2c47;
10 +
  --ink: #e6edf7;
11 +
  --mute: #93a1b8;
12 +
  --cyan: #00f0ff;
13 +
  --blue: #0072ff;
14 +
  --link: #5cd6ff;
15 +
  --accent: #00f0ff;
16 +
  --grad: linear-gradient(90deg, var(--cyan), var(--blue));
17 +
  --radius: 14px;
18 +
  --wrap: 1120px;
19 +
  color-scheme: dark;
20 +
}
21 +
22 +
@media (prefers-color-scheme: light) {
23 +
  :root {
24 +
    --bg: #f6f8fc;
25 +
    --bg-2: #ffffff;
26 +
    --panel: #ffffff;
27 +
    --line: #dbe3ef;
28 +
    --ink: #0b1324;
29 +
    --mute: #5a6880;
30 +
    --link: #0059d6;
31 +
    --accent: #0062d6;
32 +
    color-scheme: light;
33 +
  }
34 +
}
35 +
36 +
*, *::before, *::after { box-sizing: border-box; }
37 +
38 +
html { -webkit-text-size-adjust: 100%; }
39 +
40 +
body {
41 +
  margin: 0;
42 +
  background: var(--bg);
43 +
  color: var(--ink);
44 +
  font: 17px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", Arial, sans-serif;
45 +
}
46 +
47 +
a { color: var(--link); text-underline-offset: 3px; }
48 +
a:hover { text-decoration-thickness: 2px; }
49 +
code, pre { font-family: ui-monospace, "SF Mono", "Cascadia Code", "DejaVu Sans Mono", Menlo, monospace; }
50 +
img { max-width: 100%; }
51 +
52 +
.wrap {
53 +
  max-width: var(--wrap);
54 +
  margin: 0 auto;
55 +
  padding-left: max(20px, env(safe-area-inset-left, 0px));
56 +
  padding-right: max(20px, env(safe-area-inset-right, 0px));
57 +
}
58 +
59 +
.skip { position: absolute; left: -999px; }
60 +
.skip:focus { left: 12px; top: 12px; z-index: 10; background: var(--panel); padding: 8px 12px; border-radius: 8px; }
61 +
62 +
/* Header */
63 +
.site-head { border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--bg) 88%, transparent); position: sticky; top: 0; z-index: 5; backdrop-filter: blur(8px); }
64 +
.head-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 64px; flex-wrap: wrap; }
65 +
.brand { display: flex; align-items: center; gap: 10px; color: var(--ink); text-decoration: none; font-weight: 700; font-size: 20px; letter-spacing: .2px; }
66 +
.brand img { border-radius: 9px; }
67 +
.site-head nav { display: flex; align-items: center; gap: 4px 18px; flex-wrap: wrap; }
68 +
.site-head nav a { color: var(--mute); text-decoration: none; font-size: 15px; font-weight: 500; }
69 +
.site-head nav a:hover { color: var(--ink); }
70 +
.site-head nav a.nav-cta { color: var(--ink); border: 1px solid var(--line); padding: 6px 12px; border-radius: 999px; }
71 +
.site-head nav a.nav-cta:hover { border-color: var(--cyan); }
72 +
73 +
/* Sections */
74 +
section { padding-block: 72px; }
75 +
section + section { border-top: 1px solid var(--line); }
76 +
h1, h2, h3 { line-height: 1.15; text-wrap: balance; margin: 0 0 16px; }
77 +
h1 { font-size: clamp(40px, 6vw, 68px); letter-spacing: -1px; }
78 +
h2 { font-size: clamp(28px, 3.4vw, 38px); letter-spacing: -.4px; }
79 +
h3 { font-size: 20px; }
80 +
p { max-width: 68ch; margin: 0 0 16px; }
81 +
.eyebrow { text-transform: uppercase; letter-spacing: .14em; font-size: 13px; font-weight: 600; color: var(--accent); margin-bottom: 18px; }
82 +
.lede { font-size: clamp(18px, 2vw, 21px); color: var(--mute); }
83 +
.grad { background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent; }
84 +
85 +
.hero { padding-block: 96px 80px; background: radial-gradient(900px 420px at 80% -10%, color-mix(in srgb, var(--blue) 22%, transparent), transparent 70%), radial-gradient(700px 360px at 0% 0%, color-mix(in srgb, var(--cyan) 12%, transparent), transparent 70%); }
86 +
.hero.short { padding-block: 120px; }
87 +
.ctas { display: flex; gap: 12px; flex-wrap: wrap; margin: 28px 0 40px; }
88 +
.btn { display: inline-block; padding: 12px 20px; border-radius: 10px; border: 1px solid var(--line); color: var(--ink); text-decoration: none; font-weight: 600; }
89 +
.btn:hover { border-color: var(--cyan); }
90 +
.btn.primary { background: var(--grad); border-color: transparent; color: #04101f; }
91 +
.btn.primary:hover { filter: brightness(1.08); }
92 +
93 +
.term { max-width: 640px; margin: 0; padding: 18px 20px; background: #050913; color: #d7e3f4; border: 1px solid #1a2742; border-radius: var(--radius); font-size: 14.5px; line-height: 1.7; overflow-x: auto; }
94 +
.term .p { color: #5f7392; user-select: none; }
95 +
.term .ok { color: var(--cyan); }
96 +
97 +
/* Cards */
98 +
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(290px, 1fr)); gap: 20px; margin-top: 32px; }
99 +
.card { min-width: 0; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 26px; }
100 +
.card h3 { display: flex; align-items: center; gap: 10px; }
101 +
.card p { color: var(--mute); }
102 +
.card ul { margin: 0; padding-left: 20px; display: grid; gap: 8px; }
103 +
.card li { color: var(--ink); font-size: 16px; }
104 +
.card.flat p { margin: 0; }
105 +
.tag { font: 600 13px/1 ui-monospace, Menlo, monospace; padding: 5px 8px; border-radius: 6px; background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent); }
106 +
107 +
/* Why */
108 +
.two { display: grid; grid-template-columns: 1.3fr 1fr; gap: 48px; align-items: start; }
109 +
.two > * { min-width: 0; }
110 +
.facts { margin: 0; display: grid; gap: 12px; }
111 +
.facts div { border-left: 3px solid; border-image: var(--grad) 1; padding: 6px 0 6px 16px; }
112 +
.facts dt { font-weight: 700; }
113 +
.facts dd { margin: 2px 0 0; color: var(--mute); }
114 +
115 +
.status p { color: var(--mute); }
116 +
.status code { color: var(--ink); }
117 +
118 +
/* Footer */
119 +
.site-foot { border-top: 1px solid var(--line); padding-block: 36px 48px; background: var(--bg-2); }
120 +
.foot-row { display: grid; gap: 14px; }
121 +
.foot-brand { display: flex; align-items: center; gap: 10px; font-weight: 600; }
122 +
.foot-brand img { border-radius: 7px; }
123 +
.site-foot nav { display: flex; gap: 18px; flex-wrap: wrap; }
124 +
.site-foot .fine { font-size: 14px; color: var(--mute); margin: 0; }
125 +
126 +
@media (max-width: 760px) {
127 +
  section { padding-block: 52px; }
128 +
  .hero { padding-block: 64px 56px; }
129 +
  .two { grid-template-columns: 1fr; gap: 28px; }
130 +
  .site-head { position: static; }
131 +
}
added src/index.html +117 −0
1 +
<!doctype html>
2 +
<html lang="en-GB">
3 +
<head>
4 +
<meta charset="utf-8">
5 +
<meta name="viewport" content="width=device-width, initial-scale=1">
6 +
<title>Oxygen: version control you can verify</title>
7 +
<meta name="description" content="Oxygen is a version control system, a forge and a build runner designed together: every change signed, conflicts recorded and resolved once, builds reproducible, and all of it hosted where you choose.">
8 +
<link rel="icon" href="/assets/oxygen.svg" type="image/svg+xml">
9 +
<link rel="stylesheet" href="/assets/site.css">
10 +
</head>
11 +
<body>
12 +
<!-- include header -->
13 +
<main id="main">
14 +
15 +
<section class="hero">
16 +
  <div class="wrap">
17 +
    <p class="eyebrow">Version control, a forge and CI, designed together</p>
18 +
    <h1>Version control you can <span class="grad">verify</span>.</h1>
19 +
    <p class="lede">Oxygen records every change as a signed, commutative patch, keeps conflicts as part of history instead of a mess to clean up, and builds in sandboxes that leave a signed receipt. It runs on your own machines, in your own country, and proves what it did.</p>
20 +
    <div class="ctas">
21 +
      <a class="btn primary" href="https://oxygenhub.io">Explore OxygenHub</a>
22 +
      <a class="btn" href="https://oxygenhub.io/andymccall/oxygen/releases">Get oxy</a>
23 +
    </div>
24 +
    <pre class="term" aria-label="An example session"><code><span class="p">$</span> oxy clone https://oxygenhub.io/andymccall/oxygen
25 +
<span class="p">$</span> oxy commit -m "Retry the upload"
26 +
<span class="p">$</span> oxy push
27 +
<span class="ok">✔</span> Pushed origin/main · sealed by your key · logged</code></pre>
28 +
  </div>
29 +
</section>
30 +
31 +
<section class="pillars" id="oxygen">
32 +
  <div class="wrap">
33 +
    <h2>Three parts, one system</h2>
34 +
    <div class="cards">
35 +
      <article class="card">
36 +
        <h3><span class="tag">oxy</span> Oxygen</h3>
37 +
        <p>The version control system. Changes commute, so they apply in any order and reach the same result: no rebasing, and a proposal never goes stale.</p>
38 +
        <ul>
39 +
          <li>Conflicts are recorded in history and resolved once, by a change anyone can fetch.</li>
40 +
          <li>Every change is signed, with Ed25519 and the post-quantum ML-DSA-65 together.</li>
41 +
          <li>Imports and exports Git history, commit by commit.</li>
42 +
          <li>Keeps repositories on disk or in your own S3 or Google Cloud Storage buckets.</li>
43 +
        </ul>
44 +
      </article>
45 +
      <article class="card" id="hub">
46 +
        <h3><span class="tag">oh</span> OxygenHub</h3>
47 +
        <p>The forge, self-hosted or at <a href="https://oxygenhub.io">oxygenhub.io</a>: repositories, proposals and reviews, issues and boards, releases.</p>
48 +
        <ul>
49 +
          <li>Approvals are sealed with the reviewer's key, so a landing proves who agreed.</li>
50 +
          <li>Every reference update goes into a transparency log, cosigned by independent witnesses.</li>
51 +
          <li>Agents work under grants you can revoke, signing with ten-minute keys, never stored ones.</li>
52 +
          <li>A command line, Visual Studio Code extensions and an Android app.</li>
53 +
        </ul>
54 +
      </article>
55 +
      <article class="card" id="flow">
56 +
        <h3><span class="tag">flow</span> OxygenFlow</h3>
57 +
        <p>The pipeline runner. Jobs run with no network unless they name the hosts they need, from images pinned by hash.</p>
58 +
        <ul>
59 +
          <li>Three sandboxes: Firecracker microVMs, system-call filtering, and WebAssembly.</li>
60 +
          <li>Runners on Linux (x86_64, Arm and RISC-V), Windows, and Android phones.</li>
61 +
          <li>Each job leaves a signed receipt naming its inputs, image and outputs.</li>
62 +
          <li>Runners update themselves, in test groups first, and roll back on their own.</li>
63 +
        </ul>
64 +
      </article>
65 +
    </div>
66 +
  </div>
67 +
</section>
68 +
69 +
<section class="why">
70 +
  <div class="wrap two">
71 +
    <div>
72 +
      <h2>Why another version control system?</h2>
73 +
      <p>Git records snapshots and leaves merging to tools and people. Every rebase rewrites history, every conflict is solved again on the next merge, and nothing proves who wrote what unless everyone signs everything by hand.</p>
74 +
      <p>Oxygen starts from the other end. A change is a patch with a stable identity, so applying it twice or in another order changes nothing. History is never rewritten. Signing is the default, checked on every push, and recorded where anyone can audit it.</p>
75 +
    </div>
76 +
    <dl class="facts">
77 +
      <div><dt>Commutative</dt><dd>Changes apply in any order; proposals never go stale.</dd></div>
78 +
      <div><dt>Conflicts kept</dt><dd>Recorded, built on, resolved once for everyone.</dd></div>
79 +
      <div><dt>Signed</dt><dd>Classical and post-quantum signatures on every change.</dd></div>
80 +
      <div><dt>Logged</dt><dd>A witnessed transparency log of every update.</dd></div>
81 +
      <div><dt>Hermetic</dt><dd>Builds with no network, from pinned images.</dd></div>
82 +
    </dl>
83 +
  </div>
84 +
</section>
85 +
86 +
<section class="sovereign" id="sovereign">
87 +
  <div class="wrap">
88 +
    <h2>Sovereign by design</h2>
89 +
    <p class="lede">Your source, your keys, your builds, in your jurisdiction. Oxygen has no service it must call home to.</p>
90 +
    <div class="cards three">
91 +
      <div class="card flat">
92 +
        <h3>Host it where you must</h3>
93 +
        <p>Run OxygenHub and its runners on your own hardware or cloud account, in the UK or wherever your data has to stay. Storage can be your own buckets.</p>
94 +
      </div>
95 +
      <div class="card flat">
96 +
        <h3>Prove what happened</h3>
97 +
        <p>Signed changes, sealed approvals, build receipts and a witnessed log let an auditor check the history without trusting the server that holds it.</p>
98 +
      </div>
99 +
      <div class="card flat">
100 +
        <h3>Keep builds in</h3>
101 +
        <p>Jobs reach the network only through hosts you allow, every decision logged, so a build cannot quietly fetch or send what it should not.</p>
102 +
      </div>
103 +
    </div>
104 +
  </div>
105 +
</section>
106 +
107 +
<section class="status">
108 +
  <div class="wrap">
109 +
    <h2>Where it is today</h2>
110 +
    <p>Oxygen is young and moving fast, ahead of a 1.0 release. It is built with itself: its source lives on OxygenHub, every push is tested by OxygenFlow on our own runners, and every release is signed and logged. Try it at <a href="https://oxygenhub.io">oxygenhub.io</a>, or install <code>oxy</code> and <code>oh</code> from the <a href="https://oxygenhub.io/andymccall/oxygen/releases">releases page</a> or the apt repository there.</p>
111 +
  </div>
112 +
</section>
113 +
114 +
</main>
115 +
<!-- include footer -->
116 +
</body>
117 +
</html>
modified README.md +47 −1
1 -
# OxygenVCS.com Website
1 +
# oxygenvcs.com
2 +
3 +
The product site for Oxygen, OxygenHub and OxygenFlow. User documentation
4 +
lives separately, at docs.oxygenvcs.com.
5 +
6 +
Static on purpose: hand-written HTML and one stylesheet, no JavaScript, no
7 +
cookies, no third-party fonts or requests. Less to maintain, little to
8 +
attack.
9 +
10 +
## Layout
11 +
12 +
- `src/`: the pages and `assets/`. A page's `<!-- include header -->` and
13 +
  `<!-- include footer -->` lines are replaced by `partials/`.
14 +
- `scripts/build.sh`: builds `public/` and `site.tar.gz` with sh, awk and
15 +
  tar, and refuses a page or stylesheet that loads anything from another
16 +
  site.
17 +
- `.flow/pipeline.kdl`: OxygenFlow runs the build on every push and keeps
18 +
  `site.tar.gz` as an artifact with a signed provenance statement.
19 +
- `deploy/`: how a host serves it.
20 +
21 +
Preview locally: `sh scripts/build.sh && python3 -m http.server -d public 8000`.
22 +
23 +
## Deploying (pull, not push)
24 +
25 +
Nothing pushes to the server. A timer on the host runs `deploy/pull.sh`:
26 +
it finds the newest passing run of `main`, downloads its `site.tar.gz`,
27 +
checks it against the run's provenance (the file's SHA-256, this
28 +
repository, `main`), unpacks it into `releases/RUN` and points `current`
29 +
at it. The host needs no inbound access and no credentials. The last five
30 +
releases stay: roll back with `ln -sfn releases/N current` and restart
31 +
the server.
32 +
33 +
On Earth (user services of `claude`, with lingering), serving
34 +
127.0.0.1:7410 behind the Cloudflare tunnel:
35 +
36 +
```
37 +
mkdir -p ~/oxygenvcs ~/.config/systemd/user
38 +
cp deploy/pull.sh deploy/httpd.conf ~/oxygenvcs/
39 +
cp deploy/oxygenvcs-*.service deploy/oxygenvcs-pull.timer ~/.config/systemd/user/
40 +
systemctl --user daemon-reload
41 +
systemctl --user start oxygenvcs-pull.service        # first release
42 +
systemctl --user enable --now oxygenvcs-web.service oxygenvcs-pull.timer
43 +
```
44 +
45 +
The VisionFive 2 Lite (riscv64) later: the same, with busybox, curl and
46 +
python3 installed. The tunnel's route for oxygenvcs.com (and .net, .org,
47 +
.co.uk redirecting to it) points at the server's port.