anon-pi 0.1.1 → 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 +35 -7
- package/README.md +65 -23
- package/dist/anon-pi.d.ts +157 -44
- package/dist/anon-pi.d.ts.map +1 -1
- package/dist/anon-pi.js +311 -104
- package/dist/anon-pi.js.map +1 -1
- package/dist/cli.js +92 -18
- package/dist/cli.js.map +1 -1
- package/examples/Dockerfile.pi-webveil +100 -0
- package/package.json +3 -2
- package/src/anon-pi.ts +391 -135
- package/src/cli.ts +129 -19
package/src/cli.ts
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// anon-pi CLI
|
|
3
|
-
//
|
|
4
|
-
// stdio so
|
|
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.
|
|
5
10
|
|
|
6
|
-
import {
|
|
11
|
+
import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'node:fs';
|
|
7
12
|
import {spawnSync} from 'node:child_process';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
13
|
+
import {join} from 'node:path';
|
|
14
|
+
import {
|
|
15
|
+
AnonPiError,
|
|
16
|
+
buildRunPlan,
|
|
17
|
+
envFromProcess,
|
|
18
|
+
HELP,
|
|
19
|
+
MODELS_FILE,
|
|
20
|
+
pickProviderForLlm,
|
|
21
|
+
resolveConfigSeed,
|
|
22
|
+
resolveSourceModelsPath,
|
|
23
|
+
type PiModelsFile,
|
|
24
|
+
} from './anon-pi.js';
|
|
10
25
|
|
|
11
26
|
function main(argv: string[]): number {
|
|
12
27
|
const args = argv.slice(2);
|
|
@@ -16,11 +31,23 @@ function main(argv: string[]): number {
|
|
|
16
31
|
return 0;
|
|
17
32
|
}
|
|
18
33
|
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
// Subcommand dispatch: the first bare token may be `import`.
|
|
35
|
+
if (args[0] === 'import') {
|
|
36
|
+
return runImport(args.slice(1));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return runLaunch(args);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- anon-pi [WORKDIR] : launch pi jailed -----------------------------------
|
|
43
|
+
function runLaunch(args: string[]): number {
|
|
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');
|
|
22
47
|
const positionals = args.filter((a) => !a.startsWith('-'));
|
|
23
|
-
const flags = args.filter(
|
|
48
|
+
const flags = args.filter(
|
|
49
|
+
(a) => a.startsWith('-') && a !== '--ephemeral' && a !== '--eph',
|
|
50
|
+
);
|
|
24
51
|
if (flags.length > 0) {
|
|
25
52
|
process.stderr.write(
|
|
26
53
|
`anon-pi: unknown option(s): ${flags.join(' ')}\nRun \`anon-pi --help\`.\n`,
|
|
@@ -35,6 +62,7 @@ function main(argv: string[]): number {
|
|
|
35
62
|
}
|
|
36
63
|
|
|
37
64
|
const env = envFromProcess(process.env);
|
|
65
|
+
if (ephemeralFlag) env.ephemeral = true;
|
|
38
66
|
|
|
39
67
|
let plan;
|
|
40
68
|
try {
|
|
@@ -56,19 +84,23 @@ function main(argv: string[]): number {
|
|
|
56
84
|
return 1;
|
|
57
85
|
}
|
|
58
86
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
cpSync(plan.configSeed, plan.sessionAgentDir, {recursive: true});
|
|
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.
|
|
64
91
|
process.stderr.write(
|
|
65
|
-
|
|
92
|
+
'anon-pi: ephemeral session (nothing persisted; no host state)\n',
|
|
66
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
|
+
}
|
|
67
102
|
}
|
|
68
103
|
|
|
69
|
-
// Ensure the workdir exists (a fresh named folder is fine).
|
|
70
|
-
mkdirSync(plan.workdir, {recursive: true});
|
|
71
|
-
|
|
72
104
|
// Hand off to netcage with inherited stdio so -it is a real interactive TTY.
|
|
73
105
|
const res = spawnSync('netcage', plan.netcageArgs, {stdio: 'inherit'});
|
|
74
106
|
if (res.error) {
|
|
@@ -81,6 +113,84 @@ function main(argv: string[]): number {
|
|
|
81
113
|
return res.status ?? 1;
|
|
82
114
|
}
|
|
83
115
|
|
|
116
|
+
// --- anon-pi import : write the seed models.json ----------------------------
|
|
117
|
+
function runImport(args: string[]): number {
|
|
118
|
+
const force = args.includes('--force') || args.includes('-f');
|
|
119
|
+
const stray = args.filter(
|
|
120
|
+
(a) => a.startsWith('-') && a !== '--force' && a !== '-f',
|
|
121
|
+
);
|
|
122
|
+
if (stray.length > 0) {
|
|
123
|
+
process.stderr.write(
|
|
124
|
+
`anon-pi import: unknown option(s): ${stray.join(' ')}\nRun \`anon-pi --help\`.\n`,
|
|
125
|
+
);
|
|
126
|
+
return 2;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const env = envFromProcess(process.env);
|
|
130
|
+
|
|
131
|
+
if (!env.llmDirect || env.llmDirect.trim() === '') {
|
|
132
|
+
process.stderr.write(
|
|
133
|
+
'anon-pi import: set ANON_PI_LLM to the RFC1918/link-local IP[:port] of the local\n' +
|
|
134
|
+
'model whose provider should be imported (e.g. ANON_PI_LLM=192.168.1.150:8080).\n',
|
|
135
|
+
);
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const source = resolveSourceModelsPath(env);
|
|
140
|
+
if (!existsSync(source)) {
|
|
141
|
+
process.stderr.write(
|
|
142
|
+
`anon-pi import: host models.json not found at ${source}.\n` +
|
|
143
|
+
'Set ANON_PI_SOURCE_MODELS to your pi models.json, or run pi once to create it.\n',
|
|
144
|
+
);
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let hostModels: PiModelsFile;
|
|
149
|
+
try {
|
|
150
|
+
hostModels = JSON.parse(readFileSync(source, 'utf8')) as PiModelsFile;
|
|
151
|
+
} catch (e) {
|
|
152
|
+
process.stderr.write(
|
|
153
|
+
`anon-pi import: could not parse ${source}: ${(e as Error).message}\n`,
|
|
154
|
+
);
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let result;
|
|
159
|
+
try {
|
|
160
|
+
result = pickProviderForLlm(hostModels, env.llmDirect);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
if (e instanceof AnonPiError) {
|
|
163
|
+
process.stderr.write(e.message + '\n');
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
throw e;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const seedDir = resolveConfigSeed(env);
|
|
170
|
+
const dest = join(seedDir, MODELS_FILE);
|
|
171
|
+
if (existsSync(dest) && !force) {
|
|
172
|
+
process.stderr.write(
|
|
173
|
+
`anon-pi import: ${dest} already exists. Re-run with --force to overwrite.\n`,
|
|
174
|
+
);
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (result.apiKeyLooksReal) {
|
|
179
|
+
process.stderr.write(
|
|
180
|
+
`anon-pi import: WARNING: provider "${result.name}" carries a real-looking apiKey; it\n` +
|
|
181
|
+
'will be written into the seed. For a local model this is usually fine, but review\n' +
|
|
182
|
+
`${dest} if that key identifies you.\n`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
mkdirSync(seedDir, {recursive: true});
|
|
187
|
+
writeFileSync(dest, JSON.stringify(result.models, null, 2) + '\n');
|
|
188
|
+
process.stderr.write(
|
|
189
|
+
`anon-pi import: wrote ${dest} (provider "${result.name}"). Run \`anon-pi\` to launch.\n`,
|
|
190
|
+
);
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
84
194
|
function hasNetcage(): boolean {
|
|
85
195
|
const which = spawnSync(
|
|
86
196
|
process.platform === 'win32' ? 'where' : 'command',
|