FN-7132: classify task creation as a gated mutation

Classify fn_task_create as a task-agent mutation so permanent-agent policies can require approval or block task creation.

- Move fn_task_create out of the permanent-agent read-only tool set and into the shared task mutation classification.
- Add parity tests covering permanent-agent and action-gate decisions for fn_task_create.
- Preserve delegate and GitHub import tools as permanent-agent read-only coordination tools while documenting the distinction.
- Add a patch changeset for the published Fusion package.

Files changed:
 .../fn-7132-task-create-mutation-classification.md |  7 +++
 docs/agents.md                                     |  6 +--
 .../src/__tests__/gating-classifications.test.ts   | 58 +++++++++++++++++++++-
 .../src/__tests__/permanent-agent-gating.test.ts   |  3 +-
 packages/engine/src/gating-classifications.ts      | 10 ++--
 5 files changed, 75 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-7132

Fusion-Task-Lineage: 6e32ed9d-f0fa-46f2-a749-c5dbe863fb1f

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-27 12:44:50 -07:00
parent 3d26d4e74b
commit c3c4216f76
5 changed files with 75 additions and 9 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Permanent agents now obey approval/block policy when creating tasks.
category: fix
dev: Removed fn_task_create from READONLY_FN_TOOLS and classified it as task_agent_mutation in the permanent-agent gate (packages/engine/src/gating-classifications.ts); action-gate classification unchanged. fn_delegate_task and GitHub import tools intentionally left permanent-readonly.

View File

@@ -150,9 +150,9 @@ The engine classifies tool calls by behavior (not namespace alone):
- `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_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)
- `task_agent_mutation`: task/agent/workflow mutation tools (for example `fn_update_agent_config`, `fn_task_pause`, `fn_spawn_agent`, `fn_task_create`, `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-only task coordination tools like `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.
- `none`: positively recognized read-only tools (`read`, `grep`, `find`, `ls`, list/show/get-style `fn_*` tools, plus permanent-agent coordination helpers like `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.
`bash` git-write heuristic in v1:
@@ -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, logs, documents, messaging, evaluations, identity reflection, memory bookkeeping, and read-only discovery) are exempt by design and always allowed so permanent-agent heartbeats can complete. Task creation/delegation/import are governed as `task_agent_mutation` in action-gate evaluation because they mutate the task board, while the permanent-agent classifier still treats them as positively recognized `none` coordination primitives. Task field/status mutation via `fn_task_update` is also governed as `task_agent_mutation`.
- Internal Fusion runtime coordination tools (heartbeat completion, logs, documents, messaging, evaluations, identity reflection, memory bookkeeping, and read-only discovery) are exempt by design and always allowed so permanent-agent heartbeats can complete. `fn_task_create` is governed as `task_agent_mutation` in both action-gate and permanent-agent evaluation because it creates task rows; delegation/import tools remain governed in action-gate evaluation while the permanent-agent classifier still treats them as positively recognized `none` coordination primitives. Task field/status mutation via `fn_task_update` is also 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.

View File

