anon-pi 0.8.0 → 0.9.1
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 +10 -7
- package/dist/anon-pi.d.ts +58 -3
- package/dist/anon-pi.d.ts.map +1 -1
- package/dist/anon-pi.js +89 -4
- package/dist/anon-pi.js.map +1 -1
- package/dist/cli.js +138 -31
- package/dist/cli.js.map +1 -1
- package/package.json +4 -3
- package/src/anon-pi.ts +111 -4
- package/src/cli.ts +158 -31
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,
|
|
@@ -87,6 +91,7 @@ import {
|
|
|
87
91
|
type MenuEntry,
|
|
88
92
|
type SessionDirListing,
|
|
89
93
|
type ProxyFinding,
|
|
94
|
+
type NetcageDetectProxy,
|
|
90
95
|
type SocksHandshake,
|
|
91
96
|
type InitImageChoice,
|
|
92
97
|
type AnonPiConfig,
|
|
@@ -252,7 +257,13 @@ function runLaunch(parsed: ParsedLaunch): number {
|
|
|
252
257
|
const image = machineConf.image ?? env.image ?? '';
|
|
253
258
|
|
|
254
259
|
// --mount re-roots at a HOST parent; otherwise the resolved projects root.
|
|
255
|
-
|
|
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;
|
|
256
267
|
const projectsRoot = resolveProjectsRoot({
|
|
257
268
|
env,
|
|
258
269
|
config,
|
|
@@ -1207,25 +1218,28 @@ function initProxyStep(currentProxy: string | undefined): string | undefined {
|
|
|
1207
1218
|
}
|
|
1208
1219
|
process.stdout.write(' Probing common SOCKS ports (evidence only)...\n');
|
|
1209
1220
|
|
|
1210
|
-
//
|
|
1211
|
-
//
|
|
1212
|
-
//
|
|
1213
|
-
//
|
|
1214
|
-
//
|
|
1215
|
-
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
(
|
|
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}) => {
|
|
1219
1239
|
const {open, handshake} = probeSocks5('127.0.0.1', port);
|
|
1220
|
-
return {
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
open,
|
|
1224
|
-
handshake,
|
|
1225
|
-
portHint: hint,
|
|
1226
|
-
};
|
|
1227
|
-
},
|
|
1228
|
-
);
|
|
1240
|
+
return {host: '127.0.0.1', port, open, handshake, portHint: hint};
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1229
1243
|
process.stdout.write(
|
|
1230
1244
|
'\n' + formatProxyFindings(findings, processNote) + '\n\n',
|
|
1231
1245
|
);
|
|
@@ -1566,8 +1580,14 @@ function initImageStep(): string | undefined | typeof ABORT {
|
|
|
1566
1580
|
);
|
|
1567
1581
|
continue;
|
|
1568
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.
|
|
1569
1587
|
const tag =
|
|
1570
|
-
choice === 'basic'
|
|
1588
|
+
choice === 'basic'
|
|
1589
|
+
? 'localhost/anon-pi/pi:latest'
|
|
1590
|
+
: 'localhost/anon-pi/pi-webveil:latest';
|
|
1571
1591
|
const built = buildImage(dockerfile, tag);
|
|
1572
1592
|
if (!built) {
|
|
1573
1593
|
process.stdout.write(' Build failed; pick another option.\n');
|
|
@@ -1608,9 +1628,10 @@ function initProjectsStep(
|
|
|
1608
1628
|
if (ans === undefined) return currentProjects;
|
|
1609
1629
|
const trimmed = ans.trim();
|
|
1610
1630
|
if (trimmed === '') return currentProjects;
|
|
1611
|
-
//
|
|
1612
|
-
//
|
|
1613
|
-
|
|
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));
|
|
1614
1635
|
if (chosen === builtin) return undefined;
|
|
1615
1636
|
return chosen;
|
|
1616
1637
|
}
|
|
@@ -1789,29 +1810,128 @@ function matchProcessHint(processes: readonly string[]): string | undefined {
|
|
|
1789
1810
|
return undefined;
|
|
1790
1811
|
}
|
|
1791
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
|
+
|
|
1792
1822
|
/**
|
|
1793
|
-
*
|
|
1794
|
-
*
|
|
1795
|
-
*
|
|
1796
|
-
*
|
|
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
|
+
|
|
1843
|
+
/**
|
|
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 (netcage >= 0.7.1; netcage
|
|
1852
|
+
* owns 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 fallback for an
|
|
1855
|
+
* older netcage without the build/load verbs).
|
|
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.
|
|
1797
1858
|
*/
|
|
1798
1859
|
function buildImage(dockerfile: string, tag: string): boolean {
|
|
1799
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.
|
|
1800
1881
|
process.stdout.write(
|
|
1801
1882
|
` Building ${tag} from ${dockerfile} (podman build)...\n`,
|
|
1802
1883
|
);
|
|
1803
|
-
const
|
|
1884
|
+
const build = spawnSync(
|
|
1804
1885
|
'podman',
|
|
1805
1886
|
['build', '-t', tag, '-f', dockerfile, context],
|
|
1806
1887
|
{stdio: 'inherit'},
|
|
1807
1888
|
);
|
|
1808
|
-
if (
|
|
1889
|
+
if (build.error) {
|
|
1809
1890
|
process.stderr.write(
|
|
1810
|
-
` anon-pi: failed to run podman: ${
|
|
1891
|
+
` anon-pi: failed to run podman: ${build.error.message}. Is podman installed?\n`,
|
|
1811
1892
|
);
|
|
1812
1893
|
return false;
|
|
1813
1894
|
}
|
|
1814
|
-
|
|
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; on netcage >= 0.7.1 the
|
|
1905
|
+
* `netcage build`/`load` verbs remove 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, `'\\''`)}'`;
|
|
1815
1935
|
}
|
|
1816
1936
|
|
|
1817
1937
|
/** List machine names (readdir of machines/), or [] if the dir is absent. */
|
|
@@ -1975,6 +2095,13 @@ function withKeyLabel(netcageArgs: string[], key: string): string[] {
|
|
|
1975
2095
|
|
|
1976
2096
|
/** Spawn netcage with inherited stdio; propagate its exit code. */
|
|
1977
2097
|
function spawnNetcage(netcageArgs: string[]): number {
|
|
2098
|
+
// Explain the pause: netcage sets up the jail (netns, firewall, DNS, container
|
|
2099
|
+
// start) BEFORE pi paints, so without this line the user sees only a blinking
|
|
2100
|
+
// cursor. Goes to stderr so it never pollutes any piped stdout, and is
|
|
2101
|
+
// transient (pi typically clears the screen when its TUI comes up).
|
|
2102
|
+
process.stderr.write(
|
|
2103
|
+
'anon-pi: entering the netcage jail (setting up forced-egress)\u2026\n',
|
|
2104
|
+
);
|
|
1978
2105
|
const res = spawnSync('netcage', netcageArgs, {stdio: 'inherit'});
|
|
1979
2106
|
if (res.error) {
|
|
1980
2107
|
process.stderr.write(
|