fix: preserve skill markdown in security dataset

Preserve multiline SKILL.md content in security dataset exports and include the primary readme as a checksum-bearing bundle entry.
This commit is contained in:
Patrick Erichsen
2026-07-08 15:29:07 -07:00
committed by GitHub
parent 0e898b1dfd
commit 1cc673a9e5
6 changed files with 50 additions and 11 deletions
@@ -199,6 +199,10 @@ describe("Convex export dataset ingestion", () => {
},
skillMdContentRedacted: "Use this skill safely. [REDACTED_SECRET]",
bundleFilesRedacted: [
{
path: "SKILL.md",
content: "Use this skill safely. [REDACTED_SECRET]",
},
{
path: "scripts/export.py",
content: "import json\n[REDACTED_SECRET]\n",
@@ -306,6 +310,10 @@ describe("Convex export dataset ingestion", () => {
path: "docs/skills.md",
content: "Nested authored plural readme",
},
{
path: "skills.md",
content: "Primary readme [REDACTED_SECRET]",
},
],
});
});
+1 -3
View File
@@ -330,9 +330,7 @@ function bundleFilesFromExport(value: unknown) {
}
function isExcludedSkillBundlePath(path: string) {
return (
isPrimarySkillReadmePath(path) || normalizeBundlePathForComparison(path) === "skill-card.md"
);
return normalizeBundlePathForComparison(path) === "skill-card.md";
}
function isPrimarySkillReadmePath(path: string) {
@@ -6,6 +6,7 @@ import { join } from "node:path";
import { promisify } from "node:util";
import { strToU8, zipSync } from "fflate";
import { describe, expect, it } from "vitest";
import { hashString } from "./normalize";
const execFileAsync = promisify(execFile);
@@ -67,14 +68,21 @@ describe("security dataset snapshot CLI", () => {
.split(/\r?\n/)
.filter((line) => line.length > 0)
.map((line) => JSON.parse(line) as Record<string, unknown>);
const expectedSkillMdContent = "# Stored Skill\n\nUse [REDACTED_SECRET]\nKeep this line.";
expect(flatRows).toEqual([
expect.objectContaining({
id: "a".repeat(64),
skill_slug: "owner/stored-skill",
skill_version: "1.0.0",
skill_md_content: "Stored SKILL.md [REDACTED_SECRET]",
skill_md_content: expectedSkillMdContent,
skill_bundle_content: [
{
path: "SKILL.md",
content: expectedSkillMdContent,
sha256: hashString(expectedSkillMdContent),
sizeBytes: Buffer.byteLength(expectedSkillMdContent, "utf8"),
},
expect.objectContaining({
path: "scripts/run.sh",
content: "echo [REDACTED_SECRET]\n",
@@ -82,6 +90,11 @@ describe("security dataset snapshot CLI", () => {
],
}),
]);
expect((flatRows[0]?.skill_md_content as string).split("\n")).toHaveLength(4);
const skillBundleContent = flatRows[0]?.skill_bundle_content as Array<{
path: string;
}>;
expect(skillBundleContent.filter((file) => file.path === "SKILL.md")).toHaveLength(1);
const serializedRows = JSON.stringify(flatRows);
expect(serializedRows).not.toContain("storageId");
expect(serializedRows).not.toContain("skillVersions:1");
@@ -154,6 +167,8 @@ describe("security dataset snapshot CLI", () => {
});
function buildTinyConvexSnapshotZip() {
const skillMdContent = "# Stored Skill\n\nUse token=supersecret123\nKeep this line.";
const scriptContent = "echo password=scriptsecret123\n";
return zipSync({
"skills/documents.jsonl": strToU8(
`${JSON.stringify({
@@ -173,16 +188,16 @@ function buildTinyConvexSnapshotZip() {
files: [
{
path: "SKILL.md",
size: 31,
size: Buffer.byteLength(skillMdContent, "utf8"),
sha256: "skill-md-sha",
content: "Stored SKILL.md token=supersecret123",
content: skillMdContent,
contentType: "text/markdown",
},
{
path: "scripts/run.sh",
size: 35,
size: Buffer.byteLength(scriptContent, "utf8"),
sha256: "script-sha",
content: "echo password=scriptsecret123\n",
content: scriptContent,
contentType: "text/x-shellscript",
},
],
@@ -90,6 +90,19 @@ describe("security dataset snapshot merge CLI", () => {
await expect(
readFile(join(summary.snapshotDir, "hf-dataset", "data", "latest.jsonl"), "utf8"),
).resolves.not.toContain('"split":"train"');
const latestContents = await readFile(
join(summary.snapshotDir, "hf-dataset", "data", "latest.jsonl"),
"utf8",
);
const latestRows = latestContents
.split(/\r?\n/)
.filter((line) => line.length > 0)
.map((line) => JSON.parse(line) as Record<string, unknown>);
const mergedSkillContent = latestRows.find((row) => row.id === "row-a")
?.skill_md_content as string;
expect(mergedSkillContent).toBe(
"Use this skill.\nskill [REDACTED_SECRET]\nKeep this line intact.",
);
} finally {
await rm(directory, { recursive: true, force: true });
}
@@ -126,7 +139,8 @@ async function writeShardSnapshot(
id: index === 0 ? input.rowId : `${input.rowId}-${index}`,
split,
skill_slug: "owner/sk-abcdefghijklmnopqrstuvwxyz",
skill_md_content: "Use this skill. skill secret: supersecretvalue123",
skill_md_content:
"Use this skill.\nskill secret: supersecretvalue123\nKeep this line intact.",
skill_bundle_content: [
{
path: "config/sk-abcdefghijklmnopqrstuvwxyz/settings.json",
+2 -1
View File
@@ -154,7 +154,7 @@ describe("security dataset normalizer", () => {
public_slug: "suspicious-demo",
public_qualified_slug: "openclaw/suspicious-demo",
skill_md_content_redacted:
"# Suspicious Demo Use this skill to inspect shell scripts. Contact [REDACTED_SECRET] with [REDACTED_SECRET]",
"# Suspicious Demo\nUse this skill to inspect shell scripts.\nContact [REDACTED_SECRET] with [REDACTED_SECRET]",
created_month: "2026-04",
file_count: 2,
total_bytes: 300,
@@ -239,6 +239,7 @@ describe("security dataset normalizer", () => {
]);
expect(rows.artifacts[0]?.skill_md_content_redacted).toContain("Run static analysis");
expect(rows.artifacts[0]?.skill_md_content_redacted).toContain("\n");
expect(rows.artifacts[0]?.skill_md_content_redacted).toContain("[REDACTED_SECRET]");
expect(rows.artifacts[0]?.skill_md_content_redacted).not.toContain(
"abcdefghijklmnopqrstuvwxyz123456",
+4 -1
View File
@@ -307,7 +307,10 @@ export function hasSecretLikeValue(value: string) {
}
export function redactSkillContent(value: string | null | undefined) {
return redactText(value, MAX_REDACTED_SKILL_CONTENT_LENGTH);
if (!value) return null;
const redacted = redactBundleContent(value);
if (redacted.length <= MAX_REDACTED_SKILL_CONTENT_LENGTH) return redacted;
return `${redacted.slice(0, MAX_REDACTED_SKILL_CONTENT_LENGTH - 1)}...`;
}
export function normalizeArtifactExport(inputs: ArtifactExportInput[]): NormalizedDatasetRows {