Skip to content

Repository files navigation

Berthos

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.

Hard rules

These are product rules, not style nits. The doctor fails closed.

  1. Isolation is the product. The node never rents or drives the host desktop / host cursor. No --network=host, no /tmp/.X11-unix, no host DISPLAY.
  2. class=laptop is rejected. A personal laptop or daily-driver is never a public node.
  3. Only a VM guest or a dedicated server guest may be leased.
  4. 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.
  5. Windows Home/Pro OEM on the metal is not a public listing.
  6. Public macOS is out of scope for v1.
  7. Secrets stay on the node. Pairing tokens and operator files are never mounted into the guest.
  8. 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.

How to run

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`

1. Build the labeled guest image

The doctor will not pass without this image and its versioned labels.

docker build -t berthos-linux-desktop:v1 images/linux-desktop

Required labels (stamped by the Dockerfile):

LabelValue
berthos.guest.versionv1
berthos.desktopxvfb-openbox-chromium
berthos.egress.policydefault-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.

2. Doctor

berth doctor # private loopback intent (default)
berth doctor --intent public # extra wired / always-on / chassis checks
berth doctor --json

Exit 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 = true

class=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.

3. Node

berth node up
# pairing code: ABCD-EFGH# listening on http://127.0.0.1:7432

The 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.

4. Pair

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.

5. Lease a Linux guest (local loopback)

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.

6. Guest view + MCP (buyer / agent)

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 live

View 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 receipt

Nothing is charged here. Listings and settlement stay in berth-market.

HTTP (127.0.0.1 only)

MethodPathAuthNotes
GET/healthnoliveness
GET/v1/eligibilitynostoreable attestation (ok, class, checks[], image labels, timestamp)
GET/v1/nodenoparked / eligible / live lease
POST/v1/parkoperatorfail closed if ineligible
POST/v1/unparkoperator409 if a lease is live
GET/v1/pairingloopbackcurrent pairing code
POST/v1/paircodereturns a bearer token
POST/v1/leasesleasecreate; os=linux only; includes loopback viewer_url
GET/v1/leasesleaselive leases
GET/v1/leases/{id}leaseone live lease
GET/v1/leases/{id}/viewlease{ viewer_url } for the guest (not the host)
GET/v1/leases/{id}/screenshotleaseguest PNG; 404 if no live lease
POST/v1/leases/{id}/actionsleaseguest click / type / key
DELETE/v1/leases/{id}leaseend; occupancy receipt; view dies

Authorization: Authorization: Bearer <token>.

Doctor smoke (no Docker)

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 1

That 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.

What this repo does not do

  • 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.0 and 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.

License

MIT. See LICENSE.

About

VM/server computer-session node for agents. Isolated guests only. Never the host desktop, never a laptop.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - hexuria/berthos: VM/server computer-session node for agents. Isolated guests only. Never the host desktop, never a laptop. · GitHub
Skip to content

Repository files navigation

Berthos

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.

Hard rules

These are product rules, not style nits. The doctor fails closed.

  1. Isolation is the product. The node never rents or drives the host desktop / host cursor. No --network=host, no /tmp/.X11-unix, no host DISPLAY.
  2. class=laptop is rejected. A personal laptop or daily-driver is never a public node.
  3. Only a VM guest or a dedicated server guest may be leased.
  4. 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.
  5. Windows Home/Pro OEM on the metal is not a public listing.
  6. Public macOS is out of scope for v1.
  7. Secrets stay on the node. Pairing tokens and operator files are never mounted into the guest.
  8. 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.

How to run

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`

1. Build the labeled guest image

The doctor will not pass without this image and its versioned labels.

docker build -t berthos-linux-desktop:v1 images/linux-desktop

Required labels (stamped by the Dockerfile):

LabelValue
berthos.guest.versionv1
berthos.desktopxvfb-openbox-chromium
berthos.egress.policydefault-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.

2. Doctor

berth doctor # private loopback intent (default)
berth doctor --intent public # extra wired / always-on / chassis checks
berth doctor --json

Exit 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 = true

class=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.

3. Node

berth node up
# pairing code: ABCD-EFGH# listening on http://127.0.0.1:7432

The 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.

4. Pair

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.

5. Lease a Linux guest (local loopback)

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.

6. Guest view + MCP (buyer / agent)

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 live

View 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 receipt

Nothing is charged here. Listings and settlement stay in berth-market.

HTTP (127.0.0.1 only)

