吸收数字员工技能调用审计
This commit is contained in:
@@ -12,6 +12,17 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import * as path from "path";
|
||||
import * as dependencies from "@main/services/system/dependencies";
|
||||
|
||||
const mockMcpConfig = vi.hoisted(() => ({
|
||||
value: {
|
||||
mcpServers: {},
|
||||
} as Record<string, unknown>,
|
||||
}));
|
||||
|
||||
const mockDigitalEmployee = vi.hoisted(() => ({
|
||||
recordSkillCallAudit: vi.fn(),
|
||||
settings: new Map<string, unknown>(),
|
||||
}));
|
||||
|
||||
vi.mock("electron-log", () => ({
|
||||
default: {
|
||||
info: vi.fn(),
|
||||
@@ -71,6 +82,24 @@ vi.mock("@main/services/sandbox/policy", () => ({
|
||||
getBundledWindowsSandboxHelperPath: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
vi.mock("../../db", () => ({
|
||||
readSetting: (key: string) => mockDigitalEmployee.settings.get(key) ?? null,
|
||||
}));
|
||||
|
||||
vi.mock("../../../db", () => ({
|
||||
readSetting: (key: string) => mockDigitalEmployee.settings.get(key) ?? null,
|
||||
}));
|
||||
|
||||
vi.mock("../../packages/mcp", () => ({
|
||||
mcpProxyManager: {
|
||||
getConfig: () => mockMcpConfig.value,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../digitalEmployee/stateService", () => ({
|
||||
recordDigitalEmployeeSkillCallAudit: mockDigitalEmployee.recordSkillCallAudit,
|
||||
}));
|
||||
|
||||
import { AcpEngine } from "./acpEngine";
|
||||
import * as acpClient from "./acpClient";
|
||||
|
||||
@@ -147,6 +176,12 @@ function setupEngineForCreateSession(
|
||||
return { engine, newSession };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockDigitalEmployee.settings = new Map<string, unknown>();
|
||||
mockDigitalEmployee.recordSkillCallAudit.mockClear();
|
||||
mockMcpConfig.value = { mcpServers: {} };
|
||||
});
|
||||
|
||||
describe("AcpEngine.abortSession", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -477,6 +512,56 @@ describe("AcpEngine.createSession", () => {
|
||||
});
|
||||
|
||||
describe("AcpEngine.handlePermissionRequest(strict)", () => {
|
||||
it("命中禁用 MCP 工具策略时拒绝 permission request 并写审计", async () => {
|
||||
const { engine, sessionId } = setupEngine("qimingcode");
|
||||
mockMcpConfig.value = {
|
||||
mcpServers: {
|
||||
crm: {
|
||||
command: "node",
|
||||
args: ["crm.js"],
|
||||
discoveredTools: ["crm.search", "crm.delete"],
|
||||
},
|
||||
},
|
||||
};
|
||||
mockDigitalEmployee.settings.set("digital_employee_skill_policies_v1", {
|
||||
version: 1,
|
||||
skills: {
|
||||
"qimingclaw-mcp-tool-crm-crm-delete": {
|
||||
enabled: false,
|
||||
updatedAt: "2026-06-07T08:00:00.000Z",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await (engine as any).handlePermissionRequest({
|
||||
sessionId,
|
||||
toolCall: {
|
||||
toolCallId: "tc-policy-1",
|
||||
kind: "tool",
|
||||
title: "crm.delete",
|
||||
rawInput: { customer_id: "secret-customer" },
|
||||
},
|
||||
options: [
|
||||
{
|
||||
optionId: "allow-once",
|
||||
kind: "allow_once",
|
||||
name: "allow once",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ outcome: { outcome: "cancelled" } });
|
||||
expect(mockDigitalEmployee.recordSkillCallAudit).toHaveBeenCalledWith(expect.objectContaining({
|
||||
callId: "tc-policy-1",
|
||||
toolName: "crm.delete",
|
||||
skillId: "qimingclaw-mcp-tool-crm-crm-delete",
|
||||
decision: "rejected",
|
||||
reasonCodes: ["skill_disabled"],
|
||||
sessionId,
|
||||
}));
|
||||
expect(JSON.stringify(mockDigitalEmployee.recordSkillCallAudit.mock.calls[0][0])).not.toContain("secret-customer");
|
||||
});
|
||||
|
||||
it("strict 下 workspace 内写入仅放行 allow_once", async () => {
|
||||
const { engine, sessionId } = setupEngine("qimingcode");
|
||||
(engine as any).config = { engine: "qimingcode", workspaceDir: "/tmp/ws" };
|
||||
|
||||
@@ -83,6 +83,9 @@ import { ACP_ABORT_TIMEOUT, ACP_PROMPT_TIMEOUT } from "@shared/constants";
|
||||
import { APP_DATA_DIR_NAME } from "../../constants";
|
||||
import { perfEmitter } from "../perf/perfEmitter";
|
||||
import { firstTokenTrace } from "../perf/firstTokenTrace";
|
||||
import { mcpProxyManager } from "../../packages/mcp";
|
||||
import { evaluateDigitalEmployeeSkillCall } from "../../digitalEmployee/skillExecutionPolicy";
|
||||
import { recordDigitalEmployeeSkillCallAudit } from "../../digitalEmployee/stateService";
|
||||
|
||||
/** Safe JSON.stringify that handles circular references */
|
||||
function safeStringify(obj: unknown): string {
|
||||
@@ -2418,6 +2421,38 @@ User question: ${request.prompt}`;
|
||||
return { outcome: { outcome: "cancelled" } };
|
||||
}
|
||||
|
||||
const skillPolicy = evaluateDigitalEmployeeSkillCall({
|
||||
source: "qimingclaw-acp-permission",
|
||||
toolName: params.toolCall.title || params.toolCall.kind || null,
|
||||
callId: params.toolCall.toolCallId,
|
||||
sessionId: acpSessionId,
|
||||
mcpConfig: mcpProxyManager.getConfig(),
|
||||
});
|
||||
recordDigitalEmployeeSkillCallAudit({
|
||||
callId: params.toolCall.toolCallId,
|
||||
source: skillPolicy.source,
|
||||
serverId: skillPolicy.server_id,
|
||||
toolName: skillPolicy.tool_name || params.toolCall.title || params.toolCall.kind || null,
|
||||
skillId: skillPolicy.skill_id,
|
||||
decision: skillPolicy.decision,
|
||||
reasonCodes: skillPolicy.reason_codes,
|
||||
policySnapshot: skillPolicy.policy_snapshot,
|
||||
sessionId: acpSessionId,
|
||||
status: "permission_request",
|
||||
message: skillPolicy.decision === "rejected"
|
||||
? `技能权限请求已拒绝:${params.toolCall.title || params.toolCall.kind || "unknown"}`
|
||||
: `技能权限请求已审计:${params.toolCall.title || params.toolCall.kind || "unknown"}`,
|
||||
});
|
||||
if (skillPolicy.decision === "rejected") {
|
||||
log.warn(`${this.logTag} 🚫 Skill policy rejected permission request`, {
|
||||
toolCallId: params.toolCall.toolCallId,
|
||||
toolTitle: params.toolCall.title,
|
||||
reasonCodes: skillPolicy.reason_codes,
|
||||
skillId: skillPolicy.skill_id,
|
||||
});
|
||||
return { outcome: { outcome: "cancelled" } };
|
||||
}
|
||||
|
||||
const strictEnabled = this.isStrictSandboxActiveForQimingCode();
|
||||
const strictCheck = evaluateStrictWritePermission(params, {
|
||||
strictEnabled,
|
||||
|
||||
Reference in New Issue
Block a user