@appforge-ci/admin-cli 0.3.13 → 0.3.15

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.
@@ -0,0 +1,272 @@
1
+ import "../test-support/home-sandbox.js";
2
+ import { test } from "node:test";
3
+ import assert from "node:assert/strict";
4
+ import { AGENT_DAEMON_PLIST, KCPASSWORD_PATH, LOGINWINDOW_PREFS, applyRefusals, applyVmUser, kcpasswordBytes, runnerLaunchAgentAccounts, helperAgentPlist, HELPER_AGENT_LABEL, } from "./vm-user-apply.js";
5
+ const SECRET = "S3cretRandomPassw0rdForTests1234"; // 32 chars; the real generator makes 31
6
+ function world(over = {}) {
7
+ return {
8
+ users: new Set(["tooming", "ci-runner"]),
9
+ autoLogin: "ci-runner",
10
+ plist: {},
11
+ files: {},
12
+ dirs: { "/Users": ["Shared", "tooming", "ci-runner"], "/Users/ci-runner": ["actions-runner"] },
13
+ sysadminctlWorks: true,
14
+ sudoUser: "tooming",
15
+ euid: 0,
16
+ agentPlistPresent: true,
17
+ fileVault: "FileVault is Off.\n",
18
+ battery: "Now drawing from 'AC Power'\n",
19
+ ...over,
20
+ };
21
+ }
22
+ function fake(w) {
23
+ const calls = [];
24
+ const logs = [];
25
+ const tried = [];
26
+ const secretCalls = [];
27
+ const written = [];
28
+ const ownedWrites = [];
29
+ const deps = {
30
+ userExists: (u) => w.users.has(u),
31
+ listUserUids: () => "tooming 501\nci-runner 502\n",
32
+ run: (cmd, args) => {
33
+ calls.push(`${cmd} ${args.join(" ")}`);
34
+ if (cmd === "defaults" && args[0] === "write" && args[2] === "autoLoginUser")
35
+ w.autoLogin = args[4];
36
+ if (cmd === "plutil" && args[0] === "-replace")
37
+ w.plist[args[1].replace("EnvironmentVariables.", "")] = args[3];
38
+ if (cmd === "sudo" && args[0] === "dscl" && args.includes("UniqueID"))
39
+ w.users.add(args[2].split("/")[2]);
40
+ },
41
+ tryRun: (cmd, args) => {
42
+ tried.push(`${cmd} ${args.join(" ")}`);
43
+ return true;
44
+ },
45
+ exists: () => false,
46
+ mkdirp: () => { },
47
+ log: (l) => logs.push(l),
48
+ arch: "arm64",
49
+ platform: "darwin",
50
+ capture: (cmd, args) => {
51
+ const key = `${cmd} ${args.join(" ")}`;
52
+ if (key === "/usr/bin/pmset -g batt")
53
+ return w.battery;
54
+ if (key === "/usr/bin/fdesetup status")
55
+ return w.fileVault;
56
+ if (cmd === "/usr/bin/defaults" && args[2] === "autoLoginUser")
57
+ return w.autoLogin ? `${w.autoLogin}\n` : null;
58
+ if (cmd === "/usr/bin/plutil" && args[0] === "-extract" && args[1] === "ProgramArguments.0")
59
+ return "/opt/homebrew/bin/node\n";
60
+ if (cmd === "/usr/bin/plutil" && args[0] === "-extract")
61
+ return w.plist[args[1].replace("EnvironmentVariables.", "")] ?? null;
62
+ return null;
63
+ },
64
+ euid: () => w.euid,
65
+ sudoUser: () => w.sudoUser,
66
+ readText: (p) => (p === AGENT_DAEMON_PLIST ? (w.agentPlistPresent ? "<plist/>" : null) : (w.files[p] ?? null)),
67
+ listDir: (p) => w.dirs[p] ?? null,
68
+ randomPassword: () => SECRET,
69
+ runSecret: (cmd, args, label) => {
70
+ secretCalls.push({ cmd, args, label });
71
+ if (cmd === "dscl")
72
+ w.users.add(args[2].split("/")[2]);
73
+ if (cmd === "sysadminctl") {
74
+ if (!w.sysadminctlWorks)
75
+ throw new Error("sysadminctl failed");
76
+ w.autoLogin = args[3];
77
+ }
78
+ },
79
+ writeSecretFile: (path, data) => void written.push({ path, data }),
80
+ writeOwnedText: (path, text, owner, group) => {
81
+ ownedWrites.push({ path, text, owner, group });
82
+ w.files[path] = text;
83
+ },
84
+ };
85
+ return { deps, calls, tried, logs, secretCalls, written, ownedWrites };
86
+ }
87
+ test("kcpasswordBytes is the fixed XOR encoding, zero-padded to a multiple of 12", () => {
88
+ assert.equal(kcpasswordBytes("abc").toString("hex"), "1ceb3123d2bcddeaa3b91f7d");
89
+ assert.equal(kcpasswordBytes("a".repeat(12)).length, 12);
90
+ assert.equal(kcpasswordBytes("a".repeat(31)).length, 36);
91
+ });
92
+ test("runnerLaunchAgentAccounts finds runners that are LaunchAgents, from each runner dir's .service file", () => {
93
+ const w = world({ files: { "/Users/ci-runner/actions-runner/.service": "/Users/ci-runner/Library/LaunchAgents/actions.runner.x.plist\n" } });
94
+ assert.deepEqual(runnerLaunchAgentAccounts(fake(w).deps), ["ci-runner"]);
95
+ const daemon = world({ files: { "/Users/ci-runner/actions-runner/.service": "/Library/LaunchDaemons/actions.runner.x.plist\n" } });
96
+ assert.deepEqual(runnerLaunchAgentAccounts(fake(daemon).deps), []);
97
+ });
98
+ test("refuses, naming each reason: not root, no agent account, agent not a daemon, a runner LaunchAgent that would be orphaned", () => {
99
+ const w = world({
100
+ euid: 501,
101
+ sudoUser: null,
102
+ agentPlistPresent: false,
103
+ files: { "/Users/ci-runner/actions-runner/.service": "/Users/ci-runner/Library/LaunchAgents/actions.runner.x.plist" },
104
+ });
105
+ const reasons = applyRefusals({ user: "appforge-vm" }, fake(w).deps).join(" | ");
106
+ assert.match(reasons, /has to run as root/);
107
+ assert.match(reasons, /cannot tell which account runs the agent/);
108
+ assert.match(reasons, /not installed as a boot-time LaunchDaemon/);
109
+ assert.match(reasons, /a GitHub Actions runner is a LaunchAgent of ci-runner/);
110
+ assert.match(reasons, /appforge-ci#780/);
111
+ const f = fake(w);
112
+ assert.throws(() => applyVmUser({ user: "appforge-vm" }, f.deps), /^Error: refused:/);
113
+ assert.deepEqual(f.calls, [], "a refusal changes nothing");
114
+ assert.deepEqual(f.secretCalls, []);
115
+ });
116
+ test("a laptop and FileVault on refuse, as the print-only command does", () => {
117
+ const reasons = applyRefusals({ user: "appforge-vm" }, fake(world({ fileVault: "FileVault is On.\n", battery: "Battery Power\n -InternalBattery-0" })).deps).join(" | ");
118
+ assert.match(reasons, /FileVault is on/);
119
+ assert.match(reasons, /has a battery/);
120
+ });
121
+ test("a first run creates the account, sets the random password and auto-login, makes the folder, sets the knobs and restarts the agent", () => {
122
+ const w = world();
123
+ const f = fake(w);
124
+ applyVmUser({ user: "appforge-vm" }, f.deps);
125
+ assert.ok(f.calls.some((c) => c.includes("dscl . -create /Users/appforge-vm UniqueID 503")), "the account is created");
126
+ assert.equal(f.secretCalls[0].cmd, "dscl");
127
+ assert.deepEqual(f.secretCalls[0].args, [".", "-passwd", "/Users/appforge-vm", SECRET]);
128
+ assert.equal(f.secretCalls[1].cmd, "sysadminctl");
129
+ assert.equal(w.autoLogin, "appforge-vm");
130
+ assert.deepEqual(f.written, [], "sysadminctl worked, so kcpassword is not written by hand");
131
+ assert.ok(f.calls.includes("install -d -o tooming -g appforge-vm -m 770 /Users/Shared/appforge"));
132
+ assert.equal(w.plist.APPFORGE_STATUS_MIRROR_DIR, "/Users/Shared/appforge");
133
+ assert.equal(w.plist.APPFORGE_VM_INVENTORY_FILE, "/Users/Shared/appforge/inventory.json");
134
+ assert.equal(w.plist.APPFORGE_VM_TOOLING_DIR, "/Users/Shared/appforge", "the agent is told to deliver the signed VM tooling there");
135
+ // launchd keeps the job it loaded, so a plist edit needs the job reloaded; kickstart -k would restart it with the OLD environment
136
+ assert.deepEqual(f.tried.filter((c) => c.startsWith("launchctl")), ["launchctl bootout system/com.appforge.agent", "launchctl bootstrap system /Library/LaunchDaemons/com.appforge.agent.plist"]);
137
+ assert.ok(!f.calls.some((c) => c.includes("kickstart")), "kickstart does not re-read the plist");
138
+ assert.match(f.logs.join("\n"), /Reboot to start the helper's session/);
139
+ assert.equal(f.ownedWrites.length, 1);
140
+ assert.equal(f.ownedWrites[0].path, `/Users/appforge-vm/Library/LaunchAgents/${HELPER_AGENT_LABEL}.plist`);
141
+ assert.equal(f.ownedWrites[0].owner, "appforge-vm", "the helper owns its own LaunchAgent so launchd loads it at its login");
142
+ assert.ok(f.calls.includes("install -d -o appforge-vm -g appforge-vm -m 755 /Users/appforge-vm/.appforge-infra"));
143
+ });
144
+ test("when the agent's job cannot be loaded again, the apply says the agent is NOT running and fails loudly", () => {
145
+ const w = world();
146
+ const f = fake(w);
147
+ let bootstraps = 0;
148
+ const realTry = f.deps.tryRun;
149
+ f.deps.tryRun = (cmd, args) => {
150
+ realTry(cmd, args);
151
+ if (cmd === "launchctl" && args[0] === "bootstrap") {
152
+ bootstraps++;
153
+ return false;
154
+ }
155
+ return true;
156
+ };
157
+ f.deps.run = (cmd, args) => {
158
+ if (cmd === "launchctl" && args[0] === "bootstrap")
159
+ throw new Error("Bootstrap failed: 5: Input/output error");
160
+ };
161
+ const deps = { ...f.deps, run: f.deps.run };
162
+ assert.throws(() => applyVmUser({ user: "appforge-vm", agentUser: "tooming" }, deps), /Bootstrap failed/);
163
+ assert.equal(bootstraps, 5, "it retried before giving up (bootout can still be settling)");
164
+ assert.match(f.logs.join("\n"), /the agent is NOT running: run `sudo launchctl bootstrap system \/Library\/LaunchDaemons\/com\.appforge\.agent\.plist`/);
165
+ });
166
+ test("the generated password is never echoed or logged: only the secret runner sees it, and its label hides it", () => {
167
+ const f = fake(world());
168
+ applyVmUser({ user: "appforge-vm" }, f.deps);
169
+ assert.equal(f.logs.join("\n").includes(SECRET), false);
170
+ assert.equal(f.calls.join("\n").includes(SECRET), false);
171
+ for (const s of f.secretCalls)
172
+ assert.equal(s.label.includes(SECRET), false);
173
+ assert.match(f.secretCalls[0].label, /<random password, not shown>/);
174
+ });
175
+ test("when sysadminctl cannot set auto-login, /etc/kcpassword and the loginwindow preference are written instead, and read back", () => {
176
+ const w = world({ sysadminctlWorks: false });
177
+ const f = fake(w);
178
+ applyVmUser({ user: "appforge-vm" }, f.deps);
179
+ assert.equal(f.written.length, 1);
180
+ assert.equal(f.written[0].path, KCPASSWORD_PATH);
181
+ assert.deepEqual(f.written[0].data, kcpasswordBytes(SECRET));
182
+ assert.ok(f.calls.includes(`defaults write ${LOGINWINDOW_PREFS} autoLoginUser -string appforge-vm`));
183
+ assert.equal(w.autoLogin, "appforge-vm");
184
+ assert.match(f.logs.join("\n"), /writing \/etc\/kcpassword/);
185
+ });
186
+ test("if auto-login still does not read back as the helper, the run fails loudly instead of claiming success", () => {
187
+ const w = world({ sysadminctlWorks: false });
188
+ const f = fake(w);
189
+ f.deps.run = (cmd, args) => void f.calls.push(`${cmd} ${args.join(" ")}`); // the preference write is ignored
190
+ assert.throws(() => applyVmUser({ user: "appforge-vm" }, f.deps), /auto-login for 'appforge-vm' did not stick/);
191
+ });
192
+ test("re-running when everything is already true changes nothing and never touches the password", () => {
193
+ const w = world({
194
+ users: new Set(["tooming", "ci-runner", "appforge-vm"]),
195
+ autoLogin: "appforge-vm",
196
+ plist: { APPFORGE_STATUS_MIRROR_DIR: "/Users/Shared/appforge", APPFORGE_VM_INVENTORY_FILE: "/Users/Shared/appforge/inventory.json", APPFORGE_VM_TOOLING_DIR: "/Users/Shared/appforge" },
197
+ });
198
+ w.files[`/Users/appforge-vm/Library/LaunchAgents/${HELPER_AGENT_LABEL}.plist`] = helperAgentPlist({ shareDir: "/Users/Shared/appforge", nodePath: "/opt/homebrew/bin/node", home: "/Users/appforge-vm" });
199
+ const f = fake(w);
200
+ applyVmUser({ user: "appforge-vm" }, f.deps);
201
+ assert.deepEqual(f.secretCalls, [], "no password is set for an existing account");
202
+ assert.deepEqual(f.ownedWrites, [], "an identical LaunchAgent is not rewritten");
203
+ assert.deepEqual(f.written, []);
204
+ assert.equal(f.calls.some((c) => c.includes("-create") || c.startsWith("plutil") || c.startsWith("launchctl")), false);
205
+ assert.match(f.logs.join("\n"), /already the auto-login account/);
206
+ assert.match(f.logs.join("\n"), /already set/);
207
+ });
208
+ test("an existing account that is not the auto-login account is left alone and the run warns, unless --reset-password", () => {
209
+ const w = world({ users: new Set(["tooming", "ci-runner", "appforge-vm"]) });
210
+ const f = fake(w);
211
+ applyVmUser({ user: "appforge-vm" }, f.deps);
212
+ assert.deepEqual(f.secretCalls, []);
213
+ assert.equal(w.autoLogin, "ci-runner");
214
+ assert.match(f.logs.join("\n"), /auto-login is NOT set/);
215
+ assert.match(f.logs.join("\n"), /WARNING: 'appforge-vm' is NOT the auto-login account/);
216
+ const w2 = world({ users: new Set(["tooming", "ci-runner", "appforge-vm"]) });
217
+ const f2 = fake(w2);
218
+ applyVmUser({ user: "appforge-vm", resetPassword: true }, f2.deps);
219
+ assert.equal(f2.secretCalls[0].cmd, "dscl");
220
+ assert.equal(w2.autoLogin, "appforge-vm");
221
+ });
222
+ test("--dry-run prints every step and changes nothing", () => {
223
+ const w = world();
224
+ const f = fake(w);
225
+ applyVmUser({ user: "appforge-vm", dryRun: true }, f.deps);
226
+ assert.deepEqual(f.calls, []);
227
+ assert.deepEqual(f.secretCalls, []);
228
+ assert.deepEqual(f.written, []);
229
+ assert.equal(w.autoLogin, "ci-runner");
230
+ const out = f.logs.join("\n");
231
+ assert.match(out, /DRY RUN: nothing will be changed/);
232
+ assert.match(out, /\[1\/5\] create the 'appforge-vm' account/);
233
+ assert.match(out, /\[4\/5\] add the mirror knobs/);
234
+ assert.match(out, /Dry run finished/);
235
+ });
236
+ test("the agent account comes from --agent-user when given, else from the sudo user", () => {
237
+ const f = fake(world({ sudoUser: null }));
238
+ applyVmUser({ user: "appforge-vm", agentUser: "tooming" }, f.deps);
239
+ assert.ok(f.calls.includes("install -d -o tooming -g appforge-vm -m 770 /Users/Shared/appforge"));
240
+ });
241
+ test("the helper's LaunchAgent runs the delivered bundle with an auto host name, the agent's node, and the shared-folder wiring", () => {
242
+ const plist = helperAgentPlist({ shareDir: "/Users/Shared/appforge", nodePath: "/opt/homebrew/bin/node", home: "/Users/appforge-vm" });
243
+ assert.match(plist, /<string>com\.appforge\.vm-reconcile<\/string>/);
244
+ assert.match(plist, /\[ -f "\$0" \] \|\| exit 0; exec \/bin\/bash "\$0" "\$1" auto/, "quiet until the first bundle is delivered, host derived on the Mac");
245
+ assert.match(plist, /<string>\/Users\/Shared\/appforge\/vm-tooling\/current\/reconcile-bundle\.sh<\/string>/);
246
+ assert.match(plist, /<key>APPFORGE_NODE<\/key>\s*<string>\/opt\/homebrew\/bin\/node<\/string>/);
247
+ assert.match(plist, /<key>APPFORGE_AGENT_STATUS<\/key>\s*<string>\/Users\/Shared\/appforge\/status\.json<\/string>/);
248
+ assert.match(plist, /<key>APPFORGE_INVENTORY_MIRROR_DIR<\/key>\s*<string>\/Users\/Shared\/appforge<\/string>/);
249
+ assert.match(plist, /<key>StartInterval<\/key>\s*<integer>300<\/integer>/);
250
+ assert.match(plist, /<string>\/Users\/appforge-vm\/\.appforge-infra\/reconcile\.log<\/string>/);
251
+ assert.equal(plist.includes("git"), false, "no git and no credential anywhere in it");
252
+ assert.match(helperAgentPlist({ shareDir: "/Users/Shared/a&b", nodePath: "/n<ode", home: "/Users/x" }), /a&amp;b[\s\S]*\/n&lt;ode/, "paths are XML-escaped");
253
+ });
254
+ test("the LaunchAgent is installed on a first run, using the node found in the agent's own LaunchDaemon, and a changed definition is rewritten", () => {
255
+ const w = world();
256
+ const f = fake(w);
257
+ applyVmUser({ user: "appforge-vm" }, f.deps);
258
+ assert.match(f.ownedWrites[0].text, /<key>APPFORGE_NODE<\/key>\s*<string>\/opt\/homebrew\/bin\/node<\/string>/);
259
+ // a stale definition on disk is replaced
260
+ const w2 = world({ users: new Set(["tooming", "ci-runner", "appforge-vm"]), autoLogin: "appforge-vm" });
261
+ w2.files[`/Users/appforge-vm/Library/LaunchAgents/${HELPER_AGENT_LABEL}.plist`] = "<plist>old</plist>";
262
+ const f2 = fake(w2);
263
+ applyVmUser({ user: "appforge-vm" }, f2.deps);
264
+ assert.equal(f2.ownedWrites.length, 1);
265
+ });
266
+ test("--dry-run writes no LaunchAgent either", () => {
267
+ const f = fake(world());
268
+ applyVmUser({ user: "appforge-vm", dryRun: true }, f.deps);
269
+ assert.deepEqual(f.ownedWrites, []);
270
+ assert.match(f.logs.join("\n"), /\[5\/5\] install the helper's reconcile LaunchAgent/);
271
+ });
272
+ //# sourceMappingURL=vm-user-apply.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vm-user-apply.test.js","sourceRoot":"","sources":["../../src/lib/vm-user-apply.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import { type InstallRunnerUserDeps } from "./runner-user.js";
2
+ /**
3
+ * Creates the dedicated auto-login "helper" account that holds a Mac's graphical session and runs the VM tooling
4
+ * (Tart, vm-reconcile, xcode-image), and prints the steps that finish the job. Run ON the target Mac, by the operator,
5
+ * in their own terminal (`appforge-admin agents install-vm-user`).
6
+ *
7
+ * Why. A macOS Tart guest only starts for a user with a graphical session, and after an unattended reboot only the
8
+ * auto-login account has one (appforge-ci#812; docs/fleet-management.md, "macOS guests need a graphical session").
9
+ * On a Mac that shares the box with a GitHub Actions runner, option C of that issue puts the VM tooling in a third
10
+ * account, so VM disks are out of reach of CI jobs and of the agent's own builds.
11
+ *
12
+ * What it does, idempotently: refuses on a laptop or while FileVault is on (auto-login cannot work), then creates an
13
+ * ordinary account with no password, and prints the next steps.
14
+ *
15
+ * What it deliberately does NOT do: set a password, enable auto-login, or install any LaunchAgent. Auto-login stores
16
+ * the account's password on this Mac and decides who is logged in at the console, a call about this Mac's physical
17
+ * security that the operator (or their MDM) makes, so the command prints the steps and changes nothing else.
18
+ */
19
+ export declare const DEFAULT_VM_USER = "appforge-vm";
20
+ export interface InstallVmUserDeps extends InstallRunnerUserDeps {
21
+ platform: string;
22
+ /** stdout of a read-only command, or null when it could not run. */
23
+ capture(cmd: string, args: string[]): string | null;
24
+ }
25
+ export declare const realInstallVmUserDeps: InstallVmUserDeps;
26
+ /** The reasons this Mac cannot use an auto-login helper account; empty means go ahead. Fails closed: an unreadable answer refuses. */
27
+ export declare function vmUserRefusals(deps: Pick<InstallVmUserDeps, "platform" | "capture">): string[];
28
+ /** Where the agent mirrors its status.json for the helper account (group-readable; made in step 2 below). */
29
+ export declare const STATUS_SHARE_DIR = "/Users/Shared/appforge";
30
+ /** The steps the operator does next, as printable text. Pure, so a test can read exactly what will be shown. */
31
+ export declare function vmUserNextSteps(user: string, agentUser?: string): string;
32
+ export declare function installVmUser(opts?: {
33
+ user?: string;
34
+ }, deps?: InstallVmUserDeps): void;
35
+ //# sourceMappingURL=vm-user.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vm-user.d.ts","sourceRoot":"","sources":["../../src/lib/vm-user.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiD,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAE7G;;;;;;;;;;;;;;;;GAgBG;AAEH,eAAO,MAAM,eAAe,gBAAgB,CAAC;AAE7C,MAAM,WAAW,iBAAkB,SAAQ,qBAAqB;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAAC;CACrD;AAED,eAAO,MAAM,qBAAqB,EAAE,iBAUnC,CAAC;AAEF,sIAAsI;AACtI,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,iBAAiB,EAAE,UAAU,GAAG,SAAS,CAAC,GAAG,MAAM,EAAE,CAW9F;AAED,6GAA6G;AAC7G,eAAO,MAAM,gBAAgB,2BAA2B,CAAC;AAEzD,gHAAgH;AAChH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,MAA4B,GAAG,MAAM,CAiC7F;AAED,wBAAgB,aAAa,CAAC,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,EAAE,IAAI,GAAE,iBAAyC,GAAG,IAAI,CAajH"}
@@ -0,0 +1,99 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { userInfo } from "node:os";
3
+ import { ensureOrdinaryUser, realInstallRunnerUserDeps } from "./runner-user.js";
4
+ /**
5
+ * Creates the dedicated auto-login "helper" account that holds a Mac's graphical session and runs the VM tooling
6
+ * (Tart, vm-reconcile, xcode-image), and prints the steps that finish the job. Run ON the target Mac, by the operator,
7
+ * in their own terminal (`appforge-admin agents install-vm-user`).
8
+ *
9
+ * Why. A macOS Tart guest only starts for a user with a graphical session, and after an unattended reboot only the
10
+ * auto-login account has one (appforge-ci#812; docs/fleet-management.md, "macOS guests need a graphical session").
11
+ * On a Mac that shares the box with a GitHub Actions runner, option C of that issue puts the VM tooling in a third
12
+ * account, so VM disks are out of reach of CI jobs and of the agent's own builds.
13
+ *
14
+ * What it does, idempotently: refuses on a laptop or while FileVault is on (auto-login cannot work), then creates an
15
+ * ordinary account with no password, and prints the next steps.
16
+ *
17
+ * What it deliberately does NOT do: set a password, enable auto-login, or install any LaunchAgent. Auto-login stores
18
+ * the account's password on this Mac and decides who is logged in at the console, a call about this Mac's physical
19
+ * security that the operator (or their MDM) makes, so the command prints the steps and changes nothing else.
20
+ */
21
+ export const DEFAULT_VM_USER = "appforge-vm";
22
+ export const realInstallVmUserDeps = {
23
+ ...realInstallRunnerUserDeps,
24
+ platform: process.platform,
25
+ capture(cmd, args) {
26
+ try {
27
+ return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], cwd: "/" });
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ },
33
+ };
34
+ /** The reasons this Mac cannot use an auto-login helper account; empty means go ahead. Fails closed: an unreadable answer refuses. */
35
+ export function vmUserRefusals(deps) {
36
+ if (deps.platform !== "darwin")
37
+ return [`this is not macOS (${deps.platform}): the helper account is a macOS auto-login account`];
38
+ const reasons = [];
39
+ const batt = deps.capture("/usr/bin/pmset", ["-g", "batt"]);
40
+ if (batt === null)
41
+ reasons.push("could not read the power source (pmset -g batt), so this cannot be shown not to be a laptop");
42
+ else if (/InternalBattery/.test(batt))
43
+ reasons.push("this Mac has a battery (a laptop): auto-login on a laptop is not a good idea, use a Mac mini");
44
+ const fv = deps.capture("/usr/bin/fdesetup", ["status"]);
45
+ if (fv === null)
46
+ reasons.push("could not read the FileVault state (fdesetup status)");
47
+ else if (/FileVault is On/i.test(fv))
48
+ reasons.push("FileVault is on: macOS cannot log an account in automatically while it is");
49
+ else if (!/FileVault is Off/i.test(fv))
50
+ reasons.push(`unrecognised FileVault state: ${fv.trim().slice(0, 80)}`);
51
+ return reasons;
52
+ }
53
+ /** Where the agent mirrors its status.json for the helper account (group-readable; made in step 2 below). */
54
+ export const STATUS_SHARE_DIR = "/Users/Shared/appforge";
55
+ /** The steps the operator does next, as printable text. Pure, so a test can read exactly what will be shown. */
56
+ export function vmUserNextSteps(user, agentUser = userInfo().username) {
57
+ const home = `/Users/${user}`;
58
+ return [
59
+ `Next steps. Nothing below was done for you: this print-only form is the manual path for one Mac.`,
60
+ `On many Macs, do steps 1, 2, 3 and 5 in one go with: sudo appforge-admin agents install-vm-user --apply --agent-user ${agentUser}`,
61
+ `(non-interactive and idempotent, so an MDM script or Ansible can run it; --dry-run shows what it would do).`,
62
+ ``,
63
+ ` 1. Give '${user}' a password (it has none yet, so it cannot log in):`,
64
+ ` sudo dscl . -passwd ${home}`,
65
+ ` 2. Make a folder both accounts use: the agent writes its status there ('${user}' needs it to know when the agent is busy,`,
66
+ ` and ~/.appforge-agent is private to the agent's own account), and '${user}' writes its VM and image lists there so`,
67
+ ` the agent can report them to Fleet (the agent's own \`tart list\` cannot see a store in '${user}'). Group-writable for that:`,
68
+ ` sudo install -d -o ${agentUser} -g ${user} -m 770 ${STATUS_SHARE_DIR}`,
69
+ ` 3. Tell the agent to write its status there and to report the helper's inventory, by re-installing its service from a`,
70
+ ` shell that exports the knobs it already has (APPFORGE_MAX_CONCURRENT_BUILDS, APPFORGE_SANDBOX_MONITOR=1, ...) plus:`,
71
+ ` APPFORGE_STATUS_MIRROR_DIR=${STATUS_SHARE_DIR} APPFORGE_VM_INVENTORY_FILE=${STATUS_SHARE_DIR}/inventory.json \\`,
72
+ ` APPFORGE_VM_TOOLING_DIR=${STATUS_SHARE_DIR} \\`,
73
+ ` appforge-agent install-service --daemon`,
74
+ ` 4. Make '${user}' the auto-login account: System Settings > Users & Groups > "Automatically log in as" > ${user}`,
75
+ ` (FileVault is off, as this needs), or push the same setting with your MDM.`,
76
+ ` 5. Reboot, let it log in by itself, then check the layout:`,
77
+ ` appforge-agent doctor (macos-guest-session should say "the helper-account layout")`,
78
+ ` 6. Start the VM tooling in that session, as a LaunchAgent of '${user}': follow infra/vms/README.md, "Running it on a Mac",`,
79
+ ` as that user (sudo -u ${user} ...), passing --agent-status ${STATUS_SHARE_DIR}/status.json --inventory-dir ${STATUS_SHARE_DIR}`,
80
+ ` to install-launchd.sh, then load it`,
81
+ ` into its graphical session:`,
82
+ ` sudo launchctl bootstrap gui/$(id -u ${user}) ${home}/Library/LaunchAgents/com.appforge.vm-reconcile.plist`,
83
+ ``,
84
+ `Heads-up: only ONE account auto-logs in. If this Mac also hosts a GitHub Actions runner as a LaunchAgent (svc.sh installs one),`,
85
+ `that runner will not come back after the reboot in step 5 until it runs as a boot-time daemon instead (a follow-up for`,
86
+ `appforge-ci#812). 'appforge-agent doctor' shows it as runner-session. Until then macOS guests cannot start after a reboot,`,
87
+ `so do not do step 4 on a Mac whose runner you cannot afford to lose.`,
88
+ ].join("\n");
89
+ }
90
+ export function installVmUser(opts = {}, deps = realInstallVmUserDeps) {
91
+ const user = opts.user ?? DEFAULT_VM_USER;
92
+ const refusals = vmUserRefusals(deps);
93
+ if (refusals.length > 0) {
94
+ throw new Error(`refused: ${refusals.join("; ")}`);
95
+ }
96
+ ensureOrdinaryUser(user, "AppForge VM helper (dedicated)", `Creating the dedicated '${user}' account (it will hold this Mac's graphical session and run the VM tooling)`, deps);
97
+ deps.log(`\n${vmUserNextSteps(user)}`);
98
+ }
99
+ //# sourceMappingURL=vm-user.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vm-user.js","sourceRoot":"","sources":["../../src/lib/vm-user.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import "../test-support/home-sandbox.js";
2
+ //# sourceMappingURL=vm-user.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vm-user.test.d.ts","sourceRoot":"","sources":["../../src/lib/vm-user.test.ts"],"names":[],"mappings":"AAAA,OAAO,iCAAiC,CAAC"}
@@ -0,0 +1,75 @@
1
+ import "../test-support/home-sandbox.js";
2
+ import { test } from "node:test";
3
+ import assert from "node:assert/strict";
4
+ import { DEFAULT_VM_USER, installVmUser, vmUserNextSteps, vmUserRefusals } from "./vm-user.js";
5
+ const BATT_MINI = "Now drawing from 'AC Power'\n";
6
+ const BATT_LAPTOP = "Now drawing from 'Battery Power'\n -InternalBattery-0 (id=1) 80%; discharging\n";
7
+ function fakeDeps(over = {}) {
8
+ const calls = [];
9
+ const logs = [];
10
+ const answers = { "/usr/bin/pmset -g batt": BATT_MINI, "/usr/bin/fdesetup status": "FileVault is Off.\n", ...(over.answers ?? {}) };
11
+ const deps = {
12
+ userExists: () => false,
13
+ listUserUids: () => "someone 501\nci-runner 502\n",
14
+ run: (cmd, args) => calls.push({ cmd, args }),
15
+ tryRun: () => true,
16
+ exists: () => false,
17
+ mkdirp: () => { },
18
+ log: (l) => logs.push(l),
19
+ arch: "arm64",
20
+ platform: "darwin",
21
+ capture: (cmd, args) => answers[[cmd, ...args].join(" ")] ?? null,
22
+ ...over,
23
+ };
24
+ return { deps, calls, logs };
25
+ }
26
+ test("a Mac mini with FileVault off has nothing refusing it", () => {
27
+ assert.deepEqual(vmUserRefusals(fakeDeps().deps), []);
28
+ });
29
+ test("a laptop, FileVault on, a non-macOS host and unreadable answers all refuse (fail closed)", () => {
30
+ assert.match(vmUserRefusals(fakeDeps({ answers: { "/usr/bin/pmset -g batt": BATT_LAPTOP } }).deps).join(";"), /has a battery/);
31
+ assert.match(vmUserRefusals(fakeDeps({ answers: { "/usr/bin/fdesetup status": "FileVault is On.\n" } }).deps).join(";"), /FileVault is on/);
32
+ assert.match(vmUserRefusals(fakeDeps({ platform: "linux" }).deps).join(";"), /not macOS/);
33
+ assert.match(vmUserRefusals(fakeDeps({ answers: { "/usr/bin/pmset -g batt": null } }).deps).join(";"), /could not read the power source/);
34
+ assert.match(vmUserRefusals(fakeDeps({ answers: { "/usr/bin/fdesetup status": null } }).deps).join(";"), /could not read the FileVault state/);
35
+ assert.match(vmUserRefusals(fakeDeps({ answers: { "/usr/bin/fdesetup status": "weird\n" } }).deps).join(";"), /unrecognised FileVault state/);
36
+ });
37
+ test("a refusal throws before anything is created", () => {
38
+ const { deps, calls } = fakeDeps({ answers: { "/usr/bin/fdesetup status": "FileVault is On.\n" } });
39
+ assert.throws(() => installVmUser({}, deps), /refused: FileVault is on/);
40
+ assert.deepEqual(calls, [], "no command ran");
41
+ });
42
+ test("creates the default account the way install-runner-user does, then prints the steps and runs nothing else", () => {
43
+ const { deps, calls, logs } = fakeDeps();
44
+ installVmUser({}, deps);
45
+ const joined = calls.map((c) => `${c.cmd} ${c.args.join(" ")}`);
46
+ assert.ok(joined.includes(`sudo dscl . -create /Users/${DEFAULT_VM_USER} UniqueID 503`), "next free uid after 502");
47
+ assert.ok(joined.includes(`sudo dscl . -create /Users/${DEFAULT_VM_USER} RealName AppForge VM helper (dedicated)`));
48
+ assert.ok(joined.includes(`sudo createhomedir -c -u ${DEFAULT_VM_USER}`));
49
+ assert.ok(!joined.some((c) => /IsHidden|-passwd|autologin|kcpassword|launchctl|defaults write|sysadminctl/.test(c)), `only account creation ran: ${joined.join(" | ")}`);
50
+ assert.match(logs.join("\n"), /Next steps\. Nothing below was done for you/);
51
+ });
52
+ test("an account that already exists is left alone, and the steps are still printed", () => {
53
+ const { deps, calls, logs } = fakeDeps({ userExists: () => true });
54
+ installVmUser({ user: "vmhelper" }, deps);
55
+ assert.deepEqual(calls, []);
56
+ assert.match(logs.join("\n"), /user 'vmhelper' already exists, leaving it alone/);
57
+ assert.match(logs.join("\n"), /sudo dscl \. -passwd \/Users\/vmhelper/);
58
+ });
59
+ test("the printed steps cover password, the shared status folder, the agent mirror, auto-login, the reboot check, the LaunchAgent and the runner heads-up", () => {
60
+ const text = vmUserNextSteps("appforge-vm", "tooming");
61
+ assert.match(text, /sudo dscl \. -passwd \/Users\/appforge-vm/);
62
+ assert.match(text, /sudo install -d -o tooming -g appforge-vm -m 770 \/Users\/Shared\/appforge/, "group-writable: the helper publishes its inventory there");
63
+ assert.match(text, /APPFORGE_VM_INVENTORY_FILE=\/Users\/Shared\/appforge\/inventory\.json/);
64
+ assert.match(text, /--inventory-dir \/Users\/Shared\/appforge/);
65
+ assert.match(text, /APPFORGE_STATUS_MIRROR_DIR=\/Users\/Shared\/appforge APPFORGE_VM_INVENTORY_FILE=\/Users\/Shared\/appforge\/inventory\.json \\\n\s+APPFORGE_VM_TOOLING_DIR=\/Users\/Shared\/appforge \\\n\s+appforge-agent install-service --daemon/);
66
+ assert.match(text, /Automatically log in as" > appforge-vm/);
67
+ assert.match(text, /appforge-agent doctor/);
68
+ assert.match(text, /helper-account layout/);
69
+ assert.match(text, /--agent-status \/Users\/Shared\/appforge\/status\.json/);
70
+ assert.match(text, /launchctl bootstrap gui\/\$\(id -u appforge-vm\) \/Users\/appforge-vm\/Library\/LaunchAgents\/com\.appforge\.vm-reconcile\.plist/);
71
+ assert.match(text, /only ONE account auto-logs in/i);
72
+ // the order matters: the folder exists before the agent is told to write there, auto-login comes after both
73
+ assert.ok(text.indexOf("sudo install -d") < text.indexOf("APPFORGE_STATUS_MIRROR_DIR") && text.indexOf("APPFORGE_STATUS_MIRROR_DIR") < text.indexOf("Automatically log in as"));
74
+ });
75
+ //# sourceMappingURL=vm-user.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vm-user.test.js","sourceRoot":"","sources":["../../src/lib/vm-user.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appforge-ci/admin-cli",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -15,8 +15,8 @@
15
15
  "dependencies": {
16
16
  "chalk": "^5.3.0",
17
17
  "commander": "^12.1.0",
18
- "@appforge-ci/agent": "0.3.13",
19
- "@appforge-ci/core": "0.3.13"
18
+ "@appforge-ci/agent": "0.3.15",
19
+ "@appforge-ci/core": "0.3.15"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^20.16.5",