@rynx-ai/cli 0.1.11-beta.39 → 0.1.11-beta.40

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.
@@ -13,6 +13,7 @@ export interface AutostartState {
13
13
  export interface AutostartRenderInput {
14
14
  cliPath: string;
15
15
  dataDir: string;
16
+ environment?: NodeJS.ProcessEnv;
16
17
  logDir: string;
17
18
  nodePath: string;
18
19
  pathEnv: string;
@@ -1,6 +1,6 @@
1
- import { startStandaloneDaemon, standaloneDaemonRestartRequired, statusStandaloneDaemon, stopStandaloneDaemon, streamStandaloneDaemonLogs, } from "../standalone.js";
1
+ import { startStandaloneDaemon, statusStandaloneDaemon, stopStandaloneDaemon, streamStandaloneDaemonLogs, } from "../standalone.js";
2
2
  import { refreshAutostart } from "../autostart.js";
3
- import { assertLinuxSystemdInvocation, linuxUserSystemdAvailable, runLinuxSystemdLifecycle, } from "../systemd-service.js";
3
+ import { assertLinuxSystemdInvocation } from "../systemd-service.js";
4
4
  import { fail } from "./errors.js";
5
5
  const SYSTEMD_SERVICE_FLAG = "--systemd-service";
6
6
  export async function runLifecycleCommand(command, args, options = {}) {
@@ -14,18 +14,6 @@ export async function runLifecycleCommand(command, args, options = {}) {
14
14
  }
15
15
  if (args.length > 0)
16
16
  fail(`${command}: unexpected argument ${args[0]}`);
17
- if (linuxUserSystemdAvailable() &&
18
- (command === "start" ||
19
- command === "restart" ||
20
- command === "stop" ||
21
- command === "status")) {
22
- return runLinuxSystemdLifecycle(command, {
23
- restartRequired: standaloneDaemonRestartRequired,
24
- start: startStandaloneDaemon,
25
- status: statusStandaloneDaemon,
26
- stop: stopStandaloneDaemon,
27
- }, options);
28
- }
29
17
  switch (command) {
30
18
  case "start": {
31
19
  refreshAutostart();
@@ -4,7 +4,6 @@ export declare function startStandaloneDaemon(options?: {
4
4
  restart?: boolean;
5
5
  quiet?: boolean;
6
6
  }): Promise<number>;
7
- export declare function standaloneDaemonRestartRequired(): Promise<boolean>;
8
7
  /** Reuse any healthy owner; otherwise explicitly start the standalone owner. */
9
8
  export declare function ensureStandaloneDaemon(): Promise<{
10
9
  origin: string;
@@ -22,10 +22,6 @@ export async function startStandaloneDaemon(options = {}) {
22
22
  },
23
23
  });
24
24
  }
25
- export async function standaloneDaemonRestartRequired() {
26
- const { isDaemonStartRequired } = await import("@rynx-ai/daemon/lifecycle");
27
- return isDaemonStartRequired();
28
- }
29
25
  /** Reuse any healthy owner; otherwise explicitly start the standalone owner. */
30
26
  export async function ensureStandaloneDaemon() {
31
27
  const current = await resolveDaemonControlEndpoint();
@@ -1,6 +1,7 @@
1
1
  import type { AutostartRenderInput, AutostartState } from "./autostart.js";
2
2
  export declare const RYNX_SYSTEMD_SERVICE = "rynx.service";
3
3
  export declare const RYNX_SYSTEMD_SERVICE_ENV = "RYNX_SYSTEMD_SERVICE";
4
+ export declare const RYNX_SERVICE_ENV_KEYS = "RYNX_SERVICE_ENV_KEYS";
4
5
  export interface LinuxSystemdServiceState {
5
6
  loadState: string;
6
7
  activeState: string;
@@ -38,26 +39,14 @@ export interface LinuxPm2InspectionDeps {
38
39
  statUid?: (file: string) => number;
39
40
  currentUid?: number;
40
41
  }
41
- export interface LinuxLifecycleActions {
42
- restartRequired: () => Promise<boolean>;
43
- start: (options?: {
44
- restart?: boolean;
45
- quiet?: boolean;
46
- }) => Promise<number>;
47
- status: () => Promise<number>;
48
- stop: () => Promise<number>;
49
- }
50
42
  export declare function renderSystemdUnit(input: AutostartRenderInput): string;
51
43
  export declare function linuxUserSystemdAvailable(): boolean;
52
44
  export declare function inspectLinuxAutostart(): AutostartState;
53
45
  export declare function enableLinuxAutostart(): void;
54
46
  export declare function disableLinuxAutostart(): void;
55
- /** Refresh a registered or lifecycle-created unit without changing enablement. */
47
+ /** Refresh an existing autostart unit without changing enablement. */
56
48
  export declare function refreshLinuxServiceIfPresent(): boolean;
57
49
  export declare function assertLinuxSystemdInvocation(): void;
58
- export declare function runLinuxSystemdLifecycle(command: "start" | "restart" | "stop" | "status", actions: LinuxLifecycleActions, options?: {
59
- quiet?: boolean;
60
- }): Promise<number>;
61
50
  export declare function parseLinuxSystemdShow(output: string): LinuxSystemdServiceState;
62
51
  export declare function inspectLinuxPm2GodOwnership(home: string, deps?: LinuxPm2InspectionDeps): LinuxPm2GodOwnership;
63
52
  export declare function scanLinuxPm2GodPids(home: string, deps?: LinuxPm2InspectionDeps): number[];
@@ -6,10 +6,78 @@ import { fileURLToPath } from "node:url";
6
6
  import { rynxHome } from "@rynx-ai/core";
7
7
  export const RYNX_SYSTEMD_SERVICE = "rynx.service";
8
8
  export const RYNX_SYSTEMD_SERVICE_ENV = "RYNX_SYSTEMD_SERVICE";
9
+ export const RYNX_SERVICE_ENV_KEYS = "RYNX_SERVICE_ENV_KEYS";
9
10
  const COMMAND_TIMEOUT_MS = 5_000;
10
- const LIFECYCLE_TIMEOUT_MS = 60_000;
11
+ const AUTOMATIC_SERVICE_ENVIRONMENT = new Set([
12
+ "ALL_PROXY",
13
+ "AWS_CA_BUNDLE",
14
+ "CURL_CA_BUNDLE",
15
+ "GIT_SSL_CAINFO",
16
+ "HTTPS_PROXY",
17
+ "HTTP_PROXY",
18
+ "NODE_EXTRA_CA_CERTS",
19
+ "NO_PROXY",
20
+ "REQUESTS_CA_BUNDLE",
21
+ "SSL_CERT_DIR",
22
+ "SSL_CERT_FILE",
23
+ "all_proxy",
24
+ "https_proxy",
25
+ "http_proxy",
26
+ "no_proxy",
27
+ ]);
28
+ const BLOCKED_SERVICE_ENVIRONMENT = new Set([
29
+ "PATH",
30
+ "INVOCATION_ID",
31
+ "JOURNAL_STREAM",
32
+ "LISTEN_FDS",
33
+ "LISTEN_FDNAMES",
34
+ "LISTEN_PID",
35
+ "MAINPID",
36
+ "MANAGERPID",
37
+ "NOTIFY_SOCKET",
38
+ "ELECTRON_RUN_AS_NODE",
39
+ "NODE_CHANNEL_FD",
40
+ "NODE_UNIQUE_ID",
41
+ "PM2_HOME",
42
+ "RYNX_DAEMON_LIFECYCLE",
43
+ "RYNX_HOME",
44
+ "RYNX_REFRESH_LOGIN_SHELL_PATH",
45
+ RYNX_SERVICE_ENV_KEYS,
46
+ RYNX_SYSTEMD_SERVICE_ENV,
47
+ "SYSTEMD_EXEC_PID",
48
+ "SYSTEMD_INVOCATION_ID",
49
+ "WATCHDOG_PID",
50
+ "WATCHDOG_USEC",
51
+ ]);
52
+ const TRANSIENT_SERVICE_ENVIRONMENT = new Set([
53
+ "_",
54
+ "CI",
55
+ "CLICOLOR",
56
+ "CLICOLOR_FORCE",
57
+ "CODEX_CI",
58
+ "COLORTERM",
59
+ "FORCE_COLOR",
60
+ "GH_PAGER",
61
+ "GIT_PAGER",
62
+ "NO_COLOR",
63
+ "OLDPWD",
64
+ "PAGER",
65
+ "PWD",
66
+ "SHLVL",
67
+ "SSH_CLIENT",
68
+ "SSH_CONNECTION",
69
+ "SSH_TTY",
70
+ "TERM",
71
+ "TERM_PROGRAM",
72
+ "TERM_PROGRAM_VERSION",
73
+ "TERM_SESSION_ID",
74
+ "TERMINFO",
75
+ "TMUX",
76
+ "TMUX_PANE",
77
+ ]);
11
78
  export function renderSystemdUnit(input) {
12
79
  const pidFile = path.join(input.dataDir, "pm2", "pm2.pid");
80
+ const inheritedEnvironment = renderInheritedEnvironment(input.environment);
13
81
  return `[Unit]
14
82
  Description=Rynx local control plane
15
83
  After=network-online.target
@@ -26,6 +94,7 @@ SendSIGKILL=no
26
94
  TimeoutStartSec=60
27
95
  TimeoutStopSec=45
28
96
  WorkingDirectory=${systemdPath(input.dataDir)}
97
+ ${inheritedEnvironment}
29
98
  Environment=${systemdQuote(`PATH=${input.pathEnv}`)}
30
99
  Environment=${systemdQuote(`RYNX_HOME=${input.dataDir}`)}
31
100
  Environment=${systemdQuote(`${RYNX_SYSTEMD_SERVICE_ENV}=${RYNX_SYSTEMD_SERVICE}`)}
@@ -95,7 +164,7 @@ export function disableLinuxAutostart() {
95
164
  rmSync(context.unitPath, { force: true });
96
165
  requireSuccess(run("systemctl", ["--user", "daemon-reload"]), "systemctl --user daemon-reload");
97
166
  }
98
- /** Refresh a registered or lifecycle-created unit without changing enablement. */
167
+ /** Refresh an existing autostart unit without changing enablement. */
99
168
  export function refreshLinuxServiceIfPresent() {
100
169
  if (process.platform !== "linux" || !linuxUserSystemdAvailable())
101
170
  return false;
@@ -116,59 +185,6 @@ export function assertLinuxSystemdInvocation() {
116
185
  throw new Error(`--systemd-service requires the ${RYNX_SYSTEMD_SERVICE} cgroup`);
117
186
  }
118
187
  }
119
- export async function runLinuxSystemdLifecycle(command, actions, options = {}) {
120
- assertUserSystemdAvailable();
121
- const context = currentContext();
122
- if (command === "status") {
123
- return statusLinuxLifecycle(context, actions);
124
- }
125
- if (command === "stop") {
126
- return stopLinuxLifecycle(context, actions);
127
- }
128
- const unitChanged = syncLinuxService(context);
129
- const before = inspectLinuxPm2GodOwnership(context.pm2Home);
130
- assertSingleGod(before);
131
- const serviceBefore = inspectLinuxSystemdService();
132
- if (before.kind === "owned" && serviceOwnsGod(serviceBefore, before.processes[0])) {
133
- if (command === "start") {
134
- if (unitChanged || await actions.restartRequired()) {
135
- systemctlUser(["restart", RYNX_SYSTEMD_SERVICE], LIFECYCLE_TIMEOUT_MS);
136
- assertRunningServiceOwnership(context);
137
- return options.quiet ? 0 : actions.status();
138
- }
139
- const result = await actions.start(options.quiet ? { quiet: true } : undefined);
140
- if (result !== 0)
141
- return result;
142
- assertRunningServiceOwnership(context);
143
- return 0;
144
- }
145
- const previous = before.processes[0];
146
- systemctlUser(["restart", RYNX_SYSTEMD_SERVICE], LIFECYCLE_TIMEOUT_MS);
147
- const current = assertRunningServiceOwnership(context);
148
- if (current.pid === previous.pid &&
149
- current.startIdentity === previous.startIdentity) {
150
- throw new Error("systemd restart did not replace the PM2 supervisor generation");
151
- }
152
- return options.quiet ? 0 : actions.status();
153
- }
154
- if (before.kind !== "absent") {
155
- assertMigratableOwnership(before);
156
- const stopped = await actions.stop();
157
- if (stopped !== 0)
158
- return stopped;
159
- const afterStop = inspectLinuxPm2GodOwnership(context.pm2Home);
160
- if (afterStop.kind !== "absent") {
161
- throw new Error("the previous PM2 supervisor is still running after shutdown");
162
- }
163
- }
164
- if (serviceBefore.activeState !== "inactive") {
165
- systemctlUser(["stop", RYNX_SYSTEMD_SERVICE], LIFECYCLE_TIMEOUT_MS);
166
- }
167
- run("systemctl", ["--user", "reset-failed", RYNX_SYSTEMD_SERVICE]);
168
- systemctlUser(["start", RYNX_SYSTEMD_SERVICE], LIFECYCLE_TIMEOUT_MS);
169
- assertRunningServiceOwnership(context);
170
- return options.quiet ? 0 : actions.status();
171
- }
172
188
  export function parseLinuxSystemdShow(output) {
173
189
  const values = new Map();
174
190
  for (const line of output.split(/\r?\n/)) {
@@ -282,6 +298,7 @@ function currentContext() {
282
298
  dataDir,
283
299
  logDir: path.join(dataDir, "logs"),
284
300
  cliPath: fileURLToPath(new URL("./cli.js", import.meta.url)),
301
+ environment: process.env,
285
302
  nodePath: process.execPath,
286
303
  pathEnv: process.env.PATH || defaultLinuxPath(),
287
304
  pm2Home,
@@ -407,64 +424,11 @@ function assertRunningServiceOwnership(context) {
407
424
  }
408
425
  return process;
409
426
  }
410
- async function statusLinuxLifecycle(context, actions) {
411
- const ownership = inspectLinuxPm2GodOwnership(context.pm2Home);
412
- if (ownership.kind === "absent") {
413
- console.log("rynx: stopped (PM2 supervisor is absent)");
414
- return 1;
415
- }
416
- assertSingleGod(ownership);
417
- const directStatus = await actions.status();
418
- if (ownership.kind !== "owned") {
419
- console.error(`rynx: PM2 supervisor is not owned by ${RYNX_SYSTEMD_SERVICE}: ${describeOwnership(ownership)}`);
420
- return 2;
421
- }
422
- try {
423
- assertRunningServiceOwnership(context);
424
- }
425
- catch (error) {
426
- console.error(`rynx: ${errorMessage(error)}`);
427
- return 1;
428
- }
429
- return directStatus;
430
- }
431
- async function stopLinuxLifecycle(context, actions) {
432
- const ownership = inspectLinuxPm2GodOwnership(context.pm2Home);
433
- assertSingleGod(ownership);
434
- if (ownership.kind === "owned") {
435
- const state = inspectLinuxSystemdService();
436
- if (serviceOwnsGod(state, ownership.processes[0])) {
437
- systemctlUser(["stop", RYNX_SYSTEMD_SERVICE], LIFECYCLE_TIMEOUT_MS);
438
- if (inspectLinuxPm2GodOwnership(context.pm2Home).kind !== "absent") {
439
- throw new Error("PM2 supervisor survived systemd stop");
440
- }
441
- console.log("rynx stopped.");
442
- return 0;
443
- }
444
- }
445
- if (ownership.kind === "external")
446
- assertMigratableOwnership(ownership);
447
- return actions.stop();
448
- }
449
427
  function assertSingleGod(ownership) {
450
428
  if (ownership.kind !== "absent" && ownership.processes.length !== 1) {
451
429
  throw new Error(`multiple PM2 supervisors use this RYNX_HOME: ${describeOwnership(ownership)}`);
452
430
  }
453
431
  }
454
- function assertMigratableOwnership(ownership) {
455
- if (ownership.kind !== "external")
456
- return;
457
- const externalService = ownership.processes.find((entry) => serviceOwner(entry.cgroup)?.endsWith(".service"));
458
- if (externalService) {
459
- throw new Error(`refusing to replace a PM2 supervisor owned by ${serviceOwner(externalService.cgroup)}`);
460
- }
461
- }
462
- function serviceOwnsGod(state, process) {
463
- return state.activeState === "active" &&
464
- state.subState === "running" &&
465
- state.mainPid === process.pid &&
466
- cgroupHasService(process.cgroup);
467
- }
468
432
  function describeOwnership(ownership) {
469
433
  if (ownership.kind === "absent")
470
434
  return "absent";
@@ -472,9 +436,6 @@ function describeOwnership(ownership) {
472
436
  .map((entry) => `${entry.pid}@${entry.cgroup}`)
473
437
  .join(", ");
474
438
  }
475
- function serviceOwner(cgroup) {
476
- return cgroup.split("/").reverse().find((part) => part.endsWith(".service") || part.endsWith(".scope"));
477
- }
478
439
  function inspectLinuxLinger() {
479
440
  const result = run("loginctl", [
480
441
  "show-user",
@@ -550,7 +511,9 @@ function replaceFileAtomically(file, content) {
550
511
  writeFileSync(temporary, content, {
551
512
  encoding: "utf8",
552
513
  flag: "wx",
553
- mode: 0o644,
514
+ // The generated unit contains the caller's environment and may include
515
+ // runtime credentials. Keep the snapshot private to the owning user.
516
+ mode: 0o600,
554
517
  });
555
518
  renameSync(temporary, file);
556
519
  }
@@ -586,6 +549,24 @@ function signalMatches(value, number, name) {
586
549
  function defaultLinuxPath() {
587
550
  return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
588
551
  }
552
+ function renderInheritedEnvironment(env) {
553
+ if (!env)
554
+ return "";
555
+ const requested = new Set((env[RYNX_SERVICE_ENV_KEYS] ?? "")
556
+ .split(/[\s,]+/)
557
+ .filter((name) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(name)));
558
+ return Object.entries(env)
559
+ .filter(([name, value]) => value !== undefined &&
560
+ /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) &&
561
+ (AUTOMATIC_SERVICE_ENVIRONMENT.has(name) || requested.has(name)) &&
562
+ !name.startsWith("npm_") &&
563
+ !name.startsWith("RYNX_RUNNER_") &&
564
+ !BLOCKED_SERVICE_ENVIRONMENT.has(name) &&
565
+ !TRANSIENT_SERVICE_ENVIRONMENT.has(name))
566
+ .sort(([left], [right]) => left.localeCompare(right))
567
+ .map(([name, value]) => `Environment=${systemdQuote(`${name}=${value}`)}`)
568
+ .join("\n");
569
+ }
589
570
  function systemdQuote(value) {
590
571
  return `"${value
591
572
  .replaceAll("\\", "\\\\")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/cli",
3
- "version": "0.1.11-beta.39",
3
+ "version": "0.1.11-beta.40",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -51,12 +51,12 @@
51
51
  "dependencies": {
52
52
  "@clack/prompts": "^1.6.0",
53
53
  "ws": "^8.21.0",
54
- "@rynx-ai/core": "0.1.11-beta.39",
55
- "@rynx-ai/daemon": "0.1.11-beta.39",
56
- "@rynx-ai/browser-cdp": "0.1.11-beta.39",
57
- "@rynx-ai/emulator": "0.1.11-beta.39",
58
- "@rynx-ai/tmux": "0.1.11-beta.39",
59
- "@rynx-ai/protocol": "0.1.11-beta.39"
54
+ "@rynx-ai/browser-cdp": "0.1.11-beta.40",
55
+ "@rynx-ai/core": "0.1.11-beta.40",
56
+ "@rynx-ai/emulator": "0.1.11-beta.40",
57
+ "@rynx-ai/protocol": "0.1.11-beta.40",
58
+ "@rynx-ai/tmux": "0.1.11-beta.40",
59
+ "@rynx-ai/daemon": "0.1.11-beta.40"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/ws": "^8.18.1"