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:
parent
42b0ed126d
commit
2ff850de68
|
|
@ -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),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue