anon-pi 0.15.0 → 0.16.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 +34 -18
- package/dist/anon-pi.d.ts +159 -111
- package/dist/anon-pi.d.ts.map +1 -1
- package/dist/anon-pi.js +240 -130
- package/dist/anon-pi.js.map +1 -1
- package/dist/cli.js +395 -116
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/src/anon-pi.ts +322 -201
- package/src/cli.ts +450 -130
package/dist/anon-pi.js
CHANGED
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
// /projects (the projects root). `--mount <parent>` adds EXACTLY one more
|
|
15
15
|
// mount at the DISTINCT /work and re-roots cwd there; nothing else changes,
|
|
16
16
|
// so we never remount a running container.
|
|
17
|
-
// -
|
|
18
|
-
//
|
|
19
|
-
//
|
|
17
|
+
// - Every launch is THROWAWAY (the container is always `--rm`): it is removed
|
|
18
|
+
// on exit. Durable state is EXPLICIT and image-based (snapshot a running
|
|
19
|
+
// container into a named image, then a machine pinned to it); the machine
|
|
20
|
+
// home persists regardless (it is a host mount). See docs/adr/0004.
|
|
20
21
|
// - Open exactly ONE direct hole (--allow-direct <llm>) so pi can reach a
|
|
21
22
|
// local model while ALL other egress stays forced through the socks5h proxy
|
|
22
23
|
// (fail-closed; the proxy is REQUIRED and never guessed).
|
|
@@ -26,9 +27,9 @@
|
|
|
26
27
|
//
|
|
27
28
|
// This module holds every DECISION as a pure function (config load + precedence,
|
|
28
29
|
// machine/project resolvers, name validation, the RunPlan argv, the menu
|
|
29
|
-
// choice-list, project usage,
|
|
30
|
-
//
|
|
31
|
-
//
|
|
30
|
+
// choice-list, project usage, models.json generation, init's proxy detect/verify
|
|
31
|
+
// decisions). cli.ts owns only the impure edges (fs, the interactive TUI, the
|
|
32
|
+
// netcage query, the spawn).
|
|
32
33
|
import { existsSync, readFileSync } from 'node:fs';
|
|
33
34
|
import { homedir } from 'node:os';
|
|
34
35
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -291,14 +292,29 @@ export function resolveDeleteProject(args) {
|
|
|
291
292
|
*/
|
|
292
293
|
export const ROOT_TOKEN = '.';
|
|
293
294
|
/**
|
|
294
|
-
* Reserved names that a machine/project may NOT take (case-sensitive).
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
|
|
301
|
-
|
|
295
|
+
* Reserved names that a machine/project/image may NOT take (case-sensitive).
|
|
296
|
+
* `.` is the root token (see ROOT_TOKEN); `..` is parent-traversal (both are
|
|
297
|
+
* also rejected by the structural checks below, but listed here so the
|
|
298
|
+
* reserved-name concept is explicit). `pi` is the passthrough token. The
|
|
299
|
+
* SUBCOMMAND NOUN words (`machine`, `image`, `init`, `forward`, `ports`) are
|
|
300
|
+
* reserved too: each is dispatched BEFORE the launch grammar, so a folder so
|
|
301
|
+
* named would be UNREACHABLE by bare name (a latent trap). Reserving them makes
|
|
302
|
+
* validateName refuse such a name up front with a clear error, closing the
|
|
303
|
+
* trap. `--mount`'s `/work` is a CONTAINER path, not a name here, so it needs no
|
|
304
|
+
* reservation. The reservation is GLOBAL (validateName is the one validator);
|
|
305
|
+
* the menu tolerates a pre-existing folder now reserved by FILTERING it out via
|
|
306
|
+
* the try/catch isProjectName, so a now-reserved folder is skipped, not a crash.
|
|
307
|
+
*/
|
|
308
|
+
export const RESERVED_NAMES = [
|
|
309
|
+
'.',
|
|
310
|
+
'..',
|
|
311
|
+
'pi',
|
|
312
|
+
'machine',
|
|
313
|
+
'image',
|
|
314
|
+
'init',
|
|
315
|
+
'forward',
|
|
316
|
+
'ports',
|
|
317
|
+
];
|
|
302
318
|
/**
|
|
303
319
|
* PURE: validate a machine or project name as a safe single path segment, and
|
|
304
320
|
* return it unchanged on success. Rejects (with AnonPiError):
|
|
@@ -387,7 +403,7 @@ export function resolveCwd(kind, token) {
|
|
|
387
403
|
* and files written under the home land in the machine's config home on the
|
|
388
404
|
* host; a shell is the project-hopper, so `/projects` is the natural landing.
|
|
389
405
|
* The machine home is one `cd ~` away for the rare case. `menu` never reaches
|
|
390
|
-
* here (it is argv-less). Shared by resolveRunPlan +
|
|
406
|
+
* here (it is argv-less). Shared by resolveRunPlan + launchIdentityKey so the run
|
|
391
407
|
* cwd and the container-identity key always agree.
|
|
392
408
|
*/
|
|
393
409
|
export function launchCwd(_mode, kind, project) {
|
|
@@ -516,6 +532,22 @@ export function resolveProxy(args) {
|
|
|
516
532
|
export function resolveLlm(args) {
|
|
517
533
|
return nonEmpty(args.env.llmDirect) ?? nonEmpty(args.config?.llm);
|
|
518
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* PURE: resolve the IMAGE a launch runs against, highest-priority first:
|
|
537
|
+
* a per-launch `-i`/`--image` override > the machine's pinned image
|
|
538
|
+
* (machine.json) > `ANON_PI_IMAGE` (the env fallback) > undefined (the CLL then
|
|
539
|
+
* errors). The `-i` override is STRICTLY EPHEMERAL: it selects the image for
|
|
540
|
+
* THIS launch only and is NEVER written back to machine.json (that persistent
|
|
541
|
+
* pin is `machine set-image` / `machine create --image`). No mismatch warning is
|
|
542
|
+
* ever emitted (ADR-0003 section 3: `-i` is explicit + ephemeral, so a warning
|
|
543
|
+
* carries no information the user lacks). Empty strings are treated as unset at
|
|
544
|
+
* every tier (nonEmpty), so a blank env/pin falls through cleanly.
|
|
545
|
+
*/
|
|
546
|
+
export function resolveLaunchImage(args) {
|
|
547
|
+
return (nonEmpty(args.override) ??
|
|
548
|
+
nonEmpty(args.machineImage) ??
|
|
549
|
+
nonEmpty(args.envImage));
|
|
550
|
+
}
|
|
519
551
|
// --- Grammar A: the pure argv -> ParsedLaunch parser -------------------------
|
|
520
552
|
//
|
|
521
553
|
// A bare positional is a PROJECT; `-m` picks the machine. The CLI (cli.ts)
|
|
@@ -666,6 +698,24 @@ export function sessionHeaderCwd(headerLine) {
|
|
|
666
698
|
* RESERVED project name (see RESERVED_NAMES) so a project can never shadow it.
|
|
667
699
|
*/
|
|
668
700
|
export const PI_PASSTHROUGH_TOKEN = 'pi';
|
|
701
|
+
/**
|
|
702
|
+
* The retired launch flags. `--keep`/`--rm` are GONE (ADR-0004): every launch is
|
|
703
|
+
* throwaway now, so there is no flag to toggle. `--keep`'s exploratory
|
|
704
|
+
* "apt install, quit, re-enter" use case is served, better, by snapshotting a
|
|
705
|
+
* running container into a named image and pinning a machine to it (explicit +
|
|
706
|
+
* named, no inference). The label a launch is passed one of these RETIRED flags
|
|
707
|
+
* gets a clear error pointing there.
|
|
708
|
+
*/
|
|
709
|
+
export const RETIRED_LAUNCH_FLAGS = ['--keep', '--rm'];
|
|
710
|
+
/** PURE: the error message for a retired `--keep`/`--rm` flag, pointing at the image-based replacement. */
|
|
711
|
+
export function retiredKeepRmMessage(flag) {
|
|
712
|
+
return (`${flag} is gone: every launch is throwaway now (the container is always ` +
|
|
713
|
+
`removed on exit). To persist system state you set up in a session (e.g. after ` +
|
|
714
|
+
`\`apt install\`), snapshot the RUNNING container into a named image and use it:\n` +
|
|
715
|
+
` anon-pi image snapshot <name> (freeze the running container -> anon-pi/<name>:latest)\n` +
|
|
716
|
+
` anon-pi machine create <m> --image anon-pi/<name>:latest (a durable machine pinned to it)\n` +
|
|
717
|
+
`Your pi config + conversations live in the machine home (a host mount) and persist regardless.`);
|
|
718
|
+
}
|
|
669
719
|
/**
|
|
670
720
|
* PURE: whether forwarded pi args request pi's NON-INTERACTIVE (print) mode,
|
|
671
721
|
* i.e. contain `-p`/`--print`. This is the ONLY headless shape (it needs no
|
|
@@ -683,9 +733,6 @@ export function isHeadlessPiArgs(piArgs) {
|
|
|
683
733
|
* `--shell` is incompatible (a shell forwards no pi args).
|
|
684
734
|
*/
|
|
685
735
|
function finishPiNoProjectLaunch(args) {
|
|
686
|
-
if (args.keep && args.rm) {
|
|
687
|
-
args.fail('--keep and --rm are contradictory (pick one; --rm is the default)');
|
|
688
|
-
}
|
|
689
736
|
if (args.shell) {
|
|
690
737
|
args.fail('--shell forwards no pi args (a shell has no session/query). Drop --shell.');
|
|
691
738
|
}
|
|
@@ -695,14 +742,14 @@ function finishPiNoProjectLaunch(args) {
|
|
|
695
742
|
machineExplicit: args.machineExplicit,
|
|
696
743
|
project: undefined,
|
|
697
744
|
mountParent: args.mountParent,
|
|
698
|
-
|
|
745
|
+
image: args.image,
|
|
699
746
|
piArgs: args.piArgs,
|
|
700
747
|
};
|
|
701
748
|
}
|
|
702
749
|
/**
|
|
703
750
|
* PURE: parse grammar A into a ParsedLaunch. Consumes the anon-pi flags
|
|
704
|
-
* (`-m <machine>`, `--shell`, `--mount <parent>`,
|
|
705
|
-
* project positional; the FIRST bare positional is the project (`.` allowed as
|
|
751
|
+
* (`-m <machine>`, `--shell`, `--mount <parent>`, `-i`/`--image <ref>`) LEFT of
|
|
752
|
+
* the project positional; the FIRST bare positional is the project (`.` allowed as
|
|
706
753
|
* the root token). In pi mode every token AFTER the project is forwarded to pi
|
|
707
754
|
* verbatim (so `anon-pi recon -p '...'` works) — anon-pi flags must come before
|
|
708
755
|
* the project. A pi session-resume flag (`--session <id>`, `--continue`,
|
|
@@ -715,15 +762,14 @@ function finishPiNoProjectLaunch(args) {
|
|
|
715
762
|
* reserved-name guard); `--mount <parent>` is a HOST path in its own namespace,
|
|
716
763
|
* distinct from the project-name namespace (NAME vs `--mount` exclusivity), so
|
|
717
764
|
* it is NOT name-validated here. Throws AnonPiError for an unknown option, a
|
|
718
|
-
* missing `-m`/`--mount` argument, a
|
|
765
|
+
* missing `-m`/`--mount` argument, a RETIRED `--keep`/`--rm` flag, or a bad name.
|
|
719
766
|
*/
|
|
720
767
|
export function parseLaunchArgs(args) {
|
|
721
768
|
let machine = DEFAULT_MACHINE;
|
|
722
769
|
let machineSet = false;
|
|
723
770
|
let shell = false;
|
|
724
771
|
let mountParent;
|
|
725
|
-
let
|
|
726
|
-
let rmSeen = false;
|
|
772
|
+
let image;
|
|
727
773
|
let project;
|
|
728
774
|
let piArgs;
|
|
729
775
|
const fail = (msg) => {
|
|
@@ -751,13 +797,21 @@ export function parseLaunchArgs(args) {
|
|
|
751
797
|
mountParent = v;
|
|
752
798
|
continue;
|
|
753
799
|
}
|
|
754
|
-
if (a === '--
|
|
755
|
-
|
|
800
|
+
if (a === '-i' || a === '--image') {
|
|
801
|
+
// The EPHEMERAL per-launch image override. A raw ref, NOT a
|
|
802
|
+
// name-namespaced anon-pi token (it resolves in netcage's private
|
|
803
|
+
// image store, so any podman ref / `anon-pi/<name>:latest` snapshot tag
|
|
804
|
+
// is valid): not name-validated here. It never mutates machine.json.
|
|
805
|
+
const v = args[++i];
|
|
806
|
+
if (v === undefined)
|
|
807
|
+
fail(`${a} needs an image ref`);
|
|
808
|
+
image = v;
|
|
756
809
|
continue;
|
|
757
810
|
}
|
|
758
|
-
if (a
|
|
759
|
-
|
|
760
|
-
|
|
811
|
+
if (RETIRED_LAUNCH_FLAGS.includes(a)) {
|
|
812
|
+
// `--keep`/`--rm` are retired (ADR-0004): throwaway is the only
|
|
813
|
+
// behaviour now. Point at the image-based replacement.
|
|
814
|
+
fail(retiredKeepRmMessage(a));
|
|
761
815
|
}
|
|
762
816
|
if (a === '.') {
|
|
763
817
|
// the root token is a valid project positional (not a name).
|
|
@@ -774,8 +828,7 @@ export function parseLaunchArgs(args) {
|
|
|
774
828
|
machine,
|
|
775
829
|
machineExplicit: machineSet,
|
|
776
830
|
mountParent,
|
|
777
|
-
|
|
778
|
-
rm: rmSeen,
|
|
831
|
+
image,
|
|
779
832
|
shell,
|
|
780
833
|
piArgs: args.slice(i + 1),
|
|
781
834
|
fail,
|
|
@@ -815,8 +868,7 @@ export function parseLaunchArgs(args) {
|
|
|
815
868
|
machine,
|
|
816
869
|
machineExplicit: machineSet,
|
|
817
870
|
mountParent,
|
|
818
|
-
|
|
819
|
-
rm: rmSeen,
|
|
871
|
+
image,
|
|
820
872
|
shell,
|
|
821
873
|
piArgs,
|
|
822
874
|
fail,
|
|
@@ -830,9 +882,6 @@ export function parseLaunchArgs(args) {
|
|
|
830
882
|
i++;
|
|
831
883
|
break;
|
|
832
884
|
}
|
|
833
|
-
if (keepSeen && rmSeen) {
|
|
834
|
-
fail('--keep and --rm are contradictory (pick one; --rm is the default)');
|
|
835
|
-
}
|
|
836
885
|
// tokens remaining after the project.
|
|
837
886
|
const rest = args.slice(i);
|
|
838
887
|
if (shell) {
|
|
@@ -846,7 +895,7 @@ export function parseLaunchArgs(args) {
|
|
|
846
895
|
machineExplicit: machineSet,
|
|
847
896
|
project,
|
|
848
897
|
mountParent,
|
|
849
|
-
|
|
898
|
+
image,
|
|
850
899
|
};
|
|
851
900
|
}
|
|
852
901
|
if (project === undefined) {
|
|
@@ -859,7 +908,7 @@ export function parseLaunchArgs(args) {
|
|
|
859
908
|
machineExplicit: machineSet,
|
|
860
909
|
project: undefined,
|
|
861
910
|
mountParent,
|
|
862
|
-
|
|
911
|
+
image,
|
|
863
912
|
};
|
|
864
913
|
}
|
|
865
914
|
// pi mode: every token after the project is forwarded to pi verbatim.
|
|
@@ -871,7 +920,7 @@ export function parseLaunchArgs(args) {
|
|
|
871
920
|
machineExplicit: machineSet,
|
|
872
921
|
project,
|
|
873
922
|
mountParent,
|
|
874
|
-
|
|
923
|
+
image,
|
|
875
924
|
piArgs,
|
|
876
925
|
};
|
|
877
926
|
}
|
|
@@ -885,7 +934,7 @@ export function parseLaunchArgs(args) {
|
|
|
885
934
|
* - the two mounts <home>:/root and <projectsRoot>:/projects, always;
|
|
886
935
|
* - --mount adds EXACTLY <parent>:/work and re-roots cwd, nothing else;
|
|
887
936
|
* - --proxy <p> + exactly one --allow-direct <llm> (forced egress, fail-closed);
|
|
888
|
-
* - --rm
|
|
937
|
+
* - --rm on EVERY launch (throwaway always; ADR-0004).
|
|
889
938
|
*
|
|
890
939
|
* Throws AnonPiError (a plan is NEVER produced) when the image, the machine
|
|
891
940
|
* home, the proxy, or the direct-hole llm is missing.
|
|
@@ -938,9 +987,9 @@ export function resolveRunPlan(intent, homeFresh) {
|
|
|
938
987
|
// task; here the argv simply omits -it for the one headless shape.
|
|
939
988
|
const headless = mode === 'pi' && isHeadlessPiArgs(intent.piArgs);
|
|
940
989
|
const netcageArgs = ['run'];
|
|
941
|
-
//
|
|
942
|
-
|
|
943
|
-
|
|
990
|
+
// Throwaway ALWAYS: every launch is `--rm` (ADR-0004). Durable state is
|
|
991
|
+
// image-based (snapshot + a pinned machine), never an accreting container.
|
|
992
|
+
netcageArgs.push('--rm');
|
|
944
993
|
// Forced egress: the proxy + the ONE direct hole. Never omitted.
|
|
945
994
|
netcageArgs.push('--proxy', proxy, '--allow-direct', directTarget);
|
|
946
995
|
if (!headless)
|
|
@@ -1018,42 +1067,45 @@ function containerSeedThen(seedVersion, exec) {
|
|
|
1018
1067
|
`fi && ` +
|
|
1019
1068
|
`${exec}`);
|
|
1020
1069
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1039
|
-
* the cwd keeps the container identity aligned with the conversation the
|
|
1040
|
-
* kept container hosts.
|
|
1070
|
+
// --- The per-launch identity label (for `forward`/`ports`/`snapshot`) --------
|
|
1071
|
+
//
|
|
1072
|
+
// anon-pi stamps an identity key onto EVERY launch's container (an additive
|
|
1073
|
+
// netcage label; see withKeyLabel in cli.ts). It is NOT a kept-container match
|
|
1074
|
+
// key (there are no kept containers: every launch is throwaway, ADR-0004). Its
|
|
1075
|
+
// sole job is to let `forward`/`ports`/`snapshot` find a RUNNING container and
|
|
1076
|
+
// read back which machine + project it hosts (parseKeptKey -> keyProject),
|
|
1077
|
+
// while the container is up (the label goes away with the `--rm` container on
|
|
1078
|
+
// exit). netcage's `netcage.managed` label marks the container managed; this
|
|
1079
|
+
// adds anon-pi's own identity on top. anon-pi invents NO registry file.
|
|
1080
|
+
/**
|
|
1081
|
+
* PURE: the anon-pi launch-identity key stamped on EVERY (throwaway) launch,
|
|
1082
|
+
* derived from the (machine, projects-root, project) identity (ADR-0002's
|
|
1083
|
+
* cwd/project reasoning still underpins it). It is NOT a kept-container MATCH
|
|
1084
|
+
* key (every launch is throwaway; nothing is ever resumed). It exists ONLY so
|
|
1085
|
+
* `forward`/`ports`/`snapshot` can resolve a RUNNING container by machine +
|
|
1086
|
+
* project: the CLI stamps it onto a netcage label and reads it back with
|
|
1087
|
+
* parseKeptKey -> keyProject.
|
|
1041
1088
|
*
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1089
|
+
* The fields, and why each is retained:
|
|
1090
|
+
* - `machine.name`: the forward/ports filter scopes by machine.
|
|
1091
|
+
* - `cwd` (the resolved container cwd, via launchCwd): encodes the project
|
|
1092
|
+
* token (`/projects/<p>`, `/work/<p>`, or a root), so keyProject can name
|
|
1093
|
+
* the project a running container hosts.
|
|
1094
|
+
* - `projectsRoot` + `mountParent`: kept in the record for stability of the
|
|
1095
|
+
* decode shape (parseKeptKey reads them best-effort); no consumer filters on
|
|
1096
|
+
* them today, but they cost nothing and keep the label self-describing.
|
|
1046
1097
|
*
|
|
1047
|
-
*
|
|
1048
|
-
*
|
|
1049
|
-
* its internal shape is not a contract
|
|
1098
|
+
* Independent of the forced-egress inputs and forwarded pi args (identity only).
|
|
1099
|
+
* The key is a single opaque string (a `\n`-joined, field-tagged record) the CLI
|
|
1100
|
+
* stamps verbatim onto a netcage label; its internal shape is not a contract
|
|
1101
|
+
* (decode only with parseKeptKey).
|
|
1050
1102
|
*/
|
|
1051
|
-
export function
|
|
1103
|
+
export function launchIdentityKey(intent) {
|
|
1052
1104
|
const { machine, mode, projectsRoot, project, mountParent } = intent;
|
|
1053
1105
|
const mounted = nonEmpty(mountParent) !== undefined;
|
|
1054
1106
|
const rootKind = mounted ? 'mount' : 'projects';
|
|
1055
|
-
// The same cwd resolution resolveRunPlan uses, so
|
|
1056
|
-
//
|
|
1107
|
+
// The same cwd resolution resolveRunPlan uses, so keyProject names the exact
|
|
1108
|
+
// project the running container hosts (its conversation key).
|
|
1057
1109
|
const cwd = launchCwd(mode, rootKind, project);
|
|
1058
1110
|
return [
|
|
1059
1111
|
`machine=${machine.name}`,
|
|
@@ -1062,29 +1114,7 @@ export function keptContainerKey(intent) {
|
|
|
1062
1114
|
`cwd=${cwd}`,
|
|
1063
1115
|
].join('\n');
|
|
1064
1116
|
}
|
|
1065
|
-
/**
|
|
1066
|
-
* PURE: decide run-vs-start for a launch given a SUPPLIED listing of kept
|
|
1067
|
-
* `netcage.managed` containers (the CLI's netcage query result).
|
|
1068
|
-
*
|
|
1069
|
-
* - `--rm` (throwaway, `intent.keep !== true`): ALWAYS a fresh `run`. The
|
|
1070
|
-
* listing is NOT consulted (a throwaway launch never resumes a kept box).
|
|
1071
|
-
* - `--keep`: a kept container whose `key` equals this launch's
|
|
1072
|
-
* keptContainerKey is present -> `start` it (by its `ref`); else -> `run`
|
|
1073
|
-
* (resolveRunPlan leaves it kept because `--keep` omits `--rm`).
|
|
1074
|
-
*
|
|
1075
|
-
* Never spawns, never queries netcage: the listing is injected, so the whole
|
|
1076
|
-
* decision is a pure function of (intent, listing).
|
|
1077
|
-
*/
|
|
1078
|
-
export function resolveRunVsStart(intent, kept) {
|
|
1079
|
-
// Throwaway short-circuit: a `--rm` launch is always a fresh run and never
|
|
1080
|
-
// consults the listing (it must not resume a kept container).
|
|
1081
|
-
if (intent.keep !== true)
|
|
1082
|
-
return { action: 'run' };
|
|
1083
|
-
const want = keptContainerKey(intent);
|
|
1084
|
-
const match = kept.find((c) => c.key === want);
|
|
1085
|
-
return match ? { action: 'start', ref: match.ref } : { action: 'run' };
|
|
1086
|
-
}
|
|
1087
|
-
/** PURE: parse a stamped keptContainerKey back into its fields (best-effort). */
|
|
1117
|
+
/** PURE: parse a stamped launchIdentityKey back into its fields (best-effort). */
|
|
1088
1118
|
export function parseKeptKey(key) {
|
|
1089
1119
|
const out = {
|
|
1090
1120
|
machine: '',
|
|
@@ -1308,7 +1338,7 @@ export const ANON_PI_KEY_LABEL = 'anon-pi.key';
|
|
|
1308
1338
|
* ref: <Id>, name: <first Names entry or Id>}. When `runningOnly`, entries whose
|
|
1309
1339
|
* State is not "running" are dropped (forward/ports can only reach a live jail).
|
|
1310
1340
|
* The base64 DECODE of `key` is the CLI's job (Buffer), so this stays pure; the
|
|
1311
|
-
* caller decodes before matching against a
|
|
1341
|
+
* caller decodes before matching against a launchIdentityKey. [] on bad JSON.
|
|
1312
1342
|
*/
|
|
1313
1343
|
export function parseNetcagePsJson(stdout, opts = {}) {
|
|
1314
1344
|
let parsed;
|
|
@@ -2171,7 +2201,7 @@ export function parseMachineArgs(args) {
|
|
|
2171
2201
|
};
|
|
2172
2202
|
const verb = args[0];
|
|
2173
2203
|
if (verb === undefined) {
|
|
2174
|
-
fail('`machine` needs a subcommand: create | list | set-image | rm
|
|
2204
|
+
fail('`machine` needs a subcommand: create | list | set-image | rm');
|
|
2175
2205
|
}
|
|
2176
2206
|
const rest = args.slice(1);
|
|
2177
2207
|
if (verb === 'list') {
|
|
@@ -2245,15 +2275,37 @@ export function parseMachineArgs(args) {
|
|
|
2245
2275
|
fail('machine rm needs a <name>');
|
|
2246
2276
|
return { verb: 'rm', name: name, yes };
|
|
2247
2277
|
}
|
|
2278
|
+
return fail(`unknown machine subcommand: ${verb} (create | list | set-image | rm)`);
|
|
2279
|
+
}
|
|
2280
|
+
/**
|
|
2281
|
+
* PURE: parse the tokens AFTER `image` into an ImageCommand. Validates the image
|
|
2282
|
+
* name + the `-m` / `--create-machine` machine names via validateName (the
|
|
2283
|
+
* reserved-name / traversal guard), so the CLI only ever joins safe segments.
|
|
2284
|
+
* Throws AnonPiError (printed verbatim, exit 1) for an unknown/missing verb, a
|
|
2285
|
+
* missing or extra positional, an unknown flag, or a bad name.
|
|
2286
|
+
*
|
|
2287
|
+
* `<name>` is validated with the `machine` kind: it shares the same
|
|
2288
|
+
* folder-safe / reserved-name rules, and a snapshot name is an image-tag
|
|
2289
|
+
* segment (`anon-pi/<name>:latest`), so the same guard applies.
|
|
2290
|
+
*/
|
|
2291
|
+
export function parseImageArgs(args) {
|
|
2292
|
+
const fail = (msg) => {
|
|
2293
|
+
throw new AnonPiError(`anon-pi: ${msg}\nRun \`anon-pi image --help\` or \`anon-pi --help\`.`);
|
|
2294
|
+
};
|
|
2295
|
+
const verb = args[0];
|
|
2296
|
+
if (verb === undefined) {
|
|
2297
|
+
fail('`image` needs a subcommand: snapshot | list');
|
|
2298
|
+
}
|
|
2299
|
+
const rest = args.slice(1);
|
|
2300
|
+
if (verb === 'list') {
|
|
2301
|
+
if (rest.length > 0)
|
|
2302
|
+
fail(`image list takes no arguments, got: ${rest.join(' ')}`);
|
|
2303
|
+
return { verb: 'list' };
|
|
2304
|
+
}
|
|
2248
2305
|
if (verb === 'snapshot') {
|
|
2249
|
-
// snapshot <new-name> [-m <machine>] [--image-tag <ref>]: commit a RUNNING
|
|
2250
|
-
// container into a new image and create <new-name> pinned to it. The sole
|
|
2251
|
-
// positional is the new machine name; `-m` is an OPTIONAL filter (which
|
|
2252
|
-
// container when several are up), not a required source. The CLI auto-detects
|
|
2253
|
-
// the container (picker when several match).
|
|
2254
2306
|
let name;
|
|
2255
2307
|
let machine;
|
|
2256
|
-
let
|
|
2308
|
+
let createMachine;
|
|
2257
2309
|
for (let i = 0; i < rest.length; i++) {
|
|
2258
2310
|
const a = rest[i];
|
|
2259
2311
|
if (a === '-m' || a === '--machine') {
|
|
@@ -2263,41 +2315,81 @@ export function parseMachineArgs(args) {
|
|
|
2263
2315
|
machine = validateName(v, 'machine');
|
|
2264
2316
|
continue;
|
|
2265
2317
|
}
|
|
2266
|
-
if (a === '--
|
|
2318
|
+
if (a === '--create-machine') {
|
|
2267
2319
|
const v = rest[++i];
|
|
2268
2320
|
if (v === undefined)
|
|
2269
|
-
fail('--
|
|
2270
|
-
|
|
2321
|
+
fail('--create-machine needs a machine name');
|
|
2322
|
+
createMachine = validateName(v, 'machine');
|
|
2271
2323
|
continue;
|
|
2272
2324
|
}
|
|
2273
2325
|
if (a.startsWith('-'))
|
|
2274
2326
|
fail(`unknown option: ${a}`);
|
|
2275
2327
|
if (name !== undefined)
|
|
2276
|
-
fail(`
|
|
2328
|
+
fail(`image snapshot takes one <name>, got extra: ${a}`);
|
|
2277
2329
|
name = validateName(a, 'machine');
|
|
2278
2330
|
}
|
|
2279
2331
|
if (name === undefined)
|
|
2280
|
-
fail('
|
|
2332
|
+
fail('image snapshot needs a <name>');
|
|
2281
2333
|
return {
|
|
2282
2334
|
verb: 'snapshot',
|
|
2283
2335
|
name: name,
|
|
2284
2336
|
machine: nonEmpty(machine),
|
|
2285
|
-
|
|
2337
|
+
createMachine: nonEmpty(createMachine),
|
|
2286
2338
|
};
|
|
2287
2339
|
}
|
|
2288
|
-
return fail(`unknown
|
|
2289
|
-
}
|
|
2290
|
-
/**
|
|
2291
|
-
* PURE: the
|
|
2292
|
-
*
|
|
2293
|
-
*
|
|
2294
|
-
*
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2340
|
+
return fail(`unknown image subcommand: ${verb} (snapshot | list)`);
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* PURE: the clean image tag a `image snapshot <name>` writes:
|
|
2344
|
+
* `anon-pi/<name>:latest`. A same-name re-snapshot OVERWRITES this tag (that is
|
|
2345
|
+
* what `:latest` means); the previous image becomes dangling but keeps its
|
|
2346
|
+
* provenance label. The name is a validated image/machine name (a safe
|
|
2347
|
+
* image-path segment).
|
|
2348
|
+
*/
|
|
2349
|
+
export function snapshotImageTag(name) {
|
|
2350
|
+
return `anon-pi/${validateName(name, 'machine')}:latest`;
|
|
2351
|
+
}
|
|
2352
|
+
/** The podman/anon-pi provenance label keys baked into a snapshot image. */
|
|
2353
|
+
export const PROVENANCE_LABEL_SOURCE_MACHINE = 'anon-pi.source-machine';
|
|
2354
|
+
export const PROVENANCE_LABEL_SOURCE_IMAGE = 'anon-pi.source-image';
|
|
2355
|
+
export const PROVENANCE_LABEL_SNAPSHOT_AT = 'anon-pi.snapshot-at';
|
|
2356
|
+
/**
|
|
2357
|
+
* PURE: build the `LABEL k=v` change instructions a `netcage commit -c '…'`
|
|
2358
|
+
* bakes into a snapshot image (ADR-0003 §2). Provenance is best-effort HISTORY:
|
|
2359
|
+
* a label whose value is undefined/empty is OMITTED (a missing label beats a
|
|
2360
|
+
* wrong one). `at` is required (the snapshot time is always known). Each string
|
|
2361
|
+
* is ONE `LABEL key=value` instruction (the CLI passes each as a `-c` argv
|
|
2362
|
+
* element; podman round-trips `/` and `:` in the value un-quoted, verified).
|
|
2363
|
+
*/
|
|
2364
|
+
export function snapshotProvenanceLabels(args) {
|
|
2365
|
+
const labels = [];
|
|
2366
|
+
const push = (key, value) => {
|
|
2367
|
+
const v = nonEmpty(value);
|
|
2368
|
+
if (v !== undefined)
|
|
2369
|
+
labels.push(`LABEL ${key}=${v}`);
|
|
2370
|
+
};
|
|
2371
|
+
push(PROVENANCE_LABEL_SOURCE_MACHINE, args.sourceMachine);
|
|
2372
|
+
push(PROVENANCE_LABEL_SOURCE_IMAGE, args.sourceImage);
|
|
2373
|
+
push(PROVENANCE_LABEL_SNAPSHOT_AT, args.at);
|
|
2374
|
+
return labels;
|
|
2375
|
+
}
|
|
2376
|
+
/**
|
|
2377
|
+
* PURE: parse the anon-pi provenance labels read back off an image (the CLI
|
|
2378
|
+
* supplies the label map from `inspect --format '{{json .Config.Labels}}'`).
|
|
2379
|
+
* Returns only the anon-pi provenance fields (a missing/empty label => an
|
|
2380
|
+
* undefined field). Tolerant: any non-string / absent value is dropped, so a
|
|
2381
|
+
* hand-edited or partial label set never throws.
|
|
2382
|
+
*/
|
|
2383
|
+
export function parseImageProvenance(labels) {
|
|
2384
|
+
const get = (key) => {
|
|
2385
|
+
const v = labels?.[key];
|
|
2386
|
+
return typeof v === 'string' ? nonEmpty(v) : undefined;
|
|
2387
|
+
};
|
|
2388
|
+
return {
|
|
2389
|
+
sourceMachine: get(PROVENANCE_LABEL_SOURCE_MACHINE),
|
|
2390
|
+
sourceImage: get(PROVENANCE_LABEL_SOURCE_IMAGE),
|
|
2391
|
+
snapshotAt: get(PROVENANCE_LABEL_SNAPSHOT_AT),
|
|
2392
|
+
};
|
|
2301
2393
|
}
|
|
2302
2394
|
/**
|
|
2303
2395
|
* PURE: the JSON body a machine.json carries, given the pinned image (and an
|
|
@@ -2361,26 +2453,44 @@ USAGE
|
|
|
2361
2453
|
anon-pi forward [<p>] [--port …] open a host port onto a running container's in-jail server
|
|
2362
2454
|
anon-pi ports [<project>] list a running container's open in-jail TCP listeners
|
|
2363
2455
|
anon-pi -m <machine> [<p>] the same, on <machine> (its own image + home + conversations)
|
|
2456
|
+
anon-pi -i <ref> [<p>] run against <ref> for THIS launch only (also --image; ephemeral)
|
|
2364
2457
|
anon-pi --mount <parent> [<p>] root at a HOST parent folder instead of the projects root
|
|
2365
2458
|
anon-pi init onboard: verify your proxy, capture your local model, pick an image
|
|
2366
|
-
anon-pi machine … manage machines (create / list / set-image / rm
|
|
2459
|
+
anon-pi machine … manage machines (create / list / set-image / rm)
|
|
2460
|
+
anon-pi image … snapshot a running container into an image; list anon-pi images
|
|
2367
2461
|
anon-pi --delete-home [<m>] delete a machine's home (config + convos); keep its image pin + files
|
|
2368
2462
|
anon-pi --delete-project <p> delete a project's files + its per-machine sessions; keep the homes
|
|
2369
2463
|
|
|
2370
2464
|
<project> a folder under the projects root (mounted at ${CONTAINER_PROJECTS_ROOT}; pi's cwd). \`.\` means
|
|
2371
2465
|
the root itself (a scratch pi at ${CONTAINER_PROJECTS_ROOT}, ${CONTAINER_MOUNT_ROOT} for --mount, or ~).
|
|
2372
2466
|
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2467
|
+
-i <ref>, --image <ref> EPHEMERAL per-launch image override, highest priority
|
|
2468
|
+
(\`-i\` > the machine's machine.json image > ANON_PI_IMAGE). It picks the
|
|
2469
|
+
IMAGE for this launch only; \`-m\` picks the HOME, and the two compose.
|
|
2470
|
+
It NEVER changes machine.json (to re-pin a machine's image, use
|
|
2471
|
+
\`anon-pi machine set-image\` / \`machine create --image\`). No mismatch
|
|
2472
|
+
warning is printed. <ref> resolves in NETCAGE'S private image store
|
|
2473
|
+
(\`anon-pi/<name>:latest\` snapshots + \`init\`-built images live there),
|
|
2474
|
+
NOT your default podman store; anon-pi does NOT pre-check it and does
|
|
2475
|
+
NOT auto-pull (an anonymity tool must not silently fetch a remote
|
|
2476
|
+
image). A "not found" means the ref is not in netcage's store: snapshot
|
|
2477
|
+
it (\`anon-pi image snapshot <name>\`) or build it into that store.
|
|
2478
|
+
On a FRESH machine home \`-i\` is REFUSED (it would seed the home from
|
|
2479
|
+
the wrong image); establish the machine's image with
|
|
2480
|
+
\`anon-pi machine create <m> --image <ref>\` first.
|
|
2481
|
+
|
|
2482
|
+
Every launch is THROWAWAY: the container is removed on exit. To persist system
|
|
2483
|
+
state you built in a session, snapshot the running container into a named image
|
|
2484
|
+
(\`anon-pi image snapshot <name>\`) and pin a machine to it (\`anon-pi machine
|
|
2485
|
+
create <m> --image anon-pi/<name>:latest\`). Your pi config + conversations live
|
|
2486
|
+
in the machine home (a host mount) and persist regardless.
|
|
2377
2487
|
|
|
2378
2488
|
WHAT IT DOES
|
|
2379
2489
|
Runs pi inside netcage with all web/DNS egress forced through the socks5h proxy
|
|
2380
2490
|
(fail-closed) and ONE direct hole to your local model (ANON_PI_LLM). A MACHINE
|
|
2381
2491
|
is an image + a persistent HOST home (bind-mounted at ${CONTAINER_HOME_ROOT}) holding your pi
|
|
2382
|
-
config, extensions, and conversations; the container is disposable,
|
|
2383
|
-
loses nothing. Files (projects) are global by default; conversations are
|
|
2492
|
+
config, extensions, and conversations; the container is disposable (throwaway),
|
|
2493
|
+
so it loses nothing. Files (projects) are global by default; conversations are
|
|
2384
2494
|
per-machine. On a FRESH machine home the image's staged defaults + your
|
|
2385
2495
|
models.json are seeded in once; after that pi owns the home. Requires \`netcage\`.
|
|
2386
2496
|
|