refactor(tools): remove arbitrary workflow ceilings

This commit is contained in:
morluto
2026-09-29 17:36:26 +08:00
parent 6c68ff565f
commit 5f73cb061b
28 changed files with 184 additions and 195 deletions
+2 -9
View File
@@ -683,10 +683,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
facts.addProperty("external", symbol.isExternal());
facts.addProperty("type", normalizedSymbolType(symbol));
facts.addProperty("source", normalizedSource(symbol.getSource()));
addBounded(
result,
new InventoryItem(address, symbol.getName(true), facts)
);
result.add(new InventoryItem(address, symbol.getName(true), facts));
}
result.sort(INVENTORY_ORDER);
nameInventory = List.copyOf(result);
@@ -761,7 +758,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
instance.isMissingNullTerminator() ? "missing" : "present_or_not_required"
);
facts.addProperty("byte_length", Math.max(0, data.getLength()));
addBounded(result, new InventoryItem(data.getAddress(), value, facts));
result.add(new InventoryItem(data.getAddress(), value, facts));
}
result.sort(INVENTORY_ORDER);
stringInventory = List.copyOf(result);
@@ -1687,10 +1684,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
return source.name().toLowerCase(Locale.ROOT);
}
private static void addBounded(List<InventoryItem> destination, InventoryItem item) {
destination.add(item);
}
private static ValueMatcher literalMatcher(String pattern, boolean caseSensitive) {
String needle = caseSensitive ? pattern : pattern.toLowerCase(Locale.ROOT);
return value -> {
-7
View File
@@ -159,12 +159,6 @@ const registerTraceCommand = (cli: CliInstance, logger: Logger): void => {
.boolean()
.default(false)
.describe("Match the query with exact letter case"),
limit: z
.number()
.int()
.min(1)
.default(10_000)
.describe("Maximum matching results to return"),
snapshot: z
.string()
.min(1)
@@ -183,7 +177,6 @@ const registerTraceCommand = (cli: CliInstance, logger: Logger): void => {
{
query: args.query,
case_sensitive: options.caseSensitive,
limit: options.limit,
},
directAnalysisOptions(logger, options.snapshot, options.provider),
),
+2 -2
View File
@@ -173,7 +173,7 @@ export const OFFICIAL_TOOL_CONTRACTS = [
),
official(
"list_strings",
"List every provider-defined string, or filter to one address, as address/value pairs. Ghidra reports charset, missing-terminator status, byte length, and explicit value truncation.",
"List every provider-defined string, or filter to one address, as address/value pairs. Ghidra also reports encoding, terminator status, and byte length.",
z.object({ document, address: optionalAddress }),
),
official(
@@ -293,7 +293,7 @@ export const OFFICIAL_TOOL_CONTRACTS = [
/** Closed provider operation names exposed by direct analysis adapters. */
export type OfficialToolName = (typeof OFFICIAL_TOOL_CONTRACTS)[number]["name"];
/** Bounded workflows composed from one or more bridge operations. */
/** Workflow tools composed from one or more provider operations. */
export const ENHANCED_TOOL_CONTRACTS = [
enhanced(
"get_objc_classes",
+13 -1
View File
@@ -76,6 +76,18 @@ const bookmarkFacetSchema = z.discriminatedUnion("state", [
}),
]);
const addressedString = z.object({
address: z.string(),
value: z.string(),
string: z
.object({
encoding: z.string().min(1),
termination: z.enum(["missing", "present_or_not_required"]),
byte_length: z.number().int().min(0),
})
.optional(),
});
/** Exact structured-content schemas shared by direct analysis providers. */
export const officialOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
address_name: resultOf(nullableText),
@@ -90,7 +102,7 @@ export const officialOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
list_names: resultOf(z.array(addressedValue)),
list_procedures: resultOf(z.array(addressedValue)),
list_segments: segmentOutput,
list_strings: resultOf(z.array(addressedValue)),
list_strings: resultOf(z.array(addressedString)),
next_address: resultOf(z.string()),
prev_address: resultOf(z.string()),
procedure_address: resultOf(z.string()),
+1 -1
View File
@@ -31,7 +31,7 @@ const PROPERTY_DESCRIPTIONS: Readonly<Record<string, string>> = {
result: "Primary structured result returned by this operation.",
left: "Left-hand input used for comparison or differential execution.",
limits:
"Bounded resource-consumption and result-size limits for this operation.",
"Resource budgets that bound execution and retained output for this operation.",
mode: "Operation mode that selects the requested behavior.",
overwrite: "Whether an existing destination may be replaced.",
path: "Local filesystem path used by this operation.",
+17 -6
View File
@@ -5,6 +5,7 @@ import {
analysisProfilesEqual,
createAnalysisProfile,
} from "./analysisProfile.js";
import type { JsonValue } from "./jsonValue.js";
const PROVIDER = {
id: "fixture",
@@ -58,15 +59,25 @@ describe("analysis profile commitments", () => {
expect(analysisProfileSchema.parse(profile)).toEqual(profile);
});
it("rejects digest tampering and structurally unsafe parameters", () => {
it("accepts deep, large, and long-string profile parameters", () => {
let nested: JsonValue = "leaf";
for (let index = 0; index < 24; index += 1)
nested = { [`level-${String(index)}`]: nested };
const parameters = {
nested,
entries: Array.from({ length: 5_000 }, (_, index) => index),
long_value: "x".repeat(20 * 1024),
};
const profile = createAnalysisProfile(PROVIDER, parameters);
expect(analysisProfileSchema.parse(profile)).toEqual(profile);
});
it("rejects digest tampering", () => {
const profile = createAnalysisProfile(PROVIDER, { loader: "default" });
expect(() =>
analysisProfileSchema.parse({ ...profile, digest: "0".repeat(64) }),
).toThrow(/digest/u);
expect(() =>
createAnalysisProfile(PROVIDER, {
oversized: "x".repeat(65 * 1024),
}),
).toThrow(/structural limits/u);
});
});
+1 -12
View File
@@ -3,14 +3,8 @@ import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { z } from "zod";
import { isJsonWithinLimits } from "./jsonLimits.js";
import { jsonObjectSchema, type JsonValue } from "./jsonValue.js";
const PROFILE_JSON_LIMITS = Object.freeze({
maxDepth: 16,
maxStringLength: 16 * 1024,
maxNodes: 4_096,
});
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
/** Concrete provider identity committed by an analysis profile. */
@@ -20,14 +14,9 @@ export const committedProviderSchema = z.object({
version: z.string().min(1),
});
const boundedParametersSchema = jsonObjectSchema.refine(
(parameters) => isJsonWithinLimits(parameters, PROFILE_JSON_LIMITS),
{ message: "Analysis profile parameters exceed structural limits" },
);
const unsignedAnalysisProfileSchema = z.object({
provider: committedProviderSchema,
parameters: boundedParametersSchema,
parameters: jsonObjectSchema,
});
/** Canonical, provider-neutral commitment to analysis-affecting semantics. */
@@ -79,6 +79,53 @@ const createLargeNativeExportGraph = (): JavaScriptApplicationGraph => {
});
};
it("accepts deeply nested canonical application properties", () => {
let nested: unknown = "leaf";
for (let index = 0; index < 12; index += 1)
nested = { [`level-${String(index)}`]: nested };
const node = createJavaScriptApplicationNode({
kind: "native-export",
identity: {
strategy: "canonical-path",
stability: "artifact-version",
artifact_sha256: APPLICATION_GRAPH_DIGESTS.nativeAddon,
path: "native/deep-export",
},
observations: [
{
label: "deep export",
properties: { nested },
evidence: artifactEvidence(
APPLICATION_GRAPH_DIGESTS.nativeAddon,
"native/deep-export",
"native-analysis-provider",
),
},
],
});
const graph = createJavaScriptApplicationGraph({
schema: "JavaScriptApplicationGraph",
root_node_ids: [node.node_id],
nodes: [node],
edges: [],
coverage: {
status: "complete",
truncated: false,
omitted_count: 0,
limits: [],
},
limitations: [],
});
expect(graph.nodes[0]?.observations[0]?.properties).toEqual({ nested });
expect(
parseJavaScriptApplicationGraph(
JSON.parse(serializeJavaScriptApplicationGraph(graph)),
),
).toEqual(graph);
});
describe("JavaScript Application Graph", () => {
it("defines the complete provider-neutral node and relation vocabulary", () => {
expect(JAVASCRIPT_APPLICATION_NODE_KINDS).toEqual([
@@ -5,7 +5,6 @@ import {
applicationGraphEvidenceSchema,
applicationNodeIdentitySchema,
} from "./javascriptApplicationEvidenceSchemas.js";
import { isJsonWithinDepth } from "./jsonLimits.js";
import { jsonValueSchema } from "./jsonValue.js";
const boundedTextSchema = z.string().min(1);
@@ -70,15 +69,10 @@ export const JAVASCRIPT_APPLICATION_RELATIONS = [
/** Provider-neutral relation categories. */
const applicationRelationSchema = z.enum(JAVASCRIPT_APPLICATION_RELATIONS);
const applicationPropertiesSchema = z
.record(z.string().min(1), jsonValueSchema)
.superRefine((properties, context) => {
if (!isJsonWithinDepth(properties, 6))
context.addIssue({
code: "custom",
message: "Application graph properties exceed the supported depth",
});
});
const applicationPropertiesSchema = z.record(
z.string().min(1),
jsonValueSchema,
);
/** One node observation before its semantic identifier is derived. */
const applicationNodeObservationInputSchema = z.strictObject({
+1 -1
View File
@@ -9,7 +9,7 @@ const nodeIdSchema = z.string().regex(/^jag_node_[a-f0-9]{64}$/u);
const edgeIdSchema = z.string().regex(/^jag_edge_[a-f0-9]{64}$/u);
const boundedTextSchema = z.string().min(1);
/** Literal starting point for one bounded application trace. */
/** Literal starting point for one application feature trace. */
const applicationFeatureSeedSchema = z.strictObject({
kind: z.enum([
"node-id",
@@ -96,21 +96,23 @@ const node = (
kind: (typeof JAVASCRIPT_SEMANTIC_NODE_KINDS)[number],
role: string,
properties: Record<string, string | number | boolean | null> = {},
functionNodeId: string | null = null,
modulePath = "bundle.js",
options: {
readonly functionNodeId?: string | null;
readonly modulePath?: string;
} = {},
): JavaScriptSemanticGraphNode =>
createJavaScriptSemanticGraphNode({
kind,
identity: {
artifact_sha256: SHA,
module_path: modulePath,
module_path: options.modulePath ?? "bundle.js",
source_range: {
start: { line: 1, column: role.length },
end: { line: 1, column: role.length + 1 },
},
role_key: role,
},
function_node_id: functionNodeId,
function_node_id: options.functionNodeId ?? null,
application_node_ids: [],
label: role,
properties,
@@ -120,7 +122,7 @@ const node = (
it("accepts semantic module paths longer than the former schema ceiling", () => {
const modulePath = `${"segment/".repeat(600)}entry.js`;
expect(
node("module", "long-path", {}, null, modulePath).identity.module_path,
node("module", "long-path", {}, { modulePath }).identity.module_path,
).toBe(modulePath);
});
+1 -10
View File
@@ -1,7 +1,6 @@
import { z } from "zod";
import { applicationGraphEvidenceSchema } from "./javascriptApplicationEvidenceSchemas.js";
import { isJsonWithinDepth } from "./jsonLimits.js";
import { jsonValueSchema } from "./jsonValue.js";
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
@@ -190,15 +189,7 @@ const sourceRangeSchema = z
});
});
const semanticPropertiesSchema = z
.record(z.string().min(1), jsonValueSchema)
.superRefine((properties, context) => {
if (!isJsonWithinDepth(properties, 6))
context.addIssue({
code: "custom",
message: "Semantic properties exceed the supported depth",
});
});
const semanticPropertiesSchema = z.record(z.string().min(1), jsonValueSchema);
const semanticNodeIdentitySchema = z.strictObject({
artifact_sha256: digestSchema,
@@ -15,6 +15,7 @@ import {
type JavaScriptSemanticGraphNode,
} from "./javascriptSemanticGraph.js";
import { queryJavaScriptSemanticGraph } from "./javascriptSemanticQuery.js";
import type { JsonValue } from "./jsonValue.js";
const SHA = "a".repeat(64);
const JAG_ID = `jag_${"b".repeat(64)}`;
@@ -91,7 +92,7 @@ const unknownEvidence = (): ApplicationGraphEvidence => ({
const node = (
kind: (typeof JAVASCRIPT_SEMANTIC_NODE_KINDS)[number],
role: string,
properties: Record<string, string | number | boolean | null> = {},
properties: Record<string, JsonValue> = {},
functionNodeId: string | null = null,
): JavaScriptSemanticGraphNode =>
createJavaScriptSemanticGraphNode({
@@ -319,6 +320,16 @@ it("traverses paths beyond the former fixed depth ceiling", () => {
expect(result.nodes).toHaveLength(74);
});
it("accepts deeply nested semantic properties", () => {
let nested: JsonValue = "leaf";
for (let index = 0; index < 12; index += 1)
nested = { [`level-${String(index)}`]: nested };
const result = node("binding", "deep-properties", { nested });
expect(result.properties).toEqual({ nested });
});
it("keeps relevant dynamic frontiers unknown", () => {
const result = queryJavaScriptSemanticGraph(fixtureGraph(true), {
seed: { kind: "literal", value: "TOKEN" },
-48
View File
@@ -1,48 +0,0 @@
import { describe, expect, it } from "vitest";
import { isJsonWithinDepth, isJsonWithinLimits } from "./jsonLimits.js";
const limits = {
maxDepth: 3,
maxStringLength: 8,
maxNodes: 6,
};
describe("JSON limits", () => {
it("checks depth without rejecting large keys, collections, or strings", () => {
const properties = Object.fromEntries(
Array.from({ length: 1_001 }, (_, index) => [
`${"k".repeat(128)}${index}`,
"v".repeat(4_097),
]),
);
expect(isJsonWithinDepth(properties, 6)).toBe(true);
expect(isJsonWithinDepth({ nested: { value: true } }, 1)).toBe(false);
});
it("traverses nested object and array values", () => {
expect(isJsonWithinLimits({ one: ["two", { three: 3 }] }, limits)).toBe(
true,
);
expect(
isJsonWithinLimits({ one: ["two", { three: "too-long-value" }] }, limits),
).toBe(false);
});
it("enforces depth, node, and object-key limits", () => {
expect(
isJsonWithinLimits(
{ one: { two: { three: null } } },
{
...limits,
maxDepth: 2,
},
),
).toBe(false);
expect(isJsonWithinLimits([1, 2, 3], { ...limits, maxNodes: 3 })).toBe(
false,
);
expect(isJsonWithinLimits({ "too-long-key": true }, limits)).toBe(false);
});
});
-51
View File
@@ -1,51 +0,0 @@
/** Structural limits applied while traversing an unknown JSON value. */
export interface JsonLimits {
readonly maxDepth: number;
readonly maxStringLength: number;
readonly maxNodes: number;
}
/** Return whether a JSON value fits a depth constraint without size quotas. */
export const isJsonWithinDepth = (root: unknown, maxDepth: number): boolean => {
const pending: Array<{ readonly value: unknown; readonly depth: number }> = [
{ value: root, depth: 0 },
];
while (pending.length > 0) {
const current = pending.pop();
if (current === undefined) break;
if (current.depth > maxDepth) return false;
if (typeof current.value !== "object" || current.value === null) continue;
for (const value of Object.values(current.value))
pending.push({ value, depth: current.depth + 1 });
}
return true;
};
/** Return whether every node, key, and string fits within the supplied limits. */
export const isJsonWithinLimits = (
root: unknown,
limits: JsonLimits,
): boolean => {
const pending: Array<{ readonly value: unknown; readonly depth: number }> = [
{ value: root, depth: 0 },
];
let nodes = 0;
while (pending.length > 0) {
const current = pending.pop();
if (current === undefined) break;
nodes += 1;
if (nodes > limits.maxNodes || current.depth > limits.maxDepth)
return false;
if (
typeof current.value === "string" &&
current.value.length > limits.maxStringLength
)
return false;
if (typeof current.value !== "object" || current.value === null) continue;
for (const [key, value] of Object.entries(current.value)) {
if (key.length > limits.maxStringLength) return false;
pending.push({ value, depth: current.depth + 1 });
}
}
return true;
};
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -4,7 +4,7 @@ import { AnalysisInputError, AnalysisOutputError } from "../domain/errors.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
/** Read-only direct operations admitted by the Ghidra v1 adapter. */
/** Read-only direct inventory operations admitted by the Ghidra adapter. */
export const GHIDRA_INVENTORY_OPERATIONS = [
"address_name",
"list_documents",
@@ -170,7 +170,7 @@ const resultSchemas = {
search_strings: z.array(searchItem),
} satisfies Readonly<Record<GhidraInventoryOperation, z.ZodType>>;
/** Require exact, bounded Java-bridge output before creating Evidence. */
/** Validate exact Java-bridge output before creating Evidence. */
export const parseGhidraInventoryResult = (
operation: GhidraInventoryOperation,
value: JsonValue,
+6 -6
View File
@@ -14,11 +14,11 @@ import { err, ok } from "../domain/result.js";
import { GhidraSessionError } from "./GhidraSessionError.js";
import { silentLogger } from "../logger.js";
const INSTALL = "/opt/ghidra_12.1.2_PUBLIC";
const INSTALL = "/opt/ghidra_12.1.4_PUBLIC";
const installationHost = (): GhidraInstallationHost => ({
platform: "linux",
architecture: "x64",
readText: () => "application.version=12.1.2\n",
readText: () => "application.version=12.1.4\n",
executable: () => true,
probeJava: () => ({
version: "21.0.11",
@@ -85,7 +85,7 @@ describe("Ghidra provider", () => {
status: "available",
diagnostics: {
install_dir: INSTALL,
provider_version: "12.1.2",
provider_version: "12.1.4",
java_version: "21.0.11",
},
});
@@ -255,7 +255,7 @@ describe("Ghidra client projection", () => {
expect(resolved.ok).toBe(true);
if (!resolved.ok || resolved.value.profile === null) return;
expect(resolved.value.profile).toMatchObject({
provider: { id: "ghidra", name: "Ghidra", version: "12.1.2" },
provider: { id: "ghidra", name: "Ghidra", version: "12.1.4" },
parameters: {
import_mode: "ephemeral-read-only",
analyzer_preset: "ghidra-default",
@@ -289,7 +289,7 @@ describe("Ghidra client projection", () => {
provider: {
id: "ghidra",
name: "Ghidra",
version: "12.1.2",
version: "12.1.4",
},
analysisProfile: resolved.value.profile,
result: [{ address: "0x1000", value: "fixture_main" }],
@@ -411,7 +411,7 @@ const sessionInfo = () => ({
name: "REA Ghidra bridge" as const,
run_id: "11111111-1111-4111-8111-111111111111",
profile_digest: "a".repeat(64),
provider: { id: "ghidra" as const, version: "12.1.2" },
provider: { id: "ghidra" as const, version: "12.1.4" },
read_only: true as const,
analysis_complete: true,
analysis_timed_out: false,
+1 -4
View File
@@ -24,10 +24,7 @@ export const workerRequest = (prepared: PreparedReplayPlan) => ({
limits: {
resultDepth: prepared.publicPlan.limits.result_depth,
resultNodes: prepared.publicPlan.limits.result_nodes,
exceptionBytes: Math.min(
64 * 1024,
prepared.publicPlan.limits.output_bytes,
),
exceptionBytes: prepared.publicPlan.limits.output_bytes,
},
});
@@ -186,6 +186,10 @@ describe("canonical CLI catalog", () => {
expect(cliCommandOptionNames(cli, name)).toContain("provider");
}
});
it("does not impose a private result limit on feature tracing", () => {
expect(cliCommandOptionNames(createCli(), "trace")).not.toContain("limit");
});
});
describe("canonical product catalog drift", () => {
@@ -27,7 +27,7 @@ it("reads local source maps as part of static artifact analysis", async () => {
);
});
it("does not follow symlinks and analyzes text beyond the former file-size cap", async () => {
it("does not follow symlinks and analyzes each local source file", async () => {
const root = await fixtureDirectory();
const outside = await createTestTempDirectory("rea-javascript-outside-");
const outsideFile = join(outside, "secret.js");
@@ -37,22 +37,27 @@ it("does not follow symlinks and analyzes text beyond the former file-size cap",
);
await symlink(outsideFile, join(root, "escape.js"));
const baseline = await reconstructJavaScriptArtifact({ input_path: root });
const oversizedSource = `// ${"private-marker-".repeat(600_000)}\nfetch("https://inside.invalid");\n`;
await writeFile(join(root, "oversized.js"), oversizedSource);
const additionalSource = Array.from(
{ length: 1_000 },
(_value, index) => `export const value${String(index)} = ${String(index)};`,
).join("\n");
await writeFile(join(root, "additional.js"), additionalSource);
const result = await reconstructJavaScriptArtifact({ input_path: root });
const encoded = JSON.stringify(result);
expect(result.statistics.text_bytes_read).toBeGreaterThan(8 * 1024 * 1024);
expect(result.graph.coverage).toMatchObject({
status: "complete",
truncated: false,
});
expect(result.statistics.parsed_javascript_files).toBe(
baseline.statistics.parsed_javascript_files + 1,
);
expect(result.statistics.text_bytes_read).toBe(
baseline.statistics.text_bytes_read + Buffer.byteLength(additionalSource),
);
expect(encoded).not.toContain(outsideFile);
expect(encoded).not.toContain("outside-secret.invalid");
expect(result.statistics.visited_ast_nodes).toBeGreaterThan(
baseline.statistics.visited_ast_nodes,
);
});
it("keeps malformed JavaScript, package metadata, and source maps as explicit unknowns", async () => {
+3 -3
View File
@@ -18,9 +18,9 @@ describe("Ghidra MCP evidence parity", () => {
);
expect(listed).toMatchObject({
operation: "list_procedures",
provider: { id: "ghidra", name: "Ghidra", version: "12.1.2" },
provider: { id: "ghidra", name: "Ghidra", version: "12.1.4" },
analysis_profile: {
provider: { id: "ghidra", version: "12.1.2" },
provider: { id: "ghidra", version: "12.1.4" },
parameters: {
import_mode: "ephemeral-read-only",
analyzer_preset: "ghidra-default",
@@ -78,7 +78,7 @@ describe("Ghidra MCP evidence parity", () => {
);
expect(pseudocode).toMatchObject({
operation: "procedure_pseudo_code",
provider: { id: "ghidra", version: "12.1.2" },
provider: { id: "ghidra", version: "12.1.4" },
normalized_result: expect.stringContaining("return 42"),
limitations: expect.arrayContaining([
expect.stringContaining("not text-equivalent to Hopper"),
+3 -3
View File
@@ -22,7 +22,7 @@ import {
ghidraReferenceEdge,
} from "../../../src/domain/hopperValues.fixture.js";
const INSTALL = "/opt/ghidra_12.1.2_PUBLIC";
const INSTALL = "/opt/ghidra_12.1.4_PUBLIC";
export const connectGhidraMcp = async (name: string) => {
const calls: GhidraOperation[] = [];
@@ -98,7 +98,7 @@ export const sessionEvidence = (session: BinarySession, value: unknown) => {
const installationHost = (): GhidraInstallationHost => ({
platform: "linux",
architecture: "x64",
readText: () => "application.version=12.1.2\n",
readText: () => "application.version=12.1.4\n",
executable: () => true,
probeJava: () => ({
version: "21.0.11",
@@ -113,7 +113,7 @@ const sessionInfo = (profileDigest: string, targetSha256: string) => ({
name: "REA Ghidra bridge" as const,
run_id: "11111111-1111-4111-8111-111111111111",
profile_digest: profileDigest,
provider: { id: "ghidra" as const, version: "12.1.2" },
provider: { id: "ghidra" as const, version: "12.1.4" },
read_only: true as const,
analysis_complete: true,
analysis_timed_out: false,
+1 -1
View File
@@ -33,7 +33,7 @@ describe("Ghidra MCP capability routing", () => {
result: {
open: true,
analysis_provider_binding: {
provider: { id: "ghidra", version: "12.1.2" },
provider: { id: "ghidra", version: "12.1.4" },
},
tool_availability: expect.arrayContaining([
expect.objectContaining({
+22
View File
@@ -82,6 +82,28 @@ describe("read_bytes contract", () => {
});
});
describe("list_strings contract", () => {
it("describes optional provider string metadata inline", () => {
const contract = OFFICIAL_TOOL_CONTRACTS.find(
({ name }) => name === "list_strings",
);
expect(
contract?.outputSchema.shape.result.safeParse([
{
address: "0x1000",
value: "coffee",
string: {
encoding: "UTF-8",
termination: "present_or_not_required",
byte_length: 6,
},
},
{ address: "0x1008", value: "beans" },
]).success,
).toBe(true);
});
});
afterEach(async () => {
await Promise.all(
resources.splice(0).map(async (resource) => resource.close()),
@@ -23,7 +23,7 @@ const fixturePath = fileURLToPath(
new URL("../../../fixtures/fakeGhidra.mjs", import.meta.url),
);
const PROFILE_DIGEST = "a".repeat(64);
const PROVIDER_VERSION = "12.1.2";
const PROVIDER_VERSION = "12.1.4";
const TARGET_SHA256 = createHash("sha256")
.update(readFileSync(fixturePath))
.digest("hex");
@@ -98,7 +98,7 @@ describe("Ghidra headless launcher", () => {
const command = ghidraHeadlessCommand({
platform: "win32",
analyzeHeadlessPath:
"C:\\Program Files\\Ghidra 12.1.2\\support\\analyzeHeadless.bat",
"C:\\Program Files\\Ghidra 12.1.4\\support\\analyzeHeadless.bat",
arguments: [
"C:\\REA Runtime\\project",
"rea-project",
@@ -116,7 +116,7 @@ describe("Ghidra headless launcher", () => {
"/v:off",
"/s",
"/c",
'""C:\\Program Files\\Ghidra 12.1.2\\support\\analyzeHeadless.bat" "C:\\REA Runtime\\project" "rea-project" "-import" "C:\\REA Runtime\\target.exe""',
'""C:\\Program Files\\Ghidra 12.1.4\\support\\analyzeHeadless.bat" "C:\\REA Runtime\\project" "rea-project" "-import" "C:\\REA Runtime\\target.exe""',
],
});
});
@@ -159,7 +159,7 @@ describe("Ghidra headless launcher", () => {
runId: "d6fcbb66-e829-4ff6-a535-0035aec63139",
targetPath: "/tmp/fixture",
targetSha256: "b".repeat(64),
providerVersion: "12.1.2",
providerVersion: "12.1.4",
profileDigest: "a".repeat(64),
});
expect(launched.ok).toBe(true);
@@ -64,6 +64,23 @@ const request = async (
limits: { resultDepth: 16, resultNodes: 10_000, exceptionBytes: 4096 },
});
it("uses the caller's full output budget for exception text", async () => {
const message = "x".repeat(70_000);
const workerRequest = await request("exception.mjs", "esm", "default", [
message,
]);
workerRequest.limits.exceptionBytes = 80_000;
const result = await runWorker(workerRequest);
expect(result.code).toBe(0);
const response = JSON.parse(result.stdout) as {
readonly left: readonly {
readonly exception: { readonly message: string };
}[];
};
expect(response.left[0]?.exception.message).toBe(`fixture:${message}`);
});
describe("disposable JavaScript replay worker", () => {
it("rejects malformed runtime-hop requests before loading modules", async () => {
const result = await runWorker({