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
8 changes: 5 additions & 3 deletions .devcell.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
# Base stack (one of: base, go, node, python, fullstack, electronics, ultimate)
stack = "ultimate"
#
# Addon modules (from nixhome/modules/): desktop, electronics, financial,
# Addon modules (from nixhome/modules/): android, desktop, electronics, financial,
# graphics, infra, news, nixos, qa-tools, scraping, travel, go, node, python
# modules = ["electronics", "desktop"]
#
# Disable GUI (Xvfb + VNC + browser). GUI is enabled by default.
# gui = false
# Run container with Docker --privileged flag. Default: false.
# docker_privileged = true
# Timezone (IANA format). If omitted, inherits host $TZ.
# timezone = "Europe/Prague"

Expand Down Expand Up @@ -38,8 +40,8 @@ stack = "ultimate"
# 1Password documents whose fields are passed into the container as env vars.
# Requires `op` CLI on the host. Each field in the document becomes an env var:
# e.g. a field labeled "API_KEY" with value "sk-123" → env var API_KEY=sk-123.
# [op]
# documents = ["prod-api-keys", "dev-secrets"]
[op]
documents = ["prod-devcell-common"]

# AWS credential scoping. When true, credentials are scoped to read-only
# via IAM session policy. All AWS tools (cli, terraform, SDKs, MCP servers)
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ test/testdata/**/nixhome
test/results
.devcell
.devcell.toml
.gocache
.gomodcache
57 changes: 52 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@ On first run, `cell` creates `.devcell.toml` and `.devcell/` in your project dir
- **Isolated sandbox** - agents edit freely inside your project; your host system is untouched
- **12+ MCP servers** - Yahoo Finance, Google Maps, Linear, KiCad, Inkscape, and more. Backing tools ship in the image alongside their servers
- **Claude Max/Pro support** - runs Claude Code directly, no API key or proxy needed
- **Stealth Chromium** - anti-fingerprint browser with Playwright, passes bot detection out of the box
- **Stealth Chromium + zero-password login** - `cell login <url>` opens a clean browser on your host, you log in, press Enter; cookies and localStorage sync to the container. The agent never sees your password. Anti-fingerprint Playwright replays sessions that pass Cloudflare and Kasada
- **Remote desktop** - VNC and RDP into the container to watch or interact with GUI apps
- **1Password secrets** - API keys resolved at runtime, never written to disk
- **1Password secrets** - list document names in `.devcell.toml`; fields are injected as env vars into the container at runtime, written to a RAM-only tmpfs, gone when the container stops
- **Docker or VM engine** - default: Docker container. Add `--macos` to provision a Debian ARM64 VM via Vagrant + UTM instead — same nixhome toolchain, same commands, no Docker Desktop required
- **7 image stacks** - from minimal (`base`) to everything-included (`ultimate`)
- **Local ollama models** - route Claude through local models, ranked by SWE-Bench scores
- **Model ranking** - `cell models` shows cloud models (Anthropic, OpenAI, Google via OpenRouter) and local ollama models ranked by SWE-Bench score and speed, side by side

## Stacks

Seven stacks, published to `ghcr.io/dimmkirr/devcell`. Multi-arch: linux/amd64, linux/arm64.
Seven stacks, published to `public.ecr.aws/w1l3v2k8/devcell`. Multi-arch: linux/amd64, linux/arm64.

| Stack | What's inside |
|---|---|
Expand All @@ -38,9 +39,40 @@ Seven stacks, published to `ghcr.io/dimmkirr/devcell`. Multi-arch: linux/amd64,
| **node** | base + Node.js 22, npm, stealth Chromium |
| **python** | base + Python 3.13, uv, stealth Chromium |
| **fullstack** | go + node + python |
| **electronics** | base + GUI desktop + KiCad, ngspice, ESPHome, wokwi-cli |
| **electronics** | base + GUI desktop + KiCad, ngspice, ESPHome, PlatformIO, wokwi-cli |
| **ultimate** | fullstack + GUI desktop, all MCP servers, Inkscape, KiCad *(default)* |

