server for the gemini protoccol and other smolnets
  • Rust 94.9%
  • Shell 3.5%
  • Python 0.8%
  • Dockerfile 0.5%
  • Nix 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
OrcVole cbfb226910
Some checks are pending
PR checks / rust-gates (push) Waiting to run
fuzz: corpus growth from the v1.1.0 smoke runs
Inputs the 60 second smoke run of all eight targets kept on Rust 1.99.1 with the updated TLS crates; no crashes. Committed with the existing corpora, as the 2026-08-30 campaigns were.
2026-10-25 00:21:10 +01:00
.forgejo/workflows build: Rust 1.99.1, Alpine 3.24, and the house secret scan 2026-10-24 23:57:07 +01:00
assets release: publish the minisign public key and how to verify 2026-09-15 13:25:45 +01:00
content release: v1.1.0 2026-10-24 23:57:07 +01:00
docs build: Rust 1.99.1, Alpine 3.24, and the house secret scan 2026-10-24 23:57:07 +01:00
fuzz fuzz: corpus growth from the v1.1.0 smoke runs 2026-10-25 00:21:10 +01:00
packaging release: v1.1.0 2026-10-24 23:57:07 +01:00
screenshots content: the status page stopped naming a version it cannot keep up with 2026-09-15 13:25:46 +01:00
scripts content: the front page mark uses block characters, not Braille 2026-09-15 13:25:46 +01:00
src finger: the profile names its own address 2026-09-15 13:25:46 +01:00
test build: Rust 1.99.1, Alpine 3.24, and the house secret scan 2026-10-24 23:57:07 +01:00
tests render: link the rendered sibling, not the source name 2026-09-15 13:25:44 +01:00
.gitignore scaffold: C0 — cargo project, framing layer, fuzz harness, CI, house docs 2026-09-15 13:25:42 +01:00
.scan-allowlist build: Rust 1.99.1, Alpine 3.24, and the house secret scan 2026-10-24 23:57:07 +01:00
AGENTS.md docs: a changelog and a release procedure, written before the first release 2026-09-15 13:25:44 +01:00
Cargo.lock release: v1.1.0 2026-10-24 23:57:07 +01:00
Cargo.toml release: v1.1.0 2026-10-24 23:57:07 +01:00
CHANGELOG.md release: v1.1.0 2026-10-24 23:57:07 +01:00
CloudronManifest.json release: v1.1.0 2026-10-24 23:57:07 +01:00
CloudronVersions.json cloudron: publish 1.1.0 to the versions feed 2026-10-25 00:14:38 +01:00
COMPARISON.md release: v1.0.0 2026-09-15 13:25:45 +01:00
CONTRIBUTING.md docs: rebuild the documentation around what this is 2026-09-15 13:25:43 +01:00
DEBUGGING.md fuzz: extended campaigns, 1.86 billion executions, no crashes 2026-09-15 13:25:44 +01:00
deny.toml core: C3 — content watcher (debounce, self-trigger-safe) 2026-09-15 13:25:42 +01:00
DESCRIPTION.md content: rebuild the site to the top-tasks plan, one register per fact 2026-09-15 13:25:46 +01:00
dist-workspace.toml docs: field notes on the five networks, with four corrections 2026-09-15 13:25:43 +01:00
Dockerfile build: Rust 1.99.1, Alpine 3.24, and the house secret scan 2026-10-24 23:57:07 +01:00
flake.lock feat(packaging): Nix flake — final piece of the C6 packaging matrix 2026-09-15 13:25:42 +01:00
flake.nix release: v1.1.0 2026-10-24 23:57:07 +01:00
icon.png feat(cloudron): package usv for Cloudron (Dockerfile, manifest, start.sh) 2026-09-15 13:25:42 +01:00
INTEGRATIONS.md docs: reconcile the plan with the code, record the going-public batch 2026-09-15 13:25:43 +01:00
LICENSE scaffold: C0 — cargo project, framing layer, fuzz harness, CI, house docs 2026-09-15 13:25:42 +01:00
MAINTENANCE.md docs: what the v1.1.0 release actually did, and its traps 2026-10-25 00:21:10 +01:00
POSTINSTALL.md content: rebuild the site to the top-tasks plan, one register per fact 2026-09-15 13:25:46 +01:00
README.md release: v1.0.0 2026-09-15 13:25:45 +01:00
RELEASING.md docs: what the v1.1.0 release actually did, and its traps 2026-10-25 00:21:10 +01:00
rust-toolchain.toml build: Rust 1.99.1, Alpine 3.24, and the house secret scan 2026-10-24 23:57:07 +01:00
SECURITY.md docs: what the v1.1.0 release actually did, and its traps 2026-10-25 00:21:10 +01:00
start.sh cloudron: expose Spartan, Nex and Finger as optional services 2026-09-15 13:25:43 +01:00
UPGRADING.md docs: rebuild the documentation around what this is 2026-09-15 13:25:43 +01:00

