feat(FN-4641): complete Step 2 — add sandbox provisioning policy resolver
Fusion-Task-Id: FN-4641 Fusion-Task-Lineage: 50ac1fd2-8c88-4a29-9ebb-962a6b5c772c
This commit is contained in:
committed by
gsxdsm
parent
30f71d399c
commit
9ef00e1197
129
packages/core/src/__tests__/sandbox-provisioning-policy.test.ts
Normal file
129
packages/core/src/__tests__/sandbox-provisioning-policy.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { extractSandboxProvisioningRequest, resolveSandboxProvisioningPolicy } from "../sandbox-provisioning-policy.js";
|
||||
|
||||
describe("resolveSandboxProvisioningPolicy", () => {
|
||||
it("denies missing caller", () => {
|
||||
const decision = resolveSandboxProvisioningPolicy({
|
||||
backendId: "bubblewrap",
|
||||
operation: "install",
|
||||
caller: undefined,
|
||||
settings: undefined,
|
||||
});
|
||||
expect(decision.decision).toBe("deny");
|
||||
expect(decision.matchedRule).toBe("missing-caller");
|
||||
});
|
||||
|
||||
it("allows auto-approved backend id", () => {
|
||||
const decision = resolveSandboxProvisioningPolicy({
|
||||
backendId: "native",
|
||||
operation: "prepare",
|
||||
caller: { id: "agent-1" },
|
||||
settings: undefined,
|
||||
});
|
||||
expect(decision.decision).toBe("allow");
|
||||
expect(decision.matchedRule).toBe("auto-approve-backend");
|
||||
});
|
||||
|
||||
it("allows privileged caller", () => {
|
||||
const decision = resolveSandboxProvisioningPolicy({
|
||||
backendId: "bubblewrap",
|
||||
operation: "install",
|
||||
caller: { id: "user-1", isPrivileged: true },
|
||||
settings: { sandboxProvisioning: { approvalMode: "always" } },
|
||||
});
|
||||
expect(decision.decision).toBe("allow");
|
||||
expect(decision.matchedRule).toBe("privileged-caller");
|
||||
});
|
||||
|
||||
it("allows trusted agent id", () => {
|
||||
const decision = resolveSandboxProvisioningPolicy({
|
||||
backendId: "bubblewrap",
|
||||
operation: "install",
|
||||
caller: { id: "trusted-id" },
|
||||
settings: { sandboxProvisioning: { approvalMode: "trusted-only", trustedAgentIds: ["trusted-id"] } },
|
||||
});
|
||||
expect(decision.decision).toBe("allow");
|
||||
expect(decision.matchedRule).toBe("trusted-agent-id");
|
||||
});
|
||||
|
||||
it("allows trusted role", () => {
|
||||
const decision = resolveSandboxProvisioningPolicy({
|
||||
backendId: "bubblewrap",
|
||||
operation: "install",
|
||||
caller: { id: "agent-1", role: "CEO" },
|
||||
settings: { sandboxProvisioning: { approvalMode: "trusted-only", trustedRoles: ["ceo"] } },
|
||||
});
|
||||
expect(decision.decision).toBe("allow");
|
||||
expect(decision.matchedRule).toBe("trusted-role");
|
||||
});
|
||||
|
||||
it("requires approval in always mode", () => {
|
||||
const decision = resolveSandboxProvisioningPolicy({
|
||||
backendId: "bubblewrap",
|
||||
operation: "install",
|
||||
caller: { id: "agent-1" },
|
||||
settings: { sandboxProvisioning: { approvalMode: "always" } },
|
||||
});
|
||||
expect(decision.decision).toBe("require-approval");
|
||||
expect(decision.matchedRule).toBe("approval-mode-always");
|
||||
});
|
||||
|
||||
it("defaults to always mode", () => {
|
||||
const decision = resolveSandboxProvisioningPolicy({
|
||||
backendId: "bubblewrap",
|
||||
operation: "install",
|
||||
caller: { id: "agent-1" },
|
||||
settings: undefined,
|
||||
});
|
||||
expect(decision.decision).toBe("require-approval");
|
||||
expect(decision.effectiveMode).toBe("always");
|
||||
});
|
||||
|
||||
it("allows never mode", () => {
|
||||
const decision = resolveSandboxProvisioningPolicy({
|
||||
backendId: "bubblewrap",
|
||||
operation: "install",
|
||||
caller: { id: "agent-1" },
|
||||
settings: { sandboxProvisioning: { approvalMode: "never" } },
|
||||
});
|
||||
expect(decision.decision).toBe("allow");
|
||||
expect(decision.matchedRule).toBe("approval-mode-never");
|
||||
});
|
||||
|
||||
it("supports unknown backend ids by requiring approval", () => {
|
||||
const decision = resolveSandboxProvisioningPolicy({
|
||||
backendId: "unknown-backend",
|
||||
operation: "custom-bootstrap",
|
||||
caller: { id: "agent-1" },
|
||||
settings: undefined,
|
||||
});
|
||||
expect(decision.decision).toBe("require-approval");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractSandboxProvisioningRequest", () => {
|
||||
it("extracts backendId, operation, and params", () => {
|
||||
const request: any = {
|
||||
id: "apr-1",
|
||||
targetAction: {
|
||||
category: "sandbox_provisioning",
|
||||
context: { backendId: "bubblewrap", operation: "install", params: { packageName: "bubblewrap" } },
|
||||
},
|
||||
};
|
||||
expect(extractSandboxProvisioningRequest(request)).toEqual({
|
||||
backendId: "bubblewrap",
|
||||
operation: "install",
|
||||
params: { packageName: "bubblewrap" },
|
||||
});
|
||||
});
|
||||
|
||||
it("throws on wrong category", () => {
|
||||
const request: any = { id: "apr-1", targetAction: { category: "agent_provisioning", context: {} } };
|
||||
expect(() => extractSandboxProvisioningRequest(request)).toThrow("not a sandbox_provisioning request");
|
||||
});
|
||||
|
||||
it("throws for malformed context", () => {
|
||||
const request: any = { id: "apr-1", targetAction: { category: "sandbox_provisioning", context: {} } };
|
||||
expect(() => extractSandboxProvisioningRequest(request)).toThrow("invalid provisioning backend id");
|
||||
});
|
||||
});
|
||||
@@ -96,11 +96,19 @@ export {
|
||||
resolveAgentProvisioningPolicy,
|
||||
extractAgentProvisioningRequest,
|
||||
} from "./agent-provisioning-policy.js";
|
||||
export {
|
||||
resolveSandboxProvisioningPolicy,
|
||||
extractSandboxProvisioningRequest,
|
||||
} from "./sandbox-provisioning-policy.js";
|
||||
export type {
|
||||
AgentProvisioningTool,
|
||||
AgentProvisioningPolicyInput,
|
||||
AgentProvisioningPolicyDecision,
|
||||
} from "./agent-provisioning-policy.js";
|
||||
export type {
|
||||
SandboxProvisioningPolicyInput,
|
||||
SandboxProvisioningPolicyDecision,
|
||||
} from "./sandbox-provisioning-policy.js";
|
||||
export { TaskStore } from "./store.js";
|
||||
export { computeRetrySummary, RETRY_STORM_WARNING_RATIO } from "./retry-summary.js";
|
||||
export { RetryStormError, serializeRetryStormError } from "./retry-storm-error.js";
|
||||
|
||||
117
packages/core/src/sandbox-provisioning-policy.ts
Normal file
117
packages/core/src/sandbox-provisioning-policy.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { ApprovalRequest, ProjectSettings, SandboxProvisioningApprovalMode } from "./types.js";
|
||||
|
||||
type SandboxProvisioningSettings = Pick<ProjectSettings, "sandboxProvisioning">;
|
||||
|
||||
export interface SandboxProvisioningPolicyInput {
|
||||
/** Sandbox backend id (for example: native, bubblewrap, sandbox-exec, podman, docker). */
|
||||
backendId: string;
|
||||
operation: string;
|
||||
caller?: { id: string; role?: string; isPrivileged?: boolean };
|
||||
settings: SandboxProvisioningSettings | undefined;
|
||||
}
|
||||
|
||||
export interface SandboxProvisioningPolicyDecision {
|
||||
decision: "allow" | "require-approval" | "deny";
|
||||
reason: string;
|
||||
matchedRule:
|
||||
| "auto-approve-backend"
|
||||
| "privileged-caller"
|
||||
| "trusted-agent-id"
|
||||
| "trusted-role"
|
||||
| "approval-mode-always"
|
||||
| "approval-mode-trusted-only"
|
||||
| "approval-mode-never"
|
||||
| "missing-caller";
|
||||
effectiveMode: SandboxProvisioningApprovalMode;
|
||||
}
|
||||
|
||||
function normalizeMode(settings: SandboxProvisioningSettings | undefined): SandboxProvisioningApprovalMode {
|
||||
return settings?.sandboxProvisioning?.approvalMode ?? "always";
|
||||
}
|
||||
|
||||
export function resolveSandboxProvisioningPolicy(
|
||||
input: SandboxProvisioningPolicyInput,
|
||||
): SandboxProvisioningPolicyDecision {
|
||||
const effectiveMode = normalizeMode(input.settings);
|
||||
const caller = input.caller;
|
||||
|
||||
if (!caller) {
|
||||
return { decision: "deny", reason: "missing caller", matchedRule: "missing-caller", effectiveMode };
|
||||
}
|
||||
|
||||
const autoApproveBackendIds = input.settings?.sandboxProvisioning?.autoApproveBackendIds ?? ["native"];
|
||||
if (autoApproveBackendIds.includes(input.backendId)) {
|
||||
return {
|
||||
decision: "allow",
|
||||
reason: `backend ${input.backendId} is auto-approved`,
|
||||
matchedRule: "auto-approve-backend",
|
||||
effectiveMode,
|
||||
};
|
||||
}
|
||||
|
||||
if (caller.isPrivileged === true) {
|
||||
return { decision: "allow", reason: "privileged caller", matchedRule: "privileged-caller", effectiveMode };
|
||||
}
|
||||
|
||||
if (effectiveMode === "never") {
|
||||
return { decision: "allow", reason: "approval mode never", matchedRule: "approval-mode-never", effectiveMode };
|
||||
}
|
||||
|
||||
const trustedAgentIds = input.settings?.sandboxProvisioning?.trustedAgentIds ?? [];
|
||||
if (trustedAgentIds.includes(caller.id)) {
|
||||
return { decision: "allow", reason: "trusted agent id", matchedRule: "trusted-agent-id", effectiveMode };
|
||||
}
|
||||
|
||||
const trustedRoles = (input.settings?.sandboxProvisioning?.trustedRoles ?? []).map((role) => role.toLowerCase());
|
||||
if (caller.role && trustedRoles.includes(caller.role.toLowerCase())) {
|
||||
return { decision: "allow", reason: "trusted role", matchedRule: "trusted-role", effectiveMode };
|
||||
}
|
||||
|
||||
if (effectiveMode === "always") {
|
||||
return {
|
||||
decision: "require-approval",
|
||||
reason: "approval mode always",
|
||||
matchedRule: "approval-mode-always",
|
||||
effectiveMode,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
decision: "require-approval",
|
||||
reason: "trusted-only requires trusted caller",
|
||||
matchedRule: "approval-mode-trusted-only",
|
||||
effectiveMode,
|
||||
};
|
||||
}
|
||||
|
||||
export function extractSandboxProvisioningRequest(approvalRequest: ApprovalRequest): {
|
||||
backendId: string;
|
||||
operation: string;
|
||||
params: Record<string, unknown>;
|
||||
} {
|
||||
if (approvalRequest.targetAction.category !== "sandbox_provisioning") {
|
||||
throw new Error(`Approval request ${approvalRequest.id} is not a sandbox_provisioning request`);
|
||||
}
|
||||
|
||||
const context = approvalRequest.targetAction.context;
|
||||
if (!context || typeof context !== "object") {
|
||||
throw new Error(`Approval request ${approvalRequest.id} is missing provisioning context`);
|
||||
}
|
||||
|
||||
const backendId = context.backendId;
|
||||
if (typeof backendId !== "string" || backendId.trim().length === 0) {
|
||||
throw new Error(`Approval request ${approvalRequest.id} has invalid provisioning backend id`);
|
||||
}
|
||||
|
||||
const operation = context.operation;
|
||||
if (typeof operation !== "string" || operation.trim().length === 0) {
|
||||
throw new Error(`Approval request ${approvalRequest.id} has invalid provisioning operation`);
|
||||
}
|
||||
|
||||
const params = context.params;
|
||||
if (!params || typeof params !== "object") {
|
||||
throw new Error(`Approval request ${approvalRequest.id} has invalid provisioning params`);
|
||||
}
|
||||
|
||||
return { backendId, operation, params: params as Record<string, unknown> };
|
||||
}
|
||||
Reference in New Issue
Block a user