@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 +57 -8
- package/local-runtime/server/container-agent-service.js +26 -0
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +3 -2
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +1 -1
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
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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.
|
|
1
|
+
{"version":"1.0.439"}
|