# Cursor Agent in der Sandbox (`bubblewrap`)

Wrapper um `cursor-agent` mit [bubblewrap](https://github.com/containers/bubblewrap) (`bwrap`). Der Agent sieht vor allem das aktuelle Projekt und ein isoliertes Home — nicht das gesamte Benutzerverzeichnis. Beim normalen Start wird die **letzte Chat-Session** fortgesetzt (`--continue`).

| Datei | Rolle |
| --- | --- |
| `cursor-agent-sandboxed` | Wrapper inkl. kompletter `bwrap`-Policy, Bin-Allowlist und Session-Logik |
| `cursor-agent-sandboxed.md` | Diese Dokumentation |

**Voraussetzungen:** Linux mit User-Namespaces, `bwrap` im `PATH` (Paket `bubblewrap`), `cursor-agent` unter `/opt/cursor-agent`, sowie Host-Binaries `node` und `rg` unter `/usr/bin/` (siehe unten).

---

## Schnellstart

```bash
cd /pfad/zum/projekt
cursor-agent-sandboxed
```

Entspricht inhaltlich: `cursor-agent --continue` in der Sandbox (letzte Session).

```bash
cursor-agent-sandboxed --version
cursor-agent-sandboxed --list-models
cursor-agent-sandboxed -f "kurze Frage"
PROJECT_DIR=/pfad/zum/projekt cursor-agent-sandboxed
CURSOR_AGENT_SANDBOX_BINS="python3 make" cursor-agent-sandboxed
```

---

## Session-Fortsetzung (`--continue`)

Standardmäßig hängt der Wrapper `--continue` vor die Argumente, damit die letzte Chat-Session geladen wird.

Früher wurde `--resume` verwendet. `--resume` akzeptiert optional eine Chat-ID und würde sonst das nächste Argument (z. B. einen Prompt) als ID schlucken — deshalb `--continue`.

| Situation | Verhalten |
| --- | --- |
| Normaler Start (keine besonderen Flags) | `--continue` wird ergänzt |
| Aufruf enthält schon `--resume`, `--resume=*`, `--continue` | nichts verdoppeln |
| Print/Worktree: `-p`, `--print`, `-w`, `--worktree`, `--worktree=*` | kein Auto-Continue |
| Meta: `--version`, `-v`, `--help`, `-h`, `--list-models` | kein Auto-Continue |
| Subcommands (`login`, `logout`, `mcp`, `resume`, `help`, …) | kein Auto-Continue |

Damit Sessions die Host-Daten finden, werden gebunden:

- `~/.cursor/chats` → `/home/agent/.cursor/chats`
- `~/.cursor/projects` → `/home/agent/.cursor/projects`

Diese beiden Verzeichnisse sind damit **schreibbar auf dem echten Host** (Session-Fortschritt bleibt erhalten).

---

## Was ist bubblewrap?

`bubblewrap` baut eine Umgebung aus **Linux-Namespaces** und **Bind-Mounts**:

- Sichtbare Pfade werden explizit gemountet (`--ro-bind`, `--bind`, `--tmpfs`)
- Nicht gemountetes existiert in der Sandbox nicht (stärker als Landlock bei Metadaten)
- Hier: eigene PID-, IPC- und UTS-Namespaces; Netz bleibt der Host-Stack

Kein Container-Image, typischerweise kein Root. Die Policy liegt vollständig im Wrapper-Skript.

---

## Wrapper-Ablauf

1. Login-Home ermitteln (`getent`) — nicht ein bereits isoliertes `$HOME`.
2. Host-seitiges Sandbox-Home anlegen:  
   `~/.local/state/cursor-agent-sandbox/home`.
3. Dateien aus dem echten Home **kopieren** (Seed):
   - `~/.config/cursor/auth.json`
   - `~/.cursor/cli-config.json`
   - `~/.cursor/agent-cli-state.json`
   - `~/.gitconfig` (falls vorhanden)
4. Allowlist-Binds für `/usr/bin/<name>` aufbauen (`ALLOWED_BINS` + `CURSOR_AGENT_SANDBOX_BINS`).
5. `--continue` bei Bedarf vor die Argumente setzen (siehe oben).
6. `bwrap` starten und darin **`/opt/cursor-agent/cursor-agent`** ausführen (kein PATH-Lookup; der Host-Symlink `/usr/bin/cursor-agent` ist nicht gemountet).

In der Sandbox ist `HOME=/home/agent`. Pfade wie `/home/<user>/.bashrc` existieren dort nicht.

### Umgebungsvariablen

| Variable | Default | Wirkung |
| --- | --- | --- |
| `PROJECT_DIR` | `$PWD` | Workspace (`--bind`, Schreibrecht) |
| `CURSOR_AGENT_REAL_HOME` | Login-Home aus `getent` | Quelle für Seed + Chat/Project-Binds |
| `CURSOR_AGENT_SANDBOX_STATE` | `<real-home>/.local/state/cursor-agent-sandbox` | Host-Wurzel für Sandbox-Home |
| `CURSOR_AGENT_SANDBOX_HOME` | `/home/agent` | `HOME` *innerhalb* der Sandbox |
| `CURSOR_AGENT_SANDBOX_BINDS` | (leer) | Extra Host-Pfade, space-separiert, jeweils rw gebunden |
| `CURSOR_AGENT_SANDBOX_BINS` | (leer) | Extra **Namen** aus `/usr/bin`, space-separiert (z. B. `python3 make`) |

---

## Policy (im Skript)

### Namespaces / Runtime

| Option | Zweck |
| --- | --- |
| `--die-with-parent` | Kind stirbt mit dem Wrapper |
| `--unshare-pid` / `--unshare-ipc` / `--unshare-uts` | Getrennte Namespaces |
| `--proc /proc`, `--dev /dev` | Procfs und Devices |
| `--tmpfs /tmp` | Frisches Temp |
| `--tmpfs /home` | Echtes Home nicht sichtbar |

Netz wird **nicht** unshared (Cursor-API über HTTPS). Es gibt keine Port-Allowlist.

### Dateisystem

| Mount | Zweck |
| --- | --- |
| `--bind <state>/home → /home/agent` | Isoliertes Home (Auth-Kopie, Cache) |
| `--bind $PROJECT_DIR` | Workspace |
| `--bind ~/.cursor/chats`, `~/.cursor/projects` | Host-Sessions für Continue/Resume |
| `--dir /usr/bin` + `--ro-bind` je Allowlist-Binary | Nur ausgewählte Tools sichtbar |
| `--ro-bind /usr/lib` (+ try: lib64, locale, share/locale, ca-certificates) | Shared Libs / Locale / CAs |
| `--ro-bind /opt/cursor-agent` | Agent-Installation |
| `--symlink` `/bin`, `/lib`, `/lib64` → `usr/…` | Arch-usrmerge |
| `--ro-bind-try` ausgewählte `/etc/…` | DNS, NSS, OpenSSL/CAs, passwd/group |

Nicht gemountet (Beispiele): restliches `/usr/bin` (nicht in der Allowlist), restliches `/usr` (`/usr/src`, `/usr/include`, …), restliches Host-Home (`.ssh`, `.bashrc`, …), `/var`, anderes `/opt`.

### Allowlist `/usr/bin` (`ALLOWED_BINS`)

`/usr/bin` wird **nicht mehr komplett** gemountet. Stattdessen legt der Wrapper ein leeres `/usr/bin` an und bindet nur genannte Dateien read-only ein. Fehlende Namen auf dem Host werden übersprungen.

Fest im Skript (Auszug):

| Gruppe | Binaries |
| --- | --- |
| Shell | `bash`, `sh`, `env` |
| Agent-Runtime | `node`, `rg` |
| VCS / Diff | `git`, `diff`, `patch` |
| Text-Basics | `ls`, `cat`, `head`, `tail`, `less`, `grep`, `sed`, `awk`, `find`, … |
| Datei-Ops | `mkdir`, `rm`, `cp`, `mv`, `chmod`, `ln`, `readlink`, `realpath`, … |
| Meta | `pwd`, `date`, `uname`, `id`, `which`, `test`, `true`, `false`, `sleep` |

Zusätzliche Tools für einen Lauf:

```bash
CURSOR_AGENT_SANDBOX_BINS="python3 make" cursor-agent-sandboxed
```

Das ist **kein** striktes Exec-Whitelist auf Kernel-Ebene: Alles unter `/usr/lib` (z. B. Git-Helpers), `/opt/cursor-agent` und dem Projektverzeichnis bleibt erreichbar und ausführbar, sofern der Agent einen Pfad dahin kennt.

#### Warum `node` und `rg` Pflicht sind

- `/opt/cursor-agent/node` ist ein **Symlink** auf `/usr/bin/node`. Ohne Bind von `node` scheitert der Start mit:  
  `/opt/cursor-agent/node: No such file or directory`
- Der Agent ruft **ripgrep** (`rg`) für Suche auf. Fehlt `rg` in der Allowlist:  
  `Could not find ripgrep (rg) binary.`

---

## Sicherheitsmodell und Grenzen

### Typisch verhindert / erschwert

- Lesen des übrigen Home-Inhalts (`.ssh`, `.bashrc`, …) — fehlt in der Sandbox
- Beliebige Tools aus `/usr/bin` (nur Allowlist + optionale Extras)
- Writes außerhalb von Workspace, Sandbox-Home, `/tmp`, sowie Host-`~/.cursor/chats` und `~/.cursor/projects`
- Zugriff auf nicht gemountete Host-Pfade (z. B. `/usr/src`)
- Weiterleben von Kindprozessen nach Wrapper-Ende (PID-NS, `--die-with-parent`)
- PATH-Lookup von `cursor-agent` über `/usr/bin` (Start nur über `/opt/cursor-agent/cursor-agent`)

### Grenzen

| Thema | Erklärung |
| --- | --- |
| **Netzwerk** | Voller Host-Netzstack |
| **`/usr/lib`** | Weiterhin großes Shared-Library- und Helper-Fenster |
| **Allowlist ≠ Seccomp** | Kein Kernel-Exec-Filter; bekannte Pfade unter gemounteten Bäumen sind nutzbar |
| **`~/.cursor/chats` / `projects`** | Bewusst rw auf dem Host — nötig für Sessions, erweitert die Schreibfläche |
| **Projekt unter `/home/<user>/…`** | Elternpfad kann sichtbar werden; Geschwisterdateien typischerweise nicht |
| **Kein VM/Container** | Namespace-Sandbox, kein Multi-Tenant-Ersatz |
| **Kernel / Ressourcen** | Kein Schutz vor Kernel-Bugs oder CPU/RAM-Erschöpfung |

---

## Typische Checks

```bash
cursor-agent-sandboxed --version
cursor-agent-sandboxed --list-models

# Sichtbarkeit: Allowlist vs. volles /usr/bin
# (Skript selbst starten; in der Shell der Sandbox wäre z. B. expected:)
#   HOME=/home/agent
#   /usr/bin/node und /usr/bin/rg vorhanden
#   /usr/bin/python3 fehlt (außer via CURSOR_AGENT_SANDBOX_BINS)
#   /home/<user>/.bashrc fehlt, /usr/src fehlt
```

Manuelle Kurzprobe ohne den Wrapper (vereinfacht, ganzes `/usr/bin` — nur zur Namespace-Idee):

```bash
STATE="$HOME/.local/state/cursor-agent-sandbox/home"
# REAL_HOME nutzen, falls $HOME schon /home/agent ist:
# STATE="$(getent passwd "$(id -un)" | cut -d: -f6)/.local/state/cursor-agent-sandbox/home"
bwrap --die-with-parent --unshare-pid --tmpfs /home \
  --bind "$STATE" /home/agent --bind "$PWD" "$PWD" \
  --dir /usr/bin \
  --ro-bind /usr/bin/bash /usr/bin/bash \
  --ro-bind /usr/bin/ls /usr/bin/ls \
  --ro-bind /usr/lib /usr/lib \
  --symlink usr/bin /bin --symlink usr/lib /lib \
  --proc /proc --dev /dev --tmpfs /tmp \
  --setenv HOME /home/agent --chdir "$PWD" \
  -- /usr/bin/bash -c 'echo HOME=$HOME; ls /usr/bin; ls /usr/src'
```

---

## Anpassen

Policy = `bwrap`-Argumentliste, `ALLOWED_BINS` und Continue-Block in `cursor-agent-sandboxed`.

1. Extra Projektpfade: `CURSOR_AGENT_SANDBOX_BINDS`.
2. Extra Tools aus `/usr/bin`: `CURSOR_AGENT_SANDBOX_BINS` oder Eintrag in `ALLOWED_BINS`.
3. Ohne automatisches Continue: Logik im Skript entfernen oder Aufruf mit Meta-/Session-Flag.
4. Engeres Netz: Host-Firewall; oder `--unshare-net` (dann kein Cursor-API-Zugang ohne Proxy).

---

## Troubleshooting

| Symptom | Mögliche Ursache |
| --- | --- |
| `bwrap … not found` | Paket `bubblewrap` fehlt |
| Namespace-Fehler | User-Namespaces vom Kernel/Distro eingeschränkt |
| `/opt/cursor-agent/node: No such file or directory` | `/usr/bin/node` nicht in der Allowlist (Symlink aus `/opt/cursor-agent`) |
| `Could not find ripgrep (rg) binary` | `rg` fehlt in der Allowlist oder ist auf dem Host nicht installiert |
| Node / Agent startet nicht | `/opt/cursor-agent` oder `/usr/lib` nicht gebunden; fehlender Dynamic Linker (usrmerge-Symlinks) |
| OpenSSL / Zertifikate | `/etc/ssl` oder `/etc/ca-certificates` fehlen |
| `Workspace Trust Required` | interaktiv bestätigen oder `-f` / `--trust` |
| Auth-Fehler | `~/.config/cursor/auth.json` fehlt auf dem Host |
| Session findet nichts | `~/.cursor/chats` leer bzw. Bind fehlgeschlagen |
| Tool fehlt (`python3`, …) | nicht in Allowlist — `CURSOR_AGENT_SANDBOX_BINS` oder Skript erweitern |
| `cursor-agent: command not found` in der Sandbox | erwartet — Start nur über `/opt/cursor-agent/cursor-agent` |

---

## Referenzen

- bubblewrap: <https://github.com/containers/bubblewrap>
- Arch-Paket: `bubblewrap`
