Compare commits

..
Author SHA1 Message Date
morluto a7326a0b94 fix(ghidra): preserve value-flow and inventory profiles 2026-11-08 02:23:10 +08:00
morlutoandCursor Agent f912a68417 fix(ghidra): respect Java Unix socket path limits (#8382)
Co-authored-by: Cursor Agent <[email protected]>
2026-11-08 02:15:05 +08:00
morlutoandCursor Agent 6ca4e1368c fix(cli): guide analysts to typed incoming references (#8514)
Co-authored-by: Cursor Agent <[email protected]>
2026-11-08 02:14:55 +08:00
morlutoandCursor Agent 1071ad867c fix(test): report caller target selection coverage (#8406)
* fix(test): report caller target selection coverage

* test(kuna): cover passing caller target selection

---------

Co-authored-by: Cursor Agent <[email protected]>
2026-11-08 02:04:48 +08:00
morluto c06b53644b fix(ghidra)!: preserve facts in inventory searches (#8376)
* fix(ghidra)!: preserve facts in inventory searches

BREAKING CHANGE: Ghidra snapshots made before the search projection profile revision no longer match. Run fresh analysis and save a new snapshot.

* test(ghidra): update raw profile digest goldens
2026-11-08 02:04:30 +08:00
morluto 352a846127 test: reduce brittle assertions and test-only runtime paths (#9289)
* test: focus assertions on behavioral evidence

Prune redundant and wording-coupled checks while retaining diagnostic, parser, recovery, and cleanup coverage.

* refactor: remove test-only wrappers and duplicate checks

Exercise production operations through test fixtures, remove unused adapters, and keep managed-member uncertainty guidance under its existing owner.

* test: focus process validation on diagnostic facts

* refactor: remove test-only runtime helpers

Move semantic evidence fixture setup out of runtime and remove unused Windows settle recovery helpers while checking configured defaults independently.

* test: focus artifact diagnostics on format facts

* test: verify retained cleanup guidance delivery
2026-11-08 02:02:31 +08:00
morluto b64eaf6a5e fix(ghidra): reconcile value-flow scope with main 2026-11-08 01:49:13 +08:00
morlutoandCursor Agent 5679ed66f4 fix(mcp): surface pageable pseudocode workflow (#8481)
* fix(mcp): surface pageable pseudocode workflow

* fix(mcp): clarify pseudocode paging capabilities

---------

Co-authored-by: Cursor Agent <[email protected]>
2026-11-08 01:47:14 +08:00
morluto 3657287d77 chore: merge formatting fix from main 2026-11-07 23:16:53 +08:00
morluto 8d039b682f chore: merge current main 2026-11-07 22:49:02 +08:00
morluto 3f49b8b99b fix(ghidra): reconcile value flow scope with main 2026-11-07 22:48:54 +08:00
morluto d2ab1b852e test(ghidra): verify legacy value-flow snapshot recovery 2026-11-02 16:41:16 +08:00
morluto ecd7e86e44 fix(ghidra): bind value-flow scope to snapshot profile 2026-11-02 16:24:07 +08:00
morluto febd4ee704 fix(native): distinguish value trace policy commitments 2026-11-02 16:05:46 +08:00
morluto c103de06f4 fix(native): retain value trace source limitations 2026-11-02 15:49:18 +08:00
morluto da308f3992 fix(ghidra): qualify memory effect coverage 2026-11-02 15:30:32 +08:00
308 changed files with 1648 additions and 2392 deletions

No files matched your search

@@ -325,7 +325,8 @@ on the first MCP call. It retains the complete observation and returns the
original selection, text availability and length without the body. Use
`normalized_result.parent_evidence_id` with `inspect_analysis_view` and
`view: {kind: "native", facet: "pseudocode", offset: 0, limit: 20000}`;
follow `coverage.next_offset` for later pages. Offsets and limits count UTF-16
follow `coverage.next_offset` for later pages. Choose a smaller limit if one
page exceeds the client or transport budget. Offsets and limits count UTF-16
code units, not lines or tokens. Pages reuse the same observation without
repeating decompilation. Null and empty text remain distinct. Omitted or
`complete` detail preserves full inline delivery. Summary delivery requires a
+5 -2
View File
@@ -1930,6 +1930,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
limitations.add(
"Def-use links do not cross function boundaries and may omit memory aliasing, call side effects, or unresolved indirect flow."
);
limitations.add(
"Memory read/write effects cover high-p-code LOAD and STORE operations only; direct memory address varnodes in other operations are not included. An empty effect list does not establish that no memory is accessed. Recorded instruction references may provide complementary evidence."
);
limitations.add(
"Memory read/write effects include stack and temporary memory operations; they do not identify persistent state or business meaning."
);
@@ -2376,7 +2379,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
if (!match.matched()) {
continue;
}
JsonObject item = resultItem(entry.item, null);
JsonObject item = resultItem(entry.item, "procedure");
if (!match.aliases().isEmpty()) {
JsonArray aliases = new JsonArray();
for (String alias : match.aliases()) aliases.add(alias);
@@ -2391,7 +2394,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
if (!matcher.matches(item.value)) {
continue;
}
items.add(resultItem(item, null));
items.add(resultItem(item, procedureSearch ? "procedure" : "string"));
}
return items;
}
+11 -32
View File
@@ -100,36 +100,6 @@ body are excluded. `procedure_references` returns internal and external edges;
The reference collector's semantics participate in the analysis profile, so
older snapshots cannot replay the former incomplete results under this profile.
For a bounded handoff, request `procedure_references` with `detail: "summary"`.
The summary retains the complete observation and reports the selected procedure,
direction and counts. Page it with `inspect_analysis_view`, using the summary's
`normalized_result.parent_evidence_id` and a view such as
`{"kind":"native-procedure-references","collection":"references","source_address":"0x1234","offset":0,"limit":64}`.
The optional exact source and target filters apply before paging. A row's
`source_index` identifies it in the original retained collection, including
duplicates. Endpoint `kind: "selected-procedure"` refers to the full procedure
in the page summary; `kind: "null"` preserves the original null without asserting
why it was unavailable. `kind: "shared"` carries an index into `items.endpoints`,
where each other procedure object is stored once per page. Use collection
`unresolved_calls` for paged unresolved call sites and their original reasons.
Page coverage describes retained rows, not complete provider discovery or runtime
execution. A 1 MiB projection allowance, also used by other bounded analysis views, can
shorten a requested page; `byte_limited: true` and `coverage.next_offset` identify the
continuation. A single row or summary that cannot fit returns a resource error;
retrieve or export the complete parent Evidence. This allowance has no runtime
override. Omit detail to receive the complete observation inline.
For a one-shot CLI handoff, save the complete Evidence and its linked sources:
```bash
rea procedure-references /absolute/path/to/program my_function --provider ghidra \
--direction incoming --detail summary --evidence_output /absolute/path/to/references.json
```
Use `rea inspect-analysis-view` with that bundle as an `evidence-file` source to
page it without reopening the target. This command requires a provider supporting
`procedure_references`; Kuna currently reports that capability unavailable.
Ghidra regex searches use Java Pattern semantics. If compiling or matching a
pattern exhausts the engine's stack, REA returns a resource constraint and
preserves the active database and annotations. Retry in literal mode or simplify
@@ -574,8 +544,17 @@ The raw Java `is_dead` flag can remain set after Ghidra decodes a live block;
`block_membership` reports actual syntax-tree membership separately.
The provider dossier itself retains up to 3,000 p-code operations, 64 inputs per
operation and 12,000 def-use edges, with explicit omitted counts. A `STORE`
may describe stack memory and does not itself prove persistent state.
operation and 12,000 def-use edges, with explicit omitted counts. Memory effects
cover high-p-code `LOAD` and `STORE` operations; direct memory address varnodes
in other operations are excluded, so an empty effect list does not establish
that the function accesses no memory. `procedure_references` can supply
complementary recorded instruction references, with its own coverage limits. A
`STORE` may describe stack memory and does not itself prove persistent state.
The Ghidra analysis profile commits to this effect scope. Snapshots from before
that commitment are rejected; analyze without the snapshot and save a fresh one.
To keep historical Evidence, copy the old snapshot's `evidence_bundle` object to
a separate JSON file and import that bundle. Import validates the Evidence; it
does not replay the old provider analysis.
Jump-table evidence keeps numeric `mappings`, explicit `default_targets`, and
backing `data_sources` separate. A null case value denotes an unresolved
+1 -1
View File
@@ -184,7 +184,7 @@ REA_KUNA_COMMAND=/absolute/path/to/kuna GHIDRA_INSTALL_DIR=/absolute/path/to/ghi
npm run verify:kuna -- --targets DIR --functions inflate,crc32 --compare-ghidra
```
The lane compiles a small source-owned fixture with the host C compiler and verifies the MCP workflow end to end. `--targets DIR --functions a,b` adds caller-built binaries. `--compare-ghidra` (with `GHIDRA_INSTALL_DIR`) runs those same functions through Ghidra; it requires `--targets` and `--functions` and refuses empty matrices or requested function names missing from every target. See [testing](development/testing.md#kuna-lane) for coverage.
The lane compiles a small source-owned fixture with the host C compiler and verifies the MCP workflow end to end. `--targets DIR --functions a,b` adds caller-built binaries. `--compare-ghidra` (with `GHIDRA_INSTALL_DIR`) runs those same functions through Ghidra; it requires `--targets` and `--functions` and refuses empty matrices or requested function names missing from every target. The report lists examined caller binaries and their matching requested symbols, including inputs omitted from analysis. See [testing](development/testing.md#kuna-lane) for coverage.
Failed runs also report process-lineage evidence and exit unsuccessfully. If provider or transport cleanup is unconfirmed, the report says `cleanup: "incomplete"` and records `retained_workspace`; that directory is preserved for investigation and cleanup retries.
+7
View File
@@ -89,6 +89,13 @@ does not prove text is absent. Both list and search Evidence carry this
limitation. When an address is known, use `read_bytes` and `xrefs` to inspect
bytes and references.
String searches and `trace_feature` retain each matched row's Ghidra-reported
encoding, termination and byte length. These describe the database's defined
data, not independent proof of the original source encoding. Procedure searches
and trace matches likewise retain Ghidra's external and thunk classification.
The Ghidra analysis profile identifies this search projection, so older
snapshots with unqualified search rows cannot replay under the new profile.
## PE debug-symbol coverage
The post-analysis handshake reports Ghidra's PDB Program Information attributes:
+10 -1
View File
@@ -448,7 +448,16 @@ caller-built functions through Ghidra (it refuses to run without `--targets` and
reports latency, sampled process-tree CPU and peak memory, UTF-8 response size,
and goto and warning-marker counts. Those counts are not a correctness judgement,
and model tokens are not measured. Reference timings and response sizes are
reported separately; reference caches expire when the server closes. `--report PATH` writes the JSON report.
reported separately; reference caches expire when the server closes. The report's
`caller_target_selection` retains the selected directory, the original functions
argument, and each examined binary's requested names matched against `nm` text
symbols. `symbol_scan_complete` means the scan of non-`.stripped` entries in
the immediate flavor directories finished; it does not cover other directory
entries. An `inspection_failed` entry and `symbol_scan_complete: false` retain
partial discovery when symbol inspection fails. A missing symbol is not proof
that code is absent, and selection does not establish that a stripped counterpart
was verified; consult `results` for completed variants. `--report PATH` writes
the JSON report.
The first matrix run used Kuna 1.833 and Ghidra 12.1.4 with JDK 21 on macOS
arm64. It covered zlib 1.3.2 `minigzip`, coreutils 9.9 `sort` and the sqlite
@@ -0,0 +1,123 @@
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { readFile, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
import { createAnalysisProfile } from "../../dist/domain/analysisProfile.js";
import {
createAnalysisSnapshotEntry,
parseAnalysisSnapshot,
serializeAnalysisSnapshot,
snapshotBinding,
snapshotTarget,
} from "../../dist/domain/analysisSnapshot.js";
import { createEvidence } from "../../dist/domain/evidence.js";
import { createEvidenceBundle } from "../../dist/domain/evidenceBundle.js";
import {
requireMcpLifecycleResult,
requireMcpOutput,
requireMcpToolError,
} from "./mcp-verifier-results.mjs";
/** A pre-scope dossier must not be replayed into a newly qualified trace. */
export async function verifyLegacyValueScopeSnapshot(
client,
{ target, procedure, observed, env },
) {
const { value_flow_effect_scope: scope, ...legacyParameters } =
observed.analysis_profile.parameters;
const legacyProfile = createAnalysisProfile(
observed.provider,
legacyParameters,
);
const parsed = await parseBinaryTarget(target.path, {
cwd: process.cwd(),
hostArchitecture: process.arch,
});
if (!parsed.ok) throw parsed.error;
const nativeTarget = parsed.value;
const result = structuredClone(observed.normalized_result);
const flow = result.native_value_flow;
assert.equal(flow.available, true);
const describesScope = (value) =>
value.includes("LOAD and STORE") && value.includes("address varnodes");
assert.ok(flow.limitations.some(describesScope));
flow.limitations = flow.limitations.filter((value) => !describesScope(value));
const evidence = createEvidence(nativeTarget, observed.provider, {
operation: observed.operation,
parameters: observed.parameters,
result,
rawResult: result,
analysisProfile: legacyProfile,
limitations: observed.limitations,
locations: observed.locations,
});
const binding = snapshotBinding(legacyProfile);
const identity = snapshotTarget(nativeTarget);
const snapshot = parseAnalysisSnapshot({
target: identity,
binding,
entries: [
createAnalysisSnapshotEntry({
target: identity,
binding,
operation: observed.operation,
parameters: observed.parameters,
execution: {
result,
rawResult: result,
provider: observed.provider,
analysisProfile: legacyProfile,
limitations: observed.limitations,
locations: observed.locations,
subject: nativeTarget,
},
}),
],
workflow_entries: [],
evidence_bundle: createEvidenceBundle([evidence]),
});
const path = join(env.TMPDIR, `legacy-value-scope-${randomUUID()}.json`);
await writeFile(path, serializeAnalysisSnapshot(snapshot), { flag: "wx" });
try {
const bytes = await readFile(path);
const session = requireMcpLifecycleResult(
await client.callTool({ name: "binary_session", arguments: {} }),
"binary_session",
);
const reply = await client.callTool({
name: "open_binary",
arguments: {
path: target.path,
provider_id: "ghidra",
snapshot_path: path,
},
});
assert.equal(reply.isError, true);
assert.equal(scope, "pcode-load-store-explicit-v1");
const error = requireMcpToolError(reply);
assert.equal(error.code, "evidence_integrity_mismatch");
assert.deepEqual(error.details.binding_mismatch.profile_digest, {
snapshot: legacyProfile.digest,
selected: observed.analysis_profile.digest,
});
assert.deepEqual(
requireMcpLifecycleResult(
await client.callTool({ name: "binary_session", arguments: {} }),
"binary_session",
),
session,
);
const fresh = await client.callTool(
{ name: "trace_native_values", arguments: { procedure, max_depth: 0 } },
{ timeout: 180000 },
);
assert.notEqual(fresh.isError, true, JSON.stringify(fresh));
const trace = requireMcpOutput(fresh, "trace_native_values");
assert.ok(trace.normalized_result.limitations.some(describesScope));
assert.deepEqual(await readFile(path), bytes);
} finally {
await rm(path);
}
}
+43
View File
@@ -17,7 +17,9 @@ import {
requireMcpOutput,
} from "./mcp-verifier-results.mjs";
import { verifyGhidraBoundaries } from "./real-ghidra-boundaries.mjs";
import { verifyLegacyValueScopeSnapshot } from "./ghidra-value-scope-snapshot-e2e.mjs";
import { verifyGhidraProjectIdentity } from "./real-ghidra-project-identity.mjs";
import { verifyGhidraSocketBoundary } from "./real-ghidra-socket-boundary.mjs";
const defaultEntrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
const environment = () => ({
@@ -213,6 +215,14 @@ async function verifyNativeValueAdapters(
procedure,
globalAddress,
);
assert.ok(
mcpGraph.limitations.some(
(value) =>
value.includes("analyze_function") &&
value.includes("LOAD and STORE") &&
value.includes("address varnodes"),
),
);
assert.deepEqual(mcpEvidence.normalized_result, mcpGraph);
assert.deepEqual(mcpGraph, cliGraph);
const warmReply = await client.callTool(
@@ -242,6 +252,26 @@ async function verifyNativeValueAdapters(
);
assert.equal(functionEvidence.operation, "analyze_function");
assert.equal(functionEvidence.provider.id, "ghidra");
const flow = functionEvidence.normalized_result.native_value_flow;
assert.equal(flow.available, true);
const identifiesEffectScope = (value) =>
value.includes("LOAD and STORE") && value.includes("address varnodes");
assert.ok(flow.limitations.some(identifiesEffectScope));
const effectsView = await client.callTool({
name: "inspect_analysis_view",
arguments: {
source: {
kind: "retained-evidence",
evidence_id: functionEvidence.evidence_id,
},
view: { kind: "native", facet: "value_flow_effects" },
},
});
assert.notEqual(effectsView.isError, true, JSON.stringify(effectsView));
const effectsPage = requireMcpOutput(effectsView, "inspect_analysis_view");
assert.ok(
effectsPage.normalized_result.limitations.some(identifiesEffectScope),
);
assert.deepEqual(
functionEvidence.normalized_result.facet_availability,
Object.fromEntries(
@@ -267,6 +297,12 @@ async function verifyNativeValueAdapters(
functionEvidence.analysis_profile,
);
assert.deepEqual(functionProfile, upstreamProfile);
await verifyLegacyValueScopeSnapshot(client, {
target,
procedure,
observed: functionEvidence,
env,
});
const comparisonEvidence = await compareRetainedFunctionEvidence(
client,
functionEvidence,
@@ -355,6 +391,12 @@ async function verifyNativeValueAdapters(
entrypoint,
env,
});
const socketBoundary = await verifyGhidraSocketBoundary({
target,
procedure,
entrypoint,
env,
});
return {
mocked: false,
cli: true,
@@ -363,6 +405,7 @@ async function verifyNativeValueAdapters(
direct_function_comparison: true,
boundaries,
project_identity: projectIdentity,
socket_boundary: socketBoundary,
nodes: cliGraph.total_nodes,
edges: cliGraph.total_edges,
decompilations: cliGraph.decompilations,
+7 -3
View File
@@ -23,6 +23,7 @@ import { verifyGhidraEntryAliases } from "./real-ghidra-entry-aliases.mjs";
import { verifyGhidraNamespaceAnnotations } from "./real-ghidra-namespace-annotations.mjs";
import { verifyGhidraBatchIdentity } from "./real-ghidra-batch-identity.mjs";
import { verifyGhidraInstructionFlow } from "./real-ghidra-instruction-flow.mjs";
import { verifyGhidraSearchFacts } from "./real-ghidra-search-facts.mjs";
/** Probe real Ghidra location, annotation and error contracts through public adapters. */
export async function verifyGhidraBoundaries(
@@ -410,6 +411,11 @@ export async function verifyGhidraBoundaries(
(item) => item.value === "REA_GHIDRA_INVENTORY_ENTRY",
);
assert.ok(marker);
await verifyGhidraSearchFacts(call, {
strings,
procedures,
entryAddress: address,
});
assert.ok(
(
await call("search_strings", { pattern: marker.value.toLowerCase() })
@@ -598,9 +604,7 @@ export async function verifyGhidraBoundaries(
const longString = strings.find((item) => item.value === longLiteral);
assert.ok(longString, "The provider must retain the full regression literal");
const literalMatches = await call("search_strings", { pattern: longLiteral });
assert.deepEqual(literalMatches, [
{ address: longString.address, value: longLiteral },
]);
assert.deepEqual(literalMatches, [longString]);
assert.deepEqual(await cli("search", longLiteral), literalMatches);
await assert.rejects(
cli("search", regexPattern, ["--mode", "regex"]),
+58
View File
@@ -0,0 +1,58 @@
import assert from "node:assert/strict";
/** Compare public searches and composed traces with their provider inventory rows. */
export async function verifyGhidraSearchFacts(
call,
{ strings, procedures, entryAddress },
) {
const marker = strings.find(
(row) => row.value === "REA_GHIDRA_INVENTORY_ENTRY",
);
const entry = procedures.find((row) => row.address === entryAddress);
const external = procedures.find((row) => row.procedure.external);
const thunk = procedures.find((row) => row.procedure.thunk);
assert.ok(marker, "Missing source string fixture");
assert.ok(entry, "Missing selected entry procedure");
assert.ok(external, "Missing external procedure fixture");
const cases = [
{ type: "string", operation: "search_strings", row: marker },
...[entry, external, ...(thunk === undefined ? [] : [thunk])].map(
(row) => ({
type: "procedure",
operation: "search_procedures",
row,
}),
),
];
for (const { type, operation, row } of cases) {
const search = await call(operation, {
pattern: row.value,
mode: "literal",
case_sensitive: true,
});
const match = search.find(
(item) => item.address === row.address && item.value === row.value,
);
assert.deepEqual(
match,
row,
`${operation} must preserve inventory facts for ${row.value}`,
);
const trace = await call("trace_feature", {
query: row.value,
case_sensitive: true,
});
const traced = trace.matches.find(
(item) =>
item.type === type &&
item.address === row.address &&
item.value === row.value,
);
assert.deepEqual(
traced,
{ ...row, type },
`trace_feature must preserve ${operation} facts for ${row.value}`,
);
}
return { compared: cases.length, thunk_observed: thunk !== undefined };
}
+200
View File
@@ -0,0 +1,200 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import {
access,
mkdir,
mkdtemp,
readFile,
readdir,
realpath,
rm,
stat,
} from "node:fs/promises";
import { basename, dirname, join } from "node:path";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { withVerifierCleanup } from "./verifier-cleanup.mjs";
const exec = promisify(execFile);
/** Exercise the OpenJDK Unix-socket boundary through real Ghidra and REA. */
export async function verifyGhidraSocketBoundary({
target,
procedure,
entrypoint,
env,
}) {
if (process.platform !== "darwin" && process.platform !== "linux")
return {
status: "not-applicable",
reason: "The Ghidra Unix-socket lane is not used on this host.",
};
const limit = process.platform === "darwin" ? 102 : 106;
const before = createHash("sha256")
.update(await readFile(target.path))
.digest("hex");
const root = await realpath(await mkdtemp("/tmp/rea-gsock-"));
const suffixBytes = Buffer.byteLength("/rea-ghidra-XXXXXX/bridge.sock");
let cleanupIncomplete = false;
const samples = [];
try {
for (const { proposedBytes, unicode } of [
{ proposedBytes: limit, unicode: false },
{ proposedBytes: limit + 1, unicode: false },
{ proposedBytes: limit + 2, unicode: false },
{ proposedBytes: limit + 1, unicode: true },
]) {
const character = unicode ? "雪" : "";
const padding =
proposedBytes -
suffixBytes -
Buffer.byteLength(root) -
1 -
Buffer.byteLength(character);
assert.ok(
padding > 0,
"Verifier workspace leaves no room for the boundary fixture",
);
const parent = join(root, "x".repeat(padding) + character);
await mkdir(parent);
assert.equal(Buffer.byteLength(parent) + suffixBytes, proposedBytes);
if (unicode) assert.ok(Buffer.byteLength(parent) > parent.length);
const sessionEnv = { ...env, TMPDIR: parent };
const client = new Client({
name: "rea-ghidra-socket-boundary",
version: "1",
});
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env: sessionEnv,
stderr: "pipe",
});
let connected = false;
let opened = false;
let runtimeRoot;
let socketRoot;
let endpoint;
const cleanup = async (task) => {
try {
await task();
} catch (cause) {
cleanupIncomplete = true;
throw cause;
}
};
await withVerifierCleanup(async () => {
await client.connect(transport);
connected = true;
const tools = new Map(
(await client.listTools()).tools.map((tool) => [tool.name, tool]),
);
const call = async (name, arguments_ = {}) => {
const reply = await client.callTool(
{ name, arguments: arguments_ },
{ timeout: 180000, toolDefinition: tools.get(name) },
);
assert.notEqual(
reply.isError,
true,
`${name}: ${JSON.stringify(reply)}`,
);
return reply.structuredContent ?? JSON.parse(reply.content[0].text);
};
await call("open_binary", { path: target.path, provider_id: "ghidra" });
opened = true;
await call("list_documents");
const roots = (await readdir(parent)).filter((name) =>
name.startsWith("rea-ghidra-"),
);
assert.equal(roots.length, 1);
runtimeRoot = join(parent, roots[0]);
endpoint = JSON.parse(
await readFile(join(runtimeRoot, "ownership.json"), "utf8"),
).endpoint_path;
assert.equal(
Buffer.byteLength(join(runtimeRoot, "bridge.sock")),
proposedBytes,
);
const fallback = dirname(endpoint) !== runtimeRoot;
assert.equal(fallback, proposedBytes > limit);
if (fallback) {
socketRoot = dirname(endpoint);
assert.match(basename(socketRoot), /^rea-ghidra-socket-/u);
assert.equal((await stat(socketRoot)).mode & 0o777, 0o700);
}
samples.push({
proposed_bytes: proposedBytes,
unicode,
endpoint_bytes: Buffer.byteLength(endpoint),
fallback,
});
}, [
() =>
cleanup(async () => {
if (!connected || !opened) return;
const reply = await client.callTool(
{ name: "close_binary", arguments: {} },
{ timeout: 180000 },
);
assert.notEqual(
reply.isError,
true,
`close_binary: ${JSON.stringify(reply)}`,
);
}),
() =>
cleanup(async () => {
await client.close();
}),
() =>
cleanup(async () => {
await transport.close();
}),
]);
await assert.rejects(access(runtimeRoot), { code: "ENOENT" });
if (socketRoot !== undefined)
await assert.rejects(access(socketRoot), { code: "ENOENT" });
if (proposedBytes === limit + 1 && !unicode) {
const { stdout } = await exec(
process.execPath,
[
entrypoint,
"function",
target.path,
procedure,
"--provider",
"ghidra",
"--json",
],
{ env: sessionEnv, timeout: 240000, maxBuffer: 72 * 1024 * 1024 },
);
assert.ok(JSON.parse(stdout).normalized_result.procedure);
assert.deepEqual(
(await readdir(parent)).filter((name) =>
name.startsWith("rea-ghidra-"),
),
[],
"CLI left a Ghidra runtime directory",
);
}
}
assert.equal(
createHash("sha256")
.update(await readFile(target.path))
.digest("hex"),
before,
);
return {
status: "verified",
samples,
cli_boundary: true,
target_unchanged: true,
};
} finally {
// A failed cleanup retains its exact workspace for investigation and retry.
if (!cleanupIncomplete) await rm(root, { recursive: true, force: true });
}
}
+49 -13
View File
@@ -637,15 +637,37 @@ const measureGhidra = async (path, functions) => {
return answers;
};
/** Symbol-bearing caller binaries with the requested functions, beside their stripped copies. */
const targetPairs = async (root, names) => {
const describeFailure = (cause) => ({
message: cause instanceof Error ? cause.message : String(cause),
...(cause instanceof AggregateError
? { causes: cause.errors.map(describeFailure) }
: {}),
});
/** Inspect symbol-bearing caller binaries, retaining both selected and unmatched inputs. */
const targetPairs = async (root, names, selection) => {
const pairs = [];
for (const flavor of (await readdir(root)).sort()) {
const directory = join(root, flavor);
if (!(await stat(directory)).isDirectory()) continue;
for (const file of (await readdir(directory)).sort()) {
if (file.endsWith(".stripped")) continue;
const symbols = await symbolAddresses(join(directory, file), names);
let symbols;
try {
symbols = await symbolAddresses(join(directory, file), names);
} catch (cause) {
selection.examined.push({
binary: `${flavor}/${file}`,
status: "inspection_failed",
error: describeFailure(cause),
});
throw cause;
}
selection.examined.push({
binary: `${flavor}/${file}`,
status: symbols.size > 0 ? "selected" : "no_matching_text_symbols",
matched_functions: [...symbols.keys()],
});
if (symbols.size > 0) pairs.push({ flavor, directory, file, symbols });
}
}
@@ -666,6 +688,18 @@ const verifyVariant = async ({ flavor, directory, symbols }, variant) => {
const results = [];
let timeoutRecovery;
const failures = [];
const callerTargetSelection =
values.targets === undefined
? undefined
: {
directory: values.targets,
functions_argument: values.functions ?? null,
requested_functions: (values.functions ?? "")
.split(",")
.filter(Boolean),
symbol_scan_complete: false,
examined: [],
};
let inspectionResourcesClean = true;
try {
const installation = await inspectKunaInstallation(
@@ -678,13 +712,18 @@ try {
);
kunaVersion = `kuna ${installation.value.version}`;
let callerPairs = [];
if (values.targets !== undefined) {
const names = (values.functions ?? "").split(",").filter(Boolean);
if (callerTargetSelection !== undefined) {
const names = callerTargetSelection.requested_functions;
assert.ok(names.length > 0, "--targets requires --functions");
callerPairs = await targetPairs(values.targets, names);
callerPairs = await targetPairs(
callerTargetSelection.directory,
names,
callerTargetSelection,
);
callerTargetSelection.symbol_scan_complete = true;
assert.ok(
callerPairs.length > 0,
`No caller-built targets contain the requested functions in ${values.targets}`,
`No caller-built targets contain the requested functions in ${callerTargetSelection.directory}`,
);
const found = new Set(
callerPairs.flatMap(({ symbols }) => [...symbols.keys()]),
@@ -772,18 +811,15 @@ if (cleanupComplete)
cleanupComplete = false;
failures.push(cause);
}
const describeFailure = (cause) => ({
message: cause instanceof Error ? cause.message : String(cause),
...(cause instanceof AggregateError
? { causes: cause.errors.map(describeFailure) }
: {}),
});
const report = {
lane: "kuna",
status: failures.length === 0 ? "passed" : "failed",
kuna_version: kunaVersion,
platform: `${process.platform}/${process.arch}`,
verifier_run: lifecycle,
...(callerTargetSelection === undefined
? {}
: { caller_target_selection: callerTargetSelection }),
results,
...(timeoutRecovery === undefined
? {}
-2
View File
@@ -233,7 +233,6 @@ it.each([true, false])(
{ timeoutMs: 100, maxOutputBytes: 1024, volatileOutput },
undefined,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected a capture allowance refusal");
const projected = projectAnalysisError(result.error);
expect(projected.retryable).toBe(volatileOutput);
@@ -263,7 +262,6 @@ it("reports the effective fixed adb deadline", async () => {
{ timeoutMs: 600_000, maxOutputBytes: 1024 },
undefined,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected an adb deadline");
const projected = projectAnalysisError(result.error);
expect(projected).toMatchObject({
-7
View File
@@ -87,7 +87,6 @@ const refusedBy = async (
request: AdbRequest,
): Promise<ReturnType<typeof projectAnalysisError>> => {
const result = await provider.execute(request);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected a refusal");
return projectAnalysisError(result.error);
};
@@ -577,7 +576,6 @@ it.each([
await expect(lstat(packageDirectory)).rejects.toThrow();
return;
}
expect(result.ok).toBe(true);
if (!result.ok) throw new Error(result.error.message);
expect(
(
@@ -1463,7 +1461,6 @@ exit 1`,
overwrite: false,
},
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected snapshot cleanup to be reported");
expect(result.error.cleanupResources).toHaveLength(1);
expect(result.error.partialObservation).toMatchObject({
@@ -1523,7 +1520,6 @@ exit 1`,
overwrite: false,
},
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected a changed-artifact result");
const evidence = parseEvidence(result.error.partialObservation);
expect(evidence).toMatchObject({
@@ -1868,7 +1864,6 @@ it("reads Android 16 focus declarations from displays when windows omits them",
operation: "inspect_adb_window",
input: { serial: "emulator-5554" },
});
expect(execution.ok).toBe(true);
if (!execution.ok) throw execution.error;
expect(execution.value.limitations).toContainEqual(
expect.stringContaining("dumpsys window displays"),
@@ -1891,7 +1886,6 @@ it("describes missing window focus as unknown after both sections omit declarati
operation: "inspect_adb_window",
input: { serial: "emulator-5554" },
});
expect(execution.ok).toBe(true);
if (!execution.ok) throw execution.error;
expect(execution.value.limitations).toContainEqual(
expect.stringContaining("focus is unknown"),
@@ -1915,7 +1909,6 @@ it.each(["windows", "displays"])(
operation: "inspect_adb_window",
input: { serial: "emulator-5554" },
});
expect(execution.ok).toBe(true);
if (!execution.ok) throw execution.error;
expect(execution.value.result).toMatchObject({
focused_window: null,
-2
View File
@@ -114,7 +114,6 @@ it("applies the omitted 600000 ms decode deadline and a selected value on the ne
expect(outputBudgets).toEqual([4 * 1024 * 1024]);
const timedOut = await decode(provider, path, 200);
expect(timedOut.ok).toBe(false);
if (timedOut.ok) throw new Error("Expected a decode deadline");
const projected = projectAnalysisError(timedOut.error);
expect(projected).toMatchObject({
@@ -144,7 +143,6 @@ it("applies the omitted 600000 ms decode deadline and a selected value on the ne
]);
const maximum = await decode(provider, path, MAX_APKTOOL_DECODE_TIMEOUT_MS);
expect(maximum.ok).toBe(false);
if (maximum.ok) throw new Error("Expected the maximum decode deadline");
expect(projectAnalysisError(maximum.error).remediation.action).toContain(
"do not increase it further",
-2
View File
@@ -97,7 +97,6 @@ const refusedBy = async (
request: ApktoolRequest,
): Promise<ReturnType<typeof projectAnalysisError>> => {
const result = await provider.execute(request);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected a refusal");
return projectAnalysisError(result.error);
};
@@ -583,7 +582,6 @@ it("still refuses invalid UTF-8 bytes in decoded strings", async () => {
operation: "decode_android_resources",
input: { path: apk.path, include_strings: true },
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected a refusal");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "unreadable_output",
@@ -1,7 +1,6 @@
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { parseOpenStep } from "plist";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
@@ -19,7 +18,6 @@ it.each([
const executable = join(programs, "Fixture");
const infoPlist = join(bundle, "Contents", "Info.plist");
const source = `${prefix}{ CFBundleExecutable = Fixture; }`;
expect(parseOpenStep(source)).toEqual({ CFBundleExecutable: "Fixture" });
await mkdir(programs, { recursive: true });
await writeFile(infoPlist, source);
await writeFile(executable, "fixture - never executed");
@@ -286,7 +286,6 @@ it("retains completed bundle facts as bundle Evidence after cancellation", async
total_analysis_timeout_ms: 1000,
});
const result = await analyzeWebBundle(browser, selected);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected cancellation");
expect(result.error).toBe(failure);
expect(result.error.cause).toBe(reason);
@@ -313,7 +312,6 @@ it("preserves a prior caller reason without admitting browser work", async () =>
input,
{ signal: AbortSignal.abort(reason) },
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected cancelled inventory");
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(reason);
@@ -48,7 +48,6 @@ const expectRetainedCapture = (
result: Awaited<ReturnType<typeof runScenario>>,
failure: AnalysisError,
) => {
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected the retained capture");
expect(result.error).toBe(failure);
expect(parseEvidence(result.error.partialObservation)).toMatchObject({
@@ -197,7 +196,6 @@ it("rejects a successful capture that is not a completed scenario", async () =>
captureScenario: async () => ok({} as BrowserScenarioCapture),
};
const result = await captureBrowserScenario(provider, scenario);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected a completed-capture refusal");
expect(result.error).toBeInstanceOf(AnalysisOutputError);
expect(result.error.message).toContain(
-1
View File
@@ -28,7 +28,6 @@ describe("call-path traversal cancellation ownership", () => {
input,
controller.signal,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected cancellation");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
@@ -59,7 +59,6 @@ describe("JSON client configuration repeated-member edits", () => {
removeShadowedMembersAt: [path],
});
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("authorized repeats were refused");
expect(parseStrictJson(result.value)).toEqual({
mcpServers: {
@@ -109,7 +108,6 @@ describe("JSON client configuration repeated-member edits", () => {
"handles %s without losing peers",
(_label, text, expectedServers, authority) => {
const result = removeRea(text, "json", authority);
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("non-shadowed removal was refused");
expect(parseStrictJson(result.value)).toEqual({
mcpServers: expectedServers,
@@ -127,7 +125,6 @@ describe("JSON client configuration repeated-member edits", () => {
removeShadowedMembersAt: [["mcpServers", "rea"]],
});
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("authorized escaped repeat was refused");
expect(result.value).toContain(`"mcpServers":${inactive}`);
expect(parseStrictJson(result.value)).toEqual({
@@ -146,7 +143,6 @@ describe("JSON client configuration repeated-member edits", () => {
removeShadowedMembersAt: [["mcp", "rea"]],
});
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("authorized JSONC repeats were refused");
expect(result.value).toContain("// top comment");
expect(result.value).toContain("// first");
@@ -42,7 +42,6 @@ for (const format of ["toml", "grok"] as const)
format,
source,
);
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("TOML edit was refused");
for (const comment of [
"# server header note",
@@ -85,7 +84,6 @@ it("retains comments from dotted REA assignments and nested inline values", () =
"toml",
source,
);
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("TOML removal was refused");
expect(result.value).toContain(
source.includes("# dotted note")
-1
View File
@@ -102,7 +102,6 @@ type DirectAnalysisTool =
| "address_to_file_offset"
| "binary_overview"
| "procedure_pseudo_code"
| "procedure_references"
| "read_function_instructions"
| "inspect_native_instruction"
| "inspect_native_data_type"
@@ -6,7 +6,6 @@ it("preserves field diagnostics for rejected enhanced tool inputs", async () =>
const execute = vi.fn();
const tools = new EnhancedTools({ execute });
const result = await tools.execute("trace_native_values", { procedure: "" });
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected an input failure");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "invalid_request",
@@ -18,7 +17,6 @@ it("preserves field diagnostics for rejected enhanced tool inputs", async () =>
expect(execute).not.toHaveBeenCalled();
const sibling = await tools.execute("batch_decompile", { addresses: "bad" });
expect(sibling.ok).toBe(false);
if (sibling.ok) throw new Error("expected an input failure");
expect(projectAnalysisError(sibling.error)).toMatchObject({
details: { issues: [{ path: ["addresses"], reason: "invalid_type" }] },
@@ -444,7 +444,6 @@ describe("EnhancedTools cancellation ownership", () => {
controller.signal,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Pre-cancelled observation was accepted");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
@@ -212,7 +212,6 @@ it("projects completed raw capture facts on failure through the caller's selecti
record_ordinals: [0],
sensitive_values: ["private-marker"],
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Cancellation must remain the primary result");
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(reason);
@@ -270,7 +269,6 @@ it("keeps a redacted cleanup error linked to its live owner through service clos
format: "har",
sensitive_values: ["private-marker"],
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Retained cleanup must remain a failure");
const returnedError = result.error;
expect(returnedError.cleanupIncomplete).toBe(true);
+4 -1
View File
@@ -14,7 +14,10 @@ export const workflowAnalysisProfile = (
upstream_analysis_profile: upstream,
source_limitations: "provider-operation-caveats-v1",
...(operation === "trace_native_values"
? { value_flow_absence: "recorded-versus-unavailable-v1" }
? {
value_flow_limitations: "provider-attributed-v1",
value_flow_absence: "recorded-versus-unavailable-v1",
}
: {}),
...(operation === "binary_overview"
? { workflow_revision: 5 }
-1
View File
@@ -703,7 +703,6 @@ it("reports a retained workflow and the committed prefix when Unknown publicatio
? err(failure)
: records.recordUnknown(input),
});
expect(outcome.ok).toBe(false);
if (outcome.ok) throw new Error("expected failure");
const projected = projectAnalysisError(outcome.error);
expect(projected.details?.workflow_unknown_publication).toEqual({
@@ -53,7 +53,6 @@ it.each([
it("rejects an unknown operation before any effect", async () => {
const service = new AdbDeviceAnalysisService(refusedPort);
const result = await service.execute("unknown_adb_operation" as never, {});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected an unknown operation refusal");
expect(result.error).toBeInstanceOf(AnalysisInputError);
if (!(result.error instanceof AnalysisInputError)) throw result.error;
@@ -211,7 +211,6 @@ it("keeps native JSON source excerpts internal while preserving the failure caus
const resources = new ArtifactResourceScope();
try {
const result = await inspectApiContractEvidence({ path }, resources);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected a syntax refusal");
expect(result.error.cause).toBeInstanceOf(SyntaxError);
expect(JSON.stringify(result.error)).not.toContain("u:p@h");
@@ -257,7 +256,6 @@ it.each([
});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(controller.signal.aborted).toBe(true);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected the parser failure");
expect(result.error._tag).toBe(tag);
expect(result.error.partialObservation).toEqual({
@@ -84,7 +84,6 @@ it.each(["path", "operations_path"] as const)(
);
await withScope(async (resources) => {
const result = await inspectApiContractEvidence(input, resources);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected invalid UTF-8");
expect(result.error._tag).toBe("AnalysisInputError");
expect(projectAnalysisError(result.error)).toMatchObject({
@@ -148,7 +147,6 @@ it("attributes auxiliary file admission failures to operations_path and retains
{ ...input, operations_path: join(input.path, "missing") },
resources,
);
expect(missing.ok).toBe(false);
if (missing.ok) throw new Error("Expected missing operation file");
expect(projectAnalysisError(missing.error)).toMatchObject({
details: { issues: [{ path: ["operations_path"] }] },
@@ -1197,60 +1197,87 @@ describe("relationship coverage snapshot replay", () => {
});
});
it("does not replay pre-envelope call graphs under the revised workflow profile", () => {
const cache = new AnalysisSnapshotCache();
const target = ANALYSIS_SNAPSHOT_TARGET;
const profile = ANALYSIS_SNAPSHOT_PROFILE;
const operation = "get_call_graph" as const;
const parameters = { address: "0x1000", direction: "forward" };
const oldProfile = workflowAnalysisProfile(profile);
const revisedProfile = workflowAnalysisProfile(profile, operation);
const oldResult = { "0": [] };
cache.recordWorkflow({
target,
profile,
operation,
parameters,
execution: {
...createAnalysisExecution(oldResult, REA_WORKFLOW_PROVIDER, {
subject: target,
rawResult: oldResult,
}),
analysisProfile: oldProfile,
it.each([
{
operation: "get_call_graph",
parameters: { address: "0x1000", direction: "forward" },
result: { "0": [] },
},
{
operation: "trace_native_values",
parameters: { procedure: "0x1000" },
result: {
seed: "0x1000",
target_sha256: ANALYSIS_SNAPSHOT_TARGET.sha256,
nodes: [],
edges: [],
procedures: [],
unknowns: [],
total_nodes: 0,
total_edges: 0,
decompilations: 1,
next_offset: null,
page_truncated: false,
traversal: { status: "complete" },
truncated: false,
limitations: [],
},
});
const evidence = createEvidence(target, REA_WORKFLOW_PROVIDER, {
operation,
confidence: "derived",
parameters,
result: oldResult,
rawResult: oldResult,
analysisProfile: oldProfile,
});
const exported = cache.export(
target,
profile,
createEvidenceBundle([evidence]),
);
if (!exported.ok) throw exported.error;
const snapshot = parseAnalysisSnapshot(exported.value);
expect(
snapshotEvidenceForQuery(snapshot, {
},
] as const)(
"does not replay old $operation results under the revised workflow profile",
({ operation, parameters, result: oldResult }) => {
const cache = new AnalysisSnapshotCache();
const target = ANALYSIS_SNAPSHOT_TARGET;
const profile = ANALYSIS_SNAPSHOT_PROFILE;
const oldProfile = workflowAnalysisProfile(profile);
const revisedProfile = workflowAnalysisProfile(profile, operation);
cache.recordWorkflow({
target,
bindingProfile: profile,
profile,
operation,
parameters,
provider: REA_WORKFLOW_PROVIDER,
evidenceProfile: revisedProfile,
}),
).toBeUndefined();
expect(
enhancedOutputSchemas.get_call_graph.safeParse({
normalized_result: oldResult,
}).success,
).toBe(false);
});
execution: {
...createAnalysisExecution(oldResult, REA_WORKFLOW_PROVIDER, {
subject: target,
rawResult: oldResult,
}),
analysisProfile: oldProfile,
},
});
const evidence = createEvidence(target, REA_WORKFLOW_PROVIDER, {
operation,
confidence: "derived",
parameters,
result: oldResult,
rawResult: oldResult,
analysisProfile: oldProfile,
});
const exported = cache.export(
target,
profile,
createEvidenceBundle([evidence]),
);
if (!exported.ok) throw exported.error;
const snapshot = parseAnalysisSnapshot(exported.value);
expect(
snapshotEvidenceForQuery(snapshot, {
target,
bindingProfile: profile,
operation,
parameters,
provider: REA_WORKFLOW_PROVIDER,
evidenceProfile: revisedProfile,
}),
).toBeUndefined();
if (operation === "get_call_graph")
expect(
enhancedOutputSchemas.get_call_graph.safeParse({
normalized_result: oldResult,
}).success,
).toBe(false);
},
);
it("keeps older value-trace Evidence as history without replaying its source IDs", () => {
const cache = new AnalysisSnapshotCache();
@@ -1374,6 +1401,41 @@ it("declines source-bearing snapshot entries while replaying omitted and empty s
expect(cache.entries()).toHaveLength(2);
});
it("distinguishes value-flow caveat propagation from source-ownership revisions", () => {
const upstream = ANALYSIS_SNAPSHOT_PROFILE;
const prior = {
...workflowAnalysisProfile(upstream).parameters,
value_flow_absence: "recorded-versus-unavailable-v1",
workflow_revision: 1,
};
const baseline = createAnalysisProfile(REA_WORKFLOW_PROVIDER, prior);
const ownership = createAnalysisProfile(REA_WORKFLOW_PROVIDER, {
...prior,
workflow_revision: 2,
});
const limitations = createAnalysisProfile(REA_WORKFLOW_PROVIDER, {
...prior,
value_flow_limitations: "provider-attributed-v1",
});
const combined = createAnalysisProfile(REA_WORKFLOW_PROVIDER, {
...limitations.parameters,
workflow_revision: 2,
});
const scoped = workflowAnalysisProfile(upstream, "trace_native_values");
expect(scoped.digest).toBe(combined.digest);
expect(scoped.parameters.value_flow_limitations).toBe(
"provider-attributed-v1",
);
expect(
new Set([
baseline.digest,
ownership.digest,
limitations.digest,
combined.digest,
]).size,
).toBe(4);
});
it("replays source-bearing entries only when their complete retained closure exists", () => {
const target = ANALYSIS_SNAPSHOT_TARGET;
const profile = ANALYSIS_SNAPSHOT_PROFILE;
@@ -394,7 +394,6 @@ it.each([false, true])(
releaseClose();
const result = await opening;
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected failed resource switch");
expect(result.error.cleanupPrimary).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cleanupPrimary?.cause).toBe(reason);
@@ -480,7 +479,6 @@ it("keeps an earlier resource close failure primary when a later cleanup is canc
releaseCandidateClose();
const result = await opening;
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected failed resource switch");
expect(result.error).toBe(originalFailure);
expect(result.error.cleanupPrimary).toBeUndefined();
@@ -569,7 +567,6 @@ it("keeps cancellation primary when binary retirement fails before opening resou
releaseClose();
const result = await opening;
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected failed resource open");
expect(result.error.cleanupPrimary).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cleanupPrimary?.cause).toBe(reason);
@@ -661,7 +658,6 @@ it("keeps an earlier resource close failure primary when binary candidate cleanu
releaseCandidateClose();
const result = await opening;
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected failed binary open");
expect(result.selection).toEqual({ kind: "none" });
expect(result.error).toBe(originalFailure);
@@ -765,7 +761,6 @@ it("keeps an earlier binary close failure primary when candidate cleanup is canc
releaseCandidateClose();
const result = await opening;
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected failed resource open");
expect(result.error).toBe(originalFailure);
expect(result.error.cleanupPrimary).toBeUndefined();
@@ -866,7 +861,6 @@ it("cancels a binary switch during resource retirement and retains null-subject
releaseResourceClose();
const result = await opening;
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected cancelled binary open");
expect(result.error.cleanupPrimary).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cleanupPrimary?.cause).toBe(reason);
@@ -97,7 +97,6 @@ it("reports the first mixed child failure before awaiting the other owner and re
await Promise.all([nativeStarted, failureObserved]);
releaseNativeClose();
const first = await closing;
expect(first.ok).toBe(false);
if (first.ok) throw new Error("Expected mixed close failure");
expect(first.error.cleanupIncomplete).toBe(true);
@@ -113,7 +113,6 @@ it("retains every failed child resource across composite cleanup retries", async
for (let attempt = 0; attempt < 2; attempt += 1) {
const closed = await client.close();
expect(closed.ok).toBe(false);
if (closed.ok) throw new Error("Expected composite cleanup failure");
expect(closed.error).toMatchObject({
cleanupIncomplete: true,
@@ -158,7 +157,6 @@ it("preserves one child's typed cleanup error unchanged", async () => {
const closed = await client.close();
expect(closed.ok).toBe(false);
if (closed.ok) throw new Error("Expected child cleanup failure");
expect(closed.error).toBe(child.cleanupError);
});
@@ -306,7 +304,6 @@ it.each([1, 2])(
release();
}
const first = await closing;
expect(first.ok).toBe(false);
if (first.ok) throw new Error("Expected retained child cleanup");
expect(notifications).toEqual([
...children.map((child) => child.cleanupError),
@@ -318,7 +315,6 @@ it.each([1, 2])(
notifications.length = 0;
const retried = await closeAnalysisClient(client, "composite", options);
expect(retried.ok).toBe(false);
if (retried.ok) throw new Error("Expected failed cleanup retry");
expect(notifications).toEqual([
...children.map((child) => child.cleanupError),
@@ -126,7 +126,6 @@ it.each(["288", "309 "])(
identity,
inspect: () => Promise.resolve(ok(malformed)),
}).inspect({ path: report.artifact.path });
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected unsupported provider crash kind");
expect(result.error._tag).toBe("AnalysisOutputError");
},
@@ -169,7 +169,6 @@ it.each([
},
});
const result = await service.inspect(null, { signal: controller.signal });
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected cancelled admission");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
@@ -90,7 +90,6 @@ it("retains provider metadata in a late-cancellation observation", async () => {
const result = await service.inspect(input, { signal: controller.signal });
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected late cancellation");
expect(result.error).toMatchObject({ _tag: "AnalysisCancelledError" });
expect(result.error.cause).toBe(reason);
@@ -254,7 +254,6 @@ it.each([
},
});
const result = await service.inspect(null, { signal: controller.signal });
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected cancelled admission");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
@@ -171,7 +171,6 @@ it("preserves source Evidence and requested output choices when publication fail
path: "/output/seeds.tsv",
base: "0x2000",
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Failed publication accepted");
expect(result.error).toBe(failure);
expect(result.error.partialObservation).toMatchObject({
@@ -213,7 +212,6 @@ it("retains a published seed receipt if cancellation arrives after the atomic wr
{ path: receipt.path },
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Cancellation ignored");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
@@ -449,7 +449,6 @@ describe("residual unknown registry resolution and history", () => {
).toBe(true);
const before = serializeEvidenceBundle(base.export());
const conflict = base.import(branch.export());
expect(conflict.ok).toBe(false);
if (conflict.ok) throw new Error("Expected divergent history refusal");
expectHistoryConflict(conflict.error, created.value.unknown_id);
expect(base.get(source.evidence_id)?.caller_input_path).toBe("./original");
@@ -32,7 +32,6 @@ it.each([
withEvidenceSources(cancelled ? err(primary) : ok("complete"), [source]),
sourceOwner === undefined ? undefined : { sourceOwner },
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected recording failure");
expect(result.error).toBe(cancelled ? primary : failure);
expect(primary.secondaryFailures).toEqual(cancelled ? [failure] : []);
@@ -75,7 +75,6 @@ it("preserves failed Inspector cleanup when cancellation occurs during observati
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected failed cleanup");
expect(result.error).toBe(failure);
expect(result.error.cleanupIncomplete).toBe(true);
@@ -4,7 +4,7 @@ import { expect, it } from "vitest";
import { graphForJavaScript as graphFor } from "../../../tests/fixtures/javascriptSemanticGraph.js";
import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js";
import { resolveJavaScriptSemanticEvidence } from "../../domain/javascript/javascriptSemanticGraph.js";
import { resolveJavaScriptSemanticEvidence } from "../../../tests/support/javascriptSemanticGraphFixture.js";
const SHA256 = "a".repeat(64);
const GRAPH_ID = `jag_${"b".repeat(64)}`;
@@ -74,7 +74,6 @@ it("retains transitive provenance without treating dependencies as native observ
undefined,
(id) => (id === dependency.evidence_id ? dependency : undefined),
);
expect(missing.ok).toBe(false);
if (missing.ok) throw new Error("Missing ancestor accepted");
expect(missing.error).toMatchObject({
evidenceId: ancestor.evidence_id,
@@ -933,7 +932,6 @@ it("enforces the caller-resolved representation allowance and names its setting"
const lowered = await matchNativeFunctions(small, {
representationAllowance: { maximumBytes: 64 * 1024, source: "runtime" },
});
expect(lowered.ok).toBe(false);
if (lowered.ok) throw new Error("Lowered allowance admitted the request");
expect(lowered.error).toMatchObject({
_tag: "AnalysisResourceConstraintError",
@@ -164,7 +164,6 @@ describe("native value trace seed admission", () => {
},
{ procedure: "missing" },
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected seed failure");
expect(result.error).toBe(failure);
expect(attempted).toEqual([["analyze_function", { procedure: "missing" }]]);
@@ -565,6 +564,53 @@ describe("native value failed-callee traversal", () => {
});
});
it("preserves and deduplicates source value-flow limitations across analyzed functions", async () => {
const base = functionDossierSchema.parse(ghidraFunctionDossier());
const caller = functionDossierSchema.parse(
nativeFlowDossier(base, {
address: "0x1000",
name: "caller",
operations: [node("call", "0x1000", "CALL")],
parameters: [],
}),
);
const callee = functionDossierSchema.parse(
nativeFlowDossier(base, {
address: "0x2000",
name: "callee",
operations: [],
parameters: [],
}),
);
if (
!caller.native_value_flow?.available ||
!callee.native_value_flow?.available
)
throw new Error("Expected available fixture value flow");
caller.native_value_flow.limitations = ["Shared provider caveat."];
callee.native_value_flow.limitations = [
"Shared provider caveat.",
"Callee-specific caveat.",
];
const result = await traceNativeValues(
callBindingAnalysis({
callerAddress: "0x1000",
calleeAddress: "0x2000",
caller,
callee,
}),
{ procedure: "0x1000", max_functions: 2 },
);
if (!result.ok) throw result.error;
const limitations = nativeValueTraceSchema.parse(result.value).limitations;
expect(limitations).toContain(
"Fixture (fixture 1) analyze_function: Callee-specific caveat.",
);
expect(
limitations.filter((item) => item.includes("Shared provider caveat.")),
).toEqual(["Fixture (fixture 1) analyze_function: Shared provider caveat."]);
});
describe("native value call-binding fan-out", () => {
it("retains limitations embedded in resolved call-target results", async () => {
const callerAddress = "0x1000";
@@ -181,6 +181,14 @@ export const traceNativeValues = async (
analysis_profile_digest: execution.value.analysisProfile?.digest ?? null,
});
const flow = dossier.native_value_flow;
if (flow !== null)
sourceLimitations.push(
...flow.limitations.map((limitation) => ({
provider: execution.value.provider,
operation: "analyze_function" as const,
limitation,
})),
);
if (flow === null || !flow.available) {
unknowns.push({
procedure: address,
@@ -122,7 +122,6 @@ it("retains completed pyc Evidence and the opaque caller reason on late cancella
{ path: report.artifact.path },
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected cancellation");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
@@ -218,7 +218,6 @@ it.each([
},
});
const result = await service.inspect(null, { signal: controller.signal });
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected cancelled admission");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
@@ -83,7 +83,6 @@ it("retains provider metadata in a late-cancellation observation", async () => {
const result = await service.decode(input, { signal: controller.signal });
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected late cancellation");
expect(result.error).toMatchObject({ _tag: "AnalysisCancelledError" });
expect(result.error.cause).toBe(reason);
@@ -290,7 +290,6 @@ it("refuses two admitted inventories that together exceed the comparison cap", a
right: evidence(right),
scope: "both",
});
expect(combined.ok).toBe(false);
if (combined.ok) throw new Error("Expected comparison allowance refusal");
const projected = projectAnalysisError(combined.error);
expect(projected.retryable).toBe(false);
+2 -2
View File
@@ -84,7 +84,7 @@ it("stops a close hook even when retained cleanup is still failing", async () =>
(cause: unknown) => cause,
);
expect(closeFailure).toMatchObject({
message: "Artifact cleanup remains incomplete: stuck lease, close hook",
message: expect.stringContaining("stuck lease, close hook"),
failedOwners: [owner],
cleanup: {
reason: "still owned; hook still stopping",
@@ -305,7 +305,7 @@ it("retains the same failed reader owner in a retry failure and retries before a
await expect(scope.run(async () => "must not run")).rejects.toMatchObject({
reason: "unavailable",
message: "Artifact cleanup remains incomplete: fixture reader",
message: expect.stringContaining("fixture reader"),
failedOwners: [owner],
});
expect(closeAttempts).toBe(2);
@@ -72,7 +72,6 @@ it.skipIf(process.platform === "win32")(
max_entries: 32,
max_probe_bytes: 4096,
});
expect(invalid.ok).toBe(false);
if (invalid.ok) throw new Error("expected ENOTDIR refusal");
expect(projectAnalysisError(invalid.error)).toMatchObject({
code: "target_unavailable",
@@ -83,7 +82,6 @@ it.skipIf(process.platform === "win32")(
max_entries: 32,
max_probe_bytes: 4096,
});
expect(valid.ok).toBe(true);
if (!valid.ok) throw valid.error;
expect(
desktopApplicationSchema
+1 -1
View File
@@ -295,7 +295,7 @@ it("fails /images followed by a malformed structure, and leaves a plain DTB a fi
scanCanonicalArtifactInventory(await writeFit("broken.itb", bytes)),
).rejects.toMatchObject({
reason: "format",
message: "FIT is malformed: Unknown FDT token 10",
message: expect.stringMatching(/FDT.*token 10/iu),
});
const plain = await scanCanonicalArtifactInventory(
await writeFit("board.dtb", devicetreeFixture()),
+1 -1
View File
@@ -445,7 +445,7 @@ describe("GodotPckArtifactReader carrier truncation", () => {
try {
await expect(text(await reader.open(entry))).rejects.toMatchObject({
reason: "integrity",
message: "Godot PCK entry ended early: member.bin",
message: expect.stringContaining("member.bin"),
});
expect(calls).toBe(2);
} finally {
+3 -4
View File
@@ -308,8 +308,7 @@ it.each([false, true])(
if (sea)
await expect(located).rejects.toMatchObject({
reason: "limit",
message:
"Node SEA PE inspection exceeds the 512 MiB PE parser input ceiling",
message: expect.stringMatching(/PE.*512 MiB/iu),
});
else await expect(located).resolves.toBeUndefined();
} finally {
@@ -659,7 +658,7 @@ it("rejects missing Mach-O load commands after a confirmed SEA section", async (
scanCanonicalArtifactInventory(await writeCarrier("commands", bytes)),
).rejects.toMatchObject({
reason: "format",
message: "Node SEA Mach-O load commands are truncated",
message: expect.stringMatching(/Mach-O.*load commands.*truncated/iu),
});
});
@@ -708,7 +707,7 @@ it("reports a member that ends after the carrier recheck as an integrity failure
try {
await expect(buffer(await reader.open(entry))).rejects.toMatchObject({
reason: "integrity",
message: "Node SEA entry ended early: sea.js",
message: expect.stringContaining("sea.js"),
});
expect(calls).toBe(2);
} finally {
@@ -558,8 +558,7 @@ it.each(["reported carutil", "tab-indented carutil", "observed assetutil"])(
expect(error.cause).toBe(cause);
expect(projectAnalysisError(error)).toMatchObject({
code: "artifact_operation_failed",
message:
"Artifact is invalid or has changed. Get a fresh copy and try again.",
message: expect.stringMatching(/invalid.*changed.*fresh copy/iu),
details: {
operation: "inspect_asset_catalog",
reason: "format",
@@ -8,10 +8,8 @@ import { describe, expect, it } from "vitest";
import { analyzeInterfaceBuilderBundle } from "./InterfaceBuilderAnalysis.js";
import { interfaceBuilderDecodeBudgetFailure } from "./InterfaceBuilderDecodeBudget.js";
import { preflightPropertyListDecode } from "./PropertyListDecodeBudget.js";
import { unknownDateLimitation } from "../../domain/apple/plistValue.js";
import { parseXmlPropertyList } from "../../domain/propertyListKeys.js";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { scanBinaryPlistScalars } from "./PlistScalars.js";
import {
readBinaryPlistObjectOffset,
readBinaryPlistObjectTableLayout,
@@ -181,8 +179,13 @@ it("retains explicit date uncertainty for an overlapping binary plist object", a
"com.apple.ibtool.document.classes": {},
}),
);
const date = scanBinaryPlistScalars(bytes).dates[0];
if (date === undefined) throw new Error("fixture lost its date");
// The fixture date is 978307200 seconds before the 2001 plist epoch.
const encodedDate = Buffer.alloc(9);
encodedDate[0] = 0x33;
encodedDate.writeDoubleBE(-978307200, 1);
const dateOffset = bytes.indexOf(encodedDate);
expect(dateOffset).toBeGreaterThanOrEqual(0);
expect(bytes.indexOf(encodedDate, dateOffset + 1)).toBe(-1);
const table = readBinaryPlistObjectTableLayout(bytes);
let dataObjectId: number | undefined;
for (let objectId = 0; objectId < table.objectCount; objectId += 1) {
@@ -192,11 +195,11 @@ it("retains explicit date uncertainty for an overlapping binary plist object", a
if (dataObjectId === undefined)
throw new Error("fixture lost its empty data");
// Keep both objects reachable through a recognized archive while sharing source bytes.
bytes.writeDoubleBE(0.0005, date.offset + 1);
bytes[date.offset + 2] = 0x40;
writeBinaryPlistObjectOffset(bytes, table, dataObjectId, date.offset + 2);
bytes.writeDoubleBE(0.0005, dateOffset + 1);
bytes[dateOffset + 2] = 0x40;
writeBinaryPlistObjectOffset(bytes, table, dataObjectId, dateOffset + 2);
expect(readBinaryPlistObjectOffset(bytes, table, dataObjectId)).toBe(
date.offset + 2,
dateOffset + 2,
);
expect(
preflightPropertyListDecode(bytes, 1024 * 1024, undefined, {
@@ -204,7 +207,7 @@ it("retains explicit date uncertainty for an overlapping binary plist object", a
}),
).toMatchObject({
datePrecisionOverlapCount: 1,
reachableDateOffsets: [date.offset],
reachableDateOffsets: [dateOffset],
});
await writeFile(join(bundle, "Panel.nib"), bytes);
@@ -306,8 +309,10 @@ describe("binary Interface Builder plist dates", () => {
const offset = submillisecond.indexOf(marker);
submillisecond.writeDoubleBE(0.1005, offset + 1);
const unknown = await nib(submillisecond, "subms");
expect(unknown.limitations).toContain(
`Panel.nib: ${unknownDateLimitation(1)}`,
expect(unknown.limitations).toContainEqual(
expect.stringMatching(
/Panel\.nib.*\b1 date value.*decoder could not represent.*timestamps/iu,
),
);
expect(JSON.stringify(unknown)).not.toContain("2001-01-01T00:00:00.100Z");
@@ -95,7 +95,6 @@ it.skipIf(isRoot)(
format: "plist",
})
.execute("inspect_keyed_archive", {});
expect(executed.ok).toBe(false);
if (executed.ok) throw new Error("expected a denied read");
expect(executed.error).toBeInstanceOf(AnalysisAccessDeniedError);
expect(projectAnalysisError(executed.error)).toMatchObject({
+40 -20
View File
@@ -10,13 +10,9 @@ import {
decodeKeyedArchiveBytes,
inspectBundleKeyedArchive,
} from "./KeyedArchiveReader.js";
import {
PlistNestingLimitError,
unknownDateLimitation,
} from "../../domain/apple/plistValue.js";
import { PlistNestingLimitError } from "../../domain/apple/plistValue.js";
import { deeplyNestedXmlPlist } from "../../../tests/fixtures/keyedArchiveXmlNesting.js";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { scanBinaryPlistScalars } from "./PlistScalars.js";
import { embedSubmillisecondDateInWideIntegerPayload } from "../../../tests/fixtures/binaryPlistScalarAlias.js";
import {
readBinaryPlistObjectOffset,
@@ -53,6 +49,17 @@ const binaryArchiveWithDate = (): Buffer =>
}),
);
// Locate the known binary date independently of the scanner under test.
const fixtureDateOffset = (bytes: Buffer, seconds: number): number => {
const encoded = Buffer.alloc(9);
encoded[0] = 0x33;
encoded.writeDoubleBE(seconds, 1);
const offset = bytes.indexOf(encoded);
if (offset < 0 || bytes.indexOf(encoded, offset + 1) !== -1)
throw new Error("binary archive fixture must contain exactly one date");
return offset;
};
const binaryArchiveWithOverlappingDate = (): Buffer => {
const bytes = Buffer.from(
buildBinary({
@@ -65,19 +72,17 @@ const binaryArchiveWithOverlappingDate = (): Buffer => {
$top: { root: { UID: 1 } },
}),
);
const date = scanBinaryPlistScalars(bytes).dates[0];
if (date === undefined)
throw new Error("binary archive fixture lost its date");
bytes.writeDoubleBE(0.0005, date.payloadOffset);
const dateOffset = fixtureDateOffset(bytes, 0);
bytes.writeDoubleBE(0.0005, dateOffset + 1);
const objectTable = readBinaryPlistObjectTableLayout(bytes);
for (let objectId = 0; objectId < objectTable.objectCount; objectId += 1) {
const offset = readBinaryPlistObjectOffset(bytes, objectTable, objectId);
if (offset !== date.offset && bytes[offset] === 0x40) {
if (offset !== dateOffset && bytes[offset] === 0x40) {
writeBinaryPlistObjectOffset(
bytes,
objectTable,
objectId,
date.payloadOffset + 1,
dateOffset + 2,
);
return bytes;
}
@@ -87,14 +92,12 @@ const binaryArchiveWithOverlappingDate = (): Buffer => {
const binaryArchiveDateCrossingOffsetTable = (): Buffer => {
const bytes = Buffer.from(binaryArchiveWithDate());
const date = scanBinaryPlistScalars(bytes).dates[0];
if (date === undefined)
throw new Error("binary archive fixture lost its date");
const dateOffset = fixtureDateOffset(bytes, 0.1);
const objectTable = readBinaryPlistObjectTableLayout(bytes);
let dateObjectId = -1;
for (let objectId = 0; objectId < objectTable.objectCount; objectId += 1) {
if (
readBinaryPlistObjectOffset(bytes, objectTable, objectId) === date.offset
readBinaryPlistObjectOffset(bytes, objectTable, objectId) === dateOffset
)
dateObjectId = objectId;
}
@@ -585,6 +588,9 @@ const dateScalar = (iso8601: string | null) => ({
value: { $plist_type: "date", iso8601 },
});
const oneUnknownDate = /\b1 date value.*could not represent.*timestamps/iu;
const twoUnknownDates = /\b2 date value.*could not represent.*timestamps/iu;
describe("binary keyed archive dates", () => {
const selection = { offset: 0, limit: 10 };
@@ -594,7 +600,11 @@ describe("binary keyed archive dates", () => {
expect(graph.objects[1]).toMatchObject(
dateScalar("2001-01-01T00:00:00.100Z"),
);
expect(graph.limitations).not.toContain(unknownDateLimitation(1));
expect(graph.limitations).not.toContainEqual(
expect.stringMatching(
/date value.*decoder could not represent.*timestamps/iu,
),
);
expect(Buffer.compare(bytes, pythonBinaryDateControl)).toBe(0);
expect(createHash("sha256").update(bytes).digest("hex")).toBe(
"39f08db8341bd74220352dfd559485515cce1e6035e08f27a59ab8f856374a30",
@@ -609,7 +619,11 @@ describe("binary keyed archive dates", () => {
expect(inspected.objects[1]).toMatchObject(
dateScalar("2001-01-01T00:00:00.100Z"),
);
expect(inspected.limitations).not.toContain(unknownDateLimitation(1));
expect(inspected.limitations).not.toContainEqual(
expect.stringMatching(
/date value.*decoder could not represent.*timestamps/iu,
),
);
expect(
Buffer.compare(await readFile(join(root, "archive.plist")), bytes),
).toBe(0);
@@ -664,7 +678,9 @@ describe("binary keyed archive dates", () => {
expect(JSON.stringify(graph.objects[1]?.value)).not.toContain(
"2001-01-01T00:00:00.100Z",
);
expect(graph.limitations).toContain(unknownDateLimitation(1));
expect(graph.limitations).toContainEqual(
expect.stringMatching(oneUnknownDate),
);
expect(graph.limitations).not.toContainEqual(
expect.stringMatching(/number metadata.*could not.*observed/iu),
);
@@ -680,7 +696,9 @@ describe("binary keyed archive dates", () => {
parameters: { path: "archive.plist" },
});
expect(inspected.objects[1]).toMatchObject(dateScalar(null));
expect(inspected.limitations).toContain(unknownDateLimitation(1));
expect(inspected.limitations).toContainEqual(
expect.stringMatching(oneUnknownDate),
);
expect(
Buffer.compare(
await readFile(join(root, "archive.plist")),
@@ -720,7 +738,9 @@ describe("binary keyed archive dates", () => {
{ $plist_type: "date", iso8601: null },
{ $plist_type: "date", iso8601: null },
]);
expect(mixedGraph.limitations).toContain(unknownDateLimitation(2));
expect(mixedGraph.limitations).toContainEqual(
expect.stringMatching(twoUnknownDates),
);
expect(Buffer.compare(mixed, mixedOriginal)).toBe(0);
});
});
@@ -352,7 +352,9 @@ describe("Mach-O encryption reader: segment mapping", () => {
range: {
mapping: {
status: "unknown",
reason: "load command 0 LC_SEGMENT_64 appears in a 32-bit image",
reason: expect.stringMatching(
/load command 0\b.*LC_SEGMENT_64.*appears.*32-bit/iu,
),
},
},
});
@@ -486,7 +488,9 @@ describe("Mach-O encryption reader: defects are never absent", () => {
wide: false,
commands: [encryption({ cryptoff: 0, cryptsize: 0, cryptid: 0 })],
}),
reason: "load command 0 LC_ENCRYPTION_INFO_64 appears in a 32-bit image",
reason: expect.stringMatching(
/load command 0\b.*LC_ENCRYPTION_INFO_64.*appears.*32-bit/iu,
),
},
{
name: "an unexpected cmdsize",
@@ -496,8 +500,9 @@ describe("Mach-O encryption reader: defects are never absent", () => {
encryption({ cryptoff: 0, cryptsize: 0, cryptid: 0 }, true, 32),
],
}),
reason:
"load command 0 LC_ENCRYPTION_INFO_64 has cmdsize 32, expected 24",
reason: expect.stringMatching(
/load command 0\b.*LC_ENCRYPTION_INFO_64.*cmdsize 32\b.*expected 24\b/iu,
),
},
{
name: "a range beyond the slice",
@@ -669,12 +674,15 @@ describe("Mach-O encryption reader: universal files", () => {
},
{
status: "malformed",
reason:
"FAT architecture 1 CPU type/subtype disagrees with its Mach-O header",
reason: expect.stringMatching(
/FAT architecture 1\b.*CPU type\/subtype.*disagree.*Mach-O header/iu,
),
},
{
status: "malformed",
reason: "FAT architecture 2 slice range extends beyond the file",
reason: expect.stringMatching(
/FAT architecture 2\b.*slice range.*beyond.*file/iu,
),
},
{ status: "encrypted", reason: null },
]);
@@ -745,7 +753,9 @@ describe("Mach-O encryption reader: universal files", () => {
{
cpu_type: CPU.arm64.type,
status: "malformed",
reason: "FAT architecture 1 slice range extends beyond the file",
reason: expect.stringMatching(
/FAT architecture 1\b.*slice range.*beyond.*file/iu,
),
commands: [],
},
],
@@ -794,7 +804,9 @@ describe("Mach-O encryption reader: large universal files", () => {
{
container_file_offset: size,
status: "malformed",
reason: "FAT architecture 1 slice range extends beyond the file",
reason: expect.stringMatching(
/FAT architecture 1\b.*slice range.*beyond.*file/iu,
),
},
],
});
@@ -802,7 +814,9 @@ describe("Mach-O encryption reader: large universal files", () => {
// unresolved JVM identity.
expect(await readMachoImage(sparseReadAt, size)).toEqual({
status: "malformed",
reason: "FAT architecture 1 slice range extends beyond the file",
reason: expect.stringMatching(
/FAT architecture 1\b.*slice range.*beyond.*file/iu,
),
});
});
});
@@ -909,8 +923,9 @@ describe("Mach-O encryption reader: mapping budget", () => {
]);
expect(facts.slices[1]?.range?.mapping).toEqual({
status: "unknown",
reason:
"mapping needs more than the 5 segment and section records shared by all slices",
reason: expect.stringMatching(
/mapping.*more than.*5 segment and section records.*slices/iu,
),
});
});
});
@@ -1889,7 +1889,9 @@ describe("Mach-O load command reader failures", () => {
view.setUint32(4, 8, true);
expect(await read(machoImage({ commands: [short] }))).toMatchObject({
status: "malformed",
reason: "load command 0 is too short to hold a string offset",
reason: expect.stringMatching(
/load command 0\b.*too short.*string offset/iu,
),
});
view.setUint32(0, LC.CODE_SIGNATURE, true);
expect(await read(machoImage({ commands: [short] }))).toMatchObject({
@@ -1943,7 +1945,9 @@ describe("Mach-O load command reader failures", () => {
const fat = fatImage([{ cpu: CPU.arm64, bytes: machoImage({}) }]);
expect(await read(fat.subarray(0, 4100))).toMatchObject({
status: "malformed",
reason: "FAT architecture 0 slice range extends beyond the file",
reason: expect.stringMatching(
/FAT architecture 0\b.*slice range.*beyond.*file/iu,
),
});
});
@@ -1968,7 +1972,9 @@ describe("Mach-O load command reader defect precedence", () => {
new DataView(unsized.buffer).setUint32(0, LC.RPATH, true);
const decodeFirst = {
status: "malformed",
reason: "load command 0 is too short to hold a string offset",
reason: expect.stringMatching(
/load command 0\b.*too short.*string offset/iu,
),
};
// An earlier command's decode defect wins over a later framing defect,
expect(await read(machoImage({ commands: [shortRpath, unsized] }))).toEqual(
@@ -2019,7 +2025,9 @@ describe("Mach-O load command reader defect precedence", () => {
new DataView(ranged.buffer).setUint32(8 + 20 + 12, ranged.length, false);
expect(await read(ranged)).toEqual({
status: "unsupported",
reason: "FAT architecture 0 alignment exponent 53 is unsupported",
reason: expect.stringMatching(
/FAT architecture 0\b.*alignment exponent 53\b.*unsupported/iu,
),
});
});
});
+6 -2
View File
@@ -540,8 +540,12 @@ describe("binary plist numeric association", () => {
9007199254740992, 9007199254740992,
]);
expect(numbers.limitations()).toEqual([
'1 observed integer literal(s) in the original archive exceed the exact range of a JSON number. Decoded numbers may lose integer precision unless represented as { "$plist_type": "integer", "decimal": "<exact digits>" }; the original archive bytes retain those literals.',
"2 number(s) beyond the exact integer range remain as decoded because their source integer/real type or reference marker cannot be associated unambiguously; integer precision loss remains possible.",
expect.stringMatching(
/\b1 observed integer literal.*original archive.*JSON number.*lose integer precision.*\$plist_type.*integer.*decimal.*original archive bytes retain/iu,
),
expect.stringMatching(
/\b2 number.*integer range.*source integer\/real type.*cannot be associated unambiguously.*precision loss/iu,
),
]);
});
@@ -14,7 +14,6 @@ it("refuses an XML plist whose DOM exceeds the native decode budget", () => {
const parsed = parsePlistXmlSource(
xmlPlist(`<array>\n${"<true/>\n".repeat(100_000)}</array>\n`),
);
expect(parsed.ok).toBe(false);
if (parsed.ok) throw new Error("Expected a decode budget refusal");
expect(parsed.error).toBeInstanceOf(AnalysisResourceConstraintError);
expect(parsed.error.message).toContain("native plist decode budget");
@@ -28,7 +27,6 @@ it.each(["é", "_", ":", "雪", "\u{1d4b3}"])(
const parsed = parsePlistXmlSource(
xmlPlist(`<array>${`<${name}/>`.repeat(100_000)}</array>`),
);
expect(parsed.ok).toBe(false);
if (parsed.ok) throw new Error("Expected a decode budget refusal");
expect(parsed.error).toBeInstanceOf(AnalysisResourceConstraintError);
},
@@ -38,7 +36,6 @@ it("refuses text whose entity references exceed the decode budget", () => {
const parsed = parsePlistXmlSource(
xmlPlist(`<array><string>${"&amp;".repeat(1_000_000)}</string></array>`),
);
expect(parsed.ok).toBe(false);
if (parsed.ok) throw new Error("Expected a decode budget refusal");
expect(parsed.error).toBeInstanceOf(AnalysisResourceConstraintError);
});
+2 -2
View File
@@ -3,13 +3,13 @@ import { join } from "node:path";
import { expect, it, vi } from "vitest";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import {
locateBunStandaloneGraph,
probeBunStandaloneGraph,
bunModuleCountLimit,
BUN_GRAPH_SUFFIX_BYTES,
} from "./BunModuleGraphLayout.js";
import { decodeBunModuleGraph } from "./BunModuleGraphMembers.js";
import {
locateBunGraphFixture,
bunModuleGraphPayload,
bunPrefixedPayload,
machoBunSection,
@@ -25,7 +25,7 @@ async function locate(bytes: Buffer) {
await writeFile(path, bytes);
const handle = await open(path, "r");
try {
const result = await locateBunStandaloneGraph(handle, bytes.length);
const result = await locateBunGraphFixture(handle, bytes.length);
if (result.status === "present" && result.graph !== undefined) {
const graph = result.graph;
return {
-11
View File
@@ -124,17 +124,6 @@ interface SectionWindow {
readonly truncated: boolean;
}
/** Probe, then read the one claimed payload. */
export const locateBunStandaloneGraph = async (
handle: FileHandle,
fileSize: number,
signal?: AbortSignal,
): Promise<BunStandaloneRecognition> => {
const probed = await probeBunStandaloneGraph(handle, fileSize, signal);
if (probed.status === "absent" || probed.claim === undefined) return probed;
return loadBunStandaloneGraph(handle, probed.claim, signal);
};
/**
* Locate at most one graph claim with bounded reads of headers, length
* prefixes, and suffixes. Payload data is not read. Multiple real claims are
@@ -1,12 +1,26 @@
import type { FileHandle } from "node:fs/promises";
import {
BUN_OFFSETS_BYTES,
BUN_RECORD_BYTES_1_3_3,
BUN_RECORD_BYTES_1_3_9,
BUN_TRAILER,
loadBunStandaloneGraph,
probeBunStandaloneGraph,
type BunLayoutRevision,
type BunStandaloneRecognition,
type BunStringPointer,
} from "./BunModuleGraphLayout.js";
/** Exercise the same two-stage probe and payload read as the artifact reader. */
export const locateBunGraphFixture = async (
handle: FileHandle,
fileSize: number,
): Promise<BunStandaloneRecognition> => {
const probed = await probeBunStandaloneGraph(handle, fileSize);
if (probed.status === "absent" || probed.claim === undefined) return probed;
return loadBunStandaloneGraph(handle, probed.claim);
};
export interface BunFixtureModule {
readonly name?: string | Buffer;
readonly contents?: Buffer;
@@ -194,7 +194,6 @@ it.each(["inventory_artifact", "inspect_artifact"] as const)(
{},
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected provider cancellation");
expect(projectAnalysisError(result.error).code).toBe("cancelled");
expect(result.error.cause).toBe(reason);
@@ -83,7 +83,7 @@ it.each([
ok: false,
error: {
reason: "format",
message: "PYZ marshal string is not valid UTF-8",
message: expect.stringContaining("UTF-8"),
},
});
});
@@ -127,7 +127,7 @@ it.each(["6c0200000003000000", "6cfeffffff03000000"])(
ok: false,
error: {
reason: "format",
message: "PYZ marshal long is not normalized",
message: expect.stringMatching(/long.*not normalized/iu),
},
});
},
@@ -922,7 +922,7 @@ it("retains the descriptor owner when admission cleanup fails", async () => {
reader.entries()[Symbol.asyncIterator]().next(),
).rejects.toMatchObject({
reason: "format",
message: `PyInstaller source is not a regular file: ${directory}`,
message: expect.stringContaining(directory),
cleanup: {
reason: closeFailure.message,
resources: [directory],
@@ -311,7 +311,7 @@ it.each(["ENOENT", "ENOTDIR"] as const)(
);
expect(cause).toMatchObject({
reason: "integrity",
message: `PyInstaller source identity changed: ${path}`,
message: expect.stringContaining(path),
});
expect(
projectAnalysisError(
@@ -321,7 +321,7 @@ it.each(["ENOENT", "ENOTDIR"] as const)(
category: "integrity_mismatch",
details: {
reason: "integrity",
detail: `PyInstaller source identity changed: ${path}`,
detail: expect.stringContaining(path),
},
});
}
@@ -700,7 +700,6 @@ for (const cancel of [false, true]) {
release.resolve();
const result = await reading;
if (failClose || cancel) {
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected read failure");
expect(result.cause).toMatchObject(
failClose
@@ -709,7 +708,6 @@ for (const cancel of [false, true]) {
);
if (failClose) expect(owner?.handle.fd).toBeGreaterThanOrEqual(0);
} else {
expect(result.ok).toBe(true);
if (!result.ok) throw result.cause;
expect(
Buffer.compare(result.value.subarray(16), Buffer.from("body")),
@@ -20,7 +20,7 @@ it("classifies a source replacement during admission as an integrity failure", a
await expect(reader.prepareContainer("0".repeat(64))).rejects.toMatchObject(
{
reason: "integrity",
message: `PyInstaller source changed during admission: ${path}`,
message: expect.stringContaining(path),
},
);
expect(openRegularFile).toHaveBeenCalledWith(path, {
+8 -2
View File
@@ -176,7 +176,10 @@ it("reports a truncated table before deferred signature rejection", async () =>
header.writeUInt32BE(12, 8);
expect(await readImage(header, 16)).toMatchObject({
ok: false,
error: { reason: "format", message: "PYZ table is truncated" },
error: {
reason: "format",
message: expect.stringMatching(/table.*truncated/iu),
},
});
});
@@ -185,7 +188,10 @@ it("reports the signature after completing a readable table and image hash", asy
image.write("BAD\0");
expect(await readImage(image)).toMatchObject({
ok: false,
error: { reason: "format", message: "PYZ archive magic does not match" },
error: {
reason: "format",
message: expect.stringMatching(/magic.*match/iu),
},
});
});
@@ -26,7 +26,6 @@ it("shares a capture limit when browser providers use one admission", async () =
const pending = first.analyzeBundle(input);
const blocked = await second.analyzeBundle(input);
try {
expect(blocked.ok).toBe(false);
if (blocked.ok) throw new Error("expected the shared admission to refuse");
expect(projectAnalysisError(blocked.error)).toMatchObject({
code: "resource_constraint",
@@ -52,7 +51,6 @@ it("does not share a capture limit across browser providers", async () => {
const other = second.analyzeBundle(input);
try {
const blocked = await other;
expect(blocked.ok).toBe(false);
if (blocked.ok) throw new Error("expected capture to fail closed");
expect(projectAnalysisError(blocked.error).code).not.toBe(
"resource_constraint",
-1
View File
@@ -203,7 +203,6 @@ it.each(
target_id: "duplicate",
observation_ms: 0,
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected ambiguous target rejection");
expect(result.error).toBeInstanceOf(BrowserObservationError);
expect(result.error.userMessage).toContain("ambiguous");
@@ -140,7 +140,6 @@ it("retains Inspector scripts and contexts when final ASAR authorization cannot
};
try {
const control = await observeJavaScriptRuntime(provider, input);
expect(control.ok).toBe(true);
if (!control.ok) throw control.error;
expect(control.value.normalized_result).toMatchObject({
scripts: { observed_total: 1 },
@@ -149,7 +148,6 @@ it("retains Inspector scripts and contexts when final ASAR authorization cannot
await prependAsarJsonWhitespace(archive, " ");
handleState.failCloseCount = 1;
const result = await observeJavaScriptRuntime(provider, input);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected descriptor close failure");
expect(result.error.cleanupIncomplete).toBe(true);
expect(result.error.partialObservation).toMatchObject({
@@ -63,7 +63,6 @@ it("preserves completed prefix and suffix facts when one script times out", asyn
expect(calls).toEqual(
captured.scripts.items.map(({ script_key }) => script_key),
);
expect(result.ok).toBe(true);
if (!result.ok) throw result.error;
expect(
result.value.observations.chunks.edges.map(({ specifier }) => specifier),
@@ -114,7 +113,6 @@ it("marks the unstarted suffix when the aggregate deadline expires", async () =>
expect(calls).toEqual(
captured.scripts.items.slice(0, 2).map(({ script_key }) => script_key),
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected total deadline failure");
expect(result.error._tag).toBe("AnalysisTimeoutError");
expect(result.error.partialObservation).toMatchObject({
@@ -176,7 +174,6 @@ it("stops after a middle-script result-capacity failure and retains prefix facts
expect(calls).toEqual(
captured.scripts.items.slice(0, 2).map(({ script_key }) => script_key),
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected result-capacity failure");
expect(result.error).toBe(primary);
expect(result.error.partialObservation).toMatchObject({
@@ -234,7 +231,6 @@ it("carries the exact caller cancellation reason and completed prefix facts", as
const result = await pending;
analyzer.releaseReservation();
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected cancellation");
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(exactReason);
@@ -276,7 +272,6 @@ it("retains all completed facts when cancellation arrives during worker close",
const result = await pending;
analyzer.releaseReservation();
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected cancellation during finalization");
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.partialObservation).toMatchObject({
@@ -316,7 +311,6 @@ it("retains cleanup ownership after mixed worker/close failure and resolves it o
expect((await analyzer.reserve()).ok).toBe(true);
const result = await analyzer.analyze(captured, noSourceMaps, input);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected mixed failure");
expect(result.error.cleanupIncomplete).toBe(true);
expect(result.error.cleanupPrimary).toBe(primary);
@@ -347,7 +341,6 @@ it("shares one capture reservation when analyzers use one admission", async () =
const second = new IsolatedWebBundleAnalyzer(() => inertWorker(), admission);
expect((await first.reserve()).ok).toBe(true);
const blocked = await second.reserve();
expect(blocked.ok).toBe(false);
if (blocked.ok) throw new Error("Expected shared admission refusal");
expect(projectAnalysisError(blocked.error)).toMatchObject({
code: "resource_constraint",
@@ -318,7 +318,6 @@ it("retains the partial capture without taskkill when Windows lineage is unavail
application_root: dirname(process.execPath),
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected unverified cleanup to fail");
expect(terminateTree).not.toHaveBeenCalled();
expect(result.error).toMatchObject({
@@ -379,7 +378,6 @@ it("retains the initial snapshot when the final state read fails", async () => {
application_root: dirname(process.execPath),
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected final state capture to fail");
const partialObservation = result.error.partialObservation;
expect(partialObservation).toMatchObject({
@@ -412,7 +410,6 @@ it("rejects active capture before launch when EnableNodeCliInspectArguments fuse
application_root: root,
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected capture to fail");
expect(launchSpy).not.toHaveBeenCalled();
expect(result.error).toMatchObject({
@@ -509,7 +506,6 @@ it("cancels a windowless wait action and cleans up the owned application", async
expect(application.firstWindow).toHaveBeenCalledOnce();
expect(application.close).toHaveBeenCalledOnce();
expect(cleanupSpies.some((spy) => spy.mock.calls.length > 0)).toBe(true);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected the capture to be cancelled");
expect(result.error).toMatchObject({
_tag: "BrowserObservationError",
@@ -621,7 +617,6 @@ it("cancels a pending initial state read and cleans up the owned application", a
expect(application.close).toHaveBeenCalledOnce();
expect(cleanupSpies.some((spy) => spy.mock.calls.length > 0)).toBe(true);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected the capture to be cancelled");
expect(result.error).toMatchObject({
_tag: "BrowserObservationError",
@@ -666,7 +661,6 @@ it("observes an initial state read started after cancellation and rejected by cl
expect(application.close).toHaveBeenCalledOnce();
expect(cleanupSpies.some((spy) => spy.mock.calls.length > 0)).toBe(true);
expect(unhandled).toEqual([]);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected the capture to be cancelled");
expect(result.error).toMatchObject({
_tag: "BrowserObservationError",
@@ -703,7 +697,6 @@ it("keeps a failure observed with cancellation as the cancellation cause", async
const result = await captureWithController(controller, application, []);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected the capture to be cancelled");
expect(result.error).toMatchObject({
_tag: "BrowserObservationError",
@@ -742,7 +735,6 @@ it("reports an expired final state grace period as the caller's cancellation", a
await vi.advanceTimersByTimeAsync(5_000);
const result = await capture;
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected the capture to be cancelled");
expect(result.error).toMatchObject({
_tag: "BrowserObservationError",
@@ -795,7 +787,6 @@ it("cancels a completed capture when cancellation arrives during application clo
const result = await captureWithController(controller, application, []);
expect(application.close).toHaveBeenCalledOnce();
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected the capture to be cancelled");
expect(result.error).toMatchObject({
_tag: "BrowserObservationError",
@@ -838,7 +829,6 @@ it("cancels a completed capture when cancellation arrives during process cleanup
const result = await captureWithController(controller, application, []);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected the capture to be cancelled");
expect(result.error).toMatchObject({
_tag: "BrowserObservationError",
@@ -873,7 +863,6 @@ it("keeps cleanup failure precedence over cancellation observed during cleanup",
const result = await captureWithController(controller, application, []);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected cleanup to fail");
expect(result.error).toMatchObject({
cleanupIncomplete: true,
@@ -925,7 +914,6 @@ it.each(["process", "transport"] as const)(
const result = await captureWithController(controller, application, []);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected incomplete cleanup");
expect(result.error).toMatchObject({
cleanupIncomplete: true,
@@ -1019,7 +1019,6 @@ it("preserves literal replacement characters in valid UTF-8 worker facts", async
makeScript("fetch('/literal-�');"),
10_000,
);
expect(result.ok).toBe(true);
if (!result.ok) throw new Error(result.error.error.message);
if (result.value.status !== "analyzed")
throw new Error("Expected analyzed worker facts");
@@ -48,7 +48,6 @@ it("refuses before spawning or reading source when no host result capacity remai
try {
const result = await worker.analyze(script, 10_000);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected result-capacity refusal");
const refusal = result.error.error;
expect(refusal).toBeInstanceOf(AnalysisResourceConstraintError);
@@ -144,7 +144,6 @@ it("retains completed facts and resolves a result-file lease through another cle
makeScript("fetch('/retained');"),
10_000,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected retained result cleanup failure");
expect(result.error.completed).toMatchObject({
status: "analyzed",
+1 -1
View File
@@ -8,7 +8,7 @@ import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import { analysisErrorProjectionSchema } from "../../contracts/errorSchemas.js";
import { inspectWebNetworkCaptureInputSchema } from "../../domain/webNetworkCapture.js";
import { historicalCaptureFailure } from "./CaptureFailures.js";
import { NODE_CAPTURE_PROCESS_MEMORY_LIMIT_BYTES } from "./CaptureRelease.js";
import { NODE_CAPTURE_PROCESS_MEMORY_LIMIT_BYTES } from "../../../tests/fixtures/captureDecoderLimits.js";
import { redactExplicitFailure } from "../../domain/explicitSensitiveFailure.js";
import { OwnedCommandFailure } from "../../process/OwnedCommand.js";
-6
View File
@@ -24,10 +24,4 @@ export const captureDecoderProcessMemoryLimitBytes = (
export const NODE_CAPTURE_HEAP_ENFORCED =
javaScriptRuntime(process.versions).id === "node";
/** Commit limit for the default heap. A configured heap uses `captureDecoderProcessMemoryLimitBytes`. */
export const NODE_CAPTURE_PROCESS_MEMORY_LIMIT_BYTES =
captureDecoderProcessMemoryLimitBytes(
NODE_CAPTURE_HEAP_LIMITS.oldGenerationMiB,
);
/** Fixed offline interpretation profile, not an authenticated producer claim. */
@@ -217,7 +217,6 @@ it("refuses other declared trace versions with verified archive identity", async
const result = await new PlaywrightTraceDecoder(resources).inspect(
fixture.path,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected version refusal");
expect(result.error._tag).toBe("AnalysisUnsupportedTargetError");
expect(
@@ -239,7 +238,6 @@ it("preserves opaque pre-cancellation without reading or claiming a source snaps
"/unused.zip",
controller.signal,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected cancellation");
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(reason);
@@ -307,7 +305,6 @@ it("refuses oversized physical lines before parsing their representation", async
const result = await new PlaywrightTraceDecoder(resources).inspect(
fixture.path,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected line capacity refusal");
expect(result.error._tag).toBe("AnalysisResourceConstraintError");
expect(resources.hasPending).toBe(false);
@@ -329,7 +326,6 @@ it("never promotes facts from a stream whose ZIP CRC fails", async () => {
const result = await new PlaywrightTraceDecoder(resources).inspect(
fixture.path,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected CRC refusal");
expect(result.error._tag).toBe("AnalysisInputError");
const partial = playwrightTraceSchema.parse(
@@ -354,7 +350,6 @@ it("rejects a sparse over-budget archive before hashing or expansion", async ()
const result = await new PlaywrightTraceDecoder(resources).inspect(
fixture.path,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected archive admission refusal");
expect(result.error._tag).toBe("AnalysisResourceConstraintError");
expect(result.error.partialObservation).toBeUndefined();
@@ -378,7 +373,6 @@ it("bounds JSON node expansion even when many tokens fit inside a small physical
const result = await new PlaywrightTraceDecoder(resources).inspect(
fixture.path,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected node admission refusal");
expect(result.error._tag).toBe("AnalysisResourceConstraintError");
expect(resources.hasPending).toBe(false);
@@ -26,7 +26,6 @@ it("preserves only fully verified streams when cancellation arrives before the n
const result = await new PlaywrightTraceService(
new PlaywrightTraceDecoder(resources),
).inspect({ path: fixture.path }, controller.signal);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected cancellation");
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(reason);
@@ -61,7 +60,6 @@ it("retains complete Evidence and the exact failed reader owner when abort arriv
const result = await new PlaywrightTraceService(
new PlaywrightTraceDecoder(resources),
).inspect({ path: fixture.path }, controller.signal);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected composed failure");
expect(result.error.cleanupIncomplete).toBe(true);
expect(result.error.cleanupPrimary?._tag).toBe("AnalysisCancelledError");
@@ -101,7 +99,6 @@ it("keeps completed Evidence on cancellation during the outer descriptor close",
const result = await new PlaywrightTraceService(
new PlaywrightTraceDecoder(resources),
).inspect({ path: fixture.path }, controller.signal);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected cancellation");
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(reason);
@@ -136,7 +133,6 @@ it("keeps an existing unsupported-version failure ahead of cancellation during c
fixture.path,
controller.signal,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected version refusal");
expect(controller.signal.aborted).toBe(true);
expect(result.error._tag).toBe("AnalysisUnsupportedTargetError");
@@ -166,7 +162,6 @@ it("retains independent reader and file owners until both cleanup retries comple
const result = await new PlaywrightTraceService(
new PlaywrightTraceDecoder(resources),
).inspect({ path: fixture.path });
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected cleanup failure");
expect(result.error.cleanupIncomplete).toBe(true);
expect(resources.cleanupResources).toHaveLength(2);
@@ -216,7 +211,6 @@ it.skipIf(process.platform === "win32")(
const result = await new PlaywrightTraceDecoder(resources).inspect(
fixture.path,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected source identity refusal");
expect(result.error._tag).toBe("AnalysisArtifactChangedError");
expect((await readFile(held)).equals(original)).toBe(true);
@@ -248,7 +242,6 @@ it("bounds active imports without an unbounded queue and releases admission afte
try {
await reached.promise;
const second = await port.inspect(fixture.path);
expect(second.ok).toBe(false);
if (second.ok) throw new Error("Expected capacity refusal");
expect(second.error._tag).toBe("AnalysisResourceConstraintError");
release.resolve();
-12
View File
@@ -227,18 +227,6 @@ export const delimitedOutputInputError = (
{ path, reason: "invalid_value", message },
]);
/** Render a validated row page, preceded by a versioned metadata record. */
export const formatDelimitedRows = (
rows: unknown,
format: "csv" | "tsv",
options: CliDelimitedOutputOptions,
): Result<string, AnalysisInputError> => {
const prepared = prepareDelimitedOutput(rows, format, options);
return prepared.ok
? ok([...formatDelimitedOutputParts(prepared.value)].join(""))
: prepared;
};
/** Stream a page after its rows and cells have been validated and snapshotted. */
export function* formatDelimitedOutputParts({
encodedHeaders,
+1 -1
View File
@@ -295,7 +295,7 @@ const registerXrefsCommand = (
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
): void => {
cli.command(CLI_COMMANDS.xrefs, {
description: `List bounded references to an analyzed address. ${namedAddressGuidance} ${equalSelectorGuidance}`,
description: `List bounded, untyped references to an analyzed address. For typed incoming references, use inspect-native-instruction at the target address where supported. ${namedAddressGuidance} ${equalSelectorGuidance}`,
args: z.object({
path: localPathStringSchema.describe("App or program path"),
address: z
+1 -63
View File
@@ -140,68 +140,6 @@ export const registerCoreNativeCommands = (
}),
),
});
cli.command(CLI_COMMANDS.procedureReferences, {
description: `Inspect incoming or outgoing references for one native procedure. ${addressOrProcedureGuidance} ${equalSelectorGuidance}`,
args: z.object({
path: localPathStringSchema.describe("Local executable path"),
procedure: z
.string()
.min(1)
.optional()
.describe(
`Procedure name or provider address. ${addressOrProcedureGuidance}`,
),
}),
options: z.object({
procedure: z
.string()
.min(1)
.optional()
.describe(
`Procedure name or provider address; use --procedure=<value> for leading dashes. ${addressOrProcedureGuidance}`,
),
direction: z
.enum(["incoming", "outgoing"])
.default("outgoing")
.describe("Reference direction. Defaults to outgoing."),
snapshot: snapshotSelectionOption,
...analysisEvidenceDeliveryOptions,
...nativeOpenOptionFields,
}),
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.procedureReferences, async () => {
const delivery = {
providerId: "auto",
operation: "procedure_references",
signal,
detail: options.detail,
evidenceOutput: options.evidence_output,
};
const invalid = cliEvidenceDeliveryInputError(delivery);
if (invalid !== undefined) return projectAnalysisError(invalid);
const result = await runDirectAnalysis(
args.path,
"procedure_references",
{
procedure: resolveCliAnalysisSelector(
args.procedure,
options.procedure,
"procedure",
"procedure_references",
),
direction: options.direction,
},
{
...directAnalysisOptionsFromCli(logger, options),
signal,
includeEvidenceSources: options.evidence_output !== undefined,
},
);
return publishCliAnalysisEvidence(result, delivery);
}),
),
});
cli.command(CLI_COMMANDS.readBytes, {
description:
"Read provider memory bytes; Ghidra memory may include loader fixups and differ from file bytes",
@@ -374,7 +312,7 @@ export const registerCoreNativeCommands = (
cli.command(command, {
description:
operation === "inspect_native_instruction"
? "Inspect one native instruction with decoded operand facts"
? "Inspect an exact code or data address for decoded instructions and typed incoming references where supported"
: "Resolve one static native call site",
args: z.object({
path: localPathStringSchema.describe(
-1
View File
@@ -35,7 +35,6 @@ export const CLI_COMMANDS = Object.freeze({
annotateNativeFunction: "annotate-native-function",
inspectNativeApi: "inspect-native-api",
instructions: "instructions",
procedureReferences: "procedure-references",
search: "search",
importReferenceSource: "import-reference-source",
inspectArtifact: "inspect-artifact",
-2
View File
@@ -38,7 +38,6 @@ it.each([
REA_GHIDRA_CHECKPOINT_DIR: "/tmp/checkpoints",
[setting]: value,
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("conflicting inputs accepted");
expect(result.error.message).toContain("checkpoints");
expect(result.error).toMatchObject({
@@ -70,7 +69,6 @@ it.each([
parseEnvironmentValues(environment),
parseConfig(environment),
]) {
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected invalid environment");
expect(result.error.settings).toHaveLength(1);
expect(result.error.settings[0]?.constraint).toContain("NUL");
+14 -6
View File
@@ -20,7 +20,6 @@ import { parseConfig } from "./parseConfig.js";
import {
CAPTURE_DECODER_OLD_GENERATION_SETTING,
captureDecoderHeapConstraint,
captureDecoderHeapRecovery,
DEFAULT_CAPTURE_DECODER_HEAP,
parseCaptureDecoderHeap,
} from "./captureDecoderHeap.js";
@@ -97,9 +96,15 @@ it("uses a valid REA_CAPTURE_DECODER_OLD_GENERATION_MIB override", async () => {
code: "resource_constraint",
details: { resource: "memory" },
});
expect(projected.remediation.action).toContain(
captureDecoderHeapRecovery(configured(256)),
expect(projected.remediation.action).toMatch(
/REA_CAPTURE_DECODER_OLD_GENERATION_MIB.*256 MiB.*source setting/iu,
);
expect(projected.remediation.action).toContain("--max-old-space-size=256");
expect(projected.remediation.action).toMatch(
/floor 8 MiB.*maximum 16384 MiB/iu,
);
expect(projected.remediation.action).toContain("update_analysis_settings");
expect(projected.remediation.action).toMatch(/restart REA/iu);
if (process.versions.bun === undefined)
expect(projected.details).toMatchObject({
limit: {
@@ -163,16 +168,19 @@ it("reports the heap the selected adapter launches when decoder environment diff
if (!reported.ok)
throw new Error(projectAnalysisError(reported.error).message);
const limitations = reported.value.limitations.join("\n");
expect(limitations).toContain(captureDecoderHeapRecovery(launched.heap));
expect(limitations).toMatch(
/REA_CAPTURE_DECODER_OLD_GENERATION_MIB.*256 MiB.*source setting/iu,
);
vi.spyOn(launched, "command").mockRejectedValue(heapExhaustion());
const exhausted = await decoder.inspect(input);
if (exhausted.ok) throw new Error("Heap exhaustion must fail inspection");
const projected = projectAnalysisError(exhausted.error);
expect(projected.code).toBe("resource_constraint");
expect(projected.remediation.action).toContain(
captureDecoderHeapRecovery(launched.heap),
expect(projected.remediation.action).toMatch(
/REA_CAPTURE_DECODER_OLD_GENERATION_MIB.*256 MiB.*source setting/iu,
);
expect(projected.remediation.action).toContain("--max-old-space-size=256");
if (process.versions.bun === undefined)
expect(projected.details).toMatchObject({
limit: {
-1
View File
@@ -34,7 +34,6 @@ it.each([
"9007199254740992",
])("refuses invalid allowance %s without disabling admission", (value) => {
const parsed = parseMcpInputBudget(value);
expect(parsed.ok).toBe(false);
if (parsed.ok) throw new Error("Invalid admission setting was accepted");
expect(parsed.error).toBeInstanceOf(ConfigurationError);
expect(parsed.error.settings).toEqual([
+6 -17
View File
@@ -5,10 +5,7 @@ import { KunaProvider } from "../kuna/KunaProvider.js";
import { silentLogger } from "../logger.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import {
regexMatchingDeadlineError,
launchRegexMatchingLimitation,
} from "../process/RegexMatcher.js";
import { regexMatchingDeadlineError } from "../process/RegexMatcher.js";
import { parseEnvironment } from "./environment.js";
import { parseConfig } from "./parseConfig.js";
import {
@@ -85,12 +82,7 @@ it("uses a valid REA_REGEX_MATCHING_DEADLINE_MS override", () => {
.capabilities()
.find((capability) => capability.operation === "search_procedures")
?.limitations.join(" ");
expect(hopperText).toContain(
launchRegexMatchingLimitation(
parsed.value.regexMatchingDeadline,
parsed.value.regexWorkerHeapRequest,
),
);
expect(hopperText).toMatch(/10000 ms matching deadline.*source setting/iu);
vi.stubEnv(REGEX_MATCHING_DEADLINE_SETTING, "1");
try {
@@ -99,12 +91,7 @@ it("uses a valid REA_REGEX_MATCHING_DEADLINE_MS override", () => {
.capabilities()
.find((capability) => capability.operation === "search_strings")
?.limitations.join(" ");
expect(kunaText).toContain(
launchRegexMatchingLimitation(
parsed.value.regexMatchingDeadline,
parsed.value.regexWorkerHeapRequest,
),
);
expect(kunaText).toMatch(/10000 ms matching deadline.*source setting/iu);
} finally {
vi.unstubAllEnvs();
}
@@ -115,7 +102,9 @@ it("uses a valid REA_REGEX_MATCHING_DEADLINE_MS override", () => {
.capabilities()
.find((capability) => capability.operation === "search_procedures")
?.limitations,
).toContain(launchRegexMatchingLimitation());
).toContainEqual(
expect.stringMatching(/5000 ms matching deadline.*source default/iu),
);
});
it.each([
+6 -17
View File
@@ -5,10 +5,7 @@ import { KunaProvider } from "../kuna/KunaProvider.js";
import { silentLogger } from "../logger.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import {
regexWorkerHeapError,
launchRegexMatchingLimitation,
} from "../process/RegexMatcher.js";
import { regexWorkerHeapError } from "../process/RegexMatcher.js";
import { parseEnvironment } from "./environment.js";
import { parseConfig } from "./parseConfig.js";
import {
@@ -99,12 +96,7 @@ it("uses a valid REA_REGEX_WORKER_HEAP_REQUEST_MIB override", () => {
.capabilities()
.find((capability) => capability.operation === "search_procedures")
?.limitations.join(" ");
expect(hopperText).toContain(
launchRegexMatchingLimitation(
parsed.value.regexMatchingDeadline,
parsed.value.regexWorkerHeapRequest,
),
);
expect(hopperText).toMatch(/256 MiB worker heap request.*source setting/iu);
vi.stubEnv(REGEX_WORKER_HEAP_REQUEST_SETTING, "8");
try {
@@ -115,12 +107,7 @@ it("uses a valid REA_REGEX_WORKER_HEAP_REQUEST_MIB override", () => {
.capabilities()
.find((capability) => capability.operation === "search_strings")
?.limitations.join(" ");
expect(kunaText).toContain(
launchRegexMatchingLimitation(
parsed.value.regexMatchingDeadline,
parsed.value.regexWorkerHeapRequest,
),
);
expect(kunaText).toMatch(/256 MiB worker heap request.*source setting/iu);
} finally {
vi.unstubAllEnvs();
}
@@ -131,7 +118,9 @@ it("uses a valid REA_REGEX_WORKER_HEAP_REQUEST_MIB override", () => {
.capabilities()
.find((capability) => capability.operation === "search_procedures")
?.limitations,
).toContain(launchRegexMatchingLimitation());
).toContainEqual(
expect.stringMatching(/128 MiB worker heap request.*source default/iu),
);
});
it.each([
+8 -14
View File
@@ -4,12 +4,10 @@ import { ConfigurationError } from "../domain/configurationErrors.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { parseEnvironment } from "./environment.js";
import {
DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_LIMITS,
WINDOWS_CHANGE_TIME_DRIFT_MS,
WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
resolveWindowsChangeTimeSettle,
windowsChangeTimeSettleRecovery,
windowsChangeTimeSettingFailure,
windowsChangeTimeSettingMessage,
} from "./windowsChangeTimeSettle.js";
@@ -37,7 +35,14 @@ const requiredFacts = (
it("keeps today's settle defaults when the settings are unset", () => {
expect(resolveWindowsChangeTimeSettle({})).toEqual({
ok: true,
value: DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_LIMITS,
value: {
driftMs: 2_000,
driftSource: "default",
pollMs: 25,
pollSource: "default",
maxPolls: 40,
maxPollsSource: "default",
},
});
const startup = parseEnvironment({});
if (!startup.ok) throw new Error("unset settle settings were rejected");
@@ -46,17 +51,6 @@ it("keeps today's settle defaults when the settings are unset", () => {
expect(
startup.value.REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
).toBeUndefined();
requiredFacts(
windowsChangeTimeSettleRecovery(DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_LIMITS),
{
setting: WINDOWS_CHANGE_TIME_DRIFT_MS.setting,
currentValue: "2000",
source: "default",
defaultValue: 2_000,
floor: 1,
maximum: 60_000,
},
);
});
it("uses valid settle overrides", () => {
-24
View File
@@ -68,16 +68,6 @@ export interface WindowsChangeTimeSettleLimits {
readonly maxPollsSource: "default" | "setting";
}
export const DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_LIMITS: WindowsChangeTimeSettleLimits =
{
driftMs: WINDOWS_CHANGE_TIME_DRIFT_MS.defaultValue,
driftSource: "default",
pollMs: WINDOWS_CHANGE_TIME_SETTLE_POLL_MS.defaultValue,
pollSource: "default",
maxPolls: WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS.defaultValue,
maxPollsSource: "default",
};
export const windowsChangeTimeSettingFailure = (
bound: WindowsChangeTimeBound,
currentValue: string,
@@ -183,17 +173,3 @@ export const resolveWindowsChangeTimeSettle = (
maxPollsSource: maxPolls === undefined ? "default" : "setting",
});
};
/**
* Describe the effective budget. Admission still returns the last observation
* when the poll budget runs out; these settings only change how long it waits.
*/
export const windowsChangeTimeSettleRecovery = (
limits: WindowsChangeTimeSettleLimits,
): string =>
[
`The effective ${WINDOWS_CHANGE_TIME_DRIFT_MS.setting} value is ${String(limits.driftMs)} ms (source ${limits.driftSource}; default ${String(WINDOWS_CHANGE_TIME_DRIFT_MS.defaultValue)} ms, floor ${String(WINDOWS_CHANGE_TIME_DRIFT_MS.floor)} ms, maximum ${String(WINDOWS_CHANGE_TIME_DRIFT_MS.maximum)} ms).`,
`The effective ${WINDOWS_CHANGE_TIME_SETTLE_POLL_MS.setting} value is ${String(limits.pollMs)} ms (source ${limits.pollSource}; default ${String(WINDOWS_CHANGE_TIME_SETTLE_POLL_MS.defaultValue)} ms, floor ${String(WINDOWS_CHANGE_TIME_SETTLE_POLL_MS.floor)} ms, maximum ${String(WINDOWS_CHANGE_TIME_SETTLE_POLL_MS.maximum)} ms).`,
`The effective ${WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS.setting} value is ${String(limits.maxPolls)} polls (source ${limits.maxPollsSource}; default ${String(WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS.defaultValue)} polls, floor ${String(WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS.floor)} polls, maximum ${String(WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS.maximum)} polls).`,
"Set them in the environment that starts REA, then restart REA. They do not change Windows ChangeTime itself.",
].join(" ");
+1 -1
View File
@@ -17,7 +17,7 @@ export const ANALYSIS_VIEW_TOOL_CONTRACTS = [
...toolContractMetadata("inspect_analysis_view"),
kind: "application",
description:
'Use native-procedure-references to page retained procedure reference or unresolved-call rows. Endpoint kind selected-procedure refers to the summary procedure, null preserves the original null, and shared carries an index into items.endpoints, where each other procedure is stored once per page. Exact source_address and target_address filters apply to reference rows. For inspect_api_contract parents, graphql-contract pages retained SDL types/fields, operation definitions, field references, fragment-spreads or diagnostics with exact filters and selected columns. Use javascript-routes collections to query route patterns, source-linked occurrences, source status, unknowns or summary counts from retained JavaScript application or web-bundle Evidence. Use web-bundle collections endpoints or inferences to page endpoint observations and inferences from retained analyze_web_bundle Evidence; summary returns target URL, origin, and counts. For retained trace_feature Evidence, kind native-feature-trace pages matches, references or unknowns; matches filter exact address, references filter exact source_address and target_address, source_index identifies each original row, and list_unknowns pages durable question heads. Inspect selected views of supported completed Evidence without repeating analysis. For procedure_pseudo_code Evidence, summary reports text availability and the original selection; kind native with facet pseudocode pages the original UTF-16 text. Other native facets require analyze_function. Choose a view compatible with the parent operation described below. Command summary views preserve ordered GDB records or opaque objdump stdout/stderr once, source parameters, completion facts and parent identity. They do not establish current target state. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Native export tables use kind native-exports, collection exports/issues, exact names or original indices, selected fields and matching-row pagination; parent Evidence plus source_index identifies each row. Source coverage stays separate from page coverage. BEAM views use kind beam, collections atoms/exports/imports/lambdas/chunks, exact names/indices/fields and matching-row pages; import modules intersects function names. source_index identifies original parent-and-collection rows, preserving every arity and raw atom index. beam.counts preserves null missing tables versus decoded empty tables; runtime absence remains unknown. PYC views use kind python-pyc, collection code_objects, names/indices/fields and paging; source_index scopes every same-name alternative to the parent, nested_code keeps code-constant references, and pyc.coverage preserves source coverage. Go metadata uses kind go, collection dependencies/settings, exact paths/keys or indices, fields and pagination. Row index refers to the original collection; empty selections return no rows, and dependency coverage remains unknown. Rust metadata uses kind rust, collection packages/edges, exact names/versions or original indices, selected fields, edge from_indices/to_indices and matching-row pagination. Embedded-record coverage is separate from unknown runtime dependencies. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, statuLine truncated
'For inspect_api_contract parents, graphql-contract pages retained SDL types/fields, operation definitions, field references, fragment-spreads or diagnostics with exact filters and selected columns. Use javascript-routes collections to query route patterns, source-linked occurrences, source status, unknowns or summary counts from retained JavaScript application or web-bundle Evidence. Use web-bundle collections endpoints or inferences to page endpoint observations and inferences from retained analyze_web_bundle Evidence; summary returns target URL, origin, and counts. For retained trace_feature Evidence, kind native-feature-trace pages matches, references or unknowns; matches filter exact address, references filter exact source_address and target_address, source_index identifies each original row, and list_unknowns pages durable question heads. Inspect selected views of supported completed Evidence without repeating analysis. For procedure_pseudo_code Evidence, summary reports text availability and the original selection; kind native with facet pseudocode pages the original UTF-16 text. Other native facets require analyze_function. Choose a view compatible with the parent operation described below. Command summary views preserve ordered GDB records or opaque objdump stdout/stderr once, source parameters, completion facts and parent identity. They do not establish current target state. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Native export tables use kind native-exports, collection exports/issues, exact names or original indices, selected fields and matching-row pagination; parent Evidence plus source_index identifies each row. Source coverage stays separate from page coverage. BEAM views use kind beam, collections atoms/exports/imports/lambdas/chunks, exact names/indices/fields and matching-row pages; import modules intersects function names. source_index identifies original parent-and-collection rows, preserving every arity and raw atom index. beam.counts preserves null missing tables versus decoded empty tables; runtime absence remains unknown. PYC views use kind python-pyc, collection code_objects, names/indices/fields and paging; source_index scopes every same-name alternative to the parent, nested_code keeps code-constant references, and pyc.coverage preserves source coverage. Go metadata uses kind go, collection dependencies/settings, exact paths/keys or indices, fields and pagination. Row index refers to the original collection; empty selections return no rows, and dependency coverage remains unknown. Rust metadata uses kind rust, collection packages/edges, exact names/versions or original indices, selected fields, edge from_indices/to_indices and matching-row pagination. Embedded-record coverage is separate from unknown runtime dependencies. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, status and content-type sets and record ordinals; records are pointers into the parent, excluded groups carry a reason and three fixed unknowns are always stated. Templates, parameters and names are inferences; the view copies no header, cookie or body values and refuses parents inspected with sensitive values. A capture_browser_scenario parent also supports summary and bLine truncated
inputSchema: inspectAnalysisViewInputSchema,
outputSchema: evidenceResultOf(analysisViewResultSchema),
examples: [
+3 -5
View File
@@ -205,14 +205,12 @@ export const OFFICIAL_TOOL_CONTRACTS = [
),
official(
"procedure_references",
"Return incoming or outgoing reference edges for one procedure. Ghidra preserves detailed reference flags; Hopper preserves native CallReference classifications and exact endpoints. Select detail summary to retain complete Evidence and page references or unresolved calls using inspect_analysis_view kind native-procedure-references. Page coverage describes retained observations, not complete discovery or proof the target ran.",
"Return every raw incoming or outgoing reference edge for one procedure. Endpoint procedures are resolved only from provider containment. Ghidra preserves detailed reference flags. Hopper preserves native CallReference classifications and exact endpoints while leaving unavailable detailed flags explicit.",
z.object({
procedure,
direction: z.enum(["incoming", "outgoing"]).default("outgoing"),
document,
detail: analysisDetailSchema,
}),
{ evidenceParameters: "omit-detail" },
),
official(
"procedure_pseudo_code",
@@ -233,13 +231,13 @@ export const OFFICIAL_TOOL_CONTRACTS = [
),
official(
"search_procedures",
`Search every analyzed procedure name, including secondary names exact lookup accepts, using literal matching by default or regex when requested. Each procedure appears once under its primary name; matched_aliases lists matching secondary names. Results are deterministic and complete. For Ghidra case-insensitive literal searches, raw_result contains unfiltered candidates; normalized_result contains matches. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`,
`Search every analyzed procedure name, including secondary names exact lookup accepts, using literal matching by default or regex when requested. Each procedure appears once under its primary name; matched_aliases lists matching secondary names. Results are deterministic and complete. Ghidra rows retain their reported external and thunk classification. For Ghidra case-insensitive literal searches, raw_result contains unfiltered candidates; normalized_result contains matches. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`,
z.object({ ...nativeSearchInput, detail: analysisDetailSchema }),
{ evidenceParameters: "omit-detail" },
),
official(
"search_strings",
`Search every provider-reported string object or candidate using literal matching by default or regex when requested. Results are deterministic and complete over that provider inventory, not every text sequence in artifact bytes. Kuna scans mapped sequences of at least five characters, including unterminated fragments, without computing references. Ghidra may omit packed or undefined literals, including Rust strings; an empty result does not prove text is absent. For known encoded bytes, Ghidra search_bytes can find byte matches; a hit does not establish a string or use. If an address is known, xrefs can look for references; read_bytes can inspect bytes where supported (not available with Kuna). Hopper retains typed byte decoding, native display, and explicit decoding unknowns. Adjacent long-literal fragments match independently. For Ghidra case-insensitive literal searches, raw_result contains unfiltered candidates; normalized_result contains matches. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`,
`Search every provider-reported string object or candidate using literal matching by default or regex when requested. Results are deterministic and complete over that provider inventory, not every text sequence in artifact bytes. Kuna scans mapped sequences of at least five characters, including unterminated fragments, without computing references. Ghidra reports each defined string's encoding, byte length and termination; it may omit packed or undefined literals, including Rust strings; an empty result does not prove text is absent. For known encoded bytes, Ghidra search_bytes can find byte matches; a hit does not establish a string or use. If an address is known, xrefs can look for references; read_bytes can inspect bytes where supported (not available with Kuna). Hopper retains typed byte decoding, native display, and explicit decoding unknowns. Adjacent long-literal fragments match independently. For Ghidra case-insensitive literal searches, raw_result contains unfiltered candidates; normalized_result contains matches. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`,
z.object({ ...nativeSearchInput, detail: analysisDetailSchema }),
{ evidenceParameters: "omit-detail" },
),
+13 -5
View File
@@ -1,4 +1,3 @@
import { procedureReferencesSchema } from "../domain/native/procedureReferences.js";
import {
nativeAddressListSchema,
nativeObservationSourceSchemas,
@@ -12,7 +11,6 @@ import {
nativeFunctionSummaryViewResultSchema,
nativeInventorySummaryViewResultSchema,
nativeFeatureTraceSummaryViewResultSchema,
nativeProcedureReferencesSummaryViewResultSchema,
batchDecompileSummaryViewResultSchema,
nativeLoadImageSummaryViewResultSchema,
artifactInventorySummaryViewResultSchema,
@@ -35,6 +33,11 @@ import {
analysisBookmarkSchema,
nativeInventoryRowsSchemas,
} from "../domain/native/nativeInventory.js";
import {
referenceEdgeSchema,
unresolvedCallSchema,
} from "../domain/native/nativeReference.js";
import { procedureIdentitySchema } from "../domain/native/procedureInfo.js";
import { z } from "zod";
import {
evidenceBundleExportReceiptSchema,
@@ -231,9 +234,14 @@ export const officialOutputSchemas = {
),
}),
),
procedure_references: analysisDetailOutputSchemaOf(
procedureReferencesSchema,
nativeProcedureReferencesSummaryViewResultSchema,
procedure_references: evidenceResultOf(
z.object({
procedure: procedureIdentitySchema,
direction: z.enum(["incoming", "outgoing"]),
reference_kinds_available: z.boolean(),
unresolved_calls: z.array(unresolvedCallSchema),
references: z.array(referenceEdgeSchema),
}),
),
procedure_pseudo_code: analysisDetailOutputSchemaOf(
nullableText,
@@ -1,6 +1,5 @@
import { describe, expect, it } from "vitest";
import { managedNativeVerificationInputSchema } from "../domain/managed/managedNativeVerificationSchemas.js";
import { toolContract } from "./toolContracts.js";
import { toolInputSchemaWithMetadata } from "./toolSchemaMetadata.js";
@@ -133,7 +132,6 @@ describe("workflow input contracts", () => {
};
const contract = toolContract("verify_managed_native_boundaries");
for (const schema of [
managedNativeVerificationInputSchema,
contract.inputSchema,
toolInputSchemaWithMetadata(contract),
]) {
Loaded 100 of 308 files, more files were not shown because too many files have changed in this diff. Show more