Compare commits

...
14 changed files with 237 additions and 4 deletions

No files matched your search

+8
View File
@@ -1027,6 +1027,14 @@ concrete version, selection source, and complete analysis profile through
choices return typed selection details. A selected provider is never replaced
automatically after a runtime failure.
When the selected provider reports `release_verification` in its binding,
the status compares its reported release version at binding with the provider's
verified baseline. It does not recheck the executable, authenticate its bytes,
or guarantee results.
An unverified release may still be usable; the optional remediation preserves
the caller's choice. Omission means verification was not reported. Target-free
discovery does not probe a version solely to populate this fact.
When an admitted `open_binary` attempt fails, `error.details.selection_at_failure`
records the canonical binary path and SHA-256, resource root (including its
caller-selected path), or absence of selection at
+33
View File
@@ -26,6 +26,11 @@ import { parseArgs, promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { inspectKunaInstallation } from "../dist/kuna/KunaInstallation.js";
import {
KUNA_RELEASE_VERIFICATION_EXPLANATION,
KUNA_UNVERIFIED_VERSION_REMEDIATION,
KUNA_VERIFIED_VERSION,
} from "../dist/kuna/KunaRelease.js";
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
import { AnalysisCleanupLease } from "../dist/artifacts/AnalysisCleanupLease.js";
import {
@@ -277,6 +282,34 @@ const openServer = async (provider, kunaSettings = {}) => {
path: provider.path,
provider_id: provider.id,
});
if (provider.id === "kuna") {
const status = (await call("binary_session")).value;
const binding = status.analysis_provider_binding;
const verification = binding.release_verification;
assert.equal(verification.basis, "reported_version");
assert.equal(verification.verified_version, KUNA_VERIFIED_VERSION);
assert.equal(
verification.reported_version,
binding.analysis_profile.parameters.kuna_version,
);
assert.equal(
verification.status,
binding.analysis_profile.parameters.kuna_version ===
KUNA_VERIFIED_VERSION
? "verified"
: "unverified",
);
assert.equal(
verification.explanation,
KUNA_RELEASE_VERIFICATION_EXPLANATION,
);
if (verification.status === "unverified")
assert.equal(
verification.remediation,
KUNA_UNVERIFIED_VERSION_REMEDIATION,
);
else assert.equal(verification.remediation, null);
}
} catch (cause) {
// A rejected connection/target still owns its transport and workspace.
await close(cause);
+4
View File
@@ -27,6 +27,7 @@ import type { ProgressReporter } from "./ProgressReporter.js";
import type { ProviderRejectionCode } from "../domain/providerSelection.js";
import type { ProcessLineageObservation } from "../process/ProcessOwnership.js";
import type { ProviderOperationHealth } from "../domain/providerOperationHealth.js";
import type { ProviderReleaseVerification } from "../domain/providerReleaseVerification.js";
import {
relationshipCoverageSchema,
type RelationshipCoverage,
@@ -249,6 +250,9 @@ export interface ProviderIdentity {
/** Typed analysis profile resolved before provider startup; null means unresolved. */
export interface AnalysisProfileResolution {
readonly profile: AnalysisProfileCommitment | null;
/** Readiness fact for the resolved selection; omitted means not reported. */
readonly releaseVerification?: ProviderReleaseVerification;
/** Closing a client can publish state that invalidates a previewed profile. */
readonly refreshAfterRetirement?: true;
/** Keep provider-private facts bound to this selection without Evidence serialization. */
@@ -17,6 +17,11 @@ import type {
} from "../AnalysisProvider.js";
import type { AnalysisProviderCandidateStatus } from "./AnalysisProviderRegistry.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import {
providerReleaseVerificationSchema,
type ProviderReleaseVerification,
} from "../../domain/providerReleaseVerification.js";
import { analysisErrorRemediationAction } from "../../domain/analysisErrorPresentation.js";
/** Profile-resolution truth for one discovered provider candidate. */
@@ -26,6 +31,7 @@ export interface AnalysisProviderCandidateEvaluation {
readonly profile?: AnalysisProfileCommitment;
readonly refreshAfterRetirement?: true;
readonly createClient?: AnalysisProviderBoundClientFactory;
readonly releaseVerification?: ProviderReleaseVerification;
readonly profileError?: AnalysisError;
}
@@ -115,10 +121,29 @@ export const evaluateAnalysisProviderCandidate = async (
remediation: PROFILE_REMEDIATION,
}),
);
const verification =
resolved.value.releaseVerification === undefined
? undefined
: providerReleaseVerificationSchema.safeParse(
resolved.value.releaseVerification,
);
if (verification?.success === false)
return ok(
rejectedProfile(candidate, status, {
reason: "Provider returned invalid release verification",
remediation: PROFILE_REMEDIATION,
}),
);
return ok({
candidate,
status,
profile,
...(verification === undefined
? {}
: {
releaseVerification: verification.data,
}),
...(resolved.value.refreshAfterRetirement === undefined
? {}
: { refreshAfterRetirement: resolved.value.refreshAfterRetirement }),
@@ -13,6 +13,7 @@ import {
type AnalysisProviderSelector,
} from "../../domain/providerSelection.js";
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type { ProviderReleaseVerification } from "../../domain/providerReleaseVerification.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import {
AnalysisCancelledError,
@@ -80,6 +81,8 @@ export interface AnalysisProviderBinding {
readonly identity: ProviderIdentity & { readonly version: string };
readonly selectionSource: "request" | "environment" | "auto-single-candidate";
readonly profile: AnalysisProfileCommitment;
readonly releaseVerification?: ProviderReleaseVerification;
readonly refreshAfterRetirement?: true;
readonly createClient?: AnalysisProviderBoundClientFactory;
/** The provider's capabilities, attributed to its resolved identity. */
@@ -832,6 +835,12 @@ const selectedResult = (
identity: structuredClone(identity),
selectionSource: source,
profile: structuredClone(evaluated.profile),
...(evaluated.releaseVerification === undefined
? {}
: {
releaseVerification: structuredClone(evaluated.releaseVerification),
}),
...(evaluated.refreshAfterRetirement === undefined
? {}
: { refreshAfterRetirement: evaluated.refreshAfterRetirement }),
@@ -108,6 +108,13 @@ export const binarySessionStatus = async ({
provider: providerSummary(route.binding.identity),
selection_source: route.binding.selectionSource,
analysis_profile: structuredClone(route.binding.profile),
...(route.binding.releaseVerification === undefined
? {}
: {
release_verification: structuredClone(
route.binding.releaseVerification,
),
}),
};
const analysisProviderCandidates =
bundleResourceRoot !== undefined
+1 -1
View File
@@ -88,7 +88,7 @@ export const SESSION_TOOL_CONTRACTS = [
),
session(
"binary_session",
'Report the active binary target or resource-only app root, provider and capability availability when applicable, client-feature, analysis, and server-identity status without starting analysis. Start with the default summary: after selecting Ghidra, capabilities[].shared_limitations already reports enabled checkpoint or ephemeral mode. Request detail: "complete" only for provider diagnostics, candidate capabilities or static tool flags; omitted_fields lists what summary leaves out.',
'Report the active binary target or resource-only app root, provider and capability availability when applicable, client-feature, analysis, and server-identity status without starting analysis. A selected provider may report release_verification under its binding; omission means not reported, and reported-version verification does not authenticate executable bytes. Start with the default summary: after selecting Ghidra, capabilities[].shared_limitations already reports enabled checkpoint or ephemeral mode. Request detail: "complete" only for provider diagnostics, candidate capabilities or static tool flags; omitted_fields lists what summary leaves out.',
binarySessionInputSchema,
),
session(
@@ -1,5 +1,6 @@
import { procedureInfoSchema } from "../domain/native/procedureInfo.js";
import { z } from "zod";
import { providerReleaseVerificationSchema } from "../domain/providerReleaseVerification.js";
import { investigationStatisticsSchema } from "../domain/investigationLifecycle.js";
import { utcDateTimeSchema } from "../domain/offsetDateTime.js";
import { analysisCleanupRetrySchema } from "../domain/analysisErrorRecovery.js";
@@ -565,6 +566,7 @@ export const sessionProviderFacts = z
"auto-single-candidate",
]),
analysis_profile: analysisProfileSchema,
release_verification: providerReleaseVerificationSchema.optional(),
})
.nullable(),
client_features: z.object({
+19
View File
@@ -0,0 +1,19 @@
import { z } from "zod";
/** Provider-owned verification of a reported release version, not executable authenticity. */
export const providerReleaseVerificationSchema = z
.strictObject({
status: z.enum(["verified", "unverified"]),
basis: z.literal("reported_version"),
reported_version: z.string().min(1),
verified_version: z.string().min(1),
explanation: z.string().min(1),
remediation: z.string().min(1).nullable(),
})
.describe(
"Provider-reported release version comparison at binding, not a fresh executable check; this does not authenticate bytes or prove target correctness. Omission means verification was not reported.",
);
export type ProviderReleaseVerification = z.infer<
typeof providerReleaseVerificationSchema
>;
+2 -1
View File
@@ -20,6 +20,7 @@ import {
KUNA_COMMAND_REMEDIATION,
KUNA_PROVIDER_ID,
KUNA_UNSUPPORTED_HOST_REMEDIATION,
KUNA_UNVERIFIED_VERSION_REMEDIATION,
KUNA_VERIFIED_VERSION,
KUNA_VERSION_PROBE_REMEDIATION,
} from "./KunaRelease.js";
@@ -140,7 +141,7 @@ export const inspectKunaProvider = async (
const cache = await cacheCheck(environment, platform);
const versionGuidance = value.verified
? null
: `To use the verified release, select Kuna ${KUNA_VERIFIED_VERSION} by setting REA_KUNA_COMMAND to its absolute executable path in the environment that starts REA, then restart REA. You may continue with the selected unverified release.`;
: KUNA_UNVERIFIED_VERSION_REMEDIATION;
return {
id: KUNA_PROVIDER_ID,
configured,
+19 -1
View File
@@ -42,7 +42,13 @@ import {
import { KunaProjectCache } from "./KunaProjectCache.js";
import { kunaCapabilities } from "./KunaProviderCapabilities.js";
import { KUNA_PROVIDER_IDENTITY } from "./KunaProviderMetadata.js";
import { KUNA_COMMAND_REMEDIATION, KUNA_PROVIDER_ID } from "./KunaRelease.js";
import {
KUNA_COMMAND_REMEDIATION,
KUNA_PROVIDER_ID,
KUNA_RELEASE_VERIFICATION_EXPLANATION,
KUNA_UNVERIFIED_VERSION_REMEDIATION,
KUNA_VERIFIED_VERSION,
} from "./KunaRelease.js";
import { KunaRunSlots } from "./KunaRunSlots.js";
import { KunaSessionClient } from "./KunaSessionClient.js";
import { kunaUnavailable } from "./KunaUnavailable.js";
@@ -258,6 +264,18 @@ export class KunaProvider implements AnalysisProviderCandidate {
}
const { value } = installation;
return ok({
releaseVerification: {
status: value.verified
? ("verified" as const)
: ("unverified" as const),
basis: "reported_version" as const,
reported_version: value.version,
verified_version: KUNA_VERIFIED_VERSION,
explanation: KUNA_RELEASE_VERIFICATION_EXPLANATION,
remediation: value.verified
? null
: KUNA_UNVERIFIED_VERSION_REMEDIATION,
},
profile: createAnalysisProfile(
{ ...KUNA_PROVIDER_IDENTITY, version: value.version },
{
+7
View File
@@ -1,6 +1,13 @@
/** Kuna release whose CLI and project layout REA verified; others report as unverified. */
export const KUNA_VERIFIED_VERSION = "1.990";
/** Scope of the version comparison, shared by session output and verification. */
export const KUNA_RELEASE_VERIFICATION_EXPLANATION =
"At selection, REA compared the reported version with its baseline, checked on macOS arm64. This does not authenticate or recheck the executable or guarantee support or results on this host.";
/** Optional selection guidance; the current unverified release remains usable. */
export const KUNA_UNVERIFIED_VERSION_REMEDIATION = `To use the verified release, select Kuna ${KUNA_VERIFIED_VERSION} by setting REA_KUNA_COMMAND to its absolute executable path in the environment that starts REA, then restart REA. You may continue with the selected unverified release.`;
/** Stable provider ID shared by readiness, doctor and Evidence. */
export const KUNA_PROVIDER_ID = "kuna";
@@ -875,6 +875,98 @@ const structured = (result: CallToolResult): Record<string, unknown> =>
? parseMcpToolError(result)
: z.record(z.string(), z.unknown()).parse(result.structuredContent);
it.each(["full", "compact"] as const)(
"keeps provider-owned release verification bound to the selected session (%s)",
async (profile) => {
directory = await createTestTempDirectory("rea-provider-verification-");
const target = join(directory, "fixture.hop");
await writeFile(target, "fixture");
const starts: string[] = [];
const alpha = candidate("alpha", starts);
const verification = {
status: "unverified" as const,
basis: "reported_version" as const,
reported_version: "1",
verified_version: "2",
explanation: "The reported version does not authenticate the executable.",
remediation:
"Continue with the selected version or choose another explicitly.",
};
const session = composeBinarySession(
new AnalysisProviderRegistry([
{
...alpha,
resolveAnalysisProfile: async (...args) => {
const resolved = await alpha.resolveAnalysisProfile(...args);
return resolved.ok
? ok({ ...resolved.value, releaseVerification: verification })
: resolved;
},
},
candidate("beta", starts),
]),
);
const server = createServer(
{ kind: "session", session },
{
logger: silentLogger,
environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: profile },
},
);
const mcp = new Client(
{ name: "provider-verification", version: "1" },
{
jsonSchemaValidator: new AjvJsonSchemaValidator(),
},
);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push(mcp, server, { close: () => session.close() });
await server.connect(serverTransport);
await mcp.connect(clientTransport);
const definitions = new Map(
(await mcp.listTools()).tools.map((tool) => [tool.name, tool]),
);
const call = (name: string, args: Record<string, unknown> = {}) => {
const definition = definitions.get(name);
if (definition === undefined) throw new Error(`Missing tool ${name}`);
return mcp.callTool(
{ name, arguments: args },
{ toolDefinition: definition },
);
};
expect(structured(await call("binary_session")).result).toMatchObject({
analysis_provider_binding: null,
});
expect(
(await call("open_binary", { path: target, provider_id: "alpha" }))
.isError,
).toBeFalsy();
for (const detail of ["summary", "complete"]) {
expect(
structured(await call("binary_session", { detail })).result,
).toMatchObject({
analysis_provider_binding: {
provider: { id: "alpha" },
release_verification: verification,
},
});
}
expect(
(await call("open_binary", { path: target, provider_id: "beta" }))
.isError,
).toBeFalsy();
const status = toolContract("binary_session").outputSchema.parse(
(await call("binary_session")).structuredContent,
);
expect(status.result.analysis_provider_binding?.provider.id).toBe("beta");
expect(status.result.analysis_provider_binding).not.toHaveProperty(
"release_verification",
);
expect((await call("close_binary")).isError).toBeFalsy();
},
);
const checkQueuedSwitchReceipt = async (
mcp: Client,
session: ReturnType<typeof composeBinarySession>,
@@ -5,6 +5,8 @@ import { pathToFileURL } from "node:url";
import { expect, it as test } from "vitest";
import { KUNA_VERIFIED_VERSION } from "../../../../src/kuna/KunaRelease.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { fakeKunaCalls, writeFakeKuna } from "./fakeKuna.js";
@@ -116,36 +118,42 @@ it.each([
rejectRead: true,
wrongXrefs: false,
wrongStrings: false,
version: KUNA_VERIFIED_VERSION,
},
{
rejectClose: true,
rejectRead: false,
wrongXrefs: false,
wrongStrings: false,
version: "1.833",
},
{
rejectClose: true,
rejectRead: true,
wrongXrefs: false,
wrongStrings: false,
version: "1.833",
},
{
rejectClose: false,
rejectRead: false,
wrongXrefs: true,
wrongStrings: false,
version: "1.833",
},
{
rejectClose: false,
rejectRead: false,
wrongXrefs: false,
wrongStrings: true,
version: "1.833",
},
])(
"reports failure-path lineage (cleanup: $rejectClose, read: $rejectRead, wrong xrefs: $wrongXrefs, wrong strings: $wrongStrings)",
async ({ rejectClose, rejectRead, wrongXrefs, wrongStrings }) => {
async ({ rejectClose, rejectRead, wrongXrefs, wrongStrings, version }) => {
const root = await createTestTempDirectory("rea-kuna-verifier-cleanup-");
const kuna = await writeFakeKuna(root, {
version: { stdout: `kuna ${version}\n` },
extraXrefSources: [4104],
stringRows: [
{ address: 12288, text: "REA fixture prompt" },