feat(hub): RoleEntry 完整 bundle + per-run tool 白名单 + per-role 触发权限

RoleEntry 扩成 (model, systemPrompt, tools) bundle,id 兼任 slash command 名。
ToolRegistry.subset() 支持按白名单构造 per-run 视图,模型永远看不到 role 外的工具。
trigger 解析 /<role> 命令,查 RoleEntry 组装 systemPrompt + 子集 registry 传给 runner。
新增 RoleTriggerGrant 表 + canTriggerRole gate,与 ADR-0004 canTriggerAgent 串联:
先问'能不能触发 agent',再问'能触发哪个 role'。未配置 role 放行(back-compat)。

- models.ts: RoleEntry 加 systemPrompt + tools 字段
- tools.ts: ToolRegistry.subset(names) 返回共享 handler 的子集视图
- trigger.ts: extractRole 解析 slash; 传 systemPrompt + runTools; catch 链容错 P2025
- runner.ts: RunRequest.systemPrompt 改 string | undefined (exactOptionalPropertyTypes)
- schema.prisma + migration: RoleTriggerGrant(projectId, roleId, principal, revokedAt)
- permission.ts: canTriggerRole gate (有 grant 记录即白名单模式,含 revoked)
- server.ts: draft/review 两个 role 加 systemPrompt + tools 白名单
- 测试: role-permission.test.ts (5) + trigger.test.ts (+3), 53 全绿
This commit is contained in:
2026-07-07 18:47:51 +08:00
parent 0fe6cabc68
commit afaf5bee09
11 changed files with 367 additions and 24 deletions
+25
View File
@@ -60,6 +60,31 @@ export class ToolRegistry {
}
return tool.execute(args, ctx);
}
/**
* Return a per-run view restricted to `names`. Names not registered are
* silently dropped (a role's whitelist may name tools that a particular Hub
* instance doesn't register). The returned registry shares the handler
* references — no copy of the closures.
*
* Per-role tool whitelisting (ADR-0017: role-based routing is product
* config). The runner receives a subset registry so the model literally
* never sees tools outside its role's whitelist: the tool list sent to the
* provider is `subset.specs()`, and `execute` on a name not in the subset
* returns an error.
*/
subset(names: readonly string[]): ToolRegistry {
const allowed = new Set(names);
const view = new ToolRegistry();
for (const [name, tool] of this.byName) {
if (allowed.has(name)) {
// Bypass the duplicate check: we're constructing from an already-valid
// registry, not re-registering.
view.byName.set(name, tool);
}
}
return view;
}
}
/**