fix(reports): sanitize OpenClaw target paths

This commit is contained in:
Vincent Koc 2026-04-28 18:30:54 -07:00
parent e9e4b6704c
commit 332706b014
No known key found for this signature in database
11 changed files with 207 additions and 9 deletions

View File

@ -2,6 +2,11 @@
## Unreleased
### Fixed
- Sanitize absolute target OpenClaw paths from generated report artifacts and JSON CLI output.
- Normalize the dependency-install inspector finding title to use isolated-workspace wording.
## 0.3.3 - 2026-04-28
### Fixed

View File

@ -170,6 +170,7 @@ export {
classifyCompatRecordCoverage,
renderMarkdownReport,
renderTextSummary,
sanitizeReportArtifact,
writeCompatibilityReport,
writeReport,
} from "./report.js";

View File

@ -3,6 +3,7 @@ import path from "node:path";
import {
loadPluginConfig,
renderTextSummary,
sanitizeReportArtifact,
runPluginCheck,
} from "./index.js";
import {
@ -90,7 +91,7 @@ async function runCheck(commandArgs) {
});
if (json) {
console.log(JSON.stringify(report, null, 2));
console.log(JSON.stringify(sanitizeReportArtifact(report), null, 2));
} else {
console.log(renderTextSummary(report, { artifacts: paths }));
}

View File

