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
@@ -0,0 +1,27 @@
-- CreateTable
CREATE TABLE "RoleTriggerGrant" (
"id" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"roleId" TEXT NOT NULL,
"principal" TEXT NOT NULL,
"createdByUserId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"revokedAt" TIMESTAMP(3),
CONSTRAINT "RoleTriggerGrant_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "RoleTriggerGrant_projectId_roleId_revokedAt_idx" ON "RoleTriggerGrant"("projectId", "roleId", "revokedAt");
-- CreateIndex
CREATE INDEX "RoleTriggerGrant_principal_revokedAt_idx" ON "RoleTriggerGrant"("principal", "revokedAt");
-- CreateIndex
CREATE UNIQUE INDEX "RoleTriggerGrant_projectId_roleId_principal_revokedAt_key" ON "RoleTriggerGrant"("projectId", "roleId", "principal", "revokedAt");
-- AddForeignKey
ALTER TABLE "RoleTriggerGrant" ADD CONSTRAINT "RoleTriggerGrant_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RoleTriggerGrant" ADD CONSTRAINT "RoleTriggerGrant_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+30
View File
@@ -40,6 +40,7 @@ model User {
heldLocks ProjectAgentLock[] @relation("lockHolder")
feishuBindings ProjectGroupBinding[] @relation("bindingCreator")
permissionGrants PermissionGrant[] @relation("grantCreator")
roleTriggerGrants RoleTriggerGrant[] @relation("roleGrantCreator")
auditEntries AuditEntry[] @relation("auditActor")
}
@@ -81,6 +82,7 @@ model Project {
agentLock ProjectAgentLock?
permissionGrants PermissionGrant[] @relation("projectGrants")
permissionSettings PermissionSettings[] @relation("projectSettings")
roleTriggerGrants RoleTriggerGrant[] @relation("projectRoleGrants")
@@index([archivedAt])
}
@@ -260,6 +262,34 @@ model PermissionSettings {
@@index([resourceType, resourceId])
}
/// Per-role agent trigger grant. Orthogonal to ADR-0004's PermissionGrant:
/// ADR-0004 decides "can trigger an agent at all" (triggerAgent capability,
/// edit+ role); this table decides "can trigger *which* agent role" (e.g.
/// /review vs /draft). Two gates in series — both must pass.
///
/// `roleId` is an opaque string matching RoleEntry.id (ADR-0017: roles are
/// data, not a code enum). `principal` mirrors ADR-0004's opaque principal
/// (sub-typology OPEN). A project-scoped row grants the role on that project;
/// the compound unique covers "one active grant per (project, principal, role)".
/// Revocation via `revokedAt`, same pattern as PermissionGrant.
model RoleTriggerGrant {
id String @id @default(cuid())
projectId String
roleId String
principal String
createdByUserId String?
createdAt DateTime @default(now())
revokedAt DateTime?
project Project @relation("projectRoleGrants", fields: [projectId], references: [id], onDelete: Cascade)
createdBy User? @relation("roleGrantCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
@@unique([projectId, roleId, principal, revokedAt])
@@index([projectId, roleId, revokedAt])
@@index([principal, revokedAt])
}
// --- Audit (ADR Audit, content OPEN) -------------------------------------
/// spec AuditEntry: minimal skeleton — one entry relates to a run. Event type,