anon-pi 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Dockerfile.pi CHANGED
@@ -4,11 +4,11 @@
4
4
  # bash/grep/find expect.
5
5
  #
6
6
  # HOW anon-pi USES THIS IMAGE (stateful model):
7
- # anon-pi mounts a PERSISTENT per-workdir host dir at the container's
8
- # ~/.pi/agent, so pi's sessions/history/settings and any extensions you
9
- # `pi install` persist across launches. On a FRESH home, anon-pi seeds it once
10
- # from this image's STAGING dir (/opt/anon-pi-seed/agent) + your imported
11
- # models.json, then stamps a marker and never overwrites again.
7
+ # anon-pi mounts a machine's PERSISTENT host home at the container's /root, so
8
+ # pi's sessions/history/settings and any extensions you `pi install` persist
9
+ # across launches. On a FRESH home, anon-pi seeds ~/.pi/agent once from this
10
+ # image's STAGING dir (/opt/anon-pi-seed/agent) + your imported models.json,
11
+ # then stamps a marker and never overwrites again.
12
12
  #
13
13
  # So image defaults (extensions, trust.json) go in the STAGING dir, NOT in
14
14
  # ~/.pi/agent (which is the mount and would be shadowed). Set
@@ -27,13 +27,23 @@ RUN apt-get update \
27
27
  # The official pi coding agent CLI (puts `pi` on PATH).
28
28
  RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent
29
29
 
30
+ # Ensure /root has base shell dotfiles present in the image. The machine home
31
+ # bind-mounts over /root, so on a FRESH home these are the defaults the seed can
32
+ # promote / a shell can fall back to (a bare debian /root may omit .bashrc).
33
+ RUN for f in .bashrc .profile; do \
34
+ [ -f "/root/$f" ] || { [ -f "/etc/skel/$f" ] && cp "/etc/skel/$f" "/root/$f"; }; \
35
+ done; true
36
+
30
37
  # The staging dir anon-pi promotes into a fresh home on first launch.
31
38
  ENV ANON_PI_STAGE=/opt/anon-pi-seed/agent
32
39
  RUN mkdir -p "$ANON_PI_STAGE"
33
40
 
34
- # Trust /work so pi does not prompt on the mounted project (staged, so it lands
35
- # in the persistent home on first launch; anon-pi never writes this for you).
36
- RUN printf '{"/work": true}\n' > "$ANON_PI_STAGE/trust.json"
41
+ # Trust the two cwd roots pi launches into so it never prompts on the mounted
42
+ # project (staged, so it lands in the persistent machine home on first launch;
43
+ # anon-pi never writes this for you):
44
+ # /projects : the projects-root cwd (a project is /projects/<name>)
45
+ # /work : the distinct --mount root cwd (--mount <parent> mounts at /work)
46
+ RUN printf '{"/projects": true, "/work": true}\n' > "$ANON_PI_STAGE/trust.json"
37
47
 
38
48
  # --- Optional: extensions + skills (the capability layer) -------------------
39
49
  # Install into the STAGING dir (PI_CODING_AGENT_DIR), so `pi install` records the
@@ -48,4 +58,6 @@ RUN printf '{"/work": true}\n' > "$ANON_PI_STAGE/trust.json"
48
58
  # installs and configures that service in the image too. See the worked example
49
59
  # examples/Dockerfile.pi-webveil.
50
60
 
51
- WORKDIR /work
61
+ # The default cwd is the projects root /projects (the projects-root mount
62
+ # target); a bare `--mount` re-roots the cwd at the distinct /work instead.
63
+ WORKDIR /projects
package/README.md CHANGED
@@ -1,15 +1,27 @@
1
1
  # anon-pi
2
2
 
