fix(ghidra): admit installation metadata through owned bounded reads

Reject special files without blocking, cap the properties payload at 1 MiB before allocation, and verify descriptor extent and the selected name. Preserve Latin-1 data, selected symlinks, physical filesystem facts, completed observations and exact failed-close ownership.

Reuse the stable bounded reader for profile artifacts and consolidate Ghidra file failure projection. Distinguish missing-at-open from disappearance or read failure, and attribute typed prerequisite failures at the consuming operation.

Validation: 384 focused tests across 14 files, 42 seed/extension controls, 32 final metadata/profile/CLI controls, check:fast and docs:check. Controlled macOS file/process fixtures; no real Ghidra/JVM or Windows/Linux lifecycle verification.

Bounded FIFO reproduction: baseline direct and public doctor required external cleanup around 2.2s; candidate returned independently around 0.4/0.57s, all fixture groups verified empty. Warm normal doctor baseline/candidate: elapsed 1.211/1.230s, CPU 1.239/1.247s, sampled family RSS 524/524MB, reply tokens 3116/3116. No performance improvement claimed; true cold and whole-agent costs unmeasured.

Refs #4672
This commit is contained in:
morluto
2026-10-17 12:02:26 +08:00
parent c72786f289
commit 5112fe07ae
14 changed files with 825 additions and 91 deletions
@@ -4,7 +4,12 @@ import {
createAnalysisExecution,
type AnalysisClient,
} from "../AnalysisProvider.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import {
AnalysisCancelledError,
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisResourceConstraintError,
} from "../../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
@@ -12,6 +17,51 @@ import { resolvedAnalysisError } from "../../domain/analysisErrorBase.js";
import { err, ok } from "../../domain/result.js";
const identity = { id: "controlled", name: "Controlled client", version: "1" };
it.each([
new AnalysisAccessDeniedError(
"ghidra:installation",
"/selected/properties",
"EACCES",
),
new AnalysisArtifactChangedError(
"ghidra:installation",
"/selected/properties",
"selected name changed",
),
new AnalysisResourceConstraintError(
"ghidra:installation",
"file-size",
"metadata budget",
{ maximum_bytes: 1024 },
{ retryable: false },
),
])(
"attributes prerequisite $_tag to the requested operation without changing its facts",
async (physical) => {
const client = new DeferredAnalysisClient(async () => {
throw physical;
}, identity.id);
try {
const result = await client.execute("analyze_function", {});
if (result.ok) throw new Error("Expected prerequisite failure");
expect(result.error).toMatchObject({
_tag: physical._tag,
operation: "analyze_function",
});
expect(result.error.cause).toBe(physical);
if (physical instanceof AnalysisResourceConstraintError)
expect(result.error).toMatchObject({
resource: "file-size",
reportedLimits: { maximum_bytes: 1024 },
retryable: false,
});
else expect(result.error).toMatchObject({ path: "/selected/properties" });
} finally {
await client.close();
}
},
);
const gate = <T = void>() => {
let resolve!: (value: T) => void;
const promise = new Promise<T>((finish) => {