anon-pi 0.1.1 → 0.2.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 +20 -2
- package/README.md +47 -24
- package/dist/anon-pi.d.ts +88 -44
- package/dist/anon-pi.d.ts.map +1 -1
- package/dist/anon-pi.js +172 -94
- package/dist/anon-pi.js.map +1 -1
- package/dist/cli.js +75 -14
- package/dist/cli.js.map +1 -1
- package/examples/Dockerfile.pi-webveil +94 -0
- package/package.json +3 -2
- package/src/anon-pi.ts +220 -120
- package/src/cli.ts +110 -17
package/dist/anon-pi.js
CHANGED
|
@@ -17,46 +17,37 @@
|
|
|
17
17
|
// trust.json that trusts /work). anon-pi does not synthesize pi's trust.json.
|
|
18
18
|
// - Session identity = the ABSOLUTE workdir path (hashed). Same folder resumes
|
|
19
19
|
// the same session config+state; reseed is manual (delete the session dir).
|
|
20
|
-
import {
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
21
21
|
import { homedir } from 'node:os';
|
|
22
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
22
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
23
24
|
/** The container path the workdir is mounted at (pi's cwd). */
|
|
24
25
|
export const CONTAINER_WORKDIR = '/work';
|
|
25
26
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* Where the seed (just models.json) is mounted read-only in the container. The
|
|
28
|
+
* run command copies models.json FROM here INTO the container's own
|
|
29
|
+
* ~/.pi/agent, so it LAYERS onto the image's config (extensions/skills the image
|
|
30
|
+
* installed survive) instead of replacing it. This is why we mount+copy rather
|
|
31
|
+
* than mount-as-agent-dir: mounting over ~/.pi/agent would shadow the image's
|
|
32
|
+
* extensions.
|
|
31
33
|
*/
|
|
32
|
-
export const
|
|
33
|
-
/**
|
|
34
|
-
|
|
34
|
+
export const CONTAINER_SEED_DIR = '/anon-pi-seed';
|
|
35
|
+
/**
|
|
36
|
+
* The container command: copy the seeded models.json into pi's own config dir
|
|
37
|
+
* (creating it if absent), then exec pi. `$HOME/.pi/agent` is pi's default
|
|
38
|
+
* config dir when PI_CODING_AGENT_DIR is unset, i.e. exactly where the image
|
|
39
|
+
* installed pi + any extensions, so the copy augments rather than shadows.
|
|
40
|
+
*/
|
|
41
|
+
export const CONTAINER_RUN_CMD = `mkdir -p "$HOME/.pi/agent" && ` +
|
|
42
|
+
`cp ${CONTAINER_SEED_DIR}/models.json "$HOME/.pi/agent/models.json" && ` +
|
|
43
|
+
`exec pi`;
|
|
44
|
+
/** The single file the seed carries: pi's model/provider registry. */
|
|
45
|
+
export const MODELS_FILE = 'models.json';
|
|
35
46
|
const DEFAULT_PROXY = 'socks5h://127.0.0.1:9050';
|
|
36
47
|
/** A user-facing error whose message is meant to be printed verbatim (no stack). */
|
|
37
48
|
export class AnonPiError extends Error {
|
|
38
49
|
}
|
|
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). */
|
|
50
|
+
/** Resolve the anon-pi home dir (holds the seed). */
|
|
60
51
|
export function resolveAnonPiHome(env) {
|
|
61
52
|
if (env.anonPiHome)
|
|
62
53
|
return resolve(env.anonPiHome);
|
|
@@ -65,51 +56,108 @@ export function resolveAnonPiHome(env) {
|
|
|
65
56
|
: join(env.home, '.config');
|
|
66
57
|
return join(base, 'anon-pi');
|
|
67
58
|
}
|
|
68
|
-
/** The
|
|
59
|
+
/** The seed dir (holds models.json), mounted read-only into the container. */
|
|
69
60
|
export function resolveConfigSeed(env) {
|
|
70
61
|
if (env.configSeed)
|
|
71
62
|
return resolve(env.configSeed);
|
|
72
63
|
return join(resolveAnonPiHome(env), 'agent');
|
|
73
64
|
}
|
|
74
65
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
66
|
+
* Normalise a proxy-less host:port key from an ANON_PI_LLM value or a provider
|
|
67
|
+
* baseUrl, so `192.168.1.150:8080` matches `http://192.168.1.150:8080/v1`.
|
|
68
|
+
* Returns `host` (no port) or `host:port`, lowercased, scheme/path stripped.
|
|
78
69
|
*/
|
|
79
|
-
export function
|
|
80
|
-
|
|
70
|
+
export function hostPortKey(value) {
|
|
71
|
+
let v = value.trim();
|
|
72
|
+
const scheme = v.indexOf('://');
|
|
73
|
+
if (scheme >= 0)
|
|
74
|
+
v = v.slice(scheme + 3);
|
|
75
|
+
v = v.split('/')[0]; // drop path (/v1, ...)
|
|
76
|
+
v = v.replace(/^[^@]*@/, ''); // drop any user:pass@
|
|
77
|
+
return v.toLowerCase();
|
|
78
|
+
}
|
|
79
|
+
/** apiKey values that are NOT real secrets (safe to carry into the seed). */
|
|
80
|
+
const BENIGN_API_KEYS = new Set(['', 'none', 'ollama', 'no-key', 'local']);
|
|
81
|
+
/**
|
|
82
|
+
* PURE: given a parsed host models.json and the ANON_PI_LLM value, select the
|
|
83
|
+
* provider whose baseUrl points at that host:port and return a barebones
|
|
84
|
+
* models.json carrying ONLY that provider (verbatim, with its models). Throws
|
|
85
|
+
* AnonPiError if nothing matches. Carries no other provider (so etherplay /
|
|
86
|
+
* google / paid API keys never enter the seed).
|
|
87
|
+
*/
|
|
88
|
+
export function pickProviderForLlm(hostModels, llmDirect) {
|
|
89
|
+
const providers = hostModels.providers ?? {};
|
|
90
|
+
const want = hostPortKey(llmDirect);
|
|
91
|
+
const matches = [];
|
|
92
|
+
for (const [name, p] of Object.entries(providers)) {
|
|
93
|
+
if (!p || typeof p !== 'object' || !p.baseUrl)
|
|
94
|
+
continue;
|
|
95
|
+
if (hostPortKey(p.baseUrl) === want)
|
|
96
|
+
matches.push(name);
|
|
97
|
+
}
|
|
98
|
+
if (matches.length === 0) {
|
|
99
|
+
const known = Object.entries(providers)
|
|
100
|
+
.filter(([, p]) => p && p.baseUrl)
|
|
101
|
+
.map(([n, p]) => ` ${n}: ${p.baseUrl}`)
|
|
102
|
+
.join('\n');
|
|
103
|
+
throw new AnonPiError(`anon-pi import: no provider in your host models.json points at ANON_PI_LLM (${want}).\n` +
|
|
104
|
+
(known
|
|
105
|
+
? `Providers found:\n${known}\n`
|
|
106
|
+
: 'No providers with a baseUrl were found.\n') +
|
|
107
|
+
'Set ANON_PI_LLM to the host:port of a provider above, or add that provider to pi first.');
|
|
108
|
+
}
|
|
109
|
+
const name = matches[0];
|
|
110
|
+
const provider = providers[name];
|
|
111
|
+
const key = (provider.apiKey ?? '').trim().toLowerCase();
|
|
112
|
+
const apiKeyLooksReal = !BENIGN_API_KEYS.has(key);
|
|
113
|
+
return {
|
|
114
|
+
name,
|
|
115
|
+
models: { providers: { [name]: provider } },
|
|
116
|
+
apiKeyLooksReal,
|
|
117
|
+
};
|
|
81
118
|
}
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
119
|
+
/**
|
|
120
|
+
* The default host models.json path `import` reads FROM. Overridable via
|
|
121
|
+
* ANON_PI_SOURCE_MODELS; defaults to the real pi config (~/.pi/agent/models.json
|
|
122
|
+
* under the container-less host HOME, or PI_CODING_AGENT_DIR if the user set it).
|
|
123
|
+
*/
|
|
124
|
+
export function resolveSourceModelsPath(env) {
|
|
125
|
+
if (env.sourceModels && env.sourceModels.trim() !== '') {
|
|
126
|
+
return resolve(env.sourceModels);
|
|
127
|
+
}
|
|
128
|
+
const agentDir = env.piAgentDir && env.piAgentDir.trim() !== ''
|
|
129
|
+
? env.piAgentDir
|
|
130
|
+
: join(env.home, '.pi', 'agent');
|
|
131
|
+
return join(agentDir, MODELS_FILE);
|
|
85
132
|
}
|
|
86
133
|
/**
|
|
87
134
|
* Build the run plan from the environment + the (optional) workdir arg. PURE: it
|
|
88
|
-
* resolves paths and composes the netcage argv,
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
135
|
+
* resolves paths and composes the netcage argv, performing NO filesystem writes
|
|
136
|
+
* or spawns. It THROWS AnonPiError for the hard preconditions (missing image,
|
|
137
|
+
* missing llm, missing seed models.json) so the required inputs fail loud.
|
|
138
|
+
*
|
|
139
|
+
* The seed (models.json) is mounted READ-ONLY at /anon-pi-seed and copied into
|
|
140
|
+
* the container's own ~/.pi/agent by the run command, so it LAYERS onto the
|
|
141
|
+
* image's config (image-installed extensions/skills survive) rather than
|
|
142
|
+
* shadowing it.
|
|
93
143
|
*/
|
|
94
|
-
export function buildRunPlan(env, workdirArg,
|
|
144
|
+
export function buildRunPlan(env, workdirArg, seedModelsExists) {
|
|
95
145
|
if (!env.image || env.image.trim() === '') {
|
|
146
|
+
// dockerfilePath is injected (cli.ts resolves the shipped Dockerfile.pi via
|
|
147
|
+
// import.meta.url; tests pass a fixed path). Every command is emitted
|
|
148
|
+
// flush-left so it copy-pastes cleanly: an indented heredoc would bake
|
|
149
|
+
// leading spaces into the Dockerfile and break the EOF terminator, so we
|
|
150
|
+
// point at the shipped file instead of printing a heredoc.
|
|
151
|
+
const df = env.dockerfilePath ?? 'Dockerfile.pi';
|
|
96
152
|
throw new AnonPiError('anon-pi: set ANON_PI_IMAGE to a container image that has `pi` on its PATH.\n' +
|
|
97
153
|
'\n' +
|
|
98
|
-
'No
|
|
99
|
-
'
|
|
154
|
+
'No image yet? A ready Dockerfile.pi ships with anon-pi (it installs the\n' +
|
|
155
|
+
'official @earendil-works/pi-coding-agent). Build it and point at it:\n' +
|
|
100
156
|
'\n' +
|
|
101
|
-
|
|
102
|
-
'
|
|
103
|
-
' RUN apt-get update && apt-get install -y --no-install-recommends \\\n' +
|
|
104
|
-
' bash ca-certificates git ripgrep && rm -rf /var/lib/apt/lists/*\n' +
|
|
105
|
-
' RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent\n' +
|
|
106
|
-
' WORKDIR /work\n' +
|
|
107
|
-
' EOF\n' +
|
|
108
|
-
' podman build -t localhost/anon-pi-pi:latest -f Dockerfile.pi .\n' +
|
|
109
|
-
' export ANON_PI_IMAGE=localhost/anon-pi-pi:latest\n' +
|
|
157
|
+
`podman build -t localhost/anon-pi-pi:latest -f "${df}" "$(dirname "${df}")"\n` +
|
|
158
|
+
'export ANON_PI_IMAGE=localhost/anon-pi-pi:latest\n' +
|
|
110
159
|
'\n' +
|
|
111
|
-
'
|
|
112
|
-
'(Providing a pi image) for details and a community-image note.');
|
|
160
|
+
'See the README (Providing a pi image) for details and a community-image note.');
|
|
113
161
|
}
|
|
114
162
|
if (!env.llmDirect || env.llmDirect.trim() === '') {
|
|
115
163
|
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.');
|
|
@@ -121,17 +169,18 @@ export function buildRunPlan(env, workdirArg, seedExists, sessionExists) {
|
|
|
121
169
|
const raw = workdirArg && workdirArg.trim() !== '' ? workdirArg : process.cwd();
|
|
122
170
|
const workdir = isAbsolute(raw) ? raw : resolve(raw);
|
|
123
171
|
const configSeed = resolveConfigSeed(env);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
'
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
'
|
|
172
|
+
const modelsJson = join(configSeed, MODELS_FILE);
|
|
173
|
+
if (!seedModelsExists(modelsJson)) {
|
|
174
|
+
throw new AnonPiError(`anon-pi: no seed models.json at ${modelsJson}.\n` +
|
|
175
|
+
'\n' +
|
|
176
|
+
'anon-pi never populates it for you. Generate it from your local model:\n' +
|
|
177
|
+
'\n' +
|
|
178
|
+
'anon-pi import\n' +
|
|
179
|
+
'\n' +
|
|
180
|
+
'`import` reads your host ~/.pi/agent/models.json, picks the provider that\n' +
|
|
181
|
+
'serves ANON_PI_LLM, and writes just that provider here (no auth for other\n' +
|
|
182
|
+
'providers, no sessions, no identity). See the README (Populating the seed).');
|
|
131
183
|
}
|
|
132
|
-
const sessionDir = sessionAgentDir(env, workdir);
|
|
133
|
-
const needsSeed = !sessionExists(sessionDir);
|
|
134
|
-
const agentMount = resolveAgentMount(env);
|
|
135
184
|
const proxy = env.proxy && env.proxy.trim() !== '' ? env.proxy : DEFAULT_PROXY;
|
|
136
185
|
const netcageArgs = [
|
|
137
186
|
'run',
|
|
@@ -143,21 +192,44 @@ export function buildRunPlan(env, workdirArg, seedExists, sessionExists) {
|
|
|
143
192
|
'-v',
|
|
144
193
|
workdir, // netcage defaults a target-less -v to /work and cwd to /work
|
|
145
194
|
'-v',
|
|
146
|
-
|
|
147
|
-
'
|
|
148
|
-
|
|
195
|
+
// Mount the seed READ-ONLY at a neutral path; the run command copies
|
|
196
|
+
// models.json into the container's own ~/.pi/agent so image extensions
|
|
197
|
+
// survive (see CONTAINER_RUN_CMD).
|
|
198
|
+
`${configSeed}:${CONTAINER_SEED_DIR}:ro`,
|
|
149
199
|
env.image,
|
|
150
|
-
'
|
|
200
|
+
'sh',
|
|
201
|
+
'-c',
|
|
202
|
+
CONTAINER_RUN_CMD,
|
|
151
203
|
];
|
|
152
204
|
return {
|
|
153
205
|
workdir,
|
|
154
|
-
sessionAgentDir: sessionDir,
|
|
155
206
|
configSeed,
|
|
156
|
-
agentMount,
|
|
157
|
-
needsSeed,
|
|
158
207
|
netcageArgs,
|
|
159
208
|
};
|
|
160
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Absolute path to the Dockerfile.pi that ships with anon-pi, resolved from this
|
|
212
|
+
* module's location (package root, one level up from dist/anon-pi.js), or
|
|
213
|
+
* undefined if it cannot be found. Used only to make the missing-image error's
|
|
214
|
+
* build command concrete.
|
|
215
|
+
*/
|
|
216
|
+
export function shippedDockerfilePath() {
|
|
217
|
+
try {
|
|
218
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
219
|
+
// dist/anon-pi.js -> ../Dockerfile.pi; also try alongside for safety.
|
|
220
|
+
for (const p of [
|
|
221
|
+
join(here, '..', 'Dockerfile.pi'),
|
|
222
|
+
join(here, 'Dockerfile.pi'),
|
|
223
|
+
]) {
|
|
224
|
+
if (existsSync(p))
|
|
225
|
+
return p;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// import.meta.url unavailable (e.g. some test bundlers): fall through.
|
|
230
|
+
}
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
161
233
|
/** Read the AnonPiEnv from a process env map (kept separate so tests inject one). */
|
|
162
234
|
export function envFromProcess(penv) {
|
|
163
235
|
return {
|
|
@@ -168,40 +240,46 @@ export function envFromProcess(penv) {
|
|
|
168
240
|
image: penv.ANON_PI_IMAGE,
|
|
169
241
|
llmDirect: penv.ANON_PI_LLM,
|
|
170
242
|
xdgConfigHome: penv.XDG_CONFIG_HOME,
|
|
171
|
-
|
|
243
|
+
dockerfilePath: shippedDockerfilePath(),
|
|
244
|
+
sourceModels: penv.ANON_PI_SOURCE_MODELS,
|
|
245
|
+
piAgentDir: penv.PI_CODING_AGENT_DIR,
|
|
172
246
|
};
|
|
173
247
|
}
|
|
174
248
|
/** The --help text (kept here so it is covered by the same module). */
|
|
175
249
|
export const HELP = `anon-pi - launch pi inside a netcage (anonymized egress + one direct local model)
|
|
176
250
|
|
|
177
251
|
USAGE
|
|
178
|
-
anon-pi [WORKDIR]
|
|
252
|
+
anon-pi [WORKDIR] launch pi jailed, working in WORKDIR (default: cwd)
|
|
253
|
+
anon-pi import write the seed models.json from your local model
|
|
179
254
|
|
|
180
|
-
WORKDIR the host folder pi works in (mounted at
|
|
181
|
-
|
|
255
|
+
WORKDIR the host folder pi works in (mounted at ${CONTAINER_WORKDIR}; pi's cwd). Files pi
|
|
256
|
+
writes there land on the host.
|
|
182
257
|
|
|
183
258
|
WHAT IT DOES
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
259
|
+
Runs pi inside netcage with all web/DNS egress forced through the socks5h
|
|
260
|
+
proxy (fail-closed) and ONE direct hole to your local model (ANON_PI_LLM).
|
|
261
|
+
The seed models.json is mounted read-only and COPIED into the container's own
|
|
262
|
+
~/.pi/agent at start, so it layers onto the image's config: extensions and
|
|
263
|
+
skills you baked into the image survive. Requires \`netcage\`.
|
|
264
|
+
|
|
265
|
+
import
|
|
266
|
+
Reads your host ~/.pi/agent/models.json, picks the provider whose baseUrl
|
|
267
|
+
serves ANON_PI_LLM, and writes JUST that provider to the seed
|
|
268
|
+
(<ANON_PI_CONFIG>/models.json). No other provider's API keys, no sessions, no
|
|
269
|
+
identity. Re-run with --force to overwrite an existing seed.
|
|
189
270
|
|
|
190
271
|
ENVIRONMENT
|
|
191
|
-
ANON_PI_IMAGE (required) image with \`pi\` on PATH. No image yet?
|
|
192
|
-
anon-pi without it prints a ready-to-build
|
|
193
|
-
recipe; see the README (Providing a pi image).
|
|
272
|
+
ANON_PI_IMAGE (required for run) image with \`pi\` on PATH. No image yet?
|
|
273
|
+
Running anon-pi without it prints a ready-to-build
|
|
274
|
+
Dockerfile.pi recipe; see the README (Providing a pi image).
|
|
194
275
|
ANON_PI_LLM (required) RFC1918/link-local IP[:port] of the local model
|
|
195
276
|
ANON_PI_PROXY socks5h URL (default ${DEFAULT_PROXY})
|
|
196
277
|
ANON_PI_HOME anon-pi home (default $XDG_CONFIG_HOME/anon-pi or ~/.config/anon-pi)
|
|
197
|
-
ANON_PI_CONFIG
|
|
198
|
-
|
|
199
|
-
${DEFAULT_CONTAINER_AGENT_DIR}; set to your image's ~/.pi/agent, e.g. /root/.pi/agent)
|
|
278
|
+
ANON_PI_CONFIG seed dir holding models.json (default <ANON_PI_HOME>/agent)
|
|
279
|
+
ANON_PI_SOURCE_MODELS (import) host models.json to read (default ~/.pi/agent/models.json)
|
|
200
280
|
|
|
201
281
|
RESEED
|
|
202
|
-
|
|
203
|
-
rm -rf ~/.config/anon-pi/sessions/<hash>/agent
|
|
204
|
-
and the next run re-seeds it from the canonical config.
|
|
282
|
+
anon-pi import --force regenerates the seed models.json.
|
|
205
283
|
|
|
206
284
|
PLATFORM
|
|
207
285
|
Linux only (via netcage's netns/nft jail). On macOS/Windows it works only
|
package/dist/anon-pi.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anon-pi.js","sourceRoot":"","sources":["../src/anon-pi.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,+EAA+E;AAC/E,EAAE;AACF,sCAAsC;AACtC,8EAA8E;AAC9E,iFAAiF;AACjF,yEAAyE;AACzE,+EAA+E;AAC/E,kCAAkC;AAClC,gFAAgF;AAChF,sEAAsE;AACtE,6EAA6E;AAC7E,kFAAkF;AAClF,2EAA2E;AAC3E,kFAAkF;AAClF,yEAAyE;AACzE,kFAAkF;AAClF,iFAAiF;AACjF,gFAAgF;AAEhF,OAAO,EAAC,UAAU,EAAC,MAAM,
|
|
1
|
+
{"version":3,"file":"anon-pi.js","sourceRoot":"","sources":["../src/anon-pi.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// anon-pi CLI
|
|
3
|
-
//
|
|
4
|
-
// stdio so
|
|
5
|
-
|
|
2
|
+
// anon-pi CLI. Two commands:
|
|
3
|
+
// anon-pi [WORKDIR] resolve the run plan (pure) and exec `netcage run ...`
|
|
4
|
+
// with inherited stdio (so -it is a real interactive TTY).
|
|
5
|
+
// The seed models.json is mounted read-only and copied
|
|
6
|
+
// into the container's ~/.pi/agent by the run command, so
|
|
7
|
+
// it layers onto the image's config (extensions survive).
|
|
8
|
+
// anon-pi import generate the seed models.json from the host models.json,
|
|
9
|
+
// carrying only the provider that serves ANON_PI_LLM.
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
11
|
import { spawnSync } from 'node:child_process';
|
|
7
|
-
import {
|
|
8
|
-
import { AnonPiError, buildRunPlan, envFromProcess, HELP } from './anon-pi.js';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { AnonPiError, buildRunPlan, envFromProcess, HELP, MODELS_FILE, pickProviderForLlm, resolveConfigSeed, resolveSourceModelsPath, } from './anon-pi.js';
|
|
9
14
|
function main(argv) {
|
|
10
15
|
const args = argv.slice(2);
|
|
11
16
|
if (args.includes('--help') || args.includes('-h')) {
|
|
12
17
|
process.stdout.write(HELP);
|
|
13
18
|
return 0;
|
|
14
19
|
}
|
|
20
|
+
// Subcommand dispatch: the first bare token may be `import`.
|
|
21
|
+
if (args[0] === 'import') {
|
|
22
|
+
return runImport(args.slice(1));
|
|
23
|
+
}
|
|
24
|
+
return runLaunch(args);
|
|
25
|
+
}
|
|
26
|
+
// --- anon-pi [WORKDIR] : launch pi jailed -----------------------------------
|
|
27
|
+
function runLaunch(args) {
|
|
15
28
|
// The only positional is the optional workdir. Reject stray flags so a typo
|
|
16
29
|
// (e.g. --allow-direct) is not silently swallowed: anon-pi owns the netcage
|
|
17
30
|
// argv, extra flags are not passed through.
|
|
@@ -28,7 +41,7 @@ function main(argv) {
|
|
|
28
41
|
const env = envFromProcess(process.env);
|
|
29
42
|
let plan;
|
|
30
43
|
try {
|
|
31
|
-
plan = buildRunPlan(env, positionals[0], existsSync
|
|
44
|
+
plan = buildRunPlan(env, positionals[0], existsSync);
|
|
32
45
|
}
|
|
33
46
|
catch (e) {
|
|
34
47
|
if (e instanceof AnonPiError) {
|
|
@@ -43,13 +56,6 @@ function main(argv) {
|
|
|
43
56
|
'(https://github.com/wighawag/netcage). Linux only.\n');
|
|
44
57
|
return 1;
|
|
45
58
|
}
|
|
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
59
|
// Ensure the workdir exists (a fresh named folder is fine).
|
|
54
60
|
mkdirSync(plan.workdir, { recursive: true });
|
|
55
61
|
// Hand off to netcage with inherited stdio so -it is a real interactive TTY.
|
|
@@ -61,6 +67,61 @@ function main(argv) {
|
|
|
61
67
|
// Propagate netcage's exit code (which itself propagates the tool's).
|
|
62
68
|
return res.status ?? 1;
|
|
63
69
|
}
|
|
70
|
+
// --- anon-pi import : write the seed models.json ----------------------------
|
|
71
|
+
function runImport(args) {
|
|
72
|
+
const force = args.includes('--force') || args.includes('-f');
|
|
73
|
+
const stray = args.filter((a) => a.startsWith('-') && a !== '--force' && a !== '-f');
|
|
74
|
+
if (stray.length > 0) {
|
|
75
|
+
process.stderr.write(`anon-pi import: unknown option(s): ${stray.join(' ')}\nRun \`anon-pi --help\`.\n`);
|
|
76
|
+
return 2;
|
|
77
|
+
}
|
|
78
|
+
const env = envFromProcess(process.env);
|
|
79
|
+
if (!env.llmDirect || env.llmDirect.trim() === '') {
|
|
80
|
+
process.stderr.write('anon-pi import: set ANON_PI_LLM to the RFC1918/link-local IP[:port] of the local\n' +
|
|
81
|
+
'model whose provider should be imported (e.g. ANON_PI_LLM=192.168.1.150:8080).\n');
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
const source = resolveSourceModelsPath(env);
|
|
85
|
+
if (!existsSync(source)) {
|
|
86
|
+
process.stderr.write(`anon-pi import: host models.json not found at ${source}.\n` +
|
|
87
|
+
'Set ANON_PI_SOURCE_MODELS to your pi models.json, or run pi once to create it.\n');
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
let hostModels;
|
|
91
|
+
try {
|
|
92
|
+
hostModels = JSON.parse(readFileSync(source, 'utf8'));
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
process.stderr.write(`anon-pi import: could not parse ${source}: ${e.message}\n`);
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
let result;
|
|
99
|
+
try {
|
|
100
|
+
result = pickProviderForLlm(hostModels, env.llmDirect);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
if (e instanceof AnonPiError) {
|
|
104
|
+
process.stderr.write(e.message + '\n');
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
throw e;
|
|
108
|
+
}
|
|
109
|
+
const seedDir = resolveConfigSeed(env);
|
|
110
|
+
const dest = join(seedDir, MODELS_FILE);
|
|
111
|
+
if (existsSync(dest) && !force) {
|
|
112
|
+
process.stderr.write(`anon-pi import: ${dest} already exists. Re-run with --force to overwrite.\n`);
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
if (result.apiKeyLooksReal) {
|
|
116
|
+
process.stderr.write(`anon-pi import: WARNING: provider "${result.name}" carries a real-looking apiKey; it\n` +
|
|
117
|
+
'will be written into the seed. For a local model this is usually fine, but review\n' +
|
|
118
|
+
`${dest} if that key identifies you.\n`);
|
|
119
|
+
}
|
|
120
|
+
mkdirSync(seedDir, { recursive: true });
|
|
121
|
+
writeFileSync(dest, JSON.stringify(result.models, null, 2) + '\n');
|
|
122
|
+
process.stderr.write(`anon-pi import: wrote ${dest} (provider "${result.name}"). Run \`anon-pi\` to launch.\n`);
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
64
125
|
function hasNetcage() {
|
|
65
126
|
const which = spawnSync(process.platform === 'win32' ? 'where' : 'command', ['-v', 'netcage'], {
|
|
66
127
|
stdio: 'ignore',
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# A fuller ANON_PI_IMAGE example: pi + the pi-webveil extension + a local SearXNG
|
|
2
|
+
# (over a Unix socket) for anonymized web search, baked into ONE image (the
|
|
3
|
+
# capability layer). Modelled on a real working host setup (Unix socket,
|
|
4
|
+
# http-socket, json+limiter:false, unix: baseUrl, egress: direct).
|
|
5
|
+
#
|
|
6
|
+
# STATUS: a documented REFERENCE, not a CI-built image. The topology matches a
|
|
7
|
+
# working host, but pin versions and test the build for your platform before
|
|
8
|
+
# relying on it (SearXNG's pip install + engine set drift over time).
|
|
9
|
+
#
|
|
10
|
+
# WHY THE USUAL SearXNG ANONYMITY CAVEAT DOES NOT APPLY HERE:
|
|
11
|
+
# webveil warns that a LOCAL SearXNG crawls the web from its OWN process egress
|
|
12
|
+
# (direct), so a socks5 backend hop to 127.0.0.1 is FALSE confidence. Under
|
|
13
|
+
# anon-pi that risk is gone: netcage forces ALL TCP+DNS egress from EVERY
|
|
14
|
+
# process in the jail through the socks5h proxy, fail-closed. SearXNG's crawl is
|
|
15
|
+
# anonymized by the network layer regardless. So we use webveil's plain
|
|
16
|
+
# `egress: direct` and let netcage do the anonymizing.
|
|
17
|
+
#
|
|
18
|
+
# WHY A UNIX SOCKET (matches the host setup, avoids the loopback-TCP guard):
|
|
19
|
+
# webveil's `unix:` baseUrl speaks HTTP over a Unix socket, so uWSGI must use
|
|
20
|
+
# `http-socket = <path>` (NOT `socket = <path>`, which is the native uwsgi
|
|
21
|
+
# protocol webveil cannot speak). SearXNG also needs `json` in search.formats
|
|
22
|
+
# and `server.limiter: false` for a local instance.
|
|
23
|
+
#
|
|
24
|
+
# Build:
|
|
25
|
+
# podman build -t localhost/anon-pi-webveil:latest -f Dockerfile.pi-webveil .
|
|
26
|
+
# export ANON_PI_IMAGE=localhost/anon-pi-webveil:latest
|
|
27
|
+
# Then: `anon-pi import` (your local model), then `anon-pi <workdir>`.
|
|
28
|
+
FROM node:24-bookworm-slim
|
|
29
|
+
|
|
30
|
+
# --- system deps: pi's tools + SearXNG's Python/uwsgi runtime ----------------
|
|
31
|
+
RUN apt-get update \
|
|
32
|
+
&& apt-get install -y --no-install-recommends \
|
|
33
|
+
bash ca-certificates git ripgrep \
|
|
34
|
+
python3 python3-venv python3-pip python3-dev \
|
|
35
|
+
build-essential libxml2-dev libxslt1-dev zlib1g-dev libffi-dev libssl-dev \
|
|
36
|
+
uwsgi uwsgi-plugin-python3 \
|
|
37
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
38
|
+
|
|
39
|
+
# --- pi + the pi-webveil extension (via `pi install`, which records it) -------
|
|
40
|
+
RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent
|
|
41
|
+
RUN pi install npm:pi-webveil
|
|
42
|
+
|
|
43
|
+
# --- SearXNG in a venv -------------------------------------------------------
|
|
44
|
+
ENV SEARXNG_HOME=/opt/searxng
|
|
45
|
+
RUN python3 -m venv "$SEARXNG_HOME/venv" \
|
|
46
|
+
&& "$SEARXNG_HOME/venv/bin/pip" install --no-cache-dir -U pip setuptools wheel \
|
|
47
|
+
&& "$SEARXNG_HOME/venv/bin/pip" install --no-cache-dir searxng
|
|
48
|
+
|
|
49
|
+
# settings.yml: JSON API on + limiter off (both required for a local instance).
|
|
50
|
+
RUN mkdir -p /etc/searxng \
|
|
51
|
+
&& printf '%s\n' \
|
|
52
|
+
'use_default_settings: true' \
|
|
53
|
+
'server:' \
|
|
54
|
+
' secret_key: "change-me-anon-pi"' \
|
|
55
|
+
' limiter: false' \
|
|
56
|
+
' public_instance: false' \
|
|
57
|
+
'search:' \
|
|
58
|
+
' formats: [html, json]' \
|
|
59
|
+
> /etc/searxng/settings.yml
|
|
60
|
+
ENV SEARXNG_SETTINGS_PATH=/etc/searxng/settings.yml
|
|
61
|
+
|
|
62
|
+
# webveil.json (global): point at the Unix socket, backend hop DIRECT (netcage
|
|
63
|
+
# anonymizes it). fetchEgress is left DIRECT too, since netcage forces the fetch
|
|
64
|
+
# hop through the proxy as well; no webveil-side proxy config is needed in-jail.
|
|
65
|
+
RUN mkdir -p /root/.pi/agent \
|
|
66
|
+
&& printf '%s\n' \
|
|
67
|
+
'{' \
|
|
68
|
+
' "backend": "searxng",' \
|
|
69
|
+
' "baseUrl": "unix:/run/searxng/socket",' \
|
|
70
|
+
' "egress": { "mode": "direct" }' \
|
|
71
|
+
'}' \
|
|
72
|
+
> /root/.pi/agent/webveil.json \
|
|
73
|
+
&& printf '{"/work": true}\n' > /root/.pi/agent/trust.json
|
|
74
|
+
|
|
75
|
+
# --- entrypoint: start SearXNG (HTTP over the Unix socket), then exec CMD ------
|
|
76
|
+
# anon-pi passes `sh -c 'cp ... && exec pi'` as the CMD; a Dockerfile ENTRYPOINT
|
|
77
|
+
# is PREPENDED to it (OCI), so this wrapper starts SearXNG then execs anon-pi's
|
|
78
|
+
# command. `http-socket` (not `socket`) makes uWSGI speak HTTP over the socket,
|
|
79
|
+
# which is what webveil's `unix:` baseUrl requires.
|
|
80
|
+
RUN mkdir -p /run/searxng && printf '%s\n' \
|
|
81
|
+
'#!/bin/sh' \
|
|
82
|
+
'set -e' \
|
|
83
|
+
'uwsgi --master --plugin python3,http \' \
|
|
84
|
+
' --http-socket /run/searxng/socket --chmod-socket=666 \' \
|
|
85
|
+
' --venv "$SEARXNG_HOME/venv" --module searx.webapp \' \
|
|
86
|
+
' --daemonize /tmp/searxng.log' \
|
|
87
|
+
'# Wait for the socket to appear (bounded).' \
|
|
88
|
+
'i=0; while [ ! -S /run/searxng/socket ] && [ $i -lt 30 ]; do i=$((i+1)); sleep 0.5; done' \
|
|
89
|
+
'exec "$@"' \
|
|
90
|
+
> /usr/local/bin/anon-pi-entrypoint \
|
|
91
|
+
&& chmod +x /usr/local/bin/anon-pi-entrypoint
|
|
92
|
+
ENTRYPOINT ["/usr/local/bin/anon-pi-entrypoint"]
|
|
93
|
+
|
|
94
|
+
WORKDIR /work
|