Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 5 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ The Pi has no inbound ports; the browser uses the Pi's own residential network.*
## Demos

Captured from real runs against a Raspberry Pi 5 worker (Chromium 154, headless)
and a Mac worker (Chrome 154). See [docs/validation.md](docs/validation.md) for
the full acceptance results.
and a Mac worker (Chrome 154).

| | |
|---|---|
Expand Down Expand Up @@ -390,26 +389,6 @@ Run non-local deployments behind TLS (terminate `wss://` at a reverse proxy).
`--public-url` accepts either an http(s) or ws(s) URL and is normalised to a
WebSocket scheme for lease endpoints.

---

## Status

Milestone 0 and the first agent slice are implemented and tested on macOS/arm64
with Chrome 154. The same end-to-end suite (relay, profile persistence, profile
locking) also runs against Chrome on Linux x64 in CI.

- [x] Coordinator, worker, leases, capability filtering, least-loaded scheduling
- [x] Outbound control channel and per-attachment CDP tunnel
- [x] Ordinary Playwright `connectOverCDP` through the relay
- [x] Persistent worker-local profiles with exclusive locking
- [x] Lease TTL, idle cleanup, worker-enforced teardown
- [x] Playwright MCP integration via `devdooth mcp`
- [x] SQLite metadata, single-use enrollment tokens, durable device identity, revocation
- [x] Release automation, container image, install script
- [x] Raspberry Pi 5 (Linux arm64) validated end to end — see [docs/validation.md](docs/validation.md)
- [ ] Windows worker (builds, not yet run)
- [ ] Multi-owner clusters

## Development

```bash
Expand All @@ -421,11 +400,11 @@ make smoke # coordinator + worker + Playwright, end to end

`scripts/browse.mjs` is a small Playwright CLI for driving a lease one action
at a time (goto, click, fill, eval, screenshot, …). It is handy for manual
checks and as the tool surface for an LLM agent loop. See
[docs/validation.md](docs/validation.md) for real-hardware results.
checks and as the tool surface for an LLM agent loop.

See [docs/architecture.md](docs/architecture.md) for the reasoning behind the
design.
See [docs/architecture.md](docs/architecture.md) for the design,
[docs/deployment.md](docs/deployment.md) for production setup, and
[docs/threat-model.md](docs/threat-model.md) for the security model.

## Why Devdooth?

Expand Down
30 changes: 12 additions & 18 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,21 +83,18 @@ The relay copies WebSocket frames, preserving type and boundaries. It sets a
32 MiB message cap and write deadlines. A slow consumer is disconnected; it is
never buffered without limit.

## Deliberate omissions
## Non-goals

- **No SDKs yet.** The HTTP API and CLI establish the contract first.
- **Single owner, no ACLs.** Device records carry identity, so adding
`owner_id` later is not a rewrite.
- **No enforced human takeover.** Pausing an opaque CDP stream does not pause an
in-flight agent. Headful sessions let a human act locally; coordinated
ownership is future work.
- **No browser downloads.** Workers use installed browsers. This avoids
packaging and redistribution work before the core is proven.
- **No SDKs.** The HTTP API and CLI are the contract; any language can call them.
- **Single owner, no ACLs.** Device records carry identity, so ownership can be
layered on without a redesign.
- **Cooperative take-over, not enforcement.** Pausing detaches the controller and
hands the window to a human; it does not stop a command already in flight.
- **No bundled browsers.** Workers use the browsers already on the machine.

## Verified
## Test coverage

`make smoke` and `go test ./e2e/` prove, on macOS/arm64 with Chrome 154 (and
against Chrome on Linux x64 in CI):
`make smoke` and `go test ./e2e/` exercise the whole path with a real browser:

- a worker launches Chrome with a loopback-only debug port;
- a normal Playwright client connects over CDP through the relay and drives a
Expand All @@ -106,9 +103,6 @@ against Chrome on Linux x64 in CI):
- a second lease on a locked profile is rejected;
- `devdooth mcp` hands the leased browser to upstream Playwright MCP.

A Raspberry Pi 5 worker was validated end to end on real hardware (Linux arm64,
Chromium 154): outbound enrollment, capability reporting, a Playwright client on
another host, traffic proven to originate from the Pi, profile persistence,
loopback-only debugging, worker-kill detection, and reconnect identity. An LLM
agent completed a real Amazon search through the leased Pi browser. See
[docs/validation.md](validation.md).
Unit and integration tests cover enrollment and revocation, pause/resume,
coordinator restart, capability scheduling, and the relay's frame bounds. The
suite runs on macOS arm64 and on Linux x64 in CI.
154 changes: 154 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Deployment

How to run Devdooth for real: a coordinator behind TLS, and workers on the
machines that lend their browsers.

## Coordinator

The coordinator holds device metadata and relays CDP. Put it on a small host
with a stable address. Workers connect outbound, so only the coordinator needs
an inbound port.

```bash
devdooth coordinator \
--addr 127.0.0.1:8080 \
--store /var/lib/devdooth/devdooth.db \
--admin-token "$(cat /etc/devdooth/admin-token)" \
--public-url https://devdooth.example.com
```

- `--addr 127.0.0.1:8080` keeps it on loopback; the reverse proxy faces the
internet.
- `--store` is a single SQLite file (device identities and enrollment tokens).
- `--public-url` is the address callers use. It accepts an `https://` or
`wss://` URL and is normalised to `wss://` for lease endpoints.
- If `--admin-token` is omitted, one is generated and printed once.

