fix(session): report a missing Linux ps as provider unavailability in status (#7180)

Keep binary_session available when Linux owned-child probes lack procps ps. Preserve typed ownership-prerequisite failures in direct execution and report per-provider readiness in status. Independently validated on latest main through real stdio MCP plus focused regression, static and documentation checks. Closes #7164.
This commit is contained in:
김준호 authored and GitHub committed 2026-10-26 20:35:09 +08:00
1 parent e3066c1705
commit 0211a07ec5
10 files changed
+298 -21

No files matched your search

@@ -16,10 +16,12 @@ import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import {
AnalysisCapabilityUnavailableError,
AnalysisUnsupportedTargetError,
AnalysisCancelledError,
} from "../../domain/analysisErrorCore.js";
import { ProviderSelectionError } from "../../domain/providerSelectionError.js";
import { LinuxOwnedProcessPrerequisiteError } from "../../process/ProcessOwnershipObservation.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import { err, ok } from "../../domain/result.js";
@@ -109,6 +111,55 @@ it("uses one owner readiness snapshot for candidate availability and capabilitie
});
});
it("reports a missing Linux ps as one candidate's unavailability and keeps the others", async () => {
const base = candidate("alpha").provider;
const gamma = candidate("gamma").provider;
const refused: AnalysisProviderCandidate = {
...base,
// Mirrors a provider adapter re-attributing the refusal to its own operation.
inspectReadinessSnapshot: () => {
throw new AnalysisCapabilityUnavailableError(
"process-ownership",
"ghidra:inspection",
"ps is unavailable",
{
cause: new LinuxOwnedProcessPrerequisiteError(
new Error("spawn ps ENOENT"),
),
},
);
},
};
const statuses = await new AnalysisProviderRegistry([
refused,
gamma,
]).candidates();
expect(statuses).toMatchObject([
{
provider: { id: "alpha" },
availability: {
status: "unavailable",
code: "runtime_unavailable",
reason: expect.stringContaining("spawn ps ENOENT") as unknown,
diagnostics: { stage: "ownership_preparation" },
},
capabilities: [{ operation: "address_name" }],
},
{ provider: { id: "gamma" }, availability: { status: "available" } },
]);
const selected = await new AnalysisProviderRegistry([refused]).select(
DATABASE_TARGET,
"alpha",
);
if (selected.ok) throw new Error("expected the prerequisite refusal");
expect(selected.error).toMatchObject({
_tag: "AnalysisCapabilityUnavailableError",
providerId: "process-ownership",
});
});
it("selects the sole available provider and honors explicit selection precedence", async () => {
const alpha = candidate("alpha");
const beta = candidate("beta", { available: false });
@@ -35,6 +35,7 @@ import type {
AnalysisProviderCandidate,
AnalysisProviderBoundClientFactory,
AnalysisProviderInspectionOptions,
AnalysisProviderReadinessSnapshot,
CapabilityDescriptor,
ProviderAvailability,
ProviderIdentity,
@@ -46,6 +47,7 @@ import {
type AnalysisProviderCandidateEvaluation,
} from "./AnalysisProviderEvaluation.js";
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
import { ownedProcessPrerequisiteAvailability } from "../ownedProcessAvailability.js";
/** Deterministic discovery truth for one configured deep provider. */
export interface AnalysisProviderCandidateStatus {
@@ -733,21 +735,32 @@ const candidateStatus = async (
const { signal, scope, resourceScope, operation } = context;
if (signal.aborted)
throw new AnalysisCancelledError(operation, { cause: signal.reason });
const readiness =
provider.inspectReadinessSnapshot === undefined
? {
availability: await provider.inspectAvailability({
let readiness: AnalysisProviderReadinessSnapshot;
try {
readiness =
provider.inspectReadinessSnapshot === undefined
? {
availability: await provider.inspectAvailability({
signal,
scope,
resourceScope,
}),
capabilities: provider.capabilities(),
}
: await provider.inspectReadinessSnapshot({
signal,
scope,
resourceScope,
}),
capabilities: provider.capabilities(),
}
: await provider.inspectReadinessSnapshot({
signal,
scope,
resourceScope,
});
});
} catch (cause: unknown) {
// Status reports the gap per provider; execution keeps the prerequisite's owner.
const availability =
operation === "binary_session"
? ownedProcessPrerequisiteAvailability(cause)
: undefined;
if (availability === undefined) throw cause;
readiness = { availability, capabilities: provider.capabilities() };
}
return await projectCandidateStatus(provider, target, readiness, {
signal,
scope,