Files
reverse-engineering-anything/src/application/binary/SessionProviderRouter.ts
T
Kaoru0822andClaude Opus 5.5 2ff850de68 fix(session): attribute selected-provider capabilities to the resolved identity (#2013)
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>
2026-10-12 10:36:28 +08:00

206 lines
6.7 KiB
TypeScript

import type { AnalysisProviderSelector } from "../../contracts/providerSelection.js";
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
import { ProviderSelectionError } from "../../domain/providerSelectionError.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { err, ok, type Result } from "../../domain/result.js";
import type {
AnalysisClient,
AnalysisClientContext,
AnalysisProvider,
CapabilityDescriptor,
ProviderIdentity,
} from "../AnalysisProvider.js";
import {
AnalysisProviderRegistry,
type AnalysisProviderBinding,
type AnalysisProviderCandidateStatus,
type AnalysisProviderSelection,
} from "./AnalysisProviderRegistry.js";
import { CompositeProvider } from "./CompositeProvider.js";
/** Immutable operation routes and binding metadata for one target transition. */
export interface SessionProviderRoute {
readonly identity: ProviderIdentity;
readonly capabilities: ReadonlyMap<string, CapabilityDescriptor>;
readonly profile: AnalysisProfileCommitment | null;
readonly binding: AnalysisProviderBinding | null;
readonly selection: AnalysisProviderSelection | undefined;
createClient(
target: BinaryTarget,
context: AnalysisClientContext,
): AnalysisClient;
}
/** Resolve one registry-selected deep provider and disjoint auxiliary families. */
export class SessionProviderRouter {
private constructor(
private readonly registry: AnalysisProviderRegistry,
private readonly auxiliaryProviders: readonly AnalysisProvider[],
private readonly route: SessionProviderRoute,
) {}
/** Create selection-aware routing while keeping auxiliary families disjoint. */
static selectable(
registry: AnalysisProviderRegistry,
auxiliaryProviders: readonly AnalysisProvider[],
): SessionProviderRouter {
const identities = [
...registry.identities(),
...auxiliaryProviders.map((provider) => provider.identity()),
];
assertUniqueProviderIds(identities);
const auxiliaryOperations = assertDisjoint(auxiliaryProviders);
for (const operation of registry.declaredOperations())
if (auxiliaryOperations.has(operation))
throw new TypeError(
`Deep and auxiliary providers both declare operation ${operation}`,
);
const initialRoute = selectableRoute(auxiliaryProviders, undefined);
return new SessionProviderRouter(
registry,
[...auxiliaryProviders],
initialRoute,
);
}
/** Target-free routes and discovery metadata. */
initialRoute(): SessionProviderRoute {
return this.route;
}
/** Resolve target routes before any provider client is created. */
async resolve(
target: BinaryTarget,
providerId?: AnalysisProviderSelector,
signal?: AbortSignal,
): Promise<Result<SessionProviderRoute, AnalysisError>> {
const resolutionOptions = signal === undefined ? undefined : { signal };
const selected = await this.registry.select(
target,
providerId,
resolutionOptions,
);
return selected.ok
? ok(selectableRoute(this.auxiliaryProviders, selected.value))
: selected;
}
/** Candidate status authoritative for the active or target-free route. */
candidateStatuses(
route: SessionProviderRoute,
): readonly AnalysisProviderCandidateStatus[] {
return route.selection?.candidates ?? this.registry.candidates();
}
/** Produce exact candidate rejections for an intentionally unbound route. */
unboundOperationError(
operation: string,
route: SessionProviderRoute,
): ProviderSelectionError | undefined {
if (
route.binding !== null ||
route.selection === undefined ||
!this.registry.declaredOperations().includes(operation)
)
return undefined;
return this.registry.unboundOperationError(operation, route.selection);
}
}
const selectableRoute = (
auxiliaryProviders: readonly AnalysisProvider[],
selection: AnalysisProviderSelection | undefined,
): SessionProviderRoute => {
const binding = selection?.binding ?? null;
const providers = [
...auxiliaryProviders,
...(binding === null ? [] : [binding.provider]),
];
const provider =
binding === null && providers.length === 1
? providers[0]
: providers.length === 0
? undefined
: new CompositeProvider(providers);
const identity = provider?.identity() ?? emptyProviderIdentity();
// The binding attributes deep-provider capabilities to the resolved version.
const capabilities = capabilityMap([
...auxiliaryProviders.flatMap((auxiliary) => auxiliary.capabilities()),
...(binding?.capabilities ?? []),
]);
return {
identity,
capabilities,
profile: binding?.profile ?? null,
binding,
selection,
createClient: (target, context) =>
provider?.createClient(target, binding?.profile, context) ??
emptyClient(identity),
};
};
const emptyClient = (identity: ProviderIdentity): AnalysisClient => ({
execute: (operation) =>
operation === "health"
? Promise.resolve(
ok({
result: null,
rawResult: null,
provider: identity,
limitations: [],
locations: [],
subject: null,
}),
)
: Promise.resolve(
err(
new AnalysisCapabilityUnavailableError(
identity.id,
operation,
"operation is not declared by this provider set",
),
),
),
close: () => Promise.resolve(ok(null)),
});
const emptyProviderIdentity = (): ProviderIdentity => ({
id: "composite:none",
name: "REA composite analysis provider",
version: null,
});
const capabilityMap = (
capabilities: readonly CapabilityDescriptor[],
): ReadonlyMap<string, CapabilityDescriptor> =>
new Map(capabilities.map((descriptor) => [descriptor.operation, descriptor]));
const assertDisjoint = (
providers: readonly AnalysisProvider[],
): ReadonlySet<string> => {
const routes = new Set<string>();
for (const provider of providers)
for (const { operation } of provider.capabilities()) {
if (routes.has(operation))
throw new TypeError(
`Multiple auxiliary providers declare operation ${operation}`,
);
routes.add(operation);
}
return routes;
};
const assertUniqueProviderIds = (
identities: readonly ProviderIdentity[],
): void => {
const ids = new Set<string>();
for (const identity of identities) {
if (ids.has(identity.id))
throw new TypeError(`Duplicate configured provider ID: ${identity.id}`);
ids.add(identity.id);
}
};