diff --git a/docs/product-catalog.json b/docs/product-catalog.json index 061f98abef..01f67a6b39 100644 --- a/docs/product-catalog.json +++ b/docs/product-catalog.json @@ -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" } } diff --git a/docs/verification/managed-conformance-manifest.json b/docs/verification/managed-conformance-manifest.json index 6d124d44a1..d427f4a886 100644 --- a/docs/verification/managed-conformance-manifest.json +++ b/docs/verification/managed-conformance-manifest.json @@ -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": [ diff --git a/skills/reverse-engineer-anything/SKILL.md b/skills/reverse-engineer-anything/SKILL.md index f7cab0f3a1..fe96353d46 100644 --- a/skills/reverse-engineer-anything/SKILL.md +++ b/skills/reverse-engineer-anything/SKILL.md @@ -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 diff --git a/src/contracts/managed/managedToolContracts.ts b/src/contracts/managed/managedToolContracts.ts index dc57023227..56071c4fce 100644 --- a/src/contracts/managed/managedToolContracts.ts +++ b/src/contracts/managed/managedToolContracts.ts @@ -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; diff --git a/src/contracts/sessionLifecycleInputs.ts b/src/contracts/sessionLifecycleInputs.ts index fd0a1d58db..7a62d107ee 100644 --- a/src/contracts/sessionLifecycleInputs.ts +++ b/src/contracts/sessionLifecycleInputs.ts @@ -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), }); diff --git a/src/contracts/sessionPathInputs.test.ts b/src/contracts/sessionPathInputs.test.ts new file mode 100644 index 0000000000..4d4755418c --- /dev/null +++ b/src/contracts/sessionPathInputs.test.ts @@ -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); + }); + }, +); diff --git a/src/contracts/sessionToolContracts.ts b/src/contracts/sessionToolContracts.ts index 81ce1836c8..ebd7983bd1 100644 --- a/src/contracts/sessionToolContracts.ts +++ b/src/contracts/sessionToolContracts.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { isAbsoluteLocalPath } from "../domain/localPath.js"; import { sessionOutputSchemas, requireOutputSchema, @@ -47,7 +48,16 @@ const session = ( /** 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), }); diff --git a/src/domain/localPath.ts b/src/domain/localPath.ts index 43b491cf98..9248de4d71 100644 --- a/src/domain/localPath.ts +++ b/src/domain/localPath.ts @@ -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); diff --git a/tests/boundary/mcp/sessionMcp.test.ts b/tests/boundary/mcp/sessionMcp.test.ts index 5ba1562a2a..0f20b3a5fe 100644 --- a/tests/boundary/mcp/sessionMcp.test.ts +++ b/tests/boundary/mcp/sessionMcp.test.ts @@ -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;