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
+23 -5
View File
@@ -9,11 +9,17 @@
*/
/**
* A named role preset that maps to a default model. Roles are **data**, not a
* code enum: admin/teachers define them (ADR-0017: role-based routing is
* product config, not a spec invariant). `roleId` is an opaque string here —
* the registry holds the role set, so new roles are added by configuration,
* not by editing this file.
* A named role preset — the full per-run agent bundle. Roles are **data**, not
* a code enum: admin/teachers define them (ADR-0017: role-based routing is
* product config, not a spec invariant). `roleId` is an opaque string and
* doubles as the slash-command name (`/draft ...`) — the registry holds the
* role set, so new roles are added by configuration, not by editing code.
*
* A role bundles everything that distinguishes one agent persona from another:
* model, system prompt, and the tool surface (files / cph / feishu / skills /
* mcps). Per-run tool whitelisting is enforced by {@link ToolRegistry.subset};
* the model never sees tools outside its role's whitelist, even if it tries to
* call them — security does not rely on the system prompt.
*/
export interface RoleEntry {
readonly id: string;
@@ -21,6 +27,18 @@ export interface RoleEntry {
readonly label: string;
/** Default model id for runs under this role, if a routing rule is set. */
readonly defaultModel: string | undefined;
/**
* System prompt seeding the agent's persona/instructions. Prepended to the
* run's messages only at session start (resume reads it from the transcript,
* see runner.ts). `undefined` ⇒ no system prompt (bare run).
*/
readonly systemPrompt: string | undefined;
/**
* Tool names this role may use (whitelist). `undefined` ⇒ the full registered
* set (back-compat / unrestricted roles). An empty array ⇒ no tools at all.
* Names not present in the registry are silently dropped at run setup.
*/
readonly tools: readonly string[] | undefined;
}
/** A model the admin has enabled for use by the Hub. */