@ -1,4 +1,5 @@
import { renderPaddedMarkdownTable } from "./artifacts.js";
import { sanitizeReportArtifact } from "./report-sanitizer.js";
const defaultSeverityLabels = {
P0: "P0",
@ -8,6 +9,7 @@ const defaultSeverityLabels = {
};
export function renderCompatibilityMarkdownReport(report, options = {}) {
report = sanitizeReportArtifact(report, options);
return [
`# ${options.title ?? "OpenClaw Plugin Compatibility Report"}`,
"",
@ -127,6 +129,7 @@ export function renderCompatibilityMarkdownReport(report, options = {}) {
}
export function renderCompatibilityIssuesReport(report, options = {}) {
report = sanitizeReportArtifact(report, options);
return [
`# ${options.title ?? "OpenClaw Plugin Issue Findings"}`,
"",

View File

@ -53,11 +53,13 @@ export const staticInspection = Object.freeze({
export const reports = Object.freeze({
renderMarkdown: reportApi.renderMarkdownReport,
renderTextSummary: pluginApi.renderTextSummary,
sanitizeArtifact: reportApi.sanitizeReportArtifact,
write: reportApi.writeReport,
issueId: issuesApi.issueId,
classifyIssueFinding: issuesApi.classifyIssueFinding,
knownIssueCodes: issuesApi.knownIssueCodes,
openClawTargetPathCandidates: openClawTargetApi.openClawTargetPathCandidates,
readOpenClawTargetSurface: openClawTargetApi.readOpenClawTargetSurface,
});
export const contracts = Object.freeze({
@ -200,7 +202,7 @@ export {
} from "./import-loop-profile.js";
export { classifyIssueFinding, issueId, knownIssueCodes } from "./issues.js";
export { inspectFixtureSet, inspectPlugin, inspectSourceText } from "./inspector.js";
export { openClawTargetPathCandidates } from "./openclaw-target.js";
export { openClawTargetPathCandidates, readOpenClawTargetSurface } from "./openclaw-target.js";
export {
buildProfileDiff,
defaultProfileDiffOptions,
@ -216,7 +218,7 @@ export {
validateRefDiff,
writeRefDiff,
} from "./ref-diff.js";
export { renderMarkdownReport, writeReport } from "./report.js";
export { renderMarkdownReport, sanitizeReportArtifact, writeReport } from "./report.js";
export {
buildRuntimeProfile,
defaultRuntimeProfileCommands,

View File

@ -119,7 +119,7 @@ export const issueMetadataByCode = {
severity: "P2",
owner: "inspector",
decision: "inspector-follow-up",
title: "cold import requires isolated dependency installation",
title: "cold import requires dependency installation in an isolated workspace",
},
"package-entrypoint-missing": {
severity: "P1",

42
src/report-sanitizer.js Normal file
View File

@ -0,0 +1,42 @@
import path from "node:path";
export function sanitizeReportArtifact(report, options = {}) {
const sensitivePaths = sensitiveOpenClawPaths(report);
if (sensitivePaths.length === 0) {
return report;
}
const placeholder = options.openclawPathPlaceholder ?? "<OPENCLAW_PATH>";
return sanitizeValue(report, sensitivePaths, placeholder);
}
function sensitiveOpenClawPaths(report) {
const targetOpenClaw = report?.targetOpenClaw;
return unique(
[targetOpenClaw?.configuredPath, ...(targetOpenClaw?.searchedPaths ?? [])]
.filter((value) => typeof value === "string" && isAbsolutePath(value))
.sort((left, right) => right.length - left.length),
);
}
function sanitizeValue(value, sensitivePaths, placeholder) {
if (typeof value === "string") {
return sensitivePaths.reduce((result, sensitivePath) => result.replaceAll(sensitivePath, placeholder), value);
}
if (Array.isArray(value)) {
return value.map((item) => sanitizeValue(item, sensitivePaths, placeholder));
}
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value).map(([key, entryValue]) => [key, sanitizeValue(entryValue, sensitivePaths, placeholder)]),
);
}
return value;
}
function isAbsolutePath(value) {
return path.isAbsolute(value) || /^[A-Za-z]:[\\/]/u.test(value) || value.startsWith("\\\\");
}
function unique(values) {
return [...new Set(values)];
}

View File

@ -4,6 +4,7 @@ import { renderCompatibilityIssuesReport, renderCompatibilityMarkdownReport } fr
import { buildContractProbes } from "./contract-probes.js";
import { classifyCompatibilityFixture } from "./fixture-summary.js";
import { buildIssues, summarizeIssueClasses } from "./issues.js";
import { sanitizeReportArtifact } from "./report-sanitizer.js";
export function buildReport({ config, inspections, failures = [], generatedAt = "deterministic" }) {
const inspectionById = new Map(inspections.map((inspection) => [inspection.id, inspection]));
@ -233,12 +234,13 @@ export async function writeReport(report, options = {}) {
const basename = options.basename ?? "plugin-inspector-report";
const jsonPath = path.join(outDir, `${basename}.json`);
const markdownPath = path.join(outDir, `${basename}.md`);
const artifactReport = sanitizeReportArtifact(report, options);
return writeJsonMarkdownArtifacts({
jsonPath,
markdownPath,
json: report,
markdown: renderMarkdownReport(report),
json: artifactReport,
markdown: renderMarkdownReport(artifactReport),
check: options.check,
});
}
@ -249,6 +251,7 @@ export async function writeCompatibilityReport(report, options = {}) {
const jsonPath = options.jsonPath ?? path.join(outDir, `${basename}.json`);
const markdownPath = options.markdownPath ?? path.join(outDir, `${basename}.md`);
const issuesPath = options.issuesPath ?? path.join(outDir, options.issuesBasename ?? "plugin-inspector-issues.md");
const artifactReport = sanitizeReportArtifact(report, options);
const markdownOptions = compatibilityRenderOptions(options, {
title: options.markdownTitle ?? options.title,
...options.markdownOptions,
@ -260,14 +263,16 @@ export async function writeCompatibilityReport(report, options = {}) {
return writeArtifacts(
[
{ name: "jsonPath", path: jsonPath, json: report },
{ name: "markdownPath", path: markdownPath, markdown: renderCompatibilityMarkdownReport(report, markdownOptions) },
{ name: "issuesPath", path: issuesPath, markdown: renderCompatibilityIssuesReport(report, issuesOptions) },
{ name: "jsonPath", path: jsonPath, json: artifactReport },
{ name: "markdownPath", path: markdownPath, markdown: renderCompatibilityMarkdownReport(artifactReport, markdownOptions) },
{ name: "issuesPath", path: issuesPath, markdown: renderCompatibilityIssuesReport(artifactReport, issuesOptions) },
],
{ check: options.check },
);
}
export { sanitizeReportArtifact };
function compatibilityRenderOptions(options, overrides) {
const renderOptions = {
formatEvidence: options.formatEvidence,

View File

@ -111,6 +111,8 @@ test("public API exposes grouped facades for common workflows", () => {
assert.equal(fixtureSuites.runReport, runFixtureSetReport);
assert.equal(staticInspection.inspectSourceText, inspectSourceText);
assert.equal(reports.renderMarkdown, renderMarkdownReport);
assert.equal(typeof reports.sanitizeArtifact, "function");
assert.equal(typeof reports.readOpenClawTargetSurface, "function");
assert.equal(contracts.buildCapture, buildContractCapture);
assert.equal(contracts.validateCoverage, validateContractCoverage);
assert.equal(ci.buildSummary, buildCiSummary);

View File

@ -87,6 +87,25 @@ test("check command can target a plugin root and use runtime aliases", async ()
assert.equal(capture.summary.capturedCount, 1);
});
test("check command sanitizes absolute OpenClaw paths in JSON output and artifacts", async () => {
const rootDir = await createCliPluginRoot("plugin-inspector-cli-sanitize-");
const openclawPath = await createTargetOpenClaw(rootDir);
const cliPath = path.resolve("src/cli.js");
const { stdout } = await execFileAsync(
process.execPath,
[cliPath, "check", "--out", "reports", "--openclaw", openclawPath, "--json"],
{ cwd: rootDir },
);
const output = JSON.parse(stdout);
const artifact = JSON.parse(await readFile(path.join(rootDir, "reports", "plugin-inspector-report.json"), "utf8"));
assert.equal(output.targetOpenClaw.configuredPath, "<OPENCLAW_PATH>");
assert.equal(artifact.targetOpenClaw.configuredPath, "<OPENCLAW_PATH>");
assert.deepEqual(artifact.targetOpenClaw.searchedPaths, ["<OPENCLAW_PATH>"]);
assert.doesNotMatch(stdout, new RegExp(escapeRegExp(openclawPath)));
});
test("inspect command runs from a plugin root and can write CI outputs", async () => {
const rootDir = await createCliPluginRoot("plugin-inspector-cli-inspect-");
const cliPath = path.resolve("src/cli.js");
@ -318,3 +337,18 @@ async function createCliPluginRoot(prefix) {
);
return rootDir;
}
async function createTargetOpenClaw(rootDir) {
const openclawPath = path.join(rootDir, "target-openclaw");
await mkdir(path.join(openclawPath, "src/plugins/compat"), { recursive: true });
await writeFile(
path.join(openclawPath, "src/plugins/compat/registry.ts"),
'export const records = [{ code: "legacy-root-sdk-import", status: "deprecated" }];\n',
"utf8",
);
return openclawPath;
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

View File

@ -7,6 +7,7 @@ import {
buildSarifReport,
buildCompatibilityReport,
buildCompatibilityFixtureReport,
buildIssues,
classifyCompatibilityFixture,
classifyCompatRecordCoverage,
classifyPackageContracts,
@ -20,6 +21,7 @@ import {
renderMarkdownTable,
renderTextSummary,
writeArtifacts,
writeCompatibilityReport,
writeCiOutputArtifacts,
writeReport,
} from "../src/advanced.js";
@ -185,6 +187,91 @@ test("compatibility report renderer supports issue metadata and evidence links",
assert.match(issues, /\[linked\]\(plugins\/sample\/src\/index\.ts:1\)/);
});
test("compatibility report artifacts sanitize absolute OpenClaw target paths", async () => {
const outDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-sanitized-report-"));
const absoluteOpenClawPath = path.join(outDir, "openclaw");
const report = {
generatedAt: "test",
status: "pass",
targetOpenClaw: {
status: "ok",
configuredPath: absoluteOpenClawPath,
searchedPaths: [absoluteOpenClawPath],
compatRecords: [],
compatRecordStatuses: {},
},
summary: {
fixtureCount: 1,
highPriorityFixtures: 1,
breakageCount: 0,
warningCount: 0,
suggestionCount: 0,
decisionCount: 0,
issueCount: 1,
p0IssueCount: 0,
p1IssueCount: 0,
liveIssueCount: 0,
liveP0IssueCount: 0,
compatGapCount: 0,
deprecationWarningCount: 0,
inspectorGapCount: 1,
upstreamIssueCount: 0,
fixtureRegressionCount: 0,
contractProbeCount: 0,
},
fixtures: [
{
id: "sample-plugin",
priority: "high",
seams: ["native-tool"],
hooks: [],
registrations: [],
manifestContracts: [],
},
],
breakages: [],
warnings: [],
suggestions: [],
issues: [
{
fixture: "sample-plugin",
code: "package-dependency-install-required",
issueClass: "inspector-gap",
decision: "inspector-follow-up",
severity: "P2",
title: `sample-plugin: path ${absoluteOpenClawPath}`,
status: "open",
compatStatus: "none",
live: false,
evidence: [absoluteOpenClawPath],
},
],
contractProbes: [],
logs: [],
decisions: [],
};
const markdown = renderCompatibilityMarkdownReport(report);
const issues = renderCompatibilityIssuesReport(report);
const paths = await writeCompatibilityReport(report, {
jsonPath: path.join(outDir, "report.json"),
markdownPath: path.join(outDir, "report.md"),
issuesPath: path.join(outDir, "issues.md"),
});
const artifact = JSON.parse(await readFile(paths.jsonPath, "utf8"));
assert.equal(report.targetOpenClaw.configuredPath, absoluteOpenClawPath);
assert.equal(artifact.targetOpenClaw.configuredPath, "<OPENCLAW_PATH>");
assert.deepEqual(artifact.targetOpenClaw.searchedPaths, ["<OPENCLAW_PATH>"]);
assert.equal(artifact.issues[0].evidence[0], "<OPENCLAW_PATH>");
assert.equal(artifact.issues[0].title, "sample-plugin: path <OPENCLAW_PATH>");
assert.doesNotMatch(markdown, new RegExp(escapeRegExp(absoluteOpenClawPath)));
assert.doesNotMatch(issues, new RegExp(escapeRegExp(absoluteOpenClawPath)));
assert.match(markdown, /<OPENCLAW_PATH>/);
assert.match(await readFile(paths.markdownPath, "utf8"), /<OPENCLAW_PATH>/);
assert.match(await readFile(paths.issuesPath, "utf8"), /<OPENCLAW_PATH>/);
});
test("compatibility report assembly classifies fixtures, issues, probes, and compat records", async () => {
const report = await buildCompatibilityReport({
generatedAt: "test",
@ -401,6 +488,18 @@ test("package contract classifier reports install and entrypoint blockers", () =
assert.ok(result.suggestions.some((finding) => finding.code === "package-build-artifact-entrypoint"));
assert.ok(result.suggestions.some((finding) => finding.code === "package-dependency-install-required"));
assert.ok(result.decisions.some((decision) => decision.seam === "cold-import"));
const issues = buildIssues({
suggestions: result.suggestions,
targetOpenClaw: { status: "ok", compatRecordStatuses: {} },
});
assert.ok(
issues.some(
(issue) =>
issue.code === "package-dependency-install-required" &&
issue.title === "fixture: cold import requires dependency installation in an isolated workspace",
),
);
});
test("target OpenClaw coverage classifier reports missing public surface", () => {
@ -585,3 +684,7 @@ test("markdown table helper supports padded empty-table reports", () => {
);
assert.equal(renderMarkdownTable([], ["Name"], { empty: "_none_" }), "_none_");
});
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}