Add-on modules (set `modules = ["android"]` in `.devcell.toml`):

| Module | What's inside |
|---|---|
| **android** | ADB + fastboot (all platforms), Android SDK + build-tools + emulator + apktool + jadx (x86_64 only) |
| **desktop** | GUI desktop: VNC, RDP, Fluxbox, PulseAudio |
| **scraping** | Playwright stealth scripts, anti-fingerprint Chromium config |
| **infra** | Cloud CLI tools: AWS, GCP, Azure |

## Vagrant engine (no Docker required)

Run cells as native VMs instead of Docker containers — useful for Apple Silicon without Docker Desktop, or when you need full Linux kernel features (KVM, `/dev/kvm`).

```bash
cell claude --macos # provision Debian ARM64 VM via UTM, then open Claude Code
cell build --macos # re-apply nixhome flake inside the VM
cell build --update --macos # nix flake update inside VM, then re-provision
cell rdp --list # shows docker + vagrant cells side by side
```

Set permanently in `.devcell.toml`:

```toml
[cell]
engine = "vagrant"
vagrant_provider = "utm" # utm (macOS) or libvirt (Linux)
vagrant_box = "utm/bookworm"
```

On first run the CLI scaffolds a `Vagrantfile`, starts the VM, installs Nix single-user, and applies the same home-manager configuration used by Docker images. Subsequent runs detect whether provisioning is needed and skip it if the binary is already present.

## MCP servers

Baked into the image and auto-merged into each agent's config at container startup. User-defined servers are preserved. Where applicable, the backing tools ship too: KiCad, Inkscape, and OpenTofu are installed alongside their MCP servers, so the agent can run `tofu plan`, analyze PCBs, or edit SVGs. New servers ship with image updates.
Expand All @@ -60,6 +92,21 @@ Baked into the image and auto-merged into each agent's config at container start
| Notion | Database and page management | OAuth 2.1 |
| MCP-NixOS | Nix package search and docs | None |

## Browser login & anti-bot protection

`cell login` lets the agent use authenticated sessions without ever seeing passwords:

```bash
cell login https://example.com # opens a real browser on your host
# you log in normally, press Enter
# cookies + localStorage sync to the container
cell login --force https://... # wipe saved session and start fresh
```

**How it avoids bot detection:** the login browser opens with no CDP debugging port — no `--remote-debugging-port`, no special flags. Cloudflare, Kasada, and similar systems cannot detect it as automated. After you close the browser, a separate headless CDP instance reads the cookies from the same profile and writes `storage-state.json` for Playwright. The agent replays the session; your password is never exposed.

The fingerprint (`User-Agent`, platform, browser brands) is read from your real installed Chrome binary and saved alongside the session so Patchright uses an identical identity.

## Security