Unseen Servant

A server for the small internet.

Rust · MIT · v1.0.0


Pre-release: please do not share this around yet. There are no tagged releases and no published packages; every install path below builds from source.

Unseen Servant serves one folder of writing to five small networks: Gemini, Gopher, Spartan, Nex and Finger, and mirrors it to the web. You write a page once. A reader opens it in Lagrange, in a Gopher client from 1994, in lynx, or in Chrome, and it is the same page, from the same file, updated within seconds of you saving it.

                        ┌─ gemtext ────────→ Gemini · Spartan · Nex
content/*.gmi ─→ render ─┼─ menus ──────────→ Gopher
                        ├─ profile ────────→ Finger
                        └─ HTML + Markdown → the web

There is no build step, no deploy, no database, and no second copy to keep in sync.

The command is usv: usv init, usv status, usv render. After this first mention, the documentation writes Unseen Servant (usv).

Where it answers

Unseen Servant (usv) publishes the same capsule at one name across every network it serves:

Web https://unseenservant.dev
Gemini gemini://unseenservant.dev
Gopher gopher://unseenservant.dev
Spartan spartan://unseenservant.dev
Nex nex://unseenservant.dev
Finger finger://unseenservant.dev

Not live yet. The domain is registered and the deployment exists, but the name has not been pointed at it and nothing is announced. These addresses are the intended shape, not a claim that they resolve today.

One server for five networks

Each of the small networks has its own character and its own reasons to exist: Gopher's menus, Gemini's certainty, Spartan's plainness, Nex's minimalism, Finger's single page about a person. usv speaks all five plus a web mirror, from one content tree. Turning one on is a line of configuration, not a second site to maintain.

docs/smolnets.md characterises each network and what it is best at, so you can pick deliberately rather than enabling everything.

Easy to deploy

Running a capsule should not begin with choosing a Linux distribution.

Target
Cloudron (one click, once published) docs/deployment/cloudron.md
Debian / Ubuntu (.deb) docs/deployment/debian.md
Fedora / RHEL / openSUSE (RPM, RPM Package Manager) docs/deployment/rpm.md
Arch (AUR, Arch User Repository) docs/deployment/aur.md
Nix flake docs/deployment/nix.md
Container (OCI, Open Container Initiative: 8.77MB, distroless) docs/deployment/container.md
Source + systemd docs/deployment/source.md

It is one static binary with no runtime to install beside it. Every package above was built and exercised through a real install, run and remove cycle rather than only written.

git clone <repository-url> unseen-servant && cd unseen-servant
cargo build --release
./target/release/usv

That is the whole setup. With no configuration file and an empty state directory, usv mints an identity, writes a starter capsule, and serves it. Zero configuration is a supported configuration, not a degraded one. usv init runs a terminal wizard if you would rather be asked questions.

Security first, and why

A capsule is usually run by one person on a machine they also use for other things. The threat that matters is not a targeted attack; it is the slow accumulation of things that can go wrong unattended. So usv removes the categories rather than defending them:

  • Nothing is executed, ever. No CGI (Common Gateway Interface), no FastCGI, no scripting, no plugin interface, no proxying. Content is data. This is the single decision that removes the most risk: in the servers we studied, the escape hatch beyond static serving produced most of the defect load.
  • No admin web interface. There is no credential to leak, no session to hijack, no default password, and no login page to find. You edit files, or you upload over an authenticated connection.
  • Memory safety is enforced, not assumed. unsafe_code = "forbid" is set for the whole crate, so it is a compiler error rather than a code-review convention. Every parser that touches the wire is fuzzed, with committed regression corpora.
  • Fails closed. An unknown key in the configuration file is a startup error, because a typo in a security-relevant setting must not be ignored into a permissive default. An upload zone with an empty allowlist refuses to start rather than meaning "anyone".
  • Identity survives the infrastructure. The small internet uses TOFU (trust on first use): your reader's client pins your certificate the first time and warns if it changes. usv mints one per hostname and never silently regenerates it, not on restart, update, backup, restore or migration, and treats damaged key material as a loud failure rather than an excuse to make a new key.
  • Least privilege by default. One unprivileged process, every listener on an unprivileged port, an empty capability bounding set in the shipped systemd unit.

The full posture, including what it deliberately does not protect you from, is docs/security.md.

AI agents are first-class users

Not tolerated as crawlers, and not served something different from what a person gets. An agent may read a capsule, publish to one, or run one:

  • Reading. /llms.txt gives the complete page inventory in one request instead of a crawl, and every page has a Markdown form at its own address, so there is no markup to strip. robots.txt is permissive by default.
  • Writing. Publishing is a file write. Over the network that is Titan, on the same port as Gemini, authorised by client certificate against a named roster with capabilities and self-closing key-rotation windows. No accounts, no passwords, no tokens to expire.
  • Operating. Every read-only subcommand takes --json for one machine-readable object per line; logs can be emitted as JSON (JavaScript Object Notation) with USV_LOG_FORMAT=json; exit codes are a documented contract the test suite checks.

Every one of those is also an accessibility or ordinary usability feature: a site map is a navigation aid, a Markdown page is a clean read, machine-readable output is what any script wants. That is why they were built and the agent-only ideas were not.

If you are the person choosing on an agent's behalf, the question is not whether it has AI features but what it costs to let something automated publish here, and what happens when it misbehaves. Integration cost is a file write: no client library, no key, no build step. Access is granted by certificate fingerprint, so there is no password to leak into a prompt or a log, and revoking is deleting a line. The blast radius is bounded by the design rather than by your vigilance, because nothing is ever executed: a misbehaving agent writes bad prose, it does not run code. And none of it is an AI-only stack, so you are not maintaining a second system for machines.

The counterweight, stated plainly: this is a publishing surface, not an agent platform. No memory, no retrieval, no scheduling, no tool protocol, and none of those are coming. It is somewhere for an agent to put things that will still be readable in five years, not somewhere for it to think.

docs/agents.md makes the full case, and is also the reference for agents themselves.

Built from what the best servers already got right

usv is not a first attempt at this problem. Each of the small networks has a server that solved something well, and the design starts from those rather than from scratch: Agate's certificate lifecycle, Molly Brown's certificate zones, gmid's configuration semantics and its insistence on end-to-end tests against real sockets, GmCapsule's Titan handling, gophernicus and geomyidae's menu conventions.

What each one taught, and what usv deliberately declined to copy, is docs/lineage.md. Where another server is the better choice for you, COMPARISON.md says so plainly.

A codebase written to be debugged

Roughly one comment line per four lines of code, on purpose: the reasoning behind a decision lives next to it rather than only in a commit message. Every real decision is recorded as an ADR (architecture decision record) in docs/adr/, written before the code it governs. Modules are small and named for what they own.

That serves a human maintainer and an AI one equally. The wire test suite runs the real binary against real sockets and prints the exact bytes exchanged when something fails; rejections name the layer and the rule that fired; usv status --json reports the server's whole state in one parseable object. DEBUGGING.md is organised by symptom.

Protocols

Protocol Verified against
Gemini gemini-diagnostics, Lagrange
Titan Lagrange
Web (HTTP, HyperText Transfer Protocol) browsers, lynx, w3m
Gopher gelim
Spartan Lagrange
Nex gelim
Finger bombadillo
Anything else Refused with status 53; usv is not a proxy

Nothing is described as supported here until a real client has driven it. Everything but Gemini, Titan and the web mirror is off until you enable it: the four cleartext protocols offer no confidentiality and no client authentication, so gated paths are excluded from their trees structurally rather than by a check somebody has to remember. docs/protocols.md is the authority.

Configuration

One TOML (Tom's Obvious Minimal Language) file, and every default works.

[server]
http_listen = "0.0.0.0:8000"
theme = "midnight"

[[host]]
name = "example.org"

[[host.titan_zone]]
path_prefix = "/uploads/"
fingerprints = ["sha256-hex…"]     # empty here is a startup error, never "anyone"

Full reference: docs/configuration.md. Remote editing over Titan: docs/titan.md. Common questions: docs/faq.md. Everything else: docs/.

Project

Written in Rust: about 11,000 lines of code across 40 files, with 647 tests and a fuzz target for every parser.

A clone is larger than the code, because the fuzzing corpora are committed: about 92 MB, two thirds of it the configuration parser's. That is deliberate. A corpus is the accumulated evidence of every input class a parser has already survived, and a regression suite that lives only on the machine that generated it is not a regression suite. If you only want to read or build the code, git clone --depth 1 skips the history and --filter=blob:none fetches file contents on demand. Not independently audited, and v1.0.0 is a first release: Agate and gmid have years of production hardening it does not.

AI Forward. usv is written end to end by an AI, directed and reviewed by a human. If that is not something you want serving your capsule, that is a fair call to make.

The small networks docs/smolnets.md
What we learned from other servers docs/lineage.md
Agents docs/agents.md · docs/agents.html
Architecture docs/architecture.md
Debugging DEBUGGING.md
Contributing CONTRIBUTING.md
Upgrades and identity survival UPGRADING.md
Reporting a vulnerability SECURITY.md

License

MIT.