Compare commits

...
Author SHA1 Message Date
morluto 0992cb3ceb fix: preserve requested evidence paths on macOS 2026-07-13 15:20:21 +08:00
2 changed files with 17 additions and 10 deletions
+4 -3
View File
@@ -83,8 +83,9 @@ export const writeEvidenceBundle = async (
if (bytes > policy.maxBytes) if (bytes > policy.maxBytes)
return err(new EvidenceFileError("write", "too-large")); return err(new EvidenceFileError("write", "too-large"));
try { try {
const canonicalParent = await realpath(dirname(resolve(path))); const requestedPath = resolve(path);
const destination = resolve(canonicalParent, basename(resolve(path))); const canonicalParent = await realpath(dirname(requestedPath));
const destination = resolve(canonicalParent, basename(requestedPath));
if (!(await isApproved(destination, policy.roots))) if (!(await isApproved(destination, policy.roots)))
return err(new EvidenceFileError("write", "outside-root")); return err(new EvidenceFileError("write", "outside-root"));
const existing = await lstat(destination).catch((cause: unknown) => { const existing = await lstat(destination).catch((cause: unknown) => {
@@ -101,7 +102,7 @@ export const writeEvidenceBundle = async (
mode: 0o600, mode: 0o600,
fsync: true, fsync: true,
}); });
return ok({ path: destination, bytes }); return ok({ path: requestedPath, bytes });
} catch (cause: unknown) { } catch (cause: unknown) {
return err(new EvidenceFileError("write", "io", { cause })); return err(new EvidenceFileError("write", "io", { cause }));
} }
+13 -7
View File
@@ -1,6 +1,7 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import type { CallToolResult } from "@modelcontextprotocol/server"; import type { CallToolResult } from "@modelcontextprotocol/server";
import { afterEach, describe, expect, it } from "vitest"; import { afterEach, describe, expect, it } from "vitest";
@@ -8,12 +9,16 @@ import { z } from "zod";
import { BinarySession } from "../src/application/BinarySession.js"; import { BinarySession } from "../src/application/BinarySession.js";
import type { AnalysisClient } from "../src/application/AnalysisProvider.js"; import type { AnalysisClient } from "../src/application/AnalysisProvider.js";
import { probeProcessCaptureCapability } from "../src/application/ProcessHarness.js";
import { observed as ok } from "./fixtures/analysisExecution.js"; import { observed as ok } from "./fixtures/analysisExecution.js";
import { createServer } from "../src/server/createServer.js"; import { createServer } from "../src/server/createServer.js";
import { evidenceBundleSchema } from "../src/domain/evidenceBundle.js"; import { evidenceBundleSchema } from "../src/domain/evidenceBundle.js";
import { silentLogger } from "../src/logger.js"; import { silentLogger } from "../src/logger.js";
const resources: Array<{ close(): Promise<unknown> }> = []; const resources: Array<{ close(): Promise<unknown> }> = [];
const processFixture = fileURLToPath(
new URL("./fixtures/processFidelity.mjs", import.meta.url),
);
let directory: string | undefined; let directory: string | undefined;
afterEach(async () => { afterEach(async () => {
await Promise.all(resources.splice(0).map((resource) => resource.close())); await Promise.all(resources.splice(0).map((resource) => resource.close()));
@@ -173,13 +178,14 @@ describe("target-free MCP lifecycle", () => {
}, 10_000); }, 10_000);
it("records approved process residuals in the unknown registry", async () => { it("records approved process residuals in the unknown registry", async () => {
if (!(await probeProcessCaptureCapability()).available) return;
const session = new BinarySession(() => client("fixture", [])); const session = new BinarySession(() => client("fixture", []));
const server = createServer(session, session, { const server = createServer(session, session, {
logger: silentLogger, logger: silentLogger,
processPolicy: { processPolicy: {
enabled: true, enabled: true,
executableRoots: ["/usr/bin"], executableRoots: [dirname(process.execPath)],
workingRoots: ["/tmp"], workingRoots: [dirname(processFixture)],
allowedEnvironment: [], allowedEnvironment: [],
allowExternalNetwork: true, allowExternalNetwork: true,
}, },
@@ -196,13 +202,13 @@ describe("target-free MCP lifecycle", () => {
arguments: { arguments: {
approved: true, approved: true,
unknown_registry_approved: true, unknown_registry_approved: true,
executable: "/usr/bin/printf", executable: process.execPath,
arguments: ["abcdef"], arguments: [processFixture, "partial"],
working_directory: "/tmp", working_directory: dirname(processFixture),
limits: { output_bytes: 1 }, limits: { output_bytes: 1 },
}, },
}); });
expect(captured.isError).not.toBe(true); expect(captured.isError, text(captured)).not.toBe(true);
const listed = z const listed = z
.object({ .object({
result: z.array(z.object({ question: z.string(), domain: z.string() })), result: z.array(z.object({ question: z.string(), domain: z.string() })),