- Project directory mounted at `/workspace`. Host filesystem is unreachable
Expand Down
12 changes: 7 additions & 5 deletions cmd/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,17 @@ func TestClaude_WithUserArgs(t *testing.T) {
// --- codex ---

func TestCodex_DefaultFlags(t *testing.T) {
argv := buildTestArgv("codex", []string{"--dangerously-bypass-approvals-and-sandbox", "--oss", "-p", "lms"}, nil)
// No ollama: only --dangerously-bypass-approvals-and-sandbox; no --oss.
argv := buildTestArgv("codex", []string{"--dangerously-bypass-approvals-and-sandbox"}, nil)
tail := trailingAfterImage(argv)
if tail[0] != "codex" {
t.Errorf("expected codex binary, got: %v", tail)
}
for _, flag := range []string{"--dangerously-bypass-approvals-and-sandbox", "--oss", "-p", "lms"} {
if !hasArg(tail, flag) {
t.Errorf("missing flag %q in tail: %v", flag, tail)
}
if !hasArg(tail, "--dangerously-bypass-approvals-and-sandbox") {
t.Errorf("missing --dangerously-bypass-approvals-and-sandbox in tail: %v", tail)
}
if hasArg(tail, "--oss") {
t.Errorf("unexpected --oss without ollama in tail: %v", tail)
}
}

Expand Down
28 changes: 22 additions & 6 deletions cmd/apparg.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"bytes"
"fmt"
"os/exec"
"sort"
"strings"
Expand Down Expand Up @@ -57,14 +58,29 @@ func parseContainerNames(output string) []string {
}

// selectCell shows an interactive picker when multiple cells are running.
// Returns the selected AppName.
// Labels show "<name> docker" or "<name> vagrant". Returns the selected key.
func selectCell(apps map[string]string) (string, error) {
var names []string
for name := range apps {
names = append(names, name)
var keys []string
for key := range apps {
keys = append(keys, key)
}
sort.Strings(keys)
opts := make([]ux.SelectOption, len(keys))
for i, key := range keys {
var displayName, cellType string
if strings.HasPrefix(key, "vagrant-") {
displayName = strings.TrimPrefix(key, "vagrant-")
cellType = "vagrant"
} else {
displayName = key
cellType = "docker"
}
opts[i] = ux.SelectOption{
Label: fmt.Sprintf("%-28s %s", displayName, cellType),
Value: key,
}
}
sort.Strings(names)
return ux.GetSelection("Multiple cells found — select one", names)
return ux.GetSelectionKV("Multiple cells found — select one", opts)
}

// completeRunningApps provides shell completion for running cell container names.
Expand Down
42 changes: 42 additions & 0 deletions cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"os"

"github.com/DimmKirr/devcell/internal/cfg"
"github.com/DimmKirr/devcell/internal/config"
Expand Down Expand Up @@ -31,6 +32,47 @@ func runBuild(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("load config: %w", err)
}

// ── Vagrant engine ────────────────────────────────────────────────────────
// cell build --engine=vagrant → vagrant provision (re-applies nixhome flake)
// cell build --update --engine=vagrant → nix flake update inside VM, then provision
engine := scanStringFlag("--engine")
if scanFlag("--macos") {
engine = "vagrant"
}
if engine == "vagrant" {
cellCfgVagrant := cfg.LoadFromOS(c.ConfigDir, c.BaseDir)
vagrantBox := scanStringFlag("--vagrant-box")
if vagrantBox == "" {
vagrantBox = "utm/bookworm"
}
vagrantProvider := scanStringFlag("--vagrant-provider")
if vagrantProvider == "" {
vagrantProvider = "utm"
}
// Scaffold Vagrantfile idempotently (same as runVagrantAgent step 1).
nixhomeDir := resolveVagrantNixhome(c.BaseDir)
if nixhomeDir == "" {
nixhomeDir = c.BaseDir + "/nixhome"
}
vmConfigDir := os.Getenv("DEVCELL_CONFIG_DIR")
if vmConfigDir == "" {
vmConfigDir = c.HostHome + "/.config/devcell"
}
// Always regenerate Vagrantfile on build (ports, stack may have changed).
os.Remove(c.BuildDir + "/Vagrantfile")
if err := scaffold.ScaffoldLinuxVagrantfile(
c.BuildDir, vagrantBox, vagrantProvider,
cellCfgVagrant.Cell.ResolvedStack(),
c.BaseDir, nixhomeDir,
c.VNCPort, c.RDPPort,
c.HostHome, vmConfigDir,
); err != nil {
fmt.Fprintf(os.Stderr, "warning: vagrantfile scaffold failed: %v\n", err)
}
return runVagrantBuild(c.BuildDir, c.BaseDir, cellCfgVagrant, update, scanFlag("--dry-run"))
}

// ── Docker engine (default) ───────────────────────────────────────────────
if err := config.EnsureBuildDir(c.BuildDir); err != nil {
return fmt.Errorf("ensure build dir: %w", err)
}
Expand Down
Loading
Loading