anon-pi 0.4.0 → 0.6.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,198 @@ 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
+ The **first time** you launch anon-pi with no config yet, it welcomes you and runs `init` automatically before launching (so you never hit a bare "set `ANON_PI_PROXY`" wall on day one). You can also run `anon-pi init` yourself any time to reconfigure. (If you drive config purely by env — you export `ANON_PI_PROXY` yourself — the auto-onboarding is skipped.)
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
+ `init` is interactive and re-runnable. It:
45
+
46
+ 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.
47
+ 2. **Local model** — captures the `host:port` of your model, probes it, then **imports models**. It merges two sources, both scoped to that endpoint: the provider in your own `~/.pi/agent/models.json` whose URL matches it (marked `[configured]` — your hand-tuned entries, with their `contextWindow`/`maxTokens`/etc.), and the endpoint's live `/v1/models` (marked `[server]`). You pick which to import (Enter/`c` = all configured, `a` = all, numbers, or `s` = skip) and which is the **default**. Because only the provider served by this endpoint (the one `--allow-direct` hole) is read, no other provider — and no other key — can ever enter the seed. It writes `models.json` + a settings seed that sets the default model. If the matching provider carries a real-looking apiKey, init **refuses** (it would put a host credential into the anon home) unless you pass `--force-allow-local-llm-api-key`.
48
+ 3. **Image** — pick a shipped `Dockerfile` (built via `podman build`), an existing image ref, or skip.
49
+ 4. **Projects root** — the host folder mounted at `/projects` (where bare `anon-pi` looks for projects). Defaults to `~/.anon-pi/projects/`; point it at your own dev folder if you want to jail pi into files you edit with host tools (`--mount <parent>` still overrides it per-launch).
35
50
 
36
- anon-pi import # one-time: generate the seed models.json from your model
37
- anon-pi ./recon # launch
51
+ 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.
52
+
53
+ ## Usage
54
+
55
+ ```
56
+ anon-pi MENU: pick a project (pi), a shell, or a new project
57
+ anon-pi <project> pi in the project (/projects/<project>); exit pi -> host
58
+ anon-pi <project> <pi-args…> forward args to pi (headless/one-shot; no TTY needed)
59
+ anon-pi --shell [<project>] a jailed bash (at ~, or cd'd into <project>) - the project-hopper
60
+ anon-pi -m <machine> [<p>] the same, on <machine> (its own image + home + conversations)
61
+ anon-pi --mount <parent> [<p>] root at a HOST parent folder instead of the projects root
62
+ anon-pi init onboard: verify your proxy, capture your local model, pick an image
63
+ anon-pi machine … manage machines (create / list / set-image / rm)
64
+ anon-pi --delete-home [<m>] delete a machine's home (config + convos); keep its image pin + files
65
+ anon-pi --delete-project <p> delete a project's files + its per-machine sessions; keep the homes
38
66
  ```
39
67
 
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).
68
+ 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
69
 
42
- ## Environment
70
+ Every subcommand carries its own help: `anon-pi --help` (the launch surface), `anon-pi init --help`, and `anon-pi machine --help`.
43
71
 
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 |
72
+ ### Common tasks
53
73
 
54
- ## How it works
74
+ | I want to… | Command |
75
+ | --- | --- |
76
+ | Set up (first time / reconfigure) | `anon-pi init` |
77
+ | Just pick something to work on | `anon-pi` (the menu) |
78
+ | Work in a project | `anon-pi <project>` |
79
+ | Resume a project's conversation | `anon-pi <project>` (same machine + project ⇒ same session) |
80
+ | Run a one-shot prompt (scriptable) | `anon-pi <project> -p "…"` |
81
+ | Hop between projects / poke the box | `anon-pi --shell` then `cd /projects/<p> && pi` |
82
+ | A scratch pi not tied to a subfolder | `anon-pi .` |
83
+ | Use a separate anonymized environment | `anon-pi -m <machine> <project>` |
84
+ | Jail pi into a host folder you edit with host tools | `anon-pi --mount <host-parent> <subfolder>` |
85
+ | Install system tools and keep them | `anon-pi --keep --shell` (then `apt install …`) |
86
+ | Add a second machine | `anon-pi machine create <name> --image <ref>` |
87
+ | Reset a machine's conversations | `anon-pi --delete-home [<machine>]` |
88
+ | Delete a project (files + its sessions) | `anon-pi --delete-project <project>` |
55
89
 
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.
90
+ ### A first session, end to end
57
91
 
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'`.
92
+ ```sh
93
+ anon-pi init # verify proxy (see the real exit IP), capture your model, build an image
94
+ anon-pi recon # creates ~/.anon-pi/projects/recon, launches pi there
95
+ # … work in pi; exit pi returns you to your host shell …
96
+ anon-pi recon # later: same project, same machine ⇒ your conversation resumes
97
+ anon-pi --shell # sit on the machine: cd /projects/recon && pi, run tmux, etc.
98
+ ```
61
99
 
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.
100
+ 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.
63
101
 
