feat(FN-3731): add action-gate reload endpoint for reloadable exempt-tools
Adds a reload endpoint for action-gate exempt tools, exposing `POST /api/system/reload-exempt-tools` on the dashboard to hot-reload the engine's mutable EXEMPT_TOOLS registry without a restart. The endpoint is wired through `agent-action-gate.ts` with test coverage for reloadable behavior. Fusion-Task-Id: FN-3731
This commit is contained in:
5
.changeset/fn-3731-reloadable-exempt-tools.md
Normal file
5
.changeset/fn-3731-reloadable-exempt-tools.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add a runtime action-gate exempt-tools reload API so operators can refresh exemptions without restarting the engine process.
|
||||||
@@ -87,6 +87,7 @@ Unknown/unclassified tool fallback:
|
|||||||
- In permanent-agent sessions, unknown tools default to `require-approval` (fail-safe).
|
- 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.
|
- 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, task/agent coordination, messaging, evaluations, identity reflection, memory bookkeeping) are exempt by design and always allowed so permanent-agent heartbeats can complete.
|
||||||
|
- 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.
|
||||||
|
|
||||||
Interim enforcement behavior (persistence-integrated, pre-resume lifecycle):
|
Interim enforcement behavior (persistence-integrated, pre-resume lifecycle):
|
||||||
|
|
||||||
|
|||||||
@@ -44,11 +44,20 @@ const mockCentralListProjects = vi.fn().mockResolvedValue([]);
|
|||||||
const mockCentralInit = vi.fn().mockResolvedValue(undefined);
|
const mockCentralInit = vi.fn().mockResolvedValue(undefined);
|
||||||
const mockCentralClose = vi.fn().mockResolvedValue(undefined);
|
const mockCentralClose = vi.fn().mockResolvedValue(undefined);
|
||||||
const mockCentralReconcileProjectStatuses = vi.fn().mockResolvedValue(undefined);
|
const mockCentralReconcileProjectStatuses = vi.fn().mockResolvedValue(undefined);
|
||||||
const { mockPerformUpdateCheck, mockClearUpdateCheckCache, mockExecSync, mockExecFile } = vi.hoisted(() => ({
|
const {
|
||||||
|
mockPerformUpdateCheck,
|
||||||
|
mockClearUpdateCheckCache,
|
||||||
|
mockExecSync,
|
||||||
|
mockExecFile,
|
||||||
|
mockReloadExemptTools,
|
||||||
|
mockGetExemptToolNames,
|
||||||
|
} = vi.hoisted(() => ({
|
||||||
mockPerformUpdateCheck: vi.fn(),
|
mockPerformUpdateCheck: vi.fn(),
|
||||||
mockClearUpdateCheckCache: vi.fn(),
|
mockClearUpdateCheckCache: vi.fn(),
|
||||||
mockExecSync: vi.fn(),
|
mockExecSync: vi.fn(),
|
||||||
mockExecFile: vi.fn(),
|
mockExecFile: vi.fn(),
|
||||||
|
mockReloadExemptTools: vi.fn(),
|
||||||
|
mockGetExemptToolNames: vi.fn().mockReturnValue(["read", "find"]),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../update-check.js", async () => {
|
vi.mock("../update-check.js", async () => {
|
||||||
@@ -111,6 +120,8 @@ vi.mock("@fusion/core", async (importOriginal) => {
|
|||||||
vi.mock("@fusion/engine", async () => {
|
vi.mock("@fusion/engine", async () => {
|
||||||
const { createEngineMock } = await import("../test/mockCoreEngine.js");
|
const { createEngineMock } = await import("../test/mockCoreEngine.js");
|
||||||
return createEngineMock({
|
return createEngineMock({
|
||||||
|
reloadExemptTools: mockReloadExemptTools,
|
||||||
|
getExemptToolNames: mockGetExemptToolNames,
|
||||||
createFnAgent: vi.fn(async (options?: { onText?: (delta: string) => void }) => ({
|
createFnAgent: vi.fn(async (options?: { onText?: (delta: string) => void }) => ({
|
||||||
session: {
|
session: {
|
||||||
state: {
|
state: {
|
||||||
@@ -310,6 +321,54 @@ describe("route registrar ordering invariants", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("POST /api/action-gate/reload", () => {
|
||||||
|
function buildApp(store: TaskStore, options?: Parameters<typeof createApiRoutes>[1]) {
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
app.use("/api", createApiRoutes(store, options));
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockReloadExemptTools.mockReset();
|
||||||
|
mockGetExemptToolNames.mockReset();
|
||||||
|
mockGetExemptToolNames.mockReturnValue(["read", "find"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reloads defaults when no tools body is provided", async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
const res = await REQUEST(buildApp(store), "POST", "/api/action-gate/reload", JSON.stringify({}), {
|
||||||
|
"content-type": "application/json",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(mockReloadExemptTools).toHaveBeenCalledWith();
|
||||||
|
expect(res.body).toEqual({ ok: true, tools: ["read", "find"] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reloads explicit tool list when tools are provided", async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
mockGetExemptToolNames.mockReturnValue(["custom_tool"]);
|
||||||
|
const res = await REQUEST(buildApp(store), "POST", "/api/action-gate/reload", JSON.stringify({ tools: ["custom_tool"] }), {
|
||||||
|
"content-type": "application/json",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(mockReloadExemptTools).toHaveBeenCalledWith(["custom_tool"]);
|
||||||
|
expect(res.body).toEqual({ ok: true, tools: ["custom_tool"] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns bad request when tools is not a string array", async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
const res = await REQUEST(buildApp(store), "POST", "/api/action-gate/reload", JSON.stringify({ tools: ["ok", 1] }), {
|
||||||
|
"content-type": "application/json",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(mockReloadExemptTools).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("GET /api/system-stats", () => {
|
describe("GET /api/system-stats", () => {
|
||||||
const projectId = "proj-system-stats";
|
const projectId = "proj-system-stats";
|
||||||
|
|
||||||
|
|||||||
@@ -295,7 +295,12 @@ async function discoverDashboardPiExtensions(cwd: string): Promise<PiExtensionSe
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
import { createFnAgent as engineCreateFnAgentForRefine, promptWithFallback as enginePromptWithFallback } from "@fusion/engine";
|
import {
|
||||||
|
createFnAgent as engineCreateFnAgentForRefine,
|
||||||
|
getExemptToolNames as engineGetExemptToolNames,
|
||||||
|
promptWithFallback as enginePromptWithFallback,
|
||||||
|
reloadExemptTools as engineReloadExemptTools,
|
||||||
|
} from "@fusion/engine";
|
||||||
|
|
||||||
// Test-injectable override; defaults to the statically imported engine binding.
|
// Test-injectable override; defaults to the statically imported engine binding.
|
||||||
let createFnAgentForRefine: typeof import("@fusion/engine").createFnAgent | undefined = engineCreateFnAgentForRefine;
|
let createFnAgentForRefine: typeof import("@fusion/engine").createFnAgent | undefined = engineCreateFnAgentForRefine;
|
||||||
@@ -1269,6 +1274,33 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/action-gate/reload
|
||||||
|
* Reloads action-gate exempt tools from defaults or a provided override list.
|
||||||
|
*/
|
||||||
|
router.post("/action-gate/reload", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const body = (req.body && typeof req.body === "object") ? (req.body as Record<string, unknown>) : {};
|
||||||
|
const hasTools = Object.hasOwn(body, "tools");
|
||||||
|
if (hasTools) {
|
||||||
|
const tools = body.tools;
|
||||||
|
if (!Array.isArray(tools) || tools.some((tool) => typeof tool !== "string")) {
|
||||||
|
throw badRequest("Request body must provide tools as string[] when present");
|
||||||
|
}
|
||||||
|
engineReloadExemptTools(tools);
|
||||||
|
} else {
|
||||||
|
engineReloadExemptTools();
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ ok: true, tools: engineGetExemptToolNames() });
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (err instanceof ApiError) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
rethrowAsApiError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/executor/stats
|
* GET /api/executor/stats
|
||||||
* Returns executor status metadata for dashboard status surfaces.
|
* Returns executor status metadata for dashboard status surfaces.
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
import { evaluateAgentActionGate, computeApprovalDedupeKey } from "../agent-action-gate.js";
|
import {
|
||||||
|
addToExemptTools,
|
||||||
|
computeApprovalDedupeKey,
|
||||||
|
evaluateAgentActionGate,
|
||||||
|
reloadExemptTools,
|
||||||
|
} from "../agent-action-gate.js";
|
||||||
import type { AgentPermissionPolicy } from "@fusion/core";
|
import type { AgentPermissionPolicy } from "@fusion/core";
|
||||||
|
|
||||||
const unrestrictedPolicy: AgentPermissionPolicy = {
|
const unrestrictedPolicy: AgentPermissionPolicy = {
|
||||||
@@ -13,6 +18,17 @@ const unrestrictedPolicy: AgentPermissionPolicy = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const lockedDownPolicy: AgentPermissionPolicy = {
|
||||||
|
presetId: "locked-down",
|
||||||
|
rules: {
|
||||||
|
"git_write": "block",
|
||||||
|
"file_write_delete": "block",
|
||||||
|
"command_execution": "block",
|
||||||
|
"network_api": "block",
|
||||||
|
"task_agent_mutation": "block",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const approvalPolicy: AgentPermissionPolicy = {
|
const approvalPolicy: AgentPermissionPolicy = {
|
||||||
...unrestrictedPolicy,
|
...unrestrictedPolicy,
|
||||||
presetId: "approval-required",
|
presetId: "approval-required",
|
||||||
@@ -26,6 +42,9 @@ const approvalPolicy: AgentPermissionPolicy = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("agent-action-gate", () => {
|
describe("agent-action-gate", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
reloadExemptTools();
|
||||||
|
});
|
||||||
it("classifies write/edit as file_write_delete", () => {
|
it("classifies write/edit as file_write_delete", () => {
|
||||||
const write = evaluateAgentActionGate({ agentId: "a1", toolName: "write", args: { path: "a.ts" }, permissionPolicy: unrestrictedPolicy });
|
const write = evaluateAgentActionGate({ agentId: "a1", toolName: "write", args: { path: "a.ts" }, permissionPolicy: unrestrictedPolicy });
|
||||||
const edit = evaluateAgentActionGate({ agentId: "a1", toolName: "edit", args: { path: "a.ts" }, permissionPolicy: unrestrictedPolicy });
|
const edit = evaluateAgentActionGate({ agentId: "a1", toolName: "edit", args: { path: "a.ts" }, permissionPolicy: unrestrictedPolicy });
|
||||||
@@ -93,34 +112,12 @@ describe("agent-action-gate", () => {
|
|||||||
"fn_update_identity",
|
"fn_update_identity",
|
||||||
"fn_reflect_on_performance",
|
"fn_reflect_on_performance",
|
||||||
])("always allows newly exempt internal tool %s under locked-down policies", (toolName) => {
|
])("always allows newly exempt internal tool %s under locked-down policies", (toolName) => {
|
||||||
const lockedDownPolicy: AgentPermissionPolicy = {
|
|
||||||
presetId: "locked-down",
|
|
||||||
rules: {
|
|
||||||
"git_write": "block",
|
|
||||||
"file_write_delete": "block",
|
|
||||||
"command_execution": "block",
|
|
||||||
"network_api": "block",
|
|
||||||
"task_agent_mutation": "block",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const decision = evaluateAgentActionGate({ agentId: "a1", toolName, args: {}, permissionPolicy: lockedDownPolicy });
|
const decision = evaluateAgentActionGate({ agentId: "a1", toolName, args: {}, permissionPolicy: lockedDownPolicy });
|
||||||
expect(decision.disposition).toBe("allow");
|
expect(decision.disposition).toBe("allow");
|
||||||
expect(decision.category).toBe("exempt");
|
expect(decision.category).toBe("exempt");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps bash and write blocked under locked-down policy", () => {
|
it("keeps bash and write blocked under locked-down policy", () => {
|
||||||
const lockedDownPolicy: AgentPermissionPolicy = {
|
|
||||||
presetId: "locked-down",
|
|
||||||
rules: {
|
|
||||||
"git_write": "block",
|
|
||||||
"file_write_delete": "block",
|
|
||||||
"command_execution": "block",
|
|
||||||
"network_api": "block",
|
|
||||||
"task_agent_mutation": "block",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const bashDecision = evaluateAgentActionGate({
|
const bashDecision = evaluateAgentActionGate({
|
||||||
agentId: "a1",
|
agentId: "a1",
|
||||||
toolName: "bash",
|
toolName: "bash",
|
||||||
@@ -138,6 +135,64 @@ describe("agent-action-gate", () => {
|
|||||||
expect(writeDecision.disposition).toBe("block");
|
expect(writeDecision.disposition).toBe("block");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses default exemptions without reload", () => {
|
||||||
|
const decision = evaluateAgentActionGate({
|
||||||
|
agentId: "a1",
|
||||||
|
toolName: "read",
|
||||||
|
args: {},
|
||||||
|
permissionPolicy: lockedDownPolicy,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decision.category).toBe("exempt");
|
||||||
|
expect(decision.disposition).toBe("allow");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reloadExemptTools can replace and restore exemptions", () => {
|
||||||
|
reloadExemptTools(["custom_tool"]);
|
||||||
|
const customDecision = evaluateAgentActionGate({
|
||||||
|
agentId: "a1",
|
||||||
|
toolName: "custom_tool",
|
||||||
|
args: {},
|
||||||
|
permissionPolicy: lockedDownPolicy,
|
||||||
|
});
|
||||||
|
expect(customDecision.category).toBe("exempt");
|
||||||
|
expect(customDecision.disposition).toBe("allow");
|
||||||
|
|
||||||
|
reloadExemptTools([]);
|
||||||
|
const readBlocked = evaluateAgentActionGate({
|
||||||
|
agentId: "a1",
|
||||||
|
toolName: "read",
|
||||||
|
args: {},
|
||||||
|
permissionPolicy: lockedDownPolicy,
|
||||||
|
});
|
||||||
|
expect(readBlocked.category).toBe("command_execution");
|
||||||
|
expect(readBlocked.disposition).toBe("block");
|
||||||
|
|
||||||
|
reloadExemptTools();
|
||||||
|
const readRestored = evaluateAgentActionGate({
|
||||||
|
agentId: "a1",
|
||||||
|
toolName: "read",
|
||||||
|
args: {},
|
||||||
|
permissionPolicy: lockedDownPolicy,
|
||||||
|
});
|
||||||
|
expect(readRestored.category).toBe("exempt");
|
||||||
|
expect(readRestored.disposition).toBe("allow");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("addToExemptTools exempts a custom tool", () => {
|
||||||
|
addToExemptTools("my_new_tool");
|
||||||
|
|
||||||
|
const decision = evaluateAgentActionGate({
|
||||||
|
agentId: "a1",
|
||||||
|
toolName: "my_new_tool",
|
||||||
|
args: {},
|
||||||
|
permissionPolicy: lockedDownPolicy,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decision.category).toBe("exempt");
|
||||||
|
expect(decision.disposition).toBe("allow");
|
||||||
|
});
|
||||||
|
|
||||||
it("resolves disposition from policy", () => {
|
it("resolves disposition from policy", () => {
|
||||||
const result = evaluateAgentActionGate({ agentId: "a1", toolName: "write", args: { path: "a.ts" }, permissionPolicy: approvalPolicy });
|
const result = evaluateAgentActionGate({ agentId: "a1", toolName: "write", args: { path: "a.ts" }, permissionPolicy: approvalPolicy });
|
||||||
expect(result.disposition).toBe("require-approval");
|
expect(result.disposition).toBe("require-approval");
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
AgentPermissionPolicyActionCategory,
|
AgentPermissionPolicyActionCategory,
|
||||||
AgentPermissionPolicyDisposition,
|
AgentPermissionPolicyDisposition,
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
|
import { runtimeLog } from "./logger.js";
|
||||||
|
|
||||||
export type AgentActionGateResourceType = "file" | "git" | "task" | "agent" | "research" | "command" | "other";
|
export type AgentActionGateResourceType = "file" | "git" | "task" | "agent" | "research" | "command" | "other";
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ export interface AgentActionGateContext {
|
|||||||
|
|
||||||
// FN-3724: Internal Fusion runtime/coordinator tools never perform external mutations.
|
// FN-3724: Internal Fusion runtime/coordinator tools never perform external mutations.
|
||||||
// They must bypass user-configurable approval/block policies so permanent-agent heartbeats cannot deadlock.
|
// They must bypass user-configurable approval/block policies so permanent-agent heartbeats cannot deadlock.
|
||||||
const EXEMPT_TOOLS = new Set([
|
const DEFAULT_EXEMPT_TOOLS = [
|
||||||
"read",
|
"read",
|
||||||
"find",
|
"find",
|
||||||
"grep",
|
"grep",
|
||||||
@@ -55,7 +56,44 @@ const EXEMPT_TOOLS = new Set([
|
|||||||
"fn_read_evaluations",
|
"fn_read_evaluations",
|
||||||
"fn_update_identity",
|
"fn_update_identity",
|
||||||
"fn_reflect_on_performance",
|
"fn_reflect_on_performance",
|
||||||
]);
|
] as const;
|
||||||
|
|
||||||
|
let _exemptTools: Set<string> | null = null;
|
||||||
|
|
||||||
|
function getExemptTools(): Set<string> {
|
||||||
|
if (!_exemptTools) {
|
||||||
|
_exemptTools = new Set(DEFAULT_EXEMPT_TOOLS);
|
||||||
|
}
|
||||||
|
return _exemptTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads the exempt-tools registry used by the action gate.
|
||||||
|
* If no tool list is provided, the canonical default exemption set is restored.
|
||||||
|
*/
|
||||||
|
export function reloadExemptTools(newTools?: string[]): string[] {
|
||||||
|
const nextTools = newTools ?? [...DEFAULT_EXEMPT_TOOLS];
|
||||||
|
_exemptTools = new Set(nextTools);
|
||||||
|
const toolNames = [..._exemptTools];
|
||||||
|
runtimeLog.log(`[action-gate] Reloaded exempt tools (${toolNames.length})`);
|
||||||
|
return toolNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a tool to the exempt-tools registry at runtime.
|
||||||
|
*/
|
||||||
|
export function addToExemptTools(toolName: string): string[] {
|
||||||
|
const nextTools = new Set(getExemptTools());
|
||||||
|
nextTools.add(toolName);
|
||||||
|
_exemptTools = new Set(nextTools);
|
||||||
|
const toolNames = [..._exemptTools];
|
||||||
|
runtimeLog.log(`[action-gate] Added exempt tool: ${toolName}`);
|
||||||
|
return toolNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExemptToolNames(): string[] {
|
||||||
|
return [...getExemptTools()];
|
||||||
|
}
|
||||||
|
|
||||||
const TASK_AGENT_MANAGEMENT_TOOLS = new Set([
|
const TASK_AGENT_MANAGEMENT_TOOLS = new Set([
|
||||||
"fn_task_create",
|
"fn_task_create",
|
||||||
@@ -68,6 +106,8 @@ const TASK_AGENT_MANAGEMENT_TOOLS = new Set([
|
|||||||
|
|
||||||
const NETWORK_API_TOOLS = new Set(["fn_research_run"]);
|
const NETWORK_API_TOOLS = new Set(["fn_research_run"]);
|
||||||
|
|
||||||
|
const READONLY_DISCOVERY_TOOLS = new Set(["read", "find", "grep", "ls"]);
|
||||||
|
|
||||||
const GIT_WRITE_SUBCOMMANDS = new Set([
|
const GIT_WRITE_SUBCOMMANDS = new Set([
|
||||||
"add",
|
"add",
|
||||||
"commit",
|
"commit",
|
||||||
@@ -209,9 +249,13 @@ export function evaluateAgentActionGate(params: {
|
|||||||
operation = params.toolName;
|
operation = params.toolName;
|
||||||
resourceType = "file";
|
resourceType = "file";
|
||||||
resourceId = typeof args.path === "string" ? args.path : undefined;
|
resourceId = typeof args.path === "string" ? args.path : undefined;
|
||||||
} else if (EXEMPT_TOOLS.has(params.toolName)) {
|
} else if (getExemptTools().has(params.toolName)) {
|
||||||
category = "exempt";
|
category = "exempt";
|
||||||
operation = params.toolName;
|
operation = params.toolName;
|
||||||
|
} else if (READONLY_DISCOVERY_TOOLS.has(params.toolName)) {
|
||||||
|
category = "command_execution";
|
||||||
|
operation = params.toolName;
|
||||||
|
resourceType = "file";
|
||||||
} else if (TASK_AGENT_MANAGEMENT_TOOLS.has(params.toolName)) {
|
} else if (TASK_AGENT_MANAGEMENT_TOOLS.has(params.toolName)) {
|
||||||
category = "task_agent_mutation";
|
category = "task_agent_mutation";
|
||||||
operation = params.toolName;
|
operation = params.toolName;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export { AgentLogger, type AgentLoggerOptions, summarizeToolArgs } from "./agent-logger.js";
|
export { AgentLogger, type AgentLoggerOptions, summarizeToolArgs } from "./agent-logger.js";
|
||||||
|
export { reloadExemptTools, addToExemptTools, getExemptToolNames } from "./agent-action-gate.js";
|
||||||
export {
|
export {
|
||||||
createTaskCreateTool,
|
createTaskCreateTool,
|
||||||
createTaskDocumentReadTool,
|
createTaskDocumentReadTool,
|
||||||
|
|||||||
Reference in New Issue
Block a user