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:
5
.changeset/fn-3976-github-import-action-gate.md
Normal file
5
.changeset/fn-3976-github-import-action-gate.md
Normal 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.
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user