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:
Kaoru0822 2026-10-11 19:36:28 -07:00 committed by GitHub
parent 42b0ed126d
commit 2ff850de68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 51 additions and 18 deletions

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,

View File

@ -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),
};

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. */

View File

@ -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,

View File

@ -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;