MVP底座⑤:自定义工具导入路由 —— 与第一方同门两审

顺手补两处底座:试运行审此前传空连接,靠 endpoint 干活的工具一律过不了门(这道门
只对不需要连接的工具有效);现传本次申报的连接值。未注册第一方处理器的工具走通用
HTTP 实现 —— 导入的工具由此可真调、可真审:端点不通就是不通,不入目录。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-28 01:58:38 +08:00
co-authored by Claude Fable 5
parent 0fe5222f79
commit 8e7509f65b
9 changed files with 292 additions and 4 deletions
+28
View File
@@ -23,6 +23,7 @@ import {
listToolsRoute,
listTriggersRoute,
publishProcessRoute,
registerToolRoute,
saveProcessRoute,
reassignTaskRoute,
resumeTaskRoute,
@@ -63,6 +64,7 @@ export function registerGovernanceOpsRoutes(app: Hono<AppEnv>, container: Contai
const governTrigger = guard('trigger', 'govern');
const readActivity = guard('activity', 'read');
const readTool = guard('tool', 'read');
const governTool = guard('tool', 'govern');
const readData = guard('data', 'read');
const readMemory = guard('memory', 'read');
@@ -376,6 +378,32 @@ export function registerGovernanceOpsRoutes(app: Hono<AppEnv>, container: Contai
/* ── 工具管理 ────────────────────────────────────────────────────── */
/**
* 导入自定义工具 —— 与第一方走同一条门:静态审 + 试运行审,过了才入目录。
* 入目录不等于能用:还要治理台把它放进某个活动的白名单(授权在白名单与契约,不在活动代码)。
*/
app.post(registerToolRoute.path, async (c) => {
const denied = await governTool(c.get('account'));
if (denied) return c.json(denied, denyStatus(denied));
const parsed = registerToolRoute.body.safeParse(await c.req.json());
if (!parsed.success) {
const body: ApiError = { code: 'validation-failed', message: '工具契约不合 schema', detail: parsed.error.issues };
return c.json(body, 400);
}
const { contract, samples, connectionValues } = parsed.data;
const verdict = await container.toolbox.register({
contract,
// 期望形状统一按「出参是个对象」收:细的形状由契约 output schema 自己说
samples: samples.map((s) => ({ input: s.input, expectShape: { type: 'object' } })),
...(connectionValues === undefined ? {} : { connectionValues }),
});
return c.json({
accepted: verdict.accepted,
findings: verdict.staticFindings.map((f) => ({ code: f.code, severity: f.severity, message: f.message })),
trialRun: { ran: verdict.trialRun.ran, passed: verdict.trialRun.passed, failures: [...verdict.trialRun.failures] },
});
});
app.get(listToolsRoute.path, async (c) => {
const denied = await readTool(c.get('account'));
if (denied) return c.json(denied, denyStatus(denied));