sync: update from private repo (6fcb0d0)

This commit is contained in:
oss-sync
2026-06-04 03:03:12 +00:00
parent 21be01b699
commit 57685d995c
36 changed files with 2467 additions and 391 deletions
+183 -47
View File
@@ -100,6 +100,19 @@ function validateName(name: string): boolean {
return VALID_PIECE_NAME.test(name);
}
const VALID_SOURCES = new Set<string>(['builtin', 'user-custom', 'global-custom']);
/**
* Returns true when `source` is absent (caller wants priority resolution).
* Returns false when `source` is a known valid value.
* Returns a 400 error string when `source` is present but unrecognised.
*/
function parseSourceParam(source: string | undefined): { valid: true; value: PieceSource | undefined } | { valid: false; error: string } {
if (source === undefined) return { valid: true, value: undefined };
if (VALID_SOURCES.has(source)) return { valid: true, value: source as PieceSource };
return { valid: false, error: 'Invalid source' };
}
export function findPieceFile(name: string, piecesDir: string, customPiecesDir?: string): { path: string; custom: boolean } | null {
if (customPiecesDir) {
const customPath = join(customPiecesDir, `${name}.yaml`);
@@ -122,6 +135,9 @@ export interface PiecesApiOptions {
userPiecesRootDir?: string;
}
/** Canonical owner id for no-auth / legacy mode. Mirrors worker-bootstrap and piece-catalog default. */
const LOCAL_OWNER = 'local';
type AuthedUser = { id: string; role?: string };
function getUser(req: Request): AuthedUser | undefined {
@@ -137,6 +153,7 @@ function isAdminOrLegacy(user: AuthedUser | undefined): boolean {
/**
* Lookup priority for a given caller:
* 1. Caller's own user-custom dir (overrides everything below).
* No-auth callers use the 'local' owner id.
* 2. Global custom dir (admin-managed, all users see).
* 3. Built-in dir.
*/
@@ -145,9 +162,10 @@ function findPieceForCaller(
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.userPiecesRootDir) {
const ownerId = user?.id ?? LOCAL_OWNER;
const ucPath = join(userPiecesDir(opts.userPiecesRootDir, ownerId), `${name}.yaml`);
if (existsSync(ucPath)) return { path: ucPath, source: 'user-custom', ownerId };
}
if (opts.customPiecesDir) {
const gcPath = join(opts.customPiecesDir, `${name}.yaml`);
@@ -178,30 +196,31 @@ export function mountPiecesApi(
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 });
// Build custom sources (user-custom first, then global-custom).
// Within custom umbrella: dedup by name so user-custom wins over global-custom.
// Built-ins are ALWAYS emitted separately — they are never hidden by a same-named custom.
const customSources: Array<{ dir: string; source: PieceSource; ownerId?: string }> = [];
if (opts.userPiecesRootDir) {
// No-auth callers use the 'local' owner id, mirroring worker and piece-catalog defaults.
const ownerId = user?.id ?? LOCAL_OWNER;
const ucDir = userPiecesDir(opts.userPiecesRootDir, ownerId);
if (existsSync(ucDir)) customSources.push({ dir: ucDir, source: 'user-custom', ownerId });
}
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' });
customSources.push({ dir: opts.customPiecesDir, source: 'global-custom' });
}
for (const { dir, source, ownerId } of sources) {
// Emit custom pieces (dedup within custom umbrella only).
const seenCustom = new Set<string>();
for (const { dir, source, ownerId } of customSources) {
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.
if (seenCustom.has(name)) continue;
seenCustom.add(name);
let drift: DriftStatus | undefined;
if (source === 'global-custom' && existsSync(opts.piecesDir)) {
const builtinPath = join(opts.piecesDir, `${name}.yaml`);
@@ -212,7 +231,7 @@ export function mountPiecesApi(
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',
custom: true,
source,
ownerId,
drift,
@@ -222,6 +241,27 @@ export function mountPiecesApi(
}
}
}
// Always emit ALL built-ins (never hidden by custom pieces of the same name).
if (existsSync(opts.piecesDir)) {
for (const f of listPieceFiles(opts.piecesDir)) {
try {
const p = loadPieceFile(f);
const name = p.name ?? f.replace(/.*\//, '').replace('.yaml', '');
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: false,
source: 'builtin',
});
} catch {
// skip malformed piece files
}
}
}
res.json({ pieces });
} catch (e) {
res.status(500).json({ error: `Failed to list pieces: ${e}` });
@@ -232,7 +272,31 @@ export function mountPiecesApi(
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);
const sourceParsed = parseSourceParam(req.query.source as string | undefined);
if (!sourceParsed.valid) { res.status(400).json({ ok: false, error: sourceParsed.error }); return; }
const requestedSource = sourceParsed.value;
let found: { path: string; source: PieceSource; ownerId?: string } | null = null;
if (requestedSource === 'builtin') {
const biPath = join(opts.piecesDir, `${req.params.name}.yaml`);
if (existsSync(biPath)) found = { path: biPath, source: 'builtin' };
} else if (requestedSource === 'user-custom') {
if (opts.userPiecesRootDir) {
const ownerId = user?.id ?? LOCAL_OWNER;
const ucPath = join(userPiecesDir(opts.userPiecesRootDir, ownerId), `${req.params.name}.yaml`);
if (existsSync(ucPath)) found = { path: ucPath, source: 'user-custom', ownerId };
}
} else if (requestedSource === 'global-custom') {
if (opts.customPiecesDir) {
const gcPath = join(opts.customPiecesDir, `${req.params.name}.yaml`);
if (existsSync(gcPath)) found = { path: gcPath, source: 'global-custom' };
}
} else {
// No source param: priority resolution (user-custom > global-custom > builtin).
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({
@@ -253,7 +317,29 @@ export function mountPiecesApi(
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);
const sourceParsed = parseSourceParam(req.query.source as string | undefined);
if (!sourceParsed.valid) { res.status(400).json({ ok: false, error: sourceParsed.error }); return; }
const requestedSource = sourceParsed.value;
let found: { path: string; source: PieceSource; ownerId?: string } | null = null;
if (requestedSource === 'builtin') {
const biPath = join(opts.piecesDir, `${req.params.name}.yaml`);
if (existsSync(biPath)) found = { path: biPath, source: 'builtin' };
} else if (requestedSource === 'user-custom') {
if (opts.userPiecesRootDir) {
const ownerId = user?.id ?? LOCAL_OWNER;
const ucPath = join(userPiecesDir(opts.userPiecesRootDir, ownerId), `${req.params.name}.yaml`);
if (existsSync(ucPath)) found = { path: ucPath, source: 'user-custom', ownerId };
}
} else if (requestedSource === 'global-custom') {
if (opts.customPiecesDir) {
const gcPath = join(opts.customPiecesDir, `${req.params.name}.yaml`);
if (existsSync(gcPath)) found = { path: gcPath, source: 'global-custom' };
}
} else {
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).
@@ -262,7 +348,7 @@ export function mountPiecesApi(
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)) {
} else if (found.ownerId !== (user?.id ?? LOCAL_OWNER) && !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" });
@@ -293,33 +379,55 @@ export function mountPiecesApi(
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.
// POST always creates in user-custom dir. "+" is always Create Custom.
// Admins edit built-ins via PUT on existing ones, not by POST-creating new built-ins.
// No-auth / legacy with userPiecesRootDir: use the 'local' owner id so pieces are
// user-custom (deletable) and never pollute piecesDir.
// No-auth / legacy WITHOUT userPiecesRootDir: fall back to global customPiecesDir or piecesDir.
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);
let createdSource: PieceSource;
if (opts.userPiecesRootDir) {
// Both authenticated and no-auth go to user-custom dir when userPiecesRootDir is set.
// Authenticated users: 503 guard below is only needed when root is NOT set.
const ownerId = user?.id ?? LOCAL_OWNER;
destDir = userPiecesDir(opts.userPiecesRootDir, ownerId);
mkdirSync(destDir, { recursive: true });
createdSource = 'user-custom';
} else if (user) {
// Authenticated caller but userPiecesRootDir is not configured — cannot safely write.
res.status(503).json({ ok: false, error: 'User piece storage not configured' });
return;
} else if (opts.customPiecesDir) {
// Legacy (no-auth) with global custom dir configured.
destDir = opts.customPiecesDir;
mkdirSync(destDir, { recursive: true });
createdSource = 'global-custom';
} else {
// Pure legacy single-user: fall back to piecesDir (existing behavior).
destDir = opts.piecesDir;
createdSource = 'builtin';
}
// Reject if any visible-to-caller piece with this name already exists
// (built-in, global-custom, or caller's user-custom).
// Always reject if the name collides with a built-in — Custom and Default are
// separate namespaces. (This also covers the admin case, since admins should
// use PUT to update an existing built-in, not POST to create a duplicate.)
const builtinPath = join(opts.piecesDir, `${req.body.name}.yaml`);
if (existsSync(builtinPath) && destDir !== opts.piecesDir) {
res.status(409).json({ ok: false, error: `"${req.body.name}" は組み込み (built-in) Piece と同名です。Custom Piece には別名を付けてください。` });
return;
}
// Reject if the caller's visible piece with this name already exists
// (global-custom or caller's own 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 });
logger.info(`[pieces-api] created piece=${req.body.name} dest=${destDir} source=${createdSource} actor=${user?.id ?? 'legacy'}`);
res.status(201).json({ ok: true, source: createdSource });
} catch (e) {
res.status(500).json({ error: `Failed to create piece: ${e}` });
}
@@ -327,23 +435,51 @@ export function mountPiecesApi(
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);
const sourceParsed = parseSourceParam(req.query.source as string | undefined);
if (!sourceParsed.valid) { res.status(400).json({ ok: false, error: sourceParsed.error }); return; }
const requestedSource = sourceParsed.value;
let found: { path: string; source: PieceSource; ownerId?: string } | null = null;
if (requestedSource === 'builtin') {
const biPath = join(opts.piecesDir, `${req.params.name}.yaml`);
if (existsSync(biPath)) found = { path: biPath, source: 'builtin' };
} else if (requestedSource === 'user-custom') {
if (opts.userPiecesRootDir) {
const ownerId = user?.id ?? LOCAL_OWNER;
const ucPath = join(userPiecesDir(opts.userPiecesRootDir, ownerId), `${req.params.name}.yaml`);
if (existsSync(ucPath)) found = { path: ucPath, source: 'user-custom', ownerId };
}
} else if (requestedSource === 'global-custom') {
if (opts.customPiecesDir) {
const gcPath = join(opts.customPiecesDir, `${req.params.name}.yaml`);
if (existsSync(gcPath)) found = { path: gcPath, source: 'global-custom' };
}
} else {
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') {
// Built-in (Default) pieces are non-deletable for everyone — including admins.
// This covers general/chat and all other built-ins. Admins may still EDIT
// built-ins via PUT; only deletion is prohibited.
if (found.source === 'builtin') {
res.status(403).json({ ok: false, error: 'Cannot delete a built-in (Default) piece' }); return;
}
// Authz for non-builtin sources: global-custom → admin; user-custom → owner.
if (found.source === 'global-custom') {
if (!isAdminOrLegacy(user)) {
res.status(403).json({ ok: false, error: 'Only admins can delete built-in or global-custom pieces' });
res.status(403).json({ ok: false, error: 'Only admins can delete global-custom pieces' });
return;
}
} else if (found.source === 'user-custom') {
if (found.ownerId !== (user?.id ?? LOCAL_OWNER) && !isAdminOrLegacy(user)) {
res.status(403).json({ ok: false, error: "Cannot delete another user's custom piece" });
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);