refactor(binary): group session selection and snapshot ownership (#886)

This commit is contained in:
ysopwnable authored and GitHub committed 2026-10-07 15:05:29 +08:00
1 parent ab985c6a10
commit dd114d3980
59 files changed
+220 -190

No files matched your search

@@ -0,0 +1,29 @@
/** Sentinel returned when an abort signal wins an asynchronous race. */
export const ABORTED = Symbol("aborted");
/**
* Await a promise without depending on the producer to observe cancellation.
* The producer still receives its signal separately and remains responsible for
* releasing any resources that it owns.
*/
export const waitForAbortable = async <Value>(
pending: Promise<Value>,
signal: AbortSignal | undefined,
): Promise<Value | typeof ABORTED> => {
if (signal === undefined) return pending;
let onAbort: (() => void) | undefined;
const cancelled = new Promise<typeof ABORTED>((resolve) => {
if (signal.aborted) {
resolve(ABORTED);
return;
}
onAbort = () => resolve(ABORTED);
signal.addEventListener("abort", onAbort, { once: true });
});
try {
// Even an already-cancelled wait must own the producer's eventual rejection.
return await Promise.race([cancelled, pending]);
} finally {
if (onAbort !== undefined) signal.removeEventListener("abort", onAbort);
}
};
@@ -0,0 +1,29 @@
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { err, ok, type Result } from "../../domain/result.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type { AnalysisClient, ExecutionOptions } from "../AnalysisProvider.js";
/** Close one provider client and normalize unexpected adapter rejection. */
export const closeAnalysisClient = async (
client: AnalysisClient,
providerId: string,
options: Pick<ExecutionOptions, "progress"> & {
readonly retainDocument?: boolean;
} = {},
): Promise<Result<null, AnalysisError>> => {
try {
if (client.closeWithOutcome !== undefined)
return await client.closeWithOutcome(options);
await client.close();
return ok(null);
} catch (cause: unknown) {
return err(
new ProviderCleanupError(
providerId,
["provider-client"],
{ reason: "provider client close rejected unexpectedly" },
{ cause },
),
);
}
};
@@ -0,0 +1,120 @@
import {
analysisProfileSchema,
type AnalysisProfileCommitment,
} from "../../domain/analysisProfile.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import { jsonObjectSchema, type JsonValue } from "../../domain/jsonValue.js";
import { err, ok, type Result } from "../../domain/result.js";
import type { AnalysisProviderCandidate } from "../AnalysisProvider.js";
import type { AnalysisProviderCandidateStatus } from "./AnalysisProviderRegistry.js";
import { ABORTED, waitForAbortable } from "./AbortablePromise.js";
/** Profile-resolution truth for one discovered provider candidate. */
export interface AnalysisProviderCandidateEvaluation {
readonly candidate: AnalysisProviderCandidate;
readonly status: AnalysisProviderCandidateStatus;
readonly profile?: AnalysisProfileCommitment;
readonly compatibility?: Readonly<Record<string, JsonValue>>;
}
/** Resolve and validate one provider profile without starting its client. */
export const evaluateAnalysisProviderCandidate = async (
candidate: AnalysisProviderCandidate,
status: AnalysisProviderCandidateStatus,
target: BinaryTarget,
signal: AbortSignal | undefined,
): Promise<
Result<AnalysisProviderCandidateEvaluation, AnalysisCancelledError>
> => {
if (signalIsAborted(signal))
return err(new AnalysisCancelledError("open_binary"));
let resolved: Awaited<ReturnType<typeof candidate.resolveAnalysisProfile>>;
try {
const resolution = await waitForAbortable(
candidate.resolveAnalysisProfile(
target,
signal === undefined ? undefined : { signal },
),
signal,
);
if (resolution === ABORTED)
return err(new AnalysisCancelledError("open_binary"));
resolved = resolution;
} catch (cause: unknown) {
void cause;
return ok(
rejectedProfile(candidate, status, "Provider profile resolution threw"),
);
}
if (signalIsAborted(signal))
return err(new AnalysisCancelledError("open_binary"));
if (!resolved.ok && resolved.error instanceof AnalysisCancelledError)
return err(resolved.error);
if (!resolved.ok)
return ok(
rejectedProfile(candidate, status, resolved.error.message, {
error_tag: resolved.error._tag,
}),
);
try {
if (resolved.value.profile === null)
return ok(
rejectedProfile(
candidate,
status,
"Provider version or analysis profile could not be resolved",
),
);
const profile = analysisProfileSchema.parse(resolved.value.profile);
const compatibility = jsonObjectSchema.parse(resolved.value.compatibility);
const identity = candidate.identity();
if (
profile.provider.id !== identity.id ||
profile.provider.name !== identity.name ||
(identity.version !== null &&
profile.provider.version !== identity.version)
)
return ok(
rejectedProfile(
candidate,
status,
"Resolved analysis profile has mismatched provider identity",
),
);
return ok({ candidate, status, profile, compatibility });
} catch (cause: unknown) {
void cause;
return ok(
rejectedProfile(
candidate,
status,
"Provider returned an invalid analysis profile",
),
);
}
};
const signalIsAborted = (signal: AbortSignal | undefined): boolean =>
signal?.aborted === true;
const rejectedProfile = (
candidate: AnalysisProviderCandidate,
status: AnalysisProviderCandidateStatus,
reason: string,
diagnostics: Readonly<Record<string, JsonValue>> = {},
): AnalysisProviderCandidateEvaluation => ({
candidate,
status: {
...status,
availability: {
status: "unavailable",
code: "version_unresolved",
reason,
diagnostics: jsonObjectSchema.parse({
...status.availability.diagnostics,
...diagnostics,
}),
},
},
});
@@ -0,0 +1,422 @@
import { describe, expect, it } from "vitest";
import {
AnalysisProviderRegistry,
type AnalysisProviderBinding,
} from "./AnalysisProviderRegistry.js";
import {
createAnalysisExecution,
type AnalysisClient,
type AnalysisProviderCandidate,
type CapabilityDescriptor,
type ProviderAvailability,
type ProviderIdentity,
type ProviderTargetSupport,
} from "../AnalysisProvider.js";
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderSelectionError } from "../../domain/providerSelectionError.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import { err, ok } from "../../domain/result.js";
const DATABASE_TARGET: BinaryTarget = {
path: "/tmp/fixture.hop",
sha256: "a".repeat(64),
kind: "database",
format: "analysis-database",
};
const ARTIFACT_TARGET: BinaryTarget = {
path: "/tmp/app.asar",
sha256: "b".repeat(64),
kind: "archive",
format: "asar",
};
describe("analysis provider registry: discovery and selection", () => {
it("admits overlapping operations, sorts discovery, and rejects duplicate IDs", () => {
const alpha = candidate("alpha");
const beta = candidate("beta");
const registry = new AnalysisProviderRegistry([
beta.provider,
alpha.provider,
]);
expect(registry.identities().map(({ id }) => id)).toEqual([
"alpha",
"beta",
]);
expect(
registry.candidates().map(({ provider, targetSupport }) => ({
id: provider.id,
support: targetSupport.status,
})),
).toEqual([
{ id: "alpha", support: "unknown" },
{ id: "beta", support: "unknown" },
]);
expect(alpha.created).toEqual([]);
expect(beta.created).toEqual([]);
expect(
() => new AnalysisProviderRegistry([alpha.provider, alpha.provider]),
).toThrow(/Duplicate analysis provider ID: alpha/u);
expect(
() =>
new AnalysisProviderRegistry([
{
...alpha.provider,
capabilities: () => {
const descriptor = capability(alpha.provider.identity());
return [descriptor, descriptor];
},
},
]),
).toThrow(/declares operation address_name more than once/u);
expect(
() =>
new AnalysisProviderRegistry([
{
...alpha.provider,
capabilities: () => [
capability({
...alpha.provider.identity(),
name: "mismatched provider",
}),
],
},
]),
).toThrow(/published mismatched capability provenance/u);
expect(
() => new AnalysisProviderRegistry([candidate("auto").provider]),
).toThrow(/Invalid analysis provider ID: auto/u);
});
it("selects the sole usable candidate without creating a client", async () => {
const alpha = candidate("alpha");
const beta = candidate("beta", { available: false });
const registry = new AnalysisProviderRegistry([
beta.provider,
alpha.provider,
]);
const selected = await registry.select(DATABASE_TARGET);
expect(selected.ok).toBe(true);
if (!selected.ok) throw new Error("provider selection failed");
if (selected.value.binding === null)
throw new Error("expected a bound provider");
expect(bindingProjection(selected.value.binding)).toEqual({
id: "alpha",
source: "auto-single-candidate",
version: "1",
});
expect(selected.value.candidates).toMatchObject([
{ provider: { id: "alpha", version: "1" }, selected: true },
{
provider: { id: "beta" },
selected: false,
availability: { status: "unavailable", code: "runtime_missing" },
},
]);
expect(alpha.created).toEqual([]);
expect(beta.created).toEqual([]);
});
it("makes auto ambiguity explicit and honors request over environment", async () => {
const alpha = candidate("alpha");
const beta = candidate("beta");
const registry = new AnalysisProviderRegistry(
[alpha.provider, beta.provider],
"beta",
);
const environment = await registry.select(DATABASE_TARGET);
expect(environment.ok).toBe(true);
if (environment.ok)
expect(environment.value.binding?.selectionSource).toBe("environment");
expect(environment.ok && environment.value.binding?.identity.id).toBe(
"beta",
);
const request = await registry.select(DATABASE_TARGET, "alpha");
expect(request.ok && request.value.binding?.identity.id).toBe("alpha");
if (request.ok)
expect(request.value.binding?.selectionSource).toBe("request");
const automatic = await registry.select(DATABASE_TARGET, "auto");
expect(automatic.ok).toBe(false);
if (automatic.ok) return;
expect(automatic.error).toMatchObject({
reason: "ambiguous",
requestedProviderId: "auto",
candidateIds: ["alpha", "beta"],
});
expect(projectAnalysisError(automatic.error)).toMatchObject({
code: "capability_unavailable",
details: {
selection_reason: "ambiguous",
candidate_ids: ["alpha", "beta"],
},
});
expect(alpha.created).toEqual([]);
expect(beta.created).toEqual([]);
});
});
describe("analysis provider registry: binding failures and cancellation", () => {
it("returns actionable unknown, unavailable, and unsupported failures", async () => {
const unavailable = candidate("alpha", { available: false });
const registry = new AnalysisProviderRegistry([unavailable.provider]);
const unknown = await registry.select(DATABASE_TARGET, "missing");
expect(unknown.ok).toBe(false);
if (!unknown.ok)
expect(projectAnalysisError(unknown.error)).toMatchObject({
details: {
selection_reason: "unknown_provider",
requested_provider_id: "missing",
candidate_ids: ["alpha"],
},
});
const rejected = await registry.select(DATABASE_TARGET, "alpha");
expect(rejected.ok).toBe(false);
if (!rejected.ok)
expect(projectAnalysisError(rejected.error)).toMatchObject({
code: "provider_unavailable",
details: {
rejections: [
{
provider_id: "alpha",
code: "runtime_missing",
diagnostics: { executable_path: "/opt/alpha/bin/analyze" },
},
],
},
});
const unsupported = await new AnalysisProviderRegistry([
candidate("alpha", { available: false }).provider,
]).select(ARTIFACT_TARGET, "alpha");
expect(unsupported.ok).toBe(false);
if (unsupported.ok)
throw new Error("expected unsupported target rejection");
expect(unsupported.error).toBeInstanceOf(ProviderSelectionError);
if (
!unsupported.ok &&
unsupported.error instanceof ProviderSelectionError
) {
expect(unsupported.error).toMatchObject({
reason: "target_unsupported",
});
expect(unsupported.error.rejections.map(({ code }) => code)).toEqual([
"runtime_missing",
"target_kind_unsupported",
]);
}
});
it("keeps artifact-only targets unbound under auto or environment preference", async () => {
const alpha = candidate("alpha");
const environment = new AnalysisProviderRegistry([alpha.provider], "alpha");
const automatic = new AnalysisProviderRegistry([alpha.provider]);
for (const registry of [environment, automatic]) {
const selected = await registry.select(ARTIFACT_TARGET);
expect(selected).toMatchObject({
ok: true,
value: {
binding: null,
candidates: [
{
selected: false,
targetSupport: {
status: "unsupported",
code: "target_kind_unsupported",
},
},
],
},
});
}
const explicit = await automatic.select(ARTIFACT_TARGET, "alpha");
expect(explicit.ok).toBe(false);
if (explicit.ok) throw new Error("expected provider selection rejection");
expect(explicit.error).toBeInstanceOf(ProviderSelectionError);
if (explicit.error instanceof ProviderSelectionError)
expect(explicit.error.reason).toBe("target_unsupported");
});
it("does not bind a profile with unresolved version or adapter failure", async () => {
const unresolved = candidate("unresolved", { profile: "missing" });
const failed = candidate("failed", { profile: "error" });
const automatic = await new AnalysisProviderRegistry([
unresolved.provider,
]).select(DATABASE_TARGET);
expect(automatic).toMatchObject({
ok: true,
value: {
binding: null,
candidates: [
{
availability: {
status: "unavailable",
code: "version_unresolved",
},
},
],
},
});
const explicit = await new AnalysisProviderRegistry([
failed.provider,
]).select(DATABASE_TARGET, "failed");
expect(explicit.ok).toBe(false);
if (explicit.ok) throw new Error("expected provider selection rejection");
expect(explicit.error).toBeInstanceOf(ProviderSelectionError);
if (explicit.error instanceof ProviderSelectionError)
expect(explicit.error.rejections).toMatchObject([
{
providerId: "failed",
code: "version_unresolved",
diagnostics: { error_tag: "ProviderAdapterError" },
},
]);
});
it("cancels a pending profile probe even when the adapter ignores its signal", async () => {
const alpha = candidate("alpha");
let observedSignal: AbortSignal | undefined;
const ignoresCancellation: AnalysisProviderCandidate = {
...alpha.provider,
resolveAnalysisProfile: (_target, options) => {
observedSignal = options?.signal;
return new Promise<never>(() => undefined);
},
};
const controller = new AbortController();
const selecting = new AnalysisProviderRegistry([
ignoresCancellation,
]).select(DATABASE_TARGET, "alpha", { signal: controller.signal });
expect(observedSignal).toBe(controller.signal);
controller.abort();
await expect(selecting).resolves.toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError", operation: "open_binary" },
});
expect(alpha.created).toEqual([]);
});
});
interface CandidateFixture {
readonly provider: AnalysisProviderCandidate;
readonly created: string[];
}
const candidate = (
id: string,
options: {
readonly available?: boolean;
readonly profile?: "resolved" | "missing" | "error";
} = {},
): CandidateFixture => {
const identity: ProviderIdentity = {
id,
name: `${id} provider`,
version: null,
};
const created: string[] = [];
const available = options.available ?? true;
const provider: AnalysisProviderCandidate = {
identity: () => identity,
capabilities: () => [capability(identity)],
inspectAvailability: (): ProviderAvailability =>
available
? {
status: "available",
code: null,
reason: null,
diagnostics: { executable_path: `/opt/${id}/bin/analyze` },
}
: {
status: "unavailable",
code: "runtime_missing",
reason: `${id} runtime is unavailable`,
diagnostics: { executable_path: `/opt/${id}/bin/analyze` },
},
inspectTargetSupport: (target): ProviderTargetSupport =>
target.kind === "database"
? {
status: "supported",
code: null,
reason: null,
diagnostics: { target_kind: target.kind },
}
: {
status: "unsupported",
code: "target_kind_unsupported",
reason: `${id} only accepts analysis databases in this fixture`,
diagnostics: { target_kind: target.kind },
},
resolveAnalysisProfile: () => {
if (options.profile === "error")
return Promise.resolve(
err(new ProviderAdapterError(id, "resolve_analysis_profile")),
);
return Promise.resolve(
ok({
profile:
options.profile === "missing"
? null
: createAnalysisProfile(
{ id, name: identity.name, version: "1" },
{ fixture: id },
),
compatibility: {},
}),
);
},
createClient: (_target, profile): AnalysisClient => {
created.push(id);
const executionIdentity = profile?.provider ?? identity;
return {
execute: (operation) =>
Promise.resolve(
ok(
createAnalysisExecution(
operation === "health" ? null : `${id}:${operation}`,
executionIdentity,
),
),
),
close: () => Promise.resolve(),
};
},
};
return { provider, created };
};
const capability = (provider: ProviderIdentity): CapabilityDescriptor => ({
provider,
operation: "address_name",
available: true,
reason: null,
effects: {
mutatesArtifact: false,
launchesProcess: true,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem: false,
changesPermissions: false,
requiresRoot: false,
},
limitations: [],
});
const bindingProjection = (binding: AnalysisProviderBinding) => ({
id: binding.identity.id,
source: binding.selectionSource,
version: binding.identity.version,
});
@@ -0,0 +1,453 @@
import {
analysisProviderIdSchema,
analysisProviderSelectorSchema,
type AnalysisProviderSelector,
} from "../../contracts/providerSelection.js";
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import {
ProviderSelectionError,
type ProviderSelectionFailureReason,
type ProviderSelectionRejection,
} from "../../domain/providerSelectionError.js";
import { jsonObjectSchema, type JsonValue } from "../../domain/jsonValue.js";
import { err, ok, type Result } from "../../domain/result.js";
import type {
AnalysisProviderCandidate,
CapabilityDescriptor,
ProviderAvailability,
ProviderIdentity,
ProviderTargetSupport,
} from "../AnalysisProvider.js";
import {
evaluateAnalysisProviderCandidate,
type AnalysisProviderCandidateEvaluation,
} from "./AnalysisProviderEvaluation.js";
/** Deterministic discovery truth for one configured deep provider. */
export interface AnalysisProviderCandidateStatus {
readonly provider: ProviderIdentity;
readonly availability: ProviderAvailability;
readonly targetSupport:
| ProviderTargetSupport
| {
/** No target has been parsed, so support has not been evaluated. */
readonly status: "unknown";
readonly code: null;
readonly reason: string;
readonly diagnostics: Readonly<Record<string, JsonValue>>;
};
readonly selected: boolean;
readonly capabilities: readonly CapabilityDescriptor[];
}
/** One immutable deep-provider binding selected before client startup. */
export interface AnalysisProviderBinding {
readonly provider: AnalysisProviderCandidate;
readonly identity: ProviderIdentity & { readonly version: string };
readonly selectionSource: "request" | "environment" | "auto-single-candidate";
readonly profile: AnalysisProfileCommitment;
readonly compatibility: Readonly<Record<string, JsonValue>>;
}
/** Complete result used for both a bound and intentionally unbound target. */
export interface AnalysisProviderSelection {
readonly requestedProviderId: string;
readonly binding: AnalysisProviderBinding | null;
readonly candidates: readonly AnalysisProviderCandidateStatus[];
}
interface TargetSelectionContext {
readonly target: BinaryTarget;
readonly baseline: readonly AnalysisProviderCandidateStatus[];
readonly signal: AbortSignal | undefined;
}
/**
* Discover and select overlapping deep providers without starting their clients.
* Registration order never affects selection or caller-visible output.
*/
export class AnalysisProviderRegistry {
readonly #providers: readonly AnalysisProviderCandidate[];
readonly #byId: ReadonlyMap<string, AnalysisProviderCandidate>;
readonly #declaredOperations: readonly string[];
readonly #defaultSelector: AnalysisProviderSelector;
constructor(
providers: readonly AnalysisProviderCandidate[],
defaultSelector: AnalysisProviderSelector = "auto",
) {
this.#defaultSelector =
analysisProviderSelectorSchema.parse(defaultSelector);
const byId = new Map<string, AnalysisProviderCandidate>();
const declaredOperations = new Set<string>();
for (const provider of providers) {
const identity = provider.identity();
if (!analysisProviderIdSchema.safeParse(identity.id).success)
throw new TypeError(`Invalid analysis provider ID: ${identity.id}`);
if (byId.has(identity.id))
throw new TypeError(`Duplicate analysis provider ID: ${identity.id}`);
const providerOperations = new Set<string>();
for (const capability of provider.capabilities()) {
if (
capability.provider.id !== identity.id ||
capability.provider.name !== identity.name ||
capability.provider.version !== identity.version
)
throw new TypeError(
`Provider ${identity.id} published mismatched capability provenance for ${capability.operation}`,
);
if (providerOperations.has(capability.operation))
throw new TypeError(
`Provider ${identity.id} declares operation ${capability.operation} more than once`,
);
providerOperations.add(capability.operation);
declaredOperations.add(capability.operation);
}
byId.set(identity.id, provider);
}
this.#providers = Object.freeze(
[...providers].sort((left, right) =>
left.identity().id.localeCompare(right.identity().id),
),
);
this.#byId = byId;
this.#declaredOperations = Object.freeze(
[...declaredOperations].sort((left, right) => left.localeCompare(right)),
);
}
/** Return the configured default without interpreting it as priority. */
defaultSelector(): AnalysisProviderSelector {
return this.#defaultSelector;
}
/** List configured candidate identities in canonical provider-ID order. */
identities(): readonly ProviderIdentity[] {
return this.#providers.map((provider) =>
structuredClone(provider.identity()),
);
}
/** List the unique deep-operation declarations without probing candidates. */
declaredOperations(): readonly string[] {
return [...this.#declaredOperations];
}
/** Discover candidates for status without resolving profiles or starting clients. */
candidates(
target?: BinaryTarget,
): readonly AnalysisProviderCandidateStatus[] {
return this.#providers.map((provider) => candidateStatus(provider, target));
}
/** Select one whole deep-operation family for a parsed target. */
async select(
target: BinaryTarget,
requestedSelector?: AnalysisProviderSelector,
options: { readonly signal?: AbortSignal } = {},
): Promise<
Result<
AnalysisProviderSelection,
ProviderSelectionError | AnalysisCancelledError
>
> {
if (signalIsAborted(options.signal))
return err(new AnalysisCancelledError("open_binary"));
const parsedRequest =
requestedSelector === undefined
? undefined
: analysisProviderSelectorSchema.safeParse(requestedSelector);
if (parsedRequest !== undefined && !parsedRequest.success)
return err(
selectionError(
"invalid_options",
typeof requestedSelector === "string"
? requestedSelector
: "<invalid>",
this.candidates(target),
),
);
const request = parsedRequest?.data;
const selector = request ?? this.#defaultSelector;
const baseline = this.candidates(target);
if (signalIsAborted(options.signal))
return err(new AnalysisCancelledError("open_binary"));
if (
request === undefined &&
selector !== "auto" &&
baseline.length > 0 &&
baseline.every(
({ targetSupport }) => targetSupport.status === "unsupported",
)
)
return ok({
requestedProviderId: selector,
binding: null,
candidates: baseline,
});
if (selector !== "auto")
return this.#selectConcrete(
{ target, baseline, signal: options.signal },
selector,
request === undefined ? "environment" : "request",
);
return this.#selectAutomatically({
target,
baseline,
signal: options.signal,
});
}
/** Explain why a deep operation is unavailable on an unbound target. */
unboundOperationError(
operation: string,
selection: AnalysisProviderSelection,
): ProviderSelectionError {
const reason: ProviderSelectionFailureReason =
selection.candidates.length > 0 &&
selection.candidates.every(
({ targetSupport }) => targetSupport.status === "unsupported",
)
? "target_unsupported"
: "provider_unavailable";
return selectionError(
reason,
selection.requestedProviderId,
selection.candidates,
operation,
);
}
async #selectConcrete(
context: TargetSelectionContext,
providerId: string,
source: AnalysisProviderBinding["selectionSource"],
): Promise<
Result<
AnalysisProviderSelection,
ProviderSelectionError | AnalysisCancelledError
>
> {
const { baseline, signal, target } = context;
const provider = this.#byId.get(providerId);
if (provider === undefined)
return err(selectionError("unknown_provider", providerId, baseline));
const initial = baseline.find(
({ provider: identity }) => identity.id === providerId,
);
if (initial === undefined)
throw new TypeError(
"Registered provider is absent from discovery output",
);
if (initial.targetSupport.status !== "supported")
return err(selectionError("target_unsupported", providerId, baseline));
if (initial.availability.status === "unavailable")
return err(selectionError("provider_unavailable", providerId, baseline));
const evaluation = await evaluateAnalysisProviderCandidate(
provider,
initial,
target,
signal,
);
if (!evaluation.ok) return evaluation;
const evaluated = evaluation.value;
const statuses = replaceCandidate(baseline, evaluated.status);
if (
evaluated.profile === undefined ||
evaluated.compatibility === undefined
)
return err(selectionError("provider_unavailable", providerId, statuses));
return ok(
selectedResult(
providerId,
source,
{
...evaluated,
profile: evaluated.profile,
compatibility: evaluated.compatibility,
},
statuses,
),
);
}
async #selectAutomatically(
context: TargetSelectionContext,
): Promise<
Result<
AnalysisProviderSelection,
ProviderSelectionError | AnalysisCancelledError
>
> {
const { baseline, signal, target } = context;
const results = await Promise.all(
this.#providers.map(
async (
provider,
): Promise<
Result<AnalysisProviderCandidateEvaluation, AnalysisCancelledError>
> => {
const status = baseline.find(
({ provider: identity }) => identity.id === provider.identity().id,
);
if (status === undefined)
throw new TypeError(
"Registered provider is absent from discovery output",
);
return status.availability.status === "available" &&
status.targetSupport.status === "supported"
? evaluateAnalysisProviderCandidate(
provider,
status,
target,
signal,
)
: ok({
candidate: provider,
status,
} satisfies AnalysisProviderCandidateEvaluation);
},
),
);
const cancelled = results.find((result) => !result.ok);
if (cancelled !== undefined && !cancelled.ok) return cancelled;
const evaluations = results.map((result) => {
if (!result.ok)
throw new TypeError("Cancelled candidate evaluation disappeared");
return result.value;
});
const statuses = evaluations.map(({ status }) => status);
const usable = evaluations.filter(
(
evaluation,
): evaluation is AnalysisProviderCandidateEvaluation & {
readonly profile: AnalysisProfileCommitment;
readonly compatibility: Readonly<Record<string, JsonValue>>;
} =>
evaluation.profile !== undefined &&
evaluation.compatibility !== undefined,
);
if (usable.length === 0)
return ok({
requestedProviderId: "auto",
binding: null,
candidates: statuses,
});
if (usable.length > 1)
return err(selectionError("ambiguous", "auto", statuses));
const selected = usable[0];
if (selected === undefined)
throw new TypeError("Usable provider selection disappeared");
return ok(
selectedResult("auto", "auto-single-candidate", selected, statuses),
);
}
}
const candidateStatus = (
provider: AnalysisProviderCandidate,
target: BinaryTarget | undefined,
): AnalysisProviderCandidateStatus => ({
provider: structuredClone(provider.identity()),
availability: cloneAvailability(provider.inspectAvailability()),
targetSupport:
target === undefined
? {
status: "unknown",
code: null,
reason: "No target is open.",
diagnostics: {},
}
: cloneTargetSupport(provider.inspectTargetSupport(target)),
selected: false,
capabilities: provider
.capabilities()
.map((capability) => structuredClone(capability)),
});
const signalIsAborted = (signal: AbortSignal | undefined): boolean =>
signal?.aborted === true;
const selectedResult = (
requestedProviderId: string,
source: AnalysisProviderBinding["selectionSource"],
evaluated: AnalysisProviderCandidateEvaluation & {
readonly profile: AnalysisProfileCommitment;
readonly compatibility: Readonly<Record<string, JsonValue>>;
},
statuses: readonly AnalysisProviderCandidateStatus[],
): AnalysisProviderSelection => {
const selectedStatus = {
...evaluated.status,
provider: structuredClone(evaluated.profile.provider),
selected: true,
} satisfies AnalysisProviderCandidateStatus;
return {
requestedProviderId,
binding: {
provider: evaluated.candidate,
identity: structuredClone(evaluated.profile.provider),
selectionSource: source,
profile: structuredClone(evaluated.profile),
compatibility: structuredClone(evaluated.compatibility),
},
candidates: replaceCandidate(statuses, selectedStatus),
};
};
const replaceCandidate = (
statuses: readonly AnalysisProviderCandidateStatus[],
replacement: AnalysisProviderCandidateStatus,
): readonly AnalysisProviderCandidateStatus[] =>
statuses.map((status) =>
status.provider.id === replacement.provider.id ? replacement : status,
);
const selectionError = (
reason: ProviderSelectionFailureReason,
requestedProviderId: string,
candidates: readonly AnalysisProviderCandidateStatus[],
operation = "open_binary",
): ProviderSelectionError =>
new ProviderSelectionError({
operation,
reason,
requestedProviderId,
candidateIds: candidates.map(({ provider }) => provider.id),
rejections: candidateRejections(candidates),
});
const candidateRejections = (
candidates: readonly AnalysisProviderCandidateStatus[],
): readonly ProviderSelectionRejection[] =>
candidates.flatMap((candidate) =>
[
...(candidate.availability.status === "unavailable"
? [candidate.availability]
: []),
...(candidate.targetSupport.status === "unsupported"
? [candidate.targetSupport]
: []),
].map((rejection) => ({
providerId: candidate.provider.id,
code: rejection.code,
reason: rejection.reason,
diagnostics: structuredClone(rejection.diagnostics),
})),
);
const cloneAvailability = (
availability: ProviderAvailability,
): ProviderAvailability => ({
...availability,
diagnostics: jsonObjectSchema.parse(availability.diagnostics),
});
const cloneTargetSupport = (
support: ProviderTargetSupport,
): ProviderTargetSupport => ({
...support,
diagnostics: jsonObjectSchema.parse(support.diagnostics),
});
@@ -0,0 +1,115 @@
import { describe, expect, it } from "vitest";
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
import {
ANALYSIS_SNAPSHOT_PROFILE,
ANALYSIS_SNAPSHOT_PROVIDER,
ANALYSIS_SNAPSHOT_TARGET,
} from "../../domain/analysisSnapshot.fixture.js";
import { createEvidence } from "../../domain/evidence.js";
import { createEvidenceBundle } from "../../domain/evidenceBundle.js";
import { ok } from "../../domain/result.js";
import { createAnalysisExecution } from "../AnalysisProvider.js";
import { AnalysisSnapshotCache } from "./AnalysisSnapshotCache.js";
describe("analysis snapshot cache partitioning", () => {
it("returns detached data only from the exact provider/profile partition", () => {
const cache = new AnalysisSnapshotCache();
const execution = createAnalysisExecution(
"main",
ANALYSIS_SNAPSHOT_PROVIDER,
{
analysisProfile: ANALYSIS_SNAPSHOT_PROFILE,
rawResult: "main",
subject: {
path: ANALYSIS_SNAPSHOT_TARGET.path,
sha256: ANALYSIS_SNAPSHOT_TARGET.sha256,
format: ANALYSIS_SNAPSHOT_TARGET.format,
...(ANALYSIS_SNAPSHOT_TARGET.architecture === undefined
? {}
: { architecture: ANALYSIS_SNAPSHOT_TARGET.architecture }),
},
},
);
cache.record({
target: ANALYSIS_SNAPSHOT_TARGET,
profile: ANALYSIS_SNAPSHOT_PROFILE,
operation: "address_name",
parameters: { address: "0x1000", document: "fixture" },
execution,
});
const evidence = createEvidence(
ANALYSIS_SNAPSHOT_TARGET,
ANALYSIS_SNAPSHOT_PROVIDER,
{
operation: "address_name",
parameters: { address: "0x1000", document: "fixture" },
result: "main",
rawResult: "main",
analysisProfile: ANALYSIS_SNAPSHOT_PROFILE,
},
);
const exported = cache.export(
ANALYSIS_SNAPSHOT_TARGET,
ANALYSIS_SNAPSHOT_PROFILE,
createEvidenceBundle([evidence]),
);
expect(exported.ok).toBe(true);
if (!exported.ok) throw new Error("snapshot export failed");
const tampered = structuredClone(exported.value);
const tamperedEntry = tampered.entries[0];
if (tamperedEntry === undefined)
throw new Error("exported snapshot entry missing");
tamperedEntry.execution.result = "changed without Evidence";
let mergedEvidence = false;
expect(
new AnalysisSnapshotCache().import(tampered, undefined, () => {
mergedEvidence = true;
return ok(0);
}).ok,
).toBe(false);
expect(mergedEvidence).toBe(false);
const entry = exported.value.entries[0];
if (entry === undefined) throw new Error("exported snapshot entry missing");
Reflect.set(entry.execution, "provider", {
id: "forged",
name: "Forged",
version: "9",
});
entry.execution.limitations.push("forged");
expect(
cache.lookup(
ANALYSIS_SNAPSHOT_TARGET,
ANALYSIS_SNAPSHOT_PROFILE,
"address_name",
{ address: "0x1000", document: "fixture" },
),
).toMatchObject({
provider: ANALYSIS_SNAPSHOT_PROVIDER,
analysisProfile: ANALYSIS_SNAPSHOT_PROFILE,
limitations: [expect.stringContaining("local REA analysis snapshot")],
});
const changedProfile = createAnalysisProfile(ANALYSIS_SNAPSHOT_PROVIDER, {
loader: "configured-override",
});
expect(
cache.lookup(ANALYSIS_SNAPSHOT_TARGET, changedProfile, "address_name", {
address: "0x1000",
document: "fixture",
}),
).toBeUndefined();
const changedProviderProfile = createAnalysisProfile(
{ id: "other", name: "Other", version: "1" },
{ loader: "mach-o-arm64" },
);
expect(
cache.lookup(
ANALYSIS_SNAPSHOT_TARGET,
changedProviderProfile,
"address_name",
{ address: "0x1000", document: "fixture" },
),
).toBeUndefined();
});
});
@@ -0,0 +1,269 @@
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import type { EvidenceBundle } from "../../domain/evidenceBundle.js";
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
import { NoBinaryOpenError } from "../../domain/configurationErrors.js";
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
import { err, ok, type Result } from "../../domain/result.js";
import {
analysisQueryId,
createAnalysisSnapshotEntry,
snapshotBinding,
snapshotMatchesProfile,
snapshotMatchesTarget,
snapshotTarget,
parseAnalysisSnapshot,
type AnalysisSnapshot,
type AnalysisSnapshotEntry,
type AnalysisSnapshotBinding,
type AnalysisSnapshotTarget,
} from "../../domain/analysisSnapshot.js";
import type {
AnalysisExecution,
AnalysisOperation,
CapabilityDescriptor,
} from "../AnalysisProvider.js";
import { GENERATED_MCP_TOOL_CATALOG } from "../../generatedMcpToolCatalog.js";
const STATEFUL_OPERATIONS: ReadonlySet<AnalysisOperation> = new Set([
"health",
"current_address",
"current_procedure",
"current_document",
"goto_address",
"list_documents",
"set_current_document",
]);
const CURSOR_DEFAULT_OPERATIONS: ReadonlySet<AnalysisOperation> = new Set([
"address_name",
"comment",
"inline_comment",
"next_address",
"prev_address",
"xrefs",
]);
const DOCUMENT_SCOPED_OPERATIONS: ReadonlySet<string> = new Set(
GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "official-proxy")
.map(({ name }) => name)
.filter(
(name) =>
name !== "current_document" &&
name !== "list_documents" &&
name !== "set_current_document",
),
);
/** Whether an operation is immutable and independent of provider UI state. */
export const isSnapshotCacheable = (
operation: AnalysisOperation,
descriptor: CapabilityDescriptor | undefined,
parameters: Readonly<Record<string, JsonValue>>,
): descriptor is CapabilityDescriptor =>
!STATEFUL_OPERATIONS.has(operation) &&
(!DOCUMENT_SCOPED_OPERATIONS.has(operation) ||
typeof parameters.document === "string") &&
(!CURSOR_DEFAULT_OPERATIONS.has(operation) ||
typeof parameters.address === "string") &&
descriptor?.effects.mutatesArtifact === false &&
descriptor.cachePolicy !== "live" &&
descriptor.effects.mayWriteFilesystem === false &&
descriptor.effects.changesPermissions === false;
/** Bounded in-memory cache for one immutable binary identity. */
export class AnalysisSnapshotCache {
readonly #entries = new Map<string, AnalysisSnapshotEntry>();
#target: AnalysisSnapshotTarget | undefined;
#binding: AnalysisSnapshotBinding | undefined;
/** Whether staged entries belong to the supplied target and profile. */
matches(
target: BinaryTarget,
profile: AnalysisProfileCommitment | undefined,
): boolean {
return (
(this.#target === undefined && this.#binding === undefined) ||
(this.#target !== undefined &&
this.#binding !== undefined &&
profile !== undefined &&
snapshotMatchesTarget(this.#target, target) &&
snapshotMatchesProfile(this.#binding, profile))
);
}
/** Replace cache state when the target or selected profile changes. */
select(target: BinaryTarget, profile: AnalysisProfileCommitment): void {
if (!this.matches(target, profile)) this.#entries.clear();
this.#target = snapshotTarget(target);
this.#binding = snapshotBinding(profile);
}
/** Merge already-validated entries and return the new-entry count. */
stage(snapshot: AnalysisSnapshot): number {
if (
(this.#target !== undefined &&
JSON.stringify(this.#target) !== JSON.stringify(snapshot.target)) ||
(this.#binding !== undefined &&
!snapshotMatchesProfile(
this.#binding,
snapshot.binding.analysis_profile,
))
)
this.#entries.clear();
this.#target = structuredClone(snapshot.target);
this.#binding = structuredClone(snapshot.binding);
let imported = 0;
for (const entry of snapshot.entries) {
if (!this.#entries.has(entry.query_id)) imported += 1;
this.#entries.set(entry.query_id, structuredClone(entry));
}
return imported;
}
/** Build a complete snapshot for an active target. */
export(
target: BinaryTarget | undefined,
profile: AnalysisProfileCommitment | undefined,
evidenceBundle: EvidenceBundle,
): Result<AnalysisSnapshot, AnalysisError> {
if (target === undefined || profile === undefined)
return err(new NoBinaryOpenError());
try {
const snapshotTargetIdentity = snapshotTarget(target);
const binding = snapshotBinding(profile);
const retainedEvidence = structuredClone(evidenceBundle);
return ok(
parseAnalysisSnapshot({
target: snapshotTargetIdentity,
binding,
entries: this.entries(),
evidence_bundle: retainedEvidence,
}),
);
} catch (cause: unknown) {
return err(
new EvidenceIntegrityError("Analysis snapshot validation failed", {
cause,
}),
);
}
}
/** Validate target identity, merge evidence atomically, then stage entries. */
import(
snapshot: AnalysisSnapshot,
active:
| {
readonly target: BinaryTarget;
readonly profile: AnalysisProfileCommitment;
}
| undefined,
mergeEvidence: (
bundle: EvidenceBundle,
) => Result<number, EvidenceIntegrityError>,
): Result<number, AnalysisError> {
let validated: AnalysisSnapshot;
try {
validated = parseAnalysisSnapshot(snapshot);
} catch (cause: unknown) {
return err(
new EvidenceIntegrityError("Analysis snapshot validation failed", {
cause,
}),
);
}
if (
active !== undefined &&
(!snapshotMatchesTarget(validated.target, active.target) ||
!snapshotMatchesProfile(validated.binding, active.profile))
)
return err(
new EvidenceIntegrityError(
"Analysis snapshot profile_mismatch: target, provider, or analysis profile does not match the active binary",
),
);
const importedEvidence = mergeEvidence(validated.evidence_bundle);
return importedEvidence.ok ? ok(this.stage(validated)) : importedEvidence;
}
/** Return canonical entries for persistence. */
entries(): AnalysisSnapshotEntry[] {
return [...this.#entries.values()]
.sort((left, right) => left.query_id.localeCompare(right.query_id))
.map((entry) => structuredClone(entry));
}
/** Replay an exact provider-specific query, marking its cached provenance. */
lookup(
target: BinaryTarget,
profile: AnalysisProfileCommitment,
operation: AnalysisOperation,
parameters: Readonly<Record<string, JsonValue>>,
): AnalysisExecution | undefined {
const queryId = analysisQueryId(
snapshotTarget(target),
snapshotBinding(profile),
operation,
parameters,
);
const cached = this.#entries.get(queryId);
if (cached === undefined) return undefined;
const subject = cached.execution.subject;
return structuredClone({
result: cached.execution.result,
rawResult: cached.execution.raw_result,
provider: cached.execution.provider,
analysisProfile: structuredClone(profile),
limitations: [
...cached.execution.limitations,
"Loaded from a local REA analysis snapshot; this call did not re-run the provider.",
],
locations: cached.execution.locations,
subject:
subject === null
? null
: subject.architecture === null
? {
path: subject.path,
sha256: subject.sha256,
format: subject.format,
}
: {
path: subject.path,
sha256: subject.sha256,
format: subject.format,
architecture: subject.architecture,
},
});
}
/** Record one successful immutable call unless the cache is full. */
record(input: {
readonly target: BinaryTarget;
readonly profile: AnalysisProfileCommitment;
readonly operation: AnalysisOperation;
readonly parameters: Readonly<Record<string, JsonValue>>;
readonly execution: AnalysisExecution;
}): void {
const { target, profile, operation, parameters, execution } = input;
if (this.#entries.size >= 10_000) return;
this.select(target, profile);
const entry = createAnalysisSnapshotEntry({
target: snapshotTarget(target),
binding: snapshotBinding(profile),
operation,
parameters,
execution,
});
this.#entries.set(entry.query_id, entry);
}
/** Forget all target-bound entries. */
clear(): void {
this.#entries.clear();
this.#target = undefined;
this.#binding = undefined;
}
}
@@ -0,0 +1,52 @@
import {
parseAnalysisSnapshot,
serializeAnalysisSnapshot,
type AnalysisSnapshot,
} from "../../domain/analysisSnapshot.js";
import {
EvidenceFileError,
EvidenceIntegrityError,
} from "../../domain/evidenceErrors.js";
import { err, ok, type Result } from "../../domain/result.js";
import { readJsonFile, writeTextFile } from "../JsonFiles.js";
type SnapshotFailure = EvidenceFileError | EvidenceIntegrityError;
/** Read and validate an analysis snapshot from the caller's path. */
export const readAnalysisSnapshot = async (
path: string,
): Promise<Result<AnalysisSnapshot, SnapshotFailure>> => {
const loaded = await readJsonFile(path);
if (!loaded.ok) return loaded;
try {
return ok(parseAnalysisSnapshot(loaded.value));
} catch (cause: unknown) {
return err(
new EvidenceIntegrityError(snapshotValidationMessage(cause), { cause }),
);
}
};
/** Atomically write a deterministic analysis snapshot to the caller's path. */
export const writeAnalysisSnapshot = async (
snapshot: AnalysisSnapshot,
path: string,
overwrite: boolean,
): Promise<
Result<{ readonly path: string; readonly bytes: number }, SnapshotFailure>
> => {
let encoded: string;
try {
encoded = serializeAnalysisSnapshot(snapshot);
} catch (cause: unknown) {
return err(
new EvidenceIntegrityError(snapshotValidationMessage(cause), { cause }),
);
}
return writeTextFile(encoded, path, overwrite);
};
const snapshotValidationMessage = (cause: unknown): string =>
cause instanceof TypeError
? cause.message
: "Analysis snapshot validation failed";
+530
View File
@@ -0,0 +1,530 @@
import { randomUUID } from "node:crypto";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import {
analysisProfilesEqual,
type AnalysisProfileCommitment,
} from "../../domain/analysisProfile.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
import { err, ok, type Result } from "../../domain/result.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import { createEvidence } from "../../domain/evidence.js";
import type {
AnalysisClient,
AnalysisExecution,
ExecutionOptions,
AnalysisOperationPort,
ProviderIdentity,
} from "../AnalysisProvider.js";
import type { AnalysisOperation } from "../AnalysisProvider.js";
import { OFFICIAL_TOOL_CONTRACTS } from "../../contracts/officialToolContracts.js";
import { ENHANCED_TOOL_CONTRACTS } from "../../contracts/enhancedToolContracts.js";
import type { BinarySessionPort } from "./BinarySessionPort.js";
import {
SessionProviderRouter,
type SessionProviderRoute,
} from "./SessionProviderRouter.js";
import { InvestigationRecords } from "../investigation/InvestigationRecords.js";
import { BinarySessionRecords } from "./BinarySessionRecords.js";
import { binarySessionStatus } from "./BinarySessionStatus.js";
import {
resolveSessionOpen,
type BinarySessionOpenOptions,
} from "./BinarySessionOpen.js";
import {
bindExecutionTarget,
commitExecutionProfile,
prepareSessionExecution,
} from "./BinarySessionExecution.js";
import { closeAnalysisClient } from "./AnalysisClientCleanup.js";
export type { BinarySessionPort } from "./BinarySessionPort.js";
const OFFICIAL_OPERATIONS: ReadonlySet<string> = new Set(
OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name),
);
const ENHANCED_OPERATIONS: ReadonlySet<string> = new Set(
ENHANCED_TOOL_CONTRACTS.map(({ name }) => name),
);
/**
* Owns the single active target shared by CLI and MCP adapters.
*
* Target transitions are serialized because switching targets tears down the
* active provider client. A failed switch recreates the previous target instead
* of retaining a client whose resources were already shut down.
*/
export class BinarySession
extends BinarySessionRecords
implements BinarySessionPort
{
#active:
| {
readonly target: BinaryTarget;
readonly client: AnalysisClient;
readonly profile: AnalysisProfileCommitment | null;
readonly compatibility: Readonly<Record<string, JsonValue>>;
readonly route: SessionProviderRoute;
readonly runId: string;
}
| undefined;
#transition: Promise<void> = Promise.resolve();
readonly #calls = new Set<Promise<unknown>>();
readonly #providerRouter: SessionProviderRouter;
readonly #runtimeUnavailability = new Map<
string,
{ readonly reason: string }
>();
readonly #availabilityListeners = new Set<() => void | Promise<void>>();
constructor(
providerRouter: SessionProviderRouter,
records: InvestigationRecords = new InvestigationRecords(),
) {
super(records);
this.#providerRouter = providerRouter;
}
/** Identify the provider producing evidence for this session. */
providerIdentity(operation?: AnalysisOperation): ProviderIdentity {
const route = this.#currentRoute();
let selected = route.identity;
if (operation !== undefined) {
const exact = route.capabilities?.get(operation)?.provider;
if (exact !== undefined) selected = exact;
else if (ENHANCED_OPERATIONS.has(operation)) {
const providers = new Map<string, ProviderIdentity>();
for (const descriptor of route.capabilities?.values() ?? [])
if (
descriptor.available &&
OFFICIAL_OPERATIONS.has(descriptor.operation)
)
providers.set(descriptor.provider.id, descriptor.provider);
if (providers.size === 1) {
const provider = providers.values().next().value;
if (provider !== undefined) selected = provider;
}
}
}
const profile = this.#active?.profile;
return structuredClone(
profile !== null &&
profile !== undefined &&
profile.provider.id === selected.id
? profile.provider
: selected,
);
}
/** External live state cannot be replayed by a direct or composed CLI workflow. */
allowsSnapshotReplay(operation: AnalysisOperation): boolean {
const capabilities = this.#active?.route.capabilities;
const descriptor = capabilities?.get(operation);
if (descriptor !== undefined) return descriptor.cachePolicy !== "live";
return ![...(capabilities?.values() ?? [])].some(
({ cachePolicy }) => cachePolicy === "live",
);
}
/** Return the selected immutable profile, optionally scoped to an operation. */
analysisProfile(
operation?: AnalysisOperation,
): AnalysisProfileCommitment | undefined {
const profile = this.#active?.profile;
if (profile === null || profile === undefined) return undefined;
if (
operation !== undefined &&
this.providerIdentity(operation).id !== profile.provider.id
)
return undefined;
return structuredClone(profile);
}
/** Return opaque adapter metadata retained for legacy open_binary output. */
openCompatibility(): Readonly<Record<string, JsonValue>> {
return structuredClone(this.#active?.compatibility ?? {});
}
/** Observe runtime provider-health changes that affect discovery metadata. */
onAvailabilityChanged(listener: () => void | Promise<void>): () => void {
this.#availabilityListeners.add(listener);
return () => this.#availabilityListeners.delete(listener);
}
/**
* Open or switch targets after draining calls against the current target.
* Returns the switch failure even if best-effort restoration also fails.
*/
open(
path: string,
options: BinarySessionOpenOptions = {},
): Promise<Result<BinaryTarget, AnalysisError>> {
return this.#serialize(async () => {
if (isAborted(options.signal))
return err(new AnalysisCancelledError("open_binary"));
const resolved = await resolveSessionOpen({
router: this.#providerRouter,
current: this.#active,
path,
options,
stagedSnapshotMatches: (target, profile) =>
this.matchesSnapshot(target, profile),
});
if (!resolved.ok) return resolved;
const { target, route, sameTarget } = resolved.value;
const { profile, compatibility } = route;
if (isAborted(options.signal))
return err(new AnalysisCancelledError("open_binary"));
const activeProfile = this.#active?.profile;
const sameProfile =
activeProfile === null || activeProfile === undefined
? profile === null
: profile !== null && analysisProfilesEqual(activeProfile, profile);
if (sameTarget && sameProfile) {
if (options.snapshot !== undefined) {
const imported = this.importAnalysisSnapshot(options.snapshot);
if (!imported.ok) return imported;
}
this.resetSnapshotInvalidation();
return ok(target);
}
await this.#drainCalls();
const previous = this.#active;
this.#active = undefined;
if (previous !== undefined) {
const closed = await closeAnalysisClient(
previous.client,
previous.route.identity.id,
);
if (!closed.ok) {
this.clearSessionRecords();
this.#clearRuntimeAvailability();
return closed;
}
}
const runId = randomUUID();
const client = route.createClient(target, { runId });
const started = await client.execute("health", {}, options);
if (!started.ok) {
await client.close();
await this.#restore(previous);
return started;
}
if (isAborted(options.signal)) {
await client.close();
await this.#restore(previous);
return err(new AnalysisCancelledError("open_binary"));
}
this.#active = {
target,
client,
profile,
compatibility: structuredClone(compatibility),
route,
runId,
};
this.#clearRuntimeAvailability();
if (options.snapshot === undefined) {
if (profile === null) this.clearSnapshot();
else this.selectSnapshot(target, profile);
}
if (options.snapshot !== undefined) {
const imported = this.importAnalysisSnapshot(options.snapshot);
if (!imported.ok) {
this.#active = undefined;
await client.close();
await this.#restore(previous);
return imported;
}
}
this.resetSnapshotInvalidation();
return ok(target);
});
}
/** Close the active target, if any. */
close(
options: Pick<ExecutionOptions, "progress"> & {
readonly retainProviderDocuments?: boolean;
} = {},
): Promise<Result<null, AnalysisError>> {
return this.#serialize(async () => {
const previous = this.#active;
this.#active = undefined;
await this.#drainCalls();
const closed =
previous === undefined
? ok(null)
: await closeAnalysisClient(
previous.client,
previous.route.identity.id,
{
...(options.progress === undefined
? {}
: { progress: options.progress }),
...(options.retainProviderDocuments === true
? { retainDocument: true }
: {}),
},
);
this.clearSessionRecords();
this.#clearRuntimeAvailability();
return closed;
});
}
/** Describe the active binary session. */
status(): JsonValue {
return binarySessionStatus({
target: this.#active?.target,
route: this.#currentRoute(),
router: this.#providerRouter,
runtimeUnavailability: this.#runtimeUnavailability,
runId: this.#active?.runId,
runtimeLineageSnapshots:
this.#active?.client.runtimeLineageSnapshots?.() ?? [],
requestActivitySnapshots:
this.#active?.client.requestActivitySnapshots?.() ?? [],
});
}
/** Return the immutable artifact identity captured before its provider started. */
activeTarget(): BinaryTarget | undefined {
return this.#active === undefined
? undefined
: structuredClone(this.#active.target);
}
protected activeAnalysisBinding() {
return this.#active;
}
/**
* Invoke a provider operation against the active target.
* Calls may overlap, but a pending target transition prevents new calls from
* entering until the transition has settled.
*/
async execute(
name: Parameters<AnalysisOperationPort["execute"]>[0],
arguments_: Readonly<Record<string, JsonValue>>,
options?: { readonly signal?: AbortSignal },
): Promise<Result<AnalysisExecution, AnalysisError>> {
const transitioned = await this.#waitForTransition(name, options?.signal);
if (!transitioned.ok) return transitioned;
const prepared = prepareSessionExecution({
active: this.#active,
operation: name,
parameters: arguments_,
unboundOperationError: (operation, route) =>
this.#providerRouter.unboundOperationError(operation, route),
lookupSnapshot: (target, profile, operation, parameters) =>
operation === "decode_interface_builder" ||
operation === "inspect_asset_catalog" ||
operation === "inspect_keyed_archive"
? undefined
: this.lookupSnapshot(target, profile, operation, parameters),
});
if (!prepared.ok) return prepared;
const { active, capability, profile, cacheable, cached } = prepared.value;
if (cached !== undefined) return ok(cached);
const call = active.client.execute(name, arguments_, options);
this.#calls.add(call);
try {
const result = await call;
const profiled = bindExecutionTarget(
commitExecutionProfile(name, result, profile),
name,
active.target,
);
this.#observeRuntimeAvailability(name, profiled);
if (
profiled.ok &&
cacheable &&
profile !== undefined &&
name !== "decode_interface_builder" &&
name !== "inspect_asset_catalog" &&
name !== "inspect_keyed_archive"
) {
const evidence = createEvidence(
profiled.value.subject ?? active.target,
profiled.value.provider,
{
operation: name,
parameters: arguments_,
result: profiled.value.result,
analysisProfile: profile,
rawResult: profiled.value.rawResult,
limitations: profiled.value.limitations,
locations: profiled.value.locations,
},
);
const recorded = this.recordEvidence(evidence);
if (!recorded.ok) return recorded;
this.recordSnapshot({
target: active.target,
profile,
operation: name,
parameters: arguments_,
execution: profiled.value,
});
} else if (profiled.ok && capability?.effects.mutatesArtifact === true) {
this.invalidateSnapshot();
}
return profiled;
} finally {
this.#calls.delete(call);
}
}
#observeRuntimeAvailability(
operation: AnalysisOperation,
result: Result<AnalysisExecution, AnalysisError>,
): void {
if (result.ok) {
if (this.#markRuntimeAvailable(operation))
this.#emitAvailabilityChanged();
return;
}
if (result.error._tag === "AnalysisCapabilityUnavailableError") {
if (this.#markRuntimeUnavailable(operation, result.error.message))
this.#emitAvailabilityChanged();
return;
}
if (
[
"ProviderAdapterError",
"HopperProcessError",
"HopperStartError",
].includes(result.error._tag)
) {
const capabilities = this.#active?.route.capabilities;
const providerId = capabilities?.get(operation)?.provider.id;
let changed = false;
for (const descriptor of capabilities?.values() ?? [])
if (providerId !== undefined && descriptor.provider.id === providerId)
changed =
this.#markRuntimeUnavailable(
descriptor.operation,
`${result.error._tag}: ${result.error.message}`,
) || changed;
if (changed) this.#emitAvailabilityChanged();
}
}
#markRuntimeAvailable(operation: string): boolean {
return this.#runtimeUnavailability.delete(operation);
}
#markRuntimeUnavailable(operation: string, reason: string): boolean {
const current = this.#runtimeUnavailability.get(operation);
if (current?.reason === reason) return false;
this.#runtimeUnavailability.set(operation, { reason });
return true;
}
#clearRuntimeAvailability(): void {
if (this.#runtimeUnavailability.size === 0) return;
this.#runtimeUnavailability.clear();
this.#emitAvailabilityChanged();
}
#emitAvailabilityChanged(): void {
for (const listener of this.#availabilityListeners) {
try {
const notification = listener();
if (notification !== undefined) {
// best-effort cleanup: async observer notifications must not reject
// unhandled; state transitions and other listeners continue.
void notification.catch(() => undefined);
}
} catch (cause: unknown) {
// External observers are best-effort notifications. Contain only the
// callback failure so state transitions and other listeners continue.
void cause;
}
}
}
#currentRoute(): SessionProviderRoute {
return this.#active?.route ?? this.#providerRouter.initialRoute();
}
#serialize<T>(operation: () => Promise<T>): Promise<T> {
const result = this.#transition.then(operation, operation);
this.#transition = result.then(
() => undefined,
() => undefined,
);
return result;
}
async #drainCalls(): Promise<void> {
await Promise.allSettled(this.#calls);
}
async #restore(
previous:
| {
readonly target: BinaryTarget;
readonly client: AnalysisClient;
readonly profile: AnalysisProfileCommitment | null;
readonly compatibility: Readonly<Record<string, JsonValue>>;
readonly route: SessionProviderRoute;
readonly runId: string;
}
| undefined,
): Promise<void> {
if (previous === undefined) return;
const runId = randomUUID();
const client = previous.route.createClient(previous.target, { runId });
const started = await client.execute("health", {});
if (started.ok)
this.#active = {
target: previous.target,
client,
profile: previous.profile,
compatibility: previous.compatibility,
route: previous.route,
runId,
};
else await client.close();
}
async #waitForTransition(
operation: string,
signal?: AbortSignal,
): Promise<Result<undefined, AnalysisCancelledError>> {
if (signal?.aborted === true)
return err(new AnalysisCancelledError(operation));
if (signal === undefined) {
await this.#transition;
return ok(undefined);
}
return new Promise((resolve) => {
const onAbort = (): void => {
resolve(err(new AnalysisCancelledError(operation)));
};
signal.addEventListener("abort", onAbort, { once: true });
this.#transition.then(
() => {
signal.removeEventListener("abort", onAbort);
resolve(
signal.aborted
? err(new AnalysisCancelledError(operation))
: ok(undefined),
);
},
() => {
signal.removeEventListener("abort", onAbort);
resolve(
signal.aborted
? err(new AnalysisCancelledError(operation))
: ok(undefined),
);
},
);
});
}
}
const isAborted = (signal: AbortSignal | undefined): boolean =>
signal?.aborted === true;
@@ -0,0 +1,20 @@
import { InvestigationRecords } from "../investigation/InvestigationRecords.js";
import type { AnalysisProvider } from "../AnalysisProvider.js";
import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js";
import { BinarySession } from "./BinarySession.js";
import { SessionProviderRouter } from "./SessionProviderRouter.js";
/**
* Compose one provider-neutral session from deep-provider selection and
* disjoint auxiliary operation families.
*/
export const composeBinarySession = (
registry: AnalysisProviderRegistry | SessionProviderRouter,
auxiliaryProviders: readonly AnalysisProvider[] = [],
): BinarySession =>
new BinarySession(
registry instanceof SessionProviderRouter
? registry
: SessionProviderRouter.selectable(registry, auxiliaryProviders),
new InvestigationRecords(),
);
@@ -0,0 +1,152 @@
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
import { NoBinaryOpenError } from "../../domain/configurationErrors.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import type { EvidenceSubjectTarget } from "../../domain/evidence.js";
import { err, ok, type Result } from "../../domain/result.js";
import type {
AnalysisClient,
AnalysisExecution,
AnalysisOperation,
CapabilityDescriptor,
} from "../AnalysisProvider.js";
import { isSnapshotCacheable } from "./AnalysisSnapshotCache.js";
import type { SessionProviderRoute } from "./SessionProviderRouter.js";
interface ActiveExecutionBinding {
readonly target: BinaryTarget;
readonly client: AnalysisClient;
readonly profile: AnalysisProfileCommitment | null;
readonly route: SessionProviderRoute;
}
export interface PreparedSessionExecution {
readonly active: ActiveExecutionBinding;
readonly capability: CapabilityDescriptor | undefined;
readonly profile: AnalysisProfileCommitment | undefined;
readonly cacheable: boolean;
readonly cached: AnalysisExecution | undefined;
}
interface PrepareSessionExecutionInput {
readonly active: ActiveExecutionBinding | undefined;
readonly operation: AnalysisOperation;
readonly parameters: Readonly<Record<string, JsonValue>>;
readonly unboundOperationError: (
operation: AnalysisOperation,
route: SessionProviderRoute,
) => AnalysisError | undefined;
readonly lookupSnapshot: (
target: BinaryTarget,
profile: AnalysisProfileCommitment,
operation: AnalysisOperation,
parameters: Readonly<Record<string, JsonValue>>,
) => AnalysisExecution | undefined;
}
/** Resolve capability, profile, and snapshot state before invoking a provider. */
export const prepareSessionExecution = (
input: PrepareSessionExecutionInput,
): Result<PreparedSessionExecution, AnalysisError> => {
const {
active,
operation,
parameters,
unboundOperationError,
lookupSnapshot,
} = input;
if (active === undefined) return err(new NoBinaryOpenError());
const capability = active.route.capabilities?.get(operation);
if (
active.route.capabilities !== undefined &&
capability?.available !== true
) {
const selectionError = unboundOperationError(operation, active.route);
if (selectionError !== undefined) return err(selectionError);
return err(
new AnalysisCapabilityUnavailableError(
active.route.binding?.identity.id ?? active.route.identity.id,
operation,
capability?.reason ?? "operation is not declared by this provider",
),
);
}
const profile =
active.profile !== null &&
(capability === undefined ||
capability.provider.id === active.profile.provider.id)
? active.profile
: undefined;
const cacheable =
profile !== undefined &&
isSnapshotCacheable(operation, capability, parameters);
const cached = cacheable
? lookupSnapshot(active.target, profile, operation, parameters)
: undefined;
return ok({ active, capability, profile, cacheable, cached });
};
/** Attach the selected profile only when the provider identity agrees. */
export const commitExecutionProfile = (
operation: AnalysisOperation,
result: Result<AnalysisExecution, AnalysisError>,
profile: AnalysisProfileCommitment | undefined,
): Result<AnalysisExecution, AnalysisError> => {
if (!result.ok || profile === undefined) return result;
const provider = result.value.provider;
if (
provider.id !== profile.provider.id ||
provider.name !== profile.provider.name ||
provider.version !== profile.provider.version
)
return err(
new ProviderAdapterError(profile.provider.id, `${operation}:profile`),
);
return ok({
...result.value,
analysisProfile: structuredClone(profile),
});
};
/** Bind an execution to the selected target, preserving valid artifact subjects. */
export const bindExecutionTarget = (
result: Result<AnalysisExecution, AnalysisError>,
operation: AnalysisOperation,
target: BinaryTarget,
): Result<AnalysisExecution, AnalysisError> => {
if (!result.ok) return result;
const subject: EvidenceSubjectTarget = {
path: target.path,
sha256: target.sha256,
format:
target.format === "analysis-database"
? "analysis-database"
: target.format,
...(target.architecture === undefined
? {}
: { architecture: target.architecture }),
};
if (
result.value.subject !== null &&
result.value.subject.sha256 !== target.sha256 &&
!isArtifactInventorySubject(operation, result.value.subject, target)
)
return err(
new ProviderAdapterError(
result.value.provider.id,
`${operation}:subject`,
),
);
return ok({ ...result.value, subject: result.value.subject ?? subject });
};
const isArtifactInventorySubject = (
operation: AnalysisOperation,
subject: EvidenceSubjectTarget,
target: BinaryTarget,
): boolean =>
(operation === "inventory_artifact" || operation === "inspect_artifact") &&
subject.path === (target.sourcePath ?? target.path);
+113
View File
@@ -0,0 +1,113 @@
import type { ExecutableFormatHint } from "../../domain/dosCom.js";
import type { AnalysisProviderSelector } from "../../contracts/providerSelection.js";
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import {
snapshotMatchesBinding,
snapshotMatchesTarget,
snapshotTarget,
} from "../../domain/analysisSnapshot.js";
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
import { parseBinaryTarget } from "../BinaryTargetResolver.js";
import { type BinaryTarget } from "../../domain/binaryTarget.js";
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
import { err, type Result } from "../../domain/result.js";
import type { SessionProviderRoute } from "./SessionProviderRouter.js";
import { SessionProviderRouter } from "./SessionProviderRouter.js";
export interface BinarySessionOpenOptions {
readonly signal?: AbortSignal;
readonly targetKind?: BinaryTarget["kind"];
readonly formatHint?: ExecutableFormatHint;
readonly snapshot?: AnalysisSnapshot;
readonly providerId?: AnalysisProviderSelector;
}
interface CurrentOpenBinding {
readonly target: BinaryTarget;
readonly profile: AnalysisProfileCommitment | null;
readonly route: SessionProviderRoute;
}
export interface ResolvedSessionOpen {
readonly target: BinaryTarget;
readonly route: SessionProviderRoute;
readonly sameTarget: boolean;
}
interface ResolveSessionOpenInput {
readonly router: SessionProviderRouter;
readonly current: CurrentOpenBinding | undefined;
readonly path: string;
readonly options: BinarySessionOpenOptions;
readonly stagedSnapshotMatches: (
target: BinaryTarget,
profile: AnalysisProfileCommitment | null,
) => boolean;
}
/** Parse a target, resolve its provider route, and validate snapshot binding. */
export const resolveSessionOpen = async (
input: ResolveSessionOpenInput,
): Promise<Result<ResolvedSessionOpen, AnalysisError>> => {
const { router, current, path, options, stagedSnapshotMatches } = input;
const parsed = await parseBinaryTarget(
path,
process.cwd(),
process.arch,
options.targetKind,
options.formatHint,
);
if (!parsed.ok) return parsed;
const target = parsed.value;
const sameTarget =
current?.target.path === target.path &&
snapshotMatchesTarget(snapshotTarget(current.target), target);
const resolvedRoute =
sameTarget && options.providerId === undefined && current !== undefined
? ({ ok: true, value: current.route } as const)
: await router.resolve(target, options.providerId, options.signal);
if (!resolvedRoute.ok) return resolvedRoute;
const route = resolvedRoute.value;
const snapshotError = validateSnapshot({
snapshot: options.snapshot,
target,
profile: route.profile,
current,
stagedSnapshotMatches,
});
if (snapshotError !== undefined) return err(snapshotError);
return { ok: true, value: { target, route, sameTarget } };
};
interface ValidateSnapshotInput {
readonly snapshot: AnalysisSnapshot | undefined;
readonly target: BinaryTarget;
readonly profile: AnalysisProfileCommitment | null;
readonly current: CurrentOpenBinding | undefined;
readonly stagedSnapshotMatches: (
target: BinaryTarget,
profile: AnalysisProfileCommitment | null,
) => boolean;
}
const validateSnapshot = ({
snapshot,
target,
profile,
current,
stagedSnapshotMatches,
}: ValidateSnapshotInput): EvidenceIntegrityError | undefined => {
if (
snapshot !== undefined &&
(profile === null || !snapshotMatchesBinding(snapshot, target, profile))
)
return new EvidenceIntegrityError(
"Analysis snapshot profile_mismatch: target, provider, or analysis profile does not match the requested binary",
);
if (current === undefined && !stagedSnapshotMatches(target, profile))
return new EvidenceIntegrityError(
"Analysis snapshot profile_mismatch: staged target, provider, or analysis profile does not match",
);
return undefined;
};
@@ -0,0 +1,49 @@
import type { InvestigationRecordPort } from "../investigation/InvestigationRecordPort.js";
import type { ExecutableFormatHint } from "../../domain/dosCom.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type { AnalysisProviderSelector } from "../../contracts/providerSelection.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type { Result } from "../../domain/result.js";
import type {
AnalysisOperation,
AnalysisOperationPort,
ExecutionOptions,
ProviderIdentity,
} from "../AnalysisProvider.js";
/** Target lifecycle used by CLI and MCP without exposing a concrete provider. */
export interface BinarySessionPort
extends AnalysisOperationPort, InvestigationRecordPort {
open(
path: string,
options?: {
readonly signal?: AbortSignal;
readonly targetKind?: BinaryTarget["kind"];
readonly formatHint?: ExecutableFormatHint;
readonly snapshot?: AnalysisSnapshot;
readonly providerId?: AnalysisProviderSelector;
},
): Promise<Result<BinaryTarget, AnalysisError>>;
close(
options?: Pick<ExecutionOptions, "progress"> & {
readonly retainProviderDocuments?: boolean;
},
): Promise<Result<null, AnalysisError>>;
status(): JsonValue;
activeTarget(): BinaryTarget | undefined;
exportAnalysisSnapshot(): Result<AnalysisSnapshot, AnalysisError>;
importAnalysisSnapshot(
snapshot: AnalysisSnapshot,
): Result<number, AnalysisError>;
providerIdentity(operation?: AnalysisOperation): ProviderIdentity;
analysisProfile(
operation?: AnalysisOperation,
): AnalysisProfileCommitment | undefined;
openCompatibility(): Readonly<Record<string, JsonValue>>;
onAvailabilityChanged?(listener: () => void | Promise<void>): () => void;
onAnalysisSnapshotChanged?(listener: () => void | Promise<void>): () => void;
}
@@ -0,0 +1,250 @@
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import type { Evidence } from "../../domain/evidence.js";
import type { EvidenceBundle } from "../../domain/evidenceBundle.js";
import { evidenceBundleForTarget } from "../../domain/evidenceBundle.js";
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
import { type UnknownRegistryError } from "../../domain/unknownRegistryError.js";
import type {
RecordUnknownInput,
ResidualUnknown,
UnknownStatus,
UpdateUnknownInput,
} from "../../domain/residualUnknown.js";
import { err, ok, type Result } from "../../domain/result.js";
import type {
AnalysisExecution,
AnalysisOperation,
} from "../AnalysisProvider.js";
import { AnalysisSnapshotCache } from "./AnalysisSnapshotCache.js";
import { InvestigationRecords } from "../investigation/InvestigationRecords.js";
export interface ActiveAnalysisBinding {
readonly target: BinaryTarget;
readonly profile: AnalysisProfileCommitment | null;
}
/** Binary snapshot owner and compatibility facade for composed investigation records. */
export abstract class BinarySessionRecords {
readonly #records: InvestigationRecords;
readonly #snapshot = new AnalysisSnapshotCache();
#snapshotInvalidated = false;
readonly #snapshotListeners = new Set<() => void | Promise<void>>();
constructor(records: InvestigationRecords = new InvestigationRecords()) {
this.#records = records;
}
/** Observe changes to the mutable current analysis snapshot resource. */
onAnalysisSnapshotChanged(listener: () => void | Promise<void>): () => void {
this.#snapshotListeners.add(listener);
return () => this.#snapshotListeners.delete(listener);
}
recordEvidence(
evidence: Evidence,
): Result<"added" | "duplicate", EvidenceIntegrityError> {
const recorded = this.#records.recordEvidence(evidence);
if (recorded.ok && recorded.value === "added") this.#emitSnapshotChanged();
return recorded;
}
hasEvidence(evidenceId: string): boolean {
return this.#records.hasEvidence(evidenceId);
}
evidenceById(evidenceId: string): Evidence | undefined {
return this.#records.evidenceById(evidenceId);
}
exportEvidenceBundle(): EvidenceBundle {
return this.#records.exportEvidenceBundle();
}
importEvidenceBundle(
bundle: unknown,
): Result<number, EvidenceIntegrityError> {
const imported = this.#records.mergeEvidenceBundle(bundle);
if (!imported.ok) return imported;
if (imported.value.changed) this.#emitSnapshotChanged();
return ok(imported.value.recordsAdded);
}
protected abstract activeAnalysisBinding(): ActiveAnalysisBinding | undefined;
exportAnalysisSnapshot(): Result<AnalysisSnapshot, AnalysisError> {
const active = this.activeAnalysisBinding();
if (this.#snapshotInvalidated)
return err(
new EvidenceIntegrityError(
"Analysis snapshots are unavailable after analysis metadata mutations",
),
);
const target = active?.target;
const profile = active?.profile ?? undefined;
if (target !== undefined && profile === undefined)
return err(
new EvidenceIntegrityError(
"Analysis snapshots require a concrete provider analysis profile",
),
);
return this.#snapshot.export(
target,
profile,
target === undefined
? this.#records.exportEvidenceBundle()
: evidenceBundleForTarget(
this.#records.exportEvidenceBundle(),
target.sha256,
),
);
}
importAnalysisSnapshot(
snapshot: AnalysisSnapshot,
): Result<number, AnalysisError> {
const active = this.activeAnalysisBinding();
if (active?.profile === null)
return err(
new EvidenceIntegrityError(
"Analysis snapshot profile_mismatch: the active target has no concrete analysis profile",
),
);
const imported = this.#snapshot.import(
snapshot,
active === undefined
? undefined
: { target: active.target, profile: active.profile },
(bundle) => {
const imported = this.#records.mergeEvidenceBundle(bundle);
return imported.ok ? ok(imported.value.recordsAdded) : imported;
},
);
if (imported.ok) this.#emitSnapshotChanged();
return imported;
}
protected matchesSnapshot(
target: BinaryTarget,
profile: AnalysisProfileCommitment | null,
): boolean {
return this.#snapshot.matches(target, profile ?? undefined);
}
protected selectSnapshot(
target: BinaryTarget,
profile: AnalysisProfileCommitment,
): void {
this.#snapshot.select(target, profile);
this.#emitSnapshotChanged();
}
protected lookupSnapshot(
target: BinaryTarget,
profile: AnalysisProfileCommitment,
operation: AnalysisOperation,
parameters: Readonly<
Record<string, import("../../domain/jsonValue.js").JsonValue>
>,
): AnalysisExecution | undefined {
return this.#snapshot.lookup(target, profile, operation, parameters);
}
protected recordSnapshot(
input: Parameters<AnalysisSnapshotCache["record"]>[0],
): void {
this.#snapshot.record(input);
this.#emitSnapshotChanged();
}
protected invalidateSnapshot(): void {
this.#snapshot.clear();
this.#snapshotInvalidated = true;
this.#emitSnapshotChanged();
}
protected resetSnapshotInvalidation(): void {
if (!this.#snapshotInvalidated) return;
this.#snapshotInvalidated = false;
this.#emitSnapshotChanged();
}
protected clearSnapshot(): void {
this.#snapshot.clear();
this.#emitSnapshotChanged();
}
protected clearSessionRecords(): void {
this.#records.clear();
this.#snapshot.clear();
this.#snapshotInvalidated = false;
this.#emitSnapshotChanged();
}
#emitSnapshotChanged(): void {
for (const listener of this.#snapshotListeners) {
try {
const notification = listener();
if (notification !== undefined)
void notification.catch((cause: unknown) => {
// best-effort cleanup: async observer notifications must not reject unhandled.
void cause;
});
} catch (cause: unknown) {
// External resource observers are best-effort; one callback must not
// make a committed evidence mutation appear to fail.
void cause;
}
}
}
recordUnknown(
input: RecordUnknownInput,
): Result<ResidualUnknown, AnalysisError> {
const target = this.activeAnalysisBinding()?.target;
const recorded = this.#records.recordUnknown(input, target);
if (recorded.ok) this.#emitSnapshotChanged();
return recorded;
}
recordEvidenceWithUnknown(
evidence: Evidence,
input: RecordUnknownInput,
): Result<ResidualUnknown | null, AnalysisError> {
const recorded = this.#records.recordEvidenceWithUnknown(evidence, input);
if (recorded.ok) this.#emitSnapshotChanged();
return recorded;
}
updateUnknown(
input: UpdateUnknownInput,
): Result<ResidualUnknown, AnalysisError> {
const target = this.activeAnalysisBinding()?.target;
const updated = this.#records.updateUnknown(input, target);
if (updated.ok) this.#emitSnapshotChanged();
return updated;
}
listUnknowns(
filters: {
readonly status?: UnknownStatus;
readonly severity?: ResidualUnknown["severity"];
readonly domain?: string;
} = {},
): ResidualUnknown[] {
return this.#records.listUnknowns(filters);
}
verifyUnknownResolution(unknownId: string): Result<
{
readonly valid: boolean;
readonly truthVerified: boolean;
readonly unknown: ResidualUnknown;
},
UnknownRegistryError
> {
return this.#records.verifyUnknownResolution(unknownId);
}
}
@@ -0,0 +1,205 @@
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import type {
CapabilityDescriptor,
ProviderRequestActivitySnapshot,
ProviderRuntimeLineageSnapshot,
} from "../AnalysisProvider.js";
import type {
SessionProviderRoute,
SessionProviderRouter,
} from "./SessionProviderRouter.js";
interface BinarySessionStatusInput {
readonly target: BinaryTarget | undefined;
readonly route: SessionProviderRoute;
readonly router: SessionProviderRouter;
readonly runtimeUnavailability: ReadonlyMap<
string,
{ readonly reason: string }
>;
readonly runId: string | undefined;
readonly runtimeLineageSnapshots: readonly ProviderRuntimeLineageSnapshot[];
readonly requestActivitySnapshots: readonly ProviderRequestActivitySnapshot[];
}
/** Project internal provider routing state into the caller-visible session status. */
export const binarySessionStatus = ({
target,
route,
router,
runtimeUnavailability,
runId,
runtimeLineageSnapshots,
requestActivitySnapshots,
}: BinarySessionStatusInput): JsonValue => {
const configuredProvider = router.configuredIdentity();
const provider = providerSummary(configuredProvider);
const providers = router.providerIdentities(route).map(providerSummary);
const capabilities = [...(route.capabilities?.values() ?? [])]
.filter(({ operation }) => operation !== "inventory_artifact")
.sort((left, right) => left.operation.localeCompare(right.operation))
.map((descriptor) => {
const unavailable = runtimeUnavailability.get(descriptor.operation);
return unavailable === undefined
? descriptor
: {
...descriptor,
available: false as const,
reason: unavailable.reason,
};
})
.map(capabilityStatus);
const analysisProviderBinding =
route.binding === null
? null
: {
provider: providerSummary(route.binding.identity),
selection_source: route.binding.selectionSource,
analysis_profile: structuredClone(route.binding.profile),
};
const analysisProviderCandidates = router
.candidateStatuses(route)
.map((candidate) => ({
provider: providerSummary(candidate.provider),
availability: structuredClone(candidate.availability),
target_support: structuredClone(candidate.targetSupport),
selected: candidate.selected,
capabilities: candidate.capabilities
.slice()
.filter(({ operation }) => operation !== "inventory_artifact")
.sort((left, right) => left.operation.localeCompare(right.operation))
.map(capabilityStatus),
}));
const common = {
provider,
providers,
capabilities,
analysis_provider_binding: analysisProviderBinding,
analysis_provider_candidates: analysisProviderCandidates,
analysis_run:
runId === undefined
? null
: {
run_id: runId,
process_lineage: processLineageStatus(runtimeLineageSnapshots),
},
analysis_activity: requestActivityStatus(requestActivitySnapshots),
};
return target === undefined
? { open: false, ...common }
: {
open: true,
...common,
path: target.path,
sha256: target.sha256,
format: target.format,
kind: target.kind,
architecture: target.architecture ?? null,
};
};
const requestActivityStatus = (
snapshots: readonly ProviderRequestActivitySnapshot[],
) => {
const providers = snapshots
.slice()
.sort((left, right) => left.provider.id.localeCompare(right.provider.id))
.map(({ provider, active, queuedRequests }) => ({
provider: providerSummary(provider),
active:
active === null
? null
: {
request_id: active.requestId,
operation: active.operation,
elapsed_ms: active.elapsedMs,
caller_state: active.callerState,
},
queued_requests: queuedRequests,
}));
const active = snapshots.flatMap((snapshot) =>
snapshot.active === null ? [] : [snapshot.active],
);
return {
status:
snapshots.length === 0
? "not_observed"
: active.length > 0
? "busy"
: "idle",
providers,
};
};
const providerSummary = ({
id,
name,
version,
}: {
readonly id: string;
readonly name: string;
readonly version: string | null;
}) => ({ id, name, version });
const processLineageStatus = (
snapshots: readonly ProviderRuntimeLineageSnapshot[],
) =>
snapshots.length === 0
? { status: "not_observed" }
: {
status: "snapshots",
snapshots: snapshots
.slice()
.sort((left, right) =>
left.provider.id.localeCompare(right.provider.id),
)
.map(({ provider, observation }) => ({
provider: providerSummary(provider),
observation:
observation.status === "unavailable"
? {
status: observation.status,
observed_at: observation.observedAt,
launcher_pid: observation.launcherPid,
process_group_id: observation.processGroupId,
reason: observation.reason,
}
: {
status: observation.status,
observed_at: observation.observedAt,
launcher_pid: observation.lineage.launcherPid,
launcher_parent_pid: observation.lineage.launcherParentPid,
process_group_id: observation.lineage.processGroupId,
descendants: observation.lineage.descendants.map(
({ pid, parentPid, processGroupId }) => ({
pid,
parent_pid: parentPid,
process_group_id: processGroupId,
}),
),
},
})),
};
const capabilityStatus = (
descriptor: Omit<CapabilityDescriptor, "available" | "reason"> & {
readonly available: boolean;
readonly reason: string | null;
},
) => ({
operation: descriptor.operation,
available: descriptor.available,
reason: descriptor.reason,
availability_code: descriptor.availabilityCode ?? null,
effects: {
mutates_artifact: descriptor.effects.mutatesArtifact,
launches_process: descriptor.effects.launchesProcess,
may_show_ui: descriptor.effects.mayShowUi,
may_access_network: descriptor.effects.mayAccessNetwork,
may_write_filesystem: descriptor.effects.mayWriteFilesystem,
changes_permissions: descriptor.effects.changesPermissions,
requires_root: descriptor.effects.requiresRoot,
},
limitations: [...descriptor.limitations],
});
+161
View File
@@ -0,0 +1,161 @@
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
import { err, ok } from "../../domain/result.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type {
AnalysisClient,
AnalysisClientContext,
AnalysisProfileResolutionOptions,
AnalysisProvider,
CapabilityDescriptor,
ProviderIdentity,
} from "../AnalysisProvider.js";
import { closeAnalysisClient } from "./AnalysisClientCleanup.js";
/** Synthetic compatibility identity for a deterministic provider set. */
export const compositeProviderIdentity = (
identities: readonly ProviderIdentity[],
): ProviderIdentity => ({
id: `composite:${identities
.map(({ id }) => id)
.sort()
.join("+")}`,
name: "REA composite analysis provider",
version: null,
});
/** Deterministically route disjoint operations without eagerly starting children. */
export class CompositeProvider implements AnalysisProvider {
readonly #identity: ProviderIdentity;
readonly #capabilities: readonly CapabilityDescriptor[];
readonly #providerByOperation: ReadonlyMap<string, AnalysisProvider>;
readonly #profileProvider: AnalysisProvider | undefined;
constructor(readonly providers: readonly AnalysisProvider[]) {
if (providers.length === 0)
throw new RangeError("CompositeProvider requires at least one provider");
const profileProviders = providers.filter(
({ resolveAnalysisProfile }) => resolveAnalysisProfile !== undefined,
);
if (profileProviders.length > 1)
throw new TypeError(
"CompositeProvider supports at most one target-bound analysis profile",
);
this.#profileProvider = profileProviders[0];
this.#identity = Object.freeze(
compositeProviderIdentity(
providers.map((provider) => provider.identity()),
),
);
const routes = new Map<string, AnalysisProvider>();
const capabilities: CapabilityDescriptor[] = [];
for (const provider of providers) {
for (const descriptor of provider.capabilities()) {
if (routes.has(descriptor.operation))
throw new TypeError(
`Multiple providers declare operation ${descriptor.operation}`,
);
routes.set(descriptor.operation, provider);
capabilities.push(descriptor);
}
}
this.#providerByOperation = routes;
this.#capabilities = Object.freeze(
capabilities.sort(
(left, right) =>
left.operation.localeCompare(right.operation) ||
left.provider.id.localeCompare(right.provider.id),
),
);
}
identity(): ProviderIdentity {
return this.#identity;
}
capabilities(): readonly CapabilityDescriptor[] {
return this.#capabilities;
}
resolveAnalysisProfile(
target: BinaryTarget,
options?: AnalysisProfileResolutionOptions,
) {
const resolve = this.#profileProvider?.resolveAnalysisProfile;
return resolve === undefined
? Promise.resolve(ok({ profile: null, compatibility: {} }))
: resolve.call(this.#profileProvider, target, options);
}
createClient(
target: BinaryTarget,
profile?: AnalysisProfileCommitment,
context?: AnalysisClientContext,
): AnalysisClient {
const clients = new Map<AnalysisProvider, AnalysisClient>();
const clientFor = (provider: AnalysisProvider): AnalysisClient => {
const existing = clients.get(provider);
if (existing !== undefined) return existing;
const created = provider.createClient(
target,
profile?.provider.id === provider.identity().id ? profile : undefined,
context,
);
clients.set(provider, created);
return created;
};
const closeWithOutcome: NonNullable<
AnalysisClient["closeWithOutcome"]
> = async (options) => {
const outcomes = await Promise.all(
[...clients.entries()].map(([provider, client]) =>
closeAnalysisClient(client, provider.identity().id, options),
),
);
return outcomes.find((outcome) => !outcome.ok) ?? ok(null);
};
return {
execute: (operation, parameters, options) => {
if (operation === "health")
return Promise.resolve(
ok({
result: null,
rawResult: null,
provider: this.#identity,
limitations: [],
locations: [],
subject: null,
}),
);
const provider = this.#providerByOperation.get(operation);
return provider === undefined
? Promise.resolve(
err(
new AnalysisCapabilityUnavailableError(
this.#identity.id,
operation,
"operation is not declared by this provider set",
),
),
)
: clientFor(provider).execute(operation, parameters, options);
},
runtimeLineageSnapshots: () =>
[...clients.values()]
.flatMap((client) => client.runtimeLineageSnapshots?.() ?? [])
.sort((left, right) =>
left.provider.id.localeCompare(right.provider.id),
),
requestActivitySnapshots: () =>
[...clients.values()]
.flatMap((client) => client.requestActivitySnapshots?.() ?? [])
.sort((left, right) =>
left.provider.id.localeCompare(right.provider.id),
),
closeWithOutcome,
close: async () => {
await closeWithOutcome();
},
};
}
}
@@ -0,0 +1,108 @@
import { expect, it } from "vitest";
import { parseBinaryTarget } from "../BinaryTargetResolver.js";
import type {
AnalysisProvider,
ProviderIdentity,
} from "../AnalysisProvider.js";
import { LazyAnalysisProvider } from "./LazyAnalysisProvider.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import { err } from "../../domain/result.js";
const identity: ProviderIdentity = {
id: "test-provider",
name: "Test provider",
version: "1",
};
const providerWithClient = (
onCreate: () => void = () => undefined,
): AnalysisProvider => ({
identity: () => identity,
capabilities: () => [],
createClient: () => {
onCreate();
return {
execute: async (operation) => err(new AnalysisCancelledError(operation)),
close: async () => undefined,
};
},
});
const target = async () => {
const parsed = await parseBinaryTarget(process.execPath);
if (!parsed.ok) throw parsed.error;
return parsed.value;
};
it("returns a tagged provider error when lazy loading fails", async () => {
const provider = new LazyAnalysisProvider({
identity,
capabilities: [],
load: async () => {
throw new Error("provider module unavailable");
},
});
const result = await provider
.createClient(await target())
.execute("health", {});
expect(result).toMatchObject({
ok: false,
error: {
_tag: "ProviderAdapterError",
providerId: "test-provider",
operation: "health",
},
});
});
it("returns cancellation while an unabortable lazy load continues safely", async () => {
let finishLoading: ((provider: AnalysisProvider) => void) | undefined;
let markCreated: (() => void) | undefined;
const created = new Promise<void>((resolve) => {
markCreated = resolve;
});
const provider = new LazyAnalysisProvider({
identity,
capabilities: [],
load: () =>
new Promise<AnalysisProvider>((resolve) => {
finishLoading = resolve;
}),
});
const controller = new AbortController();
const client = provider.createClient(await target());
const pending = client.execute("health", {}, { signal: controller.signal });
await Promise.resolve();
controller.abort();
expect(await pending).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError", operation: "health" },
});
finishLoading?.(providerWithClient(() => markCreated?.()));
await created;
await client.close();
});
it("reports that a lazy client was closed before its first operation", async () => {
const provider = new LazyAnalysisProvider({
identity,
capabilities: [],
load: async () => providerWithClient(),
});
const client = provider.createClient(await target());
await client.close();
expect(await client.execute("health", {})).toMatchObject({
ok: false,
error: {
_tag: "ProviderAdapterError",
operation: "health",
diagnostics: { reason: "client_closed" },
},
});
});
@@ -0,0 +1,141 @@
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { err } from "../../domain/result.js";
import { ABORTED, waitForAbortable } from "./AbortablePromise.js";
import type {
AnalysisClient,
AnalysisClientContext,
AnalysisProvider,
CapabilityDescriptor,
ProviderIdentity,
ProviderRequestActivitySnapshot,
ProviderRuntimeLineageSnapshot,
} from "../AnalysisProvider.js";
type LoadAnalysisProvider = () => Promise<AnalysisProvider>;
const isAborted = (signal: AbortSignal | undefined): boolean =>
signal?.aborted === true;
/**
* Preserve synchronous provider discovery while loading implementation modules
* only when a target-bound client first executes.
*/
export class LazyAnalysisProvider implements AnalysisProvider {
readonly #providerIdentity: ProviderIdentity;
readonly #capabilityDescriptors: readonly CapabilityDescriptor[];
readonly #loadProvider: LoadAnalysisProvider;
#provider: Promise<AnalysisProvider> | undefined;
constructor(options: {
readonly identity: ProviderIdentity;
readonly capabilities: readonly CapabilityDescriptor[];
readonly load: LoadAnalysisProvider;
}) {
this.#providerIdentity = options.identity;
this.#capabilityDescriptors = options.capabilities;
this.#loadProvider = options.load;
}
/** Return source-declared provider identity without loading its implementation. */
identity(): ProviderIdentity {
return this.#providerIdentity;
}
/** Return source-declared capability metadata without loading its implementation. */
capabilities(): readonly CapabilityDescriptor[] {
return this.#capabilityDescriptors;
}
/** Create a client whose implementation is loaded on first execution. */
createClient(
target: BinaryTarget,
profile?: AnalysisProfileCommitment,
context?: AnalysisClientContext,
): AnalysisClient {
return new LazyAnalysisClient(async () => {
const provider = await this.#load();
return provider.createClient(target, profile, context);
}, this.#providerIdentity.id);
}
async #load(): Promise<AnalysisProvider> {
this.#provider ??= this.#loadProvider().catch((cause: unknown) => {
this.#provider = undefined;
throw cause;
});
return this.#provider;
}
}
class LazyAnalysisClient implements AnalysisClient {
readonly #loadClient: () => Promise<AnalysisClient>;
readonly #providerId: string;
#client: AnalysisClient | undefined;
#loading: Promise<AnalysisClient> | undefined;
#closed = false;
constructor(loadClient: () => Promise<AnalysisClient>, providerId: string) {
this.#loadClient = loadClient;
this.#providerId = providerId;
}
execute: AnalysisClient["execute"] = async (
operation,
parameters,
options,
) => {
try {
if (isAborted(options?.signal))
return err(new AnalysisCancelledError(operation));
const loaded = await waitForAbortable(
this.#load(operation),
options?.signal,
);
if (loaded === ABORTED) return err(new AnalysisCancelledError(operation));
return await loaded.execute(operation, parameters, options);
} catch (cause: unknown) {
if (cause instanceof AnalysisError) return err(cause);
return err(
new ProviderAdapterError(this.#providerId, operation, {
cause,
}),
);
}
};
runtimeLineageSnapshots(): readonly ProviderRuntimeLineageSnapshot[] {
return this.#client?.runtimeLineageSnapshots?.() ?? [];
}
requestActivitySnapshots(): readonly ProviderRequestActivitySnapshot[] {
return this.#client?.requestActivitySnapshots?.() ?? [];
}
async close(): Promise<void> {
this.#closed = true;
if (this.#loading === undefined) return;
const client = await this.#loading;
await client.close();
}
async #load(operation: string): Promise<AnalysisClient> {
if (this.#closed)
throw new ProviderAdapterError(this.#providerId, operation, {
diagnostics: { reason: "client_closed" },
});
this.#loading ??= this.#loadClient()
.then((client) => {
this.#client = client;
return client;
})
.catch((cause: unknown) => {
this.#loading = undefined;
throw cause;
});
return this.#loading;
}
}
@@ -0,0 +1,393 @@
import type { AnalysisProviderSelector } from "../../contracts/providerSelection.js";
import {
analysisProfileSchema,
type AnalysisProfileCommitment,
} from "../../domain/analysisProfile.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
} from "../../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderSelectionError } from "../../domain/providerSelectionError.js";
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
import { jsonObjectSchema, type JsonValue } from "../../domain/jsonValue.js";
import { err, ok, type Result } from "../../domain/result.js";
import type {
AnalysisClient,
AnalysisClientContext,
AnalysisClientFactory,
AnalysisProfileResolution,
AnalysisProfileResolutionOptions,
AnalysisProvider,
CapabilityDescriptor,
ProviderIdentity,
} from "../AnalysisProvider.js";
import {
AnalysisProviderRegistry,
type AnalysisProviderBinding,
type AnalysisProviderCandidateStatus,
type AnalysisProviderSelection,
} from "./AnalysisProviderRegistry.js";
import { ABORTED, waitForAbortable } from "./AbortablePromise.js";
import {
CompositeProvider,
compositeProviderIdentity,
} from "./CompositeProvider.js";
/** Immutable operation routes and binding metadata for one target transition. */
export interface SessionProviderRoute {
readonly identity: ProviderIdentity;
readonly capabilities: ReadonlyMap<string, CapabilityDescriptor> | undefined;
readonly profile: AnalysisProfileCommitment | null;
readonly compatibility: Readonly<Record<string, JsonValue>>;
readonly binding: AnalysisProviderBinding | null;
readonly selection: AnalysisProviderSelection | undefined;
createClient(
target: BinaryTarget,
context: AnalysisClientContext,
): AnalysisClient;
}
interface SingleProviderRuntime {
readonly kind: "single";
readonly provider: AnalysisProvider | AnalysisClientFactory;
readonly identity: ProviderIdentity;
readonly capabilities: ReadonlyMap<string, CapabilityDescriptor> | undefined;
readonly resolveProfile: (
target: BinaryTarget,
options?: AnalysisProfileResolutionOptions,
) => Promise<Result<AnalysisProfileResolution, AnalysisError>>;
}
interface SelectableProviderRuntime {
readonly kind: "selectable";
readonly registry: AnalysisProviderRegistry;
readonly auxiliaryProviders: readonly AnalysisProvider[];
readonly identity: ProviderIdentity;
readonly initialRoute: SessionProviderRoute;
}
/** Resolve either the compatibility single-provider seam or a selectable set. */
export class SessionProviderRouter {
private constructor(
private readonly runtime: SingleProviderRuntime | SelectableProviderRuntime,
) {}
/** Create routing for an embedding backed by one fixed provider or factory. */
static single(
provider: AnalysisProvider | AnalysisClientFactory,
options: {
readonly resolveAnalysisProfile?: (
target: BinaryTarget,
resolutionOptions?: AnalysisProfileResolutionOptions,
) => Promise<Result<AnalysisProfileResolution, AnalysisError>>;
} = {},
): SessionProviderRouter {
const identity =
typeof provider === "function"
? { id: "unidentified", name: "Unidentified provider", version: null }
: provider.identity();
const capabilities =
typeof provider === "function"
? undefined
: capabilityMap(provider.capabilities());
const resolveProfile =
options.resolveAnalysisProfile ??
(typeof provider === "function" ||
provider.resolveAnalysisProfile === undefined
? () => Promise.resolve(ok({ profile: null, compatibility: {} }))
: (
target: BinaryTarget,
resolutionOptions?: AnalysisProfileResolutionOptions,
) =>
provider.resolveAnalysisProfile?.(target, resolutionOptions) ??
Promise.resolve(ok({ profile: null, compatibility: {} })));
return new SessionProviderRouter({
kind: "single",
provider,
identity,
capabilities,
resolveProfile,
});
}
/** 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({
kind: "selectable",
registry,
auxiliaryProviders: [...auxiliaryProviders],
identity:
identities.length === 0
? emptyProviderIdentity()
: compositeProviderIdentity(identities),
initialRoute,
});
}
/** Compatibility identity covering every configured provider. */
configuredIdentity(): ProviderIdentity {
return structuredClone(this.runtime.identity);
}
/** Provider identities exposed through the flat 1.x compatibility field. */
providerIdentities(route: SessionProviderRoute): readonly ProviderIdentity[] {
if (this.runtime.kind === "single") {
if (route.capabilities === undefined) return [route.identity];
return uniqueProviderIdentities(
[...route.capabilities.values()].map(({ provider }) => provider),
);
}
const selected = route.binding?.identity;
return uniqueProviderIdentities([
...this.runtime.auxiliaryProviders.map((provider) => provider.identity()),
...this.runtime.registry
.identities()
.map((identity) =>
identity.id === selected?.id ? selected : identity,
),
]);
}
/** Target-free routes and discovery metadata. */
initialRoute(): SessionProviderRoute {
if (this.runtime.kind === "selectable") return this.runtime.initialRoute;
const runtime = this.runtime;
return {
identity: structuredClone(runtime.identity),
capabilities: runtime.capabilities,
profile: null,
compatibility: {},
binding: null,
selection: undefined,
createClient: (target, context) =>
createSingleProviderClient(runtime, target, undefined, context),
};
}
/** 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 };
if (this.runtime.kind === "selectable") {
const selected = await this.runtime.registry.select(
target,
providerId,
resolutionOptions,
);
return selected.ok
? ok(selectableRoute(this.runtime.auxiliaryProviders, selected.value))
: selected;
}
const runtime = this.runtime;
if (
providerId !== undefined &&
providerId !== "auto" &&
providerId !== runtime.identity.id
)
return err(
new ProviderSelectionError({
reason: "unknown_provider",
requestedProviderId: providerId,
candidateIds: [runtime.identity.id],
}),
);
const resolved = await waitForAbortable(
runtime.resolveProfile(target, resolutionOptions),
signal,
);
if (resolved === ABORTED)
return err(new AnalysisCancelledError("open_binary"));
if (!resolved.ok) return resolved;
const normalized = normalizeProfileResolution(
resolved.value,
runtime.identity.id,
);
if (!normalized.ok) return normalized;
return ok({
identity: structuredClone(runtime.identity),
capabilities: runtime.capabilities,
profile: normalized.value.profile,
compatibility: normalized.value.compatibility,
binding: null,
selection: undefined,
createClient: (openedTarget, context) =>
createSingleProviderClient(
runtime,
openedTarget,
normalized.value.profile ?? undefined,
context,
),
});
}
/** Candidate status authoritative for the active or target-free route. */
candidateStatuses(
route: SessionProviderRoute,
): readonly AnalysisProviderCandidateStatus[] {
if (this.runtime.kind === "single") return [];
return route.selection?.candidates ?? this.runtime.registry.candidates();
}
/** Produce exact candidate rejections for an intentionally unbound route. */
unboundOperationError(
operation: string,
route: SessionProviderRoute,
): ProviderSelectionError | undefined {
if (
this.runtime.kind !== "selectable" ||
route.binding !== null ||
route.selection === undefined
)
return undefined;
return this.runtime.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 =
providers.length === 0 ? undefined : new CompositeProvider(providers);
const identity = provider?.identity() ?? emptyProviderIdentity();
const capabilities = capabilityMap(provider?.capabilities() ?? []);
return {
identity,
capabilities,
profile: binding?.profile ?? null,
compatibility: structuredClone(binding?.compatibility ?? {}),
binding,
selection,
createClient: (target, context) =>
provider?.createClient(target, binding?.profile, context) ??
emptyClient(identity),
};
};
const createSingleProviderClient = (
runtime: SingleProviderRuntime,
target: BinaryTarget,
profile: AnalysisProfileCommitment | undefined,
context: AnalysisClientContext,
): AnalysisClient =>
typeof runtime.provider === "function"
? runtime.provider(target, profile, context)
: runtime.provider.createClient(target, profile, context);
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(),
});
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 uniqueProviderIdentities = (
identities: readonly ProviderIdentity[],
): readonly ProviderIdentity[] =>
[...new Map(identities.map((identity) => [identity.id, identity])).values()]
.sort((left, right) => left.id.localeCompare(right.id))
.map((identity) => structuredClone(identity));
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);
}
};
const normalizeProfileResolution = (
resolution: AnalysisProfileResolution,
providerId: string,
): Result<AnalysisProfileResolution, ProviderAdapterError> => {
try {
return ok({
profile:
resolution.profile === null
? null
: analysisProfileSchema.parse(resolution.profile),
compatibility: jsonObjectSchema.parse(resolution.compatibility),
});
} catch (cause: unknown) {
return err(
new ProviderAdapterError(providerId, "resolve_analysis_profile", {
cause,
}),
);
}
};