mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00be6e769b | ||
|
|
5162edf1ef | ||
|
|
01757d8ec8 | ||
|
|
f107d0d507 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
>;
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
{
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in new issue
Block a user