@yeaft/webchat-agent 1.0.442 → 1.0.444

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/cli.js CHANGED
@@ -74,11 +74,13 @@ if (command === 'doctor') {
74
74
  } else if (command === 'local') {
75
75
  await handleLocalCommand(subArgs);
76
76
  } else if (command === 'container') {
77
+ let formatContainerError = error => error.code || error.message;
77
78
  try {
78
- const { runContainerCli } = await import('./container-cli.js');
79
- await runContainerCli(subArgs);
79
+ const containerCli = await import('./container-cli.js');
80
+ formatContainerError = containerCli.formatContainerError;
81
+ await containerCli.runContainerCli(subArgs);
80
82
  } catch (error) {
81
- console.error(`Container Agent failed: ${error.code || error.message}`);
83
+ console.error(`Container Agent failed: ${formatContainerError(error)}`);
82
84
  process.exit(1);
83
85
  }
84
86
  } else if (command === 'upgrade') {
package/container-cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { spawn } from 'node:child_process';
3
3
  import { homedir } from 'node:os';
4
4
  import { join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import {
6
7
  createContainerAgent,
7
8
  DEFAULT_AGENT_SLICE,
@@ -18,6 +19,25 @@ import {
18
19
  writeAgentSecretFile,
19
20
  } from './container-manager.js';
20
21
 
22
+ function shellQuote(value) {
23
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
24
+ }
25
+
26
+ function agentCliEntry() {
27
+ return fileURLToPath(new URL('./cli.js', import.meta.url));
28
+ }
29
+
30
+ export function setupLimitsCommand() {
31
+ return `sudo ${shellQuote(process.execPath)} ${shellQuote(agentCliEntry())} container setup-limits`;
32
+ }
33
+
34
+ export function formatContainerError(error) {
35
+ const code = String(error?.code || '').trim();
36
+ const message = String(error?.message || '').trim();
37
+ if (code && message && code !== message) return `${code}: ${message}`;
38
+ return code || message || String(error);
39
+ }
40
+
21
41
  function help() {
22
42
  console.log(`
23
43
  Usage:
@@ -34,6 +54,8 @@ Resource protection (default): install attaches the container to the shared
34
54
  containers at 90% of host CPU and 70% of host memory. The slice is initialized
35
55
  automatically on first install (as root directly, otherwise via sudo; you are
36
56
  prompted for the sudo password once). Pass --no-slice to opt out of protection.
57
+ If setup-limits must be run manually for a user-local install, use the
58
+ absolute Node/CLI path printed by this command: ${setupLimitsCommand()}.
37
59
 
38
60
  --disk-size <size>: per-volume capacity quota for the data and workspace volumes, e.g.
39
61
  20G or 80% of the Docker data-root filesystem. Requires Docker overlay2 on xfs with
@@ -90,7 +112,7 @@ export async function ensureAgentSliceAuto({
90
112
  if (!interactive) {
91
113
  throw new Error(
92
114
  `Agent slice '${DEFAULT_AGENT_SLICE}' is not initialized and this shell is not interactive; ` +
93
- 'run "sudo yeaft-agent container setup-limits" once, ' +
115
+ `run ${setupLimitsCommand()} once, ` +
94
116
  'or pass --no-slice to install without shared resource limits',
95
117
  );
96
118
  }
@@ -98,7 +120,7 @@ export async function ensureAgentSliceAuto({
98
120
  if (code !== 0) {
99
121
  throw new Error(
100
122
  `Failed to initialize Agent slice '${DEFAULT_AGENT_SLICE}' via sudo (exit ${code}); ` +
101
- 'run "sudo yeaft-agent container setup-limits" manually, ' +
123
+ `run ${setupLimitsCommand()} manually, ` +
102
124
  'or pass --no-slice to install without shared resource limits',
103
125
  );
104
126
  }
@@ -106,14 +128,10 @@ export async function ensureAgentSliceAuto({
106
128
 
107
129
  function runSudoSetupLimits({ spawnImpl = spawn } = {}) {
108
130
  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');
131
+ // Use absolute paths for both the runtime and this installed module. This
132
+ // bypasses sudo's secure_path entirely and works for npm-global installs
133
+ // that only exist in the invoking user's PATH.
134
+ const args = [process.execPath, agentCliEntry(), 'container', 'setup-limits'];
117
135
  const child = spawnImpl('sudo', args, { stdio: 'inherit' });
118
136
  child.once('error', reject);
119
137
  child.once('close', code => resolvePromise(code ?? 1));
@@ -161,7 +179,7 @@ export async function runContainerCli(args) {
161
179
  });
162
180
  } else if (action === 'setup-limits') {
163
181
  if (typeof process.getuid === 'function' && process.getuid() !== 0) {
164
- throw new Error('setup-limits must run as root; use sudo');
182
+ throw new Error(`setup-limits must run as root; use ${setupLimitsCommand()}`);
165
183
  }
166
184
  const resources = await detectHostResources();
167
185
  result = await ensureAgentSlice({
@@ -191,7 +209,7 @@ export async function runContainerCli(args) {
191
209
 
192
210
  if (process.argv[1] && import.meta.url === new URL(`file://${resolve(process.argv[1])}`).href) {
193
211
  runContainerCli(process.argv.slice(2)).catch(error => {
194
- console.error(`Container Agent failed: ${error.code || error.message}`);
212
+ console.error(`Container Agent failed: ${formatContainerError(error)}`);
195
213
  process.exitCode = 1;
196
214
  });
197
215
  }
@@ -17,6 +17,23 @@ export class ContainerAgentError extends Error {
17
17
  }
18
18
  }
19
19
 
20
+ function isCgroupWritePermissionError(error) {
21
+ const code = String(error?.code || '').toUpperCase();
22
+ const message = String(error?.message || '');
23
+ return ['EACCES', 'EPERM', 'EROFS'].includes(code)
24
+ || /EACCES|EPERM|EROFS|permission denied|operation not permitted|read-only file system/i.test(message);
25
+ }
26
+
27
+ function wrapCgroupWriteError(error, slicePath) {
28
+ if (!isCgroupWritePermissionError(error)) return error;
29
+ return new ContainerAgentError(
30
+ 'CONTAINER_AGENT_CGROUP_PERMISSION_DENIED',
31
+ `Cannot write cgroup v2 slice at ${slicePath} (${error.code || 'permission denied'}). ` +
32
+ 'The host cgroup hierarchy is not writable or delegated to this process; ' +
33
+ 'run setup-limits as host root with a read-write cgroup v2 mount, or pass --no-slice to install without shared resource limits',
34
+ );
35
+ }
36
+
20
37
  export function normalizeContainerAgentName(name) {
21
38
  const value = String(name || '').trim();
22
39
  if (!NAME_PATTERN.test(value)) throw new ContainerAgentError('CONTAINER_AGENT_INVALID_NAME');
@@ -276,8 +293,9 @@ export async function ensureAgentSlice({
276
293
  try {
277
294
  const text = await fsImpl.readFile('/sys/fs/cgroup/cgroup.controllers', 'utf8');
278
295
  controllers = String(text || '').trim().split(/\s+/).filter(Boolean);
279
- } catch {
280
- // Treat missing controller list as "no controllers writable".
296
+ } catch (error) {
297
+ if (isCgroupWritePermissionError(error)) throw wrapCgroupWriteError(error, '/sys/fs/cgroup');
298
+ // Treat a missing controller list as "no controllers writable".
281
299
  }
282
300
  const wanted = ['cpu', 'memory', 'pids'].filter(name => controllers.includes(name));
283
301
  if (wanted.length === 0) {
@@ -286,35 +304,39 @@ export async function ensureAgentSlice({
286
304
  'cgroup v2 controllers (cpu/memory/pids) are unavailable; setup-limits requires a host running cgroup v2 and root access to /sys/fs/cgroup',
287
305
  );
288
306
  }
289
- await fsImpl.mkdir(slicePath, { recursive: true, mode: 0o755 });
290
- await fsImpl.writeFile(
291
- `${slicePath}/cgroup.subtree_control`,
292
- wanted.map(name => `+${name}`).join(' '),
293
- 'utf8',
294
- );
295
- const detected = resources || await detectHostResources({ readFileImpl: fsImpl.readFile });
296
- const limits = buildSliceLimits({ cpuPercent, memoryPercent, pidsLimit }, detected);
297
- if (wanted.includes('cpu') && limits.cpuQuotaUs > 0) {
298
- await fsImpl.writeFile(`${slicePath}/cpu.max`, `${limits.cpuQuotaUs} ${limits.cpuPeriodUs}`, 'utf8');
299
- }
300
- if (wanted.includes('memory') && limits.memoryMaxBytes > 0) {
301
- await fsImpl.writeFile(`${slicePath}/memory.max`, String(limits.memoryMaxBytes), 'utf8');
302
- // No swap escape hatch: the 70% RAM hard cap is the whole memory budget.
303
- await fsImpl.writeFile(`${slicePath}/memory.swap.max`, '0', 'utf8');
304
- }
305
- if (wanted.includes('pids') && limits.pidsMax > 0) {
306
- await fsImpl.writeFile(`${slicePath}/pids.max`, String(limits.pidsMax), 'utf8');
307
+ try {
308
+ await fsImpl.mkdir(slicePath, { recursive: true, mode: 0o755 });
309
+ await fsImpl.writeFile(
310
+ `${slicePath}/cgroup.subtree_control`,
311
+ wanted.map(name => `+${name}`).join(' '),
312
+ 'utf8',
313
+ );
314
+ const detected = resources || await detectHostResources({ readFileImpl: fsImpl.readFile });
315
+ const limits = buildSliceLimits({ cpuPercent, memoryPercent, pidsLimit }, detected);
316
+ if (wanted.includes('cpu') && limits.cpuQuotaUs > 0) {
317
+ await fsImpl.writeFile(`${slicePath}/cpu.max`, `${limits.cpuQuotaUs} ${limits.cpuPeriodUs}`, 'utf8');
318
+ }
319
+ if (wanted.includes('memory') && limits.memoryMaxBytes > 0) {
320
+ await fsImpl.writeFile(`${slicePath}/memory.max`, String(limits.memoryMaxBytes), 'utf8');
321
+ // No swap escape hatch: the 70% RAM hard cap is the whole memory budget.
322
+ await fsImpl.writeFile(`${slicePath}/memory.swap.max`, '0', 'utf8');
323
+ }
324
+ if (wanted.includes('pids') && limits.pidsMax > 0) {
325
+ await fsImpl.writeFile(`${slicePath}/pids.max`, String(limits.pidsMax), 'utf8');
326
+ }
327
+ const marker = {
328
+ slice,
329
+ cpuQuotaUs: limits.cpuQuotaUs,
330
+ cpuPeriodUs: limits.cpuPeriodUs,
331
+ memoryMaxBytes: limits.memoryMaxBytes,
332
+ pidsMax: limits.pidsMax,
333
+ updatedAt: new Date().toISOString(),
334
+ };
335
+ await fsImpl.writeFile(`${slicePath}/${SLICE_READY_MARKER}`, JSON.stringify(marker, null, 2), 'utf8');
336
+ return { slice, limits, controllers: wanted };
337
+ } catch (error) {
338
+ throw wrapCgroupWriteError(error, slicePath);
307
339
  }
308
- const marker = {
309
- slice,
310
- cpuQuotaUs: limits.cpuQuotaUs,
311
- cpuPeriodUs: limits.cpuPeriodUs,
312
- memoryMaxBytes: limits.memoryMaxBytes,
313
- pidsMax: limits.pidsMax,
314
- updatedAt: new Date().toISOString(),
315
- };
316
- await fsImpl.writeFile(`${slicePath}/${SLICE_READY_MARKER}`, JSON.stringify(marker, null, 2), 'utf8');
317
- return { slice, limits, controllers: wanted };
318
340
  }
319
341
 
320
342
  /**
package/index.js CHANGED
@@ -24,6 +24,7 @@ import { connect } from './connection.js';
24
24
  import { loadMcpServers } from './mcp.js';
25
25
  import { SAFE_REMOTE_UPGRADE_CAPABILITY } from './upgrade-command.js';
26
26
  import { loadConfig as loadYeaftConfig } from './yeaft/config.js';
27
+ import { initYeaftDir } from './yeaft/init.js';
27
28
  import { updateBrowserRuntimeSettings } from './yeaft/config-api.js';
28
29
  import { bootBrowserRuntime, shutdownBrowserRuntime } from './browser-runtime/index.js';
29
30
  import {
@@ -106,12 +107,15 @@ const { agentName: AGENT_NAME, instanceId: INSTANCE_ID } = resolveRuntimeIdentit
106
107
  // WebSocket connection goes live, so downstream code can assume a real path.
107
108
  const YEAFT_DIR = process.env.YEAFT_DIR || fileConfig.yeaftDir || getDefaultYeaftDir(INSTANCE_ID);
108
109
  try {
109
- if (!existsSync(YEAFT_DIR)) {
110
- mkdirSync(YEAFT_DIR, { recursive: true, mode: 0o700 });
111
- console.log(`[Agent] Created yeaft dir: ${YEAFT_DIR}`);
110
+ const initResult = initYeaftDir(YEAFT_DIR);
111
+ for (const warning of initResult.warnings || []) {
112
+ console.warn(`[Agent] ${warning}`);
113
+ }
114
+ if (initResult.created?.length > 0) {
115
+ console.log(`[Agent] Initialized yeaft dir: ${YEAFT_DIR}`);
112
116
  }
113
117
  } catch (err) {
114
- console.warn(`[Agent] Could not ensure yeaft dir ${YEAFT_DIR}: ${err?.message || err}`);
118
+ console.warn(`[Agent] Could not initialize yeaft dir ${YEAFT_DIR}: ${err?.message || err}`);
115
119
  }
116
120
 
117
121
  const agentSecret = process.env.AGENT_SECRET_FILE
@@ -17,6 +17,23 @@ export class ContainerAgentError extends Error {
17
17
  }
18
18
  }
19
19
 
20
+ function isCgroupWritePermissionError(error) {
21
+ const code = String(error?.code || '').toUpperCase();
22
+ const message = String(error?.message || '');
23
+ return ['EACCES', 'EPERM', 'EROFS'].includes(code)
24
+ || /EACCES|EPERM|EROFS|permission denied|operation not permitted|read-only file system/i.test(message);
25
+ }
26
+
27
+ function wrapCgroupWriteError(error, slicePath) {
28
+ if (!isCgroupWritePermissionError(error)) return error;
29
+ return new ContainerAgentError(
30
+ 'CONTAINER_AGENT_CGROUP_PERMISSION_DENIED',
31
+ `Cannot write cgroup v2 slice at ${slicePath} (${error.code || 'permission denied'}). ` +
32
+ 'The host cgroup hierarchy is not writable or delegated to this process; ' +
33
+ 'run setup-limits as host root with a read-write cgroup v2 mount, or pass --no-slice to install without shared resource limits',
34
+ );
35
+ }
36
+
20
37
  export function normalizeContainerAgentName(name) {
21
38
  const value = String(name || '').trim();
22
39
  if (!NAME_PATTERN.test(value)) throw new ContainerAgentError('CONTAINER_AGENT_INVALID_NAME');
@@ -276,8 +293,9 @@ export async function ensureAgentSlice({
276
293
  try {
277
294
  const text = await fsImpl.readFile('/sys/fs/cgroup/cgroup.controllers', 'utf8');
278
295
  controllers = String(text || '').trim().split(/\s+/).filter(Boolean);
279
- } catch {
280
- // Treat missing controller list as "no controllers writable".
296
+ } catch (error) {
297
+ if (isCgroupWritePermissionError(error)) throw wrapCgroupWriteError(error, '/sys/fs/cgroup');
298
+ // Treat a missing controller list as "no controllers writable".
281
299
  }
282
300
  const wanted = ['cpu', 'memory', 'pids'].filter(name => controllers.includes(name));
283
301
  if (wanted.length === 0) {
@@ -286,35 +304,39 @@ export async function ensureAgentSlice({
286
304
  'cgroup v2 controllers (cpu/memory/pids) are unavailable; setup-limits requires a host running cgroup v2 and root access to /sys/fs/cgroup',
287
305
  );
288
306
  }
289
- await fsImpl.mkdir(slicePath, { recursive: true, mode: 0o755 });
290
- await fsImpl.writeFile(
291
- `${slicePath}/cgroup.subtree_control`,
292
- wanted.map(name => `+${name}`).join(' '),
293
- 'utf8',
294
- );
295
- const detected = resources || await detectHostResources({ readFileImpl: fsImpl.readFile });
296
- const limits = buildSliceLimits({ cpuPercent, memoryPercent, pidsLimit }, detected);
297
- if (wanted.includes('cpu') && limits.cpuQuotaUs > 0) {
298
- await fsImpl.writeFile(`${slicePath}/cpu.max`, `${limits.cpuQuotaUs} ${limits.cpuPeriodUs}`, 'utf8');
299
- }
300
- if (wanted.includes('memory') && limits.memoryMaxBytes > 0) {
301
- await fsImpl.writeFile(`${slicePath}/memory.max`, String(limits.memoryMaxBytes), 'utf8');
302
- // No swap escape hatch: the 70% RAM hard cap is the whole memory budget.
303
- await fsImpl.writeFile(`${slicePath}/memory.swap.max`, '0', 'utf8');
304
- }
305
- if (wanted.includes('pids') && limits.pidsMax > 0) {
306
- await fsImpl.writeFile(`${slicePath}/pids.max`, String(limits.pidsMax), 'utf8');
307
+ try {
308
+ await fsImpl.mkdir(slicePath, { recursive: true, mode: 0o755 });
309
+ await fsImpl.writeFile(
310
+ `${slicePath}/cgroup.subtree_control`,
311
+ wanted.map(name => `+${name}`).join(' '),
312
+ 'utf8',
313
+ );
314
+ const detected = resources || await detectHostResources({ readFileImpl: fsImpl.readFile });
315
+ const limits = buildSliceLimits({ cpuPercent, memoryPercent, pidsLimit }, detected);
316
+ if (wanted.includes('cpu') && limits.cpuQuotaUs > 0) {
317
+ await fsImpl.writeFile(`${slicePath}/cpu.max`, `${limits.cpuQuotaUs} ${limits.cpuPeriodUs}`, 'utf8');
318
+ }
319
+ if (wanted.includes('memory') && limits.memoryMaxBytes > 0) {
320
+ await fsImpl.writeFile(`${slicePath}/memory.max`, String(limits.memoryMaxBytes), 'utf8');
321
+ // No swap escape hatch: the 70% RAM hard cap is the whole memory budget.
322
+ await fsImpl.writeFile(`${slicePath}/memory.swap.max`, '0', 'utf8');
323
+ }
324
+ if (wanted.includes('pids') && limits.pidsMax > 0) {
325
+ await fsImpl.writeFile(`${slicePath}/pids.max`, String(limits.pidsMax), 'utf8');
326
+ }
327
+ const marker = {
328
+ slice,
329
+ cpuQuotaUs: limits.cpuQuotaUs,
330
+ cpuPeriodUs: limits.cpuPeriodUs,
331
+ memoryMaxBytes: limits.memoryMaxBytes,
332
+ pidsMax: limits.pidsMax,
333
+ updatedAt: new Date().toISOString(),
334
+ };
335
+ await fsImpl.writeFile(`${slicePath}/${SLICE_READY_MARKER}`, JSON.stringify(marker, null, 2), 'utf8');
336
+ return { slice, limits, controllers: wanted };
337
+ } catch (error) {
338
+ throw wrapCgroupWriteError(error, slicePath);
307
339
  }
308
- const marker = {
309
- slice,
310
- cpuQuotaUs: limits.cpuQuotaUs,
311
- cpuPeriodUs: limits.cpuPeriodUs,
312
- memoryMaxBytes: limits.memoryMaxBytes,
313
- pidsMax: limits.pidsMax,
314
- updatedAt: new Date().toISOString(),
315
- };
316
- await fsImpl.writeFile(`${slicePath}/${SLICE_READY_MARKER}`, JSON.stringify(marker, null, 2), 'utf8');
317
- return { slice, limits, controllers: wanted };
318
340
  }
319
341
 
320
342
  /**
@@ -141,9 +141,10 @@ export const CONFIG = {
141
141
  image: process.env.SANDBOX_AGENT_IMAGE || 'ghcr.io/yeaft/yeaft-web-code-agent-agent:dev',
142
142
  serverUrl: process.env.SANDBOX_SERVER_URL || '',
143
143
  stateDir: process.env.SANDBOX_STATE_DIR || join(homedir(), '.yeaft', 'container-agents'),
144
- // Shared cgroup slice (created by `yeaft-agent container setup-limits` on the
145
- // Host) that caps the sum of all sandbox containers' CPU/memory usage. Empty
146
- // means no cgroup-parent is passed and sandbox containers are unprotected.
144
+ // Shared cgroup slice (created by the absolute Node/CLI command printed by
145
+ // `yeaft-agent container setup-limits` on the Host) that caps the sum of all
146
+ // sandbox containers' CPU/memory usage. Empty means no cgroup-parent is
147
+ // passed and sandbox containers are unprotected.
147
148
  cgroupParent: process.env.SANDBOX_CGROUP_PARENT || ''
148
149
  },
149
150
 
@@ -1 +1 @@
1
- {"version":"1.0.442"}
1
+ {"version":"1.0.444"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.442",
3
+ "version": "1.0.444",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",