Skip to content

Commit b710691

Browse files
Cobb04codex
andcommitted
fix(harness): sanitize nested baseline errors
Fixes #104 using docs/specs/2026-08-24-104-deepseek-harness-shared-analysis-evidence-bundle.md AC-11 and AC-12. Serialized Bundle regressions and focused Node 22/24 suites validate nested Baseline stage-error path redaction. Co-authored-by: Codex (GPT 5.6 Sol) <codex@openai.com>
1 parent b381b58 commit b710691

2 files changed

Lines changed: 137 additions & 17 deletions

File tree

scripts/coding-agent-practices/asset-baseline.mjs

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,10 @@ function text(value, limit = 320) {
145145
return String(value ?? "").replace(/[\u0000-\u001f\u007f]/gu, " ").replace(/\s+/gu, " ").trim().slice(0, limit);
146146
}
147147

148-
function compactError(error, stage) {
148+
function compactError(error, stage, context) {
149149
return {
150150
code: `${stage.toUpperCase().replace(/[^A-Z0-9]+/gu, "_")}_UNAVAILABLE`,
151-
message: text(error instanceof Error ? error.message : error),
151+
message: boundedText(error instanceof Error ? error.message : error, context),
152152
};
153153
}
154154

@@ -169,6 +169,7 @@ function pathLocator(filePath, context) {
169169
if (filePath === "<path>" || filePath.startsWith("<workspace>") || filePath.startsWith("<git-root>") || filePath.startsWith("~/")) {
170170
return filePath.split(/[\\/]/u).includes("..") ? "<path>" : filePath;
171171
}
172+
if (process.platform !== "win32" && /^(?:[A-Za-z]:[\\/]|\\\\)/u.test(filePath)) return "<path>";
172173
const absolute = path.resolve(context.workspace, filePath);
173174
const resolved = canonicalIfPresent(absolute);
174175
const workspace = canonicalIfPresent(context.workspace);
@@ -214,6 +215,10 @@ function boundedText(value, context, knownPath) {
214215
const locator = knownPath ? pathLocator(knownPath, context) ?? "<path>" : undefined;
215216
if (knownPath) result = result.replaceAll(String(knownPath), marker);
216217
return result
218+
.replace(
219+
/(["'])((?:[A-Za-z]:[\\/]|\\\\|\/)[^"']+)\1/gu,
220+
(_candidate, quote, candidate) => `${quote}${pathLocator(candidate, context) ?? "<path>"}${quote}`,
221+
)
217222
.replace(/\\\\[^\s,;)'"\]]+\\[^\s,;)'"\]]+/gu, (candidate) => pathLocator(candidate, context) ?? "<path>")
218223
.replace(/[A-Za-z]:[\\/][^\s,;)'"\]]+/gu, (candidate) => pathLocator(candidate, context) ?? "<path>")
219224
.replace(/\/(?:[^\s,;)'"\]]+\/?)+/gu, (candidate) => pathLocator(candidate, context) ?? "<path>")
@@ -400,8 +405,8 @@ function compactIntegrity(integrity, context) {
400405
};
401406
}
402407

403-
function unavailable(error, stage) {
404-
return { status: "unavailable", error: compactError(error, stage) };
408+
function unavailable(error, stage, context) {
409+
return { status: "unavailable", error: compactError(error, stage, context) };
405410
}
406411

407412
function available(data) {
@@ -547,6 +552,14 @@ export async function collectAssetBaseline(options = {}, dependencies = {}) {
547552
includeGlobalHooks: includeUserHome,
548553
includeMemories,
549554
};
555+
const pathContext = {
556+
workspace,
557+
workspaceInput: normalizeWorkspace(options.workspace ?? "."),
558+
gitRoot: topology?.gitRoot,
559+
gitRootInput: options.topology?.gitRoot,
560+
home: dependencies.homeDirectory?.() ?? os.homedir(),
561+
includeUserHome,
562+
};
550563
const collectRawInventory = dependencies.collectRawInventory ?? collectAgentCustomizeInventory;
551564
let rawInventory;
552565
try {
@@ -566,7 +579,7 @@ export async function collectAssetBaseline(options = {}, dependencies = {}) {
566579
rawInventory = mergeInheritedInventories(rawInventory, inheritedInventories, topology);
567580
}
568581
} catch (error) {
569-
const failed = unavailable(error, "inventory");
582+
const failed = unavailable(error, "inventory", pathContext);
570583
return {
571584
kind: ASSET_BASELINE_KIND,
572585
schemaVersion: ASSET_BASELINE_SCHEMA_VERSION,
@@ -580,28 +593,20 @@ export async function collectAssetBaseline(options = {}, dependencies = {}) {
580593
const lintRunner = dependencies.runLint ?? runAgentLint;
581594
const inventoryRunner = dependencies.collectPublicInventory
582595
?? (provider === "qoder" ? collectQoderInventory : collectProviderInventory);
583-
const pathContext = {
584-
workspace,
585-
workspaceInput: normalizeWorkspace(options.workspace ?? "."),
586-
gitRoot: topology?.gitRoot,
587-
gitRootInput: options.topology?.gitRoot,
588-
home: dependencies.homeDirectory?.() ?? os.homedir(),
589-
includeUserHome,
590-
};
591596
const [lintResult, inventoryResult] = await Promise.allSettled([
592597
lintRunner({ ...common, profile: "agent-assets-review", inventory: rawInventory }),
593598
inventoryRunner({ ...common, inventory: rawInventory }),
594599
]);
595600
const lintEnvelope = lintResult.status === "fulfilled"
596601
? available(compactLint(lintResult.value, pathContext))
597-
: unavailable(lintResult.reason, "lint");
602+
: unavailable(lintResult.reason, "lint", pathContext);
598603
const inventoryEnvelope = inventoryResult.status === "fulfilled"
599604
? available(await compactInventory(inventoryResult.value, workspace, {
600605
stat: dependencies.stat,
601606
now: dependencies.now,
602607
pathContext,
603608
}))
604-
: unavailable(inventoryResult.reason, "inventory");
609+
: unavailable(inventoryResult.reason, "inventory", pathContext);
605610
let integrityEnvelope;
606611
if (inventoryResult.status === "fulfilled") {
607612
try {
@@ -611,10 +616,10 @@ export async function collectAssetBaseline(options = {}, dependencies = {}) {
611616
pathContext,
612617
));
613618
} catch (error) {
614-
integrityEnvelope = unavailable(error, "integrity");
619+
integrityEnvelope = unavailable(error, "integrity", pathContext);
615620
}
616621
} else {
617-
integrityEnvelope = unavailable(new Error("The shared public inventory is unavailable."), "integrity");
622+
integrityEnvelope = unavailable(new Error("The shared public inventory is unavailable."), "integrity", pathContext);
618623
}
619624
const envelopes = { lint: lintEnvelope, inventory: inventoryEnvelope, integrity: integrityEnvelope };
620625
const availableCount = Object.values(envelopes).filter((envelope) => envelope.status === "available").length;

test/reporting/better-harness-evidence-bundle.test.mjs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { workspaceToClaudeSlugVariants } from "../../scripts/session-analysis/pl
1818
import { collectAgentCustomize } from "../../scripts/harness-analysis/evidence-bundle/agent-customize.mjs";
1919
import { collectProjectHarness } from "../../scripts/harness-analysis/evidence-bundle/project-harness.mjs";
2020
import { EVIDENCE_BUNDLE_HELP } from "../../scripts/harness-analysis/evidence-bundle/cli.mjs";
21+
import { collectAssetBaseline } from "../../scripts/coding-agent-practices/asset-baseline.mjs";
2122

2223
const NOW = new Date("2026-07-24T08:00:00.000Z");
2324

@@ -525,6 +526,120 @@ test("agentCustomize preserves valid partial and failed Baseline v2 semantics",
525526
assert.equal(failedLane.error.code, "AGENT_CUSTOMIZE_BASELINE_FAILED");
526527
});
527528

529+
function dshRawInventory() {
530+
return {
531+
provider: "dsh",
532+
generatedAt: "2026-07-24T07:00:00.000Z",
533+
diagnostics: {
534+
evidenceKind: "configured-not-observed",
535+
configurationSource: "qualified-defaults",
536+
userHomeCollection: "not-authorized",
537+
instructionCollection: "enabled",
538+
qualifiedDshVersion: "0.1.1-rc.2",
539+
qualifiedDshSourceSha: "b150a551b8d465e31e418e1b2eaf5e79bbb7d28e",
540+
},
541+
};
542+
}
543+
544+
function healthyBaselineStages() {
545+
return {
546+
collectRawInventory: async () => dshRawInventory(),
547+
runLint: async () => ({
548+
kind: "agent-lint",
549+
profile: "agent-assets-review",
550+
summary: {},
551+
findings: [],
552+
}),
553+
collectPublicInventory: async () => ({
554+
scope: { platform: "dsh" },
555+
summary: {},
556+
surfaces: [],
557+
memories: { included: false, categories: [] },
558+
warnings: [],
559+
}),
560+
reviewIntegrity: () => ({
561+
kind: "agent-asset-integrity",
562+
profile: "agent-assets-review",
563+
status: "reviewed",
564+
contentPolicy: "metadata-only",
565+
summary: {},
566+
findings: [],
567+
}),
568+
};
569+
}
570+
571+
const NESTED_BASELINE_ERROR_CASES = [
572+
{
573+
name: "failed raw inventory with a spaced POSIX path",
574+
privatePath: "/Users/example/private project/raw inventory.json",
575+
stages: ["lint", "inventory", "integrity"],
576+
configure: (message) => ({
577+
...healthyBaselineStages(),
578+
collectRawInventory: async () => { throw new Error(message); },
579+
}),
580+
},
581+
{
582+
name: "partial lint with a Windows drive path",
583+
privatePath: "C:\\Users\\example\\private folder\\lint.json",
584+
stages: ["lint"],
585+
configure: (message) => ({
586+
...healthyBaselineStages(),
587+
runLint: async () => { throw new Error(message); },
588+
}),
589+
},
590+
{
591+
name: "partial inventory with a spaced POSIX path",
592+
privatePath: "/Users/example/private project/inventory.json",
593+
stages: ["inventory"],
594+
configure: (message) => ({
595+
...healthyBaselineStages(),
596+
collectPublicInventory: async () => { throw new Error(message); },
597+
}),
598+
},
599+
{
600+
name: "partial integrity with a UNC path",
601+
privatePath: "\\\\server\\private share\\integrity.json",
602+
stages: ["integrity"],
603+
configure: (message) => ({
604+
...healthyBaselineStages(),
605+
reviewIntegrity: () => { throw new Error(message); },
606+
}),
607+
},
608+
];
609+
610+
for (const current of NESTED_BASELINE_ERROR_CASES) {
611+
test(`serialized Bundle sanitizes ${current.name}`, async () => {
612+
const safeContext = "stage context remains available";
613+
const message = `collector could not inspect '${current.privatePath}' while ${safeContext}`;
614+
const result = await collectEvidenceBundle({
615+
workspace: ".",
616+
cwd: ".",
617+
platform: "dsh",
618+
depth: "quick",
619+
}, dependencies({
620+
collectAgentCustomize: undefined,
621+
collectAssetBaseline: (options) => collectAssetBaseline(options, current.configure(message)),
622+
}));
623+
const serialized = JSON.stringify(result);
624+
625+
assert.equal(serialized.includes(current.privatePath), false);
626+
assert.match(serialized, /<path>/u);
627+
assert.match(serialized, /stage context remains available/u);
628+
for (const stage of current.stages) {
629+
assert.equal(result.lanes.agentCustomize.data.envelopes[stage].status, "unavailable");
630+
assert.match(result.lanes.agentCustomize.data.envelopes[stage].error.code, /_UNAVAILABLE$/u);
631+
}
632+
assert.doesNotMatch(
633+
serialized,
634+
/PRIVATE_SKILL_SECRET_X|PRIVATE_INSTRUCTION_SECRET_Y|sk-test-secret-credential|configuredDigest|symlinkTargetRealpath/u,
635+
);
636+
assert.doesNotMatch(
637+
serialized,
638+
/existedAtSessionTime|usedInSession|influencedSession|sameHistoricalAsset|historicalAbsence/u,
639+
);
640+
});
641+
}
642+
528643
test("lead receives configured cwd while Project Harness remains on its generic Git scope", async () => {
529644
const canonicalCwd = await realpath(".");
530645
let leadOptions;

0 commit comments

Comments
 (0)