FN-7111: classify heartbeat tool parity gaps
Classify remaining heartbeat-agent tool gaps so permission gates enforce mutating and network behaviors consistently. - Add shared classifications for workflow mutators, task update/promote/refine, verification/worktree acquisition, and research cancellation. - Apply command-execution classifications in both action-gate and permanent-agent gating paths. - Update operator docs, permission-policy examples, parity tests, and the published package changeset. Files changed: .changeset/FN-7111-tool-classification-parity.md | 7 ++ docs/agents.md | 8 +- packages/core/src/types.ts | 12 ++- .../engine/src/__tests__/agent-action-gate.test.ts | 11 ++- .../src/__tests__/gating-classifications.test.ts | 93 +++++++++++++++++++++- .../src/__tests__/permanent-agent-gating.test.ts | 44 ++++++++++ packages/engine/src/agent-action-gate.ts | 6 ++ packages/engine/src/gating-classifications.ts | 36 ++++++++- packages/engine/src/permanent-agent-gating.ts | 5 ++ 9 files changed, 206 insertions(+), 16 deletions(-) Fusion-Task-Id: FN-7111 Fusion-Task-Lineage: d2f5ee71-c742-4f10-af55-fec380f08f64 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/FN-7111-tool-classification-parity.md
Normal file
7
.changeset/FN-7111-tool-classification-parity.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Mutating agent tools now obey each agent's permission policy instead of always being allowed.
|
||||
category: security
|
||||
dev: Classifies fn_workflow_*, fn_task_update/promote/refine, fn_run_verification, fn_acquire_repo_worktree, and fn_research_cancel in shared gating classifications so both the action gate and permanent-agent gating govern them; closes the unrecognized-tool exempt→allow fall-through. Parity tests lock the decisions.
|
||||
@@ -147,10 +147,10 @@ V1 runtime action categories:
|
||||
The engine classifies tool calls by behavior (not namespace alone):
|
||||
|
||||
- `file_write_delete`: built-in `write` / `edit`, plus direct filesystem attach helpers like `fn_task_attach`; low-risk coordination/registration writes such as `fn_task_document_write` and `fn_artifact_register` are handled by the coordination-exempt/read-only allow-lists below rather than this category
|
||||
- `command_execution`: built-in `bash` when not classified as mutating git
|
||||
- `command_execution`: built-in `bash` when not classified as mutating git, plus fn tools that run bounded subprocess/worktree acquisition flows such as `fn_run_verification` and `fn_acquire_repo_worktree`
|
||||
- `git_write`: mutating git shell commands run via `bash`
|
||||
- `network_api`: external/network-facing tools (for example `fn_research_run`, `fn_research_cancel`, `fn_research_retry`, `fn_web_fetch`)
|
||||
- `task_agent_mutation`: task/agent mutation tools (for example `fn_update_agent_config`, `fn_task_pause`, `fn_spawn_agent`; action-gate task-import/create tools like `fn_task_create`, `fn_delegate_task`, `fn_task_import_github`, and `fn_task_import_github_issue` use this category in action-gate evaluation)
|
||||
- `network_api`: external/network-facing tools (for example `fn_research_run`, `fn_research_cancel`, `fn_web_fetch`, `worktrunk_install`; `fn_research_retry` is permanent-agent network-classified and remains action-gate read-only/exception behavior)
|
||||
- `task_agent_mutation`: task/agent/workflow mutation tools (for example `fn_update_agent_config`, `fn_task_pause`, `fn_spawn_agent`, `fn_task_update`, `fn_task_promote`, `fn_task_refine`, and workflow mutators such as `fn_workflow_create`, `fn_workflow_update`, `fn_workflow_delete`, `fn_workflow_settings`, `fn_workflow_select`; action-gate task-import/create tools like `fn_task_create`, `fn_delegate_task`, `fn_task_import_github`, and `fn_task_import_github_issue` use this category in action-gate evaluation)
|
||||
- Dashboard permission editors now show per-category example tools sourced from `AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES` in `@fusion/core`, plus a read-only exempt-tools panel for coordination/messaging bypass tools.
|
||||
- `none`: positively recognized read-only tools (`read`, `grep`, `find`, `ls`, list/show/get-style `fn_*` tools, plus permanent-agent coordination/task-creation helpers like `fn_task_create`, `fn_delegate_task`, `fn_task_import_github`, and `fn_task_import_github_issue`). Artifact tools mirror `fn_task_document_write` in the shipped allow-lists: `fn_artifact_register`, `fn_artifact_list`, and `fn_artifact_view` are present in `READONLY_FN_TOOLS` and `COORDINATION_EXEMPT_TOOLS`, so registration is treated as coordination/registry publication instead of a broad mutation approval.
|
||||
|
||||
@@ -163,7 +163,7 @@ Unknown/unclassified tool fallback:
|
||||
|
||||
- In permanent-agent sessions, unknown tools default to `require-approval` (fail-safe).
|
||||
- Category `none` only yields `allow` when the tool is positively recognized as read-only.
|
||||
- Internal Fusion runtime coordination tools (heartbeat completion, task/agent coordination, messaging, evaluations, identity reflection, memory bookkeeping) are exempt by design and always allowed so permanent-agent heartbeats can complete.
|
||||
- Internal Fusion runtime coordination tools (heartbeat completion, logs, documents, task creation/delegation, messaging, evaluations, identity reflection, memory bookkeeping) are exempt by design and always allowed so permanent-agent heartbeats can complete. Task field/status mutation via `fn_task_update` is not exempt; it is governed as `task_agent_mutation`.
|
||||
- Operators can reload the in-memory exempt-tool registry at runtime via `POST /api/action-gate/reload` (optional body `{ "tools": string[] }`) to apply exemption-list updates without restarting the engine process.
|
||||
- Canonical tool classification/exemption sets live in `packages/engine/src/gating-classifications.ts` and are shared by both action-gate paths.
|
||||
|
||||
|
||||
@@ -6200,8 +6200,8 @@ export const AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES: Record<
|
||||
> = {
|
||||
git_write: ["git commit", "git push", "git merge", "git branch -d", "git worktree add", "write", "edit"],
|
||||
file_write_delete: ["write", "edit", "fn_task_attach"],
|
||||
command_execution: ["bash (non-git)", "read", "find", "grep", "ls"],
|
||||
network_api: ["fn_research_run (web/research)", "fn_web_fetch", "worktrunk_install"],
|
||||
command_execution: ["bash (non-git)", "fn_run_verification", "fn_acquire_repo_worktree", "read", "find", "grep", "ls"],
|
||||
network_api: ["fn_research_run (web/research)", "fn_research_cancel", "fn_web_fetch", "worktrunk_install"],
|
||||
task_agent_mutation: [
|
||||
"fn_task_create",
|
||||
"fn_delegate_task",
|
||||
@@ -6210,6 +6210,14 @@ export const AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES: Record<
|
||||
"fn_spawn_agent",
|
||||
"fn_update_agent_config",
|
||||
"fn_update_identity",
|
||||
"fn_task_update",
|
||||
"fn_workflow_create",
|
||||
"fn_workflow_update",
|
||||
"fn_workflow_delete",
|
||||
"fn_workflow_settings",
|
||||
"fn_workflow_select",
|
||||
"fn_task_promote",
|
||||
"fn_task_refine",
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -125,9 +125,14 @@ describe("agent-action-gate", () => {
|
||||
expect(evaluateAgentActionGate({ agentId: "a1", toolName: "fn_spawn_agent", args: {}, permissionPolicy: unrestrictedPolicy }).category).toBe("task_agent_mutation");
|
||||
});
|
||||
|
||||
it("keeps routine task bookkeeping tools exempt", () => {
|
||||
expect(evaluateAgentActionGate({ agentId: "a1", toolName: "fn_task_update", args: {}, permissionPolicy: approvalPolicy }).category).toBe("exempt");
|
||||
expect(evaluateAgentActionGate({ agentId: "a1", toolName: "fn_task_update", args: {}, permissionPolicy: approvalPolicy }).disposition).toBe("allow");
|
||||
it("governs fn_task_update as task_agent_mutation under permission policy", () => {
|
||||
const approvalDecision = evaluateAgentActionGate({ agentId: "a1", toolName: "fn_task_update", args: {}, permissionPolicy: approvalPolicy });
|
||||
const blockedDecision = evaluateAgentActionGate({ agentId: "a1", toolName: "fn_task_update", args: {}, permissionPolicy: lockedDownPolicy });
|
||||
|
||||
expect(approvalDecision.category).toBe("task_agent_mutation");
|
||||
expect(approvalDecision.disposition).toBe("require-approval");
|
||||
expect(blockedDecision.category).toBe("task_agent_mutation");
|
||||
expect(blockedDecision.disposition).toBe("block");
|
||||
});
|
||||
|
||||
it.each(FN_3548_COORDINATION_TOOLS)("always allows newly exempt internal tool %s under locked-down policies", (toolName) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { evaluateAgentActionGate } from "../agent-action-gate.js";
|
||||
import {
|
||||
ACTION_GATE_NETWORK_API_TOOLS,
|
||||
ACTION_GATE_TASK_AGENT_MANAGEMENT_TOOLS,
|
||||
COMMAND_EXECUTION_FN_TOOLS,
|
||||
COORDINATION_EXEMPT_TOOLS,
|
||||
FILE_WRITE_DELETE_FN_TOOLS,
|
||||
NETWORK_API_TOOLS,
|
||||
@@ -14,6 +15,28 @@ import {
|
||||
import { classifyPermanentAgentToolCall, resolvePermanentAgentToolDecision } from "../permanent-agent-gating.js";
|
||||
import type { AgentPermissionPolicy } from "@fusion/core";
|
||||
|
||||
const unrestrictedPolicy: AgentPermissionPolicy = {
|
||||
presetId: "unrestricted",
|
||||
rules: {
|
||||
git_write: "allow",
|
||||
file_write_delete: "allow",
|
||||
command_execution: "allow",
|
||||
network_api: "allow",
|
||||
task_agent_mutation: "allow",
|
||||
},
|
||||
};
|
||||
|
||||
const approvalRequiredPolicy: AgentPermissionPolicy = {
|
||||
presetId: "approval-required",
|
||||
rules: {
|
||||
git_write: "require-approval",
|
||||
file_write_delete: "require-approval",
|
||||
command_execution: "require-approval",
|
||||
network_api: "require-approval",
|
||||
task_agent_mutation: "require-approval",
|
||||
},
|
||||
};
|
||||
|
||||
const blockedPolicy: AgentPermissionPolicy = {
|
||||
presetId: "locked-down",
|
||||
rules: {
|
||||
@@ -25,6 +48,20 @@ const blockedPolicy: AgentPermissionPolicy = {
|
||||
},
|
||||
};
|
||||
|
||||
const FN_7111_GOVERNED_TOOLS = [
|
||||
["fn_workflow_select", "task_agent_mutation"],
|
||||
["fn_workflow_create", "task_agent_mutation"],
|
||||
["fn_workflow_update", "task_agent_mutation"],
|
||||
["fn_workflow_delete", "task_agent_mutation"],
|
||||
["fn_workflow_settings", "task_agent_mutation"],
|
||||
["fn_task_update", "task_agent_mutation"],
|
||||
["fn_task_promote", "task_agent_mutation"],
|
||||
["fn_task_refine", "task_agent_mutation"],
|
||||
["fn_run_verification", "command_execution"],
|
||||
["fn_acquire_repo_worktree", "command_execution"],
|
||||
["fn_research_cancel", "network_api"],
|
||||
] as const;
|
||||
|
||||
const gitCases = [
|
||||
["git status", false, "git status"],
|
||||
["git diff", false, "git diff"],
|
||||
@@ -89,8 +126,8 @@ describe("gating-classifications parity", () => {
|
||||
"fn_task_document_write",
|
||||
"fn_task_done",
|
||||
"fn_task_log",
|
||||
"fn_task_update",
|
||||
"fn_update_identity",
|
||||
"fn_workflow_list",
|
||||
"grep",
|
||||
"ls",
|
||||
"read",
|
||||
@@ -132,6 +169,47 @@ describe("gating-classifications parity", () => {
|
||||
expect((COORDINATION_EXEMPT_TOOLS as readonly string[]).includes("worktrunk_install")).toBe(false);
|
||||
});
|
||||
|
||||
it.each(FN_7111_GOVERNED_TOOLS)("governs FN-7111 tool %s as %s across action policies", (toolName, category) => {
|
||||
expect(evaluateAgentActionGate({ agentId: "a1", toolName, args: {}, permissionPolicy: unrestrictedPolicy })).toMatchObject({
|
||||
category,
|
||||
disposition: "allow",
|
||||
});
|
||||
expect(evaluateAgentActionGate({ agentId: "a1", toolName, args: {}, permissionPolicy: approvalRequiredPolicy })).toMatchObject({
|
||||
category,
|
||||
disposition: "require-approval",
|
||||
});
|
||||
expect(evaluateAgentActionGate({ agentId: "a1", toolName, args: {}, permissionPolicy: blockedPolicy })).toMatchObject({
|
||||
category,
|
||||
disposition: "block",
|
||||
});
|
||||
});
|
||||
|
||||
it.each(FN_7111_GOVERNED_TOOLS)("blocks FN-7111 mutating tool %s under locked-down policy in both gate paths", (toolName, category) => {
|
||||
const action = evaluateAgentActionGate({ agentId: "a1", toolName, args: {}, permissionPolicy: blockedPolicy });
|
||||
const permanent = resolvePermanentAgentToolDecision({
|
||||
toolName,
|
||||
args: {},
|
||||
gating: { permissionPolicy: blockedPolicy },
|
||||
});
|
||||
|
||||
expect(action).toMatchObject({ category, disposition: "block" });
|
||||
expect(permanent).toMatchObject({ category, disposition: "block", recognized: true });
|
||||
});
|
||||
|
||||
it("recognizes fn_workflow_list as read-only coordination instead of an unknown fallback", () => {
|
||||
const permanent = classifyPermanentAgentToolCall("fn_workflow_list");
|
||||
const action = evaluateAgentActionGate({
|
||||
agentId: "a1",
|
||||
toolName: "fn_workflow_list",
|
||||
args: {},
|
||||
permissionPolicy: blockedPolicy,
|
||||
});
|
||||
|
||||
expect(permanent).toEqual({ category: "none", recognized: true });
|
||||
expect(action).toMatchObject({ category: "exempt", disposition: "allow" });
|
||||
expect((COORDINATION_EXEMPT_TOOLS as readonly string[]).includes("fn_workflow_list")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps fn_* category equivalence mappings across gates", () => {
|
||||
const fnTools = new Set<string>();
|
||||
for (const source of [
|
||||
@@ -140,6 +218,7 @@ describe("gating-classifications parity", () => {
|
||||
ACTION_GATE_NETWORK_API_TOOLS,
|
||||
FILE_WRITE_DELETE_FN_TOOLS,
|
||||
NETWORK_API_TOOLS,
|
||||
COMMAND_EXECUTION_FN_TOOLS,
|
||||
]) {
|
||||
for (const toolName of source) {
|
||||
if (toolName.startsWith("fn_")) fnTools.add(toolName);
|
||||
@@ -161,7 +240,9 @@ describe("gating-classifications parity", () => {
|
||||
? "network"
|
||||
: action.category === "file_write_delete"
|
||||
? "file-write"
|
||||
: "readonly";
|
||||
: action.category === "command_execution"
|
||||
? "command"
|
||||
: "readonly";
|
||||
|
||||
const permanentKind = permanent.category === "task_agent_mutation"
|
||||
? "mutating"
|
||||
@@ -169,8 +250,14 @@ describe("gating-classifications parity", () => {
|
||||
? "network"
|
||||
: permanent.category === "file_write_delete"
|
||||
? "file-write"
|
||||
: "readonly";
|
||||
: permanent.category === "command_execution"
|
||||
? "command"
|
||||
: "readonly";
|
||||
|
||||
if (COMMAND_EXECUTION_FN_TOOLS.has(toolName)) {
|
||||
expect({ toolName, actionKind, permanentKind }).toEqual({ toolName, actionKind: "command", permanentKind: "command" });
|
||||
continue;
|
||||
}
|
||||
if (FILE_WRITE_DELETE_FN_TOOLS.has(toolName)) {
|
||||
expect({ toolName, actionKind, permanentKind }).toEqual({ toolName, actionKind: "readonly", permanentKind: "file-write" });
|
||||
continue;
|
||||
|
||||
@@ -4,6 +4,20 @@ import {
|
||||
resolvePermanentAgentToolDecision,
|
||||
} from "../permanent-agent-gating.js";
|
||||
|
||||
const FN_7111_GOVERNED_TOOLS = [
|
||||
["fn_workflow_select", "task_agent_mutation"],
|
||||
["fn_workflow_create", "task_agent_mutation"],
|
||||
["fn_workflow_update", "task_agent_mutation"],
|
||||
["fn_workflow_delete", "task_agent_mutation"],
|
||||
["fn_workflow_settings", "task_agent_mutation"],
|
||||
["fn_task_update", "task_agent_mutation"],
|
||||
["fn_task_promote", "task_agent_mutation"],
|
||||
["fn_task_refine", "task_agent_mutation"],
|
||||
["fn_run_verification", "command_execution"],
|
||||
["fn_acquire_repo_worktree", "command_execution"],
|
||||
["fn_research_cancel", "network_api"],
|
||||
] as const;
|
||||
|
||||
const FN_3548_COORDINATION_TOOLS = [
|
||||
"fn_heartbeat_done",
|
||||
"fn_task_create",
|
||||
@@ -51,7 +65,9 @@ describe("permanent-agent-gating", () => {
|
||||
expect(classifyPermanentAgentToolCall("fn_artifact_view").category).toBe("none");
|
||||
expect(classifyPermanentAgentToolCall("fn_memory_append").category).toBe("none");
|
||||
expect(classifyPermanentAgentToolCall("fn_research_run").category).toBe("network_api");
|
||||
expect(classifyPermanentAgentToolCall("fn_research_cancel").category).toBe("network_api");
|
||||
expect(classifyPermanentAgentToolCall("worktrunk_install").category).toBe("network_api");
|
||||
expect(classifyPermanentAgentToolCall("fn_task_update").category).toBe("task_agent_mutation");
|
||||
expect(classifyPermanentAgentToolCall("fn_task_show").category).toBe("none");
|
||||
expect(classifyPermanentAgentToolCall("fn_research_get").category).toBe("none");
|
||||
expect(classifyPermanentAgentToolCall("fn_heartbeat_done")).toEqual({ category: "none", recognized: true });
|
||||
@@ -59,6 +75,34 @@ describe("permanent-agent-gating", () => {
|
||||
expect(classifyPermanentAgentToolCall("fn_read_messages")).toEqual({ category: "none", recognized: true });
|
||||
});
|
||||
|
||||
it.each(FN_7111_GOVERNED_TOOLS)("classifies FN-7111 governed tool %s as recognized %s", (toolName, category) => {
|
||||
expect(classifyPermanentAgentToolCall(toolName)).toEqual({ category, recognized: true });
|
||||
});
|
||||
|
||||
it.each(FN_7111_GOVERNED_TOOLS)("blocks FN-7111 governed tool %s under locked-down policy", (toolName, category) => {
|
||||
const decision = resolvePermanentAgentToolDecision({
|
||||
toolName,
|
||||
gating: {
|
||||
permissionPolicy: {
|
||||
presetId: "locked-down",
|
||||
rules: {
|
||||
git_write: "block",
|
||||
file_write_delete: "block",
|
||||
command_execution: "block",
|
||||
network_api: "block",
|
||||
task_agent_mutation: "block",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(decision).toMatchObject({ category, recognized: true, disposition: "block" });
|
||||
});
|
||||
|
||||
it("classifies fn_workflow_list as recognized readonly", () => {
|
||||
expect(classifyPermanentAgentToolCall("fn_workflow_list")).toEqual({ category: "none", recognized: true });
|
||||
});
|
||||
|
||||
it("uses only canonical action-category names", () => {
|
||||
const categories = [
|
||||
classifyPermanentAgentToolCall("bash", { command: "git commit -m x" }).category,
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
import {
|
||||
ACTION_GATE_NETWORK_API_TOOLS,
|
||||
ACTION_GATE_TASK_AGENT_MANAGEMENT_TOOLS,
|
||||
COMMAND_EXECUTION_FN_TOOLS,
|
||||
COORDINATION_EXEMPT_TOOLS,
|
||||
READONLY_BUILTIN_TOOLS,
|
||||
classifyGitCommand,
|
||||
@@ -85,6 +86,7 @@ export function getExemptToolNames(): string[] {
|
||||
|
||||
const TASK_AGENT_MANAGEMENT_TOOLS = ACTION_GATE_TASK_AGENT_MANAGEMENT_TOOLS;
|
||||
const NETWORK_API_TOOLS = ACTION_GATE_NETWORK_API_TOOLS;
|
||||
const COMMAND_EXECUTION_TOOLS = COMMAND_EXECUTION_FN_TOOLS;
|
||||
const READONLY_DISCOVERY_TOOLS = READONLY_BUILTIN_TOOLS;
|
||||
|
||||
function normalizeArgs(args: unknown): Record<string, unknown> {
|
||||
@@ -159,6 +161,10 @@ export function evaluateAgentActionGate(params: {
|
||||
category = "task_agent_mutation";
|
||||
operation = params.toolName;
|
||||
resourceType = params.toolName.includes("agent") || params.toolName.includes("spawn") ? "agent" : "task";
|
||||
} else if (COMMAND_EXECUTION_TOOLS.has(params.toolName)) {
|
||||
category = "command_execution";
|
||||
operation = params.toolName;
|
||||
resourceType = "command";
|
||||
} else if (NETWORK_API_TOOLS.has(params.toolName)) {
|
||||
category = "network_api";
|
||||
operation = params.toolName;
|
||||
|
||||
@@ -4,9 +4,36 @@
|
||||
export const READONLY_BUILTIN_TOOLS: ReadonlySet<string> = new Set(["read", "find", "grep", "ls"]);
|
||||
export const FILE_WRITE_BUILTIN_TOOLS: ReadonlySet<string> = new Set(["write", "edit"]);
|
||||
|
||||
const SHARED_TASK_AGENT_TOOLS = ["fn_task_add_dep", "fn_spawn_agent", "fn_update_agent_config", "fn_agent_create", "fn_agent_delete"] as const;
|
||||
/**
|
||||
* FNXC:ToolGovernance 2026-06-27-12:05:
|
||||
* Workflow edits, task status/custom-field updates, held-task promotion, and refinement creation are mutating heartbeat tools. Keep them in the shared task-agent bucket so action-gate and permanent-agent policy decisions cannot drift or fall through to silent exemption.
|
||||
*/
|
||||
const SHARED_TASK_AGENT_TOOLS = [
|
||||
"fn_task_add_dep",
|
||||
"fn_task_update",
|
||||
"fn_spawn_agent",
|
||||
"fn_update_agent_config",
|
||||
"fn_agent_create",
|
||||
"fn_agent_delete",
|
||||
"fn_workflow_select",
|
||||
"fn_workflow_create",
|
||||
"fn_workflow_update",
|
||||
"fn_workflow_delete",
|
||||
"fn_workflow_settings",
|
||||
"fn_task_promote",
|
||||
"fn_task_refine",
|
||||
] as const;
|
||||
const PROVISIONING_TOOLS = ["fn_agent_create", "fn_agent_delete"] as const;
|
||||
|
||||
/**
|
||||
* FNXC:ToolGovernance 2026-06-27-12:00:
|
||||
* Newly exposed mutating heartbeat tools must be positively classified before agents receive them, otherwise the action gate's unrecognized-tool fallback silently allows them. Verification and workspace acquisition execute subprocess/git-worktree work, so both gating paths use command_execution instead of a coordination exemption.
|
||||
*/
|
||||
export const COMMAND_EXECUTION_FN_TOOLS: ReadonlySet<string> = new Set([
|
||||
"fn_run_verification",
|
||||
"fn_acquire_repo_worktree",
|
||||
]);
|
||||
|
||||
const ACTION_GATE_TASK_AGENT_ONLY_TOOLS = [
|
||||
"fn_task_create",
|
||||
"fn_delegate_task",
|
||||
@@ -22,7 +49,6 @@ const PERMANENT_TASK_AGENT_ONLY_TOOLS = [
|
||||
"fn_task_unpause",
|
||||
"fn_task_retry",
|
||||
"fn_task_duplicate",
|
||||
"fn_task_refine",
|
||||
"fn_task_archive",
|
||||
"fn_task_unarchive",
|
||||
"fn_task_delete",
|
||||
@@ -75,6 +101,7 @@ export const NETWORK_API_TOOLS: ReadonlySet<string> = new Set([
|
||||
|
||||
export const ACTION_GATE_NETWORK_API_TOOLS: ReadonlySet<string> = new Set([
|
||||
"fn_research_run",
|
||||
"fn_research_cancel",
|
||||
"fn_web_fetch", // FN-4603: honor network_api approval policy for web fetches.
|
||||
"worktrunk_install", // FN-4624: gate binary auto-install under network_api policy.
|
||||
]);
|
||||
@@ -99,6 +126,8 @@ export const READONLY_FN_TOOLS: ReadonlySet<string> = new Set([
|
||||
"fn_insight_run_show",
|
||||
"fn_goal_list",
|
||||
"fn_goal_show",
|
||||
// FNXC:ToolGovernance 2026-06-27-12:06: Workflow listing is read-only discovery and must stay positively recognized instead of relying on an unknown-tool fallback.
|
||||
"fn_workflow_list",
|
||||
"fn_mission_list",
|
||||
"fn_mission_show",
|
||||
"fn_list_agents",
|
||||
@@ -107,7 +136,6 @@ export const READONLY_FN_TOOLS: ReadonlySet<string> = new Set([
|
||||
"fn_skills_search",
|
||||
"fn_memory_search",
|
||||
"fn_memory_get",
|
||||
"fn_task_update",
|
||||
"fn_task_log",
|
||||
"fn_task_done",
|
||||
"fn_heartbeat_done",
|
||||
@@ -125,7 +153,6 @@ export const COORDINATION_EXEMPT_TOOLS = [
|
||||
"find",
|
||||
"grep",
|
||||
"ls",
|
||||
"fn_task_update",
|
||||
"fn_task_log",
|
||||
"fn_task_done",
|
||||
/* FNXC:ArtifactRegistry 2026-06-21-00:00: Artifact registration mutates persisted registry state, but it is a low-risk coordination action classified like fn_task_document_write so permanent agents can publish discoverable deliverables without broad mutation approval. */
|
||||
@@ -145,6 +172,7 @@ export const COORDINATION_EXEMPT_TOOLS = [
|
||||
"fn_list_agents",
|
||||
"fn_agent_show",
|
||||
"fn_agent_org_chart",
|
||||
"fn_workflow_list",
|
||||
"fn_send_message",
|
||||
"fn_post_room_message",
|
||||
"fn_memory_append",
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
PermanentAgentSensitiveActionCategory,
|
||||
} from "@fusion/core";
|
||||
import {
|
||||
COMMAND_EXECUTION_FN_TOOLS,
|
||||
FILE_WRITE_BUILTIN_TOOLS,
|
||||
FILE_WRITE_DELETE_FN_TOOLS,
|
||||
NETWORK_API_TOOLS,
|
||||
@@ -31,6 +32,7 @@ const FILE_WRITE_TOOLS = FILE_WRITE_BUILTIN_TOOLS;
|
||||
// category "none" so restrictive permanent-agent policies cannot deadlock heartbeats.
|
||||
const TASK_AGENT_MUTATION_TOOLS = PERMANENT_AGENT_TASK_MUTATION_TOOLS;
|
||||
const FILE_WRITE_DELETE_TOOLS = FILE_WRITE_DELETE_FN_TOOLS;
|
||||
const COMMAND_EXECUTION_TOOLS = COMMAND_EXECUTION_FN_TOOLS;
|
||||
|
||||
function normalizeArgs(args: unknown): Record<string, unknown> {
|
||||
return args && typeof args === "object" ? (args as Record<string, unknown>) : {};
|
||||
@@ -62,6 +64,9 @@ export function classifyPermanentAgentToolCall(
|
||||
if (FILE_WRITE_DELETE_TOOLS.has(toolName)) {
|
||||
return { category: "file_write_delete", recognized: true };
|
||||
}
|
||||
if (COMMAND_EXECUTION_TOOLS.has(toolName)) {
|
||||
return { category: "command_execution", recognized: true };
|
||||
}
|
||||
if (NETWORK_API_TOOLS.has(toolName)) {
|
||||
return { category: "network_api", recognized: true };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user