fix(contracts)!: require absolute local paths for session filesystem inputs (#948)

fix(contracts): require absolute local paths for session filesystem inputs

Snapshot, evidence-bundle export, and managed-target inputs accepted any non-empty string, so relative paths were resolved against the MCP server working directory instead of the caller's intent. Refine these contract fields to absolute host paths (Windows drive and forward-slash forms included), advertise the constraint on the property descriptions, and regenerate the derived catalog, product catalog, and skill catalog digest. Relative CLI options are unaffected because CLI flows do not parse these MCP contracts.

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
Dery Ferdika
2026-10-07 23:31:55 +07:00
committed by GitHub
parent 06b047a092
commit 5b172b1042
9 changed files with 195 additions and 11 deletions
+2 -2
View File
@@ -675,9 +675,9 @@
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "af7fe81ec689c2a77f04579b6f4beb4abe86aa71d49c4844d8e0f4d7ed7a3a1e",
"tools_sha256": "8816080083d86cb4c05db8d1ef452938abef2e6fdeead566252592ffbb55d828",
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
"combined_sha256": "83b4c3445ed0ec003c0aeefa3188f123d9ae319484f8b1e2b611d2fe278c9c72",
"combined_sha256": "9b4e896d2585645f1ed1f9f3eef270cf798f08035c70091178b7bd6119b22bf5",
"providers_sha256": "aea15f517c822d80918ccaba57a4fa06557f484cf27b8acbaed4e1a660fac896"
}
}
@@ -1,5 +1,5 @@
{
"manifest_id": "ecm_880356f1ceceb9e29b43e3e89f7a326aa83132e1e1fae0573245848ba714f404",
"manifest_id": "ecm_82b2f9406099c2d152fcfa020c3acb23deb22d82405d42a610553e2bd0da94cc",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "97dc167b6e0d41ac69953501c96a918319fa6e4b3be0affe2fc2119294e7101d"
"sha256": "77c5acdd9315573e4a1a0843e4ca643626674d2872729fd5db491261d386f110"
}
],
"claims": [
+1 -1
View File
@@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi
metadata:
version: "27"
tool_count: 133
catalog_digest: "83b4c3445ed0ec003c0aeefa3188f123d9ae319484f8b1e2b611d2fe278c9c72"
catalog_digest: "9b4e896d2585645f1ed1f9f3eef270cf798f08035c70091178b7bd6119b22bf5"
---
# REA
@@ -4,16 +4,21 @@ import type { ToolContract } from "../toolContracts.js";
import { managedOutputSchemas } from "../toolOutputSchemas.js";
import { toolContractMetadata } from "../toolEffects.js";
import { requireOutputSchema } from "../toolOutputSchemaPrimitives.js";
import { isAbsoluteLocalPath } from "../../domain/localPath.js";
/** Exact caller boundary for execution-free PE/CLI triage and identity. */
const managedTargetInputSchema = z.object({
path: z
.string()
.min(1)
.optional()
.refine(isAbsoluteLocalPath, {
message:
"path must be an absolute local filesystem path (for example /tmp/Example.dll or C:\\Example.dll)",
})
.describe(
"Explicit managed PE/CLI path; omit to reuse the previously selected managed target",
),
"Explicit absolute managed PE/CLI path; omit to reuse the previously selected managed target",
)
.optional(),
});
export const managedArtifactInputSchema = managedTargetInputSchema;
+15 -2
View File
@@ -1,7 +1,20 @@
import { executableFormatHintSchema } from "../domain/dosCom.js";
import { isAbsoluteLocalPath } from "../domain/localPath.js";
import { z } from "zod";
import { analysisProviderSelectorSchema } from "./providerSelection.js";
/** Caller-supplied analysis snapshot path; must name its destination absolutely. */
const snapshotPathSchema = z
.string()
.min(1)
.refine(isAbsoluteLocalPath, {
message:
"snapshot_path must be an absolute local filesystem path (for example /tmp/rea/analysis.json or C:\\rea\\analysis.json)",
})
.describe(
"Absolute local filesystem path for the analysis snapshot; relative paths are rejected.",
);
/** Input contract for opening a target with an optional staged snapshot. */
export const openBinaryInputSchema = z.object({
path: z.string().min(1),
@@ -11,11 +24,11 @@ export const openBinaryInputSchema = z.object({
"Explicit headerless DOS COM interpretation; omission preserves header-based detection",
),
provider_id: analysisProviderSelectorSchema.optional(),
snapshot_path: z.string().min(1).optional(),
snapshot_path: snapshotPathSchema.optional(),
});
/** Input contract for closing a target after an optional atomic snapshot. */
export const closeBinaryInputSchema = z.object({
snapshot_path: z.string().min(1).optional(),
snapshot_path: snapshotPathSchema.optional(),
overwrite: z.boolean().default(false),
});
+121
View File
@@ -0,0 +1,121 @@
import { describe, expect, it } from "vitest";
import { managedArtifactInputSchema } from "./managed/managedToolContracts.js";
import { exportEvidenceBundleInputSchema } from "./sessionToolContracts.js";
import {
closeBinaryInputSchema,
openBinaryInputSchema,
} from "./sessionLifecycleInputs.js";
const openBinaryAbsolute = (snapshot_path: string) =>
openBinaryInputSchema.safeParse({ path: "/tmp/fixture", snapshot_path });
describe("session snapshot path inputs require absolute local paths", () => {
it("rejects a relative snapshot path when opening a binary", () => {
expect(openBinaryAbsolute("relative/analysis.json").success).toBe(false);
});
it("rejects a parent-relative snapshot path when opening a binary", () => {
expect(openBinaryAbsolute("../outside/analysis.json").success).toBe(false);
});
it("accepts an absolute snapshot path when opening a binary", () => {
expect(openBinaryAbsolute("/tmp/rea/analysis.json").success).toBe(true);
});
it("still accepts open_binary without a snapshot path", () => {
expect(
openBinaryInputSchema.safeParse({ path: "/tmp/fixture" }).success,
).toBe(true);
});
it("rejects a relative snapshot path when closing a binary", () => {
expect(
closeBinaryInputSchema.safeParse({ snapshot_path: "analysis.json" })
.success,
).toBe(false);
});
it("accepts an absolute snapshot path when closing a binary", () => {
expect(
closeBinaryInputSchema.safeParse({ snapshot_path: "/tmp/analysis.json" })
.success,
).toBe(true);
});
it("still accepts close_binary without a snapshot path", () => {
expect(closeBinaryInputSchema.safeParse({}).success).toBe(true);
});
});
describe("evidence bundle export path requires an absolute local path", () => {
it("rejects a relative export path", () => {
expect(
exportEvidenceBundleInputSchema.safeParse({ path: "out/bundle.json" })
.success,
).toBe(false);
});
it("rejects a parent-relative export path", () => {
expect(
exportEvidenceBundleInputSchema.safeParse({
path: "../../bundle.json",
}).success,
).toBe(false);
});
it("accepts an absolute export path and keeps overwrite semantics", () => {
expect(
exportEvidenceBundleInputSchema.safeParse({
path: "/tmp/bundle.json",
}),
).toMatchObject({ success: true, data: { overwrite: false } });
});
});
describe("managed target path requires an absolute local path", () => {
it("rejects a relative managed target path", () => {
expect(
managedArtifactInputSchema.safeParse({ path: "bin/app.dll" }).success,
).toBe(false);
});
it("accepts an absolute managed target path", () => {
expect(
managedArtifactInputSchema.safeParse({ path: "/tmp/app.dll" }).success,
).toBe(true);
});
it("still accepts omitting the path to reuse the active target", () => {
expect(managedArtifactInputSchema.safeParse({}).success).toBe(true);
});
});
describe.runIf(process.platform === "win32")(
"windows absolute path forms on windows hosts",
() => {
it("accepts drive-letter backslash snapshot paths", () => {
expect(
closeBinaryInputSchema.safeParse({
snapshot_path: "C:\\rea\\analysis.json",
}).success,
).toBe(true);
});
it("accepts drive-letter forward-slash snapshot paths", () => {
expect(
closeBinaryInputSchema.safeParse({
snapshot_path: "C:/rea/analysis.json",
}).success,
).toBe(true);
});
it("accepts forward-slash export paths", () => {
expect(
exportEvidenceBundleInputSchema.safeParse({
path: "C:/rea/bundle.json",
}).success,
).toBe(true);
});
},
);
+11 -1
View File
@@ -1,5 +1,6 @@
import { z } from "zod";
import { isAbsoluteLocalPath } from "../domain/localPath.js";
import {
sessionOutputSchemas,
requireOutputSchema,
@@ -47,7 +48,16 @@ const session = <Name extends string, Schema extends z.ZodObject>(
/** Session-owned Evidence bundle export options. */
export const exportEvidenceBundleInputSchema = z.strictObject({
path: z.string().min(1),
path: z
.string()
.min(1)
.refine(isAbsoluteLocalPath, {
message:
"path must be an absolute local filesystem path (for example /tmp/rea/evidence.json or C:\\rea\\evidence.json)",
})
.describe(
"Absolute local filesystem path for the exported evidence bundle; relative paths are rejected.",
),
overwrite: z.boolean().default(false),
});
+4
View File
@@ -1,5 +1,9 @@
import { isAbsolute, relative, sep } from "node:path";
/** True when the value is an absolute filesystem path on the host platform. */
export const isAbsoluteLocalPath = (value: string): boolean =>
isAbsolute(value);
/** Test lexical containment on the host platform; callers must resolve symlinks first. */
export const isPathWithinRoot = (root: string, path: string): boolean => {
const value = relative(root, path);
+31
View File
@@ -229,6 +229,37 @@ describe("target-free MCP workflow", () => {
}, 10_000);
});
describe("session filesystem path boundaries over MCP", () => {
it("rejects relative snapshot and export paths with an absolute-path error", async () => {
directory = await createTestTempDirectory("rea-mcp-path-boundary-");
const { mcp, first } = await createSessionMcpHarness(
directory,
provider,
resources,
);
const exported = await mcp.callTool({
name: "export_evidence_bundle",
arguments: { path: "relative-bundle.json" },
});
expect(exported.isError, JSON.stringify(exported.content)).toBe(true);
expect(JSON.stringify(exported.content)).toContain("absolute");
const opened = await mcp.callTool({
name: "open_binary",
arguments: { path: first },
});
expect(opened.isError, JSON.stringify(opened.content)).not.toBe(true);
const closed = await mcp.callTool({
name: "close_binary",
arguments: { snapshot_path: "relative-analysis.json" },
});
expect(closed.isError, JSON.stringify(closed.content)).toBe(true);
expect(JSON.stringify(closed.content)).toContain("absolute");
await mcp.callTool({ name: "close_binary", arguments: {} });
}, 10_000);
});
describe("process residuals over MCP", () => {
it("records process residuals linked to capture Evidence", async () => {
if (!(await probeProcessCaptureCapability()).available) return;