This commit is contained in:
+35
-14
@@ -19,6 +19,25 @@ export function setSkillToolDeps(deps: SkillToolDeps | null): void {
|
||||
_deps = deps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the folder `(root, leaf)` that user-scope skills should read/write,
|
||||
* mirroring AGENTS.md / memory (user-folder.ts folderTarget). When the job
|
||||
* belongs to a (case) workspace, the worker wires `ctx.folderContext`
|
||||
* (`{rootDir, leafId}`) → skills land in `data/spaces/{id}/skills/` and are
|
||||
* shared with members. Without folderContext (gitea-issue / legacy / personal)
|
||||
* we fall back to the caller's personal folder — byte-identical to before
|
||||
* (the personal root is taken from the catalog so it matches getUserSkillDir).
|
||||
*
|
||||
* `system` scope is unaffected: that path uses catalog.getSystemDir() directly.
|
||||
*/
|
||||
function skillFolderTarget(ctx: ToolContext): { root: string; leaf: string } {
|
||||
if (ctx.folderContext) {
|
||||
return { root: ctx.folderContext.rootDir, leaf: ctx.folderContext.leafId };
|
||||
}
|
||||
const personalRoot = ctx.skillCatalog?.getUserRoot() ?? _deps?.userFolderRoot ?? './data/users';
|
||||
return { root: personalRoot, leaf: ctx.userId ?? 'local' };
|
||||
}
|
||||
|
||||
// ── Skill materialization ───────────────────────────────────────────────────
|
||||
// When the agent ReadSkill's a directory-based skill, copy its files into the
|
||||
// task workspace (`{workspace}/skills/{name}/`) so its scripts are usable in
|
||||
@@ -151,7 +170,7 @@ function executeInstallSkill(
|
||||
if (!VALID_SKILL_NAME.test(skillName)) {
|
||||
return { output: `InstallSkill: invalid skill name "${skillName}". Only [a-z0-9_-] allowed.`, isError: true };
|
||||
}
|
||||
if (scope === 'system' && ctx.notesUserRole !== 'admin') {
|
||||
if (scope === 'system' && ctx.userRole !== 'admin') {
|
||||
return { output: 'InstallSkill: system-scope install requires admin role', isError: true };
|
||||
}
|
||||
|
||||
@@ -160,10 +179,12 @@ function executeInstallSkill(
|
||||
return { output: 'InstallSkill: skill catalog not available', isError: true };
|
||||
}
|
||||
const userId = ctx.userId ?? 'local';
|
||||
// user-scope skills resolve to the job's folder (space → shared, else personal).
|
||||
const { root: folderRoot, leaf: folderLeaf } = skillFolderTarget(ctx);
|
||||
|
||||
// Count check (user scope only)
|
||||
// Count check (user scope only) — counts skills in the resolved folder.
|
||||
if (scope === 'user') {
|
||||
const userSkillCount = catalog.getForUser(userId).filter(s => s.source === 'user').length;
|
||||
const userSkillCount = catalog.getForFolder(folderRoot, folderLeaf).filter(s => s.source === 'user').length;
|
||||
if (userSkillCount >= 50) {
|
||||
return { output: `InstallSkill: user skill limit reached (${userSkillCount}/50)`, isError: true };
|
||||
}
|
||||
@@ -205,7 +226,7 @@ function executeInstallSkill(
|
||||
return { output: `InstallSkill: blocked by security scan:\n${details}`, isError: true };
|
||||
}
|
||||
|
||||
const targetBase = scope === 'system' ? catalog.getSystemDir() : catalog.getUserSkillDir(userId);
|
||||
const targetBase = scope === 'system' ? catalog.getSystemDir() : catalog.getSkillDirForFolder(folderRoot, folderLeaf);
|
||||
const target = join(targetBase, skillName);
|
||||
const tmpTarget = target + '.tmp-' + Date.now();
|
||||
|
||||
@@ -221,8 +242,8 @@ function executeInstallSkill(
|
||||
return { output: `InstallSkill: install failed: ${e}`, isError: true };
|
||||
}
|
||||
|
||||
if (scope === 'system') { catalog.refreshSystem(); } else { catalog.invalidate(userId); }
|
||||
_deps?.auditLog?.('skill_installed', { skillName, scope, userId, format: 'directory', fileCount: stats.fileCount }, ctx.taskId ?? null);
|
||||
if (scope === 'system') { catalog.refreshSystem(); } else { catalog.invalidateFolder(folderRoot, folderLeaf); }
|
||||
_deps?.auditLog?.('skill_installed', { skillName, scope, userId, folderRoot, folderLeaf, format: 'directory', fileCount: stats.fileCount }, ctx.taskId ?? null);
|
||||
|
||||
const mediumFindings = findings.filter(f => f.severity === 'medium');
|
||||
let msg = `InstallSkill: installed "${skillName}" to ${scope} scope (${stats.fileCount} files)`;
|
||||
@@ -246,7 +267,7 @@ function executeInstallSkill(
|
||||
return { output: `InstallSkill: blocked by security scan:\n${details}`, isError: true };
|
||||
}
|
||||
|
||||
const targetDir = scope === 'system' ? catalog.getSystemDir() : catalog.getUserSkillDir(userId);
|
||||
const targetDir = scope === 'system' ? catalog.getSystemDir() : catalog.getSkillDirForFolder(folderRoot, folderLeaf);
|
||||
const skillDir = join(targetDir, skillName);
|
||||
const tmpDir = join(targetDir, `.${skillName}.tmp.${Date.now()}`);
|
||||
|
||||
@@ -262,8 +283,8 @@ function executeInstallSkill(
|
||||
return { output: `InstallSkill: write failed: ${(err as Error).message}`, isError: true };
|
||||
}
|
||||
|
||||
if (scope === 'system') { catalog.refreshSystem(); } else { catalog.invalidate(userId); }
|
||||
_deps?.auditLog?.('skill_installed', { skillName, scope, userId, scanSeverity: severity, findingsCount: findings.length }, ctx.taskId ?? null);
|
||||
if (scope === 'system') { catalog.refreshSystem(); } else { catalog.invalidateFolder(folderRoot, folderLeaf); }
|
||||
_deps?.auditLog?.('skill_installed', { skillName, scope, userId, folderRoot, folderLeaf, scanSeverity: severity, findingsCount: findings.length }, ctx.taskId ?? null);
|
||||
|
||||
const mediumFindings = findings.filter(f => f.severity === 'medium');
|
||||
let msg = `InstallSkill: installed "${skillName}" to ${scope} scope`;
|
||||
@@ -329,8 +350,8 @@ export function executeSkillTool(
|
||||
if (name === 'ListSkills') {
|
||||
const catalog = ctx.skillCatalog;
|
||||
if (!catalog) return { output: 'Error: skill catalog not available', isError: true };
|
||||
const userId = ctx.userId ?? 'local';
|
||||
const entries = catalog.getForUser(userId);
|
||||
const { root, leaf } = skillFolderTarget(ctx);
|
||||
const entries = catalog.getForFolder(root, leaf);
|
||||
if (entries.length === 0) return { output: 'No skills installed.', isError: false };
|
||||
const lines = entries.map(e => {
|
||||
const dirSuffix = 'dirPath' in e && e.dirPath ? ' (has scripts/)' : '';
|
||||
@@ -351,10 +372,10 @@ export function executeSkillTool(
|
||||
return { output: 'Error: skill catalog not available', isError: true };
|
||||
}
|
||||
|
||||
const userId = ctx.userId ?? 'local';
|
||||
const result = catalog.getSkillContent(skillName, userId);
|
||||
const { root, leaf } = skillFolderTarget(ctx);
|
||||
const result = catalog.getSkillContentForFolder(skillName, root, leaf);
|
||||
if (result === null) {
|
||||
const available = catalog.getForUser(userId).map(s => s.name).join(', ');
|
||||
const available = catalog.getForFolder(root, leaf).map(s => s.name).join(', ');
|
||||
return {
|
||||
output: `Skill "${skillName}" not found. Available skills: ${available || '(none)'}`,
|
||||
isError: true,
|
||||
|
||||
Reference in New Issue
Block a user