maestro/src/bridge/space-tool-policy-api.ts
oss-sync b857c33ef6
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (f6d625db)
2026-06-26 03:35:45 +00:00

132 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import express, { Router } from 'express';
import { canManageSpace } from './visibility.js';
import { viewerOf } from './space-viewer.js';
import { logger } from '../logger.js';
import { parseToolPolicy } from '../engine/workspace-tool-policy.js';
import {
listToolCategories,
SENSITIVE_CATEGORIES,
SENSITIVE_TOOLS,
} from '../engine/tools/tool-categories.js';
import type { SpaceApiDeps } from './space-api.js';
/** スペースのツールポリシー GET/PUT。createSpaceApi から分離(巨大関数分割)。挙動は等価。 */
export function registerSpaceToolPolicyRoutes(router: Router, deps: SpaceApiDeps): void {
const { repo } = deps;
const jsonParser = express.json({ limit: deps.uploadLimitMb ? `${deps.uploadLimitMb}mb` : '50mb' });
// ─── ツールポリシー ─────────────────────────────────────────────────────────
//
// GET: スペース可視者viewer 以上)が読める。
// PUT: canManageSpaceowner / admin / member.role==='owner')のみ書ける。
//
// 返却形状GET / PUT 共通):
// { policy: WorkspaceToolPolicy,
// categories: { name, sensitive, enabled }[],
// sensitiveTools: { name, enabled }[] }
//
// 未知のカテゴリ/ツール名を enabledSensitive / disabledSafe に含めると 400。
// 既知かどうかの判定: SENSITIVE_CATEGORIES listToolCategories() の和集合 +
// SENSITIVE_TOOLS を allowlist として使う。
async function buildPolicyResponse(spaceId: string) {
const json = repo.getSpaceToolPolicy(spaceId);
const policy = parseToolPolicy(json);
const allCats = await listToolCategories();
const disabledSafe = new Set(policy.disabledSafe ?? []);
const enabledSensitive = new Set(policy.enabledSensitive ?? []);
const categories = allCats.map((name) => {
const sensitive = SENSITIVE_CATEGORIES.has(name);
const enabled = sensitive ? enabledSensitive.has(name) : !disabledSafe.has(name);
return { name, sensitive, enabled };
});
const sensitiveTools = [...SENSITIVE_TOOLS].map((name) => ({
name,
enabled: enabledSensitive.has(name),
}));
return { policy, categories, sensitiveTools };
}
// 既知のカテゴリ+ツール名の allowlist を組み立てるvalidation 用)
async function buildKnownSensitiveNames(): Promise<Set<string>> {
const allCats = await listToolCategories();
const known = new Set<string>(SENSITIVE_CATEGORIES);
for (const c of allCats) known.add(c); // all cats are valid candidates
for (const t of SENSITIVE_TOOLS) known.add(t);
return known;
}
// GET /:id/tool-policy — viewer 可
router.get('/:id/tool-policy', async (req, res) => {
const viewer = viewerOf(req, deps.authActive);
const space = await repo.getSpace(req.params.id, { viewer });
if (!space) return res.status(404).json({ error: 'not found' });
try {
const body = await buildPolicyResponse(space.id);
res.json(body);
} catch (err) {
logger.error(`[space-api] tool-policy GET failed space=${space.id} err=${(err as Error).message}`);
res.status(500).json({ error: 'internal error' });
}
});
// PUT /:id/tool-policy — canManageSpace 必須
router.put('/:id/tool-policy', jsonParser, async (req, res) => {
const viewer = viewerOf(req, deps.authActive);
const space = await repo.getSpace(req.params.id, { viewer });
if (!space) return res.status(404).json({ error: 'not found' });
const memberRole = repo.getSpaceMemberRole(space.id, viewer.id);
if (!canManageSpace(viewer, { ownerId: space.ownerId }, memberRole)) {
return res.status(403).json({ error: 'forbidden' });
}
const { disabledSafe, enabledSensitive } = req.body ?? {};
// Validate: both fields must be arrays of strings if present
if (disabledSafe !== undefined && !Array.isArray(disabledSafe)) {
return res.status(400).json({ error: 'disabledSafe must be an array' });
}
if (enabledSensitive !== undefined && !Array.isArray(enabledSensitive)) {
return res.status(400).json({ error: 'enabledSensitive must be an array' });
}
if (disabledSafe && disabledSafe.some((x: unknown) => typeof x !== 'string')) {
return res.status(400).json({ error: 'disabledSafe must be an array of strings' });
}
if (enabledSensitive && enabledSensitive.some((x: unknown) => typeof x !== 'string')) {
return res.status(400).json({ error: 'enabledSensitive must be an array of strings' });
}
// Validate: unknown category/tool names → 400
const known = await buildKnownSensitiveNames();
if (disabledSafe) {
const unknown = (disabledSafe as string[]).filter((n) => !known.has(n));
if (unknown.length > 0) {
return res.status(400).json({ error: `unknown category names in disabledSafe: ${unknown.join(', ')}` });
}
}
if (enabledSensitive) {
const unknown = (enabledSensitive as string[]).filter((n) => !known.has(n));
if (unknown.length > 0) {
return res.status(400).json({ error: `unknown names in enabledSensitive: ${unknown.join(', ')}` });
}
}
const cleanPolicy = {
...(disabledSafe && disabledSafe.length > 0 ? { disabledSafe: disabledSafe as string[] } : {}),
...(enabledSensitive && enabledSensitive.length > 0 ? { enabledSensitive: enabledSensitive as string[] } : {}),
};
try {
repo.setSpaceToolPolicy(space.id, JSON.stringify(cleanPolicy));
const body = await buildPolicyResponse(space.id);
res.json(body);
} catch (err) {
logger.error(`[space-api] tool-policy PUT failed space=${space.id} err=${(err as Error).message}`);
res.status(500).json({ error: 'internal error' });
}
});
}