### TLS with Caddy

```
devdooth.example.com {
reverse_proxy 127.0.0.1:8080
}
```

Caddy gets and renews certificates automatically and proxies WebSockets without
extra configuration.

### TLS with nginx

```nginx
server {
listen 443 ssl;
server_name devdooth.example.com;

ssl_certificate /etc/letsencrypt/live/devdooth.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/devdooth.example.com/privkey.pem;

location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 1h;
}
}
```

`proxy_read_timeout` must be generous: a lease holds a long-lived WebSocket.

### systemd unit

```ini
[Unit]
Description=Devdooth coordinator
After=network-online.target

[Service]
ExecStart=/usr/local/bin/devdooth coordinator --addr 127.0.0.1:8080 --store /var/lib/devdooth/devdooth.db --public-url https://devdooth.example.com
EnvironmentFile=/etc/devdooth/coordinator.env
StateDirectory=devdooth
Restart=on-failure

[Install]
WantedBy=multi-user.target
```

`/etc/devdooth/coordinator.env` holds `DEVDOOTH_ADMIN_TOKEN=...` with `0600`
permissions. `GET /healthz` is available for a load balancer or uptime check.

## Workers

Install once, enroll, and run the worker on each machine that should lend a
browser. The worker needs no inbound port.

```bash
# enroll (once) as the same user that will run the service, into its data dir
devdooth join --coordinator https://devdooth.example.com \
--enroll-token "<token from the coordinator>" --name macbook \
--data-dir /var/lib/devdooth-worker

# run the worker
devdooth worker --coordinator https://devdooth.example.com \
--data-dir /var/lib/devdooth-worker --profiles shopping,work
```

- **Headful** machines (a desktop) add `--headful` so the browser is visible for
human take-over.
- **Containers / locked-down CI** may need `--no-sandbox`.
- Give each worker a distinct `--data-dir` if you run more than one per host.

### systemd unit

Enroll with the same user and `--data-dir` the unit uses, so the service can
find the device identity.

```ini
[Unit]
Description=Devdooth worker
After=network-online.target

[Service]
User=devdooth
ExecStart=/usr/local/bin/devdooth worker --coordinator https://devdooth.example.com --data-dir /var/lib/devdooth-worker --profiles shopping,work
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
```

### Enroll and revoke

Mint a token per machine and revoke when a machine is retired:

```bash
devdooth enroll-token --url https://devdooth.example.com --token "$ADMIN" --label macbook
devdooth devices --url https://devdooth.example.com --token "$ADMIN"
devdooth devices revoke --url https://devdooth.example.com --token "$ADMIN" --id dev_...
```

## Clients

Callers reach the coordinator over HTTPS and connect to the returned `wss://`
endpoint with any CDP client:

```bash
devdooth lease --url https://devdooth.example.com --token "$ADMIN" --profile shopping --headful
```

```python
browser = await playwright.chromium.connect_over_cdp(endpoint)
```

For an LLM agent, `devdooth mcp` leases a browser and supervises Playwright MCP,
so an MCP client can drive it without extra wiring.

## Checklist

- [ ] Coordinator on a dedicated host, bound to loopback, behind TLS.
- [ ] Admin token stored with `0600`; workers use enrolled device identities.
- [ ] Only the coordinator exposes an inbound port.
- [ ] Workers run under an account that holds only the profiles you intend to lend.
- [ ] Device tokens revoked for machines no longer in service.
27 changes: 3 additions & 24 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -471,33 +471,12 @@ <h3 class="step" style="margin-top:30px">Limitations</h3>
<ul class="clean">
<li>Chromium-family browsers only (CDP).</li>
<li>One browser process per lease; no multi-context packing.</li>
<li>Single owner; no ACLs yet.</li>
<li>Single owner; no ACLs.</li>
<li>Workers use installed browsers; Devdooth downloads none.</li>
<li>No enforced human takeover yet; headful lets a human act locally.</li>
<li>Remote uploads/downloads and filesystem semantics need per-case verification.</li>
<li>Human take-over is cooperative (pause/resume), not enforced ownership.</li>
<li>Remote uploads and downloads vary by browser; verify them per use case.</li>
</ul>
</section>

