refactor: extract execution permission policy

This commit is contained in:
kris
2026-04-02 22:46:41 +08:00
parent 384dd570de
commit a3a4f3e980
8 changed files with 204 additions and 39 deletions

View File

@@ -1,31 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { appendProjectMessage, readState } from "@/lib/boss-data";
import { appendProjectMessage, buildCollaborationGate, readState } from "@/lib/boss-data";
import {
queueGroupDispatchPlan,
queueThreadConversationReplyTask,
replyToMasterAgentUserMessage,
} from "@/lib/boss-master-agent";
function buildCollaborationGate(project?: {
isGroup: boolean;
collaborationMode: "development" | "approval_required";
approvalState: "not_required" | "pending_agent" | "pending_user" | "approved" | "rejected";
}) {
return project
? {
isGroup: project.isGroup,
collaborationMode: project.collaborationMode,
requiresMasterAgentApproval: project.isGroup && project.collaborationMode === "approval_required",
approvalState: project.approvalState,
}
: {
isGroup: false,
collaborationMode: "development" as const,
requiresMasterAgentApproval: false,
approvalState: "not_required" as const,
};
}
import { evaluatePermissionPolicy } from "@/lib/execution/permission-policy";
function dispatchFailureNotice(error?: string) {
switch (error) {
@@ -61,23 +42,28 @@ export async function POST(
(body.kind ?? "text") === "text" &&
(body.body ?? "").trim().length > 0;
if (shouldCreateDispatchPlan && project.collaborationMode === "approval_required") {
const pendingPlan = [...state.dispatchPlans]
.filter(
(plan) => plan.groupProjectId === projectId && plan.status === "pending_user_confirmation",
)
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
if (pendingPlan) {
return NextResponse.json(
{
ok: false,
message: "当前还有一条主 Agent 推荐等待你确认,请先确认或拒绝后再继续发送新指令。",
pendingPlan,
collaborationGate: buildCollaborationGate(project),
},
{ status: 409 },
);
}
const pendingPlan = shouldCreateDispatchPlan
? [...state.dispatchPlans]
.filter(
(plan) => plan.groupProjectId === projectId && plan.status === "pending_user_confirmation",
)
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0]
: null;
const permission = evaluatePermissionPolicy({
project,
hasPendingDispatchPlan: Boolean(pendingPlan),
});
if (!permission.allowed) {
return NextResponse.json(
{
ok: false,
message: permission.reason,
pendingPlan,
collaborationGate: buildCollaborationGate(project),
},
{ status: 409 },
);
}
const message = await appendProjectMessage({

View File

@@ -354,7 +354,18 @@ export interface DispatchExecution {
completedByDeviceId?: string;
}
function buildCollaborationGate(project: Pick<Project, "isGroup" | "collaborationMode" | "approvalState">) {
export function buildCollaborationGate(
project?: Pick<Project, "isGroup" | "collaborationMode" | "approvalState">,
) {
if (!project) {
return {
isGroup: false,
collaborationMode: "development" as const,
requiresMasterAgentApproval: false,
approvalState: "not_required" as const,
};
}
return {
isGroup: project.isGroup,
collaborationMode: project.collaborationMode,

View File

@@ -0,0 +1,104 @@
import { listExecutionTools, type ExecutionToolName } from "@/lib/execution/tool-registry";
type CollaborationMode = "development" | "approval_required";
type ApprovalState = "not_required" | "pending_agent" | "pending_user" | "approved" | "rejected";
type PermissionPolicyProject = {
id: string;
isGroup: boolean;
collaborationMode: CollaborationMode;
approvalState: ApprovalState;
};
type ToolPolicy = {
allowedTools: ExecutionToolName[];
deniedTools: ExecutionToolName[];
};
type CollaborationPolicy = {
mode: CollaborationMode;
canDispatchDirectly: boolean;
canCrossThreadTalk: boolean;
};
type PermissionPolicyResult = {
allowed: boolean;
requiresApproval: boolean;
reason?: string;
toolPolicy: ToolPolicy;
collaborationPolicy: CollaborationPolicy;
};
const DEFAULT_APPROVAL_REQUIRED_REASON =
"当前还有一条主 Agent 推荐等待确认,请先确认或拒绝后再继续发送新指令。";
const EMPTY_TOOL_POLICY: ToolPolicy = {
allowedTools: [],
deniedTools: [],
};
function listDefaultAllowedTools(): ExecutionToolName[] {
return listExecutionTools()
.filter((tool) => tool.kind === "execution")
.map((tool) => tool.name);
}
function buildCollaborationPolicy(mode: CollaborationMode): CollaborationPolicy {
return {
mode,
canDispatchDirectly: mode === "development",
canCrossThreadTalk: mode === "development",
};
}
function buildAllowedPolicy(mode: CollaborationMode, requiresApproval: boolean): PermissionPolicyResult {
return {
allowed: true,
requiresApproval,
toolPolicy: {
allowedTools:
mode === "approval_required"
? ["group_dispatch_plan"]
: listDefaultAllowedTools(),
deniedTools: [],
},
collaborationPolicy: buildCollaborationPolicy(mode),
};
}
export function evaluatePermissionPolicy(input: {
project?: PermissionPolicyProject;
hasPendingDispatchPlan?: boolean;
}): PermissionPolicyResult {
const project = input.project;
if (!project) {
return {
allowed: true,
requiresApproval: false,
toolPolicy: EMPTY_TOOL_POLICY,
collaborationPolicy: buildCollaborationPolicy("development"),
};
}
if (project.isGroup && project.collaborationMode === "approval_required" && input.hasPendingDispatchPlan) {
return {
allowed: false,
requiresApproval: true,
reason: DEFAULT_APPROVAL_REQUIRED_REASON,
toolPolicy: {
allowedTools: ["group_dispatch_plan"],
deniedTools: ["dispatch_execution"],
},
collaborationPolicy: buildCollaborationPolicy("approval_required"),
};
}
return buildAllowedPolicy(
project.collaborationMode,
project.isGroup && project.collaborationMode === "approval_required",
);
}
export const evaluatePermissionPolicyForTesting = evaluatePermissionPolicy;

View File

@@ -0,0 +1,23 @@
export type ExecutionToolKind = "execution" | "analysis";
export type ExecutionToolName =
| "conversation_reply"
| "group_dispatch_plan"
| "dispatch_execution"
| "attachment_analysis";
export interface ExecutionToolDefinition {
name: ExecutionToolName;
kind: ExecutionToolKind;
}
const EXECUTION_TOOLS: readonly ExecutionToolDefinition[] = [
{ name: "conversation_reply", kind: "execution" },
{ name: "group_dispatch_plan", kind: "execution" },
{ name: "dispatch_execution", kind: "execution" },
{ name: "attachment_analysis", kind: "analysis" },
] as const;
export function listExecutionTools() {
return EXECUTION_TOOLS;
}