@yeaft/webchat-agent 1.0.443 → 1.0.445
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 +5 -3
- package/connection/message-router.js +5 -1
- package/container-cli.js +30 -12
- package/container-manager.js +52 -30
- package/index.js +44 -35
- package/local-runtime/agent/container-manager.js +52 -30
- package/local-runtime/server/config.js +4 -3
- package/local-runtime/server/handlers/agent-output.js +17 -0
- package/local-runtime/server/handlers/client-conversation.js +24 -1
- package/local-runtime/server/yeaft-managed-skill-capability.js +11 -0
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +160 -98
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/startup-maintenance.js +52 -0
- package/yeaft/plugins.js +25 -11
- package/yeaft/skills.js +308 -8
- package/yeaft/web-bridge.js +122 -3
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
|
|
79
|
-
|
|
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
|
|
83
|
+
console.error(`Container Agent failed: ${formatContainerError(error)}`);
|
|
82
84
|
process.exit(1);
|
|
83
85
|
}
|
|
84
86
|
} else if (command === 'upgrade') {
|
|
@@ -30,7 +30,7 @@ import { getLlmConfig, updateLlmConfig, getYeaftSettings, updateYeaftSettings, g
|
|
|
30
30
|
import { loadConfig } from '../yeaft/config.js';
|
|
31
31
|
import { discoverLlmModels } from '../llm-model-discovery.js';
|
|
32
32
|
import { fetchModelsDev } from '../yeaft/llm/models-dev.js';
|
|
33
|
-
import { handleYeaftSessionSend, handleYeaftAskUserAnswer, handleYeaftSubAgentPrompt, handleYeaftTaskCancel, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, refreshLiveSessionConfig, handleYeaftLoadHistory, handleYeaftLoadHistoryOutline, handleYeaftSearchHistory, handleYeaftLoadHistoryWindow, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftProjectContextSync, handleYeaftProjectMutation, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, handleYeaftPluginCatalog, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager, broadcastYeaftVpSnapshotEager, preloadYeaftSkillSlashCommands } from '../yeaft/web-bridge.js';
|
|
33
|
+
import { handleYeaftSessionSend, handleYeaftAskUserAnswer, handleYeaftSubAgentPrompt, handleYeaftTaskCancel, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, refreshLiveSessionConfig, handleYeaftLoadHistory, handleYeaftLoadHistoryOutline, handleYeaftSearchHistory, handleYeaftLoadHistoryWindow, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftProjectContextSync, handleYeaftProjectMutation, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, handleYeaftPluginCatalog, handleYeaftManagedSkill, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager, broadcastYeaftVpSnapshotEager, preloadYeaftSkillSlashCommands } from '../yeaft/web-bridge.js';
|
|
34
34
|
import { startYeaftStatusRefresh, forceRefreshYeaftStatus } from '../yeaft/status-cache.js';
|
|
35
35
|
import { handleWorkCenterRequest } from '../yeaft/work-center/bridge.js';
|
|
36
36
|
import { handleBrowserRuntimeMessage } from '../browser-runtime/messages.js';
|
|
@@ -639,6 +639,10 @@ export async function handleMessage(msg) {
|
|
|
639
639
|
handleYeaftPluginCatalog(msg);
|
|
640
640
|
break;
|
|
641
641
|
|
|
642
|
+
case 'yeaft_managed_skill':
|
|
643
|
+
handleYeaftManagedSkill(msg);
|
|
644
|
+
break;
|
|
645
|
+
|
|
642
646
|
case 'yeaft_vp_create':
|
|
643
647
|
case 'unify_vp_create':
|
|
644
648
|
handleYeaftVpCreate(msg);
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
const
|
|
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(
|
|
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
|
|
212
|
+
console.error(`Container Agent failed: ${formatContainerError(error)}`);
|
|
195
213
|
process.exitCode = 1;
|
|
196
214
|
});
|
|
197
215
|
}
|
package/container-manager.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
@@ -6,8 +6,6 @@ import { homedir } from 'os';
|
|
|
6
6
|
import { createAssetOutbox } from './yeaft/asset-outbox.js';
|
|
7
7
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, chmodSync, readdirSync } from 'fs';
|
|
8
8
|
import { join, dirname } from 'path';
|
|
9
|
-
import { exec } from 'child_process';
|
|
10
|
-
import { promisify } from 'util';
|
|
11
9
|
import { fileURLToPath } from 'url';
|
|
12
10
|
import ctx from './context.js';
|
|
13
11
|
import {
|
|
@@ -33,14 +31,12 @@ import {
|
|
|
33
31
|
runAfterManagedCliRuntimeCleanup,
|
|
34
32
|
summarizeManagedCliResults,
|
|
35
33
|
} from './yeaft/managed-cli.js';
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return execAsync(command, { ...options, windowsHide: true });
|
|
43
|
-
}
|
|
34
|
+
import {
|
|
35
|
+
execHiddenFileAsync,
|
|
36
|
+
resolveNpmInvocation,
|
|
37
|
+
shouldRunPeriodicCheck,
|
|
38
|
+
writePeriodicCheckMarker,
|
|
39
|
+
} from './startup-maintenance.js';
|
|
44
40
|
|
|
45
41
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
46
42
|
|
|
@@ -177,7 +173,7 @@ async function detectCapabilities() {
|
|
|
177
173
|
// agent build can speak plaintext WS frames. New servers see this and
|
|
178
174
|
// flip `agent.encryptOutbound = false`, stopping outbound encryption
|
|
179
175
|
// to this peer. Old servers ignore the unknown capability token.
|
|
180
|
-
const capabilities = ['background_tasks', 'file_editor', 'ping_session', 'plaintext-ok', 'workbench_session_routes', SAFE_REMOTE_UPGRADE_CAPABILITY, 'work_center', 'work_center_message_v2', 'session_history_search', 'session_history_outline', 'session_history_window_prefetch', 'file_reference_resolution', 'yeaft_plugins'];
|
|
176
|
+
const capabilities = ['background_tasks', 'file_editor', 'ping_session', 'plaintext-ok', 'workbench_session_routes', SAFE_REMOTE_UPGRADE_CAPABILITY, 'work_center', 'work_center_message_v2', 'session_history_search', 'session_history_outline', 'session_history_window_prefetch', 'file_reference_resolution', 'yeaft_plugins', 'yeaft_managed_skills'];
|
|
181
177
|
if (process.platform === 'linux') capabilities.push('work_item_attachments');
|
|
182
178
|
const pty = await loadNodePty();
|
|
183
179
|
if (pty) capabilities.push('terminal');
|
|
@@ -199,7 +195,8 @@ async function ensureDependencies() {
|
|
|
199
195
|
if (!existsSync(nodeModulesPath)) {
|
|
200
196
|
console.log('[Startup] node_modules not found, running npm install...');
|
|
201
197
|
try {
|
|
202
|
-
|
|
198
|
+
const npm = resolveNpmInvocation();
|
|
199
|
+
await execHiddenFileAsync(npm.command, [...npm.argsPrefix, 'install'], { cwd: agentDir, timeout: 120000 });
|
|
203
200
|
console.log('[Startup] npm install completed');
|
|
204
201
|
} catch (e) {
|
|
205
202
|
console.warn('[Startup] npm install failed:', e.message);
|
|
@@ -222,6 +219,7 @@ async function ensureYeaftSkills() {
|
|
|
222
219
|
const installedFile = join(pluginsDir, 'installed_plugins.json');
|
|
223
220
|
const settingsFile = join(claudeDir, 'settings.json');
|
|
224
221
|
const cacheDir = join(pluginsDir, 'cache', MARKETPLACE_NAME, PLUGIN_NAME, PLUGIN_VERSION);
|
|
222
|
+
const updateCheckMarker = join(pluginsDir, '.yeaft-skills-update-check.json');
|
|
225
223
|
|
|
226
224
|
try {
|
|
227
225
|
// --- Layer 1: Clone or update the marketplace repo ---
|
|
@@ -230,38 +228,49 @@ async function ensureYeaftSkills() {
|
|
|
230
228
|
if (!existsSync(installDir)) {
|
|
231
229
|
console.log('[Startup] yeaft-skills not found, installing as marketplace plugin...');
|
|
232
230
|
mkdirSync(marketplacesDir, { recursive: true });
|
|
233
|
-
await
|
|
231
|
+
await execHiddenFileAsync('git', ['clone', REPO_URL, installDir], { timeout: 60000 });
|
|
232
|
+
writePeriodicCheckMarker(updateCheckMarker);
|
|
234
233
|
needsCacheUpdate = true;
|
|
235
234
|
console.log('[Startup] yeaft-skills installed');
|
|
236
235
|
} else {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const { stdout: h } = await execHiddenAsync('git rev-parse HEAD', { cwd: installDir, timeout: 5000 });
|
|
242
|
-
headBefore = h.trim();
|
|
243
|
-
} catch { /* ignore */ }
|
|
244
|
-
const { stdout } = await execHiddenAsync('git pull --ff-only', {
|
|
245
|
-
cwd: installDir,
|
|
246
|
-
timeout: 30000
|
|
247
|
-
});
|
|
248
|
-
if (stdout.includes('Already up to date')) {
|
|
249
|
-
console.log('[Startup] yeaft-skills is up to date');
|
|
250
|
-
} else {
|
|
251
|
-
needsCacheUpdate = true;
|
|
252
|
-
console.log('[Startup] yeaft-skills updated');
|
|
253
|
-
}
|
|
254
|
-
// Double-check: compare HEAD after pull
|
|
255
|
-
if (!needsCacheUpdate && headBefore) {
|
|
236
|
+
if (shouldRunPeriodicCheck(updateCheckMarker)) {
|
|
237
|
+
console.log('[Startup] yeaft-skills found, checking for updates...');
|
|
238
|
+
// Record HEAD before pull to detect changes
|
|
239
|
+
let headBefore = '';
|
|
256
240
|
try {
|
|
257
|
-
const { stdout:
|
|
258
|
-
|
|
241
|
+
const { stdout: h } = await execHiddenFileAsync('git', ['rev-parse', 'HEAD'], { cwd: installDir, timeout: 5000 });
|
|
242
|
+
headBefore = h.trim();
|
|
259
243
|
} catch { /* ignore */ }
|
|
244
|
+
try {
|
|
245
|
+
const { stdout } = await execHiddenFileAsync('git', ['pull', '--ff-only'], {
|
|
246
|
+
cwd: installDir,
|
|
247
|
+
timeout: 30000
|
|
248
|
+
});
|
|
249
|
+
if (stdout.includes('Already up to date')) {
|
|
250
|
+
console.log('[Startup] yeaft-skills is up to date');
|
|
251
|
+
} else {
|
|
252
|
+
needsCacheUpdate = true;
|
|
253
|
+
console.log('[Startup] yeaft-skills updated');
|
|
254
|
+
}
|
|
255
|
+
writePeriodicCheckMarker(updateCheckMarker);
|
|
256
|
+
} catch (e) {
|
|
257
|
+
writePeriodicCheckMarker(updateCheckMarker);
|
|
258
|
+
console.warn('[Startup] yeaft-skills update check failed; using cached checkout:', e.message);
|
|
259
|
+
}
|
|
260
|
+
// Double-check: compare HEAD after pull
|
|
261
|
+
if (!needsCacheUpdate && headBefore) {
|
|
262
|
+
try {
|
|
263
|
+
const { stdout: h2 } = await execHiddenFileAsync('git', ['rev-parse', 'HEAD'], { cwd: installDir, timeout: 5000 });
|
|
264
|
+
if (h2.trim() !== headBefore) needsCacheUpdate = true;
|
|
265
|
+
} catch { /* ignore */ }
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
console.log('[Startup] yeaft-skills update check skipped; recently checked');
|
|
260
269
|
}
|
|
261
270
|
}
|
|
262
271
|
// Get current commit SHA for installed_plugins.json
|
|
263
272
|
try {
|
|
264
|
-
const { stdout: sha } = await
|
|
273
|
+
const { stdout: sha } = await execHiddenFileAsync('git', ['rev-parse', 'HEAD'], { cwd: installDir, timeout: 5000 });
|
|
265
274
|
gitSha = sha.trim();
|
|
266
275
|
} catch { /* non-critical */ }
|
|
267
276
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
145
|
-
// Host) that caps the sum of all
|
|
146
|
-
// means no cgroup-parent is
|
|
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
|
|
|
@@ -882,6 +882,23 @@ export async function handleAgentOutput(agentId, agent, msg) {
|
|
|
882
882
|
break;
|
|
883
883
|
}
|
|
884
884
|
|
|
885
|
+
case 'yeaft_managed_skill_result': {
|
|
886
|
+
const targetClient = msg._requestClientId ? webClients.get(msg._requestClientId) : null;
|
|
887
|
+
if (targetClient?.authenticated && (CONFIG.skipAuth || targetClient.userId === agent.ownerId)) {
|
|
888
|
+
await sendToWebClient(targetClient, {
|
|
889
|
+
type: 'yeaft_managed_skill_result',
|
|
890
|
+
agentId,
|
|
891
|
+
requestId: msg.requestId || null,
|
|
892
|
+
scope: msg.scope || null,
|
|
893
|
+
sessionId: msg.sessionId || null,
|
|
894
|
+
result: msg.result || null,
|
|
895
|
+
catalog: msg.catalog || { tools: [], skills: [], skillSources: [], mcpServers: [] },
|
|
896
|
+
error: msg.error || null,
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
|
|
885
902
|
case 'yeaft_tool_stats':
|
|
886
903
|
// 2026-05-13: relay tool-call counters from the agent to the web
|
|
887
904
|
// client that requested them. Whitelist `snapshot`/`registered`/
|
|
@@ -31,6 +31,10 @@ import {
|
|
|
31
31
|
agentSupportsYeaftPlugins,
|
|
32
32
|
YEAFT_PLUGINS_UNSUPPORTED_ERROR,
|
|
33
33
|
} from '../yeaft-plugin-capability.js';
|
|
34
|
+
import {
|
|
35
|
+
agentSupportsYeaftManagedSkills,
|
|
36
|
+
YEAFT_MANAGED_SKILLS_UNSUPPORTED_ERROR,
|
|
37
|
+
} from '../yeaft-managed-skill-capability.js';
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
function isRetiredCollabSessionId(id) {
|
|
@@ -56,6 +60,14 @@ function emptyYeaftPluginCatalog(error = null) {
|
|
|
56
60
|
};
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
function emptyYeaftManagedSkillResult(error = null) {
|
|
64
|
+
return {
|
|
65
|
+
type: 'yeaft_managed_skill_result',
|
|
66
|
+
catalog: { tools: [], skills: [], skillSources: [], mcpServers: [] },
|
|
67
|
+
...(error ? { error } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
async function sendVpSnapshotError(client, msg, error) {
|
|
60
72
|
await sendToWebClient(client, {
|
|
61
73
|
type: 'yeaft_output',
|
|
@@ -1650,6 +1662,15 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
1650
1662
|
});
|
|
1651
1663
|
return true;
|
|
1652
1664
|
}
|
|
1665
|
+
if (relayType === 'yeaft_managed_skill'
|
|
1666
|
+
&& !agentSupportsYeaftManagedSkills(agents.get(relayAgentId))) {
|
|
1667
|
+
await sendToWebClient(client, {
|
|
1668
|
+
...emptyYeaftManagedSkillResult(YEAFT_MANAGED_SKILLS_UNSUPPORTED_ERROR),
|
|
1669
|
+
agentId: relayAgentId,
|
|
1670
|
+
requestId: msg.requestId || null,
|
|
1671
|
+
});
|
|
1672
|
+
return true;
|
|
1673
|
+
}
|
|
1653
1674
|
const relayAgent = agents.get(relayAgentId);
|
|
1654
1675
|
if (!relayAgent || relayAgent.ws?.readyState !== 1) {
|
|
1655
1676
|
if (relayType === 'yeaft_fetch_tool_stats') {
|
|
@@ -1676,7 +1697,9 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
1676
1697
|
// Direct catalog replies are request-scoped. Carry the browser client
|
|
1677
1698
|
// identity through the Agent so another owner tab cannot accidentally
|
|
1678
1699
|
// consume a response for this picker request.
|
|
1679
|
-
if (relayType === 'yeaft_plugin_catalog'
|
|
1700
|
+
if (relayType === 'yeaft_plugin_catalog' || relayType === 'yeaft_managed_skill') {
|
|
1701
|
+
rest._requestClientId = clientId;
|
|
1702
|
+
}
|
|
1680
1703
|
if (rest.type === 'yeaft_session_send' || rest.type === 'yeaft_session_chat') {
|
|
1681
1704
|
trackUserTurn(client.userId, Buffer.byteLength(JSON.stringify(msg)));
|
|
1682
1705
|
const sessionId = typeof rest.sessionId === 'string' ? rest.sessionId.trim() : '';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { YEAFT_PLUGINS_CAPABILITY } from './yeaft-plugin-capability.js';
|
|
2
|
+
|
|
3
|
+
export const YEAFT_MANAGED_SKILLS_CAPABILITY = 'yeaft_managed_skills';
|
|
4
|
+
export const YEAFT_MANAGED_SKILLS_UNSUPPORTED_ERROR = 'The selected Agent does not support Skill management; upgrade and restart the Agent';
|
|
5
|
+
|
|
6
|
+
export function agentSupportsYeaftManagedSkills(agent) {
|
|
7
|
+
if (agent?.capabilityMetadataProvided !== true) return false;
|
|
8
|
+
const capabilities = Array.isArray(agent.capabilities) ? agent.capabilities : [];
|
|
9
|
+
return capabilities.includes(YEAFT_PLUGINS_CAPABILITY)
|
|
10
|
+
&& capabilities.includes(YEAFT_MANAGED_SKILLS_CAPABILITY);
|
|
11
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.445"}
|