吸收数字员工技能调用审计

This commit is contained in:
baiyanyun
2026-06-10 09:24:02 +08:00
parent 91540dec0c
commit cc36d8dcaf
15 changed files with 894 additions and 73 deletions

View File

@@ -12,6 +12,16 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
const mockDigitalEmployee = vi.hoisted(() => ({
recordRuntimeEvent: vi.fn(),
recordSkillCallAudit: vi.fn(),
evaluateSkillCall: vi.fn(() => ({
decision: "observed",
reason_codes: ["skill_not_matched"],
source: "qimingclaw-acp",
server_id: null,
tool_name: "Write",
skill_id: null,
policy_snapshot: {},
})),
}));
vi.mock("electron-log", () => ({
@@ -29,6 +39,17 @@ vi.mock("../services/computerServer", () => ({
vi.mock("../services/digitalEmployee/stateService", () => ({
recordDigitalEmployeeRuntimeEvent: mockDigitalEmployee.recordRuntimeEvent,
recordDigitalEmployeeSkillCallAudit: mockDigitalEmployee.recordSkillCallAudit,
}));
vi.mock("../services/digitalEmployee/skillExecutionPolicy", () => ({
evaluateDigitalEmployeeSkillCall: mockDigitalEmployee.evaluateSkillCall,
}));
vi.mock("../services/packages/mcp", () => ({
mcpProxyManager: {
getConfig: () => ({ mcpServers: {} }),
},
}));
// Use dynamic import to get the actual agentService (an EventEmitter) after mocking
@@ -69,6 +90,8 @@ describe("eventForwarders", () => {
unregisterEventForwarders();
vi.clearAllMocks();
mockDigitalEmployee.recordRuntimeEvent.mockClear();
mockDigitalEmployee.recordSkillCallAudit.mockClear();
mockDigitalEmployee.evaluateSkillCall.mockClear();
});
it("registerEventForwarders registers listeners on agentService", () => {
@@ -202,6 +225,13 @@ describe("eventForwarders", () => {
}),
}),
);
expect(mockDigitalEmployee.recordSkillCallAudit).toHaveBeenCalledWith(expect.objectContaining({
callId: "tool-1",
decision: "observed",
reasonCodes: ["skill_not_matched"],
sessionId: "session-1",
}));
expect(JSON.stringify(mockDigitalEmployee.recordSkillCallAudit.mock.calls[0][0])).not.toContain("/tmp/report.md");
});
it("projects permission and file events into approval and artifact hints", () => {

View File

@@ -2,7 +2,12 @@ import { agentService } from "../services/engines/unifiedAgent";
import type { UnifiedSessionMessage } from "../services/engines/unifiedAgent";
import { pushSseEvent } from "../services/computerServer";
import { firstTokenTrace } from "../services/engines/perf/firstTokenTrace";
import { recordDigitalEmployeeRuntimeEvent } from "../services/digitalEmployee/stateService";
import {
recordDigitalEmployeeRuntimeEvent,
recordDigitalEmployeeSkillCallAudit,
} from "../services/digitalEmployee/stateService";
import { evaluateDigitalEmployeeSkillCall } from "../services/digitalEmployee/skillExecutionPolicy";
import { mcpProxyManager } from "../services/packages/mcp";
import type { HandlerContext } from "@shared/types/ipc";
import log from "electron-log";
@@ -25,6 +30,42 @@ function readEventField(data: unknown, keys: string[]): string | undefined {
function recordForwardedEvent(eventType: string, data: unknown): void {
const normalized = normalizeForwardedEvent(eventType, data);
recordDigitalEmployeeRuntimeEvent(normalized);
recordSkillCallAuditForForwardedEvent(eventType, data, normalized);
}
function recordSkillCallAuditForForwardedEvent(
eventType: string,
data: unknown,
normalized: Parameters<typeof recordDigitalEmployeeRuntimeEvent>[0],
): void {
const subtype = readEventField(data, ["subType", "sessionUpdate", "type", "messageType"]);
if (eventType !== "computer:progress" || (subtype !== "tool_call" && subtype !== "tool_call_update")) {
return;
}
try {
const toolName = readEventField(data, ["toolTitle", "title", "name", "kind"]);
const policy = evaluateDigitalEmployeeSkillCall({
source: "qimingclaw-acp",
toolName,
callId: readEventField(data, ["toolCallId", "id"]) || `${subtype}:${Date.now()}`,
sessionId: readEventField(data, ["sessionId", "session_id", "id"]),
mcpConfig: mcpProxyManager.getConfig(),
});
recordDigitalEmployeeSkillCallAudit({
callId: readEventField(data, ["toolCallId", "id"]) || `${subtype}:${Date.now()}`,
source: policy.source,
serverId: policy.server_id,
toolName: policy.tool_name || toolName,
skillId: policy.skill_id,
decision: policy.decision,
reasonCodes: policy.reason_codes,
policySnapshot: policy.policy_snapshot,
sessionId: normalized.session_id,
status: readEventField(data, ["status"]) || normalized.status,
});
} catch (error) {
log.warn("[EventForwarders] Failed to record skill call audit:", error);
}
}
function normalizeForwardedEvent(