Files
rea/src/domain/analysisView/binaryLayoutView.ts
T
Edgar Arakelyan 291059adb4 feat(analysis-view): project an environment manifest from web network captures (#4954)
* feat(analysis-view): project an environment manifest from web network captures

Add the environment-manifest view kind to inspect_analysis_view for retained
HAR Evidence: the primary origin's recorded request lines become a
deterministic tool surface (names, path templates, parameters with their
rule and basis, observed query names, value-free body shapes, record
pointers), with excluded groups, three fixed unknowns and the shared
projection budget. The four format rules always apply; after-uid-literal,
opaque-token and sibling-variance are opt-in and labelled, and literal
fold-in never touches a format-rule parameter. The api-surface accumulator
moves onto shared route, media-type and budget helpers without behaviour
change.

Refs morluto/rea#4933

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(web-network): check the manifest refusal text like the api-surface refusal

The real-capture verifier stringified the whole MCP result for the
environment-manifest refusal, so a declared "[" literal matched the result
envelope itself; compare the parsed refusal as the api-surface branch does.

Refs morluto/rea#4933

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): record the hosted web-network lane run for the manifest view

Refs morluto/rea#4933

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): reconcile the manifest view verification scope note with the hosted run

Refs morluto/rea#4933

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(analysis-view): bound body, route and name work in the environment manifest

Addresses the automated review findings on the PR: read request bodies at the
top level only instead of walking nested values; admit every literal route
part before materializing it; compare at most 32 segments in sibling-variance
and say so; keep stop words literal after a uid literal; keep the next
collision suffix per tool-name base; preserve a templated exclusion when two
groups print the same row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(analysis-view): keep manifest folding, naming and media-type reads linear

Addresses the second automated review round: fold literal siblings through
set membership, key collision suffixes by the truncated name they share, and
charge a request media type before it is scanned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(analysis-view): charge the manifest group key before building it

Addresses the third automated review round: the composite key copies the
method, origin, template and rules, so that copy is reserved before the key
is stringified.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(analysis-view): accept a +json suffix only on application media types

Addresses the fourth automated review round: `image/example+json` or
`text/example+json` no longer counts as JSON for origin selection, exclusion
or body shapes, matching the documented `application/json` and
`application/*+json` contract.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(analysis-view): require the capture parent's exact subject for a manifest

Addresses the fifth automated review round: the manifest cites the report
path and digest, so a parent whose subject is absent or names another path or
SHA-256 is refused at admission, as the capture view already does; the
api-surface view keeps its existing admission.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(analysis-view): share truncated suffix allocation work

Track suffix progress by the actual truncated prefix and suffix width. Distinct full names can collide only after suffix truncation; sharing that progress prevents quadratic rescanning while preserving shorter free names and existing allocation order.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

* fix(analysis-view): merge only identifier-like siblings under sibling-variance

Addresses the sixth automated review finding: a bucket merged whenever one
literal carried a digit, folding plain words such as `mine` into the
parameter. The rule now partitions the bucket, merges the identifier-like
siblings and leaves the other literals as their own tools.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(analysis-view): combine qualifying sibling cohorts

Keep coded identifiers and a qualifying plural hyphenated cohort in one eligibility Set before checking the minimum merge size. Read each literal once and reuse the same membership at the consumer, preserving non-identifier routes and the three-hyphen minimum.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

* docs(analysis-view): clarify opt-in sibling folding

State the additional sibling-variance selection in the guide, acceptance specification and MCP parameter description. Preserve the existing single-rule behavior and caller-selected inference rules.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-19 07:07:37 +08:00

273 lines
8.0 KiB
TypeScript

