feat(FN-3545): add agent permission policy model and persistence

Introduces a multi-agent permission policy model in `@fusion/core` (types, store persistence, API exposure) with corresponding dashboard UI wiring in the agent onboarding modal and task detail modal, plus a distributed task ID overlap retry mechanism for cluster creation, an inline fast mode toggle

Fusion-Task-Id: FN-3545
This commit is contained in:
Fusion
2026-05-07 11:57:24 -07:00
committed by gsxdsm
parent a070c6b995
commit 51c6ddb9a5
9 changed files with 415 additions and 4 deletions

View File

@@ -1862,6 +1862,7 @@ describe("Agent create/update routes", () => {
reportsTo: agentId,
runtimeConfig: { heartbeatIntervalMs: 60000 },
permissions: { read: true },
permissionPolicy: { presetId: "approval-required" },
instructionsPath: "docs/reviewer.md",
instructionsText: "Check test quality.",
soul: "Analytical and thorough.",
@@ -1879,6 +1880,16 @@ describe("Agent create/update routes", () => {
reportsTo: agentId,
runtimeConfig: { heartbeatIntervalMs: 60000 },
permissions: { read: true },
permissionPolicy: {
presetId: "approval-required",
rules: {
"git-write": "require-approval",
"file-write-delete": "require-approval",
"shell-command": "require-approval",
"network-api": "require-approval",
"task-agent-management": "require-approval",
},
},
instructionsPath: "docs/reviewer.md",
instructionsText: "Check test quality.",
soul: "Analytical and thorough.",
@@ -1900,6 +1911,7 @@ describe("Agent create/update routes", () => {
runtimeConfig: { heartbeatTimeoutMs: 120000 },
pauseReason: "manual",
permissions: { deploy: true },
permissionPolicy: { presetId: "locked-down" },
totalInputTokens: 42,
totalOutputTokens: 21,
instructionsPath: "agents/infra.md",
@@ -1921,6 +1933,16 @@ describe("Agent create/update routes", () => {
runtimeConfig: { heartbeatTimeoutMs: 120000 },
pauseReason: "manual",
permissions: { deploy: true },
permissionPolicy: {
presetId: "locked-down",
rules: {
"git-write": "block",
"file-write-delete": "block",
"shell-command": "block",
"network-api": "block",
"task-agent-management": "block",
},
},
totalInputTokens: 42,
totalOutputTokens: 21,
instructionsPath: "agents/infra.md",
@@ -1929,6 +1951,67 @@ describe("Agent create/update routes", () => {
});
});
it("POST /api/agents rejects invalid permissionPolicy preset", async () => {
const res = await REQUEST(
buildAgentApp(),
"POST",
"/api/agents",
JSON.stringify({
name: "Invalid Policy Agent",
role: "executor",
permissionPolicy: { presetId: "custom" },
}),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(400);
expect(res.body.error).toContain("permissionPolicy.presetId");
});
it("PATCH /api/agents/:id rejects invalid permissionPolicy payload shape", async () => {
const res = await REQUEST(
buildAgentApp(),
"PATCH",
`/api/agents/${agentId}`,
JSON.stringify({
permissionPolicy: "bad",
}),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(400);
expect(res.body.error).toContain("permissionPolicy must be an object");
});
it("POST /api/agents normalizes policy rules from preset and ignores caller-supplied rules", async () => {
const res = await REQUEST(
buildAgentApp(),
"POST",
"/api/agents",
JSON.stringify({
name: "Normalized Policy Agent",
role: "executor",
permissionPolicy: {
presetId: "locked-down",
rules: {
"git-write": "allow",
},
},
}),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(201);
expect(res.body.permissionPolicy.presetId).toBe("locked-down");
expect(res.body.permissionPolicy.rules).toEqual({
"git-write": "block",
"file-write-delete": "block",
"shell-command": "block",
"network-api": "block",
"task-agent-management": "block",
});
});
it("POST /api/agents returns 409 for duplicate non-ephemeral names", async () => {
const first = await REQUEST(
buildAgentApp(),