@yeaft/webchat-agent 1.0.438 → 1.0.439

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/container-cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
2
3
  import { homedir } from 'node:os';
3
4
  import { join, resolve } from 'node:path';
4
5
  import {
@@ -29,9 +30,10 @@ The secret is passed as an argument, like 'yeaft-agent install', and persisted b
29
30
  command to a private 0600 file before the container is created.
30
31
 
31
32
  Resource protection (default): install attaches the container to the shared
32
- '${DEFAULT_AGENT_SLICE}' cgroup slice created by 'setup-limits', which caps the sum of all
33
- Yeaft Agent containers at 90% of host CPU and 70% of host memory. Run setup-limits once
34
- as root before installing (or pass --no-slice to opt out of protection).
33
+ '${DEFAULT_AGENT_SLICE}' cgroup slice, which caps the sum of all Yeaft Agent
34
+ containers at 90% of host CPU and 70% of host memory. The slice is initialized
35
+ automatically on first install (as root directly, otherwise via sudo; you are
36
+ prompted for the sudo password once). Pass --no-slice to opt out of protection.
35
37
 
36
38
  --disk-size <size>: per-volume capacity quota for the data and workspace volumes, e.g.
37
39
  20G or 80% of the Docker data-root filesystem. Requires Docker overlay2 on xfs with
@@ -68,6 +70,56 @@ async function dockerDataRoot() {
68
70
  return result.stdout;
69
71
  }
70
72
 
73
+ /**
74
+ * Initialize the shared agent slice during `container install` so users never
75
+ * have to run `setup-limits` by hand. Root can write the slice directly; a
76
+ * non-root user re-invokes this CLI via an interactive sudo so the password
77
+ * prompt happens inline. Non-interactive shells skip sudo and fail with
78
+ * guidance instead of hanging.
79
+ *
80
+ * @param {object} options { spawnImpl, interactive } overrides for tests
81
+ */
82
+ export async function ensureAgentSliceAuto({
83
+ spawnImpl = spawn,
84
+ interactive = Boolean(process.stdin?.isTTY),
85
+ } = {}) {
86
+ if (typeof process.getuid === 'function' && process.getuid() === 0) {
87
+ await ensureAgentSlice();
88
+ return;
89
+ }
90
+ if (!interactive) {
91
+ throw new Error(
92
+ `Agent slice '${DEFAULT_AGENT_SLICE}' is not initialized and this shell is not interactive; ` +
93
+ 'run "sudo yeaft-agent container setup-limits" once, ' +
94
+ 'or pass --no-slice to install without shared resource limits',
95
+ );
96
+ }
97
+ const code = await runSudoSetupLimits({ spawnImpl });
98
+ if (code !== 0) {
99
+ throw new Error(
100
+ `Failed to initialize Agent slice '${DEFAULT_AGENT_SLICE}' via sudo (exit ${code}); ` +
101
+ 'run "sudo yeaft-agent container setup-limits" manually, ' +
102
+ 'or pass --no-slice to install without shared resource limits',
103
+ );
104
+ }
105
+ }
106
+
107
+ function runSudoSetupLimits({ spawnImpl = spawn } = {}) {
108
+ return new Promise((resolvePromise, reject) => {
109
+ // `sudo env PATH=...` keeps user-installed runtimes (nvm etc.) reachable
110
+ // even when sudoers sets a secure_path. Re-invoking the current entry
111
+ // script avoids depending on the npm bin being in sudo's PATH.
112
+ const entry = process.argv[1] ? resolve(process.argv[1]) : null;
113
+ const args = ['env', `PATH=${process.env.PATH || ''}`];
114
+ if (entry) args.push(process.execPath, entry);
115
+ else args.push('yeaft-agent');
116
+ args.push('container', 'setup-limits');
117
+ const child = spawnImpl('sudo', args, { stdio: 'inherit' });
118
+ child.once('error', reject);
119
+ child.once('close', code => resolvePromise(code ?? 1));
120
+ });
121
+ }
122
+
71
123
  export async function runContainerCli(args) {
72
124
  if (args.length === 0 || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return help();
73
125
  const { action, options } = parseContainerArgs(args);
@@ -87,11 +139,8 @@ export async function runContainerCli(args) {
87
139
  } else if (options.cgroupParent) {
88
140
  cgroupParent = String(options.cgroupParent).trim();
89
141
  } else if (!(await isAgentSliceReady())) {
90
- throw new Error(
91
- `Agent slice '${DEFAULT_AGENT_SLICE}' is not initialized; ` +
92
- 'run "sudo yeaft-agent container setup-limits" first, ' +
93
- 'or pass --no-slice to install without shared resource limits',
94
- );
142
+ await ensureAgentSliceAuto();
143
+ cgroupParent = DEFAULT_AGENT_SLICE;
95
144
  } else {
96
145
  cgroupParent = DEFAULT_AGENT_SLICE;
97
146
  }
@@ -5,7 +5,9 @@ import { userDb } from './database.js';
5
5
  import {
6
6
  checkContainerAgentRuntime,
7
7
  createContainerAgent,
8
+ ensureAgentSlice,
8
9
  inspectContainerAgent,
10
+ isAgentSliceReady,
9
11
  removeContainerAgent,
10
12
  startContainerAgent,
11
13
  stopContainerAgent,
@@ -15,7 +17,9 @@ import {
15
17
  const DEFAULT_RUNTIME = Object.freeze({
16
18
  check: checkContainerAgentRuntime,
17
19
  create: createContainerAgent,
20
+ ensureSlice: ensureAgentSlice,
18
21
  inspect: inspectContainerAgent,
22
+ isSliceReady: isAgentSliceReady,
19
23
  remove: removeContainerAgent,
20
24
  start: startContainerAgent,
21
25
  stop: stopContainerAgent,
@@ -100,6 +104,9 @@ export class ContainerAgentService {
100
104
  const name = this.nameForUser(user.id);
101
105
  const secretFile = join(this.config.stateDir, name, 'agent-secret');
102
106
  await this.runtime.writeSecret(secretFile, user.agent_secret);
107
+ if (this.config.cgroupParent) {
108
+ await this.ensureManagedSlice();
109
+ }
103
110
  await this.runtime.create({
104
111
  name,
105
112
  serverUrl: this.config.serverUrl,
@@ -122,6 +129,25 @@ export class ContainerAgentService {
122
129
  }
123
130
  }
124
131
 
132
+ /**
133
+ * Make sure the shared cgroup slice exists before the first managed
134
+ * container is created. The Server process must have root access to
135
+ * /sys/fs/cgroup (typical for a systemd service); a containerized Server
136
+ * without cgroup access fails here with an explicit code instead of
137
+ * silently creating an unprotected container.
138
+ */
139
+ async ensureManagedSlice() {
140
+ if (await this.runtime.isSliceReady()) return;
141
+ try {
142
+ await this.runtime.ensureSlice();
143
+ } catch (error) {
144
+ throw Object.assign(
145
+ new Error('SANDBOX_CGROUP_SLICE_UNAVAILABLE'),
146
+ { code: 'SANDBOX_CGROUP_SLICE_UNAVAILABLE', cause: error },
147
+ );
148
+ }
149
+ }
150
+
125
151
  // User-visible lifecycle operations are admitted on every request. The
126
152
  // feature flag and Docker reachability can change while a browser is open.
127
153
  async action(userId, action) {
@@ -1 +1 @@
1
- {"version":"1.0.438"}
1
+ {"version":"1.0.439"}