@zswarm/core 0.2.0 → 0.2.2

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 CHANGED
@@ -10,7 +10,7 @@ the CLI flags, and the CLI help, so they cannot drift.
10
10
  npm install @zswarm/core
11
11
  ```
12
12
 
13
- From a checkout of this repo:
13
+ From a checkout of this repo (build first with `pnpm build`):
14
14
 
15
15
  ```bash
16
16
  npm install ./packages/core
@@ -36,11 +36,11 @@ SSH LocalForward and calls `probeServe` before ops (including before reuse);
36
36
  pass a `serveTunnels` manager if the caller (MCP) should reuse it. Omitting
37
37
  the SSH port leaves `Port` to OpenSSH instead of forcing 22. `tcp://` also
38
38
  names a private Tailscale Serve frontend over a loopback backend; see
39
- [Tailscale crew](../../docs/tailscale.md).
39
+ [Tailscale crew](https://github.com/brandonkramer/zswarm/blob/master/docs/tailscale.md).
40
40
 
41
- Spawn lifecycle, body files, guarded keys, status tabs and routing context: [crew operations](../../docs/crew-operations.md).
41
+ Spawn lifecycle, body files, guarded keys, status tabs and routing context: [crew operations](https://github.com/brandonkramer/zswarm/blob/master/docs/crew-operations.md).
42
42
 
43
- Ops, env, and the event bus: the [zswarm README](../../README.md).
43
+ Ops, env, and the event bus: the [zswarm README](https://github.com/brandonkramer/zswarm#readme).
44
44
 
45
45
  ## License
46
46
 
package/dist/host/cli.js CHANGED
@@ -183,7 +183,7 @@ const COMMANDS = {
183
183
  "zswarm host install --ssh DEST [--sudo] [--prefix /usr/local/lib/zswarm] [--bin-dir /usr/local/bin]",
184
184
  " [--node V] [--zellij V] [--zswarm V] Node.js, zswarm and Zellij on a Linux/macOS host",
185
185
  "zswarm host install --script [same options] print the script (run it as: sh -s -- <args printed on stderr>)",
186
- "zswarm host doctor [--ssh DEST] [--serve TARGET] [--session S] tools, disk, services, serve, session, panes",
186
+ "zswarm host doctor (--ssh DEST | --serve TARGET) [--session S] tools, disk, services, serve, session, panes",
187
187
  ].join("\n"),
188
188
  run: host,
189
189
  },
@@ -293,6 +293,7 @@ export function launchRelay(dir, env = process.env) {
293
293
  "submit", "-l", label, "-o", log, "-e", log, "--",
294
294
  "/usr/bin/env", `PATH=${env.PATH ?? "/usr/bin:/bin"}`, `HOME=${env.HOME ?? ""}`,
295
295
  ...(env.ZSWARM_STATE_DIR ? [`ZSWARM_STATE_DIR=${env.ZSWARM_STATE_DIR}`] : []),
296
+ ...(env.ZSWARM_RELAY_DELAYS ? [`ZSWARM_RELAY_DELAYS=${env.ZSWARM_RELAY_DELAYS}`] : []),
296
297
  `ZSWARM_RELAY_LABEL=${label}`, ...argv,
297
298
  ], { encoding: "utf8" });
298
299
  if (res.status !== 0)
@@ -304,6 +305,7 @@ export function launchRelay(dir, env = process.env) {
304
305
  const res = spawnSync("systemd-run", ["--user", "--collect", "--quiet", `--unit=${unit}`,
305
306
  `--setenv=PATH=${env.PATH ?? "/usr/bin:/bin"}`,
306
307
  ...(env.ZSWARM_STATE_DIR ? [`--setenv=ZSWARM_STATE_DIR=${env.ZSWARM_STATE_DIR}`] : []),
308
+ ...(env.ZSWARM_RELAY_DELAYS ? [`--setenv=ZSWARM_RELAY_DELAYS=${env.ZSWARM_RELAY_DELAYS}`] : []),
307
309
  ...argv], { encoding: "utf8" });
308
310
  if (res.status === 0)
309
311
  return `systemd unit ${unit}`;
@@ -66,14 +66,15 @@ export declare const defaultRunner: Runner;
66
66
  /** The account services are installed for: the current user, or `runAs` when root. */
67
67
  export declare function serviceAccount(runAs?: string, run?: Runner): ServiceAccount;
68
68
  /**
69
- * Run fs work in another account's home as that account. Root installing
70
- * with --run-as drops its effective uid and gid for the duration, so the
69
+ * The argv prefix that runs a command as the account: `runuser` on Linux,
70
+ * `sudo -u` on macOS. Root writes into the account's home through it, so the
71
71
  * kernel refuses what the account itself could not do (a symlink it planted
72
- * towards a root-owned file, say) and new files are the account's own. The
73
- * work must be synchronous: the effective ids are process-wide.
72
+ * towards a root-owned file, say) and new files are the account's own. (Not
73
+ * seteuid in this process: Node disables it on Linux while io_uring may be
74
+ * enabled, CVE-2024-22017.)
74
75
  */
75
- export declare function asAccount<T>(account: ServiceAccount, fn: () => T): T;
76
- /** Write a file in the account's home as the account (see asAccount). */
76
+ export declare function accountArgv(account: ServiceAccount): string[];
77
+ /** Write a file in the account's home as the account (see accountArgv). */
77
78
  export declare function writeAccountFile(account: ServiceAccount, path: string, text: string | Buffer, mode: number): void;
78
79
  export declare function systemdUnitName(name: string, kind: ServiceKind): string;
79
80
  export declare function systemdUnits(spec: ServiceSpec): Array<{
@@ -38,33 +38,38 @@ export function serviceAccount(runAs, run = defaultRunner) {
38
38
  throw new ZellijError("usage", `cannot find the home of ${target}`);
39
39
  return { platform, user: target, uid: id("-u"), gid: id("-g"), home, foreign: true };
40
40
  }
41
+ /** Root installing for another account (`--run-as`). */
42
+ function installingFor(account) {
43
+ return account.foreign && typeof process.geteuid === "function" && process.geteuid() === 0;
44
+ }
41
45
  /**
42
- * Run fs work in another account's home as that account. Root installing
43
- * with --run-as drops its effective uid and gid for the duration, so the
46
+ * The argv prefix that runs a command as the account: `runuser` on Linux,
47
+ * `sudo -u` on macOS. Root writes into the account's home through it, so the
44
48
  * kernel refuses what the account itself could not do (a symlink it planted
45
- * towards a root-owned file, say) and new files are the account's own. The
46
- * work must be synchronous: the effective ids are process-wide.
49
+ * towards a root-owned file, say) and new files are the account's own. (Not
50
+ * seteuid in this process: Node disables it on Linux while io_uring may be
51
+ * enabled, CVE-2024-22017.)
47
52
  */
48
- export function asAccount(account, fn) {
49
- const root = account.foreign && typeof process.geteuid === "function" && process.geteuid() === 0;
50
- if (!root)
51
- return fn();
52
- process.setegid(account.gid);
53
- process.seteuid(account.uid);
54
- try {
55
- return fn();
56
- }
57
- finally {
58
- process.seteuid(0);
59
- process.setegid(0);
53
+ export function accountArgv(account) {
54
+ return account.platform === "linux"
55
+ ? ["runuser", "-u", account.user, "--"]
56
+ : ["sudo", "-n", "-u", account.user, "-H", "--"];
57
+ }
58
+ function runAsAccount(account, argv, input) {
59
+ const [cmd, ...rest] = [...accountArgv(account), ...argv];
60
+ const res = spawnSync(cmd, rest, { input, encoding: "utf8" });
61
+ if (res.status !== 0) {
62
+ throw new ZellijError("failed", `${argv[0]} as ${account.user} failed: ${(res.stderr || res.error?.message || "").trim().slice(0, 300)}`);
60
63
  }
61
64
  }
62
- /** Write a file in the account's home as the account (see asAccount). */
65
+ /** Write a file in the account's home as the account (see accountArgv). */
63
66
  export function writeAccountFile(account, path, text, mode) {
64
- asAccount(account, () => {
67
+ if (!installingFor(account)) {
65
68
  writeFileSync(path, text, { mode });
66
69
  chmodSync(path, mode);
67
- });
70
+ return;
71
+ }
72
+ runAsAccount(account, ["sh", "-c", 'umask 077 && cat > "$1" && chmod "$2" "$1"', "sh", path, mode.toString(8)], text);
68
73
  }
69
74
  function realFs(account) {
70
75
  return {
@@ -319,7 +324,10 @@ export function ensureAccountDir(account, dir, mode = 0o700) {
319
324
  for (let d = dir; d && d !== "/" && !existsSync(d); d = d.slice(0, d.lastIndexOf("/")) || "/")
320
325
  missing.unshift(d);
321
326
  for (const d of missing) {
322
- asAccount(account, () => mkdirSync(d, { mode }));
327
+ if (installingFor(account))
328
+ runAsAccount(account, ["mkdir", "-m", mode.toString(8), d]);
329
+ else
330
+ mkdirSync(d, { mode });
323
331
  }
324
332
  }
325
333
  /** Write a mode-600 file for the account, creating its directory (700) as needed. */
package/dist/index.d.ts CHANGED
@@ -32,7 +32,7 @@ export { cliUsage, mcpInputSchema, parseCliArgv, MCP_TOOL_DESCRIPTION, OP_NAMES,
32
32
  export { HOST_COMMANDS, hostUsage, parseHostArgs, runHostCommand } from "./host/cli.js";
33
33
  export { findPython, runSlot, slotScriptPath } from "./host/slot.js";
34
34
  export { installServeService, serveServiceName, serveServiceSpec, serveTokenFromEnv, serveTokenPath, uninstallServeService, SERVE_AWAIT_SESSION_ENV, SERVE_TOKEN_FILE_ENV, type ServeServiceInput, } from "./host/serve-service.js";
35
- export { asAccount, installServices, launchdLabel, launchdPlist, removeServices, serviceAccount, serviceStates, systemdUnitName, systemdUnits, writeAccountFile, type Runner, type ServiceAccount, type ServiceHost, type ServiceKind, type ServiceSpec, type ServiceState, } from "./host/services.js";
35
+ export { accountArgv, installServices, launchdLabel, launchdPlist, removeServices, serviceAccount, serviceStates, systemdUnitName, systemdUnits, writeAccountFile, type Runner, type ServiceAccount, type ServiceHost, type ServiceKind, type ServiceSpec, type ServiceState, } from "./host/services.js";
36
36
  export { crewCheck, crewDir, crewDown, crewServiceSpecs, crewStart, crewStatus, crewUp, firstLayoutPane, parseEnvFile, CREW_UNIT_ENV, CREW_WATCH_SEC, type CrewDeps, type CrewUpInput, } from "./host/crew.js";
37
37
  export { deliverMessage, doneLines, fillMessage, freshDoneLine, launchRelay, prepareRelay, pruneRelays, relayLoop, relaysDir, runRelayDir, scrubRelayToken, startRelay, waitForRelay, type RelayConfig, type RelayDeps, type RelayStartInput, type RelayStatus, } from "./host/relay.js";
38
38
  export { hostChecks, hostDoctor, hostInstall, hostInstallArgs, hostScriptPath, parseHostReport, type HostCheck, type HostDoctorInput, type HostInstallInput, type SshRunner, } from "./host/host.js";
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ export { cliUsage, mcpInputSchema, parseCliArgv, MCP_TOOL_DESCRIPTION, OP_NAMES,
30
30
  export { HOST_COMMANDS, hostUsage, parseHostArgs, runHostCommand } from "./host/cli.js";
31
31
  export { findPython, runSlot, slotScriptPath } from "./host/slot.js";
32
32
  export { installServeService, serveServiceName, serveServiceSpec, serveTokenFromEnv, serveTokenPath, uninstallServeService, SERVE_AWAIT_SESSION_ENV, SERVE_TOKEN_FILE_ENV, } from "./host/serve-service.js";
33
- export { asAccount, installServices, launchdLabel, launchdPlist, removeServices, serviceAccount, serviceStates, systemdUnitName, systemdUnits, writeAccountFile, } from "./host/services.js";
33
+ export { accountArgv, installServices, launchdLabel, launchdPlist, removeServices, serviceAccount, serviceStates, systemdUnitName, systemdUnits, writeAccountFile, } from "./host/services.js";
34
34
  export { crewCheck, crewDir, crewDown, crewServiceSpecs, crewStart, crewStatus, crewUp, firstLayoutPane, parseEnvFile, CREW_UNIT_ENV, CREW_WATCH_SEC, } from "./host/crew.js";
35
35
  export { deliverMessage, doneLines, fillMessage, freshDoneLine, launchRelay, prepareRelay, pruneRelays, relayLoop, relaysDir, runRelayDir, scrubRelayToken, startRelay, waitForRelay, } from "./host/relay.js";
36
36
  export { hostChecks, hostDoctor, hostInstall, hostInstallArgs, hostScriptPath, parseHostReport, } from "./host/host.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zswarm/core",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "zSwarm Zellij client and shared ops dispatch",
6
6
  "exports": {