feat(FN-3545): add agent permission policy model and persistence

Introduces a multi-agent permission policy model in `@fusion/core` (types, store persistence, API exposure) with corresponding dashboard UI wiring in the agent onboarding modal and task detail modal, plus a distributed task ID overlap retry mechanism for cluster creation, an inline fast mode toggle

Fusion-Task-Id: FN-3545
This commit is contained in:
Fusion
2026-05-07 11:57:24 -07:00
committed by gsxdsm
parent a070c6b995
commit 51c6ddb9a5
9 changed files with 415 additions and 4 deletions

View File

@@ -0,0 +1,70 @@
import { describe, expect, it } from "vitest";
import {
DEFAULT_AGENT_PERMISSION_POLICY_PRESET_ID,
getBuiltInAgentPermissionPolicyPresets,
isAgentPermissionPolicyPresetId,
normalizeAgentPermissionPolicyFromPreset,
resolveEffectiveAgentPermissionPolicy,
} from "../agent-permission-policy.js";
import { AGENT_PERMISSION_POLICY_ACTION_CATEGORIES } from "../types.js";
describe("agent-permission-policy", () => {
it("returns the canonical built-in preset catalog", () => {
const presets = getBuiltInAgentPermissionPolicyPresets();
expect(presets.map((preset) => preset.id)).toEqual([
"unrestricted",
"approval-required",
"locked-down",
]);
});
it("normalizes unrestricted preset with all categories allow", () => {
const policy = normalizeAgentPermissionPolicyFromPreset("unrestricted");
for (const category of AGENT_PERMISSION_POLICY_ACTION_CATEGORIES) {
expect(policy.rules[category]).toBe("allow");
}
});
it("normalizes approval-required preset with all categories require-approval", () => {
const policy = normalizeAgentPermissionPolicyFromPreset("approval-required");
for (const category of AGENT_PERMISSION_POLICY_ACTION_CATEGORIES) {
expect(policy.rules[category]).toBe("require-approval");
}
});
it("normalizes locked-down preset with all categories block", () => {
const policy = normalizeAgentPermissionPolicyFromPreset("locked-down");
for (const category of AGENT_PERMISSION_POLICY_ACTION_CATEGORIES) {
expect(policy.rules[category]).toBe("block");
}
});
it("resolves legacy missing policy to unrestricted default", () => {
const effective = resolveEffectiveAgentPermissionPolicy(undefined);
expect(effective.presetId).toBe(DEFAULT_AGENT_PERMISSION_POLICY_PRESET_ID);
for (const category of AGENT_PERMISSION_POLICY_ACTION_CATEGORIES) {
expect(effective.rules[category]).toBe("allow");
}
});
it("resolves malformed policy payload to unrestricted default", () => {
const effective = resolveEffectiveAgentPermissionPolicy({
presetId: "not-a-preset" as never,
rules: {
"git-write": "block",
"file-write-delete": "block",
"shell-command": "block",
"network-api": "block",
"task-agent-management": "block",
},
});
expect(effective.presetId).toBe(DEFAULT_AGENT_PERMISSION_POLICY_PRESET_ID);
});
it("validates known preset IDs", () => {
expect(isAgentPermissionPolicyPresetId("unrestricted")).toBe(true);
expect(isAgentPermissionPolicyPresetId("approval-required")).toBe(true);
expect(isAgentPermissionPolicyPresetId("locked-down")).toBe(true);
expect(isAgentPermissionPolicyPresetId("custom")).toBe(false);
});
});

View File

