Compare commits

...
Author SHA1 Message Date
morluto c9c4b0d029 test(mcp): keep recovery checks focused on contract facts 2026-11-07 23:16:32 +08:00
morluto c15668ce8b Merge remote-tracking branch 'origin/current-8424' into HEAD 2026-11-07 23:15:55 +08:00
morluto 967c695e95 style(test): format CLI help default annotations (#9280) 2026-11-07 23:15:21 +08:00
morluto 4c778bf24e refactor: consolidate evidence closure imports 2026-11-07 22:47:13 +08:00
morluto 0b88be0a2f Merge remote-tracking branch 'origin/current-8424' into HEAD 2026-11-07 22:46:36 +08:00
morluto 65a2d037a9 fix(mcp): reconcile capacity recovery with main 2026-11-07 22:43:42 +08:00
morluto 5c19c54d7e fix(evidence): preserve recovery metadata and admission bounds 2026-11-07 22:42:28 +08:00
Heo Hao f8bb792f15 fix(cli): stop restating schema defaults in help descriptions (#9278)
The help renderer appends "(default: X)" whenever the option field declares a
schema default, so descriptions that also spell the default out print it twice:

  --block-size <value>  Power-of-two block size from 256 to 65536 bytes
                        (default 4096) (default: 4096)

Remove the hand-written default from the eleven options that own a schema
default across inspect-byte-entropy, inspect-devicetree,
inspect-packet-capture, inspect-packet-fields, inspect-signal-decoders and
decode-signal-recording, matching the rule in cliDecimalField's contract that
the domain field owns bounds, defaults and help.

Options without a schema default keep their prose: --match_limit, --line_limit,
--limit and --packet_limit are nullable or optional, so the renderer emits no
annotation and the description is the only place the effective value appears.

Add a boundary test asserting each such option states its default exactly once,
so a restated default cannot come back unnoticed.
2026-11-07 22:38:03 +08:00
Cursor Agent 64ddb9c379 merge(codex/context-capacity-recovery): keep source publication on the parent
Completed observations still survive source-publication failure, including
caller paths inside unretained Evidence. Parent lifetime, snapshot context,
and workflow-unknown publication stay on the same error projection.
2026-11-07 12:16:38 +00:00
Cursor Agent 1dbaa35936 merge(main): keep address-context resource recovery on current errors
Facet refusals still carry resource_constraint recovery and unretained
Evidence, while caller lifetime, capability guidance, and evidence-bundle
overflow steps from main stay in the same projection.
2026-11-07 12:13:33 +00:00
morluto c99ab7ee21 fix(cli): preserve caller path in unretained evidence 2026-11-07 15:30:37 +08:00
morluto 6fd318a99a fix: preserve selected paths when reusing evidence 2026-11-04 12:27:59 +08:00
morluto 675d3367d1 fix: admit completed analysis before evidence construction 2026-11-04 12:00:20 +08:00
morluto cc2bd100a5 test: use owners for diagnostic wording 2026-11-04 09:47:40 +08:00
morluto e215b08906 fix: preserve cleanup on partial recovery limits 2026-11-04 09:35:42 +08:00
morluto 011877468d test(evidence): align recovery checks with owners 2026-11-03 12:59:53 +08:00
morluto a8f5811b6d refactor(evidence): remove superseded cleanup projection 2026-11-03 12:55:37 +08:00
morluto bc962f5c19 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-03 12:54:06 +08:00
morluto 588a4c81a0 fix(evidence): preserve caller scope in secondary failures 2026-11-03 03:28:55 +08:00
morluto 68b1d91310 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-03 03:24:52 +08:00
morluto 4295ae38df Merge commit '75c3cab5401dd48b831c1405ff4589af84f30614' into HEAD
# Conflicts:
#	docs/mcp-contracts.md
#	src/application/AnalysisContextQueries.ts
#	src/application/DirectAnalysis.ts
#	src/application/DirectAnalysisCleanup.ts
#	src/application/WorkflowEvidence.ts
#	src/contracts/errorSchemas.ts
#	src/contracts/sessionToolContracts.ts
#	src/contracts/toolOutputSchemaGroups.ts
#	src/domain/analysisErrorCleanup.ts
#	src/domain/analysisErrorProjection.ts
#	src/server/ToolResultDelivery.ts
#	src/server/registerApplicationTools/compareSourceToBundle.ts
#	src/server/registerApplicationTools/traceFeature.ts
#	src/server/registerEnhancedTools.ts
2026-11-03 03:24:40 +08:00
morluto 92adb42521 test(evidence): cover mixed recovery guidance 2026-11-01 21:01:41 +08:00
morluto f7c1679374 fix(evidence): clarify unretained recovery export 2026-11-01 21:00:43 +08:00
morluto 1de53a34c8 fix(evidence): preserve completed composed workflows 2026-11-01 21:00:43 +08:00
morluto 3d371b9b26 test(evidence): compare omission guidance with its owner 2026-11-01 21:00:43 +08:00
morluto ae0ff117b6 fix(evidence): clarify retention of omitted observations 2026-11-01 21:00:43 +08:00
morluto fc35f83b33 fix(evidence): preserve recovery identity and finalization facts 2026-11-01 21:00:43 +08:00
morluto 453dacf6a3 test: cover bounded recovery with unknown retention 2026-11-01 21:00:43 +08:00
morluto 5cf06c4517 fix: retain completed results when source publication fails 2026-11-01 21:00:43 +08:00
morluto 2f59bc0ff6 fix: expose authenticated records refused by the evidence ledger (#8426)
* fix: preserve completed evidence on retention refusal

* fix: preserve context when unretained evidence exceeds delivery limits

* fix(mcp): recover oversized context from session evidence

* fix(evidence): preserve completed atomic write results
2026-11-01 17:23:27 +08:00
morluto cb0cb206fb fix!: preserve address context resource recovery
BREAKING CHANGE: Clients validating unavailable context facets against a cached strict full output schema must refresh tools/list to accept resource_constraint errors.
2026-11-01 10:29:09 +08:00
morluto dbb99b15a0 test: use session contract for address context 2026-11-01 10:28:37 +08:00
morluto 47d514f1c0 fix!: preserve address context input errors
BREAKING CHANGE: Strict clients that cached the previous full inspect_address_context output schema must refresh tools/list and accept the typed error field on unavailable facets.
2026-11-01 10:18:09 +08:00
morluto 40e8713e59 fix: retain address context cleanup failures 2026-11-01 10:04:09 +08:00
79 changed files with 5504 additions and 245 deletions

No files matched your search

+37 -3
View File
@@ -1280,6 +1280,15 @@ Native call tracing and process capture retain available observations in
`details.partial_observation` on failure, including when cleanup succeeds.
The observation reports its partial coverage; cleanup details describe host
state separately from the execution failure.
When a completed observation cannot be published because recording a source
failed, the original source failure remains the error. `partial_observation`
can carry the failed source and `completed_observation` carries the completed
result, with `source_closure: "not-established"`. A retained reference is
reported only when its owning ledger confirms it; an inline result with
`retention: "unknown"` makes no claim about ledger membership. If an
observation cannot fit the MCP response, its corresponding `_omitted` detail
reports the delivery limit. Cleanup failure and other completed source failures
remain visible separately.
For these tools, cancellation observed during result finalization or owned
cleanup also returns typed cancellation with the available observations.
A normal target exit does not establish request completion. Incomplete cleanup
@@ -1296,6 +1305,22 @@ Those raw observations are not recorded in the Evidence bundle. If the SDK
suppresses the error reply after client cancellation, Evidence tools cannot
recover them. An empty bundle does not prove that no partial work or side
effect occurred; output receipts and cleanup observations remain distinct.
If an authenticated Evidence record exceeds the investigation's retention
allowance, the refusal carries it inline as
`details.partial_observation: {kind: "unretained-evidence", evidence: ...}`
when it fits the response budget. Its ID identifies the supplied content; it
is not a retained-session reference. If authentication itself failed or lacked
headroom, no authenticated Evidence is claimed. When the completed record does
not fit the MCP response, the original retention error carries
`details.partial_observation_omitted` with the response constraint if the
remaining error fits; the observation remains unretained. Otherwise the usual
oversized-error recovery applies.
When an atomic derived-Evidence and Unknown write exceeds the allowance after
authentication, the completed Evidence is returned the same way if absent from
the investigation. If that exact Evidence was already present, the error
instead carries `{kind: "retained-evidence", evidence_id: ...}` for the current
session. The refused write adds neither a record nor an Unknown revision; a
previously retained record or Unknown remains available.
The broader retention decision is tracked in
[#4276](https://github.com/morluto/rea/issues/4276).
@@ -1577,12 +1602,21 @@ cursor outside a procedure is represented as
procedure resolution returns typed unreadable output; provider errors never
become absence from their message text. `inspect_address_context` requires an explicit address and
returns name, procedure, comment, inline-comment, and bookmark facets;
unsupported facets are local `unavailable` outcomes. A facet whose input is
rejected includes its canonical `error` projection, including any structured
issues, alongside the other facet outcomes. Use `current_document`,
unsupported facets are local `unavailable` outcomes. A facet rejected for
invalid input, source Evidence integrity or a resource constraint includes its canonical `error`
projection, including structured issues or reported limits and recovery,
alongside the other facet outcomes. Use `current_document`,
`current_address`, and `current_procedure` for direct single-field lookups; use
the aggregate tools when you need related context together.
If unretained facet Evidence makes the context response exceed an MCP delivery
limit, REA omits those partial records in estimated size order until the reply
fits. Each affected facet identifies its unretained omission and the delivery
constraint; other facets and partials remain in the response when they fit. The
size estimate is used only for ordering and can omit more records than a perfect
wire-size ordering would. If the remaining context still cannot fit, call the
needed address operations individually and follow their delivery diagnostics.
## Integrity attribution and reconstruction claims
Artifact comparison results include `integrity_mismatch_evidence_ids`, naming
+84 -5
View File
@@ -9,18 +9,23 @@ import {
AnalysisCapabilityUnavailableError,
AnalysisInputError,
AnalysisOutputError,
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
import {
omitUnretainedEvidenceFromError,
projectAnalysisError,
unretainedEvidenceCandidates,
type AnalysisErrorProjection,
} from "../domain/analysisErrorProjection.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { analysisErrorWithCancellation } from "../domain/analysisErrorCancellation.js";
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { analysisBookmarkSchema } from "../domain/native/nativeInventory.js";
import { parseContainingProcedureResolution } from "../domain/native/nativeObservation.js";
import { nativeAddressCoordinateInputSchema } from "../domain/native/nativeAddress.js";
import { resolveAnalysisDocument } from "./AnalysisDocument.js";
import { estimateJsonStorage } from "./JsonAuthenticationHeadroom.js";
import { err, ok, type Result } from "../domain/result.js";
type Facet =
@@ -32,6 +37,16 @@ type Facet =
readonly error?: AnalysisErrorProjection;
};
type AddressContext = {
readonly address: string;
readonly document: string;
readonly name: Facet;
readonly procedure: Facet;
readonly comment: Facet;
readonly inline_comment: Facet;
readonly bookmarks: Facet;
};
/** Facets inspected after an explicit or provider-resolved document is known. */
export const ADDRESS_CONTEXT_FACET_OPERATIONS = [
"address_name",
@@ -99,12 +114,62 @@ export const getNavigationContext = async (
});
};
/** Preserve other facets and smaller partials if unretained Evidence cannot fit. */
export function* omitContextEvidenceForDelivery(
context: AddressContext,
limit: {
readonly constraint: "receive-buffer" | "string-length";
readonly bytesAtLeast: number;
},
budgetBytes: number,
): Generator<AddressContext> {
const names = [
"name",
"procedure",
"comment",
"inline_comment",
"bookmarks",
] as const;
const candidates = names.flatMap((name) => {
const facet = context[name];
if (facet.state !== "unavailable" || facet.error === undefined) return [];
return [...unretainedEvidenceCandidates(facet.error)].map((candidate) => {
const size = estimateJsonStorage(candidate.value, {
maximumValueBytes: budgetBytes,
includeSerialized: false,
}).valueBytes;
return { name, candidate, size };
});
});
candidates.sort((left, right) => right.size - left.size);
let current = context;
for (const { name, candidate } of candidates) {
const facet = current[name];
if (facet.state !== "unavailable" || facet.error === undefined) continue;
const compact = omitUnretainedEvidenceFromError(
facet.error,
limit,
candidate,
);
if (compact === undefined) continue;
current = {
...current,
[name]: {
...facet,
remediation: compact.remediation.action,
error: compact,
},
};
yield current;
}
}
/** Inspect bounded provider-neutral facets at one explicit reproducible address. */
export const inspectAddressContext = async (
analysis: AnalysisOperationPort,
input: { readonly address: string; readonly document?: string | undefined },
signal?: AbortSignal,
): Promise<Result<JsonValue, AnalysisError>> => {
): Promise<Result<AddressContext, AnalysisError>> => {
const document = await resolveAnalysisDocument(
analysis,
input.document,
@@ -225,9 +290,18 @@ const unavailableFacet = (
: "Review provider availability in binary_session with detail complete and select another supported provider if appropriate. After switching, if a name is known and search_procedures is available, use it to find candidates and verify their identity and coordinates.",
};
}
if (error instanceof AnalysisInputError) {
if (
error instanceof AnalysisInputError ||
error instanceof AnalysisResourceConstraintError ||
(error instanceof EvidenceIntegrityError &&
error.sourcePublicationRecovery !== undefined)
) {
const projected = projectAnalysisError(error);
if (projected.code === "invalid_request")
if (
projected.code === "invalid_request" ||
projected.code === "resource_constraint" ||
projected.code === "evidence_integrity_mismatch"
)
return {
state: "unavailable",
reason: projected.message,
@@ -245,7 +319,11 @@ const unavailableFacet = (
const isRequestLevelFailure = (error: AnalysisError): boolean =>
error._tag === "AnalysisCancelledError" ||
error._tag === "AnalysisTimeoutError" ||
error._tag === "NoBinaryOpenError";
error._tag === "NoBinaryOpenError" ||
(error.sourcePublicationRecovery !== undefined &&
!(error instanceof AnalysisInputError) &&
!(error instanceof AnalysisResourceConstraintError) &&
!(error instanceof EvidenceIntegrityError));
/** Wait for all facets and retain every unresolved cleanup owner. */
const contextFailure = (
@@ -292,7 +370,8 @@ const contextFailure = (
).bindCleanupDependencies([previous, failure]);
} else if (
!(failure instanceof AnalysisCancelledError) &&
isRequestLevelFailure(failure)
(isRequestLevelFailure(failure) ||
failure.sourcePublicationRecovery !== undefined)
) {
combined.retainSecondaryFailure(failure);
}
+88 -22
View File
@@ -1,3 +1,4 @@
import { admitCompletedAnalysis } from "./investigation/admitCompletedAnalysis.js";
import type { EspFlashSelection } from "../domain/firmware/espFlashLocation.js";
import type { ExecutableFormatHint } from "../domain/dosCom.js";
import { dispatchQueuedInterrupts } from "../process/queuedInterrupts.js";
@@ -25,8 +26,10 @@ import {
retainAnalysisResultSources,
type AnalysisResultWithSources,
} from "./investigation/recordEvidenceSources.js";
import { resolveInvestigationEvidenceClosure } from "./investigation/InvestigationEvidence.js";
import { createInvestigationEvidenceClosureChecker } from "./investigation/InvestigationEvidence.js";
import {
createInvestigationEvidenceClosureChecker,
resolveInvestigationEvidenceClosure,
} from "./investigation/InvestigationEvidence.js";
import type { Evidence } from "../domain/evidence.js";
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
@@ -43,6 +46,7 @@ import {
AnalysisSnapshotMismatchError,
type PublishedEvidenceFile,
} from "../domain/evidenceErrors.js";
import { analysisErrorWithCancellation } from "../domain/analysisErrorCancellation.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { access, stat } from "node:fs/promises";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
@@ -278,7 +282,13 @@ export const runProviderAnalysis = async (
operationSignal,
);
await dispatchQueuedInterrupts();
if (!original.ok) return err(cliError(original.error, path));
if (!original.ok)
return err(
cliError(
retainFinalCancellation(original.error, tool, operationSignal),
path,
),
);
const evidence = withEvidenceCallerInputPath(original.value, path);
if (operationSignal.aborted)
return err(
@@ -322,6 +332,7 @@ export const runManagedProviderExecution = async (
tool: ManagedToolName,
signal?: AbortSignal,
parameters: Readonly<Record<string, JsonValue>> = {},
recoveryParameters?: Readonly<Record<string, JsonValue>>,
): Promise<Result<AnalysisExecution, AnalysisError>> =>
withProcessCancellation(signal, async (operationSignal) => {
const session = await dependencies.createManagedBinarySession();
@@ -330,9 +341,14 @@ export const runManagedProviderExecution = async (
async () => {
const opened = await session.open(path, { signal: operationSignal });
if (!opened.ok) return opened;
return await session.execute(tool, parameters, {
signal: operationSignal,
});
return recoveryParameters === undefined
? await session.execute(tool, parameters, { signal: operationSignal })
: await session.executeForDirectObservation(
tool,
parameters,
recoveryParameters,
{ signal: operationSignal },
);
},
managedAnalysisCleanupFailure,
);
@@ -353,6 +369,7 @@ const runManagedProviderAnalysis = async (
tool,
signal,
arguments_,
evidenceParameters,
);
if (!execution.ok) return execution;
const value = execution.value;
@@ -626,13 +643,35 @@ const runAnalysis = async (
readonly cleanupError?: AnalysisError;
}>(
session,
async () => ({ result: await execute() }),
async () => {
const result = await execute();
if (
!result.ok &&
result.error.sourcePublicationRecovery?.completed.kind ===
"retained-evidence"
) {
const retained = session.evidenceForAnalysis(
result.error.sourcePublicationRecovery.completed.evidence_id,
);
if (retained !== undefined)
result.error.sourcePublicationRecovery = {
...result.error.sourcePublicationRecovery,
completed: {
kind: "inline-evidence",
retention: "one-shot-session",
source_closure: "not-established",
evidence: retained,
},
};
}
return { result };
},
(value, cleanupError) => ({ ...value, cleanupError }),
);
await dispatchQueuedInterrupts();
const result = outcome.result.ok
? completeProviderEvidence(outcome.result.value, signal)
: outcome.result;
: err(retainFinalCancellation(outcome.result.error, tool, signal));
if (!result.ok && publishedSnapshot !== undefined)
result.error.retainDiagnostics({
analysis_snapshot: { ...publishedSnapshot },
@@ -655,6 +694,19 @@ const runAnalysis = async (
: err(cliError(result.error, path));
};
/** Preserve an earlier failure and a cancellation observed during final cleanup. */
const retainFinalCancellation = (
failure: AnalysisError,
tool: string,
signal: AbortSignal,
): AnalysisError =>
signal.aborted
? analysisErrorWithCancellation(
failure,
new AnalysisCancelledError(tool, { cause: signal.reason }),
)
: failure;
const executeAnalysisTool = async (input: {
readonly session: BinarySession;
readonly openedTarget: BinaryTarget;
@@ -732,12 +784,17 @@ const executeAnalysisTool = async (input: {
: { cachePolicy: result.value.cachePolicy }),
});
}
let result = await session.execute(tool, input.arguments, {
signal,
...(input.saveSnapshot && tool === "procedure_pseudo_code"
? { snapshotContext: "one-shot-target" as const }
: {}),
});
let result = await session.executeForDirectObservation(
tool,
input.arguments,
input.evidenceParameters,
{
signal,
...(input.saveSnapshot && tool === "procedure_pseudo_code"
? { snapshotContext: "one-shot-target" as const }
: {}),
},
);
if (
tool === "xrefs" &&
typeof input.arguments.address === "string" &&
@@ -759,23 +816,32 @@ const executeAnalysisTool = async (input: {
);
if (!address.ok) return address;
if (address.value !== input.arguments.address)
result = await session.execute(
result = await session.executeForDirectObservation(
tool,
{ ...input.arguments, address: address.value },
input.evidenceParameters,
{ signal },
);
}
if (!result.ok) return result;
const observation = {
...result.value,
subject:
result.value.subject === undefined
? input.openedTarget
: result.value.subject,
};
const admitted = admitCompletedAnalysis(
tool,
input.evidenceParameters,
observation,
session.admitRecoveryObservation.bind(session),
);
if (!admitted.ok) return admitted;
const evidence = createAnalysisExecutionEvidence(
tool,
input.evidenceParameters,
{
...result.value,
subject:
result.value.subject === undefined
? input.openedTarget
: result.value.subject,
},
observation,
"observed",
);
return ok({ evidence });
+51 -12
View File
@@ -1,4 +1,7 @@
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import {
sourcePublicationRecoveryOf,
type AnalysisError,
} from "../domain/analysisErrorBase.js";
import {
childFailureDiagnostics,
projectAnalysisError,
@@ -33,19 +36,55 @@ export const projectDirectAnalysisFailure = (
callerInputPath: string,
): ReturnType<typeof projectAnalysisError> => {
const projection = projectInvocationFailure(error);
const observation = tryParseEvidence(projection.details?.partial_observation);
return observation === undefined
? projection
: {
...projection,
details: {
...projection.details,
partial_observation: withEvidenceCallerInputPath(
observation,
const partial = projection.details?.partial_observation;
const wrapped =
typeof partial === "object" &&
partial !== null &&
!Array.isArray(partial) &&
partial.kind === "unretained-evidence";
const observation = tryParseEvidence(wrapped ? partial.evidence : partial);
const rebound =
observation === undefined
? undefined
: withEvidenceCallerInputPath(observation, callerInputPath);
const completed = sourcePublicationRecoveryOf(error)?.completed;
const completedWithPath =
completed?.kind === "unretained-evidence" ||
completed?.kind === "inline-evidence"
? {
...completed,
evidence: withEvidenceCallerInputPath(
completed.evidence,
callerInputPath,
),
},
};
}
: completed?.kind === "unretained-observation"
? {
...completed,
observation: {
...completed.observation,
caller_input_path: callerInputPath,
},
}
: undefined;
if (rebound === undefined && completedWithPath === undefined)
return projection;
return {
...projection,
details: {
...projection.details,
...(rebound === undefined
? {}
: {
partial_observation: wrapped
? { ...partial, evidence: rebound }
: rebound,
}),
...(completedWithPath === undefined
? {}
: { completed_observation: completedWithPath }),
},
};
};
/** Await one-shot cleanup and retain either the outcome or both failures. */
+23 -12
View File
@@ -367,6 +367,28 @@ const providerSettingSelections = (
]),
];
/** Render what the registration comparison established without inferring its effective environment. */
export const registrationEnvironmentDetail = (
client: string,
{
unrecorded,
differing,
unresolved,
}: NonNullable<ClientRegistrationStatus["provider_settings"]>,
): string =>
[
`Provider checks used this terminal's environment; what the server ${client} starts receives is unknown.`,
...(unrecorded.length === 0
? []
: [`Its registration does not record ${unrecorded.join(", ")}.`]),
...(differing.length === 0
? []
: [`It records other values for ${differing.join(", ")}.`]),
...(unresolved.length === 0
? []
: [`REA could not resolve its ${unresolved.join(", ")}.`]),
].join(" ");
/**
* A healthy provider check describes this terminal, not the server a client
* starts. Report what the registration's file leaves out without gating health:
@@ -385,18 +407,7 @@ const registrationEnvironmentChecks = (
name: `registration_environment:${client}`,
ok: true,
classification: "unknown" as const,
detail: [
`Provider checks used this terminal's environment; what the server ${client} starts receives is unknown.`,
...(unrecorded.length === 0
? []
: [`Its registration does not record ${unrecorded.join(", ")}.`]),
...(differing.length === 0
? []
: [`It records other values for ${differing.join(", ")}.`]),
...(unresolved.length === 0
? []
: [`REA could not resolve its ${unresolved.join(", ")}.`]),
].join(" "),
detail: registrationEnvironmentDetail(client, settings),
details: {
effective_environment: "unknown",
providers: selections
@@ -4,7 +4,12 @@ import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { createDoctorHostFixture as host } from "./Doctor.fixture.js";
import { runDoctor, systemDoctorHost, type DoctorHost } from "./Doctor.js";
import {
registrationEnvironmentDetail,
runDoctor,
systemDoctorHost,
type DoctorHost,
} from "./Doctor.js";
const inspection = (
id: string,
@@ -176,9 +181,16 @@ it("states each comparison result without a claim it did not establish", async (
]),
}),
);
const detail = report.checks.find(({ name }) => name === CHECK)?.detail;
expect(detail).toContain("what the server cursor starts receives is unknown");
expect(detail).toContain("records other values for JAVA_HOME");
expect(detail).toContain("could not resolve its GHIDRA_INSTALL_DIR");
expect(detail).not.toContain("does not record");
const check = report.checks.find(({ name }) => name === CHECK);
expect(check).toMatchObject({
classification: "unknown",
details: { effective_environment: "unknown" },
});
expect(check?.detail).toBe(
registrationEnvironmentDetail("cursor", {
unrecorded: [],
differing: ["JAVA_HOME"],
unresolved: ["GHIDRA_INSTALL_DIR"],
}),
);
});
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
estimateJsonStorage,
estimateJsonArrayScalarEntry,
requireAuthenticationHeadroom,
} from "./JsonAuthenticationHeadroom.js";
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
@@ -19,6 +20,56 @@ it("stops a bounded representation scan before visiting an over-budget tail", ()
expect(measured.valueBytes).toBeGreaterThan(512);
});
describe("JSON authentication headroom", () => {
it("measures source IDs without first constructing their array", () => {
const values = ["ev_123", null, 'escaped\"\n😀'];
const empty = estimateJsonStorage({ evidence_links: [] });
const incremental = values.reduce((total, value, index) => {
const entry = estimateJsonArrayScalarEntry(value, index > 0);
return {
valueBytes: total.valueBytes + entry.valueBytes,
serializedUtf16Bytes:
total.serializedUtf16Bytes + entry.serializedUtf16Bytes,
};
}, empty);
expect(incremental).toEqual(
estimateJsonStorage({ evidence_links: values }),
);
});
it("accounts for a measured representation that has not yet been allocated", () => {
const value = { result: "small" };
const base = estimateJsonStorage(value);
const additional = { valueBytes: 8192, serializedUtf16Bytes: 2048 };
const total =
(base.valueBytes +
base.serializedUtf16Bytes +
additional.valueBytes +
additional.serializedUtf16Bytes) *
2;
const request = {
operation: "address_name",
path: "recovery",
description: "Cannot authenticate",
additional,
};
expect(
requireAuthenticationHeadroom(value, {
...request,
availableBytes: total - 1,
}),
).toMatchObject({
ok: false,
error: {
resource: "memory",
reportedLimits: { authentication_bytes: total },
},
});
expect(
requireAuthenticationHeadroom(value, {
...request,
availableBytes: total,
}),
).toEqual({ ok: true, value: undefined });
});
it("does not queue every row while deciding whether a wide report can fit", () => {
let pending = 0;
let peak = 0;
+22 -1
View File
@@ -6,6 +6,18 @@ import { err, ok, type Result } from "../domain/result.js";
/** Authentication clones parsed JSON and hashes its canonical commitment. */
const AUTHENTICATION_COPIES = 2;
/** Incremental storage of a string or null appended to an already measured array. */
export const estimateJsonArrayScalarEntry = (
value: string | null,
hasPrevious: boolean,
): { valueBytes: number; serializedUtf16Bytes: number } => ({
valueBytes: 16 + (value === null ? 16 : 64 + value.length * 2),
serializedUtf16Bytes:
((value === null ? 4 : jsonStringCodeUnits(value)) +
(hasPrevious ? 1 : 0)) *
2,
});
/**
* Parsed storage and optional UTF-16 serialization work. A bounded value scan
* returns only a lower bound after exceeding maximumValueBytes.
@@ -77,11 +89,20 @@ export const requireAuthenticationHeadroom = (
readonly path: string;
readonly description: string;
readonly availableBytes?: number;
readonly additional?: {
readonly valueBytes: number;
readonly serializedUtf16Bytes: number;
};
},
): Result<void, AnalysisResourceConstraintError> => {
const availableBytes =
options.availableBytes ?? getHeapStatistics().total_available_size;
const { valueBytes, serializedUtf16Bytes } = estimateJsonStorage(value);
const estimate = estimateJsonStorage(value);
const valueBytes =
estimate.valueBytes + (options.additional?.valueBytes ?? 0);
const serializedUtf16Bytes =
estimate.serializedUtf16Bytes +
(options.additional?.serializedUtf16Bytes ?? 0);
// Canonical hashing and manifest normalization can still encode scalars or
// metadata; account for their escaped representations before allocating them.
const requiredBytes =
+26 -17
View File
@@ -10,21 +10,30 @@ export const workflowAnalysisProfile = (
upstream: AnalysisProfileCommitment,
operation?: AnalysisOperation,
): AnalysisProfileCommitment =>
createAnalysisProfile(REA_WORKFLOW_PROVIDER, {
upstream_analysis_profile: upstream,
source_limitations: "provider-operation-caveats-v1",
...(operation === "trace_native_values"
? { value_flow_absence: "recorded-versus-unavailable-v1" }
: {}),
...(operation === "binary_overview"
? { workflow_revision: 5 }
: operation === "batch_decompile"
createAnalysisProfile(
REA_WORKFLOW_PROVIDER,
workflowAnalysisProfileParameters(upstream, operation),
);
/** The owned profile shape, with null allowed when estimating its envelope. */
export const workflowAnalysisProfileParameters = (
upstream: AnalysisProfileCommitment | null,
operation?: AnalysisOperation,
) => ({
upstream_analysis_profile: upstream,
source_limitations: "provider-operation-caveats-v1",
...(operation === "trace_native_values"
? { value_flow_absence: "recorded-versus-unavailable-v1" }
: {}),
...(operation === "binary_overview"
? { workflow_revision: 5 }
: operation === "batch_decompile"
? { workflow_revision: 4 }
: operation === "trace_feature"
? { workflow_revision: 4 }
: operation === "trace_feature"
? { workflow_revision: 4 }
: operation === "get_call_graph"
? { workflow_revision: 3 }
: operation === "trace_native_values"
? { workflow_revision: 2 }
: { workflow_revision: 1 }),
});
: operation === "get_call_graph"
? { workflow_revision: 3 }
: operation === "trace_native_values"
? { workflow_revision: 2 }
: { workflow_revision: 1 }),
});
+122
View File
@@ -1,9 +1,12 @@
import { describe, expect, it } from "vitest";
import { z } from "zod";
import {
recordWorkflowUnknowns,
createWorkflowEvidence,
executeComposedWorkflow,
retainComposedWorkflowSources,
retainPartialWorkflowEvidence,
workflowSnapshotRecord,
} from "./WorkflowEvidence.js";
import { InvestigationRecords } from "./investigation/InvestigationRecords.js";
@@ -18,6 +21,7 @@ import {
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { estimateJsonStorage } from "./JsonAuthenticationHeadroom.js";
import { nativeApiBoundarySchema } from "../domain/native/nativeApiBoundary.js";
import { projectNativeApiInspection } from "./native/NativeApiInspection.js";
import { functionDossierSchema } from "../domain/native/functionDossier.js";
@@ -25,6 +29,124 @@ import { ghidraFunctionDossier } from "../domain/ghidraValues.fixture.js";
import { ANALYSIS_SNAPSHOT_PROFILE } from "../domain/analysisSnapshot.fixture.js";
import { createEvidence } from "../domain/evidence.js";
it("preserves cleanup and reports an omitted partial when authentication headroom is unavailable", () => {
const error = new AnalysisCancelledError("get_call_graph", {
cleanup: { reason: "close failed", resources: ["session:fixture"] },
});
const outcome = {
...err(error),
partial: { result: { nodes: ["observed"] }, limitations: [] },
sourceEvidence: [],
};
retainPartialWorkflowEvidence(
outcome,
{
target: undefined,
operation: "get_call_graph",
parameters: {},
upstreamProfile: undefined,
},
() =>
err(
new AnalysisResourceConstraintError(
"get_call_graph",
"capacity",
"no headroom",
null,
),
),
);
expect(projectAnalysisError(error)).toMatchObject({
code: "cleanup_incomplete",
details: {
cleanup: "incomplete",
resources: ["session:fixture"],
partial_observation: {
kind: "omitted-observation",
reason: "insufficient-authentication-headroom",
},
},
});
});
it.each([0, 1, 100])(
"admits enough recovery representation for %i completed batch items",
(count) => {
const provider = { id: "fixture", name: "Fixture", version: "1" };
const source = createEvidence(undefined, provider, {
operation: "health",
parameters: {},
result: "source",
});
const items = Array.from({ length: count }, (_, index) => ({
address: `input-${index}`,
procedure: {
status: "resolved",
address: `0x${(index + 1).toString(16)}`,
name: null,
},
status: "ok",
pseudocode: 'escaped " and Unicode α'.repeat((index % 3) + 1),
}));
const completed = {
...ok({
result: { items, total: count, succeeded: count, failed: 0 },
limitations: [],
evidenceLinks: [source.evidence_id],
}),
sourceEvidence: [source],
};
let admitted: ReturnType<typeof estimateJsonStorage> | undefined;
const outcome = retainComposedWorkflowSources(
() =>
err(
new AnalysisResourceConstraintError(
"record_evidence",
"capacity",
"full",
null,
),
),
completed,
{
target: {
path: "/tmp/example",
sha256: "a".repeat(64),
kind: "executable",
format: "mach-o",
architecture: "arm64",
availableArchitectures: ["arm64"],
},
operation: "batch_decompile",
parameters: { addresses: items.map((item) => item.address) },
upstreamProfile: ANALYSIS_SNAPSHOT_PROFILE,
},
() => undefined,
(input, _operation, additional) => {
const base = estimateJsonStorage(input);
admitted = {
valueBytes: base.valueBytes + additional.valueBytes,
serializedUtf16Bytes:
base.serializedUtf16Bytes + additional.serializedUtf16Bytes,
};
return ok(undefined);
},
);
expect(outcome.ok).toBe(false);
if (outcome.ok || admitted === undefined)
throw new Error("Expected admitted recovery");
const error = projectAnalysisError(outcome.error);
const completedEvidence = z
.object({ completed_observation: z.object({ evidence: z.unknown() }) })
.parse(error.details).completed_observation.evidence;
const actual = estimateJsonStorage(completedEvidence);
expect(admitted.valueBytes).toBeGreaterThanOrEqual(actual.valueBytes);
expect(admitted.serializedUtf16Bytes).toBeGreaterThanOrEqual(
actual.serializedUtf16Bytes,
);
},
);
it("links shared provider source Evidence once without copying its payload into the workflow", async () => {
const source = createEvidence(
undefined,
+167 -8
View File
@@ -3,7 +3,10 @@ import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
import { jsonStringByteLength } from "../domain/jsonStringByteLength.js";
import { createAnalysisExecutionEvidence } from "../domain/analysisObservation.js";
import { REA_WORKFLOW_PROVIDER } from "../domain/investigationProviders.js";
import { workflowAnalysisProfile } from "./WorkflowAnalysisProfile.js";
import {
workflowAnalysisProfile,
workflowAnalysisProfileParameters,
} from "./WorkflowAnalysisProfile.js";
import type {
AnalysisOperation,
AnalysisOperationPort,
@@ -20,6 +23,8 @@ import {
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import {
createEvidence,
MISSING_EVIDENCE_SUBJECT_LIMITATION,
evidenceEnvelopeForAdmission,
type Evidence,
type EvidenceLocation,
} from "../domain/evidence.js";
@@ -29,9 +34,16 @@ import { UnknownRegistryError } from "../domain/unknownRegistryError.js";
import { EvidenceLedger } from "./investigation/EvidenceLedger.js";
import {
recordEvidenceSources,
retainAnalysisResultSources,
withEvidenceSources,
type AnalysisResultWithSources,
} from "./investigation/recordEvidenceSources.js";
import { preserveCompletedSourceEvidence } from "./investigation/sourcePublicationRecovery.js";
import {
admitInvestigationAuthentication,
type RecoveryAdmission,
} from "./investigation/InvestigationRetention.js";
import { estimateJsonStorage } from "./JsonAuthenticationHeadroom.js";
import { workflowUnknownInput } from "./investigation/WorkflowUnknown.js";
import {
renderSourceLimitations,
@@ -205,7 +217,7 @@ export const executeComposedWorkflow = async (
};
/** Build the shared Evidence record for a successful composed binary workflow. */
export const createWorkflowEvidence = (input: {
export interface WorkflowEvidenceInput {
readonly target: BinaryTarget | undefined;
readonly operation: AnalysisOperation;
readonly parameters: Readonly<Record<string, JsonValue>>;
@@ -213,7 +225,13 @@ export const createWorkflowEvidence = (input: {
readonly upstreamProfile: AnalysisProfileCommitment | undefined;
readonly upstreamLimitations: readonly string[];
readonly upstreamEvidenceLinks?: readonly string[];
}): Evidence =>
}
const WORKFLOW_DERIVATION_LIMITATION = "Derived by an REA composed workflow.";
export const createWorkflowEvidence = (
input: WorkflowEvidenceInput,
): Evidence =>
createEvidence(input.target, REA_WORKFLOW_PROVIDER, {
operation: input.operation,
parameters: input.parameters,
@@ -230,15 +248,134 @@ export const createWorkflowEvidence = (input: {
...(input.upstreamEvidenceLinks === undefined
? {}
: { evidenceLinks: input.upstreamEvidenceLinks }),
limitations: [
"Derived by an REA composed workflow.",
...input.upstreamLimitations,
],
limitations: [WORKFLOW_DERIVATION_LIMITATION, ...input.upstreamLimitations],
...(input.operation === "batch_decompile"
? { locations: batchProcedureLocations(input.result) }
: {}),
});
const admitWorkflowRecovery = (
observation: ComposedWorkflowObservation,
evidenceInput: Pick<
WorkflowEvidenceInput,
"target" | "operation" | "parameters" | "upstreamProfile"
>,
admitRecovery: RecoveryAdmission,
) => {
const admissionInput = {
operation: evidenceInput.operation,
parameters: evidenceInput.parameters,
target:
evidenceInput.target === undefined
? null
: {
path: evidenceInput.target.path,
sha256: evidenceInput.target.sha256,
format: evidenceInput.target.format,
architecture: evidenceInput.target.architecture ?? null,
},
profile: evidenceInput.upstreamProfile ?? null,
result: observation.result,
limitations: observation.limitations,
links: observation.evidenceLinks ?? [],
};
// The completed result can also generate a location per batch item and
// the workflow profile embeds the upstream profile. Admit these copies
// before constructing or hashing the recovery Evidence.
const shell = evidenceEnvelopeForAdmission(
evidenceInput.target,
REA_WORKFLOW_PROVIDER,
{
operation: evidenceInput.operation,
parameters: {},
result: null,
...(evidenceInput.upstreamProfile === undefined
? {}
: {
analysisProfile: {
provider: REA_WORKFLOW_PROVIDER,
parameters: workflowAnalysisProfileParameters(
null,
evidenceInput.operation,
),
digest: "0".repeat(64),
},
}),
confidence: "derived",
limitations: [WORKFLOW_DERIVATION_LIMITATION],
locations: [],
evidenceLinks: [],
},
);
const additional = estimateJsonStorage([
observation.result,
evidenceInput.upstreamProfile ?? null,
shell,
...(evidenceInput.target === undefined
? [MISSING_EVIDENCE_SUBJECT_LIMITATION]
: []),
]);
return admitRecovery(admissionInput, evidenceInput.operation, additional);
};
/** Experimental shared recovery of a workflow that completed before source admission. */
export const retainComposedWorkflowSources = (
recordEvidence: Parameters<typeof retainAnalysisResultSources>[1],
result: Awaited<ReturnType<typeof executeComposedWorkflow>>,
evidenceInput: Omit<
WorkflowEvidenceInput,
"result" | "upstreamLimitations" | "upstreamEvidenceLinks"
>,
evidenceForAnalysis?: (id: string) => Evidence | undefined,
admitRecovery: RecoveryAdmission = (input, operation, additional) =>
admitInvestigationAuthentication(input, operation, {}, additional),
): ReturnType<
typeof retainAnalysisResultSources<{
readonly result: JsonValue;
readonly limitations: readonly string[];
readonly cachePolicy?: "live";
readonly evidenceLinks?: readonly string[];
}>
> => {
const retained = retainAnalysisResultSources(
evidenceInput.operation,
recordEvidence,
result,
);
if (result.ok && !retained.ok) {
const admitted = admitWorkflowRecovery(
result.value,
evidenceInput,
admitRecovery,
);
if (!admitted.ok) {
retained.error.retainSourcePublication({
kind: "omitted",
reason:
admitted.error._tag === "AnalysisResourceConstraintError"
? "insufficient-authentication-headroom"
: "recovery-input-invalid",
source_closure: "not-established",
});
return retained;
}
const evidence = createWorkflowEvidence({
...evidenceInput,
result: result.value.result,
upstreamLimitations: result.value.limitations,
...(result.value.evidenceLinks === undefined
? {}
: { upstreamEvidenceLinks: result.value.evidenceLinks }),
});
preserveCompletedSourceEvidence(
retained.error,
evidence,
evidenceForAnalysis,
);
}
return retained;
};
/** Retain a stopped workflow's admitted observation as partial workflow Evidence on its failure. */
export const retainPartialWorkflowEvidence = (
outcome: ComposedWorkflowResult,
@@ -246,8 +383,30 @@ export const retainPartialWorkflowEvidence = (
Parameters<typeof createWorkflowEvidence>[0],
"target" | "operation" | "parameters" | "upstreamProfile"
>,
admitRecovery: RecoveryAdmission = (input, operation, additional) =>
admitInvestigationAuthentication(input, operation, {}, additional),
): void => {
if (outcome.ok || outcome.partial === undefined) return;
if (
outcome.ok ||
outcome.partial === undefined ||
outcome.error.partialObservation !== undefined
)
return;
const admitted = admitWorkflowRecovery(
outcome.partial,
context,
admitRecovery,
);
if (!admitted.ok) {
outcome.error.retainPartialObservation({
kind: "omitted-observation",
reason:
admitted.error._tag === "AnalysisResourceConstraintError"
? "insufficient-authentication-headroom"
: "recovery-input-invalid",
});
return;
}
const evidence = createWorkflowEvidence({
...context,
result: outcome.partial.result,
@@ -4,7 +4,10 @@ import {
analysisViewResultSchema,
inspectAnalysisViewInputSchema,
} from "../../domain/analysisView/analysisView.js";
import { projectNativeLoadImageView } from "../../domain/analysisView/nativeLoadImageView.js";
import {
RELOCATION_SYMBOL_UNRECORDED,
projectNativeLoadImageView,
} from "../../domain/analysisView/nativeLoadImageView.js";
import type { NativeLoadImage } from "../../domain/native/nativeLoadImage.js";
import {
nativeDosLoadImageFixture,
@@ -80,7 +83,7 @@ it("distinguishes recorded, absent, and historical unrecorded relocation symbols
{ record: { symbol: "external_symbol" } },
{ record: { symbol: null } },
]);
expect(current.unknowns.join(" ")).not.toContain("symbol was not recorded");
expect(current.unknowns).not.toContain(RELOCATION_SYMBOL_UNRECORDED);
report.observations.relocations[1]!.symbol = "";
const emptyName = await view(report, request);
@@ -88,15 +91,15 @@ it("distinguishes recorded, absent, and historical unrecorded relocation symbols
if (emptyName.items.collection !== "relocations")
throw new Error("Expected relocations");
expect(emptyName.items.rows?.[1]?.record).toMatchObject({ symbol: "" });
expect(emptyName.unknowns.join(" ")).not.toContain("symbol was not recorded");
expect(emptyName.unknowns).not.toContain(RELOCATION_SYMBOL_UNRECORDED);
delete report.observations.relocations[0]!.symbol;
const historical = await view(report, request);
if (historical.kind !== "native-load-image") throw new Error("Expected page");
expect(historical.items.rows?.[0]?.record).not.toHaveProperty("symbol");
expect(historical.unknowns.join(" ")).toContain("symbol was not recorded");
expect(historical.unknowns).toContain(RELOCATION_SYMBOL_UNRECORDED);
const omitted = await view(report, { ...request, fields: ["address"] });
expect(omitted.unknowns.join(" ")).not.toContain("symbol was not recorded");
expect(omitted.unknowns).not.toContain(RELOCATION_SYMBOL_UNRECORDED);
});
it("summarizes measured ELF facts without upgrading unsupported verification", async () => {
+72 -11
View File
@@ -1,3 +1,5 @@
import { bindSessionEvidenceExecution } from "./SessionEvidenceReuse.js";
import { admitCompletedAnalysis } from "../investigation/admitCompletedAnalysis.js";
import { randomUUID } from "node:crypto";
import {
writeEvidenceBundle,
@@ -50,6 +52,7 @@ import {
} from "./SessionProviderPolicy.js";
import { InvestigationRecords } from "../investigation/InvestigationRecords.js";
import { recordEvidenceSources } from "../investigation/recordEvidenceSources.js";
import { recoverProviderSourceFailure } from "../investigation/sourcePublicationRecovery.js";
import { BinarySessionRecords } from "./BinarySessionRecords.js";
import { binarySessionStatus } from "./BinarySessionStatus.js";
import { SessionRuntimeAvailability } from "./SessionRuntimeAvailability.js";
@@ -1375,9 +1378,34 @@ export class BinarySession
name: Parameters<AnalysisOperationPort["execute"]>[0],
arguments_: Readonly<Record<string, JsonValue>>,
options?: ExecutionOptions,
): Promise<Result<AnalysisExecution, AnalysisError>> {
return this.#trackedExecute(name, arguments_, options);
}
/** Preserve the one-shot caller's Evidence identity if source retention fails. */
executeForDirectObservation(
name: Parameters<AnalysisOperationPort["execute"]>[0],
arguments_: Readonly<Record<string, JsonValue>>,
evidenceParameters: Readonly<Record<string, JsonValue>>,
options?: ExecutionOptions,
): Promise<Result<AnalysisExecution, AnalysisError>> {
return this.#trackedExecute(name, arguments_, options, evidenceParameters);
}
#trackedExecute(
name: Parameters<AnalysisOperationPort["execute"]>[0],
arguments_: Readonly<Record<string, JsonValue>>,
options?: ExecutionOptions,
recoveryParameters?: Readonly<Record<string, JsonValue>>,
): Promise<Result<AnalysisExecution, AnalysisError>> {
const generation = this.#transitionGeneration;
const call = this.#execute(name, arguments_, options, this.#transition);
const call = this.#execute(
name,
arguments_,
options,
this.#transition,
recoveryParameters,
);
this.#calls.set(call, generation);
return call.finally(() => this.#calls.delete(call));
}
@@ -1449,6 +1477,7 @@ export class BinarySession
arguments_: Readonly<Record<string, JsonValue>>,
options: ExecutionOptions | undefined,
transition: Promise<void>,
recoveryParameters?: Readonly<Record<string, JsonValue>>,
): Promise<Result<AnalysisExecution, AnalysisError>> {
const transitioned = await this.#waitForTransition(
name,
@@ -1535,7 +1564,24 @@ export class BinarySession
(source) => this.recordEvidence(source),
execution.value.sourceEvidence,
);
if (!sourcesRecorded.ok) return sourcesRecorded;
if (!sourcesRecorded.ok) {
recoverProviderSourceFailure({
error: sourcesRecorded.error,
operation: name,
parameters: recoveryParameters ?? arguments_,
execution: {
...execution.value,
sourceEvidence: execution.value.sourceEvidence,
subject:
execution.value.subject === undefined
? active.target
: execution.value.subject,
},
admit: this.admitRecoveryObservation.bind(this),
evidenceForAnalysis: this.evidenceForAnalysis.bind(this),
});
return sourcesRecorded;
}
}
if (
execution.ok &&
@@ -1547,22 +1593,37 @@ export class BinarySession
) &&
profile !== undefined
) {
const evidenceExecution = {
...execution.value,
analysisProfile: profile,
subject:
execution.value.subject === undefined
? active.target
: execution.value.subject,
};
const admitted = admitCompletedAnalysis(
name,
arguments_,
evidenceExecution,
this.admitRecoveryObservation.bind(this),
);
if (!admitted.ok) return admitted;
const evidence = createAnalysisExecutionEvidence(
name,
arguments_,
{
...execution.value,
analysisProfile: profile,
subject:
execution.value.subject === undefined
? active.target
: execution.value.subject,
},
evidenceExecution,
"observed",
);
const recorded = this.recordEvidence(evidence);
if (!recorded.ok) return recorded;
const retained = { ...execution.value, evidenceId: evidence.evidence_id };
const authenticated = this.evidenceForAnalysis(evidence.evidence_id);
const retained =
authenticated === undefined
? { ...execution.value, evidenceId: evidence.evidence_id }
: bindSessionEvidenceExecution(
{ ...execution.value, evidenceId: evidence.evidence_id },
authenticated,
);
if (cacheable && execution.value.cachePolicy !== "live")
this.recordSnapshot({
target: active.target,
@@ -1,5 +1,6 @@
import type { EspFlashSelection } from "../../domain/firmware/espFlashLocation.js";
import type { InvestigationRecordPort } from "../investigation/InvestigationRecordPort.js";
import type { RecoveryAdmission } from "../investigation/InvestigationRetention.js";
import type { ResetInvestigationReceipt } from "../../domain/investigationLifecycle.js";
import type { ExecutableFormatHint } from "../../domain/dosCom.js";
import type {
@@ -57,6 +58,7 @@ export type BinaryOpenResult =
/** Target lifecycle used by CLI and MCP without exposing a concrete provider. */
export interface BinarySessionPort
extends AnalysisOperationPort, InvestigationRecordPort {
readonly admitRecoveryObservation: RecoveryAdmission;
/** Execute only against a positively ready, already-created provider client. */
executeIfProviderReady(
operation: AnalysisOperation,
@@ -126,6 +126,17 @@ export abstract class BinarySessionRecords {
return recorded;
}
admitRecoveryObservation(
input: unknown,
operation: string,
additional: {
readonly valueBytes: number;
readonly serializedUtf16Bytes: number;
},
): Result<void, AnalysisError> {
return this.#records.admitRecoveryObservation(input, operation, additional);
}
hasEvidence(evidenceId: string): boolean {
return this.#records.hasEvidence(evidenceId);
}
@@ -0,0 +1,70 @@
import { basename } from "node:path";
import type { AnalysisExecution } from "../AnalysisProvider.js";
import type { Evidence } from "../../domain/evidence.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import { equalJsonText } from "../../domain/jsonSerialization.js";
// Proof belongs to the exact object issued by the session. A wrapper that
// copies or transforms the observation cannot carry it forward.
const issued = new WeakMap<AnalysisExecution, Evidence>();
export const bindSessionEvidenceExecution = (
execution: AnalysisExecution,
evidence: Evidence,
): AnalysisExecution => {
if (
(execution.sourceEvidence !== undefined &&
execution.sourceEvidence.length > 0) ||
evidence.caller_input_path !== undefined
)
return execution;
// Semantic IDs intentionally omit display paths and names. A duplicate in
// the ledger can therefore refer to the same bytes at a different path.
const selected = execution.subject;
if (
selected == null
? evidence.subject !== null
: evidence.subject === null ||
evidence.subject.local_path !== selected.path ||
evidence.subject.name !== (basename(selected.path) || "artifact")
)
return execution;
const subject =
execution.subject === undefined || execution.subject === null
? execution.subject
: Object.freeze({ ...execution.subject });
const result = Object.freeze({
...execution,
result: evidence.normalized_result,
rawResult: evidence.raw_result,
provider: evidence.provider,
...(evidence.analysis_profile === null
? {}
: { analysisProfile: evidence.analysis_profile }),
limitations: evidence.limitations,
locations: evidence.locations,
...(evidence.relationship_coverage === null
? {}
: { relationshipCoverage: evidence.relationship_coverage }),
...(subject === undefined ? {} : { subject }),
});
issued.set(result, evidence);
return result;
};
export const sessionEvidenceForExecution = (
execution: AnalysisExecution,
operation: string,
parameters: Readonly<Record<string, JsonValue>>,
lookup: (id: string) => Evidence | undefined,
): Evidence | undefined => {
const evidence = issued.get(execution);
if (
evidence === undefined ||
evidence.operation !== operation ||
lookup(evidence.evidence_id) !== evidence ||
!equalJsonText(evidence.parameters, parameters)
)
return undefined;
return evidence;
};
@@ -9,7 +9,10 @@ import {
validateResidualUnknownAddition,
type EvidenceBundle,
} from "../../domain/evidenceBundle.js";
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
import {
AnalysisInputError,
AnalysisResourceConstraintError,
} from "../../domain/analysisErrorCore.js";
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
import { UnknownRegistryError } from "../../domain/unknownRegistryError.js";
import {
@@ -17,6 +20,7 @@ import {
evidenceDisplayMetadataAgree,
type Evidence,
} from "../../domain/evidence.js";
import { freezeJsonSnapshot } from "../../domain/immutableJson.js";
import { safeParseImmutableEvidence } from "../../domain/evidenceValidation.js";
import {
createResidualUnknown,
@@ -111,7 +115,8 @@ export class EvidenceLedger {
"record_evidence",
new Map([[evidence.evidence_id, evidence]]),
);
if (!admitted.ok) return admitted;
if (!admitted.ok)
return err(this.#exposeRefusedEvidence(admitted.error, evidence));
this.#records.set(evidence.evidence_id, evidence);
this.#commitAccounting(admitted.value);
return ok("added");
@@ -366,14 +371,38 @@ export class EvidenceLedger {
if (creationUnknown.revision_digest !== unknown.revision_digest)
return err(new UnknownRegistryError("already-exists"));
const checked = this.#appendIncremental([evidence]);
if (!checked.ok) return checked;
if (!checked.ok)
return err(this.#exposeRefusedEvidence(checked.error, evidence));
return ok(null);
}
const checked = this.#appendIncremental([evidence, mutation], unknown);
if (!checked.ok) return checked;
if (!checked.ok)
return err(this.#exposeRefusedEvidence(checked.error, evidence));
return ok(structuredClone(unknown));
}
/** Report authenticated completed Evidence without inferring membership from this attempt. */
#exposeRefusedEvidence(
error: AnalysisError,
evidence: Evidence,
): AnalysisError {
if (
!(error instanceof AnalysisResourceConstraintError) ||
error.resource !== "capacity"
)
return error;
return error.retainPartialObservation(
freezeJsonSnapshot(
this.#records.has(evidence.evidence_id)
? {
kind: "retained-evidence" as const,
evidence_id: evidence.evidence_id,
}
: { kind: "unretained-evidence" as const, evidence },
),
);
}
/** Apply one approved full-state update using optimistic revision matching. */
updateUnknown(
input: UpdateUnknownInput,
@@ -96,7 +96,9 @@ describe("investigation record ownership", () => {
...unknownInput("Is the missing Evidence present?"),
supporting_evidence_ids: [`ev_${"f".repeat(64)}`],
};
expect(records.recordEvidenceWithUnknown(evidence, invalid).ok).toBe(false);
const refused = records.recordEvidenceWithUnknown(evidence, invalid);
if (refused.ok) throw new Error("Expected invalid Unknown references");
expect(refused.error.partialObservation).toBeUndefined();
expect(records.hasEvidence(evidence.evidence_id)).toBe(false);
expect(records.exportEvidenceBundle()).toEqual(createEvidenceBundle([]));
});
@@ -48,6 +48,23 @@ export class InvestigationRecords
return this.#evidence.statistics();
}
/** Check live headroom before constructing a recovery representation. */
admitRecoveryObservation(
input: unknown,
operation: string,
additional: {
readonly valueBytes: number;
readonly serializedUtf16Bytes: number;
},
): Result<void, AnalysisError> {
return admitInvestigationAuthentication(
input,
operation,
this.retentionPolicy,
additional,
);
}
/** Record immutable Evidence idempotently; reject conflicting content. */
recordEvidence(
evidence: Evidence,
@@ -11,7 +11,13 @@ import {
updateUnknownInputSchema,
type ResidualUnknown,
} from "../../domain/residualUnknown.js";
import { createAnalysisExecutionEvidence } from "../../domain/analysisObservation.js";
import { estimateJsonStorage } from "../JsonAuthenticationHeadroom.js";
import { ok } from "../../domain/result.js";
import { admitCompletedAnalysis } from "./admitCompletedAnalysis.js";
import { recordEvidenceSources } from "./recordEvidenceSources.js";
import { InvestigationRecords } from "./InvestigationRecords.js";
import { isImmutableJsonSnapshot } from "../../domain/immutableJson.js";
const provider = { id: "fixture", name: "Retention fixture", version: "1" };
const observe = (index: number, path = "/fixture") =>
@@ -57,6 +63,39 @@ const recordCost = (evidence: Evidence): number => {
return records.statistics().retainedRepresentationBytes;
};
it("returns only authenticated Evidence when a completed record exceeds retention capacity", () => {
const evidence = observe(7);
const records = new InvestigationRecords({ maximumRepresentationBytes: 0 });
const refused = records.recordEvidence(evidence);
if (refused.ok) throw new Error("Expected retention refusal");
expect(refused.error._tag).toBe("AnalysisResourceConstraintError");
expect(refused.error.partialObservation).toEqual({
kind: "unretained-evidence",
evidence,
});
expect(isImmutableJsonSnapshot(refused.error.partialObservation)).toBe(true);
expect(records.hasEvidence(evidence.evidence_id)).toBe(false);
const invalid = records.recordEvidence({
...evidence,
normalized_result: "tampered",
});
if (invalid.ok) throw new Error("Expected integrity refusal");
expect(invalid.error._tag).toBe("EvidenceIntegrityError");
expect(invalid.error.partialObservation).toBeUndefined();
const mutable = createEvidence(undefined, provider, {
operation: "health",
parameters: {},
result: { value: 1 },
});
const noHeadroom = new InvestigationRecords({
availableAuthenticationBytes: () => 0,
}).recordEvidence(mutable);
if (noHeadroom.ok)
throw new Error("Expected authentication headroom refusal");
expect(noHeadroom.error._tag).toBe("AnalysisResourceConstraintError");
expect(noHeadroom.error.partialObservation).toBeUndefined();
});
it("admits exact capacity, rejects cumulative growth atomically, and releases accounting on clear", () => {
const first = observe(1);
const second = observe(2);
@@ -296,3 +335,64 @@ it("reads statistics without asking either bundle-export path to copy retained f
expect(detached).not.toHaveBeenCalled();
expect(borrowed).not.toHaveBeenCalled();
});
it("preserves the completed observation's path when a retained semantic ID has different display metadata", () => {
const earlier = observe(7, "/original/file");
const current = observe(7, "/selected/file");
expect(current.evidence_id).toBe(earlier.evidence_id);
const records = new InvestigationRecords({
maximumRepresentationBytes: recordCost(earlier),
});
expect(records.recordEvidence(earlier).ok).toBe(true);
const result = recordEvidenceSources(
records.recordEvidence.bind(records),
[observe(8)],
{
evidence: current,
evidenceForAnalysis: records.evidenceForAnalysis.bind(records),
},
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected source publication failure");
expect(result.error.sourcePublicationRecovery?.completed).toMatchObject({
kind: "unretained-evidence",
evidence: {
evidence_id: current.evidence_id,
subject: { local_path: "/selected/file" },
},
});
});
it("accounts for the generated subject limitation before admitting a subjectless observation", () => {
const observation = {
provider,
result: { value: 1 },
rawResult: null,
limitations: ["source limit"],
locations: [],
subject: null,
} as const;
let admitted: ReturnType<typeof estimateJsonStorage> | undefined;
const result = admitCompletedAnalysis(
"health",
{},
observation,
(input, _operation, additional) => {
const estimate = estimateJsonStorage(input);
admitted = {
valueBytes: estimate.valueBytes + additional.valueBytes,
serializedUtf16Bytes:
estimate.serializedUtf16Bytes + additional.serializedUtf16Bytes,
};
return ok(undefined);
},
);
expect(result.ok).toBe(true);
const actual = estimateJsonStorage(
createAnalysisExecutionEvidence("health", {}, observation, "observed"),
);
expect(admitted?.valueBytes).toBeGreaterThanOrEqual(actual.valueBytes);
expect(admitted?.serializedUtf16Bytes).toBeGreaterThanOrEqual(
actual.serializedUtf16Bytes,
);
});
@@ -16,6 +16,16 @@ export interface InvestigationRetentionPolicy {
readonly availableAuthenticationBytes?: () => number;
}
/** Owner admission used before materializing a completed recovery record. */
export type RecoveryAdmission = (
input: unknown,
operation: string,
additional: {
readonly valueBytes: number;
readonly serializedUtf16Bytes: number;
},
) => Result<void, AnalysisError>;
/**
* Reserve one retained representation, two authentication/canonical working
* copies and one detached export/control representation from current V8 room.
@@ -39,6 +49,10 @@ export const admitInvestigationAuthentication = (
input: unknown,
operation: string,
policy: InvestigationRetentionPolicy,
additional?: {
readonly valueBytes: number;
readonly serializedUtf16Bytes: number;
},
): Result<void, AnalysisError> => {
const issue = jsonValueValidationIssue(input);
if (issue !== undefined)
@@ -52,6 +66,7 @@ export const admitInvestigationAuthentication = (
path: "investigation",
description:
"Insufficient live heap headroom to authenticate investigation records",
...(additional === undefined ? {} : { additional }),
...(policy.availableAuthenticationBytes === undefined
? {}
: { availableBytes: policy.availableAuthenticationBytes() }),
@@ -0,0 +1,55 @@
import { MISSING_EVIDENCE_SUBJECT_LIMITATION } from "../../domain/evidence.js";
import {
analysisExecutionEnvelopeForAdmission,
type AnalysisObservation,
} from "../../domain/analysisObservation.js";
import { estimateJsonArrayScalarEntry } from "../JsonAuthenticationHeadroom.js";
import {
admitInvestigationAuthentication,
type RecoveryAdmission,
} from "./InvestigationRetention.js";
/** Admit borrowed source facts before constructing their completed Evidence. */
export const admitCompletedAnalysis = (
operation: string,
parameters: unknown,
observation: AnalysisObservation,
admit: RecoveryAdmission = (input, name, additional) =>
admitInvestigationAuthentication(input, name, {}, additional),
) => {
const envelope = analysisExecutionEnvelopeForAdmission(
operation,
parameters,
observation,
"observed",
);
let valueBytes = 0;
let serializedUtf16Bytes = 0;
if (observation.subject == null) {
const entry = estimateJsonArrayScalarEntry(
MISSING_EVIDENCE_SUBJECT_LIMITATION,
observation.limitations.length > 0,
);
valueBytes += entry.valueBytes;
serializedUtf16Bytes += entry.serializedUtf16Bytes;
}
for (const [index, source] of (observation.sourceEvidence ?? []).entries()) {
const entry = estimateJsonArrayScalarEntry(source.evidence_id, index > 0);
valueBytes += entry.valueBytes;
serializedUtf16Bytes += entry.serializedUtf16Bytes;
}
const admitted = admit(envelope, operation, {
valueBytes,
serializedUtf16Bytes,
});
if (!admitted.ok)
admitted.error.retainSourcePublication({
kind: "omitted",
reason:
admitted.error._tag === "AnalysisResourceConstraintError"
? "insufficient-authentication-headroom"
: "recovery-input-invalid",
source_closure: "not-established",
});
return admitted;
};
@@ -1,5 +1,6 @@
import type { EvidenceWriter } from "./InvestigationRecordPort.js";
import type { Evidence } from "../../domain/evidence.js";
import { preserveCompletedSourceEvidence } from "./sourcePublicationRecovery.js";
import { err, ok, type Result } from "../../domain/result.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
@@ -54,10 +55,22 @@ export const retainAnalysisResultSources = <Value>(
export const recordEvidenceSources = (
recordEvidence: EvidenceWriter["recordEvidence"] | undefined,
sources: readonly Evidence[],
completed?: {
readonly evidence: Evidence;
readonly evidenceForAnalysis?: (id: string) => Evidence | undefined;
},
): Result<null, AnalysisError> => {
for (const source of sources) {
const recorded = recordEvidence?.(source);
if (recorded !== undefined && !recorded.ok) return recorded;
if (recorded !== undefined && !recorded.ok) {
if (completed !== undefined)
preserveCompletedSourceEvidence(
recorded.error,
completed.evidence,
completed.evidenceForAnalysis,
);
return recorded;
}
}
return ok(null);
};
@@ -0,0 +1,207 @@
import { ZodError } from "zod";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { createAnalysisExecutionEvidence } from "../../domain/analysisObservation.js";
import { isDigest } from "../../domain/digests.js";
import {
evidenceDisplayMetadataAgree,
evidenceSchema,
isImmutableEvidence,
type Evidence,
} from "../../domain/evidence.js";
import { safeParseImmutableEvidence } from "../../domain/evidenceValidation.js";
import { freezeJsonSnapshot } from "../../domain/immutableJson.js";
import { jsonObjectSchema, type JsonValue } from "../../domain/jsonValue.js";
import type { Result } from "../../domain/result.js";
import type { AnalysisExecution } from "../AnalysisProvider.js";
import {
estimateJsonArrayScalarEntry,
estimateJsonStorage,
} from "../JsonAuthenticationHeadroom.js";
import { admitInvestigationAuthentication } from "./InvestigationRetention.js";
/** Preserve a completed result only after authenticating it and checking its actual owner. */
export const preserveCompletedSourceEvidence = (
error: AnalysisError,
evidence: Evidence,
evidenceForAnalysis: ((id: string) => Evidence | undefined) | undefined,
): void => {
if (!isImmutableEvidence(evidence)) {
const headroom = admitInvestigationAuthentication(
evidence,
evidence.operation,
{},
);
if (!headroom.ok) {
error.retainSourcePublication({
kind: "omitted",
reason:
headroom.error._tag === "AnalysisResourceConstraintError"
? "insufficient-authentication-headroom"
: "primary-authentication-failed",
source_closure: "not-established",
});
return;
}
}
const authenticated = safeParseImmutableEvidence(evidence);
if (!authenticated.ok) {
error.retainSourcePublication({
kind: "omitted",
reason: "primary-authentication-failed",
source_closure: "not-established",
});
return;
}
const valid = authenticated.value;
const retained = evidenceForAnalysis?.(valid.evidence_id);
error.retainSourcePublication(
freezeJsonSnapshot(
evidenceForAnalysis === undefined
? {
kind: "inline-evidence" as const,
retention: "unknown" as const,
source_closure: "not-established" as const,
evidence: valid,
}
: retained !== undefined &&
evidenceDisplayMetadataAgree(retained, valid)
? {
kind: "retained-evidence" as const,
source_closure: "not-established" as const,
evidence_id: valid.evidence_id,
}
: {
kind: "unretained-evidence" as const,
source_closure: "not-established" as const,
evidence: valid,
},
),
);
};
/** Materialize a provider result only after source recording failed and admission succeeds. */
export const recoverProviderSourceFailure = (input: {
readonly error: AnalysisError;
readonly operation: string;
readonly parameters: Readonly<Record<string, JsonValue>>;
readonly execution: AnalysisExecution & {
readonly sourceEvidence: readonly Evidence[];
};
readonly admit: (
value: unknown,
operation: string,
additional: {
readonly valueBytes: number;
readonly serializedUtf16Bytes: number;
},
) => Result<void, AnalysisError>;
readonly evidenceForAnalysis: (id: string) => Evidence | undefined;
}): void => {
const {
error,
operation,
parameters,
execution,
admit,
evidenceForAnalysis,
} = input;
const { sourceEvidence, ...primaryExecution } = execution;
// The schema owns the envelope shape. Measure its generated fields and then
// account for source IDs incrementally, before allocating the ID array.
const shell: Record<string, JsonValue> = Object.fromEntries(
Object.keys(evidenceSchema.shape).map((key) => [key, null]),
);
shell.evidence_id = `ev_${"0".repeat(64)}`;
shell.predicate_type = "rea.analysis";
shell.confidence = "observed";
shell.authority = "shipped-artifact";
shell.evidence_links = [];
const base = estimateJsonStorage(shell);
let sourceValueBytes = base.valueBytes;
let sourceSerializedUtf16Bytes = base.serializedUtf16Bytes;
let validSourceIds = true;
for (const [index, source] of sourceEvidence.entries()) {
const id = recoverySourceId(source);
const entry = estimateJsonArrayScalarEntry(id, index > 0);
sourceValueBytes += entry.valueBytes;
sourceSerializedUtf16Bytes += entry.serializedUtf16Bytes;
validSourceIds &&=
id !== null &&
id.length === 67 &&
id.startsWith("ev_") &&
isDigest(id.slice(3));
}
const headroom = admit(
{
operation,
parameters,
execution: primaryExecution,
subject: execution.subject,
},
operation,
{
valueBytes: sourceValueBytes,
serializedUtf16Bytes: sourceSerializedUtf16Bytes,
},
);
if (!headroom.ok) {
error.retainSourcePublication({
kind: "omitted",
reason:
headroom.error._tag === "AnalysisResourceConstraintError"
? "insufficient-authentication-headroom"
: "recovery-input-invalid",
source_closure: "not-established",
});
return;
}
let evidence: Evidence | undefined;
if (validSourceIds) {
try {
evidence = createAnalysisExecutionEvidence(
operation,
parameters,
execution,
"observed",
{ immutable: true },
);
} catch (cause) {
if (!(cause instanceof ZodError)) throw cause;
}
}
if (evidence !== undefined) {
preserveCompletedSourceEvidence(error, evidence, evidenceForAnalysis);
return;
}
const observation = jsonObjectSchema.parse({
operation,
parameters,
result: execution.result,
raw_result: execution.rawResult,
provider: execution.provider,
analysis_profile: execution.analysisProfile ?? null,
subject: execution.subject ?? null,
limitations: execution.limitations,
locations: execution.locations,
relationship_coverage: execution.relationshipCoverage ?? null,
source_evidence_ids: sourceEvidence.map(recoverySourceId),
});
error.retainSourcePublication(
freezeJsonSnapshot({
kind: "unretained-observation" as const,
source_closure: "not-established" as const,
observation,
}),
);
};
/** Read only an ordinary data property; source payloads may have failed validation. */
const recoverySourceId = (source: unknown): string | null => {
if (typeof source !== "object" || source === null) return null;
const property = Object.getOwnPropertyDescriptor(source, "evidence_id");
return property !== undefined &&
"value" in property &&
typeof property.value === "string"
? property.value
: null;
};
+6 -8
View File
@@ -266,12 +266,10 @@ export const registerBinaryDiagnosticsCommands = (
// "0x10" or "1e2".
blockSize: cliDecimalField(
inspectByteEntropyInputSchema.shape.block_size,
).describe(
"Power-of-two block size from 256 to 65536 bytes (default 4096)",
),
).describe("Power-of-two block size from 256 to 65536 bytes"),
offset: cliDecimalField(
inspectByteEntropyInputSchema.shape.offset,
).describe("Window start as a decimal file byte offset (default 0)"),
).describe("Window start as a decimal file byte offset"),
length: cliDecimalField(
inspectByteEntropyInputSchema.shape.length,
).describe("Window length in bytes; omit to extend through EOF"),
@@ -429,16 +427,16 @@ export const registerBinaryDiagnosticsCommands = (
options: z.object({
maxFileBytes: cliDecimalField(
inspectDevicetreeInputSchema.shape.max_file_bytes,
).describe("Maximum FDT file size in bytes (default 4194304)"),
).describe("Maximum FDT file size in bytes"),
maxNodes: cliDecimalField(
inspectDevicetreeInputSchema.shape.max_nodes,
).describe("Maximum number of device-tree nodes (default 8192)"),
).describe("Maximum number of device-tree nodes"),
maxProperties: cliDecimalField(
inspectDevicetreeInputSchema.shape.max_properties,
).describe("Maximum number of device-tree properties (default 32768)"),
).describe("Maximum number of device-tree properties"),
maxDepth: cliDecimalField(
inspectDevicetreeInputSchema.shape.max_depth,
).describe("Maximum device-tree nesting depth (default 64)"),
).describe("Maximum device-tree nesting depth"),
...analysisEvidenceDeliveryOptions,
}),
run: ({ args, options }) =>
+10 -2
View File
@@ -36,6 +36,14 @@ const generic = <Code extends string>(code: Code) =>
/** The canonical invalid-input projection, also used for facet-local refusals. */
export const analysisInputErrorProjectionSchema = generic("invalid_request");
/** The canonical resource refusal, including its reported limits and recovery. */
export const analysisResourceErrorProjectionSchema = generic(
"resource_constraint",
);
/** Authentication or integrity refusal for a supplied Evidence record. */
export const evidenceIntegrityErrorProjectionSchema = generic(
"evidence_integrity_mismatch",
);
/** Stable discriminated schema shared by every CLI and MCP error surface. */
export const analysisErrorProjectionSchema = z.discriminatedUnion("code", [
@@ -47,7 +55,7 @@ export const analysisErrorProjectionSchema = z.discriminatedUnion("code", [
generic("unsupported_target"),
generic("provider_unavailable"),
generic("provider_timeout"),
generic("resource_constraint"),
analysisResourceErrorProjectionSchema,
generic("cancelled"),
z
.object({
@@ -90,7 +98,7 @@ export const analysisErrorProjectionSchema = z.discriminatedUnion("code", [
})
.strict(),
generic("artifact_operation_failed"),
generic("evidence_integrity_mismatch"),
evidenceIntegrityErrorProjectionSchema,
generic("truncated"),
generic("process_capture_failed"),
generic("cleanup_incomplete"),
+1 -1
View File
@@ -187,7 +187,7 @@ export const SESSION_TOOL_CONTRACTS = [
{
...session(
"inspect_address_context",
"Inspect one explicit reproducible address for its analyzed name, containing procedure, regular and inline comments, and matching bookmarks. Each unsupported facet returns a typed unavailable outcome. A facet rejected for invalid input retains its canonical error and issues alongside any successful facets. Use xrefs, assembly, or pseudocode for those deeper views.",
"Inspect one explicit reproducible address for its analyzed name, containing procedure, regular and inline comments, and matching bookmarks. Each unsupported facet returns a typed unavailable outcome. A facet rejected for invalid input, source Evidence integrity, or a resource constraint retains its canonical error and recovery alongside successful facets. Use xrefs, assembly, or pseudocode for those deeper views.",
addressContextInputSchema,
),
requiresActiveTarget: true,
+9 -2
View File
@@ -101,6 +101,8 @@ import { decompileDirectoryManifestSchema } from "./decompileDirectory.js";
import {
analysisErrorProjectionSchema,
analysisInputErrorProjectionSchema,
analysisResourceErrorProjectionSchema,
evidenceIntegrityErrorProjectionSchema,
} from "./errorSchemas.js";
import { relationshipCoverageSchema } from "../domain/relationshipCoverage.js";
import { nativeDispatchMetadataResultSchema } from "../domain/native/objcSwiftMetadata.js";
@@ -122,13 +124,18 @@ import {
import { processCaptureSchema } from "../domain/process/processCapture.js";
import { processScenarioPlanSchema } from "../domain/process/processScenario.js";
import { electronFuseReportSchema } from "../domain/electron/electronFuses.js";
const contextFacetErrorSchema = z.discriminatedUnion("code", [
analysisInputErrorProjectionSchema,
analysisResourceErrorProjectionSchema,
evidenceIntegrityErrorProjectionSchema,
]);
const contextFacetSchema = z.discriminatedUnion("state", [
z.object({ state: z.literal("available"), value: jsonValueSchema }),
z.object({
state: z.literal("unavailable"),
reason: z.string(),
remediation: z.string(),
error: analysisInputErrorProjectionSchema.optional(),
error: contextFacetErrorSchema.optional(),
}),
]);
const bookmarkFacetSchema = z.discriminatedUnion("state", [
@@ -140,7 +147,7 @@ const bookmarkFacetSchema = z.discriminatedUnion("state", [
state: z.literal("unavailable"),
reason: z.string(),
remediation: z.string(),
error: analysisInputErrorProjectionSchema.optional(),
error: contextFacetErrorSchema.optional(),
}),
]);
+70
View File
@@ -115,6 +115,43 @@ export interface AnalysisCleanupResourceOwner {
readonly cleanupResources: readonly string[] | undefined;
}
/** Result completed before its source Evidence could be published. */
export type SourcePublicationCompletion =
| {
readonly kind: "unretained-evidence";
readonly source_closure: "not-established";
readonly evidence: Evidence;
}
| {
readonly kind: "retained-evidence";
readonly source_closure: "not-established";
readonly evidence_id: string;
}
| {
readonly kind: "unretained-observation";
readonly source_closure: "not-established";
readonly observation: Readonly<Record<string, JsonValue>>;
}
| {
readonly kind: "inline-evidence";
readonly retention: "one-shot-session" | "unknown";
readonly source_closure: "not-established";
readonly evidence: Evidence;
}
| {
readonly kind: "omitted";
readonly source_closure: "not-established";
readonly reason:
| "primary-authentication-failed"
| "insufficient-authentication-headroom"
| "recovery-input-invalid";
};
export interface SourcePublicationRecovery {
readonly source: AnalysisPartialObservation | undefined;
readonly completed: SourcePublicationCompletion;
}
/** Base class for expected analysis, provider, and session failures. */
export abstract class AnalysisError extends Error {
limit: Readonly<AnalysisSettingLimit> | undefined;
@@ -175,8 +212,19 @@ export abstract class AnalysisError extends Error {
readonly userCategory: "cancelled" | undefined = undefined;
readonly executionFailure: string | undefined = undefined;
partialObservation: AnalysisPartialObservation | undefined;
sourcePublicationRecovery: SourcePublicationRecovery | undefined;
workflowUnknownPublication: WorkflowUnknownPublication | undefined;
/** Keep the failed source and completed result with their error owner. */
retainSourcePublication(completed: SourcePublicationCompletion): this {
this.sourcePublicationRecovery = {
source: this.partialObservation,
completed,
};
this.partialObservation = undefined;
return this;
}
get cleanupPrimary(): AnalysisError | undefined {
return this.#cleanupPrimary;
}
@@ -342,6 +390,20 @@ export const primaryAnalysisError = (error: AnalysisError): AnalysisError => {
while (error.cleanupPrimary !== undefined) error = error.cleanupPrimary;
return error;
};
/** Find the completed publication outcome owned anywhere in a cleanup chain. */
export const sourcePublicationRecoveryOf = (
error: AnalysisError,
): SourcePublicationRecovery | undefined => {
for (
let owner: AnalysisError | undefined = error;
owner !== undefined;
owner = owner.cleanupPrimary
)
if (owner.sourcePublicationRecovery !== undefined)
return owner.sourcePublicationRecovery;
return undefined;
};
import type {
PartialProcessCaptureObservation,
ProcessCaptureCleanupReport,
@@ -402,6 +464,14 @@ import type { WebArtifactAcquisition } from "./webArtifactAcquisition.js";
/** Provider-neutral evidence collected before a typed analysis failure. */
export type AnalysisPartialObservation =
| {
readonly kind: "omitted-observation";
readonly reason:
| "insufficient-authentication-headroom"
| "recovery-input-invalid";
}
| { readonly kind: "unretained-evidence"; readonly evidence: Evidence }
| { readonly kind: "retained-evidence"; readonly evidence_id: string }
| GraphqlContract
| PlaywrightTrace
| SavedAnalysisSnapshotPartialObservation
+34 -4
View File
@@ -195,12 +195,15 @@ it.each([
const projected = projectAnalysisError(composed);
expect(projected).toMatchObject(original);
expect(projected.details?.cleanup_history).toMatchObject({
primary_error:
primary.capturedOutput === undefined
? original
: projectAnalysisErrorWithoutCapturedOutput(primary),
primary_error: { code: original.code, message: original.message },
cleanup_error: { code: "cleanup_incomplete" },
});
expect(projected.details?.cleanup_history).not.toHaveProperty(
"primary_error.details.partial_observation",
);
expect(projected.details?.cleanup_history).not.toHaveProperty(
"primary_error.details.captured_output",
);
expect(projected.details).not.toHaveProperty("cleanup", "incomplete");
expect(projected.details).not.toHaveProperty("resources");
expect(analysisErrorCategory(composed)).toBe(original.category);
@@ -251,6 +254,33 @@ it.each(["inner", "outer"] as const)(
},
);
it("preserves recovery and secondary failures owned by an intermediate cleanup", () => {
const primary = new AnalysisInputError("inspect");
const inner = analysisErrorWithCleanupFailure(
primary,
new ProviderCleanupError("fixture", ["input"], {}),
);
inner.retainSourcePublication({
kind: "omitted",
reason: "primary-authentication-failed",
source_closure: "not-established",
});
inner.retainSecondaryFailure(new AnalysisInputError("source"));
const outer = analysisErrorWithCleanupFailure(
inner,
new ProviderCleanupError("fixture", ["workspace"], {}),
);
expect(projectAnalysisError(outer)).toMatchObject({
details: {
completed_observation_omitted: {
reason: "primary-authentication-failed",
source_closure: "not-established",
},
secondary_failures: [{ code: "invalid_request" }],
},
});
});
it("does not infer cleanup success when a composition has no live owner binding", () => {
const primary = new AnalysisInputError("inspect");
const cleanup = new ProviderCleanupError("fixture", ["input"], {});
+1
View File
@@ -44,6 +44,7 @@ export const analysisErrorWithCleanupFailure = (
...childFailureDiagnostics({
primary_error: {
error: primary,
omitRecovery: true,
...(primary.capturedOutput === undefined
? {}
: { capturedOutput: "omit" as const }),
+4
View File
@@ -47,6 +47,10 @@ import {
} from "./analysisErrorBase.js";
import type { AnalysisErrorProjection } from "./analysisErrorProjection.js";
/** Explain how to preserve an observation the session could not retain. */
export const UNRETAINED_EVIDENCE_EXPORT_GUIDANCE =
"Unretained Evidence is not included in the exported investigation; if present in this response, save it separately.";
/** File export when one MCP frame cannot carry the retained session bundle. */
export const MCP_TRANSPORT_EVIDENCE_EXPORT =
"Export complete retained session evidence through export_evidence_bundle to a caller-selected path, or use complete CLI JSON output. The connection remains usable.";
@@ -1582,6 +1582,24 @@ describe("analysis error projection: composed failures for a caller lifetime", (
});
});
it("keeps caller lifetime for a secondary failure embedded in child diagnostics", () => {
const primary = new AnalysisInputError("read_bytes", undefined, []);
const secondary = new AnalysisTimeoutError("search_strings", 250, {
limit: analysisSettingLimit("regex.matching_deadline_ms", 250, "setting"),
continuingCallerGuidance: continuingOnly,
});
primary.retainSecondaryFailure(secondary);
const history = childFailureDiagnostics({
primary_error: { error: primary },
});
expect(history.diagnostics?.primary_error).toEqual(
projectAnalysisError(primary, "continuing"),
);
expect(history.invocationDiagnostics?.primary_error).toEqual(
projectAnalysisError(primary, "invocation"),
);
});
it("records one lifetime form when no child depends on the caller", () => {
const composed = analysisErrorWithCleanupFailure(
new AnalysisInputError("read_bytes", undefined, [
+292 -27
View File
@@ -9,6 +9,7 @@ import { isImmutableJsonSnapshot } from "./immutableJson.js";
import {
analysisErrorCategory,
UNRETAINED_EVIDENCE_EXPORT_GUIDANCE,
analysisErrorRemediationAction,
analysisErrorUserMessage,
assertKnownAnalysisErrorTag,
@@ -50,6 +51,7 @@ import { providerRetryAction } from "./providerOperationHealth.js";
import {
primaryAnalysisError,
resolvedAnalysisError,
sourcePublicationRecoveryOf,
type AnalysisCallerLifetime,
type AnalysisError,
type AnalysisErrorOptions,
@@ -67,19 +69,195 @@ import { invocationSettingLimit } from "./analysisSettingDefinitions.js";
export const projectAnalysisError = (
error: AnalysisError,
callerLifetime: AnalysisCallerLifetime = "continuing",
): AnalysisErrorProjection => projectError(error, true, callerLifetime);
): AnalysisErrorProjection =>
projectError(
error,
{
capturedOutput: true,
partialObservation: true,
secondary: true,
},
callerLifetime,
);
/** Cleanup history references output retained by its enclosing error owner. */
export const projectAnalysisErrorWithoutCapturedOutput = (
error: AnalysisError,
callerLifetime: AnalysisCallerLifetime = "continuing",
): AnalysisErrorProjection => projectError(error, false, callerLifetime);
): AnalysisErrorProjection =>
projectError(
error,
{
capturedOutput: false,
partialObservation: true,
secondary: true,
},
callerLifetime,
);
/** Bounded fallback when the completed observation itself cannot be delivered. */
export const projectAnalysisErrorWithoutPartialObservation = (
error: AnalysisError,
callerLifetime: AnalysisCallerLifetime = "continuing",
): AnalysisErrorProjection =>
projectError(
error,
{
capturedOutput: true,
partialObservation: false,
secondary: true,
},
callerLifetime,
);
export interface UnretainedEvidenceCandidate {
readonly field: "partial_observation" | "completed_observation";
readonly secondaryPath: readonly number[];
readonly value: JsonValue;
}
/** Enumerate recoveries owned by this projection, including secondary failures. */
export function* unretainedEvidenceCandidates(
projected: AnalysisErrorProjection,
secondaryPath: readonly number[] = [],
): Generator<UnretainedEvidenceCandidate> {
for (const field of [
"partial_observation",
"completed_observation",
] as const) {
const value = projected.details?.[field];
if (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
(value.kind === "unretained-evidence" ||
value.kind === "unretained-observation" ||
value.kind === "inline-evidence")
)
yield { field, secondaryPath, value };
}
const secondary = projected.details?.secondary_failures;
if (!Array.isArray(secondary)) return;
for (const [index, failure] of secondary.entries()) {
if (
typeof failure !== "object" ||
failure === null ||
Array.isArray(failure)
)
continue;
const remediation = failure.remediation;
if (
typeof remediation !== "object" ||
remediation === null ||
Array.isArray(remediation) ||
typeof remediation.action !== "string"
)
continue;
yield* unretainedEvidenceCandidates(
failure as unknown as AnalysisErrorProjection,
[...secondaryPath, index],
);
}
}
/** Omit one explicitly unretained recovery when a complete reply cannot fit. */
export const omitUnretainedEvidenceFromError = (
projected: AnalysisErrorProjection,
limit: {
readonly constraint: "receive-buffer" | "string-length";
readonly bytesAtLeast: number;
},
candidate: UnretainedEvidenceCandidate,
): AnalysisErrorProjection | undefined => {
const { field, secondaryPath } = candidate;
if (secondaryPath.length > 0) {
const [index, ...rest] = secondaryPath;
const secondary = projected.details?.secondary_failures;
if (!Array.isArray(secondary) || index === undefined) return undefined;
const failure = secondary[index];
if (
typeof failure !== "object" ||
failure === null ||
Array.isArray(failure)
)
return undefined;
const omitted = omitUnretainedEvidenceFromError(
failure as unknown as AnalysisErrorProjection,
limit,
{ ...candidate, secondaryPath: rest },
);
if (omitted === undefined) return undefined;
return {
...projected,
details: {
...projected.details,
secondary_failures: secondary.map((item, i) =>
i === index ? omitted : item,
),
},
};
}
const partial = projected.details?.[field];
if (
typeof partial !== "object" ||
partial === null ||
Array.isArray(partial) ||
(partial.kind !== "unretained-evidence" &&
partial.kind !== "unretained-observation" &&
partial.kind !== "inline-evidence")
) {
return undefined;
}
const { [field]: _partial, ...details } = projected.details!;
const evidence = partial.evidence;
const evidenceId =
typeof evidence === "object" &&
evidence !== null &&
!Array.isArray(evidence)
? evidence.evidence_id
: undefined;
const retention =
partial.kind === "inline-evidence"
? typeof partial.retention === "string"
? partial.retention
: "unknown"
: "unretained";
return {
...projected,
remediation: {
action: `${projected.remediation.action} ${recoveryOmissionRemediation(retention)}`,
},
details: {
...details,
[`${field}_omitted`]: {
reason: "mcp-response-limit",
retention,
constraint: limit.constraint,
original_response_bytes_at_least: limit.bytesAtLeast,
...(typeof evidenceId === "string" ? { evidence_id: evidenceId } : {}),
...(field === "completed_observation"
? { source_closure: "not-established" }
: {}),
},
},
};
};
/** Explain the delivered omission without inferring ledger membership. */
export const recoveryOmissionRemediation = (retention: string): string =>
retention === "unretained"
? "The omitted observation was not retained and could not fit within MCP delivery limits."
: retention === "unknown"
? "The omitted observation's retention is unknown, and it could not fit within MCP delivery limits."
: "The observation could not fit within MCP delivery limits.";
/** A child failure embedded in a composed error's diagnostics. */
export interface AnalysisChildFailure {
readonly error: AnalysisError;
/** Omit output that the composing error already retains. */
readonly capturedOutput?: "omit";
/** Omit recovery facts retained by the composing error. */
readonly omitRecovery?: boolean;
}
/**
@@ -93,17 +271,26 @@ export const childFailureDiagnostics = (
): Pick<AnalysisErrorOptions, "diagnostics" | "invocationDiagnostics"> => {
const entries = Object.entries(children);
const project = (
{ error, capturedOutput }: AnalysisChildFailure,
{ error, capturedOutput, omitRecovery }: AnalysisChildFailure,
callerLifetime: AnalysisCallerLifetime,
) => projectError(error, capturedOutput !== "omit", callerLifetime);
) =>
projectError(
error,
{
capturedOutput: capturedOutput !== "omit",
partialObservation: omitRecovery !== true,
secondary: omitRecovery !== true,
},
callerLifetime,
);
const continuing = {
...diagnostics,
...Object.fromEntries(
entries.map(([key, child]) => [key, project(child, "continuing")]),
),
};
const varying = entries.filter(([, { error }]) =>
projectionDependsOnCallerLifetime(error),
const varying = entries.filter(([, { error, omitRecovery }]) =>
projectionDependsOnCallerLifetime(error, omitRecovery !== true),
);
return varying.length === 0
? { diagnostics: continuing }
@@ -118,26 +305,44 @@ export const childFailureDiagnostics = (
};
};
/** Every lifetime-dependent input the projection reads lies on this chain. */
const projectionDependsOnCallerLifetime = (error: AnalysisError): boolean => {
/** Check the primary chain and the secondary failures included in its projection. */
const projectionDependsOnCallerLifetime = (
error: AnalysisError,
includeSecondary: boolean,
): boolean => {
const chainDepends = (first: AnalysisError): boolean => {
for (
let node: AnalysisError | undefined = first;
node !== undefined;
node = node.cleanupPrimary
)
if (
node.limit !== undefined ||
node.continuingCallerGuidance !== undefined ||
node.invocationDiagnostics !== undefined ||
node.workflowUnknownPublication !== undefined
)
return true;
return false;
};
if (chainDepends(error)) return true;
if (!includeSecondary) return false;
for (
let node: AnalysisError | undefined = error;
node !== undefined;
node = node.cleanupPrimary
)
if (
node.limit !== undefined ||
node.continuingCallerGuidance !== undefined ||
node.invocationDiagnostics !== undefined ||
node.workflowUnknownPublication !== undefined
)
return true;
if (node.secondaryFailures.some(chainDepends)) return true;
return false;
};
const projectError = (
error: AnalysisError,
includeCapturedOutput: boolean,
options: {
readonly capturedOutput: boolean;
readonly partialObservation: boolean;
readonly secondary: boolean;
},
callerLifetime: AnalysisCallerLifetime,
): AnalysisErrorProjection => {
const composed = error;
@@ -152,8 +357,30 @@ const projectError = (
const cleanupHistory =
composed === error ? undefined : composed.diagnosticsFor(callerLifetime);
const capturedOutput = composed.capturedOutput ?? error.capturedOutput;
const publication = sourcePublicationRecoveryOf(composed);
const secondary = new Set<AnalysisError>();
for (
let owner: AnalysisError | undefined = composed;
owner !== undefined;
owner = owner.cleanupPrimary
) {
for (const failure of owner.secondaryFailures) secondary.add(failure);
}
const partialObservation =
composed.partialObservation ?? error.partialObservation;
composed.partialObservation ??
error.partialObservation ??
publication?.source;
const completedSourceObservation =
publication?.completed.kind === "omitted"
? undefined
: publication?.completed;
const completedSourceOmission =
publication?.completed.kind === "omitted"
? {
reason: publication.completed.reason,
source_closure: publication.completed.source_closure,
}
: undefined;
// A finalizer can add a report above the semantic primary, including while
// its cleanup is pending or beneath another cleanup composition.
let cleanupReport = composed.cleanupReport;
@@ -169,20 +396,20 @@ const projectError = (
const underlyingCode = underlyingErrorCode(primary);
const code = error.cleanupIncomplete ? "cleanup_incomplete" : underlyingCode;
const primaryDetails = errorDetails(primary);
const publication =
const unknownPublication =
composed.workflowUnknownPublication ?? primary.workflowUnknownPublication;
const details = {
...primaryDetails,
...(publication === undefined
...(unknownPublication === undefined
? {}
: {
workflow_unknown_publication: {
...publication,
...unknownPublication,
evidence_retention:
callerLifetime === "invocation" &&
publication.evidence_retention === "retained"
unknownPublication.evidence_retention === "retained"
? "invocation-only"
: publication.evidence_retention,
: unknownPublication.evidence_retention,
},
}),
...(primary.limit === undefined
@@ -193,6 +420,17 @@ const projectError = (
? { ...invocationSettingLimit(primary.limit) }
: { ...primary.limit },
}),
...(!options.secondary || secondary.size === 0
? {}
: {
secondary_failures: [...secondary].map((failure) =>
projectError(
failure,
{ ...options, secondary: false },
callerLifetime,
),
),
}),
...(cleanupReport === undefined
? {}
: { cleanup_report: jsonValueSchema.parse(cleanupReport) }),
@@ -218,20 +456,32 @@ const projectError = (
primaryDetails?.execution_failure ?? underlyingCode,
}
: {}),
...(!includeCapturedOutput || capturedOutput === undefined
...(!options.capturedOutput || capturedOutput === undefined
? {}
: {
captured_output: {
...capturedOutput,
},
}),
...(partialObservation === undefined
...(!options.partialObservation || partialObservation === undefined
? {}
: {
partial_observation: isImmutableJsonSnapshot(partialObservation)
? partialObservation
: jsonValueSchema.parse(partialObservation),
}),
...(!options.partialObservation || completedSourceObservation === undefined
? {}
: {
completed_observation: isImmutableJsonSnapshot(
completedSourceObservation,
)
? completedSourceObservation
: jsonValueSchema.parse(completedSourceObservation),
}),
...(!options.partialObservation || completedSourceOmission === undefined
? {}
: { completed_observation_omitted: completedSourceOmission }),
};
// The retained cleanup owner's retry precedes the failed operation's hint.
const cleanupRetry = error.cleanupRetry;
@@ -240,9 +490,24 @@ const projectError = (
? errorRecovery(error)
: cleanupRetryRecovery(cleanupRetry);
const remediation = analysisErrorRemediationAction(error, callerLifetime);
const includesUnretainedEvidence =
options.partialObservation &&
[partialObservation, completedSourceObservation].some(
(observation) =>
typeof observation === "object" &&
observation !== null &&
"kind" in observation &&
observation.kind === "unretained-evidence",
);
const retentionGuidance =
error instanceof AnalysisResourceConstraintError &&
error.resource === "capacity" &&
includesUnretainedEvidence
? ` ${UNRETAINED_EVIDENCE_EXPORT_GUIDANCE}`
: "";
const publicationRemediation =
callerLifetime === "continuing" &&
publication?.evidence_retention === "retained"
unknownPublication?.evidence_retention === "retained"
? " Use get_evidence_bundle with the Evidence ID in details.workflow_unknown_publication to retrieve the completed result; use list_unknowns to inspect recorded questions. Publication stopped at the reported index; later questions were not checked in this attempt."
: "";
return {
@@ -259,8 +524,8 @@ const projectError = (
remediation: {
action:
cleanupRetry === undefined
? `${remediation}${publicationRemediation}`
: `${remediation} ${cleanupRetryRemediation(cleanupRetry)}${publicationRemediation}`,
? `${remediation}${retentionGuidance}${publicationRemediation}`
: `${remediation}${retentionGuidance} ${cleanupRetryRemediation(cleanupRetry)}${publicationRemediation}`,
},
...(recovery === undefined ? {} : { recovery }),
...(Object.keys(details).length === 0 ? {} : { details }),
+43 -16
View File
@@ -1,5 +1,6 @@
import {
createEvidence,
evidenceEnvelopeForAdmission,
type Evidence,
type EvidenceCreationOptions,
type EvidenceLocation,
@@ -29,6 +30,41 @@ export interface AnalysisObservation {
readonly subject?: EvidenceSubjectTarget | null;
}
/** Build the single borrowed observation used for admission and commitment. */
const analysisExecutionEvidenceInput = (
operation: string,
input: unknown,
execution: AnalysisObservation,
confidence: "observed" | "derived",
) => ({
operation,
parameters: input,
result: execution.result,
rawResult: execution.rawResult,
limitations: execution.limitations,
...(execution.analysisProfile === undefined
? {}
: { analysisProfile: execution.analysisProfile }),
...(execution.relationshipCoverage === undefined
? {}
: { relationshipCoverage: execution.relationshipCoverage }),
locations: execution.locations,
confidence,
});
/** Borrow the Evidence envelope with links left empty for incremental admission accounting. */
export const analysisExecutionEnvelopeForAdmission = (
operation: string,
input: unknown,
execution: AnalysisObservation,
confidence: "observed" | "derived",
) =>
evidenceEnvelopeForAdmission(
execution.subject ?? undefined,
execution.provider,
analysisExecutionEvidenceInput(operation, input, execution, confidence),
);
/** Commit identical facts on success and when retaining a completed observation after cancellation. */
export const createAnalysisExecutionEvidence = (
operation: string,
@@ -38,26 +74,17 @@ export const createAnalysisExecutionEvidence = (
options?: EvidenceCreationOptions,
): Evidence =>
createEvidence(
execution.subject === undefined
? undefined
: (execution.subject ?? undefined),
execution.subject ?? undefined,
execution.provider,
{
operation,
parameters: input,
result: execution.result,
rawResult: execution.rawResult,
limitations: execution.limitations,
...(execution.analysisProfile === undefined
? {}
: { analysisProfile: execution.analysisProfile }),
...(execution.relationshipCoverage === undefined
? {}
: { relationshipCoverage: execution.relationshipCoverage }),
locations: execution.locations,
...analysisExecutionEvidenceInput(
operation,
input,
execution,
confidence,
),
evidenceLinks:
execution.sourceEvidence?.map(({ evidence_id }) => evidence_id) ?? [],
confidence,
},
options,
);
@@ -243,7 +243,9 @@ const summarizeBatch = (items: readonly BatchDecompileItem[]) => {
};
};
const batchUnknowns = (items: readonly BatchDecompileItem[]): string[] => {
export const batchUnknowns = (
items: readonly BatchDecompileItem[],
): string[] => {
const unknownIdentity = items.filter(
({ procedure }) => procedure.status === "unknown",
).length;
@@ -199,6 +199,10 @@ const selectRows = (source: readonly (Row | string)[], view: PageRequest) => {
};
/** Query the owner's parsed historical observations without reopening the target. */
/** A selected historical relocation row omitted its symbol field. */
export const RELOCATION_SYMBOL_UNRECORDED =
"The relocation symbol was not recorded for at least one selected row.";
export const projectNativeLoadImageView = (
parent: AnalysisViewParent,
admitted: AdmittedView,
@@ -300,10 +304,7 @@ export const projectNativeLoadImageView = (
return ok({
...shared,
unknowns: symbolUnrecorded
? [
...shared.unknowns,
"The relocation symbol was not recorded for at least one selected row.",
]
? [...shared.unknowns, RELOCATION_SYMBOL_UNRECORDED]
: shared.unknowns,
kind: "native-load-image" as const,
verification_status: report.status,
+1 -1
View File
@@ -51,7 +51,7 @@ const captureInputShape = z.strictObject({
.int()
.nonnegative()
.default(0)
.describe("Skip this many matching packets after filtering; default 0"),
.describe("Skip this many matching packets after filtering"),
match_limit: z
.number()
.int()
+2 -4
View File
@@ -5,9 +5,7 @@ export const inspectPacketFieldsInputSchema = z.strictObject({
kind: z
.enum(["protocols", "fields"])
.default("protocols")
.describe(
"Return protocol registrations or field definitions; defaults to protocols",
),
.describe("Return protocol registrations or field definitions"),
protocol: z
.string()
.min(1)
@@ -30,7 +28,7 @@ export const inspectPacketFieldsInputSchema = z.strictObject({
.nonnegative()
.max(Number.MAX_SAFE_INTEGER)
.default(0)
.describe("Number of matching records to skip; defaults to 0"),
.describe("Number of matching records to skip"),
limit: z
.number()
.int()
+18
View File
@@ -9,6 +9,7 @@ import {
} from "./binaryTargetTypes.js";
import {
createEvidence,
evidenceEnvelopeForAdmission,
evidenceSchema,
parseEvidence,
immutableEvidence,
@@ -895,3 +896,20 @@ it("diagnoses a previous explicit-null coverage commitment without admitting or
});
expect(parseEvidence(current)).toEqual(current);
});
it("borrows variable-size collections for admission", () => {
const limitations = ["one", "two"];
const locations: never[] = [];
const evidenceLinks = ["ev_" + "a".repeat(64)];
const envelope = evidenceEnvelopeForAdmission(TARGET, PROVIDER, {
operation: "borrow",
parameters: {},
result: {},
limitations,
locations,
evidenceLinks,
});
expect(envelope.limitations).toBe(limitations);
expect(envelope.locations).toBe(locations);
expect(envelope.evidence_links).toBe(evidenceLinks);
});
+30 -26
View File
@@ -35,6 +35,8 @@ import {
/** Predicate shared by ordinary provider analysis observations. */
export const ANALYSIS_EVIDENCE_PREDICATE = "rea.analysis";
export const MISSING_EVIDENCE_SUBJECT_LIMITATION =
"Artifact identity is unavailable for this observation.";
/** Provider identity schema shared by evidence-bearing persistence formats. */
export const providerSchema = z.strictObject({
@@ -454,7 +456,8 @@ export function* createImmutableEvidenceSteps(
});
}
const normalizeEvidenceObservation = (
/** Borrow input collections for admission; callers account for a missing-subject limitation separately. */
export const evidenceEnvelopeForAdmission = (
target: EvidenceSubjectTarget | BinaryTarget | undefined,
provider: EvidenceProvider,
observation: EvidenceObservationInput,
@@ -469,15 +472,9 @@ const normalizeEvidenceObservation = (
architecture: target.architecture ?? null,
local_path: target.path,
};
const semantic = {
subject:
subject === null
? null
: {
digest: subject.digest,
format: subject.format,
architecture: subject.architecture,
},
return {
evidence_id: `ev_${"0".repeat(64)}`,
subject,
provider: {
id: provider.id,
name: provider.name,
@@ -492,21 +489,32 @@ const normalizeEvidenceObservation = (
confidence: observation.confidence ?? "observed",
authority: observation.authority ?? "shipped-artifact",
environment: observation.environment ?? null,
limitations: [
...(target === undefined
? [
observation.subjectUnavailableReason ??
"Artifact identity is unavailable for this observation.",
]
: []),
...(observation.limitations ?? []),
],
limitations: observation.limitations ?? [],
...(observation.relationshipCoverage === undefined
? { relationship_coverage: null }
: { relationship_coverage: observation.relationshipCoverage }),
locations: [...(observation.locations ?? [])],
evidence_links: [...(observation.evidenceLinks ?? [])],
locations: observation.locations ?? [],
evidence_links: observation.evidenceLinks ?? [],
};
};
const normalizeEvidenceObservation = (
target: EvidenceSubjectTarget | BinaryTarget | undefined,
provider: EvidenceProvider,
observation: EvidenceObservationInput,
) => {
const borrowed = evidenceEnvelopeForAdmission(target, provider, observation);
const envelope =
target === undefined
? {
...borrowed,
limitations: [
observation.subjectUnavailableReason ??
MISSING_EVIDENCE_SUBJECT_LIMITATION,
...borrowed.limitations,
],
}
: borrowed;
const sharedResult =
typeof observation.result === "object" &&
observation.result !== null &&
@@ -514,11 +522,7 @@ const normalizeEvidenceObservation = (
const selectedSchema = sharedResult
? evidenceSchema.safeExtend({ normalized_result: immutableResultSchema })
: evidenceSchema;
const normalized = selectedSchema.parse({
...semantic,
evidence_id: `ev_${"0".repeat(64)}`,
subject,
});
const normalized = selectedSchema.parse(envelope);
// The envelope has already been parsed into an independent snapshot. Only
// its derived identifier changes here; parsing again clones the full payload
// and recomputes the same digest while the previous snapshot is still live.
+1 -3
View File
@@ -24,9 +24,7 @@ export const inspectSignalDecodersInputSchema = z.strictObject({
.nonnegative()
.max(Number.MAX_SAFE_INTEGER)
.default(0)
.describe(
"Number of matching decoders or help lines to skip; defaults to 0",
),
.describe("Number of matching decoders or help lines to skip"),
limit: z
.number()
.int()
+1 -1
View File
@@ -31,7 +31,7 @@ const signalInputShape = z.strictObject({
.int()
.nonnegative()
.default(0)
.describe("Skip this many literal LF-separated output lines; default 0"),
.describe("Skip this many literal LF-separated output lines"),
line_limit: z
.number()
.int()
+9 -2
View File
@@ -44,7 +44,10 @@ import {
AnalysisResourceConstraintError,
type AnalysisInputIssue,
} from "../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import {
projectAnalysisError,
projectAnalysisErrorWithoutPartialObservation,
} from "../domain/analysisErrorProjection.js";
import { projectInputIssueGroupsWithinBudget } from "../domain/inputIssueProjection.js";
import type { z } from "zod";
import { createEvidence } from "../domain/evidence.js";
@@ -936,7 +939,11 @@ class ErrorEvidenceTransport implements Transport {
}
: { retention: recorded === undefined ? "unavailable" : "failed" }),
...(recorded !== undefined && !recorded.ok
? { retention_error: projectAnalysisError(recorded.error) }
? {
retention_error: projectAnalysisErrorWithoutPartialObservation(
recorded.error,
),
}
: {}),
},
targetContent,
+86 -5
View File
@@ -7,13 +7,16 @@ import type { BinaryOpenResult } from "../application/binary/BinarySessionPort.j
import type { Evidence } from "../domain/evidence.js";
import { safeParseEvidence } from "../domain/evidenceValidation.js";
import {
omitUnretainedEvidenceFromError,
projectAnalysisError,
unretainedEvidenceCandidates,
type AnalysisErrorProjection,
} from "../domain/analysisErrorProjection.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { NoBinaryOpenError } from "../domain/configurationErrors.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import type { Result } from "../domain/result.js";
import { estimateJsonStorage } from "../application/JsonAuthenticationHeadroom.js";
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
import {
MCP_TRANSPORT_EVIDENCE_DISCOVERY,
@@ -31,6 +34,7 @@ import {
MCP_RESULT_STRING_LIMIT,
NODE_MAX_STRING_LENGTH,
TARGET_CONTENT_RESULT_META,
type ToolResultEncoding,
} from "./toolResultEncoding.js";
/** Project a response-budget refusal without labeling REA's reserved string limit as Node's ceiling. */
@@ -128,12 +132,17 @@ export class ToolResultDelivery {
result: Result<JsonValue, AnalysisError>,
contract: ToolContract,
recovery: Readonly<Record<string, JsonValue>> = {},
alternatives?: (
failure: Extract<ToolResultEncoding, { readonly ok: false }>,
budgetBytes: number,
) => Iterable<JsonValue>,
): CallToolResult {
return result.ok
? this.successResult(
contract.kind === "session" ? { result: result.value } : result.value,
contract,
recovery,
alternatives,
)
: this.toErrorToolResult(result.error, contract.returnsTargetContent);
}
@@ -298,8 +307,22 @@ export class ToolResultDelivery {
const partial = safeParseEvidence(error.partialObservation);
if (!partial.ok) return this.toErrorToolResult(error, targetContent);
const recorded = recordEvidence(partial.value);
if (!recorded.ok) return this.toErrorToolResult(error, targetContent);
const projected = projectAnalysisError(error);
if (!recorded.ok)
return this.projectedErrorResult(
{
...projected,
details: {
...projected.details,
partial_observation: {
kind: "inline-evidence",
retention: "unknown",
evidence: partial.value,
},
},
},
targetContent,
);
return this.projectedErrorResult(
{
...projected,
@@ -319,15 +342,53 @@ export class ToolResultDelivery {
projected: AnalysisErrorProjection,
targetContent: boolean,
): CallToolResult {
const structuredContent = { error: projected };
let structuredContent = { error: projected };
// Errors reach the wire as text only; the structured copy is a private
// carrier the transport removes after its own budget check.
const encoded = encodeToolResult(
let encoded = encodeToolResult(
structuredContent,
this.resultBudgetBytes,
"error-text",
targetContent,
);
let candidateProjection = projected;
while (!encoded.ok) {
const limit = encoded;
const candidates = [...unretainedEvidenceCandidates(candidateProjection)]
.map((candidate) => ({
candidate,
size: estimateJsonStorage(candidate.value, {
maximumValueBytes: this.resultBudgetBytes,
includeSerialized: false,
}).valueBytes,
}))
.sort((left, right) => right.size - left.size);
let next: AnalysisErrorProjection | undefined;
for (const { candidate } of candidates) {
const compact = omitUnretainedEvidenceFromError(
candidateProjection,
limit,
candidate,
);
if (compact === undefined) continue;
next ??= compact;
const content = { error: compact };
const compactEncoded = encodeToolResult(
content,
this.resultBudgetBytes,
"error-text",
targetContent,
);
if (compactEncoded.ok) {
structuredContent = content;
encoded = compactEncoded;
break;
}
}
if (encoded.ok) break;
if (next === undefined) break;
candidateProjection = next;
}
// The transport retains an oversized projected error before replacing it
// with a recoverable delivery constraint, using this same budget.
return {
@@ -342,13 +403,31 @@ export class ToolResultDelivery {
value: JsonValue,
contract: ToolContract,
recovery: Readonly<Record<string, JsonValue>> = {},
alternatives?: (
failure: Extract<ToolResultEncoding, { readonly ok: false }>,
budgetBytes: number,
) => Iterable<JsonValue>,
): CallToolResult {
const encoded = encodeToolResult(
let encoded = encodeToolResult(
value,
this.resultBudgetBytes,
"structured-and-text",
contract.returnsTargetContent,
);
if (!encoded.ok && alternatives !== undefined)
for (const candidate of alternatives(encoded, this.resultBudgetBytes)) {
const candidateEncoding = encodeToolResult(
candidate,
this.resultBudgetBytes,
"structured-and-text",
contract.returnsTargetContent,
);
if (candidateEncoding.ok) {
value = candidate;
encoded = candidateEncoding;
break;
}
}
if (encoded.ok)
return {
content: [{ type: "text", text: encoded.text }],
@@ -369,7 +448,9 @@ export class ToolResultDelivery {
? "Call list_unknowns on this same connection with the same filters and offset, using a smaller limit, then follow next_offset. If a single unknown cannot fit, export the session with export_evidence_bundle to recover it."
: contract.name === "get_evidence_bundle"
? evidenceBundleOverflowStep(value)
: "Use complete CLI JSON output to consume the analysis.";
: contract.name === "inspect_address_context"
? "Call get_evidence_bundle with detail summary to discover retained facet observations, then export_evidence_bundle to inspect complete records. Some facets may not have been retained; a new analysis can differ."
: "Use complete CLI JSON output to consume the analysis.";
const projected = mcpResponseOverflowProjection({
completed: true,
constraint: encoded.constraint,
+9
View File
@@ -352,6 +352,9 @@ export const createServer = (
recordUnknown,
evidenceById,
withAdmittedAnalysis: analysisAdmission,
...(session === undefined
? {}
: { admitRecovery: session.admitRecoveryObservation.bind(session) }),
};
const evidenceOptions = {
logger: toolLogger,
@@ -369,6 +372,12 @@ export const createServer = (
enhanced: () =>
registerEnhancedTools(server, {
...analysisOptions,
...(session === undefined
? {}
: { evidenceForAnalysis: session.evidenceForAnalysis.bind(session) }),
...(session === undefined
? {}
: { admitRecovery: session.admitRecoveryObservation.bind(session) }),
analysisProfile:
session === undefined
? undefined
@@ -43,6 +43,7 @@ export const registerCompareJavaScriptExportShapesTool = (
const recorded = recordEvidenceSources(
options.recordEvidence,
result.value.sources,
{ evidence: result.value.evidence },
);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, contract);
@@ -43,6 +43,7 @@ export const registerCompareApplicationVersionsTool = (
const recorded = recordEvidenceSources(
options.recordEvidence,
result.value.sources,
{ evidence: result.value.evidence },
);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, compareContract);
@@ -72,6 +72,7 @@ export const deliverApplicationEvidence = async (
const sources = recordEvidenceSources(
options.recordEvidence,
execution.sources,
{ evidence: selected.value },
);
if (!sources.ok) return delivery.toCallToolResult(sources, contract);
const saved = recordApplicationResult(options, parent, unknownKind);
@@ -48,6 +48,7 @@ export const registerReconstructionObligationLedgerTool = (
const recorded = recordEvidenceSources(
options.recordEvidence,
resolved.value.sources.bundle.records,
{ evidence: result.value },
);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, contract);
@@ -79,6 +79,7 @@ const registerProjection = (context: {
const recordedSources = recordEvidenceSources(
options.recordEvidence,
result.value.sources,
{ evidence: result.value.evidence },
);
if (!recordedSources.ok)
return server.delivery.toCallToolResult(recordedSources, contract);
@@ -41,6 +41,7 @@ export const registerTraceJavaScriptSemanticsTool = (
const recorded = recordEvidenceSources(
options.recordEvidence,
result.value.sources,
{ evidence: result.value.evidence },
);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, contract);
@@ -58,6 +58,10 @@ export const registerArtifactComparisonTool = (
const recordedSources = recordEvidenceSources(
(source) => session.recordEvidence(source),
[left, right],
{
evidence,
evidenceForAnalysis: session.evidenceForAnalysis.bind(session),
},
);
if (!recordedSources.ok)
return server.delivery.toCallToolResult(recordedSources, contract);
+1
View File
@@ -149,6 +149,7 @@ export const registerElectronTools = (
const recorded = recordEvidenceSources(
options.recordEvidence,
result.value.sources,
{ evidence: result.value.evidence },
);
return recorded.ok
? { ok: true, value: result.value.evidence }
+14 -2
View File
@@ -1,3 +1,4 @@
import type { Evidence } from "../domain/evidence.js";
import type { ToolResultDelivery } from "./ToolResultDelivery.js";
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type {
@@ -17,6 +18,7 @@ import type { ValidatedEnhancedCall } from "../application/EnhancedToolTypes.js"
import {
composedLimitationBudget,
executeComposedWorkflow,
retainComposedWorkflowSources,
createWorkflowEvidence,
retainPartialWorkflowEvidence,
workflowSnapshotRecord,
@@ -37,6 +39,7 @@ import { executeFunctionAnalysisEvidence } from "../application/FunctionAnalysis
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import type { EvidenceLookup } from "../application/EvidenceInputResolver.js";
import { retainAnalysisResultSources } from "../application/investigation/recordEvidenceSources.js";
import type { RecoveryAdmission } from "../application/investigation/InvestigationRetention.js";
import {
requireAnalysisViewRetention,
summarizeRetainedAnalysis,
@@ -57,6 +60,8 @@ export interface EnhancedToolRegistration {
) => AnalysisProfileCommitment | undefined)
| undefined;
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
readonly admitRecovery?: RecoveryAdmission;
readonly evidenceForAnalysis?: (id: string) => Evidence | undefined;
readonly evidenceById?: EvidenceLookup | undefined;
readonly recordUnknown: UnknownRegistryPort["recordUnknown"] | undefined;
readonly allowsSnapshotReplay:
@@ -288,10 +293,17 @@ const executeEnhancedTool = async (
parameters,
upstreamProfile: registration.analysisProfile?.(name),
});
result = retainAnalysisResultSources(
name,
result = retainComposedWorkflowSources(
registration.recordEvidence,
result,
{
target: registration.activeTarget?.(),
operation: name,
parameters,
upstreamProfile: registration.analysisProfile?.(name),
},
registration.evidenceForAnalysis,
registration.admitRecovery,
);
const finish = async (
response: CallToolResult,
+42 -14
View File
@@ -1,3 +1,4 @@
import { sessionEvidenceForExecution } from "../application/binary/SessionEvidenceReuse.js";
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import {
@@ -12,6 +13,8 @@ import type {
} from "../application/AnalysisProvider.js";
import type { ToolContract } from "../contracts/toolContractTypes.js";
import { ok } from "../domain/result.js";
import { admitCompletedAnalysis } from "../application/investigation/admitCompletedAnalysis.js";
import type { RecoveryAdmission } from "../application/investigation/InvestigationRetention.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { createAnalysisExecutionEvidence } from "../domain/analysisObservation.js";
import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js";
@@ -31,6 +34,7 @@ interface EvidenceToolRegistration {
readonly activeTarget: (() => BinaryTarget | undefined) | undefined;
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
readonly evidenceById?: EvidenceToolFamilyOptions["evidenceById"];
readonly admitRecovery?: RecoveryAdmission;
readonly withAdmittedAnalysis: WithAdmittedAnalysis;
readonly analysisForAdmission?: (
analysis: AnalysisOperationPort,
@@ -90,26 +94,50 @@ export const registerEvidenceTools = (
if (!execution.ok)
return server.delivery.toCallToolResult(execution, contract);
const sourceEvidence = execution.value.sourceEvidence ?? [];
const evidence = createAnalysisExecutionEvidence(
contract.name,
contract.retainedSummary?.evidenceParameters === "omit-detail"
? providerParameters
: parameters,
{
...execution.value,
subject:
execution.value.subject === undefined
? (options.activeTarget?.() ?? null)
: execution.value.subject,
},
"observed",
);
const sourceRecorded = recordEvidenceSources(
options.recordEvidence,
sourceEvidence,
);
if (!sourceRecorded.ok)
return server.delivery.toCallToolResult(sourceRecorded, contract);
const evidenceParameters =
contract.retainedSummary?.evidenceParameters === "omit-detail"
? providerParameters
: parameters;
const observation = {
...execution.value,
subject:
execution.value.subject === undefined
? (options.activeTarget?.() ?? null)
: execution.value.subject,
};
const reusable =
options.evidenceById === undefined
? undefined
: sessionEvidenceForExecution(
execution.value,
contract.name,
evidenceParameters,
options.evidenceById,
);
if (reusable === undefined) {
const admitted = admitCompletedAnalysis(
contract.name,
evidenceParameters,
observation,
options.admitRecovery,
);
if (!admitted.ok)
return server.delivery.toCallToolResult(admitted, contract);
}
const evidence =
reusable ??
createAnalysisExecutionEvidence(
contract.name,
evidenceParameters,
observation,
"observed",
);
const projected =
delivery?.projectEvidence(parameters, evidence) ?? ok(evidence);
if (!projected.ok)
@@ -58,6 +58,10 @@ export const registerFunctionComparisonTool = (
const retained = recordEvidenceSources(
session.recordEvidence.bind(session),
sources,
{
evidence,
evidenceForAnalysis: session.evidenceForAnalysis.bind(session),
},
);
if (!retained.ok)
return server.delivery.toCallToolResult(retained, contract);
+4
View File
@@ -111,6 +111,10 @@ const registerInvestigationWorkflow = (
const retained = recordEvidenceSources(
session.recordEvidence.bind(session),
sources,
{
evidence,
evidenceForAnalysis: session.evidenceForAnalysis.bind(session),
},
);
if (!retained.ok)
return server.delivery.toCallToolResult(retained, contract);
@@ -43,6 +43,7 @@ export const registerCompareManagedMembers = (
const recorded = recordEvidenceSources(
options.recordEvidence,
result.value.sources,
{ evidence: result.value.evidence },
);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, compareContract);
@@ -52,6 +52,7 @@ export const registerProjectManagedApplicationGraph = (
request.value.sources.members?.evidence,
request.value.sources.boundaries?.evidence,
].filter((source): source is Evidence => source !== undefined),
{ evidence: result.value },
);
if (!recordedSources.ok)
return server.delivery.toCallToolResult(
@@ -49,6 +49,7 @@ export const registerVerifyManagedNativeBoundaries = (
const recordedSources = recordEvidenceSources(
options.recordEvidence,
execution.sourceEvidence,
{ evidence: execution.evidence },
);
if (!recordedSources.ok)
return server.delivery.toCallToolResult(
@@ -59,6 +59,10 @@ export const registerProcessComparisonTool = (
const retained = recordEvidenceSources(
session.recordEvidence.bind(session),
sources,
{
evidence,
evidenceForAnalysis: session.evidenceForAnalysis.bind(session),
},
);
if (!retained.ok)
return server.delivery.toCallToolResult(retained, contract);
+24 -4
View File
@@ -8,6 +8,7 @@ import { workflowUnknownInput } from "../application/investigation/WorkflowUnkno
import {
getNavigationContext,
inspectAddressContext,
omitContextEvidenceForDelivery,
} from "../application/AnalysisContextQueries.js";
import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js";
import {
@@ -323,11 +324,30 @@ const registerContextTools = (
const admitted = await admission(
addressContract.name,
context.mcpReq.signal,
async (analysis) =>
server.delivery.toCallToolResult(
await inspectAddressContext(analysis, input, context.mcpReq.signal),
async (analysis) => {
const result = await inspectAddressContext(
analysis,
input,
context.mcpReq.signal,
);
if (!result.ok)
return server.delivery.toCallToolResult(result, addressContract);
const value = result.value;
return server.delivery.toCallToolResult(
result,
addressContract,
),
{},
(failure, budgetBytes) =>
(function* () {
for (const alternative of omitContextEvidenceForDelivery(
value,
failure,
budgetBytes,
))
yield { result: alternative };
})(),
);
},
);
return admitted.ok
? admitted.value
@@ -0,0 +1,87 @@
import { describe, expect, it } from "vitest";
import { createCli } from "../../../src/cli.js";
type CliInstance = ReturnType<typeof createCli>;
const serve = async (
cli: CliInstance,
argv: readonly string[],
): Promise<{ readonly stdout: string; readonly exitCode: number }> => {
let stdout = "";
let exitCode = 0;
await cli.serve([...argv], {
env: {},
exit: (code) => {
exitCode = code;
},
stdout: (text) => {
stdout += text;
},
});
return { stdout, exitCode };
};
/**
* Options whose owning field already declares a schema default. The help
* renderer appends that default, so the description must not restate it.
*/
const singleDefaultOptions: readonly {
readonly command: string;
readonly options: readonly string[];
}[] = [
{ command: "inspect-byte-entropy", options: ["--block-size", "--offset"] },
{
command: "inspect-devicetree",
options: [
"--max-file-bytes",
"--max-nodes",
"--max-properties",
"--max-depth",
],
},
{
command: "decode-signal-recording",
options: ["--line_offset", "--line_limit"],
},
{
command: "inspect-packet-capture",
options: ["--match_offset", "--match_limit"],
},
{
command: "inspect-packet-fields",
options: ["--kind", "--offset"],
},
{ command: "inspect-signal-decoders", options: ["--offset"] },
];
const optionLine = (help: string, option: string): string | undefined => {
const line = help
.split(/\r?\n/u)
.find((candidate) => candidate.trimStart().startsWith(`${option} `));
return line?.trim();
};
describe("CLI help default annotations", () => {
it("states each schema-owned option default exactly once", async () => {
const cli = createCli({});
const repeated: string[] = [];
for (const { command, options } of singleDefaultOptions) {
const { stdout, exitCode } = await serve(cli, [command, "--help"]);
expect(exitCode, command).toBe(0);
for (const option of options) {
const line = optionLine(stdout, option);
if (line === undefined) {
repeated.push(`${command} ${option}: option line missing`);
continue;
}
const statements = line.match(/default/giu)?.length ?? 0;
if (statements !== 1)
repeated.push(
`${command} ${option}: ${statements} default statements`,
);
}
}
expect(repeated).toEqual([]);
});
});
@@ -0,0 +1,145 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { expect, it } from "vitest";
import { createTestProviderRouter } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { BinarySession } from "../../../src/application/binary/BinarySession.js";
import { InvestigationRecords } from "../../../src/application/investigation/InvestigationRecords.js";
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
import { err, ok } from "../../../src/domain/result.js";
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { SESSION_TOOL_CONTRACTS } from "../../../src/contracts/sessionToolContracts.js";
import { createServer } from "../../../src/server/createServer.js";
it.each(["compact", "full"] as const)(
"preserves retained siblings and capacity recovery (%s schema)",
async (profile) => {
const identity = {
id: "fixture",
name: "Fixture analysis provider",
version: "1",
};
const directory = await createTestTempDirectory("rea-context-capacity-");
const target = join(directory, "fixture.com");
await writeFile(target, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const router = createTestProviderRouter(
() => ({
execute: async (operation) => {
if (operation === "address_name")
return ok(createAnalysisExecution("entry", identity));
if (operation === "resolve_containing_procedure")
return ok(
createAnalysisExecution(
{
query_address: "0x100",
found: true,
procedure: {
address: "0x100",
name: "f".repeat(16000),
classification: null,
body: { available: false, reason: "not recorded" },
},
},
identity,
),
);
return err(
new AnalysisCapabilityUnavailableError(
identity.id,
operation,
"not provided",
),
);
},
close: async () => ok(null),
}),
{
resolveAnalysisProfile: async () =>
ok({ profile: createAnalysisProfile(identity, { fixture: true }) }),
},
);
const records = new InvestigationRecords({
maximumRepresentationBytes: 8000,
});
const session = new BinarySession(router, records);
const server = createServer(
{ kind: "session", session },
{
environment: { ...process.env, REA_MCP_OUTPUT_SCHEMA_PROFILE: profile },
},
);
const client = new Client({ name: "context-capacity", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const definitions = new Map(
(await client.listTools()).tools.map((tool) => [tool.name, tool]),
);
const call = (name: string, args: Record<string, string>) => {
const toolDefinition = definitions.get(name);
if (toolDefinition === undefined)
throw new Error(`Tool ${name} was not advertised`);
return client.callTool({ name, arguments: args }, { toolDefinition });
};
expect(
(
await call("open_binary", {
path: target,
format: "dos-com",
provider_id: "fixture",
})
).isError,
).not.toBe(true);
const arguments_ = { document: "fixture", address: "0x100" };
const response = await call("inspect_address_context", arguments_);
expect(response.isError).not.toBe(true);
const contract = SESSION_TOOL_CONTRACTS.find(
(
candidate,
): candidate is Extract<
(typeof SESSION_TOOL_CONTRACTS)[number],
{ readonly name: "inspect_address_context" }
> => candidate.name === "inspect_address_context",
);
if (contract === undefined)
throw new Error("inspect_address_context contract is missing");
const context = contract.outputSchema.parse(
response.structuredContent,
).result;
const direct = await call("resolve_containing_procedure", arguments_);
expect(direct.isError).toBe(true);
const expected = parseMcpToolError(direct).error;
expect(expected.code).toBe("resource_constraint");
expect(expected.details).toMatchObject({
partial_observation: {
kind: "unretained-evidence",
evidence: {
operation: "resolve_containing_procedure",
normalized_result: { found: true, query_address: "0x100" },
},
},
});
expect(context.name).toEqual({ state: "available", value: "entry" });
expect(context.procedure).toEqual({
state: "unavailable",
reason: expected.message,
remediation: expected.remediation.action,
error: expected,
});
expect(records.statistics().evidenceRecords).toBe(1);
expect(session.immutableEvidenceBundle().records[0]).toMatchObject({
operation: "address_name",
normalized_result: "entry",
});
} finally {
await client.close();
await server.close();
await session.close();
}
},
);
@@ -5,6 +5,7 @@ import type { CallToolResult } from "@modelcontextprotocol/server";
import { afterAll, expect, it, onTestFinished, vi } from "vitest";
import { z } from "zod";
import { ANALYSIS_VIEW_TOOL_CONTRACTS } from "../../../src/contracts/analysisViewToolContracts.js";
import { batchUnknowns } from "../../../src/domain/analysisView/batchDecompileView.js";
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
import { AnalysisOutputError } from "../../../src/domain/analysisErrorCore.js";
import {
@@ -264,10 +265,9 @@ it.each(["full", "compact"] as const)(
},
});
expect(summary.unknowns).toEqual(
expect.arrayContaining([
expect.stringContaining("1 items have unknown procedure identity"),
expect.stringContaining("not attributed to one successful item"),
]),
batchUnknowns(
batchDecompileResultSchema.parse(parent.normalized_result).items,
),
);
const callsAfterBatch = harness.calls.count;
@@ -0,0 +1,179 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import type { CallToolResult } from "@modelcontextprotocol/server";
import { access, mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it, vi } from "vitest";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { BinarySession } from "../../../src/application/binary/BinarySession.js";
import { InvestigationRecords } from "../../../src/application/investigation/InvestigationRecords.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
import { err, ok } from "../../../src/domain/result.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestProviderRouter } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js";
const expectRecoveredBatch = (response: CallToolResult, owned: string) => {
expect(parseMcpToolError(response).error).toMatchObject({
code: "resource_constraint",
details: {
completed_observation: {
evidence: {
normalized_result: {
items: [
{ status: "ok" },
{
status: "error",
error: {
code: "cleanup_incomplete",
details: { resources: [owned] },
},
},
],
},
},
},
},
});
};
it("keeps a mixed batch cleanup owner after source retention fails and retries it", async () => {
const root = await createTestTempDirectory("rea-batch-owned-cleanup-");
const target = join(root, "fixture.com"),
owned = join(root, "owned");
await writeFile(target, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const provider = { id: "fixture", name: "Fixture", version: "1" };
const source = createEvidence(undefined, provider, {
operation: "health",
parameters: {},
result: "source",
});
const failure = new ProviderCleanupError(provider.id, [owned], {
reason: "controlled cleanup failure",
});
let count = 0,
pending = false,
allow = false,
closes = 0;
let entered!: () => void;
let release!: () => void;
const closing = new Promise<void>((resolve) => {
entered = resolve;
});
const gate = new Promise<void>((resolve) => {
release = resolve;
});
const session = new BinarySession(
createTestProviderRouter(() => ({
execute: async (operation) => {
if (operation === "procedure_pseudo_code" && ++count === 2) {
await mkdir(owned);
pending = true;
return err(failure);
}
return ok(
createAnalysisExecution(
operation === "procedure_address"
? "0x100"
: operation === "address_name"
? "example"
: operation === "health"
? null
: "int example(void) { return 1; }",
provider,
),
);
},
close: async () => {
closes++;
if (!pending) return ok(null);
if (closes === 2) {
entered();
await gate;
return err(failure);
}
if (!allow) return err(failure);
await rm(owned, { recursive: true });
pending = false;
failure.resolveCleanup();
return ok(null);
},
})),
new InvestigationRecords({ maximumRepresentationBytes: 0 }),
);
const admit = session.withAdmittedAnalysis.bind(session);
vi.spyOn(session, "withAdmittedAnalysis").mockImplementation(
(operation, signal, run) =>
admit(operation, signal, (analysis) =>
run({
...analysis,
execute: async (...args) => {
const result = await analysis.execute(...args);
if (args[0] === "procedure_pseudo_code" && result.ok)
return {
...result,
value: { ...result.value, sourceEvidence: [source] },
};
return result;
},
}),
),
);
const server = createServer({ kind: "session", session });
const client = new Client({ name: "batch-owned-cleanup", version: "1" });
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
const call = (name: string, args: Record<string, unknown> = {}) =>
client.callTool({ name, arguments: args }, { timeout: 120000 });
expect(
(await call("open_binary", { path: target, format: "dos-com" })).isError,
).not.toBe(true);
const response = await call("batch_decompile", {
addresses: ["first", "second"],
});
expectRecoveredBatch(response, owned);
expect(pending).toBe(true);
expect(failure.cleanupIncomplete).toBe(true);
await expect(access(owned)).resolves.toBeUndefined();
const close1 = await call("close_binary");
expect(close1.isError).toBe(true);
expect(parseMcpToolError(close1).error).toMatchObject({
code: "cleanup_incomplete",
recovery: { kind: "retry_cleanup", operation: "close_binary" },
details: { resources: [owned] },
});
expect(pending).toBe(true);
const controller = new AbortController();
const cancelledClose = client.callTool(
{ name: "close_binary", arguments: {} },
{ signal: controller.signal, timeout: 120000 },
);
try {
await awaitAcknowledgement(closing, cancelledClose, "final cleanup");
controller.abort();
await expect(cancelledClose).rejects.toThrow();
} finally {
controller.abort();
release();
await Promise.allSettled([cancelledClose]);
}
allow = true;
const close2 = await call("close_binary");
expect(close2.isError).not.toBe(true);
expect(pending).toBe(false);
expect(failure.cleanupIncomplete).toBe(false);
expect(closes).toBe(3);
await expect(access(owned)).rejects.toThrow();
} finally {
allow = true;
release();
await client.close();
await server.close();
await session.close();
vi.restoreAllMocks();
}
});
@@ -0,0 +1,132 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import pino from "pino";
import { expect, it, onTestFinished } from "vitest";
import {
createAnalysisExecution,
type AnalysisOperationPort,
} from "../../../src/application/AnalysisProvider.js";
import { EvidenceLedger } from "../../../src/application/investigation/EvidenceLedger.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { AnalysisResourceConstraintError } from "../../../src/domain/analysisErrorCore.js";
import { err, ok } from "../../../src/domain/result.js";
import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js";
import { registerEnhancedTools } from "../../../src/server/registerEnhancedTools.js";
import { withAdmittedAnalysis } from "../../../src/server/analysisAdmission.js";
import { createToolResultDelivery } from "../../../src/server/toolResult.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
const provider = { id: "fixture", name: "Fixture", version: "1" };
const source = createEvidence(undefined, provider, {
operation: "health",
parameters: {},
result: "source",
});
it.each([
{ size: 100, denyAdmission: false, ownerKnown: true },
{ size: 100_000, denyAdmission: false, ownerKnown: true },
{ size: 100, denyAdmission: true, ownerKnown: true },
{ size: 100, denyAdmission: false, ownerKnown: false },
])(
"preserves capacity error with size $size, denied admission $denyAdmission, and known owner $ownerKnown",
async ({ size, denyAdmission, ownerKnown }) => {
const ledger = new EvidenceLedger({ maximumRepresentationBytes: 0 });
const record = (evidence: typeof source) => ledger.record(evidence);
const analysis: AnalysisOperationPort = {
execute: async (operation) => {
return ok(
createAnalysisExecution(
operation === "procedure_address"
? "0x1000"
: operation === "address_name"
? "example"
: "x".repeat(size),
provider,
operation === "procedure_pseudo_code"
? { sourceEvidence: [source] }
: {},
),
);
},
};
const server = new EvidenceMcpServer(
{ name: "batch-overflow", version: "1" },
{},
record,
createToolResultDelivery(10 * 1024),
);
registerEnhancedTools(server, {
withAdmittedAnalysis: withAdmittedAnalysis({ kind: "fixed", analysis }),
logger: pino({ enabled: false }),
activeTarget: () => undefined,
analysisProfile: undefined,
recordEvidence: record,
...(ownerKnown ? { hasEvidence: (id: string) => ledger.has(id) } : {}),
...(denyAdmission
? {
admitRecovery: () =>
err(
new AnalysisResourceConstraintError(
"batch_decompile",
"memory",
"no headroom",
null,
),
),
}
: {}),
evidenceById: ownerKnown ? (id) => ledger.forAnalysis(id) : () => source,
recordUnknown: undefined,
allowsSnapshotReplay: undefined,
recordWorkflowSnapshot: undefined,
});
const client = new Client({ name: "batch-overflow-test", version: "1" });
onTestFinished(async () => {
await client.close();
await server.close();
});
const [ct, st] = InMemoryTransport.createLinkedPair();
await server.connect(st);
await client.connect(ct);
const response = await client.callTool({
name: "batch_decompile",
arguments: { addresses: ["example"] },
});
expect(response.isError).toBe(true);
const error = parseMcpToolError(response).error;
expect(error).toMatchObject({
code: "resource_constraint",
details: { partial_observation: { kind: "unretained-evidence" } },
});
if (denyAdmission)
expect(error.details).toMatchObject({
completed_observation_omitted: {
reason: "insufficient-authentication-headroom",
source_closure: "not-established",
},
});
else if (!ownerKnown)
expect(error.details).toMatchObject({
completed_observation: {
kind: "inline-evidence",
retention: "unknown",
source_closure: "not-established",
evidence: { operation: "batch_decompile" },
},
});
else if (size === 100)
expect(error.details).toMatchObject({
completed_observation: {
kind: "unretained-evidence",
source_closure: "not-established",
evidence: { operation: "batch_decompile" },
},
});
else
expect(error.details).toMatchObject({
completed_observation_omitted: {
reason: "mcp-response-limit",
source_closure: "not-established",
},
});
expect(ledger.statistics().evidenceRecords).toBe(0);
},
);
@@ -0,0 +1,331 @@
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { expect, it } from "vitest";
import { createTestProviderRouter } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { BinarySession } from "../../../src/application/binary/BinarySession.js";
import { InvestigationRecords } from "../../../src/application/investigation/InvestigationRecords.js";
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
import { err, ok } from "../../../src/domain/result.js";
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
import { createServer } from "../../../src/server/createServer.js";
const verifyContext = (
content: unknown,
twoLarge: boolean,
session: BinarySession,
): void => {
expect(content).toMatchObject({
result: {
name: { state: "available", value: "entry" },
procedure: {
state: "unavailable",
error: {
code: "resource_constraint",
details: {
partial_observation_omitted: {
reason: "mcp-response-limit",
retention: "unretained",
original_response_bytes_at_least: expect.any(Number),
evidence_id: expect.stringMatching(/^ev_[a-f0-9]{64}$/u),
},
},
},
},
},
});
const procedureDetails = (
content as {
result: {
procedure: {
error: {
details: {
partial_observation_omitted: { evidence_id: string };
};
};
};
};
}
).result.procedure.error.details;
expect(procedureDetails).not.toHaveProperty("partial_observation");
expect(procedureDetails).not.toHaveProperty("evidence_reference");
expect(
session.hasEvidence(
procedureDetails.partial_observation_omitted.evidence_id,
),
).toBe(false);
const smallFacet = twoLarge ? "inline_comment" : "comment";
expect(content).toMatchObject({
result: {
[smallFacet]: {
state: "unavailable",
error: {
code: "resource_constraint",
details: {
partial_observation: {
kind: "unretained-evidence",
evidence: {
operation: smallFacet,
normalized_result: "short note",
},
},
},
},
},
...(twoLarge
? {
comment: {
state: "unavailable",
error: {
details: {
partial_observation_omitted: {
reason: "mcp-response-limit",
retention: "unretained",
},
},
},
},
}
: {}),
},
});
};
it.each([
["compact", false],
["full", false],
["compact", true],
] as const)(
"keeps context and smaller partials when unretained observations exceed the MCP budget (%s, two large=%s)",
async (profile, twoLarge) => {
const identity = {
id: "fixture",
name: "Fixture analysis provider",
version: "1",
};
const directory = await createTestTempDirectory("rea-context-capacity-");
const target = join(directory, "fixture.com");
await writeFile(target, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const router = createTestProviderRouter(
() => ({
execute: async (operation) => {
if (operation === "address_name")
return ok(createAnalysisExecution("entry", identity));
if (operation === "comment")
return ok(
createAnalysisExecution(
twoLarge ? "g".repeat(6 * 1024 * 1024) : "short note",
identity,
),
);
if (operation === "inline_comment" && twoLarge)
return ok(createAnalysisExecution("short note", identity));
if (operation === "resolve_containing_procedure")
return ok(
createAnalysisExecution(
{
query_address: "0x100",
found: true,
procedure: {
address: "0x100",
name: "f".repeat(6 * 1024 * 1024),
classification: null,
body: { available: false, reason: "not recorded" },
},
},
identity,
),
);
return err(
new AnalysisCapabilityUnavailableError(
identity.id,
operation,
"not provided",
),
);
},
close: async () => ok(null),
}),
{
resolveAnalysisProfile: async () =>
ok({ profile: createAnalysisProfile(identity, { fixture: true }) }),
},
);
const records = new InvestigationRecords({
maximumRepresentationBytes: 8000,
});
const session = new BinarySession(router, records);
const server = createServer(
{ kind: "session", session },
{
environment: {
...process.env,
REA_MCP_TOOL_FAMILIES: "session",
REA_MCP_OUTPUT_SCHEMA_PROFILE: profile,
},
},
);
const client = new Client({ name: "context-capacity", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const definitions = new Map(
(await client.listTools()).tools.map((tool) => [tool.name, tool]),
);
const call = (name: string, args: Record<string, string>) => {
const toolDefinition = definitions.get(name);
if (toolDefinition === undefined)
throw new Error(`Tool ${name} was not advertised`);
return client.callTool({ name, arguments: args }, { toolDefinition });
};
expect(
(
await call("open_binary", {
path: target,
format: "dos-com",
provider_id: "fixture",
})
).isError,
).not.toBe(true);
const arguments_ = { document: "fixture", address: "0x100" };
const response = await call("inspect_address_context", arguments_);
expect(response.isError).not.toBe(true);
verifyContext(response.structuredContent, twoLarge, session);
const bundle = await client.callTool({
name: "get_evidence_bundle",
arguments: { detail: "complete" },
});
expect(bundle.structuredContent).toMatchObject({
result: {
records: [{ operation: "address_name", normalized_result: "entry" }],
},
});
} finally {
await client.close();
await server.close();
await session.close();
}
},
);
it.each([true, false])(
"recovers retained context facets when possible (profile=%s)",
async (profile) => {
const identity = {
id: "fixture",
name: "Fixture analysis provider",
version: "1",
};
const target = join(
await createTestTempDirectory("rea-context-oversize-success-"),
"fixture.com",
);
await writeFile(target, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const router = createTestProviderRouter(
() => ({
execute: async (operation) =>
operation === "health"
? ok(createAnalysisExecution(null, identity))
: operation === "address_name"
? ok(
createAnalysisExecution(
"x".repeat(6 * 1024 * 1024),
identity,
),
)
: err(
new AnalysisCapabilityUnavailableError(
identity.id,
operation,
"not provided",
),
),
close: async () => ok(null),
}),
profile
? {
resolveAnalysisProfile: async () =>
ok({
profile: createAnalysisProfile(identity, { fixture: true }),
}),
}
: {},
);
const session = new BinarySession(router);
const server = createServer(
{ kind: "session", session },
{ environment: { ...process.env, REA_MCP_TOOL_FAMILIES: "session" } },
);
const client = new Client({
name: "context-oversize-success",
version: "1",
});
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
const opened = await client.callTool({
name: "open_binary",
arguments: { path: target, format: "dos-com" },
});
expect(opened.isError, JSON.stringify(opened.content)).not.toBe(true);
const result = await client.callTool({
name: "inspect_address_context",
arguments: { address: "0x100", document: "fixture" },
});
expect(result.isError).toBe(true);
const error = parseMcpToolError(result).error;
expect(error).toMatchObject({
code: "resource_constraint",
details: { reported_limits: { boundary: "mcp-response" } },
});
expect(error.remediation.action).toContain("get_evidence_bundle");
expect(error.remediation.action).toContain("export_evidence_bundle");
expect(error.remediation.action).toContain("may not have been retained");
const advertised = new Set(
(await client.listTools()).tools.map((tool) => tool.name),
);
expect(advertised.has("get_evidence_bundle")).toBe(true);
expect(advertised.has("export_evidence_bundle")).toBe(true);
expect(advertised.has("address_name")).toBe(false);
const summary = await client.callTool({
name: "get_evidence_bundle",
arguments: {
detail: "summary",
filters: { operation: "address_name" },
},
});
expect(summary.isError).not.toBe(true);
expect(summary.structuredContent).toMatchObject({
result: { matching_records: profile ? 1 : 0 },
});
const path = join(
await createTestTempDirectory("rea-context-recover-"),
"evidence.json",
);
const exported = await client.callTool({
name: "export_evidence_bundle",
arguments: { path },
});
expect(exported.isError).not.toBe(true);
const bundle = JSON.parse(await readFile(path, "utf8")) as {
records: { operation: string; normalized_result: unknown }[];
};
const record = bundle.records.find(
(entry) => entry.operation === "address_name",
);
if (profile)
expect(record?.normalized_result).toBe("x".repeat(6 * 1024 * 1024));
else expect(record).toBeUndefined();
} finally {
await client.close();
await server.close();
await session.close();
}
},
);
@@ -0,0 +1,108 @@
import { expect, it } from "vitest";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import { createTestProviderRouter } from "../../fixtures/binarySession.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { BinarySession } from "../../../src/application/binary/BinarySession.js";
import { InvestigationRecords } from "../../../src/application/investigation/InvestigationRecords.js";
import { compareFunctionEvidence } from "../../../src/application/investigation/FunctionComparisonEvidence.js";
import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js";
import { safeParseEvidence } from "../../../src/domain/evidenceValidation.js";
import { parseEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { analysisErrorProjectionSchema } from "../../../src/contracts/errorSchemas.js";
it.each([
{ profile: "compact", preexisting: false },
{ profile: "compact", preexisting: true },
{ profile: "full", preexisting: false },
{ profile: "full", preexisting: true },
] as const)(
"preserves the completed comparison on atomic retention failure ($profile schema, preexisting: $preexisting)",
async ({ profile, preexisting }) => {
const input = { ...FUNCTION_COMPARISON_EXAMPLE };
const computed = compareFunctionEvidence(input);
if (!computed.ok) throw computed.error;
expect(computed.value.unknown).toBeDefined();
const initial = [
input.left,
input.right,
...(preexisting ? [computed.value.evidence] : []),
];
const sizing = new InvestigationRecords();
for (const evidence of initial)
expect(sizing.recordEvidence(evidence).ok).toBe(true);
const records = new InvestigationRecords({
maximumRepresentationBytes:
sizing.statistics().retainedRepresentationBytes,
});
for (const evidence of initial)
expect(records.recordEvidence(evidence).ok).toBe(true);
const session = new BinarySession(
createTestProviderRouter(() => {
throw new Error("Comparison must not launch a provider");
}),
records,
);
const harness = await createApplicationMcpHarness({
session,
serverOptions: {
environment: { ...process.env, REA_MCP_OUTPUT_SCHEMA_PROFILE: profile },
},
});
try {
const reply = await harness.client.callTool({
name: "compare_functions",
arguments: input,
});
expect(reply.isError).toBe(true);
const error = parseMcpToolError(reply).error;
analysisErrorProjectionSchema.parse(error);
expect(error).toMatchObject({
code: "resource_constraint",
details: { operation: "record_unknown", resource: "capacity" },
});
const partial = error.details?.partial_observation;
if (preexisting) {
expect(partial).toEqual({
kind: "retained-evidence",
evidence_id: computed.value.evidence.evidence_id,
});
} else {
expect(partial).toMatchObject({
kind: "unretained-evidence",
evidence: {
evidence_id: computed.value.evidence.evidence_id,
operation: "compare_functions",
},
});
const authenticated = safeParseEvidence(
(partial as { evidence: unknown }).evidence,
);
if (!authenticated.ok) throw authenticated.error;
expect(authenticated.value).toEqual(computed.value.evidence);
}
expect(session.hasEvidence(computed.value.evidence.evidence_id)).toBe(
preexisting,
);
expect(session.hasEvidence(input.left.evidence_id)).toBe(true);
expect(session.hasEvidence(input.right.evidence_id)).toBe(true);
expect(records.statistics().unknownRevisions).toBe(0);
const bundle = await harness.client.callTool({
name: "get_evidence_bundle",
arguments: { detail: "complete" },
});
expect(bundle.isError).not.toBe(true);
const result = toolContract("get_evidence_bundle").outputSchema.parse(
bundle.structuredContent,
);
const ids = parseEvidenceBundle(result.result).records.map(
({ evidence_id }) => evidence_id,
);
expect(ids.includes(computed.value.evidence.evidence_id)).toBe(
preexisting,
);
} finally {
await harness.close();
}
},
);
@@ -0,0 +1,261 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
import { expect, it } from "vitest";
import { z } from "zod";
import { createTestProviderRouter } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { BinarySession } from "../../../src/application/binary/BinarySession.js";
import { InvestigationRecords } from "../../../src/application/investigation/InvestigationRecords.js";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { safeParseEvidence } from "../../../src/domain/evidenceValidation.js";
import { recoveryOmissionRemediation } from "../../../src/domain/analysisErrorProjection.js";
import { UNRETAINED_EVIDENCE_EXPORT_GUIDANCE } from "../../../src/domain/analysisErrorPresentation.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { ok } from "../../../src/domain/result.js";
import { createServer } from "../../../src/server/createServer.js";
import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js";
import { createToolResultDelivery } from "../../../src/server/toolResult.js";
it.each(["compact", "full"] as const)(
"delivers authenticated but unretained provider Evidence inline (%s schema)",
async (profile) => {
const identity = {
id: "fixture",
name: "Fixture analysis provider",
version: "1",
};
const target = join(
await createTestTempDirectory("rea-evidence-refusal-"),
"fixture.com",
);
await writeFile(target, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const router = createTestProviderRouter(() => ({
execute: async () =>
ok(
createAnalysisExecution("entry", identity, {
limitations: ["fixture limitation"],
}),
),
close: async () => ok(null),
}));
const records = new InvestigationRecords({ maximumRepresentationBytes: 0 });
const session = new BinarySession(router, records);
const server = createServer(
{ kind: "session", session },
{
environment: { ...process.env, REA_MCP_OUTPUT_SCHEMA_PROFILE: profile },
},
);
const client = new Client({ name: "unretained-evidence", version: "1" });
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
const definitions = new Map(
(await client.listTools()).tools.map((tool) => [tool.name, tool]),
);
const definition = definitions.get("address_name");
if (definition === undefined)
throw new Error("address_name was not advertised");
expect(
(
await client.callTool({
name: "open_binary",
arguments: { path: target, format: "dos-com" },
})
).isError,
).not.toBe(true);
const result = await client.callTool(
{
name: "address_name",
arguments: { address: "0x100", document: "fixture" },
},
{ toolDefinition: definition },
);
expect(result.isError).toBe(true);
const error = parseMcpToolError(result).error;
expect(error.code).toBe("resource_constraint");
expect(error.details).toMatchObject({
operation: "record_evidence",
resource: "capacity",
partial_observation: {
kind: "unretained-evidence",
evidence: {
operation: "address_name",
normalized_result: "entry",
provider: identity,
limitations: ["fixture limitation"],
},
},
});
const partial = error.details?.partial_observation as {
kind: string;
evidence: unknown;
};
const authenticated = safeParseEvidence(partial.evidence);
expect(authenticated.ok).toBe(true);
if (!authenticated.ok) throw authenticated.error;
expect(session.hasEvidence(authenticated.value.evidence_id)).toBe(false);
expect(records.statistics().evidenceRecords).toBe(0);
expect(error.remediation.action).toContain(
UNRETAINED_EVIDENCE_EXPORT_GUIDANCE,
);
expect(error.details).not.toHaveProperty("evidence_reference");
const bundle = await client.callTool({
name: "get_evidence_bundle",
arguments: { detail: "complete" },
});
expect(bundle.isError).not.toBe(true);
expect(bundle.structuredContent).toMatchObject({
result: { records: [] },
});
} finally {
await client.close();
await server.close();
await session.close();
}
},
);
it("keeps an oversized unretained observation failure bounded and the connection usable", async () => {
const identity = {
id: "fixture",
name: "Fixture analysis provider",
version: "1",
};
const target = join(
await createTestTempDirectory("rea-evidence-overflow-"),
"fixture.com",
);
await writeFile(target, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const value = "x".repeat(STDIO_DEFAULT_MAX_BUFFER_SIZE + 65536);
const router = createTestProviderRouter(() => ({
execute: async () => ok(createAnalysisExecution(value, identity)),
close: async () => ok(null),
}));
const records = new InvestigationRecords({ maximumRepresentationBytes: 0 });
const session = new BinarySession(router, records);
const server = createServer({ kind: "session", session });
const client = new Client({
name: "unretained-evidence-overflow",
version: "1",
});
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
expect(
(
await client.callTool({
name: "open_binary",
arguments: { path: target, format: "dos-com" },
})
).isError,
).not.toBe(true);
const result = await client.callTool(
{
name: "address_name",
arguments: { address: "0x100", document: "fixture" },
},
{ timeout: 120000 },
);
expect(result.isError).toBe(true);
const error = parseMcpToolError(result).error;
expect(error).toMatchObject({
code: "resource_constraint",
details: {
operation: "record_evidence",
resource: "capacity",
reported_limits: { maximum_representation_bytes: 0 },
partial_observation_omitted: {
reason: "mcp-response-limit",
constraint: "receive-buffer",
retention: "unretained",
},
},
});
expect(error.remediation.action).toContain(
recoveryOmissionRemediation("unretained"),
);
expect(error.remediation.action).toContain(
UNRETAINED_EVIDENCE_EXPORT_GUIDANCE,
);
expect(error.details).not.toHaveProperty("partial_observation");
expect(error.details).not.toHaveProperty("evidence_reference");
expect(Buffer.byteLength(JSON.stringify(result))).toBeLessThan(
STDIO_DEFAULT_MAX_BUFFER_SIZE,
);
expect(records.statistics().evidenceRecords).toBe(0);
expect(
(
await client.callTool({
name: "get_evidence_bundle",
arguments: { detail: "summary" },
})
).isError,
).not.toBe(true);
} finally {
await client.close();
await server.close();
await session.close();
}
}, 120000);
it("bounds fallback when the retention error itself has oversized diagnostics", async () => {
const records = new InvestigationRecords({ maximumRepresentationBytes: 0 });
const evidence = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{ operation: "address_name", parameters: {}, result: "entry" },
{ immutable: true },
);
const refused = records.recordEvidence(evidence);
if (refused.ok) throw new Error("Expected retention refusal");
refused.error.retainDiagnostics({
oversized: "x".repeat(STDIO_DEFAULT_MAX_BUFFER_SIZE + 65536),
});
const delivery = createToolResultDelivery(STDIO_DEFAULT_MAX_BUFFER_SIZE);
const server = new EvidenceMcpServer(
{ name: "retention-overflow", version: "1" },
{ capabilities: {} },
(record) => records.recordEvidence(record),
delivery,
);
server.registerTool(
"large_retention_failure",
{ inputSchema: z.object({}) },
async () => delivery.toErrorToolResult(refused.error, true),
);
const client = new Client({ name: "retention-overflow", version: "1" });
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
const result = await client.callTool({
name: "large_retention_failure",
arguments: {},
});
const error = parseMcpToolError(result).error;
expect(error).toMatchObject({
code: "resource_constraint",
details: {
reported_limits: {
boundary: "mcp-response",
original_error_code: "resource_constraint",
retention: "failed",
retention_error: { code: "resource_constraint" },
},
},
});
expect(Buffer.byteLength(JSON.stringify(result))).toBeLessThan(
STDIO_DEFAULT_MAX_BUFFER_SIZE,
);
expect(records.statistics().evidenceRecords).toBe(0);
} finally {
await client.close();
await server.close();
}
}, 120000);
@@ -17,7 +17,7 @@ import {
import type { AnalysisError } from "../../../src/domain/analysisErrorBase.js";
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { evidenceBundleSchema } from "../../../src/domain/evidenceBundle.js";
import { nativeValueTraceSchema } from "../../../src/domain/native/nativeValueTrace.js";
import { ok } from "../../../src/domain/result.js";
@@ -287,6 +287,51 @@ const openSession = async (port: AnalysisOperationPort) => {
return { client, session };
};
it("keeps the stopped trace and source-publication failure together", async () => {
const { port } = failingDiamond({
"analyze_function:0x2000": childFailure("analyze_function", true),
});
const source = createEvidence(undefined, provider, {
operation: "analyze_function",
parameters: { procedure: "0x1000" },
result: { source: "observed" },
});
const client = await connect({
execute: async (operation, parameters, options) => {
const outcome = await port.execute(operation, parameters, options);
if (
outcome.ok &&
operation === "analyze_function" &&
parameters.procedure === "0x1000"
)
return ok({ ...outcome.value, sourceEvidence: [source] });
return outcome;
},
});
const { error } = parseMcpToolError(await client.callTool(trace));
expect(error.code).toBe("cleanup_incomplete");
const partial = parseEvidence(error.details?.partial_observation);
const admitted = nativeValueTraceSchema.parse(partial.normalized_result);
expect(admitted.procedures.map(({ address }) => address)).toEqual(["0x1000"]);
expect(admitted.traversal).toEqual({
status: "truncated",
reasons: ["cleanup_incomplete"],
});
expect(partial.evidence_links).toContain(source.evidence_id);
expect(error.details?.secondary_failures).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "capability_unavailable",
details: expect.objectContaining({
provider_id: "rea",
operation: "trace_native_values",
}),
}),
]),
);
});
it("retains the admitted trace and its completed sources as session Evidence", async () => {
const { port, reads } = failingDiamond(
{
@@ -0,0 +1,216 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { BinarySession } from "../../../src/application/binary/BinarySession.js";
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
import {
createEvidence,
parseEvidence,
withEvidenceCallerInputPath,
type Evidence,
} from "../../../src/domain/evidence.js";
import { ok } from "../../../src/domain/result.js";
import { createImmutableEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import { OFFICIAL_TOOL_CONTRACTS } from "../../../src/contracts/officialToolContracts.js";
import { silentLogger } from "../../../src/logger.js";
import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js";
import {
withAdmittedAnalysis,
type WithAdmittedAnalysis,
} from "../../../src/server/analysisAdmission.js";
import { createServer } from "../../../src/server/createServer.js";
import { registerEvidenceTools } from "../../../src/server/registerEvidenceTools.js";
import { createToolResultDelivery } from "../../../src/server/toolResult.js";
import { createTestProviderRouter } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
it.each(["normal", "copied", "parameters", "forged", "source"] as const)(
"preserves direct observation identity across the %s session boundary",
async (mode) => {
const provider = {
id: "fixture",
name: "Fixture analysis provider",
version: "1",
};
const path = join(
await createTestTempDirectory("rea-session-reuse-"),
"fixture.com",
);
await writeFile(path, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
let forged: Evidence | undefined;
let source: Evidence | undefined;
const router = createTestProviderRouter(
() => ({
execute: async () =>
ok({
...createAnalysisExecution(["0x100"], provider, {
...(mode === "source" && source !== undefined
? { sourceEvidence: [source] }
: {}),
}),
...(mode === "forged" && forged !== undefined
? { evidenceId: forged.evidence_id }
: {}),
}),
close: async () => ok(null),
}),
{
resolveAnalysisProfile: async () =>
ok({ profile: createAnalysisProfile(provider, {}) }),
},
);
const session = new BinarySession(router);
const opened = await session.open(path, { formatHint: "dos-com" });
if (!opened.ok) throw opened.error;
source = createEvidence(
opened.value,
provider,
{ operation: "health", parameters: {}, result: "source" },
{ immutable: true },
);
forged = createEvidence(
opened.value,
provider,
{
operation: "xrefs",
parameters: { address: "0x100", document: "fixture" },
result: ["0xabad1dea"],
},
{ immutable: true },
);
if (mode === "forged") expect(session.recordEvidence(forged).ok).toBe(true);
const base = withAdmittedAnalysis({ kind: "session", session });
const admitted: WithAdmittedAnalysis = (name, signal, operation) =>
base(name, signal, (port) =>
operation({
...port,
execute: async (op, args, options) => {
const result = await port.execute(op, args, options);
return mode === "copied" && op === "xrefs" && result.ok
? ok({ ...result.value, result: ["0x200"] })
: result;
},
}),
);
const server = new EvidenceMcpServer(
{ name: "reuse-control", version: "1" },
{ capabilities: {} },
session.recordEvidence.bind(session),
createToolResultDelivery(undefined),
);
const contract = OFFICIAL_TOOL_CONTRACTS.find(
({ name }) => name === "xrefs",
);
if (contract === undefined) throw new Error("Missing xrefs contract");
registerEvidenceTools(server, [contract], {
logger: silentLogger,
activeTarget: () => session.activeTarget(),
recordEvidence: session.recordEvidence.bind(session),
evidenceById: session.evidenceForAnalysis.bind(session),
admitRecovery: session.admitRecoveryObservation.bind(session),
withAdmittedAnalysis: admitted,
...(mode === "parameters"
? {
parametersForExecution: (_operation, parameters) => ({
...parameters,
address: "0x300",
}),
}
: {}),
});
const client = new Client({ name: "reuse-control", version: "1" });
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
const response = await client.callTool({
name: "xrefs",
arguments: { address: "0x100", document: "fixture" },
});
expect(response.isError).not.toBe(true);
const result = response.structuredContent as {
normalized_result: string[];
parameters: { address: string };
evidence_id: string;
evidence_links: string[];
};
expect(result.normalized_result).toEqual([
mode === "copied" ? "0x200" : "0x100",
]);
expect(result.parameters.address).toBe("0x100");
if (mode === "forged")
expect(result.evidence_id).not.toBe(forged.evidence_id);
if (mode === "source")
expect(result.evidence_links).toContain(source.evidence_id);
} finally {
await client.close();
await server.close();
await session.close();
}
},
);
it("preserves the selected path when reusing an identical observation for a different file", async () => {
const directory = await createTestTempDirectory("rea-reused-subject-");
const first = join(directory, "first.com");
const second = join(directory, "second.com");
const bytes = Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]);
await writeFile(first, bytes);
await writeFile(second, bytes);
const provider = {
id: "fixture",
name: "Fixture analysis provider",
version: "1",
};
const router = createTestProviderRouter(
() => ({
execute: async () => ok(createAnalysisExecution(["0x200"], provider)),
close: async () => ok(null),
}),
{
resolveAnalysisProfile: async () =>
ok({ profile: createAnalysisProfile(provider, {}) }),
},
);
const session = new BinarySession(router);
const server = createServer({ kind: "session", session });
const client = new Client({ name: "reused-subject", version: "1" });
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
const call = async (name: string, args: Record<string, unknown>) => {
const r = await client.callTool({ name, arguments: args });
expect(r.isError).not.toBe(true);
return r.structuredContent;
};
await call("open_binary", { path: first, format: "dos-com" });
const before = await call("procedure_callees", { procedure: "0x100" });
await call("open_binary", { path: second, format: "dos-com" });
const after = await call("procedure_callees", { procedure: "0x100" });
expect(before).toMatchObject({
subject: { name: "first.com", local_path: first },
});
expect(after).toMatchObject({
subject: { name: "second.com", local_path: second },
});
expect(parseEvidence(after).evidence_id).toEqual(
parseEvidence(before).evidence_id,
);
expect(
session.importEvidenceBundle(
createImmutableEvidenceBundle([
withEvidenceCallerInputPath(parseEvidence(after), "earlier-input"),
]),
).ok,
).toBe(true);
const sameTarget = await call("procedure_callees", { procedure: "0x100" });
expect(sameTarget).not.toHaveProperty("caller_input_path");
} finally {
await client.close();
await server.close();
await session.close();
}
});
@@ -0,0 +1,331 @@
import { expect, it } from "vitest";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import { createTestProviderRouter } from "../../fixtures/binarySession.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { BinarySession } from "../../../src/application/binary/BinarySession.js";
import { InvestigationRecords } from "../../../src/application/investigation/InvestigationRecords.js";
import { compareFunctionEvidence } from "../../../src/application/investigation/FunctionComparisonEvidence.js";
import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { INVESTIGATION_EXAMPLES } from "../../../src/contracts/investigationExamples.js";
import {
JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE,
JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE,
} from "../../../src/contracts/javascript/javascriptApplicationWorkflowExamples.js";
import { compareApplicationVersionsEvidence } from "../../../src/application/javascript/JavaScriptApplicationWorkflowService.js";
import { createToolResultDelivery } from "../../../src/server/toolResult.js";
import { recoveryOmissionRemediation } from "../../../src/domain/analysisErrorProjection.js";
it("preserves the source when an inline result with unknown retention exceeds delivery", async () => {
const session = new BinarySession(
createTestProviderRouter(() => {
throw new Error("no provider needed");
}),
new InvestigationRecords({ maximumRepresentationBytes: 0 }),
);
const { client } = await createApplicationMcpHarness({
session,
serverOptions: { delivery: createToolResultDelivery(10 * 1024) },
});
const response = await client.callTool({
name: "compare_application_versions",
arguments: JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE,
});
const error = parseMcpToolError(response).error;
expect(error.remediation.action).toContain(
recoveryOmissionRemediation("unknown"),
);
expect(error).toMatchObject({
code: "resource_constraint",
details: {
partial_observation: { kind: "unretained-evidence" },
completed_observation_omitted: {
reason: "mcp-response-limit",
retention: "unknown",
source_closure: "not-established",
},
},
});
});
it("does not infer ledger membership from an independently registered reader", async () => {
const computed = compareApplicationVersionsEvidence(
JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE,
);
if (!computed.ok) throw computed.error;
const sizing = new InvestigationRecords();
expect(sizing.recordEvidence(computed.value.evidence).ok).toBe(true);
const records = new InvestigationRecords({
maximumRepresentationBytes: sizing.statistics().retainedRepresentationBytes,
});
expect(records.recordEvidence(computed.value.evidence).ok).toBe(true);
const session = new BinarySession(
createTestProviderRouter(() => {
throw new Error("no provider needed");
}),
records,
);
const { client } = await createApplicationMcpHarness({ session });
const response = await client.callTool({
name: "compare_application_versions",
arguments: JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE,
});
expect(parseMcpToolError(response).error).toMatchObject({
details: {
completed_observation: {
kind: "inline-evidence",
retention: "unknown",
evidence: { evidence_id: computed.value.evidence.evidence_id },
},
},
});
expect(session.hasEvidence(computed.value.evidence.evidence_id)).toBe(true);
});
it.each([
{
name: "compare_process_captures",
input: INVESTIGATION_EXAMPLES.compare_process_captures.input,
kind: "unretained-evidence",
},
{
name: "compare_application_versions",
input: JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE,
kind: "inline-evidence",
},
] as const)(
"returns completed $name with honest retention when source recording fails",
async ({ name, input, kind }) => {
const session = new BinarySession(
createTestProviderRouter(() => {
throw new Error("no provider needed");
}),
new InvestigationRecords({ maximumRepresentationBytes: 0 }),
);
const { client } = await createApplicationMcpHarness({ session });
const response = await client.callTool({ name, arguments: input });
const error = parseMcpToolError(response).error;
expect(error).toMatchObject({
code: "resource_constraint",
details: {
partial_observation: { kind: "unretained-evidence" },
completed_observation: {
kind,
source_closure: "not-established",
evidence: { operation: name },
},
},
});
if (kind === "inline-evidence")
expect(error.details?.completed_observation).toMatchObject({
retention: "unknown",
});
expect(session.exportEvidenceBundle().records).toEqual([]);
},
);
it.each(["zero", "normal"] as const)(
"tests completed derived work at %s source capacity",
async (mode) => {
const records = new InvestigationRecords(
mode === "zero" ? { maximumRepresentationBytes: 0 } : {},
);
const session = new BinarySession(
createTestProviderRouter(() => {
throw new Error("no provider needed");
}),
records,
);
const { client } = await createApplicationMcpHarness({ session });
const input = { ...FUNCTION_COMPARISON_EXAMPLE };
const expected = compareFunctionEvidence(input);
if (!expected.ok) throw expected.error;
const response = await client.callTool({
name: "compare_functions",
arguments: input,
});
if (mode === "zero") {
expect(response.isError).toBe(true);
const error = parseMcpToolError(response).error;
expect(error).toMatchObject({
code: "resource_constraint",
details: {
partial_observation: {
kind: "unretained-evidence",
evidence: {
evidence_id: input.left.evidence_id,
operation: "analyze_function",
},
},
},
});
expect(error).toMatchObject({
details: {
completed_observation: {
kind: "unretained-evidence",
source_closure: "not-established",
evidence: { evidence_id: expected.value.evidence.evidence_id },
},
},
});
expect(records.statistics().evidenceRecords).toBe(0);
} else {
expect(response.isError).not.toBe(true);
expect(parseEvidence(response.structuredContent)).toEqual(
expected.value.evidence,
);
expect(session.hasEvidence(input.left.evidence_id)).toBe(true);
expect(session.hasEvidence(input.right.evidence_id)).toBe(true);
expect(session.hasEvidence(expected.value.evidence.evidence_id)).toBe(
true,
);
}
},
);
it("reports a preexisting primary when source admission fails", async () => {
const input = { ...FUNCTION_COMPARISON_EXAMPLE };
const expected = compareFunctionEvidence(input);
if (!expected.ok) throw expected.error;
const sizing = new InvestigationRecords();
expect(sizing.recordEvidence(expected.value.evidence).ok).toBe(true);
const records = new InvestigationRecords({
maximumRepresentationBytes: sizing.statistics().retainedRepresentationBytes,
});
expect(records.recordEvidence(expected.value.evidence).ok).toBe(true);
const session = new BinarySession(
createTestProviderRouter(() => {
throw new Error("no provider needed");
}),
records,
);
const { client } = await createApplicationMcpHarness({ session });
const response = await client.callTool({
name: "compare_functions",
arguments: input,
});
expect(response.isError).toBe(true);
expect(parseMcpToolError(response).error).toMatchObject({
code: "resource_constraint",
details: {
partial_observation: {
kind: "unretained-evidence",
evidence: { evidence_id: input.left.evidence_id },
},
completed_observation: {
kind: "retained-evidence",
source_closure: "not-established",
evidence_id: expected.value.evidence.evidence_id,
},
},
});
expect(records.statistics()).toMatchObject({
evidenceRecords: 1,
unknownRevisions: 0,
});
});
it.each([
{
name: "trace_application_feature",
input: JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
},
{
name: "compare_source_to_bundle",
input: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE,
},
] as const)(
"returns the selected $name view when source publication is refused",
async ({ name, input }) => {
const session = new BinarySession(
createTestProviderRouter(() => {
throw new Error("no provider needed");
}),
new InvestigationRecords({ maximumRepresentationBytes: 0 }),
);
const { client } = await createApplicationMcpHarness({ session });
const result = await client.callTool({
name,
arguments: { ...input, view: { kind: "summary" } },
});
const { error } = parseMcpToolError(result);
expect(error).toMatchObject({
code: "resource_constraint",
details: {
partial_observation: { kind: "unretained-evidence" },
completed_observation: {
kind: "inline-evidence",
retention: "unknown",
source_closure: "not-established",
evidence: { normalized_result: { kind: "summary" } },
},
},
});
expect(session.exportEvidenceBundle().records).toEqual([]);
},
);
it.each([
{
name: "trace_application_feature",
input: JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
},
{
name: "compare_source_to_bundle",
input: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE,
},
] as const)(
"preserves the completed $name parent when a selected view cannot be retained",
async ({ name, input }) => {
const router = () =>
createTestProviderRouter(() => {
throw new Error("no provider needed");
});
const normal = new BinarySession(router(), new InvestigationRecords());
const normalHarness = await createApplicationMcpHarness({
session: normal,
});
const complete = await normalHarness.client.callTool({
name,
arguments: input,
});
expect(complete.isError).not.toBe(true);
const parent = parseEvidence(complete.structuredContent);
const sizing = new InvestigationRecords();
expect(parent.evidence_links.length).toBeGreaterThan(0);
for (const id of parent.evidence_links) {
const source = normal.evidenceForAnalysis(id);
if (source === undefined) throw new Error(`Missing source ${id}`);
expect(sizing.recordEvidence(source).ok).toBe(true);
}
const session = new BinarySession(
router(),
new InvestigationRecords({
maximumRepresentationBytes:
sizing.statistics().retainedRepresentationBytes,
}),
);
const { client } = await createApplicationMcpHarness({ session });
const result = await client.callTool({
name,
arguments: { ...input, view: { kind: "summary" } },
});
const { error } = parseMcpToolError(result);
expect(error).toMatchObject({
code: "resource_constraint",
details: {
partial_observation: {
kind: "unretained-evidence",
evidence: { evidence_id: parent.evidence_id, operation: name },
},
},
});
expect(
session
.exportEvidenceBundle()
.records.map((record) => record.evidence_id)
.sort(),
).toEqual([...parent.evidence_links].sort());
},
);
@@ -0,0 +1,392 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
import { afterAll, expect, it } from "vitest";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { createTestProviderRouter } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { BinarySession } from "../../../src/application/binary/BinarySession.js";
import { InvestigationRecords } from "../../../src/application/investigation/InvestigationRecords.js";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
import { err, ok } from "../../../src/domain/result.js";
import { createServer } from "../../../src/server/createServer.js";
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js";
const validation = createMcpSchemaValidation();
afterAll(() => validation.clear());
it.each(
["full", "compact"].flatMap((profile) =>
["large-source", "large-primary", "invalid-source"].map((mode) => ({
profile,
mode,
})),
),
)(
"keeps sibling context with $mode ($profile schema)",
async ({ mode, profile }) => {
const id = { id: "fixture", name: "Fixture", version: "1" };
const original = createEvidence(
undefined,
id,
{
operation: "health",
parameters: {},
result:
mode === "large-source"
? "s".repeat(STDIO_DEFAULT_MAX_BUFFER_SIZE + 65536)
: "small source",
},
{ immutable: true },
);
const source =
mode === "invalid-source"
? { ...original, normalized_result: "tampered" }
: original;
const name =
mode === "large-primary"
? "p".repeat(STDIO_DEFAULT_MAX_BUFFER_SIZE + 65536)
: "primary function";
const target = join(
await createTestTempDirectory("rea-source-context-"),
"fixture.com",
);
await writeFile(target, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const router = createTestProviderRouter(() => ({
execute: async (op) =>
op === "health"
? ok(createAnalysisExecution(null, id))
: op === "address_name"
? ok(createAnalysisExecution("entry", id))
: op === "resolve_containing_procedure"
? ok(
createAnalysisExecution(
{
query_address: "0x100",
found: true,
procedure: {
address: "0x100",
name,
classification: null,
body: { available: false, reason: "not recorded" },
},
},
id,
{ sourceEvidence: [source] },
),
)
: err(
new AnalysisCapabilityUnavailableError(
id.id,
op,
"not provided",
),
),
close: async () => ok(null),
}));
const records = new InvestigationRecords({ maximumRepresentationBytes: 0 });
const session = new BinarySession(router, records);
const server = createServer(
{ kind: "session", session },
{
environment: {
...process.env,
REA_MCP_TOOL_FAMILIES: "session",
REA_MCP_OUTPUT_SCHEMA_PROFILE: profile,
},
},
);
const client = new Client({ name: "source-context", version: "1" });
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
const opened = await client.callTool({
name: "open_binary",
arguments: { path: target, format: "dos-com" },
});
expect(opened.isError, JSON.stringify(opened.content)).not.toBe(true);
const tool = (await client.listTools()).tools.find(
({ name }) => name === "inspect_address_context",
);
if (tool?.outputSchema === undefined)
throw new Error("Missing context output schema");
expect(validation.assertions.validateSchema(tool.outputSchema)).toBe(
true,
);
const result = await client.callTool(
{
name: "inspect_address_context",
arguments: { address: "0x100", document: "fixture" },
},
{ timeout: 120000, toolDefinition: tool },
);
expect(result.isError, JSON.stringify(result.content)).not.toBe(true);
expect(
validation.assertions.validate(
tool.outputSchema,
result.structuredContent,
),
JSON.stringify(validation.assertions.errors),
).toBe(true);
expect(result.structuredContent).toMatchObject({
result: {
name: { state: "available", value: "entry" },
procedure: {
state: "unavailable",
error: {
code:
mode === "invalid-source"
? "evidence_integrity_mismatch"
: "resource_constraint",
details:
mode === "large-source"
? {
partial_observation_omitted: {
reason: "mcp-response-limit",
},
completed_observation: { kind: "unretained-evidence" },
}
: mode === "large-primary"
? {
partial_observation: { kind: "unretained-evidence" },
completed_observation_omitted: {
reason: "mcp-response-limit",
},
}
: {
completed_observation: { kind: "unretained-evidence" },
},
},
},
},
});
} finally {
await client.close();
await server.close();
await session.close();
}
},
120000,
);
it.each([false, true])(
"retains a sibling source failure when cleanup fails (large source: %s)",
async (large) => {
const provider = { id: "fixture", name: "Fixture", version: "1" };
const source = createEvidence(
undefined,
provider,
{
operation: "health",
parameters: {},
result: large
? "s".repeat(STDIO_DEFAULT_MAX_BUFFER_SIZE + 65536)
: "source",
},
{ immutable: true },
);
const target = join(
await createTestTempDirectory("rea-mixed-context-"),
"fixture.com",
);
await writeFile(target, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const session = new BinarySession(
createTestProviderRouter(() => ({
execute: async (operation) =>
operation === "health"
? ok(createAnalysisExecution(null, provider))
: operation === "address_name"
? ok(
createAnalysisExecution("completed name", provider, {
sourceEvidence: [source],
}),
)
: operation === "comment"
? err(
new ProviderCleanupError(
provider.id,
["fixture-owner"],
{},
),
)
: err(
new AnalysisCapabilityUnavailableError(
provider.id,
operation,
"not provided",
),
),
close: async () => ok(null),
})),
new InvestigationRecords({ maximumRepresentationBytes: 0 }),
);
const server = createServer({ kind: "session", session });
const client = new Client({ name: "source-cleanup-context", version: "1" });
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
expect(
(
await client.callTool({
name: "open_binary",
arguments: { path: target, format: "dos-com" },
})
).isError,
).not.toBe(true);
const result = await client.callTool(
{
name: "inspect_address_context",
arguments: { address: "0x100", document: "fixture" },
},
{ timeout: 120000 },
);
const error = parseMcpToolError(result).error;
expect(error).toMatchObject({
code: "cleanup_incomplete",
details: {
resources: ["fixture-owner"],
secondary_failures: [
{
code: "resource_constraint",
details: {
...(large
? {
partial_observation_omitted: {
reason: "mcp-response-limit",
},
}
: {
partial_observation: {
kind: "unretained-evidence",
evidence: { evidence_id: source.evidence_id },
},
}),
completed_observation: {
kind: "unretained-evidence",
evidence: { normalized_result: "completed name" },
source_closure: "not-established",
},
},
},
],
},
});
} finally {
await client.close();
await server.close();
await session.close();
}
},
120000,
);
it("keeps a smaller sibling recovery when a later completed observation exceeds delivery", async () => {
const provider = { id: "fixture", name: "Fixture", version: "1" };
const source = createEvidence(
undefined,
provider,
{
operation: "health",
parameters: {},
result: "source",
},
{ immutable: true },
);
const target = join(
await createTestTempDirectory("rea-mixed-recoveries-"),
"fixture.com",
);
await writeFile(target, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const session = new BinarySession(
createTestProviderRouter(() => ({
execute: async (operation) => {
if (operation === "health")
return ok(createAnalysisExecution(null, provider));
if (operation === "address_name")
return ok(
createAnalysisExecution("small", provider, {
sourceEvidence: [source],
}),
);
if (operation === "inline_comment")
return ok(
createAnalysisExecution(
"p".repeat(STDIO_DEFAULT_MAX_BUFFER_SIZE + 65536),
provider,
{ sourceEvidence: [source] },
),
);
if (operation === "comment")
return err(
new ProviderCleanupError(provider.id, ["fixture-owner"], {}),
);
return err(
new AnalysisCapabilityUnavailableError(
provider.id,
operation,
"not provided",
),
);
},
close: async () => ok(null),
})),
new InvestigationRecords({ maximumRepresentationBytes: 0 }),
);
const server = createServer({ kind: "session", session });
const client = new Client({
name: "multiple-recovery-context",
version: "1",
});
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
expect(
(
await client.callTool({
name: "open_binary",
arguments: { path: target, format: "dos-com" },
})
).isError,
).not.toBe(true);
const result = await client.callTool(
{
name: "inspect_address_context",
arguments: { address: "0x100", document: "fixture" },
},
{ timeout: 120000 },
);
expect(parseMcpToolError(result).error).toMatchObject({
code: "cleanup_incomplete",
details: {
resources: ["fixture-owner"],
secondary_failures: [
{
details: {
partial_observation: { kind: "unretained-evidence" },
completed_observation: {
evidence: { normalized_result: "small" },
},
},
},
{
details: {
partial_observation: { kind: "unretained-evidence" },
completed_observation_omitted: { reason: "mcp-response-limit" },
},
},
],
},
});
} finally {
await client.close();
await server.close();
await session.close();
}
}, 120000);
@@ -0,0 +1,607 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
import { expect, it } from "vitest";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { createTestProviderRouter } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { BinarySession } from "../../../src/application/binary/BinarySession.js";
import { InvestigationRecords } from "../../../src/application/investigation/InvestigationRecords.js";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { createAnalysisExecutionEvidence } from "../../../src/domain/analysisObservation.js";
import { UNRETAINED_EVIDENCE_EXPORT_GUIDANCE } from "../../../src/domain/analysisErrorPresentation.js";
import { createEvidence, type Evidence } from "../../../src/domain/evidence.js";
import { safeParseEvidence } from "../../../src/domain/evidenceValidation.js";
import { ok } from "../../../src/domain/result.js";
import { createServer } from "../../../src/server/createServer.js";
const assertPublication = ({
error,
source,
primary,
sourceKind,
primaryKind,
preexisting,
differentPath,
selectedPath,
session,
}: {
error: ReturnType<typeof parseMcpToolError>["error"];
source: Evidence;
primary: Evidence | undefined;
sourceKind:
| "small"
| "invalid"
| "invalid-id"
| "invalid-json"
| "invalid-null"
| "large"
| "large-limited";
primaryKind: "small" | "large";
preexisting: boolean;
differentPath: boolean;
selectedPath: string;
session: BinarySession;
}): void => {
expect(error.code).toBe(
sourceKind === "invalid" ||
sourceKind === "invalid-id" ||
sourceKind === "invalid-json" ||
sourceKind === "invalid-null"
? "evidence_integrity_mismatch"
: "resource_constraint",
);
if (error.code === "resource_constraint") {
expect(error.remediation.action).toContain(
UNRETAINED_EVIDENCE_EXPORT_GUIDANCE,
);
}
if (sourceKind === "small")
expect(error.details).toMatchObject({
partial_observation: {
kind: "unretained-evidence",
evidence: { evidence_id: source.evidence_id },
},
});
if (sourceKind === "large" || sourceKind === "large-limited")
expect(error.details).toMatchObject({
partial_observation_omitted: {
reason: "mcp-response-limit",
evidence_id: source.evidence_id,
},
});
if (
sourceKind === "invalid" ||
sourceKind === "invalid-id" ||
sourceKind === "invalid-json" ||
sourceKind === "invalid-null"
)
expect(error.details).not.toHaveProperty("partial_observation");
if (preexisting && differentPath)
expect(error.details).toMatchObject({
completed_observation: {
kind: "unretained-evidence",
evidence: {
evidence_id: primary!.evidence_id,
subject: { local_path: selectedPath },
},
},
});
else if (preexisting)
expect(error.details).toMatchObject({
completed_observation: {
kind: "retained-evidence",
source_closure: "not-established",
evidence_id: primary!.evidence_id,
},
});
else if (sourceKind === "invalid-id" || sourceKind === "invalid-null")
expect(error.details).toMatchObject({
completed_observation: {
kind: "unretained-observation",
source_closure: "not-established",
observation: {
operation: "address_name",
result: "completed primary",
},
},
});
else if (primaryKind === "small") {
expect(error.details).toMatchObject({
completed_observation: {
kind: "unretained-evidence",
source_closure: "not-established",
evidence: {
operation: "address_name",
normalized_result: "completed primary",
},
},
});
const details = error.details;
if (details === undefined) throw new Error("Expected error details");
const record = (details.completed_observation as { evidence: unknown })
.evidence;
const parsed = safeParseEvidence(record);
if (!parsed.ok) throw parsed.error;
expect(parsed.value.evidence_links).toEqual([source.evidence_id]);
expect(session.hasEvidence(parsed.value.evidence_id)).toBe(false);
} else
expect(error.details).toMatchObject({
completed_observation_omitted: {
reason: "mcp-response-limit",
retention: "unretained",
},
});
};
it.each([
{
source: "small",
primary: "small",
preexisting: false,
differentPath: false,
},
{
source: "invalid",
primary: "small",
preexisting: false,
differentPath: false,
},
{
source: "invalid-id",
primary: "small",
preexisting: false,
differentPath: false,
},
{
source: "invalid-json",
primary: "small",
preexisting: false,
differentPath: false,
},
{
source: "invalid-null",
primary: "small",
preexisting: false,
differentPath: false,
},
{
source: "large",
primary: "small",
preexisting: false,
differentPath: false,
},
{
source: "large-limited",
primary: "small",
preexisting: false,
differentPath: false,
},
{
source: "small",
primary: "large",
preexisting: false,
differentPath: false,
},
{
source: "large",
primary: "large",
preexisting: false,
differentPath: false,
},
{
source: "large",
primary: "small",
preexisting: true,
differentPath: false,
},
{ source: "large", primary: "small", preexisting: true, differentPath: true },
] as const)(
"retains independently bounded primary and source failure ($source source, $primary primary, preexisting: $preexisting, different path: $differentPath)",
async ({
source: sourceKind,
primary: primaryKind,
preexisting,
differentPath,
}) => {
const identity = { id: "fixture", name: "Fixture", version: "1" };
const base = createEvidence(
undefined,
identity,
{
operation: "health",
parameters: {},
result:
sourceKind === "large" || sourceKind === "large-limited"
? "s".repeat(STDIO_DEFAULT_MAX_BUFFER_SIZE + 65536)
: "source fact",
},
{ immutable: true },
);
const source =
sourceKind === "invalid"
? { ...base, normalized_result: "tampered" }
: sourceKind === "invalid-null"
? (null as unknown as typeof base)
: sourceKind === "invalid-json"
? ({
...base,
normalized_result: undefined,
} as unknown as typeof base)
: sourceKind === "invalid-id"
? { ...base, evidence_id: "not-a-valid-id" }
: base;
const result =
primaryKind === "large"
? "p".repeat(STDIO_DEFAULT_MAX_BUFFER_SIZE + 65536)
: "completed primary";
const path = join(
await createTestTempDirectory("rea-source-publication-"),
"fixture.com",
);
await writeFile(path, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const router = createTestProviderRouter(() => ({
execute: async () =>
ok(
createAnalysisExecution(result, identity, {
sourceEvidence: [source],
}),
),
close: async () => ok(null),
}));
const records = new InvestigationRecords({
maximumRepresentationBytes: preexisting ? 20000 : 0,
...(sourceKind === "large-limited"
? { availableAuthenticationBytes: () => 200000 }
: {}),
});
const session = new BinarySession(router, records);
const server = createServer({ kind: "session", session });
const client = new Client({ name: "source-publication", version: "1" });
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
expect(
(
await client.callTool({
name: "open_binary",
arguments: { path, format: "dos-com" },
})
).isError,
).not.toBe(true);
const target = session.activeTarget();
if (target === undefined) throw new Error("Expected open target");
const primary = preexisting
? createAnalysisExecutionEvidence(
"address_name",
{ document: "fixture", address: "0x100" },
{
...createAnalysisExecution(result, identity, {
sourceEvidence: [source],
}),
subject: differentPath
? { ...target, path: `${path}.old` }
: target,
},
"observed",
)
: undefined;
if (primary !== undefined)
expect(session.recordEvidence(primary).ok).toBe(true);
const response = await client.callTool(
{
name: "address_name",
arguments: { document: "fixture", address: "0x100" },
},
{ timeout: 120000 },
);
expect(response.isError).toBe(true);
const error = parseMcpToolError(response).error;
assertPublication({
error,
source,
primary,
sourceKind,
primaryKind,
preexisting,
differentPath,
selectedPath: path,
session,
});
expect(records.statistics().evidenceRecords).toBe(preexisting ? 1 : 0);
expect(
(
await client.callTool({
name: "get_evidence_bundle",
arguments: { detail: "summary" },
})
).isError,
).not.toBe(true);
} finally {
await client.close();
await server.close();
await session.close();
}
},
120000,
);
it("reports omission when the primary cannot be safely authenticated", async () => {
const identity = { id: "fixture", name: "Fixture", version: "1" };
const source = createEvidence(
undefined,
identity,
{ operation: "health", parameters: {}, result: "source" },
{ immutable: true },
);
const path = join(
await createTestTempDirectory("rea-recovery-headroom-"),
"fixture.com",
);
await writeFile(path, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const router = createTestProviderRouter(() => ({
execute: async () =>
ok(
createAnalysisExecution("primary", identity, {
sourceEvidence: [source],
}),
),
close: async () => ok(null),
}));
const records = new InvestigationRecords({
maximumRepresentationBytes: 0,
availableAuthenticationBytes: () => 0,
});
const session = new BinarySession(router, records);
const server = createServer({ kind: "session", session });
const client = new Client({ name: "recovery-headroom", version: "1" });
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
expect(
(
await client.callTool({
name: "open_binary",
arguments: { path, format: "dos-com" },
})
).isError,
).not.toBe(true);
const response = await client.callTool({
name: "address_name",
arguments: { document: "fixture", address: "0x100" },
});
const error = parseMcpToolError(response).error;
expect(error).toMatchObject({
code: "resource_constraint",
details: {
partial_observation: {
kind: "unretained-evidence",
evidence: { evidence_id: source.evidence_id },
},
completed_observation_omitted: {
reason: "insufficient-authentication-headroom",
source_closure: "not-established",
},
},
});
expect(error.details).not.toHaveProperty("completed_observation");
} finally {
await client.close();
await server.close();
await session.close();
}
});
it("accounts for a wide source ID list before constructing recovery Evidence", async () => {
const identity = { id: "fixture", name: "Fixture", version: "1" };
const source = createEvidence(
undefined,
identity,
{ operation: "health", parameters: {}, result: "source" },
{ immutable: true },
);
const path = join(
await createTestTempDirectory("rea-wide-recovery-"),
"fixture.com",
);
await writeFile(path, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const sources = Array.from({ length: 5000 }, () => source);
const router = createTestProviderRouter(() => ({
execute: async () =>
ok(
createAnalysisExecution("primary", identity, {
sourceEvidence: sources,
}),
),
close: async () => ok(null),
}));
const records = new InvestigationRecords({
maximumRepresentationBytes: 0,
availableAuthenticationBytes: () => 100000,
});
const session = new BinarySession(router, records);
const server = createServer({ kind: "session", session });
const client = new Client({ name: "wide-recovery", version: "1" });
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
expect(
(
await client.callTool({
name: "open_binary",
arguments: { path, format: "dos-com" },
})
).isError,
).not.toBe(true);
const response = await client.callTool({
name: "address_name",
arguments: { document: "fixture", address: "0x100" },
});
const error = parseMcpToolError(response).error;
expect(error).toMatchObject({
code: "resource_constraint",
details: {
completed_observation_omitted: {
reason: "insufficient-authentication-headroom",
source_closure: "not-established",
},
},
});
expect(error.details).not.toHaveProperty("completed_observation");
} finally {
await client.close();
await server.close();
await session.close();
}
});
it.each(["compact", "full"] as const)(
"reports omitted primary when a retained provider source precedes an authentication refusal (%s)",
async (profile) => {
const identity = { id: "fixture", name: "Fixture", version: "1" };
const source = createEvidence(
undefined,
identity,
{ operation: "health", parameters: {}, result: "historical source" },
{ immutable: true },
);
const path = join(
await createTestTempDirectory("rea-source-primary-headroom-"),
"fixture.com",
);
await writeFile(path, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const router = createTestProviderRouter(() => ({
execute: async () =>
ok(
createAnalysisExecution("completed primary", identity, {
sourceEvidence: [source],
}),
),
close: async () => ok(null),
}));
const records = new InvestigationRecords({
maximumRepresentationBytes: 1000000,
availableAuthenticationBytes: () => 0,
});
const session = new BinarySession(router, records);
const server = createServer(
{ kind: "session", session },
{
environment: { ...process.env, REA_MCP_OUTPUT_SCHEMA_PROFILE: profile },
},
);
const client = new Client({
name: "source-primary-headroom",
version: "1",
});
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
expect(
(
await client.callTool({
name: "open_binary",
arguments: { path, format: "dos-com" },
})
).isError,
).not.toBe(true);
const response = await client.callTool({
name: "address_name",
arguments: { document: "fixture", address: "0x100" },
});
expect(response.isError).toBe(true);
expect(parseMcpToolError(response).error).toMatchObject({
code: "resource_constraint",
details: {
completed_observation_omitted: {
reason: "insufficient-authentication-headroom",
source_closure: "not-established",
},
},
});
expect(records.statistics().evidenceRecords).toBe(1);
expect(session.hasEvidence(source.evidence_id)).toBe(true);
} finally {
await client.close();
await server.close();
await session.close();
}
},
);
it("accounts for source links before admitting a completed direct observation", async () => {
const identity = { id: "fixture", name: "Fixture", version: "1" };
const source = createEvidence(
undefined,
identity,
{ operation: "health", parameters: {}, result: "source" },
{ immutable: true },
);
const path = join(
await createTestTempDirectory("rea-wide-primary-admission-"),
"fixture.com",
);
await writeFile(path, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const router = createTestProviderRouter(() => ({
execute: async () =>
ok(
createAnalysisExecution("primary", identity, {
sourceEvidence: Array.from({ length: 5000 }, () => source),
}),
),
close: async () => ok(null),
}));
const records = new InvestigationRecords({
maximumRepresentationBytes: 1000000,
availableAuthenticationBytes: () => 100000,
});
const session = new BinarySession(router, records);
const server = createServer({ kind: "session", session });
const client = new Client({ name: "wide-primary-admission", version: "1" });
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
expect(
(
await client.callTool({
name: "open_binary",
arguments: { path, format: "dos-com" },
})
).isError,
).not.toBe(true);
const response = await client.callTool({
name: "address_name",
arguments: { document: "fixture", address: "0x100" },
});
expect(response.isError).toBe(true);
const error = parseMcpToolError(response).error;
expect(error).toMatchObject({
code: "resource_constraint",
details: {
completed_observation_omitted: {
reason: "insufficient-authentication-headroom",
source_closure: "not-established",
},
},
});
expect(error.details?.reported_limits).toMatchObject({
available_heap_bytes: 100000,
});
const reported = error.details?.reported_limits as
| { projected_value_bytes: number }
| undefined;
expect(reported?.projected_value_bytes).toBeGreaterThan(100000);
expect(records.statistics().evidenceRecords).toBe(1);
expect(session.hasEvidence(source.evidence_id)).toBe(true);
} finally {
await client.close();
await server.close();
await session.close();
}
});
+46 -1
View File
@@ -15,6 +15,7 @@ import { toolContract } from "../../../src/contracts/toolContracts.js";
import type { ToolContract } from "../../../src/contracts/toolContractTypes.js";
import { err } from "../../../src/domain/result.js";
import { EvidenceIntegrityError } from "../../../src/domain/evidenceErrors.js";
import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js";
import { HopperProcessError } from "../../../src/domain/hopperErrors.js";
import { processScenarioSchema } from "../../../src/domain/process/processScenario.js";
import { createToolResultDelivery } from "../../../src/server/toolResult.js";
@@ -526,12 +527,56 @@ describe("untrusted provenance on failed tool results", () => {
err(new EvidenceIntegrityError("fixture rejects partial")),
);
expect(unrecorded.structuredContent).toMatchObject({
error: { details: { partial_observation: evidence } },
error: {
details: {
partial_observation: {
kind: "inline-evidence",
retention: "unknown",
evidence,
},
},
},
});
expect(unrecorded._meta).toEqual(UNTRUSTED_TARGET_CONTENT);
expect(original.partialObservation).toBe(evidence);
});
it("preserves cleanup details when an unretained partial exceeds the response budget", () => {
const evidence = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{
operation: "get_call_graph",
parameters: {},
result: { nodes: "x".repeat(8192) },
},
);
const error = new AnalysisCancelledError("get_call_graph", {
cleanup: { reason: "close failed", resources: ["session:fixture"] },
partialObservation: evidence,
});
const result = createToolResultDelivery(4096).toPartialErrorToolResult(
error,
true,
() => err(new EvidenceIntegrityError("fixture rejects partial")),
);
expect(result.structuredContent).toMatchObject({
error: {
code: "cleanup_incomplete",
details: {
cleanup: "incomplete",
resources: ["session:fixture"],
partial_observation_omitted: {
reason: "mcp-response-limit",
retention: "unknown",
evidence_id: evidence.evidence_id,
},
},
},
});
expect(result.content).toHaveLength(1);
});
it("marks the response-budget error that replaces an oversized target-content result", () => {
const small = createToolResultDelivery(4096);
const result = small.toCallToolResult(
@@ -51,8 +51,11 @@ import { observed } from "../fixtures/analysisExecution.js";
import {
createBinarySessionTargets,
createTestBinarySession,
createTestProviderRouter,
} from "../fixtures/binarySession.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { BinarySession } from "../../src/application/binary/BinarySession.js";
import { InvestigationRecords } from "../../src/application/investigation/InvestigationRecords.js";
import { AnalysisCleanupLease } from "../../src/artifacts/AnalysisCleanupLease.js";
const cleanupError = new ProviderCleanupError(
@@ -278,6 +281,61 @@ it.each([false, true])(
},
);
it("preserves the caller path on an authenticated record refused by the ledger", async () => {
const [path] = await createBinarySessionTargets();
const callerPath = `${dirname(path)}/./${basename(path)}`;
const records = new InvestigationRecords({ maximumRepresentationBytes: 0 });
const session = new BinarySession(
createTestProviderRouter(
() => ({
execute: async (operation) =>
observed(operation === "health" ? null : { byte: 42 }),
close: async () => ok(null),
}),
{
resolveAnalysisProfile: async () =>
ok({
profile: createAnalysisProfile(
{
id: "fixture",
name: "Fixture analysis provider",
version: "1",
},
{ fixture: true },
),
}),
},
),
records,
);
const createSession = () => session;
const result = analysisCliErrorEnvelopeSchema.parse(
await runDirectAnalysis(
{
readConfiguration: () => parseConfig({}),
createBinarySession: createSession,
createArtifactBinarySession: createSession,
createNativeDispatchBinarySession: createSession,
createNativeBinarySession: createSession,
createManagedBinarySession: createSession,
},
callerPath,
"read_bytes",
{ address: "0x100", document: "fixture" },
).then(projectCliAnalysisResult),
);
expect(result.code).toBe("resource_constraint");
const partial = jsonObjectSchema.parse(
jsonObjectSchema.parse(result.details).partial_observation,
);
expect(partial.kind).toBe("unretained-evidence");
const evidence = parseEvidence(partial.evidence);
expect(evidence.caller_input_path).toBe(callerPath);
expect(evidence.subject?.local_path).toBe(path);
expect(evidence.normalized_result).toEqual({ byte: 42 });
expect(records.statistics().evidenceRecords).toBe(0);
});
describe("one-shot finalization cancellation", () => {
it.each(["read_bytes", "analyze_function"] as const)(
"retains completed %s Evidence when cancellation arrives during final close",
@@ -323,8 +381,11 @@ describe("one-shot finalization cancellation", () => {
async (closeFails) => {
const [path] = await createBinarySessionTargets();
const controller = new AbortController();
const failure = new AnalysisTimeoutError("read_bytes", 100, {
capturedOutput: primaryError.capturedOutput,
});
const { dependencies } = factories({
executionError: primaryError,
executionError: failure,
onClose: () => controller.abort({ later: true }),
...(closeFails ? { cleanupError } : {}),
});
@@ -337,9 +398,9 @@ describe("one-shot finalization cancellation", () => {
).then(projectCliAnalysisResult);
expect(result).toMatchObject({
code: closeFails ? "cleanup_incomplete" : "provider_timeout",
message: projectAnalysisError(primaryError).message,
message: projectAnalysisError(failure).message,
details: {
captured_output: primaryError.capturedOutput,
captured_output: failure.capturedOutput,
...(closeFails
? { cleanup_error: projectAnalysisError(cleanupError) }
: {}),
@@ -709,12 +770,13 @@ it.each(["complete", "summary"] as const)(
it("preserves a managed provider failure when cancellation arrives during close", async () => {
const [path] = await createBinarySessionTargets();
const controller = new AbortController();
const reason = new Error("later cancellation");
const failure = new AnalysisInputError("inspect_managed_members", undefined, [
{ path: ["path"], reason: "invalid_format" },
]);
const { dependencies, session } = factories({
executionError: failure,
onClose: () => controller.abort(new Error("later cancellation")),
onClose: () => controller.abort(reason),
});
const result = await runProviderAnalysis(
dependencies,
@@ -728,6 +790,13 @@ it("preserves a managed provider failure when cancellation arrives during close"
error: "Analysis failed",
...projectAnalysisError(failure),
});
expect(
failure.secondaryFailures.some(
(secondary) =>
secondary instanceof AnalysisCancelledError &&
secondary.cause === reason,
),
).toBe(true);
expect(session.activeTarget()).toBeUndefined();
});
@@ -0,0 +1,491 @@
import { expect, it, vi } from "vitest";
import { writeFile } from "node:fs/promises";
import { dirname, basename, join } from "node:path";
import {
createBinarySessionTargets,
createTestProviderRouter,
} from "../fixtures/binarySession.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { BinarySession } from "../../src/application/binary/BinarySession.js";
import { InvestigationRecords } from "../../src/application/investigation/InvestigationRecords.js";
import {
runDirectAnalysis,
runProviderAnalysis,
} from "../../src/application/DirectAnalysis.js";
import type { DirectAnalysisDependencies } from "../../src/application/DirectAnalysisDependencies.js";
import { createAnalysisExecution } from "../../src/application/AnalysisProvider.js";
import { createAnalysisExecutionEvidence } from "../../src/domain/analysisObservation.js";
import { createEvidence } from "../../src/domain/evidence.js";
import { parseConfig } from "../../src/config/parseConfig.js";
import { err, ok } from "../../src/domain/result.js";
import { ProviderCleanupError } from "../../src/domain/providerCleanupError.js";
import { primaryAnalysisError } from "../../src/domain/analysisErrorBase.js";
import { AnalysisCancelledError } from "../../src/domain/analysisErrorCore.js";
it.each(["inspect_macho", "inspect_managed_members"] as const)(
"recovers %s Evidence and caller input identity after source admission fails",
async (tool) => {
const [target] = await createBinarySessionTargets();
const path = `${dirname(target)}/./${basename(target)}`;
const provider = { id: "fixture", name: "Fixture", version: "1" };
const source = createEvidence(undefined, provider, {
operation: "health",
parameters: {},
result: "large".repeat(20000),
});
const calls: Readonly<Record<string, unknown>>[] = [];
const run = async (maximumRepresentationBytes: number) => {
const session = new BinarySession(
createTestProviderRouter(() => ({
execute: async (operation, parameters) => {
if (operation !== "health") calls.push(parameters);
return ok(
createAnalysisExecution(
operation === "health" ? null : "completed",
provider,
operation === "health" ? {} : { sourceEvidence: [source] },
),
);
},
close: async () => ok(null),
})),
new InvestigationRecords({ maximumRepresentationBytes }),
);
const create = () => session;
const dependencies: DirectAnalysisDependencies = {
readConfiguration: () => parseConfig({}),
createBinarySession: create,
createNativeDispatchBinarySession: create,
createArtifactBinarySession: create,
createNativeBinarySession: create,
createManagedBinarySession: create,
};
return runProviderAnalysis(dependencies, path, tool, {});
};
const success = await run(500000);
if (!success.ok) throw new Error(JSON.stringify(success.error));
const refused = await run(20000);
if (refused.ok) throw new Error("Expected source admission failure");
expect(refused.error).toMatchObject({
details: {
completed_observation: {
evidence: {
evidence_id: success.value.evidence_id,
parameters: { input_path: path },
caller_input_path: path,
},
},
},
});
expect(calls).toEqual([{}, {}]);
},
);
it("delivers a preexisting completed record inline after one-shot cleanup", async () => {
const provider = { id: "fixture", name: "Fixture", version: "1" };
const source = createEvidence(
undefined,
provider,
{ operation: "health", parameters: {}, result: "large".repeat(20000) },
{ immutable: true },
);
const path = join(
await createTestTempDirectory("rea-oneshot-source-"),
"fixture.com",
);
await writeFile(path, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const router = createTestProviderRouter(() => ({
execute: async (op) =>
ok(
createAnalysisExecution(
op === "health" ? null : "completed",
provider,
op === "health" ? {} : { sourceEvidence: [source] },
),
),
close: async () => ok(null),
}));
const records = new InvestigationRecords({
maximumRepresentationBytes: 20000,
});
const session = new BinarySession(router, records);
const opened = await session.open(path, { formatHint: "dos-com" });
if (!opened.ok) throw opened.error;
const args = { address: "0x100", length: 4 };
const primary = createAnalysisExecutionEvidence(
"read_bytes",
args,
{
...createAnalysisExecution("completed", provider, {
sourceEvidence: [source],
}),
subject: opened.value,
},
"observed",
);
expect(session.recordEvidence(primary).ok).toBe(true);
const create = () => session;
const dependencies: DirectAnalysisDependencies = {
readConfiguration: () => parseConfig({}),
createBinarySession: create,
createNativeDispatchBinarySession: create,
createArtifactBinarySession: create,
createNativeBinarySession: create,
createManagedBinarySession: create,
};
const result = await runDirectAnalysis(
dependencies,
path,
"read_bytes",
args,
{ formatHint: "dos-com" },
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected source failure");
expect(result.error).toMatchObject({
code: "resource_constraint",
details: {
completed_observation: {
kind: "inline-evidence",
retention: "one-shot-session",
source_closure: "not-established",
evidence: {
evidence_id: primary.evidence_id,
caller_input_path: path,
normalized_result: "completed",
},
},
},
});
});
it("keeps caller spelling on a raw observation when a source ID is malformed", async () => {
const provider = { id: "fixture", name: "Fixture", version: "1" };
const base = createEvidence(undefined, provider, {
operation: "health",
parameters: {},
result: "source",
});
const source = { ...base, evidence_id: "invalid" };
const path = join(
await createTestTempDirectory("rea-oneshot-invalid-source-"),
"fixture.com",
);
await writeFile(path, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const router = createTestProviderRouter(() => ({
execute: async (op) =>
ok(
createAnalysisExecution(
op === "health" ? null : "completed",
provider,
op === "health" ? {} : { sourceEvidence: [source] },
),
),
close: async () => ok(null),
}));
const session = new BinarySession(router);
const create = () => session;
const dependencies: DirectAnalysisDependencies = {
readConfiguration: () => parseConfig({}),
createBinarySession: create,
createNativeDispatchBinarySession: create,
createArtifactBinarySession: create,
createNativeBinarySession: create,
createManagedBinarySession: create,
};
const result = await runDirectAnalysis(
dependencies,
path,
"read_bytes",
{ address: "0x100", length: 4 },
{ formatHint: "dos-com" },
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected source failure");
expect(result.error).toMatchObject({
code: "evidence_integrity_mismatch",
details: {
completed_observation: {
kind: "unretained-observation",
source_closure: "not-established",
observation: {
operation: "read_bytes",
result: "completed",
caller_input_path: path,
},
},
},
});
});
it.each([false, true])(
"keeps the source failure and primary when cancellation arrives during final close (close fails: %s)",
async (closeFails) => {
const provider = { id: "fixture", name: "Fixture", version: "1" };
const source = createEvidence(
undefined,
provider,
{ operation: "health", parameters: {}, result: "large".repeat(20000) },
{ immutable: true },
);
const path = join(
await createTestTempDirectory("rea-source-final-close-"),
"fixture.com",
);
await writeFile(path, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const session = new BinarySession(
createTestProviderRouter(() => ({
execute: async (op) =>
ok(
createAnalysisExecution(
op === "health" ? null : "completed",
provider,
op === "health" ? {} : { sourceEvidence: [source] },
),
),
close: async () => ok(null),
})),
new InvestigationRecords({ maximumRepresentationBytes: 20000 }),
);
const opened = await session.open(path, { formatHint: "dos-com" });
if (!opened.ok) throw opened.error;
const args = { address: "0x100", length: 4 };
const primary = createAnalysisExecutionEvidence(
"read_bytes",
args,
{
...createAnalysisExecution("completed", provider, {
sourceEvidence: [source],
}),
subject: opened.value,
},
"observed",
);
expect(session.recordEvidence(primary).ok).toBe(true);
const create = () => session;
const dependencies: DirectAnalysisDependencies = {
readConfiguration: () => parseConfig({}),
createBinarySession: create,
createNativeDispatchBinarySession: create,
createArtifactBinarySession: create,
createNativeBinarySession: create,
createManagedBinarySession: create,
};
const controller = new AbortController();
const reason = { during: "final close" };
const recordEvidence = vi.spyOn(session, "recordEvidence");
const nativeClose = session.close.bind(session);
const failure = new ProviderCleanupError("fixture", ["fixture-owner"], {});
const close = vi
.spyOn(session, "close")
.mockImplementationOnce(async () => {
controller.abort(reason);
return closeFails ? err(failure) : nativeClose();
});
try {
const result = await runDirectAnalysis(
dependencies,
path,
"read_bytes",
args,
{
formatHint: "dos-com",
signal: controller.signal,
},
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected source failure");
expect(result.error).toMatchObject({
code: closeFails ? "cleanup_incomplete" : "resource_constraint",
details: {
partial_observation: {
kind: "unretained-evidence",
evidence: { evidence_id: source.evidence_id },
},
completed_observation: {
kind: "inline-evidence",
retention: "one-shot-session",
source_closure: "not-established",
evidence: {
evidence_id: primary.evidence_id,
caller_input_path: path,
normalized_result: "completed",
},
},
...(closeFails
? {
resources: ["fixture-owner"],
cleanup: "incomplete",
execution_failure: "resource_constraint",
}
: {}),
},
});
const sourceFailure = recordEvidence.mock.results
.map(({ value }) => value)
.find((value) => value !== undefined && !value.ok);
expect(sourceFailure).toBeDefined();
expect(
primaryAnalysisError(sourceFailure!.error).secondaryFailures,
).toContainEqual(expect.objectContaining({ cause: reason }));
expect(
primaryAnalysisError(sourceFailure!.error).secondaryFailures.some(
(failure) =>
failure instanceof AnalysisCancelledError &&
failure.cause === reason,
),
).toBe(true);
expect(result.error).toMatchObject({
details: {
secondary_failures: [expect.objectContaining({ code: "cancelled" })],
},
});
expect(session.activeTarget() !== undefined).toBe(closeFails);
} finally {
close.mockRestore();
const closed = await nativeClose();
if (!closed.ok) throw closed.error;
expect(session.activeTarget()).toBeUndefined();
}
},
);
it("reports an omitted completed one-shot result when authentication headroom is exhausted", async () => {
const provider = { id: "fixture", name: "Fixture", version: "1" };
const source = createEvidence(
undefined,
provider,
{ operation: "health", parameters: {}, result: "historical source" },
{ immutable: true },
);
const path = join(
await createTestTempDirectory("rea-oneshot-headroom-"),
"fixture.com",
);
await writeFile(path, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const router = createTestProviderRouter(() => ({
execute: async () =>
ok(
createAnalysisExecution("completed", provider, {
sourceEvidence: [source],
}),
),
close: async () => ok(null),
}));
const records = new InvestigationRecords({
maximumRepresentationBytes: 1000000,
availableAuthenticationBytes: () => 0,
});
const create = () => new BinarySession(router, records);
const dependencies: DirectAnalysisDependencies = {
readConfiguration: () => parseConfig({}),
createBinarySession: create,
createNativeDispatchBinarySession: create,
createArtifactBinarySession: create,
createNativeBinarySession: create,
createManagedBinarySession: create,
};
const result = await runDirectAnalysis(
dependencies,
path,
"procedure_pseudo_code",
{ procedure: "0x100" },
{ formatHint: "dos-com" },
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected admission failure");
expect(result.error).toMatchObject({
code: "resource_constraint",
details: {
completed_observation_omitted: {
reason: "insufficient-authentication-headroom",
source_closure: "not-established",
},
},
});
expect(records.statistics().evidenceRecords).toBe(1);
});
it.each([false, true])(
"preserves admission omission during final-close cancellation (close fails: %s)",
async (closeFails) => {
const provider = { id: "fixture", name: "Fixture", version: "1" };
const source = createEvidence(
undefined,
provider,
{ operation: "health", parameters: {}, result: "history" },
{ immutable: true },
);
const path = join(
await createTestTempDirectory("rea-admission-final-close-"),
"fixture.com",
);
await writeFile(path, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const session = new BinarySession(
createTestProviderRouter(() => ({
execute: async () =>
ok(
createAnalysisExecution("completed", provider, {
sourceEvidence: [source],
}),
),
close: async () => ok(null),
})),
new InvestigationRecords({
maximumRepresentationBytes: 1000000,
availableAuthenticationBytes: () => 0,
}),
);
const create = () => session;
const dependencies: DirectAnalysisDependencies = {
readConfiguration: () => parseConfig({}),
createBinarySession: create,
createNativeDispatchBinarySession: create,
createArtifactBinarySession: create,
createNativeBinarySession: create,
createManagedBinarySession: create,
};
const controller = new AbortController();
const originalClose = session.close.bind(session);
const failure = new ProviderCleanupError("fixture", ["fixture-owner"], {});
const close = vi
.spyOn(session, "close")
.mockImplementationOnce(async () => {
controller.abort({ during: "final close" });
return closeFails ? err(failure) : originalClose();
});
try {
const result = await runDirectAnalysis(
dependencies,
path,
"procedure_pseudo_code",
{ procedure: "0x100" },
{ formatHint: "dos-com", signal: controller.signal },
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected admission failure");
expect(result.error).toMatchObject({
code: closeFails ? "cleanup_incomplete" : "resource_constraint",
details: {
completed_observation_omitted: {
reason: "insufficient-authentication-headroom",
source_closure: "not-established",
},
secondary_failures: [expect.objectContaining({ code: "cancelled" })],
...(closeFails
? {
resources: ["fixture-owner"],
cleanup: "incomplete",
execution_failure: "resource_constraint",
}
: {}),
},
});
expect(session.activeTarget() !== undefined).toBe(closeFails);
} finally {
close.mockRestore();
const closed = await originalClose();
if (!closed.ok) throw closed.error;
}
},
);