diff --git a/.changeset/fn-7413-agent-permissions.md b/.changeset/fn-7413-agent-permissions.md new file mode 100644 index 0000000000..2409f3364c --- /dev/null +++ b/.changeset/fn-7413-agent-permissions.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Allow configuring permissions for ephemeral and permanent agents. +category: feature +dev: Applies capability grants and runtime permission policies consistently across agent lifetimes. diff --git a/docs/agents.md b/docs/agents.md index d73d34f328..6390805048 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -101,7 +101,7 @@ Every first-class editable agent field has a defined create/edit/import/template | `reportsTo` | ✓ | ✓ | ✓ (from manifest) | Parent agent ID | | `runtimeConfig` | ✓ | ✓ | ✗ | Heartbeat/budget config | | `permissions` | ✓ | ✓ | ✗ | Capability flags | -| `permissionPolicy` | ✓ | ✓ | ✗ | Runtime action-gating policy for permanent agents (default/fallback: `unrestricted`) | +| `permissionPolicy` | ✓ | ✓ | ✗ | Runtime action-gating policy for permanent and ephemeral agents (project default/fallback: `unrestricted`) | | `instructionsPath` | ✓ | ✓ | ✗ | File-backed instructions path | | `instructionsText` | ✓ | ✓ | ✓ (from manifest `instructionBody`) | Inline instructions | | `soul` | ✓ | ✓ | ✗ | Personality/identity description | @@ -121,7 +121,7 @@ Every first-class editable agent field has a defined create/edit/import/template | `memory` | `memory` | — | | `skills` | `metadata.skills` | — | -## Permission Policy Presets (Permanent Agents) +## Permission Policy Presets (Permanent and Ephemeral Agents) `permissionPolicy` is a first-class persisted policy contract for **runtime action gating**, separate from role/capability authorization and separate from dashboard persona presets. @@ -148,7 +148,7 @@ V1 runtime action categories: Exact tool overrides are stored as `toolRules: { [toolName]: disposition }` on either a per-agent `permissionPolicy` or the project `defaultAgentPermissionPolicy`. They apply before category rules, so a policy can block one governed tool such as `fn_task_create` while leaving the broader `task_agent_mutation` category set to `allow` for `fn_task_update` or workflow tools. -### Runtime gate v1 mapping (per tool invocation, permanent agents only) +### Runtime gate v1 mapping (per tool invocation, all agent lifetimes) The engine classifies tool calls by behavior (not namespace alone): @@ -212,13 +212,15 @@ FN-3973 follow-through: `spawn_agent` evaluation is complete; governance remains Default and legacy fallback behavior: - New **non-ephemeral/permanent** agents persist a normalized `permissionPolicy` using preset `unrestricted` when not explicitly provided. +- New and existing ephemeral/runtime task-worker agents may store an explicit `permissionPolicy` and canonical `permissions` grants. - Legacy permanent-agent rows missing `permissionPolicy` resolve to the same effective `unrestricted` policy at read time (no eager migration required). -- Ephemeral/runtime task-worker agents are intentionally left unchanged and are not backfilled with a default `permissionPolicy`. +- Legacy ephemeral/runtime task-worker rows missing `permissionPolicy` are not backfilled on disk; runtime sessions inherit the project `defaultAgentPermissionPolicy` (or `unrestricted` when no project default is configured). +- Fallback `executor-FN-*` task workers without a stored agent row use a stable synthetic actor and the same project-default policy, so exact `toolRules` such as `fn_task_create: block` apply consistently. Separation of concerns: - `permissions` capability flags (plus role defaults) determine what an agent is conceptually authorized to do (for example, `tasks:assign`, `agents:create`). -- `permissionPolicy` determines how sensitive runtime actions are gated (`allow`, `block`, `require-approval`) once the capability path is in play. +- `permissionPolicy` determines how sensitive runtime actions are gated (`allow`, `block`, `require-approval`) once the capability path is in play. `require-approval` creates an approval request with the permanent or ephemeral actor identity, pauses the associated task safely, and resumes through the existing approval lifecycle. - Dashboard persona presets (`packages/dashboard/app/components/agent-presets/`) are UI templates for identity/behavior and are **not** the source of truth for permission-policy enforcement. ### CLI agent permission prompts and notifications diff --git a/docs/settings-reference.md b/docs/settings-reference.md index dd16546196..82ebd81c15 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -1519,7 +1519,7 @@ Fusion supports scoped automations and routines: ### `defaultAgentPermissionPolicy` -Project-scoped default permission policy for permanent-agent action gates. +Project-scoped default permission policy for agent runtime action gates. It applies to permanent agents, stored ephemeral/task-worker agents that do not have an explicit per-agent policy, and fallback `executor-FN-*` task workers that have no stored agent row. ```json { @@ -1544,6 +1544,15 @@ Project-scoped default permission policy for permanent-agent action gates. - Missing categories default to `allow` via the built-in `unrestricted` seed; missing or empty `toolRules` preserve legacy category-only behavior. - Runtime precedence is per-agent exact tool rule → per-agent category rule → project default exact tool rule → project default category rule → unrestricted fallback. - Heartbeat-critical coordination/exempt tools remain non-configurable and allowed to prevent deadlocks. +- Legacy ephemeral agents without `permissionPolicy` are not rewritten on disk; they inherit this setting when a runtime session is built. + +### `ephemeralAgentsCanCreateTasks` + +Project-scoped backward-compatibility guard for ephemeral/runtime-managed task workers calling `fn_task_create`. + +- Default: `true`, preserving the historical behavior that task workers can create follow-up tasks. +- When `false`, ephemeral callers are rejected before task creation even if their unified `permissionPolicy` would otherwise allow `fn_task_create`. +- When `true`, the unified runtime policy still applies: `defaultAgentPermissionPolicy.toolRules.fn_task_create = "block"` blocks ephemeral and permanent agents, and `"require-approval"` creates an approval request before the tool can run. ## Model selection hierarchy diff --git a/packages/core/src/__tests__/agent-store.test.ts b/packages/core/src/__tests__/agent-store.test.ts index 05b5e0572a..5f48d8ae8d 100644 --- a/packages/core/src/__tests__/agent-store.test.ts +++ b/packages/core/src/__tests__/agent-store.test.ts @@ -14,6 +14,7 @@ import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, vi } import { AgentStore } from "../agent-store.js"; import { installInMemoryDbSnapshot, clearInMemoryDbSnapshot } from "./store-test-helpers.js"; import { TaskStore } from "../store.js"; +import { resolveEffectiveAgentPermissionPolicy } from "../agent-permission-policy.js"; import { validateSnapshotEnvelope } from "../shared-mesh-state.js"; import { rm } from "node:fs/promises"; import { join } from "node:path"; @@ -21,7 +22,6 @@ 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, @@ -352,16 +352,19 @@ describe("AgentStore", () => { expect(runtimeConfig.runMissedHeartbeatOnStartup).toBe(true); }); - it("stores default unrestricted permission policy for durable agents", async () => { + it("leaves durable agents without explicit permission policy to inherit project defaults", 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"); - } + expect(agent.permissionPolicy).toBeUndefined(); + const effective = resolveEffectiveAgentPermissionPolicy(agent.permissionPolicy, { + rules: { task_agent_mutation: "block" }, + toolRules: { fn_task_create: "block" }, + }); + expect(effective.rules.task_agent_mutation).toBe("block"); + expect(effective.toolRules?.fn_task_create).toBe("block"); }); it("does not backfill permission policy for ephemeral task workers", async () => { @@ -374,6 +377,45 @@ describe("AgentStore", () => { expect(agent.permissionPolicy).toBeUndefined(); }); + it("stores explicit permission policy for ephemeral task workers", async () => { + const agent = await store.createAgent({ + name: "executor-FN-101", + role: "executor", + metadata: { agentKind: "task-worker", taskWorker: true }, + permissionPolicy: { presetId: "custom", rules: { task_agent_mutation: "block" } }, + }); + + expect(agent.permissionPolicy?.presetId).toBe("custom"); + expect(agent.permissionPolicy?.rules.task_agent_mutation).toBe("block"); + expect(agent.permissionPolicy?.rules.git_write).toBe("allow"); + }); + + it("normalizes canonical permission grants for ephemeral and durable agents", async () => { + const durable = await store.createAgent({ + name: "Durable Grants", + role: "executor", + permissions: { "tasks:assign": true, "agents:create": false, nope: true }, + }); + const ephemeral = await store.createAgent({ + name: "executor-FN-102", + role: "executor", + metadata: { agentKind: "task-worker", taskWorker: true }, + permissions: { "tasks:assign": true, "agents:create": false, nope: true }, + }); + + expect(durable.permissions).toEqual({ "tasks:assign": true }); + expect(ephemeral.permissions).toEqual({ "tasks:assign": true }); + }); + + it("rejects invalid explicit policy payloads for ephemeral task workers", async () => { + await expect(store.createAgent({ + name: "executor-FN-103", + role: "executor", + metadata: { agentKind: "task-worker", taskWorker: true }, + permissionPolicy: { presetId: "custom", rules: { task_agent_mutation: "nope" as never } }, + })).rejects.toThrow(/Invalid permission policy disposition/); + }); + it("preserves custom metadata", async () => { const agent = await store.createAgent({ name: "With Meta", @@ -454,16 +496,13 @@ describe("AgentStore", () => { expect(result).toBeNull(); }); - it("resolves legacy durable agents without permissionPolicy to unrestricted", async () => { + it("leaves legacy durable agents without permissionPolicy to inherit project defaults at runtime", 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"); - } + expect(hydrated?.permissionPolicy).toBeUndefined(); }); }); @@ -1011,11 +1050,11 @@ describe("AgentStore", () => { name: "Configurable", role: "executor", runtimeConfig: { heartbeatIntervalMs: 30000 }, - permissions: { canReview: false }, + permissions: { "tasks:assign": false }, }); await store.updateAgent(created.id, { runtimeConfig: { heartbeatIntervalMs: 10000 } }); - await store.updateAgent(created.id, { permissions: { canReview: true, canExecute: true } }); + await store.updateAgent(created.id, { permissions: { "tasks:assign": true, "agents:create": true } }); await store.updateAgent(created.id, { permissionPolicy: { presetId: "locked-down", rules: { "git-write": "block", "file-write-delete": "block", diff --git a/packages/core/src/agent-store.ts b/packages/core/src/agent-store.ts index d19042324d..86c64358ef 100644 --- a/packages/core/src/agent-store.ts +++ b/packages/core/src/agent-store.ts @@ -54,9 +54,9 @@ import { } from "./types.js"; import type { CentralClaimStore, CheckoutClaimContext, RunMutationContext } from "./types.js"; import type { TaskStore } from "./store.js"; -import { computeAccessState } from "./agent-permissions.js"; +import { computeAccessState, normalizePermissions } from "./agent-permissions.js"; import { canAgentTakeImplementationTask, canAgentTakeImplementationTaskForExplicitRouting, formatRoleMismatchReason } from "./agent-role-policy.js"; -import { normalizeAgentPermissionPolicy, resolveEffectiveAgentPermissionPolicy } from "./agent-permission-policy.js"; +import { normalizeAgentPermissionPolicy } from "./agent-permission-policy.js"; import { Database } from "./db.js"; import { createAgentRunSnapshot, createAgentSnapshot, validateSnapshotEnvelope, type AgentRunSnapshot, type AgentSnapshot } from "./shared-mesh-state.js"; @@ -195,6 +195,13 @@ export function formatCurrentTaskLine(taskId: string, linkedTask: Pick | undefined): Record | undefined { + if (!raw) return undefined; + const normalized = normalizePermissions(raw); + if (normalized.size === 0) return undefined; + return Object.fromEntries([...normalized].map((permission) => [permission, true])); +} + function resolveCreationRuntimeConfig( incoming: Record | undefined, metadata: Record, @@ -639,9 +646,13 @@ export class AgentStore extends EventEmitter { const resolvedHeartbeatProcedurePath = input.heartbeatProcedurePath ?? (ephemeral ? undefined : getDefaultHeartbeatProcedurePath(agentId, input.name)); - const normalizedPermissionPolicy = ephemeral - ? input.permissionPolicy - : resolveEffectiveAgentPermissionPolicy(input.permissionPolicy); + /* + FNXC:AgentPermissions 2026-07-02-00:00: + FN-7413 makes permission configuration lifetime-agnostic: durable identity agents and ephemeral task-worker agents may both store explicit capability grants and runtime permission policies. Missing policies stay absent for every lifetime so legacy rows and newly created agents inherit the project default at runtime instead of being materialized as explicit unrestricted overrides. + */ + const normalizedPermissionPolicy = input.permissionPolicy + ? normalizeAgentPermissionPolicy(input.permissionPolicy) + : undefined; const agent: Agent = { id: agentId, @@ -659,7 +670,7 @@ export class AgentStore extends EventEmitter { ...(input.imageUrl && { imageUrl: input.imageUrl }), ...(input.reportsTo && { reportsTo: input.reportsTo }), ...(runtimeConfig && { runtimeConfig }), - ...(input.permissions && { permissions: input.permissions }), + ...(normalizeStoredPermissions(input.permissions) && { permissions: normalizeStoredPermissions(input.permissions) }), ...(normalizedPermissionPolicy && { permissionPolicy: normalizedPermissionPolicy }), ...(input.instructionsPath && { instructionsPath: input.instructionsPath }), ...(input.instructionsText && { instructionsText: input.instructionsText }), @@ -1117,7 +1128,7 @@ export class AgentStore extends EventEmitter { ...("reportsTo" in updates && { reportsTo: updates.reportsTo }), ...("runtimeConfig" in updates && { runtimeConfig: updates.runtimeConfig }), ...("pauseReason" in updates && { pauseReason: updates.pauseReason }), - ...("permissions" in updates && { permissions: updates.permissions }), + ...("permissions" in updates && { permissions: normalizeStoredPermissions(updates.permissions) }), ...("permissionPolicy" in updates && { permissionPolicy: normalizedUpdatedPermissionPolicy }), ...("lastError" in updates && { lastError: updates.lastError }), ...("totalInputTokens" in updates && { totalInputTokens: updates.totalInputTokens }), @@ -2860,10 +2871,8 @@ export class AgentStore extends EventEmitter { reportsTo: data.reportsTo, runtimeConfig: data.runtimeConfig, pauseReason: data.pauseReason, - permissions: data.permissions, - permissionPolicy: isEphemeralAgent(data) - ? data.permissionPolicy - : resolveEffectiveAgentPermissionPolicy(data.permissionPolicy), + permissions: normalizeStoredPermissions(data.permissions), + permissionPolicy: data.permissionPolicy ? normalizeAgentPermissionPolicy(data.permissionPolicy) : undefined, totalInputTokens: data.totalInputTokens, totalOutputTokens: data.totalOutputTokens, lastError: data.lastError, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 9d5f46a799..fda2d306d6 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -4033,10 +4033,10 @@ export interface ProjectSettings { /** Backend ids that may bootstrap without approval. Default: ["native"]. */ autoApproveBackendIds?: string[]; }; - /** Project default runtime permission-policy overrides for permanent agents. + /** Project default runtime permission-policy overrides for all agent lifetimes. * Rules are a partial map of category -> disposition (`allow` | `block` | `require-approval`). * Tool rules are exact tool-name overrides that take precedence over category rules. - * Missing categories and tools inherit the built-in `unrestricted` seed (`allow`). */ + * Missing categories and tools inherit the built-in `unrestricted` seed (`allow`). Agents without an explicit policy, including legacy ephemeral task workers, inherit this project default at runtime. */ defaultAgentPermissionPolicy?: { rules?: Partial; toolRules?: AgentPermissionPolicyToolRules; @@ -6388,7 +6388,7 @@ export type AgentPermissionPolicyDisposition = "allow" | "block" | "require-appr /** Exact tool-name permission overrides layered above category rules. */ export type AgentPermissionPolicyToolRules = Record; -/** Minimum portable permanent-agent gating context consumed by engine runtime wrappers. */ +/** Minimum portable agent gating context consumed by engine runtime wrappers. The legacy name is retained for API compatibility, but the context applies to permanent identity agents and ephemeral task-worker agents. */ export interface PermanentAgentGatingContext { permissionPolicy?: { presetId: string; @@ -6407,7 +6407,7 @@ export interface PermanentAgentGatingContext { findPendingApprovalRequest?: (dedupeKey: string) => Promise; } -/** Built-in permission policy preset identifiers for permanent agents. */ +/** Built-in permission policy preset identifiers for agent runtime policies. */ export const AGENT_PERMISSION_POLICY_PRESET_IDS = ["unrestricted", "approval-required", "locked-down", "custom"] as const; /** A single built-in permission policy preset identifier. */ @@ -6420,7 +6420,7 @@ export type AgentPermissionPolicyRules = Record< >; /** - * First-class persisted permission policy contract for permanent agents. + * First-class persisted permission policy contract for permanent and ephemeral agents. * * FNXC:ToolPermissions 2026-07-01-00:00: * Operators must be able to block a single governed tool such as `fn_task_create` without blocking every task-agent mutation. `toolRules` stores exact tool-name overrides and the engine resolves them before category rules while leaving heartbeat-critical exempt tools non-configurable. diff --git a/packages/dashboard/app/components/AgentDetailView.css b/packages/dashboard/app/components/AgentDetailView.css index 7669fd4c4f..a7c54592c6 100644 --- a/packages/dashboard/app/components/AgentDetailView.css +++ b/packages/dashboard/app/components/AgentDetailView.css @@ -2163,3 +2163,51 @@ FNXC:AgentDetailView 2026-06-26-01:00: background: var(--surface); margin-bottom: var(--space-sm); } + +.agent-capability-grants { + display: flex; + flex-direction: column; + gap: var(--space-sm); + margin-bottom: var(--space-md); +} + +.agent-capability-grants h4 { + margin: 0; + color: var(--text-primary); + font-size: var(--font-size-md); +} + +.agent-capability-grants-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--size-card-min, 16rem)), 1fr)); + gap: var(--space-sm); +} + +.agent-capability-grant-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-sm); + padding: var(--space-sm); + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--surface); +} + +.agent-capability-grant-name { + display: block; + color: var(--text-primary); + font-weight: 600; +} + +@media (max-width: 768px) { + .agent-capability-grants-grid { + grid-template-columns: 1fr; + } + + .agent-capability-grant-row, + .agent-permission-inherit-banner { + align-items: flex-start; + flex-direction: column; + } +} diff --git a/packages/dashboard/app/components/AgentDetailView.tsx b/packages/dashboard/app/components/AgentDetailView.tsx index 2703ed6725..d6e3edb79f 100644 --- a/packages/dashboard/app/components/AgentDetailView.tsx +++ b/packages/dashboard/app/components/AgentDetailView.tsx @@ -15,8 +15,8 @@ import remarkGfm from "remark-gfm"; import type { AgentDetail, AgentState, AgentHeartbeatRun, AgentBudgetStatus, ModelInfo, MemoryFileInfo, AgentCapability, PluginRuntimeInfo, SkillContent, AgentOnboardingSummary, AgentMailboxResponse, AgentPromptSizePoint } from "../api"; import { fetchAgent, updateAgent, updateAgentState, deleteAgent, fetchAgentLogsWithMeta, fetchAgentRunLogs, fetchAgentChildren, fetchAgentRuns, fetchAgentRunDetail, startAgentRun, stopAgentRun, updateAgentInstructions, updateAgentSoul, updateAgentMemory, fetchAgentMemoryFiles, fetchAgentMemoryFile, saveAgentMemoryFile, fetchAgentTasks, fetchChainOfCommand, fetchAgentBudgetStatus, resetAgentBudget, fetchWorkspaceFileContent, saveWorkspaceFileContent, fetchModels, fetchPluginRuntimes, fetchAgents, fetchSettingsByScope, upgradeAgentHeartbeatProcedure, fetchSkillContent, uploadAgentAvatar, deleteAgentAvatar, fetchAgentMailbox, markMessageRead, fetchAgentPromptSizes } from "../api"; import type { Agent } from "../api"; -import type { AgentLogEntry, Task, Message, ParticipantType, AgentPermissionPolicy, AgentPermissionPolicyRules } from "@fusion/core"; -import { getErrorMessage, isEphemeralAgent } from "@fusion/core"; +import type { AgentLogEntry, Task, Message, ParticipantType, AgentPermissionPolicy, AgentPermissionPolicyRules, AgentPermission } from "@fusion/core"; +import { AGENT_PERMISSIONS, getErrorMessage, isEphemeralAgent } from "@fusion/core"; import { AgentLogViewer } from "./AgentLogViewer"; import { LoadingSpinner } from "./LoadingSpinner"; import { AgentReflectionsTab } from "./AgentReflectionsTab"; @@ -43,6 +43,20 @@ function cn(...classes: (string | boolean | undefined | null)[]): string { return classes.filter(Boolean).join(" "); } +/* +FNXC:AgentPermissions 2026-07-02-00:00: +Agent Detail must show explicit capability grants next to role-default grants for both permanent and ephemeral agents. Keep this UI map aligned with core ROLE_DEFAULT_PERMISSIONS while the dashboard source build imports constants through the core types surface. +*/ +const AGENT_ROLE_DEFAULT_PERMISSION_MAP: Record = { + triage: ["tasks:create", "agents:view", "messages:read"], + executor: ["tasks:execute", "agents:view", "messages:read", "messages:send"], + reviewer: ["tasks:review", "agents:view", "messages:read", "messages:send"], + merger: ["tasks:merge", "agents:view", "messages:read"], + scheduler: ["tasks:assign", "tasks:create", "tasks:archive", "agents:view", "automations:manage", "missions:manage", "messages:read"], + engineer: ["tasks:execute", "tasks:review", "agents:view", "messages:read", "messages:send"], + custom: [], +}; + /** * Format an ISO timestamp to a relative time string. */ @@ -3759,6 +3773,7 @@ function ConfigTab({ const [modelValue, setModelValue] = useState(initialModelValue); const [selectedRuntimeId, setSelectedRuntimeId] = useState(initialRuntimeHint); const [permissionPolicyValue, setPermissionPolicyValue] = useState(agent.permissionPolicy); + const [permissionsValue, setPermissionsValue] = useState>(agent.permissions ?? {}); const [projectDefaultPermissionPolicy, setProjectDefaultPermissionPolicy] = useState<{ rules?: Partial; toolRules?: AgentPermissionPolicy["toolRules"] } | undefined>(undefined); const managerSelection = reportsToValue.trim(); @@ -3856,6 +3871,10 @@ function ConfigTab({ setPermissionPolicyValue(agent.permissionPolicy); }, [agent.permissionPolicy]); + useEffect(() => { + setPermissionsValue(agent.permissions ?? {}); + }, [agent.permissions]); + useEffect(() => { fetchSettingsByScope(projectId) .then((scoped) => setProjectDefaultPermissionPolicy(scoped.project?.defaultAgentPermissionPolicy)) @@ -3873,6 +3892,29 @@ function ConfigTab({ } }; + const roleDefaultPermissions = useMemo(() => new Set(AGENT_ROLE_DEFAULT_PERMISSION_MAP[roleValue] ?? []), [roleValue]); + const explicitPermissionSet = useMemo(() => new Set( + AGENT_PERMISSIONS.filter((permission) => permissionsValue[permission] === true), + ), [permissionsValue]); + + const handleCapabilityPermissionChange = async (permission: AgentPermission, granted: boolean) => { + const next = { ...permissionsValue }; + if (granted) { + next[permission] = true; + } else { + delete next[permission]; + } + setPermissionsValue(next); + try { + await updateAgent(agent.id, { permissions: next }, projectId); + await onSaved(); + addToast(t("agents.capabilityPermissionsUpdated", "Capability grants updated"), "success"); + } catch (err) { + setPermissionsValue(agent.permissions ?? {}); + addToast(t("agents.capabilityPermissionsFailed", "Failed to update capability grants: {{error}}", { error: getErrorMessage(err) }), "error"); + } + }; + // Load candidate managers for reports-to dropdown useEffect(() => { let cancelled = false; @@ -4715,6 +4757,41 @@ function ConfigTab({

