mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 05:16:16 +00:00
Profile resolution gives Ghidra and Hopper a concrete version, but the selected candidate's capabilities and the session route kept the provider's static identity with a null version. binary_session therefore grouped capability sets under a different identity than the binding and the selected candidate. The registry now rebinds the selected provider's capabilities to the resolved identity once, on the binding, and the route uses them. Evidence provenance already compensated downstream by substituting the profile identity; that repair is removed because the route now carries the resolved identity. Fixes #2009 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
420 lines
14 KiB
TypeScript
420 lines
14 KiB
TypeScript
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
import {
|
|
createAnalysisExecution,
|
|
type AnalysisClient,
|
|
type AnalysisOperation,
|
|
type AnalysisProvider,
|
|
type AnalysisProviderCandidate,
|
|
type CapabilityDescriptor,
|
|
type ProviderIdentity,
|
|
} from "../../../src/application/AnalysisProvider.js";
|
|
import { AnalysisProviderRegistry } from "../../../src/application/binary/AnalysisProviderRegistry.js";
|
|
import type { BinarySession } from "../../../src/application/binary/BinarySession.js";
|
|
import { composeBinarySession } from "../../../src/application/binary/BinarySessionComposition.js";
|
|
import { SessionProviderRouter } from "../../../src/application/binary/SessionProviderRouter.js";
|
|
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
|
|
import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js";
|
|
import { ProviderAdapterError } from "../../../src/domain/providerAdapterError.js";
|
|
import { err, ok } from "../../../src/domain/result.js";
|
|
|
|
type DeclaredOperation = Exclude<AnalysisOperation, "health">;
|
|
|
|
describe("target-bound provider routing", () => {
|
|
it("rejects provider identity and operation-family collisions at composition", () => {
|
|
const alpha = deepProvider("alpha");
|
|
const registry = new AnalysisProviderRegistry([alpha.provider]);
|
|
expect(() =>
|
|
SessionProviderRouter.selectable(registry, [auxiliaryProvider("alpha")]),
|
|
).toThrow(/Duplicate configured provider ID: alpha/u);
|
|
expect(() =>
|
|
SessionProviderRouter.selectable(registry, [
|
|
auxiliaryProvider("auxiliary", "address_name"),
|
|
]),
|
|
).toThrow(
|
|
/Deep and auxiliary providers both declare operation address_name/u,
|
|
);
|
|
});
|
|
|
|
it("binds one overlapping provider, preserves provenance, and never falls back", async () => {
|
|
const target = await databaseTarget();
|
|
const alpha = deepProvider("alpha");
|
|
const beta = deepProvider("beta");
|
|
const auxiliary = auxiliaryProvider();
|
|
const session = selectableSession(
|
|
[beta.provider, alpha.provider],
|
|
auxiliary,
|
|
);
|
|
|
|
const ambiguous = await session.open(target);
|
|
expect(ambiguous.ok).toBe(false);
|
|
if (!ambiguous.ok)
|
|
expect(ambiguous.error).toMatchObject({ reason: "ambiguous" });
|
|
expect(alpha.clients).toHaveLength(0);
|
|
expect(beta.clients).toHaveLength(0);
|
|
expect(session.activeTarget()).toBeUndefined();
|
|
|
|
expect((await session.open(target, { providerId: "alpha" })).ok).toBe(true);
|
|
expect(bindingStatus(session)).toMatchObject({
|
|
analysis_provider_binding: {
|
|
provider: { id: "alpha", version: "1" },
|
|
selection_source: "request",
|
|
},
|
|
analysis_provider_candidates: [
|
|
{ provider: { id: "alpha" }, selected: true },
|
|
{ provider: { id: "beta" }, selected: false },
|
|
],
|
|
});
|
|
expect(alpha.clients).toHaveLength(0);
|
|
|
|
const observed = await session.execute("address_name", { address: "0x1" });
|
|
expect(observed.ok).toBe(true);
|
|
if (observed.ok)
|
|
expect(observed.value).toMatchObject({
|
|
provider: { id: "alpha", version: "1" },
|
|
analysisProfile: { provider: { id: "alpha", version: "1" } },
|
|
result: "alpha:address_name",
|
|
subject: {
|
|
sha256: expect.any(String),
|
|
path: target,
|
|
format: "analysis-database",
|
|
},
|
|
});
|
|
expect(alpha.clients).toHaveLength(1);
|
|
expect(beta.clients).toHaveLength(0);
|
|
|
|
expect((await session.open(target, { providerId: "alpha" })).ok).toBe(true);
|
|
expect(alpha.clients).toHaveLength(1);
|
|
expect(alpha.clients[0]?.closed).toBe(false);
|
|
|
|
const auxiliaryResult = await session.execute("inspect_macho", {});
|
|
expect(auxiliaryResult.ok && auxiliaryResult.value.provider.id).toBe(
|
|
"auxiliary",
|
|
);
|
|
|
|
alpha.available = false;
|
|
expect((await session.open(target)).ok).toBe(true);
|
|
expect(bindingStatus(session)).toMatchObject({
|
|
analysis_provider_binding: { provider: { id: "alpha" } },
|
|
});
|
|
|
|
const unknown = await session.open(target, { providerId: "missing" });
|
|
expect(unknown.ok).toBe(false);
|
|
if (!unknown.ok)
|
|
expect(unknown.error).toMatchObject({ reason: "unknown_provider" });
|
|
expect(bindingStatus(session)).toMatchObject({
|
|
analysis_provider_binding: { provider: { id: "alpha" } },
|
|
});
|
|
|
|
expect((await session.open(target, { providerId: "beta" })).ok).toBe(true);
|
|
expect(alpha.clients[0]?.closed).toBe(true);
|
|
beta.fail = true;
|
|
const failed = await session.execute("address_name", { address: "0x2" });
|
|
expect(failed.ok).toBe(false);
|
|
if (!failed.ok) expect(failed.error._tag).toBe("ProviderAdapterError");
|
|
expect(alpha.clients[0]?.calls).toEqual(["address_name"]);
|
|
expect(bindingStatus(session)).toMatchObject({
|
|
analysis_provider_binding: { provider: { id: "beta" } },
|
|
});
|
|
|
|
await session.close();
|
|
expect(beta.clients[0]?.closed).toBe(true);
|
|
});
|
|
|
|
it("preserves bundle-root identity for artifact inventory operations", async () => {
|
|
const target = await databaseTarget();
|
|
const analysis = deepProvider("alpha");
|
|
const artifact = auxiliaryProvider("artifact", "inventory_artifact");
|
|
const session = selectableSession([analysis.provider], artifact);
|
|
|
|
expect((await session.open(target, { providerId: "alpha" })).ok).toBe(true);
|
|
const result = await session.execute("inventory_artifact", {});
|
|
if (!result.ok) throw result.error;
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
subject: {
|
|
path: target,
|
|
sha256: "f".repeat(64),
|
|
format: "directory",
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("preserves the active client when a provider switch is cancelled while draining", async () => {
|
|
const target = await databaseTarget();
|
|
const alpha = deepProvider("alpha");
|
|
const beta = deepProvider("beta");
|
|
const pending = deferred<ReturnType<typeof successfulExecution>>();
|
|
alpha.pending = pending.promise;
|
|
const session = selectableSession([alpha.provider, beta.provider]);
|
|
expect((await session.open(target, { providerId: "alpha" })).ok).toBe(true);
|
|
const running = session.execute("address_name", { address: "0x1" });
|
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
|
|
const controller = new AbortController();
|
|
const switching = session.open(target, {
|
|
providerId: "beta",
|
|
signal: controller.signal,
|
|
});
|
|
while (beta.profileResolutions === 0)
|
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
controller.abort();
|
|
pending.resolve(successfulExecution("alpha"));
|
|
|
|
expect(await running).toMatchObject({ ok: true });
|
|
const cancelled = await switching;
|
|
expect(cancelled).toEqual({
|
|
ok: false,
|
|
error: new AnalysisCancelledError("open_binary"),
|
|
});
|
|
expect(alpha.clients[0]?.closed).toBe(false);
|
|
expect(beta.clients).toHaveLength(0);
|
|
expect(bindingStatus(session)).toMatchObject({
|
|
analysis_provider_binding: { provider: { id: "alpha" } },
|
|
});
|
|
|
|
alpha.pending = undefined;
|
|
const retained = await session.execute("address_name", { address: "0x2" });
|
|
expect(retained.ok && retained.value.provider.id).toBe("alpha");
|
|
expect(alpha.clients).toHaveLength(1);
|
|
await session.close();
|
|
expect(alpha.clients[0]?.closed).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("selected provider capability provenance", () => {
|
|
it("labels selected-provider capabilities with the resolved identity", async () => {
|
|
const target = await databaseTarget();
|
|
const alpha = deepProvider("alpha");
|
|
const session = selectableSession([alpha.provider], auxiliaryProvider());
|
|
|
|
expect((await session.open(target, { providerId: "alpha" })).ok).toBe(true);
|
|
const resolved = { id: "alpha", name: "alpha provider", version: "1" };
|
|
const status = bindingStatus(session);
|
|
expect(status).toMatchObject({
|
|
analysis_provider_binding: { provider: resolved },
|
|
analysis_provider_candidates: [
|
|
{
|
|
provider: resolved,
|
|
selected: true,
|
|
capabilities: [{ provider: resolved }],
|
|
},
|
|
],
|
|
});
|
|
expect(
|
|
z
|
|
.object({ capabilities: z.array(z.object({ provider: z.unknown() })) })
|
|
.parse(status)
|
|
.capabilities.map(({ provider }) => provider),
|
|
).toEqual([
|
|
{ id: "alpha", name: "alpha provider", version: "1" },
|
|
{ id: "auxiliary", name: "auxiliary provider", version: "1" },
|
|
]);
|
|
await session.close();
|
|
});
|
|
});
|
|
|
|
interface ClientState {
|
|
readonly calls: string[];
|
|
closed: boolean;
|
|
}
|
|
|
|
interface DeepProviderFixture {
|
|
provider: AnalysisProviderCandidate;
|
|
readonly clients: ClientState[];
|
|
available: boolean;
|
|
fail: boolean;
|
|
profileResolutions: number;
|
|
pending: Promise<ReturnType<typeof successfulExecution>> | undefined;
|
|
}
|
|
|
|
const deepProvider = (id: string): DeepProviderFixture => {
|
|
const identity: ProviderIdentity = {
|
|
id,
|
|
name: `${id} provider`,
|
|
version: null,
|
|
};
|
|
const fixture: Omit<DeepProviderFixture, "provider"> = {
|
|
clients: [],
|
|
available: true,
|
|
fail: false,
|
|
profileResolutions: 0,
|
|
pending: undefined,
|
|
};
|
|
const provider: AnalysisProviderCandidate = {
|
|
identity: () => identity,
|
|
capabilities: () => [capability(identity, "address_name")],
|
|
inspectAvailability: () =>
|
|
fixture.available
|
|
? {
|
|
status: "available",
|
|
code: null,
|
|
reason: null,
|
|
diagnostics: { executable_path: `/opt/${id}/bin/analyze` },
|
|
}
|
|
: {
|
|
status: "unavailable",
|
|
code: "runtime_missing",
|
|
reason: `${id} runtime is unavailable`,
|
|
diagnostics: { executable_path: `/opt/${id}/bin/analyze` },
|
|
},
|
|
inspectTargetSupport: () => ({
|
|
status: "supported",
|
|
code: null,
|
|
reason: null,
|
|
diagnostics: {},
|
|
}),
|
|
resolveAnalysisProfile: () => {
|
|
fixture.profileResolutions += 1;
|
|
return Promise.resolve(
|
|
ok({
|
|
profile: createAnalysisProfile(
|
|
{ id, name: identity.name, version: "1" },
|
|
{ provider: id },
|
|
),
|
|
}),
|
|
);
|
|
},
|
|
createClient: (_target, profile): AnalysisClient => {
|
|
const state: ClientState = { calls: [], closed: false };
|
|
fixture.clients.push(state);
|
|
return {
|
|
execute: (operation) => {
|
|
if (operation === "health")
|
|
return Promise.resolve(successfulExecution(id, profile?.provider));
|
|
state.calls.push(operation);
|
|
if (fixture.fail)
|
|
return Promise.resolve(
|
|
err(new ProviderAdapterError(id, operation)),
|
|
);
|
|
if (fixture.pending !== undefined) return fixture.pending;
|
|
return Promise.resolve(successfulExecution(id, profile?.provider));
|
|
},
|
|
close: () => {
|
|
state.closed = true;
|
|
return Promise.resolve(ok(null));
|
|
},
|
|
};
|
|
},
|
|
};
|
|
return Object.assign(fixture, { provider });
|
|
};
|
|
|
|
const successfulExecution = (
|
|
id: string,
|
|
provider: ProviderIdentity = {
|
|
id,
|
|
name: `${id} provider`,
|
|
version: "1",
|
|
},
|
|
) => ok(createAnalysisExecution(`${id}:address_name`, provider));
|
|
|
|
const auxiliaryProvider = (
|
|
id = "auxiliary",
|
|
operation: DeclaredOperation = "inspect_macho",
|
|
): AnalysisProvider => {
|
|
const identity = {
|
|
id,
|
|
name: `${id} provider`,
|
|
version: "1",
|
|
};
|
|
return {
|
|
identity: () => identity,
|
|
capabilities: () => [capability(identity, operation)],
|
|
createClient: (target) => ({
|
|
execute: (operation) =>
|
|
Promise.resolve(
|
|
ok(
|
|
createAnalysisExecution(
|
|
`${operation}:auxiliary`,
|
|
identity,
|
|
operation === "inventory_artifact" ||
|
|
operation === "inspect_artifact"
|
|
? {
|
|
subject: {
|
|
path: target.sourcePath ?? target.path,
|
|
sha256: "f".repeat(64),
|
|
format: "directory",
|
|
},
|
|
}
|
|
: {},
|
|
),
|
|
),
|
|
),
|
|
close: () => Promise.resolve(ok(null)),
|
|
}),
|
|
};
|
|
};
|
|
|
|
const selectableSession = (
|
|
providers: readonly AnalysisProviderCandidate[],
|
|
auxiliary?: AnalysisProvider,
|
|
): BinarySession =>
|
|
composeBinarySession(
|
|
new AnalysisProviderRegistry(providers),
|
|
auxiliary === undefined ? [] : [auxiliary],
|
|
);
|
|
|
|
const capability = (
|
|
provider: ProviderIdentity,
|
|
operation: DeclaredOperation,
|
|
): CapabilityDescriptor => ({
|
|
provider,
|
|
operation,
|
|
available: true,
|
|
reason: null,
|
|
effects: {
|
|
mutatesArtifact: false,
|
|
launchesProcess: operation === "address_name",
|
|
mayShowUi: false,
|
|
mayAccessNetwork: false,
|
|
mayWriteFilesystem: false,
|
|
changesPermissions: false,
|
|
requiresRoot: false,
|
|
},
|
|
limitations: [],
|
|
});
|
|
|
|
const databaseTarget = async (): Promise<string> => {
|
|
const directory = await createTestTempDirectory("rea-provider-binding-");
|
|
const path = join(directory, "fixture.hop");
|
|
await writeFile(path, "fixture");
|
|
return path;
|
|
};
|
|
|
|
const bindingStatus = (session: BinarySession) =>
|
|
z
|
|
.object({
|
|
analysis_provider_binding: z.unknown(),
|
|
analysis_provider_candidates: z.array(z.unknown()),
|
|
})
|
|
.passthrough()
|
|
.parse(session.status());
|
|
|
|
interface Deferred<Value> {
|
|
readonly promise: Promise<Value>;
|
|
resolve(value: Value): void;
|
|
}
|
|
|
|
const deferred = <Value>(): Deferred<Value> => {
|
|
let resolvePromise: ((value: Value) => void) | undefined;
|
|
const promise = new Promise<Value>((resolve) => {
|
|
resolvePromise = resolve;
|
|
});
|
|
return {
|
|
promise,
|
|
resolve(value) {
|
|
resolvePromise?.(value);
|
|
},
|
|
};
|
|
};
|