@@ -7,6 +7,7 @@ import {
COORDINATION_EXEMPT_TOOLS,
FILE_WRITE_DELETE_FN_TOOLS,
NETWORK_API_TOOLS,
PERMANENT_AGENT_TASK_MUTATION_TOOLS,
READONLY_FN_TOOLS,
TASK_AGENT_MUTATION_TOOLS,
classifyGitCommand,
@@ -94,12 +95,23 @@ const gitCases = [
] as const;
const ACTION_MUTATION_PERMANENT_READONLY_TOOLS = new Set([
"fn_task_create",
"fn_delegate_task",
"fn_task_import_github",
"fn_task_import_github_issue",
]);
const policyMatrix = [
[unrestrictedPolicy, "allow"],
[approvalRequiredPolicy, "require-approval"],
[blockedPolicy, "block"],
] as const;
const permanentReadonlySiblingTaskCreationTools = [
"fn_delegate_task",
"fn_task_import_github",
"fn_task_import_github_issue",
] as const;
describe("gating-classifications parity", () => {
it("locks coordination exempt membership", () => {
expect([...COORDINATION_EXEMPT_TOOLS].sort()).toMatchInlineSnapshot(`
@@ -164,6 +176,50 @@ describe("gating-classifications parity", () => {
});
});
it("governs fn_task_create as task_agent_mutation in both gate paths", () => {
expect(READONLY_FN_TOOLS.has("fn_task_create")).toBe(false);
expect(TASK_AGENT_MUTATION_TOOLS.has("fn_task_create")).toBe(true);
expect(ACTION_GATE_TASK_AGENT_MANAGEMENT_TOOLS.has("fn_task_create")).toBe(true);
expect(PERMANENT_AGENT_TASK_MUTATION_TOOLS.has("fn_task_create")).toBe(true);
expect(classifyPermanentAgentToolCall("fn_task_create")).toEqual({
category: "task_agent_mutation",
recognized: true,
});
for (const [permissionPolicy, disposition] of policyMatrix) {
expect(resolvePermanentAgentToolDecision({
toolName: "fn_task_create",
args: {},
gating: { permissionPolicy },
})).toMatchObject({
category: "task_agent_mutation",
disposition,
recognized: true,
});
expect(evaluateAgentActionGate({
agentId: "a1",
toolName: "fn_task_create",
args: {},
permissionPolicy,
})).toMatchObject({
category: "task_agent_mutation",
disposition,
});
}
});
it.each(permanentReadonlySiblingTaskCreationTools)("keeps sibling task creation tool %s permanent-readonly", (toolName) => {
expect(READONLY_FN_TOOLS.has(toolName)).toBe(true);
expect(ACTION_GATE_TASK_AGENT_MANAGEMENT_TOOLS.has(toolName)).toBe(true);
expect(PERMANENT_AGENT_TASK_MUTATION_TOOLS.has(toolName)).toBe(false);
expect(classifyPermanentAgentToolCall(toolName)).toEqual({ category: "none", recognized: true });
expect(resolvePermanentAgentToolDecision({ toolName, args: {}, gating: { permissionPolicy: blockedPolicy } })).toMatchObject({
category: "none",
disposition: "allow",
recognized: true,
});
});
it("includes goal retrieval tools on readonly path only", () => {
expect(READONLY_FN_TOOLS.has("fn_goal_list")).toBe(true);
expect(READONLY_FN_TOOLS.has("fn_goal_show")).toBe(true);

View File

@@ -20,7 +20,6 @@ const FN_7111_GOVERNED_TOOLS = [
const FN_3548_COORDINATION_TOOLS = [
"fn_heartbeat_done",
"fn_task_create",
"fn_task_log",
"fn_task_document_write",
"fn_task_document_read",
@@ -53,7 +52,7 @@ describe("permanent-agent-gating", () => {
});
it("classifies shared fn tools by behavior", () => {
expect(classifyPermanentAgentToolCall("fn_task_create").category).toBe("none");
expect(classifyPermanentAgentToolCall("fn_task_create").category).toBe("task_agent_mutation");
expect(classifyPermanentAgentToolCall("fn_delegate_task").category).toBe("none");
expect(classifyPermanentAgentToolCall("fn_update_agent_config").category).toBe("task_agent_mutation");
expect(classifyPermanentAgentToolCall("fn_task_import_github").category).toBe("none");

View File

@@ -36,13 +36,17 @@ export const COMMAND_EXECUTION_FN_TOOLS: ReadonlySet<string> = new Set([
/**
* FNXC:ToolGovernance 2026-06-27-11:24:
* FN-7126 classifies task creation/delegation/import as task_agent_mutation in the action gate because they mutate the board and must honor operator approval/block policy. Keep the same tools in READONLY_FN_TOOLS for the permanent-agent gate, where they remain recognized `none` coordination primitives.
* FN-7126 classifies task creation/delegation/import as task_agent_mutation in the action gate because they mutate the board and must honor operator approval/block policy. In the permanent-agent gate, delegate/import tools remain recognized `none` coordination primitives while fn_task_create is governed separately as a board mutation.
*
* FNXC:ToolGovernance 2026-06-27-12:31:
* FN-7132 requires every live board-creation tool to avoid read-only classification in all gate paths. Classify fn_task_create as task_agent_mutation for both action and permanent agents so locked-down policies can block task-row creation; keep delegate and GitHub import tools in the action-only bucket until their permanent-agent coordination semantics are intentionally revisited.
*
* FNXC:ToolGovernance 2026-06-27-16:51:
* Identity reflection stays out of this action-gate mutation-only list because it is heartbeat-critical coordination, not a task-board mutation. Keep it in COORDINATION_EXEMPT_TOOLS and READONLY_FN_TOOLS so exported mutation sets do not contradict action-gate exemption semantics.
*/
const PERMANENT_AND_ACTION_TASK_AGENT_TOOLS = ["fn_task_create"] as const;
const ACTION_GATE_TASK_AGENT_ONLY_TOOLS = [
"fn_task_create",
...PERMANENT_AND_ACTION_TASK_AGENT_TOOLS,
"fn_delegate_task",
"fn_task_import_github",
"fn_task_import_github_issue",
@@ -92,6 +96,7 @@ export const ACTION_GATE_TASK_AGENT_MANAGEMENT_TOOLS: ReadonlySet<string> = new
export const PERMANENT_AGENT_TASK_MUTATION_TOOLS: ReadonlySet<string> = new Set([
...SHARED_TASK_AGENT_TOOLS,
...PERMANENT_AND_ACTION_TASK_AGENT_TOOLS,
...PERMANENT_TASK_AGENT_ONLY_TOOLS,
]);
@@ -122,7 +127,6 @@ export const READONLY_FN_TOOLS: ReadonlySet<string> = new Set([
"fn_task_search",
// FNXC:ToolGovernance 2026-06-27-00:00: `fn_task_get` is a deprecated recognition-only alias. It is no longer registered as a live tool, but historical/in-flight calls must still classify as read-only instead of falling through to unknown-tool handling.
"fn_task_get",
"fn_task_create",
"fn_task_document_write",
"fn_task_document_read",
"fn_delegate_task",