fix(artifacts): publish addressable inspection sources through both adapters
This commit is contained in:
1 parent
b885f44bef
commit
bd8e1fb2c1
6 files changed
+136
-27
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { readArtifactInspectionSources } from "../domain/artifactInspection.js";
|
||||
import { dispatchQueuedInterrupts } from "../process/queuedInterrupts.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { requireAuthenticationHeadroom } from "./JsonAuthenticationHeadroom.js";
|
||||
@@ -84,7 +85,16 @@ export const deliverAnalysisEvidence = async (
|
||||
});
|
||||
if (!headroom.ok)
|
||||
return err(headroom.error.retainPartialObservation(evidence));
|
||||
const bundle = createImmutableEvidenceBundle([evidence]);
|
||||
const sourceEvidence =
|
||||
evidence.operation === "inspect_artifact"
|
||||
? readArtifactInspectionSources(evidence.normalized_result)
|
||||
: ok([]);
|
||||
if (!sourceEvidence.ok)
|
||||
return err(sourceEvidence.error.retainPartialObservation(evidence));
|
||||
const bundle = createImmutableEvidenceBundle([
|
||||
...sourceEvidence.value,
|
||||
evidence,
|
||||
]);
|
||||
const delivered =
|
||||
detail === "summary"
|
||||
? inspectAnalysisViewValidated(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readArtifactInspectionSources } from "../domain/artifactInspection.js";
|
||||
import type { AnalysisViewRequest } from "../domain/analysisView/analysisView.js";
|
||||
import { z } from "incur";
|
||||
import { parseEvidence, type Evidence } from "../domain/evidence.js";
|
||||
@@ -97,6 +98,9 @@ export const deliverCliAnalysisEvidence = async (
|
||||
detail,
|
||||
evidenceOutput,
|
||||
...(input.view === undefined ? {} : { view: input.view }),
|
||||
...(input.sourceEvidence === undefined
|
||||
? {}
|
||||
: { sourceEvidence: input.sourceEvidence }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -127,13 +131,22 @@ export const publishCliAnalysisEvidence = async (
|
||||
return projectAnalysisError(
|
||||
delivered.error.retainPartialObservation(complete),
|
||||
);
|
||||
const inspectionSources =
|
||||
complete.operation === "inspect_artifact"
|
||||
? readArtifactInspectionSources(complete.normalized_result)
|
||||
: ok([]);
|
||||
if (!inspectionSources.ok)
|
||||
return projectAnalysisError(
|
||||
inspectionSources.error.retainPartialObservation(complete),
|
||||
);
|
||||
const published = await publishEvidenceDelivery({
|
||||
bundle: createEvidenceBundle([
|
||||
...new Map(
|
||||
[...(input.sourceEvidence ?? []), complete].map((record) => [
|
||||
record.evidence_id,
|
||||
record,
|
||||
]),
|
||||
[
|
||||
...(input.sourceEvidence ?? []),
|
||||
...inspectionSources.value,
|
||||
complete,
|
||||
].map((record) => [record.evidence_id, record]),
|
||||
).values(),
|
||||
]),
|
||||
complete,
|
||||
|
||||
@@ -123,10 +123,13 @@ export const artifactInventorySummarySchema = z.strictObject({
|
||||
limitation_count: z.number().int().nonnegative(),
|
||||
});
|
||||
|
||||
const inputError = (message: string): AnalysisInputError =>
|
||||
const inputError = (
|
||||
field: "source" | "view",
|
||||
message: string,
|
||||
): AnalysisInputError =>
|
||||
new AnalysisInputError("inspect_analysis_view", undefined, [
|
||||
{
|
||||
path: ["source"],
|
||||
path: [field],
|
||||
reason: "invalid_value",
|
||||
message,
|
||||
},
|
||||
@@ -221,7 +224,7 @@ const sourceFailure = (
|
||||
if (cause instanceof z.ZodError)
|
||||
return err(analysisViewResultInputError(input, cause));
|
||||
if (!(cause instanceof ArtifactInventoryValidationError)) throw cause;
|
||||
return err(inputError(cause.message));
|
||||
return err(inputError("source", cause.message));
|
||||
};
|
||||
|
||||
/** Project exact source records; exhausted pages cover available records, not runtime behavior. */
|
||||
@@ -233,12 +236,16 @@ export const projectArtifactInventoryView = (
|
||||
if (operation !== "inventory_artifact" && operation !== "inspect_artifact")
|
||||
return err(
|
||||
inputError(
|
||||
"source",
|
||||
"Inventory views require inventory_artifact or inspect_artifact Evidence.",
|
||||
),
|
||||
);
|
||||
if (view.kind !== "summary" && view.kind !== "inventory")
|
||||
return err(
|
||||
inputError("Artifact Evidence supports summary and inventory views."),
|
||||
inputError(
|
||||
"view",
|
||||
"Artifact Evidence supports summary and inventory views.",
|
||||
),
|
||||
);
|
||||
const parsed = parseSource(parent);
|
||||
if (!parsed.ok) return parsed;
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { readEvidencePayload } from "./evidenceValidation.js";
|
||||
import type { EvidenceIntegrityError } from "./evidenceErrors.js";
|
||||
import { ok, type Result } from "./result.js";
|
||||
import {
|
||||
canBorrowParsedJsonSnapshot,
|
||||
immutableJsonSnapshotMember,
|
||||
} from "./immutableJson.js";
|
||||
import {
|
||||
artifactIntegrityPairOf,
|
||||
artifactSha256PairSchema,
|
||||
@@ -437,3 +444,22 @@ const inspectionCoverage = (
|
||||
if (inventory.coverage.status !== "complete") return "partial";
|
||||
return "complete-within-substeps";
|
||||
};
|
||||
|
||||
/** Own the atomic source closure published or retained with an inspection. */
|
||||
export const readArtifactInspectionSources = (
|
||||
result: unknown,
|
||||
): Result<readonly Evidence[], EvidenceIntegrityError> => {
|
||||
const parsed = readEvidencePayload(artifactInspectionResultSchema, result);
|
||||
if (!parsed.ok) return parsed;
|
||||
const inspection = canBorrowParsedJsonSnapshot(result, parsed.value)
|
||||
? result
|
||||
: parsed.value;
|
||||
const substeps = immutableJsonSnapshotMember(inspection, "substeps");
|
||||
return ok(
|
||||
inspection.substeps.map((step, index) => {
|
||||
const ownedStep = immutableJsonSnapshotMember(substeps, String(index));
|
||||
immutableJsonSnapshotMember(ownedStep, "evidence");
|
||||
return step.evidence;
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -1,13 +1,9 @@
|
||||
import {
|
||||
canBorrowParsedJsonSnapshot,
|
||||
immutableJsonSnapshotMember,
|
||||
} from "../domain/immutableJson.js";
|
||||
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
|
||||
|
||||
import { ARTIFACT_TOOL_CONTRACTS } from "../contracts/artifactToolContracts.js";
|
||||
import { createArtifactExtractionDestination } from "../application/artifacts/ArtifactExtractionDestination.js";
|
||||
import { registerEvidenceTools } from "./registerEvidenceTools.js";
|
||||
import { artifactInspectionResultSchema } from "../domain/artifactInspection.js";
|
||||
import { readArtifactInspectionSources } from "../domain/artifactInspection.js";
|
||||
import {
|
||||
retainedSummaryDelivery,
|
||||
withoutDeliveryDetail,
|
||||
@@ -40,16 +36,9 @@ export const registerArtifactTools = (
|
||||
parametersForEvidence: decodeWithoutDelivery,
|
||||
sourceEvidence: (operation, result) => {
|
||||
if (operation !== "inspect_artifact") return [];
|
||||
const parsed = artifactInspectionResultSchema.parse(result);
|
||||
const inspection = canBorrowParsedJsonSnapshot(result, parsed)
|
||||
? result
|
||||
: parsed;
|
||||
const substeps = immutableJsonSnapshotMember(inspection, "substeps");
|
||||
return inspection.substeps.map((step, index) => {
|
||||
const ownedStep = immutableJsonSnapshotMember(substeps, String(index));
|
||||
immutableJsonSnapshotMember(ownedStep, "evidence");
|
||||
return step.evidence;
|
||||
});
|
||||
const parsed = readArtifactInspectionSources(result);
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
return parsed.value;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -38,7 +38,10 @@ import { artifactInspectionResultSchema } from "../../../src/domain/artifactInsp
|
||||
import type { JsonValue } from "../../../src/domain/jsonValue.js";
|
||||
import { z } from "zod";
|
||||
import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
|
||||
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
|
||||
import {
|
||||
createEvidenceBundle,
|
||||
parseEvidenceBundle,
|
||||
} from "../../../src/domain/evidenceBundle.js";
|
||||
import { inspectEvidenceBundle } from "../../../src/application/investigation/InspectEvidenceBundle.js";
|
||||
|
||||
cliTest(
|
||||
@@ -264,11 +267,72 @@ cliTest(
|
||||
const writer = new ZipWriter(new Uint8ArrayWriter());
|
||||
await writer.add("main.js", new TextReader("console.log('portable');\n"));
|
||||
await writeFile(archive, await writer.close());
|
||||
const bundlePath = join(root, "inspection-bundle.json");
|
||||
const inspected = await cli.run({
|
||||
arguments: ["inspect-artifact", archive, "--json"],
|
||||
arguments: [
|
||||
"inspect-artifact",
|
||||
archive,
|
||||
"--detail",
|
||||
"summary",
|
||||
"--evidence_output",
|
||||
bundlePath,
|
||||
"--json",
|
||||
],
|
||||
});
|
||||
expect(inspected.exitCode).toBe(0);
|
||||
const parent = parseEvidence(inspected.json);
|
||||
const savedBundle = parseEvidenceBundle(
|
||||
JSON.parse(await readFile(bundlePath, "utf8")),
|
||||
);
|
||||
const parent = savedBundle.records.find(
|
||||
(record) => record.operation === "inspect_artifact",
|
||||
)!;
|
||||
const atomicId = artifactInspectionResultSchema.parse(
|
||||
parent.normalized_result,
|
||||
).substeps[0]!.evidence_id;
|
||||
expect(savedBundle.records.map((record) => record.evidence_id)).toContain(
|
||||
atomicId,
|
||||
);
|
||||
const call = await cli.sharedMcpCall?.();
|
||||
if (call === undefined) throw new Error("Expected shared MCP session");
|
||||
expect(
|
||||
(await call("import_evidence_bundle", { path: bundlePath })).isError,
|
||||
).not.toBe(true);
|
||||
const atomicView = await call("inspect_analysis_view", {
|
||||
source: { kind: "retained-evidence", evidence_id: atomicId },
|
||||
view: {
|
||||
kind: "inventory",
|
||||
collection: "occurrences",
|
||||
logical_paths: ["main.js"],
|
||||
},
|
||||
});
|
||||
expect(atomicView.isError).not.toBe(true);
|
||||
expect(
|
||||
parseEvidence(atomicView.structuredContent).normalized_result,
|
||||
).toMatchObject({
|
||||
parent_evidence_id: atomicId,
|
||||
items: [expect.objectContaining({ logical_path: "main.js" })],
|
||||
});
|
||||
const unsupportedRequest = join(root, "unsupported-view.json");
|
||||
const unsupportedInput = {
|
||||
source: { kind: "inline", evidence: parent },
|
||||
view: { kind: "page", collection: "sections" },
|
||||
};
|
||||
await writeFile(unsupportedRequest, JSON.stringify(unsupportedInput));
|
||||
const unsupported = await cli.run({
|
||||
arguments: ["inspect-analysis-view", unsupportedRequest, "--json"],
|
||||
});
|
||||
expect(unsupported.exitCode).toBe(1);
|
||||
expect(unsupported.json).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
issues: [
|
||||
{
|
||||
path: ["view"],
|
||||
message: "Artifact Evidence supports summary and inventory views.",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await unlink(archive);
|
||||
const request = join(root, "view.json");
|
||||
await writeFile(
|
||||
|
||||
Reference in new issue
Block a user