MethodPathAuthNotes
GET/healthnoliveness
GET/v1/eligibilitynostoreable attestation (ok, class, checks[], image labels, timestamp)
GET/v1/nodenoparked / eligible / live lease
POST/v1/parkoperatorfail closed if ineligible
POST/v1/unparkoperator409 if a lease is live
GET/v1/pairingloopbackcurrent pairing code
POST/v1/paircodereturns a bearer token
POST/v1/leasesleasecreate; os=linux only; includes loopback viewer_url
GET/v1/leasesleaselive leases
GET/v1/leases/{id}leaseone live lease
GET/v1/leases/{id}/viewlease{ viewer_url } for the guest (not the host)
GET/v1/leases/{id}/screenshotleaseguest PNG; 404 if no live lease
POST/v1/leases/{id}/actionsleaseguest click / type / key
DELETE/v1/leases/{id}leaseend; occupancy receipt; view dies

Authorization: Authorization: Bearer <token>.

Doctor smoke (no Docker)

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 1

That 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.

What this repo does not do

  • 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.0 and 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.

License

MIT. See LICENSE.

About

VM/server computer-session node for agents. Isolated guests only. Never the host desktop, never a laptop.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - hexuria/berthos: VM/server computer-session node for agents. Isolated guests only. Never the host desktop, never a laptop. · GitHub
Skip to content

Repository files navigation

Berthos

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.

Hard rules

These are product rules, not style nits. The doctor fails closed.

  1. Isolation is the product. The node never rents or drives the host desktop / host cursor. No --network=host, no /tmp/.X11-unix, no host DISPLAY.
  2. class=laptop is rejected. A personal laptop or daily-driver is never a public node.
  3. Only a VM guest or a dedicated server guest may be leased.
  4. 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.
  5. Windows Home/Pro OEM on the metal is not a public listing.
  6. Public macOS is out of scope for v1.
  7. Secrets stay on the node. Pairing tokens and operator files are never mounted into the guest.
  8. 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.

How to run

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`

1. Build the labeled guest image

The doctor will not pass without this image and its versioned labels.

docker build -t berthos-linux-desktop:v1 images/linux-desktop

Required labels (stamped by the Dockerfile):

LabelValue
berthos.guest.versionv1
berthos.desktopxvfb-openbox-chromium
berthos.egress.policydefault-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.

2. Doctor

berth doctor # private loopback intent (default)
berth doctor --intent public # extra wired / always-on / chassis checks
berth doctor --json

Exit 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 = true

class=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.

3. Node

berth node up
# pairing code: ABCD-EFGH# listening on http://127.0.0.1:7432

The 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.

4. Pair

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.

5. Lease a Linux guest (local loopback)

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.

6. Guest view + MCP (buyer / agent)

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 live

View 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 receipt

Nothing is charged here. Listings and settlement stay in berth-market.

HTTP (127.0.0.1 only)

MethodPathAuthNotes
GET/healthnoliveness
GET/v1/eligibilitynostoreable attestation (ok, class, checks[], image labels, timestamp)
GET/v1/nodenoparked / eligible / live lease
POST/v1/parkoperatorfail closed if ineligible
POST/v1/unparkoperator409 if a lease is live
GET/v1/pairingloopbackcurrent pairing code
POST/v1/paircodereturns a bearer token
POST/v1/leasesleasecreate; os=linux only; includes loopback viewer_url
GET/v1/leasesleaselive leases
GET/v1/leases/{id}leaseone live lease
GET/v1/leases/{id}/viewlease{ viewer_url } for the guest (not the host)
GET/v1/leases/{id}/screenshotleaseguest PNG; 404 if no live lease
POST/v1/leases/{id}/actionsleaseguest click / type / key
DELETE/v1/leases/{id}leaseend; occupancy receipt; view dies

Authorization: Authorization: Bearer <token>.

Doctor smoke (no Docker)

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 1

That 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.

What this repo does not do

  • 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.0 and 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.

License

MIT. See LICENSE.

About

VM/server computer-session node for agents. Isolated guests only. Never the host desktop, never a laptop.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - hexuria/berthos: VM/server computer-session node for agents. Isolated guests only. Never the host desktop, never a laptop. · GitHub
Skip to content

Repository files navigation

Berthos

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.

Hard rules

These are product rules, not style nits. The doctor fails closed.

  1. Isolation is the product. The node never rents or drives the host desktop / host cursor. No --network=host, no /tmp/.X11-unix, no host DISPLAY.
  2. class=laptop is rejected. A personal laptop or daily-driver is never a public node.
  3. Only a VM guest or a dedicated server guest may be leased.
  4. 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.
  5. Windows Home/Pro OEM on the metal is not a public listing.
  6. Public macOS is out of scope for v1.
  7. Secrets stay on the node. Pairing tokens and operator files are never mounted into the guest.
  8. 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.

How to run

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`

