A Berthos node is a parked computer-session that an agent can lease — never the host desktop.
This repository is the node: a fail-closed eligibility doctor, a loopback-only HTTP daemon, a pairing booth, and an isolated Linux desktop guest image. It is the room where a session lives.
It is not the market. Listings, wallets, x402, USDC, and tokens live in the sibling repo berth-market. Quotes printed here are occupancy seconds. Nothing is charged in this process.
operator / agent CLI (berth) berth view / berth mcp
│ HTTP on 127.0.0.1 (tunnel optional, later)
▼
berthos-node — park / unpark / lease / eligibility / guest view
│ isolated guest only
▼
Linux desktop (Xvfb + openbox + Chromium) ← not the host cursor, not Finder
v1 is Linux guest only, loopback first. Public macOS is out of scope. Windows Home/Pro OEM on the metal is not a public listing. A private Windows VM for the operator's own agent is later, not this tree.
These are product rules, not style nits. The doctor fails closed.
- Isolation is the product. The node never rents or drives the host desktop / host cursor. No
--network=host, no/tmp/.X11-unix, no hostDISPLAY. class=laptopis rejected. A personal laptop or daily-driver is never a public node.- Only a VM guest or a dedicated server guest may be leased.
- Eligibility before participation. Docker (or equivalent) running, the labeled Linux desktop guest image present, default-deny egress, wired/always-on advertised for public intent, enough free vCPU/RAM, loopback bind only. Tunnel is optional. Missing probes fail, they are not skipped.
- Windows Home/Pro OEM on the metal is not a public listing.
- Public macOS is out of scope for v1.
- Secrets stay on the node. Pairing tokens and operator files are never mounted into the guest.
- No payments here. Do not look for a wallet, a token, or a listing catalog in this repo.
See docs/ELIGIBILITY.md for the check list and docs/ARCHITECTURE.md for trust boundaries and the state machine.
Requires a stable Rust toolchain and, for a real guest, Docker.
cargo install --path crates/berthos-cli
# the crates are named berthos-*; the command they install is `berth`The doctor will not pass without this image and its versioned labels.
docker build -t berthos-linux-desktop:v1 images/linux-desktopRequired labels (stamped by the Dockerfile):
| Label | Value |
|---|---|
berthos.guest.version | v1 |
berthos.desktop | xvfb-openbox-chromium |
berthos.egress.policy | default-deny |
An unlabeled or stale image is refused, not trusted. Rebuild after changing the contract. The node starts guests with --network none (empty allowlist = no outbound, DNS included). See images/linux-desktop.
berth doctor # private loopback intent (default)
berth doctor --intent public # extra wired / always-on / chassis checks
berth doctor --jsonExit 0 only when every required check passed. Exit 1 otherwise.
Advertise the node in ~/.berthos/node.toml (created on first berth node up):
class = "vm-guest"# vm-guest | dedicated-server | laptop (laptop always fails)chassis = "vm-host"# server | vm-host | laptop | unknownintent = "private"# private | publicguest_os = "linux"bind = "127.0.0.1"port = 7432always_on = truewired = trueclass=laptop fails in every intent. A laptop chassis may host a private loopback node that still leases an isolated guest. That chassis cannot go public.
berth node up
# pairing code: ABCD-EFGH# listening on http://127.0.0.1:7432The process refuses to start if the doctor is red, and refuses to listen on anything but loopback. berth node up --bind 0.0.0.0 is bind-all and is rejected.
Parked is the default (new leases allowed). Unpark while a lease is live is 409.
Capability token, not a cookie on a URL.
berth pair --code ABCD-EFGH
# token stored in ~/.berthos/client.toml (mode 0600)GET /v1/pairing reveals the code on loopback only. X-Forwarded-For is ignored.
berth up --os linux
# lease l_…# quote seconds (min 60s) — not charged--os windows and --os macos are rejected. Ending the lease destroy-and-recreates the guest (v1 revert; snapshot/restore is documented, not implemented). Occupancy is wall-clock seconds the guest is held, not clicks.
After a live linux lease, the node exposes a loopback-only view of that guest (noVNC or equivalent). It is not the host desktop. It is not published on 0.0.0.0. It dies with DELETE /v1/leases/{id} / berth end.
berth view
# http://127.0.0.1:<port>/?token=<lease-bearer># open on this machine — GUEST Xvfb, not the host DISPLAY
berth mcp
# stdio JSON-RPC: berth_screenshot, berth_click, berth_type, berth_key, berth_end# tools refuse if no lease is liveView and MCP require the lease bearer from berth pair (Authorization: Bearer <token>, stored in ~/.berthos/client.toml). The HTML viewer also accepts ?token= on loopback. See docs/SESSION.md.
Two-role reproduce (no payments in this repo). Operator parks the node; buyer (after a market pay in berth-market, or locally with no charge) opens the guest:
# operator
berth node up
# buyer / agent (same host in v1)
berth pair --code ABCD-EFGH
berth up --os linux
berth view # loopback guest desktop
berth mcp # agent screenshot / click / type / end
berth end # view gone; occupancy receiptNothing is charged here. Listings and settlement stay in berth-market.
| Method | Path | Auth | Notes |
|---|---|---|---|
GET | /health | no | liveness |
GET | /v1/eligibility | no | storeable attestation (ok, class, checks[], image labels, timestamp) |
GET | /v1/node | no | parked / eligible / live lease |
POST | /v1/park | operator | fail closed if ineligible |
POST | /v1/unpark | operator | 409 if a lease is live |
GET | /v1/pairing | loopback | current pairing code |
POST | /v1/pair | code | returns a bearer token |
POST | /v1/leases | lease | create; os=linux only; includes loopback viewer_url |
GET | /v1/leases | lease | live leases |
GET | /v1/leases/{id} | lease | one live lease |
GET | /v1/leases/{id}/view | lease | { viewer_url } for the guest (not the host) |
GET | /v1/leases/{id}/screenshot | lease | guest PNG; 404 if no live lease |
POST | /v1/leases/{id}/actions | lease | guest click / type / key |
DELETE | /v1/leases/{id} | lease | end; occupancy receipt; view dies |
Authorization: Authorization: Bearer <token>.
The CLI can simulate failures, not success:
berth doctor --simulate laptop # exit 1 — class rejected
berth doctor --simulate missing-image # exit 1
berth doctor --simulate bind-all # exit 1That is the automated smoke path when Docker is missing. Live probes (berth doctor without --simulate) talk to the daemon and require berthos-linux-desktop:v1 with the labeled contract. CI job linux keeps unit tests and --simulate even if Docker is absent. Job docker-live builds the image and runs the live doctor plus isolated lease start/destroy.
The manual path on a real box is the Quick start above: build the image, berth doctor, berth node up, berth pair, berth up --os linux.
cargo test covers the fail-closed cases as unit tests. Live Docker tests skip when the daemon or labeled image is missing.
- Marketplace listings or a catalog of other people's nodes
- Wallets, USDC, x402, tokens, cash-out
- Driving the host desktop or host Cursor
- Public Windows OEM or public macOS
- Binding
0.0.0.0and calling it a product
Talk to berth-market when you want spend/earn. Talk to this repo when you want a computer session that cannot see the operator's logged-in desktop.
MIT. See LICENSE.