{t("agents.permissionsDescription", "Per-agent settings override project defaults. Each category controls a separate approval gate.")}

+
+

{t("agents.capabilityGrantsTitle", "Capability grants")}

+

+ {t("agents.capabilityGrantsDescription", "Explicit grants add to this agent's role defaults and apply to both permanent and ephemeral agents.")} +

+
+ {AGENT_PERMISSIONS.map((permission) => { + const roleDefault = roleDefaultPermissions.has(permission); + const explicitGrant = explicitPermissionSet.has(permission); + const inputId = `agent-capability-${agent.id}-${permission.replace(/[^a-z0-9]+/gi, "-")}`; + return ( + + ); + })} +
+
+ {permissionPolicyValue === undefined ? (
{t("agents.inheritingProjectDefault", "Inheriting project default — no per-agent override set")} diff --git a/packages/dashboard/app/components/settings/sections/AgentPermissionsSection.tsx b/packages/dashboard/app/components/settings/sections/AgentPermissionsSection.tsx index 9da49832e9..7caac382a8 100644 --- a/packages/dashboard/app/components/settings/sections/AgentPermissionsSection.tsx +++ b/packages/dashboard/app/components/settings/sections/AgentPermissionsSection.tsx @@ -20,7 +20,7 @@ export function AgentPermissionsSection({ scopeBanner, form, setForm }: AgentPer {scopeBanner}

{t("settings.agentPermissions.agentPermissions", "Agent Permissions")}

- {t("settings.agentPermissions.perAgentSettingsOverrideProjectDefaultsEachCategory", "Per-agent settings override project defaults. Each category controls a separate approval gate.")} + {t("settings.agentPermissions.perAgentSettingsOverrideProjectDefaultsEachCategory", "Project defaults apply to permanent agents, ephemeral task workers, and fallback executor workers unless a per-agent override is set. Exact tool rules compose with the legacy ephemeral create-task toggle.")}
setForm((f) => ({ ...f, diff --git a/packages/dashboard/src/__tests__/routes-agents.test.ts b/packages/dashboard/src/__tests__/routes-agents.test.ts index 9c4d1e7a23..94375e3d4a 100644 --- a/packages/dashboard/src/__tests__/routes-agents.test.ts +++ b/packages/dashboard/src/__tests__/routes-agents.test.ts @@ -648,7 +648,7 @@ describe("Agent create/update routes", () => { icon: "🧪", reportsTo: agentId, runtimeConfig: { heartbeatIntervalMs: 60000 }, - permissions: { read: true }, + permissions: { "tasks:assign": true }, permissionPolicy: { presetId: "approval-required" }, instructionsPath: "docs/reviewer.md", instructionsText: "Check test quality.", @@ -666,7 +666,7 @@ describe("Agent create/update routes", () => { icon: "🧪", reportsTo: agentId, runtimeConfig: { heartbeatIntervalMs: 60000 }, - permissions: { read: true }, + permissions: { "tasks:assign": true }, permissionPolicy: { presetId: "approval-required", rules: { @@ -697,7 +697,7 @@ describe("Agent create/update routes", () => { reportsTo: "agent-parent", runtimeConfig: { heartbeatTimeoutMs: 120000 }, pauseReason: "manual", - permissions: { deploy: true }, + permissions: { "agents:create": true }, permissionPolicy: { presetId: "locked-down" }, totalInputTokens: 42, totalOutputTokens: 21, @@ -719,7 +719,7 @@ describe("Agent create/update routes", () => { reportsTo: "agent-parent", runtimeConfig: { heartbeatTimeoutMs: 120000 }, pauseReason: "manual", - permissions: { deploy: true }, + permissions: { "agents:create": true }, permissionPolicy: { presetId: "locked-down", rules: { diff --git a/packages/dashboard/src/routes/register-agent-core-routes.ts b/packages/dashboard/src/routes/register-agent-core-routes.ts index 1889b359a2..39ef932cc0 100644 --- a/packages/dashboard/src/routes/register-agent-core-routes.ts +++ b/packages/dashboard/src/routes/register-agent-core-routes.ts @@ -5,6 +5,7 @@ import type { Agent, AgentCapability, AgentUpdateInput, TaskStore, AgentPermissi import { ApprovalRequestStore, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, + AGENT_PERMISSIONS, aggregateTaskTokenTotalsByAgentLink, getDefaultHeartbeatProcedurePath, isAgentPermissionPolicyPresetId, @@ -29,6 +30,26 @@ const AVATAR_MIME_TO_EXT: Record = { }; const MAX_AVATAR_BYTES = 2 * 1024 * 1024; const VALID_POLICY_DISPOSITIONS: readonly AgentPermissionPolicyDisposition[] = ["allow", "block", "require-approval"] as const; +const VALID_AGENT_PERMISSION_KEYS = new Set(AGENT_PERMISSIONS); + +function parsePermissionsInput(input: unknown): Record { + if (typeof input !== "object" || input === null || Array.isArray(input)) { + throw badRequest("permissions must be an object"); + } + const normalized: Record = {}; + for (const [key, value] of Object.entries(input as Record)) { + if (!VALID_AGENT_PERMISSION_KEYS.has(key)) { + throw badRequest(`permissions contains unknown key: ${key}`); + } + if (typeof value !== "boolean") { + throw badRequest(`permissions.${key} must be a boolean`); + } + if (value) { + normalized[key] = true; + } + } + return normalized; +} function parsePermissionPolicyInput(input: unknown) { if (typeof input !== "object" || input === null || Array.isArray(input)) { @@ -230,9 +251,7 @@ export function registerAgentCoreListCreateRoutes(ctx: ApiRoutesContext, deps: A if (runtimeConfig !== undefined && (typeof runtimeConfig !== "object" || runtimeConfig === null || Array.isArray(runtimeConfig))) { throw badRequest("runtimeConfig must be an object"); } - if (permissions !== undefined && (typeof permissions !== "object" || permissions === null || Array.isArray(permissions))) { - throw badRequest("permissions must be an object"); - } + const normalizedPermissions = permissions !== undefined && permissions !== null ? parsePermissionsInput(permissions) : undefined; let normalizedPermissionPolicy; if (permissionPolicy !== undefined && permissionPolicy !== null) { normalizedPermissionPolicy = parsePermissionPolicyInput(permissionPolicy); @@ -291,7 +310,7 @@ export function registerAgentCoreListCreateRoutes(ctx: ApiRoutesContext, deps: A icon: icon ?? undefined, reportsTo: reportsTo ?? undefined, runtimeConfig, - permissions, + permissions: normalizedPermissions, permissionPolicy: normalizedPermissionPolicy, instructionsPath: instructionsPath ?? undefined, instructionsText: instructionsText ?? undefined, @@ -671,10 +690,7 @@ export function registerAgentCoreRoutes(ctx: ApiRoutesContext, deps: AgentCoreRo } if ("permissions" in body) { - if (body.permissions !== null && (typeof body.permissions !== "object" || Array.isArray(body.permissions))) { - throw badRequest("permissions must be an object"); - } - updates.permissions = body.permissions ?? undefined; + updates.permissions = body.permissions !== null ? parsePermissionsInput(body.permissions) : undefined; } if ("permissionPolicy" in body) { diff --git a/packages/engine/src/__tests__/heartbeat-executor.test.ts b/packages/engine/src/__tests__/heartbeat-executor.test.ts index 8e2d0d00da..903a244e23 100644 --- a/packages/engine/src/__tests__/heartbeat-executor.test.ts +++ b/packages/engine/src/__tests__/heartbeat-executor.test.ts @@ -788,7 +788,7 @@ describe("executeHeartbeat", () => { expect((store.updateAgent as any)).toHaveBeenCalledWith("agent-001", { pauseReason: "awaiting-approval" }); }); - it("omits permanent-agent gating context for ephemeral heartbeat agents", async () => { + it("passes permission gating context for ephemeral heartbeat agents", async () => { const store = createStoreWithAgentForExec({ taskId: "FN-001", metadata: { agentKind: "task-worker" }, @@ -805,8 +805,16 @@ describe("executeHeartbeat", () => { permanentAgentGating?: unknown; actionGateContext?: unknown; }; - expect(args.permanentAgentGating).toBeUndefined(); - expect(args.actionGateContext).toBeUndefined(); + expect(args.permanentAgentGating).toMatchObject({ + permissionPolicy: { presetId: "unrestricted" }, + requester: { actorId: "agent-001", actorName: "executor-ephemeral" }, + }); + expect(args.actionGateContext).toMatchObject({ + agentId: "agent-001", + agentName: "executor-ephemeral", + isEphemeral: true, + permissionPolicy: { presetId: "unrestricted" }, + }); }); describe("dependency validation", () => { diff --git a/packages/engine/src/agent-heartbeat.ts b/packages/engine/src/agent-heartbeat.ts index 97c476a15b..8be57b7f3b 100644 --- a/packages/engine/src/agent-heartbeat.ts +++ b/packages/engine/src/agent-heartbeat.ts @@ -1169,15 +1169,12 @@ export class HeartbeatMonitor { return this.approvalRequestStore; } - private buildActionGateContext(agent: Agent, taskId?: string, runId?: string, projectDefaultPolicy?: { rules?: Partial }): AgentActionGateContext | undefined { - if (isEphemeralAgent(agent)) { - return undefined; - } + private buildActionGateContext(agent: Agent, taskId?: string, runId?: string, projectDefaultPolicy?: { rules?: Partial; toolRules?: import("@fusion/core").AgentPermissionPolicyToolRules }): AgentActionGateContext | undefined { const policy = resolveEffectiveAgentPermissionPolicy(agent.permissionPolicy, projectDefaultPolicy); return { agentId: agent.id, agentName: agent.name, - isEphemeral: false, + isEphemeral: isEphemeralAgent(agent), taskId, runId, permissionPolicy: policy, @@ -1222,11 +1219,7 @@ export class HeartbeatMonitor { }; } - private buildPermanentAgentGatingContext(agent: Agent, taskId?: string, runId?: string, projectDefaultPolicy?: { rules?: Partial }): import("@fusion/core").PermanentAgentGatingContext | undefined { - if (isEphemeralAgent(agent)) { - return undefined; - } - + private buildPermanentAgentGatingContext(agent: Agent, taskId?: string, runId?: string, projectDefaultPolicy?: { rules?: Partial; toolRules?: import("@fusion/core").AgentPermissionPolicyToolRules }): import("@fusion/core").PermanentAgentGatingContext | undefined { return { permissionPolicy: resolveEffectiveAgentPermissionPolicy(agent.permissionPolicy, projectDefaultPolicy), requester: { actorId: agent.id, actorType: "agent", actorName: agent.name }, @@ -1239,13 +1232,13 @@ export class HeartbeatMonitor { targetAction: { category, action: toolName, - summary: `Permanent-agent gated action for ${toolName}`, + summary: `Agent gated action for ${toolName}`, resourceType: "tool", resourceId: toolName, context: { toolName, toolArgs: args, - source: "permanent-agent-gating", + source: "agent-gating", }, }, }), diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index b8dc655008..8cb428d152 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -2174,23 +2174,27 @@ export class TaskExecutor { return this._approvalRequestStore; } - private buildActionGateContext(taskId: string | undefined, agent: Agent | null | undefined, projectDefaultPolicy?: { rules?: Partial }): AgentActionGateContext | undefined { - if (!agent || isEphemeralAgent(agent)) { - return undefined; - } - const policy = resolveEffectiveAgentPermissionPolicy(agent.permissionPolicy, projectDefaultPolicy); + private buildActionGateContext(taskId: string | undefined, agent: Agent | null | undefined, projectDefaultPolicy?: { rules?: Partial; toolRules?: import("@fusion/core").AgentPermissionPolicyToolRules }): AgentActionGateContext | undefined { + /* + FNXC:AgentPermissions 2026-07-02-00:00: + FN-7413 requires task-scoped runtime gates for permanent identity agents, stored ephemeral agents, and fallback executor-FN task workers. Use a stable synthetic actor for fallback workers so category/exact-tool rules and approval dedupe keys apply even when no agent row exists. + */ + const actorId = agent?.id ?? `executor-${taskId ?? "unknown"}`; + const actorName = agent?.name ?? `Task worker ${taskId ?? "unknown"}`; + const isEphemeral = !agent || isEphemeralAgent(agent); + const policy = resolveEffectiveAgentPermissionPolicy(agent?.permissionPolicy, projectDefaultPolicy); return { - agentId: agent.id, - agentName: agent.name, - isEphemeral: false, + agentId: actorId, + agentName: actorName, + isEphemeral, taskId, runId: taskId ? this.getRunContextFor(taskId)?.runId : undefined, permissionPolicy: policy, createApprovalRequest: async (decision, args) => this.approvalRequestStore.create({ requester: { - actorId: agent.id, + actorId, actorType: "agent", - actorName: agent.name, + actorName, }, taskId, runId: taskId ? this.getRunContextFor(taskId)?.runId : undefined, @@ -2209,16 +2213,16 @@ export class TaskExecutor { }, }), findApprovalByDedupeKey: async (dedupeKey) => { - const latest = this.approvalRequestStore.findLatestByDedupeKey({ requesterActorId: agent.id, taskId, dedupeKey }); + const latest = this.approvalRequestStore.findLatestByDedupeKey({ requesterActorId: actorId, taskId, dedupeKey }); return latest ? { id: latest.id, status: latest.status } : null; }, findPendingApprovalByDedupeKey: async (dedupeKey) => { - const latest = this.approvalRequestStore.findLatestByDedupeKey({ requesterActorId: agent.id, taskId, dedupeKey }); + const latest = this.approvalRequestStore.findLatestByDedupeKey({ requesterActorId: actorId, taskId, dedupeKey }); return latest?.status === "pending" ? { id: latest.id } : null; }, pauseForApproval: async ({ approvalRequestId, decision }) => { if (taskId) { - await this.store.pauseTask(taskId, true, this.getRunContextFor(taskId), { pausedByAgentId: agent.id }); + await this.store.pauseTask(taskId, true, this.getRunContextFor(taskId), { pausedByAgentId: actorId }); await this.store.logEntry( taskId, `Approval required for ${decision.toolName}. Request ${approvalRequestId} created; task and agent paused awaiting decision.`, @@ -2226,57 +2230,56 @@ export class TaskExecutor { this.getRunContextFor(taskId), ); } - if (this.options.agentStore) { + if (agent && this.options.agentStore) { await this.options.agentStore.updateAgentState(agent.id, "paused"); await this.options.agentStore.updateAgent(agent.id, { pauseReason: "awaiting-approval" }); } }, markApprovalCompleted: async (approvalRequestId) => { await this.approvalRequestStore.markCompleted(approvalRequestId, { - actor: { actorId: agent.id, actorType: "agent", actorName: agent.name }, + actor: { actorId, actorType: "agent", actorName }, note: "Tool executed after approval", }); }, }; } - private buildPermanentAgentGatingContext(taskId: string | undefined, agent: Agent | null | undefined, projectDefaultPolicy?: { rules?: Partial }): import("@fusion/core").PermanentAgentGatingContext | undefined { - if (!agent || isEphemeralAgent(agent)) { - return undefined; - } + private buildPermanentAgentGatingContext(taskId: string | undefined, agent: Agent | null | undefined, projectDefaultPolicy?: { rules?: Partial; toolRules?: import("@fusion/core").AgentPermissionPolicyToolRules }): import("@fusion/core").PermanentAgentGatingContext | undefined { + const actorId = agent?.id ?? `executor-${taskId ?? "unknown"}`; + const actorName = agent?.name ?? `Task worker ${taskId ?? "unknown"}`; return { - permissionPolicy: resolveEffectiveAgentPermissionPolicy(agent.permissionPolicy, projectDefaultPolicy), + permissionPolicy: resolveEffectiveAgentPermissionPolicy(agent?.permissionPolicy, projectDefaultPolicy), requester: { - actorId: agent.id, + actorId, actorType: "agent", - actorName: agent.name, + actorName, }, taskId, runId: taskId ? this.getRunContextFor(taskId)?.runId : undefined, createApprovalRequest: async ({ category, toolName, args }) => this.approvalRequestStore.create({ requester: { - actorId: agent.id, + actorId, actorType: "agent", - actorName: agent.name, + actorName, }, taskId, runId: taskId ? this.getRunContextFor(taskId)?.runId : undefined, targetAction: { category, action: toolName, - summary: `Permanent-agent gated action for ${toolName}`, + summary: `Agent gated action for ${toolName}`, resourceType: "tool", resourceId: toolName, context: { toolName, toolArgs: args, - source: "permanent-agent-gating", + source: "agent-gating", }, }, }), findPendingApprovalRequest: async (dedupeKey) => { - const pending = this.approvalRequestStore.list({ status: "pending", requesterActorId: agent.id, taskId, limit: 100 }); + const pending = this.approvalRequestStore.list({ status: "pending", requesterActorId: actorId, taskId, limit: 100 }); return pending.find((request) => request.targetAction.context?.approvalDedupeKey === dedupeKey) ?? null; }, }; diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json index c5c830a3a1..899f9bd5a8 100644 --- a/packages/i18n/locales/en/app.json +++ b/packages/i18n/locales/en/app.json @@ -1126,7 +1126,15 @@ "weekly": "Weekly", "workingOn": "Working on", "zoomIn": "Zoom in", - "zoomOut": "Zoom out" + "zoomOut": "Zoom out", + "capabilityGrantsTitle": "Capability grants", + "capabilityGrantsDescription": "Explicit grants add to this agent's role defaults and apply to both permanent and ephemeral agents.", + "roleDefaultGrant": "Role default grant", + "explicitGrant": "Explicit grant", + "notGranted": "Not granted", + "toggleCapabilityGrant": "Toggle explicit grant for {{permission}}", + "capabilityPermissionsUpdated": "Capability grants updated", + "capabilityPermissionsFailed": "Failed to update capability grants: {{error}}" }, "app": { "backendError": { @@ -5642,7 +5650,7 @@ "agentPermissions": "Agent Permissions", "agentProvisioningApprovals": "Agent Provisioning Approvals", "configureProjectLevelApprovalBehaviorForDurableProvisioning": " Configure project-level approval behavior for durable provisioning tools (fn_agent_create/fn_agent_delete). ", - "perAgentSettingsOverrideProjectDefaultsEachCategory": "Per-agent settings override project defaults. Each category controls a separate approval gate." + "perAgentSettingsOverrideProjectDefaultsEachCategory": "Project defaults apply to permanent agents, ephemeral task workers, and fallback executor workers unless a per-agent override is set. Exact tool rules compose with the legacy ephemeral create-task toggle." }, "appearance": { "hideAISessionNotificationBanners": "Hide AI session notification banners", diff --git a/packages/i18n/locales/es/app.json b/packages/i18n/locales/es/app.json index fafcf53720..8ca36b87bd 100644 --- a/packages/i18n/locales/es/app.json +++ b/packages/i18n/locales/es/app.json @@ -1116,7 +1116,15 @@ "weekly": "Semanal", "workingOn": "Trabajando en:", "zoomIn": "Acercar", - "zoomOut": "Alejar" + "zoomOut": "Alejar", + "capabilityGrantsTitle": "Capability grants", + "capabilityGrantsDescription": "Explicit grants add to this agent's role defaults and apply to both permanent and ephemeral agents.", + "roleDefaultGrant": "Role default grant", + "explicitGrant": "Explicit grant", + "notGranted": "Not granted", + "toggleCapabilityGrant": "Toggle explicit grant for {{permission}}", + "capabilityPermissionsUpdated": "Capability grants updated", + "capabilityPermissionsFailed": "Failed to update capability grants: {{error}}" }, "app": { "backendError": { @@ -5632,7 +5640,7 @@ "agentPermissions": "", "agentProvisioningApprovals": "", "configureProjectLevelApprovalBehaviorForDurableProvisioning": "", - "perAgentSettingsOverrideProjectDefaultsEachCategory": "" + "perAgentSettingsOverrideProjectDefaultsEachCategory": "Project defaults apply to permanent agents, ephemeral task workers, and fallback executor workers unless a per-agent override is set. Exact tool rules compose with the legacy ephemeral create-task toggle." }, "appearance": { "hideAISessionNotificationBanners": "", diff --git a/packages/i18n/locales/fr/app.json b/packages/i18n/locales/fr/app.json index bc43713c1f..385732457f 100644 --- a/packages/i18n/locales/fr/app.json +++ b/packages/i18n/locales/fr/app.json @@ -1116,7 +1116,15 @@ "weekly": "Hebdomadaire", "workingOn": "En cours :", "zoomIn": "Zoomer", - "zoomOut": "Dézoomer" + "zoomOut": "Dézoomer", + "capabilityGrantsTitle": "Capability grants", + "capabilityGrantsDescription": "Explicit grants add to this agent's role defaults and apply to both permanent and ephemeral agents.", + "roleDefaultGrant": "Role default grant", + "explicitGrant": "Explicit grant", + "notGranted": "Not granted", + "toggleCapabilityGrant": "Toggle explicit grant for {{permission}}", + "capabilityPermissionsUpdated": "Capability grants updated", + "capabilityPermissionsFailed": "Failed to update capability grants: {{error}}" }, "app": { "backendError": { @@ -5632,7 +5640,7 @@ "agentPermissions": "", "agentProvisioningApprovals": "", "configureProjectLevelApprovalBehaviorForDurableProvisioning": "", - "perAgentSettingsOverrideProjectDefaultsEachCategory": "" + "perAgentSettingsOverrideProjectDefaultsEachCategory": "Project defaults apply to permanent agents, ephemeral task workers, and fallback executor workers unless a per-agent override is set. Exact tool rules compose with the legacy ephemeral create-task toggle." }, "appearance": { "hideAISessionNotificationBanners": "", diff --git a/packages/i18n/locales/ko/app.json b/packages/i18n/locales/ko/app.json index 8227c6dfe7..bff751be7a 100644 --- a/packages/i18n/locales/ko/app.json +++ b/packages/i18n/locales/ko/app.json @@ -1116,7 +1116,15 @@ "weekly": "주간", "workingOn": "작업 중", "zoomIn": "확대", - "zoomOut": "축소" + "zoomOut": "축소", + "capabilityGrantsTitle": "Capability grants", + "capabilityGrantsDescription": "Explicit grants add to this agent's role defaults and apply to both permanent and ephemeral agents.", + "roleDefaultGrant": "Role default grant", + "explicitGrant": "Explicit grant", + "notGranted": "Not granted", + "toggleCapabilityGrant": "Toggle explicit grant for {{permission}}", + "capabilityPermissionsUpdated": "Capability grants updated", + "capabilityPermissionsFailed": "Failed to update capability grants: {{error}}" }, "app": { "backendError": { @@ -5632,7 +5640,7 @@ "agentPermissions": "", "agentProvisioningApprovals": "", "configureProjectLevelApprovalBehaviorForDurableProvisioning": "", - "perAgentSettingsOverrideProjectDefaultsEachCategory": "" + "perAgentSettingsOverrideProjectDefaultsEachCategory": "Project defaults apply to permanent agents, ephemeral task workers, and fallback executor workers unless a per-agent override is set. Exact tool rules compose with the legacy ephemeral create-task toggle." }, "appearance": { "hideAISessionNotificationBanners": "", diff --git a/packages/i18n/locales/zh-CN/app.json b/packages/i18n/locales/zh-CN/app.json index ff5cab35b3..8af107f3f9 100644 --- a/packages/i18n/locales/zh-CN/app.json +++ b/packages/i18n/locales/zh-CN/app.json @@ -1116,7 +1116,15 @@ "weekly": "每周", "workingOn": "正在处理:", "zoomIn": "放大", - "zoomOut": "缩小" + "zoomOut": "缩小", + "capabilityGrantsTitle": "Capability grants", + "capabilityGrantsDescription": "Explicit grants add to this agent's role defaults and apply to both permanent and ephemeral agents.", + "roleDefaultGrant": "Role default grant", + "explicitGrant": "Explicit grant", + "notGranted": "Not granted", + "toggleCapabilityGrant": "Toggle explicit grant for {{permission}}", + "capabilityPermissionsUpdated": "Capability grants updated", + "capabilityPermissionsFailed": "Failed to update capability grants: {{error}}" }, "app": { "backendError": { @@ -5632,7 +5640,7 @@ "agentPermissions": "", "agentProvisioningApprovals": "", "configureProjectLevelApprovalBehaviorForDurableProvisioning": "", - "perAgentSettingsOverrideProjectDefaultsEachCategory": "" + "perAgentSettingsOverrideProjectDefaultsEachCategory": "Project defaults apply to permanent agents, ephemeral task workers, and fallback executor workers unless a per-agent override is set. Exact tool rules compose with the legacy ephemeral create-task toggle." }, "appearance": { "hideAISessionNotificationBanners": "", diff --git a/packages/i18n/locales/zh-TW/app.json b/packages/i18n/locales/zh-TW/app.json index 53f4f6dbe8..0fae8aa271 100644 --- a/packages/i18n/locales/zh-TW/app.json +++ b/packages/i18n/locales/zh-TW/app.json @@ -1116,7 +1116,15 @@ "weekly": "每週", "workingOn": "正在處理:", "zoomIn": "放大", - "zoomOut": "縮小" + "zoomOut": "縮小", + "capabilityGrantsTitle": "Capability grants", + "capabilityGrantsDescription": "Explicit grants add to this agent's role defaults and apply to both permanent and ephemeral agents.", + "roleDefaultGrant": "Role default grant", + "explicitGrant": "Explicit grant", + "notGranted": "Not granted", + "toggleCapabilityGrant": "Toggle explicit grant for {{permission}}", + "capabilityPermissionsUpdated": "Capability grants updated", + "capabilityPermissionsFailed": "Failed to update capability grants: {{error}}" }, "app": { "backendError": { @@ -5632,7 +5640,7 @@ "agentPermissions": "", "agentProvisioningApprovals": "", "configureProjectLevelApprovalBehaviorForDurableProvisioning": "", - "perAgentSettingsOverrideProjectDefaultsEachCategory": "" + "perAgentSettingsOverrideProjectDefaultsEachCategory": "Project defaults apply to permanent agents, ephemeral task workers, and fallback executor workers unless a per-agent override is set. Exact tool rules compose with the legacy ephemeral create-task toggle." }, "appearance": { "hideAISessionNotificationBanners": "",