<section id="status" class="wrap">
<h2>Status</h2>
<p class="sub">Pre-1.0, tested on macOS arm64 and Linux amd64 with real Chromium.</p>
<p><strong style="font-size:14px">Implemented</strong></p>
<p>
<span class="pill done">coordinator + worker + leases</span>
<span class="pill done">CDP relay &amp; Playwright</span>
<span class="pill done">worker-local profiles</span>
<span class="pill done">enrollment &amp; revocation</span>
<span class="pill done">Playwright MCP</span>
<span class="pill done">release binaries</span>
<span class="pill done">Raspberry Pi validated</span>
<span class="pill done">human take-over (pause/resume)</span>
</p>
<p style="margin-top:16px"><strong style="font-size:14px">Planned / not yet validated</strong></p>
<p>
<span class="pill next">Windows worker</span>
<span class="pill next">multi-owner clusters</span>
</p>
</section>
</main>

<footer>
Expand Down
92 changes: 92 additions & 0 deletions docs/threat-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Threat model

Devdooth hands out control of real browsers that may be signed into email,
banking, social, and internal services. That makes a lease a powerful
credential. This document states what Devdooth protects, what it assumes, and
what it explicitly does not.

## Assets

- **Browser sessions and profiles** on worker machines: cookies, tokens,
localStorage, extensions, and anything the browser can reach.
- **Credentials**: the admin token, device tokens, enrollment tokens, and
per-lease session tokens.
- **The coordinator** and its metadata store (device names and token hashes).

## Trust model

Devdooth assumes:

- **You trust the coordinator.** It can drive any browser any connected worker
offers.
- **You trust the automation clients** you give tokens to.
- **Websites are untrusted** and may be hostile.
- **One owner.** There is no multi-tenant isolation.

If any of these is false for you, do not run Devdooth that way. See
[Isolation](#isolation) for how to narrow the blast radius.

## Properties Devdooth provides

- Workers make **outbound connections only**. A worker needs no inbound port,
and the browser's debug endpoint is bound to loopback on the worker.
- **Admin and worker credentials are separate.** With durable identity enabled,
the admin token is never accepted as a worker credential. Enrolled device
tokens are random 256-bit values; the coordinator stores only a hash, and the
worker keeps its token in `<data-dir>/device.json` with `0600` permissions.
- **Enrollment tokens are single-use and expire.** Redemption is atomic, so a
replayed token fails. The device name is fixed at enrollment and cannot be
changed on reconnect.
- **Revocation takes effect immediately**: it drops the device's control
connection and any active CDP relay.
- **Per-lease credentials** are random, scoped to one lease, and bounded by the
lease's expiry.
- **The relay is bounded**: a fixed maximum frame size and write deadlines, and
a stalled peer is disconnected rather than buffered without limit.
- **TTLs are enforced by the worker**, so an abandoned browser is stopped even if
the coordinator is unreachable.
- **The relay does not parse or log CDP payloads.** Passwords and page content
are not written to logs.

## Threats and mitigations

| Threat | Mitigation | Residual risk |
|---|---|---|
| Stolen admin token | TLS; keep the token secret; rotate by restarting with a new `--admin-token` | Anyone with it can lease any browser |
| Stolen device token | Stored hashed on the coordinator; revoke the device | A leaked token, or a copy of the worker's `device.json`, impersonates that worker until revoked |
| Stolen enrollment token | Single-use, short TTL | A leaked unused token can enroll one device |
| Replayed enrollment | Atomic single-use redemption | None once used |
| Unauthorized profile access | Profiles require an explicit request and are node-scoped; access is only via a valid lease | A lease can read whatever that profile is logged into |
| Malicious website | Normal browser isolation | The page can attempt phishing, drive-bys, or data exfiltration through its own origin |
| SSRF / local network reach | Browsers reach what their network reaches | A CDP client can direct the browser at local-network services |
| Debug port exposure | Bound to loopback; no inbound worker port | A local process on the worker can reach it |
| Coordinator DoS | Bounded frames, write deadlines, capacity limits | Enough valid leases can exhaust a worker's slots |
| Abandoned session | Worker-enforced TTL and explicit release | Cost is bounded by the TTL |
| Log leakage | No payload logging | Development and credential-recovery paths deliberately print a token once |

## What Devdooth does not defend against

- **A compromised coordinator.** Treat the coordinator host as fully trusted.
- **An authorized CDP client.** Raw CDP can read local files and reach
local-network services within the browser's privileges. CDP filtering is not a
security boundary.
- **Multi-tenant abuse.** There is no per-user isolation, quota, or ACL.
- **Detection or evasion.** Devdooth is not a stealth, fingerprint-spoofing, or
CAPTCHA-bypass tool, and does not pretend to be.

## Isolation

To reduce the blast radius of a compromised lease or worker:

- Run the coordinator on a dedicated host and terminate TLS at a reverse proxy.
- Run workers under a **separate OS account, container, or VM** that holds only
the profiles and network access you are willing to lend.
- Expose only the profiles you intend to share, and start with read-mostly ones.
- Prefer **short lease TTLs** and revoke devices you no longer use.
- Restrict the worker's network egress where it makes sense. Devdooth does not
control what the browser can reach.

## Reporting

Report a vulnerability through GitHub's private "Report a vulnerability" flow on
the repository rather than a public issue.
Loading
Loading