anon-pi 0.1.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 +23 -0
- package/LICENSE +661 -0
- package/README.md +117 -0
- package/dist/anon-pi.d.ts +89 -0
- package/dist/anon-pi.d.ts.map +1 -0
- package/dist/anon-pi.js +192 -0
- package/dist/anon-pi.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +76 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
- package/src/anon-pi.ts +277 -0
- package/src/cli.ts +99 -0
- package/src/index.ts +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# anon-pi
|
|
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.
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- **Linux.** anon-pi inherits netcage's platform reality (network namespaces + nftables + rootless Podman). See [Platform](#platform).
|
|
10
|
+
- **[`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)).
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm i -g anon-pi
|
|
18
|
+
# or run without installing:
|
|
19
|
+
npx anon-pi
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
anon-pi [WORKDIR]
|
|
26
|
+
```
|
|
27
|
+
|
|
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.
|
|
30
|
+
|
|
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
|
|
35
|
+
|
|
36
|
+
anon-pi ./recon
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
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).
|
|
40
|
+
|
|
41
|
+
## Environment
|
|
42
|
+
|
|
43
|
+
| Var | Required | Default | Meaning |
|
|
44
|
+
| --- | --- | --- | --- |
|
|
45
|
+
| `ANON_PI_IMAGE` | yes | | container image with `pi` on `PATH` |
|
|
46
|
+
| `ANON_PI_LLM` | yes | | RFC1918/link-local `IP[:port]` of the local model (the one direct hole) |
|
|
47
|
+
| `ANON_PI_PROXY` | no | `socks5h://127.0.0.1:9050` | the socks5h proxy |
|
|
48
|
+
| `ANON_PI_HOME` | no | `$XDG_CONFIG_HOME/anon-pi` or `~/.config/anon-pi` | anon-pi home |
|
|
49
|
+
| `ANON_PI_CONFIG` | no | `<ANON_PI_HOME>/agent` | the canonical seed dir |
|
|
50
|
+
| `ANON_PI_AGENT_MOUNT` | no | `/opt/pi-agent` | absolute container path pi's config is mounted at (see below) |
|
|
51
|
+
|
|
52
|
+
## How it works
|
|
53
|
+
|
|
54
|
+
1. **Seed (once per workdir).** The first time you use a workdir, anon-pi copies your canonical config (`~/.config/anon-pi/agent`) into a per-session dir `~/.config/anon-pi/sessions/<hash-of-workdir>/agent`. The canonical config is only ever READ; it is never mounted into the container, so pi in the jail cannot mutate it.
|
|
55
|
+
2. **Mount.** The session config dir is mounted as pi's global config (`PI_CODING_AGENT_DIR=<mount>`, default `/opt/pi-agent`), and the workdir is mounted at `/work`.
|
|
56
|
+
3. **Run.** anon-pi execs `netcage run --proxy <proxy> --allow-direct <ANON_PI_LLM> -it -v <workdir> -v <session>:<mount> -e PI_CODING_AGENT_DIR=<mount> <image> pi`.
|
|
57
|
+
|
|
58
|
+
### Where pi's config is mounted (`ANON_PI_AGENT_MOUNT`)
|
|
59
|
+
|
|
60
|
+
By default anon-pi mounts the seeded config at `/opt/pi-agent` and points pi there with `PI_CODING_AGENT_DIR`. This absolute, image-independent path is chosen so the podman mount target and pi's config dir agree without anon-pi having to guess your image's home directory.
|
|
61
|
+
|
|
62
|
+
If you would rather pi's config live at its **standard** `~/.pi/agent` inside the container, set `ANON_PI_AGENT_MOUNT` to that home's **absolute** path, e.g. `ANON_PI_AGENT_MOUNT=/root/.pi/agent` for an image that runs as `root` (or `/home/<user>/.pi/agent` otherwise). The value must be absolute: podman does not expand `~`, and anon-pi rejects a `~`-relative or relative value rather than mounting it at a literal `~` directory. Both the mount target and `PI_CODING_AGENT_DIR` always stay in lockstep.
|
|
63
|
+
|
|
64
|
+
## Providing a pi image
|
|
65
|
+
|
|
66
|
+
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).
|
|
67
|
+
|
|
68
|
+
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)):
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
# from wherever this package's Dockerfile.pi is (e.g. node_modules/anon-pi)
|
|
72
|
+
podman build -t localhost/anon-pi-pi:latest -f Dockerfile.pi .
|
|
73
|
+
export ANON_PI_IMAGE=localhost/anon-pi-pi:latest
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The image only needs `pi` reachable on `PATH`. anon-pi passes `pi` as the run command and mounts pi's config itself, so the image needs **no `ENTRYPOINT` and no config volume** (unlike pi's upstream `Dockerfile.pi`, which is written for running pi directly).
|
|
77
|
+
|
|
78
|
+
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.
|
|
79
|
+
|
|
80
|
+
## Populating the seed
|
|
81
|
+
|
|
82
|
+
anon-pi **never** populates the canonical config for you (it will not silently copy your real identity into the anon config). Create it yourself:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
mkdir -p ~/.config/anon-pi/agent
|
|
86
|
+
cp -a ~/.pi/agent/. ~/.config/anon-pi/agent/
|
|
87
|
+
# then remove anything you do NOT want in the anonymized identity
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Put the pi config you want anon sessions to use here: anonymously-created accounts / API keys, the models and skills you want, and a **`trust.json` that trusts `/work`** so pi does not prompt about the (mounted) project on every run. anon-pi does not synthesize pi's `trust.json`; getting a valid one into the seed is your responsibility (copy it from a pi setup where `/work` is trusted, or approve once inside a session and it persists in the session dir).
|
|
91
|
+
|
|
92
|
+
## Reseed
|
|
93
|
+
|
|
94
|
+
Reseed is a manual step: delete the session dir and the next run re-seeds it.
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
rm -rf ~/.config/anon-pi/sessions/<hash>/agent
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Overriding the config per workdir
|
|
101
|
+
|
|
102
|
+
The seeded config is pi's **global** in the container. pi also supports a **project-local** config at `<cwd>/.pi/`, which layers on top of the global. Since your workdir is pi's cwd (`/work`), you can drop a `/work/.pi/` (i.e. `<workdir>/.pi/`) into the folder to override the global for that folder only. anon-pi does nothing special for this; it is pi's normal project-over-global layering. (Make sure your seed's `trust.json` trusts `/work` so the override is honored without a prompt.)
|
|
103
|
+
|
|
104
|
+
## Platform
|
|
105
|
+
|
|
106
|
+
anon-pi is **Linux-only**, because netcage's jail is built on Linux kernel primitives (network namespaces, nftables, `/dev/net/tun`, rootless Podman + pasta). There is no native macOS/Windows jail.
|
|
107
|
+
|
|
108
|
+
On macOS/Windows, Podman runs inside a Linux VM (`podman machine`), so netcage (and anon-pi) can run **inside that VM**. Two caveats matter for anon-pi:
|
|
109
|
+
|
|
110
|
+
- **`--allow-direct` to a LAN model is VM-boundary-sensitive.** "Directly over the LAN" means the *VM's* NIC, not your Mac/Windows host LAN, so a model at an RFC1918 address on the host network may not be directly reachable from inside the VM the way it is on bare Linux.
|
|
111
|
+
- **Host-loopback proxy reachback** (`ssh -D`/Tor on the host's `127.0.0.1`) is the host loopback, not the VM's.
|
|
112
|
+
|
|
113
|
+
Treat non-Linux as best-effort-via-VM, not supported.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
[AGPL-3.0-only](./LICENSE)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** The container path the workdir is mounted at (pi's cwd). */
|
|
2
|
+
export declare const CONTAINER_WORKDIR = "/work";
|
|
3
|
+
/**
|
|
4
|
+
* The DEFAULT container path the seeded pi config is mounted at (the pi global).
|
|
5
|
+
* Absolute and image-independent: both podman (the -v target) and pi (the
|
|
6
|
+
* PI_CODING_AGENT_DIR env) agree on it with no home-resolution guessing. A user
|
|
7
|
+
* who knows their image's home can override it (ANON_PI_AGENT_MOUNT) to the
|
|
8
|
+
* standard `~/.pi/agent` location, e.g. /root/.pi/agent for a root image.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_CONTAINER_AGENT_DIR = "/opt/pi-agent";
|
|
11
|
+
/** The pi env var that overrides its config dir (see pi config.ts getAgentDir). */
|
|
12
|
+
export declare const PI_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
|
|
13
|
+
/** Inputs resolved from the environment + argv, injected so this stays pure. */
|
|
14
|
+
export interface AnonPiEnv {
|
|
15
|
+
/** $HOME (or an override) used to derive default paths. */
|
|
16
|
+
home: string;
|
|
17
|
+
/** socks5h proxy URL. Default socks5h://127.0.0.1:9050. */
|
|
18
|
+
proxy?: string;
|
|
19
|
+
/** The anon-pi home dir. Default $XDG_CONFIG_HOME/anon-pi or ~/.config/anon-pi. */
|
|
20
|
+
anonPiHome?: string;
|
|
21
|
+
/** Override the canonical seed dir. Default <anonPiHome>/agent. */
|
|
22
|
+
configSeed?: string;
|
|
23
|
+
/** The container image that has `pi` on PATH. REQUIRED. */
|
|
24
|
+
image?: string;
|
|
25
|
+
/** The RFC1918/link-local IP[:port] of the local model. REQUIRED. */
|
|
26
|
+
llmDirect?: string;
|
|
27
|
+
/** XDG_CONFIG_HOME, if set (used to derive the default anon-pi home). */
|
|
28
|
+
xdgConfigHome?: string;
|
|
29
|
+
/**
|
|
30
|
+
* The ABSOLUTE container path to mount the seeded config at (and point pi's
|
|
31
|
+
* PI_CODING_AGENT_DIR at). Default /opt/pi-agent. Set it to your image's real
|
|
32
|
+
* `~/.pi/agent` (e.g. /root/.pi/agent) if you want the standard location.
|
|
33
|
+
* MUST be absolute: podman does not expand `~`, so a `~`-relative value would
|
|
34
|
+
* be mounted literally. Rejected if it starts with `~` or is not absolute.
|
|
35
|
+
*/
|
|
36
|
+
agentMount?: string;
|
|
37
|
+
}
|
|
38
|
+
/** The fully-resolved run plan cli.ts executes. */
|
|
39
|
+
export interface RunPlan {
|
|
40
|
+
/** Absolute workdir on the host (mounted at /work). */
|
|
41
|
+
workdir: string;
|
|
42
|
+
/** The per-session seeded config dir on the host (mounted as the pi global). */
|
|
43
|
+
sessionAgentDir: string;
|
|
44
|
+
/** The canonical seed dir to copy FROM (read-only by convention). */
|
|
45
|
+
configSeed: string;
|
|
46
|
+
/** The absolute container path the session config is mounted at (== pi's config dir). */
|
|
47
|
+
agentMount: string;
|
|
48
|
+
/** True iff the session dir does not exist yet and must be seeded from configSeed. */
|
|
49
|
+
needsSeed: boolean;
|
|
50
|
+
/** The argv passed to `netcage` (after the `netcage` program name). */
|
|
51
|
+
netcageArgs: string[];
|
|
52
|
+
}
|
|
53
|
+
/** A user-facing error whose message is meant to be printed verbatim (no stack). */
|
|
54
|
+
export declare class AnonPiError extends Error {
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the container agent-mount path: ANON_PI_AGENT_MOUNT or the default.
|
|
58
|
+
* It MUST be an absolute container path, because it is BOTH the podman `-v`
|
|
59
|
+
* target AND pi's PI_CODING_AGENT_DIR, and podman (unlike pi) does not expand
|
|
60
|
+
* `~`. A `~`-relative or relative value is rejected loudly rather than silently
|
|
61
|
+
* mounted at a literal `~` directory or a cwd-relative path.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveAgentMount(env: AnonPiEnv): string;
|
|
64
|
+
/** Resolve the anon-pi home dir (holds the canonical seed + per-session state). */
|
|
65
|
+
export declare function resolveAnonPiHome(env: AnonPiEnv): string;
|
|
66
|
+
/** The canonical seed dir (copied FROM, never mounted). */
|
|
67
|
+
export declare function resolveConfigSeed(env: AnonPiEnv): string;
|
|
68
|
+
/**
|
|
69
|
+
* Session id = a stable short hash of the ABSOLUTE workdir path, so re-running
|
|
70
|
+
* anon-pi on the same folder resumes the same session config+state, and moving
|
|
71
|
+
* the folder starts a new session (documented, accepted).
|
|
72
|
+
*/
|
|
73
|
+
export declare function sessionId(absWorkdir: string): string;
|
|
74
|
+
/** The per-session seeded config dir on the host for a given workdir. */
|
|
75
|
+
export declare function sessionAgentDir(env: AnonPiEnv, absWorkdir: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Build the run plan from the environment + the (optional) workdir arg. PURE: it
|
|
78
|
+
* resolves paths and composes the netcage argv, and reports whether a seed copy
|
|
79
|
+
* is needed, but performs NO filesystem writes or spawns. It THROWS AnonPiError
|
|
80
|
+
* for the two hard preconditions (missing image, missing llm) so the required
|
|
81
|
+
* inputs fail loud; the missing-SEED check is left to cli.ts (it needs a real
|
|
82
|
+
* `existsSync`), but `needsSeed` is derived from the injected `seedExists`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function buildRunPlan(env: AnonPiEnv, workdirArg: string | undefined, seedExists: (dir: string) => boolean, sessionExists: (dir: string) => boolean): RunPlan;
|
|
85
|
+
/** Read the AnonPiEnv from a process env map (kept separate so tests inject one). */
|
|
86
|
+
export declare function envFromProcess(penv: Record<string, string | undefined>): AnonPiEnv;
|
|
87
|
+
/** The --help text (kept here so it is covered by the same module). */
|
|
88
|
+
export declare const HELP = "anon-pi - launch pi inside a netcage (anonymized egress + one direct local model)\n\nUSAGE\n anon-pi [WORKDIR]\n\n WORKDIR the host folder pi works in (mounted at /work). Defaults to the\n current directory. The session config+state is keyed to this folder.\n\nWHAT IT DOES\n Seeds a per-workdir writable copy of your canonical anon-pi config into\n ~/.config/anon-pi/sessions/<hash>/agent, mounts it as pi's global config\n (PI_CODING_AGENT_DIR=<mount>, default /opt/pi-agent), mounts WORKDIR at\n /work, opens ONE direct hole to your local model, and runs pi with all other\n egress forced through the socks5h proxy, fail-closed. Requires `netcage`.\n\nENVIRONMENT\n ANON_PI_IMAGE (required) image with `pi` on PATH\n ANON_PI_LLM (required) RFC1918/link-local IP[:port] of the local model\n ANON_PI_PROXY socks5h URL (default socks5h://127.0.0.1:9050)\n ANON_PI_HOME anon-pi home (default $XDG_CONFIG_HOME/anon-pi or ~/.config/anon-pi)\n ANON_PI_CONFIG canonical seed dir (default <ANON_PI_HOME>/agent)\n ANON_PI_AGENT_MOUNT absolute container path for pi's config (default\n /opt/pi-agent; set to your image's ~/.pi/agent, e.g. /root/.pi/agent)\n\nRESEED\n Reseed is manual: delete the session dir, e.g.\n rm -rf ~/.config/anon-pi/sessions/<hash>/agent\n and the next run re-seeds it from the canonical config.\n\nPLATFORM\n Linux only (via netcage's netns/nft jail). On macOS/Windows it works only\n inside a Linux VM, where --allow-direct to a LAN model is VM-boundary-sensitive.\n";
|
|
89
|
+
//# sourceMappingURL=anon-pi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anon-pi.d.ts","sourceRoot":"","sources":["../src/anon-pi.ts"],"names":[],"mappings":"AAwBA,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,UAAU,CAAC;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,kBAAkB,CAAC;AAE3D,mFAAmF;AACnF,eAAO,MAAM,gBAAgB,wBAAwB,CAAC;AAEtD,gFAAgF;AAChF,MAAM,WAAW,SAAS;IACzB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,mDAAmD;AACnD,MAAM,WAAW,OAAO;IACvB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,eAAe,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAC;IACnB,yFAAyF;IACzF,UAAU,EAAE,MAAM,CAAC;IACnB,sFAAsF;IACtF,SAAS,EAAE,OAAO,CAAC;IACnB,uEAAuE;IACvE,WAAW,EAAE,MAAM,EAAE,CAAC;CACtB;AAID,oFAAoF;AACpF,qBAAa,WAAY,SAAQ,KAAK;CAAG;AAEzC;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,CAiBxD;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,CAOxD;AAED,2DAA2D;AAC3D,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,CAGxD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAO1E;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC3B,GAAG,EAAE,SAAS,EACd,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,EACpC,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,GACrC,OAAO,CAiET;AAED,qFAAqF;AACrF,wBAAgB,cAAc,CAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACtC,SAAS,CAWX;AAED,uEAAuE;AACvE,eAAO,MAAM,IAAI,khDAgChB,CAAC"}
|
package/dist/anon-pi.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// anon-pi: the PURE logic (no process spawning, no interactive I/O) so every
|
|
2
|
+
// decision is unit-testable. cli.ts wires this to the real filesystem + spawn.
|
|
3
|
+
//
|
|
4
|
+
// What anon-pi does (settled design):
|
|
5
|
+
// - ALWAYS seed a per-workdir writable copy of the canonical anon-pi config
|
|
6
|
+
// (~/.config/anon-pi/agent) into a per-session dir keyed by the workdir, and
|
|
7
|
+
// mount THAT as the container's pi global (PI_CODING_AGENT_DIR). The
|
|
8
|
+
// canonical config is only ever READ (at seed time), never mounted, so the
|
|
9
|
+
// container cannot mutate it.
|
|
10
|
+
// - Mount the workdir separately at /work (pi's cwd; the user's files land on
|
|
11
|
+
// the host). A user-supplied /work/.pi/ override is just pi's own
|
|
12
|
+
// project-over-global layering; anon-pi neither creates nor requires it.
|
|
13
|
+
// - Open exactly ONE direct hole (--allow-direct <ANON_PI_LLM>) so pi can reach
|
|
14
|
+
// a local model while all other egress stays forced through the proxy.
|
|
15
|
+
// - NEVER auto-populate the canonical seed: if it is absent, error and tell the
|
|
16
|
+
// user to populate it (their anon accounts / chosen skills / a valid
|
|
17
|
+
// trust.json that trusts /work). anon-pi does not synthesize pi's trust.json.
|
|
18
|
+
// - Session identity = the ABSOLUTE workdir path (hashed). Same folder resumes
|
|
19
|
+
// the same session config+state; reseed is manual (delete the session dir).
|
|
20
|
+
import { createHash } from 'node:crypto';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
23
|
+
/** The container path the workdir is mounted at (pi's cwd). */
|
|
24
|
+
export const CONTAINER_WORKDIR = '/work';
|
|
25
|
+
/**
|
|
26
|
+
* The DEFAULT container path the seeded pi config is mounted at (the pi global).
|
|
27
|
+
* Absolute and image-independent: both podman (the -v target) and pi (the
|
|
28
|
+
* PI_CODING_AGENT_DIR env) agree on it with no home-resolution guessing. A user
|
|
29
|
+
* who knows their image's home can override it (ANON_PI_AGENT_MOUNT) to the
|
|
30
|
+
* standard `~/.pi/agent` location, e.g. /root/.pi/agent for a root image.
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_CONTAINER_AGENT_DIR = '/opt/pi-agent';
|
|
33
|
+
/** The pi env var that overrides its config dir (see pi config.ts getAgentDir). */
|
|
34
|
+
export const PI_AGENT_DIR_ENV = 'PI_CODING_AGENT_DIR';
|
|
35
|
+
const DEFAULT_PROXY = 'socks5h://127.0.0.1:9050';
|
|
36
|
+
/** A user-facing error whose message is meant to be printed verbatim (no stack). */
|
|
37
|
+
export class AnonPiError extends Error {
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the container agent-mount path: ANON_PI_AGENT_MOUNT or the default.
|
|
41
|
+
* It MUST be an absolute container path, because it is BOTH the podman `-v`
|
|
42
|
+
* target AND pi's PI_CODING_AGENT_DIR, and podman (unlike pi) does not expand
|
|
43
|
+
* `~`. A `~`-relative or relative value is rejected loudly rather than silently
|
|
44
|
+
* mounted at a literal `~` directory or a cwd-relative path.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveAgentMount(env) {
|
|
47
|
+
const raw = env.agentMount && env.agentMount.trim() !== ''
|
|
48
|
+
? env.agentMount.trim()
|
|
49
|
+
: DEFAULT_CONTAINER_AGENT_DIR;
|
|
50
|
+
if (raw.startsWith('~')) {
|
|
51
|
+
throw new AnonPiError(`anon-pi: ANON_PI_AGENT_MOUNT must be an ABSOLUTE container path, not \`${raw}\`. ` +
|
|
52
|
+
'podman does not expand `~`; use the concrete home, e.g. /root/.pi/agent.');
|
|
53
|
+
}
|
|
54
|
+
if (!raw.startsWith('/')) {
|
|
55
|
+
throw new AnonPiError(`anon-pi: ANON_PI_AGENT_MOUNT must be an ABSOLUTE container path, not \`${raw}\` (it is both the mount target and pi's config dir).`);
|
|
56
|
+
}
|
|
57
|
+
return raw;
|
|
58
|
+
}
|
|
59
|
+
/** Resolve the anon-pi home dir (holds the canonical seed + per-session state). */
|
|
60
|
+
export function resolveAnonPiHome(env) {
|
|
61
|
+
if (env.anonPiHome)
|
|
62
|
+
return resolve(env.anonPiHome);
|
|
63
|
+
const base = env.xdgConfigHome && env.xdgConfigHome.trim() !== ''
|
|
64
|
+
? env.xdgConfigHome
|
|
65
|
+
: join(env.home, '.config');
|
|
66
|
+
return join(base, 'anon-pi');
|
|
67
|
+
}
|
|
68
|
+
/** The canonical seed dir (copied FROM, never mounted). */
|
|
69
|
+
export function resolveConfigSeed(env) {
|
|
70
|
+
if (env.configSeed)
|
|
71
|
+
return resolve(env.configSeed);
|
|
72
|
+
return join(resolveAnonPiHome(env), 'agent');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Session id = a stable short hash of the ABSOLUTE workdir path, so re-running
|
|
76
|
+
* anon-pi on the same folder resumes the same session config+state, and moving
|
|
77
|
+
* the folder starts a new session (documented, accepted).
|
|
78
|
+
*/
|
|
79
|
+
export function sessionId(absWorkdir) {
|
|
80
|
+
return createHash('sha256').update(absWorkdir).digest('hex').slice(0, 16);
|
|
81
|
+
}
|
|
82
|
+
/** The per-session seeded config dir on the host for a given workdir. */
|
|
83
|
+
export function sessionAgentDir(env, absWorkdir) {
|
|
84
|
+
return join(resolveAnonPiHome(env), 'sessions', sessionId(absWorkdir), 'agent');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build the run plan from the environment + the (optional) workdir arg. PURE: it
|
|
88
|
+
* resolves paths and composes the netcage argv, and reports whether a seed copy
|
|
89
|
+
* is needed, but performs NO filesystem writes or spawns. It THROWS AnonPiError
|
|
90
|
+
* for the two hard preconditions (missing image, missing llm) so the required
|
|
91
|
+
* inputs fail loud; the missing-SEED check is left to cli.ts (it needs a real
|
|
92
|
+
* `existsSync`), but `needsSeed` is derived from the injected `seedExists`.
|
|
93
|
+
*/
|
|
94
|
+
export function buildRunPlan(env, workdirArg, seedExists, sessionExists) {
|
|
95
|
+
if (!env.image || env.image.trim() === '') {
|
|
96
|
+
throw new AnonPiError('anon-pi: set ANON_PI_IMAGE to a container image that has `pi` on its PATH (e.g. ANON_PI_IMAGE=your/pi-image:tag).');
|
|
97
|
+
}
|
|
98
|
+
if (!env.llmDirect || env.llmDirect.trim() === '') {
|
|
99
|
+
throw new AnonPiError('anon-pi: set ANON_PI_LLM to the RFC1918/link-local IP[:port] of the local model pi should reach directly (e.g. ANON_PI_LLM=192.168.1.150:8080). All other egress stays forced through the proxy.');
|
|
100
|
+
}
|
|
101
|
+
const home = env.home;
|
|
102
|
+
if (!home || home.trim() === '') {
|
|
103
|
+
throw new AnonPiError('anon-pi: could not resolve HOME.');
|
|
104
|
+
}
|
|
105
|
+
const raw = workdirArg && workdirArg.trim() !== '' ? workdirArg : process.cwd();
|
|
106
|
+
const workdir = isAbsolute(raw) ? raw : resolve(raw);
|
|
107
|
+
const configSeed = resolveConfigSeed(env);
|
|
108
|
+
if (!seedExists(configSeed)) {
|
|
109
|
+
throw new AnonPiError(`anon-pi: canonical config not found at ${configSeed}.\n` +
|
|
110
|
+
'anon-pi never populates it for you. Create it yourself with the pi config you want\n' +
|
|
111
|
+
'(anon accounts, chosen models/skills, and a trust.json that trusts /work), e.g.:\n' +
|
|
112
|
+
` mkdir -p ${configSeed}\n` +
|
|
113
|
+
` cp -a ~/.pi/agent/. ${configSeed}/ # then remove any identity you do not want anonymized\n` +
|
|
114
|
+
'See the README (Populating the seed) for the trust.json requirement.');
|
|
115
|
+
}
|
|
116
|
+
const sessionDir = sessionAgentDir(env, workdir);
|
|
117
|
+
const needsSeed = !sessionExists(sessionDir);
|
|
118
|
+
const agentMount = resolveAgentMount(env);
|
|
119
|
+
const proxy = env.proxy && env.proxy.trim() !== '' ? env.proxy : DEFAULT_PROXY;
|
|
120
|
+
const netcageArgs = [
|
|
121
|
+
'run',
|
|
122
|
+
'--proxy',
|
|
123
|
+
proxy,
|
|
124
|
+
'--allow-direct',
|
|
125
|
+
env.llmDirect,
|
|
126
|
+
'-it',
|
|
127
|
+
'-v',
|
|
128
|
+
workdir, // netcage defaults a target-less -v to /work and cwd to /work
|
|
129
|
+
'-v',
|
|
130
|
+
`${sessionDir}:${agentMount}`,
|
|
131
|
+
'-e',
|
|
132
|
+
`${PI_AGENT_DIR_ENV}=${agentMount}`,
|
|
133
|
+
env.image,
|
|
134
|
+
'pi',
|
|
135
|
+
];
|
|
136
|
+
return {
|
|
137
|
+
workdir,
|
|
138
|
+
sessionAgentDir: sessionDir,
|
|
139
|
+
configSeed,
|
|
140
|
+
agentMount,
|
|
141
|
+
needsSeed,
|
|
142
|
+
netcageArgs,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** Read the AnonPiEnv from a process env map (kept separate so tests inject one). */
|
|
146
|
+
export function envFromProcess(penv) {
|
|
147
|
+
return {
|
|
148
|
+
home: penv.HOME ?? homedir(),
|
|
149
|
+
proxy: penv.ANON_PI_PROXY,
|
|
150
|
+
anonPiHome: penv.ANON_PI_HOME,
|
|
151
|
+
configSeed: penv.ANON_PI_CONFIG,
|
|
152
|
+
image: penv.ANON_PI_IMAGE,
|
|
153
|
+
llmDirect: penv.ANON_PI_LLM,
|
|
154
|
+
xdgConfigHome: penv.XDG_CONFIG_HOME,
|
|
155
|
+
agentMount: penv.ANON_PI_AGENT_MOUNT,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** The --help text (kept here so it is covered by the same module). */
|
|
159
|
+
export const HELP = `anon-pi - launch pi inside a netcage (anonymized egress + one direct local model)
|
|
160
|
+
|
|
161
|
+
USAGE
|
|
162
|
+
anon-pi [WORKDIR]
|
|
163
|
+
|
|
164
|
+
WORKDIR the host folder pi works in (mounted at /work). Defaults to the
|
|
165
|
+
current directory. The session config+state is keyed to this folder.
|
|
166
|
+
|
|
167
|
+
WHAT IT DOES
|
|
168
|
+
Seeds a per-workdir writable copy of your canonical anon-pi config into
|
|
169
|
+
~/.config/anon-pi/sessions/<hash>/agent, mounts it as pi's global config
|
|
170
|
+
(${PI_AGENT_DIR_ENV}=<mount>, default ${DEFAULT_CONTAINER_AGENT_DIR}), mounts WORKDIR at
|
|
171
|
+
${CONTAINER_WORKDIR}, opens ONE direct hole to your local model, and runs pi with all other
|
|
172
|
+
egress forced through the socks5h proxy, fail-closed. Requires \`netcage\`.
|
|
173
|
+
|
|
174
|
+
ENVIRONMENT
|
|
175
|
+
ANON_PI_IMAGE (required) image with \`pi\` on PATH
|
|
176
|
+
ANON_PI_LLM (required) RFC1918/link-local IP[:port] of the local model
|
|
177
|
+
ANON_PI_PROXY socks5h URL (default ${DEFAULT_PROXY})
|
|
178
|
+
ANON_PI_HOME anon-pi home (default $XDG_CONFIG_HOME/anon-pi or ~/.config/anon-pi)
|
|
179
|
+
ANON_PI_CONFIG canonical seed dir (default <ANON_PI_HOME>/agent)
|
|
180
|
+
ANON_PI_AGENT_MOUNT absolute container path for pi's config (default
|
|
181
|
+
${DEFAULT_CONTAINER_AGENT_DIR}; set to your image's ~/.pi/agent, e.g. /root/.pi/agent)
|
|
182
|
+
|
|
183
|
+
RESEED
|
|
184
|
+
Reseed is manual: delete the session dir, e.g.
|
|
185
|
+
rm -rf ~/.config/anon-pi/sessions/<hash>/agent
|
|
186
|
+
and the next run re-seeds it from the canonical config.
|
|
187
|
+
|
|
188
|
+
PLATFORM
|
|
189
|
+
Linux only (via netcage's netns/nft jail). On macOS/Windows it works only
|
|
190
|
+
inside a Linux VM, where --allow-direct to a LAN model is VM-boundary-sensitive.
|
|
191
|
+
`;
|
|
192
|
+
//# sourceMappingURL=anon-pi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anon-pi.js","sourceRoot":"","sources":["../src/anon-pi.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// anon-pi CLI: resolve the run plan (pure), do the one filesystem side-effect
|
|
3
|
+
// (seed the session config if absent), then exec `netcage run ...` with inherited
|
|
4
|
+
// stdio so the interactive pi session (-it) passes through the terminal cleanly.
|
|
5
|
+
import { cpSync, existsSync, mkdirSync } from 'node:fs';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { dirname } from 'node:path';
|
|
8
|
+
import { AnonPiError, buildRunPlan, envFromProcess, HELP } from './anon-pi.js';
|
|
9
|
+
function main(argv) {
|
|
10
|
+
const args = argv.slice(2);
|
|
11
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
12
|
+
process.stdout.write(HELP);
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
// The only positional is the optional workdir. Reject stray flags so a typo
|
|
16
|
+
// (e.g. --allow-direct) is not silently swallowed: anon-pi owns the netcage
|
|
17
|
+
// argv, extra flags are not passed through.
|
|
18
|
+
const positionals = args.filter((a) => !a.startsWith('-'));
|
|
19
|
+
const flags = args.filter((a) => a.startsWith('-'));
|
|
20
|
+
if (flags.length > 0) {
|
|
21
|
+
process.stderr.write(`anon-pi: unknown option(s): ${flags.join(' ')}\nRun \`anon-pi --help\`.\n`);
|
|
22
|
+
return 2;
|
|
23
|
+
}
|
|
24
|
+
if (positionals.length > 1) {
|
|
25
|
+
process.stderr.write('anon-pi: too many arguments (expected at most one WORKDIR).\nRun `anon-pi --help`.\n');
|
|
26
|
+
return 2;
|
|
27
|
+
}
|
|
28
|
+
const env = envFromProcess(process.env);
|
|
29
|
+
let plan;
|
|
30
|
+
try {
|
|
31
|
+
plan = buildRunPlan(env, positionals[0], existsSync, existsSync);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
if (e instanceof AnonPiError) {
|
|
35
|
+
process.stderr.write(e.message + '\n');
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
throw e;
|
|
39
|
+
}
|
|
40
|
+
// Fail loud if netcage is not installed, before we mutate anything.
|
|
41
|
+
if (!hasNetcage()) {
|
|
42
|
+
process.stderr.write('anon-pi: `netcage` not found on PATH. anon-pi is a launcher for netcage; install it first\n' +
|
|
43
|
+
'(https://github.com/wighawag/netcage). Linux only.\n');
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
// The one side-effect: seed the per-session config from the canonical seed the
|
|
47
|
+
// FIRST time this workdir is used. Reuse-if-present, seed-if-absent.
|
|
48
|
+
if (plan.needsSeed) {
|
|
49
|
+
mkdirSync(dirname(plan.sessionAgentDir), { recursive: true });
|
|
50
|
+
cpSync(plan.configSeed, plan.sessionAgentDir, { recursive: true });
|
|
51
|
+
process.stderr.write(`anon-pi: seeded session config -> ${plan.sessionAgentDir}\n`);
|
|
52
|
+
}
|
|
53
|
+
// Ensure the workdir exists (a fresh named folder is fine).
|
|
54
|
+
mkdirSync(plan.workdir, { recursive: true });
|
|
55
|
+
// Hand off to netcage with inherited stdio so -it is a real interactive TTY.
|
|
56
|
+
const res = spawnSync('netcage', plan.netcageArgs, { stdio: 'inherit' });
|
|
57
|
+
if (res.error) {
|
|
58
|
+
process.stderr.write(`anon-pi: failed to run netcage: ${res.error.message}\n`);
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
// Propagate netcage's exit code (which itself propagates the tool's).
|
|
62
|
+
return res.status ?? 1;
|
|
63
|
+
}
|
|
64
|
+
function hasNetcage() {
|
|
65
|
+
const which = spawnSync(process.platform === 'win32' ? 'where' : 'command', ['-v', 'netcage'], {
|
|
66
|
+
stdio: 'ignore',
|
|
67
|
+
shell: process.platform !== 'win32',
|
|
68
|
+
});
|
|
69
|
+
if (which.status === 0)
|
|
70
|
+
return true;
|
|
71
|
+
// Fallback: try running it harmlessly.
|
|
72
|
+
const probe = spawnSync('netcage', ['--help'], { stdio: 'ignore' });
|
|
73
|
+
return !probe.error;
|
|
74
|
+
}
|
|
75
|
+
process.exit(main(process.argv));
|
|
76
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,sEAAsE;AACtE,cAAc,cAAc,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "anon-pi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Launch pi inside a netcage: anonymized web egress through a socks5h proxy, one direct hole for a local model, seeded pi config on the host.",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi",
|
|
8
|
+
"netcage",
|
|
9
|
+
"anonymity",
|
|
10
|
+
"socks5h",
|
|
11
|
+
"tor",
|
|
12
|
+
"proxy",
|
|
13
|
+
"privacy",
|
|
14
|
+
"cli",
|
|
15
|
+
"agent"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/wighawag/anon-pi#readme",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/wighawag/anon-pi.git",
|
|
21
|
+
"directory": "packages/anon-pi"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/wighawag/anon-pi/issues"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"bin": {
|
|
31
|
+
"anon-pi": "./dist/cli.js"
|
|
32
|
+
},
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"src",
|
|
42
|
+
"LICENSE",
|
|
43
|
+
"Dockerfile.pi"
|
|
44
|
+
],
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=20"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^25.2.0",
|
|
51
|
+
"as-soon": "^0.1.5",
|
|
52
|
+
"tsx": "^4.21.0",
|
|
53
|
+
"typescript": "^5.3.3",
|
|
54
|
+
"vitest": "^4.0.18"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsc",
|
|
58
|
+
"dev": "as-soon -w src pnpm build",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"test:watch": "vitest"
|
|
61
|
+
}
|
|
62
|
+
}
|