1. Build the labeled guest image

The doctor will not pass without this image and its versioned labels.

docker build -t berthos-linux-desktop:v1 images/linux-desktop

Required labels (stamped by the Dockerfile):

LabelValue
berthos.guest.versionv1
berthos.desktopxvfb-openbox-chromium
berthos.egress.policydefault-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.

2. Doctor

berth doctor # private loopback intent (default)
berth doctor --intent public # extra wired / always-on / chassis checks
berth doctor --json

Exit 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 = true

class=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.

3. Node

berth node up
# pairing code: ABCD-EFGH# listening on http://127.0.0.1:7432

The 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.

4. Pair

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.

5. Lease a Linux guest (local loopback)

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.

6. Guest view + MCP (buyer / agent)

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 live

View 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 receipt

Nothing is charged here. Listings and settlement stay in berth-market.

HTTP (127.0.0.1 only)

MethodPathAuthNotes
GET/healthnoliveness
GET/v1/eligibilitynostoreable attestation (ok, class, checks[], image labels, timestamp)
GET/v1/nodenoparked / eligible / live lease
POST/v1/parkoperatorfail closed if ineligible
POST/v1/unparkoperator409 if a lease is live
GET/v1/pairingloopbackcurrent pairing code
POST/v1/paircodereturns a bearer token
POST/v1/leasesleasecreate; os=linux only; includes loopback viewer_url
GET/v1/leasesleaselive leases
GET/v1/leases/{id}leaseone live lease
GET/v1/leases/{id}/viewlease{ viewer_url } for the guest (not the host)
GET/v1/leases/{id}/screenshotleaseguest PNG; 404 if no live lease
POST/v1/leases/{id}/actionsleaseguest click / type / key
DELETE/v1/leases/{id}leaseend; occupancy receipt; view dies

Authorization: Authorization: Bearer <token>.

Doctor smoke (no Docker)

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 1

That 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.

What this repo does not do

  • 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.0 and 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.

License

MIT. See LICENSE.

About

VM/server computer-session node for agents. Isolated guests only. Never the host desktop, never a laptop.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - hexuria/berthos: VM/server computer-session node for agents. Isolated guests only. Never the host desktop, never a laptop. · GitHub
Skip to content

Repository files navigation

Berthos

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.

Hard rules

These are product rules, not style nits. The doctor fails closed.

  1. Isolation is the product. The node never rents or drives the host desktop / host cursor. No --network=host, no /tmp/.X11-unix, no host DISPLAY.
  2. class=laptop is rejected. A personal laptop or daily-driver is never a public node.
  3. Only a VM guest or a dedicated server guest may be leased.
  4. 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.
  5. Windows Home/Pro OEM on the metal is not a public listing.
  6. Public macOS is out of scope for v1.
  7. Secrets stay on the node. Pairing tokens and operator files are never mounted into the guest.
  8. 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.

How to run

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`

1. Build the labeled guest image

The doctor will not pass without this image and its versioned labels.

docker build -t berthos-linux-desktop:v1 images/linux-desktop

Required labels (stamped by the Dockerfile):

LabelValue
berthos.guest.versionv1
berthos.desktopxvfb-openbox-chromium
berthos.egress.policydefault-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.

2. Doctor

berth doctor # private loopback intent (default)
berth doctor --intent public # extra wired / always-on / chassis checks
berth doctor --json

Exit 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 = true

class=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.

3. Node

berth node up
# pairing code: ABCD-EFGH# listening on http://127.0.0.1:7432

The 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.

4. Pair

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.

5. Lease a Linux guest (local loopback)

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.

6. Guest view + MCP (buyer / agent)

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 live

View 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 receipt

Nothing is charged here. Listings and settlement stay in berth-market.

HTTP (127.0.0.1 only)

MethodPathAuthNotes
GET/healthnoliveness
GET/v1/eligibilitynostoreable attestation (ok, class, checks[], image labels, timestamp)
GET/v1/nodenoparked / eligible / live lease
POST/v1/parkoperatorfail closed if ineligible
POST/v1/unparkoperator409 if a lease is live
GET/v1/pairingloopbackcurrent pairing code
POST/v1/paircodereturns a bearer token
POST/v1/leasesleasecreate; os=linux only; includes loopback viewer_url
GET/v1/leasesleaselive leases
GET/v1/leases/{id}leaseone live lease
GET/v1/leases/{id}/viewlease{ viewer_url } for the guest (not the host)
GET/v1/leases/{id}/screenshotleaseguest PNG; 404 if no live lease
POST/v1/leases/{id}/actionsleaseguest click / type / key
DELETE/v1/leases/{id}leaseend; occupancy receipt; view dies

Authorization: Authorization: Bearer <token>.

Doctor smoke (no Docker)

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 1

That 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.

What this repo does not do

  • 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.0 and 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.

License

MIT. See LICENSE.

About

VM/server computer-session node for agents. Isolated guests only. Never the host desktop, never a laptop.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - hexuria/berthos: VM/server computer-session node for agents. Isolated guests only. Never the host desktop, never a laptop. · GitHub
Skip to content

Repository files navigation

Berthos

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.

Hard rules

These are product rules, not style nits. The doctor fails closed.

  1. Isolation is the product. The node never rents or drives the host desktop / host cursor. No --network=host, no /tmp/.X11-unix, no host DISPLAY.
  2. class=laptop is rejected. A personal laptop or daily-driver is never a public node.
  3. Only a VM guest or a dedicated server guest may be leased.
  4. 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.
  5. Windows Home/Pro OEM on the metal is not a public listing.
  6. Public macOS is out of scope for v1.
  7. Secrets stay on the node. Pairing tokens and operator files are never mounted into the guest.
  8. 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.

How to run

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`

