mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
feat(agents): emit security events for tool vetoes
This commit is contained in:
@@ -824,6 +824,59 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("emits a security event for intentional hook vetoes", async () => {
|
||||
hookRunner.hasHooks.mockImplementation((hookName: string) => hookName === "before_tool_call");
|
||||
hookRunner.runBeforeToolCall.mockResolvedValue({
|
||||
block: true,
|
||||
blockReason: "blocked by policy",
|
||||
});
|
||||
const execute = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "nope" }] });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
loopDetection: { enabled: false },
|
||||
});
|
||||
|
||||
await withDiagnosticEvents(async (emitted, flush) => {
|
||||
await tool.execute("tool-call-blocked", { path: "/tmp/file" });
|
||||
await flush();
|
||||
|
||||
const securityEvent = emitted.find(
|
||||
(event): event is Extract<DiagnosticEventPayload, { type: "security.event" }> =>
|
||||
event.type === "security.event",
|
||||
);
|
||||
expect(securityEvent).toMatchObject({
|
||||
type: "security.event",
|
||||
category: "tool",
|
||||
action: "tool.execution.blocked",
|
||||
outcome: "denied",
|
||||
severity: "medium",
|
||||
reason: "plugin-before-tool-call",
|
||||
actor: { kind: "agent" },
|
||||
target: {
|
||||
kind: "tool",
|
||||
name: "read",
|
||||
},
|
||||
policy: {
|
||||
id: "plugin-before-tool-call",
|
||||
decision: "deny",
|
||||
reason: "plugin-before-tool-call",
|
||||
},
|
||||
control: {
|
||||
id: "before-tool-call",
|
||||
family: "approval",
|
||||
},
|
||||
attributes: {
|
||||
params_kind: "object",
|
||||
tool_source: "core",
|
||||
},
|
||||
});
|
||||
expect(securityEvent?.eventId).toBeTypeOf("string");
|
||||
expect(JSON.stringify(securityEvent)).not.toContain("/tmp/file");
|
||||
expect(emitted.some((event) => event.type === "tool.execution.blocked")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not let hostile thrown values break diagnostic error emission", async () => {
|
||||
const hostileError = new Proxy(
|
||||
{},
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import {
|
||||
emitTrustedDiagnosticEvent,
|
||||
emitTrustedDiagnosticEventWithPrivateData,
|
||||
emitTrustedSecurityEvent,
|
||||
type DiagnosticEventPrivateData,
|
||||
type DiagnosticToolParamsSummary,
|
||||
type DiagnosticToolSource,
|
||||
@@ -566,6 +567,45 @@ function emitSkillUsedDiagnostic(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function emitToolBlockedSecurityEvent(params: {
|
||||
ctx?: HookContext;
|
||||
deniedReason: string;
|
||||
toolIdentity: ToolDiagnosticIdentity;
|
||||
toolName: string;
|
||||
trace?: DiagnosticTraceContext;
|
||||
paramsSummary?: DiagnosticToolParamsSummary;
|
||||
}): void {
|
||||
emitTrustedSecurityEvent({
|
||||
category: "tool",
|
||||
action: "tool.execution.blocked",
|
||||
outcome: "denied",
|
||||
severity: "medium",
|
||||
reason: params.deniedReason,
|
||||
...(params.trace ? { trace: params.trace } : {}),
|
||||
actor: {
|
||||
kind: "agent",
|
||||
},
|
||||
target: {
|
||||
kind: "tool",
|
||||
name: params.toolName,
|
||||
...(params.toolIdentity.toolOwner ? { owner: params.toolIdentity.toolOwner } : {}),
|
||||
},
|
||||
policy: {
|
||||
id: "plugin-before-tool-call",
|
||||
decision: "deny",
|
||||
reason: params.deniedReason,
|
||||
},
|
||||
control: {
|
||||
id: "before-tool-call",
|
||||
family: "approval",
|
||||
},
|
||||
attributes: {
|
||||
tool_source: params.toolIdentity.toolSource,
|
||||
...(params.paramsSummary ? { params_kind: params.paramsSummary.kind } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function notifyPluginApprovalResolution(
|
||||
approval: PluginApprovalRequest,
|
||||
resolution: PluginApprovalResolution,
|
||||
@@ -1353,6 +1393,14 @@ export function wrapToolWithBeforeToolCallHook(
|
||||
reason: outcome.reason,
|
||||
deniedReason: outcome.deniedReason ?? "plugin-before-tool-call",
|
||||
});
|
||||
emitToolBlockedSecurityEvent({
|
||||
ctx,
|
||||
deniedReason: outcome.deniedReason ?? "plugin-before-tool-call",
|
||||
toolIdentity: diagnosticIdentity,
|
||||
toolName: normalizedToolName,
|
||||
trace,
|
||||
paramsSummary: eventBase.paramsSummary,
|
||||
});
|
||||
}
|
||||
const blockedResult = buildBlockedToolResult({
|
||||
reason: outcome.reason,
|
||||
|
||||
Reference in New Issue
Block a user