3
- Launch [pi](https://github.com/earendil-works/pi-mono) inside a [netcage](https://github.com/wighawag/netcage): all of pi's web and DNS egress is forced through a socks5h proxy (fail-closed, leak-proof), while ONE direct hole is opened to a local model on your LAN. Your pi config is seeded, per-workdir, onto the host; your canonical config is never touched by the container.
3
+ Run [pi](https://github.com/earendil-works/pi-mono) on anonymized, jailed **machines**: all of pi's web and DNS egress is forced through a socks5h proxy (fail-closed, leak-proof) by [netcage](https://github.com/wighawag/netcage), while ONE direct hole is opened to a local model on your LAN. Your machines and their conversations live in a browsable host workspace (`~/.anon-pi/`); the container itself is disposable.
4
4
 
5
- anon-pi is a thin, opinionated launcher over `netcage run`. It is a separate package on purpose: netcage wraps any tool and stays tool-agnostic; anon-pi holds the pi-specific opinion.
5
+ anon-pi is a thin, opinionated launcher over `netcage run`. It is a separate package on purpose: netcage wraps any tool and stays tool-agnostic; anon-pi holds the pi-specific opinion. netcage owns the jail (network namespaces, firewall, DNS); anon-pi never touches Podman directly and never weakens the forced-egress invariant.
6
+
7
+ > **Upgrading from 0.4.0?** The model changed: a bare positional is now a
8
+ > **project**, not a host path, and `import` / `--fresh` / `--ephemeral` are
9
+ > gone. See [Migrating from 0.4.0](#migrating-from-040).
10
+
11
+ ## The model: machines + projects
12
+
13
+ - A **machine** is an **image + a persistent host home**. The home (`machines/<name>/home`) is bind-mounted into the jail at `/root`, so it holds your shell config, pi config + extensions, and your pi conversations. A machine is a named, durable, anonymized workstation. The container is disposable; the home survives.
14
+ - A **project** is a work folder under the **projects root**, mounted into the jail at `/projects/<name>` (pi's cwd). It is just files, image-agnostic. Because pi keys a conversation by its launch cwd, `/projects/<name>` is the conversation key: the same project used from two machines has a separate conversation in each machine's home.
15
+ - The **projects root** is global by default (`~/.anon-pi/projects/`, shared across machines). Override it per-launch with `--mount <host-parent>`, or persistently via `config.json` / `ANON_PI_PROJECTS`.
16
+ - The **proxy** is a `socks5h://` endpoint that anonymizes all egress. It is REQUIRED and never guessed: no proxy, no launch (fail-closed).
17
+ - The **local model** is the one non-proxied path: an RFC1918/link-local `host:port` reached directly, so pi can call a LAN model while everything else stays proxied.
6
18
 
7
19
  ## Requirements
8
20
 
9
21
  - **Linux.** anon-pi inherits netcage's platform reality (network namespaces + nftables + rootless Podman). See [Platform](#platform).
10
22
  - **[`netcage`](https://github.com/wighawag/netcage)** on your `PATH`.
11
- - A running **socks5h proxy** (local Tor, `ssh -D`, ...).
12
- - A **container image with `pi` on its `PATH`** (you provide it via `ANON_PI_IMAGE`; see [Providing a pi image](#providing-a-pi-image)).
23
+ - A running **socks5h proxy** (local Tor, `ssh -D`, wireproxy, ...).
24
+ - A **container image with `pi` on its `PATH`**. `anon-pi init` can build one for you from a shipped `Dockerfile.pi`; see [Providing a pi image](#providing-a-pi-image).
13
25
 
14
26
  ## Install
15
27
 
@@ -19,83 +31,195 @@ npm i -g anon-pi
19
31
  npx anon-pi
20
32
  ```
21
33
 
22
- ## Usage
34
+ ## Quick start
23
35
 
24
36
  ```sh
25
- anon-pi [WORKDIR]
37
+ anon-pi init # one-time: verify your proxy, capture your local model, pick/build an image
38
+ anon-pi # bare: pick a project (or a shell, or a new project) from the menu
39
+ anon-pi recon # or launch straight into a project
26
40
  ```
27
41
 
28
- - `WORKDIR` is the host folder pi works in (mounted at `/work`; pi's cwd). Defaults to the current directory. Files pi writes to `/work` land in this folder on the host.
29
- - The session config+state is keyed to this folder: re-running `anon-pi` on the same folder **resumes** the same pi config and history.
42
+ `init` is interactive and re-runnable. It:
30
43
 
31
- ```sh
32
- export ANON_PI_IMAGE=your/pi-image:tag
33
- export ANON_PI_LLM=192.168.1.150:8080 # your local model, reached directly
34
- export ANON_PI_PROXY=socks5h://127.0.0.1:9050
44
+ 1. **Proxy** — probes common SOCKS ports, confirms SOCKS5 with a real handshake, shows the findings (evidence only, it never labels the exit provider), then runs `netcage verify` and shows the real EXIT IP as proof it is not your host IP. You confirm on that evidence.
45
+ 2. **Local model** — captures the `host:port` of your model, probes reachability, and generates the machine's `models.json`.
46
+ 3. **Image** pick a shipped `Dockerfile` (built via `podman build`), an existing image ref, or skip.
47
+
48
+ It then writes `~/.anon-pi/config.json` + the `default` machine. It **never destroys** an existing home; it pre-fills your current values and only adds/updates config + the default machine.
49
+
50
+ ## Usage
35
51
 
36
- anon-pi import # one-time: generate the seed models.json from your model
37
- anon-pi ./recon # launch
52
+ ```
53
+ anon-pi MENU: pick a project (pi), a shell, or a new project
54
+ anon-pi <project> pi in the project (/projects/<project>); exit pi -> host
55
+ anon-pi <project> <pi-args…> forward args to pi (headless/one-shot; no TTY needed)
56
+ anon-pi --shell [<project>] a jailed bash (at ~, or cd'd into <project>) - the project-hopper
57
+ anon-pi -m <machine> [<p>] the same, on <machine> (its own image + home + conversations)
58
+ anon-pi --mount <parent> [<p>] root at a HOST parent folder instead of the projects root
59
+ anon-pi init onboard: verify your proxy, capture your local model, pick an image
60
+ anon-pi machine … manage machines (create / list / set-image / rm)
61
+ anon-pi --delete-home [<m>] delete a machine's home (config + convos); keep its image pin + files
62
+ anon-pi --delete-project <p> delete a project's files + its per-machine sessions; keep the homes
38
63
  ```
39
64
 
40
- You land in pi, inside the jail, cwd `/work` = `./recon`. pi's web/tool egress is anonymized through the proxy; the local model at `ANON_PI_LLM` is reachable directly; everything else is dropped if the proxy is down (fail-closed).
65
+ A `<project>` is a folder under the projects root (mounted at `/projects`; pi's cwd). The token `.` means the root itself (a scratch pi at `/projects`, at `/work` under `--mount`, or at `~` for a shell). A named project is created on the host if it does not exist yet.
41
66
 
42
- ## Environment
67
+ Every subcommand carries its own help: `anon-pi --help` (the launch surface), `anon-pi init --help`, and `anon-pi machine --help`.
43
68
 
44
- | Var | Required | Default | Meaning |
45
- | --- | --- | --- | --- |
46
- | `ANON_PI_IMAGE` | for run | | container image with `pi` on `PATH` |
47
- | `ANON_PI_LLM` | yes | | RFC1918/link-local `IP[:port]` of the local model (the one direct hole) |
48
- | `ANON_PI_PROXY` | yes | | the socks5h proxy (Tor/wireproxy/ssh -D). No default: it is what anonymizes |
49
- | `ANON_PI_EPHEMERAL` | no | (off) | set to `1` for a throwaway, non-persistent session |
50
- | `ANON_PI_HOME` | no | `$XDG_CONFIG_HOME/anon-pi` or `~/.config/anon-pi` | anon-pi home |
51
- | `ANON_PI_CONFIG` | no | `<ANON_PI_HOME>/agent` | canonical seed dir (holds the imported `models.json`) |
52
- | `ANON_PI_SOURCE_MODELS` | no | `~/.pi/agent/models.json` | (`import`) the host `models.json` to read from |
69
+ ### Common tasks
53
70
 
54
- ## How it works
71
+ | I want to… | Command |
72
+ | --- | --- |
73
+ | Set up (first time / reconfigure) | `anon-pi init` |
74
+ | Just pick something to work on | `anon-pi` (the menu) |
75
+ | Work in a project | `anon-pi <project>` |
76
+ | Resume a project's conversation | `anon-pi <project>` (same machine + project ⇒ same session) |
77
+ | Run a one-shot prompt (scriptable) | `anon-pi <project> -p "…"` |
78
+ | Hop between projects / poke the box | `anon-pi --shell` then `cd /projects/<p> && pi` |
79
+ | A scratch pi not tied to a subfolder | `anon-pi .` |
80
+ | Use a separate anonymized environment | `anon-pi -m <machine> <project>` |
81
+ | Jail pi into a host folder you edit with host tools | `anon-pi --mount <host-parent> <subfolder>` |
82
+ | Install system tools and keep them | `anon-pi --keep --shell` (then `apt install …`) |
83
+ | Add a second machine | `anon-pi machine create <name> --image <ref>` |
84
+ | Reset a machine's conversations | `anon-pi --delete-home [<machine>]` |
85
+ | Delete a project (files + its sessions) | `anon-pi --delete-project <project>` |
55
86
 
56
- **Stateful by default.** anon-pi mounts a persistent per-workdir host dir at the container's `~/.pi/agent`, so pi's **sessions, history, settings (your model choice), and any extensions you `pi install`** all persist across launches. Re-running in the same folder resumes it. The state dir is `<ANON_PI_HOME>/state/<workdir>/agent`, named with pi's own readable path convention (e.g. `--home-me-proj--`), not a hash, so you can see which folder it belongs to and delete it to reset.
87
+ ### A first session, end to end
88
+
89
+ ```sh
90
+ anon-pi init # verify proxy (see the real exit IP), capture your model, build an image
91
+ anon-pi recon # creates ~/.anon-pi/projects/recon, launches pi there
92
+ # … work in pi; exit pi returns you to your host shell …
93
+ anon-pi recon # later: same project, same machine ⇒ your conversation resumes
94
+ anon-pi --shell # sit on the machine: cd /projects/recon && pi, run tmux, etc.
95
+ ```
57
96
 
58
- 1. **Mount the persistent home.** `-v <ANON_PI_HOME>/state/<workdir>/agent:/root/.pi/agent`. Everything pi writes there survives.
59
- 2. **First-launch seed (only when the home is fresh).** anon-pi mounts the image's staged defaults and your imported `models.json`, and the container promotes them into the fresh home, then stamps a `.anon-pi-seed` marker. On later launches the marker is present, so nothing is re-copied and **your changes (added models, installed extensions) are never clobbered**.
60
- 3. **Run.** `netcage run --proxy <proxy> --allow-direct <ANON_PI_LLM> -it -v <workdir> -v <state>:/root/.pi/agent [-v <models.json>:...:ro] <image> sh -c '<seed-if-fresh> && exec pi'`.
97
+ Nothing you care about lives in the container: your conversation, config, and files are all in `~/.anon-pi/`, so the throwaway container going away on exit loses nothing.
61
98
 
62
- pi auto-selects the first available model (your local one; it needs no real API key), so no default needs to be set. Services and default extensions live in the **image**; your state lives in the persistent home.
99
+ ### The bare menu
63
100
 
64
- ### Ephemeral (throwaway) sessions
101
+ Run `anon-pi` with no project and you get an interactive, arrow-key menu (up/down or `k`/`j` to move, Enter to select, Ctrl-C to quit). It lists the projects under the active root and marks which the current machine has already worked on ("used" vs "new here"), plus a `.` here entry, a shell entry, and a "new project" entry. Picking a project launches it **byte-for-byte identically** to typing the equivalent command. `-m <machine>` and `--mount <parent>` with no project also open the menu (scoped to that machine / root).
65
102
 
66
- For a clean, no-local-trace session, pass `--ephemeral` (or `ANON_PI_EPHEMERAL=1`): anon-pi mounts **no writable state** at all. pi writes to the container's own filesystem, which netcage runs with `--rm`, so it is destroyed when the container exits. Nothing writable ever touches a host path (only the read-only `models.json` seed is mounted), there is no cleanup step, and nothing is left behind even if anon-pi is killed.
103
+ The menu needs a TTY. Without one it refuses and tells you to name a project directly (`anon-pi <project>`).
104
+
105
+ ### `--shell`: the project-hopper
106
+
107
+ pi cannot `cd` into a different project mid-session (a conversation is keyed to its launch cwd). So when you want to move between projects, or poke around the machine, use a jailed shell:
108
+
109
+ ```sh
110
+ anon-pi --shell # bash at ~ (the machine home), inside the jail
111
+ anon-pi --shell recon # bash cd'd into /projects/recon
112
+ ```
113
+
114
+ From inside the shell you can `cd` between `/projects/*` and run `pi` yourself in whichever one you want. The shell forwards no arguments (`anon-pi --shell recon extra` is an error); run pi from inside it instead. Same forced-egress jail as a pi launch.
115
+
116
+ ### Headless / one-shot
117
+
118
+ Any tokens after the project are forwarded to pi verbatim, and this path does **not** need a TTY (so it fits scripts and pipes):
67
119
 
68
120
  ```sh
69
- anon-pi --ephemeral ./scratch
121
+ anon-pi recon -p "summarize the findings in ./notes"
70
122
  ```
71
123
 
72
- ### Reset a session
124
+ ### `--mount`: root at a host parent (the caveat)
125
+
126
+ `--mount <parent>` re-roots this launch at a HOST parent folder instead of the projects root: the parent is mounted at `/work`, and a `<project>` positional then names a subfolder under it (`/work/<project>`).
127
+
128
+ The caveat: `<parent>` is a **host parent directory**, not a single project path. anon-pi mounts the parent and treats the positional as a name under it; it does not mount an arbitrary host folder as the project itself. Use `--mount` when you want a whole host tree available at `/work` (for example your real code checkout's parent), and pick the subfolder as the project.
129
+
130
+ ### Kept vs throwaway (`--rm` / `--keep`)
73
131
 
74
- Delete its state home; the next launch re-seeds:
132
+ The container is **throwaway by default** (`--rm`): it is deleted the moment pi exits. Your machine home and project files persist regardless (they are host mounts); only the container's own scratch filesystem is discarded.
133
+
134
+ Pass `--keep` for an exploratory flow where the container's filesystem should survive across exits (for example you `apt install` something, quit, and re-enter the same container):
75
135
 
76
136
  ```sh
77
- rm -rf ~/.config/anon-pi/state/<workdir-slug>/agent
137
+ anon-pi --keep recon # keep this container; re-entering resumes it
78
138
  ```
79
139
 
140
+ anon-pi finds a kept container by netcage's managed label and `netcage start`s it on re-entry. `--keep` and `--rm` together is an error (pick one; `--rm` is the default).
141
+
142
+ ## Managing machines
143
+
144
+ ```
145
+ anon-pi machine create <name> [--image <ref>] create a machine, pin its image
146
+ anon-pi machine list list machines and their images
147
+ anon-pi machine set-image <name> <ref> re-pin the image (WARNS; no reseed)
148
+ anon-pi machine rm <name> [--yes] delete the machine + its home
149
+ ```
150
+
151
+ A machine's home is seeded on FIRST LAUNCH, not at create. `set-image` re-pins the image only and **warns**: it does not reseed or touch the home, so the home's extensions were built for the old image. `rm` confirms on a TTY, skips the prompt with `--yes`, and aborts non-interactively without it (it never deletes unprompted in a script). `create` with no `--image` and no TTY is an error (a machine needs an image to launch).
152
+
153
+ If you never create a machine explicitly, launches use the `default` machine (which `init` creates). Give a machine its own image + home + conversations by naming it with `-m`.
154
+
155
+ ## Deleting data
156
+
157
+ The destructive verbs replace the old `--fresh`. Each confirms on a TTY, skips with `--yes`, and aborts non-interactively without it.
158
+
159
+ - `anon-pi --delete-home [<machine>]` deletes ONE machine's home (its config, conversations, shell env), but **keeps its image pin** (relaunch to seed a fresh home) and **keeps all project files** (they live under the projects root, not in the home). The machine defaults to `config.defaultMachine` (else `default`) when omitted.
160
+ - `anon-pi --delete-project <project>` deletes that project's files (its folder under the projects root) AND that project's per-machine session dir in every machine home. The machine homes are otherwise kept. The project name is required.
161
+
162
+ ## Environment
163
+
164
+ Environment variables are **overrides**: env wins over `config.json`, which wins over a machine's `machine.json`, which wins over the built-in defaults.
165
+
166
+ | Var | Required | Default | Meaning |
167
+ | --- | --- | --- | --- |
168
+ | `ANON_PI_PROXY` | yes | (none) | the socks5h proxy URL (Tor/wireproxy/`ssh -D`). No default: it is what anonymizes, so it is never guessed (fail-closed). |
169
+ | `ANON_PI_LLM` | yes | (none) | RFC1918/link-local `IP[:port]` of the local model (the one direct hole). |
170
+ | `ANON_PI_IMAGE` | fallback | (none) | container image with `pi` on `PATH`, used when a machine has no image pinned. |
171
+ | `ANON_PI_HOME` | no | `~/.anon-pi` | the anon-pi workspace dir (holds `config.json`, `machines/`, `projects/`). NOT under `~/.config`. |
172
+ | `ANON_PI_PROJECTS` | no | `<ANON_PI_HOME>/projects` | projects-root override (the host dir mounted at `/projects`). |
173
+
174
+ `config.json` (written by `init`) holds the persistent `proxy`, `llm`, `defaultMachine`, and optional `projects` root; a machine's `machine.json` holds its pinned `image` and optional per-machine `projects` override. Env vars override all of these per-launch.
175
+
176
+ ## Forced egress: honesty by evidence
177
+
178
+ anon-pi does not tell you "you are anonymous". It composes a launch that **forces** every bit of pi's TCP egress through your `socks5h://` proxy (fail-closed, DNS resolved proxy-side so hostnames never hit your host resolver), with exactly one direct hole to your local model. That is the invariant netcage enforces; anon-pi never strips or adds an egress flag.
179
+
180
+ For proof, it shows you **evidence**, never a label: `init` (and `netcage verify`) run a real request through the proxy and print the actual EXIT IP, so you can see it is not your host IP. It deliberately **never claims which exit provider** you are using (Tor, a VPN, an SSH tunnel): it can only observe the exit, not name the network behind it. A running `tor`/`wireproxy` process is surfaced as a WEAK local hint, clearly not a claim about the exit.
181
+
182
+ ## Layout on disk
183
+
184
+ Everything anon-pi keeps lives under `~/.anon-pi/` (override with `ANON_PI_HOME`):
185
+
186
+ ```
187
+ ~/.anon-pi/
188
+ config.json proxy, llm, defaultMachine, (optional) projects root
189
+ machines/
190
+ default/
191
+ machine.json pinned image, (optional) per-machine projects override
192
+ home/ the persistent $HOME bind-mounted at /root
193
+ .pi/agent/ pi config, extensions, sessions (your conversations)
194
+ models.json the generated local-model seed (mounted read-only on a fresh home)
195
+ projects/ the default global projects root (mounted at /projects)
196
+ recon/
197
+ ...
198
+ ```
199
+
200
+ The home is the durable, inspectable store. On a FRESH machine home, the image's staged defaults (`/opt/anon-pi-seed/agent`) and the machine's `models.json` are promoted in once and a marker is stamped; after that pi owns the home and your changes (added models, installed extensions) are never clobbered.
201
+
80
202
  ## Providing a pi image
81
203
 
82
- anon-pi does not ship or default an image: you set `ANON_PI_IMAGE` to an image that has the `pi` CLI on its `PATH`. pi's maintainers do not publish an official prebuilt image, so the reputable path is to **build a small one from the upstream-documented recipe** (which installs the official [`@earendil-works/pi-coding-agent`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) npm package, no third-party image to trust).
204
+ anon-pi does not ship or default an image: a machine points at an image that has the `pi` CLI on its `PATH`. pi's maintainers do not publish an official prebuilt image, so the reputable path is to **build a small one from the upstream-documented recipe** (which installs the official [`@earendil-works/pi-coding-agent`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) npm package, no third-party image to trust).
83
205
 
84
- A ready `Dockerfile.pi` ships in this package (adapted from pi's own [containerization docs](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/containerization.md)):
206
+ `anon-pi init` can build it for you: pick the shipped `Dockerfile.pi` and it runs `podman build` and pins the result. You can also build it yourself:
85
207
 
86
208
  ```sh
87
209
  # from wherever this package's Dockerfile.pi is (e.g. node_modules/anon-pi)
88
210
  podman build -t localhost/anon-pi-pi:latest -f Dockerfile.pi .
89
211
  export ANON_PI_IMAGE=localhost/anon-pi-pi:latest
212
+ # or pin it to a machine:
213
+ anon-pi machine set-image default localhost/anon-pi-pi:latest
90
214
  ```
91
215
 
92
- The image only needs `pi` reachable on `PATH`. anon-pi passes `pi` as the run command (via a small copy-then-exec step) and never mounts over pi's config dir, so the image needs **no `ENTRYPOINT` and no config volume** (unlike pi's upstream `Dockerfile.pi`, which is written for running pi directly).
216
+ The image only needs `pi` reachable on `PATH`. anon-pi bind-mounts the machine home over the container's `/root` and seeds a fresh home from the image's staging dir, so the image needs **no `ENTRYPOINT` and no config volume**.
93
217
 
94
218
  A community image also exists ([`gni/pi-coding-agent-container`](https://github.com/gni/pi-coding-agent-container)); it is third-party and unvetted, so review it yourself before trusting it with your (anonymized) credentials.
95
219
 
96
220
  ### Extensions, skills, and their services go in the image
97
221
 
98
- anon-pi deliberately imports **only your local model** (see below), never your extensions or skills. That is on purpose: your extension set is an identity fingerprint, extensions run code and can leak, and many need a runtime that a copied folder cannot carry (for example `pi-webveil` needs a running searxng). The right home for capabilities is the **image**, where they are installed once, reviewably, with clean config:
222
+ anon-pi deliberately generates **only your local model's `models.json`**, never copies your extensions or skills. That is on purpose: your extension set is an identity fingerprint, extensions run code and can leak, and many need a runtime that a copied folder cannot carry (for example `pi-webveil` needs a running SearXNG). The right home for capabilities is the **image**, installed once, reviewably, into the STAGING dir so anon-pi promotes them into a fresh machine home:
99
223
 
100
224
  ```dockerfile
101
225
  FROM node:24-bookworm-slim
@@ -103,45 +227,56 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
103
227
  bash ca-certificates git ripgrep && rm -rf /var/lib/apt/lists/*
104
228
  RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent
105
229
 
106
- # Extensions are installed with `pi install` (which records them in settings),
107
- # NOT a global npm install:
108
- # RUN pi install npm:pi-webveil
109
- # ...and an extension that needs a service (pi-webveil -> searxng) also installs
230
+ ENV ANON_PI_STAGE=/opt/anon-pi-seed/agent
231
+ RUN mkdir -p "$ANON_PI_STAGE"
232
+ # Trust the two cwd roots so pi never prompts on the mounted project:
233
+ RUN printf '{"/projects": true, "/work": true}\n' > "$ANON_PI_STAGE/trust.json"
234
+
235
+ # Extensions are installed with `pi install` into the STAGING dir (recorded
236
+ # there, promoted into the fresh home), NOT a global npm install:
237
+ # RUN PI_CODING_AGENT_DIR="$ANON_PI_STAGE" pi install npm:pi-webveil
238
+ # ...and an extension that needs a service (pi-webveil -> SearXNG) also installs
110
239
  # and configures that service in the image. Its egress is forced through the
111
240
  # socks proxy by netcage at runtime, so it must be happy with proxy-only,
112
241
  # DNS-through-proxy networking.
113
242
 
114
- WORKDIR /work
243
+ WORKDIR /projects
115
244
  ```
116
245
 
117
- Install image defaults into the **staging dir** (`PI_CODING_AGENT_DIR=/opt/anon-pi-seed/agent pi install ...`), NOT `~/.pi/agent`: anon-pi mounts a persistent home over `~/.pi/agent`, and promotes the staging dir into it on a fresh launch. Anything you then `pi install` *inside* a session also persists (it is written to the mounted home). See the `Dockerfile.pi` comments for the exact `pi install` form.
246
+ Install image defaults into the **staging dir** (`PI_CODING_AGENT_DIR=/opt/anon-pi-seed/agent pi install ...`), NOT `~/.pi/agent`: anon-pi mounts the persistent home over `/root` (so `~/.pi/agent` is the mount) and promotes the staging dir into it on a fresh launch. Anything you then `pi install` *inside* a session also persists (it is written to the mounted home). See the shipped `Dockerfile.pi` comments for the exact form.
118
247
 
119
- A worked example ships in this package: [`examples/Dockerfile.pi-webveil`](examples/Dockerfile.pi-webveil) builds pi + the `pi-webveil` extension (staged) + a local SearXNG (over a Unix socket, `http-socket` so webveil's `unix:` baseUrl can speak HTTP to it, JSON API on, limiter off), started by an entrypoint that then execs anon-pi's seed-then-pi command. Note the anonymity subtlety it documents: SearXNG's own crawl is anonymized here **because netcage forces every process's egress through the proxy**, so webveil's plain `egress: direct` is correct in-jail (the usual "local SearXNG leaks your IP" caveat does not apply).
248
+ A worked example ships in this package: [`examples/Dockerfile.pi-webveil`](examples/Dockerfile.pi-webveil) builds pi + the `pi-webveil` extension (staged) + a local SearXNG. Note the anonymity subtlety it documents: SearXNG's own crawl is anonymized here **because netcage forces every process's egress through the proxy**, so webveil's plain `egress: direct` is correct in-jail (the usual "local SearXNG leaks your IP" caveat does not apply).
120
249
 
121
- ## Generating the seed (`anon-pi import`)
250
+ ## Trusting the project
122
251
 
123
- anon-pi **never** copies your real pi config. Instead, `anon-pi import` synthesizes a minimal `models.json` from your local model:
124
-
125
- ```sh
126
- export ANON_PI_LLM=192.168.1.150:8080
127
- anon-pi import
128
- ```
252
+ pi treats a mounted project as untrusted until approved. The shipped Dockerfiles stage a `trust.json` trusting `/projects` and `/work` (in `/opt/anon-pi-seed/agent`), which is promoted into the machine home on first launch, so you are not prompted. You can also approve once inside a session; it persists in the home.
129
253
 
130
- It reads your host `~/.pi/agent/models.json` (override with `ANON_PI_SOURCE_MODELS`), finds the provider whose `baseUrl` serves `ANON_PI_LLM` (matched on host:port, so `192.168.1.150:8080` matches `http://192.168.1.150:8080/v1`), and writes **just that provider** to `<ANON_PI_CONFIG>/models.json`. Everything else, your paid providers and their API keys, your sessions, your trust list, is left behind on the host.
254
+ ## Migrating from 0.4.0
131
255
 
132
- That file **seeds a fresh session home** (it is copied in on first launch). Models you later add *inside* pi persist in the session home and are never clobbered by import. To change what a fresh home seeds, re-run `anon-pi import --force`; to apply it to an existing session, reset that session (delete its state home).
256
+ anon-pi 0.4.0 was a **per-workdir** launcher: a bare positional was a host folder mounted at `/work`, session state lived per-workdir under `~/.config/anon-pi/state/<slug>/`, and you seeded it with `anon-pi import`. That model is gone. What changed:
133
257
 
134
- - If no provider matches `ANON_PI_LLM`, it errors and lists the providers it did find.
135
- - If the matched provider carries a real-looking `apiKey` (not `none`/`ollama`/empty), it warns but proceeds (for a local model this is usually fine).
136
- - It refuses to overwrite the canonical seed unless you pass `--force`.
258
+ - **A bare positional is now a PROJECT, not a host path.** `anon-pi ./recon` no longer mounts host `./recon`; it means the project `recon` under the projects root (`/projects/recon`). To make a host folder available, use `--mount <host-parent>` (mounted at `/work`) and select the subfolder as the project.
259
+ - **`anon-pi import` is GONE.** Onboarding is now `anon-pi init`, which (among other things) generates the local-model `models.json` for the default machine. There is no separate import step.
260
+ - **`--fresh` is GONE.** To reset, use the explicit data verbs: `anon-pi --delete-home [<machine>]` (wipe a machine's home, keep its image pin + project files) and `anon-pi --delete-project <project>` (wipe a project's files + its per-machine sessions).
261
+ - **`--ephemeral` / `ANON_PI_EPHEMERAL` are GONE.** The container is **throwaway by default** now (`--rm`); use `--keep` when you want it to survive across exits. There is no separate ephemeral mode.
262
+ - **The layout moved.** Everything now lives under `~/.anon-pi/` (`config.json` + `machines/` + `projects/`), **not** under `~/.config/anon-pi`. `ANON_PI_HOME` still overrides the root; the old `ANON_PI_CONFIG` / `ANON_PI_SOURCE_MODELS` variables are gone.
263
+ - **Old state is NOT migrated.** anon-pi does not read or convert your old `~/.config/anon-pi/state/<slug>/` directories. Once you have moved to the new model you can delete the old tree:
137
264
 
138
- ## Trusting `/work`
265
+ ```sh
266
+ rm -rf ~/.config/anon-pi # old 0.4.0 state; nothing new reads it
267
+ ```
139
268
 
140
- pi treats a mounted project as untrusted until approved. The shipped Dockerfiles stage a `trust.json` trusting `/work` (in `/opt/anon-pi-seed/agent`), which is promoted into the session home on first launch, so you are not prompted. You can also approve once inside a session; it persists.
269
+ Start fresh with `anon-pi init`, then `anon-pi` (the menu) or `anon-pi <project>`.
141
270
 
142
- ## Overriding the config per workdir
271
+ ## Troubleshooting
143
272
 
144
- pi also supports a **project-local** config at `<cwd>/.pi/`, which layers on top of the image's global config. Since your workdir is pi's cwd (`/work`), you can drop a `/work/.pi/` (i.e. `<workdir>/.pi/`) into the folder to override settings for that folder only. anon-pi does nothing special for this; it is pi's normal project-over-global layering.
273
+ - **`netcage not found on PATH`** anon-pi is a launcher for [netcage](https://github.com/wighawag/netcage); install netcage first (Linux only).
274
+ - **`set ANON_PI_PROXY …` (fail-closed)** — no proxy is configured. Run `anon-pi init` to detect + verify one, or export `ANON_PI_PROXY=socks5h://<host:port>`. There is deliberately no default: the proxy is what anonymizes, so it is never guessed.
275
+ - **A launch says the machine has no image** — pin one: `anon-pi machine set-image default <ref>`, or export `ANON_PI_IMAGE=<ref>`, or re-run `anon-pi init` and pick/build a `Dockerfile`. See [Providing a pi image](#providing-a-pi-image).
276
+ - **`no TTY` on a bare `anon-pi`** — the menu and interactive pi need a terminal. In a script, name the project and forward args: `anon-pi <project> <pi-args…>` (that path needs no TTY).
277
+ - **The exit IP looks like your home IP** — the proxy is not actually anonymizing. Re-run `anon-pi init`; its `netcage verify` step prints the real exit IP as proof. anon-pi never claims a provider, only shows you the exit.
278
+ - **A destructive verb won't run in a script** — `machine rm` / `--delete-home` / `--delete-project` confirm on a TTY and abort non-interactively; pass `--yes` to proceed unattended.
279
+ - **`--keep` re-entry started a fresh container** — a kept container is matched by its `(machine, projects-root, project)` identity; changing any of those (a different `-m`, a different `--mount` parent, a different project) is a different launch and gets its own container.
145
280
 
146
281
  ## Platform
147
282