feat(FN-3953): wire agent provisioning approval policy into engine tools

Merged agent provisioning approval policies into the engine with guard controls that restrict which agents can be created and by whom, integrated through the CLI extension and core policy definitions.

Fusion-Task-Id: FN-3953
This commit is contained in:
Fusion
2026-05-10 15:27:36 -07:00
committed by gsxdsm
parent aa031ab601
commit f6a1862f93
10 changed files with 347 additions and 19 deletions

View File

@@ -0,0 +1,107 @@
import type { AgentProvisioningApprovalMode, ApprovalRequest, ProjectSettings } from "./types.js";
type AgentProvisioningSettings = Pick<ProjectSettings, "agentProvisioning">;
export type AgentProvisioningTool = "fn_agent_create" | "fn_agent_delete";
export interface AgentProvisioningPolicyInput {
tool: AgentProvisioningTool;
caller?: { id: string; role?: string; isPrivileged?: boolean };
settings: AgentProvisioningSettings | undefined;
}
export interface AgentProvisioningPolicyDecision {
decision: "allow" | "require-approval" | "deny";
reason: string;
matchedRule:
| "privileged-caller"
| "trusted-agent-id"
| "trusted-role"
| "approval-mode-always"
| "approval-mode-trusted-only"
| "approval-mode-never"
| "delete-always-approve"
| "missing-caller";
effectiveMode: AgentProvisioningApprovalMode;
}
function normalizeMode(settings: AgentProvisioningSettings | undefined): AgentProvisioningApprovalMode {
return settings?.agentProvisioning?.approvalMode ?? "trusted-only";
}
export function resolveAgentProvisioningPolicy(input: AgentProvisioningPolicyInput): AgentProvisioningPolicyDecision {
const effectiveMode = normalizeMode(input.settings);
const caller = input.caller;
if (!caller) {
return { decision: "deny", reason: "missing caller", matchedRule: "missing-caller", 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 alwaysApproveDelete = input.settings?.agentProvisioning?.alwaysApproveDelete ?? true;
if (input.tool === "fn_agent_delete" && alwaysApproveDelete) {
return {
decision: "require-approval",
reason: "delete requires approval by policy",
matchedRule: "delete-always-approve",
effectiveMode,
};
}
const trustedAgentIds = input.settings?.agentProvisioning?.trustedAgentIds ?? [];
if (trustedAgentIds.includes(caller.id)) {
return { decision: "allow", reason: "trusted agent id", matchedRule: "trusted-agent-id", effectiveMode };
}
const trustedRoles = (input.settings?.agentProvisioning?.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 extractAgentProvisioningRequest(approvalRequest: ApprovalRequest): {
tool: AgentProvisioningTool;
params: Record<string, unknown>;
} {
if (approvalRequest.targetAction.category !== "agent_provisioning") {
throw new Error(`Approval request ${approvalRequest.id} is not an agent_provisioning request`);
}
const context = approvalRequest.targetAction.context;
if (!context || typeof context !== "object") {
throw new Error(`Approval request ${approvalRequest.id} is missing provisioning context`);
}
const tool = context.tool;
if (tool !== "fn_agent_create" && tool !== "fn_agent_delete") {
throw new Error(`Approval request ${approvalRequest.id} has invalid provisioning tool`);
}
const params = context.params;
if (!params || typeof params !== "object") {
throw new Error(`Approval request ${approvalRequest.id} has invalid provisioning params`);
}
return { tool, params: params as Record<string, unknown> };
}

View File

@@ -1,5 +1,5 @@
export { COLUMNS, DEFAULT_COLUMN, isColumn, normalizeColumn, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_PRESET_IDS, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, normalizeApprovalRequestActionCategory, isValidApprovalRequestTransition, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, DASHBOARD_USER_ID, normalizeMessageParticipant, validateMessageMetadata, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeConflictStrategy, buildResearchDocumentKey, REPO_OVERRIDE_RE, SHARED_STATE_SNAPSHOT_VERSION } from "./types.js";
export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrStatus, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, MeshReplicatedTaskCreatePayload, MeshReplicatedTaskApplyResult, TaskSource, SourceType, TaskDetail, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, DistributedTaskIdReserveInput, DistributedTaskIdReserveResult, DistributedTaskIdCommitInput, DistributedTaskIdCommitResult, DistributedTaskIdAbortInput, DistributedTaskIdAbortResult, DistributedTaskIdStateInput, DistributedTaskIdStateResult, AutostashOrphanRecord, AutostashOutcome, MergeDetails, MergeResult, MergeConflictStrategy, CanonicalMergeConflictStrategy, Settings, GlobalSettings, ProjectSettings, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, EvalFollowUpPolicy, EvalProjectSettings, ResolvedEvalSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, UnavailableNodePolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, PermanentAgentActionCategory, PermanentAgentSensitiveActionCategory, PermanentAgentGatingContext, AgentPermissionPolicy, AgentPermissionPolicyRules, AgentPermissionPolicyActionCategory, LegacyAgentPermissionPolicyActionCategory, ApprovalRequestActionCategoryInput, AgentPermissionPolicyDisposition, AgentPermissionPolicyPresetId, ApprovalRequestStatus, ApprovalRequestAuditEventType, ApprovalRequestActorSnapshot, ApprovalRequestTargetAction, ApprovalRequestAuditEvent, ApprovalRequest, ApprovalRequestCreateInput, ApprovalRequestDecisionInput, ApprovalRequestCompletionInput, ApprovalRequestListInput, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
export { COLUMNS, DEFAULT_COLUMN, isColumn, normalizeColumn, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, normalizeApprovalRequestActionCategory, isValidApprovalRequestTransition, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, DASHBOARD_USER_ID, normalizeMessageParticipant, validateMessageMetadata, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeConflictStrategy, buildResearchDocumentKey, REPO_OVERRIDE_RE, SHARED_STATE_SNAPSHOT_VERSION } from "./types.js";
export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrStatus, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, MeshReplicatedTaskCreatePayload, MeshReplicatedTaskApplyResult, TaskSource, SourceType, TaskDetail, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, DistributedTaskIdReserveInput, DistributedTaskIdReserveResult, DistributedTaskIdCommitInput, DistributedTaskIdCommitResult, DistributedTaskIdAbortInput, DistributedTaskIdAbortResult, DistributedTaskIdStateInput, DistributedTaskIdStateResult, AutostashOrphanRecord, AutostashOutcome, MergeDetails, MergeResult, MergeConflictStrategy, CanonicalMergeConflictStrategy, Settings, GlobalSettings, ProjectSettings, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, EvalFollowUpPolicy, EvalProjectSettings, ResolvedEvalSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, UnavailableNodePolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, PermanentAgentActionCategory, PermanentAgentSensitiveActionCategory, PermanentAgentGatingContext, AgentPermissionPolicy, AgentPermissionPolicyRules, AgentPermissionPolicyActionCategory, AgentProvisioningApprovalMode, LegacyAgentPermissionPolicyActionCategory, ApprovalRequestActionCategoryInput, ApprovalRequestActionCategory, AgentPermissionPolicyDisposition, AgentPermissionPolicyPresetId, ApprovalRequestStatus, ApprovalRequestAuditEventType, ApprovalRequestActorSnapshot, ApprovalRequestTargetAction, ApprovalRequestAuditEvent, ApprovalRequest, ApprovalRequestCreateInput, ApprovalRequestDecisionInput, ApprovalRequestCompletionInput, ApprovalRequestListInput, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
export { AGENT_VALID_TRANSITIONS } from "./types.js";
export type { TaskReviewData, TaskReviewSummary, TaskReviewItem } from "./types.js";
export * from "./mesh-replication-protocol.js";
@@ -70,6 +70,15 @@ export type { ReflectionStoreEvents } from "./reflection-store.js";
export { MessageStore } from "./message-store.js";
export type { MessageStoreEvents } from "./message-store.js";
export { ApprovalRequestStore } from "./approval-request-store.js";
export {
resolveAgentProvisioningPolicy,
extractAgentProvisioningRequest,
} from "./agent-provisioning-policy.js";
export type {
AgentProvisioningTool,
AgentProvisioningPolicyInput,
AgentProvisioningPolicyDecision,
} from "./agent-provisioning-policy.js";
export { TaskStore } from "./store.js";
export {
createDistributedTaskIdAllocator,

View File

@@ -212,6 +212,7 @@ export const DEFAULT_PROJECT_SETTINGS = {
verificationFixRetries: 3,
buildTimeoutMs: 300_000,
requirePlanApproval: false,
agentProvisioning: {},
specStalenessEnabled: false,
specStalenessMaxAgeMs: 6 * 60 * 60 * 1000,
taskStuckTimeoutMs: 600_000,

View File

@@ -2169,6 +2169,13 @@ export interface ProjectSettings {
* remain in triage with status "awaiting-approval" until a user approves
* or rejects the plan. Default: false. */
requirePlanApproval?: boolean;
/** Approval policy for agent provisioning tools (fn_agent_create/fn_agent_delete). */
agentProvisioning?: {
approvalMode?: AgentProvisioningApprovalMode;
trustedRoles?: string[];
trustedAgentIds?: string[];
alwaysApproveDelete?: boolean;
};
/** When true, enforces that task specifications (PROMPT.md) are refreshed if they
* become stale. Stale specs are detected based on specStalenessMaxAgeMs.
* Default: false. */
@@ -4019,8 +4026,12 @@ export const AGENT_PERMISSION_POLICY_ACTION_CATEGORIES: readonly PermanentAgentS
"task_agent_mutation",
] as const;
export const AGENT_PROVISIONING_APPROVAL_MODES = ["always", "trusted-only", "never"] as const;
export type AgentProvisioningApprovalMode = (typeof AGENT_PROVISIONING_APPROVAL_MODES)[number];
/** A single runtime action category governed by permission policy. */
export type AgentPermissionPolicyActionCategory = PermanentAgentSensitiveActionCategory;
export type ApprovalRequestActionCategory = AgentPermissionPolicyActionCategory | "agent_provisioning";
/** How a runtime action category is handled by permission policy. */
export type AgentPermissionPolicyDisposition = "allow" | "block" | "require-approval";
@@ -4100,13 +4111,13 @@ export type LegacyAgentPermissionPolicyActionCategory =
/** Canonical + compatibility action-category input accepted at boundaries. */
export type ApprovalRequestActionCategoryInput =
| AgentPermissionPolicyActionCategory
| ApprovalRequestActionCategory
| LegacyAgentPermissionPolicyActionCategory;
/** Normalize legacy action-category aliases to canonical v1 categories. */
export function normalizeApprovalRequestActionCategory(
category: ApprovalRequestActionCategoryInput,
): AgentPermissionPolicyActionCategory {
): ApprovalRequestActionCategory {
switch (category) {
case "file_write":
case "file_delete":
@@ -4118,6 +4129,8 @@ export function normalizeApprovalRequestActionCategory(
case "task_mutation":
case "agent_mutation":
return "task_agent_mutation";
case "agent_provisioning":
return "agent_provisioning";
default:
return category;
}
@@ -4125,7 +4138,7 @@ export function normalizeApprovalRequestActionCategory(
/** Action payload gated by an approval request. */
export interface ApprovalRequestTargetAction {
category: AgentPermissionPolicyActionCategory;
category: ApprovalRequestActionCategory;
action: string;
summary: string;
resourceType: string;