anon-pi 0.2.0 → 0.3.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 +29 -19
- package/README.md +33 -14
- package/dist/anon-pi.d.ts +93 -24
- package/dist/anon-pi.d.ts.map +1 -1
- package/dist/anon-pi.js +193 -64
- package/dist/anon-pi.js.map +1 -1
- package/dist/cli.js +19 -6
- package/dist/cli.js.map +1 -1
- package/examples/Dockerfile.pi-webveil +15 -9
- package/package.json +1 -1
- package/src/anon-pi.ts +227 -71
- package/src/cli.ts +23 -6
package/src/anon-pi.ts
CHANGED
|
@@ -27,34 +27,69 @@ import {fileURLToPath} from 'node:url';
|
|
|
27
27
|
export const CONTAINER_WORKDIR = '/work';
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* extensions.
|
|
30
|
+
* The container path pi uses as its config+state home. anon-pi mounts a
|
|
31
|
+
* PERSISTENT host dir here (Model B), so everything pi writes, sessions,
|
|
32
|
+
* history, settings (your model choice), `pi install`ed extensions, downloaded
|
|
33
|
+
* bin/fd, survives across launches. Statefulness is the default; --ephemeral
|
|
34
|
+
* mounts a throwaway dir here instead.
|
|
36
35
|
*/
|
|
37
|
-
export const
|
|
36
|
+
export const CONTAINER_AGENT_DIR = '/root/.pi/agent';
|
|
38
37
|
|
|
39
38
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
39
|
+
* Where the image STAGES its first-launch defaults (extensions + trust.json).
|
|
40
|
+
* NOT the agent dir, so it never conflicts with the persistent mount. The
|
|
41
|
+
* entrypoint promotes these into the mounted agent dir only when the home is
|
|
42
|
+
* FRESH (Model C seed-if-fresh).
|
|
44
43
|
*/
|
|
45
|
-
export const
|
|
46
|
-
`mkdir -p "$HOME/.pi/agent" && ` +
|
|
47
|
-
`cp ${CONTAINER_SEED_DIR}/models.json "$HOME/.pi/agent/models.json" && ` +
|
|
48
|
-
`exec pi`;
|
|
44
|
+
export const CONTAINER_STAGE_DIR = '/opt/anon-pi-seed/agent';
|
|
49
45
|
|
|
50
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Where anon-pi mounts the canonical models.json (from `import`) read-only, so
|
|
48
|
+
* the first-launch seed can copy it into the fresh home alongside the image's
|
|
49
|
+
* staged defaults. Read-only: the container never writes back to the host seed.
|
|
50
|
+
*/
|
|
51
|
+
export const CONTAINER_MODELS_SEED = '/anon-pi-seed/models.json';
|
|
52
|
+
|
|
53
|
+
/** Marker file written into the agent dir after seeding; holds the seed version. */
|
|
54
|
+
export const SEED_MARKER = '.anon-pi-seed';
|
|
55
|
+
|
|
56
|
+
/** The single file the host-side seed carries: pi's model/provider registry. */
|
|
51
57
|
export const MODELS_FILE = 'models.json';
|
|
52
58
|
|
|
59
|
+
/**
|
|
60
|
+
* containerRunCmd builds the container command: on a FRESH home (no seed
|
|
61
|
+
* marker), promote the image's staged defaults + the mounted models.json into
|
|
62
|
+
* the persistent agent dir and stamp the marker; then exec pi. On a seeded home
|
|
63
|
+
* it does nothing but exec pi, so pi's persisted state (incl. anything you
|
|
64
|
+
* `pi install`ed or models pi added) is used as-is and NEVER clobbered.
|
|
65
|
+
*
|
|
66
|
+
* seedVersion is written into the marker so a future image can re-seed changed
|
|
67
|
+
* defaults on a version bump; v1 only seeds when the marker is absent.
|
|
68
|
+
*/
|
|
69
|
+
export function containerRunCmd(seedVersion: string): string {
|
|
70
|
+
const agent = CONTAINER_AGENT_DIR;
|
|
71
|
+
const marker = `${agent}/${SEED_MARKER}`;
|
|
72
|
+
return (
|
|
73
|
+
`mkdir -p "${agent}" && ` +
|
|
74
|
+
`if [ ! -f "${marker}" ]; then ` +
|
|
75
|
+
// image-staged defaults (extensions, trust.json), if the image provides them
|
|
76
|
+
`{ [ -d "${CONTAINER_STAGE_DIR}" ] && cp -a "${CONTAINER_STAGE_DIR}/." "${agent}/" || true; } && ` +
|
|
77
|
+
// the host-imported models.json, if mounted
|
|
78
|
+
`{ [ -f "${CONTAINER_MODELS_SEED}" ] && cp "${CONTAINER_MODELS_SEED}" "${agent}/${MODELS_FILE}" || true; } && ` +
|
|
79
|
+
`printf '%s\\n' "${seedVersion}" > "${marker}"; ` +
|
|
80
|
+
`fi && ` +
|
|
81
|
+
`exec pi`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The seed version anon-pi stamps when it seeds a fresh home (bump to re-seed). */
|
|
86
|
+
export const SEED_VERSION = '1';
|
|
87
|
+
|
|
53
88
|
/** Inputs resolved from the environment + argv, injected so this stays pure. */
|
|
54
89
|
export interface AnonPiEnv {
|
|
55
90
|
/** $HOME (or an override) used to derive default paths. */
|
|
56
91
|
home: string;
|
|
57
|
-
/** socks5h proxy URL.
|
|
92
|
+
/** socks5h proxy URL. REQUIRED (no default: the proxy is what anonymizes). */
|
|
58
93
|
proxy?: string;
|
|
59
94
|
/** The anon-pi home dir. Default $XDG_CONFIG_HOME/anon-pi or ~/.config/anon-pi. */
|
|
60
95
|
anonPiHome?: string;
|
|
@@ -73,24 +108,39 @@ export interface AnonPiEnv {
|
|
|
73
108
|
* `Dockerfile.pi`.
|
|
74
109
|
*/
|
|
75
110
|
dockerfilePath?: string;
|
|
111
|
+
/**
|
|
112
|
+
* Absolute path to the shipped examples/Dockerfile.pi-webveil (pi + pi-webveil
|
|
113
|
+
* + SearXNG), used to make the missing-image error mention the fuller build.
|
|
114
|
+
*/
|
|
115
|
+
webveilDockerfilePath?: string;
|
|
76
116
|
/** `import` source models.json override (ANON_PI_SOURCE_MODELS). */
|
|
77
117
|
sourceModels?: string;
|
|
78
118
|
/** The host pi agent dir override (PI_CODING_AGENT_DIR), used to find models.json. */
|
|
79
119
|
piAgentDir?: string;
|
|
120
|
+
/** When true, use a throwaway state home (no persistence). Default false. */
|
|
121
|
+
ephemeral?: boolean;
|
|
122
|
+
/** The seed version anon-pi stamps into a fresh home. Default SEED_VERSION. */
|
|
123
|
+
seedVersion?: string;
|
|
80
124
|
}
|
|
81
125
|
|
|
82
126
|
/** The fully-resolved run plan cli.ts executes. */
|
|
83
127
|
export interface RunPlan {
|
|
84
128
|
/** Absolute workdir on the host (mounted at /work). */
|
|
85
129
|
workdir: string;
|
|
86
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* The PERSISTENT per-workdir state dir on the host, mounted at the container's
|
|
132
|
+
* ~/.pi/agent. Everything pi writes here survives. For --ephemeral this is a
|
|
133
|
+
* throwaway path cli.ts creates + discards.
|
|
134
|
+
*/
|
|
135
|
+
stateDir: string;
|
|
136
|
+
/** The canonical host models.json (from `import`) mounted read-only for the seed, or '' if absent. */
|
|
87
137
|
configSeed: string;
|
|
138
|
+
/** True when this workdir has no state yet (fresh home; the seed will run). */
|
|
139
|
+
fresh: boolean;
|
|
88
140
|
/** The argv passed to `netcage` (after the `netcage` program name). */
|
|
89
141
|
netcageArgs: string[];
|
|
90
142
|
}
|
|
91
143
|
|
|
92
|
-
const DEFAULT_PROXY = 'socks5h://127.0.0.1:9050';
|
|
93
|
-
|
|
94
144
|
/** A user-facing error whose message is meant to be printed verbatim (no stack). */
|
|
95
145
|
export class AnonPiError extends Error {}
|
|
96
146
|
|
|
@@ -104,12 +154,35 @@ export function resolveAnonPiHome(env: AnonPiEnv): string {
|
|
|
104
154
|
return join(base, 'anon-pi');
|
|
105
155
|
}
|
|
106
156
|
|
|
107
|
-
/**
|
|
157
|
+
/**
|
|
158
|
+
* The CANONICAL host seed dir holding models.json (written by `anon-pi import`).
|
|
159
|
+
* Mounted read-only so the first-launch seed can copy models.json into a fresh
|
|
160
|
+
* persistent home. Workdir-independent (import does not need a workdir).
|
|
161
|
+
*/
|
|
108
162
|
export function resolveConfigSeed(env: AnonPiEnv): string {
|
|
109
163
|
if (env.configSeed) return resolve(env.configSeed);
|
|
110
164
|
return join(resolveAnonPiHome(env), 'agent');
|
|
111
165
|
}
|
|
112
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Encode an absolute path into a directory name using pi's OWN convention (see
|
|
169
|
+
* pi coding-agent session-manager: `--${cwd without leading slash, / \ : -> -}--`),
|
|
170
|
+
* so an anon-pi state dir is readable and matches pi's mental model (no opaque
|
|
171
|
+
* hash). e.g. /home/u/dev/x -> --home-u-dev-x--
|
|
172
|
+
*/
|
|
173
|
+
export function pathSlug(absPath: string): string {
|
|
174
|
+
return `--${absPath.replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The persistent per-workdir state dir on the host (mounted at the container's
|
|
179
|
+
* ~/.pi/agent). Keyed by the workdir via pi's path-slug convention:
|
|
180
|
+
* <anonPiHome>/state/<slug>/agent
|
|
181
|
+
*/
|
|
182
|
+
export function stateAgentDir(env: AnonPiEnv, absWorkdir: string): string {
|
|
183
|
+
return join(resolveAnonPiHome(env), 'state', pathSlug(absWorkdir), 'agent');
|
|
184
|
+
}
|
|
185
|
+
|
|
113
186
|
/**
|
|
114
187
|
* Normalise a proxy-less host:port key from an ANON_PI_LLM value or a provider
|
|
115
188
|
* baseUrl, so `192.168.1.150:8080` matches `http://192.168.1.150:8080/v1`.
|
|
@@ -220,18 +293,29 @@ export function resolveSourceModelsPath(env: AnonPiEnv): string {
|
|
|
220
293
|
/**
|
|
221
294
|
* Build the run plan from the environment + the (optional) workdir arg. PURE: it
|
|
222
295
|
* resolves paths and composes the netcage argv, performing NO filesystem writes
|
|
223
|
-
* or spawns. It THROWS AnonPiError for the
|
|
224
|
-
*
|
|
296
|
+
* or spawns. It THROWS AnonPiError for the required inputs (image, llm, proxy).
|
|
297
|
+
*
|
|
298
|
+
* Statefulness (Model B): a persistent per-workdir host dir is mounted at the
|
|
299
|
+
* container's ~/.pi/agent, so pi's sessions/history/settings/extensions persist.
|
|
300
|
+
* First-launch seed (Model C): when that home is FRESH, the container run
|
|
301
|
+
* command promotes the image's staged defaults + the imported models.json into
|
|
302
|
+
* it and stamps a marker; thereafter pi OWNS the home and nothing is clobbered.
|
|
303
|
+
*
|
|
304
|
+
* `modelsSeedExists` reports whether the canonical import models.json exists (so
|
|
305
|
+
* it is mounted for the seed); `stateExists` reports whether this workdir's
|
|
306
|
+
* state home already exists (so `fresh` is known).
|
|
225
307
|
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
308
|
+
* --ephemeral mounts NO writable state: pi writes to the container's own
|
|
309
|
+
* filesystem, which netcage runs with `--rm`, so it is destroyed when the
|
|
310
|
+
* container exits. Nothing writable ever touches a host path; there is no
|
|
311
|
+
* cleanup and no leftover-on-crash. (The read-only models.json seed is still
|
|
312
|
+
* mounted; it is a single file anon-pi never writes to.)
|
|
230
313
|
*/
|
|
231
314
|
export function buildRunPlan(
|
|
232
315
|
env: AnonPiEnv,
|
|
233
316
|
workdirArg: string | undefined,
|
|
234
|
-
|
|
317
|
+
modelsSeedExists: (modelsJsonPath: string) => boolean,
|
|
318
|
+
stateExists: (stateDir: string) => boolean,
|
|
235
319
|
): RunPlan {
|
|
236
320
|
if (!env.image || env.image.trim() === '') {
|
|
237
321
|
// dockerfilePath is injected (cli.ts resolves the shipped Dockerfile.pi via
|
|
@@ -240,6 +324,7 @@ export function buildRunPlan(
|
|
|
240
324
|
// leading spaces into the Dockerfile and break the EOF terminator, so we
|
|
241
325
|
// point at the shipped file instead of printing a heredoc.
|
|
242
326
|
const df = env.dockerfilePath ?? 'Dockerfile.pi';
|
|
327
|
+
const wv = env.webveilDockerfilePath ?? 'examples/Dockerfile.pi-webveil';
|
|
243
328
|
throw new AnonPiError(
|
|
244
329
|
'anon-pi: set ANON_PI_IMAGE to a container image that has `pi` on its PATH.\n' +
|
|
245
330
|
'\n' +
|
|
@@ -249,6 +334,12 @@ export function buildRunPlan(
|
|
|
249
334
|
`podman build -t localhost/anon-pi-pi:latest -f "${df}" "$(dirname "${df}")"\n` +
|
|
250
335
|
'export ANON_PI_IMAGE=localhost/anon-pi-pi:latest\n' +
|
|
251
336
|
'\n' +
|
|
337
|
+
'Or the fuller example with the pi-webveil extension + a local SearXNG\n' +
|
|
338
|
+
'(anonymized web search):\n' +
|
|
339
|
+
'\n' +
|
|
340
|
+
`podman build -t localhost/anon-pi-webveil:latest -f "${wv}" "$(dirname "${wv}")"\n` +
|
|
341
|
+
'export ANON_PI_IMAGE=localhost/anon-pi-webveil:latest\n' +
|
|
342
|
+
'\n' +
|
|
252
343
|
'See the README (Providing a pi image) for details and a community-image note.',
|
|
253
344
|
);
|
|
254
345
|
}
|
|
@@ -257,6 +348,30 @@ export function buildRunPlan(
|
|
|
257
348
|
'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.',
|
|
258
349
|
);
|
|
259
350
|
}
|
|
351
|
+
if (!env.proxy || env.proxy.trim() === '') {
|
|
352
|
+
// No default: this is an anonymity tool, so the proxy is REQUIRED and never
|
|
353
|
+
// guessed (mirrors netcage, which fails closed without --proxy). A silent
|
|
354
|
+
// default would anonymize through the wrong endpoint, or fail deep in the
|
|
355
|
+
// jail with a confusing DNS error, if the guessed proxy is not actually up.
|
|
356
|
+
throw new AnonPiError(
|
|
357
|
+
'anon-pi: set ANON_PI_PROXY to your socks5h proxy. anon-pi has no default:\n' +
|
|
358
|
+
'the proxy is what makes the session anonymous, so it is never guessed.\n' +
|
|
359
|
+
'\n' +
|
|
360
|
+
'Pick the one you run (copy-paste), then re-run anon-pi:\n' +
|
|
361
|
+
'\n' +
|
|
362
|
+
'# Tor (system tor / Tor Browser bundle default port)\n' +
|
|
363
|
+
'export ANON_PI_PROXY=socks5h://127.0.0.1:9050\n' +
|
|
364
|
+
'\n' +
|
|
365
|
+
'# wireproxy -> a WireGuard VPN (Mullvad, Proton, ...); use YOUR configured\n' +
|
|
366
|
+
'# [Socks5] BindAddress port (1080 in wireproxy examples):\n' +
|
|
367
|
+
'export ANON_PI_PROXY=socks5h://127.0.0.1:1080\n' +
|
|
368
|
+
'\n' +
|
|
369
|
+
'# an SSH dynamic-forward (ssh -D 1080 host) or any other socks5h endpoint\n' +
|
|
370
|
+
'export ANON_PI_PROXY=socks5h://127.0.0.1:1080\n' +
|
|
371
|
+
'\n' +
|
|
372
|
+
'Only socks5h:// is accepted (plain socks5:// resolves DNS locally and leaks).',
|
|
373
|
+
);
|
|
374
|
+
}
|
|
260
375
|
|
|
261
376
|
const home = env.home;
|
|
262
377
|
if (!home || home.trim() === '') {
|
|
@@ -267,48 +382,54 @@ export function buildRunPlan(
|
|
|
267
382
|
workdirArg && workdirArg.trim() !== '' ? workdirArg : process.cwd();
|
|
268
383
|
const workdir = isAbsolute(raw) ? raw : resolve(raw);
|
|
269
384
|
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
);
|
|
284
|
-
}
|
|
385
|
+
// Persistent per-workdir state home, unless --ephemeral (no writable mount).
|
|
386
|
+
const ephemeral = env.ephemeral === true;
|
|
387
|
+
const stateDir = ephemeral ? '' : stateAgentDir(env, workdir);
|
|
388
|
+
// Ephemeral home is always fresh (the container's throwaway layer); a
|
|
389
|
+
// persistent home is fresh iff its dir is absent.
|
|
390
|
+
const fresh = ephemeral ? true : !stateExists(stateDir);
|
|
391
|
+
|
|
392
|
+
// The canonical imported models.json is mounted (read-only) for the seed only
|
|
393
|
+
// when it exists; pi can also start with no models and you add them in-session.
|
|
394
|
+
const modelsSeed = join(resolveConfigSeed(env), MODELS_FILE);
|
|
395
|
+
const haveModelsSeed = modelsSeedExists(modelsSeed);
|
|
396
|
+
|
|
397
|
+
const proxy = env.proxy.trim();
|
|
285
398
|
|
|
286
|
-
|
|
287
|
-
|
|
399
|
+
// netcage's --allow-direct wants a bare IP[:port]/CIDR (no scheme/path), but a
|
|
400
|
+
// user naturally sets ANON_PI_LLM to a URL (http://192.168.1.150:8080). Strip
|
|
401
|
+
// it to host:port with the same helper `import` uses to match providers, so a
|
|
402
|
+
// URL, an ip:port, or a bare ip all work.
|
|
403
|
+
const directTarget = hostPortKey(env.llmDirect);
|
|
404
|
+
const seedVersion = env.seedVersion ?? SEED_VERSION;
|
|
288
405
|
|
|
289
406
|
const netcageArgs = [
|
|
290
407
|
'run',
|
|
291
408
|
'--proxy',
|
|
292
409
|
proxy,
|
|
293
410
|
'--allow-direct',
|
|
294
|
-
|
|
411
|
+
directTarget,
|
|
295
412
|
'-it',
|
|
296
413
|
'-v',
|
|
297
414
|
workdir, // netcage defaults a target-less -v to /work and cwd to /work
|
|
298
|
-
'-v',
|
|
299
|
-
// Mount the seed READ-ONLY at a neutral path; the run command copies
|
|
300
|
-
// models.json into the container's own ~/.pi/agent so image extensions
|
|
301
|
-
// survive (see CONTAINER_RUN_CMD).
|
|
302
|
-
`${configSeed}:${CONTAINER_SEED_DIR}:ro`,
|
|
303
|
-
env.image,
|
|
304
|
-
'sh',
|
|
305
|
-
'-c',
|
|
306
|
-
CONTAINER_RUN_CMD,
|
|
307
415
|
];
|
|
416
|
+
// Persistent mode ONLY: mount the per-workdir state home at ~/.pi/agent
|
|
417
|
+
// (Model B). --ephemeral mounts nothing writable: pi writes to the container's
|
|
418
|
+
// own --rm layer, gone on exit, no host state.
|
|
419
|
+
if (!ephemeral) {
|
|
420
|
+
netcageArgs.push('-v', `${stateDir}:${CONTAINER_AGENT_DIR}`);
|
|
421
|
+
}
|
|
422
|
+
// Mount the imported models.json read-only for the first-launch seed, if any.
|
|
423
|
+
if (haveModelsSeed) {
|
|
424
|
+
netcageArgs.push('-v', `${modelsSeed}:${CONTAINER_MODELS_SEED}:ro`);
|
|
425
|
+
}
|
|
426
|
+
netcageArgs.push(env.image, 'sh', '-c', containerRunCmd(seedVersion));
|
|
308
427
|
|
|
309
428
|
return {
|
|
310
429
|
workdir,
|
|
311
|
-
|
|
430
|
+
stateDir,
|
|
431
|
+
configSeed: haveModelsSeed ? modelsSeed : '',
|
|
432
|
+
fresh,
|
|
312
433
|
netcageArgs,
|
|
313
434
|
};
|
|
314
435
|
}
|
|
@@ -320,13 +441,26 @@ export function buildRunPlan(
|
|
|
320
441
|
* build command concrete.
|
|
321
442
|
*/
|
|
322
443
|
export function shippedDockerfilePath(): string | undefined {
|
|
444
|
+
return shippedFile('Dockerfile.pi');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Absolute path to the fuller pi-webveil + SearXNG example that ships with
|
|
449
|
+
* anon-pi (examples/Dockerfile.pi-webveil), or undefined if not found.
|
|
450
|
+
*/
|
|
451
|
+
export function shippedWebveilDockerfilePath(): string | undefined {
|
|
452
|
+
return shippedFile(join('examples', 'Dockerfile.pi-webveil'));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Resolve a file shipped in the package root, from this module's location
|
|
457
|
+
* (package root is one level up from dist/anon-pi.js). Returns undefined if it
|
|
458
|
+
* cannot be found or import.meta.url is unavailable.
|
|
459
|
+
*/
|
|
460
|
+
function shippedFile(rel: string): string | undefined {
|
|
323
461
|
try {
|
|
324
462
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
325
|
-
|
|
326
|
-
for (const p of [
|
|
327
|
-
join(here, '..', 'Dockerfile.pi'),
|
|
328
|
-
join(here, 'Dockerfile.pi'),
|
|
329
|
-
]) {
|
|
463
|
+
for (const p of [join(here, '..', rel), join(here, rel)]) {
|
|
330
464
|
if (existsSync(p)) return p;
|
|
331
465
|
}
|
|
332
466
|
} catch {
|
|
@@ -348,17 +482,26 @@ export function envFromProcess(
|
|
|
348
482
|
llmDirect: penv.ANON_PI_LLM,
|
|
349
483
|
xdgConfigHome: penv.XDG_CONFIG_HOME,
|
|
350
484
|
dockerfilePath: shippedDockerfilePath(),
|
|
485
|
+
webveilDockerfilePath: shippedWebveilDockerfilePath(),
|
|
351
486
|
sourceModels: penv.ANON_PI_SOURCE_MODELS,
|
|
352
487
|
piAgentDir: penv.PI_CODING_AGENT_DIR,
|
|
488
|
+
ephemeral: isTruthy(penv.ANON_PI_EPHEMERAL),
|
|
353
489
|
};
|
|
354
490
|
}
|
|
355
491
|
|
|
492
|
+
/** Whether an env-var string is set to a truthy value (1/true/yes, any case). */
|
|
493
|
+
function isTruthy(v: string | undefined): boolean {
|
|
494
|
+
if (!v) return false;
|
|
495
|
+
const s = v.trim().toLowerCase();
|
|
496
|
+
return s === '1' || s === 'true' || s === 'yes' || s === 'on';
|
|
497
|
+
}
|
|
498
|
+
|
|
356
499
|
/** The --help text (kept here so it is covered by the same module). */
|
|
357
500
|
export const HELP = `anon-pi - launch pi inside a netcage (anonymized egress + one direct local model)
|
|
358
501
|
|
|
359
502
|
USAGE
|
|
360
503
|
anon-pi [WORKDIR] launch pi jailed, working in WORKDIR (default: cwd)
|
|
361
|
-
anon-pi import
|
|
504
|
+
anon-pi import seed models.json from your local model
|
|
362
505
|
|
|
363
506
|
WORKDIR the host folder pi works in (mounted at ${CONTAINER_WORKDIR}; pi's cwd). Files pi
|
|
364
507
|
writes there land on the host.
|
|
@@ -366,28 +509,41 @@ USAGE
|
|
|
366
509
|
WHAT IT DOES
|
|
367
510
|
Runs pi inside netcage with all web/DNS egress forced through the socks5h
|
|
368
511
|
proxy (fail-closed) and ONE direct hole to your local model (ANON_PI_LLM).
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
512
|
+
|
|
513
|
+
STATEFUL by default: a persistent per-workdir home
|
|
514
|
+
(<ANON_PI_HOME>/state/<workdir>/agent) is mounted at the container's
|
|
515
|
+
~/.pi/agent, so your conversations, history, settings (model choice), and any
|
|
516
|
+
extensions you \`pi install\` persist across launches. Re-running in the same
|
|
517
|
+
folder resumes it. On a FRESH home, the image's staged defaults (extensions,
|
|
518
|
+
trust) and your imported models.json are seeded in once; after that pi owns the
|
|
519
|
+
home and nothing is overwritten. Requires \`netcage\`.
|
|
520
|
+
|
|
521
|
+
--ephemeral (or ANON_PI_EPHEMERAL=1): mount NO writable state; pi writes to the
|
|
522
|
+
container's own --rm layer, gone on exit. Nothing writable touches the host,
|
|
523
|
+
no cleanup, no leftover-on-crash.
|
|
372
524
|
|
|
373
525
|
import
|
|
374
526
|
Reads your host ~/.pi/agent/models.json, picks the provider whose baseUrl
|
|
375
|
-
serves ANON_PI_LLM, and writes JUST that provider to the seed
|
|
527
|
+
serves ANON_PI_LLM, and writes JUST that provider to the canonical seed
|
|
376
528
|
(<ANON_PI_CONFIG>/models.json). No other provider's API keys, no sessions, no
|
|
377
|
-
identity.
|
|
529
|
+
identity. It SEEDS a fresh home; models you later add inside pi persist and are
|
|
530
|
+
never clobbered. Re-run with --force to overwrite the canonical seed.
|
|
378
531
|
|
|
379
532
|
ENVIRONMENT
|
|
380
533
|
ANON_PI_IMAGE (required for run) image with \`pi\` on PATH. No image yet?
|
|
381
534
|
Running anon-pi without it prints a ready-to-build
|
|
382
535
|
Dockerfile.pi recipe; see the README (Providing a pi image).
|
|
383
536
|
ANON_PI_LLM (required) RFC1918/link-local IP[:port] of the local model
|
|
384
|
-
ANON_PI_PROXY socks5h URL (
|
|
537
|
+
ANON_PI_PROXY (required) socks5h URL of your proxy (Tor/wireproxy/ssh -D).
|
|
538
|
+
No default: the proxy is what anonymizes, so it is never guessed.
|
|
539
|
+
ANON_PI_EPHEMERAL set to 1 for a throwaway (non-persistent) session
|
|
385
540
|
ANON_PI_HOME anon-pi home (default $XDG_CONFIG_HOME/anon-pi or ~/.config/anon-pi)
|
|
386
|
-
ANON_PI_CONFIG seed dir holding models.json (default <ANON_PI_HOME>/agent)
|
|
541
|
+
ANON_PI_CONFIG canonical seed dir holding models.json (default <ANON_PI_HOME>/agent)
|
|
387
542
|
ANON_PI_SOURCE_MODELS (import) host models.json to read (default ~/.pi/agent/models.json)
|
|
388
543
|
|
|
389
|
-
|
|
390
|
-
|
|
544
|
+
RESET A SESSION
|
|
545
|
+
Delete its state home to start fresh (re-seeds next launch):
|
|
546
|
+
rm -rf <ANON_PI_HOME>/state/<workdir-slug>/agent
|
|
391
547
|
|
|
392
548
|
PLATFORM
|
|
393
549
|
Linux only (via netcage's netns/nft jail). On macOS/Windows it works only
|
package/src/cli.ts
CHANGED
|
@@ -41,11 +41,13 @@ function main(argv: string[]): number {
|
|
|
41
41
|
|
|
42
42
|
// --- anon-pi [WORKDIR] : launch pi jailed -----------------------------------
|
|
43
43
|
function runLaunch(args: string[]): number {
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
44
|
+
// One optional positional (the workdir) + the --ephemeral flag. Reject other
|
|
45
|
+
// flags so a typo is not silently swallowed: anon-pi owns the netcage argv.
|
|
46
|
+
const ephemeralFlag = args.includes('--ephemeral') || args.includes('--eph');
|
|
47
47
|
const positionals = args.filter((a) => !a.startsWith('-'));
|
|
48
|
-
const flags = args.filter(
|
|
48
|
+
const flags = args.filter(
|
|
49
|
+
(a) => a.startsWith('-') && a !== '--ephemeral' && a !== '--eph',
|
|
50
|
+
);
|
|
49
51
|
if (flags.length > 0) {
|
|
50
52
|
process.stderr.write(
|
|
51
53
|
`anon-pi: unknown option(s): ${flags.join(' ')}\nRun \`anon-pi --help\`.\n`,
|
|
@@ -60,10 +62,11 @@ function runLaunch(args: string[]): number {
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
const env = envFromProcess(process.env);
|
|
65
|
+
if (ephemeralFlag) env.ephemeral = true;
|
|
63
66
|
|
|
64
67
|
let plan;
|
|
65
68
|
try {
|
|
66
|
-
plan = buildRunPlan(env, positionals[0], existsSync);
|
|
69
|
+
plan = buildRunPlan(env, positionals[0], existsSync, existsSync);
|
|
67
70
|
} catch (e) {
|
|
68
71
|
if (e instanceof AnonPiError) {
|
|
69
72
|
process.stderr.write(e.message + '\n');
|
|
@@ -81,8 +84,22 @@ function runLaunch(args: string[]): number {
|
|
|
81
84
|
return 1;
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
// Ensure the workdir exists (a fresh named folder is fine).
|
|
85
87
|
mkdirSync(plan.workdir, {recursive: true});
|
|
88
|
+
if (env.ephemeral) {
|
|
89
|
+
// No host state dir: pi writes to the container's own --rm layer, so the
|
|
90
|
+
// session leaves NO trace on the host and there is nothing to clean up.
|
|
91
|
+
process.stderr.write(
|
|
92
|
+
'anon-pi: ephemeral session (nothing persisted; no host state)\n',
|
|
93
|
+
);
|
|
94
|
+
} else {
|
|
95
|
+
// Persistent mode: create the per-workdir state home to mount.
|
|
96
|
+
mkdirSync(plan.stateDir, {recursive: true});
|
|
97
|
+
if (plan.fresh) {
|
|
98
|
+
process.stderr.write(
|
|
99
|
+
`anon-pi: new session home ${plan.stateDir} (seeding on first launch)\n`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
86
103
|
|
|
87
104
|
// Hand off to netcage with inherited stdio so -it is a real interactive TTY.
|
|
88
105
|
const res = spawnSync('netcage', plan.netcageArgs, {stdio: 'inherit'});
|