diff --git a/CHANGELOG.md b/CHANGELOG.md index 45fb73d..af2d8ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/advanced.js b/src/advanced.js index 2ceefd2..1c0524c 100644 --- a/src/advanced.js +++ b/src/advanced.js @@ -170,6 +170,7 @@ export { classifyCompatRecordCoverage, renderMarkdownReport, renderTextSummary, + sanitizeReportArtifact, writeCompatibilityReport, writeReport, } from "./report.js"; diff --git a/src/cli.js b/src/cli.js index 4278573..c4b502e 100755 --- a/src/cli.js +++ b/src/cli.js @@ -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 })); } diff --git a/src/compatibility-report.js b/src/compatibility-report.js index 4d333e8..5c21f00 100644 --- a/src/compatibility-report.js +++ b/src/compatibility-report.js @@ -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"}`, "", diff --git a/src/index.js b/src/index.js index 5f22eab..5b0e5ef 100644 --- a/src/index.js +++ b/src/index.js @@ -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, diff --git a/src/issues.js b/src/issues.js index 101d8a4..d915a58 100644 --- a/src/issues.js +++ b/src/issues.js @@ -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", diff --git a/src/report-sanitizer.js b/src/report-sanitizer.js new file mode 100644 index 0000000..28c9a97 --- /dev/null +++ b/src/report-sanitizer.js @@ -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 ?? ""; + 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)]; +} diff --git a/src/report.js b/src/report.js index c12127c..2374c9b 100644 --- a/src/report.js +++ b/src/report.js @@ -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, diff --git a/test/api.test.js b/test/api.test.js index 527436f..834c5ad 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -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); diff --git a/test/cli.test.js b/test/cli.test.js index cac7c60..77a9012 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -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, ""); + assert.equal(artifact.targetOpenClaw.configuredPath, ""); + assert.deepEqual(artifact.targetOpenClaw.searchedPaths, [""]); + 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, "\\$&"); +} diff --git a/test/report.test.js b/test/report.test.js index 4ce907d..edd9836 100644 --- a/test/report.test.js +++ b/test/report.test.js @@ -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, ""); + assert.deepEqual(artifact.targetOpenClaw.searchedPaths, [""]); + assert.equal(artifact.issues[0].evidence[0], ""); + assert.equal(artifact.issues[0].title, "sample-plugin: path "); + assert.doesNotMatch(markdown, new RegExp(escapeRegExp(absoluteOpenClawPath))); + assert.doesNotMatch(issues, new RegExp(escapeRegExp(absoluteOpenClawPath))); + assert.match(markdown, //); + assert.match(await readFile(paths.markdownPath, "utf8"), //); + assert.match(await readFile(paths.issuesPath, "utf8"), //); +}); + 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, "\\$&"); +}