chore: sync current main into the CI baseline repair

Validate the formatting and reconstruction fixture repairs against the
latest merged feature and correctness changes.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
N0zoM1z0andKaoru0822 committed 2026-10-13 15:48:34 +00:00
commit 00915f484f
33 files changed
+1306 -474

No files matched your search

+5 -3
View File
@@ -271,9 +271,11 @@ Evidence remain complete inline, and both input forms pass the same semantic,
identity, authority, and provenance checks without running the producer again.
References belong to the current connection's Evidence ledger. Opening another
target preserves retained records; `close_binary` clears them, even when no
binary is active. A fresh connection has its own ledger. A missing reference
reports its exact ID and `details.reason: "missing"`; the server cannot infer
target or restarting the current target preserves retained records, including
when provider cleanup fails; records remain available for export and retry.
`close_binary` clears them, even when no binary is active. A fresh connection
has its own ledger. A missing reference reports its exact ID and
`details.reason: "missing"`; the server cannot infer
whether it was never recorded, cleared, or retained by another connection.
Supply complete inline Evidence, repeat its producer, or import an exported
Evidence bundle before referencing that imported record. Export a bundle before
+24
View File
@@ -89,6 +89,30 @@ check(
["src/domain/javascript/javascriptSemanticTraversal.ts"],
);
// A producer that pins both the soft and hard ceiling of every resource must take
// the vector from one owner. Audited 2026-10-13 against the other prlimit lanes:
// src/pcap/PcapNativeBound.ts pairs soft and hard but budgets 300 CPU seconds and
// has no resident-input ceiling; src/evm/EvmoleInterfaceProvider.ts,
// src/firmware/FirmwareCommand.ts and src/javascript/recovery/WakaruCommand.ts
// spell soft-only forms (`--core=0:`, `--core=0`). Those four stay as they are;
// a new paired producer calls prlimitArgumentVector instead of spelling options.
check(
"inline paired prlimit ceilings (use prlimitArgumentVector from src/process/LinuxResourceLimits)",
files,
/--core=0:0/,
["src/process/LinuxResourceLimits.ts", "src/pcap/PcapNativeBound.ts"],
);
// Census is spelling-shaped, like the rules above: a lane that expresses the same
// ceilings differently (`1024 ** 3`, camelCase fields) is not caught here, which is
// why the four lanes above are listed rather than left to a future reader.
check(
"inline resident-input budget (use BOUNDED_PRODUCER_LIMITS from src/process/LinuxResourceLimits)",
files,
/input_bytes:\s*32 \* 1024 \* 1024/,
["src/process/LinuxResourceLimits.ts"],
);
// Fixed-arity backtick keys audited 2026-10-10: every prefix field is NUL-free
// (hex digests, file paths, enums, numbers, or compositeKey JSON which escapes
// NUL), with free-form source strings only in final position. New backtick-\0
+10 -4
View File
@@ -8,6 +8,7 @@ import { CordisExpression, DshCordisYaml } from "./DshCordisYaml.js";
export const DSH_ENTRY_ID = "rea-mcp";
export const DSH_CLIENT_PACKAGE = "@deepseek-ai/dsh-mcp-client";
const LAUNCHER_KEYS = new Set(["serverName", "transport", "command", "args"]);
const SELECTOR_KEYS = new Set(["id", "name"]);
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" &&
value !== null &&
@@ -295,10 +296,15 @@ export class DshCordisDocument {
carried = true;
return [this.#entry(desired, invalidated)] as typeof inserted.items;
});
replacements.set(
op.source,
inserted.items.length === 0 ? undefined : copy,
);
if (inserted.items.length === 0) copy.delete("insert");
// An emptied operation keeps caller fields unless it would patch REA's row.
const kept =
inserted.items.length > 0 ||
(this.#id(op.map) !== DSH_ENTRY_ID &&
copy.items.some(
(pair) => !SELECTOR_KEYS.has(this.#yaml.value(pair.key) as string),
));
replacements.set(op.source, kept ? copy : undefined);
}
const output = this.#yaml.document.clone();
const outputRoot = output.contents;
+3 -1
View File
@@ -283,7 +283,9 @@ export class BinarySession
previous.route.identity.id,
);
if (!closed.ok) {
this.clearSessionRecords();
// A failed open retires the provider cache, not the investigation.
// Preserve Evidence and Unknown history for export and recovery.
this.clearSnapshot();
this.#clearRuntimeAvailability();
return closed;
}
@@ -199,6 +199,49 @@ const testAnalysis = (
});
describe("native UI action trace", () => {
it("retains Hopper targetless call evidence alongside fallback callee routes", async () => {
const result = await new EnhancedTools(
testAnalysis({
procedure_address: () => execution("procedure_address", "0x1000"),
procedure_references: (parameters) =>
ok(
createAnalysisExecution(
{
reference_kinds_available: false,
references: [],
unresolved_calls:
parameters.procedure === "0x1000"
? [{ address: "0x1008", reason: "No static target" }]
: [],
},
{ id: "hopper", name: "Hopper", version: "fixture" },
),
),
}),
).execute("trace_native_ui_action", { action: "0x1000" });
if (!result.ok) throw result.error;
const trace = nativeInvestigationTraceSchema.parse(result.value);
expect(trace.edges).toContainEqual(
expect.objectContaining({
relation: "provider_call",
to: "native:function:0x2000",
}),
);
expect(trace.unresolved).toContainEqual(
expect.objectContaining({
from: "native:function:0x1000",
to: null,
reason: "No static target",
evidence: [
expect.objectContaining({
description: "Hopper reported an unresolved call: No static target",
location: { address: "0x1008", file_offset: null },
artifact_sha256: target,
}),
],
}),
);
});
it("accepts an explicit native seed without an authored UI connection", async () => {
const result = await new EnhancedTools(
testAnalysis({
@@ -7,6 +7,59 @@ import { err, ok } from "../../domain/result.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
const provider = { id: "fixture", name: "Fixture", version: "1" };
describe("untyped native call route fallback", () => {
it.each([{ callees: [] }, { callees: ["0x2000"] }])(
"preserves targetless calls and their provider when fallback callees are $callees",
async ({ callees }) => {
const calls: string[] = [];
const referenceProvider = { ...provider, id: "hopper", name: "Hopper" };
const fallbackProvider = { ...provider, id: "fallback" };
const signal = new AbortController().signal;
const unknown = { address: "0x1008", reason: "No static target" };
const result = await readNativeCallRoutes(
{
execute: async (operation, parameters, options) => {
calls.push(operation);
expect(parameters.procedure).toBe("0x1000");
expect(options).toEqual({ signal, selectorOrigin: "provider" });
return ok(
createAnalysisExecution(
operation === "procedure_references"
? {
reference_kinds_available: false,
references: [],
unresolved_calls: [unknown],
}
: callees,
operation === "procedure_references"
? referenceProvider
: fallbackProvider,
),
);
},
},
"0x1000",
signal,
"provider",
);
expect(calls).toEqual(["procedure_references", "procedure_callees"]);
expect(result).toEqual({
ok: true,
value: {
provider: fallbackProvider,
routes: callees.map((address) => ({
address,
site: "0x1000",
relation: "provider_call",
resolution: "inferred",
})),
unknowns: [{ ...unknown, provider: referenceProvider }],
},
});
},
);
});
describe("typed native call routes", () => {
it("preserves computed targets, ambiguity and targetless call sites", async () => {
const reference = jsonValueSchema.parse(ghidraReferenceEdge());
+57 -36
View File
@@ -2,12 +2,12 @@ import { z } from "zod";
import type {
AnalysisOperationPort,
ExecutionOptions,
ProviderIdentity,
} from "../AnalysisProvider.js";
import {
parseRelatedAddresses,
referenceKindSchema,
} from "../../domain/hopperValues.js";
import { err, ok } from "../../domain/result.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
const referencesSchema = z.object({
@@ -40,15 +40,29 @@ export const readNativeCallRoutes = async (
{ procedure, direction: "outgoing" },
options,
);
let unknowns: Array<{
address: string;
reason: string;
provider: ProviderIdentity;
}> = [];
if (execution.ok) {
const parsed = referencesSchema.safeParse(execution.value.result);
if (!parsed.success)
return err(
new AnalysisOutputError(
return {
ok: false as const,
error: new AnalysisOutputError(
"procedure_references",
"Invalid typed call references",
),
);
unknowns,
};
// Missing detailed reference flags do not invalidate observed targetless
// calls. Keep their producing provider even if another provider supplies
// the fallback callee inventory.
unknowns = parsed.data.unresolved_calls.map((call) => ({
...call,
provider: execution.value.provider,
}));
const references = parsed.data.references;
if (
parsed.data.reference_kinds_available === true ||
@@ -64,45 +78,52 @@ export const readNativeCallRoutes = async (
targets.add(call.target_address);
countBySite.set(call.source_address, targets);
}
return ok({
provider: execution.value.provider,
routes: calls.map((call) => ({
address: call.target_procedure?.address ?? call.target_address,
site: call.source_address,
relation:
call.kind.available && (call.kind.computed || call.kind.indirect)
? ("indirect_call" as const)
: ("direct_call" as const),
resolution:
(countBySite.get(call.source_address)?.size ?? 0) > 1
? ("ambiguous" as const)
: call.kind.available &&
(call.kind.computed || call.kind.indirect)
? ("resolved" as const)
: ("observed" as const),
})),
unknowns: parsed.data.unresolved_calls,
});
return {
ok: true as const,
value: {
provider: execution.value.provider,
routes: calls.map((call) => ({
address: call.target_procedure?.address ?? call.target_address,
site: call.source_address,
relation:
call.kind.available && (call.kind.computed || call.kind.indirect)
? ("indirect_call" as const)
: ("direct_call" as const),
resolution:
(countBySite.get(call.source_address)?.size ?? 0) > 1
? ("ambiguous" as const)
: call.kind.available &&
(call.kind.computed || call.kind.indirect)
? ("resolved" as const)
: ("observed" as const),
})),
unknowns,
},
};
}
} else if (execution.error._tag !== "AnalysisCapabilityUnavailableError")
return err(execution.error);
return { ok: false as const, error: execution.error, unknowns };
const callees = await analysis.execute(
"procedure_callees",
{ procedure },
options,
);
if (!callees.ok) return callees;
if (!callees.ok)
return { ok: false as const, error: callees.error, unknowns };
const parsed = parseRelatedAddresses(callees.value.result, "callees");
return parsed.ok
? ok({
provider: callees.value.provider,
routes: parsed.value.map((address) => ({
address,
site: procedure,
relation: "provider_call" as const,
resolution: "inferred" as const,
})),
unknowns: [],
})
: parsed;
? {
ok: true as const,
value: {
provider: callees.value.provider,
routes: parsed.value.map((address) => ({
address,
site: procedure,
relation: "provider_call" as const,
resolution: "inferred" as const,
})),
unknowns,
},
}
: { ok: false as const, error: parsed.error, unknowns };
};
+38 -28
View File
@@ -414,9 +414,44 @@ const addDirectCallees = async (input: {
"provider",
);
examined += 1;
if (!execution.ok && execution.error._tag === "AnalysisCancelledError")
return { error: execution.error };
const unknowns = execution.ok
? execution.value.unknowns
: execution.unknowns;
for (const [index, unknown] of unknowns.entries()) {
if (edges.size >= input.input.max_edges) {
truncated = true;
omitted += unknowns.length - index;
break;
}
const id = `unknown-dispatch:${current.address}:${unknown.address}`;
edges.set(id, {
id,
from: current.node.id,
to: null,
relation: "indirect_call",
resolution: "unresolved",
reason: unknown.reason,
evidence: [
{
kind: "reference",
provider: unknown.provider,
description: `${unknown.provider.name} reported an unresolved call: ${unknown.reason}`,
location: { address: unknown.address, file_offset: null },
artifact_path: null,
artifact_sha256: input.targetSha256,
},
],
limitations: [],
});
}
if (!execution.ok) {
if (execution.error._tag === "AnalysisCancelledError")
return { error: execution.error };
if (edges.size >= input.input.max_edges) {
truncated = true;
omitted += 1;
continue;
}
const id = `unknown-callees:${current.address}`;
edges.set(id, {
id,
@@ -432,32 +467,6 @@ const addDirectCallees = async (input: {
});
continue;
}
for (const unknown of execution.value.unknowns) {
if (edges.size >= input.input.max_edges) {
truncated = true;
omitted++;
break;
}
const id = `unknown-dispatch:${current.address}:${unknown.address}`;
edges.set(id, {
id,
from: current.node.id,
to: null,
relation: "indirect_call",
resolution: "unresolved",
reason: unknown.reason,
evidence: [
{
kind: "reference",
description: unknown.reason,
location: { address: unknown.address, file_offset: null },
artifact_path: null,
artifact_sha256: input.targetSha256,
},
],
limitations: [],
});
}
for (const [index, route] of execution.value.routes.entries()) {
const address = route.address;
if (edges.size >= input.input.max_edges) {
@@ -469,6 +478,7 @@ const addDirectCallees = async (input: {
const evidence = [
{
kind: "reference" as const,
provider: execution.value.provider,
description: `${execution.value.provider.name} reported ${route.relation} from ${current.address} to ${address}`,
location: { address: route.site, file_offset: null },
artifact_path: null,
-25
View File
@@ -1,25 +0,0 @@
import { describe, expect, it } from "vitest";
import { orderAsarListing } from "./AsarArtifactReader.js";
describe("orderAsarListing", () => {
it("orders host-spelled listings by their portable archive path", () => {
const posix = orderAsarListing(["/a0", "/a", "/a/b"], "/");
const windows = orderAsarListing(["\\a0", "\\a", "\\a\\b"], "\\");
// "/" (0x2F) precedes "0" (0x30); "\" (0x5C) would follow it.
expect(posix.map(({ path }) => path)).toEqual(["a", "a/b", "a0"]);
expect(windows.map(({ path }) => path)).toEqual(["a", "a/b", "a0"]);
expect(windows.map(({ listed }) => listed)).toEqual([
"\\a",
"\\a\\b",
"\\a0",
]);
});
it("keeps POSIX backslashes as literal filename characters", () => {
expect(orderAsarListing(["/a\\b"], "/")).toEqual([
{ listed: "/a\\b", path: "a\\b" },
]);
});
});
+21 -53
View File
@@ -13,7 +13,7 @@ import { isAbsolute, join, relative, sep } from "node:path";
import { Readable, Transform } from "node:stream";
import { pipeline } from "node:stream/promises";
import { listPackage, statFile, uncache } from "@electron/asar";
import { getRawHeader, uncache } from "@electron/asar";
import {
ArtifactReaderFailure,
@@ -26,7 +26,11 @@ import { closeAsarHandle, readValidatedAsarEntry } from "./AsarEntryStream.js";
import { admitAsarHeader } from "./AsarHeader.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import { readFileHandleChunks } from "../filesystem/readFileHandleChunks.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
import {
readAsarListing,
type AsarListedEntry,
type AsarMetadata,
} from "./AsarListing.js";
/**
* Official Electron ASAR adapter with range-streamed member reads.
@@ -51,7 +55,7 @@ export class AsarArtifactReader implements ArtifactReader {
) {}
async *entries(signal?: AbortSignal): AsyncIterable<ArtifactEntry> {
let paths: readonly AsarListedPath[];
let paths: readonly AsarListedEntry[];
try {
await this.prepareContainer(undefined, signal);
paths = await this.#withSnapshotOperation(async () => {
@@ -62,8 +66,9 @@ export class AsarArtifactReader implements ArtifactReader {
this.#archiveSize = admitted.archiveBytes;
this.#headerSize = admitted.headerBytes;
abortIfNeeded(signal);
const ordered = orderAsarListing(
listPackage(snapshotPath, { isPack: false }),
const ordered = await readAsarListing(
getRawHeader(snapshotPath).header,
signal,
);
const archiveMetadata = await lstat(snapshotPath);
if (!archiveMetadata.isFile() || archiveMetadata.isSymbolicLink())
@@ -77,34 +82,25 @@ export class AsarArtifactReader implements ArtifactReader {
} catch (cause: unknown) {
throw asarFailure(this.path, "inventory", cause);
}
for (const { listed, path } of paths) {
for (const { metadata, path } of paths) {
abortIfNeeded(signal);
if (path.length === 0) continue;
const providerPath = listed.startsWith(sep)
? listed.slice(sep.length)
: listed;
let metadata: ReturnType<typeof statFile>;
try {
metadata = statFile(this.#snapshotPath(), providerPath, false);
} catch (cause: unknown) {
throw asarFailure(this.path, `stat ${path}`, cause);
}
const providerPath = path.split("/").join(sep);
const kind =
"files" in metadata
metadata.files !== undefined
? "directory"
: "link" in metadata
: metadata.link !== undefined
? "symlink"
: "file";
const entry: ArtifactEntry = {
path,
kind,
declaredSize: "size" in metadata ? metadata.size : null,
declaredSize: metadata.size ?? null,
compressedSize: null,
executable: "executable" in metadata && metadata.executable,
executable: metadata.executable === true,
encrypted: false,
byteOffset: null,
declaredSha256:
"integrity" in metadata &&
metadata.integrity !== undefined &&
metadata.integrity.algorithm === "SHA256" &&
/^[a-f0-9]{64}$/u.test(metadata.integrity.hash)
? metadata.integrity.hash
@@ -603,7 +599,7 @@ export class AsarArtifactReader implements ArtifactReader {
}
}
type AsarFileMetadata = Extract<ReturnType<typeof statFile>, { size: number }>;
type AsarFileMetadata = AsarMetadata & { readonly size: number };
type AsarEntryState = {
readonly metadata: AsarFileMetadata;
readonly entry: ArtifactEntry;
@@ -652,12 +648,11 @@ const asarUnpackedEntryFailure = (
};
const isAsarFileMetadata = (
metadata: ReturnType<typeof statFile>,
metadata: AsarMetadata,
): metadata is AsarFileMetadata =>
"size" in metadata &&
typeof metadata.size === "number" &&
!("files" in metadata) &&
!("link" in metadata);
metadata.files === undefined &&
metadata.link === undefined;
const parseArchiveOffset = (value: string): number | undefined => {
if (!/^(?:0|[1-9][0-9]*)$/u.test(value)) return undefined;
@@ -699,33 +694,6 @@ const waitForOperation = async (
});
};
interface AsarListedPath {
readonly listed: string;
readonly path: string;
}
/**
* Pair each host-spelled listing with its portable archive path, ordered by
* the portable path so Windows and POSIX hosts agree.
*
* @electron/asar returns paths assembled with the host path module. Keep that
* spelling for its API calls, but expose archive paths with portable
* separators. On POSIX, backslashes can be literal filename characters and
* must remain untouched.
*/
export const orderAsarListing = (
listed: readonly string[],
separator: string = sep,
): AsarListedPath[] =>
listed
.map((entry) => ({ listed: entry, path: toArtifactPath(entry, separator) }))
.sort((left, right) => compareUnicodeCodePoints(left.path, right.path));
const toArtifactPath = (listed: string, separator: string): string => {
const portable = separator === "\\" ? listed.replaceAll("\\", "/") : listed;
return portable.replace(/^\/+|\/+$/gu, "");
};
const asarFailure = (
path: string,
operation: string,
+110
View File
@@ -0,0 +1,110 @@
import { setImmediate } from "node:timers/promises";
import { z } from "zod";
import { ArtifactReaderFailure } from "./ArtifactReader.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
/** Capacity for expanded paths and listing records, independent of packed bytes. */
const MAX_LISTING_BYTES = 64 * 1024 * 1024;
const nodeSchema = z.looseObject({
files: z
.custom<Record<string, unknown>>(
(value) =>
value !== null && typeof value === "object" && !Array.isArray(value),
)
.optional(),
link: z.string().optional(),
size: z.number().nonnegative().safe().optional(),
offset: z.string().optional(),
unpacked: z.boolean().optional(),
executable: z.boolean().optional(),
integrity: z
.looseObject({ algorithm: z.string(), hash: z.string() })
.optional(),
});
export type AsarMetadata = z.infer<typeof nodeSchema>;
export interface AsarListedEntry {
readonly path: string;
readonly metadata: AsarMetadata;
}
function* ownChildren(files: Record<string, unknown>) {
for (const name in files)
if (Object.hasOwn(files, name)) yield [name, files[name]] as const;
}
/** Decode each node once; admit expanded paths before concatenation and retain no recursive lookup. */
export const readAsarListing = async (
header: unknown,
signal?: AbortSignal,
): Promise<AsarListedEntry[]> => {
const root = nodeSchema.parse(header);
if (root.files === undefined)
throw new ArtifactReaderFailure("format", "ASAR root has no files table");
const entries: AsarListedEntry[] = [];
const pending = [{ parent: "", children: ownChildren(root.files) }];
let retainedBytes = 0;
let examined = 0;
while (pending.length > 0) {
if (examined++ % 128 === 0) {
await setImmediate();
if (signal?.aborted)
throw new ArtifactReaderFailure(
"cancelled",
"ASAR operation cancelled",
);
}
const frame = pending.at(-1);
if (frame === undefined) break;
const next = frame.children.next();
if (next.done) {
pending.pop();
continue;
}
const [name, raw] = next.value;
// A header member is one component, not a host path to normalize. POSIX
// backslashes remain literal; Windows cannot represent them as components.
if (
name.length === 0 ||
name === "." ||
name === ".." ||
name.includes("/") ||
name.includes("\0") ||
(process.platform === "win32" && name.includes("\\"))
)
throw new ArtifactReaderFailure(
"path",
"ASAR header has an unsafe path component",
);
const units =
frame.parent.length + name.length + (frame.parent.length === 0 ? 0 : 1);
// UTF-16 storage, plus records and traversal bookkeeping. Charge every
// occurrence even when the engine can share a string prefix.
retainedBytes += units * 2 + 256;
if (retainedBytes > MAX_LISTING_BYTES)
throw new ArtifactReaderFailure(
"limit",
"ASAR expanded listing exceeds its 64 MiB representation budget",
);
const path = frame.parent.length === 0 ? name : `${frame.parent}/${name}`;
const metadata = nodeSchema.parse(raw);
if (
metadata.files === undefined &&
metadata.link === undefined &&
metadata.size === undefined
)
throw new ArtifactReaderFailure(
"format",
"ASAR entry has no directory, link or file metadata",
);
entries.push({ path, metadata });
if (metadata.files !== undefined)
pending.push({
parent: path,
children: ownChildren(metadata.files),
});
}
return entries.sort((left, right) =>
compareUnicodeCodePoints(left.path, right.path),
);
};
+3 -17
View File
@@ -8,6 +8,8 @@ import {
FIELD_RECORD_BUDGET,
} from "./parseFieldDiscovery.js";
import {
BOUNDED_PRODUCER_LIMITS as LIMITS,
prlimitArgumentVector,
readLinuxResourceLimits,
type LinuxResourceLimits,
} from "../process/LinuxResourceLimits.js";
@@ -60,15 +62,6 @@ export const TSHARK_PROVIDER_IDENTITY = {
name: "REA TShark capture adapter",
version: "tshark@4",
} as const;
// Bound the resident input copy and retained text before allocation/collection.
const LIMITS = {
input_bytes: 32 * 1024 * 1024,
output_bytes: 4 * 1024 * 1024,
command_timeout_ms: 30_000,
address_space_bytes: 1024 * 1024 * 1024,
file_bytes: 64 * 1024 * 1024,
cpu_seconds: 30,
} as const;
type Launcher = NonNullable<Parameters<typeof runOwnedCommand>[2]>["launcher"];
/** Inspect selected capture files only, retaining cleanup ownership across failures. */
@@ -165,14 +158,7 @@ export class TsharkCaptureProvider implements PacketCapturePort {
options?.signal,
);
effectiveLimits = await this.#resources.readLimits(limits);
const limitArguments = [
`--as=${effectiveLimits.address_space_bytes}:${effectiveLimits.address_space_bytes}`,
`--fsize=${effectiveLimits.file_bytes}:${effectiveLimits.file_bytes}`,
`--cpu=${effectiveLimits.cpu_seconds}:${effectiveLimits.cpu_seconds}`,
"--core=0:0",
"--",
command,
];
const limitArguments = prlimitArgumentVector(effectiveLimits, command);
phase = "workspace";
root = await this.createRuntime();
this.#resources.retain(root, undefined);
+1 -1
View File
@@ -225,7 +225,7 @@ const registerInspectionCommand = (
description:
"Inspect one artifact and return all available observations and next probes",
args: z.object({
path: z.string().describe("Application or package path"),
path: z.string().min(1).describe("Application or package path"),
}),
options: z.object({
integrityPolicy: z
+4 -1
View File
@@ -48,7 +48,10 @@ const registerOverviewCommands = (
cli.command(CLI_COMMANDS.analyze, {
description: "Get an overview of an app",
args: z.object({
path: z.string().describe("App, program, or analysis database path"),
path: z
.string()
.min(1)
.describe("App, program, or analysis database path"),
}),
options: overviewOptions.extend({
integrityPolicy: javascriptApplicationOptions.shape.integrityPolicy,
@@ -429,7 +429,12 @@ export const addSourceMapDirectives = (
end === null
)
continue;
const commentSource = source.slice(start, end);
// The parser owns comment boundaries. Exclude only the closing block
// delimiter; an asterisk elsewhere remains part of the declared URL.
const commentSource = source.slice(
start,
comment.type === "CommentBlock" ? end - 2 : end,
);
if (
!(commentSource.startsWith("//") || commentSource.startsWith("/*")) ||
!(commentSource[2] === "#" || commentSource[2] === "@")
@@ -449,9 +454,7 @@ export const addSourceMapDirectives = (
const urlOffset = commentSource.slice(equalsStart + 1).search(/\S/u);
if (urlOffset === -1) continue;
const declaredStart = equalsStart + 1 + urlOffset;
const terminator = commentSource
.slice(declaredStart)
.search(commentSource.startsWith("//") ? /\s/u : /[\s*]/u);
const terminator = commentSource.slice(declaredStart).search(/\s/u);
const declaredEnd =
terminator === -1 ? commentSource.length : declaredStart + terminator;
if (declaredEnd === declaredStart) continue;
@@ -72,10 +72,10 @@ describe("source map directives", () => {
it("preserves Unicode whitespace and URL terminator boundaries", () => {
const source =
"ğ/*@\u00a0sourceMappingURL\u2003=\ufeffapp.js.map* ignored */";
const declaredEnd = source.indexOf("* ignored");
const declaredEnd = source.indexOf(" ignored");
expect(analyzeJavaScriptStaticSource(source).source_map_urls).toEqual([
{
declared_url: "app.js.map",
declared_url: "app.js.map*",
location: {
start: { line: 1, column: 1 },
end: { line: 1, column: declaredEnd },
@@ -84,6 +84,29 @@ describe("source map directives", () => {
]);
});
it.each([
["/*#", "app*part.js.map", " */"],
["/*@", "*part.js.map", "*/"],
["/*#", "app.js.map*", "*/"],
["/*#", "**part.js.map", " */"],
["/*#", "app.js.map", "*/"],
["//#", "app*part.js.map", ""],
])("preserves the URL and range in %s %s%s", (marker, url, suffix) => {
const directive = marker + " sourceMappingURL=" + url;
const result = analyzeJavaScriptStaticSource(
"export const value = 1;\n" + directive + suffix,
);
expect(result.source_map_urls).toEqual([
{
declared_url: url,
location: {
start: { line: 2, column: 0 },
end: { line: 2, column: directive.length },
},
},
]);
});
it("reports nothing when no directive is present", () => {
expect(sourceMapUrls("const value = 1;")).toEqual([]);
});
+2
View File
@@ -1,4 +1,5 @@
import { z } from "zod";
import { providerSchema } from "../evidence.js";
import {
compareUnicodeCodePoints,
@@ -115,6 +116,7 @@ export const nativeMetadataEvidenceSchema = z.strictObject({
"decompiler",
]),
description: z.string().min(1),
provider: providerSchema.optional(),
location: nativeMetadataLocationSchema,
artifact_path: z.string().min(1).nullable().default(null),
artifact_sha256: z
@@ -143,3 +143,13 @@ export const completeReconstructionCoverageData = () => {
],
});
};
/** Complete workspace with its first verifier result and boundary narrowed once. */
export const completeReconstructionCoverageFixture = () => {
const workspace = completeReconstructionCoverageData();
const result = workspace.verifier_results[0];
const boundary = workspace.boundaries[0];
if (result === undefined || boundary === undefined)
throw new Error("Expected complete reconstruction coverage fixture");
return { workspace, result, boundary };
};
+28 -64
View File
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
completeReconstructionCoverageData,
completeReconstructionCoverageFixture,
RECONSTRUCTION_COVERAGE_NOW,
reconstructionCoverageEvidenceId,
} from "./reconstructionCoverage.fixture.js";
@@ -288,11 +289,8 @@ describe("reconstruction coverage verifier selection", () => {
describe("reconstruction coverage conflicting latest verifier observations", () => {
it("keeps conflicting tied assessments unknown regardless of input order", () => {
const workspace = completeReconstructionCoverageData();
const result = workspace.verifier_results[0];
const boundary = workspace.boundaries[0];
if (result === undefined || boundary === undefined)
throw new Error("Expected complete reconstruction coverage fixture");
const { workspace, result, boundary } =
completeReconstructionCoverageFixture();
const failed = {
...result,
status: "fail" as const,
@@ -338,11 +336,8 @@ describe("reconstruction coverage conflicting latest verifier observations", ()
describe("reconstruction coverage equivalent latest verifier observations", () => {
it("coalesces equivalent equal-instant observations and retains all evidence", () => {
const workspace = completeReconstructionCoverageData();
const result = workspace.verifier_results[0];
const boundary = workspace.boundaries[0];
if (result === undefined || boundary === undefined)
throw new Error("Expected complete reconstruction coverage fixture");
const { workspace, result, boundary } =
completeReconstructionCoverageFixture();
const first = {
...result,
observed_at: "2026-07-16T11:00:00.123456789012345678900Z",
@@ -388,11 +383,8 @@ describe("reconstruction coverage equivalent latest verifier observations", () =
describe("reconstruction coverage verifier timestamp selection", () => {
it("lets a later unambiguous observation supersede an older tie", () => {
const workspace = completeReconstructionCoverageData();
const result = workspace.verifier_results[0];
const boundary = workspace.boundaries[0];
if (result === undefined || boundary === undefined)
throw new Error("Expected complete reconstruction coverage fixture");
const { workspace, result, boundary } =
completeReconstructionCoverageFixture();
const failed = { ...result, status: "fail" as const };
const latestPass = {
...result,
@@ -412,50 +404,14 @@ describe("reconstruction coverage verifier timestamp selection", () => {
).toMatchObject({ status: "ready", reasons: [] });
});
it("orders offset-bearing verifier timestamps chronologically", () => {
const workspace = completeReconstructionCoverageData();
const result = workspace.verifier_results[0];
if (result === undefined)
throw new Error("Expected fixture verifier result");
const semantic = workspace;
const changed = createReconstructionCoverageData({
...semantic,
verifier_results: [
{ ...result, observed_at: "2026-07-16T08:00:00.000Z" },
{
...result,
observed_at: "2026-07-16T04:00:00.000-05:00",
status: "fail",
},
],
});
expect(
evaluateReconstructionClosure(
changed,
"replacement.cli",
RECONSTRUCTION_COVERAGE_NOW,
),
).toMatchObject({
status: "failed",
reasons: [expect.objectContaining({ code: "verifier-failed" })],
});
});
it("selects the later result when timestamps differ below millisecond precision", () => {
const workspace = completeReconstructionCoverageData();
const result = workspace.verifier_results[0];
if (result === undefined)
throw new Error("Expected fixture verifier result");
/** Evaluate a workspace whose newest verifier observation fails the closure. */
const expectNewestObservationFails = (passedAt: string, failedAt: string) => {
const { workspace, result } = completeReconstructionCoverageFixture();
const changed = createReconstructionCoverageData({
...workspace,
verifier_results: [
{ ...result, observed_at: "2026-07-16T11:00:00.1231Z" },
{
...result,
observed_at: "2026-07-16T11:00:00.1239Z",
status: "fail",
},
{ ...result, observed_at: passedAt },
{ ...result, observed_at: failedAt, status: "fail" },
],
});
@@ -469,15 +425,26 @@ describe("reconstruction coverage verifier timestamp selection", () => {
status: "failed",
reasons: [expect.objectContaining({ code: "verifier-failed" })],
});
};
it("orders offset-bearing verifier timestamps chronologically", () => {
expectNewestObservationFails(
"2026-07-16T08:00:00.000Z",
"2026-07-16T04:00:00.000-05:00",
);
});
it("selects the later result when timestamps differ below millisecond precision", () => {
expectNewestObservationFails(
"2026-07-16T11:00:00.1231Z",
"2026-07-16T11:00:00.1239Z",
);
});
});
describe("reconstruction coverage verifier future timestamps", () => {
it("treats an observation beyond millisecond-precision now as future", () => {
const workspace = completeReconstructionCoverageData();
const result = workspace.verifier_results[0];
if (result === undefined)
throw new Error("Expected fixture verifier result");
const { workspace, result } = completeReconstructionCoverageFixture();
const changed = createReconstructionCoverageData({
...workspace,
verifier_results: [
@@ -505,10 +472,7 @@ describe("reconstruction coverage verifier future timestamps", () => {
});
it("rejects verifier observations from the future", () => {
const workspace = completeReconstructionCoverageData();
const result = workspace.verifier_results[0];
if (result === undefined)
throw new Error("Expected fixture verifier result");
const { workspace, result } = completeReconstructionCoverageFixture();
const semantic = workspace;
const changed = createReconstructionCoverageData({
...semantic,
+31 -95
View File
@@ -35,6 +35,28 @@ const validPng = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jv4sAAAAASUVORK5CYII=",
"base64",
);
/** Screenshot payload as the native helper reports one PNG buffer. */
const screenshotOf = (bytes: Buffer) => ({
mime_type: "image/png",
base64: bytes.toString("base64"),
sha256: createHash("sha256").update(bytes).digest("hex"),
width: bytes.readUInt32BE(16),
height: bytes.readUInt32BE(20),
});
/** Observe the fixture snapshot carrying one screenshot payload. */
const observeWithScreenshot = (screenshot: Record<string, unknown>) =>
observeNativeUi(
target,
"observe_native_ui",
{ ...scope, screenshot: true },
{
environment: {},
invoke: async () => ({ ok: true, result: { ...snapshot, screenshot } }),
},
);
it.each([
{ offset: 8, value: 12 },
{ offset: 16, value: 0x8000_0000 },
@@ -44,49 +66,16 @@ it.each([
async ({ offset, value }) => {
const bytes = Buffer.from(validPng);
bytes.writeUInt32BE(value, offset);
const screenshot = {
mime_type: "image/png",
base64: bytes.toString("base64"),
sha256: createHash("sha256").update(bytes).digest("hex"),
width: bytes.readUInt32BE(16),
height: bytes.readUInt32BE(20),
};
const result = await observeNativeUi(
target,
"observe_native_ui",
{ ...scope, screenshot: true },
{
environment: {},
invoke: async () => ({
ok: true,
result: { ...snapshot, screenshot },
}),
},
);
const result = await observeWithScreenshot(screenshotOf(bytes));
expect(result.ok).toBe(false);
},
);
describe("native UI screenshot validation", () => {
it("rejects malformed or mismatched screenshot bytes from the helper", async () => {
const screenshot = {
mime_type: "image/png",
base64: validPng.toString("base64"),
const result = await observeWithScreenshot({
...screenshotOf(validPng),
sha256: "0".repeat(64),
width: 1,
height: 1,
};
const result = await observeNativeUi(
target,
"observe_native_ui",
{ ...scope, screenshot: true },
{
environment: {},
invoke: async () => ({
ok: true,
result: { ...snapshot, screenshot },
}),
},
);
});
expect(result.ok).toBe(false);
});
@@ -97,26 +86,10 @@ describe("native UI screenshot validation", () => {
])(
"rejects invalid screenshot field variants from the helper",
async (change) => {
const screenshot = {
mime_type: "image/png",
base64: validPng.toString("base64"),
sha256: createHash("sha256").update(validPng).digest("hex"),
width: 1,
height: 1,
const result = await observeWithScreenshot({
...screenshotOf(validPng),
...change,
};
const result = await observeNativeUi(
target,
"observe_native_ui",
{ ...scope, screenshot: true },
{
environment: {},
invoke: async () => ({
ok: true,
result: { ...snapshot, screenshot },
}),
},
);
});
expect(result.ok).toBe(false);
},
);
@@ -126,49 +99,12 @@ describe("native UI screenshot validation", () => {
const type = bytes.subarray(12, 16);
type[0] = (type[0] ?? 0) | 0x80;
expect(type.toString("ascii")).toBe("IHDR");
const screenshot = {
mime_type: "image/png",
base64: bytes.toString("base64"),
sha256: createHash("sha256").update(bytes).digest("hex"),
width: 1,
height: 1,
};
const result = await observeNativeUi(
target,
"observe_native_ui",
{ ...scope, screenshot: true },
{
environment: {},
invoke: async () => ({
ok: true,
result: { ...snapshot, screenshot },
}),
},
);
const result = await observeWithScreenshot(screenshotOf(bytes));
expect(result.ok).toBe(false);
});
it("accepts a self-consistent PNG screenshot from the helper", async () => {
const bytes = validPng;
const screenshot = {
mime_type: "image/png",
base64: bytes.toString("base64"),
sha256: createHash("sha256").update(bytes).digest("hex"),
width: 1,
height: 1,
};
const result = await observeNativeUi(
target,
"observe_native_ui",
{ ...scope, screenshot: true },
{
environment: {},
invoke: async () => ({
ok: true,
result: { ...snapshot, screenshot },
}),
},
);
const result = await observeWithScreenshot(screenshotOf(validPng));
expect(result.ok).toBe(true);
});
+40 -1
View File
@@ -1,5 +1,9 @@
import { expect, it } from "vitest";
import { clampLinuxResourceLimits } from "./LinuxResourceLimits.js";
import {
BOUNDED_PRODUCER_LIMITS,
clampLinuxResourceLimits,
prlimitArgumentVector,
} from "./LinuxResourceLimits.js";
const configured = {
address_space_bytes: 1073741824,
file_bytes: 67108864,
@@ -41,3 +45,38 @@ it("refuses missing, ambiguous or malformed kernel limits before launch", () =>
])
expect(() => clampLinuxResourceLimits(configured, value)).toThrow();
});
it("pins the shared producer budget in one place", () => {
expect(BOUNDED_PRODUCER_LIMITS).toEqual({
input_bytes: 33554432,
output_bytes: 4194304,
command_timeout_ms: 30000,
address_space_bytes: 1073741824,
file_bytes: 67108864,
cpu_seconds: 30,
});
});
it("pairs each effective ceiling with itself before the producer argv", () => {
expect(
prlimitArgumentVector(BOUNDED_PRODUCER_LIMITS, "/bin/producer"),
).toEqual([
"--as=1073741824:1073741824",
"--fsize=67108864:67108864",
"--cpu=30:30",
"--core=0:0",
"--",
"/bin/producer",
]);
expect(
prlimitArgumentVector(
clampLinuxResourceLimits(BOUNDED_PRODUCER_LIMITS, report("10 20")),
"/bin/producer",
),
).toEqual([
"--as=10:10",
"--fsize=67108864:67108864",
"--cpu=30:30",
"--core=0:0",
"--",
"/bin/producer",
]);
});
+23
View File
@@ -15,6 +15,29 @@ const rows = {
cpu_seconds: "Max cpu time",
} as const;
/** Bound the resident input copy and retained text before allocation/collection. */
export const BOUNDED_PRODUCER_LIMITS = {
input_bytes: 32 * 1024 * 1024,
output_bytes: 4 * 1024 * 1024,
command_timeout_ms: 30_000,
address_space_bytes: 1024 * 1024 * 1024,
file_bytes: 64 * 1024 * 1024,
cpu_seconds: 30,
} as const;
/** Pin soft and hard ceilings together, then hand the producer argv to the limiter. */
export const prlimitArgumentVector = (
limits: LinuxResourceLimits,
command: string,
): string[] => [
`--as=${limits.address_space_bytes}:${limits.address_space_bytes}`,
`--fsize=${limits.file_bytes}:${limits.file_bytes}`,
`--cpu=${limits.cpu_seconds}:${limits.cpu_seconds}`,
"--core=0:0",
"--",
command,
];
/** Never raise either inherited soft policy or hard ceiling. */
export const clampLinuxResourceLimits = (
configured: LinuxResourceLimits,
+3 -17
View File
@@ -7,6 +7,8 @@ import {
parseDecoderDiscovery,
} from "./parseDecoderDiscovery.js";
import {
BOUNDED_PRODUCER_LIMITS as LIMITS,
prlimitArgumentVector,
readLinuxResourceLimits,
type LinuxResourceLimits,
} from "../process/LinuxResourceLimits.js";
@@ -59,15 +61,6 @@ export const SIGROK_PROVIDER_IDENTITY = {
name: "REA sigrok recording adapter",
version: "sigrok-cli@0.7.2",
} as const;
// Bound the resident input copy and retained text before allocation/collection.
const LIMITS = {
input_bytes: 32 * 1024 * 1024,
output_bytes: 4 * 1024 * 1024,
command_timeout_ms: 30_000,
address_space_bytes: 1024 * 1024 * 1024,
file_bytes: 64 * 1024 * 1024,
cpu_seconds: 30,
} as const;
type Launcher = NonNullable<Parameters<typeof runOwnedCommand>[2]>["launcher"];
/** Decode selected recordings only, retaining cleanup ownership across failures. */
@@ -163,14 +156,7 @@ export class SigrokRecordingProvider implements SignalRecordingPort {
options?.signal,
);
effectiveLimits = await this.#resources.readLimits(LIMITS);
const limitArguments = [
`--as=${effectiveLimits.address_space_bytes}:${effectiveLimits.address_space_bytes}`,
`--fsize=${effectiveLimits.file_bytes}:${effectiveLimits.file_bytes}`,
`--cpu=${effectiveLimits.cpu_seconds}:${effectiveLimits.cpu_seconds}`,
"--core=0:0",
"--",
command,
];
const limitArguments = prlimitArgumentVector(effectiveLimits, command);
phase = "workspace";
root = await this.createRuntime();
this.#resources.retain(root, undefined);
@@ -0,0 +1,99 @@
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join, sep } from "node:path";
import { createPackageWithOptions, listPackage } from "@electron/asar";
import { expect } from "vitest";
import { z } from "zod";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest.for(["CLI", "stdio MCP"] as const)(
"links a block source-map URL containing an asterisk through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-block-map-");
const directory = join(root, "input");
await mkdir(directory);
const url = "app*part.js.map";
const directive = "/*# sourceMappingURL=" + url;
await writeFile(join(directory, "app.js"), directive + "*/");
await writeFile(
join(directory, "app_part.js.map"),
JSON.stringify({ version: 3, sources: [], names: [], mappings: "" }),
);
const archive = join(root, "app.asar");
await createPackageWithOptions(directory, archive, {});
// ASAR logical names can contain '*' even on Windows. Rename this header
// key without changing entry bytes, sizes or payload offsets.
const bytes = await readFile(archive);
const key = Buffer.from('"app_part.js.map"');
const offset = bytes.indexOf(key);
expect(offset).toBeGreaterThan(0);
expect(bytes.indexOf(key, offset + key.length)).toBe(-1);
Buffer.from('"' + url + '"').copy(bytes, offset);
await writeFile(archive, bytes);
expect(listPackage(archive, { isPack: false })).toContain(sep + url);
let document: unknown;
if (surface === "CLI") {
const result = await cli.run({
arguments: ["analyze-javascript-application", archive, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
document = result.json;
} else {
const { call } = await connectLocalToolsMcp();
const result = await call("analyze_javascript_application", {
input_path: archive,
format: "asar",
});
expect(result.isError).not.toBe(true);
document = result.structuredContent;
}
const {
normalized_result: { graph },
} = z
.object({
normalized_result: javascriptApplicationAnalysisResultSchema,
})
.parse(document);
const digest = createHash("sha256").update(url).digest("hex");
const map = graph.nodes.find(({ observations }) =>
observations.some(({ properties }) => properties.declared_url === url),
);
expect(
map,
"the complete declared URL must identify the map",
).toBeDefined();
expect(map?.observations).toContainEqual(
expect.objectContaining({
properties: {
declared_url: url,
declared_url_sha256: digest,
resolved_path: url,
available: true,
},
evidence: expect.objectContaining({
location: {
available: true,
value: {
kind: "source-range",
source: "app.js",
start: { line: 1, column: 0 },
end: { line: 1, column: directive.length },
},
},
}),
}),
);
expect(graph.edges).toContainEqual(
expect.objectContaining({
relation: "maps_to",
target_node_id: map?.node_id,
properties: { declared_url_sha256: digest, resolved_path: url },
}),
);
},
);
@@ -0,0 +1,35 @@
import { expect } from "vitest";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest } from "../support/cli/cliFixture.js";
cliTest.for(["analyze", "inspect-artifact"])(
"%s rejects an explicitly empty target before resolving it against cwd",
async (command, { cli }) => {
const cwd = await createTestTempDirectory("rea-empty-target-");
const result = await cli.run({
arguments: [command, "", "--json"],
cwd,
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({ code: "VALIDATION_ERROR" });
expect(result.stdout).toContain("path");
expect(result.stdout).not.toContain("directory_requires_file");
},
);
cliTest(
"inspect-artifact still admits an explicitly selected current directory",
async ({ cli }) => {
const cwd = await createTestTempDirectory("rea-explicit-target-");
const result = await cli.run({
arguments: ["inspect-artifact", ".", "--json"],
cwd,
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "target_unavailable",
details: { path: cwd, constraint: "directory_requires_file" },
});
},
);
+36 -57
View File
@@ -188,8 +188,12 @@ it("closes the service and retains an unexpected producer rejection", async () =
});
});
it("rejects an oversized selector budget before the CLI invokes its service", async () => {
const cli = Cli.create("capture-selector-test", { sync: false });
/** Serve one inspection request whose service must never be reached. */
const rejectBeforeService = async (
name: string,
args: string[],
): Promise<string> => {
const cli = Cli.create(name, { sync: false });
let called = false;
registerCaptureCommands(
cli,
@@ -207,66 +211,41 @@ it("rejects an oversized selector budget before the CLI invokes its service", as
},
);
let stdout = "";
await cli.serve(
[
"inspect-packet-capture",
"recording.pcap",
"--fields",
"x".repeat(8192),
"--display_filter",
"界".repeat(8192),
"--format",
"json",
],
{
stdout: (text) => {
stdout += text;
},
exit: () => {},
await cli.serve(args, {
stdout: (text) => {
stdout += text;
},
);
exit: () => {},
});
expect(called).toBe(false);
return stdout;
};
it("rejects an oversized selector budget before the CLI invokes its service", async () => {
const stdout = await rejectBeforeService("capture-selector-test", [
"inspect-packet-capture",
"recording.pcap",
"--fields",
"x".repeat(8192),
"--display_filter",
"界".repeat(8192),
"--format",
"json",
]);
expect(stdout).toContain("32 KiB");
});
it("rejects conflicting output options before the CLI invokes its service", async () => {
const cli = Cli.create("capture-window-test", { sync: false });
let called = false;
registerCaptureCommands(
cli,
silentLogger,
{},
{
inspectFields: async () => {
throw new Error("Unexpected field discovery call");
},
inspect: async () => {
called = true;
throw new Error("Must not inspect");
},
close: async () => {},
},
);
let stdout = "";
await cli.serve(
[
"inspect-packet-capture",
"recording.pcap",
"--fields",
"ip.addr",
"--all_matches",
"--match_limit",
"20",
"--format",
"json",
],
{
stdout: (text) => {
stdout += text;
},
exit: () => {},
},
);
expect(called).toBe(false);
const stdout = await rejectBeforeService("capture-window-test", [
"inspect-packet-capture",
"recording.pcap",
"--fields",
"ip.addr",
"--all_matches",
"--match_limit",
"20",
"--format",
"json",
]);
expect(stdout).toContain("mutually exclusive");
});
@@ -0,0 +1,130 @@
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { buffer } from "node:stream/consumers";
import { setImmediate } from "node:timers/promises";
import { expect, it } from "vitest";
import { AsarArtifactReader } from "../../../src/artifacts/AsarArtifactReader.js";
import { readAsarListing } from "../../../src/artifacts/AsarListing.js";
import {
asarHeaderBytes,
nestedAsarHeader,
} from "../../fixtures/asarHeader.js";
import { inventoryArtifact } from "../../fixtures/artifactInventory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const archiveWith = async (bytes: Buffer) => {
const root = await createTestTempDirectory("rea-asar-listing-");
const path = join(root, "app.asar");
await writeFile(path, bytes);
return path;
};
it("interrupts expanded listing admission between traversal batches", async () => {
const controller = new AbortController();
const header = {
files: Object.fromEntries(
Array.from({ length: 4000 }, (_, index) => [
`f${index}`,
{ size: 0, offset: "0" },
]),
),
};
const listing = readAsarListing(header, controller.signal);
await setImmediate();
controller.abort();
await expect(listing).rejects.toMatchObject({ reason: "cancelled" });
});
it("rejects path expansion before publishing any inventory, preserving the input", async () => {
const bytes = nestedAsarHeader(64, "d".repeat(256), 4000);
expect(bytes.length).toBeLessThan(150_000);
const path = await archiveWith(bytes);
await expect(inventoryArtifact(path)).rejects.toMatchObject({
reason: "limit",
message: "ASAR expanded listing exceeds its 64 MiB representation budget",
});
expect(await readFile(path)).toEqual(bytes);
});
it("reads deeply nested packed metadata without recursive listing or member lookup", async () => {
const path = await archiveWith(nestedAsarHeader(3500, "d", 1));
const reader = new AsarArtifactReader(path);
try {
let count = 0;
for await (const entry of reader.entries()) {
count++;
if (entry.kind === "file") {
expect(entry.path).toBe(`${"d/".repeat(3500)}f0`);
expect(await buffer(await reader.open(entry))).toEqual(Buffer.alloc(0));
}
}
expect(count).toBe(3501);
} finally {
await reader.close();
}
});
it.each(["..", ".", "", "a/../b", "a/b", "a\0b"])(
"rejects raw ASAR component %j before host normalization can alias another entry",
async (component) => {
const json = JSON.stringify({
files: { [component]: { size: 0, offset: "0" } },
});
await expect(
inventoryArtifact(await archiveWith(asarHeaderBytes(json))),
).rejects.toMatchObject({ reason: "path" });
},
);
it("orders raw names portably and retains prototype-named entries and links", async () => {
const path = await archiveWith(
asarHeaderBytes(
'{"files":{"a0":{"size":0,"offset":"0"},"__proto__":{"size":0,"offset":"0"},"a":{"files":{"b":{"link":"a0"}}},"😀":{"size":0,"offset":"0"},"\uE000":{"size":0,"offset":"0"}}}',
),
);
const reader = new AsarArtifactReader(path);
try {
const entries = [];
for await (const entry of reader.entries())
entries.push([entry.path, entry.kind]);
expect(entries).toEqual([
["__proto__", "file"],
["a", "directory"],
["a/b", "symlink"],
["a0", "file"],
["\uE000", "file"],
["😀", "file"],
]);
} finally {
await reader.close();
}
});
it("preserves POSIX literal backslashes and rejects Windows component separators", async () => {
const path = await archiveWith(
asarHeaderBytes(
JSON.stringify({ files: { "a\\b": { size: 0, offset: "0" } } }),
),
);
const reader = new AsarArtifactReader(path);
try {
const list = async () => {
const entries = [];
for await (const entry of reader.entries()) entries.push(entry);
return entries;
};
if (process.platform === "win32") {
await expect(list()).rejects.toMatchObject({ reason: "path" });
} else {
const entries = await list();
expect(entries).toHaveLength(1);
expect(entries[0]?.path).toBe("a\\b");
expect(await buffer(await reader.open(entries[0]!))).toEqual(
Buffer.alloc(0),
);
}
} finally {
await reader.close();
}
});
@@ -2195,3 +2195,135 @@ describe("DeepSeek Harness caller node metadata", () => {
},
);
});
describe("DeepSeek Harness uninstall of a shared operation", () => {
it("keeps a group patch's own fields and comment when its only row was REA's", async () => {
const { home, client } = await harnessClient();
const entry = canonicalEntry();
await writeFile(
client.configPath,
`- insert:
- id: user-a
name: plugin-a
# caller group patch
- id: tools
config:
sandbox: strict
insert:
- id: rea-mcp
name: "${entry.name}"
config:
serverName: rea
transport: stdio
command: ${entry.config.command}
args: ${JSON.stringify(entry.config.args)}
`,
);
expect((await systemUninstallHost(home).removeClient(client)).status).toBe(
"removed",
);
const text = await readFile(client.configPath, "utf8");
expect(parseYaml(text)).toEqual([
{ insert: [{ id: "user-a", name: "plugin-a" }] },
{ id: "tools", config: { sandbox: "strict" } },
]);
expect(text).toContain("# caller group patch");
});
it("keeps an operation's own fields when setup removes a second REA row from it", async () => {
// Setup refuses a REA row in a group patch, so the setup path meets the
// keep rule only on an operation without a selector.
const { home, client } = await harnessClient();
const entry = canonicalEntry();
const row = (launcher: string) => ` - id: rea-mcp
name: "${entry.name}"
config:
serverName: rea
transport: stdio
command: ${launcher}
args: [mcp]
`;
await writeFile(
client.configPath,
`- insert:
${row("first-launcher")}# caller operation
- disabled: false
insert:
${row("second-launcher")}`,
);
expect(
(await configureClientConfiguration(client, {}, command)).status,
).toBe("configured");
const text = await readFile(client.configPath, "utf8");
expect(text).toContain("# caller operation");
expect(loadedInserts(text)).toHaveLength(1);
expect((parseYaml(text) as unknown[]).at(-1)).toEqual({ disabled: false });
expect(await dshStatus(home)).toMatchObject({ state: "aligned" });
});
it("drops an emptied operation that targets REA's own row", async () => {
const { home, client } = await harnessClient();
await writeFile(
client.configPath,
stringifyYaml([
{ insert: [{ id: "user-a", name: "plugin-a" }] },
{
id: "rea-mcp",
config: { serverName: "rea" },
insert: [canonicalEntry()],
},
]),
);
const host = systemUninstallHost(home);
expect((await host.removeClient(client)).status).toBe("removed");
expect(parseYaml(await readFile(client.configPath, "utf8"))).toEqual([
{ insert: [{ id: "user-a", name: "plugin-a" }] },
]);
});
it.each([
{ selector: { id: "tools" } },
{ selector: { name: "tools" } },
{ selector: { id: "tools", name: "tools" } },
])(
"drops an emptied operation left with only its target selector $selector",
async ({ selector }) => {
const { home, client } = await harnessClient();
await writeFile(
client.configPath,
stringifyYaml([
{ insert: [{ id: "user-a", name: "plugin-a" }] },
{ ...selector, insert: [canonicalEntry()] },
]),
);
expect(
(await systemUninstallHost(home).removeClient(client)).status,
).toBe("removed");
expect(parseYaml(await readFile(client.configPath, "utf8"))).toEqual([
{ insert: [{ id: "user-a", name: "plugin-a" }] },
]);
},
);
it("still drops an insert-only operation that REA's row emptied", async () => {
const { home, client } = await harnessClient();
await writeFile(
client.configPath,
stringifyYaml([
{ insert: [{ id: "user-a", name: "plugin-a" }] },
{ insert: [canonicalEntry()] },
]),
);
expect((await systemUninstallHost(home).removeClient(client)).status).toBe(
"removed",
);
expect(parseYaml(await readFile(client.configPath, "utf8"))).toEqual([
{ insert: [{ id: "user-a", name: "plugin-a" }] },
]);
});
});
+16 -21
View File
@@ -209,13 +209,7 @@ describe("installer TTY fallback", { timeout: 20_000 }, () => {
cwd: process.cwd(),
env: {
...process.env,
HOME: fixture.home,
TMPDIR: fixture.temporary,
PATH: `${fixture.bin}:/usr/bin:/bin`,
FAKE_NPM_LOG: fixture.npmLog,
FAKE_REA_LOG: fixture.reaLog,
FAKE_REA_SOURCE: join(fixture.bin, "rea-source"),
FAKE_NPM_PREFIX: join(fixture.home, ".npm-global"),
...installerEnvironment(fixture),
TERM: "xterm-256color",
},
name: "xterm-256color",
@@ -581,6 +575,19 @@ const executable = async (path: string, source: string): Promise<void> => {
await chmod(path, 0o755);
};
/** Fixture-owned installer environment shared by the exec, pipe and PTY runs. */
const installerEnvironment = (
fixture: InstallerFixture,
): Record<string, string> => ({
HOME: fixture.home,
TMPDIR: fixture.temporary,
PATH: `${fixture.bin}:/usr/bin:/bin`,
FAKE_NPM_LOG: fixture.npmLog,
FAKE_REA_LOG: fixture.reaLog,
FAKE_REA_SOURCE: join(fixture.bin, "rea-source"),
FAKE_NPM_PREFIX: join(fixture.home, ".npm-global"),
});
const runInstaller = async (
fixture: InstallerFixture,
args: readonly string[] = [],
@@ -590,13 +597,7 @@ const runInstaller = async (
execFileAsync("/bin/bash", [script, ...args], {
env: {
...process.env,
HOME: fixture.home,
TMPDIR: fixture.temporary,
PATH: `${fixture.bin}:/usr/bin:/bin`,
FAKE_NPM_LOG: fixture.npmLog,
FAKE_REA_LOG: fixture.reaLog,
FAKE_REA_SOURCE: join(fixture.bin, "rea-source"),
FAKE_NPM_PREFIX: join(fixture.home, ".npm-global"),
...installerEnvironment(fixture),
...overrides,
},
});
@@ -618,13 +619,7 @@ const runPipedInstaller = async (
new Promise((resolve, reject) => {
const child = spawn("/bin/bash", ["-s", "--", ...args], {
env: {
HOME: fixture.home,
TMPDIR: fixture.temporary,
PATH: `${fixture.bin}:/usr/bin:/bin`,
FAKE_NPM_LOG: fixture.npmLog,
FAKE_REA_LOG: fixture.reaLog,
FAKE_REA_SOURCE: join(fixture.bin, "rea-source"),
FAKE_NPM_PREFIX: join(fixture.home, ".npm-global"),
...installerEnvironment(fixture),
...overrides,
},
stdio: ["pipe", "pipe", "pipe"],
@@ -54,6 +54,42 @@ const holdReadHandle = (path: string, reason: string) => {
};
};
/** Linux setup host with its VS Code client configuration directory prepared. */
const preparedVsCodeSetupHost = async (prefix: string) => {
const home = await createTestTempDirectory(prefix);
const host = systemSetupHost(
{ platform: "linux", homeDirectory: home } as DoctorHost,
{ HOME: home, XDG_CONFIG_HOME: join(home, "xdg") },
);
const client = (await host.supportedClients()).find(
({ name }) => name === "vscode",
);
if (client === undefined) throw new Error("VS Code setup client missing");
await mkdir(dirname(client.configPath), { recursive: true });
return { home, host, client };
};
/** Release the held read handle and show host cleanup reuses it as a third attempt. */
const expectCloseReclaimsHandle = async (
host: { close?: () => Promise<string | undefined> },
closeAttempts: readonly object[],
release: () => void,
) => {
release();
expect(await host.close?.()).toBeUndefined();
expect(closeAttempts).toHaveLength(3);
expect(closeAttempts[2]).toBe(closeAttempts[0]);
};
/** Give up on the assertion path: release the handle and let cleanup close it. */
const cleanupHeldHandle = async (
host: { close?: () => Promise<string | undefined> },
release: () => void,
) => {
release();
await host.close?.();
};
it("retains invalid UTF-8 diagnostics and the uninstall read owner when close fails", async () => {
const home = await createTestTempDirectory("rea-uninstall-utf8-close-");
const path = join(home, "client.json");
@@ -77,13 +113,9 @@ it("retains invalid UTF-8 diagnostics and the uninstall read owner when close fa
});
expect(await readFile(path)).toEqual(original);
expect(closeAttempts).toHaveLength(2);
release();
expect(await host.close?.()).toBeUndefined();
expect(closeAttempts).toHaveLength(3);
expect(closeAttempts[2]).toBe(closeAttempts[0]);
await expectCloseReclaimsHandle(host, closeAttempts, release);
} finally {
release();
await host.close?.();
await cleanupHeldHandle(host, release);
}
});
@@ -126,13 +158,9 @@ describe("ZCode fallback cleanup ownership", () => {
await expect(readFile(client.configPath)).rejects.toMatchObject({
code: "ENOENT",
});
release();
expect(await host.close?.()).toBeUndefined();
expect(closeAttempts).toHaveLength(3);
expect(closeAttempts[2]).toBe(closeAttempts[0]);
await expectCloseReclaimsHandle(host, closeAttempts, release);
} finally {
release();
await host.close?.();
await cleanupHeldHandle(host, release);
}
},
);
@@ -145,20 +173,9 @@ describe("setup client configuration cleanup ownership", () => {
] as const)(
"stops configuration and retains the %s read owner until host close",
async (encoding, original) => {
const home = await createTestTempDirectory("rea-setup-config-close-");
const environment = {
HOME: home,
XDG_CONFIG_HOME: join(home, "xdg"),
};
const host = systemSetupHost(
{ platform: "linux", homeDirectory: home } as DoctorHost,
environment,
const { host, client } = await preparedVsCodeSetupHost(
"rea-setup-config-close-",
);
const client = (await host.supportedClients()).find(
({ name }) => name === "vscode",
);
if (client === undefined) throw new Error("VS Code setup client missing");
await mkdir(dirname(client.configPath), { recursive: true });
await writeFile(client.configPath, original);
const { closeAttempts, release } = holdReadHandle(
@@ -210,32 +227,17 @@ describe("setup client configuration cleanup ownership", () => {
expect(closeAttempts[0]).toBe(closeAttempts[1]);
expect(await readFile(client.configPath)).toEqual(original);
release();
expect(await host.close?.()).toBeUndefined();
expect(closeAttempts).toHaveLength(3);
expect(closeAttempts[2]).toBe(closeAttempts[0]);
await expectCloseReclaimsHandle(host, closeAttempts, release);
} finally {
release();
await host.close?.();
await cleanupHeldHandle(host, release);
}
},
);
it("waits for an admitted configuration read and rejects later admissions", async () => {
const home = await createTestTempDirectory("rea-setup-config-inflight-");
const environment = {
HOME: home,
XDG_CONFIG_HOME: join(home, "xdg"),
};
const host = systemSetupHost(
{ platform: "linux", homeDirectory: home } as DoctorHost,
environment,
const { host, client } = await preparedVsCodeSetupHost(
"rea-setup-config-inflight-",
);
const client = (await host.supportedClients()).find(
({ name }) => name === "vscode",
);
if (client === undefined) throw new Error("VS Code setup client missing");
await mkdir(dirname(client.configPath), { recursive: true });
await writeFile(client.configPath, '{"servers": {}}\n');
let beginRead!: () => void;
@@ -0,0 +1,60 @@
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { expect, it } from "vitest";
import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js";
import { createServer } from "../../../src/server/createServer.js";
import { nestedAsarHeader } from "../../fixtures/asarHeader.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
it("reports expanded ASAR admission failure through MCP and permits a valid retry", async () => {
const root = await createTestTempDirectory("rea-asar-listing-mcp-");
const path = join(root, "app.asar");
const bytes = nestedAsarHeader(64, "d".repeat(256), 4000);
await writeFile(path, bytes);
const session = createTestBinarySession(new ArtifactProvider(process.env));
const server = createServer({ kind: "session", session });
const client = new Client({ name: "asar-listing-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
expect(
(await client.callTool({ name: "open_binary", arguments: { path } }))
.isError,
).not.toBe(true);
const result = await client.callTool({
name: "inspect_artifact",
arguments: {},
});
expect(result.isError).toBe(true);
expect(parseMcpToolError(result)).toMatchObject({
error: {
code: "truncated",
category: "truncated",
},
});
expect(result.structuredContent).toBeUndefined();
expect(await readFile(path)).toEqual(bytes);
const valid = join(root, "valid.asar");
await writeFile(valid, nestedAsarHeader(1, "lib", 1));
expect(
(
await client.callTool({
name: "open_binary",
arguments: { path: valid },
})
).isError,
).not.toBe(true);
expect(
(await client.callTool({ name: "inspect_artifact", arguments: {} }))
.isError,
).not.toBe(true);
} finally {
await Promise.allSettled([client.close(), server.close(), session.close()]);
}
});
@@ -0,0 +1,184 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import type { CallToolResult } from "@modelcontextprotocol/server";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { z } from "zod";
import { createServer } from "../../../src/server/createServer.js";
import { evidenceBundleSchema } from "../../../src/domain/evidenceBundle.js";
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
import { err, ok } from "../../../src/domain/result.js";
import {
createCacheProvider,
createTestBinarySession,
} from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
const bundleResult = (reply: CallToolResult) =>
z.object({ result: evidenceBundleSchema }).parse(reply.structuredContent)
.result;
const successfulTool = async (
client: Client,
name: string,
arguments_: Record<string, unknown> = {},
): Promise<CallToolResult> => {
const reply = await client.callTool({ name, arguments: arguments_ });
expect(reply.isError, `${name}: ${JSON.stringify(reply.content)}`).not.toBe(
true,
);
return reply;
};
const recordUnknownHistory = async (client: Client): Promise<void> => {
const fields = {
severity: "medium",
required_authority: "shipped-artifact",
required_confidence: "observed",
required_environment: null,
recommended_probes: [],
relationships: [],
};
const unknown = await successfulTool(client, "record_unknown", {
...fields,
question: "Which source revision produced this target?",
domain: "provenance",
});
const recorded = z
.object({ result: z.object({ unknown_id: z.string() }) })
.parse(unknown.structuredContent).result;
await successfulTool(client, "update_unknown", {
...fields,
unknown_id: recorded.unknown_id,
expected_revision: 1,
status: "investigating",
supporting_evidence_ids: [],
contradicting_evidence_ids: [],
resolution: null,
});
};
it.each(["switch", "restart"] as const)(
"preserves investigation records through failed target %s and cleanup retries",
async (transition) => {
const root = await createTestTempDirectory("rea-mcp-switch-evidence-");
const first = join(root, "first.hop");
const second = join(root, "second.hop");
await writeFile(first, "first source-owned target");
await writeFile(second, "second source-owned target");
const application = join(root, "application");
await mkdir(application);
await writeFile(
join(application, "index.js"),
'export const label = "retained";',
);
const nextTarget = transition === "switch" ? second : first;
const calls: string[] = [];
const base = createCacheProvider(calls);
let cleanupAllowed = true;
let created = 0;
const failure = new ProviderCleanupError(
"fixture",
[join(root, "runtime")],
{
reason: "fixture cleanup temporarily unavailable",
},
);
const session = createTestBinarySession({
...base,
createClient: (target, profile, context) => {
created += 1;
return {
...base.createClient(target, profile, context),
operationHealthSnapshot: () => ({
state: cleanupAllowed ? "idle" : "exited",
stage: "analysis",
retryAction: cleanupAllowed ? null : "restart_provider",
exitCode: cleanupAllowed ? null : 1,
requests: [],
}),
close: () =>
Promise.resolve(cleanupAllowed ? ok(null) : err(failure)),
};
},
});
const server = createServer(
{ kind: "session", session },
{ environment: {} },
);
const client = new Client({
name: "switch-evidence-recovery",
version: "1",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
const call = (name: string, arguments_: Record<string, unknown> = {}) =>
successfulTool(client, name, arguments_);
const query = { address: "0x1000", document: "fixture" };
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
await call("open_binary", { path: first });
// The cached primitive must be discarded when the provider is retired,
// independently of the investigation's Evidence and Unknown history.
await call("address_name", query);
const analyzed = await call("analyze_javascript_application", {
input_path: application,
format: "directory",
max_heap_mb: 128,
});
const applicationId = z
.object({ evidence_id: z.string() })
.parse(analyzed.structuredContent).evidence_id;
await recordUnknownHistory(client);
const before = bundleResult(await call("get_evidence_bundle"));
expect(before.records).toHaveLength(4);
expect(before.unknowns).toHaveLength(2);
cleanupAllowed = false;
for (let attempt = 0; attempt < 2; attempt += 1) {
const replacement = await client.callTool({
name: "open_binary",
arguments: { path: nextTarget },
});
expect(parseMcpToolError(replacement)).toMatchObject({
error: { code: "cleanup_incomplete" },
});
expect(session.activeTarget()).toBeUndefined();
expect(created).toBe(1);
expect(bundleResult(await call("get_evidence_bundle"))).toEqual(before);
}
const exportedPath = join(root, "retained.json");
await call("export_evidence_bundle", { path: exportedPath });
expect(
evidenceBundleSchema.parse(
JSON.parse(await readFile(exportedPath, "utf8")),
),
).toEqual(before);
cleanupAllowed = true;
await call("open_binary", { path: nextTarget });
expect(bundleResult(await call("get_evidence_bundle"))).toEqual(before);
await call("address_name", query);
expect(
calls.filter((operation) => operation === "address_name"),
).toHaveLength(2);
expect(created).toBe(2);
await call("inspect_analysis_view", {
source: { kind: "retained-evidence", evidence_id: applicationId },
view: { kind: "page", collection: "modules", offset: 0, limit: 10 },
});
await call("close_binary");
const closed = bundleResult(await call("get_evidence_bundle"));
expect(closed.records).toEqual([]);
expect(closed.unknowns).toEqual([]);
} finally {
cleanupAllowed = true;
await client.close();
await server.close();
await session.close();
}
},
);
+27
View File
@@ -0,0 +1,27 @@
/** Serialize a header-only ASAR, including Chromium pickle alignment. */
export const asarHeaderBytes = (json: string): Buffer => {
const text = Buffer.from(json);
const headerSize = 8 + Math.ceil(text.length / 4) * 4;
const bytes = Buffer.alloc(8 + headerSize);
bytes.writeUInt32LE(4, 0);
bytes.writeUInt32LE(headerSize, 4);
bytes.writeUInt32LE(headerSize - 4, 8);
bytes.writeUInt32LE(text.length, 12);
text.copy(bytes, 16);
return bytes;
};
/** Nested metadata without a recursive fixture builder or real directories. */
export const nestedAsarHeader = (
depth: number,
name: string,
leaves: number,
): Buffer => {
let json = `{"files":{${Array.from(
{ length: leaves },
(_, index) => `"f${index}":{"size":0,"offset":"0"}`,
).join(",")}}}`;
for (let index = 0; index < depth; index++)
json = `{"files":{${JSON.stringify(name)}:${json}}}`;
return asarHeaderBytes(json);
};