import {
AnalysisInputError,
type AnalysisInputIssue,
} from "../analysisErrorCore.js";
import type { AnalysisError } from "../analysisErrorBase.js";
import type { BinaryLayout } from "../native/binaryLayout.js";
import { err, ok, type Result } from "../result.js";
import type {
AnalysisViewParent,
AnalysisViewRequest,
UnsignedAnalysisView,
} from "./analysisView.js";
import {
boundedViewMessages,
completeWithinViewCoverage as completeCoverage,
pageViewCoverage as pageCoverage,
} from "./analysisViewCoverage.js";
const OPERATION = "inspect_analysis_view";
type LayoutName = BinaryLayout["sections"][number]["name"];
const inputError = (
issues: readonly AnalysisInputIssue[],
): AnalysisInputError => new AnalysisInputError(OPERATION, undefined, issues);
const layoutUnknowns = (layout: BinaryLayout): readonly string[] => {
const unknowns = [
"runtime_load_base is unknown",
"entry_point.execution_status is unknown",
"relocation_inventory_completeness is unknown",
"linkage.convenience_maps_completeness is unknown",
"linkage.runtime_library_paths are unknown",
];
if (layout.mitigations.nx_indicator === null)
unknowns.push("mitigations.nx_indicator is unknown");
if (layout.mitigations.relro === null)
unknowns.push("mitigations.relro is unknown");
return unknowns;
};
const displayName = (name: LayoutName): string => name.display;
const namedMatches = <Row extends { readonly name: LayoutName }>(
rows: readonly Row[],
name: string,
): readonly { readonly index: number; readonly row: Row }[] =>
rows.flatMap((row, index) =>
row.name.unknown_reason === null && displayName(row.name) === name
? [{ index, row }]
: [],
);
const selectNamed = <Row extends { readonly name: LayoutName }>(
collection: "sections" | "symbols",
rows: readonly Row[],
name: string,
): Result<Row, AnalysisError> => {
const matches = namedMatches(rows, name);
if (matches.length === 1) {
const selected = matches[0];
if (selected !== undefined) return ok(selected.row);
}
if (matches.length === 0)
return err(
inputError([
{
path: ["view", "selector", "name"],
reason: "invalid_value",
message: `No ${collection} named ${JSON.stringify(name)}. Collection has ${String(rows.length)} entries; page it to inspect exact indexes and names.`,
},
]),
);
return err(
inputError([
{
path: ["view", "selector", "name"],
reason: "invalid_value",
message: `Multiple ${collection} named ${JSON.stringify(name)}; select one by index.`,
expected: matches.map(({ index }) => ({ index })),
},
]),
);
};
const selectIndexed = <Row>(
collection: "sections" | "symbols",
rows: readonly Row[],
index: number,
): Result<Row, AnalysisError> => {
const row = rows[index];
if (row !== undefined) return ok(row);
return err(
inputError([
{
path: ["view", "selector", "index"],
reason: "out_of_range",
message: `${collection} index ${String(index)} is outside 0..${String(Math.max(0, rows.length - 1))} (${String(rows.length)} entries).`,
minimum: 0,
maximum: Math.max(0, rows.length - 1),
},
]),
);
};
const pageRows = <Row>(
rows: readonly Row[],
offset: number,
limit: number,
): readonly Row[] => rows.slice(offset, offset + limit);
const layoutIncompatible = (detail: string): AnalysisError =>
inputError([
{
path: ["view"],
reason: "invalid_value",
message: detail,
},
]);
const parentFields = (parent: AnalysisViewParent, layout: BinaryLayout) => ({
parent_evidence_id: parent.evidenceId,
parent_operation: "inspect_binary_layout" as const,
parent_digest: parent.evidenceId.slice(3),
artifact: {
path: layout.artifact.path,
sha256: layout.artifact.sha256,
},
limitations: boundedViewMessages(
[...parent.limitations, ...layout.limitations],
parent.evidenceId,
"limitations",
),
unknowns: boundedViewMessages(
layoutUnknowns(layout),
parent.evidenceId,
"unknowns",
),
});
const layoutSummary = (layout: BinaryLayout) => ({
bytes: layout.artifact.bytes,
format: layout.format,
architecture: layout.architecture,
image_type: layout.image_type,
entry: layout.entry_point,
counts: {
sections: layout.sections.length,
segments: layout.segments.length,
symbols: layout.symbols.length,
relocations: layout.relocations.length,
},
limitation_count: layout.limitations.length,
});
/** Project one layout view without re-running the decoder. */
export const projectBinaryLayoutView = (
parent: AnalysisViewParent,
layout: BinaryLayout,
view: Exclude<
AnalysisViewRequest,
{ kind: "api-surface" | "environment-manifest" }
>,
): Result<UnsignedAnalysisView, AnalysisError> => {
const shared = parentFields(parent, layout);
if (view.kind === "native")
return err(
layoutIncompatible("Native views require analyze_function Evidence."),
);
if (view.kind === "inventory")
return err(
layoutIncompatible("Inventory views require artifact Evidence."),
);
if (view.kind === "wasm")
return err(
layoutIncompatible("WASM views require inspect_wasm_artifact Evidence."),
);
if (view.kind === "evm")
return err(
layoutIncompatible("EVM views require inspect_evm_interface Evidence."),
);
if (view.kind === "summary")
return ok({
kind: "summary",
view,
summary: layoutSummary(layout),
coverage: completeCoverage(
layout.sections.length +
layout.segments.length +
layout.symbols.length +
layout.relocations.length,
layout.sections.length +
layout.segments.length +
layout.symbols.length +
layout.relocations.length,
),
...shared,
});
if (view.kind === "facet")
return ok({
kind: "facet",
view,
facet: view.facet === "mitigations" ? layout.mitigations : layout.linkage,
coverage: completeCoverage(1, 1),
...shared,
});
if (view.kind === "item") {
if (view.collection === "modules")
return err(
layoutIncompatible(
"modules collection applies to analyze_javascript_application Evidence.",
),
);
const selected =
view.collection === "sections"
? selectLayoutItem(view.collection, layout.sections, view.selector)
: selectLayoutItem(view.collection, layout.symbols, view.selector);
if (!selected.ok) return selected;
return ok({
kind: "item",
view,
item: selected.value,
coverage: completeCoverage(
1,
view.collection === "sections"
? layout.sections.length
: layout.symbols.length,
),
...shared,
});
}
if (view.kind !== "page")
return err(
layoutIncompatible(
"Layout views require summary, facet, item or page selections.",
),
);
if (view.collection === "modules")
return err(
layoutIncompatible(
"modules collection applies to analyze_javascript_application Evidence.",
),
);
const rows =
view.collection === "sections" ? layout.sections : layout.symbols;
const items =
view.collection === "sections"
? pageRows(layout.sections, view.offset, view.limit)
: pageRows(layout.symbols, view.offset, view.limit);
return ok({
kind: "page",
view,
items: [...items],
coverage: pageCoverage(view.offset, items.length, rows.length),
...shared,
});
};
const selectLayoutItem = <Row extends { readonly name: LayoutName }>(
collection: "sections" | "symbols",
rows: readonly Row[],
selector: Extract<AnalysisViewRequest, { readonly kind: "item" }>["selector"],
): Result<Row, AnalysisError> => {
if ("index" in selector)
return selectIndexed(collection, rows, selector.index);
if ("name" in selector) return selectNamed(collection, rows, selector.name);
return err(
layoutIncompatible(
"Section and symbol items are selected by index or exact name.",
),
);
};