1. Build the labeled guest image

The doctor will not pass without this image and its versioned labels.

docker build -t berthos-linux-desktop:v1 images/linux-desktop

Required labels (stamped by the Dockerfile):

LabelValue
berthos.guest.versionv1
berthos.desktopxvfb-openbox-chromium
berthos.egress.policydefault-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.

2. Doctor

berth doctor # private loopback intent (default)
berth doctor --intent public # extra wired / always-on / chassis checks
berth doctor --json

Exit 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 = true

class=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.

3. Node

berth node up
# pairing code: ABCD-EFGH# listening on http://127.0.0.1:7432

The 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.

4. Pair

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.

5. Lease a Linux guest (local loopback)

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.

6. Guest view + MCP (buyer / agent)

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 live

View 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 receipt

Nothing is charged here. Listings and settlement stay in berth-market.

HTTP (127.0.0.1 only)

MethodPathAuthNotes
GET/healthnoliveness
GET/v1/eligibilitynostoreable attestation (ok, class, checks[], image labels, timestamp)
GET/v1/nodenoparked / eligible / live lease
POST/v1/parkoperatorfail closed if ineligible
POST/v1/unparkoperator409 if a lease is live
GET/v1/pairingloopbackcurrent pairing code
POST/v1/paircodereturns a bearer token
POST/v1/leasesleasecreate; os=linux only; includes loopback viewer_url
GET/v1/leasesleaselive leases
GET/v1/leases/{id}leaseone live lease
GET/v1/leases/{id}/viewlease{ viewer_url } for the guest (not the host)
GET/v1/leases/{id}/screenshotleaseguest PNG; 404 if no live lease
POST/v1/leases/{id}/actionsleaseguest click / type / key
DELETE/v1/leases/{id}leaseend; occupancy receipt; view dies

Authorization: Authorization: Bearer <token>.

Doctor smoke (no Docker)

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 1

That 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.

What this repo does not do

  • 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.0 and 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.

License

MIT. See LICENSE.

About

VM/server computer-session node for agents. Isolated guests only. Never the host desktop, never a laptop.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - hexuria/berthos: VM/server computer-session node for agents. Isolated guests only. Never the host desktop, never a laptop. · GitHub
Skip to content

Repository files navigation

Berthos

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.

Hard rules

These are product rules, not style nits. The doctor fails closed.

  1. Isolation is the product. The node never rents or drives the host desktop / host cursor. No --network=host, no /tmp/.X11-unix, no host DISPLAY.
  2. class=laptop is rejected. A personal laptop or daily-driver is never a public node.
  3. Only a VM guest or a dedicated server guest may be leased.
  4. 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.
  5. Windows Home/Pro OEM on the metal is not a public listing.
  6. Public macOS is out of scope for v1.
  7. Secrets stay on the node. Pairing tokens and operator files are never mounted into the guest.
  8. 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.