64
- ### Ephemeral (throwaway) sessions
102
+ ### The bare menu
65
103
 
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.
104
+ 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).
105
+
106
+ The menu needs a TTY. Without one it refuses and tells you to name a project directly (`anon-pi <project>`).
107
+
108
+ ### `--shell`: the project-hopper
109
+
110
+ 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:
111
+
112
+ ```sh
113
+ anon-pi --shell # bash at ~ (the machine home), inside the jail
114
+ anon-pi --shell recon # bash cd'd into /projects/recon
115
+ ```
116
+
117
+ 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.
118
+
119
+ ### Headless / one-shot
120
+
121
+ 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
122
 
68
123
  ```sh
69
- anon-pi --ephemeral ./scratch
124
+ anon-pi recon -p "summarize the findings in ./notes"
70
125
  ```
71
126
 
72
- ### Reset a session
127
+ ### `--mount`: root at a host parent (the caveat)
128
+
129
+ `--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>`).
130
+
131
+ 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.
73
132
 
74
- Delete its state home; the next launch re-seeds:
133
+ ### Kept vs throwaway (`--rm` / `--keep`)
134
+
135
+ 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.
136
+
137
+ 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
138
 
76
139
  ```sh
77
- rm -rf ~/.config/anon-pi/state/<workdir-slug>/agent
140
+ anon-pi --keep recon # keep this container; re-entering resumes it
141
+ ```
142
+
143
+ 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).
144
+
145
+ ## Managing machines
146
+
147
+ ```
148
+ anon-pi machine create <name> [--image <ref>] create a machine, pin its image
149
+ anon-pi machine list list machines and their images
150
+ anon-pi machine set-image <name> <ref> re-pin the image (WARNS; no reseed)
151
+ anon-pi machine rm <name> [--yes] delete the machine + its home
78
152
  ```
79
153
 
154
+ 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).
155
+
156
+ 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`.
157
+
158
+ ## Deleting data
159
+
160
+ The destructive verbs replace the old `--fresh`. Each confirms on a TTY, skips with `--yes`, and aborts non-interactively without it.
161
+
162
+ - `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.
163
+ - `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.
164
+
165
+ ## Environment
166
+
167
+ Environment variables are **overrides**: env wins over `config.json`, which wins over a machine's `machine.json`, which wins over the built-in defaults.
168
+
169
+ | Var | Required | Default | Meaning |
170
+ | --- | --- | --- | --- |
171
+ | `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). |
172
+ | `ANON_PI_LLM` | yes | (none) | RFC1918/link-local `IP[:port]` of the local model (the one direct hole). |
173
+ | `ANON_PI_IMAGE` | fallback | (none) | container image with `pi` on `PATH`, used when a machine has no image pinned. |
174
+ | `ANON_PI_HOME` | no | `~/.anon-pi` | the anon-pi workspace dir (holds `config.json`, `machines/`, `projects/`). NOT under `~/.config`. |
175
+ | `ANON_PI_PROJECTS` | no | `<ANON_PI_HOME>/projects` | projects-root override (the host dir mounted at `/projects`). |
176
+
177
+ `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.
178
+
179
+ ## Forced egress: honesty by evidence
180
+
181
+ 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.
182
+
183
+ 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.
184
+
185
+ ## Layout on disk
186
+
187
+ Everything anon-pi keeps lives under `~/.anon-pi/` (override with `ANON_PI_HOME`):
188
+
189
+ ```
190
+ ~/.anon-pi/
191
+ config.json proxy, llm, defaultMachine, (optional) projects root
192
+ machines/
193
+ default/
194
+ machine.json pinned image, (optional) per-machine projects override
195
+ home/ the persistent $HOME bind-mounted at /root
196
+ .pi/agent/ pi config, extensions, sessions (your conversations)
197
+ models.json the generated local-model seed (mounted read-only on a fresh home)
198
+ projects/ the default global projects root (mounted at /projects)
199
+ recon/
200
+ ...
201
+ ```
202
+
203
+ 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.
204
+
80
205
  ## Providing a pi image
81
206
 
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).
207
+ 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
208
 
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)):
209
+ `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
210
 
