mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(analysis): retain one-shot target selection provenance
Retain the exact selected CLI path in one-shot provider Evidence while preserving canonical subject identity and provider inputs. The shared request-provenance boundary replaces the artifact-only special case across native, managed, and bundle-resource routes. Remove the redundant parameter fallback and make the private managed helper receive explicit Evidence parameters. Validation: real macOS codesign fixtures through the compiled CLI retained app aliases for signature, plist, and absent asset inspections, with direct-target controls. Archive selection, managed direct analysis, and snapshot replay regressions passed; Windows-specific replay cases were skipped on macOS. Build, static, format, and docs checks passed. Snapshot-backed direct request provenance still follows its existing authenticated query contract. Refs: #5479
This commit is contained in:
@@ -53,6 +53,11 @@ satisfy. A plan without execution does not establish runtime behavior.
|
||||
- Use strict inputs with explicit target identity and required fields. Preserve
|
||||
meaningful target, action, capture, and output choices; remove ignored options
|
||||
and repeated permission declarations.
|
||||
One-shot CLI provider inspections retain the caller's exact target spelling
|
||||
in Evidence `parameters.input_path`; the subject keeps the canonical inspected
|
||||
file identity. Keep request provenance separate from provider arguments.
|
||||
Snapshot entries bind Evidence parameters to provider queries, so changes to
|
||||
snapshot-backed request provenance must preserve that cache contract.
|
||||
- Return the requested facts inline with artifact/provider identity, source
|
||||
locations, Evidence references, coverage, and relevant confidence and
|
||||
limitations. Use supported selection, summaries or views when they answer
|
||||
|
||||
@@ -195,12 +195,16 @@ export const runProviderAnalysis = async (
|
||||
]),
|
||||
);
|
||||
return withProcessCancellation(signal, async (operationSignal) => {
|
||||
// The selected CLI path is a request fact. Keep it out of provider inputs
|
||||
// and target identity, which may use the resolved executable instead.
|
||||
const evidenceParameters = { ...arguments_, input_path: path };
|
||||
if (isManagedToolName(tool)) {
|
||||
const original = await runManagedProviderAnalysis(
|
||||
dependencies,
|
||||
path,
|
||||
tool,
|
||||
arguments_,
|
||||
evidenceParameters,
|
||||
operationSignal,
|
||||
);
|
||||
await dispatchQueuedInterrupts();
|
||||
@@ -227,6 +231,7 @@ export const runProviderAnalysis = async (
|
||||
logger,
|
||||
snapshotPath: undefined,
|
||||
signal: operationSignal,
|
||||
evidenceParameters,
|
||||
...(ARTIFACT_ANALYSIS_OPERATION_SET.has(tool)
|
||||
? { createSession: dependencies.createArtifactBinarySession }
|
||||
: NATIVE_INSPECTION_OPERATIONS.has(tool)
|
||||
@@ -265,6 +270,7 @@ const runManagedProviderAnalysis = async (
|
||||
path: string,
|
||||
tool: ManagedToolName,
|
||||
arguments_: Readonly<Record<string, JsonValue>>,
|
||||
evidenceParameters: Readonly<Record<string, JsonValue>>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Result<Evidence, AnalysisError>> => {
|
||||
const execution = await runManagedProviderExecution(
|
||||
@@ -277,9 +283,13 @@ const runManagedProviderAnalysis = async (
|
||||
if (!execution.ok) return execution;
|
||||
const value = execution.value;
|
||||
return ok(
|
||||
createAnalysisExecutionEvidence(tool, arguments_, value, "observed", {
|
||||
immutable: true,
|
||||
}),
|
||||
createAnalysisExecutionEvidence(
|
||||
tool,
|
||||
evidenceParameters,
|
||||
value,
|
||||
"observed",
|
||||
{ immutable: true },
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -296,6 +306,7 @@ const runAnalysis = async (
|
||||
readonly logger: Logger;
|
||||
readonly snapshotPath: string | undefined;
|
||||
readonly signal: AbortSignal;
|
||||
readonly evidenceParameters?: Readonly<Record<string, JsonValue>>;
|
||||
readonly createSession?: () => BinarySession | Promise<BinarySession>;
|
||||
readonly providerId?: AnalysisProviderSelector;
|
||||
readonly formatHint?: ExecutableFormatHint;
|
||||
@@ -304,6 +315,7 @@ const runAnalysis = async (
|
||||
},
|
||||
): Promise<JsonValue> => {
|
||||
const { logger, signal, snapshotPath } = options;
|
||||
const evidenceParameters = options.evidenceParameters ?? arguments_;
|
||||
let session: BinarySession;
|
||||
if (options.createSession !== undefined) {
|
||||
session = await options.createSession();
|
||||
@@ -332,7 +344,7 @@ const runAnalysis = async (
|
||||
return ok(
|
||||
createAnalysisExecutionEvidence(
|
||||
tool,
|
||||
arguments_,
|
||||
evidenceParameters,
|
||||
execution.value,
|
||||
"observed",
|
||||
),
|
||||
@@ -427,9 +439,9 @@ const runAnalysis = async (
|
||||
const executed = await executeAnalysisTool({
|
||||
session,
|
||||
openedTarget: opened.value,
|
||||
selectedPath: path,
|
||||
tool,
|
||||
arguments: arguments_,
|
||||
evidenceParameters,
|
||||
signal,
|
||||
evidenceProfile,
|
||||
});
|
||||
@@ -523,13 +535,13 @@ const runAnalysis = async (
|
||||
const executeAnalysisTool = async (input: {
|
||||
readonly session: BinarySession;
|
||||
readonly openedTarget: BinaryTarget;
|
||||
readonly selectedPath: string;
|
||||
readonly tool:
|
||||
| NativeToolName
|
||||
| ArtifactAnalysisOperation
|
||||
| ManagedToolName
|
||||
| DirectAnalysisTool;
|
||||
readonly arguments: Readonly<Record<string, JsonValue>>;
|
||||
readonly evidenceParameters: Readonly<Record<string, JsonValue>>;
|
||||
readonly signal: AbortSignal;
|
||||
readonly evidenceProfile: AnalysisProfileCommitment | undefined;
|
||||
}): Promise<
|
||||
@@ -573,7 +585,7 @@ const executeAnalysisTool = async (input: {
|
||||
const evidence = createWorkflowEvidence({
|
||||
target: input.openedTarget,
|
||||
operation: tool,
|
||||
parameters: input.arguments,
|
||||
parameters: input.evidenceParameters,
|
||||
result: result.value.result,
|
||||
upstreamProfile: evidenceProfile,
|
||||
upstreamLimitations: result.value.limitations,
|
||||
@@ -621,10 +633,7 @@ const executeAnalysisTool = async (input: {
|
||||
result.value.provider,
|
||||
{
|
||||
operation: tool,
|
||||
parameters:
|
||||
tool === "inspect_artifact"
|
||||
? { ...input.arguments, input_path: input.selectedPath }
|
||||
: input.arguments,
|
||||
parameters: input.evidenceParameters,
|
||||
result: result.value.result,
|
||||
...(result.value.analysisProfile === undefined
|
||||
? {}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { copyFile, mkdir, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { describe, expect } from "vitest";
|
||||
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { inspectPlistSchema } from "../../../src/domain/native/nativeInspection.js";
|
||||
import { appleAssetCatalogResultSchema } from "../../../src/domain/apple/appleAssetCatalog.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
const run = promisify(execFile);
|
||||
const environment = {
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_ANALYSIS_PROVIDER: "auto",
|
||||
} as const;
|
||||
|
||||
const signedApp = async () => {
|
||||
const root = await createTestTempDirectory("rea-selected-app-");
|
||||
const app = join(root, "Probe.app");
|
||||
const executable = join(app, "Contents/MacOS/Probe");
|
||||
await mkdir(join(app, "Contents/MacOS"), { recursive: true });
|
||||
await copyFile("/usr/bin/true", executable);
|
||||
await writeFile(
|
||||
join(app, "Contents/Info.plist"),
|
||||
'<?xml version="1.0" encoding="UTF-8"?><plist version="1.0"><dict><key>CFBundleExecutable</key><string>Probe</string><key>CFBundleIdentifier</key><string>org.example.rea.probe</string><key>CFBundlePackageType</key><string>APPL</string></dict></plist>',
|
||||
);
|
||||
await run("/usr/bin/codesign", ["-s", "-", "-f", app]);
|
||||
await symlink("Probe.app", join(root, "Selected.app"));
|
||||
return { root, app, executable };
|
||||
};
|
||||
|
||||
describe.skipIf(process.platform !== "darwin")(
|
||||
"native app CLI caller selection",
|
||||
() => {
|
||||
cliTest(
|
||||
"retains the selected app alias across signature, plist, and absent asset inspections",
|
||||
async ({ cli }) => {
|
||||
const { root, app, executable } = await signedApp();
|
||||
for (const command of [
|
||||
"inspect-signature",
|
||||
"inspect-plist",
|
||||
"inspect-asset-catalog",
|
||||
]) {
|
||||
const selectedRun = await cli.run({
|
||||
arguments: [command, "Selected.app", "--json"],
|
||||
cwd: root,
|
||||
environment,
|
||||
});
|
||||
const directRun = await cli.run({
|
||||
arguments: [command, "Probe.app", "--json"],
|
||||
cwd: root,
|
||||
environment,
|
||||
});
|
||||
expect(selectedRun.exitCode, selectedRun.stderr).toBe(0);
|
||||
expect(directRun.exitCode, directRun.stderr).toBe(0);
|
||||
const selected = parseEvidence(selectedRun.json);
|
||||
const direct = parseEvidence(directRun.json);
|
||||
expect(selected.parameters).toMatchObject({
|
||||
input_path: "Selected.app",
|
||||
});
|
||||
expect(direct.parameters).toMatchObject({ input_path: "Probe.app" });
|
||||
expect(selected.subject).toEqual(direct.subject);
|
||||
if (command === "inspect-signature") {
|
||||
expect(selected.subject?.local_path).toBe(executable);
|
||||
expect(selected.normalized_result).toEqual(
|
||||
direct.normalized_result,
|
||||
);
|
||||
} else if (command === "inspect-plist") {
|
||||
const aliasResult = inspectPlistSchema.parse(
|
||||
selected.normalized_result,
|
||||
);
|
||||
const directResult = inspectPlistSchema.parse(
|
||||
direct.normalized_result,
|
||||
);
|
||||
expect(aliasResult.resource_root).toMatchObject({
|
||||
caller_path: join(root, "Selected.app"),
|
||||
canonical_path: app,
|
||||
});
|
||||
expect(directResult.resource_root).toMatchObject({
|
||||
caller_path: app,
|
||||
canonical_path: app,
|
||||
});
|
||||
expect({
|
||||
...aliasResult,
|
||||
resource_root: directResult.resource_root,
|
||||
}).toEqual(directResult);
|
||||
} else {
|
||||
const aliasResult = appleAssetCatalogResultSchema.parse(
|
||||
selected.normalized_result,
|
||||
);
|
||||
const directResult = appleAssetCatalogResultSchema.parse(
|
||||
direct.normalized_result,
|
||||
);
|
||||
expect(aliasResult.catalogs).toEqual([]);
|
||||
expect(aliasResult.resource_root).toMatchObject({
|
||||
caller_path: join(root, "Selected.app"),
|
||||
canonical_path: app,
|
||||
});
|
||||
expect(directResult.resource_root).toMatchObject({
|
||||
caller_path: app,
|
||||
canonical_path: app,
|
||||
});
|
||||
expect({
|
||||
...aliasResult,
|
||||
resource_root: directResult.resource_root,
|
||||
}).toEqual(directResult);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user