feat(FN-4488): complete Step 2 — engine project-default wiring
Fusion-Task-Id: FN-4488 Fusion-Task-Lineage: 3fc1ac43-490f-43f2-a27e-4fdceb64d9c4
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, resolveEffectiveAgentPermissionPolicy, type AgentPermissionPolicyRules } from "@fusion/core";
|
||||
import { evaluateAgentActionGate } from "../agent-action-gate.js";
|
||||
|
||||
describe("agent action gate project-default resolution", () => {
|
||||
it("applies project default rules when agent policy is undefined", () => {
|
||||
const requireApprovalRules = AGENT_PERMISSION_POLICY_ACTION_CATEGORIES.reduce((acc, category) => {
|
||||
acc[category] = "require-approval";
|
||||
return acc;
|
||||
}, {} as Partial<AgentPermissionPolicyRules>);
|
||||
|
||||
const permissionPolicy = resolveEffectiveAgentPermissionPolicy(undefined, {
|
||||
rules: requireApprovalRules,
|
||||
});
|
||||
|
||||
const decisions = [
|
||||
evaluateAgentActionGate({ agentId: "a1", toolName: "bash", args: { command: "git commit -m x" }, permissionPolicy }),
|
||||
evaluateAgentActionGate({ agentId: "a1", toolName: "write", args: { path: "x.ts", content: "x" }, permissionPolicy }),
|
||||
evaluateAgentActionGate({ agentId: "a1", toolName: "bash", args: { command: "pnpm test" }, permissionPolicy }),
|
||||
evaluateAgentActionGate({ agentId: "a1", toolName: "fn_web_fetch", args: { url: "https://example.com" }, permissionPolicy }),
|
||||
evaluateAgentActionGate({ agentId: "a1", toolName: "fn_task_add_dep", args: { task_id: "FN-1" }, permissionPolicy }),
|
||||
];
|
||||
|
||||
for (const decision of decisions) {
|
||||
if (decision.category === "exempt") {
|
||||
continue;
|
||||
}
|
||||
expect(decision.disposition).toBe("require-approval");
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps per-agent custom rule over project default", () => {
|
||||
const permissionPolicy = resolveEffectiveAgentPermissionPolicy(
|
||||
{
|
||||
presetId: "custom",
|
||||
rules: { command_execution: "allow" },
|
||||
},
|
||||
{ rules: { command_execution: "require-approval" } },
|
||||
);
|
||||
|
||||
const decision = evaluateAgentActionGate({
|
||||
agentId: "a1",
|
||||
toolName: "bash",
|
||||
args: { command: "pnpm test" },
|
||||
permissionPolicy,
|
||||
});
|
||||
|
||||
expect(decision.disposition).toBe("allow");
|
||||
});
|
||||
});
|
||||
@@ -892,11 +892,11 @@ export class HeartbeatMonitor {
|
||||
return this.approvalRequestStore;
|
||||
}
|
||||
|
||||
private buildActionGateContext(agent: Agent, taskId?: string, runId?: string): AgentActionGateContext | undefined {
|
||||
private buildActionGateContext(agent: Agent, taskId?: string, runId?: string, projectDefaultPolicy?: { rules?: import("@fusion/core").AgentPermissionPolicy["rules"] }): AgentActionGateContext | undefined {
|
||||
if (isEphemeralAgent(agent)) {
|
||||
return undefined;
|
||||
}
|
||||
const policy = resolveEffectiveAgentPermissionPolicy(agent.permissionPolicy);
|
||||
const policy = resolveEffectiveAgentPermissionPolicy(agent.permissionPolicy, projectDefaultPolicy);
|
||||
return {
|
||||
agentId: agent.id,
|
||||
agentName: agent.name,
|
||||
@@ -945,13 +945,13 @@ export class HeartbeatMonitor {
|
||||
};
|
||||
}
|
||||
|
||||
private buildPermanentAgentGatingContext(agent: Agent, taskId?: string, runId?: string): import("@fusion/core").PermanentAgentGatingContext | undefined {
|
||||
private buildPermanentAgentGatingContext(agent: Agent, taskId?: string, runId?: string, projectDefaultPolicy?: { rules?: import("@fusion/core").AgentPermissionPolicy["rules"] }): import("@fusion/core").PermanentAgentGatingContext | undefined {
|
||||
if (isEphemeralAgent(agent)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
permissionPolicy: resolveEffectiveAgentPermissionPolicy(agent.permissionPolicy),
|
||||
permissionPolicy: resolveEffectiveAgentPermissionPolicy(agent.permissionPolicy, projectDefaultPolicy),
|
||||
requester: { actorId: agent.id, actorType: "agent", actorName: agent.name },
|
||||
taskId,
|
||||
runId,
|
||||
@@ -2330,8 +2330,8 @@ export class HeartbeatMonitor {
|
||||
},
|
||||
// Skill selection: use waking agent's skills (heartbeat has no role fallback)
|
||||
...(skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
|
||||
actionGateContext: this.buildActionGateContext(agent, taskId, run.id),
|
||||
permanentAgentGating: this.buildPermanentAgentGatingContext(agent, taskId, run.id),
|
||||
actionGateContext: this.buildActionGateContext(agent, taskId, run.id, heartbeatModelSettings.defaultAgentPermissionPolicy),
|
||||
permanentAgentGating: this.buildPermanentAgentGatingContext(agent, taskId, run.id, heartbeatModelSettings.defaultAgentPermissionPolicy),
|
||||
});
|
||||
|
||||
// Track for monitoring
|
||||
|
||||
@@ -974,11 +974,11 @@ export class TaskExecutor {
|
||||
return this._approvalRequestStore;
|
||||
}
|
||||
|
||||
private buildActionGateContext(taskId: string | undefined, agent: Agent | null | undefined): AgentActionGateContext | undefined {
|
||||
private buildActionGateContext(taskId: string | undefined, agent: Agent | null | undefined, projectDefaultPolicy?: { rules?: import("@fusion/core").AgentPermissionPolicy["rules"] }): AgentActionGateContext | undefined {
|
||||
if (!agent || isEphemeralAgent(agent)) {
|
||||
return undefined;
|
||||
}
|
||||
const policy = resolveEffectiveAgentPermissionPolicy(agent.permissionPolicy);
|
||||
const policy = resolveEffectiveAgentPermissionPolicy(agent.permissionPolicy, projectDefaultPolicy);
|
||||
return {
|
||||
agentId: agent.id,
|
||||
agentName: agent.name,
|
||||
@@ -1040,13 +1040,13 @@ export class TaskExecutor {
|
||||
};
|
||||
}
|
||||
|
||||
private buildPermanentAgentGatingContext(taskId: string | undefined, agent: Agent | null | undefined): import("@fusion/core").PermanentAgentGatingContext | undefined {
|
||||
private buildPermanentAgentGatingContext(taskId: string | undefined, agent: Agent | null | undefined, projectDefaultPolicy?: { rules?: import("@fusion/core").AgentPermissionPolicy["rules"] }): import("@fusion/core").PermanentAgentGatingContext | undefined {
|
||||
if (!agent || isEphemeralAgent(agent)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
permissionPolicy: resolveEffectiveAgentPermissionPolicy(agent.permissionPolicy),
|
||||
permissionPolicy: resolveEffectiveAgentPermissionPolicy(agent.permissionPolicy, projectDefaultPolicy),
|
||||
requester: {
|
||||
actorId: agent.id,
|
||||
actorType: "agent",
|
||||
@@ -2809,8 +2809,8 @@ export class TaskExecutor {
|
||||
pluginRunner: this.options.pluginRunner,
|
||||
runtimeHint: stepSessionRuntimeHint,
|
||||
assignedAgentRuntimeConfig: (stepSessionAgent?.runtimeConfig ?? undefined) as Record<string, unknown> | undefined,
|
||||
actionGateContext: this.buildActionGateContext(task.id, stepSessionAgent),
|
||||
permanentAgentGating: this.buildPermanentAgentGatingContext(task.id, stepSessionAgent),
|
||||
actionGateContext: this.buildActionGateContext(task.id, stepSessionAgent, settings.defaultAgentPermissionPolicy),
|
||||
permanentAgentGating: this.buildPermanentAgentGatingContext(task.id, stepSessionAgent, settings.defaultAgentPermissionPolicy),
|
||||
// Pass skill selection context from the main executor session
|
||||
skillSelection: skillContext.skillSelectionContext,
|
||||
// Pass agentStore and messageStore for delegation and messaging tools
|
||||
@@ -3416,8 +3416,8 @@ export class TaskExecutor {
|
||||
taskEnv,
|
||||
// Skill selection: use assigned agent skills if available, otherwise role fallback
|
||||
...(skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
|
||||
actionGateContext: this.buildActionGateContext(task.id, assignedAgent),
|
||||
permanentAgentGating: this.buildPermanentAgentGatingContext(task.id, assignedAgent),
|
||||
actionGateContext: this.buildActionGateContext(task.id, assignedAgent, settings.defaultAgentPermissionPolicy),
|
||||
permanentAgentGating: this.buildPermanentAgentGatingContext(task.id, assignedAgent, settings.defaultAgentPermissionPolicy),
|
||||
taskId: task.id,
|
||||
taskTitle: detail.title,
|
||||
onFallbackModelUsed: createFallbackModelObserver({
|
||||
@@ -3770,8 +3770,8 @@ export class TaskExecutor {
|
||||
taskEnv,
|
||||
// Skill selection: use assigned agent skills if available, otherwise role fallback
|
||||
...(skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
|
||||
actionGateContext: this.buildActionGateContext(task.id, assignedAgent),
|
||||
permanentAgentGating: this.buildPermanentAgentGatingContext(task.id, assignedAgent),
|
||||
actionGateContext: this.buildActionGateContext(task.id, assignedAgent, settings.defaultAgentPermissionPolicy),
|
||||
permanentAgentGating: this.buildPermanentAgentGatingContext(task.id, assignedAgent, settings.defaultAgentPermissionPolicy),
|
||||
});
|
||||
if (retrySessionFile) {
|
||||
this.store.updateTask(task.id, { sessionFile: retrySessionFile }).catch((err: unknown) => {
|
||||
|
||||
Reference in New Issue
Block a user