@yeaft/webchat-agent 1.0.444 → 1.0.446
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/Dockerfile +5 -2
- package/cli.js +9 -0
- package/connection/message-router.js +5 -1
- package/connection/upgrade.js +18 -0
- package/container-manager.js +1 -0
- package/index.js +48 -36
- package/local-runtime/agent/container-manager.js +1 -0
- package/local-runtime/server/handlers/agent-output.js +17 -0
- package/local-runtime/server/handlers/client-conversation.js +24 -1
- package/local-runtime/server/handlers/client-misc.js +15 -0
- 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/upgrade-command.js +19 -0
- package/yeaft/plugins.js +25 -11
- package/yeaft/skills.js +308 -8
- package/yeaft/web-bridge.js +122 -3
|
Binary file
|
package/package.json
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { resolveWindowsNpmCliPath } from './upgrade-command.js';
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
export const YEAFT_SKILLS_UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
export function execHiddenFileAsync(command, args = [], options = {}) {
|
|
11
|
+
return execFileAsync(command, args, {
|
|
12
|
+
...options,
|
|
13
|
+
shell: false,
|
|
14
|
+
windowsHide: true,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveNpmInvocation({
|
|
19
|
+
platform = process.platform,
|
|
20
|
+
nodePath = process.execPath,
|
|
21
|
+
resolveWindowsNpmCliPathFn = resolveWindowsNpmCliPath,
|
|
22
|
+
} = {}) {
|
|
23
|
+
if (platform === 'win32') {
|
|
24
|
+
const npmCliPath = resolveWindowsNpmCliPathFn(nodePath);
|
|
25
|
+
if (!npmCliPath) {
|
|
26
|
+
throw new Error('npm JavaScript CLI entry point could not be resolved');
|
|
27
|
+
}
|
|
28
|
+
return { command: nodePath, argsPrefix: [npmCliPath] };
|
|
29
|
+
}
|
|
30
|
+
return { command: 'npm', argsPrefix: [] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function shouldRunPeriodicCheck(
|
|
34
|
+
markerPath,
|
|
35
|
+
intervalMs = YEAFT_SKILLS_UPDATE_CHECK_INTERVAL_MS,
|
|
36
|
+
now = Date.now,
|
|
37
|
+
) {
|
|
38
|
+
try {
|
|
39
|
+
const checkedAt = JSON.parse(readFileSync(markerPath, 'utf8'))?.checkedAt;
|
|
40
|
+
return !Number.isFinite(checkedAt) || now() - checkedAt >= intervalMs;
|
|
41
|
+
} catch {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function writePeriodicCheckMarker(markerPath, now = Date.now) {
|
|
47
|
+
const checkedAt = now();
|
|
48
|
+
writeFileSync(markerPath, JSON.stringify({
|
|
49
|
+
checkedAt,
|
|
50
|
+
checkedAtIso: new Date(checkedAt).toISOString(),
|
|
51
|
+
}, null, 2));
|
|
52
|
+
}
|
package/upgrade-command.js
CHANGED
|
@@ -5,6 +5,25 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
5
5
|
|
|
6
6
|
export const DEFAULT_UPGRADE_REGISTRY = 'https://pkg.yeaft.com/';
|
|
7
7
|
export const SAFE_REMOTE_UPGRADE_CAPABILITY = 'remote_upgrade_safe';
|
|
8
|
+
export const CONTAINER_AGENT_CAPABILITY = 'container_agent';
|
|
9
|
+
export const CONTAINER_IMAGE_UPGRADE_REASON = 'container_image_upgrade_required';
|
|
10
|
+
|
|
11
|
+
export function isContainerAgentRuntime(env = process.env) {
|
|
12
|
+
return String(env?.YEAFT_AGENT_RUNTIME || '').trim() === CONTAINER_AGENT_CAPABILITY;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getAgentUpgradeCapability(env = process.env) {
|
|
16
|
+
return isContainerAgentRuntime(env) ? CONTAINER_AGENT_CAPABILITY : SAFE_REMOTE_UPGRADE_CAPABILITY;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildContainerImageUpgradeMessage(version = null) {
|
|
20
|
+
const subject = version ? `Container Agent v${version}` : 'This Container Agent';
|
|
21
|
+
return `${subject} is managed as a Docker image and cannot upgrade itself with npm. ` +
|
|
22
|
+
'On the Docker Host, run "docker pull <configured-agent-image>", then recreate the same container ' +
|
|
23
|
+
'through the existing Server/Sandbox lifecycle or the original "yeaft-agent container install" command. ' +
|
|
24
|
+
'Keep the existing /home/yeaft/.yeaft and /workspace persistent volumes and reuse the original host-side ' +
|
|
25
|
+
'0600 agent-secret file; do not remove those volumes during recreation.';
|
|
26
|
+
}
|
|
8
27
|
|
|
9
28
|
const WINDOWS_UPGRADE_LOCK_NAME = 'active.lock';
|
|
10
29
|
|
package/yeaft/plugins.js
CHANGED
|
@@ -133,16 +133,30 @@ export function buildPluginCatalog({ toolRegistry, skillManager, mcpConfig, mcpM
|
|
|
133
133
|
.sort((a, b) => a.label.localeCompare(b.label))
|
|
134
134
|
: [];
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
.
|
|
145
|
-
|
|
136
|
+
// Runtime authorization is keyed by Skill name, so this catalog must expose
|
|
137
|
+
// only the effective layered entry per name. Source-level rows belong to the
|
|
138
|
+
// management surface below; otherwise a project override would render two
|
|
139
|
+
// allowlist toggles that both mutate the same name.
|
|
140
|
+
const skills = (typeof skillManager?.list === 'function' ? skillManager.list() : [])
|
|
141
|
+
.map(skill => ({
|
|
142
|
+
id: skill.name,
|
|
143
|
+
label: skill.name,
|
|
144
|
+
description: skill.description || '',
|
|
145
|
+
category: skill.category || null,
|
|
146
|
+
tier: skill.tier || null,
|
|
147
|
+
}))
|
|
148
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
149
|
+
|
|
150
|
+
const skillSources = (typeof skillManager?.listSources === 'function' ? skillManager.listSources() : [])
|
|
151
|
+
.map(skill => ({
|
|
152
|
+
id: skill.id,
|
|
153
|
+
name: skill.name,
|
|
154
|
+
label: skill.name,
|
|
155
|
+
description: skill.description || '',
|
|
156
|
+
tier: skill.tier || null,
|
|
157
|
+
managed: skill.managed === true,
|
|
158
|
+
}))
|
|
159
|
+
.sort((a, b) => a.label.localeCompare(b.label) || String(a.id).localeCompare(String(b.id)));
|
|
146
160
|
|
|
147
161
|
const statusByName = new Map((mcpManager?.status?.() || [])
|
|
148
162
|
.map(status => [status.name, status]));
|
|
@@ -166,5 +180,5 @@ export function buildPluginCatalog({ toolRegistry, skillManager, mcpConfig, mcpM
|
|
|
166
180
|
})
|
|
167
181
|
.sort((a, b) => a.label.localeCompare(b.label));
|
|
168
182
|
|
|
169
|
-
return { tools, skills, mcpServers };
|
|
183
|
+
return { tools, skills, skillSources, mcpServers };
|
|
170
184
|
}
|
package/yeaft/skills.js
CHANGED
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
* Reference: yeaft-yeaft-design.md §8, yeaft-yeaft-core-systems.md
|
|
56
56
|
*/
|
|
57
57
|
|
|
58
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync, unlinkSync, mkdirSync, statSync, realpathSync } from 'fs';
|
|
58
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync, unlinkSync, mkdirSync, statSync, lstatSync, realpathSync } from 'fs';
|
|
59
59
|
import { join, basename, sep, dirname, resolve, delimiter } from 'path';
|
|
60
60
|
import { platform, homedir } from 'os';
|
|
61
61
|
import { fileURLToPath } from 'url';
|
|
@@ -247,6 +247,189 @@ function pathIsInside(childPath, parentPath) {
|
|
|
247
247
|
return child === parent || child.startsWith(parent + sep);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
const MANAGED_SKILL_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,79}$/;
|
|
251
|
+
const MAX_MANAGED_SKILL_TEXT_LENGTH = 64 * 1024;
|
|
252
|
+
|
|
253
|
+
function normalizeManagedSkillInput(input = {}) {
|
|
254
|
+
const name = typeof input.name === 'string' ? input.name.trim() : '';
|
|
255
|
+
if (!MANAGED_SKILL_NAME_RE.test(name)) {
|
|
256
|
+
throw new Error('skill name must start with a letter or digit and use only letters, digits, dots, dashes, or underscores');
|
|
257
|
+
}
|
|
258
|
+
const description = typeof input.description === 'string' ? input.description.trim() : '';
|
|
259
|
+
const trigger = typeof input.trigger === 'string' ? input.trigger.trim() : '';
|
|
260
|
+
const content = typeof input.content === 'string' ? input.content.trim() : '';
|
|
261
|
+
if (!description) throw new Error('skill description is required');
|
|
262
|
+
if (!content) throw new Error('skill instructions are required');
|
|
263
|
+
if (/[\r\n]/.test(description) || /[\r\n]/.test(trigger)) {
|
|
264
|
+
throw new Error('skill description and trigger must be single-line text');
|
|
265
|
+
}
|
|
266
|
+
if ([description, trigger, content].some(value => value.length > MAX_MANAGED_SKILL_TEXT_LENGTH)) {
|
|
267
|
+
throw new Error('skill fields exceed the maximum allowed length');
|
|
268
|
+
}
|
|
269
|
+
return { name, description, trigger, content, mode: 'both' };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function assertNativeSkillRoot(rootDir, { create = true } = {}) {
|
|
273
|
+
const root = resolve(rootDir || '');
|
|
274
|
+
if (!root || root === resolve('.')) throw new Error('skill root is required');
|
|
275
|
+
if (existsSync(root)) {
|
|
276
|
+
const stat = lstatSync(root);
|
|
277
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('skill root must be a non-symbolic-link directory');
|
|
278
|
+
} else if (create) {
|
|
279
|
+
mkdirSync(root, { recursive: true });
|
|
280
|
+
} else {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
return realpathSync(root);
|
|
285
|
+
} catch {
|
|
286
|
+
throw new Error('skill root could not be resolved safely');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function managedSkillFilePath(rootDir, name, options) {
|
|
291
|
+
if (!MANAGED_SKILL_NAME_RE.test(name)) throw new Error('invalid skill name');
|
|
292
|
+
const root = assertNativeSkillRoot(rootDir, options);
|
|
293
|
+
if (!root) return null;
|
|
294
|
+
const filePath = resolve(root, `${name}.md`);
|
|
295
|
+
if (!pathIsInside(filePath, root) || filePath === root) throw new Error('invalid skill path');
|
|
296
|
+
if (existsSync(filePath)) {
|
|
297
|
+
let stat;
|
|
298
|
+
try {
|
|
299
|
+
stat = lstatSync(filePath);
|
|
300
|
+
} catch {
|
|
301
|
+
throw new Error('skill path could not be inspected safely');
|
|
302
|
+
}
|
|
303
|
+
if (stat.isSymbolicLink()) throw new Error('skill path must not be a symbolic link');
|
|
304
|
+
}
|
|
305
|
+
return filePath;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function isRemovableNativeSkill(skillPath, rootDir) {
|
|
309
|
+
if (!skillPath || !rootDir || !pathIsInside(skillPath, rootDir)) return false;
|
|
310
|
+
try {
|
|
311
|
+
const stat = lstatSync(skillPath);
|
|
312
|
+
return stat.isFile() && !stat.isSymbolicLink() && skillPath.toLowerCase().endsWith('.md');
|
|
313
|
+
} catch {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Create a native Yeaft single-file skill within an explicitly selected scope.
|
|
320
|
+
* This deliberately does not edit bundled or borrowed Claude/Codex sources.
|
|
321
|
+
*
|
|
322
|
+
* @param {string} rootDir
|
|
323
|
+
* @param {{ name: string, description: string, trigger?: string, content: string }} input
|
|
324
|
+
* @returns {{ name: string, path: string }}
|
|
325
|
+
*/
|
|
326
|
+
export function createManagedSkill(rootDir, input) {
|
|
327
|
+
const skill = normalizeManagedSkillInput(input);
|
|
328
|
+
const filePath = managedSkillFilePath(rootDir, skill.name);
|
|
329
|
+
if (existsSync(filePath)) throw new Error(`skill "${skill.name}" already exists in this scope`);
|
|
330
|
+
writeFileSync(filePath, serializeSkill(skill), { encoding: 'utf8', flag: 'wx' });
|
|
331
|
+
return { name: skill.name, path: filePath };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Remove only a native single-file skill from the selected Yeaft scope.
|
|
336
|
+
* Directory skills and borrowed/project compatibility roots are intentionally
|
|
337
|
+
* left untouched because they may own references and unrelated project files.
|
|
338
|
+
*
|
|
339
|
+
* @param {string} rootDir
|
|
340
|
+
* @param {string} rawName
|
|
341
|
+
* @returns {{ name: string, removed: boolean }}
|
|
342
|
+
*/
|
|
343
|
+
export function removeManagedSkill(rootDir, rawName) {
|
|
344
|
+
const name = typeof rawName === 'string' ? rawName.trim() : '';
|
|
345
|
+
const root = assertNativeSkillRoot(rootDir, { create: false });
|
|
346
|
+
if (!root) return { name, removed: false };
|
|
347
|
+
const filePath = managedSkillFilePath(root, name, { create: false });
|
|
348
|
+
if (!filePath || !existsSync(filePath)) return { name, removed: false };
|
|
349
|
+
if (!isRemovableNativeSkill(filePath, root)) {
|
|
350
|
+
throw new Error('only native single-file skills can be removed here');
|
|
351
|
+
}
|
|
352
|
+
unlinkSync(filePath);
|
|
353
|
+
return { name, removed: true };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function managedProjectDirectory(parent, name, { create }) {
|
|
357
|
+
const candidate = join(parent, name);
|
|
358
|
+
if (!pathIsInside(candidate, parent)) throw new Error('invalid managed project Skill path');
|
|
359
|
+
if (!existsSync(candidate)) {
|
|
360
|
+
if (!create) return null;
|
|
361
|
+
// Create a single verified segment at a time. Recursive mkdir would follow
|
|
362
|
+
// a repository-controlled `.yeaft` link before we could inspect it.
|
|
363
|
+
mkdirSync(candidate);
|
|
364
|
+
}
|
|
365
|
+
let stat;
|
|
366
|
+
try {
|
|
367
|
+
stat = lstatSync(candidate);
|
|
368
|
+
} catch {
|
|
369
|
+
throw new Error('managed project Skill path could not be inspected safely');
|
|
370
|
+
}
|
|
371
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
372
|
+
throw new Error('managed project Skill path must not contain a symbolic link');
|
|
373
|
+
}
|
|
374
|
+
let canonical;
|
|
375
|
+
try {
|
|
376
|
+
canonical = realpathSync(candidate);
|
|
377
|
+
} catch {
|
|
378
|
+
throw new Error('managed project Skill path could not be resolved safely');
|
|
379
|
+
}
|
|
380
|
+
if (!pathIsInside(canonical, parent) || canonical === parent) {
|
|
381
|
+
throw new Error('managed project Skill path escapes the project directory');
|
|
382
|
+
}
|
|
383
|
+
return canonical;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Resolve `<canonical workDir>/.yeaft/skills` without following any
|
|
388
|
+
* repository-controlled path segment. `workDir` itself is canonicalized first,
|
|
389
|
+
* so a configured workspace alias still identifies its real project root.
|
|
390
|
+
*
|
|
391
|
+
* @param {string} workDir
|
|
392
|
+
* @param {{ create?: boolean }} [options]
|
|
393
|
+
* @returns {string|null}
|
|
394
|
+
*/
|
|
395
|
+
export function resolveManagedProjectSkillRoot(workDir, { create = true } = {}) {
|
|
396
|
+
const lexicalWorkDir = resolve(workDir || '');
|
|
397
|
+
if (!workDir || lexicalWorkDir === resolve('.')) {
|
|
398
|
+
throw new Error('project directory is required for project-level skills');
|
|
399
|
+
}
|
|
400
|
+
let projectRoot;
|
|
401
|
+
try {
|
|
402
|
+
projectRoot = realpathSync(lexicalWorkDir);
|
|
403
|
+
} catch {
|
|
404
|
+
throw new Error('project directory could not be resolved safely');
|
|
405
|
+
}
|
|
406
|
+
let projectStat;
|
|
407
|
+
try {
|
|
408
|
+
projectStat = lstatSync(projectRoot);
|
|
409
|
+
} catch {
|
|
410
|
+
throw new Error('project directory could not be inspected safely');
|
|
411
|
+
}
|
|
412
|
+
if (!projectStat.isDirectory() || projectStat.isSymbolicLink()) {
|
|
413
|
+
throw new Error('project directory must be a non-symbolic-link directory');
|
|
414
|
+
}
|
|
415
|
+
const yeaftRoot = managedProjectDirectory(projectRoot, '.yeaft', { create });
|
|
416
|
+
if (!yeaftRoot) return null;
|
|
417
|
+
return managedProjectDirectory(yeaftRoot, 'skills', { create });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Create a native Skill in a verified project-level Yeaft root. */
|
|
421
|
+
export function createManagedProjectSkill(workDir, input) {
|
|
422
|
+
const root = resolveManagedProjectSkillRoot(workDir, { create: true });
|
|
423
|
+
return createManagedSkill(root, input);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Remove a native Skill from a verified project-level Yeaft root. */
|
|
427
|
+
export function removeManagedProjectSkill(workDir, name) {
|
|
428
|
+
const root = resolveManagedProjectSkillRoot(workDir, { create: false });
|
|
429
|
+
if (!root) return { name: typeof name === 'string' ? name.trim() : '', removed: false };
|
|
430
|
+
return removeManagedSkill(root, name);
|
|
431
|
+
}
|
|
432
|
+
|
|
250
433
|
function shouldIgnorePath(candidatePath, ignorePaths) {
|
|
251
434
|
return ignorePaths.some(ignorePath => pathIsInside(candidatePath, ignorePath));
|
|
252
435
|
}
|
|
@@ -333,8 +516,83 @@ function discoverSkills(rootDir, subPath = '', opts = {}) {
|
|
|
333
516
|
return { skills, errors };
|
|
334
517
|
}
|
|
335
518
|
|
|
519
|
+
function secureWorkspacePath(workspaceRoot, relativePath, type) {
|
|
520
|
+
const lexicalRoot = resolve(workspaceRoot);
|
|
521
|
+
const lexicalTarget = resolve(lexicalRoot, relativePath);
|
|
522
|
+
if (!pathIsInside(lexicalTarget, lexicalRoot) || lexicalTarget === lexicalRoot) return null;
|
|
523
|
+
|
|
524
|
+
let canonicalCurrent;
|
|
525
|
+
let expectedCurrent;
|
|
526
|
+
let current = lexicalRoot;
|
|
527
|
+
try {
|
|
528
|
+
const rootStat = lstatSync(current);
|
|
529
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) return null;
|
|
530
|
+
canonicalCurrent = realpathSync(current);
|
|
531
|
+
expectedCurrent = canonicalCurrent;
|
|
532
|
+
for (const component of relativePath.split(/[\\/]+/)) {
|
|
533
|
+
if (!component || component === '.' || component === '..') return null;
|
|
534
|
+
current = join(current, component);
|
|
535
|
+
expectedCurrent = join(expectedCurrent, component);
|
|
536
|
+
const stat = lstatSync(current);
|
|
537
|
+
if (stat.isSymbolicLink() || realpathSync(current) !== expectedCurrent) return null;
|
|
538
|
+
if (current !== lexicalTarget && !stat.isDirectory()) return null;
|
|
539
|
+
if (current === lexicalTarget && ((type === 'directory' && !stat.isDirectory()) || (type === 'file' && !stat.isFile()))) {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return current;
|
|
544
|
+
} catch {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function listSecureWorkspaceDirectory(workspaceRoot, relativePath) {
|
|
550
|
+
const dir = secureWorkspacePath(workspaceRoot, relativePath, 'directory');
|
|
551
|
+
if (!dir) return null;
|
|
552
|
+
try {
|
|
553
|
+
return readdirSync(dir, { withFileTypes: true }).map(entry => ({
|
|
554
|
+
name: entry.name,
|
|
555
|
+
isFile: entry.isFile(),
|
|
556
|
+
isDirectory: entry.isDirectory(),
|
|
557
|
+
isSymbolicLink: entry.isSymbolicLink() || entry.isBlockDevice() || entry.isCharacterDevice()
|
|
558
|
+
|| entry.isFIFO() || entry.isSocket(),
|
|
559
|
+
}));
|
|
560
|
+
} catch {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function readSecureWorkspaceFile(workspaceRoot, relativePath) {
|
|
566
|
+
const file = secureWorkspacePath(workspaceRoot, relativePath, 'file');
|
|
567
|
+
if (!file) return null;
|
|
568
|
+
try {
|
|
569
|
+
if (lstatSync(file).size > MAX_PROJECT_SKILL_BYTES) {
|
|
570
|
+
return { truncated: true, buffer: Buffer.alloc(0) };
|
|
571
|
+
}
|
|
572
|
+
const buffer = readFileSync(file);
|
|
573
|
+
if (secureWorkspacePath(workspaceRoot, relativePath, 'file') !== file) return null;
|
|
574
|
+
return buffer.length > MAX_PROJECT_SKILL_BYTES
|
|
575
|
+
? { truncated: true, buffer: Buffer.alloc(0) }
|
|
576
|
+
: { truncated: false, buffer };
|
|
577
|
+
} catch {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function listSecureWorkspaceEntries(workspaceRoot, relativePath) {
|
|
583
|
+
const entries = listWorkspaceDirectory(workspaceRoot, relativePath);
|
|
584
|
+
if (entries || process.platform === 'linux') return entries;
|
|
585
|
+
return listSecureWorkspaceDirectory(workspaceRoot, relativePath);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function readSecureWorkspaceSkillFile(workspaceRoot, relativePath) {
|
|
589
|
+
const read = readWorkspaceFile(workspaceRoot, relativePath, { maxBytes: MAX_PROJECT_SKILL_BYTES });
|
|
590
|
+
if (read || process.platform === 'linux') return read;
|
|
591
|
+
return readSecureWorkspaceFile(workspaceRoot, relativePath);
|
|
592
|
+
}
|
|
593
|
+
|
|
336
594
|
function listWorkspaceSkillFiles(workspaceRoot, relativeDir) {
|
|
337
|
-
return (
|
|
595
|
+
return (listSecureWorkspaceEntries(workspaceRoot, relativeDir) || [])
|
|
338
596
|
.filter(entry => entry.isFile && !entry.isSymbolicLink)
|
|
339
597
|
.map(entry => entry.name);
|
|
340
598
|
}
|
|
@@ -343,15 +601,16 @@ function discoverWorkspaceSkills(workspaceRoot, skillsRelativeRoot, subPath = ''
|
|
|
343
601
|
const skills = [];
|
|
344
602
|
const errors = [];
|
|
345
603
|
const relativeDir = subPath ? join(skillsRelativeRoot, subPath) : skillsRelativeRoot;
|
|
346
|
-
const entries =
|
|
604
|
+
const entries = listSecureWorkspaceEntries(workspaceRoot, relativeDir);
|
|
347
605
|
if (!entries) return { skills, errors };
|
|
606
|
+
const readFile = relativePath => readSecureWorkspaceSkillFile(workspaceRoot, relativePath);
|
|
348
607
|
|
|
349
608
|
for (const entry of entries) {
|
|
350
609
|
if (entry.isSymbolicLink) continue;
|
|
351
610
|
const relPath = subPath ? join(subPath, entry.name) : entry.name;
|
|
352
611
|
const workspacePath = join(skillsRelativeRoot, relPath);
|
|
353
612
|
if (entry.isFile && entry.name.endsWith('.md') && entry.name !== 'SKILL.md') {
|
|
354
|
-
const read =
|
|
613
|
+
const read = readFile(workspacePath);
|
|
355
614
|
if (!read || read.truncated) continue;
|
|
356
615
|
const skill = parseSkill(read.buffer.toString('utf8'), entry.name);
|
|
357
616
|
if (!skill?.name) {
|
|
@@ -370,13 +629,13 @@ function discoverWorkspaceSkills(workspaceRoot, skillsRelativeRoot, subPath = ''
|
|
|
370
629
|
}
|
|
371
630
|
if (!entry.isDirectory) continue;
|
|
372
631
|
|
|
373
|
-
const childEntries =
|
|
632
|
+
const childEntries = listSecureWorkspaceEntries(workspaceRoot, workspacePath);
|
|
374
633
|
if (!childEntries) continue;
|
|
375
634
|
const skillFile = childEntries.find(child => child.name === 'SKILL.md');
|
|
376
635
|
if (skillFile) {
|
|
377
636
|
if (!skillFile.isFile || skillFile.isSymbolicLink) continue;
|
|
378
637
|
const skillRelativePath = join(workspacePath, 'SKILL.md');
|
|
379
|
-
const read =
|
|
638
|
+
const read = readFile(skillRelativePath);
|
|
380
639
|
if (!read || read.truncated) continue;
|
|
381
640
|
const skill = parseSkill(read.buffer.toString('utf8'), entry.name);
|
|
382
641
|
if (!skill?.name) {
|
|
@@ -646,7 +905,7 @@ export class SkillManager {
|
|
|
646
905
|
* surfaced for historic YAML compatibility.
|
|
647
906
|
*
|
|
648
907
|
* @param {string} [_mode] — deprecated, ignored
|
|
649
|
-
* @returns {Array<{ name: string, description: string, trigger: string, mode: string, category?: string, platforms?: string[], keywords?: string[], source: string, tier?: string, hasReferences: boolean, hasTemplates: boolean }>}
|
|
908
|
+
* @returns {Array<{ name: string, description: string, trigger: string, mode: string, category?: string, platforms?: string[], keywords?: string[], source: string, tier?: string, managed: boolean, hasReferences: boolean, hasTemplates: boolean }>}
|
|
650
909
|
*/
|
|
651
910
|
list(_mode) {
|
|
652
911
|
const skills = [...this.#skills.values()];
|
|
@@ -661,11 +920,52 @@ export class SkillManager {
|
|
|
661
920
|
keywords: s.keywords || undefined,
|
|
662
921
|
source: s._source,
|
|
663
922
|
tier: s._tier || undefined,
|
|
923
|
+
managed: (s._tier === 'user' || s._tier === 'project')
|
|
924
|
+
&& this.#nativeRootsForTier(s._tier).some(root => isRemovableNativeSkill(s._path, root)),
|
|
664
925
|
hasReferences: (s._references && s._references.length > 0) || false,
|
|
665
926
|
hasTemplates: (s._templates && s._templates.length > 0) || false,
|
|
666
927
|
}));
|
|
667
928
|
}
|
|
668
929
|
|
|
930
|
+
#nativeRootsForTier(tier) {
|
|
931
|
+
return [...this.#tierByDir.entries()]
|
|
932
|
+
.filter(([, label]) => label === tier)
|
|
933
|
+
.map(([dir]) => dir);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Return every discovered Skill source, including lower-priority sources
|
|
938
|
+
* shadowed by an effective user or project override. Runtime callers keep
|
|
939
|
+
* using `list()`; this is strictly for management UIs that must not make a
|
|
940
|
+
* removable user Skill disappear just because a project overrides its name.
|
|
941
|
+
*
|
|
942
|
+
* @returns {Array<{ id: string, name: string, description: string, source: string, tier?: string, managed: boolean }>}
|
|
943
|
+
*/
|
|
944
|
+
listSources() {
|
|
945
|
+
const sources = [];
|
|
946
|
+
for (const dir of this.#skillsDirs) {
|
|
947
|
+
const secure = this.#secureWorkspaceByDir.get(dir);
|
|
948
|
+
if (!secure && !existsSync(dir)) continue;
|
|
949
|
+
const { skills } = secure
|
|
950
|
+
? discoverWorkspaceSkills(secure.workspaceRoot, secure.relativeRoot)
|
|
951
|
+
: discoverSkills(dir, '', { ignorePaths: this.#ignorePathsByDir.get(dir) || [] });
|
|
952
|
+
const tier = this.#tierByDir.get(dir) || basename(dir);
|
|
953
|
+
for (const skill of skills) {
|
|
954
|
+
if (!matchesPlatform(skill.platforms)) continue;
|
|
955
|
+
const path = skill._path || '';
|
|
956
|
+
sources.push({
|
|
957
|
+
id: `${tier}:${displaySkillPath(path)}`,
|
|
958
|
+
name: skill.name,
|
|
959
|
+
description: skill.description || '',
|
|
960
|
+
source: skill._source,
|
|
961
|
+
tier,
|
|
962
|
+
managed: (tier === 'user' || tier === 'project') && isRemovableNativeSkill(path, dir),
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
return sources;
|
|
967
|
+
}
|
|
968
|
+
|
|
669
969
|
/**
|
|
670
970
|
* View a skill's full content + linked files (progressive disclosure view tier).
|
|
671
971
|
*
|
|
@@ -691,7 +991,7 @@ export class SkillManager {
|
|
|
691
991
|
result.linkedContent = 'Error: path traversal not allowed';
|
|
692
992
|
} else {
|
|
693
993
|
const relativePath = join(skill._secureRelativePath, filePath);
|
|
694
|
-
const read =
|
|
994
|
+
const read = readSecureWorkspaceSkillFile(skill._secureWorkspaceRoot, relativePath);
|
|
695
995
|
result.linkedContent = !read
|
|
696
996
|
? 'Error reading file: secure workspace read failed'
|
|
697
997
|
: (read.truncated ? 'Error reading file: linked file exceeds size limit' : read.buffer.toString('utf8'));
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -17,14 +17,20 @@
|
|
|
17
17
|
|
|
18
18
|
import { delimiter, join } from 'node:path';
|
|
19
19
|
import { COLLAB_TOOL_POLICY } from './tools/registry.js';
|
|
20
|
-
import { existsSync } from 'node:fs';
|
|
20
|
+
import { existsSync, lstatSync } from 'node:fs';
|
|
21
21
|
import { randomUUID } from 'node:crypto';
|
|
22
22
|
import { DEFAULT_YEAFT_DIR } from './init.js';
|
|
23
23
|
import { buildDreamOutputSnapshot } from './dream/output-snapshot.js';
|
|
24
24
|
import { Engine } from './engine.js';
|
|
25
25
|
import { loadSession } from './session.js';
|
|
26
26
|
import { loadAgentMCPConfig, loadConfig, loadMCPConfig } from './config.js';
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
createManagedProjectSkill,
|
|
29
|
+
createManagedSkill,
|
|
30
|
+
createSkillManager,
|
|
31
|
+
removeManagedProjectSkill,
|
|
32
|
+
removeManagedSkill,
|
|
33
|
+
} from './skills.js';
|
|
28
34
|
import { buildPluginCatalog, createPluginSkillManager, resolveMcpPluginPolicy } from './plugins.js';
|
|
29
35
|
import { MCPManager } from './mcp.js';
|
|
30
36
|
import { sendToServer } from '../connection/buffer.js';
|
|
@@ -53,7 +59,8 @@ import {
|
|
|
53
59
|
migrateRegisteredWorkDirSessions,
|
|
54
60
|
resolveSessionYeaftDir,
|
|
55
61
|
} from './sessions/session-crud.js';
|
|
56
|
-
import { openSession, loadSessionMeta } from './sessions/session-store.js';
|
|
62
|
+
import { openSession, loadSessionMeta, SESSION_META_FILE } from './sessions/session-store.js';
|
|
63
|
+
import { validateSessionId } from './sessions/ids.js';
|
|
57
64
|
import { loadSessionConfig, normalizeSessionConfig, resolveSessionConfig, SessionConfigError } from './sessions/session-config.js';
|
|
58
65
|
import { updateSessionConfig } from './sessions/session-crud.js';
|
|
59
66
|
import { createCoordinator } from './sessions/coordinator.js';
|
|
@@ -3261,6 +3268,118 @@ function loadPluginCatalogMcpConfig(yeaftDir) {
|
|
|
3261
3268
|
return loadAgentMCPConfig(yeaftDir);
|
|
3262
3269
|
}
|
|
3263
3270
|
|
|
3271
|
+
const MANAGED_SKILL_SCOPES = new Set(['user', 'project']);
|
|
3272
|
+
|
|
3273
|
+
function normalizedManagedSkillScope(value) {
|
|
3274
|
+
const scope = typeof value === 'string' ? value.trim() : '';
|
|
3275
|
+
return MANAGED_SKILL_SCOPES.has(scope) ? scope : '';
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
function resolveManagedSkillSession(yeaftDir, rawSessionId) {
|
|
3279
|
+
const sessionId = typeof rawSessionId === 'string' ? rawSessionId.trim() : '';
|
|
3280
|
+
const validation = validateSessionId(sessionId);
|
|
3281
|
+
if (!validation.ok) throw new Error('invalid Session id');
|
|
3282
|
+
|
|
3283
|
+
// Skill mutation is a filesystem write. Its authority root is strictly the
|
|
3284
|
+
// agent-local canonical store, not the read-compatible resolver: that one
|
|
3285
|
+
// can consult bootstrap-only group-workdirs.json and project-local legacy
|
|
3286
|
+
// Session directories. Those sources are not valid write authorization.
|
|
3287
|
+
const sessionRoot = sessionsRoot(yeaftDir);
|
|
3288
|
+
try {
|
|
3289
|
+
const sessionRootStat = lstatSync(sessionRoot);
|
|
3290
|
+
if (!sessionRootStat.isDirectory() || sessionRootStat.isSymbolicLink()) {
|
|
3291
|
+
throw new Error('the selected Session was not found');
|
|
3292
|
+
}
|
|
3293
|
+
} catch (err) {
|
|
3294
|
+
if (err?.message === 'the selected Session was not found') throw err;
|
|
3295
|
+
throw new Error('the selected Session was not found');
|
|
3296
|
+
}
|
|
3297
|
+
const sessionDir = join(sessionRoot, sessionId);
|
|
3298
|
+
// `loadSessionMeta()` deliberately supports legacy group.json for readers.
|
|
3299
|
+
// This mutation path needs the canonical agent-local session.json instead.
|
|
3300
|
+
const metaPath = join(sessionDir, SESSION_META_FILE);
|
|
3301
|
+
if (!existsSync(metaPath)) throw new Error('the selected Session was not found');
|
|
3302
|
+
try {
|
|
3303
|
+
const sessionDirStat = lstatSync(sessionDir);
|
|
3304
|
+
const metaStat = lstatSync(metaPath);
|
|
3305
|
+
if (!sessionDirStat.isDirectory() || sessionDirStat.isSymbolicLink()
|
|
3306
|
+
|| !metaStat.isFile() || metaStat.isSymbolicLink()) {
|
|
3307
|
+
throw new Error('the selected Session was not found');
|
|
3308
|
+
}
|
|
3309
|
+
} catch (err) {
|
|
3310
|
+
if (err?.message === 'the selected Session was not found') throw err;
|
|
3311
|
+
throw new Error('the selected Session was not found');
|
|
3312
|
+
}
|
|
3313
|
+
const meta = loadSessionMeta(sessionDir);
|
|
3314
|
+
if (!meta || meta.id !== sessionId) throw new Error('the selected Session was not found');
|
|
3315
|
+
|
|
3316
|
+
const workDir = normalizeSessionWorkDir(meta.workDir);
|
|
3317
|
+
if (!workDir) throw new Error('the selected Session has no project directory');
|
|
3318
|
+
return { sessionId, workDir };
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
function currentRuntimeMatchesWorkDir(workDir) {
|
|
3322
|
+
const runtime = activeSkillRuntime(captureRuntimeOwner());
|
|
3323
|
+
return runtime && normalizeSessionWorkDir(runtime.workDir) === normalizeSessionWorkDir(workDir);
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
function reloadManagedSkillRuntime(scope, workDir = '') {
|
|
3327
|
+
const owner = captureRuntimeOwner();
|
|
3328
|
+
if (!owner) return;
|
|
3329
|
+
if (scope === 'project' && currentRuntimeMatchesWorkDir(workDir)) {
|
|
3330
|
+
reloadActiveSkills(owner);
|
|
3331
|
+
return;
|
|
3332
|
+
}
|
|
3333
|
+
if (scope === 'user') reloadActiveSkills(owner);
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
function managedSkillCatalog(yeaftDir, workDir = '') {
|
|
3337
|
+
const normalizedWorkDir = normalizeSessionWorkDir(workDir);
|
|
3338
|
+
const runtime = resolvePluginCatalogRuntime(normalizedWorkDir);
|
|
3339
|
+
const manager = createSkillManager(yeaftDir, normalizedWorkDir || process.cwd());
|
|
3340
|
+
return buildPluginCatalog({ ...runtime, skillManager: manager });
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
/**
|
|
3344
|
+
* Create or remove a native Yeaft Skill in an explicit user/project scope.
|
|
3345
|
+
* The Session lookup is agent-local; browser-supplied filesystem paths are
|
|
3346
|
+
* never trusted as a write target.
|
|
3347
|
+
*/
|
|
3348
|
+
export function handleYeaftManagedSkill(msg = {}) {
|
|
3349
|
+
const requestId = msg.requestId || null;
|
|
3350
|
+
const action = msg.action === 'remove' ? 'remove' : (msg.action === 'create' ? 'create' : '');
|
|
3351
|
+
const scope = normalizedManagedSkillScope(msg.scope);
|
|
3352
|
+
const sessionId = typeof msg.sessionId === 'string' ? msg.sessionId.trim() : '';
|
|
3353
|
+
const yeaftDir = ctx.CONFIG?.yeaftDir || session?.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
3354
|
+
const respond = (payload) => sendToServer({
|
|
3355
|
+
type: 'yeaft_managed_skill_result',
|
|
3356
|
+
requestId,
|
|
3357
|
+
...(msg._requestClientId ? { _requestClientId: msg._requestClientId } : {}),
|
|
3358
|
+
...payload,
|
|
3359
|
+
});
|
|
3360
|
+
if (!action || !scope) {
|
|
3361
|
+
respond({ catalog: { tools: [], skills: [], skillSources: [], mcpServers: [] }, error: 'skill action must be create or remove and scope must be user or project' });
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
try {
|
|
3365
|
+
const managedSession = scope === 'project'
|
|
3366
|
+
? resolveManagedSkillSession(yeaftDir, sessionId)
|
|
3367
|
+
: null;
|
|
3368
|
+
const result = scope === 'project'
|
|
3369
|
+
? (action === 'create'
|
|
3370
|
+
? createManagedProjectSkill(managedSession.workDir, msg.skill || {})
|
|
3371
|
+
: removeManagedProjectSkill(managedSession.workDir, msg.name))
|
|
3372
|
+
: (action === 'create'
|
|
3373
|
+
? createManagedSkill(join(yeaftDir, 'skills'), msg.skill || {})
|
|
3374
|
+
: removeManagedSkill(join(yeaftDir, 'skills'), msg.name));
|
|
3375
|
+
reloadManagedSkillRuntime(scope, managedSession?.workDir || '');
|
|
3376
|
+
const catalog = managedSkillCatalog(yeaftDir, managedSession?.workDir || '');
|
|
3377
|
+
respond({ scope, sessionId: managedSession?.sessionId || null, result, catalog, error: null });
|
|
3378
|
+
} catch (err) {
|
|
3379
|
+
respond({ scope, sessionId: null, catalog: { tools: [], skills: [], skillSources: [], mcpServers: [] }, error: err?.message || String(err) });
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3264
3383
|
/** Test-only: read the MCP catalog source without sending a bridge response. */
|
|
3265
3384
|
export function __testLoadPluginCatalogMcpConfig(yeaftDir) {
|
|
3266
3385
|
return loadPluginCatalogMcpConfig(yeaftDir);
|