feat(FN-3976): reclassify github import action gating

Completes FN-3976 by reclassifying GitHub import action gating in the engine's `gating-classifications.ts`, updating documentation in `docs/agents.md` accordingly, and adding test coverage across four gating test suites.

Fusion-Task-Id: FN-3976
This commit is contained in:
Fusion
2026-05-11 00:56:51 -07:00
committed by gsxdsm
parent ef3281bc9d
commit c02aadec19
7 changed files with 38 additions and 5 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Reclassify `fn_task_import_github` and `fn_task_import_github_issue` into action-gate task mutation tooling, while keeping permanent-agent classification aligned with task-creation coordination behavior.

View File

@@ -74,8 +74,8 @@ The engine classifies tool calls by behavior (not namespace alone):
- `command_execution`: built-in `bash` when not classified as mutating git
- `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`)
- `task_agent_mutation`: task/agent mutation tools (for example `fn_task_create`, `fn_delegate_task`, `fn_update_agent_config`, `fn_update_identity`)
- `none`: positively recognized read-only tools (`read`, `grep`, `find`, `ls`, list/show/get-style `fn_*` tools)
- `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)
- `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`)
`bash` git-write heuristic in v1:

View File

@@ -116,6 +116,8 @@ describe("agent-action-gate", () => {
expect(evaluateAgentActionGate({ agentId: "a1", toolName: "fn_task_add_dep", args: {}, permissionPolicy: unrestrictedPolicy }).category).toBe("task_agent_mutation");
expect(evaluateAgentActionGate({ agentId: "a1", toolName: "fn_delegate_task", args: {}, permissionPolicy: unrestrictedPolicy }).category).toBe("exempt");
expect(evaluateAgentActionGate({ agentId: "a1", toolName: "fn_update_agent_config", args: {}, permissionPolicy: unrestrictedPolicy }).category).toBe("task_agent_mutation");
expect(evaluateAgentActionGate({ agentId: "a1", toolName: "fn_task_import_github", args: {}, permissionPolicy: unrestrictedPolicy }).category).toBe("task_agent_mutation");
expect(evaluateAgentActionGate({ agentId: "a1", toolName: "fn_task_import_github_issue", args: {}, permissionPolicy: unrestrictedPolicy }).category).toBe("task_agent_mutation");
expect(evaluateAgentActionGate({ agentId: "a1", toolName: "fn_update_identity", args: {}, permissionPolicy: unrestrictedPolicy }).category).toBe("exempt");
expect(evaluateAgentActionGate({ agentId: "a1", toolName: "fn_spawn_agent", args: {}, permissionPolicy: unrestrictedPolicy }).category).toBe("task_agent_mutation");
});

View File

@@ -17,4 +17,13 @@ describe("gating classifications provisioning split", () => {
expect(TASK_AGENT_MUTATION_TOOLS.has("fn_agent_create")).toBe(true);
expect(TASK_AGENT_MUTATION_TOOLS.has("fn_agent_delete")).toBe(true);
});
it("classifies github task imports as action-gated task mutations only", () => {
expect(ACTION_GATE_TASK_AGENT_MANAGEMENT_TOOLS.has("fn_task_import_github")).toBe(true);
expect(ACTION_GATE_TASK_AGENT_MANAGEMENT_TOOLS.has("fn_task_import_github_issue")).toBe(true);
expect(PERMANENT_AGENT_TASK_MUTATION_TOOLS.has("fn_task_import_github")).toBe(false);
expect(PERMANENT_AGENT_TASK_MUTATION_TOOLS.has("fn_task_import_github_issue")).toBe(false);
expect(TASK_AGENT_MUTATION_TOOLS.has("fn_task_import_github")).toBe(true);
expect(TASK_AGENT_MUTATION_TOOLS.has("fn_task_import_github_issue")).toBe(true);
});
});

View File

@@ -56,6 +56,11 @@ const gitCases = [
["echo hi\ngit checkout -b t", true, "git checkout -b"],
] as const;
const ACTION_MUTATION_PERMANENT_READONLY_TOOLS = new Set([
"fn_task_import_github",
"fn_task_import_github_issue",
]);
describe("gating-classifications parity", () => {
it("locks coordination exempt membership", () => {
expect([...COORDINATION_EXEMPT_TOOLS].sort()).toMatchInlineSnapshot(`
@@ -151,6 +156,10 @@ describe("gating-classifications parity", () => {
expect({ toolName, actionKind, permanentKind }).toEqual({ toolName, actionKind: "readonly", permanentKind: "mutating" });
continue;
}
if (ACTION_MUTATION_PERMANENT_READONLY_TOOLS.has(toolName)) {
expect({ toolName, actionKind, permanentKind }).toEqual({ toolName, actionKind: "mutating", permanentKind: "readonly" });
continue;
}
expect({ toolName, actionKind, permanentKind }).toEqual({ toolName, actionKind: permanentKind, permanentKind });
}

View File

@@ -38,6 +38,8 @@ describe("permanent-agent-gating", () => {
expect(classifyPermanentAgentToolCall("fn_task_create").category).toBe("none");
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");
expect(classifyPermanentAgentToolCall("fn_task_import_github_issue").category).toBe("none");
expect(classifyPermanentAgentToolCall("fn_update_identity").category).toBe("none");
expect(classifyPermanentAgentToolCall("fn_task_document_write").category).toBe("none");
expect(classifyPermanentAgentToolCall("fn_memory_append").category).toBe("none");

View File

@@ -7,7 +7,13 @@ export const FILE_WRITE_BUILTIN_TOOLS: ReadonlySet<string> = new Set(["write", "
const SHARED_TASK_AGENT_TOOLS = ["fn_task_add_dep", "fn_spawn_agent", "fn_update_agent_config", "fn_agent_create", "fn_agent_delete"] as const;
const PROVISIONING_TOOLS = ["fn_agent_create", "fn_agent_delete"] as const;
const ACTION_GATE_TASK_AGENT_ONLY_TOOLS = ["fn_task_create", "fn_delegate_task", "fn_update_identity"] as const;
const ACTION_GATE_TASK_AGENT_ONLY_TOOLS = [
"fn_task_create",
"fn_delegate_task",
"fn_task_import_github",
"fn_task_import_github_issue",
"fn_update_identity",
] as const;
const ACTION_GATE_SHARED_TASK_AGENT_TOOLS = SHARED_TASK_AGENT_TOOLS.filter(
(tool) => !(PROVISIONING_TOOLS as readonly string[]).includes(tool),
);
@@ -20,8 +26,6 @@ const PERMANENT_TASK_AGENT_ONLY_TOOLS = [
"fn_task_archive",
"fn_task_unarchive",
"fn_task_delete",
"fn_task_import_github",
"fn_task_import_github_issue",
"fn_task_plan",
"fn_mission_create",
"fn_mission_delete",
@@ -69,6 +73,8 @@ export const READONLY_FN_TOOLS: ReadonlySet<string> = new Set([
"fn_task_document_write",
"fn_task_document_read",
"fn_delegate_task",
"fn_task_import_github",
"fn_task_import_github_issue",
"fn_research_list",
"fn_research_get",
"fn_insight_list",