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>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 authored and GitHub committed 2026-10-12 10:36:28 +08:00
1 parent 42b0ed126d
commit 2ff850de68
5 files changed
+51 -18

No files matched your search

+2 -7
View File
@@ -560,13 +560,8 @@ const analysisProfileForRoute = (
const providerIdentityForRoute = (
route: SessionProviderRoute,
operation: string,
): ProviderIdentity => {
const selected =
route.capabilities?.get(operation)?.provider ?? route.identity;
return route.profile?.provider.id === selected.id
? route.profile.provider
: selected;
};
): ProviderIdentity =>
route.capabilities?.get(operation)?.provider ?? route.identity;
const analysisProfileForEvidence = (
session: BinarySession,
@@ -50,6 +50,8 @@ export interface AnalysisProviderBinding {
readonly identity: ProviderIdentity & { readonly version: string };
readonly selectionSource: "request" | "environment" | "auto-single-candidate";
readonly profile: AnalysisProfileCommitment;
/** The provider's capabilities, attributed to its resolved identity. */
readonly capabilities: readonly CapabilityDescriptor[];
}
/** Complete result used for both a bound and intentionally unbound target. */
@@ -361,18 +363,25 @@ const selectedResult = (
},
statuses: readonly AnalysisProviderCandidateStatus[],
): AnalysisProviderSelection => {
const identity = evaluated.profile.provider;
const capabilities = evaluated.status.capabilities.map((capability) => ({
...capability,
provider: structuredClone(identity),
}));
const selectedStatus = {
...evaluated.status,
provider: structuredClone(evaluated.profile.provider),
provider: structuredClone(identity),
selected: true,
capabilities,
} satisfies AnalysisProviderCandidateStatus;
return {
requestedProviderId,
binding: {
provider: evaluated.candidate,
identity: structuredClone(evaluated.profile.provider),
identity: structuredClone(identity),
selectionSource: source,
profile: structuredClone(evaluated.profile),
capabilities,
},
candidates: replaceCandidate(statuses, selectedStatus),
};
+1 -8
View File
@@ -142,14 +142,7 @@ export class BinarySession
}
}
}
const profile = this.#active?.profile;
return structuredClone(
profile !== null &&
profile !== undefined &&
profile.provider.id === selected.id
? profile.provider
: selected,
);
return structuredClone(selected);
}
/** External live state cannot be replayed by a direct or composed CLI workflow. */
@@ -125,7 +125,11 @@ const selectableRoute = (
? undefined
: new CompositeProvider(providers);
const identity = provider?.identity() ?? emptyProviderIdentity();
const capabilities = capabilityMap(provider?.capabilities() ?? []);
// The binding attributes deep-provider capabilities to the resolved version.
const capabilities = capabilityMap([
...auxiliaryProviders.flatMap((auxiliary) => auxiliary.capabilities()),
...(binding?.capabilities ?? []),
]);
return {
identity,
capabilities,
@@ -191,6 +191,38 @@ describe("target-bound provider routing", () => {
});
});
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;