feat(FN-4641): complete Step 3 — add engine provisioning gate helper

Fusion-Task-Id: FN-4641
Fusion-Task-Lineage: 50ac1fd2-8c88-4a29-9ebb-962a6b5c772c
This commit is contained in:
Fusion (runfusion.ai)
2026-05-15 22:38:33 -07:00
committed by gsxdsm
parent 9ef00e1197
commit a6a72ee1ff
3 changed files with 321 additions and 0 deletions

View File

@@ -0,0 +1,148 @@
import type { ApprovalRequest } from "@fusion/core";
import { describe, expect, it, vi } from "vitest";
import {
SandboxProvisioningPendingError,
requireSandboxProvisioningApproval,
} from "../provisioning-gate.js";
function makeApproval(id: string, status: ApprovalRequest["status"]): ApprovalRequest {
return {
id,
status,
requester: { actorId: "agent-1", actorType: "agent", actorName: "Executor" },
targetAction: {
category: "sandbox_provisioning",
action: "provision",
summary: "Provision sandbox backend",
resourceType: "command",
resourceId: "install-bubblewrap",
context: { backendId: "bubblewrap", operation: "install-bubblewrap", params: {} },
},
taskId: "FN-4641",
runId: "run-1",
requestedAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
}
describe("requireSandboxProvisioningApproval", () => {
it("allows auto-approved backend", async () => {
const createApprovalRequest = vi.fn();
const result = await requireSandboxProvisioningApproval({
backendId: "native",
operation: "prepare",
description: "Prepare native backend",
context: {
requester: { actorId: "agent-1", actorType: "agent", actorName: "Executor" },
settings: undefined,
createApprovalRequest,
},
});
expect(result).toEqual({ outcome: "allow" });
expect(createApprovalRequest).not.toHaveBeenCalled();
});
it("creates pending request and pauses for approval in default mode", async () => {
const createApprovalRequest = vi.fn(async () => makeApproval("apr-1", "pending"));
const pauseForApproval = vi.fn(async () => undefined);
await expect(
requireSandboxProvisioningApproval({
backendId: "bubblewrap",
operation: "install",
description: "Install bubblewrap",
context: {
taskId: "FN-4641",
requester: { actorId: "agent-1", actorType: "agent", actorName: "Executor" },
settings: undefined,
createApprovalRequest,
pauseForApproval,
},
}),
).rejects.toBeInstanceOf(SandboxProvisioningPendingError);
expect(createApprovalRequest).toHaveBeenCalledTimes(1);
expect(pauseForApproval).toHaveBeenCalledTimes(1);
});
it("returns execute-once-then-complete after approval exists", async () => {
const result = await requireSandboxProvisioningApproval({
backendId: "bubblewrap",
operation: "install",
description: "Install bubblewrap",
context: {
taskId: "FN-4641",
requester: { actorId: "agent-1", actorType: "agent", actorName: "Executor" },
settings: undefined,
createApprovalRequest: vi.fn(async () => null),
findApprovalByDedupeKey: vi.fn(async () => ({ id: "apr-1", status: "approved" })),
},
});
expect(result).toEqual({ outcome: "execute-once-then-complete", approvalRequestId: "apr-1" });
});
it("throws block when prior approval was denied", async () => {
await expect(
requireSandboxProvisioningApproval({
backendId: "bubblewrap",
operation: "install",
description: "Install bubblewrap",
context: {
requester: { actorId: "agent-1", actorType: "agent", actorName: "Executor" },
settings: undefined,
createApprovalRequest: vi.fn(async () => null),
findApprovalByDedupeKey: vi.fn(async () => ({ id: "apr-1", status: "denied" })),
},
}),
).rejects.toBeInstanceOf(SandboxProvisioningPendingError);
});
it("allows never mode without creating approval", async () => {
const createApprovalRequest = vi.fn(async () => makeApproval("apr-1", "pending"));
const result = await requireSandboxProvisioningApproval({
backendId: "bubblewrap",
operation: "install",
description: "Install bubblewrap",
context: {
requester: { actorId: "agent-1", actorType: "agent", actorName: "Executor" },
settings: { sandboxProvisioning: { approvalMode: "never" } },
createApprovalRequest,
},
});
expect(result).toEqual({ outcome: "allow" });
expect(createApprovalRequest).not.toHaveBeenCalled();
});
it("produces stable dedupe key for identical inputs", async () => {
const createApprovalRequest = vi.fn(async () => makeApproval("apr-1", "pending"));
const first = requireSandboxProvisioningApproval({
backendId: "bubblewrap",
operation: "install",
description: "Install bubblewrap",
context: {
taskId: "FN-4641",
requester: { actorId: "agent-1", actorType: "agent", actorName: "Executor" },
settings: undefined,
createApprovalRequest,
},
}).catch((error) => error as SandboxProvisioningPendingError);
const second = requireSandboxProvisioningApproval({
backendId: "bubblewrap",
operation: "install",
description: "Install bubblewrap",
context: {
taskId: "FN-4641",
requester: { actorId: "agent-1", actorType: "agent", actorName: "Executor" },
settings: undefined,
createApprovalRequest,
},
}).catch((error) => error as SandboxProvisioningPendingError);
const [firstError, secondError] = await Promise.all([first, second]);
expect(firstError.dedupeKey).toBe(secondError.dedupeKey);
});
});

View File

@@ -5,6 +5,8 @@ import { withSandboxAudit } from "./audit.js";
import type { RunAuditor } from "../run-audit.js"; import type { RunAuditor } from "../run-audit.js";
import type { SandboxBackend, SandboxCapabilities } from "./types.js"; import type { SandboxBackend, SandboxCapabilities } from "./types.js";
export { SandboxProvisioningPendingError, requireSandboxProvisioningApproval } from "./provisioning-gate.js";
export type { export type {
SandboxBackend, SandboxBackend,
SandboxCapabilities, SandboxCapabilities,

View File

@@ -0,0 +1,171 @@
import type {
ApprovalRequest,
ApprovalRequestActorSnapshot,
ApprovalRequestStatus,
ProjectSettings,
} from "@fusion/core";
import { resolveSandboxProvisioningPolicy } from "@fusion/core";
import {
computeApprovalDedupeKey,
resolveGateOutcome,
type AgentActionGateDecision,
} from "../agent-action-gate.js";
export class SandboxProvisioningPendingError extends Error {
readonly approvalRequestId: string;
readonly dedupeKey: string;
readonly decision: AgentActionGateDecision;
constructor(params: {
message: string;
approvalRequestId: string;
dedupeKey: string;
decision: AgentActionGateDecision;
}) {
super(params.message);
this.name = "SandboxProvisioningPendingError";
this.approvalRequestId = params.approvalRequestId;
this.dedupeKey = params.dedupeKey;
this.decision = params.decision;
}
}
export interface SandboxProvisioningGateContext {
taskId?: string;
runId?: string;
requester: ApprovalRequestActorSnapshot;
settings: Pick<ProjectSettings, "sandboxProvisioning"> | undefined;
createApprovalRequest: (input: {
category: "sandbox_provisioning";
toolName: string;
args: Record<string, unknown>;
}) => Promise<ApprovalRequest | null>;
findApprovalByDedupeKey?: (dedupeKey: string) => Promise<{ id: string; status: ApprovalRequestStatus } | null>;
pauseForApproval?: (info: { approvalRequestId: string; decision: AgentActionGateDecision }) => Promise<void>;
}
export async function requireSandboxProvisioningApproval(input: {
backendId: string;
operation: string;
description: string;
params?: Record<string, unknown>;
context: SandboxProvisioningGateContext;
}): Promise<{ outcome: "allow" | "execute-once-then-complete"; approvalRequestId?: string }> {
const { backendId, operation, description, context } = input;
const params = input.params ?? {};
const dedupeKey = computeApprovalDedupeKey({
agentId: context.requester.actorId,
taskId: context.taskId,
toolName: `sandbox:provisioning:${backendId}`,
category: "sandbox_provisioning",
resourceType: "command",
resourceId: operation,
operation,
});
const policyDecision = resolveSandboxProvisioningPolicy({
backendId,
operation,
caller: {
id: context.requester.actorId,
role: context.requester.actorType === "agent" ? "agent" : context.requester.actorType,
isPrivileged: context.requester.actorType === "user",
},
settings: context.settings,
});
const decision: AgentActionGateDecision = {
disposition:
policyDecision.decision === "require-approval"
? "require-approval"
: policyDecision.decision === "allow"
? "allow"
: "block",
category: "command_execution",
toolName: `sandbox:provisioning:${backendId}`,
operation,
summary: description,
resourceType: "command",
resourceId: operation,
approvalDedupeKey: dedupeKey,
metadata: {
backendId,
operation,
description,
policyReason: policyDecision.reason,
policyRule: policyDecision.matchedRule,
policyMode: policyDecision.effectiveMode,
},
};
if (policyDecision.decision === "allow") {
return { outcome: "allow" };
}
if (policyDecision.decision === "deny") {
throw new SandboxProvisioningPendingError({
message: `Sandbox provisioning denied: ${policyDecision.reason}`,
approvalRequestId: "",
dedupeKey,
decision,
});
}
const existing = context.findApprovalByDedupeKey ? await context.findApprovalByDedupeKey(dedupeKey) : null;
const gateOutcome = resolveGateOutcome(decision, existing);
if (gateOutcome.outcome === "execute-once-then-complete") {
return { outcome: "execute-once-then-complete", approvalRequestId: gateOutcome.approvalRequestId };
}
if (gateOutcome.outcome === "block") {
throw new SandboxProvisioningPendingError({
message: "Sandbox provisioning approval denied",
approvalRequestId: gateOutcome.approvalRequestId ?? "",
dedupeKey,
decision,
});
}
if (gateOutcome.approvalRequestId) {
throw new SandboxProvisioningPendingError({
message: "Sandbox provisioning approval pending",
approvalRequestId: gateOutcome.approvalRequestId,
dedupeKey,
decision,
});
}
const created = await context.createApprovalRequest({
category: "sandbox_provisioning",
toolName: `sandbox:provisioning:${backendId}`,
args: {
backendId,
operation,
description,
params,
approvalDedupeKey: dedupeKey,
},
});
if (!created) {
throw new SandboxProvisioningPendingError({
message: "Sandbox provisioning approval request was not created",
approvalRequestId: "",
dedupeKey,
decision,
});
}
if (context.pauseForApproval) {
await context.pauseForApproval({ approvalRequestId: created.id, decision });
}
throw new SandboxProvisioningPendingError({
message: "Sandbox provisioning approval pending",
approvalRequestId: created.id,
dedupeKey,
decision,
});
}