Metadata-Version: 2.4
Name: chatspace_multiply
Version: 1.2.0
Summary: Universal sandboxed CLI workspace manager - terminal applications as isolated, disposable chats.
Author: ChatSpace Engineers
License: MIT
Keywords: sandbox,workspace,cli,isolation,proxychains
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: System :: Systems Administration
Classifier: Topic :: Terminals
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# ChatSpace

[![CI](https://github.com/hello1213913/chatspace/actions/workflows/ci.yml/badge.svg)](https://github.com/hello1213913/chatspace/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/chatspace-multiply)](https://pypi.org/project/chatspace_multiply/)
![coverage gate ≥80%](https://img.shields.io/badge/coverage-gate%20%E2%89%A580%25-brightgreen)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

**Treat terminal applications like chats.**

| Rooms | Health |
|---|---|
| ![ChatSpace rooms view](docs/screenshots/rooms.png) | ![ChatSpace health view](docs/screenshots/health.png) |

ChatSpace is a **Linux-only**, zero-dependency (Python stdlib only) workspace & isolation engine — its sandbox *is* Linux namespaces via bubblewrap, so there is no macOS/Windows port by design. Every "chat" is a disposable install of *any* CLI app — `opencode`, `zcode`, `bash`, `python`, anything — with its own `HOME`, `machine-id`, `hostname` and proxy. (The live coverage number is published on every CI run's Summary page and as the `coverage-report` artifact; the badge states the enforced gate.)

```
$ chatspace new research opencode      # fresh sandboxed opencode
$ chatspace open research              # re-enter same chat later
$ chatspace list --filter running      # what is live right now
$ chatspace stop research              # kill sessions, keep everything
$ chatspace delete research --yes      # kill PGID + rm -rf
```
`stop` terminates every live session (foreground pgids, a background tmux
session, escaped daemons) and leaves the room idle — files, registry entry,
logins and settings all stay; it is the non-destructive sibling of `delete`.
Deleting a chat leaves **zero** files — `rm -rf` + `killpg` the whole process group.

---

## Adversarially tested

**41 escape attempts, all blocked.** A fake malicious agent is launched inside rooms through the real sandbox composition and tries to escape, spy, and cause damage — every attack is a unittest re-proven on every CI run: host-file tampering against `--readonly`, workspace/registry/sibling-room spying (including a filesystem-wide canary grep), the full 12-syscall seccomp denylist (EPERM, nothing executed), VPN kill-switch fail-closed behavior, deletion forensics, the `--readonly` portal escape (session bus masked, GTK portal off), and creative escapes via `/proc/1/root`, symlinks and `unshare`. Full attack table, skip grading and honest limits (private rooms edit host files **by design**; seccomp is attack-surface reduction, not confinement): [`tests/adversarial/README.md`](tests/adversarial/README.md).

```bash
$ python -m unittest discover -s tests/adversarial -t . -v   # ~3 s
```

---

## 1) Install & run — from your terminal

```bash
# from PyPI (recommended):
$ pip install chatspace_multiply
# or always-current from the repo:
$ pip install git+https://github.com/hello1213913/chatspace.git
# or from a source checkout:
$ pip install --break-system-packages -e .
$ hash -r
$ chatspace --help          # the command is `chatspace` either way
$ chatspace          # ← no args → opens shell like opencode
$ chatspace shell    # same, full-screen TUI with /help
```

Workspace is `<your-home>/.chatspace` (dynamic `Path.home()`), override with `CHATSPACE_HOME` or `--base-dir`.

**Shell completions + man page** (optional, from the repo root):

```bash
# bash
$ mkdir -p ~/.local/share/bash-completion/completions && cp completions/chatspace.bash ~/.local/share/bash-completion/completions/chatspace
# zsh (fpath += ~/.zfunc in your ~/.zshrc)
$ mkdir -p ~/.zfunc && cp completions/chatspace.zsh ~/.zfunc/_chatspace
# fish
$ cp completions/chatspace.fish ~/.config/fish/completions/chatspace.fish
# man page
$ mkdir -p ~/.local/share/man/man1 && cp docs/man/chatspace.1 ~/.local/share/man/man1/ && mandb 2>/dev/null; man chatspace
```

Room names are completed live from `chatspace list --json`.

> A condensed step-by-step run guide also ships in [`howto_run.txt`](howto_run.txt);
> architecture notes live in [`docs/architecture.md`](docs/architecture.md).

---

## 2) Full terminal walkthrough — create → open → list → delete

All below is copy-pasteable. Uses a temp workspace so your real `~/.chatspace` is untouched.

```bash
$ export CHATSPACE_HOME=/tmp/demo/.chatspace
$ chatspace doctor
chatspace doctor · python 3.12.3
  ✓ tool:opencode            /home/user/.opencode/bin/opencode
  ✓ wrapper:bwrap            /usr/bin/bwrap (kernel-level spoof active)
  ✓ workspace                writable at /tmp/demo/.chatspace
  installed AI tools: opencode

$ chatspace new research "python -c 'print(\"hi\")'"
hi
# → creates /tmp/demo/.chatspace/chats/research/{tmp,.config,.cache,.local/share}

$ chatspace new --private --device laptop work opencode
# → creates /tmp/demo/.chatspace/chats/work/{machine-id,hostname,boot-id}
#    with spoofed IDs: machine-id=6627c523bb7b..., hostname=laptop-f31b

$ chatspace new --proxy http://127.0.0.1:4000 routed "python -c 'import os; print(os.environ[\"http_proxy\"])'"
http://127.0.0.1:4000
# → also writes /tmp/demo/.chatspace/chats/routed/proxychains.conf

$ chatspace list
Chats (3)

NAME     STATUS  COMMAND              PROXY                    HOSTNAME      CREATED           PATH
------   ------  -------------------  -----------------------  ------------  ----------------  -----------------------------
research IDLE    python -c 'print…'  -                        -             2026-08-27 07:00  /tmp/demo/.chatspace/chats/research
work     IDLE    opencode             -                        laptop-f31b   2026-08-27 07:00  /tmp/demo/.chatspace/chats/work
routed   IDLE    python -c 'import…' http://127.0.0.1:4000  -             2026-08-27 07:00  /tmp/demo/.chatspace/chats/routed

3 chat(s), 0 running · storage: /tmp/demo/.chatspace

$ chatspace list --json | head -20
[
  {
    "chat_name": "research",
    "command": ["python", "-c", "print(\"hi\")"],
    "proxy": null,
    "pgid": null,
    "status": "idle",
    "machine_id": null,
    "hostname": null,
    ...
    "directory": "/tmp/demo/.chatspace/chats/research"
  }
]

$ chatspace open research
hi
# re-enters same HOME/XDG, same status → IDLE again

$ chatspace run research -- python --version
Python 3.12.3
# one-off command in research's identity, stored command unchanged

$ chatspace open routed --direct
# one normal-network session, saved proxy kept for next open

$ chatspace delete research --yes
deleted chat 'research': files removed
$ ls /tmp/demo/.chatspace/chats/research
ls: No such file or directory  # zero footprint

$ chatspace delete work --yes
deleted chat 'work': files removed
$ chatspace doctor --json | grep healthy
  "healthy": true,
```

**Private device — per-session only, host stays real:**
```bash
$ chatspace new --private --device desktop gaming opencode
$ cat /tmp/demo/.chatspace/chats/gaming/machine-id
6f17305c9fecf02538ac4bbb4709e462
$ cat /etc/machine-id          # host still real
d01cb513986090407de1c04669932ff8
$ chatspace run gaming -- sh -c 'cat /etc/machine-id; hostname; cat /etc/hostname'
6f17305c9fecf02538ac4bbb4709e462   # spoofed via bwrap --ro-bind
desktop-a3b9                      # spoofed via --hostname + /etc/hostname
desktop-a3b9
$ cat /etc/machine-id          # after exit, host still real
d01cb513986090407de1c04669932ff8
```

**Multiple same apps simultaneously:**
```bash
$ chatspace new --private app1 opencode &  # Tab 1
$ chatspace new --private app2 opencode &  # Tab 2 (parallel, different HOME/machine-id)
$ chatspace list
app1  RUNNING  opencode  -  laptop-bfea
app2  RUNNING  opencode  -  desktop-9291
# Same chat twice in two terminals:
$ chatspace open shared --concurrent  # 2nd terminal, same chat → RUNNING×2
```

**Shell & new terminal:**
```bash
$ chatspace shell
# → full-screen TUI like opencode (centered CHATSPACE logo, Ask anything... box)
# chatspace> /help          # toggles help overlay
# chatspace> /new myapp python -c "print(1)"
# chatspace> /open myapp --new-terminal   # pops OS window (gnome-terminal/tmux), shell stays free
# chatspace> /exit
$ chatspace new --new-terminal myapp opencode  # directly pop new window
```

---

## 3) All commands (terminal reference)

```text
chatspace new [--proxy URL|local] [--private] [--fully-private] [--device NAME]
              [--machine-id HEX32] [--hostname NAME] [--isolate]
              [--readonly] [--seccomp|--no-seccomp] [--clear-clipboard]
              [--vpn ALIAS] [--private-net | --network NAME]
              [--forward HOST:ROOM]... [--egress CIDRS,...]
              [--detach] [--background] [--from TEMPLATE]
              [--new-terminal]
              <chat_name> <command...>                 create + launch
chatspace open <chat_name> [--proxy URL|local | --direct] [--vpn ALIAS | --no-vpn]
               [--private-net | --network NAME | --no-network]
               [--rotate-identity] [--concurrent] [--detach | --background | --foreground] [--dir FOLDER] [--new-terminal]
                                                      re-launch existing chat
chatspace attach <chat_name>                          reattach a background (tmux) session
chatspace run <chat_name> [--proxy|--direct] [--vpn|--no-vpn] [--private-net|--network NAME|--no-network] [--concurrent] [--detach | --background | --foreground] [--dir FOLDER] [--new-terminal] <cmd...>
                                                      one-off cmd in a chat's identity
chatspace clone <src> <dst> [--seed] [--fresh-identity]
                                                      duplicate a chat (--seed copies logins + vault)
chatspace export <room> <file.tar.gz> [--sign]        archive a room (config + files + vault)
chatspace import <file.tar.gz> [new-name]             restore a verified bundle
chatspace template save <chat> <name> [--force]       snapshot a chat recipe
chatspace template list [--json] | show | remove      manage templates under ~/.chatspace/templates/
chatspace network add NAME [--subnet CIDR] [--dns IPS] [--egress CIDRS]
chatspace network list|show|status|setup|down|ensure|teardown|unset|remove
                                                      guest networks: private NAT namespaces + virtual LANs
chatspace vault set <room> KEY[=VALUE] | show | list | remove
                                                      per-room secrets injected at launch (never logged)
chatspace audit [--room NAME] [--action TYPE] [--limit N] [--json]
                                                      append-only room/identity/routing event log
chatspace edit <chat_name> --private | --fully-private | --real-device [--device NAME]
                                                      convert a chat's identity mode (next launch)
chatspace shell [--simple]                            interactive TUI (/help toggles) or simple REPL
chatspace list [--json] [--filter running|idle]         table or JSON
chatspace stop <chat_name>                              kill PGID(s) + tmux, keep all files
chatspace stop --all                                    stop every live room (idle rooms skipped)
chatspace rename <old> <new>                            move registry entry + directory
chatspace delete <chat_name> [--yes]                   kill PGID(s) + rm -rf
chatspace delete --all --yes [--filter all|running|idle]
                                                      wipe every matching room in one go
chatspace api-key add <name> [--scope read|control]    mint a scoped serve key (token prints ONCE)
chatspace api-key list [--json] | remove <name>        names + scopes only, never tokens
chatspace serve [--port 8777] [--allow-control] [--token TOKEN | --token-file FILE]
                                                      local JSON API (GETs need read, POSTs need control)
chatspace doctor [--json] [--deep]                     health + AI CLIs + sandbox probes
```

Notes:
* `new` options go **before** `<chat_name>`; everything after is the app command (`new q -- bash -i`).
* Privacy is opt-in and **per-session only** — fakes live as `<chat>/machine-id|hostname|boot-id` and are `--ro-bind` mounted only inside `bwrap` (`--bind / /` shares real `/usr`/`/lib` so agents never break). Host stays real.
* Without `--concurrent` an already-`RUNNING` chat refuses to open (safe default).
* `--detach` runs the app inside a background **tmux** session so your terminal stays free; `chatspace attach <name>` reattaches (leave with `Ctrl+B d`, app keeps running), and `delete` tears the whole session down. Requires tmux, never otherwise.
* `--background` (new/open/run) stores the room as a **background room**: this launch and EVERY later open — CLI, GUI double-click, "Open folder…" — run in the background tmux session and **no terminal window is ever held**. Made for GUI apps (browsers, editors), where the wrapper terminal would otherwise sit on screen for the app's whole lifetime; the sudo password for `--vpn` rooms arrives as a desktop dialog (or attach and type it in the pane). `open --foreground` clears the preference; `run --foreground` ignores it once. GUI-app rooms also keep one-click ergonomics: double-click just starts the app, nothing else pops up.
* `--from TEMPLATE` creates the chat from a saved recipe (`template save`); explicitly-passed flags override template values.
* Templates share exact device-identity values by design when saved from `--private` chats — regenerate with `open --rotate-identity`.
* **Privacy is not a one-way door**: an existing room can be converted in place along a three-step ladder — **real device → private → fully-private**. `chatspace edit <room> --private` generates a full fresh spoofed device identity (`--device NAME` shapes the hostname); `chatspace edit <room> --fully-private` deepens it (the real home is hidden behind `/home/<fake-user>`, a fake numeric uid/gid is presented, and OS/CPU strings are generic); `chatspace edit <room> --real-device` drops every layer. The GUI's room right-click menu offers the same ladder (**Make private…** / **Make fully private…** / **Change to private** / **Change to real device**). Every conversion keeps the room's files, command, routing, protections and per-room browser logins untouched and applies on the room's **next launch** (the running session keeps the identity it started with).

### The privacy flags, by exact difference

Each flag adds exactly ONE thing — they control different worlds and stack freely:

| Flag | Name means | Adds / removes |
|---|---|---|
| `--private` | **new identity** — fake device (machine-id, hostname, hardware, username) | adds the fake device; host stays fully readable **and writable** |
| `--fully-private` | **deep identity** — private plus fake home/uid and generic OS/CPU | hides the real home behind `/home/<fake-user>`, presents a fake numeric uid/gid, shows generic OS/CPU strings |
| `--seccomp` | **syscall filter** — "no kernel tricks", default ON for new rooms (`--no-seccomp` opts out) | removes 12 dangerous syscalls (ptrace, kexec, module loading, reboot, swap); removes nothing else |
| `--isolate` | **sealed box** — hide your home | removes visibility of your files (until you grant a folder) |
| `--readonly` | **frozen host** — look, don't touch | removes all host *writes* |

What that means in practice — one folder, `/home/user/myproject`, and an AI CLI in the room:

| What the agent tries | private | private +seccomp | isolate | isolate +grant | readonly |
|---|---|---|---|---|---|
| **Edit myproject files** | ✅ | ✅ same | ❌ can't see them | ✅ granted folder only | ⚠️ sees, write fails |
| **Edit ANY other host file** | ✅ everything | ✅ same | ❌ | ❌ granted only | ❌ |
| **Spy on another app's memory** (ptrace) | ⚠️ possible | ❌ blocked | ⚠️ | ❌ blocked | ⚠️ |
| **Reboot PC / load kernel driver** | ⚠️ possible | ❌ blocked | ⚠️ | ❌ blocked | ⚠️ |

**Does a `--seccomp` room still run code? YES, fully.** Running programs (`execve`), installing packages, builds, network — all unaffected. The filter removes only the 12 kernel-level tricks listed above (things no legitimate CLI agent does). A `--private --seccomp` room is the sweet spot for agents you don't fully trust: full productivity, worst tricks deleted.

**Folder rules by mode:** `private` needs no grant (already read-write everywhere); `--isolate` needs "Open folder…" / `--dir` for any folder; `--readonly` never allows writes; grants are remembered per room and revoked from the same menu.

**Readonly scope, stated plainly:** `--readonly` freezes the *host* — the room's own files stay writable (its profile, cache and `~/Downloads` live inside the room, or GUI apps like Chrome could not start). So a download that lands *inside the room* is correct behavior, not a leak. What readonly additionally closes is the desktop-portal escape: GUI "Save as…" dialogs are served by the host portal unless blocked, so readonly rooms mask the session bus and set `GTK_USE_PORTAL=0` — saves onto the host then fail loudly on the read-only root while opening/uploading (reads) keeps working. Without bwrap there is no enforcement at all and the launch warns that the host stays writable.

### Fully-private (the deep spoof)

`--fully-private` is `--private` plus the layers a fingerprinter reads *after* the device identity: the numeric uid, the `$HOME` path, and the OS/CPU strings. At launch the room's bwrap namespace gains:

* **Fake home** — the real home is hidden behind a tmpfs; the chat dir is the home at `/home/<fake-user>`, so `$HOME`, `pwd` and `/etc/passwd` agree on a path with no trace of you. Host files stay reachable and writable everywhere *except* under your (now invisible) home — grant a folder to re-expose it.
* **Fake numeric uid/gid** — `--unshare-user` maps the room to a deterministic fake uid/gid derived from its machine-id; `id`/`whoami`/`getpwuid` all agree with the already-spoofed username.
* **Generic OS/CPU** — `/etc/os-release` (and `/etc/lsb-release`) report a common Debian, and `/proc/cpuinfo` keeps your real core count under a generic model name, so build tools and `nproc` stay honest.

**Honest limits (by design, not bugs):** `/proc/self/mountinfo` still shows the bind source paths (a determined reader can recover the real chat dir); `sudo`/setuid-root tools don't work inside a single-uid user namespace; group-gated device nodes (GPU render nodes) become unreadable, so GUI apps fall back to software rendering; and the egress IP is orthogonal — route with `--proxy`/`--vpn`. A granted folder under `/home` re-exposes its path skeleton (directory names, not the rest of your home's contents). None of this reduces the agent's ability to run code, edit granted files, and use the network.

---

## 4) Running AI CLIs (verified live)

| Tool | How it ran in a chat | State inside sandbox |
|---|---|---|
| opencode | native binary | `.local/share/opencode/auth.json` |
| Gemini CLI | `npx -y @google/gemini-cli` | `.npm` cache + `HOME` config |
| Claude Code | `npx -y @anthropic-ai/claude-code` | same |
| Codex CLI | `npx -y @openai/codex` | same |
| freebuff | `freebuff` (Node, `~/.config/freebuff`) | `.config/freebuff/credentials.json` |

```bash
$ chatspace new work-freebuff freebuff          # account 1
$ chatspace new personal-freebuff freebuff      # account 2, separate HOME
$ chatspace run work-freebuff -- npx -y @anthropic-ai/claude-code
```

WARP proxy (your 4-session setup, now automated):
```bash
$ chatspace new opencode "opencode"                                      # 1: host IP
$ chatspace new opencode2 "opencode"                                     # 2: isolated HOME, host IP
$ chatspace new --proxy http://127.0.0.1:4000 opencode3 "opencode"        # 3: via WARP
$ chatspace new --private --proxy http://127.0.0.1:4000 opencode4 "opencode" # 4: + spoofed device via WARP
$ chatspace open opencode4  # then /connect
```

Guest networks (own IPs, virtual LANs — host files and tools untouched):
```bash
$ chatspace new --private-net solo "opencode"     # own NAT namespace + IP, invisible to the LAN
$ chatspace network ensure solo                   # build it once (needs sudo)
$ chatspace open solo                             # every launch reuses it
$ chatspace network add lab                       # a virtual LAN for a team of rooms
$ chatspace network setup lab                     # bridge + NAT (needs sudo)
$ chatspace new --network lab --forward 8080:80 web "python -m http.server"
$ chatspace new --network lab db "postgres"       # web reaches db by name; db has no public route unless forwarded
$ chatspace network ensure web && chatspace network ensure db
$ chatspace open web                              # serves on localhost:8080 via the forward
$ chatspace network status                        # bridges, members, namespace states, IPs
$ chatspace vault set web GH_TOKEN                # prompted without echo; injected at every launch
$ chatspace export web web.tar.gz --sign          # portable backup (verified on import)
$ chatspace audit --room web                      # what happened to this room
```
A VPN for the launch still wins over any guest network (one namespace per launch); the room network stays stored for later plain opens. Namespaces don't survive reboot — the first launch after one tells you the exact `network ensure` to re-run.

---

## 5) Features

| Area | What you get |
|---|---|
| Isolation | Per-chat `HOME`, `TMPDIR/TEMP/TMP`, `XDG_*` (absolute), `NPM_CONFIG_PREFIX/CACHE` → `~/.npm-prefix` inside chat |
| Networking | `--proxy http/https/socks4/socks5` → 6 env vars + `proxychains.conf` auto-wrapped when `proxychains4` exists; `--private-net` (own NAT namespace + IP), `--network NAME` (shared virtual LAN with static IPs + name resolution), `--forward H:R` publishes, per-room/per-network egress allowlists |
| Secrets | `vault set <room> KEY[=VALUE]` (prompted, no echo) → injected after credential stripping; never logged; wiped on `delete`, moved on `rename`, copied on `clone --seed` |
| Bundles | `export`/`import` portable room archives (config + files + vault, hash-verified, optional workspace-key HMAC) |
| Audit | Append-only `<base>/audit.log` (lifecycle/identity/routing, secret-free) + `chatspace audit` + `GET /api/audit` |
| API | `serve --token/--token-file` (master) or `api-key` scoped keys (`read` GETs, `control` POSTs); `GET /api/events` live room snapshots (SSE), `GET /api/chats/<name>/output` tmux follow-up, `POST .../stop-all` + `.../delete-all?yes=true&filter=` + `.../create-batch?base=&count=` batch |
| CI | `.github/actions/room-launch` runs one command in a fresh disposable room (`room`/`command`/`private`/`audit` inputs, `exit-code` output) |
| Device | Opt-in `machine-id`/`hostname`/`boot-id` per-chat, stable across `open`s, `bwrap` kernel-level + `$HOSTNAME` fallback |
| Processes | `setsid` PGID list, `SIGTERM` → 3s → `SIGKILL` on whole group; `delete` kills all concurrent pgids |
| TTY | `stdin=None` inherited — colors, `tab`, `ctrl+p` pickers work; `SIGWINCH` forwarded |
| State | Atomic `flock` `state.json`; corrupt quarantined, never destroyed |
| Shell | `chatspace` (no args) → TUI like opencode; `chatspace shell --simple` → line REPL; both support `/help` toggle |
| Background | `open/run/new --detach` → tmux background session (`list` shows `(bg)`); `attach` reattaches; liveness self-heals |
| Reuse | `clone [--seed]` duplicates chats incl. logins; `template save`/`--from` recipes in `<base>/templates/*.json` |

---

## 6) On-disk layout & architecture

```
~/.chatspace/
├── state.json                  # name, command, proxy, pgid/pgids, machine_id/hostname/boot_id, status
├── state.json.lock
└── chats/<chat_name>/
    ├── machine-id, hostname, boot-id   # only for --private, ro-bound
    ├── tmp/  .config/  .cache/  .local/share/  .local/state/  .npm-prefix/
    └── proxychains.conf
```
```
chatspace/
├── cli/              # parser.py + handlers.py + __init__.py (19K → 3 nested files)
├── shell/            # constants.py + dispatch.py + simple.py + tui.py + history.py (31K → 5 files)
├── core/             # engine.py, state.py, sandbox.py, environment.py, bubblewrap.py, proxy.py, diagnostics.py, process.py
├── ui.py, models.py, utils.py, terminal.py
```

---

## 7) Testing

```bash
$ python -m unittest discover -v     # 1000+ tests, incl. the adversarial suite
$ python -m unittest discover -s tests/adversarial -t . -v   # adversarial suite alone
$ python -m tests.cli.test_shell_terminal -v
$ python -m chatspace doctor
```

Every test uses temp dirs — your real `~/.chatspace` untouched. The
adversarial suite (see top) proves the sandbox's guarantees against a
simulated malicious agent; see [`tests/adversarial/README.md`](tests/adversarial/README.md).

---

## 8) Design guarantees

* No hardcoded `/home/user` — `Path.home()` dynamic.
* All paths absolute, chat names `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`, `rm -rf` traversal-safe.
* Sandbox safety: bwrap sandboxes each chat with writable carve-outs for the
  chat dir and private tmp dirs; the host root is **writable by default** so
  agents can edit your files — pass `--readonly` at `new` to freeze the host
  root read-only instead (tamper protection). Plus symlink-proof file writes
  and PID-reuse-guarded process teardown.
* CLI logins (Cline, Claude, Codex, Gemini, …) complete even in proxied
  chats — proxychains bypasses loopback (`localnet`), and the host-browser
  shim forwards your desktop session context. `--private` chats go further:
  login URLs open the host browser pinned to a per-chat profile
  (`<chat>/browser-profile`), so each chat keeps its own browser identity
  (cookies/storage) instead of sharing your host profile, with first-run /
  crash-restore / default-browser dialogs disabled so token flows never
  stall. The browser's canvas/GPU/UA fingerprints stay real — this is
  per-chat storage isolation, not a browser masquerade.
* Private chats additionally: the login browser egresses via the chat's
  proxy (`--proxy-server` / Firefox proxy prefs — one chat == one IP,
  loopback always direct so callbacks work), Firefox profiles get
  Tor-style `privacy.resistFingerprinting` + first-party isolation, and
  `CHATSPACE_BROWSER` overrides browser selection. Each behavior is an
  optional module — delete one file, lose one feature, nothing breaks.
* MIT — see `LICENSE`, `CHANGELOG.md`. Contributing? Start with
  `CONTRIBUTING.md`; security policy in `SECURITY.md`.
