chore: merge main into feat/dyld-shared-cache
This commit is contained in:
commit
03bbecd941
10 files changed
+277
-12
No files matched your search
@@ -718,9 +718,9 @@
|
||||
"mcp_prompts": 6
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "ef0bc1222f6236c035a576f4ba1317ed3a838d2052f672106cc00c07632298b3",
|
||||
"tools_sha256": "8e69e2a116fc08a18958869f9a8266e5dca980dece2a42748089f1240a603c7e",
|
||||
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
|
||||
"combined_sha256": "63f9e1c6471f3e88bcf5b0faa66717499e9c2e07d9cef9289f8947c4f2c32e41",
|
||||
"combined_sha256": "f1dd4e4a6913f65967dfa15daeeaee26994cecf06d94ae6b3bfff1047657f634",
|
||||
"providers_sha256": "bd826b2f7aa112a8d9ad83fe5c7e3a46534aae56c71399cda72413ce62f5e554"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"manifest_id": "ecm_9347855ec2e9c25646961e61d955e25774a2c4142e52dd9fc31b1abd272aa615",
|
||||
"manifest_id": "ecm_4b6bff63b1c4bbcb5b228437c78d2a65050d20cbf314b6bbcceada66b5a410d4",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
@@ -13,7 +13,7 @@
|
||||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "9b9765cc8f5a1d761f9d9fa21f6cbafd5ca1a2e0e7f83f706b5b72c903bf8c3a"
|
||||
"sha256": "677806b859d650d63b160f788f2816eaa7f1b6fec3637b954c3af7f0fc9b9f59"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi
|
||||
metadata:
|
||||
version: "33"
|
||||
tool_count: 140
|
||||
catalog_digest: "63f9e1c6471f3e88bcf5b0faa66717499e9c2e07d9cef9289f8947c4f2c32e41"
|
||||
catalog_digest: "f1dd4e4a6913f65967dfa15daeeaee26994cecf06d94ae6b3bfff1047657f634"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
|
||||
@@ -113,6 +114,12 @@ const parseCaptureEvidence = (input: unknown) => {
|
||||
try {
|
||||
evidence = parseEvidence(input);
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof z.ZodError)
|
||||
throw new AnalysisInputError(
|
||||
"compare_process_captures",
|
||||
{ cause },
|
||||
projectInputIssues(cause.issues, input),
|
||||
);
|
||||
throw invalidCaptureEvidence();
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { MAX_JSON_DEPTH } from "../jsonValue.js";
|
||||
import { projectKeyedArchive } from "./keyedArchive.js";
|
||||
|
||||
const deepValue = (depth: number): unknown => {
|
||||
let value: unknown = 1;
|
||||
for (let index = 0; index < depth; index += 1) value = { nested: value };
|
||||
return value;
|
||||
};
|
||||
|
||||
describe("projectKeyedArchive input bound", () => {
|
||||
it("rejects an over-deep archive value with a validation error instead of crashing", () => {
|
||||
let error: unknown;
|
||||
try {
|
||||
projectKeyedArchive(deepValue(MAX_JSON_DEPTH + 1), {
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
});
|
||||
} catch (caught) {
|
||||
error = caught;
|
||||
}
|
||||
expect(error).toBeInstanceOf(z.ZodError);
|
||||
expect(error).not.toBeInstanceOf(RangeError);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { describe, expect } from "vitest";
|
||||
import { createAnalysisProfile } from "./analysisProfile.js";
|
||||
import type { BinaryTarget } from "./binaryTarget.js";
|
||||
import { createEvidence, evidenceSchema, parseEvidence } from "./evidence.js";
|
||||
import { MAX_JSON_DEPTH } from "./jsonValue.js";
|
||||
import { createEvidenceBundle } from "./evidenceBundle.js";
|
||||
|
||||
const TARGET: BinaryTarget = {
|
||||
@@ -255,3 +256,27 @@ describe("DOS analysis evidence identity", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("evidence parameter depth bound", () => {
|
||||
it("rejects evidence whose parameters nest past the JSON depth limit", () => {
|
||||
const evidence = createEvidence(TARGET, PROVIDER, {
|
||||
operation: "health",
|
||||
parameters: {},
|
||||
result: true,
|
||||
});
|
||||
let value: unknown = 1;
|
||||
for (let index = 0; index <= MAX_JSON_DEPTH; index += 1)
|
||||
value = { nested: value };
|
||||
const result = evidenceSchema.safeParse({
|
||||
...evidence,
|
||||
parameters: { attack: value },
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (result.success) return;
|
||||
expect(
|
||||
result.error.issues.some((issue) =>
|
||||
issue.message.includes("maximum nesting depth"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { jsonObjectSchema, jsonValueSchema } from "./jsonValue.js";
|
||||
import {
|
||||
jsonObjectSchema,
|
||||
jsonValueSchema,
|
||||
MAX_JSON_DEPTH,
|
||||
} from "./jsonValue.js";
|
||||
|
||||
const values = [
|
||||
"text",
|
||||
@@ -35,3 +39,63 @@ it("still parses only JSON values at runtime", () => {
|
||||
for (const value of [undefined, Number.NaN, Infinity, () => 0, [undefined]])
|
||||
expect(jsonValueSchema.safeParse(value).success).toBe(false);
|
||||
});
|
||||
|
||||
const deepObject = (depth: number): unknown => {
|
||||
let value: unknown = 1;
|
||||
for (let index = 0; index < depth; index += 1) value = { nested: value };
|
||||
return value;
|
||||
};
|
||||
|
||||
describe("jsonValueSchema depth bound", () => {
|
||||
it("accepts values nested to the maximum depth", () => {
|
||||
expect(jsonValueSchema.safeParse(deepObject(MAX_JSON_DEPTH)).success).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects values nested past the maximum depth without throwing", () => {
|
||||
const result = jsonValueSchema.safeParse(deepObject(MAX_JSON_DEPTH + 1));
|
||||
expect(result.success).toBe(false);
|
||||
if (result.success) return;
|
||||
expect(result.error.issues[0]?.message).toContain("maximum nesting depth");
|
||||
});
|
||||
|
||||
it.each(["object", "array"])(
|
||||
"rejects the original 10000-level %s crash without throwing",
|
||||
(kind) => {
|
||||
let value: unknown = 1;
|
||||
for (let index = 0; index < 10_000; index += 1)
|
||||
value = kind === "object" ? { nested: value } : [value];
|
||||
const result = jsonValueSchema.safeParse(value);
|
||||
expect(result.success).toBe(false);
|
||||
if (result.success) return;
|
||||
expect(result.error.issues[0]?.message).toContain(
|
||||
"maximum nesting depth",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects cyclic values instead of recursing forever", () => {
|
||||
const cyclic: Record<string, unknown> = {};
|
||||
cyclic.self = cyclic;
|
||||
const result = jsonValueSchema.safeParse(cyclic);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps json semantics for non-JSON values", () => {
|
||||
expect(jsonValueSchema.safeParse(undefined).success).toBe(false);
|
||||
expect(jsonValueSchema.safeParse({ a: Number.NaN }).success).toBe(false);
|
||||
expect(
|
||||
jsonValueSchema.safeParse({ a: Number.POSITIVE_INFINITY }).success,
|
||||
).toBe(false);
|
||||
expect(jsonValueSchema.safeParse({ a: 1n }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts ordinary nested JSON", () => {
|
||||
expect(
|
||||
jsonValueSchema.safeParse({
|
||||
list: [1, "two", null, true, { deep: { further: [{}] } }],
|
||||
}).success,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
+56
-2
@@ -1,10 +1,64 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/** JSON-safe value shared by domain, application, and adapter boundaries. */
|
||||
export const jsonValueSchema = z.json();
|
||||
/**
|
||||
* Maximum nesting depth accepted for JSON values.
|
||||
*
|
||||
* Sits well below the point where the recursive `z.json()` core overflows its
|
||||
* stack (measured between 2000 and 3000 on Node 24) while leaving room for
|
||||
* legitimately deep target-derived trees such as compiled NIB view hierarchies.
|
||||
*/
|
||||
export const MAX_JSON_DEPTH = 1000;
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
const exceedsMaxDepth = (root: unknown): boolean => {
|
||||
// Depth counts edges from the root.
|
||||
const pending: { value: unknown; depth: number }[] = [
|
||||
{ value: root, depth: 0 },
|
||||
];
|
||||
while (pending.length > 0) {
|
||||
const item = pending.pop();
|
||||
if (item === undefined) break;
|
||||
if (item.depth > MAX_JSON_DEPTH) return true;
|
||||
const value = item.value;
|
||||
if (Array.isArray(value)) {
|
||||
for (const child of value)
|
||||
pending.push({ value: child, depth: item.depth + 1 });
|
||||
} else if (isRecord(value)) {
|
||||
for (const key of Object.keys(value))
|
||||
pending.push({ value: value[key], depth: item.depth + 1 });
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* JSON-safe value shared by domain, application, and adapter boundaries.
|
||||
*
|
||||
* An iterative depth guard runs before the recursive `z.json()` core so
|
||||
* hostile nested payloads surface as validation issues instead of RangeErrors.
|
||||
* The guard only refuses over-deep values; ordinary JSON parsing semantics
|
||||
* (including which members `z.json()` keeps) are unchanged.
|
||||
*/
|
||||
const jsonCore = z.json();
|
||||
// Every JSON value satisfies `{}`. Advertise that instead of Zod's
|
||||
// self-referential `$defs` projection, which MCP clients and model APIs that
|
||||
// reject recursive JSON Schemas refuse. Runtime parsing still admits only JSON.
|
||||
// The override sits on the core as well as on the pipe because the standard
|
||||
// JSON Schema projection walks into pipe stages without consulting the outer
|
||||
// node, while the public toJSONSchema projection stops at the pipe.
|
||||
jsonCore._zod.toJSONSchema = () => ({});
|
||||
export const jsonValueSchema = z
|
||||
.unknown()
|
||||
.superRefine((value, context) => {
|
||||
if (exceedsMaxDepth(value))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: `JSON value exceeds maximum nesting depth of ${MAX_JSON_DEPTH}`,
|
||||
});
|
||||
})
|
||||
.pipe(jsonCore);
|
||||
jsonValueSchema._zod.toJSONSchema = () => ({});
|
||||
|
||||
/** JSON object boundary for caller-visible parameter maps. */
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "../../../src/application/process/ProcessCli.js";
|
||||
import { createEvidence } from "../../../src/domain/evidence.js";
|
||||
import { PROCESS_PROVIDER } from "../../../src/application/process/ProcessEvidence.js";
|
||||
import { INVESTIGATION_EXAMPLES } from "../../../src/contracts/investigationExamples.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
const execFileAsync = promisify(execFile);
|
||||
@@ -292,11 +293,62 @@ describe("process CLI evidence validation", () => {
|
||||
const malformed = join(root, "malformed-evidence.json");
|
||||
await writeFile(malformed, "{}");
|
||||
|
||||
expect(await compareProcessEvidenceFiles(malformed, malformed)).toEqual({
|
||||
expect(
|
||||
await compareProcessEvidenceFiles(malformed, malformed),
|
||||
).toMatchObject({
|
||||
error: "Process command failed",
|
||||
category: "invalid_input",
|
||||
message:
|
||||
"Capture evidence is malformed. Create new capture evidence, then try again.",
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
operation: "compare_process_captures",
|
||||
issues: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: ["parameters"],
|
||||
reason: "missing_argument",
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(
|
||||
"reports the JSON depth constraint through the compiled comparison CLI",
|
||||
async () => {
|
||||
const root = await fixture();
|
||||
const left = join(root, "left.json");
|
||||
const right = join(root, "right.json");
|
||||
const input = INVESTIGATION_EXAMPLES.compare_process_captures.input;
|
||||
await writeFile(left, JSON.stringify(input.left));
|
||||
await writeFile(right, JSON.stringify(input.right));
|
||||
const cli = fileURLToPath(
|
||||
new URL("../../../scripts/rea.mjs", import.meta.url),
|
||||
);
|
||||
const cliArguments = [
|
||||
cli,
|
||||
"compare-process-captures",
|
||||
left,
|
||||
right,
|
||||
"--json",
|
||||
];
|
||||
const baseline = await execFileAsync(process.execPath, cliArguments);
|
||||
expect(JSON.parse(baseline.stdout)).toMatchObject({
|
||||
operation: "compare_process_captures",
|
||||
});
|
||||
const nested = '{"nested":'.repeat(10_000) + "1" + "}".repeat(10_000);
|
||||
await writeFile(
|
||||
left,
|
||||
JSON.stringify({
|
||||
...input.left,
|
||||
parameters: { attack: "depth-placeholder" },
|
||||
}).replace('"depth-placeholder"', nested),
|
||||
);
|
||||
await expect(
|
||||
execFileAsync(process.execPath, cliArguments),
|
||||
).rejects.toMatchObject({
|
||||
code: 1,
|
||||
stdout: expect.stringContaining("maximum nesting depth"),
|
||||
});
|
||||
},
|
||||
CLI_INTEGRATION_TIMEOUT_MS,
|
||||
);
|
||||
@@ -21,6 +21,8 @@ import { createServer } from "../../../src/server/createServer.js";
|
||||
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
import { silentLogger } from "../../../src/logger.js";
|
||||
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
|
||||
import { MAX_JSON_DEPTH } from "../../../src/domain/jsonValue.js";
|
||||
import { INVESTIGATION_EXAMPLES } from "../../../src/contracts/investigationExamples.js";
|
||||
import { ok as resultOk } from "../../../src/domain/result.js";
|
||||
import {
|
||||
createSessionMcpHarness,
|
||||
@@ -260,6 +262,40 @@ describe("session filesystem path boundaries over MCP", () => {
|
||||
}, 10_000);
|
||||
});
|
||||
|
||||
describe("json depth bounds over MCP", () => {
|
||||
it("classifies deeply nested evidence parameters as an input validation error", async () => {
|
||||
directory = await createTestTempDirectory("rea-mcp-depth-bound-");
|
||||
const { mcp } = await createSessionMcpHarness(
|
||||
directory,
|
||||
provider,
|
||||
resources,
|
||||
);
|
||||
const input = INVESTIGATION_EXAMPLES.compare_process_captures.input;
|
||||
const baseline = await mcp.callTool({
|
||||
name: "compare_process_captures",
|
||||
arguments: input,
|
||||
});
|
||||
expect(baseline.isError, JSON.stringify(baseline)).not.toBe(true);
|
||||
let value: unknown = 1;
|
||||
for (let index = 0; index <= MAX_JSON_DEPTH; index += 1)
|
||||
value = { nested: value };
|
||||
const result = await mcp.callTool({
|
||||
name: "compare_process_captures",
|
||||
arguments: {
|
||||
left: {
|
||||
...input.left,
|
||||
parameters: { attack: value },
|
||||
},
|
||||
right: input.right,
|
||||
},
|
||||
});
|
||||
const text = JSON.stringify(result.content);
|
||||
expect(result.isError, text).toBe(true);
|
||||
expect(text).toContain("Input validation");
|
||||
expect(text).toContain("maximum nesting depth");
|
||||
}, 10_000);
|
||||
});
|
||||
|
||||
describe("process residuals over MCP", () => {
|
||||
it("records process residuals linked to capture Evidence", async () => {
|
||||
if (!(await probeProcessCaptureCapability()).available) return;
|
||||
|
||||
Reference in new issue
Block a user