




















@@ -5,6 +5,7 @@ import os from "node:os";
55import path from "node:path";
66import { redactSensitiveUrlLikeString } from "@openclaw/net-policy/redact-sensitive-url";
77import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
8+import type { DiagnosticSecurityEvent } from "../infra/diagnostic-events.js";
89910const runCommandWithTimeoutMock = vi.fn();
1011const installPluginFromInstalledPackageDirMock = vi.fn();
@@ -38,6 +39,7 @@ vi.resetModules();
38393940const { installPluginFromGitSpec, isImmutableGitCommitRef, parseGitPluginSpec } =
4041await import("./git-install.js");
42+const { onInternalDiagnosticEvent } = await import("../infra/diagnostic-events.js");
41434244function expectedGitRepoDir(params: { gitDir: string; normalizedSpec: string }): string {
4345const hash = createHash("sha256")
@@ -65,6 +67,7 @@ function commandArgvAt(index: number): string[] {
6567function firstInstallOptions():
6668| {
6769expectedPluginId?: string;
70+emitSuccessSecurityEvent?: boolean;
6871packageDir?: string;
6972mode?: string;
7073installPolicyRequest?: { kind?: string; requestedSpecifier?: string };
@@ -73,13 +76,27 @@ function firstInstallOptions():
7376return installPluginFromInstalledPackageDirMock.mock.calls[0]?.[0] as
7477| {
7578expectedPluginId?: string;
79+emitSuccessSecurityEvent?: boolean;
7680packageDir?: string;
7781mode?: string;
7882installPolicyRequest?: { kind?: string; requestedSpecifier?: string };
7983}
8084| undefined;
8185}
828687+function captureSecurityEvents(): {
88+events: DiagnosticSecurityEvent[];
89+stop: () => void;
90+} {
91+const events: DiagnosticSecurityEvent[] = [];
92+const stop = onInternalDiagnosticEvent((event, metadata) => {
93+if (metadata.trusted && event.type === "security.event") {
94+events.push(event);
95+}
96+});
97+return { events, stop };
98+}
99+83100describe("parseGitPluginSpec", () => {
84101it("normalizes GitHub shorthand and ref selectors", () => {
85102const explicitRef = expectParsedGitSpec("git:github.com/acme/demo@v1.2.3");
@@ -169,10 +186,16 @@ describe("installPluginFromGitSpec", () => {
169186},
170187);
171188172-const result = await installPluginFromGitSpec({
173-spec: "git:github.com/acme/demo@v1.2.3",
174-expectedPluginId: "demo",
175-});
189+const captured = captureSecurityEvents();
190+let result: Awaited<ReturnType<typeof installPluginFromGitSpec>>;
191+try {
192+result = await installPluginFromGitSpec({
193+spec: "git:github.com/acme/demo@v1.2.3",
194+expectedPluginId: "demo",
195+});
196+} finally {
197+captured.stop();
198+}
176199177200expect(result.ok).toBe(true);
178201if (!result.ok) {
@@ -202,6 +225,64 @@ describe("installPluginFromGitSpec", () => {
202225expect(installOptions?.installPolicyRequest?.requestedSpecifier).toBe(
203226"git:github.com/acme/demo@v1.2.3",
204227);
228+expect(installOptions?.emitSuccessSecurityEvent).toBe(false);
229+expect(captured.events).toHaveLength(1);
230+expect(captured.events[0]).toMatchObject({
231+action: "plugin.installed",
232+outcome: "success",
233+target: { kind: "plugin", name: "demo" },
234+attributes: {
235+source_family: "git",
236+mode: "install",
237+extension_count: 1,
238+has_version: true,
239+},
240+});
241+});
242+243+it("does not emit git install success when committing the managed repo fails", async () => {
244+const gitRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-git-install-fail-"));
245+const gitDir = path.join(gitRoot, "not-a-directory");
246+await fs.writeFile(gitDir, "file blocks nested managed repo creation", "utf8");
247+try {
248+runCommandWithTimeoutMock
249+.mockResolvedValueOnce({ code: 0, stdout: "", stderr: "" })
250+.mockResolvedValueOnce({ code: 0, stdout: "abc123\n", stderr: "" })
251+.mockResolvedValueOnce({ code: 0, stdout: "", stderr: "" });
252+installPluginFromInstalledPackageDirMock.mockImplementation(
253+async (params: { packageDir: string }) => {
254+await fs.mkdir(params.packageDir, { recursive: true });
255+await fs.writeFile(path.join(params.packageDir, "package.json"), "{}", "utf8");
256+return {
257+ok: true,
258+pluginId: "demo",
259+targetDir: params.packageDir,
260+version: "1.2.3",
261+extensions: ["index.js"],
262+};
263+},
264+);
265+const captured = captureSecurityEvents();
266+267+let result: Awaited<ReturnType<typeof installPluginFromGitSpec>>;
268+try {
269+result = await installPluginFromGitSpec({
270+spec: "git:github.com/acme/demo",
271+ gitDir,
272+});
273+} finally {
274+captured.stop();
275+}
276+277+expect(result.ok).toBe(false);
278+if (!result.ok) {
279+expect(result.error).toContain("failed to replace managed git plugin repository");
280+}
281+expect(firstInstallOptions()?.emitSuccessSecurityEvent).toBe(false);
282+expect(captured.events).toHaveLength(0);
283+} finally {
284+await fs.rm(gitRoot, { recursive: true, force: true });
285+}
205286});
206287207288it("uses a shallow clone when no ref is requested", async () => {
@@ -249,11 +330,17 @@ describe("installPluginFromGitSpec", () => {
249330code: "security_scan_blocked",
250331},
251332});
333+const captured = captureSecurityEvents();
252334253-const result = await installPluginFromGitSpec({
254-spec: "git:github.com/acme/demo",
255-expectedPluginId: "demo",
256-});
335+let result: Awaited<ReturnType<typeof installPluginFromGitSpec>>;
336+try {
337+result = await installPluginFromGitSpec({
338+spec: "git:github.com/acme/demo",
339+expectedPluginId: "demo",
340+});
341+} finally {
342+captured.stop();
343+}
257344258345expect(result.ok).toBe(false);
259346if (!result.ok) {
@@ -277,6 +364,55 @@ describe("installPluginFromGitSpec", () => {
277364}),
278365);
279366expect(installPluginFromInstalledPackageDirMock).not.toHaveBeenCalled();
367+expect(captured.events).toHaveLength(1);
368+expect(captured.events[0]).toMatchObject({
369+action: "plugin.audit.failed",
370+outcome: "denied",
371+target: { kind: "plugin", name: "demo" },
372+attributes: {
373+source_family: "git",
374+mode: "install",
375+},
376+});
377+});
378+379+it("emits git audit errors when install policy preflight fails", async () => {
380+runCommandWithTimeoutMock
381+.mockResolvedValueOnce({ code: 0, stdout: "", stderr: "" })
382+.mockResolvedValueOnce({ code: 0, stdout: "abc123\n", stderr: "" });
383+preflightPluginGitInstallPolicyMock.mockResolvedValueOnce({
384+blocked: {
385+reason: "install policy unavailable",
386+code: "security_scan_failed",
387+},
388+});
389+const captured = captureSecurityEvents();
390+391+let result: Awaited<ReturnType<typeof installPluginFromGitSpec>>;
392+try {
393+result = await installPluginFromGitSpec({
394+spec: "git:file:///Users/example/private-plugin",
395+});
396+} finally {
397+captured.stop();
398+}
399+400+expect(result.ok).toBe(false);
401+if (!result.ok) {
402+expect(result.error).toContain("install policy unavailable");
403+}
404+expect(installPluginFromInstalledPackageDirMock).not.toHaveBeenCalled();
405+expect(captured.events).toHaveLength(1);
406+expect(captured.events[0]).toMatchObject({
407+action: "plugin.audit.failed",
408+outcome: "error",
409+target: { kind: "plugin" },
410+attributes: {
411+source_family: "git",
412+mode: "install",
413+},
414+});
415+expect(captured.events[0]?.target).not.toHaveProperty("name");
280416});
281417282418it("reports full commit refs as immutable to install policy", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。