86
211
  ```sh
87
212
  # from wherever this package's Dockerfile.pi is (e.g. node_modules/anon-pi)
88
213
  podman build -t localhost/anon-pi-pi:latest -f Dockerfile.pi .
89
214
  export ANON_PI_IMAGE=localhost/anon-pi-pi:latest
215
+ # or pin it to a machine:
216
+ anon-pi machine set-image default localhost/anon-pi-pi:latest
90
217
  ```
91
218
 
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).
219
+ 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
220
 
94
221
  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
222
 
96
223
  ### Extensions, skills, and their services go in the image
97
224
 
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:
225
+ anon-pi deliberately seeds **only your local model** (the `models.json` provider for the one `--allow-direct` endpoint, plus the default-model selection) — never your extensions or skills, and never any other provider from your config. 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
226
 
100
227
  ```dockerfile
101
228
  FROM node:24-bookworm-slim
@@ -103,45 +230,56 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
103
230
  bash ca-certificates git ripgrep && rm -rf /var/lib/apt/lists/*
104
231
  RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent
105
232
 
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
233
+ ENV ANON_PI_STAGE=/opt/anon-pi-seed/agent
234
+ RUN mkdir -p "$ANON_PI_STAGE"
235
+ # Trust the two cwd roots so pi never prompts on the mounted project:
236
+ RUN printf '{"/projects": true, "/work": true}\n' > "$ANON_PI_STAGE/trust.json"
237
+
238
+ # Extensions are installed with `pi install` into the STAGING dir (recorded
239
+ # there, promoted into the fresh home), NOT a global npm install:
240
+ # RUN PI_CODING_AGENT_DIR="$ANON_PI_STAGE" pi install npm:pi-webveil
241
+ # ...and an extension that needs a service (pi-webveil -> SearXNG) also installs
110
242
  # and configures that service in the image. Its egress is forced through the
111
243
  # socks proxy by netcage at runtime, so it must be happy with proxy-only,
112
244
  # DNS-through-proxy networking.
113
245
 
114
- WORKDIR /work
246
+ WORKDIR /projects
115
247
  ```
116
248
 
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.
249
+ 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
250
 
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).
251
+ 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
252
 
121
- ## Generating the seed (`anon-pi import`)
253
+ ## Trusting the project
122
254
 
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
- ```
255
+ 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
256
 
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.
257
+ ## Migrating from 0.4.0
131
258
 
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).
259
+ 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
260
 
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`.
261
+ - **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.
262
+ - **`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.
263
+ - **`--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).
264
+ - **`--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.
265
+ - **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.
266
+ - **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
267
 
138
- ## Trusting `/work`
268
+ ```sh
269
+ rm -rf ~/.config/anon-pi # old 0.4.0 state; nothing new reads it
270
+ ```
139
271
 
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.
272
+ Start fresh with `anon-pi init`, then `anon-pi` (the menu) or `anon-pi <project>`.
141
273
 
142
- ## Overriding the config per workdir
274
+ ## Troubleshooting
143
275
 
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.
276
+ - **`netcage not found on PATH`** anon-pi is a launcher for [netcage](https://github.com/wighawag/netcage); install netcage first (Linux only).
277
+ - **`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.
278
+ - **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).
279
+ - **`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).
280
+ - **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.
281
+ - **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.
282
+ - **`--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
283
 
146
284
  ## Platform
147
285