feat: initial public release (MAESTRO)

This commit is contained in:
oss-sync
2026-06-03 05:08:00 +00:00
commit f5c7666f6b
823 changed files with 184150 additions and 0 deletions
+373
View File
@@ -0,0 +1,373 @@
import { mkdirSync, writeFileSync, renameSync, unlinkSync, realpathSync, cpSync, rmSync, existsSync, readdirSync, lstatSync } from 'fs';
import { join } from 'path';
import type { ToolDef } from '../../llm/openai-compat.js';
import type { ToolContext, ToolResult } from './core.js';
import { VALID_SKILL_NAME } from '../skills.js';
import { scanSkillContent, scanSkillDirectory, maxSeverity } from '../skills-scanner.js';
import { logger } from '../../logger.js';
// ── Injected deps (server.ts / worker.ts call setSkillToolDeps) ─────────────
export interface SkillToolDeps {
auditLog?: (action: string, detail: object, jobId?: string | null) => void;
userFolderRoot: string;
}
let _deps: SkillToolDeps | null = null;
export function setSkillToolDeps(deps: SkillToolDeps | null): void {
_deps = deps;
}
// ── 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
// every Bash sandbox mode (the skill store lives outside the workspace and is
// only bind-mounted in the bwrap path). The copy is rw and idempotent per task.
const SKILL_MATERIALIZE_MAX_BYTES = 50 * 1024 * 1024; // 50MB
/** Total size of `dir` (skipping symlinks), or null once it exceeds `cap`. */
function dirSizeCapped(dir: string, cap: number): number | null {
let total = 0;
const stack = [dir];
while (stack.length > 0) {
const d = stack.pop()!;
let entries: string[];
try { entries = readdirSync(d); } catch { continue; }
for (const e of entries) {
const p = join(d, e);
let st;
try { st = lstatSync(p); } catch { continue; }
if (st.isSymbolicLink()) continue;
if (st.isDirectory()) { stack.push(p); continue; }
total += st.size;
if (total > cap) return null;
}
}
return total;
}
interface MaterializeResult { ok: boolean; relPath: string; note?: string }
/** Copy a skill's source dir into `{workspace}/skills/{name}/` (idempotent). */
export function materializeSkill(srcDir: string, workspacePath: string, skillName: string): MaterializeResult {
const relPath = `skills/${skillName}`;
const dest = join(workspacePath, 'skills', skillName);
if (existsSync(dest)) return { ok: true, relPath }; // already materialized this task
if (dirSizeCapped(srcDir, SKILL_MATERIALIZE_MAX_BYTES) === null) {
return { ok: false, relPath, note: 'skill exceeds 50MB copy limit' };
}
try {
mkdirSync(join(workspacePath, 'skills'), { recursive: true });
cpSync(srcDir, dest, {
recursive: true,
dereference: false,
// Skip symlinks so a link inside the skill cannot point outside the workspace.
filter: (src) => { try { return !lstatSync(src).isSymbolicLink(); } catch { return false; } },
});
return { ok: true, relPath };
} catch (e) {
return { ok: false, relPath, note: `copy failed: ${(e as Error).message}` };
}
}
// ---------------------------------------------------------------------------
// Tool definitions
// ---------------------------------------------------------------------------
export const TOOL_DEFS: Record<string, ToolDef> = {
InstallSkill: {
type: 'function',
function: {
name: 'InstallSkill',
description: 'スキル(参照知識: 手順書・ガイド)をインストール。Piece(実行テンプレート)とは異なる。通常は content に SKILL.md 全文を渡す。workspace 内にスキルディレクトリを構築済みの場合のみ sourcePath を使用。',
parameters: {
type: 'object',
properties: {
name: { type: 'string', description: 'スキル名 ([a-z0-9_-] のみ)' },
content: { type: 'string', description: 'SKILL.md の全文 (YAML frontmatter + 本文)。通常はこちらを使用' },
sourcePath: { type: 'string', description: 'workspace 内のスキルディレクトリの絶対パス (SKILL.md + scripts/ 等を含む場合のみ)。workspace 外のパスは拒否される' },
scope: { type: 'string', enum: ['system', 'user'], description: 'system=全ユーザー共有 (admin only), user=個人' },
},
required: ['name', 'scope'],
},
},
},
ReadSkill: {
type: 'function',
function: {
name: 'ReadSkill',
description: 'スキル(参照知識: 手順書・ガイド・規約)の全文を取得する。Piece の定義取得には GetPiece を使うこと。利用可能なスキル一覧はシステムプロンプトの Skills Index を参照。',
parameters: {
type: 'object',
properties: {
name: { type: 'string', description: 'スキル名 (Skills Index に表示されている名前)' },
},
required: ['name'],
},
},
},
ListSkills: {
type: 'function',
function: {
name: 'ListSkills',
description: 'インストール済みスキル(参照知識)の一覧を返す。Piece(実行テンプレート)の一覧は ListPieces を使うこと。',
parameters: {
type: 'object',
properties: {},
required: [],
},
},
},
};
// ── InstallSkill implementation ──────────────────────────────────────────────
const MAX_FILE_COUNT = 100;
const MAX_DEPTH = 3;
const MAX_TOTAL_BYTES = 5 * 1024 * 1024; // 5MB
function executeInstallSkill(
input: Record<string, unknown>,
ctx: ToolContext,
): ToolResult {
const skillName = input['name'] as string | undefined;
const content = input['content'] as string | undefined;
const sourcePath = input['sourcePath'] as string | undefined;
const scope = input['scope'] as string | undefined;
if (!skillName || typeof skillName !== 'string') {
return { output: 'InstallSkill: "name" parameter is required', isError: true };
}
if (!content && !sourcePath) {
return { output: 'InstallSkill: either "content" or "sourcePath" is required', isError: true };
}
if (content && sourcePath) {
return { output: 'InstallSkill: specify either "content" or "sourcePath", not both', isError: true };
}
if (scope !== 'system' && scope !== 'user') {
return { output: 'InstallSkill: "scope" must be "system" or "user"', isError: true };
}
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') {
return { output: 'InstallSkill: system-scope install requires admin role', isError: true };
}
const catalog = ctx.skillCatalog;
if (!catalog) {
return { output: 'InstallSkill: skill catalog not available', isError: true };
}
const userId = ctx.userId ?? 'local';
// Count check (user scope only)
if (scope === 'user') {
const userSkillCount = catalog.getForUser(userId).filter(s => s.source === 'user').length;
if (userSkillCount >= 50) {
return { output: `InstallSkill: user skill limit reached (${userSkillCount}/50)`, isError: true };
}
}
// --- sourcePath mode: copy directory from workspace ---
if (sourcePath) {
let realSource: string;
let realWorkspace: string;
try {
realSource = realpathSync(sourcePath);
realWorkspace = realpathSync(ctx.workspacePath);
} catch (e) {
return { output: `InstallSkill: sourcePath does not exist or is not accessible. Use "content" parameter instead to pass SKILL.md text directly.`, isError: true };
}
if (!realSource.startsWith(realWorkspace + '/')) {
return { output: 'InstallSkill: sourcePath must be inside the task workspace. Use "content" parameter to pass SKILL.md text directly.', isError: true };
}
if (!existsSync(join(realSource, 'SKILL.md'))) {
return { output: `InstallSkill: SKILL.md not found in ${sourcePath}`, isError: true };
}
const stats = getDirStats(realSource);
if (stats.fileCount > MAX_FILE_COUNT) {
return { output: `InstallSkill: directory contains ${stats.fileCount} files (max ${MAX_FILE_COUNT})`, isError: true };
}
if (stats.maxDepth > MAX_DEPTH) {
return { output: `InstallSkill: directory depth ${stats.maxDepth} exceeds max ${MAX_DEPTH}`, isError: true };
}
if (stats.totalBytes > MAX_TOTAL_BYTES) {
return { output: `InstallSkill: directory size ${(stats.totalBytes / 1024 / 1024).toFixed(1)}MB exceeds max 5MB`, isError: true };
}
const findings = scanSkillDirectory(realSource);
const severity = maxSeverity(findings);
if (severity === 'high') {
const details = findings.filter(f => f.severity === 'high').slice(0, 5)
.map(f => ` - [${f.pattern}] ${f.match} (line ${f.line})`).join('\n');
return { output: `InstallSkill: blocked by security scan:\n${details}`, isError: true };
}
const targetBase = scope === 'system' ? catalog.getSystemDir() : catalog.getUserSkillDir(userId);
const target = join(targetBase, skillName);
const tmpTarget = target + '.tmp-' + Date.now();
try {
mkdirSync(targetBase, { recursive: true });
cpSync(realSource, tmpTarget, { recursive: true });
if (existsSync(target)) rmSync(target, { recursive: true, force: true });
const flatPath = join(targetBase, `${skillName}.md`);
if (existsSync(flatPath)) unlinkSync(flatPath);
renameSync(tmpTarget, target);
} catch (e) {
try { rmSync(tmpTarget, { recursive: true, force: true }); } catch {}
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);
const mediumFindings = findings.filter(f => f.severity === 'medium');
let msg = `InstallSkill: installed "${skillName}" to ${scope} scope (${stats.fileCount} files)`;
if (mediumFindings.length > 0) {
msg += `\n\nWarnings: ${mediumFindings.length} medium-severity findings`;
}
return { output: msg, isError: false };
}
// --- content mode: create {name}/SKILL.md ---
const MAX_BYTES = 64 * 1024;
if (Buffer.byteLength(content!, 'utf-8') > MAX_BYTES) {
return { output: `InstallSkill: content exceeds 64 KB limit`, isError: true };
}
const findings = scanSkillContent(content!);
const severity = maxSeverity(findings);
if (severity === 'high') {
const details = findings.filter(f => f.severity === 'high')
.map(f => ` - [high] ${f.pattern}: "${f.match}" (line ${f.line})`).join('\n');
return { output: `InstallSkill: blocked by security scan:\n${details}`, isError: true };
}
const targetDir = scope === 'system' ? catalog.getSystemDir() : catalog.getUserSkillDir(userId);
const skillDir = join(targetDir, skillName);
const tmpDir = join(targetDir, `.${skillName}.tmp.${Date.now()}`);
try {
mkdirSync(tmpDir, { recursive: true });
writeFileSync(join(tmpDir, 'SKILL.md'), content!, { encoding: 'utf-8', mode: 0o600 });
if (existsSync(skillDir)) rmSync(skillDir, { recursive: true, force: true });
const flatPath = join(targetDir, `${skillName}.md`);
if (existsSync(flatPath)) unlinkSync(flatPath);
renameSync(tmpDir, skillDir);
} catch (err) {
try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
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);
const mediumFindings = findings.filter(f => f.severity === 'medium');
let msg = `InstallSkill: installed "${skillName}" to ${scope} scope`;
if (mediumFindings.length > 0) {
const warnings = mediumFindings.map(f => ` - [medium] ${f.pattern}: "${f.match}" (line ${f.line})`).join('\n');
msg += `\n\nWarnings (medium severity):\n${warnings}`;
}
return { output: msg, isError: false };
}
// ---------------------------------------------------------------------------
// Helpers for InstallSkillFromDir
// ---------------------------------------------------------------------------
interface DirStats {
fileCount: number;
totalBytes: number;
maxDepth: number;
}
function getDirStats(dir: string, depth: number = 0): DirStats {
const stats: DirStats = { fileCount: 0, totalBytes: 0, maxDepth: depth };
let entries: string[];
try {
entries = readdirSync(dir);
} catch {
return stats;
}
for (const entry of entries) {
const fullPath = join(dir, entry);
let st;
try {
st = lstatSync(fullPath);
} catch {
continue;
}
if (st.isSymbolicLink()) continue;
if (st.isDirectory()) {
const sub = getDirStats(fullPath, depth + 1);
stats.fileCount += sub.fileCount;
stats.totalBytes += sub.totalBytes;
if (sub.maxDepth > stats.maxDepth) stats.maxDepth = sub.maxDepth;
} else if (st.isFile()) {
stats.fileCount++;
stats.totalBytes += st.size;
}
}
return stats;
}
// ---------------------------------------------------------------------------
// Tool execution
// ---------------------------------------------------------------------------
export function executeSkillTool(
name: string,
input: Record<string, unknown>,
ctx: ToolContext,
): ToolResult | null {
if (name === 'InstallSkill') {
return executeInstallSkill(input, ctx);
}
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);
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/)' : '';
return `- **${e.name}** [${e.source}]: ${e.description}${dirSuffix}`;
});
return { output: lines.join('\n'), isError: false };
}
if (name !== 'ReadSkill') return null;
const skillName = input['name'] as string;
if (!skillName) {
return { output: 'Error: name is required', isError: true };
}
const catalog = ctx.skillCatalog;
if (!catalog) {
return { output: 'Error: skill catalog not available', isError: true };
}
const userId = ctx.userId ?? 'local';
const result = catalog.getSkillContent(skillName, userId);
if (result === null) {
const available = catalog.getForUser(userId).map(s => s.name).join(', ');
return {
output: `Skill "${skillName}" not found. Available skills: ${available || '(none)'}`,
isError: true,
};
}
if (result.dirPath) {
const m = materializeSkill(result.dirPath, ctx.workspacePath, skillName);
const loc = m.ok
? `このスキルのファイルは workspace の \`${m.relPath}/\` に配置しました(例: \`${m.relPath}/scripts/...\`)。スクリプトはこの相対パスで実行できます。`
: `(注: スキルのファイルを workspace にコピーできませんでした: ${m.note}。SKILL.md の手順は以下を参照)`;
return { output: `${loc}\n\n${result.content}`, isError: false };
}
return { output: result.content, isError: false };
}