anon-pi 0.7.0 → 0.9.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/README.md +43 -9
- package/dist/anon-pi.d.ts +94 -6
- package/dist/anon-pi.d.ts.map +1 -1
- package/dist/anon-pi.js +243 -15
- package/dist/anon-pi.js.map +1 -1
- package/dist/cli.js +144 -34
- package/dist/cli.js.map +1 -1
- package/package.json +4 -3
- package/src/anon-pi.ts +290 -17
- package/src/cli.ts +165 -35
package/src/cli.ts
CHANGED
|
@@ -35,6 +35,10 @@ import {
|
|
|
35
35
|
buildMenuEntries,
|
|
36
36
|
builtinProjectsRoot,
|
|
37
37
|
deriveProjectUsage,
|
|
38
|
+
expandTilde,
|
|
39
|
+
findingsFromNetcageDetect,
|
|
40
|
+
processNoteFromNetcageDetect,
|
|
41
|
+
resolveNetcageGraphroot,
|
|
38
42
|
globalModelsSeedPath,
|
|
39
43
|
globalSettingsSeedPath,
|
|
40
44
|
machineAgentDir,
|
|
@@ -51,6 +55,8 @@ import {
|
|
|
51
55
|
resolveDeleteProject,
|
|
52
56
|
parseConfigJson,
|
|
53
57
|
parseLaunchArgs,
|
|
58
|
+
isHeadlessPiArgs,
|
|
59
|
+
anonPiVersion,
|
|
54
60
|
parseMachineArgs,
|
|
55
61
|
parseMachineJson,
|
|
56
62
|
projectHostDir,
|
|
@@ -85,6 +91,7 @@ import {
|
|
|
85
91
|
type MenuEntry,
|
|
86
92
|
type SessionDirListing,
|
|
87
93
|
type ProxyFinding,
|
|
94
|
+
type NetcageDetectProxy,
|
|
88
95
|
type SocksHandshake,
|
|
89
96
|
type InitImageChoice,
|
|
90
97
|
type AnonPiConfig,
|
|
@@ -110,6 +117,14 @@ const ANON_PI_KEY_LABEL = 'anon-pi.key';
|
|
|
110
117
|
function main(argv: string[]): number {
|
|
111
118
|
const args = argv.slice(2);
|
|
112
119
|
|
|
120
|
+
// `--version`/`-V` prints anon-pi's own version and exits (before the launch
|
|
121
|
+
// grammar, so it is never parsed as a project/flag). For pi's version inside
|
|
122
|
+
// the jail, forward it: `anon-pi pi --version`.
|
|
123
|
+
if (args[0] === '--version' || args[0] === '-V') {
|
|
124
|
+
process.stdout.write(`anon-pi ${anonPiVersion() ?? '(unknown)'}\n`);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
113
128
|
// The global `--help`/`-h` prints the top-level HELP, EXCEPT when the first
|
|
114
129
|
// token is a subcommand that owns its own `--help` (so `anon-pi init --help`
|
|
115
130
|
// and `anon-pi machine --help` show THEIR help, not the global one). Those
|
|
@@ -242,7 +257,13 @@ function runLaunch(parsed: ParsedLaunch): number {
|
|
|
242
257
|
const image = machineConf.image ?? env.image ?? '';
|
|
243
258
|
|
|
244
259
|
// --mount re-roots at a HOST parent; otherwise the resolved projects root.
|
|
245
|
-
|
|
260
|
+
// Expand a leading `~` + absolutize the mount path so it is a real host dir
|
|
261
|
+
// everywhere it is used (the mount, the mkdir, the intent). path.resolve
|
|
262
|
+
// alone would leave `~/x` as a literal `~` dir.
|
|
263
|
+
const mountParent =
|
|
264
|
+
parsed.mountParent !== undefined
|
|
265
|
+
? resolve(expandTilde(parsed.mountParent, env.home))
|
|
266
|
+
: undefined;
|
|
246
267
|
const projectsRoot = resolveProjectsRoot({
|
|
247
268
|
env,
|
|
248
269
|
config,
|
|
@@ -278,10 +299,10 @@ function runLaunch(parsed: ParsedLaunch): number {
|
|
|
278
299
|
}
|
|
279
300
|
|
|
280
301
|
// No-TTY discipline: the bare MENU and every INTERACTIVE launch (interactive
|
|
281
|
-
// pi, or a shell) need a TTY; a HEADLESS pi run (
|
|
282
|
-
// NOT.
|
|
283
|
-
|
|
284
|
-
|
|
302
|
+
// pi, or a shell) need a TTY; only a HEADLESS pi run (forwarded `-p`/`--print`)
|
|
303
|
+
// does NOT. Forwarded args that stay interactive (e.g. `--session <id>`,
|
|
304
|
+
// `--model x`) still require a TTY. Check BEFORE we mutate anything or spawn.
|
|
305
|
+
const headless = parsed.mode === 'pi' && isHeadlessPiArgs(parsed.piArgs);
|
|
285
306
|
if (!headless && !process.stdin.isTTY) {
|
|
286
307
|
if (parsed.mode === 'menu') {
|
|
287
308
|
process.stderr.write(
|
|
@@ -1197,25 +1218,28 @@ function initProxyStep(currentProxy: string | undefined): string | undefined {
|
|
|
1197
1218
|
}
|
|
1198
1219
|
process.stdout.write(' Probing common SOCKS ports (evidence only)...\n');
|
|
1199
1220
|
|
|
1200
|
-
//
|
|
1201
|
-
//
|
|
1202
|
-
//
|
|
1203
|
-
//
|
|
1204
|
-
//
|
|
1205
|
-
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
(
|
|
1221
|
+
// REUSE netcage's SOCKS scanner when available: `netcage detect-proxy --json`
|
|
1222
|
+
// probes the same ports + does the SOCKS5 handshake + the weak process hint,
|
|
1223
|
+
// and is the same evidence engine that backs `netcage setup-default`. Falling
|
|
1224
|
+
// back to anon-pi's own probe keeps init working on an older netcage (or if
|
|
1225
|
+
// the verb errors). Either way the findings render through the same PURE
|
|
1226
|
+
// formatter, so the honesty invariant (never label the provider) is identical.
|
|
1227
|
+
const detected = detectProxyViaNetcage();
|
|
1228
|
+
let findings: ProxyFinding[] = detected
|
|
1229
|
+
? findingsFromNetcageDetect(detected)
|
|
1230
|
+
: [];
|
|
1231
|
+
let processNote: string | undefined = detected
|
|
1232
|
+
? processNoteFromNetcageDetect(detected)
|
|
1233
|
+
: undefined;
|
|
1234
|
+
if (findings.length === 0) {
|
|
1235
|
+
// Local probe fallback: TCP-open + a real SOCKS5 handshake per default
|
|
1236
|
+
// port. The weak process hint is HOST-WIDE, gathered once as the note.
|
|
1237
|
+
processNote = matchProcessHint(observeRunningProcesses());
|
|
1238
|
+
findings = DEFAULT_SOCKS_PROBE_PORTS.map(({port, hint}) => {
|
|
1209
1239
|
const {open, handshake} = probeSocks5('127.0.0.1', port);
|
|
1210
|
-
return {
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
open,
|
|
1214
|
-
handshake,
|
|
1215
|
-
portHint: hint,
|
|
1216
|
-
};
|
|
1217
|
-
},
|
|
1218
|
-
);
|
|
1240
|
+
return {host: '127.0.0.1', port, open, handshake, portHint: hint};
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1219
1243
|
process.stdout.write(
|
|
1220
1244
|
'\n' + formatProxyFindings(findings, processNote) + '\n\n',
|
|
1221
1245
|
);
|
|
@@ -1556,8 +1580,14 @@ function initImageStep(): string | undefined | typeof ABORT {
|
|
|
1556
1580
|
);
|
|
1557
1581
|
continue;
|
|
1558
1582
|
}
|
|
1583
|
+
// Fully-qualified `localhost/` tag: podman refuses an UNQUALIFIED short name
|
|
1584
|
+
// at run time ("did not resolve to an alias and no unqualified-search
|
|
1585
|
+
// registries defined"), so a locally-built image MUST carry the localhost/
|
|
1586
|
+
// prefix to be runnable by name.
|
|
1559
1587
|
const tag =
|
|
1560
|
-
choice === 'basic'
|
|
1588
|
+
choice === 'basic'
|
|
1589
|
+
? 'localhost/anon-pi/pi:latest'
|
|
1590
|
+
: 'localhost/anon-pi/pi-webveil:latest';
|
|
1561
1591
|
const built = buildImage(dockerfile, tag);
|
|
1562
1592
|
if (!built) {
|
|
1563
1593
|
process.stdout.write(' Build failed; pick another option.\n');
|
|
@@ -1598,9 +1628,10 @@ function initProjectsStep(
|
|
|
1598
1628
|
if (ans === undefined) return currentProjects;
|
|
1599
1629
|
const trimmed = ans.trim();
|
|
1600
1630
|
if (trimmed === '') return currentProjects;
|
|
1601
|
-
//
|
|
1602
|
-
//
|
|
1603
|
-
|
|
1631
|
+
// Expand a leading `~` (path.resolve does NOT — it would make a literal `~`
|
|
1632
|
+
// dir), then absolutize. Store the built-in as "unset" (undefined) so
|
|
1633
|
+
// config.json stays clean when the user just accepts the default path.
|
|
1634
|
+
const chosen = resolve(expandTilde(trimmed, env.home));
|
|
1604
1635
|
if (chosen === builtin) return undefined;
|
|
1605
1636
|
return chosen;
|
|
1606
1637
|
}
|
|
@@ -1779,29 +1810,128 @@ function matchProcessHint(processes: readonly string[]): string | undefined {
|
|
|
1779
1810
|
return undefined;
|
|
1780
1811
|
}
|
|
1781
1812
|
|
|
1813
|
+
/** Whether `netcage <verb>` exists (probe its help; false on any spawn error). */
|
|
1814
|
+
function hasNetcageVerb(verb: string): boolean {
|
|
1815
|
+
const res = spawnSync('netcage', [verb, '--help'], {stdio: 'ignore'});
|
|
1816
|
+
if (res.error) return false;
|
|
1817
|
+
// netcage prints an "unknown subcommand" error (non-zero) for a missing verb,
|
|
1818
|
+
// and help (exit 0) for a real one. Treat exit 0 as "exists".
|
|
1819
|
+
return res.status === 0;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
/**
|
|
1823
|
+
* Run `netcage detect-proxy --json` and parse it, to REUSE netcage's SOCKS
|
|
1824
|
+
* scanner (probe + handshake + process hint + exit-IP verify) in `init`. Returns
|
|
1825
|
+
* the parsed result, or undefined when netcage lacks the verb / errors / emits
|
|
1826
|
+
* non-JSON — in which case init falls back to its own local probe. Best-effort;
|
|
1827
|
+
* never throws.
|
|
1828
|
+
*/
|
|
1829
|
+
function detectProxyViaNetcage(): NetcageDetectProxy | undefined {
|
|
1830
|
+
const res = spawnSync('netcage', ['detect-proxy', '--json'], {
|
|
1831
|
+
encoding: 'utf8',
|
|
1832
|
+
timeout: 20000,
|
|
1833
|
+
});
|
|
1834
|
+
if (res.error || res.status !== 0 || !res.stdout) return undefined;
|
|
1835
|
+
try {
|
|
1836
|
+
const parsed = JSON.parse(res.stdout) as NetcageDetectProxy;
|
|
1837
|
+
return parsed && typeof parsed === 'object' ? parsed : undefined;
|
|
1838
|
+
} catch {
|
|
1839
|
+
return undefined;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1782
1843
|
/**
|
|
1783
|
-
* Build a shipped Dockerfile into `tag
|
|
1784
|
-
*
|
|
1785
|
-
*
|
|
1786
|
-
*
|
|
1844
|
+
* Build a shipped Dockerfile into `tag`, landing it in the SAME store
|
|
1845
|
+
* `netcage run` reads.
|
|
1846
|
+
*
|
|
1847
|
+
* Since netcage v0.7.0 that store is netcage's private podman graphroot
|
|
1848
|
+
* (`--root <graphroot>`), NOT the operator's default rootless store, so a plain
|
|
1849
|
+
* `podman build` would put the image where `netcage run` cannot see it (it would
|
|
1850
|
+
* try to pull the `localhost/…` ref and fail). We therefore:
|
|
1851
|
+
* 1. PREFER `netcage build` when netcage exposes it (future-proof: netcage owns
|
|
1852
|
+
* its store + graphroot, no path hardcoded here); else
|
|
1853
|
+
* 2. `podman build` into the default store, then `podman save | podman --root
|
|
1854
|
+
* <graphroot> load` to copy it into netcage's store (the interim workaround
|
|
1855
|
+
* until netcage ships a build/load verb).
|
|
1856
|
+
* Streams output (inherited stdio) so the user sees the build. Returns true on
|
|
1857
|
+
* success. The build CONTEXT is the Dockerfile's own directory.
|
|
1787
1858
|
*/
|
|
1788
1859
|
function buildImage(dockerfile: string, tag: string): boolean {
|
|
1789
1860
|
const context = dirname(dockerfile);
|
|
1861
|
+
|
|
1862
|
+
// 1) Prefer a native `netcage build` (it targets netcage's own store).
|
|
1863
|
+
if (hasNetcageVerb('build')) {
|
|
1864
|
+
process.stdout.write(` Building ${tag} via \`netcage build\`...\n`);
|
|
1865
|
+
const res = spawnSync(
|
|
1866
|
+
'netcage',
|
|
1867
|
+
['build', '-t', tag, '-f', dockerfile, context],
|
|
1868
|
+
{stdio: 'inherit'},
|
|
1869
|
+
);
|
|
1870
|
+
if (res.error) {
|
|
1871
|
+
process.stderr.write(
|
|
1872
|
+
` anon-pi: failed to run netcage build: ${res.error.message}\n`,
|
|
1873
|
+
);
|
|
1874
|
+
return false;
|
|
1875
|
+
}
|
|
1876
|
+
return res.status === 0;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// 2) Interim: podman build into the default store, then load into netcage's
|
|
1880
|
+
// graphroot so `netcage run` can find it.
|
|
1790
1881
|
process.stdout.write(
|
|
1791
1882
|
` Building ${tag} from ${dockerfile} (podman build)...\n`,
|
|
1792
1883
|
);
|
|
1793
|
-
const
|
|
1884
|
+
const build = spawnSync(
|
|
1794
1885
|
'podman',
|
|
1795
1886
|
['build', '-t', tag, '-f', dockerfile, context],
|
|
1796
1887
|
{stdio: 'inherit'},
|
|
1797
1888
|
);
|
|
1798
|
-
if (
|
|
1889
|
+
if (build.error) {
|
|
1799
1890
|
process.stderr.write(
|
|
1800
|
-
` anon-pi: failed to run podman: ${
|
|
1891
|
+
` anon-pi: failed to run podman: ${build.error.message}. Is podman installed?\n`,
|
|
1801
1892
|
);
|
|
1802
1893
|
return false;
|
|
1803
1894
|
}
|
|
1804
|
-
|
|
1895
|
+
if (build.status !== 0) return false;
|
|
1896
|
+
|
|
1897
|
+
return loadImageIntoNetcageStore(tag);
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
/**
|
|
1901
|
+
* Copy a locally-built image (in the default podman store) INTO netcage's
|
|
1902
|
+
* private graphroot store, so `netcage run <tag>` finds it without a pull. Uses
|
|
1903
|
+
* `podman save <tag> | podman --root <graphroot> load`. Best-effort: on failure
|
|
1904
|
+
* it warns (the image still exists in the default store; a future `netcage
|
|
1905
|
+
* build`/`load` verb removes this dance). Returns true on success.
|
|
1906
|
+
*/
|
|
1907
|
+
function loadImageIntoNetcageStore(tag: string): boolean {
|
|
1908
|
+
const graphroot = resolveNetcageGraphroot(process.env);
|
|
1909
|
+
process.stdout.write(
|
|
1910
|
+
` Loading ${tag} into netcage's store (${graphroot}) so \`netcage run\` sees it...\n`,
|
|
1911
|
+
);
|
|
1912
|
+
// `podman save <tag> | podman --root <graphroot> load`, via a shell so the
|
|
1913
|
+
// pipe is a single spawn (both ends inherit stderr for progress).
|
|
1914
|
+
const cmd =
|
|
1915
|
+
`podman save ${shQuote(tag)} | ` +
|
|
1916
|
+
`podman --root ${shQuote(graphroot)} load`;
|
|
1917
|
+
const res = spawnSync('sh', ['-c', cmd], {
|
|
1918
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
1919
|
+
});
|
|
1920
|
+
if (res.error || res.status !== 0) {
|
|
1921
|
+
process.stderr.write(
|
|
1922
|
+
` anon-pi: could not load ${tag} into netcage's store (${graphroot}).\n` +
|
|
1923
|
+
` The image is built in your default podman store, but \`netcage run\` reads\n` +
|
|
1924
|
+
` ${graphroot}. Load it by hand:\n` +
|
|
1925
|
+
` podman save ${tag} | podman --root ${graphroot} load\n`,
|
|
1926
|
+
);
|
|
1927
|
+
return false;
|
|
1928
|
+
}
|
|
1929
|
+
return true;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
/** Minimal POSIX single-quote shell-quoting for a token embedded in `sh -c`. */
|
|
1933
|
+
function shQuote(s: string): string {
|
|
1934
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1805
1935
|
}
|
|
1806
1936
|
|
|
1807
1937
|
/** List machine names (readdir of machines/), or [] if the dir is absent. */
|