feat(FN-3561): wire permanent-agent approval context in runtime paths

Wired permanent-agent approval context into runtime paths for FN-3561, updating the agents documentation and adding test coverage for the heartbeat executor to validate the runtime behavior.

Fusion-Task-Id: FN-3561
This commit is contained in:
Fusion
2026-05-07 19:51:02 -07:00
committed by gsxdsm
parent ff6a568f0c
commit a61ea03b13
12 changed files with 329 additions and 17 deletions

View File

@@ -8,6 +8,7 @@ import {
APPROVAL_REQUEST_AUDIT_EVENT_TYPES,
APPROVAL_REQUEST_STATUSES,
AGENT_PERMISSION_POLICY_ACTION_CATEGORIES,
normalizeApprovalRequestActionCategory,
isValidApprovalRequestTransition,
type ApprovalRequest,
type ApprovalRequestActorSnapshot,
@@ -35,6 +36,15 @@ describe("approval request domain contract", () => {
expect(AGENT_PERMISSION_POLICY_ACTION_CATEGORIES.length).toBeGreaterThan(0);
});
it("normalizes legacy action-category aliases", () => {
expect(normalizeApprovalRequestActionCategory("file_write")).toBe("file_write_delete");
expect(normalizeApprovalRequestActionCategory("file_delete")).toBe("file_write_delete");
expect(normalizeApprovalRequestActionCategory("command_execute")).toBe("command_execution");
expect(normalizeApprovalRequestActionCategory("network_access")).toBe("network_api");
expect(normalizeApprovalRequestActionCategory("task_mutation")).toBe("task_agent_mutation");
expect(normalizeApprovalRequestActionCategory("agent_mutation")).toBe("task_agent_mutation");
});
it("enforces the lifecycle transition matrix", () => {
expect(isValidApprovalRequestTransition("pending", "approved")).toBe(true);
expect(isValidApprovalRequestTransition("pending", "denied")).toBe(true);
@@ -116,6 +126,22 @@ describe("ApprovalRequestStore", () => {
expect(fetched?.runId).toBe("run-abc");
});
it("normalizes legacy category aliases on create/read", () => {
const created = store.create({
requester: REQUESTER,
targetAction: {
category: "file_write",
action: "write",
summary: "Write file",
resourceType: "file",
resourceId: "foo.ts",
},
});
const fetched = store.get(created.id);
expect(fetched?.targetAction.category).toBe("file_write_delete");
});
it("supports pending -> approved and approved -> completed with audit trail", () => {
const created = createSampleRequest();
const approved = store.decide(created.id, "approved", { actor: APPROVER, note: "Looks good" });

View File

@@ -3,6 +3,7 @@ import type { Database } from "./db.js";
import { fromJson, toJsonNullable } from "./db.js";
import {
isValidApprovalRequestTransition,
normalizeApprovalRequestActionCategory,
type ApprovalRequest,
type ApprovalRequestActorSnapshot,
type ApprovalRequestAuditEvent,
@@ -59,7 +60,9 @@ export class ApprovalRequestStore {
actorName: row.requesterActorName,
},
targetAction: {
category: row.targetActionCategory as ApprovalRequest["targetAction"]["category"],
category: normalizeApprovalRequestActionCategory(
row.targetActionCategory as Parameters<typeof normalizeApprovalRequestActionCategory>[0],
),
action: row.targetActionOperation,
summary: row.targetActionSummary,
resourceType: row.targetResourceType,
@@ -130,7 +133,10 @@ export class ApprovalRequestStore {
id: `apr-${randomUUID().slice(0, 8)}`,
status: "pending",
requester: input.requester,
targetAction: input.targetAction,
targetAction: {
...input.targetAction,
category: normalizeApprovalRequestActionCategory(input.targetAction.category),
},
taskId: input.taskId,
runId: input.runId,
requestedAt: now,

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, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, 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, 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, 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, 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_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, 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, 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 { AGENT_VALID_TRANSITIONS } from "./types.js";
export * from "./mesh-replication-protocol.js";
export * from "./mesh-task-replication.js";

View File

@@ -3731,6 +3731,16 @@ export interface PermanentAgentGatingContext {
presetId: string;
rules: Partial<Record<PermanentAgentSensitiveActionCategory, AgentPermissionPolicyDisposition>>;
};
requester?: ApprovalRequestActorSnapshot;
taskId?: string;
runId?: string;
sessionId?: string;
createApprovalRequest?: (input: {
category: AgentPermissionPolicyActionCategory;
toolName: string;
args: Record<string, unknown>;
}) => Promise<ApprovalRequest | null>;
findPendingApprovalRequest?: (dedupeKey: string) => Promise<ApprovalRequest | null>;
}
/** Built-in permission policy preset identifiers for permanent agents. */
@@ -3775,6 +3785,44 @@ export interface ApprovalRequestActorSnapshot {
actorName: string;
}
/** Legacy action-category aliases accepted for backward compatibility. */
export const LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES = [
"file_write",
"file_delete",
"command_execute",
"network_access",
"task_mutation",
"agent_mutation",
] as const;
export type LegacyAgentPermissionPolicyActionCategory =
(typeof LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES)[number];
/** Canonical + compatibility action-category input accepted at boundaries. */
export type ApprovalRequestActionCategoryInput =
| AgentPermissionPolicyActionCategory
| LegacyAgentPermissionPolicyActionCategory;
/** Normalize legacy action-category aliases to canonical v1 categories. */
export function normalizeApprovalRequestActionCategory(
category: ApprovalRequestActionCategoryInput,
): AgentPermissionPolicyActionCategory {
switch (category) {
case "file_write":
case "file_delete":
return "file_write_delete";
case "command_execute":
return "command_execution";
case "network_access":
return "network_api";
case "task_mutation":
case "agent_mutation":
return "task_agent_mutation";
default:
return category;
}
}
/** Action payload gated by an approval request. */
export interface ApprovalRequestTargetAction {
category: AgentPermissionPolicyActionCategory;
@@ -3813,7 +3861,9 @@ export interface ApprovalRequest {
/** Create input for a new pending approval request. */
export interface ApprovalRequestCreateInput {
requester: ApprovalRequestActorSnapshot;
targetAction: ApprovalRequestTargetAction;
targetAction: Omit<ApprovalRequestTargetAction, "category"> & {
category: ApprovalRequestActionCategoryInput;
};
taskId?: string;
runId?: string;
}