How to run

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`

1. Build the labeled guest image

The doctor will not pass without this image and its versioned labels.

docker build -t berthos-linux-desktop:v1 images/linux-desktop

Required labels (stamped by the Dockerfile):

LabelValue
berthos.guest.versionv1
berthos.desktopxvfb-openbox-chromium
berthos.egress.policydefault-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.

2. Doctor

berth doctor # private loopback intent (default)
berth doctor --intent public # extra wired / always-on / chassis checks
berth doctor --json

Exit 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 = true

class=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.

3. Node

berth node up
# pairing code: ABCD-EFGH# listening on http://127.0.0.1:7432

The 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.

4. Pair

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.

5. Lease a Linux guest (local loopback)

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.

6. Guest view + MCP (buyer / agent)

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 live

View 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 receipt

Nothing is charged here. Listings and settlement stay in berth-market.

HTTP (127.0.0.1 only)

MethodPathAuthNotes
GET/healthnoliveness
GET/v1/eligibilitynostoreable attestation (ok, class, checks[], image labels, timestamp)
GET/v1/nodenoparked / eligible / live lease
POST/v1/parkoperatorfail closed if ineligible
POST/v1/unparkoperator409 if a lease is live
GET/v1/pairingloopbackcurrent pairing code
POST/v1/paircodereturns a bearer token
POST/v1/leasesleasecreate; os=linux only; includes loopback viewer_url
GET/v1/leasesleaselive leases
GET/v1/leases/{id}leaseone live lease
GET/v1/leases/{id}/viewlease{ viewer_url } for the guest (not the host)
GET/v1/leases/{id}/screenshotleaseguest PNG; 404 if no live lease
POST/v1/leases/{id}/actionsleaseguest click / type / key
DELETE/v1/leases/{id}leaseend; occupancy receipt; view dies

Authorization: Authorization: Bearer <token>.

Doctor smoke (no Docker)

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 1

That 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.

What this repo does not do

  • 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.0 and 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.

License

MIT. See LICENSE.

About

VM/server computer-session node for agents. Isolated guests only. Never the host desktop, never a laptop.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - hexuria/berthos: VM/server computer-session node for agents. Isolated guests only. Never the host desktop, never a laptop. · GitHub
Skip to content

Repository files navigation

Berthos

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.

Hard rules

These are product rules, not style nits. The doctor fails closed.

  1. Isolation is the product. The node never rents or drives the host desktop / host cursor. No --network=host, no /tmp/.X11-unix, no host DISPLAY.
  2. class=laptop is rejected. A personal laptop or daily-driver is never a public node.
  3. Only a VM guest or a dedicated server guest may be leased.
  4. 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.
  5. Windows Home/Pro OEM on the metal is not a public listing.
  6. Public macOS is out of scope for v1.
  7. Secrets stay on the node. Pairing tokens and operator files are never mounted into the guest.
  8. 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.

How to run

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`

1. Build the labeled guest image

The doctor will not pass without this image and its versioned labels.

docker build -t berthos-linux-desktop:v1 images/linux-desktop

Required labels (stamped by the Dockerfile):

LabelValue
berthos.guest.versionv1
berthos.desktopxvfb-openbox-chromium
berthos.egress.policydefault-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.

2. Doctor

berth doctor # private loopback intent (default)
berth doctor --intent public # extra wired / always-on / chassis checks
berth doctor --json

Exit 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 = true

class=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.

3. Node

berth node up
# pairing code: ABCD-EFGH# listening on http://127.0.0.1:7432

The 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.

4. Pair

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.

5. Lease a Linux guest (local loopback)

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.

6. Guest view + MCP (buyer / agent)

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 live

View 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 receipt

Nothing is charged here. Listings and settlement stay in berth-market.

HTTP (127.0.0.1 only)

MethodPathAuthNotes
GET/healthnoliveness
GET/v1/eligibilitynostoreable attestation (ok, class, checks[], image labels, timestamp)
GET/v1/nodenoparked / eligible / live lease
POST/v1/parkoperatorfail closed if ineligible
POST/v1/unparkoperator409 if a lease is live
GET/v1/pairingloopbackcurrent pairing code
POST/v1/paircodereturns a bearer token
POST/v1/leasesleasecreate; os=linux only; includes loopback viewer_url
GET/v1/leasesleaselive leases
GET/v1/leases/{id}leaseone live lease
GET/v1/leases/{id}/viewlease{ viewer_url } for the guest (not the host)
GET/v1/leases/{id}/screenshotleaseguest PNG; 404 if no live lease
POST/v1/leases/{id}/actionsleaseguest click / type / key
DELETE/v1/leases/{id}leaseend; occupancy receipt; view dies

Authorization: Authorization: Bearer <token>.

Doctor smoke (no Docker)

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 1

That 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.

What this repo does not do

  • 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.0 and 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.

License

MIT. See LICENSE.

About

VM/server computer-session node for agents. Isolated guests only. Never the host desktop, never a laptop.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages