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
+356
View File
@@ -0,0 +1,356 @@
import { type Application, type Request, type Response } from 'express';
import { readdirSync, readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
import { join } from 'path';
import { parse, stringify } from 'yaml';
import { patchYaml } from './yaml-patch.js';
import { detectDrift, type DriftStatus } from '../engine/reflection/drift-detect.js';
import { userPiecesDir } from '../user-folder/paths.js';
import { logger } from '../logger.js';
export type PieceSource = 'builtin' | 'global-custom' | 'user-custom';
interface PieceSummary {
name: string;
description: string;
triggers?: { keywords: string[] };
drift?: DriftStatus;
requiredMcp?: string[];
/** Backward-compat: true for any non-builtin (global-custom OR user-custom). */
custom: boolean;
source: PieceSource;
/** Set only when source === 'user-custom'. */
ownerId?: string;
}
function loadPieceFile(filePath: string): any {
const raw = readFileSync(filePath, 'utf-8');
return parse(raw);
}
function listPieceFiles(piecesDir: string): string[] {
return readdirSync(piecesDir)
.filter(f => f.endsWith('.yaml'))
.map(f => join(piecesDir, f));
}
// Phase 4 (SSH): movements using these tools must declare allowed_ssh_connections.
// Kept inline (not imported from engine/) so this API module stays decoupled
// from SSH internals — pieces can be validated even when SSH is disabled.
const SSH_TOOL_NAMES = new Set(['SshExec', 'SshUpload', 'SshDownload']);
const ALLOWED_SSH_ID = /^[a-f0-9-]{8,}$/;
function validatePiece(piece: any): string | null {
if (!piece.name || !/^[a-z0-9-]+$/.test(piece.name)) return 'name must be lowercase alphanumeric with hyphens';
if (!Array.isArray(piece.movements) || piece.movements.length === 0) return 'movements must be non-empty array';
// Required so `while (steps < piece.max_movements)` actually iterates;
// otherwise the run aborts with "Exceeded max movements (undefined)".
if (typeof piece.max_movements !== 'number' || !Number.isFinite(piece.max_movements) || piece.max_movements <= 0) {
return 'max_movements is required (positive integer)';
}
const names = new Set(piece.movements.map((m: any) => m.name));
if (!names.has(piece.initial_movement)) return 'initial_movement must reference an existing movement';
// Phase 6b: rules[].next only accepts existing movement names + WAIT_SUBTASKS.
// Terminal moves (COMPLETE/ABORT/ASK) go through the `complete` tool now.
// default_next is engine-internal (context overflow / ASK limit / SpawnSubTask
// unavailable fallback) and still accepts COMPLETE/ABORT/ASK.
const validRuleNexts = new Set([...names, 'WAIT_SUBTASKS']);
const validDefaultNexts = new Set([...names, 'COMPLETE', 'ABORT', 'ASK', 'WAIT_SUBTASKS']);
for (const m of piece.movements) {
if (m.default_next && !validDefaultNexts.has(m.default_next)) {
return `movement "${m.name}": default_next "${m.default_next}" is invalid`;
}
if (Array.isArray(m.rules)) {
for (const r of m.rules) {
if (!validRuleNexts.has(r.next)) {
if (r.next === 'COMPLETE' || r.next === 'ABORT' || r.next === 'ASK') {
return `movement "${m.name}": rules[].next cannot be "${r.next}" (use the \`complete\` tool for terminal moves)`;
}
return `movement "${m.name}": rule next "${r.next}" is invalid`;
}
}
}
// Phase 4: allowed_ssh_connections consistency + format
const list = m.allowed_ssh_connections;
const tools = Array.isArray(m.allowed_tools) ? m.allowed_tools : [];
const hasSshTool = tools.some((t: unknown) => typeof t === 'string' && SSH_TOOL_NAMES.has(t));
if (list === undefined) {
if (hasSshTool) {
return `movement "${m.name}": allowed_ssh_connections is required when allowed_tools contains SSH tool(s)`;
}
} else if (!Array.isArray(list)) {
return `movement "${m.name}": allowed_ssh_connections must be an array`;
} else {
for (let i = 0; i < list.length; i++) {
const entry = list[i];
if (typeof entry !== 'string') {
return `movement "${m.name}": allowed_ssh_connections[${i}] must be a string`;
}
if (entry !== '*' && !ALLOWED_SSH_ID.test(entry)) {
return `movement "${m.name}": allowed_ssh_connections[${i}]="${entry}" must be '*' or a lowercase hex/hyphen id (8+ chars)`;
}
}
}
}
return null;
}
const VALID_PIECE_NAME = /^[a-z0-9-]+$/;
function validateName(name: string): boolean {
return VALID_PIECE_NAME.test(name);
}
export function findPieceFile(name: string, piecesDir: string, customPiecesDir?: string): { path: string; custom: boolean } | null {
if (customPiecesDir) {
const customPath = join(customPiecesDir, `${name}.yaml`);
if (existsSync(customPath)) return { path: customPath, custom: true };
}
const builtinPath = join(piecesDir, `${name}.yaml`);
if (existsSync(builtinPath)) return { path: builtinPath, custom: false };
return null;
}
export interface PiecesApiOptions {
piecesDir: string;
/** Optional admin-managed shared custom dir (global to all users). */
customPiecesDir?: string;
/**
* Root of per-user data (typically `./data/users`). When set, each authenticated
* user can read/create/update/delete pieces under `{userPiecesRootDir}/{userId}/pieces/`.
* When unset, per-user piece support is disabled and non-admin POST returns 503.
*/
userPiecesRootDir?: string;
}
type AuthedUser = { id: string; role?: string };
function getUser(req: Request): AuthedUser | undefined {
return (req as any).user as AuthedUser | undefined;
}
function isAdminOrLegacy(user: AuthedUser | undefined): boolean {
// No req.user → legacy (auth disabled or test/internal). Treat as admin so
// existing callers without auth middleware continue to work.
return !user || user.role === 'admin';
}
/**
* Lookup priority for a given caller:
* 1. Caller's own user-custom dir (overrides everything below).
* 2. Global custom dir (admin-managed, all users see).
* 3. Built-in dir.
*/
function findPieceForCaller(
opts: PiecesApiOptions,
user: AuthedUser | undefined,
name: string,
): { path: string; source: PieceSource; ownerId?: string } | null {
if (opts.userPiecesRootDir && user) {
const ucPath = join(userPiecesDir(opts.userPiecesRootDir, user.id), `${name}.yaml`);
if (existsSync(ucPath)) return { path: ucPath, source: 'user-custom', ownerId: user.id };
}
if (opts.customPiecesDir) {
const gcPath = join(opts.customPiecesDir, `${name}.yaml`);
if (existsSync(gcPath)) return { path: gcPath, source: 'global-custom' };
}
const biPath = join(opts.piecesDir, `${name}.yaml`);
if (existsSync(biPath)) return { path: biPath, source: 'builtin' };
return null;
}
/**
* Mount the pieces REST API. Read endpoints (GET) require only authentication
* (any logged-in user can list/read pieces visible to them). Write endpoints
* (POST/PUT/DELETE) enforce per-piece authorization:
* - built-in / global-custom: admin only
* - user-custom: owner or admin
*/
export function mountPiecesApi(
app: Application,
optsOrPiecesDir: PiecesApiOptions | string,
legacyCustomPiecesDir?: string,
): void {
// Backwards-compatible signature: mountPiecesApi(app, piecesDir, customPiecesDir?)
const opts: PiecesApiOptions = typeof optsOrPiecesDir === 'string'
? { piecesDir: optsOrPiecesDir, customPiecesDir: legacyCustomPiecesDir }
: optsOrPiecesDir;
app.get('/api/pieces', (req: Request, res: Response) => {
try {
const user = getUser(req);
const seen = new Set<string>();
const pieces: PieceSummary[] = [];
// Order matters: user-custom overrides global-custom, which overrides built-in.
const sources: Array<{ dir: string; source: PieceSource; ownerId?: string }> = [];
if (opts.userPiecesRootDir && user) {
const ucDir = userPiecesDir(opts.userPiecesRootDir, user.id);
if (existsSync(ucDir)) sources.push({ dir: ucDir, source: 'user-custom', ownerId: user.id });
}
if (opts.customPiecesDir && existsSync(opts.customPiecesDir)) {
sources.push({ dir: opts.customPiecesDir, source: 'global-custom' });
}
if (existsSync(opts.piecesDir)) {
sources.push({ dir: opts.piecesDir, source: 'builtin' });
}
for (const { dir, source, ownerId } of sources) {
for (const f of listPieceFiles(dir)) {
try {
const p = loadPieceFile(f);
const name = p.name ?? f.replace(/.*\//, '').replace('.yaml', '');
if (seen.has(name)) continue;
seen.add(name);
// Drift is meaningful only for global-custom that shadows a built-in.
let drift: DriftStatus | undefined;
if (source === 'global-custom' && existsSync(opts.piecesDir)) {
const builtinPath = join(opts.piecesDir, `${name}.yaml`);
drift = detectDrift(f, builtinPath);
}
pieces.push({
name,
description: p.description,
triggers: p.triggers,
requiredMcp: Array.isArray(p.required_mcp) ? p.required_mcp.filter((v: unknown): v is string => typeof v === 'string') : undefined,
custom: source !== 'builtin',
source,
ownerId,
drift,
});
} catch {
// skip malformed piece files
}
}
}
res.json({ pieces });
} catch (e) {
res.status(500).json({ error: `Failed to list pieces: ${e}` });
}
});
app.get('/api/pieces/:name', (req: Request, res: Response) => {
if (!validateName(req.params.name)) { res.status(400).json({ error: 'Invalid piece name' }); return; }
try {
const user = getUser(req);
const found = findPieceForCaller(opts, user, req.params.name);
if (!found) { res.status(404).json({ error: 'Piece not found' }); return; }
const piece = loadPieceFile(found.path);
res.json({
piece: {
...piece,
requiredMcp: Array.isArray(piece.required_mcp) ? piece.required_mcp.filter((v: unknown): v is string => typeof v === 'string') : undefined,
},
custom: found.source !== 'builtin',
source: found.source,
ownerId: found.ownerId,
});
} catch (e) {
res.status(500).json({ error: `Failed to read piece: ${e}` });
}
});
app.put('/api/pieces/:name', (req: Request, res: Response) => {
if (!validateName(req.params.name)) { res.status(400).json({ error: 'Invalid piece name' }); return; }
try {
const user = getUser(req);
const found = findPieceForCaller(opts, user, req.params.name);
if (!found) { res.status(404).json({ error: 'Piece not found' }); return; }
// Authz: built-in / global-custom → admin (or legacy no-auth); user-custom → owner (or admin).
if (found.source !== 'user-custom') {
if (!isAdminOrLegacy(user)) {
res.status(403).json({ ok: false, error: 'Only admins can modify built-in or global-custom pieces' });
return;
}
} else if (found.ownerId !== user?.id && !isAdminOrLegacy(user)) {
// Different user's user-custom — and not admin. Should be unreachable since
// findPieceForCaller scopes user-custom to the caller, but guard anyway.
res.status(403).json({ ok: false, error: "Cannot modify another user's custom piece" });
return;
}
const error = validatePiece(req.body);
if (error) { res.status(400).json({ ok: false, error }); return; }
if (req.body.name !== req.params.name) {
res.status(400).json({ ok: false, error: 'Body name must match URL parameter' }); return;
}
// Use parseDocument + setIn so untouched regions keep their original
// formatting (block styles, inline arrays, blank lines, comments).
// Full re-serialization via stringify would e.g. convert `instruction: |`
// to `instruction: >`, changing runtime prompt behavior. See #151.
const originalText = readFileSync(found.path, 'utf-8');
const patched = patchYaml(originalText, req.body);
writeFileSync(found.path, patched, 'utf-8');
res.json({ ok: true });
} catch (e) {
res.status(500).json({ error: `Failed to update piece: ${e}` });
}
});
app.post('/api/pieces', (req: Request, res: Response) => {
try {
const error = validatePiece(req.body);
if (error) { res.status(400).json({ ok: false, error }); return; }
const user = getUser(req);
const adminOrLegacy = isAdminOrLegacy(user);
// Determine destination dir:
// - admin / legacy → preserve existing behavior (write to piecesDir).
// - non-admin user → write to their user-custom dir.
let destDir: string;
if (adminOrLegacy) {
destDir = opts.piecesDir;
} else {
if (!opts.userPiecesRootDir) {
res.status(503).json({ ok: false, error: 'User pieces directory not configured on this server' });
return;
}
destDir = userPiecesDir(opts.userPiecesRootDir, user!.id);
mkdirSync(destDir, { recursive: true });
}
// Reject if any visible-to-caller piece with this name already exists
// (built-in, global-custom, or caller's user-custom).
if (findPieceForCaller(opts, user, req.body.name)) {
res.status(409).json({ ok: false, error: 'Piece already exists' }); return;
}
const filePath = join(destDir, `${req.body.name}.yaml`);
writeFileSync(filePath, stringify(req.body, { lineWidth: 120 }), 'utf-8');
logger.info(`[pieces-api] created piece=${req.body.name} dest=${destDir} actor=${user?.id ?? 'legacy'}`);
res.status(201).json({ ok: true });
} catch (e) {
res.status(500).json({ error: `Failed to create piece: ${e}` });
}
});
app.delete('/api/pieces/:name', (req: Request, res: Response) => {
if (!validateName(req.params.name)) { res.status(400).json({ error: 'Invalid piece name' }); return; }
if (req.params.name === 'general' || req.params.name === 'chat') {
res.status(403).json({ ok: false, error: 'Cannot delete general piece' }); return;
}
try {
const user = getUser(req);
const found = findPieceForCaller(opts, user, req.params.name);
if (!found) { res.status(404).json({ error: 'Piece not found' }); return; }
// Authz mirrors PUT: built-in / global-custom → admin; user-custom → owner.
if (found.source !== 'user-custom') {
if (!isAdminOrLegacy(user)) {
res.status(403).json({ ok: false, error: 'Only admins can delete built-in or global-custom pieces' });
return;
}
} else if (found.ownerId !== user?.id && !isAdminOrLegacy(user)) {
res.status(403).json({ ok: false, error: "Cannot delete another user's custom piece" });
return;
}
unlinkSync(found.path);
logger.info(`[pieces-api] deleted piece=${req.params.name} source=${found.source} actor=${user?.id ?? 'legacy'}`);
res.json({ ok: true });
} catch (e) {
res.status(500).json({ error: `Failed to delete piece: ${e}` });
}
});
}