feat(FN-3956): execute provisioning on approval decisions
The merge completes Step 1 of agent provisioning approval followthrough, executing provisioning actions when approval decisions are made, with supporting documentation in AGENTS.md and docs/agents.md. Test coverage spans provisioning policy, approval routes, gating classifications, extension integra Fusion-Task-Id: FN-3956
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Wire dashboard approval decisions for `agent_provisioning` requests to execute deferred agent create/delete actions.
|
||||
|
||||
Add focused test coverage for provisioning decision routing, policy/gating contracts, and approval request category round-trips.
|
||||
@@ -311,10 +311,14 @@ Six tools enable inter-agent coordination — discovering agents, provisioning/d
|
||||
|
||||
Create a non-ephemeral agent that reports to the caller (or, for CEO-level callers, any `reportsTo` target).
|
||||
|
||||
Provisioning can be policy-gated (`projectSettings.agentProvisioning`). Tool responses include `details.outcome` of `created`, `pending_approval`, or `denied`. Pending requests are resolved via dashboard/API approval decision route (`POST /api/approvals/:id/decision`), which executes deferred creation on approve.
|
||||
|
||||
### `agent_delete` Tool
|
||||
|
||||
Delete a non-ephemeral direct report. If the target holds a task checkout lease, deletion is blocked unless `force: true`. Assigned tasks can be reassigned via `reassign_to` or released/unassigned.
|
||||
|
||||
Provisioning policy also applies to deletes (`details.outcome`: `deleted`, `pending_approval`, or `denied`). Approval decisions emit provisioning audit events (`agent:create:approved|denied`, `agent:delete:approved|denied`) tied to the original run/task metadata.
|
||||
|
||||
### `list_agents` Tool
|
||||
|
||||
List all available agents in the system. Shows each agent's name, role, state, personality (soul), and current assignment.
|
||||
|
||||
@@ -102,6 +102,14 @@ Approval pause/resume lifecycle (FN-3548):
|
||||
- Dashboard mailbox entry points (Header/Mobile nav) display pending-approval indicators so waiting approvals are visible before opening Mailbox.
|
||||
- Agents list/board cards and Agent Detail summary display per-agent `pendingApprovalCount` badges to show which agents are blocked by waiting approvals.
|
||||
|
||||
Agent provisioning approvals (`agent_provisioning` category):
|
||||
|
||||
- `fn_agent_create` / `fn_agent_delete` can return `pending_approval` under `projectSettings.agentProvisioning` policy (`approvalMode`, trusted roles/IDs, `alwaysApproveDelete`).
|
||||
- Approval request is persisted with provisioning context (`tool` + `params`) and visible in mailbox/API approval queues.
|
||||
- Dashboard/API decision route `POST /api/approvals/:id/decision` executes deferred provisioning on `approve` via engine dispatcher (`executeApprovedAgentProvisioning`) and never executes on `deny`.
|
||||
- Decision handling emits run-audit mutations: `agent:create:approved`, `agent:create:denied`, `agent:delete:approved`, `agent:delete:denied` using original request task/run/requester linkage.
|
||||
- Malformed provisioning context or failed execution returns 500 from the decision route (no silent approval).
|
||||
|
||||
Default and legacy fallback behavior:
|
||||
|
||||
- New **non-ephemeral/permanent** agents persist a normalized `permissionPolicy` using preset `unrestricted` when not explicitly provided.
|
||||
|
||||
@@ -188,6 +188,32 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("built fn pi extension integr
|
||||
expect(persisted?.description).toBe("Ship the packed CLI contract");
|
||||
});
|
||||
|
||||
it("runs provisioning tools through the built extension", async () => {
|
||||
const createTool = api.tools.get("fn_agent_create")!;
|
||||
const created = await createTool.execute(
|
||||
"create-agent-1",
|
||||
{ name: "built-ext-agent", role: "executor" },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
|
||||
expect(created.details.outcome).toBe("created");
|
||||
expect(created.details.agentId).toMatch(/^agent-/);
|
||||
|
||||
const deleteTool = api.tools.get("fn_agent_delete")!;
|
||||
const deleted = await deleteTool.execute(
|
||||
"delete-agent-1",
|
||||
{ id: created.details.agentId },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
|
||||
expect(deleted.details.outcome).toBe("deleted");
|
||||
expect(deleted.details.agentId).toBe(created.details.agentId);
|
||||
});
|
||||
|
||||
it("delegates to real non-ephemeral agents and rejects runtime workers", async () => {
|
||||
const agent = await seedAgent(tmpDir, { name: "release-agent" });
|
||||
const runtimeWorker = await seedAgent(tmpDir, { name: "runtime-worker", ephemeral: true });
|
||||
|
||||
105
packages/core/src/__tests__/agent-provisioning-policy.test.ts
Normal file
105
packages/core/src/__tests__/agent-provisioning-policy.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { extractAgentProvisioningRequest, resolveAgentProvisioningPolicy } from "../agent-provisioning-policy.js";
|
||||
|
||||
describe("resolveAgentProvisioningPolicy", () => {
|
||||
it("denies missing caller", () => {
|
||||
const decision = resolveAgentProvisioningPolicy({ tool: "fn_agent_create", caller: undefined, settings: undefined });
|
||||
expect(decision.decision).toBe("deny");
|
||||
expect(decision.matchedRule).toBe("missing-caller");
|
||||
});
|
||||
|
||||
it("allows privileged caller", () => {
|
||||
const decision = resolveAgentProvisioningPolicy({
|
||||
tool: "fn_agent_delete",
|
||||
caller: { id: "a1", role: "executor", isPrivileged: true },
|
||||
settings: { agentProvisioning: { approvalMode: "always", alwaysApproveDelete: true } },
|
||||
});
|
||||
expect(decision.decision).toBe("allow");
|
||||
expect(decision.matchedRule).toBe("privileged-caller");
|
||||
});
|
||||
|
||||
it("allows trusted agent id in trusted-only mode", () => {
|
||||
const decision = resolveAgentProvisioningPolicy({
|
||||
tool: "fn_agent_create",
|
||||
caller: { id: "trusted-id" },
|
||||
settings: { agentProvisioning: { approvalMode: "trusted-only", trustedAgentIds: ["trusted-id"] } },
|
||||
});
|
||||
expect(decision.decision).toBe("allow");
|
||||
expect(decision.matchedRule).toBe("trusted-agent-id");
|
||||
});
|
||||
|
||||
it("matches trusted role case-insensitively", () => {
|
||||
const decision = resolveAgentProvisioningPolicy({
|
||||
tool: "fn_agent_create",
|
||||
caller: { id: "a1", role: "CEO" },
|
||||
settings: { agentProvisioning: { approvalMode: "trusted-only", trustedRoles: ["ceo"] } },
|
||||
});
|
||||
expect(decision.decision).toBe("allow");
|
||||
expect(decision.matchedRule).toBe("trusted-role");
|
||||
});
|
||||
|
||||
it("requires approval for untrusted caller in trusted-only mode", () => {
|
||||
const decision = resolveAgentProvisioningPolicy({ tool: "fn_agent_create", caller: { id: "a1" }, settings: undefined });
|
||||
expect(decision.decision).toBe("require-approval");
|
||||
expect(decision.matchedRule).toBe("approval-mode-trusted-only");
|
||||
expect(decision.effectiveMode).toBe("trusted-only");
|
||||
});
|
||||
|
||||
it("requires approval in always mode", () => {
|
||||
const decision = resolveAgentProvisioningPolicy({
|
||||
tool: "fn_agent_create",
|
||||
caller: { id: "a1" },
|
||||
settings: { agentProvisioning: { approvalMode: "always" } },
|
||||
});
|
||||
expect(decision.decision).toBe("require-approval");
|
||||
expect(decision.matchedRule).toBe("approval-mode-always");
|
||||
});
|
||||
|
||||
it("alwaysApproveDelete forces approval by default", () => {
|
||||
const decision = resolveAgentProvisioningPolicy({
|
||||
tool: "fn_agent_delete",
|
||||
caller: { id: "trusted", role: "ceo" },
|
||||
settings: { agentProvisioning: { approvalMode: "trusted-only", trustedAgentIds: ["trusted"] } },
|
||||
});
|
||||
expect(decision.decision).toBe("require-approval");
|
||||
expect(decision.matchedRule).toBe("delete-always-approve");
|
||||
});
|
||||
|
||||
it("allows trusted delete when alwaysApproveDelete is false", () => {
|
||||
const decision = resolveAgentProvisioningPolicy({
|
||||
tool: "fn_agent_delete",
|
||||
caller: { id: "trusted" },
|
||||
settings: { agentProvisioning: { approvalMode: "trusted-only", trustedAgentIds: ["trusted"], alwaysApproveDelete: false } },
|
||||
});
|
||||
expect(decision.decision).toBe("allow");
|
||||
expect(decision.matchedRule).toBe("trusted-agent-id");
|
||||
});
|
||||
|
||||
it("never mode short-circuits delete approval", () => {
|
||||
const decision = resolveAgentProvisioningPolicy({
|
||||
tool: "fn_agent_delete",
|
||||
caller: { id: "a1" },
|
||||
settings: { agentProvisioning: { approvalMode: "never", alwaysApproveDelete: true } },
|
||||
});
|
||||
expect(decision.decision).toBe("allow");
|
||||
expect(decision.matchedRule).toBe("approval-mode-never");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractAgentProvisioningRequest", () => {
|
||||
it("extracts tool and params from provisioning request", () => {
|
||||
const request: any = {
|
||||
id: "apr-1",
|
||||
targetAction: {
|
||||
category: "agent_provisioning",
|
||||
context: { tool: "fn_agent_create", params: { name: "helper" } },
|
||||
},
|
||||
};
|
||||
expect(extractAgentProvisioningRequest(request)).toEqual({ tool: "fn_agent_create", params: { name: "helper" } });
|
||||
});
|
||||
|
||||
it("throws for malformed context", () => {
|
||||
const request: any = { id: "apr-1", targetAction: { category: "agent_provisioning", context: {} } };
|
||||
expect(() => extractAgentProvisioningRequest(request)).toThrow("invalid provisioning tool");
|
||||
});
|
||||
});
|
||||
@@ -126,6 +126,23 @@ describe("ApprovalRequestStore", () => {
|
||||
expect(fetched?.runId).toBe("run-abc");
|
||||
});
|
||||
|
||||
it("round-trips agent_provisioning category unchanged", () => {
|
||||
const created = store.create({
|
||||
requester: REQUESTER,
|
||||
targetAction: {
|
||||
category: "agent_provisioning",
|
||||
action: "create",
|
||||
summary: "Create helper",
|
||||
resourceType: "agent",
|
||||
resourceId: "",
|
||||
},
|
||||
});
|
||||
|
||||
const fetched = store.get(created.id);
|
||||
expect(fetched?.targetAction.category).toBe("agent_provisioning");
|
||||
expect(store.list({ status: "pending" }).some((row) => row.id === created.id && row.targetAction.category === "agent_provisioning")).toBe(true);
|
||||
});
|
||||
|
||||
it("normalizes legacy category aliases on create/read", () => {
|
||||
const created = store.create({
|
||||
requester: REQUESTER,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DEFAULT_PROJECT_SETTINGS, PROJECT_SETTINGS_KEYS } from "../settings-schema.js";
|
||||
import { AGENT_PROVISIONING_APPROVAL_MODES } from "../types.js";
|
||||
|
||||
describe("agentProvisioning settings schema contract", () => {
|
||||
it("includes agentProvisioning key with object default", () => {
|
||||
expect(PROJECT_SETTINGS_KEYS).toContain("agentProvisioning");
|
||||
expect(DEFAULT_PROJECT_SETTINGS.agentProvisioning).toEqual({});
|
||||
});
|
||||
|
||||
it("exposes valid approval mode vocabulary", () => {
|
||||
expect(AGENT_PROVISIONING_APPROVAL_MODES).toEqual(["always", "trusted-only", "never"]);
|
||||
});
|
||||
|
||||
it("supports omitted block defaults", () => {
|
||||
const settings = DEFAULT_PROJECT_SETTINGS.agentProvisioning ?? {};
|
||||
expect(settings).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,8 @@ const state = {
|
||||
audits: new Map<string, any[]>(),
|
||||
task: { id: "FN-1", paused: true, pausedByAgentId: "agent-1" },
|
||||
agent: { id: "agent-1", state: "paused", pauseReason: "awaiting-approval" },
|
||||
runAuditEvents: [] as any[],
|
||||
provisionedAgents: new Set<string>(),
|
||||
};
|
||||
|
||||
class MockApprovalRequestStore {
|
||||
@@ -59,11 +61,31 @@ class MockAgentStore {
|
||||
}
|
||||
}
|
||||
|
||||
const executeApprovedAgentProvisioning = vi.fn(async (request: any) => {
|
||||
const tool = request?.targetAction?.context?.tool;
|
||||
if (!tool) throw new Error("Malformed agent provisioning request: missing tool");
|
||||
if (tool === "fn_agent_create") {
|
||||
const id = String(request?.targetAction?.context?.params?.name ?? "created-agent");
|
||||
state.provisionedAgents.add(id);
|
||||
return { id };
|
||||
}
|
||||
if (tool === "fn_agent_delete") {
|
||||
const id = String(request?.targetAction?.resourceId ?? "");
|
||||
state.provisionedAgents.delete(id);
|
||||
return { deletedId: id };
|
||||
}
|
||||
throw new Error(`Unsupported provisioning tool: ${tool}`);
|
||||
});
|
||||
|
||||
vi.mock("@fusion/core", () => ({
|
||||
ApprovalRequestStore: MockApprovalRequestStore,
|
||||
AgentStore: MockAgentStore,
|
||||
}));
|
||||
|
||||
vi.mock("@fusion/engine", () => ({
|
||||
executeApprovedAgentProvisioning,
|
||||
}));
|
||||
|
||||
describe("approval routes", async () => {
|
||||
const { registerApprovalRoutes } = await import("../routes/register-approval-routes.js");
|
||||
|
||||
@@ -81,6 +103,10 @@ describe("approval routes", async () => {
|
||||
pauseTask: async (_id: string, paused: boolean) => {
|
||||
state.task = { ...state.task, paused, pausedByAgentId: paused ? state.task.pausedByAgentId : undefined };
|
||||
},
|
||||
recordRunAuditEvent: (event: any) => {
|
||||
state.runAuditEvents.push(event);
|
||||
return event;
|
||||
},
|
||||
},
|
||||
engine: undefined,
|
||||
projectId: "p1",
|
||||
@@ -101,6 +127,9 @@ describe("approval routes", async () => {
|
||||
beforeEach(() => {
|
||||
updateAgent.mockClear();
|
||||
const now = new Date().toISOString();
|
||||
executeApprovedAgentProvisioning.mockClear();
|
||||
state.runAuditEvents = [];
|
||||
state.provisionedAgents = new Set(["target-1"]);
|
||||
state.task = { id: "FN-1", paused: true, pausedByAgentId: "agent-1" };
|
||||
state.agent = { id: "agent-1", state: "paused", pauseReason: "awaiting-approval" };
|
||||
state.requests = new Map([
|
||||
@@ -124,6 +153,60 @@ describe("approval routes", async () => {
|
||||
updatedAt: now,
|
||||
requestedAt: now,
|
||||
}],
|
||||
["apr-3", {
|
||||
id: "apr-3",
|
||||
status: "pending",
|
||||
requester: { actorId: "agent-1", actorType: "agent", actorName: "Agent 1" },
|
||||
targetAction: {
|
||||
category: "agent_provisioning",
|
||||
summary: "Create provisioned agent",
|
||||
action: "create",
|
||||
resourceType: "agent",
|
||||
resourceId: "",
|
||||
context: { tool: "fn_agent_create", params: { name: "created-agent", role: "executor" } },
|
||||
},
|
||||
taskId: "FN-1",
|
||||
runId: "run-1",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
requestedAt: now,
|
||||
}],
|
||||
["apr-4", {
|
||||
id: "apr-4",
|
||||
status: "pending",
|
||||
requester: { actorId: "agent-1", actorType: "agent", actorName: "Agent 1" },
|
||||
targetAction: {
|
||||
category: "agent_provisioning",
|
||||
summary: "Delete provisioned agent",
|
||||
action: "delete",
|
||||
resourceType: "agent",
|
||||
resourceId: "target-1",
|
||||
context: { tool: "fn_agent_delete", params: { agent_id: "target-1" } },
|
||||
},
|
||||
taskId: "FN-1",
|
||||
runId: "run-2",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
requestedAt: now,
|
||||
}],
|
||||
["apr-5", {
|
||||
id: "apr-5",
|
||||
status: "pending",
|
||||
requester: { actorId: "agent-1", actorType: "agent", actorName: "Agent 1" },
|
||||
targetAction: {
|
||||
category: "agent_provisioning",
|
||||
summary: "Malformed",
|
||||
action: "create",
|
||||
resourceType: "agent",
|
||||
resourceId: "",
|
||||
context: {},
|
||||
},
|
||||
taskId: "FN-1",
|
||||
runId: "run-3",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
requestedAt: now,
|
||||
}],
|
||||
]);
|
||||
state.audits = new Map([
|
||||
["apr-1", [{ id: "evt-created", eventType: "created", actor: { actorId: "agent-1", actorType: "agent", actorName: "Agent 1" }, createdAt: now }]],
|
||||
@@ -135,9 +218,9 @@ describe("approval routes", async () => {
|
||||
const app = createApp();
|
||||
const res = await get(app, "/api/approvals?status=pending");
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.total).toBe(1);
|
||||
expect(res.body.pendingCount).toBe(1);
|
||||
expect(res.body.requests).toHaveLength(1);
|
||||
expect(res.body.total).toBe(4);
|
||||
expect(res.body.pendingCount).toBe(4);
|
||||
expect(res.body.requests).toHaveLength(4);
|
||||
expect(res.body.requests[0]).toMatchObject({
|
||||
id: "apr-1",
|
||||
actionCategory: "command_execution",
|
||||
@@ -191,8 +274,65 @@ describe("approval routes", async () => {
|
||||
expect(res.body.status).toBe("denied");
|
||||
});
|
||||
|
||||
it("returns 409 for invalid transition", async () => {
|
||||
it("approves provisioning create and records audit", async () => {
|
||||
const app = createApp();
|
||||
const res = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/approvals/apr-3/decision",
|
||||
JSON.stringify({ decision: "approve" }),
|
||||
{ "content-type": "application/json" },
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(executeApprovedAgentProvisioning).toHaveBeenCalledTimes(1);
|
||||
expect(state.provisionedAgents.has("created-agent")).toBe(true);
|
||||
expect(state.runAuditEvents.at(-1)).toMatchObject({ mutationType: "agent:create:approved", runId: "run-1" });
|
||||
expect(state.task.paused).toBe(false);
|
||||
});
|
||||
|
||||
it("denies provisioning create without execution and records denied audit", async () => {
|
||||
const app = createApp();
|
||||
const res = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/approvals/apr-3/decision",
|
||||
JSON.stringify({ decision: "deny" }),
|
||||
{ "content-type": "application/json" },
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(executeApprovedAgentProvisioning).not.toHaveBeenCalled();
|
||||
expect(state.provisionedAgents.has("created-agent")).toBe(false);
|
||||
expect(state.runAuditEvents.at(-1)).toMatchObject({ mutationType: "agent:create:denied", runId: "run-1" });
|
||||
});
|
||||
|
||||
it("approves provisioning delete and records audit", async () => {
|
||||
const app = createApp();
|
||||
const res = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/approvals/apr-4/decision",
|
||||
JSON.stringify({ decision: "approve" }),
|
||||
{ "content-type": "application/json" },
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(state.provisionedAgents.has("target-1")).toBe(false);
|
||||
expect(state.runAuditEvents.at(-1)).toMatchObject({ mutationType: "agent:delete:approved", runId: "run-2" });
|
||||
});
|
||||
|
||||
it("returns 500 for malformed provisioning request context", async () => {
|
||||
const app = createApp();
|
||||
const res = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/approvals/apr-5/decision",
|
||||
JSON.stringify({ decision: "approve" }),
|
||||
{ "content-type": "application/json" },
|
||||
);
|
||||
expect(res.status).toBe(500);
|
||||
expect(res.body.error).toContain("Malformed agent provisioning request");
|
||||
});
|
||||
|
||||
it("returns 409 for invalid transition", async () => { const app = createApp();
|
||||
const res = await request(
|
||||
app,
|
||||
"POST",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AgentStore, ApprovalRequestStore, type ApprovalRequestActorSnapshot, type ApprovalRequestStatus } from "@fusion/core";
|
||||
import { AgentStore, ApprovalRequestStore, type ApprovalRequest, type ApprovalRequestActorSnapshot, type ApprovalRequestStatus } from "@fusion/core";
|
||||
import { executeApprovedAgentProvisioning } from "@fusion/engine";
|
||||
import { ApiError, badRequest, conflict, notFound } from "../api-error.js";
|
||||
import type { ApiRoutesContext } from "./types.js";
|
||||
import { emitApprovalSseEvent } from "../sse.js";
|
||||
@@ -97,6 +98,34 @@ function toDetailDto(
|
||||
};
|
||||
}
|
||||
|
||||
function emitProvisioningDecisionAudit(params: {
|
||||
scopedStore: import("@fusion/core").TaskStore;
|
||||
request: ApprovalRequest;
|
||||
decision: "approved" | "denied";
|
||||
}): void {
|
||||
const { scopedStore, request, decision } = params;
|
||||
if (request.targetAction.category !== "agent_provisioning") return;
|
||||
|
||||
const action = request.targetAction.action === "delete" ? "delete" : "create";
|
||||
const mutationType = `agent:${action}:${decision}` as const;
|
||||
const event: Parameters<typeof scopedStore.recordRunAuditEvent>[0] = {
|
||||
agentId: request.requester.actorId,
|
||||
domain: "database",
|
||||
mutationType,
|
||||
target: request.targetAction.resourceId || request.requester.actorId,
|
||||
metadata: {
|
||||
approvalRequestId: request.id,
|
||||
action,
|
||||
resourceId: request.targetAction.resourceId,
|
||||
requesterAgentId: request.requester.actorId,
|
||||
},
|
||||
runId: request.id,
|
||||
};
|
||||
if (request.taskId) event.taskId = request.taskId;
|
||||
if (request.runId) event.runId = request.runId;
|
||||
scopedStore.recordRunAuditEvent(event);
|
||||
}
|
||||
|
||||
async function resumeAfterDecision(params: {
|
||||
scopedStore: import("@fusion/core").TaskStore;
|
||||
request: import("@fusion/core").ApprovalRequest;
|
||||
@@ -210,6 +239,17 @@ export function registerApprovalRoutes(ctx: ApiRoutesContext): void {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (updated.targetAction.category === "agent_provisioning") {
|
||||
if (body.decision === "approve") {
|
||||
const agentStore = new AgentStore({ rootDir: scopedStore.getFusionDir() });
|
||||
await agentStore.init();
|
||||
await executeApprovedAgentProvisioning(updated, { agentStore });
|
||||
emitProvisioningDecisionAudit({ scopedStore, request: updated, decision: "approved" });
|
||||
} else {
|
||||
emitProvisioningDecisionAudit({ scopedStore, request: updated, decision: "denied" });
|
||||
}
|
||||
}
|
||||
|
||||
await resumeAfterDecision({ scopedStore, request: updated, runtimeLogger });
|
||||
const history = approvalStore.getAuditHistory(requestId);
|
||||
const detail = toDetailDto(updated, history);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
ACTION_GATE_TASK_AGENT_MANAGEMENT_TOOLS,
|
||||
PERMANENT_AGENT_TASK_MUTATION_TOOLS,
|
||||
TASK_AGENT_MUTATION_TOOLS,
|
||||
} from "../gating-classifications.js";
|
||||
|
||||
describe("gating classifications provisioning split", () => {
|
||||
it("keeps provisioning tools out of action-gate set", () => {
|
||||
expect(ACTION_GATE_TASK_AGENT_MANAGEMENT_TOOLS.has("fn_agent_create")).toBe(false);
|
||||
expect(ACTION_GATE_TASK_AGENT_MANAGEMENT_TOOLS.has("fn_agent_delete")).toBe(false);
|
||||
});
|
||||
|
||||
it("retains provisioning tools in permanent/task mutation sets", () => {
|
||||
expect(PERMANENT_AGENT_TASK_MUTATION_TOOLS.has("fn_agent_create")).toBe(true);
|
||||
expect(PERMANENT_AGENT_TASK_MUTATION_TOOLS.has("fn_agent_delete")).toBe(true);
|
||||
expect(TASK_AGENT_MUTATION_TOOLS.has("fn_agent_create")).toBe(true);
|
||||
expect(TASK_AGENT_MUTATION_TOOLS.has("fn_agent_delete")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ export {
|
||||
taskDocumentReadParams,
|
||||
taskDocumentWriteParams,
|
||||
taskLogParams,
|
||||
executeApprovedAgentProvisioning,
|
||||
} from "./agent-tools.js";
|
||||
export { AgentSemaphore, PRIORITY_MERGE, PRIORITY_EXECUTE, PRIORITY_SPECIFY } from "./concurrency.js";
|
||||
export { TriageProcessor, type TriageProcessorOptions } from "./triage.js";
|
||||
|
||||
Reference in New Issue
Block a user