From 2ff850de68dc9b05c6faf2bbfd26d56bc2d5d27c Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:36:28 -0700 Subject: [PATCH] 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 --- src/application/DirectAnalysis.ts | 9 ++---- .../binary/AnalysisProviderRegistry.ts | 13 ++++++-- src/application/binary/BinarySession.ts | 9 +----- .../binary/SessionProviderRouter.ts | 6 +++- .../analysis-sessions/providerBinding.test.ts | 32 +++++++++++++++++++ 5 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/application/DirectAnalysis.ts b/src/application/DirectAnalysis.ts index 839a758f3..c4d130f63 100644 --- a/src/application/DirectAnalysis.ts +++ b/src/application/DirectAnalysis.ts @@ -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, diff --git a/src/application/binary/AnalysisProviderRegistry.ts b/src/application/binary/AnalysisProviderRegistry.ts index 43e6bbb9c..5b7e78813 100644 --- a/src/application/binary/AnalysisProviderRegistry.ts +++ b/src/application/binary/AnalysisProviderRegistry.ts @@ -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), }; diff --git a/src/application/binary/BinarySession.ts b/src/application/binary/BinarySession.ts index d5841aa93..da8d8e679 100644 --- a/src/application/binary/BinarySession.ts +++ b/src/application/binary/BinarySession.ts @@ -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. */ diff --git a/src/application/binary/SessionProviderRouter.ts b/src/application/binary/SessionProviderRouter.ts index dac5c183e..cf5653537 100644 --- a/src/application/binary/SessionProviderRouter.ts +++ b/src/application/binary/SessionProviderRouter.ts @@ -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, diff --git a/tests/composition/analysis-sessions/providerBinding.test.ts b/tests/composition/analysis-sessions/providerBinding.test.ts index b6d726fdb..9ed21abfa 100644 --- a/tests/composition/analysis-sessions/providerBinding.test.ts +++ b/tests/composition/analysis-sessions/providerBinding.test.ts @@ -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;