@@ -19,6 +19,7 @@ import { mkdtempSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { createHash } from "node:crypto";
import {
AGENT_PERMISSION_POLICY_ACTION_CATEGORIES,
CheckoutConflictError,
getCanonicalAgentAssetDirectoryName,
type AgentCapability,
@@ -315,6 +316,28 @@ describe("AgentStore", () => {
expect(runtimeConfig.runMissedHeartbeatOnStartup).toBe(true);
});
it("stores default unrestricted permission policy for durable agents", async () => {
const agent = await store.createAgent({
name: "Policy Default",
role: "executor",
});
expect(agent.permissionPolicy?.presetId).toBe("unrestricted");
for (const category of AGENT_PERMISSION_POLICY_ACTION_CATEGORIES) {
expect(agent.permissionPolicy?.rules[category]).toBe("allow");
}
});
it("does not backfill permission policy for ephemeral task workers", async () => {
const agent = await store.createAgent({
name: "executor-FN-100",
role: "executor",
metadata: { agentKind: "task-worker", taskWorker: true },
});
expect(agent.permissionPolicy).toBeUndefined();
});
it("preserves custom metadata", async () => {
const agent = await store.createAgent({
name: "With Meta",
@@ -394,6 +417,18 @@ describe("AgentStore", () => {
const result = await store.getAgent("agent-nonexistent");
expect(result).toBeNull();
});
it("resolves legacy durable agents without permissionPolicy to unrestricted", async () => {
const created = await store.createAgent({ name: "Legacy Policy", role: "executor" });
const testDb = (store as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => unknown } } }).db;
testDb.prepare("UPDATE agents SET data = json_remove(data, '$.permissionPolicy') WHERE id = ?").run(created.id);
const hydrated = await store.getAgent(created.id);
expect(hydrated?.permissionPolicy?.presetId).toBe("unrestricted");
for (const category of AGENT_PERMISSION_POLICY_ACTION_CATEGORIES) {
expect(hydrated?.permissionPolicy?.rules[category]).toBe("allow");
}
});
});
// ── getAccessState ────────────────────────────────────────────────
@@ -935,7 +970,7 @@ describe("AgentStore", () => {
expect(revisions[0].after.name).toBe("Renamed");
});
it("records revisions for runtimeConfig, permissions, instructions, soul, and memory changes", async () => {
it("records revisions for runtimeConfig, permissions, permissionPolicy, instructions, soul, and memory changes", async () => {
const created = await store.createAgent({
name: "Configurable",
role: "executor",
@@ -945,6 +980,13 @@ describe("AgentStore", () => {
await store.updateAgent(created.id, { runtimeConfig: { heartbeatIntervalMs: 10000 } });
await store.updateAgent(created.id, { permissions: { canReview: true, canExecute: true } });
await store.updateAgent(created.id, { permissionPolicy: { presetId: "locked-down", rules: {
"git-write": "block",
"file-write-delete": "block",
"shell-command": "block",
"network-api": "block",
"task-agent-management": "block",
} } });
await store.updateAgent(created.id, { instructionsPath: "docs/agent.md" });
await store.updateAgent(created.id, { instructionsText: "Follow safety checks." });
await store.updateAgent(created.id, { soul: "Thoughtful collaborator" });
@@ -955,6 +997,7 @@ describe("AgentStore", () => {
expect(changedFields).toContain("runtimeConfig");
expect(changedFields).toContain("permissions");
expect(changedFields).toContain("permissionPolicy");
expect(changedFields).toContain("instructionsPath");
expect(changedFields).toContain("instructionsText");
expect(changedFields).toContain("soul");

View File

@@ -0,0 +1,80 @@
import type {
AgentPermissionPolicy,
AgentPermissionPolicyPresetId,
AgentPermissionPolicyRules,
} from "./types.js";
import { AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_PRESET_IDS } from "./types.js";
export const DEFAULT_AGENT_PERMISSION_POLICY_PRESET_ID: AgentPermissionPolicyPresetId = "unrestricted";
export interface BuiltInAgentPermissionPolicyPreset {
id: AgentPermissionPolicyPresetId;
name: string;
description: string;
rules: AgentPermissionPolicyRules;
}
const BUILT_IN_PRESETS: Record<AgentPermissionPolicyPresetId, BuiltInAgentPermissionPolicyPreset> = {
unrestricted: {
id: "unrestricted",
name: "Unrestricted",
description: "Allows all runtime action categories (legacy-compatible default).",
rules: buildRules("allow"),
},
"approval-required": {
id: "approval-required",
name: "Approval Required",
description: "Requires approval for all runtime action categories.",
rules: buildRules("require-approval"),
},
"locked-down": {
id: "locked-down",
name: "Locked Down",
description: "Blocks all runtime action categories.",
rules: buildRules("block"),
},
};
function buildRules(disposition: AgentPermissionPolicyRules[(typeof AGENT_PERMISSION_POLICY_ACTION_CATEGORIES)[number]]): AgentPermissionPolicyRules {
return AGENT_PERMISSION_POLICY_ACTION_CATEGORIES.reduce((acc, category) => {
acc[category] = disposition;
return acc;
}, {} as AgentPermissionPolicyRules);
}
export function isAgentPermissionPolicyPresetId(value: unknown): value is AgentPermissionPolicyPresetId {
return typeof value === "string" && (AGENT_PERMISSION_POLICY_PRESET_IDS as readonly string[]).includes(value);
}
export function getBuiltInAgentPermissionPolicyPresets(): BuiltInAgentPermissionPolicyPreset[] {
return AGENT_PERMISSION_POLICY_PRESET_IDS.map((id) => resolveAgentPermissionPolicyPreset(id));
}
export function resolveAgentPermissionPolicyPreset(
presetId: AgentPermissionPolicyPresetId,
): BuiltInAgentPermissionPolicyPreset {
const preset = BUILT_IN_PRESETS[presetId];
return {
...preset,
rules: { ...preset.rules },
};
}
export function normalizeAgentPermissionPolicyFromPreset(
presetId: AgentPermissionPolicyPresetId,
): AgentPermissionPolicy {
const preset = resolveAgentPermissionPolicyPreset(presetId);
return {
presetId: preset.id,
rules: { ...preset.rules },
};
}
export function resolveEffectiveAgentPermissionPolicy(
policy: AgentPermissionPolicy | undefined,
): AgentPermissionPolicy {
if (!policy || !isAgentPermissionPolicyPresetId(policy.presetId)) {
return normalizeAgentPermissionPolicyFromPreset(DEFAULT_AGENT_PERMISSION_POLICY_PRESET_ID);
}
return normalizeAgentPermissionPolicyFromPreset(policy.presetId);
}

View File

@@ -30,6 +30,7 @@ import type {
AgentConfigRevision,
AgentConfigSnapshot,
AgentAccessState,
AgentPermissionPolicy,
OrgTreeNode,
InstructionsBundleConfig,
AgentRating,
@@ -54,6 +55,7 @@ import {
import type { RunMutationContext } from "./types.js";
import type { TaskStore } from "./store.js";
import { computeAccessState } from "./agent-permissions.js";
import { resolveEffectiveAgentPermissionPolicy } from "./agent-permission-policy.js";
import { Database } from "./db.js";
import { createAgentRunSnapshot, createAgentSnapshot, validateSnapshotEnvelope, type AgentRunSnapshot, type AgentSnapshot } from "./shared-mesh-state.js";
@@ -125,6 +127,7 @@ interface AgentData {
runtimeConfig?: Record<string, unknown>;
pauseReason?: string;
permissions?: Record<string, boolean>;
permissionPolicy?: AgentPermissionPolicy;
totalInputTokens?: number;
totalOutputTokens?: number;
lastError?: string;
@@ -606,6 +609,10 @@ export class AgentStore extends EventEmitter {
const resolvedHeartbeatProcedurePath = input.heartbeatProcedurePath
?? (ephemeral ? undefined : getDefaultHeartbeatProcedurePath(agentId, input.name));
const normalizedPermissionPolicy = ephemeral
? input.permissionPolicy
: resolveEffectiveAgentPermissionPolicy(input.permissionPolicy);
const agent: Agent = {
id: agentId,
name: normalizedName,
@@ -620,6 +627,7 @@ export class AgentStore extends EventEmitter {
...(input.reportsTo && { reportsTo: input.reportsTo }),
...(runtimeConfig && { runtimeConfig }),
...(input.permissions && { permissions: input.permissions }),
...(normalizedPermissionPolicy && { permissionPolicy: normalizedPermissionPolicy }),
...(input.instructionsPath && { instructionsPath: input.instructionsPath }),
...(input.instructionsText && { instructionsText: input.instructionsText }),
...(input.soul && { soul: input.soul }),
@@ -1072,6 +1080,7 @@ export class AgentStore extends EventEmitter {
...("runtimeConfig" in updates && { runtimeConfig: updates.runtimeConfig }),
...("pauseReason" in updates && { pauseReason: updates.pauseReason }),
...("permissions" in updates && { permissions: updates.permissions }),
...("permissionPolicy" in updates && { permissionPolicy: updates.permissionPolicy }),
...("lastError" in updates && { lastError: updates.lastError }),
...("totalInputTokens" in updates && { totalInputTokens: updates.totalInputTokens }),
...("totalOutputTokens" in updates && { totalOutputTokens: updates.totalOutputTokens }),
@@ -2274,6 +2283,7 @@ export class AgentStore extends EventEmitter {
| "reportsTo"
| "runtimeConfig"
| "permissions"
| "permissionPolicy"
| "instructionsPath"
| "instructionsText"
| "soul"
@@ -2291,6 +2301,12 @@ export class AgentStore extends EventEmitter {
reportsTo: snapshot.reportsTo,
runtimeConfig: snapshot.runtimeConfig ? { ...snapshot.runtimeConfig } : undefined,
permissions: snapshot.permissions ? { ...snapshot.permissions } : undefined,
permissionPolicy: snapshot.permissionPolicy
? {
presetId: snapshot.permissionPolicy.presetId,
rules: { ...snapshot.permissionPolicy.rules },
}
: undefined,
instructionsPath: snapshot.instructionsPath,
instructionsText: snapshot.instructionsText,
soul: snapshot.soul,
@@ -2575,6 +2591,9 @@ export class AgentStore extends EventEmitter {
runtimeConfig: data.runtimeConfig,
pauseReason: data.pauseReason,
permissions: data.permissions,
permissionPolicy: isEphemeralAgent(data)
? data.permissionPolicy
: resolveEffectiveAgentPermissionPolicy(data.permissionPolicy),
totalInputTokens: data.totalInputTokens,
totalOutputTokens: data.totalOutputTokens,
lastError: data.lastError,
@@ -2605,6 +2624,7 @@ export class AgentStore extends EventEmitter {
runtimeConfig: agent.runtimeConfig,
pauseReason: agent.pauseReason,
permissions: agent.permissions,
permissionPolicy: agent.permissionPolicy,
totalInputTokens: agent.totalInputTokens,
totalOutputTokens: agent.totalOutputTokens,
lastError: agent.lastError,

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, 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, 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, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_PRESET_IDS, 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, AgentPermissionPolicy, AgentPermissionPolicyRules, AgentPermissionPolicyActionCategory, AgentPermissionPolicyDisposition, AgentPermissionPolicyPresetId, 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";
@@ -41,6 +41,15 @@ export {
computeAccessState,
isValidPermission,
} from "./agent-permissions.js";
export {
DEFAULT_AGENT_PERMISSION_POLICY_PRESET_ID,
getBuiltInAgentPermissionPolicyPresets,
resolveAgentPermissionPolicyPreset,
normalizeAgentPermissionPolicyFromPreset,
resolveEffectiveAgentPermissionPolicy,
isAgentPermissionPolicyPresetId,
} from "./agent-permission-policy.js";
export type { BuiltInAgentPermissionPolicyPreset } from "./agent-permission-policy.js";
export { AgentStore, DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS } from "./agent-store.js";
export type { AgentStoreEvents } from "./agent-store.js";
export { ReflectionStore } from "./reflection-store.js";

View File

@@ -3661,6 +3661,39 @@ export const AGENT_PERMISSIONS = [
/** A single canonical permission string. */
export type AgentPermission = (typeof AGENT_PERMISSIONS)[number];
/** Runtime action categories governed by agent permission policy presets. */
export const AGENT_PERMISSION_POLICY_ACTION_CATEGORIES = [
"git-write",
"file-write-delete",
"shell-command",
"network-api",
"task-agent-management",
] as const;
/** A single runtime action category governed by permission policy. */
export type AgentPermissionPolicyActionCategory = (typeof AGENT_PERMISSION_POLICY_ACTION_CATEGORIES)[number];
/** How a runtime action category is handled by permission policy. */
export type AgentPermissionPolicyDisposition = "allow" | "block" | "require-approval";
/** Built-in permission policy preset identifiers for permanent agents. */
export const AGENT_PERMISSION_POLICY_PRESET_IDS = ["unrestricted", "approval-required", "locked-down"] as const;
/** A single built-in permission policy preset identifier. */
export type AgentPermissionPolicyPresetId = (typeof AGENT_PERMISSION_POLICY_PRESET_IDS)[number];
/** Canonical category->disposition map for a permission policy. */
export type AgentPermissionPolicyRules = Record<
AgentPermissionPolicyActionCategory,
AgentPermissionPolicyDisposition
>;
/** First-class persisted permission policy contract for permanent agents. */
export interface AgentPermissionPolicy {
presetId: AgentPermissionPolicyPresetId;
rules: AgentPermissionPolicyRules;
}
/** Describes how an agent's task assignment capability was determined. */
export type TaskAssignSource =
| "role_default" // Granted automatically by role (e.g., scheduler gets tasks:assign)
@@ -3731,6 +3764,8 @@ export interface Agent {
pauseReason?: string;
/** Capability permission flags */
permissions?: Record<string, boolean>;
/** Runtime action gating policy (preset + normalized category rules). */
permissionPolicy?: AgentPermissionPolicy;
/** Cumulative input tokens across all runs */
totalInputTokens?: number;
/** Cumulative output tokens across all runs */
@@ -3873,6 +3908,7 @@ export interface AgentCreateInput {
reportsTo?: string;
runtimeConfig?: Record<string, unknown>;
permissions?: Record<string, boolean>;
permissionPolicy?: AgentPermissionPolicy;
instructionsPath?: string;
instructionsText?: string;
soul?: string;
@@ -3893,6 +3929,7 @@ export interface AgentUpdateInput {
runtimeConfig?: Record<string, unknown>;
pauseReason?: string;
permissions?: Record<string, boolean>;
permissionPolicy?: AgentPermissionPolicy;
lastError?: string;
totalInputTokens?: number;
totalOutputTokens?: number;
@@ -3990,6 +4027,7 @@ export interface AgentConfigSnapshot {
reportsTo?: string;
runtimeConfig?: Record<string, unknown>;
permissions?: Record<string, boolean>;
permissionPolicy?: AgentPermissionPolicy;
instructionsPath?: string;
instructionsText?: string;
soul?: string;
@@ -4119,6 +4157,12 @@ export function agentToConfigSnapshot(agent: Agent): AgentConfigSnapshot {
reportsTo: agent.reportsTo,
runtimeConfig: agent.runtimeConfig ? { ...agent.runtimeConfig } : undefined,
permissions: agent.permissions ? { ...agent.permissions } : undefined,
permissionPolicy: agent.permissionPolicy
? {
presetId: agent.permissionPolicy.presetId,
rules: { ...agent.permissionPolicy.rules },
}
: undefined,
instructionsPath: agent.instructionsPath,
instructionsText: agent.instructionsText,
soul: agent.soul,
@@ -4148,6 +4192,7 @@ export function diffConfigSnapshots(
"reportsTo",
"runtimeConfig",
"permissions",
"permissionPolicy",
"instructionsPath",
"instructionsText",
"soul",