Your environment runs on your machine. This page holds a signing key it generated itself, signs an update plan with it, and uploads the bytes to a plan server. Your runtime notices, checks the signature against a key it already trusts, and switches version — on its own, with no restart.
Press a button here. Watch the packs change there.
A plan is only accepted if it carries a signature from a key in your environment's trust root — a list of public keys on your disk. So step one is: this page shows you its public key, and you add it. Its private key never leaves your browser, and the plan server never sees it. The server stores bytes and hands them out; it cannot mint, alter, or forge a plan.
┌───────────────────────┐ sign in the browser ┌──────────────────┐
│ this page │ ─────────────────────▶ │ plan server │
│ holds a private key │ upload bytes │ stores bytes. │
│ you never send │ │ holds no keys. │
└───────────────────────┘ └──────────────────┘
│ ▲
│ you add its PUBLIC key │ poll every 60s
│ to the env's trust root │ (anonymous)
▼ │
┌──────────────────────────────────────────────────────────────────┐
│ greentic-start on YOUR machine │
│ verify signature vs trust root → snapshot → apply → rollback │
└──────────────────────────────────────────────────────────────────┘
no packs → v1 (1 pack) → v2 (2 packs)
One command installs the whole pinned toolchain. Everything else is
bash, curl and grep. No Python, no
jq, no Docker. Linux and macOS.
$ cargo binstall gtc $ gtc install --release 1.1.2 $ export DEMO=~/greentic-demo $ mkdir -p $DEMO/home && cd $DEMO
Release 1.1.2 pins greentic-deployer 1.1.11 and
greentic-start 1.1.11, which is everything this demo needs.
There are no bundles to download.
The plan names its bundle with an oci:// reference, and
op env apply pulls it from the registry itself. Nothing is
copied to your machine ahead of time, and the page never needs to know a
single path on your filesystem.
The plan still pins the bundle's sha256. Apply fails closed
unless the bytes it pulled hash to exactly that — so the registry, exactly
like the plan server, is an untrusted delivery channel and not a trusted
party.
Use a throwaway terminal.
The next step sets HOME to $DEMO/home so nothing
touches your real ~/.greentic. That also means every other
command in that terminal sees the fake home. Close it when you're done.
This runs entirely in your browser. The key is generated here and kept in this browser's local storage — nothing is uploaded but signed plans.
generating…
none yet
Once you've claimed a namespace above, this block is filled in with your endpoint and your page key. It creates an environment with no packs at all, subscribes it to the server, and tells it to trust this page.
# Claim a namespace in the console above and this fills itself in.
$ greentic-deployer op env show local | grep -o '"bundle_digest":"sha256:[0-9a-f]*"' | tail -1 (no output — nothing is deployed yet)
That prints the digest of the revision currently taking traffic. Two values matter, and the page publishes only these:
| Digest | Version |
|---|---|
sha256:6e6f24f5… | v1 — app-webchat-bot (1 pack) |
sha256:feb45faf… | v2 — app-webchat-bot, messaging-telegram (2 packs) |
Remember to re-export DEMO and HOME in this second
terminal, or it will look at your real home.
Go back to the console and press Publish v1. Within 60 seconds the runtime picks it up. Run the same check:
$ greentic-deployer op env show local | grep -o '"bundle_digest":"sha256:[0-9a-f]*"' | tail -1 "bundle_digest":"sha256:6e6f24f5c3f12c41…" ← v1. 1 pack. It came from the page.
$ greentic-deployer op env show local | grep -o '"bundle_digest":"sha256:[0-9a-f]*"' | tail -1 "bundle_digest":"sha256:feb45fafda62abe2…" ← v2. 2 packs.
Now press Publish v1 again. It goes back to one pack. Flip between them as often as you like.
Going back to v1 is not a downgrade.
Anti-rollback protects the plan sequence, which only ever grows
— 1, 2, 3, 4. The content a plan points at is free to move in
either direction. Replaying an old signed plan stays impossible.
| Claim | What enforces it |
|---|---|
| The page can't be impersonated | Plans are Ed25519-signed in the browser. The runtime accepts only signatures from a key in its own trust-root.json — the one you added by hand. |
| The server can't forge an update | It stores bytes and holds no key. A plan it invented would fail verification on your machine. |
| The network can't either | Plan reads are anonymous and unauthenticated on purpose. The signature is the trust anchor — not TLS, not the origin. |
| You can't be replayed an old version | Each plan carries a monotonic sequence, read from the signed plan rather than from anything the server says. |
| Nothing is pushed at your runtime | It polls. An environment with no updates block fetches nothing, ever. |
| A plan can't hijack the channel | The updates block is stripped from a plan target before signing — here in the page, and in the CLI. |
The page's key is not in your environment's trust root. See what the env actually trusts:
$ greentic-deployer op trust-root list local
If the key_id shown in the console above is not in that list, just
re-run the op trust-root add step from section 3. The usual
cause is clearing browser storage — the page then mints a new key,
and the old one no longer matches.
Give it a full minute — 60s is the poll floor. Then check the console log above: a successful publish prints the sequence number the server accepted. If the button reports an error, it is the server rejecting the upload, not your runtime.
op env apply fetches the bundle from
ghcr.io anonymously — no login, no Docker. If it fails, you are
either offline or behind a proxy that blocks the registry. The error names the
oci:// ref it tried.
A digest mismatch is different, and is the system working: apply pulled bytes that do not hash to the digest pinned in the signed plan, and refused them rather than deploying whatever the registry handed over.
The console needs WebCrypto Ed25519 (Chrome 137+, Firefox 130+, Safari 17+). On an older browser the buttons stay disabled and the key line says so.